feat(i18n): convert Settings AI section to t() (Batch E)
This commit is contained in:
1 parent
377a91d1b8
commit
a0ad1564a8
1 file changed
+58
-49
@@ -1442,52 +1442,59 @@
|
|||||||
</section>
|
</section>
|
||||||
{:else if section === "ai"}
|
{:else if section === "ai"}
|
||||||
<section>
|
<section>
|
||||||
<h3>AI summary provider</h3>
|
<h3>{t("settings.ai.title")}</h3>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Summaries run on a local LLM by default. Point this at Ollama on this PC or another
|
{t("settings.ai.intro_1")} <code>192.168.0.x</code>) {t("settings.ai.intro_2")}
|
||||||
machine on your LAN (e.g. <code>192.168.0.x</code>) — both count as local, so nothing
|
|
||||||
leaves your network. Hosted providers (Anthropic) are optional, off by default, and send
|
|
||||||
the transcript to a third party once you turn one on.
|
|
||||||
</p>
|
</p>
|
||||||
<label
|
<label
|
||||||
>Provider
|
>{t("settings.ai.provider")}
|
||||||
<select bind:value={llmProvider}>
|
<select bind:value={llmProvider}>
|
||||||
<option value="off">Off</option>
|
<option value="off">{t("settings.ai.off")}</option>
|
||||||
<option value="ollama">Ollama (local / LAN)</option>
|
<option value="ollama">{t("settings.ai.provider_ollama")}</option>
|
||||||
<option value="custom">Custom (OpenAI-compatible)</option>
|
<option value="custom">{t("settings.ai.provider_custom")}</option>
|
||||||
<option value="anthropic">Anthropic (Claude) — hosted, leaves this device</option>
|
<option value="anthropic">{t("settings.ai.provider_anthropic")}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{#if llmProvider !== "off"}
|
{#if llmProvider !== "off"}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#if llmProvider !== "anthropic"}
|
{#if llmProvider !== "anthropic"}
|
||||||
<label class="wide"
|
<label class="wide"
|
||||||
>Endpoint<input
|
>{t("settings.ai.endpoint")}<input
|
||||||
bind:value={llmEndpoint}
|
bind:value={llmEndpoint}
|
||||||
placeholder="http://192.168.0.42:11434"
|
placeholder="http://192.168.0.42:11434"
|
||||||
/></label
|
/></label
|
||||||
>
|
>
|
||||||
<label>Model<input bind:value={llmModel} placeholder="llama3.1" /></label>
|
<label
|
||||||
|
>{t("settings.ai.model")}<input
|
||||||
|
bind:value={llmModel}
|
||||||
|
placeholder="llama3.1"
|
||||||
|
/></label
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<label class="wide"
|
<label class="wide"
|
||||||
>Model<input bind:value={llmModel} placeholder="claude-3-5-sonnet-latest" /></label
|
>{t("settings.ai.model")}<input
|
||||||
|
bind:value={llmModel}
|
||||||
|
placeholder="claude-3-5-sonnet-latest"
|
||||||
|
/></label
|
||||||
>
|
>
|
||||||
<label class="wide"
|
<label class="wide"
|
||||||
>API key
|
>{t("settings.ai.api_key")}
|
||||||
<div class="key-input">
|
<div class="key-input">
|
||||||
<input
|
<input
|
||||||
type={showApiKey ? "text" : "password"}
|
type={showApiKey ? "text" : "password"}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
bind:value={llmApiKey}
|
bind:value={llmApiKey}
|
||||||
placeholder={settings.llmStatus?.provider === "anthropic"
|
placeholder={settings.llmStatus?.provider === "anthropic"
|
||||||
? "•••••••••••••••• (already set — leave blank to keep it)"
|
? t("settings.ai.api_key_set_placeholder")
|
||||||
: "sk-ant-…"}
|
: "sk-ant-…"}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="icon-toggle"
|
class="icon-toggle"
|
||||||
onclick={() => (showApiKey = !showApiKey)}
|
onclick={() => (showApiKey = !showApiKey)}
|
||||||
aria-label={showApiKey ? "Hide API key" : "Show API key"}
|
aria-label={showApiKey
|
||||||
|
? t("settings.ai.hide_api_key")
|
||||||
|
: t("settings.ai.show_api_key")}
|
||||||
>
|
>
|
||||||
{#if showApiKey}<EyeOff size={14} aria-hidden="true" />{:else}<Eye
|
{#if showApiKey}<EyeOff size={14} aria-hidden="true" />{:else}<Eye
|
||||||
size={14}
|
size={14}
|
||||||
@@ -1501,26 +1508,27 @@
|
|||||||
{#if llmProvider === "anthropic"}
|
{#if llmProvider === "anthropic"}
|
||||||
<div class="banner hosted">
|
<div class="banner hosted">
|
||||||
<Globe size={14} aria-hidden="true" />
|
<Globe size={14} aria-hidden="true" />
|
||||||
Anthropic is a hosted, third-party service — this meeting's transcript leaves your device
|
{t("settings.ai.anthropic_banner")}
|
||||||
when you generate a summary.
|
|
||||||
</div>
|
</div>
|
||||||
{:else if llmEndpoint && !endpointIsLocalOrLan(llmEndpoint)}
|
{:else if llmEndpoint && !endpointIsLocalOrLan(llmEndpoint)}
|
||||||
<div class="banner">
|
<div class="banner">
|
||||||
<AlertTriangle size={14} aria-hidden="true" />
|
<AlertTriangle size={14} aria-hidden="true" />
|
||||||
This endpoint isn't on your machine or LAN — your transcript would leave your network.
|
{t("settings.ai.endpoint_banner")}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="primary" onclick={onSaveLlmClick} disabled={settings.llmSaving}>
|
<button class="primary" onclick={onSaveLlmClick} disabled={settings.llmSaving}>
|
||||||
{settings.llmSaving ? "Saving…" : "Save"}
|
{settings.llmSaving ? t("settings.ai.saving") : t("settings.ai.save")}
|
||||||
</button>
|
</button>
|
||||||
<button class="ghost" onclick={() => settings.loadLlmStatus()}>
|
<button class="ghost" onclick={() => settings.loadLlmStatus()}>
|
||||||
<RefreshCw size={14} aria-hidden="true" /> Test connection
|
<RefreshCw size={14} aria-hidden="true" />
|
||||||
|
{t("settings.ai.test_connection")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if showHostedBanner}
|
{#if showHostedBanner}
|
||||||
<HostedAiBanner
|
<HostedAiBanner
|
||||||
providerLabel={HOSTED_PROVIDER_LABELS[llmProvider] ?? "this hosted provider"}
|
providerLabel={HOSTED_PROVIDER_LABELS[llmProvider] ??
|
||||||
|
t("settings.ai.this_hosted_provider")}
|
||||||
onAccept={acceptHostedBannerAndSave}
|
onAccept={acceptHostedBannerAndSave}
|
||||||
onCancel={() => (showHostedBanner = false)}
|
onCancel={() => (showHostedBanner = false)}
|
||||||
/>
|
/>
|
||||||
@@ -1529,13 +1537,17 @@
|
|||||||
{@const s = settings.llmStatus}
|
{@const s = settings.llmStatus}
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<span class="status-dot" class:ok={s.reachable} class:bad={!s.reachable}></span>
|
<span class="status-dot" class:ok={s.reachable} class:bad={!s.reachable}></span>
|
||||||
<span class="status-text">{s.reachable ? "Reachable" : "Unreachable"}</span>
|
<span class="status-text"
|
||||||
|
>{s.reachable ? t("settings.ai.reachable") : t("settings.ai.unreachable")}</span
|
||||||
|
>
|
||||||
{#if s.reachable}
|
{#if s.reachable}
|
||||||
<span class="muted"
|
<span class="muted"
|
||||||
>· {s.models.length} model{s.models.length === 1 ? "" : "s"}</span
|
>{s.models.length === 1
|
||||||
|
? t("settings.ai.model_count_one", { n: s.models.length })
|
||||||
|
: t("settings.ai.model_count_many", { n: s.models.length })}</span
|
||||||
>
|
>
|
||||||
<span class="muted" class:warn={!s.isLocal}
|
<span class="muted" class:warn={!s.isLocal}
|
||||||
>· {s.isLocal ? "on your machine / LAN" : "leaves your network"}</span
|
>{s.isLocal ? t("settings.ai.on_lan") : t("settings.ai.leaves_network")}</span
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1554,7 +1566,7 @@
|
|||||||
<span class="param-name">{opt.label}</span>
|
<span class="param-name">{opt.label}</span>
|
||||||
<span class="param-help"
|
<span class="param-help"
|
||||||
>{opt.help}{#if opt.default !== undefined}
|
>{opt.help}{#if opt.default !== undefined}
|
||||||
Default {opt.default}.{/if}</span
|
{t("settings.ai.default_value", { value: String(opt.default) })}{/if}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="param-control">
|
<div class="param-control">
|
||||||
@@ -1594,8 +1606,8 @@
|
|||||||
<button
|
<button
|
||||||
class="reset-field"
|
class="reset-field"
|
||||||
class:hidden={!isOverridden(opt)}
|
class:hidden={!isOverridden(opt)}
|
||||||
title="Reset to default"
|
title={t("settings.ai.reset_default")}
|
||||||
aria-label="Reset {opt.label}"
|
aria-label={t("settings.ai.reset_field", { name: opt.label })}
|
||||||
onclick={() => commit(opt.key, undefined)}
|
onclick={() => commit(opt.key, undefined)}
|
||||||
>
|
>
|
||||||
<RotateCcw size={13} aria-hidden="true" />
|
<RotateCcw size={13} aria-hidden="true" />
|
||||||
@@ -1607,29 +1619,27 @@
|
|||||||
<details class="advanced">
|
<details class="advanced">
|
||||||
<summary>
|
<summary>
|
||||||
<ChevronRight size={15} aria-hidden="true" class="chevron" />
|
<ChevronRight size={15} aria-hidden="true" class="chevron" />
|
||||||
<span>Advanced Ollama configuration</span>
|
<span>{t("settings.ai.advanced_title")}</span>
|
||||||
{#if advChangedCount}<span class="changed-badge">{advChangedCount} changed</span
|
{#if advChangedCount}<span class="changed-badge"
|
||||||
|
>{t("settings.ai.changed", { n: advChangedCount })}</span
|
||||||
>{/if}
|
>{/if}
|
||||||
</summary>
|
</summary>
|
||||||
<div class="adv-body">
|
<div class="adv-body">
|
||||||
{#if advChangedCount}
|
{#if advChangedCount}
|
||||||
<button class="link reset-all" onclick={resetAllAdvanced}
|
<button class="link reset-all" onclick={resetAllAdvanced}
|
||||||
>Reset all to defaults</button
|
>{t("settings.ai.reset_all")}</button
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<label class="wide adv-system"
|
<label class="wide adv-system"
|
||||||
>System prompt
|
>{t("settings.ai.system_prompt")}
|
||||||
<textarea
|
<textarea
|
||||||
rows="3"
|
rows="3"
|
||||||
bind:value={advSystem}
|
bind:value={advSystem}
|
||||||
placeholder="e.g. You are a concise meeting summarizer."
|
placeholder={t("settings.ai.system_prompt_placeholder")}
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
<p class="muted small">
|
<p class="muted small">{t("settings.ai.system_prompt_hint")}</p>
|
||||||
Added before WhispAssist's required output format, so your instructions can't
|
|
||||||
break summary/action-item parsing.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{#each OPTION_GROUPS as g (g)}
|
{#each OPTION_GROUPS as g (g)}
|
||||||
<fieldset class="adv-group">
|
<fieldset class="adv-group">
|
||||||
@@ -1637,21 +1647,19 @@
|
|||||||
{#if g === "context"}
|
{#if g === "context"}
|
||||||
<div class="param">
|
<div class="param">
|
||||||
<div class="param-label">
|
<div class="param-label">
|
||||||
<span class="param-name">Think</span>
|
<span class="param-name">{t("settings.ai.think")}</span>
|
||||||
<span class="param-help">Reasoning effort (reasoning models only).</span>
|
<span class="param-help">{t("settings.ai.think_help")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="param-control">
|
<div class="param-control">
|
||||||
<select bind:value={advThink}>
|
<select bind:value={advThink}>
|
||||||
{#each THINK_OPTIONS as t (t)}<option value={t}>{t}</option>{/each}
|
{#each THINK_OPTIONS as th (th)}<option value={th}>{th}</option>{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="param">
|
<div class="param">
|
||||||
<div class="param-label">
|
<div class="param-label">
|
||||||
<span class="param-name">Keep alive</span>
|
<span class="param-name">{t("settings.ai.keep_alive")}</span>
|
||||||
<span class="param-help"
|
<span class="param-help">{t("settings.ai.keep_alive_help")}</span>
|
||||||
>How long the model stays in RAM. e.g. 5m, 1h, 0 (unload), -1 (forever).</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="param-control">
|
<div class="param-control">
|
||||||
<input bind:value={advKeepAlive} placeholder="5m" />
|
<input bind:value={advKeepAlive} placeholder="5m" />
|
||||||
@@ -1667,8 +1675,8 @@
|
|||||||
<details class="adv-nested">
|
<details class="adv-nested">
|
||||||
<summary>
|
<summary>
|
||||||
<ChevronRight size={14} aria-hidden="true" class="chevron" />
|
<ChevronRight size={14} aria-hidden="true" class="chevron" />
|
||||||
<span>Runtime & hardware</span>
|
<span>{t("settings.ai.runtime_hardware")}</span>
|
||||||
<span class="muted small">— rarely needed</span>
|
<span class="muted small">{t("settings.ai.rarely_needed")}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<fieldset class="adv-group">
|
<fieldset class="adv-group">
|
||||||
{#each OLLAMA_OPTIONS.filter((o) => o.group === "hardware") as opt (opt.key)}
|
{#each OLLAMA_OPTIONS.filter((o) => o.group === "hardware") as opt (opt.key)}
|
||||||
@@ -1679,10 +1687,10 @@
|
|||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="primary" onclick={saveAdvanced} disabled={advSaving}>
|
<button class="primary" onclick={saveAdvanced} disabled={advSaving}>
|
||||||
{advSaving ? "Saving…" : "Save advanced"}
|
{advSaving ? t("settings.ai.saving") : t("settings.ai.save_advanced")}
|
||||||
</button>
|
</button>
|
||||||
{#if advSaved}<span class="test ok"
|
{#if advSaved}<span class="test ok"
|
||||||
><Check size={14} aria-hidden="true" /> Saved</span
|
><Check size={14} aria-hidden="true" /> {t("settings.ai.saved")}</span
|
||||||
>{/if}
|
>{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1690,7 +1698,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="primary" onclick={saveLlm} disabled={settings.llmSaving}>Turn off</button
|
<button class="primary" onclick={saveLlm} disabled={settings.llmSaving}
|
||||||
|
>{t("settings.ai.turn_off")}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in new issue
Block a user