Skip to content

Commit 898dc13

Browse files
committed
feat(web-evals): add task log viewing, export failed logs, and new run options
- Add task log viewing dialog with syntax highlighting and copy to clipboard - Add export failed logs functionality (downloads zip file) - Add 'Use Multiple Native Tool Calls' option for all providers - Add reasoning effort dropdown for Roo Code Cloud provider - Improve job token field with tooltip and validation - Mount log files in docker-compose for web access - Add archiver dependency for zip exports
1 parent fb9c57e commit 898dc13

File tree

5 files changed

+517
-31
lines changed

5 files changed

+517
-31
lines changed

apps/web-evals/src/app/runs/[id]/run.tsx

Lines changed: 184 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"use client"
22

3-
import { useMemo } from "react"
4-
import { LoaderCircle } from "lucide-react"
3+
import { useMemo, useState, useCallback, useEffect } from "react"
4+
import { toast } from "sonner"
5+
import { LoaderCircle, FileText, Copy, Check } from "lucide-react"
56

6-
import type { Run, TaskMetrics as _TaskMetrics } from "@roo-code/evals"
7+
import type { Run, TaskMetrics as _TaskMetrics, Task } from "@roo-code/evals"
78

89
import { formatCurrency, formatDuration, formatTokens, formatToolUsageSuccessRate } from "@/lib/formatters"
910
import { useRunStatus } from "@/hooks/use-run-status"
@@ -17,6 +18,12 @@ import {
1718
Tooltip,
1819
TooltipContent,
1920
TooltipTrigger,
21+
Dialog,
22+
DialogContent,
23+
DialogHeader,
24+
DialogTitle,
25+
ScrollArea,
26+
Button,
2027
} from "@/components/ui"
2128

2229
import { TaskStatus } from "./task-status"
@@ -35,10 +42,114 @@ function getToolAbbreviation(toolName: string): string {
3542
.join("")
3643
}
3744

