teja141290 commited on
Commit
1007fa0
·
1 Parent(s): be54038

Add bundled demo samples to PolicyTrace UI

Browse files
sample_data/policytrace_demo_pack/Certificate of Motor Insurance - Demo.pdf ADDED
Binary file (3.38 kB). View file
 
sample_data/policytrace_demo_pack/Policy Booklet - Demo.pdf ADDED
Binary file (3.03 kB). View file
 
sample_data/policytrace_demo_pack/Schedule of Insurance - Demo.pdf ADDED
Binary file (7.08 kB). View file
 
sample_data/policytrace_demo_pack/Statement of Fact - Demo.pdf ADDED
Binary file (3.07 kB). View file
 
src/api.py CHANGED
@@ -4,6 +4,8 @@ api.py — FastAPI server for the UK Motor Insurance Visual Audit Review UI.
4
  Endpoints
5
  ─────────
6
  GET /api/health
 
 
7
  POST /api/process — upload PDFs, run pipeline, return session_id
8
  GET /api/session/{id} — full GoldenRecordWithProvenance JSON
9
  GET /api/pdf/{session_id}/{file} — serve source PDF (path-traversal safe)
@@ -26,6 +28,7 @@ import uuid
26
  from datetime import datetime
27
  from pathlib import Path
28
  from typing import Optional
 
29
 
30
  # ── Ensure src/ is on sys.path so sibling modules resolve regardless of CWD ─
31
  sys.path.insert(0, str(Path(__file__).parent))
@@ -59,6 +62,7 @@ _DEBUG_DIR = Path(__file__).parent.parent / "output" / "debug"
59
  _DEBUG_DIR.mkdir(parents=True, exist_ok=True)
60
 
61
  _STATIC_DIR = Path(__file__).parent.parent / "ui" / "dist"
 
62
 
63
  # ---------------------------------------------------------------------------
64
  # App
@@ -136,6 +140,33 @@ def _count_leaves(obj: object) -> int:
136
  return 1
137
 
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  # ---------------------------------------------------------------------------
140
  # Endpoints
141
  # ---------------------------------------------------------------------------
@@ -146,6 +177,74 @@ async def health():
146
  return {"status": "ok", "version": "1.0.0"}
147
 
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  # ── POST /api/process ────────────────────────────────────────────────────────
150
 
151
  class ProcessResponse(BaseModel):
 
4
  Endpoints
5
  ─────────
6
  GET /api/health
7
+ GET /api/samples - list bundled synthetic demo PDFs
8
+ GET /api/samples/{file} - download a bundled synthetic demo PDF
9
  POST /api/process — upload PDFs, run pipeline, return session_id
10
  GET /api/session/{id} — full GoldenRecordWithProvenance JSON
11
  GET /api/pdf/{session_id}/{file} — serve source PDF (path-traversal safe)
 
28
  from datetime import datetime
29
  from pathlib import Path
30
  from typing import Optional
31
+ from urllib.parse import quote
32
 
33
  # ── Ensure src/ is on sys.path so sibling modules resolve regardless of CWD ─
34
  sys.path.insert(0, str(Path(__file__).parent))
 
62
  _DEBUG_DIR.mkdir(parents=True, exist_ok=True)
63
 
64
  _STATIC_DIR = Path(__file__).parent.parent / "ui" / "dist"
65
+ _SAMPLE_PACK_DIR = Path(__file__).parent.parent / "sample_data" / "policytrace_demo_pack"
66
 
67
  # ---------------------------------------------------------------------------
68
  # App
 
140
  return 1
141
 
142
 
143
+ def _sample_pdf_paths() -> list[Path]:
144
+ """Return demo PDFs in manifest order, falling back to alphabetical order."""
145
+ if not _SAMPLE_PACK_DIR.exists():
146
+ return []
147
+
148
+ pdfs_by_name = {
149
+ path.name: path
150
+ for path in sorted(_SAMPLE_PACK_DIR.glob("*.pdf"), key=lambda p: p.name.lower())
151
+ }
152
+ manifest_file = _SAMPLE_PACK_DIR / "manifest.json"
153
+ if not manifest_file.exists():
154
+ return list(pdfs_by_name.values())
155
+
156
+ try:
157
+ manifest = json.loads(manifest_file.read_text(encoding="utf-8"))
158
+ except json.JSONDecodeError:
159
+ return list(pdfs_by_name.values())
160
+
161
+ ordered: list[Path] = []
162
+ for filename in manifest.get("files", []):
163
+ path = pdfs_by_name.pop(Path(filename).name, None)
164
+ if path:
165
+ ordered.append(path)
166
+ ordered.extend(pdfs_by_name.values())
167
+ return ordered
168
+
169
+
170
  # ---------------------------------------------------------------------------
