From 7a820068baf669a0f336d6451229d0008d2cc044 Mon Sep 17 00:00:00 2001 From: abhigyanpatwari Date: Sun, 11 Jan 2026 20:39:22 +0530 Subject: [PATCH] better regex for grounding, UI improvements --- src/components/RightPanel.tsx | 23 +++--- src/core/llm/agent.ts | 21 +++--- src/hooks/useAppState.tsx | 128 +++++++++++++++++----------------- src/index.css | 111 +++++++++++++++++++++-------- 4 files changed, 164 insertions(+), 119 deletions(-) diff --git a/src/components/RightPanel.tsx b/src/components/RightPanel.tsx index 400d47d1a..f36de931d 100644 --- a/src/components/RightPanel.tsx +++ b/src/components/RightPanel.tsx @@ -49,6 +49,14 @@ export const RightPanel = () => { const [chatInput, setChatInput] = useState(''); const textareaRef = useRef(null); + const messagesEndRef = useRef(null); + + // Auto-scroll to bottom when messages update or while streaming + useEffect(() => { + if (messagesEndRef.current) { + messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [chatMessages, isChatLoading]); const resolveFilePathForUI = useCallback((requestedPath: string): string | null => { const req = requestedPath.replace(/\\/g, '/').replace(/^\.?\//, '').toLowerCase(); @@ -383,17 +391,8 @@ export const RightPanel = () => { ); } - return ( - - {children} - - ); + // Non-citation link - just render as plain text (not a useless external link) + return {children}; }, code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || ''); @@ -631,6 +630,8 @@ export const RightPanel = () => { )} + {/* Scroll anchor for auto-scroll */} +
{/* Input */} diff --git a/src/core/llm/agent.ts b/src/core/llm/agent.ts index 9e0ecb59f..1f04ca108 100644 --- a/src/core/llm/agent.ts +++ b/src/core/llm/agent.ts @@ -47,22 +47,22 @@ import { export const BASE_SYSTEM_PROMPT = `You are Nexus, a Code Analysis Agent with access to a Knowledge Graph. Your responses MUST be grounded. ## ⚠️ MANDATORY: GROUNDING -Every factual claim MUST include a citation: \`[[filename:line]]\` +Every factual claim MUST include a citation in [[file:line]] format. - When you mention a function, class, or file → cite where you found it - When you describe behavior → cite the code that proves it - NO citation = NO claim. If you can't cite it, say "I didn't find evidence for this." Examples: -- "The \`AuthService\` handles login [[src/services/auth.ts:45]]." -- "This function calls \`validateToken\` [[src/utils.ts:12]] which throws on invalid tokens [[src/utils.ts:28]]." +- "The AuthService handles login [[src/services/auth.ts:45]]." +- "This function calls validateToken [[src/utils.ts:12]] which throws on invalid tokens [[src/utils.ts:28]]." ## 🧠 CORE PROTOCOL You are an investigator. For each question: -1. **Search** → Use \`search\` or \`grep\` to find relevant code -2. **Read** → Use \`read\` to see the actual source -3. **Trace** → Use \`cypher\` to follow connections in the graph -4. **Cite** → Ground every finding with \`[[file:line]]\` -5. **Highlight** → Visualize key nodes with \`highlight\` +1. **Search** → Use search or grep to find relevant code +2. **Read** → Use read to see the actual source +3. **Trace** → Use cypher to follow connections in the graph +4. **Cite** → Ground every finding with [[file:line]] +5. **Highlight** → Visualize key nodes with highlight ## 🛠️ TOOLS - **\`search\`** — Hybrid search (keyword + semantic). Returns code matches with graph connections. @@ -178,11 +178,6 @@ export const createGraphRAGAgent = ( ? buildDynamicSystemPrompt(BASE_SYSTEM_PROMPT, codebaseContext) : BASE_SYSTEM_PROMPT; - // Log the full prompt for debugging - if (import.meta.env?.DEV) { - console.log('📝 Full System Prompt:\n', systemPrompt); - } - const agent = createReactAgent({ llm: model as any, tools: tools as any, diff --git a/src/hooks/useAppState.tsx b/src/hooks/useAppState.tsx index fe140a086..cee6084d5 100644 --- a/src/hooks/useAppState.tsx +++ b/src/hooks/useAppState.tsx @@ -44,17 +44,17 @@ interface AppState { // View state viewMode: ViewMode; setViewMode: (mode: ViewMode) => void; - + // Graph data graph: KnowledgeGraph | null; setGraph: (graph: KnowledgeGraph | null) => void; fileContents: Map; setFileContents: (contents: Map) => void; - + // Selection selectedNode: GraphNode | null; setSelectedNode: (node: GraphNode | null) => void; - + // Right Panel (unified Code + Chat) isRightPanelOpen: boolean; setRightPanelOpen: (open: boolean) => void; @@ -62,15 +62,15 @@ interface AppState { setRightPanelTab: (tab: RightPanelTab) => void; openCodePanel: () => void; openChatPanel: () => void; - + // Filters visibleLabels: NodeLabel[]; toggleLabelVisibility: (label: NodeLabel) => void; - + // Depth filter (N hops from selection) depthFilter: number | null; setDepthFilter: (depth: number | null) => void; - + // Query state highlightedNodeIds: Set; setHighlightedNodeIds: (ids: Set) => void; @@ -83,34 +83,34 @@ interface AppState { queryResult: QueryResult | null; setQueryResult: (result: QueryResult | null) => void; clearQueryHighlights: () => void; - + // Progress progress: PipelineProgress | null; setProgress: (progress: PipelineProgress | null) => void; - + // Project info projectName: string; setProjectName: (name: string) => void; - + // Worker API (shared across app) runPipeline: (file: File, onProgress: (p: PipelineProgress) => void) => Promise; runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void) => Promise; runQuery: (cypher: string) => Promise; isDatabaseReady: () => Promise; - + // Embedding state embeddingStatus: EmbeddingStatus; embeddingProgress: EmbeddingProgress | null; - + // Embedding methods startEmbeddings: (forceDevice?: 'webgpu' | 'wasm') => Promise; semanticSearch: (query: string, k?: number) => Promise; semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise; isEmbeddingReady: boolean; - + // Debug/test methods testArrayParams: () => Promise<{ success: boolean; error?: string }>; - + // LLM/Agent state llmSettings: LLMSettings; isSettingsPanelOpen: boolean; @@ -118,18 +118,18 @@ interface AppState { isAgentReady: boolean; isAgentInitializing: boolean; agentError: string | null; - + // Chat state chatMessages: ChatMessage[]; isChatLoading: boolean; currentToolCalls: ToolCallInfo[]; - + // LLM methods refreshLLMSettings: () => void; initializeAgent: () => Promise; sendChatMessage: (message: string) => Promise; clearChat: () => void; - + // Code References Panel codeReferences: CodeReference[]; isCodePanelOpen: boolean; @@ -146,36 +146,36 @@ const AppStateContext = createContext(null); export const AppStateProvider = ({ children }: { children: ReactNode }) => { // View state const [viewMode, setViewMode] = useState('onboarding'); - + // Graph data const [graph, setGraph] = useState(null); const [fileContents, setFileContents] = useState>(new Map()); - + // Selection const [selectedNode, setSelectedNode] = useState(null); - + // Right Panel const [isRightPanelOpen, setRightPanelOpen] = useState(false); const [rightPanelTab, setRightPanelTab] = useState('code'); - + const openCodePanel = useCallback(() => { // Legacy API: used by graph/tree selection. // Code is now shown in the Code References Panel (left of the graph), // so "openCodePanel" just ensures that panel becomes visible when needed. setCodePanelOpen(true); }, []); - + const openChatPanel = useCallback(() => { setRightPanelOpen(true); setRightPanelTab('chat'); }, []); - + // Filters const [visibleLabels, setVisibleLabels] = useState(DEFAULT_VISIBLE_LABELS); - + // Depth filter const [depthFilter, setDepthFilter] = useState(null); - + // Query state const [highlightedNodeIds, setHighlightedNodeIds] = useState>(new Set()); const [queryResult, setQueryResult] = useState(null); @@ -192,34 +192,34 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { const clearAIToolHighlights = useCallback(() => { setAIToolHighlightedNodeIds(new Set()); }, []); - + const clearQueryHighlights = useCallback(() => { setHighlightedNodeIds(new Set()); setQueryResult(null); }, []); - + // Progress const [progress, setProgress] = useState(null); - + // Project info const [projectName, setProjectName] = useState(''); - + // Embedding state const [embeddingStatus, setEmbeddingStatus] = useState('idle'); const [embeddingProgress, setEmbeddingProgress] = useState(null); - + // LLM/Agent state const [llmSettings, setLLMSettings] = useState(loadSettings); const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false); const [isAgentReady, setIsAgentReady] = useState(false); const [isAgentInitializing, setIsAgentInitializing] = useState(false); const [agentError, setAgentError] = useState(null); - + // Chat state const [chatMessages, setChatMessages] = useState([]); const [isChatLoading, setIsChatLoading] = useState(false); const [currentToolCalls, setCurrentToolCalls] = useState([]); - + // Code References Panel state const [codeReferences, setCodeReferences] = useState([]); const [isCodePanelOpen, setCodePanelOpen] = useState(false); @@ -278,18 +278,18 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { const addCodeReference = useCallback((ref: Omit) => { const id = `ref-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const newRef: CodeReference = { ...ref, id }; - + setCodeReferences(prev => { // Don't add duplicates (same file + line range) - const isDuplicate = prev.some(r => - r.filePath === ref.filePath && - r.startLine === ref.startLine && + const isDuplicate = prev.some(r => + r.filePath === ref.filePath && + r.startLine === ref.startLine && r.endLine === ref.endLine ); if (isDuplicate) return prev; return [...prev, newRef]; }); - + // Auto-open panel when references are added setCodePanelOpen(true); @@ -302,7 +302,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { endLine: ref.endLine, ts: Date.now(), }); - + // Track AI highlights separately so they can be toggled off in the UI if (ref.nodeId && ref.source === 'ai') { setAICitationHighlightedNodeIds(prev => new Set([...prev, ref.nodeId!])); @@ -367,7 +367,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { ): Promise => { const api = apiRef.current; if (!api) throw new Error('Worker not initialized'); - + const proxiedOnProgress = Comlink.proxy(onProgress); const serializedResult = await api.runPipeline(file, proxiedOnProgress); return deserializePipelineResult(serializedResult, createKnowledgeGraph); @@ -379,7 +379,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { ): Promise => { const api = apiRef.current; if (!api) throw new Error('Worker not initialized'); - + const proxiedOnProgress = Comlink.proxy(onProgress); const serializedResult = await api.runPipelineFromFiles(files, proxiedOnProgress); return deserializePipelineResult(serializedResult, createKnowledgeGraph); @@ -405,14 +405,14 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { const startEmbeddings = useCallback(async (forceDevice?: 'webgpu' | 'wasm'): Promise => { const api = apiRef.current; if (!api) throw new Error('Worker not initialized'); - + setEmbeddingStatus('loading'); setEmbeddingProgress(null); - + try { const proxiedOnProgress = Comlink.proxy((progress: EmbeddingProgress) => { setEmbeddingProgress(progress); - + // Update status based on phase switch (progress.phase) { case 'loading-model': @@ -432,12 +432,12 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { break; } }); - + await api.startEmbeddingPipeline(proxiedOnProgress, forceDevice); } catch (error: any) { // Check if it's WebGPU not available - let caller handle the dialog - if (error?.name === 'WebGPUNotAvailableError' || - error?.message?.includes('WebGPU not available')) { + if (error?.name === 'WebGPUNotAvailableError' || + error?.message?.includes('WebGPU not available')) { setEmbeddingStatus('idle'); // Reset to idle so user can try again } else { setEmbeddingStatus('error'); @@ -581,7 +581,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { .map(s => s.content) .filter(Boolean); const content = contentParts.join('\n\n'); - + setChatMessages(prev => { const existing = prev.find(m => m.id === assistantMessageId); const newMessage: ChatMessage = { @@ -717,17 +717,17 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { idx = toolCallsForMessage.findIndex(t => t.name === tc.name && !t.result); } if (idx >= 0) { - toolCallsForMessage[idx] = { - ...toolCallsForMessage[idx], - result: tc.result, - status: 'completed' + toolCallsForMessage[idx] = { + ...toolCallsForMessage[idx], + result: tc.result, + status: 'completed' }; } - + // Also update the tool call in steps - const stepIdx = stepsForMessage.findIndex(s => + const stepIdx = stepsForMessage.findIndex(s => s.type === 'tool_call' && s.toolCall && ( - s.toolCall.id === tc.id || + s.toolCall.id === tc.id || (s.toolCall.name === tc.name && s.toolCall.status === 'running') ) ); @@ -741,7 +741,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { }, }; } - + // Update currentToolCalls setCurrentToolCalls(prev => { let targetIdx = prev.findIndex(t => t.id === tc.id); @@ -752,16 +752,16 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { targetIdx = prev.findIndex(t => t.name === tc.name && !t.result); } if (targetIdx >= 0) { - return prev.map((t, i) => i === targetIdx - ? { ...t, result: tc.result, status: 'completed' } + return prev.map((t, i) => i === targetIdx + ? { ...t, result: tc.result, status: 'completed' } : t ); } return prev; }); - + updateMessage(); - + // Parse highlight marker from tool results if (tc.result) { const highlightMatch = tc.result.match(/\[HIGHLIGHT_NODES:([^\]]+)\]/); @@ -770,12 +770,12 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { if (rawIds.length > 0 && graph) { const matchedIds = new Set(); const graphNodeIds = graph.nodes.map(n => n.id); - + for (const rawId of rawIds) { if (graphNodeIds.includes(rawId)) { matchedIds.add(rawId); } else { - const found = graphNodeIds.find(gid => + const found = graphNodeIds.find(gid => gid.endsWith(rawId) || gid.endsWith(':' + rawId) ); if (found) { @@ -783,7 +783,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { } } } - + if (matchedIds.size > 0) { setAIToolHighlightedNodeIds(matchedIds); } @@ -826,7 +826,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { setCodeReferences(prev => { const ref = prev.find(r => r.id === id); const newRefs = prev.filter(r => r.id !== id); - + // Remove AI citation highlight if this was the only AI reference to that node if (ref?.nodeId && ref.source === 'ai') { const stillReferenced = newRefs.some(r => r.nodeId === ref.nodeId && r.source === 'ai'); @@ -838,12 +838,12 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => { }); } } - + // Auto-close panel if no references left AND no selection in top viewer if (newRefs.length === 0 && !selectedNode) { setCodePanelOpen(false); } - + return newRefs; }); }, [selectedNode]); diff --git a/src/index.css b/src/index.css index 094d639bc..c62031c0b 100644 --- a/src/index.css +++ b/src/index.css @@ -10,20 +10,20 @@ --color-surface: #101018; --color-elevated: #16161f; --color-hover: #1c1c28; - + /* Borders */ --color-border-subtle: #1e1e2a; --color-border-default: #2a2a3a; - + /* Text */ --color-text-primary: #e4e4ed; --color-text-secondary: #8888a0; --color-text-muted: #5a5a70; - + /* Accent */ --color-accent: #7c3aed; --color-accent-dim: #5b21b6; - + /* Node colors */ --color-node-file: #3b82f6; --color-node-folder: #6366f1; @@ -51,40 +51,65 @@ /* Keyframes */ @keyframes breathe { - 0%, 100% { + + 0%, + 100% { border-color: #2a2a3a; box-shadow: 0 0 0 0 rgba(124, 58, 237, 0.3); } - 50% { + + 50% { border-color: #7c3aed; box-shadow: 0 0 40px 10px rgba(124, 58, 237, 0.3); } } @keyframes pulse-glow { - 0%, 100% { + + 0%, + 100% { transform: scale(1); box-shadow: 0 0 40px rgba(124, 58, 237, 0.4); } - 50% { + + 50% { transform: scale(1.1); box-shadow: 0 0 80px rgba(124, 58, 237, 0.6); } } @keyframes slide-in { - from { opacity: 0; transform: translateX(20px); } - to { opacity: 1; transform: translateX(0); } + from { + opacity: 0; + transform: translateX(20px); + } + + to { + opacity: 1; + transform: translateX(0); + } } @keyframes slide-up { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } } @keyframes fade-in { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + + to { + opacity: 1; + } } /* ═══════════════════════════════════════════════════════════════ @@ -94,7 +119,9 @@ box-sizing: border-box; } -html, body, #root { +html, +body, +#root { height: 100%; } @@ -182,15 +209,29 @@ body { line-height: 1.3; } -.chat-prose h1 { font-size: 1.25em; } -.chat-prose h2 { font-size: 1.125em; } -.chat-prose h3 { font-size: 1em; } -.chat-prose h4 { font-size: 0.875em; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-text-secondary); } +.chat-prose h1 { + font-size: 1.25em; +} -.chat-prose > h1:first-child, -.chat-prose > h2:first-child, -.chat-prose > h3:first-child, -.chat-prose > h4:first-child { +.chat-prose h2 { + font-size: 1.125em; +} + +.chat-prose h3 { + font-size: 1em; +} + +.chat-prose h4 { + font-size: 0.875em; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary); +} + +.chat-prose>h1:first-child, +.chat-prose>h2:first-child, +.chat-prose>h3:first-child, +.chat-prose>h4:first-child { margin-top: 0; } @@ -217,15 +258,23 @@ body { margin: 0.25em 0; } -/* Inline code */ +/* Inline code - VS Code style */ .chat-prose code:not([class*="language-"]) { - padding: 0.2em 0.4em; - background: var(--color-surface); - border-radius: 4px; + padding: 0.2em 0.5em; + background: rgba(110, 118, 129, 0.2); + border-radius: 6px; font-family: var(--font-mono); - font-size: 0.85em; - color: var(--color-accent); - border: 1px solid var(--color-border-subtle); + font-size: 0.875em; + color: #e6b450 !important; + border: 1px solid rgba(110, 118, 129, 0.3); + font-weight: 500; +} + +/* Ensure inline code keeps its color inside links and other elements */ +.chat-prose a code:not([class*="language-"]), +.chat-prose strong code:not([class*="language-"]), +.chat-prose em code:not([class*="language-"]) { + color: #e6b450 !important; } /* Code blocks */ @@ -302,4 +351,4 @@ body { .sigma-container canvas { outline: none; -} +} \ No newline at end of file