humainflow-web/src/app/shared/nodes/node-parameters/node-parameters.html

388 lines
17 KiB
HTML

<!--
SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
SPDX-License-Identifier: AGPL-3.0-or-later
Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
-->
<div class="llm-params llm-params-shell">
@if (!host.schemaReady) {
<div class="llm-skeleton-overlay" aria-hidden="true">
<div class="llm-skeleton-stack">
<div class="llm-skeleton-line llm-skeleton-line-title"></div>
<div class="llm-skeleton-line llm-skeleton-line-short"></div>
<div class="llm-skeleton-grid">
<div class="llm-skeleton-pill"></div>
<div class="llm-skeleton-pill"></div>
<div class="llm-skeleton-pill llm-skeleton-pill-wide"></div>
</div>
</div>
</div>
}
@for (section of host.parameterDisplaySections; track section.key) {
@if (section.group; as group) {
<fieldset class="llm-param-fieldset">
<legend class="llm-param-legend">{{ group.legend }}</legend>
<div class="llm-param-grid">
@for (item of group.items; track item.path) {
@if (item.field; as field) {
<div class="llm-param-chip" [class.llm-param-chip-wide]="field.wide" [class.llm-param-chip-boolean]="field.type === 'boolean'" [class.llm-param-chip-disabled]="!field.enabled">
<div class="llm-param-row-head">
<span class="llm-param-key">{{ field.label }}</span>
@if (field.type === 'boolean') {
<button
type="button"
class="llm-bool-toggle"
[class.llm-bool-toggle-on]="field.booleanValue"
[disabled]="host.isReadonly || !field.enabled"
[attr.aria-pressed]="field.booleanValue"
[attr.title]="'Toggle ' + field.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="host.toggleBooleanParameter(field.path, $event)">
<span class="llm-bool-toggle-thumb"></span>
</button>
} @else if (!host.isReadonly) {
<button
type="button"
class="llm-edit-btn"
[disabled]="!field.enabled"
[attr.title]="'Edit ' + field.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="host.openParameterEditor(field.path, $event)">
<i class="bi bi-pen"></i>
</button>
} @else if (field.expandable) {
<button
type="button"
class="llm-param-view-btn"
aria-label="View full value"
(pointerdown)="$event.stopPropagation()"
(click)="host.openFieldPreview(field, $event)">
<i class="bi bi-eye"></i>
</button>
}
</div>
@if (field.type !== 'boolean') {
<span class="llm-param-value" [class.llm-param-value-choice]="field.choice"
[class.llm-param-value-empty]="!field.value || field.value === '-'"
[class.llm-param-value-clamped]="field.expandable">{{ shownValue(field) }}</span>
}
</div>
} @else {
@if (item.richContentField; as contentField) {
<div class="llm-param-block llm-param-chip llm-param-chip-wide">
<div class="llm-param-row-head">
<div class="llm-param-key">{{ contentField.label }}</div>
@if (!host.isReadonly) {
<button type="button" class="llm-edit-btn" [attr.title]="'Edit ' + contentField.label.toLowerCase()" (pointerdown)="$event.stopPropagation()" (click)="host.openMainContentEditor(contentField.path, $event)">
<i class="bi bi-pen"></i>
</button>
} @else if (contentField.expandable) {
<button
type="button"
class="llm-param-view-btn"
aria-label="View full value"
(pointerdown)="$event.stopPropagation()"
(click)="host.openMainContentPreview(contentField, $event)">
<i class="bi bi-eye"></i>
</button>
}
</div>
<div class="llm-param-text" [class.llm-param-text-clamped]="contentField.expandable">
@if (!contentField.parts.length) {
-
} @else {
@for (part of contentField.parts; track $index) {
@if (part.isDynamicInput) {
<span class="llm-inline-token">{{ host.formatDynamicInputToken(part.text) }}</span>
} @else {
<span>{{ part.text }}</span>
}
}
}
</div>
</div>
}
@if (item.optionalGroupField; as optionalGroup) {
<button
type="button"
class="llm-param-chip llm-param-chip-wide llm-optional-group"
[class.llm-param-chip-disabled]="!optionalGroup.enabled"
[disabled]="host.isReadonly || !optionalGroup.enabled"
[attr.title]="'Edit ' + optionalGroup.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="host.openOptionalGroupEditor(optionalGroup.path, $event)">
<span class="llm-param-key">{{ optionalGroup.label }}</span>
@if (optionalGroup.setCount > 0) {
<span class="llm-optional-group-count">{{ optionalGroup.setCount }} set</span>
}
<i class="bi bi-sliders llm-optional-group-icon"></i>
</button>
}
@if (item.arrayField; as arrayField) {
<app-node-list-field
class="llm-array-block llm-param-chip llm-param-chip-wide"
[label]="arrayField.label"
[items]="arrayField.items"
[readonly]="host.isReadonly"
(add)="host.addArrayItem(arrayField.path)"
(edit)="host.editArrayItem(arrayField.path, $event)"
(remove)="host.removeArrayItem(arrayField.path, $event)"
(view)="host.viewItemDefinition($any($event))" />
}
}
}
</div>
</fieldset>
} @else if (section.item; as item) {
<div class="llm-param-grid">
@if (item.field; as field) {
<div class="llm-param-chip llm-param-chip-root" [class.llm-param-chip-wide]="field.wide" [class.llm-param-chip-boolean]="field.type === 'boolean'" [class.llm-param-chip-disabled]="!field.enabled">
<div class="llm-param-row-head">
<span class="llm-param-key">{{ field.label }}</span>
@if (field.type === 'boolean') {
<button
type="button"
class="llm-bool-toggle"
[class.llm-bool-toggle-on]="field.booleanValue"
[disabled]="host.isReadonly || !field.enabled"
[attr.aria-pressed]="field.booleanValue"
[attr.title]="'Toggle ' + field.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="host.toggleBooleanParameter(field.path, $event)">
<span class="llm-bool-toggle-thumb"></span>
</button>
} @else if (!host.isReadonly) {
<button
type="button"
class="llm-edit-btn"
[disabled]="!field.enabled"
[attr.title]="'Edit ' + field.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="host.openParameterEditor(field.path, $event)">
<i class="bi bi-pen"></i>
</button>
} @else if (field.expandable) {
<button
type="button"
class="llm-param-view-btn"
aria-label="View full value"
(pointerdown)="$event.stopPropagation()"
(click)="host.openFieldPreview(field, $event)">
<i class="bi bi-eye"></i>
</button>
}
</div>
@if (field.type !== 'boolean') {
<span class="llm-param-value" [class.llm-param-value-choice]="field.choice"
[class.llm-param-value-empty]="!field.value || field.value === '-'"
[class.llm-param-value-clamped]="field.expandable">{{ shownValue(field) }}</span>
}
</div>
} @else {
@if (item.richContentField; as contentField) {
<div class="llm-param-block llm-param-chip llm-param-chip-wide">
<div class="llm-param-row-head">
<div class="llm-param-key">{{ contentField.label }}</div>
@if (!host.isReadonly) {
<button type="button" class="llm-edit-btn" [attr.title]="'Edit ' + contentField.label.toLowerCase()" (pointerdown)="$event.stopPropagation()" (click)="host.openMainContentEditor(contentField.path, $event)">
<i class="bi bi-pen"></i>
</button>
} @else if (contentField.expandable) {
<button
type="button"
class="llm-param-view-btn"
aria-label="View full value"
(pointerdown)="$event.stopPropagation()"
(click)="host.openMainContentPreview(contentField, $event)">
<i class="bi bi-eye"></i>
</button>
}
</div>
<div class="llm-param-text" [class.llm-param-text-clamped]="contentField.expandable">
@if (!contentField.parts.length) {
-
} @else {
@for (part of contentField.parts; track $index) {
@if (part.isDynamicInput) {
<span class="llm-inline-token">{{ host.formatDynamicInputToken(part.text) }}</span>
} @else {
<span>{{ part.text }}</span>
}
}
}
</div>
</div>
}
@if (item.optionalGroupField; as optionalGroup) {
<button
type="button"
class="llm-param-chip llm-param-chip-wide llm-optional-group"
[class.llm-param-chip-disabled]="!optionalGroup.enabled"
[disabled]="host.isReadonly || !optionalGroup.enabled"
[attr.title]="'Edit ' + optionalGroup.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="host.openOptionalGroupEditor(optionalGroup.path, $event)">
<span class="llm-param-key">{{ optionalGroup.label }}</span>
@if (optionalGroup.setCount > 0) {
<span class="llm-optional-group-count">{{ optionalGroup.setCount }} set</span>
}
<i class="bi bi-sliders llm-optional-group-icon"></i>
</button>
}
@if (item.arrayField; as arrayField) {
<app-node-list-field
class="llm-array-block llm-param-chip llm-param-chip-wide"
[label]="arrayField.label"
[items]="arrayField.items"
[readonly]="host.isReadonly"
(add)="host.addArrayItem(arrayField.path)"
(edit)="host.editArrayItem(arrayField.path, $event)"
(remove)="host.removeArrayItem(arrayField.path, $event)"
(view)="host.viewItemDefinition($any($event))" />
}
}
</div>
}
}
</div>
@if (host.localEditorOpen) {
<dialog #simpleEditorDialog class="llm-modal-backdrop" (pointerdown)="$event.stopPropagation()" (click)="onSimpleEditorDialogClick($event)" (cancel)="host.closeSimpleParamEditor($event)">
<div class="llm-modal" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<div class="llm-modal-title">Edit {{ host.localEditorLabel }}</div>
@if (host.localEditorBindableAsInput && host.localEditorPath !== 'name') {
<div class="llm-modal-field">
<label>Source</label>
<select
[ngModel]="host.localEditorSourceMode()"
(pointerdown)="$event.stopPropagation()"
(ngModelChange)="host.onLocalEditorSourceModeChange($event)">
<option value="static">Static value</option>
<option value="input">Workflow input</option>
@if (host.canTakeGlobalInput()) {
<!--
Kept in place and disabled when the flow declares none, rather than hidden: the same
reasoning as the Use default button below, where an option that appears and disappears
is one nobody discovers. Selecting it used to lead to a mode that could not be completed.
-->
<option value="global" [disabled]="!host.localEditorGlobalInputs.length">
From global inputs{{ host.localEditorGlobalInputs.length ? '' : ' (none declared)' }}
</option>
}
</select>
@if (host.localEditorUseInput) {
<div class="llm-modal-tip">
This field will be provided by node input{{ host.localEditorBindableInputName ? (' "' + host.localEditorBindableInputName + '"') : '' }}.
</div>
}
</div>
}
<!--
Picking the global writes ${{global.<name>}} into the field, so nothing has to be typed -
which is the point: on a provider with a closed model list there is no text box at all.
-->
@if (host.localEditorFromGlobal) {
<div class="llm-modal-field">
<label>Global input</label>
<select
[ngModel]="host.localEditorGlobalName"
[disabled]="!host.localEditorGlobalInputs.length"
(pointerdown)="$event.stopPropagation()"
(ngModelChange)="host.onLocalEditorGlobalInputChange($event)">
<option value="">{{ host.localEditorGlobalInputs.length ? 'Select a global input...' : 'No global inputs available' }}</option>
@for (globalInput of host.localEditorGlobalInputs; track globalInput.name) {
<option [value]="globalInput.name">{{ host.globalInputLabel(globalInput) }}</option>
}
</select>
@if (!host.localEditorGlobalInputs.length) {
<div class="llm-modal-tip">This flow declares no global inputs yet. Add one to the flow to use it here.</div>
}
</div>
}
<!--
Hidden in global mode: the value there is the ${{global.x}} the picker above writes, so a
second control for the same field could only contradict it - a model picker reading
"Select model..." next to a source that says the model comes from a global input.
-->
@if (!host.localEditorFromGlobal) {
<div class="llm-modal-field">
<label>{{ host.localEditorLabel }}</label>
<div class="llm-modal-control-row">
@if (host.localEditorUseInput && host.localEditorPath !== 'name') {
<input
type="text"
[ngModel]="host.localEditorBindableInputName ? ('Provided by input ' + host.localEditorBindableInputName) : 'Provided by workflow input'"
readonly
(pointerdown)="$event.stopPropagation()" />
} @else if (host.localEditorLoading) {
<input type="text" [ngModel]="host.localEditorValue" placeholder="Loading..." disabled (pointerdown)="$event.stopPropagation()" />
} @else if (host.localEditorType === 'boolean') {
<button
type="button"
class="node-toggle"
[class.node-toggle-on]="host.localEditorValue === 'true'"
[attr.aria-pressed]="host.localEditorValue === 'true'"
(pointerdown)="$event.stopPropagation()"
(click)="host.localEditorValue = host.localEditorValue === 'true' ? 'false' : 'true'">
<span class="node-toggle-track">
<span class="node-toggle-thumb"></span>
</span>
<span class="node-toggle-label">{{ host.localEditorValue === 'true' ? 'true' : 'false' }}</span>
</button>
} @else if (host.localEditorWidget === 'textarea') {
<textarea
[rows]="host.localEditorRows ?? 6"
[(ngModel)]="host.localEditorValue"
[attr.maxlength]="host.localEditorMaxLength"
[placeholder]="host.localEditorMaxLength ? 'Max ' + host.localEditorMaxLength + ' characters' : 'Value...'"
(pointerdown)="$event.stopPropagation()"></textarea>
} @else if (host.localEditorHasRetriever && !host.localEditorFreeText) {
<select [(ngModel)]="host.localEditorValue" (pointerdown)="$event.stopPropagation()">
<option value="">{{ host.localEditorOptions.length ? ('Select ' + (host.localEditorLabel | lowercase) + '...') : 'No options available' }}</option>
@for (option of host.localEditorOptions; track option.value) {
<option [value]="option.value">{{ optionLabel(option) }}</option>
}
</select>
} @else {
<input
[type]="host.localEditorType === 'number' || host.localEditorType === 'integer' ? 'number' : 'text'"
[(ngModel)]="host.localEditorValue"
[attr.maxlength]="host.localEditorMaxLength"
[attr.min]="host.localEditorMin"
[attr.max]="host.localEditorMax"
[attr.step]="host.localEditorType === 'integer' ? 1 : (host.localEditorType === 'number' ? 0.1 : null)"
[placeholder]="host.editorPlaceholder()"
(pointerdown)="$event.stopPropagation()" />
}
@if (host.showLocalEditorDefault()) {
<button
type="button"
class="llm-btn llm-btn-ghost llm-modal-default-btn"
[disabled]="!host.canUseLocalEditorDefault()"
[attr.title]="host.localEditorUsesDefault() ? 'Already using the default' : 'Clear this field so the default applies'"
(pointerdown)="$event.stopPropagation()"
(click)="host.useLocalEditorDefault($event)">
Use default
</button>
}
</div>
@if (host.localEditorError(); as error) {
<small class="llm-modal-error">{{ error }}</small>
} @else if (host.localEditorDefaultHint(); as hint) {
<small class="llm-modal-hint">{{ hint }}</small>
}
<!-- The field's own description. The modal never rendered one, so every tip was invisible. -->
@if (host.localEditorTip) {
<small class="llm-modal-tip">{{ host.localEditorTip }}</small>
}
</div>
}
<div class="llm-modal-actions">
<button type="button" class="llm-btn llm-btn-ghost" (pointerdown)="$event.stopPropagation()" (click)="host.closeSimpleParamEditor($event)">Cancel</button>
<button type="button" class="llm-btn llm-btn-primary" [disabled]="!host.canSaveLocalEditor()" (pointerdown)="$event.stopPropagation()" (click)="host.saveSimpleParamEditor($event)">Save</button>
</div>
</div>
</dialog>
}