chat worked
This commit is contained in:
@@ -26,18 +26,18 @@
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
let filterText = "";
|
||||
let currentPage = 0;
|
||||
let pageSize = 20;
|
||||
let sortColumn: keyof DashboardMetadata = "title";
|
||||
let sortDirection: "asc" | "desc" = "asc";
|
||||
let filterText = $state("");
|
||||
let currentPage = $state(0);
|
||||
let pageSize = $state(20);
|
||||
let sortColumn: keyof DashboardMetadata = $state("title");
|
||||
let sortDirection: "asc" | "desc" = $state("asc");
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: UI STATE]
|
||||
let showGitManager = false;
|
||||
let gitDashboardId: number | null = null;
|
||||
let gitDashboardTitle = "";
|
||||
let validatingIds: Set<number> = new Set();
|
||||
let showGitManager = $state(false);
|
||||
let gitDashboardId: number | null = $state(null);
|
||||
let gitDashboardTitle = $state("");
|
||||
let validatingIds: Set<number> = $state(new Set());
|
||||
// [/SECTION]
|
||||
|
||||
// [DEF:handleValidate:Function]
|
||||
@@ -48,7 +48,7 @@
|
||||
if (validatingIds.has(dashboard.id)) return;
|
||||
|
||||
validatingIds.add(dashboard.id);
|
||||
validatingIds = validatingIds; // Trigger reactivity
|
||||
validatingIds = new Set(validatingIds);
|
||||
|
||||
try {
|
||||
// TODO: Get provider_id from settings or prompt user
|
||||
@@ -83,7 +83,7 @@
|
||||
toast(e.message || "Validation failed to start", "error");
|
||||
} finally {
|
||||
validatingIds.delete(dashboard.id);
|
||||
validatingIds = validatingIds;
|
||||
validatingIds = new Set(validatingIds);
|
||||
}
|
||||
}
|
||||
// [/DEF:handleValidate:Function]
|
||||
@@ -221,14 +221,14 @@
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
indeterminate={someSelected && !allSelected}
|
||||
on:change={(e) =>
|
||||
onchange={(e) =>
|
||||
handleSelectAll((e.target as HTMLInputElement).checked)}
|
||||
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors"
|
||||
on:click={() => handleSort("title")}
|
||||
onclick={() => handleSort("title")}
|
||||
>
|
||||
{$t.dashboard.title}
|
||||
{sortColumn === "title"
|
||||
@@ -239,7 +239,7 @@
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors"
|
||||
on:click={() => handleSort("last_modified")}
|
||||
onclick={() => handleSort("last_modified")}
|
||||
>
|
||||
{$t.dashboard.last_modified}
|
||||
{sortColumn === "last_modified"
|
||||
@@ -250,7 +250,7 @@
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:text-gray-700 transition-colors"
|
||||
on:click={() => handleSort("status")}
|
||||
onclick={() => handleSort("status")}
|
||||
>
|
||||
{$t.dashboard.status}
|
||||
{sortColumn === "status"
|
||||
@@ -276,7 +276,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(dashboard.id)}
|
||||
on:change={(e) =>
|
||||
onchange={(e) =>
|
||||
handleSelectionChange(
|
||||
dashboard.id,
|
||||
(e.target as HTMLInputElement).checked,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
// [/SECTION]
|
||||
|
||||
let selectedTargetUuid = "";
|
||||
let selectedTargetUuid = $state("");
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// [DEF:resolve:Function]
|
||||
@@ -94,7 +94,7 @@
|
||||
<div class="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
|
||||
<button
|
||||
type="button"
|
||||
on:click={resolve}
|
||||
onclick={resolve}
|
||||
disabled={!selectedTargetUuid}
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:col-start-2 sm:text-sm disabled:bg-gray-400"
|
||||
>
|
||||
@@ -102,7 +102,7 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={cancel}
|
||||
onclick={cancel}
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:col-start-1 sm:text-sm"
|
||||
>
|
||||
Cancel Migration
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
let branches = [];
|
||||
let loading = false;
|
||||
let showCreate = false;
|
||||
let newBranchName = '';
|
||||
let branches = $state([]);
|
||||
let loading = $state(false);
|
||||
let showCreate = $state(false);
|
||||
let newBranchName = $state('');
|
||||
// [/SECTION]
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -129,7 +129,7 @@
|
||||
<div class="flex-grow">
|
||||
<Select
|
||||
bind:value={currentBranch}
|
||||
on:change={handleSelect}
|
||||
onchange={handleSelect}
|
||||
disabled={loading}
|
||||
options={branches.map(b => ({ value: b.name, label: b.name }))}
|
||||
/>
|
||||
@@ -138,7 +138,7 @@
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
on:click={() => showCreate = !showCreate}
|
||||
onclick={() => showCreate = !showCreate}
|
||||
disabled={loading}
|
||||
class="text-blue-600"
|
||||
>
|
||||
@@ -158,7 +158,7 @@
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
on:click={handleCreate}
|
||||
onclick={handleCreate}
|
||||
disabled={loading || !newBranchName}
|
||||
isLoading={loading}
|
||||
class="bg-green-600 hover:bg-green-700"
|
||||
@@ -168,7 +168,7 @@
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
on:click={() => showCreate = false}
|
||||
onclick={() => showCreate = false}
|
||||
disabled={loading}
|
||||
>
|
||||
{$t.common.cancel}
|
||||
@@ -178,4 +178,4 @@
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<!-- [/DEF:BranchSelector:Component] -->
|
||||
<!-- [/DEF:BranchSelector:Component] -->
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
let history = [];
|
||||
let loading = false;
|
||||
let history = $state([]);
|
||||
let loading = $state(false);
|
||||
// [/SECTION]
|
||||
|
||||
// [DEF:onMount:Function]
|
||||
@@ -66,7 +66,7 @@
|
||||
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider">
|
||||
{$t.git.history}
|
||||
</h3>
|
||||
<Button variant="ghost" size="sm" on:click={loadHistory} class="text-blue-600">
|
||||
<Button variant="ghost" size="sm" onclick={loadHistory} class="text-blue-600">
|
||||
{$t.git.refresh}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -95,4 +95,4 @@
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<!-- [/DEF:CommitHistory:Component] -->
|
||||
<!-- [/DEF:CommitHistory:Component] -->
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
let message = "";
|
||||
let committing = false;
|
||||
let status = null;
|
||||
let diff = "";
|
||||
let loading = false;
|
||||
let generatingMessage = false;
|
||||
let message = $state("");
|
||||
let committing = $state(false);
|
||||
let status = $state(null);
|
||||
let diff = $state("");
|
||||
let loading = $state(false);
|
||||
let generatingMessage = $state(false);
|
||||
// [/SECTION]
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -153,7 +153,7 @@
|
||||
>Commit Message</label
|
||||
>
|
||||
<button
|
||||
on:click={handleGenerateMessage}
|
||||
onclick={handleGenerateMessage}
|
||||
disabled={generatingMessage || loading}
|
||||
class="text-xs text-blue-600 hover:text-blue-800 disabled:opacity-50 flex items-center"
|
||||
>
|
||||
@@ -243,13 +243,13 @@
|
||||
|
||||
<div class="flex justify-end space-x-3 mt-6 pt-4 border-t">
|
||||
<button
|
||||
on:click={() => (show = false)}
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
on:click={handleCommit}
|
||||
onclick={handleCommit}
|
||||
disabled={committing ||
|
||||
!message ||
|
||||
loading ||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
// [SECTION: STATE]
|
||||
const dispatch = createEventDispatcher();
|
||||
/** @type {Object.<string, 'mine' | 'theirs' | 'manual'>} */
|
||||
let resolutions = {};
|
||||
let resolutions = $state({});
|
||||
// [/SECTION]
|
||||
|
||||
// [DEF:resolve:Function]
|
||||
@@ -126,7 +126,7 @@
|
||||
] === 'mine'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-50 hover:bg-blue-50 text-blue-600'}"
|
||||
on:click={() =>
|
||||
onclick={() =>
|
||||
resolve(conflict.file_path, "mine")}
|
||||
>
|
||||
Keep Mine
|
||||
@@ -148,7 +148,7 @@
|
||||
] === 'theirs'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-gray-50 hover:bg-green-50 text-green-600'}"
|
||||
on:click={() =>
|
||||
onclick={() =>
|
||||
resolve(conflict.file_path, "theirs")}
|
||||
>
|
||||
Keep Theirs
|
||||
@@ -161,13 +161,13 @@
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t">
|
||||
<button
|
||||
on:click={() => (show = false)}
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
on:click={handleSave}
|
||||
onclick={handleSave}
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors shadow-sm"
|
||||
>
|
||||
Resolve & Continue
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
let environments = [];
|
||||
let selectedEnv = "";
|
||||
let loading = false;
|
||||
let deploying = false;
|
||||
let environments = $state([]);
|
||||
let selectedEnv = $state("");
|
||||
let loading = $state(false);
|
||||
let deploying = $state(false);
|
||||
// [/SECTION]
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -108,7 +108,7 @@
|
||||
</p>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => (show = false)}
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300"
|
||||
>
|
||||
Close
|
||||
@@ -133,13 +133,13 @@
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button
|
||||
on:click={() => (show = false)}
|
||||
onclick={() => (show = false)}
|
||||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
on:click={handleDeploy}
|
||||
onclick={handleDeploy}
|
||||
disabled={deploying || !selectedEnv}
|
||||
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 flex items-center"
|
||||
>
|
||||
|
||||
@@ -35,20 +35,20 @@
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
let currentBranch = 'main';
|
||||
let showCommitModal = false;
|
||||
let showDeployModal = false;
|
||||
let currentBranch = $state('main');
|
||||
let showCommitModal = $state(false);
|
||||
let showDeployModal = $state(false);
|
||||
let showHistory = true;
|
||||
let showConflicts = false;
|
||||
let showConflicts = $state(false);
|
||||
let conflicts = [];
|
||||
let loading = false;
|
||||
let initialized = false;
|
||||
let checkingStatus = true;
|
||||
let loading = $state(false);
|
||||
let initialized = $state(false);
|
||||
let checkingStatus = $state(true);
|
||||
|
||||
// Initialization form state
|
||||
let configs = [];
|
||||
let selectedConfigId = "";
|
||||
let remoteUrl = "";
|
||||
let configs = $state([]);
|
||||
let selectedConfigId = $state("");
|
||||
let remoteUrl = $state("");
|
||||
// [/SECTION]
|
||||
|
||||
// [DEF:checkStatus:Function]
|
||||
@@ -167,7 +167,7 @@
|
||||
<PageHeader title="{$t.git.management}: {dashboardTitle}">
|
||||
<div slot="subtitle" class="text-sm text-gray-500">ID: {dashboardId}</div>
|
||||
<div slot="actions">
|
||||
<button on:click={() => show = false} class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<button onclick={() => show = false} class="text-gray-400 hover:text-gray-600 transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -203,7 +203,7 @@
|
||||
/>
|
||||
|
||||
<Button
|
||||
on:click={handleInit}
|
||||
onclick={handleInit}
|
||||
disabled={loading || configs.length === 0}
|
||||
isLoading={loading}
|
||||
class="w-full"
|
||||
@@ -226,14 +226,14 @@
|
||||
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">{$t.git.actions}</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
on:click={handleSync}
|
||||
onclick={handleSync}
|
||||
disabled={loading}
|
||||
class="w-full"
|
||||
>
|
||||
{$t.git.sync}
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => showCommitModal = true}
|
||||
onclick={() => showCommitModal = true}
|
||||
disabled={loading}
|
||||
class="w-full"
|
||||
>
|
||||
@@ -242,7 +242,7 @@
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={handlePull}
|
||||
onclick={handlePull}
|
||||
disabled={loading}
|
||||
class="border border-gray-200"
|
||||
>
|
||||
@@ -250,7 +250,7 @@
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
on:click={handlePush}
|
||||
onclick={handlePush}
|
||||
disabled={loading}
|
||||
class="border border-gray-200"
|
||||
>
|
||||
@@ -263,7 +263,7 @@
|
||||
<h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">{$t.git.deployment}</h3>
|
||||
<Button
|
||||
variant="primary"
|
||||
on:click={() => showDeployModal = true}
|
||||
onclick={() => showDeployModal = true}
|
||||
disabled={loading}
|
||||
class="w-full bg-green-600 hover:bg-green-700 focus-visible:ring-green-500"
|
||||
>
|
||||
@@ -300,4 +300,4 @@
|
||||
/>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<!-- [/DEF:GitManager:Component] -->
|
||||
<!-- [/DEF:GitManager:Component] -->
|
||||
|
||||
@@ -11,13 +11,17 @@
|
||||
|
||||
/** @type {Object} */
|
||||
let {
|
||||
documentation = null,
|
||||
content = "",
|
||||
type = 'markdown',
|
||||
format = 'text',
|
||||
onSave = async () => {},
|
||||
onCancel = () => {},
|
||||
} = $props();
|
||||
|
||||
let previewDoc = $derived(documentation || content);
|
||||
|
||||
let isSaving = false;
|
||||
let isSaving = $state(false);
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
@@ -31,14 +35,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if documentation}
|
||||
{#if previewDoc}
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white p-6 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col">
|
||||
<h3 class="text-lg font-semibold mb-4">{$t.llm.doc_preview_title}</h3>
|
||||
|
||||
<div class="flex-1 overflow-y-auto mb-6 prose prose-sm max-w-none border rounded p-4 bg-gray-50">
|
||||
<h4 class="text-md font-bold text-gray-800 mb-2">{$t.llm.dataset_desc}</h4>
|
||||
<p class="text-gray-700 mb-4 whitespace-pre-wrap">{documentation.description || 'No description generated.'}</p>
|
||||
<p class="text-gray-700 mb-4 whitespace-pre-wrap">{previewDoc.description || 'No description generated.'}</p>
|
||||
|
||||
<h4 class="text-md font-bold text-gray-800 mb-2">{$t.llm.column_doc}</h4>
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
@@ -49,7 +53,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
{#each Object.entries(documentation.columns || {}) as [name, desc]}
|
||||
{#each Object.entries(previewDoc.columns || {}) as [name, desc]}
|
||||
<tr>
|
||||
<td class="px-3 py-2 text-sm font-mono text-gray-900">{name}</td>
|
||||
<td class="px-3 py-2 text-sm text-gray-700">{desc}</td>
|
||||
@@ -62,14 +66,14 @@
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
class="px-4 py-2 border rounded hover:bg-gray-50"
|
||||
on:click={onCancel}
|
||||
onclick={onCancel}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{$t.llm.cancel}
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
on:click={handleSave}
|
||||
onclick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? $t.llm.applying : $t.llm.apply_doc}
|
||||
@@ -79,4 +83,4 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- [/DEF:DocPreview:Component] -->
|
||||
<!-- [/DEF:DocPreview:Component] -->
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { requestApi } from "../../lib/api";
|
||||
|
||||
/** @type {Array} */
|
||||
let { providers = [], onSave = () => {} } = $props();
|
||||
export let providers = [];
|
||||
export let onSave = () => {};
|
||||
|
||||
let editingProvider = null;
|
||||
let showForm = false;
|
||||
@@ -43,8 +43,18 @@
|
||||
}
|
||||
|
||||
function handleEdit(provider) {
|
||||
console.log("[ProviderConfig][Action] Editing provider", provider?.id);
|
||||
editingProvider = provider;
|
||||
formData = { ...provider, api_key: "" }; // Don't populate key for security
|
||||
// Normalize provider fields to editable form shape.
|
||||
formData = {
|
||||
name: provider?.name ?? "",
|
||||
provider_type: provider?.provider_type ?? "openai",
|
||||
base_url: provider?.base_url ?? "https://api.openai.com/v1",
|
||||
api_key: "",
|
||||
default_model: provider?.default_model ?? "gpt-4o",
|
||||
is_active: Boolean(provider?.is_active),
|
||||
};
|
||||
testStatus = { type: "", message: "" };
|
||||
showForm = true;
|
||||
}
|
||||
|
||||
@@ -121,19 +131,22 @@
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold">{$t.llm.providers_title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
|
||||
on:click={() => {
|
||||
on:click|preventDefault={() => {
|
||||
resetForm();
|
||||
showForm = true;
|
||||
}}
|
||||
>
|
||||
{$t.llm.add_provider}
|
||||
{$t.llm.add_provider}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showForm}
|
||||
<div
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="bg-white p-6 rounded-lg shadow-xl w-full max-w-md">
|
||||
<h3 class="text-lg font-semibold mb-4">
|
||||
@@ -241,23 +254,26 @@
|
||||
|
||||
<div class="mt-6 flex justify-between gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 border rounded hover:bg-gray-50 flex-1"
|
||||
on:click={() => {
|
||||
on:click|preventDefault={() => {
|
||||
showForm = false;
|
||||
}}
|
||||
>
|
||||
{$t.llm.cancel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 flex-1"
|
||||
disabled={isTesting}
|
||||
on:click={testConnection}
|
||||
on:click|preventDefault={testConnection}
|
||||
>
|
||||
{isTesting ? $t.llm.testing : $t.llm.test}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex-1"
|
||||
on:click={handleSubmit}
|
||||
on:click|preventDefault={handleSubmit}
|
||||
>
|
||||
{$t.llm.save}
|
||||
</button>
|
||||
@@ -286,14 +302,16 @@
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-sm text-blue-600 hover:underline"
|
||||
on:click={() => handleEdit(provider)}
|
||||
on:click|preventDefault|stopPropagation={() => handleEdit(provider)}
|
||||
>
|
||||
{$t.common.edit}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class={`text-sm ${provider.is_active ? "text-orange-600" : "text-green-600"} hover:underline`}
|
||||
on:click={() => toggleActive(provider)}
|
||||
on:click|preventDefault|stopPropagation={() => toggleActive(provider)}
|
||||
>
|
||||
{provider.is_active ? "Deactivate" : "Activate"}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// [DEF:frontend.src.components.llm.__tests__.provider_config_integration:Module]
|
||||
// @TIER: STANDARD
|
||||
// @SEMANTICS: llm, provider-config, integration-test, edit-flow
|
||||
// @PURPOSE: Protect edit-button interaction contract in LLM provider settings UI.
|
||||
// @LAYER: UI Tests
|
||||
// @RELATION: VERIFIES -> frontend/src/components/llm/ProviderConfig.svelte
|
||||
// @INVARIANT: Edit action keeps explicit click handler and opens normalized edit form.
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const COMPONENT_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
'src/components/llm/ProviderConfig.svelte',
|
||||
);
|
||||
|
||||
// [DEF:provider_config_edit_contract_tests:Function]
|
||||
// @TIER: STANDARD
|
||||
// @PURPOSE: Validate edit button handler wiring and normalized edit form state mapping.
|
||||
// @PRE: ProviderConfig component source exists in expected path.
|
||||
// @POST: Contract checks ensure edit click cannot degrade into no-op flow.
|
||||
describe('ProviderConfig edit interaction contract', () => {
|
||||
it('keeps explicit edit click handler with guarded button semantics', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain('type="button"');
|
||||
expect(source).toContain(
|
||||
"on:click|preventDefault|stopPropagation={() => handleEdit(provider)}",
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes provider payload into editable form shape', () => {
|
||||
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
|
||||
|
||||
expect(source).toContain('formData = {');
|
||||
expect(source).toContain('name: provider?.name ?? ""');
|
||||
expect(source).toContain('provider_type: provider?.provider_type ?? "openai"');
|
||||
expect(source).toContain('default_model: provider?.default_model ?? "gpt-4o"');
|
||||
expect(source).toContain('showForm = true;');
|
||||
});
|
||||
});
|
||||
// [/DEF:provider_config_edit_contract_tests:Function]
|
||||
// [/DEF:frontend.src.components.llm.__tests__.provider_config_integration:Module]
|
||||
@@ -31,8 +31,8 @@
|
||||
path = '',
|
||||
} = $props();
|
||||
|
||||
let isUploading = false;
|
||||
let dragOver = false;
|
||||
let isUploading = $state(false);
|
||||
let dragOver = $state(false);
|
||||
|
||||
async function handleUpload() {
|
||||
const file = fileInput.files[0];
|
||||
@@ -94,9 +94,9 @@
|
||||
<div
|
||||
class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-dashed rounded-md transition-colors
|
||||
{dragOver ? 'border-indigo-500 bg-indigo-50' : 'border-gray-300'}"
|
||||
on:dragover|preventDefault={() => dragOver = true}
|
||||
on:dragleave|preventDefault={() => dragOver = false}
|
||||
on:drop|preventDefault={handleDrop}
|
||||
ondragover={(event) => { event.preventDefault(); dragOver = true; }}
|
||||
ondragleave={(event) => { event.preventDefault(); dragOver = false; }}
|
||||
ondrop={(event) => { event.preventDefault(); handleDrop(event); }}
|
||||
>
|
||||
<div class="space-y-1 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
||||
@@ -111,7 +111,7 @@
|
||||
type="file"
|
||||
class="sr-only"
|
||||
bind:this={fileInput}
|
||||
on:change={handleUpload}
|
||||
onchange={handleUpload}
|
||||
disabled={isUploading}
|
||||
>
|
||||
</label>
|
||||
@@ -132,4 +132,4 @@
|
||||
<!-- [/SECTION: TEMPLATE] -->
|
||||
|
||||
|
||||
<!-- [/DEF:FileUpload:Component] -->
|
||||
<!-- [/DEF:FileUpload:Component] -->
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
<aside class="fixed right-0 top-0 z-[71] h-full w-full max-w-md border-l border-slate-200 bg-white shadow-2xl">
|
||||
<div class="flex h-14 items-center justify-between border-b border-slate-200 px-4">
|
||||
<div class="flex items-center gap-2 text-slate-800">
|
||||
<Icon name="activity" size={18} />
|
||||
<Icon name="clipboard" size={18} />
|
||||
<h2 class="text-sm font-semibold">{$t.assistant?.title || 'AI Assistant'}</h2>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @TIER: CRITICAL
|
||||
* @PURPOSE: Global task drawer for monitoring background operations
|
||||
* @LAYER: UI
|
||||
* @RELATION: BINDS_TO -> taskDrawerStore, WebSocket
|
||||
* @RELATION: BINDS_TO -> taskDrawerStore, assistantChatStore, WebSocket
|
||||
* @SEMANTICS: TaskLogViewer
|
||||
* @INVARIANT: Drawer shows logs for active task or remains closed
|
||||
*
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { taskDrawerStore, closeDrawer } from "$lib/stores/taskDrawer.js";
|
||||
import { assistantChatStore } from "$lib/stores/assistantChat.js";
|
||||
import TaskLogViewer from "../../../components/TaskLogViewer.svelte";
|
||||
import PasswordPrompt from "../../../components/PasswordPrompt.svelte";
|
||||
import { t } from "$lib/i18n";
|
||||
@@ -33,12 +34,15 @@
|
||||
let taskStatus = null;
|
||||
let recentTasks = [];
|
||||
let loadingTasks = false;
|
||||
let isAssistantOpen = false;
|
||||
|
||||
// Subscribe to task drawer store
|
||||
$: if ($taskDrawerStore) {
|
||||
isOpen = $taskDrawerStore.isOpen;
|
||||
activeTaskId = $taskDrawerStore.activeTaskId;
|
||||
}
|
||||
$: isAssistantOpen = Boolean($assistantChatStore?.isOpen);
|
||||
$: assistantOffset = isAssistantOpen ? "min(100vw, 28rem)" : "0px";
|
||||
|
||||
// Derive short task ID for display
|
||||
$: shortTaskId = activeTaskId
|
||||
@@ -191,7 +195,8 @@
|
||||
<!-- Drawer Overlay -->
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="fixed inset-0 z-50 bg-black/35 backdrop-blur-sm"
|
||||
class="fixed inset-0 z-[69] bg-black/35 backdrop-blur-sm"
|
||||
style={`right: ${assistantOffset};`}
|
||||
on:click={handleOverlayClick}
|
||||
on:keydown={(e) => e.key === 'Escape' && handleClose()}
|
||||
role="button"
|
||||
@@ -200,7 +205,8 @@
|
||||
>
|
||||
<!-- Drawer Panel -->
|
||||
<div
|
||||
class="fixed right-0 top-0 z-50 flex h-full w-full max-w-[560px] flex-col border-l border-slate-200 bg-white shadow-[-8px_0_30px_rgba(15,23,42,0.15)] transition-transform duration-300 ease-out"
|
||||
class="fixed top-0 z-[72] flex h-full w-full max-w-[560px] flex-col border-l border-slate-200 bg-white shadow-[-8px_0_30px_rgba(15,23,42,0.15)] transition-[right] duration-300 ease-out"
|
||||
style={`right: ${assistantOffset};`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Task drawer"
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
aria-label={$t.assistant?.open || "Open assistant"}
|
||||
title={$t.assistant?.title || "AI Assistant"}
|
||||
>
|
||||
<Icon name="activity" size={22} />
|
||||
<Icon name="clipboard" size={22} />
|
||||
</button>
|
||||
|
||||
<!-- Activity Indicator -->
|
||||
|
||||
@@ -93,6 +93,12 @@
|
||||
"env_actions": "Actions",
|
||||
"connections_description": "Configure database connections for data mapping.",
|
||||
"llm_description": "Configure LLM providers for dataset documentation.",
|
||||
"llm_prompts_title": "LLM Prompt Templates",
|
||||
"llm_prompts_description": "Edit reusable prompts used for documentation, dashboard validation, and git commit generation.",
|
||||
"llm_prompt_documentation": "Documentation Prompt",
|
||||
"llm_prompt_dashboard_validation": "Dashboard Validation Prompt",
|
||||
"llm_prompt_git_commit": "Git Commit Prompt",
|
||||
"save_llm_prompts": "Save LLM Prompts",
|
||||
"logging": "Logging Configuration",
|
||||
"logging_description": "Configure logging and task log levels.",
|
||||
"storage_description": "Configure file storage paths and patterns.",
|
||||
|
||||
@@ -93,6 +93,12 @@
|
||||
"env_actions": "Действия",
|
||||
"connections_description": "Настройка подключений к базам данных для маппинга.",
|
||||
"llm_description": "Настройка LLM провайдеров для документирования датасетов.",
|
||||
"llm_prompts_title": "Шаблоны промптов LLM",
|
||||
"llm_prompts_description": "Редактируйте промпты для документации, проверки дашбордов и генерации git-коммитов.",
|
||||
"llm_prompt_documentation": "Промпт документации",
|
||||
"llm_prompt_dashboard_validation": "Промпт проверки дашборда",
|
||||
"llm_prompt_git_commit": "Промпт git-коммита",
|
||||
"save_llm_prompts": "Сохранить промпты LLM",
|
||||
"logging": "Настройка логирования",
|
||||
"logging_description": "Настройка уровней логирования задач.",
|
||||
"storage_description": "Настройка путей и шаблонов файлового хранилища.",
|
||||
|
||||
@@ -10,15 +10,40 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import ProviderConfig from '../../../../components/llm/ProviderConfig.svelte';
|
||||
import { t } from '../../../../lib/i18n';
|
||||
import { addToast } from '../../../../lib/toasts';
|
||||
import { requestApi } from '../../../../lib/api';
|
||||
|
||||
let providers = [];
|
||||
let loading = true;
|
||||
let savingPrompts = false;
|
||||
let prompts = {
|
||||
documentation_prompt: '',
|
||||
dashboard_validation_prompt: '',
|
||||
git_commit_prompt: '',
|
||||
};
|
||||
|
||||
const DEFAULT_LLM_PROMPTS = {
|
||||
dashboard_validation_prompt:
|
||||
"Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\\n\\nLogs:\\n{logs}\\n\\nProvide the analysis in JSON format with the following structure:\\n{\\n \\\"status\\\": \\\"PASS\\\" | \\\"WARN\\\" | \\\"FAIL\\\",\\n \\\"summary\\\": \\\"Short summary of findings\\\",\\n \\\"issues\\\": [\\n {\\n \\\"severity\\\": \\\"WARN\\\" | \\\"FAIL\\\",\\n \\\"message\\\": \\\"Description of the issue\\\",\\n \\\"location\\\": \\\"Optional location info (e.g. chart name)\\\"\\n }\\n ]\\n}",
|
||||
documentation_prompt:
|
||||
"Generate professional documentation for the following dataset and its columns.\\nDataset: {dataset_name}\\nColumns: {columns_json}\\n\\nProvide the documentation in JSON format:\\n{\\n \\\"dataset_description\\\": \\\"General description of the dataset\\\",\\n \\\"column_descriptions\\\": [\\n {\\n \\\"name\\\": \\\"column_name\\\",\\n \\\"description\\\": \\\"Generated description\\\"\\n }\\n ]\\n}",
|
||||
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:",
|
||||
};
|
||||
|
||||
async function fetchProviders() {
|
||||
loading = true;
|
||||
try {
|
||||
providers = await requestApi('/llm/providers');
|
||||
const [providerList, consolidatedSettings] = await Promise.all([
|
||||
requestApi('/llm/providers'),
|
||||
requestApi('/settings/consolidated'),
|
||||
]);
|
||||
providers = providerList;
|
||||
prompts = {
|
||||
...DEFAULT_LLM_PROMPTS,
|
||||
...(consolidatedSettings?.llm?.prompts || {}),
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch providers", err);
|
||||
} finally {
|
||||
@@ -26,6 +51,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function savePrompts() {
|
||||
savingPrompts = true;
|
||||
try {
|
||||
const current = await requestApi('/settings/consolidated');
|
||||
const payload = {
|
||||
...current,
|
||||
llm: {
|
||||
...(current?.llm || {}),
|
||||
prompts: {
|
||||
...DEFAULT_LLM_PROMPTS,
|
||||
...prompts,
|
||||
},
|
||||
},
|
||||
};
|
||||
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);
|
||||
addToast($t.settings?.save_failed || 'Failed to save settings', 'error');
|
||||
} finally {
|
||||
savingPrompts = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(fetchProviders);
|
||||
</script>
|
||||
|
||||
@@ -43,7 +92,65 @@
|
||||
</div>
|
||||
{:else}
|
||||
<ProviderConfig {providers} onSave={fetchProviders} />
|
||||
|
||||
<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'}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
{$t.settings?.llm_prompts_description ||
|
||||
'Edit reusable prompts used for documentation, dashboard validation, and git commit generation.'}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label for="admin-documentation-prompt" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_prompt_documentation || 'Documentation Prompt'}
|
||||
</label>
|
||||
<textarea
|
||||
id="admin-documentation-prompt"
|
||||
bind:value={prompts.documentation_prompt}
|
||||
rows="8"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="admin-dashboard-validation-prompt" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_prompt_dashboard_validation || 'Dashboard Validation Prompt'}
|
||||
</label>
|
||||
<textarea
|
||||
id="admin-dashboard-validation-prompt"
|
||||
bind:value={prompts.dashboard_validation_prompt}
|
||||
rows="10"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="admin-git-commit-prompt" class="block text-sm font-medium text-gray-700">
|
||||
{$t.settings?.llm_prompt_git_commit || 'Git Commit Prompt'}
|
||||
</label>
|
||||
<textarea
|
||||
id="admin-git-commit-prompt"
|
||||
bind:value={prompts.git_commit_prompt}
|
||||
rows="8"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-60"
|
||||
disabled={savingPrompts}
|
||||
on:click={savePrompts}
|
||||
>
|
||||
{savingPrompts ? '...' : ($t.settings?.save_llm_prompts || 'Save LLM Prompts')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- [/DEF:LLMSettingsPage:Component] -->
|
||||
<!-- [/DEF:LLMSettingsPage:Component] -->
|
||||
|
||||
@@ -20,6 +20,15 @@
|
||||
import { addToast } from "$lib/toasts";
|
||||
import ProviderConfig from "../../components/llm/ProviderConfig.svelte";
|
||||
|
||||
const DEFAULT_LLM_PROMPTS = {
|
||||
dashboard_validation_prompt:
|
||||
"Analyze the attached dashboard screenshot and the following execution logs for health and visual issues.\\n\\nLogs:\\n{logs}\\n\\nProvide the analysis in JSON format with the following structure:\\n{\\n \\\"status\\\": \\\"PASS\\\" | \\\"WARN\\\" | \\\"FAIL\\\",\\n \\\"summary\\\": \\\"Short summary of findings\\\",\\n \\\"issues\\\": [\\n {\\n \\\"severity\\\": \\\"WARN\\\" | \\\"FAIL\\\",\\n \\\"message\\\": \\\"Description of the issue\\\",\\n \\\"location\\\": \\\"Optional location info (e.g. chart name)\\\"\\n }\\n ]\\n}",
|
||||
documentation_prompt:
|
||||
"Generate professional documentation for the following dataset and its columns.\\nDataset: {dataset_name}\\nColumns: {columns_json}\\n\\nProvide the documentation in JSON format:\\n{\\n \\\"dataset_description\\\": \\\"General description of the dataset\\\",\\n \\\"column_descriptions\\\": [\\n {\\n \\\"name\\\": \\\"column_name\\\",\\n \\\"description\\\": \\\"Generated description\\\"\\n }\\n ]\\n}",
|
||||
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:",
|
||||
};
|
||||
|
||||
// State
|
||||
let activeTab = "environments";
|
||||
let settings = null;
|
||||
@@ -53,6 +62,7 @@
|
||||
error = null;
|
||||
try {
|
||||
const response = await api.getConsolidatedSettings();
|
||||
response.llm = normalizeLlmSettings(response.llm);
|
||||
settings = response;
|
||||
} catch (err) {
|
||||
error = err.message || "Failed to load settings";
|
||||
@@ -62,6 +72,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLlmSettings(llm) {
|
||||
const normalized = {
|
||||
providers: [],
|
||||
default_provider: "",
|
||||
prompts: { ...DEFAULT_LLM_PROMPTS },
|
||||
...(llm || {}),
|
||||
};
|
||||
normalized.prompts = {
|
||||
...DEFAULT_LLM_PROMPTS,
|
||||
...(llm?.prompts || {}),
|
||||
};
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// Handle tab change
|
||||
function handleTabChange(tab) {
|
||||
activeTab = tab;
|
||||
@@ -78,6 +102,7 @@
|
||||
async function handleSave() {
|
||||
console.log("[SettingsPage][Action] Saving settings");
|
||||
try {
|
||||
settings.llm = normalizeLlmSettings(settings.llm);
|
||||
// In a real app we might want to only send the changed section,
|
||||
// but updateConsolidatedSettings expects full object or we can use specific endpoints.
|
||||
// For now we use the consolidated update.
|
||||
@@ -644,6 +669,73 @@
|
||||
providers={settings.llm_providers || []}
|
||||
onSave={loadSettings}
|
||||
/>
|
||||
|
||||
<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"}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
{$t.settings?.llm_prompts_description ||
|
||||
"Edit reusable prompts used for documentation, dashboard validation, and git commit generation."}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="documentation-prompt"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{$t.settings?.llm_prompt_documentation || "Documentation Prompt"}
|
||||
</label>
|
||||
<textarea
|
||||
id="documentation-prompt"
|
||||
bind:value={settings.llm.prompts.documentation_prompt}
|
||||
rows="8"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="dashboard-validation-prompt"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{$t.settings?.llm_prompt_dashboard_validation ||
|
||||
"Dashboard Validation Prompt"}
|
||||
</label>
|
||||
<textarea
|
||||
id="dashboard-validation-prompt"
|
||||
bind:value={settings.llm.prompts.dashboard_validation_prompt}
|
||||
rows="10"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="git-commit-prompt"
|
||||
class="block text-sm font-medium text-gray-700"
|
||||
>
|
||||
{$t.settings?.llm_prompt_git_commit || "Git Commit Prompt"}
|
||||
</label>
|
||||
<textarea
|
||||
id="git-commit-prompt"
|
||||
bind:value={settings.llm.prompts.git_commit_prompt}
|
||||
rows="8"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 p-2 font-mono text-xs"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
|
||||
on:click={handleSave}
|
||||
>
|
||||
{$t.settings?.save_llm_prompts || "Save LLM Prompts"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if activeTab === "storage"}
|
||||
<!-- Storage Tab -->
|
||||
|
||||
Reference in New Issue
Block a user