Вроде работает

This commit is contained in:
2026-01-30 11:10:16 +03:00
parent 8044f85ea4
commit 252a8601a9
43 changed files with 1987 additions and 270 deletions

View File

@@ -15,11 +15,14 @@
import { t } from '../lib/i18n';
import { Button, Input } from '../lib/ui';
import GitManager from './git/GitManager.svelte';
import { api } from '../lib/api';
import { addToast as toast } from '../lib/toasts.js';
// [/SECTION]
// [SECTION: PROPS]
export let dashboards: DashboardMetadata[] = [];
export let selectedIds: number[] = [];
export let environmentId: string = "ss1";
// [/SECTION]
// [SECTION: STATE]
@@ -34,8 +37,54 @@
let showGitManager = false;
let gitDashboardId: number | null = null;
let gitDashboardTitle = "";
let validatingIds: Set<number> = new Set();
// [/SECTION]
// [DEF:handleValidate:Function]
/**
* @purpose Triggers dashboard validation task.
*/
async function handleValidate(dashboard: DashboardMetadata) {
if (validatingIds.has(dashboard.id)) return;
validatingIds.add(dashboard.id);
validatingIds = validatingIds; // Trigger reactivity
try {
// TODO: Get provider_id from settings or prompt user
// For now, we assume a default provider or let the backend handle it if possible,
// but the plugin requires provider_id.
// In a real implementation, we might open a modal to select provider if not configured globally.
// Or we pick the first active one.
// Fetch active provider first
const providers = await api.fetchApi('/llm/providers');
const activeProvider = providers.find((p: any) => p.is_active);
if (!activeProvider) {
toast('No active LLM provider found. Please configure one in settings.', 'error');
return;
}
await api.postApi('/tasks', {
plugin_id: 'llm_dashboard_validation',
params: {
dashboard_id: dashboard.id.toString(),
environment_id: environmentId,
provider_id: activeProvider.id
}
});
toast('Validation task started', 'success');
} catch (e: any) {
toast(e.message || 'Validation failed to start', 'error');
} finally {
validatingIds.delete(dashboard.id);
validatingIds = validatingIds;
}
}
// [/DEF:handleValidate:Function]
// [SECTION: DERIVED]
$: filteredDashboards = dashboards.filter(d =>
d.title.toLowerCase().includes(filterText.toLowerCase())
@@ -175,6 +224,7 @@
<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')}>
{$t.dashboard.status} {sortColumn === 'status' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.dashboard.validation}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{$t.dashboard.git}</th>
</tr>
</thead>
@@ -196,6 +246,17 @@
{dashboard.status}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<Button
variant="secondary"
size="sm"
on:click={() => handleValidate(dashboard)}
disabled={validatingIds.has(dashboard.id)}
class="text-purple-600 hover:text-purple-900"
>
{validatingIds.has(dashboard.id) ? 'Validating...' : 'Validate'}
</Button>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<Button
variant="ghost"

View File

@@ -9,7 +9,7 @@
import { page } from '$app/stores';
import { t } from '$lib/i18n';
import { LanguageSwitcher } from '$lib/ui';
import { auth } from '../lib/auth/store';
import { auth } from '$lib/auth/store';
import { goto } from '$app/navigation';
function handleLogout() {
@@ -58,6 +58,7 @@
<a href="/admin/users" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.admin_users}</a>
<a href="/admin/roles" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.admin_roles}</a>
<a href="/admin/settings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.admin_settings}</a>
<a href="/admin/settings/llm" class="block px-4 py-2 text-sm text-gray-700 hover:bg-blue-50 hover:text-blue-600">{$t.nav.admin_llm}</a>
</div>
</div>
{/if}

View File

