"use client"; import React, { useEffect, useRef, useState } from "react"; import "./knowledge.css"; import { fetchGroups, fetchAllSignals, Group, Signal, getSeverityColor, parseMetaList, } from "../lib/api"; // ─── Types ──────────────────────────────────────────────────────────────────── interface NodeDef { id: string; label: string; x: number; // 0..100 percentage y: number; // 0..100 percentage size: "core" | "group" | "signal"; icon: string; color: string; tooltip: { title: string; lines: string[] }; } interface EdgeDef { from: string; to: string; dashed?: boolean; opacity?: number; } // ─── Layout helpers ─────────────────────────────────────────────────────────── const CORE = { x: 52, y: 48 }; /** Evenly distribute groups in a circle around the core */ function groupPositions(count: number) { const positions: { x: number; y: number }[] = []; for (let i = 0; i < count; i++) { const angle = (2 * Math.PI * i) / count - Math.PI / 2; const rx = 28; // ellipse horizontal radius (% units) const ry = 22; // vertical radius positions.push({ x: CORE.x + rx * Math.cos(angle), y: CORE.y + ry * Math.sin(angle), }); } return positions; } /** Place signal satellites around their parent group node */ function signalPositions( parentX: number, parentY: number, count: number, startAngle: number ) { const r = 9; return Array.from({ length: count }, (_, i) => { const angle = startAngle + (2 * Math.PI * i) / count; return { x: parentX + r * Math.cos(angle), y: parentY + r * Math.sin(angle), }; }); } const SIGNAL_TYPE_ICONS: Record = { security_concern: "security", architecture_decision: "architecture", tech_debt: "construction", consensus: "check_circle", blocker: "block", risk: "warning", sentiment_spike: "mood", knowledge_gap: "help", decision: "gavel", action_item: "task_alt", trend: "trending_up", default: "sensors", }; function getSignalTypeIcon(type: string): string { return SIGNAL_TYPE_ICONS[type] ?? SIGNAL_TYPE_ICONS.default; } // ─── Node Components ────────────────────────────────────────────────────────── function CoreNode({ node }: { node: NodeDef }) { const [hovered, setHovered] = useState(false); return (
setHovered(true)} onMouseLeave={() => setHovered(false)} >
auto_awesome
{hovered && (

NODE::CORE_AI

{node.tooltip.title}

{node.tooltip.lines.map((l, i) => (

{l}

))}
)}
); } function GroupNode({ node }: { node: NodeDef }) { const [hovered, setHovered] = useState(false); return (
setHovered(true)} onMouseLeave={() => setHovered(false)} >
{node.icon}

{node.label}

{hovered && (

NODE::GROUP

{node.tooltip.title}

{node.tooltip.lines.map((l, i) => (

{l}

))}
)}
); } function SignalNode({ node }: { node: NodeDef }) { const [hovered, setHovered] = useState(false); return (
setHovered(true)} onMouseLeave={() => setHovered(false)} >
{node.icon}
{hovered && (

SIGNAL::{node.tooltip.title.toUpperCase().replace(/ /g, "_")}

{node.tooltip.lines.map((l, i) => (

{l}

))}
)}
); } // ─── Main Component ─────────────────────────────────────────────────────────── export default function NetworkMap() { const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); const [loading, setLoading] = useState(true); const [totalSignals, setTotalSignals] = useState(0); // ── Pan state ── const [pan, setPan] = useState({ x: 0, y: 0 }); const dragging = useRef(false); const dragStart = useRef({ x: 0, y: 0, panX: 0, panY: 0 }); const handleMouseDown = (e: React.MouseEvent) => { if ((e.target as HTMLElement).closest(".pointer-events-auto")) return; dragging.current = true; dragStart.current = { x: e.clientX, y: e.clientY, panX: pan.x, panY: pan.y }; }; const handleMouseMove = (e: React.MouseEvent) => { if (!dragging.current) return; const dx = e.clientX - dragStart.current.x; const dy = e.clientY - dragStart.current.y; setPan({ x: dragStart.current.panX + dx, y: dragStart.current.panY + dy }); }; const handleMouseUp = () => { dragging.current = false; }; const resetPan = () => setPan({ x: 0, y: 0 }); useEffect(() => { async function build() { try { const [groups, allGroupSignals] = await Promise.all([ fetchGroups(), fetchAllSignals(), ]); const sigMap: Record = {}; allGroupSignals.forEach((g) => { sigMap[g.group_id] = g.signals; }); const total = allGroupSignals.reduce((acc, g) => acc + g.signals.length, 0); setTotalSignals(total); const newNodes: NodeDef[] = []; const newEdges: EdgeDef[] = []; // Core node const coreId = "core_ai"; newNodes.push({ id: coreId, label: "ThirdEye AI", x: CORE.x, y: CORE.y, size: "core", icon: "auto_awesome", color: "#A78BFA", tooltip: { title: "ThirdEye Neural Engine", lines: [ `GROUPS: ${groups.length}`, `SIGNALS: ${total}`, "STATUS: ACTIVE", ], }, }); // Group nodes const gPositions = groupPositions(Math.max(groups.length, 1)); const LENS_COLORS: Record = { dev: "#00daf3", product: "#A78BFA", client: "#ffb300", community: "#10b981", meet: "#ff6f78", unknown: "#6b7280", }; groups.forEach((group, gi) => { const pos = gPositions[gi]; const signals = sigMap[group.group_id] || []; const color = LENS_COLORS[group.lens] || LENS_COLORS.unknown; const groupId = `group_${group.group_id}`; // Count signal types const typeCounts: Record = {}; signals.forEach((s) => { const t = s.metadata.type; typeCounts[t] = (typeCounts[t] || 0) + 1; }); const topTypes = Object.entries(typeCounts) .sort(([, a], [, b]) => b - a) .slice(0, 3); newNodes.push({ id: groupId, label: group.group_name, x: pos.x, y: pos.y, size: "group", icon: "group", color, tooltip: { title: group.group_name, lines: [ `LENS: ${group.lens?.toUpperCase() || "UNKNOWN"}`, `SIGNALS: ${group.signal_count}`, ...topTypes.map(([t, c]) => `${t.replace(/_/g, " ")}: ${c}`), ], }, }); // Edge: core → group newEdges.push({ from: coreId, to: groupId, opacity: 0.8 }); // Signal satellite nodes (top 3 signal types) const angleSeed = (gi / Math.max(groups.length, 1)) * Math.PI * 2; const sPositions = signalPositions(pos.x, pos.y, topTypes.length, angleSeed); topTypes.forEach(([type, count], si) => { const sigNodeId = `sig_${gi}_${si}`; const severity = signals.find((s) => s.metadata.type === type)?.metadata.severity || "low"; newNodes.push({ id: sigNodeId, label: type, x: sPositions[si].x, y: sPositions[si].y, size: "signal", icon: getSignalTypeIcon(type), color: getSeverityColor(severity), tooltip: { title: type.replace(/_/g, " "), lines: [`COUNT: ${count}`, `SEVERITY: ${severity.toUpperCase()}`], }, }); newEdges.push({ from: groupId, to: sigNodeId, dashed: true, opacity: 0.5 }); }); // Cross-edges between adjacent groups (every even group connects to next) if (gi > 0 && gi % 2 === 0 && groups.length > 2) { newEdges.push({ from: `group_${groups[gi - 1].group_id}`, to: groupId, dashed: true, opacity: 0.2, }); } }); // If no groups yet, show a placeholder set if (groups.length === 0) { ["dev", "product", "client"].forEach((lens, i) => { const pos = gPositions[i] || { x: 30 + i * 20, y: 45 }; const gId = `placeholder_${lens}`; newNodes.push({ id: gId, label: `${lens}_group`, x: pos.x, y: pos.y, size: "group", icon: "group_off", color: "#3f3f46", tooltip: { title: `${lens.toUpperCase()} Group`, lines: ["No data yet", "Connect Telegram to populate"], }, }); newEdges.push({ from: coreId, to: gId, dashed: true, opacity: 0.15 }); }); } setNodes(newNodes); setEdges(newEdges); } catch { // silently fail } finally { setLoading(false); } } build(); }, []); // Build a quick lookup for node positions const nodePos: Record = {}; nodes.forEach((n) => { nodePos[n.id] = { x: n.x, y: n.y }; }); return (
{/* Pan hint */} {!loading && (pan.x !== 0 || pan.y !== 0) && ( )} {/* Pannable graph content */}
{/* SVG Edges */} {/* Strong glow – core edge packets */} {/* Subtle glow – secondary edges */} {edges.map((edge, i) => { const from = nodePos[edge.from]; const to = nodePos[edge.to]; if (!from || !to) return null; const isCoreEdge = !edge.dashed; // Speed: 0.7 – 1.3 s per edge, staggered so packets never sync const duration = 0.7 + (i * 0.18) % 0.6; const phase = (t: number) => -(i * t % Math.round(duration * 1000)); if (isCoreEdge) { return ( {/* Solid base track */} {/* Violet ambient trace */} {/* Packet 1 – bright white, leading */} {/* Packet 2 – violet, ~1/3 behind */} {/* Packet 3 – cyan, ~2/3 behind */} {/* Packet 4 – white micro dot, fills the gaps */} ); } // ── Secondary / dashed edge (group→signal, cross-group) ────────── const dimDuration = 1.2 + (i % 4) * 0.2; const dimPhase = -(i * 60 % Math.round(dimDuration * 1000)); return ( {/* Static dashed connector */} {/* Traveling dot 1 */} {/* Traveling dot 2 – offset by half cycle */} ); })} {/* Nodes */} {!loading && nodes.map((node) => { if (node.size === "core") return ; if (node.size === "group") return ; return ; })} {/* Signal Count Watermark */} {!loading && totalSignals > 0 && (

{totalSignals.toLocaleString()}

)}
{/* Loading Overlay (outside pan transform) */} {loading && (
autorenew

Building knowledge graph...

)} {/* Drag hint (shown once when loaded) */} {!loading && pan.x === 0 && pan.y === 0 && (

drag_pan drag to pan graph

)}
); }