388 lines
17 KiB
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>
|
|
}
|