/* global React */ const { useState, useEffect, useRef, useMemo } = React; /* ============== Workflow templates ============== */ const TEMPLATES = { support: { name: "Support Ticket Triage", blurb: "Inbound email → classify intent → fetch context → draft reply → human review → send.", inputLabel: "Incoming email", sampleInputs: [ { from: "siti@retailco.my", subject: "Order #4821 — wrong size", body: "Hi, I received my order today but the shirt is XL not L. Could I get a swap?" }, { from: "ravi@startup.io", subject: "API rate limits?", body: "We're hitting 429s on /v1/agents. What are the per-minute limits on our plan?" }, { from: "linda@fnb.com", subject: "Invoice for May", body: "Could you resend the May invoice? Accounts couldn't find it in our inbox." }, ], nodes: [ { id: "in", label: "Email Inbox", sub: "Gmail · IMAP", x: 30, y: 110, kind: "trigger" }, { id: "cls", label: "Classify Intent", sub: "GPT-4o · few-shot", x: 250, y: 50, kind: "ai" }, { id: "rag", label: "Retrieve Context", sub: "Vector DB · top-k", x: 250, y: 200, kind: "tool" }, { id: "draft", label: "Draft Reply", sub: "GPT-4o · template", x: 470, y: 110, kind: "ai" }, { id: "review", label: "Human Review", sub: "Slack · #ops-inbox", x: 690, y: 50, kind: "human" }, { id: "send", label: "Send Reply", sub: "Gmail · API", x: 690, y: 200, kind: "action" }, { id: "log", label: "Audit Log", sub: "Postgres", x: 910, y: 110, kind: "store" }, ], edges: [ ["in","cls"], ["in","rag"], ["cls","draft"], ["rag","draft"], ["draft","review"], ["draft","send"], ["review","send"], ["send","log"] ], steps: [ { node: "in", log: (i) => `Received: "${i.subject}" from ${i.from}` }, { node: "cls", log: () => `Classified intent → category, confidence 0.94` }, { node: "rag", log: () => `Retrieved 3 relevant docs from knowledge base (12 ms)` }, { node: "draft", log: () => `Drafted reply (147 tokens) — tone: friendly, concise` }, { node: "review", log: () => `Posted to #ops-inbox · auto-approved (high confidence)` }, { node: "send", log: (i) => `Reply sent to ${i.from}` }, { node: "log", log: () => `Audit row written · trace_id=tk_8f3a2c` }, ], output: (i) => `Hi ${i.from.split("@")[0]},\n\nThanks for reaching out — sorted. Reply scheduled to send in 3s.\n\n— AI Agency support`, }, leads: { name: "Lead Enrichment", blurb: "New form submission → enrich firmographics → score → route to AE → notify Slack.", inputLabel: "Form submission", sampleInputs: [ { from: "marcus@axesoft.my", subject: "Demo request — Axesoft", body: "Hi, we'd like to see your AI automation in action. ~120 staff, KL-based logistics SaaS." }, { from: "yuki@bento.co", subject: "Pricing question", body: "Asking on behalf of our COO. Series A fintech, 40 staff, looking at process automation." }, ], nodes: [ { id: "in", label: "Form", sub: "Webhook", x: 30, y: 110, kind: "trigger" }, { id: "enr", label: "Enrich", sub: "Clearbit + LinkedIn", x: 250, y: 50, kind: "tool" }, { id: "geo", label: "Geo & Sector", sub: "Lookup", x: 250, y: 200, kind: "tool" }, { id: "score",label: "ICP Score", sub: "GPT-4o", x: 470, y: 110, kind: "ai" }, { id: "route",label: "Route to AE", sub: "Round-robin", x: 690, y: 50, kind: "action" }, { id: "slack",label: "Slack Notify", sub: "#sales-pipeline", x: 690, y: 200, kind: "action" }, { id: "crm", label: "Write to CRM", sub: "HubSpot", x: 910, y: 110, kind: "store" }, ], edges: [ ["in","enr"], ["in","geo"], ["enr","score"], ["geo","score"], ["score","route"], ["score","slack"], ["route","crm"], ["slack","crm"] ], steps: [ { node: "in", log: (i) => `Form submitted by ${i.from}` }, { node: "enr", log: () => `Enriched: company size, funding, tech stack` }, { node: "geo", log: () => `Resolved: APAC · SEA · Logistics SaaS` }, { node: "score", log: () => `ICP score: 8.4 / 10 — strong fit` }, { node: "route", log: () => `Assigned to AE: Hana Y. (next in rotation)` }, { node: "slack", log: () => `Posted to #sales-pipeline with full context` }, { node: "crm", log: () => `Contact + deal created in HubSpot` }, ], output: (i) => `New lead from ${i.from} routed to Hana — ICP 8.4 / 10. CRM record opened.`, }, invoice: { name: "Invoice Processing", blurb: "Inbox watcher → OCR PDF → match PO → validate totals → approve → post to ledger.", inputLabel: "Vendor invoice", sampleInputs: [ { from: "billing@vendor.co", subject: "Invoice INV-2049 — May", body: "PDF attached, total RM 12,840.00 due in 30 days." }, { from: "ar@logistico.my", subject: "Invoice 8821", body: "Monthly fulfilment charges, RM 4,210.00." }, ], nodes: [ { id: "in", label: "Vendor Inbox", sub: "IMAP watcher", x: 30, y: 110, kind: "trigger" }, { id: "ocr", label: "OCR Extract", sub: "Vision model", x: 250, y: 110, kind: "ai" }, { id: "po", label: "Match PO", sub: "ERP lookup", x: 470, y: 50, kind: "tool" }, { id: "val", label: "Validate Totals",sub: "Rules engine", x: 470, y: 200, kind: "tool" }, { id: "ap", label: "AP Approval", sub: "Slack approval", x: 690, y: 110, kind: "human" }, { id: "post",label: "Post to Ledger", sub: "Xero · API", x: 910, y: 110, kind: "store" }, ], edges: [ ["in","ocr"], ["ocr","po"], ["ocr","val"], ["po","ap"], ["val","ap"], ["ap","post"] ], steps: [ { node: "in", log: (i) => `Detected: ${i.subject} from ${i.from}` }, { node: "ocr", log: () => `Extracted line items + totals (1.2 s)` }, { node: "po", log: () => `Matched PO #PO-7741 — vendor authorised` }, { node: "val", log: () => `Totals reconcile · tax computed correctly` }, { node: "ap", log: () => `Auto-approved (under RM 20k threshold)` }, { node: "post", log: () => `Posted to Xero ledger · ref AP-2049` }, ], output: () => `Invoice approved and posted. Vendor notified of ETA payment date.`, }, }; /* ============== Visualization ============== */ function nodeColor(kind, accent) { const map = { trigger: accent, ai: "#8b5cf6", tool: "#0891b2", human: "#f59e0b", action: "#16a34a", store: "#64748b", }; return map[kind] || accent; } function nodeIcon(kind) { const stroke = { stroke: "currentColor", fill: "none", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" }; switch (kind) { case "trigger": return ; case "ai": return ; case "tool": return ; case "human": return ; case "action": return ; case "store": return ; default: return null; } } function pathFor(a, b) { // smooth cubic curve from right edge of a to left edge of b const x1 = a.x + 170, y1 = a.y + 34; const x2 = b.x, y2 = b.y + 34; const dx = (x2 - x1) * 0.5; return `M ${x1} ${y1} C ${x1+dx} ${y1}, ${x2-dx} ${y2}, ${x2} ${y2}`; } function WorkflowGraph({ template, activeNodes, activeEdges, accent }) { const W = 1100, H = 320; const nodes = template.nodes; const edges = template.edges; const nodeMap = useMemo(() => Object.fromEntries(nodes.map(n => [n.id, n])), [nodes]); return (
{/* edges */} {edges.map(([from, to], i) => { const a = nodeMap[from], b = nodeMap[to]; if (!a || !b) return null; const d = pathFor(a, b); const isActive = activeEdges.has(`${from}->${to}`); return ( {isActive && ( )} ); })} {/* nodes */} {nodes.map(n => { const isActive = activeNodes.has(n.id); const wasActive = activeNodes.get && activeNodes.get(n.id) === "done"; const c = nodeColor(n.kind, accent); return ( {isActive && }
{nodeIcon(n.kind)}
{n.label} {n.sub}
); })}
); } /* ============== Demo Page ============== */ function DemoPage({ navigate }) { const [templateId, setTemplateId] = useState("support"); const template = TEMPLATES[templateId]; const [inputIdx, setInputIdx] = useState(0); const input = template.sampleInputs[inputIdx % template.sampleInputs.length]; const [running, setRunning] = useState(false); const [stepIdx, setStepIdx] = useState(-1); // -1 idle, 0..steps.length running, steps.length done const [logs, setLogs] = useState([]); const [activeNodes, setActiveNodes] = useState(new Map()); const [activeEdges, setActiveEdges] = useState(new Set()); const [accent, setAccent] = useState(() => getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#3b82f6"); const timerRef = useRef([]); // observe accent changes useEffect(() => { const mo = new MutationObserver(() => { setAccent(getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() || "#3b82f6"); }); mo.observe(document.documentElement, { attributes: true, attributeFilter: ["style"] }); return () => mo.disconnect(); }, []); function clearTimers() { timerRef.current.forEach(t => clearTimeout(t)); timerRef.current = []; } function reset() { clearTimers(); setRunning(false); setStepIdx(-1); setLogs([]); setActiveNodes(new Map()); setActiveEdges(new Set()); } function run() { reset(); setRunning(true); const stepDur = 1100; setLogs([{ t: now(), msg: `Started workflow: ${template.name}`, kind: "info" }]); template.steps.forEach((s, i) => { const t1 = setTimeout(() => { setStepIdx(i); setActiveNodes(prev => { const next = new Map(prev); next.set(s.node, "active"); return next; }); // edges into this node const incoming = template.edges.filter(([_, b]) => b === s.node); setActiveEdges(prev => { const next = new Set(prev); incoming.forEach(([a,b]) => next.add(`${a}->${b}`)); return next; }); setLogs(prev => [...prev, { t: now(), msg: s.log(input), kind: "step", node: s.node }]); }, i * stepDur); const t2 = setTimeout(() => { setActiveNodes(prev => { const next = new Map(prev); next.set(s.node, "done"); return next; }); }, i * stepDur + 800); timerRef.current.push(t1, t2); }); const tEnd = setTimeout(() => { setRunning(false); setStepIdx(template.steps.length); setLogs(prev => [...prev, { t: now(), msg: `Workflow completed in ${(template.steps.length * stepDur / 1000).toFixed(1)}s`, kind: "success" }]); }, template.steps.length * stepDur + 200); timerRef.current.push(tEnd); } useEffect(() => () => clearTimers(), []); useEffect(() => { reset(); }, [templateId, inputIdx]); function now() { const d = new Date(); return d.toTimeString().split(" ")[0] + "." + String(d.getMilliseconds()).padStart(3,"0"); } const progress = stepIdx < 0 ? 0 : Math.min(1, (stepIdx + 1) / template.steps.length); return (
Live demo

See an automation run, end-to-end.

Pick a workflow, hit run, and watch each node fire. The logs on the right are what your team would see in production.

{/* Template tabs */}
{Object.entries(TEMPLATES).map(([id, t]) => ( ))}
{/* Stage */}
Workflow
{template.name}
{/* Bottom panes */}
{template.inputLabel}
From{input.from}
Subject{input.subject}
{input.body}
Live log
{running ? "● running" : stepIdx >= template.steps.length ? "✓ complete" : "○ idle"}
{logs.length === 0 && (
Press ▶ Run to start the workflow.
)} {logs.map((l, i) => (
{l.t} {l.msg}
))} {stepIdx >= template.steps.length && (
Output
{template.output(input)}
)}

Want one of these wired into your stack?

We scope, prototype and ship workflows like this in 2-4 weeks.

{e.preventDefault(); navigate("contact");}} className="btn btn-accent"> Book a call
); } window.DemoPage = DemoPage;