mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(dashboard): add context-aware chat panel with Claude API integration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.78.0",
|
||||
"@dagrejs/dagre": "^2.0.4",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@understand-anything/core": "workspace:*",
|
||||
|
||||
@@ -5,6 +5,7 @@ import GraphView from "./components/GraphView";
|
||||
import CodeViewer from "./components/CodeViewer";
|
||||
import SearchBar from "./components/SearchBar";
|
||||
import NodeInfo from "./components/NodeInfo";
|
||||
import ChatPanel from "./components/ChatPanel";
|
||||
|
||||
function App() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
@@ -55,9 +56,9 @@ function App() {
|
||||
<CodeViewer />
|
||||
</div>
|
||||
|
||||
{/* Bottom-left: Chat placeholder */}
|
||||
<div className="min-h-0 min-w-0 bg-gray-800 rounded-lg flex items-center justify-center">
|
||||
<p className="text-gray-400 text-sm">Chat panel (coming soon)</p>
|
||||
{/* Bottom-left: Chat */}
|
||||
<div className="min-h-0 min-w-0">
|
||||
<ChatPanel />
|
||||
</div>
|
||||
|
||||
{/* Bottom-right: Node Info */}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useDashboardStore } from "../store";
|
||||
|
||||
export default function ChatPanel() {
|
||||
const graph = useDashboardStore((s) => s.graph);
|
||||
const selectedNodeId = useDashboardStore((s) => s.selectedNodeId);
|
||||
const apiKey = useDashboardStore((s) => s.apiKey);
|
||||
const chatMessages = useDashboardStore((s) => s.chatMessages);
|
||||
const chatLoading = useDashboardStore((s) => s.chatLoading);
|
||||
const setApiKey = useDashboardStore((s) => s.setApiKey);
|
||||
const sendChatMessage = useDashboardStore((s) => s.sendChatMessage);
|
||||
const clearChat = useDashboardStore((s) => s.clearChat);
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const [keyInput, setKeyInput] = useState("");
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectedNode = graph?.nodes.find((n) => n.id === selectedNodeId);
|
||||
|
||||
// Load API key from localStorage on mount
|
||||
useEffect(() => {
|
||||
const storedKey = localStorage.getItem("ua-api-key");
|
||||
if (storedKey) {
|
||||
setApiKey(storedKey);
|
||||
}
|
||||
}, [setApiKey]);
|
||||
|
||||
// Auto-scroll to latest message
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [chatMessages, chatLoading]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim() || chatLoading || !apiKey) return;
|
||||
sendChatMessage(input.trim());
|
||||
setInput("");
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetKey = () => {
|
||||
if (keyInput.trim()) {
|
||||
setApiKey(keyInput.trim());
|
||||
setKeyInput("");
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyInputKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSetKey();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-gray-800 rounded-lg flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-700 shrink-0">
|
||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider">
|
||||
Chat
|
||||
</h3>
|
||||
{chatMessages.length > 0 && (
|
||||
<button
|
||||
onClick={clearChat}
|
||||
className="text-[10px] text-gray-500 hover:text-gray-300 transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* API Key Input */}
|
||||
{!apiKey && (
|
||||
<div className="px-3 py-2 border-b border-gray-700 shrink-0">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="password"
|
||||
value={keyInput}
|
||||
onChange={(e) => setKeyInput(e.target.value)}
|
||||
onKeyDown={handleKeyInputKeyDown}
|
||||
placeholder="Enter Anthropic API key..."
|
||||
className="flex-1 bg-gray-700 text-xs text-white rounded px-2 py-1.5 placeholder-gray-500 outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSetKey}
|
||||
disabled={!keyInput.trim()}
|
||||
className="text-xs bg-blue-600 text-white px-2 py-1.5 rounded hover:bg-blue-500 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Set
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context Indicator */}
|
||||
{selectedNode && (
|
||||
<div className="px-3 py-1.5 border-b border-gray-700 shrink-0">
|
||||
<span className="text-[10px] text-gray-500">Context: </span>
|
||||
<span className="text-[10px] text-blue-400">{selectedNode.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto px-3 py-2 space-y-2 min-h-0">
|
||||
{chatMessages.length === 0 && (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-gray-500 text-xs text-center">
|
||||
{apiKey
|
||||
? "Ask anything about this codebase"
|
||||
: "Set your API key to start chatting"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{chatMessages.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[85%] rounded-lg px-3 py-2 text-xs leading-relaxed whitespace-pre-wrap ${
|
||||
msg.role === "user"
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-700 text-gray-200"
|
||||
}`}
|
||||
>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{chatLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-gray-700 rounded-lg px-3 py-2 text-xs text-gray-400">
|
||||
<span className="inline-flex gap-1">
|
||||
<span className="animate-pulse">Thinking</span>
|
||||
<span className="animate-pulse delay-100">.</span>
|
||||
<span className="animate-pulse delay-200">.</span>
|
||||
<span className="animate-pulse delay-300">.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="px-3 py-2 border-t border-gray-700 shrink-0">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
apiKey
|
||||
? "Ask about this codebase..."
|
||||
: "Set API key first"
|
||||
}
|
||||
disabled={!apiKey || chatLoading}
|
||||
className="flex-1 bg-gray-700 text-xs text-white rounded px-2 py-1.5 placeholder-gray-500 outline-none focus:ring-1 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!apiKey || chatLoading || !input.trim()}
|
||||
className="text-xs bg-blue-600 text-white px-3 py-1.5 rounded hover:bg-blue-500 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { create } from "zustand";
|
||||
import { SearchEngine } from "@understand-anything/core/search";
|
||||
import type { SearchResult } from "@understand-anything/core/search";
|
||||
import type { KnowledgeGraph } from "@understand-anything/core/types";
|
||||
|
||||
export interface ChatMessage {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface DashboardStore {
|
||||
graph: KnowledgeGraph | null;
|
||||
selectedNodeId: string | null;
|
||||
@@ -10,9 +16,87 @@ interface DashboardStore {
|
||||
searchResults: SearchResult[];
|
||||
searchEngine: SearchEngine | null;
|
||||
|
||||
apiKey: string;
|
||||
chatMessages: ChatMessage[];
|
||||
chatLoading: boolean;
|
||||
|
||||
setGraph: (graph: KnowledgeGraph) => void;
|
||||
selectNode: (nodeId: string | null) => void;
|
||||
setSearchQuery: (query: string) => void;
|
||||
setApiKey: (key: string) => void;
|
||||
sendChatMessage: (message: string) => Promise<void>;
|
||||
clearChat: () => void;
|
||||
}
|
||||
|
||||
function buildSystemPrompt(
|
||||
graph: KnowledgeGraph | null,
|
||||
selectedNodeId: string | null,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(
|
||||
"You are an expert code assistant for the Understand Anything dashboard. " +
|
||||
"You help users understand codebases by answering questions about the project structure, " +
|
||||
"code relationships, and architecture. Be concise and helpful.",
|
||||
);
|
||||
|
||||
if (graph) {
|
||||
const { project, nodes, edges, layers } = graph;
|
||||
parts.push(
|
||||
`\n## Project: ${project.name}\n` +
|
||||
`Description: ${project.description}\n` +
|
||||
`Languages: ${project.languages.join(", ")}\n` +
|
||||
`Frameworks: ${project.frameworks.join(", ")}\n` +
|
||||
`Analyzed at: ${project.analyzedAt}`,
|
||||
);
|
||||
|
||||
parts.push(
|
||||
`\n## Graph Overview\n` +
|
||||
`Nodes: ${nodes.length}\n` +
|
||||
`Edges: ${edges.length}\n` +
|
||||
`Layers: ${layers.map((l) => l.name).join(", ") || "none"}`,
|
||||
);
|
||||
|
||||
// Include a summary of all nodes for context
|
||||
const nodeSummaries = nodes
|
||||
.map(
|
||||
(n) =>
|
||||
`- [${n.type}] ${n.name}${n.filePath ? ` (${n.filePath})` : ""}: ${n.summary}`,
|
||||
)
|
||||
.join("\n");
|
||||
parts.push(`\n## All Nodes\n${nodeSummaries}`);
|
||||
|
||||
// Selected node details
|
||||
if (selectedNodeId) {
|
||||
const node = nodes.find((n) => n.id === selectedNodeId);
|
||||
if (node) {
|
||||
const connections = edges.filter(
|
||||
(e) => e.source === node.id || e.target === node.id,
|
||||
);
|
||||
const connDetails = connections
|
||||
.map((e) => {
|
||||
const isSource = e.source === node.id;
|
||||
const otherId = isSource ? e.target : e.source;
|
||||
const otherNode = nodes.find((n) => n.id === otherId);
|
||||
return ` ${isSource ? "->" : "<-"} [${e.type}] ${otherNode?.name ?? otherId}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
parts.push(
|
||||
`\n## Currently Selected Node\n` +
|
||||
`Name: ${node.name}\n` +
|
||||
`Type: ${node.type}\n` +
|
||||
`Summary: ${node.summary}\n` +
|
||||
`File: ${node.filePath ?? "N/A"}\n` +
|
||||
`Tags: ${node.tags.join(", ") || "none"}\n` +
|
||||
`Complexity: ${node.complexity}\n` +
|
||||
`Connections:\n${connDetails || " none"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
@@ -22,6 +106,10 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
searchResults: [],
|
||||
searchEngine: null,
|
||||
|
||||
apiKey: "",
|
||||
chatMessages: [],
|
||||
chatLoading: false,
|
||||
|
||||
setGraph: (graph) => {
|
||||
const searchEngine = new SearchEngine(graph.nodes);
|
||||
const query = get().searchQuery;
|
||||
@@ -38,4 +126,65 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
|
||||
const searchResults = engine.search(query);
|
||||
set({ searchQuery: query, searchResults });
|
||||
},
|
||||
|
||||
setApiKey: (key) => {
|
||||
localStorage.setItem("ua-api-key", key);
|
||||
set({ apiKey: key });
|
||||
},
|
||||
|
||||
sendChatMessage: async (message) => {
|
||||
const { apiKey, chatMessages, graph, selectedNodeId } = get();
|
||||
if (!apiKey || !message.trim()) return;
|
||||
|
||||
const userMessage: ChatMessage = { role: "user", content: message };
|
||||
set({
|
||||
chatMessages: [...chatMessages, userMessage],
|
||||
chatLoading: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const client = new Anthropic({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
|
||||
const systemPrompt = buildSystemPrompt(graph, selectedNodeId);
|
||||
|
||||
const response = await client.messages.create({
|
||||
model: "claude-sonnet-4-20250514",
|
||||
max_tokens: 1024,
|
||||
system: systemPrompt,
|
||||
messages: [...chatMessages, userMessage].map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
});
|
||||
|
||||
const assistantContent =
|
||||
response.content[0].type === "text"
|
||||
? response.content[0].text
|
||||
: "Unable to generate a response.";
|
||||
|
||||
const assistantMessage: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: assistantContent,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
chatMessages: [...state.chatMessages, assistantMessage],
|
||||
chatLoading: false,
|
||||
}));
|
||||
} catch (err) {
|
||||
const errorMessage: ChatMessage = {
|
||||
role: "assistant",
|
||||
content: `Error: ${err instanceof Error ? err.message : "Failed to get response. Please check your API key."}`,
|
||||
};
|
||||
set((state) => ({
|
||||
chatMessages: [...state.chatMessages, errorMessage],
|
||||
chatLoading: false,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
clearChat: () => set({ chatMessages: [] }),
|
||||
}));
|
||||
|
||||
Generated
+39
-9
@@ -45,6 +45,9 @@ importers:
|
||||
|
||||
packages/dashboard:
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.78.0
|
||||
version: 0.78.0(zod@4.3.6)
|
||||
'@dagrejs/dagre':
|
||||
specifier: ^2.0.4
|
||||
version: 2.0.4
|
||||
@@ -107,6 +110,15 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@anthropic-ai/sdk@0.78.0':
|
||||
resolution: {integrity: sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -178,6 +190,10 @@ packages:
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0-0
|
||||
|
||||
'@babel/runtime@7.28.6':
|
||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1019,6 +1035,10 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1245,6 +1265,9 @@ packages:
|
||||
tree-sitter:
|
||||
optional: true
|
||||
|
||||
ts-algebra@2.0.0:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -1429,6 +1452,12 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@anthropic-ai/sdk@0.78.0(zod@4.3.6)':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
optionalDependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
'@babel/code-frame@7.29.0':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
@@ -1518,6 +1547,8 @@ snapshots:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/helper-plugin-utils': 7.28.6
|
||||
|
||||
'@babel/runtime@7.28.6': {}
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
@@ -1968,14 +1999,6 @@ snapshots:
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
@@ -2209,6 +2232,11 @@ snapshots:
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
ts-algebra: 2.0.0
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
lightningcss-android-arm64@1.31.1:
|
||||
@@ -2394,6 +2422,8 @@ snapshots:
|
||||
node-gyp-build: 4.8.4
|
||||
tree-sitter-javascript: 0.23.1
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
@@ -2512,7 +2542,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
|
||||
Reference in New Issue
Block a user