@eshal-bot/chat-widget 0.1.20 → 0.1.22

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.
@@ -1292,6 +1292,197 @@
1292
1292
  return (_ONBOARDING_FIELD_ORD = ONBOARDING_FIELD_ORDER[key]) !== null && _ONBOARDING_FIELD_ORD !== void 0 ? _ONBOARDING_FIELD_ORD : 99;
1293
1293
  };
1294
1294
 
1295
+ const createSession = async config => {
1296
+ // Initialize chat session if needed
1297
+ if (config.sessionUrl) {
1298
+ try {
1299
+ const response = await fetch(config.sessionUrl, {
1300
+ method: "POST",
1301
+ headers: _objectSpread2({
1302
+ "Content-Type": "application/json"
1303
+ }, config.apiKey && {
1304
+ Authorization: "Bearer ".concat(config.apiKey)
1305
+ }),
1306
+ body: JSON.stringify({
1307
+ userId: config.userId,
1308
+ userName: config.userName,
1309
+ userEmail: config.userEmail
1310
+ }),
1311
+ credentials: "include"
1312
+ });
1313
+ const data = await response.json();
1314
+ return data.sessionId;
1315
+ } catch (error) {
1316
+ console.error("Session creation error:", error);
1317
+ return null;
1318
+ }
1319
+ }
1320
+ return null;
1321
+ };
1322
+
1323
+ /**
1324
+ * Fetches conversation history for a given org and conversation
1325
+ * @param {string} apiBaseUrl - Base URL for the API
1326
+ * @param {string} orgId - Organization ID
1327
+ * @param {string} conversationId - Conversation ID
1328
+ * @returns {Promise<Array>} Array of messages
1329
+ */
1330
+ const fetchConversationHistory = async (apiBaseUrl, orgId, conversationId) => {
1331
+ if (!apiBaseUrl || !orgId || !conversationId) {
1332
+ throw new Error("apiBaseUrl, orgId, and conversationId are required");
1333
+ }
1334
+ const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/conversations/").concat(orgId, "/").concat(conversationId);
1335
+ const response = await fetch(url, {
1336
+ method: "GET",
1337
+ headers: {
1338
+ "Content-Type": "application/json"
1339
+ },
1340
+ credentials: "include"
1341
+ });
1342
+ if (response.status === 404) {
1343
+ return []; // No history for this conversation yet
1344
+ }
1345
+ if (!response.ok) {
1346
+ throw new Error("HTTP error! status: ".concat(response.status));
1347
+ }
1348
+ const data = await response.json();
1349
+ return data.messages || [];
1350
+ };
1351
+
1352
+ /**
1353
+ * Fetches agent configuration from the deploy-agent endpoint
1354
+ * @param {string} apiBaseUrl - Base URL for the API
1355
+ * @param {string} orgId - Organization ID
1356
+ * @returns {Promise<object>} Agent configuration object
1357
+ */
1358
+ const fetchAgentConfig = async (apiBaseUrl, orgId) => {
1359
+ if (!apiBaseUrl || !orgId) {
1360
+ throw new Error("apiBaseUrl and orgId are required");
1361
+ }
1362
+ try {
1363
+ const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/deploy-agent/").concat(orgId);
1364
+ const response = await fetch(url, {
1365
+ method: "GET",
1366
+ headers: {
1367
+ "Content-Type": "application/json"
1368
+ },
1369
+ credentials: "include"
1370
+ });
1371
+ if (!response.ok) {
1372
+ throw new Error("HTTP error! status: ".concat(response.status));
1373
+ }
1374
+ const data = await response.json();
1375
+ return data;
1376
+ } catch (error) {
1377
+ console.error("Failed to fetch agent configuration:", error);
1378
+ throw error;
1379
+ }
1380
+ };
1381
+
1382
+ const SESSION_KEY = 'eshal_chat_session';
1383
+ const getTimeoutMs = (value, unit) => {
1384
+ if (value === undefined || value === null || !unit) return null;
1385
+ const multipliers = {
1386
+ MINUTES: 60 * 1000,
1387
+ HOURS: 60 * 60 * 1000,
1388
+ DAYS: 24 * 60 * 60 * 1000
1389
+ };
1390
+ const upperUnit = unit.toUpperCase();
1391
+ const multiplier = multipliers[upperUnit];
1392
+ if (multiplier === undefined) {
1393
+ console.warn("[Session] Unknown timeout unit: ".concat(unit));
1394
+ return null;
1395
+ }
1396
+ return Number(value) * multiplier;
1397
+ };
1398
+ const getSession = orgId => {
1399
+ try {
1400
+ const raw = localStorage.getItem("".concat(SESSION_KEY, "_").concat(orgId));
1401
+ if (!raw) return null;
1402
+ return JSON.parse(raw);
1403
+ } catch (_unused) {
1404
+ return null;
1405
+ }
1406
+ };
1407
+ const isSessionValid = (session, timeoutValue, timeoutUnit) => {
1408
+ if (!session || !session.lastActivity) return false;
1409
+
1410
+ // If no timeout is configured, session is always valid
1411
+ if (timeoutValue === undefined || timeoutValue === null || timeoutValue === 0) {
1412
+ return true;
1413
+ }
1414
+ const timeoutMs = getTimeoutMs(timeoutValue, timeoutUnit);
1415
+ if (!timeoutMs) return true; // Default to valid if unit is missing but value was provided (to avoid accidental wipes)
1416
+
1417
+ const lastActivity = new Date(session.lastActivity).getTime();
1418
+ const elapsed = Date.now() - lastActivity;
1419
+ const isValid = elapsed < timeoutMs;
1420
+ if (!isValid) {
1421
+ console.log("[Session] Session expired. Elapsed: ".concat(Math.floor(elapsed / 1000), "s, Timeout: ").concat(Math.floor(timeoutMs / 1000), "s"));
1422
+ }
1423
+ return isValid;
1424
+ };
1425
+ const saveSession = function (orgId, conversationId) {
1426
+ let extra = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
1427
+ try {
1428
+ const now = new Date().toISOString();
1429
+ console.log("[Session] Saving new session: ".concat(conversationId, " for org: ").concat(orgId));
1430
+ localStorage.setItem("".concat(SESSION_KEY, "_").concat(orgId), JSON.stringify(_objectSpread2({
1431
+ conversationId,
1432
+ lastActivity: now,
1433
+ createdAt: now
1434
+ }, extra)));
1435
+ } catch (_unused2) {}
1436
+ };
1437
+ const updateActivity = orgId => {
1438
+ try {
1439
+ const session = getSession(orgId);
1440
+ if (!session) return;
1441
+ session.lastActivity = new Date().toISOString();
1442
+ localStorage.setItem("".concat(SESSION_KEY, "_").concat(orgId), JSON.stringify(session));
1443
+ } catch (_unused3) {}
1444
+ };
1445
+ const markOnboardingCompleted = orgId => {
1446
+ try {
1447
+ const session = getSession(orgId);
1448
+ if (!session) return;
1449
+ session.onboardingCompleted = true;
1450
+ localStorage.setItem("".concat(SESSION_KEY, "_").concat(orgId), JSON.stringify(session));
1451
+ } catch (_unused4) {}
1452
+ };
1453
+ const markCsatSubmitted = orgId => {
1454
+ try {
1455
+ const session = getSession(orgId);
1456
+ if (!session) return;
1457
+ session.csatSubmitted = true;
1458
+ localStorage.setItem("".concat(SESSION_KEY, "_").concat(orgId), JSON.stringify(session));
1459
+ } catch (_unused5) {}
1460
+ };
1461
+ const clearSession = orgId => {
1462
+ try {
1463
+ localStorage.removeItem("".concat(SESSION_KEY, "_").concat(orgId));
1464
+ } catch (_unused6) {}
1465
+ };
1466
+ const savePromptSuggestions = (orgId, prompts) => {
1467
+ try {
1468
+ if (!orgId) return;
1469
+ if (!Array.isArray(prompts) || prompts.length === 0) {
1470
+ localStorage.removeItem("".concat(SESSION_KEY, "_prompts_").concat(orgId));
1471
+ return;
1472
+ }
1473
+ localStorage.setItem("".concat(SESSION_KEY, "_prompts_").concat(orgId), JSON.stringify(prompts));
1474
+ } catch (_unused7) {}
1475
+ };
1476
+ const getPromptSuggestions = orgId => {
1477
+ try {
1478
+ const raw = localStorage.getItem("".concat(SESSION_KEY, "_prompts_").concat(orgId));
1479
+ if (!raw) return [];
1480
+ return JSON.parse(raw) || [];
1481
+ } catch (_unused8) {
1482
+ return [];
1483
+ }
1484
+ };
1485
+
1295
1486
  const createMessage = _ref => {
1296
1487
  let {
1297
1488
  id,
@@ -1317,51 +1508,6 @@
1317
1508
  }
1318
1509
  return [];
1319
1510
  };
