@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
package/README.md CHANGED
@@ -1,3 +1,30 @@
1
- # Temporary Holding Version
1
+ # @g9tm/aiassistant-svelte
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Svelte 5 components for the G9 AI assistant, drawn from the headless client `@g9tm/aiassistant`. Every part can be
4
+ restyled or replaced without forking.
5
+
6
+ - Surfaces: the panel in six layouts (side, sheet, fullscreen, inline, card, compact, or `auto` by width), the chat,
7
+ and the person's pages (history, abilities, knowledge, learned items, findings, routines, actions).
8
+ - Parts and cards: messages, the composer, the work card, blocks and charts, approval cards with a PIN pad, secret
9
+ fields and dialogs.
10
+ - Five layers of customising: theme tokens (light and dark), per-part classes and styles, an `unstyled` switch that
11
+ keeps structure and classes only, a replaced component by name, and panels built from parts alone.
12
+ - `browserHost()`: a ready host for a plain browser app (fetch with the host's headers, `EventSource`, clipboard,
13
+ files, recording for dictation).
14
+ - `@g9tm/aiassistant-svelte/admin`: the manager's page, as a separate entry so people who never open it never load it.
15
+
16
+ ```svelte
17
+ <script lang="ts">
18
+ import { createAssistant } from "@g9tm/aiassistant";
19
+ import { AssistantRoot, AssistantPanel, browserHost } from "@g9tm/aiassistant-svelte";
20
+
21
+ const assistant = createAssistant(browserHost({ locale: "en", headers: () => ({ Authorization: "Bearer " + token() }) }));
22
+ let open = $state(true);
23
+ </script>
24
+
25
+ <AssistantRoot {assistant}>
26
+ <AssistantPanel bind:open layout="auto" />
27
+ </AssistantRoot>
28
+ ```
29
+
30
+ Part of the G9 AI assistant library. Documentation: see the guides that ship with the library.
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ // The root of one assistant: shares the core, texts, part styles, overrides and theme with every component inside.
3
+ // All the library's styling is here, under .g9a-styled, from theme tokens; unstyled leaves structure and classes only.
4
+ import type { Snippet } from "svelte";
5
+ import type { Assistant, PartStyles } from "@g9tm/aiassistant";
6
+ import { provideUi, type Overrides } from "./context.js";
7
+ import { themeVars } from "./theme.js";
8
+
9
+ let { assistant, styles = {}, unstyled = false, components = {}, children }: {
10
+ assistant: Assistant; styles?: PartStyles; unstyled?: boolean; components?: Overrides; children: Snippet;
11
+ } = $props();
12
+
13
+ // Getters, not copies: a host that switches unstyled, styles or a replaced component sees it at once.
14
+ provideUi({
15
+ get assistant() { return assistant; },
16
+ get texts() { return assistant.texts; },
17
+ get styles() { return styles; },
18
+ get unstyled() { return unstyled; },
19
+ get components() { return components; },
20
+ get layer() { return (assistant.host.sheetLayer ?? 1000) + 1; },
21
+ });
22
+ const vars = $derived(unstyled ? "" : themeVars(assistant.host.theme));
23
+ </script>
24
+
25
+ <div class={unstyled ? "g9a-root" : "g9a-root g9a-styled"} dir={assistant.host.dir} lang={assistant.host.locale} style={vars}>
26
+ {@render children()}
27
+ </div>
28
+
29
+ <style>
30
+ .g9a-root { display: contents; }
31
+ .g9a-styled { font-family: var(--g9a-font); font-size: var(--g9a-font-size); color: var(--g9a-ink); line-height: 1.6; }
32
+ .g9a-styled :global(button) { font: inherit; cursor: pointer; min-block-size: 38px; padding: 0 12px; border-radius: var(--g9a-radius-s); border: 1px solid var(--g9a-line); background: var(--g9a-surface); color: var(--g9a-ink); }
33
+ .g9a-styled :global(input), .g9a-styled :global(textarea) { font: inherit; padding: 8px 10px; border: 1px solid var(--g9a-line); border-radius: var(--g9a-radius-s); background: var(--g9a-surface); color: var(--g9a-ink); }
34
+ .g9a-styled :global(.g9a-panel) { box-sizing: border-box; display: flex; flex-direction: column; background: var(--g9a-surface); color: var(--g9a-ink); box-shadow: var(--g9a-shadow); overflow: hidden; }
35
+ .g9a-styled :global(.g9a-panel[data-layout="side"]) { position: fixed; inset-block: 0; inset-inline-end: 0; inline-size: min(420px, 100%); border-inline-start: 1px solid var(--g9a-line); }
36
+ .g9a-styled :global(.g9a-panel[data-layout="sheet"]) { position: fixed; inset-inline: 0; inset-block-end: 0; block-size: 85vh; border-start-start-radius: 16px; border-start-end-radius: 16px; }
37
+ .g9a-styled :global(.g9a-panel[data-layout="fullscreen"]) { position: fixed; inset: 0; }
38
+ .g9a-styled :global(.g9a-panel[data-layout="inline"]) { inline-size: 100%; block-size: 100%; box-shadow: none; border: 1px solid var(--g9a-line); border-radius: var(--g9a-radius); }
39
+ .g9a-styled :global(.g9a-panel[data-layout="card"]) { border-radius: var(--g9a-radius); border: 1px solid var(--g9a-line); max-block-size: 520px; }
40
+ .g9a-styled :global(.g9a-panel[data-layout="compact"]) { box-shadow: none; }
41
+ .g9a-styled :global(.g9a-panel__header) { display: flex; align-items: center; gap: var(--g9a-gap); padding: var(--g9a-pad); border-bottom: 1px solid var(--g9a-line); }
42
+ .g9a-styled :global(.g9a-panel__header > strong) { flex: 1; min-inline-size: 0; }
43
+ .g9a-styled :global(.g9a-panel__body), .g9a-styled :global(.g9a-chat) { display: flex; flex-direction: column; flex: 1; min-block-size: 0; }
44
+ .g9a-styled :global(.g9a-messages) { flex: 1; overflow-y: auto; padding: var(--g9a-pad); display: flex; flex-direction: column; gap: var(--g9a-gap); }
45
+ .g9a-styled :global(.g9a-message) { max-inline-size: 88%; padding: 8px 12px; border-radius: var(--g9a-radius); background: var(--g9a-surface-2); }
46
+ .g9a-styled :global(.g9a-message__user) { align-self: flex-start; background: color-mix(in srgb, var(--g9a-accent) 12%, var(--g9a-surface)); }
47
+ .g9a-styled :global(.g9a-message__assistant) { align-self: flex-end; }
48
+ .g9a-styled :global(.g9a-message[data-pending="true"]) { opacity: .7; }
49
+ .g9a-styled :global(.g9a-message__feedback) { display: flex; gap: 6px; align-items: center; margin-block-start: 4px; }
50
+ .g9a-styled :global(.g9a-message__feedback button) { min-block-size: 26px; font-size: 12px; padding: 0 8px; }
51
+ .g9a-styled :global(.g9a-message__provenance) { font-size: 11px; color: var(--g9a-muted); }
52
+ .g9a-styled :global(.g9a-p) { margin: 0 0 6px; white-space: pre-wrap; overflow-wrap: anywhere; }
53
+ .g9a-styled :global(code) { font-family: ui-monospace, monospace; background: var(--g9a-surface); border-radius: 4px; padding: 0 4px; }
54
+ .g9a-styled :global(a) { color: var(--g9a-accent); }
55
+ .g9a-styled :global(.g9a-work) { display: flex; gap: var(--g9a-gap); align-items: flex-start; padding: var(--g9a-pad); border-top: 1px solid var(--g9a-line); }
56
+ .g9a-styled :global(.g9a-work-main) { flex: 1; min-inline-size: 0; }
57
+ .g9a-styled :global(.g9a-work-head) { display: flex; justify-content: space-between; gap: var(--g9a-gap); }
58
+ .g9a-styled :global(.g9a-work__elapsed), .g9a-styled :global(.g9a-work__tool), .g9a-styled :global(.g9a-note) { color: var(--g9a-muted); font-size: 12px; }
59
+ .g9a-styled :global(.g9a-stages), .g9a-styled :global(.g9a-stages-thinking) { display: flex; flex-wrap: wrap; gap: 6px; margin-block: 4px; }
60
+ .g9a-styled :global(.g9a-work__stage), .g9a-styled :global(.g9a-stages-thinking > span) { padding: 1px 10px; border-radius: 999px; border: 1px solid var(--g9a-line); font-size: 12px; color: var(--g9a-muted); }
61
+ .g9a-styled :global([data-state="on"].g9a-work__stage), .g9a-styled :global(.g9a-stages-thinking > [data-state="on"]) { background: color-mix(in srgb, var(--g9a-accent) 20%, transparent); color: var(--g9a-ink); border-color: var(--g9a-accent); }
62
+ .g9a-styled :global(.g9a-thinking-orb) { inline-size: 18px; block-size: 18px; border-radius: 50%; background: var(--g9a-accent); animation: g9a-pulse 1.2s ease-in-out infinite; flex: none; margin-block-start: 4px; }
63
+ .g9a-styled :global(.g9a-thinking-orb[data-still="true"]) { animation: none; }
64
+ @keyframes -global-g9a-pulse { 0%, 100% { transform: scale(.8); opacity: .6; } 50% { transform: scale(1); opacity: 1; } }
65
+ @media (prefers-reduced-motion: reduce) { .g9a-styled :global(.g9a-thinking-orb) { animation: none; } }
66
+ .g9a-styled :global(.g9a-composer) { padding: var(--g9a-pad); border-top: 1px solid var(--g9a-line); }
67
+ .g9a-styled :global(.g9a-composer-row) { display: flex; gap: var(--g9a-gap); align-items: flex-end; }
68
+ .g9a-styled :global(.g9a-composer__input) { flex: 1; min-inline-size: 0; resize: none; max-block-size: 30vh; }
69
+ .g9a-styled :global(.g9a-composer__send) { background: var(--g9a-accent); color: var(--g9a-accent-ink); border-color: var(--g9a-accent); }
70
+ .g9a-styled :global(.g9a-file-chips) { display: flex; flex-wrap: wrap; gap: 4px; margin-block-end: 6px; }
71
+ .g9a-styled :global(.g9a-file-chip) { background: var(--g9a-surface-2); border-radius: 999px; padding: 2px 4px 2px 10px; font-size: 12px; }
72
+ .g9a-styled :global(.g9a-file-chip button) { min-block-size: 20px; border: 0; background: none; padding: 0 6px; }
73
+ .g9a-styled :global(.g9a-card__approval) { border: 1px solid var(--g9a-approval-card-border); border-radius: var(--g9a-radius); padding: var(--g9a-pad); background: var(--g9a-surface); }
74
+ .g9a-styled :global(.g9a-card__approval h3) { margin: 0 0 6px; font-size: 15px; }
75
+ .g9a-styled :global(.g9a-card__approval__fact) { font-size: 12px; color: var(--g9a-muted); padding-inline-start: 24px; }
76
+ .g9a-styled :global(.g9a-card__approval__confirm) { background: var(--g9a-accent); color: var(--g9a-accent-ink); border-color: var(--g9a-accent); }
77
+ .g9a-styled :global(.g9a-warning) { color: var(--g9a-warn); font-size: 13px; }
78
+ .g9a-styled :global(.g9a-irreversible) { color: var(--g9a-danger); font-size: 12px; }
79
+ .g9a-styled :global(.g9a-row) { display: flex; gap: var(--g9a-gap); margin-block-start: 8px; }
80
+ .g9a-styled :global(.g9a-mode-banner) { background: var(--g9a-surface-2); border-bottom: 1px solid var(--g9a-line); color: var(--g9a-warn); padding: 6px var(--g9a-pad); font-size: 13px; }
81
+ .g9a-styled :global(.g9a-usage-chip) { display: inline-block; block-size: 22px; line-height: 22px; padding: 0 8px; border-radius: 999px; font-size: 11px;
82
+ background: linear-gradient(to left, var(--g9a-surface-2) calc(100% - var(--g9a-usage)), color-mix(in srgb, var(--g9a-accent) 25%, transparent) 0); }
83
+ .g9a-styled :global(.g9a-tabs) { display: flex; gap: 4px; border-bottom: 1px solid var(--g9a-line); margin-block-end: 8px; }
84
+ .g9a-styled :global(.g9a-tabs [aria-selected="true"]) { border-bottom: 2px solid var(--g9a-accent); }
85
+ .g9a-styled :global(.g9a-admin-capability) { border: 1px solid var(--g9a-line); border-radius: 8px; padding: 8px; margin-block-end: 8px; display: grid; gap: 4px; }
86
+ .g9a-styled :global(.g9a-admin-capability label) { display: flex; justify-content: space-between; gap: 8px; font-size: 13px; }
87
+ .g9a-styled :global([data-bad="true"]), .g9a-styled :global([data-ok="false"]) { color: var(--g9a-danger); }
88
+ .g9a-styled :global(.g9a-block-title) { font-weight: 600; margin-block: 4px; }
89
+ .g9a-styled :global(.g9a-block__table table) { border-collapse: collapse; inline-size: 100%; font-size: 13px; }
90
+ .g9a-styled :global(.g9a-block__table th), .g9a-styled :global(.g9a-block__table td) { border-bottom: 1px solid var(--g9a-line); padding: 4px 6px; text-align: start; }
91
+ .g9a-styled :global([data-kind="number"]), .g9a-styled :global([data-kind="money"]), .g9a-styled :global([data-kind="percent"]) { text-align: end; font-variant-numeric: tabular-nums; }
92
+ .g9a-styled :global(.g9a-block__table__card) { display: grid; grid-template-columns: auto 1fr; gap: 2px 10px; margin: 0 0 6px; padding: 6px 8px; background: var(--g9a-surface); border-radius: var(--g9a-radius-s); }
93
+ .g9a-styled :global(.g9a-block__table__card dt) { color: var(--g9a-muted); }
94
+ .g9a-styled :global(.g9a-block__table__card dd) { margin: 0; }
95
+ .g9a-styled :global(.g9a-block__chart__bar) { display: grid; grid-template-columns: 1fr; gap: 2px; margin-block-end: 6px; }
96
+ .g9a-styled :global(.g9a-bar-track) { display: block; block-size: 10px; background: var(--g9a-surface); border-radius: 5px; overflow: hidden; }
97
+ .g9a-styled :global(.g9a-bar-fill) { display: block; block-size: 100%; background: var(--g9a-chart-bar); border-radius: 5px; }
98
+ .g9a-styled :global(.g9a-bar-value) { font-size: 12px; color: var(--g9a-muted); }
99
+ .g9a-styled :global(.g9a-block__chart__line) { inline-size: 100%; block-size: 140px; }
100
+ .g9a-styled :global(.g9a-block__chart__line path) { stroke: var(--g9a-chart-bar); stroke-width: 2; }
101
+ .g9a-styled :global(.g9a-donut) { inline-size: 120px; }
102
+ .g9a-styled :global(.g9a-donut path) { fill: none; stroke: var(--g9a-chart-bar); stroke-width: 14; }
103
+ .g9a-styled :global(.g9a-block__kpi) { display: flex; flex-wrap: wrap; gap: var(--g9a-gap); }
104
+ .g9a-styled :global(.g9a-block__kpi > div) { display: flex; flex-direction: column; padding: 6px 10px; background: var(--g9a-surface); border-radius: var(--g9a-radius-s); }
105
+ .g9a-styled :global(.g9a-block__actions) { display: flex; flex-wrap: wrap; gap: 6px; margin-block-start: 6px; }
106
+ .g9a-styled :global(.g9a-block__action) { background: color-mix(in srgb, var(--g9a-accent) 12%, var(--g9a-surface)); border-color: var(--g9a-accent); }
107
+ .g9a-styled :global(.g9a-block__action::after) { content: " ‹"; }
108
+ .g9a-styled :global([dir="ltr"] .g9a-block__action::after) { content: " ›"; }
109
+ .g9a-styled :global(.g9a-suggested) { font-size: 10px; color: var(--g9a-muted); margin-inline-start: 6px; }
110
+ .g9a-styled :global(.g9a-block__callout) { border-inline-start: 3px solid var(--g9a-accent); padding: 6px 10px; background: var(--g9a-surface); }
111
+ .g9a-styled :global(.g9a-block__callout[data-tone="danger"]) { border-color: var(--g9a-danger); }
112
+ .g9a-styled :global(.g9a-block__callout[data-tone="warn"]) { border-color: var(--g9a-warn); }
113
+ .g9a-styled :global(.g9a-block__image img) { max-inline-size: 100%; border-radius: var(--g9a-radius-s); }
114
+ .g9a-styled :global(.g9a-dialog) { position: fixed; inset: 10% 8%; background: var(--g9a-surface); border-radius: var(--g9a-radius); box-shadow: var(--g9a-shadow); padding: var(--g9a-pad); overflow: auto; }
115
+ .g9a-styled :global(.g9a-hero) { padding: var(--g9a-pad); border: 1px solid var(--g9a-line); border-radius: var(--g9a-radius); background: var(--g9a-surface); }
116
+ .g9a-styled :global(.g9a-badge) { margin-inline-start: 6px; background: var(--g9a-danger); color: #fff; border-radius: 999px; padding: 0 6px; font-size: 11px; }
117
+ .g9a-styled :global(.g9a-measure) { inline-size: 100%; block-size: 0; }
118
+ .g9a-styled :global(.g9a-count) { font-size: 11px; color: var(--g9a-muted); margin-inline-start: 4px; }
119
+ .g9a-styled :global(.g9a-history__list), .g9a-styled :global(.g9a-routines__list), .g9a-styled :global(.g9a-picker__list) { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
120
+ .g9a-styled :global(.g9a-history__row) { display: flex; gap: 4px; align-items: center; }
121
+ .g9a-styled :global(.g9a-history__open) { flex: 1; min-inline-size: 0; text-align: start; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
122
+ .g9a-styled :global(.g9a-history__row[data-waiting="true"] .g9a-history__open) { border-color: var(--g9a-warn); }
123
+ .g9a-styled :global(.g9a-history__delete), .g9a-styled :global(.g9a-history__delete-yes) { color: var(--g9a-danger); }
124
+ .g9a-styled :global(.g9a-routines__form) { display: grid; gap: var(--g9a-gap); }
125
+ .g9a-styled :global(.g9a-routines__slot) { display: grid; gap: 2px; }
126
+ .g9a-styled :global(.g9a-routines__slot[data-state="missing"] input), .g9a-styled :global(.g9a-routines__slot[data-state="invalid"] input) { border-color: var(--g9a-danger); }
127
+ .g9a-styled :global(.g9a-routines__choose) { inline-size: 100%; text-align: start; }
128
+ .g9a-styled :global(.g9a-picker__chosen) { font-weight: 600; margin-inline-end: 6px; }
129
+ .g9a-styled :global(.g9a-csv) { display: flex; gap: 6px; align-items: center; margin-block: 4px; }
130
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Assistant, PartStyles } from "@g9tm/aiassistant";
3
+ import { type Overrides } from "./context.js";
4
+ type $$ComponentProps = {
5
+ assistant: Assistant;
6
+ styles?: PartStyles;
7
+ unstyled?: boolean;
8
+ components?: Overrides;
9
+ children: Snippet;
10
+ };
11
+ declare const AssistantRoot: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type AssistantRoot = ReturnType<typeof AssistantRoot>;
13
+ export default AssistantRoot;
@@ -0,0 +1,2 @@
1
+ /** The manager's pages: a separate entry (`@g9tm/aiassistant-svelte/admin`), a lazy chunk only managers load. */
2
+ export { default as AdminPage } from "./pages/AdminPage.svelte";
package/dist/admin.js ADDED
@@ -0,0 +1,2 @@
1
+ /** The manager's pages: a separate entry (`@g9tm/aiassistant-svelte/admin`), a lazy chunk only managers load. */
2
+ export { default as AdminPage } from "./pages/AdminPage.svelte";
@@ -0,0 +1,84 @@
1
+ <script lang="ts">
2
+ // Every block type from the core's view model; the host may replace any type (including its x:* blocks). A block no
3
+ // renderer knows shows its fallback text, never an error or a hole.
4
+ import { blockView, can, obj } from "@g9tm/aiassistant";
5
+ import { blockOverride, cls, override, useUi } from "../context.js";
6
+ import Chart from "./Chart.svelte";
7
+ import CsvButton from "./CsvButton.svelte";
8
+ import TextView from "../parts/TextView.svelte";
9
+
10
+ let { block, compact = false }: { block: unknown; compact?: boolean } = $props();
11
+ const ui = useUi();
12
+ const t = ui.texts.t;
13
+ const view = $derived(blockView(block, ui.texts.locale, ui.assistant.customBlocks));
14
+ const custom = $derived(blockOverride(ui, view.type === "custom" ? view.name : view.type));
15
+ const Csv = $derived(override(ui, "CsvButton") ?? CsvButton);
16
+ // A table's file has the raw values (the export view of the same rows), not the text drawn for reading.
17
+ const raw = $derived(view.type === "table" ? blockView({ ...obj(block), type: "export" }, ui.texts.locale) : null);
18
+ let open = $state(false);
19
+ const fileUrl = (id: string): string => "/api/assistant/files/" + encodeURIComponent(id); // only the server's own files
20
+ </script>
21
+
22
+ {#if custom}
23
+ {@const Custom = custom}
24
+ <Custom {view} {block} {compact} />
25
+ {:else if view.type === "text"}
26
+ <div class={cls(ui, "block.text")}><TextView text={view.text} /></div>
27
+ {:else if view.type === "callout"}
28
+ <div class={cls(ui, "block.callout")} data-tone={view.tone}><TextView text={view.text} /></div>
29
+ {:else if view.type === "table"}
30
+ <div class={cls(ui, "block.table")}>
31
+ {#if view.title}<div class="g9a-block-title">{view.title}</div>{/if}
32
+ {#if view.asCards || compact}
33
+ {#each view.rows.slice(0, open ? view.rows.length : view.shown) as row, r (r)}
34
+ <dl class={cls(ui, "block.table.card")}>
35
+ {#each view.columns as c, i (c.name)}<dt>{c.label}</dt><dd data-kind={c.kind}>{row[i]}</dd>{/each}
36
+ </dl>
37
+ {/each}
38
+ {:else}
39
+ <table>
40
+ <thead><tr>{#each view.columns as c (c.name)}<th data-kind={c.kind}>{c.label}</th>{/each}</tr></thead>
41
+ <tbody>
42
+ {#each view.rows.slice(0, open ? view.rows.length : view.shown) as row, r (r)}
43
+ <tr>{#each row as v, i (i)}<td data-kind={view.columns[i]?.kind}>{v}</td>{/each}</tr>
44
+ {/each}
45
+ </tbody>
46
+ </table>
47
+ {/if}
48
+ {#if view.more > 0 && !open}<button type="button" class="g9a-more" onclick={() => (open = true)}>{t("block.more_rows", { count: view.more })}</button>{/if}
49
+ </div>
50
+ {#if raw?.type === "export"}<Csv title={view.title} labels={raw.columns.map((c) => c.label)} rows={raw.rows} />{/if}
51
+ {:else if view.type === "export"}
52
+ <div class={cls(ui, "block") + " g9a-block__export"}>
53
+ {#if view.title}<div class="g9a-block-title">{view.title}</div>{/if}
54
+ {#if can(ui.assistant.host, "save")}<Csv title={view.title} labels={view.columns.map((c) => c.label)} rows={view.rows} />{:else}<div>{view.fallbackText}</div>{/if}
55
+ </div>
56
+ {:else if view.type === "chart"}
57
+ <div class={cls(ui, "block.chart")}>
58
+ {#if view.title}<div class="g9a-block-title">{view.title}</div>{/if}
59
+ <Chart {view} />
60
+ {#if view.fewData}<div class="g9a-note">{t("block.few_data")}</div>{/if}
61
+ </div>
62
+ {:else if view.type === "kpi"}
63
+ <div class={cls(ui, "block.kpi")}>{#each view.items as k, i (i)}<div><span>{k.label}</span><strong>{k.value}</strong></div>{/each}</div>
64
+ {:else if view.type === "steps"}
65
+ <ol class={cls(ui, "block.steps")}>{#each view.items as s, i (i)}<li data-state={s.state}>{s.text}</li>{/each}</ol>
66
+ {:else if view.type === "actions"}
67
+ <div class={cls(ui, "block.actions")}>
68
+ {#each view.items as a, i (i)}
69
+ <button type="button" class={cls(ui, "block.action")} onclick={() => ui.assistant.send(a.send)}>
70
+ {a.label}{#if a.suggested}<span class="g9a-suggested">{t("block.suggested")}</span>{/if}
71
+ </button>
72
+ {/each}
73
+ </div>
74
+ {:else if view.type === "flow"}
75
+ <div class={cls(ui, "block.flow")}>{view.line}</div>
76
+ {:else if view.type === "image"}
77
+ <div class={cls(ui, "block.image")}>{#each view.fileIds as id (id)}<img src={fileUrl(id)} alt={view.fallbackText} loading="lazy" />{/each}</div>
78
+ {:else if view.type === "open" && ui.assistant.host.navigate}
79
+ <button type="button" class={cls(ui, "block.open")} onclick={() => ui.assistant.host.navigate?.(view.type === "open" ? view.pageId : "")}>
80
+ {ui.assistant.host.pageName?.(view.pageId) ?? view.fallbackText}
81
+ </button>
82
+ {:else}
83
+ <div class={cls(ui, "block.fallback")}>{view.fallbackText}</div>
84
+ {/if}
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ block: unknown;
3
+ compact?: boolean;
4
+ };
5
+ declare const BlockView: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type BlockView = ReturnType<typeof BlockView>;
7
+ export default BlockView;
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ // The built-in light chart: horizontal bars for rankings, bars, one or several lines on a time axis,
3
+ // and a donut. A host may give its own chart library through components.Block.chart with the same data.
4
+ import { shares, formatNumber, type BlockView } from "@g9tm/aiassistant";
5
+ import { cls, override, useUi } from "../context.js";
6
+
7
+ let { view }: { view: Extract<BlockView, { type: "chart" }> } = $props();
8
+ const ui = useUi();
9
+ const own = $derived(override(ui, "Chart"));
10
+ const fmt = (v: number): string => formatNumber(v, ui.texts.locale);
11
+ const first = $derived(view.series[0]?.values ?? []);
12
+ const W = 300, H = 140;
13
+ function path(values: readonly number[]): string {
14
+ const max = Math.max(1, ...view.series.flatMap((s) => s.values));
15
+ return values.map((v, i) => (i === 0 ? "M" : "L") + ((i / Math.max(1, values.length - 1)) * W).toFixed(1) + " " + (H - (v / max) * H).toFixed(1)).join(" ");
16
+ }
17
+ const donut = $derived.by(() => {
18
+ const total = first.reduce((a, b) => a + Math.max(0, b), 0) || 1;
19
+ let at = 0;
20
+ return first.map((v) => { const from = at; at += Math.max(0, v) / total; return { from, to: at }; });
21
+ });
22
+ const arc = (from: number, to: number): string => {
23
+ const p = (f: number): string => (50 + 40 * Math.sin(f * 2 * Math.PI)).toFixed(2) + " " + (50 - 40 * Math.cos(f * 2 * Math.PI)).toFixed(2);
24
+ return "M " + p(from) + " A 40 40 0 " + (to - from > 0.5 ? "1" : "0") + " 1 " + p(to);
25
+ };
26
+ </script>
27
+
28
+ {#if own}
29
+ {@const Own = own}
30
+ <Own {view} />
31
+ {:else if view.kind === "hbars" || view.kind === "bars" || view.kind === "stacked"}
32
+ <div class="g9a-hbars" role="img" aria-label={view.fallbackText}>
33
+ {#each view.labels as label, i (i)}
34
+ <div class={cls(ui, "block.chart.bar")}>
35
+ <span class="g9a-bar-label">{label}</span>
36
+ <span class="g9a-bar-track"><span class="g9a-bar-fill" style={"inline-size:" + String(Math.round((shares(first)[i] ?? 0) * 100)) + "%"}></span></span>
37
+ <span class="g9a-bar-value">{fmt(first[i] ?? 0)}</span>
38
+ </div>
39
+ {/each}
40
+ </div>
41
+ {:else if view.kind === "donut"}
42
+ <svg viewBox="0 0 100 100" class="g9a-donut" role="img" aria-label={view.fallbackText}>
43
+ {#each donut as d, i (i)}<path d={arc(d.from, Math.min(d.to, d.from + 0.9999))} data-index={i} />{/each}
44
+ </svg>
45
+ <div class="g9a-legend">{#each view.labels as l, i (i)}<span data-index={i}>{l}</span>{/each}</div>
46
+ {:else}
47
+ <svg viewBox={"0 0 " + String(W) + " " + String(H)} class={cls(ui, "block.chart.line")} role="img" aria-label={view.fallbackText} preserveAspectRatio="none">
48
+ {#each view.series as s, i (s.name)}<path d={path(s.values)} data-index={i} fill="none" />{/each}
49
+ </svg>
50
+ {#if view.series.length > 1}<div class="g9a-legend">{#each view.series as s, i (s.name)}<span data-index={i}>{s.name}</span>{/each}</div>{/if}
51
+ {/if}
@@ -0,0 +1,9 @@
1
+ import { type BlockView } from "@g9tm/aiassistant";
2
+ type $$ComponentProps = {
3
+ view: Extract<BlockView, {
4
+ type: "chart";
5
+ }>;
6
+ };
7
+ declare const Chart: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type Chart = ReturnType<typeof Chart>;
9
+ export default Chart;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ // "Save as CSV" for a table or an export block, through the host's file save (hidden when the host cannot save). The
3
+ // file is the core's: a BOM, every cell quoted, no formula injection. A short notice says whether it was saved.
4
+ import { can } from "@g9tm/aiassistant";
5
+ import { saveCsv } from "@g9tm/aiassistant/pages";
6
+ import { useUi } from "../context.js";
7
+ import { pageTexts } from "../pages/texts.js";
8
+
9
+ let { title = undefined, labels, rows }: { title?: string | undefined; labels: readonly string[]; rows: readonly (readonly string[])[] } = $props();
10
+ const ui = useUi();
11
+ const t = pageTexts(ui.texts).t;
12
+ let saved = $state<boolean | null>(null);
13
+ let busy = $state(false);
14
+
15
+ async function save(): Promise<void> {
16
+ busy = true;
17
+ saved = await saveCsv(ui.assistant.host, title, labels, rows);
18
+ busy = false;
19
+ }
20
+ </script>
21
+
22
+ {#if can(ui.assistant.host, "save")}
23
+ <div class="g9a-csv">
24
+ <button type="button" class="g9a-csv__save" disabled={busy} onclick={save}>{t("csv.save")}</button>
25
+ {#if saved !== null}<span class="g9a-note" role="status" data-saved={saved}>{t(saved ? "csv.saved" : "csv.not_saved")}</span>{/if}
26
+ </div>
27
+ {/if}
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ title?: string | undefined;
3
+ labels: readonly string[];
4
+ rows: readonly (readonly string[])[];
5
+ };
6
+ declare const CsvButton: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type CsvButton = ReturnType<typeof CsvButton>;
8
+ export default CsvButton;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A ready host for a plain browser app (a product without a bridge talks HTTP itself). Nothing here runs during
3
+ * server rendering; call it in the browser.
4
+ */
5
+ import type { AssistantHost, AssistantTheme } from "@g9tm/aiassistant";
6
+ export interface BrowserHostOptions {
7
+ locale?: "fa" | "en";
8
+ theme?: AssistantTheme;
9
+ /** Extra headers on every request (the host's own sign-in). */
10
+ headers?: () => Record<string, string>;
11
+ linkHosts?: readonly string[];
12
+ navigate?: AssistantHost["navigate"];
13
+ pageName?: AssistantHost["pageName"];
14
+ sheetLayer?: number;
15
+ }
16
+ export declare function browserHost(o?: BrowserHostOptions): AssistantHost;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * A ready host for a plain browser app (a product without a bridge talks HTTP itself). Nothing here runs during
3
+ * server rendering; call it in the browser.
4
+ */
5
+ export function browserHost(o = {}) {
6
+ const locale = o.locale ?? (document.documentElement.lang === "en" ? "en" : "fa");
7
+ const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
8
+ const dark = typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: dark)").matches;
9
+ const host = {
10
+ locale,
11
+ dir: locale === "fa" ? "rtl" : "ltr",
12
+ theme: o.theme ?? { mode: dark ? "dark" : "light", tokens: {} },
13
+ reducedMotion: reduced,
14
+ sheetLayer: o.sheetLayer ?? 1000,
15
+ linkHosts: o.linkHosts ?? [location.hostname],
16
+ async fetch(input, init) {
17
+ const headers = { ...(o.headers?.() ?? {}), ...(init?.headers ?? {}) };
18
+ const body = init?.body;
19
+ const r = await fetch(input, { method: init?.method ?? "GET", headers, credentials: "same-origin", ...(body === undefined ? {} : { body: body }) });
20
+ return { status: r.status, ok: r.ok, text: () => r.text(), headers: r.headers };
21
+ },
22
+ eventSource: (url) => new EventSource(url),
23
+ copy: (text) => navigator.clipboard.writeText(text),
24
+ // Recording for dictation: one channel of WebM/Opus (MediaRecorder's default), at most two minutes.
25
+ ...(typeof MediaRecorder === "function" && navigator.mediaDevices ? {
26
+ async record() {
27
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1 } });
28
+ const rec = new MediaRecorder(stream);
29
+ const parts = [];
30
+ rec.ondataavailable = (e) => { if (e.data.size > 0)
31
+ parts.push(e.data); };
32
+ const ended = new Promise((resolve) => { rec.onstop = () => resolve(); });
33
+ rec.start();
34
+ const cap = setTimeout(() => { if (rec.state === "recording")
35
+ rec.stop(); }, 120000);
36
+ return async () => {
37
+ clearTimeout(cap);
38
+ if (rec.state === "recording")
39
+ rec.stop();
40
+ await ended;
41
+ stream.getTracks().forEach((t) => t.stop());
42
+ const blob = new Blob(parts, { type: rec.mimeType || "audio/webm" });
43
+ return blob.size > 0 ? { bytes: new Uint8Array(await blob.arrayBuffer()), mime: blob.type.split(";")[0] ?? "audio/webm" } : null;
44
+ };
45
+ },
46
+ } : {}),
47
+ openUrl: (url) => { window.open(url, "_blank", "noopener"); },
48
+ async pickFile(accept) {
49
+ return new Promise((resolve) => {
50
+ const input = document.createElement("input");
51
+ input.type = "file";
52
+ input.accept = accept.join(",");
53
+ input.onchange = async () => {
54
+ const f = input.files?.[0];
55
+ resolve(f ? { name: f.name, type: f.type, bytes: new Uint8Array(await f.arrayBuffer()) } : null);
56
+ };
57
+ input.click();
58
+ });
59
+ },
60
+ async saveFile(name, data) {
61
+ const blob = new Blob([typeof data === "string" ? data : data]);
62
+ const a = document.createElement("a");
63
+ a.href = URL.createObjectURL(blob);
64
+ a.download = name;
65
+ a.click();
66
+ setTimeout(() => URL.revokeObjectURL(a.href), 1000);
67
+ return true;
68
+ },
69
+ onBackground(listener) {
70
+ const on = () => listener(document.visibilityState === "hidden");
71
+ document.addEventListener("visibilitychange", on);
72
+ return () => document.removeEventListener("visibilitychange", on);
73
+ },
74
+ };
75
+ if (o.navigate)
76
+ host.navigate = o.navigate;
77
+ if (o.pageName)
78
+ host.pageName = o.pageName;
79
+ return host;
80
+ }
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ // The approval card: facts, before and after, step ticks (unticking a step unticks what depends on it), the method
3
+ // (click, PinPad, code), the taint warning. Confirming always goes through the core, bound to the plan's hash.
4
+ import type { PlanView } from "@g9tm/aiassistant";
5
+ import { needsCode } from "@g9tm/aiassistant";
6
+ import { cls, override, useUi } from "../context.js";
7
+ import PinPad from "../slots/PinPad.svelte";
8
+
9
+ let { plan }: { plan: PlanView } = $props();
10
+ const ui = useUi();
11
+ const t = ui.texts.t;
12
+ const Pin = $derived(override(ui, "PinPad") ?? PinPad);
13
+ let code = $state("");
14
+ let busy = $state(false);
15
+ const say = (key: string, args: Record<string, string>): string => {
16
+ const text = t(key, args);
17
+ return text === key ? Object.values(args).join(" ") || key : text;
18
+ };
19
+
20
+ async function confirm(): Promise<void> {
21
+ busy = true;
22
+ await ui.assistant.confirm(plan.id, needsCode(plan.method) ? code : null);
23
+ code = "";
24
+ busy = false;
25
+ }
26
+ </script>
27
+
28
+ <div class={cls(ui, "card.approval")} data-answered={plan.answered ?? ""}>
29
+ <h3>{plan.readOnly ? t("card.readonly_routine") : plan.title}</h3>
30
+ {#if plan.warning}<p class="g9a-warning" role="note">{t("card.tainted")}</p>{/if}
31
+ <ol>
32
+ {#each plan.steps as s (s.index)}
33
+ <li class={cls(ui, "card.approval.step")}>
34
+ <label>
35
+ <input type="checkbox" checked={s.ticked} disabled={plan.answered !== null || plan.steps.length === 1} onchange={() => ui.assistant.tick(plan.id, s.index)} />
36
+ {s.text}{#if !s.undoable}<span class="g9a-irreversible"> ({t("card.irreversible")})</span>{/if}
37
+ </label>
38
+ {#each s.facts as f, i (i)}<div class={cls(ui, "card.approval.fact")}>{say(f.key, f.args)}</div>{/each}
39
+ {#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}
40
+ </li>
41
+ {/each}
42
+ </ol>
43
+ {#if plan.answered === null}
44
+ {#if needsCode(plan.method)}
45
+ <Pin bind:value={code} label={t("card.code")} />
46
+ {/if}
47
+ {#if plan.failCode === "confirm.waiting"}<p class="g9a-note" role="status">{t("card.waiting")}</p>
48
+ {:else if plan.failCode}<p class="g9a-note" role="alert">{plan.failCode}{#if plan.triesLeft !== null} · {t("card.tries_left", { count: plan.triesLeft })}{/if}</p>{/if}
49
+ <div class="g9a-row">
50
+ <button type="button" class={cls(ui, "card.approval.confirm")} disabled={busy || (needsCode(plan.method) && code.length === 0)} onclick={confirm}>{t("card.confirm")}</button>
51
+ <button type="button" class={cls(ui, "card.approval.refuse")} disabled={busy} onclick={() => ui.assistant.refuse(plan.id)}>{t("card.refuse")}</button>
52
+ </div>
53
+ {:else}
54
+ <p class="g9a-note">{t("card.answered")}</p>
55
+ {/if}
56
+ </div>
@@ -0,0 +1,7 @@
1
+ import type { PlanView } from "@g9tm/aiassistant";
2
+ type $$ComponentProps = {
3
+ plan: PlanView;
4
+ };
5
+ declare const ApprovalCard: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ApprovalCard = ReturnType<typeof ApprovalCard>;
7
+ export default ApprovalCard;
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ // The other cards: undo all, a question with options (and a secret field), "learn this?" and "make it a routine?".
3
+ // Each answers once; the answer stays in the core.
4
+ import { arr, obj, str } from "@g9tm/aiassistant";
5
+ import { cls, useUi } from "../context.js";
6
+ import SecretField from "./SecretField.svelte";
7
+
8
+ let { card, runId }: { card: { kind: string; id: string; body: Record<string, unknown> }; runId: string | null } = $props();
9
+ const ui = useUi();
10
+ const t = ui.texts.t;
11
+ const answered = $derived(ui.assistant.answered.of(card.id));
12
+ let text = $state("");
13
+ let undone = $state<string | null>(null);
14
+ const b = $derived(card.body);
15
+ const options = $derived(arr(b["options"]).map(obj).map((o) => str(o["label"])).filter((l) => l.length > 0));
16
+ const steps = $derived(arr(b["steps"]).map(obj).map((s) => str(s["text"], str(s["title"], str(s["tool"])))));
17
+ </script>
18
+
19
+ {#if card.kind === "undo"}
20
+ <div class={cls(ui, "card.undo")}>
21
+ <ul>{#each steps as s, i (i)}<li>{s}</li>{/each}</ul>
22
+ {#if undone === null}
23
+ <button type="button" onclick={async () => { const r = obj(await ui.assistant.undo(card.id).catch(() => null)); undone = str(r["text"], t("card.answered")); }}>{t("undo.all")}</button>
24
+ {:else}<p class="g9a-note">{undone}</p>{/if}
25
+ </div>
26
+ {:else if card.kind === "ask" && runId}
27
+ <div class={cls(ui, "card.ask")}>
28
+ <p>{str(b["question"])}</p>
29
+ {#if answered}<p class="g9a-note">{t("card.answered")}</p>
30
+ {:else}
31
+ {#each options as o (o)}<button type="button" class="g9a-option" onclick={() => ui.assistant.answer(runId, card.id, [o], null)}>{o}</button>{/each}
32
+ {#if b["secret"] === true}
33
+ <SecretField onsubmit={(v) => ui.assistant.answer(runId, card.id, [], v)} />
34
+ {:else}
35
+ <form onsubmit={(e) => { e.preventDefault(); void ui.assistant.answer(runId, card.id, [], text); }}>
36
+ <input bind:value={text} aria-label={t("ask.send")} />
37
+ <button type="submit">{t("ask.send")}</button>
38
+ </form>
39
+ {/if}
40
+ {/if}
41
+ </div>
42
+ {:else if card.kind === "learn" || card.kind === "routine" || card.kind === "routine_revision"}
43
+ <div class={cls(ui, "card.choice")}>
44
+ <p>{card.kind === "learn" ? t("learn.ask") : t("routine.ask")}</p>
45
+ {#if card.kind === "learn"}<blockquote>{str(b["text"])}</blockquote>{:else}<ol>{#each steps as s, i (i)}<li>{s}</li>{/each}</ol>{/if}
46
+ {#if answered}<p class="g9a-note">{t("card.answered")}</p>
47
+ {:else}
48
+ {@const kind = card.kind === "learn" ? "knowledge" : "learned"}
49
+ {@const yes = card.kind === "learn" ? "accept" : "approve"}
50
+ {@const no = card.kind === "learn" ? "refuse" : "retire"}
51
+ <button type="button" onclick={() => ui.assistant.choose(kind, str(b["id"], card.id), yes)}>{t("yes")}</button>
52
+ <button type="button" onclick={() => ui.assistant.choose(kind, str(b["id"], card.id), no)}>{t("no")}</button>
53
+ {/if}
54
+ </div>
55
+ {/if}
@@ -0,0 +1,11 @@
1
+ type $$ComponentProps = {
2
+ card: {
3
+ kind: string;
4
+ id: string;
5
+ body: Record<string, unknown>;
6
+ };
7
+ runId: string | null;
8
+ };
9
+ declare const Cards: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type Cards = ReturnType<typeof Cards>;
11
+ export default Cards;