From 25edbbd34c090a52ed68fa76e045a6d3ae8ded7a Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 27 Feb 2026 10:14:12 +0100 Subject: [PATCH] update for blocks --- src/app/models/flow.ts | 61 +++++- src/app/models/node-types.ts | 102 ---------- src/app/models/nodes.ts | 6 +- src/app/models/test.ts | 160 ++++++---------- .../services/authorization/authorization.ts | 2 - src/app/services/flows/flows-call.fake.ts | 180 +++++++----------- .../shared/custom-socket/custom-socket.sass | 14 ++ src/app/shared/custom-socket/custom-socket.ts | 44 +++++ .../nodes/input/input-node.component.html | 1 + src/app/shared/nodes/llm/llm.html | 80 +++++--- src/app/shared/nodes/llm/llm.ts | 4 + src/app/shared/sockets/input/input.ts | 15 +- src/app/shared/sockets/output/output.ts | 19 +- src/app/utilities/rete-editor.ts | 176 ++++++++--------- 14 files changed, 420 insertions(+), 444 deletions(-) delete mode 100644 src/app/models/node-types.ts create mode 100644 src/app/shared/custom-socket/custom-socket.sass create mode 100644 src/app/shared/custom-socket/custom-socket.ts diff --git a/src/app/models/flow.ts b/src/app/models/flow.ts index 95fe361..aed2285 100644 --- a/src/app/models/flow.ts +++ b/src/app/models/flow.ts @@ -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; + +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 | null; - nodeDefinition: INodeDefinitionType; } export type IConnectionModel = { @@ -34,4 +84,3 @@ export type IConnectionModel = { targetField: string; } - diff --git a/src/app/models/node-types.ts b/src/app/models/node-types.ts deleted file mode 100644 index bba080f..0000000 --- a/src/app/models/node-types.ts +++ /dev/null @@ -1,102 +0,0 @@ - -export interface INodeDefinitionType { - name: string; - category: string; - outputs: Record; - inputs: Record; - fixedParameters?: Record | null; - runtimeParameters?: Record | null; -} - -export interface IExecutionNodeType extends INodeDefinitionType { - executor: string; - outputMappers?: Record; - inputMappers?: Record; -} - -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, - outputs: Record, - 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; -} - diff --git a/src/app/models/nodes.ts b/src/app/models/nodes.ts index 758780a..49b9523 100644 --- a/src/app/models/nodes.ts +++ b/src/app/models/nodes.ts @@ -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; -export type HFSchemes = GetSchemes; \ No newline at end of file +export type HFSchemes = GetSchemes; diff --git a/src/app/models/test.ts b/src/app/models/test.ts index ed8b2c5..89dcdbd 100644 --- a/src/app/models/test.ts +++ b/src/app/models/test.ts @@ -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" + } ] }; diff --git a/src/app/services/authorization/authorization.ts b/src/app/services/authorization/authorization.ts index 1b35565..ddae82d 100644 --- a/src/app/services/authorization/authorization.ts +++ b/src/app/services/authorization/authorization.ts @@ -9,8 +9,6 @@ import { take, tap } from 'rxjs'; }) export class Authorization { - private readonly apiUrl = environment.apiUrl + '/auth'; - private user = signal(null); authCall: AuthorizationCallServiceBase = new environment.authorizationCallService(); diff --git a/src/app/services/flows/flows-call.fake.ts b/src/app/services/flows/flows-call.fake.ts index 5f02e34..9229f7c 100644 --- a/src/app/services/flows/flows-call.fake.ts +++ b/src/app/services/flows/flows-call.fake.ts @@ -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 { @@ -17,8 +16,8 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase { authorizationService = inject(Authorization); private data: Record = { - '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 { 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" - } - ] -}; \ No newline at end of file + "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" + } ] +}; + diff --git a/src/app/shared/custom-socket/custom-socket.sass b/src/app/shared/custom-socket/custom-socket.sass new file mode 100644 index 0000000..c3cb7e5 --- /dev/null +++ b/src/app/shared/custom-socket/custom-socket.sass @@ -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 \ No newline at end of file diff --git a/src/app/shared/custom-socket/custom-socket.ts b/src/app/shared/custom-socket/custom-socket.ts new file mode 100644 index 0000000..8b66db8 --- /dev/null +++ b/src/app/shared/custom-socket/custom-socket.ts @@ -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()); + } +} diff --git a/src/app/shared/nodes/input/input-node.component.html b/src/app/shared/nodes/input/input-node.component.html index e742cea..7ac936c 100644 --- a/src/app/shared/nodes/input/input-node.component.html +++ b/src/app/shared/nodes/input/input-node.component.html @@ -19,4 +19,5 @@ + \ No newline at end of file diff --git a/src/app/shared/nodes/llm/llm.html b/src/app/shared/nodes/llm/llm.html index b995ff0..b749766 100644 --- a/src/app/shared/nodes/llm/llm.html +++ b/src/app/shared/nodes/llm/llm.html @@ -1,7 +1,7 @@ -
+
-
+
LLM
@@ -9,38 +9,64 @@
-
+
-
+ +
-
+
- -
- {{ inputKey }} -
- + type: 'socket', + side: 'input', + key: inputKey, + nodeId: data.id, + payload: inputSocket + }" + [emit]="emit">
-
-
- {{ outputKey }} -
- -
+ + {{ inputKey }} + + +
+ + + +
+ + + + {{ outputKey }} + + + +
+ type: 'socket', + side: 'output', + key: outputKey, + nodeId: data.id, + payload: outputSocket + }" + [emit]="emit">
+
+ +
\ No newline at end of file diff --git a/src/app/shared/nodes/llm/llm.ts b/src/app/shared/nodes/llm/llm.ts index d99fea1..5fc2c8b 100644 --- a/src/app/shared/nodes/llm/llm.ts +++ b/src/app/shared/nodes/llm/llm.ts @@ -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(); } + + } diff --git a/src/app/shared/sockets/input/input.ts b/src/app/shared/sockets/input/input.ts index 230d3af..724ce41 100644 --- a/src/app/shared/sockets/input/input.ts +++ b/src/app/shared/sockets/input/input.ts @@ -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()); + } + } \ No newline at end of file diff --git a/src/app/shared/sockets/output/output.ts b/src/app/shared/sockets/output/output.ts index 9656d71..69c8c46 100644 --- a/src/app/shared/sockets/output/output.ts +++ b/src/app/shared/sockets/output/output.ts @@ -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?.(); }); } -} \ No newline at end of file + +constructor(private cdr: ChangeDetectorRef) { + this.cdr.detach(); + } + + ngOnChanges(): void { + this.cdr.detectChanges(); + requestAnimationFrame(() => this.rendered()); + } +} diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index 7bcf869..a97abdf 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -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 ->; -type AreaExtra = AngularArea2D; +type AreaExtra = AngularArea2D; export async function createEditor(container: HTMLElement, injector: Injector, flowData: FlowData) { const editor = new NodeEditor(); const area = new AreaPlugin(container); - const connection = new ConnectionPlugin(); - const render = new AngularPlugin({ injector }); + const connection = new ConnectionPlugin(); + const render = new AngularPlugin({ 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) { - 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(); + 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, flowData: FlowData) { - if (flowData.nodes.length === 0) return; - const socket = new ClassicPreset.Socket("socket"); - const nodeMapping = new Map(); - 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(); + const getSocket = (type: string) => { + if (!sockets.has(type)) sockets.set(type, new ClassicPreset.Socket(type)); + return sockets.get(type)!; + }; + + const nodeMapping = new Map(); + + 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) + ); } - } + +