@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,137 @@
1
+ import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
2
+ import { componentRegistry, ComponentContext } from "../components/registry";
3
+ import { ComponentDirective, createComponentStreamParser } from "./component-parser";
4
+ import { createStandardBubble, MessageTransform } from "../components/message-bubble";
5
+
6
+ /**
7
+ * Options for component middleware
8
+ */
9
+ export interface ComponentMiddlewareOptions {
10
+ config: AgentWidgetConfig;
11
+ message: AgentWidgetMessage;
12
+ transform: MessageTransform;
13
+ onPropsUpdate?: (props: Record<string, unknown>) => void;
14
+ }
15
+
16
+ /**
17
+ * Renders a component directive into an HTMLElement
18
+ */
19
+ export function renderComponentDirective(
20
+ directive: ComponentDirective,
21
+ options: ComponentMiddlewareOptions
22
+ ): HTMLElement | null {
23
+ const { config, message, onPropsUpdate } = options;
24
+
25
+ // Get component renderer from registry
26
+ const renderer = componentRegistry.get(directive.component);
27
+ if (!renderer) {
28
+ // Component not found, fall back to default rendering
29
+ console.warn(
30
+ `[ComponentMiddleware] Component "${directive.component}" not found in registry. Falling back to default rendering.`
31
+ );
32
+ return null;
33
+ }
34
+
35
+ // Create component context
36
+ const context: ComponentContext = {
37
+ message,
38
+ config,
39
+ updateProps: (newProps: Record<string, unknown>) => {
40
+ if (onPropsUpdate) {
41
+ onPropsUpdate(newProps);
42
+ }
43
+ }
44
+ };
45
+
46
+ try {
47
+ // Render the component
48
+ const element = renderer(directive.props, context);
49
+ return element;
50
+ } catch (error) {
51
+ console.error(
52
+ `[ComponentMiddleware] Error rendering component "${directive.component}":`,
53
+ error
54
+ );
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Creates middleware that processes component directives from streamed JSON
61
+ */
62
+ export function createComponentMiddleware() {
63
+ const parser = createComponentStreamParser();
64
+
65
+ return {
66
+ /**
67
+ * Process accumulated content and extract component directive
68
+ */
69
+ processChunk: (accumulatedContent: string): ComponentDirective | null => {
70
+ return parser.processChunk(accumulatedContent);
71
+ },
72
+
73
+ /**
74
+ * Get the currently extracted directive
75
+ */
76
+ getDirective: (): ComponentDirective | null => {
77
+ return parser.getExtractedDirective();
78
+ },
79
+
80
+ /**
81
+ * Reset the parser state
82
+ */
83
+ reset: () => {
84
+ parser.reset();
85
+ }
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Checks if a message contains a component directive in its raw content
91
+ */
92
+ export function hasComponentDirective(message: AgentWidgetMessage): boolean {
93
+ if (!message.rawContent) return false;
94
+
95
+ try {
96
+ const parsed = JSON.parse(message.rawContent);
97
+ return (
98
+ typeof parsed === "object" &&
99
+ parsed !== null &&
100
+ "component" in parsed &&
101
+ typeof parsed.component === "string"
102
+ );
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Extracts component directive from a complete message
110
+ */
111
+ export function extractComponentDirectiveFromMessage(
112
+ message: AgentWidgetMessage
113
+ ): ComponentDirective | null {
114
+ if (!message.rawContent) return null;
115
+
116
+ try {
117
+ const parsed = JSON.parse(message.rawContent);
118
+ if (
119
+ typeof parsed === "object" &&
120
+ parsed !== null &&
121
+ "component" in parsed &&
122
+ typeof parsed.component === "string"
123
+ ) {
124
+ return {
125
+ component: parsed.component,
126
+ props: (parsed.props && typeof parsed.props === "object" && parsed.props !== null
127
+ ? parsed.props
128
+ : {}) as Record<string, unknown>,
129
+ raw: message.rawContent
130
+ };
131
+ }
132
+ } catch {
133
+ // Not valid JSON or not a component directive
134
+ }
135
+
136
+ return null;
137
+ }
@@ -0,0 +1,119 @@
1
+ import { parse as parsePartialJson, STR, OBJ } from "partial-json";
2
+
3
+ /**
4
+ * Represents a component directive extracted from JSON
5
+ */
6
+ export interface ComponentDirective {
7
+ component: string;
8
+ props: Record<string, unknown>;
9
+ raw: string;
10
+ }
11
+
12
+ /**
13
+ * Checks if a parsed object is a component directive
14
+ */
15
+ function isComponentDirective(obj: unknown): obj is { component: string; props?: unknown } {
16
+ if (!obj || typeof obj !== "object") return false;
17
+ if (!("component" in obj)) return false;
18
+ const component = (obj as { component: unknown }).component;
19
+ return typeof component === "string" && component.length > 0;
20
+ }
21
+
22
+ /**
23
+ * Extracts component directive from parsed JSON object
24
+ */
25
+ function extractComponentDirective(
26
+ parsed: unknown,
27
+ rawJson: string
28
+ ): ComponentDirective | null {
29
+ if (!isComponentDirective(parsed)) {
30
+ return null;
31
+ }
32
+
33
+ const props = parsed.props && typeof parsed.props === "object" && parsed.props !== null
34
+ ? (parsed.props as Record<string, unknown>)
35
+ : {};
36
+
37
+ return {
38
+ component: parsed.component,
39
+ props,
40
+ raw: rawJson
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Creates a parser that extracts component directives from JSON streams
46
+ * This parser looks for objects with a "component" field and extracts
47
+ * the component name and props incrementally as they stream in.
48
+ */
49
+ export function createComponentStreamParser() {
50
+ let extractedDirective: ComponentDirective | null = null;
51
+ let processedLength = 0;
52
+
53
+ return {
54
+ /**
55
+ * Get the currently extracted component directive
56
+ */
57
+ getExtractedDirective: (): ComponentDirective | null => {
58
+ return extractedDirective;
59
+ },
60
+
61
+ /**
62
+ * Process a chunk of JSON and extract component directive if present
63
+ */
64
+ processChunk: (accumulatedContent: string): ComponentDirective | null => {
65
+ // Validate that the accumulated content looks like JSON
66
+ const trimmed = accumulatedContent.trim();
67
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
68
+ return null;
69
+ }
70
+
71
+ // Skip if no new content
72
+ if (accumulatedContent.length <= processedLength) {
73
+ return extractedDirective;
74
+ }
75
+
76
+ try {
77
+ // Parse partial JSON - allow partial strings and objects during streaming
78
+ // STR | OBJ allows incomplete strings and objects during streaming
79
+ const parsed = parsePartialJson(accumulatedContent, STR | OBJ);
80
+
81
+ // Try to extract component directive
82
+ const directive = extractComponentDirective(parsed, accumulatedContent);
83
+ if (directive) {
84
+ extractedDirective = directive;
85
+ }
86
+ } catch (error) {
87
+ // If parsing fails completely, keep the last extracted directive
88
+ // This can happen with very malformed JSON during streaming
89
+ }
90
+
91
+ // Update processed length
92
+ processedLength = accumulatedContent.length;
93
+
94
+ return extractedDirective;
95
+ },
96
+
97
+ /**
98
+ * Reset the parser state
99
+ */
100
+ reset: () => {
101
+ extractedDirective = null;
102
+ processedLength = 0;
103
+ }
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Type guard to check if an object is a component directive
109
+ */
110
+ export function isComponentDirectiveType(obj: unknown): obj is ComponentDirective {
111
+ return (
112
+ typeof obj === "object" &&
113
+ obj !== null &&
114
+ "component" in obj &&
115
+ typeof (obj as { component: unknown }).component === "string" &&
116
+ "props" in obj &&
117
+ typeof (obj as { props: unknown }).props === "object"
118
+ );
119
+ }
@@ -0,0 +1,16 @@
1
+ import { AgentWidgetSessionStatus } from "../session";
2
+
3
+ export const statusCopy: Record<AgentWidgetSessionStatus, string> = {
4
+ idle: "Online",
5
+ connecting: "Connecting…",
6
+ connected: "Streaming…",
7
+ error: "Offline"
8
+ };
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Content Utilities
3
+ *
4
+ * Helper functions for working with multi-modal message content.
5
+ */
6
+
7
+ import type { MessageContent, ContentPart, TextContentPart, ImageContentPart, FileContentPart } from '../types';
8
+
9
+ /**
10
+ * Normalize content to ContentPart[] format.
11
+ * Converts string content to a single text content part.
12
+ */
13
+ export function normalizeContent(content: MessageContent): ContentPart[] {
14
+ if (typeof content === 'string') {
15
+ return [{ type: 'text', text: content }];
16
+ }
17
+ return content;
18
+ }
19
+
20
+ /**
21
+ * Extract display text from content parts.
22
+ * Concatenates all text parts into a single string.
23
+ */
24
+ export function getDisplayText(content: MessageContent): string {
25
+ if (typeof content === 'string') {
26
+ return content;
27
+ }
28
+ return content
29
+ .filter((part): part is TextContentPart => part.type === 'text')
30
+ .map(part => part.text)
31
+ .join('');
32
+ }
33
+
34
+ /**
35
+ * Check if content contains any images.
36
+ */
37
+ export function hasImages(content: MessageContent): boolean {
38
+ if (typeof content === 'string') {
39
+ return false;
40
+ }
41
+ return content.some(part => part.type === 'image');
42
+ }
43
+
44
+ /**
45
+ * Get all image parts from content.
46
+ */
47
+ export function getImageParts(content: MessageContent): ImageContentPart[] {
48
+ if (typeof content === 'string') {
49
+ return [];
50
+ }
51
+ return content.filter((part): part is ImageContentPart => part.type === 'image');
52
+ }
53
+
54
+ /**
55
+ * Create a text-only content part.
56
+ */
57
+ export function createTextPart(text: string): TextContentPart {
58
+ return { type: 'text', text };
59
+ }
60
+
61
+ /**
62
+ * Create an image content part from a base64 data URI or URL.
63
+ *
64
+ * @param image - Base64 data URI (data:image/...) or URL
65
+ * @param options - Optional mimeType and alt text
66
+ */
67
+ export function createImagePart(
68
+ image: string,
69
+ options?: { mimeType?: string; alt?: string }
70
+ ): ImageContentPart {
71
+ return {
72
+ type: 'image',
73
+ image,
74
+ ...(options?.mimeType && { mimeType: options.mimeType }),
75
+ ...(options?.alt && { alt: options.alt }),
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Convert a File object to an image content part.
81
+ * Reads the file and converts it to a base64 data URI.
82
+ */
83
+ export async function fileToImagePart(file: File): Promise<ImageContentPart> {
84
+ return new Promise((resolve, reject) => {
85
+ const reader = new FileReader();
86
+ reader.onload = () => {
87
+ const dataUri = reader.result as string;
88
+ resolve({
89
+ type: 'image',
90
+ image: dataUri,
91
+ mimeType: file.type,
92
+ alt: file.name,
93
+ });
94
+ };
95
+ reader.onerror = () => reject(new Error('Failed to read file'));
96
+ reader.readAsDataURL(file);
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Validate that a file is an acceptable image type.
102
+ *
103
+ * @param file - The file to validate
104
+ * @param acceptedTypes - Array of accepted MIME types (default: common image types)
105
+ * @param maxSizeBytes - Maximum file size in bytes (default: 10MB)
106
+ */
107
+ export function validateImageFile(
108
+ file: File,
109
+ acceptedTypes: string[] = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
110
+ maxSizeBytes: number = 10 * 1024 * 1024
111
+ ): { valid: boolean; error?: string } {
112
+ if (!acceptedTypes.includes(file.type)) {
113
+ return {
114
+ valid: false,
115
+ error: `Invalid file type. Accepted types: ${acceptedTypes.join(', ')}`,
116
+ };
117
+ }
118
+
119
+ if (file.size > maxSizeBytes) {
120
+ const maxSizeMB = Math.round(maxSizeBytes / (1024 * 1024));
121
+ return {
122
+ valid: false,
123
+ error: `File too large. Maximum size: ${maxSizeMB}MB`,
124
+ };
125
+ }
126
+
127
+ return { valid: true };
128
+ }
129
+
130
+ // ============================================================================
131
+ // Generic File Utilities (for PDF, TXT, DOCX, etc.)
132
+ // ============================================================================
133
+
134
+ /**
135
+ * Common image MIME types
136
+ */
137
+ export const IMAGE_MIME_TYPES = [
138
+ 'image/png',
139
+ 'image/jpeg',
140
+ 'image/gif',
141
+ 'image/webp',
142
+ 'image/svg+xml',
143
+ 'image/bmp',
144
+ ];
145
+
146
+ /**
147
+ * Common document MIME types
148
+ */
149
+ export const DOCUMENT_MIME_TYPES = [
150
+ 'application/pdf',
151
+ 'text/plain',
152
+ 'text/markdown',
153
+ 'text/csv',
154
+ 'application/msword',
155
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
156
+ 'application/vnd.ms-excel',
157
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
158
+ 'application/json',
159
+ ];
160
+
161
+ /**
162
+ * All supported file types (images + documents)
163
+ */
164
+ export const ALL_SUPPORTED_MIME_TYPES = [...IMAGE_MIME_TYPES, ...DOCUMENT_MIME_TYPES];
165
+
166
+ /**
167
+ * Check if a MIME type is an image
168
+ */
169
+ export function isImageMimeType(mimeType: string): boolean {
170
+ return IMAGE_MIME_TYPES.includes(mimeType) || mimeType.startsWith('image/');
171
+ }
172
+
173
+ /**
174
+ * Check if a file is an image
175
+ */
176
+ export function isImageFile(file: File): boolean {
177
+ return isImageMimeType(file.type);
178
+ }
179
+
180
+ /**
181
+ * Create a file content part from a base64 data URI.
182
+ */
183
+ export function createFilePart(
184
+ data: string,
185
+ mimeType: string,
186
+ filename: string
187
+ ): FileContentPart {
188
+ return {
189
+ type: 'file',
190
+ data,
191
+ mimeType,
192
+ filename,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Convert a File object to a content part.
198
+ * Returns ImageContentPart for images, FileContentPart for other files.
199
+ */
200
+ export async function fileToContentPart(file: File): Promise<ImageContentPart | FileContentPart> {
201
+ return new Promise((resolve, reject) => {
202
+ const reader = new FileReader();
203
+ reader.onload = () => {
204
+ const dataUri = reader.result as string;
205
+
206
+ if (isImageFile(file)) {
207
+ // Return image content part for images
208
+ resolve({
209
+ type: 'image',
210
+ image: dataUri,
211
+ mimeType: file.type,
212
+ alt: file.name,
213
+ });
214
+ } else {
215
+ // Return file content part for documents
216
+ resolve({
217
+ type: 'file',
218
+ data: dataUri,
219
+ mimeType: file.type,
220
+ filename: file.name,
221
+ });
222
+ }
223
+ };
224
+ reader.onerror = () => reject(new Error('Failed to read file'));
225
+ reader.readAsDataURL(file);
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Validate that a file is an acceptable type.
231
+ *
232
+ * @param file - The file to validate
233
+ * @param acceptedTypes - Array of accepted MIME types
234
+ * @param maxSizeBytes - Maximum file size in bytes (default: 10MB)
235
+ */
236
+ export function validateFile(
237
+ file: File,
238
+ acceptedTypes: string[] = ALL_SUPPORTED_MIME_TYPES,
239
+ maxSizeBytes: number = 10 * 1024 * 1024
240
+ ): { valid: boolean; error?: string } {
241
+ if (!acceptedTypes.includes(file.type)) {
242
+ return {
243
+ valid: false,
244
+ error: `Invalid file type "${file.type}". Accepted types: ${acceptedTypes.join(', ')}`,
245
+ };
246
+ }
247
+
248
+ if (file.size > maxSizeBytes) {
249
+ const maxSizeMB = Math.round(maxSizeBytes / (1024 * 1024));
250
+ return {
251
+ valid: false,
252
+ error: `File too large. Maximum size: ${maxSizeMB}MB`,
253
+ };
254
+ }
255
+
256
+ return { valid: true };
257
+ }
258
+
259
+ /**
260
+ * Get file parts from content.
261
+ */
262
+ export function getFileParts(content: MessageContent): FileContentPart[] {
263
+ if (typeof content === 'string') {
264
+ return [];
265
+ }
266
+ return content.filter((part): part is FileContentPart => part.type === 'file');
267
+ }
268
+
269
+ /**
270
+ * Check if content contains any files.
271
+ */
272
+ export function hasFiles(content: MessageContent): boolean {
273
+ if (typeof content === 'string') {
274
+ return false;
275
+ }
276
+ return content.some(part => part.type === 'file');
277
+ }
278
+
279
+ /**
280
+ * Get file extension from filename
281
+ */
282
+ export function getFileExtension(filename: string): string {
283
+ const parts = filename.split('.');
284
+ return parts.length > 1 ? parts.pop()!.toLowerCase() : '';
285
+ }
286
+
287
+ /**
288
+ * Get a display-friendly file type name
289
+ */
290
+ export function getFileTypeName(mimeType: string, filename: string): string {
291
+ const ext = getFileExtension(filename).toUpperCase();
292
+
293
+ const typeMap: Record<string, string> = {
294
+ 'application/pdf': 'PDF',
295
+ 'text/plain': 'TXT',
296
+ 'text/markdown': 'MD',
297
+ 'text/csv': 'CSV',
298
+ 'application/msword': 'DOC',
299
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
300
+ 'application/vnd.ms-excel': 'XLS',
301
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
302
+ 'application/json': 'JSON',
303
+ };
304
+
305
+ return typeMap[mimeType] || ext || 'FILE';
306
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * DOM utility functions
3
+ */
4
+ export const createElement = <K extends keyof HTMLElementTagNameMap>(
5
+ tag: K,
6
+ className?: string
7
+ ): HTMLElementTagNameMap[K] => {
8
+ const element = document.createElement(tag);
9
+ if (className) {
10
+ element.className = className;
11
+ }
12
+ return element;
13
+ };
14
+
15
+ export const createFragment = (): DocumentFragment => {
16
+ return document.createDocumentFragment();
17
+ };
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
@@ -0,0 +1,41 @@
1
+ type Handler<T> = (payload: T) => void;
2
+
3
+ export type EventUnsubscribe = () => void;
4
+
5
+ export const createEventBus = <EventMap extends Record<string, any>>() => {
6
+ const listeners = new Map<keyof EventMap, Set<Handler<any>>>();
7
+
8
+ const on = <K extends keyof EventMap>(
9
+ event: K,
10
+ handler: Handler<EventMap[K]>
11
+ ): EventUnsubscribe => {
12
+ if (!listeners.has(event)) {
13
+ listeners.set(event, new Set());
14
+ }
15
+ listeners.get(event)!.add(handler as Handler<any>);
16
+ return () => off(event, handler);
17
+ };
18
+
19
+ const off = <K extends keyof EventMap>(
20
+ event: K,
21
+ handler: Handler<EventMap[K]>
22
+ ) => {
23
+ listeners.get(event)?.delete(handler as Handler<any>);
24
+ };
25
+
26
+ const emit = <K extends keyof EventMap>(event: K, payload: EventMap[K]) => {
27
+ listeners.get(event)?.forEach((handler) => {
28
+ try {
29
+ handler(payload);
30
+ } catch (error) {
31
+ if (typeof console !== "undefined") {
32
+ // eslint-disable-next-line no-console
33
+ console.error("[AgentWidget] Event handler error:", error);
34
+ }
35
+ }
36
+ });
37
+ };
38
+
39
+ return { on, off, emit };
40
+ };
41
+