@happyvertical/smrt-chat 0.55.9 → 0.55.11

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/AGENTS.md +25 -14
  2. package/README.md +12 -0
  3. package/agents/dev-workbench.md +69 -0
  4. package/agents/helper-preferences.md +65 -0
  5. package/agents/overview-tools.md +172 -0
  6. package/agents/token-streaming.md +3 -2
  7. package/dist/chunks/{ChatService-C9UxuJaw.js → ChatService-CFaaDHyU.js} +2 -2
  8. package/dist/chunks/{ChatService-C9UxuJaw.js.map → ChatService-CFaaDHyU.js.map} +1 -1
  9. package/dist/chunks/chat-stream-n-7AqsgF.js +2027 -0
  10. package/dist/chunks/chat-stream-n-7AqsgF.js.map +1 -0
  11. package/dist/dev-assistant-transport.d.ts +5 -0
  12. package/dist/dev-assistant-transport.d.ts.map +1 -0
  13. package/dist/dev-character-persistence-client.d.ts +24 -0
  14. package/dist/dev-character-persistence-client.d.ts.map +1 -0
  15. package/dist/dev-helper-client.d.ts +4 -0
  16. package/dist/dev-helper-client.d.ts.map +1 -0
  17. package/dist/dev-helper-server.d.ts +18 -0
  18. package/dist/dev-helper-server.d.ts.map +1 -0
  19. package/dist/helper-preferences-service.d.ts +22 -0
  20. package/dist/helper-preferences-service.d.ts.map +1 -0
  21. package/dist/helper-preferences.d.ts +84 -0
  22. package/dist/helper-preferences.d.ts.map +1 -0
  23. package/dist/helper-preferences.js +32 -0
  24. package/dist/helper-preferences.js.map +1 -0
  25. package/dist/helper-profile-store.d.ts +27 -0
  26. package/dist/helper-profile-store.d.ts.map +1 -0
  27. package/dist/index.d.ts +5 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +683 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/internal/agent-runtime.js +1 -1
  32. package/dist/manifest.json +122 -3
  33. package/dist/overview-tools.d.ts +221 -0
  34. package/dist/overview-tools.d.ts.map +1 -0
  35. package/dist/playground.d.ts +1 -1
  36. package/dist/playground.d.ts.map +1 -1
  37. package/dist/playground.js +99 -88
  38. package/dist/playground.js.map +1 -1
  39. package/dist/recipes.d.ts +81 -0
  40. package/dist/recipes.d.ts.map +1 -1
  41. package/dist/routes/api/dev-character-conversation/protocol.d.ts +33 -0
  42. package/dist/routes/api/dev-character-conversation/protocol.d.ts.map +1 -0
  43. package/dist/routes/api/dev-character-persistence/config.d.ts +24 -0
  44. package/dist/routes/api/dev-character-persistence/config.d.ts.map +1 -0
  45. package/dist/scripts/provision-dev-character-persistence.d.ts +3 -0
  46. package/dist/scripts/provision-dev-character-persistence.d.ts.map +1 -0
  47. package/dist/smrt-knowledge.json +144 -11
  48. package/dist/svelte/components/assistant/AssistantDock.svelte +58 -2
  49. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts +9 -0
  50. package/dist/svelte/components/assistant/AssistantDock.svelte.d.ts.map +1 -1
  51. package/dist/svelte/components/assistant/FloatingAssistant.svelte +309 -0
  52. package/dist/svelte/components/assistant/FloatingAssistant.svelte.d.ts +27 -0
  53. package/dist/svelte/components/assistant/FloatingAssistant.svelte.d.ts.map +1 -0
  54. package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte +105 -0
  55. package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte.d.ts +11 -0
  56. package/dist/svelte/components/assistant/captions/CaptionOverlay.svelte.d.ts.map +1 -0
  57. package/dist/svelte/components/assistant/captions/HeardCaptions.svelte +65 -0
  58. package/dist/svelte/components/assistant/captions/HeardCaptions.svelte.d.ts +19 -0
  59. package/dist/svelte/components/assistant/captions/HeardCaptions.svelte.d.ts.map +1 -0
  60. package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte +65 -0
  61. package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte.d.ts +21 -0
  62. package/dist/svelte/components/assistant/captions/SpokenCaptions.svelte.d.ts.map +1 -0
  63. package/dist/svelte/components/assistant/captions/caption-overlay-context.d.ts +3 -0
  64. package/dist/svelte/components/assistant/captions/caption-overlay-context.d.ts.map +1 -0
  65. package/dist/svelte/components/assistant/captions/caption-overlay-context.js +2 -0
  66. package/dist/svelte/components/assistant/captions/caption-state.svelte.d.ts +71 -0
  67. package/dist/svelte/components/assistant/captions/caption-state.svelte.d.ts.map +1 -0
  68. package/dist/svelte/components/assistant/captions/caption-state.svelte.js +225 -0
  69. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.d.ts.map +1 -1
  70. package/dist/svelte/components/assistant/create-assistant-dock-controller.svelte.js +3 -1
  71. package/dist/svelte/components/helper/HelperControlPanel.svelte +401 -0
  72. package/dist/svelte/components/helper/HelperControlPanel.svelte.d.ts +23 -0
  73. package/dist/svelte/components/helper/HelperControlPanel.svelte.d.ts.map +1 -0
  74. package/dist/svelte/components/helper/registry.d.ts +53 -0
  75. package/dist/svelte/components/helper/registry.d.ts.map +1 -0
  76. package/dist/svelte/components/helper/registry.js +100 -0
  77. package/dist/svelte/i18n.d.ts +14 -0
  78. package/dist/svelte/i18n.d.ts.map +1 -1
  79. package/dist/svelte/i18n.js +17 -0
  80. package/dist/svelte/i18n.messages.d.ts +32 -0
  81. package/dist/svelte/i18n.messages.d.ts.map +1 -1
  82. package/dist/svelte/i18n.messages.js +33 -0
  83. package/dist/svelte/index.d.ts +7 -0
  84. package/dist/svelte/index.d.ts.map +1 -1
  85. package/dist/svelte/index.js +15 -5
  86. package/dist/svelte/playground.d.ts +94 -0
  87. package/dist/svelte/playground.d.ts.map +1 -1
  88. package/dist/svelte/playground.js +16 -5
  89. package/dist/sveltekit.js +2 -2
  90. package/package.json +27 -17
  91. package/dist/chunks/chat-stream-BBk_5_Ph.js +0 -2027
  92. package/dist/chunks/chat-stream-BBk_5_Ph.js.map +0 -1
