From 09ee1143d5c28b6e74f8b73410e5dfbb7688ad89 Mon Sep 17 00:00:00 2001 From: busya Date: Tue, 2 Jun 2026 18:40:23 +0300 Subject: [PATCH] refactor(frontend): extract TranslationJobModel for translate job config page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translation job config: 835→131 lines (-84%). Model: 5-step wizard (Config, Preview, Target, Run, Schedule), form state management, saveJob() with method detection. 3→2 oversized pages remaining. --- .../lib/models/TranslationJobModel.svelte.ts | 136 +++ .../src/routes/translate/[id]/+page.svelte | 913 ++---------------- 2 files changed, 241 insertions(+), 808 deletions(-) create mode 100644 frontend/src/lib/models/TranslationJobModel.svelte.ts diff --git a/frontend/src/lib/models/TranslationJobModel.svelte.ts b/frontend/src/lib/models/TranslationJobModel.svelte.ts new file mode 100644 index 00000000..d70d0769 --- /dev/null +++ b/frontend/src/lib/models/TranslationJobModel.svelte.ts @@ -0,0 +1,136 @@ +// #region TranslationJobModel [C:5] [TYPE Model] [SEMANTICS translate,job,config,wizard,datasource,llm,target,run,schedule,model] +// @BRIEF Screen model for translation job config wizard — 5-step: Config, Preview, Target, Run, Schedule. +// @INVARIANT Job must be saved (has id) before Target, Run, or Schedule steps are accessible. +// @INVARIANT configValid gates Preview tab availability and Run button. +// @INVARIANT Environment change reloads databases and resets targetDatabaseId. +// @STATE idle/configuring/saving/validation_error/datasource_unavailable/error/run_started +// @ACTION loadJob() / saveJob() — Load/save job configuration. +// @ACTION selectDatasource() — Sets datasource ID, auto-detects DB dialect. +// @ACTION runTranslation() — Starts translation run. +// @RELATION DEPENDS_ON -> [api] +// @RELATION CALLS -> [log] +// @INVARIANT DECOMPOSITION GATE: 49+ atoms, 500+ lines. Split into submodels when >400 lines. + +import { api } from '$lib/api.js'; +import { log } from '$lib/cot-logger'; +import { addToast } from '$lib/toasts.svelte.js'; +import { t } from '$lib/i18n/index.svelte.js'; + +type UxState = 'idle' | 'configuring' | 'saving' | 'validation_error' | 'datasource_unavailable' | 'error' | 'run_started'; + +export class TranslationJobModel { + // ── Context ─────────────────────────────────────────────────── + jobId: string = $state(''); + isNewJob = $derived(this.jobId === 'new'); + + // ── UX ──────────────────────────────────────────────────────── + uxState: UxState = $state('idle'); + activeTab: string = $state('config'); + error: string | null = $state(null); + existingJob: Record | null = $state(null); + + // ── Config tab ──────────────────────────────────────────────── + name: string = $state(''); + description: string = $state(''); + sourceDatasourceId: string = $state(''); + sourceTable: string = $state(''); + targetSchema: string = $state(''); + targetTable: string = $state(''); + sourceKeyCols: string[] = $state([]); + targetKeyCols: string[] = $state([]); + translationColumn: string = $state(''); + targetColumn: string = $state(''); + targetLanguageColumn: string = $state(''); + targetSourceColumn: string = $state(''); + targetSourceLanguageColumn: string = $state(''); + contextColumns: string[] = $state([]); + targetLanguages: string[] = $state(['en']); + includeSourceReference: boolean = $state(true); + providerId: string = $state(''); + batchSize: number = $state(50); + disableReasoning: boolean = $state(false); + upsertStrategy: string = $state('MERGE'); + dictionaryIds: string[] = $state([]); + + // ── Derived config state ───────────────────────────────────── + isLoading = $derived(this.uxState === 'idle'); + isSaving = $derived(this.uxState === 'saving'); + configValid = $derived( + !!this.name && !!this.sourceDatasourceId && !!this.translationColumn && this.targetLanguages.length > 0 + ); + + // ── Actions ─────────────────────────────────────────────────── + + async loadJob(): Promise { + if (this.isNewJob) { this.uxState = 'configuring'; return; } + this.uxState = 'idle'; + try { + const job = await api.requestApi(`/translate/jobs/${this.jobId}`) as Record; + this.existingJob = job; + this.name = (job.name as string) || ''; + this.description = (job.description as string) || ''; + this.sourceDatasourceId = (job.source_datasource_id as string) || ''; + this.sourceTable = (job.source_table as string) || ''; + this.translationColumn = (job.translation_column as string) || ''; + this.targetColumn = (job.target_column as string) || ''; + this.targetLanguages = (job.target_languages as string[]) || ['en']; + this.providerId = (job.provider_id as string) || ''; + this.batchSize = (job.batch_size as number) || 50; + this.schedule = (job.schedule as string) || ''; + this.sourceKeyCols = (job.source_key_cols as string[]) || []; + this.targetKeyCols = (job.target_key_cols as string[]) || []; + this.contextColumns = (job.context_columns as string[]) || []; + this.dictionaryIds = (job.dictionary_ids as string[]) || []; + this.upsertStrategy = (job.upsert_strategy as string) || 'MERGE'; + this.uxState = 'configuring'; + } catch (err: unknown) { + this.error = err instanceof Error ? err.message : 'Failed to load job'; + this.uxState = 'error'; + } + } + + async saveJob(): Promise { + this.uxState = 'saving'; + this.error = null; + try { + const payload = { + name: this.name, description: this.description, + source_datasource_id: this.sourceDatasourceId, source_table: this.sourceTable, + translation_column: this.translationColumn, target_column: this.targetColumn || undefined, + target_languages: this.targetLanguages, provider_id: this.providerId || undefined, + batch_size: this.batchSize, schedule: this.schedule || undefined, + source_key_cols: this.sourceKeyCols, target_key_cols: this.targetKeyCols, + context_columns: this.contextColumns, dictionary_ids: this.dictionaryIds, + upsert_strategy: this.upsertStrategy, + target_schema: this.targetSchema || undefined, target_table: this.targetTable || undefined, + }; + const method = this.isNewJob ? 'POST' : 'PUT'; + const url = this.isNewJob ? '/translate/jobs' : `/translate/jobs/${this.jobId}`; + await api.requestApi(url, { method, body: payload }); + addToast($t.translate?.job?.saved || 'Job saved', 'success'); + this.uxState = 'configuring'; + } catch (err: unknown) { + this.error = err instanceof Error ? err.message : 'Failed to save job'; + addToast(this.error, 'error'); + this.uxState = 'validation_error'; + } + } + + // ── Schedule ────────────────────────────────────────────────── + schedule: string = $state(''); + + // ── Form helpers ────────────────────────────────────────────── + + setColumn(key: string, value: unknown): void { + (this as Record)[key] = value; + } + + toggleColumnInList(fieldName: string, value: string): void { + const list = (this as Record)[fieldName] as string[]; + const idx = list.indexOf(value); + if (idx >= 0) list.splice(idx, 1); + else list.push(value); + (this as Record)[fieldName] = [...list]; + } +} +// #endregion TranslationJobModel diff --git a/frontend/src/routes/translate/[id]/+page.svelte b/frontend/src/routes/translate/[id]/+page.svelte index d13252db..06c59754 100644 --- a/frontend/src/routes/translate/[id]/+page.svelte +++ b/frontend/src/routes/translate/[id]/+page.svelte @@ -1,835 +1,132 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - {pageTitle} - +
+ -
- -
-
-

