tr]:last:border-b-0", className)}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className,
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
diff --git a/evals/apps/web/src/components/ui/tabs.tsx b/evals/apps/web/src/components/ui/tabs.tsx
new file mode 100644
index 00000000000..ffd089ff1c9
--- /dev/null
+++ b/evals/apps/web/src/components/ui/tabs.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import { useEffect, useRef, useState } from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const [indicatorStyle, setIndicatorStyle] = useState({
+ left: 0,
+ top: 0,
+ width: 0,
+ height: 0,
+ })
+
+ const tabsListRef = useRef(null)
+
+ const updateIndicator = React.useCallback(() => {
+ if (!tabsListRef.current) {
+ return
+ }
+
+ const activeTab = tabsListRef.current.querySelector('[data-state="active"]')
+
+ if (!activeTab) {
+ return
+ }
+
+ const activeRect = activeTab.getBoundingClientRect()
+ const tabsRect = tabsListRef.current.getBoundingClientRect()
+
+ requestAnimationFrame(() => {
+ setIndicatorStyle({
+ left: activeRect.left - tabsRect.left,
+ top: activeRect.top - tabsRect.top,
+ width: activeRect.width,
+ height: activeRect.height,
+ })
+ })
+ }, [])
+
+ useEffect(() => {
+ const timeoutId = setTimeout(updateIndicator, 0)
+
+ window.addEventListener("resize", updateIndicator)
+ const observer = new MutationObserver(updateIndicator)
+
+ if (tabsListRef.current) {
+ observer.observe(tabsListRef.current, {
+ attributes: true,
+ childList: true,
+ subtree: true,
+ })
+ }
+
+ return () => {
+ clearTimeout(timeoutId)
+ window.removeEventListener("resize", updateIndicator)
+ observer.disconnect()
+ }
+ }, [updateIndicator])
+
+ return (
+
+ )
+})
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsContent, TabsList, TabsTrigger }
diff --git a/evals/apps/web/src/components/ui/textarea.tsx b/evals/apps/web/src/components/ui/textarea.tsx
new file mode 100644
index 00000000000..c4ebbe2d584
--- /dev/null
+++ b/evals/apps/web/src/components/ui/textarea.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/evals/apps/web/src/components/ui/tooltip.tsx b/evals/apps/web/src/components/ui/tooltip.tsx
new file mode 100644
index 00000000000..f456fdf676c
--- /dev/null
+++ b/evals/apps/web/src/components/ui/tooltip.tsx
@@ -0,0 +1,47 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps) {
+ return
+}
+
+function Tooltip({ ...props }: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function TooltipTrigger({ ...props }: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/evals/apps/web/src/hooks/use-event-source.ts b/evals/apps/web/src/hooks/use-event-source.ts
new file mode 100644
index 00000000000..d076e68a5a7
--- /dev/null
+++ b/evals/apps/web/src/hooks/use-event-source.ts
@@ -0,0 +1,57 @@
+import { useCallback, useEffect, useRef, useState } from "react"
+
+export type EventSourceStatus = "waiting" | "connected" | "error"
+
+export type EventSourceEvent = Event & { data: string }
+
+type UseEventSourceOptions = {
+ url: string
+ withCredentials?: boolean
+ onMessage: (event: MessageEvent) => void
+}
+
+export function useEventSource({ url, withCredentials, onMessage }: UseEventSourceOptions) {
+ const sourceRef = useRef(null)
+ const statusRef = useRef("waiting")
+ const [status, setStatus] = useState("waiting")
+ const handleMessage = useCallback((event: MessageEvent) => onMessage(event), [onMessage])
+
+ const createEventSource = useCallback(() => {
+ sourceRef.current = new EventSource(url, { withCredentials })
+
+ sourceRef.current.onopen = () => {
+ statusRef.current = "connected"
+ setStatus("connected")
+ }
+
+ sourceRef.current.onmessage = (event) => {
+ handleMessage(event)
+ }
+
+ sourceRef.current.onerror = () => {
+ statusRef.current = "error"
+ setStatus("error")
+ // sourceRef.current?.close()
+ // sourceRef.current = null
+ }
+ }, [url, withCredentials, handleMessage])
+
+ useEffect(() => {
+ createEventSource()
+
+ setTimeout(() => {
+ if (statusRef.current === "waiting") {
+ sourceRef.current?.close()
+ sourceRef.current = null
+ createEventSource()
+ }
+ }, 100)
+
+ return () => {
+ sourceRef.current?.close()
+ sourceRef.current = null
+ }
+ }, [createEventSource])
+
+ return status
+}
diff --git a/evals/apps/web/src/hooks/use-exercises.ts b/evals/apps/web/src/hooks/use-exercises.ts
new file mode 100644
index 00000000000..2149f70cda9
--- /dev/null
+++ b/evals/apps/web/src/hooks/use-exercises.ts
@@ -0,0 +1,5 @@
+import { useQuery } from "@tanstack/react-query"
+
+import { getExercises } from "@/lib/server/exercises"
+
+export const useExercises = () => useQuery({ queryKey: ["exercises"], queryFn: getExercises })
diff --git a/evals/apps/web/src/hooks/use-open-router-models.ts b/evals/apps/web/src/hooks/use-open-router-models.ts
new file mode 100644
index 00000000000..fe4e2638a35
--- /dev/null
+++ b/evals/apps/web/src/hooks/use-open-router-models.ts
@@ -0,0 +1,75 @@
+import { z } from "zod"
+import { useQuery } from "@tanstack/react-query"
+
+import { type ModelInfo } from "@evals/types"
+
+const supportsPromptCache = ["anthropic/claude-3.7-sonnet", "anthropic/claude-3.5-sonnet", "anthropic/claude-3-5-haiku"]
+
+const supportsComputerUse = ["anthropic/claude-3.7-sonnet", "anthropic/claude-3.5-sonnet"]
+
+const supportsThinking = ["anthropic/claude-3.7-sonnet:thinking"]
+
+const parsePrice = (price?: string) => (price ? parseFloat(price) * 1_000_000 : undefined)
+
+export const openRouterModelSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ description: z.string(),
+ created: z.number(),
+ context_length: z.number(),
+ pricing: z.object({
+ prompt: z.string().optional(),
+ completion: z.string().optional(),
+ }),
+ top_provider: z
+ .object({
+ max_completion_tokens: z.number().nullish(),
+ })
+ .optional(),
+ architecture: z
+ .object({
+ modality: z.string(),
+ })
+ .optional(),
+})
+
+export type OpenRouterModel = z.infer & { modelInfo: ModelInfo }
+
+export const getOpenRouterModels = async (): Promise => {
+ const response = await fetch("https://openrouter.ai/api/v1/models")
+
+ if (!response.ok) {
+ console.error("Failed to fetch OpenRouter models")
+ return []
+ }
+
+ const result = z.object({ data: z.array(openRouterModelSchema) }).safeParse(await response.json())
+
+ if (!result.success) {
+ console.error(result.error)
+ return []
+ }
+
+ return result.data.data
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .map((rawModel) => ({
+ ...rawModel,
+ modelInfo: {
+ maxTokens: rawModel.top_provider?.max_completion_tokens ?? undefined,
+ contextWindow: rawModel.context_length,
+ supportsImages: rawModel.architecture?.modality?.includes("image"),
+ supportsPromptCache: supportsPromptCache.some((model) => rawModel.id.startsWith(model)),
+ supportsComputerUse: supportsComputerUse.some((model) => rawModel.id.startsWith(model)),
+ inputPrice: parsePrice(rawModel.pricing?.prompt),
+ outputPrice: parsePrice(rawModel.pricing?.completion),
+ description: rawModel.description,
+ thinking: supportsThinking.some((model) => rawModel.id.startsWith(model)),
+ },
+ }))
+}
+
+export const useOpenRouterModels = () =>
+ useQuery({
+ queryKey: ["getOpenRouterModels"],
+ queryFn: getOpenRouterModels,
+ })
diff --git a/evals/apps/web/src/hooks/use-process-tree.ts b/evals/apps/web/src/hooks/use-process-tree.ts
new file mode 100644
index 00000000000..35d7e7ce044
--- /dev/null
+++ b/evals/apps/web/src/hooks/use-process-tree.ts
@@ -0,0 +1,11 @@
+import { useQuery } from "@tanstack/react-query"
+
+import { getProcessList } from "@/lib/server/processes"
+
+export const useProcessList = (pid: number | null) =>
+ useQuery({
+ queryKey: ["process-tree", pid],
+ queryFn: () => (pid ? getProcessList(pid) : []),
+ enabled: !!pid,
+ refetchInterval: 30_000,
+ })
diff --git a/evals/apps/web/src/hooks/use-run-status.ts b/evals/apps/web/src/hooks/use-run-status.ts
new file mode 100644
index 00000000000..a8e755eac25
--- /dev/null
+++ b/evals/apps/web/src/hooks/use-run-status.ts
@@ -0,0 +1,76 @@
+import { useState, useCallback, useRef } from "react"
+import { useQuery, keepPreviousData } from "@tanstack/react-query"
+
+import { TokenUsage, taskEventSchema, RooCodeEventName, EvalEventName } from "@evals/types"
+import { Run } from "@evals/db"
+
+import { getTasks } from "@/lib/server/tasks"
+import { useEventSource } from "@/hooks/use-event-source"
+
+export const useRunStatus = (run: Run) => {
+ const [tasksUpdatedAt, setTasksUpdatedAt] = useState()
+ const [usageUpdatedAt, setUsageUpdatedAt] = useState()
+
+ const tokenUsage = useRef |