More progress

This commit is contained in:
cte 2025-03-24 10:49:42 -07:00
parent e9fadc7cb1
commit 568bc5e576
8 changed files with 234 additions and 21 deletions

View file

@ -37,6 +37,7 @@
"react-use": "^17.6.0",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^3.24.2"
},
"devDependencies": {

View file

@ -1,16 +1,33 @@
"use client"
import { LoaderCircle } from "lucide-react"
import { useState, useRef, useEffect } from "react"
import { LoaderCircle, RectangleEllipsis } from "lucide-react"
import * as db from "@benchmark/db"
import { useRunStatus } from "@/hooks/use-run-status"
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, ScrollArea } from "@/components/ui"
import { TaskStatus } from "./task-status"
import { ConnectionStatus } from "./connection-status"
export function Run({ run }: { run: db.Run }) {
const { tasks, status, clientId, runningTaskId } = useRunStatus(run)
const { tasks, status, clientId, runningTaskId, output, outputCounts } = useRunStatus(run)
const scrollAreaRef = useRef<HTMLDivElement>(null)
const [selectedTask, setSelectedTask] = useState<db.Task>()
useEffect(() => {
if (selectedTask) {
const scrollArea = scrollAreaRef.current
if (scrollArea) {
scrollArea.scrollTo({
top: scrollArea.scrollHeight,
behavior: "smooth",
})
}
}
}, [selectedTask, outputCounts])
return (
<>
@ -29,6 +46,14 @@ export function Run({ run }: { run: db.Run }) {
<div>
{task.language}/{task.exercise}
</div>
{(outputCounts[task.id] ?? 0) > 0 && (
<div
className="flex items-center gap-1 cursor-pointer"
onClick={() => setSelectedTask(task)}>
<RectangleEllipsis className="size-4" />
<div className="text-xs">({outputCounts[task.id]})</div>
</div>
)}
</div>
))
)}
@ -36,6 +61,27 @@ export function Run({ run }: { run: db.Run }) {
<div className="absolute top-5 right-5">
<ConnectionStatus status={status} clientId={clientId} pid={run.pid} />
</div>
<Drawer open={!!selectedTask} onOpenChange={() => setSelectedTask(undefined)}>
<DrawerContent>
<div className="mx-auto w-full max-w-2xl">
<DrawerHeader>
<DrawerTitle>
{selectedTask?.language}/{selectedTask?.exercise}
</DrawerTitle>
</DrawerHeader>
<div className="font-mono text-xs pb-12">
{selectedTask && (
<ScrollArea viewportRef={scrollAreaRef} className="h-96 rounded-sm border">
<div className="p-4">
<h4 className="mb-4 text-sm font-medium leading-none">Tags</h4>
{output.get(selectedTask.id)?.map((line, i) => <div key={i}>{line}</div>)}
</div>
</ScrollArea>
)}
</div>
</div>
</DrawerContent>
</Drawer>
</>
)
}

View file

@ -0,0 +1,98 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
function Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
}
function DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
}
function DrawerPortal({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
}
function DrawerClose({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
}
function DrawerOverlay({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"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",
className,
)}
{...props}
/>
)
}
function DrawerContent({ className, children, ...props }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"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",
"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",
"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",
"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",
className,
)}
{...props}>
<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" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
)
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="drawer-header" className={cn("flex flex-col gap-1.5 py-4", className)} {...props} />
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return <div data-slot="drawer-footer" className={cn("mt-auto flex flex-col gap-2 py-4", className)} {...props} />
}
function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View file

@ -2,6 +2,7 @@ export * from "./badge"
export * from "./button"
export * from "./command"
export * from "./dialog"
export * from "./drawer"
export * from "./form"
export * from "./input"
export * from "./label"

View file

