teja141290 commited on
Commit
5e1be84
·
1 Parent(s): 62d8a6c

Light review UI in the ai-toolstack brand, plus small accuracy fixes

Browse files
config/settings.yaml CHANGED
@@ -32,7 +32,7 @@ pii:
32
  - UK_NIN # National Insurance Number
33
  - CREDIT_CARD
34
  - IBAN_CODE
35
- - LOCATION # postcodes / addresses
36
  - IP_ADDRESS
37
  - URL
38
 
@@ -50,7 +50,9 @@ debug:
50
  # When on, the API writes artifacts inside the session folder, so they are
51
  # removed with the session (DELETE /api/session/{id} or the TTL cleanup).
52
  enabled: false
53
- # Root folder for debug runs. Each execution creates a timestamped sub-folder.
 
 
54
  output_dir: "./output/debug"
55
  # Save the raw Markdown produced by docling for each PDF.
56
  save_markdown: true
 
32
  - UK_NIN # National Insurance Number
33
  - CREDIT_CARD
34
  - IBAN_CODE
35
+ - LOCATION # place names only; UK postcodes are NOT caught
36
  - IP_ADDRESS
37
  - URL
38
 
 
50
  # When on, the API writes artifacts inside the session folder, so they are
51
  # removed with the session (DELETE /api/session/{id} or the TTL cleanup).
52
  enabled: false
53
+ # Used only by the command-line runner (src/main.py): root folder for its debug
54
+ # runs, one timestamped sub-folder each. The API ignores it and writes debug
55
+ # output inside the session folder instead.
56
  output_dir: "./output/debug"
57
  # Save the raw Markdown produced by docling for each PDF.
58
  save_markdown: true
src/api.py CHANGED
@@ -259,8 +259,8 @@ async def process_documents(files: list[UploadFile] = File(...)):
259
 
260
  Returns a ``session_id`` which the UI uses for all subsequent requests.
261
 
262
- Note: This endpoint is synchronous and may take 30–90 seconds depending
263
- on Groq API response times.
264
  """
265
  if not files:
266
  raise HTTPException(status_code=400, detail="No files uploaded.")
 
259
 
260
  Returns a ``session_id`` which the UI uses for all subsequent requests.
261
 
262
+ Note: This endpoint is synchronous. A full four-PDF pack takes about two
263
+ to three minutes on Groq's free tier, mostly waiting on rate limits.
264
  """
265
  if not files:
266
  raise HTTPException(status_code=400, detail="No files uploaded.")
src/privacy.py CHANGED
@@ -3,7 +3,11 @@ privacy.py — PII detection and masking via Microsoft Presidio.
3
 
4
  Entities masked before any text is sent to the LLM:
5
  PERSON, PHONE_NUMBER, EMAIL_ADDRESS, UK_NHS, UK_NIN,
6
- CREDIT_CARD, IBAN_CODE, DATE_TIME (opt-in), LOCATION
 
 
 
 
7
 
8
  Usage
9
  -----
@@ -37,7 +41,7 @@ UK_MOTOR_ENTITIES: list[str] = [
37
  "UK_NIN", # National Insurance Number
38
  "CREDIT_CARD",
39
  "IBAN_CODE",
40
- "LOCATION", # postcodes / addresses
41
  "IP_ADDRESS",
42
  "URL",
43
  ]
 
3
 
4
  Entities masked before any text is sent to the LLM:
5
  PERSON, PHONE_NUMBER, EMAIL_ADDRESS, UK_NHS, UK_NIN,
6
+ CREDIT_CARD, IBAN_CODE, LOCATION, IP_ADDRESS, URL, DATE_TIME (opt-in)
7
+
8
+ Not masked: UK postcodes (Presidio has no UK postcode recogniser; LOCATION is
9
+ a named-entity model that catches place names, not postcodes) and often street
10
+ lines. Dates of birth are not masked unless DATE_TIME is enabled.
11
 
12
  Usage
13
  -----
 
41
  "UK_NIN", # National Insurance Number
42
  "CREDIT_CARD",
43
  "IBAN_CODE",
44
+ "LOCATION", # place names only; UK postcodes are NOT caught
45
  "IP_ADDRESS",
46
  "URL",
47
  ]
src/prompts.py CHANGED
@@ -11,8 +11,8 @@ Usage
11
  registry = PromptRegistry(config_path="custom.yaml")
