- "frontend/src/api/public-client.ts" - "frontend/src/pages/CreatorDetail.tsx" - "frontend/src/App.css" GSD-Task: S04/T01
858 lines
24 KiB
TypeScript
858 lines
24 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[];
|
|
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<string, unknown> | 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<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;
|
|
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<string, string> | null;
|
|
featured: boolean;
|
|
avatar_url: string | null;
|
|
technique_count: number;
|
|
moment_count: number;
|
|
techniques: CreatorTechniqueItem[];
|
|
genre_breakdown: Record<string, 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 interface SuggestionItem {
|
|
text: string;
|
|
type: "topic" | "technique" | "creator";
|
|
}
|
|
|
|
export interface SuggestionsResponse {
|
|
suggestions: SuggestionItem[];
|
|
}
|
|
|
|
export async function fetchSuggestions(): Promise<SuggestionsResponse> {
|
|
return request<SuggestionsResponse>(`${BASE}/search/suggestions`);
|
|
}
|
|
|
|
export async function searchApi(
|
|
q: string,
|
|
scope?: string,
|
|
limit?: number,
|
|
sort?: string,
|
|
): Promise<SearchResponse> {
|
|
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<SearchResponse>(`${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<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);
|
|
if (params.sort) qs.set("sort", params.sort);
|
|
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 fetchRandomTechnique(): Promise<{ slug: string }> {
|
|
return request<{ slug: string }>(`${BASE}/techniques/random`);
|
|
}
|
|
|
|
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}`,
|
|
);
|
|
}
|
|
|
|
// ── Stats ─────────────────────────────────────────────────────────────────────
|
|
|
|
export interface StatsResponse {
|
|
technique_count: number;
|
|
creator_count: number;
|
|
}
|
|
|
|
export async function fetchStats(): Promise<StatsResponse> {
|
|
return request<StatsResponse>(`${BASE}/stats`);
|
|
}
|
|
|
|
// ── Popular Searches ─────────────────────────────────────────────────────────
|
|
|
|
export interface PopularSearchItem {
|
|
query: string;
|
|
count: number;
|
|
}
|
|
|
|
export interface PopularSearchesResponse {
|
|
items: PopularSearchItem[];
|
|
cached: boolean;
|
|
}
|
|
|
|
export async function fetchPopularSearches(): Promise<PopularSearchesResponse> {
|
|
return request<PopularSearchesResponse>(`${BASE}/search/popular`);
|
|
}
|
|
|
|
// ── Topics ───────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchTopics(): Promise<TopicCategory[]> {
|
|
return request<TopicCategory[]>(`${BASE}/topics`);
|
|
}
|
|
|
|
export async function fetchSubTopicTechniques(
|
|
categorySlug: string,
|
|
subtopicSlug: string,
|
|
params: { limit?: number; offset?: number; sort?: string } = {},
|
|
): 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.sort) qs.set("sort", params.sort);
|
|
const query = qs.toString();
|
|
return request<TechniqueListResponse>(
|
|
`${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<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;
|
|
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<string, unknown> | 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<PipelineVideoListResponse> {
|
|
return request<PipelineVideoListResponse>(`${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<RecentActivityResponse> {
|
|
return request<RecentActivityResponse>(`${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<PipelineRunsResponse> {
|
|
return request<PipelineRunsResponse>(`${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<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.run_id) qs.set("run_id", params.run_id);
|
|
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",
|
|
});
|
|
}
|
|
|
|
export interface CleanRetriggerResponse {
|
|
status: string;
|
|
video_id: string;
|
|
cleaned: Record<string, string>;
|
|
}
|
|
|
|
export async function cleanRetriggerPipeline(videoId: string): Promise<CleanRetriggerResponse> {
|
|
return request<CleanRetriggerResponse>(`${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<Record<string, unknown>>;
|
|
synthesis_groups: ChunkingSynthesisGroup[];
|
|
}
|
|
|
|
export async function fetchChunkingData(videoId: string): Promise<ChunkingDataResponse> {
|
|
return request<ChunkingDataResponse>(`${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<RerunStageResponse> {
|
|
const body: Record<string, string | undefined> = {};
|
|
if (promptOverride) {
|
|
body.prompt_override = promptOverride;
|
|
}
|
|
return request<RerunStageResponse>(
|
|
`${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<StalePagesResponse> {
|
|
return request<StalePagesResponse>(`${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<BulkResynthResponse> {
|
|
return request<BulkResynthResponse>(`${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<string, string | number>;
|
|
}
|
|
|
|
export async function wipeAllOutput(): Promise<WipeAllResponse> {
|
|
return request<WipeAllResponse>(`${BASE}/admin/pipeline/wipe-all-output`, {
|
|
method: "POST",
|
|
});
|
|
}
|
|
|
|
// ── Debug Mode ──────────────────────────────────────────────────────────────
|
|
|
|
export interface DebugModeResponse {
|
|
debug_mode: boolean;
|
|
}
|
|
|
|
export async function fetchDebugMode(): Promise<DebugModeResponse> {
|
|
return request<DebugModeResponse>(`${BASE}/admin/pipeline/debug-mode`);
|
|
}
|
|
|
|
export async function setDebugMode(enabled: boolean): Promise<DebugModeResponse> {
|
|
return request<DebugModeResponse>(`${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<string, string> | 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<UpdateCreatorProfileResponse> {
|
|
return request<UpdateCreatorProfileResponse>(
|
|
`${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<AdminTechniquePageListResponse> {
|
|
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<AdminTechniquePageListResponse>(
|
|
`${BASE}/admin/pipeline/technique-pages${query ? `?${query}` : ""}`,
|
|
);
|
|
}
|