1/** 2 * Destinaiton AI Chat Widget - Enhanced Version 3 * Embeddable chat widget 4 * 5 * Usage: Add this script tag to your website: 6 * <script src="https://your-domain.com/js/destination-chat-widget.js"></script> 7 * 8 * 9 */ 10 11(function () { 12 'use strict'; 13 14 // Prevent multiple initialization 15 if (window.DestinationWidget) { 16 return; 17 } 18 19 // Default configuration 20 const defaultConfig = { 21 apiEndpoint: 'https://ai.visitfamagusta.com.cy/api/CyprusTravel/ask', 22 position: 'bottom-right', 23 primaryColor: '#ff8c33', 24 greeting: 'Hello! How can I help you explore Famagusta Region?', 25 apiKey: 'fam_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p', 26 regionName: 'Famagusta', 27 caption: 'Your Personal AI Travel Guide', 28 autoOpen: false, 29 favicon: 'https://ai.visitfamagusta.com.cy/img/favicon-famagusta.png', 30 poweredBy: 'Revitup', 31 poweredByUrl: 'https://www.revitup.direct/' 32 ,suggestions: [{"emoji":"\uD83C\uDFD6\uFE0F","text":"Best beaches?","message":"Best beaches in Famagusta Region?"},{"emoji":"\uD83C\uDFDB\uFE0F","text":"Historical sites?","message":"Historical sites in Famagusta Region?"},{"emoji":"\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC66","text":"Family activities?","message":"Family activities in Famagusta Region?"},{"emoji":"\uD83C\uDF77","text":"Romantic spots for couples?","message":"Romantic spots for couples in Famagusta Region?"}] 33 }; 34 35 36 // Merge user config with defaults 37 const config = Object.assign({}, defaultConfig, window.DestinationConfig || {}); 38 39 class DestinationWidget { 40 constructor() { 41 this.isOpen = false; 42 this.isInitialized = false; 43 this.messages = []; 44 this.sessionId = this.generateSessionId(); 45 this.userId = this.getUserId(); 46 47 // Load saved conversation and state 48 this.loadConversationState(); 49 50 this.init(); 51 } 52 53 init() { 54 if (document.readyState === 'loading') { 55 document.addEventListener('DOMContentLoaded', () => this.render()); 56 } else { 57 this.render(); 58 } 59 } 60 61 render() { 62 if (this.isInitialized) return; 63 64 this.injectStyles(); 65 this.createWidget(); 66 this.bindEvents(); 67 this.isInitialized = true; 68 69 // Restore conversation if exists 70 this.restoreConversation(); 71 72 if (config.autoOpen) { 73 setTimeout(() => this.openChat(), 2000); 74 } 75 } 76 77 injectStyles() { 78 const styles = ` 79 #destination-chat-widget { 80 position: fixed; 81 ${config.position.includes('bottom') ? 'bottom: 10px;' : 'top: 20px;'} 82 ${config.position.includes('right') ? 'right: 20px;' : 'left: 20px;'} 83 z-index: 2147483647; 84 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 85 * { box-sizing: border-box; } 86 } 87 88 .destination-chat-trigger { 89 min-width: 160px; 90 height: 50px; 91 border-radius: 25px; 92 background: ${config.primaryColor}; 93 border: none; 94 cursor: pointer; 95 box-shadow: 0 4px 20px rgba(29, 132, 181, 0.4); 96 display: flex; 97 align-items: center; 98 justify-content: center; 99 transition: all 0.3s ease; 100 color: white; 101 font-size: 16px; 102 font-weight: bold; 103 outline: none; 104 padding: 0 20px; 105 gap: 8px; 106 } 107 108 .destination-input-footer { 109 padding: 5px 15px; 110 background: #f9fafb; 111 border-top: 1px solid #e5e7eb; 112 } 113 114 .destination-input-footer p { 115 margin: 0; 116 font-size: 11px; 117 color: #6b7280; 118 text-align: center; 119 } 120 121 .destination-chat-trigger:hover { 122 transform: scale(1.05); 123 box-shadow: 0 6px 25px rgba(29, 132, 181, 0.6); 124 } 125 126 .destination-chat-trigger.active { 127 background: #dc2626; 128 min-width: 50px; 129 border-radius: 25px; 130 } 131 132 .destination-trigger-text { 133 display: inline; 134 transition: opacity 0.2s ease; 135 } 136 137 .destination-chat-trigger.active .destination-trigger-text { 138 display: none; 139 } 140 141 .destination-avatar.ai img { 142 height: 28px; 143 border-radius: 20px; 144 } 145 146 .destination-trigger-icon { 147 font-size: 20px; 148 transition: transform 0.2s ease; 149 } 150 151 .destination-chat-trigger.active .destination-trigger-icon { 152 transform: rotate(45deg); 153 } 154 155 .destination-chat-container { 156 position: absolute; 157 ${config.position.includes('bottom') ? 'bottom: 70px;' : 'top: 70px;'} 158 ${config.position.includes('right') ? 'right: 0;' : 'left: 0;'} 159 width: 450px; 160 height: 700px; 161 background: white; 162 border-radius: 12px; 163 box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); 164 display: none; 165 flex-direction: column; 166 overflow: hidden; 167 border: 1px solid #e5e7eb; 168 opacity: 0; 169 transform: translateY(20px); 170 transition: all 0.3s ease; 171 } 172 173 .destination-chat-container.open { 174 display: flex; 175 opacity: 1;
176 transform: translateY(0); 177 } 178 179 .destination-chat-header { 180 background: ${config.primaryColor}; 181 color: white; 182 padding: 20px 8px; 183 text-align: center; 184 position: relative; 185 } 186 187 .destination-chat-header h3 { 188 margin: 0; 189 font-size: 18px; 190 font-weight: 600; 191 color: white; 192 } 193 194 .destination-chat-header p { 195 margin: 8px 0 0 0; 196 font-size: 14px; 197 opacity: 0.9; 198 } 199 200 .destination-header-controls { 201 position: absolute; 202 top: 15px; 203 right: 15px; 204 display: flex; 205 gap: 8px; 206 } 207 208 .destination-chat-close, 209 .destination-chat-clear { 210 background: none; 211 border: none; 212 color: white; 213 font-size: 18px; 214 cursor: pointer; 215 width: 30px; 216 height: 30px; 217 border-radius: 50%; 218 display: flex; 219 align-items: center; 220 justify-content: center; 221 transition: background-color 0.2s; 222 outline: none; 223 } 224 225 .destination-chat-close:hover, 226 .destination-chat-clear:hover { 227 background-color: rgba(255, 255, 255, 0.2); 228 } 229 230 .destination-chat-clear { 231 font-size: 16px; 232 } 233 234 .destination-chat-messages { 235 flex: 1; 236 padding: 20px; 237 overflow-y: auto; 238 background: #f9fafb; 239 } 240 241 .destination-message { 242 margin-bottom: 16px; 243 display: flex; 244 align-items: flex-start; 245 gap: 10px; 246 } 247 248 .destination-message.user { 249 flex-direction: row-reverse; 250 } 251 252 .destination-avatar { 253 width: 32px; 254 height: 32px; 255 border-radius: 50%; 256 display: flex; 257 align-items: center; 258 justify-content: center; 259 font-size: 14px; 260 flex-shrink: 0; 261 } 262 263 .destination-avatar.ai { 264 background: ${config.primaryColor}; 265 color: white; 266 } 267 268 .destination-avatar.user { 269 background: #6b7280; 270 color: white; 271 } 272 273 .destination-message-content { 274 max-width: 80%; 275 padding: 12px 16px; 276 border-radius: 18px; 277 font-size: 14px; 278 line-height: 1.4; 279 word-wrap: break-word; 280 } 281 282 .destination-message.ai .destination-message-content { 283 background: white; 284 color: #374151; 285 border-bottom-left-radius: 4px; 286 } 287 288 .destination-message.user .destination-message-content { 289 background: ${config.primaryColor}; 290 color: white; 291 border-bottom-right-radius: 4px; 292 } 293 294 .destination-typing { 295 display: flex; 296 align-items: center; 297 gap: 10px; 298 margin-bottom: 16px; 299 } 300 301 .destination-typing-dots { 302 display: flex; 303 gap: 4px; 304 padding: 12px 16px; 305 background: white; 306 border-radius: 18px;
307 border-bottom-left-radius: 4px; 308 } 309 310 .destination-typing-dots span { 311 width: 6px; 312 height: 6px; 313 background: #9ca3af; 314 border-radius: 50%; 315 animation: destination-bounce 1.4s infinite ease-in-out; 316 } 317 318 .destination-typing-dots span:nth-child(2) { 319 animation-delay: 0.2s; 320 } 321 322 .destination-typing-dots span:nth-child(3) { 323 animation-delay: 0.4s; 324 } 325 326 @keyframes destination-bounce { 327 0%, 80%, 100% { 328 transform: scale(0); 329 } 330 40% { 331 transform: scale(1); 332 } 333 } 334 335 /* Powered by footer */ 336 .destination-chat-footer { 337 padding: 0px 15px; 338 background: #f9fafb; 339 border-top: 1px solid #e5e7eb; 340 text-align: center; 341 } 342 343 .destination-powered-by { 344 font-size: 11px; 345 color: #6b7280; 346 margin: 0; 347 display: flex; 348 align-items: center; 349 justify-content: center; 350 gap: 4px; 351 } 352 353 .destination-powered-by a { 354 color: ${config.primaryColor}; 355 text-decoration: none; 356 font-weight: 500; 357 display: flex; 358 align-items: center; 359 gap: 2px; 360 } 361 362 .destination-powered-by a:hover { 363 text-decoration: underline; 364 } 365 366 .destination-powered-by .external-icon { 367 font-size: 10px; 368 opacity: 0.7; 369 } 370 371 .destination-chat-input { 372 padding: 5px; 373 border-top: 1px solid #e5e7eb; 374 background: white; 375 } 376 377 .destination-input-container { 378 display: flex; 379 gap: 10px; 380 align-items: flex-end; 381 } 382 383 .destination-input-field { 384 flex: 1; 385 padding: 12px 12px; 386 border: 1px solid #d1d5db; 387 border-radius: 20px; 388 font-size: 15px; 389 resize: none; 390 outline: none; 391 max-height: 100px; 392 min-height: 44px; 393 font-family: inherit; 394 } 395 396 .destination-input-field:focus { 397 border-color: ${config.primaryColor}; 398 } 399 400 .destination-send-btn { 401 width: 44px; 402 height: 44px; 403 border-radius: 50%; 404 background: ${config.primaryColor}; 405 border: none; 406 color: white; 407 cursor: pointer; 408 display: flex; 409 align-items: center; 410 justify-content: center; 411 transition: background-color 0.2s; 412 outline: none; 413 } 414 415 .destination-send-btn:hover { 416 background: #a7612b; 417 } 418 419 .destination-send-btn:disabled { 420 background: #9ca3af; 421 cursor: not-allowed; 422 } 423 424 .destination-suggestions { 425 gap: 8px; 426 margin-top: 12px; 427 } 428 429 .destination-suggestion { 430 background: #f3f4f6; 431 border: 1px solid #d1d5db; 432 border-radius: 16px; 433 padding: 6px 12px; 434 font-size: 12px; 435 cursor: pointer; 436 transition: all 0.2s; 437 outline: none; 438 font-family: inherit; 439 margin: 0.3em; 440 } 441 442 .destination-suggestion:hover { 443 background: ${config.primaryColor}; 444 color: white; 445 border-color: ${config.primaryColor}; 446 } 447 448 .destination-welcome { 449 text-align: center; 450 } 451 452 .destination-welcome h4 { 453 margin: 0 0 8px 0; 454 color: ${config.primaryColor}; 455 font-size: 14px; 456 } 457 458 .destination-welcome p { 459 margin: 0; 460 color: #6b7280; 461 font-size: 13px; 462 line-height: 1.4; 463 } 464 465 /* Mobile Responsiveness */ 466 @media (max-width: 420px) { 467 .destination-chat-container { 468 width: calc(100vw - 20px); 469 height: calc(100vh - 10em); 470 ${config.position.includes('bottom') ? 'bottom: 60px;' : 'top: 80px;'} 471 ${config.position.includes('right') ? 'right: -10px;' : 'left: -10px;'} 472 } 473 474 .destination-chat-trigger { 475 min-width: 140px; 476 font-size: 14px; 477 } 478 } 479 480 /* Notification */ 481 .destination-notification { 482 position: fixed; 483 bottom: 24px; 484 left: 50%; 485 transform: translateX(-50%); 486 background-color: ${config.primaryColor}; 487 color: white; 488 padding: 12px 24px; 489 border-radius: 8px; 490 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 491 z-index: 2147483646; 492 opacity: 0; 493 transition: opacity 0.3s ease; 494 font-size: 14px; 495 } 496 497 .destination-notification.show { 498 opacity: 1;
499 } 500 501 /* Clear conversation confirmation */ 502 .destination-clear-confirm { 503 position: absolute; 504 top: 60px; 505 right: 15px; 506 background: rgba(220, 38, 38, 0.95); 507 color: white; 508 padding: 8px 12px; 509 border-radius: 6px; 510 font-size: 12px; 511 display: none; 512 gap: 8px; 513 align-items: center; 514 z-index: 10; 515 } 516 517 .destination-clear-confirm.show { 518 display: flex; 519 } 520 521 .destination-clear-confirm button { 522 background: none; 523 border: 1px solid white; 524 color: white; 525 padding: 2px 8px; 526 border-radius: 3px; 527 font-size: 11px; 528 cursor: pointer; 529 transition: background-color 0.2s; 530 } 531 532 .destination-clear-confirm button:hover { 533 background: rgba(255, 255, 255, 0.2); 534 } 535 `; 536 537 const styleSheet = document.createElement('style'); 538 styleSheet.textContent = styles; 539 document.head.appendChild(styleSheet); 540 } 541 542 createWidget() { 543 const widgetHTML = ` 544 <div id="destination-chat-widget"> 545 <button class="destination-chat-trigger" id="destination-trigger"> 546 <span class="destination-trigger-icon">ð¬</span> 547 <span class="destination-trigger-text">${config.caption}</span> 548 </button> 549 550 <div class="destination-chat-container" id="destination-container"> 551 <div class="destination-chat-header"> 552 <div class="destination-header-controls"> 553 <button class="destination-chat-clear" id="destination-clear" title="Clear conversation">ðï¸</button> 554 <button class="destination-chat-close" id="destination-close">Ã</button> 555 </div> 556 <h3>${config.caption}</h3> 557 558 559 <div class="destination-clear-confirm" id="destination-clear-confirm"> 560 <span>Clear all messages?</span> 561 <button id="destination-clear-yes">Yes</button> 562 <button id="destination-clear-no">No</button> 563 </div> 564 </div> 565 566 <div class="destination-chat-messages" id="destination-messages"> 567 <div class="destination-welcome" id="destination-welcome"> 568 <h4>ð ${config.greeting}</h4> 569 <div class="destination-suggestions"> 570 ${config.suggestions.map(suggestion => 571 `<button class="destination-suggestion" data-message="${suggestion.message}"> 572 ${suggestion.emoji} ${suggestion.text} 573 </button>` 574 ).join('')} 575 </div> 576 </div> 577 </div> 578 579 <div class="destination-chat-input"> 580 <div class="destination-input-container"> 581 <textarea 582 class="destination-input-field" 583 id="destination-input" 584 placeholder="Ask me about ${config.regionName} Region..." 585 rows="1" 586 ></textarea> 587 <button class="destination-send-btn" id="destination-send">
588 <span>â¤</span> 589 </button> 590 591 </div> 592 </div> 593 594 <!-- Footer with Powered by --> 595 <div class="destination-chat-footer"> 596 <p class="destination-powered-by"> 597 Powered by 598 <a href="${config.poweredByUrl}" target="_blank" rel="noopener noreferrer"> 599 ${config.poweredBy} 600 <span class="external-icon">â</span> 601 </a> 602 </p> 603 <div class="destination-input-footer"> 604 <p style="font-size: 11px; color: #6b7280; text-align: center; "> 605 â ï¸ This chatbot can make mistakes. Please verify important information. 606 </p> 607</div> 608 </div> 609 </div> 610 </div> 611 `; 612 613 document.body.insertAdjacentHTML('beforeend', widgetHTML); 614 } 615 616 bindEvents() { 617 // Toggle chat 618 this.getElementById('destination-trigger').addEventListener('click', () => { 619 this.toggleChat(); 620 }); 621 622 this.getElementById('destination-close').addEventListener('click', () => { 623 this.closeChat(); 624 }); 625 626 // Clear conversation 627 this.getElementById('destination-clear').addEventListener('click', () => { 628 this.showClearConfirmation(); 629 }); 630 631 this.getElementById('destination-clear-yes').addEventListener('click', () => { 632 this.clearConversation(); 633 this.hideClearConfirmation(); 634 }); 635 636 this.getElementById('destination-clear-no').addEventListener('click', () => { 637 this.hideClearConfirmation(); 638 }); 639 640 // Send message 641 this.getElementById('destination-send').addEventListener('click', () => { 642 this.sendMessage(); 643 }); 644 645 // Enter to send 646 this.getElementById('destination-input').addEventListener('keypress', (e) => { 647 if (e.key === 'Enter' && !e.shiftKey) { 648 e.preventDefault(); 649 this.sendMessage(); 650 } 651 }); 652 653 // Auto-resize textarea 654 this.getElementById('destination-input').addEventListener('input', (e) => { 655 e.target.style.height = 'auto'; 656 e.target.style.height = Math.min(e.target.scrollHeight, 100) + 'px'; 657 }); 658 659 // Suggestion chips 660 this.querySelectorAll('.destination-suggestion').forEach(chip => { 661 chip.addEventListener('click', () => { 662 const message = chip.dataset.message; 663 this.sendPredefinedMessage(message); 664 }); 665 }); 666 667 // Auto-save conversation 668 window.addEventListener('beforeunload', () => { 669 this.saveConversationState(); 670 }); 671 672 // Hide clear confirmation when clicking outside 673 document.addEventListener('click', (e) => { 674 const confirmDialog = this.getElementById('destination-clear-confirm'); 675 const clearButton = this.getElementById('destination-clear'); 676 if (!confirmDialog.contains(e.target) && !clearButton.contains(e.target)) { 677 this.hideClearConfirmation(); 678 } 679 }); 680 } 681 682 toggleChat() { 683 if (this.isOpen) { 684 this.closeChat(); 685 } else { 686 this.openChat(); 687 } 688 } 689 690 openChat() { 691 this.isOpen = true; 692 const container = this.getElementById('destination-container'); 693 const trigger = this.getElementById('destination-trigger'); 694 695 container.classList.add('open'); 696 trigger.classList.add('active'); 697 698 // Save state 699 this.saveConversationState(); 700 701 // Focus input after animation 702 //setTimeout(() => { 703 // this.getElementById('destination-input').focus(); 704 //}, 300); 705 706 // Track widget open event 707 this.trackEvent('widget_opened'); 708 } 709 710 closeChat() { 711 this.isOpen = false; 712 const container = this.getElementById('destination-container'); 713 const trigger = this.getElementById('destination-trigger'); 714 715 container.classList.remove('open'); 716 trigger.classList.remove('active'); 717 718 // Hide clear confirmation if open 719 this.hideClearConfirmation(); 720 721 // Save state 722 this.saveConversationState(); 723 724 // Track widget close event 725 this.trackEvent('widget_closed'); 726 } 727 728 showClearConfirmation() { 729 const confirmDialog = this.getElementById('destination-clear-confirm'); 730 confirmDialog.classList.add('show'); 731 } 732 733 hideClearConfirmation() { 734 const confirmDialog = this.getElementById('destination-clear-confirm'); 735 confirmDialog.classList.remove('show'); 736 } 737 738 clearConversation() { 739 // Clear messages array 740 this.messages = []; 741 742 // Generate new session ID 743 this.sessionId = this.generateSessionId(); 744 745 // Clear the messages container and restore welcome message 746 const messagesContainer = this.getElementById('destination-messages'); 747 messagesContainer.innerHTML = ` 748 <div class="destination-welcome" id="destination-welcome"> 749 <h4>ð ${config.greeting}</h4> 750 <div class="destination-suggestions"> 751 ${config.suggestions.map(suggestion => 752 `<button class="destination-suggestion" data-message="${suggestion.message}"> 753 ${suggestion.emoji} ${suggestion.text} 754 </button>` 755 ).join('')} 756 </div> 757 </div> 758 `; 759 760 // Re-bind suggestion events 761 this.querySelectorAll('.destination-suggestion').forEach(chip => { 762 chip.addEventListener('click', () => { 763 const message = chip.dataset.message; 764 this.sendPredefinedMessage(message); 765 }); 766 }); 767 768 // Clear stored conversation 769 localStorage.removeItem('famagusta_conversation'); 770 localStorage.removeItem('famagusta_widget_state'); 771 772 // Show notification 773 this.showNotification('Conversation cleared', 2000); 774 775 // Track event 776 this.trackEvent('conversation_cleared'); 777 } 778 779 sendMessage() { 780 const input = this.getElementById('destination-input'); 781 const message = input.value.trim(); 782 783 if (!message) return; 784 785 this.addMessage(message, 'user'); 786 input.value = ''; 787 input.style.height = 'auto'; 788 789 this.showTyping(); 790 this.callAPI(message); 791 792 // Save conversation after adding message 793 this.saveConversationState(); 794 795 // Track message sent event 796 this.trackEvent('message_sent', { message_length: message.length }); 797 } 798 799 sendPredefinedMessage(message) { 800 this.addMessage(message, 'user'); 801 this.showTyping(); 802 this.callAPI(message); 803 804 // Save conversation after adding message 805 this.saveConversationState(); 806 807 // Track suggestion clicked event 808 this.trackEvent('suggestion_clicked', { suggestion: message });
809 } 810 811 addMessage(content, type) { 812 const messagesContainer = this.getElementById('destination-messages'); 813 const messageDiv = document.createElement('div'); 814 messageDiv.className = `destination-message ${type}`; 815 816 const avatar = document.createElement('div'); 817 avatar.className = `destination-avatar ${type}`; 818 if (type === 'user') { 819 avatar.innerHTML = 'ð¤'; 820 } else { 821 avatar.innerHTML = '<img height="25" src="' + config.favicon +'" />'; 822 } 823 824 const messageContent = document.createElement('div'); 825 messageContent.className = 'destination-message-content'; 826 827 // Handle HTML content for AI messages (for links, formatting) 828 if (type === 'ai') { 829 messageContent.innerHTML = this.formatMessage(content); 830 } else { 831 messageContent.textContent = content; 832 } 833 834 messageDiv.appendChild(avatar); 835 messageDiv.appendChild(messageContent); 836 837 // Remove welcome message if it exists 838 const welcomeMessage = messagesContainer.querySelector('.destination-welcome'); 839 if (welcomeMessage && type === 'user') { 840 welcomeMessage.remove(); 841 } 842 843 messagesContainer.appendChild(messageDiv); 844 845 // Store message for conversation history 846 this.messages.push({ content, type, timestamp: new Date() }); 847 848 this.scrollToBottom(); 849 } 850 851 formatMessage(content) { 852 853 // Check if content already contains HTML tags 854 const hasHTMLTags = /<[^>]+>/.test(content); 855 856 // If content already has HTML, return it as-is (no processing needed) 857 if (hasHTMLTags) { 858 return content; 859 } 860 861 // First, handle markdown-style links [text](url) 862 let formatted = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color: #1d84b5; text-decoration: underline;">$1</a>'); 863 864 // Convert plain URLs to links 865 const urlRegex = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/g; 866 formatted = formatted.replace(urlRegex, (match) => { 867 // Don't convert URLs that are already inside HTML tags 868 const beforeUrl = formatted.substring(0, formatted.indexOf(match)); 869 const lastOpenTag = beforeUrl.lastIndexOf('<'); 870 const lastCloseTag = beforeUrl.lastIndexOf('>'); 871 872 if (lastOpenTag > lastCloseTag) { 873 return match; // URL is inside an HTML tag, don't process 874 } 875 876 return `<a href="${match}" target="_blank" style="color: #1d84b5; text-decoration: underline;">${match}</a>`; 877 }); 878 879 // Convert phone numbers to tel links 880 const phoneRegex = /(\+?\d{1,4}[\s\-\(\)]?(\d{1,3}[\s\-\(\)]?){1,4}\d{1,4})/g; 881 formatted = formatted.replace(phoneRegex, '<a href="tel:$1" style="color: #1d84b5; text-decoration: underline;">$1</a>'); 882 883 // Handle basic formatting 884 formatted = formatted 885 .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') 886 .replace(/\*(.*?)\*/g, '<em>$1</em>') 887 .replace(/\n/g, '<br>'); 888 889 return formatted; 890 } 891 892 showTyping() { 893 // Remove any existing typing indicator 894 this.hideTyping(); 895 896 // Create and add typing indicator after the last message 897 const messagesContainer = this.getElementById('destination-messages'); 898 const typingDiv = document.createElement('div'); 899 typingDiv.className = 'destination-typing'; 900 typingDiv.id = 'destination-typing'; 901 typingDiv.innerHTML = ` 902 <div class="destination-avatar ai"><img height="25" src="${config.favicon}" /> </div> 903 <div class="destination-typing-dots">
904 <span></span> 905 <span></span> 906 <span></span> 907 </div> 908 `; 909 910 messagesContainer.appendChild(typingDiv); 911 this.scrollToBottom(); 912 } 913 914 hideTyping() { 915 const typingIndicator = this.getElementById('destination-typing'); 916 if (typingIndicator) { 917 typingIndicator.remove(); 918 } 919 } 920 921 async callAPI(message) { 922 try { 923 const userLocation = await this.getUserLocation(); 924 925 const requestBody = { 926 Message: message, 927 UserId: this.userId, 928 SessionId: this.sessionId, 929 UserLocation: userLocation || "34.9934,34.0049", 930 UseLocationForRecommendations: Boolean(userLocation), 931 ConversationHistory: this.getConversationHistory(), 932 Destination: config.regionName.toLowerCase(), 933 IsEmbeddedWidget: true 934 }; 935 936 // ADD AUTHENTICATION HEADERS: 937 const headers = { 938 'Content-Type': 'application/json', 939 'X-API-Key': config.apiKey 940 }; 941 942 const response = await fetch(config.apiEndpoint, { 943 method: 'POST', 944 headers: headers, 945 body: JSON.stringify(requestBody) 946 }); 947 948 // ADD AUTHENTICATION ERROR HANDLING: 949 if (response.status === 401) { 950 this.hideTyping(); 951 this.addMessage("ð Authentication Error Please contact the website administrator.", 'ai'); 952 return; 953 } 954 955 // 429 - Rate Limit Exceeded 956 if (response.status === 429) { 957 this.hideTyping(); 958 let message = "â° You've reached the request limit. Please wait a moment before trying again."; 959 this.addMessage(message, 'ai'); 960 return; 961 } 962 963 if (!response.ok) { 964 throw new Error(`HTTP ${response.status}: ${response.statusText}`); 965 } 966 967 const data = await response.json(); 968 this.hideTyping(); 969 970 const aiMessage = data.message || "I'm sorry, I couldn't process your request right now."; 971 this.addMessage(aiMessage, 'ai'); 972 973 // Save conversation after AI response 974 this.saveConversationState(); 975 976 // Track successful API response 977 this.trackEvent('api_response_received', { 978 response_length: aiMessage.length, 979 has_sources: Boolean(data.sources && data.sources.length > 0) 980 }); 981 982 } catch (error) { 983 console.error('Widget API Error:', error); 984 this.hideTyping(); 985 986 let errorMessage = "I'm sorry, I'm having trouble connecting right now. "; 987 988 if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) { 989 errorMessage += "Please check your internet connection and try again."; 990 } else if (error.message.includes('HTTP 500')) { 991 errorMessage += "Our servers are experiencing issues. Please try again in a few moments."; 992 } else { 993 errorMessage += "Please try again later."; 994 } 995 996 this.addMessage(errorMessage, 'ai'); 997 998 // Track API error 999 this.trackEvent('api_error', { error_message: error.message }); 1000 } 1001 } 1002 1003 async getUserLocation() { 1004 return new Promise((resolve) => { 1005 if ("geolocation" in navigator) { 1006 navigator.geolocation.getCurrentPosition( 1007 (position) => { 1008 resolve(`${position.coords.latitude},${position.coords.longitude}`); 1009 }, 1010 () => resolve(null), 1011 { timeout: 5000, enableHighAccuracy: fal
1011se } 1012 ); 1013 } else { 1014 resolve(null); 1015 } 1016 }); 1017 } 1018 1019 getUserId() { 1020 let userId = localStorage.getItem('famagusta_user_id'); 1021 if (!userId) { 1022 userId = 'user_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now(); 1023 localStorage.setItem('famagusta_user_id', userId); 1024 } 1025 return userId; 1026 } 1027 1028 generateSessionId() { 1029 return 'session_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now(); 1030 } 1031 1032 getConversationHistory() { 1033 // Return last 6 messages for context 1034 return this.messages.slice(-6).map(msg => ({ 1035 role: msg.type === 'user' ? 'user' : 'assistant', 1036 content: msg.content 1037 })); 1038 } 1039 1040 // Conversation persistence methods 1041 saveConversationState() { 1042 try { 1043 const state = { 1044 messages: this.messages, 1045 sessionId: this.sessionId, 1046 isOpen: this.isOpen, 1047 timestamp: new Date().toISOString() 1048 }; 1049 localStorage.setItem('famagusta_conversation', JSON.stringify(state)); 1050 } catch (error) { 1051 console.warn('Failed to save conversation state:', error); 1052 } 1053 } 1054 1055 loadConversationState() { 1056 try { 1057 const saved = localStorage.getItem('famagusta_conversation'); 1058 if (saved) { 1059 const state = JSON.parse(saved); 1060 1061 // Check if conversation is less than 24 hours old 1062 const savedTime = new Date(state.timestamp); 1063 const now = new Date(); 1064 const hoursDiff = (now - savedTime) / (1000 * 60 * 60); 1065 1066 if (hoursDiff < 24) { 1067 this.messages = state.messages || []; 1068 this.sessionId = state.sessionId || this.generateSessionId(); 1069 this.isOpen = state.isOpen || false; 1070 return true; 1071 } else { 1072 // Clear old conversation 1073 localStorage.removeItem('famagusta_conversation'); 1074 } 1075 } 1076 } catch (error) { 1077 console.warn('Failed to load conversation state:', error); 1078 localStorage.removeItem('famagusta_conversation'); 1079 } 1080 return false; 1081 } 1082 1083 restoreConversation() { 1084 if (this.messages.length > 0) { 1085 const messagesContainer = this.getElementById('destination-messages'); 1086 1087 // Remove welcome message 1088 const welcomeMessage = messagesContainer.querySelector('.destination-welcome'); 1089 if (welcomeMessage) { 1090 welcomeMessage.remove(); 1091 } 1092 1093 // Restore all messages 1094 this.messages.forEach(msg => { 1095 const messageDiv = document.createElement('div'); 1096 messageDiv.className = `destination-message ${msg.type}`; 1097 1098 const avatar = document.createElement('div'); 1099 avatar.className = `destination-avatar ${msg.type}`; 1100 if (msg.type === 'user') { 1101 avatar.innerHTML = 'ð¤'; 1102 } else { 1103 avatar.innerHTML = '<img height="25" src="' + config.favicon + '" />'; 1104 } 1105 1106 const messageContent = document.createElement('div'); 1107 messageContent.className = 'destination-message-content'; 1108 1109 if (msg.type === 'ai') { 1110 messageContent.innerHTML = this.formatMessage(msg.content); 1111 } else { 1112 messageContent.textContent = msg.content; 1113 } 1114 1115 messageDiv.appendChild(avatar); 1116 messageDiv.appendChild(messageContent); 1117 messagesContainer.appendChild(messageDiv); 1118 }); 1119 1120 this.scrollToBottom(); 1121 } 1122 1123 // Restore open state if it was open 1124 if (this.isOpen) { 1125 setTimeout(() => this.openChat(), 100); 1126 } 1127 } 1128 1129 scrollToBottom() { 1130 const messagesContainer = this.getElementById('destination-messages'); 1131 setTimeout(() => { 1132 messagesContainer.scrollTop = messagesContainer.scrollHeight; 1133 }, 100); 1134 } 1135 1136 showNotification(message, duration = 3000) { 1137 // Remove existing notification 1138 const existing = document.querySelector('.destination-notification'); 1139 if (existing) { 1140 existing.remove(); 1141 } 1142 1143 const notification = document.createElement('div');
1144 notification.className = 'destination-notification'; 1145 notification.textContent = message; 1146 1147 document.body.appendChild(notification); 1148 1149 // Show notification 1150 setTimeout(() => notification.classList.add('show'), 10); 1151 1152 // Hide and remove notification 1153 setTimeout(() => { 1154 notification.classList.remove('show'); 1155 setTimeout(() => notification.remove(), 300); 1156 }, duration); 1157 } 1158 1159 trackEvent(eventName, properties = {}) { 1160 // Analytics tracking - integrate with your analytics service 1161 if (typeof gtag !== 'undefined') { 1162 gtag('event', eventName, { 1163 event_category: 'Famagusta_Chat_Widget', 1164 ...properties 1165 }); 1166 } 1167 1168 if (typeof fbq !== 'undefined') { 1169 fbq('track', 'CustomEvent', { 1170 event_name: eventName, 1171 ...properties 1172 }); 1173 } 1174 1175 } 1176 1177 // Helper methods for DOM manipulation within the widget 1178 getElementById(id) { 1179 return document.getElementById(id); 1180 } 1181 1182 querySelectorAll(selector) { 1183 return document.querySelectorAll(selector); 1184 } 1185 1186 // Public API methods 1187 open() { 1188 this.openChat(); 1189 } 1190 1191 close() { 1192 this.closeChat(); 1193 } 1194 1195 sendCustomMessage(message) { 1196 if (this.isInitialized) { 1197 this.sendPredefinedMessage(message); 1198 if (!this.isOpen) { 1199 this.openChat(); 1200 } 1201 } 1202 } 1203 1204 updateConfig(newConfig) { 1205 Object.assign(config, newConfig); 1206 // Re-render if needed 1207 if (this.isInitialized) { 1208 this.showNotification('Configuration updated'); 1209 } 1210 } 1211 1212 clear() { 1213 this.clearConversation(); 1214 } 1215 1216 destroy() { 1217 const widget = this.getElementById('destination-chat-widget'); 1218 if (widget) { 1219 widget.remove(); 1220 } 1221 1222 // Clear stored data 1223 localStorage.removeItem('famagusta_conversation'); 1224 localStorage.removeItem('famagusta_widget_state'); 1225 1226 this.isInitialized = false; 1227 this.isOpen = false; 1228 } 1229 } 1230 1231 // Initialize the widget 1232 const widget = new DestinationWidget(); 1233 1234 // Expose global API 1235 window.DestinationWidget = { 1236 open: () => widget.open(), 1237 close: () => widget.close(), 1238 sendMessage: (message) => widget.sendCustomMessage(message), 1239 updateConfig: (config) => widget.updateConfig(config), 1240 clear: () => widget.clear(), 1241 destroy: () => widget.destroy(), 1242 isOpen: () => widget.isOpen 1243 }; 1244 1245 // Auto-initialize flag 1246 window.DestinationWidgetLoaded = true; 1247 1248})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.