diff --git a/site/app/jobs/[jobName]/[trialName]/trajectory/components/ansi-log.tsx b/site/app/jobs/[jobName]/[trialName]/trajectory/components/ansi-log.tsx new file mode 100644 index 0000000000..0150327331 --- /dev/null +++ b/site/app/jobs/[jobName]/[trialName]/trajectory/components/ansi-log.tsx @@ -0,0 +1,99 @@ +import { useMemo } from "react"; + +interface Style { + color?: string; + bold?: boolean; +} + +function parseAnsi(text: string): { text: string; style: Style }[] { + const ansiRegex = /[\u001b\x1b]\[([0-9;]*)m/g; + const segments: { text: string; style: Style }[] = []; + + let match; + let lastIndex = 0; + let currentStyle: Style = {}; + + const codeToStyle = (code: number, style: Style): Style => { + const newStyle = { ...style }; + if (code === 0) { + return {}; + } + if (code === 1) { + newStyle.bold = true; + } else if (code === 22) { + newStyle.bold = false; + } else if (code >= 30 && code <= 37) { + const colors = [ + "text-black dark:text-white", // 30 + "text-red-500 dark:text-red-400", // 31 + "text-emerald-500 dark:text-emerald-400", // 32 + "text-amber-500 dark:text-amber-400", // 33 + "text-blue-500 dark:text-blue-400", // 34 + "text-magenta-500 dark:text-magenta-400", // 35 + "text-cyan-500 dark:text-cyan-400", // 36 + "text-gray-500 dark:text-gray-400", // 37 + ]; + newStyle.color = colors[code - 30]; + } else if (code === 39) { + delete newStyle.color; + } else if (code >= 90 && code <= 97) { + const brightColors = [ + "text-gray-400 dark:text-gray-500", // 90 + "text-red-400 dark:text-red-300", // 91 + "text-emerald-400 dark:text-emerald-300", // 92 + "text-amber-400 dark:text-amber-300", // 93 + "text-blue-400 dark:text-blue-300", // 94 + "text-magenta-400 dark:text-magenta-300", // 95 + "text-cyan-400 dark:text-cyan-300", // 96 + "text-white dark:text-black", // 97 + ]; + newStyle.color = brightColors[code - 90]; + } + return newStyle; + }; + + while ((match = ansiRegex.exec(text)) !== null) { + const textSegment = text.slice(lastIndex, match.index); + if (textSegment) { + segments.push({ text: textSegment, style: { ...currentStyle } }); + } + + const codesStr = match[1]; + const codes = codesStr ? codesStr.split(";").map(Number) : [0]; + for (const code of codes) { + currentStyle = codeToStyle(code, currentStyle); + } + + lastIndex = ansiRegex.lastIndex; + } + + const remainingText = text.slice(lastIndex); + if (remainingText) { + segments.push({ text: remainingText, style: { ...currentStyle } }); + } + + return segments; +} + +export function AnsiLog({ text }: { text: string }) { + const segments = useMemo(() => parseAnsi(text), [text]); + + return ( +
+      {segments.map((seg, i) => {
+        const classes = [];
+        if (seg.style.bold) classes.push("font-bold");
+        if (seg.style.color) classes.push(seg.style.color);
+        
+        if (classes.length > 0) {
+          return (
+            
+              {seg.text}
+            
+          );
+        }
+        return seg.text;
+      })}
+    
+ ); +} diff --git a/site/app/jobs/[jobName]/[trialName]/trajectory/components/trajectory-page.tsx b/site/app/jobs/[jobName]/[trialName]/trajectory/components/trajectory-page.tsx index 0fab8fc83c..7cf4053f09 100644 --- a/site/app/jobs/[jobName]/[trialName]/trajectory/components/trajectory-page.tsx +++ b/site/app/jobs/[jobName]/[trialName]/trajectory/components/trajectory-page.tsx @@ -10,6 +10,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; import { HttpError } from "@/lib/http-error"; import { ArtifactsPanel, type ArtifactNodeWithUrl } from "./artifacts-panel"; +import { AnsiLog } from "./ansi-log"; export type TabConfig = { value: string; @@ -183,11 +184,7 @@ export function TrajectoryPage({ return

{emptyMessage}

; } - return ( -
-        {text}
-      
- ); + return ; }; return (