From 8aa6ba59a77c799ef016e876f5f5e35306807a54 Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Sat, 4 Jul 2026 09:21:29 -0500 Subject: [PATCH] feat(ui): redesign AI status/actions + collapsible advanced Ollama config (T5.2) --- src/lib/views/Settings.svelte | 481 ++++++++++++++++++++++++++++++++-- 1 file changed, 463 insertions(+), 18 deletions(-) diff --git a/src/lib/views/Settings.svelte b/src/lib/views/Settings.svelte index 194543b..5bcddb2 100644 --- a/src/lib/views/Settings.svelte +++ b/src/lib/views/Settings.svelte @@ -21,7 +21,17 @@ UploadCloud, ShieldCheck, Sparkles, + RefreshCw, + ChevronRight, + RotateCcw, } from "@lucide/svelte"; + import { + OLLAMA_OPTIONS, + THINK_OPTIONS, + OLLAMA_GROUP_LABELS, + type OllamaOption, + type OllamaGroup, + } from "../llmParams"; let { onClose }: { onClose: () => void } = $props(); @@ -68,6 +78,93 @@ } } + // ---- Advanced Ollama configuration (sparse; only overrides are stored) ---- + const OPTION_GROUPS: OllamaGroup[] = ["sampling", "repetition", "mirostat", "context"]; + type OptVal = number | boolean | string[]; + let advSystem = $state(""); + let advThink = $state("off"); + let advKeepAlive = $state(""); + let advOptions = $state>({}); + let advSaving = $state(false); + let advSaved = $state(false); + $effect(() => { + // (Re)load the form whenever persisted settings change. + const a = settings.settings.llm_advanced ?? {}; + advSystem = a.system ?? ""; + advThink = a.think ?? "off"; + advKeepAlive = a.keep_alive ?? ""; + advOptions = { ...(a.options ?? {}) }; + }); + let advChangedCount = $derived( + Object.keys(advOptions).length + + (advSystem.trim() ? 1 : 0) + + (advThink !== "off" ? 1 : 0) + + (advKeepAlive.trim() ? 1 : 0), + ); + function mirostatOff(): boolean { + const m = advOptions["mirostat"]; + return (m === undefined ? 0 : (m as number)) === 0; + } + function optDisplay(opt: OllamaOption): string { + const v = advOptions[opt.key]; + if (opt.kind === "tags") return Array.isArray(v) ? v.join(", ") : ""; + if (v !== undefined) return String(v); + return opt.default !== undefined ? String(opt.default) : ""; + } + function isOverridden(opt: OllamaOption): boolean { + return opt.key in advOptions; + } + function commit(key: string, value: OptVal | undefined) { + const next = { ...advOptions }; + if (value === undefined) delete next[key]; + else next[key] = value; + advOptions = next; + advSaved = false; + } + function setNumber(opt: OllamaOption, raw: string) { + const t = raw.trim(); + if (t === "") return commit(opt.key, undefined); + const n = Number(t); + if (Number.isNaN(n)) return; + commit(opt.key, opt.default !== undefined && n === opt.default ? undefined : n); + } + function setEnum(opt: OllamaOption, raw: string) { + const n = Number(raw); + commit(opt.key, opt.default !== undefined && n === opt.default ? undefined : n); + } + function setBool(opt: OllamaOption, checked: boolean) { + commit(opt.key, opt.default !== undefined && checked === opt.default ? undefined : checked); + } + function setTags(opt: OllamaOption, raw: string) { + const arr = raw + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + commit(opt.key, arr.length ? arr : undefined); + } + function resetAllAdvanced() { + advSystem = ""; + advThink = "off"; + advKeepAlive = ""; + advOptions = {}; + advSaved = false; + } + async function saveAdvanced() { + advSaving = true; + advSaved = false; + const adv: NonNullable<(typeof settings.settings)["llm_advanced"]> = {}; + if (advSystem.trim()) adv.system = advSystem.trim(); + if (advThink !== "off") adv.think = advThink; + if (advKeepAlive.trim()) adv.keep_alive = advKeepAlive.trim(); + if (Object.keys(advOptions).length) adv.options = advOptions; + try { + await settings.patch({ llm_advanced: Object.keys(adv).length ? adv : null }); + advSaved = true; + } finally { + advSaving = false; + } + } + // ---- Calendar / .pst import (T6.1/T6.2/T6.3, FR-CAL-1/2) ---- let pstPath = $state(""); let pstPassword = $state(""); @@ -698,25 +795,172 @@ - - {#if settings.llmStatus} - - {#if settings.llmStatus.reachable} - {/if} + - {#if settings.llmStatus?.reachable && settings.llmStatus.models.length} -

Models: {settings.llmStatus.models.slice(0, 6).join(", ")}

