@runtypelabs/persona 3.18.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -2
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +383 -6
- package/dist/index.d.ts +383 -6
- package/dist/index.global.js +102 -1636
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +1514 -626
- package/dist/theme-editor.d.cts +192 -1
- package/dist/theme-editor.d.ts +192 -1
- package/dist/theme-editor.js +1628 -626
- package/dist/widget.css +348 -0
- package/package.json +1 -1
- package/src/components/composer-builder.test.ts +52 -0
- package/src/components/composer-builder.ts +67 -490
- package/src/components/composer-parts.test.ts +152 -0
- package/src/components/composer-parts.ts +452 -0
- package/src/components/header-builder.ts +22 -299
- package/src/components/header-parts.ts +360 -0
- package/src/components/panel.test.ts +61 -0
- package/src/components/panel.ts +262 -5
- package/src/components/pill-composer-builder.test.ts +85 -0
- package/src/components/pill-composer-builder.ts +183 -0
- package/src/index.ts +5 -0
- package/src/runtime/init.ts +4 -2
- package/src/runtime/persist-state.test.ts +152 -0
- package/src/session.test.ts +123 -0
- package/src/session.ts +58 -4
- package/src/styles/widget.css +348 -0
- package/src/types.ts +196 -1
- package/src/ui.component-directive.test.ts +183 -0
- package/src/ui.composer-bar.test.ts +1009 -0
- package/src/ui.ts +827 -72
- package/src/utils/attachment-manager.ts +1 -1
- package/src/utils/component-middleware.test.ts +134 -0
- package/src/utils/component-middleware.ts +44 -13
- package/src/utils/dock.test.ts +45 -0
- package/src/utils/dock.ts +3 -0
- package/src/utils/icons.ts +314 -58
- package/src/utils/stream-animation.ts +7 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "../ui";
|
|
6
|
+
import { createLocalStorageAdapter } from "../utils/storage";
|
|
7
|
+
import type { AgentWidgetStorageAdapter } from "../types";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_KEY = "persona-state";
|
|
10
|
+
|
|
11
|
+
const baseConfig = () => ({
|
|
12
|
+
apiUrl: "https://api.example.com/chat",
|
|
13
|
+
launcher: { enabled: false } as const,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const inject = (controller: ReturnType<typeof createAgentExperience>) =>
|
|
17
|
+
controller.injectAssistantMessage({ content: "hello world" });
|
|
18
|
+
|
|
19
|
+
describe("persistState gates storage adapter", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
document.body.innerHTML = "";
|
|
22
|
+
try {
|
|
23
|
+
window.localStorage.clear();
|
|
24
|
+
} catch {
|
|
25
|
+
/* jsdom edge cases */
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("persistState: false skips the default localStorage adapter", () => {
|
|
30
|
+
const mount = document.createElement("div");
|
|
31
|
+
document.body.appendChild(mount);
|
|
32
|
+
|
|
33
|
+
const controller = createAgentExperience(mount, {
|
|
34
|
+
...baseConfig(),
|
|
35
|
+
persistState: false,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
inject(controller);
|
|
39
|
+
|
|
40
|
+
expect(window.localStorage.getItem(DEFAULT_KEY)).toBeNull();
|
|
41
|
+
controller.destroy();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("persistState: false ignores any user-supplied storageAdapter (strict semantic)", () => {
|
|
45
|
+
const mount = document.createElement("div");
|
|
46
|
+
document.body.appendChild(mount);
|
|
47
|
+
|
|
48
|
+
const customAdapter: AgentWidgetStorageAdapter = {
|
|
49
|
+
load: vi.fn(() => null),
|
|
50
|
+
save: vi.fn(),
|
|
51
|
+
clear: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const controller = createAgentExperience(mount, {
|
|
55
|
+
...baseConfig(),
|
|
56
|
+
persistState: false,
|
|
57
|
+
storageAdapter: customAdapter,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
inject(controller);
|
|
61
|
+
|
|
62
|
+
expect(customAdapter.load).not.toHaveBeenCalled();
|
|
63
|
+
expect(customAdapter.save).not.toHaveBeenCalled();
|
|
64
|
+
controller.destroy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("default config (persistState undefined) writes to the default localStorage key", () => {
|
|
68
|
+
const mount = document.createElement("div");
|
|
69
|
+
document.body.appendChild(mount);
|
|
70
|
+
|
|
71
|
+
const controller = createAgentExperience(mount, baseConfig());
|
|
72
|
+
|
|
73
|
+
inject(controller);
|
|
74
|
+
|
|
75
|
+
const stored = window.localStorage.getItem(DEFAULT_KEY);
|
|
76
|
+
expect(stored).not.toBeNull();
|
|
77
|
+
const parsed = JSON.parse(stored!);
|
|
78
|
+
expect(parsed.messages).toBeInstanceOf(Array);
|
|
79
|
+
expect(parsed.messages.length).toBeGreaterThan(0);
|
|
80
|
+
controller.destroy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("persistState: true keeps using the default localStorage adapter", () => {
|
|
84
|
+
const mount = document.createElement("div");
|
|
85
|
+
document.body.appendChild(mount);
|
|
86
|
+
|
|
87
|
+
const controller = createAgentExperience(mount, {
|
|
88
|
+
...baseConfig(),
|
|
89
|
+
persistState: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
inject(controller);
|
|
93
|
+
|
|
94
|
+
expect(window.localStorage.getItem(DEFAULT_KEY)).not.toBeNull();
|
|
95
|
+
controller.destroy();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("two widgets with different storageAdapter keys keep their messages isolated", () => {
|
|
99
|
+
const mountA = document.createElement("div");
|
|
100
|
+
const mountB = document.createElement("div");
|
|
101
|
+
document.body.appendChild(mountA);
|
|
102
|
+
document.body.appendChild(mountB);
|
|
103
|
+
|
|
104
|
+
const controllerA = createAgentExperience(mountA, {
|
|
105
|
+
...baseConfig(),
|
|
106
|
+
storageAdapter: createLocalStorageAdapter("persona-state-test-a"),
|
|
107
|
+
});
|
|
108
|
+
const controllerB = createAgentExperience(mountB, {
|
|
109
|
+
...baseConfig(),
|
|
110
|
+
storageAdapter: createLocalStorageAdapter("persona-state-test-b"),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
controllerA.injectAssistantMessage({ content: "message in A" });
|
|
114
|
+
controllerB.injectAssistantMessage({ content: "message in B" });
|
|
115
|
+
|
|
116
|
+
const storedA = JSON.parse(window.localStorage.getItem("persona-state-test-a")!);
|
|
117
|
+
const storedB = JSON.parse(window.localStorage.getItem("persona-state-test-b")!);
|
|
118
|
+
|
|
119
|
+
const aHasA = storedA.messages.some((m: { content?: string }) => m.content === "message in A");
|
|
120
|
+
const aHasB = storedA.messages.some((m: { content?: string }) => m.content === "message in B");
|
|
121
|
+
const bHasA = storedB.messages.some((m: { content?: string }) => m.content === "message in A");
|
|
122
|
+
const bHasB = storedB.messages.some((m: { content?: string }) => m.content === "message in B");
|
|
123
|
+
|
|
124
|
+
expect(aHasA).toBe(true);
|
|
125
|
+
expect(aHasB).toBe(false);
|
|
126
|
+
expect(bHasB).toBe(true);
|
|
127
|
+
expect(bHasA).toBe(false);
|
|
128
|
+
expect(window.localStorage.getItem(DEFAULT_KEY)).toBeNull();
|
|
129
|
+
|
|
130
|
+
controllerA.destroy();
|
|
131
|
+
controllerB.destroy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("persistState: false does not read from localStorage on init", () => {
|
|
135
|
+
// Pre-seed the default key with a stored message.
|
|
136
|
+
window.localStorage.setItem(
|
|
137
|
+
DEFAULT_KEY,
|
|
138
|
+
JSON.stringify({ messages: [{ id: "stale", role: "assistant", content: "stale" }] })
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const mount = document.createElement("div");
|
|
142
|
+
document.body.appendChild(mount);
|
|
143
|
+
|
|
144
|
+
const controller = createAgentExperience(mount, {
|
|
145
|
+
...baseConfig(),
|
|
146
|
+
persistState: false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(controller.getMessages()).toEqual([]);
|
|
150
|
+
controller.destroy();
|
|
151
|
+
});
|
|
152
|
+
});
|
package/src/session.test.ts
CHANGED
|
@@ -244,6 +244,129 @@ describe('AgentWidgetSession - Message Injection', () => {
|
|
|
244
244
|
expect(messages[0].content).toBe('Legacy message');
|
|
245
245
|
});
|
|
246
246
|
});
|
|
247
|
+
|
|
248
|
+
describe('rawContent forwarding', () => {
|
|
249
|
+
it('preserves rawContent on injected messages', () => {
|
|
250
|
+
const directive = JSON.stringify({
|
|
251
|
+
text: 'Booking form',
|
|
252
|
+
component: 'BookingForm',
|
|
253
|
+
props: { title: 'Schedule' }
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const result = session.injectMessage({
|
|
257
|
+
role: 'assistant',
|
|
258
|
+
content: 'Booking form',
|
|
259
|
+
rawContent: directive
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
expect(result.rawContent).toBe(directive);
|
|
263
|
+
expect(messages[0].rawContent).toBe(directive);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('forwards rawContent through injectAssistantMessage', () => {
|
|
267
|
+
const directive = JSON.stringify({ text: 'Form', component: 'Form', props: {} });
|
|
268
|
+
|
|
269
|
+
const result = session.injectAssistantMessage({
|
|
270
|
+
content: 'Form',
|
|
271
|
+
rawContent: directive
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(result.rawContent).toBe(directive);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('forwards rawContent through injectMessageBatch', () => {
|
|
278
|
+
const directiveA = JSON.stringify({ text: 'A', component: 'CompA', props: {} });
|
|
279
|
+
const directiveB = JSON.stringify({ text: 'B', component: 'CompB', props: {} });
|
|
280
|
+
|
|
281
|
+
const results = session.injectMessageBatch([
|
|
282
|
+
{ role: 'assistant', content: 'A', rawContent: directiveA },
|
|
283
|
+
{ role: 'assistant', content: 'B', rawContent: directiveB }
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
expect(results[0].rawContent).toBe(directiveA);
|
|
287
|
+
expect(results[1].rawContent).toBe(directiveB);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('omits rawContent when not provided', () => {
|
|
291
|
+
const result = session.injectMessage({
|
|
292
|
+
role: 'assistant',
|
|
293
|
+
content: 'plain'
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(result.rawContent).toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('injectComponentDirective', () => {
|
|
301
|
+
it('builds rawContent from component + props + text', () => {
|
|
302
|
+
const result = session.injectComponentDirective({
|
|
303
|
+
component: 'DynamicForm',
|
|
304
|
+
props: { title: 'Book a demo', fields: [{ label: 'Email' }] },
|
|
305
|
+
text: 'Share your details to book a demo.'
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(result.role).toBe('assistant');
|
|
309
|
+
expect(result.content).toBe('Share your details to book a demo.');
|
|
310
|
+
expect(result.id).toMatch(/^ast_/);
|
|
311
|
+
expect(result.rawContent).toBeDefined();
|
|
312
|
+
|
|
313
|
+
const parsed = JSON.parse(result.rawContent as string);
|
|
314
|
+
expect(parsed).toEqual({
|
|
315
|
+
text: 'Share your details to book a demo.',
|
|
316
|
+
component: 'DynamicForm',
|
|
317
|
+
props: { title: 'Book a demo', fields: [{ label: 'Email' }] }
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('defaults text to empty string and props to {}', () => {
|
|
322
|
+
const result = session.injectComponentDirective({ component: 'DynamicForm' });
|
|
323
|
+
|
|
324
|
+
expect(result.content).toBe('');
|
|
325
|
+
const parsed = JSON.parse(result.rawContent as string);
|
|
326
|
+
expect(parsed).toEqual({ text: '', component: 'DynamicForm', props: {} });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('forwards llmContent for redacted LLM context', () => {
|
|
330
|
+
const result = session.injectComponentDirective({
|
|
331
|
+
component: 'DynamicForm',
|
|
332
|
+
props: { title: 'Book a demo' },
|
|
333
|
+
text: 'Booking form below.',
|
|
334
|
+
llmContent: '[Showed booking form]'
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(result.llmContent).toBe('[Showed booking form]');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('honors custom id, createdAt, sequence', () => {
|
|
341
|
+
const result = session.injectComponentDirective({
|
|
342
|
+
component: 'DynamicForm',
|
|
343
|
+
id: 'my-form-1',
|
|
344
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
345
|
+
sequence: 999
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(result.id).toBe('my-form-1');
|
|
349
|
+
expect(result.createdAt).toBe('2026-01-01T00:00:00.000Z');
|
|
350
|
+
expect(result.sequence).toBe(999);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('upserts an existing directive by id', () => {
|
|
354
|
+
session.injectComponentDirective({
|
|
355
|
+
component: 'DynamicForm',
|
|
356
|
+
props: { title: 'v1' },
|
|
357
|
+
id: 'reuse-me'
|
|
358
|
+
});
|
|
359
|
+
session.injectComponentDirective({
|
|
360
|
+
component: 'DynamicForm',
|
|
361
|
+
props: { title: 'v2' },
|
|
362
|
+
id: 'reuse-me'
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(messages).toHaveLength(1);
|
|
366
|
+
const parsed = JSON.parse(messages[0].rawContent as string);
|
|
367
|
+
expect(parsed.props.title).toBe('v2');
|
|
368
|
+
});
|
|
369
|
+
});
|
|
247
370
|
});
|
|
248
371
|
|
|
249
372
|
describe('AgentWidgetSession - cancel()', () => {
|
package/src/session.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
InjectAssistantMessageOptions,
|
|
12
12
|
InjectUserMessageOptions,
|
|
13
13
|
InjectSystemMessageOptions,
|
|
14
|
+
InjectComponentDirectiveOptions,
|
|
14
15
|
PersonaArtifactRecord,
|
|
15
16
|
PersonaArtifactManualUpsert
|
|
16
17
|
} from "./types";
|
|
@@ -572,7 +573,8 @@ export class AgentWidgetSession {
|
|
|
572
573
|
createdAt,
|
|
573
574
|
sequence,
|
|
574
575
|
streaming = false,
|
|
575
|
-
voiceProcessing
|
|
576
|
+
voiceProcessing,
|
|
577
|
+
rawContent
|
|
576
578
|
} = options;
|
|
577
579
|
|
|
578
580
|
// Generate appropriate ID based on role
|
|
@@ -594,7 +596,8 @@ export class AgentWidgetSession {
|
|
|
594
596
|
// Only include optional fields if provided
|
|
595
597
|
...(llmContent !== undefined && { llmContent }),
|
|
596
598
|
...(contentParts !== undefined && { contentParts }),
|
|
597
|
-
...(voiceProcessing !== undefined && { voiceProcessing })
|
|
599
|
+
...(voiceProcessing !== undefined && { voiceProcessing }),
|
|
600
|
+
...(rawContent !== undefined && { rawContent })
|
|
598
601
|
};
|
|
599
602
|
|
|
600
603
|
// Use upsert to handle both new messages and updates (streaming)
|
|
@@ -673,7 +676,9 @@ export class AgentWidgetSession {
|
|
|
673
676
|
id,
|
|
674
677
|
createdAt,
|
|
675
678
|
sequence,
|
|
676
|
-
streaming = false
|
|
679
|
+
streaming = false,
|
|
680
|
+
voiceProcessing,
|
|
681
|
+
rawContent
|
|
677
682
|
} = options;
|
|
678
683
|
|
|
679
684
|
const messageId =
|
|
@@ -692,7 +697,9 @@ export class AgentWidgetSession {
|
|
|
692
697
|
sequence: sequence ?? this.nextSequence(),
|
|
693
698
|
streaming,
|
|
694
699
|
...(llmContent !== undefined && { llmContent }),
|
|
695
|
-
...(contentParts !== undefined && { contentParts })
|
|
700
|
+
...(contentParts !== undefined && { contentParts }),
|
|
701
|
+
...(voiceProcessing !== undefined && { voiceProcessing }),
|
|
702
|
+
...(rawContent !== undefined && { rawContent })
|
|
696
703
|
};
|
|
697
704
|
|
|
698
705
|
results.push(message);
|
|
@@ -705,6 +712,53 @@ export class AgentWidgetSession {
|
|
|
705
712
|
return results;
|
|
706
713
|
}
|
|
707
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Convenience method for injecting a registered component directive as
|
|
717
|
+
* an assistant message — the same shape Persona produces from a streamed
|
|
718
|
+
* `{ "text": "...", "component": "...", "props": {...} }` payload.
|
|
719
|
+
*
|
|
720
|
+
* Sets `content` to `text`, `rawContent` to the JSON directive (so
|
|
721
|
+
* `extractComponentDirectiveFromMessage` can find it), and forwards
|
|
722
|
+
* `llmContent` / `id` / `createdAt` / `sequence`.
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* session.injectComponentDirective({
|
|
726
|
+
* component: "DynamicForm",
|
|
727
|
+
* props: { title: "Book a demo", fields: [...] },
|
|
728
|
+
* text: "Share your details to book a demo.",
|
|
729
|
+
* llmContent: "[Showed booking form]"
|
|
730
|
+
* });
|
|
731
|
+
*/
|
|
732
|
+
public injectComponentDirective(
|
|
733
|
+
options: InjectComponentDirectiveOptions
|
|
734
|
+
): AgentWidgetMessage {
|
|
735
|
+
const {
|
|
736
|
+
component,
|
|
737
|
+
props = {},
|
|
738
|
+
text = "",
|
|
739
|
+
llmContent,
|
|
740
|
+
id,
|
|
741
|
+
createdAt,
|
|
742
|
+
sequence
|
|
743
|
+
} = options;
|
|
744
|
+
|
|
745
|
+
const directive: { text: string; component: string; props: Record<string, unknown> } = {
|
|
746
|
+
text,
|
|
747
|
+
component,
|
|
748
|
+
props
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
return this.injectMessage({
|
|
752
|
+
role: "assistant",
|
|
753
|
+
content: text,
|
|
754
|
+
rawContent: JSON.stringify(directive),
|
|
755
|
+
...(llmContent !== undefined && { llmContent }),
|
|
756
|
+
...(id !== undefined && { id }),
|
|
757
|
+
...(createdAt !== undefined && { createdAt }),
|
|
758
|
+
...(sequence !== undefined && { sequence })
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
708
762
|
public async sendMessage(
|
|
709
763
|
rawInput: string,
|
|
710
764
|
options?: {
|