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

612 lines
24 KiB
HTML

@if (focusOpen) {
<div class="llm-node__focus-backdrop" (pointerdown)="$event.stopPropagation()" (click)="closeFocus($event)"></div>
}
<div class="llm-node"
[class.llm-node-error]="hasUpdateBlockError()"
[class.llm-node-delete-pending]="deleteConfirmOpen"
[class.llm-node--human]="isHumanNode()"
[class.llm-node--focus]="focusOpen"
(pointerdown)="focusOpen && $event.stopPropagation()"
(click)="focusOpen && $event.stopPropagation()">
@if (isCreatingOnServer()) {
<div class="llm-loading-overlay">
<div class="llm-loading-chip">
<span class="llm-spinner" aria-hidden="true"></span>
<span>Updating block...</span>
</div>
</div>
}
@if (isSchemaLoading && !isCreatingOnServer()) {
<div class="llm-loading-overlay">
<div class="llm-loading-chip">
<span class="llm-spinner" aria-hidden="true"></span>
<span>Loading block...</span>
</div>
</div>
}
@if (deleteConfirmOpen) {
<div class="llm-delete-overlay"></div>
}
<div class="llm-header" [class.cursor-move]="!isReadonly">
<div class="llm-icon">
@if (nodeIcon().type === 'class') {
<i [class]="nodeIcon().value"></i>
} @else {
<img [src]="nodeIcon().value" alt="Node" class="llm-icon-image" />
}
</div>
<div class="llm-header-content">
<span class="llm-title">{{ nodeTitle() }}</span>
<div class="llm-subtitle-row">
<span class="llm-subtitle">{{ name }}</span>
@if (!isReadonly) {
<button type="button" class="llm-name-edit-btn" title="Edit name" (pointerdown)="$event.stopPropagation()" (click)="openNameEditor($event)">
<i class="bi bi-pencil-square"></i>
</button>
}
</div>
<div class="llm-node-id">{{ nodeIdLabel }}</div>
</div>
<div class="llm-header-actions">
@if (hasUpdateBlockError()) {
<div class="llm-error-alert-wrap">
<div class="llm-error-alert">
<i class="bi bi-exclamation-circle-fill"></i>
</div>
<div class="llm-error-tooltip">
<div class="llm-error-title">Block update failed</div>
<div class="llm-error-item">{{ updateBlockErrorMessage() }}</div>
</div>
</div>
}
@if (missingRequiredParams.length) {
<div class="llm-warning-wrap">
<div class="llm-warning">
<i class="bi bi-exclamation-triangle-fill"></i>
</div>
<div class="llm-warning-tooltip">
<div class="llm-warning-title">Missing required fields</div>
@for (missing of missingRequiredParams; track missing) {
<div class="llm-warning-item">{{ missing }}</div>
}
</div>
</div>
}
@if (biasAnnotationBadge; as biasBadge) {
<div class="llm-bias-badge-wrap" [class.llm-bias-badge-wrap-probe]="biasBadge.hasExecutableProbe">
<div class="llm-bias-badge">
<i class="bi bi-clipboard2-pulse-fill"></i>
<span>{{ biasBadge.count }}</span>
</div>
<div class="llm-bias-badge-tooltip">
<div class="llm-bias-badge-title">
{{ biasBadge.count }} bias annotation{{ biasBadge.count === 1 ? '' : 's' }}@if (biasBadge.maxSeverityLabel) {, max severity: {{ biasBadge.maxSeverityLabel }}}
</div>
@if (biasBadge.hasExecutableProbe) {
<div class="llm-bias-badge-item">Includes an executable behavioral probe</div>
}
</div>
</div>
}
@if (!isReadonly && deleteConfirmOpen) {
<div class="llm-delete-confirm" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<span class="llm-delete-confirm-text">Delete node?</span>
<button type="button" class="llm-delete-confirm-cancel" (click)="cancelDelete($event)">Cancel</button>
<button type="button" class="llm-delete-confirm-action" (click)="confirmDelete($event)">Delete</button>
</div>
}
@if (!isReadonly) {
<button
type="button"
class="llm-clone-btn"
[matTooltip]="focusOpen ? 'Close expanded node' : 'Expand node'"
(pointerdown)="$event.stopPropagation()"
(click)="toggleFocus($event)">
<i class="bi" [class.bi-fullscreen]="!focusOpen" [class.bi-fullscreen-exit]="focusOpen"></i>
</button>
@if (!focusOpen) {
<button type="button" class="llm-clone-btn" matTooltip="Clone node" (pointerdown)="$event.stopPropagation()" (click)="cloneCurrentNode($event)">
<i class="bi bi-copy"></i>
</button>
<button type="button" class="llm-delete-btn" matTooltip="Delete node" (pointerdown)="$event.stopPropagation()" (click)="confirmDelete($event)">
<i class="bi bi-trash"></i>
</button>
}
}
</div>
</div>
<div class="llm-body">
<div class="llm-column">
<div class="llm-column-title">Inputs</div>
@for (input of inputs; track input.key) {
<div class="llm-row llm-row-input">
<div
refComponent
class="llm-socket"
[data]="{
type: 'socket',
side: 'input',
key: input.key,
nodeId: data.id,
__readonly: isReadonly,
payload: input.socket
}"
[emit]="emit">
</div>
<span class="llm-pill llm-pill-input">
<span class="llm-pill-meta">
<span class="llm-pill-name">{{ inputDisplayLabel(input.key) }}</span>
@if (!isReadonly && canTogglePortMultiplicity('input', input.key)) {
<select
class="llm-pill-kind-select"
[ngModel]="portCurrentKindValue('input', input.key)"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation()"
(ngModelChange)="onPortKindChange('input', input.key, $event)">
@for (option of portSelectableKindOptions('input', input.key); track option.value) {
<option [value]="option.value">{{ option.label }}</option>
}
</select>
} @else {
<span class="llm-pill-kind">{{ portCurrentKindLabel('input', input.key) }}</span>
}
</span>
</span>
</div>
}
</div>
<div class="llm-column">
<div class="llm-column-title llm-column-title-right">{{ outputsTitle() }}</div>
@for (output of outputs; track output.key) {
<div class="llm-row llm-row-output">
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)">
<span class="llm-pill-meta llm-pill-meta-output">
<span class="llm-pill-name">{{ outputDisplayLabel(output.key) }}</span>
@if (!isReadonly && canTogglePortMultiplicity('output', output.key)) {
<select
class="llm-pill-kind-select"
[ngModel]="portCurrentKindValue('output', output.key)"
(pointerdown)="$event.stopPropagation()"
(click)="$event.stopPropagation()"
(ngModelChange)="onPortKindChange('output', output.key, $event)">
@for (option of portSelectableKindOptions('output', output.key); track option.value) {
<option [value]="option.value">{{ option.label }}</option>
}
</select>
} @else {
<span class="llm-pill-kind">{{ portCurrentKindLabel('output', output.key) }}</span>
}
</span>
</span>
<div
refComponent
class="llm-socket llm-socket-right"
[data]="{
type: 'socket',
side: 'output',
key: output.key,
nodeId: data.id,
__readonly: isReadonly,
payload: output.socket
}"
[emit]="emit">
</div>
</div>
}
</div>
</div>
@if (hasExecutionDependencyPorts()) {
<div class="llm-body llm-body-dependency">
<div class="llm-column">
<div class="llm-column-title">Dependency</div>
@if (dependencyInput; as dependencyInput) {
<div class="llm-row llm-row-input">
<div
refComponent
class="llm-socket"
[data]="{
type: 'socket',
side: 'input',
key: dependencyInput.key,
nodeId: data.id,
__readonly: isReadonly,
payload: dependencyInput.socket
}"
[emit]="emit">
</div>
<span class="llm-pill llm-pill-input llm-pill-dependency-input">Depends on</span>
</div>
}
</div>
<div class="llm-column">
<div class="llm-column-title llm-column-title-right">Dependant</div>
@if (dependantOutput; as dependantOutput) {
<div class="llm-row llm-row-output">
<span class="llm-pill llm-pill-output llm-pill-dependency-output">Prerequisite of</span>
<div
refComponent
class="llm-socket llm-socket-right"
[data]="{
type: 'socket',
side: 'output',
key: dependantOutput.key,
nodeId: data.id,
__readonly: isReadonly,
payload: dependantOutput.socket
}"
[emit]="emit">
</div>
</div>
}
</div>
</div>
}
<div class="llm-params llm-params-shell">
@if (!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 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]="isReadonly || !field.enabled"
[attr.aria-pressed]="field.booleanValue"
[attr.title]="'Toggle ' + field.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="toggleBooleanParameter(field.path, $event)">
<span class="llm-bool-toggle-thumb"></span>
</button>
} @else if (!isReadonly) {
<button
type="button"
class="llm-edit-btn"
[disabled]="!field.enabled"
[attr.title]="'Edit ' + field.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="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)="openFieldPreview(field, $event)">
<i class="bi bi-eye"></i>
</button>
}
</div>
@if (field.type !== 'boolean') {
<span class="llm-param-value" [class.llm-param-value-clamped]="field.expandable">{{ field.value }}</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 (!isReadonly) {
<button type="button" class="llm-edit-btn" [attr.title]="'Edit ' + contentField.label.toLowerCase()" (pointerdown)="$event.stopPropagation()" (click)="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)="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">{{ formatDynamicInputToken(part.text) }}</span>
} @else {
<span>{{ part.text }}</span>
}
}
}
</div>
</div>
}
@if (item.arrayField; as arrayField) {
<div class="llm-array-block llm-param-chip llm-param-chip-wide">
<div class="llm-param-row-head">
<div class="llm-param-key">{{ arrayField.label }}</div>
@if (!isReadonly) {
<button
type="button"
class="llm-edit-btn"
[attr.title]="'Add ' + arrayField.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="addArrayItem(arrayField.path, $event)">
<i class="bi bi-plus-lg"></i>
</button>
}
</div>
@if (!arrayField.items.length) {
<div class="llm-array-empty">No items</div>
} @else {
@for (entry of arrayField.items; track entry.index) {
<div class="llm-array-item">
<span class="llm-array-item-summary">{{ entry.summary }}</span>
<div class="llm-array-item-actions">
@if (!isReadonly) {
<button
type="button"
class="llm-edit-btn"
title="Edit item"
(pointerdown)="$event.stopPropagation()"
(click)="editArrayItem(arrayField.path, entry.index, $event)">
<i class="bi bi-pen"></i>
</button>
}
@if (!isReadonly) {
<button
type="button"
class="llm-edit-btn llm-array-remove-btn"
title="Remove item"
(pointerdown)="$event.stopPropagation()"
(click)="removeArrayItem(arrayField.path, entry.index, $event)">
<i class="bi bi-trash"></i>
</button>
}
</div>
</div>
}
}
</div>
}
}
}
</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]="isReadonly || !field.enabled"
[attr.aria-pressed]="field.booleanValue"
[attr.title]="'Toggle ' + field.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="toggleBooleanParameter(field.path, $event)">
<span class="llm-bool-toggle-thumb"></span>
</button>
} @else if (!isReadonly) {
<button
type="button"
class="llm-edit-btn"
[disabled]="!field.enabled"
[attr.title]="'Edit ' + field.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="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)="openFieldPreview(field, $event)">
<i class="bi bi-eye"></i>
</button>
}
</div>
@if (field.type !== 'boolean') {
<span class="llm-param-value" [class.llm-param-value-clamped]="field.expandable">{{ field.value }}</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 (!isReadonly) {
<button type="button" class="llm-edit-btn" [attr.title]="'Edit ' + contentField.label.toLowerCase()" (pointerdown)="$event.stopPropagation()" (click)="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)="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">{{ formatDynamicInputToken(part.text) }}</span>
} @else {
<span>{{ part.text }}</span>
}
}
}
</div>
</div>
}
@if (item.arrayField; as arrayField) {
<div class="llm-array-block llm-param-chip llm-param-chip-wide">
<div class="llm-param-row-head">
<div class="llm-param-key">{{ arrayField.label }}</div>
@if (!isReadonly) {
<button
type="button"
class="llm-edit-btn"
[attr.title]="'Add ' + arrayField.label.toLowerCase()"
(pointerdown)="$event.stopPropagation()"
(click)="addArrayItem(arrayField.path, $event)">
<i class="bi bi-plus-lg"></i>
</button>
}
</div>
@if (!arrayField.items.length) {
<div class="llm-array-empty">No items</div>
} @else {
@for (entry of arrayField.items; track entry.index) {
<div class="llm-array-item">
<span class="llm-array-item-summary">{{ entry.summary }}</span>
<div class="llm-array-item-actions">
@if (!isReadonly) {
<button
type="button"
class="llm-edit-btn"
title="Edit item"
(pointerdown)="$event.stopPropagation()"
(click)="editArrayItem(arrayField.path, entry.index, $event)">
<i class="bi bi-pen"></i>
</button>
}
@if (!isReadonly) {
<button
type="button"
class="llm-edit-btn llm-array-remove-btn"
title="Remove item"
(pointerdown)="$event.stopPropagation()"
(click)="removeArrayItem(arrayField.path, entry.index, $event)">
<i class="bi bi-trash"></i>
</button>
}
</div>
</div>
}
}
</div>
}
}
</div>
}
}
</div>
<app-bias-annotations
[blockId]="blockId ?? ''"
[block]="biasBlock"
[annotations]="biasAnnotations"
[readonly]="isReadonly"
(annotationsChange)="updateBiasAnnotations($event)" />
@if (localEditorOpen) {
<dialog #simpleEditorDialog class="llm-modal-backdrop" (pointerdown)="$event.stopPropagation()" (click)="onSimpleEditorDialogClick($event)" (cancel)="closeSimpleParamEditor($event)">
<div class="llm-modal" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<div class="llm-modal-title">Edit {{ localEditorLabel }}</div>
@if (localEditorBindableAsInput && localEditorPath !== 'name') {
<div class="llm-modal-field">
<label>Source</label>
<select
[ngModel]="localEditorUseInput ? 'input' : 'static'"
(pointerdown)="$event.stopPropagation()"
(ngModelChange)="onLocalEditorSourceModeChange($event)">
<option value="static">Static value</option>
<option value="input">Workflow input</option>
</select>
@if (localEditorUseInput) {
<div class="llm-modal-tip">
This field will be provided by node input{{ localEditorBindableInputName ? (' "' + localEditorBindableInputName + '"') : '' }}.
</div>
}
</div>
}
<div class="llm-modal-field">
<label>{{ localEditorLabel }}</label>
@if (localEditorUseInput && localEditorPath !== 'name') {
<input
type="text"
[ngModel]="localEditorBindableInputName ? ('Provided by input ' + localEditorBindableInputName) : 'Provided by workflow input'"
readonly
(pointerdown)="$event.stopPropagation()" />
} @else if (localEditorLoading) {
<input type="text" [ngModel]="localEditorValue" placeholder="Loading..." disabled (pointerdown)="$event.stopPropagation()" />
} @else if (localEditorType === 'boolean') {
<button
type="button"
class="node-toggle"
[class.node-toggle-on]="localEditorValue === 'true'"
[attr.aria-pressed]="localEditorValue === 'true'"
(pointerdown)="$event.stopPropagation()"
(click)="localEditorValue = localEditorValue === 'true' ? 'false' : 'true'">
<span class="node-toggle-track">
<span class="node-toggle-thumb"></span>
</span>
<span class="node-toggle-label">{{ localEditorValue === 'true' ? 'true' : 'false' }}</span>
</button>
} @else if (localEditorWidget === 'textarea') {
<textarea
[rows]="localEditorRows ?? 6"
[(ngModel)]="localEditorValue"
[attr.maxlength]="localEditorMaxLength"
[placeholder]="localEditorMaxLength ? 'Max ' + localEditorMaxLength + ' characters' : 'Value...'"
(pointerdown)="$event.stopPropagation()"></textarea>
} @else if (localEditorHasRetriever) {
<select [(ngModel)]="localEditorValue" (pointerdown)="$event.stopPropagation()">
<option value="">{{ localEditorOptions.length ? ('Select ' + (localEditorLabel | lowercase) + '...') : 'No options available' }}</option>
@for (option of localEditorOptions; track option.value) {
<option [value]="option.value">{{ option.label }}</option>
}
</select>
} @else {
<input
[type]="localEditorType === 'number' || localEditorType === 'integer' ? 'number' : 'text'"
[(ngModel)]="localEditorValue"
[attr.maxlength]="localEditorMaxLength"
[placeholder]="localEditorMaxLength ? 'Max ' + localEditorMaxLength + ' characters' : 'Value...'"
(pointerdown)="$event.stopPropagation()" />
}
</div>
<div class="llm-modal-actions">
<button type="button" class="llm-btn llm-btn-ghost" (pointerdown)="$event.stopPropagation()" (click)="closeSimpleParamEditor($event)">Cancel</button>
<button type="button" class="llm-btn llm-btn-primary" [disabled]="!canSaveLocalEditor()" (pointerdown)="$event.stopPropagation()" (click)="saveSimpleParamEditor($event)">Save</button>
</div>
</div>
</dialog>
}
</div>