@uptrademedia/site-kit 1.0.15 → 1.0.17

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.
@@ -13,11 +13,23 @@ function generateVisitorId() {
13
13
  const stored = typeof localStorage !== "undefined" ? localStorage.getItem("engage_visitor_id") : null;
14
14
  if (stored) return stored;
15
15
  const id = `visitor_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
16
- if (typeof localStorage !== "undefined") {
17
- localStorage.setItem("engage_visitor_id", id);
18
- }
16
+ if (typeof localStorage !== "undefined") localStorage.setItem("engage_visitor_id", id);
19
17
  return id;
20
18
  }
19
+ function adjustColor(hex, amount) {
20
+ const num = parseInt(hex.replace("#", ""), 16);
21
+ const r = Math.min(255, Math.max(0, (num >> 16) + amount));
22
+ const g = Math.min(255, Math.max(0, (num >> 8 & 255) + amount));
23
+ const b = Math.min(255, Math.max(0, (num & 255) + amount));
24
+ return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
25
+ }
26
+ function isLightColor(hex) {
27
+ const num = parseInt(hex.replace("#", ""), 16);
28
+ const r = num >> 16;
29
+ const g = num >> 8 & 255;
30
+ const b = num & 255;
31
+ return (r * 299 + g * 587 + b * 114) / 1e3 > 160;
32
+ }
21
33
  function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
22
34
  const [isOpen, setIsOpen] = useState(false);
23
35
  const [messages, setMessages] = useState([]);
@@ -35,15 +47,23 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
35
47
  const [handoffOfflinePrompt, setHandoffOfflinePrompt] = useState(null);
36
48
  const [pendingFiles, setPendingFiles] = useState([]);
37
49
  const [lastFailedSend, setLastFailedSend] = useState(null);
50
+ const [showWelcome, setShowWelcome] = useState(true);
38
51
  useRef(null);
39
52
  const messagesEndRef = useRef(null);
40
53
  const inputRef = useRef(null);
41
54
  const socketRef = useRef(null);
42
55
  const pollingIntervalRef = useRef(null);
43
- const position = config?.position || "bottom-right";
44
- const buttonColor = config?.buttonColor || "#00afab";
56
+ const position = config?.position || widgetConfig?.position || "bottom-right";
57
+ const primaryColor = widgetConfig?.brand_primary || config?.buttonColor || "#00afab";
58
+ const secondaryColor = widgetConfig?.brand_secondary || config?.brandSecondary || adjustColor(primaryColor, -30);
59
+ const businessName = widgetConfig?.project_name || widgetConfig?.business_info?.name || "Chat with us";
60
+ const logoUrl = widgetConfig?.logo_url || null;
61
+ const welcomeEnabled = widgetConfig?.welcome_screen_enabled !== false;
62
+ const quickActions = Array.isArray(widgetConfig?.welcome_quick_actions) ? widgetConfig.welcome_quick_actions : [];
63
+ const showPoweredBy = widgetConfig?.show_powered_by !== false;
45
64
  const welcomeMessage = widgetConfig?.initial_message ?? widgetConfig?.welcome_message ?? config?.welcomeMessage ?? "Hi! How can I help you today?";
46
- const offlineFormPrompt = handoffOfflinePrompt ?? widgetConfig?.form_description ?? widgetConfig?.offline_message ?? config?.offlineMessage ?? "We're currently offline. Leave us a message and we'll get back to you!";
65
+ const offlineHeading = widgetConfig?.offline_heading ?? "No agents available right now";
66
+ const offlineSubheading = handoffOfflinePrompt ?? widgetConfig?.offline_subheading ?? widgetConfig?.form_description ?? widgetConfig?.offline_message ?? config?.offlineMessage ?? "Leave us a message and we'll get back to you!";
47
67
  const baseUrl = propApiUrl || getApiConfig().apiUrl;
48
68
  const fetchWidgetConfig = useCallback(async () => {
49
69
  try {
@@ -70,6 +90,7 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
70
90
  setAvailability(data);
71
91
  if (data.mode === "offline" && !sessionId) {
72
92
  setShowOfflineForm(true);
93
+ setShowWelcome(false);
73
94
  }
74
95
  }
75
96
  } catch (error) {
@@ -81,10 +102,7 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
81
102
  const { apiKey } = getApiConfig();
82
103
  const response = await fetch(`${baseUrl}/api/engage/widget/session`, {
83
104
  method: "POST",
84
- headers: {
85
- "Content-Type": "application/json",
86
- ...apiKey && { "x-api-key": apiKey }
87
- },
105
+ headers: { "Content-Type": "application/json", ...apiKey && { "x-api-key": apiKey } },
88
106
  body: JSON.stringify({
89
107
  projectId,
90
108
  visitorId,
@@ -97,13 +115,15 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
97
115
  const sid = data.id || data.session_id;
98
116
  setSessionId(sid);
99
117
  if (data.messages?.length > 0) {
100
- setMessages(data.messages.map((m) => ({
101
- id: m.id,
102
- role: m.role === "visitor" ? "user" : m.role,
103
- content: m.content,
104
- timestamp: new Date(m.created_at),
105
- agentName: m.sender_name
106
- })));
118
+ setMessages(
119
+ data.messages.map((m) => ({
120
+ id: m.id,
121
+ role: m.role === "visitor" ? "user" : m.role,
122
+ content: m.content,
123
+ timestamp: new Date(m.created_at),
124
+ agentName: m.sender_name
125
+ }))
126
+ );
107
127
  }
108
128
  return sid;
109
129
  }
@@ -121,11 +141,11 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
121
141
  role,
122
142
  content: data.content ?? "",
123
143
  timestamp: /* @__PURE__ */ new Date(),
124
- agentName: data.agentName
144
+ agentName: data.agentName,
145
+ ...data.attachments?.length ? { attachments: data.attachments } : {},
146
+ ...data.suggestions?.length ? { suggestions: data.suggestions } : {}
125
147
  };
126
- const withAttachments = data.attachments?.length ? { ...newMessage, attachments: data.attachments } : newMessage;
127
- const withSuggestions = data.suggestions?.length ? { ...withAttachments, suggestions: data.suggestions } : withAttachments;
128
- setMessages((prev) => [...prev, withSuggestions]);
148
+ setMessages((prev) => [...prev, newMessage]);
129
149
  if (role === "assistant" || role === "agent") {
130
150
  setAgentTyping(false);
131
151
  setIsLoading(false);
@@ -133,41 +153,50 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
133
153
  break;
134
154
  }
135
155
  case "agent:joined":
136
- setMessages((prev) => [...prev, {
137
- id: `system-${Date.now()}`,
138
- role: "system",
139
- content: data.agentName ? `${data.agentName} has joined the chat.` : "An agent has joined the chat.",
140
- timestamp: /* @__PURE__ */ new Date()
141
- }]);
156
+ setMessages((prev) => [
157
+ ...prev,
158
+ {
159
+ id: `system-${Date.now()}`,
160
+ role: "system",
161
+ content: data.agentName ? `${data.agentName} has joined the chat.` : "An agent has joined the chat.",
162
+ timestamp: /* @__PURE__ */ new Date()
163
+ }
164
+ ]);
142
165
  break;
143
166
  case "typing":
144
167
  setAgentTyping(data.isTyping);
145
168
  break;
146
169
  case "handoff:initiated":
147
- setMessages((prev) => [...prev, {
148
- id: `system-${Date.now()}`,
149
- role: "system",
150
- content: data.message || "Connecting you with a team member...",
151
- timestamp: /* @__PURE__ */ new Date()
152
- }]);
170
+ setMessages((prev) => [
171
+ ...prev,
172
+ { id: `system-${Date.now()}`, role: "system", content: data.message || "Connecting you with a team member...", timestamp: /* @__PURE__ */ new Date() }
173
+ ]);
153
174
  break;
154
175
  case "chat:closed":
155
- setMessages((prev) => [...prev, {
156
- id: `system-${Date.now()}`,
157
- role: "system",
158
- content: data.message || "This chat has been closed.",
159
- timestamp: /* @__PURE__ */ new Date()
160
- }]);
176
+ setMessages((prev) => [
177
+ ...prev,
178
+ { id: `system-${Date.now()}`, role: "system", content: data.message || "This chat has been closed.", timestamp: /* @__PURE__ */ new Date() }
179
+ ]);
161
180
  break;
162
181
  }
163
182
  }, []);
164
183
  const connectSocket = useCallback(
165
184
  (currentSessionId) => {
166
185
  if (socketRef.current?.connected) return;
186
+ if (socketRef.current) {
187
+ socketRef.current.disconnect();
188
+ socketRef.current = null;
189
+ }
167
190
  const namespaceUrl = `${baseUrl.replace(/\/$/, "")}/engage/chat`;
168
191
  const socket = io(namespaceUrl, {
169
192
  query: { projectId, visitorId, sessionId: currentSessionId },
170
- transports: ["websocket", "polling"]
193
+ transports: ["websocket", "polling"],
194
+ // Auto-reconnect config
195
+ reconnection: true,
196
+ reconnectionAttempts: 10,
197
+ reconnectionDelay: 1e3,
198
+ reconnectionDelayMax: 1e4,
199
+ timeout: 15e3
171
200
  });
172
201
  socket.on("connect", () => {
173
202
  setConnectionStatus("connected");
@@ -181,81 +210,100 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
181
210
  if (Array.isArray(data) && data.length) {
182
211
  setMessages((prev) => {
183
212
  const byId = new Map(prev.map((m) => [m.id, m]));
184
- data.forEach((m) => byId.set(m.id, { id: m.id, role: m.role === "visitor" ? "user" : m.role, content: m.content, timestamp: new Date(m.created_at), agentName: m.sender_name, attachments: m.attachments }));
213
+ data.forEach(
214
+ (m) => byId.set(m.id, {
215
+ id: m.id,
216
+ role: m.role === "visitor" ? "user" : m.role,
217
+ content: m.content,
218
+ timestamp: new Date(m.created_at),
219
+ agentName: m.sender_name,
220
+ attachments: m.attachments
221
+ })
222
+ );
185
223
  return Array.from(byId.values()).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
186
224
  });
187
225
  }
188
226
  }).catch((err) => console.warn("[ChatWidget] Refetch messages on reconnect failed", err));
189
227
  }
228
+ if (lastFailedSend) {
229
+ socket.emit("visitor:message", {
230
+ content: lastFailedSend.content,
231
+ attachments: lastFailedSend.attachments?.length ? lastFailedSend.attachments : void 0
232
+ });
233
+ setLastFailedSend(null);
234
+ setMessages((prev) => prev.filter((m) => !m.sendFailed));
235
+ setIsLoading(true);
236
+ }
190
237
  });
191
- socket.on("message", (data) => {
192
- handleSocketMessage({ type: "message", ...data });
193
- });
194
- socket.on("agent:joined", (data) => {
195
- handleSocketMessage({ type: "agent:joined", ...data });
196
- });
197
- socket.on("typing", (data) => {
198
- handleSocketMessage({ type: "typing", ...data });
199
- });
200
- socket.on("handoff:initiated", (data) => {
201
- handleSocketMessage({ type: "handoff:initiated", ...data });
202
- });
203
- socket.on("chat:closed", (data) => {
204
- handleSocketMessage({ type: "chat:closed", ...data });
205
- });
238
+ socket.on("message", (data) => handleSocketMessage({ type: "message", ...data }));
239
+ socket.on("agent:joined", (data) => handleSocketMessage({ type: "agent:joined", ...data }));
240
+ socket.on("typing", (data) => handleSocketMessage({ type: "typing", ...data }));
241
+ socket.on("handoff:initiated", (data) => handleSocketMessage({ type: "handoff:initiated", ...data }));
242
+ socket.on("chat:closed", (data) => handleSocketMessage({ type: "chat:closed", ...data }));
206
243
  socket.on("disconnect", (reason) => {
207
244
  setConnectionStatus("disconnected");
208
245
  console.log("[ChatWidget] Socket disconnected:", reason);
209
246
  });
247
+ socket.on("reconnect_attempt", (attempt) => {
248
+ setConnectionStatus("connecting");
249
+ console.log(`[ChatWidget] Reconnect attempt #${attempt}`);
250
+ });
251
+ socket.on("reconnect_failed", () => {
252
+ console.warn("[ChatWidget] All reconnect attempts failed, falling back to polling");
253
+ if (isOpen && currentSessionId) startPolling(currentSessionId);
254
+ });
210
255
  socket.on("connect_error", (err) => {
211
256
  console.error("[ChatWidget] Socket connect error:", err);
212
- setConnectionStatus("disconnected");
213
- if (isOpen && currentSessionId) startPolling(currentSessionId);
257
+ setConnectionStatus("connecting");
214
258
  });
