@eshal-bot/chat-widget 0.1.21 → 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;
1828
-
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
- }
1960
+ if (!trimmedInput) return;
1852
1961
 
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
- }
1884
-
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
- }
1985
+ if (!trimmedQuestion) return;
1911
1986
 
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
 
@@ -58491,10 +58619,12 @@
58491
58619
  companyName,
58492
58620
  apiBaseUrl,
58493
58621
  organizationId,
58494
- onPromptSuggestionClick
58622
+ onPromptSuggestionClick,
58623
+ conversationResetKey
58495
58624
  } = _ref;
58496
58625
  const messagesEndRef = reactExports.useRef(null);
58497
58626
  const shouldAutoScrollRef = reactExports.useRef(true);
58627
+ const hasInitialScrolledRef = reactExports.useRef(false);
58498
58628
  const autoScrollKey = reactExports.useMemo(() => messages.map(message => {
58499
58629
  var _message$content$leng, _message$content;
58500
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);
@@ -58520,6 +58650,38 @@
58520
58650
  container.removeEventListener("scroll", updateAutoScrollIntent);
58521
58651
  };
58522
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]);
58523
58685
  reactExports.useEffect(() => {
58524
58686
  if (!shouldAutoScrollRef.current) {
58525
58687
  return undefined;
@@ -58528,8 +58690,8 @@
58528
58690
  return undefined;
58529
58691
  }
58530
58692
  const frame = window.requestAnimationFrame(() => {
58531
- var _messagesEndRef$curre;
58532
- (_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({
58533
58695
  behavior: "smooth",
58534
58696
  block: "end"
58535
58697
  });
@@ -58681,6 +58843,7 @@
58681
58843
  isOpen,
58682
58844
  isMinimized,
58683
58845
  isLoading,
58846
+ isConversationLoading = false,
58684
58847
  placeholder,
58685
58848
  textColor,
58686
58849
  fontFamily,
@@ -58756,13 +58919,13 @@
58756
58919
  const handleChange = event => {
58757
58920
  setInputValue(event.target.value);
58758
58921
  };
58759
- const isDisabled = !inputValue.trim() || isLoading;
58922
+ const isDisabled = !inputValue.trim() || isLoading || isConversationLoading;
58760
58923
  const separatorColor = isDark ? "#374151" : "#e5e7eb";
58761
58924
 
58762
58925
  // Check if onboarding flow exists and is not completed
58763
58926
  const hasOnboardingQuestions = onboardingQuestions && onboardingQuestions.length > 0;
58764
58927
  const isOnboardingIncomplete = hasOnboardingQuestions && !onboardingCompleted;
58765
- const isVoiceButtonDisabled = csatVisible || onboardingEnabled && isOnboardingIncomplete || voiceStatus === "connecting";
58928
+ const isVoiceButtonDisabled = csatVisible || isConversationLoading || onboardingEnabled && isOnboardingIncomplete || voiceStatus === "connecting";
58766
58929
 
58767
58930
  // Determine tooltip message
58768
58931
  const getTooltipMessage = () => {
@@ -58943,9 +59106,9 @@
58943
59106
  onFocus: () => setIsFocused(true),
58944
59107
  onBlur: () => setIsFocused(false),
58945
59108
  onKeyDown: handleKeyPress,
58946
- placeholder: placeholder || "Type your message...",
59109
+ placeholder: isConversationLoading ? "Agent is loading..." : placeholder || "Type your message...",
58947
59110
  rows: 1,
58948
- disabled: csatVisible || isLoading || isVoiceSessionActive && voiceStatus !== "connected",
59111
+ disabled: csatVisible || isConversationLoading || isLoading || isVoiceSessionActive && voiceStatus !== "connected",
58949
59112
  dir: isRtl ? "rtl" : "ltr",
58950
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"),
58951
59114
  style: {
@@ -59700,6 +59863,7 @@
59700
59863
  companyLogo,
59701
59864
  messages,
59702
59865
  isLoading,
59866
+ isConversationLoading = false,
59703
59867
  inputValue,
59704
59868
  setInputValue,
59705
59869
  onSend,
@@ -59752,12 +59916,30 @@
59752
59916
  csatTriggerType = "ON_END",
59753
59917
  csatIdleTimeoutMins = 30,
59754
59918
  onPromptSuggestionClick,
59755
- onboardingEnabled = false
59919
+ onboardingEnabled = false,
59920
+ onResetConversation
59756
59921
  } = _ref;
59757
59922
  useGoogleFont(fontFamily);
59758
59923
  const messagesContainerRef = reactExports.useRef(null);
59759
59924
  const [showCsat, setShowCsat] = reactExports.useState(false);
59760
- 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]);
59761
59943
  // const [lastUserMessageTime, setLastUserMessageTime] = useState(null);
59762
59944
 
59763
59945
  // Auto-scroll hook
@@ -59846,6 +60028,8 @@
59846
60028
  const handleCsatRating = async (rating, format) => {
59847
60029
  setCsatSubmitted(true); // optimistic UI
59848
60030
  setShowCsat(false);
60031
+ markCsatSubmitted(organizationId); // persist so page refresh doesn't re-show CSAT
60032
+
59849
60033
  try {
59850
60034
  const headers = {
59851
60035
  "Content-Type": "application/json"
@@ -59868,6 +60052,8 @@
59868
60052
  if (!response.ok) {
59869
60053
  console.error("CSAT submission failed:", await response.text());
59870
60054
  }
60055
+
60056
+ // Session continues after CSAT submission — no reset
59871
60057
  } catch (error) {
59872
60058
  console.error("CSAT submission error:", error);
59873
60059
  }
@@ -59966,7 +60152,8 @@
59966
60152
  conciergeName: conciergeName,
59967
60153
  companyName: companyName,
59968
60154
  apiBaseUrl: apiBaseUrl,
59969
- organizationId: organizationId
60155
+ organizationId: organizationId,
60156
+ conversationResetKey: conversationId
59970
60157
  }), showCsat && csatEnabled && !csatSubmitted && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
59971
60158
  className: "",
59972
60159
  children: /*#__PURE__*/jsxRuntimeExports.jsx(CSATWidget, {
@@ -59996,6 +60183,7 @@
59996
60183
  isOpen: isOpen,
59997
60184
  isMinimized: isMinimized,
59998
60185
  isLoading: isLoading,
60186
+ isConversationLoading: isConversationLoading,
59999
60187
  placeholder: placeholder,
60000
60188
  textColor: resolvedTextColor,
60001
60189
  fontFamily: fontFamily,
@@ -60061,64 +60249,6 @@
60061
60249
  });
60062
60250
  };
60063
60251
 
60064
- const createSession = async config => {
60065
- // Initialize chat session if needed
60066
- if (config.sessionUrl) {
60067
- try {
60068
- const response = await fetch(config.sessionUrl, {
60069
- method: "POST",
60070
- headers: _objectSpread2({
60071
- "Content-Type": "application/json"
60072
- }, config.apiKey && {
60073
- Authorization: "Bearer ".concat(config.apiKey)
60074
- }),
60075
- body: JSON.stringify({
60076
- userId: config.userId,
60077
- userName: config.userName,
60078
- userEmail: config.userEmail
60079
- }),
60080
- credentials: "include"
60081
- });
60082
- const data = await response.json();
60083
- return data.sessionId;
60084
- } catch (error) {
60085
- console.error("Session creation error:", error);
60086
- return null;
60087
- }
60088
- }
60089
- return null;
60090
- };
60091
-
60092
- /**
60093
- * Fetches agent configuration from the deploy-agent endpoint
60094
- * @param {string} apiBaseUrl - Base URL for the API
60095
- * @param {string} orgId - Organization ID
60096
- * @returns {Promise<object>} Agent configuration object
60097
- */
60098
- const fetchAgentConfig = async (apiBaseUrl, orgId) => {
60099
- if (!apiBaseUrl || !orgId) {
60100
- throw new Error("apiBaseUrl and orgId are required");
60101
- }
60102
- try {
60103
- const url = "".concat(apiBaseUrl.replace(/\/$/, ''), "/api/v1/deploy-agent/").concat(orgId);
60104
- const response = await fetch(url, {
60105
- method: "GET",
60106
- headers: {
60107
- "Content-Type": "application/json"
60108
- },
60109
- credentials: "include"
60110
- });
60111
- if (!response.ok) {
60112
- throw new Error("HTTP error! status: ".concat(response.status));
60113
- }
60114
- const data = await response.json();
60115
- return data;
60116
- } catch (error) {
60117
- console.error("Failed to fetch agent configuration:", error);
60118
- throw error;
60119
- }
60120
- };
60121
-
60122
60252
  const ChatWidget = _ref => {
60123
60253
  var _agentConfig$concierg;
60124
60254
  let {
@@ -60193,7 +60323,7 @@
60193
60323
 
60194
60324
  // Map API response to widget props (use props as overrides if provided)
60195
60325
  const widgetConfig = reactExports.useMemo(() => {
60196
- 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;
60197
60327
  if (!agentConfig) {
60198
60328
  return null;
60199
60329
  }
@@ -60227,6 +60357,8 @@
60227
60357
  allowedDomains: finalAllowedDomains,
60228
60358
  launcherPosition: (_ref23 = position !== null && position !== void 0 ? position : agentConfig.launcherPosition) !== null && _ref23 !== void 0 ? _ref23 : "bottom-right",
60229
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,
60230
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",
60231
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",
60232
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",
@@ -60277,6 +60409,7 @@
60277
60409
  messages,
60278
60410
  inputValue,
60279
60411
  isLoading,
60412
+ isConversationLoading,
60280
60413
  widgetRef,
60281
60414
  setInputValue,
60282
60415
  handleSend,
@@ -60296,7 +60429,8 @@
60296
60429
  onboardingActive,
60297
60430
  onboardingCompleted,
60298
60431
  handleOnboardingFormSubmit,
60299
- conversationId
60432
+ conversationId,
60433
+ resetConversation
60300
60434
  } = useChatState(widgetConfig ? {
60301
60435
  welcomeMessage: widgetConfig.welcomeMessage,
60302
60436
  quickQuestions: widgetConfig.quickQuestions,
@@ -60309,7 +60443,9 @@
60309
60443
  enableVoiceInteraction: widgetConfig.enableVoiceInteraction,
60310
60444
  onboardingQuestions: widgetConfig.onboardingQuestions,
60311
60445
  onboardingEnabled: widgetConfig.onboardingEnabled,
60312
- collectionPrompt: widgetConfig.collectionPrompt
60446
+ collectionPrompt: widgetConfig.collectionPrompt,
60447
+ inactivityTimeoutValue: widgetConfig.inactivityTimeoutValue,
60448
+ inactivityTimeoutUnit: widgetConfig.inactivityTimeoutUnit
60313
60449
  } : defaultConfig);
60314
60450
 
60315
60451
  // Set CSS variables for theming (must be called before early returns)
@@ -60408,7 +60544,8 @@
60408
60544
  csatPrompt: widgetConfig.csatPrompt,
60409
60545
  csatTriggerType: widgetConfig.csatTriggerType,
60410
60546
  csatIdleTimeoutMins: widgetConfig.csatIdleTimeoutMins,
60411
- onboardingEnabled: widgetConfig.onboardingEnabled
60547
+ onboardingEnabled: widgetConfig.onboardingEnabled,
60548
+ onResetConversation: resetConversation
60412
60549
  }), !isOpen && /*#__PURE__*/jsxRuntimeExports.jsx(ToggleButton, {
60413
60550
  isOpen: isOpen,
60414
60551
  isDark: isDark,