Skip to content

Workspace – Node View

Last updated: 2026-03-19


  • Trigger: “Node view” button on the Workspace detail page (/account/workspaces/[workspaceId])
  • Presentation: Full-screen bottom sheet (SheetContent side="bottom" h-dvh)
  • Implementation files:
    • src/app/account/workspaces/[workspaceId]/_components/workspace-node-view.tsx
    • src/app/account/workspaces/[workspaceId]/page.tsx (sheet wiring + header controls)
    • src/app/globals.css (sheet open/close keyframes only; dashed edge animations are inline in the component)

Visual graph representation of a workspace’s resource structure: knowledge bases, agents, channels, users, and apps — and the relationships between them. Provides an at-a-glance map of how workspace resources are connected.


The node view sheet header currently contains one control group:

  1. Connections toggles — a bordered control group with two Switches:
    • KB toggles all dashed KB-connection edges (agent→KB and channel→KB)
    • Agents toggles agent→channel assignment edges Both values are local to the sheet session and are not persisted.

Workspace switching is not on the graph’s workspace node. It appears in two places: (1) the account TopBar immediately after the nav toggle on workspace detail routes (/account/workspaces/[workspaceId]), and (2) the node view sheet header (top-left), which updates the in-sheet workspace via nodeSheetWorkspaceId without leaving the sheet when multiple workspaces exist.



The node graph is rendered with ReactFlow (@xyflow/react). Initial layout is computed by ELK (Eclipse Layout Kernel) using the layered algorithm with RIGHT direction. After the first layout, users can drag nodes freely; dragged positions are preserved across re-layouts and persisted to the server.

Layout algorithm options:

  • elk.algorithm: layered
  • elk.direction: RIGHT
  • Node-to-node spacing between layers: 100px
  • Node-to-node spacing within a layer: 40px
  • Node placement strategy: NETWORK_SIMPLEX
  • Crossing minimization: LAYER_SWEEP
  • Canvas padding: 40px on all sides

Auto-fit: On first open and after a workspace switch, the graph auto-fits to the viewport with padding: 0.15 and a 350ms animation.

ELK exclusion: Per-row handle edges (agent→KB dashed edges and channel-connection→KB dashed edges) are excluded from ELK’s input because ELK has no port declarations for them. ReactFlow renders these edges directly from named handles.


All nodes use the custom graph node type rendered by GraphNodeCard. Node widths and heights are fixed (not resizable).

  • Width: 294px
  • The root node. Shows: workspace avatar/icon, workspace name, and 5 section rows.
  • Each section row is a clickable button that toggles the corresponding collection node open/closed:
    • Knowledge bases (count)
    • Agents (count)
    • Users (count)
    • Channels (count of connections scoped to this workspace)
    • Apps (count)
  • Each row has a source Handle on the right. The handle is only visible when the corresponding collection node is open.
  • Chevron icon (chevron_right / close_small) indicates open/closed state.
  • Selecting the node highlights it and opens the inspector.

KB collection node (kind: "kb-collection")

Section titled “KB collection node (kind: "kb-collection")”
  • Width: 296px
  • Shows: database icon, “Knowledge bases” label, total count.
  • Lists all KBs in the workspace. Each KB row is a clickable button that toggles the KB detail node open/closed.
  • Each KB row has a source Handle (kb-item-source:{kbId}). Handle is visible only when the KB detail node is open.
  • Visible when the Knowledge bases section is toggled on in the workspace node.
  • Width: 240px
  • Shows: KB name (links to /account/knowledge-bases/{kbId}), “Default” badge (if this is the workspace’s default KB), objectives count, documents count.
  • Has a target Handle on the left (kb-detail-target:{kbId}).
  • Visible when the parent KB row is toggled on in the KB collection node.

Agents header node (kind: "agents-header")

Section titled “Agents header node (kind: "agents-header")”
  • Width: 296px
  • Shows: robot_2 icon, “Agents” label, total count.
  • Lists all agents in the workspace. Each agent row shows the agent name and a source Handle on the right (agent-row-source:{agentId}).
  • The agent row handle is visible only when: KB connections toggle is on AND the KB section is open AND the agent’s KB is expanded (visible in visibleKbIds).
  • Visible when the Agents section is toggled on in the workspace node.

Users collection node (kind: "users-collection")

Section titled “Users collection node (kind: "users-collection")”
  • Width: 296px
  • Shows: person icon, “Users” label, total count.
  • Lists up to 16 users, sorted alphabetically. The list is scrollable with a max height.
  • Visible when the Users section is toggled on in the workspace node.

