feat(assistant): add multi-dialog UX, task-aware llm settings, and i18n cleanup
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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] -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user