@g9tm/aiassistant-svelte 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,88 +1,82 @@
1
- <script lang="ts" module>
2
- export type PageKind = "history" | "abilities" | "knowledge" | "learned" | "findings" | "routines" | "actions" | "admin";
3
- </script>
4
-
5
- <script lang="ts">
6
- // The assistant's pages, each from its own endpoint, read with type checks only.
7
- import { arr, num, obj, str } from "@g9tm/aiassistant";
8
- import { cls, useUi } from "../context.js";
9
-
10
- let { kind, onopen = undefined }: { kind: PageKind; onopen?: ((conversationId: string) => void) | undefined } = $props();
11
- const ui = useUi();
12
- const t = ui.texts.t;
13
- const api = ui.assistant.api;
14
- let data = $state<Record<string, unknown>>({});
15
- let error = $state<string | null>(null);
16
- let search = $state("");
17
- let purpose = $state("");
18
- let code = $state("");
19
- let token = $state<string | null>(null);
20
-
21
- const PATHS: Record<PageKind, string> = {
22
- history: "conversations", abilities: "capabilities", knowledge: "knowledge", learned: "learned/dashboard?capability=chat",
23
- findings: "findings", routines: "routines", actions: "conversations", admin: "gates",
24
- };
25
-
26
- async function load(): Promise<void> {
27
- try {
28
- data = obj(await api.get(new Map(Object.entries(PATHS)).get(kind) ?? ""));
29
- error = null;
30
- } catch (e) {
31
- error = e instanceof Error ? e.message : "error.network";
32
- }
33
- }
34
- $effect(() => { void load(); });
35
-
36
- const today = new Date().toDateString();
37
- const conversations = $derived(arr(data["conversations"]).map(obj).filter((c) => str(c["title"]).includes(search)));
38
- async function act(path: string, body?: unknown): Promise<void> {
39
- await api.post(path, body).catch(() => undefined);
40
- await load();
41
- }
42
- </script>
43
-
44
- <div class={cls(ui, "panel.body")} data-page={kind}>
45
- {#if error}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
46
- {#if kind === "history"}
47
- <input bind:value={search} placeholder={t("history.search")} aria-label={t("history.search")} />
48
- {#each [["history.pinned", conversations.filter((c) => c["pinned"] === true)], ["history.today", conversations.filter((c) => c["pinned"] !== true && new Date(str(c["updatedAt"])).toDateString() === today)], ["history.earlier", conversations.filter((c) => c["pinned"] !== true && new Date(str(c["updatedAt"])).toDateString() !== today)]] as [title, list] (title)}
49
- {#if (list as Record<string, unknown>[]).length > 0}
50
- <h4>{t(title as string)}</h4>
51
- <ul>{#each list as Record<string, unknown>[] as c (str(c["id"]))}<li><button type="button" onclick={async () => { await ui.assistant.open(str(c["id"])); onopen?.(str(c["id"])); }}>{str(c["title"])}</button></li>{/each}</ul>
52
- {/if}
53
- {/each}
54
- {:else if kind === "abilities"}
55
- {#each arr(data["groups"]).map(obj) as g (str(g["name"]))}
56
- <h4>{str(g["title"], str(g["name"]))}</h4>
57
- <ul>{#each arr(g["tools"]).map(obj) as tool (str(tool["name"]))}<li>{str(tool["title"], str(tool["name"]))}</li>{/each}</ul>
58
- {/each}
59
- {#each arr(data["closed"]).map(obj) as c (str(c["name"]))}<p class="g9a-note">{str(c["name"])} · {str(c["layer"])}</p>{/each}
60
- {:else if kind === "knowledge"}
61
- <ul>{#each arr(data["notes"] ?? data["items"]).map(obj) as n, i (i)}<li>{str(n["text"], str(n["name"]))}</li>{/each}</ul>
62
- {:else if kind === "learned"}
63
- <p>{Math.round(num(data["modelFreeRate"]) * 100)}% · {num(data["tokensSaved"])}</p>
64
- <ul>{#each arr(data["items"]).map(obj) as it, i (i)}<li data-status={str(it["status"])}>{str(it["signature"])} · {str(it["status"])} · {num(it["hits"])}</li>{/each}</ul>
65
- {:else if kind === "findings"}
66
- <ul>
67
- {#each arr(data["findings"]).map(obj) as f (str(f["id"]))}
68
- <li>
69
- {str(obj(f["title"])["text"], t(str(obj(f["title"])["key"])))}
70
- <button type="button" onclick={() => act("findings/" + encodeURIComponent(str(f["id"])) + "/fixed")}>✓</button>
71
- <button type="button" onclick={() => act("findings/" + encodeURIComponent(str(f["id"])) + "/dismiss", { reason: "—" })}>{t("no")}</button>
72
- </li>
73
- {/each}
74
- </ul>
75
- {:else if kind === "routines"}
76
- <ul>{#each arr(data["routines"] ?? data["items"]).map(obj) as r (str(r["id"]))}<li>{str(r["title"], str(r["name"]))} <button type="button" onclick={() => act("routines/" + encodeURIComponent(str(r["id"])) + "/preview")}>▶</button></li>{/each}</ul>
77
- {:else if kind === "actions"}
78
- <ul>{#each [...ui.assistant.snapshot().plans.values()].filter((p) => p.answered === "confirmed") as p (p.id)}<li>{p.title} <button type="button" onclick={() => ui.assistant.undo(p.id)}>{t("undo.all")}</button></li>{/each}</ul>
79
- {:else if kind === "admin"}
80
- <form onsubmit={async (e) => { e.preventDefault(); const r = obj(await api.post("gates", { purpose, code }).catch(() => null)); token = str(r["token"]) || null; purpose = ""; code = ""; await load(); }}>
81
- <input bind:value={purpose} placeholder="purpose" aria-label="purpose" />
82
- <input bind:value={code} type="password" placeholder={t("card.code")} aria-label={t("card.code")} />
83
- <button type="submit">+</button>
84
- </form>
85
- {#if token}<p class="g9a-note"><code>{token}</code></p>{/if}
86
- <ul>{#each arr(data["gates"]).map(obj) as g (str(g["id"]))}<li data-revoked={g["revoked"] === true}>{str(g["purpose"])} · {num(g["callsToday"])} <button type="button" onclick={async () => { await api.delete("gates/" + encodeURIComponent(str(g["id"]))); await load(); }}>×</button></li>{/each}</ul>
87
- {/if}
88
- </div>
1
+ <script lang="ts" module>
2
+ export type PageKind = "history" | "abilities" | "knowledge" | "learned" | "findings" | "routines" | "actions" | "admin";
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ // The assistant's pages, each from its own endpoint, read with type checks only.
7
+ import { arr, num, obj, str } from "@g9tm/aiassistant";
8
+ import { cls, override, useUi } from "../context.js";
9
+ import History from "./History.svelte";
10
+ import RoutineWizard from "./RoutineWizard.svelte";
11
+
12
+ let { kind, onopen = undefined }: { kind: PageKind; onopen?: ((conversationId: string) => void) | undefined } = $props();
13
+ const ui = useUi();
14
+ const t = ui.texts.t;
15
+ const api = ui.assistant.api;
16
+ let data = $state<Record<string, unknown>>({});
17
+ let error = $state<string | null>(null);
18
+ let purpose = $state("");
19
+ let code = $state("");
20
+ let token = $state<string | null>(null);
21
+
22
+ const PATHS: Record<PageKind, string> = {
23
+ history: "conversations", abilities: "capabilities", knowledge: "knowledge", learned: "learned/dashboard?capability=chat",
24
+ findings: "findings", routines: "routines", actions: "conversations", admin: "gates",
25
+ };
26
+
27
+ async function load(): Promise<void> {
28
+ try {
29
+ data = obj(await api.get(new Map(Object.entries(PATHS)).get(kind) ?? ""));
30
+ error = null;
31
+ } catch (e) {
32
+ error = e instanceof Error ? e.message : "error.network";
33
+ }
34
+ }
35
+ // History and the routine wizard are their own (replaceable) pages and read their own data.
36
+ const Own = $derived(kind === "history" ? override(ui, "History") ?? History : kind === "routines" ? override(ui, "RoutineWizard") ?? RoutineWizard : null);
37
+ $effect(() => { if (kind !== "history" && kind !== "routines") void load(); });
38
+ async function act(path: string, body?: unknown): Promise<void> {
39
+ await api.post(path, body).catch(() => undefined);
40
+ await load();
41
+ }
42
+ </script>
43
+
44
+ {#if Own}
45
+ <Own {onopen} />
46
+ {:else}
47
+ <div class={cls(ui, "panel.body")} data-page={kind}>
48
+ {#if error}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
49
+ {#if kind === "abilities"}
50
+ {#each arr(data["groups"]).map(obj) as g (str(g["name"]))}
51
+ <h4>{str(g["title"], str(g["name"]))}</h4>
52
+ <ul>{#each arr(g["tools"]).map(obj) as tool (str(tool["name"]))}<li>{str(tool["title"], str(tool["name"]))}</li>{/each}</ul>
53
+ {/each}
54
+ {#each arr(data["closed"]).map(obj) as c (str(c["name"]))}<p class="g9a-note">{str(c["name"])} · {str(c["layer"])}</p>{/each}
55
+ {:else if kind === "knowledge"}
56
+ <ul>{#each arr(data["notes"] ?? data["items"]).map(obj) as n, i (i)}<li>{str(n["text"], str(n["name"]))}</li>{/each}</ul>
57
+ {:else if kind === "learned"}
58
+ <p>{Math.round(num(data["modelFreeRate"]) * 100)}% · {num(data["tokensSaved"])}</p>
59
+ <ul>{#each arr(data["items"]).map(obj) as it, i (i)}<li data-status={str(it["status"])}>{str(it["signature"])} · {str(it["status"])} · {num(it["hits"])}</li>{/each}</ul>
60
+ {:else if kind === "findings"}
61
+ <ul>
62
+ {#each arr(data["findings"]).map(obj) as f (str(f["id"]))}
63
+ <li>
64
+ {str(obj(f["title"])["text"], t(str(obj(f["title"])["key"])))}
65
+ <button type="button" onclick={() => act("findings/" + encodeURIComponent(str(f["id"])) + "/fixed")}>✓</button>
66
+ <button type="button" onclick={() => act("findings/" + encodeURIComponent(str(f["id"])) + "/dismiss", { reason: "—" })}>{t("no")}</button>
67
+ </li>
68
+ {/each}
69
+ </ul>
70
+ {:else if kind === "actions"}
71
+ <ul>{#each [...ui.assistant.snapshot().plans.values()].filter((p) => p.answered === "confirmed") as p (p.id)}<li>{p.title} <button type="button" onclick={() => ui.assistant.undo(p.id)}>{t("undo.all")}</button></li>{/each}</ul>
72
+ {:else if kind === "admin"}
73
+ <form onsubmit={async (e) => { e.preventDefault(); const r = obj(await api.post("gates", { purpose, code }).catch(() => null)); token = str(r["token"]) || null; purpose = ""; code = ""; await load(); }}>
74
+ <input bind:value={purpose} placeholder="purpose" aria-label="purpose" />
75
+ <input bind:value={code} type="password" placeholder={t("card.code")} aria-label={t("card.code")} />
76
+ <button type="submit">+</button>
77
+ </form>
78
+ {#if token}<p class="g9a-note"><code>{token}</code></p>{/if}
79
+ <ul>{#each arr(data["gates"]).map(obj) as g (str(g["id"]))}<li data-revoked={g["revoked"] === true}>{str(g["purpose"])} · {num(g["callsToday"])} <button type="button" onclick={async () => { await api.delete("gates/" + encodeURIComponent(str(g["id"]))); await load(); }}>×</button></li>{/each}</ul>
80
+ {/if}
81
+ </div>
82
+ {/if}
@@ -1,6 +1,197 @@
1
- <script lang="ts">
2
- import Page from "./Page.svelte";
3
- let { onopen = undefined }: { onopen?: ((id: string) => void) | undefined } = $props();
4
- </script>
5
-
6
- <Page kind="routines" {onopen} />
1
+ <script lang="ts">
2
+ // The routine wizard: the routines the person may run; choosing one asks for its slots (entity slots through a
3
+ // searchable picker, the rest typed and checked by the core's slotValue), then "Preview". A read-only routine answers
4
+ // with its report; one that changes things returns its card, confirmed here (with the code its method asks for) and
5
+ // run bound to the preview's run and hash.
6
+ import { ApiError, arr, needsCode, num, obj, planView, str, type PlanView } from "@g9tm/aiassistant";
7
+ import { missingSlots, pageCalls, readRoutines, slotValue, type Routine, type RoutineSlot } from "@g9tm/aiassistant/pages";
8
+ import { cls, override, useUi } from "../context.js";
9
+ import BlockView from "../blocks/BlockView.svelte";
10
+ import PinPad from "../slots/PinPad.svelte";
11
+ import SlotPicker from "./SlotPicker.svelte";
12
+ import { pageTexts } from "./texts.js";
13
+
14
+ // The same props as the other pages (a routine opens no conversation).
15
+ let {}: { onopen?: ((conversationId: string) => void) | undefined } = $props();
16
+ const ui = useUi();
17
+ const texts = pageTexts(ui.texts);
18
+ const t = texts.t;
19
+ const api = ui.assistant.api;
20
+ const Picker = $derived(override(ui, "SlotPicker") ?? SlotPicker);
21
+ const Pin = $derived(override(ui, "PinPad") ?? PinPad);
22
+
23
+ type Card = { plan: PlanView; routineRunId: string; hash: string };
24
+ let routines = $state<Routine[]>([]);
25
+ let loadFailed = $state(false);
26
+ let chosen = $state<Routine | null>(null);
27
+ let typed = $state<Record<string, string>>({});
28
+ let picks = $state<Record<string, { id: string; label: string }>>({});
29
+ let tried = $state(false);
30
+ let serverMissing = $state<string[]>([]);
31
+ let report = $state<unknown[] | null>(null);
32
+ let card = $state<Card | null>(null);
33
+ let code = $state("");
34
+ let busy = $state(false);
35
+ let failure = $state<string | null>(null);
36
+ let triesLeft = $state<number | null>(null);
37
+ let result = $state<string | null>(null);
38
+
39
+ $effect(() => {
40
+ pageCalls.routines(api).then((raw) => { routines = readRoutines(raw); loadFailed = false; }, () => { loadFailed = true; });
41
+ });
42
+
43
+ const has = (o: Record<string, unknown>, k: string): boolean => Object.prototype.hasOwnProperty.call(o, k);
44
+ const textOf = (name: string): string => (has(typed, name) ? String(new Map(Object.entries(typed)).get(name) ?? "") : "");
45
+ const pickOf = (name: string): { id: string; label: string } | null => new Map(Object.entries(picks)).get(name) ?? null;
46
+ const valueOf = (slot: RoutineSlot): string | null => (slot.field === "pick" ? pickOf(slot.name)?.id ?? null : slotValue(slot, textOf(slot.name)));
47
+ const values = $derived(Object.fromEntries((chosen?.slots ?? []).flatMap((s) => { const v = valueOf(s); return v === null ? [] : [[s.name, v]]; })) as Record<string, string>);
48
+ const missing = $derived(chosen ? missingSlots(chosen.slots, values).map((s) => s.name) : []);
49
+ /** A slot's state: filled, missing (shown once the person asked for the preview, or the server said so) or not right. */
50
+ function stateOf(slot: RoutineSlot): "ok" | "missing" | "invalid" {
51
+ if (slot.field !== "pick" && textOf(slot.name).trim().length > 0 && valueOf(slot) === null) return "invalid";
52
+ if (serverMissing.includes(slot.name)) return "missing";
53
+ return tried && missing.includes(slot.name) ? "missing" : "ok";
54
+ }
55
+ function labelOf(slot: RoutineSlot): string {
56
+ const key = "slot." + slot.name;
57
+ const v = t(key);
58
+ return v === key ? slot.name : v;
59
+ }
60
+ const say = (key: string, args: Record<string, string>): string => {
61
+ const text = t(key, args);
62
+ return text === key ? Object.values(args).join(" ") || key : text;
63
+ };
64
+
65
+ function choose(r: Routine | null): void {
66
+ chosen = r;
67
+ typed = {};
68
+ picks = {};
69
+ tried = false;
70
+ serverMissing = [];
71
+ report = null;
72
+ card = null;
73
+ code = "";
74
+ failure = null;
75
+ triesLeft = null;
76
+ result = null;
77
+ }
78
+
79
+ function failed(e: unknown): void {
80
+ failure = e instanceof ApiError ? e.code : "error.network";
81
+ const left = e instanceof ApiError ? obj(e.body)["triesLeft"] : undefined;
82
+ triesLeft = typeof left === "number" ? left : null;
83
+ }
84
+
85
+ async function preview(): Promise<void> {
86
+ tried = true;
87
+ failure = null;
88
+ serverMissing = [];
89
+ const r0 = chosen;
90
+ if (!r0 || missing.length > 0 || chosen?.slots.some((s) => stateOf(s) === "invalid")) return;
91
+ busy = true;
92
+ try {
93
+ const r = obj(await pageCalls.preview(api, r0.id, values, ui.assistant.snapshot().conversationId ?? undefined));
94
+ const state = str(r["state"]);
95
+ if (state === "report") report = arr(r["blocks"]);
96
+ else if (state === "missing" || !r["card"]) serverMissing = arr(r["missing"]).filter((m): m is string => typeof m === "string");
97
+ else {
98
+ const plan = planView(r["card"]);
99
+ card = { plan, routineRunId: str(r["routineRunId"]), hash: str(r["hash"], plan.hash) };
100
+ }
101
+ } catch (e) {
102
+ failed(e);
103
+ }
104
+ busy = false;
105
+ }
106
+
107
+ async function run(): Promise<void> {
108
+ const r0 = chosen;
109
+ const c = card;
110
+ if (!r0 || !c) return;
111
+ busy = true;
112
+ failure = null;
113
+ triesLeft = null;
114
+ try {
115
+ const r = obj(await pageCalls.run(api, r0.id, c.routineRunId, c.hash, needsCode(c.plan.method) ? code : undefined));
116
+ if (r["ok"] === false) {
117
+ failure = str(obj(r["error"])["code"], "routine.failed");
118
+ triesLeft = typeof r["triesLeft"] === "number" ? num(r["triesLeft"]) : null;
119
+ } else {
120
+ result = str(r["text"]) || t("routine.done");
121
+ card = null;
122
+ }
123
+ } catch (e) {
124
+ failed(e);
125
+ }
126
+ code = "";
127
+ busy = false;
128
+ }
129
+ </script>
130
+
131
+ <div class={cls(ui, "panel.body") + " g9a-routines"} data-page="routines">
132
+ {#if loadFailed}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
133
+ {#if !chosen}
134
+ <h4>{t("routine.list")}</h4>
135
+ <ul class="g9a-routines__list">
136
+ {#each routines as r (r.id)}
137
+ <li><button type="button" class="g9a-routines__choose" data-id={r.id} onclick={() => choose(r)}>{r.title || r.id}</button></li>
138
+ {:else}
139
+ <li class="g9a-note">{t("routine.empty")}</li>
140
+ {/each}
141
+ </ul>
142
+ {:else}
143
+ <div class="g9a-row"><button type="button" class="g9a-routines__back" onclick={() => choose(null)}>{t("routine.back")}</button></div>
144
+ <h4>{chosen.title || chosen.id}</h4>
145
+ {#if result !== null}
146
+ <p class="g9a-note g9a-routines__result" role="status">{result}</p>
147
+ {:else if report}
148
+ <div class="g9a-routines__report">{#each report as b, i (i)}<BlockView block={b} />{/each}</div>
149
+ {:else if card}
150
+ {@const plan = card.plan}
151
+ <div class={cls(ui, "card.approval")} data-routine-card={plan.id}>
152
+ <h3>{plan.readOnly ? t("card.readonly_routine") : plan.title}</h3>
153
+ {#if plan.warning}<p class="g9a-warning" role="note">{t("card.tainted")}</p>{/if}
154
+ <ol>
155
+ {#each plan.steps as s (s.index)}
156
+ <li class={cls(ui, "card.approval.step")}>
157
+ {s.text}{#if !s.undoable}<span class="g9a-irreversible"> ({t("card.irreversible")})</span>{/if}
158
+ {#each s.facts as f, i (i)}<div class={cls(ui, "card.approval.fact")}>{say(f.key, f.args)}</div>{/each}
159
+ {#each s.diff as d, i (i)}<div class={cls(ui, "card.approval.fact")}>{say(d.name, d.args)}: {d.before ?? "—"} ← {d.after ?? "—"}</div>{/each}
160
+ </li>
161
+ {/each}
162
+ </ol>
163
+ {#if needsCode(plan.method)}<Pin bind:value={code} label={t("card.code")} />{/if}
164
+ {#if failure}
165
+ <p class="g9a-note" role="alert" data-code={failure}>{failure === "confirm.waiting" ? t("card.waiting") : t("routine.failed")}{#if triesLeft !== null} · {t("card.tries_left", { count: triesLeft })}{/if}</p>
166
+ {/if}
167
+ <div class="g9a-row">
168
+ <button type="button" class={cls(ui, "card.approval.confirm")} disabled={busy || (needsCode(plan.method) && code.length === 0)} onclick={run}>{t("card.confirm")}</button>
169
+ <button type="button" class={cls(ui, "card.approval.refuse")} disabled={busy} onclick={() => { card = null; failure = null; }}>{t("card.refuse")}</button>
170
+ </div>
171
+ </div>
172
+ {:else}
173
+ <form class="g9a-routines__form" onsubmit={(e) => { e.preventDefault(); void preview(); }} novalidate>
174
+ {#each chosen.slots as slot (slot.name)}
175
+ {@const st = stateOf(slot)}
176
+ <div class="g9a-routines__slot" data-slot={slot.name} data-field={slot.field} data-state={st}>
177
+ {#if slot.field === "pick"}
178
+ <span class="g9a-routines__label">{labelOf(slot)}</span>
179
+ <Picker {slot} label={labelOf(slot)} picked={pickOf(slot.name)} onpick={(p: { id: string; label: string } | null) => {
180
+ const next = { ...picks };
181
+ if (p) next[slot.name] = p; else delete next[slot.name];
182
+ picks = next;
183
+ }} />
184
+ {:else}
185
+ <label for={"g9a-slot-" + slot.name}>{labelOf(slot)}</label>
186
+ <input id={"g9a-slot-" + slot.name} value={textOf(slot.name)} inputmode={slot.field === "number" || slot.field === "money" ? "decimal" : undefined}
187
+ aria-invalid={st !== "ok"} oninput={(e) => { typed = { ...typed, [slot.name]: e.currentTarget.value }; }} />
188
+ {/if}
189
+ {#if st !== "ok"}<span class="g9a-note" data-bad="true">{t(st === "missing" ? "routine.missing" : "routine.invalid")}</span>{/if}
190
+ </div>
191
+ {/each}
192
+ {#if failure}<p class="g9a-note" role="alert" data-code={failure}>{t("routine.failed")}</p>{/if}
193
+ <div class="g9a-row"><button type="submit" class="g9a-routines__preview" disabled={busy}>{t("routine.preview")}</button></div>
194
+ </form>
195
+ {/if}
196
+ {/if}
197
+ </div>
@@ -1,5 +1,5 @@
1
1
  type $$ComponentProps = {
2
- onopen?: ((id: string) => void) | undefined;
2
+ onopen?: ((conversationId: string) => void) | undefined;
3
3
  };
4
4
  declare const RoutineWizard: import("svelte").Component<$$ComponentProps, {}, "">;
5
5
  type RoutineWizard = ReturnType<typeof RoutineWizard>;
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ // A routine's entity slot: a searchable picker over the host's things of the slot's kind (`GET pick`), asked a short
3
+ // while after the person stops typing; an answer that came after a newer question is dropped.
4
+ import { pageCalls, readPicks, type RoutineSlot } from "@g9tm/aiassistant/pages";
5
+ import { useUi } from "../context.js";
6
+ import { pageTexts } from "./texts.js";
7
+
8
+ let { slot, label, picked = null, onpick, delay = 250 }: {
9
+ slot: RoutineSlot; label: string; picked?: { id: string; label: string } | null; onpick: (pick: { id: string; label: string } | null) => void; delay?: number;
10
+ } = $props();
11
+ const ui = useUi();
12
+ const t = pageTexts(ui.texts).t;
13
+ let q = $state("");
14
+ let found = $state<{ id: string; label: string }[]>([]);
15
+ let asked = $state(false);
16
+ let failed = $state(false);
17
+ let handle: ReturnType<typeof setTimeout> | undefined;
18
+ let seq = 0;
19
+
20
+ function ask(text: string): void {
21
+ if (handle !== undefined) clearTimeout(handle);
22
+ const mine = ++seq;
23
+ handle = setTimeout(() => {
24
+ pageCalls.pick(ui.assistant.api, slot.kind, text.trim()).then((raw) => {
25
+ if (mine !== seq) return;
26
+ found = readPicks(raw);
27
+ asked = true;
28
+ failed = false;
29
+ }, () => {
30
+ if (mine === seq) failed = true;
31
+ });
32
+ }, delay);
33
+ }
34
+ $effect(() => () => { if (handle !== undefined) clearTimeout(handle); });
35
+ </script>
36
+
37
+ <div class="g9a-picker" data-slot={slot.name}>
38
+ {#if picked}
39
+ <span class="g9a-picker__chosen">{picked.label}</span>
40
+ <button type="button" class="g9a-picker__change" onclick={() => { onpick(null); q = ""; found = []; asked = false; }}>{t("routine.change")}</button>
41
+ {:else}
42
+ <input type="search" value={q} aria-label={label} placeholder={t("routine.search")}
43
+ oninput={(e) => { q = e.currentTarget.value; ask(q); }} onfocus={() => { if (!asked) ask(q); }} />
44
+ {#if failed}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
45
+ {#if found.length > 0}
46
+ <ul class="g9a-picker__list" role="listbox" aria-label={label}>
47
+ {#each found as p (p.id)}
48
+ <li role="option" aria-selected="false"><button type="button" onclick={() => onpick(p)}>{p.label}</button></li>
49
+ {/each}
50
+ </ul>
51
+ {:else if asked}
52
+ <p class="g9a-note">{t("routine.none_found")}</p>
53
+ {/if}
54
+ {/if}
55
+ </div>
@@ -0,0 +1,17 @@
1
+ import { type RoutineSlot } from "@g9tm/aiassistant/pages";
2
+ type $$ComponentProps = {
3
+ slot: RoutineSlot;
4
+ label: string;
5
+ picked?: {
6
+ id: string;
7
+ label: string;
8
+ } | null;
9
+ onpick: (pick: {
10
+ id: string;
11
+ label: string;
12
+ } | null) => void;
13
+ delay?: number;
14
+ };
15
+ declare const SlotPicker: import("svelte").Component<$$ComponentProps, {}, "">;
16
+ type SlotPicker = ReturnType<typeof SlotPicker>;
17
+ export default SlotPicker;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The few words of the person's pages the core catalogue does not have (its history.*, routine.*, csv.* and pdf.* keys
3
+ * are shared with the other renderer): the host's texts and overrides win, then the core's, then these.
4
+ */
5
+ import { type Texts } from "@g9tm/aiassistant";
6
+ /** The pages' texts over the assistant's: a key the host or the core knows keeps its text. */
7
+ export declare function pageTexts(texts: Texts): Texts;
8
+ /** Every key the pages add (for hosts that translate to another language). */
9
+ export declare function pageKeys(): string[];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The few words of the person's pages the core catalogue does not have (its history.*, routine.*, csv.* and pdf.* keys
3
+ * are shared with the other renderer): the host's texts and overrides win, then the core's, then these.
4
+ */
5
+ import { createTexts } from "@g9tm/aiassistant";
6
+ const FA = {
7
+ "history.tabs": "گفت‌وگوها",
8
+ "history.empty": "چیزی اینجا نیست",
9
+ "history.untitled": "بی‌نام",
10
+ "routine.list": "روال‌ها",
11
+ "routine.empty": "هنوز روالی نیست",
12
+ "routine.search": "جست‌وجو…",
13
+ "routine.none_found": "چیزی پیدا نشد",
14
+ "routine.change": "تغییر",
15
+ "routine.failed": "اجرا نشد",
16
+ };
17
+ const EN = {
18
+ "history.tabs": "Conversations",
19
+ "history.empty": "Nothing here",
20
+ "history.untitled": "Untitled",
21
+ "routine.list": "Routines",
22
+ "routine.empty": "No routines yet",
23
+ "routine.search": "Search…",
24
+ "routine.none_found": "Nothing found",
25
+ "routine.change": "Change",
26
+ "routine.failed": "It did not run",
27
+ };
28
+ /** The pages' texts over the assistant's: a key the host or the core knows keeps its text. */
29
+ export function pageTexts(texts) {
30
+ const own = createTexts(texts.locale, texts.locale === "fa" ? FA : EN);
31
+ return {
32
+ locale: texts.locale,
33
+ t(key, args) {
34
+ const v = texts.t(key, args);
35
+ return v !== key ? v : own.t(key, args);
36
+ },
37
+ };
38
+ }
39
+ /** Every key the pages add (for hosts that translate to another language). */
40
+ export function pageKeys() {
41
+ return Object.keys(EN).sort();
42
+ }
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ // "Download PDF" of the open conversation, once it has messages: the server's signed link (ten minutes), opened by the
3
+ // host. Busy while the link is asked for; a notice if it could not be made. Hidden when the host cannot open links.
4
+ import { onDestroy } from "svelte";
5
+ import { obj, str } from "@g9tm/aiassistant";
6
+ import { useUi } from "../context.js";
7
+ import { AssistantState } from "../store.svelte.js";
8
+ import { pageTexts } from "../pages/texts.js";
9
+
10
+ const ui = useUi();
11
+ const t = pageTexts(ui.texts).t;
12
+ const live = new AssistantState(ui.assistant);
13
+ onDestroy(() => live.dispose());
14
+ const id = $derived(live.current.conversationId);
15
+ const shown = $derived(id !== null && live.current.messages.length > 0 && typeof ui.assistant.host.openUrl === "function");
16
+ let busy = $state(false);
17
+ let failed = $state(false);
18
+
19
+ async function download(): Promise<void> {
20
+ if (!id || busy) return;
21
+ busy = true;
22
+ failed = false;
23
+ try {
24
+ const url = str(obj(await ui.assistant.api.documentLink(id))["url"]);
25
+ if (!/^(https:\/\/|\/(?!\/))/u.test(url)) throw new Error("no link"); // the server's own path or https only
26
+ ui.assistant.host.openUrl?.(url);
27
+ } catch {
28
+ failed = true;
29
+ }
30
+ busy = false;
31
+ }
32
+ </script>
33
+
34
+ {#if shown}
35
+ <button type="button" class="g9a-pdf" aria-busy={busy} disabled={busy} onclick={download}>{busy ? t("pdf.busy") : t("pdf.download")}</button>
36
+ {#if failed}<span class="g9a-note g9a-pdf__failed" role="alert">{t("pdf.failed")}</span>{/if}
37
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const PdfButton: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type PdfButton = ReturnType<typeof PdfButton>;
3
+ export default PdfButton;