diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index b5b5358..62bc08b 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -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:*", diff --git a/packages/dashboard/src/App.tsx b/packages/dashboard/src/App.tsx index 61a3c20..6ff1b21 100644 --- a/packages/dashboard/src/App.tsx +++ b/packages/dashboard/src/App.tsx @@ -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() { - {/* Bottom-left: Chat placeholder */} -
-

Chat panel (coming soon)

+ {/* Bottom-left: Chat */} +
+
{/* Bottom-right: Node Info */} diff --git a/packages/dashboard/src/components/ChatPanel.tsx b/packages/dashboard/src/components/ChatPanel.tsx new file mode 100644 index 0000000..5013471 --- /dev/null +++ b/packages/dashboard/src/components/ChatPanel.tsx @@ -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(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 ( +
+ {/* Header */} +
+

+ Chat +

+ {chatMessages.length > 0 && ( + + )} +
+ + {/* API Key Input */} + {!apiKey && ( +
+
+ 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" + /> + +
+
+ )} + + {/* Context Indicator */} + {selectedNode && ( +
+ Context: + {selectedNode.name} +
+ )} + + {/* Messages */} +
+ {chatMessages.length === 0 && ( +
+

+ {apiKey + ? "Ask anything about this codebase" + : "Set your API key to start chatting"} +

+
+ )} + + {chatMessages.map((msg, i) => ( +
+
+ {msg.content} +
+
+ ))} + + {chatLoading && ( +
+
+ + Thinking + . + . + . + +
+
+ )} + +
+
+ + {/* Input */} +
+
+ 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" + /> + +
+
+
+ ); +} diff --git a/packages/dashboard/src/store.ts b/packages/dashboard/src/store.ts index 37b8904..5dcdf92 100644 --- a/packages/dashboard/src/store.ts +++ b/packages/dashboard/src/store.ts @@ -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; + 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()((set, get) => ({ @@ -22,6 +106,10 @@ export const useDashboardStore = create()((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()((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: [] }), })); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84d6e77..7f8183b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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