/** * Typed API client for Chrysopedia public endpoints. * * Mirrors backend schemas: SearchResponse, TechniquePageDetail, TopicCategory, CreatorBrowseItem. * Uses the same request pattern as client.ts. */ // ── Types ─────────────────────────────────────────────────────────────────── export interface SearchResultItem { title: string; slug: string; type: string; score: number; summary: string; creator_name: string; creator_slug: string; topic_category: string; topic_tags: string[]; } export interface SearchResponse { items: SearchResultItem[]; total: number; query: string; fallback_used: boolean; } export interface KeyMomentSummary { id: string; title: string; summary: string; start_time: number; end_time: number; content_type: string; plugins: string[] | null; video_filename: string; } export interface CreatorInfo { name: string; slug: string; genres: string[] | null; } export interface RelatedLinkItem { target_title: string; target_slug: string; relationship: string; } export interface TechniquePageDetail { id: string; title: string; slug: string; topic_category: string; topic_tags: string[] | null; summary: string | null; body_sections: Record | null; signal_chains: unknown[] | null; plugins: string[] | null; creator_id: string; source_quality: string | null; view_count: number; review_status: string; created_at: string; updated_at: string; key_moments: KeyMomentSummary[]; creator_info: CreatorInfo | null; related_links: RelatedLinkItem[]; version_count: number; } export interface TechniquePageVersionSummary { version_number: number; created_at: string; pipeline_metadata: Record | null; } export interface TechniquePageVersionListResponse { items: TechniquePageVersionSummary[]; total: number; } export interface TechniquePageVersionDetail { version_number: number; content_snapshot: Record; pipeline_metadata: Record | null; created_at: string; } export interface TechniqueListItem { id: string; title: string; slug: string; topic_category: string; topic_tags: string[] | null; summary: string | null; creator_id: string; source_quality: string | null; view_count: number; review_status: string; created_at: string; updated_at: string; } export interface TechniqueListResponse { items: TechniqueListItem[]; total: number; offset: number; limit: number; } export interface TopicSubTopic { name: string; technique_count: number; creator_count: number; } export interface TopicCategory { name: string; description: string; sub_topics: TopicSubTopic[]; } export interface CreatorBrowseItem { id: string; name: string; slug: string; genres: string[] | null; folder_name: string; view_count: number; created_at: string; updated_at: string; technique_count: number; video_count: number; } export interface CreatorBrowseResponse { items: CreatorBrowseItem[]; total: number; offset: number; limit: number; } export interface CreatorDetailResponse { id: string; name: string; slug: string; genres: string[] | null; folder_name: string; view_count: number; created_at: string; updated_at: string; video_count: number; } // ── Helpers ────────────────────────────────────────────────────────────────── const BASE = "/api/v1"; 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: unknown = await res.json(); if (typeof body === "object" && body !== null && "detail" in body) { const d = (body as { detail: unknown }).detail; detail = typeof d === "string" ? d : Array.isArray(d) ? d.map((e: any) => e.msg || JSON.stringify(e)).join("; ") : JSON.stringify(d); } } catch { // body not JSON — keep statusText } throw new ApiError(res.status, detail); } return res.json() as Promise; } // ── Search ─────────────────────────────────────────────────────────────────── export async function searchApi( q: string, scope?: string, limit?: number, ): Promise { const qs = new URLSearchParams({ q }); if (scope) qs.set("scope", scope); if (limit !== undefined) qs.set("limit", String(limit)); return request(`${BASE}/search?${qs.toString()}`); } // ── Techniques ─────────────────────────────────────────────────────────────── export interface TechniqueListParams { limit?: number; offset?: number; category?: string; creator_slug?: string; } export async function fetchTechniques( params: TechniqueListParams = {}, ): Promise { const qs = new URLSearchParams(); if (params.limit !== undefined) qs.set("limit", String(params.limit)); if (params.offset !== undefined) qs.set("offset", String(params.offset)); if (params.category) qs.set("category", params.category); if (params.creator_slug) qs.set("creator_slug", params.creator_slug); const query = qs.toString(); return request( `${BASE}/techniques${query ? `?${query}` : ""}`, ); } export async function fetchTechnique( slug: string, ): Promise { return request(`${BASE}/techniques/${slug}`); } export async function fetchTechniqueVersions( slug: string, ): Promise { return request( `${BASE}/techniques/${slug}/versions`, ); } export async function fetchTechniqueVersion( slug: string, versionNumber: number, ): Promise { return request( `${BASE}/techniques/${slug}/versions/${versionNumber}`, ); } // ── Topics ─────────────────────────────────────────────────────────────────── export async function fetchTopics(): Promise { return request(`${BASE}/topics`); } // ── Creators ───────────────────────────────────────────────────────────────── export interface CreatorListParams { sort?: string; genre?: string; limit?: number; offset?: number; } export async function fetchCreators( params: CreatorListParams = {}, ): Promise { const qs = new URLSearchParams(); if (params.sort) qs.set("sort", params.sort); if (params.genre) qs.set("genre", params.genre); if (params.limit !== undefined) qs.set("limit", String(params.limit)); if (params.offset !== undefined) qs.set("offset", String(params.offset)); const query = qs.toString(); return request( `${BASE}/creators${query ? `?${query}` : ""}`, ); } export async function fetchCreator( slug: string, ): Promise { return request(`${BASE}/creators/${slug}`); } // ── Content Reports ───────────────────────────────────────────────────────── export interface ContentReportCreate { content_type: string; content_id?: string | null; content_title?: string | null; report_type: string; description: string; page_url?: string | null; } export interface ContentReport { id: string; content_type: string; content_id: string | null; content_title: string | null; report_type: string; description: string; status: string; admin_notes: string | null; page_url: string | null; created_at: string; resolved_at: string | null; } export interface ContentReportListResponse { items: ContentReport[]; total: number; offset: number; limit: number; } export async function submitReport( body: ContentReportCreate, ): Promise { return request(`${BASE}/reports`, { method: "POST", body: JSON.stringify(body), }); } export async function fetchReports(params: { status?: string; content_type?: string; offset?: number; limit?: number; } = {}): Promise { const qs = new URLSearchParams(); if (params.status) qs.set("status", params.status); if (params.content_type) qs.set("content_type", params.content_type); 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}/admin/reports${query ? `?${query}` : ""}`, ); } export async function updateReport( id: string, body: { status?: string; admin_notes?: string }, ): Promise { return request(`${BASE}/admin/reports/${id}`, { method: "PATCH", body: JSON.stringify(body), }); } // ── Pipeline Admin ────────────────────────────────────────────────────────── export interface PipelineVideoItem { id: string; filename: string; processing_status: string; creator_name: string; created_at: string | null; updated_at: string | null; event_count: number; total_tokens_used: number; last_event_at: string | null; } export interface PipelineVideoListResponse { items: PipelineVideoItem[]; total: number; } export interface PipelineEvent { id: string; video_id: string; stage: string; event_type: string; prompt_tokens: number | null; completion_tokens: number | null; total_tokens: number | null; model: string | null; duration_ms: number | null; payload: Record | null; created_at: string | null; } export interface PipelineEventListResponse { items: PipelineEvent[]; total: number; offset: number; limit: number; } export interface WorkerTask { id: string; name: string; args: unknown[]; time_start: number | null; } export interface WorkerInfo { name: string; active_tasks: WorkerTask[]; reserved_tasks: number; total_completed: number; uptime: string | null; pool_size: number | null; } export interface WorkerStatusResponse { online: boolean; workers: WorkerInfo[]; error?: string; } export interface TriggerResponse { status: string; video_id: string; current_processing_status?: string; } export interface RevokeResponse { status: string; video_id: string; tasks_revoked: number; } export async function fetchPipelineVideos(): Promise { return request(`${BASE}/admin/pipeline/videos`); } export async function fetchPipelineEvents( videoId: string, params: { offset?: number; limit?: number; stage?: string; event_type?: string; order?: "asc" | "desc" } = {}, ): Promise { const qs = new URLSearchParams(); if (params.offset !== undefined) qs.set("offset", String(params.offset)); if (params.limit !== undefined) qs.set("limit", String(params.limit)); if (params.stage) qs.set("stage", params.stage); if (params.event_type) qs.set("event_type", params.event_type); if (params.order) qs.set("order", params.order); const query = qs.toString(); return request( `${BASE}/admin/pipeline/events/${videoId}${query ? `?${query}` : ""}`, ); } export async function fetchWorkerStatus(): Promise { return request(`${BASE}/admin/pipeline/worker-status`); } export async function triggerPipeline(videoId: string): Promise { return request(`${BASE}/admin/pipeline/trigger/${videoId}`, { method: "POST", }); } export async function revokePipeline(videoId: string): Promise { return request(`${BASE}/admin/pipeline/revoke/${videoId}`, { method: "POST", }); }