// ai-client.jsx — frontend wrapper that talks to the assistant proxy. // // One function: streamAssistant({ messages, personality, taskHint, onMeta, // onDelta, onDone, onError, signal }). Posts to /api/assistant, reads the // SSE stream, fires callbacks as events arrive. // // The proxy lives at http://localhost:3001 in dev. If you serve it from a // different host or port, set window.CHRONO_API_BASE before this script // runs, or pass `baseUrl` in the call. const DEFAULT_BASE_URL = 'http://localhost:3001'; async function streamAssistant({ messages, personality = 'concierge', taskHint = null, events = [], userContext = null, // NOTE: respect an empty-string base ('' = same origin). A plain `|| DEFAULT` // would wrongly fall back to localhost because '' is falsy. baseUrl = (typeof window !== 'undefined' && window.CHRONO_API_BASE != null) ? window.CHRONO_API_BASE : DEFAULT_BASE_URL, signal, onMeta, onDelta, onDone, onError, }) { let response; try { const headers = { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }; // Optional shared-secret for beta deployments. Set window.CHRONO_API_TOKEN // to authenticate against a proxy started with CHRONO_API_TOKEN set. const apiToken = typeof window !== 'undefined' && window.CHRONO_API_TOKEN; if (apiToken) headers['Authorization'] = `Bearer ${apiToken}`; response = await fetch(`${baseUrl}/api/assistant`, { method: 'POST', headers, body: JSON.stringify({ messages, personality, taskHint, events, userContext }), signal, }); } catch (err) { onError && onError({ message: `Cannot reach assistant proxy at ${baseUrl}. Is the server running?`, cause: err }); return; } if (!response.ok) { let detail = ''; try { detail = (await response.json()).error || ''; } catch (_) {} onError && onError({ message: detail || `HTTP ${response.status}`, status: response.status }); return; } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let currentEvent = 'message'; while (true) { let chunk; try { chunk = await reader.read(); } catch (err) { onError && onError({ message: 'stream read failed', cause: err }); return; } if (chunk.done) break; buffer += decoder.decode(chunk.value, { stream: true }); // SSE frames are separated by a blank line. let frameEnd; while ((frameEnd = buffer.indexOf('\n\n')) !== -1) { const raw = buffer.slice(0, frameEnd); buffer = buffer.slice(frameEnd + 2); const lines = raw.split('\n'); let evt = 'message'; let data = ''; for (const line of lines) { if (line.startsWith('event: ')) evt = line.slice(7).trim(); else if (line.startsWith('data: ')) data += line.slice(6); } if (!data) continue; let payload; try { payload = JSON.parse(data); } catch (_) { continue; } if (evt === 'meta') onMeta && onMeta(payload); else if (evt === 'delta') onDelta && onDelta(payload); else if (evt === 'done') { onDone && onDone(payload); return; } else if (evt === 'error') { onError && onError(payload); return; } } } } // Convenience: classify intent from frontend without calling the server. // Frontends can use this to set taskHint before sending (e.g. when the user // triggers /plan from a button — we know it's complex without a heuristic). function quickComplexity(text = '') { const lower = text.toLowerCase(); if (/\b(week|month|reschedule everything|rearrange|reorganize|all my events|multiple events)\b/.test(lower)) return 'complex'; return 'simple'; } // Submit beta feedback (bug report or feature idea) to the proxy. Returns // { ok: true } on success, or { ok: false, error } so the UI can show why. async function submitFeedback({ type, message, email, meta } = {}) { const baseUrl = (typeof window !== 'undefined' && window.CHRONO_API_BASE != null) ? window.CHRONO_API_BASE : DEFAULT_BASE_URL; const headers = { 'Content-Type': 'application/json' }; const apiToken = typeof window !== 'undefined' && window.CHRONO_API_TOKEN; if (apiToken) headers['Authorization'] = `Bearer ${apiToken}`; try { const res = await fetch(`${baseUrl}/api/feedback`, { method: 'POST', headers, body: JSON.stringify({ type, message, email, meta }), }); if (!res.ok) { let detail = ''; try { detail = (await res.json()).error || ''; } catch (_) {} return { ok: false, error: detail || `HTTP ${res.status}` }; } return { ok: true }; } catch (err) { return { ok: false, error: `Cannot reach the server at ${baseUrl}.` }; } } Object.assign(window, { streamAssistant, quickComplexity, submitFeedback });