215
259
  socketRef.current = socket;
216
260
  },
217
- [projectId, visitorId, baseUrl, isOpen, handleSocketMessage, getApiConfig]
261
+ [projectId, visitorId, baseUrl, isOpen, handleSocketMessage, lastFailedSend]
218
262
  );
219
- const startPolling = useCallback((currentSessionId) => {
220
- if (pollingIntervalRef.current) return;
221
- pollingIntervalRef.current = setInterval(async () => {
222
- try {
223
- const { apiKey } = getApiConfig();
224
- const response = await fetch(
225
- `${baseUrl}/api/engage/widget/messages?sessionId=${currentSessionId}`,
226
- { headers: apiKey ? { "x-api-key": apiKey } : {} }
227
- );
228
- if (response.ok) {
229
- const { data } = await response.json();
230
- setMessages((prev) => {
231
- const existingIds = new Set(prev.map((m) => m.id));
232
- const newMessages = data.filter((m) => !existingIds.has(m.id));
233
- if (newMessages.length > 0) {
234
- return [...prev, ...newMessages.map((m) => ({
235
- id: m.id,
236
- role: m.role === "visitor" ? "user" : m.role,
237
- content: m.content,
238
- timestamp: new Date(m.created_at),
239
- agentName: m.sender_name
240
- }))];
241
- }
242
- return prev;
263
+ const startPolling = useCallback(
264
+ (currentSessionId) => {
265
+ if (pollingIntervalRef.current) return;
266
+ pollingIntervalRef.current = setInterval(async () => {
267
+ try {
268
+ const { apiKey } = getApiConfig();
269
+ const response = await fetch(`${baseUrl}/api/engage/widget/messages?sessionId=${currentSessionId}`, {
270
+ headers: apiKey ? { "x-api-key": apiKey } : {}
243
271
  });
272
+ if (response.ok) {
273
+ const { data } = await response.json();
274
+ setMessages((prev) => {
275
+ const existingIds = new Set(prev.map((m) => m.id));
276
+ const newMessages = data.filter((m) => !existingIds.has(m.id));
277
+ if (newMessages.length > 0) {
278
+ return [
279
+ ...prev,
280
+ ...newMessages.map((m) => ({
281
+ id: m.id,
282
+ role: m.role === "visitor" ? "user" : m.role,
283
+ content: m.content,
284
+ timestamp: new Date(m.created_at),
285
+ agentName: m.sender_name
286
+ }))
287
+ ];
288
+ }
289
+ return prev;
290
+ });
291
+ }
292
+ } catch (error) {
293
+ console.error("[ChatWidget] Polling failed:", error);
244
294
  }
245
- } catch (error) {
246
- console.error("[ChatWidget] Polling failed:", error);
247
- }
248
- }, 3e3);
249
- }, [baseUrl]);
295
+ }, 3e3);
296
+ },
297
+ [baseUrl]
298
+ );
250
299
  useEffect(() => {
251
300
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
252
301
  }, [messages, agentTyping]);
253
302
  useEffect(() => {
254
- if (isOpen && inputRef.current) {
255
- inputRef.current.focus();
256
- }
257
- }, [isOpen]);
303
+ if (isOpen && !showWelcome && inputRef.current) inputRef.current.focus();
304
+ }, [isOpen, showWelcome]);
258
305
  useEffect(() => {
306
+ fetchWidgetConfig();
259
307
  checkAvailability();
260
308
  const interval = setInterval(checkAvailability, 6e4);
261
309
  return () => {
@@ -265,12 +313,9 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
265
313
  socketRef.current = null;
266
314
  }
267
315
  };
268
- }, [checkAvailability]);
316
+ }, [fetchWidgetConfig, checkAvailability]);
269
317
  useEffect(() => {
270
- if (isOpen) fetchWidgetConfig();
271
- }, [isOpen, fetchWidgetConfig]);
272
- useEffect(() => {
273
- if (isOpen && !sessionId && availability?.mode !== "offline") {
318
+ if (isOpen && !showWelcome && !sessionId && availability?.mode !== "offline") {
274
319
  initSession().then((id) => {
275
320
  if (id && (availability?.mode === "live" || availability?.mode === "ai")) {
276
321
  setConnectionStatus("connecting");
@@ -281,18 +326,27 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
281
326
  return () => {
282
327
  if (pollingIntervalRef.current) clearInterval(pollingIntervalRef.current);
283
328
  };
284
- }, [isOpen, sessionId, availability?.mode, initSession, connectSocket]);
329
+ }, [isOpen, showWelcome, sessionId, availability?.mode, initSession, connectSocket]);
285
330
  const handleToggle = useCallback(() => {
286
331
  setIsOpen((prev) => !prev);
287
- if (!isOpen && messages.length === 0 && availability?.mode !== "offline") {
288
- setMessages([{
289
- id: "welcome",
290
- role: "assistant",
291
- content: welcomeMessage,
292
- timestamp: /* @__PURE__ */ new Date()
293
- }]);
294
- }
295
- }, [isOpen, messages.length, welcomeMessage, availability?.mode]);
332
+ }, []);
333
+ const startChat = useCallback(
334
+ (initialMessage) => {
335
+ setShowWelcome(false);
336
+ setMessages([{ id: "welcome", role: "assistant", content: welcomeMessage, timestamp: /* @__PURE__ */ new Date() }]);
337
+ if (initialMessage) {
338
+ setInputValue(initialMessage);
339
+ setTimeout(() => {
340
+ setInputValue("");
341
+ const userMsg = { id: `user-${Date.now()}`, role: "user", content: initialMessage, timestamp: /* @__PURE__ */ new Date() };
342
+ setMessages((prev) => [...prev, userMsg]);
343
+ setIsLoading(true);
344
+ setLastFailedSend({ content: initialMessage, attachments: [] });
345
+ }, 100);
346
+ }
347
+ },
348
+ [welcomeMessage]
349
+ );
296
350
  const uploadWidgetFile = useCallback(
297
351
  async (file) => {
298
352
  if (!sessionId || !visitorId) return null;
@@ -351,13 +405,17 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
351
405
  return;
352
406
  }
353
407
  setLastFailedSend({ content: userMessage.content, attachments });
354
- setMessages((prev) => [
355
- ...prev,
356
- { id: `error-${Date.now()}`, role: "assistant", content: "Connection lost.", timestamp: /* @__PURE__ */ new Date(), sendFailed: true }
357
- ]);
358
- setIsLoading(false);
408
+ setTimeout(() => {
409
+ if (!socketRef.current?.connected) {
410
+ setMessages((prev) => [
411
+ ...prev,
412
+ { id: `error-${Date.now()}`, role: "system", content: "Reconnecting...", timestamp: /* @__PURE__ */ new Date(), sendFailed: true }
413
+ ]);
414
+ setIsLoading(false);
415
+ }
416
+ }, 3e3);
359
417
  },
360
- [inputValue, isLoading, pendingFiles, uploadWidgetFile, sessionId]
418
+ [inputValue, isLoading, pendingFiles, uploadWidgetFile]
361
419
  );
