feat(ui): AI settings section — pick provider, set LAN/local endpoint + model, test (FR-LLM-1)
This commit is contained in:
1 parent
df7ea29c67
commit
4fc271dd1a
1 file changed
+108
-3
@@ -20,13 +20,53 @@
|
||||
CalendarDays,
|
||||
UploadCloud,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
} from "@lucide/svelte";
|
||||
|
||||
let { onClose }: { onClose: () => void } = $props();
|
||||
|
||||
let section = $state<"recording" | "hardware" | "storage" | "calendar" | "sync" | "privacy">(
|
||||
"recording",
|
||||
);
|
||||
let section = $state<
|
||||
"recording" | "hardware" | "storage" | "calendar" | "sync" | "ai" | "privacy"
|
||||
>("recording");
|
||||
|
||||
// ---- AI summary provider (T5.2, FR-LLM-1) ----
|
||||
let llmProvider = $state(settings.settings.llm_provider);
|
||||
let llmEndpoint = $state(settings.settings.llm_endpoint);
|
||||
let llmModel = $state(settings.settings.llm_model);
|
||||
$effect(() => {
|
||||
// Re-sync the form when settings (re)load or are saved elsewhere.
|
||||
llmProvider = settings.settings.llm_provider;
|
||||
llmEndpoint = settings.settings.llm_endpoint;
|
||||
llmModel = settings.settings.llm_model;
|
||||
});
|
||||
async function saveLlm() {
|
||||
await settings.setLlmProvider({
|
||||
provider: llmProvider,
|
||||
endpoint: llmEndpoint || undefined,
|
||||
model: llmModel || undefined,
|
||||
});
|
||||
}
|
||||
/** Client-side mirror of the backend's loopback+private-LAN check, for a live
|
||||
* "leaves your network" hint while typing. */
|
||||
function endpointIsLocalOrLan(url: string): boolean {
|
||||
try {
|
||||
const h = new URL(url).hostname.replace(/^\[|\]$/g, "");
|
||||
if (h === "localhost" || h.toLowerCase().endsWith(".local") || h === "::1") return true;
|
||||
const m = h.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!m) return false;
|
||||
const a = Number(m[1]);
|
||||
const b = Number(m[2]);
|
||||
return (
|
||||
a === 127 ||
|
||||
a === 10 ||
|
||||
(a === 192 && b === 168) ||
|
||||
(a === 172 && b >= 16 && b <= 31) ||
|
||||
(a === 169 && b === 254)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Calendar / .pst import (T6.1/T6.2/T6.3, FR-CAL-1/2) ----
|
||||
let pstPath = $state("");
|
||||
@@ -246,6 +286,9 @@
|
||||
<button class:active={section === "sync"} onclick={() => (section = "sync")}>
|
||||
<UploadCloud size={14} aria-hidden="true" /> Sync
|
||||
</button>
|
||||
<button class:active={section === "ai"} onclick={() => (section = "ai")}>
|
||||
<Sparkles size={14} aria-hidden="true" /> AI
|
||||
</button>
|
||||
<button class:active={section === "privacy"} onclick={() => (section = "privacy")}>
|
||||
<ShieldCheck size={14} aria-hidden="true" /> Privacy
|
||||
</button>
|
||||
@@ -620,6 +663,68 @@
|
||||
TLS is required for non-LAN targets.
|
||||
</p>
|
||||
</section>
|
||||
{:else if section === "ai"}
|
||||
<section>
|
||||
<h3>AI summary provider</h3>
|
||||
<p class="muted">
|
||||
Summaries run on a local LLM. 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.
|
||||
</p>
|
||||
<label
|
||||
>Provider
|
||||
<select bind:value={llmProvider}>
|
||||
<option value="off">Off</option>
|
||||
<option value="ollama">Ollama (local / LAN)</option>
|
||||
<option value="custom">Custom (OpenAI-compatible)</option>
|
||||
</select>
|
||||
</label>
|
||||
{#if llmProvider !== "off"}
|
||||
<div class="grid">
|
||||
<label class="wide"
|
||||
>Endpoint<input
|
||||
bind:value={llmEndpoint}
|
||||
placeholder="http://192.168.0.42:11434"
|
||||
/></label
|
||||
>
|
||||
<label>Model<input bind:value={llmModel} placeholder="llama3.1" /></label>
|
||||
</div>
|
||||
{#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.
|
||||
</div>
|
||||
{/if}
|
||||
<div class="actions">
|
||||
<button class="primary" onclick={saveLlm} disabled={settings.llmSaving}>
|
||||
{settings.llmSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
<button onclick={() => settings.loadLlmStatus()}>Test / refresh</button>
|
||||
{#if settings.llmStatus}
|
||||
<span
|
||||
class="test"
|
||||
class:ok={settings.llmStatus.reachable}
|
||||
class:fail={!settings.llmStatus.reachable}
|
||||
>
|
||||
{#if settings.llmStatus.reachable}<Check size={14} aria-hidden="true" />{:else}<X
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>{/if}
|
||||
{settings.llmStatus.reachable ? "reachable" : "unreachable"}
|
||||
{#if settings.llmStatus.reachable && !settings.llmStatus.isLocal}· leaves your
|
||||
network{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if settings.llmStatus?.reachable && settings.llmStatus.models.length}
|
||||
<p class="muted">Models: {settings.llmStatus.models.slice(0, 6).join(", ")}</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="actions">
|
||||
<button class="primary" onclick={saveLlm} disabled={settings.llmSaving}>Turn off</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{:else}
|
||||
<section>
|
||||
<h3>Privacy</h3>
|
||||
|
||||
Reference in new issue
Block a user