mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(dashboard): add contextual node explanation with Claude API
Add "Explain This" button to NodeInfo panel that generates a detailed plain-English explanation of the selected node using Claude API. Explanations are cached per node ID and cleared on node switch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
46f02ab7c6
commit
8747ed30b4
@@ -1,3 +1,4 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useDashboardStore } from "../store";
|
||||
|
||||
const typeBadgeColors: Record<string, string> = {
|
||||
@@ -17,6 +18,12 @@ const complexityBadgeColors: Record<string, string> = {
|
||||
export default function NodeInfo() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
const apiKey = useDashboardStore((s) => s.apiKey);
|
||||
const nodeExplanation = useDashboardStore((s) => s.nodeExplanation);
|
||||
const nodeExplanationLoading = useDashboardStore(
|
||||
(s) => s.nodeExplanationLoading,
|
||||
);
|
||||
const explainNode = useDashboardStore((s) => s.explainNode);
|
||||
|
||||
const node = graph?.nodes.find((n) => n.id === selectedNodeId) ?? null;
|
||||
|
||||
@@ -75,6 +82,47 @@ export default function NodeInfo() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{apiKey && (
|
||||
<div className="mb-4">
|
||||
{!nodeExplanation && !nodeExplanationLoading && (
|
||||
<button
|
||||
onClick={() => explainNode(node.id)}
|
||||
className="text-xs bg-indigo-600 text-white px-3 py-1.5 rounded hover:bg-indigo-500 transition-colors"
|
||||
>
|
||||
Explain This
|
||||
</button>
|
||||
)}
|
||||
{nodeExplanationLoading && (
|
||||
<div className="text-xs text-gray-400 animate-pulse">
|
||||
Generating explanation...
|
||||
</div>
|
||||
)}
|
||||
{nodeExplanation && (
|
||||
<div className="bg-gray-700/50 rounded-lg p-3 text-sm text-gray-300 leading-relaxed">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
p: ({ children }) => (
|
||||
<p className="mb-2 last:mb-0">{children}</p>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-white">
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
code: ({ children }) => (
|
||||
<code className="bg-gray-900 rounded px-1 py-0.5 text-[11px]">
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{nodeExplanation}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{node.tags.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">
|
||||
|
||||
@@ -25,6 +25,10 @@ interface DashboardStore {
|
||||
|
||||
showLayers: boolean;
|
||||
|
||||
nodeExplanation: string | null;
|
||||
nodeExplanationLoading: boolean;
|
||||
nodeExplanationCache: Record<string, string>;
|
||||
|
||||
tourActive: boolean;
|
||||
currentTourStep: number;
|
||||
tourHighlightedNodeIds: string[];
|
||||
@@ -36,6 +40,7 @@ interface DashboardStore {
|
||||
sendChatMessage: (message: string) => Promise<void>;
|
||||
clearChat: () => void;
|
||||
toggleLayers: () => void;
|
||||
explainNode: (nodeId: string) => Promise<void>;
|
||||
|
||||
startTour: () => void;
|
||||
stopTour: () => void;
|
||||
@@ -132,6 +137,10 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
|
||||
showLayers: false,
|
||||
|
||||
nodeExplanation: null,
|
||||
nodeExplanationLoading: false,
|
||||
nodeExplanationCache: {},
|
||||
|
||||
tourActive: false,
|
||||
currentTourStep: 0,
|
||||
tourHighlightedNodeIds: [],
|
||||
@@ -142,7 +151,7 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
const searchResults = query.trim() ? searchEngine.search(query) : [];
|
||||
set({ graph, searchEngine, searchResults });
|
||||
},
|
||||
selectNode: (nodeId) => set({ selectedNodeId: nodeId }),
|
||||
selectNode: (nodeId) => set({ selectedNodeId: nodeId, nodeExplanation: null }),
|
||||
setSearchQuery: (query) => {
|
||||
const engine = get().searchEngine;
|
||||
if (!engine || !query.trim()) {
|
||||
@@ -216,6 +225,84 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
|
||||
toggleLayers: () => set((state) => ({ showLayers: !state.showLayers })),
|
||||
|
||||
explainNode: async (nodeId) => {
|
||||
const { apiKey, graph, nodeExplanationCache } = get();
|
||||
if (!apiKey || !graph) return;
|
||||
|
||||
// Check cache first
|
||||
if (nodeExplanationCache[nodeId]) {
|
||||
set({ nodeExplanation: nodeExplanationCache[nodeId] });
|
||||
return;
|
||||
}
|
||||
|
||||
const node = graph.nodes.find((n) => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
set({ nodeExplanationLoading: true, nodeExplanation: null });
|
||||
|
||||
try {
|
||||
const connections = graph.edges.filter(
|
||||
(e) => e.source === nodeId || e.target === nodeId,
|
||||
);
|
||||
const connDetails = connections
|
||||
.map((e) => {
|
||||
const isSource = e.source === nodeId;
|
||||
const otherId = isSource ? e.target : e.source;
|
||||
const otherNode = graph.nodes.find((n) => n.id === otherId);
|
||||
return `${isSource ? "->" : "<-"} [${e.type}] ${otherNode?.name ?? otherId}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const layer = graph.layers.find((l) => l.nodeIds.includes(nodeId));
|
||||
|
||||
const prompt = [
|
||||
`Explain the following code component in plain English. Be thorough but accessible.`,
|
||||
``,
|
||||
`**Component:** ${node.name}`,
|
||||
`**Type:** ${node.type}`,
|
||||
`**File:** ${node.filePath ?? "N/A"}`,
|
||||
`**Summary:** ${node.summary}`,
|
||||
`**Complexity:** ${node.complexity}`,
|
||||
`**Tags:** ${node.tags.join(", ") || "none"}`,
|
||||
layer ? `**Layer:** ${layer.name} — ${layer.description}` : "",
|
||||
``,
|
||||
`**Connections:**`,
|
||||
connDetails || " none",
|
||||
``,
|
||||
`Explain:`,
|
||||
`1. What this component does and WHY it exists`,
|
||||
`2. How it fits into the larger architecture`,
|
||||
`3. Key relationships with other components`,
|
||||
`4. Any patterns or concepts worth understanding`,
|
||||
``,
|
||||
`Keep the explanation concise (2-4 paragraphs). Use markdown formatting.`,
|
||||
].join("\n");
|
||||
|
||||
const client = new Anthropic({ apiKey, dangerouslyAllowBrowser: true });
|
||||
const response = await client.messages.create({
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 512,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
});
|
||||
|
||||
const text =
|
||||
response.content[0].type === "text"
|
||||
? response.content[0].text
|
||||
: "Unable to generate explanation.";
|
||||
|
||||
set((state) => ({
|
||||
nodeExplanation: text,
|
||||
nodeExplanationLoading: false,
|
||||
nodeExplanationCache: { ...state.nodeExplanationCache, [nodeId]: text },
|
||||
}));
|
||||
} catch (err) {
|
||||
set({
|
||||
nodeExplanation: `Error: ${err instanceof Error ? err.message : "Failed to generate explanation"}`,
|
||||
nodeExplanationLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
startTour: () => {
|
||||
const { graph } = get();
|
||||
if (!graph || graph.tour.length === 0) return;
|
||||
|
||||
Reference in New Issue
Block a user