Files
familienarchiv/frontend/src/lib/components/TrainingHistory.svelte
2026-04-18 12:30:54 +02:00

151 lines
5.1 KiB
Svelte

<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
import type { TrainingRun } from '$lib/types/training.js';
interface Props {
runs: TrainingRun[];
personNames?: Record<string, string>;
}
let { runs, personNames }: Props = $props();
const COLLAPSED_COUNT = 3;
let expanded = $state(false);
const visibleRuns = $derived(expanded ? runs : runs.slice(0, COLLAPSED_COUNT));
const hasMore = $derived(runs.length > COLLAPSED_COUNT);
const dateFormatter = new Intl.DateTimeFormat('de-DE', {
day: 'numeric',
month: 'short',
year: 'numeric'
});
function formatDate(iso: string): string {
return dateFormatter.format(new Date(iso));
}
function formatCer(cer: number | undefined | null): string {
if (cer == null) return '—';
return (cer * 100).toFixed(1) + ' %';
}
</script>
<table class="w-full text-sm">
<thead>
<tr class="border-b border-line text-xs font-bold tracking-widest text-ink-3 uppercase">
<th class="pb-2 text-left">{m.training_history_col_date()}</th>
<th class="pb-2 text-left">{m.training_history_col_status()}</th>
<th class="hidden pb-2 text-left md:table-cell">{m.training_col_type()}</th>
<th class="hidden pb-2 text-left md:table-cell">{m.training_col_person()}</th>
<th class="pb-2 text-right">{m.training_history_col_blocks()}</th>
<th class="hidden pb-2 text-right md:table-cell">{m.training_history_col_docs()}</th>
<th class="hidden pb-2 text-right md:table-cell">{m.training_history_col_cer()}</th>
</tr>
</thead>
<tbody id="training-history-rows">
{#if runs.length === 0}
<tr>
<td colspan="7" class="py-4 text-center text-sm text-ink-2">
{m.training_history_empty()}
</td>
</tr>
{:else}
{#each visibleRuns as run (run.id)}
<tr class="border-b border-line/50 last:border-0">
<td class="py-2 text-ink-2">{formatDate(run.createdAt)}</td>
<td class="py-2">
{#if run.status === 'QUEUED'}
<span
class="inline-flex items-center gap-1 rounded-sm bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700"
>
<span aria-hidden="true" class="h-1.5 w-1.5 rounded-full bg-amber-500"></span>
{m.training_status_queued()}
</span>
{:else if run.status === 'DONE'}
<span
class="inline-flex items-center gap-1 rounded-sm bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-700"
>
<svg
aria-hidden="true"
class="h-3 w-3 shrink-0"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
{m.training_status_done()}
</span>
{:else if run.status === 'FAILED'}
<span
class="inline-flex items-center gap-1 rounded-sm bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-700"
>
<svg
aria-hidden="true"
class="h-3 w-3 shrink-0"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
{m.training_status_failed()}
</span>
{#if run.errorMessage}
<details class="mt-0.5">
<summary class="cursor-pointer text-xs text-red-700 underline">
{m.training_error_detail_label()}
</summary>
<p class="mt-1 text-xs text-red-600">{run.errorMessage}</p>
</details>
{/if}
{:else}
<span
class="inline-flex items-center gap-1 rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-700"
>
<span
aria-hidden="true"
class="h-1.5 w-1.5 rounded-full bg-yellow-500 motion-safe:animate-pulse"
></span>
{m.training_status_running()}
</span>
{/if}
</td>
<td class="hidden py-2 text-left text-ink-2 md:table-cell">
{run.personId ? m.training_type_personalized() : m.training_type_base()}
</td>
<td class="hidden py-2 text-left text-ink-2 md:table-cell">
{run.personId && personNames?.[run.personId] ? personNames[run.personId] : '—'}
</td>
<td class="py-2 text-right text-ink-2">{run.blockCount}</td>
<td class="hidden py-2 text-right text-ink-2 md:table-cell">{run.documentCount}</td>
<td class="hidden py-2 text-right md:table-cell"
>{run.status === 'DONE' && run.cer != null ? formatCer(run.cer) : '—'}</td
>
</tr>
{/each}
{/if}
</tbody>
</table>
{#if hasMore}
<div class="mt-2 text-center">
<button
type="button"
aria-expanded={expanded}
aria-controls="training-history-rows"
class="text-xs font-medium text-ink-3 transition-colors hover:text-ink"
onclick={() => (expanded = !expanded)}
>
{expanded ? m.comp_expandable_show_less() : m.comp_expandable_show_more()}
</button>
</div>
{/if}