Commit ·
8c75a7a
1
Parent(s): 6267e20
fix(ci): unblock UI job — anchor gitignore, include ui/src/libLast CI's Cannot find module @/lib/api (and @/lib/samples) turned outto be a gitignore over-match, not a tsconfig issue. .gitignore had barelib/ and lib64/ from its Python section — those aren't anchored, sothey silently matched ui/src/lib/ too, and neither api.ts nor samples.tsever made it into a commit. CI's fresh clone had an empty ui/src/lib/,tsc -b failed on the missing modules, docker-build gated behind and skipped.- .gitignore: anchor lib/ and lib64/ to repo root as /lib/, /lib64/- Force-add ui/src/lib/api.ts + ui/src/lib/samples.ts- Cascading fix: 'Parameter s implicitly has any' in Dropzone was because samples.ts couldn't resolve, so SampleDoc type didn't flow into the SAMPLE_DOCS.map callback. Present-again = inferred-again.
Browse files- .gitignore +2 -2
- ui/src/lib/api.ts +65 -0
- ui/src/lib/samples.ts +41 -0
.gitignore
CHANGED
|
@@ -18,8 +18,8 @@ dist/
|
|
| 18 |
downloads/
|
| 19 |
eggs/
|
| 20 |
.eggs/
|
| 21 |
-
lib/
|
| 22 |
-
lib64/
|
| 23 |
parts/
|
| 24 |
sdist/
|
| 25 |
var/
|
|
|
|
| 18 |
downloads/
|
| 19 |
eggs/
|
| 20 |
.eggs/
|
| 21 |
+
/lib/
|
| 22 |
+
/lib64/
|
| 23 |
parts/
|
| 24 |
sdist/
|
| 25 |
var/
|
ui/src/lib/api.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Thin fetch client for the FastAPI backend.
|
| 3 |
+
*
|
| 4 |
+
* Dev: Vite proxies /api/* to http://localhost:8000. See vite.config.ts.
|
| 5 |
+
* Prod: set VITE_API_BASE to your deployed API's origin.
|
| 6 |
+
*/
|
| 7 |
+
import type { APIErrorEnvelope, DocType, ExtractResponse } from "@/types";
|
| 8 |
+
|
| 9 |
+
const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? "/api";
|
| 10 |
+
|
| 11 |
+
export class APIError extends Error {
|
| 12 |
+
code: string;
|
| 13 |
+
requestId?: string | null;
|
| 14 |
+
details?: Record<string, unknown>;
|
| 15 |
+
|
| 16 |
+
constructor(envelope: APIErrorEnvelope["error"]) {
|
| 17 |
+
super(envelope.message);
|
| 18 |
+
this.code = envelope.code;
|
| 19 |
+
this.requestId = envelope.request_id;
|
| 20 |
+
this.details = envelope.details;
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
async function parseError(res: Response): Promise<never> {
|
| 25 |
+
let envelope: APIErrorEnvelope | null = null;
|
| 26 |
+
try {
|
| 27 |
+
envelope = (await res.json()) as APIErrorEnvelope;
|
| 28 |
+
} catch {
|
| 29 |
+
/* body wasn't JSON */
|
| 30 |
+
}
|
| 31 |
+
if (envelope?.error) throw new APIError(envelope.error);
|
| 32 |
+
throw new APIError({ code: `http_${res.status}`, message: res.statusText });
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export interface ExtractArgs {
|
| 36 |
+
file: File;
|
| 37 |
+
docType: DocType;
|
| 38 |
+
model?: string;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export async function extract({ file, docType, model }: ExtractArgs): Promise<ExtractResponse> {
|
| 42 |
+
const form = new FormData();
|
| 43 |
+
form.append("file", file, file.name);
|
| 44 |
+
form.append("doc_type", docType);
|
| 45 |
+
if (model) form.append("model", model);
|
| 46 |
+
|
| 47 |
+
const res = await fetch(`${API_BASE}/extract`, {
|
| 48 |
+
method: "POST",
|
| 49 |
+
body: form,
|
| 50 |
+
});
|
| 51 |
+
if (!res.ok) await parseError(res);
|
| 52 |
+
return (await res.json()) as ExtractResponse;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
export async function listSchemas(): Promise<{ doc_types: string[] }> {
|
| 56 |
+
const res = await fetch(`${API_BASE}/schemas`);
|
| 57 |
+
if (!res.ok) await parseError(res);
|
| 58 |
+
return await res.json();
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
export async function health(): Promise<{ status: string }> {
|
| 62 |
+
const res = await fetch(`${API_BASE}/health`);
|
| 63 |
+
if (!res.ok) await parseError(res);
|
| 64 |
+
return await res.json();
|
| 65 |
+
}
|
ui/src/lib/samples.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Sample documents surfaced as one-click "Try it" buttons on the landing page.
|
| 3 |
+
* Each sample points at a file in /public/samples/ that ships with the frontend.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import type { DocType } from "@/types";
|
| 7 |
+
|
| 8 |
+
export interface SampleDoc {
|
| 9 |
+
id: string;
|
| 10 |
+
label: string;
|
| 11 |
+
docType: DocType;
|
| 12 |
+
path: string;
|
| 13 |
+
description: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export const SAMPLE_DOCS: SampleDoc[] = [
|
| 17 |
+
{
|
| 18 |
+
id: "coffee-receipt",
|
| 19 |
+
label: "Coffee receipt",
|
| 20 |
+
docType: "receipt",
|
| 21 |
+
path: "/samples/coffee_receipt.png",
|
| 22 |
+
description: "A short cafe receipt — merchant, total, tax.",
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
id: "software-invoice",
|
| 26 |
+
label: "Software invoice",
|
| 27 |
+
docType: "invoice",
|
| 28 |
+
path: "/samples/software_invoice.pdf",
|
| 29 |
+
description: "A B2B software invoice with line items and net-30 terms.",
|
| 30 |
+
},
|
| 31 |
+
];
|
| 32 |
+
|
| 33 |
+
export async function loadSampleAsFile(sample: SampleDoc): Promise<File> {
|
| 34 |
+
const res = await fetch(sample.path);
|
| 35 |
+
if (!res.ok) {
|
| 36 |
+
throw new Error(`Failed to load sample: ${sample.path}`);
|
| 37 |
+
}
|
| 38 |
+
const blob = await res.blob();
|
| 39 |
+
const filename = sample.path.split("/").pop() ?? "sample";
|
| 40 |
+
return new File([blob], filename, { type: blob.type });
|
| 41 |
+
}
|