Dashboard Development
The dashboard is a Next.js 14 App Router web application that provides a browser-based interface for the same legal analysis skills available in Claude Code. It supports both demo mode (zero API cost, fixture data) and live mode (real Anthropic API calls with SSE streaming).
Tech Stack
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 14 | App Router framework |
| React | 18 | UI library |
| TypeScript | 5.x | Type safety |
| Tailwind CSS | 3.x | Utility-first styling |
| shadcn/ui | latest | Component library (Radix UI primitives) |
| Radix UI | latest | Accessible headless components |
| Provider adapters | current | Anthropic and OpenAI review analysis |
| lucide-react | latest | Icons |
| next-themes | latest | Dark/light theme support |
Setup
cd dashboard
npm install
npm run devThe development server starts at http://127.0.0.1:3000. Hot module replacement is enabled -- changes to components and pages appear immediately.
TIP
No environment variables are required for demo mode. For live mode, users provide their own provider API key via the browser UI -- it is stored in localStorage and sent with each request. The server never stores API keys.
Available Commands
| Command | Description |
|---|---|
npm run dev | Start Next.js dev server (localhost:3000) |
npm run build | Production build (next build) |
npm run lint | Run ESLint |
npm test | Compile TypeScript to .test-dist/ then run node --test |
npm run smoke | HTTP route smoke tests (scripts/api-smoke.mjs) |
npm run ui-smoke | Headless browser UI smoke tests (scripts/ui-smoke.mjs) |
WARNING
Always run npm run build before submitting a PR. The CI pipeline runs a production build and will catch type errors that npm run dev might miss.
Test Workflow
Important: Tests Run from Compiled Output
Dashboard tests run against compiled JavaScript, not TypeScript source. The npm test command first compiles .ts files to .test-dist/ using tsc -p tsconfig.test.json, then runs node --test on the emitted .test.js files.
This means: if you edit a .ts test file, you must re-run npm test (which rebuilds) for the change to take effect. Running node --test directly on stale .test-dist/ files will use the old compiled version and will not reflect your edits.
Full Test Run
cd dashboard
npm testThis executes two steps:
tsc -p tsconfig.test.json-- compiles*.test.tsto.test-dist/node --test .test-dist/**/*.test.js-- runs all compiled tests
Single Test File
After a full npm test has compiled the tests, you can run a single file for faster iteration:
# First compile (only needed once after editing)
npm test
# Then run a single test file repeatedly
node --test .test-dist/lib/review-utils.test.jsAdding a New Test
- Create
dashboard/lib/your-module.test.tsalongside the module being tested - Use the
node:testrunner (not Jest or Vitest):
import { describe, it } from "node:test"
import assert from "node:assert/strict"
describe("yourFunction", () => {
it("should handle the expected case", () => {
const result = yourFunction(input)
assert.strictEqual(result, expected)
})
})- Run
npm testto compile and execute - For faster iteration on a single test, run
npm testonce then usenode --test .test-dist/lib/your-module.test.js
Smoke Tests
npm run smoke # HTTP route checks (scripts/api-smoke.mjs)
npm run ui-smoke # Headless browser UI checks (scripts/ui-smoke.mjs)The smoke tests verify that:
- All API routes respond with correct status codes
- The UI renders without JavaScript errors
- Navigation between pages works
- Demo mode functions without an API key
Adding a New Page
1. Create the Page Component
Create dashboard/app/your-page/page.tsx:
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export default function YourPage() {
return (
<div className="container mx-auto py-8 space-y-6">
<h1 className="text-3xl font-bold">Your Page Title</h1>
<Card>
<CardHeader>
<CardTitle>Section</CardTitle>
</CardHeader>
<CardContent>
<p>Content goes here.</p>
</CardContent>
</Card>
</div>
)
}INFO
All pages that use React hooks or browser APIs must include "use client" at the top. Server Components are the default in Next.js 14 App Router.
2. Add to the Sidebar
Open dashboard/components/sidebar.tsx and add a navigation entry:
{
name: "Your Page",
href: "/your-page",
icon: YourIcon,
}Adding an API Route
1. Create the Route Handler
Create dashboard/app/api/your-route/route.ts:
import { NextRequest, NextResponse } from "next/server"
import { validateReviewRouteBody, mapReviewRouteError } from "@/lib/api-route-utils"
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validation = validateReviewRouteBody(body)
if (!validation.ok) {
return NextResponse.json(
{ error: validation.error },
{ status: validation.status }
)
}
// Your route logic here
const result = await processRequest(validation)
return NextResponse.json(result)
} catch (error) {
const mapped = mapReviewRouteError(error)
return NextResponse.json(
{ error: mapped.error },
{ status: mapped.status }
)
}
}2. Follow Existing Patterns
Study the existing route handlers for consistent patterns:
| Route | Path | Purpose |
|---|---|---|
| Review | app/api/review/route.ts | Main analysis endpoint (supports SSE streaming) |
| Anthropic | app/api/anthropic/validate/route.ts | API key validation |
| Document | app/api/document/extract/route.ts | Document text extraction |
| Legislation | app/api/legislation/search/route.ts | Legislation search |
| Compare | app/api/compare/route.ts | Contract comparison |
| Generate | app/api/generate/route.ts | Document generation |
3. SSE Streaming Pattern
For long-running analysis routes, support SSE when the request Accept header is text/event-stream:
if (request.headers.get("accept") === "text/event-stream") {
const stream = streamSkillAnalysis(apiKey, skillId, documentText)
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
}
// Fallback to buffered JSON for non-streaming clients
const result = await runSkillAnalysis(apiKey, skillId, documentText)
return NextResponse.json(result)TIP
The SSE protocol uses named events: progress (with stage and percent), result (final review object), and error (error message). Follow this convention for consistency.
Adding a Component
Use Existing UI Primitives
The dashboard uses shadcn/ui components built on Radix UI. Always prefer existing primitives:
| Component | Import | Common Use |
|---|---|---|
Card | @/components/ui/card | Content containers |
Badge | @/components/ui/badge | Status indicators, tags |
Button | @/components/ui/button | Actions |
Dialog | @/components/ui/dialog | Modal windows |
Tabs | @/components/ui/tabs | Tabbed content |
Table | @/components/ui/table | Data tables |
Select | @/components/ui/select | Dropdown selection |
Input | @/components/ui/input | Text inputs |
Creating a New Component
// dashboard/components/my-component.tsx
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
interface MyComponentProps {
title: string
status: "pass" | "fail" | "warning"
children: React.ReactNode
}
export function MyComponent({ title, status, children }: MyComponentProps) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{title}</CardTitle>
<Badge variant={status === "pass" ? "default" : "destructive"}>
{status}
</Badge>
</div>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
)
}Theming
The dashboard uses CSS variables for theming. All colour values reference hsl(var(--variable)) tokens defined in the global stylesheet. Do not use hard-coded colour values:
// Correct -- uses theme tokens
className="text-primary bg-muted border-border"
// Incorrect -- hard-coded colours
className="text-blue-600 bg-gray-100 border-gray-300"Adding a Skill to the Dashboard
To make a new Claude Code skill available in the web UI, update these files:
| # | File | Change |
|---|---|---|
| 1 | lib/skills-data.ts | Add skill definition (id, name, command, category, description) |
| 2 | lib/api.ts | Add system prompt to SKILL_PROMPTS and type mapping to SKILL_TO_TYPE |
| 3 | lib/api-route-utils.ts | Add skill ID to VALID_LIVE_REVIEW_SKILLS array |
| 4 | app/review/page.tsx | Add skill ID to LIVE_SUPPORTED_SKILLS Set |
See Adding Skills -- Step 4 for detailed instructions.
Project Structure
dashboard/
app/
api/ # Route handlers
review/route.ts # Main analysis (SSE + JSON)
anthropic/validate/route.ts # API key validation
document/extract/route.ts # File text extraction
legislation/ # Legislation lookup routes
compare/route.ts # Contract comparison
generate/route.ts # Document generation
review/page.tsx # Review page
generate/page.tsx # Document generator page
layout.tsx # Root layout
page.tsx # Home page
components/
ui/ # shadcn/ui primitives (Card, Badge, Button, etc.)
sidebar.tsx # Navigation sidebar
review-view.tsx # Review results display
demo-*.tsx # Demo mode components
lib/
api.ts # Anthropic SDK: runSkillAnalysis, streamSkillAnalysis
api-route-utils.ts # Request validation, error mapping, VALID_LIVE_REVIEW_SKILLS
skills-data.ts # Skill catalog (id, name, command, category, description)
types.ts # TypeScript interfaces for all review types
document-extraction.ts # TXT / DOCX / PDF text extraction
document-ocr.ts # OCR fallback (OpenAI vision)
review-utils.ts # Review lifecycle helpers
storage-utils.ts # Client-side persistence helpers
storage.ts # localStorage wrapper
legislation.ts # Statute data
linkify-statutes.tsx # Statute link rendering in JSX
demo-data/ # Fixtures for demo mode (zero API cost)
public/ # Static assets
.test-dist/ # Compiled test output (gitignored)
tsconfig.json # Main TypeScript config
tsconfig.test.json # Test TypeScript config