!function(d){ initVF(); function initVF(){ const fpCSS=d.createElement('link');fpCSS.rel='stylesheet';fpCSS.href='https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css';d.head.appendChild(fpCSS); const fpJS=d.createElement('script');fpJS.src='https://cdn.jsdelivr.net/npm/flatpickr';d.head.appendChild(fpJS); const vf=d.createElement('script');vf.src='https://cdn.voiceflow.com/widget-next/bundle.mjs';vf.type='text/javascript'; vf.onload=()=>{ let V; const MZ=(()=>{const BP=1024;let m,o,c=0;const en=()=>{if(innerWidth>BP)return;if(!m){m=d.querySelector('meta[name="viewport"]');if(!m){m=d.createElement('meta');m.name='viewport';d.head.appendChild(m)}o=m.getAttribute('content')||'width=device-width, initial-scale=1'}c++;m.setAttribute('content','width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover')},de=()=>{if(innerWidth>BP)return;setTimeout(()=>{c=Math.max(0,c-1);if(m&&c===0&&o!=null)m.setAttribute('content',o)},50)},bl=()=>{try{d.activeElement?.blur?.()}catch(e){}};return{enable:en,disable:de,blurActive:bl}})(); const CH=(()=>{const A='data-vf-hide-composer',CSS='.vfrc-input-container,.vfrc-footer,.vfrc-composer,.vfrc-message-composer,.vfrc-chat-input-container{display:none!important}';let hid=false,roots=new Set,obs;const all=()=>{const res=new Set([d]),q=[d],seen=new Set(q);while(q.length){const r=q.shift(),nodes=(r instanceof Document||r instanceof ShadowRoot)?r.querySelectorAll('*'):[];nodes.forEach(el=>{if(el&&el.shadowRoot&&!seen.has(el.shadowRoot)){seen.add(el.shadowRoot);res.add(el.shadowRoot);q.push(el.shadowRoot)}})}return[...res]},add=r=>{if(!r||!(r instanceof Document||r instanceof ShadowRoot))return;if(r.querySelector(`style[${A}]`))return;const s=d.createElement('style');s.setAttribute(A,'1');s.textContent=CSS;try{r.appendChild(s);roots.add(r)}catch(e){}},rem=r=>{const s=r.querySelector?.(`style[${A}]`);if(s)try{s.remove()}catch(e){}roots.delete(r)};return{hide(){hid=true;all().forEach(add);if(!obs){obs=new MutationObserver(()=>{if(hid)all().forEach(add)});obs.observe(d.documentElement,{childList:true,subtree:true})}},show(){hid=false;roots.forEach(rem);roots.clear();obs?.disconnect?.();obs=null}}})(); const AH=(()=>{const SEL='.vfrc-avatar,[class*="vfrc-avatar"]',obsMap=new WeakMap(),tidy=c=>{c.querySelectorAll(SEL).forEach(a=>{a.style.display='none';a.style.visibility='hidden';a.style.width='0';a.style.margin='0';a.style.padding='0'})},find=n=>{let e=n;for(let i=0;i<12&&e;i++){if(e.querySelector?.(SEL))return e;e=e.parentElement||(e.getRootNode&&e.getRootNode().host)||null}return null},hideFor=node=>{const c=find(node);if(!c)return;tidy(c);if(obsMap.has(c))return;const mo=new MutationObserver(muts=>{let changed=false;for(const m of muts){if(m.addedNodes&&m.addedNodes.length)changed=true}if(changed)tidy(c)});mo.observe(c,{childList:true,subtree:true});obsMap.set(c,mo);setTimeout(()=>{mo.disconnect();obsMap.delete(c)},1200)};return{hideFor}})(); const SI=(()=>({inject:(css,attr)=>{const roots=[],q=[d],seen=new Set(q);while(q.length){const r=q.shift();roots.push(r);const nodes=(r instanceof Document||r instanceof ShadowRoot)?r.querySelectorAll('*'):[];nodes.forEach(el=>{if(el&&el.shadowRoot&&!seen.has(el.shadowRoot)){seen.add(el.shadowRoot);q.push(el.shadowRoot)}})}roots.forEach(r=>{if(!r||!(r instanceof Document||r instanceof ShadowRoot))return;if(r.querySelector(`style[${attr}]`))return;const s=d.createElement('style');s.setAttribute(attr,'1');s.textContent=css;try{r.appendChild(s)}catch(e){}})}}))(); SI.inject('.vf-wide-card{width:100%!important;max-width:100%!important;display:block!important}.vf-wide-card>.vf-form-card{width:100%!important;max-width:100%!important}.vf-savefx-success{background:#00bf63!important;transition:background .22s ease,transform .22s ease,opacity .3s ease}.vf-savefx-bounce{transform:translateY(-1px) scale(1.02)}.vf-savefx-hide{opacity:0;transform:translateY(-6px) scale(.98);pointer-events:none;transition:opacity .28s ease,transform .28s ease}.vf-progress-card{padding:16px;border:1px solid #e5e7eb;border-radius:8px;margin:12px 0;background:#fafafa;width:100%}.vf-progress-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.vf-progress-title{font-weight:700;color:#111827;font-size:14px}.vf-progress-sub{color:#6b7280;font-size:12px}.vf-progress-track{position:relative;height:10px;background:#e5e7eb;border-radius:9999px;overflow:hidden}.vf-progress-fill{position:absolute;inset:0 auto 0 0;width:0;height:100%;border-radius:9999px;background:#165dfb;transition:width .7s cubic-bezier(.22,1,.36,1)}.vf-step-dots{display:flex;justify-content:space-between;margin-top:10px}.vf-step-dot{width:22px;height:22px;border-radius:50%;background:#e5e7eb;display:flex;align-items:center;justify-content:center;color:#fff;font-size:12px;box-shadow:inset 0 0 0 2px #fff}.vf-step-dot.complete{background:#22c55e}.vf-step-dot.current{background:#3b82f6;animation:vfPulse 1.2s ease-in-out infinite}@keyframes vfPulse{0%{transform:scale(1);box-shadow:0 0 0 0 rgba(59,130,246,.55)}70%{box-shadow:0 0 0 10px rgba(59,130,246,0)}100%{transform:scale(1)}}','data-vf-styles'); SI.inject('.vf-loader-card{padding:16px;border:1px solid #e5e7eb;border-radius:12px;margin:12px 0;background:#f8fafc;width:100%}.vf-loader-wrap{display:flex;align-items:center;gap:14px}.vf-spinner{width:46px;height:46px;border-radius:9999px;border:4px solid #dbeafe;border-top-color:#0551fb;animation:vfSpin 1s linear infinite;flex:0 0 auto}@keyframes vfSpin{to{transform:rotate(360deg)}}.vf-loader-text{display:flex;flex-direction:column}.vf-loader-title{font-weight:800;color:#0f172a;font-size:16px}.vf-loader-sub{color:#334155;font-size:14px;margin-top:2px}.vf-dots{display:inline-flex;gap:4px;margin-top:6px}.vf-dot{width:6px;height:6px;border-radius:9999px;background:#0551fb;opacity:.35;animation:vfBounce 1s ease-in-out infinite}.vf-dot:nth-child(2){animation-delay:.15s}.vf-dot:nth-child(3){animation-delay:.3s}@keyframes vfBounce{0%,80%,100%{transform:translateY(0);opacity:.35}40%{transform:translateY(-6px);opacity:1}}','data-vf-loader-styles'); SI.inject('[class*="vfrc-message--extension-"]{background:transparent!important;padding:0!important;border-radius:0!important}','data-vf-unbubble'); SI.inject('.vfrc-chat--dialog{flex:1 1 auto!important;min-height:0!important}','data-vf-fill'); function vfClosestWrapper(n){let e=n,h=0;while(e&&h<20){const c=(e.className||'')+'';if(e.getAttribute?.('role')==='listitem'||/vfrc-.*(message|bubble|response|chat|row)/i.test(c))return e;e=e.parentNode||(e.getRootNode&&e.getRootNode().host)||null;h++}return null} function removeIfEmptyWrapper(el){try{if(!el)return;if(el.childElementCount===0&&(el.textContent||'').trim()===''){const w=vfClosestWrapper(el)||el.parentNode;if(w)try{w.remove()}catch(e){}}}catch(e){}} function forceRemoveWrapper(el){ try{ if(!el)return; let current=el; for(let i=0;i<15;i++){ if(!current)break; const classList=current.className||''; if(classList.includes('vfrc-system-response')){ current.style.display='none'; current.style.height='0'; current.style.margin='0'; current.style.padding='0'; current.style.overflow='hidden'; break; } current=current.parentElement||(current.getRootNode&¤t.getRootNode().host)||null; } }catch(e){} } const __VF_LOADERS=new Set(),__vfParsePayload=t=>{try{return typeof t?.payload==='string'?JSON.parse(t.payload):(t?.payload||{})}catch(_){return{}}}; function __vfRemoveAllLoaders(){const now=Date.now();__VF_LOADERS.forEach(x=>{const{node,contentEl,started,minMs}=x,delay=Math.max(0,(started+minMs)-now);setTimeout(()=>{try{node?.remove()}catch(e){}removeIfEmptyWrapper(contentEl);try{__VF_LOADERS.delete(x)}catch(e){}if(__VF_LOADERS.size===0){CH.show();MZ.disable()}},delay)})} /* ==================== TRACKING UTILITY ==================== */ const TRACK = (() => { const sessions = new Map(); function trackEvent(name, params) { if (typeof gtag !== 'function') return; try { gtag('event', name, params); } catch (e) {} } function trackStart(key) { sessions.set(key, Date.now()); } function trackComplete(key, extra = {}) { const start = sessions.get(key); if (!start) return; const dur = Math.round((Date.now() - start) / 1000); trackEvent(`${key}_complete`, { ...extra, duration_seconds: dur }); } return { trackEvent, trackStart, trackComplete, sessions }; })(); /* ==================== TRACKING EXTENSIONS ==================== */ const mkStartTracker = (name, key, event, params) => ({ name, type: 'response', match: ({trace}) => trace?.type === name || trace?.payload?.name === name, render: ({element}) => { TRACK.trackStart(key); TRACK.trackEvent(event, params); forceRemoveWrapper(element); return () => {}; } }); const mkCompleteTracker = (name, key, extra) => ({ name, type: 'response', match: ({trace}) => trace?.type === name || trace?.payload?.name === name, render: ({trace, element}) => { TRACK.trackComplete(key, extra(__vfParsePayload(trace))); forceRemoveWrapper(element); return () => {}; } }); const ConversationStartTracker = mkStartTracker('conversationStart', 'conversation', 'conversation_start', {event_category: 'engagement', event_label: 'User opened Voiceflow chatbot'}); const JobDiagnosisStartTracker = mkStartTracker('jobDiagnoseStart', 'job_diagnosis', 'job_diagnosis_start', {event_category: 'funnel'}); const JobDiagnosisCompleteTracker = mkCompleteTracker('jobDiagnoseComplete', 'job_diagnosis', p => ({job_type: p.job_diagnosed || 'unknown'})); const LocationCaptureStartTracker = mkStartTracker('locationCaptureStart', 'location_capture', 'location_capture_start', {event_category: 'funnel'}); const LocationCaptureCompleteTracker = mkCompleteTracker('locationCaptureCompleted', 'location_capture', p => ({suburb: p.suburb || 'unknown'})); const CheckForProsStartTracker = mkStartTracker('checkForProsStart', 'check_for_pros', 'check_for_pros_start', {event_category: 'funnel'}); const CheckForProsCompleteTracker = mkCompleteTracker('checkForProsCompleted', 'check_for_pros', p => ({pros_found: p.prosFoundCount || 0})); const LO1={name:'ext_loading_on_first',type:'response',match:({trace})=>trace?.type==='ext_loading_on_first'||trace?.payload?.name==='ext_loading_on_first',render:({trace,element})=>{const p=__vfParsePayload(trace),label=p.label||'Please wait a moment...',minMs=Math.max(1000,Number(p.min_ms||0));CH.hide();MZ.enable();const w=d.createElement('div');w.className='vf-anti-zoom vf-wide-card vf-loader-card';w.setAttribute('role','status');w.setAttribute('aria-live','polite');w.setAttribute('aria-busy','true');w.innerHTML=`
Looking for nearby Pros
${label}
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);__VF_LOADERS.add({node:w,contentEl:element,wrapper:vfClosestWrapper(element),started:Date.now(),minMs});return()=>{}}}; const LF1={name:'ext_loading_off_first',type:'response',match:({trace})=>trace?.type==='ext_loading_off_first'||trace?.payload?.name==='ext_loading_off_first',render:()=>{__vfRemoveAllLoaders();return()=>{}}}; const SF=(()=>{function hideBoth(a,b){if(a)a.classList.add('vf-savefx-hide');if(b)b.classList.add('vf-savefx-hide');setTimeout(()=>{if(a)a.style.display='none';if(b)b.style.display='none'},320)}return{celebrate(btn,sib){if(!btn)return;btn.classList.add('vf-savefx-success','vf-savefx-bounce');setTimeout(()=>btn.classList.remove('vf-savefx-bounce'),240);setTimeout(()=>hideBoth(btn,sib),1e3)},hideBoth}})(); const stretch=(el,wrap)=>{try{el.style.width=el.style.maxWidth='100%';el.style.alignSelf='stretch'}catch(e){}try{wrap.style.width=wrap.style.maxWidth='100%'}catch(e){}let p=el.parentElement;for(let i=0;i<4&&p;i++){try{if(p.style){p.style.width=p.style.maxWidth='100%';p.style.flex='1 1 100%';p.style.alignSelf='stretch'}}catch(e){}p=p.parentElement||(p.getRootNode&&p.getRootNode().host)||null}}; const setVar=(k,v)=>{try{V?.setVariable?.(k,v)}catch(e){}}; const makeShell=({label,description,htmlField,submitText='Save',errorText='Please fix the error above.'})=>{const w=d.createElement('div');w.className='vf-wide-card';w.innerHTML=`
${description?`

${description}

`:''}${htmlField}
`;if(!d.getElementById('vf-anti-zoom-style')){const s=d.createElement('style');s.id='vf-anti-zoom-style';s.textContent='@media(max-width:1024px){.vf-anti-zoom input,.vf-anti-zoom textarea,.vf-anti-zoom select{font-size:16px!important;-webkit-text-size-adjust:100%}}';d.head.appendChild(s)}return w}; /* -------------------- Location Picker -------------------- */ const LP = { name: 'ext_location_picker', type: 'response', match: ({trace}) => trace?.type === 'ext_location_picker' || trace?.payload?.name === 'ext_location_picker', render: ({element}) => { CH.hide(); MZ.enable(); const shell = makeShell({ label: 'Where do you need the work done?', description: 'Please enter your location below.', submitText: 'Confirm Location', errorText: 'Please select a location before continuing.', htmlField: '' + '
' + '' + '' + '
' + '
' + '
Type at least 3 characters to see suggestions
' + '
' + '
Selected Location:
' + '
' + '
' + '
' + '
' + 'Getting your location...' + '
' }); element.appendChild(shell); stretch(element, shell); AH.hideFor(shell); const input = shell.querySelector('.vf-location-input'); const inputIcon = shell.querySelector('.vf-input-icon'); const dropdownContainer = shell.querySelector('.vf-dropdown-container'); const inputHelper = shell.querySelector('.vf-input-helper'); const selectedDiv = shell.querySelector('.vf-selected-location'); const selectedAddress = shell.querySelector('.vf-selected-address'); const loading = shell.querySelector('.vf-location-loading'); const error = shell.querySelector('.vf-error'); const save = shell.querySelector('.vf-submit'); const autocompleteWrapper = shell.querySelector('.vf-autocomplete-wrapper'); const label = shell.querySelector('label'); const description = shell.querySelector('.vf-desc'); const fallbackLink = d.createElement('div'); fallbackLink.className = 'vf-location-fallback'; fallbackLink.innerHTML = 'Issues providing your location? Click here...'; save.insertAdjacentElement('afterend', fallbackLink); const fallbackAnchor = fallbackLink.querySelector('a'); let selectedLocation = null; let autocompleteService = null; let placesService = null; let sessionToken = null; let currentPredictions = []; save.disabled = true; save.style.background = '#9ca3af'; save.style.cursor = 'not-allowed'; function enableSubmit() { save.disabled = false; save.style.background = '#0551fb'; save.style.cursor = 'pointer'; } function showLoading(text) { loading.querySelector('span').textContent = text || 'Getting your location...'; loading.style.display = 'flex'; } function hideLoading() { loading.style.display = 'none'; } function showError(message) { error.textContent = message; error.style.display = 'block'; hideLoading(); } function hideError() { error.style.display = 'none'; } function showSelected(location) { selectedLocation = location; selectedAddress.textContent = location.formattedAddress; selectedDiv.classList.add('show'); inputHelper.classList.remove('show'); dropdownContainer.classList.remove('show'); enableSubmit(); hideError(); hideLoading(); if (window.innerWidth <= 768) { input.style.display = 'none'; if (inputIcon) inputIcon.style.display = 'none'; autocompleteWrapper.style.marginBottom = '0'; } // Scoped scroll: only nudge the message dialog so the Confirm button is visible. // We deliberately DO NOT use save.scrollIntoView() — it scrolls the widget's // outer overflow:hidden wrapper (header + messages + composer), which clips the // header off the top and leaves a blank composer block that persists all chat. requestAnimationFrame(() => { requestAnimationFrame(() => { try { const up = el => el.parentElement || (el.getRootNode && el.getRootNode().host) || null; const isScrollable = el => { try { const cs = getComputedStyle(el); return /(auto|scroll)/.test(cs.overflowY) && el.scrollHeight > el.clientHeight + 4; } catch (e) { return false; } }; // Defensive: undo any clipping on non-scrolling ancestors (the wrapper bug). let n = up(save); for (let i = 0; i < 30 && n && n !== d.documentElement && n !== d.body; i++) { try { if (!isScrollable(n) && n.scrollTop) n.scrollTop = 0; } catch (e) {} n = up(n); } // Find the real scroll container: prefer .vfrc-chat--dialog if it scrolls, // else the nearest auto/scroll ancestor. Never the page or body. let dialog = null, node = save; for (let i = 0; i < 30 && node; i++) { if (node.classList && node.classList.contains('vfrc-chat--dialog')) { dialog = node; break; } node = up(node); } if (!dialog || !isScrollable(dialog)) { let m = up(save); for (let i = 0; i < 30 && m && m !== d.documentElement && m !== d.body; i++) { if (isScrollable(m)) { dialog = m; break; } m = up(m); } } if (!dialog || !isScrollable(dialog)) return; // Scroll only that container, just enough to reveal the button + margin. const cRect = dialog.getBoundingClientRect(); const bRect = save.getBoundingClientRect(); const delta = (bRect.bottom - cRect.bottom) + 24; if (delta > 0) dialog.scrollTo({ top: dialog.scrollTop + delta, behavior: 'smooth' }); } catch (e) {} }); }); } function extractSuburb(addressComponents) { const suburbTypes = ['sublocality_level_1', 'locality', 'administrative_area_level_3']; for (const type of suburbTypes) { const component = addressComponents.find(comp => comp.types.includes(type)); if (component) return component.long_name; } return ''; } function renderPredictions(predictions) { dropdownContainer.innerHTML = ''; if (!predictions || predictions.length === 0) { dropdownContainer.classList.remove('show'); return; } const limitedPredictions = predictions.slice(0, 2); currentPredictions = limitedPredictions; limitedPredictions.forEach((prediction, index) => { const item = d.createElement('div'); item.className = 'vf-prediction-item'; item.style.cssText = 'padding:16px;cursor:pointer;border-bottom:1px solid #e5e7eb;display:flex;align-items:center;gap:12px;-webkit-tap-highlight-color:transparent;transition:background 0.15s ease'; const icon = d.createElement('div'); icon.style.cssText = 'width:20px;height:20px;flex-shrink:0;color:#6b7280'; icon.innerHTML = '📍'; item.appendChild(icon); const textContainer = d.createElement('div'); textContainer.style.cssText = 'flex:1;min-width:0'; const mainText = d.createElement('div'); mainText.style.cssText = 'font-weight:600;color:#111827;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'; mainText.textContent = prediction.structured_formatting.main_text; const secondaryText = d.createElement('div'); secondaryText.style.cssText = 'color:#6b7280;font-size:13px;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis'; secondaryText.textContent = prediction.structured_formatting.secondary_text; textContainer.appendChild(mainText); textContainer.appendChild(secondaryText); item.appendChild(textContainer); if (index === limitedPredictions.length - 1) { item.style.borderBottom = 'none'; } item.addEventListener('mouseenter', () => { item.style.background = '#eff6ff'; }); item.addEventListener('mouseleave', () => { item.style.background = 'white'; }); item.addEventListener('click', () => { selectPrediction(prediction); }); item.addEventListener('touchstart', () => { item.style.background = '#dbeafe'; }, { passive: true }); item.addEventListener('touchend', () => { item.style.background = '#eff6ff'; }, { passive: true }); dropdownContainer.appendChild(item); }); dropdownContainer.classList.add('show'); } function selectPrediction(prediction) { if (!placesService) return; showLoading('Getting location details...'); dropdownContainer.classList.remove('show'); placesService.getDetails( { placeId: prediction.place_id, fields: ['address_components', 'formatted_address', 'geometry', 'name'], sessionToken: sessionToken }, (place, status) => { hideLoading(); if (status === google.maps.places.PlacesServiceStatus.OK && place) { const location = { formattedAddress: place.formatted_address, suburb: extractSuburb(place.address_components || []), lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }; input.value = place.formatted_address; showSelected(location); sessionToken = new google.maps.places.AutocompleteSessionToken(); } else { showError('Could not get location details. Please try another address.'); } } ); } function loadGoogleMaps() { return new Promise((resolve, reject) => { if (window.google && window.google.maps) { resolve(); return; } const script = d.createElement('script'); script.src = 'https://maps.googleapis.com/maps/api/js?key=AIzaSyC51PJx9faiS3ptAxYuMfkYKKtTU00Tz-Q&libraries=places'; script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load Google Maps')); d.head.appendChild(script); }); } async function initializeGoogleMaps() { try { await loadGoogleMaps(); autocompleteService = new google.maps.places.AutocompleteService(); const placesDiv = d.createElement('div'); placesDiv.style.display = 'none'; d.body.appendChild(placesDiv); placesService = new google.maps.places.PlacesService(placesDiv); sessionToken = new google.maps.places.AutocompleteSessionToken(); setupAutocomplete(); } catch (err) { showError('Failed to load location services. Please refresh and try again.'); } } function setupAutocomplete() { let searchTimeout; input.addEventListener('input', () => { const value = input.value.trim(); clearTimeout(searchTimeout); if (value.length > 0 && value.length < 3) { inputHelper.textContent = `Type ${3 - value.length} more character${3 - value.length === 1 ? '' : 's'}`; inputHelper.classList.add('show'); dropdownContainer.classList.remove('show'); return; } else if (value.length === 0) { inputHelper.classList.remove('show'); dropdownContainer.classList.remove('show'); return; } inputHelper.textContent = 'Select an address from the suggestions below'; inputHelper.classList.add('show'); searchTimeout = setTimeout(() => { if (value.length >= 3) { performSearch(value); } }, 300); }); input.addEventListener('keydown', (e) => { if (e.key === ' ' || e.keyCode === 32) { e.stopPropagation(); } }); d.addEventListener('click', (e) => { if (!autocompleteWrapper.contains(e.target)) { dropdownContainer.classList.remove('show'); } }); } function performSearch(query) { if (!autocompleteService) return; autocompleteService.getPlacePredictions( { input: query, componentRestrictions: { country: 'za' }, types: ['address'], sessionToken: sessionToken }, (predictions, status) => { if (status === google.maps.places.PlacesServiceStatus.OK && predictions) { renderPredictions(predictions); } else if (status === google.maps.places.PlacesServiceStatus.ZERO_RESULTS) { dropdownContainer.innerHTML = '
No addresses found. Try a different search.
'; dropdownContainer.classList.add('show'); } else { dropdownContainer.classList.remove('show'); } } ); } fallbackAnchor.addEventListener('click', (e) => { e.preventDefault(); V.interact({ type: 'captured', payload: { locationFallback: true } }); MZ.blurActive(); CH.show(); MZ.disable(); }); save.onclick = () => { if (!selectedLocation) { showError('Please select a location before continuing.'); return; } hideError(); SF.celebrate(save); setTimeout(() => { if (label) label.style.display = 'none'; if (description) description.style.display = 'none'; if (autocompleteWrapper) autocompleteWrapper.style.display = 'none'; if (loading) loading.style.display = 'none'; if (error) error.style.display = 'none'; if (save) save.style.display = 'none'; if (fallbackLink) fallbackLink.style.display = 'none'; if (inputHelper) inputHelper.style.display = 'none'; }, 1000); setVar('formattedAddress', selectedLocation.formattedAddress); setVar('suburb', selectedLocation.suburb); setVar('googleLat', selectedLocation.lat.toString()); setVar('googleLong', selectedLocation.lng.toString()); MZ.blurActive(); CH.show(); MZ.disable(); V.interact({ type: 'captured', payload: { formattedAddress: selectedLocation.formattedAddress, suburb: selectedLocation.suburb, latitude: selectedLocation.lat, longitude: selectedLocation.lng, locationConfirmed: true } }); }; initializeGoogleMaps(); return () => { CH.show(); MZ.disable(); }; } }; /* -------------------- Calendar -------------------- */ const CE = { name: 'calendar-picker', type: 'response', match: ({trace}) => trace?.type === 'calendar' || trace?.payload?.name === 'calendar-picker', render: ({element}) => { CH.hide(); MZ.enable(); const shell = makeShell({ label: 'When would you like the work to be done?', description: '', submitText: 'Confirm date', errorText: 'Please pick a date before confirming.', htmlField: `
Pick a date, then confirm to continue.
` }); element.appendChild(shell); stretch(element, shell); AH.hideFor(shell); const calHost = shell.querySelector('.vf-calendar'); const err = shell.querySelector('.vf-error'); const save = shell.querySelector('.vf-submit'); const help = shell.querySelector('.vf-help'); const lbl = shell.querySelector('label'); if (lbl) { lbl.style.fontSize = '18px'; lbl.style.lineHeight = '1.35'; lbl.style.marginBottom = '14px'; } save.disabled = true; const picked = d.createElement('div'); picked.className = 'vf-picked'; picked.innerHTML = 'Selected date'; save.insertAdjacentElement('beforebegin', picked); const pickedVal = picked.querySelector('.vf-picked-val'); const fmtNice = iso => { try { const [y, m, dd] = iso.split('-').map(Number), dt = new Date(y, m - 1, dd); return dt.toLocaleDateString('en-GB', {weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'}); } catch { return iso; } }; const now = new Date, today = new Date(now.getFullYear(), now.getMonth(), now.getDate()), max = new Date(today.getFullYear() + 1, 11, 31); let selectedISO = null; const fp = flatpickr(calHost, { inline: true, appendTo: calHost, monthSelectorType: 'static', minDate: today, maxDate: max, showMonths: 1, disableMobile: true, dateFormat: 'Y-m-d', defaultDate: null, onChange: (_d, s) => { selectedISO = s; if (s) { save.disabled = false; err.style.display = 'none'; if (help) help.style.display = 'none'; picked.classList.add('show'); pickedVal.textContent = fmtNice(s); } } }); save.onclick = () => { if (!selectedISO) { err.style.display = 'block'; return; } err.style.display = 'none'; SF.celebrate(save); ['label', '.vf-desc', '.vf-cal-wrap'].forEach(s => { const n = shell.querySelector(s); if (n) n.style.display = 'none'; }); picked.classList.add('show'); setVar('date', selectedISO); MZ.blurActive(); CH.show(); MZ.disable(); V.interact({ type: 'captured', payload: { date: selectedISO, urgent: false } }); }; return () => { CH.show(); MZ.disable(); }; } }; /* -------------------- Intent Picker -------------------- */ const IP = { name: 'ext_Intent', type: 'response', match: ({trace}) => trace?.type === 'ext_Intent' || trace?.payload?.name === 'ext_Intent', render: ({element}) => { CH.hide(); MZ.enable(); const shell = d.createElement('div'); shell.className = 'vf-wide-card'; shell.innerHTML = `
You selected:
How can I help you today?
🔧
Get a plumber
Get an Electrician
Jess
Ask Jess
Ask Jess anything about Improving, Maintaining and Protecting your home
We currently only support plumbing and electrical services in Gauteng and Cape Town areas. We'll be expanding soon 🚀.
`; element.appendChild(shell); stretch(element, shell); AH.hideFor(shell); const cards = shell.querySelectorAll('.vf-intent-card'); const header = shell.querySelector('.vf-intent-header'); const grid = shell.querySelector('.vf-intent-grid'); const disclaimer = shell.querySelector('.vf-intent-disclaimer'); const selectedView = shell.querySelector('.vf-intent-selected-view'); const selectedIcon = shell.querySelector('.vf-intent-selected-icon'); const selectedValue = shell.querySelector('.vf-intent-selected-value'); const formCard = shell.querySelector('.vf-form-card'); cards.forEach(card => { card.onclick = () => { const intent = card.getAttribute('data-intent'); const label = card.getAttribute('data-label'); const icon = card.querySelector('.vf-intent-icon').innerHTML; cards.forEach(c => { c.style.pointerEvents = 'none'; c.style.opacity = '0.4'; }); card.classList.add('selected'); card.style.opacity = '1'; setVar('mainIntent', intent); setTimeout(() => { header.style.opacity = '0'; grid.style.opacity = '0'; disclaimer.style.opacity = '0'; setTimeout(() => { header.style.display = 'none'; grid.style.display = 'none'; disclaimer.style.display = 'none'; selectedIcon.innerHTML = icon; selectedValue.textContent = label; selectedView.style.display = 'flex'; formCard.style.padding = '12px'; requestAnimationFrame(() => { selectedView.style.opacity = '0'; selectedView.style.transform = 'translateY(-10px)'; requestAnimationFrame(() => { selectedView.style.transition = 'all 0.3s ease'; selectedView.style.opacity = '1'; selectedView.style.transform = 'translateY(0)'; }); }); }, 300); }, 400); MZ.blurActive(); CH.show(); MZ.disable(); V.interact({ type: 'captured', payload: { mainIntent: intent } }); }; }); return () => { CH.show(); MZ.disable(); }; } }; /* -------------------- Handover to PWA (one-way gate) -------------------- */ const HO = { name: 'ext_handover', type: 'response', match: ({trace}) => trace?.type === 'ext_handover' || trace?.payload?.name === 'ext_handover', render: ({trace, element}) => { CH.hide(); MZ.enable(); const p = (() => { try { return typeof trace?.payload === 'string' ? JSON.parse(trace.payload) : (trace?.payload || {}); } catch (_) { return {}; } })(); let userId = p.user_id || p.userId || ''; let service = p.service || ''; let location = p.location || ''; let transcriptId = p.vf_transcript_id || p.transcriptId || ''; try { if (!userId && V?.getVariable) userId = V.getVariable('user_id') || ''; if (!transcriptId && V?.getVariable) transcriptId = V.getVariable('vf_transcript_id') || ''; } catch (e) {} const base = 'https://marketplace.dev.kandua.com/'; const handoverUrl = `${base}?conversation=${encodeURIComponent(userId)}&redirect=book&service=${encodeURIComponent(service)}&location=${encodeURIComponent(location)}`; const shell = d.createElement('div'); shell.className = 'vf-wide-card'; shell.innerHTML = `
We've found top-rated pros near you
One last step to complete your booking
Sign in or create your account to complete your booking and manage it all in one place.
Complete booking
Takes a minute — we'll just need your name, number and any extra details or photos.
✅ Your booking has moved to the secure portal. Please complete it there — this conversation is now closed.
`; element.appendChild(shell); stretch(element, shell); AH.hideFor(shell); const btn = shell.querySelector('.vf-handover-btn'); const locked = shell.querySelector('.vf-handover-locked'); const czTrack = shell.querySelector('.vf-ho-track'); const czDots = shell.querySelector('.vf-ho-dots'); let czTimer = null, czResume = null; const czStop = () => { if (czTimer) { clearInterval(czTimer); czTimer = null; } }; if (czTrack && czDots) { const slides = czTrack.querySelectorAll('.vf-ho-slide'); const n = slides.length; let active = 0, raf = null; const drag = { on: false, x0: 0, sl0: 0 }; for (let i = 0; i < n; i++) { const b = d.createElement('button'); b.className = 'vf-ho-dot' + (i === 0 ? ' on' : ''); b.setAttribute('aria-label', 'Slide ' + (i + 1)); b.addEventListener('click', ((idx) => () => { go(idx); hold(); })(i)); czDots.appendChild(b); } const dots = czDots.querySelectorAll('.vf-ho-dot'); const paint = (k) => { for (let i = 0; i < dots.length; i++) dots[i].classList.toggle('on', i === k); }; const go = (k) => { active = k; czTrack.scrollTo({ left: k * czTrack.clientWidth, behavior: 'smooth' }); paint(k); }; const start = () => { czStop(); czTimer = setInterval(() => go((active + 1) % n), 3500); }; const hold = () => { czStop(); clearTimeout(czResume); czResume = setTimeout(start, 6000); }; czTrack.addEventListener('scroll', () => { if (raf) return; raf = requestAnimationFrame(() => { raf = null; const k = Math.round(czTrack.scrollLeft / czTrack.clientWidth); if (k !== active) { active = k; paint(k); } }); }, { passive: true }); czTrack.addEventListener('mouseenter', () => { if (!drag.on) czStop(); }); czTrack.addEventListener('mouseleave', () => { if (!drag.on) start(); }); czTrack.addEventListener('pointerdown', (e) => { if (e.pointerType === 'touch') return; drag.on = true; drag.x0 = e.clientX; drag.sl0 = czTrack.scrollLeft; czTrack.classList.add('drag'); czTrack.style.scrollSnapType = 'none'; czStop(); try { czTrack.setPointerCapture(e.pointerId); } catch (_) {} }); czTrack.addEventListener('pointermove', (e) => { if (!drag.on) return; czTrack.scrollLeft = drag.sl0 - (e.clientX - drag.x0); }); const endDrag = (e) => { if (!drag.on) return; drag.on = false; czTrack.classList.remove('drag'); const k = Math.max(0, Math.min(n - 1, Math.round(czTrack.scrollLeft / czTrack.clientWidth))); czTrack.style.scrollSnapType = 'x mandatory'; go(k); hold(); try { czTrack.releasePointerCapture(e.pointerId); } catch (_) {} }; czTrack.addEventListener('pointerup', endDrag); czTrack.addEventListener('pointercancel', endDrag); start(); } const lockConversation = () => { try { czStop(); CH.hide(); MZ.disable(); MZ.blurActive(); locked.classList.add('show'); let ticks = 0; const guard = setInterval(() => { CH.hide(); ticks++; if (ticks > 20) clearInterval(guard); }, 500); } catch (e) {} }; btn.addEventListener('click', () => { try { if (typeof gtag === 'function') { gtag('event', 'handover_to_pwa', { event_category: 'funnel', event_label: 'User clicked through to booking portal', user_id: userId || 'unknown', transcript_id: transcriptId || 'unknown' }); } } catch (e) {} lockConversation(); }); CH.hide(); requestAnimationFrame(() => { try { CH.hide(); } catch (e) {} }); setTimeout(() => { try { CH.hide(); } catch (e) {} }, 50); const composerGuard = setInterval(() => { try { CH.hide(); } catch (e) {} }, 500); return () => { try { czStop(); clearTimeout(czResume); clearInterval(composerGuard); } catch (e) {} CH.hide(); MZ.disable(); }; } }; function makeProgressExt({name,step,title,subtitle}){return{name,type:'response',match:({trace})=>trace?.type===name||trace?.payload?.name===name,render:({element})=>{const pct=Math.min(100,Math.max(0,step*25)),w=d.createElement('div');w.className='vf-wide-card';w.innerHTML=`
Step ${step} of 4 — ${title}
0%
1
2
3
4
${subtitle?`
${subtitle}
`:''}
`;element.appendChild(w);stretch(element,w);AH.hideFor(w);const pctEl=w.querySelector('.vf-progress-pct'),fill=w.querySelector('.vf-progress-fill'),dots=[...w.querySelectorAll('.vf-step-dot')];dots.forEach((dot,i)=>{const idx=i+1;dot.classList.remove('complete','current');if(idx1-Math.pow(1-t,3);(function f(n){const t=Math.min(1,(n-st)/dur),v=Math.round(sv+dl*ez(t));fill.style.width=v+'%';pctEl.textContent=v+'%';if(t<1)requestAnimationFrame(f)})(st)}}} const P1=makeProgressExt({name:'ext_progress_step1',step:1,title:'Problem diagnosis',subtitle:'We are understanding your issue.'}), P2=makeProgressExt({name:'ext_progress_step2',step:2,title:'Location',subtitle:'Where do you need help?'}); V=window.voiceflow.chat; V.load({ verify:{projectID:'697882dbdd5ecc37d71465bd'}, url:'https://general-runtime.sanlamstudios.voiceflow.com', versionID:'production', assistant:{persistence:'memory',extensions:[ LO1,LF1,P1,P2,LP,CE,IP,HO, ConversationStartTracker, JobDiagnosisStartTracker, JobDiagnosisCompleteTracker, LocationCaptureStartTracker, LocationCaptureCompleteTracker, CheckForProsStartTracker, CheckForProsCompleteTracker ]} }).then(()=>{d.querySelectorAll('.launch_jess').forEach(el=>el.addEventListener('click',()=>V.open()))}); }; d.body.appendChild(vf); } }(document);