| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { API_URL } from "./api"; |
|
|
| const SESSION_KEY = "lexora-workspace-session"; |
| const SESSION_HEADER = "X-Lexora-Session"; |
|
|
| export interface WorkspaceDocument { |
| doc_id: string; |
| title: string; |
| source: string; |
| kind: "pdf" | "docx" | "image" | "html" | "text"; |
| pages: number; |
| chunks: number; |
| ocr_engine: string | null; |
| |
| truncated: boolean; |
| |
| coverage: number; |
| } |
|
|
| export interface Workspace { |
| session_id: string; |
| documents: WorkspaceDocument[]; |
| total_chunks: number; |
| |
| |
| calibrated: boolean; |
| limits: { |
| max_documents: number; |
| max_bytes: number; |
| max_pages: number; |
| session_ttl_s: number; |
| }; |
| } |
|
|
| export function readSession(): string | null { |
| if (typeof window === "undefined") return null; |
| try { |
| return window.sessionStorage.getItem(SESSION_KEY); |
| } catch { |
| return null; |
| } |
| } |
|
|
| function writeSession(id: string | null): void { |
| if (typeof window === "undefined" || !id) return; |
| try { |
| window.sessionStorage.setItem(SESSION_KEY, id); |
| } catch { |
| |
| } |
| } |
|
|
| function sessionHeaders(extra: HeadersInit = {}): HeadersInit { |
| const id = readSession(); |
| return id ? { ...extra, [SESSION_HEADER]: id } : extra; |
| } |
|
|
| |
| async function unwrap(response: Response): Promise<Workspace> { |
| writeSession(response.headers.get(SESSION_HEADER)); |
| if (!response.ok) { |
| let detail = `Upload failed (${response.status}).`; |
| try { |
| const body = (await response.json()) as { detail?: string }; |
| if (body.detail) detail = body.detail; |
| } catch { |
| |
| } |
| throw new Error(detail); |
| } |
| return (await response.json()) as Workspace; |
| } |
|
|
| export async function getWorkspace(signal?: AbortSignal): Promise<Workspace> { |
| return unwrap( |
| await fetch(`${API_URL}/api/workspace`, { headers: sessionHeaders(), signal }), |
| ); |
| } |
|
|
| export async function uploadFile(file: File, signal?: AbortSignal): Promise<Workspace> { |
| const form = new FormData(); |
| form.append("file", file); |
| |
| return unwrap( |
| await fetch(`${API_URL}/api/workspace/upload`, { |
| method: "POST", |
| headers: sessionHeaders(), |
| body: form, |
| signal, |
| }), |
| ); |
| } |
|
|
| export async function addLink(url: string, signal?: AbortSignal): Promise<Workspace> { |
| return unwrap( |
| await fetch(`${API_URL}/api/workspace/link`, { |
| method: "POST", |
| headers: sessionHeaders({ "Content-Type": "application/json" }), |
| body: JSON.stringify({ url }), |
| signal, |
| }), |
| ); |
| } |
|
|
| export async function removeDocument(docId: string): Promise<Workspace> { |
| return unwrap( |
| await fetch(`${API_URL}/api/workspace/${encodeURIComponent(docId)}`, { |
| method: "DELETE", |
| headers: sessionHeaders(), |
| }), |
| ); |
| } |
|
|
| export const ACCEPTED_FILE_TYPES = |
| ".pdf,.docx,.txt,.md,.markdown,.rst,.csv,.log,.html,.htm,.png,.jpg,.jpeg,.webp,.tif,.tiff,.bmp"; |
|
|
| |
| export function describeKind(document: WorkspaceDocument): string { |
| const pages = document.pages === 1 ? "1 page" : `${document.pages} pages`; |
| const base: Record<WorkspaceDocument["kind"], string> = { |
| pdf: "PDF", |
| docx: "Word document", |
| image: "Image", |
| html: "Web page", |
| text: "Text", |
| }; |
| const read = document.ocr_engine |
| ? ` · read by ${document.ocr_engine === "claude-vision" ? "Claude vision" : "OCR"}` |
| : ""; |
| return `${base[document.kind]} · ${pages}${read}`; |
| } |
|
|
| |
| |
| |
| |
| |
| export function coverageWarning(document: WorkspaceDocument): string | null { |
| if (document.truncated) { |
| return "Only the first part of this document was read — it is longer than the page limit."; |
| } |
| if (document.coverage < 0.999) { |
| return `About ${Math.round((1 - document.coverage) * 100)}% of this document could not be read.`; |
| } |
| return null; |
| } |
|
|