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