@@ -9,6 +9,7 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { selectedTask } from '../lib/stores.js';
import { api } from '../lib/api.js';
let tasks = [];
let loading = true;
@@ -21,11 +22,7 @@
// @POST: tasks array is updated and selectedTask status synchronized.
async function fetchTasks() {
try {
const token = localStorage.getItem('auth_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const res = await fetch('/api/tasks?limit=10', { headers });
if (!res.ok) throw new Error('Failed to fetch tasks');
tasks = await res.json();
tasks = await api.fetchApi('/tasks?limit=10');
// [DEBUG] Check for tasks requiring attention
tasks.forEach(t => {
@@ -56,15 +53,10 @@
async function clearTasks(status = null) {
if (!confirm('Are you sure you want to clear tasks?')) return;
try {
let url = '/api/tasks';
const params = new URLSearchParams();
if (status) params.append('status', status);
const token = localStorage.getItem('auth_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const res = await fetch(`${url}?${params.toString()}`, { method: 'DELETE', headers });
if (!res.ok) throw new Error('Failed to clear tasks');
let endpoint = '/tasks';
if (status) endpoint += `?status=${status}`;
await api.requestApi(endpoint, 'DELETE');
await fetchTasks();
} catch (e) {
error = e.message;
@@ -79,16 +71,8 @@
async function selectTask(task) {
try {
// Fetch the full task details (including logs) before setting it as selected
const token = localStorage.getItem('auth_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const res = await fetch(`/api/tasks/${task.id}`, { headers });
if (res.ok) {
const fullTask = await res.json();
selectedTask.set(fullTask);
} else {
// Fallback to the list version if fetch fails
selectedTask.set(task);
}
const fullTask = await api.getTask(task.id);
selectedTask.set(fullTask);
} catch (e) {
console.error("Failed to fetch full task details:", e);
selectedTask.set(task);

View File

@@ -13,7 +13,7 @@
import { onMount, onDestroy } from 'svelte';
import { get } from 'svelte/store';
import { selectedTask, taskLogs } from '../lib/stores.js';
import { getWsUrl } from '../lib/api.js';
import { getWsUrl, api } from '../lib/api.js';
import { addToast } from '../lib/toasts.js';
import MissingMappingModal from './MissingMappingModal.svelte';
import PasswordPrompt from './PasswordPrompt.svelte';
@@ -141,13 +141,11 @@
try {
// We need to find the environment ID by name first
const envsRes = await fetch('/api/environments');
const envs = await envsRes.json();
const envs = await api.fetchApi('/environments');
const targetEnv = envs.find(e => e.name === task.params.to_env);
if (targetEnv) {
const res = await fetch(`/api/environments/${targetEnv.id}/databases`);
targetDatabases = await res.json();
targetDatabases = await api.fetchApi(`/environments/${targetEnv.id}/databases`);
}
} catch (e) {
console.error('Failed to fetch target databases', e);
@@ -165,31 +163,22 @@
try {
// 1. Save mapping to backend
const envsRes = await fetch('/api/environments');
const envs = await envsRes.json();
const envs = await api.fetchApi('/environments');
const srcEnv = envs.find(e => e.name === task.params.from_env);
const tgtEnv = envs.find(e => e.name === task.params.to_env);
await fetch('/api/mappings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source_env_id: srcEnv.id,
target_env_id: tgtEnv.id,
source_db_uuid: sourceDbUuid,
target_db_uuid: targetDbUuid,
source_db_name: missingDbInfo.name,
target_db_name: targetDbName
})
await api.postApi('/mappings', {
source_env_id: srcEnv.id,
target_env_id: tgtEnv.id,
source_db_uuid: sourceDbUuid,
target_db_uuid: targetDbUuid,
source_db_name: missingDbInfo.name,
target_db_name: targetDbName
});
// 2. Resolve task
await fetch(`/api/tasks/${task.id}/resolve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
resolution_params: { resolved_mapping: { [sourceDbUuid]: targetDbUuid } }
})
await api.postApi(`/tasks/${task.id}/resolve`, {
resolution_params: { resolved_mapping: { [sourceDbUuid]: targetDbUuid } }
});
connectionStatus = 'connected';
@@ -209,11 +198,7 @@
const { passwords } = event.detail;
try {
await fetch(`/api/tasks/${task.id}/resume`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ passwords })
});
await api.postApi(`/tasks/${task.id}/resume`, { passwords });
showPasswordPrompt = false;
connectionStatus = 'connected';

View File

@@ -12,6 +12,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { auth } from '../../lib/auth/store';
import { api } from '../../lib/api';
import { goto } from '$app/navigation';
// [SECTION: TEMPLATE]
@@ -23,20 +24,8 @@
if ($auth.token && !$auth.user) {
auth.setLoading(true);
try {
const response = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${$auth.token}`
}
});
if (response.ok) {
const user = await response.json();
auth.setUser(user);
} else {
// Token invalid or expired
auth.logout();
goto('/login');
}
const user = await api.fetchApi('/auth/me');
auth.setUser(user);
} catch (e) {
console.error('Failed to verify session:', e);
auth.logout();

View File

@@ -14,6 +14,7 @@
import { createEventDispatcher, onMount } from 'svelte';
import { gitService } from '../../services/gitService';
import { addToast as toast } from '../../lib/toasts.js';
import { api } from '../../lib/api';
// [/SECTION]
// [SECTION: PROPS]
@@ -27,10 +28,32 @@
let status = null;
let diff = '';
let loading = false;
let generatingMessage = false;
// [/SECTION]
const dispatch = createEventDispatcher();
// [DEF:handleGenerateMessage:Function]
/**
* @purpose Generates a commit message using LLM.
*/
async function handleGenerateMessage() {
generatingMessage = true;
try {
console.log(`[CommitModal][Action] Generating commit message for dashboard ${dashboardId}`);
// postApi returns the JSON data directly or throws an error
const data = await api.postApi(`/git/repositories/${dashboardId}/generate-message`);
message = data.message;
toast('Commit message generated', 'success');
} catch (e) {
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
toast(e.message || 'Failed to generate message', 'error');
} finally {
generatingMessage = false;
}
}
// [/DEF:handleGenerateMessage:Function]
// [DEF:loadStatus:Function]
/**
* @purpose Загружает текущий статус репозитория и diff.
@@ -99,8 +122,21 @@
<!-- Left: Message and Files -->
<div class="w-full md:w-1/3 flex flex-col">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Commit Message</label>
<textarea
<div class="flex justify-between items-center mb-1">
<label class="block text-sm font-medium text-gray-700">Commit Message</label>
<button
on:click={handleGenerateMessage}
disabled={generatingMessage || loading}
class="text-xs text-blue-600 hover:text-blue-800 disabled:opacity-50 flex items-center"
>
{#if generatingMessage}
<span class="animate-spin mr-1"></span> Generating...
{:else}
<span class="mr-1"></span> Generate with AI
{/if}
</button>
</div>
<textarea
bind:value={message}
class="w-full border rounded p-2 h-32 focus:ring-2 focus:ring-blue-500 outline-none resize-none"
placeholder="Describe your changes..."

View File

@@ -0,0 +1,79 @@
<!-- [DEF:DocPreview:Component] -->
<!--
@TIER: STANDARD
@PURPOSE: UI component for previewing generated dataset documentation before saving.
@LAYER: UI
@RELATION: DEPENDS_ON -> backend/src/plugins/llm_analysis/plugin.py
-->
<script>
import { t } from '../../lib/i18n';
/** @type {Object} */
export let documentation = null;
export let onSave = async (doc) => {};
export let onCancel = () => {};
let isSaving = false;
async function handleSave() {
isSaving = true;
try {
await onSave(documentation);
} catch (err) {
console.error("Save failed", err);
} finally {
isSaving = false;
}
}
</script>
{#if documentation}
<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>
<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">
<thead class="bg-gray-100">
<tr>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Column</th>
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each Object.entries(documentation.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>
</tr>
{/each}
</tbody>
</table>
</div>
<div class="flex justify-end gap-3">
<button
class="px-4 py-2 border rounded hover:bg-gray-50"
on:click={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}
disabled={isSaving}
>
{isSaving ? $t.llm.applying : $t.llm.apply_doc}
</button>
</div>
</div>
</div>
{/if}
<!-- [/DEF:DocPreview:Component] -->

View File

@@ -0,0 +1,219 @@
<!-- [DEF:ProviderConfig:Component] -->
<!--
@TIER: STANDARD
@PURPOSE: UI form for managing LLM provider configurations.
@LAYER: UI
@RELATION: DEPENDS_ON -> backend/src/api/routes/llm.py
-->
<script>
import { onMount } from 'svelte';
import { t } from '../../lib/i18n';
import { requestApi } from '../../lib/api';
/** @type {Array} */
export let providers = [];
export let onSave = () => {};
let editingProvider = null;
let showForm = false;
let formData = {
name: '',
provider_type: 'openai',
base_url: 'https://api.openai.com/v1',
api_key: '',
default_model: 'gpt-4o',
is_active: true
};
let testStatus = { type: '', message: '' };
let isTesting = false;
function resetForm() {
formData = {
name: '',
provider_type: 'openai',
base_url: 'https://api.openai.com/v1',
api_key: '',
default_model: 'gpt-4o',
is_active: true
};
editingProvider = null;
testStatus = { type: '', message: '' };
}
function handleEdit(provider) {
editingProvider = provider;
formData = { ...provider, api_key: '' }; // Don't populate key for security
showForm = true;
}
async function testConnection() {
console.log("[ProviderConfig][Action] Testing connection", formData);
isTesting = true;
testStatus = { type: 'info', message: $t.llm.testing };
try {
const endpoint = editingProvider ? `/llm/providers/${editingProvider.id}/test` : '/llm/providers/test';
const result = await requestApi(endpoint, 'POST', formData);
if (result.success) {
testStatus = { type: 'success', message: $t.llm.connection_success };
} else {
testStatus = { type: 'error', message: $t.llm.connection_failed.replace('{error}', result.error || 'Unknown error') };
}
} catch (err) {
testStatus = { type: 'error', message: $t.llm.connection_failed.replace('{error}', err.message) };
} finally {
isTesting = false;
}
}
async function handleSubmit() {
console.log("[ProviderConfig][Action] Submitting provider config");
const method = editingProvider ? 'PUT' : 'POST';
const endpoint = editingProvider ? `/llm/providers/${editingProvider.id}` : '/llm/providers';
try {
await requestApi(endpoint, method, formData);
showForm = false;
resetForm();
onSave();
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function toggleActive(provider) {
try {
await requestApi(`/llm/providers/${provider.id}`, 'PUT', {
...provider,
is_active: !provider.is_active
});
onSave();
} catch (err) {
console.error("Failed to toggle status", err);
}
}
</script>
<div class="p-4">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-bold">{$t.llm.providers_title}</h2>
<button
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
on:click={() => { resetForm(); showForm = true; }}
>
{$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">
<div class="bg-white p-6 rounded-lg shadow-xl w-full max-w-md">
<h3 class="text-lg font-semibold mb-4">{editingProvider ? $t.llm.edit_provider : $t.llm.new_provider}</h3>
<div class="space-y-4">
<div>
<label for="provider-name" class="block text-sm font-medium text-gray-700">{$t.llm.name}</label>
<input id="provider-name" type="text" bind:value={formData.name} class="mt-1 block w-full border rounded-md p-2" placeholder="e.g. My OpenAI" />
</div>
<div>
<label for="provider-type" class="block text-sm font-medium text-gray-700">{$t.llm.type}</label>
<select id="provider-type" bind:value={formData.provider_type} class="mt-1 block w-full border rounded-md p-2">
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
<option value="kilo">Kilo</option>
</select>
</div>
<div>
<label for="provider-base-url" class="block text-sm font-medium text-gray-700">{$t.llm.base_url}</label>
<input id="provider-base-url" type="text" bind:value={formData.base_url} class="mt-1 block w-full border rounded-md p-2" />
</div>
<div>
<label for="provider-api-key" class="block text-sm font-medium text-gray-700">{$t.llm.api_key}</label>
<input id="provider-api-key" type="password" bind:value={formData.api_key} class="mt-1 block w-full border rounded-md p-2" placeholder={editingProvider ? "••••••••" : "sk-..."} />
</div>
<div>
<label for="provider-default-model" class="block text-sm font-medium text-gray-700">{$t.llm.default_model}</label>
<input id="provider-default-model" type="text" bind:value={formData.default_model} class="mt-1 block w-full border rounded-md p-2" placeholder="gpt-4o" />
</div>
<div class="flex items-center">
<input id="provider-active" type="checkbox" bind:checked={formData.is_active} class="mr-2" />
<label for="provider-active" class="text-sm font-medium text-gray-700">{$t.llm.active}</label>
</div>
</div>
{#if testStatus.message}
<div class={`mt-4 p-2 rounded text-sm ${testStatus.type === 'success' ? 'bg-green-100 text-green-800' : testStatus.type === 'error' ? 'bg-red-100 text-red-800' : 'bg-blue-100 text-blue-800'}`}>
{testStatus.message}
</div>
{/if}
<div class="mt-6 flex justify-between gap-2">
<button
class="px-4 py-2 border rounded hover:bg-gray-50 flex-1"
on:click={() => { showForm = false; }}
>
{$t.llm.cancel}
</button>
<button
class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 flex-1"
disabled={isTesting}
on:click={testConnection}
>
{isTesting ? $t.llm.testing : $t.llm.test}
</button>
<button
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 flex-1"
on:click={handleSubmit}
>
{$t.llm.save}
</button>
</div>
</div>
</div>
{/if}
<div class="grid gap-4">
{#each providers as provider}
<div class="border rounded-lg p-4 flex justify-between items-center bg-white shadow-sm">
<div>
<div class="font-bold flex items-center gap-2">
{provider.name}
<span class={`text-xs px-2 py-0.5 rounded-full ${provider.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}>
{provider.is_active ? $t.llm.active : 'Inactive'}
</span>
</div>
<div class="text-sm text-gray-500">{provider.provider_type}{provider.default_model}</div>
</div>
<div class="flex gap-2">
<button
class="text-sm text-blue-600 hover:underline"
on:click={() => handleEdit(provider)}
>
{$t.common.edit}
</button>
<button
class={`text-sm ${provider.is_active ? 'text-orange-600' : 'text-green-600'} hover:underline`}
on:click={() => toggleActive(provider)}
>
{provider.is_active ? 'Deactivate' : 'Activate'}
</button>
</div>
</div>
{:else}
<div class="text-center py-8 text-gray-500 border-2 border-dashed rounded-lg">
{$t.llm.no_providers}
</div>
{/each}
</div>
</div>
<!-- [/DEF:ProviderConfig:Component] -->

View File

@@ -0,0 +1,75 @@
<!-- [DEF:frontend/src/components/llm/ValidationReport.svelte:Component] -->
<!-- @TIER: STANDARD -->
<!-- @PURPOSE: Displays the results of an LLM-based dashboard validation task. -->
<script>
export let result = null;
function getStatusColor(status) {
switch (status) {
case 'PASS': return 'text-green-600 bg-green-100';
case 'WARN': return 'text-yellow-600 bg-yellow-100';
case 'FAIL': return 'text-red-600 bg-red-100';
default: return 'text-gray-600 bg-gray-100';
}
}
</script>
{#if result}
<div class="bg-white shadow rounded-lg p-6 space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-gray-900">Validation Report</h2>
<span class={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(result.status)}`}>
{result.status}
</span>
</div>
<div class="prose max-w-none">
<h3 class="text-lg font-semibold">Summary</h3>
<p>{result.summary}</p>
</div>
{#if result.issues && result.issues.length > 0}
<div class="space-y-4">
<h3 class="text-lg font-semibold">Detected Issues</h3>
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900">Severity</th>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Message</th>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Location</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{#each result.issues as issue}
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm">
<span class={`px-2 py-1 rounded text-xs font-medium ${getStatusColor(issue.severity)}`}>
{issue.severity}
</span>
</td>
<td class="px-3 py-4 text-sm text-gray-500">{issue.message}</td>
<td class="px-3 py-4 text-sm text-gray-500">{issue.location || 'N/A'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
{#if result.screenshot_path}
<div class="space-y-4">
<h3 class="text-lg font-semibold">Screenshot</h3>
<img src={`/api/storage/file?path=${encodeURIComponent(result.screenshot_path)}`} alt="Dashboard Screenshot" class="rounded-lg border border-gray-200 shadow-sm max-w-full" />
</div>
{/if}
</div>
{:else}
<div class="text-center py-12 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
<p class="text-gray-500">No validation result available.</p>
</div>
{/if}
<!-- [/DEF:frontend/src/components/llm/ValidationReport.svelte] -->

View File

@@ -11,10 +11,12 @@
import { onMount } from 'svelte';
import { runTask } from '../../services/toolsService.js';
import { getConnections } from '../../services/connectionService.js';
import { api } from '../../lib/api';
import { selectedTask } from '../../lib/stores.js';
import { addToast } from '../../lib/toasts.js';
import { t } from '../../lib/i18n';
import { Button, Card, Select, Input } from '../../lib/ui';
import DocPreview from '../llm/DocPreview.svelte';
// [/SECTION]
let envs = [];
@@ -27,6 +29,8 @@
let tableSchema = 'public';
let excelPath = '';
let isRunning = false;
let isGeneratingDocs = false;
let generatedDoc = null;
// [DEF:fetchData:Function]
// @PURPOSE: Fetches environments and saved connections.
@@ -34,8 +38,7 @@
// @POST: envs and connections arrays are populated.
async function fetchData() {
try {
const envsRes = await fetch('/api/environments');
envs = await envsRes.json();
envs = await api.fetchApi('/environments');
connections = await getConnections();
} catch (e) {
addToast($t.mapper.errors.fetch_failed, 'error');
@@ -86,6 +89,53 @@
}
// [/DEF:handleRunMapper:Function]
// [DEF:handleGenerateDocs:Function]
// @PURPOSE: Triggers the LLM Documentation task.
// @PRE: selectedEnv and datasetId are set.
// @POST: Documentation task is started.
async function handleGenerateDocs() {
if (!selectedEnv || !datasetId) {
addToast($t.mapper.errors.required_fields, 'warning');
return;
}
isGeneratingDocs = true;
try {
// Fetch active provider first
const providers = await api.fetchApi('/llm/providers');
const activeProvider = providers.find(p => p.is_active);
if (!activeProvider) {
addToast('No active LLM provider found', 'error');
return;
}
const task = await runTask('llm_documentation', {
dataset_id: datasetId,
environment_id: selectedEnv,
provider_id: activeProvider.id
});
selectedTask.set(task);
addToast('Documentation generation started', 'success');
} catch (e) {
addToast(e.message || 'Failed to start documentation generation', 'error');
} finally {
isGeneratingDocs = false;
}
}
// [/DEF:handleGenerateDocs:Function]
async function handleApplyDoc(doc) {
try {
await api.put(`/mappings/datasets/${datasetId}/metadata`, doc);
generatedDoc = null;
addToast('Documentation applied successfully', 'success');
} catch (err) {
addToast(err.message || 'Failed to apply documentation', 'error');
}
}
onMount(fetchData);
</script>
@@ -167,7 +217,18 @@
</div>
{/if}
<div class="flex justify-end pt-2">
<div class="flex justify-end pt-2 space-x-3">
<Button
variant="secondary"
on:click={handleGenerateDocs}
disabled={isGeneratingDocs || isRunning}
>
{#if isGeneratingDocs}
<span class="animate-spin mr-1"></span> Generating...
{:else}
<span class="mr-1"></span> Generate Docs
{/if}
</Button>
<Button
variant="primary"
on:click={handleRunMapper}
@@ -178,6 +239,12 @@
</div>
</div>
</Card>
<DocPreview
documentation={generatedDoc}
onCancel={() => generatedDoc = null}
onSave={handleApplyDoc}
/>
</div>
<!-- [/SECTION] -->
<!-- [/DEF:MapperTool:Component] -->

View File

@@ -0,0 +1,48 @@
<!-- [DEF:LLMSettingsPage:Component] -->
<!--
@TIER: STANDARD
@PURPOSE: Admin settings page for LLM provider configuration.
@LAYER: UI
@RELATION: CALLS -> frontend/src/components/llm/ProviderConfig.svelte
-->
<script>
import { onMount } from 'svelte';
import ProviderConfig from '../../../../components/llm/ProviderConfig.svelte';
import { requestApi } from '../../../../lib/api';
let providers = [];
let loading = true;
async function fetchProviders() {
loading = true;
try {
providers = await requestApi('/llm/providers');
} catch (err) {
console.error("Failed to fetch providers", err);
} finally {
loading = false;
}
}
onMount(fetchProviders);
</script>
<div class="max-w-4xl mx-auto py-8 px-4">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">LLM Settings</h1>
<p class="mt-2 text-gray-600">
Configure LLM providers for dashboard validation, documentation generation, and git assistance.
</p>
</div>
{#if loading}
<div class="flex justify-center py-12">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
{:else}
<ProviderConfig {providers} onSave={fetchProviders} />
{/if}
</div>
<!-- [/DEF:LLMSettingsPage:Component] -->

View File

@@ -13,6 +13,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { auth } from '../../lib/auth/store';
import { api } from '../../lib/api';
import { goto } from '$app/navigation';
let username = '';
@@ -53,18 +54,12 @@
auth.setToken(data.access_token);
// Fetch user profile
const profileRes = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${data.access_token}`
}
});
if (profileRes.ok) {
const user = await profileRes.json();
try {
const user = await api.fetchApi('/auth/me');
auth.setUser(user);
goto('/');
} else {
error = 'Failed to fetch user profile';
} catch (err) {
error = 'Failed to fetch user profile: ' + err.message;
}
} else {
const errData = await response.json();

View File

@@ -311,6 +311,7 @@
<DashboardGrid
{dashboards}
bind:selectedIds={selectedDashboardIds}
environmentId={sourceEnvId}
/>
{:else}
<p class="text-gray-500 italic">Select a source environment to view dashboards.</p>