Aarya2004
Deploy: sync hosted Space to local app (chat, document capture, Modal backends, pages, mobile/QR)
47b2a99 | // The single place that knows the API URLs. Everything else calls these. | |
| const THREAD_ID = "ui"; | |
| async function consumeStream(url, body, onEvent) { | |
| const res = await fetch(url, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ ...body, thread_id: THREAD_ID }), | |
| }); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ""; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const frames = buffer.split("\n\n"); | |
| buffer = frames.pop(); | |
| for (const frame of frames) { | |
| const line = frame.replace(/^data: /, "").trim(); | |
| if (line) onEvent(JSON.parse(line)); | |
| } | |
| } | |
| } | |
| // Which models fill each role right now (mode + per-role labels) for the badge. | |
| export async function modelInfo() { | |
| const res = await fetch("/api/model_info"); | |
| return res.json(); | |
| } | |
| // Upload one image (base64 data URL); returns the server-side path. | |
| export async function uploadImage(dataUrl, filename) { | |
| const res = await fetch("/api/upload", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ data: dataUrl, filename }), | |
| }); | |
| const out = await res.json(); | |
| return out.path; | |
| } | |
| // Stream a run; onEvent({type:"trace"|"pause"|"estimate", ...}) per SSE frame. | |
| export function forgeEstimateStream(transcript, trade, imagePaths, onEvent) { | |
| return consumeStream( | |
| "/api/forge_estimate_stream", | |
| { transcript, trade, image_paths: imagePaths }, | |
| onEvent, | |
| ); | |
| } | |
| // Resume a paused run with the human-supplied value; continues streaming. | |
| export function resumeEstimateStream(value, onEvent) { | |
| return consumeStream("/api/resume_estimate_stream", { value }, onEvent); | |
| } | |
| // Server-authoritative recompute of an edited estimate. | |
| export async function recalc(rows, jobTitle, taxRate) { | |
| const res = await fetch("/api/recalc", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ rows, job_title: jobTitle, tax_rate: taxRate }), | |
| }); | |
| return res.json(); | |
| } | |
| // Document Capture (ADR-0011): parse a handed-over document (supplier quote, spec | |
| // sheet) into {model, observations, proposed_items} for the human to confirm. | |
| export async function parseDocument(dataUrl, filename) { | |
| const res = await fetch("/api/parse_document", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ data: dataUrl, filename }), | |
| }); | |
| return res.json(); | |
| } | |
| // Transcribe a recorded voice note (base64 data URL) into text (Cohere Transcribe). | |
| export async function transcribeNote(dataUrl, filename) { | |
| const res = await fetch("/api/transcribe", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ data: dataUrl, filename }), | |
| }); | |
| const out = await res.json(); | |
| return out.transcript || ""; | |
| } | |
| // Refine the current estimate conversationally (the Digital Apprentice chat). | |
| // Carries the Refinement Thread (ADR-0013) in and back out. Returns | |
| // {estimate, reply, needs_price, changed, thread}. | |
| export async function chatAboutEstimate(message, rows, taxRate, thread, pending) { | |
| const res = await fetch("/api/chat", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ message, rows, tax_rate: taxRate, thread, pending }), | |
| }); | |
| return res.json(); | |
| } | |
| // --- Saved Estimates (ADR-0013): per-account Estimate Store. --- | |
| // Persist (create or update-in-place via `id`) a Saved Estimate + its thread. | |
| // Returns {id}. | |
| export async function saveEstimate(rows, jobTitle, taxRate, thread, id) { | |
| const res = await fetch("/api/save_estimate", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ rows, job_title: jobTitle, tax_rate: taxRate, thread, id }), | |
| }); | |
| return res.json(); | |
| } | |
| // The account's saved estimates, newest first ({estimates: [{id, job_title, total}]}). | |
| export async function listEstimates() { | |
| const res = await fetch("/api/estimates"); | |
| return res.json(); | |
| } | |
| // Reopen one saved estimate (frozen snapshot + thread), or null if missing. | |
| export async function loadEstimate(id) { | |
| const res = await fetch(`/api/estimate/${id}`); | |
| if (!res.ok) return null; | |
| return res.json(); | |
| } | |
| // Discard a saved estimate. | |
| export async function deleteEstimate(id) { | |
| await fetch(`/api/estimate/${id}`, { method: "DELETE" }); | |
| } | |
| // --- QR phone-capture pairing (Tier 3). --- | |
| // Open a pairing for this desktop session. Returns {code, capture_url, qr_svg}. | |
| export async function createPairing() { | |
| const res = await fetch("/api/pair/create", { method: "POST" }); | |
| return res.json(); | |
| } | |
| // Poll for the phone's capture (delivered once). Returns the capture or null. | |
| export async function pollPairing(code) { | |
| const res = await fetch(`/api/pair/${code}`); | |
| const out = await res.json(); | |
| return out.capture; | |
| } | |
| // Translate the customer-facing estimate copy into a language (Cohere Aya). | |
| export async function translateEstimate(rows, jobTitle, taxRate, language) { | |
| const res = await fetch("/api/translate", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ rows, job_title: jobTitle, tax_rate: taxRate, language }), | |
| }); | |
| return res.json(); | |
| } | |
| // Download a PDF of the current (edited) estimate. | |
| export async function downloadPdf(rows, jobTitle, taxRate) { | |
| const res = await fetch("/api/pdf", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ rows, job_title: jobTitle, tax_rate: taxRate }), | |
| }); | |
| const blob = await res.blob(); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = "estimate.pdf"; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Finalize & Send (S10): deliver the estimate to a customer by SMS or email. | |
| // Returns {status:"sent"|"drafted", transmitted, channel, recipient, summary, provider_id}. | |
| // On bad input the server replies 400 with a plain-text reason. | |
| export async function sendEstimate(channel, recipient, rows, jobTitle, taxRate) { | |
| const res = await fetch("/api/send_estimate", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ channel, recipient, rows, job_title: jobTitle, tax_rate: taxRate }), | |
| }); | |
| if (!res.ok) { | |
| const reason = await res.text(); | |
| throw new Error(reason || "Send failed."); | |
| } | |
| return res.json(); | |
| } | |
| // Download a machine-readable JSON of the current estimate (the "no lock-in" export). | |
| export async function downloadJson(rows, jobTitle, taxRate) { | |
| const res = await fetch("/api/export_json", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ rows, job_title: jobTitle, tax_rate: taxRate }), | |
| }); | |
| const payload = await res.json(); | |
| const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = "estimate.json"; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| } | |