Spaces:
Sleeping
Sleeping
Commit ·
5e1be84
1
Parent(s): 62d8a6c
Light review UI in the ai-toolstack brand, plus small accuracy fixes
Browse files- config/settings.yaml +4 -2
- src/api.py +2 -2
- src/privacy.py +6 -2
- src/prompts.py +2 -2
- ui/src/Brand.tsx +10 -9
- ui/src/FieldRow.tsx +1 -1
- ui/src/PDFPane.tsx +5 -5
- ui/src/ReviewDashboard.tsx +3 -3
- ui/src/SessionPage.tsx +3 -3
- ui/src/UploadPage.tsx +25 -25
- ui/src/index.css +7 -10
- ui/tailwind.config.js +14 -12
config/settings.yaml
CHANGED
|
@@ -32,7 +32,7 @@ pii:
|
|
| 32 |
- UK_NIN # National Insurance Number
|
| 33 |
- CREDIT_CARD
|
| 34 |
- IBAN_CODE
|
| 35 |
-
- LOCATION # postcodes
|
| 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 |
-
#
|
|
|
|
|
|
|
| 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
|
| 263 |
-
on Groq
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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) # → "
|
| 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
|
| 6 |
-
<rect
|
| 7 |
-
<rect x="
|
| 8 |
-
<rect x="
|
| 9 |
-
<
|
|
|
|
| 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={
|
| 25 |
-
<span className="text-[17px] font-semibold tracking-[-0.
|
| 26 |
-
<span className="
|
| 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-
|
| 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-
|
| 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
|
| 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-
|
| 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-
|
| 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-
|
| 70 |
-
override: 'text-
|
| 71 |
-
flag: 'text-
|
| 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-
|
| 64 |
{error}
|
| 65 |
</p>
|
| 66 |
<button
|
| 67 |
onClick={() => navigate('/')}
|
| 68 |
-
className="text-sm font-medium text-signal hover:text-
|
| 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
|
| 10 |
-
['02', '
|
| 11 |
-
['03', 'classify', '
|
| 12 |
-
['04', 'extract', 'typed record +
|
| 13 |
-
['05', '
|
| 14 |
-
['06', '
|
| 15 |
-
['07', 'review', 'you verify,
|
| 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="
|
| 115 |
-
<h1 className="text-[
|
| 116 |
-
|
| 117 |
</h1>
|
| 118 |
<p className="text-[17px] leading-relaxed text-ink-300 max-w-[34rem]">
|
| 119 |
-
Upload
|
| 120 |
-
|
| 121 |
-
|
| 122 |
</p>
|
| 123 |
|
| 124 |
-
<ol className="rounded-xl border border-ink-
|
| 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-
|
| 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-
|
| 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
|
| 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-
|
| 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-
|
| 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-
|
| 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
|
| 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 & 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.
|
| 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
|
| 39 |
-
.btn-
|
| 40 |
-
.
|
| 41 |
-
|
| 42 |
-
|
| 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: #
|
| 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 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
},
|