Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>EXIT Realty CW | Property Concierge</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <style> | |
| body { | |
| font-family: 'Inter', system-ui, -apple-system, sans-serif; | |
| font-size: 15px; | |
| } | |
| /* Brand utilities (since Tailwind CDN has no custom theme) */ | |
| .text-exit-blue { | |
| color: #1e40af ; | |
| } | |
| .bg-exit-blue { | |
| background-color: #1e40af ; | |
| } | |
| .border-exit-blue { | |
| border-color: #1e40af ; | |
| } | |
| .bg-exit-green { | |
| background-color: #22c55e ; | |
| } | |
| .exit-gradient { | |
| background: linear-gradient(135deg, #1e40af 0%, #22c55e 100%); | |
| } | |
| .exit-shadow { | |
| box-shadow: 0 8px 32px 0 rgba(30, 64, 175, 0.18), 0 1.5px 6px 0 rgba(34, 197, 94, 0.10); | |
| } | |
| .exit-glass { | |
| background: rgba(255, 255, 255, 0.92); | |
| backdrop-filter: blur(8px); | |
| } | |
| .exit-logo { | |
| width: 110px; | |
| height: auto; | |
| } | |
| .exit-btn { | |
| background: #1e40af; | |
| color: #fff; | |
| font-weight: 600; | |
| border-radius: 0.5rem; | |
| transition: background 0.2s; | |
| font-size: 0.95rem; | |
| padding: 0.5rem 1rem; | |
| } | |
| .exit-btn:hover { | |
| background: #143075; | |
| } | |
| /* Typing indicator */ | |
| .typing-dots { | |
| display: inline-flex; | |
| gap: 4px; | |
| align-items: center; | |
| } | |
| .typing-dots span { | |
| width: 4px; | |
| height: 4px; | |
| border-radius: 9999px; | |
| background: currentColor; | |
| opacity: 0.6; | |
| animation: blink 1s infinite ease-in-out; | |
| } | |
| .typing-dots span:nth-child(2) { | |
| animation-delay: 0.2s; | |
| } | |
| .typing-dots span:nth-child(3) { | |
| animation-delay: 0.4s; | |
| } | |
| @keyframes blink { | |
| 0%, | |
| 80%, | |
| 100% { | |
| opacity: 0.2 | |
| } | |
| 40% { | |
| opacity: 1 | |
| } | |
| } | |
| /* Mobile-first adjustments */ | |
| @media (max-width: 640px) { | |
| .exit-logo { | |
| width: 90px; | |
| } | |
| main { | |
| padding-left: 0.25rem ; | |
| padding-right: 0.25rem ; | |
| } | |
| .exit-glass { | |
| padding: 0.75rem ; | |
| } | |
| .exit-glass h1 { | |
| font-size: 1.1rem ; | |
| margin-top: 1.5rem ; | |
| } | |
| .exit-glass p { | |
| font-size: 0.92rem ; | |
| } | |
| #concierge-chat-box { | |
| height: 12rem ; | |
| font-size: 0.92rem; | |
| } | |
| .exit-glass form { | |
| flex-direction: column ; | |
| gap: 0.25rem ; | |
| } | |
| .exit-btn { | |
| width: 100%; | |
| padding: 0.5rem 0 ; | |
| font-size: 0.92rem; | |
| } | |
| .exit-glass .absolute { | |
| top: -1.5rem ; | |
| padding: 0.5rem ; | |
| } | |
| .exit-glass .absolute i { | |
| width: 1.3rem ; | |
| height: 1.3rem ; | |
| } | |
| .exit-glass { | |
| border-width: 2px ; | |
| } | |
| .why-exit-flex { | |
| flex-direction: column ; | |
| } | |
| .why-exit-flex>div { | |
| min-width: 0 ; | |
| } | |
| #bottom-nav { | |
| height: 48px ; | |
| } | |
| #bottom-nav i { | |
| width: 1.1rem ; | |
| height: 1.1rem ; | |
| } | |
| #bottom-nav span { | |
| font-size: 0.7rem ; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="exit-gradient min-h-screen flex flex-col justify-center items-center"> | |
| <header class="w-full flex justify-center py-3"> | |
| <img src="https://www.exitrealty.com/images/logos/EXIT_Realty_Logo.png" alt="EXIT Realty CW Logo" | |
| class="exit-logo shadow-lg rounded-xl bg-white p-1" style="max-width:72px;"> | |
| </header> | |
| <main class="w-full max-w-2xl mx-auto px-6"> | |
| <section | |
| class="exit-glass exit-shadow rounded-3xl p-10 flex flex-col items-center border-4 border-white relative"> | |
| <div | |
| class="absolute -top-12 left-1/2 -translate-x-1/2 bg-white border-4 border-exit-blue rounded-full p-5 shadow-lg"> | |
| <i data-feather="message-circle" class="w-10 h-10 text-exit-blue"></i> | |
| </div> | |
| <h1 class="text-xl font-extrabold text-exit-blue mt-2 mb-1 text-center tracking-tight" | |
| style="line-height:1.1">Property Concierge</h1> | |
| <p class="text-lg text-gray-700 mb-6 text-center max-w-lg">Experience the next level of real estate service. | |
| Our AI-powered Property Concierge is here to answer your questions, match you with the perfect home, and | |
| guide you through every step of your real estate journey—24/7, with a personal touch.</p> | |
| <div id="concierge-chat-box" | |
| class="w-full h-80 bg-gray-50 rounded-xl border border-gray-200 p-4 overflow-y-auto mb-4 flex flex-col space-y-2"> | |
| </div> | |
| <!-- Profile: display name and email --> | |
| <div id="profile-panel" class="w-full mb-3 grid grid-cols-1 sm:grid-cols-2 gap-2"> | |
| <input id="display-name-input" class="border rounded px-3 py-2" placeholder="Your name (for display)" /> | |
| <input id="display-email-input" class="border rounded px-3 py-2" placeholder="Your email (optional)" /> | |
| </div> | |
| <div id="quick-replies" class="w-full flex flex-wrap gap-2 mb-3"> | |
| <button class="px-3 py-1.5 text-sm bg-white border rounded-full hover:bg-gray-50" | |
| data-qr="I want to talk to an agent">Talk to an agent</button> | |
| <button class="px-3 py-1.5 text-sm bg-white border rounded-full hover:bg-gray-50" | |
| data-qr="I'd like to schedule a showing">Schedule a showing</button> | |
| <button class="px-3 py-1.5 text-sm bg-white border rounded-full hover:bg-gray-50" | |
| data-qr="Please provide a free valuation of my home">Get a valuation</button> | |
| <button class="px-3 py-1.5 text-sm bg-white border rounded-full hover:bg-gray-50" | |
| data-qr="Show me new listings today in Plover, WI">New listings today</button> | |
| </div> | |
| <form id="concierge-chat-form" class="w-full flex gap-2 sticky bottom-16 bg-white z-40 pt-2 pb-2" | |
| autocomplete="off" style="box-shadow:0 -2px 8px 0 rgba(30,64,175,0.04)"> | |
| <input id="concierge-chat-input" type="text" placeholder="Type your message..." | |
| class="flex-1 px-4 py-2 rounded-lg border border-gray-300 focus:border-exit-blue focus:ring-1 focus:ring-exit-blue focus:outline-none" | |
| required> | |
| <button type="submit" class="exit-btn px-6 py-2 flex items-center gap-1"> | |
| <i data-feather="send" class="w-4 h-4"></i> | |
| <span>Send</span> | |
| </button> | |
| </form> | |
| <div id="lead-capture" class="w-full mt-3 hidden"> | |
| <div class="bg-white border rounded-xl p-3"> | |
| <div class="text-sm font-semibold text-exit-blue mb-2">Connect with the EXIT team</div> | |
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-2"> | |
| <input id="lead-name" class="border rounded px-3 py-2" placeholder="Full name" /> | |
| <input id="lead-email" class="border rounded px-3 py-2" placeholder="Email" /> | |
| <input id="lead-phone" class="border rounded px-3 py-2" placeholder="Phone" /> | |
| </div> | |
| <div class="flex items-center gap-2 mt-2"> | |
| <button id="lead-submit" class="exit-btn px-4 py-2">Send to EXIT team</button> | |
| <a id="lead-call" href="tel:+17155983794" class="px-3 py-2 text-sm underline">Call (715) | |
| 598-3794</a> | |
| <a id="lead-email-link" href="mailto:info@exitcw.com?subject=New Inquiry" | |
| class="px-3 py-2 text-sm underline">Email info@exitcw.com</a> | |
| </div> | |
| <div id="lead-status" class="text-xs text-gray-500 mt-2"></div> | |
| </div> | |
| </div> | |
| <div class="mt-4 text-xs text-gray-400 text-center">Your conversation is private and secure. EXIT Realty CW | |
| is committed to your success.</div> | |
| </section> | |
| <section class="mt-12 text-center"> | |
| <h2 class="text-2xl font-bold text-white mb-2">Why Choose EXIT Realty CW?</h2> | |
| <div class="flex why-exit-flex flex-col md:flex-row gap-6 justify-center"> | |
| <div class="bg-white/90 rounded-xl p-6 flex-1 min-w-[220px]"> | |
| <i data-feather="users" class="w-8 h-8 text-exit-blue mb-2"></i> | |
| <div class="font-semibold text-exit-blue mb-1">Local Experts</div> | |
| <div class="text-gray-600 text-sm">Our agents know the neighborhoods, schools, and market trends | |
| inside out.</div> | |
| </div> | |
| <div class="bg-white/90 rounded-xl p-6 flex-1 min-w-[220px]"> | |
| <i data-feather="home" class="w-8 h-8 text-exit-blue mb-2"></i> | |
| <div class="font-semibold text-exit-blue mb-1">Personalized Service</div> | |
| <div class="text-gray-600 text-sm">We tailor every experience to your needs, whether buying, | |
| selling, or investing.</div> | |
| </div> | |
| <div class="bg-white/90 rounded-xl p-6 flex-1 min-w-[220px]"> | |
| <i data-feather="award" class="w-8 h-8 text-exit-blue mb-2"></i> | |
| <div class="font-semibold text-exit-blue mb-1">Trusted Results</div> | |
| <div class="text-gray-600 text-sm">EXIT Realty CW delivers results you can count on, with integrity | |
| and care.</div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Brokerage Intelligence: Zillow Blitz Assistant --> | |
| <section class="mt-10 bg-white/95 rounded-2xl p-5 border shadow exit-shadow"> | |
| <div class="flex items-center justify-between gap-3 flex-wrap"> | |
| <h3 class="text-xl font-extrabold text-exit-blue">Daily Property Intelligence</h3> | |
| <div id="intel-toast" class="hidden text-sm text-white bg-exit-blue px-3 py-1 rounded">Report generated | |
| </div> | |
| </div> | |
| <div class="mt-4 grid gap-4"> | |
| <!-- Source selection --> | |
| <div class="grid gap-3"> | |
| <div class="text-sm font-semibold">Sources</div> | |
| <div class="flex flex-wrap gap-3"> | |
| <label class="flex items-center gap-2 bg-gray-100 rounded-md px-3 py-2"><input id="src-zillow" | |
| type="checkbox" checked> <span>Zillow</span></label> | |
| <label class="flex items-center gap-2 bg-gray-100 rounded-md px-3 py-2"><input id="src-homes" | |
| type="checkbox" checked> <span>Homes.com</span></label> | |
| <label class="flex items-center gap-2 bg-gray-100 rounded-md px-3 py-2"><input id="src-mls" | |
| type="checkbox"> <span>MLS (CSV)</span></label> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs text-gray-600"> | |
| <input id="intel-mls-csv" type="file" accept=".csv"> | |
| <span>Upload MLS CSV (optional)</span> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <input id="intel-location" class="border rounded px-3 py-2 w-full" | |
| placeholder="Location (e.g., Plover, WI)" value="Plover, WI" /> | |
| <button id="intel-generate" class="exit-btn px-4 py-2">Generate</button> | |
| <span id="intel-loading" class="hidden text-sm text-gray-500">Loading…</span> | |
| </div> | |
| </div> | |
| <!-- Filters --> | |
| <div class="grid sm:grid-cols-2 md:grid-cols-4 gap-3"> | |
| <input id="f-price-min" class="border rounded px-3 py-2" placeholder="Min $" /> | |
| <input id="f-price-max" class="border rounded px-3 py-2" placeholder="Max $" /> | |
| <input id="f-city" class="border rounded px-3 py-2" placeholder="City" /> | |
| <select id="f-type" class="border rounded px-3 py-2"> | |
| <option value="">Any Type</option> | |
| <option value="residential">Residential</option> | |
| <option value="condo">Condo</option> | |
| <option value="townhome">Townhome</option> | |
| <option value="land">Land</option> | |
| </select> | |
| </div> | |
| <!-- Dashboard --> | |
| <div id="intel-dashboard" class=""></div> | |
| <!-- View toggles & exports --> | |
| <div class="flex flex-wrap items-center justify-between gap-3"> | |
| <label class="flex items-center gap-2 text-sm"><input id="intel-toggle-raw" type="checkbox"> | |
| <span>Show raw table</span></label> | |
| <div class="flex gap-2"> | |
| <button id="intel-export-csv" class="bg-gray-800 text-white px-3 py-2 rounded">Export | |
| CSV</button> | |
| <button id="intel-export-pdf" | |
| class="bg-gray-800 text-white px-3 py-2 rounded">Print/PDF</button> | |
| </div> | |
| </div> | |
| <!-- Report Output --> | |
| <div id="intel-report-output" class="grid gap-3"></div> | |
| <!-- Scheduling & notifications --> | |
| <div class="bg-gray-50 rounded-xl p-4 border grid gap-3"> | |
| <div class="font-semibold">Automation & Scheduling</div> | |
| <div class="grid sm:grid-cols-2 md:grid-cols-4 gap-3 items-center"> | |
| <label class="flex items-center gap-2 text-sm"><input id="sched-enable" type="checkbox"> | |
| <span>Enable daily report</span></label> | |
| <div class="flex items-center gap-2"><span class="text-sm text-gray-600">Time</span><input | |
| id="sched-time" type="time" value="08:00" class="border rounded px-2 py-1" /></div> | |
| <input id="slack-webhook" class="border rounded px-2 py-2" | |
| placeholder="Slack webhook URL (optional)" /> | |
| <input id="email-to" class="border rounded px-2 py-2" placeholder="Email to (optional)" /> | |
| </div> | |
| <div class="text-xs text-gray-500">Email sending requires a backend; we prepare a mailto link when | |
| the report is ready. <a id="mailto-link" href="#" class="underline">Open draft</a></div> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Bottom Navigation Bar (Material style, mobile only) --> | |
| <nav id="bottom-nav" | |
| class="fixed bottom-0 left-0 w-full z-50 bg-white border-t border-gray-200 flex justify-around items-center h-16 shadow-lg sm:hidden"> | |
| <button id="nav-chat" class="flex flex-col items-center justify-center flex-1 h-full focus:outline-none" | |
| aria-label="Chat"> | |
| <i data-feather="message-circle" class="w-6 h-6"></i> | |
| <span class="text-xs mt-0.5">Chat</span> | |
| </button> | |
| <button id="nav-gmail" class="flex flex-col items-center justify-center flex-1 h-full focus:outline-none" | |
| aria-label="Gmail Compose"> | |
| <i data-feather="mail" class="w-6 h-6"></i> | |
| <span class="text-xs mt-0.5">Gmail</span> | |
| </button> | |
| <button id="nav-outlook" class="flex flex-col items-center justify-center flex-1 h-full focus:outline-none" | |
| aria-label="Outlook Compose"> | |
| <i data-feather="send" class="w-6 h-6"></i> | |
| <span class="text-xs mt-0.5">Outlook</span> | |
| </button> | |
| <a id="nav-call" href="tel:+17155983794" | |
| class="flex flex-col items-center justify-center flex-1 h-full focus:outline-none" aria-label="Call"> | |
| <i data-feather="phone" class="w-6 h-6"></i> | |
| <span class="text-xs mt-0.5">Call</span> | |
| </a> | |
| <a id="nav-email" href="mailto:info@exitcw.com?subject=New Inquiry" | |
| class="flex flex-col items-center justify-center flex-1 h-full focus:outline-none" aria-label="Email"> | |
| <i data-feather="at-sign" class="w-6 h-6"></i> | |
| <span class="text-xs mt-0.5">Email</span> | |
| </a> | |
| </nav> | |
| <footer class="w-full text-center py-8 mt-12 text-white/80 text-sm"> | |
| © 2025 EXIT Realty CW. All rights reserved. | <a href="#" class="underline hover:text-white">Privacy | |
| Policy</a> | |
| </footer> | |
| <script src="chat.ddgs.js"></script> | |
| <script src="brokerage-intel.js"></script> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function () { | |
| feather.replace(); | |
| const chatBox = document.getElementById('concierge-chat-box'); | |
| const chatForm = document.getElementById('concierge-chat-form'); | |
| const chatInput = document.getElementById('concierge-chat-input'); | |
| const displayNameInput = document.getElementById('display-name-input'); | |
| const displayEmailInput = document.getElementById('display-email-input'); | |
| const navGmailBtn = document.getElementById('nav-gmail'); | |
| const navOutlookBtn = document.getElementById('nav-outlook'); | |
| const quickReplies = document.getElementById('quick-replies'); | |
| const leadCapture = document.getElementById('lead-capture'); | |
| const leadName = document.getElementById('lead-name'); | |
| const leadEmail = document.getElementById('lead-email'); | |
| const leadPhone = document.getElementById('lead-phone'); | |
| const leadSubmit = document.getElementById('lead-submit'); | |
| const leadStatus = document.getElementById('lead-status'); | |
| const userProfile = { name: '', email: '', phone: '', service: '', vehicle: '', mileage: '', details: '', contact_method: '', best_time: '', hear_about: '', urgency: '', newsletter: false }; | |
| const chatHistory = []; | |
| const ASSISTANT_NAME = 'Ellie — EXIT Realty CW Concierge'; | |
| const empathyPhrases = [ | |
| "I hear you.", | |
| "That makes total sense.", | |
| "Thanks for sharing that.", | |
| "I’ve got you.", | |
| "We’ll make this easy." | |
| ]; | |
| const LLAMA_PROXY_URL = "https://llama-universal-netlify-project.netlify.app/.netlify/functions/llama-proxy?path=/chat/completions"; | |
| let exitTeamLinks = []; | |
| chatInput.focus(); | |
| setTimeout(() => { | |
| document.querySelector('main').scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| }, 200); | |
| // Prefetch EXIT team/contact links via DDGS | |
| (async () => { | |
| try { | |
| const tool = window.propertyConciergeTools && window.propertyConciergeTools.ddgs; | |
| if (!tool) return; | |
| const q1 = await tool.fn({ query: 'EXIT Realty CW contact' }); | |
| const q2 = await tool.fn({ query: 'site:exitcw.com team' }); | |
| const q3 = await tool.fn({ query: 'site:exitcw.com agent' }); | |
| exitTeamLinks = [...(q1.results || []), ...(q2.results || []), ...(q3.results || [])] | |
| .filter((r, i, arr) => r.url && arr.findIndex(x => x.url === r.url) === i) | |
| .slice(0, 5); | |
| } catch { } | |
| })(); | |
| function safeLabel(name, fallback) { | |
| const s = (name || '').trim(); | |
| return s.length ? s : fallback; | |
| } | |
| function addMessage(message, isUser = false) { | |
| const msg = document.createElement('div'); | |
| msg.className = `flex ${isUser ? 'justify-end' : 'justify-start'}`; | |
| const displayName = localStorage.getItem('display_name'); | |
| const userLabel = safeLabel(displayName, 'You'); | |
| const assistantLabel = 'Ellie'; | |
| const labelHtml = `<div class="text-[10px] text-gray-500 mb-0.5 ml-1">${isUser ? userLabel : assistantLabel}</div>`; | |
| msg.innerHTML = ` | |
| <div> | |
| ${labelHtml} | |
| <div class="inline-block max-w-xs px-4 py-2 rounded-lg shadow ${isUser ? 'bg-exit-blue text-white' : 'bg-gray-50 text-gray-900'}"> | |
| ${message} | |
| </div> | |
| </div> | |
| `; | |
| chatBox.appendChild(msg); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } | |
| // Typing indicator helpers | |
| let typingEl = null; | |
| function showTyping() { | |
| if (typingEl) return; | |
| typingEl = document.createElement('div'); | |
| typingEl.className = 'flex justify-start'; | |
| typingEl.innerHTML = ` | |
| <div class="inline-block max-w-xs px-4 py-2 rounded-lg shadow bg-gray-50 text-gray-900"> | |
| <span class="typing-dots"><span></span><span></span><span></span></span> | |
| </div>`; | |
| chatBox.appendChild(typingEl); | |
| chatBox.scrollTop = chatBox.scrollHeight; | |
| } | |
| function hideTyping() { | |
| if (typingEl?.parentNode) typingEl.parentNode.removeChild(typingEl); | |
| typingEl = null; | |
| } | |
| function startConversation() { | |
| const welcome = `Hi, I’m <b>${ASSISTANT_NAME}</b>. How can I help today? Looking for a home, selling, or just exploring?`; | |
| addMessage(welcome); | |
| chatHistory.push({ role: "assistant", content: welcome }); | |
| } | |
| function extractProfileInfo(message) { | |
| if (!userProfile.email && /@/.test(message) && /\./.test(message)) { | |
| userProfile.email = message.match(/([\w.-]+@[\w.-]+)/)?.[1] || userProfile.email; | |
| } | |
| if (!userProfile.phone && /\d{3}[\s.-]?\d{3}[\s.-]?\d{4}/.test(message)) { | |
| userProfile.phone = message.match(/(\d{3}[\s.-]?\d{3}[\s.-]?\d{4})/)?.[1] || userProfile.phone; | |
| } | |
| if (!userProfile.name && /my name is|i am|this is/i.test(message)) { | |
| const nameMatch = message.match(/(?:my name is|i am|this is)\s+([A-Za-z ]+)/i); | |
| if (nameMatch) userProfile.name = nameMatch[1].trim(); | |
| } | |
| // Price range extraction | |
| const range = message.match(/\$?\s*([0-9]{2,3}[,0-9]{0,3})\s*(k|K)?\s*[-to]{1,3}\s*\$?\s*([0-9]{2,3}[,0-9]{0,3})\s*(k|K)?/); | |
| if (range) { | |
| const low = Number(range[1].replace(/,/g, '')) * (range[2] ? 1000 : 1); | |
| const high = Number(range[3].replace(/,/g, '')) * (range[4] ? 1000 : 1); | |
| userProfile.price_low = low; userProfile.price_high = high; | |
| } | |
| const under = message.match(/under\s+\$?\s*([0-9]{2,3}[,0-9]{0,3})\s*(k|K)?/i); | |
| if (under) { | |
| const high = Number(under[1].replace(/,/g, '')) * (under[2] ? 1000 : 1); | |
| userProfile.price_high = high; | |
| } | |
| const cityMatch = message.match(/in\s+([A-Za-z\s]+),\s*(WI|Wisconsin)/i); | |
| if (cityMatch) { userProfile.city = cityMatch[1].trim(); userProfile.state = 'WI'; } | |
| if (!userProfile.service && /(buy|sell|explor|listing|property|home|house|land|commercial)/i.test(message)) { | |
| userProfile.service = message; | |
| } | |
| } | |
| function extractAddress(message) { | |
| const addressRegex = /(\d+\s+[\w\s]+,?\s*[\w\s]+,?\s*WI\b)/i; | |
| const match = message.match(addressRegex); | |
| return match ? match[0] : null; | |
| } | |
| function empathyPrefix(text) { | |
| const casual = /^(hi|hello|hey|yo|sup|what'?s? up|howdy|good (morning|afternoon|evening)|greetings)[!.,\s]*$/i; | |
| if (casual.test(text.trim())) return ""; | |
| if (/frustrat|stress|overwhelm|confus|worri|anxious|nerv|discourag|tough|hard|difficult|lost|stuck|scared|nervous|afraid|uncertain|unsure/i.test(text)) return empathyPhrases[0]; | |
| if (/thank|appreciate|grateful/i.test(text)) return "You're very welcome."; | |
| if (/excited|great|awesome|amazing|yay|love/i.test(text)) return "Love the energy!"; | |
| return ""; | |
| } | |
| async function llamaReply(lastUserMessage) { | |
| const messages = chatHistory.map(m => ({ role: m.role, content: m.content })); | |
| const systemPrompt = | |
| `You are ${ASSISTANT_NAME}, a warm, friendly, and expert real estate concierge for EXIT Realty CW.\n\n` + | |
| `Principles:\n` + | |
| `- Match the user's tone: casual for greetings, empathetic only if the user expresses stress or negative emotion.\n` + | |
| `- Be concise, human, and conversational (no scripts).\n` + | |
| `- Ask one clear follow-up question at a time.\n` + | |
| `- Offer concrete next steps: schedule a showing, request a valuation, connect with an agent.\n` + | |
| `- When the user seems ready or asks about agents, proactively offer to connect with the EXIT team and collect name, email, and phone (one at a time).\n` + | |
| `- Reflect user details (location, price range, timeline) to show you’re listening.\n` + | |
| `- If the user mentions a specific address or property, suggest relevant steps (book a tour, check availability).\n` + | |
| `- Keep it friendly, supportive, and encouraging.\n\n` + | |
| `User profile so far: ${JSON.stringify(userProfile)}.`; | |
| messages.unshift({ role: "system", content: systemPrompt }); | |
| const body = { | |
| model: "Llama-3.3-8B-Instruct", | |
| messages: messages, | |
| max_tokens: 256, | |
| temperature: 0.7 | |
| }; | |
| try { | |
| showTyping(); | |
| const res = await fetch(LLAMA_PROXY_URL, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(body) | |
| }); | |
| if (!res.ok) throw new Error("Llama API error"); | |
| const data = await res.json(); | |
| let reply = "(No response)"; | |
| if (data.choices?.[0]?.message?.content) { | |
| reply = data.choices[0].message.content; | |
| } else if (data.completion_message?.content?.text) { | |
| reply = data.completion_message.content.text; | |
| } | |
| const prefix = empathyPrefix(lastUserMessage || ''); | |
| addMessage(`${prefix} ${reply}`); | |
| chatHistory.push({ role: "assistant", content: reply }); | |
| extractProfileInfo(reply); | |
| } catch (err) { | |
| addMessage("Sorry, I'm having trouble reaching our concierge AI right now."); | |
| } finally { | |
| hideTyping(); | |
| } | |
| } | |
| async function agentReply(message) { | |
| if (message.toLowerCase().startsWith('search:')) { | |
| const query = message.replace(/^search:/i, '').trim(); | |
| addMessage('<span class="italic text-gray-500">Searching the web for: ' + query + ' ...</span>'); | |
| const tool = window.propertyConciergeTools && window.propertyConciergeTools.ddgs; | |
| if (tool) { | |
| const result = await tool.fn({ query }); | |
| if (result.results && result.results.length > 0) { | |
| let html = '<b>Top web results:</b><ul class="list-disc pl-5">'; | |
| for (const r of result.results.slice(0, 3)) { | |
| html += `<li><a href="${r.url}" target="_blank" class="text-exit-blue underline">${r.title}</a><br><span class="text-xs text-gray-600">${r.snippet}</span></li>`; | |
| } | |
| html += '</ul>'; | |
| addMessage(html); | |
| } else { | |
| addMessage('<span class="italic text-gray-500">No relevant web results found.</span>'); | |
| } | |
| } else { | |
| addMessage('<span class="italic text-red-500">Web search tool not available.</span>'); | |
| } | |
| return; | |
| } | |
| const address = extractAddress(message); | |
| if (address) { | |
| addMessage('<span class="italic text-gray-500">Looking up information for: ' + address + ' ...</span>'); | |
| const tool = window.propertyConciergeTools && window.propertyConciergeTools.ddgs; | |
| if (tool) { | |
| const result = await tool.fn({ query: address }); | |
| if (result.results && result.results.length > 0) { | |
| let html = '<b>Top web results for this property:</b><ul class="list-disc pl-5">'; | |
| for (const r of result.results.slice(0, 3)) { | |
| html += `<li><a href="${r.url}" target="_blank" class="text-exit-blue underline">${r.title}</a><br><span class="text-xs text-gray-600">${r.snippet}</span></li>`; | |
| } | |
| html += '</ul>'; | |
| addMessage(html); | |
| } else { | |
| addMessage('<span class="italic text-gray-500">No relevant web results found for this address.</span>'); | |
| } | |
| } else { | |
| addMessage('<span class="italic text-red-500">Web search tool not available.</span>'); | |
| } | |
| } | |
| extractProfileInfo(message); | |
| const funnelIntent = /(agent|talk to an agent|talk to agent|contact|call me|reach me|schedule|showing|valuation|market analysis|consult|appointment)/i.test(message); | |
| if (funnelIntent) { | |
| leadCapture.classList.remove('hidden'); | |
| let linksHtml = ''; | |
| if (exitTeamLinks.length) { | |
| linksHtml = '<ul class="list-disc pl-5 text-sm">' + exitTeamLinks.slice(0, 3).map(l => `<li><a href="${l.url}" target="_blank" class="text-exit-blue underline">${l.title}</a></li>`).join('') + '</ul>'; | |
| } | |
| addMessage(`I can connect you with the EXIT team right away. ${linksHtml || ''}`); | |
| } | |
| await llamaReply(message); | |
| } | |
| async function sendMessage(e) { | |
| e.preventDefault(); | |
| const message = chatInput.value.trim(); | |
| if (!message) return; | |
| addMessage(message, true); | |
| chatHistory.push({ role: "user", content: message }); | |
| chatInput.value = ''; | |
| await agentReply(message); | |
| } | |
| chatForm.addEventListener('submit', sendMessage); | |
| chatInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| chatForm.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); | |
| } | |
| }); | |
| // Quick replies | |
| if (quickReplies) { | |
| quickReplies.addEventListener('click', (e) => { | |
| const btn = e.target.closest('button[data-qr]'); | |
| if (!btn) return; | |
| chatInput.value = btn.getAttribute('data-qr'); | |
| chatForm.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); | |
| }); | |
| } | |
| // Profile inputs: load/save | |
| try { | |
| const savedName = localStorage.getItem('display_name'); | |
| const savedEmail = localStorage.getItem('display_email'); | |
| if (savedName) displayNameInput.value = savedName; | |
| if (savedEmail) displayEmailInput.value = savedEmail; | |
| if (savedEmail) userProfile.email = savedEmail; | |
| if (savedName) userProfile.name = savedName; | |
| } catch { } | |
| displayNameInput.addEventListener('input', (e) => { | |
| const v = e.target.value.trim(); | |
| try { localStorage.setItem('display_name', v); } catch { } | |
| if (v) userProfile.name = v; | |
| }); | |
| displayEmailInput.addEventListener('input', (e) => { | |
| const v = e.target.value.trim(); | |
| try { localStorage.setItem('display_email', v); } catch { } | |
| if (v) userProfile.email = v; | |
| }); | |
| // Email compose helpers (Gmail/Outlook) with recent transcript | |
| function stripHtml(s) { return (s || '').replace(/<[^>]*>/g, ''); } | |
| function buildTranscript(limit = 12) { | |
| const recent = chatHistory.slice(-limit); | |
| const displayName = localStorage.getItem('display_name') || 'You'; | |
| return recent.map(m => { | |
| const who = m.role === 'assistant' ? 'Ellie' : displayName; | |
| return `[${who}] ${stripHtml(m.content)}`; | |
| }).join('\n'); | |
| } | |
| function openGmailCompose(to, subject, body) { | |
| const url = `https://mail.google.com/mail/?view=cm&fs=1&to=${encodeURIComponent(to)}&su=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; | |
| window.open(url, '_blank'); | |
| } | |
| function openOutlookCompose(to, subject, body) { | |
| const url = `https://outlook.office.com/mail/deeplink/compose?to=${encodeURIComponent(to)}&subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; | |
| window.open(url, '_blank'); | |
| } | |
| if (navGmailBtn) navGmailBtn.addEventListener('click', () => { | |
| const to = 'info@exitcw.com'; | |
| const subject = 'EXIT CW | Chat with Ellie (handoff)'; | |
| const body = `Name: ${localStorage.getItem('display_name') || userProfile.name || ''}\nEmail: ${localStorage.getItem('display_email') || userProfile.email || ''}\n\nRecent transcript:\n${buildTranscript(12)}`; | |
| openGmailCompose(to, subject, body); | |
| }); | |
| if (navOutlookBtn) navOutlookBtn.addEventListener('click', () => { | |
| const to = 'info@exitcw.com'; | |
| const subject = 'EXIT CW | Chat with Ellie (handoff)'; | |
| const body = `Name: ${localStorage.getItem('display_name') || userProfile.name || ''}\nEmail: ${localStorage.getItem('display_email') || userProfile.email || ''}\n\nRecent transcript:\n${buildTranscript(12)}`; | |
| openOutlookCompose(to, subject, body); | |
| }); | |
| // Scroll-aware hide/reveal for bottom nav and input | |
| let lastScrollY = window.scrollY; | |
| let navEl = document.getElementById('bottom-nav'); | |
| let chatFormEl = document.getElementById('concierge-chat-form'); | |
| window.addEventListener('scroll', () => { | |
| const curr = window.scrollY; | |
| if (curr > lastScrollY + 10) { | |
| // scrolling down | |
| navEl && (navEl.style.transform = 'translateY(100%)'); | |
| chatFormEl && (chatFormEl.style.transform = 'translateY(100%)'); | |
| } else if (curr < lastScrollY - 10) { | |
| // scrolling up | |
| navEl && (navEl.style.transform = 'translateY(0)'); | |
| chatFormEl && (chatFormEl.style.transform = 'translateY(0)'); | |
| } | |
| lastScrollY = curr; | |
| }); | |
| // On desktop, hide bottom nav and make input non-sticky | |
| function handleResize() { | |
| const navEl = document.getElementById('bottom-nav'); | |
| const chatFormEl = document.getElementById('concierge-chat-form'); | |
| if (window.innerWidth >= 640) { | |
| navEl && (navEl.style.display = 'none'); | |
| chatFormEl && (chatFormEl.classList.remove('sticky', 'bottom-16', 'bg-white', 'z-40', 'pt-2', 'pb-2')); | |
| } else { | |
| navEl && (navEl.style.display = 'flex'); | |
| chatFormEl && (chatFormEl.classList.add('sticky', 'bottom-16', 'bg-white', 'z-40', 'pt-2', 'pb-2')); | |
| } | |
| } | |
| window.addEventListener('resize', handleResize); | |
| handleResize(); | |
| // Lead submit | |
| if (leadSubmit) { | |
| leadSubmit.addEventListener('click', async (e) => { | |
| e.preventDefault(); | |
| const name = (leadName?.value || userProfile.name || '').trim(); | |
| const email = (leadEmail?.value || userProfile.email || '').trim(); | |
| const phone = (leadPhone?.value || userProfile.phone || '').trim(); | |
| if (!name || !email) { if (leadStatus) leadStatus.textContent = 'Please provide at least name and email.'; return; } | |
| if (leadStatus) leadStatus.textContent = 'Sending…'; | |
| try { | |
| const fd = new URLSearchParams(); | |
| fd.append('name', name); | |
| fd.append('email', email); | |
| fd.append('phone', phone); | |
| fd.append('_subject', 'New Lead from Property Concierge (index2)'); | |
| fd.append('source', 'index2.html'); | |
| fd.append('timestamp', new Date().toISOString()); | |
| await fetch('https://formsubmit.co/ajax/info@exitcw.com', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: fd.toString() }); | |
| if (leadStatus) leadStatus.textContent = 'Thanks! Our team will reach out shortly.'; | |
| addMessage('Thanks! I’ve shared your details with our team. We’ll be in touch soon.'); | |
| leadCapture?.classList.add('hidden'); | |
| } catch (err) { | |
| if (leadStatus) leadStatus.textContent = 'Could not send right now. Please call or email instead.'; | |
| } | |
| }); | |
| } | |
| setTimeout(() => { | |
| startConversation(); | |
| }, 400); | |
| }); | |
| </script> | |
| </body> | |
| </html> |