solved node input output issue

This commit is contained in:
Lucio Lelii 2026-01-28 17:19:57 +01:00
parent 2154b51096
commit ee4505bfea
13 changed files with 329 additions and 62 deletions

BIN
public/llm_node.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -2,9 +2,7 @@ import { GetSchemes, ClassicPreset } from "rete";
import { INodeModel } from "./flow";
export type HFNode = ClassicPreset.Node & {
data?: {
value: INodeModel;
};
data: INodeModel;
};
export type HFConnection = ClassicPreset.Connection<HFNode, HFNode>;

View File

@ -1,21 +1,17 @@
<div class="rounded-xl bg-white shadow-md border border-gray-200 cursor-pointer">
<div class="p-3 flex items-center justify-between gap-2">
<p class="text-sm font-semibold text-gray-700">
{{ data.label }}
</p>
<div class="relative rounded-xl bg-white shadow-md border border-gray-200 overflow-visible">
<!-- header/row relativa -->
<div class="relative w-full p-3 flex items-center justify-between">
<p class="text-sm font-semibold text-gray-700">{{ data.label }}</p>
<div class="flex justify-end">
<div
refComponent
[data]="{
type: 'socket',
side: 'output',
key: outputKey,
nodeId: data.id,
payload: outputSocket
}"
[emit]="emit"
></div>
</div>
<div refComponent
[data]="{
type: 'socket',
side: 'output',
key: outputKey,
nodeId: data.id,
payload: outputSocket
}"
[emit]="emit"
></div>
</div>
</div>
</div>

View File

@ -2,11 +2,9 @@ import {
Component,
Input,
HostBinding,
ChangeDetectorRef,
} from "@angular/core";
import { CommonModule, KeyValue } from "@angular/common";
import { ReteModule } from "rete-angular-plugin/21";
import { HFNode } from "@models/nodes";
import { ClassicPreset } from "rete";
@Component({
@ -22,7 +20,6 @@ export class InputNodeComponent {
@Input() emit!: (data: any) => void;
@Input() rendered!: () => void;
seed = 0;
@HostBinding("class.selected") get selected() {
return this.data.selected;
@ -35,6 +32,8 @@ export class InputNodeComponent {
const entries = Object.entries(this.data.outputs);
const [key, output] = entries[0];
console.log("InputNodeComponent ngOnInit", key, output);
this.outputKey = key;
this.outputSocket = (output as any).socket;
}

View File

@ -0,0 +1,80 @@
/* Nodo base */
.node {
width: 220px;
border-radius: 14px;
background: #ffffff;
border: 1px solid #e5e7eb; /* gray-200 */
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
overflow: hidden;
}
/* Header */
.header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: linear-gradient(135deg, #7c3aed, #6366f1); /* violet → indigo */
color: white;
}
/* Icona LLM */
.icon {
width: 28px;
height: 28px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
}
.icon img {
width: 18px;
height: 18px;
}
/* Titolo */
.title {
font-size: 14px;
font-weight: 600;
white-space: nowrap;
}
/* Body */
.body {
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
/* Riga input/output */
.io {
display: flex;
align-items: center;
justify-content: space-between;
}
/* Label IO */
.label {
font-size: 12px;
color: #374151; /* gray-700 */
}
/* Socket */
.socket {
width: 12px;
height: 12px;
border-radius: 999px;
background: #6366f1;
border: 2px solid white;
box-shadow: 0 0 0 1px #6366f1;
cursor: pointer;
}
/* Hover */
.socket:hover {
transform: scale(1.15);
}

View File

@ -0,0 +1,46 @@
<div class="relative w-55 rounded-2xl bg-white border border-gray-200 shadow-lg overflow-visible font-sans">
<!-- Header -->
<div
class=" cursor-move flex items-center gap-2 px-3 py-2 bg-linear-to-br from-violet-600 to-indigo-500 text-white rounded-t-2xl">
<div class="w-12 h-8 rounded-lg bg-white/20 flex items-center justify-center">
<img src="llm_node.png" alt="LLM" class="w-12 h-8" />
</div>
<span class="text-sm font-semibold truncate">LLM Node</span>
</div>
<!-- Body -->
<div class="p-3 grid grid-cols-2 gap-4">
<div class="grid grid-rows-2 gap-1">
<div refComponent class="absolute left-1"
[data]="{
type: 'socket',
side: 'input',
key: inputKey,
nodeId: data.id,
payload: inputSocket }" [emit]="emit"></div>
<div class="text-xs text-gray-700 pl-4">
{{ inputKey }}
</div>
</div>
<div class="grid grid-rows-2 gap-1">
<div class="text-xs text-gray-700 text-right pr-4">
{{ outputKey }}
</div>
<div refComponent class="absolute right-1"
[data]="{
type: 'socket',
side: 'output',
key: outputKey,
nodeId: data.id,
payload: outputSocket}" [emit]="emit"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Llm } from './llm';
describe('Llm', () => {
let component: Llm;
let fixture: ComponentFixture<Llm>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Llm]
})
.compileComponents();
fixture = TestBed.createComponent(Llm);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,50 @@
import { CommonModule } from '@angular/common';
import { Component, HostBinding, Input } from '@angular/core';
import { ClassicPreset } from 'rete';
import { ReteModule } from 'rete-angular-plugin/21';
@Component({
selector: 'app-llm',
imports: [CommonModule, ReteModule],
templateUrl: './llm.html',
styleUrl: './llm.css',
host: {
"data-testid": "node"
}
})
export class LLMNodeComponent {
@Input() data!: any;
@Input() emit!: (data: any) => void;
@Input() rendered!: () => void;
@HostBinding("class.selected") get selected() {
return this.data.selected;
}
outputKey!: string;
outputSocket!: ClassicPreset.Socket;
inputKey!: string;
inputSocket!: ClassicPreset.Socket;
ngOnInit() {
const outputEntries = Object.entries(this.data.outputs);
const [outKey, output] = outputEntries[0];
this.outputKey = outKey;
this.outputSocket = (output as any).socket;
const inputEntries = Object.entries(this.data.inputs);
const [inKey, input] = inputEntries[0];
this.inputKey = inKey;
this.inputSocket = (input as any).socket;
}
ngAfterViewInit() {
this.rendered();
}
}

View File

@ -1,20 +1,15 @@
<div class="rounded-xl bg-white shadow-md border border-gray-200 cursor-pointer">
<div class="p-3 flex items-center justify-between gap-2">
<div class="flex justify-start">
<div
refComponent
[data]="{
<div class="relative rounded-xl bg-white shadow-md border border-gray-200 overflow-visible">
<!-- header/row relativa -->
<div class="relative p-3 flex items-center justify-between overflow-visible">
<div refComponent [data]="{
type: 'socket',
side: 'input',
key: inputKey,
nodeId: data.id,
payload: inputSocket
}"
[emit]="emit"
></div>
</div>
<p class="text-sm font-semibold text-gray-700">
{{ data.label }}
</p>
}" [emit]="emit"></div>
<p class="text-sm font-semibold text-gray-700">{{ data.label }}</p>
</div>
</div>
</div>