1320
- const validateOnboardingAnswer = (question, answer) => {
1321
- var _question$fieldType;
1322
- if (!answer.trim()) {
1323
- return {
1324
- isValid: false,
1325
- errorMessage: "This field is required."
1326
- };
1327
- }
1328
- const fieldType = (_question$fieldType = question.fieldType) === null || _question$fieldType === void 0 ? void 0 : _question$fieldType.toLowerCase();
1329
-
1330
- // Email validation
1331
- if (fieldType === "email") {
1332
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1333
- if (!emailRegex.test(answer)) {
1334
- return {
1335
- isValid: false,
1336
- errorMessage: "Please enter a valid email address."
1337
- };
1338
- }
1339
- }
1340
-
1341
- // Phone validation
1342
- if (fieldType === "phone") {
1343
- const phoneRegex = /^[+]?[\d\s-]{10,15}$/;
1344
- if (!phoneRegex.test(answer.replace(/\s/g, ""))) {
1345
- return {
1346
- isValid: false,
1347
- errorMessage: "Please enter a valid phone number."
1348
- };
1349
- }
1350
- }
1351
-
1352
- // Name validation
1353
- if (fieldType === "fullname") {
1354
- if (answer.length > 50) {
1355
- return {
1356
- isValid: false,
1357
- errorMessage: "Name must be less than 50 characters."
1358
- };
1359
- }
1360
- }
1361
- return {
1362
- isValid: true
1363
- };
1364
- };
1365
1511
  const useChatState = _ref2 => {
1366
1512
  let {
1367
1513
  welcomeMessage,
@@ -1375,12 +1521,22 @@
1375
1521
  enableVoiceInteraction = false,
1376
1522
  onboardingQuestions = [],
1377
1523
  onboardingEnabled = false,
1378
- collectionPrompt
1524
+ collectionPrompt,
1525
+ inactivityTimeoutValue,
1526
+ inactivityTimeoutUnit
1379
1527
  } = _ref2;
1380
1528
  const [isOpen, setIsOpen] = reactExports.useState(autoOpen);
1381
1529
  const [isMinimized, setIsMinimized] = reactExports.useState(false);
1382
1530
  const [isDark, setIsDark] = reactExports.useState(darkMode);
1383
1531
  const [messages, setMessages] = reactExports.useState(() => {
1532
+ // If a valid session exists, start empty — history will be loaded by the session effect
1533
+ try {
1534
+ const existing = getSession(organizationId);
1535
+ // Only start empty if we explicitly know there's a valid session to restore
1536
+ if (inactivityTimeoutValue !== undefined && existing && isSessionValid(existing, inactivityTimeoutValue, inactivityTimeoutUnit)) {
1537
+ return [];
1538
+ }
1539
+ } catch (_unused) {}
1384
1540
  if (!welcomeMessage) {
1385
1541
  return [];
1386
1542
  }
@@ -1397,23 +1553,37 @@
1397
1553
  const widgetRef = reactExports.useRef(null);
1398
1554
  const messageIdRef = reactExports.useRef(1);
1399
1555
 
1400
- // Onboarding state: show form first when onboarding is enabled (no message required)
1556
+ // Onboarding state: restore from session so it survives page refresh
1401
1557
  const [onboardingActive, setOnboardingActive] = reactExports.useState(false);
1402
- const [onboardingCompleted, setOnboardingCompleted] = reactExports.useState(false);
1403
- const [currentQuestionIndex, setCurrentQuestionIndex] = reactExports.useState(0);
1558
+ const [onboardingCompleted, setOnboardingCompleted] = reactExports.useState(() => {
1559
+ try {
1560
+ const existing = getSession(organizationId);
1561
+ if (existing && existing.onboardingCompleted) return true;
1562
+ } catch (_unused2) {}
1563
+ return false;
1564
+ });
1404
1565
  const [pendingUserIntent, setPendingUserIntent] = reactExports.useState(null);
1405
1566
  const [onboardingAnswers, setOnboardingAnswers] = reactExports.useState({});
1406
- const [hasUserInteracted, setHasUserInteracted] = reactExports.useState(false);
1407
- const [isProcessingAnswer, setIsProcessingAnswer] = reactExports.useState(false);
1408
1567
  const pendingIntentProcessedRef = reactExports.useRef(false);
1409
1568
 
1410
1569
  // Bidi/Voice state
1570
+ const [isConversationLoading, setIsConversationLoading] = reactExports.useState(false);
1411
1571
  const [isVoiceSessionActive, setIsVoiceSessionActive] = reactExports.useState(false);
1412
1572
  const [bidiMessages, setBidiMessages] = reactExports.useState([]);
1413
1573
  const [voiceStatus, setVoiceStatus] = reactExports.useState("idle"); // 'idle' | 'connecting' | 'connected' | 'error'
1414
1574
  const [voiceError, setVoiceError] = reactExports.useState(null);
1415
- const [voiceConclusionForCsat, setVoiceConclusionForCsat] = reactExports.useState(false); // set when backend sends conclusion_detected in voice so ChatWindow can show CSAT
1416
- const [bidiSessionId] = reactExports.useState(() => "widget-session-".concat(Math.random().toString(36).slice(2, 9)));
1575
+ const [voiceConclusionForCsat, setVoiceConclusionForCsat] = reactExports.useState(false);
1576
+ const [bidiSessionId, setBidiSessionId] = reactExports.useState(() => {
1577
+ try {
1578
+ const existing = getSession(organizationId);
1579
+ // Only restore if config is loaded and verified valid
1580
+ if (inactivityTimeoutValue !== undefined && existing && isSessionValid(existing, inactivityTimeoutValue, inactivityTimeoutUnit)) {
1581
+ return existing.conversationId;
1582
+ }
1583
+ } catch (_unused3) {}
1584
+ // Otherwise start with a fresh ID (which may be reverted to restored ID in effect if found valid later)
1585
+ return "widget-session-".concat(Math.random().toString(36).slice(2, 9));
1586
+ });
1417
1587
 
1418
1588
  // Bidi refs
1419
1589
  const websocketRef = reactExports.useRef(null);
@@ -1460,7 +1630,106 @@
1460
1630
  setIsDark(darkMode);
1461
1631
  }, [darkMode]);
1462
1632
 
1463
- // When onboarding is enabled, show the form as the first screen (no message required)
1633
+ // Session manager: on mount, either restore existing session or create a new one
1634
+ const historyLoadedRef = reactExports.useRef(false); // true once fetch is initiated
1635
+ const historyHasMessagesRef = reactExports.useRef(false); // true once history with messages is loaded
1636
+ const inactivityInitializedRef = reactExports.useRef(false); // true after first inactivity effect run
1637
+ reactExports.useEffect(() => {
1638
+ // Wait for all configuration to be ready
1639
+ if (!organizationId || !bidiSessionId || inactivityTimeoutValue === undefined) return;
1640
+ const existing = getSession(organizationId);
1641
+
1642
+ // Check validity ONLY if an existing session was found
1643
+ const hasValidSession = existing && isSessionValid(existing, inactivityTimeoutValue, inactivityTimeoutUnit);
1644
+ if (hasValidSession) {
1645
+ // Ensure the stateful session ID matches the persisted valid session
1646
+ if (bidiSessionId !== existing.conversationId) {
1647
+ setBidiSessionId(existing.conversationId);
1648
+ return;
1649
+ }
1650
+
1651
+ // Restore session — fetch conversation history once
1652
+ if (!historyLoadedRef.current && apiBaseUrl) {
1653
+ historyLoadedRef.current = true;
1654
+ setIsConversationLoading(true);
1655
+ fetchConversationHistory(apiBaseUrl, organizationId, bidiSessionId).then(msgs => {
1656
+ // Filter out onboarding field messages (name/email/phone) so they don't appear as chat bubbles
1657
+ const ONBOARDING_TYPES = ['userName', 'email', 'phone', 'fullname', 'mobileno', 'name'];
1658
+ const chatMsgs = msgs.filter(msg => {
1659
+ const msgType = msg.type || msg.messageType;
1660
+ if (msgType && ONBOARDING_TYPES.includes(msgType)) return false;
1661
+ return true;
1662
+ });
1663
+ if (chatMsgs.length > 0) {
1664
+ historyHasMessagesRef.current = true;
1665
+ const historyMessages = chatMsgs.map((msg, index) => _objectSpread2(_objectSpread2({}, createMessage({
1666
+ id: msg.id || "history-".concat(index, "-").concat(Date.now()),
1667
+ role: msg.role || (msg.sender === 'user' ? 'user' : 'assistant'),
1668
+ content: msg.message || msg.content || ''
1669
+ })), {}, {
1670
+ timestamp: msg.time ? new Date(msg.time) : new Date()
1671
+ }, Array.isArray(msg.prompts) && msg.prompts.length > 0 ? {
1672
+ prompts: msg.prompts
1673
+ } : {}));
1674
+
1675
+ // If the API didn't return prompts on any message, restore from localStorage
1676
+ const hasAnyPrompts = historyMessages.some(m => m.role === 'assistant' && Array.isArray(m.prompts) && m.prompts.length > 0);
1677
+ if (!hasAnyPrompts) {
1678
+ const savedPrompts = getPromptSuggestions(organizationId);
1679
+ if (savedPrompts.length > 0) {
1680
+ // Attach saved prompts to the last assistant message
1681
+ for (let i = historyMessages.length - 1; i >= 0; i -= 1) {
1682
+ if (historyMessages[i].role === 'assistant') {
1683
+ historyMessages[i] = _objectSpread2(_objectSpread2({}, historyMessages[i]), {}, {
1684
+ prompts: savedPrompts
1685
+ });
1686
+ break;
1687
+ }
1688
+ }
1689
+ }
1690
+ }
1691
+ setMessages(historyMessages);
1692
+ }
1693
+ }).catch(err => {
1694
+ console.error('[Session] History fetch failed:', err);
1695
+ }).finally(() => {
1696
+ setIsConversationLoading(false);
1697
+ });
1698
+ }
1699
+ } else {
1700
+ // Session is expired, invalid, or non-existent
1701
+ if (!historyLoadedRef.current) {
1702
+ historyLoadedRef.current = true;
1703
+ }
1704
+
1705
+ // If we are currently holding an EXPIRED ID, rotate it
1706
+ if (existing && bidiSessionId === existing.conversationId) {
1707
+ const newId = "widget-session-".concat(Math.random().toString(36).slice(2, 9));
1708
+ console.warn("[Session] Rotating expired ID ".concat(bidiSessionId, " -> ").concat(newId));
1709
+ setBidiSessionId(newId);
1710
+ // Note: saveSession will be called on the next run with the newId
1711
+ } else {
1712
+ // We have a fresh ID (either from initializer or rotation), persist it if not already there
1713
+ if (!existing || existing.conversationId !== bidiSessionId) {
1714
+ saveSession(organizationId, bidiSessionId);
1715
+ }
1716
+ }
1717
+ }
1718
+ }, [organizationId, bidiSessionId, apiBaseUrl, inactivityTimeoutValue, inactivityTimeoutUnit]);
1719
+
1720
+ // Persist prompt suggestions to localStorage so they survive page refresh
1721
+ reactExports.useEffect(() => {
1722
+ if (!organizationId || !messages || messages.length === 0) return;
1723
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
1724
+ const msg = messages[i];
1725
+ if ((msg === null || msg === void 0 ? void 0 : msg.role) === 'assistant' && Array.isArray(msg.prompts) && msg.prompts.length > 0) {
1726
+ savePromptSuggestions(organizationId, msg.prompts);
1727
+ return;
1728
+ }
1729
+ }
1730
+ }, [messages, organizationId]);
1731
+
1732
+ // Show the onboarding form on first load; bypass it if already completed (persisted in session)
1464
1733
  reactExports.useEffect(() => {
1465
1734
  if (!onboardingEnabled || onboardingCompleted) return;
1466
1735
  const questions = getOnboardingQuestions(onboardingQuestions);
@@ -1469,9 +1738,9 @@
1469
1738
  }
1470
1739
  }, [onboardingEnabled, onboardingCompleted, onboardingQuestions]);
1471
1740
 
