@runtypelabs/persona 1.36.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 (61) hide show
  1. package/README.md +1080 -0
  2. package/dist/index.cjs +140 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2626 -0
  5. package/dist/index.d.ts +2626 -0
  6. package/dist/index.global.js +1843 -0
  7. package/dist/index.global.js.map +1 -0
  8. package/dist/index.js +140 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/install.global.js +2 -0
  11. package/dist/install.global.js.map +1 -0
  12. package/dist/widget.css +1627 -0
  13. package/package.json +79 -0
  14. package/src/@types/idiomorph.d.ts +37 -0
  15. package/src/client.test.ts +387 -0
  16. package/src/client.ts +1589 -0
  17. package/src/components/composer-builder.ts +530 -0
  18. package/src/components/feedback.ts +379 -0
  19. package/src/components/forms.ts +170 -0
  20. package/src/components/header-builder.ts +455 -0
  21. package/src/components/header-layouts.ts +303 -0
  22. package/src/components/launcher.ts +193 -0
  23. package/src/components/message-bubble.ts +528 -0
  24. package/src/components/messages.ts +54 -0
  25. package/src/components/panel.ts +204 -0
  26. package/src/components/reasoning-bubble.ts +144 -0
  27. package/src/components/registry.ts +87 -0
  28. package/src/components/suggestions.ts +97 -0
  29. package/src/components/tool-bubble.ts +288 -0
  30. package/src/defaults.ts +321 -0
  31. package/src/index.ts +175 -0
  32. package/src/install.ts +284 -0
  33. package/src/plugins/registry.ts +77 -0
  34. package/src/plugins/types.ts +95 -0
  35. package/src/postprocessors.ts +194 -0
  36. package/src/runtime/init.ts +162 -0
  37. package/src/session.ts +376 -0
  38. package/src/styles/tailwind.css +20 -0
  39. package/src/styles/widget.css +1627 -0
  40. package/src/types.ts +1635 -0
  41. package/src/ui.ts +3341 -0
  42. package/src/utils/actions.ts +227 -0
  43. package/src/utils/attachment-manager.ts +384 -0
  44. package/src/utils/code-generators.test.ts +500 -0
  45. package/src/utils/code-generators.ts +1806 -0
  46. package/src/utils/component-middleware.ts +137 -0
  47. package/src/utils/component-parser.ts +119 -0
  48. package/src/utils/constants.ts +16 -0
  49. package/src/utils/content.ts +306 -0
  50. package/src/utils/dom.ts +25 -0
  51. package/src/utils/events.ts +41 -0
  52. package/src/utils/formatting.test.ts +166 -0
  53. package/src/utils/formatting.ts +470 -0
  54. package/src/utils/icons.ts +92 -0
  55. package/src/utils/message-id.ts +37 -0
  56. package/src/utils/morph.ts +36 -0
  57. package/src/utils/positioning.ts +17 -0
  58. package/src/utils/storage.ts +72 -0
  59. package/src/utils/theme.ts +105 -0
  60. package/src/widget.css +1 -0
  61. package/widget.css +1 -0
