@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.
- package/dist/chat-widget.esm.js +5 -5
- package/dist/chat-widget.js +587 -450
- package/dist/chat-widget.min.js +7 -7
- package/dist/chat-widget.umd.js +5 -5
- package/dist/index.d.ts +4 -3
- package/package.json +1 -1
package/dist/chat-widget.js
CHANGED
|
@@ -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:
|
|
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(
|
|
1403
|
-
|
|
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);
|
|
1416
|
-
const [bidiSessionId] = reactExports.useState(() =>
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
}, [
|
|
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
|
-
//
|
|
1987
|
+
// Quick questions are only visible after onboarding is complete (input is hidden during onboarding)
|
|
1913
1988
|
await sendMessage(trimmedQuestion);
|
|
1914
|
-
}, [
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2548
|
-
var _event$
|
|
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$
|
|
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$
|
|
58532
|
-
(_messagesEndRef$
|
|
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(
|
|
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,
|