update for blocks

This commit is contained in:
Lucio Lelii 2026-02-27 10:14:12 +01:00
parent 7b36044c33
commit 25edbbd34c
14 changed files with 420 additions and 444 deletions

View File

@ -1,4 +1,4 @@
import { INodeDefinitionType } from "./node-types";
export type FlowVisibility = 'PUBLIC' | 'PRIVATE';
@ -14,16 +14,66 @@ export type Flow = {
};
export type FlowData = {
nodes: INodeModel[];
connections: IConnectionModel[];
}
blocks: FlowBlock[];
connections: FlowBlockConnection[];
};
export type FlowBlock = {
id: string;
sink: boolean;
name: string;
position?: { x: number, y: number };
inputs: FlowPort[];
outputs: FlowPort[];
specificConfiguration: FlowBlockConfiguration;
typeName: "LLMBlock" | "HumanInteractionBlock" | string;
};
export type FlowPort = {
name: string;
type: string;
multiple: boolean;
};
export type FlowBlockConnection = {
id: string;
sourceId: string;
sourceName: string;
targetId: string;
targetName: string;
};
export type FlowBlockConfiguration =
| LLMBlockConfiguration
| HumanInteractiveBlockConfiguration
| Record<string, unknown>;
export type LLMDescriptor = {
provider: string;
model: string;
};
export type LLMBlockConfiguration = {
type: "LLMBlockConfiguration";
name: string;
llmDescriptor: LLMDescriptor;
prompt: string;
};
export type HumanInteractiveBlockConfiguration = {
type: "HumanInteractiveBlockConfiguration";
name: string;
actionDescription: string;
llmDescriptor: LLMDescriptor;
inputAsList: boolean;
outputAsList: boolean;
};
export type INodeModel = {
key: string;
name?: string;
position: { x: number, y: number } | null;
parameters?: Record<string, any> | null;
nodeDefinition: INodeDefinitionType;
}
export type IConnectionModel = {
@ -34,4 +84,3 @@ export type IConnectionModel = {
targetField: string;
}

View File

@ -1,102 +0,0 @@
export interface INodeDefinitionType {
name: string;
category: string;
outputs: Record<string, IOObject>;
inputs: Record<string, IOObject>;
fixedParameters?: Record<string, IParameter> | null;
runtimeParameters?: Record<string, IParameter> | null;
}
export interface IExecutionNodeType extends INodeDefinitionType {
executor: string;
outputMappers?: Record<string, IMapper>;
inputMappers?: Record<string, IMapper>;
}
export interface IInputNodeType extends INodeDefinitionType {
simulabe: boolean;
}
export interface IHumanInteractionNodeType extends INodeDefinitionType {
simulabe: boolean;
interactionDescription: string;
}
export interface IIOutputNodeType extends INodeDefinitionType {
}
export interface IMapper {
type: string;
}
export interface ITraslatorMapper extends IMapper {
translation: string;
}
export interface IDirectMapper extends IMapper {
fieldName: string;
}
export interface IParserMapper extends IMapper {
from?: string;
to?: string;
fieldName: string;
multiSeparator?: string;
}
export interface IParameter {
name: string;
label: string;
description: string;
type: ParameterType;
required: boolean;
validations?: IValidation[];
specificAttributes?: any;
};
export enum ParameterType {
Text = 'Text',
LongText = 'LongText',
Select = 'Select',
DynamicMultiOptions = 'DynamicMultiOptions',
Number = 'Number',
Boolean = 'Boolean'
}
export interface IExecutor {
identifier: string,
name: string,
description: string,
inputs: Record<string, IOObject>,
outputs: Record<string, IOObject>,
mandatoryParameters: IParameter[],
}
export enum IOType {
TEXT = 'TEXT',
//CSV = 'CSV',
}
export interface IOObject {
type: IOType;
multiple: boolean;
}
export interface IValidation {
validator: string;
value?: number | string;
message: string;
}
export interface IFlowError {
type: string;
node: string | undefined;
connection: string | undefined;
input: string | undefined;
output: string | undefined;
parameter: string | undefined;
}

View File

@ -1,10 +1,10 @@
import { GetSchemes, ClassicPreset } from "rete";
import { INodeModel } from "./flow";
import { FlowBlock } from "./flow";
export type HFNode = ClassicPreset.Node & {
data: INodeModel;
data?: FlowBlock;
};
export type HFConnection = ClassicPreset.Connection<HFNode, HFNode>;
export type HFSchemes = GetSchemes<HFNode, HFConnection>;
export type HFSchemes = GetSchemes<HFNode, HFConnection>;

View File

@ -1,100 +1,62 @@
export const flowTest = {
"id": "testFlow",
"createdBy": "lucio.lelii",
"name": "Test Flow",
"description": "This is a test flow",
"nodes": [
{
"key": "1328196f-5ddd-43b0-a702-2d1376970c70",
"name": "Input Node",
"position": null,
"parameters": null,
"nodeDefinition": {
"category": "Input",
"runtimeParameters": null,
"fixedParameters": null,
"inputs": {},
"outputs": {
"phrase": {
"type": "TEXT",
"multiple": false
}
},
"simulable": false,
"name": "Phrase"
}
},
{
"key": "f142511e-f410-4974-9354-3a1e056e056f",
"name": "translator to french",
"position": null,
"parameters": {
"LLMProvider": "OllamaTestProvider",
"LLMModel": "sam860/gemma3:270m"
},
"nodeDefinition": {
"category": "Execution",
"executor": "genericAIExecutorTest",
"inputMappers": {
"prompt": {
"type": "translator",
"translation": "translate the following phrase : \"${{phrase}}\" in french, return only the translation"
}
},
"outputMappers": {
"translated": {
"type": "direct",
"fieldName": "response"
}
},
"name": "French Translator",
"runtimeParameters": null,
"fixedParameters": null,
"inputs": {
"phrase": {
"type": "TEXT",
"multiple": false
}
},
"outputs": {
"translated": {
"type": "TEXT",
"multiple": false
}
}
}
},
{
"key": "e3488c96-b55b-4763-bc25-54d3d3755d85",
"name": "Output Node",
"position": null,
"parameters": null,
"nodeDefinition": {
"category": "Output",
"runtimeParameters": null,
"fixedParameters": null,
"outputs": {},
"inputs": {
"translated": {
"type": "TEXT",
"multiple": false
}
},
"name": "Translated Phrase"
}
}
],
"connections": [
{
"key": "18739497-51e6-4d09-be40-398c3601a326",
"from": "1328196f-5ddd-43b0-a702-2d1376970c70:phrase",
"to": "f142511e-f410-4974-9354-3a1e056e056f:phrase"
},
{
"key": "f88d9442-4a9f-4b4b-ac3f-9794dd0bf6db",
"from": "f142511e-f410-4974-9354-3a1e056e056f:translated",
"to": "e3488c96-b55b-4763-bc25-54d3d3755d85:translated"
}
],
"public": false
export const flowTest = {
"name" : "Test Flow",
"description" : "This is a test flow",
"blocks" : [ {
"id" : "cabd6f4e-5a05-41f8-9bf7-4de20391ac4e",
"sink" : false,
"name" : "first",
"inputs" : [ {
"name" : "name",
"type" : "TEXT",
"multiple" : false
} ],
"outputs" : [ {
"name" : "response",
"type" : "TEXT",
"multiple" : false
} ],
"specificConfiguration" : {
"type" : "LLMBlockConfiguration",
"name" : "first",
"llmDescriptor" : {
"provider" : "testProvider",
"model" : "testModel"
},
"prompt" : "Make a question about ${{name}}"
},
"typeName" : "LLMBlock"
}, {
"id" : "0063a3ec-3863-4045-bd3b-61eaf87b4604",
"sink" : true,
"name" : "interactive",
"inputs" : [ {
"name" : "input",
"type" : "TEXT",
"multiple" : false
} ],
"outputs" : [ {
"name" : "output",
"type" : "TEXT",
"multiple" : false
} ],
"specificConfiguration" : {
"type" : "HumanInteractiveBlockConfiguration",
"name" : "interactive",
"actionDescription" : "Answer the question in input",
"llmDescriptor" : {
"provider" : "testProvider",
"model" : "testModel"
},
"inputAsList" : false,
"outputAsList" : false
},
"typeName" : "HumanInteractionBlock"
} ],
"connections" : [ {
"id" : "8da696c2-dc03-4717-b2ce-18637ae6f7f8",
"sourceId" : "cabd6f4e-5a05-41f8-9bf7-4de20391ac4e",
"sourceName" : "response",
"targetId" : "0063a3ec-3863-4045-bd3b-61eaf87b4604",
"targetName" : "input"
} ]
};

View File

@ -9,8 +9,6 @@ import { take, tap } from 'rxjs';
})
export class Authorization {
private readonly apiUrl = environment.apiUrl + '/auth';
private user = signal<User | null>(null);
authCall: AuthorizationCallServiceBase = new environment.authorizationCallService();

View File

@ -3,7 +3,6 @@ import { FlowsCallServiceBase } from "./flows-call.base";
import { BehaviorSubject, Observable, of } from "rxjs";
import { Authorization } from "@services/authorization/authorization";
import { inject } from "@angular/core";
import { IOType } from "@models/node-types";
export class FlowsCallServiceFake extends FlowsCallServiceBase {
override getFlowById(flowId: string): Observable<Flow> {
@ -17,8 +16,8 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase {
authorizationService = inject(Authorization);
private data: Record<string, Flow> = {
'1': { id: '1', name: 'A Flow', data: { nodes: [], connections: [] }, visibility: 'PUBLIC', author: 'Alice', createdAt: new Date("December 17, 2023 03:24:00"), updatedAt: new Date("January 7, 2026 12:24:00") },
'2': { id: '2', name: 'Test Flow', data: { nodes: [], connections: [] }, visibility: 'PRIVATE', author: 'Bob', createdAt: new Date("April 25, 2025 12:24:00"), updatedAt: new Date("April 27, 2025 18:42:00") },
'1': { id: '1', name: 'A Flow', data: { blocks: [], connections: [] }, visibility: 'PUBLIC', author: 'Alice', createdAt: new Date("December 17, 2023 03:24:00"), updatedAt: new Date("January 7, 2026 12:24:00") },
'2': { id: '2', name: 'Test Flow', data: { blocks: [], connections: [] }, visibility: 'PRIVATE', author: 'Bob', createdAt: new Date("April 25, 2025 12:24:00"), updatedAt: new Date("April 27, 2025 18:42:00") },
'testFlow': flowFromJson(testDataFlow)
}
@ -33,7 +32,7 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase {
override createNewFlow(name?: string): Observable<Flow> {
const newId = (Object.keys(this.data).length + 1).toString();
this.data[newId] = { id: newId, name: name || `New Flow`, data: { nodes: [], connections: [] }, visibility: 'PRIVATE', author: this.authorizationService.loggedInUser()!.username, createdAt: new Date(), updatedAt: new Date() };
this.data[newId] = { id: newId, name: name || `New Flow`, data: { blocks: [], connections: [] }, visibility: 'PRIVATE', author: this.authorizationService.loggedInUser()!.username, createdAt: new Date(), updatedAt: new Date() };
return of(this.data[newId]);
}
@ -55,7 +54,7 @@ export function flowFromJson(raw: any): Flow {
name: raw.name,
description: raw.description,
data: {
nodes: raw.nodes,
blocks: raw.blocks,
connections: raw.connections
}
};
@ -63,108 +62,69 @@ export function flowFromJson(raw: any): Flow {
const testDataFlow ={
"id": "my test flow",
"author": "lucio.lelii",
"visibility": "PUBLIC",
"createdAt": "2026-01-21T10:38:50.671+00:00",
"updatedAt": "2026-01-21T10:38:50.671+00:00",
"name": "my test flow",
"description": "This is a test flow",
"nodes": [
{
"key": "3ec4a0e5-914a-41e0-ab74-d4e64a4dba09",
"name": "Input Node",
"position": null,
"parameters": null,
"nodeDefinition": {
"category": "Input",
"runtimeParameters": null,
"fixedParameters": null,
"inputs": {},
"outputs": {
"phrase": {
"type": "TEXT",
"multiple": false
}
},
"simulable": false,
"name": "Phrase"
}
},
{
"key": "80519c40-20ba-49cf-800a-598ee2e64f65",
"name": "translator to french",
"position": null,
"parameters": {
"LLMProvider": "OllamaTestProvider",
"LLMModel": "sam860/gemma3:270m"
},
"nodeDefinition": {
"category": "Execution",
"executor": "genericAIExecutorTest",
"inputMappers": {
"prompt": {
"type": "translator",
"translation": "translate the following phrase : \"${{phrase}}\" in french, return only the translation"
}
},
"outputMappers": {
"translated": {
"type": "direct",
"fieldName": "response"
}
},
"name": "French Translator",
"runtimeParameters": null,
"fixedParameters": null,
"inputs": {
"phrase": {
"type": "TEXT",
"multiple": false
}
},
"outputs": {
"translated": {
"type": "TEXT",
"multiple": false
}
}
}
},
{
"key": "ff349078-dce1-4ecd-8b2f-bd43100633fc",
"name": "Output Node",
"position": null,
"parameters": null,
"nodeDefinition": {
"category": "Output",
"runtimeParameters": null,
"fixedParameters": null,
"outputs": {},
"inputs": {
"translated": {
"type": "TEXT",
"multiple": false
}
},
"name": "Translated Phrase"
}
}
],
"connections": [
{
"key": "c335e3da-2af3-4e8f-a8ee-9bfc236196ee",
"sourceNode": "3ec4a0e5-914a-41e0-ab74-d4e64a4dba09",
"sourceField": "phrase",
"targetNode": "80519c40-20ba-49cf-800a-598ee2e64f65",
"targetField": "phrase"
},
{
"key": "e0239aef-d570-463d-b453-c3906ab0e4b0",
"sourceNode": "80519c40-20ba-49cf-800a-598ee2e64f65",
"sourceField": "translated",
"targetNode": "ff349078-dce1-4ecd-8b2f-bd43100633fc",
"targetField": "translated"
}
]
};
"name" : "Test Flow",
"visibility": "PRIVATE",
"author": "lucio",
"description" : "This is a test flow",
"createdAt": "2023-12-17T03:24:00.000Z",
"updatedAt": "2026-01-07T12:24:00.000Z",
"blocks" : [ {
"id" : "cabd6f4e-5a05-41f8-9bf7-4de20391ac4e",
"sink" : false,
"name" : "first",
"inputs" : [ {
"name" : "name",
"type" : "TEXT",
"multiple" : false
} ],
"outputs" : [ {
"name" : "response",
"type" : "TEXT",
"multiple" : false
} ],
"specificConfiguration" : {
"type" : "LLMBlockConfiguration",
"name" : "first",
"llmDescriptor" : {
"provider" : "testProvider",
"model" : "testModel"
},
"prompt" : "Make a question about ${{name}}"
},
"typeName" : "LLMBlock"
}, {
"id" : "0063a3ec-3863-4045-bd3b-61eaf87b4604",
"sink" : true,
"name" : "interactive",
"inputs" : [ {
"name" : "input",
"type" : "TEXT",
"multiple" : false
} ],
"outputs" : [ {
"name" : "output",
"type" : "TEXT",
"multiple" : false
} ],
"specificConfiguration" : {
"type" : "HumanInteractiveBlockConfiguration",
"name" : "interactive",
"actionDescription" : "Answer the question in input",
"llmDescriptor" : {
"provider" : "testProvider",
"model" : "testModel"
},
"inputAsList" : false,
"outputAsList" : false
},
"typeName" : "HumanInteractionBlock"
} ],
"connections" : [ {
"id" : "8da696c2-dc03-4717-b2ce-18637ae6f7f8",
"sourceId" : "cabd6f4e-5a05-41f8-9bf7-4de20391ac4e",
"sourceName" : "response",
"targetId" : "0063a3ec-3863-4045-bd3b-61eaf87b4604",
"targetName" : "input"
} ]
};

View File

@ -0,0 +1,14 @@
@use "sass:math"
:host
display: inline-block
cursor: pointer
border: 1px solid grey
width: 10px
height: 20px
vertical-align: middle
background: #fff
z-index: 2
box-sizing: border-box
&:hover
background: #ddd

View File

@ -0,0 +1,44 @@
import { Component, AfterViewInit, Input, HostListener, HostBinding, ChangeDetectorRef } from "@angular/core";
import { ReteModule } from "rete-angular-plugin/21";
@Component({
template: ``,
})
export class CustomSocket {
@Input() data!: any;
@Input() emit!: any;
@Input() rendered!: any;
@HostBinding("title") get title() {
return this.data.name;
}
@HostBinding("style.width") w = "15px";
@HostBinding("style.height") h = "15px";
@HostBinding("style.display") d = "block";
@HostBinding("style.borderRadius") br = "9999px";
@HostBinding("style.border") border = "2px solid white";
@HostBinding("style.cursor") cursor = "crosshair";
@HostBinding("style.background")
get bg() {
return this.data?.side === "input" ? "rgb(34,197,94)" : "rgb(99,102,241)";
}
@HostBinding("style.boxShadow")
get sh() {
console.log("CustomSocket sh data", this.data);
const c = this.data?.side === "input" ? "rgb(34,197,94)" : "rgb(99,102,241)";
return `0 0 0 1px ${c}`;
}
constructor(private cdr: ChangeDetectorRef) {
this.cdr.detach();
}
ngOnChanges(): void {
this.cdr.detectChanges();
requestAnimationFrame(() => this.rendered());
}
}

View File

@ -19,4 +19,5 @@
</div>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<div class="cursor-move relative user-select-none w-55 rounded-2xl bg-white border border-gray-200 shadow-lg overflow-visible font-sans">
<div
class=" relative user-select-none w-55 rounded-2xl bg-white border border-gray-200 shadow-lg overflow-visible font-sans">
<!-- Header -->
<div
class="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="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>
@ -9,38 +9,64 @@
</div>
<!-- Body -->
<div class="p-3 grid grid-cols-2 gap-4">
<div class="pt-3 pb-3 grid grid-cols-2 gap-4 w-full items-start">
<div class="grid grid-rows-2 gap-1">
<!-- COLONNA INPUT (sinistra) -->
<div class="grid grid-cols-[16px_1fr] items-center h-6 w-full">
<div refComponent class="absolute left-1"
<!-- Socket input -->
<div
refComponent
class="relative z-20"
[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>
type: 'socket',
side: 'input',
key: inputKey,
nodeId: data.id,
payload: inputSocket
}"
[emit]="emit">
</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"
<!-- Label input -->
<span
class="inline-flex items-center justify-start px-2 h-6 w-full
text-xs font-medium
bg-emerald-50 text-emerald-700 border border-emerald-200">
{{ inputKey }}
</span>
</div>
<!-- COLONNA OUTPUT (destra) -->
<div class="grid grid-cols-[1fr_16px] items-center h-6 w-full">
<!-- Label output -->
<span
class="inline-flex items-center justify-end px-2 h-6 w-full
text-xs font-medium
bg-red-50 text-red-700 border border-red-200">
{{ outputKey }}
</span>
<!-- Socket output -->
<div
refComponent
class="relative z-20 justify-self-end"
[data]="{
type: 'socket',
side: 'output',
key: outputKey,
nodeId: data.id,
payload: outputSocket}" [emit]="emit"></div>
type: 'socket',
side: 'output',
key: outputKey,
nodeId: data.id,
payload: outputSocket
}"
[emit]="emit">
</div>
</div>
</div>
</div>

View File

@ -35,16 +35,20 @@ export class LLMNodeComponent {
this.outputKey = outKey;
this.outputSocket = (output as any).socket;
console.log("output data", this.outputSocket);
const inputEntries = Object.entries(this.data.inputs);
const [inKey, input] = inputEntries[0];
this.inputKey = inKey;
this.inputSocket = (input as any).socket;
console.log("input data", this.inputSocket);
}
ngAfterViewInit() {
this.rendered();
}
}

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, HostBinding, Input } from "@angular/core";
import { ChangeDetectorRef, Component, ElementRef, HostBinding, Input } from "@angular/core";
import { ReteModule } from "rete-angular-plugin/21";
@Component({
@ -7,8 +7,7 @@ import { ReteModule } from "rete-angular-plugin/21";
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)]
cursor-crosshair relative -left-3 w-1 h-6 flex items-center justify-center rounded-full bg-green-500 shadow-[0_0_0_1px_rgb(34,197,94)] z-50
`
},
template: ``
@ -32,4 +31,14 @@ export class InputSocket {
this.rendered?.();
});
}
constructor(private cdr: ChangeDetectorRef) {
this.cdr.detach();
}
ngOnChanges(): void {
this.cdr.detectChanges();
requestAnimationFrame(() => this.rendered());
}
}

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, HostBinding, Input } from '@angular/core';
import { ChangeDetectorRef, Component, HostBinding, HostListener, Input } from '@angular/core';
import { ReteModule } from 'rete-angular-plugin/21';
@Component({
@ -7,8 +7,8 @@ import { ReteModule } from 'rete-angular-plugin/21';
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)]
cursor-crosshair relative -right-2 w-1 h-6 flex items-center justify-center rounded-full bg-red-500
shadow-[0_0_0_1px_rgb(239,68,68)]
`
},
template: ``
@ -25,10 +25,19 @@ export class OutputSocket {
ngOnInit(): void {
console.log("OutputSocket ngOnInit", this.data);
}
ngAfterViewInit() {
requestAnimationFrame(() => {
this.rendered?.();
});
}
}
constructor(private cdr: ChangeDetectorRef) {
this.cdr.detach();
}
ngOnChanges(): void {
this.cdr.detectChanges();
requestAnimationFrame(() => this.rendered());
}
}

