@bootdesk/js-web-adapter-react 0.1.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 +93 -0
- package/dist/index.cjs +2536 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +395 -0
- package/dist/index.d.ts +395 -0
- package/dist/index.js +2466 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +1 -0
- package/package.json +64 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2536 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AttachmentList: () => AttachmentList,
|
|
34
|
+
CardProvider: () => CardProvider,
|
|
35
|
+
CardRenderer: () => CardRenderer,
|
|
36
|
+
ChatProvider: () => ChatProvider,
|
|
37
|
+
ChatWidget: () => ChatWidget,
|
|
38
|
+
DefaultCard: () => DefaultCard,
|
|
39
|
+
Dropzone: () => Dropzone,
|
|
40
|
+
ErrorBoundary: () => ErrorBoundary,
|
|
41
|
+
FileCardComponent: () => FileCardComponent,
|
|
42
|
+
FloatingButton: () => FloatingButton,
|
|
43
|
+
Header: () => Header,
|
|
44
|
+
ImageCardComponent: () => ImageCardComponent,
|
|
45
|
+
InputArea: () => InputArea,
|
|
46
|
+
LaravelEchoBroadcastClient: () => import_js_web_adapter_core4.LaravelEchoBroadcastClient,
|
|
47
|
+
LocaleProvider: () => LocaleProvider,
|
|
48
|
+
MarkdownRenderer: () => MarkdownRenderer,
|
|
49
|
+
MessageContent: () => MessageContent,
|
|
50
|
+
MessageList: () => MessageList,
|
|
51
|
+
PushManager: () => import_js_web_adapter_core3.PushManager,
|
|
52
|
+
PushPermissionPrompt: () => PushPermissionPrompt,
|
|
53
|
+
PushToggle: () => PushToggle,
|
|
54
|
+
PusherBroadcastClient: () => import_js_web_adapter_core4.PusherBroadcastClient,
|
|
55
|
+
TypingIndicator: () => TypingIndicator,
|
|
56
|
+
WebChatClient: () => import_js_web_adapter_core4.WebChatClient,
|
|
57
|
+
createPushSubscriptionHandlers: () => import_js_web_adapter_core3.createPushSubscriptionHandlers,
|
|
58
|
+
getAvailableLocales: () => getAvailableLocales,
|
|
59
|
+
registerLocale: () => registerLocale,
|
|
60
|
+
renderMarkdown: () => renderMarkdown,
|
|
61
|
+
useAttachmentUpload: () => useAttachmentUpload,
|
|
62
|
+
useCardRegistry: () => useCardRegistry,
|
|
63
|
+
useCardRendererRegistry: () => useCardRegistry,
|
|
64
|
+
useChatClient: () => useChatClient,
|
|
65
|
+
useChatContext: () => useChatContext,
|
|
66
|
+
useLocale: () => useLocale,
|
|
67
|
+
useMessages: () => useMessages,
|
|
68
|
+
usePushNotifications: () => usePushNotifications,
|
|
69
|
+
useStreaming: () => useStreaming,
|
|
70
|
+
useTyping: () => useTyping
|
|
71
|
+
});
|
|
72
|
+
module.exports = __toCommonJS(index_exports);
|
|
73
|
+
|
|
74
|
+
// src/components/ChatWidget.tsx
|
|
75
|
+
var import_react13 = require("react");
|
|
76
|
+
|
|
77
|
+
// src/hooks/useChatClient.ts
|
|
78
|
+
var import_react = require("react");
|
|
79
|
+
var import_js_web_adapter_core = require("@bootdesk/js-web-adapter-core");
|
|
80
|
+
function useChatClient(config) {
|
|
81
|
+
const client = (0, import_react.useMemo)(() => new import_js_web_adapter_core.WebChatClient(config), [config]);
|
|
82
|
+
(0, import_react.useEffect)(() => {
|
|
83
|
+
client.connect().catch((error) => {
|
|
84
|
+
console.error("Failed to connect chat client:", error);
|
|
85
|
+
});
|
|
86
|
+
return () => {
|
|
87
|
+
client.disconnect();
|
|
88
|
+
};
|
|
89
|
+
}, [client]);
|
|
90
|
+
return client;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/hooks/useMessages.ts
|
|
94
|
+
var import_react2 = require("react");
|
|
95
|
+
function useMessages(client, enabled = true) {
|
|
96
|
+
const [messages, setMessages] = (0, import_react2.useState)([]);
|
|
97
|
+
const [loading, setLoading] = (0, import_react2.useState)(false);
|
|
98
|
+
const [isLoadingHistory, setIsLoadingHistory] = (0, import_react2.useState)(false);
|
|
99
|
+
const [hasMore, setHasMore] = (0, import_react2.useState)(false);
|
|
100
|
+
const [nextCursor, setNextCursor] = (0, import_react2.useState)(void 0);
|
|
101
|
+
const [loadError, setLoadError] = (0, import_react2.useState)(null);
|
|
102
|
+
const abortRef = (0, import_react2.useRef)(null);
|
|
103
|
+
(0, import_react2.useEffect)(() => {
|
|
104
|
+
if (!enabled) return;
|
|
105
|
+
if (abortRef.current) {
|
|
106
|
+
abortRef.current.abort();
|
|
107
|
+
}
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
abortRef.current = controller;
|
|
110
|
+
const { signal } = controller;
|
|
111
|
+
setIsLoadingHistory(true);
|
|
112
|
+
setLoadError(null);
|
|
113
|
+
const loadInitial = async () => {
|
|
114
|
+
try {
|
|
115
|
+
const result = await client.loadMessages({ limit: 50, skipStateSeed: true }, signal);
|
|
116
|
+
if (signal.aborted) return;
|
|
117
|
+
setMessages(result.messages);
|
|
118
|
+
setHasMore(result.hasMore);
|
|
119
|
+
setNextCursor(result.nextCursor);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (signal.aborted) return;
|
|
122
|
+
setLoadError(error instanceof Error ? error : new Error("Failed to load messages"));
|
|
123
|
+
} finally {
|
|
124
|
+
if (!signal.aborted) {
|
|
125
|
+
setIsLoadingHistory(false);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
loadInitial();
|
|
130
|
+
return () => {
|
|
131
|
+
controller.abort();
|
|
132
|
+
if (abortRef.current === controller) {
|
|
133
|
+
abortRef.current = null;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, [client, enabled]);
|
|
137
|
+
const reloadMessages = (0, import_react2.useCallback)(async () => {
|
|
138
|
+
setIsLoadingHistory(true);
|
|
139
|
+
setLoadError(null);
|
|
140
|
+
try {
|
|
141
|
+
const result = await client.loadMessages({ limit: 50, skipStateSeed: true });
|
|
142
|
+
setMessages(result.messages);
|
|
143
|
+
setHasMore(result.hasMore);
|
|
144
|
+
setNextCursor(result.nextCursor);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
setLoadError(error instanceof Error ? error : new Error("Failed to reload messages"));
|
|
147
|
+
} finally {
|
|
148
|
+
setIsLoadingHistory(false);
|
|
149
|
+
}
|
|
150
|
+
}, [client]);
|
|
151
|
+
const retryLoad = (0, import_react2.useCallback)(async () => {
|
|
152
|
+
setLoadError(null);
|
|
153
|
+
setIsLoadingHistory(true);
|
|
154
|
+
try {
|
|
155
|
+
const result = await client.loadMessages({ limit: 50, skipStateSeed: true });
|
|
156
|
+
setMessages(result.messages);
|
|
157
|
+
setHasMore(result.hasMore);
|
|
158
|
+
setNextCursor(result.nextCursor);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
setLoadError(error instanceof Error ? error : new Error("Failed to load messages"));
|
|
161
|
+
} finally {
|
|
162
|
+
setIsLoadingHistory(false);
|
|
163
|
+
}
|
|
164
|
+
}, [client]);
|
|
165
|
+
(0, import_react2.useEffect)(() => {
|
|
166
|
+
const unsubscribes = [];
|
|
167
|
+
unsubscribes.push(
|
|
168
|
+
client.addEventListener("message:added", (message) => {
|
|
169
|
+
setMessages((prev) => {
|
|
170
|
+
if (prev.some((m) => m.id === message.id)) return prev;
|
|
171
|
+
return [...prev, message];
|
|
172
|
+
});
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
const features2 = client.getFeatures();
|
|
176
|
+
if (features2.editMessages) {
|
|
177
|
+
unsubscribes.push(
|
|
178
|
+
client.addEventListener(
|
|
179
|
+
"message:edited",
|
|
180
|
+
({ messageId, newText }) => {
|
|
181
|
+
setMessages(
|
|
182
|
+
(prev) => prev.map(
|
|
183
|
+
(msg) => msg.id === messageId ? { ...msg, content: { ...msg.content, text: newText } } : msg
|
|
184
|
+
)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
if (features2.deleteMessages) {
|
|
191
|
+
unsubscribes.push(
|
|
192
|
+
client.addEventListener("message:deleted", ({ messageId }) => {
|
|
193
|
+
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
|
194
|
+
})
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (features2.reactions) {
|
|
198
|
+
unsubscribes.push(
|
|
199
|
+
client.addEventListener(
|
|
200
|
+
"reaction:added",
|
|
201
|
+
({ messageId, emoji }) => {
|
|
202
|
+
setMessages(
|
|
203
|
+
(prev) => prev.map((msg) => {
|
|
204
|
+
if (msg.id !== messageId || !msg.reactions) return msg;
|
|
205
|
+
const existing = msg.reactions.find((r) => r.emoji === emoji);
|
|
206
|
+
if (existing) {
|
|
207
|
+
return {
|
|
208
|
+
...msg,
|
|
209
|
+
reactions: msg.reactions.map(
|
|
210
|
+
(r) => r.emoji === emoji ? { ...r, count: r.count + 1 } : r
|
|
211
|
+
)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
...msg,
|
|
216
|
+
reactions: [...msg.reactions, { emoji, count: 1, users: [] }]
|
|
217
|
+
};
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
unsubscribes.push(
|
|
224
|
+
client.addEventListener(
|
|
225
|
+
"reaction:removed",
|
|
226
|
+
({ messageId, emoji }) => {
|
|
227
|
+
setMessages(
|
|
228
|
+
(prev) => prev.map((msg) => {
|
|
229
|
+
if (msg.id !== messageId || !msg.reactions) return msg;
|
|
230
|
+
const idx = msg.reactions.findIndex((r) => r.emoji === emoji);
|
|
231
|
+
if (idx === -1) return msg;
|
|
232
|
+
const updated = [...msg.reactions];
|
|
233
|
+
if (updated[idx].count <= 1) {
|
|
234
|
+
updated.splice(idx, 1);
|
|
235
|
+
} else {
|
|
236
|
+
updated[idx] = { ...updated[idx], count: updated[idx].count - 1 };
|
|
237
|
+
}
|
|
238
|
+
return { ...msg, reactions: updated };
|
|
239
|
+
})
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return () => {
|
|
246
|
+
unsubscribes.forEach((unsub) => unsub());
|
|
247
|
+
};
|
|
248
|
+
}, [client]);
|
|
249
|
+
const sendMessage = (0, import_react2.useCallback)(
|
|
250
|
+
async (text, attachments) => {
|
|
251
|
+
setLoading(true);
|
|
252
|
+
try {
|
|
253
|
+
await client.sendMessage(text, attachments || []);
|
|
254
|
+
} finally {
|
|
255
|
+
setLoading(false);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
[client]
|
|
259
|
+
);
|
|
260
|
+
const editMessage = (0, import_react2.useCallback)(
|
|
261
|
+
async (id, text) => {
|
|
262
|
+
if (!client.getFeatures().editMessages) {
|
|
263
|
+
throw new Error("Edit messages not enabled. Set features.editMessages = true.");
|
|
264
|
+
}
|
|
265
|
+
const endpoint = client.getEndpoints().editMessage || "/api/chat/messages/{id}/edit";
|
|
266
|
+
await client.getHttpClient().editMessage(id, text, endpoint);
|
|
267
|
+
setMessages(
|
|
268
|
+
(prev) => prev.map((msg) => msg.id === id ? { ...msg, content: { ...msg.content, text } } : msg)
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
[client]
|
|
272
|
+
);
|
|
273
|
+
const deleteMessage = (0, import_react2.useCallback)(
|
|
274
|
+
async (id) => {
|
|
275
|
+
if (!client.getFeatures().deleteMessages) {
|
|
276
|
+
throw new Error("Delete messages not enabled. Set features.deleteMessages = true.");
|
|
277
|
+
}
|
|
278
|
+
const endpoint = client.getEndpoints().deleteMessage || "/api/chat/messages/{id}";
|
|
279
|
+
await client.getHttpClient().deleteMessage(id, endpoint);
|
|
280
|
+
setMessages((prev) => prev.filter((msg) => msg.id !== id));
|
|
281
|
+
},
|
|
282
|
+
[client]
|
|
283
|
+
);
|
|
284
|
+
const addReaction = (0, import_react2.useCallback)(
|
|
285
|
+
async (id, emoji) => {
|
|
286
|
+
await client.addReaction(id, emoji);
|
|
287
|
+
},
|
|
288
|
+
[client]
|
|
289
|
+
);
|
|
290
|
+
const removeReaction = (0, import_react2.useCallback)(
|
|
291
|
+
async (id, emoji) => {
|
|
292
|
+
await client.removeReaction(id, emoji);
|
|
293
|
+
},
|
|
294
|
+
[client]
|
|
295
|
+
);
|
|
296
|
+
const loadMore = (0, import_react2.useCallback)(async () => {
|
|
297
|
+
if (!nextCursor || isLoadingHistory) return;
|
|
298
|
+
setIsLoadingHistory(true);
|
|
299
|
+
try {
|
|
300
|
+
const result = await client.loadMessages({
|
|
301
|
+
limit: 50,
|
|
302
|
+
before: nextCursor
|
|
303
|
+
});
|
|
304
|
+
setMessages((prev) => [...result.messages, ...prev]);
|
|
305
|
+
setHasMore(result.hasMore);
|
|
306
|
+
setNextCursor(result.nextCursor);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error("Failed to load more messages:", error);
|
|
309
|
+
} finally {
|
|
310
|
+
setIsLoadingHistory(false);
|
|
311
|
+
}
|
|
312
|
+
}, [client, nextCursor, isLoadingHistory]);
|
|
313
|
+
const features = client.getFeatures();
|
|
314
|
+
const canEdit = !!features.editMessages;
|
|
315
|
+
const canDelete = !!features.deleteMessages;
|
|
316
|
+
const canReact = !!features.reactions;
|
|
317
|
+
return {
|
|
318
|
+
messages,
|
|
319
|
+
loading,
|
|
320
|
+
isLoadingHistory,
|
|
321
|
+
hasMore,
|
|
322
|
+
loadMore,
|
|
323
|
+
reloadMessages,
|
|
324
|
+
retryLoad,
|
|
325
|
+
loadError,
|
|
326
|
+
canEdit,
|
|
327
|
+
canDelete,
|
|
328
|
+
canReact,
|
|
329
|
+
sendMessage,
|
|
330
|
+
editMessage,
|
|
331
|
+
deleteMessage,
|
|
332
|
+
addReaction,
|
|
333
|
+
removeReaction
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/hooks/useStreaming.ts
|
|
338
|
+
var import_react3 = require("react");
|
|
339
|
+
function useStreaming(client) {
|
|
340
|
+
const [streamingMessages, setStreamingMessages] = (0, import_react3.useState)(
|
|
341
|
+
/* @__PURE__ */ new Map()
|
|
342
|
+
);
|
|
343
|
+
(0, import_react3.useEffect)(() => {
|
|
344
|
+
const unsub = client.onStreamingChunk((event) => {
|
|
345
|
+
setStreamingMessages((prev) => {
|
|
346
|
+
const next = new Map(prev);
|
|
347
|
+
const existing = next.get(event.messageId);
|
|
348
|
+
const newText = (existing?.fullText || "") + event.chunk;
|
|
349
|
+
if (event.isFinal) {
|
|
350
|
+
next.delete(event.messageId);
|
|
351
|
+
} else {
|
|
352
|
+
next.set(event.messageId, {
|
|
353
|
+
messageId: event.messageId,
|
|
354
|
+
fullText: newText,
|
|
355
|
+
isComplete: event.isFinal
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return next;
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
return unsub;
|
|
362
|
+
}, [client]);
|
|
363
|
+
const isStreaming = streamingMessages.size > 0;
|
|
364
|
+
return { streamingMessages, isStreaming };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/hooks/useTyping.ts
|
|
368
|
+
var import_react4 = require("react");
|
|
369
|
+
function useTyping(client) {
|
|
370
|
+
const [typingUsers, setTypingUsers] = (0, import_react4.useState)(/* @__PURE__ */ new Set());
|
|
371
|
+
const timeoutsRef = (0, import_react4.useRef)(/* @__PURE__ */ new Map());
|
|
372
|
+
(0, import_react4.useEffect)(() => {
|
|
373
|
+
const unsub = client.onTypingStarted((event) => {
|
|
374
|
+
const existing = timeoutsRef.current.get(event.userId);
|
|
375
|
+
if (existing) clearTimeout(existing);
|
|
376
|
+
setTypingUsers((prev) => new Set(prev).add(event.userId));
|
|
377
|
+
const timeoutId = setTimeout(() => {
|
|
378
|
+
timeoutsRef.current.delete(event.userId);
|
|
379
|
+
setTypingUsers((prev) => {
|
|
380
|
+
const next = new Set(prev);
|
|
381
|
+
next.delete(event.userId);
|
|
382
|
+
return next;
|
|
383
|
+
});
|
|
384
|
+
}, 3e3);
|
|
385
|
+
timeoutsRef.current.set(event.userId, timeoutId);
|
|
386
|
+
});
|
|
387
|
+
return () => {
|
|
388
|
+
unsub();
|
|
389
|
+
for (const id of timeoutsRef.current.values()) {
|
|
390
|
+
clearTimeout(id);
|
|
391
|
+
}
|
|
392
|
+
timeoutsRef.current.clear();
|
|
393
|
+
};
|
|
394
|
+
}, [client]);
|
|
395
|
+
const isSomeoneTyping = typingUsers.size > 0;
|
|
396
|
+
return { typingUsers, isSomeoneTyping };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/hooks/usePushNotifications.ts
|
|
400
|
+
var import_react5 = require("react");
|
|
401
|
+
var import_js_web_adapter_core2 = require("@bootdesk/js-web-adapter-core");
|
|
402
|
+
function usePushNotifications(options) {
|
|
403
|
+
const { enabled = false } = options;
|
|
404
|
+
const [status, setStatus] = (0, import_react5.useState)("unsupported");
|
|
405
|
+
const pushManagerRef = (0, import_react5.useRef)(null);
|
|
406
|
+
(0, import_react5.useEffect)(() => {
|
|
407
|
+
if (!enabled) return;
|
|
408
|
+
const pushManager = new import_js_web_adapter_core2.PushManager({
|
|
409
|
+
getVapidPublicKey: options.getVapidPublicKey,
|
|
410
|
+
onSubscribe: options.onSubscribe,
|
|
411
|
+
onUnsubscribe: options.onUnsubscribe,
|
|
412
|
+
serviceWorkerUrl: options.serviceWorkerUrl,
|
|
413
|
+
notificationOptions: options.notificationOptions
|
|
414
|
+
});
|
|
415
|
+
pushManagerRef.current = pushManager;
|
|
416
|
+
const unsubscribeStatus = pushManager.onStatusChange(setStatus);
|
|
417
|
+
pushManager.initialize();
|
|
418
|
+
return () => {
|
|
419
|
+
unsubscribeStatus();
|
|
420
|
+
};
|
|
421
|
+
}, [enabled]);
|
|
422
|
+
const subscribe = (0, import_react5.useCallback)(async () => {
|
|
423
|
+
await pushManagerRef.current?.subscribe();
|
|
424
|
+
}, []);
|
|
425
|
+
const unsubscribe = (0, import_react5.useCallback)(async () => {
|
|
426
|
+
await pushManagerRef.current?.unsubscribe();
|
|
427
|
+
}, []);
|
|
428
|
+
return {
|
|
429
|
+
status,
|
|
430
|
+
isSupported: import_js_web_adapter_core2.PushManager.isSupported(),
|
|
431
|
+
isSubscribed: status === "subscribed",
|
|
432
|
+
subscribe,
|
|
433
|
+
unsubscribe
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/hooks/useAttachmentUpload.ts
|
|
438
|
+
var import_react6 = require("react");
|
|
439
|
+
|
|
440
|
+
// src/types/AttachmentUpload.ts
|
|
441
|
+
function isMultiStepUpload(config) {
|
|
442
|
+
return "requestSignedUrl" in config;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/hooks/useAttachmentUpload.ts
|
|
446
|
+
function generateAttachmentId() {
|
|
447
|
+
return `att-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
448
|
+
}
|
|
449
|
+
function useAttachmentUpload(uploadConfig) {
|
|
450
|
+
const [attachments, setAttachments] = (0, import_react6.useState)([]);
|
|
451
|
+
const abortControllers = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
|
|
452
|
+
const uploadFileRef = (0, import_react6.useRef)();
|
|
453
|
+
const addFiles = (0, import_react6.useCallback)((files) => {
|
|
454
|
+
const fileArray = Array.isArray(files) ? files : Array.from(files);
|
|
455
|
+
const newAttachments = fileArray.map((file) => ({
|
|
456
|
+
id: generateAttachmentId(),
|
|
457
|
+
file,
|
|
458
|
+
name: file.name,
|
|
459
|
+
mimeType: file.type,
|
|
460
|
+
size: file.size,
|
|
461
|
+
status: "pending",
|
|
462
|
+
progress: 0
|
|
463
|
+
}));
|
|
464
|
+
setAttachments((prev) => [...prev, ...newAttachments]);
|
|
465
|
+
newAttachments.forEach((att) => uploadFileRef.current?.(att));
|
|
466
|
+
}, []);
|
|
467
|
+
const uploadFile = (0, import_react6.useCallback)(
|
|
468
|
+
async (attachment) => {
|
|
469
|
+
const controller = new AbortController();
|
|
470
|
+
abortControllers.current.set(attachment.id, controller);
|
|
471
|
+
try {
|
|
472
|
+
setAttachments(
|
|
473
|
+
(prev) => prev.map(
|
|
474
|
+
(a) => a.id === attachment.id ? { ...a, status: "uploading", progress: 0 } : a
|
|
475
|
+
)
|
|
476
|
+
);
|
|
477
|
+
if (isMultiStepUpload(uploadConfig)) {
|
|
478
|
+
const signedUrl = await uploadConfig.requestSignedUrl({
|
|
479
|
+
name: attachment.name,
|
|
480
|
+
mimeType: attachment.mimeType,
|
|
481
|
+
size: attachment.size
|
|
482
|
+
});
|
|
483
|
+
if (controller.signal.aborted) return;
|
|
484
|
+
const uploadSuccess = await uploadConfig.uploadToSignedUrl(
|
|
485
|
+
signedUrl,
|
|
486
|
+
attachment.file,
|
|
487
|
+
(progress) => {
|
|
488
|
+
setAttachments(
|
|
489
|
+
(prev) => prev.map((a) => a.id === attachment.id ? { ...a, progress } : a)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
if (controller.signal.aborted) return;
|
|
494
|
+
if (!uploadSuccess) {
|
|
495
|
+
throw new Error("Upload to signed URL failed");
|
|
496
|
+
}
|
|
497
|
+
const finalUrl = await uploadConfig.confirmUpload(signedUrl, {
|
|
498
|
+
name: attachment.name,
|
|
499
|
+
mimeType: attachment.mimeType,
|
|
500
|
+
size: attachment.size
|
|
501
|
+
});
|
|
502
|
+
setAttachments(
|
|
503
|
+
(prev) => prev.map(
|
|
504
|
+
(a) => a.id === attachment.id ? { ...a, status: "uploaded", progress: 100, url: finalUrl } : a
|
|
505
|
+
)
|
|
506
|
+
);
|
|
507
|
+
} else {
|
|
508
|
+
const formData = new FormData();
|
|
509
|
+
formData.append("file", attachment.file);
|
|
510
|
+
const xhr = new XMLHttpRequest();
|
|
511
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
512
|
+
if (e.lengthComputable) {
|
|
513
|
+
const progress = Math.round(e.loaded / e.total * 100);
|
|
514
|
+
setAttachments(
|
|
515
|
+
(prev) => prev.map((a) => a.id === attachment.id ? { ...a, progress } : a)
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
const response = await new Promise((resolve, reject) => {
|
|
520
|
+
xhr.onload = () => {
|
|
521
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
522
|
+
resolve(JSON.parse(xhr.responseText));
|
|
523
|
+
} else {
|
|
524
|
+
reject(new Error(`Upload failed: ${xhr.status}`));
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
xhr.onerror = () => reject(new Error("Network error"));
|
|
528
|
+
xhr.onabort = () => reject(new Error("Upload cancelled"));
|
|
529
|
+
xhr.open("POST", uploadConfig.endpoint);
|
|
530
|
+
if (uploadConfig.headers) {
|
|
531
|
+
Object.entries(uploadConfig.headers).forEach(([key, value]) => {
|
|
532
|
+
xhr.setRequestHeader(key, value);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
xhr.send(formData);
|
|
536
|
+
});
|
|
537
|
+
setAttachments(
|
|
538
|
+
(prev) => prev.map(
|
|
539
|
+
(a) => a.id === attachment.id ? { ...a, status: "uploaded", progress: 100, url: response.url } : a
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (controller.signal.aborted) return;
|
|
545
|
+
setAttachments(
|
|
546
|
+
(prev) => prev.map(
|
|
547
|
+
(a) => a.id === attachment.id ? {
|
|
548
|
+
...a,
|
|
549
|
+
status: "error",
|
|
550
|
+
error: error instanceof Error ? error.message : "Upload failed"
|
|
551
|
+
} : a
|
|
552
|
+
)
|
|
553
|
+
);
|
|
554
|
+
} finally {
|
|
555
|
+
abortControllers.current.delete(attachment.id);
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
[uploadConfig]
|
|
559
|
+
);
|
|
560
|
+
uploadFileRef.current = uploadFile;
|
|
561
|
+
const removeAttachment = (0, import_react6.useCallback)((id) => {
|
|
562
|
+
const controller = abortControllers.current.get(id);
|
|
563
|
+
if (controller) {
|
|
564
|
+
controller.abort();
|
|
565
|
+
}
|
|
566
|
+
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
567
|
+
}, []);
|
|
568
|
+
const clearAttachments = (0, import_react6.useCallback)(() => {
|
|
569
|
+
abortControllers.current.forEach((c) => c.abort());
|
|
570
|
+
abortControllers.current.clear();
|
|
571
|
+
setAttachments([]);
|
|
572
|
+
}, []);
|
|
573
|
+
const resetUploads = (0, import_react6.useCallback)(() => {
|
|
574
|
+
setAttachments(
|
|
575
|
+
(prev) => prev.map(
|
|
576
|
+
(a) => a.status === "error" ? { ...a, status: "pending", progress: 0, error: void 0 } : a
|
|
577
|
+
)
|
|
578
|
+
);
|
|
579
|
+
}, []);
|
|
580
|
+
const getUploadedAttachments = (0, import_react6.useCallback)(() => {
|
|
581
|
+
return attachments.filter((a) => a.status === "uploaded" && a.url);
|
|
582
|
+
}, [attachments]);
|
|
583
|
+
const isUploading = attachments.some((a) => a.status === "uploading");
|
|
584
|
+
const isComplete = attachments.length > 0 && attachments.every((a) => a.status === "uploaded" || a.status === "error");
|
|
585
|
+
return {
|
|
586
|
+
attachments,
|
|
587
|
+
addFiles,
|
|
588
|
+
removeAttachment,
|
|
589
|
+
clearAttachments,
|
|
590
|
+
resetUploads,
|
|
591
|
+
getUploadedAttachments,
|
|
592
|
+
isUploading,
|
|
593
|
+
isComplete
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/hooks/useBridge.ts
|
|
598
|
+
var import_react7 = require("react");
|
|
599
|
+
function useBridge() {
|
|
600
|
+
const notificationCbRef = (0, import_react7.useRef)(null);
|
|
601
|
+
const [config, setConfig] = (0, import_react7.useState)(null);
|
|
602
|
+
const isInIframe = typeof window !== "undefined" && window !== window.parent;
|
|
603
|
+
const notifyMessage = (0, import_react7.useCallback)(
|
|
604
|
+
(text) => {
|
|
605
|
+
if (!isInIframe) return;
|
|
606
|
+
window.parent.postMessage({ type: "chat-message", text }, "*");
|
|
607
|
+
},
|
|
608
|
+
[isInIframe]
|
|
609
|
+
);
|
|
610
|
+
const notifyViewportConfig = (0, import_react7.useCallback)(
|
|
611
|
+
(viewportContent) => {
|
|
612
|
+
if (!isInIframe) return;
|
|
613
|
+
window.parent.postMessage({ type: "chat-viewport-config", content: viewportContent }, "*");
|
|
614
|
+
},
|
|
615
|
+
[isInIframe]
|
|
616
|
+
);
|
|
617
|
+
const onNotificationClicked = (0, import_react7.useCallback)((cb) => {
|
|
618
|
+
notificationCbRef.current = cb;
|
|
619
|
+
}, []);
|
|
620
|
+
(0, import_react7.useEffect)(() => {
|
|
621
|
+
if (!isInIframe) return;
|
|
622
|
+
function handleMessage(event) {
|
|
623
|
+
const data = event.data;
|
|
624
|
+
if (!data || typeof data !== "object" || !data.type) return;
|
|
625
|
+
if (data.type === "chat-config") {
|
|
626
|
+
const configData = { ...data };
|
|
627
|
+
delete configData.type;
|
|
628
|
+
setConfig(configData);
|
|
629
|
+
}
|
|
630
|
+
if (data.type === "chat-notification-clicked") {
|
|
631
|
+
notificationCbRef.current?.();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
window.addEventListener("message", handleMessage);
|
|
635
|
+
return () => window.removeEventListener("message", handleMessage);
|
|
636
|
+
}, [isInIframe]);
|
|
637
|
+
return { config, isInIframe, notifyMessage, notifyViewportConfig, onNotificationClicked };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/i18n/LocaleProvider.tsx
|
|
641
|
+
var import_react8 = require("react");
|
|
642
|
+
|
|
643
|
+
// src/i18n/types.ts
|
|
644
|
+
function getBaseLocale(locale) {
|
|
645
|
+
return locale.split("-")[0] || locale;
|
|
646
|
+
}
|
|
647
|
+
function getFallbackChain(locale) {
|
|
648
|
+
const base = getBaseLocale(locale);
|
|
649
|
+
if (locale === base) {
|
|
650
|
+
return [locale, "en"];
|
|
651
|
+
}
|
|
652
|
+
return [locale, base, "en"];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/i18n/locales/en.json
|
|
656
|
+
var en_default = {
|
|
657
|
+
chatWidget: {
|
|
658
|
+
title: "Chat",
|
|
659
|
+
placeholder: "Type a message...",
|
|
660
|
+
openChat: "Open chat",
|
|
661
|
+
closeChat: "Close chat",
|
|
662
|
+
connectionStatus: {
|
|
663
|
+
connected: "Connected",
|
|
664
|
+
disconnected: "Disconnected"
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
inputArea: {
|
|
668
|
+
send: "Send",
|
|
669
|
+
uploading: "Uploading...",
|
|
670
|
+
dropzone: {
|
|
671
|
+
dropFiles: "Drop files here",
|
|
672
|
+
dropOrClick: "Drop or click to attach"
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
typingIndicator: {
|
|
676
|
+
typing: "typing",
|
|
677
|
+
isTyping: "is typing..."
|
|
678
|
+
},
|
|
679
|
+
messageList: {
|
|
680
|
+
emptyState: "No messages yet. Start the conversation!"
|
|
681
|
+
},
|
|
682
|
+
attachmentList: {
|
|
683
|
+
remove: "Remove",
|
|
684
|
+
uploadFailed: "Upload failed"
|
|
685
|
+
},
|
|
686
|
+
header: {
|
|
687
|
+
enterFullscreen: "Enter fullscreen",
|
|
688
|
+
exitFullscreen: "Exit fullscreen",
|
|
689
|
+
closeChat: "Close chat",
|
|
690
|
+
lightMode: "Light mode",
|
|
691
|
+
darkMode: "Dark mode",
|
|
692
|
+
autoMode: "System theme"
|
|
693
|
+
},
|
|
694
|
+
floatingButton: {
|
|
695
|
+
openChat: "Open chat",
|
|
696
|
+
closeChat: "Close chat"
|
|
697
|
+
},
|
|
698
|
+
common: {
|
|
699
|
+
loading: "Loading...",
|
|
700
|
+
error: "Error",
|
|
701
|
+
retry: "Retry",
|
|
702
|
+
cancel: "Cancel",
|
|
703
|
+
download: "Download"
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// src/i18n/locales/en-US.json
|
|
708
|
+
var en_US_default = {
|
|
709
|
+
chatWidget: {
|
|
710
|
+
title: "Chat"
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/i18n/locales/en-GB.json
|
|
715
|
+
var en_GB_default = {
|
|
716
|
+
chatWidget: {
|
|
717
|
+
title: "Chat"
|
|
718
|
+
},
|
|
719
|
+
common: {
|
|
720
|
+
loading: "Loading..."
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// src/i18n/locales/pt.json
|
|
725
|
+
var pt_default = {
|
|
726
|
+
chatWidget: {
|
|
727
|
+
title: "Chat",
|
|
728
|
+
placeholder: "Digite uma mensagem...",
|
|
729
|
+
openChat: "Abrir chat",
|
|
730
|
+
closeChat: "Fechar chat",
|
|
731
|
+
connectionStatus: {
|
|
732
|
+
connected: "Conectado",
|
|
733
|
+
disconnected: "Desconectado"
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
inputArea: {
|
|
737
|
+
send: "Enviar",
|
|
738
|
+
uploading: "Enviando...",
|
|
739
|
+
dropzone: {
|
|
740
|
+
dropFiles: "Solte arquivos aqui",
|
|
741
|
+
dropOrClick: "Solte ou clique para anexar"
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
typingIndicator: {
|
|
745
|
+
typing: "digitando",
|
|
746
|
+
isTyping: "est\xE1 digitando..."
|
|
747
|
+
},
|
|
748
|
+
messageList: {
|
|
749
|
+
emptyState: "Nenhuma mensagem ainda. Inicie a conversa!"
|
|
750
|
+
},
|
|
751
|
+
attachmentList: {
|
|
752
|
+
remove: "Remover",
|
|
753
|
+
uploadFailed: "Falha no envio"
|
|
754
|
+
},
|
|
755
|
+
header: {
|
|
756
|
+
enterFullscreen: "Tela cheia",
|
|
757
|
+
exitFullscreen: "Sair da tela cheia",
|
|
758
|
+
closeChat: "Fechar chat"
|
|
759
|
+
},
|
|
760
|
+
floatingButton: {
|
|
761
|
+
openChat: "Abrir chat",
|
|
762
|
+
closeChat: "Fechar chat"
|
|
763
|
+
},
|
|
764
|
+
common: {
|
|
765
|
+
loading: "Carregando...",
|
|
766
|
+
error: "Erro",
|
|
767
|
+
retry: "Tentar novamente",
|
|
768
|
+
cancel: "Cancelar",
|
|
769
|
+
download: "Baixar"
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
// src/i18n/locales/pt-BR.json
|
|
774
|
+
var pt_BR_default = {
|
|
775
|
+
chatWidget: {
|
|
776
|
+
placeholder: "Digite uma mensagem..."
|
|
777
|
+
},
|
|
778
|
+
inputArea: {
|
|
779
|
+
send: "Enviar",
|
|
780
|
+
uploading: "Enviando...",
|
|
781
|
+
dropzone: {
|
|
782
|
+
dropFiles: "Solte arquivos aqui",
|
|
783
|
+
dropOrClick: "Solte ou clique para anexar"
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
typingIndicator: {
|
|
787
|
+
isTyping: "est\xE1 digitando..."
|
|
788
|
+
},
|
|
789
|
+
attachmentList: {
|
|
790
|
+
uploadFailed: "Falha no envio"
|
|
791
|
+
},
|
|
792
|
+
common: {
|
|
793
|
+
loading: "Carregando...",
|
|
794
|
+
retry: "Tentar novamente"
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// src/i18n/locales/pt-PT.json
|
|
799
|
+
var pt_PT_default = {
|
|
800
|
+
chatWidget: {
|
|
801
|
+
placeholder: "Escreva uma mensagem..."
|
|
802
|
+
},
|
|
803
|
+
inputArea: {
|
|
804
|
+
send: "Enviar",
|
|
805
|
+
uploading: "A enviar...",
|
|
806
|
+
dropzone: {
|
|
807
|
+
dropFiles: "Largue ficheiros aqui",
|
|
808
|
+
dropOrClick: "Largue ou clique para anexar"
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
typingIndicator: {
|
|
812
|
+
isTyping: "est\xE1 a escrever..."
|
|
813
|
+
},
|
|
814
|
+
attachmentList: {
|
|
815
|
+
uploadFailed: "Falha no envio"
|
|
816
|
+
},
|
|
817
|
+
common: {
|
|
818
|
+
loading: "A carregar...",
|
|
819
|
+
retry: "Tentar novamente"
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// src/i18n/locales/es.json
|
|
824
|
+
var es_default = {
|
|
825
|
+
chatWidget: {
|
|
826
|
+
title: "Chat",
|
|
827
|
+
placeholder: "Escribe un mensaje...",
|
|
828
|
+
openChat: "Abrir chat",
|
|
829
|
+
closeChat: "Cerrar chat",
|
|
830
|
+
connectionStatus: {
|
|
831
|
+
connected: "Conectado",
|
|
832
|
+
disconnected: "Desconectado"
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
inputArea: {
|
|
836
|
+
send: "Enviar",
|
|
837
|
+
uploading: "Subiendo...",
|
|
838
|
+
dropzone: {
|
|
839
|
+
dropFiles: "Suelta archivos aqu\xED",
|
|
840
|
+
dropOrClick: "Suelta o haz clic para adjuntar"
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
typingIndicator: {
|
|
844
|
+
typing: "escribiendo",
|
|
845
|
+
isTyping: "est\xE1 escribiendo..."
|
|
846
|
+
},
|
|
847
|
+
messageList: {
|
|
848
|
+
emptyState: "No hay mensajes todav\xEDa. \xA1Inicia la conversaci\xF3n!"
|
|
849
|
+
},
|
|
850
|
+
attachmentList: {
|
|
851
|
+
remove: "Eliminar",
|
|
852
|
+
uploadFailed: "Error al subir"
|
|
853
|
+
},
|
|
854
|
+
header: {
|
|
855
|
+
enterFullscreen: "Pantalla completa",
|
|
856
|
+
exitFullscreen: "Salir de pantalla completa",
|
|
857
|
+
closeChat: "Cerrar chat"
|
|
858
|
+
},
|
|
859
|
+
floatingButton: {
|
|
860
|
+
openChat: "Abrir chat",
|
|
861
|
+
closeChat: "Cerrar chat"
|
|
862
|
+
},
|
|
863
|
+
common: {
|
|
864
|
+
loading: "Cargando...",
|
|
865
|
+
error: "Error",
|
|
866
|
+
retry: "Reintentar",
|
|
867
|
+
cancel: "Cancelar",
|
|
868
|
+
download: "Descargar"
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
// src/i18n/mergeLocale.ts
|
|
873
|
+
function deepMerge(target, source) {
|
|
874
|
+
const result = { ...target };
|
|
875
|
+
for (const key of Object.keys(source)) {
|
|
876
|
+
const sourceVal = source[key];
|
|
877
|
+
const targetVal = target[key];
|
|
878
|
+
if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
879
|
+
result[key] = deepMerge(targetVal, sourceVal);
|
|
880
|
+
} else if (sourceVal !== void 0) {
|
|
881
|
+
result[key] = sourceVal;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return result;
|
|
885
|
+
}
|
|
886
|
+
var systemLocales = {
|
|
887
|
+
en: en_default,
|
|
888
|
+
"en-US": deepMerge(en_default, en_US_default),
|
|
889
|
+
"en-GB": deepMerge(en_default, en_GB_default),
|
|
890
|
+
pt: pt_default,
|
|
891
|
+
"pt-BR": deepMerge(pt_default, pt_BR_default),
|
|
892
|
+
"pt-PT": deepMerge(pt_default, pt_PT_default),
|
|
893
|
+
es: es_default
|
|
894
|
+
};
|
|
895
|
+
function registerLocale(locale, strings) {
|
|
896
|
+
systemLocales[locale] = strings;
|
|
897
|
+
}
|
|
898
|
+
function mergeLocale(locale, overrides) {
|
|
899
|
+
const chain = getFallbackChain(locale);
|
|
900
|
+
let base = systemLocales["en"] || en_default;
|
|
901
|
+
for (const code of chain) {
|
|
902
|
+
if (code === "en") continue;
|
|
903
|
+
const localeStrings = systemLocales[code];
|
|
904
|
+
if (localeStrings) {
|
|
905
|
+
base = deepMerge(base, localeStrings);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (!overrides) return base;
|
|
909
|
+
return deepMerge(base, overrides);
|
|
910
|
+
}
|
|
911
|
+
function getAvailableLocales() {
|
|
912
|
+
return Object.keys(systemLocales);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/i18n/LocaleProvider.tsx
|
|
916
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
917
|
+
var LocaleContext = (0, import_react8.createContext)(void 0);
|
|
918
|
+
function LocaleProvider({ children, locale }) {
|
|
919
|
+
const config = (0, import_react8.useMemo)(() => {
|
|
920
|
+
if (!locale) return { locale: "en" };
|
|
921
|
+
if (typeof locale === "string") return { locale };
|
|
922
|
+
return locale;
|
|
923
|
+
}, [locale]);
|
|
924
|
+
const value = (0, import_react8.useMemo)(() => {
|
|
925
|
+
const strings = mergeLocale(config.locale, config.overrides);
|
|
926
|
+
const t = (path) => {
|
|
927
|
+
const parts = path.split(".");
|
|
928
|
+
let current = strings;
|
|
929
|
+
for (const part of parts) {
|
|
930
|
+
if (current == null) return path;
|
|
931
|
+
current = current[part];
|
|
932
|
+
}
|
|
933
|
+
return typeof current === "string" ? current : path;
|
|
934
|
+
};
|
|
935
|
+
return { locale: config.locale, strings, t };
|
|
936
|
+
}, [config.locale, config.overrides]);
|
|
937
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LocaleContext.Provider, { value, children });
|
|
938
|
+
}
|
|
939
|
+
function useLocale() {
|
|
940
|
+
const context = (0, import_react8.useContext)(LocaleContext);
|
|
941
|
+
if (!context) {
|
|
942
|
+
const strings = mergeLocale("en");
|
|
943
|
+
return {
|
|
944
|
+
locale: "en",
|
|
945
|
+
strings,
|
|
946
|
+
t: (path) => {
|
|
947
|
+
const parts = path.split(".");
|
|
948
|
+
let current = strings;
|
|
949
|
+
for (const part of parts) {
|
|
950
|
+
if (current == null) return path;
|
|
951
|
+
current = current[part];
|
|
952
|
+
}
|
|
953
|
+
return typeof current === "string" ? current : path;
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return context;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/components/FloatingButton.tsx
|
|
961
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
962
|
+
function FloatingButton({
|
|
963
|
+
onClick,
|
|
964
|
+
isOpen,
|
|
965
|
+
position = "bottom-right",
|
|
966
|
+
className,
|
|
967
|
+
icon,
|
|
968
|
+
openIcon,
|
|
969
|
+
badgeCount,
|
|
970
|
+
size = 60,
|
|
971
|
+
backgroundColor,
|
|
972
|
+
ariaLabel
|
|
973
|
+
}) {
|
|
974
|
+
const { t } = useLocale();
|
|
975
|
+
const positionClasses = {
|
|
976
|
+
"bottom-right": "fixed bottom-5 right-5",
|
|
977
|
+
"bottom-left": "fixed bottom-5 left-5",
|
|
978
|
+
"top-right": "fixed top-5 right-5",
|
|
979
|
+
"top-left": "fixed top-5 left-5"
|
|
980
|
+
};
|
|
981
|
+
const iconSize = Math.floor(size * 0.4);
|
|
982
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
983
|
+
"button",
|
|
984
|
+
{
|
|
985
|
+
onClick,
|
|
986
|
+
className: `${positionClasses[position]} chat-floating-button hover:scale-105 transition-transform ${className || ""}`,
|
|
987
|
+
style: { width: size, height: size, backgroundColor },
|
|
988
|
+
"data-chat-floating-button": "true",
|
|
989
|
+
"data-testid": "chat-floating-button",
|
|
990
|
+
"aria-label": ariaLabel || (isOpen ? t("floatingButton.closeChat") : t("floatingButton.openChat")),
|
|
991
|
+
children: [
|
|
992
|
+
badgeCount && badgeCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
993
|
+
"span",
|
|
994
|
+
{
|
|
995
|
+
className: "absolute -top-1 -right-1 min-w-5 h-5 rounded-full bg-chat-error text-white text-xs font-bold flex items-center justify-center px-1",
|
|
996
|
+
"data-chat-badge": "true",
|
|
997
|
+
children: badgeCount > 99 ? "99+" : badgeCount
|
|
998
|
+
}
|
|
999
|
+
),
|
|
1000
|
+
isOpen ? openIcon || icon || /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1001
|
+
"svg",
|
|
1002
|
+
{
|
|
1003
|
+
width: iconSize,
|
|
1004
|
+
height: iconSize,
|
|
1005
|
+
viewBox: "0 0 24 24",
|
|
1006
|
+
fill: "none",
|
|
1007
|
+
stroke: "#fff",
|
|
1008
|
+
strokeWidth: "2",
|
|
1009
|
+
strokeLinecap: "round",
|
|
1010
|
+
strokeLinejoin: "round",
|
|
1011
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 6L6 18M6 6l12 12" })
|
|
1012
|
+
}
|
|
1013
|
+
) : icon || /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1014
|
+
"svg",
|
|
1015
|
+
{
|
|
1016
|
+
width: iconSize,
|
|
1017
|
+
height: iconSize,
|
|
1018
|
+
viewBox: "0 0 24 24",
|
|
1019
|
+
fill: "none",
|
|
1020
|
+
stroke: "#fff",
|
|
1021
|
+
strokeWidth: "2",
|
|
1022
|
+
strokeLinecap: "round",
|
|
1023
|
+
strokeLinejoin: "round",
|
|
1024
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" })
|
|
1025
|
+
}
|
|
1026
|
+
)
|
|
1027
|
+
]
|
|
1028
|
+
}
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// src/components/Header.tsx
|
|
1033
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1034
|
+
function Header({
|
|
1035
|
+
title = "Chat",
|
|
1036
|
+
onClose,
|
|
1037
|
+
onToggleFullscreen,
|
|
1038
|
+
isFullscreen = false,
|
|
1039
|
+
showConnectionStatus = true,
|
|
1040
|
+
isConnected = true,
|
|
1041
|
+
className,
|
|
1042
|
+
theme,
|
|
1043
|
+
onThemeChange
|
|
1044
|
+
}) {
|
|
1045
|
+
const { t } = useLocale();
|
|
1046
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1047
|
+
"div",
|
|
1048
|
+
{
|
|
1049
|
+
className: `chat-header ${className || ""}`,
|
|
1050
|
+
"data-chat-header": "true",
|
|
1051
|
+
"data-testid": "chat-header",
|
|
1052
|
+
children: [
|
|
1053
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
1054
|
+
showConnectionStatus && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1055
|
+
"div",
|
|
1056
|
+
{
|
|
1057
|
+
className: `w-2 h-2 rounded-full ${isConnected ? "bg-chat-success" : "bg-chat-error"}`,
|
|
1058
|
+
"data-chat-connection-status": "true",
|
|
1059
|
+
title: isConnected ? "Connected" : "Disconnected"
|
|
1060
|
+
}
|
|
1061
|
+
),
|
|
1062
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "m-0 text-base font-semibold text-chat-text", children: title })
|
|
1063
|
+
] }),
|
|
1064
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-1", children: [
|
|
1065
|
+
onThemeChange && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1066
|
+
"button",
|
|
1067
|
+
{
|
|
1068
|
+
onClick: () => onThemeChange(theme === "light" ? "dark" : theme === "dark" ? "auto" : "light"),
|
|
1069
|
+
className: "p-1 bg-transparent border-none cursor-pointer text-chat-text-secondary rounded hover:bg-chat-surface transition",
|
|
1070
|
+
"data-chat-theme-toggle": "true",
|
|
1071
|
+
"aria-label": theme === "light" ? t("header.darkMode") : theme === "dark" ? t("header.autoMode") : t("header.lightMode"),
|
|
1072
|
+
title: theme === "light" ? t("header.darkMode") : theme === "dark" ? t("header.autoMode") : t("header.lightMode"),
|
|
1073
|
+
children: theme === "light" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1074
|
+
"svg",
|
|
1075
|
+
{
|
|
1076
|
+
width: "18",
|
|
1077
|
+
height: "18",
|
|
1078
|
+
viewBox: "0 0 24 24",
|
|
1079
|
+
fill: "none",
|
|
1080
|
+
stroke: "currentColor",
|
|
1081
|
+
strokeWidth: "2",
|
|
1082
|
+
strokeLinecap: "round",
|
|
1083
|
+
strokeLinejoin: "round",
|
|
1084
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })
|
|
1085
|
+
}
|
|
1086
|
+
) : theme === "dark" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1087
|
+
"svg",
|
|
1088
|
+
{
|
|
1089
|
+
width: "18",
|
|
1090
|
+
height: "18",
|
|
1091
|
+
viewBox: "0 0 24 24",
|
|
1092
|
+
fill: "none",
|
|
1093
|
+
stroke: "currentColor",
|
|
1094
|
+
strokeWidth: "2",
|
|
1095
|
+
strokeLinecap: "round",
|
|
1096
|
+
strokeLinejoin: "round",
|
|
1097
|
+
children: [
|
|
1098
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "5" }),
|
|
1099
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
|
|
1100
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
|
|
1101
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
|
|
1102
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
|
|
1103
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
|
|
1104
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
|
|
1105
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
|
|
1106
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })
|
|
1107
|
+
]
|
|
1108
|
+
}
|
|
1109
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
1110
|
+
"svg",
|
|
1111
|
+
{
|
|
1112
|
+
width: "18",
|
|
1113
|
+
height: "18",
|
|
1114
|
+
viewBox: "0 0 24 24",
|
|
1115
|
+
fill: "none",
|
|
1116
|
+
stroke: "currentColor",
|
|
1117
|
+
strokeWidth: "2",
|
|
1118
|
+
strokeLinecap: "round",
|
|
1119
|
+
strokeLinejoin: "round",
|
|
1120
|
+
children: [
|
|
1121
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }),
|
|
1122
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
|
|
1123
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
|
|
1124
|
+
]
|
|
1125
|
+
}
|
|
1126
|
+
)
|
|
1127
|
+
}
|
|
1128
|
+
),
|
|
1129
|
+
onToggleFullscreen && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1130
|
+
"button",
|
|
1131
|
+
{
|
|
1132
|
+
onClick: onToggleFullscreen,
|
|
1133
|
+
className: "p-1 bg-transparent border-none cursor-pointer text-chat-text-secondary rounded hover:bg-chat-surface transition",
|
|
1134
|
+
"data-chat-fullscreen-toggle": "true",
|
|
1135
|
+
"aria-label": isFullscreen ? t("header.exitFullscreen") : t("header.enterFullscreen"),
|
|
1136
|
+
title: isFullscreen ? t("header.exitFullscreen") : t("header.enterFullscreen"),
|
|
1137
|
+
children: isFullscreen ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1138
|
+
"svg",
|
|
1139
|
+
{
|
|
1140
|
+
width: "20",
|
|
1141
|
+
height: "20",
|
|
1142
|
+
viewBox: "0 0 24 24",
|
|
1143
|
+
fill: "none",
|
|
1144
|
+
stroke: "currentColor",
|
|
1145
|
+
strokeWidth: "2",
|
|
1146
|
+
strokeLinecap: "round",
|
|
1147
|
+
strokeLinejoin: "round",
|
|
1148
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" })
|
|
1149
|
+
}
|
|
1150
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1151
|
+
"svg",
|
|
1152
|
+
{
|
|
1153
|
+
width: "20",
|
|
1154
|
+
height: "20",
|
|
1155
|
+
viewBox: "0 0 24 24",
|
|
1156
|
+
fill: "none",
|
|
1157
|
+
stroke: "currentColor",
|
|
1158
|
+
strokeWidth: "2",
|
|
1159
|
+
strokeLinecap: "round",
|
|
1160
|
+
strokeLinejoin: "round",
|
|
1161
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" })
|
|
1162
|
+
}
|
|
1163
|
+
)
|
|
1164
|
+
}
|
|
1165
|
+
),
|
|
1166
|
+
onClose && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1167
|
+
"button",
|
|
1168
|
+
{
|
|
1169
|
+
onClick: onClose,
|
|
1170
|
+
className: "p-1 bg-transparent border-none cursor-pointer text-chat-text-secondary rounded hover:bg-chat-surface transition",
|
|
1171
|
+
"data-chat-close": "true",
|
|
1172
|
+
"aria-label": t("header.closeChat"),
|
|
1173
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1174
|
+
"svg",
|
|
1175
|
+
{
|
|
1176
|
+
width: "20",
|
|
1177
|
+
height: "20",
|
|
1178
|
+
viewBox: "0 0 24 24",
|
|
1179
|
+
fill: "none",
|
|
1180
|
+
stroke: "currentColor",
|
|
1181
|
+
strokeWidth: "2",
|
|
1182
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M18 6L6 18M6 6l12 12" })
|
|
1183
|
+
}
|
|
1184
|
+
)
|
|
1185
|
+
}
|
|
1186
|
+
)
|
|
1187
|
+
] })
|
|
1188
|
+
]
|
|
1189
|
+
}
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// src/components/MessageList.tsx
|
|
1194
|
+
var import_react10 = require("react");
|
|
1195
|
+
|
|
1196
|
+
// src/cards/CardContext.tsx
|
|
1197
|
+
var import_react9 = require("react");
|
|
1198
|
+
|
|
1199
|
+
// src/utils/markdown.tsx
|
|
1200
|
+
var import_marked = require("marked");
|
|
1201
|
+
var import_dompurify = __toESM(require("dompurify"), 1);
|
|
1202
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1203
|
+
var renderer = new import_marked.marked.Renderer();
|
|
1204
|
+
renderer.link = ({ href, text }) => {
|
|
1205
|
+
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
|
1206
|
+
};
|
|
1207
|
+
import_marked.marked.setOptions({
|
|
1208
|
+
gfm: true,
|
|
1209
|
+
breaks: true,
|
|
1210
|
+
renderer
|
|
1211
|
+
});
|
|
1212
|
+
function renderMarkdown(text) {
|
|
1213
|
+
const rawHtml = import_marked.marked.parse(text);
|
|
1214
|
+
return import_dompurify.default.sanitize(rawHtml, { ADD_ATTR: ["target"] });
|
|
1215
|
+
}
|
|
1216
|
+
function MarkdownRenderer({
|
|
1217
|
+
text,
|
|
1218
|
+
className
|
|
1219
|
+
}) {
|
|
1220
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1221
|
+
"div",
|
|
1222
|
+
{
|
|
1223
|
+
className: `prose prose-sm max-w-none prose-headings:font-semibold prose-a:text-chat-primary prose-a:no-underline hover:prose-a:underline prose-code:bg-chat-surface prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:font-mono prose-code:text-sm prose-blockquote:border-l-chat-border prose-blockquote:text-chat-text-secondary ${className || ""}`,
|
|
1224
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(text) }
|
|
1225
|
+
}
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/cards/DefaultCard.tsx
|
|
1230
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1231
|
+
function DefaultCard({
|
|
1232
|
+
card: rawCard,
|
|
1233
|
+
onActionClick
|
|
1234
|
+
}) {
|
|
1235
|
+
if (rawCard.type !== "card") {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
const card = rawCard;
|
|
1239
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
1240
|
+
"div",
|
|
1241
|
+
{
|
|
1242
|
+
className: "border border-chat-border rounded-lg overflow-hidden max-w-sm bg-[var(--chat-background)]",
|
|
1243
|
+
"data-chat-card": "default",
|
|
1244
|
+
children: [
|
|
1245
|
+
card.header && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "px-3 py-2 bg-chat-surface border-b border-chat-border font-semibold text-sm", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MarkdownRenderer, { text: card.header }) }),
|
|
1246
|
+
card.image && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1247
|
+
"img",
|
|
1248
|
+
{
|
|
1249
|
+
src: card.image.url,
|
|
1250
|
+
alt: card.image.alt || "",
|
|
1251
|
+
className: "block w-full h-auto max-h-48 object-cover",
|
|
1252
|
+
"data-chat-card-image": "true"
|
|
1253
|
+
}
|
|
1254
|
+
),
|
|
1255
|
+
card.sections?.map((section, index) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "px-3 py-2", "data-chat-section": index, children: [
|
|
1256
|
+
section.text && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mb-2 text-sm leading-relaxed", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MarkdownRenderer, { text: section.text }) }),
|
|
1257
|
+
section.fields?.map((field, fieldIndex) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mb-1 last:mb-0", "data-chat-field": fieldIndex, children: [
|
|
1258
|
+
field.title && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-xs text-chat-text-secondary font-medium", children: field.title }),
|
|
1259
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-sm text-chat-text", children: field.value })
|
|
1260
|
+
] }, fieldIndex))
|
|
1261
|
+
] }, index)),
|
|
1262
|
+
card.elements?.map((element, elIndex) => {
|
|
1263
|
+
switch (element.type) {
|
|
1264
|
+
case "text":
|
|
1265
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1266
|
+
"div",
|
|
1267
|
+
{
|
|
1268
|
+
className: `px-3 py-2 text-sm ${element.style === "muted" ? "text-chat-text-secondary" : element.style === "bold" ? "text-chat-text font-bold" : "text-chat-text"}`,
|
|
1269
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MarkdownRenderer, { text: element.content })
|
|
1270
|
+
},
|
|
1271
|
+
`el-${elIndex}`
|
|
1272
|
+
);
|
|
1273
|
+
case "divider":
|
|
1274
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("hr", { className: "border-0 border-t border-chat-border" }, `el-${elIndex}`);
|
|
1275
|
+
case "link":
|
|
1276
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1277
|
+
"a",
|
|
1278
|
+
{
|
|
1279
|
+
href: element.url,
|
|
1280
|
+
target: "_blank",
|
|
1281
|
+
rel: "noopener noreferrer",
|
|
1282
|
+
className: "block px-3 py-2 text-chat-primary text-sm no-underline hover:underline",
|
|
1283
|
+
children: element.label
|
|
1284
|
+
},
|
|
1285
|
+
`el-${elIndex}`
|
|
1286
|
+
);
|
|
1287
|
+
case "table":
|
|
1288
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("table", { className: "w-full border-collapse text-xs", children: [
|
|
1289
|
+
element.headers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tr", { children: element.headers.map((h, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1290
|
+
"th",
|
|
1291
|
+
{
|
|
1292
|
+
className: "px-2 py-1 text-left border-b border-chat-border font-semibold",
|
|
1293
|
+
children: h
|
|
1294
|
+
},
|
|
1295
|
+
i
|
|
1296
|
+
)) }) }),
|
|
1297
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tbody", { children: element.rows.map((row, rIdx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tr", { children: row.map((cell, cIdx) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { className: "px-2 py-1 border-b border-chat-border", children: cell }, cIdx)) }, rIdx)) })
|
|
1298
|
+
] }, `el-${elIndex}`);
|
|
1299
|
+
case "link_button":
|
|
1300
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1301
|
+
"a",
|
|
1302
|
+
{
|
|
1303
|
+
href: element.url,
|
|
1304
|
+
target: "_blank",
|
|
1305
|
+
rel: "noopener noreferrer",
|
|
1306
|
+
className: `block px-2 py-1.5 mx-2 my-1 rounded text-sm font-medium text-center no-underline ${element.style === "primary" ? "bg-chat-primary text-white" : element.style === "danger" ? "bg-chat-error text-white" : "bg-chat-surface text-chat-text hover:bg-chat-background"}`,
|
|
1307
|
+
children: element.label
|
|
1308
|
+
},
|
|
1309
|
+
`el-${elIndex}`
|
|
1310
|
+
);
|
|
1311
|
+
case "image":
|
|
1312
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1313
|
+
"img",
|
|
1314
|
+
{
|
|
1315
|
+
src: element.url,
|
|
1316
|
+
alt: element.alt || "",
|
|
1317
|
+
className: "block w-full h-auto max-h-36 object-cover"
|
|
1318
|
+
},
|
|
1319
|
+
`el-${elIndex}`
|
|
1320
|
+
);
|
|
1321
|
+
default:
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
}),
|
|
1325
|
+
card.actions && card.actions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex flex-wrap gap-2 px-3 py-2", "data-chat-actions": "true", children: card.actions.map((action) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
1326
|
+
"button",
|
|
1327
|
+
{
|
|
1328
|
+
onClick: () => onActionClick?.(action.id, action.value || ""),
|
|
1329
|
+
className: `px-3 py-1.5 border border-chat-border rounded text-sm font-medium transition cursor-pointer ${action.style === "primary" ? "bg-chat-primary text-white border-transparent hover:bg-chat-primary-hover" : action.style === "danger" ? "bg-chat-error text-white border-transparent hover:opacity-90" : "bg-chat-surface text-chat-text hover:bg-chat-background"}`,
|
|
1330
|
+
"data-chat-action": action.id,
|
|
1331
|
+
children: action.label
|
|
1332
|
+
},
|
|
1333
|
+
action.id
|
|
1334
|
+
)) })
|
|
1335
|
+
]
|
|
1336
|
+
}
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/cards/ImageCard.tsx
|
|
1341
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
1342
|
+
function ImageCardComponent({ card: rawCard }) {
|
|
1343
|
+
if (rawCard.type !== "image") {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
const card = rawCard;
|
|
1347
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "rounded-lg overflow-hidden max-w-full", "data-chat-card": "image", children: [
|
|
1348
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1349
|
+
"img",
|
|
1350
|
+
{
|
|
1351
|
+
src: card.url,
|
|
1352
|
+
alt: card.alt || "",
|
|
1353
|
+
className: "block max-w-full h-auto",
|
|
1354
|
+
"data-chat-image": "true"
|
|
1355
|
+
}
|
|
1356
|
+
),
|
|
1357
|
+
card.title && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "px-3 py-2 text-sm bg-chat-surface", children: card.title })
|
|
1358
|
+
] });
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/utils/formatSize.ts
|
|
1362
|
+
function formatSize(bytes) {
|
|
1363
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1364
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1365
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/cards/FileCard.tsx
|
|
1369
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
1370
|
+
function FileCardComponent({ card: rawCard }) {
|
|
1371
|
+
if (rawCard.type !== "file") {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
const card = rawCard;
|
|
1375
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
1376
|
+
"div",
|
|
1377
|
+
{
|
|
1378
|
+
className: "flex items-center gap-3 p-3 border border-chat-border rounded-lg max-w-xs",
|
|
1379
|
+
"data-chat-card": "file",
|
|
1380
|
+
children: [
|
|
1381
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "w-10 h-10 flex items-center justify-center bg-chat-surface rounded", children: "\u{1F4C4}" }),
|
|
1382
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
1383
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-sm font-medium truncate", children: card.name }),
|
|
1384
|
+
card.size && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-xs text-chat-text-secondary", children: formatSize(card.size) })
|
|
1385
|
+
] }),
|
|
1386
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1387
|
+
"a",
|
|
1388
|
+
{
|
|
1389
|
+
href: card.url,
|
|
1390
|
+
download: card.name,
|
|
1391
|
+
className: "px-3 py-2 bg-chat-primary text-white rounded text-sm font-medium no-underline hover:opacity-90",
|
|
1392
|
+
"data-chat-file-download": "true",
|
|
1393
|
+
children: "Download"
|
|
1394
|
+
}
|
|
1395
|
+
)
|
|
1396
|
+
]
|
|
1397
|
+
}
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// src/cards/CardContext.tsx
|
|
1402
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
1403
|
+
var CardContext = (0, import_react9.createContext)(void 0);
|
|
1404
|
+
function CardProvider({ children, renderers }) {
|
|
1405
|
+
const value = (0, import_react9.useMemo)(() => {
|
|
1406
|
+
const defaultRenderers = /* @__PURE__ */ new Map([
|
|
1407
|
+
["card", DefaultCard],
|
|
1408
|
+
["image", ImageCardComponent],
|
|
1409
|
+
["file", FileCardComponent]
|
|
1410
|
+
]);
|
|
1411
|
+
if (renderers) {
|
|
1412
|
+
Object.entries(renderers).forEach(([type, renderer2]) => {
|
|
1413
|
+
defaultRenderers.set(type, renderer2);
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
return {
|
|
1417
|
+
renderers: defaultRenderers,
|
|
1418
|
+
registerRenderer: (type, renderer2) => {
|
|
1419
|
+
defaultRenderers.set(type, renderer2);
|
|
1420
|
+
},
|
|
1421
|
+
getRenderer: (type) => {
|
|
1422
|
+
return defaultRenderers.get(type);
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
}, [renderers]);
|
|
1426
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CardContext.Provider, { value, children });
|
|
1427
|
+
}
|
|
1428
|
+
function useCardRegistry() {
|
|
1429
|
+
const context = (0, import_react9.useContext)(CardContext);
|
|
1430
|
+
if (!context) {
|
|
1431
|
+
throw new Error("useCardRegistry must be used within CardProvider");
|
|
1432
|
+
}
|
|
1433
|
+
return context;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// src/cards/CardRenderer.tsx
|
|
1437
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1438
|
+
function CardRenderer({ card, onActionClick }) {
|
|
1439
|
+
const { getRenderer } = useCardRegistry();
|
|
1440
|
+
const Renderer = getRenderer(card.type);
|
|
1441
|
+
if (Renderer) {
|
|
1442
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Renderer, { card, onActionClick });
|
|
1443
|
+
}
|
|
1444
|
+
if (isPHPCard(card)) {
|
|
1445
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(DefaultCard, { card, onActionClick });
|
|
1446
|
+
}
|
|
1447
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { padding: "8px", background: "#f3f4f6", borderRadius: "4px" }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { style: { fontSize: "12px", overflow: "auto" }, children: JSON.stringify(card, null, 2) }) });
|
|
1448
|
+
}
|
|
1449
|
+
function isPHPCard(card) {
|
|
1450
|
+
return card.type === "card";
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// src/components/MessageContent.tsx
|
|
1454
|
+
var import_jsx_runtime10 = require("react/jsx-runtime");
|
|
1455
|
+
function MessageContent({ message, onActionClick }) {
|
|
1456
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { "data-chat-message-content": "true", children: [
|
|
1457
|
+
message.content.text && !message.content.cards?.length && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "break-words text-sm leading-relaxed", "data-chat-text": "true", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MarkdownRenderer, { text: message.content.text }) }),
|
|
1458
|
+
message.content.cards?.map((card, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: index > 0 ? "mt-2" : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1459
|
+
CardRenderer,
|
|
1460
|
+
{
|
|
1461
|
+
card,
|
|
1462
|
+
onActionClick: (actionId, value) => onActionClick?.(message.id, actionId, value)
|
|
1463
|
+
}
|
|
1464
|
+
) }, index)),
|
|
1465
|
+
message.attachments?.map((attachment) => {
|
|
1466
|
+
const isImage = attachment.type === "image" || attachment.mimeType?.startsWith("image/");
|
|
1467
|
+
return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "mt-2", children: isImage ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("a", { href: attachment.url, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1468
|
+
"img",
|
|
1469
|
+
{
|
|
1470
|
+
src: attachment.url,
|
|
1471
|
+
alt: attachment.name || "Image",
|
|
1472
|
+
className: "max-w-full rounded object-cover",
|
|
1473
|
+
loading: "lazy",
|
|
1474
|
+
"data-chat-attachment": attachment.id
|
|
1475
|
+
}
|
|
1476
|
+
) }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
1477
|
+
"a",
|
|
1478
|
+
{
|
|
1479
|
+
href: attachment.url,
|
|
1480
|
+
target: "_blank",
|
|
1481
|
+
rel: "noopener noreferrer",
|
|
1482
|
+
className: "inline-flex items-center gap-2 px-3 py-2 bg-chat-surface rounded text-sm no-underline text-chat-text hover:bg-chat-background transition",
|
|
1483
|
+
"data-chat-attachment": attachment.id,
|
|
1484
|
+
children: [
|
|
1485
|
+
"\u{1F4CE} ",
|
|
1486
|
+
attachment.name
|
|
1487
|
+
]
|
|
1488
|
+
}
|
|
1489
|
+
) }, attachment.id);
|
|
1490
|
+
})
|
|
1491
|
+
] });
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/utils/formatTimestamp.ts
|
|
1495
|
+
function formatTimestamp(timestamp) {
|
|
1496
|
+
const date = new Date(timestamp);
|
|
1497
|
+
const now = /* @__PURE__ */ new Date();
|
|
1498
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1499
|
+
const diffMins = Math.floor(diffMs / 6e4);
|
|
1500
|
+
if (diffMins < 1) return "Just now";
|
|
1501
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
1502
|
+
if (diffMins < 1440) return `${Math.floor(diffMins / 60)}h ago`;
|
|
1503
|
+
return date.toLocaleDateString();
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/components/MessageList.tsx
|
|
1507
|
+
var import_jsx_runtime11 = require("react/jsx-runtime");
|
|
1508
|
+
function MessageList({
|
|
1509
|
+
messages,
|
|
1510
|
+
currentUserId,
|
|
1511
|
+
isLoading = false,
|
|
1512
|
+
onReactionClick,
|
|
1513
|
+
onActionClick,
|
|
1514
|
+
className
|
|
1515
|
+
}) {
|
|
1516
|
+
const { t } = useLocale();
|
|
1517
|
+
const containerRef = (0, import_react10.useRef)(null);
|
|
1518
|
+
const listEndRef = (0, import_react10.useRef)(null);
|
|
1519
|
+
const hasInitiallyScrolled = (0, import_react10.useRef)(false);
|
|
1520
|
+
const isNearBottom = (0, import_react10.useRef)(true);
|
|
1521
|
+
(0, import_react10.useEffect)(() => {
|
|
1522
|
+
if (!hasInitiallyScrolled.current && messages.length > 0) {
|
|
1523
|
+
listEndRef.current?.scrollIntoView?.();
|
|
1524
|
+
hasInitiallyScrolled.current = true;
|
|
1525
|
+
}
|
|
1526
|
+
}, [messages.length]);
|
|
1527
|
+
const prevMessagesLength = (0, import_react10.useRef)(messages.length);
|
|
1528
|
+
(0, import_react10.useEffect)(() => {
|
|
1529
|
+
if (messages.length > prevMessagesLength.current) {
|
|
1530
|
+
listEndRef.current?.scrollIntoView?.({ behavior: "smooth" });
|
|
1531
|
+
}
|
|
1532
|
+
prevMessagesLength.current = messages.length;
|
|
1533
|
+
}, [messages.length]);
|
|
1534
|
+
(0, import_react10.useEffect)(() => {
|
|
1535
|
+
const el = containerRef.current;
|
|
1536
|
+
if (!el) return;
|
|
1537
|
+
const handleScroll = () => {
|
|
1538
|
+
const threshold = 100;
|
|
1539
|
+
isNearBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
1540
|
+
};
|
|
1541
|
+
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
1542
|
+
const observer = new ResizeObserver(() => {
|
|
1543
|
+
if (isNearBottom.current) {
|
|
1544
|
+
listEndRef.current?.scrollIntoView?.({ behavior: "smooth" });
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
observer.observe(el);
|
|
1548
|
+
return () => {
|
|
1549
|
+
el.removeEventListener("scroll", handleScroll);
|
|
1550
|
+
observer.disconnect();
|
|
1551
|
+
};
|
|
1552
|
+
}, []);
|
|
1553
|
+
const groupedMessages = (0, import_react10.useMemo)(() => {
|
|
1554
|
+
const groups = [];
|
|
1555
|
+
let currentGroup = null;
|
|
1556
|
+
for (const message of messages) {
|
|
1557
|
+
const userId = message.author.id;
|
|
1558
|
+
if (!currentGroup || currentGroup.user !== userId) {
|
|
1559
|
+
if (currentGroup) {
|
|
1560
|
+
groups.push(currentGroup);
|
|
1561
|
+
}
|
|
1562
|
+
currentGroup = { user: userId, messages: [message] };
|
|
1563
|
+
} else {
|
|
1564
|
+
currentGroup.messages.push(message);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
if (currentGroup) {
|
|
1568
|
+
groups.push(currentGroup);
|
|
1569
|
+
}
|
|
1570
|
+
return groups;
|
|
1571
|
+
}, [messages]);
|
|
1572
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1573
|
+
"div",
|
|
1574
|
+
{
|
|
1575
|
+
ref: containerRef,
|
|
1576
|
+
className: `chat-message-list flex-1 min-h-0 overflow-y-auto ${className || ""}`,
|
|
1577
|
+
"data-chat-message-list": "true",
|
|
1578
|
+
"data-testid": "chat-message-list",
|
|
1579
|
+
children: [
|
|
1580
|
+
groupedMessages.length === 0 && !isLoading && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex flex-col items-center justify-center h-full min-h-[200px] text-center px-6", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-chat-text-secondary text-sm", children: t("messageList.emptyState") }) }),
|
|
1581
|
+
groupedMessages.map((group, groupIndex) => {
|
|
1582
|
+
const isOwn = group.user === currentUserId;
|
|
1583
|
+
const firstMessage = group.messages[0];
|
|
1584
|
+
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-1", children: [
|
|
1585
|
+
firstMessage.author.name && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-xs text-chat-text-secondary", children: firstMessage.author.name }),
|
|
1586
|
+
group.messages.map((message, msgIndex) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col", "data-chat-message-id": message.id, children: [
|
|
1587
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: isOwn ? "chat-message-bubble-own" : "chat-message-bubble-other", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MessageContent, { message, onActionClick }) }),
|
|
1588
|
+
message.reactions && message.reactions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex gap-1 mt-1", children: message.reactions.map((reaction, rIndex) => /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
1589
|
+
"button",
|
|
1590
|
+
{
|
|
1591
|
+
onClick: () => onReactionClick?.(message.id, reaction.emoji),
|
|
1592
|
+
className: `flex items-center gap-1 px-2 py-0.5 border border-chat-border rounded-full text-sm cursor-pointer transition ${reaction.hasReacted ? "bg-chat-surface" : "bg-transparent hover:bg-chat-surface"}`,
|
|
1593
|
+
"data-chat-reaction": reaction.emoji,
|
|
1594
|
+
children: [
|
|
1595
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: reaction.emoji }),
|
|
1596
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-chat-text-secondary", children: reaction.count })
|
|
1597
|
+
]
|
|
1598
|
+
},
|
|
1599
|
+
`${reaction.emoji}-${rIndex}`
|
|
1600
|
+
)) }),
|
|
1601
|
+
msgIndex === 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-xs text-chat-text-secondary mt-1", children: formatTimestamp(message.timestamp) })
|
|
1602
|
+
] }, message.id))
|
|
1603
|
+
] }, `${group.user}-${groupIndex}`);
|
|
1604
|
+
}),
|
|
1605
|
+
isLoading && groupedMessages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex items-center justify-center min-h-[200px]", "data-chat-loading": "true", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex gap-1.5", children: [
|
|
1606
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1607
|
+
"span",
|
|
1608
|
+
{
|
|
1609
|
+
className: "w-2 h-2 rounded-full bg-chat-text-secondary animate-bounce",
|
|
1610
|
+
style: { animationDelay: "0ms" }
|
|
1611
|
+
}
|
|
1612
|
+
),
|
|
1613
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1614
|
+
"span",
|
|
1615
|
+
{
|
|
1616
|
+
className: "w-2 h-2 rounded-full bg-chat-text-secondary animate-bounce",
|
|
1617
|
+
style: { animationDelay: "160ms" }
|
|
1618
|
+
}
|
|
1619
|
+
),
|
|
1620
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
|
|
1621
|
+
"span",
|
|
1622
|
+
{
|
|
1623
|
+
className: "w-2 h-2 rounded-full bg-chat-text-secondary animate-bounce",
|
|
1624
|
+
style: { animationDelay: "320ms" }
|
|
1625
|
+
}
|
|
1626
|
+
)
|
|
1627
|
+
] }) }),
|
|
1628
|
+
isLoading && groupedMessages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex justify-center py-4", "data-chat-loading": "true", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "text-chat-text-secondary", children: t("common.loading") }) }),
|
|
1629
|
+
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: listEndRef, className: "h-px" })
|
|
1630
|
+
]
|
|
1631
|
+
}
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/components/InputArea.tsx
|
|
1636
|
+
var import_react12 = require("react");
|
|
1637
|
+
|
|
1638
|
+
// src/components/Dropzone.tsx
|
|
1639
|
+
var import_react11 = require("react");
|
|
1640
|
+
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
1641
|
+
function Dropzone({
|
|
1642
|
+
onFilesSelected,
|
|
1643
|
+
disabled = false,
|
|
1644
|
+
accept,
|
|
1645
|
+
maxSize,
|
|
1646
|
+
multiple = true,
|
|
1647
|
+
className
|
|
1648
|
+
}) {
|
|
1649
|
+
const { t } = useLocale();
|
|
1650
|
+
const [isDragging, setIsDragging] = (0, import_react11.useState)(false);
|
|
1651
|
+
const inputRef = (0, import_react11.useRef)(null);
|
|
1652
|
+
const dragCounter = (0, import_react11.useRef)(0);
|
|
1653
|
+
const handleDragEnter = (0, import_react11.useCallback)((e) => {
|
|
1654
|
+
e.preventDefault();
|
|
1655
|
+
e.stopPropagation();
|
|
1656
|
+
dragCounter.current++;
|
|
1657
|
+
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
|
1658
|
+
setIsDragging(true);
|
|
1659
|
+
}
|
|
1660
|
+
}, []);
|
|
1661
|
+
const handleDragLeave = (0, import_react11.useCallback)((e) => {
|
|
1662
|
+
e.preventDefault();
|
|
1663
|
+
e.stopPropagation();
|
|
1664
|
+
dragCounter.current--;
|
|
1665
|
+
if (dragCounter.current === 0) {
|
|
1666
|
+
setIsDragging(false);
|
|
1667
|
+
}
|
|
1668
|
+
}, []);
|
|
1669
|
+
const handleDragOver = (0, import_react11.useCallback)((e) => {
|
|
1670
|
+
e.preventDefault();
|
|
1671
|
+
e.stopPropagation();
|
|
1672
|
+
}, []);
|
|
1673
|
+
const handleDrop = (0, import_react11.useCallback)(
|
|
1674
|
+
(e) => {
|
|
1675
|
+
e.preventDefault();
|
|
1676
|
+
e.stopPropagation();
|
|
1677
|
+
setIsDragging(false);
|
|
1678
|
+
dragCounter.current = 0;
|
|
1679
|
+
if (disabled) return;
|
|
1680
|
+
const files = e.dataTransfer.files;
|
|
1681
|
+
if (files && files.length > 0) {
|
|
1682
|
+
const filtered = filterFiles(files, maxSize, accept);
|
|
1683
|
+
if (filtered.length > 0) {
|
|
1684
|
+
onFilesSelected(multiple ? filtered : [filtered[0]]);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
[disabled, maxSize, accept, multiple, onFilesSelected]
|
|
1689
|
+
);
|
|
1690
|
+
const handleInputChange = (0, import_react11.useCallback)(
|
|
1691
|
+
(e) => {
|
|
1692
|
+
if (disabled) return;
|
|
1693
|
+
const files = e.target.files;
|
|
1694
|
+
if (files && files.length > 0) {
|
|
1695
|
+
const filtered = filterFiles(files, maxSize, accept);
|
|
1696
|
+
if (filtered.length > 0) {
|
|
1697
|
+
onFilesSelected(multiple ? filtered : [filtered[0]]);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (inputRef.current) inputRef.current.value = "";
|
|
1701
|
+
},
|
|
1702
|
+
[disabled, maxSize, accept, multiple, onFilesSelected]
|
|
1703
|
+
);
|
|
1704
|
+
const handleClick = (0, import_react11.useCallback)(() => {
|
|
1705
|
+
inputRef.current?.click();
|
|
1706
|
+
}, []);
|
|
1707
|
+
function filterFiles(files, maxSize2, accept2) {
|
|
1708
|
+
return Array.from(files).filter((file) => {
|
|
1709
|
+
if (maxSize2 && file.size > maxSize2) return false;
|
|
1710
|
+
if (accept2) {
|
|
1711
|
+
const accepted = accept2.split(",").map((a) => a.trim().toLowerCase());
|
|
1712
|
+
const mimeType = file.type.toLowerCase();
|
|
1713
|
+
const ext = "." + file.name.split(".").pop()?.toLowerCase();
|
|
1714
|
+
return accepted.some((a) => {
|
|
1715
|
+
if (a.endsWith("/*")) return mimeType.startsWith(a.replace("/*", "/"));
|
|
1716
|
+
return a === mimeType || a === ext;
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
return true;
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
1723
|
+
"div",
|
|
1724
|
+
{
|
|
1725
|
+
role: "button",
|
|
1726
|
+
tabIndex: 0,
|
|
1727
|
+
onClick: handleClick,
|
|
1728
|
+
onKeyDown: (e) => {
|
|
1729
|
+
if (e.key === "Enter" || e.key === " ") handleClick();
|
|
1730
|
+
},
|
|
1731
|
+
onDragEnter: handleDragEnter,
|
|
1732
|
+
onDragLeave: handleDragLeave,
|
|
1733
|
+
onDragOver: handleDragOver,
|
|
1734
|
+
onDrop: handleDrop,
|
|
1735
|
+
className: `flex items-center justify-center p-2 border-2 border-dashed rounded-lg cursor-pointer transition ${isDragging ? "border-chat-primary bg-chat-primary/10" : "border-chat-border"} ${disabled ? "cursor-not-allowed opacity-50" : ""} ${className || ""}`,
|
|
1736
|
+
"data-chat-dropzone": "true",
|
|
1737
|
+
"data-testid": "chat-dropzone",
|
|
1738
|
+
children: [
|
|
1739
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
|
|
1740
|
+
"input",
|
|
1741
|
+
{
|
|
1742
|
+
ref: inputRef,
|
|
1743
|
+
type: "file",
|
|
1744
|
+
accept,
|
|
1745
|
+
multiple,
|
|
1746
|
+
onChange: handleInputChange,
|
|
1747
|
+
className: "hidden",
|
|
1748
|
+
disabled,
|
|
1749
|
+
"aria-hidden": "true"
|
|
1750
|
+
}
|
|
1751
|
+
),
|
|
1752
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "text-center", children: [
|
|
1753
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
1754
|
+
"svg",
|
|
1755
|
+
{
|
|
1756
|
+
width: "20",
|
|
1757
|
+
height: "20",
|
|
1758
|
+
viewBox: "0 0 24 24",
|
|
1759
|
+
fill: "none",
|
|
1760
|
+
stroke: isDragging ? "var(--chat-primary)" : "var(--chat-text-secondary)",
|
|
1761
|
+
strokeWidth: "2",
|
|
1762
|
+
strokeLinecap: "round",
|
|
1763
|
+
strokeLinejoin: "round",
|
|
1764
|
+
className: "mx-auto mb-1",
|
|
1765
|
+
children: [
|
|
1766
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
|
|
1767
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("polyline", { points: "17 8 12 3 7 8" }),
|
|
1768
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
|
|
1769
|
+
]
|
|
1770
|
+
}
|
|
1771
|
+
),
|
|
1772
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "text-xs text-chat-text-secondary", children: isDragging ? t("inputArea.dropzone.dropFiles") : t("inputArea.dropzone.dropOrClick") })
|
|
1773
|
+
] })
|
|
1774
|
+
]
|
|
1775
|
+
}
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/components/AttachmentList.tsx
|
|
1780
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
1781
|
+
function AttachmentList({
|
|
1782
|
+
attachments,
|
|
1783
|
+
onRemove,
|
|
1784
|
+
className
|
|
1785
|
+
}) {
|
|
1786
|
+
const { t } = useLocale();
|
|
1787
|
+
if (attachments.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_jsx_runtime13.Fragment, {});
|
|
1788
|
+
function getFileIcon(mimeType) {
|
|
1789
|
+
if (mimeType.startsWith("image/")) return "\u{1F5BC}\uFE0F";
|
|
1790
|
+
if (mimeType === "application/pdf") return "\u{1F4C4}";
|
|
1791
|
+
if (mimeType.includes("video")) return "\u{1F3AC}";
|
|
1792
|
+
if (mimeType.includes("audio")) return "\u{1F3B5}";
|
|
1793
|
+
return "\u{1F4CE}";
|
|
1794
|
+
}
|
|
1795
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: `flex flex-wrap gap-1 p-1 ${className || ""}`, "data-chat-attachment-list": "true", children: attachments.map((att) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
|
|
1796
|
+
"div",
|
|
1797
|
+
{
|
|
1798
|
+
className: `flex items-center gap-1 px-2 py-1 text-xs rounded border max-w-[200px] ${att.status === "error" ? "bg-chat-error/15 border-chat-error" : "bg-chat-surface shadow-sm border-chat-border"}`,
|
|
1799
|
+
"data-chat-attachment-item": att.id,
|
|
1800
|
+
children: [
|
|
1801
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: getFileIcon(att.mimeType) }),
|
|
1802
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex-1 min-w-0", children: [
|
|
1803
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1804
|
+
"div",
|
|
1805
|
+
{
|
|
1806
|
+
className: `overflow-hidden text-ellipsis whitespace-nowrap ${att.status === "error" ? "text-chat-error" : "text-chat-text"}`,
|
|
1807
|
+
children: att.name
|
|
1808
|
+
}
|
|
1809
|
+
),
|
|
1810
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "text-[10px] text-chat-text-secondary", children: att.status === "uploading" ? `${att.progress}%` : att.status === "error" ? att.error || t("attachmentList.uploadFailed") : formatSize(att.size) }),
|
|
1811
|
+
att.status === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "h-0.5 bg-chat-border rounded overflow-hidden mt-0.5", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1812
|
+
"div",
|
|
1813
|
+
{
|
|
1814
|
+
className: "h-full bg-chat-primary transition-[width] duration-150",
|
|
1815
|
+
style: { width: `${att.progress}%` }
|
|
1816
|
+
}
|
|
1817
|
+
) })
|
|
1818
|
+
] }),
|
|
1819
|
+
onRemove && att.status !== "uploading" && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
|
|
1820
|
+
"button",
|
|
1821
|
+
{
|
|
1822
|
+
onClick: () => onRemove(att.id),
|
|
1823
|
+
className: "bg-none border-none cursor-pointer text-chat-text-secondary p-0.5 leading-none hover:text-chat-error",
|
|
1824
|
+
"aria-label": `Remove ${att.name}`,
|
|
1825
|
+
children: "\xD7"
|
|
1826
|
+
}
|
|
1827
|
+
)
|
|
1828
|
+
]
|
|
1829
|
+
},
|
|
1830
|
+
att.id
|
|
1831
|
+
)) });
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/components/InputArea.tsx
|
|
1835
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
1836
|
+
function InputArea({
|
|
1837
|
+
onSend,
|
|
1838
|
+
disabled = false,
|
|
1839
|
+
placeholder = "Type a message...",
|
|
1840
|
+
className,
|
|
1841
|
+
enableAttachments = false,
|
|
1842
|
+
uploadConfig,
|
|
1843
|
+
accept,
|
|
1844
|
+
maxFileSize
|
|
1845
|
+
}) {
|
|
1846
|
+
const [text, setText] = (0, import_react12.useState)("");
|
|
1847
|
+
const [showDropzone, setShowDropzone] = (0, import_react12.useState)(false);
|
|
1848
|
+
const textareaRef = (0, import_react12.useRef)(null);
|
|
1849
|
+
const sendingRef = (0, import_react12.useRef)(false);
|
|
1850
|
+
const { attachments, addFiles, removeAttachment, clearAttachments, isUploading } = useAttachmentUpload(uploadConfig);
|
|
1851
|
+
const { t } = useLocale();
|
|
1852
|
+
(0, import_react12.useEffect)(() => {
|
|
1853
|
+
const textarea = textareaRef.current;
|
|
1854
|
+
if (!textarea) return;
|
|
1855
|
+
textarea.style.height = "auto";
|
|
1856
|
+
textarea.style.height = Math.min(textarea.scrollHeight, 120) + "px";
|
|
1857
|
+
}, [text]);
|
|
1858
|
+
const handleSubmit = async () => {
|
|
1859
|
+
const trimmed = text.trim();
|
|
1860
|
+
if (!trimmed && attachments.length === 0 || disabled || sendingRef.current) return;
|
|
1861
|
+
if (isUploading) return;
|
|
1862
|
+
sendingRef.current = true;
|
|
1863
|
+
const uploadedAttachments = attachments.filter((a) => a.status === "uploaded" && a.url).map((a) => ({
|
|
1864
|
+
url: a.url,
|
|
1865
|
+
name: a.name,
|
|
1866
|
+
mimeType: a.mimeType,
|
|
1867
|
+
size: a.size
|
|
1868
|
+
}));
|
|
1869
|
+
setText("");
|
|
1870
|
+
setShowDropzone(false);
|
|
1871
|
+
clearAttachments();
|
|
1872
|
+
try {
|
|
1873
|
+
await onSend(trimmed, uploadedAttachments);
|
|
1874
|
+
} finally {
|
|
1875
|
+
sendingRef.current = false;
|
|
1876
|
+
}
|
|
1877
|
+
setTimeout(() => textareaRef.current?.focus(), 0);
|
|
1878
|
+
};
|
|
1879
|
+
const handleKeyDown = (e) => {
|
|
1880
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
1881
|
+
e.preventDefault();
|
|
1882
|
+
handleSubmit();
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
const canSend = (text.trim().length > 0 || attachments.some((a) => a.status === "uploaded")) && !isUploading;
|
|
1886
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1887
|
+
"div",
|
|
1888
|
+
{
|
|
1889
|
+
className: `chat-input-area ${className || ""}`,
|
|
1890
|
+
"data-chat-input-area": "true",
|
|
1891
|
+
"data-testid": "chat-input-area",
|
|
1892
|
+
children: [
|
|
1893
|
+
enableAttachments && attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(AttachmentList, { attachments, onRemove: removeAttachment }),
|
|
1894
|
+
enableAttachments && showDropzone && uploadConfig && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1895
|
+
Dropzone,
|
|
1896
|
+
{
|
|
1897
|
+
onFilesSelected: addFiles,
|
|
1898
|
+
disabled: disabled || isUploading,
|
|
1899
|
+
accept,
|
|
1900
|
+
maxSize: maxFileSize
|
|
1901
|
+
}
|
|
1902
|
+
),
|
|
1903
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex gap-3", children: [
|
|
1904
|
+
enableAttachments && uploadConfig && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1905
|
+
"button",
|
|
1906
|
+
{
|
|
1907
|
+
onClick: () => setShowDropzone((prev) => !prev),
|
|
1908
|
+
disabled,
|
|
1909
|
+
className: `p-2 rounded-lg cursor-pointer transition ${showDropzone ? "bg-chat-primary/10 text-chat-primary" : "bg-transparent text-chat-text-secondary"} disabled:cursor-not-allowed disabled:opacity-50`,
|
|
1910
|
+
"data-chat-attachment-toggle": "true",
|
|
1911
|
+
"aria-label": "Toggle file attachment",
|
|
1912
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1913
|
+
"svg",
|
|
1914
|
+
{
|
|
1915
|
+
width: "20",
|
|
1916
|
+
height: "20",
|
|
1917
|
+
viewBox: "0 0 24 24",
|
|
1918
|
+
fill: "none",
|
|
1919
|
+
stroke: "currentColor",
|
|
1920
|
+
strokeWidth: "2",
|
|
1921
|
+
strokeLinecap: "round",
|
|
1922
|
+
strokeLinejoin: "round",
|
|
1923
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" })
|
|
1924
|
+
}
|
|
1925
|
+
)
|
|
1926
|
+
}
|
|
1927
|
+
),
|
|
1928
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1929
|
+
"textarea",
|
|
1930
|
+
{
|
|
1931
|
+
ref: textareaRef,
|
|
1932
|
+
value: text,
|
|
1933
|
+
onChange: (e) => setText(e.target.value),
|
|
1934
|
+
onKeyDown: handleKeyDown,
|
|
1935
|
+
placeholder,
|
|
1936
|
+
className: "chat-input",
|
|
1937
|
+
"data-chat-input": "true",
|
|
1938
|
+
rows: 1
|
|
1939
|
+
}
|
|
1940
|
+
),
|
|
1941
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1942
|
+
"button",
|
|
1943
|
+
{
|
|
1944
|
+
onClick: handleSubmit,
|
|
1945
|
+
disabled: disabled || !canSend,
|
|
1946
|
+
className: "chat-send-button",
|
|
1947
|
+
"data-chat-send-button": "true",
|
|
1948
|
+
"aria-label": t("inputArea.send"),
|
|
1949
|
+
children: isUploading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
|
|
1950
|
+
"svg",
|
|
1951
|
+
{
|
|
1952
|
+
width: "20",
|
|
1953
|
+
height: "20",
|
|
1954
|
+
viewBox: "0 0 24 24",
|
|
1955
|
+
fill: "none",
|
|
1956
|
+
stroke: "currentColor",
|
|
1957
|
+
strokeWidth: "2",
|
|
1958
|
+
strokeLinecap: "round",
|
|
1959
|
+
strokeLinejoin: "round",
|
|
1960
|
+
className: "animate-spin",
|
|
1961
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
|
|
1962
|
+
}
|
|
1963
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
|
|
1964
|
+
"svg",
|
|
1965
|
+
{
|
|
1966
|
+
width: "20",
|
|
1967
|
+
height: "20",
|
|
1968
|
+
viewBox: "0 0 24 24",
|
|
1969
|
+
fill: "none",
|
|
1970
|
+
stroke: "currentColor",
|
|
1971
|
+
strokeWidth: "2",
|
|
1972
|
+
strokeLinecap: "round",
|
|
1973
|
+
strokeLinejoin: "round",
|
|
1974
|
+
children: [
|
|
1975
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
|
|
1976
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
|
|
1977
|
+
]
|
|
1978
|
+
}
|
|
1979
|
+
)
|
|
1980
|
+
}
|
|
1981
|
+
)
|
|
1982
|
+
] })
|
|
1983
|
+
]
|
|
1984
|
+
}
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// src/components/TypingIndicator.tsx
|
|
1989
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
1990
|
+
function TypingIndicator({ users = [] }) {
|
|
1991
|
+
const { t } = useLocale();
|
|
1992
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
1993
|
+
"div",
|
|
1994
|
+
{
|
|
1995
|
+
className: "chat-typing-indicator",
|
|
1996
|
+
"data-chat-typing-indicator": "true",
|
|
1997
|
+
"data-testid": "chat-typing-indicator",
|
|
1998
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "flex items-center gap-2", children: [
|
|
1999
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "flex gap-1", children: [
|
|
2000
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2001
|
+
"span",
|
|
2002
|
+
{
|
|
2003
|
+
className: "w-1.5 h-1.5 rounded-full bg-chat-text-secondary animate-bounce",
|
|
2004
|
+
style: { animationDelay: "0ms" }
|
|
2005
|
+
}
|
|
2006
|
+
),
|
|
2007
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2008
|
+
"span",
|
|
2009
|
+
{
|
|
2010
|
+
className: "w-1.5 h-1.5 rounded-full bg-chat-text-secondary animate-bounce",
|
|
2011
|
+
style: { animationDelay: "160ms" }
|
|
2012
|
+
}
|
|
2013
|
+
),
|
|
2014
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
2015
|
+
"span",
|
|
2016
|
+
{
|
|
2017
|
+
className: "w-1.5 h-1.5 rounded-full bg-chat-text-secondary animate-bounce",
|
|
2018
|
+
style: { animationDelay: "320ms" }
|
|
2019
|
+
}
|
|
2020
|
+
)
|
|
2021
|
+
] }),
|
|
2022
|
+
users.length > 0 && ` ${users[0]} ${t("typingIndicator.isTyping")}`
|
|
2023
|
+
] })
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/components/ChatWidget.tsx
|
|
2029
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
2030
|
+
function ChatWidget({
|
|
2031
|
+
client,
|
|
2032
|
+
initialMode = "floating",
|
|
2033
|
+
theme: themeProp,
|
|
2034
|
+
onThemeChange,
|
|
2035
|
+
position = "bottom-right",
|
|
2036
|
+
className,
|
|
2037
|
+
showClose = true,
|
|
2038
|
+
showFullscreenToggle = true,
|
|
2039
|
+
title = "Chat",
|
|
2040
|
+
placeholder = "Type a message...",
|
|
2041
|
+
onOpen,
|
|
2042
|
+
onClose,
|
|
2043
|
+
embedded,
|
|
2044
|
+
floatingButton,
|
|
2045
|
+
enableAttachments = false,
|
|
2046
|
+
uploadConfig,
|
|
2047
|
+
accept,
|
|
2048
|
+
maxFileSize,
|
|
2049
|
+
renderPushPrompt
|
|
2050
|
+
}) {
|
|
2051
|
+
const {
|
|
2052
|
+
config: iframeConfig,
|
|
2053
|
+
isInIframe,
|
|
2054
|
+
notifyMessage,
|
|
2055
|
+
notifyViewportConfig,
|
|
2056
|
+
onNotificationClicked
|
|
2057
|
+
} = useBridge();
|
|
2058
|
+
const autoEmbedded = isInIframe && embedded !== false;
|
|
2059
|
+
const effectiveEmbedded = embedded === true || autoEmbedded;
|
|
2060
|
+
const effectiveMode = effectiveEmbedded ? "embedded" : initialMode;
|
|
2061
|
+
const [theme, setTheme] = (0, import_react13.useState)(() => {
|
|
2062
|
+
if (themeProp) return themeProp;
|
|
2063
|
+
try {
|
|
2064
|
+
const stored = localStorage.getItem("chat-theme");
|
|
2065
|
+
if (stored === "light" || stored === "dark" || stored === "auto") return stored;
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
2068
|
+
return "auto";
|
|
2069
|
+
});
|
|
2070
|
+
const [systemDark, setSystemDark] = (0, import_react13.useState)(
|
|
2071
|
+
() => typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
2072
|
+
);
|
|
2073
|
+
const effectiveTheme = theme === "auto" ? systemDark ? "dark" : "light" : theme;
|
|
2074
|
+
(0, import_react13.useEffect)(() => {
|
|
2075
|
+
if (themeProp && themeProp !== theme) {
|
|
2076
|
+
setTheme(themeProp);
|
|
2077
|
+
try {
|
|
2078
|
+
localStorage.setItem("chat-theme", themeProp);
|
|
2079
|
+
} catch {
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}, [themeProp]);
|
|
2083
|
+
(0, import_react13.useEffect)(() => {
|
|
2084
|
+
if (theme !== "auto") return;
|
|
2085
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
2086
|
+
const handler = (e) => setSystemDark(e.matches);
|
|
2087
|
+
mq.addEventListener("change", handler);
|
|
2088
|
+
return () => mq.removeEventListener("change", handler);
|
|
2089
|
+
}, [theme]);
|
|
2090
|
+
const handleThemeChange = (newTheme) => {
|
|
2091
|
+
setTheme(newTheme);
|
|
2092
|
+
try {
|
|
2093
|
+
localStorage.setItem("chat-theme", newTheme);
|
|
2094
|
+
} catch {
|
|
2095
|
+
}
|
|
2096
|
+
onThemeChange?.(newTheme);
|
|
2097
|
+
};
|
|
2098
|
+
const [isOpen, setIsOpen] = (0, import_react13.useState)(effectiveMode === "fullscreen");
|
|
2099
|
+
const [displayMode, setDisplayMode] = (0, import_react13.useState)(effectiveMode);
|
|
2100
|
+
const [isConnected] = (0, import_react13.useState)(true);
|
|
2101
|
+
const [isSmallScreen, setIsSmallScreen] = (0, import_react13.useState)(
|
|
2102
|
+
() => typeof window !== "undefined" && window.innerWidth < 800
|
|
2103
|
+
);
|
|
2104
|
+
(0, import_react13.useEffect)(() => {
|
|
2105
|
+
const mq = window.matchMedia("(max-width: 799px)");
|
|
2106
|
+
const handler = (e) => {
|
|
2107
|
+
setIsSmallScreen(e.matches);
|
|
2108
|
+
if (e.matches) setDisplayMode("fullscreen");
|
|
2109
|
+
};
|
|
2110
|
+
setIsSmallScreen(mq.matches);
|
|
2111
|
+
if (mq.matches) setDisplayMode("fullscreen");
|
|
2112
|
+
mq.addEventListener("change", handler);
|
|
2113
|
+
return () => mq.removeEventListener("change", handler);
|
|
2114
|
+
}, []);
|
|
2115
|
+
(0, import_react13.useEffect)(() => {
|
|
2116
|
+
if (initialMode !== "fullscreen") return;
|
|
2117
|
+
if (!isSmallScreen) setIsOpen(true);
|
|
2118
|
+
}, [initialMode, isSmallScreen]);
|
|
2119
|
+
(0, import_react13.useEffect)(() => {
|
|
2120
|
+
if (isInIframe) {
|
|
2121
|
+
notifyViewportConfig("interactive-widget=resizes-content");
|
|
2122
|
+
return () => notifyViewportConfig("");
|
|
2123
|
+
}
|
|
2124
|
+
const isFullscreen = displayMode === "fullscreen" || isSmallScreen;
|
|
2125
|
+
const active = isOpen && isFullscreen;
|
|
2126
|
+
const meta = document.querySelector('meta[name="viewport"]');
|
|
2127
|
+
if (!meta) return;
|
|
2128
|
+
if (active) {
|
|
2129
|
+
const original = meta.getAttribute("content") ?? "";
|
|
2130
|
+
if (!original.includes("interactive-widget=")) {
|
|
2131
|
+
meta.setAttribute("content", `${original}, interactive-widget=resizes-content`);
|
|
2132
|
+
}
|
|
2133
|
+
return () => {
|
|
2134
|
+
meta.setAttribute("content", original);
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
}, [isOpen, displayMode, isSmallScreen, isInIframe, notifyViewportConfig]);
|
|
2138
|
+
const { messages, sendMessage, loading, isLoadingHistory, reloadMessages } = useMessages(
|
|
2139
|
+
client,
|
|
2140
|
+
isOpen || effectiveEmbedded
|
|
2141
|
+
);
|
|
2142
|
+
const { isSomeoneTyping } = useTyping(client);
|
|
2143
|
+
(0, import_react13.useEffect)(() => {
|
|
2144
|
+
if (!isInIframe || !iframeConfig) return;
|
|
2145
|
+
if (iframeConfig.theme?.cssVariables) {
|
|
2146
|
+
const root = document.documentElement;
|
|
2147
|
+
for (const [key, value] of Object.entries(iframeConfig.theme.cssVariables)) {
|
|
2148
|
+
root.style.setProperty(key, value);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
const mode = iframeConfig.theme?.mode;
|
|
2152
|
+
if (mode === "light" || mode === "dark" || mode === "auto") {
|
|
2153
|
+
setTheme(mode);
|
|
2154
|
+
}
|
|
2155
|
+
}, [isInIframe, iframeConfig]);
|
|
2156
|
+
(0, import_react13.useEffect)(() => {
|
|
2157
|
+
if (!isInIframe) return;
|
|
2158
|
+
onNotificationClicked(() => {
|
|
2159
|
+
reloadMessages();
|
|
2160
|
+
});
|
|
2161
|
+
}, [isInIframe, onNotificationClicked, reloadMessages]);
|
|
2162
|
+
const effectiveTitle = isInIframe && iframeConfig?.title || title;
|
|
2163
|
+
const effectivePlaceholder = isInIframe && iframeConfig?.placeholder || placeholder;
|
|
2164
|
+
const currentUserId = "getCurrentUserId" in client ? client.getCurrentUserId() : "";
|
|
2165
|
+
const handleSend = (0, import_react13.useCallback)(
|
|
2166
|
+
async (text, attachments = []) => {
|
|
2167
|
+
await sendMessage(text, attachments);
|
|
2168
|
+
if (isInIframe) {
|
|
2169
|
+
notifyMessage(text);
|
|
2170
|
+
}
|
|
2171
|
+
},
|
|
2172
|
+
[sendMessage, isInIframe, notifyMessage]
|
|
2173
|
+
);
|
|
2174
|
+
const handleActionClick = (0, import_react13.useCallback)(
|
|
2175
|
+
(messageId, actionId, value) => {
|
|
2176
|
+
client.sendAction(messageId, actionId, value).catch((err) => {
|
|
2177
|
+
console.error("Action failed:", err);
|
|
2178
|
+
});
|
|
2179
|
+
},
|
|
2180
|
+
[client]
|
|
2181
|
+
);
|
|
2182
|
+
const handleReactionClick = (0, import_react13.useCallback)((_messageId, _emoji) => {
|
|
2183
|
+
}, []);
|
|
2184
|
+
const toggleOpen = (0, import_react13.useCallback)(() => {
|
|
2185
|
+
setIsOpen((prev) => {
|
|
2186
|
+
if (!prev) onOpen?.();
|
|
2187
|
+
else onClose?.();
|
|
2188
|
+
return !prev;
|
|
2189
|
+
});
|
|
2190
|
+
}, [onOpen, onClose]);
|
|
2191
|
+
const toggleFullscreen = (0, import_react13.useCallback)(() => {
|
|
2192
|
+
setDisplayMode((prev) => prev === "fullscreen" ? "floating" : "fullscreen");
|
|
2193
|
+
}, []);
|
|
2194
|
+
const close = (0, import_react13.useCallback)(() => {
|
|
2195
|
+
setIsOpen(false);
|
|
2196
|
+
setDisplayMode("floating");
|
|
2197
|
+
onClose?.();
|
|
2198
|
+
}, [onClose]);
|
|
2199
|
+
const embeddedClose = (0, import_react13.useCallback)(() => {
|
|
2200
|
+
if (isInIframe) {
|
|
2201
|
+
window.parent.postMessage({ type: "chat-close" }, "*");
|
|
2202
|
+
}
|
|
2203
|
+
}, [isInIframe]);
|
|
2204
|
+
if (effectiveEmbedded) {
|
|
2205
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
2206
|
+
"div",
|
|
2207
|
+
{
|
|
2208
|
+
className: "flex flex-col h-full min-h-[300px] overflow-hidden bg-chat-background",
|
|
2209
|
+
"data-chat-widget": "embedded",
|
|
2210
|
+
"data-chat-theme": effectiveTheme,
|
|
2211
|
+
children: [
|
|
2212
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2213
|
+
Header,
|
|
2214
|
+
{
|
|
2215
|
+
title: effectiveTitle,
|
|
2216
|
+
isFullscreen: false,
|
|
2217
|
+
showConnectionStatus: true,
|
|
2218
|
+
isConnected,
|
|
2219
|
+
className: className?.header,
|
|
2220
|
+
theme,
|
|
2221
|
+
onThemeChange: handleThemeChange,
|
|
2222
|
+
onClose: isInIframe ? embeddedClose : void 0
|
|
2223
|
+
}
|
|
2224
|
+
),
|
|
2225
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2226
|
+
MessageList,
|
|
2227
|
+
{
|
|
2228
|
+
messages,
|
|
2229
|
+
currentUserId,
|
|
2230
|
+
isLoading: isLoadingHistory || loading,
|
|
2231
|
+
onActionClick: handleActionClick,
|
|
2232
|
+
onReactionClick: handleReactionClick,
|
|
2233
|
+
className: className?.messageList
|
|
2234
|
+
}
|
|
2235
|
+
),
|
|
2236
|
+
isSomeoneTyping && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TypingIndicator, {}),
|
|
2237
|
+
renderPushPrompt?.(),
|
|
2238
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2239
|
+
InputArea,
|
|
2240
|
+
{
|
|
2241
|
+
onSend: handleSend,
|
|
2242
|
+
placeholder: effectivePlaceholder,
|
|
2243
|
+
className: className?.inputArea,
|
|
2244
|
+
enableAttachments,
|
|
2245
|
+
uploadConfig,
|
|
2246
|
+
accept,
|
|
2247
|
+
maxFileSize
|
|
2248
|
+
}
|
|
2249
|
+
)
|
|
2250
|
+
]
|
|
2251
|
+
}
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
|
|
2255
|
+
!effectiveEmbedded && !isOpen && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2256
|
+
FloatingButton,
|
|
2257
|
+
{
|
|
2258
|
+
onClick: toggleOpen,
|
|
2259
|
+
isOpen,
|
|
2260
|
+
position,
|
|
2261
|
+
icon: floatingButton?.icon,
|
|
2262
|
+
openIcon: floatingButton?.openIcon,
|
|
2263
|
+
badgeCount: floatingButton?.badgeCount,
|
|
2264
|
+
size: floatingButton?.size,
|
|
2265
|
+
backgroundColor: floatingButton?.backgroundColor,
|
|
2266
|
+
ariaLabel: floatingButton?.ariaLabel,
|
|
2267
|
+
className: floatingButton?.className
|
|
2268
|
+
}
|
|
2269
|
+
),
|
|
2270
|
+
isOpen && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
2271
|
+
"div",
|
|
2272
|
+
{
|
|
2273
|
+
className: `flex flex-col overflow-hidden ${displayMode === "fullscreen" ? "fixed inset-0 z-50" : `absolute ${position === "bottom-right" ? "bottom-20 right-5" : position === "bottom-left" ? "bottom-20 left-5" : ""} w-[480px] max-w-[min(800px,calc(100dvw-40px))] h-dvh max-h-[min(600px,80dvh)] z-10 shadow-xl border border-chat-border rounded-2xl`} bg-chat-background`,
|
|
2274
|
+
"data-chat-widget": displayMode,
|
|
2275
|
+
"data-chat-position": position,
|
|
2276
|
+
"data-chat-theme": effectiveTheme,
|
|
2277
|
+
children: [
|
|
2278
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2279
|
+
Header,
|
|
2280
|
+
{
|
|
2281
|
+
title: effectiveTitle,
|
|
2282
|
+
onClose: displayMode === "floating" ? close : showClose ? close : void 0,
|
|
2283
|
+
onToggleFullscreen: showFullscreenToggle && !isSmallScreen ? toggleFullscreen : void 0,
|
|
2284
|
+
isFullscreen: displayMode === "fullscreen",
|
|
2285
|
+
showConnectionStatus: true,
|
|
2286
|
+
isConnected,
|
|
2287
|
+
className: className?.header,
|
|
2288
|
+
theme,
|
|
2289
|
+
onThemeChange: handleThemeChange
|
|
2290
|
+
}
|
|
2291
|
+
),
|
|
2292
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2293
|
+
MessageList,
|
|
2294
|
+
{
|
|
2295
|
+
messages,
|
|
2296
|
+
currentUserId,
|
|
2297
|
+
isLoading: isLoadingHistory || loading,
|
|
2298
|
+
onActionClick: handleActionClick,
|
|
2299
|
+
onReactionClick: handleReactionClick,
|
|
2300
|
+
className: className?.messageList
|
|
2301
|
+
}
|
|
2302
|
+
),
|
|
2303
|
+
isSomeoneTyping && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(TypingIndicator, {}),
|
|
2304
|
+
renderPushPrompt?.(),
|
|
2305
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
2306
|
+
InputArea,
|
|
2307
|
+
{
|
|
2308
|
+
onSend: handleSend,
|
|
2309
|
+
placeholder: effectivePlaceholder,
|
|
2310
|
+
disabled: loading,
|
|
2311
|
+
className: className?.inputArea,
|
|
2312
|
+
enableAttachments,
|
|
2313
|
+
uploadConfig,
|
|
2314
|
+
accept,
|
|
2315
|
+
maxFileSize
|
|
2316
|
+
}
|
|
2317
|
+
)
|
|
2318
|
+
]
|
|
2319
|
+
}
|
|
2320
|
+
)
|
|
2321
|
+
] });
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// src/providers/ChatProvider.tsx
|
|
2325
|
+
var import_react14 = require("react");
|
|
2326
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
2327
|
+
var ChatContext = (0, import_react14.createContext)(void 0);
|
|
2328
|
+
function ChatProvider({
|
|
2329
|
+
children,
|
|
2330
|
+
client,
|
|
2331
|
+
cardRenderers
|
|
2332
|
+
}) {
|
|
2333
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(CardProvider, { renderers: cardRenderers, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(ChatContext.Provider, { value: { client }, children }) });
|
|
2334
|
+
}
|
|
2335
|
+
function useChatContext() {
|
|
2336
|
+
const context = (0, import_react14.useContext)(ChatContext);
|
|
2337
|
+
if (!context) {
|
|
2338
|
+
throw new Error("useChatContext must be used within ChatProvider");
|
|
2339
|
+
}
|
|
2340
|
+
return context;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// src/components/ErrorBoundary.tsx
|
|
2344
|
+
var import_react15 = require("react");
|
|
2345
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
2346
|
+
var ErrorBoundary = class extends import_react15.Component {
|
|
2347
|
+
constructor(props) {
|
|
2348
|
+
super(props);
|
|
2349
|
+
this.state = { error: null };
|
|
2350
|
+
}
|
|
2351
|
+
static getDerivedStateFromError(error) {
|
|
2352
|
+
return { error };
|
|
2353
|
+
}
|
|
2354
|
+
componentDidCatch(error, errorInfo) {
|
|
2355
|
+
this.props.onError?.(error, errorInfo);
|
|
2356
|
+
}
|
|
2357
|
+
render() {
|
|
2358
|
+
if (!this.state.error) return this.props.children;
|
|
2359
|
+
const { fallback } = this.props;
|
|
2360
|
+
if (typeof fallback === "function") {
|
|
2361
|
+
return fallback(this.state.error);
|
|
2362
|
+
}
|
|
2363
|
+
if (fallback) return fallback;
|
|
2364
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
|
|
2365
|
+
"div",
|
|
2366
|
+
{
|
|
2367
|
+
className: "flex flex-col items-center justify-center p-6 text-center",
|
|
2368
|
+
"data-chat-error-boundary": "true",
|
|
2369
|
+
children: [
|
|
2370
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-chat-error text-lg font-semibold mb-2", children: "Something went wrong" }),
|
|
2371
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-chat-text-secondary text-sm mb-4", children: this.state.error.message }),
|
|
2372
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
|
|
2373
|
+
"button",
|
|
2374
|
+
{
|
|
2375
|
+
onClick: () => this.setState({ error: null }),
|
|
2376
|
+
className: "px-4 py-2 bg-chat-primary text-white rounded text-sm cursor-pointer hover:opacity-90",
|
|
2377
|
+
children: "Try again"
|
|
2378
|
+
}
|
|
2379
|
+
)
|
|
2380
|
+
]
|
|
2381
|
+
}
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
|
|
2386
|
+
// src/components/PushPermissionPrompt.tsx
|
|
2387
|
+
var import_react16 = require("react");
|
|
2388
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
2389
|
+
function PushPermissionPrompt({
|
|
2390
|
+
autoHide = true,
|
|
2391
|
+
title,
|
|
2392
|
+
description,
|
|
2393
|
+
onStatusChange,
|
|
2394
|
+
getVapidPublicKey,
|
|
2395
|
+
onSubscribe,
|
|
2396
|
+
onUnsubscribe
|
|
2397
|
+
}) {
|
|
2398
|
+
const { t } = useLocale();
|
|
2399
|
+
const { status, isSupported, isSubscribed, subscribe, unsubscribe } = usePushNotifications({
|
|
2400
|
+
enabled: true,
|
|
2401
|
+
getVapidPublicKey,
|
|
2402
|
+
onSubscribe,
|
|
2403
|
+
onUnsubscribe
|
|
2404
|
+
});
|
|
2405
|
+
const [dismissed, setDismissed] = (0, import_react16.useState)(false);
|
|
2406
|
+
if (!isSupported) return null;
|
|
2407
|
+
if (autoHide && (isSubscribed || status === "denied")) return null;
|
|
2408
|
+
if (dismissed) return null;
|
|
2409
|
+
const handleEnable = async () => {
|
|
2410
|
+
await subscribe();
|
|
2411
|
+
onStatusChange?.(true);
|
|
2412
|
+
};
|
|
2413
|
+
const handleDisable = async () => {
|
|
2414
|
+
await unsubscribe();
|
|
2415
|
+
onStatusChange?.(false);
|
|
2416
|
+
};
|
|
2417
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
|
|
2418
|
+
"div",
|
|
2419
|
+
{
|
|
2420
|
+
className: "p-3 bg-chat-surface rounded-lg flex items-start gap-3",
|
|
2421
|
+
"data-chat-push-prompt": "true",
|
|
2422
|
+
children: [
|
|
2423
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex-1", children: [
|
|
2424
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "font-semibold mb-1 text-chat-text", children: title || t("push.title") }),
|
|
2425
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-sm text-chat-text-secondary", children: description || t("push.description") })
|
|
2426
|
+
] }),
|
|
2427
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex gap-2", children: [
|
|
2428
|
+
!isSubscribed && status !== "denied" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2429
|
+
"button",
|
|
2430
|
+
{
|
|
2431
|
+
onClick: handleEnable,
|
|
2432
|
+
className: "px-3 py-1.5 bg-chat-primary text-white border-none rounded cursor-pointer text-sm hover:opacity-90",
|
|
2433
|
+
children: t("push.enable")
|
|
2434
|
+
}
|
|
2435
|
+
),
|
|
2436
|
+
isSubscribed && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2437
|
+
"button",
|
|
2438
|
+
{
|
|
2439
|
+
onClick: handleDisable,
|
|
2440
|
+
className: "px-3 py-1.5 bg-transparent text-chat-text-secondary border border-chat-border rounded cursor-pointer text-sm hover:bg-chat-surface",
|
|
2441
|
+
children: t("push.disable")
|
|
2442
|
+
}
|
|
2443
|
+
),
|
|
2444
|
+
/* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
2445
|
+
"button",
|
|
2446
|
+
{
|
|
2447
|
+
onClick: () => setDismissed(true),
|
|
2448
|
+
className: "px-1.5 bg-transparent border-none cursor-pointer text-chat-text-secondary hover:text-chat-text",
|
|
2449
|
+
children: "\u2715"
|
|
2450
|
+
}
|
|
2451
|
+
)
|
|
2452
|
+
] })
|
|
2453
|
+
]
|
|
2454
|
+
}
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
// src/components/PushToggle.tsx
|
|
2459
|
+
var import_jsx_runtime20 = require("react/jsx-runtime");
|
|
2460
|
+
function PushToggle({ getVapidPublicKey, onSubscribe, onUnsubscribe }) {
|
|
2461
|
+
const { t } = useLocale();
|
|
2462
|
+
const { status, isSupported, isSubscribed, subscribe, unsubscribe } = usePushNotifications({
|
|
2463
|
+
enabled: true,
|
|
2464
|
+
getVapidPublicKey,
|
|
2465
|
+
onSubscribe,
|
|
2466
|
+
onUnsubscribe
|
|
2467
|
+
});
|
|
2468
|
+
if (!isSupported) {
|
|
2469
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "text-sm text-chat-text-secondary", children: t("push.unsupported") });
|
|
2470
|
+
}
|
|
2471
|
+
if (status === "denied") {
|
|
2472
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "text-sm text-chat-text-secondary", children: t("push.denied") });
|
|
2473
|
+
}
|
|
2474
|
+
return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("label", { className: "flex items-center gap-2 cursor-pointer", children: [
|
|
2475
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
|
|
2476
|
+
"input",
|
|
2477
|
+
{
|
|
2478
|
+
type: "checkbox",
|
|
2479
|
+
checked: isSubscribed,
|
|
2480
|
+
onChange: async (e) => {
|
|
2481
|
+
if (e.target.checked) await subscribe();
|
|
2482
|
+
else await unsubscribe();
|
|
2483
|
+
},
|
|
2484
|
+
disabled: status === "subscribing",
|
|
2485
|
+
className: "w-4 h-4 cursor-pointer"
|
|
2486
|
+
}
|
|
2487
|
+
),
|
|
2488
|
+
/* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-sm", children: status === "subscribing" ? t("push.subscribing") : t("push.notifications") })
|
|
2489
|
+
] });
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// src/index.ts
|
|
2493
|
+
var import_js_web_adapter_core3 = require("@bootdesk/js-web-adapter-core");
|
|
2494
|
+
var import_js_web_adapter_core4 = require("@bootdesk/js-web-adapter-core");
|
|
2495
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2496
|
+
0 && (module.exports = {
|
|
2497
|
+
AttachmentList,
|
|
2498
|
+
CardProvider,
|
|
2499
|
+
CardRenderer,
|
|
2500
|
+
ChatProvider,
|
|
2501
|
+
ChatWidget,
|
|
2502
|
+
DefaultCard,
|
|
2503
|
+
Dropzone,
|
|
2504
|
+
ErrorBoundary,
|
|
2505
|
+
FileCardComponent,
|
|
2506
|
+
FloatingButton,
|
|
2507
|
+
Header,
|
|
2508
|
+
ImageCardComponent,
|
|
2509
|
+
InputArea,
|
|
2510
|
+
LaravelEchoBroadcastClient,
|
|
2511
|
+
LocaleProvider,
|
|
2512
|
+
MarkdownRenderer,
|
|
2513
|
+
MessageContent,
|
|
2514
|
+
MessageList,
|
|
2515
|
+
PushManager,
|
|
2516
|
+
PushPermissionPrompt,
|
|
2517
|
+
PushToggle,
|
|
2518
|
+
PusherBroadcastClient,
|
|
2519
|
+
TypingIndicator,
|
|
2520
|
+
WebChatClient,
|
|
2521
|
+
createPushSubscriptionHandlers,
|
|
2522
|
+
getAvailableLocales,
|
|
2523
|
+
registerLocale,
|
|
2524
|
+
renderMarkdown,
|
|
2525
|
+
useAttachmentUpload,
|
|
2526
|
+
useCardRegistry,
|
|
2527
|
+
useCardRendererRegistry,
|
|
2528
|
+
useChatClient,
|
|
2529
|
+
useChatContext,
|
|
2530
|
+
useLocale,
|
|
2531
|
+
useMessages,
|
|
2532
|
+
usePushNotifications,
|
|
2533
|
+
useStreaming,
|
|
2534
|
+
useTyping
|
|
2535
|
+
});
|
|
2536
|
+
//# sourceMappingURL=index.cjs.map
|