diff --git a/frontend/src/api/admin-pipeline.ts b/frontend/src/api/admin-pipeline.ts new file mode 100644 index 0000000..e20a7da --- /dev/null +++ b/frontend/src/api/admin-pipeline.ts @@ -0,0 +1,333 @@ +import { request, BASE } from "./client"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +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 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 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 interface CleanRetriggerResponse { + status: string; + video_id: string; + cleaned: Record; +} + +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 interface RerunStageResponse { + status: string; + video_id: string; + stage: string; + prompt_override: boolean; +} + +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 interface BulkResynthResponse { + status: string; + stage: string; + total: number; + dispatched: number; + skipped: Array<{ video_id: string; reason: string }> | null; +} + +export interface WipeAllResponse { + status: string; + deleted: Record; +} + +export interface DebugModeResponse { + debug_mode: boolean; +} + +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[]; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +export async function fetchPipelineVideos(): Promise { + return request(`${BASE}/admin/pipeline/videos`); +} + +export async function fetchRecentActivity(limit = 10): Promise { + return request(`${BASE}/admin/pipeline/recent-activity?limit=${limit}`); +} + +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 async function cleanRetriggerPipeline(videoId: string): Promise { + return request(`${BASE}/admin/pipeline/clean-retrigger/${videoId}`, { + method: "POST", + }); +} + +export async function fetchChunkingData(videoId: string): Promise { + return request(`${BASE}/admin/pipeline/chunking/${videoId}`); +} + +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, + }, + ); +} + +export async function fetchStalePages(): Promise { + return request(`${BASE}/admin/pipeline/stale-pages`); +} + +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 }), + }); +} + +export async function wipeAllOutput(): Promise { + return request(`${BASE}/admin/pipeline/wipe-all-output`, { + method: "POST", + }); +} + +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 }), + }); +} + +export async function updateCreatorProfile( + creatorId: string, + payload: UpdateCreatorProfilePayload, +): Promise { + return request( + `${BASE}/admin/pipeline/creators/${creatorId}`, + { method: "PUT", body: JSON.stringify(payload) }, + ); +} diff --git a/frontend/src/api/admin-techniques.ts b/frontend/src/api/admin-techniques.ts new file mode 100644 index 0000000..323b305 --- /dev/null +++ b/frontend/src/api/admin-techniques.ts @@ -0,0 +1,47 @@ +import { request, BASE } from "./client"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +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; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +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}` : ""}`, + ); +} diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 0000000..8ba30d7 --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,70 @@ +import { request, BASE } from "./client"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface RegisterRequest { + email: string; + password: string; + display_name: string; + invite_code: string; + creator_slug?: string | null; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface TokenResponse { + access_token: string; + token_type: string; +} + +export interface UserResponse { + id: string; + email: string; + display_name: string; + role: string; + creator_id: string | null; + is_active: boolean; + created_at: string; +} + +export interface UpdateProfileRequest { + display_name?: string | null; + current_password?: string | null; + new_password?: string | null; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +export async function authRegister(data: RegisterRequest): Promise { + return request(`${BASE}/auth/register`, { + method: "POST", + body: JSON.stringify(data), + }); +} + +export async function authLogin(email: string, password: string): Promise { + return request(`${BASE}/auth/login`, { + method: "POST", + body: JSON.stringify({ email, password }), + }); +} + +export async function authGetMe(token: string): Promise { + return request(`${BASE}/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + }); +} + +export async function authUpdateProfile( + token: string, + data: UpdateProfileRequest, +): Promise { + return request(`${BASE}/auth/me`, { + method: "PUT", + headers: { Authorization: `Bearer ${token}` }, + body: JSON.stringify(data), + }); +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..41389ad --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,56 @@ +/** + * Shared API client infrastructure: request helper, error class, auth token management. + */ + +export const BASE = "/api/v1"; +export const AUTH_TOKEN_KEY = "chrysopedia_token"; + +export class ApiError extends Error { + constructor( + public status: number, + public detail: string, + ) { + super(`API ${status}: ${detail}`); + this.name = "ApiError"; + } +} + +function getStoredToken(): string | null { + try { + return localStorage.getItem(AUTH_TOKEN_KEY); + } catch { + return null; + } +} + +export async function request(url: string, init?: RequestInit): Promise { + const token = getStoredToken(); + const headers: Record = { + "Content-Type": "application/json", + ...(init?.headers as Record), + }; + if (token && !headers["Authorization"]) { + headers["Authorization"] = `Bearer ${token}`; + } + + const res = await fetch(url, { + ...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; +} diff --git a/frontend/src/api/creators.ts b/frontend/src/api/creators.ts new file mode 100644 index 0000000..7ddd812 --- /dev/null +++ b/frontend/src/api/creators.ts @@ -0,0 +1,83 @@ +import { request, BASE } from "./client"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +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; +} + +export interface CreatorListParams { + sort?: string; + genre?: string; + limit?: number; + offset?: number; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +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}`); +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..eb78d5e --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,15 @@ +/** + * Barrel re-export for all API modules. + * Consumers can import from "../api" for backward compatibility. + */ + +export { ApiError, AUTH_TOKEN_KEY } from "./client"; +export * from "./search"; +export * from "./techniques"; +export * from "./creators"; +export * from "./topics"; +export * from "./stats"; +export * from "./reports"; +export * from "./admin-pipeline"; +export * from "./admin-techniques"; +export * from "./auth"; diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts deleted file mode 100644 index 0e102f6..0000000 --- a/frontend/src/api/public-client.ts +++ /dev/null @@ -1,945 +0,0 @@ -/** - * 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; -} - -// ── Auth Types ─────────────────────────────────────────────────────────────── - -export interface RegisterRequest { - email: string; - password: string; - display_name: string; - invite_code: string; - creator_slug?: string | null; -} - -export interface LoginRequest { - email: string; - password: string; -} - -export interface TokenResponse { - access_token: string; - token_type: string; -} - -export interface UserResponse { - id: string; - email: string; - display_name: string; - role: string; - creator_id: string | null; - is_active: boolean; - created_at: string; -} - -export interface UpdateProfileRequest { - display_name?: string | null; - current_password?: string | null; - new_password?: string | null; -} - -// ── Helpers ────────────────────────────────────────────────────────────────── - -const BASE = "/api/v1"; -const AUTH_TOKEN_KEY = "chrysopedia_token"; - -export class ApiError extends Error { - constructor( - public status: number, - public detail: string, - ) { - super(`API ${status}: ${detail}`); - this.name = "ApiError"; - } -} - -function getStoredToken(): string | null { - try { - return localStorage.getItem(AUTH_TOKEN_KEY); - } catch { - return null; - } -} - -async function request(url: string, init?: RequestInit): Promise { - const token = getStoredToken(); - const headers: Record = { - "Content-Type": "application/json", - ...(init?.headers as Record), - }; - if (token && !headers["Authorization"]) { - headers["Authorization"] = `Bearer ${token}`; - } - - const res = await fetch(url, { - ...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}` : ""}`, - ); -} - - -// ── Auth ───────────────────────────────────────────────────────────────────── - -export { AUTH_TOKEN_KEY }; - -export async function authRegister(data: RegisterRequest): Promise { - return request(`${BASE}/auth/register`, { - method: "POST", - body: JSON.stringify(data), - }); -} - -export async function authLogin(email: string, password: string): Promise { - return request(`${BASE}/auth/login`, { - method: "POST", - body: JSON.stringify({ email, password }), - }); -} - -export async function authGetMe(token: string): Promise { - return request(`${BASE}/auth/me`, { - headers: { Authorization: `Bearer ${token}` }, - }); -} - -export async function authUpdateProfile( - token: string, - data: UpdateProfileRequest, -): Promise { - return request(`${BASE}/auth/me`, { - method: "PUT", - headers: { Authorization: `Bearer ${token}` }, - body: JSON.stringify(data), - }); -} diff --git a/frontend/src/api/reports.ts b/frontend/src/api/reports.ts new file mode 100644 index 0000000..7dff61e --- /dev/null +++ b/frontend/src/api/reports.ts @@ -0,0 +1,71 @@ +import { request, BASE } from "./client"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +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; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +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), + }); +} diff --git a/frontend/src/api/search.ts b/frontend/src/api/search.ts new file mode 100644 index 0000000..c0318c5 --- /dev/null +++ b/frontend/src/api/search.ts @@ -0,0 +1,69 @@ +import { request, BASE } from "./client"; + +// ── 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 SuggestionItem { + text: string; + type: "topic" | "technique" | "creator"; +} + +export interface SuggestionsResponse { + suggestions: SuggestionItem[]; +} + +export interface PopularSearchItem { + query: string; + count: number; +} + +export interface PopularSearchesResponse { + items: PopularSearchItem[]; + cached: boolean; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +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()}`); +} + +export async function fetchPopularSearches(): Promise { + return request(`${BASE}/search/popular`); +} diff --git a/frontend/src/api/stats.ts b/frontend/src/api/stats.ts new file mode 100644 index 0000000..6813d3c --- /dev/null +++ b/frontend/src/api/stats.ts @@ -0,0 +1,14 @@ +import { request, BASE } from "./client"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface StatsResponse { + technique_count: number; + creator_count: number; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +export async function fetchStats(): Promise { + return request(`${BASE}/stats`); +} diff --git a/frontend/src/api/techniques.ts b/frontend/src/api/techniques.ts new file mode 100644 index 0000000..0fc8dd9 --- /dev/null +++ b/frontend/src/api/techniques.ts @@ -0,0 +1,165 @@ +import { request, BASE } from "./client"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +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 TechniqueListParams { + limit?: number; + offset?: number; + category?: string; + creator_slug?: string; + sort?: string; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +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}`, + ); +} diff --git a/frontend/src/api/topics.ts b/frontend/src/api/topics.ts new file mode 100644 index 0000000..94250b0 --- /dev/null +++ b/frontend/src/api/topics.ts @@ -0,0 +1,37 @@ +import { request, BASE } from "./client"; +import type { TechniqueListResponse } from "./techniques"; + +// ── Types ──────────────────────────────────────────────────────────────────── + +export interface TopicSubTopic { + name: string; + technique_count: number; + creator_count: number; +} + +export interface TopicCategory { + name: string; + description: string; + sub_topics: TopicSubTopic[]; +} + +// ── Functions ──────────────────────────────────────────────────────────────── + +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}` : ""}`, + ); +} diff --git a/frontend/src/components/ReportIssueModal.tsx b/frontend/src/components/ReportIssueModal.tsx index c5be1e2..7b27f16 100644 --- a/frontend/src/components/ReportIssueModal.tsx +++ b/frontend/src/components/ReportIssueModal.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { submitReport, type ContentReportCreate } from "../api/public-client"; +import { submitReport, type ContentReportCreate } from "../api"; interface ReportIssueModalProps { contentType: string; diff --git a/frontend/src/components/SearchAutocomplete.tsx b/frontend/src/components/SearchAutocomplete.tsx index c8d91be..a5d56cd 100644 --- a/frontend/src/components/SearchAutocomplete.tsx +++ b/frontend/src/components/SearchAutocomplete.tsx @@ -14,7 +14,7 @@ import { fetchSuggestions, type SearchResultItem, type SuggestionItem, -} from "../api/public-client"; +} from "../api"; interface SearchAutocompleteProps { onSearch: (query: string) => void; diff --git a/frontend/src/components/TableOfContents.tsx b/frontend/src/components/TableOfContents.tsx index 89c27c0..a31acf4 100644 --- a/frontend/src/components/TableOfContents.tsx +++ b/frontend/src/components/TableOfContents.tsx @@ -8,7 +8,7 @@ */ import { useCallback } from "react"; -import type { BodySectionV2 } from "../api/public-client"; +import type { BodySectionV2 } from "../api"; export function slugify(text: string): string { return text diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx index 748ab11..af5312f 100644 --- a/frontend/src/context/AuthContext.tsx +++ b/frontend/src/context/AuthContext.tsx @@ -14,7 +14,7 @@ import { ApiError, type UserResponse, type RegisterRequest, -} from "../api/public-client"; +} from "../api"; interface AuthContextValue { user: UserResponse | null; diff --git a/frontend/src/pages/AdminPipeline.tsx b/frontend/src/pages/AdminPipeline.tsx index f80de1f..a85e05b 100644 --- a/frontend/src/pages/AdminPipeline.tsx +++ b/frontend/src/pages/AdminPipeline.tsx @@ -28,7 +28,7 @@ import { type PipelineRunItem, type WorkerStatusResponse, type RecentActivityItem, -} from "../api/public-client"; +} from "../api"; // ── Helpers ────────────────────────────────────────────────────────────────── diff --git a/frontend/src/pages/AdminReports.tsx b/frontend/src/pages/AdminReports.tsx index 9b18cec..929ab07 100644 --- a/frontend/src/pages/AdminReports.tsx +++ b/frontend/src/pages/AdminReports.tsx @@ -10,7 +10,7 @@ import { fetchReports, updateReport, type ContentReport, -} from "../api/public-client"; +} from "../api"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; const STATUS_OPTIONS = [ diff --git a/frontend/src/pages/AdminTechniquePages.tsx b/frontend/src/pages/AdminTechniquePages.tsx index d014312..2faf83e 100644 --- a/frontend/src/pages/AdminTechniquePages.tsx +++ b/frontend/src/pages/AdminTechniquePages.tsx @@ -11,7 +11,7 @@ import { fetchTechnique, type AdminTechniquePageItem, type SourceVideoSummary, -} from "../api/public-client"; +} from "../api"; // ── Helpers ────────────────────────────────────────────────────────────────── diff --git a/frontend/src/pages/CreatorDetail.tsx b/frontend/src/pages/CreatorDetail.tsx index 96e6f4a..5ba13a8 100644 --- a/frontend/src/pages/CreatorDetail.tsx +++ b/frontend/src/pages/CreatorDetail.tsx @@ -11,7 +11,7 @@ import { fetchCreator, updateCreatorProfile, type CreatorDetailResponse, -} from "../api/public-client"; +} from "../api"; import CreatorAvatar from "../components/CreatorAvatar"; import { SocialIcon } from "../components/SocialIcons"; import SortDropdown from "../components/SortDropdown"; diff --git a/frontend/src/pages/CreatorSettings.tsx b/frontend/src/pages/CreatorSettings.tsx index 437f629..1a14f74 100644 --- a/frontend/src/pages/CreatorSettings.tsx +++ b/frontend/src/pages/CreatorSettings.tsx @@ -1,7 +1,7 @@ import { useState, type FormEvent } from "react"; import { useAuth, ApiError } from "../context/AuthContext"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; -import { authUpdateProfile } from "../api/public-client"; +import { authUpdateProfile } from "../api"; import { SidebarNav } from "./CreatorDashboard"; import dashStyles from "./CreatorDashboard.module.css"; import styles from "./CreatorSettings.module.css"; diff --git a/frontend/src/pages/CreatorsBrowse.tsx b/frontend/src/pages/CreatorsBrowse.tsx index 524f196..5d25bae 100644 --- a/frontend/src/pages/CreatorsBrowse.tsx +++ b/frontend/src/pages/CreatorsBrowse.tsx @@ -13,7 +13,7 @@ import { Link } from "react-router-dom"; import { fetchCreators, type CreatorBrowseItem, -} from "../api/public-client"; +} from "../api"; import CreatorAvatar from "../components/CreatorAvatar"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 09b84d0..70123ed 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -21,7 +21,7 @@ import { type TechniqueListItem, type StatsResponse, type PopularSearchItem, -} from "../api/public-client"; +} from "../api"; export default function Home() { useDocumentTitle("Chrysopedia — Production Knowledge, Distilled"); diff --git a/frontend/src/pages/SearchResults.tsx b/frontend/src/pages/SearchResults.tsx index be24e92..e812de8 100644 --- a/frontend/src/pages/SearchResults.tsx +++ b/frontend/src/pages/SearchResults.tsx @@ -8,7 +8,7 @@ import { useCallback, useEffect, useState } from "react"; import { Link, useSearchParams, useNavigate } from "react-router-dom"; -import { searchApi, type SearchResultItem } from "../api/public-client"; +import { searchApi, type SearchResultItem } from "../api"; import { catSlug } from "../utils/catSlug"; import SearchAutocomplete from "../components/SearchAutocomplete"; import SortDropdown from "../components/SortDropdown"; diff --git a/frontend/src/pages/SubTopicPage.tsx b/frontend/src/pages/SubTopicPage.tsx index 63e3c4d..7dad1ce 100644 --- a/frontend/src/pages/SubTopicPage.tsx +++ b/frontend/src/pages/SubTopicPage.tsx @@ -10,7 +10,7 @@ import { Link, useParams } from "react-router-dom"; import { fetchSubTopicTechniques, type TechniqueListItem, -} from "../api/public-client"; +} from "../api"; import { catSlug } from "../utils/catSlug"; import SortDropdown from "../components/SortDropdown"; import TagList from "../components/TagList"; diff --git a/frontend/src/pages/TechniquePage.tsx b/frontend/src/pages/TechniquePage.tsx index 23fa8a8..499cbea 100644 --- a/frontend/src/pages/TechniquePage.tsx +++ b/frontend/src/pages/TechniquePage.tsx @@ -16,7 +16,7 @@ import { type TechniquePageVersionSummary, type TechniquePageVersionDetail, type BodySectionV2, -} from "../api/public-client"; +} from "../api"; import ReportIssueModal from "../components/ReportIssueModal"; import CopyLinkButton from "../components/CopyLinkButton"; import CreatorAvatar from "../components/CreatorAvatar"; diff --git a/frontend/src/pages/TopicsBrowse.tsx b/frontend/src/pages/TopicsBrowse.tsx index 5cb140a..880dd9a 100644 --- a/frontend/src/pages/TopicsBrowse.tsx +++ b/frontend/src/pages/TopicsBrowse.tsx @@ -11,7 +11,7 @@ import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { fetchTopics, type TopicCategory } from "../api/public-client"; +import { fetchTopics, type TopicCategory } from "../api"; import { CATEGORY_ICON } from "../components/CategoryIcons"; import { catSlug } from "../utils/catSlug"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; diff --git a/frontend/src/utils/citations.tsx b/frontend/src/utils/citations.tsx index 4c3a2a5..ee742cc 100644 --- a/frontend/src/utils/citations.tsx +++ b/frontend/src/utils/citations.tsx @@ -6,7 +6,7 @@ */ import React from "react"; -import type { KeyMomentSummary } from "../api/public-client"; +import type { KeyMomentSummary } from "../api"; // Matches [1], [2,3], [1,2,3], etc. const CITATION_RE = /\[(\d+(?:,\s*\d+)*)\]/g; diff --git a/frontend/tsconfig.app.tsbuildinfo b/frontend/tsconfig.app.tsbuildinfo index 68a2c2c..ceadddd 100644 --- a/frontend/tsconfig.app.tsbuildinfo +++ b/frontend/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/admin-pipeline.ts","./src/api/admin-techniques.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/creators.ts","./src/api/index.ts","./src/api/reports.ts","./src/api/search.ts","./src/api/stats.ts","./src/api/techniques.ts","./src/api/topics.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ProtectedRoute.tsx","./src/components/ReportIssueModal.tsx","./src/components/SearchAutocomplete.tsx","./src/components/SocialIcons.tsx","./src/components/SortDropdown.tsx","./src/components/TableOfContents.tsx","./src/components/TagList.tsx","./src/context/AuthContext.tsx","./src/hooks/useCountUp.ts","./src/hooks/useDocumentTitle.ts","./src/hooks/useSortPreference.ts","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/AdminTechniquePages.tsx","./src/pages/CreatorDashboard.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorSettings.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/Login.tsx","./src/pages/Register.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx","./src/utils/catSlug.ts","./src/utils/citations.tsx"],"version":"5.6.3"} \ No newline at end of file