View File

@ -1,35 +1,28 @@
import { Injector, Input } from "@angular/core";
import { NodeEditor, GetSchemes, ClassicPreset } from "rete";
import { Injector } from "@angular/core";
import { NodeEditor, ClassicPreset } from "rete";
import { AreaPlugin, AreaExtensions } from "rete-area-plugin";
import {
ConnectionPlugin,
Presets as ConnectionPresets
} from "rete-connection-plugin";
import { AngularPlugin, Presets, AngularArea2D, NodeComponent } from "rete-angular-plugin/21";
import { AngularPlugin, Presets, AngularArea2D } from "rete-angular-plugin/21";
import { InputNodeComponent } from "@shared/nodes/input/input-node.component";
import { OutputNodeComponent } from "@shared/nodes/output/output-node.component";
import { HFNode, HFSchemes } from "@models/nodes";
import { FlowData, INodeModel } from "@models/flow";
import { FlowBlock, FlowData } from "@models/flow";
import { LLMNodeComponent } from "@shared/nodes/llm/llm";
import { OutputSocket } from "@shared/sockets/output/output";
import { InputSocket } from "@shared/sockets/input/input";
import { CustomSocket } from "@shared/custom-socket/custom-socket";
type Schemes = GetSchemes<
ClassicPreset.Node,
ClassicPreset.Connection<ClassicPreset.Node, ClassicPreset.Node>
>;
type AreaExtra = AngularArea2D<Schemes>;
type AreaExtra = AngularArea2D<HFSchemes>;
export async function createEditor(container: HTMLElement, injector: Injector, flowData: FlowData) {
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 });
const connection = new ConnectionPlugin<HFSchemes, AreaExtra>();
const render = new AngularPlugin<HFSchemes, AreaExtra>({ injector });
AreaExtensions.selectableNodes(area, AreaExtensions.selector(), {
accumulating: AreaExtensions.accumulateOnCtrl()
});
render.addPreset(
Presets.classic.setup({
@ -43,22 +36,22 @@ export async function createEditor(container: HTMLElement, injector: Injector, f
}
return LLMNodeComponent;
},
socket(context) {
const side = context.side;
return side === "input" ? InputSocket : OutputSocket;
socket() {
return CustomSocket;
}
},
})
);
area.addPipe((c) => {
if (c.type === "render") console.log(c.data);
editor.addPipe((c) => {
if (c.type === "connectioncreate") console.log(c.data);
return c;
});
connection.addPreset(ConnectionPresets.classic.setup());
AreaExtensions.simpleNodesOrder(area);
editor.use(area);
area.use(connection);
area.use(render);
@ -73,86 +66,95 @@ export async function createEditor(container: HTMLElement, injector: Injector, f
}
export function exportGraph(editor: NodeEditor<HFSchemes>) {
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,
const nodeIdToBlockId = new Map<string, string>();
const blocks: FlowBlock[] = editor.getNodes().map((node) => {
const blockData = node.data;
const blockId = blockData?.id ?? node.id;
nodeIdToBlockId.set(node.id, blockId);
}));
const inputs = Object.entries(node.inputs).map(([name, input]) => ({
name,
type: ((input as any).socket?.name as string) ?? "ANY",
multiple: false
}));
const connections = editor.getConnections().map(c => ({
id: c.id,
from: {
nodeId: c.source,
output: c.sourceOutput
},
to: {
nodeId: c.target,
input: c.targetInput
}
const outputs = Object.entries(node.outputs).map(([name, output]) => ({
name,
type: ((output as any).socket?.name as string) ?? "ANY",
multiple: false
}));
return {
id: blockId,
sink: blockData?.sink ?? false,
name: blockData?.name ?? node.label,
position: blockData?.position,
inputs,
outputs,
specificConfiguration: blockData?.specificConfiguration ?? {},
typeName: blockData?.typeName ?? "LLMBlock"
};
});
const connections = editor.getConnections().map((c) => ({
id: String(c.id),
sourceId: nodeIdToBlockId.get(c.source) ?? c.source,
sourceName: c.sourceOutput,
targetId: nodeIdToBlockId.get(c.target) ?? c.target,
targetName: c.targetInput
}));
return {
nodes,
blocks,
connections
};
}
async function loadFlowData(editor: NodeEditor<HFSchemes>, flowData: FlowData) {
if (flowData.nodes.length === 0) return;
const socket = new ClassicPreset.Socket("socket");
const nodeMapping = new Map<string, string>();
for (const nodeData of flowData.nodes) {
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);
node.addOutput(output,
new ClassicPreset.Output(
socket, output
)
);
});
Object.entries(nodeData.nodeDefinition?.inputs).forEach(([input, def]) => {
console.log("Adding input", input);
node.addInput(input,
new ClassicPreset.Input(
socket, input
)
);
});
if (!flowData.blocks?.length) return;
const sockets = new Map<string, ClassicPreset.Socket>();
const getSocket = (type: string) => {
if (!sockets.has(type)) sockets.set(type, new ClassicPreset.Socket(type));
return sockets.get(type)!;
};
const nodeMapping = new Map<string, any>();
for (const block of flowData.blocks) {
const nodeLabel =
block.typeName === "InputBlock"
? "Input"
: block.typeName === "OutputBlock"
? "Output"
: block.typeName;
const node = new ClassicPreset.Node(nodeLabel) as HFNode;
node.data = block;
nodeMapping.set(block.id, node.id);
for (const output of block.outputs ?? []) {
node.addOutput(output.name, new ClassicPreset.Output(getSocket(output.type ?? "ANY")));
}
for (const input of block.inputs ?? []) {
node.addInput(input.name, new ClassicPreset.Input(getSocket(input.type ?? "ANY")));
}
await editor.addNode(node);
/*if (nodeData.position != null)
area.translate(node.id, { x: nodeData.position.x, y: nodeData.position.y });*/
}
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(
node,
connections.sourceField,
targetNode,
connections.targetField
);
await editor.addConnection(connection);
}
for (const c of flowData.connections ?? []) {
if (!nodeMapping.has(c.sourceId) || !nodeMapping.has(c.targetId)) continue;
}
const sourceNode = editor.getNode(nodeMapping.get(c.sourceId)) as any;
const targetNode = editor.getNode(nodeMapping.get(c.targetId)) as any;
await editor.addConnection(
new ClassicPreset.Connection(sourceNode, c.sourceName, targetNode, c.targetName)
);
}
}