@ -5,10 +5,15 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
type ScrollAreaProps = React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {
viewportRef?: React.RefObject<HTMLDivElement | null>
}
function ScrollArea({ className, children, viewportRef, ...props }: ScrollAreaProps) {
return (
<ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props}>
<ScrollAreaPrimitive.Viewport
ref={viewportRef}
data-slot="scroll-area-viewport"
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">
{children}

View file

@ -1,4 +1,4 @@
import { useState, useCallback } from "react"
import { useState, useCallback, useRef } from "react"
import { useQuery, keepPreviousData } from "@tanstack/react-query"
import { Run } from "@benchmark/db"
@ -10,6 +10,8 @@ import { useEventSource } from "@/hooks/use-event-source"
export const useRunStatus = (run: Run) => {
const [clientId, setClientId] = useState<string>()
const [runningTaskId, setRunningTaskId] = useState<number>()
const outputRef = useRef<Map<number, string[]>>(new Map())
const [outputCounts, setOutputCounts] = useState<Record<number, number>>({})
const { data: tasks } = useQuery({
queryKey: ["run", run.id, runningTaskId],
@ -42,20 +44,28 @@ export const useRunStatus = (run: Run) => {
if (payload.type === "Ack") {
setClientId(payload.data.clientId as string)
} else if (payload.type === "TaskEvent") {
const {
eventName,
data: { task },
} = payload.data
const taskId = payload.data.data.task.id
if (eventName === "connect" || eventName === "taskStarted") {
setRunningTaskId(task.id)
} else if (eventName === "taskFinished") {
if (payload.data.eventName === "connect" || payload.data.eventName === "taskStarted") {
setRunningTaskId(taskId)
} else if (payload.data.eventName === "taskFinished") {
setRunningTaskId(undefined)
} else if (payload.data.eventName === "message") {
const { text } = payload.data.data.message.message
console.log(`message: ${taskId} ->`, text)
outputRef.current.set(taskId, [...(outputRef.current.get(taskId) || []), text])
const outputCounts: Record<number, number> = {}
for (const [taskId, messages] of outputRef.current.entries()) {
outputCounts[taskId] = messages.length
}
setOutputCounts(outputCounts)
}
}
}, [])
const status = useEventSource({ url, onMessage })
return { tasks, status, clientId, runningTaskId }
return { tasks, status, clientId, runningTaskId, output: outputRef.current, outputCounts }
}

View file

@ -17,6 +17,48 @@ export const createRunSchema = z
export type CreateRun = z.infer<typeof createRunSchema>
/**
* TaskEvent
*/
export const taskEventSchema = z.discriminatedUnion("eventName", [
z.object({
eventName: z.literal("connect"),
data: z.object({ task: z.object({ id: z.number() }) }),
}),
z.object({
eventName: z.literal("taskStarted"),
data: z.object({ task: z.object({ id: z.number() }) }),
}),
z.object({
eventName: z.literal("message"),
data: z.object({
task: z.object({ id: z.number() }),
message: z.object({
taskId: z.string(),
action: z.enum(["created", "updated"]),
message: z.object({
ask: z.string().optional(),
say: z.string().optional(),
partial: z.boolean(),
text: z.string(),
}),
}),
}),
}),
z.object({
eventName: z.literal("taskTokenUsageUpdated"),
data: z.object({
task: z.object({ id: z.number() }),
usage: z.object({}),
}),
}),
z.object({
eventName: z.literal("taskFinished"),
data: z.object({ task: z.object({ id: z.number() }) }),
}),
])
/**
* IpcServerMessage
*/
@ -28,15 +70,7 @@ export const ipcServerMessageSchema = z.discriminatedUnion("type", [
}),
z.object({
type: z.literal("TaskEvent"),
data: z.object({
eventName: z.enum(["connect", "taskStarted", "message", "taskTokenUsageUpdated", "taskFinished"]),
data: z.object({
task: z.object({ id: z.number() }),
// message: z.object({}).optional(),
// usage: z.object({}).optional(),
// taskMetrics: z.object({}).optional(),
}),
}),
data: taskEventSchema,
}),
])

View file

@ -150,6 +150,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@4.0.14)
vaul:
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
zod:
specifier: ^3.24.2
version: 3.24.2
@ -4190,6 +4193,12 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vaul@1.1.2:
resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
@ -8149,6 +8158,15 @@ snapshots:
util-deprecate@1.0.2: {}
vaul@1.1.2(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
'@radix-ui/react-dialog': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
wcwidth@1.0.1:
dependencies:
defaults: 1.0.4