362
420
  const retryFailedSend = useCallback(() => {
363
421
  if (!lastFailedSend || !sessionId) return;
@@ -369,7 +427,6 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
369
427
  setIsLoading(true);
370
428
  } else {
371
429
  connectSocket(sessionId);
372
- setLastFailedSend(lastFailedSend);
373
430
  }
374
431
  }, [lastFailedSend, sessionId, connectSocket]);
375
432
  const requestHandoff = useCallback(async () => {
@@ -381,71 +438,54 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
381
438
  });
382
439
  const avail = availRes.ok ? (await availRes.json()).data : null;
383
440
  if (avail?.agentsOnline === 0) {
384
- setHandoffOfflinePrompt(
385
- widgetConfig?.form_description ?? widgetConfig?.offline_message ?? "Nobody is online right now. Leave your details and we'll get back to you."
386
- );
441
+ setHandoffOfflinePrompt(widgetConfig?.offline_subheading ?? "Nobody is online right now. Leave your details and we'll get back to you.");
387
442
  setShowOfflineForm(true);
388
443
  setMessages((prev) => [
389
444
  ...prev,
390
- {
391
- id: `handoff-offline-${Date.now()}`,
392
- role: "system",
393
- content: "We're not available at the moment. Please leave your info below and we'll follow up soon.",
394
- timestamp: /* @__PURE__ */ new Date()
395
- }
445
+ { id: `handoff-offline-${Date.now()}`, role: "system", content: offlineHeading, timestamp: /* @__PURE__ */ new Date() }
396
446
  ]);
397
447
  return;
398
448
  }
399
449
  await fetch(`${baseUrl}/api/engage/widget/handoff`, {
400
450
  method: "POST",
401
- headers: {
402
- "Content-Type": "application/json",
403
- ...apiKey && { "x-api-key": apiKey }
404
- },
451
+ headers: { "Content-Type": "application/json", ...apiKey && { "x-api-key": apiKey } },
405
452
  body: JSON.stringify({ sessionId })
406
453
  });
407
454
  setMessages((prev) => [
408
455
  ...prev,
409
- {
410
- id: `handoff-${Date.now()}`,
411
- role: "system",
412
- content: "Connecting you with a team member. Please hold on!",
413
- timestamp: /* @__PURE__ */ new Date()
414
- }
456
+ { id: `handoff-${Date.now()}`, role: "system", content: "Connecting you with a team member. Please hold on!", timestamp: /* @__PURE__ */ new Date() }
415
457
  ]);
416
458
  } catch (error) {
417
459
  console.error("[ChatWidget] Handoff request failed:", error);
418
460
  }
