EQUILIBRIUM UI KIT / FIGMA DOCUMENT. Now let's do it as a real UI document under Figma, and not just a description. You can give it to the designer or assemble it yourself. We give:
- Frame structure
- dimensions
- Components
- Styles
- logic of interaction
EQUILIBRIUM UI KIT / FIGMA DOCUMENT
FILE STRUCTURE FIGMA
Create file: EQUILIBRIUM / INTERFACE V1
Pages (pages):
- 00 — Foundations (styles)
- 01 — Components
- 02 — Screens
- 03 — Flows (logic)
PAGE 00 — FOUNDATIONS
Color system
Main:
- Background: #0B0F1A (deep dark)
- Primary: #4DA3FF (blue mind)
- Accent: #7B61FF (purple transformation)
Status:
- 🟢 Harmony: #3DDC97
- 🟡 Tension: #FFC857
- 🔴 Crisis: #FF4D4F
- 🔵 Growth: #4DA3FF
- ⚫ Unknown: #2A2F3A
Surfaces:
- Card: #121826
- Border: #1F2633
Typography
- Heading: Inter / Bold / 20–28px
- Text: Inter / Regular / 14–16px
- Small: 12px
Effects
- Glow (for hyper edges):
- blur: 20–40
- opacity: 0.3–0.6
- Hover:
- scale: 1.05
- glow gain
PAGE 01 — COMPONENTS
Node (node). Dimensions:
- S: 16px
- M: 32px
- L: 64px
Properties:
Status:
HyperEdge (hyperrib)
. It's not a line.
Visual:
Options
Flow Line
Panel (card)
Size
Contains:
Button
Types:
- Primary
- Secondary
- Ghost
Mode Switch. Switch:
- Overview
- Diagnostics
- Scenario
- Action
Timeline
PAGE 02 — SCREENS
SCREEN 1 — GLOBAL DASHBOARD
Frame
1440 × 900 (desktop)
Layout
TOP BAR (64px)
LEFT PANEL (240px) | CENTER GRAPH | RIGHT PANEL (400px)
BOTTOM TIMELINE (120px)
TOP BAR
Left → Right:
- Logo (EQUILIBRIUM)
- Search
- Level selector
- Mode switch
- AI button
- Settings
LEFT PANEL
Sections:
- Node types
- Types of Connections
- Status
Use the checkboxes
CENTER — GRAPH AREA
This is the main frame.
Inside:
- Nodes (Auto Layout disabled)
- HyperEdges (under layer)
- Flows (top layer animation) > Use absolute positioning
RIGHT PANEL
Condition 1 — Node Vie
- Title
- Type
- Metrics
- Connections
- Buttons
Condition 2 — Edge View
- Meaning
- Participants
- Resources
- Effect
- Metrics
BOTTOM — TIMELINE
- Time Line
- Event Points
- drag
PAGE 03 — FLOWS
Flow 1 — Node selection
Click →
→ backlight
→ Opening Panel
Flow 2 — Selection of hyperribs
Click →
→ Protocol Disclosure
Flow 3 — Creating a connection
- Select Node
- Press .
- Select Type
- fill in the fields
Flow 4 — Diagnostics
Switching mode →
→ problem highlighting
Flow 5 — Script
Parameter change →
→ graph update
START COMPONENTS (minimum)
If you want to quickly:
Do it:
- Node (1 component)
- Panel
- Graph area
- Top bar
That's enough for the first prototype.
CRITICAL RULES (remember)
- Don't overload the screen.
- Don't make small elements
- Don't make complicated forms.
3 main principles:
Simplicity
> Readability
Feeling of a living system
Stack MVP
MVP
We go into the code so that we get the skeleton of MVP, not a set of beautiful words. Below we give:
- stack,
- structure of the project,
- data models,
- front to React,
- st.,
- API,
- Neo4j-scheme,
- The first working scenario.
For the visual graph, it is now reasonable to take React Flow; the current version uses the ?xyflow/react package, and custom nodes there are made with conventional React components. For the graph database, this naturally lies under the official neo4j-driver.
Stack MVP
Frontend
- Next.js / React
- TypeScript
- @xyflow/react
- Zustand
Backend
- Next.js Route Handlers or Node/Express
- TypeScript
- Neo4j
- neo4j-driver
Why so
- React Flow is already sharpened under node-based UI.
- Neo4j is well suited because we need nodes, connections, relationship types and graph queries. The official JS driver is directly supported.
Project structure
equilibrium-mvp/
├─ app/
│ ├─ page.tsx
│ ├─ api/
│ │ ├─ graph/route.ts
│ │ ├─ node/route.ts
│ │ └─ edge/route.ts
├─ components/
│ ├─ graph/
│ │ ├─ EquilibriumCanvas.tsx
│ │ ├─ nodes/
│ │ │ ├─ MeaningNode.tsx
│ │ │ ├─ ActorNode.tsx
│ │ │ └─ EffectNode.tsx
│ │ ├─ panels/
│ │ │ ├─ LeftFilters.tsx
│ │ │ ├─ RightInspector.tsx
│ │ │ └─ BottomTimeline.tsx
│ │ └─ TopBar.tsx
├─ lib/
│ ├─ neo4j.ts
│ ├─ mapper.ts
│ └─ constants.ts
├─ store/
│ └─ equilibrium.store.ts
├─ types/
│ └─ equilibrium.ts
├─ cypher/
│ ├─ schema.cypher
│ ├─ seed.cypher
│ └─ queries.cypher
├─ package.json
└─ .env.local
Data types
Start not with 12 types, but with 6. Otherwise you'll drown.
types/equilibrium.ts
export type EQNodeType =
| 'meaning'
| 'actor'
| 'process'
| 'resource'
| 'norm'
| 'effect';
export type EQEdgeType =
| 'intent'
| 'cooperation'
| 'production'
| 'feedback'
| 'evolution';
export type EQState =
| 'harmony'
| 'tension'
| 'crisis'
| 'growth'
| 'transformation'
| 'unknown';
export interface EQMetrics {
trust?: number;
activity?: number;
load?: number;
influence?: number;
stability?: number;
ethics?: number;
}
export interface EQNodeData {
label: string;
nodeType: EQNodeType;
description?: string;
state: EQState;
metrics: EQMetrics;
tags?: string[];
}
export interface EQNode {
id: string;
type: EQNodeType;
position: { x: number; y: number };
data: EQNodeData;
}
export interface EQEdgeData {
label: string;
edgeType: EQEdgeType;
meaning?: string;
process?: string;
norm?: string;
metric?: string;
ethics?: string;
intensity?: number;
risk?: number;
state: EQState;
}
export interface EQEdge {
id: string;
source: string;
target: string;
type: 'default' | 'hyper';
data: EQEdgeData;
}
export interface GraphPayload {
nodes: EQNode[];
edges: EQEdge[];
}
Zustand store
store/equilibrium.store.ts
'use client';
import { create } from 'zustand';
import type { GraphPayload, EQNode, EQEdge, EQState, EQNodeType, EQEdgeType } from '@/types/equilibrium';
interface FiltersState {
nodeTypes: EQNodeType[];
edgeTypes: EQEdgeType[];
states: EQState[];
}
interface EquilibriumStore {
graph: GraphPayload;
selectedNode: EQNode | null;
selectedEdge: EQEdge | null;
filters: FiltersState;
setGraph: (graph: GraphPayload) => void;
selectNode: (node: EQNode | null) => void;
selectEdge: (edge: EQEdge | null) => void;
updateFilters: (filters: Partial<FiltersState>) => void;
upsertNode: (node: EQNode) => void;
upsertEdge: (edge: EQEdge) => void;
}
export const useEquilibriumStore = create<EquilibriumStore>((set) => ({
graph: { nodes: [], edges: [] },
selectedNode: null,
selectedEdge: null,
filters: {
nodeTypes: ['meaning', 'actor', 'process', 'resource', 'norm', 'effect'],
edgeTypes: ['intent', 'cooperation', 'production', 'feedback', 'evolution'],
states: ['harmony', 'tension', 'crisis', 'growth', 'transformation', 'unknown'],
},
setGraph: (graph) => set({ graph }),
selectNode: (node) => set({ selectedNode: node, selectedEdge: null }),
selectEdge: (edge) => set({ selectedEdge: edge, selectedNode: null }),
updateFilters: (filters) =>
set((state) => ({
filters: { ...state.filters, ...filters },
})),
upsertNode: (node) =>
set((state) => {
const exists = state.graph.nodes.some((n) => n.id === node.id);
return {
graph: {
...state.graph,
nodes: exists
? state.graph.nodes.map((n) => (n.id === node.id ? node : n))
: [...state.graph.nodes, node],
},
};
}),
upsertEdge: (edge) =>
set((state) => {
const exists = state.graph.edges.some((e) => e.id === edge.id);
return {
graph: {
...state.graph,
edges: exists
? state.graph.edges.map((e) => (e.id === edge.id ? edge : e))
: [...state.graph.edges, edge],
},
};
}),
}));
Graph Component
React Flow renders node, edge, and handles interaction via callbacks like onNodesChange. That's exactly what we need for the base canvas.
components/graph/EquilibriumCanvas.tsx
'use client';
import { useEffect, useMemo } from 'react';
import {
Background,
Controls,
MiniMap,
ReactFlow,
ReactFlowProvider,
addEdge,
useEdgesState,
useNodesState,
Connection,
Edge,
NodeMouseHandler,
EdgeMouseHandler,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { useEquilibriumStore } from '@/store/equilibrium.store';
import MeaningNode from './nodes/MeaningNode';
import ActorNode from './nodes/ActorNode';
import EffectNode from './nodes/EffectNode';
const nodeTypes = {
meaning: MeaningNode,
actor: ActorNode,
effect: EffectNode,
};
function CanvasInner() {
const { graph, selectNode, selectEdge } = useEquilibriumStore();
const initialNodes = useMemo(() => graph.nodes, [graph.nodes]);
const initialEdges = useMemo(
() =>
graph.edges.map((e) => ({
...e,
animated: e.data.state === 'growth',
style: {
strokeWidth: 2,
},
})),
[graph.edges]
);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
useEffect(() => setNodes(initialNodes), [initialNodes, setNodes]);
useEffect(() => setEdges(initialEdges), [initialEdges, setEdges]);
const onConnect = (connection: Connection) => {
setEdges((eds) =>
addEdge(
{
...connection,
type: 'default',
data: {
label: 'New relation',
edgeType: 'cooperation',
state: 'unknown',
},
},
eds
)
);
};
const onNodeClick: NodeMouseHandler = (_, node) => {
selectNode(node as any);
};
const onEdgeClick: EdgeMouseHandler = (_, edge) => {
selectEdge(edge as any);
};
return (
<div style={{ width: '100%', height: '100%' }}>
<ReactFlow
nodes={nodes}
edges={edges as Edge[]}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
nodeTypes={nodeTypes}
fitView
>
<MiniMap />
<Controls />
<Background gap={20} />
</ReactFlow>
</div>
);
}
export default function EquilibriumCanvas() {
return (
<ReactFlowProvider>
<CanvasInner />
</ReactFlowProvider>
);
}
Custom Nodes
In React Flow, custom nodes are the usual React components that are transferred to nodeTypes. This is an officially supported path.
components/graph/nodes/MeaningNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
export default function MeaningNode({ data }: NodeProps) {
return (
<div
style={{
minWidth: 180,
borderRadius: 16,
padding: 12,
background: '#121826',
border: '1px solid #1F2633',
boxShadow: '0 0 24px rgba(123, 97, 255, 0.22)',
}}
>
<Handle type="target" position={Position.Left} />
<div style={{ fontSize: 12, opacity: 0.7 }}>Meaning</div>
<div style={{ fontSize: 16, fontWeight: 700 }}>{data.label}</div>
<div style={{ fontSize: 12, marginTop: 8 }}>
Ethics: ?data.metrics?.ethics ?? '-'
</div>
<Handle type="source" position={Position.Right} />
</div>
);
}
components/graph/nodes/ActorNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
export default function ActorNode({ data }: NodeProps) {
return (
<div
style={{
minWidth: 180,
borderRadius: 16,
padding: 12,
background: '#121826',
border: '1px solid #1F2633',
boxShadow: '0 0 24px rgba(77, 163, 255, 0.18)',
}}
>
<Handle type="target" position={Position.Left} />
<div style={{ fontSize: 12, opacity: 0.7 }}>Subject</div>
<div style={{ fontSize: 16, fontWeight: 700 }}>{data.label}</div>
<div style={{ fontSize: 12, marginTop: 8 }}>
Trust: ?data.metrics?.trust ?? '-'
</div>
<Handle type="source" position={Position.Right} />
</div>
);
}
components/graph/nodes/EffectNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
export default function EffectNode({ data }: NodeProps) {
return (
<div
style={{
minWidth: 180,
borderRadius: 16,
padding: 12,
background: '#121826',
border: '1px solid #1F2633',
boxShadow: '0 0 24px rgba(61, 220, 151, 0.18)',
}}
>
<Handle type="target" position={Position.Left} />
<div style={{ fontSize: 12, opacity: 0.7 }}>Effect</div>
<div style={{ fontSize: 16, fontWeight: 700 }}>{data.label}</div>
<div style={{ fontSize: 12, marginTop: 8 }}>
Stability: ?data.metrics?.stability ?? '-'
</div>
<Handle type="source" position={Position.Right} />
</div>
);
}
Main page
app/page.tsx
'use client';
import { useEffect } from 'react';
import EquilibriumCanvas from '@/components/graph/EquilibriumCanvas';
import { useEquilibriumStore } from '@/store/equilibrium.store';
export default function HomePage() {
const { setGraph, selectedNode, selectedEdge } = useEquilibriumStore();
useEffect(() => {
fetch('/api/graph')
.then((r) => r.json())
.then(setGraph)
.catch(console.error);
}, [setGraph]);
return (
<main
style={{
display: 'grid',
gridTemplateColumns: '240px 1fr 400px',
gridTemplateRows: '64px 1fr 120px',
height: '100vh',
background: '#0B0F1A',
color: 'white',
}}
>
<header style={{ gridColumn: '1 / 4', borderBottom: '1px solid #1F2633', display: 'flex', alignItems: 'center', padding: '0 16px' }}>
EQUILIBRIUM
</header>
<aside style={{ borderRight: '1px solid #1F2633', padding: 16 }}>
<h3>Filters</h3>
<div>Nodes, links, states</div>
</aside>
<section style={{ position: 'relative' }}>
<EquilibriumCanvas />
</section>
<aside style={{ borderLeft: '1px solid #1F2633', padding: 16 }}>
!selectedNode && !selectedEdge && <div>Choose node or link</div>
{selectedNode && (
<>
<h3>{selectedNode.data.label}</h3>
<p>Type: {selectedNode.data.nodeType}</p>
<p>Status: {selectedNode.data.state}</p>
<pre style={{ whiteSpace: 'pre-wrap' }}>
{JSON.stringify(selectedNode.data.metrics, null, 2)}
</pre>
</>
)}
{selectedEdge && (
<>
<h3>{selectedEdge.data.label}</h3>
<p>Type: {selectedEdge.data.edgeType}</p>
<p>Status: {selectedEdge.data.state}</p>
<p>Meaning: ?selectedEdge.data.meaning || '-'}</p>
<p>Ethics: ?selectedEdge.data.ethics || '-'}</p>
</>
)}
</aside>
<footer style={{ gridColumn: '1 / 4', borderTop: '1px solid #1F2633', padding: 16 }}>
Timeline / Simulation
</footer>
</main>
);
}
Neo4j connection
The official driver is connected via neo4j.driver(...); the documentation also recommends explicitly specifying the target base for performance.
lib/neo4j.ts
import neo4j from 'neo4j-driver';
const uri = process.env.NEO4J_URI!;
const username = process.env.NEO4J_USERNAME!;
const password = process.env.NEO4J_PASSWORD!;
const database = process.env.NEO4J_DATABASE || 'neo4j';
export const driver = neo4j.driver(uri, neo4j.auth.basic(username, password));
export { database };
API Read Count
app/api/graph/route.ts
import { NextResponse } from 'next/server';
import { driver, database } from '@/lib/neo4j';
export async function GET() {
const query = `
MATCH (n:EQNode)
OPTIONAL MATCH (n)-[r:RELATES_TO]->(m:EQNode)
RETURN collect(DISTINCT {
id: n.id,
type: n.nodeType,
position: { x: n.x, y: n.y },
data: {
label: n.label,
nodeType: n.nodeType,
description: n.description,
state: n.state,
metrics: {
trust: n.trust,
activity: n.activity,
load: n.load,
influence: n.influence,
stability: n.stability,
ethics: n.ethics
},
tags: n.tags
}
}) AS nodes,
collect(DISTINCT CASE WHEN r IS NULL THEN NULL ELSE {
id: r.id,
source: startNode(r).id,
target: endNode(r).id,
type: 'default',
data: {
label: r.label,
edgeType: r.edgeType,
meaning: r.meaning,
process: r.process,
norm: r.norm,
metric: r.metric,
ethics: r.ethics,
intensity: r.intensity,
risk: r.risk,
state: r.state
}
} END) AS edges
`;
const result = await driver.executeQuery(query, {}, { database });
const record = result.records[0];
const nodes = record.get('nodes');
const edges = record.get('edges').filter(Boolean);
return NextResponse.json({ nodes, edges });
}
Creating a node
app/api/node/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { driver, database } from '@/lib/neo4j';
export async function POST(req: NextRequest) {
const body = await req.json();
const query = `
CREATE (n:EQNode {
id: $id,
label: $label,
nodeType: $nodeType,
description: $description,
state: $state,
x: $x,
y: $y,
trust: $trust,
activity: $activity,
load: $load,
influence: $influence,
stability: $stability,
ethics: $ethics,
tags: $tags
})
RETURN n
`;
await driver.executeQuery(
query,
{
id: body.id,
label: body.data.label,
nodeType: body.data.nodeType,
description: body.data.description || '',
state: body.data.state,
x: body.position.x,
y: body.position.y,
trust: body.data.metrics?.trust ?? null,
activity: body.data.metrics?.activity ?? null,
load: body.data.metrics?.load ?? null,
influence: body.data.metrics?.influence ?? null,
stability: body.data.metrics?.stability ?? null,
ethics: body.data.metrics?.ethics ?? null,
tags: body.data.tags ?? [],
},
{ database }
);
return NextResponse.json({ ok: true });
}
Creating a connection
app/api/edge/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { driver, database } from '@/lib/neo4j';
export async function POST(req: NextRequest) {
const body = await req.json();
const query = `
MATCH (a:EQNode {id: $source})
MATCH (b:EQNode {id: $target})
CREATE (a)-[r:RELATES_TO {
id: $id,
label: $label,
edgeType: $edgeType,
meaning: $meaning,
process: $process,
norm: $norm,
metric: $metric,
ethics: $ethics,
intensity: $intensity,
risk: $risk,
state: $state
}]->(b)
RETURN r
`;
await driver.executeQuery(
query,
{
id: body.id,
source: body.source,
target: body.target,
label: body.data.label,
edgeType: body.data.edgeType,
meaning: body.data.meaning || '',
process: body.data.process || '',
norm: body.data.norm || '',
metric: body.data.metric || '',
ethics: body.data.ethics || '',
intensity: body.data.intensity ?? null,
risk: body.data.risk ?? null,
state: body.data.state,
},
{ database }
);
return NextResponse.json({ ok: true });
}
Cypher-scheme
cypher/schema.cypher
CREATE CONSTRAINT eqnode_id IF NOT EXISTS
FOR (n:EQNode)REQUIRE n.id IS UNIQUE;
cypher/seed.cypher
CREATE (:EQNode {
id: 'meaning-1',
label: 'Development of competences',
nodeType: 'meaning',
state: 'growth',
x: 100,
y: 120,
ethics: 0.9
});
CREATE (:EQNode {
id: 'actor-1',
label: 'Cooperative FENIX',
nodeType: 'actor',
state: 'harmony',
x: 420,
y: 180,
trust: 0.82,
activity: 0.77
});
CREATE (:EQNode {
id: 'effect-1',
label: 'Employment growth',
nodeType: 'effect',
state: 'growth',
x: 760,
y: 200,
stability: 0.71
});
MATCH (a:EQNode {id: 'meaning-1'})
MATCH (b:EQNode {id: 'actor-1'})
CREATE (a)-[:RELATES_TO {
id: 'edge-1',
label: 'Intention',
edgeType: 'intent',
meaning: 'Start the educational circuit',
ethics: 'Availability',
state: 'growth',
intensity: 0.8
}]->(b);
MATCH (a:EQNode {id: 'actor-1'})
MATCH (b:EQNode {id: 'effect-1'})
CREATE (a)-[:RELATES_TO {
id: 'edge-2',
label: 'Feedback',
edgeType: 'feedback',
meaning: 'Assessment of the result of the program',
metric: 'Employment Index',
state: 'harmony',
intensity: 0.7
}]->(b);
Example JSON for front
"nodes": [
{
"id": "meaning-1",
"type": "meaning",
"position": { "x": 100, "y": 120 },
"data": {
"label": "Development of competences",
"nodeType": "meaning",
"state": "growth",
"metrics": { "ethics": 0.9 }
}
},
{
"id": "actor-1",
"type": "actor",
"position": { "x": 420, "y": 180 },
"data": {
"label": "Cooperative FENIX",
"nodeType": "actor",
"state": "harmony",
"metrics": { "trust": 0.82, "activity": 0.77 }
}
}
],
"edges": [
{
"id": "edge-1",
"source": "meaning-1",
"target": "actor-1",
"type": "default",
"data": {
"label": "Intention",
"edgeType": "intent",
"meaning": "Starting the educational circuit",
"state": "growth",
"intensity": 0.8
}
}
]
}
What to do with the “hyper-ribs” really
Here's an important point. React Flow works with regular edge between two nodes. This is his basic model as a flowgraph UI. So for MVP do this:
Option A is practical
- store “hyperrebro” as a separate node such as process or protocol
- Connecting participants with regular Edges
That is:
Meaning -> ProtocolHub -> Actor
Resource -> ProtocolHub
Norm -> ProtocolHub
ProtocolHub -> Effect
It's an honest decision. It does not break the UI and allows you to show a multidimensional connection.
Type for this
export type EQNodeType =
| 'meaning'
| 'actor'
| 'process'
| 'resource'
| 'norm'
| 'effect'
| 'protocol';
And then the “hyperrebro” at the data level is a protocol node with a set of input/output relationships. It's the best start.
The first working scenario
Don't spray. Do one scenario:
Scenario: “Cooperative Education”
Nodes:
- Meaning: competence development
- Subject: Cooperative
- Resource: fund
- Regulation: regulation
- Protocol: educational program
- Effect: Employment growth
Connections:
- meaning → protocol
- actor → protocol
- resource → protocol
- norm → protocol
- protocol → effect
This is MVP, which can be shown.
Commands to run
package.json
Dependency
{
"dependencies": {
"@xyflow/react": "^12",
"neo4j-driver": "^6",
"next": "^15",
"react": "^19",
"react-dom": "^19",
"zustand": "^5"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "^19",
"typescript": "^5"
}
}
The ?xyflow/react package is the current name for React Flow, and the Neo4j JS driver is now in the current 6.x branch.
.env.local
NEO4J_URI=neo4j://localhost
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your_password
NEO4J_DATABASE=neo4j
What will you have after that?
After implementing this framework, you will have:
- The interactive graph,
- the castes,
- right panel inspector,
- download data from Neo4j,
- creating nodes and connections through API,
- basis for scenario, diagnosis and simulation.
This is no longer a concept. This is a living product.
Below I give the already assembled frame of the system: what is each module, what are the custom nodes, how to arrange an interactive graph, and code that can be immediately put in MVP. For React Flow, the current package is ?xyflow/react; custom nodes there are made as normal React components, transmitted via nodeTypes, and connection points are specified via <Handle />. For Neo4j, it is now normal to use the official JS driver and driver.executeQuery() with parameters and an explicit indication of the base.
Definitions of system modules
We recommend that the first working version capture 10 modules.
Graph Core
heart of the system. Stores nodes, connections, user choices, modes, filters, layout, and interface events. What he does:
- keeps the current state of the graph
- Updates nodes and connections
- Tells UI what is selected
- Repainting
Node Registry
Registry of node types. What he does:
- binds node.type → React component
- Specifies how to render meaning, actor, resource, protocol, and so on
- allows you to expand the system without rewriting the canvas
Edge Registry. Register of connections. What he does:
- Defines the visuals and behavior of connections
- sets styles for intent, cooperation, feedback, evolution
- later may include custom ribs with signatures and risk indicators
Protocol Engine
This is the key module EQUILIBRIUM. What he does:
- Collects multi-dimensional action from multiple entities
- actually implements the "hyperrebro" as a separate protocol node
- connects meaning, actors, resources, norms and effects into one circuit
For MVP, this is the best path, because React Flow works by its nature with conventional edges between two nodes; a multidimensional connection is more convenient to imagine as a separate hub node.
Inspector Panel. Right panel. What he does:
- shows the passport of the selected node
- shows the passport of the selected connection
- allows you to edit attributes
- displays metrics, risks, ethics, load
Filter & Views. Left panel. What he does:
- filters by node types
- Filters by state
- filters by types of links
- switches modes: review, diagnosis, scenario, action
Timeline & Events. Bottom panel. What he does:
- Shows events in time
- Allows you to lose count status
- In the future, it will simulate scenarios.
Diagnostics Engine. Diagnostics of the system. What he does:
- looking for overloaded nodes
- Finds conflict links
- Highlighting the crisis areas
- calculates “holes” in the contour: for example, there is a process, but there is no norm
Persistence Layer. Layer of storage.. What he does:
- reads and writes the graph in Neo4j
- Saves node positions
- preserves metrics and states
- gives JSON to the front
AI/Rule Layer. Layer of recommendations. What he does:
- offers new connections
- Warns of risks
- the Simple Heuristics
- explains why the system marks the node as a crisis
Types of system nodes
You do not need 30 types to start. It needs 7.
export type EQNodeType =
| 'meaning' // meaning
| 'actor' // subject
| 'resource' // resource
| 'norm' // norm
| 'effect' // effect
| 'protocol' // hyperrebro as hub node
| 'metric'; // metric
Meaning. Meaning, mission, value. Answers the question: “Why?”
Actor. Person, organization, agent, co-operative. Answers the question: “who”.
Resource. Money, fund, data, energy, time. Answers the question: “at the expense of what.”
Norm. Regulations, rules, contract, law. Answers the question: “What are the limitations?”
Effect Result. Answers the question: “What happened?”
Protocol. Central working module. This is the implementation of multidimensional communication. He gathers around himself the other entities.
Metric. Result meter: confidence index, sustainability index, employment index.
Types of Connections
export type EQEdgeType =
| 'intent'
| 'input'
| 'constraint'
| 'output'
| 'feedback'
| 'evolution';
Meaning:
- intent - meaning is included in the protocol
- input — resource or actor participates in the protocol
- contrast - the norm limits the protocol
- output - the protocol creates an effect
- feedback - the effect affects back
- evolution — change of state in time
Basic types of data
types/equilibrium.ts
export type EQState =
| 'harmony'
| 'tension'
| 'crisis'
| 'growth'
| 'transformation'
| 'unknown';
export type EQNodeType =
| 'meaning'
| 'actor'
| 'resource'
| 'norm'
| 'effect'
| 'protocol'
| 'metric';
export type EQEdgeType =
| 'intent'
| 'input'
| 'constraint'
| 'output'
| 'feedback'
| 'evolution';
export interface EQMetrics {
trust?: number;
load?: number;
activity?: number;
influence?: number;
stability?: number;
ethics?: number;
value?: number;
}
export interface EQNodeData {
label: string;
nodeType: EQNodeType;
description?: string;
state: EQState;
metrics: EQMetrics;
tags?: string[];
}
export interface EQNode {
id: string;
type: EQNodeType;
position: { x: number; y: number };
data: EQNodeData;
}
export interface EQEdgeData {
label: string;
edgeType: EQEdgeType;
state: EQState;
meaning?: string;
risk?: number;
intensity?: number;
}
export interface EQEdge {
id: string;
source: string;
target: string;
type?: string;
data: EQEdgeData;
}
export interface EQGraph {
nodes: EQNode[];
edges: EQEdge[];
}
Custom nodes: description
In React Flow, a custom node is a normal React component that receives NodeProps. A node can be made with multiple Handles, with interactive controls and its own UI. This is an officially supported model. Below is a set of nodes that is already similar to the system.
MeaningNode. Meaningful knot. Usually small, bright, with one entrance and one exit.
ActorNode. Subject Node. Shows trust, activity and load.
ResourceNode. Resource node. Usually compact, with an indicator of availability.
NormNode. Normative knot. Shows the type of restriction.
EffectNode. Node result. Shows influence and sustainability.
MetricNode. Measurement node. It is used as an evaluation indicator.
ProtocolNode. The most important. Central hub. It has several connection points:
- The Left Meaning
- The actors from above
- Bottom line resources
- Right Effects
- separate handle for standards
- separate handle for metrics
Basic Common Node Container
components/graph/nodes/NodeShell.tsx
'use client';
import { ReactNode } from 'react';
type Props = {
title: string;
subtitle: string;
children?: ReactNode;
glow?: string;
};
export default function NodeShell({ title, subtitle, children, glow }: Props) {
return (
<div
style={{
minWidth: 190,
borderRadius: 18,
padding: 12,
background: '#121826',
border: '1px solid #1F2633',
boxShadow: glow || '0 0 18px rgba(255,255,255,0.08)',
color: '#fff',
}}
>
<div
className="drag-handle"
style={{
fontSize: 11,
opacity: 0.65,
marginBottom: 6,
cursor: 'grab',
}}
>
{subtitle}
</div>
<div style={{ fontWeight: 700, fontSize: 15 }}>{title}</div>
<div style={{ marginTop: 10 }}>{children}</div>
</div>
Custom Nodes: Code
MeaningNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
import NodeShell from './NodeShell';
export default function MeaningNode({ data }: NodeProps) {
return (
<NodeShell
title={data.label}
subtitle="The Meaning"
glow="0 0 24px rgba(123, 97, 255, 0.24)"
>
<div style={{ fontSize: 12, opacity: 0.85 }}>
Ethics: ?data.metrics?.ethics ?? '-'
</div>
<Handle type="target" position={Position.Left} id="in" />
<Handle type="source" position={Position.Right} id="out" />
</NodeShell>
);
ActorNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
import NodeShell from './NodeShell';
export default function ActorNode({ data }: NodeProps) {
return (
<NodeShell
title={data.label}
subtitle="Subject"
glow="0 0 24px rgba(77, 163, 255, 0.20)"
>
<div style={{ fontSize: 12 }}>Trust: ?data.metrics?.trust ?? '-'</div>
<div style={{ fontSize: 12 }}>Activity: ?data.metrics?.activity ?? '-'</div>
<div style={{ fontSize: 12 }}>Load: ?data.metrics?.load ?? '-'</div>
<Handle type="target" position={Position.Left} id="in" />
<Handle type="source" position={Position.Right} id="out" />
</NodeShell>
);
}
ResourceNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
import NodeShell from './NodeShell';
export default function ResourceNode({ data }: NodeProps) {
return (
<NodeShell
title={data.label}
subtitle="Resource"
glow="0 0 24px rgba(255, 200, 87, 0.18)"
>
<div style={{ fontSize: 12 }}>Availability: ?data.metrics?.value ?? '-'</div>
<div style={{ fontSize: 12 }}>Stability: ?data.metrics?.stability ?? '-'</div>
<Handle type="target" position={Position.Left} id="in" />
<Handle type="source" position={Position.Right} id="out" />
</NodeShell>
);
NormNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
import NodeShell from './NodeShell';
export default function NormNode({ data }: NodeProps) {
return (
<NodeShell
title={data.label}
subtitle="Norma"
glow="0 0 24px rgba(255,255,255,0.12)"
>
<div style={{ fontSize: 12 }}>{data.description || 'Rules/Restrictions'</div>
<Handle type="target" position={Position.Left} id="in" />
<Handle type="source" position={Position.Right} id="out" />
</NodeShell>
);
}
EffectNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
import NodeShell from './NodeShell';
export default function EffectNode({ data }: NodeProps) {
return (
<NodeShell
title={data.label}
subtitle="effect"
glow="0 0 24px rgba(61, 220, 151, 0.22)"
>
<div style={{ fontSize: 12 }}>Influence: ?data.metrics?.influence ?? '-'</div>
<div style={{ fontSize: 12 }}>Sustainability: ?data.metrics?.stable ?? '-'</div>
<Handle type="target" position={Position.Left} id="in" />
<Handle type="source" position={Position.Right} id="out" />
</NodeShell>
);
}
MetricNode.tsx
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
import NodeShell from './NodeShell';
export default function MetricNode({ data }: NodeProps) {
return (
<NodeShell
title={data.label}
subtitle="Metrica"
glow="0 0 24px rgba(80, 180, 255, 0.16)"
>
<div style={{ fontSize: 12 }}>Value: ?data.metrics?.value ?? '-'</div>
<Handle type="target" position={Position.Left} id="in" />
<Handle type="source" position={Position.Right} id="out" />
</NodeShell>
);
ProtocolNode.tsx. It's the main node.
'use client';
import { Handle, Position, NodeProps } from '@xyflow/react';
export default function ProtocolNode({ data }: NodeProps) {
return (
<div
style={{
minWidth: 260,
borderRadius: 20,
padding: 14,
background: '#161d2d',
border: '1px solid #2a3550',
boxShadow: '0 0 28px rgba(123, 97, 255, 0.28)',
color: '#fff',
}}
>
<div className="drag-handle" style={{ fontSize: 11, opacity: 0.65, marginBottom: 6 }}>
Protocol / Hyperrebro
</div>
<div style={{ fontSize: 16, fontWeight: 800 }}>{data.label}</div>
<div style={{ marginTop: 10, fontSize: 12, opacity: 0.9 }}>
|| 'Multidimensional contour of action'
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 6,
marginTop: 10,
fontSize: 12,
}}
>
<div>Ethics: .data.metrics?.ethics ?? '-'Zqqq</div>
<div>Load: ?? '-'-'--</div>
<div>Stability: ?data.metrics?.stability ?? '-'QQ</div>
<div>Activity: ??? '-'-'--</div>
</div>
<Handle type="target" position={Position.Left} id="meaning" style={{ top: '20%' }} />
<Handle type="target" position={Position.Top} id="actors" style={{ left: '35%' }} />
<Handle type="target" position={Position.Bottom} id="resources" style={{ left: '35%' }} />
<Handle type="target" position={Position.Left} id="norms" style={{ top: '75%' }} />
<Handle type="source" position={Position.Right} id="effects" style={{ top: '35%' }} />
<Handle type="source" position={Position.Right} id="metrics" style={{ top: '75%' }} />
</div>
);
A node with multiple Handles is fully consistent with the React Flow model, where you can have as many source/target connections points as you want in custom nodes.
Registry of Custom Nodes
components/graph/nodes/index.ts
import MeaningNode from './MeaningNode';
import ActorNode from './ActorNode';
import ResourceNode from './ResourceNode';
import NormNode from './NormNode';
import EffectNode from './EffectNode';
import MetricNode from './MetricNode';
import ProtocolNode from './ProtocolNode';
export const nodeTypes = {
meaning: MeaningNode,
actor: ActorNode,
resource: ResourceNode,
norm: NormNode,
effect: EffectNode,
metric: MetricNode,
protocol: ProtocolNode,
};
Interactive graph: canvas code
<ReactFlow /> is the main component that renders nodes and edges, handles interactions, and works with state. This is the official centerpiece of the library.
components/graph/EquilibriumCanvas.tsx
'use client';
import { useEffect, useMemo, useState } from 'react';
import {
ReactFlow,
ReactFlowProvider,
Background,
MiniMap,
Controls,
addEdge,
useNodesState,
useEdgesState,
Connection,
Edge,
NodeMouseHandler,
EdgeMouseHandler,
BackgroundVariant,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { nodeTypes } from './nodes';
import { EQGraph } from '@/types/equilibrium';
type Props = {
graph: EQGraph;
onSelectNode?: (node: any) => void;
onSelectEdge?: (edge: any) => void;
};
function edgeStyleByState(state?: string) {
switch (state) {
case 'growth':
return { stroke: '#4DA3FF', strokeWidth: 2.5 };
case 'crisis':
return { stroke: '#FF4D4F', strokeWidth: 2.5 };
case 'harmony':
return { stroke: '#3DDC97', strokeWidth: 2.5 };
case 'tension':
return { stroke: '#FFC857', strokeWidth: 2.5 };
default:
return { stroke: '#76839a', strokeWidth: 1.8 };
}
}
function CanvasInner({ graph, onSelectNode, onSelectEdge }: Props) {
const initialNodes = useMemo(() => graph.nodes, [graph.nodes]);
const initialEdges = useMemo(
() =>
graph.edges.map((e) => ({
...e,
animated: e.data.state === 'growth' || e.data.edgeType === 'feedback',
style: edgeStyleByState(e.data.state),
label: e.data.label,
})),
[graph.edges]
);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges as Edge[]);
const [selectedId, setSelectedId] = useState<string | null>(null);
useEffect(() => setNodes(initialNodes), [initialNodes, setNodes]);
useEffect(() => setEdges(initialEdges as Edge[]), [initialEdges, setEdges]);
const onConnect = (connection: Connection) => {
setEdges((eds) =>
addEdge(
{
...connection,
type: 'smoothstep',
data: {
label: 'New connection',
edgeType: 'input',
state: 'unknown',
},
style: edgeStyleByState('unknown'),
},
eds
)
);
};
const handleNodeClick: NodeMouseHandler = (_, node) => {
setSelectedId(node.id);
onSelectNode?.(node);
};
const handleEdgeClick: EdgeMouseHandler = (_, edge) => {
setSelectedId(edge.id);
onSelectEdge?.(edge);
};
return (
<div style={{ width: '100%', height: '100%' }}>
<ReactFlow
nodes={nodes.map((n) => ({
...n,
selected: n.id === selectedId,
dragHandle: '.drag-handle',
}))}
edges={edges}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={handleNodeClick}
onEdgeClick={handleEdgeClick}
fitView
>
<MiniMap />
<Controls />
<Background variant={BackgroundVariant.Dots} gap={20} size={1} />
</ReactFlow>
</div>
);
}
export default function EquilibriumCanvas(props: Props) {
return (
<ReactFlowProvider>
<CanvasInner {...props} />
</ReactFlowProvider>
);
}
Example of an interactive screen
app/page.tsx
'use client';
import { useEffect, useState } from 'react';
import EquilibriumCanvas from '@/components/graph/EquilibriumCanvas';
import { EQGraph } from '@/types/equilibrium';
export default function HomePage() {
const [graph, setGraph] = useState<EQGraph>({ nodes: [], edges: [] });
const [selectedNode, setSelectedNode] = useState<any>(null);
const [selectedEdge, setSelectedEdge] = useState<any>(null);
useEffect(() => {
fetch('/api/graph')
.then((r) => r.json())
.then(setGraph)
.catch(console.error);
}, []);
return (
<main
style={{
display: 'grid',
gridTemplateColumns: '260px 1fr 400px',
gridTemplateRows: '64px 1fr 120px',
height: '100vh',
background: '#0B0F1A',
color: '#fff',
}}
>
<header
style={{
gridColumn: '1 / 4',
borderBottom: '1px solid #1F2633',
display: 'flex',
alignItems: 'center',
padding: '0 16px',
fontWeight: 800,
}}
>
EQUILIBRIUM
</header>
<aside style={{ borderRight: '1px solid #1F2633', padding: 16 }}>
<h3>Filters</h3>
<div>Node types</div>
<div>Status</div>
<div>Link types</div>
</aside>
<section style={{ position: 'relative' }}>
<EquilibriumCanvas
graph={graph}
onSelectNode={(node) => {
setSelectedEdge(null);
setSelectedNode(node);
}}
onSelectEdge={(edge) => {
setSelectedNode(null);
setSelectedEdge(edge);
}}
/>
</section>
<aside style={{ borderLeft: '1px solid #1F2633', padding: 16, overflow: 'auto' }}>
SelectNode & & !selectedEdge && <div>Select node or link</div>
{selectedNode && (
<>
<h3>{selectedNode.data.label}</h3>
<div>Type: {selectedNode.data.nodeType}</div>
<div>Status: {selectedNode.data.state}</div>
<div style={{ marginTop: 12, fontSize: 13, opacity: 0.9 }}>
{selectedNode.data.description || 'No description'}
</div>
<pre style={{ marginTop: 12, fontSize: 12, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(selectedNode.data.metrics, null, 2)}
</pre>
</>
)}
{selectedEdge && (
<>
<h3>{selectedEdge.data.label}</h3>
<div>Type: {selectedEdge.data.edgeType}</div>
<div>Status: {selectedEdge.data.state}</div>
<div>Meaning: ?selectedEdge.data.meaning || '-'}</div>
<div>Risk: ? ? '-'-'--</div>
<div>Intensity: ? '-'-'--</div>
</>
)}
</aside>
<footer
style={{
gridColumn: '1 / 4',
borderTop: '1px solid #1F2633',
padding: 16,
}}
>
Timeline / Events / Simulation
</footer>
</main>
);
}
Graph example for EQUILIBRIUM. This is no longer an abstraction, but a working scenario.
lib/mockGraph.ts
import { EQGraph } from '@/types/equilibrium';
export const mockGraph: EQGraph = {
nodes: [
{
id: 'meaning-1',
type: 'meaning',
position: { x: 60, y: 220 },
data: {
label: 'Development of competences',
nodeType: 'meaning',
description: "The growth of the human potential of the region",
state: 'growth',
metrics: { ethics: 0.92 },
},
},
{
id: 'actor-1',
type: 'actor',
position: { x: 280, y: 60 },
data: {
label: 'Cooperative FENIX',
nodeType: 'actor',
state: 'harmony',
metrics: { trust: 0.82, activity: 0.77, load: 0.42 },
},
},
{
id: 'resource-1',
type: 'resource',
position: { x: 280, y: 360 },
data: {
label: 'Educational Fund',
nodeType: 'resource',
state: 'harmony',
metrics: { value: 0.76, stability: 0.66 },
},
},
{
id: 'norm-1',
type: 'norm',
position: { x: 280, y: 500 },
data: {
label: 'Program Regulations',
nodeType: 'norm',
description: 'Transparency of selection and accessibility of participation',
state: 'harmony',
metrics: {},
},
},
{
id: 'protocol-1',
type: 'protocol',
position: { x: 640, y: 230 },
data: {
label: 'Cooperative Education Protocol',
nodeType: 'protocol',
description: 'Contour: training → participation → employment',
state: 'growth',
metrics: {
ethics: 0.9,
load: 0.58,
stability: 0.71,
activity: 0.79,
},
},
},
{
id: 'effect-1',
type: 'effect',
position: { x: 1020, y: 180 },
data: {
label: 'Employment growth',
nodeType: 'effect',
state: 'growth',
metrics: { influence: 0.74, stability: 0.69 },
},
},
{
id: 'metric-1',
type: 'metric',
position: { x: 1020, y: 340 },
data: {
label: 'Employment Index',
nodeType: 'metric',
state: 'harmony',
metrics: { value: 0.63 },
},
},
],
edges: [
{
id: 'e1',
source: 'meaning-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
label: 'Intention',
edgeType: 'intent',
state: 'growth',
meaning: 'The meaning is included in the protocol',
intensity: 0.88,
},
},
{
id: 'e2',
source: 'actor-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
label: 'Participation',
edgeType: 'input',
state: 'harmony',
meaning: 'Subject executes protocol',
intensity: 0.72,
},
},
{
id: 'e3',
source: 'resource-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
label: 'Resource',
edgeType: 'input',
state: 'harmony',
intensity: 0.68,
},
},
{
id: 'e4',
source: 'norm-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
label: 'Restriction',
edgeType: 'constraint',
state: 'harmony',
meaning: 'Norm limits and directs',
},
},
{
id: 'e5',
source: 'protocol-1',
target: 'effect-1',
type: 'smoothstep',
data: {
label: 'Result',
edgeType: 'output',
state: 'growth',
intensity: 0.81,
},
},
{
id: 'e6',
source: 'protocol-1',
target: 'metric-1',
type: 'smoothstep',
data: {
label: 'Measurement',
edgeType: 'output',
state: 'harmony',
},
},
{
id: 'e7',
source: 'effect-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
label: 'Feedback',
edgeType: 'feedback',
state: 'harmony',
risk: 0.21,
},
},
],
};
Neo4j: storage scheme
The official JS driver Neo4j recommends using parameterized queries, and executeQuery() itself creates a transaction. It is also useful to always explicitly specify a database to reduce overhead.
lib/neo4j.ts
import neo4j from 'neo4j-driver';
const uri = process.env.NEO4J_URI!;
const username = process.env.NEO4J_USERNAME!;
const password = process.env.NEO4J_PASSWORD!;
const database = process.env.NEO4J_DATABASE || 'neo4j';
export const driver = neo4j.driver(uri, neo4j.auth.basic(username, password));
export { database };
cypher/schema.cypher
CREATE CONSTRAINT eqnode_id IF NOT EXISTS
FOR (n:EQNode)
REQUIRE n.id IS UNIQUE;
app/api/graph/route.ts
import { NextResponse } from 'next/server';
import { driver, database } from '@/lib/neo4j';
export async function GET() {
const query = `
MATCH (n:EQNode)
OPTIONAL MATCH (n)-[r:RELATES_TO]->(m:EQNode)
RETURN
collect(DISTINCT {
id: n.id,
type: n.nodeType,
position: { x: n.x, y: n.y },
data: {
label: n.label,
nodeType: n.nodeType,
description: n.description,
state: n.state,
metrics: {
trust: n.trust,
load: n.load,
activity: n.activity,
influence: n.influence,
stability: n.stability,
ethics: n.ethics,
value: n.value
}
}
}) AS nodes,
collect(DISTINCT CASE WHEN r IS NULL THEN NULL ELSE {
id: r.id,
source: startNode(r).id,
target: endNode(r).id,
type: 'smoothstep',
data: {
label: r.label,
edgeType: r.edgeType,
state: r.state,
meaning: r.meaning,
risk: r.risk,
intensity: r.intensity
}
} END) AS edges
`;
const result = await driver.executeQuery(query, {}, { database });
const record = result.records[0];
const nodes = record.get('nodes');
const edges = record.get('edges').filter(Boolean);
return NextResponse.json({ nodes, edges });
}
Diagnosis: Basic rules. This is not AI, but a rigid logic. That's where we should start.
lib/diagnostics.ts
import { EQGraph } from '@/types/equilibrium';
export interface DiagnosticItem {
type: 'warning' | 'error' | 'info';
targetId: string;
message: string;
}
export function runDiagnostics(graph: EQGraph): DiagnosticItem[] {
const result: DiagnosticItem[] = [];
for (const node of graph.nodes) {
if (node.type === 'protocol') {
const incoming = graph.edges.filter((e) => e.target === node.id);
const outgoing = graph.edges.filter((e) => e.source === node.id);
const hasMeaning = incoming.some((e) => e.data.edgeType === 'intent');
const hasActor = incoming.some((e) => e.source.startsWith('actor-'));
const hasResource = incoming.some((e) => e.source.startsWith('resource-'));
const hasNorm = incoming.some((e) => e.data.edgeType === 'constraint');
const hasOutput = outgoing.some((e) => e.data.edgeType === 'output');
if (!hasMeaning) {
result.push({
type: 'error',
targetId: node.id,
message: 'The protocol has no semantic basis',
});
}
if (!hasActor) {
result.push({
type: 'warning',
targetId: node.id,
message: 'The protocol has no executing entity',
});
}
if (!hasResource) {
result.push({
type: 'warning',
targetId: node.id,
message: 'The protocol has no resource input',
});
}
if (!hasNorm) {
result.push({
type: 'warning',
targetId: node.id,
message: 'The protocol has no regulatory limit',
});
}
if (!hasOutput) {
result.push({
type: 'error',
targetId: node.id,
message: 'The protocol has no result',
});
}
if ((node.data.metrics.load ?? 0) > 0.85) {
result.push({
type: 'warning',
targetId: node.id,
message: 'Protocol overloaded',
});
}
}
}
return result;
}
What is an "interactive graph" in fact
Not just a canvas with nodes, but 7 functions:
- Drag
Moving nodes. - Connect
Create a new connection through hands. - Select
Selecting a node or connection. - Inspect
Displays the content on the right. - Filter
Leave only the desired types on the screen. - Diagnose
Highlight mistakes and risks. - Evolve
Add a new protocol, resource, norm, metric.
React Flow is designed for node-based editors and interactive diagrams; the documentation separately describes the customization of nodes, hands, theming, and examples.
What else you need to add to version 2
It's on the case, no jewelry. The following modules are required:
- LeftFilters.tsx
- RightInspector.tsx
- BottomTimeline.tsx
- CreateProtocolModal.tsx
- DiagnosticsPanel.tsx
And the following improvements:
- Maintaining node positions in the database
- Form of creating protocol
- Editing metrics
- Layout algorithm
- Highlighting Conflict Relationships
- SSR-safe configuration if there is a server render; in React Flow for SSR separately there is NodeHandle when DOM is not yet measured.
How to build it in MVP
Minimum stack:
- Next.js
- TypeScript
- @xyflow/react
- Neo4j
- Zustand
Minimal entities:
- 7 node types
- 6 types of links
- 1 Screen
- 1 right panel
- 1 Diagnostics
- 1 script
Minimum scenario:
- meaning
- Subject
- resource
- Norm
- Protocol
- effect
- metrics
This will look like a real operating panel, and not as a scheme “for presentation”.
The most important architectural conclusion
The hyperrebro in EQUILIBRIUM should not be attempted to be forcibly drawn as a magical super-line.
For a working interface, it must be done as a protocol node around which all participants in the process gather.
This decision:
- Does not break React Flow,
- normally stored in Neo4j,
- easily diagnosed,
- It is easy to expand,
- It looks good in UI.
This is the best way to make the system real.
Further, it is logical to collect a full set of project files: package.json, tsconfig, app/page.tsx, all node components, API-routs, seed for Neo4j and CSS, so that it can be simply inserted into the repository and run.