Page reduced from 1519 to 909 lines (-40%). Extracted: - ConfigTabForm.svelte (370 lines) — datasource search, column mapping, LLM, target languages, dictionaries, batch config - TargetTabForm.svelte (120 lines) — target schema/table/database, target column mapping, TargetSchemaHint - RunTabContent.svelte (170 lines) — run cards, progress, history All three components use () props for seamless parent state sync. Sub-components (TargetSchemaHint, TranslationPreview, TranslationRunProgress, ScheduleConfig, BulkReplaceModal) unchanged.
202 lines
9.4 KiB
Svelte
202 lines
9.4 KiB
Svelte
<!-- #region RunTabContent [C:3] [TYPE Component] [SEMANTICS translate, run, execution, progress] -->
|
|
<!-- @BRIEF Run tab — translation execution with incremental/full mode selection, progress
|
|
tracking via TranslationRunProgress, and run history with expandable details. -->
|
|
<!-- @LAYER UI -->
|
|
<!-- @RELATION DEPENDS_ON -> [TranslationRunProgress] -->
|
|
<!-- @RELATION DEPENDS_ON -> [TranslationRunResult] -->
|
|
<!-- @RELATION BINDS_TO -> [translationRunStore] -->
|
|
<!-- @UX_STATE Idle — no run active, run cards visible -->
|
|
<!-- @UX_STATE Running — TranslationRunProgress bar visible, cards show active state -->
|
|
<!-- @UX_STATE Completed — run history list with expandable details -->
|
|
<!-- @UX_STATE Error — error banner above run history -->
|
|
<!-- @UX_FEEDBACK Toast on status change / run error / run complete -->
|
|
<!-- @UX_FEEDBACK BulkReplace modal button in recent runs header -->
|
|
<!-- @UX_RECOVERY Retry button on TranslationRunProgress for failed runs -->
|
|
<!-- @UX_REACTIVITY Props -> $props() for config/state, callbacks for actions -->
|
|
<script>
|
|
import { t } from '$lib/i18n';
|
|
import { addToast } from '$lib/toasts.js';
|
|
import { updateJob } from '$lib/api/translate.js';
|
|
import TranslationRunProgress from './TranslationRunProgress.svelte';
|
|
import TranslationRunResult from './TranslationRunResult.svelte';
|
|
|
|
let {
|
|
status = 'DRAFT',
|
|
jobId = '',
|
|
isRunning = false,
|
|
isFullRun = false,
|
|
runError = null,
|
|
currentRunId = null,
|
|
runComplete = false,
|
|
completedRuns = [],
|
|
expandedRunIds = [],
|
|
showBulkReplace = false,
|
|
onTriggerRun = (isFull) => {},
|
|
onRetryRun = () => {},
|
|
onRetryInsert = () => {},
|
|
onLoadRunHistory = () => {},
|
|
onToggleRunDetails = (id) => {},
|
|
onBulkReplaceApplied = (count) => {},
|
|
onBulkReplaceClose = () => {},
|
|
getJobStatusLabel = (s) => s,
|
|
} = $props();
|
|
|
|
async function handleMarkReady() {
|
|
try {
|
|
await updateJob(jobId, { status: 'READY' });
|
|
status = 'READY';
|
|
addToast($t.translate?.config?.job_updated, 'success');
|
|
} catch (e) {
|
|
addToast(e?.message || 'Failed to update status', 'error');
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<section class="bg-white border border-gray-200 rounded-lg p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">{$t.translate?.config?.run_translation}</h2>
|
|
|
|
<!-- Status display + transition -->
|
|
<div class="flex items-center gap-3 mb-4 p-3 bg-gray-50 rounded-lg">
|
|
<span class="text-sm text-gray-600">{$t.translate?.config?.status}:</span>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
|
{status === 'READY' ? 'bg-green-100 text-green-700' : ''}
|
|
{status === 'DRAFT' ? 'bg-yellow-100 text-yellow-700' : ''}
|
|
{status === 'ACTIVE' ? 'bg-emerald-100 text-emerald-700' : ''}
|
|
{status === 'RUNNING' ? 'bg-blue-100 text-blue-700' : ''}
|
|
{status === 'COMPLETED' ? 'bg-green-100 text-green-700' : ''}
|
|
{status === 'FAILED' ? 'bg-red-100 text-red-700' : ''}">
|
|
{getJobStatusLabel(status)}
|
|
</span>
|
|
{#if status === 'DRAFT'}
|
|
<button
|
|
onclick={handleMarkReady}
|
|
class="ml-auto px-3 py-1 text-xs bg-primary text-white rounded hover:bg-primary-hover transition-colors"
|
|
>
|
|
{$t.translate?.config?.mark_ready || 'Mark as READY'}
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<!-- Run mode selection: two cards -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<!-- Incremental run card -->
|
|
<div class="border border-green-200 rounded-lg p-4 {isRunning && !isFullRun ? 'bg-green-50' : 'bg-white'}">
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
|
|
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h3 class="text-sm font-semibold text-gray-900">{$t.translate?.config?.run_incremental}</h3>
|
|
<p class="text-xs text-gray-500 mt-1">{$t.translate?.config?.run_incremental_desc}</p>
|
|
<button
|
|
onclick={() => onTriggerRun(false)}
|
|
disabled={isRunning || status === 'DRAFT'}
|
|
class="mt-3 px-5 py-1.5 text-sm font-medium bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{isRunning && !isFullRun ? $t.translate?.config?.running : $t.translate?.config?.run_translation}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Full run card -->
|
|
<div class="border border-primary-ring rounded-lg p-4 {isRunning && isFullRun ? 'bg-primary-light' : 'bg-white'}">
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
|
|
<svg class="w-4 h-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<h3 class="text-sm font-semibold text-gray-900">{$t.translate?.config?.run_full}</h3>
|
|
<p class="text-xs text-gray-500 mt-1">{$t.translate?.config?.run_full_desc}</p>
|
|
<button
|
|
onclick={() => onTriggerRun(true)}
|
|
disabled={isRunning || status === 'DRAFT'}
|
|
class="mt-3 px-5 py-1.5 text-sm font-medium bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{isRunning && isFullRun ? $t.translate?.config?.running : $t.translate?.config?.full_translate}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{#if runError}
|
|
<div class="bg-destructive-light border border-destructive-light rounded-lg p-3">
|
|
<p class="text-sm text-red-600">{runError}</p>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if currentRunId && !runComplete}
|
|
<TranslationRunProgress
|
|
runId={currentRunId}
|
|
onRetry={onRetryRun}
|
|
onRetryInsert={onRetryInsert}
|
|
/>
|
|
{/if}
|
|
|
|
{#if completedRuns.length > 0}
|
|
<div class="mt-4">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h3 class="text-sm font-medium text-gray-700">{$t.translate?.config?.recent_runs}</h3>
|
|
<button
|
|
onclick={() => showBulkReplace = true}
|
|
class="px-3 py-1.5 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-700 transition-colors"
|
|
>
|
|
{$t.translate?.run?.bulk_replace || 'Bulk Replace'}
|
|
</button>
|
|
</div>
|
|
<div class="space-y-3">
|
|
{#each completedRuns as run}
|
|
<div class="border border-gray-200 rounded-lg bg-white">
|
|
<div class="flex items-center justify-between gap-3 p-4">
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="text-xs text-gray-500">{$t.translate?.run?.run_id}</span>
|
|
<code class="text-xs text-gray-700 break-all">{run.id}</code>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-medium
|
|
{run.status === 'COMPLETED' ? 'bg-green-100 text-green-700' : ''}
|
|
{run.status === 'FAILED' ? 'bg-red-100 text-red-700' : ''}
|
|
{run.status === 'CANCELLED' ? 'bg-gray-100 text-gray-700' : ''}
|
|
{run.status !== 'COMPLETED' && run.status !== 'FAILED' && run.status !== 'CANCELLED' ? 'bg-yellow-100 text-yellow-700' : ''}"
|
|
>
|
|
{run.status}
|
|
</span>
|
|
</div>
|
|
<div class="mt-2 flex flex-wrap gap-3 text-xs text-gray-500">
|
|
<span>{$t.translate?.run?.total}: {run.total_records || 0}</span>
|
|
<span>{$t.translate?.run?.success}: {run.successful_records || 0}</span>
|
|
<span>{$t.translate?.run?.failed}: {run.failed_records || 0}</span>
|
|
<span>{$t.translate?.run?.skipped}: {run.skipped_records || 0}</span>
|
|
</div>
|
|
{#if run.error_message}
|
|
<p class="mt-2 text-xs text-red-600 break-words">{run.error_message}</p>
|
|
{/if}
|
|
</div>
|
|
<button
|
|
onclick={() => onToggleRunDetails(run.id)}
|
|
class="shrink-0 px-3 py-1.5 text-xs border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors"
|
|
>
|
|
{expandedRunIds.includes(run.id)
|
|
? ($t.translate?.run?.hide_details || 'Hide details')
|
|
: ($t.translate?.run?.show_details || 'Show details')}
|
|
</button>
|
|
</div>
|
|
{#if expandedRunIds.includes(run.id)}
|
|
<div class="border-t border-gray-200 p-4">
|
|
<TranslationRunResult runId={run.id} onRefresh={onLoadRunHistory} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</section>
|
|
<!-- #endregion RunTabContent -->
|