@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,194 @@
1
+ import { Marked, type RendererObject } from "marked";
2
+ import type { AgentWidgetMarkdownConfig, AgentWidgetMarkdownRendererOverrides, AgentWidgetMarkdownOptions } from "./types";
3
+
4
+ /**
5
+ * Options for creating a markdown processor
6
+ */
7
+ export type MarkdownProcessorOptions = {
8
+ /** Marked parsing options */
9
+ markedOptions?: AgentWidgetMarkdownOptions;
10
+ /** Custom renderer overrides */
11
+ renderer?: AgentWidgetMarkdownRendererOverrides;
12
+ };
13
+
14
+ /**
15
+ * Converts AgentWidgetMarkdownRendererOverrides to marked's RendererObject format
16
+ */
17
+ const convertRendererOverrides = (
18
+ overrides?: AgentWidgetMarkdownRendererOverrides
19
+ ): Partial<RendererObject> | undefined => {
20
+ if (!overrides) return undefined;
21
+
22
+ // The token-based API in marked v12+ matches our type definitions
23
+ // We can pass through the overrides directly
24
+ return overrides as Partial<RendererObject>;
25
+ };
26
+
27
+ /**
28
+ * Creates a configured markdown processor with custom options and renderers.
29
+ *
30
+ * @param options - Configuration options for the markdown processor
31
+ * @returns A function that converts markdown text to HTML
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * // Basic usage with defaults
36
+ * const processor = createMarkdownProcessor();
37
+ * const html = processor("# Hello World");
38
+ *
39
+ * // With custom options
40
+ * const processor = createMarkdownProcessor({
41
+ * markedOptions: { gfm: true, breaks: true },
42
+ * renderer: {
43
+ * link(token) {
44
+ * return `<a href="${token.href}" target="_blank">${token.text}</a>`;
45
+ * }
46
+ * }
47
+ * });
48
+ * ```
49
+ */
50
+ export const createMarkdownProcessor = (options?: MarkdownProcessorOptions) => {
51
+ const opts = options?.markedOptions;
52
+ const markedInstance = new Marked({
53
+ gfm: opts?.gfm ?? true,
54
+ breaks: opts?.breaks ?? true,
55
+ pedantic: opts?.pedantic,
56
+ silent: opts?.silent,
57
+ });
58
+
59
+ const rendererOverrides = convertRendererOverrides(options?.renderer);
60
+ if (rendererOverrides) {
61
+ markedInstance.use({ renderer: rendererOverrides });
62
+ }
63
+
64
+ return (text: string): string => {
65
+ return markedInstance.parse(text) as string;
66
+ };
67
+ };
68
+
69
+ /**
70
+ * Creates a markdown processor from AgentWidgetMarkdownConfig.
71
+ * This is a convenience function that maps the widget config to processor options.
72
+ *
73
+ * @param config - The markdown configuration from widget config
74
+ * @returns A function that converts markdown text to HTML
75
+ */
76
+ export const createMarkdownProcessorFromConfig = (config?: AgentWidgetMarkdownConfig) => {
77
+ if (!config) {
78
+ return createMarkdownProcessor();
79
+ }
80
+
81
+ return createMarkdownProcessor({
82
+ markedOptions: config.options,
83
+ renderer: config.renderer,
84
+ });
85
+ };
86
+
87
+ // Create default markdown processor instance
88
+ const defaultMarkdownProcessor = createMarkdownProcessor();
89
+
90
+ /**
91
+ * Basic markdown renderer using default settings.
92
+ * Remember to sanitize the returned HTML if you render untrusted content in your host page.
93
+ *
94
+ * For custom configuration, use `createMarkdownProcessor()` or `createMarkdownProcessorFromConfig()`.
95
+ */
96
+ export const markdownPostprocessor = (text: string): string => {
97
+ return defaultMarkdownProcessor(text);
98
+ };
99
+
100
+ /**
101
+ * Escapes HTML entities. Used as the default safe renderer.
102
+ */
103
+ export const escapeHtml = (text: string): string =>
104
+ text
105
+ .replace(/&/g, "&amp;")
106
+ .replace(/</g, "&lt;")
107
+ .replace(/>/g, "&gt;")
108
+ .replace(/"/g, "&quot;")
109
+ .replace(/'/g, "&#39;");
110
+
111
+ const escapeAttribute = (value: string) =>
112
+ value.replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
113
+
114
+ const makeToken = (idx: number) => `%%FORM_PLACEHOLDER_${idx}%%`;
115
+
116
+ const directiveReplacer = (source: string, placeholders: Array<{ token: string; type: string }>) => {
117
+ let working = source;
118
+
119
+ // JSON directive pattern e.g. <Directive>{"component":"form","type":"init"}</Directive>
120
+ working = working.replace(/<Directive>([\s\S]*?)<\/Directive>/gi, (match, jsonText) => {
121
+ try {
122
+ const parsed = JSON.parse(jsonText.trim());
123
+ if (parsed && typeof parsed === "object" && parsed.component === "form" && parsed.type) {
124
+ const token = makeToken(placeholders.length);
125
+ placeholders.push({ token, type: String(parsed.type) });
126
+ return token;
127
+ }
128
+ } catch (error) {
129
+ return match;
130
+ }
131
+ return match;
132
+ });
133
+
134
+ // XML-style directive e.g. <Form type="init" />
135
+ working = working.replace(/<Form\s+type="([^"]+)"\s*\/>/gi, (_, type) => {
136
+ const token = makeToken(placeholders.length);
137
+ placeholders.push({ token, type });
138
+ return token;
139
+ });
140
+
141
+ return working;
142
+ };
143
+
144
+ /**
145
+ * Creates a directive postprocessor with custom markdown configuration.
146
+ * Converts special directives (either `<Form type="init" />` or
147
+ * `<Directive>{"component":"form","type":"init"}</Directive>`) into placeholder
148
+ * elements that the widget upgrades after render. Remaining text is rendered as
149
+ * Markdown with the provided configuration.
150
+ *
151
+ * @param markdownConfig - Optional markdown configuration
152
+ * @returns A function that processes text with directives and markdown
153
+ */
154
+ export const createDirectivePostprocessor = (markdownConfig?: AgentWidgetMarkdownConfig) => {
155
+ const processor = createMarkdownProcessorFromConfig(markdownConfig);
156
+
157
+ return (text: string): string => {
158
+ const placeholders: Array<{ token: string; type: string }> = [];
159
+ const withTokens = directiveReplacer(text, placeholders);
160
+ let html = processor(withTokens);
161
+
162
+ placeholders.forEach(({ token, type }) => {
163
+ const tokenRegex = new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
164
+ const safeType = escapeAttribute(type);
165
+ const replacement = `<div class="tvw-form-directive" data-tv-form="${safeType}"></div>`;
166
+ html = html.replace(tokenRegex, replacement);
167
+ });
168
+
169
+ return html;
170
+ };
171
+ };
172
+
173
+ /**
174
+ * Converts special directives (either `<Form type="init" />` or
175
+ * `<Directive>{"component":"form","type":"init"}</Directive>`) into placeholder
176
+ * elements that the widget upgrades after render. Remaining text is rendered as
177
+ * Markdown using default settings.
178
+ *
179
+ * For custom markdown configuration, use `createDirectivePostprocessor()`.
180
+ */
181
+ export const directivePostprocessor = (text: string): string => {
182
+ const placeholders: Array<{ token: string; type: string }> = [];
183
+ const withTokens = directiveReplacer(text, placeholders);
184
+ let html = markdownPostprocessor(withTokens);
185
+
186
+ placeholders.forEach(({ token, type }) => {
187
+ const tokenRegex = new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
188
+ const safeType = escapeAttribute(type);
189
+ const replacement = `<div class="tvw-form-directive" data-tv-form="${safeType}"></div>`;
190
+ html = html.replace(tokenRegex, replacement);
191
+ });
192
+
193
+ return html;
194
+ };
@@ -0,0 +1,162 @@
1
+ import { createAgentExperience, AgentWidgetController } from "../ui";
2
+ import { AgentWidgetConfig, AgentWidgetInitOptions, AgentWidgetEvent } from "../types";
3
+
4
+ const ensureTarget = (target: string | HTMLElement): HTMLElement => {
5
+ if (typeof window === "undefined" || typeof document === "undefined") {
6
+ throw new Error("Chat widget can only be mounted in a browser environment");
7
+ }
8
+
9
+ if (typeof target === "string") {
10
+ const element = document.querySelector<HTMLElement>(target);
11
+ if (!element) {
12
+ throw new Error(`Chat widget target "${target}" was not found`);
13
+ }
14
+ return element;
15
+ }
16
+
17
+ return target;
18
+ };
19
+
20
+ const widgetCssHref = (): string | null => {
21
+ try {
22
+ // This works in ESM builds but not in IIFE builds
23
+ if (typeof import.meta !== "undefined" && import.meta.url) {
24
+ return new URL("../widget.css", import.meta.url).href;
25
+ }
26
+ } catch {
27
+ // Fallback for IIFE builds where CSS should be loaded separately
28
+ }
29
+ return null;
30
+ };
31
+
32
+ const mountStyles = (root: ShadowRoot | HTMLElement) => {
33
+ const href = widgetCssHref();
34
+
35
+ const adoptExistingStylesheet = () => {
36
+ if (!(root instanceof ShadowRoot)) {
37
+ return;
38
+ }
39
+
40
+ if (root.querySelector('link[data-persona]')) {
41
+ return;
42
+ }
43
+
44
+ const globalLink = document.head.querySelector<HTMLLinkElement>(
45
+ 'link[data-persona]'
46
+ );
47
+ if (!globalLink) {
48
+ return;
49
+ }
50
+
51
+ const clonedLink = globalLink.cloneNode(true) as HTMLLinkElement;
52
+ root.insertBefore(clonedLink, root.firstChild);
53
+ };
54
+
55
+ if (root instanceof ShadowRoot) {
56
+ // For shadow DOM, we need to load CSS into the shadow root
57
+ if (href) {
58
+ const link = document.createElement("link");
59
+ link.rel = "stylesheet";
60
+ link.href = href;
61
+ link.setAttribute("data-persona", "true");
62
+ root.insertBefore(link, root.firstChild);
63
+ } else {
64
+ adoptExistingStylesheet();
65
+ }
66
+ // If href is null (IIFE build), CSS should already be loaded globally
67
+ } else {
68
+ // For non-shadow DOM, check if CSS is already loaded
69
+ const existing = document.head.querySelector<HTMLLinkElement>(
70
+ "link[data-persona]"
71
+ );
72
+ if (!existing) {
73
+ if (href) {
74
+ // ESM build - load CSS dynamically
75
+ const link = document.createElement("link");
76
+ link.rel = "stylesheet";
77
+ link.href = href;
78
+ link.setAttribute("data-persona", "true");
79
+ document.head.appendChild(link);
80
+ }
81
+ // IIFE build - CSS should be loaded via <link> tag before script
82
+ // If not found, we'll assume it's loaded globally or warn in dev
83
+ }
84
+ }
85
+ };
86
+
87
+ export type AgentWidgetInitHandle = AgentWidgetController & { host: HTMLElement };
88
+
89
+ export const initAgentWidget = (
90
+ options: AgentWidgetInitOptions
91
+ ): AgentWidgetInitHandle => {
92
+ const target = ensureTarget(options.target);
93
+ const host = document.createElement("div");
94
+ host.className = "persona-host";
95
+
96
+ // When launcher is disabled (inline embed mode), ensure the host fills its container
97
+ const launcherEnabled = options.config?.launcher?.enabled ?? true;
98
+ if (!launcherEnabled) {
99
+ host.style.height = "100%";
100
+ }
101
+
102
+ target.appendChild(host);
103
+
104
+ const useShadow = options.useShadowDom === true;
105
+ let mount: HTMLElement;
106
+ let root: ShadowRoot | HTMLElement;
107
+
108
+ if (useShadow) {
109
+ const shadowRoot = host.attachShadow({ mode: "open" });
110
+ root = shadowRoot;
111
+ mount = document.createElement("div");
112
+ mount.id = "persona-root";
113
+ // When launcher is disabled, ensure mount fills the host
114
+ if (!launcherEnabled) {
115
+ mount.style.height = "100%";
116
+ mount.style.display = "flex";
117
+ mount.style.flexDirection = "column";
118
+ mount.style.flex = "1";
119
+ mount.style.minHeight = "0";
120
+ }
121
+ shadowRoot.appendChild(mount);
122
+ mountStyles(shadowRoot);
123
+ } else {
124
+ root = host;
125
+ mount = document.createElement("div");
126
+ mount.id = "persona-root";
127
+ // When launcher is disabled, ensure mount fills the host
128
+ if (!launcherEnabled) {
129
+ mount.style.height = "100%";
130
+ mount.style.display = "flex";
131
+ mount.style.flexDirection = "column";
132
+ mount.style.flex = "1";
133
+ mount.style.minHeight = "0";
134
+ }
135
+ host.appendChild(mount);
136
+ mountStyles(host);
137
+ }
138
+
139
+ let controller = createAgentExperience(mount, options.config, {
140
+ debugTools: options.debugTools
141
+ });
142
+ options.onReady?.();
143
+
144
+ const handle: AgentWidgetInitHandle = {
145
+ ...controller,
146
+ host,
147
+ destroy() {
148
+ controller.destroy();
149
+ host.remove();
150
+ if (options.windowKey && typeof window !== "undefined") {
151
+ delete (window as any)[options.windowKey];
152
+ }
153
+ }
154
+ };
155
+
156
+ // Store on window if windowKey is provided
157
+ if (options.windowKey && typeof window !== 'undefined') {
158
+ (window as any)[options.windowKey] = handle;
159
+ }
160
+
161
+ return handle;
162
+ };