1472
- // Sync welcome message updates
1741
+ // Sync welcome message updates — skip entirely when history has been loaded
1473
1742
  reactExports.useEffect(() => {
1474
- if (!welcomeMessage) {
1743
+ if (!welcomeMessage || historyHasMessagesRef.current) {
1475
1744
  return;
1476
1745
  }
1477
1746
  setMessages(prev => {
@@ -1486,7 +1755,6 @@
1486
1755
  }
1487
1756
  const [first, ...rest] = prev;
1488
1757
  if (first.isWelcome) {
1489
- // Keep the same ID when updating welcome message content
1490
1758
  return [_objectSpread2(_objectSpread2({}, first), {}, {
1491
1759
  content: welcomeMessage
1492
1760
  }), ...rest];
@@ -1620,10 +1888,12 @@
1620
1888
  // For regular messages, process the stream
1621
1889
  if (apiType === 'inquiry' || apiType === 'complaint' || apiType === 'technical') {
1622
1890
  await processStream(stream, setMessages);
1891
+ updateActivity(organizationId);
1623
1892
  return {
1624
1893
  success: true
1625
1894
  };
1626
1895
  }
1896
+ updateActivity(organizationId);
1627
1897
  return {
1628
1898
  success: true
1629
1899
  };
@@ -1652,15 +1922,6 @@
1652
1922
  }
1653
1923
  }
1654
1924
  }, [apiBaseUrl, apiKey, getNextMessageId, organizationId, messages, bidiSessionId]);
1655
- const startOnboarding = reactExports.useCallback(() => {
1656
- const questions = getOnboardingQuestions(onboardingQuestions);
1657
- if (questions.length === 0) {
1658
- return;
1659
- }
1660
- setOnboardingActive(true);
1661
- setCurrentQuestionIndex(0);
1662
- // Form UI shows all questions at once; no intro/first-question messages
1663
- }, [onboardingQuestions]);
1664
1925
  const handleOnboardingFormSubmit = reactExports.useCallback(async answers => {
1665
1926
  const questions = getOnboardingQuestions(onboardingQuestions);
1666
1927
  if (questions.length === 0) return;
@@ -1684,17 +1945,8 @@
1684
1945
  }
1685
1946
  setOnboardingActive(false);
1686
1947
  setOnboardingCompleted(true);
1948
+ markOnboardingCompleted(organizationId);
1687
1949
  setOnboardingAnswers(prev => _objectSpread2(_objectSpread2({}, prev), answers));
1688
-
1689
- // Confirmation message disabled - no need to show after onboarding
1690
- // const successMessage = createMessage({
1691
- // id: getNextMessageId(),
1692
- // role: "assistant",
1693
- // content: "Thank you! I've received your information.",
1694
- // isProcessing: false,
1695
- // });
1696
- // setMessages((prev) => [...prev, successMessage]);
1697
-
1698
1950
  if (pendingUserIntent && !pendingIntentProcessedRef.current) {
1699
1951
  pendingIntentProcessedRef.current = true;
1700
1952
  setTimeout(() => {
@@ -1702,157 +1954,16 @@
1702
1954
  setPendingUserIntent(null);
1703
1955
  }, 500);
1704
1956
  }
1705
- }, [onboardingQuestions, sendMessage, getNextMessageId, pendingUserIntent]);
1706
- const handleOnboardingAnswer = reactExports.useCallback(async answer => {
1707
- if (isProcessingAnswer) {
1708
- return;
1709
- }
1710
- const questions = getOnboardingQuestions(onboardingQuestions);
1711
- const currentQuestion = questions[currentQuestionIndex];
1712
- if (!currentQuestion) {
1713
- console.error("No question found at index", currentQuestionIndex);
1714
- return;
1715
- }
1716
-
1717
- // Validate answer first
1718
- const validation = validateOnboardingAnswer(currentQuestion, answer);
1719
- if (!validation.isValid) {
1720
- const errorMessage = createMessage({
1721
- id: getNextMessageId(),
1722
- role: "assistant",
1723
- content: validation.errorMessage || "Invalid input. Please try again.",
1724
- isProcessing: false
1725
- });
1726
- setMessages(prev => [...prev, errorMessage]);
1727
- return;
1728
- }
1729
- setIsProcessingAnswer(true);
1730
- const fieldType = currentQuestion.fieldType || "question_".concat(currentQuestionIndex);
1731
-
1732
- // Add user message to UI
1733
- const userAnswer = createMessage({
1734
- id: getNextMessageId(),
1735
- role: "user",
1736
- content: answer,
1737
- timestamp: new Date(),
1738
- isProcessing: false
1739
- });
1740
- setMessages(prev => [...prev, userAnswer]);
1741
-
1742
- // Save answer locally
1743
- setOnboardingAnswers(prev => _objectSpread2(_objectSpread2({}, prev), {}, {
1744
- [fieldType]: answer
1745
- }));
1746
- try {
1747
- // Send to API
1748
- const response = await sendMessage(answer, fieldType);
1749
- if (!response || !response.success) {
1750
- const errorMsg = createMessage({
1751
- id: getNextMessageId(),
1752
- role: "assistant",
1753
- content: (response === null || response === void 0 ? void 0 : response.error) || "Failed to save your answer. Please try again.",
1754
- isProcessing: false
1755
- });
1756
- setMessages(prev => [...prev, errorMsg]);
1757
- setIsProcessingAnswer(false);
1758
- return;
1759
- }
1760
- const nextIndex = currentQuestionIndex + 1;
1761
- if (nextIndex < questions.length) {
1762
- setCurrentQuestionIndex(nextIndex);
1763
- setIsProcessingAnswer(false);
1764
- setTimeout(() => {
1765
- var _nextQuestion$askToTy;
1766
- const nextQuestion = questions[nextIndex];
1767
- const questionMessage = createMessage({
1768
- id: getNextMessageId(),
1769
- role: "assistant",
1770
- content: (_nextQuestion$askToTy = nextQuestion.askToType) !== null && _nextQuestion$askToTy !== void 0 ? _nextQuestion$askToTy : nextQuestion,
1771
- isProcessing: false
1772
- });
1773
- setMessages(prev => [...prev, questionMessage]);
1774
- }, 300);
1775
- } else {
1776
- // Onboarding completed
1777
- setOnboardingActive(false);
1778
- setOnboardingCompleted(true);
1779
- setIsProcessingAnswer(false);
1780
- setTimeout(() => {
1781
- // Confirmation message disabled - no need to show after onboarding
1782
- // const successMessage = createMessage({
1783
- // id: getNextMessageId(),
1784
- // role: "assistant",
1785
- // content: "Thank you! I've received your information.",
1786
- // isProcessing: false,
1787
- // });
1788
- // setMessages((prev) => [...prev, successMessage]);
1789
-
1790
- // If there's a pending user intent, pre-fill input
1791
- if (pendingUserIntent && !pendingIntentProcessedRef.current) {
1792
- pendingIntentProcessedRef.current = true;
1793
- setTimeout(() => {
1794
- setInputValue(pendingUserIntent);
1795
- setPendingUserIntent(null);
1796
- }, 500);
1797
- }
1798
- }, 300);
1799
- }
1800
- } catch (error) {
1801
- const errorMsg = createMessage({
1802
- id: getNextMessageId(),
1803
- role: "assistant",
1804
- content: "An error occurred. Please try again.",
1805
- isProcessing: false
1806
- });
1807
- setMessages(prev => [...prev, errorMsg]);
1808
- setIsProcessingAnswer(false);
1809
- }
1810
- }, [onboardingQuestions, currentQuestionIndex, pendingUserIntent, sendMessage, isProcessingAnswer, getNextMessageId]);
1957
+ }, [onboardingQuestions, sendMessage, getNextMessageId, pendingUserIntent, organizationId]);
1811
1958
  const handleSmartSubmit = reactExports.useCallback(async () => {
1812
- const questions = getOnboardingQuestions(onboardingQuestions);
1813
1959
  const trimmedInput = inputValue.trim();
1814
- if (!trimmedInput) {
1815
- return;
1816
- }
1817
-
1818
- // Case 1: No onboarding questions OR onboarding already completed OR onboarding disabled
1819
- if (questions.length === 0 || onboardingCompleted || !onboardingEnabled) {
1820
- await sendMessage(trimmedInput);
1821
- return;
1822
- }
1823
-
1824
- // Case 2: User's FIRST interaction - start onboarding
1825
- if (!hasUserInteracted && !onboardingActive) {
1826
- setHasUserInteracted(true);
1827
- pendingIntentProcessedRef.current = false;
1960
+ if (!trimmedInput) return;
1828
1961
 
1829
- // Add user message to chat immediately
1830
- const userMessage = createMessage({
1831
- id: getNextMessageId(),
1832
- role: "user",
1833
- content: trimmedInput,
1834
- timestamp: new Date(),
1835
- isProcessing: false
1836
- });
1837
- setMessages(prev => [...prev, userMessage]);
1838
-
1839
- // Store the user's intent
1840
- setPendingUserIntent(trimmedInput);
1841
- setInputValue("");
1842
- startOnboarding();
1843
- return;
1844
- }
1845
-
1846
- // Case 3: Onboarding is ACTIVE - handle answer
1847
- if (onboardingActive) {
1848
- await handleOnboardingAnswer(trimmedInput);
1849
- setInputValue("");
1850
- return;
1851
- }
1852
-
1853
- // Default: send as regular message
1962
+ // Onboarding is handled entirely by the OnboardingForm component.
1963
+ // The text input is hidden while onboarding is active, so any message
1964
+ // arriving here is a normal chat message.
1854
1965
  await sendMessage(trimmedInput);
1855
- }, [onboardingQuestions, inputValue, onboardingActive, onboardingCompleted, hasUserInteracted, startOnboarding, handleOnboardingAnswer, sendMessage, getNextMessageId]);
1966
+ }, [inputValue, sendMessage]);
1856
1967
  const handleSend = reactExports.useCallback(async () => {
1857
1968
  await handleSmartSubmit();
1858
1969
  }, [handleSmartSubmit]);
@@ -1871,47 +1982,11 @@
1871
1982
  };
1872
1983
  const handleQuickQuestion = reactExports.useCallback(async question => {
1873
1984
  const trimmedQuestion = question === null || question === void 0 ? void 0 : question.trim();
1874
- if (!trimmedQuestion) {
1875
- return;
1876
- }
1877
- const questions = getOnboardingQuestions(onboardingQuestions);
1878
-
1879
- // Case 1: No onboarding questions OR onboarding already completed OR onboarding disabled
1880
- if (questions.length === 0 || onboardingCompleted || !onboardingEnabled) {
1881
- await sendMessage(trimmedQuestion);
1882
- return;
1883
- }
1985
+ if (!trimmedQuestion) return;
1884
1986
 
1885
- // Case 2: User's FIRST interaction - start onboarding
1886
- if (!hasUserInteracted && !onboardingActive) {
1887
- setHasUserInteracted(true);
1888
- pendingIntentProcessedRef.current = false;
1889
-
1890
- // Add user message to chat immediately
1891
- const userMessage = createMessage({
1892
- id: getNextMessageId(),
1893
- role: "user",
1894
- content: trimmedQuestion,
1895
- timestamp: new Date(),
1896
- isProcessing: false
1897
- });
1898
- setMessages(prev => [...prev, userMessage]);
1899
-
1900
- // Store the user's intent
1901
- setPendingUserIntent(trimmedQuestion);
1902
- startOnboarding();
1903
- return;
1904
- }
1905
-
1906
- // Case 3: Onboarding is ACTIVE - handle answer
1907
- if (onboardingActive) {
1908
- await handleOnboardingAnswer(trimmedQuestion);
1909
- return;
1910
- }
1911
-
1912
- // Default: send as regular message
1987
+ // Quick questions are only visible after onboarding is complete (input is hidden during onboarding)
1913
1988
  await sendMessage(trimmedQuestion);
1914
- }, [onboardingQuestions, onboardingActive, onboardingCompleted, hasUserInteracted, startOnboarding, handleOnboardingAnswer, sendMessage, getNextMessageId]);
1989
+ }, [sendMessage]);
1915
1990
 