419
- }, [sessionId, baseUrl, projectId, widgetConfig]);
420
- const handleOfflineSubmit = useCallback(async (e) => {
421
- e.preventDefault();
422
- if (!offlineForm.name || !offlineForm.email || !offlineForm.message) return;
423
- setIsLoading(true);
424
- try {
425
- const { apiKey } = getApiConfig();
426
- const response = await fetch(`${baseUrl}/api/engage/widget/offline-form`, {
427
- method: "POST",
428
- headers: {
429
- "Content-Type": "application/json",
430
- ...apiKey && { "x-api-key": apiKey }
431
- },
432
- body: JSON.stringify({
433
- projectId,
434
- visitorId,
435
- ...offlineForm,
436
- pageUrl: typeof window !== "undefined" ? window.location.href : "",
437
- ...widgetConfig?.offlineFormSlug && { formSlug: widgetConfig.offlineFormSlug }
438
- })
439
- });
440
- if (response.ok) {
441
- setOfflineSubmitted(true);
461
+ }, [sessionId, baseUrl, projectId, widgetConfig, offlineHeading]);
462
+ const handleOfflineSubmit = useCallback(
463
+ async (e) => {
464
+ e.preventDefault();
465
+ if (!offlineForm.name || !offlineForm.email || !offlineForm.message) return;
466
+ setIsLoading(true);
467
+ try {
468
+ const { apiKey } = getApiConfig();
469
+ const response = await fetch(`${baseUrl}/api/engage/widget/offline-form`, {
470
+ method: "POST",
471
+ headers: { "Content-Type": "application/json", ...apiKey && { "x-api-key": apiKey } },
472
+ body: JSON.stringify({
473
+ projectId,
474
+ visitorId,
475
+ ...offlineForm,
476
+ pageUrl: typeof window !== "undefined" ? window.location.href : "",
477
+ ...widgetConfig?.offlineFormSlug && { formSlug: widgetConfig.offlineFormSlug }
478
+ })
479
+ });
480
+ if (response.ok) setOfflineSubmitted(true);
481
+ } catch (error) {
482
+ console.error("[ChatWidget] Offline form submission failed:", error);
483
+ } finally {
484
+ setIsLoading(false);
442
485
  }
443
- } catch (error) {
444
- console.error("[ChatWidget] Offline form submission failed:", error);
445
- } finally {
446
- setIsLoading(false);
447
- }
448
- }, [offlineForm, projectId, visitorId, baseUrl]);
486
+ },
487
+ [offlineForm, projectId, visitorId, baseUrl, widgetConfig]
488
+ );
449
489
  const handleKeyDown = useCallback((e) => {
450
490
  if (e.key === "Enter" && !e.shiftKey) {
451
491
  e.preventDefault();
@@ -457,12 +497,16 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
457
497
  if (socket?.connected) {
458
498
  socket.emit("visitor:typing", { isTyping: true });
459
499
  setTimeout(() => {
460
- if (socketRef.current?.connected) {
461
- socketRef.current.emit("visitor:typing", { isTyping: false });
462
- }
500
+ if (socketRef.current?.connected) socketRef.current.emit("visitor:typing", { isTyping: false });
463
501
  }, 2e3);
464
502
  }
465
503
  }, []);
504
+ const statusLabel = (() => {
505
+ if (showOfflineForm) return null;
506
+ if (availability?.mode === "live" && availability.agentsOnline > 0) return { dot: "#22c55e", text: "Online" };
507
+ if (availability?.mode === "ai") return { dot: "#a78bfa", text: "AI Assistant" };
508
+ return { dot: "#9ca3af", text: "We'll respond soon" };
509
+ })();
466
510
  const ChatButton = /* @__PURE__ */ jsx(
467
511
  "button",
468
512
  {
@@ -475,7 +519,7 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
475
519
  width: 60,
476
520
  height: 60,
477
521
  borderRadius: "50%",
478
- backgroundColor: buttonColor,
522
+ backgroundColor: primaryColor,
479
523
  border: "none",
480
524
  display: "flex",
481
525
  alignItems: "center",
@@ -493,395 +537,364 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
493
537
  e.currentTarget.style.transform = "scale(1)";
494
538
  e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.25)";
495
539
  },
496
- children: isOpen ? (
497
- // Close icon (X)
498
- /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
499
- /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
500
- /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
501
- ] })
502
- ) : (
503
- // Chat icon
504
- /* @__PURE__ */ jsx("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ 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" }) })
505
- )
540
+ children: isOpen ? /* @__PURE__ */ jsxs("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
541
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
542
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
543
+ ] }) : /* @__PURE__ */ jsx("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ 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" }) })
506
544
  }
507
545
  );
508
- const ChatPopup = isOpen && /* @__PURE__ */ jsxs(
546
+ const Header = /* @__PURE__ */ jsxs(
509
547
  "div",
510
548
  {
511
549
  style: {
512
- position: "fixed",
513
- [position === "bottom-left" ? "left" : "right"]: 20,
514
- bottom: 90,
515
- width: 380,
516
- maxWidth: "calc(100vw - 40px)",
517
- height: 500,
518
- maxHeight: "calc(100vh - 120px)",
519
- backgroundColor: "#ffffff",
520
- borderRadius: 16,
521
- boxShadow: "0 8px 32px rgba(0, 0, 0, 0.2)",
550
+ padding: "16px 20px",
551
+ background: `linear-gradient(135deg, ${primaryColor}, ${adjustColor(primaryColor, -25)})`,
552
+ color: isLightColor(primaryColor) ? "#1a1a1a" : "white",
522
553
  display: "flex",
523
- flexDirection: "column",
524
- overflow: "hidden",
525
- zIndex: 9998,
526
- animation: "chatSlideUp 0.3s ease-out"
554
+ alignItems: "center",
555
+ gap: 12
527
556
  },
528
557
  children: [
529
- /* @__PURE__ */ jsxs(
558
+ /* @__PURE__ */ jsx(
530
559
  "div",
531
560
  {
532
561
  style: {
533
- padding: "16px 20px",
534
- background: `linear-gradient(135deg, ${buttonColor}, ${adjustColor(buttonColor, -20)})`,
535
- color: "white",
562
+ width: 40,
563
+ height: 40,
564
+ borderRadius: "50%",
565
+ backgroundColor: "rgba(255,255,255,0.2)",
536
566
  display: "flex",
537
567
  alignItems: "center",
538
- gap: 12
568
+ justifyContent: "center",
569
+ overflow: "hidden",
570
+ flexShrink: 0
539
571
  },
540
- children: [
541
- /* @__PURE__ */ jsx(
542
- "div",
543
- {
544
- style: {
545
- width: 40,
546
- height: 40,
547
- borderRadius: "50%",
548
- backgroundColor: "rgba(255,255,255,0.2)",
549
- display: "flex",
550
- alignItems: "center",
551
- justifyContent: "center"
552
- },
553
- children: /* @__PURE__ */ jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ 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" }) })
554
- }
555
- ),
556
- /* @__PURE__ */ jsxs("div", { style: { flex: 1 }, children: [
557
- /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, fontSize: 16 }, children: showOfflineForm ? "Leave a Message" : "Chat with us" }),
558
- /* @__PURE__ */ jsx("div", { style: { fontSize: 13, opacity: 0.9, display: "flex", alignItems: "center", gap: 6 }, children: availability?.mode === "live" && availability.agentsOnline > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
559
- /* @__PURE__ */ jsx("span", { style: { width: 8, height: 8, borderRadius: "50%", backgroundColor: "#22c55e" } }),
560
- availability.agentsOnline,
561
- " online"
562
- ] }) : availability?.mode === "ai" ? "AI-powered support" : "We'll respond soon" })
563
- ] }),
564
- availability?.mode === "offline" && !offlineSubmitted && /* @__PURE__ */ jsx(
565
- "button",
566
- {
567
- onClick: () => setShowOfflineForm((prev) => !prev),
568
- style: {
569
- background: "rgba(255,255,255,0.2)",
570
- border: "none",
571
- borderRadius: 6,
572
- padding: "4px 8px",
573
- color: "white",
574
- fontSize: 12,
575
- cursor: "pointer"
576
- },
577
- children: showOfflineForm ? "Try Chat" : "Leave Message"
578
- }
579
- )
580
- ]
572
+ children: logoUrl ? /* @__PURE__ */ jsx("img", { src: logoUrl, alt: "", style: { width: 28, height: 28, objectFit: "contain", filter: isLightColor(primaryColor) ? "none" : "brightness(0) invert(1)" } }) : /* @__PURE__ */ jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ 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" }) })
581
573
  }
582
574
  ),
