@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,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
|
+
}
|