diff --git a/frontend/pluto_duck_frontend/app/globals.css b/frontend/pluto_duck_frontend/app/globals.css index 7bbb49f..476c4a1 100644 --- a/frontend/pluto_duck_frontend/app/globals.css +++ b/frontend/pluto_duck_frontend/app/globals.css @@ -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; } diff --git a/frontend/pluto_duck_frontend/components/ai-elements/code-block.tsx b/frontend/pluto_duck_frontend/components/ai-elements/code-block.tsx index 90cf7c3..a44ef9f 100644 --- a/frontend/pluto_duck_frontend/components/ai-elements/code-block.tsx +++ b/frontend/pluto_duck_frontend/components/ai-elements/code-block.tsx @@ -10,7 +10,6 @@ import { type HTMLAttributes, useContext, useEffect, - useRef, useState, } from "react"; import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki"; @@ -50,6 +49,19 @@ const lineNumberTransformer: ShikiTransformer = { }, }; +function escapeHtml(raw: string): string { + return raw + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function createPlainCodeHtml(code: string): string { + return `
${escapeHtml(code)}`;
+}
+
export async function highlightCode(
code: string,
language: BundledLanguage,
@@ -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 = ({
@@ -83,19 +102,19 @@ export const CodeBlock = ({
}: CodeBlockProps) => {
const [html, setHtml] = useState