import { useState } from "react"; import { useDashboardStore } from "../store"; import { useI18n } from "../contexts/I18nContext"; import type { NodeType, EdgeType, KnowledgeGraph, GraphNode } from "@understand-anything/core/types"; // Badge color classes keyed by NodeType — must be kept in sync with core NodeType union. const typeBadgeColors: Record = { file: "text-node-file border border-node-file/30 bg-node-file/10", function: "text-node-function border border-node-function/30 bg-node-function/10", class: "text-node-class border border-node-class/30 bg-node-class/10", module: "text-node-module border border-node-module/30 bg-node-module/10", concept: "text-node-concept border border-node-concept/30 bg-node-concept/10", config: "text-node-config border border-node-config/30 bg-node-config/10", document: "text-node-document border border-node-document/30 bg-node-document/10", service: "text-node-service border border-node-service/30 bg-node-service/10", table: "text-node-table border border-node-table/30 bg-node-table/10", endpoint: "text-node-endpoint border border-node-endpoint/30 bg-node-endpoint/10", pipeline: "text-node-pipeline border border-node-pipeline/30 bg-node-pipeline/10", schema: "text-node-schema border border-node-schema/30 bg-node-schema/10", resource: "text-node-resource border border-node-resource/30 bg-node-resource/10", domain: "text-node-concept border border-node-concept/30 bg-node-concept/10", flow: "text-node-pipeline border border-node-pipeline/30 bg-node-pipeline/10", step: "text-node-function border border-node-function/30 bg-node-function/10", article: "text-node-article border border-node-article/30 bg-node-article/10", entity: "text-node-entity border border-node-entity/30 bg-node-entity/10", topic: "text-node-topic border border-node-topic/30 bg-node-topic/10", claim: "text-node-claim border border-node-claim/30 bg-node-claim/10", source: "text-node-source border border-node-source/30 bg-node-source/10", }; const complexityBadgeColors: Record = { simple: "text-node-function border border-node-function/30 bg-node-function/10", moderate: "text-accent-dim border border-accent-dim/30 bg-accent-dim/10", complex: "text-[#c97070] border border-[#c97070]/30 bg-[#c97070]/10", }; function getDirectionalLabel(edgeType: string, isSource: boolean, t: ReturnType["t"]): string { const labels = t.edgeLabels[edgeType as EdgeType]; if (!labels) { const formatted = edgeType.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); return isSource ? formatted : `${formatted} (reverse)`; } return isSource ? labels.forward : labels.backward; } function KnowledgeNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) { const navigateToNode = useDashboardStore((s) => s.navigateToNode); const { t } = useI18n(); const meta = node.knowledgeMeta; // Wikilinks (outgoing related edges) const wikilinks = graph.edges .filter((e) => e.type === "related" && e.source === node.id) .map((e) => graph.nodes.find((n) => n.id === e.target)) .filter((n): n is GraphNode => n !== undefined); // Backlinks (incoming related edges) const backlinks = graph.edges .filter((e) => e.type === "related" && e.target === node.id) .map((e) => graph.nodes.find((n) => n.id === e.source)) .filter((n): n is GraphNode => n !== undefined); // Category const categoryEdge = graph.edges.find( (e) => e.type === "categorized_under" && e.source === node.id ); const categoryNode = categoryEdge ? graph.nodes.find((n) => n.id === categoryEdge.target) : null; return (
{categoryNode && (

{t.nodeInfo.category}

)} {meta?.wikilinks && meta.wikilinks.length > 0 && (

{t.nodeInfo.wikilinks} ({wikilinks.length})

{wikilinks.map((n) => ( ))}
)} {backlinks.length > 0 && (

{t.nodeInfo.backlinks} ({backlinks.length})

{backlinks.map((n) => ( ))}
)} {meta?.content && (

{t.common.preview}

{meta.content.slice(0, 1500)} {meta.content.length > 1500 && ( ... {t.common.truncated} )}
)}
); } function DomainNodeDetails({ node, graph }: { node: GraphNode; graph: KnowledgeGraph }) { const navigateToDomain = useDashboardStore((s) => s.navigateToDomain); const selectNode = useDashboardStore((s) => s.selectNode); const { t } = useI18n(); const meta = node.domainMeta; if (node.type === "domain") { const flows = graph.edges .filter((e) => e.type === "contains_flow" && e.source === node.id) .map((e) => graph.nodes.find((n) => n.id === e.target)) .filter((n): n is GraphNode => n !== undefined); return (
{Array.isArray(meta?.entities) && meta.entities.length > 0 ? (

{t.nodeInfo.entities}

{meta.entities.map((e) => ( {e} ))}
) : null} {Array.isArray(meta?.businessRules) && meta.businessRules.length > 0 ? (

{t.nodeInfo.businessRules}

    {meta.businessRules.map((r, i) => (
  • -{r}
  • ))}
) : null} {Array.isArray(meta?.crossDomainInteractions) && meta.crossDomainInteractions.length > 0 ? (

{t.nodeInfo.crossDomain}

    {meta.crossDomainInteractions.map((c, i) => (
  • {c}
  • ))}
) : null} {flows.length > 0 && (

{t.nodeInfo.flows}

{flows.map((f) => ( ))}
)}
); } if (node.type === "flow") { const steps = graph.edges .filter((e) => e.type === "flow_step" && e.source === node.id) .sort((a, b) => a.weight - b.weight) .map((e) => graph.nodes.find((n) => n.id === e.target)) .filter((n): n is GraphNode => n !== undefined); return (
{meta?.entryPoint ? (

{t.nodeInfo.entryPoint}

{meta.entryPoint}
) : null} {steps.length > 0 && (

{t.nodeInfo.steps}

    {steps.map((s, i) => (
  1. ))}
)}
); } if (node.type === "step") { if (!node.filePath) return null; return (

{t.nodeInfo.implementation}

{node.filePath} {node.lineRange && :{node.lineRange[0]}-{node.lineRange[1]}}
); } return null; } export default function NodeInfo() { const graph = useDashboardStore((s) => s.graph); const selectedNodeId = useDashboardStore((s) => s.selectedNodeId); const nodeHistory = useDashboardStore((s) => s.nodeHistory); const goBackNode = useDashboardStore((s) => s.goBackNode); const [languageExpanded, setLanguageExpanded] = useState(true); const { t } = useI18n(); const navigateToNode = useDashboardStore((s) => s.navigateToNode); const navigateToHistoryIndex = useDashboardStore((s) => s.navigateToHistoryIndex); const setFocusNode = useDashboardStore((s) => s.setFocusNode); const openCodeViewer = useDashboardStore((s) => s.openCodeViewer); const focusNodeId = useDashboardStore((s) => s.focusNodeId); const viewMode = useDashboardStore((s) => s.viewMode); const domainGraph = useDashboardStore((s) => s.domainGraph); const activeGraph = viewMode === "domain" && domainGraph ? domainGraph : graph; const node = activeGraph?.nodes.find((n) => n.id === selectedNodeId) ?? null; // Resolve history node names for the breadcrumb trail const historyNodes = nodeHistory.map((id) => { const n = activeGraph?.nodes.find((gn) => gn.id === id); return { id, name: n?.name ?? id }; }); if (!node) { return (

{t.common.selectNode}

); } const allEdges = activeGraph?.edges ?? []; const connections = allEdges.filter( (e) => e.source === node.id || e.target === node.id, ); // Separate child nodes (contained IN this file) from other connections const childEdges = connections.filter( (e) => e.type === "contains" && e.source === node.id, ); const otherConnections = connections.filter( (e) => !(e.type === "contains" && e.source === node.id), ); // Resolve child nodes const childNodes = childEdges .map((e) => activeGraph?.nodes.find((n) => n.id === e.target)) .filter((n): n is GraphNode => n !== undefined); const knownType = node.type as NodeType; const typeBadge = typeBadgeColors[knownType] ?? typeBadgeColors.file; const complexityBadge = complexityBadgeColors[node.complexity] ?? complexityBadgeColors.simple; if (import.meta.env.DEV && !(knownType in typeBadgeColors)) { console.warn(`[NodeInfo] Unknown node type "${node.type}" — using "file" badge colors`); } return (
{/* Navigation history trail */} {historyNodes.length > 0 && (
│ {historyNodes.slice(-3).map((h, i, arr) => ( {i < arr.length - 1 && ( › )} ))} › {node.name}
)}
{node.type} {node.complexity}

