Add schema-driven block IO compatibility and bindable inputs

This commit is contained in:
Lucio Lelii 2026-03-13 16:51:53 +01:00
parent c0d2f75285
commit 6f3ca99b1e
6 changed files with 429 additions and 44 deletions

View File

@ -47,10 +47,16 @@ export type FlowBlock = {
typeName: BlockTypeName;
};
export type FlowValueKind = {
type: string;
multiple: boolean;
};
export type FlowPort = {
name: string;
type: string;
multiple: boolean;
valueKinds?: FlowValueKind[];
};
export type FlowBlockConnection = {
@ -86,3 +92,46 @@ export type HumanInteractiveBlockConfiguration = {
inputAsList: boolean;
outputAsList: boolean;
};
export function currentFlowPortValueKind(port: FlowPort): FlowValueKind {
return {
type: String(port.type ?? 'ANY').toUpperCase(),
multiple: Boolean(port.multiple)
};
}
export function normalizeFlowPortValueKinds(port: FlowPort): FlowValueKind[] {
const rawKinds = Array.isArray(port.valueKinds) ? port.valueKinds : [];
const normalized = rawKinds
.filter((kind): kind is FlowValueKind => !!kind && typeof kind.type === 'string')
.map((kind) => ({
type: String(kind.type).toUpperCase(),
multiple: Boolean(kind.multiple)
}));
if (!normalized.length) {
return [currentFlowPortValueKind(port)];
}
const unique = new Map<string, FlowValueKind>();
for (const kind of normalized) {
unique.set(`${kind.type}:${kind.multiple ? 'multi' : 'single'}`, kind);
}
return Array.from(unique.values());
}
export function flowValueKindLabel(kind: FlowValueKind): string {
const type = String(kind.type ?? 'ANY').toUpperCase();
return kind.multiple ? `${type}[]` : type;
}
export function areFlowValueKindsCompatible(sourceKinds: FlowValueKind[], targetKinds: FlowValueKind[]): boolean {
return sourceKinds.some((source) =>
targetKinds.some((target) => {
if (Boolean(source.multiple) !== Boolean(target.multiple)) return false;
const sourceType = String(source.type ?? 'ANY').toUpperCase();
const targetType = String(target.type ?? 'ANY').toUpperCase();
return sourceType === 'ANY' || targetType === 'ANY' || sourceType === targetType;
})
);
}

View File

@ -124,10 +124,30 @@ export class BlocksCallService extends BlocksCallServiceBase {
.map((port) => ({
name: String(port["name"]),
type: String(port["type"] ?? "TEXT"),
multiple: Boolean(port["multiple"] ?? false)
multiple: Boolean(port["multiple"] ?? false),
valueKinds: this.toValueKinds(port["valueKinds"], {
type: String(port["type"] ?? "TEXT"),
multiple: Boolean(port["multiple"] ?? false)
})
}));
}
private toValueKinds(raw: unknown, fallback: { type: string; multiple: boolean }) {
if (!Array.isArray(raw)) {
return [{ type: fallback.type, multiple: fallback.multiple }];
}
const kinds = raw
.map((item) => this.toRecord(item))
.filter((item) => typeof item["type"] === "string")
.map((item) => ({
type: String(item["type"] ?? fallback.type),
multiple: Boolean(item["multiple"] ?? false)
}));
return kinds.length ? kinds : [{ type: fallback.type, multiple: fallback.multiple }];
}
private toPosition(raw: unknown): { x: number; y: number } | undefined {
const value = this.toRecord(raw);
const x = value["x"];

View File

@ -445,7 +445,7 @@
.llm-row {
display: grid;
align-items: center;
height: 24px;
min-height: 34px;
min-width: 0;
}
@ -469,20 +469,45 @@
}
.llm-pill {
height: 24px;
min-height: 32px;
border-radius: 7px;
border: 1px solid transparent;
font-size: 11px;
font-weight: 600;
padding: 0 8px;
padding: 4px 8px;
display: inline-flex;
align-items: center;
min-width: 0;
overflow: hidden;
}
.llm-pill-meta {
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1px;
}
.llm-pill-meta-output {
align-items: flex-end;
}
.llm-pill-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.llm-pill-kind,
.llm-pill-kind-select {
font-size: 10px;
line-height: 1;
font-weight: 700;
opacity: 0.86;
}
.llm-pill-input {
justify-content: flex-start;
color: #166534;
@ -523,6 +548,17 @@
border-color: #fecdd3;
}
.llm-pill-kind-select {
max-width: 88px;
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 4px;
background: rgba(255, 255, 255, 0.72);
color: inherit;
padding: 1px 16px 1px 4px;
cursor: pointer;
appearance: auto;
}
.llm-input-info {
border: none;
background: transparent;

View File

@ -86,7 +86,23 @@
[emit]="emit">
</div>
<span class="llm-pill llm-pill-input">
{{ input.key }}
<span class="llm-pill-meta">
<span class="llm-pill-name">{{ inputDisplayLabel(input.key) }}</span>
@if (canTogglePortMultiplicity('input', input.key)) {
<select
class="llm-pill-kind-select"
[ngModel]="portCurrentKindValue('input', input.key)"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation()"
(ngModelChange)="onPortKindChange('input', input.key, $event)">
@for (option of portSelectableKindOptions('input', input.key); track option.value) {
<option [value]="option.value">{{ option.label }}</option>
}
</select>
} @else {
<span class="llm-pill-kind">{{ portCurrentKindLabel('input', input.key) }}</span>
}
</span>
</span>
</div>
}
@ -96,7 +112,25 @@
<div class="llm-column-title llm-column-title-right">{{ outputsTitle() }}</div>
@for (output of outputs; track output.key) {
<div class="llm-row llm-row-output">
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)">{{ output.key }}</span>
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)">
<span class="llm-pill-meta llm-pill-meta-output">
<span class="llm-pill-name">{{ outputDisplayLabel(output.key) }}</span>
@if (canTogglePortMultiplicity('output', output.key)) {
<select
class="llm-pill-kind-select"
[ngModel]="portCurrentKindValue('output', output.key)"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation()"
(ngModelChange)="onPortKindChange('output', output.key, $event)">
@for (option of portSelectableKindOptions('output', output.key); track option.value) {
<option [value]="option.value">{{ option.label }}</option>
}
</select>
} @else {
<span class="llm-pill-kind">{{ portCurrentKindLabel('output', output.key) }}</span>
}
</span>
</span>
<div
refComponent
class="llm-socket llm-socket-right"
@ -242,9 +276,32 @@
<div class="llm-modal-backdrop" (pointerdown)="$event.stopPropagation()" (click)="closeSimpleParamEditor($event)">
<div class="llm-modal" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<div class="llm-modal-title">Edit {{ localEditorLabel }}</div>
@if (localEditorBindableAsInput && localEditorPath !== 'name') {
<div class="llm-modal-field">
<label>Source</label>
<select
[ngModel]="localEditorUseInput ? 'input' : 'static'"
(pointerdown)="$event.stopPropagation()"
(ngModelChange)="onLocalEditorSourceModeChange($event)">
<option value="static">Static value</option>
<option value="input">Workflow input</option>
</select>
@if (localEditorUseInput) {
<div class="llm-modal-tip">
This field will be provided by node input{{ localEditorBindableInputName ? (' "' + localEditorBindableInputName + '"') : '' }}.
</div>
}
</div>
}
<div class="llm-modal-field">
<label>{{ localEditorLabel }}</label>
@if (localEditorLoading) {
@if (localEditorUseInput && localEditorPath !== 'name') {
<input
type="text"
[ngModel]="localEditorBindableInputName ? ('Provided by input ' + localEditorBindableInputName) : 'Provided by workflow input'"
readonly
(pointerdown)="$event.stopPropagation()" />
} @else if (localEditorLoading) {
<input type="text" [ngModel]="localEditorValue" placeholder="Loading..." disabled (pointerdown)="$event.stopPropagation()" />
} @else if (localEditorType === 'boolean') {
<button
@ -259,6 +316,13 @@
</span>
<span class="node-toggle-label">{{ localEditorValue === 'true' ? 'true' : 'false' }}</span>
</button>
} @else if (localEditorWidget === 'textarea') {
<textarea
rows="6"
[(ngModel)]="localEditorValue"
[attr.maxlength]="localEditorMaxLength"
[placeholder]="localEditorMaxLength ? 'Max ' + localEditorMaxLength + ' characters' : 'Value...'"
(pointerdown)="$event.stopPropagation()"></textarea>
} @else if (localEditorHasRetriever) {
<select [(ngModel)]="localEditorValue" (pointerdown)="$event.stopPropagation()">
<option value="">{{ localEditorOptions.length ? ('Select ' + (localEditorLabel | lowercase) + '...') : 'No options available' }}</option>
@ -277,7 +341,7 @@
</div>
<div class="llm-modal-actions">
<button type="button" class="llm-btn llm-btn-ghost" (pointerdown)="$event.stopPropagation()" (click)="closeSimpleParamEditor($event)">Cancel</button>
<button type="button" class="llm-btn llm-btn-primary" [disabled]="localEditorLoading" (pointerdown)="$event.stopPropagation()" (click)="saveSimpleParamEditor($event)">Save</button>
<button type="button" class="llm-btn llm-btn-primary" [disabled]="!canSaveLocalEditor()" (pointerdown)="$event.stopPropagation()" (click)="saveSimpleParamEditor($event)">Save</button>
</div>
</div>
</div>

View File

@ -1,9 +1,9 @@
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 { ClassicPreset } from 'rete';
import { ReteModule } from 'rete-angular-plugin/21';
import { FlowData } from '@models/flow';
import {
NodeSettingField,
NodeSettingOption,
@ -40,6 +40,7 @@ type EditableFieldDefinition = {
path: string;
label: string;
type: FieldType;
enumOptions: string[];
retrieverBlockType: string | null;
retrieverKey: string | null;
retrieverUrl: string | null;
@ -48,6 +49,10 @@ type EditableFieldDefinition = {
widget: 'textarea' | null;
acceptVariableAsPlaceholder: boolean;
structural: boolean;
bindableAsInput: boolean;
inputName: string | null;
inputType: string | null;
inputMultiple: boolean | null;
structuralReason?: string;
placeholder?: string;
tip?: string;
@ -140,6 +145,10 @@ export class GenericNodeComponent {
localEditorHasRetriever = false;
localEditorType: FieldType = 'string';
localEditorMaxLength: number | null = null;
localEditorWidget: 'textarea' | null = null;
localEditorBindableAsInput = false;
localEditorUseInput = false;
localEditorBindableInputName: string | null = null;
deleteConfirmOpen = false;
missingRequiredParams: string[] = [];
@ -192,6 +201,10 @@ export class GenericNodeComponent {
this.localEditorLoading = false;
this.localEditorHasRetriever = false;
this.localEditorOpen = true;
this.localEditorWidget = null;
this.localEditorBindableAsInput = false;
this.localEditorUseInput = false;
this.localEditorBindableInputName = null;
}
async openParameterEditor(path: string, event?: Event) {
@ -202,26 +215,21 @@ export class GenericNodeComponent {
if (!definition) return;
if (!this.isFieldVisible(definition)) return;
if (definition.ui.widget === 'textarea') {
const currentValue = this.valueToEditorString(
this.getByPath(this.blockConfiguration ?? {}, definition.path),
definition.type
);
await this.openTextareaEditor(definition.path, definition.label, currentValue, definition.ui);
return;
}
this.localEditorPath = definition.path;
this.localEditorLabel = definition.label;
this.localEditorType = definition.type;
this.localEditorMaxLength = null;
this.localEditorWidget = definition.ui.widget;
this.localEditorValue = this.valueToEditorString(this.getByPath(this.blockConfiguration ?? {}, definition.path), definition.type);
this.localEditorOptions = [];
this.localEditorLoading = !!definition.retrieverKey;
this.localEditorHasRetriever = !!definition.retrieverKey;
this.localEditorOptions = [...definition.enumOptions];
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.localEditorOpen = true;
if (definition.retrieverKey) {
if (definition.retrieverKey && !this.localEditorUseInput) {
const missingDependencies = definition.retrieverDependsOn
.filter((dep) => {
const value = this.getByPath(this.blockConfiguration ?? {}, dep.path);
@ -250,6 +258,10 @@ export class GenericNodeComponent {
this.localEditorHasRetriever = false;
this.localEditorType = 'string';
this.localEditorMaxLength = null;
this.localEditorWidget = null;
this.localEditorBindableAsInput = false;
this.localEditorUseInput = false;
this.localEditorBindableInputName = null;
}
saveSimpleParamEditor(event?: Event) {
@ -257,6 +269,7 @@ export class GenericNodeComponent {
event?.stopPropagation();
if (!this.localEditorPath) return;
if (!this.canSaveLocalEditor()) return;
const config = this.ensureBlockConfiguration();
if (this.localEditorPath === 'name') {
@ -265,7 +278,9 @@ export class GenericNodeComponent {
this.name = nameValue || this.name;
} else {
const previousValue = this.getByPath(config, this.localEditorPath);
const parsedValue = this.parseEditorValue(this.localEditorValue, this.localEditorType);
const parsedValue = this.localEditorUseInput
? this.emptyValueForFieldType(this.localEditorType)
: this.parseEditorValue(this.localEditorValue, this.localEditorType);
this.setByPath(config, this.localEditorPath, parsedValue);
if (!this.areValuesEqual(previousValue, parsedValue)) {
this.resetDependentRetrieverFields(config, this.localEditorPath);
@ -345,6 +360,71 @@ export class GenericNodeComponent {
return null;
}
inputDisplayLabel(inputKey: string): string {
return this.portDisplayLabel('input', inputKey);
}
outputDisplayLabel(outputKey: string): string {
return this.portDisplayLabel('output', outputKey);
}
canTogglePortMultiplicity(kind: 'input' | 'output', key: string): boolean {
const port = this.resolvePorts(kind).find((candidate) => candidate.name === key);
return !!port && this.portSelectableKinds(port).length > 1;
}
portCurrentKindLabel(kind: 'input' | 'output', key: string): string {
const port = this.resolvePorts(kind).find((candidate) => candidate.name === key);
return port ? flowValueKindLabel(currentFlowPortValueKind(port)) : 'ANY';
}
portCurrentKindValue(kind: 'input' | 'output', key: string): string {
const port = this.resolvePorts(kind).find((candidate) => candidate.name === key);
if (!port) return '';
const current = currentFlowPortValueKind(port);
const exact = this.portSelectableKinds(port).find((kindOption) =>
kindOption.type === current.type && kindOption.multiple === current.multiple
);
const selected = exact ?? this.portSelectableKinds(port)[0];
return selected ? this.flowValueKindValue(selected) : '';
}
portSelectableKindOptions(kind: 'input' | 'output', key: string): Array<{ value: string; label: string }> {
const port = this.resolvePorts(kind).find((candidate) => candidate.name === key);
if (!port) return [];
return this.portSelectableKinds(port).map((kindOption) => ({
value: this.flowValueKindValue(kindOption),
label: flowValueKindLabel(kindOption)
}));
}
onPortKindChange(kind: 'input' | 'output', key: string, nextValue: string, event?: Event) {
event?.preventDefault();
event?.stopPropagation();
const ports = this.resolvePorts(kind);
const index = ports.findIndex((candidate) => candidate.name === key);
if (index < 0) return;
const port = ports[index];
const nextKind = this.portSelectableKinds(port).find(
(kindOption) => this.flowValueKindValue(kindOption) === nextValue
);
if (!nextKind) return;
ports[index] = {
...port,
type: nextKind.type,
multiple: nextKind.multiple
};
this.syncPortSocketType(kind, key, nextKind.type);
this.markFlowDirty();
this.refreshView();
}
hasMainContent(): boolean {
return this.richContentFields.length > 0;
}
@ -489,6 +569,7 @@ export class GenericNodeComponent {
path,
label: pathToLabel(path),
type: this.toFieldType(childResolved?.type),
enumOptions: this.toEnumOptions(childResolved),
retrieverBlockType: this.toRetrieverBlockType(childResolved),
retrieverKey: this.toRetrieverKey(childResolved),
retrieverUrl: this.toRetrieverUrl(childResolved),
@ -577,6 +658,16 @@ export class GenericNodeComponent {
: undefined;
const acceptVariableAsPlaceholder = schema?.['x-ui-accept-variable-as-placeholder'] === true;
const structural = schema?.['x-ui-structural'] === true;
const bindableAsInput = schema?.['x-ui-bindable-as-input'] === true;
const inputName = typeof schema?.['x-ui-input-name'] === 'string'
? String(schema['x-ui-input-name'])
: null;
const inputType = typeof schema?.['x-ui-input-type'] === 'string'
? String(schema['x-ui-input-type']).toUpperCase()
: null;
const inputMultiple = typeof schema?.['x-ui-input-multiple'] === 'boolean'
? Boolean(schema['x-ui-input-multiple'])
: null;
const structuralReason = typeof schema?.['x-ui-structural-reason'] === 'string'
? String(schema['x-ui-structural-reason'])
: undefined;
@ -587,6 +678,10 @@ export class GenericNodeComponent {
widget: normalizedWidget,
acceptVariableAsPlaceholder,
structural,
bindableAsInput,
inputName,
inputType,
inputMultiple,
structuralReason,
placeholder,
tip,
@ -665,6 +760,12 @@ export class GenericNodeComponent {
return this.parseRetrieverUrl(schema['x-retriever-url'])?.blockType ?? null;
}
private toEnumOptions(schema: Record<string, any> | null | undefined): string[] {
const raw = schema?.['enum'];
if (!Array.isArray(raw)) return [];
return raw.filter((value): value is string => typeof value === 'string');
}
private toRetrieverUrl(schema: Record<string, any> | null | undefined): string | null {
if (!schema || typeof schema !== 'object') return null;
const rawUrl = schema['x-retriever-url'];
@ -686,6 +787,103 @@ export class GenericNodeComponent {
return { blockType, key };
}
private portDisplayLabel(kind: 'input' | 'output', key: string): string {
const ports = this.resolvePorts(kind);
const port = ports.find((candidate) => candidate.name === key);
return port?.name ?? key;
}
private portValueKinds(port: FlowPort) {
return normalizeFlowPortValueKinds(port);
}
private portSelectableKinds(port: FlowPort): FlowValueKind[] {
const expanded = new Map<string, FlowValueKind>();
for (const kind of this.portValueKinds(port)) {
const type = String(kind.type ?? 'ANY').toUpperCase();
if (type === 'ANY') {
for (const concreteType of ['TEXT', 'FILE']) {
const concreteKind = { type: concreteType, multiple: Boolean(kind.multiple) };
expanded.set(this.flowValueKindValue(concreteKind), concreteKind);
}
continue;
}
expanded.set(this.flowValueKindValue(kind), kind);
}
return Array.from(expanded.values());
}
private resolvePorts(kind: 'input' | 'output'): FlowPort[] {
const ports = this.data?.data?.[kind === 'input' ? 'inputs' : 'outputs'];
return Array.isArray(ports) ? ports as FlowPort[] : [];
}
private flowValueKindValue(kind: FlowValueKind): string {
return `${String(kind.type ?? 'ANY').toUpperCase()}::${kind.multiple ? 'multi' : 'single'}`;
}
private isBindableFieldUsingInput(definition: EditableFieldDefinition): boolean {
if (!definition.ui.bindableAsInput) return false;
const value = this.getByPath(this.blockConfiguration ?? {}, definition.path);
if (!this.isMissingValue(value)) return false;
const actualInputName = definition.ui.inputName;
if (!actualInputName) return true;
return this.resolvePorts('input').some((port) => port.name === actualInputName);
}
private fieldDisplayValue(definition: EditableFieldDefinition, value: unknown): string {
if (this.isBindableFieldUsingInput(definition)) {
const actualPort = this.resolvePorts('input').find((port) => port.name === definition.ui.inputName);
const fallbackType = definition.ui.inputType ?? definition.type.toUpperCase();
const fallbackMultiple = Boolean(definition.ui.inputMultiple);
const kindLabel = actualPort
? flowValueKindLabel(currentFlowPortValueKind(actualPort))
: flowValueKindLabel({ type: fallbackType, multiple: fallbackMultiple });
const inputName = definition.ui.inputName ?? definition.label;
return `Provided by input ${inputName} (${kindLabel})`;
}
return valueToDisplayString(value);
}
canSaveLocalEditor(): boolean {
if (this.localEditorLoading) return false;
if (!this.localEditorBindableAsInput || this.localEditorUseInput) return true;
if (this.localEditorType === 'boolean') return true;
return this.localEditorValue.trim().length > 0;
}
private emptyValueForFieldType(type: FieldType): unknown {
if (type === 'number' || type === 'integer' || type === 'boolean') return null;
return '';
}
private syncPortSocketType(kind: 'input' | 'output', key: string, type: string) {
const socketHost = this.data?.[kind === 'input' ? 'inputs' : 'outputs']?.[key];
if (!socketHost) return;
socketHost.socket = new ClassicPreset.Socket(type);
}
async onLocalEditorSourceModeChange(nextMode: string) {
this.localEditorUseInput = nextMode === 'input';
if (this.localEditorUseInput || !this.localEditorPath) {
this.localEditorLoading = false;
return;
}
const definition = this.editableFieldDefinitions.find((field) => field.path === this.localEditorPath);
if (!definition?.retrieverKey || this.localEditorOptions.length > 0) return;
this.localEditorLoading = true;
await this.loadLocalEditorOptions(definition);
}
private toRetrieverDependsOn(schema: Record<string, any> | null | undefined, pathPrefix: string): RetrieverDependency[] {
if (!schema || typeof schema !== 'object') return [];
@ -759,7 +957,7 @@ export class GenericNodeComponent {
return {
path: definition.path,
label: definition.label,
value: valueToDisplayString(value),
value: this.fieldDisplayValue(definition, value),
wide: this.shouldRenderWideField(definition.label, definition.ui.widget === 'textarea')
};
}).filter((field) => !richContentPaths.has(field.path))
@ -1403,7 +1601,11 @@ export class GenericNodeComponent {
.filter((field) => this.isFieldConditionSatisfied(field.path));
const missingFields = requiredFields
.filter((field) => this.isMissingValue(this.getByPath(config, field.path)));
.filter((field) => {
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);
@ -1569,10 +1771,15 @@ export class GenericNodeComponent {
return this.isPathVisible(field.path);
}
private isFieldConditionSatisfied(path: string): boolean {
private isFieldConditionSatisfied(path: string, visited = new Set<string>()): boolean {
if (visited.has(path)) return true;
visited.add(path);
const ui = this.getFieldUiMeta(path);
return ui.visibleWhen.every((rule) =>
evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath))
);
return ui.visibleWhen.every((rule) => {
if (!rule) return true;
if (!this.isFieldConditionSatisfied(rule.field, visited)) return false;
return evaluateUiConditionRule(rule, this.blockConfiguration, (fieldPath) => this.resolveFieldSchema(fieldPath));
});
}
}

View File

@ -7,7 +7,7 @@ import {
} from "rete-connection-plugin";
import { AngularPlugin, Presets, AngularArea2D } from "rete-angular-plugin/21";
import { HFNode, HFSchemes } from "@models/nodes";
import { FlowBlock, FlowData } from "@models/flow";
import { areFlowValueKindsCompatible, FlowBlock, FlowData, normalizeFlowPortValueKinds } from "@models/flow";
import { GenericNodeComponent } from "@shared/nodes/generic-node/generic-node";
import { TaskStepNodeComponent } from "@shared/nodes/task-step-node/task-step-node";
import { CustomSocket } from "@shared/custom-socket/custom-socket";
@ -55,10 +55,22 @@ export async function createEditor(
},
})
);
editor.addPipe((context) => {
if (context.type !== "connectioncreate") return context;
editor.addPipe((c) => {
if (c.type === "connectioncreate") console.log(c.data);
return c;
const sourceNode = editor.getNode(context.data.source) as HFNode | undefined;
const targetNode = editor.getNode(context.data.target) as HFNode | undefined;
const sourcePort = resolveNodePort(sourceNode, "output", context.data.sourceOutput);
const targetPort = resolveNodePort(targetNode, "input", context.data.targetInput);
if (!sourcePort || !targetPort) return;
const compatible = areFlowValueKindsCompatible(
normalizeFlowPortValueKinds(sourcePort),
normalizeFlowPortValueKinds(targetPort)
);
return compatible ? context : undefined;
});
connection.addPreset(ConnectionPresets.classic.setup());
@ -85,17 +97,8 @@ export function exportGraph(editor: NodeEditor<HFSchemes>) {
const blockId = blockData?.id ?? node.id;
nodeIdToBlockId.set(node.id, blockId);
const inputs = Object.entries(node.inputs).map(([name, input]) => ({
name,
type: ((input as any).socket?.name as string) ?? "ANY",
multiple: false
}));
const outputs = Object.entries(node.outputs).map(([name, output]) => ({
name,
type: ((output as any).socket?.name as string) ?? "ANY",
multiple: false
}));
const inputs = cloneValue(blockData?.inputs ?? []);
const outputs = cloneValue(blockData?.outputs ?? []);
return {
id: blockId,
@ -256,6 +259,12 @@ function getSocket(editor: NodeEditor<HFSchemes>, type: string) {
return map.get(type)!;
}
function resolveNodePort(node: HFNode | undefined, kind: "input" | "output", portName: string) {
const ports = node?.data?.[kind === "input" ? "inputs" : "outputs"];
if (!Array.isArray(ports)) return null;
return ports.find((port) => port?.name === portName) ?? null;
}
function toNodeLabel(typeName: string) {
if (typeName === "InputBlock" || typeName === "SourceBlock") return "Input";
if (typeName === "OutputBlock") return "Output";