look and feel in the longtext parmeter changed

This commit is contained in:
Lucio Lelii 2026-03-24 14:05:21 +01:00
parent 55418d2226
commit 3bf669180d
13 changed files with 143 additions and 39 deletions

View File

@ -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}`;

View File

@ -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);

View File

@ -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;

View File

@ -140,7 +140,7 @@
</button>
}
</div>
<span class="container-node__param-value">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
<span class="container-node__param-value" [class.container-node__param-value--clamped]="field.expandable">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
</div>
}
@for (contentField of richContentFields; track contentField.path) {
@ -148,7 +148,7 @@
<div class="container-node__param-head">
<span class="container-node__param-key">{{ contentField.label }}</span>
</div>
<div class="container-node__param-value">
<div class="container-node__param-value" [class.container-node__param-value--clamped]="contentField.expandable">
@for (part of contentField.parts; track $index) {
@if (part.isDynamicInput) {
<span class="llm-inline-token">{{ formatDynamicInputToken(part.text) }}</span>

View File

@ -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<string, any> | 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;

View File

@ -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;

View File

@ -203,7 +203,7 @@
</button>
}
</div>
<span class="llm-param-value">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
<span class="llm-param-value" [class.llm-param-value-clamped]="field.expandable">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
</div>
}
</div>
@ -242,7 +242,7 @@
</button>
}
</div>
<span class="llm-param-value">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
<span class="llm-param-value" [class.llm-param-value-clamped]="field.expandable">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
</div>
}
</div>
@ -259,7 +259,7 @@
</button>
}
</div>
<div class="llm-param-text">
<div class="llm-param-text" [class.llm-param-text-clamped]="contentField.expandable">
@if (!contentField.parts.length) {
-
} @else {

View File

@ -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<string, any> | 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

View File

@ -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<string, any> | 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) {

View File

@ -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<string, unknown>, nextReadonlyNode as FlowNode & Record<string, unknown>)) {
continue;
}
currentNode.data = nextReadonlyNode;
await rete.area.update('node', currentNode.id);
}
}
private hasSameReadonlyNodePayload(currentNode: FlowNode & Record<string, unknown>, nextNode: FlowNode & Record<string, unknown>) {
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();

View File

@ -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;
}

View File

@ -154,7 +154,7 @@
(fileInputChange)="onFileInputChange($event.input, $event.files)">
</app-task-execution-inputs-panel>
} @else if (activeAsideTab() === 'logs') {
<div class="p-3 space-y-3">
<div #logsScrollViewport class="execution-logs-scroll p-3 space-y-3">
@if (logsLoading()) {
<div class="text-xs text-slate-500">Loading execution log...</div>
} @else if (logsError()) {
@ -184,9 +184,6 @@
{{ event.levelText }}
</span>
</div>
@if (event.details) {
<pre class="execution-log-details">{{ event.details | json }}</pre>
}
</div>
}
}

View File

@ -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<ExecutionEventLogEntry[]>([]);
readonly logsLoading = signal(false);
readonly logsError = signal<string | null>(null);
@ViewChild('logsScrollViewport') private logsScrollViewport?: ElementRef<HTMLDivElement>;
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<string, string>,