Files
legal-ai/web-ui/src/components/cases/workflow-timeline.tsx
Chaim 7c48c8753e
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
fix(status): render pipeline phase for legacy statuses (analyst_verified)
A case parked on a status the trimmed PHASES list dropped — but the agents
still set, e.g. `analyst_verified` (HEARTBEAT.md), `research_complete`
(legal-researcher) — rendered with NO active phase: phaseIndexOf returned -1,
so the pipeline stepper went fully blank and the chair couldn't see where the
case stood (reported on 1043-02-26, currently at analyst_verified).

Add LEGACY_STATUS_PHASE in case-status.ts mapping each legacy/intermediate
status to its display phase (analyst_verified/research_complete → "ניתוח וכיוון"),
and a single phaseIndexOf() exported from the SSoT. status-hero.tsx and
workflow-timeline.tsx both dropped their identical local phaseIndexOf (which had
the bug) and import the shared one — no parallel path (G2).

Display-only data-mapping fix; the stepper design is unchanged. Does NOT address
the root drift (agents still set analyst_verified) — that prompt-alignment is a
separate, carefully-sequenced follow-up needing cross-company agent sync.

Invariants: G2 (single phase-resolution path, legacy handled in the SSoT, no
new parallel logic). No backend / status-model change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:31:17 +00:00

104 lines
3.6 KiB
TypeScript

"use client";
import { PHASES, phaseIndexOf, type CaseStatus, type PhaseKey } from "@/lib/api/case-status";
import { STATUS_LABELS, STATUS_ICONS, STATUS_DESCRIPTIONS } from "@/components/cases/status-badge";
import {
FolderInput, ClipboardList, Brain, PenLine, CheckCircle2,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
/*
* Vertical RTL workflow timeline. Phases + their statuses come from the SSoT
* (@/lib/api/case-status); only the per-phase icon is a view-layer concern and
* stays here. The user sees "where am I in the process" rather than micro-steps.
*/
const PHASE_ICONS: Record<PhaseKey, LucideIcon> = {
intake: FolderInput,
prep: ClipboardList,
thinking: Brain,
writing: PenLine,
done: CheckCircle2,
};
export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
const currentIdx = phaseIndexOf(status);
return (
<ol className="relative">
{PHASES.map((phase, i) => {
const state =
currentIdx === -1 ? "pending"
: i < currentIdx ? "done"
: i === currentIdx ? "current"
: "pending";
const dotTone =
state === "done" ? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
: state === "current" ? "bg-gold [box-shadow:0_0_0_1px_var(--color-gold)]"
: "bg-rule";
const labelTone =
state === "done" ? "text-ink-soft"
: state === "current" ? "text-navy font-semibold"
: "text-ink-muted";
const iconTone =
state === "done" ? "text-success"
: state === "current" ? "text-gold-deep"
: "text-ink-muted/50";
const PhaseIcon = PHASE_ICONS[phase.key];
const isLast = i === PHASES.length - 1;
return (
<li
key={phase.key}
className="relative flex items-center gap-3 py-2"
>
{/* connector line below the dot (mockup .tl .line) */}
{!isLast && (
<span
className="absolute top-[26px] w-px h-[20px] bg-rule"
style={{ insetInlineStart: "5px" }}
aria-hidden
/>
)}
<span
className={`inline-block w-[11px] h-[11px] rounded-full border-2 border-surface shrink-0 ${dotTone}`}
aria-hidden
/>
<div className="flex items-center gap-2 grow min-w-0">
<span className={`text-[0.84rem] flex items-center gap-1.5 ${labelTone}`}>
<PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} />
{phase.label}
</span>
<span className="ms-auto text-[0.72rem] text-ink-muted tabular-nums shrink-0">
{state === "current" ? "כעת" : state === "done" ? "✓" : "—"}
</span>
</div>
</li>
);
})}
{/* current micro-status detail under the active phase */}
{currentIdx !== -1 && status && (
<li className="ps-[26px] pt-1">
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
{STATUS_ICONS[status] &&
(() => {
const Icon = STATUS_ICONS[status];
return <Icon className="w-3 h-3 shrink-0" />;
})()}
{STATUS_LABELS[status]}
</span>
{STATUS_DESCRIPTIONS[status] && (
<span className="block text-[0.65rem] text-ink-muted leading-snug mt-0.5">
{STATUS_DESCRIPTIONS[status]}
</span>
)}
</li>
)}
</ol>
);
}