583
- showOfflineForm ? /* @__PURE__ */ jsx("div", { style: { padding: 20, flex: 1, overflowY: "auto" }, children: offlineSubmitted ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: 20 }, children: [
584
- /* @__PURE__ */ jsx("div", { style: { fontSize: 48, marginBottom: 16 }, children: "\u2713" }),
585
- /* @__PURE__ */ jsx("h3", { style: { margin: "0 0 8px", fontSize: 18, fontWeight: 600 }, children: "Message Sent!" }),
586
- /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#666", fontSize: 14 }, children: "We'll get back to you as soon as possible." })
587
- ] }) : /* @__PURE__ */ jsxs("form", { onSubmit: handleOfflineSubmit, children: [
588
- /* @__PURE__ */ jsx("p", { style: { margin: "0 0 16px", color: "#666", fontSize: 14 }, children: offlineFormPrompt }),
589
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 12 }, children: /* @__PURE__ */ jsx(
590
- "input",
591
- {
592
- type: "text",
593
- placeholder: "Your name *",
594
- value: offlineForm.name,
595
- onChange: (e) => setOfflineForm((prev) => ({ ...prev, name: e.target.value })),
596
- required: true,
597
- style: {
598
- width: "100%",
599
- padding: "10px 12px",
600
- borderRadius: 8,
601
- border: "1px solid #e5e7eb",
602
- fontSize: 14,
603
- outline: "none",
604
- boxSizing: "border-box"
605
- }
606
- }
607
- ) }),
608
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 12 }, children: /* @__PURE__ */ jsx(
609
- "input",
610
- {
611
- type: "email",
612
- placeholder: "Your email *",
613
- value: offlineForm.email,
614
- onChange: (e) => setOfflineForm((prev) => ({ ...prev, email: e.target.value })),
615
- required: true,
616
- style: {
617
- width: "100%",
618
- padding: "10px 12px",
619
- borderRadius: 8,
620
- border: "1px solid #e5e7eb",
621
- fontSize: 14,
622
- outline: "none",
623
- boxSizing: "border-box"
624
- }
625
- }
626
- ) }),
627
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 12 }, children: /* @__PURE__ */ jsx(
628
- "input",
629
- {
630
- type: "tel",
631
- placeholder: "Phone (optional)",
632
- value: offlineForm.phone,
633
- onChange: (e) => setOfflineForm((prev) => ({ ...prev, phone: e.target.value })),
634
- style: {
635
- width: "100%",
636
- padding: "10px 12px",
637
- borderRadius: 8,
638
- border: "1px solid #e5e7eb",
639
- fontSize: 14,
640
- outline: "none",
641
- boxSizing: "border-box"
642
- }
643
- }
644
- ) }),
645
- /* @__PURE__ */ jsx("div", { style: { marginBottom: 16 }, children: /* @__PURE__ */ jsx(
646
- "textarea",
647
- {
648
- placeholder: "How can we help? *",
649
- value: offlineForm.message,
650
- onChange: (e) => setOfflineForm((prev) => ({ ...prev, message: e.target.value })),
651
- required: true,
652
- rows: 4,
653
- style: {
654
- width: "100%",
655
- padding: "10px 12px",
656
- borderRadius: 8,
657
- border: "1px solid #e5e7eb",
658
- fontSize: 14,
659
- outline: "none",
660
- resize: "vertical",
661
- boxSizing: "border-box"
662
- }
663
- }
664
- ) }),
665
- /* @__PURE__ */ jsx(
666
- "button",
667
- {
668
- type: "submit",
669
- disabled: isLoading,
670
- style: {
671
- width: "100%",
672
- padding: "12px",
673
- borderRadius: 8,
674
- border: "none",
675
- backgroundColor: buttonColor,
676
- color: "white",
677
- fontSize: 14,
678
- fontWeight: 600,
679
- cursor: isLoading ? "wait" : "pointer",
680
- opacity: isLoading ? 0.7 : 1
681
- },
682
- children: isLoading ? "Sending..." : "Send Message"
683
- }
684
- )
685
- ] }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
686
- /* @__PURE__ */ jsxs(
575
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
576
+ /* @__PURE__ */ jsx("div", { style: { fontWeight: 600, fontSize: 16, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: showOfflineForm ? offlineHeading : businessName }),
577
+ statusLabel && /* @__PURE__ */ jsxs("div", { style: { fontSize: 13, opacity: 0.9, display: "flex", alignItems: "center", gap: 6, marginTop: 2 }, children: [
578
+ /* @__PURE__ */ jsx("span", { style: { width: 8, height: 8, borderRadius: "50%", backgroundColor: statusLabel.dot, flexShrink: 0 } }),
579
+ statusLabel.text
580
+ ] })
581
+ ] }),
582
+ connectionStatus === "connecting" && !showWelcome && !showOfflineForm && /* @__PURE__ */ jsxs("div", { style: { fontSize: 11, opacity: 0.7, display: "flex", alignItems: "center", gap: 4 }, children: [
583
+ /* @__PURE__ */ jsx("span", { style: { animation: "chatDot 1s infinite ease-in-out" }, children: "\u25CF" }),
584
+ "Connecting"
585
+ ] })
586
+ ]
587
+ }
588
+ );
589
+ const WelcomeScreen = /* @__PURE__ */ jsxs("div", { style: { flex: 1, display: "flex", flexDirection: "column", padding: 20, gap: 16 }, children: [
590
+ /* @__PURE__ */ jsx("div", { style: { textAlign: "center", paddingTop: 8 }, children: /* @__PURE__ */ jsx("div", { style: { fontSize: 15, color: "#374151", lineHeight: 1.5 }, children: welcomeMessage }) }),
591
+ quickActions.length > 0 && /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: quickActions.map((action, i) => /* @__PURE__ */ jsx(
592
+ "button",
593
+ {
594
+ type: "button",
595
+ onClick: () => startChat(action),
596
+ style: {
597
+ padding: "12px 16px",
598
+ borderRadius: 12,
599
+ border: `1px solid ${primaryColor}33`,
600
+ backgroundColor: `${primaryColor}08`,
601
+ color: primaryColor,
602
+ fontSize: 14,
603
+ cursor: "pointer",
604
+ textAlign: "left",
605
+ transition: "background-color 0.15s, border-color 0.15s",
606
+ lineHeight: 1.4
607
+ },
608
+ onMouseEnter: (e) => {
609
+ e.currentTarget.style.backgroundColor = `${primaryColor}15`;
610
+ e.currentTarget.style.borderColor = `${primaryColor}55`;
611
+ },
612
+ onMouseLeave: (e) => {
613
+ e.currentTarget.style.backgroundColor = `${primaryColor}08`;
614
+ e.currentTarget.style.borderColor = `${primaryColor}33`;
615
+ },
616
+ children: action
617
+ },
618
+ i
619
+ )) }),
620
+ /* @__PURE__ */ jsx(
621
+ "button",
622
+ {
623
+ type: "button",
624
+ onClick: () => startChat(),
625
+ style: {
626
+ padding: "12px 20px",
627
+ borderRadius: 12,
628
+ border: "none",
629
+ backgroundColor: primaryColor,
630
+ color: isLightColor(primaryColor) ? "#1a1a1a" : "white",
631
+ fontSize: 14,
632
+ fontWeight: 600,
633
+ cursor: "pointer",
634
+ marginTop: "auto"
635
+ },
636
+ children: "Start a conversation"
637
+ }
638
+ ),
639
+ widgetConfig?.business_info?.phone && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", fontSize: 13, color: "#6b7280" }, children: [
640
+ "Or call us at",
641
+ " ",
642
+ /* @__PURE__ */ jsx("a", { href: `tel:${widgetConfig.business_info.phone}`, style: { color: primaryColor, textDecoration: "none", fontWeight: 500 }, children: widgetConfig.business_info.phone })
643
+ ] })
644
+ ] });
645
+ const OfflineFormView = /* @__PURE__ */ jsx("div", { style: { padding: 20, flex: 1, overflowY: "auto" }, children: offlineSubmitted ? /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: 20 }, children: [
646
+ /* @__PURE__ */ jsx("div", { style: { width: 56, height: 56, borderRadius: "50%", backgroundColor: `${primaryColor}15`, display: "flex", alignItems: "center", justifyContent: "center", margin: "0 auto 16px" }, children: /* @__PURE__ */ jsx("svg", { width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: primaryColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polyline", { points: "20 6 9 17 4 12" }) }) }),
647
+ /* @__PURE__ */ jsx("h3", { style: { margin: "0 0 8px", fontSize: 18, fontWeight: 600, color: "#111827" }, children: "Message Sent!" }),
648
+ /* @__PURE__ */ jsx("p", { style: { margin: 0, color: "#6b7280", fontSize: 14 }, children: offlineSubheading })
649
+ ] }) : /* @__PURE__ */ jsxs("form", { onSubmit: handleOfflineSubmit, children: [
650
+ /* @__PURE__ */ jsx("p", { style: { margin: "0 0 16px", color: "#6b7280", fontSize: 14 }, children: offlineSubheading }),
651
+ [
652
+ { name: "name", type: "text", placeholder: "Your name *", required: true },
653
+ { name: "email", type: "email", placeholder: "Your email *", required: true },
654
+ { name: "phone", type: "tel", placeholder: "Phone (optional)", required: false }
655
+ ].map((field) => /* @__PURE__ */ jsx("div", { style: { marginBottom: 12 }, children: /* @__PURE__ */ jsx(
656
+ "input",
657
+ {
658
+ type: field.type,
659
+ placeholder: field.placeholder,
660
+ value: offlineForm[field.name],
661
+ onChange: (e) => setOfflineForm((prev) => ({ ...prev, [field.name]: e.target.value })),
662
+ required: field.required,
663
+ style: { width: "100%", padding: "10px 12px", borderRadius: 8, border: "1px solid #e5e7eb", fontSize: 14, outline: "none", boxSizing: "border-box" },
664
+ onFocus: (e) => e.currentTarget.style.borderColor = primaryColor,
665
+ onBlur: (e) => e.currentTarget.style.borderColor = "#e5e7eb"
666
+ }
667
+ ) }, field.name)),
668
+ /* @__PURE__ */ jsx("div", { style: { marginBottom: 16 }, children: /* @__PURE__ */ jsx(
669
+ "textarea",
670
+ {
671
+ placeholder: "How can we help? *",
672
+ value: offlineForm.message,
673
+ onChange: (e) => setOfflineForm((prev) => ({ ...prev, message: e.target.value })),
674
+ required: true,
675
+ rows: 4,
676
+ style: { width: "100%", padding: "10px 12px", borderRadius: 8, border: "1px solid #e5e7eb", fontSize: 14, outline: "none", resize: "vertical", boxSizing: "border-box" },
677
+ onFocus: (e) => e.currentTarget.style.borderColor = primaryColor,
678
+ onBlur: (e) => e.currentTarget.style.borderColor = "#e5e7eb"
679
+ }
680
+ ) }),
681
+ /* @__PURE__ */ jsx(
682
+ "button",
683
+ {
684
+ type: "submit",
685
+ disabled: isLoading,
686
+ style: {
687
+ width: "100%",
688
+ padding: "12px",
689
+ borderRadius: 8,
690
+ border: "none",
691
+ backgroundColor: primaryColor,
692
+ color: isLightColor(primaryColor) ? "#1a1a1a" : "white",
693
+ fontSize: 14,
694
+ fontWeight: 600,
695
+ cursor: isLoading ? "wait" : "pointer",
696
+ opacity: isLoading ? 0.7 : 1
697
+ },
698
+ children: isLoading ? "Sending..." : "Send Message"
699
+ }
700
+ )
701
+ ] }) });
702
+ const MessagesView = /* @__PURE__ */ jsxs(Fragment, { children: [
703
+ /* @__PURE__ */ jsxs(
704
+ "div",
705
+ {
706
+ style: { flex: 1, overflowY: "auto", padding: 16, display: "flex", flexDirection: "column", gap: 12, backgroundColor: "#f9fafb" },
707
+ children: [
708
+ messages.map((message) => /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: message.role === "user" ? "flex-end" : "flex-start" }, children: /* @__PURE__ */ jsxs(
687
709
  "div",
688
710
  {
689
711
  style: {
690
- flex: 1,
691
- overflowY: "auto",
692
- padding: 16,
693
- display: "flex",
694
- flexDirection: "column",
695
- gap: 12,
696
- backgroundColor: "#f8f9fa"
712
+ maxWidth: "80%",
713
+ padding: message.role === "system" ? "8px 12px" : "10px 14px",
714
+ borderRadius: message.role === "user" ? "16px 16px 4px 16px" : message.role === "system" ? "8px" : "16px 16px 16px 4px",
715
+ backgroundColor: message.role === "user" ? primaryColor : message.role === "system" ? "#e5e7eb" : "#ffffff",
716
+ color: message.role === "user" ? isLightColor(primaryColor) ? "#1a1a1a" : "white" : message.role === "system" ? "#6b7280" : "#111827",
717
+ boxShadow: message.role === "system" ? "none" : "0 1px 2px rgba(0,0,0,0.08)",
718
+ fontSize: message.role === "system" ? 13 : 14,
719
+ fontStyle: message.role === "system" ? "italic" : "normal",
720
+ lineHeight: 1.5,
721
+ whiteSpace: "pre-wrap",
722
+ wordBreak: "break-word"
697
723
  },
698
724
  children: [
699
- messages.map((message) => /* @__PURE__ */ jsx(
700
- "div",
725
+ message.agentName && message.role === "agent" && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, opacity: 0.6, marginBottom: 4 }, children: message.agentName }),
726
+ message.content,
727
+ message.attachments?.length ? /* @__PURE__ */ jsx("div", { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 6 }, children: message.attachments.map(
728
+ (att, i) => att.mimeType?.startsWith("image/") ? /* @__PURE__ */ jsx("a", { href: att.url, target: "_blank", rel: "noopener noreferrer", style: { display: "block" }, children: /* @__PURE__ */ jsx("img", { src: att.url, alt: att.name, style: { maxWidth: "100%", maxHeight: 200, borderRadius: 8, objectFit: "contain" } }) }, i) : /* @__PURE__ */ jsxs("a", { href: att.url, target: "_blank", rel: "noopener noreferrer", style: { fontSize: 13, wordBreak: "break-all" }, children: [
729
+ "\u{1F4CE} ",
730
+ att.name
731
+ ] }, i)
732
+ ) }) : null,
733
+ message.suggestions?.length ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 8 }, children: message.suggestions.map((s, i) => /* @__PURE__ */ jsx(
734
+ "button",
701
735
  {
736
+ type: "button",
737
+ onClick: () => {
738
+ setInputValue(s);
739
+ inputRef.current?.focus();
740
+ },
702
741
  style: {
703
- display: "flex",
704
- justifyContent: message.role === "user" ? "flex-end" : "flex-start"
742
+ padding: "6px 12px",
743
+ borderRadius: 16,
744
+ border: `1px solid ${secondaryColor}`,
745
+ backgroundColor: `${secondaryColor}10`,
746
+ color: secondaryColor,
747
+ fontSize: 13,
748
+ cursor: "pointer"
705
749
  },
706
- children: /* @__PURE__ */ jsxs(
707
- "div",
708
- {
709
- style: {
710
- maxWidth: "80%",
711
- padding: message.role === "system" ? "8px 12px" : "10px 14px",
712
- borderRadius: message.role === "user" ? "16px 16px 4px 16px" : message.role === "system" ? "8px" : "16px 16px 16px 4px",
713
- backgroundColor: message.role === "user" ? buttonColor : message.role === "system" ? "#e5e7eb" : "#ffffff",
714
- color: message.role === "user" ? "white" : message.role === "system" ? "#666" : "#1a1a1a",
715
- boxShadow: message.role === "system" ? "none" : "0 1px 2px rgba(0,0,0,0.1)",
716
- fontSize: message.role === "system" ? 13 : 14,
717
- fontStyle: message.role === "system" ? "italic" : "normal",
718
- lineHeight: 1.5,
719
- whiteSpace: "pre-wrap",
720
- wordBreak: "break-word"
721
- },
722
- children: [
723
- message.agentName && message.role === "agent" && /* @__PURE__ */ jsx("div", { style: { fontSize: 12, opacity: 0.7, marginBottom: 4 }, children: message.agentName }),
724
- message.content,
725
- message.attachments?.length ? /* @__PURE__ */ jsx("div", { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 6 }, children: message.attachments.map((att, i) => att.mimeType?.startsWith("image/") ? /* @__PURE__ */ jsx("a", { href: att.url, target: "_blank", rel: "noopener noreferrer", style: { display: "block" }, children: /* @__PURE__ */ jsx("img", { src: att.url, alt: att.name, style: { maxWidth: "100%", maxHeight: 200, borderRadius: 8, objectFit: "contain" } }) }, i) : /* @__PURE__ */ jsxs("a", { href: att.url, target: "_blank", rel: "noopener noreferrer", style: { fontSize: 13, wordBreak: "break-all" }, children: [
726
- "\u{1F4CE} ",
727
- att.name
728
- ] }, i)) }) : null,
729
- message.suggestions?.length ? /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginTop: 8 }, children: message.suggestions.map((s, i) => /* @__PURE__ */ jsx(
730
- "button",
731
- {
732
- type: "button",
733
- onClick: () => {
734
- setInputValue(s);
735
- inputRef.current?.focus();
736
- },
737
- style: {
738
- padding: "6px 12px",
739
- borderRadius: 16,
740
- border: `1px solid ${buttonColor}`,
741
- backgroundColor: "rgba(255,255,255,0.9)",
742
- color: buttonColor,
743
- fontSize: 13,
744
- cursor: "pointer"
745
- },
746
- children: s
747
- },
748
- i
749
- )) }) : null,
750
- message.sendFailed && lastFailedSend && /* @__PURE__ */ jsx("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsx(
751
- "button",
752
- {
753
- type: "button",
754
- onClick: retryFailedSend,
755
- style: {
756
- padding: "6px 12px",
757
- borderRadius: 6,
758
- border: "1px solid #ef4444",
759
- backgroundColor: "#fef2f2",
760
- color: "#dc2626",
761
- fontSize: 13,
762
- cursor: "pointer"
763
- },
764
- children: "Retry send"
765
- }
766
- ) }),
767
- message.content.includes("speak with") && /* @__PURE__ */ jsx(
768
- "button",
769
- {
770
- onClick: requestHandoff,
771
- style: {
772
- display: "block",
773
- marginTop: 8,
774
- padding: "6px 12px",
775
- borderRadius: 6,
776
- border: `1px solid ${buttonColor}`,
777
- backgroundColor: "transparent",
778
- color: buttonColor,
779
- fontSize: 13,
780
- cursor: "pointer"
781
- },
782
- children: "Talk to a person"
783
- }
784
- )
785
- ]
786
- }
787
- )
750
+ children: s
788
751
  },
789
- message.id
790
- )),
791
- (isLoading || agentTyping) && /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "flex-start" }, children: /* @__PURE__ */ jsxs(
792
- "div",
752
+ i
753
+ )) }) : null,
754
+ message.sendFailed && lastFailedSend && /* @__PURE__ */ jsx("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsx(
755
+ "button",
756
+ {
757
+ type: "button",
758
+ onClick: retryFailedSend,
759
+ style: { padding: "6px 12px", borderRadius: 6, border: "1px solid #ef4444", backgroundColor: "#fef2f2", color: "#dc2626", fontSize: 13, cursor: "pointer" },
760
+ children: "Retry send"
761
+ }
762
+ ) }),
763
+ message.role === "assistant" && widgetConfig?.handoff_enabled !== false && messages.filter((m) => m.role === "user").length >= 2 && message.id === messages.filter((m) => m.role === "assistant").slice(-1)[0]?.id && /* @__PURE__ */ jsx(
764
+ "button",
793
765
  {
766
+ onClick: requestHandoff,
794
767
  style: {
795
- padding: "10px 14px",
796
- borderRadius: "16px 16px 16px 4px",
797
- backgroundColor: "#ffffff",
798
- boxShadow: "0 1px 2px rgba(0,0,0,0.1)",
799
- display: "flex",
800
- gap: 4
768
+ display: "inline-block",
769
+ marginTop: 8,
770
+ padding: "6px 12px",
771
+ borderRadius: 6,
772
+ border: `1px solid ${secondaryColor}`,
773
+ backgroundColor: "transparent",
774
+ color: secondaryColor,
775
+ fontSize: 13,
776
+ cursor: "pointer"
801
777
  },
802
- children: [
803
- /* @__PURE__ */ jsx("span", { style: { animation: "chatDot 1.4s infinite ease-in-out", animationDelay: "0s" }, children: "\u25CF" }),
804
- /* @__PURE__ */ jsx("span", { style: { animation: "chatDot 1.4s infinite ease-in-out", animationDelay: "0.2s" }, children: "\u25CF" }),
805
- /* @__PURE__ */ jsx("span", { style: { animation: "chatDot 1.4s infinite ease-in-out", animationDelay: "0.4s" }, children: "\u25CF" })
806
- ]
778
+ children: "Talk to a person"
807
779
  }
808
- ) }),
809
- /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
780
+ )
810
781
  ]
