From 9eb61c59305858d7892acafee5bfda2cd8c24bcb Mon Sep 17 00:00:00 2001 From: marsh15 Date: Wed, 30 Sep 2026 19:39:07 +0530 Subject: [PATCH] feat: polish the evidence workbench design and interactions Unify the neutral interface, improve mobile record reading and report hierarchy, and make evidence inspection and authenticated request recovery easier to navigate. Preserve source provenance and saved/live result labels. --- DESIGN.md | 11 ++++ frontend/e2e/openai-auth.spec.ts | 3 + frontend/src/components/AppShell.tsx | 74 +++++++--------------- frontend/src/components/Auth.tsx | 51 ++++++++++++--- frontend/src/components/EvidenceDrawer.tsx | 35 +++++----- frontend/src/components/IncidentAi.tsx | 25 +++++--- frontend/src/components/Usage.tsx | 21 +++++- frontend/src/components/ui/button.tsx | 2 +- frontend/src/components/ui/input.tsx | 2 +- frontend/src/components/ui/sheet.tsx | 6 +- frontend/src/components/ui/table.tsx | 6 +- frontend/src/index.css | 41 ++++++++++-- frontend/src/pages/Comparison.tsx | 40 ++++++------ frontend/src/pages/Imports.tsx | 24 +++---- frontend/src/pages/IncidentEvaluation.tsx | 30 ++++----- frontend/src/pages/IncidentImports.tsx | 17 ++--- frontend/src/pages/IncidentLibrary.tsx | 31 +++++---- frontend/src/pages/IncidentWorkbench.tsx | 29 +++++---- frontend/src/pages/Library.tsx | 16 ++--- frontend/src/pages/Notes.tsx | 12 ++-- 20 files changed, 284 insertions(+), 192 deletions(-) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..d1ec4c1 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,11 @@ +# FloorReplay design + +FloorReplay is a quiet evidence workbench. Its visual identity follows the manufacturing investigation: recorded output, source rows, knowledge cutoffs, immutable revisions, and human decisions. It uses warm neutral page surfaces, white documents, charcoal navigation, and restrained amber for evidence limitations and observed shortfall. Status text always explains color. + +Inter is the interface face. JetBrains Mono is self-hosted for source identifiers, formulas, and structured records. Headings use a clear size hierarchy with modest negative tracking. Numeric tables use tabular figures. No decorative charts, confidence gauges, neon accents, or staged entrances. + +The shell shares one 72rem content grid. Mobile navigation wraps into its own row. Pages have a title, explanation, task controls, and report content in that order. Incident investigations offer direct section links. Related production measurements form one divided document row rather than disconnected dashboard cards. + +Controls provide visible focus and short feedback. Frequent inspection actions stay immediate. Popovers dismiss with Escape, restore trigger focus, and align to their trigger. Motion respects reduced-motion preferences. Touch controls have larger hit areas. Tables scroll within their containers; incident library records have a narrow-screen reading layout. + +Saved deterministic bundles, live analyses, historical model outputs, and unavailable capabilities retain explicit labels. Design changes must preserve evidence provenance and synthetic-data notices. diff --git a/frontend/e2e/openai-auth.spec.ts b/frontend/e2e/openai-auth.spec.ts index 5f8d58b..04506c5 100644 --- a/frontend/e2e/openai-auth.spec.ts +++ b/frontend/e2e/openai-auth.spec.ts @@ -30,6 +30,7 @@ test('reviewer auth stays in memory and a disconnected AI operation is recovered await page.getByRole('button', { name: 'Continue', exact: true }).click() await expect(page.getByText('Shift reviewer · reviewer')).toBeVisible() await page.getByRole('button', { name: 'Generate draft' }).click() + await page.getByText('Recover an interrupted request', { exact: true }).click() await expect(page.getByRole('button', { name: 'Recover existing request' })).toBeVisible() await page.getByRole('button', { name: 'Recover existing request' }).click() await expect(page.getByText('The output record establishes a shortfall.')).toBeVisible() @@ -72,6 +73,7 @@ for (const terminal of [ await page.getByRole('button', { name: 'Generate draft' }).click() await expect(page.getByRole('alert').filter({ hasText: terminal.displayed })).toBeVisible() await expect(page.getByText(/Awaiting human review/)).toHaveCount(0) + await page.getByText('Recover an interrupted request', { exact: true }).click() await page.getByRole('button', { name: 'Recover existing request' }).click() await expect(page.getByRole('alert').filter({ hasText: terminal.displayed })).toBeVisible() const identity = await page.getByLabel('Request identity').inputValue() @@ -81,6 +83,7 @@ for (const terminal of [ await page.getByLabel('Password').fill('test-password') await page.getByRole('button', { name: 'Continue', exact: true }).click() await expect(page.getByRole('button', { name: 'Generate draft' })).toBeDisabled() + await page.getByText('Recover an interrupted request', { exact: true }).click() await page.getByLabel('Request identity').fill(identity) await page.getByRole('button', { name: 'Recover existing request' }).click() await expect(page.getByRole('alert').filter({ hasText: terminal.displayed })).toBeVisible() diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index ccc3e84..f998a0d 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -1,9 +1,5 @@ -/** - * App shell: quiet top bar, two primary destinations. Light theme is - * locked for this workbench; state colors supplement labels everywhere. - */ - import { useQuery } from '@tanstack/react-query' +import { ArrowLeftRight, FlaskConical, Library } from 'lucide-react' import { Link, useLocation } from 'react-router' import { api } from '@/lib/api' import { UsagePanel } from '@/components/Usage' @@ -11,62 +7,38 @@ import { SignInControl } from '@/components/Auth' import { cn } from '@/lib/utils' const NAV = [ - { to: '/', label: 'Incidents' }, - { to: '/evaluation', label: 'Evaluation lab' }, - { to: '/coverage', label: 'Coverage archive' }, + { to: '/', label: 'Incidents', icon: Library }, + { to: '/evaluation', label: 'Evaluation lab', icon: FlaskConical }, + { to: '/coverage', label: 'Coverage archive', icon: ArrowLeftRight }, ] export function AppShell({ children }: { children: React.ReactNode }) { const { pathname } = useLocation() - const capabilities = useQuery({ - queryKey: ['capabilities'], - queryFn: api.capabilities, - staleTime: 60_000, - }) + const capabilities = useQuery({ queryKey: ['capabilities'], queryFn: api.capabilities, staleTime: 60_000 }) return ( -
-
-
- - FloorReplay - - Synthetic manufacturing data - +
+ Skip to content +
+
+ + + FloorReplaySynthetic manufacturing data -
-
{children}
-
-

