humainflow-web/src/app/shared/nodes/task-step-node/task-step-node.html

436 lines
15 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-node"
[class.llm-node--human]="isHumanNode()"
[class.llm-node-skipped]="isSkipped() && !hasExecutionErrors() && !hasExecutionWarnings()"
[class.llm-node-running]="isRunning() && !hasExecutionErrors()"
[class.llm-node-subflow-running]="isSubflowRunning() && !hasExecutionErrors()"
[class.llm-node-completed]="isCompleted() && !needsAttention() && !hasExecutionErrors() && !hasExecutionWarnings()"
[class.llm-node-attention]="needsAttention()"
[class.llm-node-error]="hasExecutionErrors()"
[class.llm-node-warning]="hasExecutionWarnings() && !hasExecutionErrors()"
[class.llm-node-bias-active]="isBiasActive() && !hasExecutionErrors()"
[class.llm-node-bias-changed]="(isBiasDownstreamChanged() || isBiasRoutingChangeSource()) && !hasExecutionErrors()">
@if (isBiasActive() || isBiasDownstreamChanged() || isBiasRoutingChangeSource()) {
<div class="llm-bias-canvas-badge-wrap">
@if (isBiasActive()) {
<span class="llm-bias-canvas-badge llm-bias-canvas-badge-active" title="Bias annotation active on this node">
<i class="bi bi-clipboard2-pulse-fill"></i>
</span>
}
@if (isBiasDownstreamChanged() || isBiasRoutingChangeSource()) {
<span class="llm-bias-canvas-badge llm-bias-canvas-badge-changed" title="Output changed by the highlighted bias report">
<i class="bi bi-arrow-left-right"></i>
</span>
}
</div>
}
@if (isSubflowRunning() && !hasExecutionErrors()) {
<div class="llm-subflow-badge-wrap">
<span class="llm-subflow-badge"
[title]="containerIterationIndex()
? 'Running iteration ' + containerIterationIndex() + ' in the subflow - open it in the execution tree'
: 'Running its subflow - open it in the execution tree'">
<i class="bi bi-arrow-repeat"></i>
@if (containerIterationIndex()) {
<span>Iteration {{ containerIterationIndex() }}</span>
} @else {
<span>Subflow</span>
}
</span>
</div>
}
@if (loopRound()) {
<div class="llm-subflow-badge-wrap">
<span class="llm-subflow-badge llm-loop-round-badge"
[title]="'Round ' + loopRound() + ' of its loop - earlier rounds are kept in the execution history'">
<i class="bi bi-arrow-repeat"></i>
<span>Round {{ loopRound() }}</span>
</span>
</div>
}
@if (needsAttention() && isHumanNode()) {
<div class="llm-attention-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<button
type="button"
class="llm-attention-trigger"
aria-label="Open waiting step details"
[disabled]="interactionSubmitting"
(click)="openInteractionModal($event)">
<i class="bi bi-hand-index-thumb-fill"></i>
</button>
</div>
}
@if (hasExecutionErrors()) {
<div class="llm-error-alert-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<button
type="button"
class="llm-error-alert"
aria-label="Open the execution error"
(click)="openExecutionErrors($event)">
<i class="bi bi-exclamation-triangle-fill"></i>
</button>
<div class="llm-error-tooltip">
<div class="llm-error-title">{{ executionErrorHeadline() }}</div>
<div class="llm-error-item">{{ executionErrorSummary() }}</div>
<div class="llm-error-hint">Click to read and copy the full error</div>
</div>
</div>
}
@if (hasExecutionWarnings()) {
<div class="llm-warning-alert-wrap">
<div class="llm-warning-alert">
<i class="bi bi-exclamation-circle-fill"></i>
</div>
<div class="llm-warning-list-tooltip">
<div class="llm-warning-list-title">Execution warnings</div>
@for (warning of executionWarnings(); track $index) {
<div class="llm-warning-list-item">{{ warning }}</div>
}
</div>
</div>
}
<div class="llm-header">
<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>
</div>
<div class="llm-node-metadata">
<span class="llm-node-capability-badge" [title]="capabilitiesTooltip()">
{{ visualRoleLabel() }}
</span>
@if (allBiasAnnotations().length) {
<button
type="button"
class="llm-node-bias-summary"
[class.llm-node-bias-summary-active]="activeBiasAnnotationCount() > 0"
[title]="'View ' + biasAnnotationBadgeLabel()"
(pointerdown)="$event.stopPropagation()"
(click)="openBiasAnnotations($event)">
<i class="bi bi-clipboard2-pulse-fill"></i>
{{ biasAnnotationBadgeLabel() }}
</button>
}
@if (stepSkipReason(); as skipReason) {
<span class="llm-node-skip-reason" [title]="skipReason">
Skipped: {{ skipReason }}
</span>
}
</div>
</div>
@if (hasMeasurableBiasAnnotations()) {
<button
type="button"
class="llm-bias-impact-trigger"
aria-label="Measure bias impact"
[title]="measureBiasImpactTooltip()"
[disabled]="!canMeasureBiasImpact()"
(pointerdown)="$event.stopPropagation()"
(click)="measureBiasImpact($event)">
<i class="bi bi-bar-chart-line"></i>
</button>
}
</div>
@if (allBiasAnnotations().length) {
<app-bias-annotations
[blockId]="data?.data?.id ?? ''"
[block]="biasAnnotationsBlock()"
[annotations]="allBiasAnnotations()"
[readonly]="true"
[hideTrigger]="true" />
}
<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">
@if (isInputConnected(input.key)) {
<span class="llm-socket-wrap llm-socket-wrap-input">
<div
refComponent
class="llm-socket"
[data]="{
type: 'socket',
side: 'input',
key: input.key,
nodeId: data.id,
__readonly: true,
payload: input.socket
}"
[emit]="emit">
</div>
</span>
} @else {
<span class="llm-input-value-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<button
type="button"
class="llm-input-value-icon"
aria-label="Input value available in Intermediate tab">
<i class="bi bi-box-arrow-in-right"></i>
</button>
</span>
}
<span class="llm-pill llm-pill-input">
<span class="llm-pill-meta">
@if (inputDisplayLabelParts(input.key).context; as context) {
<span class="llm-pill-context">{{ context }}</span>
}
<span class="llm-pill-name">{{ inputDisplayLabelParts(input.key).name }}</span>
<span class="llm-pill-kind">{{ inputKindLabel(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">
@if (outputDisplayLabelParts(output.key).context; as context) {
<span class="llm-pill-context">{{ context }}</span>
}
<span class="llm-pill-name">{{ outputDisplayLabelParts(output.key).name }}</span>
<span class="llm-pill-kind">{{ outputKindLabel(output.key) }}</span>
</span>
</span>
@if (isOutputConnected(output.key)) {
<span class="llm-socket-wrap llm-socket-wrap-output">
<div
refComponent
class="llm-socket llm-socket-right"
[data]="{
type: 'socket',
side: 'output',
key: output.key,
nodeId: data.id,
__readonly: true,
payload: output.socket
}"
[emit]="emit">
</div>
@if (showOutputTooltip() && executionOutputTooltip(output.key); as outputTooltip) {
<span class="llm-io-tooltip llm-io-tooltip-output">{{ outputTooltip }}</span>
}
</span>
} @else {
<span class="llm-output-value-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
<button
type="button"
class="llm-output-value-icon"
aria-label="Show output result">
<i class="bi bi-box-arrow-right"></i>
</button>
@if (showOutputTooltip()) {
<span class="llm-io-tooltip llm-io-tooltip-output">{{ outputValueTooltip(output.key) }}</span>
}
</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 (hasConnectedDependencyInput() && 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: true,
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 (hasConnectedDependantOutput() && 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: true,
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>
}
@if (parameterFieldGroups.length) {
<div class="llm-param-groups">
@for (group of parameterFieldGroups; track group.key) {
<fieldset class="llm-param-fieldset">
<legend class="llm-param-legend">{{ group.legend }}</legend>
<div class="llm-param-grid">
@for (field of group.fields; track field.path) {
<div class="llm-param-chip" [class.llm-param-chip-wide]="field.wide">
<div class="llm-param-row-head">
<span class="llm-param-key">{{ field.label }}</span>
@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>
<span
class="llm-param-value"
[class.llm-param-value-wide]="field.wide"
[class.llm-param-value-clamped]="field.expandable">
{{ field.value }}
</span>
</div>
}
</div>
</fieldset>
}
</div>
}
@if (parameterFields.length) {
<div class="llm-param-grid">
@for (field of parameterFields; track field.path) {
<div class="llm-param-chip" [class.llm-param-chip-wide]="field.wide">
<div class="llm-param-row-head">
<span class="llm-param-key">{{ field.label }}</span>
@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>
<span
class="llm-param-value"
[class.llm-param-value-wide]="field.wide"
[class.llm-param-value-clamped]="field.expandable">
{{ field.value }}
</span>
</div>
}
</div>
}
@if (hasMainContent()) {
@for (contentField of mainContentFields; track contentField.path) {
<div class="llm-param-block">
<div class="llm-param-row-head">
<div class="llm-param-key">{{ contentField.label }}</div>
@if (contentField.expandable) {
<button
type="button"
class="llm-param-view-btn"
aria-label="View full content"
(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">
@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 (arrayFields.length) {
<div class="llm-array-sections">
@for (arrayField of arrayFields; track arrayField.path) {
@if (arrayField.items.length) {
<div class="llm-array-block">
<div class="llm-param-row-head">
<div class="llm-param-key">{{ arrayField.label }}</div>
</div>
@for (item of arrayField.items; track item.index) {
<div class="llm-array-item">
<span class="llm-array-item-summary">{{ item.summary }}</span>
</div>
}
</div>
}
}
</div>
}
@if (hasViewableSubflow()) {
<div class="llm-subflow-section">
@for (subflow of viewableSubflows(); track subflow.path) {
<button type="button" class="llm-subflow-view" (pointerdown)="$event.stopPropagation()" (click)="openSubflowPreview(subflow.path, $event)">
View {{ subflow.label }}
</button>
}
</div>
}
</div>
</div>