import React, { useEffect, useRef, useState } from 'react'; import { Boxes, Database, Figma, Radio, Share2, Sparkles, Users, Zap } from 'lucide-react'; import { api, apiData } from '../services/api'; import { useLlmConfig } from '../services/useLlmConfig'; import ModelGate from './ModelGate'; /** * DataHub Render Flow (Phase 3): source flow diagram with green-lightning * verified-connection boxes → Render → OASIS streams personas one by one into * the graph below, each carrying its READ-ONLY discovered steering. */ const SOURCES = [ { id: 'hubspot', label: 'HubSpot', icon: Database }, { id: 'salesforce', label: 'Salesforce', icon: Database }, { id: 'figma', label: 'Figma', icon: Figma }, { id: 'last30days', label: 'last30days', icon: Radio }, { id: 'monitoring', label: 'Monitoring', icon: Share2 }, { id: 'neo4j', label: 'Neo4j', icon: Boxes }, ] as const; interface Health { configured: boolean; connected: boolean | null; detail: string; } interface Node { id: string; index: number; label: string; profile: any; discovered_steering?: any; provenance?: any; } const SourceBox: React.FC<{ source: (typeof SOURCES)[number]; health?: Health }> = ({ source, health }) => { const state = !health ? 'verifying' : health.connected ? 'live' : health.connected === false && health.configured ? 'failing' : health.configured ? 'ready' : 'off'; const styles: Record = { verifying: 'border-amber-500/50 animate-pulse text-amber-300', live: 'border-green-500 text-green-300 shadow-[0_0_18px_rgba(34,197,94,0.4)]', ready: 'border-teal-500/40 text-teal-300', failing: 'border-rose-500/60 text-rose-300', off: 'border-dashed border-gray-700 text-gray-500', }; return (
{state === 'live' && } {source.label}
); }; const RenderFlow: React.FC = () => { const [health, setHealth] = useState>({}); const [company, setCompany] = useState('Acme Web Shop'); const [product, setProduct] = useState('Acme Storefront'); const [businessCase, setBusinessCase] = useState('usability_test'); const [count, setCount] = useState(16); const [hubId, setHubId] = useState(''); const [nodes, setNodes] = useState([]); const [target, setTarget] = useState(0); const [rendering, setRendering] = useState(false); const [enriching, setEnriching] = useState(false); const [enrichNote, setEnrichNote] = useState(''); const [selected, setSelected] = useState(null); const llm = useLlmConfig(); const pollRef = useRef | null>(null); const enrich = async () => { setEnriching(true); setEnrichNote(''); try { const env = await api(`/api/personas/${hubId}/enrich`, { body: { batch_size: 10, concurrency: 8 } }); const graph = await apiData(`/api/personas/${hubId}/graph`); setNodes(graph.nodes || []); setEnrichNote(`Enriched ${env.data.enriched} personas · ${env.data.llm_calls} LLM calls · ${env.data.llm}`); } catch (e: any) { setEnrichNote(e.status === 422 ? 'Configure a BYOK text model to enrich (Dev → settings).' : e.message); } finally { setEnriching(false); } }; useEffect(() => { SOURCES.forEach((s) => apiData(`/api/connectors/${s.id}/verify`) .then((h) => setHealth((prev) => ({ ...prev, [s.id]: h }))) .catch(() => undefined), ); }, []); const anyGreen = Object.values(health).some((h) => h.connected) || true; // synthetic always allowed const render = async () => { setRendering(true); setNodes([]); setSelected(null); try { const env = await api('/api/personas/generate', { body: { company_name: company, product_name: product, business_case: businessCase, count, stream: true }, }); const id = env.artifact_id!; setHubId(id); setTarget(count); if (pollRef.current) clearInterval(pollRef.current); pollRef.current = setInterval(async () => { try { const graph = await apiData(`/api/personas/${id}/graph`); setNodes(graph.nodes || []); if ((graph.nodes || []).length >= count && pollRef.current) { clearInterval(pollRef.current); setRendering(false); } } catch { /* keep polling */ } }, 400); } catch { setRendering(false); } }; useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current); }, []); const cols = Math.ceil(Math.sqrt(Math.max(1, target))); return (
{/* Authored "Test setup" — editable, company/test-case scoped */}
Test setup (authored · editable · this test case)
setCompany(e.target.value)} placeholder="Company" className="rounded-lg border border-gray-800 bg-black p-2 text-xs outline-none focus:border-teal-500" /> setProduct(e.target.value)} placeholder="Product" className="rounded-lg border border-gray-800 bg-black p-2 text-xs outline-none focus:border-teal-500" />
Personas setCount(+e.target.value)} className="flex-1 accent-teal-500" /> {count}
Change → Render → new cohort
{/* Source flow diagram */}
{SOURCES.map((s) => )}
Unify
OASIS generator
{/* The graph canvas — personas appear one by one */}
{nodes.length === 0 ? (

Press Render — personas grow into the graph one by one as OASIS generates them.

) : ( <>
{rendering ? `Shaping persona ${nodes.length} of ${target}…` : `${nodes.length} personas`} {nodes.at(-1)?.provenance?.datahub_snapshot_ids?.length ? ' · shaped by DataHub cohort' : ' · synthetic'} {!rendering && hubId && ( )}
{!rendering && hubId && !llm.textConfigured && (
)} {enrichNote &&
{enrichNote}
}
{nodes.map((node) => ( ))}
)}
{/* Per-persona READ-ONLY discovered steering card */}
{!selected ? (

Select a persona to see how it steers the agent.

) : (
{selected.label}
{selected.profile?.age} · {selected.profile?.gender} · {selected.profile?.mbti} · {selected.profile?.country}
🔒 backend · read-only steering

{selected.profile?.persona}

{selected.discovered_steering && (
Because this persona is…
{Object.entries(selected.discovered_steering.observing || {}).map(([k, v]) => (
{k.replace(/_/g, ' ')} {JSON.stringify(v)}
))}
allowed actions {(selected.discovered_steering.acting_allowed || []).length}
frustration abort {selected.discovered_steering.frustration_abort} steps
)} {selected.provenance?.datahub_snapshot_ids?.length > 0 && (
shaped by: {selected.provenance.datahub_snapshot_ids.join(', ')}
)}
)}
); }; export default RenderFlow;