@cef-ai/widget-runtime 0.2.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 (56) hide show
  1. package/README.md +34 -0
  2. package/dist/audio/capture-upload.d.ts +116 -0
  3. package/dist/audio/capture-upload.js +347 -0
  4. package/dist/audio/clip-registry.d.ts +20 -0
  5. package/dist/audio/clip-registry.js +47 -0
  6. package/dist/audio/ddc-delegation.d.ts +18 -0
  7. package/dist/audio/ddc-delegation.js +64 -0
  8. package/dist/audio/mic.d.ts +1 -0
  9. package/dist/audio/mic.js +23 -0
  10. package/dist/audio/segment.d.ts +3 -0
  11. package/dist/audio/segment.js +24 -0
  12. package/dist/audio/wav.d.ts +2 -0
  13. package/dist/audio/wav.js +27 -0
  14. package/dist/bridge-wallet.d.ts +46 -0
  15. package/dist/bridge-wallet.js +80 -0
  16. package/dist/bridge.d.ts +6 -0
  17. package/dist/bridge.js +15 -0
  18. package/dist/build-tools.d.ts +4 -0
  19. package/dist/build-tools.js +12 -0
  20. package/dist/entry.d.ts +11 -0
  21. package/dist/entry.js +159 -0
  22. package/dist/host-bridge.d.ts +19 -0
  23. package/dist/host-bridge.js +214 -0
  24. package/dist/index.d.ts +25 -0
  25. package/dist/index.js +21 -0
  26. package/dist/kinds/composite.d.ts +7 -0
  27. package/dist/kinds/composite.js +206 -0
  28. package/dist/kinds/conversation.d.ts +7 -0
  29. package/dist/kinds/conversation.js +667 -0
  30. package/dist/kinds/dashboard.d.ts +2 -0
  31. package/dist/kinds/dashboard.js +53 -0
  32. package/dist/kinds/list.d.ts +2 -0
  33. package/dist/kinds/list.js +22 -0
  34. package/dist/kinds/record.d.ts +2 -0
  35. package/dist/kinds/record.js +14 -0
  36. package/dist/kinds/submit.d.ts +6 -0
  37. package/dist/kinds/submit.js +481 -0
  38. package/dist/manifest.d.ts +32 -0
  39. package/dist/manifest.js +47 -0
  40. package/dist/mount.d.ts +19 -0
  41. package/dist/mount.js +48 -0
  42. package/dist/render-helpers.d.ts +9 -0
  43. package/dist/render-helpers.js +42 -0
  44. package/dist/runtime.d.ts +12 -0
  45. package/dist/runtime.js +36 -0
  46. package/dist/standalone-wallet.d.ts +21 -0
  47. package/dist/standalone-wallet.js +102 -0
  48. package/dist/types.d.ts +245 -0
  49. package/dist/types.js +7 -0
  50. package/dist/validate-config.d.ts +2 -0
  51. package/dist/validate-config.js +74 -0
  52. package/dist/vault-client.d.ts +45 -0
  53. package/dist/vault-client.js +122 -0
  54. package/dist/widget-runtime.js +348 -0
  55. package/dist/widget.html +33 -0
  56. package/package.json +43 -0
