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

This commit is contained in:
2026-01-30 11:10:16 +03:00
parent 83c24d4b85
commit 1702f3a5e9
41 changed files with 1987 additions and 270 deletions

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