Skip to content

Commit 568bc5e

Browse files
committed
More progress
1 parent e9fadc7 commit 568bc5e

File tree

8 files changed

+235
-22
lines changed

8 files changed

+235
-22
lines changed

benchmark/apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"react-use": "^17.6.0",
3838
"tailwind-merge": "^3.0.2",
3939
"tailwindcss-animate": "^1.0.7",
40+
"vaul": "^1.1.2",
4041
"zod": "^3.24.2"
4142
},
4243
"devDependencies": {

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
"use client"
22

3-
import { LoaderCircle } from "lucide-react"
3+
import { useState, useRef, useEffect } from "react"
4+
import { LoaderCircle, RectangleEllipsis } from "lucide-react"
45

56
import * as db from "@benchmark/db"
67

78
import { useRunStatus } from "@/hooks/use-run-status"
9+
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, ScrollArea } from "@/components/ui"
810

911
import { TaskStatus } from "./task-status"
1012
import { ConnectionStatus } from "./connection-status"
1113

1214
export function Run({ run }: { run: db.Run }) {
13-
const { tasks, status, clientId, runningTaskId } = useRunStatus(run)
15+
const { tasks, status, clientId, runningTaskId, output, outputCounts } = useRunStatus(run)
16+
const scrollAreaRef = useRef<HTMLDivElement>(null)
17+
const [selectedTask, setSelectedTask] = useState<db.Task>()
18+
19+
useEffect(() => {
20+
if (selectedTask) {
21+
const scrollArea = scrollAreaRef.current
22+
23+
if (scrollArea) {
24+
scrollArea.scrollTo({
25+
top: scrollArea.scrollHeight,
26+
behavior: "smooth",
27+
})
28+
}
29+
}
30+
}, [selectedTask, outputCounts])
1431

1532
return (
1633
<>
@@ -29,13 +46,42 @@ export function Run({ run }: { run: db.Run }) {
2946
<div>
3047
{task.language}/{task.exercise}
3148
</div>
49+
{(outputCounts[task.id] ?? 0) > 0 && (
50+
<div
51+
className="flex items-center gap-1 cursor-pointer"
52+
onClick={() => setSelectedTask(task)}>
53+
<RectangleEllipsis className="size-4" />
54+
<div className="text-xs">({outputCounts[task.id]})</div>
55+
</div>
56+
)}
3257
</div>
3358
))
3459
)}
3560
</div>
3661
<div className="absolute top-5 right-5">
3762
<ConnectionStatus status={status} clientId={clientId} pid={run.pid} />
3863
</div>
64+
<Drawer open={!!selectedTask} onOpenChange={() => setSelectedTask(undefined)}>
65+
<DrawerContent>
66+
<div className="mx-auto w-full max-w-2xl">
67+
<DrawerHeader>
68+
<DrawerTitle>
69+
{selectedTask?.language}/{selectedTask?.exercise}
70+
</DrawerTitle>
71+
</DrawerHeader>
72+
<div className="font-mono text-xs pb-12">
73+
{selectedTask && (
74+
<ScrollArea viewportRef={scrollAreaRef} className="h-96 rounded-sm border">
75+
<div className="p-4">
76+
<h4 className="mb-4 text-sm font-medium leading-none">Tags</h4>
77+
{output.get(selectedTask.id)?.map((line, i) => <div key={i}>{line}</div>)}
78+
</div>
79+
</ScrollArea>
80+
)}
81+
</div>
82+
</div>
83+
</DrawerContent>
84+
</Drawer>
3985
</>
4086
)
4187
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import { Drawer as DrawerPrimitive } from "vaul"
5+
6+
import { cn } from "@/lib/utils"
7+
8+
function Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
9+
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
10+
}
11+
12+
function DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
13+
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
14+
}
15+
16+
function DrawerPortal({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
17+
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
18+
}
19+
20+
function DrawerClose({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
21+
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
22+
}
23+
24+
function DrawerOverlay({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
25+
return (
26+
<DrawerPrimitive.Overlay
27+
data-slot="drawer-overlay"
28+
className={cn(
29+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
30+
className,
31+
)}
32+
{...props}
33+
/>
34+
)
35+
}
36+
37+
function DrawerContent({ className, children, ...props }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
38+
return (
39+
<DrawerPortal data-slot="drawer-portal">
40+
<DrawerOverlay />
41+
<DrawerPrimitive.Content
42+
data-slot="drawer-content"
43+
className={cn(
44+
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
45+
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-sm data-[vaul-drawer-direction=top]:border-b",
46+
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-sm data-[vaul-drawer-direction=bottom]:border-t",
47+
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
48+
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
49+
className,
50+
)}
51+
{...props}>
52+
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
53+
{children}
54+
</DrawerPrimitive.Content>
55+
</DrawerPortal>
56+
)
57+
}
58+
59+
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
60+
return <div data-slot="drawer-header" className={cn("flex flex-col gap-1.5 py-4", className)} {...props} />
61+
}
62+
63+
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
64+
return <div data-slot="drawer-footer" className={cn("mt-auto flex flex-col gap-2 py-4", className)} {...props} />
65+
}
66+
67+
function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
68+
return (
69+
<DrawerPrimitive.Title
70+
data-slot="drawer-title"
71+
className={cn("text-foreground font-semibold", className)}
72+
{...props}
73+
/>
74+
)
75+
}
76+
77+
function DrawerDescription({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
78+
return (
79+
<DrawerPrimitive.Description
80+
data-slot="drawer-description"
81+
className={cn("text-muted-foreground text-sm", className)}
82+
{...props}
83+
/>
84+
)
85+
}
86+
87+
export {
88+
Drawer,
89+
DrawerPortal,
90+
DrawerOverlay,
91+
DrawerTrigger,
92+
DrawerClose,
93+
DrawerContent,
94+
DrawerHeader,
95+
DrawerFooter,
96+
DrawerTitle,
97+
DrawerDescription,
98+
}

benchmark/apps/web/src/components/ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from "./badge"
22
export * from "./button"
33
export * from "./command"
44
export * from "./dialog"
5+
export * from "./drawer"
56
export * from "./form"
67
export * from "./input"
78
export * from "./label"

benchmark/apps/web/src/components/ui/scroll-area.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
55

66
import { cn } from "@/lib/utils"
77

8-
function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
8+
type ScrollAreaProps = React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {
9+
viewportRef?: React.RefObject<HTMLDivElement | null>
10+
}
11+
12+
function ScrollArea({ className, children, viewportRef, ...props }: ScrollAreaProps) {
913
return (
1014
<ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props}>
1115
<ScrollAreaPrimitive.Viewport
16+
ref={viewportRef}
1217
data-slot="scroll-area-viewport"
1318
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1">
1419
{children}

benchmark/apps/web/src/hooks/use-run-status.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useCallback } from "react"
1+
import { useState, useCallback, useRef } from "react"
22
import { useQuery, keepPreviousData } from "@tanstack/react-query"
33

44
import { Run } from "@benchmark/db"
@@ -10,6 +10,8 @@ import { useEventSource } from "@/hooks/use-event-source"
1010
export const useRunStatus = (run: Run) => {
1111
const [clientId, setClientId] = useState<string>()
1212
const [runningTaskId, setRunningTaskId] = useState<number>()
13+
const outputRef = useRef<Map<number, string[]>>(new Map())
14+
const [outputCounts, setOutputCounts] = useState<Record<number, number>>({})
1315

1416
const { data: tasks } = useQuery({
1517
queryKey: ["run", run.id, runningTaskId],
@@ -42,20 +44,28 @@ export const useRunStatus = (run: Run) => {
4244
if (payload.type === "Ack") {
4345
setClientId(payload.data.clientId as string)
4446
} else if (payload.type === "TaskEvent") {
45-
const {
46-
eventName,
47-
data: { task },
48-
} = payload.data
49-
50-
if (eventName === "connect" || eventName === "taskStarted") {
51-
setRunningTaskId(task.id)
52-
} else if (eventName === "taskFinished") {
47+
const taskId = payload.data.data.task.id
48+
49+
if (payload.data.eventName === "connect" || payload.data.eventName === "taskStarted") {
50+
setRunningTaskId(taskId)
51+
} else if (payload.data.eventName === "taskFinished") {
5352
setRunningTaskId(undefined)
53+
} else if (payload.data.eventName === "message") {
54+
const { text } = payload.data.data.message.message
55+
console.log(`message: ${taskId} ->`, text)
56+
outputRef.current.set(taskId, [...(outputRef.current.get(taskId) || []), text])
57+
const outputCounts: Record<number, number> = {}
58+
59+
for (const [taskId, messages] of outputRef.current.entries()) {
60+
outputCounts[taskId] = messages.length
61+
}
62+
63+
setOutputCounts(outputCounts)
5464
}
5565
}
5666
}, [])
5767

5868
const status = useEventSource({ url, onMessage })
5969

60-
return { tasks, status, clientId, runningTaskId }
70+
return { tasks, status, clientId, runningTaskId, output: outputRef.current, outputCounts }
6171
}

benchmark/apps/web/src/lib/schemas.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,48 @@ export const createRunSchema = z
1717

1818
export type CreateRun = z.infer<typeof createRunSchema>
1919

20+
/**
21+
* TaskEvent
22+
*/
23+
24+
export const taskEventSchema = z.discriminatedUnion("eventName", [
25+
z.object({
26+
eventName: z.literal("connect"),
27+
data: z.object({ task: z.object({ id: z.number() }) }),
28+
}),
29+
z.object({
30+
eventName: z.literal("taskStarted"),
31+
data: z.object({ task: z.object({ id: z.number() }) }),
32+
}),
33+
z.object({
34+
eventName: z.literal("message"),
35+
data: z.object({
36+
task: z.object({ id: z.number() }),
37+
message: z.object({
38+
taskId: z.string(),
39+
action: z.enum(["created", "updated"]),
40+
message: z.object({
41+
ask: z.string().optional(),
42+
say: z.string().optional(),
43+
partial: z.boolean(),
44+
text: z.string(),
45+
}),
46+
}),
47+
}),
48+
}),
49+
z.object({
50+
eventName: z.literal("taskTokenUsageUpdated"),
51+
data: z.object({
52+
task: z.object({ id: z.number() }),
53+
usage: z.object({}),
54+
}),
55+
}),
56+
z.object({
57+
eventName: z.literal("taskFinished"),
58+
data: z.object({ task: z.object({ id: z.number() }) }),
59+
}),
60+
])
61+
2062
/**
2163
* IpcServerMessage
2264
*/
@@ -28,15 +70,7 @@ export const ipcServerMessageSchema = z.discriminatedUnion("type", [
2870
}),
2971
z.object({
3072
type: z.literal("TaskEvent"),
31-
data: z.object({
32-
eventName: z.enum(["connect", "taskStarted", "message", "taskTokenUsageUpdated", "taskFinished"]),
33-
data: z.object({
34-
task: z.object({ id: z.number() }),
35-
// message: z.object({}).optional(),
36-
// usage: z.object({}).optional(),
37-
// taskMetrics: z.object({}).optional(),
38-
}),
39-
}),
73+
data: taskEventSchema,
4074
}),
4175
])
4276

benchmark/pnpm-lock.yaml

Lines changed: 18 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)