+ {#if settings.llmStatus} + {@const s = settings.llmStatus} +
+ + {s.reachable ? "Reachable" : "Unreachable"} + {#if s.reachable} + · {s.models.length} model{s.models.length === 1 ? "" : "s"} + · {s.isLocal ? "on your machine / LAN" : "leaves your network"} + {/if} +
+ {#if s.reachable && s.models.length} +

+ {s.models.slice(0, 6).join(", ")}{s.models.length > 6 ? " …" : ""} +

+ {/if} + {/if} + + {#if llmProvider === "ollama"} + {#snippet paramRow(opt: OllamaOption)} + {@const disabled = opt.needsMirostat && mirostatOff()} +
+
+ {opt.label} + {opt.help}{#if opt.default !== undefined} + Default {opt.default}.{/if} +
+
+ {#if opt.kind === "bool"} + setBool(opt, e.currentTarget.checked)} + /> + {:else if opt.kind === "enum"} + + {:else if opt.kind === "tags"} + setTags(opt, e.currentTarget.value)} + /> + {:else} + setNumber(opt, e.currentTarget.value)} + /> + {/if} + +
+
+ {/snippet} + +
+ + +
+ {#if advChangedCount} + + {/if} + + +

+ Added before WhispAssist's required output format, so your instructions can't + break summary/action-item parsing. +

+ + {#each OPTION_GROUPS as g (g)} +
+ {OLLAMA_GROUP_LABELS[g]} + {#if g === "context"} +
+
+ Think + Reasoning effort (reasoning models only). +
+
+ +
+
+
+
+ Keep alive + How long the model stays in RAM. e.g. 5m, 1h, 0 (unload), -1 (forever). +
+
+ +
+
+ {/if} + {#each OLLAMA_OPTIONS.filter((o) => o.group === g) as opt (opt.key)} + {@render paramRow(opt)} + {/each} +
+ {/each} + +
+ + +
+ {#each OLLAMA_OPTIONS.filter((o) => o.group === "hardware") as opt (opt.key)} + {@render paramRow(opt)} + {/each} +
+
+ +
+ + {#if advSaved}{/if} +
+
+
{/if} {:else}
@@ -1153,4 +1397,205 @@ ul.hosts li { padding: 0.2rem 0; } + + /* ---- AI provider: status + advanced config ---- */ + .ghost { + display: inline-flex; + align-items: center; + gap: 0.35rem; + background: transparent; + border: 1px solid var(--border); + color: var(--fg, inherit); + } + .ghost:hover { + background: var(--hover, rgba(127, 127, 127, 0.08)); + } + .status-card { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.6rem; + padding: 0.5rem 0.7rem; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 0.85rem; + } + .status-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--muted); + flex: none; + } + .status-dot.ok { + background: var(--success, #16a34a); + } + .status-dot.bad { + background: var(--danger, #dc2626); + } + .status-text { + font-weight: 600; + } + .muted.warn { + color: var(--danger, #dc2626); + } + + .advanced, + .adv-nested { + margin-top: 0.9rem; + border: 1px solid var(--border); + border-radius: 8px; + } + .adv-nested { + margin-top: 0.6rem; + } + .advanced > summary, + .adv-nested > summary { + display: flex; + align-items: center; + gap: 0.45rem; + padding: 0.6rem 0.75rem; + cursor: pointer; + list-style: none; + font-weight: 600; + user-select: none; + } + .advanced > summary::-webkit-details-marker, + .adv-nested > summary::-webkit-details-marker { + display: none; + } + .advanced :global(.chevron), + .adv-nested :global(.chevron) { + transition: transform 0.18s ease; + flex: none; + color: var(--muted); + } + .advanced[open] > summary :global(.chevron), + .adv-nested[open] > summary :global(.chevron) { + transform: rotate(90deg); + } + @media (prefers-reduced-motion: reduce) { + .advanced :global(.chevron), + .adv-nested :global(.chevron) { + transition: none; + } + } + .changed-badge { + margin-left: auto; + font-size: 0.7rem; + font-weight: 500; + padding: 0.1rem 0.45rem; + border-radius: 10px; + background: var(--accent, #2563eb); + color: var(--accent-fg, #fff); + } + .adv-body { + padding: 0 0.75rem 0.85rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + } + .reset-all { + align-self: flex-end; + font-size: 0.78rem; + } + .adv-system textarea { + width: 100%; + resize: vertical; + font-family: inherit; + font-size: 0.85rem; + padding: 0.4rem 0.5rem; + border: 1px solid var(--border-strong, var(--border)); + border-radius: 6px; + } + .adv-group { + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.4rem 0.75rem 0.6rem; + margin: 0; + display: flex; + flex-direction: column; + } + .adv-group legend { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted); + padding: 0 0.35rem; + } + .param { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.4rem 0; + border-bottom: 1px solid var(--border); + } + .param:last-child { + border-bottom: none; + } + .param-label { + display: flex; + flex-direction: column; + gap: 0.1rem; + min-width: 0; + flex: 1; + } + .param-name { + font-size: 0.85rem; + font-weight: 500; + } + .param-help { + font-size: 0.74rem; + color: var(--muted); + line-height: 1.35; + } + .param-control { + display: flex; + align-items: center; + gap: 0.3rem; + flex: none; + } + .param-control input[type="number"], + .param-control select { + width: 6.5rem; + text-align: right; + font-variant-numeric: tabular-nums; + padding: 0.3rem 0.4rem; + border: 1px solid var(--border-strong, var(--border)); + border-radius: 6px; + } + .param-control select { + text-align: left; + } + .param-control input:not([type="number"]):not([type="checkbox"]) { + width: 9rem; + padding: 0.3rem 0.4rem; + border: 1px solid var(--border-strong, var(--border)); + border-radius: 6px; + } + .param-disabled { + opacity: 0.45; + } + .reset-field { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.6rem; + height: 1.6rem; + padding: 0; + border: none; + background: transparent; + color: var(--muted); + cursor: pointer; + border-radius: 5px; + } + .reset-field:hover { + background: var(--hover, rgba(127, 127, 127, 0.1)); + color: var(--fg, inherit); + } + .reset-field.hidden { + visibility: hidden; + }