// ============================================================================ // public/screens/flow-builder.jsx — Visual Flow Builder (Phase 4) // ---------------------------------------------------------------------------- // Custom SVG canvas: draggable nodes, bezier edges, pan/zoom, auto-save. // Node types: start, greeting, listen, intent, branch, pitch, objection, // book, transfer, whatsapp, voicemail, end // ============================================================================ const NODE_TYPES = { start: { label: "Start", color: "#888780", icon: "play", hasIn: false, hasOut: true }, greeting: { label: "Greeting", color: "#1D9E75", icon: "message", hasIn: true, hasOut: true }, listen: { label: "Listen", color: "#378ADD", icon: "mic", hasIn: true, hasOut: true }, intent: { label: "Intent", color: "#BA7517", icon: "branch", hasIn: true, hasOut: true, multiOut: true }, branch: { label: "Branch", color: "#BA7517", icon: "branch", hasIn: true, hasOut: true, multiOut: true }, pitch: { label: "Pitch", color: "#1D9E75", icon: "send", hasIn: true, hasOut: true }, objection: { label: "Objection", color: "#D85A30", icon: "shield", hasIn: true, hasOut: true }, book: { label: "Book meeting",color: "#534AB7", icon: "calendar",hasIn: true, hasOut: true }, transfer: { label: "Transfer", color: "#534AB7", icon: "phone", hasIn: true, hasOut: true }, whatsapp: { label: "WhatsApp", color: "#1D9E75", icon: "whatsapp",hasIn: true, hasOut: true }, voicemail: { label: "Voicemail", color: "#888780", icon: "mic", hasIn: true, hasOut: true }, end: { label: "End", color: "#888780", icon: "square", hasIn: true, hasOut: false }, }; const NODE_W = 180, NODE_H = 64, PORT_R = 7; function FlowBuilderScreen({ go }) { const [flows, setFlows] = React.useState(null); const [activeFlowId, setActive] = React.useState(null); const [error, setError] = React.useState(null); const [showCreate, setShowCreate] = React.useState(false); const loadFlows = React.useCallback(async () => { const r = await VoaisAPI.get("/api/flows"); if (r.ok && r.data?.ok) setFlows(r.data.flows); }, []); React.useEffect(() => { loadFlows(); }, [loadFlows]); if (activeFlowId) { return { setActive(null); loadFlows(); }}/>; } if (flows === null) return ; return (
{flows.length} flows
} onClick={() => setShowCreate(true)}>New flow
{flows.length === 0 && (

No flows yet

Flows define the conversation logic your AI agents follow. Start from a template or build from scratch.

} onClick={() => setShowCreate(true)}>Create your first flow
)} {flows.length > 0 && (
{flows.map(f => ( setActive(f.id)} onDelete={async () => { if (!confirm("Delete this flow?")) return; const r = await VoaisAPI.del("/api/flows/" + f.id); if (!r.ok) alert(r.data?.msg || "Delete failed."); loadFlows(); }}/> ))}
)} {showCreate && setShowCreate(false)} onCreated={(id) => { setShowCreate(false); setActive(id); }}/>}
); } // ── Flow list card ────────────────────────────────────────────────────── function FlowListCard({ flow, onOpen, onDelete }) { const statusTone = flow.status === "published" ? "green" : flow.status === "draft" ? "gray" : "yellow"; return (

{flow.name}

{flow.status}
{flow.description &&
{flow.description}
}
{flow.node_count || 0} nodes {flow.edge_count || 0} edges {flow.agents_using > 0 && {flow.agents_using} agent{flow.agents_using > 1 ? "s" : ""}}
Open editor { e.stopPropagation(); onDelete(); }} title="Delete">
); } // ── Create Flow Modal ─────────────────────────────────────────────────── function CreateFlowModal({ onClose, onCreated }) { const [name, setName] = React.useState(""); const [desc, setDesc] = React.useState(""); const [template, setTemplate] = React.useState(""); const [templates, setTemplates] = React.useState([]); const [submitting, setSub] = React.useState(false); React.useEffect(() => { VoaisAPI.get("/api/flows/templates").then(r => { if (r.ok && r.data?.ok) setTemplates(r.data.templates); }); }, []); const submit = async () => { if (!name.trim()) return; setSub(true); const r = await VoaisAPI.post("/api/flows", { name: name.trim(), description: desc.trim() || null, template: template || null }); setSub(false); if (r.ok && r.data?.flowId) onCreated(r.data.flowId); else alert(r.data?.msg || "Failed to create flow."); }; return (
setName(e.target.value)}/>