Fix blank portal: add root element heights, use dagre for graph layout
- Set explicit height on html/body/#root so React Flow container renders - Replace @dagrejs/dagre (broken CJS dynamic require) with dagre v0.8.5 - Hierarchical top-down layout with dagre, arrow markers on edges - Move React Flow controls to top-right to avoid floating button overlap
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" style="height:100%">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Cortex Portal</title>
|
<title>Cortex Portal</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-950 text-gray-100">
|
<body class="bg-gray-950 text-gray-100" style="margin:0;height:100%;overflow:hidden">
|
||||||
<div id="root"></div>
|
<div id="root" style="height:100%"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
33
portal/package-lock.json
generated
33
portal/package-lock.json
generated
@@ -9,7 +9,9 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.62.0",
|
"@tanstack/react-query": "^5.62.0",
|
||||||
|
"@types/dagre": "^0.7.53",
|
||||||
"@xyflow/react": "^12.4.0",
|
"@xyflow/react": "^12.4.0",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
@@ -1546,6 +1548,12 @@
|
|||||||
"@types/d3-selection": "*"
|
"@types/d3-selection": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/dagre": {
|
||||||
|
"version": "0.7.53",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.53.tgz",
|
||||||
|
"integrity": "sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -1816,6 +1824,16 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dagre": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"graphlib": "^2.1.8",
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -1967,6 +1985,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/graphlib": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||||
@@ -2271,6 +2298,12 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||||
|
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.62.0",
|
"@tanstack/react-query": "^5.62.0",
|
||||||
|
"@types/dagre": "^0.7.53",
|
||||||
"@xyflow/react": "^12.4.0",
|
"@xyflow/react": "^12.4.0",
|
||||||
|
"dagre": "^0.8.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
@@ -18,9 +20,9 @@
|
|||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"typescript": "^5.9.0",
|
"typescript": "^5.9.0",
|
||||||
"vite": "^6.0.0",
|
"vite": "^6.0.0"
|
||||||
"@vitejs/plugin-react": "^4.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="h-screen w-screen overflow-hidden relative">
|
<div className="h-screen w-screen overflow-hidden relative">
|
||||||
{/* Full-screen graph */}
|
{/* Full-screen graph */}
|
||||||
<GraphView selectedId={selectedId} onSelect={selectNode} />
|
<div className="absolute inset-0">
|
||||||
|
<GraphView selectedId={selectedId} onSelect={selectNode} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Floating action buttons — bottom-left */}
|
{/* Floating action buttons — bottom-left */}
|
||||||
<div className="fixed bottom-5 left-5 flex flex-col gap-2 z-30">
|
<div className="fixed bottom-5 left-5 flex flex-col gap-2 z-30">
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import {
|
|||||||
useEdgesState,
|
useEdgesState,
|
||||||
type Node as FlowNode,
|
type Node as FlowNode,
|
||||||
type Edge as FlowEdge,
|
type Edge as FlowEdge,
|
||||||
|
Position,
|
||||||
|
MarkerType,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
import Dagre from 'dagre';
|
||||||
import { useGraph } from '../hooks/useGraph';
|
import { useGraph } from '../hooks/useGraph';
|
||||||
import type { CortexNode } from '../types';
|
import type { CortexNode, CortexEdge } from '../types';
|
||||||
|
|
||||||
const KIND_COLORS: Record<string, string> = {
|
const KIND_COLORS: Record<string, string> = {
|
||||||
memory: '#6366f1',
|
memory: '#6366f1',
|
||||||
@@ -19,23 +22,43 @@ const KIND_COLORS: Record<string, string> = {
|
|||||||
decision: '#ef4444',
|
decision: '#ef4444',
|
||||||
};
|
};
|
||||||
|
|
||||||
function layoutNodes(nodes: CortexNode[]): FlowNode[] {
|
const NODE_WIDTH = 180;
|
||||||
const cols = Math.max(Math.ceil(Math.sqrt(nodes.length)), 1);
|
const NODE_HEIGHT = 60;
|
||||||
return nodes.map((n, i) => ({
|
|
||||||
id: n.id,
|
function layoutGraph(nodes: CortexNode[], edges: CortexEdge[]): FlowNode[] {
|
||||||
position: { x: (i % cols) * 280 + 50, y: Math.floor(i / cols) * 140 + 50 },
|
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||||
data: { label: n.title, kind: n.kind, status: n.status },
|
g.setGraph({ rankdir: 'TB', nodesep: 60, ranksep: 100, marginx: 40, marginy: 40 });
|
||||||
style: {
|
|
||||||
background: KIND_COLORS[n.kind] || '#6b7280',
|
for (const n of nodes) {
|
||||||
color: '#fff',
|
g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
||||||
border: 'none',
|
}
|
||||||
borderRadius: 8,
|
for (const e of edges) {
|
||||||
padding: '12px 16px',
|
g.setEdge(e.fromId, e.toId);
|
||||||
fontSize: 13,
|
}
|
||||||
fontWeight: 500,
|
|
||||||
minWidth: 160,
|
Dagre.layout(g);
|
||||||
},
|
|
||||||
}));
|
return nodes.map((n) => {
|
||||||
|
const pos = g.node(n.id);
|
||||||
|
return {
|
||||||
|
id: n.id,
|
||||||
|
position: { x: pos.x - NODE_WIDTH / 2, y: pos.y - NODE_HEIGHT / 2 },
|
||||||
|
sourcePosition: Position.Bottom,
|
||||||
|
targetPosition: Position.Top,
|
||||||
|
data: { label: n.title, kind: n.kind, status: n.status },
|
||||||
|
style: {
|
||||||
|
background: KIND_COLORS[n.kind] || '#6b7280',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: '10px 16px',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
width: NODE_WIDTH,
|
||||||
|
textAlign: 'center' as const,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GraphView({
|
export default function GraphView({
|
||||||
@@ -47,7 +70,11 @@ export default function GraphView({
|
|||||||
}) {
|
}) {
|
||||||
const { data, isLoading } = useGraph();
|
const { data, isLoading } = useGraph();
|
||||||
|
|
||||||
const initialNodes = useMemo(() => (data ? layoutNodes(data.nodes) : []), [data]);
|
const initialNodes = useMemo(
|
||||||
|
() => (data ? layoutGraph(data.nodes, data.edges) : []),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
const initialEdges = useMemo<FlowEdge[]>(
|
const initialEdges = useMemo<FlowEdge[]>(
|
||||||
() =>
|
() =>
|
||||||
data
|
data
|
||||||
@@ -57,8 +84,12 @@ export default function GraphView({
|
|||||||
target: e.toId,
|
target: e.toId,
|
||||||
label: e.type,
|
label: e.type,
|
||||||
animated: e.type === 'blocked_by',
|
animated: e.type === 'blocked_by',
|
||||||
style: { stroke: '#64748b' },
|
style: { stroke: '#64748b', strokeWidth: 1.5 },
|
||||||
labelStyle: { fill: '#94a3b8', fontSize: 11 },
|
labelStyle: { fill: '#94a3b8', fontSize: 10 },
|
||||||
|
labelBgStyle: { fill: '#1e293b', fillOpacity: 0.9 },
|
||||||
|
labelBgPadding: [6, 3] as [number, number],
|
||||||
|
labelBgBorderRadius: 4,
|
||||||
|
markerEnd: { type: MarkerType.ArrowClosed, color: '#64748b', width: 16, height: 16 },
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
[data],
|
[data],
|
||||||
@@ -67,7 +98,6 @@ export default function GraphView({
|
|||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
|
||||||
// Sync when data changes
|
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
setNodes(initialNodes);
|
setNodes(initialNodes);
|
||||||
setEdges(initialEdges);
|
setEdges(initialEdges);
|
||||||
@@ -94,11 +124,16 @@ export default function GraphView({
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
fitView
|
fitView
|
||||||
|
fitViewOptions={{ padding: 0.3 }}
|
||||||
className="bg-gray-950"
|
className="bg-gray-950"
|
||||||
>
|
>
|
||||||
<Background color="#334155" gap={20} />
|
<Background color="#334155" gap={20} />
|
||||||
<Controls className="!bg-gray-800 !border-gray-700 [&>button]:!bg-gray-800 [&>button]:!border-gray-700 [&>button]:!text-gray-300" />
|
<Controls
|
||||||
|
position="top-right"
|
||||||
|
className="!bg-gray-800 !border-gray-700 [&>button]:!bg-gray-800 [&>button]:!border-gray-700 [&>button]:!text-gray-300"
|
||||||
|
/>
|
||||||
<MiniMap
|
<MiniMap
|
||||||
|
position="bottom-right"
|
||||||
nodeColor={(n) => KIND_COLORS[(n.data as any)?.kind] || '#6b7280'}
|
nodeColor={(n) => KIND_COLORS[(n.data as any)?.kind] || '#6b7280'}
|
||||||
className="!bg-gray-900 !border-gray-700"
|
className="!bg-gray-900 !border-gray-700"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user