mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
@@ -6,6 +6,7 @@ import CodeViewer from "./components/CodeViewer";
|
||||
import SearchBar from "./components/SearchBar";
|
||||
import NodeInfo from "./components/NodeInfo";
|
||||
import LayerLegend from "./components/LayerLegend";
|
||||
import DiffToggle from "./components/DiffToggle";
|
||||
import LearnPanel from "./components/LearnPanel";
|
||||
import PersonaSelector from "./components/PersonaSelector";
|
||||
import ProjectOverview from "./components/ProjectOverview";
|
||||
@@ -18,6 +19,7 @@ function App() {
|
||||
const persona = useDashboardStore((s) => s.persona);
|
||||
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
|
||||
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
|
||||
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,6 +41,32 @@ function App() {
|
||||
});
|
||||
}, [setGraph]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/diff-overlay.json")
|
||||
.then((res) => {
|
||||
if (!res.ok) return null;
|
||||
return res.json();
|
||||
})
|
||||
.then((data: unknown) => {
|
||||
if (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
"changedNodeIds" in data &&
|
||||
"affectedNodeIds" in data &&
|
||||
Array.isArray((data as Record<string, unknown>).changedNodeIds) &&
|
||||
Array.isArray((data as Record<string, unknown>).affectedNodeIds)
|
||||
) {
|
||||
const d = data as { changedNodeIds: string[]; affectedNodeIds: string[] };
|
||||
if (d.changedNodeIds.length > 0) {
|
||||
setDiffOverlay(d.changedNodeIds, d.affectedNodeIds);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Silently ignore - diff overlay is optional
|
||||
});
|
||||
}, [setDiffOverlay]);
|
||||
|
||||
// Determine sidebar content
|
||||
// Learn persona always shows LearnPanel; tour active overrides everything
|
||||
const sidebarContent = tourActive || persona === "junior" ? (
|
||||
@@ -61,6 +89,7 @@ function App() {
|
||||
<PersonaSelector />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<DiffToggle />
|
||||
<LayerLegend />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -32,6 +32,9 @@ export interface CustomNodeData extends Record<string, unknown> {
|
||||
searchScore?: number;
|
||||
isSelected: boolean;
|
||||
isTourHighlighted: boolean;
|
||||
isDiffChanged: boolean;
|
||||
isDiffAffected: boolean;
|
||||
isDiffFaded: boolean;
|
||||
onNodeClick?: (nodeId: string) => void;
|
||||
}
|
||||
|
||||
@@ -61,6 +64,15 @@ export default function CustomNode({
|
||||
}
|
||||
}
|
||||
|
||||
// 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";
|
||||
}
|
||||
|
||||
const name = data.label ?? "unnamed";
|
||||
const truncatedName =
|
||||
name.length > 24 ? name.slice(0, 22) + "..." : name;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useDashboardStore } from "../store";
|
||||
|
||||
export default function DiffToggle() {
|
||||
const diffMode = useDashboardStore((s) => s.diffMode);
|
||||
const toggleDiffMode = useDashboardStore((s) => s.toggleDiffMode);
|
||||
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
|
||||
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
|
||||
|
||||
const hasDiff = changedNodeIds.size > 0;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={toggleDiffMode}
|
||||
disabled={!hasDiff}
|
||||
className={`px-2 py-0.5 rounded text-[11px] font-medium transition-colors ${
|
||||
diffMode && hasDiff
|
||||
? "bg-[var(--color-diff-changed-dim)] text-[var(--color-diff-changed)]"
|
||||
: hasDiff
|
||||
? "bg-elevated text-text-secondary hover:bg-surface"
|
||||
: "bg-elevated text-text-muted cursor-not-allowed"
|
||||
}`}
|
||||
title={
|
||||
hasDiff
|
||||
? diffMode
|
||||
? "Hide diff overlay"
|
||||
: "Show diff overlay"
|
||||
: "No diff data loaded"
|
||||
}
|
||||
>
|
||||
Diff {diffMode && hasDiff ? "ON" : "OFF"}
|
||||
</button>
|
||||
|
||||
{diffMode && hasDiff && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: "var(--color-diff-changed)" }}
|
||||
/>
|
||||
<span className="text-text-secondary text-[11px]">
|
||||
Changed
|
||||
<span className="text-text-muted ml-0.5">
|
||||
({changedNodeIds.size})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: "var(--color-diff-affected)" }}
|
||||
/>
|
||||
<span className="text-text-secondary text-[11px]">
|
||||
Affected
|
||||
<span className="text-text-muted ml-0.5">
|
||||
({affectedNodeIds.size})
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,9 @@ export default function GraphView() {
|
||||
const showLayers = useDashboardStore((s) => s.showLayers);
|
||||
const tourHighlightedNodeIds = useDashboardStore((s) => s.tourHighlightedNodeIds);
|
||||
const persona = useDashboardStore((s) => s.persona);
|
||||
const diffMode = useDashboardStore((s) => s.diffMode);
|
||||
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
|
||||
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
|
||||
|
||||
const handleNodeSelect = useCallback(
|
||||
(nodeId: string) => {
|
||||
@@ -78,20 +81,41 @@ export default function GraphView() {
|
||||
searchScore: matchResult?.score,
|
||||
isSelected: selectedNodeId === node.id,
|
||||
isTourHighlighted: tourHighlightedNodeIds.includes(node.id),
|
||||
isDiffChanged: diffMode && changedNodeIds.has(node.id),
|
||||
isDiffAffected: diffMode && affectedNodeIds.has(node.id),
|
||||
isDiffFaded: diffMode && !changedNodeIds.has(node.id) && !affectedNodeIds.has(node.id),
|
||||
onNodeClick: handleNodeSelect,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => ({
|
||||
id: `e-${i}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.type,
|
||||
animated: edge.type === "calls",
|
||||
style: { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
|
||||
labelStyle: { fill: "#a39787", fontSize: 10 },
|
||||
}));
|
||||
const diffNodeIds = new Set([...changedNodeIds, ...affectedNodeIds]);
|
||||
const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => {
|
||||
const sourceInDiff = diffNodeIds.has(edge.source);
|
||||
const targetInDiff = diffNodeIds.has(edge.target);
|
||||
const isImpacted = diffMode && (sourceInDiff || targetInDiff);
|
||||
|
||||
return {
|
||||
id: `e-${i}`,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
label: edge.type,
|
||||
animated: edge.type === "calls" || isImpacted,
|
||||
style: isImpacted
|
||||
? {
|
||||
stroke: sourceInDiff && targetInDiff
|
||||
? "rgba(224, 82, 82, 0.7)"
|
||||
: "rgba(212, 160, 48, 0.5)",
|
||||
strokeWidth: 2.5,
|
||||
}
|
||||
: diffMode
|
||||
? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }
|
||||
: { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
|
||||
labelStyle: diffMode && !isImpacted
|
||||
? { fill: "rgba(163,151,135,0.3)", fontSize: 10 }
|
||||
: { fill: "#a39787", fontSize: 10 },
|
||||
};
|
||||
});
|
||||
|
||||
// Run dagre layout on all nodes (without groups)
|
||||
const laid = applyDagreLayout(flowNodes, flowEdges);
|
||||
@@ -190,7 +214,7 @@ export default function GraphView() {
|
||||
];
|
||||
|
||||
return { initialNodes: allNodes, initialEdges: laid.edges };
|
||||
}, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect]);
|
||||
}, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds]);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
--color-node-module: #c9a06c;
|
||||
--color-node-concept: #b07a8a;
|
||||
|
||||
/* Diff overlay colors */
|
||||
--color-diff-changed: #e05252;
|
||||
--color-diff-affected: #d4a030;
|
||||
--color-diff-changed-dim: rgba(224, 82, 82, 0.25);
|
||||
--color-diff-affected-dim: rgba(212, 160, 48, 0.25);
|
||||
|
||||
/* Fonts */
|
||||
--font-serif: 'DM Serif Display', Georgia, serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
@@ -113,6 +119,22 @@ body {
|
||||
box-shadow: 0 0 20px rgba(212, 165, 116, 0.15);
|
||||
}
|
||||
|
||||
/* Diff overlay glow effects */
|
||||
.diff-changed-glow {
|
||||
box-shadow: 0 0 16px rgba(224, 82, 82, 0.25);
|
||||
}
|
||||
|
||||
.diff-affected-glow {
|
||||
box-shadow: 0 0 12px rgba(212, 160, 48, 0.2);
|
||||
}
|
||||
|
||||
/* Diff fade for unrelated nodes */
|
||||
.diff-faded {
|
||||
opacity: 0.25;
|
||||
filter: saturate(0.3);
|
||||
transition: opacity 0.3s ease, filter 0.3s ease;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for dark luxury theme */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@@ -28,6 +28,10 @@ interface DashboardStore {
|
||||
|
||||
persona: Persona;
|
||||
|
||||
diffMode: boolean;
|
||||
changedNodeIds: Set<string>;
|
||||
affectedNodeIds: Set<string>;
|
||||
|
||||
setGraph: (graph: KnowledgeGraph) => void;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
@@ -36,6 +40,10 @@ interface DashboardStore {
|
||||
openCodeViewer: (nodeId: string) => void;
|
||||
closeCodeViewer: () => void;
|
||||
|
||||
setDiffOverlay: (changed: string[], affected: string[]) => void;
|
||||
toggleDiffMode: () => void;
|
||||
clearDiffOverlay: () => void;
|
||||
|
||||
startTour: () => void;
|
||||
stopTour: () => void;
|
||||
setTourStep: (step: number) => void;
|
||||
@@ -67,6 +75,10 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
|
||||
persona: "junior",
|
||||
|
||||
diffMode: false,
|
||||
changedNodeIds: new Set<string>(),
|
||||
affectedNodeIds: new Set<string>(),
|
||||
|
||||
setGraph: (graph) => {
|
||||
const searchEngine = new SearchEngine(graph.nodes);
|
||||
const query = get().searchQuery;
|
||||
@@ -96,6 +108,22 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
openCodeViewer: (nodeId) => set({ codeViewerOpen: true, codeViewerNodeId: nodeId }),
|
||||
closeCodeViewer: () => set({ codeViewerOpen: false, codeViewerNodeId: null }),
|
||||
|
||||
setDiffOverlay: (changed, affected) =>
|
||||
set({
|
||||
diffMode: true,
|
||||
changedNodeIds: new Set(changed),
|
||||
affectedNodeIds: new Set(affected),
|
||||
}),
|
||||
|
||||
toggleDiffMode: () => set((state) => ({ diffMode: !state.diffMode })),
|
||||
|
||||
clearDiffOverlay: () =>
|
||||
set({
|
||||
diffMode: false,
|
||||
changedNodeIds: new Set<string>(),
|
||||
affectedNodeIds: new Set<string>(),
|
||||
}),
|
||||
|
||||
startTour: () => {
|
||||
const { graph } = get();
|
||||
if (!graph || !graph.tour || graph.tour.length === 0) return;
|
||||
|
||||
@@ -38,6 +38,26 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
}
|
||||
if (req.url === "/diff-overlay.json") {
|
||||
const graphDir = process.env.GRAPH_DIR;
|
||||
const candidates = [
|
||||
...(graphDir
|
||||
? [path.resolve(graphDir, ".understand-anything/diff-overlay.json")]
|
||||
: []),
|
||||
path.resolve(process.cwd(), ".understand-anything/diff-overlay.json"),
|
||||
path.resolve(process.cwd(), "../../../.understand-anything/diff-overlay.json"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
fs.createReadStream(candidate).pipe(res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.statusCode = 404;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -55,3 +55,16 @@ The knowledge graph JSON has this structure:
|
||||
- **Affected Layers**: Which architectural layers are touched and cross-layer concerns
|
||||
- **Risk Assessment**: Based on node `complexity` values, number of cross-layer edges, and blast radius (number of affected components)
|
||||
- Suggest what to review carefully and any potential issues
|
||||
|
||||
8. **Write diff overlay for dashboard** — after producing the analysis, write the diff data to `.understand-anything/diff-overlay.json` so the dashboard can visualize changed and affected components. The file contains:
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"baseBranch": "<the base branch used>",
|
||||
"generatedAt": "<ISO timestamp>",
|
||||
"changedFiles": ["<list of changed file paths>"],
|
||||
"changedNodeIds": ["<node IDs from step 4>"],
|
||||
"affectedNodeIds": ["<node IDs from step 5, excluding changedNodeIds>"]
|
||||
}
|
||||
```
|
||||
After writing, tell the user they can run `/understand-anything:understand-dashboard` to see the diff overlay visually.
|
||||
|
||||
Reference in New Issue
Block a user