Skip to content

Commit e044268

Browse files
aksOpsclaude
andcommitted
feat: single-page UI with treemap, stats, MCP console + backend fixes
UI consolidation: - Merge Dashboard, Codebase Map, MCP Console, and Explorer into single page - Stats cards (compact top row) + treemap (80%) + MCP tools sidebar (20%) - MCP tool form opens in Modal on click, response in Modal - Inline search field filters tool list (no search modal) - Tool descriptions shown inline with proper multiline wrapping - File viewer via double-click on leaf files (fixes breadcrumb back opening viewer) - Remove Explorer tab, Codebase Map tab, MCP Console tab - Breadcrumb: dark pill style, visible in both light/dark themes Backend fixes: - File tree: upgrade FILE→DIRECTORY when path used as intermediate (monorepo fix) - File tree: exclude test paths by default (excludeTests=true param) - Collapse single-child directory chains in treemap (Java package paths) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8da0f91 commit e044268

9 files changed

Lines changed: 532 additions & 323 deletions

File tree

src/main/frontend/src/App.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
import { Routes, Route, Navigate } from 'react-router-dom';
22
import AppLayout from './components/AppLayout';
33
import Dashboard from './pages/Dashboard';
4-
import CodebaseMap from './pages/CodebaseMap';
5-
import Explorer from './pages/Explorer';
6-
import McpConsole from './pages/McpConsole';
74

