PageSourceSearch

https://ai.visitfamagusta.com.cy/js/famagusta-chat-widget.js

js visitfamagusta.com.cy collected 2026-10-02 14:50:39 UTC 46,561 bytes, 1,248 lines download raw bytes

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.