feat: Implement LLM provider deletion and refactor ConfigManager to preserve unknown payload sections.

This commit is contained in:
2026-03-14 09:19:08 +03:00
parent feb07bf366
commit 3928455189
9 changed files with 256 additions and 9 deletions

View File

@@ -31,6 +31,7 @@
let testStatus = $state({ type: "", message: "" });
let isTesting = $state(false);
let togglingProviderIds = $state(new Set());
let deletingProviderIds = $state(new Set());
function isMultimodalModel(modelName) {
const token = (modelName || "").toLowerCase();
@@ -155,6 +156,38 @@
onSave();
}
}
async function handleDelete(provider) {
if (deletingProviderIds.has(provider.id)) return;
if (
!confirm(
$t.llm.delete_confirm.replace("{name}", provider.name || provider.id),
)
) {
return;
}
deletingProviderIds.add(provider.id);
deletingProviderIds = new Set(deletingProviderIds);
try {
await requestApi(`/llm/providers/${provider.id}`, "DELETE");
providers = providers.filter(({ id }) => id !== provider.id);
addToast($t.llm.delete_success.replace("{name}", provider.name), "success");
onSave();
} catch (err) {
addToast(
$t.llm.delete_failed.replace(
"{error}",
err?.message || $t.common.error,
),
"error",
);
} finally {
deletingProviderIds.delete(provider.id);
deletingProviderIds = new Set(deletingProviderIds);
}
}
</script>
<div class="p-4">
@@ -345,11 +378,23 @@
>
{$t.common.edit}
</button>
<button
type="button"
class="text-sm text-red-600 hover:underline"
onclick={() => handleDelete(provider)}
disabled={deletingProviderIds.has(provider.id)}
>
{#if deletingProviderIds.has(provider.id)}
...
{:else}
{$t.common.delete}
{/if}
</button>
<button
type="button"
class={`text-sm ${provider.is_active ? "text-orange-600" : "text-green-600"} hover:underline`}
onclick={() => toggleActive(provider)}
disabled={togglingProviderIds.has(provider.id)}
disabled={togglingProviderIds.has(provider.id) || deletingProviderIds.has(provider.id)}
>
{#if togglingProviderIds.has(provider.id)}
...

View File

@@ -1,7 +1,7 @@
// [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.
// @SEMANTICS: llm, provider-config, integration-test, edit-flow, delete-flow
// @PURPOSE: Protect edit and delete interaction contracts 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.
@@ -17,7 +17,7 @@ const COMPONENT_PATH = path.resolve(
// [DEF:provider_config_edit_contract_tests:Function]
// @TIER: STANDARD
// @PURPOSE: Validate edit button handler wiring and normalized edit form state mapping.
// @PURPOSE: Validate edit and delete handler wiring plus 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', () => {
@@ -25,9 +25,7 @@ describe('ProviderConfig edit interaction contract', () => {
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
expect(source).toContain('type="button"');
expect(source).toContain(
"on:click|preventDefault|stopPropagation={() => handleEdit(provider)}",
);
expect(source).toContain('onclick={() => handleEdit(provider)}');
});
it('normalizes provider payload into editable form shape', () => {
@@ -39,6 +37,15 @@ describe('ProviderConfig edit interaction contract', () => {
expect(source).toContain('default_model: provider?.default_model ?? "gpt-4o"');
expect(source).toContain('showForm = true;');
});
it('keeps explicit delete flow with confirmation and delete request', () => {
const source = fs.readFileSync(COMPONENT_PATH, 'utf-8');
expect(source).toContain('async function handleDelete(provider)');
expect(source).toContain('$t.llm.delete_confirm.replace("{name}", provider.name || provider.id)');
expect(source).toContain('await requestApi(`/llm/providers/${provider.id}`, "DELETE")');
expect(source).toContain('onclick={() => handleDelete(provider)}');
});
});
// [/DEF:provider_config_edit_contract_tests:Function]
// [/DEF:frontend.src.components.llm.__tests__.provider_config_integration:Module]

View File

@@ -111,6 +111,9 @@
"connection_success": "Connection successful!",
"connection_failed": "Connection failed: {error}",
"no_providers": "No providers configured.",
"delete_confirm": "Delete LLM provider \"{name}\"?",
"delete_success": "Provider \"{name}\" deleted.",
"delete_failed": "Failed to delete provider: {error}",
"multimodal": "Multimodal",
"text_only": "Text only",
"doc_preview_title": "Documentation Preview",

View File

@@ -111,6 +111,9 @@
"connection_success": "Подключение успешно!",
"connection_failed": "Ошибка подключения: {error}",
"no_providers": "Провайдеры не настроены.",
"delete_confirm": "Удалить LLM-провайдера \"{name}\"?",
"delete_success": "Провайдер \"{name}\" удалён.",
"delete_failed": "Не удалось удалить провайдера: {error}",
"multimodal": "Мультимодальная",
"text_only": "Только текст",
"doc_preview_title": "Предпросмотр документации",