chrysopedia/frontend/src/api/public-client.ts
jlightner ee24731e59 feat: Added Head/Tail segmented toggle to EventLog with order param wir…
- "frontend/src/api/public-client.ts"
- "frontend/src/pages/AdminPipeline.tsx"
- "frontend/src/App.css"

GSD-Task: S02/T02
2026-03-30 11:15:21 +00:00

470 lines
13 KiB
TypeScript

/**
* Typed API client for Chrysopedia public endpoints.
*
* Mirrors backend schemas: SearchResponse, TechniquePageDetail, TopicCategory, CreatorBrowseItem.
* Uses the same request<T> 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<string, unknown> | 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<string, unknown> | null;
}
export interface TechniquePageVersionListResponse {
items: TechniquePageVersionSummary[];
total: number;
}
export interface TechniquePageVersionDetail {
version_number: number;
content_snapshot: Record<string, unknown>;
pipeline_metadata: Record<string, unknown> | 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<T>(url: string, init?: RequestInit): Promise<T> {
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<T>;
}
// ── Search ───────────────────────────────────────────────────────────────────
export async function searchApi(
q: string,
scope?: string,
limit?: number,
): Promise<SearchResponse> {
const qs = new URLSearchParams({ q });
if (scope) qs.set("scope", scope);
if (limit !== undefined) qs.set("limit", String(limit));
return request<SearchResponse>(`${BASE}/search?${qs.toString()}`);
}
// ── Techniques ───────────────────────────────────────────────────────────────
export interface TechniqueListParams {
limit?: number;
offset?: number;
category?: string;
creator_slug?: string;
}
export async function fetchTechniques(
params: TechniqueListParams = {},
): Promise<TechniqueListResponse> {
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<TechniqueListResponse>(
`${BASE}/techniques${query ? `?${query}` : ""}`,
);
}
export async function fetchTechnique(
slug: string,
): Promise<TechniquePageDetail> {
return request<TechniquePageDetail>(`${BASE}/techniques/${slug}`);
}
export async function fetchTechniqueVersions(
slug: string,
): Promise<TechniquePageVersionListResponse> {
return request<TechniquePageVersionListResponse>(
`${BASE}/techniques/${slug}/versions`,
);
}
export async function fetchTechniqueVersion(
slug: string,
versionNumber: number,
): Promise<TechniquePageVersionDetail> {
return request<TechniquePageVersionDetail>(
`${BASE}/techniques/${slug}/versions/${versionNumber}`,
);
}
// ── Topics ───────────────────────────────────────────────────────────────────
export async function fetchTopics(): Promise<TopicCategory[]> {
return request<TopicCategory[]>(`${BASE}/topics`);
}
// ── Creators ─────────────────────────────────────────────────────────────────
export interface CreatorListParams {
sort?: string;
genre?: string;
limit?: number;
offset?: number;
}
export async function fetchCreators(
params: CreatorListParams = {},
): Promise<CreatorBrowseResponse> {
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<CreatorBrowseResponse>(
`${BASE}/creators${query ? `?${query}` : ""}`,
);
}
export async function fetchCreator(
slug: string,
): Promise<CreatorDetailResponse> {
return request<CreatorDetailResponse>(`${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<ContentReport> {
return request<ContentReport>(`${BASE}/reports`, {
method: "POST",
body: JSON.stringify(body),
});
}
export async function fetchReports(params: {
status?: string;
content_type?: string;
offset?: number;
limit?: number;
} = {}): Promise<ContentReportListResponse> {
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<ContentReportListResponse>(
`${BASE}/admin/reports${query ? `?${query}` : ""}`,
);
}
export async function updateReport(
id: string,
body: { status?: string; admin_notes?: string },
): Promise<ContentReport> {
return request<ContentReport>(`${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<string, unknown> | 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<PipelineVideoListResponse> {
return request<PipelineVideoListResponse>(`${BASE}/admin/pipeline/videos`);
}
export async function fetchPipelineEvents(
videoId: string,
params: { offset?: number; limit?: number; stage?: string; event_type?: string; order?: "asc" | "desc" } = {},
): Promise<PipelineEventListResponse> {
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<PipelineEventListResponse>(
`${BASE}/admin/pipeline/events/${videoId}${query ? `?${query}` : ""}`,
);
}
export async function fetchWorkerStatus(): Promise<WorkerStatusResponse> {
return request<WorkerStatusResponse>(`${BASE}/admin/pipeline/worker-status`);
}
export async function triggerPipeline(videoId: string): Promise<TriggerResponse> {
return request<TriggerResponse>(`${BASE}/admin/pipeline/trigger/${videoId}`, {
method: "POST",
});
}
export async function revokePipeline(videoId: string): Promise<RevokeResponse> {
return request<RevokeResponse>(`${BASE}/admin/pipeline/revoke/${videoId}`, {
method: "POST",
});
}