| Total |
|
@@ -88,13 +97,11 @@ export default function RunBilling({ loaderData }: any) {
-
- By Model
-
+ By model
-
+
| Model |
Stages |
Tokens |
@@ -119,7 +126,7 @@ export default function RunBilling({ loaderData }: any) {
))}
-
+
| 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
+
+
+
);
}
function AssistantBlock({ content }: { content: string }) {
return (
-
-
+
-
-
-
+
+
+
+ );
+}
+
+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) => {
| |