Skip to content

Commit e01de3e

Browse files
authored
Merge pull request #945 from jupyter-naas/refactor-sidebar
Refactor sidebar
2 parents fe0dbd7 + 3e4cf9b commit e01de3e

19 files changed

Lines changed: 397 additions & 130 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,4 @@ website/build
136136
.pnpm-store/
137137
node_modules/
138138

139+
CLAUDE.md

libs/naas-abi-marketplace/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/naas-abi/naas_abi/apps/nexus/apps/web/next.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1+
const path = require('path');
2+
13
/** @type {import('next').NextConfig} */
24
const nextConfig = {
35
reactStrictMode: true,
46
transpilePackages: ['@nexus/ui', '@embedpdf/snippet'],
7+
webpack(config) {
8+
// pnpm's strict package isolation stops webpack from resolving this ESM-only
9+
// package through normal module lookup; point it directly to the bundle file.
10+
config.resolve.alias['@embedpdf/snippet'] = path.resolve(
11+
__dirname,
12+
'node_modules/@embedpdf/snippet/dist/embedpdf.js'
13+
);
14+
return config;
15+
},
516
async rewrites() {
617
return [
718
{

libs/naas-abi/naas_abi/apps/nexus/apps/web/src/app/workspace/[workspaceId]/graph/page.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -539,8 +539,8 @@ export default function GraphPage() {
539539
edges: GraphEdge[];
540540
}>>([]);
541541

542-
// Relations filter — on by default
543-
const [showRelations, setShowRelations] = useState(true);
542+
// Relations filter — off by default
543+
const [showRelations, setShowRelations] = useState(false);
544544

545545
// Increment to trigger physics re-layout in VisNetwork
546546
const [stabilizeKey, setStabilizeKey] = useState(0);
@@ -2261,7 +2261,9 @@ export default function GraphPage() {
22612261
<div className="flex h-full items-center justify-center">
22622262
<div className="flex items-center gap-2 text-muted-foreground">
22632263
<Loader2 size={20} className="animate-spin" />
2264-
<span>Loading NEXUS Knowledge Graph...</span>
2264+
<span>
2265+
Loading {graphOptions.find((g) => g.id === selectedGraphId)?.name ?? 'Knowledge Graph'}
2266+
</span>
22652267
</div>
22662268
</div>
22672269
) : error ? (
@@ -2322,6 +2324,7 @@ export default function GraphPage() {
23222324
onNodeSelect={setSelectedNodeId}
23232325
onEdgeSelect={setSelectedEdgeId}
23242326
stabilizeKey={stabilizeKey}
2327+
layoutDirection={showRelations ? undefined : 'LR'}
23252328
/>
23262329
{filteredNodes.length > 0 && (
23272330
<div className="absolute bottom-4 left-4 z-10 flex flex-col gap-1.5 rounded-lg border bg-card/95 px-3 py-2 shadow-lg backdrop-blur-sm w-52">

libs/naas-abi/naas_abi/apps/nexus/apps/web/src/components/graph/vis-network.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,79 @@ const LR_NODE_SLOT = 110; // px per leaf slot in cross-axis (y)
132132
const TD_LEVEL_GAP = 180; // px between depth levels (main axis = y)
133133
const TD_NODE_SLOT = 168; // px per leaf slot in cross-axis (x)
134134

135+
// Bucket grid layout constants (flat / no-edge case)
136+
const GRID_BUCKET_ORDER = [
137+
'Material Entity', 'Process', 'Quality', 'Realizable',
138+
'Temporal Region', 'Site', 'GDC', 'Unknown',
139+
];
140+
const GRID_ZONE_COLS = 3; // bucket zones per row
141+
const GRID_ZONE_W = 1400; // px between bucket zone centres (x)
142+
const GRID_ZONE_H = 900; // px between bucket zone centres (y)
143+
const GRID_NODE_W = 240; // horizontal node spacing within a zone
144+
const GRID_NODE_H = 160; // vertical node spacing within a zone
145+
const GRID_NODES_PER_ROW = 5; // nodes per row inside a zone
146+
147+
/**
148+
* 2-D bucket grid layout used when no is_a edges exist.
149+
* Zones are arranged in a GRID_ZONE_COLS-column grid; nodes within each zone
150+
* are arranged in rows of GRID_NODES_PER_ROW, sorted alphabetically.
151+
*/
152+
function computeBucketGridPositions(
153+
nodes: GraphNode[],
154+
nodesById: Map<string, GraphNode>,
155+
): Map<string, { x: number; y: number }> {
156+
const byBucket = new Map<string, string[]>();
157+
for (const node of nodes) {
158+
const bucket = resolveNodeBucketKey(node, nodesById);
159+
const list = byBucket.get(bucket) ?? [];
160+
list.push(node.id);
161+
byBucket.set(bucket, list);
162+
}
163+
164+
for (const [, ids] of byBucket) {
165+
ids.sort((a, b) =>
166+
(nodesById.get(a)?.label ?? a).localeCompare(nodesById.get(b)?.label ?? b)
167+
);
168+
}
169+
170+
const activeBuckets = [
171+
...GRID_BUCKET_ORDER.filter((b) => byBucket.has(b)),
172+
...Array.from(byBucket.keys()).filter((b) => !GRID_BUCKET_ORDER.includes(b)),
173+
];
174+
175+
const positions = new Map<string, { x: number; y: number }>();
176+
177+
activeBuckets.forEach((bucket, zoneIdx) => {
178+
const ids = byBucket.get(bucket) ?? [];
179+
const zoneCol = zoneIdx % GRID_ZONE_COLS;
180+
const zoneRow = Math.floor(zoneIdx / GRID_ZONE_COLS);
181+
const zoneOriginX = zoneCol * GRID_ZONE_W;
182+
const zoneOriginY = zoneRow * GRID_ZONE_H;
183+
const cols = GRID_NODES_PER_ROW;
184+
const usedW = (Math.min(ids.length, cols) - 1) * GRID_NODE_W;
185+
const usedH = (Math.ceil(ids.length / cols) - 1) * GRID_NODE_H;
186+
187+
ids.forEach((id, i) => {
188+
positions.set(id, {
189+
x: zoneOriginX - usedW / 2 + (i % cols) * GRID_NODE_W,
190+
y: zoneOriginY - usedH / 2 + Math.floor(i / cols) * GRID_NODE_H,
191+
});
192+
});
193+
});
194+
195+
if (positions.size > 0) {
196+
const xs = Array.from(positions.values()).map((p) => p.x);
197+
const ys = Array.from(positions.values()).map((p) => p.y);
198+
const cx = (Math.min(...xs) + Math.max(...xs)) / 2;
199+
const cy = (Math.min(...ys) + Math.max(...ys)) / 2;
200+
for (const [id, pos] of positions) {
201+
positions.set(id, { x: pos.x - cx, y: pos.y - cy });
202+
}
203+
}
204+
205+
return positions;
206+
}
207+
135208
// Resolve a stable bucket key for a node (used to sort / group siblings)
136209
function resolveNodeBucketKey(
137210
node: GraphNode,
@@ -179,6 +252,11 @@ function computeHierarchicalPositions(
179252
const nodeIds = new Set(nodes.map((n) => n.id));
180253
const isaEdges = edges.filter((e) => e.properties?.relation_kind === 'is_a');
181254

255+
// Flat case: no is_a edges → use the 2-D bucket grid instead of a 1-D tree row.
256+
if (isaEdges.length === 0) {
257+
return computeBucketGridPositions(nodes, nodesById);
258+
}
259+
182260
// ── 1. Build tree ──────────────────────────────────────────────────────────
183261
const childrenOf = new Map<string, string[]>();
184262
const parentOf = new Map<string, string>();

libs/naas-abi/naas_abi/apps/nexus/apps/web/src/components/shell/sidebar/apps-section.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useWorkspaceStore } from '@/stores/workspace';
66
import { CollapsibleSection } from './collapsible-section';
77
import { getWorkspacePath } from './utils';
88

9-
export function AppsSection({ collapsed }: { collapsed: boolean }) {
9+
export function AppsSection({ collapsed, detailOnly }: { collapsed: boolean; detailOnly?: boolean }) {
1010
const { currentWorkspaceId } = useWorkspaceStore();
1111

1212
return (
@@ -17,6 +17,7 @@ export function AppsSection({ collapsed }: { collapsed: boolean }) {
1717
description="Extensions and integrations"
1818
href={getWorkspacePath(currentWorkspaceId, '/apps')}
1919
collapsed={collapsed}
20+
detailOnly={detailOnly}
2021
>
2122
<button
2223
className={cn(

libs/naas-abi/naas_abi/apps/nexus/apps/web/src/components/shell/sidebar/chat-section.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ const ProjectGroup = React.memo(function ProjectGroup({
220220
);
221221
});
222222

223-
export function ChatSection({ collapsed }: { collapsed: boolean }) {
223+
export function ChatSection({ collapsed, detailOnly }: { collapsed: boolean; detailOnly?: boolean }) {
224224
const router = useRouter();
225225
const pathname = usePathname();
226226
const [agentsExpanded, setAgentsExpanded] = useState(true);
@@ -291,6 +291,7 @@ export function ChatSection({ collapsed }: { collapsed: boolean }) {
291291
description="Interact with ABI-powered agents"
292292
href={getWorkspacePath(currentWorkspaceId, '/chat')}
293293
collapsed={collapsed}
294+
detailOnly={detailOnly}
294295
onNavigate={() => setActiveConversation(null)}
295296
>
296297
{/* New Chat button */}

libs/naas-abi/naas_abi/apps/nexus/apps/web/src/components/shell/sidebar/collapsible-section.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface SectionProps {
1616
href: string;
1717
collapsed: boolean;
1818
suppressActiveHighlight?: boolean;
19+
detailOnly?: boolean;
1920
children?: React.ReactNode;
2021
onAdd?: () => void;
2122
onNavigate?: () => void;
@@ -29,20 +30,26 @@ export function CollapsibleSection({
2930
href,
3031
collapsed,
3132
suppressActiveHighlight = false,
33+
detailOnly = false,
3234
children,
3335
onAdd,
3436
onNavigate,
3537
}: SectionProps) {
3638
const pathname = usePathname();
3739
const { expandedSections, toggleSection, sidebarCollapsed, toggleSidebar } = useWorkspaceStore();
3840
const isExpanded = expandedSections.includes(id);
39-
const isActive = pathname.startsWith(href);
40-
const hasChildren = Boolean(children);
41-
const showSectionActive = isActive && !suppressActiveHighlight;
4241
const [showTooltip, setShowTooltip] = useState(false);
4342
const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
4443
const sectionRef = useRef<HTMLDivElement>(null);
4544

45+
if (detailOnly) {
46+
return <div className="space-y-0.5">{children}</div>;
47+
}
48+
49+
const isActive = pathname.startsWith(href);
50+
const hasChildren = Boolean(children);
51+
const showSectionActive = isActive && !suppressActiveHighlight;
52+
4653
const handleIconClick = (e: React.MouseEvent) => {
4754
if (collapsed && sidebarCollapsed) {
4855
e.preventDefault();

libs/naas-abi/naas_abi/apps/nexus/apps/web/src/components/shell/sidebar/files-section.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useWorkspaceStore } from '@/stores/workspace';
99
import { CollapsibleSection } from './collapsible-section';
1010
import { getWorkspacePath } from './utils';
1111

12-
export function FilesSection({ collapsed }: { collapsed: boolean }) {
12+
export function FilesSection({ collapsed, detailOnly }: { collapsed: boolean; detailOnly?: boolean }) {
1313
const router = useRouter();
1414
const { currentWorkspaceId } = useWorkspaceStore();
1515
const {
@@ -29,6 +29,7 @@ export function FilesSection({ collapsed }: { collapsed: boolean }) {
2929
description="Workspace file storage"
3030
href={getWorkspacePath(currentWorkspaceId, '/files')}
3131
collapsed={collapsed}
32+
detailOnly={detailOnly}
3233
>
3334
{/* Local section */}
3435
<div className="space-y-0.5">

0 commit comments

Comments
 (0)