Files
recommender/packages/frontend/src/api/client.ts
2026-04-20 19:37:33 -03:00

147 lines
3.8 KiB
TypeScript

import type { MediaType, Recommendation, RecommendationSummary, FeedbackEntry } from '../types/index.js';
const BASE = '/api';
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
headers: {
...(options?.body ? { 'Content-Type': 'application/json' } : {}),
},
...options,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`HTTP ${res.status}: ${text}`);
}
return res.json() as Promise<T>;
}
export function createRecommendation(body: {
main_prompt: string;
liked_series: string;
disliked_series: string;
themes: string;
brainstorm_count?: number;
media_type: MediaType;
use_web_search?: boolean;
use_validator?: boolean;
hard_requirements?: boolean;
self_expansive?: boolean;
expansive_passes?: number;
expansive_mode?: 'soft' | 'extreme';
}): Promise<{ id: string }> {
return request('/recommendations', {
method: 'POST',
body: JSON.stringify(body),
});
}
export function listRecommendations(): Promise<RecommendationSummary[]> {
return request('/recommendations');
}
export function getRecommendation(id: string): Promise<Recommendation> {
return request(`/recommendations/${id}`);
}
export function rerankRecommendation(id: string): Promise<{ ok: boolean }> {
return request(`/recommendations/${id}/rerank`, { method: 'POST' });
}
export function submitFeedback(body: {
item_name: string;
stars: number;
feedback?: string;
}): Promise<{ ok: boolean }> {
return request('/feedback', {
method: 'POST',
body: JSON.stringify(body),
});
}
export function getFeedback(): Promise<FeedbackEntry[]> {
return request('/feedback');
}
export function deleteRecommendation(id: string): Promise<{ ok: boolean }> {
return request(`/recommendations/${id}`, { method: 'DELETE' });
}
export function createContinuousRecommendation(body: {
liked_series: string;
disliked_series?: string;
themes?: string;
requirements?: string;
avoid?: string;
total_count: number;
media_type: MediaType;
use_web_search?: boolean;
validate_results?: boolean;
}): Promise<{ id: string }> {
return (async () => {
const res = await fetch(`${BASE}/recommendations/continuous`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'text/event-stream',
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`HTTP ${res.status}: ${text}`);
}
if (!res.body) {
throw new Error('Missing response stream for continuous recommendation');
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { value, done } = await reader.read();
if (value) {
buffer += decoder.decode(value, { stream: !done });
}
let boundary = buffer.indexOf('\n\n');
while (boundary !== -1) {
const rawEvent = buffer.slice(0, boundary).trim();
buffer = buffer.slice(boundary + 2);
if (rawEvent) {
const dataLine = rawEvent
.split('\n')
.find((line) => line.startsWith('data:'));
if (dataLine) {
const payload = dataLine.slice(5).trim();
const event = JSON.parse(payload) as { type?: string; id?: string };
if (event.type === 'created' && event.id) {
await reader.cancel();
return { id: event.id };
}
}
}
boundary = buffer.indexOf('\n\n');
}
if (done) {
break;
}
}
} finally {
reader.releaseLock();
}
throw new Error('Continuous recommendation stream ended before returning an id');
})();
}