Refine container rendering and assistant runtime config

This commit is contained in:
Lucio Lelii 2026-03-27 17:39:59 +01:00
parent b10e9c59e0
commit d508ccf6d1
7 changed files with 144 additions and 17 deletions

View File

@ -224,13 +224,6 @@ function mapIntent(raw: unknown): AssistantIntent | null {
function resolveAssistantUrl(url: string): string {
if (!url) return url;
if (/^https?:\/\//i.test(url)) return url;
const apiBase = environment.apiUrl;
if (/^https?:\/\//i.test(apiBase)) {
return new URL(url, `${apiBase.replace(/\/+$/, '')}/`).toString();
}
const normalizedBase = apiBase.startsWith('/') ? apiBase : `/${apiBase}`;
const origin = typeof window !== 'undefined' ? window.location.origin : '';
return new URL(url, `${origin}${normalizedBase.replace(/\/+$/, '')}/`).toString();
return `${apiBase}${url.startsWith('/') ? url : `/${url}`}`;
}

View File

@ -153,6 +153,26 @@
text-overflow: ellipsis;
}
.container-node__name-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.container-node__name-edit {
border: 0;
width: 24px;
height: 24px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.16);
color: #f8fafc;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.container-node__delete {
border: 0;
width: 30px;
@ -337,12 +357,18 @@
background: #fff7ed;
color: #92400e;
border-color: #fed7aa;
align-items: center;
justify-content: center;
text-align: center;
}
.container-node__port-label--dependency-output {
background: #eff6ff;
color: #1d4ed8;
border-color: #bfdbfe;
align-items: center;
justify-content: center;
text-align: center;
}
.container-node__port-context {

View File

@ -8,7 +8,14 @@
</div>
<div class="container-node__titles">
<div class="container-node__eyebrow">{{ containerLabel }}</div>
<div class="container-node__name">{{ name }}</div>
<div class="container-node__name-row">
<div class="container-node__name">{{ name }}</div>
@if (!isReadonly) {
<button type="button" class="container-node__name-edit" title="Edit name" (pointerdown)="$event.stopPropagation()" (click)="openNameEditor($event)">
<i class="bi bi-pencil-square"></i>
</button>
}
</div>
</div>
<div class="container-node__header-actions">
@if (missingRequiredParams.length) {
@ -42,6 +49,28 @@
</div>
</div>
@if (nameEditorOpen) {
<div class="llm-modal-backdrop" (pointerdown)="$event.stopPropagation()" (click)="cancelNameEditor($event)">
<div class="llm-modal" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<div class="llm-modal-title">Edit Name</div>
<div class="llm-modal-field">
<label>Name</label>
<input
type="text"
maxlength="20"
[ngModel]="draftName"
(ngModelChange)="onDraftNameChange($event)"
(keydown.enter)="saveNameEditor($event)"
(pointerdown)="$event.stopPropagation()" />
</div>
<div class="llm-modal-actions">
<button type="button" class="llm-btn llm-btn-ghost" (pointerdown)="$event.stopPropagation()" (click)="cancelNameEditor($event)">Cancel</button>
<button type="button" class="llm-btn llm-btn-primary" [disabled]="!draftName.trim()" (pointerdown)="$event.stopPropagation()" (click)="saveNameEditor($event)">Save</button>
</div>
</div>
</div>
}
<div class="container-node__ports">
<div class="container-node__port-column">
<div class="container-node__port-title">Inputs</div>

View File

@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, HostBinding, Input, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatTooltipModule } from '@angular/material/tooltip';
import { currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowContainer, FlowData, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY } from '@models/flow';
import { NodeSettingField, NodeSettingOption, NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog';
@ -62,7 +63,7 @@ type StructuredRetrieverConfig = {
@Component({
selector: 'app-container-node',
imports: [CommonModule, ReteModule, MatTooltipModule],
imports: [CommonModule, FormsModule, ReteModule, MatTooltipModule],
templateUrl: './container-node.html',
styleUrl: './container-node.css',
host: {
@ -86,6 +87,8 @@ export class ContainerNodeComponent {
parameterFields: ContainerFieldView[] = [];
richContentFields: RichContentView[] = [];
schemaReady = false;
nameEditorOpen = false;
draftName = '';
@Input() data!: any;
@Input() emit!: (data: any) => void;
@ -223,17 +226,22 @@ export class ContainerNodeComponent {
return requiredFields
.filter((field, index, fields) => fields.findIndex((candidate) => candidate.path === field.path) === index)
.filter((field) => field.path !== 'name' && field.path !== 'subFlow')
.filter((field) => field.path !== 'name')
.filter((field) => field.path !== 'subFlow')
.filter((field) => !field.path.startsWith('subFlow.'))
.filter((field) => this.isFieldEnabled(field.path))
.filter((field) => this.isMissingValue(getValueByPath(config, field.path)))
.map((field) => field.label)
.concat(
this.schemaRequirements.requiredObjects
.filter((field) => field.path !== 'name' && field.path !== 'subFlow')
.filter((field) => field.path !== 'name')
.filter((field) => field.path !== 'subFlow')
.filter((field) => !field.path.startsWith('subFlow.'))
.filter((field) => this.isFieldEnabled(field.path))
.filter((field) => this.isMissingValue(getValueByPath(config, field.path)))
.map((field) => field.label)
)
.concat(this.subFlow ? [] : ['Subflow'])
.filter((field, index, fields) => fields.indexOf(field) === index);
}
@ -353,6 +361,49 @@ export class ContainerNodeComponent {
}
}
openNameEditor(event?: Event) {
event?.preventDefault();
event?.stopPropagation();
if (this.isReadonly) return;
this.draftName = this.name;
this.nameEditorOpen = true;
}
cancelNameEditor(event?: Event) {
event?.preventDefault();
event?.stopPropagation();
this.nameEditorOpen = false;
this.draftName = this.name;
}
onDraftNameChange(value: string) {
this.draftName = value;
}
saveNameEditor(event?: Event) {
event?.preventDefault();
event?.stopPropagation();
if (this.isReadonly) return;
const nextName = this.draftName.trim().slice(0, 20);
if (!nextName || nextName === this.name) {
this.cancelNameEditor();
return;
}
const nextConfiguration = this.cloneConfiguration();
nextConfiguration['name'] = nextName;
this.data.data = {
...this.data.data,
name: nextName,
specificConfiguration: nextConfiguration
};
this.updateCurrentFlowData(nextConfiguration);
this.refreshParameterFields();
this.refreshView();
this.nameEditorOpen = false;
}
async openParameterEditor(path: string, event?: Event) {
event?.preventDefault();
event?.stopPropagation();
@ -660,6 +711,7 @@ export class ContainerNodeComponent {
.filter((field) => field.parts.length > 0);
this.parameterFields = this.containerFieldDefinitions
.filter((field) => !this.isContainerTypeField(field.path))
.filter((field) => this.isFieldVisible(field.path))
.filter((field) => !richContentPaths.has(field.path))
.map((field) => ({
@ -689,7 +741,7 @@ export class ContainerNodeComponent {
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
if (shouldSkipSchemaField(key, childResolved)) continue;
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
if (path === 'name' || path === 'subFlow') continue;
if (path === 'name' || path === 'subFlow' || this.isContainerTypeField(path)) continue;
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
if (hasChildren) {
@ -720,10 +772,21 @@ export class ContainerNodeComponent {
private richContentPaths(): string[] {
return this.containerFieldDefinitions
.filter((field) => !this.isContainerTypeField(field.path))
.filter((field) => field.widget === 'textarea')
.map((field) => field.path);
}
private isContainerTypeField(path: string): boolean {
return [
'type',
'typeName',
'containerType',
'configurationType',
'configurationClass'
].some((key) => path === key || path.endsWith(`.${key}`));
}
private toRichContentParts(path: string): { text: string; isDynamicInput: boolean }[] {
const content = String(getValueByPath(this.configuration ?? {}, path) ?? '').trim();
if (!content) return [];

View File

@ -890,7 +890,20 @@ export class TaskStepNodeComponent {
}
private shouldHideConfigPath(path: string): boolean {
return this.isContainerNode() && (path === 'subFlow' || path.startsWith('subFlow.'));
const isContainerTypePath = [
'type',
'typeName',
'containerType',
'configurationType',
'configurationClass'
].some((key) => path === key || path.endsWith(`.${key}`));
return this.isContainerNode()
&& (
isContainerTypePath
|| path === 'subFlow'
|| path.startsWith('subFlow.')
);
}
private toArrayFieldItems(definition: ArrayFieldDefinition, value: unknown): ArrayFieldItemView[] {

View File

@ -186,10 +186,13 @@
}
.execution-log-icon {
width: 18px;
height: 18px;
flex: 0 0 auto;
width: 20px;
height: 20px;
font-size: 18px;
color: #475569;
line-height: 20px;
overflow: visible;
margin-top: 1px;
}

View File

@ -9,7 +9,7 @@ import { TaskExecutionsCallService } from "@services/task-executions/task-execut
export const environment = {
apiUrl: '/api',
production: true,
assistantEnabled: false,
assistantEnabled: true,
tourModeAlwaysOn: false,
turnstileEnabled: true,
authorizationCallService: AuthorizationCallService,