|
30 | 30 | crisisScan, |
31 | 31 | emergencyResources, |
32 | 32 | companionChat, |
33 | | - ollamaModels, |
| 33 | + aiStatus, |
| 34 | + aiStart, |
34 | 35 | pwLookup, |
35 | 36 | knowledgeSearch, |
36 | 37 | type ExperienceSummary, |
|
44 | 45 | type ChatMsg, |
45 | 46 | type PwInfo, |
46 | 47 | type KnowledgeHit, |
| 48 | + type AiStatus, |
47 | 49 | } from "$lib/api"; |
48 | 50 | import { captureToken, hasToken, inTauri } from "$lib/portal"; |
49 | 51 |
|
|
95 | 97 | let newSub = $state(""); |
96 | 98 |
|
97 | 99 | // companion |
| 100 | + let ai = $state<AiStatus | null>(null); |
98 | 101 | let models = $state<string[]>([]); |
99 | 102 | let model = $state(""); |
100 | 103 | let chat = $state<ChatMsg[]>([]); |
101 | 104 | let ask = $state(""); |
102 | 105 | let thinking = $state(false); |
| 106 | + let waking = $state(false); |
103 | 107 |
|
104 | 108 | onMount(async () => { |
105 | 109 | captureToken(); |
106 | 110 | paired = inTauri() || hasToken(); |
107 | 111 | if (!paired) return; |
108 | 112 | await refresh(); |
| 113 | + await loadAi(); |
| 114 | + }); |
| 115 | +
|
| 116 | + /** Ask the desktop what its local model is doing *right now*. |
| 117 | + * |
| 118 | + * This used to run once, at mount. If Ollama happened to be asleep at that |
| 119 | + * moment, the Companion stayed dead for the life of the page — no retry, no |
| 120 | + * explanation — which is exactly what you don't want mid-session. So: re-check |
| 121 | + * whenever the Talk tab is opened, and say which of the three states we're in |
| 122 | + * rather than one vague "not reachable". */ |
| 123 | + async function loadAi() { |
109 | 124 | try { |
110 | | - models = await ollamaModels(); |
111 | | - model = models[0] ?? ""; |
| 125 | + ai = await aiStatus(); |
| 126 | + models = ai.models; |
| 127 | + if (!models.includes(model)) model = models[0] ?? ""; |
112 | 128 | } catch { |
113 | | - models = []; // The Companion needs the desktop's Ollama; it may simply be off. |
| 129 | + ai = null; |
| 130 | + models = []; |
114 | 131 | } |
115 | | - }); |
| 132 | + } |
| 133 | +
|
| 134 | + /** Wake the desktop's model server from the phone. The alternative is walking to |
| 135 | + * the desk, which is the situation the portal exists to avoid. */ |
| 136 | + const wakeAi = () => |
| 137 | + run(async () => { |
| 138 | + waking = true; |
| 139 | + try { |
| 140 | + await aiStart(); |
| 141 | + // `ollama serve` takes a moment to accept connections. |
| 142 | + await new Promise((r) => setTimeout(r, 1500)); |
| 143 | + await loadAi(); |
| 144 | + } finally { |
| 145 | + waking = false; |
| 146 | + } |
| 147 | + }); |
| 148 | +
|
| 149 | + function goTo(v: View) { |
| 150 | + view = v; |
| 151 | + if (v === "companion") loadAi(); |
| 152 | + } |
116 | 153 |
|
117 | 154 | /** Anything that fails does so out loud — a phone that silently drops a dose you |
118 | 155 | * thought you logged is worse than a phone that says it couldn't. */ |
|
555 | 592 | {#if view === "companion"} |
556 | 593 | <section class="pane"> |
557 | 594 | <h2>Companion</h2> |
558 | | - {#if !models.length} |
| 595 | + {#if !ai} |
| 596 | + <p class="muted">Couldn't ask the desktop about its local model.</p> |
| 597 | + <button disabled={busy} onclick={loadAi}>Try again</button> |
| 598 | + {:else if !ai.installed} |
559 | 599 | <p class="muted"> |
560 | | - The Companion runs on the desktop's local model, and it isn't reachable right now. |
561 | | - Everything else on this screen still works. |
| 600 | + The Companion runs on a local model, and this desktop doesn't have one installed. |
| 601 | + Install it over there — it's a big download, so it isn't something to start from a phone. |
562 | 602 | </p> |
| 603 | + {:else if !ai.running} |
| 604 | + <p class="muted"> |
| 605 | + The local model isn't running on the desktop right now. Everything else on this screen |
| 606 | + still works. |
| 607 | + </p> |
| 608 | + <button class="primary" disabled={busy || waking} onclick={wakeAi}> |
| 609 | + {waking ? "Starting it…" : "Start it on the desktop"} |
| 610 | + </button> |
| 611 | + {:else if !models.length} |
| 612 | + <p class="muted"> |
| 613 | + The model server is running, but no models are installed. Pull one on the desktop. |
| 614 | + </p> |
| 615 | + <button disabled={busy} onclick={loadAi}>Check again</button> |
563 | 616 | {:else} |
| 617 | + {#if models.length > 1} |
| 618 | + <select bind:value={model}> |
| 619 | + {#each models as m}<option>{m}</option>{/each} |
| 620 | + </select> |
| 621 | + {/if} |
564 | 622 | <div class="chat"> |
565 | 623 | {#each chat as m} |
566 | 624 | <p class="msg {m.role}">{m.content}</p> |
|
574 | 632 | {/if} |
575 | 633 |
|
576 | 634 | <nav> |
577 | | - <button class:on={view === "now"} onclick={() => (view = "now")}>Now</button> |
578 | | - <button class:on={view === "journal"} onclick={() => (view = "journal")}>Journal</button> |
579 | | - <button class:on={view === "combo"} onclick={() => (view = "combo")}>Combo</button> |
580 | | - <button class:on={view === "reference"} onclick={() => (view = "reference")}>Look up</button> |
581 | | - <button class:on={view === "companion"} onclick={() => (view = "companion")}>Talk</button> |
| 635 | + <button class:on={view === "now"} onclick={() => goTo("now")}>Now</button> |
| 636 | + <button class:on={view === "journal"} onclick={() => goTo("journal")}>Journal</button> |
| 637 | + <button class:on={view === "combo"} onclick={() => goTo("combo")}>Combo</button> |
| 638 | + <button class:on={view === "reference"} onclick={() => goTo("reference")}>Look up</button> |
| 639 | + <button class:on={view === "companion"} onclick={() => goTo("companion")}>Talk</button> |
582 | 640 | </nav> |
583 | 641 | {/if} |
584 | 642 | </main> |
|
0 commit comments