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

319 lines
11 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.
-->
@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>
@if (laneBadge; as lane) {
<div class="llm-lane-chip" [style.border-color]="lane.color || '#94a3b8'">
<i class="bi bi-diagram-3"></i>
<span>{{ lane.name }}</span>
</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) {
@if (!focusOpen && hasOrderedParameterItems()) {
<button
type="button"
class="llm-clone-btn"
[class.llm-clone-btn--active]="nodeSettingsOpen"
[attr.aria-expanded]="nodeSettingsOpen"
[matTooltip]="nodeSettingsOpen ? 'Hide settings' : 'Show settings'"
(pointerdown)="$event.stopPropagation()"
(click)="toggleNodeSettings($event)">
<i class="bi bi-gear"></i>
</button>
}
<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-arrows-angle-expand]="!focusOpen" [class.bi-x-lg]="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" [title]="inputDisplayLabel(input.key)">
<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)" [title]="outputDisplayLabel(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 (resourceLinkInput) {
<div class="llm-body llm-body-dependency llm-body-resource">
<div class="llm-column">
@if (resourceLinkInput; as resourceLinkInput) {
<div class="llm-row llm-row-input llm-row-resource">
<span
refComponent
class="llm-pill llm-pill-input llm-pill-resource"
[class.llm-pill-resource--connected]="resourceLinkIsConnected"
[attr.title]="resourceLinkIsConnected ? 'Connected to ' + connectedResourceName + ' — drag another Resource here to replace it' : 'Drag a Resource here'"
[data]="{
type: 'socket',
side: 'input',
key: resourceLinkInput.key,
nodeId: data.id,
__readonly: isReadonly,
payload: resourceLinkInput.socket
}"
[emit]="emit">
@if (resourceLinkIsConnected) {
<i class="bi bi-link-45deg" aria-hidden="true"></i>
Connected to {{ connectedResourceName }}
} @else {
Resource
}
</span>
</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>
}
<app-node-parameters [host]="this" [showParameters]="nodeSettingsOpen || focusOpen" />
@if (biasAnnotationsAllowed) {
<app-bias-annotations
[blockId]="blockId ?? ''"
[block]="biasBlock"
[annotations]="biasAnnotations"
[readonly]="isReadonly"
(annotationsChange)="updateBiasAnnotations($event)" />
}
</div>