chat worked

This commit is contained in:
2026-02-23 20:20:25 +03:00
parent 18e96a58bc
commit 40e6d8cd4c
29 changed files with 1033 additions and 196 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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] -->

View File

@@ -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] -->

View File

@@ -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 ||

View File

@@ -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

View File

@@ -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"
>

View File

@@ -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] -->

View File

@@ -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] -->

View File

@@ -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>

View File

@@ -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]

View File

@@ -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] -->

View File

@@ -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

View File

@@ -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"

View File

@@ -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 -->

View File

@@ -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.",

View File

@@ -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": "Настройка путей и шаблонов файлового хранилища.",

View File

@@ -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] -->

View File

@@ -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 -->