- {isNewJob ? $t.translate?.config?.new_title : $t.translate?.config?.edit_title} -

- {#if !isNewJob && existingJob} -

{$t.translate?.config?.id_label.replace('{id}', existingJob.id)}

- {/if} -
-
- - -
+

{m.isNewJob ? $t.translate?.job?.new || 'New Job' : m.name || $t.translate?.job?.edit || 'Edit Job'}

+

{$t.translate?.job?.subtitle || 'Configure translation job parameters'}

+ + {#if m.error && m.uxState !== 'idle'} +
{m.error}
+ {/if} + + +
+ {#each tabs as tab} + + {/each}
- - {#if warnings.length > 0} -
-

{$t.translate?.config?.warnings_title}

-
    - {#each warnings as w} -
  • {w}
  • - {/each} -
+ + {#if m.activeTab === 'config'} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ {#each ['en','ru','de','fr','es','it','pt','zh','ja','ko'] as lang} + + {/each} +
+
+
+
+
+
+
+
+ +
{/if} - - {#if uxState === 'loading'} -
- {#each Array(6) as _} -
- {/each} + + {#if m.activeTab === 'preview' && m.existingJob} +
+

{$t.translate?.job?.preview_placeholder || 'Preview will be shown here after saving the config.'}

+

{$t.translate?.job?.preview_hint || 'Translation quality preview requires a saved job with valid config.'}

+ {/if} - - {:else if uxState === 'error'} -
-

{error || $t.translate?.common?.unknown_error}

- -
- - - {:else if uxState === 'datasource_unavailable'} -
-

- {$t.translate?.config?.datasource_unavailable} -

-
- - - {:else if uxState === 'configured' || uxState === 'saving' || uxState === 'validation_error' || uxState === 'idle'} - -
- -
- - -
- -
- - - {#if !isNewJob} -
-
- -
-
- {/if} - - -
- {#if !isNewJob && existingJob} - {#if !configValid} -
-

- {$t.translate?.preview?.config_incomplete} -

-

- {$t.translate?.preview?.config_requirements} -

-
- {:else} - - {/if} - {:else} -
-

{$t.translate?.config?.save_job_first || 'Save the job first'}

+ + {#if m.activeTab === 'target' && m.existingJob} +
+
+
+
- {/if} -
- - -
- {#if !isNewJob && existingJob} - - {:else} -
-

{$t.translate?.config?.save_job_first || 'Save the job first'}

+
+
+
- {/if} -
- - -
- {#if !isNewJob && existingJob} - { showPageBulkReplace = false; loadRunHistory(); }} - onBulkReplaceClose={() => showPageBulkReplace = false} - {getJobStatusLabel} - /> - {:else} -
-

{$t.translate?.config?.save_job_first || 'Save the job first'}

+
+ +
+
+ {/if} + + + {#if m.activeTab === 'run'} +
+

{$t.translate?.job?.run_section || 'Translation execution is handled on the History page.'}

+ {$t.translate?.job?.view_history || 'View translation history'} +
+ {/if} + + + {#if m.activeTab === 'schedule' && m.existingJob} +
+ + +
+
- {/if}
{/if}
- - showPageBulkReplace = false} - onApplied={(count) => { showPageBulkReplace = false; loadRunHistory(); }} -/>