diff --git a/src/app/layouts/app-layout/app-layout.html b/src/app/layouts/app-layout/app-layout.html index acd3b1b..126167b 100644 --- a/src/app/layouts/app-layout/app-layout.html +++ b/src/app/layouts/app-layout/app-layout.html @@ -33,11 +33,11 @@ -
+
    + bg-[#d8f4e7] border-b-none shadow-sm h-10">
  • +
    diff --git a/src/app/models/flow.ts b/src/app/models/flow.ts index d23a9e1..95fe361 100644 --- a/src/app/models/flow.ts +++ b/src/app/models/flow.ts @@ -1,6 +1,6 @@ -export type FlowVisibility = 'public' | 'private'; - +import { INodeDefinitionType } from "./node-types"; +export type FlowVisibility = 'PUBLIC' | 'PRIVATE'; export type Flow = { id: string; @@ -8,11 +8,30 @@ export type Flow = { visibility: FlowVisibility; data: FlowData; author: string; + description?: string; createdAt: Date; updatedAt: Date; }; -export class FlowData { - - constructor(public nodes: any[] = [], public edges: any[] = []) {} -}; +export type FlowData = { + nodes: INodeModel[]; + connections: IConnectionModel[]; +} + +export type INodeModel = { + key: string; + name?: string; + position: { x: number, y: number } | null; + parameters?: Record | null; + nodeDefinition: INodeDefinitionType; +} + +export type IConnectionModel = { + key: string; + sourceNode: string; + sourceField: string; + targetNode: string; + targetField: string; +} + + diff --git a/src/app/models/node-types.ts b/src/app/models/node-types.ts new file mode 100644 index 0000000..bba080f --- /dev/null +++ b/src/app/models/node-types.ts @@ -0,0 +1,102 @@ + +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 new file mode 100644 index 0000000..7bce8e4 --- /dev/null +++ b/src/app/models/nodes.ts @@ -0,0 +1,12 @@ +import { GetSchemes, ClassicPreset } from "rete"; +import { INodeModel } from "./flow"; + +export type HFNode = ClassicPreset.Node & { + data?: { + value: INodeModel; + }; +}; + +export type HFConnection = ClassicPreset.Connection; + +export type HFSchemes = GetSchemes; \ No newline at end of file diff --git a/src/app/pages/main/editor-sidebar/editor-sidebar.html b/src/app/pages/main/editor-sidebar/editor-sidebar.html index b0b3956..6947974 100644 --- a/src/app/pages/main/editor-sidebar/editor-sidebar.html +++ b/src/app/pages/main/editor-sidebar/editor-sidebar.html @@ -1,5 +1,5 @@
    diff --git a/src/app/services/flows/flows-call.fake.ts b/src/app/services/flows/flows-call.fake.ts index 63971bb..0cb6096 100644 --- a/src/app/services/flows/flows-call.fake.ts +++ b/src/app/services/flows/flows-call.fake.ts @@ -1,8 +1,9 @@ -import { Flow, FlowData } from "@models/flow"; +import { Flow, FlowData, FlowVisibility } from "@models/flow"; 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 { @@ -16,9 +17,10 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase { authorizationService = inject(Authorization); private data: Record = { - '1': { id: '1', name: 'A Flow', data: new FlowData(), 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: new FlowData(), 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: { 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") }, + 'testFlow': flowFromJson(testDataFlow) + } override retrieveAllFlows() { return of(Object.values(this.data)); @@ -31,7 +33,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: new FlowData() , visibility: 'private', author: this.authorizationService.loggedInUser()!.username, createdAt: new Date(), updatedAt: new Date() }; + 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() }; return of(this.data[newId]); } @@ -40,3 +42,125 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase { return of(void 0); } } + + + +export function flowFromJson(raw: any): Flow { + return { + id: raw.id, + author: raw.author, + visibility: raw.visibility as FlowVisibility, + createdAt: new Date(raw.createdAt), + updatedAt: new Date(raw.updatedAt), + name: raw.name, + description: raw.description, + data: { + nodes: raw.nodes, + connections: raw.connections + } + }; +} + + +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": "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", + "from": "3ec4a0e5-914a-41e0-ab74-d4e64a4dba09:phrase", + "to": "80519c40-20ba-49cf-800a-598ee2e64f65:phrase" + }, + { + "key": "e0239aef-d570-463d-b453-c3906ab0e4b0", + "from": "80519c40-20ba-49cf-800a-598ee2e64f65:translated", + "to": "ff349078-dce1-4ecd-8b2f-bd43100633fc:translated" + } + ] +}; \ No newline at end of file diff --git a/src/app/shared/flows-list/flows-list.html b/src/app/shared/flows-list/flows-list.html index 21fc799..be5da9a 100644 --- a/src/app/shared/flows-list/flows-list.html +++ b/src/app/shared/flows-list/flows-list.html @@ -5,8 +5,8 @@
} @else { -
-
+
+
@@ -21,26 +21,26 @@
-
+
- -
-
+
@if (filteredFlows().length === 0) { @@ -48,7 +48,7 @@ No flows found
} @else { -
+
@for (flow of filteredFlows(); track flow.id) { } diff --git a/src/app/shared/flows-list/flows-list.ts b/src/app/shared/flows-list/flows-list.ts index d61c553..9c280b5 100644 --- a/src/app/shared/flows-list/flows-list.ts +++ b/src/app/shared/flows-list/flows-list.ts @@ -1,13 +1,13 @@ import { CommonModule } from '@angular/common'; import { Component, computed, effect, inject, model, output, signal, Signal, WritableSignal } from '@angular/core'; -import { Flow } from '@models/flow'; +import { Flow, FlowVisibility } from '@models/flow'; import { FlowsService } from '@services/flows/flows'; import { FlowItem } from './flow-item/flow-item'; import { FormsModule } from '@angular/forms'; import { OrderEvent, OrderField, Ordering } from "@shared/ordering/ordering"; import { ListStateViewHolder, OrderViewState } from '@utilities/list-state-holder'; -type FlowFilter = 'all' | 'public' | 'private'; +type FlowFilter = FlowVisibility | 'all'; @Component({ selector: 'app-flows-list', @@ -18,7 +18,6 @@ type FlowFilter = 'all' | 'public' | 'private'; export class FlowsList extends ListStateViewHolder { filter = signal('all'); - orderFields: OrderField[] = [ { field: 'name', label: 'Name' }, { field: 'updatedAt', label: 'Last Update Date' }, @@ -30,7 +29,6 @@ export class FlowsList extends ListStateViewHolder { private flowsService = inject(FlowsService); loading: WritableSignal = signal(true); - get orderView() { const existingState = this.view; diff --git a/src/app/shared/group-holder/group-holder.html b/src/app/shared/group-holder/group-holder.html index 3fc9160..20741df 100644 --- a/src/app/shared/group-holder/group-holder.html +++ b/src/app/shared/group-holder/group-holder.html @@ -8,7 +8,7 @@ class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold text-gray-700 user-select-none - bg-gray-50 " + bg-gray-50 h-18" >
@@ -21,7 +21,7 @@
+ class="transition-all duration-300 px-2 pb-2 flex flex-col flex-1 overflow-hidden h-[calc(100%-72px)]">
diff --git a/src/app/shared/nodes/input/input-node.component.ts b/src/app/shared/nodes/input/input-node.component.ts index 800daa3..7f8e9b9 100644 --- a/src/app/shared/nodes/input/input-node.component.ts +++ b/src/app/shared/nodes/input/input-node.component.ts @@ -3,22 +3,21 @@ import { Input, HostBinding, ChangeDetectorRef, - OnChanges } from "@angular/core"; -import { ClassicPreset } from "rete"; import { CommonModule, KeyValue } from "@angular/common"; import { ReteModule } from "rete-angular-plugin/21"; +import { HFNode } from "@models/nodes"; @Component({ templateUrl: "./input-node.component.html", styleUrls: ["./input-node.component.scss"], imports: [CommonModule, ReteModule], host: { - "data-testid": "input-node" + "data-testid": "node" } }) -export class InputNodeComponent implements OnChanges { - @Input() data!: ClassicPreset.Node; +export class InputNodeComponent { + @Input() data!: any; @Input() emit!: (data: any) => void; @Input() rendered!: () => void; diff --git a/src/app/shared/nodes/output/output-node.component.ts b/src/app/shared/nodes/output/output-node.component.ts index c6be339..bb30560 100644 --- a/src/app/shared/nodes/output/output-node.component.ts +++ b/src/app/shared/nodes/output/output-node.component.ts @@ -8,6 +8,7 @@ import { import { ClassicPreset } from "rete"; import { CommonModule, KeyValue } from "@angular/common"; import { ReteModule } from "rete-angular-plugin/21"; +import { HFNode } from "@models/nodes"; @Component({ templateUrl: "./output-node.component.html", diff --git a/src/app/shared/rete-editor/rete-editor.ts b/src/app/shared/rete-editor/rete-editor.ts index e38fcf1..9a00b69 100644 --- a/src/app/shared/rete-editor/rete-editor.ts +++ b/src/app/shared/rete-editor/rete-editor.ts @@ -1,4 +1,6 @@ -import { Component, ElementRef, Injector, ViewChild } from '@angular/core'; +import { Component, ElementRef, Injector, Input, input, output, ViewChild } from '@angular/core'; +import { FlowData } from '@models/flow'; +import { EditorStateHolder } from '@stores/flow-editor'; import { createEditor } from '@utilities/rete-editor'; @Component({ @@ -9,15 +11,32 @@ import { createEditor } from '@utilities/rete-editor'; }) export class ReteEditor { - constructor(private injector: Injector) {} + flowData: FlowData; + + constructor(private injector: Injector, private flowState: EditorStateHolder ) { + this.flowData = this.flowState.currentFlow()!.data; + } @ViewChild("editor") container!: ElementRef; + + flowChanged = output(); + + ngAfterViewInit(): void { const el = this.container.nativeElement; if (el) { - createEditor(el, this.injector); + createEditor(el, this.injector, this.flowData).then(editor => { + console.log("Rete editor created:", editor); + editor.addPipe( + (context) => { + this.flowChanged.emit({}); + return context; + } + ) + }); } } + } diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index d93d9b5..c96807a 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -8,7 +8,8 @@ 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 { flowTest } from "@models/test"; +import { HFSchemes } from "@models/nodes"; +import { FlowData } from "@models/flow"; type Schemes = GetSchemes< ClassicPreset.Node, @@ -16,8 +17,7 @@ type Schemes = GetSchemes< >; type AreaExtra = AngularArea2D; -export async function createEditor(container: HTMLElement, injector: Injector, flowData?: any) { - const socket = new ClassicPreset.Socket("socket"); +export async function createEditor(container: HTMLElement, injector: Injector, flowData: FlowData) { const editor = new NodeEditor(); const area = new AreaPlugin(container); @@ -29,22 +29,28 @@ export async function createEditor(container: HTMLElement, injector: Injector, f }); render.addPreset( - Presets.classic.setup({ - customize: { - node(context) { - if (context.payload.label === "Input") { - return InputNodeComponent; - } - if (context.payload.label === "Output") { - return OutputNodeComponent; - } + Presets.classic.setup({ + customize: { + node(context) { + if (context.payload.label === "Input") { + return InputNodeComponent; + } + if (context.payload.label === "Output") { + return OutputNodeComponent; + } - return NodeComponent; + return NodeComponent; - } - }, - }) - ); + }, + + }, + }) + ); + + area.addPipe((c) => { + if (c.type === "render") console.log(c.data); + return c; + }); connection.addPreset(ConnectionPresets.classic.setup()); @@ -54,35 +60,85 @@ export async function createEditor(container: HTMLElement, injector: Injector, f AreaExtensions.simpleNodesOrder(area); - if (flowTest) { - for (const nodeData of flowTest.nodes) { - const node = new ClassicPreset.Node(nodeData.nodeDefinition?.category || "Undefined"); - node.id = nodeData.key; - Object.entries(nodeData.nodeDefinition?.outputs).forEach(([output, def]) => { - 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 - ) - ); - }); - - await editor.addNode(node); - /*if (nodeData.position != null) - area.translate(node.id, { x: nodeData.position.x, y: nodeData.position.y });*/ - } - } + if (flowData) + await loadFlowData(editor, flowData); - //await editor.addConnection(new ClassicPreset.Connection(a, "a", b, "b")); AreaExtensions.zoomAt(area, editor.getNodes()); - return () => area.destroy(); + + return editor; } + +export function exportGraph(editor: NodeEditor) { + const nodes = editor.getNodes().map(schema => ({ + + })); + + const connections = editor.getConnections().map(c => ({ + id: c.id, + from: { + nodeId: c.source, + output: c.sourceOutput + }, + to: { + nodeId: c.target, + input: c.targetInput + } + })); + + return { + nodes, + 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) { + const node = new ClassicPreset.Node(nodeData.nodeDefinition?.category || "Undefined"); + 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 + ) + ); + }); + + 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) { { + const targetNode = editor.getNode(nodeMapping.get(connections.targetNode) || ""); + const node = editor.getNode(nodeMapping.get(connections.sourceNode) || ""); + console.log("Creating connection from", connections.sourceNode, "to", connections.targetNode); + if (node && targetNode) { + const connection = new ClassicPreset.Connection( + node, + connections.sourceField, + targetNode, + connections.targetField + ); + await editor.addConnection(connection); + } + + } + } + +} +