feat(admin): OCR admin pages — overview & model detail #265
@@ -1,23 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TrainingHistory from './TrainingHistory.svelte';
|
import TrainingHistory from './TrainingHistory.svelte';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import type { TrainingRun } from '$lib/types/training.js';
|
||||||
interface Run {
|
|
||||||
id: string;
|
|
||||||
status: 'RUNNING' | 'DONE' | 'FAILED';
|
|
||||||
blockCount: number;
|
|
||||||
documentCount: number;
|
|
||||||
modelName: string;
|
|
||||||
errorMessage?: string;
|
|
||||||
triggeredBy?: string;
|
|
||||||
createdAt: string;
|
|
||||||
completedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TrainingInfo {
|
interface TrainingInfo {
|
||||||
availableSegBlocks?: number;
|
availableSegBlocks?: number;
|
||||||
ocrServiceAvailable?: boolean;
|
ocrServiceAvailable?: boolean;
|
||||||
runs?: Run[];
|
runs?: TrainingRun[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { PageData } from './$types';
|
|||||||
import OcrHealthBar from './OcrHealthBar.svelte';
|
import OcrHealthBar from './OcrHealthBar.svelte';
|
||||||
import OcrStatCards from './OcrStatCards.svelte';
|
import OcrStatCards from './OcrStatCards.svelte';
|
||||||
import OcrModelsTable from './OcrModelsTable.svelte';
|
import OcrModelsTable from './OcrModelsTable.svelte';
|
||||||
|
import OcrTrainingCard from '$lib/components/OcrTrainingCard.svelte';
|
||||||
|
import SegmentationTrainingCard from '$lib/components/SegmentationTrainingCard.svelte';
|
||||||
import * as m from '$lib/paraglide/messages.js';
|
import * as m from '$lib/paraglide/messages.js';
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
@@ -27,6 +29,12 @@ const { trainingInfo } = $derived(data);
|
|||||||
availableSegBlocks={trainingInfo.availableSegBlocks ?? 0}
|
availableSegBlocks={trainingInfo.availableSegBlocks ?? 0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Training -->
|
||||||
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
|
<OcrTrainingCard trainingInfo={trainingInfo} />
|
||||||
|
<SegmentationTrainingCard trainingInfo={trainingInfo} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sender models -->
|
<!-- Sender models -->
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import OcrTrainingCard from '$lib/components/OcrTrainingCard.svelte';
|
|
||||||
import SegmentationTrainingCard from '$lib/components/SegmentationTrainingCard.svelte';
|
|
||||||
import type { components } from '$lib/generated/api.js';
|
|
||||||
|
|
||||||
type TrainingInfo = components['schemas']['TrainingInfoResponse'];
|
|
||||||
|
|
||||||
let trainingInfo: TrainingInfo | null = $state(null);
|
|
||||||
|
|
||||||
let backfillResult: number | null = $state(null);
|
let backfillResult: number | null = $state(null);
|
||||||
let backfillLoading = $state(false);
|
let backfillLoading = $state(false);
|
||||||
@@ -58,16 +51,8 @@ async function triggerImport() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchTrainingInfo() {
|
|
||||||
const res = await fetch('/api/ocr/training-info');
|
|
||||||
if (res.ok) {
|
|
||||||
trainingInfo = await res.json();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
fetchImportStatus();
|
fetchImportStatus();
|
||||||
fetchTrainingInfo();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => stopPolling());
|
onDestroy(() => stopPolling());
|
||||||
@@ -103,12 +88,6 @@ async function backfillFileHashes() {
|
|||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
<div class="flex-1 overflow-y-auto p-6">
|
||||||
<div class="mx-auto max-w-2xl space-y-5">
|
<div class="mx-auto max-w-2xl space-y-5">
|
||||||
<!-- OCR Recognition Training -->
|
|
||||||
<OcrTrainingCard trainingInfo={trainingInfo} />
|
|
||||||
|
|
||||||
<!-- OCR Segmentation Training -->
|
|
||||||
<SegmentationTrainingCard trainingInfo={trainingInfo} />
|
|
||||||
|
|
||||||
<!-- Backfill versions -->
|
<!-- Backfill versions -->
|
||||||
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
|
<div class="rounded-sm border border-line bg-surface p-6 shadow-sm">
|
||||||
<h2 class="mb-1 font-sans text-sm font-bold text-ink">{m.admin_system_backfill_heading()}</h2>
|
<h2 class="mb-1 font-sans text-sm font-bold text-ink">{m.admin_system_backfill_heading()}</h2>
|
||||||
|
|||||||
@@ -78,8 +78,6 @@ describe('Admin system page — mass import card', () => {
|
|||||||
startedAt: null
|
startedAt: null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
// training info fetch → empty
|
|
||||||
.mockResolvedValueOnce({ ok: true, json: async () => ({}) })
|
|
||||||
// trigger POST → returns RUNNING immediately
|
// trigger POST → returns RUNNING immediately
|
||||||
.mockResolvedValueOnce({
|
.mockResolvedValueOnce({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user