feat(execution): add intermediate inputs tab and preview modal
This commit is contained in:
parent
4bcb9f5d3e
commit
877844fca0
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -81,21 +81,15 @@
|
|||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
@if (showInputTooltip() && executionInputTooltip(input.key); as inputTooltip) {
|
||||
<span class="llm-io-tooltip llm-io-tooltip-input">{{ inputTooltip }}</span>
|
||||
}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="llm-input-value-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
|
||||
<button
|
||||
type="button"
|
||||
class="llm-input-value-icon"
|
||||
aria-label="Show input value">
|
||||
aria-label="Input value available in Intermediate tab">
|
||||
<i class="bi bi-box-arrow-in-right"></i>
|
||||
</button>
|
||||
@if (showInputTooltip()) {
|
||||
<span class="llm-io-tooltip llm-io-tooltip-input">{{ inputValueTooltip(input.key) }}</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
<span class="llm-pill llm-pill-input">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, ExecutionIntermediateInputEntry[]>();
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,13 @@
|
|||
(click)="selectAsideTab('inputs')">
|
||||
Inputs
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="execution-aside-tab"
|
||||
[class.execution-aside-tab-active]="activeAsideTab() === 'intermediate'"
|
||||
(click)="selectAsideTab('intermediate')">
|
||||
Intermediate
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="execution-aside-tab"
|
||||
|
|
@ -154,6 +161,46 @@
|
|||
(textInputSubmit)="submitTextInput($event)"
|
||||
(fileInputChange)="onFileInputChange($event.input, $event.files)">
|
||||
</app-task-execution-inputs-panel>
|
||||
} @else if (activeAsideTab() === 'intermediate') {
|
||||
<div class="p-3 space-y-3">
|
||||
@if (!executionIntermediateInputGroups().length) {
|
||||
<div class="text-xs text-slate-500">No intermediate inputs available.</div>
|
||||
} @else {
|
||||
@for (group of executionIntermediateInputGroups(); track group.nodeTitle) {
|
||||
<div class="execution-intermediate-group">
|
||||
<div class="execution-intermediate-group-title">{{ group.nodeTitle }}</div>
|
||||
<div class="execution-intermediate-group-list">
|
||||
@for (input of group.inputs; track input.key) {
|
||||
<div class="execution-intermediate-item">
|
||||
<div class="execution-intermediate-item-head">
|
||||
<div class="min-w-0">
|
||||
<div class="execution-intermediate-item-name">{{ input.inputName }}</div>
|
||||
<div class="execution-intermediate-item-meta">
|
||||
<span>{{ input.sourceLabel }}</span>
|
||||
<span>{{ input.inputType }}</span>
|
||||
@if (input.itemLabel) {
|
||||
<span>{{ input.itemLabel }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (input.isLong) {
|
||||
<button
|
||||
type="button"
|
||||
class="execution-output-view-btn"
|
||||
aria-label="View full intermediate input"
|
||||
(click)="openIntermediateInputPreview(input, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="execution-intermediate-preview">{{ input.preview }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} @else if (activeAsideTab() === 'logs') {
|
||||
<div #logsScrollViewport class="execution-logs-scroll p-3 space-y-3">
|
||||
@if (logsLoading()) {
|
||||
|
|
@ -248,6 +295,23 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (intermediateInputPreviewModal(); as inputModal) {
|
||||
<div class="fixed inset-0 z-9999 flex items-center justify-center p-4">
|
||||
<div class="absolute inset-0 bg-black/45" (click)="closeIntermediateInputPreview($event)"></div>
|
||||
<div class="relative z-10 flex max-h-[90vh] w-full max-w-4xl flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl">
|
||||
<div class="flex items-start justify-between gap-4 border-b border-slate-200 px-5 py-4">
|
||||
<div class="min-w-0">
|
||||
<h3 class="text-lg font-semibold text-slate-900">{{ intermediateInputPreviewTitle(inputModal) }}</h3>
|
||||
<p class="mt-1 break-all text-xs text-slate-500">{{ intermediateInputPreviewSubtitle(inputModal) }}</p>
|
||||
</div>
|
||||
<button type="button" mat-stroked-button (click)="closeIntermediateInputPreview($event)">Close</button>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-auto px-5 py-4">
|
||||
<pre class="execution-output-modal">{{ inputModal.value }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="h-full bg-white border border-slate-200 rounded-md text-slate-400 text-center flex items-center justify-center">
|
||||
Select an execution from the left panel
|
||||
|
|
|
|||
|
|
@ -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<TaskExecution | null>(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<Record<string, boolean>>({});
|
||||
readonly authorizationErrors = signal<Record<string, string>>({});
|
||||
readonly outputPreviewModal = signal<ExecutionOutputEntry | null>(null);
|
||||
readonly intermediateInputPreviewModal = signal<ExecutionIntermediateInputEntry | null>(null);
|
||||
readonly executionLogs = signal<ExecutionEventLogEntry[]>([]);
|
||||
readonly logsLoading = signal(false);
|
||||
readonly logsError = signal<string | null>(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<ExecutionIntermediateInputEntry[]>(() =>
|
||||
buildExecutionIntermediateInputs(this.execution())
|
||||
);
|
||||
|
||||
readonly executionIntermediateInputGroups = computed<ExecutionIntermediateInputGroup[]>(() =>
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue