PageSourceSearch

https://www.wtso.com/assets/js/wtso-assistant.js?_1.023

js wtso.com collected 2026-09-26 06:15:09 UTC 62,842 bytes, 1,377 lines download raw bytes

1(function () {
2    var ASSISTANT_SESSION_MESSAGE_LIMIT = 60;
3    var ASSISTANT_REQUEST_HISTORY_LIMIT = 30;
4    var ASSISTANT_HISTORY_TEXT_LIMIT = 1500;
5
6    function ready(fn) {
7        if (document.readyState !== 'loading') {
8            fn();
9        } else {
10            document.addEventListener('DOMContentLoaded', fn);
11        }
12    }
13
14    function escapeHtml(value) {
15        return String(value || '').replace(/[&<>"']/g, function (char) {
16            return {
17                '&': '&amp;',
18                '<': '&lt;',
19                '>': '&gt;',
20                '"': '&quot;',
21                "'": '&#039;'
22            }[char];
23        });
24    }
25
26    function money(value) {
27        var number = parseFloat(value);
28        if (isNaN(number)) {
29            return '';
30        }
31        return '$' + number.toFixed(2);
32    }
33
34    function renderReply(text, options) {
35        var clean = formatReplyText(String(text || '').trim(), options || {});
36        if (!clean) {
37            return '';
38        }
39
40        return clean.split(/\n{2,}/).map(function (block) {
41            var lines = block.split(/\n/).map(function (line) {
42                return line.trim();
43            }).filter(Boolean);
44            if (lines.length && lines.every(isBulletLine)) {
45                return '<ul>' + lines.map(function (line) {
46                    return '<li>' + renderInlineMarkdown(line.replace(/^[-*•]\s+/, '')) + '</li>';
47                }).join('') + '</ul>';
48            }
49            if (lines.length && lines.every(isNumberedLine)) {
50                return '<ol>' + lines.map(function (line) {
51                    return '<li>' + renderInlineMarkdown(line.replace(/^[0-9]+[.)]\s+/, '')) + '</li>';
52                }).join('') + '</ol>';
53            }
54            return '<p>' + renderInlineMarkdown(block).replace(/\n/g, '<br>') + '</p>';
55        }).join('');
56    }
57
58    function renderInlineMarkdown(text) {
59        var html = escapeHtml(String(text || ''));
60        return html.replace(/(\*\*|__)(.+?)\1/g, '<strong>$2</strong>');
61    }
62
63    function isBulletLine(line) {
64        return /^[-*•]\s+/.test(line);
65    }
66
67    function isNumberedLine(line) {
68        return /^[0-9]+[.)]\s+/.test(line);
69    }
70
71    function formatReplyText(text, options) {
72        var clean = text.replace(/\r\n?/g, '\n').replace(/[ \t]+\n/g, '\n').trim();
73        if (!clean) {
74            return '';
75        }
76        if (options && options.streaming) {
77            return clean;
78        }
79        if (/\n\s*\n/.test(clean) || /^\s*([-*•]|\d+[.)])\s+/m.test(clean) || clean.length < 360) {
80            return clean;
81        }
82
83        var sentences = clean.match(/[^.!?]+[.!?]+(?=\s|$)|[^.!?]+$/g);
84        if (!sentences || sentences.length < 4) {
85            return clean;
86        }
87
88        var paragraphs = [];
89        var current = '';
90        sentences.forEach(function (sentence) {
91            sentence = sentence.trim();
92            if (!sentence) {
93                return;
94            }
95            if (current && (current.length + sentence.length > 230 || current.split(/[.!?]+/).length > 3)) {
96                paragraphs.push(current);
97                current = '';
98            }
99            current += (current ? ' ' : '') + sentence;
100        });
101        if (current) {
102            paragraphs.push(current);
103        }
104
105        return paragraphs.join('\n\n');
106    }
107
108    function toolLabel(name) {
109        return {
110            get_current_offer: "Today's offers",
111            search_products: 'Inventory',
112            search_last_chance_offers: 'Last Chance',
113            search_cellar_inventory: 'Cellar inventory',
114            get_product_details: 'Product',
115            check_shipping_eligibility: 'Shipping',
116            get_customer_cellar_summary: 'Cellar',
117            get_customer_taste_profile: 'Taste profile',
118            get_case_club_status: 'Case Club',
119            search_case_club_bank: 'Case Club bank',
120            prepare_case_club_ship_all: 'Case Club shipment',
121            get_case_club_addresses: 'Case Club addresses',
122            get_case_club_payment_methods: 'Case Club cards',
123            prepare_case_club_custom_shipment: 'Case Club shipment',
124            plan_party_wines: 'Party plan',
125            get_recent_orders_summary: 'Orders',
126            get_customer_orders: 'Orders',
127            get_customer_order_details: 'Order details',
128            get_customer_order_tracking: 'Tracking',
129            get_order_or_shipment_status: 'Order / shipment',
130            prepare_cancel_order: 'Cancellation',
131            prepare_change_order_ship_date: 'Ship date',
132            prepare_customer_service_email: 'Customer Service',
133            prepare_add_to_cart: 'Cart'
134        }[name] || 'Site data';
135    }
136
137    function storageAvailable() {
138        try {
139            var key = '__wtso_assistant_test__';
140            window.localStorage.setItem(key, '1');
141            window.localStorage.removeItem(key);
142            return true;
143        } catch (error) {
144            return false;
145        }
146    }
147
148    ready(function () {
149        var root = document.getElementById('wtso-assistant');
150        if (!root) {
151            return;
152        }
153
154        var endpoint = root.getAttribute('data-endpoint');
155        var streamEndpoint = root.getAttribute('data-stream-endpoint');
156        var actionEndpoint = root.getAttribute('data-action-endpoint');
157        var cartEndpoint = root.getAttribute('data-cart-endpoint');
158        var assistantCartType = root.getAttribute('data-cart-type') || '999';
159        var initialCartQty = root.getAttribute('data-initial-cart-qty');
160        var launcher = document.getElementById('wtso-assistant-launcher');
161        var close = document.getElementById('wtso-assistant-close');
162        var backdrop = document.getElementById('wtso-assistant-backdrop');
163        var reset = document.getElementById('wtso-assistant-reset');
164        var cartButton = document.getElementById('wtso-assistant-cart');
165        var cartCount = document.getElementById('wtso-assistant-cart-count');
166        var panel = document.getElementById('wtso-assistant-panel');
167        var body = document.getElementById('wtso-assistant-body');
168        var form = document.getElementById('wtso-assistant-form');
169        var input = document.getElementById('wtso-assistant-input');
170        var sendButton = form.querySelector('button[type="submit"]');
171        var initialBodyHtml = body.innerHTML;
172        var initialStarter = document.getElementById('wtso-assistant-starter');
173        var initialStarterHtml = initialStarter ? initialStarter.outerHTML : '';
174        var storageKey = 'wtsoAssistantSession:v1';
175        var canStore = storageAvailable();
176        var savedSession = loadSession();
177        var conversation = savedSession.messages || [];
178        var defaultSuggestions = readSuggestionButtons(initialStarter);
179        var activeSuggestions = savedSession.suggestions && savedSession.suggestions.length
180            ? savedSession.suggestions.slice(0, 5)
181            : defaultSuggestions.slice(0, 5);
182        var pendingActions = {};
183        var nextActionId = 1;
184        var lastCartSync = { source: '', value: null, at: 0 };
185        var sseRetryDelay = 0;
186
187        function setOpen(isOpen) {
188            root.classList.toggle('is-open', isOpen);
189            launcher.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
190            panel.setAttribute('aria-hidden', isOpen ? 'false' : 'true');
191            saveSession();
192            if (isOpen) {
193                setTimeout(function () { input.focus(); }, 50);
194            }
195        }
196
197        function setLoading(isLoading) {
198            root.classList.toggle('wtso-assistant-loading', isLoading);
199            input.disabled = isLoading;
200            sendButton.disabled = isLoading;
201            sendButton.textContent = isLoading ? 'Working' : 'Send';
202        }
203
204        function scrollToBottom() {
205            body.scrollTop = body.scrollHeight;
206        }
207
208        function syncAssistantCartBadge(value, source) {
209            if (!cartCount) {
210                return;
211            }
212            var count = parseInt(String(value || '').replace(/[^0-9]/g, ''), 10) || 0;
213            lastCartSync = {
214                source: source || 'unknown',
215                value: count,
216                at: Date.now()
217            };
218            var label = count > 99 ? '99+' : String(count);
219            if (cartCount.textContent !== label) {
220                cartCount.textContent = label;
221            }
222            if (cartCount.classList.contains('is-empty') !== (count <= 0)) {
223                cartCount.classList.toggle('is-empty', count <= 0);
224            }
225        }
226
227        function pageCartCount() {
228            var badges = document.querySelectorAll('[id^="cart_qty"]');
229            if (!badges.length) {
230                return null;
231            }
232            var found = false;
233            var count = 0;
234            Array.prototype.forEach.call(badges, function (badge) {
235                if (cartCount && badge === cartCount) {
236                    return;
237                }
238                var value = parseInt(String(badge.textContent || '').replace(/[^0-9]/g, ''), 10);
239                if (!isNaN(value)) {
240                    found = true;
241                    count = Math.max(count, value);
242                }
243            });
244            return found ? count : null;
245        }
246
247        function refreshAssistantCartBadge() {
248            var pageCount = pageCartCount();
249            if (pageCount !== null) {
250                syncAssistantCartBadge(pageCount, 'page');
251                return;
252            }
253            if (!cartEndpoint) {
254                return;
255            }
256            var cartUrl = new URL(cartEndpoint, window.location.href);
257            if (cartUrl.origin !== window.location.origin) {
258                cartUrl = new URL(cartUrl.pathname + cartUrl.search, window.location.href);
259            }
260            cartUrl.searchParams.set('v', Date.now());
261            fetch(cartUrl.toString(), {
262                method: 'GET',
263                cache: 'no-store',
264                headers: {
265                    'X-Requested-With': 'XMLHttpRequest',
266                    'Accept': 'application/json'
267                },
268                credentials: 'same-origin'
269            }).then(function (response) {
270                return response.json();
271            }).then(function (data) {
272                var quantity = data && data.cart ? data.cart.total_quantity : 0;
273                if (pageCartCount() === null) {
274                    syncAssistantCartBadge(quantity, 'server');
275                }
276            }).catch(function () {
277                // The header cart badge is helpful, but the assistant should still work if this fails.
278            });
279        }
280
281        function watchSiteCartBadge() {
282            if (!window.MutationObserver) {
283                return;
284            }
285            var observer = new MutationObserver(function () {
286                var count = pageCartCount();
287                if (count !== null) {
288                    if (lastCartSync.source === 'server' && Date.now() - lastCartSync.at < 1500 && count !== lastCartSync.value) {
289                        window.setTimeout(refreshAssistantCartBadge, 250);
290                        return;
291                    }
292                    syncAssistantCartBadge(count, 'page');
293                }
294            });
295            Array.prototype.forEach.call(document.querySelectorAll('[id^="cart_qty"]'), function (badge) {
296                if (!cartCount || badge !== cartCount) {
297                    observer.observe(badge, { childList: true, characterData: true, subtree: true });
298                }
299            });
300            if (document.body) {
301                observer.observe(document.body, { childList: true, subtree: true });
302            }
303        }
304
305        function primeAssistantCartBadge() {
306            var attempts = 0;
307            if (initialCartQty !== null) {
308                syncAssistantCartBadge(initialCartQty, 'initial');
309            }
310            refreshAssistantCartBadge();
311            var timer = window.setInterval(function () {
312                attempts++;
313                refreshAssistantCartBadge();
314                if (attempts >= 24) {
315                    window.clearInterval(timer);
316                }
317            }, 500);
318        }
319
320        function loadSession() {
321            if (!canStore) {
322                return { messages: [], open: false, suggestions: [] };
323            }
324            try {
325                var parsed = JSON.parse(window.localStorage.getItem(storageKey) || '{}');
326                return {
327                    messages: Array.isArray(parsed.messages) ? parsed.messages.filter(validStoredMessage).slice(-ASSISTANT_SESSION_MESSAGE_LIMIT) : [],
328                    open: !!parsed.open,
329                    suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.filter(function (prompt) {
330                        return typeof prompt === 'string' && prompt.trim();
331                    }).slice(0, 5) : []
332                };
333            } catch (error) {
334                return { messages: [], open: false, suggestions: [] };
335            }
336        }
337
338        function saveSession() {
339            if (!canStore) {
340                return;
341            }
342            try {
343                window.localStorage.setItem(storageKey, JSON.stringify({
344                    messages: conversation.slice(-ASSISTANT_SESSION_MESSAGE_LIMIT).map(compactMessage),
345                    open: root.classList.contains('is-open'),
346                    suggestions: activeSuggestions.slice(0, 5)
347                }));
348            } catch (error) {
349                // Ignore storage quota/privacy failures; the assistant still works for the current page.
350            }
351        }
352
353        function compactMessage(message) {
354            return {
355                role: message.role === 'user' ? 'user' : 'assistant',
356                text: String(message.text || '').slice(0, 2500),
357                tools: Array.isArray(message.tools) ? message.tools.slice(0, 8) : [],
358                cards: Array.isArray(message.cards) ? message.cards.slice(0, 4) : [],
359                morePrompt: String(message.morePrompt || '').slice(0, 80),
360                createdAt: message.createdAt || Date.now()
361            };
362        }
363
364        function validStoredMessage(message) {
365            return message && (message.role === 'user' || message.role === 'assistant') && typeof message.text === 'string';
366        }
367
368        function readSuggestionButtons(container) {
369            if (!container) {
370                return [];
371            }
372            return Array.prototype.map.call(container.querySelectorAll('[data-prompt]'), function (button) {
373                return String(button.getAttribute('data-prompt') || '').trim();
374            }).filter(Boolean);
375        }
376
377        function ensureStarter() {
378            var starter = document.getElementById('wtso-assistant-starter');
379            if (starter) {
380                return starter;
381            }
382            if (!initialStarterHtml) {
383                return null;
384            }
385            var wrapper = document.createElement('div');
386            wrapper.innerHTML = initialStarterHtml;
387            starter = wrapper.firstElementChild;
388            if (!starter) {
389                return null;
390            }
391            body.insertBefore(starter, body.firstChild);
392            return starter;
393        }
394
395        function setSuggestions(prompts) {
396            var starter = ensureStarter();
397            var suggestions = [];
398            if (Array.isArray(prompts)) {
399                suggestions = prompts.filter(function (prompt) {
400                    return typeof prompt === 'string' && prompt.trim();
401                }).slice(0, 5);
402            }
403            if (!suggestions.length) {
404                suggestions = defaultSuggestions.slice(0, 5);
405            }
406            activeSuggestions = suggestions.slice(0);
407            if (!starter) {
408                saveSession();
409                return;
410            }
411
412            var container = starter.querySelector('#wtso-assistant-suggestions');
413            if (!container) {
414                saveSession();
415                return;
416            }
417            container.innerHTML = activeSuggestions.map(function (prompt) {
418                var clean = String(prompt || '').trim();
419                return '<button type="button" data-prompt="' + escapeHtml(clean) + '">' + escapeHtml(clean) + '</button>';
420            }).join('');
421            saveSession();
422        }
423
424        function createRecord(role, text) {
425            var record = {
426                role: role === 'user' ? 'user' : 'assistant',
427                text: String(text || ''),
428                tools: [],
429                cards: [],
430                createdAt: Date.now()
431            };
432            conversation.push(record);
433            if (conversation.length > ASSISTANT_SESSION_MESSAGE_LIMIT) {
434                conversation = conversation.slice(-ASSISTANT_SESSION_MESSAGE_LIMIT);
435            }
436            saveSession();
437            return record;
438        }
439
440        function recentHistory(prompt) {
441            var recent = conversation.slice(-ASSISTANT_REQUEST_HISTORY_LIMIT);
442            if (recent.length) {
443                var last = recent[recent.length - 1];
444                if (last.role === 'user' && last.text === prompt) {
445                    recent = recent.slice(0, -1);
446                }
447            }
448            return recent.filter(function (message) {
449                return message.text && message.text.trim();
450            }).map(function (message) {
451                return {
452                    role: message.role,
453                    text: message.text.slice(0, ASSISTANT_HISTORY_TEXT_LIMIT)
454                };
455            });
456        }
457
458        function appendMessage(role, text, options) {
459            options = options || {};
460            var record = options.record || null;
461            if (options.persist !== false && !record) {
462                record = createRecord(role, text);
463            }
464
465            var wrapper = document.createElement('div');
466            wrapper.className = 'wtso-assistant-message wtso-assistant-message-' + role;
467
468            var bubble = document.createElement('div');
469            bubble.className = 'wtso-assistant-bubble';
470
471            var reply = document.createElement('div');
472            reply.className = 'wtso-assistant-reply';
473            reply.innerHTML = renderReply(text);
474
475            var status = document.createElement('div');
476            status.className = 'wtso-assistant-status';
477            status.hidden = true;
478
479            var tools = document.createElement('div');
480            tools.className = 'wtso-assistant-tool-list';
481            tools.hidden = true;
482
483            var cards = document.createElement('div');
484            cards.className = 'wtso-assistant-cards';
485            cards.hidden = true;
486
487            var more = document.createElement('div');
488            more.className = 'wtso-assistant-more';
489            more.hidden = true;
490
491            var actions = document.createElement('div');
492            actions.className = 'wtso-assistant-actions';
493            actions.hidden = true;
494
495            bubble.appendChild(reply);
496            bubble.appendChild(status);
497            bubble.appendChild(tools);
498            bubble.appendChild(cards);
499            bubble.appendChild(more);
500            bubble.appendChild(actions);
501            wrapper.appendChild(bubble);
502            body.appendChild(wrapper);
503            scrollToBottom();
504
505            return {
506                wrapper: wrapper,
507                bubble: bubble,
508                reply: reply,
509                status: status,
510                tools: tools,
511                cards: cards,
512                more: more,
513                actions: actions,
514                text: String(text || ''),
515                toolNames: [],
516                record: record,
517                streamEnded: false,
518                setText: function (value) {
519                    this.text = String(value || '');
520                    this.reply.innerHTML = renderReply(this.text);
521                    if (this.record) {
522                        this.record.text = this.text;
523                        saveSession();
524                    }
525                    scrollToBottom();
526                },
527                appendText: function (value) {
528                    this.text += String(value || '');
529                    this.reply.innerHTML = renderReply(this.text, { streaming: true });
530                    if (this.record) {
531                        this.record.text = this.text;
532                        saveSession();
533                    }
534                    scrollToBottom();
535                },
536                setThinking: function (label, message) {
537                    this.wrapper.classList.add('is-thinking');
538                    this.setStatus(label || 'Thinking', message || 'Reading your question');
539                },
540                clearThinking: function () {
541                    this.wrapper.classList.remove('is-thinking');
542                    this.status.hidden = true;
543                },
544                setStatus: function (label, message) {
545                    this.status.hidden = false;
546                    this.status.innerHTML = '<span class="wtso-assistant-status-label">' + escapeHtml(label) + '</span>' +
547                        '<span>' + escapeHtml(message || '') + '</span>' +
548                        '<span class="wtso-assistant-dots" aria-hidden="true"><i></i><i></i><i></i></span>';
549                    scrollToBottom();
550                },
551                addTool: function (name) {
552                    if (!name || this.toolNames.indexOf(name) !== -1) {
553                        return;
554                    }
555                    this.toolNames.push(name);
556                    if (this.record && this.record.tools.indexOf(name) === -1) {
557                        this.record.tools.push(name);
558                        saveSession();
559                    }
560                    this.renderTools();
561                },
562                setTools: function (calls) {
563                    var self = this;
564                    self.toolNames = [];
565                    (calls || []).forEach(function (call) {
566                        if (call && call.name && self.toolNames.indexOf(call.name) === -1) {
567                            self.toolNames.push(call.name);
568                        }
569                    });
570                    if (this.record) {
571                        this.record.tools = this.toolNames.slice(0, 8);
572                        saveSession();
573                    }
574                    this.renderTools();
575                },
576                renderTools: function () {
577                    if (!this.toolNames.length) {
578                        this.tools.hidden = true;
579                        return;
580                    }
581                    this.tools.hidden = false;
582                    this.tools.innerHTML = this.toolNames.map(function (name) {
583                        return '<span>' + escapeHtml(toolLabel(name)) + '</span>';
584                    }).join('');
585                    scrollToBottom();
586                },
587                setCards: function (items) {
588                    if (!items || !items.length) {
589                        this.cards.hidden = true;
590                        this.cards.innerHTML = '';
591                        return;
592                    }
593                    this.cards.hidden = false;
594                    this.cards.innerHTML = renderCards(items);
595                    if (this.record) {
596                        this.record.cards = items.slice(0, 4);
597                        saveSession();
598                    }
599                    scrollToBottom();
600                },
601                setMorePrompt: function (prompt) {
602                    if (!this.more) {
603                        return;
604                    }
605                    prompt = String(prompt || '').trim();
606                    if (!prompt) {
607                        this.more.hidden = true;
608                        this.more.innerHTML = '';
609                        if (this.record) {
610                            this.record.morePrompt = '';
611                            saveSession();
612                        }
613                        return;
614                    }
615                    this.more.hidden = false;
616                    this.more.innerHTML = '<button type="button" data-prompt="' + escapeHtml(prompt) + '">' + escapeHtml(prompt) + '</button>';
617                    if (this.record) {
618                        this.record.morePrompt = prompt;
619                        saveSession();
620                    }
621                    scrollToBottom();
622                },
623                setActions: function (items) {
624                    var html = renderActions(items || []);
625                    if (!html) {
626                        this.actions.hidden = true;
627                        this.actions.innerHTML = '';
628                        return;
629                    }
630                    this.actions.hidden = false;
631                    this.actions.innerHTML = html;
632                    scrollToBottom();
633                }
634            };
635        }
636
637        function renderActions(actions) {
638            return actions.filter(function (action) {
639                return action && (
640                    action.type === 'customer_service_email'
641                    || action.type === 'open_cart'
642                    || action.type === 'open_login'
643                    || action.type === 'case_club_ship_all'
644                    || action.type === 'case_club_show_bank'
645                    || action.type === 'case_club_clear_selection'
646                    || action.type === 'case_club_prepare_selection_shipment'
647                    || action.type === 'case_club_create_shipment'
648                    || action.type === 'taste_profile_question'
649                    || action.type === 'change_order_ship_date'
650                    || action.type === 'change_order_address'
651                    || action.type === 'cancel_order'
652                    // Was missing, so prepare_add_to_cart queued an action that nothing could
653                    // draw: the reply said "click the Add to Cart button below" and no button
654                    // ever appeared.
655                    || action.type === 'add_to_cart'
656                );
657            }).map(function (action) {
658                var id = 'a' + nextActionId++;
659                pendingActions[id] = action;
660                if (action.type === 'taste_profile_question') {
661                    var profileTitle = String(action.title || 'Taste profile').slice(0, 120);
662                    var profileSummary = String(action.summary || 'Choose the closest answer.').slice(0, 180);
663                    var optionHtml = '';
664                    if (Array.isArray(action.options)) {
665                        optionHtml = action.options.map(function (option) {
666                            if (!option || !option.type) {
667                                return '';
668                            }
669                            var optionId = 'a' + nextActionId++;
670                            pendingActions[optionId] = option;
671                            return '<button type="button" data-assistant-confirm-action="' + escapeHtml(optionId) + '">' + escapeHtml(String(option.label || 'Choose').slice(0, 48)) + '</button>';
672                        }).join('');
673                    }
674                    return '<div class="wtso-assistant-action wtso-assistant-action-taste-profile" data-assistant-action-id="' + escapeHtml(id) + '">' +
675                        '<div><strong>' + escapeHtml(profileTitle) + '</strong><span>' + escapeHtml(profileSummary) + '</span></div>' +
676                        '<div class="wtso-assistant-choice-grid">' + optionHtml + '</div>' +
677                        '</div>';
678                }
679                if (action.type === 'open_cart') {
680                    return '<div class="wtso-assistant-action wtso-assistant-action-cart" data-assistant-action-id="' + escapeHtml(id) + '">' +
681                        '<div><strong>Ready when you are</strong><span>Review cart contents and continue checkout.</span></div>' +
682                        '<button type="button" data-assistant-confirm-action="' + escapeHtml(id) + '">View Cart</button>' +
683                        '</div>';
684                }
685                if (action.type === 'open_login') {
686                    return '<div class="wtso-assistant-action" data-assistant-action-id="' + escapeHtml(id) + '">' +
687                        '<div><strong>Sign in to your account</strong><span>
687Access your orders, cellar, and personalized recommendations.</span></div>' +
688                        '<button type="button" data-assistant-confirm-action="' + escapeHtml(id) + '">Sign In</button>' +
689                        '</div>';
690                }
691                if (action.type === 'add_to_cart') {
692                    var addQty = Math.max(1, parseInt(action.quantity, 10) || 1);
693                    var addName = String(action.product_name || 'this wine').slice(0, 90);
694                    return '<div class="wtso-assistant-action wtso-assistant-action-cart" data-assistant-action-id="' + escapeHtml(id) + '">' +
695                        '<div><strong>Add to cart</strong><span>' +
696                        escapeHtml(addQty + (addQty === 1 ? ' bottle' : ' bottles') + ' of ' + addName) +
697                        '</span></div>' +
698                        '<button type="button" data-assistant-confirm-action="' + escapeHtml(id) + '">Add to Cart</button>' +
699                        '</div>';
700                }
701                if (action.type === 'case_club_ship_all' || action.type === 'case_club_show_bank' || action.type === 'case_club_clear_selection' || action.type === 'case_club_prepare_selection_shipment' || action.type === 'case_club_create_shipment') {
702                    var caseTitle = String(action.title || 'Build Case Club shipment').slice(0, 120);
703                    var caseSummary = String(action.summary || 'Create a shipment plan and review it before checkout.').slice(0, 160);
704                    var caseLabel = String(action.label || (action.type === 'case_club_create_shipment' ? 'Create Shipment' : 'Build Shipment')).slice(0, 40);
705                    return '<div class="wtso-assistant-action wtso-assistant-action-case-club" data-assistant-action-id="' + escapeHtml(id) + '">' +
706                        '<div><strong>' + escapeHtml(caseTitle) + '</strong><span>' + escapeHtml(caseSummary) + '</span></div>' +
707                        '<button type="button" data-assistant-confirm-action="' + escapeHtml(id) + '">' + escapeHtml(caseLabel) + '</button>' +
708                        '</div>';
709                }
710                if (action.type === 'change_order_ship_date') {
711                    var shipDate = String(action.ship_date || '').slice(0, 20);
712                    var shipOrderId = String(action.order_id || '').slice(0, 20);
713                    return '<div class="wtso-assistant-action" data-assistant-action-id="' + escapeHtml(id) + '">' +
714                        '<div><strong>Change ship date — Order #' + escapeHtml(shipOrderId) + '</strong><span>Ship on ' + escapeHtml(shipDate) + '</span></div>' +
715                        '<button type="button" data-assistant-confirm-action="' + escapeHtml(id) + '">Confirm</button>' +
716                        '</div>';
717                }
718                if (action.type === 'change_order_address') {
719                    var addrLabel = String(action.address_label || 'Selected address').slice(0, 160);
720                    var addrOrderId = String(action.order_id || '').slice(0, 20);
721                    return '<div class="wtso-assistant-action" data-assistant-action-id="' + escapeHtml(id) + '">' +
722                        '<div><strong>Ship order #' + escapeHtml(addrOrderId) + ' to</strong><span>' + escapeHtml(addrLabel) + '</span></div>' +
723                        '<button type="button" data-assistant-confirm-action="' + escapeHtml(id) + '">Confirm</button>' +
724                        '</div>';
725                }
726                if (action.type === 'cancel_order') {
727                    var cancelOrderId = String(action.order_id || '').slice(0, 20);
728                    return '<div class="wtso-assistant-action" data-assistant-action-id="' + escapeHtml(id) + '">' +
729                        '<div><strong>Cancel order #' + escapeHtml(cancelOrderId) + '</strong><span>This cannot be undone once confirmed.</span></div>' +
730                        '<button type="button" data-assistant-confirm-action="' + escapeHtml(id) + '">Cancel Order</button>' +
731                        '</div>';
732                }
733                var subject = String(action.subject || 'Customer service request').slice(0, 120);
734                return '<div class="wtso-assistant-action" data-assistant-action-id="' + escapeHtml(id) + '">' +
735                    '<div><strong>Ready to send</strong><span>' + escapeHtml(subject) + '</span></div>' +
736                    '<button type="button" data-assistant-confirm-action="' + escapeHtml(id) + '">Send to Customer Service</button>' +
737                    '</div>';
738            }).join('');
739        }
740
741        function renderCards(cards) {
742            var expandedCaseClub = cards.every(function (card) {
743                var source = String(card.catalog_source || card.source || '');
744                return source === 'case_club_selection' || source === 'case_club_shipment';
745            });
746            return cards.slice(0, expandedCaseClub ? 12 : 4).map(function (card) {
747                var details = [card.varietal, card.appellation, card.region, card.country].filter(
747Boolean).join(' / ');
748                var price = money(card.price);
749                var retail = money(card.retail_price);
750                var source = String(card.catalog_source || card.source || '');
751                var isCaseClubBank = source === 'case_club_bank';
752                var isCaseClubSelection = source === 'case_club_selection';
753                // A flash deal or Collector's Drop wine has to be bought as its own order, and the
754                // chat cart posts to a bucket that is neither one. Send the customer to the real
755                // offer page instead of adding here or opening a detail popup bound to that cart.
756                var isLiveOffer = source === 'current_offer' || source === 'collectors_drop';
757                var offerUrl = isLiveOffer ? String(card.url || '') : '';
758                var canAdd = !!price && !isCaseClubBank && !isCaseClubSelection && !isLiveOffer;
759                var badges = [];
760                if (Array.isArray(card.personalization_badges)) {
761                    card.personalization_badges.slice(0, 3).forEach(function (badge) {
762                        if (badge) {
763                            badges.push(badge);
764                        }
765                    });
766                }
767                if (card.party_role) {
768                    badges.push(card.party_role);
769                }
770                if (card.recommended_quantity) {
771                    badges.push(parseInt(card.recommended_quantity, 10) + ' bottles');
772                }
773                if (card.percent_off) {
774                    badges.push(card.percent_off + '% off');
775                }
776                if (card.rating) {
777                    badges.push(card.rating + ' pts');
778                }
779                // card.inventory is now withheld above a dozen bottles, so the badge reads the
780                // low_inventory flag instead; the old inventory < 100 test would have shown the
781                // badge on almost nothing.
782                if (card.low_inventory) {
783                    badges.push('Low inventory');
784                }
785
786                var ratings = Array.isArray(card.ratings) ? card.ratings.slice(0, 3) : [];
787                var productId = parseInt(card.product_id, 10) || 0;
788                var storeId = parseInt(card.store_id, 10) || 0;
789                var html = '<article class="wtso-assistant-card" data-product-id="' + escapeHtml(productId) + '" data-store-id="' + escapeHtml(storeId) + '" data-card-source="' + escapeHtml(source) + '">';
790                if (offerUrl) {
791                    html += '<a class="wtso-assistant-card-image" href="' + escapeHtml(offerUrl) + '" aria-label="View this offer"><img src="' + escapeHtml(card.image) + '" alt=""></a>';
792                } else {
793                    html += '<button type="button" class="wtso-assistant-card-image" data-assistant-action="details" aria-label="View wine details"><img src="' + escapeHtml(card.image) + '" alt=""></button>';
794                }
795                html += '<div class="wtso-assistant-card-copy">';
796                if (badges.length) {
797                    html += '<div class="wtso-assistant-card-badges">' + badges.map(function (badge) {
798                        return '<span>' + escapeHtml(badge) + '</span>';
799                    }).join('') + '</div>';
800                }
801                html += '<h3>' + escapeHtml(card.name) + '</h3>';
802                if (card.party_reason) {
803                    html += '<p class="wtso-assistant-card-role">' + escapeHtml(card.party_reason) + '</p>';
804                }
805                if (details) {
806                    html += '<p class="wtso-assistant-card-meta">' + escapeHtml(details) + '</p>';
807                }
808                if (ratings.length) {
809                    html += '<div class="wtso-assistant-card-ratings">' + ratings.map(function (rating) {
810                        var ratingText = String(rating.text || '').trim();
811                        var initials = String(rating.initials || '').trim();
812                        var label = ratingText || initials;
813                        return '<span>' + escapeHtml(label) + '</span>';
814                    }).join('') + '</div>';
815                }
816                html += '<div class="wtso-assistant-card-row">';
817                html += '<div>';
818                if (isCaseClubBank) {
819                    html += '<span class="wtso-assistant-card-bank-count">' + escapeHtml(parseInt(card.bank_quantity, 10) || 0) + ' banked</span>';
820                } else if (isCaseClubSelection) {
821                    html += '<span class="wtso-assistant-card-bank-count">' + escapeHtml(parseInt(card.selected_quantity, 10) || parseInt(card.bank_quantity, 10) || 0) + ' selected</span>';
822                } else {
823                    html += '<span class="wtso-assistant-card-price">' + escapeHtml(price) + '</span>';
824                }
825                if (retail) {
826                    html += '<span class="wtso-assistant-card-retail">' + escapeHtml(retail) + '</span>';
827                }
828                html += '</div>';
829                html += '<div class="wtso-assistant-card-actions">';
830                if (offerUrl) {
831                    html += '<a class="wtso-assistant-card-add" href="' + escapeHtml(offerUrl) + '">'
832                        + escapeHtml(source === 'collectors_drop' ? 'Shop the Drop' : 'Shop this offer') + '</a>';
833                } else {
834                    html += '<button type="button" class="wtso-assistant-card-link" data-assistant-action="details">View</button>';
835                    if (isCaseClubBank) {
836                        html += '<button type="button" class="wtso-assistant-card-add wtso-assistant-card-select" data-assistant-action="case-club-select" data-quantity="1">Select</button>';
837                    } else if (isCaseClubSelection) {
838                        html += '<button type="button" class="wtso-assistant-card-add wtso-assistant-card-select" data-assistant-action="case-club-select" data-quantity="1">+1</button>';
839                        html += '<button type="button" class="wtso-assistant-card-link wtso-assistant-card-remove" data-assistant-action="case-club-remove" data-quantity="1">Remove</button>';
840                    }
841                    if (canAdd) {
842                        var qty = Math.max(1, parseInt(card.recommended_quantity, 10) || 1);
843                        var addLabel = qty > 1 ? 'Add ' + qty : 'Add';
844                        html += '<button type="button" class="wtso-assistant-card-add" data-assistant-action="add" data-quantity="' + escapeHtml(qty) + '">' + escapeHtml(addLabel) + '</button>';
845                    }
846                }
847                html += '</div>';
848                html += '</div>';
849                html += '</div>';
850                html += '</article>';
851                return html;
852            }).join('');
853        }
854
855        function assistantOpenDetails(productId) {
856            if (!productId) {
857                return;
858            }
859            if (typeof window.open_details === 'function') {
860                window.open_details(productId, assistantCartType);
861                return;
862            }
863            if (!window.jQuery) {
864                window.location.href = '/wine-search?terms=' + encodeURIComponent(productId);
865                return;
866            }
867
868            var $ = window.jQuery;
869            if (!$('#product_detail').length || !$('#returnDisplay').length) {
870                window.location.href = '/wine-search?terms=' + encodeURIComponent(productId);
871                return;
872            }
873            $('#returnDisplay').html('<center><img src="' + baseUrl() + 'assets/images/winebottle-loading.gif" /></center>');
874            $('.cart_checkout_btn').css('display', 'none');
875            $('#product_detail').modal('show');
876            $.getJSON(baseUrl() + 'cart/product_detail_pop/' + productId + '/' + encodeURIComponent(assistantCartType) + '?v=' + Date.now(), function (data) {
877                $('#modal-title').html('Wine Details');
878                $('#returnDisplay').html(data && data.reply ? data.reply : 'Product details are unavailable.');
879            });
880        }
881
882        function assistantAddToCart(productId, button) {
883            if (!productId || button.disabled) {
884                return;
885            }
886
887            var quantity = Math.min(99, Math.max(1, parseInt(button.getAttribute('data-quantity'), 10) || 1));
888            var originalText = button.textContent;
889            button.disabled = true;
890            button.textContent = 'Adding';
891
892            addCartQuantity(productId, quantity).then(function (data) {
893                if (!data || data.cart_total === null || data.error) {
894                    throw new Error(data && data.error ? data.error : 'Unable to add this wine.');
895                }
896                syncCartDisplays(data);
897                button.textContent = quantity > 1 ? 'Added ' + quantity : 'Added';
898                setTimeout(function () {
899                    button.disabled = false;
900                    button.textContent = originalText;
901                }, 1400);
902            }).catch(function (error) {
903                button.disabled = false;
904                button.textContent = originalText;
905                window.alert(error && error.message ? error.message : 'Unable to add this wine.');
906            });
907        }
908
909        function addCartQuantity(productId, quantity) {
910            var lastData = null;
911            var chain = Promise.resolve();
912            for (var i = 0; i < quantity; i++) {
913                chain = chain.then(function () {
914                    return fetch(baseUrl() + 'cart/update_cart?products_id=' + encodeURIComponent(productId) + '&type=' + encodeURIComponent(assistantCartType) + '&action=inc', {
915                        method: 'GET',
916                        headers: {
917                            'X-Requested-With': 'XMLHttpRequest',
918                            'Accept': 'application/json'
919                        },
920                        credentials: 'same-origin'
921                    }).then(function (response) {
922                        return response.json();
923                    }).then(function (data) {
924                        lastData = data;
925                        if (!data || data.cart_total === null || data.error) {
926                            throw new Error(data && data.error ? data.error : 'Unable to add this wine.');
927                        }
928                        return data;
929                    });
930                });
931            }
932            return chain.then(function () {
933                return lastData;
934            });
935        }
936
937        function syncCartDisplays(data) {
938            if (window.jQuery) {
939                var $ = window.jQuery;
940                $('input[id^="bottles_' + data.products_id + '"]').val(data.new_qty);
941                $('select[id^="' + data.products_id + '_qty"]').val(data.new_qty);
942                if (typeof window.update_displays === 'function') {
943                    window.update_displays(data);
944                    syncAssistantCartBadge(data.cart_qty, 'server');
945                    return;
946                }
947                $('div[id^="cart_total"]').html(data.cart_total);
948                $('div[id^="cart_qty"]').html(data.cart_qty);
949                syncAssistantCartBadge(data.cart_qty, 'server');
950                if (typeof window.async_crdl_cart === 'function') {
951                    window.async_crdl_cart();
952                }
953            }
954        }
955
956        function assistantOpenCart() {
957            refreshAssistantCartBadge();
958            if (typeof window.view_cart === 'function') {
959                window.view_cart();
960                return;
961            }
962            if (window.jQuery && window.jQuery('#product_detail').length && window.jQuery('#returnDisplay').length) {
963                var $ = window.jQuery;
964                $('#returnDisplay').html('');
965                $('#product_detail').modal('show');
966                $.getJSON(baseUrl() + 'cart/view_cart', function (data) {
967                    $('#modal-title').html('My Cart');
968                    $('#returnDisplay').html(data && data.reply ? data.reply : 'Your cart is unavailable.');
969                    $('#cart_checkout_btn').css('display', 'inline-block');
970                });
971                return;
972            }
973            window.location.href = baseUrl() + 'cart/checkout';
974        }
975
976        function sendAssistantAction(actionId, button) {
977            var action = pendingActions[actionId];
978            if (!action || button.disabled) {
979                return;
980            }
981            if (action.type === 'open_cart') {
982                assistantOpenCart();
983                return;
984            }
985            if (action.type === 'open_login') {
986                if (typeof window.openAccountDrawer === 'function') {
987                    window.openAccountDrawer();
988                }
989                return;
990            }
991            if (!actionEndpoint) {
992                return;
993            }
994
995            var originalText = button.textContent;
996            button.disabled = true;
997            button.textContent = (action.type.indexOf('taste_profile_') === 0)
998                ? (action.type === 'taste_profile_save' ? 'Saving' : 'Selected')
999                : (action.type.indexOf('case_club_') === 0)
1000                ? (action.type === 'case_club_create_shipment' ? 'Creating' : 'Working')
1001                : 'Sending';
1002
1003            fetch(actionEndpoint, {
1004                method: 'POST',
1005                headers: {
1006                    'Content-Type': 'application/json',
1007                    'X-Requested-With': 'XMLHttpRequest'
1008                },
1009                credentials: 'same-origin',
1010                body: JSON.stringify({ action: action })
1011            }).then(function (response) {
1012                return response.json().then(function (data) {
1013                    if (!response.ok || !data || !data.ok) {
1014                        throw new Error(data && data.error ? data.error : 'Unable to send this message.');
1015                    }
1016                    return data;
1017                });
1018            }).then(function (data) {
1019                delete pendingActions[actionId];
1020                button.textContent = data.redirect_url ? 'Opening' : 'Done';
1021                button.closest('.wtso-assistant-action').classList.add('is-complete');
1022                appendAssistantResult(data, data.message || 'Done.');
1023                if (action.type === 'add_to_cart') {
1024                    // The cart changed on the server, so the header badge is now stale.
1025                    refreshAssistantCartBadge();
1026                }
1027                if (data.redirect_url) {
1028                    window.setTimeout(function () {
1029                        window.location.href = data.redirect_url;
1030                    }, 350);
1031                }
1032            }).catch(function (error) {
1033                button.disabled = false;
1034                button.textContent = originalText;
1035                window.alert(error && error.message ? error.message : 'Unable to send this message.');
1036            });
1037        }
1038
1039        function sendCaseClubSelection(card, mode, button) {
1040            if (!actionEndpoint || !card || !button || button.disabled) {
1041                return;
1042            }
1043
1044            var productId = parseInt(card.getAttribute('data-product-id'), 10) || 0;
1045            var storeId = parseInt(card.getAttribute('data-store-id'), 10) || 0;
1046            if (!productId && !storeId) {
1047                return;
1048            }
1049
1050            var originalText = button.textContent;
1051            button.disabled = true;
1052            button.textContent = mode === 'remove' ? 'Removing' : 'Selecting';
1053
1054            fetch(actionEndpoint, {
1055                method: 'POST',
1056                headers: {
1057                    'Content-Type': 'application/json',
1058                    'X-Requested-With': 'XMLHttpRequest'
1059                },
1060                credentials: 'same-origin',
1061                body: JSON.stringify({
1062                    action: {
1063                        type: 'case_club_select_bank_item',
1064                        mode: mode,
1065                        product_id: productId,
1066                        store_id: storeId,
1067                        quantity: Math.max(1, parseInt(button.getAttribute('data-quantity'), 10) || 1)
1068                    }
1069                })
1070            }).then(function (response) {
1071                return response.json().then(function (data) {
1072                    if (!response.ok || !data || !data.ok) {
1073                        throw new Error(data && data.error ? data.error : 'Unable to update this shipment selection.');
1074                    }
1075                    return data;
1076                });
1077            }).then(function (data) {
1078                button.textContent = mode === 'remove' ? 'Removed' : 'Selected';
1079                appendAssistantResult(data, 'Updated your Case Club shipment selection.');
1080            }).catch(function (error) {
1081                button.disabled = false;
1082                button.textContent = originalText;
1083                window.alert(error && error.message ? error.message : 'Unable to update this shipment selection.');
1084            });
1085        }
1086
1087        function appendAssistantResult(data, fallbackText) {
1088            var message = appendMessage('assistant', data && data.message ? data.message : fallbackText);
1089            if (data && data.tool_calls) {
1090                message.setTools(data.tool_calls);
1091            }
1092            if (data && data.products) {
1093                message.setCards(data.products);
1094            } else if (data && data.cards) {
1095                message.setCards(data.cards);
1096            }
1097            var actions = [];
1098            if (data && Array.isArray(data.actions)) {
1099                actions = data.actions;
1100            } else if (data && data.action) {
1101                actions = [data.action];
1102            }
1103            message.setActions(actions);
1104            return message;
1105        }
1106
1107        function baseUrl() {
1108            return typeof window.BASEURL === 'string' && window.BASEURL ? window.BASEURL : '/';
1109        }
1110
1111        function requestBody(prompt) {
1112            return JSON.stringify({
1113                message: prompt,
1114                context: {
1115                    url: window.location.href,
1116                    path: window.location.pathname,
1117                    history: recentHistory(prompt)
1118                }
1119            });
1120        }
1121
1122        function applyFinal(message, data) {
1123            message.clearThinking();
1124            if (data && data.reply) {
1125                message.setText(data.reply);
1126            } else if (!message.text.trim()) {
1127                message.setText(data.reply || 'Done.');
1128            }
1129            message.setTools(data.tool_calls || []);
1130            message.setCards(data.cards || []);
1131            message.setMorePrompt(data.more_prompt || '');
1132            message.setActions(data.actions || []);
1133            setSuggestions(data.suggestions || []);
1134        }
1135
1136        function sendJson(prompt, assistantMessage) {
1137            return fetch(endpoint, {
1138                method: 'POST',
1139                headers: {
1140                    'Content-Type': 'application/json',
1141                    'X-Requested-With': 'XMLHttpRequest'
1142                },
1143                credentials: 'same-origin',
1144                body: requestBody(prompt)
1145            }).then(function (response) {
1146                return response.json();
1147            }).then(function (data) {
1148                if (!data || !data.ok) {
1149                    assistantMessage.clearThinking();
1150                    assistantMessage.setText(data && data.error ? data.error : 'The assistant endpoint did not return a usable response.');
1151                    return;
1152                }
1153                applyFinal(assistantMessage, data);
1154            });
1155        }
1156
1157        function sendStream(prompt, assistantMessage) {
1158            if (!streamEndpoint || !window.ReadableStream || !window.TextDecoder) {
1159                return Promise.reject(new Error('Streaming is not supported.'));
1160            }
1161            assistantMessage.streamEnded = false;
1162
1163            return fetch(streamEndpoint, {
1164                method: 'POST',
1165                headers: {
1166                    'Content-Type': 'application/json',
1167                    'X-Requested-With': 'XMLHttpRequest',
1168                    'Accept': 'text/event-stream'
1169                },
1170                credentials: 'same-origin',
1171                body: requestBody(prompt)
1172            }).then(function (response) {
1173                if (!response.ok || !response.body) {
1174                    throw new Error('Streaming endpoint failed.');
1175                }
1176
1177                var reader = response.body.getReader();
1178                var decoder = new TextDecoder();
1179                var buffer = '';
1180
1181                function read() {
1182                    return reader.read().then(function (result) {
1183                        if (result.done) {
1184                            buffer += decoder.decode();
1185                            if (buffer.trim()) {
1186                                processSseBlock(buffer, assistantMessage);
1187                            }
1188                            if (!assistantMessage.streamEnded) {
1189                                throw new Error('Streaming ended before the final event.');
1190                            }
1191                            return;
1192                        }
1193                        buffer += decoder.decode(result.value, { stream: true });
1194                        var parts = buffer.split(/\r?\n\r?\n/);
1195                        buffer = parts.pop();
1196                        parts.forEach(function (part) {
1197                            processSseBlock(part, assistantMessage);
1198                        });
1199                        return read();
1200                    });
1201                }
1202
1203                return read();
1204            });
1205        }
1206
1207        function processSseBlock(block, assistantMessage) {
1208            var eventName = 'message';
1209            var dataLines = [];
1210            block.split(/\r?\n/).forEach(function (line) {
1211                if (!line || line.charAt(0) === ':') {
1212                    return;
1213                }
1214                var colon = line.indexOf(':');
1215                var field = colon === -1 ? line : line.slice(0, colon);
1216                var value = colon === -1 ? '' : line.slice(colon + 1);
1217                if (value.charAt(0) === ' ') {
1218                    value = value.slice(1);
1219                }
1220                if (field === 'event') {
1221                    eventName = value.trim();
1222                } else if (field === 'data') {
1223                    dataLines.push(value);
1224                } else if (field === 'retry') {
1225                    var retry = parseInt(value, 10);
1226                    if (!isNaN(retry)) {
1227                        sseRetryDelay = retry;
1228                    }
1229                }
1230            });
1231
1232            if (!dataLines.length) {
1233                return;
1234            }
1235
1236            var data;
1237            try {
1238                data = JSON.parse(dataLines.join('\n'));
1239            } catch (error) {
1240                try {
1241                    data = JSON.parse(dataLines.join(''));
1242                } catch (secondError) {
1243                    if (window.console && window.console.warn) {
1244                        window.console.warn('Assistant stream sent malformed JSON.', secondError);
1245                    }
1246                    return;
1247                }
1248            }
1249
1250            if (eventName === 'status') {
1251                assistantMessage.setStatus(data.label || 'Working', data.message || '');
1252            } else if (eventName === 'tool') {
1253                assistantMessage.addTool(data.name);
1254            } else if (eventName === 'delta') {
1255                assistantMessage.wrapper.classList.remove('is-thinking');
1256                assistantMessage.status.hidden = true;
1257                assistantMessage.appendText(data.text || '');
1258            } else if (eventName === 'final') {
1259                assistantMessage.streamEnded = true;
1260                applyFinal(assistantMessage, data);
1261            } else if (eventName === 'error') {
1262                assistantMessage.streamEnded = true;
1263                assistantMessage.clearThinking();
1264                assistantMessage.setText(data.message || 'The assistant could not complete the request.');
1265            }
1266        }
1267
1268        function sendMessage(text) {
1269            var prompt = String(text || '').trim();
1270            if (!prompt || root.classList.contains('wtso-assistant-loading')) {
1271                return;
1272            }
1273
1274            appendMessage('user', prompt);
1275            var assistantMessage = appendMessage('assistant', '');
1276            assistantMessage.setThinking('Thinking', 'Reading your question');
1277            input.value = '';
1278            setLoading(true);
1279
1280            sendStream(prompt, assistantMessage).catch(function () {
1281                if (assistantMessage.streamEnded) {
1282                    return;
1283                }
1284                assistantMessage.clearThinking();
1285                assistantMessage.setText('');
1286                assistantMessage.setTools([]);
1287                assistantMessage.setCards([]);
1288                assistantMessage.setMorePrompt('');
1289                assistantMessage.setActions([]);
1290                assistantMessage.setThinking('Working', 'Using the standard assistant endpoint');
1291                return sendJson(prompt, assistantMessage);
1292            }).catch(function () {
1293                assistantMessage.clearThinking();
1294                assistantMessage.setText('The assistant could not reach the site tools. Try again in a moment.');
1295            }).finally(function () {
1296                setLoading(false);
1297                input.focus();
1298            });
1299        }
1300
1301        launcher.addEventListener('click', function () {
1302            setOpen(!root.classList.contains('is-open'));
1303        });
1304        close.addEventListener('click', function () {
1305            setOpen(false);
1306        });
1307        if (backdrop) {
1308            backdrop.addEventListener('click', function () {
1309                setOpen(false);
1310            });
1311        }
1312        reset.addEventListener('click', function () {
1313            conversation = [];
1314            saveSession();
1315            body.innerHTML = initialBodyHtml;
1316            activeSuggestions = defaultSuggestions.slice(0, 5);
1317            setSuggestions(activeSuggestions);
1318            input.value = '';
1319            input.focus();
1320        });
1321        if (cartButton) {
1322            cartButton.addEventListener('click', function () {
1323                assistantOpenCart();
1324            });
1325        }
1326        form.addEventListener('submit', function (event) {
1327            event.preventDefault();
1328            sendMessage(input.value);
1329        });
1330        body.addEventListener('click', function (event) {
1331            var promptButton = event.target.closest('[data-prompt]');
1332            if (promptButton) {
1333                sendMessage(promptButton.getAttribute('data-prompt'));
1334                return;
1335            }
1336
1337            var confirmActionButton = event.target.closest('[data-assistant-confirm-action]');
1338            if (confirmActionButton) {
1339                sendAssistantAction(confirmActionButton.getAttribute('data-assistant-confirm-action'), confirmActionButton);
1340                return;
1341            }
1342
1343            var actionButton = event.target.closest('[data-assistant-action]');
1344            if (actionButton) {
1345                var card = actionButton.closest('.wtso-assistant-card');
1346                var productId = card ? parseInt(card.getAttribute('data-product-id'), 10) : 0;
1347                if (actionButton.getAttribute('data-assistant-action') === 'details') {
1348                    assistantOpenDetails(productId);
1349                } else if (actionButton.getAttribute('data-assistant-action') === 'add') {
1350                    assistantAddToCart(productId, actionButton);
1351                } else if (actionButton.getAttribute('data-assistant-action') === 'case-club-select') {
1352                    sendCaseClubSelection(card, 'add', actionButton);
1353                } else if (actionButton.getAttribute('data-assistant-action') === 'case-club-remove') {
1354                    sendCaseClubSelection(card, 'remove', actionButton);
1355                }
1356            }
1357        });
1358
1359        if (conversation.length) {
1360            body.innerHTML = '';
1361            setSuggestions(activeSuggestions);
1362            conversation.forEach(function (message) {
1363                var restored = appendMessage(message.role, message.text, { persist: false });
1364                restored.setTools((message.tools || []).map(function (name) { return { name: name }; }));
1365                restored.setCards(message.cards || []);
1366                restored.setMorePrompt(message.morePrompt || '');
1367            });
1368        } else {
1369            setSuggestions(activeSuggestions);
1370        }
1371        if (savedSession.open) {
1372            setOpen(true);
1373        }
1374        primeAssistantCartBadge();
1375        watchSiteCartBadge();
1376    });
1377})();

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.