319 lines
11 KiB
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>
|