diff --git a/apps/fabro-web/app/components/graph-toolbar.tsx b/apps/fabro-web/app/components/graph-toolbar.tsx new file mode 100644 index 000000000..93707c89c --- /dev/null +++ b/apps/fabro-web/app/components/graph-toolbar.tsx @@ -0,0 +1,84 @@ +import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid"; + +type Direction = "LR" | "TB"; + +export const GRAPH_ZOOM_STEPS = [25, 50, 75, 100, 150, 200]; +export const GRAPH_DEFAULT_ZOOM_INDEX = 2; + +export function GraphToolbar({ + direction, + setDirection, + fitToWindow, + zoomIndex, + setZoomIndex, +}: { + direction: Direction; + setDirection: (d: Direction) => void; + fitToWindow: () => void; + zoomIndex: number; + setZoomIndex: (updater: (i: number) => number) => void; +}) { + const group = + "flex items-center gap-0.5 px-0.5 [&:not(:first-child)]:border-l [&:not(:first-child)]:border-line-strong [&:not(:first-child)]:pl-1 [&:not(:first-child)]:ml-1"; + const btn = + "flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-teal-500"; + const btnActive = "bg-overlay-strong text-fg-3"; + const btnDisabled = + "disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"; + + return ( +
+
+ + +
+
+ +
+
+ + +
+
+ ); +} diff --git a/apps/fabro-web/app/components/stage-sidebar.tsx b/apps/fabro-web/app/components/stage-sidebar.tsx index 46c801c76..7bbfe1fe0 100644 --- a/apps/fabro-web/app/components/stage-sidebar.tsx +++ b/apps/fabro-web/app/components/stage-sidebar.tsx @@ -1,6 +1,12 @@ import { useState, useEffect, useRef } from "react"; import { Link, useRevalidator } from "react-router"; -import { CheckCircleIcon, ArrowPathIcon, PauseCircleIcon, XCircleIcon } from "@heroicons/react/24/solid"; +import { + ArrowPathIcon, + CheckCircleIcon, + NoSymbolIcon, + PauseCircleIcon, + XCircleIcon, +} from "@heroicons/react/24/solid"; import { DocumentTextIcon, MapIcon } from "@heroicons/react/24/outline"; import { formatDurationSecs } from "../lib/format"; @@ -19,7 +25,7 @@ export const statusConfig: Record +

No billing recorded for this run yet.

