@uptrademedia/site-kit 1.0.16 → 1.0.18

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