Audit findings 2.1 + 2.2 (high).
2.1 WorkflowBar's static phase list was fiction from F2 on (F2 "Schema
linking" vs memoria, F4 "SQL plan" vs schema_linking, …): every live
session showed the wrong phase name. Both maps now mirror
harness/workflow.yaml (F1 chiarimento … F8 datamart).
2.2 forceAdvance (6ee5bda) let reviewer_decide advance:true bypass the
phase gate on ANY phase, contradicting SKILL.md's "auto-advance only
empty F2 / skipped F6". reviewer_decide is back on advanceIfReady (exit-6
no-op) and tells the model to close via reviewer_confirm; forceAdvance
stays only where selection IS the approval by design: reviewer_schema_linking
(F4) and the F8 promotion close path. SKILL.md now names the three
self-closing gates (F3 rewrite_question, F4 schema-linking advance:true,
F8 memory_promote) so gate and skill state one contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
136 lines
5.2 KiB
TypeScript
136 lines
5.2 KiB
TypeScript
// frontend/src/shell/WorkflowBar.tsx
|
|
import { useSessionStore } from "../store/sessionStore";
|
|
import { ElapsedTimer } from "./ElapsedTimer";
|
|
|
|
// Mirror of harness/workflow.yaml phase ids/names (F1 chiarimento … F8 datamart).
|
|
// Keep the two lists in lockstep: the harness is the single source of phase truth,
|
|
// and a drifted label here mislabels every live session from that phase on.
|
|
const PHASES = [
|
|
{ id: "F1", name: "Clarification" },
|
|
{ id: "F2", name: "Memory" },
|
|
{ id: "F3", name: "Rewrite" },
|
|
{ id: "F4", name: "Schema linking" },
|
|
{ id: "F5", name: "Plan" },
|
|
{ id: "F6", name: "CTE build" },
|
|
{ id: "F7", name: "Final SQL" },
|
|
{ id: "F8", name: "Datamart" },
|
|
];
|
|
|
|
// 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: "Recalling relevant memory", it: "Recupero delle memorie utili" },
|
|
F3: { en: "Rewriting the question", it: "Riscrittura della domanda" },
|
|
F4: { en: "Linking the schema", it: "Collegamento dello schema" },
|
|
F5: { en: "Planning the SQL", it: "Pianificazione della query" },
|
|
F6: { en: "Building the CTEs", it: "Costruzione delle CTE" },
|
|
F7: { en: "Finalizing the SQL", it: "Finalizzazione della query" },
|
|
F8: { en: "Building the datamart", it: "Costruzione del datamart" },
|
|
};
|
|
|
|
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>
|
|
);
|
|
}
|