// ════════════════════════════════════════════════════════════════════ // NexFlow · Service-demo modal + six animated mini-products // ──────────────────────────────────────────────────────────────────── // Modal hosts six animated demos, each driven by a narration script. // The narrator emits `{index, focus, text}` events as each sentence is // spoken; demos subscribe and highlight the element being talked about // (focused node glows, others dim). Clicking a focusable element jumps // the narration to the segment that describes it. Prev/Next controls // step through the script. Audio + speech engine lives in // engine/demo-audio.js — `window.NF_DemoAudio`. // Entry point: `window.NF_ServiceDemos.openDemo(serviceId)`. // ════════════════════════════════════════════════════════════════════ const { useState: useStateD, useEffect: useEffectD, useRef: useRefD, useMemo: useMemoD } = React; const AUD = () => window.NF_DemoAudio; // ════════════════════════════════════════════════════════════════════ // N8N-AUTHENTIC SVG ICON SET · used inside workflow nodes // ════════════════════════════════════════════════════════════════════ // Each icon is authored on a 24×24 viewBox and renders in white. They // embed inside the outer SVG canvas as nested s; the surrounding // node draws the brand-coloured pill behind them. // ──────────────────────────────────────────────────────────────────── function N8nIcon({ name }) { // children rendered at 24x24, scaled by the parent g switch (name) { case 'webhook': return ( ); case 'openai': return ( ); case 'globe': return ( ); case 'crm': return ( ); case 'calendar': return ( ); case 'sms': return ( ); default: return null; } } // ════════════════════════════════════════════════════════════════════ // DEMO REGISTRY · metadata + narration scripts with focus targets // ════════════════════════════════════════════════════════════════════ // Each narration segment carries an optional `focus` that the demo // uses to highlight the element being described. The narrator emits // the focus to subscribers on segment change so the visual follows the // voice without polling. // ──────────────────────────────────────────────────────────────────── const DEMO_META = { n8n: { title: 'Real-estate pipeline · live run', url: 'workflows / real-estate-pipeline.json', badge: 'n8n · live', stats: [['11.2s','avg run'],['$0.04','per lead'],['99.94%','success · 30d']], // Per-node payload snippets shown in the side-rail when the node is active. // Kept short (3–6 lines) so they read at glance — these mirror the kind // of data finance / ops teams see when they audit a real n8n execution. payloads: [ { lang: 'json', label: 'POST /webhook/rea-leads', body: '{\n "source": "realestate.com.au",\n "listing": "3br-bondi-1.8M",\n "buyer": "sophie.k@gmail.com",\n "msg": "Saturday inspection?"\n}' }, { lang: 'json', label: 'openai · score', body: '{\n "score": 84,\n "intent": "hot · inspection",\n "reasons": ["finance pre-approved",\n "specific listing", "timing this week"]\n}' }, { lang: 'json', label: 'corelogic · enrich', body: '{\n "comparables": 12,\n "within_1km": 4,\n "median": "$1.74M",\n "trend_90d": "+2.1%"\n}' }, { lang: 'json', label: 'ghl · upsert', body: '{\n "contact_id": "ghl_8q42",\n "pipeline": "hot_buyers",\n "owner": "noor",\n "tags": ["sat-inspection"]\n}' }, { lang: 'json', label: 'cal.com · hold', body: '{\n "slot": "2026-05-26T10:00:00+10:00",\n "agent": "Noor",\n "hold_ttl_min": 45,\n "link": "cal.com/m/r-bondi-tue"\n}' }, { lang: 'text', label: 'twilio · sms sent', body: 'To: +61 4** *** ***\nFrom: NexFlow Realty\n\nHi Sophie — confirmed for\nTue 10:00 AEST at 12 Smith St.\nReply Y to confirm, R to reschedule.' }, ], narration: [ { text: 'A new enquiry lands from realestate dot com. The trigger fires.', focus: { node: 0 } }, { text: 'OpenAI reads the message and scores the lead — eighty-four out of one hundred. Serious buyer.', focus: { node: 1 } }, { text: 'CoreLogic enriches the lead with twelve comparable properties within one kilometre.', focus: { node: 2 } }, { text: 'GoHighLevel creates the contact and pushes it into the hot pipeline.', focus: { node: 3 } }, { text: 'A viewing is auto-booked into the agent\u2019s calendar for Tuesday at ten.', focus: { node: 4 } }, { text: 'Twilio sends the SMS confirmation. End to end, eleven seconds. Nobody touched a keyboard.', focus: { node: 5 } }, ], }, agents: { title: 'Support triage agent · live queue', url: 'agents / support-triage.yaml', badge: 'agentic · live', stats: [['1.8s','median decision'],['86%','auto-resolved'],['100%','logged']], // Per-worker classifier output — mirrors what the actual agent emits // into the audit log. Drives the "current ticket" detail card. classifications: { classify: { intent: 'refund_request', confidence: 0.94, route: 'auto', model: 'claude-sonnet-4-5' }, enrich: { docs_pulled: 3, kb_match: 'returns_policy_v3', cache_hit: true, model: 'rag·local' }, draft: { tone: 'apologetic_clear', tokens_in: 412, tokens_out: 187, model: 'gpt-4o-2024-11-20' }, dispatch: { channel: 'email', sla_ms: 1840, cost_usd: 0.0021, status: 'sent' }, }, narration: [ { text: 'Support tickets arrive faster than any human can read them.', focus: { panel: 'queue' } }, { text: 'The agent classifies each ticket — what it is, who owns it.', focus: { worker: 'classify' } }, { text: 'It enriches from your knowledge base — past tickets, docs, prices.', focus: { worker: 'enrich' } }, { text: 'It drafts the right reply in your tone of voice.', focus: { worker: 'draft' } }, { text: 'It dispatches the response back to the customer.', focus: { worker: 'dispatch' } }, { text: 'Eighty-six percent resolve without a human ever touching them.', focus: { panel: 'done' } }, ], }, crm: { title: 'GoHighLevel pipeline · this week', url: 'crm / leads-pipeline', badge: 'crm · live', stats: [['+22%','close-rate'],['8.4h','/ wk reclaimed'],['11.2s','enrichment']], narration: [ { text: 'Inbound leads land in the new column the moment they arrive.', focus: { col: 0 } }, { text: 'The AI scores them and enriches them with public data.', focus: { col: 1 } }, { text: 'Viewings are booked automatically into the agent\u2019s calendar.', focus: { col: 2 } }, { text: 'Closed deals move to won. Twenty-two percent lift in close-rate.', focus: { col: 3 } }, { text: 'Eight hours a week reclaimed from manual data entry.', focus: { col: null } }, ], }, chat: { title: 'Nex concierge · grounded chat', url: 'chat / nex.session', badge: 'chatbot · live', stats: [['~8s','first reply'],['41%','book-rate'],['12','knowledge bases']], // RAG source chips shown under each bot reply — the visible audit trail // for what the chatbot was actually grounded in. Indexed by message # // (only bot messages have sources). sources: [ ['pricing.md · §2', 'system-prompt.md'], ['case-studies/real-estate.md · §4', 'pricing.md · spark'], ['pricing.md · plans', 'pricing.md · spark', 'pricing.md · flow'], ['booking-flow.md', 'calendars/noor.json'], ], narration: [ { text: 'Nex is grounded in your pricing, your docs, your case studies.', focus: { panel: 'session' } }, { text: 'It answers honest questions with honest numbers.', focus: { panel: 'chat' } }, { text: 'When the visitor is ready, it books a real call into your real calendar.', focus: { panel: 'book' } }, { text: 'If intent is high it hands off to a human in Slack.', focus: { panel: 'handoff' } }, { text: 'Forty-one percent of conversations end in a booked call.', focus: { panel: 'book' } }, ], }, doc: { title: 'Invoice OCR → Xero · INV-2847', url: 'document-ai / invoice-2847.pdf', badge: 'ocr · live', stats: [['99.4%','match accuracy'],['8h','/ mo saved'],['100%','audit-logged']], narration: [ { text: 'An invoice hits the inbox. The agent reads it line by line.', focus: { field: 'header' } }, { text: 'It extracts the invoice number, the vendor, and the date.', focus: { field: 'header-fields' } }, { text: 'Subtotal, tax, and total — all matched to ninety-nine percent confidence.', focus: { field: 'totals' } }, { text: 'The bill is posted to Xero before you\u2019ve finished your coffee.', focus: { field: 'posted' } }, { text: 'Under five hundred dollars auto-approves. Above goes to finance.', focus: { field: 'approval' } }, ], }, report: { title: 'Operations digest · Monday 07:00', url: 'reports / weekly-ops.live', badge: 'reporting · live', stats: [['7:00','daily delivery'],['30+','sources'],['~5 KPIs','per pulse']], narration: [ { text: 'Every Monday at seven, the digest lands in your inbox.', focus: { panel: 'kpis' } }, { text: 'Revenue, orders, leads, NPS — pulled live from Stripe, Xero, your CRM.', focus: { panel: 'kpis' } }, { text: 'The bar chart shows daily orders for the last fortnight.', focus: { panel: 'bars' } }, { text: 'The gauge shows how much of your ops is automated.', focus: { panel: 'gauge' } }, { text: 'Anomalies trip alerts before your board sees them.', focus: { panel: 'kpis' } }, ], }, }; // ════════════════════════════════════════════════════════════════════ // DemoModal wrapper — chrome, captions, controls, focus relay // ════════════════════════════════════════════════════════════════════ function DemoModal({ serviceId, onClose, opts = {} }) { const meta = DEMO_META[serviceId] || DEMO_META.n8n; const data = window.NF_DATA; const svc = (data && data.SERVICES.find(s => s.id === serviceId)) || null; const [caption, setCaption] = useStateD(''); const [segment, setSegment] = useStateD(null); const [muted, setMuted] = useStateD(() => AUD()?.isMuted?.() || false); const frameRef = useRefD(null); // Audio + narration lifecycle — robust against rapid demo switching. useEffectD(() => { const A = AUD(); if (!A) return; A.onCaption(setCaption); A.onSegment(setSegment); A.startPad(); // Defer 240ms so voice list resolves and any pending cancel() drains const t = setTimeout(() => { A.speak(meta.narration || [], { loop: true }); }, 240); return () => { clearTimeout(t); A.stopSpeech(); A.onCaption(null); A.onSegment(null); A.stopPad(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [serviceId]); // ESC + body-scroll lock + Tab focus-trap. // ESC / Arrow keys live on window so they fire regardless of which // child has focus. The Tab handler lives on the modal frame so it // only intercepts focus moves inside the dialog. useEffectD(() => { function onKey(e) { if (e.key === 'Escape') onClose(); if (e.key === 'ArrowRight') AUD()?.next?.(); if (e.key === 'ArrowLeft') AUD()?.prev?.(); } window.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; const frame = frameRef.current; let cleanupFrameKey = null; if (frame) { // Pull initial focus into the dialog so screen readers + keyboard // users land inside it. The Close button is the safest target. const closeBtn = frame.querySelector('.close'); if (closeBtn) requestAnimationFrame(() => closeBtn.focus()); function onFrameKey(e) { if (e.key !== 'Tab') return; const fs = Array.from(frame.querySelectorAll( 'button:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])' )).filter(el => el.offsetParent !== null); if (!fs.length) return; const first = fs[0], last = fs[fs.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } frame.addEventListener('keydown', onFrameKey); cleanupFrameKey = () => frame.removeEventListener('keydown', onFrameKey); } return () => { window.removeEventListener('keydown', onKey); document.body.style.overflow = ''; if (cleanupFrameKey) cleanupFrameKey(); }; }, [onClose]); function toggleMute() { const next = !muted; setMuted(next); AUD()?.setMuted(next); if (!next) AUD()?.speak(meta.narration || [], { loop: true }); } const focus = segment?.focus || null; const totalSegs = (meta.narration || []).length; const segIndex = segment?.index ?? -1; const Demo = serviceId === 'agents' ? DemoAgents : serviceId === 'crm' ? DemoCRM : serviceId === 'chat' ? DemoChat : serviceId === 'doc' ? DemoDoc : serviceId === 'report' ? DemoReport : DemoWorkflow; // Demos request a specific narration index when the user clicks a node function jumpTo(i) { AUD()?.jumpTo(i); } return (
{ if (e.target === e.currentTarget) onClose(); }}>
{meta.badge}
{caption} {segIndex + 1} / {totalSegs}
); } // ════════════════════════════════════════════════════════════════════ // DEMO 1 · n8n workflow · narration-driven node focus // ════════════════════════════════════════════════════════════════════ const N8N_PALETTE = { webhook: '#F8884B', openai: '#10A37F', http: '#127C92', crm: '#0E7C63', cal: '#1E5BC8', twilio: '#F22F46', }; function DemoWorkflow({ meta, focus, jumpTo }) { const W = 196, H = 76; const NODES = [ { x: 8, y: 28, name: 'REA.com', op: 'webhook · POST', icon: 'webhook', color: N8N_PALETTE.webhook }, { x: 232, y: 28, name: 'OpenAI', op: 'score lead · 4o', icon: 'openai', color: N8N_PALETTE.openai }, { x: 456, y: 28, name: 'CoreLogic', op: 'enrich · http GET', icon: 'globe', color: N8N_PALETTE.http }, { x: 456, y: 156, name: 'GoHighLevel', op: 'upsert contact', icon: 'crm', color: N8N_PALETTE.crm }, { x: 232, y: 156, name: 'Cal.com', op: 'book viewing', icon: 'calendar', color: N8N_PALETTE.cal }, { x: 8, y: 156, name: 'Twilio', op: 'send sms', icon: 'sms', color: N8N_PALETTE.twilio }, ]; const EDGES = [[0,1], [1,2], [2,3], [3,4], [4,5]]; const LOG = [ ['00:00.0', 'trigger · sophie.k@gmail.com · "3br Bondi · $1.8M"', 'now'], ['00:01.2', 'openai · score 84/100 · serious buyer', 'ok'], ['00:02.7', 'corelogic · 12 comparables · 4 within 1km', 'ok'], ['00:05.4', 'ghl · contact "Sophie K." created · hot pipeline', 'ok'], ['00:07.9', 'cal.com · slot held · Tue 10:00 AEST', 'ok'], ['00:11.2', 'twilio · sms delivered · +61 4** ***', 'ok'], ]; // Active node = the one the narrator is currently describing. // All earlier nodes are "done"; later nodes are dim. const activeNode = (focus && typeof focus.node === 'number') ? focus.node : 0; // Animate the packet moving along the edge into the active node // whenever the focus changes. Source is the previous node. const packetRef = useRefD(null); const prevNodeRef = useRefD(-1); useEffectD(() => { if (activeNode <= 0 || !packetRef.current) { if (packetRef.current) packetRef.current.style.visibility = 'hidden'; prevNodeRef.current = activeNode; return; } const ai = activeNode - 1, bi = activeNode; const pa = NODES[ai], pb = NODES[bi]; if (!pa || !pb) return; const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); tempPath.setAttribute('d', edgePath(pa, pb)); const len = tempPath.getTotalLength(); let raf, start = performance.now(); packetRef.current.style.visibility = 'visible'; function frame(now) { const k = Math.min(1, (now - start) / 1100); const pt = tempPath.getPointAtLength(k * len); if (packetRef.current) packetRef.current.setAttribute('transform', `translate(${pt.x},${pt.y})`); if (k < 1) raf = requestAnimationFrame(frame); else if (packetRef.current) setTimeout(() => { if (packetRef.current) packetRef.current.style.visibility = 'hidden'; }, 400); } raf = requestAnimationFrame(frame); prevNodeRef.current = activeNode; return () => cancelAnimationFrame(raf); // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeNode]); function edgePath(a, b) { const sx = a.x + W, sy = a.y + H / 2; const ex = b.x, ey = b.y + H / 2; // Vertical wrap (same column, different row) — swing right and back if (a.x === b.x) { const cpx = sx + 72; return `M${sx},${sy} C${cpx},${sy} ${cpx},${ey} ${ex + W},${ey}`; } const dx = ex - sx; if (dx >= 0) { const cp = Math.max(40, Math.abs(dx) * 0.55); return `M${sx},${sy} C${sx + cp},${sy} ${ex - cp},${ey} ${ex},${ey}`; } // Backward horizontal — drop, traverse along the bottom, come back up const dy = (a.y < 110 ? 96 : -96); return `M${sx},${sy} L${sx + 16},${sy} C${sx + 56},${sy} ${sx + 56},${sy + dy} ${sx + 16},${sy + dy} L${ex - 16},${ey + dy} C${ex - 56},${ey + dy} ${ex - 56},${ey} ${ex - 16},${ey} L${ex},${ey}`; } // Payload to surface in the side-rail when activeNode changes. // Drives the JSON preview that makes the workflow read as a real product // rather than a static diagram. Defaults to the trigger payload at idx 0. const payload = (meta.payloads && meta.payloads[activeNode]) || (meta.payloads && meta.payloads[0]) || null; return (

