parent
68635649f8
commit
79b85e2bd5
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<app-title-toolbar></app-title-toolbar>
|
||||
<!-- editor canvas -->
|
||||
<div class="flex-1 h-full">
|
||||
<app-rete-editor></app-rete-editor>
|
||||
<app-rete-editor [flowId]="flow()!.id" [flowData]="flow()!.data"></app-rete-editor>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
|
|
@ -30,4 +30,4 @@
|
|||
<!-- <app-right-panel></app-right-panel> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,11 +20,15 @@ export type FlowData = {
|
|||
|
||||
export type BlockTypeName = "HumanInteractionBlock" | "LLMBlock" | "SourceBlock" | string;
|
||||
|
||||
export type BlockTypeSchema = Record<string, unknown> | 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 = {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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<User> {
|
||||
override login(_username: string, _password: string): Observable<User> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override register(userRegistration: UserRegistration): Observable<void> {
|
||||
override register(_userRegistration: UserRegistration): Observable<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<BlockType[]>;
|
||||
|
||||
abstract createNewBlock(configuration : any) : Observable<FlowBlock>;
|
||||
abstract createEmptyBlock(blockType: BlockTypeName) : Observable<FlowBlock>;
|
||||
|
||||
abstract updateBlock(blockId : string, configuration : any) : Observable<FlowBlock>;
|
||||
|
||||
}
|
||||
|
|
@ -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<BlockType[]> {
|
||||
return of(this.blockTypes);
|
||||
}
|
||||
|
||||
override createNewBlock(configuration: any): Observable<FlowBlock> {
|
||||
const typeName = configuration?.typeName ?? "LLMBlock";
|
||||
override createEmptyBlock(blockType: string): Observable<FlowBlock> {
|
||||
const descriptor = this.blockTypes.find((b) => b.type === blockType);
|
||||
const typeName = descriptor?.type ?? blockType ?? "LLMBlock";
|
||||
const schema = descriptor?.schema as Record<string, any> | 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<FlowBlock> {
|
||||
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<string, any> = {};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
override createNewBlock(configuration: any): Observable<FlowBlock> {
|
||||
override createEmptyBlock(_blockType: string): Observable<FlowBlock> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
override updateBlock(_blockId: string, _configuration: any): Observable<FlowBlock> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
} ]
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@ import { FlowsCallServiceBase } from "./flows-call.base";
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
export class FlowsCallService extends FlowsCallServiceBase {
|
||||
override getFlowById(flowId: string): Observable<Flow> {
|
||||
override getFlowById(_flowId: string): Observable<Flow> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override createNewFlow(name?: string): Observable<Flow> {
|
||||
override createNewFlow(_name?: string): Observable<Flow> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override deleteFlow(flowId: string): Observable<void> {
|
||||
override deleteFlow(_flowId: string): Observable<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override retrieveAllFlows(): Observable<Flow[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
override updateFlow(flow: Flow): Observable<void> {
|
||||
override updateFlow(_flow: Flow): Observable<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class FieldRetreiverCallServiceBase {
|
||||
abstract retrieveValues(
|
||||
blockType: string,
|
||||
key: string,
|
||||
context?: Record<string, string>
|
||||
): Observable<string[]>;
|
||||
}
|
||||
|
|
@ -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<string, string[]> = {
|
||||
LLMBlock: ["OpenAI", "Anthropic", "OllamaTestProvider"],
|
||||
HumanInteractionBlock: ["OpenAI", "OllamaTestProvider"]
|
||||
};
|
||||
|
||||
private readonly modelsByProvider: Record<string, string[]> = {
|
||||
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<string, string>
|
||||
): Observable<string[]> {
|
||||
if (key === "providers") {
|
||||
return of(this.providersByBlockType[blockType] ?? []);
|
||||
}
|
||||
|
||||
if (key === "models") {
|
||||
const provider = context?.["provider"];
|
||||
return of(provider ? (this.modelsByProvider[provider] ?? []) : []);
|
||||
}
|
||||
|
||||
return of([]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, string>
|
||||
): Observable<string[]> {
|
||||
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<string[]>(url, { params });
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<string, string>
|
||||
) {
|
||||
return this.fieldRetreiverCallService.retrieveValues(blockType, key, context).pipe(
|
||||
catchError((err) => {
|
||||
console.error('Field retrieval failed', err);
|
||||
return throwError(() => err);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -33,13 +33,13 @@
|
|||
</div>
|
||||
} @else {
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full min-w-0 h-full py-2 gap-2 flex flex-col items-stretch max-h-[calc(100vh-320px)]">
|
||||
@for (block of filteredBlocks(); track block.name) {
|
||||
@for (block of filteredBlocks(); track block.type) {
|
||||
<div
|
||||
class="border px-3 py-2 w-full min-w-0 bg-slate-50 border-slate-200 overflow-hidden cursor-grab active:cursor-grabbing"
|
||||
draggable="true"
|
||||
(dragstart)="onDragStart($event, block)">
|
||||
<div class="flex items-center justify-between gap-2 text-sm min-w-0">
|
||||
<span class="font-medium text-slate-700 min-w-0 truncate">{{ block.name }}</span>
|
||||
<span class="font-medium text-slate-700 min-w-0 truncate">{{ block.type }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ block.description }}</p>
|
||||
<span class="text-xs px-2 py-0.5 rounded"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Component, computed, inject, model, signal, Signal, WritableSignal } from '@angular/core';
|
||||
import { BlockType } from '@models/flow';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { OrderEvent, OrderField, Ordering } from '@shared/ordering/ordering';
|
||||
import { ListStateViewHolder, OrderViewState } from '@utilities/list-state-holder';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BLOCK_TYPE_DRAG_MIME } from './block-drag';
|
||||
|
|
@ -55,29 +54,14 @@ export class BlocksList extends ListStateViewHolder<BlockType> {
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ConfirmDialog>;
|
||||
describe('ConfirmDialogHostComponent', () => {
|
||||
let component: ConfirmDialogHostComponent;
|
||||
let fixture: ComponentFixture<ConfirmDialogHostComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ConfirmDialog]
|
||||
imports: [ConfirmDialogHostComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ConfirmDialog);
|
||||
fixture = TestBed.createComponent(ConfirmDialogHostComponent);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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: ``,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
<div class="group border
|
||||
px-3 py-2 cursor-pointer w-full
|
||||
px-3 py-2 w-full
|
||||
transition
|
||||
hover:shadow-[0_0_12px_rgba(99,102,241,0.6)]
|
||||
bg-indigo-50 border-indigo-200 select-none" (click)="toggleDetails()" (dblclick)="open()">
|
||||
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()">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between text-sm ">
|
||||
<div class="flex items-center gap-2 text-indigo-700 font-medium">
|
||||
<div class="flex items-center gap-2 font-medium"
|
||||
[class.text-indigo-700]="flow().id !== openedFlowId()"
|
||||
[class.text-emerald-700]="flow().id === openedFlowId()">
|
||||
<i class="bi" [ngClass]="{
|
||||
'bi-globe': flow().visibility === 'PUBLIC',
|
||||
'bi-lock-fill': flow().visibility === 'PRIVATE'
|
||||
|
|
@ -17,9 +28,14 @@
|
|||
{{ flow().name }}
|
||||
</div>
|
||||
|
||||
<i class="bi bi-chevron-down text-xs text-indigo-400
|
||||
transition-transform duration-200" [class.rotate-180]="expanded()">
|
||||
</i>
|
||||
<button
|
||||
type="button"
|
||||
class="p-0 m-0 border-0 bg-transparent text-indigo-400 hover:text-indigo-600"
|
||||
(click)="$event.stopPropagation(); toggleDetails()"
|
||||
title="Details">
|
||||
<i class="bi bi-chevron-down text-xs transition-transform duration-200" [class.rotate-180]="expanded()">
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Dettagli -->
|
||||
|
|
@ -74,4 +90,4 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, input, Input, Output } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-group-holder',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
<div class="hi-node">
|
||||
<div class="hi-header cursor-move">
|
||||
<div class="hi-icon">
|
||||
<i class="bi bi-person-check-fill"></i>
|
||||
</div>
|
||||
<div class="hi-header-content">
|
||||
<span class="hi-title">Human Task</span>
|
||||
<div class="hi-subtitle-row">
|
||||
<span class="hi-subtitle">{{ name }}</span>
|
||||
<button type="button" class="hi-name-edit-btn" title="Edit name" (pointerdown)="$event.stopPropagation()" (click)="openNameEditor($event)">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (missingRequiredParams.length) {
|
||||
<div class="hi-warning-wrap">
|
||||
<div class="hi-warning">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<div class="hi-warning-tooltip">
|
||||
<div class="hi-warning-title">Missing required fields</div>
|
||||
@for (missing of missingRequiredParams; track missing) {
|
||||
<div class="hi-warning-item">{{ missing }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="hi-body">
|
||||
<div class="hi-columns">
|
||||
<div class="hi-column">
|
||||
<div class="hi-column-title">Inputs</div>
|
||||
@for (input of inputs; track input.key) {
|
||||
<div class="hi-row hi-row-input">
|
||||
<div
|
||||
refComponent
|
||||
class="hi-socket"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'input',
|
||||
key: input.key,
|
||||
nodeId: data.id,
|
||||
payload: input.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="hi-pill hi-pill-input">{{ input.key }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="hi-column">
|
||||
<div class="hi-column-title hi-column-title-right">Outputs</div>
|
||||
@for (output of outputs; track output.key) {
|
||||
<div class="hi-row hi-row-output">
|
||||
<span class="hi-pill hi-pill-output">{{ output.key }}</span>
|
||||
<div
|
||||
refComponent
|
||||
class="hi-socket hi-socket-right"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'output',
|
||||
key: output.key,
|
||||
nodeId: data.id,
|
||||
payload: output.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hi-panel">
|
||||
<div class="hi-panel-title-row">
|
||||
<div class="hi-panel-title">Action</div>
|
||||
<button type="button" class="hi-edit-btn" title="Edit action" (pointerdown)="$event.stopPropagation()" (click)="openActionEditor($event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hi-panel-text">{{ actionDescription || '-' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="hi-panel hi-panel-simulated">
|
||||
<div class="hi-panel-title">Simulated with</div>
|
||||
<div class="hi-meta">
|
||||
<span class="hi-meta-chip">
|
||||
<span class="hi-meta-chip-head">
|
||||
Provider
|
||||
<button type="button" class="hi-edit-btn" title="Edit provider" (pointerdown)="$event.stopPropagation()" (click)="openSimpleParamEditor('provider', $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
</span>
|
||||
<span class="hi-meta-chip-value">{{ provider || '-' }}</span>
|
||||
</span>
|
||||
<span class="hi-meta-chip">
|
||||
<span class="hi-meta-chip-head">
|
||||
Model
|
||||
<button type="button" class="hi-edit-btn" title="Edit model" (pointerdown)="$event.stopPropagation()" (click)="openSimpleParamEditor('model', $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
</span>
|
||||
<span class="hi-meta-chip-value">{{ model || '-' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (localEditorOpen) {
|
||||
<div class="hi-modal-backdrop" (pointerdown)="$event.stopPropagation()" (click)="closeSimpleParamEditor($event)">
|
||||
<div class="hi-modal" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
|
||||
<div class="hi-modal-title">Edit {{ localEditorLabel }}</div>
|
||||
<div class="hi-modal-field">
|
||||
<label>{{ localEditorLabel }}</label>
|
||||
@if (localEditorLoading) {
|
||||
<input type="text" [ngModel]="localEditorValue" placeholder="Loading..." disabled (pointerdown)="$event.stopPropagation()" />
|
||||
} @else if (localEditorKey !== 'name' && localEditorOptions.length) {
|
||||
<select [(ngModel)]="localEditorValue" (pointerdown)="$event.stopPropagation()">
|
||||
<option value="">Select {{ localEditorLabel | lowercase }}...</option>
|
||||
@for (option of localEditorOptions; track option) {
|
||||
<option [value]="option">{{ option }}</option>
|
||||
}
|
||||
</select>
|
||||
} @else {
|
||||
<input type="text" [(ngModel)]="localEditorValue" [attr.maxlength]="localEditorKey === 'name' ? 20 : null" [placeholder]="localEditorKey === 'name' ? 'Max 20 characters' : 'Value...'" (pointerdown)="$event.stopPropagation()" />
|
||||
}
|
||||
</div>
|
||||
<div class="hi-modal-actions">
|
||||
<button type="button" class="hi-btn hi-btn-ghost" (pointerdown)="$event.stopPropagation()" (click)="closeSimpleParamEditor($event)">Cancel</button>
|
||||
<button type="button" class="hi-btn hi-btn-primary" (pointerdown)="$event.stopPropagation()" (click)="saveSimpleParamEditor($event)">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
@ -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<string, any> {
|
||||
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<string, any>, path: string): unknown {
|
||||
return path.split('.').reduce<unknown>((acc, key) => {
|
||||
if (acc == null || typeof acc !== 'object') return undefined;
|
||||
return (acc as Record<string, unknown>)[key];
|
||||
}, source);
|
||||
}
|
||||
|
||||
private isMissingValue(value: unknown): boolean {
|
||||
if (value == null) return true;
|
||||
if (typeof value === 'string') return value.trim().length === 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,26 @@
|
|||
</div>
|
||||
<div class="llm-header-content">
|
||||
<span class="llm-title">LLM Node</span>
|
||||
<span class="llm-subtitle">{{ name }}</span>
|
||||
<div class="llm-subtitle-row">
|
||||
<span class="llm-subtitle">{{ name }}</span>
|
||||
<button type="button" class="llm-name-edit-btn" title="Edit name" (pointerdown)="$event.stopPropagation()" (click)="openNameEditor($event)">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (missingRequiredParams.length) {
|
||||
<div class="llm-warning-wrap">
|
||||
<div class="llm-warning">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<div class="llm-warning-tooltip">
|
||||
<div class="llm-warning-title">Missing required fields</div>
|
||||
@for (missing of missingRequiredParams; track missing) {
|
||||
<div class="llm-warning-item">{{ missing }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="llm-body">
|
||||
|
|
@ -54,10 +72,6 @@
|
|||
</div>
|
||||
|
||||
<div class="llm-params">
|
||||
<div class="llm-params-head">
|
||||
<div class="llm-params-title">Parameters</div>
|
||||
</div>
|
||||
|
||||
<div class="llm-param-grid">
|
||||
<div class="llm-param-chip">
|
||||
<div class="llm-param-row-head">
|
||||
|
|
@ -109,7 +123,18 @@
|
|||
<div class="llm-modal-title">Edit {{ localEditorLabel }}</div>
|
||||
<div class="llm-modal-field">
|
||||
<label>{{ localEditorLabel }}</label>
|
||||
<input type="text" [(ngModel)]="localEditorValue" placeholder="Value..." (pointerdown)="$event.stopPropagation()" />
|
||||
@if (localEditorLoading) {
|
||||
<input type="text" [ngModel]="localEditorValue" placeholder="Loading..." disabled (pointerdown)="$event.stopPropagation()" />
|
||||
} @else if (localEditorKey !== 'name' && localEditorOptions.length) {
|
||||
<select [(ngModel)]="localEditorValue" (pointerdown)="$event.stopPropagation()">
|
||||
<option value="">Select {{ localEditorLabel | lowercase }}...</option>
|
||||
@for (option of localEditorOptions; track option) {
|
||||
<option [value]="option">{{ option }}</option>
|
||||
}
|
||||
</select>
|
||||
} @else {
|
||||
<input type="text" [(ngModel)]="localEditorValue" [attr.maxlength]="localEditorKey === 'name' ? 20 : null" [placeholder]="localEditorKey === 'name' ? 'Max 20 characters' : 'Value...'" (pointerdown)="$event.stopPropagation()" />
|
||||
}
|
||||
</div>
|
||||
<div class="llm-modal-actions">
|
||||
<button type="button" class="llm-btn llm-btn-ghost" (pointerdown)="$event.stopPropagation()" (click)="closeSimpleParamEditor($event)">Cancel</button>
|
||||
|
|
|
|||
|
|
@ -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<Llm>;
|
||||
describe('LLMNodeComponent', () => {
|
||||
let component: LLMNodeComponent;
|
||||
let fixture: ComponentFixture<LLMNodeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Llm]
|
||||
imports: [LLMNodeComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Llm);
|
||||
fixture = TestBed.createComponent(LLMNodeComponent);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, any> | 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<string, any>, path: string): unknown {
|
||||
return path.split('.').reduce<unknown>((acc, key) => {
|
||||
if (acc == null || typeof acc !== 'object') return undefined;
|
||||
return (acc as Record<string, unknown>)[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 [];
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<FlowData>();
|
||||
readonly flowId = input.required<string>();
|
||||
|
||||
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<any>();
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -9,15 +9,13 @@
|
|||
<h2 class="m-0 text-sm font-semibold text-gray-800">
|
||||
<input type="text" class="border-0 text-sm font-semibold text-gray-800
|
||||
focus:ring-0 p-0 m-0 box-border
|
||||
min-w-[4ch] max-w-[16ch]
|
||||
w-52 max-w-52
|
||||
bg-transparent"
|
||||
[value]="title()"
|
||||
(blur)="changeTitle(titleInput.value)"
|
||||
(keydown.enter)="changeTitle(titleInput.value); $event.target.blur()"
|
||||
[style.width.ch]="titleWidth()"
|
||||
maxlength="20"
|
||||
size="20"
|
||||
(input)="tempTitle.set(titleInput.value)"
|
||||
#titleInput
|
||||
/>
|
||||
</h2>
|
||||
|
|
|
|||
|
|
@ -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<string | null> = 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 );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
if (this.isDirty()) {
|
||||
async openDocument(doc: Flow, options?: { skipDirtyCheck?: boolean }): Promise<boolean> {
|
||||
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 {
|
|||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable, Signal, signal } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ListView, OrderViewState } from '@utilities/list-state-holder';
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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<any>) {
|
||||
|
|
|
|||
|
|
@ -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<T> {
|
|||
this.state = new ListView<T>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HFSchemes>;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue