/** * Browser → template-server client. Mirrors every endpoint in * `components/server/index.ts`. Each function builds a request, throws on * non-2xx with the server's error body, and parses the response — keeping * the React components free of fetch boilerplate. * * IMPORTANT: keep these as relative requests. Kazzle injects the * sibling server URL into Vite only so the dev-server proxy can route these * calls inside the checkout computer. Browser code must not read or fetch * KAZZLE_APP_COMPONENT_*_URL directly. */ import type { AsyncJob, ChatMessage, ChatOptions, ChatResponse, ImageResult, StructuredOutputSchema, TranscriptionResult, } from './types'; async function jsonRequest(path: string, body: unknown): Promise { const res = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`); return res.json() as Promise; } export async function sendChat(messages: ChatMessage[], options: ChatOptions = {}): Promise { const data = await jsonRequest('/chat', { messages, ...options }); return data.message; } export async function sendStructuredChat( messages: ChatMessage[], jsonSchema: StructuredOutputSchema, options: Omit = {}, ): Promise { const data = await jsonRequest('/chat', { messages, ...options, response_format: { type: 'json_schema', json_schema: jsonSchema, }, }); if (!('structured' in data)) throw new Error('/chat returned no structured output'); return data.structured as T; } export async function generateImage(prompt: string): Promise { return jsonRequest('/image', { prompt }); } export async function synthesizeSpeech(text: string, voice = 'alloy'): Promise { const res = await fetch('/speech', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, voice }), }); if (!res.ok) throw new Error(`/speech failed: ${res.status} ${await res.text()}`); return res.blob(); } export async function transcribeAudio(audio: Blob): Promise { const form = new FormData(); form.append('file', audio, 'audio.mp3'); const res = await fetch('/transcribe', { method: 'POST', body: form }); if (!res.ok) throw new Error(`/transcribe failed: ${res.status} ${await res.text()}`); return res.json() as Promise; } export async function startVideoJob(prompt: string): Promise { return jsonRequest('/video', { prompt }); } export async function pollVideoJob(jobId: string): Promise { const res = await fetch(`/video/${encodeURIComponent(jobId)}`); if (!res.ok) throw new Error(`/video/${jobId} failed: ${res.status} ${await res.text()}`); return res.json() as Promise; }