{meta.title}

{activeNode >= NODES.length - 1 ? '✓ run complete' : `executing · ${NODES[activeNode]?.name}`}
execution log · scrubbed of PII
{LOG.slice(0, activeNode + 1).map(([ts, body, k], i) => (
[{ts}] {body}
))} {payload && (
{payload.lang} {payload.label}
{payload.body}
)}
); } // ════════════════════════════════════════════════════════════════════ // DEMO 2 · agentic AI — narration-driven worker highlights // ════════════════════════════════════════════════════════════════════ function DemoAgents({ meta, focus, jumpTo }) { const initialQueue = [ { id:1, src:'support · email', body:'Refund request · order #18472' }, { id:2, src:'support · chat', body:'How do I cancel my subscription?' }, { id:3, src:'support · webform', body:'Login fails after password reset' }, { id:4, src:'support · email', body:'Wrong size — exchange for L' }, { id:5, src:'support · chat', body:'Tracking number not updating' }, ]; const WORKERS = ['classify','enrich','draft','dispatch']; const PANEL_TO_SEG = { queue: 0, classify: 1, enrich: 2, draft: 3, dispatch: 4, done: 5 }; const [queue, setQueue] = useStateD(initialQueue); const [busy, setBusy] = useStateD(new Set()); const [done, setDone] = useStateD([]); const counter = useRefD(initialQueue.length); // Light up the worker the narrator is currently describing const focusedWorker = focus && focus.worker; const focusedPanel = focus && focus.panel; // Per-worker classifier preview — surfaces what the agent is actually // deciding right now. Without it, the four pills read as abstract "AI". const cls = (meta.classifications && focusedWorker && meta.classifications[focusedWorker]) || null; const currentTicket = queue[0] || initialQueue[0]; useEffectD(() => { let cancelled = false; function tick() { if (cancelled) return; setQueue(q => { if (q.length === 0) return q; const next = q[0]; const rest = q.slice(1); WORKERS.forEach((w, i) => { setTimeout(() => !cancelled && setBusy(b => { const n = new Set(b); n.add(w); return n; }), i * 350); setTimeout(() => !cancelled && setBusy(b => { const n = new Set(b); n.delete(w); return n; }), i * 350 + 700); }); setTimeout(() => { if (cancelled) return; setDone(d => [{ ...next, status:'auto-resolved' }, ...d].slice(0, 4)); counter.current += 1; const fresh = [...initialQueue][counter.current % initialQueue.length]; setQueue(qq => qq.length < 4 ? [...qq, { ...fresh, id: counter.current * 10 + Math.random() }] : qq); }, WORKERS.length * 350 + 600); return rest; }); } tick(); const id = setInterval(tick, 3400); return () => { cancelled = true; clearInterval(id); }; }, []); return (

