Skip to content

Commit b0dc456

Browse files
feat(site): add task instruction viewer (#23)
* feat(site): add task instruction viewer - Implement a side sheet and drawer to view task instructions directly on the tasks page. - Add `react-markdown` and `remark-gfm` for rendering instruction markdown. - Update `compute-tasks.ts` to copy task instructions to the public directory for client-side fetching. - Add necessary UI components (Drawer, Sheet, Skeleton). 🤖 Generated with [Pochi](https://getpochi.com) | [Task](https://app.getpochi.com/share/p-a4d8c81f864f409f953c83589b4cbe77) Co-Authored-By: Pochi <noreply@getpochi.com> * update * update: styling * update * update: build instruction.md * update * update * update * update * update --------- Co-authored-by: Pochi <noreply@getpochi.com>
1 parent b6c69b0 commit b0dc456

File tree

13 files changed

+2296
-1552
lines changed

13 files changed

+2296
-1552
lines changed

site/app/(home)/page.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ import tasksData from "../../tasks.json";
33
import zealtConfig from "../../../zealt.json";
44
import LeaderboardTable, { type LeaderboardEntry } from "./components/leaderboard-table";
55

6+
type TaskTrial = {
7+
agent: string;
8+
model: string;
9+
passed: boolean;
10+
latency_sec: number | null;
11+
};
12+
13+
type TaskValue = {
14+
trials?: TaskTrial[];
15+
};
16+
617
export default function Home() {
718
// Process tasks.json to compute leaderboard stats directly on the server
819
const statsMap = new Map<string, {
@@ -14,8 +25,16 @@ export default function Home() {
1425
agent: string;
1526
}>();
1627

17-
Object.values(tasksData).forEach((trials: any[]) => {
18-
trials.forEach(trial => {
28+
Object.values(tasksData as Record<string, unknown>).forEach((taskValue) => {
29+
let trials: TaskTrial[] = [];
30+
if (Array.isArray(taskValue)) {
31+
trials = taskValue as TaskTrial[];
32+
} else if (typeof taskValue === "object" && taskValue !== null) {
33+
const task = taskValue as TaskValue;
34+
trials = Array.isArray(task.trials) ? task.trials : [];
35+
}
36+
37+
trials.forEach((trial) => {
1938
// Simplify model name
2039
const modelName = trial.model.split('/').pop() || trial.model;
2140
const agentName = trial.agent.charAt(0).toUpperCase() + trial.agent.slice(1);
@@ -33,7 +52,10 @@ export default function Home() {
3352
});
3453
}
3554

36-
const stats = statsMap.get(key)!;
55+
const stats = statsMap.get(key);
56+
if (!stats) {
57+
return;
58+
}
3759
stats.total += 1;
3860
if (trial.passed) {
3961
stats.passed += 1;

site/app/tasks/[name]/[jobId]/trajectory/components/trajectory-page.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ export function TrajectoryPage({ title, trajectoryUrl, fallbackUrl }: Trajectory
1515

1616
useEffect(() => {
1717
if (!trajectoryUrl) {
18-
window.location.replace(fallbackUrl);
18+
if (fallbackUrl) {
19+
window.location.replace(fallbackUrl);
20+
}
1921
return;
2022
}
2123

@@ -32,7 +34,9 @@ export function TrajectoryPage({ title, trajectoryUrl, fallbackUrl }: Trajectory
3234
};
3335

3436
const handleIframeError = () => {
35-
window.location.replace(fallbackUrl);
37+
if (fallbackUrl) {
38+
window.location.replace(fallbackUrl);
39+
}
3640
}
3741

3842
if (trajectoryUrl) {

site/app/tasks/[name]/[jobId]/trajectory/page.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ function isTrialEntry(value: unknown): value is TrialEntry {
5555
}
5656

5757
function findTrialEntry(taskName: string, jobId: string): TrialEntry | null {
58-
for (const trials of Object.values(tasksData as Record<string, unknown>)) {
58+
for (const task of Object.values(tasksData as Record<string, unknown>)) {
59+
if (typeof task !== "object" || task === null) {
60+
continue;
61+
}
62+
63+
const trials = (task as { trials?: unknown }).trials;
5964
if (!Array.isArray(trials)) {
6065
continue;
6166
}
@@ -84,7 +89,12 @@ export const dynamicParams = false;
8489
export function generateStaticParams(): RouteParams[] {
8590
const params: RouteParams[] = [];
8691

87-
for (const trials of Object.values(tasksData as Record<string, unknown>)) {
92+
for (const task of Object.values(tasksData as Record<string, unknown>)) {
93+
if (typeof task !== "object" || task === null) {
94+
continue;
95+
}
96+
97+
const trials = (task as { trials?: unknown }).trials;
8898
if (!Array.isArray(trials)) {
8999
continue;
90100
}

site/app/tasks/page.tsx

Lines changed: 157 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
"use client";
22

3-
import { useState, useMemo, useEffect, Suspense } from "react";
4-
import { Check, X as XIcon, Search, AlertTriangle, ArrowUpDown, ArrowUp, ArrowDown, Filter, X } from "lucide-react";
3+
import { useState, useMemo, useEffect, Suspense, useCallback } from "react";
4+
import {
5+
Check,
6+
X as XIcon,
7+
Search,
8+
AlertTriangle,
9+
ArrowUpDown,
10+
ArrowUp,
11+
ArrowDown,
12+
Filter,
13+
X,
14+
ExternalLink,
15+
} from "lucide-react";
516
import Link from "next/link";
617
import { useRouter, useSearchParams, usePathname } from "next/navigation";
718
import { clsx, type ClassValue } from "clsx";
@@ -13,17 +24,35 @@ import {
1324
HoverCardContent,
1425
HoverCardTrigger,
1526
} from "@/components/ui/hover-card";
27+
import {
28+
Sheet,
29+
SheetContent,
30+
SheetDescription,
31+
SheetHeader,
32+
SheetTitle,
33+
} from "@/components/ui/sheet";
34+
import {
35+
Drawer,
36+
DrawerContent,
37+
DrawerDescription,
38+
DrawerHeader,
39+
DrawerTitle,
40+
} from "@/components/ui/drawer";
41+
import { Button } from "@/components/ui/button";
42+
import { Skeleton } from "@/components/ui/skeleton";
1643
import { MultiSelect } from "./components/multi-select";
1744
import { BackToTop } from "./components/back-to-top";
1845

46+
1947
function cn(...inputs: ClassValue[]) {
2048
return twMerge(clsx(inputs));
2149
}
2250

2351
// Convert object to array and sort by task name
24-
const tasksData = Object.entries(tasksDataRaw).map(([taskName, trials]) => {
52+
const tasksData = Object.entries(tasksDataRaw).map(([taskName, { trials, instruction }]) => {
2553
return {
2654
taskName,
55+
instruction,
2756
trials: (trials as any[]).map(t => {
2857
const trialNameParts = String(t.trial_name ?? "").split("__");
2958
const taskName = trialNameParts[0] || "";
@@ -49,9 +78,24 @@ const allTrialsFlat = tasksData.flatMap(task =>
4978
);
5079

5180
const allModels = Array.from(new Set(allTrialsFlat.map(tr => tr.model)));
52-
const allAgents = Array.from(new Set(allTrialsFlat.map(tr => tr.agent)));
5381
const allCombos = Array.from(new Set(allTrialsFlat.map(tr => `${tr.model} (${tr.agent})`))).sort();
5482

83+
function useMediaQuery(query: string) {
84+
const [matches, setMatches] = useState(false);
85+
86+
useEffect(() => {
87+
const media = window.matchMedia(query);
88+
setMatches(media.matches);
89+
90+
const listener = (event: MediaQueryListEvent) => setMatches(event.matches);
91+
media.addEventListener("change", listener);
92+
93+
return () => media.removeEventListener("change", listener);
94+
}, [query]);
95+
96+
return matches;
97+
}
98+
5599
function TasksContent() {
56100
const router = useRouter();
57101
const pathname = usePathname();
@@ -69,18 +113,13 @@ function TasksContent() {
69113
const selectedAgents = queryAgentStr ? queryAgentStr.split(",") : [];
70114

71115
const [searchQuery, setSearchQuery] = useState(queryQ);
116+
const [selectedTask, setSelectedTask] = useState<string | null>(null);
117+
const [isInstructionOpen, setIsInstructionOpen] = useState(false);
118+
const isDesktop = useMediaQuery("(min-width: 1024px)");
72119

73120
const hasActiveFilters = selectedStatuses.length > 0 || selectedModels.length > 0 || selectedAgents.length > 0 || searchQuery !== "" || querySort !== "default";
74121

75-
// Debounce search query to URL
76-
useEffect(() => {
77-
const timer = setTimeout(() => {
78-
updateParams({ q: searchQuery });
79-
}, 300);
80-
return () => clearTimeout(timer);
81-
}, [searchQuery]);
82-
83-
const updateParams = (updates: Record<string, string | null>) => {
122+
const updateParams = useCallback((updates: Record<string, string | null>) => {
84123
const params = new URLSearchParams(searchParams.toString());
85124
Object.entries(updates).forEach(([key, value]) => {
86125
if (value === null || value === "" || value === "all" || (key === "sort" && value === "default")) {
@@ -89,8 +128,27 @@ function TasksContent() {
89128
params.set(key, value);
90129
}
91130
});
92-
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
93-
};
131+
const nextQuery = params.toString();
132+
const currentQuery = searchParams.toString();
133+
if (nextQuery === currentQuery) {
134+
return;
135+
}
136+
137+
const nextUrl = nextQuery ? `${pathname}?${nextQuery}` : pathname;
138+
router.replace(nextUrl, { scroll: false });
139+
}, [pathname, router, searchParams]);
140+
141+
// Debounce search query to URL
142+
useEffect(() => {
143+
if (searchQuery === queryQ) {
144+
return;
145+
}
146+
147+
const timer = setTimeout(() => {
148+
updateParams({ q: searchQuery });
149+
}, 300);
150+
return () => clearTimeout(timer);
151+
}, [queryQ, searchQuery, updateParams]);
94152

95153
const activeCombos = useMemo(() => {
96154
const combos = allCombos.filter(combo => {
@@ -171,7 +229,7 @@ function TasksContent() {
171229
}
172230

173231
const avgDuration = Object.values(comboMap).length > 0
174-
? Object.values(comboMap).reduce((sum: number, t: any) => sum + t.exec_duration, 0) / Object.values(comboMap).length
232+
? Object.values(comboMap).reduce((sum, t) => sum + t.exec_duration, 0) / Object.values(comboMap).length
175233
: 0;
176234

177235
return {
@@ -217,6 +275,47 @@ function TasksContent() {
217275
return queryOrder === "asc" ? <ArrowUp className="w-3 h-3" /> : <ArrowDown className="w-3 h-3" />;
218276
};
219277

278+
const selectedTaskInstructionUrl = selectedTask
279+
? `${zealtConfig.github_repo}/tree/main/tasks/${selectedTask}/instruction.md`
280+
: "";
281+
282+
const selectedTaskInstruction = selectedTask
283+
? tasksData.find(task => task.taskName === selectedTask)?.instruction || ""
284+
: "";
285+
286+
const instructionBody = (
287+
<>
288+
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-5 sm:px-7 py-4 sm:py-5">
289+
{selectedTask ? (
290+
selectedTaskInstruction ? (
291+
<pre className="m-0 p-0 text-xs sm:text-sm leading-6 sm:leading-7 text-foreground/95 whitespace-pre-wrap wrap-break-word font-mono">
292+
{selectedTaskInstruction}
293+
</pre>
294+
) : (
295+
<div className="rounded-lg border border-border/60 bg-secondary/20 px-4 py-3 text-sm text-muted-foreground">
296+
This task has no instruction content.
297+
</div>
298+
)
299+
) : (
300+
<Skeleton className="h-28 w-full" />
301+
)}
302+
</div>
303+
304+
<div className="shrink-0 border-t border-border/60 px-5 sm:px-7 py-3 bg-card/80">
305+
<Button variant="outline" asChild className="h-8 w-full text-xs sm:h-9 sm:w-auto sm:text-sm">
306+
<a
307+
href={selectedTaskInstructionUrl}
308+
target="_blank"
309+
rel="noopener noreferrer"
310+
>
311+
<ExternalLink className="h-4 w-4" />
312+
Open instruction.md
313+
</a>
314+
</Button>
315+
</div>
316+
</>
317+
);
318+
220319
return (
221320
<div className="container mx-auto px-4 sm:px-8 lg:px-12 py-8 max-w-screen-2xl h-[100dvh] flex flex-col overflow-hidden">
222321
{/* Header Section */}
@@ -326,23 +425,25 @@ function TasksContent() {
326425
</tr>
327426
</thead>
328427
<tbody className="divide-y divide-border/30">
329-
{filteredAndSortedTasks.map((task, index) => (
428+
{filteredAndSortedTasks.map((task) => (
330429
<tr
331430
key={task.taskName}
332431
className="hover:bg-secondary/30 even:bg-secondary/5 transition-colors duration-200 group"
333432
>
334433
<td className="md:sticky left-0 z-20 bg-background border-r border-border/50 p-0 font-mono w-[200px] min-w-[200px] max-w-[200px] md:w-[350px] md:min-w-[350px] md:max-w-[350px] md:shadow-[1px_0_0_rgba(0,0,0,0.05)]">
335-
<a
336-
href={`${zealtConfig.github_repo}/tree/main/tasks/${task.taskName}/instruction.md`}
337-
target="_blank"
338-
rel="noopener noreferrer"
339-
className="group/task flex items-center gap-2 px-3 sm:px-6 py-2 w-full h-full text-foreground hover:text-primary transition-colors focus:outline-none bg-transparent group-even:bg-secondary/5 group-hover:bg-secondary/30"
340-
title={`View ${task.taskName} instruction on GitHub`}
434+
<button
435+
type="button"
436+
onClick={() => {
437+
setSelectedTask(task.taskName);
438+
setIsInstructionOpen(true);
439+
}}
440+
className="group/task flex items-center gap-2 px-3 sm:px-6 py-2 w-full h-full text-foreground hover:text-primary transition-colors focus:outline-none bg-transparent group-even:bg-secondary/5 group-hover:bg-secondary/30 cursor-pointer text-left"
441+
title={`View ${task.taskName} instruction`}
341442
>
342443
<span className="truncate w-full block group-hover/task:underline text-xs md:text-sm">
343444
{task.taskName}
344445
</span>
345-
</a>
446+
</button>
346447
</td>
347448
{activeCombos.map(combo => {
348449
const trial = task.comboMap[combo];
@@ -421,6 +522,38 @@ function TasksContent() {
421522
)}
422523
</div>
423524

525+
{isDesktop ? (
526+
<Sheet open={isInstructionOpen} onOpenChange={setIsInstructionOpen}>
527+
<SheetContent
528+
side="right"
529+
className="h-full min-h-0 w-[640px] lg:w-[680px] xl:w-[760px] 2xl:w-[820px] max-w-[90vw] border-l border-border/70 bg-card/80 p-0 shadow-[0_0_0_1px_rgba(255,255,255,0.04),0_24px_80px_rgba(0,0,0,0.55)] backdrop-blur-md data-[state=open]:duration-320 data-[state=closed]:duration-220 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95"
530+
>
531+
<div className="flex h-full min-h-0 flex-col">
532+
<SheetHeader className="border-b border-border/60 bg-card/80 px-7 py-5 pr-14">
533+
<SheetTitle className="text-base">{selectedTask || "Task Instruction"}</SheetTitle>
534+
<SheetDescription className="sr-only">{selectedTask}</SheetDescription>
535+
</SheetHeader>
536+
{instructionBody}
537+
</div>
538+
</SheetContent>
539+
</Sheet>
540+
) : (
541+
<Drawer open={isInstructionOpen} onOpenChange={setIsInstructionOpen} direction="bottom">
542+
<DrawerContent className="inset-x-0 bottom-0 h-[76dvh] max-h-[76dvh] rounded-t-2xl border-t border-border/70 bg-card/95 p-0">
543+
<div className="mx-auto mt-3 h-1.5 w-14 rounded-full bg-muted-foreground/40" />
544+
<div className="flex h-full min-h-0 flex-col">
545+
<DrawerHeader className="border-b border-border/60 px-5 pb-4">
546+
<DrawerTitle className="text-base">
547+
{selectedTask || "Task Instruction"}
548+
</DrawerTitle>
549+
<SheetDescription className="sr-only">{selectedTask}</SheetDescription>
550+
</DrawerHeader>
551+
{instructionBody}
552+
</div>
553+
</DrawerContent>
554+
</Drawer>
555+
)}
556+
424557
<BackToTop />
425558
</div>
426559
);

site/bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)