Improve schema-driven nodes and interactive execution chat
This commit is contained in:
parent
971d829685
commit
19b0dadacc
|
|
@ -1,4 +1,4 @@
|
|||
# HumainFlowGuiA21
|
||||
# HumAInFlow
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 21.0.3.
|
||||
|
||||
|
|
|
|||
|
|
@ -12,5 +12,5 @@ import { SubflowPreviewDialogHostComponent } from '@shared/subflow-preview-dialo
|
|||
styleUrl: './app.css'
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('humainFlow-gui-a21');
|
||||
protected readonly title = signal('HumAInFlow');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,11 +42,23 @@ export type NodeFamily = 'block' | 'container';
|
|||
|
||||
export type BlockTypeSchema = Record<string, unknown> | null;
|
||||
|
||||
export type BlockInteractionContractKind = 'chat-session' | 'single-response' | string;
|
||||
|
||||
export type BlockInteractionContract = {
|
||||
kind: BlockInteractionContractKind;
|
||||
messageField: string | null;
|
||||
completionField: string | null;
|
||||
historyField: string | null;
|
||||
responseField: string | null;
|
||||
supportsPartialResult: boolean;
|
||||
};
|
||||
|
||||
export type BlockType = {
|
||||
type: BlockTypeName;
|
||||
family: NodeFamily;
|
||||
description: string;
|
||||
userInteractive: boolean;
|
||||
interactionContract?: BlockInteractionContract | null;
|
||||
hasExampleBlock?: boolean;
|
||||
exampleBlockEndpoint?: string | null;
|
||||
configurationType: string | null;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export type TaskExecutionStep = {
|
|||
id: string;
|
||||
inputs: TaskExecutionStepInput[];
|
||||
outputs: TaskExecutionStepOutput[];
|
||||
result?: Record<string, unknown>;
|
||||
status: StepStatus;
|
||||
started: boolean;
|
||||
simulated: boolean;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
|
||||
.editor-sidebar-content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
<div class="editor-sidebar-content h-full min-h-0 min-w-0 overflow-hidden">
|
||||
@switch (open) {
|
||||
@case ('flows') {
|
||||
<div data-tour="editor-sidebar-flows">
|
||||
<div data-tour="editor-sidebar-flows" class="h-full min-h-0 flex flex-col">
|
||||
<app-group-holder title="Flows" icon="bi-lightning-charge-fill">
|
||||
@if (!creatingFlow()) {
|
||||
<i ngProjectAs="header-button"
|
||||
|
|
@ -64,14 +64,14 @@
|
|||
</div>
|
||||
}
|
||||
@case ('blocks') {
|
||||
<div data-tour="editor-sidebar-blocks">
|
||||
<div data-tour="editor-sidebar-blocks" class="h-full min-h-0 flex flex-col">
|
||||
<app-group-holder title="Blocks" icon="bi-boxes">
|
||||
<app-blocks-list></app-blocks-list>
|
||||
</app-group-holder>
|
||||
</div>
|
||||
}
|
||||
@case ('containers') {
|
||||
<div data-tour="editor-sidebar-containers">
|
||||
<div data-tour="editor-sidebar-containers" class="h-full min-h-0 flex flex-col">
|
||||
<app-group-holder title="Containers" icon="bi-box-seam">
|
||||
<app-containers-list></app-containers-list>
|
||||
</app-group-holder>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ export class BlocksCallServiceFake extends BlocksCallServiceBase {
|
|||
"family": "block",
|
||||
"description": "A block that requires human interaction",
|
||||
"userInteractive": true,
|
||||
"interactionContract": {
|
||||
"kind": "single-response",
|
||||
"messageField": null,
|
||||
"completionField": "output",
|
||||
"historyField": null,
|
||||
"responseField": "output",
|
||||
"supportsPartialResult": false
|
||||
},
|
||||
"configurationType": "HumanInteractiveBlockConfiguration",
|
||||
"configurationClass": "it.cnr.isti.workflow.manager.blocks.configurations.HumanInteractiveBlockConfiguration",
|
||||
"schema": {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
family: 'block',
|
||||
description: String(value["description"] ?? ""),
|
||||
userInteractive: Boolean(value["userInteractive"] ?? value["interactive"] ?? false),
|
||||
interactionContract: this.toInteractionContract(value["interactionContract"]),
|
||||
hasExampleBlock: Boolean(value["hasExampleBlock"] ?? false),
|
||||
exampleBlockEndpoint: this.toApiPath(value["exampleBlockEndpoint"]),
|
||||
configurationType: this.toNullableString(value["configurationType"]),
|
||||
|
|
@ -166,6 +167,27 @@ export class BlocksCallService extends BlocksCallServiceBase {
|
|||
return raw as Record<string, unknown>;
|
||||
}
|
||||
|
||||
private toInteractionContract(raw: unknown): BlockType["interactionContract"] {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
||||
const value = raw as Record<string, unknown>;
|
||||
const kind = typeof value["kind"] === "string" && value["kind"].trim().length > 0
|
||||
? value["kind"].trim()
|
||||
: null;
|
||||
if (!kind) return null;
|
||||
|
||||
const asNullableString = (input: unknown) =>
|
||||
typeof input === "string" && input.trim().length > 0 ? input.trim() : null;
|
||||
|
||||
return {
|
||||
kind,
|
||||
messageField: asNullableString(value["messageField"]),
|
||||
completionField: asNullableString(value["completionField"]),
|
||||
historyField: asNullableString(value["historyField"]),
|
||||
responseField: asNullableString(value["responseField"]),
|
||||
supportsPartialResult: value["supportsPartialResult"] === true
|
||||
};
|
||||
}
|
||||
|
||||
private toRecord(value: unknown): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
||||
return value as Record<string, unknown>;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
import { BlockInteractionContractKind } from '@models/flow';
|
||||
|
||||
export type HumanInteractionChatMessage = {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type HumanInteractionDialogInput = {
|
||||
title?: string;
|
||||
actionDescription: string;
|
||||
currentInput: string;
|
||||
kind: BlockInteractionContractKind;
|
||||
actionDescription?: string;
|
||||
currentInput?: string;
|
||||
history?: HumanInteractionChatMessage[];
|
||||
latestResponse?: string;
|
||||
messageField?: string | null;
|
||||
completionField?: string | null;
|
||||
};
|
||||
|
||||
export type HumanInteractionDialogResult = {
|
||||
mode: 'confirm' | 'edit';
|
||||
mode: 'message' | 'complete';
|
||||
value: string;
|
||||
};
|
||||
|
||||
|
|
@ -15,8 +27,13 @@ export type HumanInteractionDialogResult = {
|
|||
export class HumanInteractionDialogService {
|
||||
private _state = signal<{
|
||||
title: string;
|
||||
kind: BlockInteractionContractKind;
|
||||
actionDescription: string;
|
||||
currentInput: string;
|
||||
history: HumanInteractionChatMessage[];
|
||||
latestResponse: string;
|
||||
messageField: string | null;
|
||||
completionField: string | null;
|
||||
resolve: (value: HumanInteractionDialogResult | null) => void;
|
||||
} | null>(null);
|
||||
|
||||
|
|
@ -26,8 +43,13 @@ export class HumanInteractionDialogService {
|
|||
return new Promise((resolve) => {
|
||||
this._state.set({
|
||||
title: input.title ?? 'Human interaction',
|
||||
actionDescription: input.actionDescription,
|
||||
currentInput: input.currentInput,
|
||||
kind: input.kind,
|
||||
actionDescription: input.actionDescription ?? '',
|
||||
currentInput: input.currentInput ?? '',
|
||||
history: input.history ?? [],
|
||||
latestResponse: input.latestResponse ?? '',
|
||||
messageField: input.messageField ?? null,
|
||||
completionField: input.completionField ?? null,
|
||||
resolve
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,11 +6,58 @@
|
|||
<div class="flex items-start justify-between gap-4 border-b border-slate-200 px-5 py-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-900">{{ currentState.title }}</h3>
|
||||
<p class="mt-1 text-sm text-slate-500">Confirm the input as node output or edit it before sending.</p>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
@if (currentState.kind === 'chat-session') {
|
||||
Continue the chat session or send the final completion value.
|
||||
} @else {
|
||||
Confirm the input as node output or edit it before sending.
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" mat-stroked-button (click)="cancel($event)">Close</button>
|
||||
</div>
|
||||
|
||||
@if (currentState.kind === 'chat-session') {
|
||||
<div class="flex min-h-0 flex-1 flex-col bg-slate-50">
|
||||
<div class="flex-1 overflow-y-auto px-5 py-5">
|
||||
<div class="flex flex-col gap-3">
|
||||
@for (message of currentState.history; track $index) {
|
||||
<div class="flex" [class.justify-end]="message.role === 'user'" [class.justify-start]="message.role !== 'user'">
|
||||
<div
|
||||
class="max-w-[85%] rounded-2xl px-4 py-3 shadow-sm"
|
||||
[class.bg-emerald-600]="message.role === 'user'"
|
||||
[class.text-white]="message.role === 'user'"
|
||||
[class.bg-white]="message.role !== 'user'"
|
||||
[class.text-slate-900]="message.role !== 'user'"
|
||||
[class.border]="message.role !== 'user'"
|
||||
[class.border-slate-200]="message.role !== 'user'">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide opacity-70">{{ message.role }}</div>
|
||||
<div class="mt-1 whitespace-pre-wrap break-words text-sm">{{ message.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!currentState.history.length) {
|
||||
<div class="py-8 text-center text-sm text-slate-500">No messages yet.</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-slate-200 bg-white px-5 py-4">
|
||||
<mat-form-field appearance="outline" class="w-full">
|
||||
<mat-label>Type a message</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
class="min-h-[3.5rem] max-h-[14rem] resize-y"
|
||||
rows="2"
|
||||
[ngModel]="draftValue"
|
||||
[attr.data-autofocus]="'true'"
|
||||
(ngModelChange)="setDraftValue($event)">
|
||||
</textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="grid flex-1 gap-4 overflow-y-auto px-5 py-4">
|
||||
<fieldset class="rounded-lg border border-slate-200 bg-slate-50 p-3">
|
||||
<legend class="px-1 text-xs font-semibold uppercase tracking-wide text-slate-500">Action Description</legend>
|
||||
|
|
@ -24,11 +71,13 @@
|
|||
|
||||
@if (editing()) {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Edit Response</mat-label>
|
||||
<mat-label>
|
||||
Edit Response
|
||||
</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
class="min-h-56"
|
||||
rows="10"
|
||||
class="min-h-40"
|
||||
rows="7"
|
||||
[ngModel]="draftValue"
|
||||
[attr.data-autofocus]="'true'"
|
||||
(ngModelChange)="setDraftValue($event)">
|
||||
|
|
@ -36,10 +85,14 @@
|
|||
</mat-form-field>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="flex justify-end gap-2 border-t border-slate-200 px-5 py-4">
|
||||
<button type="button" mat-stroked-button (click)="cancel($event)">Cancel</button>
|
||||
@if (!editing()) {
|
||||
@if (currentState.kind === 'chat-session') {
|
||||
<button type="button" mat-stroked-button [disabled]="!canSendEditedOutput()" (click)="sendChatMessage($event)">Send</button>
|
||||
<button type="button" mat-flat-button [disabled]="!canSendEditedOutput()" (click)="completeChatSession($event)">Send Final Response</button>
|
||||
} @else if (!editing()) {
|
||||
<button type="button" mat-stroked-button (click)="startEditing($event)">Edit Response</button>
|
||||
<button type="button" mat-flat-button (click)="confirmInput($event)">Confirm Input</button>
|
||||
} @else {
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ export class HumanInteractionDialogHostComponent {
|
|||
effect(() => {
|
||||
const state = this.state();
|
||||
if (!state) return;
|
||||
this.editing.set(false);
|
||||
this.draftValue = state.currentInput;
|
||||
this.editing.set(state.kind !== 'chat-session');
|
||||
this.draftValue = '';
|
||||
queueMicrotask(() => {
|
||||
const target = this.host.nativeElement.querySelector('[data-autofocus="true"]') as HTMLElement | null;
|
||||
target?.focus();
|
||||
|
|
@ -64,7 +64,7 @@ export class HumanInteractionDialogHostComponent {
|
|||
confirmInput(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
this.closeWith({ mode: 'confirm', value: this.state()?.currentInput ?? '' });
|
||||
this.closeWith({ mode: 'complete', value: this.state()?.currentInput ?? '' });
|
||||
}
|
||||
|
||||
sendEditedOutput(event?: Event) {
|
||||
|
|
@ -72,7 +72,23 @@ export class HumanInteractionDialogHostComponent {
|
|||
event?.stopPropagation();
|
||||
const value = this.draftValue.trim();
|
||||
if (!value) return;
|
||||
this.closeWith({ mode: 'edit', value: this.draftValue });
|
||||
this.closeWith({ mode: 'complete', value: this.draftValue });
|
||||
}
|
||||
|
||||
sendChatMessage(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
const value = this.draftValue.trim();
|
||||
if (!value) return;
|
||||
this.closeWith({ mode: 'message', value: this.draftValue });
|
||||
}
|
||||
|
||||
completeChatSession(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
const value = this.draftValue.trim();
|
||||
if (!value) return;
|
||||
this.closeWith({ mode: 'complete', value: this.draftValue });
|
||||
}
|
||||
|
||||
canSendEditedOutput(): boolean {
|
||||
|
|
|
|||
|
|
@ -272,6 +272,10 @@
|
|||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.container-node__param-chip--disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.container-node__param-chip--wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
|
@ -318,6 +322,12 @@
|
|||
background: rgba(15, 118, 110, 0.12);
|
||||
}
|
||||
|
||||
.container-node__param-edit:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container-node__dropzone {
|
||||
margin: 0 14px 14px;
|
||||
min-height: 128px;
|
||||
|
|
|
|||
|
|
@ -100,13 +100,14 @@
|
|||
@if (hasParameterFields()) {
|
||||
<div class="container-node__params">
|
||||
@for (field of parameterFields; track field.path) {
|
||||
<div class="container-node__param-chip" [class.container-node__param-chip--wide]="field.wide">
|
||||
<div class="container-node__param-chip" [class.container-node__param-chip--wide]="field.wide" [class.container-node__param-chip--disabled]="!field.enabled">
|
||||
<div class="container-node__param-head">
|
||||
<span class="container-node__param-key">{{ field.label }}</span>
|
||||
@if (!isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="container-node__param-edit"
|
||||
[disabled]="!field.enabled"
|
||||
title="Edit field"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openParameterEditor(field.path, $event)">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, HostBinding, Input, inject } from '@angular/core';
|
||||
import { currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowContainer, FlowData } from '@models/flow';
|
||||
import { NodeSettingField, NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog';
|
||||
import { NodeSettingField, NodeSettingOption, NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog';
|
||||
import { ContainersService } from '@services/containers/containers';
|
||||
import { FieldRetriever } from '@services/retriever/field-retriever';
|
||||
import { ClassicPreset } from 'rete';
|
||||
|
|
@ -11,17 +11,25 @@ import { EditorStateHolder } from '@stores/flow-editor';
|
|||
import { CONTAINER_SUBFLOW_DRAG_MIME } from './container-node-drag';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { extractSchemaRequirements, SchemaRequirements } from '../schema-requirements';
|
||||
import { evaluateUiConditionRule, getValueByPath, pathToLabel, readUiConditionRule, resolveSchemaRef, valueToDisplayString } from '../node-utility';
|
||||
import { evaluateUiConditionRule, getValueByPath, parentPath, pathToLabel, readUiConditionRule, resolveSchemaPath, resolveSchemaRef, schemaFieldLabel, shouldSkipSchemaField, valueToDisplayString } from '../node-utility';
|
||||
|
||||
type ContainerFieldType = 'string' | 'number' | 'integer' | 'boolean' | 'unknown';
|
||||
|
||||
type NodeOptionsSource = {
|
||||
collection: 'inputs' | 'outputs';
|
||||
valueField: string;
|
||||
labelField: string;
|
||||
};
|
||||
|
||||
type ContainerFieldDefinition = {
|
||||
path: string;
|
||||
label: string;
|
||||
type: ContainerFieldType;
|
||||
enumOptions: string[];
|
||||
nodeOptionsSource: NodeOptionsSource | null;
|
||||
widget: 'textarea' | null;
|
||||
structural: boolean;
|
||||
enabledWhen: ReturnType<typeof readUiConditionRule>[];
|
||||
};
|
||||
|
||||
type ContainerFieldView = {
|
||||
|
|
@ -29,6 +37,7 @@ type ContainerFieldView = {
|
|||
label: string;
|
||||
value: string;
|
||||
wide: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
type StructuredRetrieverConfig = {
|
||||
|
|
@ -56,7 +65,7 @@ export class ContainerNodeComponent {
|
|||
private containersService = inject(ContainersService);
|
||||
private settingsDialog = inject(NodeSettingsDialogService);
|
||||
private containerSchema: Record<string, any> | null = null;
|
||||
private schemaRequirements: SchemaRequirements = { required: [], conditional: [] };
|
||||
private schemaRequirements: SchemaRequirements = { required: [], requiredObjects: [], conditional: [] };
|
||||
private containerFieldDefinitions: ContainerFieldDefinition[] = [];
|
||||
deleteConfirmOpen = false;
|
||||
replaceConfirmSelection: string[] | null = null;
|
||||
|
|
@ -171,8 +180,17 @@ export class ContainerNodeComponent {
|
|||
return requiredFields
|
||||
.filter((field, index, fields) => fields.findIndex((candidate) => candidate.path === field.path) === index)
|
||||
.filter((field) => field.path !== 'name' && field.path !== 'subFlow')
|
||||
.filter((field) => this.isFieldEnabled(field.path))
|
||||
.filter((field) => this.isMissingValue(getValueByPath(config, field.path)))
|
||||
.map((field) => field.label);
|
||||
.map((field) => field.label)
|
||||
.concat(
|
||||
this.schemaRequirements.requiredObjects
|
||||
.filter((field) => field.path !== 'name' && field.path !== 'subFlow')
|
||||
.filter((field) => this.isFieldEnabled(field.path))
|
||||
.filter((field) => this.isMissingValue(getValueByPath(config, field.path)))
|
||||
.map((field) => field.label)
|
||||
)
|
||||
.filter((field, index, fields) => fields.indexOf(field) === index);
|
||||
}
|
||||
|
||||
hasParameterFields() {
|
||||
|
|
@ -288,7 +306,7 @@ export class ContainerNodeComponent {
|
|||
if (this.isReadonly) return;
|
||||
|
||||
const definition = this.containerFieldDefinitions.find((field) => field.path === path);
|
||||
if (!definition) return;
|
||||
if (!definition || !this.isFieldEnabled(definition.path)) return;
|
||||
|
||||
const initialValue = this.getEditorInitialValue(definition);
|
||||
const field: NodeSettingField = {
|
||||
|
|
@ -297,7 +315,7 @@ export class ContainerNodeComponent {
|
|||
type: this.toDialogFieldType(definition),
|
||||
required: this.missingRequiredParams.includes(definition.label),
|
||||
rows: definition.widget === 'textarea' ? 6 : undefined,
|
||||
options: definition.enumOptions.map((option) => ({ label: option, value: option }))
|
||||
options: this.resolveSelectableOptions(definition)
|
||||
};
|
||||
|
||||
const result = await this.settingsDialog.open({
|
||||
|
|
@ -536,6 +554,11 @@ export class ContainerNodeComponent {
|
|||
private isMissingValue(value: unknown): boolean {
|
||||
if (value == null) return true;
|
||||
if (typeof value === 'string') return value.trim().length === 0;
|
||||
if (Array.isArray(value)) return value.length === 0 || value.every((item) => this.isMissingValue(item));
|
||||
if (typeof value === 'object') {
|
||||
const entries = Object.values(value as Record<string, unknown>);
|
||||
return entries.length === 0 || entries.every((item) => this.isMissingValue(item));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -547,7 +570,8 @@ export class ContainerNodeComponent {
|
|||
path: field.path,
|
||||
label: field.label,
|
||||
value: valueToDisplayString(getValueByPath(config, field.path)),
|
||||
wide: field.widget === 'textarea' || field.label.length >= 18
|
||||
wide: field.widget === 'textarea' || field.label.length >= 18,
|
||||
enabled: this.isFieldEnabled(field.path)
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -563,11 +587,11 @@ export class ContainerNodeComponent {
|
|||
if (!properties) return;
|
||||
|
||||
for (const [key, childSchema] of Object.entries(properties)) {
|
||||
if (key === 'type') continue;
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
|
||||
if (shouldSkipSchemaField(key, childResolved)) continue;
|
||||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
if (path === 'name' || path === 'subFlow') continue;
|
||||
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
|
||||
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
|
||||
if (hasChildren) {
|
||||
walk(childResolved as Record<string, any>, path);
|
||||
|
|
@ -576,15 +600,17 @@ export class ContainerNodeComponent {
|
|||
|
||||
definitions.push({
|
||||
path,
|
||||
label: pathToLabel(path),
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
type: this.toFieldType(childResolved),
|
||||
enumOptions: Array.isArray(childResolved?.enum)
|
||||
? childResolved.enum.filter((item: unknown): item is string => typeof item === 'string')
|
||||
: [],
|
||||
nodeOptionsSource: this.toNodeOptionsSource(childResolved),
|
||||
widget: typeof childResolved?.['x-ui-widget'] === 'string' && String(childResolved['x-ui-widget']).toLowerCase() === 'textarea'
|
||||
? 'textarea'
|
||||
: null,
|
||||
structural: childResolved?.['x-ui-structural'] === true
|
||||
structural: childResolved?.['x-ui-structural'] === true,
|
||||
enabledWhen: [readUiConditionRule(childResolved?.['x-ui-enabled-when'])].filter((rule) => !!rule)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -607,6 +633,23 @@ export class ContainerNodeComponent {
|
|||
return evaluateUiConditionRule(rule, this.configuration ?? {}, (fieldPath) => this.resolveFieldSchema(fieldPath));
|
||||
}
|
||||
|
||||
private isFieldEnabled(path: string, visited = new Set<string>()): boolean {
|
||||
if (visited.has(path)) return true;
|
||||
visited.add(path);
|
||||
|
||||
const parent = parentPath(path);
|
||||
if (parent && !this.isFieldEnabled(parent, visited)) return false;
|
||||
|
||||
const schema = this.resolveFieldSchema(path);
|
||||
const rules = [readUiConditionRule(schema?.['x-ui-enabled-when'])].filter((rule) => !!rule);
|
||||
return rules.every((rule) => {
|
||||
if (!rule) return true;
|
||||
const dependency = typeof rule.field === 'string' ? rule.field : null;
|
||||
if (dependency && !this.isFieldVisible(dependency, visited)) return false;
|
||||
return evaluateUiConditionRule(rule, this.configuration ?? {}, (fieldPath) => this.resolveFieldSchema(fieldPath));
|
||||
});
|
||||
}
|
||||
|
||||
private toFieldType(schema: Record<string, any> | null): ContainerFieldType {
|
||||
const type = typeof schema?.['type'] === 'string' ? String(schema['type']) : 'unknown';
|
||||
if (type === 'string' || type === 'number' || type === 'integer' || type === 'boolean') {
|
||||
|
|
@ -618,10 +661,59 @@ export class ContainerNodeComponent {
|
|||
private toDialogFieldType(definition: ContainerFieldDefinition): NodeSettingField['type'] {
|
||||
if (definition.type === 'boolean') return 'checkbox';
|
||||
if (definition.widget === 'textarea') return 'textarea';
|
||||
if (definition.enumOptions.length) return 'select';
|
||||
if (definition.enumOptions.length || definition.nodeOptionsSource) return 'select';
|
||||
return 'text';
|
||||
}
|
||||
|
||||
private toNodeOptionsSource(schema: Record<string, any> | null | undefined): NodeOptionsSource | null {
|
||||
if (!schema || typeof schema !== 'object') return null;
|
||||
const raw = schema['x-ui-options-from-node'];
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
|
||||
const collection = (raw as Record<string, unknown>)['collection'];
|
||||
const valueField = (raw as Record<string, unknown>)['valueField'];
|
||||
const labelField = (raw as Record<string, unknown>)['labelField'];
|
||||
if ((collection !== 'inputs' && collection !== 'outputs')
|
||||
|| typeof valueField !== 'string'
|
||||
|| typeof labelField !== 'string'
|
||||
|| valueField.trim().length === 0
|
||||
|| labelField.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
collection,
|
||||
valueField: valueField.trim(),
|
||||
labelField: labelField.trim()
|
||||
};
|
||||
}
|
||||
|
||||
private resolveSelectableOptions(definition: ContainerFieldDefinition): NodeSettingOption[] | undefined {
|
||||
if (definition.nodeOptionsSource) {
|
||||
return this.resolveNodeOptions(definition.nodeOptionsSource);
|
||||
}
|
||||
if (!definition.enumOptions.length) return undefined;
|
||||
return definition.enumOptions.map((option) => ({ label: option, value: option }));
|
||||
}
|
||||
|
||||
private resolveNodeOptions(source: NodeOptionsSource): NodeSettingOption[] {
|
||||
const items = source.collection === 'inputs'
|
||||
? (Array.isArray(this.data?.data?.inputs) ? this.data.data.inputs : [])
|
||||
: (Array.isArray(this.data?.data?.outputs) ? this.data.data.outputs : []);
|
||||
|
||||
return items
|
||||
.map((item: unknown) => {
|
||||
const record = item && typeof item === 'object' ? item as Record<string, unknown> : null;
|
||||
const value = record?.[source.valueField];
|
||||
const label = record?.[source.labelField];
|
||||
if (typeof value !== 'string' || typeof label !== 'string' || value.trim().length === 0 || label.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
return { label, value } satisfies NodeSettingOption;
|
||||
})
|
||||
.filter((option: NodeSettingOption | null): option is NodeSettingOption => option != null);
|
||||
}
|
||||
|
||||
private getEditorInitialValue(definition: ContainerFieldDefinition): string | boolean {
|
||||
const raw = getValueByPath(this.configuration ?? {}, definition.path);
|
||||
if (definition.type === 'boolean') return raw === true;
|
||||
|
|
@ -780,17 +872,7 @@ export class ContainerNodeComponent {
|
|||
}
|
||||
|
||||
private resolveFieldSchema(path: string): Record<string, any> | null {
|
||||
if (!this.containerSchema) return null;
|
||||
|
||||
let current: Record<string, any> | null = this.containerSchema;
|
||||
for (const key of path.split('.')) {
|
||||
const resolved = current ? resolveSchemaRef(current, this.containerSchema) : null;
|
||||
const properties = resolved?.properties as Record<string, any> | undefined;
|
||||
if (!properties?.[key]) return null;
|
||||
current = resolveSchemaRef(properties[key], this.containerSchema) as Record<string, any> | null;
|
||||
}
|
||||
|
||||
return current;
|
||||
return resolveSchemaPath(this.containerSchema, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -717,6 +717,10 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.llm-param-chip-disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.llm-param-chip-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
|
@ -754,6 +758,12 @@
|
|||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.llm-edit-btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.llm-param-block {
|
||||
border: 1px solid #dbe2ea;
|
||||
background: #ffffff;
|
||||
|
|
|
|||
|
|
@ -163,13 +163,14 @@
|
|||
<legend class="llm-param-legend">{{ group.legend }}</legend>
|
||||
<div class="llm-param-grid">
|
||||
@for (field of group.fields; track field.path) {
|
||||
<div class="llm-param-chip" [class.llm-param-chip-wide]="field.wide">
|
||||
<div class="llm-param-chip" [class.llm-param-chip-wide]="field.wide" [class.llm-param-chip-disabled]="!field.enabled">
|
||||
<div class="llm-param-row-head">
|
||||
<span class="llm-param-key">{{ field.label }}</span>
|
||||
@if (!isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-edit-btn"
|
||||
[disabled]="!field.enabled"
|
||||
[attr.title]="'Edit ' + field.label.toLowerCase()"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openParameterEditor(field.path, $event)">
|
||||
|
|
@ -189,13 +190,14 @@
|
|||
@if (parameterFields.length) {
|
||||
<div class="llm-param-grid">
|
||||
@for (field of parameterFields; track field.path) {
|
||||
<div class="llm-param-chip" [class.llm-param-chip-wide]="field.wide">
|
||||
<div class="llm-param-chip" [class.llm-param-chip-wide]="field.wide" [class.llm-param-chip-disabled]="!field.enabled">
|
||||
<div class="llm-param-row-head">
|
||||
<span class="llm-param-key">{{ field.label }}</span>
|
||||
@if (!isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-edit-btn"
|
||||
[disabled]="!field.enabled"
|
||||
[attr.title]="'Edit ' + field.label.toLowerCase()"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openParameterEditor(field.path, $event)">
|
||||
|
|
@ -345,8 +347,8 @@
|
|||
} @else if (localEditorHasRetriever) {
|
||||
<select [(ngModel)]="localEditorValue" (pointerdown)="$event.stopPropagation()">
|
||||
<option value="">{{ localEditorOptions.length ? ('Select ' + (localEditorLabel | lowercase) + '...') : 'No options available' }}</option>
|
||||
@for (option of localEditorOptions; track option) {
|
||||
<option [value]="option">{{ option }}</option>
|
||||
@for (option of localEditorOptions; track option.value) {
|
||||
<option [value]="option.value">{{ option.label }}</option>
|
||||
}
|
||||
</select>
|
||||
} @else {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, HostBinding, inject, Input } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { currentFlowPortValueKind, flowValueKindLabel, FlowData, FlowPort, FlowValueKind, normalizeFlowPortValueKinds } from '@models/flow';
|
||||
import { BlockType, currentFlowPortValueKind, flowValueKindLabel, FlowData, FlowPort, FlowValueKind, normalizeFlowPortValueKinds } from '@models/flow';
|
||||
import { ClassicPreset } from 'rete';
|
||||
import { ReteModule } from 'rete-angular-plugin/21';
|
||||
import {
|
||||
|
|
@ -22,10 +22,16 @@ import {
|
|||
parentPath,
|
||||
pathToLabel,
|
||||
readUiConditionRule,
|
||||
readUiLabel,
|
||||
readUiGroup,
|
||||
resolveSchemaRef,
|
||||
resolveSchemaPath,
|
||||
schemaFieldDescription,
|
||||
schemaFieldLabel,
|
||||
shouldSkipSchemaField,
|
||||
splitTemplatedTextParts,
|
||||
toStringOrNull,
|
||||
validateUniqueByConstraint,
|
||||
valueToDisplayString
|
||||
} from '../node-utility';
|
||||
|
||||
|
|
@ -36,11 +42,18 @@ type RetrieverDependency = {
|
|||
path: string;
|
||||
};
|
||||
|
||||
type NodeOptionsSource = {
|
||||
collection: 'inputs' | 'outputs';
|
||||
valueField: string;
|
||||
labelField: string;
|
||||
};
|
||||
|
||||
type EditableFieldDefinition = {
|
||||
path: string;
|
||||
label: string;
|
||||
type: FieldType;
|
||||
enumOptions: string[];
|
||||
nodeOptionsSource: NodeOptionsSource | null;
|
||||
retrieverBlockType: string | null;
|
||||
retrieverKey: string | null;
|
||||
retrieverUrl: string | null;
|
||||
|
|
@ -54,10 +67,12 @@ type EditableFieldDefinition = {
|
|||
inputType: string | null;
|
||||
inputMultiple: boolean | null;
|
||||
structuralReason?: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
tip?: string;
|
||||
rows?: number;
|
||||
visibleWhen: UiConditionRule[];
|
||||
enabledWhen: UiConditionRule[];
|
||||
group: string | null;
|
||||
};
|
||||
};
|
||||
|
|
@ -67,15 +82,18 @@ type EditableFieldView = {
|
|||
label: string;
|
||||
value: string;
|
||||
wide: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
type ArrayFieldDefinition = {
|
||||
path: string;
|
||||
label: string;
|
||||
itemSchema: Record<string, any> | null;
|
||||
uniqueBy: string | null;
|
||||
ui: {
|
||||
structural: boolean;
|
||||
visibleWhen: UiConditionRule[];
|
||||
enabledWhen: UiConditionRule[];
|
||||
group: string | null;
|
||||
};
|
||||
};
|
||||
|
|
@ -148,7 +166,7 @@ export class GenericNodeComponent {
|
|||
localEditorPath: string | null = null;
|
||||
localEditorLabel = '';
|
||||
localEditorValue = '';
|
||||
localEditorOptions: string[] = [];
|
||||
localEditorOptions: NodeSettingOption[] = [];
|
||||
localEditorLoading = false;
|
||||
localEditorHasRetriever = false;
|
||||
localEditorType: FieldType = 'string';
|
||||
|
|
@ -161,9 +179,10 @@ export class GenericNodeComponent {
|
|||
|
||||
missingRequiredParams: string[] = [];
|
||||
private blockSchema: Record<string, any> | null = null;
|
||||
private blockDescriptor: BlockType | null = null;
|
||||
private editableFieldDefinitions: EditableFieldDefinition[] = [];
|
||||
private arrayFieldDefinitions: ArrayFieldDefinition[] = [];
|
||||
private schemaRequirements: SchemaRequirements = { required: [], conditional: [] };
|
||||
private schemaRequirements: SchemaRequirements = { required: [], requiredObjects: [], conditional: [] };
|
||||
private conditionalRequiredByPath = new Map<string, boolean>();
|
||||
private refreshingConditionalRequirements = false;
|
||||
|
||||
|
|
@ -231,7 +250,7 @@ export class GenericNodeComponent {
|
|||
|
||||
const definition = this.editableFieldDefinitions.find((field) => field.path === path);
|
||||
if (!definition) return;
|
||||
if (!this.isFieldVisible(definition)) return;
|
||||
if (!this.isFieldVisible(definition) || !this.isPathEnabled(definition.path)) return;
|
||||
|
||||
this.localEditorPath = definition.path;
|
||||
this.localEditorLabel = definition.label;
|
||||
|
|
@ -239,12 +258,12 @@ export class GenericNodeComponent {
|
|||
this.localEditorMaxLength = null;
|
||||
this.localEditorWidget = definition.ui.widget;
|
||||
this.localEditorValue = this.valueToEditorString(this.getByPath(this.blockConfiguration ?? {}, definition.path), definition.type);
|
||||
this.localEditorOptions = [...definition.enumOptions];
|
||||
this.localEditorOptions = this.resolveSelectableOptions(definition);
|
||||
this.localEditorBindableAsInput = definition.ui.bindableAsInput;
|
||||
this.localEditorUseInput = this.isBindableFieldUsingInput(definition);
|
||||
this.localEditorBindableInputName = definition.ui.inputName;
|
||||
this.localEditorLoading = !!definition.retrieverKey && !this.localEditorUseInput;
|
||||
this.localEditorHasRetriever = definition.enumOptions.length > 0 || !!definition.retrieverKey;
|
||||
this.localEditorHasRetriever = this.localEditorOptions.length > 0 || !!definition.retrieverKey || !!definition.nodeOptionsSource;
|
||||
this.localEditorOpen = true;
|
||||
|
||||
if (definition.retrieverKey && !this.localEditorUseInput) {
|
||||
|
|
@ -323,7 +342,7 @@ export class GenericNodeComponent {
|
|||
|
||||
if (!this.isPathVisible(path)) return;
|
||||
|
||||
const contentLabel = pathToLabel(path);
|
||||
const contentLabel = this.fieldDisplayLabel(path);
|
||||
const ui = this.getFieldUiMeta(path);
|
||||
const currentValue = String(this.getByPath(this.blockConfiguration ?? {}, path) ?? '');
|
||||
await this.openTextareaEditor(path, contentLabel, currentValue, ui);
|
||||
|
|
@ -351,17 +370,17 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
isHumanNode(): boolean {
|
||||
return this.blockType === 'HumanInteractionBlock';
|
||||
return !!this.blockDescriptor?.interactionContract;
|
||||
}
|
||||
|
||||
isConditionalNode(): boolean {
|
||||
return this.blockType === 'ConditionalBlock';
|
||||
const outputNames = this.resolvePorts('output').map((port) => port.name.trim().toLowerCase());
|
||||
return outputNames.includes('true') && outputNames.includes('false');
|
||||
}
|
||||
|
||||
nodeTitle(): string {
|
||||
const type = this.blockType;
|
||||
if (!type) return 'Node';
|
||||
if (type === 'HumanInteractionBlock') return 'Human Task';
|
||||
return type
|
||||
.replace(/Block$/, '')
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
|
|
@ -505,6 +524,7 @@ export class GenericNodeComponent {
|
|||
if (!type) return;
|
||||
|
||||
const blockType = await this.blocksService.getBlockType(type);
|
||||
this.blockDescriptor = blockType ?? null;
|
||||
this.blockSchema = (blockType?.schema ?? null) as Record<string, any> | null;
|
||||
this.schemaRequirements = extractSchemaRequirements(this.blockSchema);
|
||||
this.editableFieldDefinitions = this.buildEditableFieldDefinitions(this.blockSchema);
|
||||
|
|
@ -563,7 +583,7 @@ export class GenericNodeComponent {
|
|||
const walk = (
|
||||
node: Record<string, any>,
|
||||
pathPrefix: string,
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; group: string | null }
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; enabledWhen: UiConditionRule[]; group: string | null }
|
||||
) => {
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
|
@ -583,6 +603,7 @@ export class GenericNodeComponent {
|
|||
if (hasChildren) {
|
||||
walk(childResolved as Record<string, any>, path, {
|
||||
visibleWhen: childUi.visibleWhen,
|
||||
enabledWhen: childUi.enabledWhen,
|
||||
group: childUi.group
|
||||
});
|
||||
continue;
|
||||
|
|
@ -594,9 +615,10 @@ export class GenericNodeComponent {
|
|||
seen.add(path);
|
||||
definitions.push({
|
||||
path,
|
||||
label: pathToLabel(path),
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
type: this.toFieldType(childResolved?.type),
|
||||
enumOptions: this.toEnumOptions(childResolved),
|
||||
nodeOptionsSource: this.toNodeOptionsSource(childResolved),
|
||||
retrieverBlockType: this.toRetrieverBlockType(childResolved),
|
||||
retrieverKey: this.toRetrieverKey(childResolved),
|
||||
retrieverUrl: this.toRetrieverUrl(childResolved),
|
||||
|
|
@ -619,7 +641,7 @@ export class GenericNodeComponent {
|
|||
const walk = (
|
||||
node: Record<string, any>,
|
||||
pathPrefix: string,
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; group: string | null }
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; enabledWhen: UiConditionRule[]; group: string | null }
|
||||
) => {
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
|
@ -639,11 +661,15 @@ export class GenericNodeComponent {
|
|||
seen.add(path);
|
||||
definitions.push({
|
||||
path,
|
||||
label: pathToLabel(path),
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
itemSchema: this.resolveArrayItemSchema(childResolved, schema),
|
||||
uniqueBy: typeof childResolved?.['x-ui-unique-by'] === 'string' && String(childResolved['x-ui-unique-by']).trim().length > 0
|
||||
? String(childResolved['x-ui-unique-by']).trim()
|
||||
: null,
|
||||
ui: {
|
||||
structural: childUi.structural,
|
||||
visibleWhen: childUi.visibleWhen,
|
||||
enabledWhen: childUi.enabledWhen,
|
||||
group: childUi.group
|
||||
}
|
||||
});
|
||||
|
|
@ -654,6 +680,7 @@ export class GenericNodeComponent {
|
|||
if (hasChildren) {
|
||||
walk(childResolved as Record<string, any>, path, {
|
||||
visibleWhen: childUi.visibleWhen,
|
||||
enabledWhen: childUi.enabledWhen,
|
||||
group: childUi.group
|
||||
});
|
||||
}
|
||||
|
|
@ -666,7 +693,7 @@ export class GenericNodeComponent {
|
|||
|
||||
private toFieldUiMeta(
|
||||
schema: Record<string, any> | null | undefined,
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; group: string | null }
|
||||
inheritedUi?: { visibleWhen: UiConditionRule[]; enabledWhen: UiConditionRule[]; group: string | null }
|
||||
) {
|
||||
const rawWidget = typeof schema?.['x-ui-widget'] === 'string'
|
||||
? String(schema['x-ui-widget']).toLowerCase().trim()
|
||||
|
|
@ -676,9 +703,7 @@ export class GenericNodeComponent {
|
|||
const placeholder = typeof schema?.['x-ui-placeholder'] === 'string'
|
||||
? String(schema['x-ui-placeholder'])
|
||||
: undefined;
|
||||
const tip = typeof schema?.['x-ui-tip'] === 'string'
|
||||
? String(schema['x-ui-tip'])
|
||||
: undefined;
|
||||
const tip = schemaFieldDescription(schema) ?? undefined;
|
||||
const rowsRaw = schema?.['x-ui-rows'];
|
||||
const rows = typeof rowsRaw === 'number' && Number.isFinite(rowsRaw) && rowsRaw > 0
|
||||
? Math.trunc(rowsRaw)
|
||||
|
|
@ -699,7 +724,9 @@ export class GenericNodeComponent {
|
|||
? String(schema['x-ui-structural-reason'])
|
||||
: undefined;
|
||||
const visibleWhen = readUiConditionRule(schema?.['x-ui-visible-when']);
|
||||
const enabledWhen = readUiConditionRule(schema?.['x-ui-enabled-when']);
|
||||
const group = readUiGroup(schema?.['x-ui-group']) ?? inheritedUi?.group ?? null;
|
||||
const label = readUiLabel(schema?.['x-ui-label']) ?? undefined;
|
||||
|
||||
return {
|
||||
widget: normalizedWidget,
|
||||
|
|
@ -710,6 +737,7 @@ export class GenericNodeComponent {
|
|||
inputType,
|
||||
inputMultiple,
|
||||
structuralReason,
|
||||
label,
|
||||
placeholder,
|
||||
tip,
|
||||
rows,
|
||||
|
|
@ -717,6 +745,10 @@ export class GenericNodeComponent {
|
|||
...(inheritedUi?.visibleWhen ?? []),
|
||||
...(visibleWhen ? [visibleWhen] : [])
|
||||
],
|
||||
enabledWhen: [
|
||||
...(inheritedUi?.enabledWhen ?? []),
|
||||
...(enabledWhen ? [enabledWhen] : [])
|
||||
],
|
||||
group
|
||||
};
|
||||
}
|
||||
|
|
@ -726,17 +758,24 @@ export class GenericNodeComponent {
|
|||
if (!root) return this.toFieldUiMeta(null);
|
||||
|
||||
let current: Record<string, any> | null = root;
|
||||
let inheritedUi = { visibleWhen: [] as UiConditionRule[], group: null as string | null };
|
||||
let inheritedUi = { visibleWhen: [] as UiConditionRule[], enabledWhen: [] as UiConditionRule[], group: null as string | null };
|
||||
|
||||
for (const segment of path.split('.')) {
|
||||
if (!current) return this.toFieldUiMeta(null, inheritedUi);
|
||||
const resolved = resolveSchemaRef(current, root);
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return this.toFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
if (/^\d+$/.test(segment)) {
|
||||
const items = resolved?.items;
|
||||
if (!items || typeof items !== 'object') return this.toFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(items as Record<string, any>, root);
|
||||
} else {
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return this.toFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
}
|
||||
const nextUi = this.toFieldUiMeta(current, inheritedUi);
|
||||
inheritedUi = {
|
||||
visibleWhen: nextUi.visibleWhen,
|
||||
enabledWhen: nextUi.enabledWhen,
|
||||
group: nextUi.group
|
||||
};
|
||||
}
|
||||
|
|
@ -749,19 +788,7 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
private resolveFieldSchema(path: string): Record<string, any> | null {
|
||||
const root = this.blockSchema;
|
||||
if (!root) return null;
|
||||
|
||||
let current: Record<string, any> | null = root;
|
||||
for (const segment of path.split('.')) {
|
||||
if (!current) return null;
|
||||
const resolved = resolveSchemaRef(current, root);
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return null;
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
}
|
||||
|
||||
return current;
|
||||
return resolveSchemaPath(this.blockSchema, path);
|
||||
}
|
||||
|
||||
private toFieldType(type: unknown): FieldType {
|
||||
|
|
@ -793,6 +820,29 @@ export class GenericNodeComponent {
|
|||
return raw.filter((value): value is string => typeof value === 'string');
|
||||
}
|
||||
|
||||
private toNodeOptionsSource(schema: Record<string, any> | null | undefined): NodeOptionsSource | null {
|
||||
if (!schema || typeof schema !== 'object') return null;
|
||||
const raw = schema['x-ui-options-from-node'];
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
|
||||
const collection = (raw as Record<string, unknown>)['collection'];
|
||||
const valueField = (raw as Record<string, unknown>)['valueField'];
|
||||
const labelField = (raw as Record<string, unknown>)['labelField'];
|
||||
if ((collection !== 'inputs' && collection !== 'outputs')
|
||||
|| typeof valueField !== 'string'
|
||||
|| typeof labelField !== 'string'
|
||||
|| valueField.trim().length === 0
|
||||
|| labelField.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
collection,
|
||||
valueField: valueField.trim(),
|
||||
labelField: labelField.trim()
|
||||
};
|
||||
}
|
||||
|
||||
private toRetrieverUrl(schema: Record<string, any> | null | undefined): string | null {
|
||||
if (!schema || typeof schema !== 'object') return null;
|
||||
const rawUrl = schema['x-retriever-url'];
|
||||
|
|
@ -948,7 +998,7 @@ export class GenericNodeComponent {
|
|||
definition.retrieverUrl
|
||||
)
|
||||
);
|
||||
this.localEditorOptions = options ?? [];
|
||||
this.localEditorOptions = (options ?? []).map((option) => ({ label: option, value: option }));
|
||||
} catch {
|
||||
this.localEditorOptions = [];
|
||||
} finally {
|
||||
|
|
@ -956,6 +1006,31 @@ export class GenericNodeComponent {
|
|||
}
|
||||
}
|
||||
|
||||
private resolveSelectableOptions(definition: EditableFieldDefinition): NodeSettingOption[] {
|
||||
if (definition.nodeOptionsSource) {
|
||||
return this.resolveNodeOptions(definition.nodeOptionsSource);
|
||||
}
|
||||
return definition.enumOptions.map((option) => ({ label: option, value: option }));
|
||||
}
|
||||
|
||||
private resolveNodeOptions(source: NodeOptionsSource): NodeSettingOption[] {
|
||||
const items = this.resolvePorts(source.collection === 'inputs' ? 'input' : 'output');
|
||||
return items
|
||||
.map((item) => {
|
||||
const record = item as unknown as Record<string, unknown>;
|
||||
const value = record[source.valueField];
|
||||
const label = record[source.labelField];
|
||||
if (typeof value !== 'string' || typeof label !== 'string' || value.trim().length === 0 || label.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value
|
||||
} satisfies NodeSettingOption;
|
||||
})
|
||||
.filter((option): option is NodeSettingOption => option != null);
|
||||
}
|
||||
|
||||
private refreshParameterFields() {
|
||||
const config = this.blockConfiguration ?? {};
|
||||
const richContentPaths = new Set(this.richContentPaths());
|
||||
|
|
@ -966,7 +1041,7 @@ export class GenericNodeComponent {
|
|||
.filter((path) => this.isPathVisible(path))
|
||||
.map((path) => ({
|
||||
path,
|
||||
label: pathToLabel(path),
|
||||
label: this.fieldDisplayLabel(path),
|
||||
parts: this.toRichContentParts(path)
|
||||
}));
|
||||
|
||||
|
|
@ -985,7 +1060,8 @@ export class GenericNodeComponent {
|
|||
path: definition.path,
|
||||
label: definition.label,
|
||||
value: this.fieldDisplayValue(definition, value),
|
||||
wide: this.shouldRenderWideField(definition.label, definition.ui.widget === 'textarea')
|
||||
wide: this.shouldRenderWideField(definition.label, definition.ui.widget === 'textarea'),
|
||||
enabled: this.isPathEnabled(definition.path)
|
||||
};
|
||||
}).filter((field) => !richContentPaths.has(field.path))
|
||||
.filter((field) => this.isPathVisible(field.path));
|
||||
|
|
@ -1005,7 +1081,7 @@ export class GenericNodeComponent {
|
|||
this.parameterFields = rootFields;
|
||||
this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({
|
||||
key,
|
||||
legend: key.startsWith('group:') ? key.slice('group:'.length) : pathToLabel(key),
|
||||
legend: key.startsWith('group:') ? key.slice('group:'.length) : this.fieldDisplayLabel(key),
|
||||
fields
|
||||
}));
|
||||
this.refreshView();
|
||||
|
|
@ -1017,9 +1093,10 @@ export class GenericNodeComponent {
|
|||
.filter((entry) => this.isPathVisible(entry.path))
|
||||
.map((entry) => ({
|
||||
path: entry.path,
|
||||
label: pathToLabel(entry.path),
|
||||
label: this.fieldDisplayLabel(entry.path),
|
||||
value: valueToDisplayString(entry.value),
|
||||
wide: this.shouldRenderWideField(pathToLabel(entry.path), false)
|
||||
wide: this.shouldRenderWideField(this.fieldDisplayLabel(entry.path), false),
|
||||
enabled: this.isPathEnabled(entry.path)
|
||||
}));
|
||||
|
||||
for (const field of fallbackFields) {
|
||||
|
|
@ -1037,7 +1114,7 @@ export class GenericNodeComponent {
|
|||
this.parameterFields = rootFields;
|
||||
this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({
|
||||
key,
|
||||
legend: pathToLabel(key),
|
||||
legend: this.fieldDisplayLabel(key),
|
||||
fields
|
||||
}));
|
||||
|
||||
|
|
@ -1094,6 +1171,12 @@ export class GenericNodeComponent {
|
|||
if (!result) return;
|
||||
|
||||
const nextItem = this.parseArrayItemDialogResult(definition, result, currentItem);
|
||||
const duplicateError = this.validateUniqueArrayItem(definition, items, nextItem, index);
|
||||
if (duplicateError) {
|
||||
window.alert(duplicateError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index == null) {
|
||||
items.push(nextItem);
|
||||
} else {
|
||||
|
|
@ -1140,9 +1223,9 @@ export class GenericNodeComponent {
|
|||
const initial: Record<string, string | boolean> = {};
|
||||
|
||||
for (const [key, rawPropertySchema] of Object.entries(properties)) {
|
||||
if (key === 'type' || key.startsWith('__')) continue;
|
||||
|
||||
const propertySchema = resolveSchemaRef(rawPropertySchema as Record<string, any>, schemaRoot);
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
|
||||
if (this.hasDynamicSchema(propertySchema)) {
|
||||
const dynamicFields = await this.buildDynamicSchemaFields(key, propertySchema, item);
|
||||
fields.push(...dynamicFields.fields);
|
||||
|
|
@ -1150,7 +1233,7 @@ export class GenericNodeComponent {
|
|||
continue;
|
||||
}
|
||||
|
||||
const label = pathToLabel(key);
|
||||
const label = schemaFieldLabel(key, propertySchema);
|
||||
const currentValue = item[key];
|
||||
const options = await this.loadNodeSettingOptions(propertySchema, item, '');
|
||||
const isObjectLike = propertySchema?.type === 'object';
|
||||
|
|
@ -1167,7 +1250,7 @@ export class GenericNodeComponent {
|
|||
rows: fieldType === 'textarea' ? 8 : undefined,
|
||||
options,
|
||||
placeholder: typeof propertySchema?.['x-ui-placeholder'] === 'string' ? String(propertySchema['x-ui-placeholder']) : undefined,
|
||||
tip: typeof propertySchema?.['x-ui-tip'] === 'string' ? String(propertySchema['x-ui-tip']) : undefined
|
||||
tip: schemaFieldDescription(propertySchema) ?? undefined
|
||||
});
|
||||
|
||||
if (fieldType === 'checkbox') {
|
||||
|
|
@ -1205,9 +1288,8 @@ export class GenericNodeComponent {
|
|||
const nextItem: Record<string, unknown> = {};
|
||||
|
||||
for (const [key, rawPropertySchema] of Object.entries(properties)) {
|
||||
if (key === 'type' || key.startsWith('__')) continue;
|
||||
|
||||
const propertySchema = resolveSchemaRef(rawPropertySchema as Record<string, any>, schemaRoot);
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
if (this.hasDynamicSchema(propertySchema)) {
|
||||
const dynamicValue = this.extractNestedDialogValues(result, key);
|
||||
nextItem[key] = Object.keys(dynamicValue).length ? dynamicValue : (previousItem[key] ?? {});
|
||||
|
|
@ -1252,8 +1334,8 @@ export class GenericNodeComponent {
|
|||
|
||||
const item: Record<string, unknown> = {};
|
||||
for (const [key, rawPropertySchema] of Object.entries(properties)) {
|
||||
if (key === 'type' || key.startsWith('__')) continue;
|
||||
const propertySchema = resolveSchemaRef(rawPropertySchema as Record<string, any>, schemaRoot);
|
||||
if (shouldSkipSchemaField(key, propertySchema)) continue;
|
||||
if (Object.prototype.hasOwnProperty.call(propertySchema ?? {}, 'default')) {
|
||||
item[key] = propertySchema.default;
|
||||
continue;
|
||||
|
|
@ -1304,7 +1386,7 @@ export class GenericNodeComponent {
|
|||
return {
|
||||
fields: [{
|
||||
key: `${baseKey}.__hint`,
|
||||
label: pathToLabel(baseKey),
|
||||
label: schemaFieldLabel(baseKey, propertySchema),
|
||||
type: 'display',
|
||||
readonly: true
|
||||
}],
|
||||
|
|
@ -1323,7 +1405,7 @@ export class GenericNodeComponent {
|
|||
return {
|
||||
fields: [{
|
||||
key: `${baseKey}.__hint`,
|
||||
label: pathToLabel(baseKey),
|
||||
label: schemaFieldLabel(baseKey, propertySchema),
|
||||
type: 'display',
|
||||
readonly: true
|
||||
}],
|
||||
|
|
@ -1333,7 +1415,7 @@ export class GenericNodeComponent {
|
|||
};
|
||||
}
|
||||
|
||||
return this.buildDialogFieldsFromSchema(baseKey, pathToLabel(baseKey), resolvedSchema, item[baseKey]);
|
||||
return this.buildDialogFieldsFromSchema(baseKey, schemaFieldLabel(baseKey, propertySchema), resolvedSchema, item[baseKey]);
|
||||
}
|
||||
|
||||
private async buildDialogFieldsFromSchema(
|
||||
|
|
@ -1359,11 +1441,10 @@ export class GenericNodeComponent {
|
|||
if (!properties) return;
|
||||
|
||||
for (const [childKey, rawChildSchema] of Object.entries(properties)) {
|
||||
if (childKey === 'type' || childKey.startsWith('__')) continue;
|
||||
|
||||
const childSchema = resolveSchemaRef(rawChildSchema as Record<string, any>, schema);
|
||||
if (shouldSkipSchemaField(childKey, childSchema)) continue;
|
||||
const nextPath = `${pathPrefix}.${childKey}`;
|
||||
const nextLabel = `${titlePrefix} ${pathToLabel(childKey)}`;
|
||||
const nextLabel = `${titlePrefix} ${schemaFieldLabel(childKey, childSchema)}`;
|
||||
const currentNestedValue = getValueByPath(currentRecord, nextPath.slice(`${keyPrefix}.`.length));
|
||||
|
||||
const hasChildren = !!childSchema?.['properties'] || childSchema?.type === 'object';
|
||||
|
|
@ -1391,7 +1472,7 @@ export class GenericNodeComponent {
|
|||
rows: fieldType === 'textarea' ? 8 : undefined,
|
||||
options,
|
||||
placeholder: typeof childSchema?.['x-ui-placeholder'] === 'string' ? String(childSchema['x-ui-placeholder']) : undefined,
|
||||
tip: typeof childSchema?.['x-ui-tip'] === 'string' ? String(childSchema['x-ui-tip']) : undefined
|
||||
tip: schemaFieldDescription(childSchema) ?? undefined
|
||||
});
|
||||
|
||||
if (fieldType === 'checkbox') {
|
||||
|
|
@ -1419,11 +1500,20 @@ export class GenericNodeComponent {
|
|||
return nested;
|
||||
}
|
||||
|
||||
private fieldDisplayLabel(path: string): string {
|
||||
return schemaFieldLabel(path, this.resolveFieldSchema(path));
|
||||
}
|
||||
|
||||
private async loadNodeSettingOptions(
|
||||
propertySchema: Record<string, any>,
|
||||
item: Record<string, unknown>,
|
||||
pathPrefix: string
|
||||
): Promise<NodeSettingOption[] | undefined> {
|
||||
const enumOptions = this.toEnumOptions(propertySchema);
|
||||
if (enumOptions.length) {
|
||||
return enumOptions.map((value) => ({ label: value, value }));
|
||||
}
|
||||
|
||||
const retrieverKey = this.toRetrieverKey(propertySchema);
|
||||
const retrieverBlockType = this.toRetrieverBlockType(propertySchema);
|
||||
if (!retrieverKey || !retrieverBlockType) return undefined;
|
||||
|
|
@ -1482,6 +1572,27 @@ export class GenericNodeComponent {
|
|||
return summaryParts.length ? summaryParts.join(' · ') : `Item ${index + 1}`;
|
||||
}
|
||||
|
||||
private validateUniqueArrayItem(
|
||||
definition: ArrayFieldDefinition,
|
||||
items: unknown[],
|
||||
nextItem: Record<string, unknown>,
|
||||
currentIndex: number | null
|
||||
): string | null {
|
||||
const violation = validateUniqueByConstraint(
|
||||
items,
|
||||
nextItem,
|
||||
definition.uniqueBy,
|
||||
currentIndex,
|
||||
(value) => this.isMissingValue(value),
|
||||
(left, right) => this.areValuesEqual(left, right)
|
||||
);
|
||||
if (!violation) return null;
|
||||
|
||||
const uniqueLabel = pathToLabel(violation.path);
|
||||
const fieldLabel = definition.label;
|
||||
return `${fieldLabel} requires a unique ${uniqueLabel}. "${String(violation.value)}" is already used.`;
|
||||
}
|
||||
|
||||
private valueToEditorString(value: unknown, type: FieldType): string {
|
||||
if (type === 'boolean') {
|
||||
return value === true ? 'true' : 'false';
|
||||
|
|
@ -1615,6 +1726,11 @@ export class GenericNodeComponent {
|
|||
private isMissingValue(value: unknown): boolean {
|
||||
if (value == null) return true;
|
||||
if (typeof value === 'string') return value.trim().length === 0;
|
||||
if (Array.isArray(value)) return value.length === 0 || value.every((item) => this.isMissingValue(item));
|
||||
if (typeof value === 'object') {
|
||||
const entries = Object.values(value as Record<string, unknown>);
|
||||
return entries.length === 0 || entries.every((item) => this.isMissingValue(item));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -1632,12 +1748,22 @@ export class GenericNodeComponent {
|
|||
|
||||
const missingFields = requiredFields
|
||||
.filter((field) => {
|
||||
if (!this.isPathEnabled(field.path)) return false;
|
||||
const definition = this.editableFieldDefinitions.find((candidate) => candidate.path === field.path);
|
||||
if (definition && this.isBindableFieldUsingInput(definition)) return false;
|
||||
return this.isMissingValue(this.getByPath(config, field.path));
|
||||
});
|
||||
|
||||
this.missingRequiredParams = missingFields.map((field) => field.label);
|
||||
const missingLabels = new Set(missingFields.map((field) => field.label));
|
||||
|
||||
for (const objectField of this.schemaRequirements.requiredObjects) {
|
||||
if (objectField.path === 'name') continue;
|
||||
if (!this.isFieldConditionSatisfied(objectField.path)) continue;
|
||||
if (!this.isMissingValue(this.getByPath(config, objectField.path))) continue;
|
||||
missingLabels.add(objectField.label);
|
||||
}
|
||||
|
||||
this.missingRequiredParams = Array.from(missingLabels);
|
||||
|
||||
if (!this.refreshingConditionalRequirements) {
|
||||
void this.refreshConditionalRequirements();
|
||||
|
|
@ -1807,6 +1933,18 @@ export class GenericNodeComponent {
|
|||
return this.isFieldConditionSatisfied(path);
|
||||
}
|
||||
|
||||
private isPathEnabled(path: string, visited = new Set<string>()): boolean {
|
||||
if (visited.has(path)) return true;
|
||||
visited.add(path);
|
||||
|
||||
const ui = this.getFieldUiMeta(path);
|
||||
return ui.enabledWhen.every((rule) => {
|
||||
if (!rule) return true;
|
||||
if (!this.isFieldConditionSatisfied(rule.field, visited)) return false;
|
||||
return evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath));
|
||||
});
|
||||
}
|
||||
|
||||
private isFieldVisible(field: EditableFieldDefinition): boolean {
|
||||
return this.isPathVisible(field.path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
export type TemplatedTextPart = { text: string; isDynamicInput: boolean };
|
||||
export type UiConditionRule =
|
||||
| { field: string; equals: string }
|
||||
| { field: string; in: string[] };
|
||||
| { field: string; in: string[] }
|
||||
| { field: string; present: boolean };
|
||||
|
||||
export function toStringOrNull(value: unknown): string | null {
|
||||
if (typeof value === 'string' && value.trim().length > 0) return value;
|
||||
|
|
@ -45,6 +46,27 @@ export function pathToLabel(path: string): string {
|
|||
.replace(/^./, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function readUiLabel(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const normalized = value.trim();
|
||||
return normalized.length ? normalized : null;
|
||||
}
|
||||
|
||||
export function readUiDescription(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const normalized = value.trim();
|
||||
return normalized.length ? normalized : null;
|
||||
}
|
||||
|
||||
export function schemaFieldLabel(path: string, schema: Record<string, any> | null | undefined): string {
|
||||
return readUiLabel(schema?.['x-ui-label']) ?? pathToLabel(path);
|
||||
}
|
||||
|
||||
export function schemaFieldDescription(schema: Record<string, any> | null | undefined): string | null {
|
||||
return readUiDescription(schema?.['x-ui-description'])
|
||||
?? (typeof schema?.['x-ui-tip'] === 'string' ? String(schema['x-ui-tip']).trim() || null : null);
|
||||
}
|
||||
|
||||
export function parentPath(path: string): string | null {
|
||||
const index = path.lastIndexOf('.');
|
||||
if (index <= 0) return null;
|
||||
|
|
@ -71,12 +93,38 @@ export function resolveSchemaRef(node: Record<string, any>, root: Record<string,
|
|||
};
|
||||
}
|
||||
|
||||
export function resolveSchemaPath(root: Record<string, any> | null | undefined, path: string): Record<string, any> | null {
|
||||
if (!root || !path.trim()) return root ?? null;
|
||||
|
||||
let current: Record<string, any> | null = root;
|
||||
for (const segment of path.split('.')) {
|
||||
if (!current) return null;
|
||||
|
||||
const resolved = resolveSchemaRef(current, root);
|
||||
if (!resolved || typeof resolved !== 'object') return null;
|
||||
|
||||
if (/^\d+$/.test(segment)) {
|
||||
const items = resolved.items;
|
||||
if (!items || typeof items !== 'object') return null;
|
||||
current = resolveSchemaRef(items as Record<string, any>, root) as Record<string, any> | null;
|
||||
continue;
|
||||
}
|
||||
|
||||
const properties = resolved.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return null;
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root) as Record<string, any> | null;
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
export function readUiConditionRule(value: unknown): UiConditionRule | null {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
|
||||
const field = (value as Record<string, unknown>)['field'];
|
||||
const equals = (value as Record<string, unknown>)['equals'];
|
||||
const includes = (value as Record<string, unknown>)['in'];
|
||||
const present = (value as Record<string, unknown>)['present'];
|
||||
if (typeof field !== 'string' || field.trim().length === 0) return null;
|
||||
if (typeof equals === 'string') {
|
||||
return {
|
||||
|
|
@ -94,6 +142,13 @@ export function readUiConditionRule(value: unknown): UiConditionRule | null {
|
|||
};
|
||||
}
|
||||
|
||||
if (typeof present === 'boolean') {
|
||||
return {
|
||||
field: field.trim(),
|
||||
present
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +165,47 @@ export function getValueByPath(source: Record<string, any> | null | undefined, p
|
|||
}, source ?? {});
|
||||
}
|
||||
|
||||
export function validateUniqueByConstraint(
|
||||
items: unknown[],
|
||||
nextItem: Record<string, unknown>,
|
||||
uniqueBy: string | null | undefined,
|
||||
currentIndex: number | null,
|
||||
isMissingValue: (value: unknown) => boolean,
|
||||
areValuesEqual: (left: unknown, right: unknown) => boolean
|
||||
): { path: string; value: unknown } | null {
|
||||
if (!uniqueBy) return null;
|
||||
|
||||
const uniqueValue = getValueByPath(nextItem, uniqueBy);
|
||||
if (isMissingValue(uniqueValue)) return null;
|
||||
|
||||
const duplicateIndex = items.findIndex((item, index) => {
|
||||
if (currentIndex != null && index === currentIndex) return false;
|
||||
const candidateValue = item && typeof item === 'object' && !Array.isArray(item)
|
||||
? getValueByPath(item as Record<string, unknown>, uniqueBy)
|
||||
: undefined;
|
||||
return areValuesEqual(candidateValue, uniqueValue);
|
||||
});
|
||||
|
||||
if (duplicateIndex < 0) return null;
|
||||
|
||||
return {
|
||||
path: uniqueBy,
|
||||
value: uniqueValue
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldSkipSchemaField(key: string, schema: Record<string, any> | null | undefined): boolean {
|
||||
if (key.startsWith('__')) return true;
|
||||
if (key !== 'type') return false;
|
||||
|
||||
const enumValues = Array.isArray(schema?.['enum'])
|
||||
? schema['enum'].filter((value: unknown): value is string => typeof value === 'string')
|
||||
: [];
|
||||
const defaultValue = typeof schema?.['default'] === 'string' ? String(schema['default']) : null;
|
||||
|
||||
return enumValues.length === 1 && defaultValue != null && enumValues[0] === defaultValue;
|
||||
}
|
||||
|
||||
export function evaluateUiConditionRule(
|
||||
rule: UiConditionRule | null | undefined,
|
||||
config: Record<string, any> | null | undefined,
|
||||
|
|
@ -123,6 +219,11 @@ export function evaluateUiConditionRule(
|
|||
const type = typeof schemaType === 'string' ? schemaType : null;
|
||||
const expectedValues = 'in' in rule ? rule.in : null;
|
||||
const expectedValue = 'equals' in rule ? rule.equals : null;
|
||||
const expectedPresence = 'present' in rule ? rule.present : null;
|
||||
if (expectedPresence != null) {
|
||||
const isPresent = isMeaningfullyPresent(actualValue);
|
||||
return isPresent === expectedPresence;
|
||||
}
|
||||
|
||||
if (type === 'boolean' || typeof actualValue === 'boolean') {
|
||||
if (expectedValues) {
|
||||
|
|
@ -153,6 +254,16 @@ function parseBooleanCondition(value: string): boolean {
|
|||
return value.trim().toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
function isMeaningfullyPresent(value: unknown): boolean {
|
||||
if (value == null) return false;
|
||||
if (typeof value === 'string') return value.trim().length > 0;
|
||||
if (Array.isArray(value)) return value.some((item) => isMeaningfullyPresent(item));
|
||||
if (typeof value === 'object') {
|
||||
return Object.values(value as Record<string, unknown>).some((item) => isMeaningfullyPresent(item));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function splitTemplatedTextParts(text: string | null): TemplatedTextPart[] {
|
||||
if (!text) return [];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { readUiConditionRule, resolveSchemaRef, type UiConditionRule } from './node-utility';
|
||||
import { readUiConditionRule, resolveSchemaRef, schemaFieldLabel, type UiConditionRule } from './node-utility';
|
||||
|
||||
export type RequiredField = {
|
||||
path: string;
|
||||
|
|
@ -17,20 +17,23 @@ export type ConditionalRequiredField = {
|
|||
|
||||
export type SchemaRequirements = {
|
||||
required: RequiredField[];
|
||||
requiredObjects: RequiredField[];
|
||||
conditional: ConditionalRequiredField[];
|
||||
};
|
||||
|
||||
export function extractSchemaRequirements(schema: Record<string, unknown> | null): SchemaRequirements {
|
||||
if (!schema) return { required: [], conditional: [] };
|
||||
if (!schema) return { required: [], requiredObjects: [], conditional: [] };
|
||||
|
||||
const required: RequiredField[] = [];
|
||||
const requiredObjects: RequiredField[] = [];
|
||||
const conditional: ConditionalRequiredField[] = [];
|
||||
const seenRequired = new Set<string>();
|
||||
const seenRequiredObjects = new Set<string>();
|
||||
const seenConditional = new Set<string>();
|
||||
|
||||
walkSchema(schema as Record<string, any>, schema as Record<string, any>, '', required, conditional, seenRequired, seenConditional);
|
||||
walkSchema(schema as Record<string, any>, schema as Record<string, any>, '', required, requiredObjects, conditional, seenRequired, seenRequiredObjects, seenConditional);
|
||||
|
||||
return { required, conditional };
|
||||
return { required, requiredObjects, conditional };
|
||||
}
|
||||
|
||||
function walkSchema(
|
||||
|
|
@ -38,8 +41,10 @@ function walkSchema(
|
|||
root: Record<string, any>,
|
||||
pathPrefix: string,
|
||||
required: RequiredField[],
|
||||
requiredObjects: RequiredField[],
|
||||
conditional: ConditionalRequiredField[],
|
||||
seenRequired: Set<string>,
|
||||
seenRequiredObjects: Set<string>,
|
||||
seenConditional: Set<string>,
|
||||
requireAllDescendants = false,
|
||||
inheritedRequiredWhen: UiConditionRule | null = null
|
||||
|
|
@ -56,12 +61,17 @@ function walkSchema(
|
|||
const propertyPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
const propertyResolved = resolveSchemaRef(propertySchema as Record<string, any>, root);
|
||||
const hasChildren = !!propertyResolved?.properties || propertyResolved?.type === 'object';
|
||||
const label = toLabel(key);
|
||||
const label = schemaFieldLabel(key, propertyResolved);
|
||||
const isRequiredBySchema = requiredSet.has(key);
|
||||
const isRequiredByAncestor = requireAllDescendants && key !== 'type';
|
||||
const isRequired = isRequiredBySchema || isRequiredByAncestor;
|
||||
const requiredWhen = readUiConditionRule(propertyResolved?.['x-ui-required-when']) ?? inheritedRequiredWhen;
|
||||
|
||||
if (isRequiredBySchema && hasChildren && key !== 'type' && !seenRequiredObjects.has(propertyPath)) {
|
||||
seenRequiredObjects.add(propertyPath);
|
||||
requiredObjects.push({ path: propertyPath, label });
|
||||
}
|
||||
|
||||
if (isRequired && !hasChildren && key !== 'type' && !seenRequired.has(propertyPath)) {
|
||||
seenRequired.add(propertyPath);
|
||||
required.push({ path: propertyPath, label });
|
||||
|
|
@ -106,8 +116,10 @@ function walkSchema(
|
|||
root,
|
||||
propertyPath,
|
||||
required,
|
||||
requiredObjects,
|
||||
conditional,
|
||||
seenRequired,
|
||||
seenRequiredObjects,
|
||||
seenConditional,
|
||||
isRequired && !childHasOwnRequired,
|
||||
requiredWhen
|
||||
|
|
@ -116,13 +128,6 @@ function walkSchema(
|
|||
}
|
||||
}
|
||||
|
||||
function toLabel(key: string): string {
|
||||
return key
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/^./, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function parseRetrieverUrl(rawUrl: unknown): { blockType: string; key: string } | null {
|
||||
if (typeof rawUrl !== 'string' || rawUrl.trim().length === 0) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
[class.llm-node-attention]="needsAttention()"
|
||||
[class.llm-node-error]="hasExecutionErrors()"
|
||||
[class.llm-node-warning]="hasExecutionWarnings() && !hasExecutionErrors()">
|
||||
@if (needsAttention()) {
|
||||
@if (needsAttention() && isHumanNode()) {
|
||||
<div class="llm-attention-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||
import { ChangeDetectorRef, Component, HostBinding, Input, inject } from '@angular/core';
|
||||
import { ClassicPreset } from 'rete';
|
||||
import { ReteModule } from 'rete-angular-plugin/21';
|
||||
import { FlowBlock, FlowContainer, FlowData, FlowPort } from '@models/flow';
|
||||
import { BlockInteractionContract, BlockType, FlowBlock, FlowContainer, FlowData, FlowPort } from '@models/flow';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { ContainersService } from '@services/containers/containers';
|
||||
import { SubflowPreviewDialogService } from '@services/dialogs/subflow-preview-dialog';
|
||||
|
|
@ -17,6 +17,9 @@ import {
|
|||
readUiConditionRule,
|
||||
readUiGroup,
|
||||
resolveSchemaRef,
|
||||
resolveSchemaPath,
|
||||
schemaFieldLabel,
|
||||
shouldSkipSchemaField,
|
||||
splitTemplatedTextParts,
|
||||
toStringOrNull,
|
||||
valueToDisplayString
|
||||
|
|
@ -101,6 +104,7 @@ export class TaskStepNodeComponent {
|
|||
interactionSubmitting = false;
|
||||
|
||||
private blockSchema: Record<string, any> | null = null;
|
||||
private blockDescriptor: BlockType | null = null;
|
||||
private variablePlaceholderPaths = new Set<string>();
|
||||
private arrayFieldDefinitions: ArrayFieldDefinition[] = [];
|
||||
private mainContentPaths = new Set<string>();
|
||||
|
|
@ -138,7 +142,7 @@ export class TaskStepNodeComponent {
|
|||
const richContentPaths = mainContentEntries.map((entry) => entry.path);
|
||||
this.mainContentFields = mainContentEntries.map((entry) => ({
|
||||
path: entry.path,
|
||||
label: pathToLabel(entry.path),
|
||||
label: this.displayLabelForPath(entry.path),
|
||||
parts: this.toMainContentParts(entry.path, String(entry.value))
|
||||
}));
|
||||
this.arrayFields = this.arrayFieldDefinitions
|
||||
|
|
@ -157,9 +161,9 @@ export class TaskStepNodeComponent {
|
|||
.filter((entry) => !this.isEmptyDisplayValue(entry.value))
|
||||
.map((entry) => ({
|
||||
path: entry.path,
|
||||
label: pathToLabel(entry.path),
|
||||
label: this.displayLabelForPath(entry.path),
|
||||
value: valueToDisplayString(entry.value),
|
||||
wide: this.shouldRenderWideField(pathToLabel(entry.path), this.mainContentPaths.has(entry.path))
|
||||
wide: this.shouldRenderWideField(this.displayLabelForPath(entry.path), this.mainContentPaths.has(entry.path))
|
||||
}));
|
||||
|
||||
for (const field of orderedFields) {
|
||||
|
|
@ -178,7 +182,7 @@ export class TaskStepNodeComponent {
|
|||
this.parameterFields = rootFields;
|
||||
this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({
|
||||
key,
|
||||
legend: key.startsWith('group:') ? key.slice('group:'.length) : pathToLabel(key),
|
||||
legend: key.startsWith('group:') ? key.slice('group:'.length) : this.displayLabelForPath(key),
|
||||
fields
|
||||
}));
|
||||
|
||||
|
|
@ -190,11 +194,12 @@ export class TaskStepNodeComponent {
|
|||
}
|
||||
|
||||
isHumanNode(): boolean {
|
||||
return this.blockType === 'HumanInteractionBlock';
|
||||
return !!this.interactionContract();
|
||||
}
|
||||
|
||||
isConditionalNode(): boolean {
|
||||
return this.blockType === 'ConditionalBlock';
|
||||
const outputNames = this.resolvePorts('output').map((port) => port.name.trim().toLowerCase());
|
||||
return outputNames.includes('true') && outputNames.includes('false');
|
||||
}
|
||||
|
||||
nodeTitle(): string {
|
||||
|
|
@ -248,7 +253,7 @@ export class TaskStepNodeComponent {
|
|||
}
|
||||
|
||||
isContainerNode(): boolean {
|
||||
return this.data?.data?.nodeFamily === 'container' || this.blockType === 'GenericContainer';
|
||||
return this.data?.data?.nodeFamily === 'container';
|
||||
}
|
||||
|
||||
hasViewableSubflow(): boolean {
|
||||
|
|
@ -364,19 +369,29 @@ export class TaskStepNodeComponent {
|
|||
|
||||
const executionId = this.executionId();
|
||||
const executionNodeId = this.executionNodeId();
|
||||
const interactionFieldName = this.interactionFieldName();
|
||||
if (!executionId || !executionNodeId || !interactionFieldName) return;
|
||||
const contract = this.interactionContract();
|
||||
if (!executionId || !executionNodeId || !contract) return;
|
||||
|
||||
const currentInput = this.currentInputValue();
|
||||
const actionDescription = this.actionDescriptionValue();
|
||||
|
||||
const result = await this.humanInteractionDialog.open({
|
||||
title: `Send output for ${this.name || 'Interaction Step'}`,
|
||||
title: this.interactionDialogTitle(contract),
|
||||
kind: contract.kind,
|
||||
actionDescription,
|
||||
currentInput
|
||||
currentInput,
|
||||
history: this.chatHistory(contract),
|
||||
latestResponse: this.latestInteractionResponse(contract),
|
||||
messageField: contract.messageField,
|
||||
completionField: contract.completionField
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
const interactionFieldName = result.mode === 'message'
|
||||
? contract.messageField
|
||||
: contract.completionField;
|
||||
if (!interactionFieldName) return;
|
||||
|
||||
this.interactionSubmitting = true;
|
||||
this.taskExecutionsService.submitInteractionText(
|
||||
executionId,
|
||||
|
|
@ -432,11 +447,6 @@ export class TaskStepNodeComponent {
|
|||
return typeof value === 'string' && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
private interactionFieldName(): string | null {
|
||||
const outputKey = this.outputs[0]?.key;
|
||||
return typeof outputKey === 'string' && outputKey.length > 0 ? outputKey : null;
|
||||
}
|
||||
|
||||
private getExecutionMessages(key: '__executionErrors' | '__executionWarnings'): string[] {
|
||||
const values = this.blockConfiguration?.[key];
|
||||
if (!Array.isArray(values)) return [];
|
||||
|
|
@ -499,12 +509,13 @@ export class TaskStepNodeComponent {
|
|||
const type = this.blockType;
|
||||
if (!type) return;
|
||||
|
||||
const nodeFamily = this.data?.data?.nodeFamily === 'container' || type === 'GenericContainer'
|
||||
const nodeFamily = this.data?.data?.nodeFamily === 'container'
|
||||
? 'container'
|
||||
: 'block';
|
||||
const typeDescriptor = nodeFamily === 'container'
|
||||
? await this.containersService.getContainerType(type)
|
||||
: await this.blocksService.getBlockType(type);
|
||||
this.blockDescriptor = (typeDescriptor ?? null) as BlockType | null;
|
||||
this.blockSchema = (typeDescriptor?.schema ?? null) as Record<string, any> | null;
|
||||
this.variablePlaceholderPaths = this.extractVariablePlaceholderPaths(this.blockSchema);
|
||||
this.arrayFieldDefinitions = this.extractArrayFieldDefinitions(this.blockSchema);
|
||||
|
|
@ -512,6 +523,113 @@ export class TaskStepNodeComponent {
|
|||
this.rebuildDisplayState();
|
||||
}
|
||||
|
||||
private interactionContract(): BlockInteractionContract | null {
|
||||
return this.blockDescriptor?.interactionContract ?? null;
|
||||
}
|
||||
|
||||
private interactionDialogTitle(contract: BlockInteractionContract): string {
|
||||
if (contract.kind === 'chat-session') {
|
||||
return `Chat with ${this.name || 'Interaction Step'}`;
|
||||
}
|
||||
return `Send response for ${this.name || 'Interaction Step'}`;
|
||||
}
|
||||
|
||||
private executionPartialResult(): Record<string, unknown> | null {
|
||||
const value = this.blockConfiguration?.['__executionPartialResult'];
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
private executionResultData(): Record<string, unknown> | null {
|
||||
const value = this.blockConfiguration?.['__executionResultData'];
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
private stepResultData(): Record<string, unknown> | null {
|
||||
const value = this.blockConfiguration?.['__stepResultData'];
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
private executionScopedFieldName(fieldName: string): string | null {
|
||||
const nodeId = this.executionNodeId();
|
||||
return nodeId ? `${nodeId}:${fieldName}` : null;
|
||||
}
|
||||
|
||||
private interactionFieldValue(fieldName: string | null | undefined, preferPartial: boolean): unknown {
|
||||
if (!fieldName) return undefined;
|
||||
|
||||
if (!preferPartial) {
|
||||
const stepSource = this.stepResultData();
|
||||
if (stepSource && Object.prototype.hasOwnProperty.call(stepSource, fieldName)) {
|
||||
return stepSource[fieldName];
|
||||
}
|
||||
}
|
||||
|
||||
const scopedFieldName = this.executionScopedFieldName(fieldName);
|
||||
const executionSource = preferPartial ? this.executionPartialResult() : this.executionResultData();
|
||||
if (executionSource) {
|
||||
if (Object.prototype.hasOwnProperty.call(executionSource, fieldName)) {
|
||||
return executionSource[fieldName];
|
||||
}
|
||||
if (scopedFieldName && Object.prototype.hasOwnProperty.call(executionSource, scopedFieldName)) {
|
||||
return executionSource[scopedFieldName];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private chatHistory(contract: BlockInteractionContract): Array<{ role: 'user' | 'assistant' | 'system'; content: string }> {
|
||||
const historyField = contract.historyField;
|
||||
if (!historyField) return [];
|
||||
|
||||
const rawHistory = this.interactionFieldValue(historyField, Boolean(contract.supportsPartialResult));
|
||||
if (!Array.isArray(rawHistory)) return [];
|
||||
|
||||
return rawHistory
|
||||
.map((entry) => {
|
||||
if (typeof entry === 'string') {
|
||||
return this.parseChatHistoryLine(entry);
|
||||
}
|
||||
if (!entry || typeof entry !== 'object') return null;
|
||||
const record = entry as Record<string, unknown>;
|
||||
const role = record['role'];
|
||||
const content = record['content'] ?? record['message'] ?? record['text'];
|
||||
if ((role !== 'user' && role !== 'assistant' && role !== 'system') || typeof content !== 'string') {
|
||||
return null;
|
||||
}
|
||||
return { role, content };
|
||||
})
|
||||
.filter((entry): entry is { role: 'user' | 'assistant' | 'system'; content: string } => entry != null);
|
||||
}
|
||||
|
||||
private latestInteractionResponse(contract: BlockInteractionContract): string {
|
||||
const fieldName = contract.responseField;
|
||||
if (!fieldName) return '';
|
||||
const value =
|
||||
this.interactionFieldValue(fieldName, true)
|
||||
?? this.interactionFieldValue(fieldName, false);
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
|
||||
private parseChatHistoryLine(rawLine: string): { role: 'user' | 'assistant' | 'system'; content: string } | null {
|
||||
const line = rawLine.trim();
|
||||
if (!line) return null;
|
||||
|
||||
const prefixed = line.match(/^\[(USER|ASSISTANT|SYSTEM)\]\s*([\s\S]*)$/i);
|
||||
if (prefixed) {
|
||||
const role = prefixed[1].toLowerCase() as 'user' | 'assistant' | 'system';
|
||||
return {
|
||||
role,
|
||||
content: prefixed[2] ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: line
|
||||
};
|
||||
}
|
||||
|
||||
private extractVariablePlaceholderPaths(schema: Record<string, any> | null): Set<string> {
|
||||
const paths = new Set<string>();
|
||||
if (!schema) return paths;
|
||||
|
|
@ -565,13 +683,13 @@ export class TaskStepNodeComponent {
|
|||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
|
||||
if (childResolved?.type === 'array') {
|
||||
if (key === 'type' || key === 'name' || key.startsWith('__') || seen.has(path)) {
|
||||
if (shouldSkipSchemaField(key, childResolved) || key === 'name' || seen.has(path)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(path);
|
||||
definitions.push({
|
||||
path,
|
||||
label: pathToLabel(path),
|
||||
label: schemaFieldLabel(path, childResolved),
|
||||
itemSchema: this.resolveArrayItemSchema(childResolved, schema)
|
||||
});
|
||||
continue;
|
||||
|
|
@ -729,6 +847,10 @@ export class TaskStepNodeComponent {
|
|||
return current;
|
||||
}
|
||||
|
||||
private displayLabelForPath(path: string): string {
|
||||
return schemaFieldLabel(path, this.resolveFieldSchema(path));
|
||||
}
|
||||
|
||||
private shouldRenderWideField(label: string, isTextarea: boolean) {
|
||||
return isTextarea || label.trim().length >= 18;
|
||||
}
|
||||
|
|
@ -767,19 +889,7 @@ export class TaskStepNodeComponent {
|
|||
}
|
||||
|
||||
private resolveFieldSchema(path: string): Record<string, any> | null {
|
||||
const root = this.blockSchema;
|
||||
if (!root) return null;
|
||||
|
||||
let current: Record<string, any> | null = root;
|
||||
for (const segment of path.split('.')) {
|
||||
if (!current) return null;
|
||||
const resolved = resolveSchemaRef(current, root);
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return null;
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
}
|
||||
|
||||
return current;
|
||||
return resolveSchemaPath(this.blockSchema, path);
|
||||
}
|
||||
|
||||
private getFieldUiMeta(path: string) {
|
||||
|
|
@ -792,9 +902,15 @@ export class TaskStepNodeComponent {
|
|||
for (const segment of path.split('.')) {
|
||||
if (!current) return this.toFieldUiMeta(null, inheritedUi);
|
||||
const resolved = resolveSchemaRef(current, root);
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return this.toFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
if (/^\d+$/.test(segment)) {
|
||||
const items = resolved?.items;
|
||||
if (!items || typeof items !== 'object') return this.toFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(items as Record<string, any>, root);
|
||||
} else {
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return this.toFieldUiMeta(null, inheritedUi);
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
}
|
||||
const nextUi = this.toFieldUiMeta(current, inheritedUi);
|
||||
inheritedUi = {
|
||||
visibleWhen: nextUi.visibleWhen,
|
||||
|
|
|
|||
|
|
@ -149,7 +149,10 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
__executionOutputs: this.getExecutionOutputValues(step, contextResults),
|
||||
__connectedOutputs: this.getConnectedOutputs(step),
|
||||
__executionErrors: this.getExecutionErrors(step.id, contextErrors),
|
||||
__executionWarnings: this.getExecutionWarnings(step.id, contextWarnings)
|
||||
__executionWarnings: this.getExecutionWarnings(step.id, contextWarnings),
|
||||
__stepResultData: step.result ?? null,
|
||||
__executionPartialResult: (this.execution()?.context as Record<string, unknown> | undefined)?.['partialResult'] ?? null,
|
||||
__executionResultData: this.execution()?.context.result ?? {}
|
||||
},
|
||||
position: stepNode.position ?? {
|
||||
x: 120 + (index % 3) * 340,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en" data-theme="nord" >
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>HumainFlowGuiA21</title>
|
||||
<title>HumAInFlow</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
|
|
|
|||
Loading…
Reference in New Issue