{meta.title}

{done.length} resolved · {queue.length} queued
jumpTo(PANEL_TO_SEG.queue)}>
inbound queue{queue.length}
{queue.map(t => (
{t.src}{t.body}
))}
jumpTo(PANEL_TO_SEG.done)}>
resolved{counter.current - queue.length}
{done.map((t, i) => (
✓ {t.status}{t.body}
))}
); } // ════════════════════════════════════════════════════════════════════ // DEMO 3 · CRM pipeline — focused column glows // ════════════════════════════════════════════════════════════════════ function DemoCRM({ meta, focus, jumpTo }) { const NAMES = ['Sophie K.','Marcus L.','Aisha P.','Lukas R.','Diego M.','Wren T.','Bea C.','Jade O.','Noah W.','Pia G.']; const VALUES = ['$ 1.8M','$ 2.4M','$ 980k','$ 1.2M','$ 1.6M','$ 2.1M','$ 760k','$ 1.4M','$ 2.9M','$ 1.05M']; const initials = (n) => n.split(' ').map(w => w[0]).join('').slice(0, 2); function mkLead(i, col) { return { id: i + '-' + Math.random().toString(36).slice(2, 7), name: NAMES[i % NAMES.length], val: VALUES[i % VALUES.length], // AI fit score, 60–98. Stays stable across moves so the lead // "keeps its identity" as it walks across the kanban. score: 60 + (Math.floor(Math.random() * 39)), col }; } const [cols, setCols] = useStateD(() => ([ [mkLead(0, 0), mkLead(1, 0), mkLead(2, 0)], [mkLead(3, 1), mkLead(4, 1), mkLead(5, 1)], [mkLead(6, 2), mkLead(7, 2)], [mkLead(8, 3), mkLead(9, 3), mkLead(0, 3)], ])); const counter = useRefD(10); useEffectD(() => { function tick() { setCols(prev => { const next = prev.map(c => [...c]); const movable = next.slice(0, 3).map((c, i) => c.length > 0 ? i : -1).filter(i => i >= 0); if (movable.length) { const from = movable[Math.floor(Math.random() * movable.length)]; const lead = next[from].shift(); if (lead) { lead.col = from + 1; next[from + 1] = [lead, ...next[from + 1]].slice(0, 4); } } if (next[0].length < 3) next[0].push(mkLead(counter.current++, 0)); if (next[3].length > 4) next[3] = next[3].slice(0, 4); return next; }); } const id = setInterval(tick, 1800); return () => clearInterval(id); }, []); const TITLES = [ { n:'01 · NEW', k:'inbound' }, { n:'02 · SCORED', k:'enriched' }, { n:'03 · VIEWING', k:'booked' }, { n:'04 · CLOSED', k:'won this Q' }, ]; const focusedCol = focus && typeof focus.col === 'number' ? focus.col : null; return (