@@ -0,0 +1,25 @@
1
+ export { buildWidgetManifest, type BuildManifestInput } from './manifest.js';
2
+ export { injectWidgetBridge, type BridgeInput } from './bridge.js';
3
+ export type { WidgetManifest, WidgetRuntimeApi, Identity, QueryResult } from './types.js';
4
+ export { AgentNotConnectedError } from './types.js';
5
+ export { validateConfig } from './validate-config.js';
6
+ export { mountKind } from './mount.js';
7
+ export type { UploadAudioFn, MountDeps } from './mount.js';
8
+ export { renderRecord } from './kinds/record.js';
9
+ export { renderList } from './kinds/list.js';
10
+ export { renderDashboard } from './kinds/dashboard.js';
11
+ export { renderSubmit } from './kinds/submit.js';
12
+ export { renderConversation } from './kinds/conversation.js';
13
+ export { renderComposite, registerCompositePanel, getCompositePanel, type CompositePanelRenderer } from './kinds/composite.js';
14
+ export * from './render-helpers.js';
15
+ export type { WidgetKind, WidgetConfig, RecordConfig, ListConfig, DashboardConfig, SubmitConfig, ConversationConfig, CustomPanelConfig, CompositePanel, CompositeConfig } from './types.js';
16
+ export { captureAndUploadAudio, createVaultUploader, createVaultObjectStore } from './audio/capture-upload.js';
17
+ export type { CaptureUploadInput, CaptureUploadDeps, VaultUploaderConfig, VaultStoreConfig, VaultObjectStore } from './audio/capture-upload.js';
18
+ export { mintDelegationToken, Operation } from './audio/ddc-delegation.js';
19
+ export type { AsyncSignerPair, MintOpts } from './audio/ddc-delegation.js';
20
+ export { encodeWav } from './audio/wav.js';
21
+ export { segmentPcm, decodeToMono16k } from './audio/segment.js';
22
+ export { requestMicStream } from './audio/mic.js';
23
+ export { createHostBridge, type HostBridge } from './host-bridge.js';
24
+ export { toVaultSdkSigner, toClientSdkWallet, toUploaderSignerPair, type VaultSignedWallet } from './bridge-wallet.js';
25
+ export { connectStandaloneWallet, type StandaloneWallet } from './standalone-wallet.js';
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ // packages/widget-runtime/src/index.ts
2
+ export { buildWidgetManifest } from './manifest.js';
3
+ export { injectWidgetBridge } from './bridge.js';
4
+ export { AgentNotConnectedError } from './types.js';
5
+ export { validateConfig } from './validate-config.js';
6
+ export { mountKind } from './mount.js';
7
+ export { renderRecord } from './kinds/record.js';
8
+ export { renderList } from './kinds/list.js';
9
+ export { renderDashboard } from './kinds/dashboard.js';
10
+ export { renderSubmit } from './kinds/submit.js';
11
+ export { renderConversation } from './kinds/conversation.js';
12
+ export { renderComposite, registerCompositePanel, getCompositePanel } from './kinds/composite.js';
13
+ export * from './render-helpers.js';
14
+ export { captureAndUploadAudio, createVaultUploader, createVaultObjectStore } from './audio/capture-upload.js';
15
+ export { mintDelegationToken, Operation } from './audio/ddc-delegation.js';
16
+ export { encodeWav } from './audio/wav.js';
17
+ export { segmentPcm, decodeToMono16k } from './audio/segment.js';
18
+ export { requestMicStream } from './audio/mic.js';
19
+ export { createHostBridge } from './host-bridge.js';
20
+ export { toVaultSdkSigner, toClientSdkWallet, toUploaderSignerPair } from './bridge-wallet.js';
21
+ export { connectStandaloneWallet } from './standalone-wallet.js';
@@ -0,0 +1,7 @@
1
+ import type { CompositeConfig, WidgetRuntimeApi } from '../types.js';
2
+ import { type MountDeps } from '../mount.js';
3
+ /** A custom panel renderer registered by the host app bundle before boot. */
4
+ export type CompositePanelRenderer = (root: HTMLElement, runtime: WidgetRuntimeApi, deps?: MountDeps) => void | Promise<void>;
5
+ export declare function registerCompositePanel(name: string, fn: CompositePanelRenderer): void;
6
+ export declare function getCompositePanel(name: string): CompositePanelRenderer | undefined;
7
+ export declare function renderComposite(root: HTMLElement, config: CompositeConfig, runtime: WidgetRuntimeApi, deps?: MountDeps): Promise<void>;
@@ -0,0 +1,206 @@
1
+ import { esc } from '../render-helpers.js';
2
+ import { mountKind } from '../mount.js';
3
+ const REGISTRY = new Map();
4
+ export function registerCompositePanel(name, fn) {
5
+ REGISTRY.set(name, fn);
6
+ }
7
+ export function getCompositePanel(name) {
8
+ return REGISTRY.get(name);
9
+ }
10
+ async function isUnlocked(config, runtime) {
11
+ const { statusQuery, unlockWhen } = config.gating;
12
+ try {
13
+ const res = await runtime.query(statusQuery);
14
+ const idx = res.columns.findIndex((c) => c.toLowerCase() === unlockWhen.column.toLowerCase());
15
+ if (idx === -1 || res.rows.length === 0)
16
+ return false;
17
+ return String(res.rows[0][idx] ?? '') === unlockWhen.equals;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ async function hasBadge(panel, runtime) {
24
+ if (!panel.badgeWhen)
25
+ return false;
26
+ try {
27
+ const res = await runtime.query(panel.badgeWhen.query);
28
+ const idx = res.columns.findIndex((c) => c.toLowerCase() === panel.badgeWhen.column.toLowerCase());
29
+ if (idx === -1 || res.rows.length === 0)
30
+ return false;
31
+ return String(res.rows[0][idx] ?? '') === panel.badgeWhen.equals;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ export async function renderComposite(root, config, runtime, deps) {
38
+ // Resolve gating + badges concurrently so the first paint waits on one
39
+ // round-trip phase, not two serial ones.
40
+ const badges = new Map();
41
+ // unlocked is mutable: a panel (e.g. the intake conversation) can flip the
42
+ // gate to confirmed mid-session via deps.onDone → recheckGate, unlocking the
43
+ // rail without a page reload.
44
+ let [unlocked] = await Promise.all([
45
+ isUnlocked(config, runtime),
46
+ Promise.all(config.panels.map(async (p) => badges.set(p.id, await hasBadge(p, runtime)))),
47
+ ]);
48
+ let activeId = unlocked ? config.defaultPanel : config.gating.firstRunPanel;
49
+ let panelController = null;
50
+ const panelById = (id) => config.panels.find((p) => p.id === id);
51
+ const isPanelLocked = (p) => !!p.gated && !unlocked;
52
+ function railHtml() {
53
+ const groups = config.groups.map((g) => {
54
+ const items = config.panels.filter((p) => p.group === g.id).map((p) => {
55
+ const locked = isPanelLocked(p);
56
+ const badge = badges.get(p.id) ? ' <span class="dc-badge">new</span>' : '';
57
+ const check = !locked && p.gated ? ' <span class="dc-check">✓</span>' : '';
58
+ const lock = locked ? ' 🔒' : '';
59
+ return `<div data-rail-item data-panel="${esc(p.id)}" data-locked="${locked}"
60
+ class="dc-rail-item${p.id === activeId ? ' active' : ''}${locked ? ' locked' : ''}"
61
+ role="button" tabindex="0">${p.icon ? esc(p.icon) + ' ' : ''}${esc(p.label)}${check}${badge}${lock}</div>`;
62
+ }).join('');
63
+ return `<div class="dc-rail-group" data-rail-group>${esc(g.label)}</div>${items}`;
64
+ }).join('');
65
+ const menuBtn = config.menu?.length ? `<button data-menu-toggle class="dc-gear" aria-label="Menu">⚙</button>` : '';
66
+ return `<div class="dc-rail">${groups}</div>${menuBtn}`;
67
+ }
68
+ function menuHtml() {
69
+ if (!config.menu?.length)
70
+ return '';
71
+ const items = config.menu.map((m, i) => `<div data-menu-action="${i}" class="dc-menu-item" role="button" tabindex="0">${esc(m.label)}</div>`).join('');
72
+ return `<div data-menu class="dc-menu" hidden>${items}</div>`;
73
+ }
74
+ function shellHtml() {
75
+ return `
76
+ <div class="dc-shell">
77
+ <aside class="dc-aside">${railHtml()}${menuHtml()}</aside>
78
+ <main class="dc-content" data-content></main>
79
+ </div>`;
80
+ }
81
+ async function mountPanel(id) {
82
+ activeId = id;
83
+ panelController?.abort();
84
+ panelController = new AbortController();
85
+ const panel = panelById(id);
86
+ const content = root.querySelector('[data-content]');
87
+ const host = document.createElement('div');
88
+ host.setAttribute('data-active-panel', id);
89
+ content.replaceChildren(host);
90
+ const panelDeps = { ...deps, signal: panelController.signal, onDone: () => void recheckGate() };
91
+ if (isPanelLocked(panel)) {
92
+ host.innerHTML = `<p class="dc-muted">Finish intake first to unlock this.</p>`;
93
+ return;
94
+ }
95
+ if (panel.config.kind === 'custom') {
96
+ const fn = getCompositePanel(panel.config.renderer);
97
+ if (!fn) {
98
+ host.innerHTML = `<p class="error">Unknown panel renderer.</p>`;
99
+ return;
100
+ }
101
+ await fn(host, runtime, panelDeps);
102
+ return;
103
+ }
104
+ await mountKind(host, panel.config, runtime, panelDeps);
105
+ }
106
+ function heroHtml() {
107
+ const h = config.gating.hero;
108
+ if (!h)
109
+ return '';
110
+ return `<div class="dc-hero" data-hero>
111
+ <h2>${esc(h.title)}</h2><p>${esc(h.body)}</p>
112
+ <button data-hero-cta class="dc-cta">${esc(h.cta)}</button>
113
+ </div>`;
114
+ }
115
+ // In-DOM confirmation. The widget runs in a sandboxed iframe without
116
+ // `allow-modals`, so native confirm()/alert()/prompt() are ignored ("Ignored
117
+ // call to 'confirm()'. The document is sandboxed…") and would silently cancel
118
+ // the action. Inline-styled so it renders without depending on host CSS.
119
+ function confirmDialog(message) {
120
+ return new Promise((resolve) => {
121
+ const overlay = document.createElement('div');
122
+ overlay.setAttribute('data-confirm', '');
123
+ overlay.style.cssText =
124
+ 'position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.45);z-index:50;padding:16px;';
125
+ const box = document.createElement('div');
126
+ box.setAttribute('role', 'dialog');
127
+ box.setAttribute('aria-modal', 'true');
128
+ box.style.cssText =
129
+ 'background:Canvas;color:CanvasText;border:1px solid color-mix(in srgb, currentColor 18%, transparent);border-radius:12px;padding:18px;max-width:340px;box-shadow:0 8px 28px rgba(0,0,0,0.25);';
130
+ box.innerHTML =
131
+ `<p style="margin:0 0 14px">${esc(message)}</p>` +
132
+ `<div style="display:flex;gap:8px;justify-content:flex-end">` +
133
+ `<button data-cancel style="padding:7px 14px;border-radius:8px;border:1px solid currentColor;background:transparent;cursor:pointer">Cancel</button>` +
134
+ `<button data-ok style="padding:7px 14px;border-radius:8px;border:none;background:#ff5e8a;color:#fff;font-weight:600;cursor:pointer">Confirm</button>` +
135
+ `</div>`;
136
+ overlay.appendChild(box);
137
+ const done = (v) => { overlay.remove(); resolve(v); };
138
+ box.querySelector('[data-cancel]').addEventListener('click', () => done(false));
139
+ box.querySelector('[data-ok]').addEventListener('click', () => done(true));
140
+ overlay.addEventListener('click', (e) => { if (e.target === overlay)
141
+ done(false); });
142
+ root.appendChild(overlay);
143
+ });
144
+ }
145
+ function wireRail() {
146
+ root.querySelectorAll('[data-rail-item]').forEach((el) => {
147
+ el.addEventListener('click', () => {
148
+ const id = el.getAttribute('data-panel');
149
+ if (isPanelLocked(panelById(id)))
150
+ return;
151
+ renderActive(id);
152
+ });
153
+ });
154
+ root.querySelector('[data-menu-toggle]')?.addEventListener('click', () => {
155
+ const menu = root.querySelector('[data-menu]');
156
+ if (menu)
157
+ menu.toggleAttribute('hidden');
158
+ });
159
+ config.menu?.forEach((m, i) => {
160
+ root.querySelector(`[data-menu-action="${i}"]`)?.addEventListener('click', async () => {
161
+ root.querySelector('[data-menu]')?.setAttribute('hidden', '');
162
+ if (m.kind === 'goto') {
163
+ renderActive(m.panel);
164
+ return;
165
+ }
166
+ if (m.confirm && !(await confirmDialog(m.confirm)))
167
+ return;
168
+ await runtime.publish(m.event.type, { schema_version: 1, ...(m.event.payload ?? {}) }, undefined);
169
+ });
170
+ });
171
+ root.querySelector('[data-hero-cta]')?.addEventListener('click', () => renderActive(config.gating.firstRunPanel));
172
+ }
173
+ // showHero is the landing state shown ONCE while locked. Navigations (hero
174
+ // CTA, rail, menu-goto) pass showHero=false so they mount the requested
175
+ // panel — the firstRunPanel (intake) is un-gated and reachable while locked,
176
+ // which is how the user unlocks the rest. Without this, renderActive re-drew
177
+ // the hero on every call whenever !unlocked, so the CTA/rail were no-ops.
178
+ function renderActive(id, showHero = false) {
179
+ root.innerHTML = shellHtml();
180
+ const content = root.querySelector('[data-content]');
181
+ if (showHero && !unlocked && config.gating.hero) {
182
+ content.innerHTML = heroHtml();
183
+ activeId = config.gating.firstRunPanel;
184
+ }
185
+ else {
186
+ void mountPanel(id);
187
+ }
188
+ wireRail();
189
+ }
190
+ // After a panel signals done (e.g. the intake conversation completes),
191
+ // re-check the gate and unlock the rail without a page reload. The gate
192
+ // (statusQuery) can lag the panel's own done signal by a beat, so retry a
193
+ // few times before giving up.
194
+ async function recheckGate(attempt = 0) {
195
+ if (unlocked)
196
+ return;
197
+ if (await isUnlocked(config, runtime)) {
198
+ unlocked = true;
199
+ renderActive(config.defaultPanel);
200
+ return;
201
+ }
202
+ if (attempt < 5)
203
+ setTimeout(() => void recheckGate(attempt + 1), 1500);
204
+ }
205
+ renderActive(activeId, !unlocked);
206
+ }
@@ -0,0 +1,7 @@
1
+ import type { ConversationConfig, WidgetRuntimeApi } from '../types.js';
2
+ import type { UploadAudioFn } from '../mount.js';
3
+ export declare function renderConversation(root: HTMLElement, config: ConversationConfig, runtime: WidgetRuntimeApi, deps?: {
4
+ uploadAudio?: UploadAudioFn;
5
+ signal?: AbortSignal;
6
+ onDone?: () => void;
7
+ }): void;