diff --git a/bun.lock b/bun.lock index 95187bc..3590adc 100644 --- a/bun.lock +++ b/bun.lock @@ -281,7 +281,7 @@ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], @@ -1477,10 +1477,10 @@ "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -1499,8 +1499,6 @@ "@istanbuljs/load-nyc-config/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], - "@jest/reporters/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@jest/reporters/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "@jest/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], @@ -1563,8 +1561,6 @@ "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "istanbul-lib-source-maps/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "jest-config/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "jest-config/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], @@ -1597,8 +1593,6 @@ "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - "v8-to-istanbul/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/frontend/components/graph/GraphCanvas.tsx b/frontend/components/graph/GraphCanvas.tsx index e090700..513e591 100644 --- a/frontend/components/graph/GraphCanvas.tsx +++ b/frontend/components/graph/GraphCanvas.tsx @@ -1,5 +1,5 @@ import { CodeEdge, CodeNode } from "@/lib/types"; -import { useEffect, useRef, useImperativeHandle, forwardRef } from "react"; +import { useEffect, useRef, useImperativeHandle, forwardRef, useState } from "react"; import cytoscape from "cytoscape"; import fcose from "cytoscape-fcose"; import { toElements } from "./cytoscopeUtils"; @@ -7,6 +7,117 @@ import { getCytoscapeStyles, getLayoutConfig } from "./cytoscapeConfig"; cytoscape.use(fcose); +// ─── Tooltip ────────────────────────────────────────────────────────────────── + +interface TooltipInfo { + x: number; + y: number; + type: string; + filePath: string; + score: number; + hasState: boolean; + hooks: string[]; + children: string[]; +} + +const TOOLTIP_TYPE_COLORS: Record = { + COMPONENT: { bg: "#2dd4bf18", text: "#2dd4bf", border: "#2dd4bf30" }, + HOOK: { bg: "#c084fc18", text: "#c084fc", border: "#c084fc30" }, + FUNCTION: { bg: "#60a5fa18", text: "#60a5fa", border: "#60a5fa30" }, + STATE_STORE: { bg: "#fb923c18", text: "#fb923c", border: "#fb923c30" }, + UTILITY: { bg: "#94a3b818", text: "#94a3b8", border: "#94a3b830" }, + FILE: { bg: "#f472b618", text: "#f472b6", border: "#f472b630" }, + GHOST: { bg: "#6b728018", text: "#6b7280", border: "#6b728030" }, + ROUTE: { bg: "#818cf818", text: "#818cf8", border: "#818cf830" }, + TEST: { bg: "#f9731618", text: "#f97316", border: "#f9731630" }, + STORY: { bg: "#a78bfa18", text: "#a78bfa", border: "#a78bfa30" }, +}; + +function NodeTooltip({ t }: { t: TooltipInfo }) { + const colors = TOOLTIP_TYPE_COLORS[t.type] ?? { bg: "#21262d", text: "#8b949e", border: "#30363d" }; + const shortPath = t.filePath.split("/").slice(-3).join("/"); + const hasExtra = t.hasState || t.hooks.length > 0 || t.children.length > 0; + + return ( +
+
+ {/* Type badge */} + + {t.type.replace("_", " ")} + + + {/* File path */} +
+ {shortPath} +
+ + {/* Divider */} + {hasExtra && ( +
+ )} + + {/* Hooks */} + {t.hooks.length > 0 && ( +
+ {t.hooks.slice(0, 4).join(", ")}{t.hooks.length > 4 ? " …" : ""} +
+ )} + + {/* Local state pill */} + {t.hasState && ( +
+ + Local state +
+ )} + + {/* Direct children */} + {t.children.length > 0 && ( +
+
+ Children +
+
+ {t.children.slice(0, 8).map((name) => ( + + {name} + + ))} + {t.children.length > 8 && ( + + +{t.children.length - 8} more + + )} +
+
+ )} +
+
+ ); +} + // ─── Exposed handle ─────────────────────────────────────────────────────────── export interface GraphCanvasHandle { @@ -30,6 +141,7 @@ const GraphCanvas = forwardRef( ({ nodes, edges, onNodeClick }, ref) => { const containerRef = useRef(null); const cyRef = useRef(null); + const [tooltip, setTooltip] = useState(null); useImperativeHandle(ref, () => ({ focusNode(nodeId: string) { @@ -148,8 +260,35 @@ const GraphCanvas = forwardRef( e.target.style("z-index", 9999); onNodeClick?.(e.target.id()); }); - cy.on("mouseover", "node", (e) => e.target.addClass("hover")); - cy.on("mouseout", "node", (e) => e.target.removeClass("hover")); + cy.on("mouseover", "node", (e) => { + e.target.addClass("hover"); + const pos = e.target.renderedPosition() as { x: number; y: number }; + const meta = e.target.data("metadata") as Record | undefined; + const hooks = Array.isArray(meta?.hooks) ? (meta!.hooks as string[]) : []; + + const children: string[] = []; + e.target.outgoers("edge").forEach((edge: cytoscape.EdgeSingular) => { + if (edge.data("type") === "PROP_PASS") { + children.push(edge.target().data("label") as string); + } + }); + + setTooltip({ + x: pos.x, + y: pos.y, + type: e.target.data("type") as string, + filePath: e.target.data("filePath") as string, + score: e.target.data("score") as number, + hasState: !!(meta?.hasState), + hooks, + children, + }); + }); + cy.on("mouseout", "node", (e) => { + e.target.removeClass("hover"); + setTooltip(null); + }); + cy.on("pan zoom", () => setTooltip(null)); cy.on("tap", (e) => { if (e.target === cy) { cy.nodes().style("z-index", 1); @@ -164,11 +303,14 @@ const GraphCanvas = forwardRef( }, []); return ( -
+
+
+ {tooltip && } +
); }, );