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" ;
516import Link from "next/link" ;
617import { useRouter , useSearchParams , usePathname } from "next/navigation" ;
718import { 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" ;
1643import { MultiSelect } from "./components/multi-select" ;
1744import { BackToTop } from "./components/back-to-top" ;
1845
46+
1947function 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
5180const allModels = Array . from ( new Set ( allTrialsFlat . map ( tr => tr . model ) ) ) ;
52- const allAgents = Array . from ( new Set ( allTrialsFlat . map ( tr => tr . agent ) ) ) ;
5381const 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+
5599function 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 ) ;
0 commit comments