added rete.js
This commit is contained in:
parent
240de8a6e2
commit
e51d9a2fac
|
|
@ -23,7 +23,10 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-angular": "^0.561.0",
|
||||
"rete": "^2.0.6",
|
||||
"rete-angular-plugin": "^2.6.0",
|
||||
"rete-area-plugin": "^2.1.5",
|
||||
"rete-connection-plugin": "^2.0.5",
|
||||
"rete-render-utils": "^2.0.3",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
|
@ -8241,7 +8244,6 @@
|
|||
"integrity": "sha512-kPmlKCGFES2VWtY7Y7SCB8ZeXRMsgX5deza9cu4OwmfM/ZUimd461kC3hRyccoyVxE4POlHUx0gg2jcGfusHFg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
}
|
||||
|
|
@ -8270,7 +8272,6 @@
|
|||
"resolved": "https://registry.npmjs.org/rete-area-plugin/-/rete-area-plugin-2.1.5.tgz",
|
||||
"integrity": "sha512-iquEvwkQlcsO4cmgM3Z37TG0AWaE536dfA+lCJAze5YJzVx4RBaViUCqdB4dUA/utSytpBCkiDC4D3ztM9akGQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
|
|
@ -8278,6 +8279,19 @@
|
|||
"rete": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rete-connection-plugin": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/rete-connection-plugin/-/rete-connection-plugin-2.0.5.tgz",
|
||||
"integrity": "sha512-KFtlOyEJRc0y9STVgo2T+t+j9u5fxiTxbyzPbMCm0uqncb3b8d2ABDIzvWoNo5zQAh2Oz/OvlUovupbzrGzpSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rete": "^2.0.1",
|
||||
"rete-area-plugin": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rete-render-utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rete-render-utils/-/rete-render-utils-2.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-angular": "^0.561.0",
|
||||
"rete": "^2.0.6",
|
||||
"rete-angular-plugin": "^2.6.0",
|
||||
"rete-area-plugin": "^2.1.5",
|
||||
"rete-connection-plugin": "^2.0.5",
|
||||
"rete-render-utils": "^2.0.3",
|
||||
"rxjs": "~7.8.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
<div class="flex-1 overflow-hidden">
|
||||
<app-title-toolbar></app-title-toolbar>
|
||||
<!-- editor canvas -->
|
||||
<div class="flex-1 bg-amber-200 h-full">
|
||||
<div class="flex-1 h-full">
|
||||
<app-rete-editor></app-rete-editor>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Component, inject } from '@angular/core';
|
||||
import { Flow } from '@models/flow';
|
||||
import { Component, inject} from '@angular/core';
|
||||
import { EditorSidebar } from "@pages/main/editor-sidebar/editor-sidebar";
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { TitleToolbar } from "@shared/title-toolbar/title-toolbar";
|
||||
import { ReteEditor } from "@shared/rete-editor/rete-editor";
|
||||
|
||||
@Component({
|
||||
selector: 'app-flow-editor',
|
||||
imports: [EditorSidebar, TitleToolbar],
|
||||
imports: [EditorSidebar, TitleToolbar, ReteEditor],
|
||||
templateUrl: './flow-editor.html',
|
||||
styleUrl: './flow-editor.css',
|
||||
})
|
||||
|
|
@ -15,4 +15,5 @@ export class FlowEditor {
|
|||
private editorState: EditorStateHolder = inject(EditorStateHolder);
|
||||
|
||||
flow = this.editorState.currentFlow;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
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
|
||||
};
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
.enabled:hover {
|
||||
background-color: var(--color-gray-100) ;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,26 @@
|
|||
<aside class="h-full flex flex-row
|
||||
bg-white border-r border-gray-200
|
||||
transition-all duration-300 p-1" [class.w-82]="!collapsed()" [class.w-14]="collapsed()">
|
||||
|
||||
<div
|
||||
class="transition-all duration-300 ease-in-out overflow-hidden"
|
||||
[class.w-14]="collapsed()"
|
||||
[class.w-72]="!collapsed()"
|
||||
>
|
||||
<aside class="h-full w-72 bg-white border-r border-gray-200 flex">
|
||||
<div class="flex flex-col h-full">
|
||||
|
||||
|
||||
<button (click)="collapse()" class="relative
|
||||
w-6 h-6 rounded-full
|
||||
bg-white border shadow
|
||||
flex items-center justify-center
|
||||
hover:bg-gray-50 z-10" [class.invisible]="collapsed()">
|
||||
hover:bg-gray-50 z-10" [class.invisible]="open == null">
|
||||
|
||||
<i class="bi bi-chevron-left text-xs transition-transform"></i>
|
||||
</button>
|
||||
|
||||
<!-- ICON MODE -->
|
||||
<div class="flex flex-col gap-4 items-center pt-6 flex-1">
|
||||
<i class="bi bi-lightning-charge-fill text-indigo-500 text-xl cursor-pointer enabled p-2"
|
||||
(click)="openSide('flows')"></i>
|
||||
<i [ngClass]="{'cursor-none bg-gray-200' : open == 'flows'}"
|
||||
[ngClass]="{'cursor-pointer enabled' : open != 'flows'}"
|
||||
class="bi bi-lightning-charge-fill text-indigo-500 text-xl p-2"
|
||||
(click)="open != 'flows' && openSide('flows')"></i>
|
||||
<i [ngClass]="{'cursor-pointer text-gray-600 enabled' : !blockDisabled()}"
|
||||
class="bi bi-boxes text-gray-200 text-xl p-2" (click)="!blockDisabled() && openSide('blocks')"></i>
|
||||
</div>
|
||||
|
|
@ -51,4 +54,5 @@
|
|||
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
</aside>
|
||||
</div>
|
||||
|
|
@ -35,6 +35,7 @@ export class EditorSidebar {
|
|||
|
||||
collapse() {
|
||||
this.collapsed.set(true);
|
||||
this.open = null;
|
||||
}
|
||||
|
||||
createNewFlow() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<div class="rounded-xl bg-white shadow-md border border-gray-200">
|
||||
<div class="p-3 flex flex-col items-center gap-2">
|
||||
<div class="w-full flex justify-end">
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
refComponent
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'output',
|
||||
key: data.outputs[0]?.id,
|
||||
nodeId: data.id,
|
||||
payload: data.outputs[0]?.socket,
|
||||
seed: seed
|
||||
}"
|
||||
[emit]="emit"
|
||||
data-testid="output-socket"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm font-semibold text-gray-700">Input</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
.io-card {
|
||||
height: 60px;
|
||||
width: 100px;
|
||||
transform: skew(-20deg);
|
||||
background-color: rgb(163, 216, 163);
|
||||
}
|
||||
|
||||
.body {
|
||||
transform: skew(20deg);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: bolder;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
color: white;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.output {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.input {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.output-socket {
|
||||
text-align: right; margin-right: -1px; display: inline-block;
|
||||
}
|
||||
|
||||
.input-socket {
|
||||
text-align: left; margin-left: -1px; display: inline-block;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
Component,
|
||||
Input,
|
||||
HostBinding,
|
||||
ChangeDetectorRef,
|
||||
OnChanges
|
||||
} from "@angular/core";
|
||||
import { ClassicPreset } from "rete";
|
||||
import { CommonModule, KeyValue } from "@angular/common";
|
||||
import { ReteModule } from "rete-angular-plugin/21";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./input-node.component.html",
|
||||
styleUrls: ["./input-node.component.scss"],
|
||||
imports: [CommonModule, ReteModule],
|
||||
host: {
|
||||
"data-testid": "input-node"
|
||||
}
|
||||
})
|
||||
export class InputNodeComponent implements OnChanges {
|
||||
@Input() data!: ClassicPreset.Node;
|
||||
@Input() emit!: (data: any) => void;
|
||||
@Input() rendered!: () => void;
|
||||
|
||||
seed = 0;
|
||||
|
||||
@HostBinding("class.selected") get selected() {
|
||||
return this.data.selected;
|
||||
}
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {
|
||||
this.cdr.detach();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.cdr.detectChanges();
|
||||
requestAnimationFrame(() => this.rendered());
|
||||
this.seed++; // force render sockets
|
||||
}
|
||||
|
||||
sortByIndex(
|
||||
a: KeyValue<string, { index?: number } | undefined>,
|
||||
b: KeyValue<string, { index?: number } | undefined>
|
||||
): number {
|
||||
return (a.value?.index ?? 0) - (b.value?.index ?? 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<div class="rounded-xl bg-white shadow-md border border-gray-200">
|
||||
<div class="p-3 flex items-center justify-center">
|
||||
<p class="text-sm font-semibold text-gray-700">Output</p>
|
||||
<div class="w-full flex justify-end">
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
refComponent
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'input',
|
||||
key: data.inputs[0]?.id,
|
||||
nodeId: data.id,
|
||||
payload: data.inputs[0]?.socket,
|
||||
seed: seed
|
||||
}"
|
||||
[emit]="emit"
|
||||
data-testid="output-socket"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
.io-card {
|
||||
height: 60px;
|
||||
width: 100px;
|
||||
transform: skew(-20deg);
|
||||
background-color: rgb(238, 173, 161);
|
||||
}
|
||||
|
||||
.body {
|
||||
transform: skew(20deg);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center ;
|
||||
font-size: 20px;
|
||||
font-weight:bolder;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
padding: 0;
|
||||
color:white;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import {
|
||||
Component,
|
||||
Input,
|
||||
HostBinding,
|
||||
ChangeDetectorRef,
|
||||
OnChanges
|
||||
} from "@angular/core";
|
||||
import { ClassicPreset } from "rete";
|
||||
import { CommonModule, KeyValue } from "@angular/common";
|
||||
import { ReteModule } from "rete-angular-plugin/21";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./output-node.component.html",
|
||||
styleUrls: ["./output-node.component.scss"],
|
||||
imports: [CommonModule, ReteModule],
|
||||
host: {
|
||||
"data-testid": "output-node"
|
||||
}
|
||||
})
|
||||
export class OutputNodeComponent implements OnChanges {
|
||||
@Input() data!: ClassicPreset.Node;
|
||||
@Input() emit!: (data: any) => void;
|
||||
@Input() rendered!: () => void;
|
||||
|
||||
seed = 0;
|
||||
|
||||
@HostBinding("class.selected") get selected() {
|
||||
return this.data.selected;
|
||||
}
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {
|
||||
this.cdr.detach();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.cdr.detectChanges();
|
||||
requestAnimationFrame(() => this.rendered());
|
||||
this.seed++; // force render sockets
|
||||
}
|
||||
|
||||
sortByIndex<
|
||||
N extends object,
|
||||
T extends KeyValue<string, N & { index?: number }>
|
||||
>(a: T, b: T) {
|
||||
const ai = a.value.index || 0;
|
||||
const bi = b.value.index || 0;
|
||||
|
||||
return ai - bi;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
<div class="h-full w-full" #editor>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ReteEditor } from './rete-editor';
|
||||
|
||||
describe('ReteEditor', () => {
|
||||
let component: ReteEditor;
|
||||
let fixture: ComponentFixture<ReteEditor>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReteEditor]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ReteEditor);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Component, ElementRef, Injector, ViewChild } from '@angular/core';
|
||||
import { createEditor } from '@utilities/rete-editor';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rete-editor',
|
||||
imports: [],
|
||||
templateUrl: './rete-editor.html',
|
||||
styleUrl: './rete-editor.css',
|
||||
})
|
||||
export class ReteEditor {
|
||||
|
||||
constructor(private injector: Injector) {}
|
||||
|
||||
@ViewChild("editor") container!: ElementRef;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const el = this.container.nativeElement;
|
||||
|
||||
if (el) {
|
||||
createEditor(el, this.injector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { Injector } from "@angular/core";
|
||||
import { NodeEditor, GetSchemes, 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 { InputNodeComponent } from "@shared/nodes/input/input-node.component";
|
||||
import { OutputNodeComponent } from "@shared/nodes/output/output-node.component";
|
||||
import { flowTest } from "@models/test";
|
||||
|
||||
type Schemes = GetSchemes<
|
||||
ClassicPreset.Node,
|
||||
ClassicPreset.Connection<ClassicPreset.Node, ClassicPreset.Node>
|
||||
>;
|
||||
type AreaExtra = AngularArea2D<Schemes>;
|
||||
|
||||
export async function createEditor(container: HTMLElement, injector: Injector, flowData?: any) {
|
||||
const socket = new ClassicPreset.Socket("socket");
|
||||
|
||||
const editor = new NodeEditor<Schemes>();
|
||||
const area = new AreaPlugin<Schemes, AreaExtra>(container);
|
||||
const connection = new ConnectionPlugin<Schemes, AreaExtra>();
|
||||
const render = new AngularPlugin<Schemes, AreaExtra>({ injector });
|
||||
|
||||
AreaExtensions.selectableNodes(area, AreaExtensions.selector(), {
|
||||
accumulating: AreaExtensions.accumulateOnCtrl()
|
||||
});
|
||||
|
||||
render.addPreset(
|
||||
Presets.classic.setup({
|
||||
customize: {
|
||||
node(context) {
|
||||
if (context.payload.label === "Input") {
|
||||
return InputNodeComponent;
|
||||
}
|
||||
if (context.payload.label === "Output") {
|
||||
return OutputNodeComponent;
|
||||
}
|
||||
|
||||
return NodeComponent;
|
||||
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
connection.addPreset(ConnectionPresets.classic.setup());
|
||||
|
||||
editor.use(area);
|
||||
area.use(connection);
|
||||
area.use(render);
|
||||
|
||||
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 });*/
|
||||
}
|
||||
}
|
||||
|
||||
//await editor.addConnection(new ClassicPreset.Connection(a, "a", b, "b"));
|
||||
|
||||
AreaExtensions.zoomAt(area, editor.getNodes());
|
||||
|
||||
return () => area.destroy();
|
||||
}
|
||||
Loading…
Reference in New Issue