Channels collection node (kind: "channels-collection")

Section titled “Channels collection node (kind: "channels-collection")”
  • Width: 296px
  • Shows: graph_4 icon, “Channels” label, total connection count (scoped to this workspace’s KBs).
  • Lists channel types that have at least one connection. Channel types with zero connections are not shown (not disabled — simply omitted).
  • Each channel type row is a clickable button that toggles the channel detail node open/closed.
  • Each row has a source Handle (channel-type-source:{channelType}). Handle is visible only when the channel detail node is open.
  • Visible when the Channels section is toggled on in the workspace node.

Channel detail node (kind: "channel-detail")

Section titled “Channel detail node (kind: "channel-detail")”
  • Width: 240px
  • Shows: channel type label, total connections count, and a list of individual connections.
  • Each connection row shows the platform logo (ChannelIcon component), connection label, and a source Handle on the right (channel-conn-source:{connectionId}).
  • The connection row handle is visible only when: KB connections toggle is on AND the KB section is open AND the connection’s KB is expanded (visible in visibleKbIds).
  • Has a target Handle on the left (channel-detail-target:{channelType}).
  • Visible when the parent channel type row is toggled on in the channels collection node.

Apps collection node (kind: "apps-collection")

Section titled “Apps collection node (kind: "apps-collection")”
  • Width: 296px
  • Shows: widgets icon, “Apps” label, count.
  • Lists activated apps (e.g. “Comment Responder”, “Chatti Live”) with their icons.
  • Visible when the Apps section is toggled on in the workspace node.

Structural edges currently use:

  • light mode: #a2aab4
  • dark mode: #5a5a62
  • base strokeWidth: 1
EdgeSource → Target
Workspace → KB collectionws:{id}kb-collection:{id}
Workspace → Agents headerws:{id}agents-header:{id}
Workspace → Users collectionws:{id}users-collection:{id}
Workspace → Channels collectionws:{id}channels-collection:{id}
Workspace → Apps collectionws:{id}apps-collection:{id}
KB collection → KB detailkb-collection:{id}kb-detail:{kbId}
Channels collection → Channel detailchannels-collection:{id}channel-detail:{id}:{channelType}

Agent→KB dashed edges (animated, emerald)

Section titled “Agent→KB dashed edges (animated, emerald)”
  • Class: edge-agent-kb
  • stroke: "#34d399" (emerald), strokeWidth: 1.5
  • stroke-dasharray: 5 4 (dashes and gaps)
  • Animated: dashes “run” right-to-left (from KB toward agent) via @keyframes dash-flow-rtl (1s linear infinite)
  • zIndex: 10 (renders above node elements)
  • Source: agents-header:{workspaceId} via per-row handle agent-row-source:{agentId}
  • Target: kb-detail:{kbId} via kb-detail-target:{kbId}
  • Condition: drawn when KB section is open AND the agent’s KB is in visibleKbIds

Channel→KB dashed edges (animated, light blue)

Section titled “Channel→KB dashed edges (animated, light blue)”
  • Class: edge-channel-kb
  • stroke: "#7dd3fc" (light blue), strokeWidth: 1.5
  • stroke-dasharray: 5 4 (dashes and gaps)
  • Animated: same dash-flow-rtl keyframes as agent→KB edges
  • zIndex: 10
  • Source: channel-detail:{workspaceId}:{channelType} via per-row handle channel-conn-source:{connectionId}
  • Target: kb-detail:{kbId} via kb-detail-target:{kbId}
  • Condition: drawn when KB section is open AND the connection’s KB is in visibleKbIds

When the user hovers over any edge:

  • The hovered edge: opacity: 1, stroke: "#555", strokeWidth: 2.5
  • All other edges: opacity: 0.12
  • All edges have transition: "opacity 0.15s, stroke 0.15s, stroke-width 0.15s"
  • When no edge is hovered, all edges render at full opacity with transition: "opacity 0.15s"

The workspace node’s 5 section rows each control a visibleSections boolean:

type WorkspaceSectionKey = "knowledgeBases" | "agents" | "users" | "channels" | "apps";
type VisibleSectionsState = Record<WorkspaceSectionKey, boolean>;

Default (no persisted layout): All sections open (true).

When knowledgeBases is toggled off:

  • The KB collection node and all KB detail nodes are removed from the graph.
  • All KB item handles in the KB collection node are hidden.
  • All agent row handles are hidden (no KB to connect to).
  • All channel connection row handles are hidden.
  • All dashed edges are removed.
  • visibleKbIds is cleared.

