diff --git a/portal/index.html b/portal/index.html
index 4e8b875..3d92ea6 100644
--- a/portal/index.html
+++ b/portal/index.html
@@ -1,12 +1,12 @@
-
+
{/* Full-screen graph */}
-
+
+
+
{/* Floating action buttons — bottom-left */}
diff --git a/portal/src/components/GraphView.tsx b/portal/src/components/GraphView.tsx
index d294b81..1a22bed 100644
--- a/portal/src/components/GraphView.tsx
+++ b/portal/src/components/GraphView.tsx
@@ -8,9 +8,12 @@ import {
useEdgesState,
type Node as FlowNode,
type Edge as FlowEdge,
+ Position,
+ MarkerType,
} from '@xyflow/react';
+import Dagre from 'dagre';
import { useGraph } from '../hooks/useGraph';
-import type { CortexNode } from '../types';
+import type { CortexNode, CortexEdge } from '../types';
const KIND_COLORS: Record = {
memory: '#6366f1',
@@ -19,23 +22,43 @@ const KIND_COLORS: Record = {
decision: '#ef4444',
};
-function layoutNodes(nodes: CortexNode[]): FlowNode[] {
- const cols = Math.max(Math.ceil(Math.sqrt(nodes.length)), 1);
- return nodes.map((n, i) => ({
- id: n.id,
- position: { x: (i % cols) * 280 + 50, y: Math.floor(i / cols) * 140 + 50 },
- data: { label: n.title, kind: n.kind, status: n.status },
- style: {
- background: KIND_COLORS[n.kind] || '#6b7280',
- color: '#fff',
- border: 'none',
- borderRadius: 8,
- padding: '12px 16px',
- fontSize: 13,
- fontWeight: 500,
- minWidth: 160,
- },
- }));
+const NODE_WIDTH = 180;
+const NODE_HEIGHT = 60;
+
+function layoutGraph(nodes: CortexNode[], edges: CortexEdge[]): FlowNode[] {
+ const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
+ g.setGraph({ rankdir: 'TB', nodesep: 60, ranksep: 100, marginx: 40, marginy: 40 });
+
+ for (const n of nodes) {
+ g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
+ }
+ for (const e of edges) {
+ g.setEdge(e.fromId, e.toId);
+ }
+
+ 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({
@@ -47,7 +70,11 @@ export default function GraphView({
}) {
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(
() =>
data
@@ -57,8 +84,12 @@ export default function GraphView({
target: e.toId,
label: e.type,
animated: e.type === 'blocked_by',
- style: { stroke: '#64748b' },
- labelStyle: { fill: '#94a3b8', fontSize: 11 },
+ style: { stroke: '#64748b', strokeWidth: 1.5 },
+ 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],
@@ -67,7 +98,6 @@ export default function GraphView({
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
- // Sync when data changes
useMemo(() => {
setNodes(initialNodes);
setEdges(initialEdges);
@@ -94,11 +124,16 @@ export default function GraphView({
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
fitView
+ fitViewOptions={{ padding: 0.3 }}
className="bg-gray-950"
>
-
+
KIND_COLORS[(n.data as any)?.kind] || '#6b7280'}
className="!bg-gray-900 !border-gray-700"
/>