From 6f3ca99b1e66d1c02d202e73989121c6db516f06 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Fri, 13 Mar 2026 16:51:53 +0100 Subject: [PATCH] Add schema-driven block IO compatibility and bindable inputs --- src/app/models/flow.ts | 49 ++++ src/app/services/blocks/blocks-call.ts | 22 +- .../nodes/generic-node/generic-node.css | 42 ++- .../nodes/generic-node/generic-node.html | 72 ++++- .../shared/nodes/generic-node/generic-node.ts | 249 ++++++++++++++++-- src/app/utilities/rete-editor.ts | 39 +-- 6 files changed, 429 insertions(+), 44 deletions(-) diff --git a/src/app/models/flow.ts b/src/app/models/flow.ts index 4a1383b..ab0d19a 100644 --- a/src/app/models/flow.ts +++ b/src/app/models/flow.ts @@ -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(); + 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; + }) + ); +} diff --git a/src/app/services/blocks/blocks-call.ts b/src/app/services/blocks/blocks-call.ts index d29a32b..6fe99ef 100644 --- a/src/app/services/blocks/blocks-call.ts +++ b/src/app/services/blocks/blocks-call.ts @@ -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"]; diff --git a/src/app/shared/nodes/generic-node/generic-node.css b/src/app/shared/nodes/generic-node/generic-node.css index edb03f6..1bd1a61 100644 --- a/src/app/shared/nodes/generic-node/generic-node.css +++ b/src/app/shared/nodes/generic-node/generic-node.css @@ -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; diff --git a/src/app/shared/nodes/generic-node/generic-node.html b/src/app/shared/nodes/generic-node/generic-node.html index dc39dbf..6c8e787 100644 --- a/src/app/shared/nodes/generic-node/generic-node.html +++ b/src/app/shared/nodes/generic-node/generic-node.html @@ -86,7 +86,23 @@ [emit]="emit"> - {{ input.key }} + + {{ inputDisplayLabel(input.key) }} + @if (canTogglePortMultiplicity('input', input.key)) { + + } @else { + {{ portCurrentKindLabel('input', input.key) }} + } + } @@ -96,7 +112,25 @@
{{ outputsTitle() }}
@for (output of outputs; track output.key) {
- {{ output.key }} + + + {{ outputDisplayLabel(output.key) }} + @if (canTogglePortMultiplicity('output', output.key)) { + + } @else { + {{ portCurrentKindLabel('output', output.key) }} + } + +
Edit {{ localEditorLabel }}
+ @if (localEditorBindableAsInput && localEditorPath !== 'name') { +
+ + + @if (localEditorUseInput) { +
+ This field will be provided by node input{{ localEditorBindableInputName ? (' "' + localEditorBindableInputName + '"') : '' }}. +
+ } +
+ }
- @if (localEditorLoading) { + @if (localEditorUseInput && localEditorPath !== 'name') { + + } @else if (localEditorLoading) { } @else if (localEditorType === 'boolean') { + } @else if (localEditorWidget === 'textarea') { + } @else if (localEditorHasRetriever) {