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) => (
+
+ ))}
+
+ {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