193 lines
5.3 KiB
TypeScript
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 }),
|
|
});
|
|
}
|