Extends Pagepiper's document shelving pipeline (renamed from "ingest" — see below) to cover the formats most likely to appear in a real-world engineering document corpus, prompted by scoping a STERIS licensing pitch that needs DOCX/ODT coverage. - Rename the ingest pipeline to "shelve" throughout (scripts/, app/api, tests, docs, frontend). "Glean" (Turnstone's term) was considered and rejected — that's a harvest metaphor for log/knowledge extraction, not a fit for documents entering a library. Documented as a general CF naming principle in the org-level CLAUDE.md. - Wire DOCX into the upload/scan UI, README, and docs — the extraction logic (heading-based chunking, table serialization) already existed but wasn't exposed to users or covered by tests. - Add ODT support via odfpy, mirroring DOCX's chunking strategy. - Add Apple Pages support via headless LibreOffice conversion to ODT. No maintained Python library parses the IWA format directly; libreoffice bundles libetonyek, the only real open-source Pages parser. Adds libreoffice-writer to the Docker image (~300-400MB) for this. - 24 new/updated tests across shelve_docx, shelve_odt, and shelve_pages; full suite (72 tests) passing. Known gaps not addressed here: no Windchill/DocPortal connector exists yet (metadata-only PowerShell recon only), Excel/.xlsx is unsupported, and circuitforge_core.tasks.dispatch_task does not currently exist in circuitforge-core — cf-orch dispatch is dead code, always falling through to local BackgroundTasks. See circuitforge-plans/pagepiper/superpowers/plans/2026-07-10-steris-licensing-pitch.md for the full writeup.
139 lines
4.5 KiB
TypeScript
139 lines
4.5 KiB
TypeScript
// web/src/api.ts
|
|
const BASE = import.meta.env.VITE_API_BASE ?? ""
|
|
|
|
export interface Document {
|
|
id: string
|
|
title: string
|
|
file_path: string
|
|
status: "pending" | "processing" | "ready" | "error"
|
|
task_id: string | null
|
|
page_count: number | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface SearchResult {
|
|
chunk_id: string
|
|
doc_id: string
|
|
page_number: number
|
|
text_snippet: string
|
|
bm25_score: number
|
|
}
|
|
|
|
export interface Citation {
|
|
doc_id: string
|
|
page_number: number
|
|
snippet: string
|
|
bm25_score: number | null
|
|
}
|
|
|
|
export interface ChatResponse {
|
|
answer: string
|
|
citations: Citation[]
|
|
}
|
|
|
|
export interface TaskStatus {
|
|
status: string
|
|
progress?: number
|
|
error?: string
|
|
}
|
|
|
|
export interface DocumentStatus {
|
|
id: string
|
|
status: "pending" | "processing" | "ready" | "error"
|
|
task_id: string | null
|
|
page_count: number | null
|
|
vec_count: number
|
|
error_msg: string | null
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
role: string
|
|
content: string
|
|
}
|
|
|
|
export const api = {
|
|
async getLibrary(): Promise<Document[]> {
|
|
const r = await fetch(`${BASE}/api/library`)
|
|
if (!r.ok) throw new Error(await r.text())
|
|
return r.json()
|
|
},
|
|
async scanLibrary(): Promise<{ discovered: number; queued: number; tasks: { doc_id: string; task_id: string }[] }> {
|
|
const r = await fetch(`${BASE}/api/library/scan`, { method: "POST" })
|
|
if (!r.ok) throw new Error(await r.text())
|
|
return r.json()
|
|
},
|
|
async reshelveDocument(docId: string): Promise<{ task_id: string }> {
|
|
const r = await fetch(`${BASE}/api/library/${docId}/reshelve`, { method: "POST" })
|
|
if (!r.ok) throw new Error(await r.text())
|
|
return r.json()
|
|
},
|
|
async deleteDocument(docId: string): Promise<void> {
|
|
const r = await fetch(`${BASE}/api/library/${docId}`, { method: "DELETE" })
|
|
if (!r.ok) throw new Error(await r.text())
|
|
},
|
|
async uploadDocument(file: File): Promise<{ doc_id: string; task_id: string | null; filename: string; status: string }> {
|
|
const form = new FormData()
|
|
form.append("file", file)
|
|
const r = await fetch(`${BASE}/api/library/upload`, { method: "POST", body: form })
|
|
if (!r.ok) throw new Error(await r.text())
|
|
return r.json()
|
|
},
|
|
async getTaskStatus(taskId: string): Promise<TaskStatus> {
|
|
const r = await fetch(`${BASE}/api/shelve/${taskId}`)
|
|
if (!r.ok) throw new Error(await r.text())
|
|
return r.json()
|
|
},
|
|
async getDocumentStatus(docId: string): Promise<DocumentStatus> {
|
|
const r = await fetch(`${BASE}/api/library/${docId}/status`)
|
|
if (!r.ok) throw new Error(await r.text())
|
|
return r.json()
|
|
},
|
|
async search(query: string, topK = 10, docIds?: string[]): Promise<SearchResult[]> {
|
|
const r = await fetch(`${BASE}/api/search`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ query, top_k: topK, doc_ids: docIds ?? null }),
|
|
})
|
|
if (!r.ok) throw new Error(await r.text())
|
|
return r.json()
|
|
},
|
|
async chat(
|
|
message: string,
|
|
history: ChatMessage[],
|
|
docIds?: string[],
|
|
topK = 5,
|
|
): Promise<ChatResponse> {
|
|
const r = await fetch(`${BASE}/api/chat`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ message, history, doc_ids: docIds ?? null, top_k: topK }),
|
|
})
|
|
if (!r.ok) {
|
|
const body = await r.json().catch(() => ({}))
|
|
const err: Error & { status?: number; detail?: unknown } = new Error(
|
|
(body as { detail?: { message?: string } }).detail?.message ?? "Request failed"
|
|
)
|
|
err.status = r.status
|
|
err.detail = (body as { detail?: unknown }).detail
|
|
throw err
|
|
}
|
|
return r.json()
|
|
},
|
|
async chatFeedbackStatus(): Promise<{ enabled: boolean }> {
|
|
const r = await fetch(`${BASE}/api/chat/feedback/status`)
|
|
if (!r.ok) return { enabled: false }
|
|
return r.json()
|
|
},
|
|
async submitChatFeedback(
|
|
rating: 1 | -1,
|
|
question: string,
|
|
answer: string,
|
|
docIds: string[],
|
|
): Promise<void> {
|
|
await fetch(`${BASE}/api/chat/feedback`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rating, question, answer, doc_ids: docIds }),
|
|
})
|
|
},
|
|
}
|