@adatechnology/conversations-ui 0.1.0-rc.5 → 0.1.0-rc.6

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.
@@ -0,0 +1,1465 @@
1
+ import {
2
+ parseWhatsAppFormatting
3
+ } from "./chunk-OGRRHQQW.js";
4
+
5
+ // src/ConversationLocalesProvider.tsx
6
+ import { createContext, useContext } from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ var DEFAULT_LOCALES = {
9
+ bubble: {
10
+ customer: "Cliente",
11
+ bot: "Bot",
12
+ agent: "Atendente",
13
+ document: "documento",
14
+ media: "m\xEDdia",
15
+ templateLabel: "Template",
16
+ readAt: "Lida \xE0s ",
17
+ windowExpired: "Janela de 24h expirada",
18
+ viewImage: "Ver imagem",
19
+ listenAudio: "Ouvir \xE1udio",
20
+ viewVideo: "Ver v\xEDdeo",
21
+ moderationFlagged: "Linguagem ofensiva",
22
+ mediaLoading: "Carregando...",
23
+ mediaRetry: "Erro \u2014 tentar novamente",
24
+ mediaError: "Erro",
25
+ mediaUnavailable: "M\xEDdia indispon\xEDvel",
26
+ imageAlt: "Imagem",
27
+ untitledDocument: "Documento",
28
+ downloadFile: "Baixar"
29
+ },
30
+ selection: {
31
+ select: "Selecionar"
32
+ },
33
+ dateDivider: {
34
+ today: "Hoje",
35
+ yesterday: "Ontem"
36
+ }
37
+ };
38
+ var ConversationLocalesContext = createContext(DEFAULT_LOCALES);
39
+ function ConversationLocalesProvider({ children, locales }) {
40
+ const merged = {
41
+ bubble: { ...DEFAULT_LOCALES.bubble, ...locales?.bubble },
42
+ selection: { ...DEFAULT_LOCALES.selection, ...locales?.selection },
43
+ dateDivider: { ...DEFAULT_LOCALES.dateDivider, ...locales?.dateDivider }
44
+ };
45
+ return /* @__PURE__ */ jsx(ConversationLocalesContext.Provider, { value: merged, children });
46
+ }
47
+ function useConversationLocales() {
48
+ return useContext(ConversationLocalesContext);
49
+ }
50
+
51
+ // src/StatusTicks.tsx
52
+ import { AlertTriangle } from "lucide-react";
53
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
54
+ var STATUS_COLOR_CLASS = {
55
+ sent: "text-black/40 dark:text-white/40",
56
+ delivered: "text-black/40 dark:text-white/40",
57
+ read: "text-sky-500",
58
+ failed: "text-red-500"
59
+ };
60
+ function Ticks({ double }) {
61
+ return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 20 12", width: "15", height: "9", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [
62
+ double && /* @__PURE__ */ jsx2("path", { d: "M1 6.5L4.5 10L11 2", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }),
63
+ /* @__PURE__ */ jsx2(
64
+ "path",
65
+ {
66
+ d: double ? "M6 6.5L9.5 10L19 1" : "M1 6.5L5 10.5L14.5 1",
67
+ stroke: "currentColor",
68
+ strokeWidth: "1.6",
69
+ strokeLinecap: "round",
70
+ strokeLinejoin: "round"
71
+ }
72
+ )
73
+ ] });
74
+ }
75
+ function StatusTicks({ status, title }) {
76
+ const colorClass = STATUS_COLOR_CLASS[status] ?? STATUS_COLOR_CLASS.sent;
77
+ return /* @__PURE__ */ jsx2("span", { className: `cursor-help leading-none flex items-center ${colorClass}`, title, children: status === "failed" ? /* @__PURE__ */ jsx2(AlertTriangle, { size: 11 }) : /* @__PURE__ */ jsx2(Ticks, { double: status !== "sent" }) });
78
+ }
79
+
80
+ // src/AudioPlayer.tsx
81
+ import { useEffect, useRef, useState } from "react";
82
+ import { Pause, Play } from "lucide-react";
83
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
84
+ var BARS = Array.from({ length: 30 }, (_, i) => {
85
+ const heights = [3, 5, 8, 12, 7, 10, 14, 9, 6, 11, 15, 8, 4, 13, 7, 10, 6, 12, 9, 5, 14, 8, 11, 6, 13, 7, 10, 5, 9, 4];
86
+ return heights[i % heights.length];
87
+ });
88
+ function fmt(sec) {
89
+ if (!isFinite(sec)) return "0:00";
90
+ const m = Math.floor(sec / 60);
91
+ const s = Math.floor(sec % 60);
92
+ return `${m}:${s.toString().padStart(2, "0")}`;
93
+ }
94
+ function AudioPlayer({ src, isMine = false }) {
95
+ const audioRef = useRef(null);
96
+ const [playing, setPlaying] = useState(false);
97
+ const [progress, setProgress] = useState(0);
98
+ const [duration, setDuration] = useState(0);
99
+ const [current, setCurrent] = useState(0);
100
+ useEffect(() => {
101
+ const audio = audioRef.current;
102
+ if (!audio) return;
103
+ const onTime = () => {
104
+ setCurrent(audio.currentTime);
105
+ setProgress(audio.duration ? audio.currentTime / audio.duration : 0);
106
+ };
107
+ const onMeta = () => setDuration(audio.duration);
108
+ const onEnd = () => {
109
+ setPlaying(false);
110
+ setProgress(0);
111
+ setCurrent(0);
112
+ };
113
+ audio.addEventListener("timeupdate", onTime);
114
+ audio.addEventListener("loadedmetadata", onMeta);
115
+ audio.addEventListener("ended", onEnd);
116
+ return () => {
117
+ audio.removeEventListener("timeupdate", onTime);
118
+ audio.removeEventListener("loadedmetadata", onMeta);
119
+ audio.removeEventListener("ended", onEnd);
120
+ };
121
+ }, []);
122
+ const toggle = () => {
123
+ const audio = audioRef.current;
124
+ if (!audio) return;
125
+ if (playing) {
126
+ audio.pause();
127
+ setPlaying(false);
128
+ } else {
129
+ audio.play();
130
+ setPlaying(true);
131
+ }
132
+ };
133
+ const seek = (e) => {
134
+ const audio = audioRef.current;
135
+ if (!audio || !audio.duration) return;
136
+ const rect = e.currentTarget.getBoundingClientRect();
137
+ const ratio = (e.clientX - rect.left) / rect.width;
138
+ audio.currentTime = ratio * audio.duration;
139
+ };
140
+ const activeBars = Math.round(progress * BARS.length);
141
+ const playBtn = isMine ? "bg-green-600 hover:bg-green-700 text-white" : "bg-gray-600 hover:bg-gray-700 text-white";
142
+ const activeBar = isMine ? "bg-green-700" : "bg-gray-700";
143
+ const inactiveBar = isMine ? "bg-green-300" : "bg-gray-300";
144
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 w-full", style: { minWidth: "200px", maxWidth: "260px" }, children: [
145
+ /* @__PURE__ */ jsx3("audio", { ref: audioRef, src, preload: "metadata" }),
146
+ /* @__PURE__ */ jsx3(
147
+ "button",
148
+ {
149
+ onClick: toggle,
150
+ className: `flex-shrink-0 w-9 h-9 rounded-full flex items-center justify-center transition-colors ${playBtn}`,
151
+ children: playing ? /* @__PURE__ */ jsx3(Pause, { size: 16 }) : /* @__PURE__ */ jsx3(Play, { size: 16, className: "translate-x-0.5" })
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1 flex flex-col gap-1", children: [
155
+ /* @__PURE__ */ jsx3("div", { className: "flex items-end gap-0.5 h-8 cursor-pointer", onClick: seek, children: BARS.map((h, i) => /* @__PURE__ */ jsx3(
156
+ "div",
157
+ {
158
+ className: `flex-1 rounded-full transition-colors ${i < activeBars ? activeBar : inactiveBar}`,
159
+ style: { height: `${h * 2}px` }
160
+ },
161
+ i
162
+ )) }),
163
+ /* @__PURE__ */ jsx3("span", { className: "text-gray-400 tabular-nums text-xs", children: playing || current > 0 ? fmt(current) : fmt(duration) })
164
+ ] })
165
+ ] });
166
+ }
167
+
168
+ // src/FileIcon.tsx
169
+ import {
170
+ FileArchive,
171
+ FileAudio,
172
+ FileImage,
173
+ FileSpreadsheet,
174
+ FileText,
175
+ FileVideo,
176
+ File as FileGeneric,
177
+ Presentation
178
+ } from "lucide-react";
179
+
180
+ // src/lib/cn.ts
181
+ import { clsx } from "clsx";
182
+ import { twMerge } from "tailwind-merge";
183
+ function cn(...inputs) {
184
+ return twMerge(clsx(inputs));
185
+ }
186
+
187
+ // src/FileIcon.tsx
188
+ import { jsx as jsx4 } from "react/jsx-runtime";
189
+ var IMAGE_STYLE = { Icon: FileImage, colorClass: "text-violet-500" };
190
+ var VIDEO_STYLE = { Icon: FileVideo, colorClass: "text-fuchsia-500" };
191
+ var AUDIO_STYLE = { Icon: FileAudio, colorClass: "text-amber-500" };
192
+ var SHEET_STYLE = { Icon: FileSpreadsheet, colorClass: "text-green-600" };
193
+ var WORD_STYLE = { Icon: FileText, colorClass: "text-blue-500" };
194
+ var SLIDES_STYLE = { Icon: Presentation, colorClass: "text-orange-600" };
195
+ var TEXT_STYLE = { Icon: FileText, colorClass: "text-gray-500" };
196
+ var EXTENSION_STYLE = {
197
+ pdf: { Icon: FileText, colorClass: "text-red-500" },
198
+ doc: WORD_STYLE,
199
+ docx: WORD_STYLE,
200
+ xls: SHEET_STYLE,
201
+ xlsx: SHEET_STYLE,
202
+ csv: SHEET_STYLE,
203
+ ppt: SLIDES_STYLE,
204
+ pptx: SLIDES_STYLE,
205
+ zip: { Icon: FileArchive, colorClass: "text-orange-500" },
206
+ txt: TEXT_STYLE,
207
+ plain: TEXT_STYLE,
208
+ image: IMAGE_STYLE,
209
+ jpg: IMAGE_STYLE,
210
+ jpeg: IMAGE_STYLE,
211
+ png: IMAGE_STYLE,
212
+ webp: IMAGE_STYLE,
213
+ gif: IMAGE_STYLE,
214
+ heic: IMAGE_STYLE,
215
+ video: VIDEO_STYLE,
216
+ mp4: VIDEO_STYLE,
217
+ "3gp": VIDEO_STYLE,
218
+ "3gpp": VIDEO_STYLE,
219
+ mov: VIDEO_STYLE,
220
+ webm: VIDEO_STYLE,
221
+ audio: AUDIO_STYLE,
222
+ mp3: AUDIO_STYLE,
223
+ mpeg: AUDIO_STYLE,
224
+ ogg: AUDIO_STYLE,
225
+ oga: AUDIO_STYLE,
226
+ opus: AUDIO_STYLE,
227
+ aac: AUDIO_STYLE,
228
+ amr: AUDIO_STYLE,
229
+ m4a: AUDIO_STYLE,
230
+ wav: AUDIO_STYLE
231
+ };
232
+ var MEDIA_FAMILIES = /* @__PURE__ */ new Set(["image", "video", "audio"]);
233
+ function resolveFileIconExtension(filename, mimeType) {
234
+ const fromFilename = filename?.split(".").pop()?.toLowerCase();
235
+ if (fromFilename && EXTENSION_STYLE[fromFilename]) return fromFilename;
236
+ const [family, subtype] = (mimeType ?? "").split(";")[0].toLowerCase().split("/");
237
+ if (family && MEDIA_FAMILIES.has(family)) return family;
238
+ return subtype ?? "";
239
+ }
240
+ function FileIcon({ filename, mimeType, size = 20, className }) {
241
+ const extension = resolveFileIconExtension(filename, mimeType);
242
+ const style = EXTENSION_STYLE[extension] ?? { Icon: FileGeneric, colorClass: "text-gray-500" };
243
+ const { Icon, colorClass } = style;
244
+ return /* @__PURE__ */ jsx4(Icon, { size, className: cn(colorClass, className) });
245
+ }
246
+
247
+ // src/lib/format.ts
248
+ function formatTimestamp(timestamp) {
249
+ try {
250
+ const date = new Date(timestamp);
251
+ const hours = date.getHours().toString().padStart(2, "0");
252
+ const minutes = date.getMinutes().toString().padStart(2, "0");
253
+ return `${hours}:${minutes}`;
254
+ } catch {
255
+ return timestamp;
256
+ }
257
+ }
258
+ function formatDateTime(iso) {
259
+ const d = new Date(iso);
260
+ return d.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
261
+ }
262
+ function isSameDay(a, b) {
263
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
264
+ }
265
+ var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB"];
266
+ function formatFileSize(bytes) {
267
+ if (!isFinite(bytes) || bytes < 0) return "";
268
+ if (bytes < 1) return "0 B";
269
+ const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), FILE_SIZE_UNITS.length - 1);
270
+ const value = bytes / 1024 ** exponent;
271
+ const formatted = exponent === 0 ? value.toString() : value.toFixed(value < 10 ? 1 : 0);
272
+ return `${formatted} ${FILE_SIZE_UNITS[exponent]}`;
273
+ }
274
+
275
+ // src/MediaRenderer.tsx
276
+ import { useState as useState2 } from "react";
277
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
278
+ function resolveMediaSource(message) {
279
+ if (message.mediaUrl) return message.mediaUrl;
280
+ if (message.base64) {
281
+ const prefix = message.mimeType ? `data:${message.mimeType};base64,` : "data:application/octet-stream;base64,";
282
+ return prefix + message.base64;
283
+ }
284
+ return null;
285
+ }
286
+ function hasLazyRef(message) {
287
+ return Boolean(message.uploadId || message.mediaId);
288
+ }
289
+ function useLazyMediaUrl(message, onResolveUrl) {
290
+ const [url, setUrl] = useState2(null);
291
+ const [loading, setLoading] = useState2(false);
292
+ const [error, setError] = useState2(false);
293
+ const load = async () => {
294
+ if (url || loading || !onResolveUrl) return;
295
+ setLoading(true);
296
+ setError(false);
297
+ try {
298
+ const resolved = await onResolveUrl(message);
299
+ if (resolved) setUrl(resolved);
300
+ else setError(true);
301
+ } catch {
302
+ setError(true);
303
+ } finally {
304
+ setLoading(false);
305
+ }
306
+ };
307
+ return { url, loading, error, load };
308
+ }
309
+ function MediaRenderer({ message, onLightbox, onResolveUrl, className }) {
310
+ const { bubble } = useConversationLocales();
311
+ const eagerSrc = resolveMediaSource(message);
312
+ const lazy = useLazyMediaUrl(message, onResolveUrl);
313
+ const src = eagerSrc ?? lazy.url;
314
+ const canLazyLoad = !eagerSrc && hasLazyRef(message) && Boolean(onResolveUrl);
315
+ const lazyButtonClass = "text-xs text-blue-600 underline flex items-center gap-1";
316
+ switch (message.type) {
317
+ case "image":
318
+ case "sticker": {
319
+ if (!src && canLazyLoad) {
320
+ return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewImage });
321
+ }
322
+ return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5("img", { src, alt: message.caption ?? bubble.imageAlt, className: "w-full max-h-80 object-cover cursor-pointer hover:opacity-90 transition-opacity", onClick: () => onLightbox(src), loading: "lazy" }) : /* @__PURE__ */ jsx5("div", { className: "w-full h-40 bg-gray-200 flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs3("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
323
+ /* @__PURE__ */ jsx5("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }),
324
+ /* @__PURE__ */ jsx5("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
325
+ /* @__PURE__ */ jsx5("polyline", { points: "21 15 16 10 5 21" })
326
+ ] }) }) });
327
+ }
328
+ case "video": {
329
+ if (!src && canLazyLoad) {
330
+ return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.viewVideo });
331
+ }
332
+ return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5("video", { src, className: "w-full max-h-80 rounded-lg", controls: true, preload: "metadata", children: /* @__PURE__ */ jsx5("track", { kind: "captions" }) }) : /* @__PURE__ */ jsx5("div", { className: "w-full h-32 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400", children: /* @__PURE__ */ jsxs3("svg", { width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
333
+ /* @__PURE__ */ jsx5("polygon", { points: "23 7 16 12 23 17 23 7" }),
334
+ /* @__PURE__ */ jsx5("rect", { x: "1", y: "5", width: "15", height: "14", rx: "2", ry: "2" })
335
+ ] }) }) });
336
+ }
337
+ case "audio": {
338
+ if (!src && canLazyLoad) {
339
+ return /* @__PURE__ */ jsx5("button", { onClick: lazy.load, className: lazyButtonClass, children: lazy.loading ? bubble.mediaLoading : lazy.error ? bubble.mediaRetry : bubble.listenAudio });
340
+ }
341
+ return /* @__PURE__ */ jsx5("div", { className: "min-w-[200px]", children: src ? /* @__PURE__ */ jsx5(AudioPlayer, { src, isMine: message.direction === "outbound" }) : /* @__PURE__ */ jsx5("div", { className: "h-12 bg-gray-200 rounded-lg flex items-center justify-center text-gray-400 text-xs", children: bubble.mediaUnavailable }) });
342
+ }
343
+ case "document": {
344
+ const typeLabel = message.mimeType?.split("/")[1]?.toUpperCase() ?? "FILE";
345
+ const sizeLabel = message.sizeBytes ? formatFileSize(message.sizeBytes) : null;
346
+ return /* @__PURE__ */ jsxs3("div", { className: cn("flex items-center gap-3 min-w-[200px]", className), children: [
347
+ /* @__PURE__ */ jsx5("div", { className: "w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx5(FileIcon, { filename: message.filename, mimeType: message.mimeType }) }),
348
+ /* @__PURE__ */ jsxs3("div", { className: "flex-1 min-w-0", children: [
349
+ /* @__PURE__ */ jsx5("p", { className: "text-sm font-medium truncate", children: message.filename ?? bubble.untitledDocument }),
350
+ /* @__PURE__ */ jsx5("p", { className: "text-xs text-gray-500", children: sizeLabel ? `${typeLabel} \xB7 ${sizeLabel}` : typeLabel })
351
+ ] }),
352
+ src ? /* @__PURE__ */ jsx5("a", { href: src, download: message.filename, className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors", "aria-label": bubble.downloadFile, children: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
353
+ /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
354
+ /* @__PURE__ */ jsx5("polyline", { points: "7 10 12 15 17 10" }),
355
+ /* @__PURE__ */ jsx5("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
356
+ ] }) }) : canLazyLoad ? /* @__PURE__ */ jsx5(
357
+ "button",
358
+ {
359
+ onClick: lazy.load,
360
+ disabled: lazy.loading,
361
+ className: "w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 flex-shrink-0 transition-colors disabled:opacity-50",
362
+ "aria-label": bubble.downloadFile,
363
+ children: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: "text-gray-600", children: [
364
+ /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
365
+ /* @__PURE__ */ jsx5("polyline", { points: "7 10 12 15 17 10" }),
366
+ /* @__PURE__ */ jsx5("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
367
+ ] })
368
+ }
369
+ ) : null,
370
+ lazy.error && /* @__PURE__ */ jsx5("span", { className: "text-xs text-red-500 flex-shrink-0", children: bubble.mediaError })
371
+ ] });
372
+ }
373
+ default:
374
+ return null;
375
+ }
376
+ }
377
+
378
+ // src/Lightbox.tsx
379
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
380
+ var DEFAULT_LIGHTBOX_LABELS = {
381
+ imageAlt: "Imagem",
382
+ close: "Fechar"
383
+ };
384
+ function Lightbox({ imageUrl, caption, onClose, labels }) {
385
+ const imageAltLabel = labels?.imageAlt ?? DEFAULT_LIGHTBOX_LABELS.imageAlt;
386
+ const closeLabel = labels?.close ?? DEFAULT_LIGHTBOX_LABELS.close;
387
+ return /* @__PURE__ */ jsx6("div", { className: "fixed inset-0 z-50 bg-black/85 flex items-center justify-center p-4", onClick: onClose, children: /* @__PURE__ */ jsxs4("div", { className: "max-w-[90vw] max-h-[90vh] flex flex-col items-center", onClick: (e) => e.stopPropagation(), children: [
388
+ /* @__PURE__ */ jsx6("img", { src: imageUrl, alt: caption ?? imageAltLabel, className: "max-w-full max-h-[80vh] object-contain rounded-lg" }),
389
+ caption && /* @__PURE__ */ jsx6("p", { className: "text-white text-sm mt-3 text-center", children: caption }),
390
+ /* @__PURE__ */ jsx6("button", { onClick: onClose, className: "mt-4 px-4 py-2 bg-white/20 text-white rounded-lg hover:bg-white/30 transition-colors", children: closeLabel })
391
+ ] }) });
392
+ }
393
+
394
+ // src/lib/createMediaUrlResolver.ts
395
+ function createMediaUrlResolver(api) {
396
+ return async (message) => {
397
+ if (message.uploadId) return api.getDocumentUrl(message.uploadId, "inline");
398
+ if (message.mediaId) {
399
+ const { mimeType, data } = await api.getMediaProxyUrl(message.mediaId);
400
+ return `data:${mimeType};base64,${data}`;
401
+ }
402
+ return null;
403
+ };
404
+ }
405
+
406
+ // src/providers/ConversationsProvider.tsx
407
+ import { createContext as createContext2, useContext as useContext2 } from "react";
408
+ import { jsx as jsx7 } from "react/jsx-runtime";
409
+ var ConversationsContext = createContext2(null);
410
+ function ConversationsProvider({
411
+ api,
412
+ sse,
413
+ children
414
+ }) {
415
+ return /* @__PURE__ */ jsx7(ConversationsContext.Provider, { value: { api, sse }, children });
416
+ }
417
+ function useConversations() {
418
+ return useContext2(ConversationsContext);
419
+ }
420
+
421
+ // src/MessageBubble.tsx
422
+ import { useMemo, useState as useState3 } from "react";
423
+ import { Check } from "lucide-react";
424
+ import { Fragment, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
425
+ var BUBBLE_COLOR = {
426
+ agent: "bg-[#d9fdd3] dark:bg-[#005c4b]",
427
+ bot: "bg-[#d7f0ec] dark:bg-[#0a3d3a]",
428
+ customer: "bg-white dark:bg-[#202c33]"
429
+ };
430
+ var MEDIA_TYPES = /* @__PURE__ */ new Set(["image", "audio", "video", "document", "sticker"]);
431
+ function MessageBubble({
432
+ message,
433
+ isMine,
434
+ senderName,
435
+ isFirstInGroup = true,
436
+ isSelecting = false,
437
+ isSelected = false,
438
+ onToggleSelect,
439
+ onResolveMediaUrl,
440
+ className
441
+ }) {
442
+ const { bubble, selection } = useConversationLocales();
443
+ const [lightboxSrc, setLightboxSrc] = useState3(null);
444
+ const context = useConversations();
445
+ const resolveMediaUrl = useMemo(
446
+ () => onResolveMediaUrl ?? (context?.api ? createMediaUrlResolver(context.api) : void 0),
447
+ [onResolveMediaUrl, context?.api]
448
+ );
449
+ const bubbleColor = BUBBLE_COLOR[message.sender] ?? BUBBLE_COLOR.customer;
450
+ const hasError = message.status === "failed";
451
+ const isMedia = MEDIA_TYPES.has(message.type);
452
+ const isTemplate = message.type === "template";
453
+ const displayName = message.sender === "agent" && senderName ? senderName : bubble[message.sender] ?? message.sender;
454
+ const tooltipText = message.status === "read" && message.readAt ? `${bubble.readAt}${formatDateTime(message.readAt)}` : message.status === "failed" ? bubble.windowExpired : void 0;
455
+ const tailCornerClass = isFirstInGroup ? isMine ? "rounded-tr-md" : "rounded-tl-md" : "";
456
+ const checkbox = /* @__PURE__ */ jsx8(
457
+ "button",
458
+ {
459
+ onClick: (e) => {
460
+ e.stopPropagation();
461
+ onToggleSelect?.();
462
+ },
463
+ title: selection.select,
464
+ className: `
465
+ flex-shrink-0 self-end mb-1 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all
466
+ ${isSelected ? "bg-teal-600 border-teal-600" : "bg-white/80 dark:bg-black/40 border-black/20 dark:border-white/30"}
467
+ ${isSelecting ? "opacity-100" : "opacity-0 group-hover:opacity-100"}
468
+ `,
469
+ children: isSelected && /* @__PURE__ */ jsx8(Check, { size: 12, className: "text-white", strokeWidth: 3 })
470
+ }
471
+ );
472
+ return /* @__PURE__ */ jsxs5(
473
+ "div",
474
+ {
475
+ className: cn(
476
+ "flex items-end gap-1.5 group",
477
+ isMine ? "justify-end" : "justify-start",
478
+ isFirstInGroup ? "mt-2" : "mt-0.5",
479
+ className
480
+ ),
481
+ children: [
482
+ isMine && checkbox,
483
+ /* @__PURE__ */ jsxs5(
484
+ "div",
485
+ {
486
+ onClick: isSelecting ? onToggleSelect : void 0,
487
+ className: `
488
+ max-w-[75%] sm:max-w-[65%] rounded-2xl ${tailCornerClass} px-2.5 py-1.5 shadow-sm
489
+ ${bubbleColor}
490
+ ${hasError ? "ring-1 ring-inset ring-red-400" : ""}
491
+ ${isSelecting ? "cursor-pointer" : ""}
492
+ ${isSelected ? "ring-2 ring-teal-500" : ""}
493
+ relative
494
+ `,
495
+ children: [
496
+ isMine && isFirstInGroup && (message.sender === "bot" || message.sender === "agent" && senderName) && /* @__PURE__ */ jsx8("div", { className: "text-xs font-semibold mb-0.5 text-teal-700 dark:text-teal-400", children: displayName }),
497
+ message.moderation?.isOffensive && /* @__PURE__ */ jsxs5(
498
+ "div",
499
+ {
500
+ className: "mb-1 inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-950/60 dark:text-amber-300",
501
+ title: message.moderation.terms.length > 0 ? message.moderation.terms.join(", ") : void 0,
502
+ children: [
503
+ /* @__PURE__ */ jsx8("span", { "aria-hidden": true, children: "\u26A0\uFE0F" }),
504
+ /* @__PURE__ */ jsx8("span", { children: bubble.moderationFlagged })
505
+ ]
506
+ }
507
+ ),
508
+ isMedia ? /* @__PURE__ */ jsx8(MediaRenderer, { message, onLightbox: setLightboxSrc, onResolveUrl: resolveMediaUrl }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
509
+ isTemplate && /* @__PURE__ */ jsxs5("p", { className: "text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mb-0.5", children: [
510
+ /* @__PURE__ */ jsx8("span", { children: "\u{1F4E8}" }),
511
+ /* @__PURE__ */ jsxs5("span", { className: "font-medium", children: [
512
+ bubble.templateLabel,
513
+ message.templateName ? ` \u2014 ${message.templateName}` : ""
514
+ ] })
515
+ ] }),
516
+ /* @__PURE__ */ jsx8("div", { className: "text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap break-words leading-[19px]", children: parseWhatsAppFormatting(message.content ?? "") })
517
+ ] }),
518
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-end gap-1 mt-0.5 select-none", children: [
519
+ /* @__PURE__ */ jsx8("span", { className: "text-xs text-black/40 dark:text-white/40 font-medium", children: formatTimestamp(message.timestamp) }),
520
+ isMine && message.status && /* @__PURE__ */ jsx8(StatusTicks, { status: message.status, title: tooltipText })
521
+ ] })
522
+ ]
523
+ }
524
+ ),
525
+ !isMine && checkbox,
526
+ lightboxSrc && /* @__PURE__ */ jsx8(Lightbox, { imageUrl: lightboxSrc, onClose: () => setLightboxSrc(null) })
527
+ ]
528
+ }
529
+ );
530
+ }
531
+
532
+ // src/Wallpaper.tsx
533
+ import { jsx as jsx9 } from "react/jsx-runtime";
534
+ function ConversationWallpaper({ children, className }) {
535
+ return /* @__PURE__ */ jsx9("div", { className: cn("cv-wallpaper", className), children });
536
+ }
537
+
538
+ // src/EmojiPicker.tsx
539
+ import { useState as useState4, useCallback } from "react";
540
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
541
+ var EMOJI_CATEGORIES = [
542
+ {
543
+ name: "Smileys",
544
+ emojis: ["\u{1F600}", "\u{1F603}", "\u{1F604}", "\u{1F601}", "\u{1F605}", "\u{1F602}", "\u{1F923}", "\u{1F60A}", "\u{1F607}", "\u{1F642}", "\u{1F609}", "\u{1F60C}", "\u{1F60D}", "\u{1F970}", "\u{1F618}", "\u{1F617}", "\u{1F60B}", "\u{1F61B}", "\u{1F61C}", "\u{1F92A}"]
545
+ },
546
+ {
547
+ name: "Gestures",
548
+ emojis: ["\u{1F44D}", "\u{1F44E}", "\u{1F44C}", "\u270C\uFE0F", "\u{1F91E}", "\u{1F91F}", "\u{1F918}", "\u{1F919}", "\u{1F44B}", "\u{1F91A}", "\u{1F590}\uFE0F", "\u270B", "\u{1F596}", "\u{1F44F}", "\u{1F64C}", "\u{1F91D}", "\u{1F64F}", "\u270D\uFE0F", "\u{1F485}", "\u{1F933}"]
549
+ },
550
+ {
551
+ name: "Hearts",
552
+ emojis: ["\u2764\uFE0F", "\u{1F9E1}", "\u{1F49B}", "\u{1F49A}", "\u{1F499}", "\u{1F49C}", "\u{1F5A4}", "\u{1F90D}", "\u{1F90E}", "\u{1F494}", "\u2763\uFE0F", "\u{1F495}", "\u{1F49E}", "\u{1F493}", "\u{1F497}", "\u{1F496}", "\u{1F498}", "\u{1F49D}", "\u{1F49F}", "\u2665\uFE0F"]
553
+ },
554
+ {
555
+ name: "Food",
556
+ emojis: ["\u{1F354}", "\u{1F35F}", "\u{1F355}", "\u{1F32D}", "\u{1F37F}", "\u{1F9C2}", "\u{1F953}", "\u{1F95A}", "\u{1F373}", "\u{1F9C7}", "\u{1F95E}", "\u{1F9C8}", "\u{1F35E}", "\u{1F950}", "\u{1F968}", "\u{1F96F}", "\u{1F956}", "\u{1F9C0}", "\u{1F957}", "\u{1F959}"]
557
+ },
558
+ {
559
+ name: "Drinks",
560
+ emojis: ["\u2615", "\u{1F375}", "\u{1F376}", "\u{1F37E}", "\u{1F377}", "\u{1F378}", "\u{1F379}", "\u{1F37A}", "\u{1F37B}", "\u{1F942}", "\u{1F943}", "\u{1F964}", "\u{1F9CB}", "\u{1F9C3}", "\u{1F9C9}", "\u{1F9CA}", "\u{1F962}", "\u{1F37D}\uFE0F", "\u{1F374}", "\u{1F944}"]
561
+ },
562
+ {
563
+ name: "Objects",
564
+ emojis: ["\u{1F381}", "\u{1F382}", "\u{1F388}", "\u{1F389}", "\u{1F38A}", "\u{1F380}", "\u{1F4F1}", "\u{1F4BB}", "\u231A", "\u{1F4F7}", "\u{1F511}", "\u{1F4B0}", "\u{1F4B3}", "\u{1F4DD}", "\u{1F4CC}", "\u{1F4CD}", "\u2702\uFE0F", "\u{1F50D}", "\u{1F4A1}", "\u{1F514}"]
565
+ }
566
+ ];
567
+ var EmojiPicker = ({ onSelect, className = "" }) => {
568
+ const [activeCategory, setActiveCategory] = useState4(0);
569
+ const handleSelect = useCallback(
570
+ (emoji) => {
571
+ onSelect(emoji);
572
+ },
573
+ [onSelect]
574
+ );
575
+ return /* @__PURE__ */ jsxs6("div", { className: `bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden ${className}`, children: [
576
+ /* @__PURE__ */ jsx10("div", { className: "flex border-b border-gray-200 overflow-x-auto", children: EMOJI_CATEGORIES.map((category, index) => /* @__PURE__ */ jsxs6(
577
+ "button",
578
+ {
579
+ onClick: () => setActiveCategory(index),
580
+ className: `px-3 py-2 text-xs font-medium whitespace-nowrap border-b-2 transition-colors ${activeCategory === index ? "border-blue-500 text-blue-600" : "border-transparent text-gray-500 hover:text-gray-700"}`,
581
+ children: [
582
+ category.emojis[0],
583
+ " ",
584
+ category.name
585
+ ]
586
+ },
587
+ category.name
588
+ )) }),
589
+ /* @__PURE__ */ jsx10("div", { className: "grid grid-cols-10 gap-0.5 p-2 max-h-[240px] overflow-y-auto", children: EMOJI_CATEGORIES[activeCategory].emojis.map((emoji) => /* @__PURE__ */ jsx10(
590
+ "button",
591
+ {
592
+ onClick: () => handleSelect(emoji),
593
+ className: "w-8 h-8 flex items-center justify-center text-lg hover:bg-gray-100 rounded transition-colors cursor-pointer",
594
+ "aria-label": emoji,
595
+ children: emoji
596
+ },
597
+ emoji
598
+ )) })
599
+ ] });
600
+ };
601
+
602
+ // src/MessageComposer.tsx
603
+ import { useState as useState5, useRef as useRef2, useCallback as useCallback2 } from "react";
604
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
605
+ var DEFAULT_MESSAGE_COMPOSER_LABELS = {
606
+ emoji: "Emoji",
607
+ attach: "Anexar",
608
+ send: "Enviar"
609
+ };
610
+ var DEFAULT_ACCEPTED_FILE_TYPES = "image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.zip";
611
+ var MessageComposer = ({
612
+ onSend,
613
+ onAttach,
614
+ value: externalValue,
615
+ onChange: externalOnChange,
616
+ features,
617
+ placeholder = "Digite uma mensagem...",
618
+ maxLength,
619
+ disabled = false,
620
+ acceptedFileTypes = DEFAULT_ACCEPTED_FILE_TYPES,
621
+ className,
622
+ classNames,
623
+ labels
624
+ }) => {
625
+ const emojiLabel = labels?.emoji ?? DEFAULT_MESSAGE_COMPOSER_LABELS.emoji;
626
+ const attachLabel = labels?.attach ?? DEFAULT_MESSAGE_COMPOSER_LABELS.attach;
627
+ const sendLabel = labels?.send ?? DEFAULT_MESSAGE_COMPOSER_LABELS.send;
628
+ const [internalText, setInternalText] = useState5("");
629
+ const [showEmoji, setShowEmoji] = useState5(false);
630
+ const [attachments, setAttachments] = useState5([]);
631
+ const textareaRef = useRef2(null);
632
+ const fileInputRef = useRef2(null);
633
+ const isControlled = externalValue !== void 0;
634
+ const text = isControlled ? externalValue : internalText;
635
+ const setText = useCallback2((newText) => {
636
+ if (isControlled) {
637
+ externalOnChange?.(newText);
638
+ } else {
639
+ setInternalText(newText);
640
+ }
641
+ }, [isControlled, externalOnChange]);
642
+ const showEmojiButton = features?.emoji !== false;
643
+ const showAttachButton = features?.documents !== false;
644
+ const sendMessage = useCallback2(() => {
645
+ const trimmed = text.trim();
646
+ if (!trimmed && attachments.length === 0) return;
647
+ if (trimmed) onSend(trimmed);
648
+ for (const a of attachments) {
649
+ onAttach?.(a.file);
650
+ URL.revokeObjectURL(a.previewUrl);
651
+ }
652
+ if (!isControlled) setInternalText("");
653
+ setAttachments([]);
654
+ setShowEmoji(false);
655
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
656
+ }, [text, attachments, onSend, onAttach, isControlled]);
657
+ const handleKeyDown = useCallback2((e) => {
658
+ if (e.key === "Enter" && !e.shiftKey) {
659
+ e.preventDefault();
660
+ if (!disabled) sendMessage();
661
+ }
662
+ }, [sendMessage, disabled]);
663
+ const handleInput = useCallback2(() => {
664
+ const ta = textareaRef.current;
665
+ if (!ta) return;
666
+ ta.style.height = "auto";
667
+ ta.style.height = `${Math.min(ta.scrollHeight, 100)}px`;
668
+ }, []);
669
+ const handleEmojiSelect = useCallback2((emoji) => {
670
+ const ta = textareaRef.current;
671
+ if (!ta) {
672
+ setText(text + emoji);
673
+ return;
674
+ }
675
+ const start = ta.selectionStart;
676
+ const end = ta.selectionEnd;
677
+ const newText = text.slice(0, start) + emoji + text.slice(end);
678
+ setText(newText);
679
+ requestAnimationFrame(() => {
680
+ ta.focus();
681
+ ta.setSelectionRange(start + emoji.length, start + emoji.length);
682
+ handleInput();
683
+ });
684
+ }, [text, setText, handleInput]);
685
+ const handleFileChange = useCallback2((e) => {
686
+ const files = e.target.files;
687
+ if (!files) return;
688
+ const previews = [];
689
+ for (let i = 0; i < files.length; i++) {
690
+ const file = files[i];
691
+ previews.push({ file, previewUrl: file.type.startsWith("image/") ? URL.createObjectURL(file) : "" });
692
+ }
693
+ setAttachments((prev) => [...prev, ...previews]);
694
+ if (fileInputRef.current) fileInputRef.current.value = "";
695
+ }, []);
696
+ const removeAttachment = useCallback2((index) => {
697
+ setAttachments((prev) => {
698
+ const next = [...prev];
699
+ if (next[index].previewUrl) URL.revokeObjectURL(next[index].previewUrl);
700
+ next.splice(index, 1);
701
+ return next;
702
+ });
703
+ }, []);
704
+ const insertFormatting = useCallback2((marker) => {
705
+ const ta = textareaRef.current;
706
+ if (!ta) return;
707
+ const start = ta.selectionStart;
708
+ const end = ta.selectionEnd;
709
+ const sel = text.slice(start, end);
710
+ if (sel) {
711
+ setText(text.slice(0, start) + marker + sel + marker + text.slice(end));
712
+ requestAnimationFrame(() => {
713
+ ta.focus();
714
+ ta.setSelectionRange(start + marker.length + sel.length + marker.length, start + marker.length + sel.length + marker.length);
715
+ });
716
+ }
717
+ }, [text, setText]);
718
+ const canSend = text.trim().length > 0 || attachments.length > 0;
719
+ const remaining = maxLength ? maxLength - text.length : null;
720
+ return (
721
+ /* A barra é a superfície (cinza, largura cheia, sem raio) e o campo dentro é que arredonda —
722
+ ordem do WhatsApp. Invertido, o pill arredondado ia até a borda da tela e os cantos
723
+ descobriam o fundo branco da página, que lia como defeito. */
724
+ /* @__PURE__ */ jsxs7("div", { className: cn("bg-[#f0f2f5] px-2 py-2", className), children: [
725
+ attachments.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex gap-2 px-1 pb-2 overflow-x-auto", children: attachments.map((a, i) => /* @__PURE__ */ jsxs7("div", { className: "relative flex-shrink-0", children: [
726
+ a.previewUrl ? /* @__PURE__ */ jsx11("img", { src: a.previewUrl, alt: "", className: "w-16 h-16 object-cover rounded-lg border border-gray-200" }) : /* @__PURE__ */ jsx11("div", { className: "w-16 h-16 bg-gray-100 rounded-lg border border-gray-200 flex items-center justify-center", children: /* @__PURE__ */ jsxs7("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#9ca3af", strokeWidth: "1.5", children: [
727
+ /* @__PURE__ */ jsx11("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
728
+ /* @__PURE__ */ jsx11("polyline", { points: "14 2 14 8 20 8" })
729
+ ] }) }),
730
+ /* @__PURE__ */ jsx11("button", { onClick: () => removeAttachment(i), className: "absolute -top-2 -right-2 w-5 h-5 bg-gray-600 text-white rounded-full flex items-center justify-center hover:bg-gray-800 text-xs", children: "\u2715" })
731
+ ] }, i)) }),
732
+ /* @__PURE__ */ jsxs7("div", { className: cn("flex items-end gap-1.5 rounded-xl bg-white px-3 py-2", classNames?.field), children: [
733
+ showEmojiButton && /* @__PURE__ */ jsxs7("div", { className: "relative flex-shrink-0", children: [
734
+ /* @__PURE__ */ jsx11("button", { onClick: () => setShowEmoji((v) => !v), className: "w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 transition-colors", "aria-label": emojiLabel, children: /* @__PURE__ */ jsxs7("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
735
+ /* @__PURE__ */ jsx11("circle", { cx: "12", cy: "12", r: "10" }),
736
+ /* @__PURE__ */ jsx11("path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }),
737
+ /* @__PURE__ */ jsx11("circle", { cx: "9", cy: "9", r: "0.5", fill: "currentColor" }),
738
+ /* @__PURE__ */ jsx11("circle", { cx: "15", cy: "9", r: "0.5", fill: "currentColor" })
739
+ ] }) }),
740
+ showEmoji && /* @__PURE__ */ jsx11("div", { className: "absolute bottom-full left-0 mb-2 z-10", children: /* @__PURE__ */ jsx11(EmojiPicker, { onSelect: handleEmojiSelect }) })
741
+ ] }),
742
+ /* @__PURE__ */ jsx11(
743
+ "textarea",
744
+ {
745
+ ref: textareaRef,
746
+ value: text,
747
+ onChange: (e) => {
748
+ setText(e.target.value);
749
+ handleInput();
750
+ },
751
+ onKeyDown: handleKeyDown,
752
+ placeholder,
753
+ rows: 1,
754
+ disabled,
755
+ className: "flex-1 resize-none bg-transparent text-[15px] text-[#3b4a54] placeholder-[#8696a0] outline-none py-1.5 max-h-[100px] leading-relaxed",
756
+ style: { fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }
757
+ }
758
+ ),
759
+ showAttachButton && /* @__PURE__ */ jsxs7(Fragment2, { children: [
760
+ /* @__PURE__ */ jsx11("input", { ref: fileInputRef, type: "file", multiple: true, accept: acceptedFileTypes, onChange: handleFileChange, className: "hidden" }),
761
+ /* @__PURE__ */ jsx11("button", { onClick: () => fileInputRef.current?.click(), className: "w-9 h-9 flex items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 flex-shrink-0 transition-colors", "aria-label": attachLabel, children: /* @__PURE__ */ jsx11("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx11("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" }) }) })
762
+ ] }),
763
+ /* @__PURE__ */ jsx11(
764
+ "button",
765
+ {
766
+ onClick: sendMessage,
767
+ disabled: !canSend || disabled,
768
+ className: `w-10 h-10 flex items-center justify-center rounded-full flex-shrink-0 transition-all ${canSend && !disabled ? "bg-[#00a884] text-white hover:bg-[#06cf9c] shadow-sm" : "bg-gray-200 text-gray-400 cursor-not-allowed"}`,
769
+ "aria-label": sendLabel,
770
+ children: /* @__PURE__ */ jsx11("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx11("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
771
+ }
772
+ )
773
+ ] }),
774
+ remaining !== null && /* @__PURE__ */ jsx11("div", { className: "flex justify-end mt-1 pr-1", children: /* @__PURE__ */ jsx11("span", { className: `text-xs ${remaining < 20 ? "text-red-500" : "text-gray-400"}`, children: remaining }) })
775
+ ] })
776
+ );
777
+ };
778
+
779
+ // src/DateDivider.tsx
780
+ import { jsx as jsx12 } from "react/jsx-runtime";
781
+ function DateDivider({ iso, className, classNames }) {
782
+ const { dateDivider } = useConversationLocales();
783
+ return /* @__PURE__ */ jsx12("div", { className: cn("flex justify-center sticky top-0 z-10 my-2 pointer-events-none", classNames?.root, className), children: /* @__PURE__ */ jsx12(
784
+ "span",
785
+ {
786
+ className: cn(
787
+ "bg-white/95 dark:bg-gray-800/95 text-gray-600 dark:text-gray-300 text-xs font-medium px-3 py-1 rounded-lg shadow-sm",
788
+ classNames?.label
789
+ ),
790
+ children: formatDividerLabel(iso, dateDivider)
791
+ }
792
+ ) });
793
+ }
794
+ function formatDividerLabel(iso, dateDivider) {
795
+ const date = new Date(iso);
796
+ const today = /* @__PURE__ */ new Date();
797
+ const yesterday = new Date(today);
798
+ yesterday.setDate(today.getDate() - 1);
799
+ if (isSameDay(date, today)) return dateDivider.today;
800
+ if (isSameDay(date, yesterday)) return dateDivider.yesterday;
801
+ return date.toLocaleDateString("pt-BR", { day: "2-digit", month: "long", year: "numeric" });
802
+ }
803
+
804
+ // src/lib/phone.ts
805
+ function formatPhone(number) {
806
+ const digits = number.replace(/\D/g, "");
807
+ if (digits.length === 13) {
808
+ return `+${digits.slice(0, 2)} (${digits.slice(2, 4)}) ${digits.slice(4, 9)}-${digits.slice(9, 13)}`;
809
+ }
810
+ if (digits.length === 12) {
811
+ return `+${digits.slice(0, 2)} (${digits.slice(2, 4)}) ${digits.slice(4, 8)}-${digits.slice(8, 12)}`;
812
+ }
813
+ if (digits.length === 11) {
814
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7, 11)}`;
815
+ }
816
+ if (digits.length === 10) {
817
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6, 10)}`;
818
+ }
819
+ return number;
820
+ }
821
+ var COUNTRY_FLAG_BY_DIAL_CODE = {
822
+ "55": "\u{1F1E7}\u{1F1F7}",
823
+ "351": "\u{1F1F5}\u{1F1F9}",
824
+ "34": "\u{1F1EA}\u{1F1F8}",
825
+ "54": "\u{1F1E6}\u{1F1F7}",
826
+ "56": "\u{1F1E8}\u{1F1F1}",
827
+ "57": "\u{1F1E8}\u{1F1F4}",
828
+ "52": "\u{1F1F2}\u{1F1FD}",
829
+ "598": "\u{1F1FA}\u{1F1FE}",
830
+ "595": "\u{1F1F5}\u{1F1FE}",
831
+ "44": "\u{1F1EC}\u{1F1E7}",
832
+ "49": "\u{1F1E9}\u{1F1EA}",
833
+ "39": "\u{1F1EE}\u{1F1F9}",
834
+ "33": "\u{1F1EB}\u{1F1F7}"
835
+ };
836
+ function phoneCountryFlag(number) {
837
+ const digits = number.replace(/\D/g, "");
838
+ for (const length of [3, 2, 1]) {
839
+ const flag = COUNTRY_FLAG_BY_DIAL_CODE[digits.slice(0, length)];
840
+ if (flag) return flag;
841
+ }
842
+ return "";
843
+ }
844
+ function phoneInitials(number) {
845
+ const digits = number.replace(/\D/g, "");
846
+ return digits.slice(-2);
847
+ }
848
+
849
+ // src/hooks/useAsyncResource.ts
850
+ import { useCallback as useCallback3, useEffect as useEffect2, useRef as useRef3, useState as useState6 } from "react";
851
+ function useAsyncResource(fetcher, deps) {
852
+ const [data, setData] = useState6(void 0);
853
+ const [loading, setLoading] = useState6(false);
854
+ const [error, setError] = useState6(void 0);
855
+ const requestIdRef = useRef3(0);
856
+ const load = useCallback3(async () => {
857
+ const requestId = ++requestIdRef.current;
858
+ setLoading(true);
859
+ setError(void 0);
860
+ try {
861
+ const result = await fetcher();
862
+ if (requestId === requestIdRef.current) setData(result);
863
+ } catch (err) {
864
+ if (requestId === requestIdRef.current) setError(err instanceof Error ? err : new Error(String(err)));
865
+ } finally {
866
+ if (requestId === requestIdRef.current) setLoading(false);
867
+ }
868
+ }, deps);
869
+ useEffect2(() => {
870
+ load();
871
+ }, [load]);
872
+ return { data, loading, error, refetch: load };
873
+ }
874
+
875
+ // src/lib/paginated.ts
876
+ function conversationsOf(result) {
877
+ return Array.isArray(result) ? result : result.conversations;
878
+ }
879
+ function documentsOf(result) {
880
+ return Array.isArray(result) ? result : result.documents;
881
+ }
882
+ function totalOf(result) {
883
+ return Array.isArray(result) ? result.length : result.total;
884
+ }
885
+
886
+ // src/hooks/useConversationDocuments.ts
887
+ function useConversationDocuments(conversationId, params) {
888
+ const context = useConversations();
889
+ if (!context) {
890
+ throw new Error("useConversationDocuments requires an ancestor <ConversationsProvider>");
891
+ }
892
+ const { api } = context;
893
+ const { data, loading, error, refetch } = useAsyncResource(
894
+ () => conversationId ? api.getDocuments(conversationId, params) : Promise.resolve([]),
895
+ [conversationId, params?.search, params?.page, params?.limit, params?.source, params?.sortDirection]
896
+ );
897
+ if (data === void 0) {
898
+ return { documents: [], total: 0, loading, error, refetch };
899
+ }
900
+ return { documents: documentsOf(data), total: totalOf(data), loading, error, refetch };
901
+ }
902
+
903
+ // src/ConversationDocumentsPanel.tsx
904
+ import { useState as useState7 } from "react";
905
+ import { ArrowUpDown, Bot, Download, Eye, Users } from "lucide-react";
906
+ import { jsx as jsx13, jsxs as jsxs8 } from "react/jsx-runtime";
907
+ var DOCUMENT_SOURCE_FILTER = {
908
+ ALL: "all",
909
+ CUSTOMER: "customer",
910
+ TEAM: "team"
911
+ };
912
+ var TEAM_SOURCES = /* @__PURE__ */ new Set(["agent", "bot"]);
913
+ var DEFAULT_CONVERSATION_DOCUMENTS_LABELS = {
914
+ toggle: "\u{1F4CE} Arquivos",
915
+ title: "Arquivos da conversa",
916
+ noResults: "Nenhum arquivo encontrado para os filtros aplicados",
917
+ view: "Visualizar",
918
+ sourceFilterAll: "Todas as origens",
919
+ sourceFilterCustomer: "Cliente",
920
+ sourceFilterTeam: "Equipe",
921
+ sortMostRecent: "Mais recentes",
922
+ sortOldest: "Mais antigos",
923
+ clearFilters: "Limpar filtros",
924
+ selectAll: "Selecionar todos desta p\xE1gina",
925
+ downloadSelected: (count) => `Baixar ${count} selecionado${count === 1 ? "" : "s"} (.zip)`,
926
+ archiveFailed: "N\xE3o foi poss\xEDvel montar o arquivo compactado.",
927
+ total: (count) => `${count} arquivo${count === 1 ? "" : "s"}`,
928
+ page: (current, last) => `${current} / ${last}`,
929
+ searchPlaceholder: "Buscar por nome do arquivo",
930
+ empty: "Nenhum arquivo nesta conversa.",
931
+ loading: "Carregando arquivos\u2026",
932
+ failure: "N\xE3o foi poss\xEDvel carregar os arquivos.",
933
+ download: "Baixar"
934
+ };
935
+ var DEFAULT_PER_PAGE = 10;
936
+ function ConversationDocumentsPanel({
937
+ conversationId,
938
+ open,
939
+ perPage = DEFAULT_PER_PAGE,
940
+ labels: labelsOverride,
941
+ className,
942
+ classNames
943
+ }) {
944
+ const labels = { ...DEFAULT_CONVERSATION_DOCUMENTS_LABELS, ...labelsOverride };
945
+ const context = useConversations();
946
+ const [search, setSearch] = useState7("");
947
+ const [sourceFilter, setSourceFilter] = useState7(DOCUMENT_SOURCE_FILTER.ALL);
948
+ const [sortDirection, setSortDirection] = useState7("desc");
949
+ const [page, setPage] = useState7(1);
950
+ const [selectedIds, setSelectedIds] = useState7([]);
951
+ const [archiveError, setArchiveError] = useState7(false);
952
+ const hasFilters = search !== "" || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== "desc";
953
+ const { documents, total, loading, error } = useConversationDocuments(open ? conversationId : void 0, {
954
+ search,
955
+ page,
956
+ limit: perPage,
957
+ sortDirection,
958
+ ...sourceFilter === DOCUMENT_SOURCE_FILTER.ALL ? {} : { source: sourceFilter }
959
+ });
960
+ const lastPage = Math.max(1, Math.ceil(total / perPage));
961
+ function applyFilter(change) {
962
+ change();
963
+ setPage(1);
964
+ }
965
+ async function handleOpen(uploadId, disposition) {
966
+ const url = await context?.api.getDocumentUrl(uploadId, disposition);
967
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
968
+ }
969
+ const canArchive = typeof context?.api.downloadDocumentsArchive === "function";
970
+ const pageIds = documents.map((document2) => document2.id);
971
+ const allOnPageSelected = pageIds.length > 0 && pageIds.every((id) => selectedIds.includes(id));
972
+ function toggleSelected(uploadId) {
973
+ setArchiveError(false);
974
+ setSelectedIds(
975
+ (current) => current.includes(uploadId) ? current.filter((id) => id !== uploadId) : [...current, uploadId]
976
+ );
977
+ }
978
+ function toggleAllOnPage() {
979
+ setArchiveError(false);
980
+ setSelectedIds(
981
+ (current) => allOnPageSelected ? current.filter((id) => !pageIds.includes(id)) : [...current, ...pageIds.filter((id) => !current.includes(id))]
982
+ );
983
+ }
984
+ async function handleDownloadSelected() {
985
+ const archive = context?.api.downloadDocumentsArchive;
986
+ if (!archive || selectedIds.length === 0) return;
987
+ setArchiveError(false);
988
+ try {
989
+ const blob = await archive(conversationId, selectedIds);
990
+ const url = URL.createObjectURL(blob);
991
+ const anchor = document.createElement("a");
992
+ anchor.href = url;
993
+ anchor.download = `conversa-${conversationId}.zip`;
994
+ anchor.click();
995
+ URL.revokeObjectURL(url);
996
+ setSelectedIds([]);
997
+ } catch {
998
+ setArchiveError(true);
999
+ }
1000
+ }
1001
+ if (!open) return null;
1002
+ return /* @__PURE__ */ jsx13("div", { className: cn("border-b", classNames?.root, className), children: /* @__PURE__ */ jsxs8("section", { className: cn("px-4 py-3", classNames?.body), children: [
1003
+ /* @__PURE__ */ jsx13("p", { className: cn("mb-2 text-sm font-medium", classNames?.title), children: labels.title }),
1004
+ /* @__PURE__ */ jsxs8("div", { className: cn("mb-2 flex flex-wrap items-center gap-2 border-b pb-2", classNames?.filters), children: [
1005
+ /* @__PURE__ */ jsx13(
1006
+ "input",
1007
+ {
1008
+ type: "search",
1009
+ value: search,
1010
+ onChange: (event) => applyFilter(() => setSearch(event.target.value)),
1011
+ placeholder: labels.searchPlaceholder,
1012
+ "aria-label": labels.searchPlaceholder,
1013
+ className: cn("w-full rounded-md border px-3 py-2 text-sm sm:w-52", classNames?.search)
1014
+ }
1015
+ ),
1016
+ /* @__PURE__ */ jsxs8(
1017
+ "select",
1018
+ {
1019
+ value: sourceFilter,
1020
+ onChange: (event) => applyFilter(() => setSourceFilter(event.target.value)),
1021
+ "aria-label": labels.sourceFilterAll,
1022
+ className: cn("w-full rounded-md border px-2 py-2 text-sm sm:w-36", classNames?.sourceSelect),
1023
+ children: [
1024
+ /* @__PURE__ */ jsx13("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
1025
+ /* @__PURE__ */ jsx13("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
1026
+ /* @__PURE__ */ jsx13("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
1027
+ ]
1028
+ }
1029
+ ),
1030
+ /* @__PURE__ */ jsxs8(
1031
+ "button",
1032
+ {
1033
+ type: "button",
1034
+ onClick: () => applyFilter(() => setSortDirection(sortDirection === "desc" ? "asc" : "desc")),
1035
+ className: cn("cv-header-action inline-flex items-center gap-1", classNames?.sortButton),
1036
+ children: [
1037
+ /* @__PURE__ */ jsx13(ArrowUpDown, { size: 14 }),
1038
+ sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest
1039
+ ]
1040
+ }
1041
+ ),
1042
+ hasFilters ? /* @__PURE__ */ jsx13(
1043
+ "button",
1044
+ {
1045
+ type: "button",
1046
+ onClick: () => applyFilter(() => {
1047
+ setSearch("");
1048
+ setSourceFilter(DOCUMENT_SOURCE_FILTER.ALL);
1049
+ setSortDirection("desc");
1050
+ }),
1051
+ className: cn("cv-header-action", classNames?.clearButton),
1052
+ children: labels.clearFilters
1053
+ }
1054
+ ) : null
1055
+ ] }),
1056
+ loading ? /* @__PURE__ */ jsx13("p", { className: cn("text-xs text-gray-500", classNames?.status), children: labels.loading }) : null,
1057
+ error ? /* @__PURE__ */ jsx13("p", { role: "alert", className: cn("text-xs text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
1058
+ !loading && !error && documents.length === 0 ? /* @__PURE__ */ jsx13("p", { className: cn("text-xs text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
1059
+ canArchive && documents.length > 0 ? /* @__PURE__ */ jsxs8("div", { className: cn("mb-2 flex flex-wrap items-center gap-3 text-xs", classNames?.selectionBar), children: [
1060
+ /* @__PURE__ */ jsxs8("label", { className: "inline-flex items-center gap-1.5", children: [
1061
+ /* @__PURE__ */ jsx13(
1062
+ "input",
1063
+ {
1064
+ type: "checkbox",
1065
+ checked: allOnPageSelected,
1066
+ onChange: toggleAllOnPage,
1067
+ className: cn(classNames?.checkbox)
1068
+ }
1069
+ ),
1070
+ labels.selectAll
1071
+ ] }),
1072
+ selectedIds.length > 0 ? /* @__PURE__ */ jsx13("button", { type: "button", onClick: () => void handleDownloadSelected(), className: "cv-header-action", children: labels.downloadSelected(selectedIds.length) }) : null,
1073
+ archiveError ? /* @__PURE__ */ jsx13("span", { role: "alert", className: "text-red-600 dark:text-red-400", children: labels.archiveFailed }) : null
1074
+ ] }) : null,
1075
+ /* @__PURE__ */ jsx13("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
1076
+ const isFromCustomer = !TEAM_SOURCES.has(document2.source);
1077
+ const SourceIcon = isFromCustomer ? Users : Bot;
1078
+ return /* @__PURE__ */ jsxs8(
1079
+ "li",
1080
+ {
1081
+ className: cn(
1082
+ "flex items-center justify-between gap-2 rounded-lg border px-3 py-2 dark:border-gray-700",
1083
+ classNames?.item
1084
+ ),
1085
+ children: [
1086
+ /* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
1087
+ canArchive ? /* @__PURE__ */ jsx13(
1088
+ "input",
1089
+ {
1090
+ type: "checkbox",
1091
+ checked: selectedIds.includes(document2.id),
1092
+ onChange: () => toggleSelected(document2.id),
1093
+ "aria-label": `${labels.download}: ${document2.filename}`,
1094
+ className: cn("shrink-0", classNames?.checkbox)
1095
+ }
1096
+ ) : null,
1097
+ /* @__PURE__ */ jsx13(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
1098
+ /* @__PURE__ */ jsxs8("div", { className: "min-w-0 flex-1", children: [
1099
+ /* @__PURE__ */ jsxs8(
1100
+ "div",
1101
+ {
1102
+ className: cn(
1103
+ "mb-0.5 flex items-center gap-1 text-[11px] font-medium",
1104
+ isFromCustomer ? "text-blue-600 dark:text-blue-400" : "text-emerald-600 dark:text-emerald-400",
1105
+ classNames?.sourceBadge
1106
+ ),
1107
+ children: [
1108
+ /* @__PURE__ */ jsx13(SourceIcon, { size: 11 }),
1109
+ isFromCustomer ? labels.sourceFilterCustomer : labels.sourceFilterTeam
1110
+ ]
1111
+ }
1112
+ ),
1113
+ /* @__PURE__ */ jsx13(
1114
+ "div",
1115
+ {
1116
+ className: cn("truncate text-sm font-medium", classNames?.filename),
1117
+ title: document2.filename,
1118
+ children: document2.filename
1119
+ }
1120
+ ),
1121
+ /* @__PURE__ */ jsxs8("div", { className: cn("text-xs text-gray-500 dark:text-gray-400", classNames?.meta), children: [
1122
+ formatDateTime(document2.linkedAt),
1123
+ " \xB7 ",
1124
+ formatFileSize(document2.sizeBytes)
1125
+ ] })
1126
+ ] })
1127
+ ] }),
1128
+ /* @__PURE__ */ jsxs8("div", { className: "flex shrink-0 gap-1", children: [
1129
+ /* @__PURE__ */ jsx13(
1130
+ "button",
1131
+ {
1132
+ type: "button",
1133
+ onClick: () => void handleOpen(document2.id, "inline"),
1134
+ title: labels.view,
1135
+ "aria-label": `${labels.view}: ${document2.filename}`,
1136
+ className: cn("cv-header-icon", classNames?.viewButton),
1137
+ children: /* @__PURE__ */ jsx13(Eye, { size: 14 })
1138
+ }
1139
+ ),
1140
+ /* @__PURE__ */ jsx13(
1141
+ "button",
1142
+ {
1143
+ type: "button",
1144
+ onClick: () => void handleOpen(document2.id, "attachment"),
1145
+ title: labels.download,
1146
+ "aria-label": `${labels.download}: ${document2.filename}`,
1147
+ className: cn("cv-header-icon", classNames?.downloadButton),
1148
+ children: /* @__PURE__ */ jsx13(Download, { size: 14 })
1149
+ }
1150
+ )
1151
+ ] })
1152
+ ]
1153
+ },
1154
+ document2.id
1155
+ );
1156
+ }) }),
1157
+ total > perPage ? /* @__PURE__ */ jsxs8(
1158
+ "div",
1159
+ {
1160
+ className: cn(
1161
+ "mt-2 flex items-center justify-between border-t pt-2 text-xs dark:border-gray-700",
1162
+ classNames?.pagination
1163
+ ),
1164
+ children: [
1165
+ /* @__PURE__ */ jsx13("span", { className: "text-gray-400", children: labels.total(total) }),
1166
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
1167
+ /* @__PURE__ */ jsx13(
1168
+ "button",
1169
+ {
1170
+ type: "button",
1171
+ onClick: () => setPage(page - 1),
1172
+ disabled: page <= 1,
1173
+ "aria-label": labels.sortOldest,
1174
+ className: "cv-header-icon disabled:opacity-40",
1175
+ children: "\u2039"
1176
+ }
1177
+ ),
1178
+ /* @__PURE__ */ jsx13("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
1179
+ /* @__PURE__ */ jsx13(
1180
+ "button",
1181
+ {
1182
+ type: "button",
1183
+ onClick: () => setPage(page + 1),
1184
+ disabled: page >= lastPage,
1185
+ "aria-label": labels.sortMostRecent,
1186
+ className: "cv-header-icon disabled:opacity-40",
1187
+ children: "\u203A"
1188
+ }
1189
+ )
1190
+ ] })
1191
+ ]
1192
+ }
1193
+ ) : null
1194
+ ] }) });
1195
+ }
1196
+
1197
+ // src/DocumentsLibrary.tsx
1198
+ import { useEffect as useEffect3, useState as useState8 } from "react";
1199
+ import { ArrowUpDown as ArrowUpDown2, Bot as Bot2, Download as Download2, Eye as Eye2, MessageSquare, Users as Users2 } from "lucide-react";
1200
+ import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
1201
+ var DEFAULT_DOCUMENTS_LIBRARY_LABELS = {
1202
+ title: "Documentos",
1203
+ searchPlaceholder: "Buscar por nome do arquivo ou telefone",
1204
+ empty: "Nenhum arquivo trocado ainda.",
1205
+ noResults: "Nenhum arquivo encontrado para os filtros aplicados",
1206
+ loading: "Carregando arquivos\u2026",
1207
+ failure: "N\xE3o foi poss\xEDvel carregar os arquivos.",
1208
+ view: "Visualizar",
1209
+ download: "Baixar",
1210
+ openConversation: "Abrir conversa",
1211
+ sourceFilterAll: "Todas as origens",
1212
+ sourceFilterCustomer: "Cliente",
1213
+ sourceFilterTeam: "Equipe",
1214
+ sortMostRecent: "Mais recentes",
1215
+ sortOldest: "Mais antigos",
1216
+ clearFilters: "Limpar filtros",
1217
+ total: (count) => `${count} arquivo${count === 1 ? "" : "s"}`,
1218
+ page: (current, last) => `${current} / ${last}`
1219
+ };
1220
+ var TEAM_SOURCES2 = /* @__PURE__ */ new Set(["agent", "bot"]);
1221
+ var DEFAULT_PER_PAGE2 = 20;
1222
+ function DocumentsLibrary({
1223
+ perPage = DEFAULT_PER_PAGE2,
1224
+ onOpenConversation,
1225
+ labels: labelsOverride,
1226
+ className,
1227
+ classNames
1228
+ }) {
1229
+ const labels = { ...DEFAULT_DOCUMENTS_LIBRARY_LABELS, ...labelsOverride };
1230
+ const context = useConversations();
1231
+ const [search, setSearch] = useState8("");
1232
+ const [sourceFilter, setSourceFilter] = useState8(DOCUMENT_SOURCE_FILTER.ALL);
1233
+ const [sortDirection, setSortDirection] = useState8("desc");
1234
+ const [page, setPage] = useState8(1);
1235
+ const [documents, setDocuments] = useState8([]);
1236
+ const [total, setTotal] = useState8(0);
1237
+ const [loading, setLoading] = useState8(false);
1238
+ const [failed, setFailed] = useState8(false);
1239
+ const hasFilters = search !== "" || sourceFilter !== DOCUMENT_SOURCE_FILTER.ALL || sortDirection !== "desc";
1240
+ const lastPage = Math.max(1, Math.ceil(total / perPage));
1241
+ const fetchAll = context?.api.getAllDocuments;
1242
+ useEffect3(() => {
1243
+ if (!fetchAll) return;
1244
+ let active = true;
1245
+ setLoading(true);
1246
+ setFailed(false);
1247
+ void fetchAll({
1248
+ search,
1249
+ page,
1250
+ limit: perPage,
1251
+ sortDirection,
1252
+ ...sourceFilter === DOCUMENT_SOURCE_FILTER.ALL ? {} : { source: sourceFilter }
1253
+ }).then((result) => {
1254
+ if (!active) return;
1255
+ setDocuments(result.documents);
1256
+ setTotal(result.total);
1257
+ }).catch(() => {
1258
+ if (active) setFailed(true);
1259
+ }).finally(() => {
1260
+ if (active) setLoading(false);
1261
+ });
1262
+ return () => {
1263
+ active = false;
1264
+ };
1265
+ }, [fetchAll, search, sourceFilter, sortDirection, page, perPage]);
1266
+ function applyFilter(change) {
1267
+ change();
1268
+ setPage(1);
1269
+ }
1270
+ async function handleOpen(uploadId, disposition) {
1271
+ const url = await context?.api.getDocumentUrl(uploadId, disposition);
1272
+ if (url) window.open(url, "_blank", "noopener,noreferrer");
1273
+ }
1274
+ if (!fetchAll) return null;
1275
+ return /* @__PURE__ */ jsxs9("div", { className: cn("space-y-3", classNames?.root, className), children: [
1276
+ /* @__PURE__ */ jsx14("h2", { className: cn("text-lg font-semibold", classNames?.title), children: labels.title }),
1277
+ /* @__PURE__ */ jsxs9("div", { className: cn("flex flex-wrap items-center gap-2", classNames?.filters), children: [
1278
+ /* @__PURE__ */ jsx14(
1279
+ "input",
1280
+ {
1281
+ type: "search",
1282
+ value: search,
1283
+ onChange: (event) => applyFilter(() => setSearch(event.target.value)),
1284
+ placeholder: labels.searchPlaceholder,
1285
+ "aria-label": labels.searchPlaceholder,
1286
+ className: cn("w-full rounded-md border px-3 py-2 text-sm sm:w-64", classNames?.search)
1287
+ }
1288
+ ),
1289
+ /* @__PURE__ */ jsxs9(
1290
+ "select",
1291
+ {
1292
+ value: sourceFilter,
1293
+ onChange: (event) => applyFilter(() => setSourceFilter(event.target.value)),
1294
+ "aria-label": labels.sourceFilterAll,
1295
+ className: cn("w-full rounded-md border px-2 py-2 text-sm sm:w-40", classNames?.sourceSelect),
1296
+ children: [
1297
+ /* @__PURE__ */ jsx14("option", { value: DOCUMENT_SOURCE_FILTER.ALL, children: labels.sourceFilterAll }),
1298
+ /* @__PURE__ */ jsx14("option", { value: DOCUMENT_SOURCE_FILTER.CUSTOMER, children: labels.sourceFilterCustomer }),
1299
+ /* @__PURE__ */ jsx14("option", { value: DOCUMENT_SOURCE_FILTER.TEAM, children: labels.sourceFilterTeam })
1300
+ ]
1301
+ }
1302
+ ),
1303
+ /* @__PURE__ */ jsxs9(
1304
+ "button",
1305
+ {
1306
+ type: "button",
1307
+ onClick: () => applyFilter(() => setSortDirection(sortDirection === "desc" ? "asc" : "desc")),
1308
+ className: cn("cv-header-action inline-flex items-center gap-1", classNames?.sortButton),
1309
+ children: [
1310
+ /* @__PURE__ */ jsx14(ArrowUpDown2, { size: 14 }),
1311
+ sortDirection === "desc" ? labels.sortMostRecent : labels.sortOldest
1312
+ ]
1313
+ }
1314
+ ),
1315
+ hasFilters ? /* @__PURE__ */ jsx14(
1316
+ "button",
1317
+ {
1318
+ type: "button",
1319
+ onClick: () => applyFilter(() => {
1320
+ setSearch("");
1321
+ setSourceFilter(DOCUMENT_SOURCE_FILTER.ALL);
1322
+ setSortDirection("desc");
1323
+ }),
1324
+ className: cn("cv-header-action", classNames?.clearButton),
1325
+ children: labels.clearFilters
1326
+ }
1327
+ ) : null
1328
+ ] }),
1329
+ loading ? /* @__PURE__ */ jsx14("p", { className: cn("text-sm text-gray-500", classNames?.status), children: labels.loading }) : null,
1330
+ failed ? /* @__PURE__ */ jsx14("p", { role: "alert", className: cn("text-sm text-red-600 dark:text-red-400", classNames?.status), children: labels.failure }) : null,
1331
+ !loading && !failed && documents.length === 0 ? /* @__PURE__ */ jsx14("p", { className: cn("text-sm text-gray-500", classNames?.status), children: hasFilters ? labels.noResults : labels.empty }) : null,
1332
+ /* @__PURE__ */ jsx14("ul", { className: cn("space-y-2", classNames?.list), children: documents.map((document2) => {
1333
+ const isFromCustomer = !TEAM_SOURCES2.has(document2.source);
1334
+ const SourceIcon = isFromCustomer ? Users2 : Bot2;
1335
+ return /* @__PURE__ */ jsxs9(
1336
+ "li",
1337
+ {
1338
+ className: cn(
1339
+ "flex items-center justify-between gap-3 rounded-lg border px-3 py-2 dark:border-gray-700",
1340
+ classNames?.item
1341
+ ),
1342
+ children: [
1343
+ /* @__PURE__ */ jsxs9("div", { className: "flex min-w-0 flex-1 items-center gap-3", children: [
1344
+ /* @__PURE__ */ jsx14(FileIcon, { filename: document2.filename, mimeType: document2.mimeType }),
1345
+ /* @__PURE__ */ jsxs9("div", { className: "min-w-0 flex-1", children: [
1346
+ /* @__PURE__ */ jsx14("div", { className: cn("truncate text-sm font-medium", classNames?.filename), title: document2.filename, children: document2.filename }),
1347
+ /* @__PURE__ */ jsxs9("div", { className: cn("flex flex-wrap items-center gap-x-2 text-xs text-gray-500", classNames?.meta), children: [
1348
+ /* @__PURE__ */ jsxs9("span", { className: "inline-flex items-center gap-1", children: [
1349
+ /* @__PURE__ */ jsx14(SourceIcon, { size: 11 }),
1350
+ isFromCustomer ? labels.sourceFilterCustomer : labels.sourceFilterTeam
1351
+ ] }),
1352
+ /* @__PURE__ */ jsx14("span", { children: "\xB7" }),
1353
+ /* @__PURE__ */ jsx14("span", { children: formatDateTime(document2.linkedAt) }),
1354
+ /* @__PURE__ */ jsx14("span", { children: "\xB7" }),
1355
+ /* @__PURE__ */ jsx14("span", { children: formatFileSize(document2.sizeBytes) })
1356
+ ] })
1357
+ ] })
1358
+ ] }),
1359
+ onOpenConversation ? /* @__PURE__ */ jsxs9(
1360
+ "button",
1361
+ {
1362
+ type: "button",
1363
+ onClick: () => onOpenConversation(document2.conversationId),
1364
+ title: labels.openConversation,
1365
+ className: cn("cv-header-action inline-flex shrink-0 items-center gap-1", classNames?.conversationLink),
1366
+ children: [
1367
+ /* @__PURE__ */ jsx14(MessageSquare, { size: 12 }),
1368
+ formatPhone(document2.conversationId)
1369
+ ]
1370
+ }
1371
+ ) : /* @__PURE__ */ jsx14("span", { className: cn("shrink-0 text-xs text-gray-500", classNames?.conversationLink), children: formatPhone(document2.conversationId) }),
1372
+ /* @__PURE__ */ jsxs9("div", { className: "flex shrink-0 gap-1", children: [
1373
+ /* @__PURE__ */ jsx14(
1374
+ "button",
1375
+ {
1376
+ type: "button",
1377
+ onClick: () => void handleOpen(document2.id, "inline"),
1378
+ title: labels.view,
1379
+ "aria-label": `${labels.view}: ${document2.filename}`,
1380
+ className: "cv-header-icon",
1381
+ children: /* @__PURE__ */ jsx14(Eye2, { size: 14 })
1382
+ }
1383
+ ),
1384
+ /* @__PURE__ */ jsx14(
1385
+ "button",
1386
+ {
1387
+ type: "button",
1388
+ onClick: () => void handleOpen(document2.id, "attachment"),
1389
+ title: labels.download,
1390
+ "aria-label": `${labels.download}: ${document2.filename}`,
1391
+ className: "cv-header-icon",
1392
+ children: /* @__PURE__ */ jsx14(Download2, { size: 14 })
1393
+ }
1394
+ )
1395
+ ] })
1396
+ ]
1397
+ },
1398
+ `${document2.conversationId}:${document2.id}`
1399
+ );
1400
+ }) }),
1401
+ total > perPage ? /* @__PURE__ */ jsxs9("div", { className: cn("flex items-center justify-between border-t pt-2 text-xs dark:border-gray-700", classNames?.pagination), children: [
1402
+ /* @__PURE__ */ jsx14("span", { className: "text-gray-400", children: labels.total(total) }),
1403
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
1404
+ /* @__PURE__ */ jsx14(
1405
+ "button",
1406
+ {
1407
+ type: "button",
1408
+ onClick: () => setPage(page - 1),
1409
+ disabled: page <= 1,
1410
+ className: "cv-header-icon disabled:opacity-40",
1411
+ children: "\u2039"
1412
+ }
1413
+ ),
1414
+ /* @__PURE__ */ jsx14("span", { className: "text-gray-500", children: labels.page(page, lastPage) }),
1415
+ /* @__PURE__ */ jsx14(
1416
+ "button",
1417
+ {
1418
+ type: "button",
1419
+ onClick: () => setPage(page + 1),
1420
+ disabled: page >= lastPage,
1421
+ className: "cv-header-icon disabled:opacity-40",
1422
+ children: "\u203A"
1423
+ }
1424
+ )
1425
+ ] })
1426
+ ] }) : null
1427
+ ] });
1428
+ }
1429
+
1430
+ export {
1431
+ ConversationLocalesProvider,
1432
+ useConversationLocales,
1433
+ StatusTicks,
1434
+ AudioPlayer,
1435
+ cn,
1436
+ FileIcon,
1437
+ formatTimestamp,
1438
+ formatDateTime,
1439
+ isSameDay,
1440
+ formatFileSize,
1441
+ MediaRenderer,
1442
+ DEFAULT_LIGHTBOX_LABELS,
1443
+ Lightbox,
1444
+ createMediaUrlResolver,
1445
+ ConversationsProvider,
1446
+ useConversations,
1447
+ MessageBubble,
1448
+ ConversationWallpaper,
1449
+ EmojiPicker,
1450
+ DEFAULT_MESSAGE_COMPOSER_LABELS,
1451
+ MessageComposer,
1452
+ DateDivider,
1453
+ formatPhone,
1454
+ phoneCountryFlag,
1455
+ phoneInitials,
1456
+ useAsyncResource,
1457
+ conversationsOf,
1458
+ totalOf,
1459
+ useConversationDocuments,
1460
+ DOCUMENT_SOURCE_FILTER,
1461
+ DEFAULT_CONVERSATION_DOCUMENTS_LABELS,
1462
+ ConversationDocumentsPanel,
1463
+ DEFAULT_DOCUMENTS_LIBRARY_LABELS,
1464
+ DocumentsLibrary
1465
+ };