@g9tm/aiassistant-svelte 0.0.0-stage → 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.
Files changed (92) hide show
  1. package/README.md +29 -2
  2. package/dist/AssistantRoot.svelte +130 -0
  3. package/dist/AssistantRoot.svelte.d.ts +13 -0
  4. package/dist/admin.d.ts +2 -0
  5. package/dist/admin.js +2 -0
  6. package/dist/blocks/BlockView.svelte +84 -0
  7. package/dist/blocks/BlockView.svelte.d.ts +7 -0
  8. package/dist/blocks/Chart.svelte +51 -0
  9. package/dist/blocks/Chart.svelte.d.ts +9 -0
  10. package/dist/blocks/CsvButton.svelte +27 -0
  11. package/dist/blocks/CsvButton.svelte.d.ts +8 -0
  12. package/dist/browser.d.ts +16 -0
  13. package/dist/browser.js +80 -0
  14. package/dist/cards/ApprovalCard.svelte +56 -0
  15. package/dist/cards/ApprovalCard.svelte.d.ts +7 -0
  16. package/dist/cards/Cards.svelte +55 -0
  17. package/dist/cards/Cards.svelte.d.ts +11 -0
  18. package/dist/cards/SecretField.svelte +21 -0
  19. package/dist/cards/SecretField.svelte.d.ts +6 -0
  20. package/dist/context.d.ts +33 -0
  21. package/dist/context.js +35 -0
  22. package/dist/index.d.ts +38 -0
  23. package/dist/index.js +36 -0
  24. package/dist/pages/Abilities.svelte +6 -0
  25. package/dist/pages/Abilities.svelte.d.ts +6 -0
  26. package/dist/pages/ActionsPage.svelte +6 -0
  27. package/dist/pages/ActionsPage.svelte.d.ts +6 -0
  28. package/dist/pages/AdminPage.svelte +53 -0
  29. package/dist/pages/AdminPage.svelte.d.ts +6 -0
  30. package/dist/pages/FindingsPage.svelte +6 -0
  31. package/dist/pages/FindingsPage.svelte.d.ts +6 -0
  32. package/dist/pages/History.svelte +96 -0
  33. package/dist/pages/History.svelte.d.ts +6 -0
  34. package/dist/pages/KnowledgePage.svelte +6 -0
  35. package/dist/pages/KnowledgePage.svelte.d.ts +6 -0
  36. package/dist/pages/LearnedPage.svelte +6 -0
  37. package/dist/pages/LearnedPage.svelte.d.ts +6 -0
  38. package/dist/pages/Page.svelte +82 -0
  39. package/dist/pages/Page.svelte.d.ts +8 -0
  40. package/dist/pages/RoutineWizard.svelte +197 -0
  41. package/dist/pages/RoutineWizard.svelte.d.ts +6 -0
  42. package/dist/pages/SlotPicker.svelte +55 -0
  43. package/dist/pages/SlotPicker.svelte.d.ts +17 -0
  44. package/dist/pages/admin/AdminAudit.svelte +33 -0
  45. package/dist/pages/admin/AdminAudit.svelte.d.ts +3 -0
  46. package/dist/pages/admin/AdminCapabilities.svelte +91 -0
  47. package/dist/pages/admin/AdminCapabilities.svelte.d.ts +3 -0
  48. package/dist/pages/admin/AdminConnections.svelte +58 -0
  49. package/dist/pages/admin/AdminConnections.svelte.d.ts +6 -0
  50. package/dist/pages/admin/AdminFeedback.svelte +38 -0
  51. package/dist/pages/admin/AdminFeedback.svelte.d.ts +3 -0
  52. package/dist/pages/admin/AdminModels.svelte +103 -0
  53. package/dist/pages/admin/AdminModels.svelte.d.ts +6 -0
  54. package/dist/pages/admin/AdminUsage.svelte +92 -0
  55. package/dist/pages/admin/AdminUsage.svelte.d.ts +3 -0
  56. package/dist/pages/texts.d.ts +9 -0
  57. package/dist/pages/texts.js +42 -0
  58. package/dist/parts/Composer.svelte +69 -0
  59. package/dist/parts/Composer.svelte.d.ts +14 -0
  60. package/dist/parts/Message.svelte +40 -0
  61. package/dist/parts/Message.svelte.d.ts +9 -0
  62. package/dist/parts/MessageList.svelte +28 -0
  63. package/dist/parts/MessageList.svelte.d.ts +9 -0
  64. package/dist/parts/PdfButton.svelte +37 -0
  65. package/dist/parts/PdfButton.svelte.d.ts +3 -0
  66. package/dist/parts/RevealText.svelte +26 -0
  67. package/dist/parts/RevealText.svelte.d.ts +7 -0
  68. package/dist/parts/Small.svelte +24 -0
  69. package/dist/parts/Small.svelte.d.ts +10 -0
  70. package/dist/parts/StagesThinking.svelte +15 -0
  71. package/dist/parts/StagesThinking.svelte.d.ts +8 -0
  72. package/dist/parts/TextView.svelte +36 -0
  73. package/dist/parts/TextView.svelte.d.ts +6 -0
  74. package/dist/parts/ThinkingOrb.svelte +7 -0
  75. package/dist/parts/ThinkingOrb.svelte.d.ts +18 -0
  76. package/dist/parts/WorkCard.svelte +41 -0
  77. package/dist/parts/WorkCard.svelte.d.ts +7 -0
  78. package/dist/slots/Dialog.svelte +20 -0
  79. package/dist/slots/Dialog.svelte.d.ts +9 -0
  80. package/dist/slots/PinPad.svelte +6 -0
  81. package/dist/slots/PinPad.svelte.d.ts +7 -0
  82. package/dist/store.svelte.d.ts +15 -0
  83. package/dist/store.svelte.js +31 -0
  84. package/dist/surfaces/AssistantChat.svelte +31 -0
  85. package/dist/surfaces/AssistantChat.svelte.d.ts +6 -0
  86. package/dist/surfaces/AssistantPanel.svelte +52 -0
  87. package/dist/surfaces/AssistantPanel.svelte.d.ts +13 -0
  88. package/dist/surfaces/Surfaces.svelte +43 -0
  89. package/dist/surfaces/Surfaces.svelte.d.ts +16 -0
  90. package/dist/theme.d.ts +9 -0
  91. package/dist/theme.js +22 -0
  92. package/package.json +46 -3
