/** * Thin REST client for the local backend. * The page never talks to the robot directly - only to this origin. */ class ApiError extends Error { constructor(message, code, status) { super(message); this.name = 'ApiError'; this.code = code || 'error'; this.status = status || 0; } } async function request(path, options = {}) { let response; try { response = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...options, }); } catch (err) { // The backend itself is unreachable - a different failure from "robot offline". throw new ApiError( 'Cannot reach the local server. Is backend/main.py still running?', 'backend_down', 0, ); } let body = null; const text = await response.text(); if (text) { try { body = JSON.parse(text); } catch { body = { error: text }; } } if (!response.ok) { const message = (body && (body.error || body.detail)) || `Request failed (HTTP ${response.status})`; throw new ApiError( typeof message === 'string' ? message : JSON.stringify(message), (body && body.errorCode) || 'http_error', response.status, ); } return body; } export const api = { status: () => request('/api/robot/status'), config: () => request('/api/config'), diagnostics: () => request('/api/robot/diagnostics'), reloadConfig: () => request('/api/config/reload', { method: 'POST' }), reconnect: () => request('/api/robot/reconnect', { method: 'POST' }), speak: (text) => request('/api/robot/speak', { method: 'POST', body: JSON.stringify({ text }), }), stop: () => request('/api/robot/stop', { method: 'POST' }), history: () => request('/api/speech/history'), clearHistory: () => request('/api/speech/history', { method: 'DELETE' }), audio: () => request('/api/audio'), audioUrl: (id) => `/api/audio/${id}/file`, deleteAudio: (id) => request(`/api/audio/${id}`, { method: 'DELETE' }), clearAudio: () => request('/api/audio', { method: 'DELETE' }), }; export { ApiError };