12
 
13
  system_prompt = registry.get(DocumentType.SCHEDULE)
14
- print(registry.active_version) # → "v1"
15
- print(registry.available_versions) # → ["v1"]
16
  """
17
  from __future__ import annotations
18
 
 
11
  registry = PromptRegistry(config_path="custom.yaml")
12
 
13
  system_prompt = registry.get(DocumentType.SCHEDULE)
14
+ print(registry.active_version) # → "v2" (the active_version in config/prompts.yaml)
15
+ print(registry.available_versions) # → ["v1", "v2"]
16
  """
17
  from __future__ import annotations
18
 
ui/src/Brand.tsx CHANGED
@@ -1,16 +1,17 @@
1
  /** ai·toolstack brand primitives shared by every PolicyTrace screen. */
2
 
3
  export function Mark({ size = 24 }: { size?: number }) {
 
4
  return (
5
- <svg width={size} height={size} viewBox="0 0 56 56" aria-hidden="true" focusable="false">
6
- <rect x="0.5" y="0.5" width="55" height="55" rx="12" fill="#1F2940" stroke="#2C3752" />
7
- <rect x="12" y="14" width="32" height="6" rx="2" fill="#E0A946" />
8
- <rect x="16" y="25" width="28" height="6" rx="2" fill="#ECEAE4" />
9
- <rect x="12" y="36" width="32" height="6" rx="2" fill="#56617A" />
 
10
  </svg>
11
  )
12
  }
