@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,92 @@
1
+ import * as icons from "lucide";
2
+ import type { IconNode } from "lucide";
3
+
4
+ /**
5
+ * Renders a Lucide icon as an inline SVG element
6
+ * This approach requires no CSS and works on any page
7
+ *
8
+ * @param iconName - The Lucide icon name in kebab-case (e.g., "arrow-up")
9
+ * @param size - The size of the icon (default: 24)
10
+ * @param color - The stroke color (default: "currentColor")
11
+ * @param strokeWidth - The stroke width (default: 2)
12
+ * @returns SVGElement or null if icon not found
13
+ */
14
+ export const renderLucideIcon = (
15
+ iconName: string,
16
+ size: number | string = 24,
17
+ color: string = "currentColor",
18
+ strokeWidth: number = 2
19
+ ): SVGElement | null => {
20
+ try {
21
+ // Convert kebab-case to PascalCase (e.g., "arrow-up" -> "ArrowUp")
22
+ const pascalName = iconName
23
+ .split("-")
24
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
25
+ .join("");
26
+
27
+ // Lucide's icons object contains IconNode data directly, not functions
28
+ const iconData = (icons as Record<string, IconNode>)[pascalName] as IconNode;
29
+
30
+ if (!iconData) {
31
+ console.warn(`Lucide icon "${iconName}" not found (tried "${pascalName}"). Available icons: https://lucide.dev/icons`);
32
+ return null;
33
+ }
34
+
35
+ return createSvgFromIconData(iconData, size, color, strokeWidth);
36
+ } catch (error) {
37
+ console.warn(`Failed to render Lucide icon "${iconName}":`, error);
38
+ return null;
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Helper function to create SVG from IconNode data
44
+ */
45
+ function createSvgFromIconData(
46
+ iconData: IconNode,
47
+ size: number | string,
48
+ color: string,
49
+ strokeWidth: number
50
+ ): SVGElement | null {
51
+ if (!iconData || !Array.isArray(iconData)) {
52
+ return null;
53
+ }
54
+
55
+ // Create SVG element
56
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
57
+ svg.setAttribute("width", String(size));
58
+ svg.setAttribute("height", String(size));
59
+ svg.setAttribute("viewBox", "0 0 24 24");
60
+ svg.setAttribute("fill", "none");
61
+ svg.setAttribute("stroke", color);
62
+ svg.setAttribute("stroke-width", String(strokeWidth));
63
+ svg.setAttribute("stroke-linecap", "round");
64
+ svg.setAttribute("stroke-linejoin", "round");
65
+ svg.setAttribute("aria-hidden", "true");
66
+
67
+ // Render elements from icon data
68
+ // IconNode format: [["path", {"d": "..."}], ["rect", {"x": "...", "y": "..."}], ...]
69
+ iconData.forEach((elementData) => {
70
+ if (Array.isArray(elementData) && elementData.length >= 2) {
71
+ const tagName = elementData[0] as string;
72
+ const attrs = elementData[1] as Record<string, string>;
73
+
74
+ if (attrs) {
75
+ // Create the appropriate SVG element (path, rect, circle, ellipse, line, etc.)
76
+ const element = document.createElementNS("http://www.w3.org/2000/svg", tagName);
77
+
78
+ // Apply all attributes, but skip 'stroke' (we want to use the parent SVG's stroke for consistent coloring)
79
+ Object.entries(attrs).forEach(([key, value]) => {
80
+ if (key !== "stroke") {
81
+ element.setAttribute(key, String(value));
82
+ }
83
+ });
84
+
85
+ svg.appendChild(element);
86
+ }
87
+ }
88
+ });
89
+
90
+ return svg;
91
+ }
92
+
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Message ID utilities for client-side message tracking
3
+ * Used for feedback integration with the Travrse API
4
+ */
5
+
6
+ /**
7
+ * Generate a unique message ID for tracking
8
+ * Format: msg_{timestamp_base36}_{random_8chars}
9
+ */
10
+ export function generateMessageId(): string {
11
+ const timestamp = Date.now().toString(36);
12
+ const random = Math.random().toString(36).substring(2, 10);
13
+ return `msg_${timestamp}_${random}`;
14
+ }
15
+
16
+ /**
17
+ * Generate a unique user message ID
18
+ * Format: usr_{timestamp_base36}_{random_8chars}
19
+ */
20
+ export function generateUserMessageId(): string {
21
+ const timestamp = Date.now().toString(36);
22
+ const random = Math.random().toString(36).substring(2, 10);
23
+ return `usr_${timestamp}_${random}`;
24
+ }
25
+
26
+ /**
27
+ * Generate a unique assistant message ID
28
+ * Format: ast_{timestamp_base36}_{random_8chars}
29
+ */
30
+ export function generateAssistantMessageId(): string {
31
+ const timestamp = Date.now().toString(36);
32
+ const random = Math.random().toString(36).substring(2, 10);
33
+ return `ast_${timestamp}_${random}`;
34
+ }
35
+
36
+
37
+
@@ -0,0 +1,36 @@
1
+ import { Idiomorph } from "idiomorph";
2
+
3
+ export type MorphOptions = {
4
+ preserveTypingAnimation?: boolean;
5
+ };
6
+
7
+ /**
8
+ * Morph a container's contents using idiomorph with chat-widget-specific
9
+ * preservation rules for typing indicators.
10
+ *
11
+ * Action buttons are matched by their `id` attribute (set to `actions-{messageId}`)
12
+ * so idiomorph updates them in place rather than recreating them.
13
+ */
14
+ export const morphMessages = (
15
+ container: HTMLElement,
16
+ newContent: HTMLElement,
17
+ options: MorphOptions = {}
18
+ ): void => {
19
+ const { preserveTypingAnimation = true } = options;
20
+
21
+ Idiomorph.morph(container, newContent.innerHTML, {
22
+ morphStyle: "innerHTML",
23
+ callbacks: {
24
+ beforeNodeMorphed(oldNode: Node, newNode: Node): boolean | void {
25
+ if (!(oldNode instanceof HTMLElement)) return;
26
+
27
+ // Preserve typing indicator dots to maintain animation continuity
28
+ if (preserveTypingAnimation) {
29
+ if (oldNode.classList.contains("tvw-animate-typing")) {
30
+ return false;
31
+ }
32
+ }
33
+ },
34
+ },
35
+ });
36
+ };
@@ -0,0 +1,17 @@
1
+ export const positionMap: Record<
2
+ "bottom-right" | "bottom-left" | "top-right" | "top-left",
3
+ string
4
+ > = {
5
+ "bottom-right": "tvw-bottom-6 tvw-right-6",
6
+ "bottom-left": "tvw-bottom-6 tvw-left-6",
7
+ "top-right": "tvw-top-6 tvw-right-6",
8
+ "top-left": "tvw-top-6 tvw-left-6"
9
+ };
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
@@ -0,0 +1,72 @@
1
+ import type {
2
+ AgentWidgetMessage,
3
+ AgentWidgetStorageAdapter,
4
+ AgentWidgetStoredState
5
+ } from "../types";
6
+
7
+ const safeJsonParse = (value: string | null) => {
8
+ if (!value) return null;
9
+ try {
10
+ return JSON.parse(value);
11
+ } catch (error) {
12
+ if (typeof console !== "undefined") {
13
+ // eslint-disable-next-line no-console
14
+ console.error("[AgentWidget] Failed to parse stored state:", error);
15
+ }
16
+ return null;
17
+ }
18
+ };
19
+
20
+ const sanitizeMessages = (messages: AgentWidgetMessage[]) =>
21
+ messages.map((message) => ({
22
+ ...message,
23
+ streaming: false
24
+ }));
25
+
26
+ export const createLocalStorageAdapter = (
27
+ key = "persona-state"
28
+ ): AgentWidgetStorageAdapter => {
29
+ const getStorage = () => {
30
+ if (typeof window === "undefined" || !window.localStorage) {
31
+ return null;
32
+ }
33
+ return window.localStorage;
34
+ };
35
+
36
+ return {
37
+ load: () => {
38
+ const storage = getStorage();
39
+ if (!storage) return null;
40
+ return safeJsonParse(storage.getItem(key));
41
+ },
42
+ save: (state: AgentWidgetStoredState) => {
43
+ const storage = getStorage();
44
+ if (!storage) return;
45
+ try {
46
+ const payload: AgentWidgetStoredState = {
47
+ ...state,
48
+ messages: state.messages ? sanitizeMessages(state.messages) : undefined
49
+ };
50
+ storage.setItem(key, JSON.stringify(payload));
51
+ } catch (error) {
52
+ if (typeof console !== "undefined") {
53
+ // eslint-disable-next-line no-console
54
+ console.error("[AgentWidget] Failed to persist state:", error);
55
+ }
56
+ }
57
+ },
58
+ clear: () => {
59
+ const storage = getStorage();
60
+ if (!storage) return;
61
+ try {
62
+ storage.removeItem(key);
63
+ } catch (error) {
64
+ if (typeof console !== "undefined") {
65
+ // eslint-disable-next-line no-console
66
+ console.error("[AgentWidget] Failed to clear stored state:", error);
67
+ }
68
+ }
69
+ }
70
+ };
71
+ };
72
+
@@ -0,0 +1,105 @@
1
+ import { AgentWidgetConfig, AgentWidgetTheme } from "../types";
2
+
3
+ /**
4
+ * Detects the current color scheme from the page.
5
+ * 1. Checks if <html> element has 'dark' class
6
+ * 2. Falls back to prefers-color-scheme media query
7
+ */
8
+ export const detectColorScheme = (): 'light' | 'dark' => {
9
+ // Check for 'dark' class on <html> element
10
+ if (typeof document !== 'undefined' && document.documentElement.classList.contains('dark')) {
11
+ return 'dark';
12
+ }
13
+
14
+ // Fall back to media query
15
+ if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
16
+ return 'dark';
17
+ }
18
+
19
+ return 'light';
20
+ };
21
+
22
+ /**
23
+ * Gets the active theme based on colorScheme setting and current detection.
24
+ */
25
+ export const getActiveTheme = (config?: AgentWidgetConfig): AgentWidgetTheme => {
26
+ const colorScheme = config?.colorScheme ?? 'light';
27
+ const lightTheme = config?.theme ?? {};
28
+ const darkTheme = config?.darkTheme ?? lightTheme;
29
+
30
+ if (colorScheme === 'light') {
31
+ return lightTheme;
32
+ }
33
+
34
+ if (colorScheme === 'dark') {
35
+ return darkTheme;
36
+ }
37
+
38
+ // colorScheme === 'auto'
39
+ const detectedScheme = detectColorScheme();
40
+ return detectedScheme === 'dark' ? darkTheme : lightTheme;
41
+ };
42
+
43
+ /**
44
+ * Creates observers for theme changes (HTML class and media query).
45
+ * Returns a cleanup function.
46
+ */
47
+ export const createThemeObserver = (
48
+ callback: (scheme: 'light' | 'dark') => void
49
+ ): (() => void) => {
50
+ const cleanupFns: Array<() => void> = [];
51
+
52
+ // Observe HTML class changes
53
+ if (typeof document !== 'undefined' && typeof MutationObserver !== 'undefined') {
54
+ const observer = new MutationObserver(() => {
55
+ callback(detectColorScheme());
56
+ });
57
+
58
+ observer.observe(document.documentElement, {
59
+ attributes: true,
60
+ attributeFilter: ['class']
61
+ });
62
+
63
+ cleanupFns.push(() => observer.disconnect());
64
+ }
65
+
66
+ // Observe media query changes
67
+ if (typeof window !== 'undefined' && window.matchMedia) {
68
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
69
+ const handleChange = () => callback(detectColorScheme());
70
+
71
+ // Use addEventListener if available (modern browsers), otherwise addListener
72
+ if (mediaQuery.addEventListener) {
73
+ mediaQuery.addEventListener('change', handleChange);
74
+ cleanupFns.push(() => mediaQuery.removeEventListener('change', handleChange));
75
+ } else if (mediaQuery.addListener) {
76
+ // Legacy Safari
77
+ mediaQuery.addListener(handleChange);
78
+ cleanupFns.push(() => mediaQuery.removeListener(handleChange));
79
+ }
80
+ }
81
+
82
+ return () => {
83
+ cleanupFns.forEach(fn => fn());
84
+ };
85
+ };
86
+
87
+ export const applyThemeVariables = (
88
+ element: HTMLElement,
89
+ config?: AgentWidgetConfig
90
+ ) => {
91
+ const theme = getActiveTheme(config);
92
+ Object.entries(theme).forEach(([key, value]) => {
93
+ // Skip undefined or empty values
94
+ if (value === undefined || value === null || value === "") {
95
+ return;
96
+ }
97
+ // Convert camelCase to kebab-case (e.g., radiusSm → radius-sm)
98
+ const kebabKey = key.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
99
+ element.style.setProperty(`--cw-${kebabKey}`, String(value));
100
+ });
101
+ };
102
+
103
+
104
+
105
+
package/src/widget.css ADDED
@@ -0,0 +1 @@
1
+ @import "./styles/widget.css";
package/widget.css ADDED
@@ -0,0 +1 @@
1
+ @import "./src/widget.css";