a5545e5687
Fastify rejetait le body vide annoncé application/json (FST_ERR_CTP_EMPTY_JSON_BODY) sur DELETE /api/conversations/:id. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016w5jRe87MGdd6AMvXQcHNi
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
// Client de l'API CHLOVA. Same-origin en prod (le backend sert le SPA) ;
|
|
// en dev, Vite proxifie /api vers le backend.
|
|
|
|
export interface Asset {
|
|
id: string;
|
|
type: string;
|
|
version: string;
|
|
riskTier: string;
|
|
status: string;
|
|
createdAt: number;
|
|
expiresAt: number | null;
|
|
execCount: number;
|
|
commitLink: string | null;
|
|
docLink: string | null;
|
|
}
|
|
|
|
export interface ConversationMeta {
|
|
id: string;
|
|
title: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
export interface ChatMessage {
|
|
role: "user" | "assistant";
|
|
content: string;
|
|
ts: number;
|
|
}
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
async function req<T>(path: string, token: string | null, init?: RequestInit): Promise<T> {
|
|
const headers: Record<string, string> = {};
|
|
// content-type seulement s'il y a un corps : sinon Fastify rejette un body
|
|
// vide annoncé en application/json (DELETE, etc.).
|
|
if (init?.body != null) headers["content-type"] = "application/json";
|
|
if (token) headers.authorization = `Bearer ${token}`;
|
|
const res = await fetch(`/api${path}`, { ...init, headers: { ...headers, ...(init?.headers as Record<string, string>) } });
|
|
if (!res.ok) {
|
|
const body = (await res.json().catch(() => ({}))) as { error?: string };
|
|
throw new ApiError(res.status, body.error ?? `HTTP ${res.status}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export const api = {
|
|
login: (user: string, password: string, totp: string) =>
|
|
req<{ token: string }>("/auth/login", null, {
|
|
method: "POST",
|
|
body: JSON.stringify({ user, password, totp }),
|
|
}),
|
|
|
|
chat: (token: string, message: string, conversationId?: string | null) =>
|
|
req<{ reply: string; conversationId: string | null }>("/chat", token, {
|
|
method: "POST",
|
|
body: JSON.stringify({ message, conversationId: conversationId ?? undefined }),
|
|
}),
|
|
|
|
conversations: (token: string) =>
|
|
req<{ conversations: ConversationMeta[] }>("/conversations", token),
|
|
|
|
conversation: (token: string, id: string) =>
|
|
req<{ messages: ChatMessage[] }>(`/conversations/${encodeURIComponent(id)}`, token),
|
|
|
|
deleteConversation: (token: string, id: string) =>
|
|
req<{ ok: boolean }>(`/conversations/${encodeURIComponent(id)}`, token, { method: "DELETE" }),
|
|
|
|
review: (token: string) => req<{ assets: Asset[] }>("/review", token),
|
|
|
|
approve: (token: string, id: string) =>
|
|
req<{ asset: Asset }>(`/review/${encodeURIComponent(id)}/approve`, token, { method: "POST" }),
|
|
|
|
refuse: (token: string, id: string) =>
|
|
req<{ asset: Asset }>(`/review/${encodeURIComponent(id)}/refuse`, token, { method: "POST" }),
|
|
|
|
state: (token: string) => req<{ phase: string; tools: number }>("/state", token),
|
|
};
|