From 3bf669180d5a0112efd41269821bfbee2b0667d1 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Tue, 24 Mar 2026 14:05:21 +0100 Subject: [PATCH] look and feel in the longtext parmeter changed --- src/app/services/blocks/blocks.ts | 4 +++ src/app/services/containers/containers.ts | 4 +++ .../nodes/container-node/container-node.css | 11 ++++++ .../nodes/container-node/container-node.html | 4 +-- .../nodes/container-node/container-node.ts | 25 ++++++++++---- .../nodes/generic-node/generic-node.css | 19 +++++++++++ .../nodes/generic-node/generic-node.html | 6 ++-- .../shared/nodes/generic-node/generic-node.ts | 26 ++++++++++---- .../nodes/task-step-node/task-step-node.ts | 13 ++++--- src/app/shared/rete-editor/rete-editor.ts | 34 ++++++++++++++++++- .../task-execution-viewer.css | 16 +++------ .../task-execution-viewer.html | 5 +-- .../task-execution-viewer.ts | 15 +++++++- 13 files changed, 143 insertions(+), 39 deletions(-) diff --git a/src/app/services/blocks/blocks.ts b/src/app/services/blocks/blocks.ts index 1a82608..7ca57ae 100644 --- a/src/app/services/blocks/blocks.ts +++ b/src/app/services/blocks/blocks.ts @@ -64,6 +64,10 @@ export class BlocksService { return blockTypes.find((blockType) => blockType.type === typeName); } + peekBlockType(typeName: BlockTypeName) { + return this._blockTypes().find((blockType) => blockType.type === typeName) ?? null; + } + createEmptyBlock(blockType: BlockTypeName, context?: BlockDraftContext) { const flowId = typeof context?.flowId === 'string' && context.flowId.trim().length > 0 ? context.flowId.trim() : ''; const cacheKey = `${String(blockType)}::${flowId}`; diff --git a/src/app/services/containers/containers.ts b/src/app/services/containers/containers.ts index 60f18c9..7a384b2 100644 --- a/src/app/services/containers/containers.ts +++ b/src/app/services/containers/containers.ts @@ -64,6 +64,10 @@ export class ContainersService { return containerTypes.find((containerType) => containerType.type === typeName); } + peekContainerType(typeName: BlockTypeName) { + return this._containerTypes().find((containerType) => containerType.type === typeName) ?? null; + } + createEmptyContainer(containerType: BlockTypeName) { const cacheKey = String(containerType); const cached = this.emptyContainerCache.get(cacheKey); diff --git a/src/app/shared/nodes/container-node/container-node.css b/src/app/shared/nodes/container-node/container-node.css index 0427e18..dfe9a1b 100644 --- a/src/app/shared/nodes/container-node/container-node.css +++ b/src/app/shared/nodes/container-node/container-node.css @@ -423,6 +423,16 @@ white-space: nowrap; } +.container-node__param-value--clamped { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre-wrap; + word-break: break-word; +} + .container-node__param-edit { border: 0; background: transparent; @@ -445,6 +455,7 @@ pointer-events: none; } + .container-node__dropzone { margin: 0 14px 14px; min-height: 128px; diff --git a/src/app/shared/nodes/container-node/container-node.html b/src/app/shared/nodes/container-node/container-node.html index 0e59983..7aec37f 100644 --- a/src/app/shared/nodes/container-node/container-node.html +++ b/src/app/shared/nodes/container-node/container-node.html @@ -140,7 +140,7 @@ } - {{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }} + {{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }} } @for (contentField of richContentFields; track contentField.path) { @@ -148,7 +148,7 @@
{{ contentField.label }}
-
+
@for (part of contentField.parts; track $index) { @if (part.isDynamicInput) { {{ formatDynamicInputToken(part.text) }} diff --git a/src/app/shared/nodes/container-node/container-node.ts b/src/app/shared/nodes/container-node/container-node.ts index a616901..5eb30b9 100644 --- a/src/app/shared/nodes/container-node/container-node.ts +++ b/src/app/shared/nodes/container-node/container-node.ts @@ -37,6 +37,7 @@ type ContainerFieldView = { label: string; value: string; wide: boolean; + expandable: boolean; enabled: boolean; type: ContainerFieldType; booleanValue: boolean; @@ -45,6 +46,8 @@ type ContainerFieldView = { type RichContentView = { path: string; label: string; + rawValue: string; + expandable: boolean; parts: { text: string; isDynamicInput: boolean }[]; }; @@ -569,7 +572,7 @@ export class ContainerNodeComponent { private async loadSchemaContext() { try { - const containerType = await this.containersService.getContainerType(this.typeName); + const containerType = this.containersService.peekContainerType(this.typeName) ?? await this.containersService.getContainerType(this.typeName); this.containerSchema = (containerType?.schema ?? null) as Record | null; this.schemaRequirements = extractSchemaRequirements(this.containerSchema); this.containerFieldDefinitions = this.buildContainerFieldDefinitions(this.containerSchema); @@ -602,11 +605,16 @@ export class ContainerNodeComponent { const richContentPaths = new Set(this.richContentPaths()); this.richContentFields = this.richContentPaths() .filter((path) => this.isFieldVisible(path)) - .map((path) => ({ - path, - label: this.containerFieldDefinitions.find((field) => field.path === path)?.label ?? pathToLabel(path), - parts: this.toRichContentParts(path) - })) + .map((path) => { + const rawValue = String(getValueByPath(config, path) ?? ''); + return { + path, + label: this.containerFieldDefinitions.find((field) => field.path === path)?.label ?? pathToLabel(path), + rawValue, + expandable: this.isLongTextValue(rawValue), + parts: this.toRichContentParts(path) + }; + }) .filter((field) => field.parts.length > 0); this.parameterFields = this.containerFieldDefinitions @@ -617,6 +625,7 @@ export class ContainerNodeComponent { label: field.label, value: valueToDisplayString(getValueByPath(config, field.path)), wide: field.widget === 'textarea' || field.label.length >= 18, + expandable: this.isLongTextValue(valueToDisplayString(getValueByPath(config, field.path))), enabled: this.isFieldEnabled(field.path), type: field.type, booleanValue: getValueByPath(config, field.path) === true @@ -775,6 +784,10 @@ export class ContainerNodeComponent { .filter((option: NodeSettingOption | null): option is NodeSettingOption => option != null); } + private isLongTextValue(value: string): boolean { + return String(value ?? '').trim().length > 80; + } + private getEditorInitialValue(definition: ContainerFieldDefinition): string | boolean { const raw = getValueByPath(this.configuration ?? {}, definition.path); if (definition.type === 'boolean') return raw === true; diff --git a/src/app/shared/nodes/generic-node/generic-node.css b/src/app/shared/nodes/generic-node/generic-node.css index d07854d..c26d5e4 100644 --- a/src/app/shared/nodes/generic-node/generic-node.css +++ b/src/app/shared/nodes/generic-node/generic-node.css @@ -897,6 +897,14 @@ line-height: 1.45; } +.llm-param-text-clamped { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; +} + .llm-inline-token { display: inline-flex; align-items: center; @@ -1004,6 +1012,17 @@ white-space: nowrap; } +.llm-param-value-clamped { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + white-space: pre-wrap; + word-break: break-word; +} + + .llm-modal-backdrop { position: fixed; inset: 0; diff --git a/src/app/shared/nodes/generic-node/generic-node.html b/src/app/shared/nodes/generic-node/generic-node.html index 0481618..0a62268 100644 --- a/src/app/shared/nodes/generic-node/generic-node.html +++ b/src/app/shared/nodes/generic-node/generic-node.html @@ -203,7 +203,7 @@ }
- {{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }} + {{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}
} @@ -242,7 +242,7 @@ } - {{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }} + {{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }} } @@ -259,7 +259,7 @@ } -
+
@if (!contentField.parts.length) { - } @else { diff --git a/src/app/shared/nodes/generic-node/generic-node.ts b/src/app/shared/nodes/generic-node/generic-node.ts index cb34b94..db7be6e 100644 --- a/src/app/shared/nodes/generic-node/generic-node.ts +++ b/src/app/shared/nodes/generic-node/generic-node.ts @@ -86,6 +86,7 @@ type EditableFieldView = { label: string; value: string; wide: boolean; + expandable: boolean; enabled: boolean; type: FieldType; booleanValue: boolean; @@ -124,6 +125,8 @@ type EditableFieldGroupView = { type RichContentView = { path: string; label: string; + rawValue: string; + expandable: boolean; parts: { text: string; isDynamicInput: boolean }[]; }; @@ -545,7 +548,7 @@ export class GenericNodeComponent { } try { - const blockType = await this.blocksService.getBlockType(type); + const blockType = this.blocksService.peekBlockType(type) ?? await this.blocksService.getBlockType(type); this.blockDescriptor = blockType ?? null; this.blockSchema = (blockType?.schema ?? null) as Record | null; this.schemaRequirements = extractSchemaRequirements(this.blockSchema); @@ -1078,6 +1081,10 @@ export class GenericNodeComponent { .filter((option): option is NodeSettingOption => option != null); } + private isLongTextValue(value: string): boolean { + return String(value ?? '').trim().length > 80; + } + private refreshParameterFields() { const config = this.blockConfiguration ?? {}; const richContentPaths = new Set(this.richContentPaths()); @@ -1086,11 +1093,16 @@ export class GenericNodeComponent { this.richContentFields = this.richContentPaths() .filter((path) => this.isPathVisible(path)) - .map((path) => ({ - path, - label: this.fieldDisplayLabel(path), - parts: this.toRichContentParts(path) - })); + .map((path) => { + const rawValue = String(this.getByPath(config, path) ?? ''); + return { + path, + label: this.fieldDisplayLabel(path), + rawValue, + expandable: this.isLongTextValue(rawValue), + parts: this.toRichContentParts(path) + }; + }); this.arrayFields = this.arrayFieldDefinitions .filter((definition) => this.isPathVisible(definition.path)) @@ -1108,6 +1120,7 @@ export class GenericNodeComponent { label: definition.label, value: this.fieldDisplayValue(definition, value), wide: this.shouldRenderWideField(definition.label, definition.ui.widget === 'textarea'), + expandable: this.isLongTextValue(this.fieldDisplayValue(definition, value)), enabled: this.isPathEnabled(definition.path), type: definition.type, booleanValue: value === true @@ -1145,6 +1158,7 @@ export class GenericNodeComponent { label: this.fieldDisplayLabel(entry.path), value: valueToDisplayString(entry.value), wide: this.shouldRenderWideField(this.fieldDisplayLabel(entry.path), false), + expandable: this.isLongTextValue(valueToDisplayString(entry.value)), enabled: this.isPathEnabled(entry.path), type: (typeof entry.value === 'boolean' ? 'boolean' : 'unknown') as FieldType, booleanValue: entry.value === true diff --git a/src/app/shared/nodes/task-step-node/task-step-node.ts b/src/app/shared/nodes/task-step-node/task-step-node.ts index 117c82f..bd71faf 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.ts +++ b/src/app/shared/nodes/task-step-node/task-step-node.ts @@ -543,9 +543,14 @@ export class TaskStepNodeComponent { const nodeFamily = this.data?.data?.nodeFamily === 'container' ? 'container' : 'block'; - const typeDescriptor = nodeFamily === 'container' - ? await this.containersService.getContainerType(type) - : await this.blocksService.getBlockType(type); + const cachedDescriptor = nodeFamily === 'container' + ? this.containersService.peekContainerType(type) + : this.blocksService.peekBlockType(type); + const typeDescriptor = cachedDescriptor ?? ( + 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 | null; const typeKey = this.nodeTypeCacheKey(); @@ -1076,7 +1081,7 @@ export class TaskStepNodeComponent { const normalized = String(value ?? ''); if (!normalized.trim()) return false; const lineCount = normalized.split(/\r?\n/).length; - return lineCount > 2 || normalized.length > 160; + return lineCount > 2 || normalized.length > 80; } private async openReadonlyTextDialog(label: string, value: string) { diff --git a/src/app/shared/rete-editor/rete-editor.ts b/src/app/shared/rete-editor/rete-editor.ts index 353a9b7..9a55c04 100644 --- a/src/app/shared/rete-editor/rete-editor.ts +++ b/src/app/shared/rete-editor/rete-editor.ts @@ -351,17 +351,49 @@ export class ReteEditor implements OnChanges, OnDestroy { const currentNode = currentNodes.get(String(nextNode.id)); if (!currentNode?.data) continue; - currentNode.data = { + const nextReadonlyNode = { ...currentNode.data, ...nextNode, position: nextNode.position ?? currentNode.data.position, __readonly: true }; + if (this.hasSameReadonlyNodePayload(currentNode.data as FlowNode & Record, nextReadonlyNode as FlowNode & Record)) { + continue; + } + + currentNode.data = nextReadonlyNode; + await rete.area.update('node', currentNode.id); } } + private hasSameReadonlyNodePayload(currentNode: FlowNode & Record, nextNode: FlowNode & Record) { + return JSON.stringify({ + id: currentNode.id, + name: currentNode.name, + position: currentNode.position, + inputs: currentNode.inputs, + outputs: currentNode.outputs, + specificConfiguration: currentNode.specificConfiguration, + typeName: currentNode.typeName, + userInteractive: currentNode['userInteractive'], + nodeFamily: currentNode.nodeFamily, + __readonly: currentNode['__readonly'] + }) === JSON.stringify({ + id: nextNode.id, + name: nextNode.name, + position: nextNode.position, + inputs: nextNode.inputs, + outputs: nextNode.outputs, + specificConfiguration: nextNode.specificConfiguration, + typeName: nextNode.typeName, + userInteractive: nextNode['userInteractive'], + nodeFamily: nextNode.nodeFamily, + __readonly: nextNode['__readonly'] + }); + } + private getDropPosition(event: DragEvent) { const host = this.container.nativeElement as HTMLElement; const rect = host.getBoundingClientRect(); diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.css b/src/app/shared/task-execution-viewer/task-execution-viewer.css index da1a0b3..2e27083 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.css +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.css @@ -202,16 +202,8 @@ color: #991b1b; } -.execution-log-details { - margin: 10px 0 0; - border: 1px solid #dbe4ee; - border-radius: 10px; - background: #ffffff; - color: #0f172a; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - font-size: 11px; - line-height: 1.45; - padding: 10px; - white-space: pre-wrap; - word-break: break-word; +.execution-logs-scroll { + height: 100%; + overflow-y: auto; + overscroll-behavior: contain; } diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.html b/src/app/shared/task-execution-viewer/task-execution-viewer.html index ab2096b..ec7ff37 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.html +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.html @@ -154,7 +154,7 @@ (fileInputChange)="onFileInputChange($event.input, $event.files)"> } @else if (activeAsideTab() === 'logs') { -
+
@if (logsLoading()) {
Loading execution log...
} @else if (logsError()) { @@ -184,9 +184,6 @@ {{ event.levelText }}
- @if (event.details) { -
{{ event.details | json }}
- }
} } diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.ts b/src/app/shared/task-execution-viewer/task-execution-viewer.ts index d41af38..a0bc087 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.ts +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, effect, inject, input, OnDestroy, signal } from '@angular/core'; +import { Component, computed, effect, ElementRef, inject, input, OnDestroy, signal, ViewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -84,6 +84,7 @@ export class TaskExecutionViewerComponent implements OnDestroy { readonly executionLogs = signal([]); readonly logsLoading = signal(false); readonly logsError = signal(null); + @ViewChild('logsScrollViewport') private logsScrollViewport?: ElementRef; constructor() { effect(() => { @@ -141,6 +142,12 @@ export class TaskExecutionViewerComponent implements OnDestroy { onCleanup(() => clearInterval(timer)); }); + effect(() => { + this.executionLogs(); + this.logsLoading(); + queueMicrotask(() => this.scrollLogsToBottom()); + }); + effect(() => { const dialogState = this.humanInteractionDialog.state(); const execution = this.execution(); @@ -732,6 +739,12 @@ export class TaskExecutionViewerComponent implements OnDestroy { }); } + private scrollLogsToBottom() { + const element = this.logsScrollViewport?.nativeElement; + if (!element || this.activeAsideTab() !== 'logs') return; + element.scrollTop = element.scrollHeight; + } + private async loadSimulationOptions( key: string, context: Record,