@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,227 @@
1
+ import type {
2
+ AgentWidgetActionContext,
3
+ AgentWidgetActionEventPayload,
4
+ AgentWidgetActionHandler,
5
+ AgentWidgetActionHandlerResult,
6
+ AgentWidgetActionParser,
7
+ AgentWidgetParsedAction,
8
+ AgentWidgetControllerEventMap,
9
+ AgentWidgetMessage
10
+ } from "../types";
11
+
12
+ type ActionManagerProcessContext = {
13
+ text: string;
14
+ message: AgentWidgetMessage;
15
+ streaming: boolean;
16
+ raw?: string;
17
+ };
18
+
19
+ type ActionManagerOptions = {
20
+ parsers: AgentWidgetActionParser[];
21
+ handlers: AgentWidgetActionHandler[];
22
+ getSessionMetadata: () => Record<string, unknown>;
23
+ updateSessionMetadata: (
24
+ updater: (prev: Record<string, unknown>) => Record<string, unknown>
25
+ ) => void;
26
+ emit: <K extends keyof AgentWidgetControllerEventMap>(
27
+ event: K,
28
+ payload: AgentWidgetControllerEventMap[K]
29
+ ) => void;
30
+ documentRef: Document | null;
31
+ };
32
+
33
+ const stripCodeFence = (value: string) => {
34
+ const match = value.match(/```(?:json)?\s*([\s\S]*?)```/i);
35
+ return match ? match[1] : value;
36
+ };
37
+
38
+ const extractJsonObject = (value: string) => {
39
+ const trimmed = value.trim();
40
+ const start = trimmed.indexOf("{");
41
+ if (start === -1) return null;
42
+
43
+ let depth = 0;
44
+ for (let i = start; i < trimmed.length; i += 1) {
45
+ const char = trimmed[i];
46
+ if (char === "{") depth += 1;
47
+ if (char === "}") {
48
+ depth -= 1;
49
+ if (depth === 0) {
50
+ return trimmed.slice(start, i + 1);
51
+ }
52
+ }
53
+ }
54
+ return null;
55
+ };
56
+
57
+ export const defaultJsonActionParser: AgentWidgetActionParser = ({ text }) => {
58
+ if (!text) return null;
59
+ if (!text.includes("{")) return null;
60
+
61
+ try {
62
+ const withoutFence = stripCodeFence(text);
63
+ const jsonBody = extractJsonObject(withoutFence);
64
+ if (!jsonBody) return null;
65
+ const parsed = JSON.parse(jsonBody);
66
+ if (!parsed || typeof parsed !== "object" || !parsed.action) {
67
+ return null;
68
+ }
69
+ const { action, ...payload } = parsed;
70
+ return {
71
+ type: String(action),
72
+ payload,
73
+ raw: parsed
74
+ };
75
+ } catch {
76
+ return null;
77
+ }
78
+ };
79
+
80
+ const asString = (value: unknown) =>
81
+ typeof value === "string" ? value : value == null ? "" : String(value);
82
+
83
+ export const defaultActionHandlers: Record<
84
+ string,
85
+ AgentWidgetActionHandler
86
+ > = {
87
+ message: (action) => {
88
+ if (action.type !== "message") return;
89
+ const text = asString((action.payload as Record<string, unknown>).text);
90
+ return {
91
+ handled: true,
92
+ displayText: text
93
+ };
94
+ },
95
+ messageAndClick: (action, context) => {
96
+ if (action.type !== "message_and_click") return;
97
+ const payload = action.payload as Record<string, unknown>;
98
+ const selector = asString(payload.element);
99
+ if (selector && context.document?.querySelector) {
100
+ const element = context.document.querySelector<HTMLElement>(selector);
101
+ if (element) {
102
+ setTimeout(() => {
103
+ element.click();
104
+ }, 400);
105
+ } else if (typeof console !== "undefined") {
106
+ // eslint-disable-next-line no-console
107
+ console.warn("[AgentWidget] Element not found for selector:", selector);
108
+ }
109
+ }
110
+ return {
111
+ handled: true,
112
+ displayText: asString(payload.text)
113
+ };
114
+ }
115
+ };
116
+
117
+ const ensureArrayOfStrings = (value: unknown): string[] => {
118
+ if (Array.isArray(value)) {
119
+ return value.map((entry) => String(entry));
120
+ }
121
+ return [];
122
+ };
123
+
124
+ export const createActionManager = (options: ActionManagerOptions) => {
125
+ let processedIds = new Set(
126
+ ensureArrayOfStrings(options.getSessionMetadata().processedActionMessageIds)
127
+ );
128
+
129
+ const syncFromMetadata = () => {
130
+ processedIds = new Set(
131
+ ensureArrayOfStrings(options.getSessionMetadata().processedActionMessageIds)
132
+ );
133
+ };
134
+
135
+ const persistProcessedIds = () => {
136
+ const latestIds = Array.from(processedIds);
137
+ options.updateSessionMetadata((prev) => ({
138
+ ...prev,
139
+ processedActionMessageIds: latestIds
140
+ }));
141
+ };
142
+
143
+ const process = (context: ActionManagerProcessContext): { text: string; persist: boolean } | null => {
144
+ if (
145
+ context.streaming ||
146
+ context.message.role !== "assistant" ||
147
+ !context.text ||
148
+ processedIds.has(context.message.id)
149
+ ) {
150
+ return null;
151
+ }
152
+
153
+ const parseSource =
154
+ (typeof context.raw === "string" && context.raw) ||
155
+ (typeof context.message.rawContent === "string" &&
156
+ context.message.rawContent) ||
157
+ (typeof context.text === "string" && context.text) ||
158
+ null;
159
+
160
+ if (
161
+ !parseSource &&
162
+ typeof context.text === "string" &&
163
+ context.text.trim().startsWith("{") &&
164
+ typeof console !== "undefined"
165
+ ) {
166
+ // eslint-disable-next-line no-console
167
+ console.warn(
168
+ "[AgentWidget] Structured response detected but no raw payload was provided. Ensure your stream parser returns { text, raw }."
169
+ );
170
+ }
171
+
172
+ const action = parseSource
173
+ ? options.parsers.reduce<AgentWidgetParsedAction | null>(
174
+ (acc, parser) =>
175
+ acc || parser?.({ text: parseSource, message: context.message }) || null,
176
+ null
177
+ )
178
+ : null;
179
+
180
+ if (!action) {
181
+ return null;
182
+ }
183
+
184
+ processedIds.add(context.message.id);
185
+ persistProcessedIds();
186
+
187
+ const eventPayload: AgentWidgetActionEventPayload = {
188
+ action,
189
+ message: context.message
190
+ };
191
+ options.emit("action:detected", eventPayload);
192
+
193
+ for (const handler of options.handlers) {
194
+ if (!handler) continue;
195
+ try {
196
+ const handlerResult = handler(action, {
197
+ message: context.message,
198
+ metadata: options.getSessionMetadata(),
199
+ updateMetadata: options.updateSessionMetadata,
200
+ document: options.documentRef
201
+ } as AgentWidgetActionContext) as AgentWidgetActionHandlerResult | void;
202
+
203
+ if (!handlerResult) continue;
204
+
205
+ if (handlerResult.handled) {
206
+ // persistMessage defaults to true if not specified
207
+ const persist = handlerResult.persistMessage !== false;
208
+ const displayText = handlerResult.displayText !== undefined ? handlerResult.displayText : "";
209
+ return { text: displayText, persist };
210
+ }
211
+ } catch (error) {
212
+ if (typeof console !== "undefined") {
213
+ // eslint-disable-next-line no-console
214
+ console.error("[AgentWidget] Action handler error:", error);
215
+ }
216
+ }
217
+ }
218
+
219
+ return { text: "", persist: true };
220
+ };
221
+
222
+ return {
223
+ process,
224
+ syncFromMetadata
225
+ };
226
+ };
227
+
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Attachment Manager
3
+ *
4
+ * Handles file selection, validation, preview generation, and content part creation
5
+ * for the composer attachment feature. Supports both images and documents.
6
+ */
7
+
8
+ import { createElement } from "./dom";
9
+ import { renderLucideIcon } from "./icons";
10
+ import type {
11
+ AgentWidgetAttachmentsConfig,
12
+ ContentPart,
13
+ ImageContentPart,
14
+ FileContentPart
15
+ } from "../types";
16
+ import {
17
+ fileToContentPart,
18
+ validateFile,
19
+ isImageFile,
20
+ getFileTypeName,
21
+ ALL_SUPPORTED_MIME_TYPES
22
+ } from "./content";
23
+
24
+ /**
25
+ * Pending attachment with preview
26
+ */
27
+ export interface PendingAttachment {
28
+ id: string;
29
+ file: File;
30
+ previewUrl: string | null; // null for non-image files
31
+ contentPart: ImageContentPart | FileContentPart;
32
+ }
33
+
34
+ /**
35
+ * Attachment manager configuration
36
+ */
37
+ export interface AttachmentManagerConfig {
38
+ allowedTypes?: string[];
39
+ maxFileSize?: number;
40
+ maxFiles?: number;
41
+ onFileRejected?: (file: File, reason: "type" | "size" | "count") => void;
42
+ onAttachmentsChange?: (attachments: PendingAttachment[]) => void;
43
+ }
44
+
45
+ /**
46
+ * Default configuration values
47
+ */
48
+ const DEFAULTS = {
49
+ allowedTypes: ALL_SUPPORTED_MIME_TYPES,
50
+ maxFileSize: 10 * 1024 * 1024, // 10MB
51
+ maxFiles: 4
52
+ };
53
+
54
+ /**
55
+ * Generate a unique ID for attachments
56
+ */
57
+ function generateAttachmentId(): string {
58
+ return `attach_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
59
+ }
60
+
61
+ /**
62
+ * Get the appropriate Lucide icon name for a file type
63
+ */
64
+ function getFileIconName(mimeType: string): string {
65
+ if (mimeType === 'application/pdf') return 'file-text';
66
+ if (mimeType.startsWith('text/')) return 'file-text';
67
+ if (mimeType.includes('word')) return 'file-text';
68
+ if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'file-spreadsheet';
69
+ if (mimeType === 'application/json') return 'file-json';
70
+ return 'file';
71
+ }
72
+
73
+ /**
74
+ * Creates and manages attachments for the composer
75
+ */
76
+ export class AttachmentManager {
77
+ private attachments: PendingAttachment[] = [];
78
+ private config: Required<
79
+ Pick<AttachmentManagerConfig, "allowedTypes" | "maxFileSize" | "maxFiles">
80
+ > &
81
+ Pick<AttachmentManagerConfig, "onFileRejected" | "onAttachmentsChange">;
82
+ private previewsContainer: HTMLElement | null = null;
83
+
84
+ constructor(config: AttachmentManagerConfig = {}) {
85
+ this.config = {
86
+ allowedTypes: config.allowedTypes ?? DEFAULTS.allowedTypes,
87
+ maxFileSize: config.maxFileSize ?? DEFAULTS.maxFileSize,
88
+ maxFiles: config.maxFiles ?? DEFAULTS.maxFiles,
89
+ onFileRejected: config.onFileRejected,
90
+ onAttachmentsChange: config.onAttachmentsChange
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Set the previews container element
96
+ */
97
+ setPreviewsContainer(container: HTMLElement | null): void {
98
+ this.previewsContainer = container;
99
+ }
100
+
101
+ /**
102
+ * Update the configuration (e.g., when allowed types change)
103
+ */
104
+ updateConfig(config: Partial<AttachmentManagerConfig>): void {
105
+ if (config.allowedTypes !== undefined) {
106
+ this.config.allowedTypes = config.allowedTypes.length > 0 ? config.allowedTypes : DEFAULTS.allowedTypes;
107
+ }
108
+ if (config.maxFileSize !== undefined) {
109
+ this.config.maxFileSize = config.maxFileSize;
110
+ }
111
+ if (config.maxFiles !== undefined) {
112
+ this.config.maxFiles = config.maxFiles;
113
+ }
114
+ if (config.onFileRejected !== undefined) {
115
+ this.config.onFileRejected = config.onFileRejected;
116
+ }
117
+ if (config.onAttachmentsChange !== undefined) {
118
+ this.config.onAttachmentsChange = config.onAttachmentsChange;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Get current attachments
124
+ */
125
+ getAttachments(): PendingAttachment[] {
126
+ return [...this.attachments];
127
+ }
128
+
129
+ /**
130
+ * Get content parts for all attachments
131
+ */
132
+ getContentParts(): ContentPart[] {
133
+ return this.attachments.map((a) => a.contentPart);
134
+ }
135
+
136
+ /**
137
+ * Check if there are any attachments
138
+ */
139
+ hasAttachments(): boolean {
140
+ return this.attachments.length > 0;
141
+ }
142
+
143
+ /**
144
+ * Get the number of attachments
145
+ */
146
+ count(): number {
147
+ return this.attachments.length;
148
+ }
149
+
150
+ /**
151
+ * Handle file input change event
152
+ */
153
+ async handleFileSelect(files: FileList | null): Promise<void> {
154
+ if (!files || files.length === 0) return;
155
+
156
+ const filesToProcess = Array.from(files);
157
+
158
+ for (const file of filesToProcess) {
159
+ // Check if we've hit the max files limit
160
+ if (this.attachments.length >= this.config.maxFiles) {
161
+ this.config.onFileRejected?.(file, "count");
162
+ continue;
163
+ }
164
+
165
+ // Validate the file
166
+ const validation = validateFile(
167
+ file,
168
+ this.config.allowedTypes,
169
+ this.config.maxFileSize
170
+ );
171
+
172
+ if (!validation.valid) {
173
+ const reason = validation.error?.includes("type") ? "type" : "size";
174
+ this.config.onFileRejected?.(file, reason);
175
+ continue;
176
+ }
177
+
178
+ try {
179
+ // Convert to content part (handles both images and files)
180
+ const contentPart = await fileToContentPart(file);
181
+
182
+ // Create preview URL only for images
183
+ const previewUrl = isImageFile(file) ? URL.createObjectURL(file) : null;
184
+
185
+ const attachment: PendingAttachment = {
186
+ id: generateAttachmentId(),
187
+ file,
188
+ previewUrl,
189
+ contentPart
190
+ };
191
+
192
+ this.attachments.push(attachment);
193
+ this.renderPreview(attachment);
194
+ } catch (error) {
195
+ console.error("[AttachmentManager] Failed to process file:", error);
196
+ }
197
+ }
198
+
199
+ this.updatePreviewsVisibility();
200
+ this.config.onAttachmentsChange?.(this.getAttachments());
201
+ }
202
+
203
+ /**
204
+ * Remove an attachment by ID
205
+ */
206
+ removeAttachment(id: string): void {
207
+ const index = this.attachments.findIndex((a) => a.id === id);
208
+ if (index === -1) return;
209
+
210
+ const attachment = this.attachments[index];
211
+
212
+ // Revoke the object URL to free memory (only for images)
213
+ if (attachment.previewUrl) {
214
+ URL.revokeObjectURL(attachment.previewUrl);
215
+ }
216
+
217
+ // Remove from array
218
+ this.attachments.splice(index, 1);
219
+
220
+ // Remove from DOM
221
+ const previewEl = this.previewsContainer?.querySelector(
222
+ `[data-attachment-id="${id}"]`
223
+ );
224
+ if (previewEl) {
225
+ previewEl.remove();
226
+ }
227
+
228
+ this.updatePreviewsVisibility();
229
+ this.config.onAttachmentsChange?.(this.getAttachments());
230
+ }
231
+
232
+ /**
233
+ * Clear all attachments
234
+ */
235
+ clearAttachments(): void {
236
+ // Revoke all object URLs
237
+ for (const attachment of this.attachments) {
238
+ if (attachment.previewUrl) {
239
+ URL.revokeObjectURL(attachment.previewUrl);
240
+ }
241
+ }
242
+
243
+ this.attachments = [];
244
+
245
+ // Clear the previews container
246
+ if (this.previewsContainer) {
247
+ this.previewsContainer.innerHTML = "";
248
+ }
249
+
250
+ this.updatePreviewsVisibility();
251
+ this.config.onAttachmentsChange?.(this.getAttachments());
252
+ }
253
+
254
+ /**
255
+ * Render a preview for an attachment (image thumbnail or file icon)
256
+ */
257
+ private renderPreview(attachment: PendingAttachment): void {
258
+ if (!this.previewsContainer) return;
259
+
260
+ const isImage = isImageFile(attachment.file);
261
+
262
+ const previewWrapper = createElement(
263
+ "div",
264
+ "tvw-attachment-preview tvw-relative tvw-inline-block"
265
+ );
266
+ previewWrapper.setAttribute("data-attachment-id", attachment.id);
267
+ previewWrapper.style.width = "48px";
268
+ previewWrapper.style.height = "48px";
269
+
270
+ if (isImage && attachment.previewUrl) {
271
+ // Render image thumbnail
272
+ const img = createElement("img") as HTMLImageElement;
273
+ img.src = attachment.previewUrl;
274
+ img.alt = attachment.file.name;
275
+ img.className =
276
+ "tvw-w-full tvw-h-full tvw-object-cover tvw-rounded-lg tvw-border tvw-border-gray-200";
277
+ img.style.width = "48px";
278
+ img.style.height = "48px";
279
+ img.style.objectFit = "cover";
280
+ img.style.borderRadius = "8px";
281
+ previewWrapper.appendChild(img);
282
+ } else {
283
+ // Render file icon with type label
284
+ const filePreview = createElement("div");
285
+ filePreview.style.width = "48px";
286
+ filePreview.style.height = "48px";
287
+ filePreview.style.borderRadius = "8px";
288
+ filePreview.style.backgroundColor = "var(--cw-container, #f3f4f6)";
289
+ filePreview.style.border = "1px solid var(--cw-border, #e5e7eb)";
290
+ filePreview.style.display = "flex";
291
+ filePreview.style.flexDirection = "column";
292
+ filePreview.style.alignItems = "center";
293
+ filePreview.style.justifyContent = "center";
294
+ filePreview.style.gap = "2px";
295
+ filePreview.style.overflow = "hidden";
296
+
297
+ // File icon
298
+ const iconName = getFileIconName(attachment.file.type);
299
+ const fileIcon = renderLucideIcon(iconName, 20, "var(--cw-muted, #6b7280)", 1.5);
300
+ if (fileIcon) {
301
+ filePreview.appendChild(fileIcon);
302
+ }
303
+
304
+ // File type label
305
+ const typeLabel = createElement("span");
306
+ typeLabel.textContent = getFileTypeName(attachment.file.type, attachment.file.name);
307
+ typeLabel.style.fontSize = "8px";
308
+ typeLabel.style.fontWeight = "600";
309
+ typeLabel.style.color = "var(--cw-muted, #6b7280)";
310
+ typeLabel.style.textTransform = "uppercase";
311
+ typeLabel.style.lineHeight = "1";
312
+ filePreview.appendChild(typeLabel);
313
+
314
+ previewWrapper.appendChild(filePreview);
315
+ }
316
+
317
+ // Create remove button
318
+ const removeBtn = createElement(
319
+ "button",
320
+ "tvw-attachment-remove tvw-absolute tvw-flex tvw-items-center tvw-justify-center"
321
+ ) as HTMLButtonElement;
322
+ removeBtn.type = "button";
323
+ removeBtn.setAttribute("aria-label", "Remove attachment");
324
+ removeBtn.style.position = "absolute";
325
+ removeBtn.style.top = "-4px";
326
+ removeBtn.style.right = "-4px";
327
+ removeBtn.style.width = "18px";
328
+ removeBtn.style.height = "18px";
329
+ removeBtn.style.borderRadius = "50%";
330
+ removeBtn.style.backgroundColor = "rgba(0, 0, 0, 0.6)";
331
+ removeBtn.style.border = "none";
332
+ removeBtn.style.cursor = "pointer";
333
+ removeBtn.style.display = "flex";
334
+ removeBtn.style.alignItems = "center";
335
+ removeBtn.style.justifyContent = "center";
336
+ removeBtn.style.padding = "0";
337
+
338
+ // Add X icon
339
+ const xIcon = renderLucideIcon("x", 10, "#ffffff", 2);
340
+ if (xIcon) {
341
+ removeBtn.appendChild(xIcon);
342
+ } else {
343
+ removeBtn.textContent = "×";
344
+ removeBtn.style.color = "#ffffff";
345
+ removeBtn.style.fontSize = "14px";
346
+ removeBtn.style.lineHeight = "1";
347
+ }
348
+
349
+ // Remove on click
350
+ removeBtn.addEventListener("click", (e) => {
351
+ e.preventDefault();
352
+ e.stopPropagation();
353
+ this.removeAttachment(attachment.id);
354
+ });
355
+
356
+ previewWrapper.appendChild(removeBtn);
357
+ this.previewsContainer.appendChild(previewWrapper);
358
+ }
359
+
360
+ /**
361
+ * Update the visibility of the previews container
362
+ */
363
+ private updatePreviewsVisibility(): void {
364
+ if (!this.previewsContainer) return;
365
+ this.previewsContainer.style.display =
366
+ this.attachments.length > 0 ? "flex" : "none";
367
+ }
368
+
369
+ /**
370
+ * Create an AttachmentManager from widget config
371
+ */
372
+ static fromConfig(
373
+ config?: AgentWidgetAttachmentsConfig,
374
+ onAttachmentsChange?: (attachments: PendingAttachment[]) => void
375
+ ): AttachmentManager {
376
+ return new AttachmentManager({
377
+ allowedTypes: config?.allowedTypes,
378
+ maxFileSize: config?.maxFileSize,
379
+ maxFiles: config?.maxFiles,
380
+ onFileRejected: config?.onFileRejected,
381
+ onAttachmentsChange
382
+ });
383
+ }
384
+ }