fix(ui): upgrade global indicator from thin bar to rich progress panel
- Show full stats grid (total/success/fail/skip) + progress bar + cancel button during active run - Compact colored banner for terminal states (completed/partial/failed/cancelled) with auto-dismiss - Import cancelRun from translate API for functional cancel button - fromStore() reactivity confirmed working — no store changes needed - Hide on /translate/[id] to avoid duplication (existing behavior preserved)
This commit is contained in:
@@ -1,19 +1,22 @@
|
||||
<!-- #region TranslationRunGlobalIndicator [C:3] [TYPE Component] [SEMANTICS translate, progress, global, indicator] -->
|
||||
<!-- @BRIEF Persistent mini progress indicator for translation runs, rendered in root layout. -->
|
||||
<!-- @BRIEF Persistent rich progress panel for active translation runs, rendered in root layout. -->
|
||||
<!-- @LAYER UI -->
|
||||
<!-- @RELATION BINDS_TO -> [translationRunStore] -->
|
||||
<!-- @UX_STATE hidden -> No active or recently completed run -->
|
||||
<!-- @UX_STATE running -> Thin blue bar + "Translating X/Y" banner -->
|
||||
<!-- @UX_STATE completed -> Green bar + "Completed" banner (auto-hides after 5s) -->
|
||||
<!-- @UX_STATE failed -> Red bar + "Failed" banner (auto-hides after 8s) -->
|
||||
<!-- @UX_FEEDBACK Click banner navigates to the translation run page -->
|
||||
<!-- @UX_STATE running -> Rich panel with stats grid + progress bar + cancel button -->
|
||||
<!-- @UX_STATE completed -> Compact green banner with auto-dismiss after 5s -->
|
||||
<!-- @UX_STATE failed -> Compact red banner with auto-dismiss after 8s -->
|
||||
<!-- @UX_STATE cancelled -> Compact gray banner with auto-dismiss after 8s -->
|
||||
<!-- @UX_FEEDBACK Rich stats during active run; compact auto-dismiss for terminal states -->
|
||||
<script>
|
||||
import { translationRunStore } from '$lib/stores/translationRun.js';
|
||||
import { translationRunStore, stopTranslationRun } from '$lib/stores/translationRun.js';
|
||||
import { fromStore } from 'svelte/store';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/i18n';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { cancelRun } from '$lib/api/translate.js';
|
||||
import { addToast } from '$lib/toasts.js';
|
||||
|
||||
const runState = fromStore(translationRunStore);
|
||||
|
||||
@@ -25,6 +28,7 @@
|
||||
// Terminal state auto-dismiss timers
|
||||
let dismissTimer = $state(null);
|
||||
let dismissed = $state(false);
|
||||
let isCancelling = $state(false);
|
||||
|
||||
let uxState = $derived(runState.current?.uxState || 'idle');
|
||||
|
||||
@@ -37,21 +41,7 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
let barColor = $derived.by(() => {
|
||||
if (uxState === 'running' || uxState === 'inserting') return 'bg-blue-600';
|
||||
if (uxState === 'completed' || uxState === 'partial') return 'bg-green-500';
|
||||
if (uxState === 'failed') return 'bg-red-500';
|
||||
if (uxState === 'cancelled') return 'bg-gray-400';
|
||||
return 'bg-blue-600';
|
||||
});
|
||||
|
||||
let bannerBg = $derived.by(() => {
|
||||
if (uxState === 'running' || uxState === 'inserting') return 'bg-blue-600';
|
||||
if (uxState === 'completed' || uxState === 'partial') return 'bg-green-500';
|
||||
if (uxState === 'failed') return 'bg-red-500';
|
||||
if (uxState === 'cancelled') return 'bg-gray-500';
|
||||
return 'bg-blue-600';
|
||||
});
|
||||
let isActive = $derived(uxState === 'running' || uxState === 'inserting');
|
||||
|
||||
let label = $derived.by(() => {
|
||||
if (uxState === 'inserting') return $t.translate?.run?.insert_phase || 'Inserting...';
|
||||
@@ -63,6 +53,12 @@
|
||||
return '';
|
||||
});
|
||||
|
||||
let totalRecords = $derived(runState.current?.totalRecords || 0);
|
||||
let successfulRecords = $derived(runState.current?.successfulRecords || 0);
|
||||
let failedRecords = $derived(runState.current?.failedRecords || 0);
|
||||
let skippedRecords = $derived(runState.current?.skippedRecords || 0);
|
||||
let progressPct = $derived(runState.current?.progressPct || 0);
|
||||
|
||||
// Auto-dismiss terminal states after a few seconds
|
||||
$effect(() => {
|
||||
if (uxState === 'completed' || uxState === 'partial') {
|
||||
@@ -88,52 +84,116 @@
|
||||
goto(`/translate/${jobId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(e) {
|
||||
e.stopPropagation();
|
||||
const runId = runState.current?.runId;
|
||||
if (!runId || isCancelling) return;
|
||||
isCancelling = true;
|
||||
try {
|
||||
await cancelRun(runId);
|
||||
addToast($t.translate?.run?.run_cancelled || 'Run cancelled', 'info');
|
||||
} catch (err) {
|
||||
addToast(err?.message || $t.translate?.run?.cancel_failed || 'Cancel failed', 'error');
|
||||
} finally {
|
||||
isCancelling = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 z-[100] cursor-pointer shadow-sm"
|
||||
onclick={handleClick}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="fixed top-0 left-0 right-0 z-[100] shadow-md"
|
||||
role="status"
|
||||
aria-label={label}
|
||||
>
|
||||
<!-- Animated thin bar -->
|
||||
<div class="h-1 bg-gray-200">
|
||||
<div
|
||||
class="h-full {barColor} transition-all duration-500"
|
||||
style="width: {Math.min(runState.current?.progressPct || 0, 100)}%"
|
||||
/>
|
||||
</div>
|
||||
<!-- Compact stats banner -->
|
||||
<div class="{bannerBg} text-white text-xs px-3 py-1 flex items-center gap-3">
|
||||
<span class="font-medium">{label}</span>
|
||||
{#if isActive}
|
||||
<!-- ═══ RICH ACTIVE PANEL ═══ -->
|
||||
<div class="bg-white border-b border-gray-200 px-4 py-3">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<!-- Header row: spinner + label + actions -->
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600" />
|
||||
<span class="text-sm font-semibold text-gray-900">{label}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={handleClick}
|
||||
class="text-xs text-blue-600 hover:text-blue-800 underline"
|
||||
>
|
||||
{$t.common?.open || 'Open'}
|
||||
</button>
|
||||
<button
|
||||
onclick={handleCancel}
|
||||
disabled={isCancelling}
|
||||
class="px-2 py-1 text-xs border border-red-300 text-red-600 rounded hover:bg-red-50 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isCancelling
|
||||
? ($t.translate?.run?.cancelling || 'Cancelling...')
|
||||
: ($t.translate?.run?.cancel || 'Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if uxState === 'running' || uxState === 'inserting'}
|
||||
<span class="opacity-80">
|
||||
{runState.current?.successfulRecords || 0}/{runState.current?.totalRecords || 0}
|
||||
</span>
|
||||
{#if (runState.current?.failedRecords || 0) > 0}
|
||||
<span class="text-red-200">
|
||||
{$t.translate?.run?.failed || 'Failed'}: {runState.current?.failedRecords}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="ml-auto opacity-70 hover:opacity-100 transition-opacity">
|
||||
<!-- Progress bar with percentage -->
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2.5 rounded-full transition-all duration-500"
|
||||
style="width: {Math.min(progressPct, 100)}%"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Stats grid -->
|
||||
<div class="grid grid-cols-4 gap-2 text-xs">
|
||||
<div class="bg-gray-50 rounded px-2 py-1 text-center">
|
||||
<span class="text-gray-500 block">{($t.translate?.run?.total || 'Total')}</span>
|
||||
<span class="font-bold text-gray-900">{totalRecords}</span>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded px-2 py-1 text-center">
|
||||
<span class="text-green-600 block">{($t.translate?.run?.success || 'OK')}</span>
|
||||
<span class="font-bold text-green-700">{successfulRecords}</span>
|
||||
</div>
|
||||
<div class="bg-red-50 rounded px-2 py-1 text-center">
|
||||
<span class="text-red-600 block">{($t.translate?.run?.failed || 'Fail')}</span>
|
||||
<span class="font-bold text-red-700">{failedRecords}</span>
|
||||
</div>
|
||||
<div class="bg-yellow-50 rounded px-2 py-1 text-center">
|
||||
<span class="text-yellow-600 block">{($t.translate?.run?.skipped || 'Skip')}</span>
|
||||
<span class="font-bold text-yellow-700">{skippedRecords}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- ═══ COMPACT TERMINAL BANNER ═══ -->
|
||||
<div
|
||||
class="px-4 py-2 text-white text-sm flex items-center justify-between cursor-pointer"
|
||||
class:bg-green-600={uxState === 'completed' || uxState === 'partial'}
|
||||
class:bg-red-600={uxState === 'failed'}
|
||||
class:bg-gray-500={uxState === 'cancelled'}
|
||||
onclick={handleClick}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if uxState === 'completed'}
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>
|
||||
{:else if uxState === 'partial' || uxState === 'failed'}
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
{:else}
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
{/if}
|
||||
<span class="font-medium">{label}</span>
|
||||
{#if uxState === 'partial'}
|
||||
<span class="opacity-80">{successfulRecords}/{totalRecords}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="opacity-70 hover:opacity-100 transition-opacity">
|
||||
▶ {$t.common?.open || 'Open'}
|
||||
</span>
|
||||
{:else if uxState === 'partial'}
|
||||
<span class="opacity-80">
|
||||
{runState.current?.successfulRecords || 0}/{runState.current?.totalRecords || 0}
|
||||
</span>
|
||||
<span class="ml-auto opacity-70 hover:opacity-100 transition-opacity">
|
||||
▶ {$t.common?.open || 'Open'}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="ml-auto opacity-70 hover:opacity-100 transition-opacity">
|
||||
▶ {$t.common?.open || 'Open'}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<!-- #endregion TranslationRunGlobalIndicator -->
|
||||
|
||||
Reference in New Issue
Block a user