@@ -0,0 +1,197 @@
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>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ onopen?: ((conversationId: string) => void) | undefined;
3
+ };
4
+ declare const RoutineWizard: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type RoutineWizard = ReturnType<typeof RoutineWizard>;
6
+ export default 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,33 @@
1
+ <script lang="ts">
2
+ // The audit, newest first: steps run, cards answered, undos, gates and settings changed.
3
+ import type { AuditView } from "@g9tm/aiassistant/admin";
4
+ import { AdminClient, adminTexts } from "@g9tm/aiassistant/admin";
5
+ import { useUi } from "../../context.js";
6
+
7
+ const ui = useUi();
8
+ const t = adminTexts(ui.texts).t;
9
+ let rows = $state<AuditView[]>([]);
10
+ let error = $state<string | null>(null);
11
+
12
+ $effect(() => {
13
+ new AdminClient(ui.assistant.api).audit(200).then((r) => { rows = r; error = null; }, (e: unknown) => { error = e instanceof Error ? e.message : "error.network"; });
14
+ });
15
+ </script>
16
+
17
+ {#if error}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
18
+ {#if rows.length === 0 && !error}<p class="g9a-note">{t("admin.empty")}</p>{/if}
19
+ <table>
20
+ <tbody>
21
+ {#each rows as a, i (i)}
22
+ <tr data-tainted={a.tainted}>
23
+ <td><time>{new Date(a.at).toLocaleString(ui.texts.locale)}</time></td>
24
+ <td>{a.user}</td>
25
+ <td>{a.actor}</td>
26
+ <td dir="ltr">{a.action}</td>
27
+ <td dir="ltr">{a.tool ?? ""}</td>
28
+ <td dir="ltr">{a.decision ?? ""}</td>
29
+ <td dir="ltr">{a.outcome ?? ""}</td>
30
+ </tr>
31
+ {/each}
32
+ </tbody>
33
+ </table>
@@ -0,0 +1,3 @@
1
+ declare const AdminAudit: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type AdminAudit = ReturnType<typeof AdminAudit>;
3
+ export default AdminAudit;
@@ -0,0 +1,91 @@
1
+ <script lang="ts">
2
+ // Capabilities as they run now; a manager changes the route, the limits or the switch without a release, and runs
3
+ // one entry-test probe at a time on the route's first model.
4
+ import type { CapabilityLimitsView, CapabilityView, ProbeView } from "@g9tm/aiassistant/admin";
5
+ import { AdminClient, adminTexts, parseRoute } from "@g9tm/aiassistant/admin";
6
+ import { useUi } from "../../context.js";
7
+
8
+ const ui = useUi();
9
+ const t = adminTexts(ui.texts).t;
10
+ const admin = new AdminClient(ui.assistant.api);
11
+ let list = $state<CapabilityView[]>([]);
12
+ let probes = $state<string[]>([]);
13
+ let routes = $state<Record<string, string>>({});
14
+ let error = $state<string | null>(null);
15
+ let results = $state<ProbeView[]>([]);
16
+ let busy = $state(false);
17
+
18
+ const FIELDS: [keyof CapabilityLimitsView, string][] = [
19
+ ["maxRounds", "admin.max_rounds"], ["perMinute", "admin.per_minute"], ["perPersonPerDay", "admin.per_person_day"],
20
+ ["perTenantPerDay", "admin.per_space_day"], ["monthlyTokenBudget", "admin.monthly_tokens"],
21
+ ];
22
+
23
+ async function load(): Promise<void> {
24
+ try {
25
+ const r = await admin.capabilities();
26
+ list = r.capabilities;
27
+ probes = r.probes;
28
+ routes = Object.fromEntries(r.capabilities.map((c) => [c.id, c.route.join(" > ")]));
29
+ error = null;
30
+ } catch (e) {
31
+ error = e instanceof Error ? e.message : "error.network";
32
+ }
33
+ }
34
+ $effect(() => { void load(); });
35
+
36
+ function replace(c: CapabilityView): void {
37
+ list = list.map((x) => (x.id === c.id ? c : x));
38
+ routes = { ...routes, [c.id]: c.route.join(" > ") };
39
+ }
40
+
41
+ async function save(c: CapabilityView): Promise<void> {
42
+ const route = parseRoute(new Map(Object.entries(routes)).get(c.id) ?? "");
43
+ try {
44
+ replace(await admin.setCapability(c.id, { route, limits: c.limits, enabled: c.enabled }));
45
+ } catch (e) {
46
+ error = e instanceof Error ? e.message : "error.network";
47
+ }
48
+ }
49
+
50
+ async function reset(c: CapabilityView): Promise<void> {
51
+ try { replace(await admin.resetCapability(c.id)); } catch (e) { error = e instanceof Error ? e.message : "error.network"; }
52
+ }
53
+
54
+ async function probeAll(c: CapabilityView): Promise<void> {
55
+ const model = c.route.at(0);
56
+ if (!model || busy) return;
57
+ busy = true;
58
+ results = [];
59
+ for (const p of probes) {
60
+ // one probe per request: each shows as it ends
61
+ try { results = [...results, await admin.probe(p, model)]; } catch { results = [...results, { probe: p, model, quality: "error", rightKind: false, tokensIn: 0, ms: 0, head: null, error: "error.network" }]; }
62
+ }
63
+ busy = false;
64
+ }
65
+ </script>
66
+
67
+ {#if error}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
68
+ {#if list.length === 0 && !error}<p class="g9a-note">{t("admin.empty")}</p>{/if}
69
+ {#each list as c (c.id)}
70
+ <section class="g9a-admin-capability" data-overridden={c.overridden}>
71
+ <h4>{c.id} · {c.kind}{#if c.overridden} · <small>{t("admin.overridden")}</small>{/if}</h4>
72
+ <label>{t("admin.enabled")} <input type="checkbox" bind:checked={c.enabled} /></label>
73
+ <label>{t("admin.route")} <input dir="ltr" bind:value={routes[c.id]} /></label>
74
+ {#each FIELDS as [key, label] (key)}
75
+ <label>{t(label)} <input type="number" min="0" dir="ltr" bind:value={c.limits[key]} /></label>
76
+ {/each}
77
+ <div>
78
+ <button type="button" onclick={() => save(c)}>{t("admin.save")}</button>
79
+ {#if c.overridden}<button type="button" onclick={() => reset(c)}>{t("admin.reset")}</button>{/if}
80
+ {#if probes.length > 0 && c.route.length > 0}<button type="button" disabled={busy} onclick={() => probeAll(c)}>{t("admin.probe")}</button>{/if}
81
+ </div>
82
+ </section>
83
+ {/each}
84
+ {#if results.length > 0}
85
+ <h4>{t("admin.probe")}</h4>
86
+ <ul>
87
+ {#each results as r (r.probe)}
88
+ <li data-ok={r.rightKind}>{r.rightKind ? "✓" : "✗"} {r.probe} · {r.quality} · {r.tokensIn} · {r.ms} ms{#if r.error} · {r.error}{/if}</li>
89
+ {/each}
90
+ </ul>
91
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const AdminCapabilities: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type AdminCapabilities = ReturnType<typeof AdminCapabilities>;
3
+ export default AdminCapabilities;
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ // The "Connections" group: other MCP servers (an address, a way in, a secret sent once, a test listing the
3
+ // tools as the assistant would name them, read-only or a change), with the route their traffic takes.
4
+ import { AdminClient, adminTexts } from "@g9tm/aiassistant/admin";
5
+ import type { McpServerView } from "@g9tm/aiassistant/admin";
6
+ import { useUi } from "../../context.js";
7
+
8
+ let { level = "app" }: { level?: string } = $props();
9
+ const ui = useUi();
10
+ const t = adminTexts(ui.texts).t;
11
+ const admin = new AdminClient(ui.assistant.api);
12
+ let client = $state(true);
13
+ let servers = $state<McpServerView[]>([]);
14
+ let tested = $state<{ id: string; ok: boolean; error: string | null; tools: { name: string; readOnly: boolean; allowed: boolean }[] } | null>(null);
15
+ let id = $state("");
16
+ let url = $state("");
17
+ let auth = $state("none");
18
+ let secret = $state("");
19
+ let error = $state<string | null>(null);
20
+
21
+ async function load(): Promise<void> {
22
+ try { const r = await admin.mcpServers(); client = r.client; servers = r.servers; error = null; } catch (e) { error = e instanceof Error ? e.message : "error.network"; }
23
+ }
24
+ $effect(() => { void load(); });
25
+
26
+ async function add(): Promise<void> {
27
+ if (!id.trim() || !url.trim()) return;
28
+ try {
29
+ await admin.saveMcpServer(id.trim(), { url: url.trim(), auth, secret: secret || undefined, level });
30
+ id = ""; url = ""; secret = "";
31
+ await load();
32
+ } catch (e) { error = e instanceof Error ? e.message : "error.network"; }
33
+ }
34
+ </script>
35
+
36
+ {#if error}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
37
+ <h4>{t("admin.mcp")}</h4>
38
+ {#if !client}<p class="g9a-note">{t("admin.no_client")}</p>{/if}
39
+ <ul>
40
+ {#each servers as s (s.id)}
41
+ <li data-enabled={s.enabled}>
42
+ <strong dir="ltr">{s.id}</strong> · <span dir="ltr">{s.url}</span> · {s.auth}{s.hasSecret ? " 🔒" : ""} · {s.level} · <span dir="ltr">{s.route}</span>
43
+ <button type="button" onclick={async () => { tested = { id: s.id, ...(await admin.testMcpServer(s.id).catch(() => ({ ok: false, error: "error.network", tools: [] }))) }; }}>{t("admin.test")}</button>
44
+ <button type="button" onclick={async () => { await admin.deleteMcpServer(s.id).catch(() => undefined); await load(); }}>{t("admin.remove")}</button>
45
+ </li>
46
+ {/each}
47
+ </ul>
48
+ {#if tested}
49
+ <p>{tested.id}: {tested.ok ? "✓" : "✗ " + (tested.error ?? "")}</p>
50
+ <ul>{#each tested.tools as x (x.name)}<li dir="ltr" data-allowed={x.allowed}>{x.name} {x.readOnly ? "(read)" : "(change)"}</li>{/each}</ul>
51
+ {/if}
52
+ <form class="g9a-row" onsubmit={(e) => { e.preventDefault(); void add(); }}>
53
+ <input dir="ltr" bind:value={id} placeholder="name" aria-label="name" />
54
+ <input dir="ltr" bind:value={url} placeholder="https://…/mcp" aria-label="url" />
55
+ <select bind:value={auth} aria-label="auth"><option value="none">none</option><option value="bearer">bearer</option></select>
56
+ {#if auth !== "none"}<input dir="ltr" type="password" bind:value={secret} placeholder={t("admin.secret")} aria-label={t("admin.secret")} autocomplete="off" />{/if}
57
+ <button type="submit">{t("admin.add")}</button>
58
+ </form>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ level?: string;
3
+ };
4
+ declare const AdminConnections: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type AdminConnections = ReturnType<typeof AdminConnections>;
6
+ export default AdminConnections;
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ // The agents' suggestions: a repeat counts on one row; the manager marks each and leaves a note. The two
3
+ // knowledge budgets are shown as bars above.
4
+ import { AdminClient, adminTexts } from "@g9tm/aiassistant/admin";
5
+ import type { FeedbackView } from "@g9tm/aiassistant/admin";
6
+ import { useUi } from "../../context.js";
7
+
8
+ const ui = useUi();
9
+ const t = adminTexts(ui.texts).t;
10
+ const admin = new AdminClient(ui.assistant.api);
11
+ let rows = $state<FeedbackView[]>([]);
12
+ let budget = $state<{ notes: number; maxNotes: number; pinnedChars: number; maxPinnedChars: number } | null>(null);
13
+ const STATES = ["new", "seen", "planned", "done", "dismissed"];
14
+
15
+ async function load(): Promise<void> {
16
+ rows = (await admin.agentFeedback().catch(() => ({ fresh: 0, rows: [] }))).rows;
17
+ budget = await admin.budget().catch(() => null);
18
+ }
19
+ $effect(() => { void load(); });
20
+ </script>
21
+
22
+ {#if budget}
23
+ <p>{t("admin.budget", { notes: budget.notes, max: budget.maxNotes, chars: budget.pinnedChars, maxChars: budget.maxPinnedChars })}</p>
24
+ <progress max={budget.maxNotes} value={budget.notes}></progress>
25
+ <progress max={budget.maxPinnedChars} value={budget.pinnedChars}></progress>
26
+ {/if}
27
+ {#if rows.length === 0}<p class="g9a-note">{t("admin.empty")}</p>{/if}
28
+ <ul>
29
+ {#each rows as f (f.id)}
30
+ <li data-status={f.status}>
31
+ <strong>{f.title}</strong> · {f.kind} · ×{f.count}
32
+ {#if f.detail}<p>{f.detail}</p>{/if}
33
+ <select value={f.status} aria-label={f.title} onchange={async (e) => { await admin.setFeedback(f.id, (e.currentTarget as HTMLSelectElement).value).catch(() => undefined); await load(); }}>
34
+ {#each STATES as s (s)}<option value={s}>{s}</option>{/each}
35
+ </select>
36
+ </li>
37
+ {/each}
38
+ </ul>
@@ -0,0 +1,3 @@
1
+ declare const AdminFeedback: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type AdminFeedback = ReturnType<typeof AdminFeedback>;
3
+ export default AdminFeedback;
@@ -0,0 +1,103 @@
1
+ <script lang="ts">
2
+ // The "Models" group: providers (from a preset, a key sent once, a test that reads the models), the host's
3
+ // backends with their usage windows, and the gateway's keys (a key shown once) and its request log.
4
+ import { AdminClient, adminTexts } from "@g9tm/aiassistant/admin";
5
+ import type { ProviderView, WindowSetView } from "@g9tm/aiassistant/admin";
6
+ import { useUi } from "../../context.js";
7
+
8
+ let { level = "app" }: { level?: string } = $props();
9
+ const ui = useUi();
10
+ const t = adminTexts(ui.texts).t;
11
+ const admin = new AdminClient(ui.assistant.api);
12
+ let providers = $state<ProviderView[]>([]);
13
+ let presets = $state<{ id: string; baseUrl: string }[]>([]);
14
+ let backends = $state<{ id: string; state: string; until: string | null; usage: WindowSetView | null }[]>([]);
15
+ let keys = $state<{ id: string; name: string; revoked: boolean; lastUsedAt: string | null }[]>([]);
16
+ let log = $state<{ count: number; errors: number; tokens: number }>({ count: 0, errors: 0, tokens: 0 });
17
+ let preset = $state("");
18
+ let newId = $state("");
19
+ let secret = $state("");
20
+ let keyName = $state("");
21
+ let shown = $state<string | null>(null);
22
+ let error = $state<string | null>(null);
23
+
24
+ async function load(): Promise<void> {
25
+ try {
26
+ const p = await admin.providers();
27
+ providers = p.providers;
28
+ presets = p.presets;
29
+ backends = await admin.backends().catch(() => []);
30
+ keys = await admin.gatewayKeys().catch(() => []);
31
+ log = await admin.gatewayRequests(1).catch(() => ({ count: 0, errors: 0, tokens: 0, rows: [] }));
32
+ error = null;
33
+ } catch (e) {
34
+ error = e instanceof Error ? e.message : "error.network";
35
+ }
36
+ }
37
+ $effect(() => { void load(); });
38
+
39
+ async function add(): Promise<void> {
40
+ const id = (newId || preset).trim();
41
+ if (!id) return;
42
+ try {
43
+ await admin.saveProvider(id, { preset: preset || undefined, secret: secret || undefined, level });
44
+ secret = "";
45
+ newId = "";
46
+ await load();
47
+ } catch (e) { error = e instanceof Error ? e.message : "error.network"; }
48
+ }
49
+
50
+ async function test(id: string): Promise<void> {
51
+ try { const p = await admin.testProvider(id); providers = providers.map((x) => (x.id === id ? p : x)); } catch (e) { error = e instanceof Error ? e.message : "error.network"; }
52
+ }
53
+
54
+ async function makeKey(): Promise<void> {
55
+ if (!keyName.trim()) return;
56
+ try { shown = await admin.createGatewayKey(keyName.trim()); keyName = ""; await load(); } catch (e) { error = e instanceof Error ? e.message : "error.network"; }
57
+ }
58
+ </script>
59
+
60
+ {#if error}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
61
+ <h4>{t("admin.providers")}</h4>
62
+ <ul>
63
+ {#each providers as p (p.id)}
64
+ <li data-state={p.state}>
65
+ <strong dir="ltr">{p.id}</strong> · {p.state} · {p.level} · <span dir="ltr">{p.route}</span>{#if p.defaultModel} · <span dir="ltr">{p.defaultModel}</span>{/if} · {p.models.length}
66
+ <button type="button" onclick={() => test(p.id)}>{t("admin.test")}</button>
67
+ <button type="button" onclick={async () => { await admin.deleteProvider(p.id).catch(() => undefined); await load(); }}>{t("admin.remove")}</button>
68
+ </li>
69
+ {/each}
70
+ </ul>
71
+ <form class="g9a-row" onsubmit={(e) => { e.preventDefault(); void add(); }}>
72
+ <select bind:value={preset} aria-label={t("admin.providers")}>
73
+ <option value="">—</option>
74
+ {#each presets as x (x.id)}<option value={x.id}>{x.id}</option>{/each}
75
+ </select>
76
+ <input dir="ltr" bind:value={newId} placeholder="id" aria-label="id" />
77
+ <input dir="ltr" type="password" bind:value={secret} placeholder={t("admin.secret")} aria-label={t("admin.secret")} autocomplete="off" />
78
+ <button type="submit">{t("admin.add")}</button>
79
+ </form>
80
+
81
+ {#if backends.length > 0}
82
+ <h4>{t("admin.backends")}</h4>
83
+ <ul>
84
+ {#each backends as b (b.id)}
85
+ <li data-state={b.state}><span dir="ltr">{b.id}</span> · {b.state}{#if b.until} · {new Date(b.until).toLocaleString(ui.texts.locale)}{/if}
86
+ {#each b.usage?.windows ?? [] as w (w.name)} · {w.name} {Math.round(w.usedPercent)}%{/each}</li>
87
+ {/each}
88
+ </ul>
89
+ {/if}
90
+
91
+ <h4>{t("admin.gateway")}</h4>
92
+ <p>{log.count} · ✗ {log.errors} · {log.tokens}</p>
93
+ {#if shown}<p class="g9a-note">{t("admin.key_once")} <code dir="ltr">{shown}</code></p>{/if}
94
+ <ul>
95
+ {#each keys as k (k.id)}
96
+ <li data-revoked={k.revoked}>{k.name}{#if k.lastUsedAt} · {new Date(k.lastUsedAt).toLocaleString(ui.texts.locale)}{/if}
97
+ {#if !k.revoked}<button type="button" onclick={async () => { await admin.revokeGatewayKey(k.id).catch(() => undefined); await load(); }}>{t("admin.remove")}</button>{/if}</li>
98
+ {/each}
99
+ </ul>
100
+ <form class="g9a-row" onsubmit={(e) => { e.preventDefault(); void makeKey(); }}>
101
+ <input bind:value={keyName} placeholder={t("admin.gateway")} aria-label={t("admin.gateway")} />
102
+ <button type="submit">{t("admin.add")}</button>
103
+ </form>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ level?: string;
3
+ };
4
+ declare const AdminModels: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type AdminModels = ReturnType<typeof AdminModels>;
6
+ export default AdminModels;