mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
style(runs): tighten stages, graph toolbar, billing, and stage sidebar
run-stages: - replace the full-width tinted System/Assistant cards with a subtler left-accent bar and header so dense streams read cleanly - unify the Running/Timed out/exit/duration indicators under a shared StatusPill, and bump stdout/stderr labels from 10px to 11px - raise the selected stage header from text-sm font-medium to text-base font-semibold - command output preformatted text is now text-sm on mobile run-overview and run-graph: - extract the floating direction/fit/zoom controls into a single GraphToolbar capsule with internal dividers, shared between both graph views - drop the translucent canvas in favor of solid bg-panel-alt - wrap the "no workflow graph" message in a proper empty-state panel run-billing: - tfoot now uses bg-overlay so totals read heavier than the body - table headers gain font-medium and a readable fg-3 instead of fg-muted - "By model" heading is a real section heading, not an eyebrow - add an empty state when a run has no billing yet stage-sidebar: - cancelled stages now use NoSymbolIcon so they don't look identical to failed stages at a glance Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b5b84da64
commit
174c62e2f7
6 changed files with 208 additions and 175 deletions
84
apps/fabro-web/app/components/graph-toolbar.tsx
Normal file
84
apps/fabro-web/app/components/graph-toolbar.tsx
Normal file
|
|
@ -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 (
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-label="Graph controls"
|
||||
className="absolute right-3 top-3 z-10 flex items-center rounded-md border border-line bg-panel p-0.5"
|
||||
>
|
||||
<div className={group}>
|
||||
<button
|
||||
type="button"
|
||||
title="Left to right"
|
||||
onClick={() => setDirection("LR")}
|
||||
aria-pressed={direction === "LR"}
|
||||
className={`${btn} ${direction === "LR" ? btnActive : ""}`}
|
||||
>
|
||||
<ArrowRightIcon className="size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Top to bottom"
|
||||
onClick={() => setDirection("TB")}
|
||||
aria-pressed={direction === "TB"}
|
||||
className={`${btn} ${direction === "TB" ? btnActive : ""}`}
|
||||
>
|
||||
<ArrowDownIcon className="size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className={group}>
|
||||
<button type="button" title="Fit to window" onClick={fitToWindow} className={btn}>
|
||||
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
|
||||
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className={group}>
|
||||
<button
|
||||
type="button"
|
||||
title="Zoom out"
|
||||
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
|
||||
disabled={zoomIndex === 0}
|
||||
className={`${btn} ${btnDisabled}`}
|
||||
>
|
||||
<MinusIcon className="size-4" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Zoom in"
|
||||
onClick={() => setZoomIndex((i) => Math.min(GRAPH_ZOOM_STEPS.length - 1, i + 1))}
|
||||
disabled={zoomIndex === GRAPH_ZOOM_STEPS.length - 1}
|
||||
className={`${btn} ${btnDisabled}`}
|
||||
>
|
||||
<PlusIcon className="size-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<StageStatus, { icon: typeof CheckCircleIcon; c
|
|||
running: { icon: ArrowPathIcon, color: "text-teal-500" },
|
||||
pending: { icon: PauseCircleIcon, color: "text-fg-muted" },
|
||||
failed: { icon: XCircleIcon, color: "text-coral" },
|
||||
cancelled: { icon: XCircleIcon, color: "text-fg-muted" },
|
||||
cancelled: { icon: NoSymbolIcon, color: "text-fg-muted" },
|
||||
};
|
||||
|
||||
interface StageSidebarProps {
|
||||
|
|
|
|||
|
|
@ -39,12 +39,21 @@ export async function loader({ request, params }: any) {
|
|||
export default function RunBilling({ loaderData }: any) {
|
||||
const { stages, totalRuntime, totalUsdMicros, totalInput, totalOutput, modelBreakdown } =
|
||||
loaderData;
|
||||
|
||||
if (!stages.length) {
|
||||
return (
|
||||
<div className="rounded-md border border-line bg-panel-alt p-10 text-center">
|
||||
<p className="text-sm/6 text-fg-3">No billing recorded for this run yet.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="overflow-hidden rounded-md border border-line">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
|
||||
<tr className="border-b border-line bg-panel/60 text-left text-xs font-medium text-fg-3">
|
||||
<th className="px-4 py-2.5 font-medium">Stage</th>
|
||||
<th className="px-4 py-2.5 font-medium">Model</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
|
||||
|
|
@ -69,7 +78,7 @@ export default function RunBilling({ loaderData }: any) {
|
|||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t border-line-strong bg-panel/40">
|
||||
<tr className="border-t border-line-strong bg-overlay">
|
||||
<td className="px-4 py-3 font-medium text-fg">Total</td>
|
||||
<td />
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg">
|
||||
|
|
@ -88,13 +97,11 @@ export default function RunBilling({ loaderData }: any) {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-wider text-fg-muted">
|
||||
By Model
|
||||
</h3>
|
||||
<h3 className="mb-3 text-sm font-semibold text-fg">By model</h3>
|
||||
<div className="overflow-hidden rounded-md border border-line">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-panel/60 text-left text-xs text-fg-muted">
|
||||
<tr className="border-b border-line bg-panel/60 text-left text-xs font-medium text-fg-3">
|
||||
<th className="px-4 py-2.5 font-medium">Model</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Stages</th>
|
||||
<th className="px-4 py-2.5 font-medium text-right">Tokens</th>
|
||||
|
|
@ -119,7 +126,7 @@ export default function RunBilling({ loaderData }: any) {
|
|||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t border-line-strong bg-panel/40">
|
||||
<tr className="border-t border-line-strong bg-overlay">
|
||||
<td className="px-4 py-3 font-medium text-fg">Total</td>
|
||||
<td className="px-4 py-3 text-right font-mono text-xs tabular-nums font-medium text-fg">
|
||||
{stages.length}
|
||||
|
|
|
|||
|
|
@ -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<typeof getGraph
|
|||
|
||||
}
|
||||
|
||||
const ZOOM_STEPS = [25, 50, 75, 100, 150, 200];
|
||||
const DEFAULT_ZOOM_INDEX = 2;
|
||||
|
||||
export default function RunGraph({ loaderData }: any) {
|
||||
const { id } = useParams();
|
||||
const { stages, graphSvg } = loaderData;
|
||||
|
|
@ -179,11 +180,11 @@ export default function RunGraph({ loaderData }: any) {
|
|||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [zoomIndex, setZoomIndex] = useState(DEFAULT_ZOOM_INDEX);
|
||||
const [zoomIndex, setZoomIndex] = useState(GRAPH_DEFAULT_ZOOM_INDEX);
|
||||
const [direction, setDirection] = useState<Direction>("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) {
|
|||
<StageSidebar stages={stages} runId={id!} activeLink="graph" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="graph-svg relative rounded-md border border-line bg-panel-alt/40">
|
||||
<div className="absolute right-3 top-3 z-10 flex items-center gap-2">
|
||||
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
title="Left to right"
|
||||
onClick={() => setDirection("LR")}
|
||||
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
|
||||
>
|
||||
<ArrowRightIcon className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Top to bottom"
|
||||
onClick={() => setDirection("TB")}
|
||||
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
|
||||
>
|
||||
<ArrowDownIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center rounded-md border border-line bg-panel/90 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
title="Fit to window"
|
||||
onClick={fitToWindow}
|
||||
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
|
||||
>
|
||||
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
|
||||
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
title="Zoom out"
|
||||
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
|
||||
disabled={zoomIndex === 0}
|
||||
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
|
||||
>
|
||||
<MinusIcon className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Zoom in"
|
||||
onClick={() => setZoomIndex((i) => Math.min(ZOOM_STEPS.length - 1, i + 1))}
|
||||
disabled={zoomIndex === ZOOM_STEPS.length - 1}
|
||||
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="graph-svg relative rounded-md border border-line bg-panel-alt">
|
||||
<GraphToolbar
|
||||
direction={direction}
|
||||
setDirection={setDirection}
|
||||
fitToWindow={fitToWindow}
|
||||
zoomIndex={zoomIndex}
|
||||
setZoomIndex={setZoomIndex}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { ArrowDownIcon, ArrowRightIcon, MinusIcon, PlusIcon } from "@heroicons/react/20/solid";
|
||||
import { apiFetch, apiJsonOrNull } from "../api";
|
||||
import { isVisibleStage } from "../data/runs";
|
||||
import { formatDurationSecs } from "../lib/format";
|
||||
|
|
@ -8,6 +7,11 @@ import { useTheme } from "../lib/theme";
|
|||
import { getGraphTheme } from "../lib/graph-theme";
|
||||
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 };
|
||||
|
|
@ -33,9 +37,6 @@ export async function loader({ request, params }: any) {
|
|||
return { stages, graphSvg, runStatus };
|
||||
}
|
||||
|
||||
const ZOOM_STEPS = [25, 50, 75, 100, 150, 200];
|
||||
const DEFAULT_ZOOM_INDEX = 2;
|
||||
|
||||
type Direction = "LR" | "TB";
|
||||
|
||||
export default function RunOverview({ loaderData }: any) {
|
||||
|
|
@ -46,11 +47,11 @@ export default function RunOverview({ loaderData }: any) {
|
|||
const svgRef = useRef<SVGSVGElement | null>(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<Direction>("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) {
|
|||
|
||||
<div className="min-w-0 flex-1">
|
||||
{graphSvg ? (
|
||||
<div className="graph-svg relative rounded-md border border-line bg-panel-alt/40">
|
||||
<div className="absolute right-3 top-3 z-10 flex items-center gap-2">
|
||||
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
title="Left to right"
|
||||
onClick={() => setDirection("LR")}
|
||||
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "LR" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
|
||||
>
|
||||
<ArrowRightIcon className="size-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Top to bottom"
|
||||
onClick={() => setDirection("TB")}
|
||||
className={`flex size-7 items-center justify-center rounded transition-colors ${direction === "TB" ? "bg-overlay-strong text-fg-3" : "text-fg-muted hover:bg-overlay hover:text-fg-3"}`}
|
||||
>
|
||||
<ArrowDownIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center rounded-md border border-line bg-panel/90 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
title="Fit to window"
|
||||
onClick={fitToWindow}
|
||||
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3"
|
||||
>
|
||||
<svg viewBox="0 0 14 14" fill="none" stroke="currentColor" className="size-3.5" aria-hidden="true">
|
||||
<rect x="1" y="1" width="12" height="12" rx="1.5" strokeWidth="1.5" strokeDasharray="3 2" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-0.5 rounded-md border border-line bg-panel/90 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
title="Zoom out"
|
||||
onClick={() => setZoomIndex((i) => Math.max(0, i - 1))}
|
||||
disabled={zoomIndex === 0}
|
||||
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
|
||||
>
|
||||
<MinusIcon className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="Zoom in"
|
||||
onClick={() => setZoomIndex((i) => Math.min(ZOOM_STEPS.length - 1, i + 1))}
|
||||
disabled={zoomIndex === ZOOM_STEPS.length - 1}
|
||||
className="flex size-7 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-3 disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-fg-muted"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="graph-svg relative rounded-md border border-line bg-panel-alt">
|
||||
<GraphToolbar
|
||||
direction={direction}
|
||||
setDirection={setDirection}
|
||||
fitToWindow={fitToWindow}
|
||||
zoomIndex={zoomIndex}
|
||||
setZoomIndex={setZoomIndex}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
|
@ -276,7 +230,9 @@ export default function RunOverview({ loaderData }: any) {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-fg-muted">No workflow graph available.</p>
|
||||
<div className="rounded-md border border-line bg-panel-alt p-10 text-center">
|
||||
<p className="text-sm/6 text-fg-3">No workflow graph available.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -176,28 +176,52 @@ function Markdown({ content }: { content: string }) {
|
|||
|
||||
function SystemBlock({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="rounded-md border border-amber/10 bg-amber/5 overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<section className="border-l-2 border-amber/50 pl-4">
|
||||
<header className="mb-1.5 flex items-center gap-2">
|
||||
<CommandLineIcon className="size-4 shrink-0 text-amber" />
|
||||
<span className="text-xs font-medium text-fg-3">System Prompt</span>
|
||||
</div>
|
||||
<div className="border-t border-line px-3 py-2.5">
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-fg-3">System prompt</span>
|
||||
</header>
|
||||
<Markdown content={content} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function AssistantBlock({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="rounded-md border border-teal-500/10 bg-teal-500/5 overflow-hidden">
|
||||
<div className="flex items-center gap-2 px-3 py-2">
|
||||
<section className="border-l-2 border-teal-500/50 pl-4">
|
||||
<header className="mb-1.5 flex items-center gap-2">
|
||||
<ChatBubbleLeftIcon className="size-4 shrink-0 text-teal-500" />
|
||||
<span className="text-xs font-medium text-fg-3">Assistant</span>
|
||||
</div>
|
||||
<div className="border-t border-line px-3 py-2.5">
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</header>
|
||||
<Markdown content={content} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<span className={`rounded px-1.5 py-0.5 text-[11px] font-medium ${toneClass}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StreamLabel({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="mb-1 font-mono text-[11px] uppercase tracking-wider text-fg-muted">
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -215,46 +239,48 @@ function CommandBlock({ turn }: { turn: Extract<TurnType, { kind: "command" }> }
|
|||
<span className="text-xs font-medium text-fg-3">
|
||||
{turn.language === "python" ? "Python" : "Shell"}
|
||||
</span>
|
||||
{turn.running && (
|
||||
<span className="ml-auto text-[11px] font-medium text-teal-500 animate-pulse">Running...</span>
|
||||
)}
|
||||
{!turn.running && turn.timedOut && (
|
||||
<span className="ml-auto rounded bg-coral/15 px-1.5 py-0.5 text-[11px] font-medium text-coral">Timed out</span>
|
||||
)}
|
||||
{!turn.running && !turn.timedOut && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className={`rounded px-1.5 py-0.5 text-[11px] font-medium ${failed ? "bg-coral/15 text-coral" : "bg-mint/15 text-mint"}`}>
|
||||
exit {turn.exitCode ?? "?"}
|
||||
</span>
|
||||
{turn.durationMs != null && (
|
||||
<span className="text-[11px] tabular-nums text-fg-muted">
|
||||
{turn.durationMs < 1000 ? `${turn.durationMs}ms` : `${(turn.durationMs / 1000).toFixed(1)}s`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{turn.running ? (
|
||||
<StatusPill tone="running">Running…</StatusPill>
|
||||
) : turn.timedOut ? (
|
||||
<StatusPill tone="failed">Timed out</StatusPill>
|
||||
) : (
|
||||
<>
|
||||
<StatusPill tone={failed ? "failed" : "success"}>
|
||||
exit {turn.exitCode ?? "?"}
|
||||
</StatusPill>
|
||||
{turn.durationMs != null && (
|
||||
<StatusPill tone="neutral">
|
||||
{turn.durationMs < 1000
|
||||
? `${turn.durationMs}ms`
|
||||
: `${(turn.durationMs / 1000).toFixed(1)}s`}
|
||||
</StatusPill>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Script */}
|
||||
{turn.script && (
|
||||
<div className="border-t border-line px-3 py-2.5">
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{turn.script}</pre>
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed text-fg-3 sm:text-xs">{turn.script}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* stdout */}
|
||||
{turn.stdout && (
|
||||
<div className="border-t border-line px-3 py-2.5">
|
||||
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-fg-muted">stdout</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-fg-3">{turn.stdout}</pre>
|
||||
<StreamLabel label="stdout" />
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed text-fg-3 sm:text-xs">{turn.stdout}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* stderr */}
|
||||
{turn.stderr && (
|
||||
<div className="border-t border-line px-3 py-2.5">
|
||||
<div className="mb-1 text-[10px] font-medium uppercase tracking-wider text-fg-muted">stderr</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-coral">{turn.stderr}</pre>
|
||||
<StreamLabel label="stderr" />
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed text-coral sm:text-xs">{turn.stderr}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -303,8 +329,8 @@ export default function RunStages({ loaderData }: any) {
|
|||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<SelectedIcon className={`size-5 ${selectedConfig.color} ${isRunning ? "animate-spin" : ""}`} />
|
||||
<h3 className="text-sm font-medium text-fg">{selectedStage.name}</h3>
|
||||
<span className="font-mono text-xs text-fg-muted">{headerDuration}</span>
|
||||
<h3 className="text-base font-semibold text-fg">{selectedStage.name}</h3>
|
||||
<span className="font-mono text-xs tabular-nums text-fg-muted">{headerDuration}</span>
|
||||
</div>
|
||||
|
||||
{turns.map((turn: TurnType, i: number) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue