";
while(d.firstChild) document.body.appendChild(d.firstChild);
})();
(function() {
// ─────────────────────────────────────────────────────────────────────────
// KNOWLEDGE BASE — extracted from Lioher product PDFs
// ─────────────────────────────────────────────────────────────────────────
const KNOWLEDGE_BASE = "=== LIOHER KNOWLEDGE BASE ===\n\n--- CABINET BOXES (Premium Frameless) ---\nConstruction: 18mm (3/4\") thickness, 8mm (5/16\") back panel with dowels. Frameless design.\nCore material: MDF with moisture-resistant core (PB P3 HYDRO X CARB2/EPA particleboard).\nKey features:\n- Moisture Resistant & Antibacterial: MDF core with PUR-banded edges, protects against extreme humidity, prevents 99.99% of bacteria/microbes, subdues mold and fungus.\n- Reliable Construction: Boxes made exactly to size ordered, in square, easier to install.\n- Construction on Legs: Adjustable legs for leveling, keeps box off ground in case of standing water.\nVs Plywood: Lioher boxes outperform plywood in moisture resistance, dimensional accuracy, and water protection.\nCertifications: CARB Phase 2 / EPA TSCA, FSC, PEFC, E1 formaldehyde, Fire D-s2 d0.\n\n--- LUXE COLLECTION (High Gloss Panels) ---\nComposition: MDF core with decorative paper, UV lacquered front. PUR glue and ABS edges on finished components.\nApplications: Furniture, cabinetry, decorative components (vertical and horizontal surfaces).\nFinish performance: Stain Resistance Class 5 (except coffee), Cold Liquids Class 5, Crack Resistance Class 5, Cold check no change, Color Fastness Blue>6/Grey 5, Dry/Damp Heat Class 5, Scratch 16N, Abrasion Class 4 (433 cycles), Antibacterial 100%, Gloss 90 GU (very high gloss), Warp 2mm/m.\nFormaldehyde: E1 (E0.5/CARB2/TSCA). Fire: D-s2 d0.\nCleaning: Non-abrasive cloth, soap and water only. No solvents, alcohol, or ammonia.\nStorage: 10-40C, 30-70% humidity. Keep protective film until installation.\n\n--- ZENIT COLLECTION (Super Matt Panels) ---\nNew Generation 3.0. Composition: MDF base board with decorative paper both sides, UV lacquered front. PUR glue and ABS/PMMA edges.\nApplications: Vertical and horizontal surfaces.\nFinish performance: Stain Grade 5, Cold Liquids Grade 5, Crack Grade 5, Cold check no damage, Light fastness Blue>6/Grey 5, Dry/Damp Heat Grade 5, Scratch Class 4 (5N), Abrasion Class 4 (700 cycles), Antibacterial 100%, Gloss 4 GU (true super matt), Warp 2mm/m.\nFormaldehyde: E1 (E0.5/CARB2/TSCA). Fire: D-s2 d0.\nCleaning: Non-abrasive cloth, soap and water only. No solvents, alcohol, or ammonia.\nStorage: 10-40C, 30-70% humidity.\n\n--- SYNCRON COLLECTION (Texture Panels) ---\nComposition: Particle board (PB) with decorative paper both sides. Realistic texture with registered finishes - complete match between texture and design.\nApplications: Vertical applications, furniture, cabinetry, decorative components.\nFinish performance: Stain Grade 5, Cold Liquids Grade 5, Crack Grade 5, Cold check no effect, Color Fastness Blue>6/Grey 5, Dry Heat (100C) Grade 5, Damp Heat (85C) Grade 5, Scratch 18N, Abrasion Class 3A (208 cycles), Antibacterial 100%, Warp 2mm/m.\nFormaldehyde: E1 (E0.5/CARB2/TSCA). Fire: D-s2 d0.\nCleaning: Non-abrasive cloth, soap and water only. No solvents, alcohol, or ammonia.\n\n--- OUTDOOR KITCHENS ---\nTarget: Professional installers, Retailers, Builders.\nLead time: 3-4 weeks (fully assembled).\nSelling points: Premium high-end line with competitive pricing. Innovative European cabinetry for outdoors. Weather-resistant. Installer-friendly (fully assembled, adjustable legs, easy modular connection). Beautiful designs for interior and exterior.\nConstruction features:\n1. Groove-assembled with epoxy glue, brass inserts and stainless-steel screws\n2. Cabinet box: Phenolic Board (standard) or Tricoya (based on availability)\n3. 4 pre-drilled side holes for modular connection\n4. Gas/BBQ cabinets: built-in ventilation grilles\n5. Doors & sides: Tricoya with UV overlay, 7 designs, interior in soft Linen design\n6. Stainless steel hardware\n7. Heavy duty adjustable PVC legs (6\" to 6.5\"), 6\" toe kick, 36\" decorative side panels\n8. Fully assembled\nPhenolic Board vs Aluminum: Phenolic wins on thermal (insulates heat - better near grills), aesthetics (more customization), workability (easier to cut/drill), cost (lower). Both excellent on water resistance. Aluminum is naturally UV-stable and lighter.\nPrice tier: $$ to $$$\n\n--- LIOHER CLOSETS (Modular Closets & Organizers) ---\nKey benefit: Always in stock, no lead time for standard products.\nDesign: Lino Miami (light textile finish, warm interior).\nFeatures: Fully modular (any size), easy/quick assembly, slim construction with shared panels, 3D configurator software available.\nUse cases: Closets, garages, laundry rooms, offices, hallways, bedrooms, entrances, shops.\nAvailable sizes:\n- Height: 84\" or 96\"\n- Width: 18\", 24\", 30\", 36\" (total shelf width, not including sides)\n- Depth: 14\", 16\", 19\", 24\"\n- Panel thickness: 3/4\"\nComponents: Side panels, Share panels, Shelves (fixed or adjustable, tiltable as shoe rack), Drawers, Toe kick.\nDrawer heights: 5-5/8\", 8-3/4\", 9-15/16\", 10-11/16\". Available for 16\", 19\", 24\" depth. Hidden/slim system, anthracite finish, whisper-quiet operation.\nAccessories: Wardrobe tube (18-36\"), Full rotation mirror (35\"/47-3/8\"), Pull-out shelf (18-36\"), Pant organizer (18-36\"), Wire closet basket, Wardrobe lift (max 26 lbs, 24-36\"), Valet hook, Pull-out shoe organizer (18-36\"), Divided lingerie drawer (18-36\"), Shelf shoe fence (18-36\"), Belt rack (6 hook, 13-7/8\"), Double hook, Jewelry organizer (18-36\"). All in Black finish.\nDoor upgrades: 100+ Lioher designs available, extended lead times. Contact customer service.\nAssembly: Rafix system for stability, 32mm line-drilling pattern.\nWall backing: 4x9 matching panels at 8mm thickness (installs before closet, not traditional backing).\nWarranty: 5-year limited warranty.\n\n--- SLAT WALL ---\nProduct: Decorative wall and ceiling covering panels.\nApplications: Residential interiors, offices and studios, retail and hospitality, public and institutional spaces.\nSpecifications: Size 108.5\" x 49\". Install directly to wall or with air chamber for improved acoustics.\nFeatures: High-definition Syncron texture, durable, stain-resistant, easy to clean, antibacterial treatment, sound absorption (acoustic version), eco-conscious materials.\nAvailable designs: Como Ash 2, Nocce 2, Rosales 2, Rosales 3, Gris Plomo SM, Black SM.\nCustom finishes: Available with 20-panel minimum order.\n\n--- AVAILABILITY CHART 2026/2027 ---\nTotal: 107 designs across three collections.\n\nLUXE (High Gloss) designs include:\nSolids: AGUA MARINA HG, ALBERO HG, ANTRACITA HG, ANTRACITA PE, ARENA HG, AZUL INDIGO HG, AZUL MARINO HG, BASALTO HG, BLACK HG, BLANCO HG (Finger Pull, Laminates), BLANCO PE, BLANCO POLAR HG, CASHMERE HG (Finger Pull, Laminates), GRIS NUBE HG, GRIS PERLA HG, GRIS PLOMO HG, POMPEI HG\nWood: EUROLINE 3 HG, GUAYANA HG, OLIVO HG, OLMO 3 HG, PICASSO 1 HG, ROSALES 3 HG, VELAZQUEZ 1 HG\nFantasy: CUZCO ORO HG, GRIS METALLIC HG, METALLO 1 HG, METALLO 4 HG, TEXTIL PLATA HG\nMarble/Stone: BERNINI HG, NUVOLA 3 HG, PORCELAIN 1, SIENA HG\nPearl: ANTRACITA PE, BLANCO PE\nPrice tiers: Classic A, Classic C, Select, Premium\n\nSYNCRON (Texture) designs include:\nWood: ALHAMBRA 1/2/3, ANV OAK 2, BLACK MINERVA, CASHMERE MINERVA, COMO ASH 1/2/3, FRAPPE 1/3, ICE 1, IDA 1/2/3, LAKELAND 3, MURATTI 1/2/3/4, NOCCE 1/3, OLMO 3, PICASSO 1/2/3, ROSALES 1/2/3/4, VELAZQUEZ 1/2/5, WHITE MINERVA, WOODLINE 1/3/4\nMarble/Stone: BERNINI, BRUSHED CONCRETE 1 (has ClimaCore), BRUSHED CONCRETE 2, EVORA 4, SIENA, VERDE NOSTA\nFantasy: TEXTIL PLATA\nReeded: REEDED BLACK, REEDED BLANCO, REEDED CASHMERE\nSome designs available in 8mm: COMO ASH 2, MURATTI 4, NOCCE 1, ROSALES 1/2/3, WOODLINE 3\nSome with Shaker option: COMO ASH 2, MURATTI 4, NOCCE 1, ROSALES 1/2/3, WOODLINE 3\n\nZENIT (Super Matt) designs include:\nSolids: AGAVE SM, AGUA MARINA SM, ALBA WHITE SM, ALBERO SM, ANTRACITA SM, ARENA SM (Laminates SOON), AZUL INDIGO SM, AZUL MARINO SM, BASALTO SM, BLACK SM, BLANCO SM, BLANCO POLAR SM, CASHMERE SM, CORAL SM, GRIS NUBE SM, GRIS PERLA SM, GRIS PLOMO SM, POMPEI SM, TAUPE SM (Laminates SOON), VERDE SALVIA SM\nMD premium: BLACK MD, BLANCO MD\nMetal Plus: CHAMPAGNE MP, LIGHT GOLD MP, TITANIO MP\nMarble/Stone: NUVOLA 3 SM, TITAN 1 SM\nWith Shaker: AGAVE SM, ANTRACITA SM, BLANCO SM, CASHMERE SM\nWith Finger Pull: BLACK SM, BLANCO SM, CASHMERE SM\n\nFEATURE GLOSSARY:\n- Sequence match: Design repeats across panels for seamless look\n- Finger Pull: Has integrated finger pull groove (no separate handle needed)\n- Mallorca: Available in Mallorca project/format\n- Cava: Available in Cava format\n- Shaker: Available in Shaker door style\n- Laminates Y=available, SOON=coming soon\n- ClimaCore: Available with ClimaCore moisture technology\n- Double side Y=finished both sides, N=one side only\n- Edge 41: Compatible with Edge 41 edging system\n- 8mm thick: Available in thinner 8mm option\n\n--- WARRANTY & CONTACT ---\nAll products: 5-year limited warranty.\nPhone: 305-685-0005\nWebsite: lioher.com\nWholesale only - works exclusively with trade professionals (contractors, builders, installers, interior designers, architects). Homeowners welcome for design consultations.\n\n\n--- PRICING (Partner Package 2026 \u2014 List Price / Before Discount) ---\n\nIMPORTANT: These are LIST prices. Trade professionals always receive discounts through our Loyalty Program. Never quote these as final prices \u2014 always mention discounts are available.\n\nPANELS (108x48\" sheet, 3/4\" / 18mm):\nLuxe / Zenit:\n Classic A: $7.50/sqft \u2014 $271/sheet\n Classic B: $9.01/sqft \u2014 $325/sheet\n Classic C: $9.52/sqft \u2014 $344/sheet\n Select: $10.02/sqft \u2014 $362/sheet\n Premium: $12.54/sqft \u2014 $453/sheet\n 8mm panels: $8.01/sqft \u2014 $289/sheet\nSyncron:\n Classic: $5.49/sqft \u2014 $198/sheet\n Select: $5.99/sqft \u2014 $216/sheet\n Premium: $6.50/sqft \u2014 $235/sheet\n 8mm panels: $4.48/sqft \u2014 $162/sheet\n\nLAMINATES (96x48\"):\n Luxe/Zenit & Syncron: $2.84/sqft \u2014 $91/sheet\n Syncron laminate: $4.25/sqft \u2014 $136/sheet\n\nSLATWALL:\n Zenit: $10.52/sqft \u2014 $380/sheet\n Syncron: $9.52/sqft \u2014 $344/sheet\n Pattern drill: $46/sheet (net price, no discount)\n\nMELAMINE PANEL:\n White: $2.26/sqft \u2014 $81.44/sheet\n White 8mm: $1.80/sqft \u2014 $65.15/sheet\n\nEDGEBANDING (7/8\" / 23mm):\nLuxe/Zenit cut to size: Classic A/B $0.47/ft, Classic C $0.55/ft, Select $1.00/ft, Premium $1.09/ft\nLuxe/Zenit full roll: Classic A/B $0.56/ft, Classic C $0.66/ft, Select $1.20/ft, Premium $1.31/ft\n1-5/8\" (41mm) cut: $1.82/ft | full roll: $2.18/ft\nSyncron all: $0.40/ft cut | $0.48/ft full roll\nSyncron 1-5/8\": $1.73/ft cut | $2.08/ft full roll\n\nDOORS & DRAWER FRONTS ($/sqft, min 1 sqft per piece):\nLuxe/Zenit Slab: Classic A $17.27 | Classic B&C $24.55 | Select $25.45 | Premium $27.27 | Sequence $29.09\nLuxe/Zenit Finger Pull: Classic A/B/C $30.91\nLuxe/Zenit Cava: Classic B&C $38.18\nLuxe/Zenit Mallorca: Classic A $32.73 | Classic B&C $34.55 | Select $35.45 | Premium $40.00\nSyncron Slab: Classic $17.27 | Select $17.73 | Premium $20.00 | Sequence $21.82\nSyncron Finger Pull: Classic/Select $29.09\nSyncron Cava: Classic/Select $29.09\nSyncron Mallorca: Classic $28.28 | Select $29.09 | Premium $31.82\nDrilling: $2.45 to $2.94 extra\nLead times: Slab 2-3 weeks | Finger Pull/Cava/Mallorca 3-4 weeks\nOrders >150 pieces: 6-week lead time\n\n--- DOOR STYLE AVAILABILITY BY DESIGN (from official 2026-27 chart) ---\nIMPORTANT: Use this data to answer ANY question about which door styles are available for a specific design. Do NOT guess \u2014 use this list.\n\nALL designs come standard with Slab door.\nAdditional door styles listed below (Y = available):\n\nLUXE (High Gloss) designs:\n- BLANCO HG: Slab, Finger Pull, Mallorca + Laminate\n- BLANCO POLAR HG: Slab, Mallorca + Laminate\n- CASHMERE HG: Slab, Finger Pull, Mallorca + Laminate\n- AGUA MARINA HG: Slab, Mallorca\n- ALBERO HG: Slab, Mallorca\n- ANTRACITA HG: Slab, Mallorca\n- ARENA HG: Slab, Mallorca\n- AZUL INDIGO HG: Slab, Mallorca\n- AZUL MARINO HG: Slab, Mallorca\n- BASALTO HG: Slab, Mallorca\n- BERNINI HG: Slab, Mallorca\n- BLACK HG: Slab, Mallorca\n- NUVOLA 3 HG: Slab, Mallorca\n- CUZCO ORO HG: Slab, Mallorca\n- EUROLINE 3 HG: Slab, Mallorca\n- GRIS METALLIC HG: Slab, Mallorca\n- GRIS NUBE HG: Slab, Mallorca\n- GRIS PERLA HG: Slab, Mallorca\n- GRIS PLOMO HG: Slab, Mallorca\n- GUAYANA HG: Slab, Mallorca\n- METALLO 1 HG: Slab, Mallorca\n- METALLO 4 HG: Slab, Mallorca\n- OLIVO HG: Slab, Mallorca\n- OLMO 3 HG: Slab, Mallorca\n- PICASSO 1 HG: Slab, Mallorca\n- POMPEI HG: Slab, Mallorca\n- PORCELAIN 1 HG: Slab, Mallorca\n- ROSALES 3 HG: Slab, Mallorca\n- SIENA HG: Slab, Mallorca\n- TEXTIL PLATA HG: Slab, Mallorca\n- VELAZQUEZ 1 HG: Slab, Mallorca\n\nZENIT (SuperMatt) designs:\n- CASHMERE SM: Slab, Finger Pull, Mallorca, Cava/Shaker + Laminate\n- BLANCO SM: Slab, Finger Pull, Mallorca, Cava/Shaker + Laminate\n- BLACK SM: Slab, Finger Pull, Mallorca + Laminate\n- ANTRACITA SM: Slab, Mallorca, Cava/Shaker + Laminate\n- AGAVE SM: Slab, Mallorca, Cava/Shaker\n- AZUL INDIGO SM: Slab, Mallorca + Laminate\n- AZUL MARINO SM: Slab, Mallorca + Laminate\n- BASALTO SM: Slab, Mallorca + Laminate\n- BLANCO POLAR SM: Slab, Mallorca + Laminate\n- GRIS NUBE SM: Slab, Mallorca + Laminate\n- GRIS PERLA SM: Slab, Mallorca + Laminate\n- GRIS PLOMO SM: Slab, Mallorca + Laminate\n- VERDE SALVIA SM: Slab, Mallorca + Laminate\n- AGUA MARINA SM: Slab, Mallorca\n- ALBA WHITE SM: Slab, Mallorca\n- ALBERO SM: Slab, Mallorca\n- ARENA SM: Slab, Mallorca\n- CORAL SM: Slab, Mallorca\n- NUVOLA 3 SM: Slab, Mallorca\n- POMPEI SM: Slab, Mallorca\n- TAUPE SM: Slab, Mallorca\n- TITAN 1 SM: Slab, Mallorca\n- VERDE SALVIA SM: Slab, Mallorca + Laminate\n\nSYNCRON (Textured) designs:\n- MURATTI 4: Slab, Finger Pull, Mallorca, Cava/Shaker + Laminate\n- NOCCE 1: Slab, Finger Pull, Mallorca, Cava/Shaker + Laminate\n- PICASSO 2: Slab, Finger Pull, Mallorca + Laminate\n- VELAZQUEZ 2: Slab, Finger Pull, Mallorca + Laminate\n- COMO ASH 2: Slab, Mallorca, Cava/Shaker + Laminate\n- ROSALES 1: Slab, Mallorca, Cava/Shaker + Laminate\n- ROSALES 2: Slab, Mallorca, Cava/Shaker + Laminate\n- ROSALES 3: Slab, Mallorca, Cava/Shaker + Laminate\n- WOODLINE 3: Slab, Mallorca, Cava/Shaker\n- ALHAMBRA 1/2/3: Slab, Mallorca + Laminate\n- ANV OAK 2: Slab, Mallorca + Laminate\n- COMO ASH 1/3: Slab, Mallorca + Laminate\n- FRAPPE 1/3: Slab, Mallorca\n- ICE 1: Slab, Mallorca + Laminate\n- IDA 1/2/3: Slab, Mallorca + Laminate\n- LAKELAND 3: Slab, Mallorca + Laminate\n- MURATTI 1/3: Slab, Mallorca + Laminate\n- MURATTI 2: Slab, Mallorca\n- NOCCE 3: Slab, Mallorca + Laminate\n- OLMO 3: Slab, Mallorca + Laminate\n- PICASSO 1/3: Slab, Mallorca + Laminate\n- REEDED BLACK: Slab, Mallorca\n- REEDED BLANCO: Slab, Mallorca\n- REEDED CASHMERE: Slab, Mallorca\n- ROSALES 4: Slab, Mallorca + Laminate\n- VELAZQUEZ 1/5: Slab, Mallorca + Laminate\n- WOODLINE 1: Slab, Mallorca\n- WOODLINE 4: Slab, Mallorca + Laminate\n- BERNINI: Slab, Mallorca\n- BLACK MINERVA: Slab, Mallorca\n- CASHMERE MINERVA: Slab, Mallorca\n- BRUSHED CONCRETE 1/2: Slab, Mallorca\n- EVORA 4: Slab, Mallorca\n- SIENA: Slab, Mallorca\n- TEXTIL PLATA: Slab, Mallorca\n- VERDE NOSTA: Slab, Mallorca\n- WHITE MINERVA: Slab, Mallorca\n\nCOMPACT SLAB (Countertops):\nBianco / White core / Black / Black core \u2014 Size B: $20.00/sqft \u2014 $1,100/sheet\nNuvola 3 / Bernini \u2014 Size B: $23.00/sqft \u2014 $1,265/sheet\nPalazzo / Tiziano / Opak Black \u2014 Size A: $27.40/sqft \u2014 $1,620/sheet\nHimalaya / Atenas / Statuario Bianco / Opak Blanco \u2014 Size A: $42.70/sqft \u2014 $2,562/sheet\nPositano \u2014 Size A: $48.00/sqft \u2014 $2,880/sheet\nAlicante \u2014 Size A: $51.10/sqft \u2014 $3,066/sheet\nServices (net, no discount): Regular Cut $8.00/lft | 45\u00b0 Cut $12.00/lft | Sink Cut $45.00 | Installation $8.00/lft | Sink Installation $22.00\n\nFLOATING SHELVES:\nSyncron: from $137 to $662\nZenit: from $149 to $750\nSize: Depth 11\" / Width 12\" to 90\" in 3\" increments | Thickness 1-3/8\" | Lead time 3 weeks\n\nHANDLES: Visit shop.lioher.com for all designs and sizes.\n\nASSEMBLED CABINETS (per linear foot, estimate based on 10 lft basic kitchen):\nSlab doors: Syncron from $425/lft | Luxe/Zenit from $499/lft\nGola Finger Pull: Syncron from $445/lft | Luxe/Zenit from $520/lft\nFinger Pull: Syncron from $464/lft | Luxe/Zenit from $544/lft\nCava/Mallorca: Syncron from $510/lft | Zenit from $584/lft\nLead time: 4-6 weeks. Assembled or unassembled. 200+ door & design combos. 700+ cabinet configs.\n\nPROMOTIONAL ITEMS (no discount):\nSamples: Luxe/Zenit/Syncron 3x5\" = $2 | Cava/Mallorca 12x15\" = $20 | FingerPull 6x12\" = $20\nSample Books: Book Luxe $70 | Book Zenit $60 | Book Syncron $70\nTower Display (fits 40 samples): Display + Samples = $500\nCustom Showroom Display: 30% rebate, up to $2,000/year credit per account.\n\n--- LOYALTY PROGRAM & DISCOUNTS ---\nLioher offers a tiered loyalty/discount program for trade professionals. Discounts depend on:\n- Purchase volume (different discount brackets based on how much you buy annually)\n- Account type and relationship with Lioher\n- Special project or bulk order pricing also available\n\nThe exact discount brackets are NOT shared publicly \u2014 they are discussed personally with the sales team to find the best fit for each partner's business.\n\nWhen asked about pricing: Give them the list price above, then ALWAYS add: \"As a trade professional, you'll receive a discount through our Loyalty Program \u2014 the exact discount depends on your purchase volume. Book a Quick Call with our team to learn which bracket applies to you.\"\n\nFor Projects & Bulk Orders: Lioher evaluates special pricing, modified ETAs, and timed delivery schedules case by case. Direct them to contact the sales team.\n\nWARRANTY:\n- Limited Lifetime Warranty on all cabinetry sold in USA and Canada (original purchaser only, non-transferable)\n- Does NOT cover: improper handling, water/chemical damage, normal wear and tear, improper installation\n- For cabinet doors: warping warranty only for doors 32\" or less (0.8mm tolerance)\n- Doors 50\" and over: not covered for warping\n- LUXE/ZENIT: protective film must be removed after installation (within 6 months)\n- Claims: file through professional portal at webserv.lioher.com \u2014 response within 48 business hours\n";
const SYSTEM_PROMPT = "You are Lio, a warm and knowledgeable sales assistant for Lioher \u2014 a premium modern cabinetry company in the USA selling wholesale factory-direct to trade professionals and homeowners.\n\n" + KNOWLEDGE_BASE + "\n\n=== YOUR PERSONALITY ===\n- Warm, confident, and consultative \u2014 like a knowledgeable showroom expert, not a robot\n- You guide people who don't know what they want \u2014 ask smart questions to help them discover\n- You're direct and helpful \u2014 never vague or overly salesy\n- You speak in plain English \u2014 no jargon unless they use it first\n- STRICT: Maximum 2-3 SHORT sentences per reply. Never list specs unless asked.\n- Never say \"Great question!\" or \"Certainly!\" \u2014 just answer naturally\n- Never dump product details unprompted \u2014 ask ONE question to guide them instead\n- If they ask about a product, give ONE sentence summary then ask what matters most to them\n\n=== CUSTOMER TYPES \u2014 DETECT AND ADAPT ===\nWhen someone starts chatting, quickly figure out who they are and adjust:\n\nHOMEOWNER: Mentions \"my kitchen\", \"my house\", \"renovation\", \"remodel\", \"home project\"\n\u2192 Focus on: inspiration, design help, showroom visit, working with a pro installer\n\u2192 Tone: warmer, more visual, budget-aware\n\u2192 Next step: Book a Discovery & Selection or Online Design appointment\n\nTRADE PROFESSIONAL: Mentions \"client\", \"project\", \"contractor\", \"designer\", \"installer\", \"cabinet shop\", \"my customer\"\n\u2192 Focus on: pricing tiers, loyalty program discounts, lead times, bulk orders\n\u2192 Tone: more technical, business-to-business\n\u2192 Next step: Quick Call with Inside Sales or Open an Account\n\nJUST BROWSING / UNSURE: Vague messages like \"just looking\", \"checking it out\", no clear intent\n\u2192 Start with a discovery question: \"What type of project are you working on?\" or \"Are you a homeowner or do you work in the trade?\"\n\u2192 Don't push the lead form \u2014 warm them up first\n\n=== DISCOVERY FLOW (for confused or vague customers) ===\nIf someone doesn't know what they want, guide them step by step:\n\nStep 1 \u2014 Project type:\n\"What type of project are you working on? Kitchen, closet, bathroom, or something else?\"\n\nStep 2 \u2014 Role:\n\"Are you a homeowner doing a renovation, or do you work in the trade (contractor, designer, installer)?\"\n\nStep 3 \u2014 Style direction:\n\"Are you drawn more to a high-gloss look, a super matte finish, or a natural wood texture?\"\n\u2192 High-gloss \u2192 LUXE collection\n\u2192 Super matte \u2192 ZENIT collection\n\u2192 Wood texture \u2192 SYNCRON collection\n\nStep 4 \u2014 Next step based on what you learned:\n\u2192 Suggest the right collection + booking type\n\n=== COMMON QUESTIONS \u2014 HANDLE CONFIDENTLY ===\n\n\"What's the difference between LUXE, ZENIT, and SYNCRON?\"\n\u2192 LUXE = high gloss, very reflective, modern and sleek (90 GU gloss level)\n\u2192 ZENIT = super matte, zero fingerprints, soft luxurious look (4 GU gloss level)\n\u2192 SYNCRON = textured panels that look and feel like real wood grain or stone\n\u2192 All three are MDF-based, European-made, antibacterial, and CARB2 certified\n\n\"What's the difference between matte and gloss?\"\n\u2192 Gloss (LUXE) reflects light and makes spaces feel larger and more modern\n\u2192 Matte (ZENIT) absorbs light, hides fingerprints, feels more sophisticated and premium\n\u2192 Personal preference \u2014 both are equally durable\n\n\"Are you good quality? How do you compare to other brands?\"\n\u2192 Lioher is European-manufactured, sold factory-direct so trade pros get better pricing\n\u2192 All panels are antibacterial (100%), CARB2/EPA certified, and come with a limited lifetime warranty\n\u2192 Unlike most brands, Lioher sells direct \u2014 no middleman markup\n\n\"Do you work with homeowners?\"\n\u2192 Yes! Homeowners are absolutely welcome \u2014 we love working with homeowners\n\u2192 For purchasing and installation, homeowners work through one of our certified trade professionals\n\u2192 We have a directory of professionals who use Lioher products at lioher.com/find-a-professional/\n\u2192 We also offer design consultations and showroom visits directly\n\n\"What's your lead time?\"\n\u2192 Panels: typically 1-2 weeks\n\u2192 Slab doors: 2-3 weeks\n\u2192 Finger Pull / Cava / Mallorca doors: 3-4 weeks\n\u2192 Assembled cabinets: 4-6 weeks\n\u2192 Orders over 150 pieces: 6 weeks\n\n\"Do you have financing?\"\n\u2192 Yes! Financing is available. Visit lioher.com/financing or ask our sales team for details\n\n\"Can I see samples before ordering?\"\n\u2192 Absolutely \u2014 samples are available at shop.lioher.com\n\u2192 3x5\" samples: $2 each | Door samples with Cava/Mallorca: $20 | Full sample books from $60\n\n\"What colors are trending?\"\n\u2192 Right now we're seeing huge demand for Cashmere SM and Blanco SM (ZENIT) for matte kitchens\n\u2192 For gloss, Blanco HG and Black HG remain the top sellers\n\u2192 Wood textures like Picasso 2 and Muratti 4 (SYNCRON) are very popular for modern organic designs\n\n\"I'm just remodeling my kitchen, where do I start?\"\n\u2192 Great starting point \u2014 first decide on your finish direction: gloss, matte, or wood texture\n\u2192 Then we can match you with the right collection and connect you with a designer or our sales team\n\u2192 Want me to help you figure out which direction fits your style?\n\n\"How much does a kitchen cost?\"\n\u2192 Assembled cabinets start from $425/linear foot for SYNCRON Slab and $499/linear foot for LUXE/ZENIT Slab\n\u2192 A typical 10-linear-foot basic kitchen starts around $4,250-$5,000 before countertops and installation\n\u2192 For a precise quote, our team does free kitchen design consultations \u2014 want me to set one up?\n\n=== HOMEOWNER HANDLING ===\nWhen someone says they are a homeowner:\n\u2192 Be warm and welcoming \u2014 homeowners are very important to us\n\u2192 Explain we sell through trade professionals\n\u2192 ALWAYS share the Find a Professional page: lioher.com/find-a-professional/\n\u2192 Offer a design consultation appointment\n\u2192 Offer to connect them with our team via a Quick Call\n\u2192 NEVER say \"we only work with trade professionals\" \u2014 instead say \"we work with homeowners through our network of certified professionals\"\n\nExample response when asked about homeowners (VERY SHORT \u2014 1-2 sentences max, NO links in text \u2014 links appear as buttons automatically):\n\"Absolutely! We work with homeowners through our network of certified professionals, and we also offer free design consultations. What type of project are you planning?\"\n\n=== RULES ===\n- Never make up prices \u2014 only use prices from the knowledge base\n- Never give discounts or loyalty tier percentages \u2014 always say \"your discount depends on your purchase volume, speak with our team\"\n- If someone asks about a product not in the knowledge base \u2014 say \"I don't have that info but our team can help\" and offer a Quick Call\n- STRICT SCOPE: You only answer questions related to Lioher products, design, interiors, and cabinetry. For completely unrelated topics, politely redirect.\n- Never use bullet points in responses \u2014 write in short natural sentences\n- Never ask more than one question at a time\n- If someone mentions a specific Lioher product by name, acknowledge it directly\n- Always end responses with a clear next step or offer\n- If the conversation has been going for 4+ exchanges and no booking/lead yet, gently offer to connect them with the team\n\n=== PRICING RULES ===\n- Panels, doors, countertops, edgebanding, shelves: Give list price directly\n- Closets: \"Use our 3D configurator at lioher.com/closets-3 to design and get a quote\"\n- Outdoor kitchens: \"Book a Quick Call for a custom quote \u2014 pricing depends on configuration\"\n- Always mention: \"As a trade professional, you'll receive a discount through our Loyalty Program\"\n\n=== LOCATIONS & CENTERS ===\nWhen someone asks about a location, nearest center, showroom, or wants to speak with a specific center \u2014 ask for their city first, then give them the closest one with full details.\n\nPROXIMITY GUIDE:\n- Miami, Coral Gables, Hialeah, Kendall, Brickell, Doral, Medley \u2192 Miami Lakes OR Doral (both close, mention both)\n- Fort Lauderdale, Hollywood, Weston, Davie, Miramar, Pembroke Pines, Hallandale, Aventura \u2192 Pompano Beach\n- Boca Raton, Delray Beach, Boynton Beach, Palm Beach \u2192 West Palm Beach\n- Tampa, St. Petersburg, Clearwater, Sarasota \u2192 Tampa\n- Orlando, Kissimmee, Sanford, Daytona \u2192 Orlando\n- Naples, Cape Coral, Bonita Springs, Fort Myers area \u2192 Fort Myers\n- Los Angeles, Burbank, Glendale, San Fernando Valley, Ventura \u2192 Van Nuys\n- Las Vegas, Henderson, Phoenix, Arizona \u2192 Las Vegas\n\nCENTER DETAILS:\nMiami Lakes: 13939 NW 60th Ave, Miami Lakes FL 33014 | \ud83d\udcde 305-685-0005 | insidesaleseast@lioher.com\nDoral: 1607 NW 82nd Ave, Doral FL 33126 | \ud83d\udcde (305) 932-4203 | insidesaleseast@lioher.com\nPompano Beach: 1718 West Atlantic Blvd, Pompano Beach FL 33069 | \ud83d\udcde 954-678-2203 | insidesaleseast@lioher.com\nWest Palm Beach: 7788 Central Industrial Dr Unit 7, West Palm Beach FL 33404 | \ud83d\udcde 561-484-7204 | insidesaleseast@lioher.com\nTampa: 5204 Tampa W Blvd, Tampa FL 33634 | \ud83d\udcde 813-433-0387 | insidesaleseast@lioher.com\nOrlando: 10511 Satellite Blvd, Orlando FL 32837 | \ud83d\udcde 689-244-6222 | insidesaleseast@lioher.com\nFort Myers: 12140 Metro Pkwy Suite K, Fort Myers FL 33966 | \ud83d\udcde 239-686-0914 | insidesaleseast@lioher.com\nVan Nuys CA: 16159 Stagg St, Van Nuys CA 91406 | \ud83d\udcde 323-925-4446 | vannuyscs@lioher.com\nLas Vegas NV: 4060 Frehner Rd Bldg 100, North Las Vegas NV 89030 | \ud83d\udcde 702-507-0503 | vegascs@lioher.com\n\nLOCATION RULES:\n- ALWAYS ask \"What city are you in?\" before recommending a center\n- Give: center name + full address + phone + email in one reply\n- If they say they want to SPEAK WITH or CALL a center, give the phone number directly\n- If they want to VISIT, also offer to book a Discovery & Selection appointment\n- If they're not in any of the listed areas, give them the nearest based on state, and offer a Quick Call or online design session as alternative\n\n=== BOOKING GUIDE ===\nMatch the customer to the right appointment:\n- Quick general question or pricing \u2192 Quick Call: https://usa-appointments.zohobookings.com/#/4336222000004023032\n- Homeowner wanting kitchen design \u2192 Online Design (Homeowner): https://usa-appointments.zohobookings.com/#/4336222000002845072\n- Trade pro wanting design help \u2192 Online Design (Trade Pro): https://usa-appointments.zohobookings.com/#/4336222000002828054\n- Wants to visit in person \u2192 Discovery & Selection: https://lioher.com/discovery-selection-locations/\n- Wants to open a trade account \u2192 https://lioher.com/open-new-trade-account/\n";
const PRODUCT_DATA = {
'Panels & Surfaces': {
intro: 'Which surface collection interests you?',
items: ['LUXE — High Gloss', 'ZENIT — Super Matt', 'SYNCRON — Texture']
},
'Cabinetry': {
intro: 'Which cabinetry line are you interested in?',
items: ['Kitchens & Vanity', 'Outdoor Kitchens', 'Closets', 'Cabinet Doors']
},
'Other Products': {
intro: 'What are you looking for?',
items: ['Countertops', 'Slat Wall', 'Floating Shelves', 'Handles']
}
};
// ── CONFIG ─────────────────────────────────────────────────────────────────
const CONFIG = {
aiProxy: 'https://lioher.app.n8n.cloud/webhook/lioher-ai-proxy',
webhooks: {
lead: 'https://lioher.app.n8n.cloud/webhook/lioher-lead',
allChats: 'https://lioher.app.n8n.cloud/webhook/lioher-all-chats',
followup: 'https://lioher.app.n8n.cloud/webhook/lioher-followup'
},
appointments: {
quickCall: 'https://usa-appointments.zohobookings.com/#/4336222000004023032',
homeowner: 'https://usa-appointments.zohobookings.com/#/4336222000002845072',
tradePro: 'https://usa-appointments.zohobookings.com/#/4336222000002828054',
discoverySelection: 'https://lioher.com/discovery-selection-locations/'
},
urls: {
locations: 'https://lioher.com/location/',
shop: 'https://shop.lioher.com',
findPro: 'https://lioher.com/find-a-professional/',
closets: 'https://lioher.com/closets-3/',
closetConfigurator: 'https://17squares.com/Lioher/?designer=1',
openAccount: 'https://webserv.lioher.com/privatearea/registration.php'
},
bookingProxy: 'https://lioher.app.n8n.cloud/webhook/lioher-slots',
bookingCreate: 'https://lioher.app.n8n.cloud/webhook/lioher-book',
serviceIds: {
quickCall: '4336222000004023032',
homeowner: '4336222000002845072',
tradePro: '4336222000002828054'
}
};
// ── STATE ──────────────────────────────────────────────────────────────────
let history = [];
let isOpen = false;
let isTyping = false;
let sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2,9);
let chatLogged = false;
let greeted = false;
let leadCollected = false;
let leadInfo = {};
let followUpEmailOffered = false;
let conversationSummary = [];
let selectedAppointmentType = '';
let selectedAppointmentUrl = '';
let sessionDirty = false;
let chatMessagesEl = null;
let lastWidgetType = 'none';
let buyingSignalScore = 0;
let leadFormShownBySignal = false;
let detectedLanguage = 'en';
// Auto-detect East/West coast from browser timezone
function getRegion() {
try {
var tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '';
var westKeywords = ['Los_Angeles', 'Phoenix', 'Denver', 'Boise', 'Anchorage', 'Juneau', 'Sitka', 'Yakutat', 'Nome', 'Adak', 'Honolulu', 'Seattle', 'Portland', 'Las_Vegas'];
return westKeywords.some(function(k) { return tz.includes(k); }) ? 'west' : 'east';
} catch(e) { return 'east'; }
}
function getChatMessages() {
if (!chatMessagesEl) chatMessagesEl = document.getElementById('chat-messages');
return chatMessagesEl;
}
function saveSession() {
if (!sessionDirty) return;
try {
sessionStorage.setItem('lio_history', JSON.stringify(history));
sessionStorage.setItem('lio_isOpen', isOpen);
sessionStorage.setItem('lio_greeted', greeted);
sessionStorage.setItem('lio_leadCollected', leadCollected);
sessionStorage.setItem('lio_leadInfo', JSON.stringify(leadInfo));
sessionStorage.setItem('lio_followUpOffered', followUpEmailOffered);
sessionStorage.setItem('lio_apptType', selectedAppointmentType);
sessionStorage.setItem('lio_apptUrl', selectedAppointmentUrl);
sessionStorage.setItem('lio_lastWidget', lastWidgetType);
sessionStorage.setItem('lio_language', detectedLanguage);
sessionDirty = false;
} catch(e) {}
}
function restoreSession() {
try {
const h = sessionStorage.getItem('lio_history');
if (!h) return false;
history = JSON.parse(h);
greeted = sessionStorage.getItem('lio_greeted') === 'true';
leadCollected = sessionStorage.getItem('lio_leadCollected') === 'true';
leadInfo = JSON.parse(sessionStorage.getItem('lio_leadInfo') || '{}');
followUpEmailOffered = sessionStorage.getItem('lio_followUpOffered') === 'true';
selectedAppointmentType = sessionStorage.getItem('lio_apptType') || '';
selectedAppointmentUrl = sessionStorage.getItem('lio_apptUrl') || '';
lastWidgetType = sessionStorage.getItem('lio_lastWidget') || 'none';
detectedLanguage = sessionStorage.getItem('lio_language') || 'en';
return history.length > 0;
} catch(e) { return false; }
}
function replayHistory() {
const container = getChatMessages();
container.innerHTML = '';
var lastAgentDiv = null;
history.forEach(msg => {
const div = document.createElement('div');
div.className = 'msg ' + (msg.role === 'user' ? 'user' : 'agent');
const bubble = document.createElement('div');
bubble.className = 'msg-bubble';
bubble.textContent = msg.content;
div.appendChild(bubble);
container.appendChild(div);
if (msg.role !== 'user') lastAgentDiv = div;
});
// Restore last widget if it was the greeting menu
if (lastWidgetType === 'greeting' && lastAgentDiv) {
showMainMenu(lastAgentDiv, ['Products & Collections', 'Loyalty Program', 'Buy Samples', 'Book an appointment', 'Find a Professional', 'Showroom locations', 'Talk to a Real Person']);
}
scrollBottom();
}
window.addEventListener('beforeunload', saveSession);
setInterval(saveSession, 5000);
function toggleChat() {
isOpen = !isOpen;
document.getElementById('lioher-chat').classList.toggle('open', isOpen);
document.getElementById('lioher-launcher').classList.toggle('open', isOpen);
if (isOpen) {
chatLogged = false;
sessionStorage.setItem('lioher_chat_interacted', 'true');
var proactive = document.getElementById('lioher-proactive');
if (proactive) proactive.remove();
}
if (isOpen && !greeted) {
if (restoreSession()) {
greeted = true;
replayHistory();
addMessage('agent', 'Welcome back! Continuing where we left off.');
} else {
greet();
greeted = true;
}
}
if (isOpen) setTimeout(() => document.getElementById('lioher-input').focus(), 350);
if (!isOpen && history.length > 1 && !chatLogged) {
logAllConversation();
chatLogged = true;
}
}
function getTime() {
return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function scrollBottom() {
const c = getChatMessages();
c.scrollTop = c.scrollHeight;
}
function addMessage(role, text) {
const container = getChatMessages();
const div = document.createElement('div');
div.className = 'msg ' + role;
const bubble = document.createElement('div');
bubble.className = 'msg-bubble';
bubble.textContent = text;
div.appendChild(bubble);
const time = document.createElement('div');
time.className = 'msg-time';
time.textContent = getTime();
div.appendChild(time);
container.appendChild(div);
scrollBottom();
sessionDirty = true;
return div;
}
function appendWidget(parentDiv, widget) {
parentDiv.insertBefore(widget, parentDiv.querySelector('.msg-time'));
scrollBottom();
}
function showTyping() {
const container = getChatMessages();
const div = document.createElement('div');
div.className = 'msg agent';
div.id = 'typing-indicator';
div.innerHTML = '
';
container.appendChild(div);
scrollBottom();
}
function removeTyping() {
const el = document.getElementById('typing-indicator');
if (el) el.remove();
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 100) + 'px';
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
}
// ── LEAD FORM FACTORY ──────────────────────────────────────────────────
function createLeadForm(prefix, fields, onSubmitFn) {
const form = document.createElement('div');
form.className = 'lead-form';
let html = '
Your details
';
fields.forEach(f => {
html += '';
});
form.innerHTML = html;
const submitBtn = document.createElement('button');
submitBtn.className = 'lead-form-submit';
submitBtn.textContent = onSubmitFn.buttonText || 'Submit';
submitBtn.onclick = () => onSubmitFn(form, prefix);
form.appendChild(submitBtn);
return form;
}
// ── SHOWROOM FLOW ──────────────────────────────────────────────────────────
function showShowroomFlow() {
const replyDiv = addMessage('agent', "I'll grab your details so our team can follow up, and then you can explore our locations:");
const fields = [
{ id: 'first', placeholder: 'First Name' },
{ id: 'last', placeholder: 'Last Name' },
{ id: 'company', placeholder: 'Company Name' },
{ id: 'phone', placeholder: 'Phone Number', type: 'tel' },
{ id: 'email', placeholder: 'Email Address', type: 'email' },
{ id: 'zip', placeholder: 'Zip Code' }
];
const onSubmit = function(form, prefix) {
const first = document.getElementById(prefix + '-first').value.trim();
const last = document.getElementById(prefix + '-last').value.trim();
const company = document.getElementById(prefix + '-company').value.trim();
const phone = document.getElementById(prefix + '-phone').value.trim();
const email = document.getElementById(prefix + '-email').value.trim();
const zip = document.getElementById(prefix + '-zip').value.trim();
[prefix+'-first',prefix+'-last',prefix+'-phone',prefix+'-email'].forEach(id => document.getElementById(id).classList.remove('error'));
let valid = true;
if (!first) { document.getElementById(prefix+'-first').classList.add('error'); valid = false; }
if (!last) { document.getElementById(prefix+'-last').classList.add('error'); valid = false; }
if (!phone) { document.getElementById(prefix+'-phone').classList.add('error'); valid = false; }
if (!email) { document.getElementById(prefix+'-email').classList.add('error'); valid = false; }
if (!valid) return;
form.remove();
leadCollected = true;
leadInfo = { first, last, company, phone, email };
sendLeadEmail(first, last, company, phone, email, 'Showroom Visit', zip, false);
const confirmDiv = addMessage('agent', 'Thanks, ' + first + '! Our team will follow up with you. Here are our locations:');
const card = createMenuCard([{ label: 'View All Locations', url: CONFIG.urls.locations }]);
appendWidget(confirmDiv, card);
history.push({ role: 'user', content: 'My details: ' + first + ' ' + last + ', company: ' + (company||'N/A') + ', phone: ' + phone + ', email: ' + email });
};
onSubmit.buttonText = 'Submit';
const form = createLeadForm('sf', fields, onSubmit);
// Add locations button
const locBtn = document.createElement('a');
locBtn.href = CONFIG.urls.locations;
locBtn.target = '_blank';
locBtn.className = 'lead-form-submit';
locBtn.style.cssText = 'display:block;text-align:center;text-decoration:none;margin-top:8px;background:#f0f4fb;color:#5b7ebf;border:1px solid #5b7ebf;font-weight:600;padding:9px;border-radius:8px;font-family:Montserrat,sans-serif;font-size:12.5px;letter-spacing:0.04em;transition:background 0.2s;';
locBtn.onmouseenter = () => { locBtn.style.background = '#5b7ebf'; locBtn.style.color = '#fff'; };
locBtn.onmouseleave = () => { locBtn.style.background = '#f0f4fb'; locBtn.style.color = '#5b7ebf'; };
locBtn.textContent = 'View All Locations';
form.appendChild(locBtn);
appendWidget(replyDiv, form);
}
// ── FOLLOW-UP EMAIL FORM ────────────────────────────────────────────────
function showFollowUpForm(parentDiv) {
const fields = [
{ id: 'first', placeholder: 'First Name' },
{ id: 'last', placeholder: 'Last Name' },
{ id: 'email', placeholder: 'Email Address', type: 'email' }
];
const onSubmit = function(form, prefix) {
const first = document.getElementById(prefix + '-first').value.trim();
const last = document.getElementById(prefix + '-last').value.trim();
const email = document.getElementById(prefix + '-email').value.trim();
if (!first || !email) {
if (!first) document.getElementById(prefix + '-first').style.borderColor = '#e88';
if (!email) document.getElementById(prefix + '-email').style.borderColor = '#e88';
return;
}
form.remove();
sendFollowUpEmail(first, last, email, false);
addMessage('agent', 'A summary will be sent to ' + email + ' at the end of the day \u2014 thanks ' + first + '!');
};
onSubmit.buttonText = 'Send Me the Summary';
const form = createLeadForm('fu', fields, onSubmit);
appendWidget(parentDiv, form);
}
function buildTranscript() {
return history.map(m => {
const role = m.role === 'user' ? 'Customer' : 'Lio';
return role + ': ' + m.content;
}).join('\n');
}
function sendLeadEmail(first, last, company, phone, email, appointmentType, zip, isCallback) {
leadCollected = true;
leadInfo = { first, last, company, phone, email, zip };
const transcript = buildTranscript();
try {
fetch(CONFIG.webhooks.lead, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first, last, company, phone, email, zip,
appointmentType,
isCallback,
sendSMS: true,
region: getRegion(),
transcript: transcript,
timestamp: new Date().toISOString()
})
}).then(function(r) { if (!r.ok) console.error('Webhook failed:', r.status); }).catch(function(e) { console.error('Webhook error:', e); });
} catch(e) { console.log('Lead notification failed:', e); }
}
function logAllConversation() {
if (history.length <= 1) return; // skip if only greeting
const transcript = buildTranscript ? buildTranscript() : history.map(m => (m.role === 'user' ? 'Customer' : 'Lio') + ': ' + m.content).join('\n');
try {
fetch(CONFIG.webhooks.allChats, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: sessionId,
transcript: transcript,
leadCaptured: leadCollected ? 'Yes' : 'No',
name: leadCollected ? (leadInfo.first + ' ' + leadInfo.last) : '',
phone: leadCollected ? leadInfo.phone : '',
email: leadCollected ? leadInfo.email : '',
appointmentType: leadCollected ? (selectedAppointmentType || 'Callback Request') : '',
region: getRegion(),
timestamp: new Date().toISOString()
})
}).then(function(r) { if (!r.ok) console.error('Webhook failed:', r.status); }).catch(function(e) { console.error('Webhook error:', e); });
} catch(e) { console.log('All chats log failed:', e); }
}
function sendFollowUpEmail(first, last, email, hadCallback) {
const summary = history.filter(m => m.role === 'user').map(m => m.content).join(' | ');
try {
fetch(CONFIG.webhooks.followup, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first, last, email,
hadCallback,
callbackPhone: hadCallback ? leadInfo.phone : '',
summary: summary,
region: getRegion(),
timestamp: new Date().toISOString()
})
}).then(function(r) { if (!r.ok) console.error('Webhook failed:', r.status); }).catch(function(e) { console.error('Webhook error:', e); });
} catch(e) { console.log('Follow-up email failed:', e); }
}
// ── APPOINTMENT MENU ───────────────────────────────────────────────────────
function showAppointmentMenu() {
const replyDiv = addMessage('agent', 'What type of appointment are you looking for?');
const card = document.createElement('div');
card.className = 'menu-card';
const options = [
{ label: 'Quick Call', url: CONFIG.appointments.quickCall, direct: true },
{ label: 'Online Kitchen Design', url: null, direct: false },
{ label: 'In-Person Showroom \u2014 Discovery & Selection', url: CONFIG.appointments.discoverySelection, direct: true },
];
options.forEach(opt => {
const btn = document.createElement('button');
btn.className = 'menu-btn';
btn.textContent = opt.label;
btn.onclick = () => {
card.remove();
addMessage('user', opt.label);
if (opt.label === 'Quick Call') {
showInlineBooking('quickCall');
} else if (opt.label.includes('Showroom')) {
showInlineBooking('tradePro');
} else if (!opt.direct) {
showKitchenDesignMenu();
} else {
showInlineBooking('quickCall');
}
};
card.appendChild(btn);
});
appendWidget(replyDiv, card);
}
// ── KITCHEN DESIGN SUB-MENU ────────────────────────────────────────────────
function showKitchenDesignMenu() {
const replyDiv = addMessage('agent', 'Are you a homeowner or a trade professional?');
const card = document.createElement('div');
card.className = 'menu-card';
const opts = [
{ label: 'Homeowner', serviceType: 'homeowner' },
{ label: 'Trade Professional', serviceType: 'tradePro' },
];
opts.forEach(opt => {
const btn = document.createElement('button');
btn.className = 'menu-btn';
btn.textContent = opt.label;
btn.onclick = () => {
card.remove();
addMessage('user', opt.label);
showInlineBooking(opt.serviceType);
};
card.appendChild(btn);
});
appendWidget(replyDiv, card);
}
// ── SHOW BOOKING CARD (now uses inline booking) ────────────────────────────
function showBookingCard(type, url) {
selectedAppointmentType = type;
selectedAppointmentUrl = url;
var urlToService = {};
urlToService[CONFIG.appointments.quickCall] = 'quickCall';
urlToService[CONFIG.appointments.homeowner] = 'homeowner';
urlToService[CONFIG.appointments.tradePro] = 'tradePro';
var serviceType = urlToService[url] || 'quickCall';
showInlineBooking(serviceType);
}
// ── INLINE BOOKING ────────────────────────────────────────────────────────
function showInlineBooking(serviceType) {
var serviceMap = {
'quickCall': CONFIG.serviceIds.quickCall,
'homeowner': CONFIG.serviceIds.homeowner,
'tradePro': CONFIG.serviceIds.tradePro,
'Quick Call': CONFIG.serviceIds.quickCall,
'Book an Appointment': CONFIG.serviceIds.quickCall,
'Homeowner Consultation': CONFIG.serviceIds.homeowner,
'Trade Pro Consultation': CONFIG.serviceIds.tradePro
};
var serviceId = serviceMap[serviceType] || CONFIG.serviceIds.quickCall;
var serviceName = serviceType || 'Appointment';
var msgDiv = addMessage('agent', 'Let me find available times for you.');
var card = document.createElement('div');
card.className = 'booking-card';
// Generate next 5 business days
var dates = [];
var now = new Date();
var d = new Date(now);
d.setDate(d.getDate() + 1);
while (dates.length < 5) {
var day = d.getDay();
if (day !== 0 && day !== 6) {
dates.push(new Date(d));
}
d.setDate(d.getDate() + 1);
}
var dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
var monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function formatDateForApi(dt) {
return dt.getDate().toString().padStart(2,'0') + '-' + monthNames[dt.getMonth()] + '-' + dt.getFullYear();
}
function formatDateDisplay(dt) {
return monthNames[dt.getMonth()] + ' ' + dt.getDate();
}
function formatTime12h(timeStr) {
var parts = timeStr.split(':');
var h = parseInt(parts[0]);
var m = parts[1];
var ampm = h >= 12 ? 'PM' : 'AM';
if (h > 12) h -= 12;
if (h === 0) h = 12;
return h + ':' + m + ' ' + ampm;
}
card.innerHTML = '
';
timesContainer.querySelectorAll('.booking-time-btn').forEach(function(timeBtn) {
timeBtn.addEventListener('click', function() {
timesContainer.querySelectorAll('.booking-time-btn').forEach(function(b) { b.classList.remove('selected'); });
this.classList.add('selected');
var selectedTime = this.dataset.time;
var fullDateTime = selectedDate + ' ' + selectedTime + ':00';
card.remove();
var matchDate = dates.find(function(dt) { return formatDateForApi(dt) === selectedDate; });
addMessage('user', formatDateDisplay(matchDate || new Date()) + ' at ' + formatTime12h(selectedTime));
if (leadCollected && leadInfo.name !== undefined || (leadCollected && leadInfo.first && leadInfo.phone && leadInfo.email)) {
var bookName = (leadInfo.first || '') + ' ' + (leadInfo.last || '');
confirmAndBook(serviceId, serviceName, fullDateTime, selectedDate, selectedTime, bookName.trim(), leadInfo.email, leadInfo.phone);
} else {
var formDiv = addMessage('agent', 'Great choice! Just need your details to confirm the booking.');
var bkFields = [
{ id: 'first', placeholder: 'First Name' },
{ id: 'last', placeholder: 'Last Name' },
{ id: 'phone', placeholder: 'Phone Number', type: 'tel' },
{ id: 'email', placeholder: 'Email Address', type: 'email' }
];
var bkSubmit = function(form, prefix) {
var first = document.getElementById(prefix + '-first').value.trim();
var last = document.getElementById(prefix + '-last').value.trim();
var phone = document.getElementById(prefix + '-phone').value.trim();
var email = document.getElementById(prefix + '-email').value.trim();
[prefix+'-first',prefix+'-phone',prefix+'-email'].forEach(function(id) { document.getElementById(id).classList.remove('error'); });
var valid = true;
if (!first) { document.getElementById(prefix+'-first').classList.add('error'); valid = false; }
if (!phone) { document.getElementById(prefix+'-phone').classList.add('error'); valid = false; }
if (!email) { document.getElementById(prefix+'-email').classList.add('error'); valid = false; }
if (!valid) return;
form.remove();
leadCollected = true;
leadInfo = { first: first, last: last || '', company: '', phone: phone, email: email };
confirmAndBook(serviceId, serviceName, fullDateTime, selectedDate, selectedTime, first + ' ' + (last || ''), email, phone);
};
bkSubmit.buttonText = 'Confirm Booking';
var bkForm = createLeadForm('bk', bkFields, bkSubmit);
appendWidget(formDiv, bkForm);
}
});
});
})
.catch(function(err) {
timesContainer.innerHTML = '
Unable to load times. Please try again.
';
console.error('Booking slots error:', err);
});
});
});
}
function confirmAndBook(serviceId, serviceName, fullDateTime, selectedDate, selectedTime, name, email, phone) {
var bookingDiv = addMessage('agent', 'Booking your appointment...');
fetch(CONFIG.bookingCreate, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
service_id: serviceId,
from_time: fullDateTime,
name: name,
email: email,
phone: phone,
notes: 'Booked via Lioher CS chatbot — ' + serviceName
})
})
.then(function(res) { return res.json(); })
.then(function(data) {
if (data.success) {
var monthNames2 = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function fmtTime(ts) {
var pp = ts.split(':');
var hh = parseInt(pp[0]);
var mm = pp[1];
var ap = hh >= 12 ? 'PM' : 'AM';
if (hh > 12) hh -= 12;
if (hh === 0) hh = 12;
return hh + ':' + mm + ' ' + ap;
}
var confirmCard = document.createElement('div');
confirmCard.className = 'booking-confirm';
confirmCard.innerHTML = '
\u2705 Appointment Confirmed!
' +
'
Service: ' + serviceName + '
' +
'
Date: ' + selectedDate + '
' +
'
Time: ' + fmtTime(selectedTime) + ' ET
' +
'
Name: ' + name + '
' +
'
Email: ' + email + '
' +
'
A confirmation will be sent to your email.
';
appendWidget(bookingDiv, confirmCard);
sendLeadEmail(leadInfo.first || name.split(' ')[0], leadInfo.last || name.split(' ').slice(1).join(' '), leadInfo.company || '', phone, email, serviceName, leadInfo.zip || '', false);
} else {
addMessage('agent', "I wasn't able to complete the booking. The slot may have been taken. Would you like to try another time?");
}
})
.catch(function(err) {
addMessage('agent', "Something went wrong with the booking. You can call us at 305-685-0005 or I can have someone call you back.");
console.error('Booking error:', err);
});
}
// ── MENU CARD HELPER ────────────────────────────────────────────────────
function createMenuCard(options) {
const card = document.createElement('div');
card.className = 'menu-card';
options.forEach(opt => {
if (opt.url) {
const link = document.createElement('a');
link.className = 'menu-btn samples';
link.textContent = opt.label;
link.href = opt.url;
link.target = '_blank';
card.appendChild(link);
} else if (opt.onclick) {
const btn = document.createElement('button');
btn.className = 'menu-btn';
btn.textContent = opt.label;
btn.onclick = opt.onclick;
card.appendChild(btn);
}
});
return card;
}
// ── MAIN MENU (shared) ───────────────────────────────────────────────────
function showMainMenu(parentDiv, options) {
var menuOptions = options || ['Products & Collections', 'Loyalty Program', 'Buy Samples', 'Book an appointment', 'Find a Professional', 'Showroom locations'];
const qr = document.createElement('div');
qr.className = 'quick-replies';
menuOptions.forEach(label => {
const chip = document.createElement('button');
chip.className = 'qr-chip';
chip.textContent = label;
chip.onclick = () => {
qr.remove();
handleMenuChoice(label);
};
qr.appendChild(chip);
});
appendWidget(parentDiv, qr);
}
function handleMenuChoice(label) {
if (label === 'Products & Collections') {
addMessage('user', label);
showCategoryMenu();
} else if (label === 'Buy Samples') {
addMessage('user', label);
const replyDiv = addMessage('agent', 'You can order samples directly from our shop at shop.lioher.com \u2014 here is the link:');
const samplesCard = createMenuCard([{ label: 'Visit shop.lioher.com', url: CONFIG.urls.shop }]);
appendWidget(replyDiv, samplesCard);
} else if (label === 'Loyalty Program') {
addMessage('user', label);
const lpDiv = addMessage('agent', "Our Loyalty Program rewards trade professionals with 5 tiers \u2014 Silver, Gold, Platinum, Partner, and Partner Pro \u2014 offering free samples, sample books, tower displays, social media features, and discounts on delivery and design fees based on your annual purchase volume.");
const qr2 = document.createElement('div');
qr2.className = 'quick-replies';
const chip = document.createElement('button');
chip.className = 'qr-chip';
chip.textContent = 'Learn more \u2014 Quick Call';
chip.onclick = () => {
qr2.remove();
addMessage('user', 'Learn more \u2014 Quick Call');
showAppointmentMenu();
};
qr2.appendChild(chip);
appendWidget(lpDiv, qr2);
} else if (label === 'Book an appointment') {
addMessage('user', label);
showAppointmentMenu();
} else if (label === 'Find a Professional') {
addMessage('user', label);
const replyDiv = addMessage('agent', 'You can find a certified Lioher professional near you on our directory:');
const profCard = createMenuCard([{ label: 'Find a Professional Near Me', url: CONFIG.urls.findPro }]);
appendWidget(replyDiv, profCard);
} else if (label === 'Showroom locations') {
addMessage('user', label);
showShowroomFlow();
} else if (label === 'Open an Account') {
addMessage('user', label);
const rd = addMessage('agent', 'Open a new trade account here:');
const ac = createMenuCard([{ label: 'Open a Trade Account', url: CONFIG.urls.openAccount }]);
appendWidget(rd, ac);
} else if (label === 'Talk to a Real Person' || label === 'Hablar con una Persona') {
addMessage('user', label);
if (leadCollected) {
handoffToHuman('Customer requested human agent');
addMessage('agent', "I've notified our inside sales team — they have your full conversation and will reach out to " + leadInfo.phone + " shortly. You can keep chatting with me in the meantime!");
} else {
var handoffDiv = addMessage('agent', "I'll connect you with our team! Just need a couple details so they can reach you.");
var hoForm = createLeadForm('ho', [
{ id: 'first', placeholder: 'First Name' },
{ id: 'last', placeholder: 'Last Name' },
{ id: 'phone', placeholder: 'Phone Number', type: 'tel' },
{ id: 'email', placeholder: 'Email Address', type: 'email' }
], function(form, prefix) {
var first = document.getElementById(prefix + '-first').value.trim();
var last = document.getElementById(prefix + '-last').value.trim();
var phone = document.getElementById(prefix + '-phone').value.trim();
var email = document.getElementById(prefix + '-email').value.trim();
[prefix+'-first',prefix+'-phone',prefix+'-email'].forEach(function(id){ document.getElementById(id).classList.remove('error'); });
var valid = true;
if (!first) { document.getElementById(prefix+'-first').classList.add('error'); valid = false; }
if (!phone) { document.getElementById(prefix+'-phone').classList.add('error'); valid = false; }
if (!email) { document.getElementById(prefix+'-email').classList.add('error'); valid = false; }
if (!valid) return;
form.remove();
leadCollected = true;
leadInfo = { first: first, last: last || '', company: '', phone: phone, email: email };
handoffToHuman('Customer requested human agent');
addMessage('agent', "Our inside sales team has been notified with your full conversation. They'll call " + phone + " shortly!");
});
hoForm.buttonText = 'Connect Me';
appendWidget(handoffDiv, hoForm);
}
} else {
sendUserMessage(label);
}
}
// ── GREET ──────────────────────────────────────────────────────────────────
function greet() {
const greetDiv = addMessage('agent', "Hey there! I'm Lio, and I'm here to make your Lioher experience as smooth as possible. Whether you're exploring our collections, have a quick question, or want to connect with our team \u2014 I've got you. What brings you here today?");
showMainMenu(greetDiv, ['Products & Collections', 'Loyalty Program', 'Buy Samples', 'Book an appointment', 'Find a Professional', 'Showroom locations', 'Talk to a Real Person']);
lastWidgetType = 'greeting';
sessionDirty = true;
}
// ── CATEGORY MENU ──────────────────────────────────────────────────────────
function showCategoryMenu() {
const replyDiv = addMessage('agent', 'What category are you interested in?');
const card = document.createElement('div');
card.className = 'menu-card';
card.innerHTML = '
' + selectedAppointmentType + '';
div.appendChild(card);
}
const time = document.createElement('div');
time.className = 'msg-time';
time.textContent = getTime();
div.appendChild(time);
container.appendChild(div);
scrollBottom();
history.push({ role: 'user', content: 'My details: ' + first + ' ' + last + ', company: ' + (company||'N/A') + ', phone: ' + phone + ', email: ' + email });
history.push({ role: 'assistant', content: 'Thanks ' + first + ', here are your booking options.' });
// Send lead to n8n
sendLeadEmail(first, last, company, phone, email, selectedAppointmentType || 'Callback Request', '', !selectedAppointmentUrl);
leadCollected = true;
leadInfo = { first, last, company, phone, email };
};
onSubmit.buttonText = 'Continue to Booking';
const form = createLeadForm('lf', fields, onSubmit);
appendWidget(parentDiv, form);
}
// ── CONTEXTUAL QUICK REPLIES ────────────────────────────────────────────
function generateQuickReplies(responseText) {
var r = responseText.toLowerCase();
var quickRepliesES = {
kitchens: ['Ver Estilos de Cocina', 'Visitar Showroom', 'Solicitar Cotización', 'Hablar con una Persona'],
closets: ['Diseñar Mi Closet', 'Visitar Showroom', 'Ver Opciones de Closet', 'Hablar con una Persona'],
outdoor: ['Ver Cocinas Exteriores', 'Visitar Showroom', 'Hablar con una Persona'],
surfaces: ['Pedir Muestras', 'Ver Acabados', 'Visitar Showroom', 'Hablar con una Persona'],
professional: ['Abrir Cuenta Trade', 'Encontrar Profesional', 'Agendar Llamada', 'Hablar con una Persona'],
appointment: ['Showroom Miami', 'Ver Ubicaciones', 'Hablar con una Persona'],
fallback: ['Ver Productos', 'Visitar Showroom', 'Hablar con Ventas', 'Hablar con una Persona']
};
if (detectedLanguage === 'es') {
if (/kitchen|cabinet|vanity|cocina|gabinete/i.test(r)) return quickRepliesES.kitchens;
if (/closet/i.test(r)) return quickRepliesES.closets;
if (/outdoor|exterior/i.test(r)) return quickRepliesES.outdoor;
if (/panel|surface|luxe|zenit|syncron|acabado/i.test(r)) return quickRepliesES.surfaces;
if (/professional|trade|dealer|profesional/i.test(r)) return quickRepliesES.professional;
if (/appointment|showroom|visit|cita|visita/i.test(r)) return quickRepliesES.appointment;
return quickRepliesES.fallback;
}
if (/kitchen|cabinet|vanity/i.test(r)) return ['See Kitchen Styles', 'Book a Showroom Visit', 'Get a Quote', 'Talk to a Real Person'];
if (/closet/i.test(r)) return ['Design My Closet', 'Book a Showroom Visit', 'See Closet Options', 'Talk to a Real Person'];
if (/outdoor/i.test(r)) return ['See Outdoor Kitchens', 'Book a Showroom Visit', 'Talk to a Real Person'];
if (/panel|surface|luxe|zenit|syncron/i.test(r)) return ['Order Samples', 'See All Finishes', 'Book a Showroom Visit', 'Talk to a Real Person'];
if (/professional|trade|dealer/i.test(r)) return ['Open a Trade Account', 'Find a Professional', 'Book a Call', 'Talk to a Real Person'];
if (/appointment|showroom|visit/i.test(r)) return ['Miami Showroom', 'View All Locations', 'Talk to a Real Person'];
return ['Browse Products', 'Book a Showroom Visit', 'Talk to Sales', 'Talk to a Real Person'];
}
function showQuickReplies(replies) {
var container = document.createElement('div');
container.className = 'quick-replies';
container.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;padding:8px 16px;';
replies.forEach(function(text) {
var chip = document.createElement('button');
chip.className = 'quick-chip';
chip.textContent = text;
chip.style.cssText = 'background:#f5f3ef;border:1px solid #e0ddd8;color:#333;padding:6px 14px;border-radius:16px;font-size:11px;font-family:Montserrat,sans-serif;cursor:pointer;transition:all 0.15s;font-weight:500;';
chip.addEventListener('mouseover', function(){ this.style.background='#e8eef9';this.style.borderColor='#5b7ebf';this.style.color='#3b5fa0'; });
chip.addEventListener('mouseout', function(){ this.style.background='#f5f3ef';this.style.borderColor='#e0ddd8';this.style.color='#333'; });
chip.addEventListener('click', function() {
container.remove();
addMessage('user', text);
sendUserMessage(text);
});
container.appendChild(chip);
});
getChatMessages().appendChild(container);
scrollBottom();
}
// ── HANDOFF TO HUMAN ────────────────────────────────────────────────────
function handoffToHuman(reason) {
var transcript = buildTranscript();
var payload = {
type: 'human_handoff',
reason: reason || 'Customer requested human agent',
name: leadCollected ? (leadInfo.first + ' ' + leadInfo.last) : 'Unknown',
phone: leadCollected ? leadInfo.phone : '',
email: leadCollected ? leadInfo.email : '',
company: leadCollected ? (leadInfo.company || '') : '',
transcript: transcript,
page: window.location.href,
timestamp: new Date().toISOString(),
sessionId: sessionId
};
fetch(CONFIG.webhooks.lead, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first: payload.name.split(' ')[0] || 'Unknown',
last: payload.name.split(' ').slice(1).join(' ') || '',
company: payload.company,
phone: payload.phone,
email: payload.email,
zip: '',
appointmentType: 'LIVE HANDOFF — ' + (reason || 'Customer requested human'),
isCallback: true,
sendSMS: true,
region: getRegion(),
transcript: transcript,
timestamp: payload.timestamp
})
}).then(function(r) { if (!r.ok) console.error('Handoff webhook failed:', r.status); }).catch(function(e) { console.error('Handoff error:', e); });
}
// ── BUYING SIGNAL DETECTION ─────────────────────────────────────────────
function detectBuyingSignals(text) {
var t = text.toLowerCase();
var score = 0;
var strongSignals = ['budget', 'timeline', 'when can', 'how soon', 'ready to', 'move forward', 'next step', 'place an order', 'want to buy', 'ready to order', "let's do it", "i'm interested", 'im interested', 'send me a quote', 'get a quote'];
strongSignals.forEach(function(s) { if (t.includes(s)) score += 3; });
var mediumSignals = ['how much', 'price', 'cost', 'square foot', 'sqft', 'per foot', 'delivery', 'lead time', 'install', 'dimensions', 'measurements', 'my project', 'my kitchen', 'my home', 'renovation', 'remodel'];
mediumSignals.forEach(function(s) { if (t.includes(s)) score += 2; });
var softSignals = ['which color', 'what finish', 'compare', 'difference between', 'warranty', 'durability', 'sample', 'catalog', 'specific', 'custom', 'nearest showroom', 'your location'];
softSignals.forEach(function(s) { if (t.includes(s)) score += 1; });
return score;
}
// ── LANGUAGE DETECTION ──────────────────────────────────────────────────
function detectLanguage(text) {
var t = text.toLowerCase();
var spanishIndicators = ['hola', 'buenos', 'buenas', 'necesito', 'quiero', 'busco', 'tienen', 'precio', 'cocina', 'gabinete', 'puedo', 'donde', 'como', 'cuando', 'gracias', 'por favor', 'ayuda', 'informacion', 'cotizacion', 'presupuesto', 'closet', 'mueble'];
var matches = spanishIndicators.filter(function(w) { return t.includes(w); });
return matches.length >= 1 ? 'es' : 'en';
}
// ── SEND ───────────────────────────────────────────────────────────────────
function sendMessage() {
const input = document.getElementById('lioher-input');
const text = input.value.trim();
if (!text || isTyping) return;
input.value = '';
input.style.height = 'auto';
sendUserMessage(text);
}
async function sendUserMessage(text) {
addMessage('user', text);
history.push({ role: 'user', content: text });
buyingSignalScore += detectBuyingSignals(text);
if (history.filter(function(m){ return m.role === 'user'; }).length <= 1) {
detectedLanguage = detectLanguage(text);
sessionDirty = true;
}
lastWidgetType = 'none';
sessionDirty = true;
document.getElementById('lioher-send').disabled = true;
isTyping = true;
showTyping();
try {
// Retry up to 2 times on failure
let response, lastError;
for (let attempt = 0; attempt < 2; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 25000); // 25 second timeout
response = await fetch(CONFIG.aiProxy, {
signal: controller.signal,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: (function() {
var langInstruction = detectedLanguage === 'es'
? { role: 'user', content: '[SYSTEM: The customer is speaking Spanish. Respond entirely in Spanish from now on. Be warm and professional. Use "usted" form.]' }
: null;
return langInstruction ? [langInstruction].concat(history) : history;
})(),
max_tokens: 350
})
});
clearTimeout(timeoutId);
break; // success - exit retry loop
} catch(retryErr) {
lastError = retryErr;
if (attempt < 1) await new Promise(r => setTimeout(r, 2000)); // wait 2s before retry
}
}
if (!response) throw lastError;
const data = await response.json();
const reply = data.content?.[0]?.text || "I'm having trouble connecting. Please try again.";
history.push({ role: 'assistant', content: reply });
removeTyping();
const t = text.toLowerCase();
const r = reply.toLowerCase();
const bookingWords = ['book', 'appointment', 'schedule', 'call', 'meeting', 'showroom', 'consult'];
const wantsBooking = bookingWords.some(kw => t.includes(kw) || r.includes(kw));
const speakWords = ['speak with', 'talk to', 'talk to someone', 'connect me', 'real person', 'human', 'agent', 'representative', 'someone from', 'hablar con una persona', 'hablar con ventas', 'persona real'];
const wantsHuman = speakWords.some(kw => t.includes(kw));
// Detect homeowner mentions
const homeownerWords = ['homeowner', 'home owner', 'my house', 'my home', 'my kitchen', 'my closet', 'renovation', 'remodel', 'my bathroom', 'residential', 'personal project', 'my project', 'sell to home', 'work with home', 'for homeowner'];
const isHomeowner = homeownerWords.some(kw => t.includes(kw)) || (r.includes('find-a-professional') || r.includes('certified') && r.includes('professional'));
// Detect conversation winding down — offer follow-up email summary
const endSignals = ['thank you', 'thanks', 'that\'s all', 'thats all', 'bye', 'goodbye', 'perfect', 'great', 'awesome', 'got it', 'sounds good', 'no more questions'];
const isEnding = endSignals.some(s => t.includes(s));
if (isEnding && !followUpEmailOffered) {
followUpEmailOffered = true;
setTimeout(() => {
const endDiv = addMessage('agent', 'Would you like a follow-up email summary of our conversation?');
const qr = document.createElement('div');
qr.className = 'quick-replies';
['Yes, send me a summary', 'No thanks'].forEach(label => {
const chip = document.createElement('button');
chip.className = 'qr-chip';
chip.textContent = label;
chip.onclick = () => {
qr.remove();
if (label.startsWith('Yes')) {
addMessage('user', label);
if (leadCollected) {
// Already have their info — send summary directly
sendFollowUpEmail(leadInfo.first, leadInfo.last, leadInfo.email, true);
addMessage('agent', 'A summary will be sent to ' + leadInfo.email + ' at the end of the day!');
} else {
// Collect their email
const emailDiv = addMessage('agent', 'What email should I send it to?');
showFollowUpForm(emailDiv);
}
} else {
addMessage('user', label);
addMessage('agent', 'No problem! Feel free to come back anytime.');
}
};
qr.appendChild(chip);
});
appendWidget(endDiv, qr);
}, 1000);
}
// Re-show main menu chips if AI replied to a greeting
const greetingTriggers = ['hi', 'hello', 'hey', 'hola', 'good morning', 'good afternoon', 'good evening', 'sup', 'what\'s up', 'whats up'];
const isGreeting = greetingTriggers.some(g => t.trim() === g || t.trim() === g + '!' || t.trim() === g + '.');
// Detect pricing / loyalty program questions
const pricingWords = ['price', 'pricing', 'cost', 'how much', 'rate', 'discount', 'loyalty', 'bracket', 'promo', 'deal', 'offer', 'sqft', 'per foot', 'per sheet'];
const wantsPricing = pricingWords.some(kw => t.includes(kw));
// COLLECT_LEAD: AI decided user said yes to a callback
if (reply.trim() === 'COLLECT_LEAD') {
if (!leadCollected) {
const rd = addMessage('agent', "Perfect! Let me grab your details and someone will call you back shortly.");
showLeadForm(rd);
} else {
addMessage('agent', "You're all set — our team will be in touch at " + leadInfo.phone + " soon!");
}
} else {
const msgDiv = addMessage('agent', reply);
// Show contextual quick reply chips after every AI response
showQuickReplies(generateQuickReplies(reply));
// Smart lead form: show when buying signals are strong enough
if (buyingSignalScore >= 5 && !leadCollected && !leadFormShownBySignal) {
leadFormShownBySignal = true;
setTimeout(function() {
var signalDiv = addMessage('agent', detectedLanguage === 'es' ? "Parece que su proyecto va en serio. ¿Le gustaría que uno de nuestros especialistas se comunique con usted?" : "It sounds like you're getting serious about your project! Want me to have one of our specialists reach out to help?");
var sqr = document.createElement('div');
sqr.className = 'quick-replies';
var signalLabels = detectedLanguage === 'es' ? ['Sí, que me llamen', 'Todavía no, solo estoy explorando'] : ['Yes, have someone call me', 'Not yet, just exploring'];
signalLabels.forEach(function(label) {
var chip = document.createElement('button');
chip.className = 'qr-chip';
chip.textContent = label;
chip.onclick = function() {
sqr.remove();
addMessage('user', label);
if (label.startsWith('Yes') || label.startsWith('Sí')) {
var rd = addMessage('agent', detectedLanguage === 'es' ? "¡Excelente! Déjeme tomar sus datos." : "Great! Let me grab your details.");
showLeadForm(rd);
} else {
addMessage('agent', detectedLanguage === 'es' ? "¡No hay problema! Estoy aquí cuando lo necesite." : "No problem! I'm here whenever you're ready.");
}
};
sqr.appendChild(chip);
});
appendWidget(signalDiv, sqr);
}, 1500);
}
// After pricing/loyalty reply — show Yes/No callback chips (always show form regardless)
if (wantsPricing && !isGreeting) {
const qr = document.createElement('div');
qr.className = 'quick-replies';
['Yes, call me back', 'No thanks'].forEach(label => {
const chip = document.createElement('button');
chip.className = 'qr-chip';
chip.textContent = label;
chip.onclick = () => {
qr.remove();
addMessage('user', label);
if (label.startsWith('Yes')) {
if (!leadCollected) {
const rd = addMessage('agent', "Great! Let me grab your details and a rep will call you back.");
showLeadForm(rd);
} else {
sendLeadEmail(leadInfo.first, leadInfo.last, leadInfo.company, leadInfo.phone, leadInfo.email, 'Pricing Callback Request', leadInfo.zip || '', true);
addMessage('agent', "Our team will call " + leadInfo.phone + " soon!");
}
} else {
addMessage('agent', "No problem! Let me know if you need anything else.");
}
};
qr.appendChild(chip);
});
appendWidget(msgDiv, qr);
}
// Re-show main options after a mid-convo greeting
if (isGreeting) {
showMainMenu(msgDiv, ['Products & Collections', 'Loyalty Program', 'Buy Samples', 'Book an appointment', 'Open an Account', 'Showroom locations', 'Talk to a Real Person']);
} else if (isHomeowner) {
// Show homeowner-specific options
const profCard = createMenuCard([
{ label: 'Find a Lioher Professional Near Me', url: CONFIG.urls.findPro },
{ label: 'Have someone call me back', onclick: () => {
profCard.remove();
addMessage('user', 'Have someone call me back');
const rd = addMessage('agent', 'Perfect! Leave your details and an inside sales rep will call you within minutes.');
showLeadForm(rd);
}
}
]);
appendWidget(msgDiv, profCard);
} else if (wantsHuman) {
if (leadCollected) {
handoffToHuman('Customer requested human agent');
addMessage('agent', "I've notified our inside sales team — they have your full conversation and will reach out to " + leadInfo.phone + " shortly. You can keep chatting with me in the meantime!");
} else {
var handoffDiv2 = addMessage('agent', "I'll connect you with our team! Just need a couple details so they can reach you.");
var hoForm2 = createLeadForm('ho2', [
{ id: 'first', placeholder: 'First Name' },
{ id: 'last', placeholder: 'Last Name' },
{ id: 'phone', placeholder: 'Phone Number', type: 'tel' },
{ id: 'email', placeholder: 'Email Address', type: 'email' }
], function(form, prefix) {
var first = document.getElementById(prefix + '-first').value.trim();
var last = document.getElementById(prefix + '-last').value.trim();
var phone = document.getElementById(prefix + '-phone').value.trim();
var email = document.getElementById(prefix + '-email').value.trim();
[prefix+'-first',prefix+'-phone',prefix+'-email'].forEach(function(id){ document.getElementById(id).classList.remove('error'); });
var valid = true;
if (!first) { document.getElementById(prefix+'-first').classList.add('error'); valid = false; }
if (!phone) { document.getElementById(prefix+'-phone').classList.add('error'); valid = false; }
if (!email) { document.getElementById(prefix+'-email').classList.add('error'); valid = false; }
if (!valid) return;
form.remove();
leadCollected = true;
leadInfo = { first: first, last: last || '', company: '', phone: phone, email: email };
handoffToHuman('Customer requested human agent');
addMessage('agent', "Our inside sales team has been notified with your full conversation. They'll call " + phone + " shortly!");
});
hoForm2.buttonText = 'Connect Me';
appendWidget(handoffDiv2, hoForm2);
}
} else if (wantsBooking) {
showInlineBooking(selectedAppointmentType || 'quickCall');
}
}
} catch (err) {
removeTyping();
const errDiv = addMessage('agent', "I'm having a little trouble right now. You can visit lioher.com or call us at 305-685-0005 — or I can have someone call you back!");
const errQr = document.createElement('div');
errQr.className = 'quick-replies';
const errChip = document.createElement('button');
errChip.className = 'qr-chip';
errChip.textContent = 'Yes, call me back';
errChip.onclick = () => {
errQr.remove();
addMessage('user', 'Yes, call me back');
const rd = addMessage('agent', "No problem! Let me grab your details and someone will call you shortly.");
showLeadForm(rd);
};
errQr.appendChild(errChip);
appendWidget(errDiv, errQr);
console.error(err);
}
isTyping = false;
document.getElementById('lioher-send').disabled = false;
}
// ── AUTO POPUP BUBBLE ──────────────────────────────────────────────────────
let bubbleShown = false;
function showPopupBubble() {
if (bubbleShown || isOpen) return;
bubbleShown = true;
const bubble = document.createElement('div');
bubble.id = 'lio-popup-bubble';
bubble.style.cssText = 'position:fixed;bottom:104px;right:28px;background:#ffffff;border:1px solid rgba(91,126,191,0.25);border-radius:16px 16px 4px 16px;padding:12px 16px;font-family:Montserrat,Arial,sans-serif;font-size:13px;font-weight:400;color:#1a1a1a;max-width:220px;box-shadow:0 8px 32px rgba(0,0,0,0.14);z-index:9997;cursor:pointer;line-height:1.5;opacity:0;transform:translateY(12px) scale(0.9);transition:opacity 0.35s ease,transform 0.35s cubic-bezier(0.34,1.56,0.64,1);';
bubble.innerHTML = '
Any questions? I\'m here to help! 👋✕
';
document.body.appendChild(bubble);
// Animate in
requestAnimationFrame(() => {
requestAnimationFrame(() => {
bubble.style.opacity = '1';
bubble.style.transform = 'translateY(0) scale(1)';
});
});
function closeBubble() {
bubble.style.opacity = '0';
bubble.style.transform = 'translateY(8px) scale(0.95)';
setTimeout(() => { if (bubble.parentNode) bubble.remove(); }, 300);
}
document.getElementById('lio-bubble-x').addEventListener('click', function(e) {
e.stopPropagation();
closeBubble();
});
bubble.addEventListener('click', function() {
closeBubble();
if (!isOpen) toggleChat();
});
// Auto-hide after 8 seconds
setTimeout(closeBubble, 8000);
}
// Show bubble after 3 seconds if user hasn't opened chat
setTimeout(showPopupBubble, 3000);
// ── PROACTIVE CHAT TRIGGERS ─────────────────────────────────────────────
function setupProactiveTriggers() {
var path = window.location.pathname.toLowerCase();
var triggerDelay = null;
var triggerMessage = '';
// Product pages — 20 seconds
if (path.includes('/products/kitchens') || path.includes('/outdoor-kitchens') || path.includes('/closets')) {
triggerDelay = 20000;
triggerMessage = 'Need help choosing the right cabinets for your project?';
}
// Surface collections — 25 seconds
else if (path.includes('/luxe-collection') || path.includes('/zenit-collection') || path.includes('/syncron-collection')) {
triggerDelay = 25000;
triggerMessage = 'Want to see how these finishes look in person? We can send you samples.';
}
// Doors page
else if (path.includes('/doors')) {
triggerDelay = 20000;
triggerMessage = 'Looking for the perfect cabinet doors? I can help you find the right style.';
}
// Design/Inspiration pages — 30 seconds
else if (path.includes('/designs') || path.includes('/inspiration') || path.includes('/gallery')) {
triggerDelay = 30000;
triggerMessage = 'Love what you see? I can help you bring this design to life.';
}
// Booking pages — 15 seconds
else if (path.includes('/design-locations') || path.includes('/conference-room') || path.includes('/discovery-selection')) {
triggerDelay = 15000;
triggerMessage = 'Need help picking the right appointment type? I can guide you.';
}
// Homepage — 35 seconds
else if (path === '/' || path === '') {
triggerDelay = 35000;
triggerMessage = 'Welcome to Lioher! Are you a homeowner or a trade professional?';
}
// Exit intent on any page
document.addEventListener('mouseleave', function(e) {
if (e.clientY < 0 && !isOpen && !sessionStorage.getItem('lioher_popup_dismissed') && !sessionStorage.getItem('lioher_chat_interacted')) {
showProactivePopup('Before you go — want us to help you find what you need?');
}
}, { once: true });
if (triggerDelay && !sessionStorage.getItem('lioher_popup_dismissed') && !sessionStorage.getItem('lioher_chat_interacted')) {
setTimeout(function() {
if (!isOpen) {
showProactivePopup(triggerMessage);
}
}, triggerDelay);
}
}
function showProactivePopup(message) {
// Remove any existing popup
var existing = document.getElementById('lioher-proactive');
if (existing) existing.remove();
var popup = document.createElement('div');
popup.id = 'lioher-proactive';
popup.style.cssText = 'position:fixed;bottom:100px;right:28px;max-width:280px;background:#fff;border-radius:16px;padding:16px 20px;box-shadow:0 8px 32px rgba(0,0,0,0.15);z-index:9997;font-family:Montserrat,sans-serif;animation:si-fadeIn 0.3s ease;border:1px solid rgba(91,126,191,0.2);';
popup.innerHTML = '