171
  # Endpoints
172
  # ---------------------------------------------------------------------------
 
177
  return {"status": "ok", "version": "1.0.0"}
178
 
179
 
180
+ class SampleFileInfo(BaseModel):
181
+ filename: str
182
+ size_bytes: int
183
+ url: str
184
+
185
+
186
+ class SamplePackResponse(BaseModel):
187
+ pack_name: str
188
+ description: str
189
+ warning: str
190
+ expected_policy_number: Optional[str] = None
191
+ expected_vrm: Optional[str] = None
192
+ files: list[SampleFileInfo]
193
+
194
+
195
+ @app.get("/api/samples", response_model=SamplePackResponse)
196
+ async def list_samples():
197
+ """Return the bundled synthetic PDFs that are safe for public demos."""
198
+ manifest: dict = {}
199
+ manifest_file = _SAMPLE_PACK_DIR / "manifest.json"
200
+ if manifest_file.exists():
201
+ try:
202
+ manifest = json.loads(manifest_file.read_text(encoding="utf-8"))
203
+ except json.JSONDecodeError:
204
+ logger.warning("Could not parse sample manifest at %s", manifest_file)
205
+
206
+ files = [
207
+ SampleFileInfo(
208
+ filename=path.name,
209
+ size_bytes=path.stat().st_size,
210
+ url=f"/api/samples/{quote(path.name)}",
211
+ )
212
+ for path in _sample_pdf_paths()
213
+ ]
214
+ return SamplePackResponse(
215
+ pack_name="PolicyTrace demo pack",
216
+ description=manifest.get(
217
+ "purpose",
218
+ "Synthetic demo data for AI Tool Stack PolicyTrace.",
219
+ ),
220
+ warning=manifest.get(
221
+ "warning",
222
+ "No real customer, insurer, vehicle, or policy data is included.",
223
+ ),
224
+ expected_policy_number=manifest.get("expected_policy_number"),
225
+ expected_vrm=manifest.get("expected_vrm"),
226
+ files=files,
227
+ )
228
+
229
+
230
+ @app.get("/api/samples/{filename}")
231
+ async def serve_sample_pdf(filename: str):
232
+ """Serve a bundled synthetic PDF from the public demo pack."""
233
+ safe_name = Path(filename).name
234
+ if not safe_name.lower().endswith(".pdf"):
235
+ raise HTTPException(status_code=400, detail="Only PDF files can be served.")
236
+
237
+ pdf_path = _SAMPLE_PACK_DIR / safe_name
238
+ if not pdf_path.exists() or not pdf_path.is_file():
239
+ raise HTTPException(status_code=404, detail=f"Sample PDF '{safe_name}' not found.")
240
+
241
+ return FileResponse(
242
+ str(pdf_path),
243
+ media_type="application/pdf",
244
+ headers={"Content-Disposition": f'inline; filename="{safe_name}"'},
245
+ )
246
+
247
+
248
  # ── POST /api/process ────────────────────────────────────────────────────────
249
 
250
  class ProcessResponse(BaseModel):
ui/src/UploadPage.tsx CHANGED
@@ -1,13 +1,15 @@
1
- import { useCallback, useState } from 'react'
2
  import { useNavigate } from 'react-router-dom'
3
  import { api } from './api'
4
  import { useStore } from './store'
 
5
  import logoUrl from './assets/ai-toolstack-logo.svg'
6
 
7
  const BRAND = {
8
- dark: '#1F2937',
9
- blue: '#2563EB',
10
- teal: '#008080',
 
11
  } as const
12
 
