Как на maintenance с шаблоном баннера — добавил чипсы-шаблоны SQL-запросов над textarea в модалке маппинга колонок: - information_schema — вставляет запрос к information_schema с JOIN pg_description для получения описаний колонок - Custom table — вставляет запрос к кастомной таблице маппинга - Clear — очищает поле Чипсы в стиле rounded-full с hover-эффектами, подпись "Шаблоны:" и title-tooltip на каждой кнопке. i18n: 6 новых ключей (sql_templates, sql_template_*).
467 lines
20 KiB
Svelte
467 lines
20 KiB
Svelte
<!-- #region DatasetHub [C:5] [TYPE Page] [SEMANTICS sveltekit,dataset,workspace] -->
|
|
<script>
|
|
import { onMount, onDestroy } from "svelte";
|
|
import { SvelteSet } from "svelte/reactivity";
|
|
import { fromStore } from "svelte/store";
|
|
import { t } from "$lib/i18n";
|
|
import { api } from "$lib/api.js";
|
|
import HelpTooltip from "$lib/ui/HelpTooltip.svelte";
|
|
import { debounce } from "$lib/utils/debounce.js";
|
|
import {
|
|
environmentContextStore,
|
|
initializeEnvironmentContext,
|
|
setSelectedEnvironment,
|
|
} from "$lib/stores/environmentContext.js";
|
|
import {
|
|
openDrawerForTaskIfPreferred,
|
|
} from "$lib/stores/taskDrawer.js";
|
|
import StatsBar from "./StatsBar.svelte";
|
|
import DatasetList from "./DatasetList.svelte";
|
|
import DatasetPreview from "./DatasetPreview.svelte";
|
|
|
|
const envCtx = fromStore(environmentContextStore);
|
|
|
|
let selectedEnv = $state(null);
|
|
let datasets = $state([]);
|
|
let dsError = $state(null), dsLoading = $state(true);
|
|
let stats = $state(null);
|
|
let activeFilter = $state(null);
|
|
let page = $state(1), pageSize = $state(10), total = $state(0), totalPages = $state(1);
|
|
let searchQuery = $state("");
|
|
let selectedIds = $state(new SvelteSet());
|
|
let selectedDatasetId = $state(null), detailData = $state(null), detailLoading = $state(false), detailError = $state(null);
|
|
let showMapColumnsModal = $state(false), showGenerateDocsModal = $state(false);
|
|
let mapSourceType = $state("sqllab"), mapDatabases = $state([]), mapDatabaseId = $state(null), mapSqlQuery = $state(""), mapFileData = $state(null);
|
|
let llmProvider = $state(""), llmOptions = $state({});
|
|
let isMobile = $state(false), mobileSlideOpen = $state(false);
|
|
let ws = null;
|
|
|
|
// Debounced search
|
|
const debouncedSearch = debounce((q) => { searchQuery = q; page = 1; loadDatasets(); }, 300);
|
|
|
|
async function loadDatasets(filter, p) {
|
|
if (!selectedEnv) return;
|
|
dsLoading = true;
|
|
dsError = null;
|
|
try {
|
|
const resp = await api.getDatasets(selectedEnv, {
|
|
search: searchQuery || undefined,
|
|
filter: filter ?? activeFilter ?? undefined,
|
|
page: p ?? page,
|
|
page_size: pageSize,
|
|
});
|
|
datasets = resp.datasets.map(d => ({
|
|
id: d.id,
|
|
table_name: d.table_name,
|
|
schema: d.schema,
|
|
database: d.database,
|
|
column_count: d.mapped_fields?.total ?? 0,
|
|
metric_count: d.metric_count ?? 0,
|
|
mappedFields: d.mapped_fields ? { total: d.mapped_fields.total, mapped: d.mapped_fields.mapped } : null,
|
|
lastTask: d.last_task ? { status: d.last_task.status, id: d.last_task.task_id } : null,
|
|
}));
|
|
stats = resp.stats ?? null;
|
|
total = resp.total ?? 0;
|
|
totalPages = resp.total_pages ?? 1;
|
|
page = resp.page ?? 1;
|
|
// Clean selected IDs
|
|
const keepIds = new Set();
|
|
datasets.forEach(d => { if (selectedIds.has(d.id)) keepIds.add(d.id); });
|
|
selectedIds = keepIds;
|
|
} catch (e) {
|
|
dsError = e.message || $t.datasets?.load_failed;
|
|
} finally {
|
|
dsLoading = false;
|
|
}
|
|
}
|
|
|
|
async function loadDetail(datasetId) {
|
|
if (!datasetId) return;
|
|
detailLoading = true;
|
|
detailError = null;
|
|
try {
|
|
const d = await api.getDatasetDetail(selectedEnv, datasetId);
|
|
detailData = { ...d, schema: d.schema_name ?? d.schema };
|
|
} catch (e) {
|
|
detailError = e.message || $t.datasets?.load_detail_failed;
|
|
} finally {
|
|
detailLoading = false;
|
|
}
|
|
}
|
|
|
|
function selectDataset(id) {
|
|
selectedDatasetId = id;
|
|
detailData = null;
|
|
detailError = null;
|
|
if (isMobile) mobileSlideOpen = true;
|
|
loadDetail(id);
|
|
}
|
|
|
|
function closeDetail() {
|
|
selectedDatasetId = null;
|
|
detailData = null;
|
|
detailError = null;
|
|
mobileSlideOpen = false;
|
|
}
|
|
|
|
function handleFilter(filterKey) {
|
|
activeFilter = filterKey;
|
|
page = 1;
|
|
loadDatasets(filterKey, 1);
|
|
}
|
|
|
|
function handlePageChange(p) {
|
|
page = p;
|
|
loadDatasets(undefined, p);
|
|
}
|
|
|
|
function handleAction(dataset, action) {
|
|
if (action === "map_columns") {
|
|
selectedIds = new SvelteSet([dataset.id]);
|
|
showMapColumnsModal = true;
|
|
mapSourceType = "sqllab";
|
|
mapDatabaseId = null;
|
|
mapSqlQuery = "";
|
|
mapFileData = null;
|
|
// Fetch databases on open
|
|
if (selectedEnv) {
|
|
api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {});
|
|
}
|
|
} else if (action === "generate_docs") {
|
|
selectedIds = new SvelteSet([dataset.id]);
|
|
showGenerateDocsModal = true;
|
|
llmProvider = "";
|
|
llmOptions = {};
|
|
}
|
|
}
|
|
|
|
function handleCheckbox(dataset, checked) {
|
|
if (checked === "all") {
|
|
datasets.forEach(d => selectedIds.add(d.id));
|
|
selectedIds = new Set(selectedIds);
|
|
} else if (checked === "clear-all") {
|
|
selectedIds = new SvelteSet();
|
|
} else if (checked) {
|
|
selectedIds.add(dataset.id);
|
|
selectedIds = new Set(selectedIds);
|
|
} else {
|
|
selectedIds.delete(dataset.id);
|
|
selectedIds = new Set(selectedIds);
|
|
}
|
|
}
|
|
|
|
// Bulk actions
|
|
async function handleBulkMapColumns() {
|
|
if (selectedIds.size === 0) return;
|
|
if (mapSourceType === "sqllab" && !mapDatabaseId) return;
|
|
if (mapSourceType === "xlsx" && (!mapFileData || mapFileData.length === 0)) return;
|
|
try {
|
|
let fileData = null;
|
|
if (mapSourceType === "xlsx" && mapFileData?.length) fileData = mapFileData[0].name;
|
|
const resp = await api.postApi("/datasets/map-columns", {
|
|
env_id: selectedEnv, dataset_ids: [...selectedIds],
|
|
source_type: mapSourceType, database_id: mapDatabaseId || undefined, sql_query: mapSqlQuery || undefined, file_data: fileData || undefined,
|
|
});
|
|
showMapColumnsModal = false;
|
|
selectedIds = new SvelteSet();
|
|
if (resp.task_id) openDrawerForTaskIfPreferred(resp.task_id);
|
|
} catch (e) {
|
|
alert($t.datasets?.mapping_task_failed);
|
|
}
|
|
}
|
|
|
|
async function handleBulkGenerateDocs() {
|
|
if (selectedIds.size === 0) return;
|
|
if (!llmProvider) { alert($t.datasets?.select_llm_provider); return; }
|
|
try {
|
|
const resp = await api.postApi("/datasets/generate-docs", {
|
|
env_id: selectedEnv, dataset_ids: [...selectedIds], llm_provider: llmProvider, options: llmOptions,
|
|
});
|
|
showGenerateDocsModal = false;
|
|
selectedIds = new SvelteSet();
|
|
if (resp.task_id) openDrawerForTaskIfPreferred(resp.task_id);
|
|
} catch (e) {
|
|
alert($t.datasets?.docs_task_failed);
|
|
}
|
|
}
|
|
|
|
// WebSocket
|
|
function connectWs() {
|
|
if (!selectedEnv) return;
|
|
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
const baseUrl = `${proto}//${window.location.host}`;
|
|
ws = new WebSocket(`${baseUrl}/ws/datasets/${selectedEnv}`);
|
|
ws.onmessage = (e) => {
|
|
try {
|
|
const msg = JSON.parse(e.data);
|
|
if (msg.type === "dataset.updated") {
|
|
loadDatasets();
|
|
// Refresh detail too if it's one of the updated datasets
|
|
if (selectedDatasetId && msg.payload?.dataset_ids?.includes(selectedDatasetId)) {
|
|
loadDetail(selectedDatasetId);
|
|
}
|
|
}
|
|
} catch {}
|
|
};
|
|
ws.onerror = () => {};
|
|
}
|
|
|
|
function disconnectWs() {
|
|
if (ws) { ws.close(); ws = null; }
|
|
}
|
|
|
|
// Mobile check
|
|
function checkMobile() { isMobile = window.innerWidth < 768; }
|
|
let resizeHandler = null;
|
|
|
|
onMount(async () => {
|
|
await initializeEnvironmentContext();
|
|
selectedEnv = envCtx.current?.selectedEnvId;
|
|
if (selectedEnv) {
|
|
setSelectedEnvironment(selectedEnv);
|
|
await loadDatasets();
|
|
connectWs();
|
|
}
|
|
checkMobile();
|
|
resizeHandler = () => { checkMobile(); };
|
|
window.addEventListener("resize", resizeHandler);
|
|
document.addEventListener("keydown", handleEscape);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
disconnectWs();
|
|
if (resizeHandler) window.removeEventListener("resize", resizeHandler);
|
|
document.removeEventListener("keydown", handleEscape);
|
|
});
|
|
|
|
function handleEscape(e) { if (e.key === "Escape" && mobileSlideOpen) closeDetail(); }
|
|
|
|
// Environment change
|
|
$effect(() => {
|
|
const next = envCtx.current?.selectedEnvId;
|
|
if (next && next !== selectedEnv) {
|
|
selectedEnv = next;
|
|
setSelectedEnvironment(next);
|
|
page = 1;
|
|
selectedIds = new SvelteSet();
|
|
closeDetail();
|
|
disconnectWs();
|
|
loadDatasets();
|
|
connectWs();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div class="max-w-screen-2xl mx-auto px-4 pt-4 pb-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h1 class="text-2xl font-bold text-gray-900">{$t.nav?.datasets}</h1>
|
|
<div class="flex items-center gap-3">
|
|
<span class="px-2 py-1 text-xs rounded border border-gray-300 bg-gray-50 text-gray-600">
|
|
{$t.datasets?.all}: {stats?.total ?? 0}
|
|
</span>
|
|
<button class="px-3 py-1 text-sm bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.refresh_tip} onclick={() => loadDatasets()}>
|
|
{$t.common?.refresh ?? '⟳'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Banner for list -->
|
|
{#if dsError}
|
|
<div class="bg-red-50 border border-red-300 text-red-700 px-3 py-2 rounded mb-4 text-sm flex items-center justify-between" role="alert">
|
|
<span>{dsError}</span>
|
|
<button class="px-2 py-0.5 bg-red-600 text-white rounded text-xs" onclick={() => loadDatasets()}>{$t.common?.retry ?? 'Retry'}</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Stats Bar -->
|
|
<StatsBar {stats} activeFilter={activeFilter} isLoading={dsLoading} onfilter={handleFilter} />
|
|
|
|
<!-- Split View -->
|
|
<div class="grid gap-4 {isMobile ? 'block' : 'grid-cols-[40%_60%]'}" style="min-height: 60vh">
|
|
<!-- Left Panel: Dataset List -->
|
|
<div class={isMobile && mobileSlideOpen ? 'hidden' : ''}>
|
|
<DatasetList
|
|
{datasets}
|
|
{selectedIds}
|
|
isLoading={dsLoading}
|
|
error={null}
|
|
{total}
|
|
{page}
|
|
{totalPages}
|
|
{pageSize}
|
|
{searchQuery}
|
|
onselect={selectDataset}
|
|
onaction={handleAction}
|
|
onpagechange={handlePageChange}
|
|
onsearch={debouncedSearch}
|
|
oncheckbox={handleCheckbox}
|
|
/>
|
|
</div>
|
|
|
|
<!-- Right Panel (Desktop inline / Mobile slide-over) -->
|
|
{#if isMobile && mobileSlideOpen}
|
|
<!-- Backdrop -->
|
|
<button
|
|
class="fixed inset-0 bg-black/30 z-40"
|
|
onclick={closeDetail}
|
|
aria-label="Close detail"
|
|
></button>
|
|
<!-- Slide-over -->
|
|
<div
|
|
class="fixed inset-y-0 right-0 w-[90vw] max-w-lg bg-white shadow-xl z-50 overflow-y-auto"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
>
|
|
<div class="sticky top-0 bg-white border-b border-gray-200 px-4 py-2 flex items-center justify-between z-10">
|
|
<span class="font-medium text-sm">{detailData?.table_name ?? $t.datasets?.no_selection}</span>
|
|
<button class="text-gray-400 hover:text-gray-700 text-lg" onclick={closeDetail} aria-label="Close">×</button>
|
|
</div>
|
|
<DatasetPreview dataset={detailData} isLoading={detailLoading} error={detailError} onretry={() => detailData && loadDetail(detailData.id)} envId={selectedEnv} />
|
|
</div>
|
|
{:else if !isMobile}
|
|
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
<DatasetPreview dataset={detailData} isLoading={detailLoading} error={detailError} onretry={() => selectedDatasetId && loadDetail(selectedDatasetId)} envId={selectedEnv} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Bulk Action Panel (bottom) -->
|
|
{#if selectedIds.size >= 2}
|
|
<div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg p-3 z-50">
|
|
<div class="max-w-screen-2xl mx-auto flex items-center justify-between">
|
|
<span class="font-medium text-sm">✓ {selectedIds.size} {$t.datasets?.selected}</span>
|
|
<div class="flex gap-2">
|
|
<button class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.bulk_map_tip} onclick={() => { showMapColumnsModal = true; mapSourceType = "sqllab"; mapDatabaseId = null; mapSqlQuery = ""; mapFileData = null; if (selectedEnv) api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {}); }}>
|
|
🗺 {$t.datasets?.action_map_columns}
|
|
</button>
|
|
<button class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.bulk_docs_tip} onclick={() => { showGenerateDocsModal = true; }}>
|
|
📄 {$t.datasets?.generate_docs}
|
|
</button>
|
|
<button class="px-3 py-1.5 text-sm border border-gray-300 rounded hover:bg-gray-100" title={$t.datasets?.clear_selection_tip} onclick={() => selectedIds = new SvelteSet()}>
|
|
✕ {$t.common?.cancel ?? 'Clear'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Map Columns Modal -->
|
|
{#if showMapColumnsModal}
|
|
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" role="dialog" aria-modal="true" onclick={(e) => { if (e.target === e.currentTarget) showMapColumnsModal = false; }}>
|
|
<div class="bg-white rounded-lg shadow-xl p-6 max-w-md w-full" style="max-height: 90vh; overflow-y: auto;" onclick={(e) => e.stopPropagation()}>
|
|
<h3 class="text-lg font-semibold mb-4">{$t.datasets?.bulk_map_columns}</h3>
|
|
<p class="text-sm text-gray-600 mb-3">{$t.datasets?.selected_count?.replace('{count}', String(selectedIds.size))}</p>
|
|
|
|
<span class="flex items-center gap-1.5 mb-1.5">
|
|
<span class="text-sm font-medium text-gray-700">{$t.datasets?.source_type ?? 'Тип источника'}</span>
|
|
<HelpTooltip text={$t.datasets?.source_type_tip || ""} />
|
|
</span>
|
|
<select
|
|
class="w-full border rounded px-2 py-1.5 mb-3"
|
|
title={$t.datasets?.source_type_tip}
|
|
bind:value={mapSourceType}
|
|
onchange={() => { if (mapSourceType === 'sqllab' && mapDatabases.length === 0 && selectedEnv) { api.getEnvironmentDatabases(selectedEnv).then(dbs => mapDatabases = dbs ?? []).catch(() => {}); } }}
|
|
>
|
|
<option value="sqllab">{$t.datasets?.source_sqllab ?? 'Superset SQL Lab'}</option>
|
|
<option value="xlsx">{$t.datasets?.source_xlsx}</option>
|
|
</select>
|
|
{#if mapSourceType === "sqllab"}
|
|
<span class="flex items-center gap-1.5 mb-1.5">
|
|
<label class="text-sm font-medium text-gray-700" for="map-database-select">{$t.datasets?.source_database ?? 'База данных'}</label>
|
|
<HelpTooltip text={$t.datasets?.database_tip || ""} />
|
|
</span>
|
|
<select
|
|
id="map-database-select"
|
|
class="w-full border rounded px-2 py-1.5 mb-3"
|
|
title={$t.datasets?.database_tip}
|
|
bind:value={mapDatabaseId}
|
|
>
|
|
<option value="">-- {$t.datasets?.select_database ?? 'выберите базу данных'} --</option>
|
|
{#each mapDatabases as db (db.id)}
|
|
<option value={db.id}>{db.database_name || db.name}</option>
|
|
{/each}
|
|
</select>
|
|
<span class="flex items-center gap-1.5 mb-1.5">
|
|
<label class="text-sm font-medium text-gray-700" for="map-sql-query">
|
|
{$t.datasets?.sql_query ?? 'SQL-запрос'}
|
|
<span class="text-xs text-gray-400 font-normal ml-1">({$t.datasets?.sql_query_hint ?? 'оставьте пустым для information_schema'})</span>
|
|
</label>
|
|
<HelpTooltip text={$t.datasets?.sql_query_tip || ""} />
|
|
</span>
|
|
<!-- SQL template chips -->
|
|
<div class="flex flex-wrap gap-1.5 mb-2">
|
|
<span class="text-xs text-gray-400 mr-0.5 self-center">{$t.datasets?.sql_templates ?? 'Шаблоны'}:</span>
|
|
<button
|
|
type="button"
|
|
class="px-2 py-0.5 text-xs border border-gray-300 rounded-full text-gray-500 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-600 transition-colors"
|
|
title={$t.datasets?.sql_template_info_schema_tip}
|
|
onclick={() => { mapSqlQuery = "SELECT c.column_name, c.data_type, pgd.description\nFROM information_schema.columns c\nLEFT JOIN pg_catalog.pg_description pgd ON pgd.objsubid = c.ordinal_position\n AND pgd.objoid = (SELECT attrelid FROM pg_attribute WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = c.table_name) AND attname = c.column_name)\nWHERE c.table_schema = 'public'\n AND c.table_name = 'your_table_name'\nORDER BY c.ordinal_position;" }}
|
|
>information_schema</button>
|
|
<button
|
|
type="button"
|
|
class="px-2 py-0.5 text-xs border border-gray-300 rounded-full text-gray-500 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-600 transition-colors"
|
|
title={$t.datasets?.sql_template_custom_tip}
|
|
onclick={() => { mapSqlQuery = "SELECT column_name, description AS verbose_name\nFROM my_column_mappings\nWHERE table_name = 'your_table_name'\nORDER BY column_name;" }}
|
|
>{$t.datasets?.sql_template_custom ?? 'Кастомная таблица'}</button>
|
|
<button
|
|
type="button"
|
|
class="px-2 py-0.5 text-xs border border-gray-300 rounded-full text-gray-400 hover:border-red-300 hover:text-red-500 transition-colors"
|
|
onclick={() => { mapSqlQuery = ""; }}
|
|
>{$t.datasets?.sql_template_clear ?? 'Очистить'}</button>
|
|
</div>
|
|
<textarea
|
|
id="map-sql-query"
|
|
class="w-full border rounded px-2 py-1.5 mb-4 font-mono text-xs"
|
|
rows="4"
|
|
title={$t.datasets?.sql_query_tip}
|
|
placeholder="SELECT column_name, description AS verbose_name FROM my_mappings WHERE table_name = 'my_table';"
|
|
bind:value={mapSqlQuery}
|
|
></textarea>
|
|
{:else}
|
|
<span class="flex items-center gap-1.5 mb-1.5">
|
|
<label class="text-sm font-medium text-gray-700" for="map-file-input">{$t.datasets?.xlsx_file ?? 'XLSX-файл'}</label>
|
|
<HelpTooltip text={$t.datasets?.file_input_tip || ""} />
|
|
</span>
|
|
<input
|
|
id="map-file-input"
|
|
type="file"
|
|
accept=".xlsx"
|
|
class="w-full mb-4"
|
|
title={$t.datasets?.file_input_tip}
|
|
onchange={(e) => mapFileData = e.target.files}
|
|
/>
|
|
{/if}
|
|
<div class="flex justify-end gap-2">
|
|
<button class="px-3 py-1.5 border rounded hover:bg-gray-100" title={$t.datasets?.cancel_tip} onclick={() => showMapColumnsModal = false}>{$t.common?.cancel}</button>
|
|
<button class="px-3 py-1.5 bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.start_mapping_tip} onclick={handleBulkMapColumns}>{$t.datasets?.start_mapping}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Generate Docs Modal -->
|
|
{#if showGenerateDocsModal}
|
|
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" role="dialog" aria-modal="true" onclick={(e) => { if (e.target === e.currentTarget) showGenerateDocsModal = false; }}>
|
|
<div class="bg-white rounded-lg shadow-xl p-6 max-w-md w-full" onclick={(e) => e.stopPropagation()}>
|
|
<h3 class="text-lg font-semibold mb-4">{$t.datasets?.bulk_docs_generation}</h3>
|
|
<p class="text-sm text-gray-600 mb-3">{$t.datasets?.selected_count?.replace('{count}', String(selectedIds.size))}</p>
|
|
<span class="flex items-center gap-1.5 mb-1.5">
|
|
<span class="text-sm font-medium text-gray-700">{$t.datasets?.select_llm_provider ?? 'LLM-провайдер'}</span>
|
|
<HelpTooltip text={$t.datasets?.llm_provider_tip || ""} />
|
|
</span>
|
|
<select class="w-full border rounded px-2 py-1.5 mb-4" title={$t.datasets?.llm_provider_tip} bind:value={llmProvider}>
|
|
<option value="">{$t.datasets?.select_llm_provider_option}</option>
|
|
<option value="openai">OpenAI</option>
|
|
<option value="anthropic">Anthropic</option>
|
|
<option value="local">Local LLM</option>
|
|
</select>
|
|
<div class="flex justify-end gap-2">
|
|
<button class="px-3 py-1.5 border rounded hover:bg-gray-100" title={$t.datasets?.cancel_tip} onclick={() => showGenerateDocsModal = false}>{$t.common?.cancel}</button>
|
|
<button class="px-3 py-1.5 bg-primary text-white rounded hover:bg-primary-hover" title={$t.datasets?.start_docs_tip} onclick={handleBulkGenerateDocs}>{$t.datasets?.generate_docs}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<!-- #endregion DatasetHub --> |