Quillwright / quillwright /web /js /client.js
Aarya2004
Deploy: sync hosted Space to local app (chat, document capture, Modal backends, pages, mobile/QR)
47b2a99
Raw
History Blame Contribute Delete
7.22 kB
// 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);
}