improved height managing

This commit is contained in:
Lucio Lelii 2026-01-23 10:39:58 +01:00
parent e51d9a2fac
commit 5213e2bdfa
13 changed files with 412 additions and 82 deletions

View File

@ -33,11 +33,11 @@
</header>
<!-- Wrapper per compensare header fisso -->
<div class="pt-16 flex flex-col flex-1 h-full overflow-hidden">
<div class="pt-16 flex flex-col flex-1 h-[calc(100vh-64px)] overflow-hidden">
<ul class="nav fixed w-full top-16 z-40
flex gap-1 px-3
bg-[#d8f4e7] border-b-none shadow-sm">
bg-[#d8f4e7] border-b-none shadow-sm h-10">
<li class="nav-item">
<a routerLink="/editor" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }" tabindex="-1" class="nav-link
@ -64,7 +64,7 @@
<!-- Contenuto principale scrollabile -->
<main class="main-content w-full h-full mt-12 overflow-hidden ml-2 bg-white pb-4 pr-4 rounded-md shadow-sm">
<main class="main-content w-full h-[calc(100vh-104px)] mt-12 overflow-hidden ml-2 bg-white pb-4 pr-4 rounded-md shadow-sm">
<router-outlet></router-outlet>
</main>

View File

@ -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<string, any> | null;
nodeDefinition: INodeDefinitionType;
}
export type IConnectionModel = {
key: string;
sourceNode: string;
sourceField: string;
targetNode: string;
targetField: string;
}

View File

@ -0,0 +1,102 @@
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;
}

12
src/app/models/nodes.ts Normal file
View File

@ -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<HFNode, HFNode>;
export type HFSchemes = GetSchemes<HFNode, HFConnection>;

View File

@ -1,5 +1,5 @@
<div
class="transition-all duration-300 ease-in-out overflow-hidden"
class="transition-all duration-300 ease-in-out overflow-hidden h-[calc(100vh-104px)]"
[class.w-14]="collapsed()"
[class.w-72]="!collapsed()"
>

View File

@ -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<Flow> {
@ -16,9 +17,10 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase {
authorizationService = inject(Authorization);
private data: Record<string, Flow> = {
'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<Flow> {
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"
}
]
};

View File

@ -5,8 +5,8 @@
</div>
</div>
} @else {
<div class="h-screen flex flex-col">
<div class="w-full max-w-sm py-2">
<div class="h-full flex flex-col">
<div class="w-full max-w-sm py-2 h-15">
<div class="relative flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="absolute w-5 h-5 top-2.5 left-2.5 text-slate-600">
@ -21,26 +21,26 @@
</div>
</div>
<div class="btn-group w-full mb-3" role="group">
<div class="btn-group w-full mb-3 h-8" role="group">
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'all'"
[class.btn-outline-primary]="filter() !== 'all'" (click)="filter.set('all')">
All
</button>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'public'"
[class.btn-outline-primary]="filter() !== 'public'" (click)="filter.set('public')">
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'PUBLIC'"
[class.btn-outline-primary]="filter() !== 'PUBLIC'" (click)="filter.set('PUBLIC')">
Public
</button>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'private'"
[class.btn-outline-primary]="filter() !== 'private'" (click)="filter.set('private')">
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'PRIVATE'"
[class.btn-outline-primary]="filter() !== 'PRIVATE'" (click)="filter.set('PRIVATE')">
Private
</button>
</div>
<div class="flex pt-2 pb-1 text-sm w-full items-center text-center justify-center">
<div class="flex py-2 text-sm w-full items-center text-center justify-center h-18">
<app-ordering [orderFields]="orderFields" [orderView]="orderView" (orderChanged)="onOrderChanged($event)" ></app-ordering>
</div>
@if (filteredFlows().length === 0) {
@ -48,7 +48,7 @@
No flows found
</div>
} @else {
<div class="flex-1 overflow-y-auto h-full w-full py-2 gap-2 flex flex-col items-stretch max-h-[calc(100vh-32rem)]">
<div class="flex-1 overflow-y-auto w-full h-full py-2 gap-2 flex flex-col items-stretch max-h-[calc(100vh-380px)]">
@for (flow of filteredFlows(); track flow.id) {
<app-flow-item [(detailOpenedId)]="detailOpenedId" [flow]="flow"></app-flow-item>
}

View File

@ -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<Flow> {
filter = signal<FlowFilter>('all');
orderFields: OrderField[] = [
{ field: 'name', label: 'Name' },
{ field: 'updatedAt', label: 'Last Update Date' },
@ -30,7 +29,6 @@ export class FlowsList extends ListStateViewHolder<Flow> {
private flowsService = inject(FlowsService);
loading: WritableSignal<boolean> = signal(true);
get orderView() {
const existingState = this.view;

View File

@ -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"
>
<div class="flex items-center gap-2">
@ -21,7 +21,7 @@
</div>
<!-- Body -->
<div
class="transition-all duration-300 px-2 pb-2 flex flex-col flex-1 overflow-hidden">
class="transition-all duration-300 px-2 pb-2 flex flex-col flex-1 overflow-hidden h-[calc(100%-72px)]">
<ng-content></ng-content>
</div>

View File

@ -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;

View File

@ -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",

View File

@ -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<any>();
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;
}
)
});
}
}
}

View File

@ -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<Schemes>;
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<Schemes>();
const area = new AreaPlugin<Schemes, AreaExtra>(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<HFSchemes>) {
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<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) {
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);
}
}
}
}