From 79b85e2bd5580242fc4764eb32123cecbd2a4b0d Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Mon, 2 Mar 2026 15:58:05 +0100 Subject: [PATCH] added field retriever service improved node design --- src/app/guards/auth-guard.ts | 2 +- src/app/layouts/flow-editor/flow-editor.html | 4 +- src/app/models/flow.ts | 10 +- src/app/pages/auth/login/login.ts | 4 +- src/app/pages/auth/signup/signup.ts | 3 +- .../authorization/authorization-call.ts | 11 +- .../services/authorization/authorization.ts | 2 +- src/app/services/blocks/block-call.base.ts | 6 +- src/app/services/blocks/blocks-call.fake.ts | 285 +++++++++++-- src/app/services/blocks/blocks-call.ts | 6 +- src/app/services/blocks/blocks.ts | 17 +- .../services/dialogs/confirm-dialog.spec.ts | 8 +- src/app/services/flows/flows-call.fake.ts | 9 +- src/app/services/flows/flows-call.ts | 10 +- src/app/services/flows/flows.spec.ts | 8 +- src/app/services/flows/flows.ts | 2 +- .../retreiver/field-retreiver-call.base.ts | 9 + .../retreiver/field-retreiver-call.fake.ts | 32 ++ .../retreiver/field-retreiver-call.ts | 22 + .../retreiver/field-retreiver.spec.ts | 16 + src/app/services/retreiver/field-retreiver.ts | 24 ++ src/app/shared/blocks-list/blocks-list.html | 4 +- src/app/shared/blocks-list/blocks-list.ts | 20 +- .../confirm-dialog/confirm-dialog.spec.ts | 12 +- .../shared/confirm-dialog/confirm-dialog.ts | 2 +- src/app/shared/custom-socket/custom-socket.ts | 3 +- .../flows-list/flow-item/flow-item.html | 30 +- .../shared/flows-list/flow-item/flow-item.ts | 26 +- src/app/shared/flows-list/flows-list.ts | 3 +- src/app/shared/group-holder/group-holder.ts | 2 +- .../human-interaction/human-interaction.css | 391 ++++++++++++++++++ .../human-interaction/human-interaction.html | 136 ++++++ .../human-interaction/human-interaction.ts | 232 +++++++++++ .../nodes/input/input-node.component.ts | 2 +- src/app/shared/nodes/llm/llm.css | 83 ++++ src/app/shared/nodes/llm/llm.html | 37 +- src/app/shared/nodes/llm/llm.spec.ts | 12 +- src/app/shared/nodes/llm/llm.ts | 110 ++++- src/app/shared/rete-editor/rete-editor.css | 10 + src/app/shared/rete-editor/rete-editor.ts | 158 ++++--- src/app/shared/sockets/input/input.ts | 4 +- src/app/shared/sockets/output/output.ts | 2 +- .../shared/title-toolbar/title-toolbar.html | 4 +- src/app/shared/title-toolbar/title-toolbar.ts | 15 +- src/app/stores/flow-editor.spec.ts | 8 +- src/app/stores/flow-editor.ts | 7 +- src/app/stores/list-state.ts | 2 +- src/app/utilities/form-utility.ts | 3 +- src/app/utilities/list-state-holder.ts | 4 +- src/app/utilities/rete-editor.ts | 5 +- src/environments/environment.development.ts | 4 +- src/environments/environment.ts | 4 +- 52 files changed, 1591 insertions(+), 234 deletions(-) create mode 100644 src/app/services/retreiver/field-retreiver-call.base.ts create mode 100644 src/app/services/retreiver/field-retreiver-call.fake.ts create mode 100644 src/app/services/retreiver/field-retreiver-call.ts create mode 100644 src/app/services/retreiver/field-retreiver.spec.ts create mode 100644 src/app/services/retreiver/field-retreiver.ts create mode 100644 src/app/shared/nodes/human-interaction/human-interaction.css create mode 100644 src/app/shared/nodes/human-interaction/human-interaction.html create mode 100644 src/app/shared/nodes/human-interaction/human-interaction.ts diff --git a/src/app/guards/auth-guard.ts b/src/app/guards/auth-guard.ts index a798f2c..0b3e470 100644 --- a/src/app/guards/auth-guard.ts +++ b/src/app/guards/auth-guard.ts @@ -2,7 +2,7 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { Authorization } from '@services/authorization/authorization'; -export const authGuard: CanActivateFn = (route, state) => { +export const authGuard: CanActivateFn = (_route, _state) => { const authService = inject(Authorization); const router = inject(Router); if (authService.isLoggedIn()) { diff --git a/src/app/layouts/flow-editor/flow-editor.html b/src/app/layouts/flow-editor/flow-editor.html index a8db7c9..9d37ba1 100644 --- a/src/app/layouts/flow-editor/flow-editor.html +++ b/src/app/layouts/flow-editor/flow-editor.html @@ -16,7 +16,7 @@
- +
} @else { @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/src/app/models/flow.ts b/src/app/models/flow.ts index 56b2e9f..a6f93a1 100644 --- a/src/app/models/flow.ts +++ b/src/app/models/flow.ts @@ -20,11 +20,15 @@ export type FlowData = { export type BlockTypeName = "HumanInteractionBlock" | "LLMBlock" | "SourceBlock" | string; +export type BlockTypeSchema = Record | null; + export type BlockType = { - userInteractive: boolean; - blockConfigurationClass: string | null; + type: BlockTypeName; description: string; - name: BlockTypeName; + userInteractive: boolean; + configurationType: string | null; + configurationClass: string | null; + schema: BlockTypeSchema; }; export type FlowBlock = { diff --git a/src/app/pages/auth/login/login.ts b/src/app/pages/auth/login/login.ts index 50fc5a1..4fffd54 100644 --- a/src/app/pages/auth/login/login.ts +++ b/src/app/pages/auth/login/login.ts @@ -1,6 +1,6 @@ -import { Component, computed, effect, inject, signal } from '@angular/core'; +import { Component, effect, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute, Router, RouterLink } from "@angular/router"; +import { Router, RouterLink } from "@angular/router"; import { FormUtility } from '@utilities/form-utility'; import { Authorization } from '@services/authorization/authorization'; import { Field, form, required } from '@angular/forms/signals'; diff --git a/src/app/pages/auth/signup/signup.ts b/src/app/pages/auth/signup/signup.ts index d9a1984..067df8d 100644 --- a/src/app/pages/auth/signup/signup.ts +++ b/src/app/pages/auth/signup/signup.ts @@ -1,11 +1,10 @@ -import { Component, computed, effect, ElementRef, inject, signal, ViewChild, viewChild } from '@angular/core'; +import { Component, effect, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { form, Field, minLength, email, required, validate, maxLength, disabled } from '@angular/forms/signals' import { Router, RouterLink } from '@angular/router'; import { UserRegistration } from '@models/user'; import { Authorization } from '@services/authorization/authorization'; import { FormUtility } from '@utilities/form-utility'; -import { max, min } from 'rxjs'; @Component({ diff --git a/src/app/services/authorization/authorization-call.ts b/src/app/services/authorization/authorization-call.ts index ca2bbaf..855380e 100644 --- a/src/app/services/authorization/authorization-call.ts +++ b/src/app/services/authorization/authorization-call.ts @@ -1,20 +1,15 @@ -import { HttpClient } from "@angular/common/http"; import { AuthorizationCallServiceBase } from "./authorization-call.base"; -import { inject } from "@angular/core"; import { User, UserRegistration } from "@models/user"; import { Observable } from "rxjs"; export class AuthorizationCallService extends AuthorizationCallServiceBase { - - private readonly httpClient = inject(HttpClient); - - override login(username: string, password: string): Observable { + override login(_username: string, _password: string): Observable { throw new Error("Method not implemented."); } - override register(userRegistration: UserRegistration): Observable { + override register(_userRegistration: UserRegistration): Observable { throw new Error("Method not implemented."); } -} \ No newline at end of file +} diff --git a/src/app/services/authorization/authorization.ts b/src/app/services/authorization/authorization.ts index ddae82d..1db71f5 100644 --- a/src/app/services/authorization/authorization.ts +++ b/src/app/services/authorization/authorization.ts @@ -1,4 +1,4 @@ -import { effect, Injectable, signal } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { User, UserRegistration } from '@models/user'; import { AuthorizationCallServiceBase } from './authorization-call.base'; import { environment } from '@environment'; diff --git a/src/app/services/blocks/block-call.base.ts b/src/app/services/blocks/block-call.base.ts index 2af4dbb..31554f1 100644 --- a/src/app/services/blocks/block-call.base.ts +++ b/src/app/services/blocks/block-call.base.ts @@ -1,10 +1,12 @@ -import { BlockType, FlowBlock } from "@models/flow"; +import { BlockType, BlockTypeName, FlowBlock } from "@models/flow"; import { Observable } from "rxjs"; export abstract class BlocksCallServiceBase { abstract retrieveAllBlocksTypes() : Observable; - abstract createNewBlock(configuration : any) : Observable; + abstract createEmptyBlock(blockType: BlockTypeName) : Observable; + + abstract updateBlock(blockId : string, configuration : any) : Observable; } \ No newline at end of file diff --git a/src/app/services/blocks/blocks-call.fake.ts b/src/app/services/blocks/blocks-call.fake.ts index ff72b2a..f17561e 100644 --- a/src/app/services/blocks/blocks-call.fake.ts +++ b/src/app/services/blocks/blocks-call.fake.ts @@ -4,45 +4,274 @@ import { BlocksCallServiceBase } from "./block-call.base"; export class BlocksCallServiceFake extends BlocksCallServiceBase { private readonly blockTypes: BlockType[] = [ - { - userInteractive: true, - blockConfigurationClass: - "it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration", - description: "A block that requires human interaction", - name: "HumanInteractionBlock" - }, - { - userInteractive: false, - blockConfigurationClass: - "it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration", - description: "This type represents a LLM node in the workflow manager", - name: "LLMBlock" - }, - { - userInteractive: true, - blockConfigurationClass: null, - description: "This type represents a source node in the workflow manager", - name: "SourceBlock" + { + "type": "HumanInteractionBlock", + "description": "A block that requires human interaction", + "userInteractive": true, + "configurationType": "HumanInteractiveBlockConfiguration", + "configurationClass": "it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration", + "schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "HumanInteractiveBlockConfiguration", + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "HumanInteractiveBlockConfiguration" + ], + "default": "HumanInteractiveBlockConfiguration" + }, + "name": { + "type": "string" + }, + "actionDescription": { + "type": "string" + }, + "llmDescriptor": { + "$ref": "#/definitions/LLMDescriptor" + }, + "inputAsList": { + "type": "boolean" + }, + "outputAsList": { + "type": "boolean" + } + }, + "required": [ + "type", + "name", + "actionDescription", + "llmDescriptor", + "inputAsList", + "outputAsList" + ], + "definitions": { + "LLMDescriptor": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "type": "string", + "x-retriever-name": "providers", + "x-retriever-url": "/retriever/{blockType}/providers", + "x-retriever-owner": "LLMDescriptor" + }, + "model": { + "type": "string", + "x-retriever-name": "models", + "x-retriever-url": "/retriever/{blockType}/models", + "x-retriever-owner": "LLMDescriptor", + "x-retriever-depends-on": [ + "provider" + ] + }, + "authorization": { + "type": "string" + } + } + } + } } - ]; + }, + { + "type": "LLMBlock", + "description": "This type represents a LLM node in the workflow manager", + "userInteractive": false, + "configurationType": "LLMBlockConfiguration", + "configurationClass": "it.cnr.isti.workflow.manager.blocks.configurations.LLMBlockConfiguration", + "schema": { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "LLMBlockConfiguration", + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "LLMBlockConfiguration" + ], + "default": "LLMBlockConfiguration" + }, + "name": { + "type": "string" + }, + "llmDescriptor": { + "$ref": "#/definitions/LLMDescriptor" + }, + "prompt": { + "type": "string" + } + }, + "required": [ + "type", + "name", + "llmDescriptor", + "prompt" + ], + "definitions": { + "LLMDescriptor": { + "type": "object", + "additionalProperties": false, + "properties": { + "provider": { + "type": "string", + "x-retriever-name": "providers", + "x-retriever-url": "/retriever/{blockType}/providers", + "x-retriever-owner": "LLMDescriptor" + }, + "model": { + "type": "string", + "x-retriever-name": "models", + "x-retriever-url": "/retriever/{blockType}/models", + "x-retriever-owner": "LLMDescriptor", + "x-retriever-depends-on": [ + "provider" + ] + }, + "authorization": { + "type": "string" + } + } + } + } + } + }, + { + "type": "SourceBlock", + "description": "This type represents a source node in the workflow manager", + "userInteractive": true, + "configurationType": null, + "configurationClass": null, + "schema": null + } +] override retrieveAllBlocksTypes(): Observable { return of(this.blockTypes); } - override createNewBlock(configuration: any): Observable { - const typeName = configuration?.typeName ?? "LLMBlock"; + override createEmptyBlock(blockType: string): Observable { + const descriptor = this.blockTypes.find((b) => b.type === blockType); + const typeName = descriptor?.type ?? blockType ?? "LLMBlock"; + const schema = descriptor?.schema as Record | null; + + const specificConfiguration = schema + ? this.buildObjectFromSchema(schema, schema) + : {}; + + if (typeof specificConfiguration === "object" && specificConfiguration != null) { + if (!("name" in specificConfiguration)) { + (specificConfiguration as any).name = typeName; + } + } + + const io = this.defaultIOForBlockType(typeName); const block: FlowBlock = { id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}`, - sink: false, - name: configuration?.name ?? "new-block", - position: configuration?.position, - inputs: configuration?.inputs ?? [], - outputs: configuration?.outputs ?? [], - specificConfiguration: configuration?.specificConfiguration ?? {}, + sink: typeName === "HumanInteractionBlock", + name: typeName, + position: undefined, + inputs: io.inputs, + outputs: io.outputs, + specificConfiguration, typeName }; return of(block); } + + override updateBlock(blockId: string, configuration: any): Observable { + const typeName = configuration?.typeName ?? "LLMBlock"; + const io = this.defaultIOForBlockType(typeName); + const block: FlowBlock = { + id: blockId, + sink: typeName === "HumanInteractionBlock", + name: configuration?.name ?? typeName, + position: configuration?.position, + inputs: configuration?.inputs ?? io.inputs, + outputs: configuration?.outputs ?? io.outputs, + specificConfiguration: configuration?.specificConfiguration ?? {}, + typeName + }; + return of(block); + } + + private defaultIOForBlockType(typeName: string) { + if (typeName === "SourceBlock") { + return { + inputs: [], + outputs: [{ name: "output", type: "TEXT", multiple: false }] + }; + } + + if (typeName === "HumanInteractionBlock") { + return { + inputs: [{ name: "input", type: "TEXT", multiple: false }], + outputs: [{ name: "output", type: "TEXT", multiple: false }] + }; + } + + return { + inputs: [{ name: "input", type: "TEXT", multiple: false }], + outputs: [{ name: "output", type: "TEXT", multiple: false }] + }; + } + + private buildObjectFromSchema(node: any, root: any): any { + const resolved = this.resolveRef(node, root); + if (!resolved || typeof resolved !== "object") return {}; + + if (resolved.type === "object" || resolved.properties) { + const result: Record = {}; + const props = resolved.properties ?? {}; + for (const [key, propSchema] of Object.entries(props)) { + result[key] = this.buildValueFromSchema(propSchema, root); + } + return result; + } + + return this.buildValueFromSchema(resolved, root); + } + + private buildValueFromSchema(node: any, root: any): any { + const resolved = this.resolveRef(node, root); + if (!resolved || typeof resolved !== "object") return null; + + if (Object.prototype.hasOwnProperty.call(resolved, "default")) { + return resolved.default; + } + + if (Array.isArray(resolved.enum) && resolved.enum.length > 0) { + return resolved.enum[0]; + } + + const type = resolved.type; + if (type === "string") return ""; + if (type === "boolean") return false; + if (type === "number" || type === "integer") return 0; + if (type === "array") return []; + if (type === "object" || resolved.properties) { + return this.buildObjectFromSchema(resolved, root); + } + + return null; + } + + private resolveRef(node: any, root: any): any { + if (!node || typeof node !== "object") return node; + if (!node.$ref) return node; + + const ref = node.$ref as string; + if (!ref.startsWith("#/")) return node; + + const path = ref.slice(2).split("/"); + let current: any = root; + for (const segment of path) { + current = current?.[segment]; + if (current == null) return node; + } + return current; + } } diff --git a/src/app/services/blocks/blocks-call.ts b/src/app/services/blocks/blocks-call.ts index c793580..20f1689 100644 --- a/src/app/services/blocks/blocks-call.ts +++ b/src/app/services/blocks/blocks-call.ts @@ -7,7 +7,11 @@ export class BlocksCallService extends BlocksCallServiceBase { throw new Error("Method not implemented."); } - override createNewBlock(configuration: any): Observable { + override createEmptyBlock(_blockType: string): Observable { + throw new Error("Method not implemented."); + } + + override updateBlock(_blockId: string, _configuration: any): Observable { throw new Error("Method not implemented."); } } diff --git a/src/app/services/blocks/blocks.ts b/src/app/services/blocks/blocks.ts index 4bc6de2..7afa846 100644 --- a/src/app/services/blocks/blocks.ts +++ b/src/app/services/blocks/blocks.ts @@ -1,6 +1,6 @@ import { Injectable, signal } from '@angular/core'; import { environment } from '@environment'; -import { BlockType } from '@models/flow'; +import { BlockType, BlockTypeName } from '@models/flow'; import { BlocksCallServiceBase } from './block-call.base'; import { catchError, throwError } from 'rxjs'; @@ -29,10 +29,19 @@ export class BlocksService { }); } - createNewBlock(configuration: any) { - return this.blocksCallService.createNewBlock(configuration).pipe( + createEmptyBlock(blockType: BlockTypeName) { + return this.blocksCallService.createEmptyBlock(blockType).pipe( catchError((err) => { - console.error('Create block failed', err); + console.error('Create empty block failed', err); + return throwError(() => err); + }) + ); + } + + updateBlock(blockId: string, configuration: any) { + return this.blocksCallService.updateBlock(blockId, configuration).pipe( + catchError((err) => { + console.error('Update block failed', err); return throwError(() => err); }) ); diff --git a/src/app/services/dialogs/confirm-dialog.spec.ts b/src/app/services/dialogs/confirm-dialog.spec.ts index 5cf3c85..f3e50ba 100644 --- a/src/app/services/dialogs/confirm-dialog.spec.ts +++ b/src/app/services/dialogs/confirm-dialog.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { ConfirmDialog } from './confirm-dialog'; +import { ConfirmDialogService } from './confirm-dialog'; -describe('ConfirmDialog', () => { - let service: ConfirmDialog; +describe('ConfirmDialogService', () => { + let service: ConfirmDialogService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(ConfirmDialog); + service = TestBed.inject(ConfirmDialogService); }); it('should be created', () => { diff --git a/src/app/services/flows/flows-call.fake.ts b/src/app/services/flows/flows-call.fake.ts index 9229f7c..aad3550 100644 --- a/src/app/services/flows/flows-call.fake.ts +++ b/src/app/services/flows/flows-call.fake.ts @@ -1,6 +1,6 @@ -import { Flow, FlowData, FlowVisibility } from "@models/flow"; +import { Flow, FlowVisibility } from "@models/flow"; import { FlowsCallServiceBase } from "./flows-call.base"; -import { BehaviorSubject, Observable, of } from "rxjs"; +import { Observable, of } from "rxjs"; import { Authorization } from "@services/authorization/authorization"; import { inject } from "@angular/core"; @@ -45,8 +45,9 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase { export function flowFromJson(raw: any): Flow { + const flowId = raw.id ?? raw.name ?? crypto.randomUUID(); return { - id: raw.id, + id: flowId, author: raw.author, visibility: raw.visibility as FlowVisibility, createdAt: new Date(raw.createdAt), @@ -62,6 +63,7 @@ export function flowFromJson(raw: any): Flow { const testDataFlow ={ + "id": "testFlow", "name" : "Test Flow", "visibility": "PRIVATE", "author": "lucio", @@ -127,4 +129,3 @@ const testDataFlow ={ "targetName" : "input" } ] }; - diff --git a/src/app/services/flows/flows-call.ts b/src/app/services/flows/flows-call.ts index c0497f1..07963f6 100644 --- a/src/app/services/flows/flows-call.ts +++ b/src/app/services/flows/flows-call.ts @@ -3,20 +3,20 @@ import { FlowsCallServiceBase } from "./flows-call.base"; import { Observable } from "rxjs"; export class FlowsCallService extends FlowsCallServiceBase { - override getFlowById(flowId: string): Observable { + override getFlowById(_flowId: string): Observable { throw new Error("Method not implemented."); } - override createNewFlow(name?: string): Observable { + override createNewFlow(_name?: string): Observable { throw new Error("Method not implemented."); } - override deleteFlow(flowId: string): Observable { + override deleteFlow(_flowId: string): Observable { throw new Error("Method not implemented."); } override retrieveAllFlows(): Observable { throw new Error("Method not implemented."); } - override updateFlow(flow: Flow): Observable { + override updateFlow(_flow: Flow): Observable { throw new Error("Method not implemented."); } -} \ No newline at end of file +} diff --git a/src/app/services/flows/flows.spec.ts b/src/app/services/flows/flows.spec.ts index 583c80a..531ba95 100644 --- a/src/app/services/flows/flows.spec.ts +++ b/src/app/services/flows/flows.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { Flows } from './flows'; +import { FlowsService } from './flows'; -describe('Flows', () => { - let service: Flows; +describe('FlowsService', () => { + let service: FlowsService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(Flows); + service = TestBed.inject(FlowsService); }); it('should be created', () => { diff --git a/src/app/services/flows/flows.ts b/src/app/services/flows/flows.ts index 501b2c0..472c4c8 100644 --- a/src/app/services/flows/flows.ts +++ b/src/app/services/flows/flows.ts @@ -2,7 +2,7 @@ import { Injectable, signal } from '@angular/core'; import { environment } from '@environment'; import { Flow } from '@models/flow'; import { FlowsCallServiceBase } from './flows-call.base'; -import { BehaviorSubject, catchError, combineLatest, Observable, switchMap, tap, throwError } from 'rxjs'; +import { catchError, combineLatest, Observable, switchMap, tap, throwError } from 'rxjs'; @Injectable({ providedIn: 'root', diff --git a/src/app/services/retreiver/field-retreiver-call.base.ts b/src/app/services/retreiver/field-retreiver-call.base.ts new file mode 100644 index 0000000..d1a70b3 --- /dev/null +++ b/src/app/services/retreiver/field-retreiver-call.base.ts @@ -0,0 +1,9 @@ +import { Observable } from "rxjs"; + +export abstract class FieldRetreiverCallServiceBase { + abstract retrieveValues( + blockType: string, + key: string, + context?: Record + ): Observable; +} diff --git a/src/app/services/retreiver/field-retreiver-call.fake.ts b/src/app/services/retreiver/field-retreiver-call.fake.ts new file mode 100644 index 0000000..38e0550 --- /dev/null +++ b/src/app/services/retreiver/field-retreiver-call.fake.ts @@ -0,0 +1,32 @@ +import { Observable, of } from "rxjs"; +import { FieldRetreiverCallServiceBase } from "./field-retreiver-call.base"; + +export class FieldRetreiverCallServiceFake extends FieldRetreiverCallServiceBase { + private readonly providersByBlockType: Record = { + LLMBlock: ["OpenAI", "Anthropic", "OllamaTestProvider"], + HumanInteractionBlock: ["OpenAI", "OllamaTestProvider"] + }; + + private readonly modelsByProvider: Record = { + OpenAI: ["gpt-4.1-mini", "gpt-4.1"], + Anthropic: ["claude-3-5-sonnet", "claude-3-7-sonnet"], + OllamaTestProvider: ["sam860/gemma3:270m", "llama3.2:3b"] + }; + + override retrieveValues( + blockType: string, + key: string, + context?: Record + ): Observable { + if (key === "providers") { + return of(this.providersByBlockType[blockType] ?? []); + } + + if (key === "models") { + const provider = context?.["provider"]; + return of(provider ? (this.modelsByProvider[provider] ?? []) : []); + } + + return of([]); + } +} diff --git a/src/app/services/retreiver/field-retreiver-call.ts b/src/app/services/retreiver/field-retreiver-call.ts new file mode 100644 index 0000000..d8e44cf --- /dev/null +++ b/src/app/services/retreiver/field-retreiver-call.ts @@ -0,0 +1,22 @@ +import { HttpClient, HttpParams } from "@angular/common/http"; +import { inject } from "@angular/core"; +import { environment } from "@environment"; +import { Observable } from "rxjs"; +import { FieldRetreiverCallServiceBase } from "./field-retreiver-call.base"; + +export class FieldRetreiverCallService extends FieldRetreiverCallServiceBase { + private readonly http = inject(HttpClient); + + override retrieveValues( + blockType: string, + key: string, + context?: Record + ): Observable { + const url = `${environment.apiUrl}/retriever/${encodeURIComponent(blockType)}/${encodeURIComponent(key)}`; + let params = new HttpParams(); + for (const [ctxKey, ctxValue] of Object.entries(context ?? {})) { + params = params.set(ctxKey, ctxValue); + } + return this.http.get(url, { params }); + } +} diff --git a/src/app/services/retreiver/field-retreiver.spec.ts b/src/app/services/retreiver/field-retreiver.spec.ts new file mode 100644 index 0000000..8e3a2d3 --- /dev/null +++ b/src/app/services/retreiver/field-retreiver.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FieldRetreiver } from './field-retreiver'; + +describe('FieldRetreiver', () => { + let service: FieldRetreiver; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FieldRetreiver); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/retreiver/field-retreiver.ts b/src/app/services/retreiver/field-retreiver.ts new file mode 100644 index 0000000..454e55d --- /dev/null +++ b/src/app/services/retreiver/field-retreiver.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { environment } from '@environment'; +import { catchError, throwError } from 'rxjs'; +import { FieldRetreiverCallServiceBase } from './field-retreiver-call.base'; + +@Injectable({ + providedIn: 'root', +}) +export class FieldRetreiver { + fieldRetreiverCallService: FieldRetreiverCallServiceBase = new environment.fieldRetreiverCallService(); + + retrieveValues( + blockType: string, + key: string, + context?: Record + ) { + return this.fieldRetreiverCallService.retrieveValues(blockType, key, context).pipe( + catchError((err) => { + console.error('Field retrieval failed', err); + return throwError(() => err); + }) + ); + } +} diff --git a/src/app/shared/blocks-list/blocks-list.html b/src/app/shared/blocks-list/blocks-list.html index d340f5f..cb56785 100644 --- a/src/app/shared/blocks-list/blocks-list.html +++ b/src/app/shared/blocks-list/blocks-list.html @@ -33,13 +33,13 @@ } @else {
- @for (block of filteredBlocks(); track block.name) { + @for (block of filteredBlocks(); track block.type) {
- {{ block.name }} + {{ block.type }}

{{ block.description }}

{ const term = this.searchTerm().toLowerCase(); return blocks.filter((b) => - b.name.toLowerCase().includes(term) || b.description.toLowerCase().includes(term) + b.type.toLowerCase().includes(term) || b.description.toLowerCase().includes(term) ); }); - onOrderChanged(event: OrderEvent) { - const { orderBy, orderDir } = event; - this.view.order = { orderBy, orderDir }; - const blocks = this.filteredBlocks(); - if (!orderBy) return blocks; - - return blocks.sort((a, b) => { - const aValue = (a as any)[orderBy]; - const bValue = (b as any)[orderBy]; - if (aValue < bValue) return orderDir === 'asc' ? -1 : 1; - if (aValue > bValue) return orderDir === 'asc' ? 1 : -1; - return 0; - }); - } - onDragStart(event: DragEvent, block: BlockType) { if (!event.dataTransfer) return; event.dataTransfer.effectAllowed = 'copy'; event.dataTransfer.setData(BLOCK_TYPE_DRAG_MIME, JSON.stringify(block)); - event.dataTransfer.setData('text/plain', block.name); + event.dataTransfer.setData('text/plain', block.type); } } diff --git a/src/app/shared/confirm-dialog/confirm-dialog.spec.ts b/src/app/shared/confirm-dialog/confirm-dialog.spec.ts index 85ecf02..0665973 100644 --- a/src/app/shared/confirm-dialog/confirm-dialog.spec.ts +++ b/src/app/shared/confirm-dialog/confirm-dialog.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ConfirmDialog } from './confirm-dialog'; +import { ConfirmDialogHostComponent } from './confirm-dialog'; -describe('ConfirmDialog', () => { - let component: ConfirmDialog; - let fixture: ComponentFixture; +describe('ConfirmDialogHostComponent', () => { + let component: ConfirmDialogHostComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ConfirmDialog] + imports: [ConfirmDialogHostComponent] }) .compileComponents(); - fixture = TestBed.createComponent(ConfirmDialog); + fixture = TestBed.createComponent(ConfirmDialogHostComponent); component = fixture.componentInstance; await fixture.whenStable(); }); diff --git a/src/app/shared/confirm-dialog/confirm-dialog.ts b/src/app/shared/confirm-dialog/confirm-dialog.ts index 305a068..89d2aab 100644 --- a/src/app/shared/confirm-dialog/confirm-dialog.ts +++ b/src/app/shared/confirm-dialog/confirm-dialog.ts @@ -1,4 +1,4 @@ -import { Component, inject, Input } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; @Component({ diff --git a/src/app/shared/custom-socket/custom-socket.ts b/src/app/shared/custom-socket/custom-socket.ts index 8b66db8..b1536da 100644 --- a/src/app/shared/custom-socket/custom-socket.ts +++ b/src/app/shared/custom-socket/custom-socket.ts @@ -1,5 +1,4 @@ -import { Component, AfterViewInit, Input, HostListener, HostBinding, ChangeDetectorRef } from "@angular/core"; -import { ReteModule } from "rete-angular-plugin/21"; +import { Component, Input, HostBinding, ChangeDetectorRef } from "@angular/core"; @Component({ template: ``, diff --git a/src/app/shared/flows-list/flow-item/flow-item.html b/src/app/shared/flows-list/flow-item/flow-item.html index 643fcc0..e5f1bfb 100644 --- a/src/app/shared/flows-list/flow-item/flow-item.html +++ b/src/app/shared/flows-list/flow-item/flow-item.html @@ -1,12 +1,23 @@
+ select-none" + [class.cursor-pointer]="flow().id !== openedFlowId()" + [class.cursor-default]="flow().id === openedFlowId()" + [class.bg-indigo-50]="flow().id !== openedFlowId()" + [class.border-indigo-200]="flow().id !== openedFlowId()" + [class.bg-emerald-50]="flow().id === openedFlowId()" + [class.border-emerald-300]="flow().id === openedFlowId()" + [class.ring-1]="flow().id === openedFlowId()" + [class.ring-emerald-200]="flow().id === openedFlowId()" + (click)="flow().id !== openedFlowId() && open()">
-
+
- +
@@ -74,4 +90,4 @@
-
\ No newline at end of file +
diff --git a/src/app/shared/flows-list/flow-item/flow-item.ts b/src/app/shared/flows-list/flow-item/flow-item.ts index 86c38c6..60c901a 100644 --- a/src/app/shared/flows-list/flow-item/flow-item.ts +++ b/src/app/shared/flows-list/flow-item/flow-item.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, effect, EventEmitter, inject, input, model, output, signal, untracked } from '@angular/core'; +import { Component, computed, effect, inject, input, model, signal } from '@angular/core'; import { Flow } from '@models/flow'; +import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; import { FlowsService } from '@services/flows/flows'; import { EditorStateHolder } from '@stores/flow-editor'; @@ -13,6 +14,7 @@ import { EditorStateHolder } from '@stores/flow-editor'; export class FlowItem { private editorState = inject(EditorStateHolder); + private confirm = inject(ConfirmDialogService); private flowsService = inject(FlowsService); @@ -34,9 +36,17 @@ export class FlowItem { }); } - open() { + async open() { console.log('Opening flow:', this.flow()); - this.editorState.openDocument(this.flow()); + if (this.editorState.isDirty() && this.openedFlowId() !== this.flow().id) { + const confirmed = await this.confirm.open( + 'You have unsaved changes in the current flow. Open another flow anyway?' + ); + if (!confirmed) return; + await this.editorState.openDocument(this.flow(), { skipDirtyCheck: true }); + return; + } + await this.editorState.openDocument(this.flow()); } clone() { @@ -48,7 +58,14 @@ export class FlowItem { }); } - remove() { + async remove() { + const confirmed = await this.confirm.open( + this.editorState.isDirty() + ? 'You have unsaved changes in the current flow. Delete this flow anyway?' + : 'Are you sure you want to delete this flow?' + ); + if (!confirmed) return; + this.flowsService.deleteFlow(this.flow().id).subscribe({ next: () => { console.log('Flow deleted:', this.flow().id); @@ -69,4 +86,3 @@ export class FlowItem { } } } - diff --git a/src/app/shared/flows-list/flows-list.ts b/src/app/shared/flows-list/flows-list.ts index 9c280b5..ceebfcd 100644 --- a/src/app/shared/flows-list/flows-list.ts +++ b/src/app/shared/flows-list/flows-list.ts @@ -1,5 +1,4 @@ -import { CommonModule } from '@angular/common'; -import { Component, computed, effect, inject, model, output, signal, Signal, WritableSignal } from '@angular/core'; +import { Component, computed, effect, inject, model, signal, Signal, WritableSignal } from '@angular/core'; import { Flow, FlowVisibility } from '@models/flow'; import { FlowsService } from '@services/flows/flows'; import { FlowItem } from './flow-item/flow-item'; diff --git a/src/app/shared/group-holder/group-holder.ts b/src/app/shared/group-holder/group-holder.ts index 912adb0..49fac0c 100644 --- a/src/app/shared/group-holder/group-holder.ts +++ b/src/app/shared/group-holder/group-holder.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, input, Input, Output } from '@angular/core'; +import { Component, Input } from '@angular/core'; @Component({ selector: 'app-group-holder', diff --git a/src/app/shared/nodes/human-interaction/human-interaction.css b/src/app/shared/nodes/human-interaction/human-interaction.css new file mode 100644 index 0000000..6729331 --- /dev/null +++ b/src/app/shared/nodes/human-interaction/human-interaction.css @@ -0,0 +1,391 @@ +.hi-node { + position: relative; + width: 292px; + border-radius: 16px; + border: 1px solid #f7d7a7; + background: linear-gradient(180deg, #fffdf9 0%, #fff7ed 100%); + box-shadow: 0 10px 24px rgba(124, 45, 18, 0.14); + color: #431407; + overflow: visible; +} + +.hi-header { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + background: linear-gradient(135deg, #f59e0b 0%, #ea580c 100%); + border-top-left-radius: 16px; + border-top-right-radius: 16px; + border-bottom: 1px solid #fdba74; +} + +.hi-icon { + width: 30px; + height: 30px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + color: #fffbeb; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 13px; + flex-shrink: 0; +} + +.hi-header-content { + display: flex; + flex-direction: column; + min-width: 0; +} + +.hi-subtitle-row { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.hi-warning { + width: 24px; + height: 24px; + border-radius: 999px; + border: 1px solid #fecaca; + background: #dc2626; + color: #fff7ed; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 13px; + box-shadow: 0 0 0 2px rgba(127, 29, 29, 0.18); +} + +.hi-title { + color: #fffbeb; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.02em; +} + +.hi-subtitle { + color: #fff7ed; + font-size: 11px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.hi-name-edit-btn { + border: none; + background: rgba(255, 255, 255, 0.2); + color: #fff7ed; + border-radius: 999px; + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: background-color 0.15s ease; +} + +.hi-name-edit-btn:hover { + background: rgba(255, 255, 255, 0.3); +} + +.hi-warning-wrap { + position: relative; + margin-left: auto; +} + +.hi-warning-tooltip { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 180px; + max-width: 240px; + border: 1px solid #fecaca; + border-radius: 8px; + background: #fff1f2; + color: #7f1d1d; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2); + padding: 8px; + z-index: 80; + opacity: 0; + visibility: hidden; + transform: translateY(-2px); + transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease; +} + +.hi-warning-wrap:hover .hi-warning-tooltip { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.hi-warning-title { + font-size: 11px; + font-weight: 700; + margin-bottom: 4px; +} + +.hi-warning-item { + font-size: 11px; +} + + +.hi-body { + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.hi-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.hi-column { + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.hi-column-title { + font-size: 11px; + font-weight: 700; + color: #9a3412; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.hi-column-title-right { + text-align: right; +} + +.hi-row { + display: grid; + align-items: center; + min-width: 0; + height: 24px; +} + +.hi-row-input { + grid-template-columns: 16px 1fr; + gap: 6px; +} + +.hi-row-output { + grid-template-columns: 1fr 16px; + gap: 6px; +} + +.hi-socket { + position: relative; + z-index: 20; +} + +.hi-socket-right { + justify-self: end; +} + +.hi-pill { + height: 24px; + border-radius: 7px; + font-size: 11px; + font-weight: 600; + padding: 0 8px; + display: inline-flex; + align-items: center; + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.hi-pill-input { + color: #7c2d12; + background: #ffedd5; + border: 1px solid #fed7aa; + justify-content: flex-start; +} + +.hi-pill-output { + color: #9f1239; + background: #fff1f2; + border: 1px solid #fecdd3; + justify-content: flex-end; +} + +.hi-panel { + border: 1px solid #fdba74; + border-radius: 10px; + background: #fff7ed; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.hi-panel-simulated { + border-color: #f9a8d4; + background: #fff1f2; +} + +.hi-panel-title-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.hi-panel-title { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.03em; + color: #c2410c; + font-weight: 700; +} + +.hi-panel-text { + font-size: 11px; + color: #431407; + white-space: pre-wrap; + word-break: break-word; +} + +.hi-meta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; +} + +.hi-meta-chip { + border: 1px solid #fdba74; + background: #fff; + border-radius: 7px; + padding: 4px 6px; + color: #7c2d12; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.hi-meta-chip-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.hi-meta-chip-value { + font-size: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.hi-edit-btn { + border: none; + background: transparent; + color: #9a3412; + border-radius: 999px; + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: color 0.15s ease, background-color 0.15s ease; +} + +.hi-edit-btn:hover { + color: #ea580c; + background: rgba(234, 88, 12, 0.12); +} + +.hi-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(124, 45, 18, 0.4); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 12px; +} + +.hi-modal { + width: min(360px, calc(100vw - 24px)); + border-radius: 12px; + border: 1px solid #fdba74; + background: #fff; + box-shadow: 0 16px 40px rgba(124, 45, 18, 0.25); + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.hi-modal-title { + font-size: 13px; + font-weight: 700; + color: #7c2d12; +} + +.hi-modal-field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.hi-modal-field label { + font-size: 11px; + font-weight: 600; + color: #9a3412; +} + +.hi-modal-field input, +.hi-modal-field select { + border: 1px solid #fdba74; + border-radius: 8px; + font-size: 12px; + color: #431407; + padding: 7px 8px; +} + +.hi-modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.hi-btn { + border-radius: 8px; + padding: 6px 10px; + font-size: 12px; + font-weight: 600; + border: 1px solid transparent; + cursor: pointer; +} + +.hi-btn-ghost { + border-color: #fdba74; + color: #9a3412; + background: #fff; +} + +.hi-btn-primary { + background: #ea580c; + color: #fff; +} diff --git a/src/app/shared/nodes/human-interaction/human-interaction.html b/src/app/shared/nodes/human-interaction/human-interaction.html new file mode 100644 index 0000000..941f2ea --- /dev/null +++ b/src/app/shared/nodes/human-interaction/human-interaction.html @@ -0,0 +1,136 @@ +
+
+
+ +
+
+ Human Task +
+ {{ name }} + +
+
+ @if (missingRequiredParams.length) { +
+
+ +
+
+
Missing required fields
+ @for (missing of missingRequiredParams; track missing) { +
{{ missing }}
+ } +
+
+ } +
+ +
+
+
+
Inputs
+ @for (input of inputs; track input.key) { +
+
+
+ {{ input.key }} +
+ } +
+ +
+
Outputs
+ @for (output of outputs; track output.key) { +
+ {{ output.key }} +
+
+
+ } +
+
+ +
+
+
Action
+ +
+
{{ actionDescription || '-' }}
+
+ +
+
Simulated with
+
+ + + Provider + + + {{ provider || '-' }} + + + + Model + + + {{ model || '-' }} + +
+
+
+ + @if (localEditorOpen) { +
+
+
Edit {{ localEditorLabel }}
+
+ + @if (localEditorLoading) { + + } @else if (localEditorKey !== 'name' && localEditorOptions.length) { + + } @else { + + } +
+
+ + +
+
+
+ } +
diff --git a/src/app/shared/nodes/human-interaction/human-interaction.ts b/src/app/shared/nodes/human-interaction/human-interaction.ts new file mode 100644 index 0000000..bf5e4af --- /dev/null +++ b/src/app/shared/nodes/human-interaction/human-interaction.ts @@ -0,0 +1,232 @@ +import { CommonModule } from '@angular/common'; +import { Component, HostBinding, inject, Input } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ClassicPreset } from 'rete'; +import { ReteModule } from 'rete-angular-plugin/21'; +import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog'; +import { EditorStateHolder } from '@stores/flow-editor'; +import { FieldRetreiver } from '@services/retreiver/field-retreiver'; +import { firstValueFrom } from 'rxjs'; + +@Component({ + selector: 'app-human-interaction-node', + imports: [CommonModule, FormsModule, ReteModule], + templateUrl: './human-interaction.html', + styleUrl: './human-interaction.css', + host: { + 'data-testid': 'node' + } +}) +export class HumanInteractionNodeComponent { + private settingsDialog = inject(NodeSettingsDialogService); + private editorState = inject(EditorStateHolder); + private fieldRetreiver = inject(FieldRetreiver); + + @Input() data!: any; + @Input() emit!: (data: any) => void; + @Input() rendered!: () => void; + + @HostBinding('class.selected') get selected() { + return this.data.selected; + } + + outputs: { key: string; socket: ClassicPreset.Socket }[] = []; + inputs: { key: string; socket: ClassicPreset.Socket }[] = []; + + name = 'Human Interaction'; + actionDescription = ''; + provider: string | null = null; + model: string | null = null; + localEditorOpen = false; + localEditorKey: 'provider' | 'model' | 'name' | null = null; + localEditorLabel = ''; + localEditorValue = ''; + localEditorOptions: string[] = []; + localEditorLoading = false; + missingRequiredParams: string[] = []; + + ngOnInit() { + this.outputs = []; + this.inputs = []; + + for (const [key, output] of Object.entries(this.data.outputs ?? {})) { + this.outputs.push({ key, socket: (output as any).socket }); + } + + for (const [key, input] of Object.entries(this.data.inputs ?? {})) { + this.inputs.push({ key, socket: (input as any).socket }); + } + + const config = this.data?.data?.specificConfiguration ?? {}; + this.name = (config?.name as string) || this.name; + this.actionDescription = (config?.actionDescription as string) || ''; + this.provider = (config?.llmDescriptor?.provider as string) || null; + this.model = (config?.llmDescriptor?.model as string) || null; + this.refreshValidationState(); + } + + ngAfterViewInit() { + this.rendered(); + } + + async openSimpleParamEditor(key: 'provider' | 'model' | 'name', event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + this.localEditorKey = key; + this.localEditorLabel = + key === 'provider' ? 'Provider' : + key === 'model' ? 'Model' : 'Name'; + this.localEditorValue = + key === 'provider' ? (this.provider ?? '') : + key === 'model' ? (this.model ?? '') : + (this.name ?? ''); + this.localEditorOptions = []; + this.localEditorLoading = key !== 'name'; + this.localEditorOpen = true; + if (key !== 'name') { + await this.loadSimpleEditorOptions(); + } + } + + closeSimpleParamEditor(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + this.localEditorOpen = false; + this.localEditorKey = null; + this.localEditorOptions = []; + this.localEditorLoading = false; + } + + saveSimpleParamEditor(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + if (!this.localEditorKey) return; + + const config = this.ensureBlockConfiguration(); + const descriptor = (config['llmDescriptor'] ??= {}); + const value = this.localEditorValue.trim(); + descriptor[this.localEditorKey] = value; + + if (this.localEditorKey === 'provider') { + this.provider = value || null; + this.model = null; + descriptor['model'] = ''; + } + if (this.localEditorKey === 'model') { + this.model = value || null; + } + if (this.localEditorKey === 'name') { + const nameValue = value.slice(0, 20); + config['name'] = nameValue; + this.name = nameValue || this.name; + } + + this.refreshValidationState(); + this.markFlowDirty(); + this.localEditorOpen = false; + this.localEditorKey = null; + this.localEditorOptions = []; + this.localEditorLoading = false; + } + + async openActionEditor(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + const result = await this.settingsDialog.open({ + title: `Action for "${this.name}"`, + fields: [ + { + key: 'actionDescription', + label: 'Action description', + type: 'textarea', + rows: 8, + placeholder: 'Describe the human task...' + } + ], + initial: { actionDescription: this.actionDescription ?? '' } + }); + if (!result) return; + + const config = this.ensureBlockConfiguration(); + config['actionDescription'] = String(result['actionDescription'] ?? ''); + this.actionDescription = config['actionDescription'] || ''; + this.refreshValidationState(); + this.markFlowDirty(); + } + + openNameEditor(event?: Event) { + this.openSimpleParamEditor('name', event); + } + + private ensureBlockConfiguration(): Record { + if (!this.data?.data) { + this.data.data = {}; + } + if (!this.data.data.specificConfiguration) { + this.data.data.specificConfiguration = {}; + } + return this.data.data.specificConfiguration; + } + + private markFlowDirty() { + const flow = this.editorState.currentFlow(); + if (!flow) return; + this.editorState.updateData(flow.data); + } + + private async loadSimpleEditorOptions() { + const key = this.localEditorKey; + const blockType = this.blockType; + if (!key || !blockType) { + this.localEditorLoading = false; + return; + } + + const retrieverKey = key === 'provider' ? 'providers' : 'models'; + const context = key === 'model' && this.provider + ? { provider: this.provider } + : undefined; + + try { + const options = await firstValueFrom( + this.fieldRetreiver.retrieveValues(blockType, retrieverKey, context) + ); + this.localEditorOptions = options ?? []; + } catch { + this.localEditorOptions = []; + } finally { + this.localEditorLoading = false; + } + } + + private get blockType(): string | null { + const typeName = this.data?.data?.typeName; + return typeof typeName === 'string' && typeName.length > 0 ? typeName : null; + } + + private refreshValidationState() { + const config = this.data?.data?.specificConfiguration ?? {}; + const requiredFields: Array<{ path: string; label: string }> = [ + { path: 'actionDescription', label: 'Action description' }, + { path: 'llmDescriptor.provider', label: 'Provider' }, + { path: 'llmDescriptor.model', label: 'Model' } + ]; + + this.missingRequiredParams = requiredFields + .filter((field) => this.isMissingValue(this.getByPath(config, field.path))) + .map((field) => field.label); + } + + private getByPath(source: Record, path: string): unknown { + return path.split('.').reduce((acc, key) => { + if (acc == null || typeof acc !== 'object') return undefined; + return (acc as Record)[key]; + }, source); + } + + private isMissingValue(value: unknown): boolean { + if (value == null) return true; + if (typeof value === 'string') return value.trim().length === 0; + return false; + } +} diff --git a/src/app/shared/nodes/input/input-node.component.ts b/src/app/shared/nodes/input/input-node.component.ts index 3017b9a..430f97a 100644 --- a/src/app/shared/nodes/input/input-node.component.ts +++ b/src/app/shared/nodes/input/input-node.component.ts @@ -3,7 +3,7 @@ import { Input, HostBinding, } from "@angular/core"; -import { CommonModule, KeyValue } from "@angular/common"; +import { CommonModule } from "@angular/common"; import { ReteModule } from "rete-angular-plugin/21"; import { ClassicPreset } from "rete"; diff --git a/src/app/shared/nodes/llm/llm.css b/src/app/shared/nodes/llm/llm.css index 4852ae0..ca440cc 100644 --- a/src/app/shared/nodes/llm/llm.css +++ b/src/app/shared/nodes/llm/llm.css @@ -1,4 +1,5 @@ .llm-node { + position: relative; width: 280px; border-radius: 16px; border: 1px solid #dbe2ea; @@ -44,6 +45,27 @@ line-height: 1.2; } +.llm-subtitle-row { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.llm-warning { + width: 24px; + height: 24px; + border-radius: 999px; + border: 1px solid #fecaca; + background: #dc2626; + color: #fff7ed; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 13px; + box-shadow: 0 0 0 2px rgba(127, 29, 29, 0.18); +} + .llm-title { font-size: 13px; font-weight: 700; @@ -59,6 +81,66 @@ overflow: hidden; } +.llm-name-edit-btn { + border: none; + background: rgba(255, 255, 255, 0.2); + color: #eff6ff; + border-radius: 999px; + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: background-color 0.15s ease; +} + +.llm-name-edit-btn:hover { + background: rgba(255, 255, 255, 0.3); +} + +.llm-warning-wrap { + position: relative; + margin-left: auto; +} + +.llm-warning-tooltip { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 180px; + max-width: 240px; + border: 1px solid #fecaca; + border-radius: 8px; + background: #fff1f2; + color: #7f1d1d; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.2); + padding: 8px; + z-index: 80; + opacity: 0; + visibility: hidden; + transform: translateY(-2px); + transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s ease; +} + +.llm-warning-wrap:hover .llm-warning-tooltip { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.llm-warning-title { + font-size: 11px; + font-weight: 700; + margin-bottom: 4px; +} + +.llm-warning-item { + font-size: 11px; +} + + .llm-body { padding: 12px; display: grid; @@ -319,6 +401,7 @@ } .llm-modal-field input, +.llm-modal-field select, .llm-modal-field textarea { border: 1px solid #cbd5e1; border-radius: 8px; diff --git a/src/app/shared/nodes/llm/llm.html b/src/app/shared/nodes/llm/llm.html index ff51385..e44dd0d 100644 --- a/src/app/shared/nodes/llm/llm.html +++ b/src/app/shared/nodes/llm/llm.html @@ -5,8 +5,26 @@
LLM Node - {{ name }} +
+ {{ name }} + +
+ @if (missingRequiredParams.length) { +
+
+ +
+
+
Missing required fields
+ @for (missing of missingRequiredParams; track missing) { +
{{ missing }}
+ } +
+
+ }
@@ -54,10 +72,6 @@
-
-
Parameters
-
-
@@ -109,7 +123,18 @@
Edit {{ localEditorLabel }}
- + @if (localEditorLoading) { + + } @else if (localEditorKey !== 'name' && localEditorOptions.length) { + + } @else { + + }
diff --git a/src/app/shared/nodes/llm/llm.spec.ts b/src/app/shared/nodes/llm/llm.spec.ts index b7a6c7d..3888e46 100644 --- a/src/app/shared/nodes/llm/llm.spec.ts +++ b/src/app/shared/nodes/llm/llm.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Llm } from './llm'; +import { LLMNodeComponent } from './llm'; -describe('Llm', () => { - let component: Llm; - let fixture: ComponentFixture; +describe('LLMNodeComponent', () => { + let component: LLMNodeComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Llm] + imports: [LLMNodeComponent] }) .compileComponents(); - fixture = TestBed.createComponent(Llm); + fixture = TestBed.createComponent(LLMNodeComponent); component = fixture.componentInstance; await fixture.whenStable(); }); diff --git a/src/app/shared/nodes/llm/llm.ts b/src/app/shared/nodes/llm/llm.ts index a99aed9..60c3f14 100644 --- a/src/app/shared/nodes/llm/llm.ts +++ b/src/app/shared/nodes/llm/llm.ts @@ -4,6 +4,9 @@ import { FormsModule } from '@angular/forms'; import { ClassicPreset } from 'rete'; import { ReteModule } from 'rete-angular-plugin/21'; import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog'; +import { EditorStateHolder } from '@stores/flow-editor'; +import { FieldRetreiver } from '@services/retreiver/field-retreiver'; +import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-llm', @@ -17,6 +20,8 @@ import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialo export class LLMNodeComponent { private settingsDialog = inject(NodeSettingsDialogService); + private editorState = inject(EditorStateHolder); + private fieldRetreiver = inject(FieldRetreiver); @Input() data!: any; @Input() emit!: (data: any) => void; @@ -39,9 +44,12 @@ export class LLMNodeComponent { model: string | null = null; name: string = 'noName'; localEditorOpen = false; - localEditorKey: 'provider' | 'model' | null = null; + localEditorKey: 'provider' | 'model' | 'name' | null = null; localEditorLabel = ''; localEditorValue = ''; + localEditorOptions: string[] = []; + localEditorLoading = false; + missingRequiredParams: string[] = []; ngOnInit() { this.outputs = []; @@ -80,19 +88,31 @@ export class LLMNodeComponent { if (value == null || typeof value === 'object') return; this.parameterEntries.push({ key, value: String(value) }); }); + + this.refreshValidationState(); } ngAfterViewInit() { this.rendered(); } - openSimpleParamEditor(key: 'provider' | 'model', event?: Event) { + async openSimpleParamEditor(key: 'provider' | 'model' | 'name', event?: Event) { event?.preventDefault(); event?.stopPropagation(); this.localEditorKey = key; - this.localEditorLabel = key === 'provider' ? 'Provider' : 'Model'; - this.localEditorValue = key === 'provider' ? (this.provider ?? '') : (this.model ?? ''); + this.localEditorLabel = + key === 'provider' ? 'Provider' : + key === 'model' ? 'Model' : 'Name'; + this.localEditorValue = + key === 'provider' ? (this.provider ?? '') : + key === 'model' ? (this.model ?? '') : + (this.name ?? ''); + this.localEditorOptions = []; + this.localEditorLoading = key !== 'name'; this.localEditorOpen = true; + if (key !== 'name') { + await this.loadSimpleEditorOptions(); + } } closeSimpleParamEditor(event?: Event) { @@ -100,6 +120,8 @@ export class LLMNodeComponent { event?.stopPropagation(); this.localEditorOpen = false; this.localEditorKey = null; + this.localEditorOptions = []; + this.localEditorLoading = false; } saveSimpleParamEditor(event?: Event) { @@ -114,13 +136,25 @@ export class LLMNodeComponent { if (this.localEditorKey === 'provider') { this.provider = value || null; + this.model = null; + descriptor['model'] = ''; } if (this.localEditorKey === 'model') { this.model = value || null; } + if (this.localEditorKey === 'name') { + const nameValue = value.slice(0, 20); + config['name'] = nameValue; + this.name = nameValue || this.name; + } + + this.refreshValidationState(); + this.markFlowDirty(); this.localEditorOpen = false; this.localEditorKey = null; + this.localEditorOptions = []; + this.localEditorLoading = false; } async openPromptEditor(event?: Event) { @@ -146,6 +180,12 @@ export class LLMNodeComponent { config['prompt'] = String(result['prompt'] ?? ''); this.prompt = config['prompt'] || null; this.promptParts = this.splitPromptParts(this.prompt); + this.refreshValidationState(); + this.markFlowDirty(); + } + + openNameEditor(event?: Event) { + this.openSimpleParamEditor('name', event); } private get blockConfiguration(): Record | null { @@ -167,6 +207,68 @@ export class LLMNodeComponent { return null; } + private markFlowDirty() { + const flow = this.editorState.currentFlow(); + if (!flow) return; + this.editorState.updateData(flow.data); + } + + private async loadSimpleEditorOptions() { + const key = this.localEditorKey; + const blockType = this.blockType; + if (!key || !blockType) { + this.localEditorLoading = false; + return; + } + + const retrieverKey = key === 'provider' ? 'providers' : 'models'; + const context = key === 'model' && this.provider + ? { provider: this.provider } + : undefined; + + try { + const options = await firstValueFrom( + this.fieldRetreiver.retrieveValues(blockType, retrieverKey, context) + ); + this.localEditorOptions = options ?? []; + } catch { + this.localEditorOptions = []; + } finally { + this.localEditorLoading = false; + } + } + + private get blockType(): string | null { + const typeName = this.data?.data?.typeName; + return typeof typeName === 'string' && typeName.length > 0 ? typeName : null; + } + + private refreshValidationState() { + const config = this.blockConfiguration ?? {}; + const requiredFields: Array<{ path: string; label: string }> = [ + { path: 'prompt', label: 'Prompt' }, + { path: 'llmDescriptor.provider', label: 'Provider' }, + { path: 'llmDescriptor.model', label: 'Model' } + ]; + + this.missingRequiredParams = requiredFields + .filter((field) => this.isMissingValue(this.getByPath(config, field.path))) + .map((field) => field.label); + } + + private getByPath(source: Record, path: string): unknown { + return path.split('.').reduce((acc, key) => { + if (acc == null || typeof acc !== 'object') return undefined; + return (acc as Record)[key]; + }, source); + } + + private isMissingValue(value: unknown): boolean { + if (value == null) return true; + if (typeof value === 'string') return value.trim().length === 0; + return false; + } + private splitPromptParts(prompt: string | null): { text: string; isDynamicInput: boolean }[] { if (!prompt) return []; diff --git a/src/app/shared/rete-editor/rete-editor.css b/src/app/shared/rete-editor/rete-editor.css index e69de29..6e66f3a 100644 --- a/src/app/shared/rete-editor/rete-editor.css +++ b/src/app/shared/rete-editor/rete-editor.css @@ -0,0 +1,10 @@ +:host ::ng-deep [data-testid="node"] { + user-select: none; + -webkit-user-select: none; +} + +:host ::ng-deep [data-testid="node"] input, +:host ::ng-deep [data-testid="node"] textarea { + user-select: text; + -webkit-user-select: text; +} diff --git a/src/app/shared/rete-editor/rete-editor.ts b/src/app/shared/rete-editor/rete-editor.ts index d51ca36..2e6bf34 100644 --- a/src/app/shared/rete-editor/rete-editor.ts +++ b/src/app/shared/rete-editor/rete-editor.ts @@ -1,8 +1,10 @@ -import { Component, ElementRef, Injector, output, ViewChild } from '@angular/core'; +import { Component, ElementRef, Injector, input, OnChanges, OnDestroy, output, SimpleChanges, ViewChild } from '@angular/core'; import { BlockType, FlowBlock, FlowData } from '@models/flow'; +import { BlocksService } from '@services/blocks/blocks'; import { BLOCK_TYPE_DRAG_MIME } from '@shared/blocks-list/block-drag'; import { EditorStateHolder } from '@stores/flow-editor'; -import { addBlockToEditor, createEditor, ReteEditorInstance } from '@utilities/rete-editor'; +import { addBlockToEditor, createEditor, exportGraph, ReteEditorInstance } from '@utilities/rete-editor'; +import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-rete-editor', @@ -10,37 +12,48 @@ import { addBlockToEditor, createEditor, ReteEditorInstance } from '@utilities/r templateUrl: './rete-editor.html', styleUrl: './rete-editor.css', }) -export class ReteEditor { +export class ReteEditor implements OnChanges, OnDestroy { + readonly flowData = input.required(); + readonly flowId = input.required(); - flowData: FlowData; - - constructor(private injector: Injector, private flowState: EditorStateHolder ) { - this.flowData = this.flowState.currentFlow()!.data; - } + constructor( + private injector: Injector, + private flowState: EditorStateHolder, + private blocksService: BlocksService + ) {} @ViewChild("editor") container!: ElementRef; private rete?: ReteEditorInstance; + private viewReady = false; + private loadVersion = 0; + private readonly dirtyEventTypes = new Set([ + 'nodecreated', + 'noderemoved', + 'connectioncreated', + 'connectionremoved' + ]); flowChanged = output(); ngAfterViewInit(): void { - const el = this.container.nativeElement; + this.viewReady = true; + void this.reloadEditor(); + } - if (el) { - createEditor(el, this.injector, this.flowData).then((rete) => { - this.rete = rete; - rete.editor.addPipe( - (context) => { - this.flowChanged.emit({}); - return context; - } - ); - }); + ngOnChanges(changes: SimpleChanges): void { + if (!this.viewReady) return; + if (changes['flowId']) { + void this.reloadEditor(); } } + ngOnDestroy(): void { + this.rete?.area.destroy(); + this.rete = undefined; + } + onDragOver(event: DragEvent) { event.preventDefault(); if (event.dataTransfer) { @@ -55,10 +68,54 @@ export class ReteEditor { const blockType: BlockType = JSON.parse(payload); const position = this.getDropPosition(event); - const newBlock = this.createBlockFromType(blockType, position); + let newBlock: FlowBlock; + try { + newBlock = await firstValueFrom(this.blocksService.createEmptyBlock(blockType.type)); + } catch (error) { + console.error('Failed to create empty block', error); + return; + } + + newBlock = { + ...newBlock, + position + }; await addBlockToEditor(this.rete.editor, this.rete.area, newBlock, position); - this.flowChanged.emit({}); + const updatedData = exportGraph(this.rete.editor); + this.flowState.updateData(updatedData); + this.flowChanged.emit(updatedData); + } + + private async reloadEditor() { + const host = this.container?.nativeElement as HTMLElement | undefined; + if (!host) return; + + const currentVersion = ++this.loadVersion; + this.rete?.area.destroy(); + this.rete = undefined; + host.innerHTML = ''; + + const rete = await createEditor(host, this.injector, this.flowData()); + if (currentVersion !== this.loadVersion) { + rete.area.destroy(); + return; + } + + this.rete = rete; + rete.editor.addPipe((context) => { + if (this.dirtyEventTypes.has(context.type)) { + this.markFlowChanged(rete, context); + } + return context; + }); + + rete.area.addPipe((context: any) => { + if (context?.type === 'nodetranslated') { + this.markFlowChanged(rete, context); + } + return context; + }); } private getDropPosition(event: DragEvent) { @@ -71,52 +128,23 @@ export class ReteEditor { return { x, y }; } - private createBlockFromType(blockType: BlockType, position: { x: number; y: number }): FlowBlock { - const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}`; - const common = { - id, - sink: false, - name: blockType.name, - position, - typeName: blockType.name - } as const; + private syncNodePositionFromContext(rete: ReteEditorInstance, context: any) { + if (context?.type !== 'nodetranslated') return; - if (blockType.name === 'SourceBlock') { - return { - ...common, - inputs: [], - outputs: [{ name: 'output', type: 'TEXT', multiple: false }], - specificConfiguration: {} - }; - } + const movedNode = rete.editor.getNode(context?.data?.id) as any; + const pos = context?.data?.position; + if (!movedNode?.data || !pos) return; - if (blockType.name === 'HumanInteractionBlock') { - return { - ...common, - sink: true, - inputs: [{ name: 'input', type: 'TEXT', multiple: false }], - outputs: [{ name: 'output', type: 'TEXT', multiple: false }], - specificConfiguration: { - type: 'HumanInteractiveBlockConfiguration', - name: blockType.name, - actionDescription: 'Human task', - llmDescriptor: { provider: 'default', model: 'default' }, - inputAsList: false, - outputAsList: false - } - }; - } - - return { - ...common, - inputs: [{ name: 'input', type: 'TEXT', multiple: false }], - outputs: [{ name: 'output', type: 'TEXT', multiple: false }], - specificConfiguration: { - type: 'LLMBlockConfiguration', - name: blockType.name, - llmDescriptor: { provider: 'default', model: 'default' }, - prompt: '' - } + movedNode.data = { + ...movedNode.data, + position: { x: pos.x, y: pos.y } }; } + + private markFlowChanged(rete: ReteEditorInstance, context: any) { + this.syncNodePositionFromContext(rete, context); + const updatedData = exportGraph(rete.editor); + this.flowState.updateData(updatedData); + this.flowChanged.emit(updatedData); + } } diff --git a/src/app/shared/sockets/input/input.ts b/src/app/shared/sockets/input/input.ts index 724ce41..648a3b6 100644 --- a/src/app/shared/sockets/input/input.ts +++ b/src/app/shared/sockets/input/input.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, ElementRef, HostBinding, Input } from "@angular/core"; +import { ChangeDetectorRef, Component, HostBinding, Input } from "@angular/core"; import { ReteModule } from "rete-angular-plugin/21"; @Component({ @@ -41,4 +41,4 @@ export class InputSocket { 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 69c8c46..3b9fa8d 100644 --- a/src/app/shared/sockets/output/output.ts +++ b/src/app/shared/sockets/output/output.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, HostBinding, HostListener, Input } from '@angular/core'; +import { ChangeDetectorRef, Component, HostBinding, Input } from '@angular/core'; import { ReteModule } from 'rete-angular-plugin/21'; @Component({ diff --git a/src/app/shared/title-toolbar/title-toolbar.html b/src/app/shared/title-toolbar/title-toolbar.html index 4b99101..2067dc2 100644 --- a/src/app/shared/title-toolbar/title-toolbar.html +++ b/src/app/shared/title-toolbar/title-toolbar.html @@ -9,15 +9,13 @@

diff --git a/src/app/shared/title-toolbar/title-toolbar.ts b/src/app/shared/title-toolbar/title-toolbar.ts index 34ee4bd..0f877f1 100644 --- a/src/app/shared/title-toolbar/title-toolbar.ts +++ b/src/app/shared/title-toolbar/title-toolbar.ts @@ -1,4 +1,4 @@ -import { Component, computed, ElementRef, inject, linkedSignal, ViewChild, WritableSignal } from '@angular/core'; +import { Component, computed, ElementRef, inject, ViewChild } from '@angular/core'; import { EditorStateHolder } from '@stores/flow-editor'; @Component({ @@ -17,22 +17,10 @@ export class TitleToolbar { return flow ? flow.name : 'No Flow Opened'; }); - tempTitle: WritableSignal = linkedSignal(() =>null); - - titleWidth = computed(() => { - const temp = this.tempTitle(); - if (temp !== null) { - return Math.max(temp.length-1, 4) - } - const len = this.title().length || 4; - return Math.min(len, 20)-2; - }); - notSaved = computed(() => this.editorState.isDirty()); changeTitle(value: string) { - this.tempTitle.set(null); const trimmed = value.trim(); if (value === this.title()) return; if (trimmed.length < 4) { @@ -40,7 +28,6 @@ export class TitleToolbar { return; } console.log('Updating flow title to:', trimmed); - const flow = this.editorState.currentFlow()!; this.editorState.updateFlowTitle(trimmed ); } diff --git a/src/app/stores/flow-editor.spec.ts b/src/app/stores/flow-editor.spec.ts index d9d27ab..6bf825d 100644 --- a/src/app/stores/flow-editor.spec.ts +++ b/src/app/stores/flow-editor.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { EditorStateHolder as Editor } from './editor'; +import { EditorStateHolder } from './flow-editor'; -describe('Editor', () => { - let service: Editor; +describe('EditorStateHolder', () => { + let service: EditorStateHolder; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(Editor); + service = TestBed.inject(EditorStateHolder); }); it('should be created', () => { diff --git a/src/app/stores/flow-editor.ts b/src/app/stores/flow-editor.ts index b98b907..4b07fa4 100644 --- a/src/app/stores/flow-editor.ts +++ b/src/app/stores/flow-editor.ts @@ -2,7 +2,6 @@ import { computed, inject, Injectable, signal } from '@angular/core'; import { Flow, FlowData } from '@models/flow'; import { ConfirmDialogService } from '@services/dialogs/confirm-dialog'; import { FlowsService } from '@services/flows/flows'; -import { FlowsCallService } from '@services/flows/flows-call'; import { tap } from 'rxjs'; @Injectable({ providedIn: 'root' }) @@ -27,8 +26,8 @@ export class EditorStateHolder { undoEnabled = computed(() => this.previousDataStack.length > 1); /** Intent: open document */ - async openDocument(doc: Flow): Promise { - if (this.isDirty()) { + async openDocument(doc: Flow, options?: { skipDirtyCheck?: boolean }): Promise { + if (this.isDirty() && !options?.skipDirtyCheck) { const confirmed = await this.confirm.open( 'You have unsaved changes. Open another document?' ); @@ -104,4 +103,4 @@ export class EditorStateHolder { } -} \ No newline at end of file +} diff --git a/src/app/stores/list-state.ts b/src/app/stores/list-state.ts index 9c0abdc..69ee180 100644 --- a/src/app/stores/list-state.ts +++ b/src/app/stores/list-state.ts @@ -1,4 +1,4 @@ -import { Injectable, Signal, signal } from '@angular/core'; +import { Injectable } from '@angular/core'; import { ListView, OrderViewState } from '@utilities/list-state-holder'; diff --git a/src/app/utilities/form-utility.ts b/src/app/utilities/form-utility.ts index f43b6d7..567ac0c 100644 --- a/src/app/utilities/form-utility.ts +++ b/src/app/utilities/form-utility.ts @@ -1,5 +1,4 @@ -import { NgModel } from "@angular/forms"; -import { FieldState } from "@angular/forms/signals"; +import { FieldState } from "@angular/forms/signals"; export class FormUtility { isInvalid(input: FieldState) { diff --git a/src/app/utilities/list-state-holder.ts b/src/app/utilities/list-state-holder.ts index b4d9c72..3ea8330 100644 --- a/src/app/utilities/list-state-holder.ts +++ b/src/app/utilities/list-state-holder.ts @@ -1,4 +1,4 @@ -import { inject, signal, Signal } from "@angular/core"; +import { inject, Signal } from "@angular/core"; import { ListState } from "@stores/list-state"; export class OrderViewState { @@ -35,4 +35,4 @@ export class ListStateViewHolder { this.state = new ListView(); } -} \ No newline at end of file +} diff --git a/src/app/utilities/rete-editor.ts b/src/app/utilities/rete-editor.ts index 6cf978c..22e88e1 100644 --- a/src/app/utilities/rete-editor.ts +++ b/src/app/utilities/rete-editor.ts @@ -11,6 +11,7 @@ import { OutputNodeComponent } from "@shared/nodes/output/output-node.component" import { HFNode, HFSchemes } from "@models/nodes"; import { FlowBlock, FlowData } from "@models/flow"; import { LLMNodeComponent } from "@shared/nodes/llm/llm"; +import { HumanInteractionNodeComponent } from "@shared/nodes/human-interaction/human-interaction"; import { CustomSocket } from "@shared/custom-socket/custom-socket"; type AreaExtra = AngularArea2D; @@ -44,6 +45,9 @@ export async function createEditor( if (context.payload.label === "Output") { return OutputNodeComponent; } + if (context.payload.label === "HumanInteractionBlock") { + return HumanInteractionNodeComponent; + } return LLMNodeComponent; }, socket() { @@ -190,4 +194,3 @@ function toNodeLabel(typeName: string) { return typeName; } - diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index 83b8ee4..12dbfa0 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -1,11 +1,13 @@ import { AuthorizationCallFakeService } from "@services/authorization/authorization-call.fake"; import { BlocksCallServiceFake } from "@services/blocks/blocks-call.fake"; import { FlowsCallServiceFake } from "@services/flows/flows-call.fake"; +import { FieldRetreiverCallServiceFake } from "@services/retreiver/field-retreiver-call.fake"; export const environment = { production: false, apiUrl: 'http://localhost:8080', authorizationCallService: AuthorizationCallFakeService, // Assign the appropriate service here flowsCallService: FlowsCallServiceFake, - blocksCallService: BlocksCallServiceFake + blocksCallService: BlocksCallServiceFake, + fieldRetreiverCallService: FieldRetreiverCallServiceFake }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 7becd89..d41d25e 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,11 +1,13 @@ import { AuthorizationCallService } from "@services/authorization/authorization-call"; import { BlocksCallService } from "@services/blocks/blocks-call"; import { FlowsCallService } from "@services/flows/flows-call"; +import { FieldRetreiverCallService } from "@services/retreiver/field-retreiver-call"; export const environment = { apiUrl: '/api', production: true, authorizationCallService: AuthorizationCallService, flowsCallService: FlowsCallService, - blocksCallService: BlocksCallService + blocksCallService: BlocksCallService, + fieldRetreiverCallService: FieldRetreiverCallService };