better regex for grounding, UI improvements

This commit is contained in:
abhigyanpatwari 2026-01-11 20:39:22 +05:30
parent 569466e446
commit 7a820068ba
4 changed files with 164 additions and 119 deletions

View file

@ -49,6 +49,14 @@ export const RightPanel = () => {
const [chatInput, setChatInput] = useState('');
const textareaRef = useRef<HTMLTextAreaElement>(null);
const messagesEndRef = useRef<HTMLDivElement>(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 = () => {
</a>
);
}
return (
<a
href={href}
className="text-accent underline underline-offset-2 hover:text-purple-300"
target="_blank"
rel="noopener noreferrer"
{...props}
>
{children}
</a>
);
// Non-citation link - just render as plain text (not a useless external link)
return <span {...props}>{children}</span>;
},
code: ({ className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '');
@ -631,6 +630,8 @@ export const RightPanel = () => {
</div>
)}
{/* Scroll anchor for auto-scroll */}
<div ref={messagesEndRef} />
</div>
{/* Input */}

View file

@ -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,

View file

@ -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<string, string>;
setFileContents: (contents: Map<string, string>) => 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<string>;
setHighlightedNodeIds: (ids: Set<string>) => 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<PipelineResult>;
runPipelineFromFiles: (files: FileEntry[], onProgress: (p: PipelineProgress) => void) => Promise<PipelineResult>;
runQuery: (cypher: string) => Promise<any[]>;
isDatabaseReady: () => Promise<boolean>;
// Embedding state
embeddingStatus: EmbeddingStatus;
embeddingProgress: EmbeddingProgress | null;
// Embedding methods
startEmbeddings: (forceDevice?: 'webgpu' | 'wasm') => Promise<void>;
semanticSearch: (query: string, k?: number) => Promise<SemanticSearchResult[]>;
semanticSearchWithContext: (query: string, k?: number, hops?: number) => Promise<any[]>;
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<void>;
sendChatMessage: (message: string) => Promise<void>;
clearChat: () => void;
// Code References Panel
codeReferences: CodeReference[];
isCodePanelOpen: boolean;
@ -146,36 +146,36 @@ const AppStateContext = createContext<AppState | null>(null);
export const AppStateProvider = ({ children }: { children: ReactNode }) => {
// View state
const [viewMode, setViewMode] = useState<ViewMode>('onboarding');
// Graph data
const [graph, setGraph] = useState<KnowledgeGraph | null>(null);
const [fileContents, setFileContents] = useState<Map<string, string>>(new Map());
// Selection
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
// Right Panel
const [isRightPanelOpen, setRightPanelOpen] = useState(false);
const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab>('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<NodeLabel[]>(DEFAULT_VISIBLE_LABELS);
// Depth filter
const [depthFilter, setDepthFilter] = useState<number | null>(null);
// Query state
const [highlightedNodeIds, setHighlightedNodeIds] = useState<Set<string>>(new Set());
const [queryResult, setQueryResult] = useState<QueryResult | null>(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<PipelineProgress | null>(null);
// Project info
const [projectName, setProjectName] = useState<string>('');
// Embedding state
const [embeddingStatus, setEmbeddingStatus] = useState<EmbeddingStatus>('idle');
const [embeddingProgress, setEmbeddingProgress] = useState<EmbeddingProgress | null>(null);
// LLM/Agent state
const [llmSettings, setLLMSettings] = useState<LLMSettings>(loadSettings);
const [isSettingsPanelOpen, setSettingsPanelOpen] = useState(false);
const [isAgentReady, setIsAgentReady] = useState(false);
const [isAgentInitializing, setIsAgentInitializing] = useState(false);
const [agentError, setAgentError] = useState<string | null>(null);
// Chat state
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
const [isChatLoading, setIsChatLoading] = useState(false);
const [currentToolCalls, setCurrentToolCalls] = useState<ToolCallInfo[]>([]);
// Code References Panel state
const [codeReferences, setCodeReferences] = useState<CodeReference[]>([]);
const [isCodePanelOpen, setCodePanelOpen] = useState(false);
@ -278,18 +278,18 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
const addCodeReference = useCallback((ref: Omit<CodeReference, 'id'>) => {
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<PipelineResult> => {
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<PipelineResult> => {
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<void> => {
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<string>();
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]);

View file

@ -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;
}
}