Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions frontend/pluto_duck_frontend/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,21 @@
}
}

@keyframes stepIn {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.animate-step-in {
animation: stepIn 0.4s cubic-bezier(0.23, 1, 0.32, 1) forwards;
}

.animate-text-glow {
animation: textGlow 2.2s ease-in-out infinite;
}
53 changes: 36 additions & 17 deletions frontend/pluto_duck_frontend/components/ai-elements/code-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
type HTMLAttributes,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki";
Expand Down Expand Up @@ -50,6 +49,19 @@ const lineNumberTransformer: ShikiTransformer = {
},
};

function escapeHtml(raw: string): string {
return raw
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}

function createPlainCodeHtml(code: string): string {
return `<pre class="shiki"><code>${escapeHtml(code)}</code></pre>`;
}

export async function highlightCode(
code: string,
language: BundledLanguage,
Expand All @@ -59,18 +71,25 @@ export async function highlightCode(
? [lineNumberTransformer]
: [];

return await Promise.all([
codeToHtml(code, {
lang: language,
theme: "one-light",
transformers,
}),
codeToHtml(code, {
lang: language,
theme: "one-dark-pro",
transformers,
}),
]);
try {
return await Promise.all([
codeToHtml(code, {
lang: language,
theme: "one-light",
transformers,
}),
codeToHtml(code, {
lang: language,
theme: "one-dark-pro",
transformers,
}),
]);
} catch (error) {
// Keep chat rendering resilient when Shiki theme chunks fail to load in runtime.
console.error("Failed to highlight code block with Shiki:", error);
const fallbackHtml = createPlainCodeHtml(code);
return [fallbackHtml, fallbackHtml];
}
}

export const CodeBlock = ({
Expand All @@ -83,19 +102,19 @@ export const CodeBlock = ({
}: CodeBlockProps) => {
const [html, setHtml] = useState<string>("");
const [darkHtml, setDarkHtml] = useState<string>("");
const mounted = useRef(false);

useEffect(() => {
let cancelled = false;

highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
if (!mounted.current) {
if (!cancelled) {
setHtml(light);
setDarkHtml(dark);
mounted.current = true;
}
});

return () => {
mounted.current = false;
cancelled = true;
};
}, [code, language, showLineNumbers]);

Expand Down
7 changes: 4 additions & 3 deletions frontend/pluto_duck_frontend/components/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
PromptInputModelSelectItem,
type PromptInputMessage,
} from '../ai-elements/prompt-input';
import { ActivityLoader } from '../ai-elements/activity-loader';
import { LoadingDots } from '../ai-elements/loading-dots';
import { MentionMenu } from './MentionMenu';
import { ChatOnboarding } from './ChatOnboarding';
import { RenderItem, type FeedbackType } from './renderers';
Expand Down Expand Up @@ -282,8 +282,9 @@ const ConversationMessages = memo(function ConversationMessages({
})}

{chatLoadingMode === 'agent-streaming-fallback' && (
<div className="px-2.5 py-2.5">
<ActivityLoader />
<div aria-live="polite" aria-atomic="true">
<span className="sr-only">에이전트가 응답을 생성하고 있습니다</span>
<LoadingDots className="animate-step-in" />
</div>
)}
</>
Expand Down