@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,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
|
+
}
|
package/src/utils/dom.ts
ADDED
|
@@ -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
|
+
|