811
782
  }
812
- ),
813
- lastFailedSend && /* @__PURE__ */ jsxs("div", { style: { padding: "8px 12px", backgroundColor: "#fef2f2", borderTop: "1px solid #fecaca", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }, children: [
814
- /* @__PURE__ */ jsx("span", { style: { fontSize: 13, color: "#dc2626" }, children: "Failed to send" }),
815
- /* @__PURE__ */ jsx("button", { type: "button", onClick: retryFailedSend, style: { padding: "4px 10px", borderRadius: 6, border: "1px solid #dc2626", background: "#fff", color: "#dc2626", fontSize: 12, cursor: "pointer" }, children: "Retry" })
816
- ] }),
817
- /* @__PURE__ */ jsx("form", { onSubmit: handleSubmit, style: { padding: 12, borderTop: "1px solid #e5e7eb", backgroundColor: "#ffffff" }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
818
- /* @__PURE__ */ jsx(
819
- "input",
820
- {
821
- ref: inputRef,
822
- type: "text",
823
- value: inputValue,
824
- onChange: (e) => {
825
- setInputValue(e.target.value);
826
- handleTyping();
827
- },
828
- onKeyDown: handleKeyDown,
829
- placeholder: "Type a message...",
830
- disabled: isLoading,
831
- style: {
832
- flex: 1,
833
- padding: "10px 14px",
834
- borderRadius: 24,
835
- border: "1px solid #e5e7eb",
836
- fontSize: 14,
837
- outline: "none",
838
- transition: "border-color 0.2s"
839
- },
840
- onFocus: (e) => e.currentTarget.style.borderColor = buttonColor,
841
- onBlur: (e) => e.currentTarget.style.borderColor = "#e5e7eb"
842
- }
843
- ),
844
- /* @__PURE__ */ jsx(
845
- "button",
846
- {
847
- type: "submit",
848
- disabled: !inputValue.trim() || isLoading,
849
- style: {
850
- width: 40,
851
- height: 40,
852
- borderRadius: "50%",
853
- border: "none",
854
- backgroundColor: (inputValue.trim() || pendingFiles.length) && !isLoading ? buttonColor : "#e5e7eb",
855
- color: "white",
856
- cursor: inputValue.trim() && !isLoading ? "pointer" : "not-allowed",
857
- display: "flex",
858
- alignItems: "center",
859
- justifyContent: "center",
860
- transition: "background-color 0.2s"
861
- },
862
- children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
863
- }
864
- )
865
- ] }) })
783
+ ) }, message.id)),
784
+ (isLoading || agentTyping) && /* @__PURE__ */ jsx("div", { style: { display: "flex", justifyContent: "flex-start" }, children: /* @__PURE__ */ jsxs(
785
+ "div",
786
+ {
787
+ style: {
788
+ padding: "10px 14px",
789
+ borderRadius: "16px 16px 16px 4px",
790
+ backgroundColor: "#ffffff",
791
+ boxShadow: "0 1px 2px rgba(0,0,0,0.08)",
792
+ display: "flex",
793
+ gap: 4,
794
+ color: "#9ca3af"
795
+ },
796
+ children: [
797
+ /* @__PURE__ */ jsx("span", { style: { animation: "chatDot 1.4s infinite ease-in-out", animationDelay: "0s" }, children: "\u25CF" }),
798
+ /* @__PURE__ */ jsx("span", { style: { animation: "chatDot 1.4s infinite ease-in-out", animationDelay: "0.2s" }, children: "\u25CF" }),
799
+ /* @__PURE__ */ jsx("span", { style: { animation: "chatDot 1.4s infinite ease-in-out", animationDelay: "0.4s" }, children: "\u25CF" })
800
+ ]
801
+ }
802
+ ) }),
803
+ /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
804
+ ]
805
+ }
806
+ ),
807
+ lastFailedSend && /* @__PURE__ */ jsxs("div", { style: { padding: "8px 12px", backgroundColor: "#fef2f2", borderTop: "1px solid #fecaca", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }, children: [
808
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 13, color: "#dc2626" }, children: "Failed to send" }),
809
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: retryFailedSend, style: { padding: "4px 10px", borderRadius: 6, border: "1px solid #dc2626", background: "#fff", color: "#dc2626", fontSize: 12, cursor: "pointer" }, children: "Retry" })
810
+ ] }),
811
+ /* @__PURE__ */ jsx("form", { onSubmit: handleSubmit, style: { padding: 12, borderTop: "1px solid #e5e7eb", backgroundColor: "#ffffff" }, children: /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
812
+ /* @__PURE__ */ jsx(
813
+ "input",
814
+ {
815
+ ref: inputRef,
816
+ type: "text",
817
+ value: inputValue,
818
+ onChange: (e) => {
819
+ setInputValue(e.target.value);
820
+ handleTyping();
821
+ },
822
+ onKeyDown: handleKeyDown,
823
+ placeholder: "Type a message...",
824
+ disabled: isLoading,
825
+ style: {
826
+ flex: 1,
827
+ padding: "10px 14px",
828
+ borderRadius: 24,
829
+ border: "1px solid #e5e7eb",
830
+ fontSize: 14,
831
+ outline: "none",
832
+ transition: "border-color 0.2s"
833
+ },
834
+ onFocus: (e) => e.currentTarget.style.borderColor = primaryColor,
835
+ onBlur: (e) => e.currentTarget.style.borderColor = "#e5e7eb"
836
+ }
837
+ ),
838
+ /* @__PURE__ */ jsx(
839
+ "button",
840
+ {
841
+ type: "submit",
842
+ disabled: !inputValue.trim() || isLoading,
843
+ style: {
844
+ width: 40,
845
+ height: 40,
846
+ borderRadius: "50%",
847
+ border: "none",
848
+ backgroundColor: (inputValue.trim() || pendingFiles.length) && !isLoading ? primaryColor : "#e5e7eb",
849
+ color: (inputValue.trim() || pendingFiles.length) && !isLoading && isLightColor(primaryColor) ? "#1a1a1a" : "white",
850
+ cursor: inputValue.trim() && !isLoading ? "pointer" : "not-allowed",
851
+ display: "flex",
852
+ alignItems: "center",
853
+ justifyContent: "center",
854
+ transition: "background-color 0.2s",
855
+ flexShrink: 0
856
+ },
857
+ children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" }) })
858
+ }
859
+ )
860
+ ] }) })
861
+ ] });
862
+ const ChatPopup = isOpen && /* @__PURE__ */ jsxs(
863
+ "div",
864
+ {
865
+ style: {
866
+ position: "fixed",
867
+ [position === "bottom-left" ? "left" : "right"]: 20,
868
+ bottom: 90,
869
+ width: 380,
870
+ maxWidth: "calc(100vw - 40px)",
871
+ height: 520,
872
+ maxHeight: "calc(100vh - 120px)",
873
+ backgroundColor: "#ffffff",
874
+ borderRadius: 16,
875
+ boxShadow: "0 8px 32px rgba(0, 0, 0, 0.2)",
876
+ display: "flex",
877
+ flexDirection: "column",
878
+ overflow: "hidden",
879
+ zIndex: 9998,
880
+ animation: "chatSlideUp 0.3s ease-out"
881
+ },
882
+ children: [
883
+ Header,
884
+ showOfflineForm ? OfflineFormView : showWelcome && welcomeEnabled && messages.length === 0 ? WelcomeScreen : MessagesView,
885
+ showPoweredBy && /* @__PURE__ */ jsxs("div", { style: { padding: "6px 0", textAlign: "center", fontSize: 11, color: "#9ca3af", backgroundColor: "#ffffff", borderTop: "1px solid #f3f4f6" }, children: [
886
+ "Powered by",
887
+ " ",
888
+ /* @__PURE__ */ jsx("a", { href: "https://uptrademedia.com", target: "_blank", rel: "noopener noreferrer", style: { color: "#6b7280", textDecoration: "none", fontWeight: 500 }, children: "Uptrade" })
866
889
  ] }),
867
890
  /* @__PURE__ */ jsx("style", { children: `
868
891
  @keyframes chatSlideUp {
869
- from {
870
- opacity: 0;
871
- transform: translateY(20px);
872
- }
873
- to {
874
- opacity: 1;
875
- transform: translateY(0);
876
- }
892
+ from { opacity: 0; transform: translateY(20px); }
893
+ to { opacity: 1; transform: translateY(0); }
877
894
  }
878
895
  @keyframes chatDot {
879
- 0%, 80%, 100% {
880
- opacity: 0.3;
881
- }
882
- 40% {
883
- opacity: 1;
884
- }
896
+ 0%, 80%, 100% { opacity: 0.3; }
897
+ 40% { opacity: 1; }
885
898
  }
886
899
  ` })
887
900
  ]
@@ -892,13 +905,6 @@ function ChatWidget({ projectId, config, apiUrl: propApiUrl }) {
892
905
  ChatButton
893
906
  ] });
894
907
  }
895
- function adjustColor(hex, amount) {
896
- const num = parseInt(hex.replace("#", ""), 16);
897
- const r = Math.min(255, Math.max(0, (num >> 16) + amount));
898
- const g = Math.min(255, Math.max(0, (num >> 8 & 255) + amount));
899
- const b = Math.min(255, Math.max(0, (num & 255) + amount));
900
- return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
901
- }
902
908
  function handleAction(action, node, onClose, onAction) {
903
909
  if (!action?.action || action.action === "none") return;
904
910
  if (onAction) {
@@ -1514,5 +1520,5 @@ function getDeviceType() {
1514
1520
  }
1515
1521
 
1516
1522
  export { ChatWidget, DesignRenderer, EngageWidget };
1517
- //# sourceMappingURL=chunk-DYM5ML2V.mjs.map
1518
- //# sourceMappingURL=chunk-DYM5ML2V.mjs.map
1523
+ //# sourceMappingURL=chunk-XIF2CBLV.mjs.map
1524
+ //# sourceMappingURL=chunk-XIF2CBLV.mjs.map