2026-09-03 00:10:18 +04:00

70 lines
2.2 KiB
JavaScript

/**
* 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 };