13
  export function UploadPage() {
@@ -18,19 +20,56 @@ export function UploadPage() {
18
  const [error, setError] = useState<string | null>(null)
19
  const [files, setFiles] = useState<File[]>([])
20
  const [dragOver, setDragOver] = useState(false)
 
 
21
 
22
- const handleFiles = useCallback((incoming: FileList | null) => {
23
- if (!incoming) return
24
- const pdfs = Array.from(incoming).filter((f) => f.name.toLowerCase().endsWith('.pdf'))
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  setFiles((prev) => {
26
  const names = new Set(prev.map((f) => f.name))
27
  return [...prev, ...pdfs.filter((f) => !names.has(f.name))]
28
  })
29
  }, [])
30
 
 
 
 
 
 
31
  const removeFile = (name: string) =>
32
  setFiles((prev) => prev.filter((f) => f.name !== name))
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  const handleSubmit = async () => {
35
  if (!files.length) return
36
  setLoading(true)
@@ -71,9 +110,9 @@ export function UploadPage() {
71
  <div className="mb-8 text-center">
72
  <div className="inline-flex items-center gap-2 mb-4">
73
  <svg width="28" height="28" viewBox="0 0 28 28" fill="none" aria-hidden="true">
74
- <path d="M4 18L14 22L24 18" stroke={BRAND.dark} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
75
- <path d="M4 14L14 18L24 14" stroke={BRAND.blue} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
76
- <path d="M4 10L14 14L24 10L14 6L4 10Z" stroke={BRAND.teal} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
77
  </svg>
78
  <h1 className="text-2xl font-bold tracking-tight" style={{ color: BRAND.dark }}>
79
  PolicyTrace
@@ -128,6 +167,51 @@ export function UploadPage() {
128
  />
129
  </div>
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  {/* File list */}
132
  {files.length > 0 && (
133
  <ul className="mt-4 space-y-2">
 
1
+ import { useCallback, useEffect, useState } from 'react'
2
  import { useNavigate } from 'react-router-dom'
3
  import { api } from './api'
4
  import { useStore } from './store'
5
+ import type { SamplePack } from './types'
6
  import logoUrl from './assets/ai-toolstack-logo.svg'
7
 
8
  const BRAND = {
9
+ dark: '#07111D',
10
+ blue: '#5166D6',
11
+ teal: '#4EA8A8',
12
+ mint: '#63E0DC',
13
  } as const
14
 
15
  export function UploadPage() {
 
20
  const [error, setError] = useState<string | null>(null)
21
  const [files, setFiles] = useState<File[]>([])
22
  const [dragOver, setDragOver] = useState(false)
23
+ const [samplePack, setSamplePack] = useState<SamplePack | null>(null)
24
+ const [loadingSamples, setLoadingSamples] = useState(false)
25
 
26
+ useEffect(() => {
27
+ let isMounted = true
28
+ api.getSamples()
29
+ .then((pack) => {
30
+ if (isMounted) setSamplePack(pack)
31
+ })
32
+ .catch(() => {
33
+ if (isMounted) setSamplePack(null)
34
+ })
35
+ return () => {
36
+ isMounted = false
37
+ }
38
+ }, [])
39
+
40
+ const addPdfFiles = useCallback((incoming: File[]) => {
41
+ const pdfs = incoming.filter((f) => f.name.toLowerCase().endsWith('.pdf'))
42
  setFiles((prev) => {
43
  const names = new Set(prev.map((f) => f.name))
44
  return [...prev, ...pdfs.filter((f) => !names.has(f.name))]
45
  })
46
  }, [])
47
 
48
+ const handleFiles = useCallback((incoming: FileList | null) => {
49
+ if (!incoming) return
50
+ addPdfFiles(Array.from(incoming))
51
+ }, [addPdfFiles])
52
+
53
  const removeFile = (name: string) =>
54
  setFiles((prev) => prev.filter((f) => f.name !== name))
55
 
56
+ const handleLoadSamples = async () => {
57
+ if (!samplePack?.files.length) return
58
+ setLoadingSamples(true)
59
+ setError(null)
60
+ try {
61
+ const demoFiles = await Promise.all(
62
+ samplePack.files.map((sample) => api.downloadSampleFile(sample)),
63
+ )
64
+ addPdfFiles(demoFiles)
65
+ } catch (err: unknown) {
66
+ const msg = err instanceof Error ? err.message : 'Could not load the demo sample files.'
67
+ setError(msg)
68
+ } finally {
69
+ setLoadingSamples(false)
70
+ }
71
+ }
72
+
73
  const handleSubmit = async () => {
74
  if (!files.length) return
75
  setLoading(true)
 
110
  <div className="mb-8 text-center">
111
  <div className="inline-flex items-center gap-2 mb-4">
112
  <svg width="28" height="28" viewBox="0 0 28 28" fill="none" aria-hidden="true">
113
+ <path d="M4 18L14 22L24 18" stroke={BRAND.blue} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
114
+ <path d="M4 14L14 18L24 14" stroke={BRAND.teal} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
115
+ <path d="M4 10L14 14L24 10L14 6L4 10Z" stroke={BRAND.mint} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
116
  </svg>
117
  <h1 className="text-2xl font-bold tracking-tight" style={{ color: BRAND.dark }}>
118
  PolicyTrace
 
167
  />
168
  </div>
169
 
170
+ {samplePack?.files.length ? (
171
+ <section className="mt-4 rounded-2xl border border-teal-100 bg-white p-4 shadow-sm">
172
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
173
+ <div>
174
+ <h2 className="text-sm font-semibold" style={{ color: BRAND.dark }}>
175
+ Try the synthetic demo pack
176
+ </h2>
177
+ <p className="mt-1 text-xs leading-relaxed text-gray-500">
178
+ Safe sample PDFs are bundled with the Hugging Face Space. Load them here,
179
+ then run the same upload and review flow.
180
+ </p>
181
+ {(samplePack.expected_policy_number || samplePack.expected_vrm) && (
182
+ <p className="mt-2 text-xs font-medium" style={{ color: BRAND.teal }}>
183
+ Expected: {samplePack.expected_policy_number}
184
+ {samplePack.expected_vrm ? ` / ${samplePack.expected_vrm}` : ''}
185
+ </p>
186
+ )}
187
+ </div>
188
+ <button
189
+ type="button"
190
+ onClick={handleLoadSamples}
191
+ disabled={loadingSamples}
192
+ className="shrink-0 rounded-xl px-4 py-2 text-xs font-semibold text-white transition-colors disabled:opacity-50"
193
+ style={{ backgroundColor: BRAND.teal }}
194
+ >
195
+ {loadingSamples ? 'Loading samples...' : 'Load demo PDFs'}
196
+ </button>
197
+ </div>
198
+
199
+ <div className="mt-3 flex flex-wrap gap-2">
200
+ {samplePack.files.map((sample) => (
201
+ <a
202
+ key={sample.filename}
203
+ href={sample.url}
204
+ target="_blank"
205
+ rel="noopener noreferrer"
206
+ className="inline-block max-w-full truncate rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs font-medium text-gray-600 hover:border-teal-200 hover:text-teal-700"
207
+ >
208
+ {sample.filename}
209
+ </a>
210
+ ))}
211
+ </div>
212
+ </section>
213
+ ) : null}
214
+
215
  {/* File list */}
216
  {files.length > 0 && (
217
  <ul className="mt-4 space-y-2">
ui/src/api.ts CHANGED
@@ -1,9 +1,19 @@
1
  import axios from 'axios'
2
- import type { ProcessResponse, ReviewAction, ReviewState, SessionData } from './types'
3
 
4
  const http = axios.create({ baseURL: '/' })
5
 
6
  export const api = {
 
 
 
 
 
 
 
 
 
 
7
  async processDocuments(files: File[]): Promise<ProcessResponse> {
8
  const form = new FormData()
9
  for (const f of files) form.append('files', f)
 
1
  import axios from 'axios'
2
+ import type { ProcessResponse, ReviewAction, ReviewState, SampleFile, SamplePack, SessionData } from './types'
3
 
4
  const http = axios.create({ baseURL: '/' })
5
 
6
  export const api = {
7
+ async getSamples(): Promise<SamplePack> {
8
+ const { data } = await http.get<SamplePack>('/api/samples')
9
+ return data
10
+ },
11
+
12
+ async downloadSampleFile(sample: SampleFile): Promise<File> {
13
+ const { data } = await http.get<Blob>(sample.url, { responseType: 'blob' })
14
+ return new File([data], sample.filename, { type: 'application/pdf' })
15
+ },
16
+
17
  async processDocuments(files: File[]): Promise<ProcessResponse> {
18
  const form = new FormData()
19
  for (const f of files) form.append('files', f)
ui/src/assets/ai-toolstack-logo.svg CHANGED
ui/src/types.ts CHANGED
@@ -168,3 +168,18 @@ export interface ProcessResponse {
168
  fields_extracted: number
169
  provenance_coverage: number
170
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  fields_extracted: number
169
  provenance_coverage: number
170
  }
171
+
172
+ export interface SampleFile {
173
+ filename: string
174
+ size_bytes: number
175
+ url: string
176
+ }
177
+
178
+ export interface SamplePack {
179
+ pack_name: string
180
+ description: string
181
+ warning: string
182
+ expected_policy_number?: string
183
+ expected_vrm?: string
184
+ files: SampleFile[]
185
+ }