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