/** * 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[]; technique_page_slug?: string; match_context?: string; section_anchor?: string; section_heading?: string; } export interface SearchResponse { items: SearchResultItem[]; partial_matches: 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; source_video_id: string; video_filename: string; } export interface CreatorInfo { name: string; slug: string; genres: string[] | null; } export interface RelatedLinkItem { target_title: string; target_slug: string; relationship: string; creator_name: string; topic_category: string; reason: string; } export interface BodySubSectionV2 { heading: string; content: string; } export interface BodySectionV2 { heading: string; content: string; subsections: BodySubSectionV2[]; } export interface SourceVideoSummary { id: string; filename: string; content_type: string; added_at: string | null; } export interface TechniquePageDetail { id: string; title: string; slug: string; topic_category: string; topic_tags: string[] | null; summary: string | null; body_sections: BodySectionV2[] | Record | null; body_sections_format: string; signal_chains: unknown[] | null; plugins: string[] | null; creator_id: string; source_quality: string | null; view_count: number; created_at: string; updated_at: string; key_moments: KeyMomentSummary[]; creator_info: CreatorInfo | null; related_links: RelatedLinkItem[]; version_count: number; source_videos: SourceVideoSummary[]; } 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; creator_name: string; creator_slug: string; source_quality: string | null; view_count: number; key_moment_count: number; 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; last_technique_at: string | null; } export interface CreatorBrowseResponse { items: CreatorBrowseItem[]; total: number; offset: number; limit: number; } export interface CreatorTechniqueItem { title: string; slug: string; topic_category: string; summary: string | null; topic_tags: string[] | null; key_moment_count: number; created_at: string; } 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; bio: string | null; social_links: Record | null; featured: boolean; avatar_url: string | null; technique_count: number; moment_count: number; techniques: CreatorTechniqueItem[]; genre_breakdown: Record; } // ── 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 interface SuggestionItem { text: string; type: "topic" | "technique" | "creator"; } export interface SuggestionsResponse { suggestions: SuggestionItem[]; } export async function fetchSuggestions(): Promise { return request(`${BASE}/search/suggestions`); } export async function searchApi( q: string, scope?: string, limit?: number, sort?: string, ): Promise { const qs = new URLSearchParams({ q }); if (scope) qs.set("scope", scope); if (limit !== undefined) qs.set("limit", String(limit)); if (sort) qs.set("sort", sort); return request(`${BASE}/search?${qs.toString()}`); } // ── Techniques ─────────────────────────────────────────────────────────────── export interface TechniqueListParams { limit?: number; offset?: number; category?: string; creator_slug?: string; sort?: 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); if (params.sort) qs.set("sort", params.sort); 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 fetchRandomTechnique(): Promise<{ slug: string }> { return request<{ slug: string }>(`${BASE}/techniques/random`); } 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}`, ); } // ── Stats ───────────────────────────────────────────────────────────────────── export interface StatsResponse { technique_count: number; creator_count: number; } export async function fetchStats(): Promise { return request(`${BASE}/stats`); } // ── Popular Searches ───────────────────────────────────────────────────────── export interface PopularSearchItem { query: string; count: number; } export interface PopularSearchesResponse { items: PopularSearchItem[]; cached: boolean; } export async function fetchPopularSearches(): Promise { return request(`${BASE}/search/popular`); } // ── Topics ─────────────────────────────────────────────────────────────────── export async function fetchTopics(): Promise { return request(`${BASE}/topics`); } export async function fetchSubTopicTechniques( categorySlug: string, subtopicSlug: string, params: { limit?: number; offset?: number; sort?: string } = {}, ): 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.sort) qs.set("sort", params.sort); const query = qs.toString(); return request( `${BASE}/topics/${encodeURIComponent(categorySlug)}/${encodeURIComponent(subtopicSlug)}${query ? `?${query}` : ""}`, ); } // ── 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; active_stage: string | null; active_stage_status: string | null; stage_started_at: string | null; latest_run: { id: string; run_number: number; trigger: string; status: string; started_at: string | null; finished_at: string | null; error_stage: string | null; total_tokens: number; } | 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; system_prompt_text: string | null; user_prompt_text: string | null; response_text: string | 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 interface RecentActivityItem { id: string; video_id: string; filename: string; creator_name: string; stage: string; event_type: string; total_tokens: number | null; duration_ms: number | null; created_at: string | null; } export interface RecentActivityResponse { items: RecentActivityItem[]; } export async function fetchRecentActivity(limit = 10): Promise { return request(`${BASE}/admin/pipeline/recent-activity?limit=${limit}`); } export interface PipelineRunItem { id: string; run_number: number; trigger: string; status: string; started_at: string | null; finished_at: string | null; error_stage: string | null; total_tokens: number; event_count: number; } export interface PipelineRunsResponse { items: PipelineRunItem[]; legacy_event_count: number; } export async function fetchPipelineRuns(videoId: string): Promise { return request(`${BASE}/admin/pipeline/runs/${videoId}`); } export async function fetchPipelineEvents( videoId: string, params: { offset?: number; limit?: number; stage?: string; event_type?: string; run_id?: 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.run_id) qs.set("run_id", params.run_id); 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", }); } export interface CleanRetriggerResponse { status: string; video_id: string; cleaned: Record; } export async function cleanRetriggerPipeline(videoId: string): Promise { return request(`${BASE}/admin/pipeline/clean-retrigger/${videoId}`, { method: "POST", }); } // ── Chunking Inspector ───────────────────────────────────────────────────── export interface ChunkingTopicBoundary { topic_label: string; segment_count: number; start_time: number; end_time: number; start_index: number; end_index: number; } export interface ChunkingSynthesisGroup { category: string; moment_count: number; exceeds_chunk_threshold: boolean; chunks_needed: number; } export interface ChunkingDataResponse { video_id: string; total_segments: number; total_moments: number; classification_source: string; synthesis_chunk_size: number; topic_boundaries: ChunkingTopicBoundary[]; key_moments: Array<{ id: string; title: string; content_type: string; start_time: number; end_time: number; plugins: string[]; technique_page_id: string | null; }>; classification: Array>; synthesis_groups: ChunkingSynthesisGroup[]; } export async function fetchChunkingData(videoId: string): Promise { return request(`${BASE}/admin/pipeline/chunking/${videoId}`); } // ── Single-Stage Re-Run ──────────────────────────────────────────────────── export interface RerunStageResponse { status: string; video_id: string; stage: string; prompt_override: boolean; } export async function rerunStage( videoId: string, stageName: string, promptOverride?: string, ): Promise { const body: Record = {}; if (promptOverride) { body.prompt_override = promptOverride; } return request( `${BASE}/admin/pipeline/rerun-stage/${videoId}/${stageName}`, { method: "POST", body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined, }, ); } // ── Stale Pages & Bulk Re-Synthesize ─────────────────────────────────────── export interface StalePageCreator { creator: string; stale_count: number; page_slugs: string[]; } export interface StalePagesResponse { current_prompt_hash: string; total_pages: number; stale_pages: number; fresh_pages: number; stale_by_creator: StalePageCreator[]; } export async function fetchStalePages(): Promise { return request(`${BASE}/admin/pipeline/stale-pages`); } export interface BulkResynthResponse { status: string; stage: string; total: number; dispatched: number; skipped: Array<{ video_id: string; reason: string }> | null; } export async function bulkResynthesize( videoIds?: string[], stage = "stage5_synthesis", ): Promise { return request(`${BASE}/admin/pipeline/bulk-resynthesize`, { method: "POST", body: JSON.stringify({ video_ids: videoIds ?? null, stage }), }); } // ── Wipe All Output ──────────────────────────────────────────────────────── export interface WipeAllResponse { status: string; deleted: Record; } export async function wipeAllOutput(): Promise { return request(`${BASE}/admin/pipeline/wipe-all-output`, { method: "POST", }); } // ── Debug Mode ────────────────────────────────────────────────────────────── export interface DebugModeResponse { debug_mode: boolean; } export async function fetchDebugMode(): Promise { return request(`${BASE}/admin/pipeline/debug-mode`); } export async function setDebugMode(enabled: boolean): Promise { return request(`${BASE}/admin/pipeline/debug-mode`, { method: "PUT", body: JSON.stringify({ debug_mode: enabled }), }); } // ── Admin: Technique Pages ───────────────────────────────────────────────── export interface AdminTechniquePageItem { id: string; title: string; slug: string; creator_name: string; creator_slug: string; topic_category: string; body_sections_format: string; source_video_count: number; version_count: number; created_at: string; updated_at: string; } export interface AdminTechniquePageListResponse { items: AdminTechniquePageItem[]; total: number; offset: number; limit: number; } // ── Admin: Creator Profile ────────────────────────────────────────────────── export interface UpdateCreatorProfilePayload { bio?: string | null; social_links?: Record | null; featured?: boolean; avatar_url?: string | null; } export interface UpdateCreatorProfileResponse { status: string; creator: string; fields: string[]; } export async function updateCreatorProfile( creatorId: string, payload: UpdateCreatorProfilePayload, ): Promise { return request( `${BASE}/admin/pipeline/creators/${creatorId}`, { method: "PUT", body: JSON.stringify(payload) }, ); } export async function fetchAdminTechniquePages( params: { multi_source_only?: boolean; creator?: string; sort?: string; offset?: number; limit?: number; } = {}, ): Promise { const qs = new URLSearchParams(); if (params.multi_source_only) qs.set("multi_source_only", "true"); if (params.creator) qs.set("creator", params.creator); if (params.sort) qs.set("sort", params.sort); 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/pipeline/technique-pages${query ? `?${query}` : ""}`, ); }