From b7dfa2b7e470c3b06897ce15223fc0e1143b84e2 Mon Sep 17 00:00:00 2001 From: Shine1i Date: Sun, 22 Feb 2026 06:26:40 +0100 Subject: [PATCH] refactor: extract and modularize execution tabs and helpers for enhanced code reusability and maintainability --- .../executions/execution-columns-tab.tsx | 54 ++ .../executions/execution-data-tab.tsx | 157 ++++ .../executions/execution-overview-tab.tsx | 226 ++++++ .../executions/execution-raw-tab.tsx | 18 + .../executions/execution-sidebar.tsx | 70 ++ .../executions/executions-view-helpers.ts | 175 +++++ .../components/executions/executions-view.tsx | 733 +++--------------- 7 files changed, 818 insertions(+), 615 deletions(-) create mode 100644 studio/frontend/src/features/recipe-studio/components/executions/execution-columns-tab.tsx create mode 100644 studio/frontend/src/features/recipe-studio/components/executions/execution-data-tab.tsx create mode 100644 studio/frontend/src/features/recipe-studio/components/executions/execution-overview-tab.tsx create mode 100644 studio/frontend/src/features/recipe-studio/components/executions/execution-raw-tab.tsx create mode 100644 studio/frontend/src/features/recipe-studio/components/executions/execution-sidebar.tsx create mode 100644 studio/frontend/src/features/recipe-studio/components/executions/executions-view-helpers.ts diff --git a/studio/frontend/src/features/recipe-studio/components/executions/execution-columns-tab.tsx b/studio/frontend/src/features/recipe-studio/components/executions/execution-columns-tab.tsx new file mode 100644 index 0000000000..525dfefba0 --- /dev/null +++ b/studio/frontend/src/features/recipe-studio/components/executions/execution-columns-tab.tsx @@ -0,0 +1,54 @@ +import type { ReactElement } from "react"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { AnalysisColumnStat } from "./executions-view-helpers"; + +type ExecutionColumnsTabProps = { + analysisColumns: AnalysisColumnStat[]; +}; + +export function ExecutionColumnsTab({ + analysisColumns, +}: ExecutionColumnsTabProps): ReactElement { + return ( +
+

Column statistics

+ {analysisColumns.length === 0 ? ( +

No column statistics yet.

+ ) : ( + + + + Column + Type + Data type + Unique + Nulls + Input tok avg + Output tok avg + + + + {analysisColumns.map((column) => ( + + {column.column_name} + {column.column_type} + {column.simple_dtype} + {column.num_unique ?? "--"} + {column.num_null ?? "--"} + {column.input_tokens_mean ?? "--"} + {column.output_tokens_mean ?? "--"} + + ))} + +
+ )} +
+ ); +} diff --git a/studio/frontend/src/features/recipe-studio/components/executions/execution-data-tab.tsx b/studio/frontend/src/features/recipe-studio/components/executions/execution-data-tab.tsx new file mode 100644 index 0000000000..56ca1d5057 --- /dev/null +++ b/studio/frontend/src/features/recipe-studio/components/executions/execution-data-tab.tsx @@ -0,0 +1,157 @@ +import type { ReactElement } from "react"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@/components/ui/button"; +import { DataTable } from "@/components/ui/data-table"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/utils"; +import { isExecutionInProgress } from "../../executions/execution-helpers"; +import type { RecipeExecutionRecord } from "../../execution-types"; +import { formatCellValue, isExpandableCellValue } from "./executions-view-helpers"; + +type ExecutionDataTabProps = { + execution: RecipeExecutionRecord; + datasetColumnNames: string[]; + hiddenDatasetColumns: string[]; + canPageDataset: boolean; + currentDatasetPage: number; + totalPages: number; + tableColumns: ColumnDef>[]; + datasetRowsForTable: Record[]; + visibleDatasetColumnNames: string[]; + expandedDatasetRows: Record; + selectedExecutionIdSafe: string | null; + onSetHiddenColumns: (updater: (current: string[]) => string[]) => void; + onPrevPage: () => void; + onNextPage: () => void; + onToggleRowExpanded: (rowId: string) => void; +}; + +export function ExecutionDataTab({ + execution, + datasetColumnNames, + hiddenDatasetColumns, + canPageDataset, + currentDatasetPage, + totalPages, + tableColumns, + datasetRowsForTable, + visibleDatasetColumnNames, + expandedDatasetRows, + selectedExecutionIdSafe, + onSetHiddenColumns, + onPrevPage, + onNextPage, + onToggleRowExpanded, +}: ExecutionDataTabProps): ReactElement { + return ( +
+
+

Dataset sample

+
+ {datasetColumnNames.length > 0 && ( + + + + + + Visible columns + {datasetColumnNames.map((columnName) => ( + { + event.preventDefault(); + }} + onCheckedChange={(checked) => { + onSetHiddenColumns((currentColumns) => { + if (checked) { + return currentColumns.filter((name) => name !== columnName); + } + return [...currentColumns, columnName]; + }); + }} + > + {columnName} + + ))} + + + )} + {canPageDataset && ( + <> + + Page {currentDatasetPage}/{totalPages} + + + + + )} +
+
+ {execution.dataset.length === 0 ? ( +

No rows returned.

+ ) : tableColumns.length === 0 ? ( +

+ All columns hidden. Use Columns to show at least one. +

+ ) : ( +
+ { + const canExpand = visibleDatasetColumnNames.some((columnName) => + isExpandableCellValue(formatCellValue(row[columnName])), + ); + if (!canExpand) { + return undefined; + } + return cn( + "cursor-pointer", + expandedDatasetRows[rowId] ? "bg-primary/[0.05]" : "hover:bg-primary/[0.06]", + ); + }} + onRowClick={(row, _rowIndex, rowId) => { + const canExpand = visibleDatasetColumnNames.some((columnName) => + isExpandableCellValue(formatCellValue(row[columnName])), + ); + if (!canExpand || !selectedExecutionIdSafe) { + return; + } + onToggleRowExpanded(rowId); + }} + /> +
+ )} +
+ ); +} diff --git a/studio/frontend/src/features/recipe-studio/components/executions/execution-overview-tab.tsx b/studio/frontend/src/features/recipe-studio/components/executions/execution-overview-tab.tsx new file mode 100644 index 0000000000..14037aff4f --- /dev/null +++ b/studio/frontend/src/features/recipe-studio/components/executions/execution-overview-tab.tsx @@ -0,0 +1,226 @@ +import type { ReactElement, RefObject, UIEvent } from "react"; +import { + Database01Icon, + Database02Icon, + Flag02Icon, +} from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { isExecutionInProgress } from "../../executions/execution-helpers"; +import type { RecipeExecutionRecord } from "../../execution-types"; +import type { ModelUsageRow } from "./executions-view-helpers"; +import { formatMetricValue } from "./executions-view-helpers"; + +type ExecutionOverviewTabProps = { + execution: RecipeExecutionRecord; + showSummaryCards: boolean; + recordsMetric: number | null; + totalMetric: number | null; + runDuration: string; + columnCount: number; + llmColumnCount: number; + nullRate: number | null; + sideEffects: string[]; + lowUniquenessColumns: string[]; + modelUsageRows: ModelUsageRow[]; + totalInputTokens: number; + totalOutputTokens: number; + terminalLines: string[]; + terminalRef: RefObject; + onTerminalScroll: (event: UIEvent) => void; +}; + +export function ExecutionOverviewTab({ + execution, + showSummaryCards, + recordsMetric, + totalMetric, + runDuration, + columnCount, + llmColumnCount, + nullRate, + sideEffects, + lowUniquenessColumns, + modelUsageRows, + totalInputTokens, + totalOutputTokens, + terminalLines, + terminalRef, + onTerminalScroll, +}: ExecutionOverviewTabProps): ReactElement { + return ( +
+ {showSummaryCards && ( +
+
+
+
+

Run summary

+ +
+
+

+ Records:{" "} + + {formatMetricValue(recordsMetric)} / {formatMetricValue(totalMetric)} + +

+

+ Duration: {runDuration} +

+

+ Columns analyzed:{" "} + {formatMetricValue(columnCount)} +

+

+ Final stage:{" "} + {execution.stage ?? "--"} +

+
+
+
+
+

Insights

+ +
+
+

+ LLM columns:{" "} + {formatMetricValue(llmColumnCount)} +

+

+ Null rate: {nullRate?.toFixed(1) ?? "--"}% +

+

+ Dropped columns:{" "} + {formatMetricValue(sideEffects.length)} +

+ {sideEffects.length > 0 && ( +
+ {sideEffects.map((name) => ( + + {name} + + ))} +
+ )} +

+ Low uniqueness flags:{" "} + + {formatMetricValue(lowUniquenessColumns.length)} + +

+ {lowUniquenessColumns.length > 0 && ( +
+ {lowUniquenessColumns.slice(0, 3).map((name) => ( + + {name} + + ))} + {lowUniquenessColumns.length > 3 && ( + + +{lowUniquenessColumns.length - 3} more + + )} +
+ )} +
+
+
+
+
+

Model usage

+ +
+ {modelUsageRows.length === 0 ? ( +

No model usage yet.

+ ) : ( +
+
+
+

Total input

+

+ {formatMetricValue(totalInputTokens)} +

+
+
+

Total output

+

+ {formatMetricValue(totalOutputTokens)} +

+
+
+
+ + + + Model + Input + Output + + + + {modelUsageRows.map((usage) => ( + + {usage.model} + + {formatMetricValue(usage.input)} + + + {formatMetricValue(usage.output)} + + + ))} + +
+
+
+ )} +
+
+ )} +
+
+

Terminal output

+

{terminalLines.length} lines

+
+
+ {terminalLines.length === 0 ? ( +

+ {isExecutionInProgress(execution.status) + ? "Waiting for logs..." + : "No logs captured."} +

+ ) : ( + terminalLines.map((line, index) => ( +

+ {line} +

+ )) + )} +
+
+
+ ); +} diff --git a/studio/frontend/src/features/recipe-studio/components/executions/execution-raw-tab.tsx b/studio/frontend/src/features/recipe-studio/components/executions/execution-raw-tab.tsx new file mode 100644 index 0000000000..718722fc67 --- /dev/null +++ b/studio/frontend/src/features/recipe-studio/components/executions/execution-raw-tab.tsx @@ -0,0 +1,18 @@ +import type { ReactElement } from "react"; + +type ExecutionRawTabProps = { + rawExecution: Record | null; +}; + +export function ExecutionRawTab({ + rawExecution, +}: ExecutionRawTabProps): ReactElement { + return ( +
+

Raw execution

+
+        {JSON.stringify(rawExecution, null, 2)}
+      
+
+ ); +} diff --git a/studio/frontend/src/features/recipe-studio/components/executions/execution-sidebar.tsx b/studio/frontend/src/features/recipe-studio/components/executions/execution-sidebar.tsx new file mode 100644 index 0000000000..defdfd4a45 --- /dev/null +++ b/studio/frontend/src/features/recipe-studio/components/executions/execution-sidebar.tsx @@ -0,0 +1,70 @@ +import type { ReactElement } from "react"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import type { RecipeExecutionRecord } from "../../execution-types"; +import { + formatStatus, + formatTimestamp, + statusRightBorder, + statusTone, +} from "./executions-view-helpers"; + +type ExecutionSidebarProps = { + executions: RecipeExecutionRecord[]; + selectedExecutionId: string | null; + onSelectExecution: (id: string) => void; +}; + +export function ExecutionSidebar({ + executions, + selectedExecutionId, + onSelectExecution, +}: ExecutionSidebarProps): ReactElement { + return ( + + ); +} diff --git a/studio/frontend/src/features/recipe-studio/components/executions/executions-view-helpers.ts b/studio/frontend/src/features/recipe-studio/components/executions/executions-view-helpers.ts new file mode 100644 index 0000000000..b5b12f0075 --- /dev/null +++ b/studio/frontend/src/features/recipe-studio/components/executions/executions-view-helpers.ts @@ -0,0 +1,175 @@ +import type { + RecipeExecutionAnalysis, + RecipeExecutionStatus, +} from "../../execution-types"; +import { isExecutionInProgress } from "../../executions/execution-helpers"; + +export type AnalysisColumnStat = { + column_name: string; + column_type: string; + simple_dtype: string; + num_unique: number | null; + num_null: number | null; + input_tokens_mean: number | null; + output_tokens_mean: number | null; +}; + +export type ModelUsageRow = { + model: string; + input: number | null; + output: number | null; +}; + +export const PREVIEW_DATASET_PAGE_SIZE = 20; +export const TERMINAL_STICKY_BOTTOM_THRESHOLD_PX = 24; + +export function formatTimestamp(value: number): string { + return new Date(value).toLocaleString(); +} + +export function formatCellValue(value: unknown): string { + if (value === null || value === undefined) { + return "--"; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + try { + return JSON.stringify(value); + } catch { + return String(value); + } +} + +export function isExpandableCellValue(value: string): boolean { + return value.length > 180; +} + +export function truncateCellValue(value: string): string { + if (value.length <= 180) { + return value; + } + return `${value.slice(0, 180).trimEnd()}...`; +} + +function parseNumber(value: unknown): number | null { + return typeof value === "number" && Number.isFinite(value) ? value : null; +} + +function parseString(value: unknown): string { + return typeof value === "string" && value.length > 0 ? value : "--"; +} + +export function parseAnalysisColumns( + analysis: RecipeExecutionAnalysis | null, +): AnalysisColumnStat[] { + const items = Array.isArray(analysis?.column_statistics) + ? analysis.column_statistics + : []; + return items + .map((item) => { + if (!item || typeof item !== "object" || Array.isArray(item)) { + return null; + } + const row = item as Record; + return { + column_name: parseString(row.column_name), + column_type: parseString(row.column_type), + simple_dtype: parseString(row.simple_dtype), + num_unique: parseNumber(row.num_unique), + num_null: parseNumber(row.num_null), + input_tokens_mean: parseNumber(row.input_tokens_mean), + output_tokens_mean: parseNumber(row.output_tokens_mean), + }; + }) + .filter((item): item is AnalysisColumnStat => item !== null); +} + +export function statusTone(status: RecipeExecutionStatus): string { + if (status === "completed") { + return "bg-emerald-100 text-emerald-700"; + } + if (status === "error" || status === "cancelled") { + return "bg-red-100 text-red-700"; + } + if (isExecutionInProgress(status)) { + return "bg-amber-100 text-amber-700"; + } + return "bg-muted text-muted-foreground"; +} + +export function statusRightBorder(status: RecipeExecutionStatus): string { + if (status === "completed") { + return "border-r-emerald-500"; + } + if (status === "error" || status === "cancelled") { + return "border-r-red-500"; + } + if (isExecutionInProgress(status)) { + return "border-r-amber-500"; + } + return "border-r-border"; +} + +export function formatStatus(status: RecipeExecutionStatus): string { + if (status === "cancelled") { + return "cancelled"; + } + return status; +} + +export function formatPercent(value: number | null | undefined): string { + if (typeof value !== "number" || Number.isNaN(value)) { + return "--"; + } + return `${value.toFixed(1)}%`; +} + +export function formatDuration(startedAt: number, finishedAt: number | null): string { + if (!finishedAt || finishedAt <= startedAt) { + return "--"; + } + const seconds = Math.round((finishedAt - startedAt) / 1000); + return `${seconds}s`; +} + +export function formatMetricValue(value: number | null | undefined): string { + if (typeof value !== "number" || Number.isNaN(value)) { + return "--"; + } + return value.toLocaleString(); +} + +export function parseModelUsageRows( + value: Record | null, +): ModelUsageRow[] { + if (!value) { + return []; + } + return Object.entries(value) + .map(([name, data]) => { + if (!data || typeof data !== "object" || Array.isArray(data)) { + return null; + } + const modelObj = data as Record; + const tokens = + modelObj.tokens && + typeof modelObj.tokens === "object" && + !Array.isArray(modelObj.tokens) + ? (modelObj.tokens as Record) + : null; + const modelName = + typeof modelObj.model === "string" && modelObj.model.length > 0 + ? modelObj.model + : name; + return { + model: modelName, + input: parseNumber(tokens?.input), + output: parseNumber(tokens?.output), + }; + }) + .filter((item): item is ModelUsageRow => item !== null); +} diff --git a/studio/frontend/src/features/recipe-studio/components/executions/executions-view.tsx b/studio/frontend/src/features/recipe-studio/components/executions/executions-view.tsx index 03c09b35d9..5b4f30bb06 100644 --- a/studio/frontend/src/features/recipe-studio/components/executions/executions-view.tsx +++ b/studio/frontend/src/features/recipe-studio/components/executions/executions-view.tsx @@ -2,38 +2,37 @@ import { useEffect, useMemo, useRef, useState, type ReactElement } from "react"; import type { ColumnDef } from "@tanstack/react-table"; import { CheckmarkCircle02Icon, - Database01Icon, - Database02Icon, Flag02Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { DataTable } from "@/components/ui/data-table"; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { Progress } from "@/components/ui/progress"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import type { - RecipeExecutionAnalysis, RecipeExecutionRecord, - RecipeExecutionStatus, } from "../../execution-types"; import { isExecutionInProgress } from "../../executions/execution-helpers"; +import { ExecutionColumnsTab } from "./execution-columns-tab"; +import { ExecutionDataTab } from "./execution-data-tab"; +import { ExecutionOverviewTab } from "./execution-overview-tab"; +import { ExecutionRawTab } from "./execution-raw-tab"; +import { ExecutionSidebar } from "./execution-sidebar"; +import { + PREVIEW_DATASET_PAGE_SIZE, + TERMINAL_STICKY_BOTTOM_THRESHOLD_PX, + formatCellValue, + formatDuration, + formatPercent, + formatStatus, + formatTimestamp, + isExpandableCellValue, + parseAnalysisColumns, + parseModelUsageRows, + statusTone, + truncateCellValue, +} from "./executions-view-helpers"; type ExecutionsViewProps = { executions: RecipeExecutionRecord[]; @@ -44,170 +43,6 @@ type ExecutionsViewProps = { onLoadDatasetPage: (id: string, page: number) => void; }; -type AnalysisColumnStat = { - column_name: string; - column_type: string; - simple_dtype: string; - num_unique: number | null; - num_null: number | null; - input_tokens_mean: number | null; - output_tokens_mean: number | null; -}; - -type ModelUsageRow = { - model: string; - input: number | null; - output: number | null; -}; - -const PREVIEW_DATASET_PAGE_SIZE = 20; -const TERMINAL_STICKY_BOTTOM_THRESHOLD_PX = 24; - -function formatTimestamp(value: number): string { - return new Date(value).toLocaleString(); -} - -function formatCellValue(value: unknown): string { - if (value === null || value === undefined) { - return "--"; - } - if (typeof value === "string") { - return value; - } - if (typeof value === "number" || typeof value === "boolean") { - return String(value); - } - try { - return JSON.stringify(value); - } catch { - return String(value); - } -} - -function isExpandableCellValue(value: string): boolean { - return value.length > 180; -} - -function truncateCellValue(value: string): string { - if (value.length <= 180) { - return value; - } - return `${value.slice(0, 180).trimEnd()}...`; -} - -function parseNumber(value: unknown): number | null { - return typeof value === "number" && Number.isFinite(value) ? value : null; -} - -function parseString(value: unknown): string { - return typeof value === "string" && value.length > 0 ? value : "--"; -} - -function parseAnalysisColumns(analysis: RecipeExecutionAnalysis | null): AnalysisColumnStat[] { - const items = Array.isArray(analysis?.column_statistics) - ? analysis.column_statistics - : []; - return items - .map((item) => { - if (!item || typeof item !== "object" || Array.isArray(item)) { - return null; - } - const row = item as Record; - return { - column_name: parseString(row.column_name), - column_type: parseString(row.column_type), - simple_dtype: parseString(row.simple_dtype), - num_unique: parseNumber(row.num_unique), - num_null: parseNumber(row.num_null), - input_tokens_mean: parseNumber(row.input_tokens_mean), - output_tokens_mean: parseNumber(row.output_tokens_mean), - }; - }) - .filter((item): item is AnalysisColumnStat => item !== null); -} - -function statusTone(status: RecipeExecutionStatus): string { - if (status === "completed") { - return "bg-emerald-100 text-emerald-700"; - } - if (status === "error" || status === "cancelled") { - return "bg-red-100 text-red-700"; - } - if (isExecutionInProgress(status)) { - return "bg-amber-100 text-amber-700"; - } - return "bg-muted text-muted-foreground"; -} - -function statusRightBorder(status: RecipeExecutionStatus): string { - if (status === "completed") { - return "border-r-emerald-500"; - } - if (status === "error" || status === "cancelled") { - return "border-r-red-500"; - } - if (isExecutionInProgress(status)) { - return "border-r-amber-500"; - } - return "border-r-border"; -} - -function formatStatus(status: RecipeExecutionStatus): string { - if (status === "cancelled") { - return "cancelled"; - } - return status; -} - -function formatPercent(value: number | null | undefined): string { - if (typeof value !== "number" || Number.isNaN(value)) { - return "--"; - } - return `${value.toFixed(1)}%`; -} - -function formatDuration(startedAt: number, finishedAt: number | null): string { - if (!finishedAt || finishedAt <= startedAt) { - return "--"; - } - const seconds = Math.round((finishedAt - startedAt) / 1000); - return `${seconds}s`; -} - -function formatMetricValue(value: number | null | undefined): string { - if (typeof value !== "number" || Number.isNaN(value)) { - return "--"; - } - return value.toLocaleString(); -} - -function parseModelUsageRows(value: Record | null): ModelUsageRow[] { - if (!value) { - return []; - } - return Object.entries(value) - .map(([name, data]) => { - if (!data || typeof data !== "object" || Array.isArray(data)) { - return null; - } - const modelObj = data as Record; - const tokens = - modelObj.tokens && typeof modelObj.tokens === "object" && !Array.isArray(modelObj.tokens) - ? (modelObj.tokens as Record) - : null; - const modelName = - typeof modelObj.model === "string" && modelObj.model.length > 0 - ? modelObj.model - : name; - return { - model: modelName, - input: parseNumber(tokens?.input), - output: parseNumber(tokens?.output), - }; - }) - .filter((item): item is ModelUsageRow => item !== null); -} - export function ExecutionsView({ executions, selectedExecutionId, @@ -476,53 +311,11 @@ export function ExecutionsView({ return (
- +
{!selectedExecution ? (
@@ -540,7 +333,9 @@ export function ExecutionsView({ {selectedExecution.rows} rows Started {formatTimestamp(selectedExecution.createdAt)} - Duration {formatDuration(selectedExecution.createdAt, selectedExecution.finishedAt)} + + Duration {formatDuration(selectedExecution.createdAt, selectedExecution.finishedAt)} + {selectedExecution.stage && ( Stage: {selectedExecution.stage} @@ -595,18 +390,10 @@ export function ExecutionsView({ progressComplete ? "text-emerald-900" : "text-amber-900", )} > -

- Done: {selectedExecution.progress?.done ?? "--"} -

-

- Total: {selectedExecution.progress?.total ?? "--"} -

-

- Rate: {selectedExecution.progress?.rate ?? "--"} rec/s -

-

- ETA: {selectedExecution.progress?.eta_sec ?? "--"} s -

+

Done: {selectedExecution.progress?.done ?? "--"}

+

Total: {selectedExecution.progress?.total ?? "--"}

+

Rate: {selectedExecution.progress?.rate ?? "--"} rec/s

+

ETA: {selectedExecution.progress?.eta_sec ?? "--"} s

{selectedExecution.current_column && selectedExecution.column_progress && (

)}

- - {showSummaryCards && ( -
-
-
-
-

Run summary

- -
-
-

- Records:{" "} - - {formatMetricValue(recordsMetric)} / {formatMetricValue(totalMetric)} - -

-

- Duration: {runDuration} -

-

- Columns analyzed:{" "} - - {formatMetricValue(columnCount)} - -

-

- Final stage:{" "} - - {selectedExecution.stage ?? "--"} - -

-
-
-
-
-

Insights

- -
-
-

- LLM columns:{" "} - - {formatMetricValue(llmColumnCount)} - -

-

- Null rate:{" "} - {formatPercent(nullRate)} -

-

- Dropped columns:{" "} - - {formatMetricValue(sideEffects.length)} - -

- {sideEffects.length > 0 && ( -
- {sideEffects.map((name) => ( - - {name} - - ))} -
- )} -

- Low uniqueness flags:{" "} - - {formatMetricValue(lowUniquenessColumns.length)} - -

- {lowUniquenessColumns.length > 0 && ( -
- {lowUniquenessColumns.slice(0, 3).map((name) => ( - - {name} - - ))} - {lowUniquenessColumns.length > 3 && ( - - +{lowUniquenessColumns.length - 3} more - - )} -
- )} -
-
-
-
-
-

Model usage

- -
- {modelUsageRows.length === 0 ? ( -

No model usage yet.

- ) : ( -
-
-
-

Total input

-

- {formatMetricValue(totalInputTokens)} -

-
-
-

Total output

-

- {formatMetricValue(totalOutputTokens)} -

-
-
-
- - - - Model - Input - Output - - - - {modelUsageRows.map((usage) => ( - - - {usage.model} - - - {formatMetricValue(usage.input)} - - - {formatMetricValue(usage.output)} - - - ))} - -
-
-
- )} -
-
- )} -
-
-

Terminal output

-

- {terminalLines.length} lines -

-
-
{ - const element = event.currentTarget; - const distanceFromBottom = - element.scrollHeight - element.scrollTop - element.clientHeight; - shouldStickTerminalToBottomRef.current = - distanceFromBottom <= TERMINAL_STICKY_BOTTOM_THRESHOLD_PX; - }} - > - {terminalLines.length === 0 ? ( -

- {isExecutionInProgress(selectedExecution.status) - ? "Waiting for logs..." - : "No logs captured."} -

- ) : ( - terminalLines.map((line, index) => ( -

- {line} -

- )) - )} -
-
+ + { + const element = event.currentTarget; + const distanceFromBottom = + element.scrollHeight - element.scrollTop - element.clientHeight; + shouldStickTerminalToBottomRef.current = + distanceFromBottom <= TERMINAL_STICKY_BOTTOM_THRESHOLD_PX; + }} + /> - -
-

Column statistics

- {analysisColumns.length === 0 ? ( -

- No column statistics yet. -

- ) : ( - - - - Column - Type - Data type - Unique - Nulls - Input tok avg - Output tok avg - - - - {analysisColumns.map((column) => ( - - {column.column_name} - {column.column_type} - {column.simple_dtype} - {column.num_unique ?? "--"} - {column.num_null ?? "--"} - {column.input_tokens_mean ?? "--"} - {column.output_tokens_mean ?? "--"} - - ))} - -
- )} -
+ + - -
-

Dataset sample

-
- {datasetColumnNames.length > 0 && ( - - - - - - Visible columns - {datasetColumnNames.map((columnName) => ( - { - event.preventDefault(); - }} - onCheckedChange={(checked) => { - const selectedId = selectedExecution?.id; - if (!selectedId) { - return; - } - setHiddenDatasetColumnsByExecution((current) => { - const currentColumns = current[selectedId] ?? []; - const nextColumns = checked - ? currentColumns.filter((name) => name !== columnName) - : [...currentColumns, columnName]; - return { - ...current, - [selectedId]: nextColumns, - }; - }); - }} - > - {columnName} - - ))} - - - )} - {canPageDataset && selectedExecution && ( - <> - - Page {currentDatasetPage}/{totalPages} - - - - - )} -
-
- {selectedExecution.dataset.length === 0 ? ( -

No rows returned.

- ) : tableColumns.length === 0 ? ( -

- All columns hidden. Use Columns to show at least one. -

- ) : ( -
- { - const canExpand = visibleDatasetColumnNames.some((columnName) => - isExpandableCellValue(formatCellValue(row[columnName])), - ); - if (!canExpand) { - return undefined; - } - return cn( - "cursor-pointer", - expandedDatasetRows[rowId] - ? "bg-primary/[0.05]" - : "hover:bg-primary/[0.06]", - ); - }} - onRowClick={(row, _rowIndex, rowId) => { - const canExpand = visibleDatasetColumnNames.some((columnName) => - isExpandableCellValue(formatCellValue(row[columnName])), - ); - if (!canExpand || !selectedExecutionIdSafe) { - return; - } - setExpandedDatasetRowsByExecution((current) => { - const rows = current[selectedExecutionIdSafe] ?? {}; - return { - ...current, - [selectedExecutionIdSafe]: { - ...rows, - [rowId]: !rows[rowId], - }, - }; - }); - }} - /> -
- )} + + { + const selectedId = selectedExecution.id; + setHiddenDatasetColumnsByExecution((current) => { + const currentColumns = current[selectedId] ?? []; + return { + ...current, + [selectedId]: updater(currentColumns), + }; + }); + }} + onPrevPage={() => { + if (selectedExecution.kind === "preview") { + const selectedId = selectedExecution.id; + setPreviewDatasetPageByExecution((current) => ({ + ...current, + [selectedId]: Math.max(1, currentDatasetPage - 1), + })); + return; + } + onLoadDatasetPage(selectedExecution.id, currentDatasetPage - 1); + }} + onNextPage={() => { + if (selectedExecution.kind === "preview") { + const selectedId = selectedExecution.id; + setPreviewDatasetPageByExecution((current) => ({ + ...current, + [selectedId]: Math.min(totalPages, currentDatasetPage + 1), + })); + return; + } + onLoadDatasetPage(selectedExecution.id, currentDatasetPage + 1); + }} + onToggleRowExpanded={(rowId) => { + setExpandedDatasetRowsByExecution((current) => { + const rows = current[selectedExecution.id] ?? {}; + return { + ...current, + [selectedExecution.id]: { + ...rows, + [rowId]: !rows[rowId], + }, + }; + }); + }} + /> - -
-

Raw execution

-
-                      {JSON.stringify(rawExecution, null, 2)}
-                    
-
+ + )}