Files
ThothII/frontend/src/shell/WorkflowBar.tsx
T
marcopanandClaude Fable 5 4cd1cbc493 style(frontend): look&feel v2 — layered shadows, sharper radii, mono data register
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>
2026-07-05 13:04:46 +02:00

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>
);
}