{meta.title}

auto-enriched · auto-routed · auto-booked ▲ +4 this wk
{cols.map((leads, i) => (
jumpTo(i)}>
{TITLES[i].n}{leads.length}
{leads.map(lead => (
{initials(lead.name)}
{lead.name} = 80 ? 'hot' : (lead.score >= 70 ? 'warm' : 'cool'))}>{lead.score}
{TITLES[i].k} {lead.val}
))}
))}
); } // ════════════════════════════════════════════════════════════════════ // DEMO 4 · Chatbot // ════════════════════════════════════════════════════════════════════ function DemoChat({ meta, focus, jumpTo }) { const SCRIPT = [ { role:'bot', text:"Hi — I'm Nex. Ask me about pricing, what we build, or book a 15-min map call." }, { role:'user', text:"Do you handle real-estate workflows?" }, { role:'bot', text:"Yes — REA enquiry → AI score → CRM → SMS confirmation. Typical run: 11.2s. Want the live demo or pricing?" }, { role:'user', text:"What's it cost?" }, { role:'bot', text:"From A$2,400 one-off (Spark) — ships in ~2 weeks, you own the code. Or A$1,800/mo on Flow for ongoing builds." }, { role:'user', text:"Book me a 15-min map." }, { role:'bot', text:"On it — paid scoping call ($50 USD, fully credited if you build). Pick a time?", quicks: true }, ]; const PANEL_TO_SEG = { session: 0, chat: 1, book: 2, handoff: 3 }; const focusedPanel = focus && focus.panel; const [msgs, setMsgs] = useStateD([]); const [typing, setTyping] = useStateD(false); useEffectD(() => { let cancelled = false; let i = 0; function next() { if (cancelled) return; if (i >= SCRIPT.length) { setTimeout(() => { if (!cancelled) { setMsgs([]); i = 0; setTimeout(next, 600); } }, 5000); return; } const s = SCRIPT[i]; if (s.role === 'bot') { setTyping(true); setTimeout(() => { if (cancelled) return; setTyping(false); setMsgs(m => [...m, s]); i++; setTimeout(next, 900); }, 950 + s.text.length * 8); } else { setTimeout(() => { if (cancelled) return; setMsgs(m => [...m, s]); i++; setTimeout(next, 700); }, 1100); } } setTimeout(next, 400); return () => { cancelled = true; }; }, []); const last = msgs[msgs.length - 1]; return (

