@if (!contentField.parts.length) {
@@ -230,6 +243,7 @@
{{ arrayField.label }}
+ @if (!isReadonly) {
+ }
@if (!arrayField.items.length) {
No items
@@ -246,6 +261,7 @@
{{ item.summary }}
+ @if (!isReadonly) {
+ }
+ @if (!isReadonly) {
+ }
}
diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts
index 5087479..7504aaf 100644
--- a/src/app/shared/nodes/generic-node/generic-node.ts
+++ b/src/app/shared/nodes/generic-node/generic-node.ts
@@ -132,6 +132,10 @@ export class GenericNodeComponent {
return this.blockId;
}
+ @HostBinding('class.llm-node-readonly') get readonlyClass() {
+ return this.isReadonly;
+ }
+
outputs: { key: string; socket: ClassicPreset.Socket }[] = [];
inputs: { key: string; socket: ClassicPreset.Socket }[] = [];
parameterFields: EditableFieldView[] = [];
@@ -192,9 +196,18 @@ export class GenericNodeComponent {
this.rendered();
}
+ get isReadonly() {
+ return this.data?.data?.['__readonly'] === true;
+ }
+
+ get nodeIdLabel() {
+ return this.blockId ?? 'unknown-id';
+ }
+
async openNameEditor(event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
this.localEditorPath = 'name';
this.localEditorLabel = 'Name';
@@ -214,6 +227,7 @@ export class GenericNodeComponent {
async openParameterEditor(path: string, event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
const definition = this.editableFieldDefinitions.find((field) => field.path === path);
if (!definition) return;
@@ -271,6 +285,7 @@ export class GenericNodeComponent {
saveSimpleParamEditor(event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
if (!this.localEditorPath) return;
if (!this.canSaveLocalEditor()) return;
@@ -304,6 +319,7 @@ export class GenericNodeComponent {
async openMainContentEditor(path: string, event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
if (!this.isPathVisible(path)) return;
@@ -316,6 +332,7 @@ export class GenericNodeComponent {
async confirmDelete(event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
if (!this.deleteConfirmOpen) {
this.deleteConfirmOpen = true;
return;
@@ -406,6 +423,7 @@ export class GenericNodeComponent {
onPortKindChange(kind: 'input' | 'output', key: string, nextValue: string, event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
const ports = this.resolvePorts(kind);
const index = ports.findIndex((candidate) => candidate.name === key);
@@ -1029,18 +1047,21 @@ export class GenericNodeComponent {
async addArrayItem(path: string, event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
await this.openArrayItemEditor(path, null);
}
async editArrayItem(path: string, index: number, event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
await this.openArrayItemEditor(path, index);
}
removeArrayItem(path: string, index: number, event?: Event) {
event?.preventDefault();
event?.stopPropagation();
+ if (this.isReadonly) return;
const config = this.ensureBlockConfiguration();
const current = this.getByPath(config, path);
@@ -1650,6 +1671,12 @@ export class GenericNodeComponent {
}
private async fetchConditionalRequirement(blockType: string, field: ConditionalRequiredField) {
+ if (field.requiredWhen) {
+ return evaluateUiConditionRule(field.requiredWhen, this.blockConfiguration ?? {}, (path) => this.resolveFieldSchema(path));
+ }
+
+ if (!field.retrieverKey) return false;
+
const retrieverBlockType = field.retrieverBlockType ?? blockType;
const context: Record
= {};
for (const dep of field.dependsOn) {
@@ -1690,7 +1717,11 @@ export class GenericNodeComponent {
private cloneFlowData(value: T): T {
if (typeof globalThis.structuredClone === 'function') {
- return globalThis.structuredClone(value);
+ try {
+ return globalThis.structuredClone(value);
+ } catch {
+ // Node runtime objects may include non-cloneable values.
+ }
}
return JSON.parse(JSON.stringify(value)) as T;
}
@@ -1732,7 +1763,7 @@ export class GenericNodeComponent {
).subscribe({
next: (createdBlock) => {
const current = (this.data?.data ?? {}) as Record;
- const replaceNode = current['replaceWithCreatedBlock'];
+ const replaceNode = current['replaceWithCreatedNode'];
if (typeof replaceNode === 'function') {
void replaceNode({
...createdBlock,
diff --git a/src/app/shared/nodes/schema-requirements.ts b/src/app/shared/nodes/schema-requirements.ts
index efe1601..f35e6ad 100644
--- a/src/app/shared/nodes/schema-requirements.ts
+++ b/src/app/shared/nodes/schema-requirements.ts
@@ -1,4 +1,4 @@
-import { resolveSchemaRef } from './node-utility';
+import { readUiConditionRule, resolveSchemaRef, type UiConditionRule } from './node-utility';
export type RequiredField = {
path: string;
@@ -9,9 +9,10 @@ export type ConditionalRequiredField = {
path: string;
label: string;
retrieverBlockType: string | null;
- retrieverKey: string;
+ retrieverKey: string | null;
retrieverUrl: string | null;
dependsOn: Array<{ key: string; path: string }>;
+ requiredWhen: UiConditionRule | null;
};
export type SchemaRequirements = {
@@ -40,7 +41,8 @@ function walkSchema(
conditional: ConditionalRequiredField[],
seenRequired: Set,
seenConditional: Set,
- requireAllDescendants = false
+ requireAllDescendants = false,
+ inheritedRequiredWhen: UiConditionRule | null = null
) {
const resolved = resolveSchemaRef(node, root);
if (!resolved || typeof resolved !== 'object') return;
@@ -58,6 +60,7 @@ function walkSchema(
const isRequiredBySchema = requiredSet.has(key);
const isRequiredByAncestor = requireAllDescendants && key !== 'type';
const isRequired = isRequiredBySchema || isRequiredByAncestor;
+ const requiredWhen = readUiConditionRule(propertyResolved?.['x-ui-required-when']) ?? inheritedRequiredWhen;
if (isRequired && !hasChildren && key !== 'type' && !seenRequired.has(propertyPath)) {
seenRequired.add(propertyPath);
@@ -65,9 +68,10 @@ function walkSchema(
}
const retrieverRequiredUrl = propertyResolved?.['x-retriever-required-url'];
- if (typeof retrieverRequiredUrl === 'string') {
+ if ((requiredWhen || typeof retrieverRequiredUrl === 'string') && key !== 'type' && !hasChildren) {
const parsedRetriever = parseRetrieverUrl(retrieverRequiredUrl);
- const retrieverKey = parsedRetriever?.key ?? String(propertyResolved?.['x-retriever-name'] ?? key);
+ const retrieverKey = parsedRetriever?.key
+ ?? (typeof propertyResolved?.['x-retriever-name'] === 'string' ? String(propertyResolved['x-retriever-name']) : null);
const retrieverBlockType = parsedRetriever?.blockType ?? null;
const rawDepends = Array.isArray(propertyResolved?.['x-retriever-required-depends-on'])
? (propertyResolved['x-retriever-required-depends-on'] as unknown[])
@@ -80,7 +84,7 @@ function walkSchema(
path: pathPrefix ? `${pathPrefix}.${dep}` : dep
}));
- const signature = `${propertyPath}|${retrieverKey}|${dependsOn.map((d) => d.path).join(',')}`;
+ const signature = `${propertyPath}|${retrieverKey ?? 'local'}|${dependsOn.map((d) => d.path).join(',')}|${JSON.stringify(requiredWhen ?? null)}`;
if (!seenConditional.has(signature)) {
seenConditional.add(signature);
conditional.push({
@@ -88,8 +92,9 @@ function walkSchema(
label,
retrieverBlockType,
retrieverKey,
- retrieverUrl: retrieverRequiredUrl,
- dependsOn
+ retrieverUrl: typeof retrieverRequiredUrl === 'string' ? retrieverRequiredUrl : null,
+ dependsOn,
+ requiredWhen
});
}
}
@@ -104,7 +109,8 @@ function walkSchema(
conditional,
seenRequired,
seenConditional,
- isRequired && !childHasOwnRequired
+ isRequired && !childHasOwnRequired,
+ requiredWhen
);
}
}
diff --git a/src/app/shared/nodes/task-step-node/task-step-node.css b/src/app/shared/nodes/task-step-node/task-step-node.css
index 58c7d82..8d23617 100644
--- a/src/app/shared/nodes/task-step-node/task-step-node.css
+++ b/src/app/shared/nodes/task-step-node/task-step-node.css
@@ -11,6 +11,11 @@
color: #0f172a;
}
+:host.llm-node-readonly,
+:host.llm-node-readonly * {
+ cursor: default !important;
+}
+
.llm-node--human {
border-color: #f7d7a7;
background: linear-gradient(180deg, #fffdf9 0%, #fff7ed 100%);
diff --git a/src/app/shared/nodes/task-step-node/task-step-node.html b/src/app/shared/nodes/task-step-node/task-step-node.html
index aa1ca27..07f18f9 100644
--- a/src/app/shared/nodes/task-step-node/task-step-node.html
+++ b/src/app/shared/nodes/task-step-node/task-step-node.html
@@ -45,7 +45,7 @@
}
-