147 lines
3.8 KiB
TypeScript
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');
|
|
})();
|
|
}
|