Colors (GSD palette) untouched. New visual vocabulary across the whole GUI: - warm-tinted layered shadow tokens (--shadow-xs/sm/md) replacing flat rings; cards/dialogs framed with hairline border + real elevation - strict radii scale: xl cards / lg inputs / md controls / 2xl dialogs; buttons move from pills to rounded-md - third type register: --font-mono + tabular-nums on SQL, timers, dates, ids; standardized .thot-label micro-label tier - prose: framed tables with uppercase headers, elevated code blocks - SqlViewer badges moved from hardcoded Tailwind greens/reds to success/ destructive token tints; ResultsPanel inline styles replaced with classes - artifact gate: first option rendered as filled primary action Verified offline via Playwright against a mock backend replaying a real psd session (no VPN needed). tsc clean, 115/115 vitest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
133 lines
5.0 KiB
TypeScript
133 lines
5.0 KiB
TypeScript
// frontend/src/shell/WorkflowBar.tsx
|
|
import { useSessionStore } from "../store/sessionStore";
|
|
import { ElapsedTimer } from "./ElapsedTimer";
|
|
|
|
const PHASES = [
|
|
{ id: "F1", name: "Disambiguation" },
|
|
{ id: "F2", name: "Schema linking" },
|
|
{ id: "F3", name: "Exploration" },
|
|
{ id: "F4", name: "SQL plan" },
|
|
{ id: "F5", name: "SQL generation" },
|
|
{ id: "F6", name: "Validation" },
|
|
{ id: "F7", name: "Review" },
|
|
{ id: "F8", name: "Results" },
|
|
];
|
|
|
|
// Synthetic, human-readable title for the current phase, shown under the dots.
|
|
// Static bilingual strings (no LLM): PHASE_LANG picks which set is displayed.
|
|
// The rest of the app chrome is English, so the label follows suit.
|
|
const PHASE_LANG: "en" | "it" = "en";
|
|
const PHASE_TITLES: Record<string, { en: string; it: string }> = {
|
|
F1: { en: "Clarifying the question", it: "Chiarimento della domanda" },
|
|
F2: { en: "Linking the schema", it: "Collegamento dello schema" },
|
|
F3: { en: "Exploring the data", it: "Esplorazione dei dati" },
|
|
F4: { en: "Planning the SQL", it: "Pianificazione della query" },
|
|
F5: { en: "Generating the SQL", it: "Generazione della query" },
|
|
F6: { en: "Validating the results", it: "Validazione dei risultati" },
|
|
F7: { en: "Reviewing with you", it: "Revisione con te" },
|
|
F8: { en: "Presenting results", it: "Presentazione dei risultati" },
|
|
};
|
|
|
|
type DotState = "done" | "running" | "error" | "pending";
|
|
|
|
// Each state is a tinted chip; red is reserved strictly for error (the GSD theme's
|
|
// primary is already red), green = done, amber = the live phase, neutral = pending.
|
|
const DOT: Record<DotState, string> = {
|
|
done: "bg-[oklch(var(--success)/0.16)] text-[oklch(0.5_0.13_165)] ring-[oklch(var(--success)/0.32)]",
|
|
running:
|
|
"bg-[oklch(var(--warning)/0.24)] text-[oklch(0.46_0.11_79)] ring-[oklch(var(--warning)/0.6)]",
|
|
error: "bg-destructive/15 text-destructive ring-destructive/45",
|
|
pending: "bg-transparent text-muted-foreground/70 ring-border",
|
|
};
|
|
|
|
export function WorkflowBar({
|
|
finalized = false,
|
|
running = false,
|
|
resetKey = null,
|
|
}: {
|
|
finalized?: boolean;
|
|
/** The harness/model is actively working (drives the processing timer). */
|
|
running?: boolean;
|
|
/** Active session id — remounts the timer (resets it) when the session changes. */
|
|
resetKey?: string | null;
|
|
}) {
|
|
const currentPhase = useSessionStore((s) => s.currentPhase);
|
|
const phaseError = useSessionStore((s) => s.phaseError);
|
|
const activeIdx = PHASES.findIndex((p) => p.id === currentPhase);
|
|
// On a finalized session all phases are done — surface the closing phase's title.
|
|
const titleId = finalized ? "F8" : currentPhase;
|
|
const phaseTitle = titleId ? PHASE_TITLES[titleId]?.[PHASE_LANG] : null;
|
|
|
|
return (
|
|
<div className="flex flex-col items-center gap-1">
|
|
<div className="flex items-center justify-center gap-3">
|
|
<nav
|
|
aria-label="Workflow progress"
|
|
className="flex items-center gap-0 overflow-x-auto py-0.5"
|
|
>
|
|
{PHASES.map((p, i) => {
|
|
const isActive = currentPhase === p.id;
|
|
const isDone = activeIdx > -1 && i < activeIdx;
|
|
const state: DotState = finalized
|
|
? "done"
|
|
: isActive
|
|
? phaseError === p.id
|
|
? "error"
|
|
: "running"
|
|
: isDone
|
|
? "done"
|
|
: "pending";
|
|
const connectorDone = finalized || (activeIdx > -1 && i <= activeIdx);
|
|
return (
|
|
<div key={p.id} className="flex items-center">
|
|
{i > 0 && (
|
|
<span
|
|
aria-hidden
|
|
className={[
|
|
"mx-1 h-px w-3 shrink-0 sm:w-4",
|
|
connectorDone ? "bg-[oklch(var(--success)/0.4)]" : "bg-border",
|
|
].join(" ")}
|
|
/>
|
|
)}
|
|
<span className="relative inline-flex shrink-0">
|
|
{state === "running" && (
|
|
<span
|
|
aria-hidden
|
|
className="absolute -inset-[3px] animate-pulse rounded-full bg-[oklch(var(--warning)/0.22)]"
|
|
/>
|
|
)}
|
|
<span
|
|
data-testid={`phase-${p.id}`}
|
|
data-active={isActive ? "true" : "false"}
|
|
data-state={state}
|
|
title={`${p.id}: ${p.name}`}
|
|
aria-label={`${p.id} ${p.name} — ${state}`}
|
|
aria-current={isActive ? "step" : undefined}
|
|
className={[
|
|
"relative inline-flex size-6 items-center justify-center rounded-full",
|
|
"text-[0.625rem] font-semibold leading-none tracking-tight tabular-nums",
|
|
"ring-1 ring-inset transition-colors",
|
|
DOT[state],
|
|
].join(" ")}
|
|
>
|
|
{p.id}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</nav>
|
|
<ElapsedTimer key={resetKey ?? "none"} running={running} />
|
|
</div>
|
|
{phaseTitle && (
|
|
<span
|
|
data-testid="phase-title"
|
|
className="thot-label"
|
|
>
|
|
{phaseTitle}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|