feat(analysis): admin coverage overview + per-page result inspector UI
Reworks /admin/analysis from a blind enqueue form into a coverage browser: - Loads a paginated overview of mangas with a CoverageBadge (Full/Partial/None, analyzed/total) per row; debounced search filters it. - Drill manga → chapters (coverage badge each) → page grid (chips colored done/queued/failed/none) with a legend. - Click a page chip → Modal showing the full result: status, model, time, NSFW + content-warning chips, scene description, tags, and OCR lines (labeled by kind). Unanalyzed/failed pages get a "Queue this page" action (force re-analyze). - Keeps "Queue all" + per-manga/chapter enqueue and the include-analyzed toggle; enqueue refreshes the open chapter's grid so queued pages show. API client: getAnalysisMangaCoverage / ChapterCoverage / ChapterPages / PageDetail + types. New CoverageBadge component. Tests: vitest for the four clients; Playwright for overview+badges+queue-all, drill+inspect, and queue-from-detail. svelte-check + build clean; 266 vitest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { request, apiUrl, type Page } from './client';
|
||||
import type { User } from './auth';
|
||||
import type { MangaDetail } from './mangas';
|
||||
import type { Chapter } from './chapters';
|
||||
import type { ContentWarning } from './page_tags';
|
||||
|
||||
// ---- users -----------------------------------------------------------------
|
||||
|
||||
@@ -441,3 +442,93 @@ export async function analyzePage(pageId: string): Promise<{ enqueued: boolean }
|
||||
{ method: 'POST' }
|
||||
);
|
||||
}
|
||||
|
||||
// ---- AI content analysis: coverage & inspection ----------------------------
|
||||
|
||||
export type MangaCoverage = {
|
||||
manga_id: string;
|
||||
title: string;
|
||||
total_pages: number;
|
||||
analyzed_pages: number;
|
||||
};
|
||||
|
||||
export type MangaCoveragePage = {
|
||||
items: MangaCoverage[];
|
||||
page: Page;
|
||||
};
|
||||
|
||||
export type ChapterCoverage = {
|
||||
chapter_id: string;
|
||||
number: number;
|
||||
title: string | null;
|
||||
total_pages: number;
|
||||
analyzed_pages: number;
|
||||
};
|
||||
|
||||
/** Per-page status in a chapter grid. */
|
||||
export type PageAnalysisStatus = 'done' | 'failed' | 'queued' | 'none';
|
||||
|
||||
export type PageStatusItem = {
|
||||
page_id: string;
|
||||
page_number: number;
|
||||
status: PageAnalysisStatus;
|
||||
};
|
||||
|
||||
export type OcrLine = { kind: string; text: string };
|
||||
|
||||
export type PageAnalysisDetail = {
|
||||
page_id: string;
|
||||
page_number: number;
|
||||
chapter_id: string;
|
||||
manga_id: string;
|
||||
/** `done` | `failed` | `none` (no analysis row yet). */
|
||||
status: 'done' | 'failed' | 'none';
|
||||
is_nsfw: boolean;
|
||||
scene_description: string | null;
|
||||
model: string | null;
|
||||
error: string | null;
|
||||
analyzed_at: string | null;
|
||||
ocr: OcrLine[];
|
||||
tags: string[];
|
||||
content_warnings: ContentWarning[];
|
||||
};
|
||||
|
||||
/** Paginated per-manga analysis coverage (admin overview). */
|
||||
export async function getAnalysisMangaCoverage(
|
||||
opts: { search?: string; limit?: number; offset?: number } = {}
|
||||
): Promise<MangaCoveragePage> {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.search) params.set('search', opts.search);
|
||||
if (opts.limit != null) params.set('limit', String(opts.limit));
|
||||
if (opts.offset != null) params.set('offset', String(opts.offset));
|
||||
const qs = params.toString();
|
||||
return request<MangaCoveragePage>(
|
||||
`/v1/admin/analysis/mangas${qs ? `?${qs}` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAnalysisChapterCoverage(
|
||||
mangaId: string
|
||||
): Promise<ChapterCoverage[]> {
|
||||
const r = await request<{ items: ChapterCoverage[] }>(
|
||||
`/v1/admin/analysis/mangas/${encodeURIComponent(mangaId)}/chapters`
|
||||
);
|
||||
return r.items;
|
||||
}
|
||||
|
||||
export async function getAnalysisChapterPages(
|
||||
chapterId: string
|
||||
): Promise<PageStatusItem[]> {
|
||||
const r = await request<{ items: PageStatusItem[] }>(
|
||||
`/v1/admin/analysis/chapters/${encodeURIComponent(chapterId)}/pages`
|
||||
);
|
||||
return r.items;
|
||||
}
|
||||
|
||||
export async function getAnalysisPageDetail(
|
||||
pageId: string
|
||||
): Promise<PageAnalysisDetail> {
|
||||
return request<PageAnalysisDetail>(
|
||||
`/v1/admin/analysis/pages/${encodeURIComponent(pageId)}`
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user