Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>NWO Agent Graph</title> | |
| <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script> | |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> | |
| <script src="https://unpkg.com/@supabase/supabase-js@2/dist/umd/supabase.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/react-force-graph-3d"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/react-force-graph-2d"></script> | |
| <style> | |
| :root{ | |
| --bg:#0a130a; | |
| --bg-2:#0d1a0d; | |
| --bg-3:#112011; | |
| --panel:#0e180e; | |
| --border:rgba(151,196,89,0.18); | |
| --border-strong:rgba(151,196,89,0.35); | |
| --text:#ffffff; | |
| --text-2:#cfe0cf; | |
| --text-3:#9db09d; | |
| --text-4:#5a6b5a; | |
| --brand:#97c459; | |
| --brand-deep:#1D9E75; | |
| --brand-soft:rgba(151,196,89,0.12); | |
| --brand-mid:rgba(151,196,89,0.22); | |
| --alert:#c4a459; | |
| --critical:#c95959; | |
| } | |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} | |
| html,body,#root{height:100%;width:100%;overflow:hidden} | |
| body{background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,sans-serif;font-size:14px} | |
| ::-webkit-scrollbar{width:5px;height:5px} | |
| ::-webkit-scrollbar-track{background:var(--bg-2)} | |
| ::-webkit-scrollbar-thumb{background:rgba(151,196,89,0.25);border-radius:2px} | |
| ::-webkit-scrollbar-thumb:hover{background:rgba(151,196,89,0.45)} | |
| input,select,textarea{background:var(--bg-2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;padding:8px 10px;outline:none;font-family:inherit;width:100%} | |
| input:focus,select:focus,textarea:focus{border-color:var(--brand)} | |
| button{cursor:pointer;font-family:inherit} | |
| button:disabled{opacity:.4;cursor:not-allowed} | |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script> | |
| const CFG = { | |
| supabaseUrl: 'https://kbweprgbawghpzfpxiav.supabase.co', | |
| supabaseAnon: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtid2VwcmdiYXdnaHB6ZnB4aWF2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzYwMjM4MjcsImV4cCI6MjA5MTU5OTgyN30.fxtsT5JZ0bV_GgTuMi_0RqEAAdnGctmPdxLy-fSQ4u4', | |
| }; | |
| const sb = supabase.createClient(CFG.supabaseUrl, CFG.supabaseAnon, { | |
| auth:{persistSession:true,autoRefreshToken:true} | |
| }); | |
| (async function(){ | |
| const hash = window.location.hash; | |
| if(!hash.includes('access_token')) return; | |
| const p = new URLSearchParams(hash.slice(1)); | |
| const at = p.get('access_token'), rt = p.get('refresh_token'); | |
| if(at && rt){ | |
| try{ await sb.auth.setSession({access_token:at, refresh_token:rt}); }catch(e){} | |
| history.replaceState(null,'',window.location.pathname); | |
| } | |
| })(); | |
| const ACTOR_COLOR = {human:'#97c459', agent:'#1D9E75', robot:'#4a8f5c', cron:'#2d5a3f'}; | |
| const actorColor = t => ACTOR_COLOR[t] || '#688068'; | |
| const ACTOR_ICON = {human:'👤', agent:'✦', robot:'⬡', cron:'⏱'}; | |
| const {createElement:h,useState,useEffect,useRef,useCallback,useMemo} = React; | |
| const ForceGraph3D = window.ForceGraph3D; | |
| const ForceGraph2D = window.ForceGraph2D; | |
| function LoginPage(){ | |
| const [email,setEmail]=useState(''); | |
| const [sent,setSent]=useState(false); | |
| const [loading,setLoading]=useState(false); | |
| const [err,setErr]=useState(''); | |
| async function send(){ | |
| if(!email.trim())return; | |
| setLoading(true);setErr(''); | |
| const{error}=await sb.auth.signInWithOtp({email:email.trim(),options:{emailRedirectTo:"https://huggingface.co/spaces/CPater/nwo-agent-graph"}}); | |
| if(error)setErr(error.message);else setSent(true); | |
| setLoading(false); | |
| } | |
| return h('div',{style:{position:'fixed',inset:0,background:'rgba(10,19,10,0.92)',backdropFilter:'blur(4px)',display:'flex',alignItems:'center',justifyContent:'center',padding:16,zIndex:200}}, | |
| h('div',{style:{background:'var(--panel)',border:'1px solid var(--border-strong)',borderRadius:16,padding:'36px 32px',width:380,maxWidth:'100%',boxShadow:'0 20px 60px rgba(0,0,0,0.5)'}}, | |
| h('div',{style:{fontSize:28,textAlign:'center',marginBottom:4,color:'var(--brand)'}},'⬡'), | |
| h('div',{style:{fontSize:20,fontWeight:500,textAlign:'center',marginBottom:6,color:'var(--text)'}},'NWO Agent Graph'), | |
| h('div',{style:{fontSize:13,color:'var(--text-3)',textAlign:'center',marginBottom:28}},'Sign in to access your private graph and manage robots'), | |
| err&&h('div',{style:{padding:'8px 12px',borderRadius:6,background:'rgba(201,89,89,0.12)',border:'1px solid rgba(201,89,89,0.35)',color:'var(--critical)',fontSize:12,marginBottom:12}},err), | |
| sent | |
| ?h('div',{style:{padding:'10px 12px',borderRadius:8,background:'var(--brand-soft)',border:'1px solid var(--border-strong)',color:'var(--brand)',fontSize:13}},'✉ Magic link sent — check your email') | |
| :h('div',null, | |
| h('label',{style:{fontSize:11,color:'var(--text-3)',display:'block',marginBottom:4,letterSpacing:1}},'EMAIL'), | |
| h('input',{type:'email',placeholder:'you@example.com',value:email,onChange:e=>setEmail(e.target.value),onKeyDown:e=>e.key==='Enter'&&send(),autoFocus:true,style:{marginBottom:12}}), | |
| h('button',{onClick:send,disabled:loading||!email,style:{width:'100%',padding:'11px 0',borderRadius:8,border:'1px solid var(--border-strong)',background:'var(--brand-soft)',color:'var(--brand)',fontSize:14,fontWeight:500}},loading?'Sending…':'Send magic link')), | |
| h('div',{style:{marginTop:20,fontSize:11,color:'var(--text-4)',textAlign:'center',lineHeight:1.6}},'Public graph is visible without signing in') | |
| ) | |
| ); | |
| } | |
| function ActorBadge({type,name,compact}){ | |
| const icon = ACTOR_ICON[type] || '?'; | |
| const color = actorColor(type); | |
| return h('span',{style:{display:'inline-flex',alignItems:'center',gap:3,padding:compact?'1px 6px':'2px 8px',borderRadius:4,fontSize:compact?10:11,fontWeight:500,background:'rgba(151,196,89,0.08)',color:'var(--text-2)',border:'1px solid var(--border)'}}, | |
| h('span',{style:{color}}, icon),' ',name||type); | |
| } | |
| function FeedPanel({userId,onNodeSelect}){ | |
| const [posts,setPosts]=useState([]); | |
| const [filter,setFilter]=useState('all'); | |
| const [draft,setDraft]=useState(''); | |
| const [sending,setSending]=useState(false); | |
| useEffect(()=>{ | |
| sb.from('graph_posts') | |
| .select('id,content,actor_type,nwo_agent_id,created_at,metadata,node_id,visibility,graph_nodes(name,category,color)') | |
| .order('created_at',{ascending:false}).limit(60) | |
| .then(({data})=>setPosts(data||[])); | |
| const ch=sb.channel('feed_rt') | |
| .on('postgres_changes',{event:'INSERT',schema:'public',table:'graph_posts'},p=>{ | |
| setPosts(prev=>[p.new,...prev].slice(0,200)); | |
| }).subscribe(); | |
| return()=>sb.removeChannel(ch); | |
| },[]); | |
| async function post(){ | |
| if(!draft.trim())return; | |
| setSending(true); | |
| await sb.from('graph_posts').insert({content:draft.trim(),actor_type:'human',actor_id:userId,visibility:'public'}); | |
| setDraft('');setSending(false); | |
| } | |
| const FILTERS=['all','human','agent','robot']; | |
| const visible=filter==='all'?posts:posts.filter(p=>p.actor_type===filter); | |
| return h('div',{style:{display:'flex',flexDirection:'column',height:'100%',background:'var(--panel)',borderLeft:'1px solid var(--border)'}}, | |
| h('div',{style:{padding:'10px 14px',borderBottom:'1px solid var(--border)',display:'flex',alignItems:'center',justifyContent:'space-between'}}, | |
| h('span',{style:{fontSize:11,fontWeight:600,color:'var(--text-3)',letterSpacing:1}},'FEED'), | |
| h('span',{style:{fontSize:10,color:'var(--text-4)'}},posts.length,' posts') | |
| ), | |
| h('div',{style:{display:'flex',gap:4,padding:'6px 10px',borderBottom:'1px solid var(--border)',flexWrap:'wrap'}}, | |
| FILTERS.map(f=>h('button',{key:f,onClick:()=>setFilter(f),style:{padding:'2px 8px',borderRadius:4,fontSize:10,border:`1px solid ${filter===f?'var(--brand)':'var(--border)'}`,background:filter===f?'var(--brand-soft)':'transparent',color:filter===f?'var(--brand)':'var(--text-3)'}},f,' (',f==='all'?posts.length:posts.filter(p=>p.actor_type===f).length,')')) | |
| ), | |
| h('div',{style:{flex:1,overflowY:'auto'}}, | |
| visible.length===0&&h('div',{style:{padding:20,color:'var(--text-4)',fontSize:12}},'No posts yet.'), | |
| visible.map(p=>h('div',{key:p.id,style:{padding:'10px 14px',borderBottom:'1px solid var(--border)'}}, | |
| h('div',{style:{display:'flex',alignItems:'center',gap:6,marginBottom:5,flexWrap:'wrap'}}, | |
| h(ActorBadge,{type:p.actor_type,name:p.nwo_agent_id?.slice(0,10),compact:true}), | |
| h('span',{style:{fontSize:10,color:'var(--text-4)',marginLeft:'auto'}},new Date(p.created_at).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})) | |
| ), | |
| h('div',{style:{fontSize:13,color:'var(--text-2)',lineHeight:1.5}},p.content), | |
| p.graph_nodes?.name&&h('div',{onClick:()=>onNodeSelect&&onNodeSelect(p.node_id),style:{fontSize:11,color:'var(--brand)',marginTop:4,cursor:'pointer'}},'→ ',p.graph_nodes.name), | |
| p.visibility==='private'&&h('span',{style:{fontSize:9,color:'var(--text-4)'}},' 🔒') | |
| )) | |
| ), | |
| h('div',{style:{padding:'10px 14px',borderTop:'1px solid var(--border)'}}, | |
| h('textarea',{rows:2,placeholder:'Post to the graph feed…',value:draft,onChange:e=>setDraft(e.target.value),onKeyDown:e=>e.key==='Enter'&&e.ctrlKey&&post(),style:{marginBottom:6,resize:'none'}}), | |
| h('button',{onClick:post,disabled:sending||!draft.trim(),style:{padding:'5px 14px',borderRadius:5,border:'1px solid var(--border-strong)',background:'var(--brand-soft)',color:'var(--brand)',fontSize:12}},sending?'Posting…':'Post ↵') | |
| ) | |
| ); | |
| } | |
| function GraphCanvas({nodes,links,onNodeClick,width,height,mode}){ | |
| const fgRef=useRef(null); | |
| const data=useMemo(()=>({ | |
| nodes:nodes.map(n=>({...n,_color:n.color||actorColor(n.actor_type)})), | |
| links:links.map(l=>({...l})) | |
| }),[nodes,links]); | |
| if(mode==='3d' && ForceGraph3D){ | |
| return h(ForceGraph3D,{ | |
| ref:fgRef, | |
| graphData:data, | |
| width:width, | |
| height:height, | |
| backgroundColor:'#0a130a', | |
| nodeId:'id', | |
| nodeLabel:n=>`${n.name}${n.description?'\n'+n.description.slice(0,120):''}`, | |
| nodeRelSize:6, | |
| nodeVal:n=>(n.val||1)*1.5, | |
| nodeColor:n=>n._color, | |
| nodeOpacity:0.92, | |
| nodeResolution:16, | |
| linkColor:()=>'rgba(151,196,89,0.35)', | |
| linkWidth:l=>Math.max(0.5,(l.value||0.5)*1.5), | |
| linkOpacity:0.45, | |
| linkDirectionalParticles:2, | |
| linkDirectionalParticleSpeed:0.004, | |
| linkDirectionalParticleWidth:1.2, | |
| linkDirectionalParticleColor:()=>'#97c459', | |
| onNodeClick:onNodeClick, | |
| onNodeDragEnd:node=>{ | |
| node.fx=node.x; node.fy=node.y; node.fz=node.z; | |
| sb.from('graph_nodes').update({x_position:node.x,y_position:node.y}).eq('id',node.id); | |
| }, | |
| enableNodeDrag:true, | |
| enableNavigationControls:true, | |
| showNavInfo:false, | |
| warmupTicks:50, | |
| cooldownTicks:100, | |
| }); | |
| } | |
| if(ForceGraph2D){ | |
| return h(ForceGraph2D,{ | |
| ref:fgRef, | |
| graphData:data, | |
| width:width, | |
| height:height, | |
| backgroundColor:'#0a130a', | |
| nodeId:'id', | |
| nodeLabel:n=>n.name, | |
| nodeRelSize:6, | |
| nodeVal:n=>(n.val||1)*1.5, | |
| nodeColor:n=>n._color, | |
| linkColor:()=>'rgba(151,196,89,0.3)', | |
| linkWidth:l=>Math.max(0.5,(l.value||0.5)*1.5), | |
| onNodeClick:onNodeClick, | |
| nodeCanvasObjectMode:()=>'after', | |
| nodeCanvasObject:(node,ctx,scale)=>{ | |
| const label = node.name && node.name.length > 22 ? node.name.slice(0,20)+'…' : (node.name||''); | |
| const r = Math.sqrt((node.val||1)*1.5) * 6; | |
| ctx.font = `${11/scale}px system-ui,sans-serif`; | |
| ctx.fillStyle = 'rgba(255,255,255,0.8)'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(label, node.x, node.y + r + 10/scale); | |
| } | |
| }); | |
| } | |
| return h('div',{style:{padding:20,color:'var(--text-3)'}},'Loading graph library…'); | |
| } | |
| function NodeDetail({node,onClose,userId,onTogglePrivacy,onNodeSelect,allNodes,allLinks}){ | |
| const [msg,setMsg]=useState(''); | |
| const [children,setChildren]=useState([]); | |
| const [grandchildren,setGrandchildren]=useState({}); | |
| const [loadingChildren,setLoadingChildren]=useState(true); | |
| const isOwner=userId&&node.owner_user_id===userId; | |
| const isPrivate=node.visibility==='private'; | |
| useEffect(()=>{ | |
| setLoadingChildren(true); | |
| const directLinks=(allLinks||[]).filter(l=>{ | |
| const src=l.source?.id||l.source; | |
| return src===node.id; | |
| }); | |
| const childIds=new Set(directLinks.map(l=>l.target?.id||l.target)); | |
| const childNodes=(allNodes||[]).filter(n=>childIds.has(n.id)); | |
| setChildren(childNodes); | |
| const gc={}; | |
| childNodes.forEach(child=>{ | |
| const gcLinks=(allLinks||[]).filter(l=>(l.source?.id||l.source)===child.id); | |
| const gcIds=new Set(gcLinks.map(l=>l.target?.id||l.target)); | |
| gc[child.id]=(allNodes||[]).filter(n=>gcIds.has(n.id)); | |
| }); | |
| setGrandchildren(gc); | |
| setLoadingChildren(false); | |
| },[node.id,allNodes,allLinks]); | |
| async function requestExpand(){ | |
| setMsg('Queuing…'); | |
| let queued=false; | |
| try{ | |
| const r=await fetch('https://redciprianpater-nwo-agent-graph.hf.space/graph/expand',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({node_id:node.id})}); | |
| if(r.ok){queued=true;} | |
| }catch(e){} | |
| if(!queued){ | |
| const{error}=await sb.from('graph_nodes').update({expand_requested:true,expand_done:false}).eq('id',node.id); | |
| if(!error)queued=true; | |
| } | |
| setMsg(queued?'✦ Queued — new nodes appear within 60 seconds':'Could not queue — try signing in first'); | |
| setTimeout(()=>setMsg(''),6000); | |
| } | |
| async function togglePrivacy(){ | |
| const v=isPrivate?'public':'private'; | |
| await sb.from('graph_nodes').update({visibility:v}).eq('id',node.id); | |
| onTogglePrivacy?.(node.id,v); | |
| setMsg('Visibility updated');setTimeout(()=>setMsg(''),3000); | |
| } | |
| return h('div',{style:{position:'absolute',right:0,top:0,bottom:0,width:320,background:'var(--panel)',borderLeft:'1px solid var(--border-strong)',overflowY:'auto',zIndex:10,boxShadow:'-12px 0 28px rgba(0,0,0,0.35)'}}, | |
| h('div',{style:{padding:'14px 16px',borderBottom:'1px solid var(--border)',position:'sticky',top:0,background:'var(--panel)',zIndex:1}}, | |
| h('button',{onClick:onClose,style:{position:'absolute',top:12,right:12,background:'none',border:'none',color:'var(--text-3)',fontSize:22,cursor:'pointer',lineHeight:1}},'×'), | |
| h('div',{style:{marginBottom:8}},h(ActorBadge,{type:node.actor_type})), | |
| h('div',{style:{fontSize:16,fontWeight:500,color:'var(--text)',marginBottom:4,paddingRight:24}},node.name), | |
| h('div',{style:{display:'flex',gap:8,alignItems:'center',flexWrap:'wrap'}}, | |
| node.category&&h('span',{style:{fontSize:10,color:'var(--text-3)',background:'var(--brand-soft)',padding:'2px 6px',borderRadius:3,border:'1px solid var(--border)'}},node.category), | |
| h('span',{style:{fontSize:10,color:isPrivate?'var(--brand)':'var(--text-4)'}},isPrivate?'🔒 Private':'🌐 Public') | |
| ) | |
| ), | |
| h('div',{style:{padding:'12px 16px'}}, | |
| node.description&&h('div',{style:{fontSize:12,color:'var(--text-2)',lineHeight:1.6,marginBottom:12,background:'var(--bg-2)',padding:'8px 10px',borderRadius:6,border:'1px solid var(--border)'}},node.description), | |
| node.battery_level!=null&&h('div',{style:{marginBottom:12}}, | |
| h('div',{style:{fontSize:10,color:'var(--text-3)',marginBottom:4,letterSpacing:1}},'BATTERY'), | |
| h('div',{style:{height:5,borderRadius:3,background:'var(--bg-3)',overflow:'hidden',marginBottom:3}}, | |
| h('div',{style:{height:'100%',width:`${node.battery_level}%`,background:node.battery_level<20?'var(--critical)':node.battery_level<50?'var(--alert)':'var(--brand)',borderRadius:3}}) | |
| ), | |
| h('div',{style:{fontSize:11,color:'var(--text-2)'}},node.battery_level.toFixed(0),'%') | |
| ), | |
| node.nwo_agent_id&&h('div',{style:{marginBottom:12}}, | |
| h('div',{style:{fontSize:10,color:'var(--text-3)',marginBottom:3,letterSpacing:1}},'NWO AGENT'), | |
| h('div',{style:{fontSize:10,color:'var(--text-2)',fontFamily:'monospace',wordBreak:'break-all',background:'var(--bg-2)',padding:'4px 8px',borderRadius:4,border:'1px solid var(--border)'}},node.nwo_agent_id) | |
| ), | |
| h('div',{style:{marginBottom:12}}, | |
| h('div',{style:{fontSize:10,color:'var(--text-3)',letterSpacing:1,marginBottom:8,display:'flex',alignItems:'center',justifyContent:'space-between'}}, | |
| 'EXPANSION TREE', | |
| h('span',{style:{fontSize:10,color:children.length?'var(--brand)':'var(--text-4)'}},children.length,' nodes') | |
| ), | |
| loadingChildren&&h('div',{style:{fontSize:11,color:'var(--text-4)',padding:'6px 0'}},'Loading…'), | |
| children.length===0&&!loadingChildren&&h('div',{style:{fontSize:11,color:'var(--text-4)',padding:'8px 10px',border:'1px dashed var(--border)',borderRadius:6,textAlign:'center',lineHeight:1.5}}, | |
| 'No child nodes yet.',h('br'), | |
| h('span',{style:{fontSize:10}},'Click "Queue BitNet expansion" below') | |
| ), | |
| children.map(child=>{ | |
| const gc=grandchildren[child.id]||[]; | |
| const cColor=child.color||actorColor(child.actor_type); | |
| return h('div',{key:child.id,style:{marginBottom:6}}, | |
| h('div',{onClick:()=>onNodeSelect&&onNodeSelect(child), | |
| style:{display:'flex',alignItems:'flex-start',gap:8,padding:'7px 10px',borderRadius:6,background:'var(--bg-2)',border:'1px solid var(--border)',cursor:'pointer',transition:'background 0.15s'}, | |
| onMouseEnter:e=>e.currentTarget.style.background='var(--bg-3)', | |
| onMouseLeave:e=>e.currentTarget.style.background='var(--bg-2)'}, | |
| h('div',{style:{width:8,height:8,borderRadius:'50%',background:cColor,flexShrink:0,marginTop:3}}), | |
| h('div',{style:{flex:1,minWidth:0}}, | |
| h('div',{style:{fontSize:12,color:'var(--text)',fontWeight:500,marginBottom:2,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}},child.name), | |
| child.description&&h('div',{style:{fontSize:10,color:'var(--text-3)',lineHeight:1.4,overflow:'hidden',textOverflow:'ellipsis',display:'-webkit-box',WebkitLineClamp:2,WebkitBoxOrient:'vertical'}},child.description) | |
| ), | |
| child.category&&h('span',{style:{fontSize:9,color:'var(--text-3)',background:'var(--brand-soft)',padding:'1px 4px',borderRadius:2,flexShrink:0}},child.category) | |
| ), | |
| gc.length>0&&h('div',{style:{marginLeft:16,marginTop:3}}, | |
| gc.map(gc_node=>h('div',{key:gc_node.id,onClick:()=>onNodeSelect&&onNodeSelect(gc_node), | |
| style:{display:'flex',alignItems:'center',gap:6,padding:'5px 8px',borderRadius:4,background:'var(--bg-2)',border:'1px solid var(--border)',cursor:'pointer',marginBottom:3}, | |
| onMouseEnter:e=>e.currentTarget.style.background='var(--bg-3)', | |
| onMouseLeave:e=>e.currentTarget.style.background='var(--bg-2)'}, | |
| h('div',{style:{width:6,height:6,borderRadius:'50%',background:gc_node.color||actorColor(gc_node.actor_type),flexShrink:0}}), | |
| h('div',{style:{fontSize:11,color:'var(--text-2)',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',flex:1}},gc_node.name) | |
| )) | |
| ) | |
| ); | |
| }) | |
| ), | |
| h('div',{style:{borderTop:'1px solid var(--border)',paddingTop:12}}, | |
| h('div',{style:{fontSize:10,color:'var(--text-3)',letterSpacing:1,marginBottom:8}},'ACTIONS'), | |
| h('button',{onClick:requestExpand,style:{display:'block',width:'100%',padding:'8px 10px',marginBottom:6,borderRadius:6,border:'1px solid var(--border-strong)',background:'var(--brand-soft)',color:'var(--brand)',fontSize:12,textAlign:'left'}},'✦ Queue BitNet expansion'), | |
| (isOwner||!userId)&&h('button',{onClick:togglePrivacy,style:{display:'block',width:'100%',padding:'8px 10px',marginBottom:6,borderRadius:6,border:`1px solid ${isPrivate?'var(--border-strong)':'var(--border)'}`,background:isPrivate?'var(--brand-soft)':'transparent',color:isPrivate?'var(--brand)':'var(--text-3)',fontSize:12,textAlign:'left'}},isPrivate?'🌐 Make public':'🔒 Make private'), | |
| msg&&h('div',{style:{marginTop:6,padding:'6px 10px',borderRadius:5,background:'var(--bg-2)',border:'1px solid var(--border)',fontSize:11,color:'var(--text-2)'}},msg) | |
| ) | |
| ) | |
| ); | |
| } | |
| function AddNodeModal({onAdd,onClose}){ | |
| const [name,setName]=useState(''); | |
| const [desc,setDesc]=useState(''); | |
| const [cat,setCat]=useState('topic'); | |
| const [priv,setPriv]=useState(false); | |
| const [saving,setSaving]=useState(false); | |
| async function add(){ | |
| if(!name.trim())return; | |
| setSaving(true); | |
| await onAdd(name.trim(),desc.trim(),cat,priv); | |
| setSaving(false);onClose(); | |
| } | |
| return h('div',{onClick:e=>e.target===e.currentTarget&&onClose(), | |
| style:{position:'fixed',inset:0,background:'rgba(10,19,10,0.8)',backdropFilter:'blur(4px)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:100}}, | |
| h('div',{style:{background:'var(--panel)',border:'1px solid var(--border-strong)',borderRadius:14,padding:24,width:380,maxWidth:'90vw',boxShadow:'0 20px 60px rgba(0,0,0,0.5)'}}, | |
| h('div',{style:{fontSize:15,fontWeight:500,color:'var(--text)',marginBottom:16}},'Add graph node'), | |
| h('label',{style:{fontSize:11,color:'var(--text-3)',display:'block',marginBottom:4,letterSpacing:1}},'NAME'), | |
| h('input',{value:name,onChange:e=>setName(e.target.value),placeholder:'Topic or concept',autoFocus:true,style:{marginBottom:10}}), | |
| h('label',{style:{fontSize:11,color:'var(--text-3)',display:'block',marginBottom:4,letterSpacing:1}},'DESCRIPTION'), | |
| h('textarea',{rows:2,value:desc,onChange:e=>setDesc(e.target.value),placeholder:'Optional',style:{marginBottom:10,resize:'none'}}), | |
| h('label',{style:{fontSize:11,color:'var(--text-3)',display:'block',marginBottom:4,letterSpacing:1}},'CATEGORY'), | |
| h('select',{value:cat,onChange:e=>setCat(e.target.value),style:{marginBottom:10}},['topic','event','observation','task','inference'].map(c=>h('option',{key:c,value:c},c))), | |
| h('label',{style:{display:'flex',alignItems:'center',gap:8,marginBottom:16,cursor:'pointer',fontSize:12,color:'var(--text-2)'}}, | |
| h('input',{type:'checkbox',checked:priv,onChange:e=>setPriv(e.target.checked),style:{width:'auto'}}), | |
| '🔒 Private node (only visible to you)' | |
| ), | |
| h('div',{style:{display:'flex',gap:8,justifyContent:'flex-end'}}, | |
| h('button',{onClick:onClose,style:{padding:'7px 16px',borderRadius:6,border:'1px solid var(--border)',background:'transparent',color:'var(--text-3)',fontSize:13}},'Cancel'), | |
| h('button',{onClick:add,disabled:!name.trim()||saving,style:{padding:'7px 16px',borderRadius:6,border:'1px solid var(--border-strong)',background:'var(--brand-soft)',color:'var(--brand)',fontSize:13}},saving?'Adding…':'Add node') | |
| ) | |
| ) | |
| ); | |
| } | |
| function App(){ | |
| const [user,setUser]=useState(undefined); | |
| const [graphData,setGraphData]=useState({nodes:[],links:[]}); | |
| const [filter,setFilter]=useState('all'); | |
| const [selected,setSelected]=useState(null); | |
| const [showAdd,setShowAdd]=useState(false); | |
| const [showLogin,setShowLogin]=useState(false); | |
| const [showFeed,setShowFeed]=useState(true); | |
| const [liveMode,setLiveMode]=useState(false); | |
| const [viewMode,setViewMode]=useState('3d'); | |
| const [dims,setDims]=useState({w:800,h:600}); | |
| const canvasWrap=useRef(null); | |
| useEffect(()=>{ | |
| sb.auth.getSession().then(({data:{session}})=>setUser(session?.user||null)); | |
| const{data:{subscription}}=sb.auth.onAuthStateChange((_,s)=>setUser(s?.user||null)); | |
| return()=>subscription.unsubscribe(); | |
| },[]); | |
| useEffect(()=>{loadGraph();},[user]); | |
| async function loadGraph(){ | |
| let nodes=null,links=null; | |
| try{ | |
| const r=await sb.from('graph_nodes').select('id,name,val,color,depth_level,description,category,actor_type,visibility,owner_user_id,x_position,y_position,nwo_agent_id,battery_level,created_at').order('created_at',{ascending:false}).limit(500); | |
| nodes=r.data; | |
| }catch(e){ | |
| const r=await sb.from('graph_nodes').select('id,name,val,color,depth_level,description,category,actor_type,x_position,y_position,nwo_agent_id,battery_level,created_at').order('created_at',{ascending:false}).limit(500); | |
| nodes=r.data; | |
| } | |
| try{ | |
| const r=await sb.from('graph_links').select('source_id,target_id,similarity_score,link_type').limit(1000); | |
| links=r.data; | |
| }catch(e){links=[];} | |
| setGraphData({ | |
| nodes:(nodes||[]).map(n=>({...n,x:n.x_position,y:n.y_position,val:parseFloat(n.val)||1})), | |
| links:(links||[]).map(l=>({source:l.source_id,target:l.target_id,value:parseFloat(l.similarity_score)||0.5})) | |
| }); | |
| } | |
| useEffect(()=>{ | |
| const nodeCh=sb.channel('nodes_rt') | |
| .on('postgres_changes',{event:'INSERT',schema:'public',table:'graph_nodes'},p=>{ | |
| setLiveMode(true); | |
| const newNode={...p.new,x:p.new.x_position||null,y:p.new.y_position||null,val:parseFloat(p.new.val)||1}; | |
| setGraphData(prev=>({ | |
| nodes:prev.nodes.find(n=>n.id===newNode.id)?prev.nodes:[newNode,...prev.nodes].slice(0,500), | |
| links:prev.links | |
| })); | |
| }).subscribe(); | |
| const linkCh=sb.channel('links_rt') | |
| .on('postgres_changes',{event:'INSERT',schema:'public',table:'graph_links'},p=>{ | |
| const newLink={source:p.new.source_id,target:p.new.target_id,value:parseFloat(p.new.similarity_score)||0.5,link_type:p.new.link_type}; | |
| setGraphData(prev=>({ | |
| nodes:prev.nodes, | |
| links:prev.links.find(l=>(l.source?.id||l.source)===newLink.source&&(l.target?.id||l.target)===newLink.target)?prev.links:[...prev.links,newLink] | |
| })); | |
| }).subscribe(); | |
| const poll=setInterval(()=>{ | |
| sb.from('graph_nodes').select('id,name,val,color,depth_level,description,category,actor_type,visibility,owner_user_id,x_position,y_position,nwo_agent_id,battery_level,created_at').order('created_at',{ascending:false}).limit(500) | |
| .then(({data})=>{ | |
| if(!data)return; | |
| setGraphData(prev=>{ | |
| const existingIds=new Set(prev.nodes.map(n=>n.id)); | |
| const newNodes=data.filter(n=>!existingIds.has(n.id)).map(n=>({...n,x:n.x_position||null,y:n.y_position||null,val:parseFloat(n.val)||1})); | |
| if(newNodes.length===0)return prev; | |
| setLiveMode(true); | |
| return{nodes:[...newNodes,...prev.nodes].slice(0,500),links:prev.links}; | |
| }); | |
| sb.from('graph_links').select('source_id,target_id,similarity_score,link_type').limit(1000) | |
| .then(({data:ldata})=>{ | |
| if(!ldata)return; | |
| setGraphData(prev=>({nodes:prev.nodes,links:ldata.map(l=>({source:l.source_id,target:l.target_id,value:parseFloat(l.similarity_score)||0.5,link_type:l.link_type}))})); | |
| }); | |
| }); | |
| },30000); | |
| return()=>{sb.removeChannel(nodeCh);sb.removeChannel(linkCh);clearInterval(poll);}; | |
| },[]); | |
| useEffect(()=>{ | |
| const obs=new ResizeObserver(e=>{ | |
| const{width,height}=e[0].contentRect; | |
| setDims({w:width,h:height}); | |
| }); | |
| if(canvasWrap.current)obs.observe(canvasWrap.current); | |
| return()=>obs.disconnect(); | |
| },[]); | |
| const visible=filter==='all'?graphData.nodes:graphData.nodes.filter(n=>n.actor_type===filter); | |
| const vids=new Set(visible.map(n=>n.id)); | |
| const vlinks=graphData.links.filter(l=>vids.has(l.source?.id||l.source)&&vids.has(l.target?.id||l.target)); | |
| async function addNode(name,desc,cat,isPrivate){ | |
| const{data}=await sb.from('graph_nodes').insert({ | |
| name,description:desc,category:cat,val:2,color:'#97c459', | |
| depth_level:0,actor_type:'human',user_id:user?.id, | |
| owner_user_id:user?.id,visibility:isPrivate?'private':'public', | |
| expand_requested:true,expand_done:false | |
| }).select().single(); | |
| if(data)setGraphData(prev=>({nodes:[{...data,x:null,y:null,val:2},...prev.nodes],links:prev.links})); | |
| } | |
| function handleTogglePrivacy(id,vis){ | |
| setGraphData(prev=>({...prev,nodes:prev.nodes.map(n=>n.id===id?{...n,visibility:vis}:n)})); | |
| if(selected?.id===id)setSelected(s=>({...s,visibility:vis})); | |
| } | |
| if(user===undefined)return h('div',{style:{height:'100vh',display:'flex',alignItems:'center',justifyContent:'center',color:'var(--text-3)'}},'Loading…'); | |
| return h('div',{style:{display:'flex',flexDirection:'column',height:'100vh',background:'var(--bg)'}}, | |
| h('div',{style:{display:'flex',alignItems:'center',gap:10,padding:'6px 14px',background:'var(--bg-2)',borderBottom:'1px solid var(--border)',flexWrap:'wrap'}}, | |
| h('span',{style:{fontSize:16,color:'var(--brand)'}},'⬡'), | |
| h('span',{style:{fontSize:13,fontWeight:500,color:'var(--text)'}},'NWO Agent Graph'), | |
| h('div',{style:{width:1,height:16,background:'var(--border)'}}), | |
| ['all','human','agent','robot'].map(f=>h('button',{key:f,onClick:()=>setFilter(f),style:{padding:'3px 10px',borderRadius:4,border:`1px solid ${filter===f?'var(--brand)':'var(--border)'}`,background:filter===f?'var(--brand-soft)':'transparent',color:filter===f?'var(--brand)':'var(--text-3)',fontSize:11}},f)), | |
| h('button',{onClick:()=>user?setShowAdd(true):setShowLogin(true),style:{padding:'4px 12px',borderRadius:5,border:'1px solid var(--border-strong)',background:'var(--brand-soft)',color:'var(--brand)',fontSize:12}},'+ Node'), | |
| h('button',{onClick:loadGraph,style:{padding:'4px 8px',borderRadius:4,border:'1px solid var(--border)',background:'transparent',color:'var(--text-3)',fontSize:12}},'↻'), | |
| h('div',{style:{display:'flex',border:'1px solid var(--border)',borderRadius:4,overflow:'hidden'}}, | |
| ['3d','2d'].map(m=>h('button',{key:m,onClick:()=>setViewMode(m),style:{padding:'3px 10px',background:viewMode===m?'var(--brand-soft)':'transparent',color:viewMode===m?'var(--brand)':'var(--text-3)',border:'none',fontSize:11,textTransform:'uppercase',letterSpacing:1}},m)) | |
| ), | |
| liveMode&&h('span',{style:{fontSize:10,color:'var(--brand)',display:'flex',alignItems:'center',gap:4}}, | |
| h('span',{style:{width:6,height:6,borderRadius:'50%',background:'var(--brand)',display:'inline-block',animation:'pulse 2s ease-in-out infinite'}}),'LIVE'), | |
| h('span',{style:{fontSize:11,color:'var(--text-4)'}},visible.length,' nodes'), | |
| h('div',{style:{marginLeft:'auto',display:'flex',alignItems:'center',gap:8}}, | |
| user | |
| ?h('div',{style:{display:'flex',alignItems:'center',gap:8}}, | |
| h('span',{style:{fontSize:11,color:'var(--text-3)'}},user.email?.slice(0,24)), | |
| h('button',{onClick:()=>sb.auth.signOut(),style:{fontSize:11,padding:'3px 8px',border:'1px solid var(--border)',borderRadius:4,background:'transparent',color:'var(--text-3)'}},'Sign out')) | |
| :h('button',{onClick:()=>setShowLogin(true),style:{fontSize:12,padding:'4px 12px',border:'1px solid var(--border-strong)',borderRadius:5,background:'var(--brand-soft)',color:'var(--brand)'}},'Sign in') | |
| ) | |
| ), | |
| h('div',{style:{display:'flex',flex:1,overflow:'hidden',position:'relative'}}, | |
| h('div',{ref:canvasWrap,style:{flex:1,position:'relative',overflow:'hidden',background:'var(--bg)'}}, | |
| dims.w>0 && h(GraphCanvas,{nodes:visible,links:vlinks,onNodeClick:setSelected,width:dims.w,height:dims.h,mode:viewMode}), | |
| h('div',{style:{position:'absolute',bottom:12,left:12,display:'flex',gap:8,flexWrap:'wrap',pointerEvents:'none'}}, | |
| [['human','👤'],['agent','✦'],['robot','⬡'],['cron','⏱']].map(([t,i])=>{ | |
| const c=actorColor(t); | |
| return h('span',{key:t,style:{fontSize:10,color:'var(--text-2)',background:'rgba(10,19,10,0.72)',padding:'3px 8px',borderRadius:4,border:'1px solid var(--border)',display:'inline-flex',alignItems:'center',gap:4}}, | |
| h('span',{style:{width:8,height:8,borderRadius:'50%',background:c,display:'inline-block'}}), | |
| h('span',{style:{color:'var(--text-2)'}}, i,' ',t) | |
| ); | |
| }) | |
| ), | |
| selected&&h(NodeDetail,{node:selected,onClose:()=>setSelected(null),userId:user?.id,onTogglePrivacy:handleTogglePrivacy,onNodeSelect:setSelected,allNodes:graphData.nodes,allLinks:graphData.links}) | |
| ), | |
| h('button',{onClick:()=>setShowFeed(v=>!v),style:{position:'absolute',right:showFeed?320:0,top:'50%',transform:'translateY(-50%)',zIndex:20,background:'var(--panel)',border:'1px solid var(--border)',borderRadius:showFeed?'6px 0 0 6px':'0 6px 6px 0',color:'var(--text-3)',padding:'8px 3px',fontSize:10,width:16}},showFeed?'▶':'◀'), | |
| showFeed&&h('div',{style:{width:320,minWidth:320}}, | |
| h(FeedPanel,{userId:user?.id,onNodeSelect:id=>{const n=graphData.nodes.find(n=>n.id===id);if(n)setSelected(n);}}) | |
| ) | |
| ), | |
| showLogin&&h(LoginPage,null), | |
| showAdd&&h(AddNodeModal,{onAdd:addNode,onClose:()=>setShowAdd(false)}) | |
| ); | |
| } | |
| ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App)); | |
| </script> | |
| </body> | |
| </html> | |