/** * Typed API client for Chrysopedia review queue endpoints. * * All functions use fetch() with JSON handling and throw on non-OK responses. * Base URL is empty so requests go through the Vite dev proxy or nginx in prod. */ // ── Types ─────────────────────────────────────────────────────────────────── export interface KeyMomentRead { id: string; source_video_id: string; technique_page_id: string | null; title: string; summary: string; start_time: number; end_time: number; content_type: string; plugins: string[] | null; raw_transcript: string | null; review_status: string; created_at: string; updated_at: string; } export interface ReviewQueueItem extends KeyMomentRead { video_filename: string; creator_name: string; } export interface ReviewQueueResponse { items: ReviewQueueItem[]; total: number; offset: number; limit: number; } export interface ReviewStatsResponse { pending: number; approved: number; edited: number; rejected: number; } export interface ReviewModeResponse { review_mode: boolean; } export interface MomentEditRequest { title?: string; summary?: string; start_time?: number; end_time?: number; content_type?: string; plugins?: string[]; } export interface MomentSplitRequest { split_time: number; } export interface MomentMergeRequest { target_moment_id: string; } export interface QueueParams { status?: string; offset?: number; limit?: number; } // ── Helpers ────────────────────────────────────────────────────────────────── const BASE = "/api/v1/review"; class ApiError extends Error { constructor( public status: number, public detail: string, ) { super(`API ${status}: ${detail}`); this.name = "ApiError"; } } async function request(url: string, init?: RequestInit): Promise { const res = await fetch(url, { ...init, headers: { "Content-Type": "application/json", ...init?.headers, }, }); if (!res.ok) { let detail = res.statusText; try { const body = await res.json(); detail = body.detail ?? detail; } catch { // body not JSON — keep statusText } throw new ApiError(res.status, detail); } return res.json() as Promise; } // ── Queue ──────────────────────────────────────────────────────────────────── export async function fetchQueue( params: QueueParams = {}, ): Promise { const qs = new URLSearchParams(); if (params.status) qs.set("status", params.status); if (params.offset !== undefined) qs.set("offset", String(params.offset)); if (params.limit !== undefined) qs.set("limit", String(params.limit)); const query = qs.toString(); return request( `${BASE}/queue${query ? `?${query}` : ""}`, ); } export async function fetchStats(): Promise { return request(`${BASE}/stats`); } // ── Actions ────────────────────────────────────────────────────────────────── export async function approveMoment(id: string): Promise { return request(`${BASE}/moments/${id}/approve`, { method: "POST", }); } export async function rejectMoment(id: string): Promise { return request(`${BASE}/moments/${id}/reject`, { method: "POST", }); } export async function editMoment( id: string, data: MomentEditRequest, ): Promise { return request(`${BASE}/moments/${id}`, { method: "PUT", body: JSON.stringify(data), }); } export async function splitMoment( id: string, splitTime: number, ): Promise { const body: MomentSplitRequest = { split_time: splitTime }; return request(`${BASE}/moments/${id}/split`, { method: "POST", body: JSON.stringify(body), }); } export async function mergeMoments( id: string, targetId: string, ): Promise { const body: MomentMergeRequest = { target_moment_id: targetId }; return request(`${BASE}/moments/${id}/merge`, { method: "POST", body: JSON.stringify(body), }); } // ── Mode ───────────────────────────────────────────────────────────────────── export async function getReviewMode(): Promise { return request(`${BASE}/mode`); } export async function setReviewMode( enabled: boolean, ): Promise { return request(`${BASE}/mode`, { method: "PUT", body: JSON.stringify({ review_mode: enabled }), }); }