{meta.title}

grounded in your pricing · docs · case studies
jumpTo(PANEL_TO_SEG.chat)}>
N
Nexconcierge · grounded
live
{msgs.map((m, i) => { // Source chips appear under bot replies — the visible audit // trail for what the chatbot was grounded in. We index by the // running count of bot messages, not the global index, so // chips stay correctly associated as the script replays. const botIdx = msgs.slice(0, i + 1).filter(x => x.role === 'bot').length - 1; const srcs = m.role === 'bot' && meta.sources && meta.sources[botIdx]; return (
{m.text}
{srcs && (
grounded in {srcs.map((s, j) => {s})}
)}
); })} {typing && (
)}
{last && last.quicks && (
)}
jumpTo(PANEL_TO_SEG.session)}>
Session
grounded.match · 0.91 confidence
intentbook · pricing
verticalreal estate
price tierspark · A$2.4k
sourcespricing · case · faq
jumpTo(PANEL_TO_SEG.handoff)}>
Hand-off
to human when intent · book or quote ≥ A$10k
channelslack · #sales
latency< 1 biz day
{last && last.quicks && (
jumpTo(PANEL_TO_SEG.book)}>
Booking initialised
→ Cal.com hold placed
slotTue 10:00 AEST
paymentStripe · $50 USD
)}
); } // ════════════════════════════════════════════════════════════════════ // DEMO 5 · Document AI (invoice OCR → Xero) — scan progress follows // narration; clicking a panel jumps to its segment // ════════════════════════════════════════════════════════════════════ function DemoDoc({ meta, focus, jumpTo }) { const ROWS = 9; const FIELDS = [ { key:'invoice no.', val:'INV-2847', conf:'0.99', group:'header-fields' }, { key:'vendor', val:'Office Group Pty Ltd', conf:'0.98', group:'header-fields' }, { key:'date', val:'12 / 15 / 26', conf:'0.99', group:'header-fields' }, { key:'subtotal', val:'$ 350.00', conf:'0.99', group:'totals' }, { key:'tax · gst', val:'$ 39.40', conf:'0.99', group:'totals' }, { key:'total', val:'$ 389.40', conf:'0.99', group:'totals' }, ]; // Map narration field-target to a normalised scan progress (0..1). const PROG = { 'header': 0.18, 'header-fields': 0.42, 'totals': 0.78, 'posted': 0.98, 'approval': 0.98 }; const PANEL_TO_SEG = { 'header': 0, 'header-fields': 1, 'totals': 2, 'posted': 3, 'approval': 4 }; const focusedField = focus && focus.field; const targetT = focusedField && PROG[focusedField] != null ? PROG[focusedField] : 0; const [t, setT] = useStateD(0); // Tween t toward targetT for a smooth scan beam motion. useEffectD(() => { let raf; function frame() { setT(prev => { const delta = targetT - prev; if (Math.abs(delta) < 0.002) return targetT; return prev + delta * 0.06; }); raf = requestAnimationFrame(frame); } raf = requestAnimationFrame(frame); return () => cancelAnimationFrame(raf); }, [targetT]); const scanProgress = Math.min(t / 0.85, 1); const scanTop = `${6 + scanProgress * 86}%`; const allDone = t > 0.9; return (