85
export default function App() {
96
return (
107
<Routes>
118
<Route element={<AppLayout />}>
129
<Route path="/" element={<Dashboard />} />
13-
<Route path="/map" element={<CodebaseMap />} />
14-
<Route path="/explorer" element={<Explorer />} />
15-
<Route path="/explorer/:kind" element={<Explorer />} />
16-
<Route path="/console" element={<McpConsole />} />
1710
<Route path="*" element={<Navigate to="/" replace />} />
1811
</Route>
1912
</Routes>

src/main/frontend/src/components/AppLayout.tsx

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,21 @@
1-
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
2-
import { Layout, Menu, Switch, Typography, Space } from 'antd';
3-
import {
4-
DashboardOutlined,
5-
AppstoreOutlined,
6-
SearchOutlined,
7-
CodeOutlined,
8-
SunOutlined,
9-
MoonOutlined,
10-
} from '@ant-design/icons';
1+
import { Outlet } from 'react-router-dom';
2+
import { Layout, Switch, Typography, Space } from 'antd';
3+
import { SunOutlined, MoonOutlined } from '@ant-design/icons';
114
import { useTheme } from '@/context/ThemeContext';
125

136
const { Header, Content } = Layout;
147

15-
const menuItems = [
16-
{ key: '/', icon: <DashboardOutlined />, label: 'Dashboard' },
17-
{ key: '/map', icon: <AppstoreOutlined />, label: 'Codebase Map' },
18-
{ key: '/explorer', icon: <SearchOutlined />, label: 'Explorer' },
19-
{ key: '/console', icon: <CodeOutlined />, label: 'MCP Console' },
20-
];
21-
228
export default function AppLayout() {
23-
const navigate = useNavigate();
24-
const location = useLocation();
259
const { isDark, toggle } = useTheme();
2610

27-
const selectedKey = menuItems.find(
28-
item => item.key !== '/' && location.pathname.startsWith(item.key)
29-
)?.key ?? '/';
30-
3111
return (
3212
<Layout style={{ minHeight: '100vh' }}>
3313
<Header
3414
style={{
3515
padding: '0 24px',
3616
display: 'flex',
3717
alignItems: 'center',
38-
gap: 0,
18+
justifyContent: 'space-between',
3919
position: 'sticky',
4020
top: 0,
4121
zIndex: 100,
@@ -44,17 +24,10 @@ export default function AppLayout() {
4424
>
4525
<Typography.Title
4626
level={4}
47-
style={{ color: '#2563eb', margin: '0 24px 0 0', whiteSpace: 'nowrap', lineHeight: '64px' }}
27+
style={{ color: '#2563eb', margin: 0, whiteSpace: 'nowrap', lineHeight: '64px' }}
4828
>
4929
Code IQ
5030
</Typography.Title>
51-
<Menu
52-
mode="horizontal"
53-
selectedKeys={[selectedKey]}
54-
items={menuItems}
55-
onClick={({ key }) => navigate(key)}
56-
style={{ flex: 1, border: 'none', background: 'transparent' }}
57-
/>
5831
<Space>
5932
<Switch
6033
checked={isDark}

src/main/frontend/src/index.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,12 @@ body {
33
padding: 0;
44
}
55
/* Ant Design handles all theming */
6+
7+
/* MCP tool list: allow multiline descriptions */
8+
.mcp-tool-menu .ant-menu-item {
9+
height: auto !important;
10+
line-height: normal !important;
11+
padding-top: 4px !important;
12+
padding-bottom: 4px !important;
13+
white-space: normal !important;
14+
}

src/main/frontend/src/pages/CodebaseMap.tsx

Lines changed: 91 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState, useMemo } from 'react';
2-
import { Select, Typography, Space, Spin, Alert } from 'antd';
1+
import { useState, useMemo, useCallback } from 'react';
2+
import { Typography, Spin, Alert, Drawer } from 'antd';
33
import ReactECharts from 'echarts-for-react';
44
import { useApi } from '@/hooks/useApi';
55
import { api } from '@/lib/api';
@@ -51,16 +51,10 @@ function dominantLang(nodes: FileTreeNode[]): string {
5151
return Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? 'other';
5252
}
5353

54-
/**
55-
* Collapse single-child directory chains into one node.
56-
* e.g., src → main → java → io → github becomes "src/main/java/io/github"
57-
* This avoids 10+ clicks through single-child directories (common in Java packages).
58-
*/
5954
function collapseTree(nodes: FileTreeNode[]): FileTreeNode[] {
6055
return nodes.map(n => {
6156
if (n.type !== 'directory' || !n.children || n.children.length === 0) return n;
6257

63-
// Collapse: if this directory has exactly 1 child that is also a directory, merge names
6458
let current = n;
6559
let collapsedName = n.name;
6660
while (
@@ -89,14 +83,12 @@ function toEChartsNodes(nodes: FileTreeNode[]): EChartsTreeNode[] {
8983
const children = toEChartsNodes(n.children);
9084
if (children.length === 0) continue;
9185
const lang = dominantLang(n.children);
92-
// Directory nodes: NO value — ECharts sums from children for correct proportions
9386
result.push({
9487
name: n.name,
9588
children,
9689
itemStyle: { color: LANG_COLORS[lang] ?? '#666' },
9790
});
9891
} else {
99-
// Leaf file node: value = nodeCount (determines rectangle size)
10092
const lang = inferLang(n.name);
10193
result.push({
10294
name: n.name,
@@ -112,34 +104,43 @@ function fileTreeToECharts(nodes: FileTreeNode[]): EChartsTreeNode[] {
112104
return toEChartsNodes(collapseTree(nodes));
113105
}
114106

115-
function collectLanguages(nodes: FileTreeNode[]): string[] {
116-
const langs = new Set<string>();
117-
function walk(items: FileTreeNode[]) {
118-
for (const item of items) {
119-
if (item.type === 'file') {
120-
const lang = inferLang(item.name);
121-
if (lang !== 'other') langs.add(lang);
122-
}
123-
if (item.children) walk(item.children);
124-
}
125-
}
126-
walk(nodes);
127-
return Array.from(langs).sort();
128-
}
129-
130107
export default function CodebaseMap() {
131108
const { isDark } = useTheme();
132-
const [langFilter, setLangFilter] = useState<string | undefined>(undefined);
109+
const [fileDrawer, setFileDrawer] = useState<{ path: string; content: string } | null>(null);
110+
const [fileLoading, setFileLoading] = useState(false);
133111

134112
const { data: treeData, loading, error } = useApi<FileTreeResponse>(
135113
() => api.getFileTree(), []
136114
);
137115

138116
const tree = treeData?.tree ?? [];
139117
const totalFiles = treeData?.total_files ?? 0;
140-
const uniqueLangs = useMemo(() => collectLanguages(tree), [tree]);
141118
const treemapData = useMemo(() => fileTreeToECharts(tree), [tree]);
142119

120+
// On click: if leaf node (no children), open file in drawer
121+
const onClickNode = useCallback(async (params: {
122+
data?: { children?: unknown[] };
123+
treePathInfo?: Array<{ name: string }>;
124+
}) => {
125+
if (params.data?.children && (params.data.children as unknown[]).length > 0) return;
126+
const pathParts = params.treePathInfo?.map(p => p.name).filter(Boolean) ?? [];
127+
if (pathParts.length === 0) return;
128+
const filePath = pathParts.join('/');
129+
setFileLoading(true);
130+
try {
131+
const content = await api.readFile(filePath);
132+
setFileDrawer({ path: filePath, content });
133+
} catch {
134+
setFileDrawer({ path: filePath, content: '// Could not load file' });
135+
} finally {
136+
setFileLoading(false);
137+
}
138+
}, []);
139+
140+
const onEvents = useMemo(() => ({
141+
click: onClickNode,
142+
}), [onClickNode]);
143+
143144
const chartOption = useMemo(() => ({
144145
tooltip: {
145146
formatter: (info: { name: string; value: number; treePathInfo?: Array<{ name: string }> }) => {
@@ -150,21 +151,32 @@ export default function CodebaseMap() {
150151
series: [{
151152
type: 'treemap',
152153
data: treemapData,
154+
top: 0,
155+
left: 0,
156+
right: 0,
157+
bottom: 0,
158+
width: '100%',
159+
height: '100%',
153160
leafDepth: 2,
154161
drillDownIcon: '▶ ',
155162
roam: false,
156163
nodeClick: 'zoomToNode',
157164
breadcrumb: {
158165
show: true,
159-
top: 4,
160-
left: 4,
166+
bottom: 8,
167+
left: 'center',
168+
height: 28,
161169
itemStyle: {
162-
color: isDark ? '#1a1a1a' : '#f5f5f5',
163-
borderColor: isDark ? '#303030' : '#d9d9d9',
170+
color: isDark ? '#1f1f1f' : '#fff',
171+
borderColor: isDark ? '#444' : '#bbb',
172+
borderWidth: 1,
173+
shadowBlur: 3,
174+
shadowColor: isDark ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0.15)',
164175
},
165176
textStyle: {
166177
color: isDark ? '#e0e0e0' : '#333',
167-
fontSize: 13,
178+
fontSize: 14,
179+
fontWeight: 'bold' as const,
168180
},
169181
},
170182
levels: [
@@ -222,44 +234,61 @@ export default function CodebaseMap() {
222234
}
223235

224236
return (
225-
<div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 96px)', margin: '-16px -24px', padding: '8px 16px 0' }}>
226-
<div style={{
227-
display: 'flex',
228-
justifyContent: 'space-between',
229-
alignItems: 'center',
230-
marginBottom: 4,
231-
flexShrink: 0,
232-
}}>
233-
<Space>
234-
<Typography.Title level={4} style={{ margin: 0 }}>Codebase Map</Typography.Title>
235-
<Typography.Text type="secondary">
236-
{totalFiles.toLocaleString()} files · {uniqueLangs.length} languages
237-
</Typography.Text>
238-
</Space>
239-
<Select
240-
allowClear
241-
placeholder="Filter by language"
242-
style={{ width: 180 }}
243-
value={langFilter}
244-
onChange={setLangFilter}
245-
options={uniqueLangs.map(l => ({ label: l.charAt(0).toUpperCase() + l.slice(1), value: l }))}
246-
/>
247-
</div>
248-
249-
<div style={{ flex: 1, minHeight: 0 }}>
250-
{treemapData.length > 0 ? (
237+
<div style={{ position: 'relative', height: 'calc(100vh - 64px)', margin: '-16px -24px' }}>
238+
{treemapData.length > 0 ? (
239+
<>
240+
<div style={{
241+
position: 'absolute',
242+
top: 6,
243+
right: 10,
244+
zIndex: 10,
245+
background: isDark ? 'rgba(10,10,10,0.8)' : 'rgba(255,255,255,0.85)',
246+
borderRadius: 4,
247+
padding: '2px 10px',
248+
fontSize: 12,
249+
color: isDark ? '#888' : '#999',
250+
}}>
251+
{totalFiles.toLocaleString()} files
252+
</div>
251253
<ReactECharts
252254
option={chartOption}
253255
style={{ height: '100%', width: '100%' }}
254256
theme={isDark ? 'dark' : undefined}
255257
opts={{ renderer: 'canvas' }}
258+
onEvents={onEvents}
256259
/>
260+
</>
261+
) : (
262+
<div style={{ textAlign: 'center', padding: 60 }}>
263+
<Typography.Text type="secondary">No file data available. Run index + enrich first.</Typography.Text>
264+
</div>
265+
)}
266+
267+
<Drawer
268+
title={fileDrawer?.path}
269+
placement="right"
270+
width="60%"
271+
open={!!fileDrawer}
272+
onClose={() => setFileDrawer(null)}
273+
styles={{ body: { padding: 0 } }}
274+
>
275+
{fileLoading ? (
276+
<div style={{ textAlign: 'center', padding: 40 }}><Spin /></div>
257277
) : (
258-
<div style={{ textAlign: 'center', padding: 60 }}>
259-
<Typography.Text type="secondary">No file data available. Run index + enrich first.</Typography.Text>
260-
</div>
278+
<pre style={{
279+
margin: 0,
280+
padding: 16,
281+
fontSize: 13,
282+
lineHeight: 1.5,
283+
overflow: 'auto',
284+
height: '100%',
285+
background: isDark ? '#0a0a0a' : '#fafafa',
286+
color: isDark ? '#d4d4d4' : '#1f1f1f',
287+
}}>
288+
{fileDrawer?.content}
289+
</pre>
261290
)}
262-
</div>
291+
</Drawer>
263292
</div>
264293
);
265294
}

0 commit comments

Comments
 (0)