@g9tm/aiassistant-svelte 0.0.0-stage → 1.0.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.
- package/README.md +29 -2
- package/dist/AssistantRoot.svelte +118 -0
- package/dist/AssistantRoot.svelte.d.ts +13 -0
- package/dist/admin.d.ts +2 -0
- package/dist/admin.js +2 -0
- package/dist/blocks/BlockView.svelte +74 -0
- package/dist/blocks/BlockView.svelte.d.ts +7 -0
- package/dist/blocks/Chart.svelte +51 -0
- package/dist/blocks/Chart.svelte.d.ts +9 -0
- package/dist/browser.d.ts +16 -0
- package/dist/browser.js +80 -0
- package/dist/cards/ApprovalCard.svelte +56 -0
- package/dist/cards/ApprovalCard.svelte.d.ts +7 -0
- package/dist/cards/Cards.svelte +55 -0
- package/dist/cards/Cards.svelte.d.ts +11 -0
- package/dist/cards/SecretField.svelte +21 -0
- package/dist/cards/SecretField.svelte.d.ts +6 -0
- package/dist/context.d.ts +33 -0
- package/dist/context.js +35 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +32 -0
- package/dist/pages/Abilities.svelte +6 -0
- package/dist/pages/Abilities.svelte.d.ts +6 -0
- package/dist/pages/ActionsPage.svelte +6 -0
- package/dist/pages/ActionsPage.svelte.d.ts +6 -0
- package/dist/pages/AdminPage.svelte +53 -0
- package/dist/pages/AdminPage.svelte.d.ts +6 -0
- package/dist/pages/FindingsPage.svelte +6 -0
- package/dist/pages/FindingsPage.svelte.d.ts +6 -0
- package/dist/pages/History.svelte +6 -0
- package/dist/pages/History.svelte.d.ts +6 -0
- package/dist/pages/KnowledgePage.svelte +6 -0
- package/dist/pages/KnowledgePage.svelte.d.ts +6 -0
- package/dist/pages/LearnedPage.svelte +6 -0
- package/dist/pages/LearnedPage.svelte.d.ts +6 -0
- package/dist/pages/Page.svelte +88 -0
- package/dist/pages/Page.svelte.d.ts +8 -0
- package/dist/pages/RoutineWizard.svelte +6 -0
- package/dist/pages/RoutineWizard.svelte.d.ts +6 -0
- package/dist/pages/admin/AdminAudit.svelte +33 -0
- package/dist/pages/admin/AdminAudit.svelte.d.ts +3 -0
- package/dist/pages/admin/AdminCapabilities.svelte +91 -0
- package/dist/pages/admin/AdminCapabilities.svelte.d.ts +3 -0
- package/dist/pages/admin/AdminConnections.svelte +58 -0
- package/dist/pages/admin/AdminConnections.svelte.d.ts +6 -0
- package/dist/pages/admin/AdminFeedback.svelte +38 -0
- package/dist/pages/admin/AdminFeedback.svelte.d.ts +3 -0
- package/dist/pages/admin/AdminModels.svelte +103 -0
- package/dist/pages/admin/AdminModels.svelte.d.ts +6 -0
- package/dist/pages/admin/AdminUsage.svelte +92 -0
- package/dist/pages/admin/AdminUsage.svelte.d.ts +3 -0
- package/dist/parts/Composer.svelte +69 -0
- package/dist/parts/Composer.svelte.d.ts +14 -0
- package/dist/parts/Message.svelte +40 -0
- package/dist/parts/Message.svelte.d.ts +9 -0
- package/dist/parts/MessageList.svelte +28 -0
- package/dist/parts/MessageList.svelte.d.ts +9 -0
- package/dist/parts/RevealText.svelte +26 -0
- package/dist/parts/RevealText.svelte.d.ts +7 -0
- package/dist/parts/Small.svelte +24 -0
- package/dist/parts/Small.svelte.d.ts +10 -0
- package/dist/parts/StagesThinking.svelte +15 -0
- package/dist/parts/StagesThinking.svelte.d.ts +8 -0
- package/dist/parts/TextView.svelte +36 -0
- package/dist/parts/TextView.svelte.d.ts +6 -0
- package/dist/parts/ThinkingOrb.svelte +7 -0
- package/dist/parts/ThinkingOrb.svelte.d.ts +18 -0
- package/dist/parts/WorkCard.svelte +41 -0
- package/dist/parts/WorkCard.svelte.d.ts +7 -0
- package/dist/slots/Dialog.svelte +20 -0
- package/dist/slots/Dialog.svelte.d.ts +9 -0
- package/dist/slots/PinPad.svelte +6 -0
- package/dist/slots/PinPad.svelte.d.ts +7 -0
- package/dist/store.svelte.d.ts +15 -0
- package/dist/store.svelte.js +31 -0
- package/dist/surfaces/AssistantChat.svelte +31 -0
- package/dist/surfaces/AssistantChat.svelte.d.ts +6 -0
- package/dist/surfaces/AssistantPanel.svelte +49 -0
- package/dist/surfaces/AssistantPanel.svelte.d.ts +13 -0
- package/dist/surfaces/Surfaces.svelte +43 -0
- package/dist/surfaces/Surfaces.svelte.d.ts +16 -0
- package/dist/theme.d.ts +9 -0
- package/dist/theme.js +22 -0
- package/package.json +46 -3
|
@@ -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,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The usage ledger: the model-free rate, by capability and model, the subscription windows, the latest calls, and
|
|
3
|
+
// a run's black box when a call is opened.
|
|
4
|
+
import { faDigits } from "@g9tm/aiassistant";
|
|
5
|
+
import type { CallView, TimelineLine, UsageGroupView, UsageView, WindowSetView } from "@g9tm/aiassistant/admin";
|
|
6
|
+
import { AdminClient, adminTexts } from "@g9tm/aiassistant/admin";
|
|
7
|
+
import { useUi } from "../../context.js";
|
|
8
|
+
|
|
9
|
+
const ui = useUi();
|
|
10
|
+
const t = adminTexts(ui.texts).t;
|
|
11
|
+
const admin = new AdminClient(ui.assistant.api);
|
|
12
|
+
let days = $state(7);
|
|
13
|
+
let usage = $state<UsageView | null>(null);
|
|
14
|
+
let windows = $state<WindowSetView[]>([]);
|
|
15
|
+
let calls = $state<CallView[]>([]);
|
|
16
|
+
let box = $state<{ runId: string; lines: TimelineLine[] } | null>(null);
|
|
17
|
+
let error = $state<string | null>(null);
|
|
18
|
+
|
|
19
|
+
const pct = (x: number): string => {
|
|
20
|
+
const s = String(Math.round(x * 100)) + "%";
|
|
21
|
+
return ui.texts.locale === "fa" ? faDigits(s) : s;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function load(d: number): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
[usage, windows, calls] = await Promise.all([admin.usage(d), admin.windows().catch(() => []), admin.calls(d, 50)]);
|
|
27
|
+
error = null;
|
|
28
|
+
} catch (e) {
|
|
29
|
+
error = e instanceof Error ? e.message : "error.network";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
$effect(() => { void load(days); });
|
|
33
|
+
|
|
34
|
+
async function open(runId: string): Promise<void> {
|
|
35
|
+
try { box = { runId, lines: (await admin.timeline(runId)).lines }; } catch { box = null; }
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
{#snippet groupTable(title: string, groups: UsageGroupView[])}
|
|
40
|
+
<h4>{t(title)}</h4>
|
|
41
|
+
<table>
|
|
42
|
+
<tbody>
|
|
43
|
+
{#each groups as g (g.id)}
|
|
44
|
+
<tr>
|
|
45
|
+
<th scope="row" dir="ltr">{g.id}</th>
|
|
46
|
+
<td>{g.rows}</td>
|
|
47
|
+
<td>{pct(g.rows === 0 ? 0 : g.withoutModel / g.rows)}</td>
|
|
48
|
+
<td>{t("admin.tokens", { in: g.tokensIn, cached: g.tokensCached, out: g.tokensOut })}</td>
|
|
49
|
+
<td>{g.avgMs} ms</td>
|
|
50
|
+
<td>{g.errors > 0 ? "✗ " + String(g.errors) : ""}</td>
|
|
51
|
+
</tr>
|
|
52
|
+
{/each}
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
{/snippet}
|
|
56
|
+
|
|
57
|
+
{#if error}<p class="g9a-note" role="alert">{t("error.network")}</p>{/if}
|
|
58
|
+
<label>
|
|
59
|
+
<select bind:value={days}>
|
|
60
|
+
{#each [1, 7, 30, 90] as d (d)}<option value={d}>{d}</option>{/each}
|
|
61
|
+
</select>
|
|
62
|
+
</label>
|
|
63
|
+
{#if usage}
|
|
64
|
+
<p><strong>{t("admin.without_model", { percent: pct(usage.withoutModelRate) })}</strong> · {t("admin.rows", { count: usage.rows })}</p>
|
|
65
|
+
{@render groupTable("admin.capabilities", usage.capabilities)}
|
|
66
|
+
{@render groupTable("admin.route", usage.models)}
|
|
67
|
+
{/if}
|
|
68
|
+
{#if windows.length > 0}
|
|
69
|
+
<h4>{t("admin.windows")}</h4>
|
|
70
|
+
{#each windows as s (s.backend)}
|
|
71
|
+
{#each s.windows as w (w.name)}
|
|
72
|
+
<p class="g9a-usage-chip" style={"--g9a-usage: " + String(w.usedPercent) + "%"}>{s.backend} · {w.name} · {t("usage.left", { percent: pct(1 - w.usedPercent / 100) })}</p>
|
|
73
|
+
{/each}
|
|
74
|
+
{/each}
|
|
75
|
+
{/if}
|
|
76
|
+
<h4>{t("admin.calls")}</h4>
|
|
77
|
+
<ul>
|
|
78
|
+
{#each calls as c (c.id)}
|
|
79
|
+
<li data-outcome={c.outcome}>
|
|
80
|
+
{#if c.runId}<button type="button" onclick={() => open(c.runId ?? "")}>{c.capability} · {c.role} #{c.round}</button>{:else}{c.capability} · {c.role}{/if}
|
|
81
|
+
· <span dir="ltr">{c.model ?? "—"}</span> · {c.outcome} · {c.tokensIn}/{c.tokensOut} · {c.ms} ms
|
|
82
|
+
</li>
|
|
83
|
+
{/each}
|
|
84
|
+
</ul>
|
|
85
|
+
{#if box}
|
|
86
|
+
<h4>{t("admin.timeline")} · <span dir="ltr">{box.runId}</span></h4>
|
|
87
|
+
<ol>
|
|
88
|
+
{#each box.lines as line, i (i)}
|
|
89
|
+
<li data-kind={line.kind} data-bad={line.bad}><time>{new Date(line.at).toLocaleTimeString(ui.texts.locale)}</time> {line.title} <span dir="ltr">{line.detail}</span></li>
|
|
90
|
+
{/each}
|
|
91
|
+
</ol>
|
|
92
|
+
{/if}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The composer: input, attach (only when the host can pick a file), mic (only when the host can dictate), send or stop.
|
|
3
|
+
import { can } from "@g9tm/aiassistant";
|
|
4
|
+
import { cls, sty, useUi } from "../context.js";
|
|
5
|
+
|
|
6
|
+
let { busy, attachments, placeholder = undefined }: { busy: boolean; attachments: { id: string; name: string; kind: string }[]; placeholder?: string | undefined } = $props();
|
|
7
|
+
const ui = useUi();
|
|
8
|
+
const t = ui.texts.t;
|
|
9
|
+
const host = ui.assistant.host;
|
|
10
|
+
let text = $state("");
|
|
11
|
+
let input: HTMLTextAreaElement | undefined = $state();
|
|
12
|
+
|
|
13
|
+
async function send(): Promise<void> {
|
|
14
|
+
if (busy) { await ui.assistant.stop(); return; }
|
|
15
|
+
const value = text;
|
|
16
|
+
if (await ui.assistant.send(value)) text = "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function attach(): Promise<void> {
|
|
20
|
+
const f = await host.pickFile?.([".pdf", ".xlsx", ".csv", "image/*"]);
|
|
21
|
+
if (f) await ui.assistant.attach(f.name, f.bytes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function dictate(): Promise<void> {
|
|
25
|
+
const said = await host.dictate?.();
|
|
26
|
+
if (said) text = (text + " " + said).trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Recording: a tap starts, a second tap stops and the server turns the audio into text (shown only when it can).
|
|
30
|
+
let stopRecording = $state<(() => Promise<{ bytes: Uint8Array; mime: string } | null>) | null>(null);
|
|
31
|
+
const live = ui.assistant.stateWatch;
|
|
32
|
+
let voice = $state(live.state.voice);
|
|
33
|
+
$effect(() => live.subscribe((s) => { voice = s.voice; }));
|
|
34
|
+
async function record(): Promise<void> {
|
|
35
|
+
if (stopRecording) {
|
|
36
|
+
const stop = stopRecording;
|
|
37
|
+
stopRecording = null;
|
|
38
|
+
const audio = await stop();
|
|
39
|
+
if (audio) {
|
|
40
|
+
const said = await ui.assistant.api.speech(audio.bytes, audio.mime).catch(() => "");
|
|
41
|
+
if (said) text = (text + " " + said).trim();
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
stopRecording = (await host.record?.().catch(() => null)) ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function focus(): void {
|
|
49
|
+
input?.focus();
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<form class={cls(ui, "composer")} style={sty(ui, "composer")} onsubmit={(e) => { e.preventDefault(); void send(); }}>
|
|
54
|
+
{#if attachments.length > 0}
|
|
55
|
+
<div class={cls(ui, "file-chips")}>
|
|
56
|
+
{#each attachments as a (a.id)}
|
|
57
|
+
<span class={cls(ui, "file-chip")}>{a.name}<button type="button" aria-label={t("files.remove")} onclick={() => ui.assistant.detach(a.id)}>×</button></span>
|
|
58
|
+
{/each}
|
|
59
|
+
</div>
|
|
60
|
+
{/if}
|
|
61
|
+
<div class="g9a-composer-row">
|
|
62
|
+
{#if can(host, "attach")}<button type="button" class={cls(ui, "composer.attach")} aria-label={t("composer.attach")} onclick={attach}>📎</button>{/if}
|
|
63
|
+
<textarea bind:this={input} bind:value={text} class={cls(ui, "composer.input")} rows="1" placeholder={placeholder ?? t("composer.placeholder")} aria-label={placeholder ?? t("composer.placeholder")}
|
|
64
|
+
onkeydown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); void send(); } }}></textarea>
|
|
65
|
+
{#if can(host, "mic")}<button type="button" class={cls(ui, "composer.mic")} aria-label={t("composer.mic")} onclick={dictate}>🎙</button>
|
|
66
|
+
{:else if voice && can(host, "record")}<button type="button" class={cls(ui, "composer.mic")} aria-label={t("composer.mic")} aria-pressed={stopRecording !== null} onclick={record}>{stopRecording ? "■" : "🎙"}</button>{/if}
|
|
67
|
+
<button type="submit" class={cls(ui, busy ? "composer.stop" : "composer.send")}>{busy ? t("composer.stop") : t("composer.send")}</button>
|
|
68
|
+
</div>
|
|
69
|
+
</form>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
busy: boolean;
|
|
3
|
+
attachments: {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
kind: string;
|
|
7
|
+
}[];
|
|
8
|
+
placeholder?: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
declare const Composer: import("svelte").Component<$$ComponentProps, {
|
|
11
|
+
focus: () => void;
|
|
12
|
+
}, "">;
|
|
13
|
+
type Composer = ReturnType<typeof Composer>;
|
|
14
|
+
export default Composer;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// One message: its text (revealed only when it arrived in front of the person), blocks, card, provenance and feedback.
|
|
3
|
+
import { untrack } from "svelte";
|
|
4
|
+
import type { MessageView } from "@g9tm/aiassistant";
|
|
5
|
+
import { cls, override, useUi } from "../context.js";
|
|
6
|
+
import BlockView from "../blocks/BlockView.svelte";
|
|
7
|
+
import ApprovalCard from "../cards/ApprovalCard.svelte";
|
|
8
|
+
import Cards from "../cards/Cards.svelte";
|
|
9
|
+
import RevealText from "./RevealText.svelte";
|
|
10
|
+
import Small from "./Small.svelte";
|
|
11
|
+
|
|
12
|
+
let { message, plans, runId }: { message: MessageView; plans: ReadonlyMap<string, import("@g9tm/aiassistant").PlanView>; runId: string | null } = $props();
|
|
13
|
+
const ui = useUi();
|
|
14
|
+
const t = ui.texts.t;
|
|
15
|
+
// Decided once, when the message first appears (the list is keyed by id): an answer seen once is never revealed again.
|
|
16
|
+
const reveal = untrack(() => ui.assistant.watched.reveal(message.id) && message.role === "assistant" && !message.pending);
|
|
17
|
+
const Approval = $derived(override(ui, "ApprovalCard") ?? ApprovalCard);
|
|
18
|
+
const Other = $derived(override(ui, "Card") ?? Cards);
|
|
19
|
+
const plan = $derived(message.card?.kind === "plan" ? plans.get(message.card.id) : undefined);
|
|
20
|
+
let copied = $state(false);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class={cls(ui, "message") + " " + cls(ui, message.role === "user" ? "message.user" : "message.assistant")} data-kind={message.kind} data-pending={message.pending}>
|
|
24
|
+
{#if message.card}
|
|
25
|
+
{#if plan}<Approval {plan} />{:else if message.card.kind !== "plan"}<Other card={message.card} {runId} />{/if}
|
|
26
|
+
{:else}
|
|
27
|
+
{#if message.text.length > 0}<div class={cls(ui, "message.text")}><RevealText text={message.text} {reveal} /></div>{/if}
|
|
28
|
+
{#each message.blocks as b, i (i)}<BlockView block={b} />{/each}
|
|
29
|
+
{#if message.next.length > 0}
|
|
30
|
+
<div class={cls(ui, "block.actions")}>{#each message.next as n (n)}<button type="button" class={cls(ui, "block.action")} onclick={() => ui.assistant.send(n)}>{n}</button>{/each}</div>
|
|
31
|
+
{/if}
|
|
32
|
+
{#if message.role === "assistant"}
|
|
33
|
+
<div class={cls(ui, "message.feedback")}>
|
|
34
|
+
<Small kind="provenance" provenance={message.provenance} />
|
|
35
|
+
{#if ui.assistant.host.copy}<button type="button" onclick={async () => { await ui.assistant.host.copy?.(message.text); copied = true; }}>{copied ? "✓" : t("feedback.copy")}</button>{/if}
|
|
36
|
+
{#if message.provenance === "learned"}<button type="button" onclick={() => ui.assistant.notRight(message.id)}>{t("feedback.not_right")}</button>{/if}
|
|
37
|
+
</div>
|
|
38
|
+
{/if}
|
|
39
|
+
{/if}
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MessageView } from "@g9tm/aiassistant";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
message: MessageView;
|
|
4
|
+
plans: ReadonlyMap<string, import("@g9tm/aiassistant").PlanView>;
|
|
5
|
+
runId: string | null;
|
|
6
|
+
};
|
|
7
|
+
declare const Message: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Message = ReturnType<typeof Message>;
|
|
9
|
+
export default Message;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The messages, opened at the end; a new question stays at the top of the view so its answer is read from the start.
|
|
3
|
+
// New messages are announced politely to screen readers.
|
|
4
|
+
import type { MessageView, PlanView } from "@g9tm/aiassistant";
|
|
5
|
+
import { cls, override, useUi } from "../context.js";
|
|
6
|
+
import Message from "./Message.svelte";
|
|
7
|
+
|
|
8
|
+
let { messages, plans, runId }: { messages: MessageView[]; plans: ReadonlyMap<string, PlanView>; runId: string | null } = $props();
|
|
9
|
+
const ui = useUi();
|
|
10
|
+
const Item = $derived(override(ui, "Message") ?? Message);
|
|
11
|
+
let box: HTMLDivElement | undefined = $state();
|
|
12
|
+
let count = 0;
|
|
13
|
+
$effect(() => {
|
|
14
|
+
const n = messages.length;
|
|
15
|
+
if (!box || n === count) return;
|
|
16
|
+
const last = messages[n - 1];
|
|
17
|
+
const node = box.querySelector<HTMLElement>('[data-id="' + CSS.escape(last?.id ?? "") + '"]');
|
|
18
|
+
if (count === 0) box.scrollTop = box.scrollHeight; // history opens at its end
|
|
19
|
+
else if (last?.role === "user" && node) box.scrollTop = node.offsetTop - box.offsetTop; // the question at the top
|
|
20
|
+
count = n;
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class={cls(ui, "messages")} bind:this={box} aria-live="polite">
|
|
25
|
+
{#each messages as m (m.id)}
|
|
26
|
+
<div data-id={m.id}><Item message={m} {plans} {runId} /></div>
|
|
27
|
+
{/each}
|
|
28
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MessageView, PlanView } from "@g9tm/aiassistant";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
messages: MessageView[];
|
|
4
|
+
plans: ReadonlyMap<string, PlanView>;
|
|
5
|
+
runId: string | null;
|
|
6
|
+
};
|
|
7
|
+
declare const MessageList: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type MessageList = ReturnType<typeof MessageList>;
|
|
9
|
+
export default MessageList;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Reveals an answer that arrived in front of the person over about 1.5 s; history and reduced motion show it whole.
|
|
3
|
+
import { onDestroy } from "svelte";
|
|
4
|
+
import { visibleText, REVEAL_MS } from "@g9tm/aiassistant";
|
|
5
|
+
import { cls, useUi } from "../context.js";
|
|
6
|
+
import { Ticker } from "../store.svelte.js";
|
|
7
|
+
import TextView from "./TextView.svelte";
|
|
8
|
+
|
|
9
|
+
let { text, reveal = false }: { text: string; reveal?: boolean } = $props();
|
|
10
|
+
const ui = useUi();
|
|
11
|
+
const started = Date.now();
|
|
12
|
+
const ticker = new Ticker();
|
|
13
|
+
const animate = $derived(reveal && !ui.assistant.host.reducedMotion);
|
|
14
|
+
$effect(() => {
|
|
15
|
+
if (animate) ticker.start(80);
|
|
16
|
+
});
|
|
17
|
+
const shown = $derived(animate ? visibleText(text, ticker.now - started, false) : text);
|
|
18
|
+
$effect(() => {
|
|
19
|
+
if (ticker.now - started >= REVEAL_MS) ticker.stop();
|
|
20
|
+
});
|
|
21
|
+
onDestroy(() => ticker.stop());
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class={cls(ui, "reveal-text")} aria-busy={shown.length < text.length}>
|
|
25
|
+
<TextView text={shown} />
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// Small parts used across the components (each also exported on its own).
|
|
3
|
+
export type SmallKind = "provenance" | "mode" | "usage";
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { windowView, type AssistantState, type Provenance } from "@g9tm/aiassistant";
|
|
8
|
+
import { cls, useUi } from "../context.js";
|
|
9
|
+
|
|
10
|
+
let { kind, provenance = null, state = null }: { kind: SmallKind; provenance?: Provenance | null; state?: AssistantState | null } = $props();
|
|
11
|
+
const ui = useUi();
|
|
12
|
+
const t = ui.texts.t;
|
|
13
|
+
const until = $derived(state?.until ? new Date(state.until).toLocaleTimeString(ui.texts.locale === "fa" ? "fa-IR" : "en-GB", { hour: "2-digit", minute: "2-digit" }) : null);
|
|
14
|
+
const usage = $derived(state?.usage[0] ? windowView(state.usage[0], Date.now()) : null);
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
{#if kind === "provenance" && provenance}
|
|
18
|
+
<span class={cls(ui, "message.provenance")} data-provenance={provenance}>{t("provenance." + provenance)}</span>
|
|
19
|
+
{:else if kind === "mode" && state && state.mode !== "on"}
|
|
20
|
+
<div class={cls(ui, "mode-banner")} role="status">{state.mode === "off" ? t("mode.off") : until ? t("mode.offline_until", { until }) : t("mode.offline")}</div>
|
|
21
|
+
{:else if kind === "usage" && usage}
|
|
22
|
+
<span class={cls(ui, "usage-chip")} style={"--g9a-usage:" + String(Math.round(usage.left * 100)) + "%"}>{t("usage.left", { percent: Math.round(usage.left * 100) + "%" })}</span>
|
|
23
|
+
{/if}
|
|
24
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SmallKind = "provenance" | "mode" | "usage";
|
|
2
|
+
import { type AssistantState, type Provenance } from "@g9tm/aiassistant";
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
kind: SmallKind;
|
|
5
|
+
provenance?: Provenance | null;
|
|
6
|
+
state?: AssistantState | null;
|
|
7
|
+
};
|
|
8
|
+
declare const Small: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
|
+
type Small = ReturnType<typeof Small>;
|
|
10
|
+
export default Small;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Named stages lit by the run's real stage events (a pipeline's roles too); never a show of thinking.
|
|
3
|
+
import type { RunView } from "@g9tm/aiassistant";
|
|
4
|
+
import { cls, useUi } from "../context.js";
|
|
5
|
+
|
|
6
|
+
let { run, names = {} }: { run: RunView; names?: Record<string, string> } = $props();
|
|
7
|
+
const ui = useUi();
|
|
8
|
+
const name = (s: RunView["stages"][number]): string => (s.role ? (new Map(Object.entries(names)).get(s.role) ?? s.role) : ui.texts.t("stage." + s.id));
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div class={cls(ui, "stages-thinking")}>
|
|
12
|
+
{#each run.stages.filter((s) => s.id === "role" || names[s.id] !== undefined || s.id !== "waiting") as s, i (i)}
|
|
13
|
+
<span data-state={s.state} data-stage={s.id}>{name(s)}</span>
|
|
14
|
+
{/each}
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RunView } from "@g9tm/aiassistant";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
run: RunView;
|
|
4
|
+
names?: Record<string, string>;
|
|
5
|
+
};
|
|
6
|
+
declare const StagesThinking: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type StagesThinking = ReturnType<typeof StagesThinking>;
|
|
8
|
+
export default StagesThinking;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The model's text as safe tokens: paragraphs, lists, bold, code, links (https on the host's hosts only). Never HTML.
|
|
3
|
+
import { parseText, type Inline } from "@g9tm/aiassistant";
|
|
4
|
+
import { useUi } from "../context.js";
|
|
5
|
+
|
|
6
|
+
let { text }: { text: string } = $props();
|
|
7
|
+
const ui = useUi();
|
|
8
|
+
const blocks = $derived(parseText(text, ui.assistant.host.linkHosts ?? []));
|
|
9
|
+
|
|
10
|
+
function open(e: MouseEvent, href: string): void {
|
|
11
|
+
if (ui.assistant.host.openUrl) {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
ui.assistant.host.openUrl(href);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#snippet line(xs: Inline[])}
|
|
19
|
+
{#each xs as x, i (i)}
|
|
20
|
+
{#if x.kind === "bold"}<strong>{x.text}</strong>
|
|
21
|
+
{:else if x.kind === "code"}<code>{x.text}</code>
|
|
22
|
+
{:else if x.kind === "link" && x.href}<a href={x.href} target="_blank" rel="noopener noreferrer" onclick={(e) => open(e, x.href ?? "")}>{x.text}</a>
|
|
23
|
+
{:else}{x.text}{/if}
|
|
24
|
+
{/each}
|
|
25
|
+
{/snippet}
|
|
26
|
+
|
|
27
|
+
{#each blocks as b, i (i)}
|
|
28
|
+
{#if b.kind === "paragraph"}
|
|
29
|
+
<p class="g9a-p">{@render line(b.inlines)}</p>
|
|
30
|
+
{:else if b.ordered}
|
|
31
|
+
<ol class="g9a-list">{#each b.items as it, n (n)}<li>{@render line(it)}</li>{/each}</ol>
|
|
32
|
+
{:else}
|
|
33
|
+
<ul class="g9a-list">{#each b.items as it, n (n)}<li>{@render line(it)}</li>{/each}</ul>
|
|
34
|
+
{/if}
|
|
35
|
+
{/each}
|
|
36
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The thinking orb: transform and opacity only; static with reduced motion.
|
|
3
|
+
import { cls, useUi } from "../context.js";
|
|
4
|
+
const ui = useUi();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<span class={cls(ui, "thinking-orb")} data-still={ui.assistant.host.reducedMotion} aria-hidden="true"></span>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const ThinkingOrb: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type ThinkingOrb = InstanceType<typeof ThinkingOrb>;
|
|
18
|
+
export default ThinkingOrb;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// "Working": the run's real stages (from its stage events), the progress line, tools, the clock, stop; after 40 s,
|
|
3
|
+
// "still working". The thinking orb is static with reduced motion.
|
|
4
|
+
import { onDestroy, untrack } from "svelte";
|
|
5
|
+
import { finished, LONG_RUNNING_MS, type RunView } from "@g9tm/aiassistant";
|
|
6
|
+
import { cls, useUi } from "../context.js";
|
|
7
|
+
import { Ticker } from "../store.svelte.js";
|
|
8
|
+
import ThinkingOrb from "./ThinkingOrb.svelte";
|
|
9
|
+
|
|
10
|
+
let { run }: { run: RunView } = $props();
|
|
11
|
+
const ui = useUi();
|
|
12
|
+
const t = ui.texts.t;
|
|
13
|
+
const ticker = new Ticker();
|
|
14
|
+
// The clock of this run: set again when another run comes into the same card (a second question).
|
|
15
|
+
const runId = $derived(run.runId);
|
|
16
|
+
const started = $derived.by(() => { void runId; return untrack(() => Date.now() - run.elapsedMs); });
|
|
17
|
+
$effect(() => {
|
|
18
|
+
if (finished(run)) ticker.stop();
|
|
19
|
+
else ticker.start(1000);
|
|
20
|
+
});
|
|
21
|
+
onDestroy(() => ticker.stop());
|
|
22
|
+
const seconds = $derived(Math.max(0, Math.round((ticker.now - started) / 1000)));
|
|
23
|
+
const long = $derived(run.longRunning || ticker.now - started >= LONG_RUNNING_MS);
|
|
24
|
+
const label = (s: RunView["stages"][number]): string => (s.id === "role" ? t("stage.role", { role: s.role ?? "" }) : t("stage." + s.id));
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
{#if !finished(run) && run.state !== "waiting-confirm" && run.state !== "waiting-answer"}
|
|
28
|
+
<div class={cls(ui, "work")} role="status">
|
|
29
|
+
<ThinkingOrb />
|
|
30
|
+
<div class="g9a-work-main">
|
|
31
|
+
<div class="g9a-work-head"><strong>{long ? t("work.long") : t("work.title")}</strong><span class={cls(ui, "work.elapsed")}>{t("work.elapsed", { seconds })}</span></div>
|
|
32
|
+
<div class="g9a-stages">
|
|
33
|
+
{#each run.stages.filter((s) => s.id !== "waiting") as s, i (i)}<span class={cls(ui, "work.stage")} data-state={s.state}>{label(s)}</span>{/each}
|
|
34
|
+
</div>
|
|
35
|
+
{#if run.progress}<div class={cls(ui, "work.progress")}>{t(run.progress.key, run.progress.args)}</div>{/if}
|
|
36
|
+
{#each run.tools.slice(-3) as tool, i (i)}<div class={cls(ui, "work.tool")} data-state={tool.state}>{t(tool.title.key) === tool.title.key ? tool.name : t(tool.title.key)}</div>{/each}
|
|
37
|
+
{#if run.streamingText}<div class="g9a-stream">{run.streamingText}</div>{/if}
|
|
38
|
+
</div>
|
|
39
|
+
<button type="button" class={cls(ui, "composer.stop")} onclick={() => ui.assistant.stop()}>{t("composer.stop")}</button>
|
|
40
|
+
</div>
|
|
41
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The default Dialog slot: above the panel (sheetLayer + 1), Escape closes, focus returns to what opened it.
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import { useUi } from "../context.js";
|
|
5
|
+
|
|
6
|
+
let { open = $bindable(false), label, children }: { open?: boolean; label: string; children: Snippet } = $props();
|
|
7
|
+
const ui = useUi();
|
|
8
|
+
let before: Element | null = null;
|
|
9
|
+
$effect(() => {
|
|
10
|
+
if (open) before = document.activeElement;
|
|
11
|
+
else if (before instanceof HTMLElement) before.focus();
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#if open}
|
|
16
|
+
<div class="g9a-dialog" role="dialog" aria-modal="true" aria-label={label} style:z-index={ui.layer + 1} tabindex="-1"
|
|
17
|
+
onkeydown={(e) => { if (e.key === "Escape") open = false; }}>
|
|
18
|
+
{@render children()}
|
|
19
|
+
</div>
|
|
20
|
+
{/if}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
label: string;
|
|
5
|
+
children: Snippet;
|
|
6
|
+
};
|
|
7
|
+
declare const Dialog: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
8
|
+
type Dialog = ReturnType<typeof Dialog>;
|
|
9
|
+
export default Dialog;
|