{node.name}

{node.summary}

{node.filePath && (
{t.common.file}
{node.filePath} {node.lineRange && ( L{node.lineRange[0]}-{node.lineRange[1]} )}
)} {node.languageNotes && (
{languageExpanded && (

{node.languageNotes}

)}
)} {node.tags.length > 0 && (

{t.common.tags}

{node.tags.map((tag) => ( {tag} ))}
)} {/* Knowledge-specific details */} {activeGraph && node && (node.type === "article" || node.type === "entity" || node.type === "topic" || node.type === "claim" || node.type === "source") && ( )} {/* Domain-specific details */} {activeGraph && node && (node.type === "domain" || node.type === "flow" || node.type === "step") && ( )} {/* Child classes/functions within this file */} {childNodes.length > 0 && (

{t.nodeInfo.definedInThisFile} ({childNodes.length})

{childNodes.map((child) => { if (!child) return null; const childTypeBadge = typeBadgeColors[child.type as NodeType] ?? typeBadgeColors.file; const childComplexity = complexityBadgeColors[child.complexity] ?? complexityBadgeColors.simple; return (
navigateToNode(child.id)} >
{child.type} {child.name} {child.complexity}
{child.summary && (

{child.summary}

)}
); })}
)} {/* Other connections (excluding "contains" children) */} {otherConnections.length > 0 && (

{t.common.connections} ({otherConnections.length})

{otherConnections.map((edge, i) => { const isSource = edge.source === node.id; const otherId = isSource ? edge.target : edge.source; const otherNode = activeGraph?.nodes.find((n) => n.id === otherId); const dirLabel = getDirectionalLabel(edge.type, isSource, t); const arrow = isSource ? "\u2192" : "\u2190"; return (
{ navigateToNode(otherId); }} > {arrow} {dirLabel} {otherNode?.name ?? otherId}
); })}
)}
); }