- FloorReplay is an unofficial engineering exploration using synthetic data. It is not - affiliated with Genorai and has not been validated in a real factory. -

+
{children}
+
+

FloorReplay is an unofficial engineering exploration using synthetic data. It is not affiliated with Genorai and has not been validated in a real factory.

) diff --git a/frontend/src/components/Auth.tsx b/frontend/src/components/Auth.tsx index ba450a8..226da7c 100644 --- a/frontend/src/components/Auth.tsx +++ b/frontend/src/components/Auth.tsx @@ -1,10 +1,12 @@ import type { components } from '@/lib/generated-api' -import { createContext, useContext, useState } from 'react' +import { createContext, useContext, useRef, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' import { API_BASE, ApiError } from '@/lib/api' import { session } from '@/lib/session' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { Popover } from 'radix-ui' +import { LogIn, X } from 'lucide-react' export interface AuthUser { id: string; username: string; display_name: string; role: 'owner' | 'reviewer' } interface AuthContextValue { user: AuthUser | null; signIn: (username: string, password: string) => Promise; signOut: () => Promise } @@ -34,15 +36,48 @@ export function useAuth() { export function SignInControl() { const { user, signIn, signOut } = useAuth() const [open, setOpen] = useState(false) + const usernameInput = useRef(null) const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState('') const [pending, setPending] = useState(false) - if (user) return
{user.display_name} · {user.role}
- return
{open &&
{ - event.preventDefault(); setPending(true); setError('') - try { await signIn(username.trim(), password); setPassword(''); setOpen(false) } - catch (failure) { setError(failure instanceof Error ? failure.message : 'Sign-in failed') } - finally { setPending(false) } - }}>

Owner or invited reviewer

Sign in again after reloading this page.

{error &&

{error}

}
}
+ + if (user) return ( +
+ {user.display_name} · {user.role} + +
+ ) + + return ( + { setOpen(nextOpen); if (!nextOpen) setPassword('') }}> + + + + + { event.preventDefault(); usernameInput.current?.focus() }} align="end" sideOffset={10} collisionPadding={16} aria-labelledby="sign-in-heading" className="z-50 w-[min(22rem,calc(100vw-2rem))] rounded-xl bg-popover p-5 text-popover-foreground shadow-[0_8px_32px_-8px_rgba(24,35,30,0.22)] outline-none"> +
+

Reviewer access

Sign in as an owner or invited reviewer.

+ +
+
{ + event.preventDefault(); setPending(true); setError('') + try { await signIn(username.trim(), password); setPassword(''); setOpen(false) } + catch (failure) { setError(failure instanceof Error ? failure.message : 'Sign-in failed') } + finally { setPending(false) } + }}> + + + {error &&

{error}

} + +

