chrysopedia/frontend/src/api/client.ts
jlightner c6efec8363 feat: Split key moment card header into standalone h3 title and flex-ro…
- "frontend/src/pages/TechniquePage.tsx"
- "frontend/src/App.css"

GSD-Task: S03/T01
2026-03-30 08:55:48 +00:00

193 lines
5.3 KiB
TypeScript

/**
* Typed API client for Chrysopedia review queue endpoints.
*
* All functions use fetch() with JSON handling and throw on non-OK responses.
* Base URL is empty so requests go through the Vite dev proxy or nginx in prod.
*/
// ── Types ───────────────────────────────────────────────────────────────────
export interface KeyMomentRead {
id: string;
source_video_id: string;
technique_page_id: string | null;
title: string;
summary: string;
start_time: number;
end_time: number;
content_type: string;
plugins: string[] | null;
raw_transcript: string | null;
review_status: string;
created_at: string;
updated_at: string;
}
export interface ReviewQueueItem extends KeyMomentRead {
video_filename: string;
creator_name: string;
}
export interface ReviewQueueResponse {
items: ReviewQueueItem[];
total: number;
offset: number;
limit: number;
}
export interface ReviewStatsResponse {
pending: number;
approved: number;
edited: number;
rejected: number;
}
export interface ReviewModeResponse {
review_mode: boolean;
}
export interface MomentEditRequest {
title?: string;
summary?: string;
start_time?: number;
end_time?: number;
content_type?: string;
plugins?: string[];
}
export interface MomentSplitRequest {
split_time: number;
}
export interface MomentMergeRequest {
target_moment_id: string;
}
export interface QueueParams {
status?: string;
offset?: number;
limit?: number;
}
// ── Helpers ──────────────────────────────────────────────────────────────────
const BASE = "/api/v1/review";
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 = await res.json();
detail = body.detail ?? detail;
} catch {
// body not JSON — keep statusText
}
throw new ApiError(res.status, detail);
}
return res.json() as Promise<T>;
}
// ── Queue ────────────────────────────────────────────────────────────────────
export async function fetchQueue(
params: QueueParams = {},
): Promise<ReviewQueueResponse> {
const qs = new URLSearchParams();
if (params.status) qs.set("status", params.status);
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<ReviewQueueResponse>(
`${BASE}/queue${query ? `?${query}` : ""}`,
);
}
export async function fetchMoment(
momentId: string,
): Promise<ReviewQueueItem> {
return request<ReviewQueueItem>(`${BASE}/moments/${momentId}`);
}
export async function fetchStats(): Promise<ReviewStatsResponse> {
return request<ReviewStatsResponse>(`${BASE}/stats`);
}
// ── Actions ──────────────────────────────────────────────────────────────────
export async function approveMoment(id: string): Promise<KeyMomentRead> {
return request<KeyMomentRead>(`${BASE}/moments/${id}/approve`, {
method: "POST",
});
}
export async function rejectMoment(id: string): Promise<KeyMomentRead> {
return request<KeyMomentRead>(`${BASE}/moments/${id}/reject`, {
method: "POST",
});
}
export async function editMoment(
id: string,
data: MomentEditRequest,
): Promise<KeyMomentRead> {
return request<KeyMomentRead>(`${BASE}/moments/${id}`, {
method: "PUT",
body: JSON.stringify(data),
});
}
export async function splitMoment(
id: string,
splitTime: number,
): Promise<KeyMomentRead[]> {
const body: MomentSplitRequest = { split_time: splitTime };
return request<KeyMomentRead[]>(`${BASE}/moments/${id}/split`, {
method: "POST",
body: JSON.stringify(body),
});
}
export async function mergeMoments(
id: string,
targetId: string,
): Promise<KeyMomentRead> {
const body: MomentMergeRequest = { target_moment_id: targetId };
return request<KeyMomentRead>(`${BASE}/moments/${id}/merge`, {
method: "POST",
body: JSON.stringify(body),
});
}
// ── Mode ─────────────────────────────────────────────────────────────────────
export async function getReviewMode(): Promise<ReviewModeResponse> {
return request<ReviewModeResponse>(`${BASE}/mode`);
}
export async function setReviewMode(
enabled: boolean,
): Promise<ReviewModeResponse> {
return request<ReviewModeResponse>(`${BASE}/mode`, {
method: "PUT",
body: JSON.stringify({ review_mode: enabled }),
});
}