Extraction is expensive (multi-minute LLM calls) and runs across every
appraisal in the case at once, so we don't kick it off silently on every
tag save. The chair tags the appraisals, then runs extraction once when
they're ready.
- New POST /api/cases/{n}/extract-appraiser-facts endpoint returns the
extractor's summary as-is: status=completed with fact counts and
conflicts, or status=sides_missing with the list of still-untagged
appraisal docs.
- DocumentTypeEditor now has a two-phase popover. After a successful
save on an appraisal doc, the body switches to a confirmation view
with a "חלץ עובדות שמאיות עכשיו" button. The result (completed /
sides_missing / no_appraisals / error) renders in the same popover
so the chair sees exactly which appraisals still need tagging
without closing and reopening anything.
- useExtractAppraiserFacts React-Query mutation invalidates the case
detail on success so downstream views (conflict rendering in
block-tet context) pick up the new facts.
עוזר משפטי — 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.