@@ -0,0 +1,401 @@
1
+ <script lang="ts">
2
+ import { Input, Select, Switch } from '@happyvertical/smrt-ui/forms';
3
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
4
+ import { Button } from '@happyvertical/smrt-ui/ui';
5
+ import { tick } from 'svelte';
6
+ import type {
7
+ HelperClient,
8
+ HelperOffering,
9
+ HelperPreferences,
10
+ HelperSnapshot,
11
+ } from '../../../helper-preferences.js';
12
+ import { M } from '../../i18n.messages.js';
13
+ import type { HelperRendererHandle, HelperStyleRegistry } from './registry.js';
14
+
15
+ export interface Props {
16
+ /** Authenticated browser transport; only server snapshots become applied state. */
17
+ client: HelperClient;
18
+ /** Trusted style implementations available to this host. */
19
+ registry: HelperStyleRegistry;
20
+ /** Optional host-owned current snapshot. Absent snapshots load on mount. */
21
+ snapshot?: HelperSnapshot;
22
+ /**
23
+ * Authenticated bridge for a selected offering's runtime payload. The
24
+ * payload is deliberately never persisted by this panel or preferences.
25
+ */
26
+ loadPayload?: (offering: HelperOffering) => Promise<unknown>;
27
+ /** Receives each successful server-returned snapshot. */
28
+ onchanged?: (snapshot: HelperSnapshot) => void;
29
+ /** Notifies the host after an authenticated custom setup creates an offering. */
30
+ oncustomsaved?: (offering: HelperOffering) => void;
31
+ }
32
+ const { t } = useI18n();
33
+ const panelInstanceId = $props.id();
34
+ let {
35
+ client,
36
+ registry,
37
+ snapshot = $bindable(),
38
+ loadPayload,
39
+ onchanged,
40
+ oncustomsaved,
41
+ }: Props = $props();
42
+ let loading = $state(!snapshot);
43
+ let saving = $state(false);
44
+ let message = $state<string | null>(null);
45
+ let messageIsError = $state(false);
46
+ let draft = $state<HelperPreferences | null>(
47
+ snapshot?.preferences
48
+ ? { ...snapshot.preferences }
49
+ : snapshot?.recoveryDraft
50
+ ? { ...snapshot.recoveryDraft }
51
+ : null,
52
+ );
53
+ // A server refresh after custom setup is reported through `onchanged`. Keep
54
+ // the intentionally staged local choice until a materially different server
55
+ // snapshot arrives, so a host echo cannot turn it into applied preferences.
56
+ let pendingDraft: HelperPreferences | null = null;
57
+ let pendingSnapshotFingerprint: string | null = null;
58
+ let setupStyleId = $state<string | null>(null);
59
+ let setupTarget = $state<HTMLElement>();
60
+ let previewTarget = $state<HTMLElement>();
61
+ let previewError = $state<string | null>(null);
62
+
63
+ $effect(() => {
64
+ if (!snapshot) return;
65
+ // `recoveryDraft` is a server-issued candidate only. Keeping it separate
66
+ // from `preferences` prevents an unavailable helper from being rendered as
67
+ // applied while still allowing permitted replacement fields to be saved.
68
+ const staged =
69
+ pendingSnapshotFingerprint === snapshotFingerprint(snapshot)
70
+ ? pendingDraft
71
+ : null;
72
+ if (!staged) {
73
+ pendingDraft = null;
74
+ pendingSnapshotFingerprint = null;
75
+ }
76
+ draft =
77
+ staged ??
78
+ (snapshot.preferences
79
+ ? { ...snapshot.preferences }
80
+ : snapshot.recoveryDraft
81
+ ? { ...snapshot.recoveryDraft }
82
+ : null);
83
+ });
84
+ $effect(() => {
85
+ if (snapshot) {
86
+ loading = false;
87
+ return;
88
+ }
89
+ let active = true;
90
+ void client
91
+ .load()
92
+ .then((next) => {
93
+ if (!active) return;
94
+ snapshot = next;
95
+ draft = next.preferences
96
+ ? { ...next.preferences }
97
+ : next.recoveryDraft
98
+ ? { ...next.recoveryDraft }
99
+ : null;
100
+ })
101
+ .catch((error: unknown) => {
102
+ if (active)
103
+ message =
104
+ error instanceof Error
105
+ ? error.message
106
+ : t(M['chat.helper.load_failed']);
107
+ })
108
+ .finally(() => {
109
+ if (active) loading = false;
110
+ });
111
+ return () => {
112
+ active = false;
113
+ };
114
+ });
115
+
116
+ const editable = (field: keyof Omit<HelperPreferences, 'version'>) =>
117
+ !!snapshot?.permissions.editableFields.includes(field);
118
+ const canSave = $derived(
119
+ !!draft && (snapshot?.permissions.editableFields.length ?? 0) > 0,
120
+ );
121
+ const savedPreferences = $derived(snapshot?.preferences ?? null);
122
+ const dirty = $derived(
123
+ !!draft &&
124
+ (!savedPreferences ||
125
+ draft.offeringId !== savedPreferences.offeringId ||
126
+ draft.name !== savedPreferences.name ||
127
+ draft.voiceId !== savedPreferences.voiceId ||
128
+ draft.placement !== savedPreferences.placement ||
129
+ draft.heardSubtitles !== savedPreferences.heardSubtitles ||
130
+ draft.spokenSubtitles !== savedPreferences.spokenSubtitles),
131
+ );
132
+ const availableOfferings = $derived(
133
+ (snapshot?.offerings ?? []).filter((offering) =>
134
+ registry.get(offering.styleId),
135
+ ),
136
+ );
137
+ // A recovery draft may name an offering that no longer appears in the
138
+ // gallery. In that case retain a single keyboard entry point on the first
139
+ // editable offering instead of leaving every radio outside the tab order.
140
+ const galleryTabStopIndex = $derived(
141
+ !editable('offeringId')
142
+ ? -1
143
+ : Math.max(
144
+ 0,
145
+ availableOfferings.findIndex(
146
+ (offering) => offering.id === draft?.offeringId,
147
+ ),
148
+ ),
149
+ );
150
+ const availableSetups = $derived(
151
+ registry
152
+ .list()
153
+ .filter(
154
+ (style) =>
155
+ style.Setup && snapshot?.permissions.customStyleIds.includes(style.id),
156
+ ),
157
+ );
158
+ const previewOffering = $derived(
159
+ draft
160
+ ? (availableOfferings.find(
161
+ (offering) => offering.id === draft?.offeringId,
162
+ ) ?? null)
163
+ : (snapshot?.offering ?? null),
164
+ );
165
+
166
+ $effect(() => {
167
+ const offering = previewOffering;
168
+ const style = offering ? registry.get(offering.styleId) : undefined;
169
+ const target = previewTarget;
170
+ let disposed = false;
171
+ let handle: HelperRendererHandle | null = null;
172
+ previewError = null;
173
+ if (!offering || !style || !target) return;
174
+ void (async () => {
175
+ try {
176
+ const payload = loadPayload ? await loadPayload(offering) : null;
177
+ const mounted = await style.mount({ target, offering, payload });
178
+ if (disposed) mounted.destroy();
179
+ else handle = mounted;
180
+ } catch (error) {
181
+ if (!disposed)
182
+ previewError =
183
+ error instanceof Error
184
+ ? error.message
185
+ : t(M['chat.helper.preview_failed']);
186
+ }
187
+ })();
188
+ return () => {
189
+ disposed = true;
190
+ handle?.destroy();
191
+ handle = null;
192
+ };
193
+ });
194
+
195
+ function update<K extends keyof HelperPreferences>(
196
+ key: K,
197
+ value: HelperPreferences[K],
198
+ ) {
199
+ if (!draft) return;
200
+ draft = { ...draft, [key]: value };
201
+ if (!messageIsError) message = null;
202
+ }
203
+ function snapshotFingerprint(next: HelperSnapshot) {
204
+ return JSON.stringify({
205
+ preferences: next.preferences,
206
+ offering: next.offering,
207
+ offerings: next.offerings,
208
+ voices: next.voices,
209
+ permissions: next.permissions,
210
+ selection: next.selection,
211
+ source: next.source,
212
+ recovery: next.recovery,
213
+ recoveryDraft: next.recoveryDraft,
214
+ });
215
+ }
216
+ function focusOffering(offeringId: string) {
217
+ const index = availableOfferings.findIndex(
218
+ (offering) => offering.id === offeringId,
219
+ );
220
+ void tick().then(() =>
221
+ document.getElementById(`${panelInstanceId}-offering-${index}`)?.focus(),
222
+ );
223
+ }
224
+ function closeSetup() {
225
+ const styleId = setupStyleId;
226
+ setupStyleId = null;
227
+ if (styleId)
228
+ void tick().then(() =>
229
+ document.getElementById(`${panelInstanceId}-setup-${styleId}`)?.focus(),
230
+ );
231
+ }
232
+ function openSetup(styleId: string) {
233
+ setupStyleId = styleId;
234
+ void tick().then(() =>
235
+ setupTarget?.querySelector<HTMLElement>('button')?.focus(),
236
+ );
237
+ }
238
+ function moveGallerySelection(event: KeyboardEvent) {
239
+ if (!draft || !editable('offeringId')) return;
240
+ const offerings = availableOfferings;
241
+ if (!offerings.length) return;
242
+ const current = Math.max(
243
+ 0,
244
+ offerings.findIndex((offering) => offering.id === draft?.offeringId),
245
+ );
246
+ let index: number | null = null;
247
+ if (event.key === 'ArrowRight' || event.key === 'ArrowDown')
248
+ index = (current + 1) % offerings.length;
249
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowUp')
250
+ index = (current - 1 + offerings.length) % offerings.length;
251
+ if (event.key === 'Home') index = 0;
252
+ if (event.key === 'End') index = offerings.length - 1;
253
+ if (index === null) return;
254
+ event.preventDefault();
255
+ update('offeringId', offerings[index].id);
256
+ focusOffering(offerings[index].id);
257
+ }
258
+ async function save() {
259
+ if (!draft || saving) return;
260
+ saving = true;
261
+ message = null;
262
+ messageIsError = false;
263
+ try {
264
+ apply(await client.save(draft));
265
+ message = t(M['chat.helper.saved']);
266
+ } catch (error) {
267
+ message =
268
+ error instanceof Error ? error.message : t(M['chat.helper.save_failed']);
269
+ messageIsError = true;
270
+ } finally {
271
+ saving = false;
272
+ }
273
+ }
274
+ async function reset() {
275
+ if (saving || !snapshot?.permissions.canReset) return;
276
+ saving = true;
277
+ message = null;
278
+ messageIsError = false;
279
+ try {
280
+ apply(await client.reset());
281
+ message = t(M['chat.helper.reset']);
282
+ } catch (error) {
283
+ message =
284
+ error instanceof Error ? error.message : t(M['chat.helper.reset_failed']);
285
+ messageIsError = true;
286
+ } finally {
287
+ saving = false;
288
+ }
289
+ }
290
+ function apply(next: HelperSnapshot) {
291
+ pendingDraft = null;
292
+ pendingSnapshotFingerprint = null;
293
+ snapshot = next;
294
+ draft = next.preferences
295
+ ? { ...next.preferences }
296
+ : next.recoveryDraft
297
+ ? { ...next.recoveryDraft }
298
+ : null;
299
+ onchanged?.(next);
300
+ }
301
+ async function customSaved(offering: HelperOffering) {
302
+ oncustomsaved?.(offering);
303
+ try {
304
+ const priorDraft = draft;
305
+ const next = await client.load();
306
+ snapshot = next;
307
+ const baseline = next.preferences ?? next.recoveryDraft ?? null;
308
+ const savedOffering = next.offerings.find(
309
+ (candidate) =>
310
+ candidate.id === offering.id && !!registry.get(candidate.styleId),
311
+ );
312
+ if (
313
+ baseline &&
314
+ savedOffering &&
315
+ next.permissions.editableFields.includes('offeringId')
316
+ ) {
317
+ const retained = { ...baseline };
318
+ for (const field of next.permissions.editableFields) {
319
+ if (priorDraft) retained[field] = priorDraft[field] as never;
320
+ }
321
+ pendingDraft = { ...retained, offeringId: savedOffering.id };
322
+ pendingSnapshotFingerprint = snapshotFingerprint(next);
323
+ } else {
324
+ pendingDraft = null;
325
+ pendingSnapshotFingerprint = null;
326
+ }
327
+ draft = pendingDraft ?? (baseline ? { ...baseline } : null);
328
+ onchanged?.(next);
329
+ setupStyleId = null;
330
+ message = t(M['chat.helper.photo_saved_apply']);
331
+ messageIsError = false;
332
+ if (savedOffering) focusOffering(savedOffering.id);
333
+ } catch (error) {
334
+ message =
335
+ error instanceof Error ? error.message : t(M['chat.helper.load_failed']);
336
+ messageIsError = true;
337
+ }
338
+ }
339
+ </script>
340
+
341
+ <section class="helper-panel" aria-busy={loading || saving} aria-labelledby="helper-panel-title">
342
+ <h2 id="helper-panel-title">{t(M['chat.helper.title'])}</h2>
343
+ {#if message}<p class="message" role={messageIsError ? 'alert' : 'status'} aria-live={messageIsError ? 'assertive' : 'polite'}>{message}</p>{/if}
344
+ {#if snapshot?.recovery}<p class="message" role="status">{snapshot.recovery.message}</p>{/if}
345
+ {#if loading}<p>{t(M['chat.helper.loading'])}</p>
346
+ {:else if !snapshot}<p>{t(M['chat.helper.unavailable'])}</p>
347
+ {:else}
348
+ <fieldset disabled={saving}>
349
+ <legend>{t(M['chat.helper.gallery'])}</legend>
350
+ <div class="gallery" role="radiogroup" tabindex="-1" aria-label={t(M['chat.helper.gallery'])} onkeydown={moveGallerySelection}>
351
+ {#each availableOfferings as offering, index (offering.id)}
352
+ <Button id={`${panelInstanceId}-offering-${index}`} tabindex={galleryTabStopIndex === index ? 0 : -1} type="button" variant="secondary" class={draft?.offeringId === offering.id ? 'selected' : ''} role="radio" aria-checked={draft?.offeringId === offering.id}
353
+ disabled={!draft || !editable('offeringId')} onclick={() => update('offeringId', offering.id)}>{offering.label}</Button>
354
+ {/each}
355
+ </div>
356
+ {#if previewOffering}
357
+ <div class="preview" aria-label={t(M['chat.helper.preview'])} bind:this={previewTarget}></div>
358
+ {/if}
359
+ {#if previewError}<p class="message" role="status">{previewError}</p>{/if}
360
+ {#if availableSetups.length}
361
+ <div class="setup-actions">
362
+ {#each availableSetups as style (style.id)}
363
+ <Button id={`${panelInstanceId}-setup-${style.id}`} type="button" variant="secondary" disabled={!editable('offeringId')} onclick={() => openSetup(style.id)}>{t(M['chat.helper.add_style'], { style: style.label })}</Button>
364
+ {/each}
365
+ </div>
366
+ {/if}
367
+ {#if setupStyleId && registry.get(setupStyleId)?.Setup}
368
+ {@const Setup = registry.get(setupStyleId)?.Setup}
369
+ <section bind:this={setupTarget} class="custom-setup" aria-label={t(M['chat.helper.custom_setup'])}>
370
+ <Button type="button" variant="secondary" onclick={closeSetup}>{t(M['chat.helper.back_to_settings'])}</Button>
371
+ {#if Setup}<Setup onsaved={customSaved} oncancel={closeSetup} />{/if}
372
+ </section>
373
+ {/if}
374
+ {#if draft}
375
+ <label>{t(M['chat.helper.name'])}<Input value={draft.name} disabled={!editable('name')} oninput={(event) => update('name', event.currentTarget.value)} /></label>
376
+ <label>{t(M['chat.helper.voice'])}<Select value={draft.voiceId} disabled={!editable('voiceId')} onchange={(event) => update('voiceId', event.currentTarget.value)}>{#each snapshot.voices as voice (voice.id)}<option value={voice.id}>{voice.label}</option>{/each}</Select></label>
377
+ <label>{t(M['chat.helper.placement'])}<Select value={draft.placement} disabled={!editable('placement')} onchange={(event) => update('placement', event.currentTarget.value as HelperPreferences['placement'])}><option value="bottom-left">{t(M['chat.helper.bottom_left'])}</option><option value="bottom-right">{t(M['chat.helper.bottom_right'])}</option></Select></label>
378
+ <Switch label={t(M['chat.helper.heard_subtitles'])} checked={draft.heardSubtitles} disabled={!editable('heardSubtitles')} onchange={(event) => update('heardSubtitles', event.currentTarget.checked)} />
379
+ <Switch label={t(M['chat.helper.spoken_subtitles'])} checked={draft.spokenSubtitles} disabled={!editable('spokenSubtitles')} onchange={(event) => update('spokenSubtitles', event.currentTarget.checked)} />
380
+ {:else}
381
+ <p class="message">{t(M['chat.helper.unavailable'])}</p>
382
+ {/if}
383
+ </fieldset>
384
+ {#if dirty}<p class="message" role="status">{t(M['chat.helper.unsaved_changes'])}</p>{/if}
385
+ <div class="actions">{#if canSave}<Button type="button" onclick={save} disabled={saving || !dirty}>{saving ? t(M['chat.helper.saving']) : t(M['chat.helper.save_changes'])}</Button>{/if}{#if snapshot.permissions.canReset}<Button type="button" variant="secondary" onclick={reset} disabled={saving}>{t(M['chat.helper.reset_button'])}</Button>{/if}</div>
386
+ {/if}
387
+ </section>
388
+
389
+ <style>
390
+ .helper-panel { display: grid; gap: var(--smrt-spacing-4); max-width: 42rem; padding: var(--smrt-spacing-4); color: var(--smrt-color-on-surface); }
391
+ fieldset { display: grid; gap: var(--smrt-spacing-3); border: 0; margin: 0; padding: 0; }
392
+ label { display: grid; gap: var(--smrt-spacing-1); font: var(--smrt-typography-label-large-font); }
393
+ .gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: var(--smrt-spacing-2); }
394
+ .gallery :global(.button) { min-height: 3rem; border-color: var(--smrt-color-outline); background: var(--smrt-color-surface); color: inherit; }
395
+ .gallery :global(.button.selected) { border-color: var(--smrt-color-primary); background: var(--smrt-color-primary-container); }
396
+ .preview { min-height: 5rem; display: grid; place-items: center; overflow: hidden; border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface-container-low); }
397
+ .preview :global(svg), .preview :global(img) { max-width: 100%; max-height: 12rem; }
398
+ .setup-actions, .actions { display: flex; flex-wrap: wrap; gap: var(--smrt-spacing-2); }
399
+ .custom-setup { display: grid; gap: var(--smrt-spacing-3); padding: var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-small); }
400
+ .message { margin: 0; color: var(--smrt-color-on-surface-variant); }
401
+ </style>
@@ -0,0 +1,23 @@
1
+ import type { HelperClient, HelperOffering, HelperSnapshot } from '../../../helper-preferences.js';
2
+ import type { HelperStyleRegistry } from './registry.js';
3
+ export interface Props {
4
+ /** Authenticated browser transport; only server snapshots become applied state. */
5
+ client: HelperClient;
6
+ /** Trusted style implementations available to this host. */
7
+ registry: HelperStyleRegistry;
8
+ /** Optional host-owned current snapshot. Absent snapshots load on mount. */
9
+ snapshot?: HelperSnapshot;
10
+ /**
11
+ * Authenticated bridge for a selected offering's runtime payload. The
12
+ * payload is deliberately never persisted by this panel or preferences.
13
+ */
14
+ loadPayload?: (offering: HelperOffering) => Promise<unknown>;
15
+ /** Receives each successful server-returned snapshot. */
16
+ onchanged?: (snapshot: HelperSnapshot) => void;
17
+ /** Notifies the host after an authenticated custom setup creates an offering. */
18
+ oncustomsaved?: (offering: HelperOffering) => void;
19
+ }
20
+ declare const HelperControlPanel: import("svelte").Component<Props, {}, "snapshot">;
21
+ type HelperControlPanel = ReturnType<typeof HelperControlPanel>;
22
+ export default HelperControlPanel;
23
+ //# sourceMappingURL=HelperControlPanel.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HelperControlPanel.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/helper/HelperControlPanel.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EAEd,cAAc,EACf,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAwB,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAG/E,MAAM,WAAW,KAAK;IACpB,mFAAmF;IACnF,MAAM,EAAE,YAAY,CAAC;IACrB,4DAA4D;IAC5D,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7D,yDAAyD;IACzD,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,iFAAiF;IACjF,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;CACpD;AAkXD,QAAA,MAAM,kBAAkB,mDAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { type PhotoCutoutRig } from '@happyvertical/animation';
2
+ import type { Component } from 'svelte';
3
+ import type { HelperOffering } from '../../../helper-preferences.js';
4
+ export interface HelperRendererHandle {
5
+ destroy(): void;
6
+ setMouthOpen(value: number): void;
7
+ setSpeaking?(speaking: boolean): void;
8
+ }
9
+ export interface HelperStyleSetupProps {
10
+ onsaved: (offering: HelperOffering) => void;
11
+ oncancel: () => void;
12
+ }
13
+ export interface HelperStyleMountInput {
14
+ target: HTMLElement;
15
+ offering: HelperOffering;
16
+ /** Resolved by an authenticated host bridge; never persist a URL here. */
17
+ payload: unknown;
18
+ }
19
+ export interface HelperStyleDefinition {
20
+ id: string;
21
+ label: string;
22
+ Setup?: Component<HelperStyleSetupProps>;
23
+ mount(input: HelperStyleMountInput): HelperRendererHandle | Promise<HelperRendererHandle>;
24
+ }
25
+ export interface HelperStyleRegistry {
26
+ get(id: string): HelperStyleDefinition | undefined;
27
+ list(): readonly HelperStyleDefinition[];
28
+ }
29
+ export interface HappyRuntime {
30
+ gsap: unknown;
31
+ morphSVG?: object;
32
+ }
33
+ export interface HappyHelperStyleOptions {
34
+ gsap?: unknown;
35
+ morphSVG?: object;
36
+ /** Called only while mounting Happy, so GSAP remains a host-owned lazy chunk. */
37
+ loadRuntime?: () => Promise<HappyRuntime>;
38
+ }
39
+ export declare const HAPPY_HELPER_OFFERING: HelperOffering;
40
+ export declare function createHelperStyleRegistry(styles: readonly HelperStyleDefinition[]): HelperStyleRegistry;
41
+ /**
42
+ * The animation package owns Happy's SVG geometry. This adapter never starts
43
+ * speech: host playback can signal `setSpeaking`, while voice remains host-owned.
44
+ */
45
+ export declare function createHappyHelperStyle(options?: HappyHelperStyleOptions): HelperStyleDefinition;
46
+ export interface PhotoCutoutPayload {
47
+ rig: PhotoCutoutRig;
48
+ image: Blob;
49
+ }
50
+ export declare function createPhotoCutoutHelperStyle(options?: {
51
+ Setup?: Component<HelperStyleSetupProps>;
52
+ }): HelperStyleDefinition;
53
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/helper/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAErE,MAAM,WAAW,oBAAoB;IACnC,OAAO,IAAI,IAAI,CAAC;IAChB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,WAAW,CAAC,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAC5C,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,cAAc,CAAC;IACzB,0EAA0E;IAC1E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC,qBAAqB,CAAC,CAAC;IACzC,KAAK,CACH,KAAK,EAAE,qBAAqB,GAC3B,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAAC;IACnD,IAAI,IAAI,SAAS,qBAAqB,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC;CAC3C;AAED,eAAO,MAAM,qBAAqB,EAAE,cAKnC,CAAC;AAEF,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,SAAS,qBAAqB,EAAE,GACvC,mBAAmB,CAUrB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,GAAE,uBAA4B,GACpC,qBAAqB,CAgCvB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,cAAc,CAAC;IACpB,KAAK,EAAE,IAAI,CAAC;CACb;AAYD,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,SAAS,CAAC,qBAAqB,CAAC,CAAA;CAAO,GACzD,qBAAqB,CAkCvB"}
@@ -0,0 +1,100 @@
1
+ import { createHappy, mountPhotoCutout, } from '@happyvertical/animation';
2
+ export const HAPPY_HELPER_OFFERING = {
3
+ id: 'happy',
4
+ label: 'Happy',
5
+ styleId: 'happy',
6
+ source: 'ready-made',
7
+ };
8
+ export function createHelperStyleRegistry(styles) {
9
+ const byId = new Map();
10
+ for (const style of styles) {
11
+ if (!style.id.trim())
12
+ throw new Error('Helper style ids must not be blank.');
13
+ if (byId.has(style.id))
14
+ throw new Error(`Duplicate helper style id: ${style.id}`);
15
+ byId.set(style.id, style);
16
+ }
17
+ return { get: (id) => byId.get(id), list: () => [...byId.values()] };
18
+ }
19
+ /**
20
+ * The animation package owns Happy's SVG geometry. This adapter never starts
21
+ * speech: host playback can signal `setSpeaking`, while voice remains host-owned.
22
+ */
23
+ export function createHappyHelperStyle(options = {}) {
24
+ return {
25
+ id: 'happy',
26
+ label: 'Happy',
27
+ async mount({ target, payload }) {
28
+ if (payload !== null && payload !== undefined)
29
+ throw new Error('Happy does not accept an asset payload.');
30
+ const runtime = options.loadRuntime
31
+ ? await options.loadRuntime()
32
+ : { gsap: options.gsap, morphSVG: options.morphSVG };
33
+ if (!runtime.gsap)
34
+ throw new Error('Happy requires a trusted GSAP runtime.');
35
+ const happy = createHappy({
36
+ target,
37
+ gsap: runtime.gsap,
38
+ morphSVG: runtime.morphSVG,
39
+ breathe: !prefersReducedMotion(),
40
+ });
41
+ let speaking = false;
42
+ return {
43
+ destroy: () => happy.destroy(),
44
+ // Happy's public character API does not expose a raw mouth amount.
45
+ // Do not call `say()`: it owns browser speech and would autoplay audio.
46
+ setMouthOpen: (amount) => happy.setSpeaking(amount > 0.05),
47
+ setSpeaking: (next) => {
48
+ if (speaking === next)
49
+ return;
50
+ speaking = next;
51
+ happy.setSpeaking(next);
52
+ },
53
+ };
54
+ },
55
+ };
56
+ }
57
+ function isPhotoPayload(payload) {
58
+ return (!!payload &&
59
+ typeof payload === 'object' &&
60
+ 'rig' in payload &&
61
+ 'image' in payload &&
62
+ payload.image instanceof Blob);
63
+ }
64
+ export function createPhotoCutoutHelperStyle(options = {}) {
65
+ return {
66
+ id: 'photo-cutout',
67
+ label: 'Canadian split-head photo',
68
+ ...(options.Setup ? { Setup: options.Setup } : {}),
69
+ mount({ target, payload }) {
70
+ if (!isPhotoPayload(payload))
71
+ throw new Error('Photographic helper requires a validated rig and image.');
72
+ const handle = mountPhotoCutout(payload.rig, {
73
+ target,
74
+ resolveAsset: async () => new File([payload.image], 'helper-cutout.png', {
75
+ type: payload.image.type || 'image/png',
76
+ }),
77
+ });
78
+ let mouthOpen = 0;
79
+ const setMouthOpen = (value) => {
80
+ mouthOpen = Math.max(0, Math.min(1, value));
81
+ handle.setMouthOpen(mouthOpen);
82
+ };
83
+ return {
84
+ destroy: () => handle.destroy(),
85
+ setMouthOpen,
86
+ // CharacterConversation supplies the raw playback envelope first, then
87
+ // this boolean state. Keep that amplitude; only an explicit stop closes
88
+ // the mouth so cleanup stays deterministic.
89
+ setSpeaking: (speaking) => {
90
+ if (!speaking)
91
+ setMouthOpen(0);
92
+ },
93
+ };
94
+ },
95
+ };
96
+ }
97
+ function prefersReducedMotion() {
98
+ return (typeof matchMedia === 'function' &&
99
+ matchMedia('(prefers-reduced-motion: reduce)').matches);
100
+ }
@@ -1,4 +1,16 @@
1
1
  export declare const M: {
2
+ readonly 'chat.character_conversation.saved_conversation': "chat.character_conversation.saved_conversation";
3
+ readonly 'chat.character_conversation.saved_character': "chat.character_conversation.saved_character";
4
+ readonly 'chat.character_conversation.listening_notice': "chat.character_conversation.listening_notice";
5
+ readonly 'chat.character_conversation.voice_input': "chat.character_conversation.voice_input";
6
+ readonly 'chat.character_conversation.speak': "chat.character_conversation.speak";
7
+ readonly 'chat.character_conversation.unavailable': "chat.character_conversation.unavailable";
8
+ readonly 'chat.character_conversation.message': "chat.character_conversation.message";
9
+ readonly 'chat.character_conversation.send': "chat.character_conversation.send";
10
+ readonly 'chat.character_conversation.assistant_reply': "chat.character_conversation.assistant_reply";
11
+ readonly 'chat.character_conversation.speech_failed': "chat.character_conversation.speech_failed";
12
+ readonly 'chat.caption_overlay.scroll_up': "chat.caption_overlay.scroll_up";
13
+ readonly 'chat.caption_overlay.scroll_down': "chat.caption_overlay.scroll_down";
2
14
  readonly 'chat.agent_chat.conversation': "chat.agent_chat.conversation";
3
15
  readonly 'chat.agent_chat.inactive_notice': "chat.agent_chat.inactive_notice";
4
16
  readonly 'chat.agent_chat.input_placeholder': "chat.agent_chat.input_placeholder";
@@ -33,6 +45,8 @@ export declare const M: {
33
45
  readonly 'chat.assistant_thread_list.conversations_label': "chat.assistant_thread_list.conversations_label";
34
46
  readonly 'chat.assistant_thread_list.new_conversation': "chat.assistant_thread_list.new_conversation";
35
47
  readonly 'chat.assistant_thread_list.untitled': "chat.assistant_thread_list.untitled";
48
+ readonly 'chat.floating_assistant.collapse': "chat.floating_assistant.collapse";
49
+ readonly 'chat.floating_assistant.close': "chat.floating_assistant.close";
36
50
  readonly 'chat.assistant_dock.no_surfaces': "chat.assistant_dock.no_surfaces";
37
51
  readonly 'chat.assistant_dock.loading_conversations': "chat.assistant_dock.loading_conversations";
38
52
  readonly 'chat.assistant_dock.choose_conversation': "chat.assistant_dock.choose_conversation";
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/svelte/i18n.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2IZ,CAAC"}
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/svelte/i18n.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiKZ,CAAC"}
@@ -11,6 +11,20 @@
11
11
  */
12
12
  import { defineMessages } from '@happyvertical/smrt-ui/i18n';
13
13
  export const M = defineMessages({
14
+ // Character conversation workbench
15
+ 'chat.character_conversation.saved_conversation': 'Saved character conversation',
16
+ 'chat.character_conversation.saved_character': 'Saved photographic character',
17
+ 'chat.character_conversation.listening_notice': 'Listening mode keeps the conversation and confirmations active while hiding message history.',
18
+ 'chat.character_conversation.voice_input': 'Voice input',
19
+ 'chat.character_conversation.speak': 'Speak to your assistant',
20
+ 'chat.character_conversation.unavailable': 'Speech input is unavailable. Type your message below.',
21
+ 'chat.character_conversation.message': 'Type your message',
22
+ 'chat.character_conversation.send': 'Send message',
23
+ 'chat.character_conversation.assistant_reply': 'Assistant reply',
24
+ 'chat.character_conversation.speech_failed': 'Could not play the spoken reply. The reply is shown below.',
25
+ // CaptionOverlay
26
+ 'chat.caption_overlay.scroll_up': 'Scroll captions up',
27
+ 'chat.caption_overlay.scroll_down': 'Scroll captions down',
14
28
  // AgentChat
15
29
  'chat.agent_chat.conversation': 'Agent conversation',
16
30
  'chat.agent_chat.inactive_notice': 'Session {status}. Cannot send messages.',
@@ -52,6 +66,9 @@ export const M = defineMessages({
52
66
  'chat.assistant_thread_list.conversations_label': 'Assistant conversations',
53
67
  'chat.assistant_thread_list.new_conversation': '+ New conversation',
54
68
  'chat.assistant_thread_list.untitled': 'Untitled',
69
+ // FloatingAssistant (#3642)
70
+ 'chat.floating_assistant.collapse': 'Collapse assistant',
71
+ 'chat.floating_assistant.close': 'Close',
55
72
  // AssistantDock (#2904)
56
73
  'chat.assistant_dock.no_surfaces': 'Nothing on this page can be changed from the chat. You can still ask questions.',
57
74
  'chat.assistant_dock.loading_conversations': 'Loading conversations…',