Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ coverage/




789 changes: 788 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"langchain": "^0.3.37",
"lru-cache": "^11.2.4",
"lucide-react": "^0.562.0",
"mermaid": "^11.12.2",
"minisearch": "^7.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
56 changes: 37 additions & 19 deletions src/components/CodeReferencesPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Code, PanelLeftClose, PanelLeft, Trash2, X, Target } from 'lucide-react';
import { Code, PanelLeftClose, PanelLeft, Trash2, X, Target, FileCode, Sparkles, MousePointerClick } from 'lucide-react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { useAppState } from '../hooks/useAppState';
Expand Down Expand Up @@ -131,15 +131,22 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
<aside className="h-full w-12 bg-surface border-r border-border-subtle flex flex-col items-center py-3 gap-2 flex-shrink-0">
<button
onClick={() => setIsCollapsed(false)}
className="p-2 text-text-secondary hover:text-text-primary hover:bg-hover rounded transition-colors"
className="p-2 text-text-secondary hover:text-cyan-400 hover:bg-cyan-500/10 rounded transition-colors"
title="Expand Code Panel"
>
<PanelLeft className="w-5 h-5" />
</button>
<div className="w-6 h-px bg-border-subtle my-1" />
<div className="text-[10px] text-text-muted rotate-90 whitespace-nowrap font-mono">
{showCitations ? `${aiReferences.length} refs` : 'Selected'}
</div>
{showSelectedViewer && (
<div className="text-[9px] text-amber-400 rotate-90 whitespace-nowrap font-medium tracking-wide">
SELECTED
</div>
)}
{showCitations && (
<div className="text-[9px] text-cyan-400 rotate-90 whitespace-nowrap font-medium tracking-wide mt-4">
AI • {aiReferences.length}
</div>
)}
</aside>
);
}
Expand All @@ -157,18 +164,17 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
title="Drag to resize"
/>
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 border-b border-border-subtle bg-elevated/40">
<div className="flex items-center justify-between px-3 py-2.5 border-b border-border-subtle bg-gradient-to-r from-elevated/60 to-surface/60">
<div className="flex items-center gap-2">
<Code className="w-4 h-4 text-cyan-300" />
<span className="text-sm font-medium">Code</span>
{showCitations && <span className="text-xs text-text-muted">• {aiReferences.length} refs</span>}
<Code className="w-4 h-4 text-cyan-400" />
<span className="text-sm font-semibold text-text-primary">Code Inspector</span>
</div>
<div className="flex items-center gap-1.5">
{showCitations && (
<button
onClick={() => clearCodeReferences()}
className="p-1.5 text-text-muted hover:text-text-primary hover:bg-hover rounded transition-colors"
title="Clear all code references"
className="p-1.5 text-text-muted hover:text-red-400 hover:bg-red-500/10 rounded transition-colors"
title="Clear AI citations"
>
<Trash2 className="w-4 h-4" />
</button>
Expand All @@ -187,14 +193,18 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
{/* Top: Selected file viewer (when a node is selected) */}
{showSelectedViewer && (
<div className={`${showCitations ? 'h-[42%]' : 'flex-1'} min-h-0 flex flex-col`}>
<div className="px-3 py-2 bg-surface/40 border-b border-border-subtle flex items-center gap-2">
<span className="text-[11px] text-text-muted uppercase tracking-wide">Selected</span>
<div className="px-3 py-2 bg-gradient-to-r from-amber-500/8 to-orange-500/5 border-b border-amber-500/20 flex items-center gap-2">
<div className="flex items-center gap-1.5 px-2 py-0.5 bg-amber-500/15 rounded-md border border-amber-500/25">
<MousePointerClick className="w-3 h-3 text-amber-400" />
<span className="text-[10px] text-amber-300 font-semibold uppercase tracking-wide">Selected</span>
</div>
<FileCode className="w-3.5 h-3.5 text-amber-400/70 ml-1" />
<span className="text-xs text-text-primary font-mono truncate flex-1">
{selectedNode?.properties?.filePath ?? selectedNode?.properties?.name}
{selectedNode?.properties?.filePath?.split('/').pop() ?? selectedNode?.properties?.name}
</span>
<button
onClick={() => setSelectedNode(null)}
className="p-1 text-text-muted hover:text-text-primary hover:bg-hover rounded transition-colors"
className="p-1 text-text-muted hover:text-amber-400 hover:bg-amber-500/10 rounded transition-colors"
title="Clear selection"
>
<X className="w-4 h-4" />
Expand Down Expand Up @@ -255,12 +265,21 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =

{/* Divider between Selected viewer and AI refs (more visible) */}
{showSelectedViewer && showCitations && (
<div className="h-2 bg-gradient-to-r from-cyan-500/0 via-cyan-400/35 to-cyan-500/0 border-y border-cyan-400/25" />
<div className="h-1.5 bg-gradient-to-r from-transparent via-border-subtle to-transparent" />
)}

{/* Bottom: AI citations list */}
{showCitations && (
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin p-3 space-y-3">
<div className="flex-1 min-h-0 flex flex-col">
{/* AI Citations Section Header */}
<div className="px-3 py-2 bg-gradient-to-r from-cyan-500/8 to-teal-500/5 border-b border-cyan-500/20 flex items-center gap-2">
<div className="flex items-center gap-1.5 px-2 py-0.5 bg-cyan-500/15 rounded-md border border-cyan-500/25">
<Sparkles className="w-3 h-3 text-cyan-400" />
<span className="text-[10px] text-cyan-300 font-semibold uppercase tracking-wide">AI Citations</span>
</div>
<span className="text-xs text-text-muted ml-1">{aiReferences.length} reference{aiReferences.length !== 1 ? 's' : ''}</span>
</div>
<div className="flex-1 min-h-0 overflow-y-auto scrollbar-thin p-3 space-y-3">
{refsWithSnippets.map(({ ref, content, start, highlightStart, highlightEnd, totalLines }) => {
const nodeColor = ref.label ? (NODE_COLORS as any)[ref.label] || '#6b7280' : '#6b7280';
const hasRange = typeof ref.startLine === 'number';
Expand Down Expand Up @@ -368,11 +387,10 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
</div>
);
})}
</div>
</div>
)}
</div>
</aside>
);
};


29 changes: 27 additions & 2 deletions src/components/FileTreePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ const TreeItem = ({
onClick={handleClick}
className={`
w-full flex items-center gap-1.5 px-2 py-1 text-left text-sm
hover:bg-hover transition-colors rounded
${isSelected ? 'bg-accent/20 text-accent' : 'text-text-secondary hover:text-text-primary'}
hover:bg-hover transition-colors rounded relative
${isSelected ? 'bg-amber-500/15 text-amber-300 border-l-2 border-amber-400' : 'text-text-secondary hover:text-text-primary border-l-2 border-transparent'}
${matchesSearch ? 'bg-accent/10' : ''}
`}
style={{ paddingLeft: `${depth * 12 + 8}px` }}
Expand Down Expand Up @@ -216,6 +216,31 @@ export const FileTreePanel = ({ onFocusNode }: FileTreePanelProps) => {
}
}, [fileTree.length]); // Only run when tree first loads

// Auto-expand to selected file when selectedNode changes (e.g., from graph click)
useEffect(() => {
const path = selectedNode?.properties?.filePath;
if (!path) return;

// Expand all parent folders leading to this file
const parts = path.split('/').filter(Boolean);
const pathsToExpand: string[] = [];
let currentPath = '';

// Build all parent paths (exclude the last part if it's a file)
for (let i = 0; i < parts.length - 1; i++) {
currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
pathsToExpand.push(currentPath);
}

if (pathsToExpand.length > 0) {
setExpandedPaths(prev => {
const next = new Set(prev);
pathsToExpand.forEach(p => next.add(p));
return next;
});
}
}, [selectedNode?.id]); // Trigger when selected node changes

const toggleExpanded = useCallback((path: string) => {
setExpandedPaths(prev => {
const next = new Set(prev);
Expand Down
43 changes: 39 additions & 4 deletions src/components/GraphCanvas.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useCallback, useState, forwardRef, useImperativeHandle } from 'react';
import { ZoomIn, ZoomOut, Maximize2, Focus, RotateCcw, Play, Pause } from 'lucide-react';
import { useEffect, useCallback, useMemo, useState, forwardRef, useImperativeHandle } from 'react';
import { ZoomIn, ZoomOut, Maximize2, Focus, RotateCcw, Play, Pause, Lightbulb, LightbulbOff } from 'lucide-react';
import { useSigma } from '../hooks/useSigma';
import { useAppState } from '../hooks/useAppState';
import { knowledgeGraphToGraphology, filterGraphByDepth, SigmaNodeAttributes, SigmaEdgeAttributes } from '../lib/graph-adapter';
Expand All @@ -11,8 +11,28 @@ export interface GraphCanvasHandle {
}

export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
const { graph, setSelectedNode, selectedNode: appSelectedNode, visibleLabels, openCodePanel, depthFilter, highlightedNodeIds } = useAppState();
const {
graph,
setSelectedNode,
selectedNode: appSelectedNode,
visibleLabels,
openCodePanel,
depthFilter,
highlightedNodeIds,
aiCitationHighlightedNodeIds,
aiToolHighlightedNodeIds,
isAIHighlightsEnabled,
toggleAIHighlights,
} = useAppState();
const [hoveredNodeName, setHoveredNodeName] = useState<string | null>(null);

const effectiveHighlightedNodeIds = useMemo(() => {
if (!isAIHighlightsEnabled) return highlightedNodeIds;
const next = new Set(highlightedNodeIds);
for (const id of aiCitationHighlightedNodeIds) next.add(id);
for (const id of aiToolHighlightedNodeIds) next.add(id);
return next;
}, [highlightedNodeIds, aiCitationHighlightedNodeIds, aiToolHighlightedNodeIds, isAIHighlightsEnabled]);

const handleNodeClick = useCallback((nodeId: string) => {
if (!graph) return;
Expand Down Expand Up @@ -55,7 +75,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {
onNodeClick: handleNodeClick,
onNodeHover: handleNodeHover,
onStageClick: handleStageClick,
highlightedNodeIds,
highlightedNodeIds: effectiveHighlightedNodeIds,
});

// Expose focusNode to parent via ref
Expand Down Expand Up @@ -244,6 +264,21 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle>((_, ref) => {

{/* Query FAB */}
<QueryFAB />

{/* AI Highlights toggle - Top Right */}
<div className="absolute top-4 right-4 z-20">
<button
onClick={toggleAIHighlights}
className={
isAIHighlightsEnabled
? 'w-10 h-10 flex items-center justify-center bg-cyan-500/15 border border-cyan-400/40 rounded-lg text-cyan-200 hover:bg-cyan-500/20 hover:border-cyan-300/60 transition-colors'
: 'w-10 h-10 flex items-center justify-center bg-elevated border border-border-subtle rounded-lg text-text-muted hover:bg-hover hover:text-text-primary transition-colors'
}
title={isAIHighlightsEnabled ? 'Turn off AI highlights' : 'Turn on AI highlights'}
>
{isAIHighlightsEnabled ? <Lightbulb className="w-4 h-4" /> : <LightbulbOff className="w-4 h-4" />}
</button>
</div>
</div>
);
});
Expand Down
139 changes: 139 additions & 0 deletions src/components/MermaidDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { useEffect, useRef, useState } from 'react';
import mermaid from 'mermaid';
import { AlertTriangle, Maximize2, Minimize2 } from 'lucide-react';

// Initialize mermaid with dark theme
mermaid.initialize({
startOnLoad: false,
theme: 'dark',
themeVariables: {
primaryColor: '#06b6d4',
primaryTextColor: '#e4e4ed',
primaryBorderColor: '#1e1e2a',
lineColor: '#3b3b54',
secondaryColor: '#1e1e2a',
tertiaryColor: '#0a0a10',
background: '#0a0a10',
mainBkg: '#0f0f18',
nodeBorder: '#3b3b54',
clusterBkg: '#1e1e2a',
titleColor: '#e4e4ed',
edgeLabelBackground: '#0f0f18',
nodeTextColor: '#e4e4ed',
},
flowchart: {
curve: 'basis',
padding: 15,
nodeSpacing: 50,
rankSpacing: 50,
},
sequence: {
actorMargin: 50,
boxMargin: 10,
boxTextMargin: 5,
noteMargin: 10,
messageMargin: 35,
},
fontFamily: '"JetBrains Mono", "Fira Code", monospace',
fontSize: 13,
});

interface MermaidDiagramProps {
code: string;
}

export const MermaidDiagram = ({ code }: MermaidDiagramProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const [error, setError] = useState<string | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
const [svg, setSvg] = useState<string>('');

useEffect(() => {
const renderDiagram = async () => {
if (!containerRef.current) return;

try {
// Generate unique ID for this diagram
const id = `mermaid-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

// Render the diagram
const { svg: renderedSvg } = await mermaid.render(id, code.trim());
setSvg(renderedSvg);
setError(null);
} catch (err) {
console.error('Mermaid render error:', err);
setError(err instanceof Error ? err.message : 'Failed to render diagram');
setSvg('');
}
};

renderDiagram();
}, [code]);

if (error) {
return (
<div className="my-3 p-4 bg-rose-500/10 border border-rose-500/30 rounded-lg">
<div className="flex items-center gap-2 text-rose-300 text-sm mb-2">
<AlertTriangle className="w-4 h-4" />
<span className="font-medium">Diagram Error</span>
</div>
<pre className="text-xs text-rose-200/70 font-mono whitespace-pre-wrap">{error}</pre>
<details className="mt-2">
<summary className="text-xs text-text-muted cursor-pointer hover:text-text-secondary">
Show source
</summary>
<pre className="mt-2 p-2 bg-surface rounded text-xs text-text-muted overflow-x-auto">
{code}
</pre>
</details>
</div>
);
}

return (
<div className={`my-3 relative group ${isExpanded ? 'fixed inset-4 z-50' : ''}`}>
{/* Backdrop for expanded view */}
{isExpanded && (
<div
className="absolute inset-0 -m-4 bg-deep/95 backdrop-blur-sm"
onClick={() => setIsExpanded(false)}
/>
)}

<div className={`
relative bg-gradient-to-b from-surface to-elevated
border border-border-subtle rounded-xl overflow-hidden
${isExpanded ? 'h-full' : ''}
`}>
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 bg-surface/60 border-b border-border-subtle">
<span className="text-[10px] text-text-muted uppercase tracking-wider font-medium">
Diagram
</span>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="p-1 text-text-muted hover:text-text-primary hover:bg-hover rounded transition-colors"
title={isExpanded ? 'Minimize' : 'Expand'}
>
{isExpanded ? (
<Minimize2 className="w-3.5 h-3.5" />
) : (
<Maximize2 className="w-3.5 h-3.5" />
)}
</button>
</div>

{/* Diagram container */}
<div
ref={containerRef}
className={`
flex items-center justify-center p-4 overflow-auto
${isExpanded ? 'h-[calc(100%-40px)]' : 'max-h-[400px]'}
`}
dangerouslySetInnerHTML={{ __html: svg }}
/>
</div>
</div>
);
};

Loading