@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.
- package/README.md +29 -2
- package/dist/AssistantRoot.svelte +130 -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 +84 -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/blocks/CsvButton.svelte +27 -0
- package/dist/blocks/CsvButton.svelte.d.ts +8 -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 +38 -0
- package/dist/index.js +36 -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 +96 -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 +82 -0
- package/dist/pages/Page.svelte.d.ts +8 -0
- package/dist/pages/RoutineWizard.svelte +197 -0
- package/dist/pages/RoutineWizard.svelte.d.ts +6 -0
- package/dist/pages/SlotPicker.svelte +55 -0
- package/dist/pages/SlotPicker.svelte.d.ts +17 -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/pages/texts.d.ts +9 -0
- package/dist/pages/texts.js +42 -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/PdfButton.svelte +37 -0
- package/dist/parts/PdfButton.svelte.d.ts +3 -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 +52 -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,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,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,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,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,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;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The default PinPad slot: a numeric code field (a host may give its own).
|
|
3
|
+
let { value = $bindable(""), label }: { value?: string; label: string } = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<input class="g9a-pin" type="password" inputmode="numeric" autocomplete="one-time-code" aria-label={label} placeholder={label} bind:value />
|