13
-
14
  export function Lockup({ product = 'PolicyTrace' }: { product?: string }) {
15
  return (
16
  <div className="flex items-center gap-3 min-w-0">
@@ -21,9 +22,9 @@ export function Lockup({ product = 'PolicyTrace' }: { product?: string }) {
21
  className="flex items-center gap-2.5 text-ink-100"
22
  aria-label="ai·toolstack home"
23
  >
24
- <Mark size={26} />
25
- <span className="text-[17px] font-semibold tracking-[-0.035em] hidden sm:inline">
26
- <span className="font-mono font-medium text-ink-300">ai·</span>toolstack
27
  </span>
28
  </a>
29
  <span className="text-ink-600 select-none hidden sm:inline" aria-hidden="true">/</span>
 
1
  /** ai·toolstack brand primitives shared by every PolicyTrace screen. */
2
 
3
  export function Mark({ size = 24 }: { size?: number }) {
4
+ // Three layers of a system; the middle one ends in an amber dot: a value traced to its source.
5
  return (
6
+ <svg width={size} height={size} viewBox="0 0 32 32" aria-hidden="true" focusable="false">
7
+ <rect width="32" height="32" rx="7" fill="#1F2940" />
8
+ <rect x="7" y="7.5" width="18" height="3" rx="1.5" fill="#8A93A8" />
9
+ <rect x="7" y="14.5" width="11" height="3" rx="1.5" fill="#F6F5F1" />
10
+ <circle cx="23.5" cy="16" r="2.4" fill="#E0A946" />
11
+ <rect x="7" y="21.5" width="18" height="3" rx="1.5" fill="#8A93A8" />
12
  </svg>
13
  )
14
  }
 
15
  export function Lockup({ product = 'PolicyTrace' }: { product?: string }) {
16
  return (
17
  <div className="flex items-center gap-3 min-w-0">
 
22
  className="flex items-center gap-2.5 text-ink-100"
23
  aria-label="ai·toolstack home"
24
  >
25
+ <Mark size={24} />
26
+ <span className="text-[17px] font-semibold tracking-[-0.02em] hidden sm:inline">
27
+ ai<span className="text-signal-deep">·</span>toolstack
28
  </span>
29
  </a>
30
  <span className="text-ink-600 select-none hidden sm:inline" aria-hidden="true">/</span>
ui/src/FieldRow.tsx CHANGED
@@ -72,7 +72,7 @@ export function FieldRow({ entry, sessionId, isActive, review, onClick }: Props)
72
  if (e.key === 'Escape') setEditing(false)
73
  }}
74
  />
75
- <button onClick={handleSaveOverride} className="text-[12px] font-medium px-3 rounded-md bg-ink text-ink-100 hover:bg-ink-700">Save</button>
76
  <button onClick={() => setEditing(false)} className="text-[12px] font-medium px-3 rounded-md border border-paper-rule text-paper-ink hover:bg-paper">Cancel</button>
77
  </div>
78
  ) : (
 
72
  if (e.key === 'Escape') setEditing(false)
73
  }}
74
  />
75
+ <button onClick={handleSaveOverride} className="text-[12px] font-medium px-3 rounded-md bg-ink-100 text-white hover:bg-black">Save</button>
76
  <button onClick={() => setEditing(false)} className="text-[12px] font-medium px-3 rounded-md border border-paper-rule text-paper-ink hover:bg-paper">Cancel</button>
77
  </div>
78
  ) : (
ui/src/PDFPane.tsx CHANGED
@@ -111,11 +111,11 @@ export function PDFPane({ sessionId }: Props) {
111
  aria-selected={on}
112
  onClick={() => setActivePdf(f)}
113
  className={`relative shrink-0 inline-flex items-center gap-2 h-9 px-3 rounded-md text-[13px] transition-colors ${
114
- on ? 'bg-ink-700 text-ink-100' : 'text-ink-300 hover:text-ink-100 hover:bg-ink-700/60'
115
  }`}
116
  >
117
  <span className="truncate max-w-[14rem]">{f.replace(/\.pdf$/i, '').replace(/ - Demo$/i, '')}</span>
118
- <span className={`font-mono text-[11px] ${on ? 'text-signal' : 'text-ink-400'}`}>{count}</span>
119
  {on && <span className="absolute left-3 right-3 -bottom-[9px] h-[2px] bg-signal rounded-full" aria-hidden="true" />}
120
  </button>
121
  )
@@ -125,7 +125,7 @@ export function PDFPane({ sessionId }: Props) {
125
  {/* PDF scroll area */}
126
  <div
127
  ref={containerRef}
128
- className="flex-1 overflow-y-auto pdf-scroll-container scroll-ink grid-bg bg-ink-900 p-4 sm:p-6 space-y-6"
129
  >
130
  {pdfUrl ? (
131
  <Document
@@ -156,7 +156,7 @@ export function PDFPane({ sessionId }: Props) {
156
  // the canvas dimensions exactly (inline-block can shrink-wrap)
157
  style={{ position: 'relative', width: containerWidth }}
158
  className={`rounded-sm overflow-hidden transition-shadow ${
159
- hasActive ? 'ring-2 ring-signal ring-offset-4 ring-offset-ink-900' : ''
160
  }`}
161
  >
162
  <Page
@@ -225,7 +225,7 @@ function LoadingPlaceholder() {
225
 
226
  function ErrorPlaceholder() {
227
  return (
228
- <div className="flex items-center justify-center p-12 text-[#F2A35E] text-sm font-mono">
229
  Failed to load PDF.
230
  </div>
231
  )
 
111
  aria-selected={on}
112
  onClick={() => setActivePdf(f)}
113
  className={`relative shrink-0 inline-flex items-center gap-2 h-9 px-3 rounded-md text-[13px] transition-colors ${
114
+ on ? 'bg-white text-ink-100 shadow-sheet' : 'text-ink-300 hover:text-ink-100 hover:bg-white/70'
115
  }`}
116
  >
117
  <span className="truncate max-w-[14rem]">{f.replace(/\.pdf$/i, '').replace(/ - Demo$/i, '')}</span>
118
+ <span className={`font-mono text-[11px] ${on ? 'text-signal-deep' : 'text-ink-400'}`}>{count}</span>
119
  {on && <span className="absolute left-3 right-3 -bottom-[9px] h-[2px] bg-signal rounded-full" aria-hidden="true" />}
120
  </button>
121
  )
 
125
  {/* PDF scroll area */}
126
  <div
127
  ref={containerRef}
128
+ className="flex-1 overflow-y-auto pdf-scroll-container scroll-ink wash p-4 sm:p-6 space-y-6"
129
  >
130
  {pdfUrl ? (
131
  <Document
 
156
  // the canvas dimensions exactly (inline-block can shrink-wrap)
157
  style={{ position: 'relative', width: containerWidth }}
158
  className={`rounded-sm overflow-hidden transition-shadow ${
159
+ hasActive ? 'ring-2 ring-signal ring-offset-4 ring-offset-[#F3F2EF]' : ''
160
  }`}
161
  >
162
  <Page
 
225
 
226
  function ErrorPlaceholder() {
227
  return (
228
+ <div className="flex items-center justify-center p-12 text-state-flag text-sm font-mono">
229
  Failed to load PDF.
230
  </div>
231
  )
ui/src/ReviewDashboard.tsx CHANGED
@@ -66,9 +66,9 @@ export function ReviewDashboard({ sessionId }: Props) {
66
  }
67
 
68
  const TONES = {
69
- pass: 'text-[#6FCF97]',
70
- override: 'text-[#A9B8E0]',
71
- flag: 'text-[#F2A35E]',
72
  } as const
73
 
74
  function Stat({ label, value, tone }: { label: string; value: number | string; tone?: keyof typeof TONES }) {
 
66
  }
67
 
68
  const TONES = {
69
+ pass: 'text-state-pass',
70
+ override: 'text-state-override',
71
+ flag: 'text-state-flag',
72
  } as const
73
 
74
  function Stat({ label, value, tone }: { label: string; value: number | string; tone?: keyof typeof TONES }) {
ui/src/SessionPage.tsx CHANGED
@@ -46,7 +46,7 @@ export function SessionPage() {
46
  return (
47
  <div className="min-h-screen flex items-center justify-center bg-ink grid-bg">
48
  <div className="text-center space-y-3">
49
- <svg className="animate-spin h-8 w-8 mx-auto text-signal" viewBox="0 0 24 24" fill="none">
50
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
51
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z" />
52
  </svg>
@@ -60,12 +60,12 @@ export function SessionPage() {
60
  return (
61
  <div className="min-h-screen flex items-center justify-center bg-ink grid-bg">
62
  <div className="text-center space-y-4 max-w-sm">
63
- <p role="alert" className="text-sm text-[#F2B79A] bg-state-flag/10 border border-state-flag/40 rounded-xl px-4 py-3">
64
  {error}
65
  </p>
66
  <button
67
  onClick={() => navigate('/')}
68
- className="text-sm font-medium text-signal hover:text-signal-hover"
69
  >
70
  ← Back to upload
71
  </button>
 
46
  return (
47
  <div className="min-h-screen flex items-center justify-center bg-ink grid-bg">
48
  <div className="text-center space-y-3">
49
+ <svg className="animate-spin h-8 w-8 mx-auto text-signal-deep" viewBox="0 0 24 24" fill="none">
50
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
51
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z" />
52
  </svg>
 
60
  return (
61
  <div className="min-h-screen flex items-center justify-center bg-ink grid-bg">
62
  <div className="text-center space-y-4 max-w-sm">
63
+ <p role="alert" className="text-sm text-state-flag bg-state-flag-tint border border-state-flag/40 rounded-xl px-4 py-3">
64
  {error}
65
  </p>
66
  <button
67
  onClick={() => navigate('/')}
68
+ className="text-sm font-medium text-signal-deep hover:text-ink-100"
69
  >
70
  ← Back to upload
71
  </button>
ui/src/UploadPage.tsx CHANGED
@@ -6,13 +6,13 @@ import type { SamplePack } from './types'
6
  import { ExternalIcon, LINKS, Lockup, Spinner } from './Brand'
7
 
8
  const PIPELINE = [
9
- ['01', 'parse', 'Docling layout + text'],
10
- ['02', 'pii_mask', 'Presidio, before any model call'],
11
- ['03', 'classify', 'route each document'],
12
- ['04', 'extract', 'typed record + verbatim citations'],
13
- ['05', 'arbitrate', 'source-authority rules'],
14
- ['06', 'provenance', 'match citations to PDF geometry'],
15
- ['07', 'review', 'you verify, flag or override'],
16
  ] as const
17
 
18
  export function UploadPage() {
@@ -111,27 +111,27 @@ export function UploadPage() {
111
 
112
  {/* Left: what this is */}
113
  <section className="space-y-7 lg:pt-4">
114
- <p className="eyebrow text-signal">Reference system · Document AI</p>
115
- <h1 className="text-[40px] sm:text-[52px] leading-[1.0] font-semibold tracking-[-0.045em] text-ink-100">
116
- Reviewable extraction for motor insurance packs.
117
  </h1>
118
  <p className="text-[17px] leading-relaxed text-ink-300 max-w-[34rem]">
119
- Upload a Schedule, Certificate, Statement of Fact and Policy Booklet. PolicyTrace builds one typed
120
- Golden Record, resolves disagreements between documents, and links every field back to the exact
121
- place it came from in the PDF.
122
  </p>
123
 
124
- <ol className="rounded-xl border border-ink-600 bg-ink-800/90 font-mono text-[13px] overflow-hidden" aria-label="Pipeline">
125
  {PIPELINE.map(([n, name, desc], i) => {
126
  const state = !loading ? 'idle' : i < step ? 'done' : i === step ? 'active' : 'idle'
127
  return (
128
  <li key={n} className="flex items-center gap-4 px-4 py-2.5 border-b border-dashed border-ink-700 last:border-b-0">
129
  <span className="w-6 text-ink-400">{n}</span>
130
- <span className={`w-24 ${state === 'active' ? 'text-signal' : 'text-ink-100'}`}>{name}</span>
131
  <span className="flex-1 text-ink-400 truncate">{desc}</span>
132
  <span className="w-14 text-right">
133
- {state === 'done' && <span className="text-[#6FCF97]">ok</span>}
134
- {state === 'active' && <span className="inline-flex text-signal"><Spinner className="h-3.5 w-3.5" /></span>}
135
  </span>
136
  </li>
137
  )
@@ -140,7 +140,7 @@ export function UploadPage() {
140
  </section>
141
 
142
  {/* Right: upload card */}
143
- <section className="rounded-2xl border border-ink-600 bg-ink-800 p-5 sm:p-6 shadow-page" aria-label="Upload documents">
144
  <div
145
  role="button"
146
  tabIndex={0}
@@ -151,7 +151,7 @@ export function UploadPage() {
151
  onDrop={(e) => { e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files) }}
152
  onClick={() => inputRef.current?.click()}
153
  className={`rounded-xl border border-dashed px-6 py-10 text-center cursor-pointer transition-colors ${
154
- dragOver ? 'border-signal bg-signal/10' : 'border-ink-500 hover:border-ink-400 bg-ink-900/40'
155
  }`}
156
  >
157
  <svg className={`mx-auto mb-4 h-9 w-9 ${dragOver ? 'text-signal' : 'text-ink-400'}`} fill="none" viewBox="0 0 24 24"
@@ -159,7 +159,7 @@ export function UploadPage() {
159
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 16V4m0 0-4 4m4-4 4 4M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
160
  </svg>
161
  <p className="text-[15px] font-medium text-ink-100">
162
- Drop PDFs here or <span className="text-signal">browse</span>
163
  </p>
164
  <p className="mt-1.5 font-mono text-[12px] text-ink-400">Schedule · Certificate · Statement of Fact · Booklet</p>
165
  <input ref={inputRef} type="file" accept=".pdf" multiple className="hidden"
@@ -167,7 +167,7 @@ export function UploadPage() {
167
  </div>
168
 
169
  {samplePack?.files.length ? (
170
- <div className="mt-4 rounded-xl border border-ink-600 bg-ink-900/50 p-4">
171
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
172
  <div>
173
  <p className="text-sm font-medium text-ink-100">No documents to hand?</p>
@@ -185,7 +185,7 @@ export function UploadPage() {
185
  {files.length > 0 && (
186
  <ul className="mt-4 space-y-2" aria-label="Selected files">
187
  {files.map((f) => (
188
- <li key={f.name} className="flex items-center justify-between gap-3 rounded-lg border border-ink-600 bg-ink-900/40 px-3.5 py-2.5">
189
  <div className="flex items-center gap-3 min-w-0">
190
  <span className="shrink-0 rounded font-mono text-[10px] font-medium px-1.5 py-0.5 bg-ink-700 text-ink-300">PDF</span>
191
  <span className="text-sm text-ink-200 truncate">{f.name}</span>
@@ -200,13 +200,13 @@ export function UploadPage() {
200
  )}
201
 
202
  {error && (
203
- <div role="alert" className="mt-4 rounded-lg border border-state-flag/40 bg-state-flag/10 px-3.5 py-3 text-sm text-[#F2B79A]">
204
  {error}
205
  </div>
206
  )}
207
 
208
  <button onClick={handleSubmit} disabled={!files.length || loading} className="btn-signal w-full mt-5 text-[15px]">
209
- {loading ? <><Spinner /> Extracting · usually 30–90 s</> : <>Extract &amp; review{files.length ? ` ${files.length} file${files.length > 1 ? 's' : ''}` : ''}</>}
210
  </button>
211
 
212
  <p className="mt-4 text-[12px] leading-relaxed text-ink-400">
@@ -217,7 +217,7 @@ export function UploadPage() {
217
  </main>
218
 
219
  <footer className="border-t border-ink-700 px-5 sm:px-8 py-4 flex flex-wrap gap-x-6 gap-y-1 justify-between font-mono text-[12px] text-ink-400">
220
- <span>PolicyTrace · a reference system from <a className="text-ink-300 hover:text-signal" href="https://www.ai-toolstack.com/" target="_blank" rel="noopener noreferrer">ai·toolstack</a></span>
221
  <span>Docling · Presidio · Groq · Instructor · FastAPI · React</span>
222
  </footer>
223
  </div>
 
6
  import { ExternalIcon, LINKS, Lockup, Spinner } from './Brand'
7
 
8
  const PIPELINE = [
9
+ ['01', 'parse', 'Docling: text and page positions'],
10
+ ['02', 'mask', 'Presidio: names out before any model call'],
11
+ ['03', 'classify', 'which document is which'],
12
+ ['04', 'extract', 'typed record + the exact phrase read'],
13
+ ['05', 'merge', 'a written rule picks the winner'],
14
+ ['06', 'trace', 'each value back to its PDF line'],
15
+ ['07', 'review', 'you verify, override or flag'],
16
  ] as const
17
 
18
  export function UploadPage() {
 
111
 
112
  {/* Left: what this is */}
113
  <section className="space-y-7 lg:pt-4">
114
+ <p className="inline-flex font-mono text-[12px] uppercase tracking-[0.06em] text-[#2F5FA8] bg-[#E8EFFA] rounded-full px-2.5 py-1">Reference system · Document AI</p>
115
+ <h1 className="text-[34px] sm:text-[44px] leading-[1.08] font-semibold tracking-[-0.03em] text-ink-100">
116
+ A policy pack in. One checked record out.
117
  </h1>
118
  <p className="text-[17px] leading-relaxed text-ink-300 max-w-[34rem]">
119
+ Upload the Schedule, Certificate, Statement of Fact and policy booklet. PolicyTrace pulls the facts a
120
+ claims handler needs into one typed record, settles disagreements between documents with a written rule,
121
+ and links every value to the line in the PDF it came from.
122
  </p>
123
 
124
+ <ol className="rounded-xl border border-ink-700 bg-white font-mono text-[13px] overflow-hidden" aria-label="Pipeline">
125
  {PIPELINE.map(([n, name, desc], i) => {
126
  const state = !loading ? 'idle' : i < step ? 'done' : i === step ? 'active' : 'idle'
127
  return (
128
  <li key={n} className="flex items-center gap-4 px-4 py-2.5 border-b border-dashed border-ink-700 last:border-b-0">
129
  <span className="w-6 text-ink-400">{n}</span>
130
+ <span className={`w-24 ${state === 'active' ? 'text-signal-deep' : 'text-ink-100'}`}>{name}</span>
131
  <span className="flex-1 text-ink-400 truncate">{desc}</span>
132
  <span className="w-14 text-right">
133
+ {state === 'done' && <span className="text-state-pass">ok</span>}
134
+ {state === 'active' && <span className="inline-flex text-signal-deep"><Spinner className="h-3.5 w-3.5" /></span>}
135
  </span>
136
  </li>
137
  )
 
140
  </section>
141
 
142
  {/* Right: upload card */}
143
+ <section className="rounded-2xl border border-ink-700 bg-white p-5 sm:p-6 shadow-page" aria-label="Upload documents">
144
  <div
145
  role="button"
146
  tabIndex={0}
 
151
  onDrop={(e) => { e.preventDefault(); setDragOver(false); handleFiles(e.dataTransfer.files) }}
152
  onClick={() => inputRef.current?.click()}
153
  className={`rounded-xl border border-dashed px-6 py-10 text-center cursor-pointer transition-colors ${
154
+ dragOver ? 'border-signal bg-signal-tint' : 'border-ink-500 hover:border-ink-400 bg-ink-800'
155
  }`}
156
  >
157
  <svg className={`mx-auto mb-4 h-9 w-9 ${dragOver ? 'text-signal' : 'text-ink-400'}`} fill="none" viewBox="0 0 24 24"
 
159
  <path strokeLinecap="round" strokeLinejoin="round" d="M12 16V4m0 0-4 4m4-4 4 4M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2" />
160
  </svg>
161
  <p className="text-[15px] font-medium text-ink-100">
162
+ Drop PDFs here or <span className="text-signal-deep underline decoration-signal underline-offset-2">browse</span>
163
  </p>
164
  <p className="mt-1.5 font-mono text-[12px] text-ink-400">Schedule · Certificate · Statement of Fact · Booklet</p>
165
  <input ref={inputRef} type="file" accept=".pdf" multiple className="hidden"
 
167
  </div>
168
 
169
  {samplePack?.files.length ? (
170
+ <div className="mt-4 rounded-xl border border-ink-700 bg-ink-800 p-4">
171
  <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
172
  <div>
173
  <p className="text-sm font-medium text-ink-100">No documents to hand?</p>
 
185
  {files.length > 0 && (
186
  <ul className="mt-4 space-y-2" aria-label="Selected files">
187
  {files.map((f) => (
188
+ <li key={f.name} className="flex items-center justify-between gap-3 rounded-lg border border-ink-700 bg-ink-800 px-3.5 py-2.5">
189
  <div className="flex items-center gap-3 min-w-0">
190
  <span className="shrink-0 rounded font-mono text-[10px] font-medium px-1.5 py-0.5 bg-ink-700 text-ink-300">PDF</span>
191
  <span className="text-sm text-ink-200 truncate">{f.name}</span>
 
200
  )}
201
 
202
  {error && (
203
+ <div role="alert" className="mt-4 rounded-lg border border-state-flag/40 bg-state-flag/10 px-3.5 py-3 text-sm text-state-flag">
204
  {error}
205
  </div>
206
  )}
207
 
208
  <button onClick={handleSubmit} disabled={!files.length || loading} className="btn-signal w-full mt-5 text-[15px]">
209
+ {loading ? <><Spinner /> Extracting · usually 2–3 min</> : <>Extract &amp; review{files.length ? ` ${files.length} file${files.length > 1 ? 's' : ''}` : ''}</>}
210
  </button>
211
 
212
  <p className="mt-4 text-[12px] leading-relaxed text-ink-400">
 
217
  </main>
218
 
219
  <footer className="border-t border-ink-700 px-5 sm:px-8 py-4 flex flex-wrap gap-x-6 gap-y-1 justify-between font-mono text-[12px] text-ink-400">
220
+ <span>PolicyTrace · a reference system from <a className="text-ink-300 hover:text-signal-deep" href="https://www.ai-toolstack.com/" target="_blank" rel="noopener noreferrer">ai·toolstack</a></span>
221
  <span>Docling · Presidio · Groq · Instructor · FastAPI · React</span>
222
  </footer>
223
  </div>
ui/src/index.css CHANGED
@@ -22,7 +22,7 @@
22
  @apply bg-ink text-ink-100 font-sans antialiased;
23
  text-rendering: optimizeLegibility;
24
  }
25
- ::selection { background: rgba(224, 169, 70, 0.35); }
26
  :focus-visible { outline: 2px solid #E0A946; outline-offset: 2px; border-radius: 6px; }
27
  }
28
 
@@ -35,14 +35,11 @@
35
  disabled:opacity-50 disabled:cursor-not-allowed;
36
  min-height: 44px;
37
  }
38
- .btn-signal { @apply btn bg-signal text-ink hover:bg-signal-hover; }
39
- .btn-ghost { @apply btn border border-ink-600 text-ink-100 hover:bg-ink-800; }
40
- .grid-bg {
41
- background-image:
42
- linear-gradient(rgba(44, 55, 82, 0.35) 1px, transparent 1px),
43
- linear-gradient(90deg, rgba(44, 55, 82, 0.35) 1px, transparent 1px);
44
- background-size: 56px 56px;
45
- }
46
  }
47
 
48
  /* ── react-pdf ──────────────────────────────────────────────────── */
@@ -52,7 +49,7 @@
52
  .pdf-scroll-container { scroll-behavior: smooth; }
53
 
54
  /* Thin scrollbars that suit both panes */
55
- .scroll-ink { scrollbar-color: #2C3752 transparent; scrollbar-width: thin; }
56
  .scroll-paper { scrollbar-color: #D3D0C6 transparent; scrollbar-width: thin; }
57
 
58
  @media (prefers-reduced-motion: reduce) {
 
22
  @apply bg-ink text-ink-100 font-sans antialiased;
23
  text-rendering: optimizeLegibility;
24
  }
25
+ ::selection { background: rgba(224, 169, 70, 0.30); }
26
  :focus-visible { outline: 2px solid #E0A946; outline-offset: 2px; border-radius: 6px; }
27
  }
28
 
 
35
  disabled:opacity-50 disabled:cursor-not-allowed;
36
  min-height: 44px;
37
  }
38
+ .btn { border-radius: 999px; }
39
+ .btn-signal { @apply btn bg-ink-100 text-white hover:bg-black; }
40
+ .btn-ghost { @apply btn border border-ink-500 bg-white text-ink-100 hover:border-ink-100; }
41
+ .grid-bg { background-color: #F6F5F1; }
42
+ .wash { background-color: #F3F2EF; }
 
 
 
43
  }
44
 
45
  /* ── react-pdf ──────────────────────────────────────────────────── */
 
49
  .pdf-scroll-container { scroll-behavior: smooth; }
50
 
51
  /* Thin scrollbars that suit both panes */
52
+ .scroll-ink { scrollbar-color: #D9D6CF transparent; scrollbar-width: thin; }
53
  .scroll-paper { scrollbar-color: #D3D0C6 transparent; scrollbar-width: thin; }
54
 
55
  @media (prefers-reduced-motion: reduce) {
ui/tailwind.config.js CHANGED
@@ -1,5 +1,5 @@
1
  /** @type {import('tailwindcss').Config} */
2
- // ai·toolstack design tokens (mirrors the ai-toolstack.com theme)
3
  export default {
4
  content: [
5
  "./index.html",
@@ -9,16 +9,18 @@ export default {
9
  extend: {
10
  colors: {
11
  ink: {
12
- DEFAULT: '#121826', // midnight slate ground
13
- 900: '#121826',
14
- 800: '#182033', // surface
15
- 700: '#1F2940', // raised
16
- 600: '#2C3752', // rule
17
- 500: '#3A4766',
18
- 400: '#7E88A2', // dim text (AA on ink)
19
- 300: '#9CA6BA', // muted text
20
- 200: '#CDD3DE', // body text on ink
21
- 100: '#ECEAE4', // bone
 
 
22
  },
23
  paper: {
24
  DEFAULT: '#F6F5F1',
@@ -48,7 +50,7 @@ export default {
48
  },
49
  boxShadow: {
50
  sheet: '0 1px 0 rgba(18,24,38,0.04), 0 8px 24px -16px rgba(18,24,38,0.25)',
51
- page: '0 24px 60px -30px rgba(0,0,0,0.65)',
52
  },
53
  },
54
  },
 
1
  /** @type {import('tailwindcss').Config} */
2
+ // ai·toolstack design tokens (mirrors the ai-toolstack.com light theme)
3
  export default {
4
  content: [
5
  "./index.html",
 
9
  extend: {
10
  colors: {
11
  ink: {
12
+ // Light scheme matching ai-toolstack.com 2.5. Names kept so classes still read
13
+ // "ground → surface → raised → rule → text"; values run light to dark.
14
+ DEFAULT: '#FFFFFF', // page ground
15
+ 900: '#FFFFFF',
16
+ 800: '#FAFAF8', // surface (bars, cards)
17
+ 700: '#ECEAE5', // raised / rule
18
+ 600: '#E2E0DA', // stronger rule
19
+ 500: '#D9D6CF',
20
+ 400: '#7A8190', // dim text
21
+ 300: '#5B6270', // muted text
22
+ 200: '#4A5160', // body text
23
+ 100: '#1A1D24', // ink
24
  },
25
  paper: {
26
  DEFAULT: '#F6F5F1',
 
50
  },
51
  boxShadow: {
52
  sheet: '0 1px 0 rgba(18,24,38,0.04), 0 8px 24px -16px rgba(18,24,38,0.25)',
53
+ page: '0 1px 2px rgba(26,29,36,0.06), 0 12px 32px -18px rgba(26,29,36,0.28)',
54
  },
55
  },
56
  },