EQUILIBRIUM UI KIT / FIGMA Documento. Vamos a hacer esto como un verdadero documento de la UI bajo Figma, no solo una descripción. Puedes devolvérselo a un diseñador o puedes recogerlo tú mismo. Dimos:
- Estructura de marcos
- Tamaño
- Componentes
- estilos
- lógica de la interacción
EQUILIBRIUM UI KIT / FIGMA
ESTRUCTURA FAIL FIGMA
Crea un archivo: EQUILIBRIUM / INTERFACE V1
Páginas (Pages):
- 00 Foundations (stiles)
- 01 — Components
- 02 — Screens
- 03 Flows (Liquidación)
PAGE 00 — FOUNDATIONS
Sistema de colores
Principales:
- Background: #0B0F1A
- Primary: #4DA3FF
- Accent: #7B61FF (fiolet de transformación)
Antecedentes:
- 🟢 Harmony: #3DDC97
- 🟡 Tension: #FFC857
- 🔴 Crisis: #FF4D4F
- 🔵 Growth: #4DA3FF
- ⚫ Unknown: #2A2F3A
Superficie:
- Card: #121826
- Border: #1F2633
Impresión
- Título: Inter / Bold / 20–28px
- Texto: Inter / Regular / 14–16px
- Pequeño: 12px
Efectos
- Glow (para hiperbero):
- blur: 20–40
- opacity: 0.3–0.6
- Hover:
- scale: 1.05
- glow incremental
PAGE 01 — COMPONENTS
Node (ruido). Tamaño:
- S: 16px
- M: 32px
- L: 64px
Propiedades:
Antecedentes:
HyperEdge (hiperrebro)
. No es una línea.
Visual:
Opciones
Flow Line
Panel (mapa)
Tamaño
Contiene:
Button
Tipos:
- Primary
- Secondary
- Ghost
Mode Switch. Interruptor:
- Sinopsis
- Diagnostico
- Escenario
- Acción
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
De izquierda a derecha:
- Logo (EQUILIBRIUM)
- Search
- Level selector
- Mode switch
- AI button
- Settings
LEFT PANEL
Sections:
- Tipos de nodos
- Tipos de vínculos
- Deliberaciones
- Usa los cheques.
CENTER — GRAPH AREA
Es el marco principal.
Dentro:
- Nodes (Auto Layout disabled)
- HyperEdges (under layer)
- Flows (top layer animation) 👉 Utiliza posicionamiento absoluto
RIGHT PANEL
Estado 1 Node Vie
- Title
- Type
- Metrics
- Connections
- Buttons
Estado financiero 2 Edge View
- Significado
- Asistencia
- Recursos
- Efecto
- Metrics
BOTTOM — TIMELINE
- línea horaria
- punto de acontecimiento
- drag
PAGE 03 — FLOWS
Flow 1 ○ Selección de un nodo
Click
- Resaltar
- Abrir el panel
Flow 2 ○ Selección de hiperrrebras
Click
- La divulgación del protocolo;
Flow 3
- seleccionar un nodo
- clic en
- seleccionar tipo
- Rellenar campos
Flow 4 - Diagnóstico
Cambia de modo
- Resaltar los problemas
Flow 5 ○ Escenario
Cambia la opción de configuración
○ Actualización de la columna
COMPONENTE PARA EL START (mínimo)
Si quieres hacerlo rápido:
Hazlo:
- Node (1 componente)
- Panel
- Graph area
- Top bar
- Es suficiente para el primer prototipo.
REGLAMENTO CRÍTICO (Recordar)
- ○ No sobrecargas la pantalla
- - No hagas pequeños elementos.
- No hagas formas complicadas.
3 Principios básicos:
- Simplicidad
Leer
- Sentido de un sistema vivo
Acero MVP
MVP
Vamos al código para que tengamos un esqueleto MVP, no un conjunto de palabras bonitas. A continuación:
- Acero,
- la estructura del proyecto,
- modelos de datos,
- Frente en React,
- Stor,
- API,
- Neo4j-scemum,
- Primer escenario de trabajo.
Ahora es razonable tomar un React Flow para el gráfico visual; en la versión actual se utiliza un paquete @xyflow/react, y los nodos de casta se fabrican con los componentes React normales. Para el BD del gráfico, es natural que el neo4j-driver sea oficial.
Acero MVP
Frontend
- Next.js / React
- TypeScript
- @xyflow/react
- Zustand
Backend
- Next.js Route Handlers o Node/Express
- TypeScript
- Neo4j
- neo4j-driver
¿Por qué?
- React Flow ya está encerrado bajo node-based UI.
- Neo4j está bien, porque necesitamos nudos, conexiones, tipos de relación y preguntas. El controlador JS oficial se mantiene directamente.
Estructura del proyecto
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
Tipos de datos
Empieza con 12 tipos, no con 6. Si no, te ahogas.
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],
},
};
}),
}));
Componente de columna
React Flow Renderitis nodes, edges y procesa la interacción a través de colbaquis como onNodesChange. Eso es exactamente lo que necesitamos para el lienzo básico.
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>
);
}
Nodos de atómico
En React Flow, los nudos de casta son los componentes de react que se transmiten en nodeTypes. Es un camino que se apoya oficialmente.
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 }}}El propósito </div>
<div style={{ fontSize: 16, fontWeight: 700 }}>{data.label}</div>
<div style={{ fontSize: 12, marginTop: 8 }}>
Ética: {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 }}>Sujeto </div>
<div style={{ fontSize: 16, fontWeight: 700 }}>{data.label}</div>
<div style={{ fontSize: 12, marginTop: 8 }}>
Confianza: {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 }}>
Estabilidad: {data.metrics?...stability ?? '-'}
</div>
<Handle type="source" position={Position.Right} />
</div>
);
}
Página principal
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> Filtraciones </h3>
Sitios, comunicaciones, estados </div>
</aside>
<section style={{ position: 'relative' }}>
<EquilibriumCanvas />
</section>
<aside style={{ borderLeft: '1px solid #1F2633', padding: 16 }}>
{!selectedNode & ' !selectedEdge && <div> Elige un nodo o una conexión </div}
{selectedNode && (
<>
<h3>{selectedNode.data.label}</h3>
Tip: {selectedNode.data.nodeType} </p>
Estado: {selectedNode.data.state} </p>
<pre style={{ whiteSpace: 'pre-wrap' }}>
{JSON.stringify(selectedNode.data.metrics, null, 2)}
</pre>
</>
)}
{selectedEdge && (
<>
<h3>{selectedEdge.data.label}</h3>
Tip: {selectedEdge.data.edgeType} </p>
Estado: {selectedEdge.data.state} </p>
<p>Significado: {selectedEdge.data.meaning || '-'}</p>
<p>Ética: {selectedEdge.data.ethics || '-'}</p>
</>
)}
</aside>
<footer style={{ gridColumn: '1 / 4', borderTop: '1px solid #1F2633', padding: 16 }}>
Timeline / Simulation
</footer>
</main>
);
}
Neo4j
El controlador oficial está conectado a través del Neo4j.driver_...; en la documentación también se recomienda que se indique claramente la base de referencia para la productividad.
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 de lectura del grafo
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 });
}
Crear un nodo
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 });
}
Establecimiento de redes
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-scem
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',
El label: & Desarrollo de competencias,
nodeType: 'meaning',
state: 'growth',
x: 100,
y: 120,
ethics: 0.9
});
CREATE (:EQNode {
id: 'actor-1',
Label: "Cooperación FENIX,"
nodeType: 'actor',
state: 'harmony',
x: 420,
y: 180,
trust: 0.82,
activity: 0.77
});
CREATE (:EQNode {
id: 'effect-1',
El label: "Potencia de empleo,"
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',
El label: " Intención ,"
edgeType: 'intent',
Meaning: "Ejecutar el contorno educativo."
ethics: 'accesibilidad',
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: "Retroalimentación,"
edgeType: 'feedback',
meaning: 'Evaluación del resultado del programa',
metric: "Índice de empleo,"
state: 'harmony',
intensity: 0.7
}]->(b);
Ejemplo JSON para el frente
"nodes": [
{
"id": "meaning-1",
"type": "meaning",
"position": { "x": 100, "y": 120 },
"data": {
"Label": "Desarrollo de competencias,"
"nodeType": "meaning",
"state": "growth",
"metrics": { "ethics": 0.9 }
}
},
{
"id": "actor-1",
"type": "actor",
"position": { "x": 420, "y": 180 },
"data": {
"Label": "Cooperación 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," "Intención,"
"edgeType": "intent",
"Meaning": "El lanzamiento de un contorno educativo,"
"state": "growth",
"intensity": 0.8
}
}
]
}
¿Qué hacemos con los hipérboles de verdad?
Este es un momento importante. React Flow trabaja con los edges normales entre dos nudos. Es su modelo de base como un flowgraph UI. Así que para MVP, haz lo siguiente:
Opción A - práctica
- guardar el hiperrebro como un nodo separado del tipo de proceso o protocolo
- Conectar a los participantes con los edges normales
Es decir:
Meaning -> ProtocolHub -> Actor
Resource -> ProtocolHub
Norm -> ProtocolHub
ProtocolHub -> Effect
Es una decisión justa. No rompe la UI y le permite mostrar una conexión multidimensional.
Tipo para esto.
export type EQNodeType =
| 'meaning'
| 'actor'
| 'process'
| 'resource'
| 'norm'
| 'effect'
| 'protocol';
Y entonces el hiperrrebro a nivel de datos es un protocol-relator con un conjunto de relaciones entrantes o en curso. Es el mejor comienzo.
Primer escenario de trabajo
No te arruines. Haz un guión:
Hipótesis: Escuela Cooperativa
Espejos:
- Significado: desarrollo de competencias
- Entidad: Cooperativa
- Recursos: Fondo
- Norma: Reglamento
- Protocolo: programa educativo
- Efecto: crecimiento del empleo
Relaciones:
- meaning → protocol
- actor → protocol
- resource → protocol
- norm → protocol
- protocol → effect
Este es el MVP que se puede mostrar.
Comandos de lanzamiento
package.json
dependencia
{
"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"
}
}
El paquete @xyflow/react ° es un nombre pertinente para React Flow, y Neo4j JS driver está en la rama 6.x.
.env.local
NEO4J_URI=neo4j://localhost
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your_password
NEO4J_DATABASE=neo4j
¿Qué tendrás después de eso?
Una vez que se ponga en marcha, tendrás:
- grafo interactivo,
- Nodos de casta,
- Detective Inspector de la derecha,
- cargando datos de Neo4j,
- Creación de nodos y conexiones a través de API;
- La base del guión, diagnóstico y simulación.
Ya no es un concepto. Es un marco de productos vivo.
A continuación le doy el marco del sistema que ya se ha reunido: qué es cada módulo, qué es lo que necesitan los nudos de casta, cómo construir un conector interactivo, y el código que puede colocarse inmediatamente en MVP. Para React Flow, el paquete actual @xyflow/react; los nudos de casta se hacen como componentes de React, se transmiten a través de nodeTypes y los puntos de conexión se establecen a través de <Handle />. Para Neo4j, ahora es normal utilizar un controlador oficial JS y driver.executeQuery, con parámetros e indicaciones claras de la base.
Definiciones de los módulos del sistema
Recomendamos que se registren 10 módulos para la primera versión de trabajo.
Graph Core
El corazón del sistema. Guarda los nodos, las comunicaciones, la selección del usuario, los modos, los filtros, layout y los eventos de la interfaz. Lo que hace:
- mantiene el estado actual del grafo
- Actualiza los nodos y las comunicaciones
- informa UI que está seleccionado
- activa el nuevo dibujo
Node Registry
Registro de tipos de nodos. Lo que hace:
- vincula node.type → componente de React
- Prepara cómo reventar meaning, actor, resource, protocol, etc.
- permite ampliar el sistema sin reescribir el lienzo
Edge Registry. Registro de comunicaciones. Lo que hace:
- define el visual y el comportamiento de las relaciones
- crea estilos para intent, cooperation, feedback, evolution
- Más adelante puede incluir costillas de casta con firmas e indicadores de riesgo
Protocol Engine
Este es el módulo clave EQUILIBRIUM. Lo que hace:
- Recoge un efecto multidimensional de varias cosas
- En realidad, aplica el hiperrrebro como un protocolo separado.
- Conecta el sentido, los actores, los recursos, las normas y los efectos en un solo contorno
Para MVP es la mejor manera, porque el React Flow por naturaleza funciona con costillas normales entre dos nudos; es más fácil imaginar una conexión multidimensional como un nodo de hab.
Inspector Panel. El panel derecho. Lo que hace:
- muestra el pasaporte del nodo seleccionado
- muestra el pasaporte de la conexión seleccionada
- permite editar atributos
- Muestra métricas, riesgos, ética, carga
Filter & Views. El panel izquierdo. Lo que hace:
- filtra por tipo de nodo
- filtra en posición
- filtra por tipo de conexión
- cambia los regímenes: visión, diagnóstico, guión, acción
Timeline & Events. El panel inferior. Lo que hace:
- Muestra eventos en el tiempo
- permite reproducir el estado del grafo
- Simulación de escenarios en el futuro
Diagnostics Engine. Diagnóstico del sistema. Lo que hace:
- Busca nudos sobrecargados
- Encuentra relaciones de conflicto.
- Pone de relieve las zonas de crisis
- Calcula los agujeros en el contorno: por ejemplo, hay un proceso, pero no una norma.
Persistence Layer. La capa de almacenamiento... Lo que hace:
- lee y escribe el grafo en Neo4j
- guarda las posiciones de los nodos
- guarda métricas y estados
- entrega JSON al frente.
AI/Rule Layer. Una serie de recomendaciones. Lo que hace:
- ofrece nuevos vínculos
- Advertencia sobre los riesgos
- Piensa en los euristicos simples.
- explica por qué el sistema marca el nodo como una crisis
Tipos de nodos del sistema
No se necesitan 30 tipos para empezar. Necesito 7.
export type EQNodeType =
'meaning' / / Significado
○ 'actor' / / sujeto
* Resource / / recurso
○ 'norm' // norma
* 'effect' / / effect
○ 'protocol' // hiperrebro como nudo hab
○ 'metric'; / / metric
Meaning. El propósito, la misión, el valor. Responde a la pregunta: ¿Por qué?
Actor. Hombre, organización, agente, cooperativa. Responde a la pregunta:
Resource. Dinero, fondos, datos, energía, tiempo. Responde a la pregunta: ¿De qué se trata?
Norma. Reglamento, regla, contrato, ley. Responde a la pregunta: ¿cuáles son las restricciones?
Effect result. Responde a la pregunta: ¿Qué ha pasado?
Protocol. Módulo de trabajo central. Esta es la aplicación de una conexión multidimensional. Reúne a las otras criaturas alrededor de él.
Metric. Medidor de resultados: índice de confianza, índice de sostenibilidad, índice de empleo.
Tipos de vínculos
export type EQEdgeType =
| 'intent'
| 'input'
| 'constraint'
| 'output'
| 'feedback'
| 'evolution';
Significado:
- Intent ○ El sentido está en el protocolo
- input ○ recurso o actor participa en el protocolo
- la norma limita el protocolo
- output ○ El protocolo produce efecto
- El efecto feedback es un efecto de retorno.
- evolution ○ cambios en el tiempo
Tipos de datos básicos
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[];
}
Nodos de casco: descripción
En React Flow, el nudo de la casta es un simple React-componente que recibe NodeProps. Se puede hacer con varios Handle, controles interactivos y su propia UI. Es un modelo que se apoya oficialmente. Abajo, un conjunto de nodos que ya son como un sistema.
MeaningNode. Un nudo de palabras. Por lo general, una pequeña, brillante, con una entrada y una salida.
ActorNode. El núcleo del sudes. Muestra confianza, actividad y presión.
ResourceNode. Un nudo de recursos. Generalmente compacta, con un índice de accesibilidad.
Norma Node. Un nodo normativo. Muestra el tipo de restricción.
EffectNode. Un punto de resultado. Muestra influencia y resistencia.
MetricNode. Un centro de medición. Se utiliza como indicador de estimación.
ProtocolNode. Lo más importante. Hub Central. Tiene varios puntos de conexión:
- a la izquierda
- De arriba a arriba de los actores
- recursos de abajo abajo
- efectos a la derecha
- Handle para las normas
- Handle para metric
Contenedor de base común
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>
Umbrales: código
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="El significado"
glow="0 0 24px rgba(123, 97, 255, 0.24)"
>
<div style={{ fontSize: 12, opacity: 0.85 }}>
Ética: {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= "Sujeto"
glow="0 0 24px rgba(77, 163, 255, 0.20)"
>
<div style={{{fontSize: 12 }}>Confianza: {data.metrics?.trust ?? '-}}>
{div style={{{fontSize: 12 }}>Emergencia: {data.metrics?.activity ??? '-'}} </div>
{div style={{{fontSize: 12 }}}Gaso: {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=Resurs
glow="0 0 24px rgba(255, 200, 87, 0.18)"
>
<div style={{{fontSize: 12 }}>Acceso: {data.metrics?.value ??? '-'}}}/div>
<div style={{{fontSize: 12 }}>Estabilidad: {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 || 'Regla / restricción'}</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 ? '-} </div>
<div style={{{fontSize: 12 }}}>Sostenibilidad: {data.metrics?.stability ?? '-'}} </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=Metrical
glow="0 0 24px rgba(80, 180, 255, 0.16)"
>
<div style={{{fontSize: 12 }}}Cignidad: {data.metrics?.value ?? '-'}}}/div>
<Handle type="target" position={Position.Left} id="in" />
<Handle type="source" position={Position.Right} id="out" />
</NodeShell>
);
ProtocolNode.tsx. Es el nudo principal.
'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 }}>
Protocolo / Hiperrebro
</div>
<div style={{ fontSize: 16, fontWeight: 800 }}>{data.label}</div>
<div style={{ marginTop: 10, fontSize: 12, opacity: 0.9 }}>
{data.description || 'Sistema multidimensional de acción'}
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 6,
marginTop: 10,
fontSize: 12,
}}
>
Etica: {data.metrics?.ethics ?? '-'}} </div>
Cargando: {data.metrics?.load ?? '-'}} </div>
<div>Estabilidad: {data.metrics?.stability ?? '-'}} </div>
Activo: {data.metrics?.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>
);
El eje de varios Handle coincide plenamente con el modelo React Flow, donde los nudos de casta pueden tener el mayor número posible de puntos de connection source/target.
Registro de nudos de castas
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,
};
Columna interactiva: código de lienzo
<ReactFlow />El componente principal que renderiza los nudos y las costillas, procesa las interacciones y trabaja con el estado. Es el punto central oficial de la biblioteca.
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: {
El label: " Nueva comunicación ,"
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>
);
}
Ejemplo de pantalla interactiva
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> Filtraciones </h3>
Títulos de los nodos </div>
<div>Estados</div>
Tipos de comunicación </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' }}>
{!selectedNode & & & !selectedEdge && <div> Seleccione un nodo o una conexión </div>}
{selectedNode && (
<>
<h3>{selectedNode.data.label}</h3>
<div> Tip: {selectedNode.data.nodeType} </div>
Estado: {selectedNode.data.state} </div>
<div style={{ marginTop: 12, fontSize: 13, opacity: 0.9 }}>
{selectedNode.data.description || 'Sin descripción'}
</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> Tip: {selectedEdge.data.edgeType} </div>
Estado: {selectedEdge.data.state} </div>
<div>Significado: {selectedEdge.data.meaning || '-'}</div>
<div>Risk: {selectedEdge.data.risk ?? '-'}} <>
Intensidad: {selectedEdge.data.intensity ? '-'} <}}>
</>
)}
</aside>
<footer
style={{
gridColumn: '1 / 4',
borderTop: '1px solid #1F2633',
padding: 16,
}}
>
Timeline / Events / Simulation
</footer>
</main>
);
}
Ejemplo de barra para EQUILIBRIUM. No es una abstracción, es un guión de trabajo.
lib/mockGraph.ts
import { EQGraph } from '@/types/equilibrium';
export const mockGraph: EQGraph = {
nodes: [
{
id: 'meaning-1',
type: 'meaning',
position: { x: 60, y: 220 },
data: {
El label: & Desarrollo de competencias,
nodeType: 'meaning',
description: "Rostro del potencial humano de la región,"
state: 'growth',
metrics: { ethics: 0.92 },
},
},
{
id: 'actor-1',
type: 'actor',
position: { x: 280, y: 60 },
data: {
Label: "Cooperación 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: "Fondation d'economique,"
nodeType: 'resource',
state: 'harmony',
metrics: { value: 0.76, stability: 0.66 },
},
},
{
id: 'norm-1',
type: 'norm',
position: { x: 280, y: 500 },
data: {
El label: " Regla del programa ,"
nodeType: 'norm',
description: "La transparencia de la selección y la accesibilidad de la participación,"
state: 'harmony',
metrics: {},
},
},
{
id: 'protocol-1',
type: 'protocol',
position: { x: 640, y: 230 },
data: {
El label: "Protocolo de la formación cooperativa,"
nodeType: 'protocol',
description: 'Contorno: aprendizaje de participación de empleo',
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: {
El label: "Potencia de empleo,"
nodeType: 'effect',
state: 'growth',
metrics: { influence: 0.74, stability: 0.69 },
},
},
{
id: 'metric-1',
type: 'metric',
position: { x: 1020, y: 340 },
data: {
Label: 'Índice de empleo',
nodeType: 'metric',
state: 'harmony',
metrics: { value: 0.63 },
},
},
],
edges: [
{
id: 'e1',
source: 'meaning-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
El label: " Intención ,"
edgeType: 'intent',
state: 'growth',
Meaning: "El punto está en el protocolo."
intensity: 0.88,
},
},
{
id: 'e2',
source: 'actor-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
label: 'Participación',
edgeType: 'input',
state: 'harmony',
Meaning: "El sudes está cumpliendo el protocolo,"
intensity: 0.72,
},
},
{
id: 'e3',
source: 'resource-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
label: 'Resurs',
edgeType: 'input',
state: 'harmony',
intensity: 0.68,
},
},
{
id: 'e4',
source: 'norm-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
El label: " Limitación,"
edgeType: 'constraint',
state: 'harmony',
meaning: "La norma limita y guía,"
},
},
{
id: 'e5',
source: 'protocol-1',
target: 'effect-1',
type: 'smoothstep',
data: {
label: 'Resultat',
edgeType: 'output',
state: 'growth',
intensity: 0.81,
},
},
{
id: 'e6',
source: 'protocol-1',
target: 'metric-1',
type: 'smoothstep',
data: {
El label: " Medición ,"
edgeType: 'output',
state: 'harmony',
},
},
{
id: 'e7',
source: 'effect-1',
target: 'protocol-1',
type: 'smoothstep',
data: {
label: "Retroalimentación,"
edgeType: 'feedback',
state: 'harmony',
risk: 0.21,
},
},
],
};
Neo4j: plan de almacenamiento
La JS oficial driver Neo4j recomienda que se utilicen las consultas predeterminadas, mientras que el executeQuery es el que crea la transacción. También es útil siempre indicar claramente la base de datos para reducir el exceso de 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 });
}
Diagnóstico: reglas básicas. No es una I.I. es una lógica dura. Tienes que empezar con ella.
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: "El protocolo no tiene ningún sentido,"
});
}
if (!hasActor) {
result.push({
type: 'warning',
targetId: node.id,
message: "El protocolo no tiene entidad de ejecución,"
});
}
if (!hasResource) {
result.push({
type: 'warning',
targetId: node.id,
Message: "El protocolo no tiene entrada de recursos,"
});
}
if (!hasNorm) {
result.push({
type: 'warning',
targetId: node.id,
message: "El protocolo no tiene límites normativos,"
});
}
if (!hasOutput) {
result.push({
type: 'error',
targetId: node.id,
message: "El protocolo no tiene resultado,"
});
}
if ((node.data.metrics.load ?? 0) > 0.85) {
result.push({
type: 'warning',
targetId: node.id,
message: 'Protocolo sobrecargado',
});
}
}
}
return result;
}
¿Qué es el gráfico interactivo de la página web?
No solo un lienzo de nudos, sino 7 funciones:
- Drag
Mueve los nodos. - Connect
Crear una nueva conexión a través de las manos. - Select
Seleccione un nudo o una conexión. - Inspect
Muestra el contenido a la derecha. - Filter
Dejar en pantalla los tipos correctos. - Diagnose
Resaltar errores y riesgos. - Evolve
Añadir nuevo protocolo, recurso, norma, metrología.
React Flow es precisamente para editores node-basados y gráficos interactivos; la documentación describe por separado la castamación de nudos, handles, theming y ejemplos.
¿Qué más hay que añadir a la versión 2?
No hay decoraciones. Se necesitan los siguientes módulos:
- LeftFilters.tsx
- RightInspector.tsx
- BottomTimeline.tsx
- CreateProtocolModal.tsx
- DiagnosticsPanel.tsx
Y las siguientes mejoras:
- Mantener las posiciones de los nodos en el BD
- forma de creación del protocolo
- edición de metric
- layout algoritmo
- Iluminación de los vínculos de conflicto
- SSR-safe configuración si hay un render de servidores; en React Flow para SSR hay un NodeHandle por separado cuando no se ha medido DOM.
Cómo conseguir esto en MVP
Vidrio mínimo:
- Next.js
- TypeScript
- @xyflow/react
- Neo4j
- Zustand
Aspectos mínimos:
- 7 tipos de nudos
- 6 tipos de conexiones
- 1
- 1 Panel derecho
- 1
- 1
Hipótesis mínima
- Significado
- sudes
- Recursos
- norma
- Protocolo
- efecto
- metrica
Esto va a parecer un verdadero panel de operaciones, no un esquema para la presentación.
La conclusión arquitectónica más importante
El hiperrrebro de EQUILIBRIUM no tiene por qué tratar de dibujar por la fuerza como superlinea mágica.
Para una interfaz de trabajo, tienes que hacerlo como un protocol-relator que reúne a todos los participantes en el proceso.
Esta decisión es la siguiente:
- No rompe React Flow,
- Se encuentra en Neo4j,
- Se diagnostica fácilmente,
- se expande fácilmente,
- Se muestra bien en la UI.
Y es la manera más saludable de hacer que el sistema sea real.
A continuación, es lógico reunir un conjunto completo de archivos del proyecto: package.json, tsconfig, app/page.tsx, todos los componentes node, API rowts, seed para Neo4j y CSS, para que pueda simplemente insertarse en el repositorio y lanzarlo.