View File

@ -2,13 +2,10 @@ import {
Component,
Input,
HostBinding,
ChangeDetectorRef,
OnChanges
} from "@angular/core";
import { ClassicPreset } from "rete";
import { CommonModule, KeyValue } from "@angular/common";
import { CommonModule } from "@angular/common";
import { ReteModule } from "rete-angular-plugin/21";
import { HFNode } from "@models/nodes";
@Component({
templateUrl: "./output-node.component.html",
@ -23,8 +20,6 @@ export class OutputNodeComponent {
@Input() emit!: (data: any) => void;
@Input() rendered!: () => void;
seed = 0;
@HostBinding("class.selected") get selected() {
return this.data.selected;
}
@ -40,6 +35,8 @@ export class OutputNodeComponent {
const entries = Object.entries(this.data.inputs);
const [key, input] = entries[0];
console.log("OutputNodeComponent ngOnInit", key, input);
this.inputKey = key;
this.inputSocket = (input as any).socket;

View File

@ -0,0 +1,35 @@
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
import { ReteModule } from "rete-angular-plugin/21";
@Component({
standalone: true,
imports: [ReteModule],
host: {
refComponent: '',
class: `
cursor-pointer relative -left-1 w-[15px] h-[15px] flex items-center justify-center rounded-full bg-green-500 border-2 border-white
shadow-[0_0_0_1px_rgb(34,197,94)]
`
},
template: ``
})
export class InputSocket {
@Input() data!: any;
@Input() emit!: (data: any) => void;
@Input() rendered!: () => void;
@HostBinding("title") get title() {
return this.data?.name ?? "";
}
ngOnInit(): void {
console.log("InputSocket ngOnInit", this.data);
}
ngAfterViewInit() {
requestAnimationFrame(() => {
this.rendered?.();
});
}
}

View File

@ -0,0 +1,34 @@
import { Component, ElementRef, HostBinding, Input } from '@angular/core';
import { ReteModule } from 'rete-angular-plugin/21';
@Component({
standalone: true,
imports: [ReteModule],
host: {
refComponent: '',
class: `
cursor-pointer relative -right-1 w-[15px] h-[15px] flex items-center justify-center rounded-full bg-indigo-500 border-2 border-white
shadow-[0_0_0_1px_rgb(99,102,241)]
`
},
template: ``
})
export class OutputSocket {
@Input() data!: any;
@Input() emit!: (data: any) => void;
@Input() rendered!: () => void;
@HostBinding("title") get title() {
return this.data?.name ?? "";
}
ngOnInit(): void {
console.log("OutputSocket ngOnInit", this.data);
}
ngAfterViewInit() {
requestAnimationFrame(() => {
this.rendered?.();
});
}
}

View File

@ -1,4 +1,4 @@
import { Injector } from "@angular/core";
import { Injector, Input } from "@angular/core";
import { NodeEditor, GetSchemes, ClassicPreset } from "rete";
import { AreaPlugin, AreaExtensions } from "rete-area-plugin";
import {
@ -8,8 +8,11 @@ import {
import { AngularPlugin, Presets, AngularArea2D, NodeComponent } from "rete-angular-plugin/21";
import { InputNodeComponent } from "@shared/nodes/input/input-node.component";
import { OutputNodeComponent } from "@shared/nodes/output/output-node.component";
import { HFSchemes } from "@models/nodes";
import { FlowData } from "@models/flow";
import { HFNode, HFSchemes } from "@models/nodes";
import { FlowData, INodeModel } from "@models/flow";
import { LLMNodeComponent } from "@shared/nodes/llm/llm";
import { OutputSocket } from "@shared/sockets/output/output";
import { InputSocket } from "@shared/sockets/input/input";
type Schemes = GetSchemes<
ClassicPreset.Node,
@ -19,8 +22,8 @@ type AreaExtra = AngularArea2D<Schemes>;
export async function createEditor(container: HTMLElement, injector: Injector, flowData: FlowData) {
const editor = new NodeEditor<Schemes>();
const area = new AreaPlugin<Schemes, AreaExtra>(container);
const editor = new NodeEditor<HFSchemes>();
const area = new AreaPlugin<HFSchemes, AreaExtra>(container);
const connection = new ConnectionPlugin<Schemes, AreaExtra>();
const render = new AngularPlugin<Schemes, AreaExtra>({ injector });
@ -38,11 +41,13 @@ export async function createEditor(container: HTMLElement, injector: Injector, f
if (context.payload.label === "Output") {
return OutputNodeComponent;
}
return NodeComponent;
return LLMNodeComponent;
},
socket(context) {
const side = context.side;
return side === "input" ? InputSocket : OutputSocket;
}
},
})
);
@ -63,15 +68,17 @@ export async function createEditor(container: HTMLElement, injector: Injector, f
if (flowData)
await loadFlowData(editor, flowData);
AreaExtensions.zoomAt(area, editor.getNodes());
return editor;
}
export function exportGraph(editor: NodeEditor<HFSchemes>) {
const nodes = editor.getNodes().map(schema => ({
const nodes: INodeModel[] = editor.getNodes().map(node => ({
key: node.data.key,
name: node.data.name,
position: node.data.position,
nodeDefinition: node.data.nodeDefinition,
parameters: node.data.parameters,
}));
@ -99,7 +106,9 @@ async function loadFlowData(editor: NodeEditor<HFSchemes>, flowData: FlowData) {
const socket = new ClassicPreset.Socket("socket");
const nodeMapping = new Map<string, string>();
for (const nodeData of flowData.nodes) {
const node = new ClassicPreset.Node(nodeData.nodeDefinition?.category || "Undefined");
if (!nodeData.nodeDefinition) continue;
const node: HFNode = new ClassicPreset.Node(nodeData.nodeDefinition.category) as HFNode;
node.data = nodeData;
nodeMapping.set(nodeData.key, node.id);
Object.entries(nodeData.nodeDefinition?.outputs).forEach(([output, def]) => {
console.log("Adding output", output);
@ -123,9 +132,14 @@ async function loadFlowData(editor: NodeEditor<HFSchemes>, flowData: FlowData) {
area.translate(node.id, { x: nodeData.position.x, y: nodeData.position.y });*/
}
for (const connections of flowData.connections) { {
const targetNode = editor.getNode(nodeMapping.get(connections.targetNode) || "");
const node = editor.getNode(nodeMapping.get(connections.sourceNode) || "");
for (const connections of flowData.connections) {
{
if (!nodeMapping.has(connections.sourceNode) || !nodeMapping.has(connections.targetNode)) {
console.warn("Cannot create connection, node not found", connections);
continue;
}
const targetNode = editor.getNode(nodeMapping.get(connections.targetNode)!) as HFNode;
const node = editor.getNode(nodeMapping.get(connections.sourceNode)!) as HFNode;
if (node && targetNode) {
console.log("Creating connection from", node.id, "to", targetNode.id);
const connection = new ClassicPreset.Connection(