+ + ); + } + return (
- + @@ -69,7 +78,7 @@ export default function RunBilling({ loaderData }: any) { ))} - +
Stage Model Tokens
Total @@ -88,13 +97,11 @@ export default function RunBilling({ loaderData }: any) {
-

- By Model -

+

By model

- + @@ -119,7 +126,7 @@ export default function RunBilling({ loaderData }: any) { ))} - +
Model Stages Tokens
Total {stages.length} diff --git a/apps/fabro-web/app/routes/run-graph.tsx b/apps/fabro-web/app/routes/run-graph.tsx index 40282d60f..9b34a9c51 100644 --- a/apps/fabro-web/app/routes/run-graph.tsx +++ b/apps/fabro-web/app/routes/run-graph.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "react-router"; -import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid"; import { useTheme } from "../lib/theme"; import { getGraphTheme } from "../lib/graph-theme"; import { apiFetch, apiJsonOrNull } from "../api"; @@ -8,6 +7,11 @@ import { isVisibleStage } from "../data/runs"; import { formatDurationSecs } from "../lib/format"; import { StageSidebar } from "../components/stage-sidebar"; import type { Stage } from "../components/stage-sidebar"; +import { + GRAPH_DEFAULT_ZOOM_INDEX, + GRAPH_ZOOM_STEPS, + GraphToolbar, +} from "../components/graph-toolbar"; import type { PaginatedRunStageList } from "@qltysh/fabro-api-client"; export const handle = { wide: true }; @@ -169,9 +173,6 @@ function annotateRunningNodes(svg: SVGSVGElement, gt: ReturnType(null); const svgRef = useRef(null); const [error, setError] = useState(null); - const [zoomIndex, setZoomIndex] = useState(DEFAULT_ZOOM_INDEX); + const [zoomIndex, setZoomIndex] = useState(GRAPH_DEFAULT_ZOOM_INDEX); const [direction, setDirection] = useState("LR"); const [pan, setPan] = useState({ x: 0, y: 0 }); const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null); - const zoom = ZOOM_STEPS[zoomIndex]; + const zoom = GRAPH_ZOOM_STEPS[zoomIndex]; const { theme } = useTheme(); const graphTheme = getGraphTheme(theme); @@ -264,8 +265,8 @@ export default function RunGraph({ loaderData }: any) { const fitPct = Math.min(containerW / svgW, containerH / svgH) * 100; let best = 0; - for (let i = ZOOM_STEPS.length - 1; i >= 0; i--) { - if (ZOOM_STEPS[i] <= fitPct) { best = i; break; } + for (let i = GRAPH_ZOOM_STEPS.length - 1; i >= 0; i--) { + if (GRAPH_ZOOM_STEPS[i] <= fitPct) { best = i; break; } } setZoomIndex(best); setPan({ x: 0, y: 0 }); @@ -280,61 +281,14 @@ export default function RunGraph({ loaderData }: any) {
-
-
-
- - -
- -
- -
- -
- - -
-
+
+
(null); const navigate = useNavigate(); const { theme } = useTheme(); - const [zoomIndex, setZoomIndex] = useState(DEFAULT_ZOOM_INDEX); + const [zoomIndex, setZoomIndex] = useState(GRAPH_DEFAULT_ZOOM_INDEX); const [direction, setDirection] = useState("LR"); const [pan, setPan] = useState({ x: 0, y: 0 }); const dragState = useRef<{ startX: number; startY: number; startPanX: number; startPanY: number } | null>(null); - const zoom = ZOOM_STEPS[zoomIndex]; + const zoom = GRAPH_ZOOM_STEPS[zoomIndex]; // Render SVG with stage annotations useEffect(() => { @@ -190,8 +191,8 @@ export default function RunOverview({ loaderData }: any) { const fitPct = Math.min(containerW / svgW, containerH / svgH) * 100; let best = 0; - for (let i = ZOOM_STEPS.length - 1; i >= 0; i--) { - if (ZOOM_STEPS[i] <= fitPct) { best = i; break; } + for (let i = GRAPH_ZOOM_STEPS.length - 1; i >= 0; i--) { + if (GRAPH_ZOOM_STEPS[i] <= fitPct) { best = i; break; } } setZoomIndex(best); setPan({ x: 0, y: 0 }); @@ -203,61 +204,14 @@ export default function RunOverview({ loaderData }: any) {
{graphSvg ? ( -
-
-
- - -
- -
- -
- -
- - -
-
+
+
) : ( -

No workflow graph available.

+
+

No workflow graph available.

+
)}
diff --git a/apps/fabro-web/app/routes/run-stages.tsx b/apps/fabro-web/app/routes/run-stages.tsx index 0bbff46f0..f390a61dd 100644 --- a/apps/fabro-web/app/routes/run-stages.tsx +++ b/apps/fabro-web/app/routes/run-stages.tsx @@ -176,28 +176,52 @@ function Markdown({ content }: { content: string }) { function SystemBlock({ content }: { content: string }) { return ( -
-
+
+
- System Prompt -
-
- -
-
+ System prompt + + + ); } function AssistantBlock({ content }: { content: string }) { return ( -
-
+
+
Assistant -
-
- -
+ + + + ); +} + +function StatusPill({ + tone, + children, +}: { + tone: "running" | "failed" | "success" | "neutral"; + children: React.ReactNode; +}) { + const toneClass = { + running: "bg-teal-500/15 text-teal-500", + failed: "bg-coral/15 text-coral", + success: "bg-mint/15 text-mint", + neutral: "bg-overlay text-fg-3", + }[tone]; + return ( + + {children} + + ); +} + +function StreamLabel({ label }: { label: string }) { + return ( +
+ {label}
); } @@ -215,46 +239,48 @@ function CommandBlock({ turn }: { turn: Extract } {turn.language === "python" ? "Python" : "Shell"} - {turn.running && ( - Running... - )} - {!turn.running && turn.timedOut && ( - Timed out - )} - {!turn.running && !turn.timedOut && ( -
- - exit {turn.exitCode ?? "?"} - - {turn.durationMs != null && ( - - {turn.durationMs < 1000 ? `${turn.durationMs}ms` : `${(turn.durationMs / 1000).toFixed(1)}s`} - - )} -
- )} +
+ {turn.running ? ( + Running… + ) : turn.timedOut ? ( + Timed out + ) : ( + <> + + exit {turn.exitCode ?? "?"} + + {turn.durationMs != null && ( + + {turn.durationMs < 1000 + ? `${turn.durationMs}ms` + : `${(turn.durationMs / 1000).toFixed(1)}s`} + + )} + + )} +
{/* Script */} {turn.script && (
-
{turn.script}
+
{turn.script}
)} {/* stdout */} {turn.stdout && (
-
stdout
-
{turn.stdout}
+ +
{turn.stdout}
)} {/* stderr */} {turn.stderr && (
-
stderr
-
{turn.stderr}
+ +
{turn.stderr}
)}
@@ -303,8 +329,8 @@ export default function RunStages({ loaderData }: any) {
-

{selectedStage.name}

- {headerDuration} +

{selectedStage.name}

+ {headerDuration}
{turns.map((turn: TurnType, i: number) => {