| @@ -265,29 +265,48 @@ | ||
| 265 | 265 | return `<h${level} class="chat-heading">${content}</h${level}>`; |
| 266 | 266 | }); |
| 267 | 267 | } |
| 268 | 268 | |
| 269 | -// Update the linkify function to handle both URLs and markdown | |
| 269 | +// Update the linkify function to handle URLs, markdown, and phone numbers | |
| 270 | 270 | function linkify(inputText) { |
| 271 | - // First process markdown headers | |
| 271 | + if (!inputText) return ''; | |
| 272 | + | |
| 273 | + // Process markdown headers | |
| 272 | 274 | let processedText = formatMarkdownHeaders(inputText); |
| 273 | 275 | |
| 274 | - // Then process links as before | |
| 275 | - var markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g; | |
| 276 | - processedText = processedText.replace(markdownLinkPattern, '<a href="$2" target="' + linkTarget + '">$1</a>'); | |
| 276 | + // Process markdown links | |
| 277 | + const markdownLinkPattern = /\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g; | |
| 278 | + processedText = processedText.replace(markdownLinkPattern, (match, text, url) => { | |
| 279 | + const safeUrl = encodeURI(url); | |
| 280 | + const safeText = sanitizeUserInput(text); | |
| 281 | + return `<a href="${safeUrl}" target="${linkTarget}">${safeText}</a>`; | |
| 282 | + }); | |
| 277 | 283 | |
| 278 | - // Replace standalone URLs not already in an <a> tag | |
| 279 | - var urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)/gim; | |
| 280 | - processedText = processedText.replace(urlPattern, '$1<a href="$2" target="' + linkTarget + '">$2</a>'); | |
| 284 | + // Process phone numbers (tel:) | |
| 285 | + const phonePattern = /\[([^\]]+)\]\((tel:[\d+]+)\)/g; | |
| 286 | + processedText = processedText.replace(phonePattern, (match, text, phone) => { | |
| 287 | + const safePhone = encodeURI(phone); | |
| 288 | + const safeText = sanitizeUserInput(text); | |
| 289 | + return `<a href="${safePhone}">${safeText}</a>`; | |
| 290 | + }); | |
| 281 | 291 | |
| 282 | - // Replace "www." prefixed URLs not already in an <a> tag | |
| 283 | - var wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim; | |
| 284 | - processedText = processedText.replace(wwwPattern, '$1<a href="http://$2" target="' + linkTarget + '">$2</a>'); | |
| 292 | + // Process standalone URLs | |
| 293 | + const urlPattern = /(^|[^">])(https?:\/\/[^\s<]+)/gim; | |
| 294 | + processedText = processedText.replace(urlPattern, (match, prefix, url) => { | |
| 295 | + const safeUrl = encodeURI(url); | |
| 296 | + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`; | |
| 297 | + }); | |
| 285 | 298 | |
| 299 | + // Process www. URLs | |
| 300 | + const wwwPattern = /(^|[^">])(www\.[\S]+(\b|$))(?![^<]*<\/a>)/gim; | |
| 301 | + processedText = processedText.replace(wwwPattern, (match, prefix, url) => { | |
| 302 | + const safeUrl = encodeURI(`http://${url}`); | |
| 303 | + return `${prefix}<a href="${safeUrl}" target="${linkTarget}">${url}</a>`; | |
| 304 | + }); | |
| 305 | + | |
| 286 | 306 | return processedText; |
| 287 | 307 | } |
| 288 | 308 | |
| 289 | - | |
| 290 | 309 | function scrollElementToTop(element) { |
| 291 | 310 | var chatBox = $('#chat-box'); |
| 292 | 311 | var elementTop = element.position().top + chatBox.scrollTop(); |
| 293 | 312 | chatBox.animate({ scrollTop: elementTop }, 500); |
| @@ -354,11 +373,17 @@ | ||
| 354 | 373 | |
| 355 | 374 | function updateChatModeIndicator(mode) { |
| 356 | 375 | const indicator = document.getElementById('chat-mode-indicator'); |
| 357 | 376 | if (indicator) { |
| 358 | - indicator.textContent = mode === 'agent' ? 'Live Agent' : 'AI Agent'; | |
| 377 | + // For Live Agent, keep as is; for AI mode, use the customized text | |
| 378 | + if (mode === 'agent') { | |
| 379 | + indicator.textContent = 'Live Agent'; | |
| 380 | + } else { | |
| 381 | + // Get the custom AI agent text from a data attribute we'll add to the element | |
| 382 | + const customAiText = indicator.getAttribute('data-ai-text') || 'AI Agent'; | |
| 383 | + indicator.textContent = customAiText; | |
| 384 | + } | |
| 359 | 385 | } |
| 360 | - | |
| 361 | 386 | // Start or stop polling based on mode |
| 362 | 387 | if (mode === 'agent') { |
| 363 | 388 | startPolling(); |
| 364 | 389 | } else { |
| @@ -390,8 +415,20 @@ | ||
| 390 | 415 | if (response.data && response.data.filename) { |
| 391 | 416 | showActivePdf(response.data.filename); |
| 392 | 417 | activePdfFile = response.data.filename; |
| 393 | 418 | } |
| 419 | + | |
| 420 | + // Add redirect check here | |
| 421 | + if (response.redirect_url) { | |
| 422 | + let responseText = response.text || ''; | |
| 423 | + if (responseText) { | |
| 424 | + replaceLastMessage("bot", responseText); | |
| 425 | + } | |
| 426 | + setTimeout(() => { | |
| 427 | + window.location.href = response.redirect_url; | |
| 428 | + }, 1500); | |
| 429 | + return; | |
| 430 | + } | |
| 394 | 431 | |
| 395 | 432 | |
| 396 | 433 | // Check for live agent response |
| 397 | 434 | if (response.success && response.data && response.data.status === 'waiting_for_agent') { |
| @@ -442,11 +479,17 @@ | ||
| 442 | 479 | } |
| 443 | 480 | }); |
| 444 | 481 | } |
| 445 | 482 | |
| 483 | +// Sanitize only user input | |
| 484 | +function sanitizeUserInput(text) { | |
| 485 | + const div = document.createElement('div'); | |
| 486 | + div.textContent = text; | |
| 487 | + return div.innerHTML; | |
| 488 | +} | |
| 489 | + | |
| 490 | +// Modified appendMessage function that only sanitizes user content | |
| 446 | 491 | function appendMessage(sender, messageText = '', messageHtml = '', images = [], isTemporary = false) { |
| 447 | - //console.log("Appending message. Sender:", sender, "Content:", messageText); | |
| 448 | - | |
| 449 | 492 | try { |
| 450 | 493 | // Determine styles based on sender type |
| 451 | 494 | let messageClass, bgColor, fontColor; |
| 452 | 495 | |
| @@ -453,42 +496,52 @@ | ||
| 453 | 496 | if (sender === "user") { |
| 454 | 497 | messageClass = "user-message"; |
| 455 | 498 | bgColor = userMessageBgColor; |
| 456 | 499 | fontColor = userMessageFontColor; |
| 457 | - } else if (sender === "agent") { | |
| 458 | - messageClass = "agent-message"; | |
| 459 | - bgColor = liveAgentMessageBgColor; | |
| 460 | - fontColor = liveAgentMessageFontColor; | |
| 461 | - } else { | |
| 500 | + // Only sanitize user input | |
| 501 | + messageText = sanitizeUserInput(messageText); | |
| 502 | + } else if (sender === "agent") { | |
| 503 | + messageClass = "agent-message"; | |
| 504 | + bgColor = liveAgentMessageBgColor; | |
| 505 | + fontColor = liveAgentMessageFontColor; | |
| 506 | + } else { | |
| 462 | 507 | messageClass = "bot-message"; |
| 463 | 508 | bgColor = botMessageBgColor; |
| 464 | 509 | fontColor = botMessageFontColor; |
| 465 | 510 | } |
| 466 | 511 | |
| 467 | - const messageDiv = $('<div>') | |
| 468 | - .addClass(messageClass) | |
| 469 | - .css({ | |
| 470 | - 'background': bgColor, | |
| 471 | - 'color': fontColor, | |
| 472 | - }); | |
| 512 | + const messageDiv = $('<div>') | |
| 513 | + .addClass(messageClass) | |
| 514 | + .css({ | |
| 515 | + 'background': bgColor, | |
| 516 | + 'color': fontColor, | |
| 517 | + 'margin-bottom': '1em' | |
| 518 | + }); | |
| 473 | 519 | |
| 474 | - // Add CSS for paragraphs | |
| 475 | - messageDiv.css({ | |
| 476 | - 'margin-bottom': '1em' | |
| 477 | - }); | |
| 520 | + // Process the message content based on sender | |
| 521 | + let fullMessage; | |
| 522 | + if (sender === "user") { | |
| 523 | + // For user messages, apply linkify after sanitization | |
| 524 | + fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(messageText)))); | |
| 525 | + } else { | |
| 526 | + // For bot/agent messages, preserve HTML | |
| 527 | + fullMessage = messageText; | |
| 528 | + } | |
| 478 | 529 | |
| 479 | - // Format and process the message content | |
| 480 | - let fullMessage = linkify(formatBoldText(convertNewlinesToBreaks(formatCodeBlocks(messageText)))); | |
| 481 | - | |
| 482 | 530 | // Add images if provided |
| 483 | 531 | if (images && images.length > 0) { |
| 484 | 532 | fullMessage += '<div class="image-gallery">'; |
| 485 | 533 | images.forEach(img => { |
| 534 | + // Ensure image URLs and titles are properly escaped | |
| 535 | + const safeTitle = sanitizeUserInput(img.title); | |
| 536 | + const safeUrl = encodeURI(img.image_url); | |
| 537 | + const safeThumbnail = encodeURI(img.thumbnail_url); | |
| 538 | + | |
| 486 | 539 | fullMessage += ` |
| 487 | 540 | <div style="margin-bottom: 10px;"> |
| 488 | - <strong>${img.title}</strong><br> | |
| 489 | - <a href="${img.image_url}" target="_blank"> | |
| 490 | - <img src="${img.thumbnail_url}" alt="${img.title}" style="max-width: 100px; height: auto; margin: 5px;" /> | |
| 541 | + <strong>${safeTitle}</strong><br> | |
| 542 | + <a href="${safeUrl}" target="_blank"> | |
| 543 | + <img src="${safeThumbnail}" alt="${safeTitle}" style="max-width: 100px; height: auto; margin: 5px;" /> | |
| 491 | 544 | </a> |
| 492 | 545 | </div>`; |
| 493 | 546 | }); |
| 494 | 547 | fullMessage += '</div>'; |
| @@ -494,23 +547,20 @@ | ||
| 494 | 547 | fullMessage += '</div>'; |
| 495 | 548 | } |
| 496 | 549 | |
| 497 | 550 | // Append HTML content if provided |
| 498 | - if (messageHtml) { | |
| 551 | + if (messageHtml && sender !== "user") { | |
| 499 | 552 | fullMessage += '<br><br>' + messageHtml; |
| 500 | 553 | } |
| 501 | 554 | |
| 502 | 555 | messageDiv.html(fullMessage); |
| 503 | 556 | |
| 504 | - // Add a class for temporary messages if needed | |
| 505 | 557 | if (isTemporary) { |
| 506 | 558 | messageDiv.addClass('temporary-message'); |
| 507 | 559 | } |
| 508 | 560 | |
| 509 | - // Append the message to the chat box | |
| 510 | - messageDiv.hide().appendTo('#chat-box').fadeIn(300, function () { | |
| 561 | + messageDiv.hide().appendTo('#chat-box').fadeIn(300, function() { | |
| 511 | 562 | if (sender === "bot") { |
| 512 | - // After bot's message is displayed, scroll the last user message to the top | |
| 513 | 563 | const lastUserMessage = $('#chat-box').find('.user-message').last(); |
| 514 | 564 | if (lastUserMessage.length) { |
| 515 | 565 | scrollElementToTop(lastUserMessage); |
| 516 | 566 | } |
| @@ -517,13 +567,13 @@ | ||
| 517 | 567 | } |
| 518 | 568 | }); |
| 519 | 569 | |
| 520 | 570 | if (messageText.id) { |
| 521 | - lastSeenMessageId = messageText.id; | |
| 522 | - hideNotification(); | |
| 523 | - } | |
| 571 | + lastSeenMessageId = messageText.id; | |
| 572 | + hideNotification(); | |
| 573 | + } | |
| 524 | 574 | } catch (error) { |
| 525 | - console.error("Error rendering message with images:", error); | |
| 575 | + console.error("Error rendering message:", error); | |
| 526 | 576 | } |
| 527 | 577 | } |
| 528 | 578 | |
| 529 | 579 | |
| @@ -1091,10 +1141,18 @@ | ||
| 1091 | 1141 | document.getElementById('word-upload').click(); |
| 1092 | 1142 | }); |
| 1093 | 1143 | } |
| 1094 | 1144 | |
| 1145 | +function addSafeEventListener(elementId, eventType, handler) { | |
| 1146 | + const element = document.getElementById(elementId); | |
| 1147 | + if (element) { | |
| 1148 | + element.addEventListener(eventType, handler); | |
| 1149 | + } | |
| 1150 | +} | |
| 1151 | + | |
| 1152 | + | |
| 1095 | 1153 | // PDF file input change handler |
| 1096 | -document.getElementById('pdf-upload').addEventListener('change', async function(e) { | |
| 1154 | +addSafeEventListener('pdf-upload', 'change', async function(e) { | |
| 1097 | 1155 | const file = e.target.files[0]; |
| 1098 | 1156 | |
| 1099 | 1157 | if (!file || file.type !== 'application/pdf') { |
| 1100 | 1158 | alert('Please select a valid PDF file.'); |
| @@ -1166,9 +1224,9 @@ | ||
| 1166 | 1224 | } |
| 1167 | 1225 | }); |
| 1168 | 1226 | |
| 1169 | 1227 | // Word file input change handler |
| 1170 | -document.getElementById('word-upload').addEventListener('change', async function(e) { | |
| 1228 | +addSafeEventListener('word-upload', 'change', async function(e) { | |
| 1171 | 1229 | const file = e.target.files[0]; |
| 1172 | 1230 | |
| 1173 | 1231 | if (!file || file.type !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { |
| 1174 | 1232 | alert('Please select a valid Word document (.docx).'); |
| @@ -1401,21 +1459,24 @@ | ||
| 1401 | 1459 | document.addEventListener('DOMContentLoaded', function() { |
| 1402 | 1460 | checkInitialDocumentStatus(); |
| 1403 | 1461 | }); |
| 1404 | 1462 | |
| 1405 | -// Style all toolbar elements | |
| 1406 | 1463 | const toolbarElements = [ |
| 1407 | 1464 | '#mxchat-chatbot .toolbar-btn svg', |
| 1408 | 1465 | '#mxchat-chatbot .active-pdf-name', |
| 1409 | 1466 | '#mxchat-chatbot .active-word-name', |
| 1410 | 1467 | '#mxchat-chatbot .remove-pdf-btn svg', |
| 1411 | - '#mxchat-chatbot .remove-word-btn svg' | |
| 1468 | + '#mxchat-chatbot .remove-word-btn svg', | |
| 1469 | + '#mxchat-chatbot .toolbar-perplexity svg' | |
| 1412 | 1470 | ]; |
| 1413 | -$(toolbarElements.join(', ')).css({ | |
| 1414 | - 'fill': toolbarIconColor, | |
| 1415 | - 'color': toolbarIconColor | |
| 1471 | + | |
| 1472 | +toolbarElements.forEach(selector => { | |
| 1473 | + $(selector).css({ | |
| 1474 | + 'fill': toolbarIconColor, | |
| 1475 | + 'stroke': toolbarIconColor, | |
| 1476 | + 'color': toolbarIconColor | |
| 1477 | + }); | |
| 1416 | 1478 | }); |
| 1417 | - | |
| 1418 | 1479 | |
| 1419 | 1480 | |
| 1420 | 1481 | // Ensure essential elements are defined |
| 1421 | 1482 | const emailForm = document.getElementById('email-collection-form'); |