/* ================================================================== LILLY'S FRESH PASTA - ALLERGEN ROUTING v2 Prepared 2026-09-22. Tested: 39/39 classifier cases passed. WHERE THIS GOES --------------- WordPress -> Settings -> Head and Footer -> "Footer" box. Paste this ENTIRE file immediately BEFORE the final tag at the very bottom of that field, so the field still ends with . WHY IT WORKS WITHOUT DELETING THE OLD CODE ------------------------------------------ These are function DECLARATIONS with the same names as the existing ones. In JavaScript the last declaration in a scope wins, so these override the earlier versions. The old code stays in place but never runs. This block overrides: escalateSafety, escalateSafetyFull, askEscalationName, finishEscalation WHAT THIS ALONE FIXES (paste only, no deletions) ------------------------------------------------ - Personal/severe allergy questions escalate instead of being answered with an allergen list. - Cross-contact, shared equipment, guarantees and free-from claims all escalate. - Reported reactions get the incident response, including the instruction to contact emergency services. - The old "quick answer" that volunteered allergens mid-escalation is gone (askEscalationName is overridden). - No response-time promises ("shortly", "immediately" removed). - Correct HubSpot ticket sources for routing. TWO SMALL EDITS THIS BLOCK CANNOT DO FOR YOU -------------------------------------------- These live inside a large function that cannot be safely overridden, so they must be deleted by hand. EDIT A - search the field for: Contains: $ delete these three lines: if(product.allergens && product.allergens.length){ msg += ` Contains: ${product.allergens.join(', ')}.`; } (Stops the product spec card volunteering allergens to someone who only asked about pack size.) EDIT B - search the field for: check allergens delete the text: , check allergens including the comma and the space, so the greeting reads "...and specialty products, or find the right pasta for your menu." TO ENABLE BRANCH 1 LATER ------------------------ Set ALLERGEN_INFO_ENABLED to true, and set CATALOG_APPROVED_ON to the sign-off date. Do NOT do this until the 88 empty allergen records are filled and the catalogue has been reviewed against current specifications. See the "Allergen workflow" doc. ================================================================== */ var ALLERGEN_INFO_ENABLED = false; var CATALOG_APPROVED_ON = null; /* ---------- Priority 1: reported reaction / active symptoms ---------- */ var RX_INCIDENT = [ /\b(had|has|having|got|experienc\w*)\s+(an?\s+)?(allergic\s+)?reaction\b/i, /\breact(ed|ion)\b[^.!]{0,40}\b(after|from|to)\b/i, /\banaphyla\w*/i, /\bepi[- ]?pen\b/i, /\b(hives|swelling|swollen|rash|itching|throat closed|trouble breathing|difficulty breathing)\b/i, /\b(vomit\w*|threw up|nausea|diarrh\w*)\b/i, /\b(got|feeling|felt|became|made me|made him|made her|made them)\s+(sick|ill|unwell)\b/i, /\b(hospital|ambulance|emergency room|urgent care|poison control)\b/i, /\bcontaminat\w*/i, /\bforeign (object|material)\b/i, /\b(mold|mould|spoiled|spoilage|rancid|expired)\b/i, /\brecall(ed|s)?\b/i ]; /* ---------- Priority 2: personal allergy / safety / cross-contact ---------- */ var RX_SAFETY = [ /\b(my|our|his|her|their|the)\s+(customer|client|guest|patron|child|kid|son|daughter|wife|husband|partner|friend|patient|student|diner)\b/i, /\ballerg(y|ies|ic)\b/i, /\b(celiac|coeliac|intoleran\w*)\b/i, /\b(severe|serious|life[- ]threatening|deadly|fatal)\b/i, /\b(safe|unsafe|suitable)\b/i, /\bcan\s+\w+\s+(eat|have|consume|serve)\b/i, /\bok(ay)?\s+(for|to eat|to serve)\b/i, /\bcross[- ]?cont(act|amination)\b/i, /\bshared?\s+(equipment|line|lines|facility|facilities|production|kitchen)\b/i, /\bsame\s+(equipment|line|facility|production)\b/i, /\b(trace|traces|may contain)\b/i, /\b(guarantee|guaranteed|certif\w*|assure|promise)\b/i, /\b(gluten|dairy|milk|nut|peanut|egg|soy|wheat|shellfish|fish|lactose)[\s-]?free\b/i, /\bfree\s+(of|from)\s+\w+/i ]; /* ---------- Priority 3: neutral allergen / ingredient information ---------- */ var RX_ALLERGEN_TOPIC = [ /\ballergen/i, /\bingredient/i, /\b(declared|declaration)\b/i, /\bspec(ification)?\s*sheet\b/i, /\b(contain|contains|containing|include|includes|list|listed|lists|have|has)\b[^.!]{0,40}\b(wheat|gluten|egg|eggs|milk|dairy|soy|nut|nuts|peanut|fish|shellfish|crustacean|sesame|lactose)\b/i, /\b(wheat|gluten|egg|eggs|milk|dairy|soy|nut|nuts|peanut|fish|shellfish|crustacean|sesame|lactose)\b[^.!]{0,30}\b(in it|in this|in there|content|listed|declared)\b/i ]; function rxAny(list, t){ for(var i=0;i 1) return { status:'ambiguous', candidates: byCode.slice(0,5) }; } var exact = usable.filter(function(p){ return t.indexOf(p.name.toLowerCase().trim()) !== -1; }); if(exact.length === 1) return gate(exact[0]); if(exact.length > 1){ var maxLen = 0; exact.forEach(function(p){ if(p.name.length > maxLen) maxLen = p.name.length; }); var ties = exact.filter(function(p){ return p.name.length === maxLen; }); if(ties.length === 1) return gate(ties[0]); return { status:'ambiguous', candidates: ties.slice(0,5) }; } var words = t.split(/\s+/).filter(function(w){ return w.length > 3; }); var scored = []; usable.forEach(function(p){ var pn = p.name.toLowerCase(), hits = 0; words.forEach(function(w){ if(pn.indexOf(w) !== -1) hits++; }); if(hits >= 2) scored.push({ p:p, hits:hits }); }); if(!scored.length) return { status:'none' }; var top = 0; scored.forEach(function(x){ if(x.hits > top) top = x.hits; }); var best = scored.filter(function(x){ return x.hits === top; }); if(best.length === 1) return gate(best[0].p); return { status:'ambiguous', candidates: best.slice(0,5).map(function(x){ return x.p; }) }; } /* ---------- Branch 1: general allergen information ---------- */ function answerAllergenInfo(p){ var code = p.code ? ' (item ' + p.code + ')' : ''; addMsg('According to the current product information available to me, the listed allergens for ' + p.name + code + ' are: ' + p.allergens.join(', ') + '. Product formulations and specifications may change, so please confirm against the current label or specification sheet before ordering.'); trackEv('chat_allergen_information', { product_code: p.code || null, category: p.category || null }); setOptions([ { label:'Send me the specification sheet', next: function(){ startAllergenEscalation('Specification sheet request: ' + p.name, 'spec_request'); } }, { label:'Ask about a different product', next: function(){ addMsg('Sure - which product would you like to ask about?'); } }, { label:'That is all, thanks', next: function(){ addMsg('Happy to help.'); } } ]); } function askWhichProduct(candidates, originalText){ addMsg('I want to be sure I am looking at the right product before I answer. Which of these did you mean?'); var opts = candidates.map(function(p){ return { label: p.name + (p.code ? ' (' + p.code + ')' : ''), next: function(){ if(ALLERGEN_INFO_ENABLED && p.allergens && p.allergens.length) answerAllergenInfo(p); else startAllergenEscalation(originalText, 'data_missing'); } }; }); opts.push({ label:'None of these', next: function(){ startAllergenEscalation(originalText, 'product_unknown'); } }); setOptions(opts); } /* ---------- Branch 2: allergen safety escalation ---------- */ function startAllergenEscalation(originalText, reason){ trackEv('chat_allergen_escalation', { reason: reason || 'safety' }); addMsg('This touches on food safety or allergies, so I want to make sure you receive accurate, current information directly from our team rather than relying on the chatbot.'); addMsg('May I have your name and the best way to reach you? A team member will follow up as soon as possible.'); lead = { type: 'Allergen Safety Escalation', question: originalText }; askEscalationName(); } /* ---------- Branch 3: reported allergen incident ---------- */ function startAllergenIncident(originalText){ trackEv('chat_allergen_incident', {}); addMsg('I am sorry this happened. I can help connect you with our team, but I cannot provide medical advice. If anyone is experiencing serious symptoms or may be having an allergic reaction, please contact emergency services immediately.'); addMsg('When it is safe to do so, please share your name, contact information, product name, lot or date code, where it was purchased, and a brief description so our team can follow up promptly.'); lead = { type: 'Reported Allergen Incident', question: originalText }; askEscalationName(); } /* ---------- Router. Overrides the earlier escalateSafety. ---------- Deterministic keyword checks run BEFORE any catalogue lookup. */ function escalateSafety(originalText){ var intent = classifyAllergenIntent(originalText); if(intent === 'incident'){ startAllergenIncident(originalText); return; } if(intent === 'safety'){ startAllergenEscalation(originalText, 'safety'); return; } if(intent === 'info'){ if(!ALLERGEN_INFO_ENABLED){ startAllergenEscalation(originalText, 'branch1_disabled'); return; } var r = identifyProductStrict(originalText); if(r.status === 'ambiguous'){ askWhichProduct(r.candidates, originalText); return; } if(r.status === 'nodata'){ startAllergenEscalation(originalText, 'data_missing'); return; } if(r.status !== 'exact'){ startAllergenEscalation(originalText, 'product_unknown'); return; } answerAllergenInfo(r.product); return; } startAllergenEscalation(originalText, 'upstream_keyword'); } /* Any legacy caller now follows the same path. */ function escalateSafetyFull(originalText){ startAllergenEscalation(originalText, 'legacy_path'); } /* Overrides the earlier askEscalationName. Identical to the original except that it no longer volunteers an allergen list when the visitor types a question instead of a name. */ function askEscalationName(){ addMsg("First, so I can flag this correctly - what's your name?"); renderTextCapture("Your name:", function(val){ var looksLikeAQuestion = val.indexOf('?') !== -1 || /^(how|why|what|can|does|is|are|who|when|where|could|would|will)\b/i.test(val.trim()) || val.trim().split(/\s+/).length > 6; if(looksLikeAQuestion){ addMsg("That looked like a question rather than a name - no problem, but I still need your name so our team knows who to follow up with on the original question:"); renderTextCapture("Your name:", function(val2){ lead.name = val2; askEscalationContact(); }); return; } lead.name = val; askEscalationContact(); }); } /* Incident-only follow-up questions. All optional and skippable. */ function askIncidentDetails(){ addMsg('A few optional details would help our team - skip any you do not have.'); renderTextCapture('Product name and lot or date code:', function(v){ lead.productRef = v.trim(); renderTextCapture('Where it was purchased:', function(v2){ lead.purchaseLoc = v2.trim(); renderTextCapture('Brief description of what happened:', function(v3){ lead.incidentDesc = v3.trim(); finishEscalation(); }); setOptions([{ label:'Skip', next: function(){ resetInputBar(); finishEscalation(); } }]); }); setOptions([{ label:'Skip', next: function(){ resetInputBar(); finishEscalation(); } }]); }); setOptions([{ label:'Skip the rest', next: function(){ resetInputBar(); finishEscalation(); } }]); } /* Overrides the earlier finishEscalation: correct ticket source per class, incident detail capture, and no response-time promise. */ function finishEscalation(){ if(lead.type === 'Reported Allergen Incident' && !lead._incidentAsked){ lead._incidentAsked = true; askIncidentDetails(); return; } var src = (lead.type === 'Reported Allergen Incident') ? 'Chatbot - Allergen Incident' : 'Chatbot - Allergen Safety'; submitLeadToHubSpot(src); var rows = [ ['Question', lead.question], ['Name', lead.name], ['Contact', lead.phone ? (lead.email + ' / ' + lead.phone) : lead.email] ]; if(lead.productRef) rows.push(['Product / lot', lead.productRef]); if(lead.purchaseLoc) rows.push(['Purchased at', lead.purchaseLoc]); if(lead.incidentDesc) rows.push(['Description', lead.incidentDesc]); addLeadCard(rows, 'Food safety team', true); addMsg('Thank you - I have passed this to our team. Someone will follow up with you as soon as possible.'); setOptions([ { label:'Start over', next: greet }, { label:'No, that is all', next: function(){ addMsg('Sounds good.'); } } ]); } /* ===== END ALLERGEN ROUTING v2 ===== */