Sign in again after reloading this page.

+
+
+
+
+ ) } diff --git a/frontend/src/components/EvidenceDrawer.tsx b/frontend/src/components/EvidenceDrawer.tsx index c989851..eee1dc9 100644 --- a/frontend/src/components/EvidenceDrawer.tsx +++ b/frontend/src/components/EvidenceDrawer.tsx @@ -10,6 +10,7 @@ import { Skeleton } from '@/components/ui/skeleton' import { api, type EvidenceRef } from '@/lib/api' import { formatInstant } from '@/lib/status' import { Mono } from '@/components/status' +import { Button } from '@/components/ui/button' import { Table, TableBody, @@ -42,7 +43,7 @@ export function EvidenceDrawer({ open: boolean onOpenChange: (open: boolean) => void }) { - const { data, isPending, isError, error } = useQuery({ + const { data, isPending, isError, error, refetch } = useQuery({ queryKey: ['snapshot', evidence?.snapshot_id], queryFn: () => api.snapshot(evidence!.snapshot_id), enabled: open && evidence !== null && evidence.snapshot_id !== 'event', @@ -55,9 +56,9 @@ export function EvidenceDrawer({ return ( - - - + + + {evidence ? evidence.snapshot_id : ''} @@ -69,28 +70,26 @@ export function EvidenceDrawer({ - {isPending && !isEvent ? ( -
+ {isPending && evidence && !isEvent ? ( +
) : isError ? ( -

- Snapshot could not be loaded: {error.message} -

+

Snapshot could not be loaded: {error.message}

) : isEvent ? ( -
+

This evidence reference points at the confirmed unavailability event pinned to the scenario revision, not a snapshot row.

) : data ? ( -
-
+
+
Scope
-
{data.scope}
+
{data.scope}
Evidence time
{formatInstant(data.declared_evidence_at)}
Coverage
@@ -101,13 +100,14 @@ export function EvidenceDrawer({
+ {evidence?.source_ref &&

Referenced source row: {evidence.source_ref}{rows.some((row) => row.source_ref === evidence.source_ref) ? ' · Marked below' : ' · No matching row in this snapshot'}

} {rows.length > 0 ? (
- +
{columns.map((column) => ( - + {column} ))} @@ -120,12 +120,13 @@ export function EvidenceDrawer({ return ( {columns.map((column) => ( {column === 'source_ref' ? ( - {String(row[column] ?? '')} + <>{String(row[column] ?? '')}{highlighted && Referenced} ) : ( String(row[column] ?? '') )} diff --git a/frontend/src/components/IncidentAi.tsx b/frontend/src/components/IncidentAi.tsx index a8c8349..0a6706a 100644 --- a/frontend/src/components/IncidentAi.tsx +++ b/frontend/src/components/IncidentAi.tsx @@ -6,6 +6,9 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { useAuth } from '@/components/Auth' +const selectClass = 'mt-1.5 h-9 w-full rounded-md border border-input bg-background px-2.5 text-sm outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 disabled:opacity-50' +const evidenceLinkClass = 'rounded-sm font-mono text-xs underline decoration-border underline-offset-4 hover:decoration-current focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring' + const TASKS: AiTask[] = ['question', 'investigation', 'summary', 'recovery', 'note'] export function OpenAiPanel({ report, enabled, reasons, onEvidence }: { report: AnalysisReport; enabled: boolean; reasons: string[]; onEvidence: (id: string) => void }) { const { user } = useAuth() @@ -23,20 +26,22 @@ export function OpenAiPanel({ report, enabled, reasons, onEvidence }: { report: const current = status.data ?? run const output = current?.status === 'COMPLETED' ? current.result?.output : undefined const claims = output?.claims ?? output?.selected_claims ?? output?.hypotheses?.map((item) => item.explanation) ?? output?.assertions?.map((item) => item.assertion) ?? [] - return

OpenAI draft

Drafts require human review. Calculated metrics and uncertainty remain attached to every draft.

+ return

OpenAI draft

Drafts require human review. Calculated metrics and uncertainty remain attached to every draft.

{!enabled &&

{reasons.join(' ')}

} -
- {retrievalMode === 'hybrid' && } +
+ {retrievalMode === 'hybrid' && } + {retrievalMode === 'hybrid' && !corpora.isPending && !selectedCorpus &&

No indexed corpus available. Use current evidence only.

} + {retrievalMode === 'hybrid' && corpora.isError &&

Corpus releases could not be loaded: {corpora.error.message}

} {task === 'note' &&

Extract observations from the pinned source evidence. Confirm draft assertions before publication.

} -