!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=``;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 = `
`;
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 = `
`;
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%
${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);