1916
1991
  /**
1917
1992
  * Handle user decision for HITL (Human-in-the-Loop)
@@ -1948,35 +2023,19 @@
1948
2023
  if (!currentInputMessageIdRef.current) {
1949
2024
  return;
1950
2025
  }
1951
- console.log("[BIDI] finalizePendingInputMessage called", {
1952
- messageId: currentInputMessageIdRef.current
1953
- });
1954
2026
  setBidiMessages(prev => {
1955
2027
  const updated = prev.map(message => message.id === currentInputMessageIdRef.current ? _objectSpread2(_objectSpread2({}, message), {}, {
1956
2028
  isProcessing: false
1957
2029
  }) : message);
1958
- console.log("[BIDI] finalizePendingInputMessage: updated state", {
1959
- messageId: currentInputMessageIdRef.current,
1960
- found: prev.some(m => m.id === currentInputMessageIdRef.current),
1961
- prevLength: prev.length,
1962
- newLength: updated.length
1963
- });
1964
2030
  return updated;
1965
2031
  });
1966
2032
  currentInputMessageIdRef.current = null;
1967
2033
  }, []);
1968
2034
  const appendUserTranscription = reactExports.useCallback(function (text) {
1969
2035
  let finished = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
1970
- console.log("[BIDI] appendUserTranscription called", {
1971
- text: text === null || text === void 0 ? void 0 : text.substring(0, 50),
1972
- finished,
1973
- currentInputId: currentInputMessageIdRef.current
1974
- });
1975
-
1976
2036
  // Mark that user has spoken - now we can process assistant responses
1977
2037
  userHasSpokenRef.current = true;
1978
2038
  if (!text || typeof text === 'string' && !text.trim()) {
1979
- console.log("[BIDI] appendUserTranscription: skipping empty text");
1980
2039
  if (finished && currentInputMessageIdRef.current) {
1981
2040
  setBidiMessages(prev => prev.map(message => message.id === currentInputMessageIdRef.current ? _objectSpread2(_objectSpread2({}, message), {}, {
1982
2041
  isProcessing: false
@@ -2008,17 +2067,9 @@
2008
2067
  const assistantTime = latestAssistantMessage.timestamp instanceof Date ? latestAssistantMessage.timestamp.getTime() : new Date(latestAssistantMessage.timestamp).getTime();
2009
2068
  // Use timestamp 1ms before the assistant message to ensure user appears first
2010
2069
  userTimestamp = new Date(assistantTime - 1);
2011
- console.log("[BIDI] appendUserTranscription: using timestamp before assistant message", {
2012
- assistantTime: new Date(assistantTime).toISOString(),
2013
- userTime: userTimestamp.toISOString(),
2014
- assistantId: latestAssistantMessage.id
2015
- });
2016
2070
  } else {
2017
2071
  // No assistant message yet, use current time
2018
2072
  userTimestamp = new Date(now);
2019
- console.log("[BIDI] appendUserTranscription: no assistant message, using current time", {
2020
- userTime: userTimestamp.toISOString()
2021
- });
2022
2073
  }
2023
2074
  const userMessage = createMessage({
2024
2075
  id: newId,
@@ -2039,13 +2090,6 @@
2039
2090
  result = [...result, userMessage];
2040
2091
  // Set ref AFTER adding message to result array
2041
2092
  currentInputMessageIdRef.current = newId;
2042
- console.log("[BIDI] appendUserTranscription: created new user message", {
2043
- messageId: newId,
2044
- content: chunk.substring(0, 50),
2045
- prevLength,
2046
- newLength: result.length,
2047
- allMessageIds: result.map(m => m.id)
2048
- });
2049
2093
  } else {
2050
2094
  console.warn("[BIDI] appendUserTranscription: message already exists, updating instead", {
2051
2095
  messageId: newId
@@ -2061,18 +2105,10 @@
2061
2105
  const userMessageExists = result.some(m => m.id === currentInputMessageIdRef.current);
2062
2106
  if (userMessageExists) {
2063
2107
  result = result.map(message => {
2064
- var _message$content, _message$content2;
2065
2108
  if (message.id !== currentInputMessageIdRef.current) {
2066
2109
  return message;
2067
2110
  }
2068
2111
  const combined = cleanCJKSpaces("".concat(message.content || "").concat(chunk));
2069
- console.log("[BIDI] appendUserTranscription: updating existing message", {
2070
- messageId: message.id,
2071
- oldContent: (_message$content = message.content) === null || _message$content === void 0 ? void 0 : _message$content.substring(0, 30),
2072
- newContent: combined.substring(0, 50),
2073
- oldLength: ((_message$content2 = message.content) === null || _message$content2 === void 0 ? void 0 : _message$content2.length) || 0,
2074
- newLength: combined.length
2075
- });
2076
2112
  return _objectSpread2(_objectSpread2({}, message), {}, {
2077
2113
  content: combined,
2078
2114
  // CRITICAL: Don't update timestamp when appending content - preserve original timestamp for ordering
@@ -2107,12 +2143,6 @@
2107
2143
  });
2108
2144
  }
2109
2145
  }
2110
- console.log("[BIDI] appendUserTranscription: state update complete", {
2111
- prevLength,
2112
- newLength: result.length,
2113
- messageIds: result.map(m => m.id),
2114
- messageRoles: result.map(m => m.role)
2115
- });
2116
2146
 
2117
2147
  // CRITICAL: Safety check - never return fewer messages than we started with
2118
2148
  if (result.length < prevLength) {
@@ -2141,9 +2171,6 @@
2141
2171
  return result;
2142
2172
  });
2143
2173
  if (finished) {
2144
- console.log("[BIDI] appendUserTranscription: finalizing message", {
2145
- messageId: currentInputMessageIdRef.current
2146
- });
2147
2174
  currentInputMessageIdRef.current = null;
2148
2175
  setIsLoading(true);
2149
2176
  }
@@ -2182,9 +2209,6 @@
2182
2209
  if (isProcessingAssistantUpdateRef.current) {
2183
2210
  // Check if this is the exact same content we're already processing
2184
2211
  if (lastProcessedAssistantContentRef.current === contentKey) {
2185
- console.log("[BIDI] appendAssistantContent: skipping duplicate - already processing", {
2186
- contentKey
2187
- });
2188
2212
  return;
2189
2213
  }
2190
2214
  // If it's different content, we need to wait - but for now, let it through
@@ -2220,9 +2244,6 @@
2220
2244
  result = result.map(message => message.id === inputMessageIdToFinalize ? _objectSpread2(_objectSpread2({}, message), {}, {
2221
2245
  isProcessing: false
2222
2246
  }) : message);
2223
- console.log("[BIDI] appendAssistantContent: finalized input message atomically", {
2224
- inputMessageId: inputMessageIdToFinalize
2225
- });
2226
2247
  } else {
2227
2248
  console.warn("[BIDI] appendAssistantContent: input message not found for finalization", {
2228
2249
  inputMessageId: inputMessageIdToFinalize,
@@ -2258,13 +2279,6 @@
2258
2279
  result = [...result, assistantMessage];
2259
2280
  // Set ref AFTER adding message to result array to ensure consistency
2260
2281
  currentAssistantMessageIdRef.current = newId;
2261
- console.log("[BIDI] appendAssistantContent: created new assistant message", {
2262
- messageId: newId,
2263
- content: text.substring(0, 100),
2264
- prevLength,
2265
- newLength: result.length,
2266
- allMessageIds: result.map(m => m.id)
2267
- });
2268
2282
  } else {
2269
2283
  console.error("[BIDI] appendAssistantContent: ERROR - message ID collision!", {
2270
2284
  messageId: newId,
@@ -2283,14 +2297,14 @@
2283
2297
  const assistantMessageExists = result.some(m => m.id === currentAssistantMessageIdRef.current);
2284
2298
  if (assistantMessageExists) {
2285
2299
  result = result.map(message => {
2286
- var _message$content3;
2300
+ var _message$content;
2287
2301
  if (message.id !== currentAssistantMessageIdRef.current) {
2288
2302
  return message;
2289
2303
  }
2290
2304
  const newContent = "".concat(message.content || "").concat(text);
2291
2305
  console.log("[BIDI] appendAssistantContent: updating existing message", {
2292
2306
  messageId: message.id,
2293
- oldContentLength: ((_message$content3 = message.content) === null || _message$content3 === void 0 ? void 0 : _message$content3.length) || 0,
2307
+ oldContentLength: ((_message$content = message.content) === null || _message$content === void 0 ? void 0 : _message$content.length) || 0,
2294
2308
  newContentLength: newContent.length,
2295
2309
  textLength: text.length
2296
2310
  });
@@ -2469,8 +2483,34 @@
2469
2483
  }) : message;
2470
2484
  }));
2471
2485
  }, []);
2486
+ const stopVoiceSession = reactExports.useCallback(() => {
2487
+ var _audioPlayerNodeRef$c, _audioPlayerContextRe, _audioRecorderNodeRef, _audioRecorderContext;
2488
+ setIsVoiceSessionActive(false);
2489
+ setIsLoading(false);
2490
+ isVoiceSessionActiveRef.current = false;
2491
+ setVoiceStatus("idle");
2492
+ setVoiceError(null);
2493
+
2494
+ // Reset userHasSpoken flag when stopping session
2495
+ userHasSpokenRef.current = false;
2496
+ if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) {
2497
+ websocketRef.current.close();
2498
+ }
2499
+ websocketRef.current = null;
2500
+ (_audioPlayerNodeRef$c = audioPlayerNodeRef.current) === null || _audioPlayerNodeRef$c === void 0 || _audioPlayerNodeRef$c.disconnect();
2501
+ (_audioPlayerContextRe = audioPlayerContextRef.current) === null || _audioPlayerContextRe === void 0 || _audioPlayerContextRe.close();
2502
+ audioPlayerNodeRef.current = null;
2503
+ audioPlayerContextRef.current = null;
2504
+ (_audioRecorderNodeRef = audioRecorderNodeRef.current) === null || _audioRecorderNodeRef === void 0 || _audioRecorderNodeRef.disconnect();
2505
+ (_audioRecorderContext = audioRecorderContextRef.current) === null || _audioRecorderContext === void 0 || _audioRecorderContext.close();
2506
+ audioRecorderNodeRef.current = null;
2507
+ audioRecorderContextRef.current = null;
2508
+ stopMicrophone(micStreamRef.current);
2509
+ micStreamRef.current = null;
2510
+ isAudioReadyRef.current = false;
2511
+ }, []);
2472
2512
  const handleBidiEvent = reactExports.useCallback(event => {
2473
- var _event$content, _event$conclusion_det, _event$outputTranscri3, _event$content4;
2513
+ var _event$content, _event$conclusion_det, _event$outputTranscri2, _event$content3;
2474
2514
  console.log("[BIDI] handleBidiEvent called", {
2475
2515
  event,
2476
2516
  isVoiceSessionActive: isVoiceSessionActiveRef.current,
@@ -2495,10 +2535,28 @@
2495
2535
  setVoiceConclusionForCsat(true);
2496
2536
  }
2497
2537
  if (event.turnComplete) {
2498
- console.log("[BIDI] handleBidiEvent: processing turnComplete");
2499
2538
  handleTurnComplete();
2500
2539
  return;
2501
2540
  }
2541
+
2542
+ // Handle conversation conclusion detection from bidi/voice
2543
+ if (typeof conclusionDetected === "boolean" && conclusionDetected) {
2544
+ stopVoiceSession();
2545
+ // Mark the last bidi assistant message with conclusionDetected so ChatWindow picks it up
2546
+ setBidiMessages(prev => {
2547
+ const updated = [...prev];
2548
+ for (let i = updated.length - 1; i >= 0; i--) {
2549
+ if (updated[i].role === "assistant") {
2550
+ updated[i] = _objectSpread2(_objectSpread2({}, updated[i]), {}, {
2551
+ conclusionDetected: true
2552
+ });
2553
+ break;
2554
+ }
2555
+ }
2556
+ return updated;
2557
+ });
2558
+ return;
2559
+ }
2502
2560
  if (event.interrupted) {
2503
2561
  console.log("[BIDI] handleBidiEvent: processing interrupted");
2504
2562
  handleInterrupted();
@@ -2517,6 +2575,7 @@
2517
2575
 
2518
2576
  // Only call appendUserTranscription if there's actual text or if it's finished
2519
2577
  if (transcriptionText || transcriptionFinished) {
2578
+ updateActivity(organizationId);
2520
2579
  appendUserTranscription(transcriptionText, transcriptionFinished);
2521
2580
  } else {
2522
2581
  console.warn("[BIDI] handleBidiEvent: inputTranscription event has no text and not finished", event.inputTranscription);
@@ -2529,30 +2588,16 @@
2529
2588
  var _event$outputTranscri, _event$content2;
2530
2589
  // Silently ignore assistant responses before user speaks
2531
2590
  if ((_event$outputTranscri = event.outputTranscription) !== null && _event$outputTranscri !== void 0 && _event$outputTranscri.text || (_event$content2 = event.content) !== null && _event$content2 !== void 0 && _event$content2.parts) {
2532
- var _event$outputTranscri2, _event$content3;
2533
- console.log("[BIDI] handleBidiEvent: ignoring assistant response - user hasn't spoken yet", {
2534
- hasOutputTranscription: !!((_event$outputTranscri2 = event.outputTranscription) !== null && _event$outputTranscri2 !== void 0 && _event$outputTranscri2.text),
2535
- hasContentParts: !!((_event$content3 = event.content) !== null && _event$content3 !== void 0 && _event$content3.parts)
2536
- });
2537
2591
  return;
2538
2592
  }
2539
2593
  }
2540
- if ((_event$outputTranscri3 = event.outputTranscription) !== null && _event$outputTranscri3 !== void 0 && _event$outputTranscri3.text) {
2541
- console.log("[BIDI] handleBidiEvent: processing outputTranscription", {
2542
- text: event.outputTranscription.text.substring(0, 50),
2543
- finished: event.outputTranscription.finished
2544
- });
2594
+ if ((_event$outputTranscri2 = event.outputTranscription) !== null && _event$outputTranscri2 !== void 0 && _event$outputTranscri2.text) {
2545
2595
  appendAssistantContent(event.outputTranscription.text, Boolean(event.outputTranscription.finished));
2546
2596
  }
2547
- if ((_event$content4 = event.content) !== null && _event$content4 !== void 0 && _event$content4.parts) {
2548
- var _event$content5;
2597
+ if ((_event$content3 = event.content) !== null && _event$content3 !== void 0 && _event$content3.parts) {
2598
+ var _event$content4;
2549
2599
  // Check if the content has a finished flag, or if the event itself indicates completion
2550
- const contentFinished = ((_event$content5 = event.content) === null || _event$content5 === void 0 ? void 0 : _event$content5.finished) || event.finished || false;
2551
- console.log("[BIDI] handleBidiEvent: processing content parts", {
2552
- partsCount: event.content.parts.length,
2553
- contentFinished,
2554
- eventFinished: event.finished
2555
- });
2600
+ const contentFinished = ((_event$content4 = event.content) === null || _event$content4 === void 0 ? void 0 : _event$content4.finished) || event.finished || false;
2556
2601
  handleContentParts(event.content.parts, contentFinished);
2557
2602
  }
2558
2603
 
@@ -2560,7 +2605,7 @@
2560
2605
  if (!event.turnComplete && !event.interrupted && !event.inputTranscription && !event.outputTranscription && !event.content) {
2561
2606
  console.warn("[BIDI] handleBidiEvent: event had no recognized fields", event);
2562
2607
  }
2563
- }, [appendAssistantContent, appendUserTranscription, handleContentParts, handleInterrupted, handleTurnComplete]);
2608
+ }, [appendAssistantContent, appendUserTranscription, handleContentParts, handleInterrupted, handleTurnComplete, stopVoiceSession]);
2564
2609
  const handleRecorderAudio = reactExports.useCallback(pcmData => {
2565
2610
  const ws = websocketRef.current;
2566
2611
  if (!isVoiceSessionActiveRef.current || !ws || ws.readyState !== WebSocket.OPEN) {
@@ -2692,37 +2737,6 @@
2692
2737
  setVoiceStatus("error");
2693
2738
  }
2694
2739
  }, [enableVoiceInteraction, organizationId, welcomeMessage, connectWebsocket, initializeAudioPipeline, messages]);
2695
- const stopVoiceSession = reactExports.useCallback(() => {
2696
- var _audioPlayerNodeRef$c, _audioPlayerContextRe, _audioRecorderNodeRef, _audioRecorderContext;
2697
- setIsVoiceSessionActive(false);
2698
- setIsLoading(false);
2699
- isVoiceSessionActiveRef.current = false;
2700
- setVoiceStatus("idle");
2701
- setVoiceError(null);
2702
-
2703
- // Reset userHasSpoken flag when stopping session
2704
- userHasSpokenRef.current = false;
2705
- if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) {
2706
- websocketRef.current.close();
2707
- }
2708
- websocketRef.current = null;
2709
- (_audioPlayerNodeRef$c = audioPlayerNodeRef.current) === null || _audioPlayerNodeRef$c === void 0 || _audioPlayerNodeRef$c.disconnect();
2710
- (_audioPlayerContextRe = audioPlayerContextRef.current) === null || _audioPlayerContextRe === void 0 || _audioPlayerContextRe.close();
2711
- audioPlayerNodeRef.current = null;
2712
- audioPlayerContextRef.current = null;
2713
- (_audioRecorderNodeRef = audioRecorderNodeRef.current) === null || _audioRecorderNodeRef === void 0 || _audioRecorderNodeRef.disconnect();
2714
- (_audioRecorderContext = audioRecorderContextRef.current) === null || _audioRecorderContext === void 0 || _audioRecorderContext.close();
2715
- audioRecorderNodeRef.current = null;
2716
- audioRecorderContextRef.current = null;
2717
- stopMicrophone(micStreamRef.current);
2718
- micStreamRef.current = null;
2719
- isAudioReadyRef.current = false;
2720
- }, []);
2721
-
2722
- // Keep ref updated so handleBidiEvent (defined earlier) can call stopVoiceSession without TDZ
2723
- reactExports.useEffect(() => {
2724
- stopVoiceSessionRef.current = stopVoiceSession;
2725
- }, [stopVoiceSession]);
2726
2740
  const handleVoiceToggle = reactExports.useCallback(() => {
2727
2741
  if (isVoiceSessionActive) {
2728
2742
  stopVoiceSession();
@@ -2750,6 +2764,7 @@
2750
2764
  type: "text",
2751
2765
  text: trimmed
2752
2766
  }));
2767
+ updateActivity(organizationId);
2753
2768
  setIsLoading(true);
2754
2769
  const userMessage = createMessage({
2755
2770
  id: "bidi-user-".concat(Date.now()),
@@ -2782,6 +2797,116 @@
2782
2797
  isAudioReadyRef.current = false;
2783
2798
  };
2784
2799
  }, []);
2800
+
2801
+ // Reset entire conversation state after CSAT / closing — mirrors preview behaviour
2802
+ const resetConversation = reactExports.useCallback(() => {
2803
+ // Clear persisted session and prompt suggestions so the next mount starts fresh
2804
+ clearSession(organizationId);
2805
+ savePromptSuggestions(organizationId, []);
2806
+
2807
+ // Reset messages
2808
+ setMessages([]);
2809
+ setBidiMessages([]);
2810
+
2811
+ // Reset onboarding
2812
+ setOnboardingActive(false);
2813
+ setOnboardingCompleted(false);
2814
+ setOnboardingAnswers({});
2815
+ setPendingUserIntent(null);
2816
+ pendingIntentProcessedRef.current = false;
2817
+ userHasSpokenRef.current = false;
2818
+ historyLoadedRef.current = false;
2819
+ historyHasMessagesRef.current = false;
2820
+
2821
+ // Create a new session id and persist it
2822
+ const newSessionId = "widget-session-".concat(Math.random().toString(36).slice(2, 9));
2823
+ saveSession(organizationId, newSessionId);
2824
+ setBidiSessionId(newSessionId);
2825
+
2826
+ // Re-show the welcome message
2827
+ if (welcomeMessage && welcomeMessage.trim() !== "") {
2828
+ setMessages([_objectSpread2(_objectSpread2({}, createMessage({
2829
+ id: Date.now().toString(),
2830
+ role: "assistant",
2831
+ content: welcomeMessage
2832
+ })), {}, {
2833
+ isWelcome: true
2834
+ })]);
2835
+ }
2836
+
2837
+ // Re-trigger onboarding if enabled
2838
+ if (onboardingEnabled) {
2839
+ const questions = getOnboardingQuestions(onboardingQuestions);
2840
+ if (questions.length > 0) {
2841
+ setOnboardingActive(true);
2842
+ }
2843
+ }
2844
+ }, [organizationId, welcomeMessage, onboardingEnabled, onboardingQuestions]);
2845
+
2846
+ // Keep a stable ref to resetConversation so the timer effect does not need to
2847
+ // list it as a dependency (avoids spurious re-runs — and spurious updateActivity
2848
+ // calls — whenever resetConversation's identity changes due to parent re-renders).
2849
+ const resetConversationRef = reactExports.useRef(resetConversation);
2850
+ reactExports.useEffect(() => {
2851
+ resetConversationRef.current = resetConversation;
2852
+ }, [resetConversation]);
2853
+
2854
+ // Precise inactivity timeout: schedules a setTimeout for exact remaining time,
2855
+ // then reschedules itself after each check (handles activity updates in localStorage).
2856
+ //
2857
+ // Corner cases handled:
2858
+ // - Initial mount / page refresh: reads stored lastActivity and checks if already
2859
+ // expired → resets immediately, otherwise schedules for the remaining time.
2860
+ // - Config change (value/unit updated): refreshes lastActivity to NOW so the new
2861
+ // timeout starts fresh WITHOUT resetting the conversation or creating a new session ID.
2862
+ // - orgId change: resets the initialized flag so the next run is treated as initial mount.
2863
+ reactExports.useEffect(() => {
2864
+ // Reset initialized flag when org changes so we do an immediate expiry check for the new org
2865
+ inactivityInitializedRef.current = false;
2866
+ }, [organizationId]);
2867
+ reactExports.useEffect(() => {
2868
+ if (!organizationId || !inactivityTimeoutValue) return;
2869
+ const timeoutMs = getTimeoutMs(inactivityTimeoutValue, inactivityTimeoutUnit);
2870
+ if (!timeoutMs) return;
2871
+ const isConfigChange = inactivityInitializedRef.current;
2872
+ inactivityInitializedRef.current = true;
2873
+ if (isConfigChange) {
2874
+ // The actual timeout config (value/unit) changed at runtime — refresh lastActivity
2875
+ // to now so the new timeout period starts from this moment, preserving the session.
2876
+ const existing = getSession(organizationId);
2877
+ if (existing) {
2878
+ updateActivity(organizationId);
2879
+ }
2880
+ }
2881
+ // On page refresh or initial mount, isConfigChange is false so we do NOT touch
2882
+ // lastActivity — the stored timestamp is used as-is to calculate remaining time.
2883
+
2884
+ let timerId = null;
2885
+ const scheduleCheck = () => {
2886
+ const session = getSession(organizationId);
2887
+ if (!session) return;
2888
+ const lastActivityTime = new Date(session.lastActivity).getTime();
2889
+ const elapsed = Date.now() - lastActivityTime;
2890
+ const remaining = timeoutMs - elapsed;
2891
+ if (remaining <= 0) {
2892
+ console.warn("[Session] Inactivity timeout reached! Resetting conversation immediately.");
2893
+ resetConversationRef.current();
2894
+ return;
2895
+ }
2896
+ timerId = setTimeout(scheduleCheck, remaining);
2897
+ };
2898
+
2899
+ // On initial mount: check immediately (session may already be expired).
2900
+ // On config change: lastActivity was just set to now, so remaining ≈ timeoutMs.
2901
+ scheduleCheck();
2902
+ return () => {
2903
+ if (timerId) clearTimeout(timerId);
2904
+ };
2905
+ // resetConversation intentionally omitted — accessed via resetConversationRef to
2906
+ // prevent the effect from re-running (and resetting lastActivity) on identity changes.
2907
+ // bidiSessionId IS included so the timer restarts after a session reset.
2908
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2909
+ }, [organizationId, inactivityTimeoutValue, inactivityTimeoutUnit, bidiSessionId]);
2785
2910
  const memoizedQuickQuestions = reactExports.useMemo(() => quickQuestions, [quickQuestions]);
2786
2911
 
2787
2912
  // Merge and sort messages from both sources (text and voice) by timestamp
@@ -2838,6 +2963,7 @@
2838
2963
  messages: activeMessages,
2839
2964
  inputValue,
2840
2965
  isLoading,
2966
+ isConversationLoading,
2841
2967
  widgetRef,
2842
2968
  quickQuestions: memoizedQuickQuestions,
2843
2969
  // Bidi/Voice state
@@ -2862,7 +2988,9 @@
2862
2988
  handleDecision,
2863
2989
  // Bidi/Voice actions
2864
2990
  handleVoiceToggle,
2865
- sendBidiTextMessage
2991
+ sendBidiTextMessage,
2992
+ // Session lifecycle
2993
+ resetConversation
2866
2994
  };
2867
2995
  };
2868
2996
 
@@ -3017,7 +3145,7 @@
3017
3145
  */