45+
// Format log content with basic highlighting
46+
function formatLogContent(log: string): React.ReactNode[] {
47+
const lines = log.split("\n")
48+
return lines.map((line, index) => {
49+
// Highlight timestamps [YYYY-MM-DDTHH:MM:SS.sssZ]
50+
let formattedLine = line.replace(
51+
/\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\]/g,
52+
'<span class="text-blue-400">[$1]</span>',
53+
)
54+
55+
// Highlight log levels
56+
formattedLine = formattedLine.replace(/\|\s*(INFO)\s*\|/g, '| <span class="text-green-400">$1</span> |')
57+
formattedLine = formattedLine.replace(
58+
/\|\s*(WARN|WARNING)\s*\|/g,
59+
'| <span class="text-yellow-400">$1</span> |',
60+
)
61+
formattedLine = formattedLine.replace(/\|\s*(ERROR)\s*\|/g, '| <span class="text-red-400">$1</span> |')
62+
formattedLine = formattedLine.replace(/\|\s*(DEBUG)\s*\|/g, '| <span class="text-gray-400">$1</span> |')
63+
64+
// Highlight task identifiers like taskCreated, taskFocused, etc.
65+
formattedLine = formattedLine.replace(
66+
/(taskCreated|taskFocused|taskStarted|taskCompleted|EvalPass|EvalFail)/g,
67+
'<span class="text-purple-400">$1</span>',
68+
)
69+
70+
// Highlight message arrows
71+
formattedLine = formattedLine.replace(//g, '<span class="text-cyan-400">→</span>')
72+
73+
return (
74+
<div
75+
key={index}
76+
className="hover:bg-white/5"
77+
dangerouslySetInnerHTML={{ __html: formattedLine || "&nbsp;" }}
78+
/>
79+
)
80+
})
81+
}
82+
3883
export function Run({ run }: { run: Run }) {
3984
const runStatus = useRunStatus(run)
4085
const { tasks, tokenUsage, usageUpdatedAt } = runStatus
4186

87+
const [selectedTask, setSelectedTask] = useState<Task | null>(null)
88+
const [taskLog, setTaskLog] = useState<string | null>(null)
89+
const [isLoadingLog, setIsLoadingLog] = useState(false)
90+
const [copied, setCopied] = useState(false)
91+
92+
const onCopyLog = useCallback(async () => {
93+
if (!taskLog) return
94+
95+
try {
96+
await navigator.clipboard.writeText(taskLog)
97+
setCopied(true)
98+
toast.success("Log copied to clipboard")
99+
setTimeout(() => setCopied(false), 2000)
100+
} catch (error) {
101+
console.error("Failed to copy log:", error)
102+
toast.error("Failed to copy log")
103+
}
104+
}, [taskLog])
105+
106+
// Handle ESC key to close the dialog
107+
useEffect(() => {
108+
const handleKeyDown = (e: KeyboardEvent) => {
109+
if (e.key === "Escape" && selectedTask) {
110+
setSelectedTask(null)
111+
}
112+
}
113+
114+
document.addEventListener("keydown", handleKeyDown)
115+
return () => document.removeEventListener("keydown", handleKeyDown)
116+
}, [selectedTask])
117+
118+
const onViewTaskLog = useCallback(
119+
async (task: Task) => {
120+
// Only allow viewing logs for completed tasks
121+
if (task.passed === null || task.passed === undefined) {
122+
toast.error("Task is still running")
123+
return
124+
}
125+
126+
setSelectedTask(task)
127+
setIsLoadingLog(true)
128+
setTaskLog(null)
129+
130+
try {
131+
const response = await fetch(`/api/runs/${run.id}/logs/${task.id}`)
132+
133+
if (!response.ok) {
134+
const error = await response.json()
135+
toast.error(error.error || "Failed to load log")
136+
setSelectedTask(null)
137+
return
138+
}
139+
140+
const data = await response.json()
141+
setTaskLog(data.logContent)
142+
} catch (error) {
143+
console.error("Error loading task log:", error)
144+
toast.error("Failed to load log")
145+
setSelectedTask(null)
146+
} finally {
147+
setIsLoadingLog(false)
148+
}
149+
},
150+
[run.id],
151+
)
152+
42153
const taskMetrics: Record<number, TaskMetrics> = useMemo(() => {
43154
const metrics: Record<number, TaskMetrics> = {}
44155

@@ -241,15 +352,28 @@ export function Run({ run }: { run: Run }) {
241352
</TableHeader>
242353
<TableBody>
243354
{tasks.map((task) => (
244-
<TableRow key={task.id}>
355+
<TableRow
356+
key={task.id}
357+
className={task.finishedAt ? "cursor-pointer hover:bg-muted/50" : ""}
358+
onClick={() => task.finishedAt && onViewTaskLog(task)}>
245359
<TableCell>
246360
<div className="flex items-center gap-2">
247361
<TaskStatus
248362
task={task}
249363
running={!!task.startedAt || !!tokenUsage.get(task.id)}
250364
/>
251-
<div>
252-
{task.language}/{task.exercise}
365+
<div className="flex items-center gap-2">
366+
<span>
367+
{task.language}/{task.exercise}
368+
</span>
369+
{task.finishedAt && (
370+
<Tooltip>
371+
<TooltipTrigger asChild>
372+
<FileText className="size-3 text-muted-foreground" />
373+
</TooltipTrigger>
374+
<TooltipContent>Click to view log</TooltipContent>
375+
</Tooltip>
376+
)}
253377
</div>
254378
</div>
255379
</TableCell>
@@ -282,6 +406,60 @@ export function Run({ run }: { run: Run }) {
282406
</Table>
283407
)}
284408
</div>
409+
410+
{/* Task Log Dialog - Full Screen */}
411+
<Dialog open={!!selectedTask} onOpenChange={() => setSelectedTask(null)}>
412+
<DialogContent className="w-[95vw] !max-w-[95vw] h-[90vh] flex flex-col">
413+
<DialogHeader className="flex-shrink-0">
414+
<div className="flex items-center justify-between pr-8">
415+
<DialogTitle className="flex items-center gap-2">
416+
<FileText className="size-4" />
417+
{selectedTask?.language}/{selectedTask?.exercise}
418+
<span
419+
className={`ml-2 text-sm ${selectedTask?.passed ? "text-green-600" : "text-red-600"}`}>
420+
({selectedTask?.passed ? "Passed" : "Failed"})
421+
</span>
422+
</DialogTitle>
423+
{taskLog && (
424+
<Button
425+
variant="outline"
426+
size="sm"
427+
onClick={onCopyLog}
428+
className="flex items-center gap-1">
429+
{copied ? (
430+
<>
431+
<Check className="size-4" />
432+
Copied!
433+
</>
434+
) : (
435+
<>
436+
<Copy className="size-4" />
437+
Copy Log
438+
</>
439+
)}
440+
</Button>
441+
)}
442+
</div>
443+
</DialogHeader>
444+
<div className="flex-1 min-h-0 overflow-hidden">
445+
{isLoadingLog ? (
446+
<div className="flex items-center justify-center h-full">
447+
<LoaderCircle className="size-6 animate-spin" />
448+
</div>
449+
) : taskLog ? (
450+
<ScrollArea className="h-full w-full">
451+
<div className="text-xs font-mono bg-muted p-4 rounded-md overflow-x-auto">
452+
{formatLogContent(taskLog)}
453+
</div>
454+
</ScrollArea>
455+
) : (
456+
<div className="flex items-center justify-center h-full text-muted-foreground">
457+
Log file not available (may have been cleared)
458+
</div>
459+
)}
460+
</div>
461+
</DialogContent>
462+
</Dialog>
285463
</>
286464
)
287465
}

0 commit comments

Comments
 (0)