diff --git a/src/app/shared/nodes/task-step-node/task-step-node.css b/src/app/shared/nodes/task-step-node/task-step-node.css index ef90127..5635a6f 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.css +++ b/src/app/shared/nodes/task-step-node/task-step-node.css @@ -438,7 +438,7 @@ align-items: center; justify-content: center; padding: 0; - cursor: help; + cursor: default; border-radius: 999px; } @@ -502,16 +502,10 @@ pointer-events: none; } -.llm-io-tooltip-input { - left: calc(100% + 8px); -} - .llm-io-tooltip-output { right: calc(100% + 8px); } -.llm-input-value-wrap:hover .llm-io-tooltip, -.llm-input-value-wrap:focus-within .llm-io-tooltip, .llm-output-value-wrap:hover .llm-io-tooltip, .llm-output-value-wrap:focus-within .llm-io-tooltip, .llm-socket-wrap:hover .llm-io-tooltip, diff --git a/src/app/shared/nodes/task-step-node/task-step-node.html b/src/app/shared/nodes/task-step-node/task-step-node.html index c82b03d..b1ad74f 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.html +++ b/src/app/shared/nodes/task-step-node/task-step-node.html @@ -81,21 +81,15 @@ }" [emit]="emit"> - @if (showInputTooltip() && executionInputTooltip(input.key); as inputTooltip) { - {{ inputTooltip }} - } } @else { - @if (showInputTooltip()) { - {{ inputValueTooltip(input.key) }} - } } diff --git a/src/app/shared/nodes/task-step-node/task-step-node.ts b/src/app/shared/nodes/task-step-node/task-step-node.ts index eebbbbc..a24e226 100644 --- a/src/app/shared/nodes/task-step-node/task-step-node.ts +++ b/src/app/shared/nodes/task-step-node/task-step-node.ts @@ -352,10 +352,6 @@ export class TaskStepNodeComponent { return connectedInputs.includes(inputName); } - inputValueTooltip(inputName: string): string { - return this.executionInputTooltip(inputName) ?? 'not ready yet'; - } - showInputTooltip(): boolean { const statusGroup = this.blockConfiguration?.['__executionStatusGroup']; return statusGroup !== 'INIT'; @@ -767,6 +763,10 @@ export class TaskStepNodeComponent { ).subscribe({ next: () => { this.interactionSubmitting = false; + if (result.mode === 'complete') { + this.humanInteractionDialog.close(result); + return; + } this.humanInteractionDialog.update({ isSubmitting: false, submitError: null diff --git a/src/app/shared/task-execution-viewer/execution-viewer.utils.ts b/src/app/shared/task-execution-viewer/execution-viewer.utils.ts index 2bdde40..24f2591 100644 --- a/src/app/shared/task-execution-viewer/execution-viewer.utils.ts +++ b/src/app/shared/task-execution-viewer/execution-viewer.utils.ts @@ -26,17 +26,35 @@ export type ExecutionOutputEntry = { itemLabel: string | null; }; +export type ExecutionIntermediateInputEntry = { + key: string; + nodeTitle: string; + inputName: string; + inputType: string; + sourceLabel: string; + value: string; + preview: string; + isLong: boolean; + itemLabel: string | null; +}; + export type ExecutionOutputGroup = { nodeTitle: string; outputs: ExecutionOutputEntry[]; }; +export type ExecutionIntermediateInputGroup = { + nodeTitle: string; + inputs: ExecutionIntermediateInputEntry[]; +}; + export type ExecutionLogEntryView = ExecutionEventLogEntry & { messageText: string; levelText: string; }; const OUTPUT_PREVIEW_LIMIT = 80; +const INTERMEDIATE_INPUT_PREVIEW_LIMIT = 120; export function stepTitle(step: TaskExecutionStep | null | undefined): string { return getTaskExecutionStepNode(step)?.name?.trim() || step?.id || 'Step'; @@ -158,6 +176,52 @@ export function buildExecutionOutputGroups(outputs: ExecutionOutputEntry[]): Exe .sort((a, b) => a.nodeTitle.localeCompare(b.nodeTitle)); } +export function buildExecutionIntermediateInputs(execution: TaskExecution | null): ExecutionIntermediateInputEntry[] { + if (!execution) return []; + const contextInputs = execution.context.inputs ?? {}; + + return Object.values(execution.context.steps ?? {}) + .flatMap((step) => { + const inputValues = getExecutionInputValues(step, contextInputs); + return Object.entries(inputValues).flatMap(([inputName, rawValue]) => { + const input = (step.inputs ?? []).find((candidate) => candidate.descriptor?.name === inputName); + const values = Array.isArray(rawValue) ? rawValue : [rawValue]; + const isArrayValue = Array.isArray(rawValue); + + return values.map((item, index) => { + const value = stringifyOutputValue(item); + const isLong = value.length > INTERMEDIATE_INPUT_PREVIEW_LIMIT; + return { + key: isArrayValue ? `${step.id}:${inputName}:${index}` : `${step.id}:${inputName}`, + nodeTitle: stepTitle(step), + inputName, + inputType: String(input?.descriptor?.type ?? 'TEXT').toUpperCase(), + sourceLabel: input?.registered ? 'Connected input' : 'Prepared input', + value, + preview: isLong ? `${value.slice(0, INTERMEDIATE_INPUT_PREVIEW_LIMIT)}...` : value, + isLong, + itemLabel: isArrayValue ? `Item ${index + 1}` : null, + }; + }); + }); + }) + .filter((entry) => entry.value.trim().length > 0) + .sort((a, b) => a.nodeTitle.localeCompare(b.nodeTitle) || a.inputName.localeCompare(b.inputName)); +} + +export function buildExecutionIntermediateInputGroups( + inputs: ExecutionIntermediateInputEntry[] +): ExecutionIntermediateInputGroup[] { + const groups = new Map(); + for (const input of inputs) { + if (!groups.has(input.nodeTitle)) groups.set(input.nodeTitle, []); + groups.get(input.nodeTitle)!.push(input); + } + return Array.from(groups.entries()) + .map(([nodeTitle, items]) => ({ nodeTitle, inputs: items })) + .sort((a, b) => a.nodeTitle.localeCompare(b.nodeTitle)); +} + export function buildVisibleExecutionLogs(logs: ExecutionEventLogEntry[]): ExecutionLogEntryView[] { return [...logs] .sort((a, b) => a.timestamp - b.timestamp) diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.css b/src/app/shared/task-execution-viewer/task-execution-viewer.css index 55d66bb..026b611 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.css +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.css @@ -156,8 +156,9 @@ color: #64748b; font-size: 12px; font-weight: 700; - padding: 9px 12px; + padding: 9px 9px; cursor: pointer; + white-space: nowrap; } .execution-aside-tab-active { @@ -184,9 +185,78 @@ word-break: break-word; } +.execution-intermediate-group { + border: 1px solid #d9eadf; + border-radius: 8px; + background: #f7fbf8; + padding: 12px; +} + +.execution-intermediate-group-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 14px; + font-weight: 800; + color: #0f172a; +} + +.execution-intermediate-group-list { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.execution-intermediate-item { + border: 1px solid #cfe6d5; + border-radius: 8px; + background: #ffffff; + padding: 10px 12px; +} + +.execution-intermediate-item-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.execution-intermediate-item-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; + font-weight: 800; + text-transform: uppercase; + color: #15803d; +} + +.execution-intermediate-item-meta { + display: flex; + flex-wrap: wrap; + gap: 4px 8px; + margin-top: 3px; + color: #64748b; + font-size: 10px; + font-weight: 700; +} + +.execution-intermediate-preview { + margin-top: 8px; + white-space: pre-wrap; + word-break: break-word; + user-select: text; + color: #0f172a; + font-size: 12px; + line-height: 1.45; +} + .execution-output-group { border: 1px solid #e2e8f0; - border-radius: 12px; + border-radius: 8px; background: #f8fafc; padding: 12px; } @@ -206,7 +276,7 @@ .execution-output-item { border: 1px solid #dbe7f5; - border-radius: 10px; + border-radius: 8px; background: #ffffff; padding: 10px 12px; } diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.html b/src/app/shared/task-execution-viewer/task-execution-viewer.html index a4c56c9..d636a13 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.html +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.html @@ -120,6 +120,13 @@ (click)="selectAsideTab('inputs')"> Inputs + + } + +
{{ input.preview }}
+ + } + + + } + } + } @else if (activeAsideTab() === 'logs') {
@if (logsLoading()) { @@ -248,6 +295,23 @@
} + @if (intermediateInputPreviewModal(); as inputModal) { +
+
+
+
+
+

{{ intermediateInputPreviewTitle(inputModal) }}

+

{{ intermediateInputPreviewSubtitle(inputModal) }}

+
+ +
+
+
{{ inputModal.value }}
+
+
+
+ } } @else {
Select an execution from the left panel diff --git a/src/app/shared/task-execution-viewer/task-execution-viewer.ts b/src/app/shared/task-execution-viewer/task-execution-viewer.ts index 0c92dde..ff43dae 100644 --- a/src/app/shared/task-execution-viewer/task-execution-viewer.ts +++ b/src/app/shared/task-execution-viewer/task-execution-viewer.ts @@ -39,6 +39,8 @@ import { firstValueFrom } from 'rxjs'; import { ExecutionOutputEntry, ExecutionOutputGroup, + ExecutionIntermediateInputEntry, + ExecutionIntermediateInputGroup, ExecutionLogEntryView, stepTitle, stepNodeId, @@ -50,6 +52,8 @@ import { formatExecutionOutputLabel, buildExecutionOutputs, buildExecutionOutputGroups, + buildExecutionIntermediateInputs, + buildExecutionIntermediateInputGroups, buildVisibleExecutionLogs, isInputSet, normalizeEditableInputValue, @@ -81,7 +85,7 @@ export class TaskExecutionViewerComponent implements OnDestroy { private static readonly SIMULATOR_MODEL_RETRIEVER_URL = '/retriever/LLM/models'; readonly execution = input(null); readonly contextAsideOpen = signal(true); - readonly activeAsideTab = signal<'inputs' | 'logs' | 'output'>('inputs'); + readonly activeAsideTab = signal<'inputs' | 'intermediate' | 'logs' | 'output'>('inputs'); readonly startInProgress = signal(false); readonly simulateInProgress = signal(false); readonly cancelInProgress = signal(false); @@ -93,6 +97,7 @@ export class TaskExecutionViewerComponent implements OnDestroy { readonly savingAuthorizations = signal>({}); readonly authorizationErrors = signal>({}); readonly outputPreviewModal = signal(null); + readonly intermediateInputPreviewModal = signal(null); readonly executionLogs = signal([]); readonly logsLoading = signal(false); readonly logsError = signal(null); @@ -109,6 +114,7 @@ export class TaskExecutionViewerComponent implements OnDestroy { this.authorizationErrors.set({}); this.activeAsideTab.set('inputs'); this.outputPreviewModal.set(null); + this.intermediateInputPreviewModal.set(null); this.executionLogs.set([]); this.logsError.set(null); this.logsLoading.set(false); @@ -324,6 +330,14 @@ export class TaskExecutionViewerComponent implements OnDestroy { buildExecutionOutputGroups(this.executionOutputs()) ); + readonly executionIntermediateInputs = computed(() => + buildExecutionIntermediateInputs(this.execution()) + ); + + readonly executionIntermediateInputGroups = computed(() => + buildExecutionIntermediateInputGroups(this.executionIntermediateInputs()) + ); + readonly inputsReadOnly = computed(() => { const status = this.execution()?.context.status; return getExecutionStatusGroup(status) !== 'INIT'; @@ -468,7 +482,7 @@ export class TaskExecutionViewerComponent implements OnDestroy { this.contextAsideOpen.update((open) => !open); } - selectAsideTab(tab: 'inputs' | 'logs' | 'output') { + selectAsideTab(tab: 'inputs' | 'intermediate' | 'logs' | 'output') { if (tab === 'output' && !this.executionOutputTabEnabled()) return; this.activeAsideTab.set(tab); } @@ -496,6 +510,30 @@ export class TaskExecutionViewerComponent implements OnDestroy { this.outputPreviewModal.set(null); } + openIntermediateInputPreview(input: ExecutionIntermediateInputEntry, event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + if (!input.isLong) return; + this.intermediateInputPreviewModal.set(input); + } + + intermediateInputPreviewTitle(input: ExecutionIntermediateInputEntry | null): string { + if (!input) return 'Intermediate input'; + return input.nodeTitle; + } + + intermediateInputPreviewSubtitle(input: ExecutionIntermediateInputEntry | null): string { + if (!input) return ''; + const itemLabel = input.itemLabel ? ` ยท ${input.itemLabel}` : ''; + return `${input.inputName}${itemLabel}`; + } + + closeIntermediateInputPreview(event?: Event) { + event?.preventDefault(); + event?.stopPropagation(); + this.intermediateInputPreviewModal.set(null); + } + startExecution() { const executionId = this.execution()?.id; if (!executionId || !this.canStartExecution() || this.startInProgress()) return;