Feature chore bug 007 #21

Merged
iamdoubz merged 68 commits from feature_chore_bug_007 into main 2026-07-12 12:59:04 -05:00
Showing only changes of commit a0ad1564a8 - Show all commits
+58 -49
View File
@@ -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 &amp; 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}