When knowledgeBases is toggled back on:

  • All KBs are re-expanded (all added back to visibleKbIds).

visibleKbIds: Set<string> tracks which individual KB detail nodes are expanded.

Default (no persisted layout): All KBs in the workspace are visible.

Toggling a KB row in the KB collection node adds/removes it from visibleKbIds.


visibleChannelTypes: Set<ChannelId> tracks which channel detail nodes are expanded.

Default (no persisted layout): All channel types that have at least one connection are visible (derived from the connections prop).

Toggling a channel type row in the channels collection node adds/removes it from visibleChannelTypes.


The node view layout is persisted server-side, per user, per workspace. The persistence key is nodeViewLayouts in the user data store.

interface NodeViewLayout {
visibleSections: Record<WorkspaceSectionKey, boolean>;
visibleKbIds: string[];
visibleChannelTypes?: string[];
positions: Record<string, { x: number; y: number }>;
}
  • visibleSections: which section collection nodes are open
  • visibleKbIds: which KB detail nodes are expanded
  • visibleChannelTypes: which channel detail nodes are expanded
  • positions: node positions that the user has dragged (keyed by node ID)

Saves are debounced — a 800ms idle timer fires after the last change. Rapid changes (dragging, toggling) are batched into a single save. The save is a POST /api/user-data with { type: "save-node-view-layouts", nodeViewLayouts: allLayouts }.

The allLayouts object always includes layouts for all workspaces (not just the current one), so switching workspaces and saving does not overwrite other workspaces’ layouts.

On mount, the layout for the current workspaceId is read from initialLayouts (server-provided via the parent page). On workspace switch, the layout for the new workspace is read from the allLayoutsRef (which is kept in sync with all saves during the session).

If no persisted layout exists for a workspace, the defaults described above apply.


The animated dashed edges (agent→KB and channel→KB) use inline ReactFlow edge styles applied directly in workspace-node-view.tsx — they are not defined as global CSS classes in src/app/globals.css.

globals.css contains only the node-view sheet open/close keyframes (node-sheet-enter, node-sheet-exit) and the .node-view-sheet animation rules. The dash-flow-rtl keyframe and .edge-agent-kb / .edge-channel-kb CSS classes do not exist in globals.css.

The animation is applied via the edge’s style prop in ReactFlow:

// Applied inline on each dashed edge object
style: {
strokeDasharray: "5 4",
animation: "dash-flow-rtl 1s linear infinite",
// stroke and strokeWidth set per edge type (emerald / light blue)
}

The dash-flow-rtl keyframe itself is defined inline (e.g. via a <style> tag or CSS-in-JS) within the node view component, not in the global stylesheet.

The animation logic: stroke-dashoffset animates from 0 to 18 (matching the dash cycle length 5 + 4 + 5 + 4 = 18) for a seamless right-to-left flowing dash effect.


<ReactFlow
nodeTypes={{ graph: GraphNodeCard }}
defaultEdgeOptions={{ style: { strokeWidth: 1 } }}
nodesDraggable
nodesConnectable={false}
elementsSelectable
panOnScroll
zoomOnScroll={false}
fitView
colorMode={resolvedTheme === "dark" ? "dark" : "light"}
onNodeClick={(_, node) => setSelectedNodeId(node.id)}
onPaneClick={() => setSelectedNodeId(null)}
onEdgeMouseEnter={(_, edge) => setHoveredEdgeId(edge.id)}
onEdgeMouseLeave={() => setHoveredEdgeId(null)}
>
<Controls />
<MiniMap />
<FitViewController ... />
</ReactFlow>

  • @xyflow/react — ReactFlow graph renderer
  • elkjs — ELK layout algorithm
  • ChannelIcon (src/app/account/channels/_components/channel-icon.tsx) — platform-specific logos in channel detail node rows
  • next-themes — dark/light mode for ReactFlow colorMode
  • src/app/globals.css — node-view sheet open/close keyframes (node-sheet-enter, node-sheet-exit); dashed edge animations are inline in the component

StateBehavior
First open (no persisted layout)All sections open, all KBs expanded, all channel types with connections expanded, graph auto-fits
Persisted layout existsSections, KBs, channel types, and node positions restored from server
Workspace switchLayout for new workspace loaded from server (or defaults if none); graph auto-fits
KB connections toggle offAll dashed edges and source handle dots hidden instantly
Inspector closed + node clickInspector opens and shows selected node details
No connections for a channel typeChannel type row not shown in channels collection node