feat: Split 945-line public-client.ts into 10 domain API modules with s…

- "frontend/src/api/client.ts"
- "frontend/src/api/index.ts"
- "frontend/src/api/search.ts"
- "frontend/src/api/techniques.ts"
- "frontend/src/api/creators.ts"
- "frontend/src/api/topics.ts"
- "frontend/src/api/stats.ts"
- "frontend/src/api/reports.ts"

GSD-Task: S05/T01
This commit is contained in:
jlightner 2026-04-03 23:04:56 +00:00
parent bd2be703a5
commit 0c99b1a8b7
29 changed files with 977 additions and 962 deletions

View file

@ -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<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 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<string, string>;
}
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 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<string, string | number>;
}
export interface DebugModeResponse {
debug_mode: boolean;
}
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[];
}
// ── Functions ────────────────────────────────────────────────────────────────
export async function fetchPipelineVideos(): Promise<PipelineVideoListResponse> {
return request<PipelineVideoListResponse>(`${BASE}/admin/pipeline/videos`);
}
export async function fetchRecentActivity(limit = 10): Promise<RecentActivityResponse> {
return request<RecentActivityResponse>(`${BASE}/admin/pipeline/recent-activity?limit=${limit}`);
}
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 async function cleanRetriggerPipeline(videoId: string): Promise<CleanRetriggerResponse> {
return request<CleanRetriggerResponse>(`${BASE}/admin/pipeline/clean-retrigger/${videoId}`, {
method: "POST",
});
}
export async function fetchChunkingData(videoId: string): Promise<ChunkingDataResponse> {
return request<ChunkingDataResponse>(`${BASE}/admin/pipeline/chunking/${videoId}`);
}
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,
},
);
}
export async function fetchStalePages(): Promise<StalePagesResponse> {
return request<StalePagesResponse>(`${BASE}/admin/pipeline/stale-pages`);
}
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 }),
});
}
export async function wipeAllOutput(): Promise<WipeAllResponse> {
return request<WipeAllResponse>(`${BASE}/admin/pipeline/wipe-all-output`, {
method: "POST",
});
}
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 }),
});
}
export async function updateCreatorProfile(
creatorId: string,
payload: UpdateCreatorProfilePayload,
): Promise<UpdateCreatorProfileResponse> {
return request<UpdateCreatorProfileResponse>(
`${BASE}/admin/pipeline/creators/${creatorId}`,
{ method: "PUT", body: JSON.stringify(payload) },
);
}

View file

@ -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<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}` : ""}`,
);
}

70
frontend/src/api/auth.ts Normal file
View file

@ -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<UserResponse> {
return request<UserResponse>(`${BASE}/auth/register`, {
method: "POST",
body: JSON.stringify(data),
});
}
export async function authLogin(email: string, password: string): Promise<TokenResponse> {
return request<TokenResponse>(`${BASE}/auth/login`, {
method: "POST",
body: JSON.stringify({ email, password }),
});
}
export async function authGetMe(token: string): Promise<UserResponse> {
return request<UserResponse>(`${BASE}/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function authUpdateProfile(
token: string,
data: UpdateProfileRequest,
): Promise<UserResponse> {
return request<UserResponse>(`${BASE}/auth/me`, {
method: "PUT",
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(data),
});
}

View file

@ -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<T>(url: string, init?: RequestInit): Promise<T> {
const token = getStoredToken();
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(init?.headers as Record<string, string>),
};
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<T>;
}

View file

@ -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<string, string> | null;
featured: boolean;
avatar_url: string | null;
technique_count: number;
moment_count: number;
techniques: CreatorTechniqueItem[];
genre_breakdown: Record<string, number>;
}
export interface CreatorListParams {
sort?: string;
genre?: string;
limit?: number;
offset?: number;
}
// ── Functions ────────────────────────────────────────────────────────────────
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}`);
}

15
frontend/src/api/index.ts Normal file
View file

@ -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";

View file

@ -1,945 +0,0 @@
/**
* 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>;
}
// ── 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<T>(url: string, init?: RequestInit): Promise<T> {
const token = getStoredToken();
const headers: Record<string, string> = {
"Content-Type": "application/json",
...(init?.headers as Record<string, string>),
};
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<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}` : ""}`,
);
}
// ── Auth ─────────────────────────────────────────────────────────────────────
export { AUTH_TOKEN_KEY };
export async function authRegister(data: RegisterRequest): Promise<UserResponse> {
return request<UserResponse>(`${BASE}/auth/register`, {
method: "POST",
body: JSON.stringify(data),
});
}
export async function authLogin(email: string, password: string): Promise<TokenResponse> {
return request<TokenResponse>(`${BASE}/auth/login`, {
method: "POST",
body: JSON.stringify({ email, password }),
});
}
export async function authGetMe(token: string): Promise<UserResponse> {
return request<UserResponse>(`${BASE}/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function authUpdateProfile(
token: string,
data: UpdateProfileRequest,
): Promise<UserResponse> {
return request<UserResponse>(`${BASE}/auth/me`, {
method: "PUT",
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(data),
});
}

View file

@ -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<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),
});
}

View file

@ -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<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()}`);
}
export async function fetchPopularSearches(): Promise<PopularSearchesResponse> {
return request<PopularSearchesResponse>(`${BASE}/search/popular`);
}

14
frontend/src/api/stats.ts Normal file
View file

@ -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<StatsResponse> {
return request<StatsResponse>(`${BASE}/stats`);
}

View file

@ -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<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 TechniqueListParams {
limit?: number;
offset?: number;
category?: string;
creator_slug?: string;
sort?: string;
}
// ── Functions ────────────────────────────────────────────────────────────────
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}`,
);
}

View file

@ -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<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}` : ""}`,
);
}

View file

@ -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;

View file

@ -14,7 +14,7 @@ import {
fetchSuggestions,
type SearchResultItem,
type SuggestionItem,
} from "../api/public-client";
} from "../api";
interface SearchAutocompleteProps {
onSearch: (query: string) => void;

View file

@ -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

View file

@ -14,7 +14,7 @@ import {
ApiError,
type UserResponse,
type RegisterRequest,
} from "../api/public-client";
} from "../api";
interface AuthContextValue {
user: UserResponse | null;

View file

@ -28,7 +28,7 @@ import {
type PipelineRunItem,
type WorkerStatusResponse,
type RecentActivityItem,
} from "../api/public-client";
} from "../api";
// ── Helpers ──────────────────────────────────────────────────────────────────

View file

@ -10,7 +10,7 @@ import {
fetchReports,
updateReport,
type ContentReport,
} from "../api/public-client";
} from "../api";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
const STATUS_OPTIONS = [

View file

@ -11,7 +11,7 @@ import {
fetchTechnique,
type AdminTechniquePageItem,
type SourceVideoSummary,
} from "../api/public-client";
} from "../api";
// ── Helpers ──────────────────────────────────────────────────────────────────

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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");

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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;

View file

@ -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"}
{"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"}