The chair pointed out three UX gaps after uploading a new precedent:
1. The status said "מחלץ הלכות" but nothing was actually running — the
field only meant "halacha_extraction_status != completed", which
includes the post-upload "pending" state where the local MCP worker
hasn't been told to drain anything yet. Misleading.
2. The page didn't refresh on its own. The chair had to F5 to see new
counts after extraction completed.
3. Clicking the trash icon mid-extraction would cascade-delete the row
while the extractor was still using it (FK errors, partial writes).
Fixes:
- ingest_precedent now auto-queues both metadata and halacha extraction
on upload by stamping the request timestamps. The chair (or me) drains
the queue with one `precedent_process_pending` call from chat —
no need to click any button before that.
- StatusPill is now five-state with proper labels:
"נכשל" (extraction_status=failed) — red
"מעבד טקסט" — shimmer (extraction_status=processing)
"בתור" — neutral (chunks queued, not yet running)
"מחלץ הלכות" — shimmer (halacha_extraction_status=processing)
"ממתין לחילוץ" — neutral (queued for local MCP worker)
"לא חולץ" — neutral (pending without queue stamp — shouldn't happen)
"X/Y מאושרות" — gold (done, with halachot count)
The shimmer is a CSS-only sliding-stripe animation defined in globals.
- usePrecedents has a conditional refetchInterval — polls every 5s while
any row is mid-extraction or queued, then stops once everything settles
to completed/failed. New helper isPrecedentActive() centralises the
"is this row mid-something" check so the UI and the destructive-action
guard agree.
- Trash button is disabled (opacity 30%, tooltip explains) while the row
is active. Pencil/edit stays enabled — editing metadata fields during
extraction is safe (last write wins, low-stakes race).
Schema: list_external_case_law now exposes the two *_requested_at
timestamps so the UI can distinguish "queued" from "never asked".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
עוזר משפטי — Web UI (Next.js rewrite)
The Next.js 16 rewrite of legal-ai.nautilus.marcusgroup.org, currently hosted side-by-side with the legacy vanilla index.html at:
- Staging: https://legal-ai-next.nautilus.marcusgroup.org (auto-deployed from
ui-rewritebranch via Coolify) - Production FastAPI: https://legal-ai.nautilus.marcusgroup.org (same backend, old UI still default)
The rewrite talks to the existing FastAPI via proxy rewrites in next.config.ts — no CORS setup, no duplicated backend.
Stack
- Next.js 16.2.3 (App Router, Turbopack,
output: "standalone") - React 19.2 · TypeScript · Tailwind v4 · shadcn/ui (radix-nova preset)
- TanStack Query v5 + TanStack Table v8
- react-hook-form + zod for mutations
- react-dropzone for uploads; EventSource for SSE progress
- Heebo via
next/font/google; design tokens insrc/app/globals.css
Local development
npm install
npm run dev # http://localhost:3000
npm run build # full type check + production build
npm run lint
npm run api:types # regenerate src/lib/api/types.ts from FastAPI's OpenAPI
API connection
By default the dev server proxies to production FastAPI (https://legal-ai.nautilus.marcusgroup.org). To point at a different backend, set:
export NEXT_PUBLIC_API_ORIGIN=http://localhost:8000
npm run dev
Project layout
src/
├── app/ # Route segments (App Router)
│ ├── layout.tsx # Root: Providers + RTL html + fonts
│ ├── page.tsx # Home: KPIs + status donut + cases table
│ ├── error.tsx # Route-segment error boundary
│ ├── global-error.tsx # Root crash fallback
│ ├── not-found.tsx # 404
│ ├── cases/
│ │ ├── new/ # 3-step create wizard
│ │ └── [caseNumber]/
│ │ ├── page.tsx # Case detail (tabs + workflow timeline)
│ │ └── compose/ # Research analysis + chair-position editor
│ ├── training/ # Style portrait + corpus + compare (3 tabs)
│ ├── skills/ # Paperclip skills inventory
│ └── diagnostics/ # DB health + failed/stuck docs
├── components/
│ ├── app-shell.tsx # Header + nav with aria-current
│ ├── cases/ # Home + detail screens
│ ├── compose/ # Research analysis editor
│ ├── documents/ # UploadSheet
│ ├── training/ # Style report / corpus / compare panels
│ ├── wizard/ # Case create wizard + parties-field
│ └── ui/ # shadcn primitives
├── lib/
│ ├── api/ # Typed hooks per domain (cases, documents, research,
│ │ # system, skills, training)
│ ├── schemas/ # zod schemas (case create / update)
│ ├── practice-area.ts # Multi-tenant axis enum + deriveSubtype()
│ ├── sse.ts # EventSource wrapper
│ ├── providers.tsx # QueryClient + Toaster
│ └── utils.ts # cn()
Smoke test (run after every deploy)
Use any browser at the staging URL. Every step should be doable without console errors and each mutation should produce a visible toast.
1. Home · /
- Header nav shows 5 items; the current page is underlined in gold
- 4 KPI cards render real numbers (סה״כ · בהכנה · בכתיבה · מוכנים)
- Cases table lists existing cases; search filters by case number or title
- "פיזור סטטוסים" donut renders with a legend
- "+ תיק חדש" button in the top-left navigates to
/cases/new
2. Create case · /cases/new
- 3-step wizard: פרטי יסוד → צדדים → השלמות
- Type
1500-25→ appeal_subtype auto-fills to "רישוי ובנייה" - Type
8500-25→ subtype auto-fills to "היטל השבחה" - Manually pick a different subtype → auto-fill stops
- Submitting with invalid case number shows a zod field error (no crash)
- Successful create → toast "תיק חדש נוצר" → router pushes to
/cases/{number}
3. Case detail · /cases/[caseNumber]
- Header shows status badge + gold "ועדת ערר · X" practice-area badge
- Tabs switch cleanly: סקירה / מסמכים / פעולות
- Workflow timeline on the right shows the current phase highlighted in gold
- פעולות tab → "עריכת פרטי תיק" dialog opens; submitting updates the header without full reload (optimistic cache patch)
- "העלאת מסמכים" sheet opens from the tab row; drag-drop fires a POST and a live progress bar appears via SSE
4. Compose · /cases/[caseNumber]/compose
- If analysis-and-research.md exists: threshold claims + issues render as collapsible cards
- Chair-position textarea auto-saves on blur with "✓ נשמר {time}" indicator
- If 404 (no analysis yet): empty state card renders, no error toast
5. Training · /training
- Report tab: headline card, 4 KPIs, subject donut, anatomy bars, top-12 signature phrases
- Corpus tab: table of corpus decisions with a trash icon per row (aria-label present)
- Deleting a decision refreshes both the corpus table and the report KPIs
- Compare tab: two Selects, pick 2 different decisions, side-by-side panels + shared/only-A/only-B pattern lists
6. Skills · /skills
- Card grid of Paperclip skills with sync-status badges (מסונכרן / DB בלבד / לא סונכרן)
- Chars + file counts render; "לא ידוע" doesn't appear for installed skills
7. Diagnostics · /diagnostics
- DB status card shows "מחובר" in green
- Table counts populate for cases / documents / chunks / corpus / patterns
- Failed + stuck document lists render (empty states OK)
- Page self-refreshes every 10s — check the network tab for recurring calls
8. Error boundary
- Visit
/cases/NOT-REAL-999-99→ case detail shows an error card with the FastAPI message and "חזרה לרשימת התיקים" button (no white screen) - Visit
/anything-broken-xyz→ custom 404 page with "חזרה לבית" button
9. Keyboard + RTL
- Tab through the home page — focus rings are gold, visible
- Wizard progresses via Enter on the "הבא" button
- Screen reader announces nav items with "עמוד נוכחי" on the active one
Deploy
git push # → Coolify auto-build on branch ui-rewrite (~90 s)
Known issue: the Gitea → Coolify webhook is not firing at the time of writing. Trigger a manual deploy via the Coolify MCP (
mcp__coolify__deploywith app UUIDl146g36mtlp0k03vrwkyrgkk) or the Coolify UI until the webhook is fixed.
Phase tracking
See ~/.claude/plans/joyful-marinating-sutton.md for the 7-phase rewrite plan and ~/legal-ai-ui-rewrite/.taskmaster/tasks/tasks.json for the task board.
| Phase | Scope | Status |
|---|---|---|
| 1 | Scaffold + Coolify staging | ✅ |
| 2 | API client + typed hooks + probe | ✅ |
| 3 | Read views (home, case detail, compose) | ✅ |
| 4 | Mutations (wizard, edit, upload+SSE) | ✅ |
| 4.5 | Practice-area integration | ✅ |
| 5 | Secondary screens (training, skills, diagnostics) | ✅ |
| 6 | Polish, a11y, error boundaries, smoke test | ✅ |
| 7 | DNS cutover to production | pending |
Backend contract
The new UI consumes the existing FastAPI at legal-ai/web/app.py. Key endpoints currently relied on:
| Endpoint | Hook | Used by |
|---|---|---|
GET /api/cases?detail=true |
useCases |
home table, KPIs |
GET /api/cases/{n}/details |
useCase |
case detail |
POST /api/cases/create |
useCreateCase |
wizard |
PUT /api/cases/{n} |
useUpdateCase |
inline edit |
DELETE /api/cases?case_number=... |
(MCP only so far) | admin cleanup |
POST /api/cases/{n}/documents/upload-tagged |
useUploadDocument |
upload sheet |
GET /api/progress/{task_id} (SSE) |
useProgress |
upload progress |
GET /api/cases/{n}/research/analysis |
useResearchAnalysis |
compose |
PATCH .../chair-position |
useSaveChairPosition |
chair editor |
GET /api/training/style-report |
useStyleReport |
training/report tab |
GET /api/training/corpus |
useCorpus |
training/corpus tab |
GET /api/training/compare |
useCompare |
training/compare tab |
DELETE /api/training/corpus/{id} |
useDeleteCorpusEntry |
corpus tab |
GET /api/system/diagnostics |
useDiagnostics |
diagnostics page |
GET /api/admin/skills |
useSkills |
skills page |
Any new endpoint should get a typed hook in src/lib/api/ — do not reach into fetch from component code.