feat(assistant): add multi-dialog UX, task-aware llm settings, and i18n cleanup

This commit is contained in:
2026-02-23 23:45:01 +03:00
parent ab1c87ffba
commit 7df7b4f98c
30 changed files with 1145 additions and 221 deletions

View File

@@ -17,6 +17,13 @@
let providers = [];
let loading = true;
let savingPrompts = false;
let plannerProvider = '';
let plannerModel = '';
let bindings = {
dashboard_validation: '',
documentation: '',
git_commit: '',
};
let prompts = {
documentation_prompt: '',
dashboard_validation_prompt: '',
@@ -31,6 +38,30 @@
git_commit_prompt:
"Generate a concise and professional git commit message based on the following diff and recent history.\\nUse Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\nRecent History:\\n{history}\\n\\nDiff:\\n{diff}\\n\\nCommit Message:",
};
const DEFAULT_LLM_PROVIDER_BINDINGS = {
dashboard_validation: '',
documentation: '',
git_commit: '',
};
function isMultimodalModel(modelName) {
const token = (modelName || '').toLowerCase();
if (!token) return false;
return (
token.includes('gpt-4o') ||
token.includes('gpt-4.1') ||
token.includes('vision') ||
token.includes('vl') ||
token.includes('gemini') ||
token.includes('claude-3') ||
token.includes('claude-sonnet-4')
);
}
function getProviderById(providerId) {
if (!providerId) return null;
return providers.find((item) => item.id === providerId) || null;
}
async function fetchProviders() {
loading = true;
@@ -44,6 +75,12 @@
...DEFAULT_LLM_PROMPTS,
...(consolidatedSettings?.llm?.prompts || {}),
};
bindings = {
...DEFAULT_LLM_PROVIDER_BINDINGS,
...(consolidatedSettings?.llm?.provider_bindings || {}),
};
plannerProvider = consolidatedSettings?.llm?.assistant_planner_provider || '';
plannerModel = consolidatedSettings?.llm?.assistant_planner_model || '';
} catch (err) {
console.error("Failed to fetch providers", err);
} finally {
@@ -51,7 +88,7 @@
}
}
async function savePrompts() {
async function saveSettings() {
savingPrompts = true;
try {
const current = await requestApi('/settings/consolidated');
@@ -63,12 +100,18 @@
...DEFAULT_LLM_PROMPTS,
...prompts,
},
provider_bindings: {
...DEFAULT_LLM_PROVIDER_BINDINGS,
...bindings,
},
assistant_planner_provider: plannerProvider || '',
assistant_planner_model: plannerModel || '',
},
};
await requestApi('/settings/consolidated', 'PATCH', payload);
addToast($t.settings?.save_success || 'Settings saved', 'success');
} catch (err) {
console.error('[LLMSettingsPage][Coherence:Failed] Failed to save prompts', err);
console.error('[LLMSettingsPage][Coherence:Failed] Failed to save llm settings', err);
addToast($t.settings?.save_failed || 'Failed to save settings', 'error');
} finally {
savingPrompts = false;
@@ -93,6 +136,121 @@
{:else}
<ProviderConfig {providers} onSave={fetchProviders} />
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
<h2 class="text-lg font-semibold text-gray-900">
{$t.settings?.llm_chatbot_settings_title || 'Chatbot Planner Settings'}
</h2>
<p class="mt-1 text-sm text-gray-600">
{$t.settings?.llm_chatbot_settings_description ||
'Select provider and optional model override for assistant intent planning.'}
</p>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label for="admin-planner-provider" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_chatbot_provider || 'Chatbot Provider'}
</label>
<select
id="admin-planner-provider"
bind:value={plannerProvider}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || 'Use Default'}</option>
{#each providers as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
</option>
{/each}
</select>
</div>
<div>
<label for="admin-planner-model" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_chatbot_model || 'Chatbot Model Override'}
</label>
<input
id="admin-planner-model"
type="text"
bind:value={plannerModel}
placeholder={$t.settings?.llm_chatbot_model_placeholder || 'Optional, e.g. gpt-4.1-mini'}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
/>
</div>
</div>
</div>
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
<h2 class="text-lg font-semibold text-gray-900">
{$t.settings?.llm_provider_bindings_title || 'Provider Bindings by Task'}
</h2>
<p class="mt-1 text-sm text-gray-600">
{$t.settings?.llm_provider_bindings_description ||
'Select which provider is used by default for each LLM task.'}
</p>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label for="admin-binding-dashboard-validation" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_binding_dashboard_validation || 'Dashboard Validation Provider'}
</label>
<select
id="admin-binding-dashboard-validation"
bind:value={bindings.dashboard_validation}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || 'Use Default'}</option>
{#each providers as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
</option>
{/each}
</select>
{#if bindings.dashboard_validation && !isMultimodalModel(getProviderById(bindings.dashboard_validation)?.default_model)}
<p class="mt-1 text-xs text-amber-700">
{$t.settings?.llm_multimodal_warning ||
'Dashboard validation requires a multimodal model (image input).'}
</p>
{/if}
</div>
<div>
<label for="admin-binding-documentation" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_binding_documentation || 'Documentation Provider'}
</label>
<select
id="admin-binding-documentation"
bind:value={bindings.documentation}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || 'Use Default'}</option>
{#each providers as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
</option>
{/each}
</select>
</div>
<div class="md:col-span-2">
<label for="admin-binding-git-commit" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_binding_git_commit || 'Git Commit Provider'}
</label>
<select
id="admin-binding-git-commit"
bind:value={bindings.git_commit}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || 'Use Default'}</option>
{#each providers as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
</option>
{/each}
</select>
</div>
</div>
</div>
<div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
<h2 class="text-lg font-semibold text-gray-900">
{$t.settings?.llm_prompts_title || 'LLM Prompt Templates'}
@@ -144,7 +302,7 @@
<button
class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-60"
disabled={savingPrompts}
on:click={savePrompts}
on:click={saveSettings}
>
{savingPrompts ? '...' : ($t.settings?.save_llm_prompts || 'Save LLM Prompts')}
</button>

View File

@@ -135,7 +135,7 @@
// Update selection state
updateSelectionState();
} catch (err) {
error = err.message || 'Failed to load datasets';
error = err.message || ($t.datasets?.load_failed || 'Failed to load datasets');
console.error('[DatasetHub][Coherence:Failed]', err);
} finally {
isLoading = false;
@@ -289,7 +289,7 @@
}
} catch (err) {
console.error('[DatasetHub][Coherence:Failed]', err);
alert('Failed to create mapping task');
alert($t.datasets?.mapping_task_failed || 'Failed to create mapping task');
}
}
@@ -297,7 +297,7 @@
async function handleBulkGenerateDocs() {
if (selectedIds.size === 0) return;
if (!llmProvider) {
alert('Please select an LLM provider');
alert($t.datasets?.select_llm_provider || 'Please select an LLM provider');
return;
}
@@ -320,7 +320,7 @@
}
} catch (err) {
console.error('[DatasetHub][Coherence:Failed]', err);
alert('Failed to create documentation generation task');
alert($t.datasets?.docs_task_failed || 'Failed to create documentation generation task');
}
}
@@ -430,18 +430,18 @@
on:click={handleSelectAll}
disabled={total === 0}
>
{isAllSelected ? 'Deselect All' : 'Select All'}
{isAllSelected ? ($t.datasets?.deselect_all || 'Deselect All') : ($t.datasets?.select_all || 'Select All')}
</button>
<button
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100"
on:click={handleSelectVisible}
disabled={datasets.length === 0}
>
{isAllVisibleSelected ? 'Deselect Visible' : 'Select Visible'}
{isAllVisibleSelected ? ($t.datasets?.deselect_visible || 'Deselect Visible') : ($t.datasets?.select_visible || 'Select Visible')}
</button>
{#if selectedIds.size > 0}
<span class="text-sm text-gray-600">
{selectedIds.size} selected
{($t.datasets?.selected_count || "{count} selected").replace("{count}", String(selectedIds.size))}
</span>
{/if}
</div>
@@ -449,7 +449,7 @@
<input
type="text"
class="px-2 py-1 border border-gray-300 rounded bg-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Search datasets..."
placeholder={$t.datasets?.search_placeholder || "Search datasets..."}
on:input={handleSearch}
value={searchQuery}
/>
@@ -554,7 +554,10 @@
{#if totalPages > 1}
<div class="flex items-center justify-between px-3 py-3 bg-gray-50 border-t border-gray-200">
<div class="text-sm text-gray-500">
Showing {((currentPage - 1) * pageSize) + 1}-{Math.min(currentPage * pageSize, total)} of {total}
{($t.dashboard?.showing || "Showing {start} to {end} of {total} dashboards")
.replace("{start}", String(((currentPage - 1) * pageSize) + 1))
.replace("{end}", String(Math.min(currentPage * pageSize, total)))
.replace("{total}", String(total))}
</div>
<div class="flex items-center gap-2">
<button
@@ -562,14 +565,14 @@
on:click={() => handlePageChange(1)}
disabled={currentPage === 1}
>
First
{$t.common?.first || 'First'}
</button>
<button
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
>
Previous
{$t.dashboard?.previous || 'Previous'}
</button>
{#each Array.from({length: totalPages}, (_, i) => i + 1) as pageNum}
<button
@@ -584,14 +587,14 @@
on:click={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
{$t.dashboard?.next || 'Next'}
</button>
<button
class="px-2 py-1 text-sm border border-gray-300 rounded hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={() => handlePageChange(totalPages)}
disabled={currentPage === totalPages}
>
Last
{$t.common?.last || 'Last'}
</button>
</div>
<div>
@@ -600,11 +603,11 @@
value={pageSize}
on:change={handlePageSizeChange}
>
<option value={5}>5 per page</option>
<option value={10}>10 per page</option>
<option value={25}>25 per page</option>
<option value={50}>50 per page</option>
<option value={100}>100 per page</option>
<option value={5}>5 {$t.common?.per_page || 'per page'}</option>
<option value={10}>10 {$t.common?.per_page || 'per page'}</option>
<option value={25}>25 {$t.common?.per_page || 'per page'}</option>
<option value={50}>50 {$t.common?.per_page || 'per page'}</option>
<option value={100}>100 {$t.common?.per_page || 'per page'}</option>
</select>
</div>
</div>
@@ -616,7 +619,7 @@
<div class="flex items-center justify-between max-w-7xl mx-auto">
<div class="flex items-center gap-4">
<span class="font-medium">
{selectedIds.size} selected
{selectedIds.size} {$t.datasets?.selected || 'selected'}
</span>
</div>
<div class="flex gap-3">
@@ -624,19 +627,19 @@
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700"
on:click={() => showMapColumnsModal = true}
>
Map Columns
{$t.datasets?.action_map_columns || 'Map Columns'}
</button>
<button
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700"
on:click={() => showGenerateDocsModal = true}
>
Generate Docs
{$t.datasets?.generate_docs || 'Generate Docs'}
</button>
<button
class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100"
on:click={() => selectedIds.clear()}
>
Cancel
{$t.common?.cancel || 'Cancel'}
</button>
</div>
</div>
@@ -649,8 +652,8 @@
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" on:click={() => showMapColumnsModal = false}>
<div class="bg-white rounded-lg shadow-2xl max-w-xl w-full m-4 max-h-[80vh] overflow-y-auto" on:click|stopPropagation>
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between relative">
<h2 class="text-xl font-bold">Bulk Column Mapping</h2>
<button on:click={() => showMapColumnsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label="Close modal">
<h2 class="text-xl font-bold">{$t.datasets?.bulk_map_columns || 'Bulk Column Mapping'}</h2>
<button on:click={() => showMapColumnsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label={$t.common?.close_modal || "Close modal"}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
@@ -660,28 +663,28 @@
<div class="p-4">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">Source Type</label>
<label class="block text-sm font-medium mb-2">{$t.datasets?.source_type || 'Source Type'}</label>
<select
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
bind:value={mapSourceType}
>
<option value="postgresql">PostgreSQL Comments</option>
<option value="xlsx">XLSX File</option>
<option value="postgresql">{$t.datasets?.source_postgresql_comments || 'PostgreSQL Comments'}</option>
<option value="xlsx">{$t.datasets?.source_xlsx || 'XLSX File'}</option>
</select>
</div>
{#if mapSourceType === 'postgresql'}
<div>
<label class="block text-sm font-medium mb-2">Connection ID</label>
<label class="block text-sm font-medium mb-2">{$t.datasets?.connection_id || 'Connection ID'}</label>
<input
type="text"
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter connection ID..."
placeholder={$t.datasets?.connection_id_placeholder || "Enter connection ID..."}
bind:value={mapConnectionId}
/>
</div>
{:else}
<div>
<label class="block text-sm font-medium mb-2">XLSX File</label>
<label class="block text-sm font-medium mb-2">{$t.datasets?.xlsx_file || 'XLSX File'}</label>
<input
type="file"
class="w-full"
@@ -692,7 +695,7 @@
</div>
{/if}
<div>
<label class="block text-sm font-medium mb-2">Selected Datasets</label>
<label class="block text-sm font-medium mb-2">{$t.datasets?.selected_datasets || 'Selected Datasets'}</label>
<div class="max-h-40 overflow-y-auto">
{#each Array.from(selectedIds) as id}
{#each datasets as d}
@@ -706,14 +709,14 @@
</div>
</div>
<div class="px-4 py-3 border-t border-gray-200 flex justify-end gap-3">
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showMapColumnsModal = false}>Cancel</button>
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showMapColumnsModal = false}>{$t.common?.cancel || 'Cancel'}</button>
<button
type="button"
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
on:click|preventDefault={handleBulkMapColumns}
disabled={selectedIds.size === 0 || (mapSourceType === 'postgresql' && !mapConnectionId) || (mapSourceType === 'xlsx' && (!mapFileData || mapFileData.length === 0))}
>
Start Mapping
{$t.datasets?.start_mapping || 'Start Mapping'}
</button>
</div>
</div>
@@ -725,8 +728,8 @@
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50" on:click={() => showGenerateDocsModal = false}>
<div class="bg-white rounded-lg shadow-2xl max-w-xl w-full m-4 max-h-[80vh] overflow-y-auto" on:click|stopPropagation>
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between relative">
<h2 class="text-xl font-bold">Bulk Documentation Generation</h2>
<button on:click={() => showGenerateDocsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label="Close modal">
<h2 class="text-xl font-bold">{$t.datasets?.bulk_docs_generation || 'Bulk Documentation Generation'}</h2>
<button on:click={() => showGenerateDocsModal = false} class="absolute top-4 right-4 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full" aria-label={$t.common?.close_modal || "Close modal"}>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
@@ -736,19 +739,19 @@
<div class="p-4">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-2">LLM Provider</label>
<label class="block text-sm font-medium mb-2">{$t.dashboard?.llm_provider || 'LLM Provider'}</label>
<select
class="w-full px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
bind:value={llmProvider}
>
<option value="">Select LLM provider...</option>
<option value="">{$t.datasets?.select_llm_provider_option || 'Select LLM provider...'}</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="cohere">Cohere</option>
</select>
</div>
<div>
<label class="block text-sm font-medium mb-2">Selected Datasets</label>
<label class="block text-sm font-medium mb-2">{$t.datasets?.selected_datasets || 'Selected Datasets'}</label>
<div class="max-h-40 overflow-y-auto">
{#each Array.from(selectedIds) as id}
{#each datasets as d}
@@ -762,13 +765,13 @@
</div>
</div>
<div class="px-4 py-3 border-t border-gray-200 flex justify-end gap-3">
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showGenerateDocsModal = false}>Cancel</button>
<button class="px-3 py-1.5 border border-gray-300 rounded hover:bg-gray-100" on:click={() => showGenerateDocsModal = false}>{$t.common?.cancel || 'Cancel'}</button>
<button
class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
on:click={handleBulkGenerateDocs}
disabled={!llmProvider || selectedIds.size === 0}
>
Generate Documentation
{$t.datasets?.generate_documentation || 'Generate Documentation'}
</button>
</div>
</div>

View File

@@ -38,7 +38,7 @@
// Load dataset details from API
async function loadDatasetDetail() {
if (!datasetId || !envId) {
error = "Missing dataset ID or environment ID";
error = $t.datasets?.missing_context || "Missing dataset ID or environment ID";
isLoading = false;
return;
}
@@ -49,7 +49,7 @@
const response = await api.getDatasetDetail(envId, datasetId);
dataset = response;
} catch (err) {
error = err.message || "Failed to load dataset details";
error = err.message || ($t.datasets?.load_detail_failed || "Failed to load dataset details");
console.error("[DatasetDetail][Coherence:Failed]", err);
} finally {
isLoading = false;
@@ -63,7 +63,7 @@
// Navigate back to dataset list
function goBack() {
goto(`/dashboards?env_id=${envId}`);
goto(`/datasets?env_id=${envId}`);
}
// Get column type icon/color
@@ -232,7 +232,7 @@
<span class="text-sm text-gray-500"
>{$t.datasets?.type || "Type"}</span
>
<span class="text-sm font-medium text-gray-900">SQL Lab View</span>
<span class="text-sm font-medium text-gray-900">{$t.datasets?.sql_lab_view || "SQL Lab View"}</span>
</div>
{/if}
{#if dataset.created_on}
@@ -334,17 +334,17 @@
</div>
<div class="flex items-center gap-2 text-xs text-gray-500">
{#if column.is_dttm}
<span class="text-xs text-green-600">📅 Date/Time</span>
<span class="text-xs text-green-600">📅 {$t.datasets?.date_time || "Date/Time"}</span>
{/if}
{#if !column.is_active}
<span class="text-xs text-gray-400">(Inactive)</span>
<span class="text-xs text-gray-400">({$t.datasets?.inactive || "Inactive"})</span>
{/if}
<span
class="inline-flex items-center px-2 py-0.5 text-xs rounded-full {column.description
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-600'}"
>
{column.description ? "✓ Mapped" : "Unmapped"}
{column.description ? `✓ ${$t.datasets?.mapped || "Mapped"}` : ($t.datasets?.unmapped || "Unmapped")}
</span>
</div>
{#if column.description}

View File

@@ -64,10 +64,10 @@
</script>
<div class="max-w-6xl mx-auto p-6">
<PageHeader title="Git Dashboard Management">
<PageHeader title={$t.git?.management || "Git Management"}>
<div slot="actions" class="flex items-center space-x-4">
<Select
label="Environment"
label={$t.dashboard?.environment || "Environment"}
bind:value={selectedEnvId}
options={environments.map(e => ({ value: e.id, label: e.name }))}
/>
@@ -79,15 +79,15 @@
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
{:else}
<Card title="Select Dashboard to Manage">
<Card title={$t.git?.select_dashboard || "Select Dashboard to Manage"}>
{#if fetchingDashboards}
<p class="text-gray-500">Loading dashboards...</p>
<p class="text-gray-500">{$t.common?.loading || "Loading..."}</p>
{:else if dashboards.length > 0}
<DashboardGrid {dashboards} />
{:else}
<p class="text-gray-500 italic">No dashboards found in this environment.</p>
<p class="text-gray-500 italic">{$t.dashboard?.no_dashboards || "No dashboards found in this environment."}</p>
{/if}
</Card>
{/if}
</div>
<!-- [/DEF:GitDashboardPage:Component] -->
<!-- [/DEF:GitDashboardPage:Component] -->

View File

@@ -202,7 +202,7 @@
// Task status update will be handled by store/websocket
} catch (e) {
console.error("Failed to resume task:", e);
passwordPromptErrorMessage = e.message;
passwordPromptErrorMessage = e.message || ($t.migration?.resume_failed || "Failed to resume task");
// Keep prompt open
}
}
@@ -216,15 +216,15 @@
*/
async function startMigration() {
if (!sourceEnvId || !targetEnvId) {
error = "Please select both source and target environments.";
error = $t.migration?.select_both_envs || "Please select both source and target environments.";
return;
}
if (sourceEnvId === targetEnvId) {
error = "Source and target environments must be different.";
error = $t.migration?.different_envs || "Source and target environments must be different.";
return;
}
if (selectedDashboardIds.length === 0) {
error = "Please select at least one dashboard to migrate.";
error = $t.migration?.select_dashboards || "Please select at least one dashboard to migrate.";
return;
}
@@ -249,7 +249,7 @@
selectedTask.set(task);
} catch (fetchErr) {
// Fallback: create a temporary task object to switch view immediately
console.warn("Could not fetch task details immediately, using placeholder.");
console.warn($t.migration?.task_placeholder_warn || "Could not fetch task details immediately, using placeholder.");
selectedTask.set({
id: result.task_id,
plugin_id: 'superset-migration',
@@ -283,7 +283,7 @@
</div>
{:else}
{#if loading}
<p>Loading environments...</p>
<p>{$t.migration?.loading_envs || "Loading environments..."}</p>
{:else if error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
@@ -292,12 +292,12 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<EnvSelector
label="Source Environment"
label={$t.migration?.source_env || "Source Environment"}
bind:selectedId={sourceEnvId}
{environments}
/>
<EnvSelector
label="Target Environment"
label={$t.migration?.target_env || "Target Environment"}
bind:selectedId={targetEnvId}
{environments}
/>
@@ -305,7 +305,7 @@
<!-- [DEF:DashboardSelectionSection:Component] -->
<div class="mb-8">
<h2 class="text-lg font-medium mb-4">Select Dashboards</h2>
<h2 class="text-lg font-medium mb-4">{$t.migration?.select_dashboards_title || "Select Dashboards"}</h2>
{#if sourceEnvId}
<DashboardGrid
@@ -314,7 +314,7 @@
environmentId={sourceEnvId}
/>
{:else}
<p class="text-gray-500 italic">Select a source environment to view dashboards.</p>
<p class="text-gray-500 italic">{$t.dashboard?.select_source || "Select a source environment to view dashboards."}</p>
{/if}
</div>
<!-- [/DEF:DashboardSelectionSection:Component] -->
@@ -329,15 +329,15 @@
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
/>
<label for="replace-db" class="ml-2 block text-sm text-gray-900">
Replace Database (Apply Mappings)
{$t.migration?.replace_db || "Replace Database (Apply Mappings)"}
</label>
</div>
{#if replaceDb}
<div class="mb-8 p-4 border rounded-md bg-gray-50">
<h3 class="text-md font-medium mb-4">Database Mappings</h3>
<h3 class="text-md font-medium mb-4">{$t.migration?.database_mappings || "Database Mappings"}</h3>
{#if fetchingDbs}
<p>Loading databases and suggestions...</p>
<p>{$t.migration?.loading_dbs || "Loading databases and suggestions..."}</p>
{:else if sourceDatabases.length > 0}
<MappingTable
{sourceDatabases}
@@ -351,7 +351,7 @@
on:click={fetchDatabases}
class="text-indigo-600 hover:text-indigo-500 text-sm font-medium"
>
Refresh Databases & Suggestions
{$t.migration?.refresh_dbs || "Refresh Databases & Suggestions"}
</button>
{/if}
</div>
@@ -361,7 +361,7 @@
on:click={startMigration}
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || selectedDashboardIds.length === 0}
>
Start Migration
{$t.migration?.start || "Start Migration"}
</Button>
{/if}
</div>

View File

@@ -106,7 +106,7 @@
});
mappings = [...mappings.filter(m => m.source_db_uuid !== sourceUuid), savedMapping];
success = "Mapping saved successfully";
success = $t.migration?.mapping_saved || "Mapping saved successfully";
} catch (e) {
error = e.message;
}
@@ -116,20 +116,20 @@
<!-- [SECTION: TEMPLATE] -->
<div class="max-w-6xl mx-auto p-6">
<PageHeader title="Database Mapping Management" />
<PageHeader title={$t.migration?.mapping_management || "Database Mapping Management"} />
{#if loading}
<p>Loading environments...</p>
<p>{$t.migration?.loading_envs || "Loading environments..."}</p>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<EnvSelector
label="Source Environment"
label={$t.migration?.source_env || "Source Environment"}
bind:selectedId={sourceEnvId}
{environments}
on:change={() => { sourceDatabases = []; mappings = []; suggestions = []; }}
/>
<EnvSelector
label="Target Environment"
label={$t.migration?.target_env || "Target Environment"}
bind:selectedId={targetEnvId}
{environments}
on:change={() => { targetDatabases = []; mappings = []; suggestions = []; }}
@@ -142,7 +142,7 @@
disabled={!sourceEnvId || !targetEnvId || sourceEnvId === targetEnvId || fetchingDbs}
isLoading={fetchingDbs}
>
Fetch Databases & Suggestions
{$t.migration?.fetch_dbs || "Fetch Databases & Suggestions"}
</Button>
</div>
@@ -167,7 +167,7 @@
on:update={handleUpdate}
/>
{:else if !fetchingDbs && sourceEnvId && targetEnvId}
<p class="text-gray-500 italic">Select environments and click "Fetch Databases" to start mapping.</p>
<p class="text-gray-500 italic">{$t.migration?.mapping_hint || 'Select environments and click "Fetch Databases" to start mapping.'}</p>
{/if}
{/if}
</div>

View File

@@ -28,6 +28,11 @@
git_commit_prompt:
"Generate a concise and professional git commit message based on the following diff and recent history.\\nUse Conventional Commits format (e.g., feat: ..., fix: ..., docs: ...).\\n\\nRecent History:\\n{history}\\n\\nDiff:\\n{diff}\\n\\nCommit Message:",
};
const DEFAULT_LLM_PROVIDER_BINDINGS = {
dashboard_validation: "",
documentation: "",
git_commit: "",
};
// State
let activeTab = "environments";
@@ -77,15 +82,50 @@
providers: [],
default_provider: "",
prompts: { ...DEFAULT_LLM_PROMPTS },
provider_bindings: { ...DEFAULT_LLM_PROVIDER_BINDINGS },
assistant_planner_provider: "",
assistant_planner_model: "",
...(llm || {}),
};
normalized.prompts = {
...DEFAULT_LLM_PROMPTS,
...(llm?.prompts || {}),
};
normalized.provider_bindings = {
...DEFAULT_LLM_PROVIDER_BINDINGS,
...(llm?.provider_bindings || {}),
};
normalized.assistant_planner_provider = llm?.assistant_planner_provider || "";
normalized.assistant_planner_model = llm?.assistant_planner_model || "";
return normalized;
}
function isMultimodalModel(modelName) {
const token = (modelName || "").toLowerCase();
if (!token) return false;
return (
token.includes("gpt-4o") ||
token.includes("gpt-4.1") ||
token.includes("vision") ||
token.includes("vl") ||
token.includes("gemini") ||
token.includes("claude-3") ||
token.includes("claude-sonnet-4")
);
}
function getProviderById(providerId) {
if (!providerId) return null;
return (settings?.llm_providers || []).find((p) => p.id === providerId) || null;
}
function isDashboardValidationBindingValid() {
const providerId = settings?.llm?.provider_bindings?.dashboard_validation;
if (!providerId) return true;
const provider = getProviderById(providerId);
return provider ? isMultimodalModel(provider.default_model) : true;
}
// Handle tab change
function handleTabChange(tab) {
activeTab = tab;
@@ -670,6 +710,121 @@
onSave={loadSettings}
/>
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
<h3 class="text-base font-semibold text-gray-900">
{$t.settings?.llm_chatbot_settings_title || "Chatbot Planner Settings"}
</h3>
<p class="mt-1 text-sm text-gray-600">
{$t.settings?.llm_chatbot_settings_description ||
"Select provider and optional model override for assistant intent planning."}
</p>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label for="planner-provider" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_chatbot_provider || "Chatbot Provider"}
</label>
<select
id="planner-provider"
bind:value={settings.llm.assistant_planner_provider}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
{#each settings.llm_providers || [] as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
</option>
{/each}
</select>
</div>
<div>
<label for="planner-model" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_chatbot_model || "Chatbot Model Override"}
</label>
<input
id="planner-model"
type="text"
bind:value={settings.llm.assistant_planner_model}
placeholder={$t.settings?.llm_chatbot_model_placeholder || "Optional, e.g. gpt-4.1-mini"}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
/>
</div>
</div>
</div>
<div class="mt-6 rounded-lg border border-gray-200 bg-white p-4">
<h3 class="text-base font-semibold text-gray-900">
{$t.settings?.llm_provider_bindings_title || "Provider Bindings by Task"}
</h3>
<p class="mt-1 text-sm text-gray-600">
{$t.settings?.llm_provider_bindings_description ||
"Select which provider is used by default for each LLM task."}
</p>
<div class="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label for="binding-dashboard-validation" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_binding_dashboard_validation || "Dashboard Validation Provider"}
</label>
<select
id="binding-dashboard-validation"
bind:value={settings.llm.provider_bindings.dashboard_validation}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
{#each settings.llm_providers || [] as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
</option>
{/each}
</select>
{#if !isDashboardValidationBindingValid()}
<p class="mt-1 text-xs text-amber-700">
{$t.settings?.llm_multimodal_warning ||
"Dashboard validation requires a multimodal model (image input)."}
</p>
{/if}
</div>
<div>
<label for="binding-documentation" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_binding_documentation || "Documentation Provider"}
</label>
<select
id="binding-documentation"
bind:value={settings.llm.provider_bindings.documentation}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
{#each settings.llm_providers || [] as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
</option>
{/each}
</select>
</div>
<div class="md:col-span-2">
<label for="binding-git-commit" class="block text-sm font-medium text-gray-700">
{$t.settings?.llm_binding_git_commit || "Git Commit Provider"}
</label>
<select
id="binding-git-commit"
bind:value={settings.llm.provider_bindings.git_commit}
class="mt-1 block w-full rounded-md border border-gray-300 p-2 text-sm"
>
<option value="">{$t.dashboard?.use_default || "Use Default"}</option>
{#each settings.llm_providers || [] as provider}
<option value={provider.id}>
{provider.name} ({provider.default_model})
</option>
{/each}
</select>
</div>
</div>
</div>
<div class="mt-6 rounded-lg border border-gray-200 bg-gray-50 p-4">
<h3 class="text-base font-semibold text-gray-900">
{$t.settings?.llm_prompts_title || "LLM Prompt Templates"}