@@ -0,0 +1,1806 @@
1
+ import type { AgentWidgetConfig } from "../types";
2
+
3
+ type ParserType = "plain" | "json" | "regex-json" | "xml";
4
+ export type CodeFormat = "esm" | "script-installer" | "script-manual" | "script-advanced" | "react-component" | "react-advanced";
5
+
6
+ /**
7
+ * Hook code templates for code generation.
8
+ * Each hook can be provided as a string (code template) OR as an actual function.
9
+ * Functions are automatically serialized via `.toString()`.
10
+ *
11
+ * IMPORTANT: When providing functions:
12
+ * - Functions must be self-contained (no external variables/closures)
13
+ * - External variables will be undefined when the generated code runs
14
+ * - Use arrow functions or regular function expressions
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // Both of these work:
19
+ *
20
+ * // As string:
21
+ * { getHeaders: "async () => ({ 'Authorization': 'Bearer token' })" }
22
+ *
23
+ * // As function (recommended - better IDE support):
24
+ * { getHeaders: async () => ({ 'Authorization': 'Bearer token' }) }
25
+ * ```
26
+ */
27
+ export type CodeGeneratorHooks = {
28
+ /**
29
+ * Custom getHeaders function.
30
+ * Should return an object with header key-value pairs.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * async () => ({ 'Authorization': `Bearer ${await getAuthToken()}` })
35
+ * ```
36
+ */
37
+ getHeaders?: string | (() => Record<string, string> | Promise<Record<string, string>>);
38
+
39
+ /**
40
+ * Custom onFeedback callback for message actions.
41
+ * Receives a feedback object with type, messageId, and message.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * (feedback) => { console.log('Feedback:', feedback.type); }
46
+ * ```
47
+ */
48
+ onFeedback?: string | ((feedback: { type: string; messageId: string; message: unknown }) => void);
49
+
50
+ /**
51
+ * Custom onCopy callback for message actions.
52
+ * Receives the message that was copied.
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * (message) => { analytics.track('message_copied', { id: message.id }); }
57
+ * ```
58
+ */
59
+ onCopy?: string | ((message: unknown) => void);
60
+
61
+ /**
62
+ * Custom requestMiddleware function.
63
+ * Receives { payload, config } context.
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * ({ payload }) => ({ ...payload, metadata: { pageUrl: window.location.href } })
68
+ * ```
69
+ */
70
+ requestMiddleware?: string | ((context: { payload: unknown; config: unknown }) => unknown);
71
+
72
+ /**
73
+ * Custom action handlers array.
74
+ * Array of handler functions.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * [
79
+ * (action, context) => {
80
+ * if (action.type === 'custom') {
81
+ * return { handled: true };
82
+ * }
83
+ * }
84
+ * ]
85
+ * ```
86
+ */
87
+ actionHandlers?: string | Array<(action: unknown, context: unknown) => unknown>;
88
+
89
+ /**
90
+ * Custom action parsers array.
91
+ * Array of parser functions.
92
+ */
93
+ actionParsers?: string | Array<(context: unknown) => unknown>;
94
+
95
+ /**
96
+ * Custom postprocessMessage function.
97
+ * Receives { text, message, streaming, raw } context.
98
+ * Will override the default markdownPostprocessor.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * ({ text }) => customMarkdownProcessor(text)
103
+ * ```
104
+ */
105
+ postprocessMessage?: string | ((context: { text: string; message?: unknown; streaming?: boolean; raw?: string }) => string);
106
+
107
+ /**
108
+ * Custom context providers array.
109
+ * Array of provider functions.
110
+ */
111
+ contextProviders?: string | Array<() => unknown>;
112
+
113
+ /**
114
+ * Custom stream parser factory.
115
+ * Should be a function that returns a StreamParser.
116
+ */
117
+ streamParser?: string | (() => unknown);
118
+ };
119
+
120
+ /**
121
+ * Options for code generation beyond format selection.
122
+ */
123
+ export type CodeGeneratorOptions = {
124
+ /**
125
+ * Custom hook code to inject into the generated snippet.
126
+ * Hooks are JavaScript/TypeScript code strings that will be
127
+ * inserted at appropriate locations in the output.
128
+ */
129
+ hooks?: CodeGeneratorHooks;
130
+
131
+ /**
132
+ * Whether to include comments explaining each hook.
133
+ * @default true
134
+ */
135
+ includeHookComments?: boolean;
136
+ };
137
+
138
+ // Internal type for normalized hooks (always strings)
139
+ type NormalizedHooks = {
140
+ [K in keyof CodeGeneratorHooks]: string | undefined;
141
+ };
142
+
143
+ /**
144
+ * Serialize a hook value (string, function, or array of functions) to a string.
145
+ */
146
+ function serializeHook(hook: string | Function | Function[] | undefined): string | undefined {
147
+ if (hook === undefined) return undefined;
148
+ if (typeof hook === 'string') return hook;
149
+ if (Array.isArray(hook)) {
150
+ return `[${hook.map(fn => fn.toString()).join(', ')}]`;
151
+ }
152
+ return hook.toString();
153
+ }
154
+
155
+ /**
156
+ * Normalize hooks by converting any functions to their string representations.
157
+ */
158
+ function normalizeHooks(hooks: CodeGeneratorHooks | undefined): NormalizedHooks | undefined {
159
+ if (!hooks) return undefined;
160
+
161
+ return {
162
+ getHeaders: serializeHook(hooks.getHeaders),
163
+ onFeedback: serializeHook(hooks.onFeedback),
164
+ onCopy: serializeHook(hooks.onCopy),
165
+ requestMiddleware: serializeHook(hooks.requestMiddleware),
166
+ actionHandlers: serializeHook(hooks.actionHandlers),
167
+ actionParsers: serializeHook(hooks.actionParsers),
168
+ postprocessMessage: serializeHook(hooks.postprocessMessage),
169
+ contextProviders: serializeHook(hooks.contextProviders),
170
+ streamParser: serializeHook(hooks.streamParser),
171
+ };
172
+ }
173
+
174
+ // =============================================================================
175
+ // Template Literals for Code Generation
176
+ // These are injected into generated code as-is.
177
+ // =============================================================================
178
+
179
+ /**
180
+ * Template: Parser for JSON wrapped in markdown code fences (TypeScript).
181
+ * @internal
182
+ */
183
+ const TEMPLATE_MARKDOWN_JSON_PARSER_TS = `({ text, message }: any) => {
184
+ const jsonSource = (message as any).rawContent || text || message.content;
185
+ if (!jsonSource || typeof jsonSource !== 'string') return null;
186
+ let cleanJson = jsonSource
187
+ .replace(/^\`\`\`(?:json)?\\s*\\n?/, '')
188
+ .replace(/\\n?\`\`\`\\s*$/, '')
189
+ .trim();
190
+ if (!cleanJson.startsWith('{') || !cleanJson.endsWith('}')) return null;
191
+ try {
192
+ const parsed = JSON.parse(cleanJson);
193
+ if (parsed.action) return { type: parsed.action, payload: parsed };
194
+ } catch (e) { return null; }
195
+ return null;
196
+ }`;
197
+
198
+ /**
199
+ * Template: Parser for JSON wrapped in markdown code fences (ES5).
200
+ * @internal
201
+ */
202
+ const TEMPLATE_MARKDOWN_JSON_PARSER_ES5 = `function(ctx) {
203
+ var jsonSource = ctx.message.rawContent || ctx.text || ctx.message.content;
204
+ if (!jsonSource || typeof jsonSource !== 'string') return null;
205
+ var cleanJson = jsonSource
206
+ .replace(/^\`\`\`(?:json)?\\s*\\n?/, '')
207
+ .replace(/\\n?\`\`\`\\s*$/, '')
208
+ .trim();
209
+ if (!cleanJson.startsWith('{') || !cleanJson.endsWith('}')) return null;
210
+ try {
211
+ var parsed = JSON.parse(cleanJson);
212
+ if (parsed.action) return { type: parsed.action, payload: parsed };
213
+ } catch (e) { return null; }
214
+ return null;
215
+ }`;
216
+
217
+ /**
218
+ * Template: Handler for nav_then_click actions (TypeScript).
219
+ * @internal
220
+ */
221
+ const TEMPLATE_NAV_THEN_CLICK_HANDLER_TS = `(action: any, context: any) => {
222
+ if (action.type !== 'nav_then_click') return;
223
+ const payload = action.payload || action.raw || {};
224
+ const url = payload?.page;
225
+ const text = payload?.on_load_text || 'Navigating...';
226
+ if (!url) return { handled: true, displayText: text };
227
+ const messageId = context.message?.id;
228
+ const processedActions = JSON.parse(localStorage.getItem(PROCESSED_ACTIONS_KEY) || '[]');
229
+ const actionKey = \`nav_\${messageId}_\${url}\`;
230
+ if (processedActions.includes(actionKey)) {
231
+ return { handled: true, displayText: text };
232
+ }
233
+ processedActions.push(actionKey);
234
+ localStorage.setItem(PROCESSED_ACTIONS_KEY, JSON.stringify(processedActions));
235
+ const targetUrl = url.startsWith('http') ? url : new URL(url, window.location.origin).toString();
236
+ window.location.href = targetUrl;
237
+ return { handled: true, displayText: text };
238
+ }`;
239
+
240
+ /**
241
+ * Template: Handler for nav_then_click actions (ES5).
242
+ * @internal
243
+ */
244
+ const TEMPLATE_NAV_THEN_CLICK_HANDLER_ES5 = `function(action, context) {
245
+ if (action.type !== 'nav_then_click') return;
246
+ var payload = action.payload || action.raw || {};
247
+ var url = payload.page;
248
+ var text = payload.on_load_text || 'Navigating...';
249
+ if (!url) return { handled: true, displayText: text };
250
+ var messageId = context.message ? context.message.id : null;
251
+ var processedActions = JSON.parse(localStorage.getItem(PROCESSED_ACTIONS_KEY) || '[]');
252
+ var actionKey = 'nav_' + messageId + '_' + url;
253
+ if (processedActions.includes(actionKey)) {
254
+ return { handled: true, displayText: text };
255
+ }
256
+ processedActions.push(actionKey);
257
+ localStorage.setItem(PROCESSED_ACTIONS_KEY, JSON.stringify(processedActions));
258
+ var targetUrl = url.startsWith('http') ? url : new URL(url, window.location.origin).toString();
259
+ window.location.href = targetUrl;
260
+ return { handled: true, displayText: text };
261
+ }`;
262
+
263
+ /**
264
+ * Template: Stream parser callback (TypeScript).
265
+ * @internal
266
+ */
267
+ const TEMPLATE_STREAM_PARSER_CALLBACK_TS = `(parsed: any) => {
268
+ if (!parsed || typeof parsed !== 'object') return null;
269
+ if (parsed.action === 'nav_then_click') return 'Navigating...';
270
+ if (parsed.action === 'message') return parsed.text || '';
271
+ if (parsed.action === 'message_and_click') return parsed.text || 'Processing...';
272
+ return parsed.text || null;
273
+ }`;
274
+
275
+ /**
276
+ * Template: Stream parser callback (ES5).
277
+ * @internal
278
+ */
279
+ const TEMPLATE_STREAM_PARSER_CALLBACK_ES5 = `function(parsed) {
280
+ if (!parsed || typeof parsed !== 'object') return null;
281
+ if (parsed.action === 'nav_then_click') return 'Navigating...';
282
+ if (parsed.action === 'message') return parsed.text || '';
283
+ if (parsed.action === 'message_and_click') return parsed.text || 'Processing...';
284
+ return parsed.text || null;
285
+ }`;
286
+
287
+ function detectParserTypeFromStreamParser(streamParser: any): ParserType | null {
288
+ if (!streamParser) return null;
289
+ const fnString = streamParser.toString();
290
+ if (fnString.includes("createJsonStreamParser") || fnString.includes("partial-json")) {
291
+ return "json";
292
+ }
293
+ if (fnString.includes("createRegexJsonParser") || fnString.includes("regex")) {
294
+ return "regex-json";
295
+ }
296
+ if (fnString.includes("createXmlParser") || fnString.includes("<text>")) {
297
+ return "xml";
298
+ }
299
+ return null;
300
+ }
301
+
302
+ function getParserTypeFromConfig(config: AgentWidgetConfig): ParserType {
303
+ return config.parserType ?? detectParserTypeFromStreamParser(config.streamParser) ?? "plain";
304
+ }
305
+
306
+ // Helper to generate toolCall config
307
+ function generateToolCallConfig(config: any, indent: string): string[] {
308
+ const lines: string[] = [];
309
+ if (config.toolCall) {
310
+ lines.push(`${indent}toolCall: {`);
311
+ Object.entries(config.toolCall).forEach(([key, value]) => {
312
+ if (typeof value === "string") {
313
+ lines.push(`${indent} ${key}: "${value}",`);
314
+ }
315
+ });
316
+ lines.push(`${indent}},`);
317
+ }
318
+ return lines;
319
+ }
320
+
321
+ // Helper to generate messageActions config (with optional hook callbacks)
322
+ function generateMessageActionsConfig(config: any, indent: string, hooks?: CodeGeneratorHooks): string[] {
323
+ const lines: string[] = [];
324
+ const hasSerializableProps = config.messageActions && Object.entries(config.messageActions).some(
325
+ ([key, value]) => key !== "onFeedback" && key !== "onCopy" && value !== undefined
326
+ );
327
+ const hasHookCallbacks = hooks?.onFeedback || hooks?.onCopy;
328
+
329
+ if (hasSerializableProps || hasHookCallbacks) {
330
+ lines.push(`${indent}messageActions: {`);
331
+
332
+ // Add serializable properties from config
333
+ if (config.messageActions) {
334
+ Object.entries(config.messageActions).forEach(([key, value]) => {
335
+ // Skip function callbacks - we'll add from hooks if provided
336
+ if (key === "onFeedback" || key === "onCopy") return;
337
+ if (typeof value === "string") {
338
+ lines.push(`${indent} ${key}: "${value}",`);
339
+ } else if (typeof value === "boolean") {
340
+ lines.push(`${indent} ${key}: ${value},`);
341
+ }
342
+ });
343
+ }
344
+
345
+ // Add hook callbacks
346
+ if (hooks?.onFeedback) {
347
+ lines.push(`${indent} onFeedback: ${hooks.onFeedback},`);
348
+ }
349
+ if (hooks?.onCopy) {
350
+ lines.push(`${indent} onCopy: ${hooks.onCopy},`);
351
+ }
352
+
353
+ lines.push(`${indent}},`);
354
+ }
355
+ return lines;
356
+ }
357
+
358
+ // Helper to generate markdown config (excluding renderer functions)
359
+ function generateMarkdownConfig(config: any, indent: string): string[] {
360
+ const lines: string[] = [];
361
+ if (config.markdown) {
362
+ const hasOptions = config.markdown.options && Object.keys(config.markdown.options).length > 0;
363
+ const hasDisableDefaultStyles = config.markdown.disableDefaultStyles !== undefined;
364
+
365
+ if (hasOptions || hasDisableDefaultStyles) {
366
+ lines.push(`${indent}markdown: {`);
367
+
368
+ if (hasOptions) {
369
+ lines.push(`${indent} options: {`);
370
+ Object.entries(config.markdown.options).forEach(([key, value]) => {
371
+ if (typeof value === "string") {
372
+ lines.push(`${indent} ${key}: "${value}",`);
373
+ } else if (typeof value === "boolean") {
374
+ lines.push(`${indent} ${key}: ${value},`);
375
+ }
376
+ });
377
+ lines.push(`${indent} },`);
378
+ }
379
+
380
+ if (hasDisableDefaultStyles) {
381
+ lines.push(`${indent} disableDefaultStyles: ${config.markdown.disableDefaultStyles},`);
382
+ }
383
+
384
+ lines.push(`${indent}},`);
385
+ }
386
+ }
387
+ return lines;
388
+ }
389
+
390
+ // Helper to generate layout config (excluding render functions and slots)
391
+ function generateLayoutConfig(config: any, indent: string): string[] {
392
+ const lines: string[] = [];
393
+ if (config.layout) {
394
+ const hasHeader = config.layout.header && Object.keys(config.layout.header).some(
395
+ (key: string) => key !== "render"
396
+ );
397
+ const hasMessages = config.layout.messages && Object.keys(config.layout.messages).some(
398
+ (key: string) => key !== "renderUserMessage" && key !== "renderAssistantMessage"
399
+ );
400
+
401
+ if (hasHeader || hasMessages) {
402
+ lines.push(`${indent}layout: {`);
403
+
404
+ // Header config (excluding render function)
405
+ if (hasHeader) {
406
+ lines.push(`${indent} header: {`);
407
+ Object.entries(config.layout.header).forEach(([key, value]) => {
408
+ if (key === "render") return; // Skip render function
409
+ if (typeof value === "string") {
410
+ lines.push(`${indent} ${key}: "${value}",`);
411
+ } else if (typeof value === "boolean") {
412
+ lines.push(`${indent} ${key}: ${value},`);
413
+ }
414
+ });
415
+ lines.push(`${indent} },`);
416
+ }
417
+
418
+ // Messages config (excluding render functions)
419
+ if (hasMessages) {
420
+ lines.push(`${indent} messages: {`);
421
+ Object.entries(config.layout.messages).forEach(([key, value]) => {
422
+ // Skip render functions
423
+ if (key === "renderUserMessage" || key === "renderAssistantMessage") return;
424
+
425
+ if (key === "avatar" && typeof value === "object" && value !== null) {
426
+ lines.push(`${indent} avatar: {`);
427
+ Object.entries(value as Record<string, unknown>).forEach(([avatarKey, avatarValue]) => {
428
+ if (typeof avatarValue === "string") {
429
+ lines.push(`${indent} ${avatarKey}: "${avatarValue}",`);
430
+ } else if (typeof avatarValue === "boolean") {
431
+ lines.push(`${indent} ${avatarKey}: ${avatarValue},`);
432
+ }
433
+ });
434
+ lines.push(`${indent} },`);
435
+ } else if (key === "timestamp" && typeof value === "object" && value !== null) {
436
+ // Only emit serializable timestamp properties (skip format function)
437
+ const hasSerializableTimestamp = Object.entries(value as Record<string, unknown>).some(
438
+ ([k]) => k !== "format"
439
+ );
440
+ if (hasSerializableTimestamp) {
441
+ lines.push(`${indent} timestamp: {`);
442
+ Object.entries(value as Record<string, unknown>).forEach(([tsKey, tsValue]) => {
443
+ if (tsKey === "format") return; // Skip format function
444
+ if (typeof tsValue === "string") {
445
+ lines.push(`${indent} ${tsKey}: "${tsValue}",`);
446
+ } else if (typeof tsValue === "boolean") {
447
+ lines.push(`${indent} ${tsKey}: ${tsValue},`);
448
+ }
449
+ });
450
+ lines.push(`${indent} },`);
451
+ }
452
+ } else if (typeof value === "string") {
453
+ lines.push(`${indent} ${key}: "${value}",`);
454
+ } else if (typeof value === "boolean") {
455
+ lines.push(`${indent} ${key}: ${value},`);
456
+ }
457
+ });
458
+ lines.push(`${indent} },`);
459
+ }
460
+
461
+ lines.push(`${indent}},`);
462
+ }
463
+ }
464
+ return lines;
465
+ }
466
+
467
+ // Helper to generate hook-related config lines
468
+ function generateHooksConfig(hooks: CodeGeneratorHooks | undefined, indent: string): string[] {
469
+ const lines: string[] = [];
470
+ if (!hooks) return lines;
471
+
472
+ if (hooks.getHeaders) {
473
+ lines.push(`${indent}getHeaders: ${hooks.getHeaders},`);
474
+ }
475
+
476
+ if (hooks.requestMiddleware) {
477
+ lines.push(`${indent}requestMiddleware: ${hooks.requestMiddleware},`);
478
+ }
479
+
480
+ if (hooks.actionParsers) {
481
+ lines.push(`${indent}actionParsers: ${hooks.actionParsers},`);
482
+ }
483
+
484
+ if (hooks.actionHandlers) {
485
+ lines.push(`${indent}actionHandlers: ${hooks.actionHandlers},`);
486
+ }
487
+
488
+ if (hooks.contextProviders) {
489
+ lines.push(`${indent}contextProviders: ${hooks.contextProviders},`);
490
+ }
491
+
492
+ if (hooks.streamParser) {
493
+ lines.push(`${indent}streamParser: ${hooks.streamParser},`);
494
+ }
495
+
496
+ return lines;
497
+ }
498
+
499
+ export function generateCodeSnippet(
500
+ config: any,
501
+ format: CodeFormat = "esm",
502
+ options?: CodeGeneratorOptions
503
+ ): string {
504
+ // Remove non-serializable properties
505
+ const cleanConfig = { ...config };
506
+ delete cleanConfig.postprocessMessage;
507
+ delete cleanConfig.initialMessages;
508
+
509
+ // Normalize hooks - convert functions to strings via .toString()
510
+ const normalizedOptions: CodeGeneratorOptions | undefined = options
511
+ ? { ...options, hooks: normalizeHooks(options.hooks) as CodeGeneratorHooks }
512
+ : undefined;
513
+
514
+ if (format === "esm") {
515
+ return generateESMCode(cleanConfig, normalizedOptions);
516
+ } else if (format === "script-installer") {
517
+ return generateScriptInstallerCode(cleanConfig);
518
+ } else if (format === "script-advanced") {
519
+ return generateScriptAdvancedCode(cleanConfig, normalizedOptions);
520
+ } else if (format === "react-component") {
521
+ return generateReactComponentCode(cleanConfig, normalizedOptions);
522
+ } else if (format === "react-advanced") {
523
+ return generateReactAdvancedCode(cleanConfig, normalizedOptions);
524
+ } else {
525
+ return generateScriptManualCode(cleanConfig, normalizedOptions);
526
+ }
527
+ }
528
+
529
+ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
530
+ const hooks = options?.hooks;
531
+ const parserType = getParserTypeFromConfig(config as AgentWidgetConfig);
532
+ const shouldEmitParserType = parserType !== "plain";
533
+
534
+ const lines: string[] = [
535
+ "import '@runtypelabs/persona/widget.css';",
536
+ "import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';",
537
+ "",
538
+ "initAgentWidget({",
539
+ " target: 'body',",
540
+ " config: {"
541
+ ];
542
+
543
+ if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
544
+ if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
545
+ if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
546
+ if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
547
+
548
+ if (config.theme) {
549
+ lines.push(" theme: {");
550
+ Object.entries(config.theme).forEach(([key, value]) => {
551
+ lines.push(` ${key}: "${value}",`);
552
+ });
553
+ lines.push(" },");
554
+ }
555
+
556
+ if (config.launcher) {
557
+ lines.push(" launcher: {");
558
+ Object.entries(config.launcher).forEach(([key, value]) => {
559
+ if (typeof value === "string") {
560
+ lines.push(` ${key}: "${value}",`);
561
+ } else if (typeof value === "boolean") {
562
+ lines.push(` ${key}: ${value},`);
563
+ }
564
+ });
565
+ lines.push(" },");
566
+ }
567
+
568
+ if (config.copy) {
569
+ lines.push(" copy: {");
570
+ Object.entries(config.copy).forEach(([key, value]) => {
571
+ lines.push(` ${key}: "${value}",`);
572
+ });
573
+ lines.push(" },");
574
+ }
575
+
576
+ if (config.sendButton) {
577
+ lines.push(" sendButton: {");
578
+ Object.entries(config.sendButton).forEach(([key, value]) => {
579
+ if (typeof value === "string") {
580
+ lines.push(` ${key}: "${value}",`);
581
+ } else if (typeof value === "boolean") {
582
+ lines.push(` ${key}: ${value},`);
583
+ }
584
+ });
585
+ lines.push(" },");
586
+ }
587
+
588
+ if (config.voiceRecognition) {
589
+ lines.push(" voiceRecognition: {");
590
+ Object.entries(config.voiceRecognition).forEach(([key, value]) => {
591
+ if (typeof value === "string") {
592
+ lines.push(` ${key}: "${value}",`);
593
+ } else if (typeof value === "boolean") {
594
+ lines.push(` ${key}: ${value},`);
595
+ } else if (typeof value === "number") {
596
+ lines.push(` ${key}: ${value},`);
597
+ }
598
+ });
599
+ lines.push(" },");
600
+ }
601
+
602
+ if (config.statusIndicator) {
603
+ lines.push(" statusIndicator: {");
604
+ Object.entries(config.statusIndicator).forEach(([key, value]) => {
605
+ if (typeof value === "string") {
606
+ lines.push(` ${key}: "${value}",`);
607
+ } else if (typeof value === "boolean") {
608
+ lines.push(` ${key}: ${value},`);
609
+ }
610
+ });
611
+ lines.push(" },");
612
+ }
613
+
614
+ if (config.features) {
615
+ lines.push(" features: {");
616
+ Object.entries(config.features).forEach(([key, value]) => {
617
+ lines.push(` ${key}: ${value},`);
618
+ });
619
+ lines.push(" },");
620
+ }
621
+
622
+ if (config.suggestionChips && config.suggestionChips.length > 0) {
623
+ lines.push(" suggestionChips: [");
624
+ config.suggestionChips.forEach((chip: string) => {
625
+ lines.push(` "${chip}",`);
626
+ });
627
+ lines.push(" ],");
628
+ }
629
+
630
+ if (config.suggestionChipsConfig) {
631
+ lines.push(" suggestionChipsConfig: {");
632
+ if (config.suggestionChipsConfig.fontFamily) {
633
+ lines.push(` fontFamily: "${config.suggestionChipsConfig.fontFamily}",`);
634
+ }
635
+ if (config.suggestionChipsConfig.fontWeight) {
636
+ lines.push(` fontWeight: "${config.suggestionChipsConfig.fontWeight}",`);
637
+ }
638
+ if (config.suggestionChipsConfig.paddingX) {
639
+ lines.push(` paddingX: "${config.suggestionChipsConfig.paddingX}",`);
640
+ }
641
+ if (config.suggestionChipsConfig.paddingY) {
642
+ lines.push(` paddingY: "${config.suggestionChipsConfig.paddingY}",`);
643
+ }
644
+ lines.push(" },");
645
+ }
646
+
647
+ // Add toolCall config
648
+ lines.push(...generateToolCallConfig(config, " "));
649
+
650
+ // Add messageActions config (with hook callbacks if provided)
651
+ lines.push(...generateMessageActionsConfig(config, " ", hooks));
652
+
653
+ // Add markdown config
654
+ lines.push(...generateMarkdownConfig(config, " "));
655
+
656
+ // Add layout config
657
+ lines.push(...generateLayoutConfig(config, " "));
658
+
659
+ // Add hook-based config (getHeaders, requestMiddleware, actionParsers, actionHandlers, etc.)
660
+ lines.push(...generateHooksConfig(hooks, " "));
661
+
662
+ if (config.debug) {
663
+ lines.push(` debug: ${config.debug},`);
664
+ }
665
+
666
+ // Use custom postprocessMessage if provided, otherwise default
667
+ if (hooks?.postprocessMessage) {
668
+ lines.push(` postprocessMessage: ${hooks.postprocessMessage}`);
669
+ } else {
670
+ lines.push(" postprocessMessage: ({ text }) => markdownPostprocessor(text)");
671
+ }
672
+ lines.push(" }");
673
+ lines.push("});");
674
+
675
+ return lines.join("\n");
676
+ }
677
+
678
+ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions): string {
679
+ const hooks = options?.hooks;
680
+ const parserType = getParserTypeFromConfig(config as AgentWidgetConfig);
681
+ const shouldEmitParserType = parserType !== "plain";
682
+
683
+ const lines: string[] = [
684
+ "// ChatWidget.tsx",
685
+ "'use client'; // Required for Next.js - remove for Vite/CRA",
686
+ "",
687
+ "import { useEffect } from 'react';",
688
+ "import '@runtypelabs/persona/widget.css';",
689
+ "import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';",
690
+ "import type { AgentWidgetInitHandle } from '@runtypelabs/persona';",
691
+ "",
692
+ "export function ChatWidget() {",
693
+ " useEffect(() => {",
694
+ " let handle: AgentWidgetInitHandle | null = null;",
695
+ "",
696
+ " handle = initAgentWidget({",
697
+ " target: 'body',",
698
+ " config: {"
699
+ ];
700
+
701
+ if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
702
+ if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
703
+ if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
704
+ if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
705
+
706
+ if (config.theme) {
707
+ lines.push(" theme: {");
708
+ Object.entries(config.theme).forEach(([key, value]) => {
709
+ lines.push(` ${key}: "${value}",`);
710
+ });
711
+ lines.push(" },");
712
+ }
713
+
714
+ if (config.launcher) {
715
+ lines.push(" launcher: {");
716
+ Object.entries(config.launcher).forEach(([key, value]) => {
717
+ if (typeof value === "string") {
718
+ lines.push(` ${key}: "${value}",`);
719
+ } else if (typeof value === "boolean") {
720
+ lines.push(` ${key}: ${value},`);
721
+ }
722
+ });
723
+ lines.push(" },");
724
+ }
725
+
726
+ if (config.copy) {
727
+ lines.push(" copy: {");
728
+ Object.entries(config.copy).forEach(([key, value]) => {
729
+ lines.push(` ${key}: "${value}",`);
730
+ });
731
+ lines.push(" },");
732
+ }
733
+
734
+ if (config.sendButton) {
735
+ lines.push(" sendButton: {");
736
+ Object.entries(config.sendButton).forEach(([key, value]) => {
737
+ if (typeof value === "string") {
738
+ lines.push(` ${key}: "${value}",`);
739
+ } else if (typeof value === "boolean") {
740
+ lines.push(` ${key}: ${value},`);
741
+ }
742
+ });
743
+ lines.push(" },");
744
+ }
745
+
746
+ if (config.voiceRecognition) {
747
+ lines.push(" voiceRecognition: {");
748
+ Object.entries(config.voiceRecognition).forEach(([key, value]) => {
749
+ if (typeof value === "string") {
750
+ lines.push(` ${key}: "${value}",`);
751
+ } else if (typeof value === "boolean") {
752
+ lines.push(` ${key}: ${value},`);
753
+ } else if (typeof value === "number") {
754
+ lines.push(` ${key}: ${value},`);
755
+ }
756
+ });
757
+ lines.push(" },");
758
+ }
759
+
760
+ if (config.statusIndicator) {
761
+ lines.push(" statusIndicator: {");
762
+ Object.entries(config.statusIndicator).forEach(([key, value]) => {
763
+ if (typeof value === "string") {
764
+ lines.push(` ${key}: "${value}",`);
765
+ } else if (typeof value === "boolean") {
766
+ lines.push(` ${key}: ${value},`);
767
+ }
768
+ });
769
+ lines.push(" },");
770
+ }
771
+
772
+ if (config.features) {
773
+ lines.push(" features: {");
774
+ Object.entries(config.features).forEach(([key, value]) => {
775
+ lines.push(` ${key}: ${value},`);
776
+ });
777
+ lines.push(" },");
778
+ }
779
+
780
+ if (config.suggestionChips && config.suggestionChips.length > 0) {
781
+ lines.push(" suggestionChips: [");
782
+ config.suggestionChips.forEach((chip: string) => {
783
+ lines.push(` "${chip}",`);
784
+ });
785
+ lines.push(" ],");
786
+ }
787
+
788
+ if (config.suggestionChipsConfig) {
789
+ lines.push(" suggestionChipsConfig: {");
790
+ if (config.suggestionChipsConfig.fontFamily) {
791
+ lines.push(` fontFamily: "${config.suggestionChipsConfig.fontFamily}",`);
792
+ }
793
+ if (config.suggestionChipsConfig.fontWeight) {
794
+ lines.push(` fontWeight: "${config.suggestionChipsConfig.fontWeight}",`);
795
+ }
796
+ if (config.suggestionChipsConfig.paddingX) {
797
+ lines.push(` paddingX: "${config.suggestionChipsConfig.paddingX}",`);
798
+ }
799
+ if (config.suggestionChipsConfig.paddingY) {
800
+ lines.push(` paddingY: "${config.suggestionChipsConfig.paddingY}",`);
801
+ }
802
+ lines.push(" },");
803
+ }
804
+
805
+ // Add toolCall config
806
+ lines.push(...generateToolCallConfig(config, " "));
807
+
808
+ // Add messageActions config (with hook callbacks if provided)
809
+ lines.push(...generateMessageActionsConfig(config, " ", hooks));
810
+
811
+ // Add markdown config
812
+ lines.push(...generateMarkdownConfig(config, " "));
813
+
814
+ // Add layout config
815
+ lines.push(...generateLayoutConfig(config, " "));
816
+
817
+ // Add hook-based config (getHeaders, requestMiddleware, actionParsers, actionHandlers, etc.)
818
+ lines.push(...generateHooksConfig(hooks, " "));
819
+
820
+ if (config.debug) {
821
+ lines.push(` debug: ${config.debug},`);
822
+ }
823
+
824
+ // Use custom postprocessMessage if provided, otherwise default
825
+ if (hooks?.postprocessMessage) {
826
+ lines.push(` postprocessMessage: ${hooks.postprocessMessage}`);
827
+ } else {
828
+ lines.push(" postprocessMessage: ({ text }) => markdownPostprocessor(text)");
829
+ }
830
+ lines.push(" }");
831
+ lines.push(" });");
832
+ lines.push("");
833
+ lines.push(" // Cleanup on unmount");
834
+ lines.push(" return () => {");
835
+ lines.push(" if (handle) {");
836
+ lines.push(" handle.destroy();");
837
+ lines.push(" }");
838
+ lines.push(" };");
839
+ lines.push(" }, []);");
840
+ lines.push("");
841
+ lines.push(" return null; // Widget injects itself into the DOM");
842
+ lines.push("}");
843
+ lines.push("");
844
+ lines.push("// Usage in your app:");
845
+ lines.push("// import { ChatWidget } from './components/ChatWidget';");
846
+ lines.push("//");
847
+ lines.push("// export default function App() {");
848
+ lines.push("// return (");
849
+ lines.push("// <div>");
850
+ lines.push("// {/* Your app content */}");
851
+ lines.push("// <ChatWidget />");
852
+ lines.push("// </div>");
853
+ lines.push("// );");
854
+ lines.push("// }");
855
+
856
+ return lines.join("\n");
857
+ }
858
+
859
+ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions): string {
860
+ const hooks = options?.hooks;
861
+ const lines: string[] = [
862
+ "// ChatWidgetAdvanced.tsx",
863
+ "'use client'; // Required for Next.js - remove for Vite/CRA",
864
+ "",
865
+ "import { useEffect } from 'react';",
866
+ "import '@runtypelabs/persona/widget.css';",
867
+ "import {",
868
+ " initAgentWidget,",
869
+ " createFlexibleJsonStreamParser,",
870
+ " defaultJsonActionParser,",
871
+ " defaultActionHandlers,",
872
+ " markdownPostprocessor",
873
+ "} from '@runtypelabs/persona';",
874
+ "import type { AgentWidgetInitHandle } from '@runtypelabs/persona';",
875
+ "",
876
+ "const STORAGE_KEY = 'chat-widget-state';",
877
+ "const PROCESSED_ACTIONS_KEY = 'chat-widget-processed-actions';",
878
+ "",
879
+ "// Types for DOM elements",
880
+ "interface PageElement {",
881
+ " type: string;",
882
+ " tagName: string;",
883
+ " selector: string;",
884
+ " innerText: string;",
885
+ " href?: string;",
886
+ "}",
887
+ "",
888
+ "interface DOMContext {",
889
+ " page_elements: PageElement[];",
890
+ " page_element_count: number;",
891
+ " element_types: Record<string, number>;",
892
+ " page_url: string;",
893
+ " page_title: string;",
894
+ " timestamp: string;",
895
+ "}",
896
+ "",
897
+ "// DOM context provider - extracts page elements for AI context",
898
+ "const collectDOMContext = (): DOMContext => {",
899
+ " const selectors = {",
900
+ " products: '[data-product-id], .product-card, .product-item, [role=\"article\"]',",
901
+ " buttons: 'button, [role=\"button\"], .btn',",
902
+ " links: 'a[href]',",
903
+ " inputs: 'input, textarea, select'",
904
+ " };",
905
+ "",
906
+ " const elements: PageElement[] = [];",
907
+ " Object.entries(selectors).forEach(([type, selector]) => {",
908
+ " document.querySelectorAll(selector).forEach((element) => {",
909
+ " if (!(element instanceof HTMLElement)) return;",
910
+ " ",
911
+ " // Exclude elements within the widget",
912
+ " const widgetHost = element.closest('.persona-host');",
913
+ " if (widgetHost) return;",
914
+ " ",
915
+ " const text = element.innerText?.trim();",
916
+ " if (!text) return;",
917
+ "",
918
+ " const selectorString =",
919
+ " element.id ? `#${element.id}` :",
920
+ " element.getAttribute('data-testid') ? `[data-testid=\"${element.getAttribute('data-testid')}\"]` :",
921
+ " element.getAttribute('data-product-id') ? `[data-product-id=\"${element.getAttribute('data-product-id')}\"]` :",
922
+ " element.tagName.toLowerCase();",
923
+ "",
924
+ " const elementData: PageElement = {",
925
+ " type,",
926
+ " tagName: element.tagName.toLowerCase(),",
927
+ " selector: selectorString,",
928
+ " innerText: text.substring(0, 200)",
929
+ " };",
930
+ "",
931
+ " if (type === 'links' && element instanceof HTMLAnchorElement && element.href) {",
932
+ " elementData.href = element.href;",
933
+ " }",
934
+ "",
935
+ " elements.push(elementData);",
936
+ " });",
937
+ " });",
938
+ "",
939
+ " const counts = elements.reduce((acc, el) => {",
940
+ " acc[el.type] = (acc[el.type] || 0) + 1;",
941
+ " return acc;",
942
+ " }, {} as Record<string, number>);",
943
+ "",
944
+ " return {",
945
+ " page_elements: elements.slice(0, 50),",
946
+ " page_element_count: elements.length,",
947
+ " element_types: counts,",
948
+ " page_url: window.location.href,",
949
+ " page_title: document.title,",
950
+ " timestamp: new Date().toISOString()",
951
+ " };",
952
+ "};",
953
+ "",
954
+ "export function ChatWidgetAdvanced() {",
955
+ " useEffect(() => {",
956
+ " let handle: AgentWidgetInitHandle | null = null;",
957
+ "",
958
+ " // Load saved state",
959
+ " const loadSavedMessages = () => {",
960
+ " const savedState = localStorage.getItem(STORAGE_KEY);",
961
+ " if (savedState) {",
962
+ " try {",
963
+ " const { messages } = JSON.parse(savedState);",
964
+ " return messages || [];",
965
+ " } catch (e) {",
966
+ " console.error('Failed to load saved state:', e);",
967
+ " }",
968
+ " }",
969
+ " return [];",
970
+ " };",
971
+ "",
972
+ " handle = initAgentWidget({",
973
+ " target: 'body',",
974
+ " config: {"
975
+ ];
976
+
977
+ if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
978
+ if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
979
+ if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
980
+
981
+ if (config.theme) {
982
+ lines.push(" theme: {");
983
+ Object.entries(config.theme).forEach(([key, value]) => {
984
+ lines.push(` ${key}: "${value}",`);
985
+ });
986
+ lines.push(" },");
987
+ }
988
+
989
+ if (config.launcher) {
990
+ lines.push(" launcher: {");
991
+ Object.entries(config.launcher).forEach(([key, value]) => {
992
+ if (typeof value === "string") {
993
+ lines.push(` ${key}: "${value}",`);
994
+ } else if (typeof value === "boolean") {
995
+ lines.push(` ${key}: ${value},`);
996
+ }
997
+ });
998
+ lines.push(" },");
999
+ }
1000
+
1001
+ if (config.copy) {
1002
+ lines.push(" copy: {");
1003
+ Object.entries(config.copy).forEach(([key, value]) => {
1004
+ lines.push(` ${key}: "${value}",`);
1005
+ });
1006
+ lines.push(" },");
1007
+ }
1008
+
1009
+ if (config.sendButton) {
1010
+ lines.push(" sendButton: {");
1011
+ Object.entries(config.sendButton).forEach(([key, value]) => {
1012
+ if (typeof value === "string") {
1013
+ lines.push(` ${key}: "${value}",`);
1014
+ } else if (typeof value === "boolean") {
1015
+ lines.push(` ${key}: ${value},`);
1016
+ }
1017
+ });
1018
+ lines.push(" },");
1019
+ }
1020
+
1021
+ if (config.voiceRecognition) {
1022
+ lines.push(" voiceRecognition: {");
1023
+ Object.entries(config.voiceRecognition).forEach(([key, value]) => {
1024
+ if (typeof value === "string") {
1025
+ lines.push(` ${key}: "${value}",`);
1026
+ } else if (typeof value === "boolean") {
1027
+ lines.push(` ${key}: ${value},`);
1028
+ } else if (typeof value === "number") {
1029
+ lines.push(` ${key}: ${value},`);
1030
+ }
1031
+ });
1032
+ lines.push(" },");
1033
+ }
1034
+
1035
+ if (config.statusIndicator) {
1036
+ lines.push(" statusIndicator: {");
1037
+ Object.entries(config.statusIndicator).forEach(([key, value]) => {
1038
+ if (typeof value === "string") {
1039
+ lines.push(` ${key}: "${value}",`);
1040
+ } else if (typeof value === "boolean") {
1041
+ lines.push(` ${key}: ${value},`);
1042
+ }
1043
+ });
1044
+ lines.push(" },");
1045
+ }
1046
+
1047
+ if (config.features) {
1048
+ lines.push(" features: {");
1049
+ Object.entries(config.features).forEach(([key, value]) => {
1050
+ lines.push(` ${key}: ${value},`);
1051
+ });
1052
+ lines.push(" },");
1053
+ }
1054
+
1055
+ if (config.suggestionChips && config.suggestionChips.length > 0) {
1056
+ lines.push(" suggestionChips: [");
1057
+ config.suggestionChips.forEach((chip: string) => {
1058
+ lines.push(` "${chip}",`);
1059
+ });
1060
+ lines.push(" ],");
1061
+ }
1062
+
1063
+ if (config.suggestionChipsConfig) {
1064
+ lines.push(" suggestionChipsConfig: {");
1065
+ if (config.suggestionChipsConfig.fontFamily) {
1066
+ lines.push(` fontFamily: "${config.suggestionChipsConfig.fontFamily}",`);
1067
+ }
1068
+ if (config.suggestionChipsConfig.fontWeight) {
1069
+ lines.push(` fontWeight: "${config.suggestionChipsConfig.fontWeight}",`);
1070
+ }
1071
+ if (config.suggestionChipsConfig.paddingX) {
1072
+ lines.push(` paddingX: "${config.suggestionChipsConfig.paddingX}",`);
1073
+ }
1074
+ if (config.suggestionChipsConfig.paddingY) {
1075
+ lines.push(` paddingY: "${config.suggestionChipsConfig.paddingY}",`);
1076
+ }
1077
+ lines.push(" },");
1078
+ }
1079
+
1080
+ // Add toolCall config
1081
+ lines.push(...generateToolCallConfig(config, " "));
1082
+
1083
+ // Add messageActions config (with hook callbacks if provided)
1084
+ lines.push(...generateMessageActionsConfig(config, " ", hooks));
1085
+
1086
+ // Add markdown config
1087
+ lines.push(...generateMarkdownConfig(config, " "));
1088
+
1089
+ // Add layout config
1090
+ lines.push(...generateLayoutConfig(config, " "));
1091
+
1092
+ // Add getHeaders if provided
1093
+ if (hooks?.getHeaders) {
1094
+ lines.push(` getHeaders: ${hooks.getHeaders},`);
1095
+ }
1096
+
1097
+ // Add contextProviders if provided
1098
+ if (hooks?.contextProviders) {
1099
+ lines.push(` contextProviders: ${hooks.contextProviders},`);
1100
+ }
1101
+
1102
+ if (config.debug) {
1103
+ lines.push(` debug: ${config.debug},`);
1104
+ }
1105
+
1106
+ lines.push(" initialMessages: loadSavedMessages(),");
1107
+
1108
+ // Stream parser - use custom if provided, otherwise default
1109
+ if (hooks?.streamParser) {
1110
+ lines.push(` streamParser: ${hooks.streamParser},`);
1111
+ } else {
1112
+ lines.push(" // Flexible JSON stream parser for handling structured actions");
1113
+ lines.push(` streamParser: () => createFlexibleJsonStreamParser(${TEMPLATE_STREAM_PARSER_CALLBACK_TS}),`);
1114
+ }
1115
+
1116
+ // Action parsers - merge custom with defaults if provided
1117
+ if (hooks?.actionParsers) {
1118
+ lines.push(" // Action parsers (custom merged with defaults)");
1119
+ lines.push(` actionParsers: [...(${hooks.actionParsers}), defaultJsonActionParser,`);
1120
+ lines.push(` // Built-in parser for markdown-wrapped JSON`);
1121
+ lines.push(` ${TEMPLATE_MARKDOWN_JSON_PARSER_TS}`);
1122
+ lines.push(" ],");
1123
+ } else {
1124
+ lines.push(" // Action parsers to detect JSON actions in responses");
1125
+ lines.push(" actionParsers: [");
1126
+ lines.push(" defaultJsonActionParser,");
1127
+ lines.push(` // Parser for markdown-wrapped JSON`);
1128
+ lines.push(` ${TEMPLATE_MARKDOWN_JSON_PARSER_TS}`);
1129
+ lines.push(" ],");
1130
+ }
1131
+
1132
+ // Action handlers - merge custom with defaults if provided
1133
+ if (hooks?.actionHandlers) {
1134
+ lines.push(" // Action handlers (custom merged with defaults)");
1135
+ lines.push(` actionHandlers: [...(${hooks.actionHandlers}),`);
1136
+ lines.push(" defaultActionHandlers.message,");
1137
+ lines.push(" defaultActionHandlers.messageAndClick,");
1138
+ lines.push(` // Built-in handler for nav_then_click action`);
1139
+ lines.push(` ${TEMPLATE_NAV_THEN_CLICK_HANDLER_TS}`);
1140
+ lines.push(" ],");
1141
+ } else {
1142
+ lines.push(" // Action handlers for navigation and other actions");
1143
+ lines.push(" actionHandlers: [");
1144
+ lines.push(" defaultActionHandlers.message,");
1145
+ lines.push(" defaultActionHandlers.messageAndClick,");
1146
+ lines.push(` // Handler for nav_then_click action`);
1147
+ lines.push(` ${TEMPLATE_NAV_THEN_CLICK_HANDLER_TS}`);
1148
+ lines.push(" ],");
1149
+ }
1150
+
1151
+ // postprocessMessage - use custom if provided, otherwise default
1152
+ if (hooks?.postprocessMessage) {
1153
+ lines.push(` postprocessMessage: ${hooks.postprocessMessage},`);
1154
+ } else {
1155
+ lines.push(" postprocessMessage: ({ text }) => markdownPostprocessor(text),");
1156
+ }
1157
+
1158
+ // requestMiddleware - merge custom with DOM context if provided
1159
+ if (hooks?.requestMiddleware) {
1160
+ lines.push(" // Request middleware (custom merged with DOM context)");
1161
+ lines.push(" requestMiddleware: ({ payload, config }) => {");
1162
+ lines.push(` const customResult = (${hooks.requestMiddleware})({ payload, config });`);
1163
+ lines.push(" const merged = customResult || payload;");
1164
+ lines.push(" return {");
1165
+ lines.push(" ...merged,");
1166
+ lines.push(" metadata: { ...merged.metadata, ...collectDOMContext() }");
1167
+ lines.push(" };");
1168
+ lines.push(" }");
1169
+ } else {
1170
+ lines.push(" requestMiddleware: ({ payload }) => {");
1171
+ lines.push(" return {");
1172
+ lines.push(" ...payload,");
1173
+ lines.push(" metadata: collectDOMContext()");
1174
+ lines.push(" };");
1175
+ lines.push(" }");
1176
+ }
1177
+ lines.push(" }");
1178
+ lines.push(" });");
1179
+ lines.push("");
1180
+ lines.push(" // Save state on message events");
1181
+ lines.push(" const handleMessage = () => {");
1182
+ lines.push(" const session = handle?.getSession?.();");
1183
+ lines.push(" if (session) {");
1184
+ lines.push(" localStorage.setItem(STORAGE_KEY, JSON.stringify({");
1185
+ lines.push(" messages: session.messages,");
1186
+ lines.push(" timestamp: new Date().toISOString()");
1187
+ lines.push(" }));");
1188
+ lines.push(" }");
1189
+ lines.push(" };");
1190
+ lines.push("");
1191
+ lines.push(" // Clear state on clear chat");
1192
+ lines.push(" const handleClearChat = () => {");
1193
+ lines.push(" localStorage.removeItem(STORAGE_KEY);");
1194
+ lines.push(" localStorage.removeItem(PROCESSED_ACTIONS_KEY);");
1195
+ lines.push(" };");
1196
+ lines.push("");
1197
+ lines.push(" window.addEventListener('persona:message', handleMessage);");
1198
+ lines.push(" window.addEventListener('persona:clear-chat', handleClearChat);");
1199
+ lines.push("");
1200
+ lines.push(" // Cleanup on unmount");
1201
+ lines.push(" return () => {");
1202
+ lines.push(" window.removeEventListener('persona:message', handleMessage);");
1203
+ lines.push(" window.removeEventListener('persona:clear-chat', handleClearChat);");
1204
+ lines.push(" if (handle) {");
1205
+ lines.push(" handle.destroy();");
1206
+ lines.push(" }");
1207
+ lines.push(" };");
1208
+ lines.push(" }, []);");
1209
+ lines.push("");
1210
+ lines.push(" return null; // Widget injects itself into the DOM");
1211
+ lines.push("}");
1212
+ lines.push("");
1213
+ lines.push("// Usage: Collects DOM context for AI-powered navigation");
1214
+ lines.push("// Features:");
1215
+ lines.push("// - Extracts page elements (products, buttons, links)");
1216
+ lines.push("// - Persists chat history across page loads");
1217
+ lines.push("// - Handles navigation actions (nav_then_click)");
1218
+ lines.push("// - Processes structured JSON actions from AI");
1219
+ lines.push("//");
1220
+ lines.push("// Example usage in Next.js:");
1221
+ lines.push("// import { ChatWidgetAdvanced } from './components/ChatWidgetAdvanced';");
1222
+ lines.push("//");
1223
+ lines.push("// export default function RootLayout({ children }) {");
1224
+ lines.push("// return (");
1225
+ lines.push("// <html lang=\"en\">");
1226
+ lines.push("// <body>");
1227
+ lines.push("// {children}");
1228
+ lines.push("// <ChatWidgetAdvanced />");
1229
+ lines.push("// </body>");
1230
+ lines.push("// </html>");
1231
+ lines.push("// );");
1232
+ lines.push("// }");
1233
+
1234
+ return lines.join("\n");
1235
+ }
1236
+
1237
+ // Helper to build a serializable config object for JSON export
1238
+ function buildSerializableConfig(config: any): Record<string, any> {
1239
+ const parserType = getParserTypeFromConfig(config as AgentWidgetConfig);
1240
+ const shouldEmitParserType = parserType !== "plain";
1241
+
1242
+ const serializableConfig: Record<string, any> = {};
1243
+
1244
+ if (config.apiUrl) serializableConfig.apiUrl = config.apiUrl;
1245
+ if (config.clientToken) serializableConfig.clientToken = config.clientToken;
1246
+ if (config.flowId) serializableConfig.flowId = config.flowId;
1247
+ if (shouldEmitParserType) serializableConfig.parserType = parserType;
1248
+ if (config.theme) serializableConfig.theme = config.theme;
1249
+ if (config.launcher) serializableConfig.launcher = config.launcher;
1250
+ if (config.copy) serializableConfig.copy = config.copy;
1251
+ if (config.sendButton) serializableConfig.sendButton = config.sendButton;
1252
+ if (config.voiceRecognition) serializableConfig.voiceRecognition = config.voiceRecognition;
1253
+ if (config.statusIndicator) serializableConfig.statusIndicator = config.statusIndicator;
1254
+ if (config.features) serializableConfig.features = config.features;
1255
+ if (config.suggestionChips?.length > 0) serializableConfig.suggestionChips = config.suggestionChips;
1256
+ if (config.suggestionChipsConfig) serializableConfig.suggestionChipsConfig = config.suggestionChipsConfig;
1257
+ if (config.debug) serializableConfig.debug = config.debug;
1258
+
1259
+ // Add toolCall config (only serializable parts)
1260
+ if (config.toolCall) {
1261
+ const toolCallConfig: Record<string, any> = {};
1262
+ Object.entries(config.toolCall).forEach(([key, value]) => {
1263
+ if (typeof value === "string") toolCallConfig[key] = value;
1264
+ });
1265
+ if (Object.keys(toolCallConfig).length > 0) {
1266
+ serializableConfig.toolCall = toolCallConfig;
1267
+ }
1268
+ }
1269
+
1270
+ // Add messageActions config (excluding callbacks)
1271
+ if (config.messageActions) {
1272
+ const messageActionsConfig: Record<string, any> = {};
1273
+ Object.entries(config.messageActions).forEach(([key, value]) => {
1274
+ if (key !== "onFeedback" && key !== "onCopy" && value !== undefined) {
1275
+ if (typeof value === "string" || typeof value === "boolean") {
1276
+ messageActionsConfig[key] = value;
1277
+ }
1278
+ }
1279
+ });
1280
+ if (Object.keys(messageActionsConfig).length > 0) {
1281
+ serializableConfig.messageActions = messageActionsConfig;
1282
+ }
1283
+ }
1284
+
1285
+ // Add markdown config (excluding renderer functions)
1286
+ if (config.markdown) {
1287
+ const markdownConfig: Record<string, any> = {};
1288
+ if (config.markdown.options) markdownConfig.options = config.markdown.options;
1289
+ if (config.markdown.disableDefaultStyles !== undefined) {
1290
+ markdownConfig.disableDefaultStyles = config.markdown.disableDefaultStyles;
1291
+ }
1292
+ if (Object.keys(markdownConfig).length > 0) {
1293
+ serializableConfig.markdown = markdownConfig;
1294
+ }
1295
+ }
1296
+
1297
+ // Add layout config (excluding render functions)
1298
+ if (config.layout) {
1299
+ const layoutConfig: Record<string, any> = {};
1300
+
1301
+ if (config.layout.header) {
1302
+ const headerConfig: Record<string, any> = {};
1303
+ Object.entries(config.layout.header).forEach(([key, value]) => {
1304
+ if (key !== "render" && (typeof value === "string" || typeof value === "boolean")) {
1305
+ headerConfig[key] = value;
1306
+ }
1307
+ });
1308
+ if (Object.keys(headerConfig).length > 0) {
1309
+ layoutConfig.header = headerConfig;
1310
+ }
1311
+ }
1312
+
1313
+ if (config.layout.messages) {
1314
+ const messagesConfig: Record<string, any> = {};
1315
+ Object.entries(config.layout.messages).forEach(([key, value]) => {
1316
+ if (key !== "renderUserMessage" && key !== "renderAssistantMessage") {
1317
+ if (key === "avatar" && typeof value === "object" && value !== null) {
1318
+ messagesConfig.avatar = value;
1319
+ } else if (key === "timestamp" && typeof value === "object" && value !== null) {
1320
+ // Exclude format function
1321
+ const tsConfig: Record<string, any> = {};
1322
+ Object.entries(value as Record<string, unknown>).forEach(([tsKey, tsValue]) => {
1323
+ if (tsKey !== "format" && (typeof tsValue === "string" || typeof tsValue === "boolean")) {
1324
+ tsConfig[tsKey] = tsValue;
1325
+ }
1326
+ });
1327
+ if (Object.keys(tsConfig).length > 0) {
1328
+ messagesConfig.timestamp = tsConfig;
1329
+ }
1330
+ } else if (typeof value === "string" || typeof value === "boolean") {
1331
+ messagesConfig[key] = value;
1332
+ }
1333
+ }
1334
+ });
1335
+ if (Object.keys(messagesConfig).length > 0) {
1336
+ layoutConfig.messages = messagesConfig;
1337
+ }
1338
+ }
1339
+
1340
+ if (Object.keys(layoutConfig).length > 0) {
1341
+ serializableConfig.layout = layoutConfig;
1342
+ }
1343
+ }
1344
+
1345
+ return serializableConfig;
1346
+ }
1347
+
1348
+ function generateScriptInstallerCode(config: any): string {
1349
+ const serializableConfig = buildSerializableConfig(config);
1350
+
1351
+ // Escape single quotes in JSON for HTML attribute
1352
+ const configJson = JSON.stringify(serializableConfig, null, 0).replace(/'/g, "&#39;");
1353
+
1354
+ return `<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js" data-config='${configJson}'></script>`;
1355
+ }
1356
+
1357
+ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions): string {
1358
+ const hooks = options?.hooks;
1359
+ const parserType = getParserTypeFromConfig(config as AgentWidgetConfig);
1360
+ const shouldEmitParserType = parserType !== "plain";
1361
+
1362
+ const lines: string[] = [
1363
+ "<!-- Load CSS -->",
1364
+ "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/widget.css\" />",
1365
+ "",
1366
+ "<!-- Load JavaScript -->",
1367
+ "<script src=\"https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/index.global.js\"></script>",
1368
+ "",
1369
+ "<!-- Initialize widget -->",
1370
+ "<script>",
1371
+ " window.AgentWidget.initAgentWidget({",
1372
+ " target: 'body',",
1373
+ " config: {"
1374
+ ];
1375
+
1376
+ if (config.apiUrl) lines.push(` apiUrl: "${config.apiUrl}",`);
1377
+ if (config.clientToken) lines.push(` clientToken: "${config.clientToken}",`);
1378
+ if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
1379
+ if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
1380
+
1381
+ if (config.theme) {
1382
+ lines.push(" theme: {");
1383
+ Object.entries(config.theme).forEach(([key, value]) => {
1384
+ lines.push(` ${key}: "${value}",`);
1385
+ });
1386
+ lines.push(" },");
1387
+ }
1388
+
1389
+ if (config.launcher) {
1390
+ lines.push(" launcher: {");
1391
+ Object.entries(config.launcher).forEach(([key, value]) => {
1392
+ if (typeof value === "string") {
1393
+ lines.push(` ${key}: "${value}",`);
1394
+ } else if (typeof value === "boolean") {
1395
+ lines.push(` ${key}: ${value},`);
1396
+ }
1397
+ });
1398
+ lines.push(" },");
1399
+ }
1400
+
1401
+ if (config.copy) {
1402
+ lines.push(" copy: {");
1403
+ Object.entries(config.copy).forEach(([key, value]) => {
1404
+ lines.push(` ${key}: "${value}",`);
1405
+ });
1406
+ lines.push(" },");
1407
+ }
1408
+
1409
+ if (config.sendButton) {
1410
+ lines.push(" sendButton: {");
1411
+ Object.entries(config.sendButton).forEach(([key, value]) => {
1412
+ if (typeof value === "string") {
1413
+ lines.push(` ${key}: "${value}",`);
1414
+ } else if (typeof value === "boolean") {
1415
+ lines.push(` ${key}: ${value},`);
1416
+ }
1417
+ });
1418
+ lines.push(" },");
1419
+ }
1420
+
1421
+ if (config.voiceRecognition) {
1422
+ lines.push(" voiceRecognition: {");
1423
+ Object.entries(config.voiceRecognition).forEach(([key, value]) => {
1424
+ if (typeof value === "string") {
1425
+ lines.push(` ${key}: "${value}",`);
1426
+ } else if (typeof value === "boolean") {
1427
+ lines.push(` ${key}: ${value},`);
1428
+ } else if (typeof value === "number") {
1429
+ lines.push(` ${key}: ${value},`);
1430
+ }
1431
+ });
1432
+ lines.push(" },");
1433
+ }
1434
+
1435
+ if (config.statusIndicator) {
1436
+ lines.push(" statusIndicator: {");
1437
+ Object.entries(config.statusIndicator).forEach(([key, value]) => {
1438
+ if (typeof value === "string") {
1439
+ lines.push(` ${key}: "${value}",`);
1440
+ } else if (typeof value === "boolean") {
1441
+ lines.push(` ${key}: ${value},`);
1442
+ }
1443
+ });
1444
+ lines.push(" },");
1445
+ }
1446
+
1447
+ if (config.features) {
1448
+ lines.push(" features: {");
1449
+ Object.entries(config.features).forEach(([key, value]) => {
1450
+ lines.push(` ${key}: ${value},`);
1451
+ });
1452
+ lines.push(" },");
1453
+ }
1454
+
1455
+ if (config.suggestionChips && config.suggestionChips.length > 0) {
1456
+ lines.push(" suggestionChips: [");
1457
+ config.suggestionChips.forEach((chip: string) => {
1458
+ lines.push(` "${chip}",`);
1459
+ });
1460
+ lines.push(" ],");
1461
+ }
1462
+
1463
+ if (config.suggestionChipsConfig) {
1464
+ lines.push(" suggestionChipsConfig: {");
1465
+ if (config.suggestionChipsConfig.fontFamily) {
1466
+ lines.push(` fontFamily: "${config.suggestionChipsConfig.fontFamily}",`);
1467
+ }
1468
+ if (config.suggestionChipsConfig.fontWeight) {
1469
+ lines.push(` fontWeight: "${config.suggestionChipsConfig.fontWeight}",`);
1470
+ }
1471
+ if (config.suggestionChipsConfig.paddingX) {
1472
+ lines.push(` paddingX: "${config.suggestionChipsConfig.paddingX}",`);
1473
+ }
1474
+ if (config.suggestionChipsConfig.paddingY) {
1475
+ lines.push(` paddingY: "${config.suggestionChipsConfig.paddingY}",`);
1476
+ }
1477
+ lines.push(" },");
1478
+ }
1479
+
1480
+ // Add toolCall config
1481
+ lines.push(...generateToolCallConfig(config, " "));
1482
+
1483
+ // Add messageActions config (with hook callbacks if provided)
1484
+ lines.push(...generateMessageActionsConfig(config, " ", hooks));
1485
+
1486
+ // Add markdown config
1487
+ lines.push(...generateMarkdownConfig(config, " "));
1488
+
1489
+ // Add layout config
1490
+ lines.push(...generateLayoutConfig(config, " "));
1491
+
1492
+ // Add hook-based config (getHeaders, requestMiddleware, actionParsers, actionHandlers, etc.)
1493
+ lines.push(...generateHooksConfig(hooks, " "));
1494
+
1495
+ if (config.debug) {
1496
+ lines.push(` debug: ${config.debug},`);
1497
+ }
1498
+
1499
+ // Use custom postprocessMessage if provided, otherwise default
1500
+ if (hooks?.postprocessMessage) {
1501
+ lines.push(` postprocessMessage: ${hooks.postprocessMessage}`);
1502
+ } else {
1503
+ lines.push(" postprocessMessage: ({ text }) => window.AgentWidget.markdownPostprocessor(text)");
1504
+ }
1505
+ lines.push(" }");
1506
+ lines.push(" });");
1507
+ lines.push("</script>");
1508
+
1509
+ return lines.join("\n");
1510
+ }
1511
+
1512
+ function generateScriptAdvancedCode(config: any, options?: CodeGeneratorOptions): string {
1513
+ const hooks = options?.hooks;
1514
+ const serializableConfig = buildSerializableConfig(config);
1515
+ const configJson = JSON.stringify(serializableConfig, null, 2);
1516
+
1517
+ const lines: string[] = [
1518
+ "<script>",
1519
+ "(function() {",
1520
+ " 'use strict';",
1521
+ "",
1522
+ " // Configuration",
1523
+ ` var CONFIG = ${configJson.split('\n').map((line, i) => i === 0 ? line : ' ' + line).join('\n')};`,
1524
+ "",
1525
+ " // Constants",
1526
+ " var CDN_BASE = 'https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist';",
1527
+ " var STORAGE_KEY = 'chat-widget-state';",
1528
+ " var PROCESSED_ACTIONS_KEY = 'chat-widget-processed-actions';",
1529
+ "",
1530
+ " // DOM context provider - extracts page elements for AI context",
1531
+ " var domContextProvider = function() {",
1532
+ " var selectors = {",
1533
+ " products: '[data-product-id], .product-card, .product-item, [role=\"article\"]',",
1534
+ " buttons: 'button, [role=\"button\"], .btn',",
1535
+ " links: 'a[href]',",
1536
+ " inputs: 'input, textarea, select'",
1537
+ " };",
1538
+ "",
1539
+ " var elements = [];",
1540
+ " Object.entries(selectors).forEach(function(entry) {",
1541
+ " var type = entry[0], selector = entry[1];",
1542
+ " document.querySelectorAll(selector).forEach(function(element) {",
1543
+ " if (!(element instanceof HTMLElement)) return;",
1544
+ " var widgetHost = element.closest('.persona-host');",
1545
+ " if (widgetHost) return;",
1546
+ " var text = element.innerText ? element.innerText.trim() : '';",
1547
+ " if (!text) return;",
1548
+ "",
1549
+ " var selectorString = element.id ? '#' + element.id :",
1550
+ " element.getAttribute('data-testid') ? '[data-testid=\"' + element.getAttribute('data-testid') + '\"]' :",
1551
+ " element.getAttribute('data-product-id') ? '[data-product-id=\"' + element.getAttribute('data-product-id') + '\"]' :",
1552
+ " element.tagName.toLowerCase();",
1553
+ "",
1554
+ " var elementData = {",
1555
+ " type: type,",
1556
+ " tagName: element.tagName.toLowerCase(),",
1557
+ " selector: selectorString,",
1558
+ " innerText: text.substring(0, 200)",
1559
+ " };",
1560
+ "",
1561
+ " if (type === 'links' && element instanceof HTMLAnchorElement && element.href) {",
1562
+ " elementData.href = element.href;",
1563
+ " }",
1564
+ " elements.push(elementData);",
1565
+ " });",
1566
+ " });",
1567
+ "",
1568
+ " var counts = elements.reduce(function(acc, el) {",
1569
+ " acc[el.type] = (acc[el.type] || 0) + 1;",
1570
+ " return acc;",
1571
+ " }, {});",
1572
+ "",
1573
+ " return {",
1574
+ " page_elements: elements.slice(0, 50),",
1575
+ " page_element_count: elements.length,",
1576
+ " element_types: counts,",
1577
+ " page_url: window.location.href,",
1578
+ " page_title: document.title,",
1579
+ " timestamp: new Date().toISOString()",
1580
+ " };",
1581
+ " };",
1582
+ "",
1583
+ " // Load CSS dynamically",
1584
+ " var loadCSS = function() {",
1585
+ " if (document.querySelector('link[data-persona]')) return;",
1586
+ " var link = document.createElement('link');",
1587
+ " link.rel = 'stylesheet';",
1588
+ " link.href = CDN_BASE + '/widget.css';",
1589
+ " link.setAttribute('data-persona', 'true');",
1590
+ " document.head.appendChild(link);",
1591
+ " };",
1592
+ "",
1593
+ " // Load JS dynamically",
1594
+ " var loadJS = function(callback) {",
1595
+ " if (window.AgentWidget) { callback(); return; }",
1596
+ " var script = document.createElement('script');",
1597
+ " script.src = CDN_BASE + '/index.global.js';",
1598
+ " script.onload = callback;",
1599
+ " script.onerror = function() { console.error('Failed to load AgentWidget'); };",
1600
+ " document.head.appendChild(script);",
1601
+ " };",
1602
+ "",
1603
+ " // Create widget config with advanced features",
1604
+ " var createWidgetConfig = function(agentWidget) {",
1605
+ " var widgetConfig = Object.assign({}, CONFIG);",
1606
+ ""
1607
+ ];
1608
+
1609
+ // Add getHeaders if provided
1610
+ if (hooks?.getHeaders) {
1611
+ lines.push(` widgetConfig.getHeaders = ${hooks.getHeaders};`);
1612
+ lines.push("");
1613
+ }
1614
+
1615
+ // Add contextProviders if provided
1616
+ if (hooks?.contextProviders) {
1617
+ lines.push(` widgetConfig.contextProviders = ${hooks.contextProviders};`);
1618
+ lines.push("");
1619
+ }
1620
+
1621
+ // Stream parser - use custom if provided, otherwise default
1622
+ if (hooks?.streamParser) {
1623
+ lines.push(` widgetConfig.streamParser = ${hooks.streamParser};`);
1624
+ } else {
1625
+ lines.push(" // Flexible JSON stream parser for handling structured actions");
1626
+ lines.push(" widgetConfig.streamParser = function() {");
1627
+ lines.push(` return agentWidget.createFlexibleJsonStreamParser(${TEMPLATE_STREAM_PARSER_CALLBACK_ES5});`);
1628
+ lines.push(" };");
1629
+ }
1630
+ lines.push("");
1631
+
1632
+ // Action parsers - merge custom with defaults if provided
1633
+ if (hooks?.actionParsers) {
1634
+ lines.push(" // Action parsers (custom merged with defaults)");
1635
+ lines.push(` var customParsers = ${hooks.actionParsers};`);
1636
+ lines.push(" widgetConfig.actionParsers = customParsers.concat([");
1637
+ lines.push(" agentWidget.defaultJsonActionParser,");
1638
+ lines.push(` ${TEMPLATE_MARKDOWN_JSON_PARSER_ES5}`);
1639
+ lines.push(" ]);");
1640
+ } else {
1641
+ lines.push(" // Action parsers to detect JSON actions in responses");
1642
+ lines.push(" widgetConfig.actionParsers = [");
1643
+ lines.push(" agentWidget.defaultJsonActionParser,");
1644
+ lines.push(` ${TEMPLATE_MARKDOWN_JSON_PARSER_ES5}`);
1645
+ lines.push(" ];");
1646
+ }
1647
+ lines.push("");
1648
+
1649
+ // Action handlers - merge custom with defaults if provided
1650
+ if (hooks?.actionHandlers) {
1651
+ lines.push(" // Action handlers (custom merged with defaults)");
1652
+ lines.push(` var customHandlers = ${hooks.actionHandlers};`);
1653
+ lines.push(" widgetConfig.actionHandlers = customHandlers.concat([");
1654
+ lines.push(" agentWidget.defaultActionHandlers.message,");
1655
+ lines.push(" agentWidget.defaultActionHandlers.messageAndClick,");
1656
+ lines.push(` ${TEMPLATE_NAV_THEN_CLICK_HANDLER_ES5}`);
1657
+ lines.push(" ]);");
1658
+ } else {
1659
+ lines.push(" // Action handlers for navigation and other actions");
1660
+ lines.push(" widgetConfig.actionHandlers = [");
1661
+ lines.push(" agentWidget.defaultActionHandlers.message,");
1662
+ lines.push(" agentWidget.defaultActionHandlers.messageAndClick,");
1663
+ lines.push(` ${TEMPLATE_NAV_THEN_CLICK_HANDLER_ES5}`);
1664
+ lines.push(" ];");
1665
+ }
1666
+ lines.push("");
1667
+
1668
+ // requestMiddleware - merge custom with DOM context if provided
1669
+ if (hooks?.requestMiddleware) {
1670
+ lines.push(" // Request middleware (custom merged with DOM context)");
1671
+ lines.push(" widgetConfig.requestMiddleware = function(ctx) {");
1672
+ lines.push(` var customResult = (${hooks.requestMiddleware})(ctx);`);
1673
+ lines.push(" var merged = customResult || ctx.payload;");
1674
+ lines.push(" return Object.assign({}, merged, { metadata: Object.assign({}, merged.metadata, domContextProvider()) });");
1675
+ lines.push(" };");
1676
+ } else {
1677
+ lines.push(" // Send DOM context with each request");
1678
+ lines.push(" widgetConfig.requestMiddleware = function(ctx) {");
1679
+ lines.push(" return Object.assign({}, ctx.payload, { metadata: domContextProvider() });");
1680
+ lines.push(" };");
1681
+ }
1682
+ lines.push("");
1683
+
1684
+ // postprocessMessage - use custom if provided, otherwise default
1685
+ if (hooks?.postprocessMessage) {
1686
+ lines.push(` widgetConfig.postprocessMessage = ${hooks.postprocessMessage};`);
1687
+ } else {
1688
+ lines.push(" // Markdown postprocessor");
1689
+ lines.push(" widgetConfig.postprocessMessage = function(ctx) {");
1690
+ lines.push(" return agentWidget.markdownPostprocessor(ctx.text);");
1691
+ lines.push(" };");
1692
+ }
1693
+ lines.push("");
1694
+
1695
+ // Add messageActions callbacks if provided
1696
+ if (hooks?.onFeedback || hooks?.onCopy) {
1697
+ lines.push(" // Message action callbacks");
1698
+ lines.push(" widgetConfig.messageActions = widgetConfig.messageActions || {};");
1699
+ if (hooks?.onFeedback) {
1700
+ lines.push(` widgetConfig.messageActions.onFeedback = ${hooks.onFeedback};`);
1701
+ }
1702
+ if (hooks?.onCopy) {
1703
+ lines.push(` widgetConfig.messageActions.onCopy = ${hooks.onCopy};`);
1704
+ }
1705
+ lines.push("");
1706
+ }
1707
+
1708
+ lines.push(...[
1709
+ " return widgetConfig;",
1710
+ " };",
1711
+ "",
1712
+ " // Initialize widget",
1713
+ " var init = function() {",
1714
+ " var agentWidget = window.AgentWidget;",
1715
+ " if (!agentWidget) {",
1716
+ " console.error('AgentWidget not loaded');",
1717
+ " return;",
1718
+ " }",
1719
+ "",
1720
+ " var widgetConfig = createWidgetConfig(agentWidget);",
1721
+ "",
1722
+ " // Load saved state",
1723
+ " var savedState = localStorage.getItem(STORAGE_KEY);",
1724
+ " if (savedState) {",
1725
+ " try {",
1726
+ " var parsed = JSON.parse(savedState);",
1727
+ " widgetConfig.initialMessages = parsed.messages || [];",
1728
+ " } catch (e) {",
1729
+ " console.error('Failed to load saved state:', e);",
1730
+ " }",
1731
+ " }",
1732
+ "",
1733
+ " // Initialize widget",
1734
+ " var handle = agentWidget.initAgentWidget({",
1735
+ " target: 'body',",
1736
+ " useShadowDom: false,",
1737
+ " config: widgetConfig",
1738
+ " });",
1739
+ "",
1740
+ " // Save state on message events",
1741
+ " window.addEventListener('persona:message', function() {",
1742
+ " var session = handle.getSession ? handle.getSession() : null;",
1743
+ " if (session) {",
1744
+ " localStorage.setItem(STORAGE_KEY, JSON.stringify({",
1745
+ " messages: session.messages,",
1746
+ " timestamp: new Date().toISOString()",
1747
+ " }));",
1748
+ " }",
1749
+ " });",
1750
+ "",
1751
+ " // Clear state on clear chat",
1752
+ " window.addEventListener('persona:clear-chat', function() {",
1753
+ " localStorage.removeItem(STORAGE_KEY);",
1754
+ " localStorage.removeItem(PROCESSED_ACTIONS_KEY);",
1755
+ " });",
1756
+ " };",
1757
+ "",
1758
+ " // Wait for framework hydration to complete (Next.js, Nuxt, etc.)",
1759
+ " // This prevents the framework from removing dynamically added CSS during reconciliation",
1760
+ " var waitForHydration = function(callback) {",
1761
+ " var executed = false;",
1762
+ " ",
1763
+ " var execute = function() {",
1764
+ " if (executed) return;",
1765
+ " executed = true;",
1766
+ " callback();",
1767
+ " };",
1768
+ "",
1769
+ " var afterDom = function() {",
1770
+ " // Strategy 1: Use requestIdleCallback if available (best for detecting idle after hydration)",
1771
+ " if (typeof requestIdleCallback !== 'undefined') {",
1772
+ " requestIdleCallback(function() {",
1773
+ " // Double requestAnimationFrame ensures at least one full paint cycle completed",
1774
+ " requestAnimationFrame(function() {",
1775
+ " requestAnimationFrame(execute);",
1776
+ " });",
1777
+ " }, { timeout: 3000 }); // Max wait 3 seconds, then proceed anyway",
1778
+ " } else {",
1779
+ " // Strategy 2: Fallback for Safari (no requestIdleCallback)",
1780
+ " // 300ms is typically enough for hydration on most pages",
1781
+ " setTimeout(execute, 300);",
1782
+ " }",
1783
+ " };",
1784
+ "",
1785
+ " if (document.readyState === 'loading') {",
1786
+ " document.addEventListener('DOMContentLoaded', afterDom);",
1787
+ " } else {",
1788
+ " // DOM already ready, but still wait for potential hydration",
1789
+ " afterDom();",
1790
+ " }",
1791
+ " };",
1792
+ "",
1793
+ " // Boot sequence: wait for hydration, then load CSS and JS, then initialize",
1794
+ " // This prevents Next.js/Nuxt/etc. from removing dynamically added CSS during reconciliation",
1795
+ " waitForHydration(function() {",
1796
+ " loadCSS();",
1797
+ " loadJS(function() {",
1798
+ " init();",
1799
+ " });",
1800
+ " });",
1801
+ "})();",
1802
+ "</script>"
1803
+ ]);
1804
+
1805
+ return lines.join("\n");
1806
+ }