{meta.title}

{allDone ? 'posted to xero · ✓ approved < $500' : 'scanning…'}
jumpTo(PANEL_TO_SEG['header'])}>
Office Group Pty Ltd
Supplier ID 41824753921 · Service account
INV-2847
12 Dec 2026
Bill to
NexFlow Pty Ltd · support@nex-flow.io
itemqtyunittotal
{[ {n:'Ergonomic mesh chair · X4 Pro', q:2, u:'$ 129.00', t:'$ 258.00'}, {n:'Standing desk converter', q:1, u:'$ 68.00', t:'$ 68.00'}, {n:'Cable management tray', q:3, u:'$ 8.00', t:'$ 24.00'}, ].map((li, i) => { const rowAt = 0.30 + (i / 4) * 0.32; return (
rowAt ? 'scanned' : '')}> {li.n} {li.q} {li.u} {li.t}
); })}
0.72 ? 'scanned' : '')}> subtotal$ 350.00
0.80 ? 'scanned' : '')}> GST · 10%$ 39.40
0.85 ? 'scanned' : '')}> total due$ 389.40
{/* Field-highlight box: a faint indigo rectangle that tracks the scan beam, calling out which region is currently being captured. Hidden once the run completes. */} {t < 0.92 && (
)}
extracted fields {allDone ? '✓ verified' : 'live'}
{FIELDS.map((f, i) => { const visible = (f.group === 'header-fields' && t >= PROG['header-fields'] - 0.10) || (f.group === 'totals' && t >= PROG['totals'] - 0.10); const done = (f.group === 'header-fields' && t >= PROG['header-fields']) || (f.group === 'totals' && t >= PROG['totals']); const isFxd = focusedField === f.group; return (
jumpTo(PANEL_TO_SEG[f.group])}>
{f.key}
{f.val}
{f.conf}
); })}
jumpTo(PANEL_TO_SEG['posted'])}>
Posted to Xero · auto-approved
amount < $500 threshold · Slack #finance notified
); } // ════════════════════════════════════════════════════════════════════ // DEMO 6 · Reporting / dashboard — focused panel glows // ════════════════════════════════════════════════════════════════════ function DemoReport({ meta, focus, jumpTo }) { const KPIS = [ { k:'revenue · MTD', d:'+12.4%', base:48720, scale:120 }, { k:'orders · today', d:'+4', base:147, scale:6 }, { k:'leads · this wk', d:'+22%', base:84, scale:3 }, { k:'NPS · last 100', d:'+3', base:72, scale:1 }, ]; const PANEL_TO_SEG = { kpis: 0, bars: 2, gauge: 3 }; const focusedPanel = focus && focus.panel; const [tick, setTick] = useStateD(0); const [bars, setBars] = useStateD(() => Array.from({ length: 14 }, () => Math.random() * 0.5 + 0.3)); const [gv, setGv] = useStateD(0.74); useEffectD(() => { const id = setInterval(() => { setTick(t => t + 1); setBars(prev => prev.map((v) => Math.max(0.15, Math.min(1, v + (Math.random() - 0.45) * 0.18)))); setGv(g => Math.max(0.4, Math.min(0.96, g + (Math.random() - 0.5) * 0.06))); }, 1100); return () => clearInterval(id); }, []); const C = 2 * Math.PI * 64; const offset = C - C * gv; return (

