From 138d3d8ce688a22f7382df9504e6bfce6a636dc7 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Sat, 19 Sep 2026 21:55:02 +0300 Subject: [PATCH] Cost control: bare top-right number with dropdown; pipeline pills move under it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner-directed placement wave (2026-09-20): the bottom-left stack overflowed the composer in narrow windows, and the pill chrome was too prominent for a passive number. - The cost surface is now JUST a number (cumulative dollars) top right, below the actions row — no pill container, hover-highlight kept, dropdown breakdown kept, viewport-capped + scrollable. - Same dismiss pattern as the settings gear: outside click or Escape closes (shared usePopoverDismiss; two dropdowns never open at once). - Running-pipeline pills moved under the cost number in the same top-right column — one right-aligned column grows downward from the top, so no window width can push chrome over the composer. --- components/graph-view.tsx | 99 ++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/components/graph-view.tsx b/components/graph-view.tsx index 5c78023..2aa6609 100644 --- a/components/graph-view.tsx +++ b/components/graph-view.tsx @@ -63,7 +63,7 @@ import { useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; -import { CoinsIcon, FileTextIcon, Loader2Icon, SearchIcon, SettingsIcon, UploadIcon, WaypointsIcon } from "lucide-react"; +import { FileTextIcon, Loader2Icon, SearchIcon, SettingsIcon, UploadIcon, WaypointsIcon } from "lucide-react"; import { incrementalLayout, type GraphCostSummary, type GraphNode, type GraphSnapshot } from "@/src/core"; import { GraphCanvasRenderer, type GraphRenderSettings } from "./graph-canvas.ts"; import { GraphCoverageControl } from "./graph-coverage.tsx"; @@ -234,11 +234,45 @@ function fmtRunCost(dollars: number | null): string { } /** - * The graph-pipeline cost pill's expanded panel (2026-09 graph cost - * metrics wave): stage totals (extraction / audit / cluster-naming — - * dollars headline, tokens secondary) + the recent-runs list. Presenta- - * tional: the fold happens server-side (core/cost-summary.ts); dollars - * are inferred read-time, never stored. + * The graph-pipeline cost control (2026-09-20 placement wave): a bare + * cumulative-dollars number — no pill chrome (owner: "just a number"), + * hover-highlighted, click toggles the breakdown panel. Same dismiss + * pattern as the settings gear: outside click or Escape closes. The + * panel is viewport-capped + scrollable like the coverage dropdown. + */ +function GraphCostControl({ costs }: { costs: GraphCostSummary }) { + const [open, setOpen] = useState(false); + // Click anywhere outside (or Escape) closes the panel — the root + // wraps button + panel, so the toggle itself keeps working. + const rootRef = useRef(null); + usePopoverDismiss(rootRef, open, () => setOpen(false)); + return ( +
+ + {open && ( +
+ +
+ )} +
+ ); +} + +/** + * The cost control's expanded panel: stage totals (extraction / audit / + * cluster-naming — dollars headline, tokens secondary) + the recent-runs + * list. Presentational: the fold happens server-side + * (core/cost-summary.ts); dollars are inferred read-time, never stored. */ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) { const stageRows: [string, GraphCostSummary["stages"]["extraction"]][] = [ @@ -849,7 +883,6 @@ export function GraphView() { // without a manual reload. Best-effort: a fetch failure never blocks // the view — the pill just stays on its last numbers. const [costs, setCosts] = useState(null); - const [costsOpen, setCostsOpen] = useState(false); const refreshCosts = useCallback(() => { fetch("/api/graph/costs") .then((res) => res.json() as Promise) @@ -1113,7 +1146,8 @@ export function GraphView() { threshold the row wraps to a second line instead of overflowing left over the header toggle cluster (92px gutter + 36px button); 9rem keeps one clear column of air alongside it. */} -
+
+
{!isEmpty && ( <> {/* Search — its own container (icon + input + no-match note). */} @@ -1176,38 +1210,25 @@ export function GraphView() { )}
- {/* Bottom-left stack (2026-09-17 merge): the graph-pipeline cost - pill + its expandable panel, and the running-pipeline pills — - one container so they stack instead of overlapping. The pill - shows cumulative pipeline spend (dollars headline, inferred - read-time — never stored); click toggles the breakdown. */} - {(costs?.runs.length ?? 0) > 0 || pipelineRuns.length > 0 ? ( -
- {costsOpen && costs && } - {costs && costs.runs.length > 0 && ( - - )} - {pipelineRuns.map((run) => ( -
- - {run.phase} - {run.title} - {Math.max(0, Math.round((Date.now() - run.startedAt) / 1000))}s -
- ))} + {/* Top-right tail (owner, 2026-09-20): the pipeline cost number — + just a number, hover-highlighted, dropdown breakdown — then + the running-pipeline pills under it. Lives BELOW the actions + row so narrow windows can never push it over the composer + the way the old bottom-left stack did; the whole top-right + cluster is one column, right-aligned. */} + {costs && costs.runs.length > 0 && } + {pipelineRuns.map((run) => ( +
+ + {run.phase} + {run.title} + {Math.max(0, Math.round((Date.now() - run.startedAt) / 1000))}s
- ) : null} + ))} +
{selected && !isEmpty && (