import { memo } from "react"; import { Handle, Position } from "@xyflow/react"; import type { NodeProps, Node } from "@xyflow/react"; import type { NodeType } from "@understand-anything/core/types"; import { useI18n } from "../contexts/I18nContext"; // Color maps keyed by NodeType — must be kept in sync with core NodeType union. const typeColors: Record = { file: "var(--color-node-file)", function: "var(--color-node-function)", class: "var(--color-node-class)", module: "var(--color-node-module)", concept: "var(--color-node-concept)", config: "var(--color-node-config)", document: "var(--color-node-document)", service: "var(--color-node-service)", table: "var(--color-node-table)", endpoint: "var(--color-node-endpoint)", pipeline: "var(--color-node-pipeline)", schema: "var(--color-node-schema)", resource: "var(--color-node-resource)", domain: "var(--color-node-concept)", flow: "var(--color-node-pipeline)", step: "var(--color-node-function)", article: "var(--color-node-article)", entity: "var(--color-node-entity)", topic: "var(--color-node-topic)", claim: "var(--color-node-claim)", source: "var(--color-node-source)", }; const typeTextColors: Record = { file: "text-node-file", function: "text-node-function", class: "text-node-class", module: "text-node-module", concept: "text-node-concept", config: "text-node-config", document: "text-node-document", service: "text-node-service", table: "text-node-table", endpoint: "text-node-endpoint", pipeline: "text-node-pipeline", schema: "text-node-schema", resource: "text-node-resource", domain: "text-node-concept", flow: "text-node-pipeline", step: "text-node-function", article: "text-node-article", entity: "text-node-entity", topic: "text-node-topic", claim: "text-node-claim", source: "text-node-source", }; const complexityColors: Record = { simple: "text-node-function", moderate: "text-accent-dim", complex: "text-[#c97070]", }; export interface CustomNodeData extends Record { label: string; nodeType: string; summary: string; complexity: string; isHighlighted: boolean; searchScore?: number; isSelected: boolean; isTourHighlighted: boolean; isDiffChanged: boolean; isDiffAffected: boolean; isDiffFaded: boolean; isNeighbor: boolean; isSelectionFaded: boolean; onNodeClick?: (nodeId: string) => void; incomingCount?: number; outgoingCount?: number; tags?: string[]; } export type CustomFlowNode = Node; function CustomNodeComponent({ id, data, }: NodeProps) { const knownType = data.nodeType as NodeType; const barColor = typeColors[knownType] ?? typeColors.file; const textColor = typeTextColors[knownType] ?? typeTextColors.file; const complexityColor = complexityColors[data.complexity] ?? complexityColors.simple; const { t } = useI18n(); if (import.meta.env.DEV && !(knownType in typeColors)) { console.warn(`[CustomNode] Unknown node type "${data.nodeType}" — using "file" colors`); } let extraClass = ""; if (data.isSelected) { extraClass = "ring-2 ring-accent node-glow"; } else if (data.isTourHighlighted) { extraClass = "ring-2 ring-accent-dim animate-accent-pulse"; } else if (data.isHighlighted) { const score = data.searchScore ?? 1; if (score <= 0.1) { extraClass = "ring-2 ring-accent-bright"; } else if (score <= 0.3) { extraClass = "ring-2 ring-accent"; } else { extraClass = "ring-1 ring-accent-dim/60"; } } // Diff overlay styling (composes with above) if (data.isDiffChanged) { extraClass += " ring-2 ring-[var(--color-diff-changed)] diff-changed-glow"; } else if (data.isDiffAffected) { extraClass += " ring-1 ring-[var(--color-diff-affected)] diff-affected-glow"; } else if (data.isDiffFaded) { extraClass += " diff-faded"; } // Selection-based dimming (when another node is selected, fade unrelated nodes) if (data.isSelectionFaded) { extraClass += " opacity-20 pointer-events-auto"; } else if (data.isNeighbor) { extraClass += " ring-1 ring-gold-dim/50"; } const name = data.label ?? "unnamed"; const truncatedName = name.length > 24 ? name.slice(0, 22) + "..." : name; return (
data.onNodeClick?.(id)} > {/* Left color bar */}
{data.nodeType}
{data.complexity} {data.tags?.includes("tested") && ( )}
{truncatedName}
{data.summary}
); } const CustomNode = memo(CustomNodeComponent); export default CustomNode;