@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.
- package/README.md +1080 -0
- package/dist/index.cjs +140 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2626 -0
- package/dist/index.d.ts +2626 -0
- package/dist/index.global.js +1843 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +140 -0
- package/dist/index.js.map +1 -0
- package/dist/install.global.js +2 -0
- package/dist/install.global.js.map +1 -0
- package/dist/widget.css +1627 -0
- package/package.json +79 -0
- package/src/@types/idiomorph.d.ts +37 -0
- package/src/client.test.ts +387 -0
- package/src/client.ts +1589 -0
- package/src/components/composer-builder.ts +530 -0
- package/src/components/feedback.ts +379 -0
- package/src/components/forms.ts +170 -0
- package/src/components/header-builder.ts +455 -0
- package/src/components/header-layouts.ts +303 -0
- package/src/components/launcher.ts +193 -0
- package/src/components/message-bubble.ts +528 -0
- package/src/components/messages.ts +54 -0
- package/src/components/panel.ts +204 -0
- package/src/components/reasoning-bubble.ts +144 -0
- package/src/components/registry.ts +87 -0
- package/src/components/suggestions.ts +97 -0
- package/src/components/tool-bubble.ts +288 -0
- package/src/defaults.ts +321 -0
- package/src/index.ts +175 -0
- package/src/install.ts +284 -0
- package/src/plugins/registry.ts +77 -0
- package/src/plugins/types.ts +95 -0
- package/src/postprocessors.ts +194 -0
- package/src/runtime/init.ts +162 -0
- package/src/session.ts +376 -0
- package/src/styles/tailwind.css +20 -0
- package/src/styles/widget.css +1627 -0
- package/src/types.ts +1635 -0
- package/src/ui.ts +3341 -0
- package/src/utils/actions.ts +227 -0
- package/src/utils/attachment-manager.ts +384 -0
- package/src/utils/code-generators.test.ts +500 -0
- package/src/utils/code-generators.ts +1806 -0
- package/src/utils/component-middleware.ts +137 -0
- package/src/utils/component-parser.ts +119 -0
- package/src/utils/constants.ts +16 -0
- package/src/utils/content.ts +306 -0
- package/src/utils/dom.ts +25 -0
- package/src/utils/events.ts +41 -0
- package/src/utils/formatting.test.ts +166 -0
- package/src/utils/formatting.ts +470 -0
- package/src/utils/icons.ts +92 -0
- package/src/utils/message-id.ts +37 -0
- package/src/utils/morph.ts +36 -0
- package/src/utils/positioning.ts +17 -0
- package/src/utils/storage.ts +72 -0
- package/src/utils/theme.ts +105 -0
- package/src/widget.css +1 -0
- 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";
|