/* ==================================================================
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 ===== */