3018
3146
 
3019
3147
 
3020
- const __iconNode$b = [
3148
+ const __iconNode$a = [
3021
3149
  ["path", { d: "M2 10v3", key: "1fnikh" }],
3022
3150
  ["path", { d: "M6 6v11", key: "11sgs0" }],
3023
3151
  ["path", { d: "M10 3v18", key: "yhl04a" }],
@@ -3025,7 +3153,7 @@
3025
3153
  ["path", { d: "M18 5v13", key: "123xd1" }],
3026
3154
  ["path", { d: "M22 10v3", key: "154ddg" }]
3027
3155
  ];
3028
- const AudioLines = createLucideIcon("audio-lines", __iconNode$b);
3156
+ const AudioLines = createLucideIcon("audio-lines", __iconNode$a);
3029
3157
 
3030
3158
  /**
3031
3159
  * @license lucide-react v0.575.0 - ISC
@@ -3035,12 +3163,12 @@
3035
3163
  */
3036
3164
 
3037
3165
 
3038
- const __iconNode$a = [
3166
+ const __iconNode$9 = [
3039
3167
  ["path", { d: "M12 15V3", key: "m9g1x1" }],
3040
3168
  ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }],
3041
3169
  ["path", { d: "m7 10 5 5 5-5", key: "brsn70" }]
3042
3170
  ];
