Translate commit AI prompts, model labels, and git/keychain errors to English so the app and generated messages are consistent. Also add a discard confirmation dialog and update the UI text/styles to match the new flow. - src-tauri/crates/commit_ai/src/cloud.rs - Translate HTTP and API error messages to English. - Keep request timeout and token sizing behavior unchanged. - src-tauri/crates/commit_ai/src/lib.rs - Translate model labels, prompt text, and validation errors. - Keep diff truncation and message sanitization logic intact. - src-tauri/src/git.rs - Translate git, credential, merge, and history errors. - Update AI provider validation messages to English. - src/lib/components/* - Update AI settings, commit panel, credential, and loading UI text. - Add discard confirmation dialog for destructive actions. - src/App.svelte, src/app.css - Adjust app layout and styling for the new dialog and text changes.
257 lines
9.6 KiB
Svelte
257 lines
9.6 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { AlertCircle, Bot, Check, Cpu, Eye, EyeOff, Globe, Key, LoaderCircle, X } from "@lucide/svelte";
|
|
import { credDelete, credLoad, credSave } from "../git";
|
|
import type { AiSettings, CommitAiProvider, LocalModelOption } from "../types";
|
|
|
|
interface Props {
|
|
settings: AiSettings;
|
|
localModels: LocalModelOption[];
|
|
onSave: (settings: AiSettings) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
let { settings, localModels = [], onSave, onClose }: Props = $props();
|
|
|
|
type CloudProvider = Exclude<CommitAiProvider, "local">;
|
|
|
|
const CRED_KEYS: Record<CloudProvider, string> = {
|
|
openai: "ai:openai",
|
|
anthropic: "ai:anthropic",
|
|
custom: "ai:custom",
|
|
};
|
|
|
|
let provider = $state<CommitAiProvider>("local");
|
|
let localModelId = $state("");
|
|
let openaiModel = $state("");
|
|
let anthropicModel = $state("");
|
|
let customBaseUrl = $state("");
|
|
let customModel = $state("");
|
|
|
|
let openaiApiKey = $state("");
|
|
let anthropicApiKey = $state("");
|
|
let customApiKey = $state("");
|
|
let showKey = $state(false);
|
|
let loadingKeys = $state(true);
|
|
let saving = $state(false);
|
|
let error = $state("");
|
|
|
|
$effect(() => {
|
|
provider = settings.provider;
|
|
localModelId = settings.localModelId;
|
|
openaiModel = settings.openaiModel;
|
|
anthropicModel = settings.anthropicModel;
|
|
customBaseUrl = settings.customBaseUrl;
|
|
customModel = settings.customModel;
|
|
});
|
|
|
|
onMount(() => {
|
|
(async () => {
|
|
try {
|
|
const [openai, anthropic, custom] = await Promise.all([
|
|
credLoad(CRED_KEYS.openai),
|
|
credLoad(CRED_KEYS.anthropic),
|
|
credLoad(CRED_KEYS.custom),
|
|
]);
|
|
openaiApiKey = openai?.password ?? "";
|
|
anthropicApiKey = anthropic?.password ?? "";
|
|
customApiKey = custom?.password ?? "";
|
|
} catch (err) {
|
|
error = err instanceof Error ? err.message : String(err);
|
|
} finally {
|
|
loadingKeys = false;
|
|
}
|
|
})();
|
|
});
|
|
|
|
async function persistKey(target: CloudProvider, value: string) {
|
|
const key = CRED_KEYS[target];
|
|
const trimmed = value.trim();
|
|
if (trimmed) {
|
|
await credSave(key, "api-key", trimmed, null);
|
|
} else {
|
|
await credDelete(key);
|
|
}
|
|
}
|
|
|
|
async function handleSave() {
|
|
saving = true;
|
|
error = "";
|
|
try {
|
|
await Promise.all([
|
|
persistKey("openai", openaiApiKey),
|
|
persistKey("anthropic", anthropicApiKey),
|
|
persistKey("custom", customApiKey),
|
|
]);
|
|
onSave({
|
|
provider,
|
|
localModelId,
|
|
openaiModel: openaiModel.trim() || "gpt-4o-mini",
|
|
anthropicModel: anthropicModel.trim() || "claude-3-5-haiku-latest",
|
|
customBaseUrl: customBaseUrl.trim(),
|
|
customModel: customModel.trim(),
|
|
});
|
|
} catch (err) {
|
|
error = err instanceof Error ? err.message : String(err);
|
|
} finally {
|
|
saving = false;
|
|
}
|
|
}
|
|
|
|
function formatSize(mb: number): string {
|
|
return mb >= 1000 ? `${(mb / 1000).toFixed(1)} GB` : `${mb} MB`;
|
|
}
|
|
|
|
let selectedLocalModel = $derived(localModels.find((option) => option.id === localModelId));
|
|
</script>
|
|
|
|
<div
|
|
class="dialog-backdrop"
|
|
role="presentation"
|
|
onclick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
|
>
|
|
<div class="dialog ai-settings-dialog" role="dialog" aria-modal="true" aria-label="AI settings" tabindex="-1">
|
|
<header class="dialog-header">
|
|
<div>
|
|
<span class="eyebrow">Commit AI</span>
|
|
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">AI settings</h2>
|
|
</div>
|
|
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
|
<X size={18} aria-hidden="true" />
|
|
</button>
|
|
</header>
|
|
|
|
<form class="ai-settings-form" onsubmit={(e) => { e.preventDefault(); void handleSave(); }}>
|
|
<div class="ai-provider-options" role="radiogroup" aria-label="AI provider">
|
|
<button type="button" class="ai-provider-option" class:active={provider === "local"} onclick={() => { provider = "local"; }}>
|
|
<Cpu size={16} aria-hidden="true" />
|
|
Local AI
|
|
</button>
|
|
<button type="button" class="ai-provider-option" class:active={provider === "openai"} onclick={() => { provider = "openai"; }}>
|
|
<Bot size={16} aria-hidden="true" />
|
|
OpenAI
|
|
</button>
|
|
<button type="button" class="ai-provider-option" class:active={provider === "anthropic"} onclick={() => { provider = "anthropic"; }}>
|
|
<Bot size={16} aria-hidden="true" />
|
|
Anthropic (Claude)
|
|
</button>
|
|
<button type="button" class="ai-provider-option" class:active={provider === "custom"} onclick={() => { provider = "custom"; }}>
|
|
<Globe size={16} aria-hidden="true" />
|
|
Custom endpoint
|
|
</button>
|
|
</div>
|
|
|
|
{#if provider === "local"}
|
|
<label class="cred-field">
|
|
<span class="cred-field-label">Model</span>
|
|
<select bind:value={localModelId}>
|
|
{#each localModels as option (option.id)}
|
|
<option value={option.id}>{option.label} — {formatSize(option.approx_size_mb)}</option>
|
|
{/each}
|
|
</select>
|
|
</label>
|
|
<div class="cred-token-hint">
|
|
<AlertCircle size={13} aria-hidden="true" />
|
|
<span>
|
|
Switching downloads the model{selectedLocalModel ? ` (${formatSize(selectedLocalModel.approx_size_mb)})` : ""}
|
|
in the background — depending on your internet connection this can take several minutes.
|
|
After that it stays cached locally and loads instantly on the next start.
|
|
</span>
|
|
</div>
|
|
{:else if provider === "openai"}
|
|
<label class="cred-field">
|
|
<span class="cred-field-label">Model</span>
|
|
<input type="text" bind:value={openaiModel} placeholder="gpt-4o-mini" autocomplete="off" spellcheck="false" />
|
|
</label>
|
|
<div class="cred-field">
|
|
<span class="cred-field-label">API key</span>
|
|
<div class="cred-input">
|
|
<Key size={15} class="cred-field-icon" aria-hidden="true" />
|
|
<input
|
|
type={showKey ? "text" : "password"}
|
|
bind:value={openaiApiKey}
|
|
placeholder="sk-..."
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
disabled={loadingKeys}
|
|
/>
|
|
<button type="button" class="cred-reveal" tabindex="-1" onclick={() => { showKey = !showKey; }} aria-label={showKey ? "Hide" : "Show"}>
|
|
{#if showKey}<EyeOff size={14} aria-hidden="true" />{:else}<Eye size={14} aria-hidden="true" />{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{:else if provider === "anthropic"}
|
|
<label class="cred-field">
|
|
<span class="cred-field-label">Model</span>
|
|
<input type="text" bind:value={anthropicModel} placeholder="claude-3-5-haiku-latest" autocomplete="off" spellcheck="false" />
|
|
</label>
|
|
<div class="cred-field">
|
|
<span class="cred-field-label">API key</span>
|
|
<div class="cred-input">
|
|
<Key size={15} class="cred-field-icon" aria-hidden="true" />
|
|
<input
|
|
type={showKey ? "text" : "password"}
|
|
bind:value={anthropicApiKey}
|
|
placeholder="sk-ant-..."
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
disabled={loadingKeys}
|
|
/>
|
|
<button type="button" class="cred-reveal" tabindex="-1" onclick={() => { showKey = !showKey; }} aria-label={showKey ? "Hide" : "Show"}>
|
|
{#if showKey}<EyeOff size={14} aria-hidden="true" />{:else}<Eye size={14} aria-hidden="true" />{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<label class="cred-field">
|
|
<span class="cred-field-label">Endpoint URL</span>
|
|
<input type="text" bind:value={customBaseUrl} placeholder="http://localhost:11434/v1" autocomplete="off" spellcheck="false" />
|
|
</label>
|
|
<label class="cred-field">
|
|
<span class="cred-field-label">Model</span>
|
|
<input type="text" bind:value={customModel} placeholder="llama3.1" autocomplete="off" spellcheck="false" />
|
|
</label>
|
|
<div class="cred-field">
|
|
<span class="cred-field-label">API key (optional)</span>
|
|
<div class="cred-input">
|
|
<Key size={15} class="cred-field-icon" aria-hidden="true" />
|
|
<input
|
|
type={showKey ? "text" : "password"}
|
|
bind:value={customApiKey}
|
|
placeholder="Optional"
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
disabled={loadingKeys}
|
|
/>
|
|
<button type="button" class="cred-reveal" tabindex="-1" onclick={() => { showKey = !showKey; }} aria-label={showKey ? "Hide" : "Show"}>
|
|
{#if showKey}<EyeOff size={14} aria-hidden="true" />{:else}<Eye size={14} aria-hidden="true" />{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="cred-token-hint">
|
|
<Globe size={13} aria-hidden="true" />
|
|
<span>For local OpenAI-compatible servers like Ollama or LM Studio. The base URL should end in /v1.</span>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if error}
|
|
<p class="commit-block-reason">{error}</p>
|
|
{/if}
|
|
|
|
<div class="new-branch-actions">
|
|
<button class="btn-secondary" type="button" onclick={onClose} disabled={saving}>
|
|
Cancel
|
|
</button>
|
|
<button class="btn-primary" type="submit" disabled={saving || loadingKeys}>
|
|
{#if saving}
|
|
<LoaderCircle class="spin" size={16} aria-hidden="true" />
|
|
{:else}
|
|
<Check size={16} aria-hidden="true" />
|
|
{/if}
|
|
Save
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|