{meta.title}

delivered to inbox · slack · pdf
jumpTo(PANEL_TO_SEG.kpis)}> {KPIS.map((kpi, ki) => { const v = kpi.base + (tick % 7) * kpi.scale * 0.6; const formatted = (kpi.k.startsWith('revenue') ? '$ ' : '') + Math.round(v).toLocaleString(); // Synthesise a 14-point sparkline per KPI, seeded by index so each // KPI has its own stable trend curve. The last point follows // `tick` so the line breathes in sync with the rest of the demo. const pts = Array.from({length: 14}, (_, j) => { const phase = (j / 13) * Math.PI * 2 + ki; return 0.5 + Math.sin(phase + tick * 0.4) * 0.18 + Math.sin(phase * 0.6 + ki) * 0.12; }); const w = 78, h = 22; const path = pts.map((p, j) => { const x = (j / (pts.length - 1)) * w; const y = h - p * h; return (j === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1); }).join(' '); const last = pts[pts.length - 1]; return (
{kpi.k}
{formatted}
▲ {kpi.d}
); })}
jumpTo(PANEL_TO_SEG.bars)}>
orders · 14 dayslive
{bars.map((v, i) => (
))}
last sync · just now ▲ 18% wow source · stripe · ga · xero
jumpTo(PANEL_TO_SEG.gauge)}>
automation coveragetarget 80%
= 0.75 ? '#4FD8A3' : '#0E7C63' }}/>
{Math.round(gv * 100)}%
of ops automated
); } // ── Mount helper ──────────────────────────────────────────────────── function DemoRoot() { const [open, setOpen] = useStateD(null); const [opts, setOpts] = useStateD({}); const close = () => { setOpen(null); setOpts({}); }; useEffectD(() => { window.NF_ServiceDemos = { openDemo: (id, nextOpts) => { setOpen(id); setOpts(nextOpts || {}); }, closeDemo: close, }; return () => { delete window.NF_ServiceDemos; }; }, []); if (!open) return null; return ; } window.NF_DemoRoot = DemoRoot;