|
|
@@ -1,14 +1,45 @@
|
|
|
"use client"
|
|
|
|
|
|
-import { useMemo, useState } from "react"
|
|
|
+import { useCallback, useEffect, useMemo, useState } from "react"
|
|
|
import { useRouter } from "next/navigation"
|
|
|
-import { ArrowDown, ArrowUp, ArrowUpDown, Rocket } from "lucide-react"
|
|
|
+import {
|
|
|
+ ArrowDown,
|
|
|
+ ArrowUp,
|
|
|
+ ArrowUpDown,
|
|
|
+ Combine,
|
|
|
+ Ellipsis,
|
|
|
+ LoaderCircle,
|
|
|
+ Rocket,
|
|
|
+ RotateCcw,
|
|
|
+ Trash2,
|
|
|
+ X,
|
|
|
+} from "lucide-react"
|
|
|
+import { toast } from "sonner"
|
|
|
|
|
|
import type { Run, TaskMetrics } from "@roo-code/evals"
|
|
|
import type { ToolName } from "@roo-code/types"
|
|
|
|
|
|
+import { deleteIncompleteRuns, deleteOldRuns } from "@/actions/runs"
|
|
|
import {
|
|
|
+ AlertDialog,
|
|
|
+ AlertDialogAction,
|
|
|
+ AlertDialogCancel,
|
|
|
+ AlertDialogContent,
|
|
|
+ AlertDialogDescription,
|
|
|
+ AlertDialogFooter,
|
|
|
+ AlertDialogHeader,
|
|
|
+ AlertDialogTitle,
|
|
|
Button,
|
|
|
+ DropdownMenu,
|
|
|
+ DropdownMenuContent,
|
|
|
+ DropdownMenuItem,
|
|
|
+ DropdownMenuTrigger,
|
|
|
+ MultiSelect,
|
|
|
+ Select,
|
|
|
+ SelectContent,
|
|
|
+ SelectItem,
|
|
|
+ SelectTrigger,
|
|
|
+ SelectValue,
|
|
|
Table,
|
|
|
TableBody,
|
|
|
TableCell,
|
|
|
@@ -26,6 +57,41 @@ type RunWithTaskMetrics = Run & { taskMetrics: TaskMetrics | null }
|
|
|
type SortColumn = "model" | "provider" | "passed" | "failed" | "percent" | "cost" | "duration" | "createdAt"
|
|
|
type SortDirection = "asc" | "desc"
|
|
|
|
|
|
+type TimeframeOption = "all" | "24h" | "7d" | "30d" | "90d"
|
|
|
+
|
|
|
+const TIMEFRAME_OPTIONS: { value: TimeframeOption; label: string }[] = [
|
|
|
+ { value: "all", label: "All time" },
|
|
|
+ { value: "24h", label: "Last 24 hours" },
|
|
|
+ { value: "7d", label: "Last 7 days" },
|
|
|
+ { value: "30d", label: "Last 30 days" },
|
|
|
+ { value: "90d", label: "Last 90 days" },
|
|
|
+]
|
|
|
+
|
|
|
+// LocalStorage keys
|
|
|
+const STORAGE_KEYS = {
|
|
|
+ TIMEFRAME: "evals-runs-timeframe",
|
|
|
+ MODEL_FILTER: "evals-runs-model-filter",
|
|
|
+ PROVIDER_FILTER: "evals-runs-provider-filter",
|
|
|
+ CONSOLIDATED_TOOLS: "evals-runs-consolidated-tools",
|
|
|
+}
|
|
|
+
|
|
|
+function getTimeframeStartDate(timeframe: TimeframeOption): Date | null {
|
|
|
+ if (timeframe === "all") return null
|
|
|
+ const now = new Date()
|
|
|
+ switch (timeframe) {
|
|
|
+ case "24h":
|
|
|
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
|
|
+ case "7d":
|
|
|
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
|
+ case "30d":
|
|
|
+ return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
|
|
+ case "90d":
|
|
|
+ return new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000)
|
|
|
+ default:
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// Generate abbreviation from tool name (e.g., "read_file" -> "RF", "list_code_definition_names" -> "LCDN")
|
|
|
function getToolAbbreviation(toolName: string): string {
|
|
|
return toolName
|
|
|
@@ -54,6 +120,109 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|
|
const [sortColumn, setSortColumn] = useState<SortColumn | null>("createdAt")
|
|
|
const [sortDirection, setSortDirection] = useState<SortDirection>("desc")
|
|
|
|
|
|
+ // Filter state - initialize from localStorage
|
|
|
+ const [timeframeFilter, setTimeframeFilter] = useState<TimeframeOption>(() => {
|
|
|
+ if (typeof window === "undefined") return "all"
|
|
|
+ const stored = localStorage.getItem(STORAGE_KEYS.TIMEFRAME)
|
|
|
+ return (stored as TimeframeOption) || "all"
|
|
|
+ })
|
|
|
+ const [modelFilter, setModelFilter] = useState<string[]>(() => {
|
|
|
+ if (typeof window === "undefined") return []
|
|
|
+ const stored = localStorage.getItem(STORAGE_KEYS.MODEL_FILTER)
|
|
|
+ return stored ? JSON.parse(stored) : []
|
|
|
+ })
|
|
|
+ const [providerFilter, setProviderFilter] = useState<string[]>(() => {
|
|
|
+ if (typeof window === "undefined") return []
|
|
|
+ const stored = localStorage.getItem(STORAGE_KEYS.PROVIDER_FILTER)
|
|
|
+ return stored ? JSON.parse(stored) : []
|
|
|
+ })
|
|
|
+
|
|
|
+ // Tool column consolidation state - initialize from localStorage
|
|
|
+ const [consolidatedToolColumns, setConsolidatedToolColumns] = useState<string[]>(() => {
|
|
|
+ if (typeof window === "undefined") return []
|
|
|
+ const stored = localStorage.getItem(STORAGE_KEYS.CONSOLIDATED_TOOLS)
|
|
|
+ return stored ? JSON.parse(stored) : []
|
|
|
+ })
|
|
|
+
|
|
|
+ // Delete runs state
|
|
|
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
|
|
+ const [showDeleteOldConfirm, setShowDeleteOldConfirm] = useState(false)
|
|
|
+ const [isDeleting, setIsDeleting] = useState(false)
|
|
|
+
|
|
|
+ // Persist filters to localStorage
|
|
|
+ useEffect(() => {
|
|
|
+ localStorage.setItem(STORAGE_KEYS.TIMEFRAME, timeframeFilter)
|
|
|
+ }, [timeframeFilter])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ localStorage.setItem(STORAGE_KEYS.MODEL_FILTER, JSON.stringify(modelFilter))
|
|
|
+ }, [modelFilter])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ localStorage.setItem(STORAGE_KEYS.PROVIDER_FILTER, JSON.stringify(providerFilter))
|
|
|
+ }, [providerFilter])
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ localStorage.setItem(STORAGE_KEYS.CONSOLIDATED_TOOLS, JSON.stringify(consolidatedToolColumns))
|
|
|
+ }, [consolidatedToolColumns])
|
|
|
+
|
|
|
+ // Count incomplete runs (runs without taskMetricsId)
|
|
|
+ const incompleteRunsCount = useMemo(() => {
|
|
|
+ return runs.filter((run) => run.taskMetrics === null).length
|
|
|
+ }, [runs])
|
|
|
+
|
|
|
+ // Count runs older than 30 days
|
|
|
+ const oldRunsCount = useMemo(() => {
|
|
|
+ const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
|
|
+ return runs.filter((run) => run.createdAt < thirtyDaysAgo).length
|
|
|
+ }, [runs])
|
|
|
+
|
|
|
+ const handleDeleteIncompleteRuns = useCallback(async () => {
|
|
|
+ setIsDeleting(true)
|
|
|
+ try {
|
|
|
+ const result = await deleteIncompleteRuns()
|
|
|
+ if (result.success) {
|
|
|
+ toast.success(`Deleted ${result.deletedCount} incomplete run${result.deletedCount !== 1 ? "s" : ""}`)
|
|
|
+ if (result.storageErrors.length > 0) {
|
|
|
+ toast.warning(`Some storage folders could not be deleted: ${result.storageErrors.length} errors`)
|
|
|
+ }
|
|
|
+ router.refresh()
|
|
|
+ } else {
|
|
|
+ toast.error("Failed to delete incomplete runs")
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Error deleting incomplete runs:", error)
|
|
|
+ toast.error("Failed to delete incomplete runs")
|
|
|
+ } finally {
|
|
|
+ setIsDeleting(false)
|
|
|
+ setShowDeleteConfirm(false)
|
|
|
+ }
|
|
|
+ }, [router])
|
|
|
+
|
|
|
+ const handleDeleteOldRuns = useCallback(async () => {
|
|
|
+ setIsDeleting(true)
|
|
|
+ try {
|
|
|
+ const result = await deleteOldRuns()
|
|
|
+ if (result.success) {
|
|
|
+ toast.success(
|
|
|
+ `Deleted ${result.deletedCount} run${result.deletedCount !== 1 ? "s" : ""} older than 30 days`,
|
|
|
+ )
|
|
|
+ if (result.storageErrors.length > 0) {
|
|
|
+ toast.warning(`Some storage folders could not be deleted: ${result.storageErrors.length} errors`)
|
|
|
+ }
|
|
|
+ router.refresh()
|
|
|
+ } else {
|
|
|
+ toast.error("Failed to delete old runs")
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Error deleting old runs:", error)
|
|
|
+ toast.error("Failed to delete old runs")
|
|
|
+ } finally {
|
|
|
+ setIsDeleting(false)
|
|
|
+ setShowDeleteOldConfirm(false)
|
|
|
+ }
|
|
|
+ }, [router])
|
|
|
+
|
|
|
const handleSort = (column: SortColumn) => {
|
|
|
if (sortColumn === column) {
|
|
|
setSortDirection(sortDirection === "asc" ? "desc" : "asc")
|
|
|
@@ -63,11 +232,59 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Collect all unique tool names from all runs and sort by total attempts
|
|
|
- const toolColumns = useMemo<ToolName[]>(() => {
|
|
|
- const toolTotals = new Map<ToolName, number>()
|
|
|
+ // Derive unique models and providers from runs
|
|
|
+ const modelOptions = useMemo(() => {
|
|
|
+ const models = new Set<string>()
|
|
|
+ for (const run of runs) {
|
|
|
+ if (run.model) models.add(run.model)
|
|
|
+ }
|
|
|
+ return Array.from(models)
|
|
|
+ .sort()
|
|
|
+ .map((model) => ({ label: model, value: model }))
|
|
|
+ }, [runs])
|
|
|
|
|
|
+ const providerOptions = useMemo(() => {
|
|
|
+ const providers = new Set<string>()
|
|
|
for (const run of runs) {
|
|
|
+ const provider = run.settings?.apiProvider
|
|
|
+ if (provider) providers.add(provider)
|
|
|
+ }
|
|
|
+ return Array.from(providers)
|
|
|
+ .sort()
|
|
|
+ .map((provider) => ({ label: provider, value: provider }))
|
|
|
+ }, [runs])
|
|
|
+
|
|
|
+ // Filter runs based on filter state
|
|
|
+ const filteredRuns = useMemo(() => {
|
|
|
+ return runs.filter((run) => {
|
|
|
+ // Timeframe filter
|
|
|
+ const timeframeStart = getTimeframeStartDate(timeframeFilter)
|
|
|
+ if (timeframeStart && run.createdAt < timeframeStart) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // Model filter
|
|
|
+ if (modelFilter.length > 0 && !modelFilter.includes(run.model)) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // Provider filter
|
|
|
+ if (providerFilter.length > 0) {
|
|
|
+ const provider = run.settings?.apiProvider
|
|
|
+ if (!provider || !providerFilter.includes(provider)) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true
|
|
|
+ })
|
|
|
+ }, [runs, timeframeFilter, modelFilter, providerFilter])
|
|
|
+
|
|
|
+ // Collect all unique tool names from filtered runs and sort by total attempts
|
|
|
+ const allToolColumns = useMemo<ToolName[]>(() => {
|
|
|
+ const toolTotals = new Map<ToolName, number>()
|
|
|
+
|
|
|
+ for (const run of filteredRuns) {
|
|
|
if (run.taskMetrics?.toolUsage) {
|
|
|
for (const [toolName, usage] of Object.entries(run.taskMetrics.toolUsage)) {
|
|
|
const tool = toolName as ToolName
|
|
|
@@ -81,13 +298,32 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|
|
return Array.from(toolTotals.entries())
|
|
|
.sort((a, b) => b[1] - a[1])
|
|
|
.map(([name]): ToolName => name)
|
|
|
- }, [runs])
|
|
|
+ }, [filteredRuns])
|
|
|
+
|
|
|
+ // Tool column options for the consolidation dropdown
|
|
|
+ const toolColumnOptions = useMemo(() => {
|
|
|
+ return allToolColumns.map((tool) => ({
|
|
|
+ label: tool,
|
|
|
+ value: tool,
|
|
|
+ }))
|
|
|
+ }, [allToolColumns])
|
|
|
+
|
|
|
+ // Separate consolidated and individual tool columns
|
|
|
+ const individualToolColumns = useMemo(() => {
|
|
|
+ return allToolColumns.filter((tool) => !consolidatedToolColumns.includes(tool))
|
|
|
+ }, [allToolColumns, consolidatedToolColumns])
|
|
|
|
|
|
- // Sort runs based on current sort column and direction
|
|
|
+ // Create a "consolidated" column if any tools are selected for consolidation
|
|
|
+ const hasConsolidatedColumn = consolidatedToolColumns.length > 0
|
|
|
+
|
|
|
+ // Use individualToolColumns for rendering
|
|
|
+ const toolColumns = individualToolColumns
|
|
|
+
|
|
|
+ // Sort filtered runs based on current sort column and direction
|
|
|
const sortedRuns = useMemo(() => {
|
|
|
- if (!sortColumn) return runs
|
|
|
+ if (!sortColumn) return filteredRuns
|
|
|
|
|
|
- return [...runs].sort((a, b) => {
|
|
|
+ return [...filteredRuns].sort((a, b) => {
|
|
|
let aVal: string | number | Date | null = null
|
|
|
let bVal: string | number | Date | null = null
|
|
|
|
|
|
@@ -139,14 +375,170 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|
|
|
|
|
return sortDirection === "asc" ? comparison : -comparison
|
|
|
})
|
|
|
- }, [runs, sortColumn, sortDirection])
|
|
|
+ }, [filteredRuns, sortColumn, sortDirection])
|
|
|
|
|
|
- // Calculate colSpan for empty state (7 base columns + dynamic tools + 3 end columns)
|
|
|
- const totalColumns = 7 + toolColumns.length + 3
|
|
|
+ // Calculate colSpan for empty state (7 base columns + dynamic tools + consolidated column + 3 end columns)
|
|
|
+ const totalColumns = 7 + toolColumns.length + (hasConsolidatedColumn ? 1 : 0) + 3
|
|
|
+
|
|
|
+ // Check if any filters or settings are active
|
|
|
+ const hasActiveFilters = timeframeFilter !== "all" || modelFilter.length > 0 || providerFilter.length > 0
|
|
|
+ const hasConsolidatedTools = consolidatedToolColumns.length > 0
|
|
|
+ const hasAnyCustomization = hasActiveFilters || hasConsolidatedTools
|
|
|
+
|
|
|
+ const clearAllFilters = () => {
|
|
|
+ setTimeframeFilter("all")
|
|
|
+ setModelFilter([])
|
|
|
+ setProviderFilter([])
|
|
|
+ }
|
|
|
+
|
|
|
+ const resetAll = () => {
|
|
|
+ setTimeframeFilter("all")
|
|
|
+ setModelFilter([])
|
|
|
+ setProviderFilter([])
|
|
|
+ setConsolidatedToolColumns([])
|
|
|
+ localStorage.removeItem(STORAGE_KEYS.TIMEFRAME)
|
|
|
+ localStorage.removeItem(STORAGE_KEYS.MODEL_FILTER)
|
|
|
+ localStorage.removeItem(STORAGE_KEYS.PROVIDER_FILTER)
|
|
|
+ localStorage.removeItem(STORAGE_KEYS.CONSOLIDATED_TOOLS)
|
|
|
+ }
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
- <Table className="border border-t-0">
|
|
|
+ {/* Filter Controls */}
|
|
|
+ <div className="flex items-center gap-4 p-4 border border-b-0 rounded-t-md bg-muted/30">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-sm font-medium text-muted-foreground">Timeframe:</span>
|
|
|
+ <Select
|
|
|
+ value={timeframeFilter}
|
|
|
+ onValueChange={(value) => setTimeframeFilter(value as TimeframeOption)}>
|
|
|
+ <SelectTrigger className="w-[140px]">
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ {TIMEFRAME_OPTIONS.map((option) => (
|
|
|
+ <SelectItem key={option.value} value={option.value}>
|
|
|
+ {option.label}
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-sm font-medium text-muted-foreground">Model:</span>
|
|
|
+ <MultiSelect
|
|
|
+ options={modelOptions}
|
|
|
+ value={modelFilter}
|
|
|
+ onValueChange={setModelFilter}
|
|
|
+ placeholder="All models"
|
|
|
+ className="w-[200px]"
|
|
|
+ maxCount={1}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-sm font-medium text-muted-foreground">Provider:</span>
|
|
|
+ <MultiSelect
|
|
|
+ options={providerOptions}
|
|
|
+ value={providerFilter}
|
|
|
+ onValueChange={setProviderFilter}
|
|
|
+ placeholder="All providers"
|
|
|
+ className="w-[180px]"
|
|
|
+ maxCount={1}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Tooltip>
|
|
|
+ <TooltipTrigger asChild>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <Combine className="h-4 w-4 text-muted-foreground" />
|
|
|
+ <span className="text-sm font-medium text-muted-foreground">Consolidate:</span>
|
|
|
+ </div>
|
|
|
+ </TooltipTrigger>
|
|
|
+ <TooltipContent>Select tool columns to consolidate into a combined column</TooltipContent>
|
|
|
+ </Tooltip>
|
|
|
+ <div className="relative min-w-[100px] w-fit max-w-[140px]">
|
|
|
+ <div className={consolidatedToolColumns.length > 0 ? "[&>div>div]:invisible" : ""}>
|
|
|
+ <MultiSelect
|
|
|
+ options={toolColumnOptions}
|
|
|
+ value={consolidatedToolColumns}
|
|
|
+ onValueChange={setConsolidatedToolColumns}
|
|
|
+ placeholder="None"
|
|
|
+ className="w-full min-w-[100px]"
|
|
|
+ maxCount={0}
|
|
|
+ popoverAutoWidth
|
|
|
+ footer={
|
|
|
+ hasAnyCustomization && (
|
|
|
+ <Button
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ className="w-full justify-start text-muted-foreground hover:text-foreground"
|
|
|
+ onClick={resetAll}>
|
|
|
+ <RotateCcw className="h-4 w-4 mr-2" />
|
|
|
+ Reset all filters & consolidation
|
|
|
+ </Button>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ {consolidatedToolColumns.length > 0 && (
|
|
|
+ <div className="absolute inset-0 flex items-center px-3 pointer-events-none">
|
|
|
+ <span className="text-sm font-medium whitespace-nowrap">
|
|
|
+ {consolidatedToolColumns.length} tool
|
|
|
+ {consolidatedToolColumns.length !== 1 ? "s" : ""}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {hasActiveFilters && (
|
|
|
+ <Button variant="ghost" size="sm" onClick={clearAllFilters}>
|
|
|
+ <X className="h-4 w-4 mr-1" />
|
|
|
+ Clear filters
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className="flex items-center gap-2 ml-auto">
|
|
|
+ {/* Bulk Actions Menu */}
|
|
|
+ {(incompleteRunsCount > 0 || oldRunsCount > 0) && (
|
|
|
+ <DropdownMenu>
|
|
|
+ <Button variant="ghost" size="sm" asChild>
|
|
|
+ <DropdownMenuTrigger disabled={isDeleting}>
|
|
|
+ <Ellipsis className="h-4 w-4" />
|
|
|
+ </DropdownMenuTrigger>
|
|
|
+ </Button>
|
|
|
+ <DropdownMenuContent align="end">
|
|
|
+ {incompleteRunsCount > 0 && (
|
|
|
+ <DropdownMenuItem
|
|
|
+ onClick={() => setShowDeleteConfirm(true)}
|
|
|
+ disabled={isDeleting}
|
|
|
+ className="text-destructive focus:text-destructive">
|
|
|
+ <Trash2 className="h-4 w-4 mr-2" />
|
|
|
+ Delete {incompleteRunsCount} incomplete run
|
|
|
+ {incompleteRunsCount !== 1 ? "s" : ""}
|
|
|
+ </DropdownMenuItem>
|
|
|
+ )}
|
|
|
+ {oldRunsCount > 0 && (
|
|
|
+ <DropdownMenuItem
|
|
|
+ onClick={() => setShowDeleteOldConfirm(true)}
|
|
|
+ disabled={isDeleting}
|
|
|
+ className="text-destructive focus:text-destructive">
|
|
|
+ <Trash2 className="h-4 w-4 mr-2" />
|
|
|
+ Delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""} over 30d
|
|
|
+ </DropdownMenuItem>
|
|
|
+ )}
|
|
|
+ </DropdownMenuContent>
|
|
|
+ </DropdownMenu>
|
|
|
+ )}
|
|
|
+ <div className="text-sm text-muted-foreground">
|
|
|
+ {filteredRuns.length} of {runs.length} runs
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Table className="border border-t-0 rounded-t-none">
|
|
|
<TableHeader>
|
|
|
<TableRow>
|
|
|
<TableHead
|
|
|
@@ -188,6 +580,23 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|
|
</div>
|
|
|
</TableHead>
|
|
|
<TableHead>Tokens</TableHead>
|
|
|
+ {hasConsolidatedColumn && (
|
|
|
+ <TableHead className="text-xs text-center">
|
|
|
+ <Tooltip>
|
|
|
+ <TooltipTrigger>
|
|
|
+ <Combine className="h-3 w-3 inline" />
|
|
|
+ </TooltipTrigger>
|
|
|
+ <TooltipContent>
|
|
|
+ <div className="text-xs">
|
|
|
+ <div className="font-semibold mb-1">Consolidated Tools:</div>
|
|
|
+ {consolidatedToolColumns.map((tool) => (
|
|
|
+ <div key={tool}>{tool}</div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </TooltipContent>
|
|
|
+ </Tooltip>
|
|
|
+ </TableHead>
|
|
|
+ )}
|
|
|
{toolColumns.map((toolName) => (
|
|
|
<TableHead key={toolName} className="text-xs text-center">
|
|
|
<Tooltip>
|
|
|
@@ -214,16 +623,34 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|
|
<TableBody>
|
|
|
{sortedRuns.length ? (
|
|
|
sortedRuns.map(({ taskMetrics, ...run }) => (
|
|
|
- <Row key={run.id} run={run} taskMetrics={taskMetrics} toolColumns={toolColumns} />
|
|
|
+ <Row
|
|
|
+ key={run.id}
|
|
|
+ run={run}
|
|
|
+ taskMetrics={taskMetrics}
|
|
|
+ toolColumns={toolColumns}
|
|
|
+ consolidatedToolColumns={consolidatedToolColumns}
|
|
|
+ />
|
|
|
))
|
|
|
) : (
|
|
|
<TableRow>
|
|
|
- <TableCell colSpan={totalColumns} className="text-center">
|
|
|
- No eval runs yet.
|
|
|
- <Button variant="link" onClick={() => router.push("/runs/new")}>
|
|
|
- Launch
|
|
|
- </Button>
|
|
|
- one now.
|
|
|
+ <TableCell colSpan={totalColumns} className="text-center py-8">
|
|
|
+ {runs.length === 0 ? (
|
|
|
+ <>
|
|
|
+ No eval runs yet.
|
|
|
+ <Button variant="link" onClick={() => router.push("/runs/new")}>
|
|
|
+ Launch
|
|
|
+ </Button>
|
|
|
+ one now.
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ No runs match the current filters.
|
|
|
+ <Button variant="link" onClick={clearAllFilters}>
|
|
|
+ Clear filters
|
|
|
+ </Button>
|
|
|
+ to see all runs.
|
|
|
+ </>
|
|
|
+ )}
|
|
|
</TableCell>
|
|
|
</TableRow>
|
|
|
)}
|
|
|
@@ -235,6 +662,70 @@ export function Runs({ runs }: { runs: RunWithTaskMetrics[] }) {
|
|
|
onClick={() => router.push("/runs/new")}>
|
|
|
<Rocket className="size-6" />
|
|
|
</Button>
|
|
|
+
|
|
|
+ {/* Delete Incomplete Runs Confirmation Dialog */}
|
|
|
+ <AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
|
|
+ <AlertDialogContent>
|
|
|
+ <AlertDialogHeader>
|
|
|
+ <AlertDialogTitle>Delete Incomplete Runs</AlertDialogTitle>
|
|
|
+ <AlertDialogDescription>
|
|
|
+ Are you sure you want to delete {incompleteRunsCount} incomplete run
|
|
|
+ {incompleteRunsCount !== 1 ? "s" : ""}? This will permanently remove all database records
|
|
|
+ and storage folders for these runs. This action cannot be undone.
|
|
|
+ </AlertDialogDescription>
|
|
|
+ </AlertDialogHeader>
|
|
|
+ <AlertDialogFooter>
|
|
|
+ <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
|
|
+ <AlertDialogAction
|
|
|
+ onClick={handleDeleteIncompleteRuns}
|
|
|
+ disabled={isDeleting}
|
|
|
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
|
+ {isDeleting ? (
|
|
|
+ <>
|
|
|
+ <LoaderCircle className="h-4 w-4 mr-2 animate-spin" />
|
|
|
+ Deleting...
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ Delete {incompleteRunsCount} run{incompleteRunsCount !== 1 ? "s" : ""}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </AlertDialogAction>
|
|
|
+ </AlertDialogFooter>
|
|
|
+ </AlertDialogContent>
|
|
|
+ </AlertDialog>
|
|
|
+
|
|
|
+ {/* Delete Old Runs Confirmation Dialog */}
|
|
|
+ <AlertDialog open={showDeleteOldConfirm} onOpenChange={setShowDeleteOldConfirm}>
|
|
|
+ <AlertDialogContent>
|
|
|
+ <AlertDialogHeader>
|
|
|
+ <AlertDialogTitle>Delete Old Runs</AlertDialogTitle>
|
|
|
+ <AlertDialogDescription>
|
|
|
+ Are you sure you want to delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""} older than
|
|
|
+ 30 days? This will permanently remove all database records and storage folders for these
|
|
|
+ runs. This action cannot be undone.
|
|
|
+ </AlertDialogDescription>
|
|
|
+ </AlertDialogHeader>
|
|
|
+ <AlertDialogFooter>
|
|
|
+ <AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
|
|
+ <AlertDialogAction
|
|
|
+ onClick={handleDeleteOldRuns}
|
|
|
+ disabled={isDeleting}
|
|
|
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
|
+ {isDeleting ? (
|
|
|
+ <>
|
|
|
+ <LoaderCircle className="h-4 w-4 mr-2 animate-spin" />
|
|
|
+ Deleting...
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ Delete {oldRunsCount} run{oldRunsCount !== 1 ? "s" : ""}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </AlertDialogAction>
|
|
|
+ </AlertDialogFooter>
|
|
|
+ </AlertDialogContent>
|
|
|
+ </AlertDialog>
|
|
|
</>
|
|
|
)
|
|
|
}
|