3043
- const Download = createLucideIcon("download", __iconNode$a);
3171
+ const Download = createLucideIcon("download", __iconNode$9);
3044
3172
 
3045
3173
  /**
3046
3174
  * @license lucide-react v0.575.0 - ISC
@@ -3050,12 +3178,12 @@
3050
3178
  */
3051
3179
 
3052
3180
 
3053
- const __iconNode$9 = [
3181
+ const __iconNode$8 = [
3054
3182
  ["path", { d: "M15 3h6v6", key: "1q9fwt" }],
3055
3183
  ["path", { d: "M10 14 21 3", key: "gplh6r" }],
3056
3184
  ["path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6", key: "a6xqqp" }]
3057
3185
  ];
3058
- const ExternalLink = createLucideIcon("external-link", __iconNode$9);
3186
+ const ExternalLink = createLucideIcon("external-link", __iconNode$8);
3059
3187
 
3060
3188
  /**
3061
3189
  * @license lucide-react v0.575.0 - ISC
@@ -3065,7 +3193,7 @@
3065
3193
  */
3066
3194
 
3067
3195
 
3068
- const __iconNode$8 = [
3196
+ const __iconNode$7 = [
3069
3197
  [
3070
3198
  "path",
3071
3199
  {
@@ -3078,7 +3206,7 @@
3078
3206
  ["path", { d: "M16 13H8", key: "t4e002" }],
3079
3207
  ["path", { d: "M16 17H8", key: "z1uh3a" }]
3080
3208
  ];
3081
- const FileText = createLucideIcon("file-text", __iconNode$8);
3209
+ const FileText = createLucideIcon("file-text", __iconNode$7);
3082
3210
 
3083
3211
  /**
3084
3212
  * @license lucide-react v0.575.0 - ISC
@@ -3088,12 +3216,12 @@
3088
3216
  */
3089
3217
 
3090
3218
 
3091
- const __iconNode$7 = [
3219
+ const __iconNode$6 = [
3092
3220
  ["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
3093
3221
  ["path", { d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20", key: "13o1zl" }],
3094
3222
  ["path", { d: "M2 12h20", key: "9i4pu4" }]
3095
3223
  ];
3096
- const Globe = createLucideIcon("globe", __iconNode$7);
3224
+ const Globe = createLucideIcon("globe", __iconNode$6);
3097
3225
 
3098
3226
  /**
3099
3227
  * @license lucide-react v0.575.0 - ISC
@@ -3103,8 +3231,8 @@
3103
3231
  */
3104
3232
 
3105
3233
 
3106
- const __iconNode$6 = [["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]];
3107
- const LoaderCircle = createLucideIcon("loader-circle", __iconNode$6);
3234
+ const __iconNode$5 = [["path", { d: "M21 12a9 9 0 1 1-6.219-8.56", key: "13zald" }]];
3235
+ const LoaderCircle = createLucideIcon("loader-circle", __iconNode$5);
3108
3236
 
3109
3237
  /**
3110
3238
  * @license lucide-react v0.575.0 - ISC
@@ -3114,7 +3242,7 @@
3114
3242
  */
3115
3243
 
3116
3244
 
3117
- const __iconNode$5 = [
3245
+ const __iconNode$4 = [
3118
3246
  [
3119
3247
  "path",
3120
3248
  {
@@ -3123,7 +3251,7 @@
3123
3251
  }
3124
3252
  ]
3125
3253
  ];
3126
- const MessageCircle = createLucideIcon("message-circle", __iconNode$5);
3254
+ const MessageCircle = createLucideIcon("message-circle", __iconNode$4);
3127
3255
 
3128
3256
  /**
3129
3257
  * @license lucide-react v0.575.0 - ISC
@@ -3133,7 +3261,7 @@
3133
3261
  */
3134
3262
 
3135
3263
 
3136
- const __iconNode$4 = [
3264
+ const __iconNode$3 = [
3137
3265
  [
3138
3266
  "path",
3139
3267
  {
@@ -3143,7 +3271,7 @@
3143
3271
  ],
3144
3272
  ["path", { d: "m21.854 2.147-10.94 10.939", key: "12cjpa" }]
3145
3273
  ];
3146
- const Send = createLucideIcon("send", __iconNode$4);
3274
+ const Send = createLucideIcon("send", __iconNode$3);
3147
3275
 
3148
3276
  /**
3149
3277
  * @license lucide-react v0.575.0 - ISC
@@ -3153,7 +3281,7 @@
3153
3281
  */
3154
3282
 
3155
3283
 
3156
- const __iconNode$3 = [
3284
+ const __iconNode$2 = [
3157
3285
  ["path", { d: "M12 2v2", key: "tus03m" }],
3158
3286
  [
3159
3287
  "path",
@@ -3166,7 +3294,7 @@
3166
3294
  ["path", { d: "m19 5-1.256 1.256", key: "1yg6a6" }],
3167
3295
  ["path", { d: "M20 12h2", key: "1q8mjw" }]
3168
3296
  ];
3169
- const SunMoon = createLucideIcon("sun-moon", __iconNode$3);
3297
+ const SunMoon = createLucideIcon("sun-moon", __iconNode$2);
3170
3298
 
3171
3299
  /**
3172
3300
  * @license lucide-react v0.575.0 - ISC
@@ -3176,7 +3304,7 @@
3176
3304
  */
3177
3305
 
3178
3306
 
3179
- const __iconNode$2 = [
3307
+ const __iconNode$1 = [
3180
3308
  ["circle", { cx: "12", cy: "12", r: "4", key: "4exip2" }],
3181
3309
  ["path", { d: "M12 2v2", key: "tus03m" }],
3182
3310
  ["path", { d: "M12 20v2", key: "1lh1kg" }],
@@ -3187,21 +3315,7 @@
3187
3315
  ["path", { d: "m6.34 17.66-1.41 1.41", key: "1m8zz5" }],
3188
3316
  ["path", { d: "m19.07 4.93-1.41 1.41", key: "1shlcs" }]
3189
3317
  ];
3190
- const Sun = createLucideIcon("sun", __iconNode$2);
3191
-
3192
- /**
3193
- * @license lucide-react v0.575.0 - ISC
3194
- *
3195
- * This source code is licensed under the ISC license.
3196
- * See the LICENSE file in the root directory of this source tree.
3197
- */
3198
-
3199
-
3200
- const __iconNode$1 = [
3201
- ["path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2", key: "975kel" }],
3202
- ["circle", { cx: "12", cy: "7", r: "4", key: "17ys0d" }]
3203
- ];
3204
- const User = createLucideIcon("user", __iconNode$1);
3318
+ const Sun = createLucideIcon("sun", __iconNode$1);
3205
3319
 
3206
3320
  /**
3207
3321
  * @license lucide-react v0.575.0 - ISC
@@ -57678,17 +57792,9 @@
57678
57792
  const messageFontFamily = userMessageBubbleFontFamily !== undefined && userMessageBubbleFontFamily !== null ? userMessageBubbleFontFamily : fontFamily !== undefined && fontFamily !== null ? fontFamily : "Inter";
57679
57793
  const messageFontSize = userMessageBubbleFontSize !== undefined && userMessageBubbleFontSize !== null ? userMessageBubbleFontSize : fontSize !== undefined && fontSize !== null ? fontSize : "14px";
57680
57794
  useGoogleFont(messageFontFamily);
57681
- return /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
57795
+ return /*#__PURE__*/jsxRuntimeExports.jsx("div", {
57682
57796
  className: "flex gap-3 flex-row-reverse animate-in",
57683
- children: [/*#__PURE__*/jsxRuntimeExports.jsx("div", {
57684
- className: "flex-shrink-0 mt-1",
57685
- children: /*#__PURE__*/jsxRuntimeExports.jsx("div", {
57686
- className: "w-8 h-8 rounded-full flex items-center justify-center ".concat(isDark ? "bg-gray-700" : "bg-gray-200"),
57687
- children: /*#__PURE__*/jsxRuntimeExports.jsx(User, {
57688
- className: "w-4 h-4 text-gray-500"
57689
- })
57690
- })
57691
- }), /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
57797
+ children: /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
57692
57798
  className: "flex flex-col items-end max-w-[75%]",
57693
57799
  children: [/*#__PURE__*/jsxRuntimeExports.jsx("div", {
57694
57800
  className: "rounded-2xl px-4 py-2.5 shadow-lg ".concat(isRtl ? "rounded-tl-sm" : "rounded-tr-sm"),
@@ -57717,7 +57823,7 @@
57717
57823
  dir: "auto",
57718
57824
  children: formatTime(message.timestamp)
57719
57825
  })]
57720
- })]
57826
+ })
57721
57827
  });
57722
57828
  };
57723
57829
  const MessageBubble = _ref19 => {
@@ -58513,10 +58619,12 @@
58513
58619
  companyName,
58514
58620
  apiBaseUrl,
58515
58621
  organizationId,
58516
- onPromptSuggestionClick
58622
+ onPromptSuggestionClick,
58623
+ conversationResetKey
58517
58624
  } = _ref;
58518
58625
  const messagesEndRef = reactExports.useRef(null);
58519
58626
  const shouldAutoScrollRef = reactExports.useRef(true);
58627
+ const hasInitialScrolledRef = reactExports.useRef(false);
58520
58628
  const autoScrollKey = reactExports.useMemo(() => messages.map(message => {
58521
58629
  var _message$content$leng, _message$content;
58522
58630
  return "".concat(message.id, ":").concat((_message$content$leng = (_message$content = message.content) === null || _message$content === void 0 ? void 0 : _message$content.length) !== null && _message$content$leng !== void 0 ? _message$content$leng : 0, ":").concat(message.isProcessing ? 1 : 0);
@@ -58542,6 +58650,38 @@
58542
58650
  container.removeEventListener("scroll", updateAutoScrollIntent);
58543
58651
  };
58544
58652
  }, [updateAutoScrollIntent, messagesContainerRef]);
58653
+
58654
+ // When the conversation resets (new session ID), unlock initial scroll so we
58655
+ // jump to the bottom when the welcome message appears.
58656
+ reactExports.useEffect(() => {
58657
+ if (!conversationResetKey) return;
58658
+ shouldAutoScrollRef.current = true;
58659
+ hasInitialScrolledRef.current = false;
58660
+ }, [conversationResetKey]);
58661
+
58662
+ // Force instant scroll to bottom on first message load (covers history restore + post-reset)
58663
+ reactExports.useEffect(() => {
58664
+ if (hasInitialScrolledRef.current || messages.length === 0 || typeof window === "undefined") {
58665
+ return undefined;
58666
+ }
58667
+ hasInitialScrolledRef.current = true;
58668
+ let frame2;
58669
+ const frame1 = window.requestAnimationFrame(() => {
58670
+ frame2 = window.requestAnimationFrame(() => {
58671
+ var _messagesEndRef$curre;
58672
+ (_messagesEndRef$curre = messagesEndRef.current) === null || _messagesEndRef$curre === void 0 || _messagesEndRef$curre.scrollIntoView({
58673
+ behavior: "instant",
58674
+ block: "end"
58675
+ });
58676
+ shouldAutoScrollRef.current = true;
58677
+ });
58678
+ });
58679
+ return () => {
58680
+ window.cancelAnimationFrame(frame1);
58681
+ window.cancelAnimationFrame(frame2);
58682
+ };
58683
+ // eslint-disable-next-line react-hooks/exhaustive-deps
58684
+ }, [messages.length === 0]);
58545
58685
  reactExports.useEffect(() => {
58546
58686
  if (!shouldAutoScrollRef.current) {
58547
58687
  return undefined;
@@ -58550,8 +58690,8 @@
58550
58690
  return undefined;
58551
58691
  }
58552
58692
  const frame = window.requestAnimationFrame(() => {
58553
- var _messagesEndRef$curre;
58554
- (_messagesEndRef$curre = messagesEndRef.current) === null || _messagesEndRef$curre === void 0 || _messagesEndRef$curre.scrollIntoView({
58693
+ var _messagesEndRef$curre2;
58694
+ (_messagesEndRef$curre2 = messagesEndRef.current) === null || _messagesEndRef$curre2 === void 0 || _messagesEndRef$curre2.scrollIntoView({
58555
58695
  behavior: "smooth",
58556
58696
  block: "end"
58557
58697
  });
@@ -58703,6 +58843,7 @@
58703
58843
  isOpen,
58704
58844
  isMinimized,
58705
58845
  isLoading,
58846
+ isConversationLoading = false,
58706
58847
  placeholder,
58707
58848
  textColor,
58708
58849
  fontFamily,
@@ -58778,13 +58919,13 @@
58778
58919
  const handleChange = event => {
58779
58920
  setInputValue(event.target.value);
58780
58921
  };
58781
- const isDisabled = !inputValue.trim() || isLoading;
58922
+ const isDisabled = !inputValue.trim() || isLoading || isConversationLoading;
58782
58923
  const separatorColor = isDark ? "#374151" : "#e5e7eb";
58783
58924
 
58784
58925
  // Check if onboarding flow exists and is not completed
58785
58926
  const hasOnboardingQuestions = onboardingQuestions && onboardingQuestions.length > 0;
58786
58927
  const isOnboardingIncomplete = hasOnboardingQuestions && !onboardingCompleted;
58787
- const isVoiceButtonDisabled = csatVisible || onboardingEnabled && isOnboardingIncomplete || voiceStatus === "connecting";
58928
+ const isVoiceButtonDisabled = csatVisible || isConversationLoading || onboardingEnabled && isOnboardingIncomplete || voiceStatus === "connecting";
58788
58929
 
58789
58930
  // Determine tooltip message
58790
58931
  const getTooltipMessage = () => {
@@ -58965,9 +59106,9 @@
58965
59106
  onFocus: () => setIsFocused(true),
58966
59107
  onBlur: () => setIsFocused(false),
58967
59108
  onKeyDown: handleKeyPress,
58968
- placeholder: placeholder || "Type your message...",
59109
+ placeholder: isConversationLoading ? "Agent is loading..." : placeholder || "Type your message...",
58969
59110
  rows: 1,
58970
- disabled: csatVisible || isLoading || isVoiceSessionActive && voiceStatus !== "connected",
59111
+ disabled: csatVisible || isConversationLoading || isLoading || isVoiceSessionActive && voiceStatus !== "connected",
58971
59112
  dir: isRtl ? "rtl" : "ltr",
58972
59113
  className: "w-full px-4 bg-transparent rounded-xl leading-normal ".concat(isDark ? "text-gray-100" : "text-gray-900", " placeholder-gray-400 resize-none outline-none scrollbar-hide"),
58973
59114
  style: {
@@ -59722,6 +59863,7 @@
59722
59863
  companyLogo,
59723
59864
  messages,
59724
59865
  isLoading,
59866
+ isConversationLoading = false,
59725
59867
  inputValue,
59726
59868
  setInputValue,
59727
59869
  onSend,
@@ -59774,12 +59916,30 @@
59774
59916
  csatTriggerType = "ON_END",
59775
59917
  csatIdleTimeoutMins = 30,
59776
59918
  onPromptSuggestionClick,
59777
- onboardingEnabled = false
59919
+ onboardingEnabled = false,
59920
+ onResetConversation
59778
59921
  } = _ref;
59779
59922
  useGoogleFont(fontFamily);
59780
59923
  const messagesContainerRef = reactExports.useRef(null);
59781
59924
  const [showCsat, setShowCsat] = reactExports.useState(false);
59782
- const [csatSubmitted, setCsatSubmitted] = reactExports.useState(false);
59925
+ const [csatSubmitted, setCsatSubmitted] = reactExports.useState(() => {
59926
+ try {
59927
+ var _getSession$csatSubmi, _getSession;
59928
+ return (_getSession$csatSubmi = (_getSession = getSession(organizationId)) === null || _getSession === void 0 ? void 0 : _getSession.csatSubmitted) !== null && _getSession$csatSubmi !== void 0 ? _getSession$csatSubmi : false;
59929
+ } catch (_unused) {
59930
+ return false;
59931
+ }
59932
+ });
59933
+
59934
+ // Sync csatSubmitted from session whenever the conversation changes
59935
+ reactExports.useEffect(() => {
59936
+ try {
59937
+ var _getSession$csatSubmi2, _getSession2;
59938
+ const submitted = (_getSession$csatSubmi2 = (_getSession2 = getSession(organizationId)) === null || _getSession2 === void 0 ? void 0 : _getSession2.csatSubmitted) !== null && _getSession$csatSubmi2 !== void 0 ? _getSession$csatSubmi2 : false;
59939
+ setCsatSubmitted(submitted);
59940
+ if (submitted) setShowCsat(false);
59941
+ } catch (_unused2) {}
59942
+ }, [conversationId, organizationId]);
59783
59943
  // const [lastUserMessageTime, setLastUserMessageTime] = useState(null);
59784
59944
 
59785
59945
  // Auto-scroll hook
@@ -59868,6 +60028,8 @@
59868
60028
  const handleCsatRating = async (rating, format) => {
59869
60029
  setCsatSubmitted(true); // optimistic UI
59870
60030
  setShowCsat(false);
60031
+ markCsatSubmitted(organizationId); // persist so page refresh doesn't re-show CSAT
60032
+
59871
60033
  try {
59872
60034
  const headers = {
59873
60035
  "Content-Type": "application/json"
@@ -59890,6 +60052,8 @@
59890
60052
  if (!response.ok) {
59891
60053
  console.error("CSAT submission failed:", await response.text());
59892
60054
  }
60055
+
60056
+ // Session continues after CSAT submission — no reset
59893
60057
  } catch (error) {
59894
60058
  console.error("CSAT submission error:", error);
59895
60059
  }
@@ -59988,7 +60152,8 @@
59988
60152
  conciergeName: conciergeName,
59989
60153
  companyName: companyName,
59990
60154
  apiBaseUrl: apiBaseUrl,
59991
- organizationId: organizationId
60155
+ organizationId: organizationId,
60156
+ conversationResetKey: conversationId
59992
60157
  }), showCsat && csatEnabled && !csatSubmitted && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
59993
60158
  className: "",
59994
60159
  children: /*#__PURE__*/jsxRuntimeExports.jsx(CSATWidget, {
@@ -60018,6 +60183,7 @@
60018
60183
  isOpen: isOpen,
60019
60184
  isMinimized: isMinimized,
60020
60185
  isLoading: isLoading,
60186
+ isConversationLoading: isConversationLoading,
60021
60187
  placeholder: placeholder,
60022
60188
  textColor: resolvedTextColor,
60023
60189
  fontFamily: fontFamily,
@@ -60083,64 +60249,6 @@
60083
60249
  });
60084
60250
  };
60085
60251
 
60086
- const createSession = async config => {
60087
- // Initialize chat session if needed
60088
- if (config.sessionUrl) {
60089
- try {
60090
- const response = await fetch(config.sessionUrl, {
60091
- method: "POST",
60092
- headers: _objectSpread2({
60093
- "Content-Type": "application/json"
60094
- }, config.apiKey && {
60095
- Authorization: "Bearer ".concat(config.apiKey)
60096
- }),
60097
- body: JSON.stringify({
60098
- userId: config.userId,
60099
- userName: config.userName,
60100
- userEmail: config.userEmail
60101
- }),
60102
- credentials: "include"
60103
- });
60104
- const data = await response.json();
60105
- return data.sessionId;
60106
- } catch (error) {
60107
- console.error("Session creation error:", error);
60108
- return null;
60109
- }
60110
- }
60111
- return null;
60112
- };
60113
-
60114
- /**
60115
- * Fetches agent configuration from the deploy-agent endpoint
60116
- * @param {string} apiBaseUrl - Base URL for the API
60117
- * @param {string} orgId - Organization ID
60118
- * @returns {Promise<object>} Agent configuration object
60119
- */
60120
- const fetchAgentConfig = async (apiBaseUrl, orgId) => {
60121
- if (!apiBaseUrl || !orgId) {
60122
- throw new Error("apiBaseUrl and orgId are required");
60123
- }
60124
- try {
60125
- const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/deploy-agent/").concat(orgId);
60126
- const response = await fetch(url, {
60127
- method: "GET",
60128
- headers: {
60129
- "Content-Type": "application/json"
60130
- },
60131
- credentials: "include"
60132
- });
60133
- if (!response.ok) {
60134
- throw new Error("HTTP error! status: ".concat(response.status));
60135
- }
60136
- const data = await response.json();
60137
- return data;
60138
- } catch (error) {
60139
- console.error("Failed to fetch agent configuration:", error);
60140
- throw error;
60141
- }
60142
- };
60143
-
60144
60252
  const ChatWidget = _ref => {
60145
60253
  var _agentConfig$concierg;
60146
60254
  let {
@@ -60215,7 +60323,7 @@
60215
60323
 
60216
60324
  // Map API response to widget props (use props as overrides if provided)
60217
60325
  const widgetConfig = reactExports.useMemo(() => {
60218
- var _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref0, _ref1, _ref10, _ref11, _ref12, _ref13, _ref14, _ref15, _ref16, _ref17, _ref18, _ref19, _ref20, _ref21, _ref22, _ref23, _ref24, _ref25, _ref26, _ref27, _ref28, _ref29, _ref30, _ref31, _ref32, _ref33, _ref34, _ref35, _ref36, _agentConfig$csatEnab, _agentConfig$csatForm, _agentConfig$csatProm, _agentConfig$csatTrig, _agentConfig$csatIdle, _agentConfig$csatFoll, _agentConfig$csatFoll2;
60326
+ var _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref0, _ref1, _ref10, _ref11, _ref12, _ref13, _ref14, _ref15, _ref16, _ref17, _ref18, _ref19, _ref20, _ref21, _ref22, _ref23, _ref24, _agentConfig$inactivi, _agentConfig$inactivi2, _ref25, _ref26, _ref27, _ref28, _ref29, _ref30, _ref31, _ref32, _ref33, _ref34, _ref35, _ref36, _agentConfig$csatEnab, _agentConfig$csatForm, _agentConfig$csatProm, _agentConfig$csatTrig, _agentConfig$csatIdle, _agentConfig$csatFoll, _agentConfig$csatFoll2;
60219
60327
  if (!agentConfig) {
60220
60328
  return null;
60221
60329
  }
@@ -60249,6 +60357,8 @@
60249
60357
  allowedDomains: finalAllowedDomains,
60250
60358
  launcherPosition: (_ref23 = position !== null && position !== void 0 ? position : agentConfig.launcherPosition) !== null && _ref23 !== void 0 ? _ref23 : "bottom-right",
60251
60359
  onboardingEnabled: (_ref24 = onboardingEnabled !== null && onboardingEnabled !== void 0 ? onboardingEnabled : agentConfig.onboardingEnabled) !== null && _ref24 !== void 0 ? _ref24 : false,
60360
+ inactivityTimeoutValue: (_agentConfig$inactivi = agentConfig.inactivityTimeoutValue) !== null && _agentConfig$inactivi !== void 0 ? _agentConfig$inactivi : null,
60361
+ inactivityTimeoutUnit: (_agentConfig$inactivi2 = agentConfig.inactivityTimeoutUnit) !== null && _agentConfig$inactivi2 !== void 0 ? _agentConfig$inactivi2 : null,
60252
60362
  agentMessageBubbleFontFamily: (_ref25 = (_ref26 = (_ref27 = agentMessageBubbleFontFamily !== null && agentMessageBubbleFontFamily !== void 0 ? agentMessageBubbleFontFamily : agentConfig.agentMessageBubbleFontFamily) !== null && _ref27 !== void 0 ? _ref27 : fontFamily) !== null && _ref26 !== void 0 ? _ref26 : agentConfig.fontFamily) !== null && _ref25 !== void 0 ? _ref25 : "Inter",
60253
60363
  agentMessageBubbleFontSize: (_ref28 = (_ref29 = (_ref30 = agentMessageBubbleFontSize !== null && agentMessageBubbleFontSize !== void 0 ? agentMessageBubbleFontSize : agentConfig.agentMessageBubbleFontSize) !== null && _ref30 !== void 0 ? _ref30 : fontSize) !== null && _ref29 !== void 0 ? _ref29 : agentConfig.fontSize) !== null && _ref28 !== void 0 ? _ref28 : "14px",
60254
60364
  userMessageBubbleFontFamily: (_ref31 = (_ref32 = (_ref33 = userMessageBubbleFontFamily !== null && userMessageBubbleFontFamily !== void 0 ? userMessageBubbleFontFamily : agentConfig.userMessageBubbleFontFamily) !== null && _ref33 !== void 0 ? _ref33 : fontFamily) !== null && _ref32 !== void 0 ? _ref32 : agentConfig.fontFamily) !== null && _ref31 !== void 0 ? _ref31 : "Inter",
@@ -60299,6 +60409,7 @@
60299
60409
  messages,
60300
60410
  inputValue,
60301
60411
  isLoading,
60412
+ isConversationLoading,
60302
60413
  widgetRef,
60303
60414
  setInputValue,
60304
60415
  handleSend,
@@ -60318,7 +60429,8 @@
60318
60429
  onboardingActive,
60319
60430
  onboardingCompleted,
60320
60431
  handleOnboardingFormSubmit,
60321
- conversationId
60432
+ conversationId,
60433
+ resetConversation
60322
60434
  } = useChatState(widgetConfig ? {
60323
60435
  welcomeMessage: widgetConfig.welcomeMessage,
60324
60436
  quickQuestions: widgetConfig.quickQuestions,
@@ -60331,7 +60443,9 @@
60331
60443
  enableVoiceInteraction: widgetConfig.enableVoiceInteraction,
60332
60444
  onboardingQuestions: widgetConfig.onboardingQuestions,
60333
60445
  onboardingEnabled: widgetConfig.onboardingEnabled,
60334
- collectionPrompt: widgetConfig.collectionPrompt
60446
+ collectionPrompt: widgetConfig.collectionPrompt,
60447
+ inactivityTimeoutValue: widgetConfig.inactivityTimeoutValue,
60448
+ inactivityTimeoutUnit: widgetConfig.inactivityTimeoutUnit
60335
60449
  } : defaultConfig);
60336
60450
 
60337
60451
  // Set CSS variables for theming (must be called before early returns)
@@ -60430,7 +60544,8 @@
60430
60544
  csatPrompt: widgetConfig.csatPrompt,
60431
60545
  csatTriggerType: widgetConfig.csatTriggerType,
60432
60546
  csatIdleTimeoutMins: widgetConfig.csatIdleTimeoutMins,
60433
- onboardingEnabled: widgetConfig.onboardingEnabled
60547
+ onboardingEnabled: widgetConfig.onboardingEnabled,
60548
+ onResetConversation: resetConversation
60434
60549
  }), !isOpen && /*#__PURE__*/jsxRuntimeExports.jsx(ToggleButton, {
60435
60550
  isOpen: isOpen,
60436
60551
  isDark: isDark,