Skip to main content
$refs[o] && $refs[o].focus())" @scroll.window="hidden = !activePanel && window.scrollY > scrollY && window.scrollY > 80; scrolled = window.scrollY > 5; scrollY = window.scrollY" @saussy-ai-search.window="opener = null; activePanel = 'search'" :class="{ '!bg-white shadow-sm': !overlap || scrolled, '-translate-y-full': hidden }" x-trap.inert="activePanel !== null && activeArea === null" class="!fixed w-full top-0 left-1/2 -translate-x-1/2 isolate z-[999] transition-all duration-300" style="opacity:0">
Saussy Burbank Homes
{ if ($refs.areasPanel) $refs.areasPanel.focus(); }, 0) } else { activePanel = null; activeArea = null }">
Open
Areas and Communities
Panel
Open Search
{ if ($refs.menuPanel) $refs.menuPanel.focus(); }, 0)">
Open Main Menu
$refs[o] && $refs[o].focus())" style="display: none;">
Areas and Communities
{ const p = document.querySelector('[data-area-panel="charlotte-nc"]'); if (p) p.focus(); }, 0)">
Charlotte, NC
{ const p = document.querySelector('[data-area-panel="charleston-sc"]'); if (p) p.focus(); }, 0)">
Charleston, SC
{ const p = document.querySelector('[data-area-panel="myrtle-beach-sc"]'); if (p) p.focus(); }, 0)">
Myrtle Beach, SC
Explore
Available Homes
Gallery
Blog
Company
About Us
Meet the Team
Our Process
Contact
Sales
Homeowners
Careers
General Inquiries
Areas and Communities
Charlotte, NC
Charlotte, NC
Davidson Cottages
Davidson Pointe
Coming Soon
Eastland Yards
Strawberry Lake
Coming Soon
The River District
Areas and Communities
Charleston, SC
Johns Island
Kiawah River
Summerville
Nexton - Midtown
Areas and Communities
Myrtle Beach, SC
SayeBrook
SayeBrook
Areas and Communities
Charlotte, NC
Charlotte, NC
Davidson Cottages
Davidson Pointe
Coming Soon
Eastland Yards
Strawberry Lake
Coming Soon
The River District
Charleston, SC
Johns Island
Kiawah River
Summerville
Nexton - Midtown
Myrtle Beach, SC
SayeBrook
SayeBrook
{ if (mode === 'landing') transitionToChat(query); else sendMessage(query) })" class="absolute inset-0 overflow-hidden" style="display: none;" x-data="{ query: '', chips: ["Davidson Pointe Charlotte","Strawberry Lake Charlotte","River District $700s","Eastland Yards $400s","Davidson Cottages $500s","Move-In Ready","For Sale"], defaultChips: ["Davidson Pointe Charlotte","Strawberry Lake Charlotte","River District $700s","Eastland Yards $400s","Davidson Cottages $500s","Move-In Ready","For Sale"], chipsVisible: true, recentQueries: JSON.parse(sessionStorage.getItem('saussyAI_recent') || '[]'), messages: [], currentResults: null, resultsLoading: null, contactFormVisible: false, contactFormCommunity: '', contactFormSubmitted: false, loading: false, suggestLoading: false, error: null, debounceTimer: null, mode: 'landing', trayOpen: false, _trayDragY: 0, _trayDragging: false, init() { const saved = sessionStorage.getItem('saussyAI_messages'); if (saved) { try { const parsed = JSON.parse(saved); if (parsed && parsed.length) { parsed.forEach(m => { m.streaming = false; if (m.role === 'assistant' && !m.html && m.text) { m.html = this.escHtml(m.text); } }); this.messages = parsed; this.mode = 'chat'; } } catch(e) {} } document.addEventListener('gform_confirmation_loaded', () => { if (this.contactFormVisible) { this.contactFormSubmitted = true; } }); // When the mobile results tray opens, x-trap focuses the close X and // paints a :focus-visible ring. Redirect focus to the tray container // so keyboard users still see the ring when they Tab to close. this.$watch('trayOpen', value => { if (value) { setTimeout(() => { if (this.$refs.resultsTray) this.$refs.resultsTray.focus(); }, 0); } }); }, escHtml(str) { return String(str).replace(/&/g,'&').replace(//g,'>').replace(/\x22/g,'"'); }, parseMessageContent(rawText) { const segments = []; let plainText = ''; let remaining = rawText; while (remaining.length > 0) { const idx = remaining.indexOf('{{link:'); if (idx === -1) { segments.push({ type: 'text', content: remaining }); plainText += remaining; break; } if (idx > 0) { segments.push({ type: 'text', content: remaining.slice(0, idx) }); plainText += remaining.slice(0, idx); } const end = remaining.indexOf('}}', idx); if (end === -1) { segments.push({ type: 'text', content: remaining.slice(idx) }); plainText += remaining.slice(idx); break; } const inner = remaining.slice(idx + 7, end); const pipe = inner.indexOf('|'); const url = pipe !== -1 ? inner.slice(0, pipe) : inner; const display = pipe !== -1 ? inner.slice(pipe + 1) : inner; segments.push({ type: 'link', url, content: display }); plainText += display; remaining = remaining.slice(end + 2); } return { segments, plainText }; }, buildHtmlAtProgress(segments, progress) { let html = ''; let textPos = 0; for (const seg of segments) { if (seg.type === 'link') { if (textPos <= progress) { html += '
' + this.escHtml(seg.content) + '
'; } } else { if (textPos >= progress) break; const show = Math.min(seg.content.length, progress - textPos); html += this.escHtml(seg.content.slice(0, show)); textPos += seg.content.length; } } return html; }, async animateTypewriter(aiIdx, rawText) { return new Promise(resolve => { const parsed = this.parseMessageContent(this.phoneToLinks(rawText)); const msg = this.messages[aiIdx]; msg.text = parsed.plainText; const textLen = parsed.segments.filter(s => s.type === 'text').reduce((n, s) => n + s.content.length, 0); const speed = textLen < 50 ? 0.018 : textLen < 200 ? 0.012 : 0.009; const duration = Math.min(textLen * speed, 5); msg.html = '\u200B'; this.$nextTick(() => { const thread = this.$refs.chatThread; const row = thread ? thread.querySelector('.ai-chat-ai-row:last-child') : null; if (row && typeof gsap !== 'undefined') { gsap.fromTo(row, { opacity: 0, y: 8, scale: 0.98 }, { opacity: 1, y: 0, scale: 1, duration: 0.35, ease: 'power3.out' }); } if (typeof gsap !== 'undefined' && textLen > 0) { const obj = { n: 0 }; gsap.to(obj, { n: textLen, duration, ease: 'none', onUpdate: () => { msg.html = this.buildHtmlAtProgress(parsed.segments, Math.round(obj.n)); this.scrollChat(); }, onComplete: () => { msg.html = this.buildHtmlAtProgress(parsed.segments, Infinity); this.scrollChat(); resolve(); } }); } else { msg.html = this.buildHtmlAtProgress(parsed.segments, Infinity); this.scrollChat(); resolve(); } }); }); }, get hasResults() { const r = this.currentResults; return r && (r.communities?.length || r.listings?.length || r.floor_plans?.length); }, get resultsCount() { const r = this.currentResults; if (!r) return 0; return (r.communities ? r.communities.length : 0) + (r.listings ? r.listings.length : 0) + (r.floor_plans ? r.floor_plans.length : 0); }, phoneToLinks(text) { return text.replace(/(\(?\d{3}\)?[\s\-.]\d{3}[\s\-.]\d{4}|\(?\d{3}\)?[\s\-.]?\d{3}[\s\-.]?\d{4})/g, (match) => { const digits = match.replace(/\D/g, ''); if (digits.length !== 10 && digits.length !== 11) return match; return '{{link:tel:' + digits + '|' + match + '}}'; }); }, scrollChat() { this.$nextTick(() => { const el = this.$refs.chatThread; if (!el) return; const target = el.scrollHeight - el.clientHeight; const distFromBottom = target - el.scrollTop; if (distFromBottom > 200 && el.scrollTop > 0) return; if (typeof gsap !== 'undefined') { gsap.to(el, { scrollTop: target, duration: 0.45, ease: 'power2.inOut', overwrite: true }); } else { el.scrollTop = target; } }); }, async transitionToChat(q) { if (typeof gsap !== 'undefined' && this.$refs.landingPanel) { await new Promise(resolve => { gsap.to(this.$refs.landingPanel, { opacity: 0, y: 12, duration: 0.35, ease: 'power2.in', onComplete: resolve }); }); } this.mode = 'chat'; await this.$nextTick(); if (typeof gsap !== 'undefined') { const left = this.$refs.chatPanelEl; const right = this.$refs.resultsPanel; const tl = gsap.timeline(); if (left) tl.fromTo(left, { opacity: 0, x: -15 }, { opacity: 1, x: 0, duration: 0.5, ease: 'power3.out' }); if (right) tl.fromTo(right, { opacity: 0, x: 15 }, { opacity: 1, x: 0, duration: 0.5, ease: 'power3.out' }, left ? '-=0.4' : 0); } if (this.$refs.chatInput) this.$refs.chatInput.focus(); await this.sendMessage(q); }, async sendMessage(q) { const text = (q !== undefined ? q : this.query).trim(); if (!text || this.loading) return; this.query = ''; if (this.$refs.chatInput) this.$refs.chatInput.style.height = 'auto'; this.recentQueries = [text, ...this.recentQueries.filter(r => r !== text)].slice(0, 5); sessionStorage.setItem('saussyAI_recent', JSON.stringify(this.recentQueries)); const history = this.messages .filter(m => (m.role === 'user' || m.role === 'assistant') && m.text) .map(m => ({ role: m.role, content: m.text })); console.log('[AI Chat] sending — turns in history:', history.length, history.map(m => m.role + ':' + m.content.slice(0,40))); this.messages.push({ role: 'user', text, html: '' }); this.loading = true; this.error = null; this.resultsLoading = null; this.messages.push({ role: 'assistant', text: '', html: '', streaming: true }); let aiIdx = this.messages.length - 1; let responseBuffer = ''; sessionStorage.setItem('saussyAI_messages', JSON.stringify(this.messages)); this.scrollChat(); let timedOut = false; const timeout = setTimeout(() => { timedOut = true; this.loading = false; this.resultsLoading = null; if (this.messages[aiIdx]) { this.messages[aiIdx].streaming = false; if (!this.messages[aiIdx].html) { const msg = 'That one took a bit too long on our end. Try asking again, or give us a call \u2014 we\u2019re happy to help.'; this.messages[aiIdx].text = msg; this.messages[aiIdx].html = this.escHtml(msg); } } this.scrollChat(); }, 30000); try { const res = await fetch(saussyAI.chatUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': saussyAI.nonce }, body: JSON.stringify({ message: text, history }), }); if (!res.ok) throw new Error('Request failed'); const reader = res.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done || timedOut) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (!line.startsWith('data: ')) continue; try { const ev = JSON.parse(line.slice(6)); if (ev.type === 'text_delta') { clearTimeout(timeout); responseBuffer += ev.text; } else if (ev.type === 'segment_break') { if (responseBuffer.trim()) { await this.animateTypewriter(aiIdx, responseBuffer); this.messages[aiIdx].streaming = false; } responseBuffer = ''; this.messages.push({ role: 'assistant', text: '', html: '', streaming: true }); aiIdx = this.messages.length - 1; this.scrollChat(); } else if (ev.type === 'loading') { this.resultsLoading = ev.message; } else if (ev.type === 'results') { this.currentResults = ev; this.resultsLoading = null; this.$nextTick(() => this.animateResultCards()); } else if (ev.type === 'contact_form') { this.showContactForm(ev.community || ''); } else if (ev.type === 'done') { clearTimeout(timeout); if (responseBuffer.trim()) { await this.animateTypewriter(aiIdx, responseBuffer); } else if (!this.messages[aiIdx].html) { this.messages.splice(aiIdx, 1); } responseBuffer = ''; if (this.messages[aiIdx]) this.messages[aiIdx].streaming = false; this.loading = false; sessionStorage.setItem('saussyAI_messages', JSON.stringify(this.messages)); this.scrollChat(); } else if (ev.type === 'error') { clearTimeout(timeout); console.error('[AI Chat] error event:', ev); const errMsg = ev.message || 'Something went wrong. Please try again.'; this.messages[aiIdx].text = errMsg; this.messages[aiIdx].html = this.escHtml(errMsg); this.messages[aiIdx].streaming = false; this.loading = false; this.resultsLoading = null; this.scrollChat(); } } catch(e) {} } } } catch(e) { clearTimeout(timeout); if (!timedOut) { if (this.messages[aiIdx]) { const fallback = this.messages[aiIdx].html ? '' : 'Something went wrong. Please try again.'; if (fallback) { this.messages[aiIdx].text = fallback; this.messages[aiIdx].html = this.escHtml(fallback); } this.messages[aiIdx].streaming = false; } this.loading = false; this.resultsLoading = null; this.scrollChat(); } } clearTimeout(timeout); if (!timedOut && this.loading) { this.loading = false; if (this.messages[aiIdx]) this.messages[aiIdx].streaming = false; sessionStorage.setItem('saussyAI_messages', JSON.stringify(this.messages)); this.scrollChat(); } }, showContactForm(community) { this.contactFormCommunity = community; this.contactFormVisible = true; this.contactFormSubmitted = false; this.$nextTick(() => { // Move the pre-rendered Gravity Form node into the correct slot. // Prefer desktop slot unless the mobile tray is explicitly open. const source = document.getElementById('ai-contact-form-prerender'); const slots = document.querySelectorAll('.ai-contact-form-slot'); let target = null; if (this.trayOpen && slots.length > 1) { target = slots[1]; } else { target = slots[0] || null; } if (source && target && !target.hasChildNodes()) { while (source.firstChild) { target.appendChild(source.firstChild); } } // Pre-fill the hidden Community of Interest field (Gravity Forms Field ID 7). // Only overwrite if we have a community from chat context — otherwise // let the PHP gform_pre_render default (set on single-community pages) stand. if (community && community.trim() && target) { const field = target.querySelector('input[name=input_7]'); if (field) field.value = community.trim(); } // Scroll the card into view. Small delay lets Alpine's x-show transition settle. const card = document.getElementById('ai-contact-form-card') || (target ? target.closest('.ai-contact-form-card') : null); if (card) { setTimeout(() => card.scrollIntoView({ behavior: 'smooth', block: 'start' }), 50); } }); }, clearChat() { const reset = () => { this.messages = []; this.currentResults = null; this.resultsLoading = null; this.contactFormVisible = false; this.contactFormCommunity = ''; this.contactFormSubmitted = false; this.query = ''; this.chips = this.defaultChips; this.chipsVisible = true; this.mode = 'landing'; sessionStorage.removeItem('saussyAI_messages'); }; if (typeof gsap !== 'undefined') { const right = this.$refs.chatPanelEl; const left = this.$refs.resultsPanel; const tl = gsap.timeline({ onComplete: () => { reset(); this.$nextTick(() => { if (this.$refs.landingPanel) gsap.fromTo(this.$refs.landingPanel, { opacity: 0, y: 20 }, { opacity: 1, y: 0, duration: 0.35, ease: 'power2.out' }); if (this.$refs.landingInput) this.$refs.landingInput.focus(); }); }}); if (right) tl.to(right, { opacity: 0, x: -15, duration: 0.28, ease: 'power2.in' }); if (left) tl.to(left, { opacity: 0, x: 15, duration: 0.28, ease: 'power2.in' }, '-=0.18'); } else { reset(); this.$nextTick(() => this.$refs.landingInput && this.$refs.landingInput.focus()); } }, openTray() { this.trayOpen = true; this.$nextTick(() => { const tray = this.$refs.resultsTray; if (tray && typeof gsap !== 'undefined') { gsap.fromTo(tray, { y: '100%' }, { y: '0%', duration: 0.4, ease: 'power3.out' }); } }); }, closeTray() { const tray = this.$refs.resultsTray; if (tray && typeof gsap !== 'undefined') { gsap.to(tray, { y: '100%', duration: 0.3, ease: 'power2.in', onComplete: () => { this.trayOpen = false; } }); } else { this.trayOpen = false; } }, animateResultCards() { if (typeof gsap === 'undefined' || !this.$refs.resultsPanel) return; const cards = this.$refs.resultsPanel.querySelectorAll('.ai-search-result-item'); if (!cards.length) return; gsap.fromTo(cards, { opacity: 0, y: 12 }, { opacity: 1, y: 0, duration: 0.35, ease: 'power3.out', stagger: 0.04 } ); }, onChipClick(chip, event) { if (typeof gsap !== 'undefined' && event && event.currentTarget) { const btn = event.currentTarget; gsap.to(btn, { scale: 0.96, opacity: 0.7, duration: 0.1, ease: 'power2.in', onComplete: () => this.transitionToChat(chip) }); } else { this.transitionToChat(chip); } }, resizeTextarea() { const el = this.$refs.chatInput; if (!el) return; el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 100) + 'px'; }, onLandingInput() { clearTimeout(this.debounceTimer); if (this.query.length < 3) return; this.debounceTimer = setTimeout(() => this.fetchSuggestions(), 500); }, async fetchSuggestions() { this.suggestLoading = true; try { const res = await fetch(saussyAI.restUrl + 'suggest', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': saussyAI.nonce }, body: JSON.stringify({ query: this.query, recentSearches: this.recentQueries }), }); const data = await res.json(); if (data.suggestions && data.suggestions.length) this.chips = data.suggestions; } catch(e) {} this.suggestLoading = false; }, }">
What can we help you find?
Home Assistant
New chat
View Results
Results
Ask me to find homes, communities, or floor plans — results will appear here.
Communities
Coming Soon
—
Listings
—
—
Floor Plans
—
—
Get In Touch
Thanks — someone from our team will be in touch soon.
80) { $el.style.transform = ''; closeTray(); } else { gsap && gsap.to($el, { y: 0, duration: 0.3, ease: 'power3.out', onComplete: () => { $el.style.transform = ''; } }); } }">
Results
Communities
—
Listings
—
—
Floor Plans
—
Get In Touch
Thanks — someone from our team will be in touch soon.
First and Last Name
(Required)
Phone
(Required)
Email
(Required)
Additional Comments
(Required)
Contact Saussy Burbank
Sales
Interested Homebuyers
Contact
Homeowners
Homeowner Care
Contact
Careers
Employment & Subcontracts
Contact