humainflow-web/src/app/shared/task-execution-viewer/task-execution-viewer.html

595 lines
30 KiB
HTML

<section class="h-full p-1" [class.execution-viewer-fullscreen]="isFullscreen()">
@if (execution()) {
<div class="h-full bg-white border border-slate-200 rounded-md flex flex-col overflow-hidden">
<div class="px-5 pt-2 border-b border-slate-200 bg-slate-50">
<div class="execution-header-main">
<div class="execution-header-details">
<h2 class="text-base font-semibold text-slate-900">
{{ execution()!.name }}
<button
type="button"
class="execution-header-fullscreen-toggle"
[matTooltip]="isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'"
[attr.aria-label]="isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'"
(click)="toggleFullscreen()">
<mat-icon [fontIcon]="isFullscreen() ? 'fullscreen_exit' : 'fullscreen'"></mat-icon>
</button>
</h2>
<div class="execution-header-badges">
@if (isSimulatedExecution()) {
<span class="execution-badge execution-badge-simulated"
matTooltip="Interactions were answered by a simulator, not by a person.">
Simulated
</span>
}
@if (isBiasVariant()) {
<span class="execution-badge execution-badge-bias"
matTooltip="Bias or mitigation probes were active, so this result is not a baseline.">
{{ biasVariantLabel() }}
</span>
}
@if (isSubflowExecution()) {
<span class="execution-badge execution-badge-subflow"
matTooltip="This run is one iteration of a container subflow.">
Subflow
</span>
}
<button
type="button"
class="execution-header-details-toggle"
[attr.aria-expanded]="headerDetailsOpen()"
(click)="toggleHeaderDetails()">
<mat-icon [fontIcon]="headerDetailsOpen() ? 'expand_less' : 'expand_more'"></mat-icon>
<span>Details</span>
</button>
</div>
@if (headerDetailsOpen()) {
<div class="execution-header-detail-list">
<p class="text-sm text-slate-500">Execution ID: {{ execution()!.id }}</p>
@if (isSubflowExecution()) {
<div class="execution-subflow-context">
<strong>Interactive container subflow</strong>
<span>Parent: {{ parentExecution()?.name || execution()!.parentExecutionId || 'Unknown' }}</span>
<span>Container: {{ parentContainerStep()?.node?.name || execution()!.parentStepId || 'Unknown' }}</span>
@if (subflowIterationIndex(); as iterationIndex) {
<span>Iteration {{ iterationIndex }}</span>
}
@if (execution()!.subflowRole) {
<span>Role: {{ execution()!.subflowRole }}</span>
}
</div>
}
@if (isSimulatedExecution() && simulationDescriptorLabel(); as simulationDescriptor) {
<p class="text-xs text-sky-900">Simulator: {{ simulationDescriptor }}</p>
}
@if (isBiasVariant() && execution()!.biasExecutionContext; as biasContext) {
<div class="execution-bias-variant-context">
<span>Experiment: {{ biasContext.experimentId || 'not available' }}</span>
<span>Baseline: {{ execution()!.rerunOfExecutionId || 'not available' }}</span>
<span>Interventions: {{ biasVariantLabel() }}</span>
</div>
}
</div>
}
</div>
</div>
</div>
<div class="p-1 grid grid-cols-1 md:grid-cols-5 gap-3 border-b border-slate-200">
<div class="bg-slate-50 border border-slate-200 rounded-md p-2">
<div class="text-xs text-slate-500">Status</div>
<div class="text-sm font-medium text-slate-800">{{ execution()!.context.status }}</div>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-md p-2">
<div class="text-xs text-slate-500">Start</div>
<div class="text-sm font-medium text-slate-800">{{ execution()!.context.startTime ? (execution()!.context.startTime | date:'dd/MM/yyyy HH:mm:ss') : '-' }}</div>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-md p-2">
<div class="text-xs text-slate-500">End</div>
<div class="text-sm font-medium text-slate-800">{{ execution()!.context.endTime ? (execution()!.context.endTime | date:'dd/MM/yyyy HH:mm:ss') : '-' }}</div>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-md p-2">
<div class="text-xs text-slate-500">Duration</div>
<div class="text-sm font-medium text-slate-800">{{ formattedDuration() }}</div>
</div>
<div class="bg-slate-50 border border-slate-200 rounded-md p-2">
<div class="text-xs text-slate-500">Steps</div>
<div class="text-sm font-medium text-slate-800">{{ stepsArray().length }}</div>
</div>
</div>
@if (execution()!.context.status === 'CANCELLED') {
<div class="mx-1 mt-1 rounded-md border border-slate-300 bg-slate-50 px-4 py-3 text-sm text-slate-700">
Execution cancelled. This execution is closed. Start a new execution to continue.
</div>
}
@if (execution()!.context.status === 'SUSPENDED') {
<div class="mx-1 mt-1 rounded-md border border-violet-200 bg-violet-50 px-4 py-3 text-sm text-violet-900">
Execution suspended after service restart. Resume the execution to continue.
</div>
}
@if (authorizationGate().missingProviders.length || authorizationGate().runtime.length) {
<div class="mx-1 mt-1 flex flex-wrap items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-900">
<span>
@if (authorizationGate().missingProviders.length) {
This execution needs a Vault credential for {{ authorizationGate().missingProviders.join(', ') }} before it can start.
} @else {
This execution needs a provider authorization before it can start.
}
</span>
<button type="button" mat-stroked-button (click)="openAuthorizationPanel()">Choose credential</button>
</div>
}
@if (outcomes().length) {
<div class="execution-outcomes">
<button
type="button"
class="execution-outcomes-title"
[attr.aria-expanded]="outcomesOpen()"
(click)="toggleOutcomes()">
<mat-icon [fontIcon]="outcomesOpen() ? 'expand_less' : 'expand_more'"></mat-icon>
<mat-icon fontIcon="flag"></mat-icon>
<span>{{ outcomes().length === 1 ? 'Outcome' : 'Outcomes' }}</span>
@if (!outcomesOpen()) {
<span class="execution-outcomes-summary">{{ outcomeCodes() }}</span>
}
</button>
@if (outcomesOpen()) {
@for (outcome of outcomes(); track outcome.stepId + outcome.timestamp) {
<div class="execution-outcome">
<div class="execution-outcome-head">
<span class="execution-outcome-code">{{ outcome.code }}</span>
<span class="execution-outcome-label">{{ outcome.label }}</span>
<span class="execution-outcome-step">via {{ stepNameForOutcome(outcome.stepId) }}</span>
</div>
@if (outcome.payload !== null && outcome.payload !== undefined) {
<div class="execution-outcome-payload">
@if (isTextPayload(outcome.payload)) {
<div class="execution-outcome-text">{{ outcome.payload }}</div>
} @else {
<app-json-viewer [value]="outcome.payload" [initiallyExpanded]="true" />
}
</div>
} @else {
<div class="execution-outcome-empty">
This End node received no value, so the outcome carries only its label.
</div>
}
</div>
}
}
</div>
}
<div class="flex-1 min-h-0 flex gap-2 p-1">
<div class="execution-graph-shell flex-1 border border-slate-200 rounded-md bg-slate-50 min-h-0 overflow-hidden">
<div class="px-3 py-2 text-xs font-semibold text-slate-600 border-b border-slate-200 bg-white">Execution Graph (read-only)</div>
<div class="execution-graph-body h-[calc(100%-33px)] execution-rete-readonly">
<div class="execution-graph-actions">
<button
type="button"
class="execution-action-button execution-graph-action-button execution-action-button-play"
[matTooltip]="startExecutionTooltip()"
[attr.aria-label]="startExecutionTooltip()"
[disabled]="!canStartExecution() || startInProgress()"
(click)="startExecution()">
<mat-icon fontIcon="play_arrow"></mat-icon>
<!-- Both run buttons keep the play; the badge carries the one thing that differs. -->
<span class="execution-action-badge"><mat-icon fontIcon="person"></mat-icon></span>
</button>
@if (execution()!.simulationAvailable === true) {
<button
type="button"
class="execution-action-button execution-graph-action-button execution-action-button-simulate"
matTooltip="Run simulated - an LLM answers the human steps"
aria-label="Run simulated - an LLM answers the human steps"
[disabled]="!canSimulateExecution()"
(click)="simulateExecution()">
<mat-icon fontIcon="play_arrow"></mat-icon>
<span class="execution-action-badge"><mat-icon fontIcon="smart_toy"></mat-icon></span>
</button>
}
<button
type="button"
class="execution-action-button execution-graph-action-button execution-action-button-stop"
[matTooltip]="cancelExecutionTooltip()"
[attr.aria-label]="cancelExecutionTooltip()"
[disabled]="!canCancelExecution()"
(click)="cancelExecution()">
<mat-icon fontIcon="stop"></mat-icon>
</button>
<button
type="button"
class="execution-action-button execution-graph-action-button execution-action-button-resume"
matTooltip="Resume execution"
aria-label="Resume execution"
[disabled]="!canResumeExecution()"
(click)="resumeExecution()">
<mat-icon fontIcon="restart_alt"></mat-icon>
</button>
<button
type="button"
class="execution-action-button execution-graph-action-button execution-graph-action-button-bias"
matTooltip="Create biased rerun"
aria-label="Create biased rerun"
[disabled]="!canCreateBiasedRerun()"
(click)="openBiasedRerunDialog()">
<mat-icon fontIcon="account_tree"></mat-icon>
</button>
@if (isBiasVariant()) {
<button
type="button"
class="execution-action-button execution-graph-action-button execution-graph-action-button-compare"
matTooltip="Compare with baseline"
aria-label="Compare with baseline"
[disabled]="!canCompareBiasExecution()"
(click)="openCompareDialog()">
<mat-icon fontIcon="compare_arrows"></mat-icon>
</button>
}
</div>
@if (isBiasHighlightActive()) {
<div class="execution-bias-legend">
<span class="execution-bias-legend-title">Bias report highlighted on canvas:</span>
<span class="execution-bias-legend-item"><i class="execution-bias-legend-dot execution-bias-legend-dot-active"></i> Bias annotation active</span>
<span class="execution-bias-legend-item"><i class="execution-bias-legend-dot execution-bias-legend-dot-changed"></i> Changed by bias / routing changed</span>
<button type="button" class="execution-bias-legend-reset" (click)="clearBiasHighlight()">Back to normal view</button>
</div>
}
<app-rete-editor
[flowId]="execution()!.id"
[flowData]="executionFlowData()"
[nodeView]="'execution'"
[readonly]="true">
</app-rete-editor>
</div>
</div>
<button
type="button"
class="self-center h-16 w-7 shrink-0 rounded-md border border-slate-300 bg-white text-slate-600 hover:bg-slate-50"
(click)="toggleContextAside()"
[attr.aria-label]="contextAsideOpen() ? 'Close execution context' : 'Open execution context'">
<i class="bi" [class.bi-chevron-right]="contextAsideOpen()" [class.bi-chevron-left]="!contextAsideOpen()"></i>
</button>
@if (contextAsideOpen()) {
<aside class="w-[22.5rem] shrink-0 border border-slate-200 rounded-md bg-white min-h-0 overflow-hidden flex flex-col">
<div class="execution-aside-tabs flex border-b border-slate-200 bg-slate-50 px-2 pt-2">
<button
type="button"
class="execution-aside-tab"
[class.execution-aside-tab-active]="activeAsideTab() === 'inputs'"
(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"
[class.execution-aside-tab-active]="activeAsideTab() === 'logs'"
(click)="selectAsideTab('logs')">
Logs
</button>
<button
type="button"
class="execution-aside-tab"
[class.execution-aside-tab-active]="activeAsideTab() === 'output'"
[disabled]="!executionOutputTabEnabled()"
(click)="selectAsideTab('output')">
Output
</button>
<button
type="button"
class="execution-aside-tab"
[class.execution-aside-tab-active]="activeAsideTab() === 'bias-reports'"
(click)="selectAsideTab('bias-reports')">
Bias impact
</button>
</div>
<div class="min-h-0 flex-1 overflow-auto">
@if (activeAsideTab() === 'inputs') {
@if (llmProviderCapabilitiesLoading()) {
<div class="m-3 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-800">Checking LLM provider credentials...</div>
}
@if (llmProviderCapabilitiesError(); as llmError) {
<div class="m-3 flex items-center justify-between gap-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">
<span>{{ llmError }}</span>
<button type="button" mat-stroked-button (click)="retryLlmProviderCapabilities()">Retry</button>
</div>
}
@for (entry of settledVaultAuthorizations(); track entry.requirement.key) {
<section class="m-3 flex items-center justify-between gap-3 rounded-md border border-emerald-200 bg-emerald-50 p-3">
<div class="min-w-0">
<div class="text-xs font-semibold text-slate-800">{{ entry.provider }} credential</div>
<div class="mt-1 truncate text-[11px] text-slate-600">{{ providedCredentialLabel(entry) }}</div>
</div>
<button type="button" mat-stroked-button (click)="changeVaultAuthorization(entry)">Change</button>
</section>
}
@for (entry of pendingVaultAuthorizations(); track entry.requirement.key) {
<section class="m-3 rounded-md border border-amber-200 bg-amber-50 p-3">
<div class="text-xs font-semibold text-slate-800">Vault credential for {{ entry.provider }}</div>
<div class="mt-1 text-[11px] text-slate-600">Select an active credential to start the execution.</div>
@if (entry.requiresCredential === false) {
<div class="mt-1 text-[11px] text-amber-800">This provider is listed as not requiring a credential, but the execution is asking for one.</div>
}
<mat-form-field appearance="outline" class="mt-2 w-full">
<mat-label>Credential</mat-label>
<mat-select
[value]="selectedLlmCredentialFor(entry)"
[disabled]="llmCredentialLoadingFor(entry) || authorizationSavingFor(entry)"
(selectionChange)="selectLlmCredential(entry, $event.value)">
@for (credential of llmCredentialOptionsFor(entry); track credential.id) {
<mat-option [value]="credential.id">
{{ credential.label }}{{ credential.description ? ' — ' + credential.description : '' }}
</mat-option>
}
</mat-select>
@if (llmCredentialLoadingFor(entry)) { <mat-hint>Loading credentials...</mat-hint> }
@if (authorizationSavingFor(entry)) { <mat-hint>Saving credential...</mat-hint> }
</mat-form-field>
@if (llmCredentialErrorFor(entry); as credentialError) {
<div class="mb-2 flex items-center justify-between gap-3 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">
<span>{{ credentialError }}</span>
<button type="button" mat-stroked-button (click)="retryLlmCredentials(entry)">Retry</button>
</div>
}
@if (authorizationErrorFor(entry); as authorizationError) {
<div class="mb-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">{{ authorizationError }}</div>
}
@if (!llmCredentialLoadingFor(entry) && !llmCredentialOptionsFor(entry).length && !llmCredentialErrorFor(entry)) {
<div class="mt-2 text-xs text-amber-800">No compatible credentials available.</div>
}
<div class="mt-2 flex flex-wrap gap-2">
@if (!llmCredentialLoadingFor(entry)) {
<button type="button" mat-stroked-button (click)="openVaultCredentialForm(entry.provider)">Add credential</button>
}
@if (editingAuthorizationKeys()[entry.requirement.key]) {
<button type="button" mat-stroked-button (click)="cancelVaultAuthorizationChange(entry)">Cancel</button>
}
</div>
</section>
}
@if (credentialFormOpen()) {
<form class="m-3 rounded-md border border-slate-200 bg-white p-3" (ngSubmit)="saveExecutionCredential()">
<div class="mb-2 text-xs font-semibold text-slate-800">New {{ credentialFormProvider() }} credential</div>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Label</mat-label>
<input matInput name="executionCredentialLabel" required [ngModel]="credentialFormLabel()" (ngModelChange)="credentialFormLabel.set($event)" />
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>Description (optional)</mat-label>
<input matInput name="executionCredentialDescription" [ngModel]="credentialFormDescription()" (ngModelChange)="credentialFormDescription.set($event)" />
</mat-form-field>
<mat-form-field appearance="outline" class="w-full">
<mat-label>API key</mat-label>
<input matInput type="password" autocomplete="off" name="executionCredentialValue" required [ngModel]="credentialFormValue()" (ngModelChange)="credentialFormValue.set($event)" />
<mat-hint>The value will not be shown after saving.</mat-hint>
</mat-form-field>
@if (credentialFormError(); as formError) {
<div class="mb-2 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">{{ formError }}</div>
}
<div class="flex flex-wrap gap-2">
<button type="submit" mat-flat-button [disabled]="credentialFormSaving() || !credentialFormLabel().trim() || !credentialFormValue().trim()">{{ credentialFormSaving() ? 'Saving...' : 'Save credential' }}</button>
<button type="button" mat-stroked-button [disabled]="credentialFormSaving()" (click)="closeVaultCredentialForm()">Cancel</button>
</div>
</form>
}
<app-task-execution-inputs-panel
[editableInputs]="editableInputs()"
[authorizationRequirements]="runtimeAuthorizationRequirements()"
[authorizationValues]="pendingAuthorizationValues()"
[savingAuthorizations]="savingAuthorizations()"
[authorizationErrors]="authorizationErrors()"
[savingInputs]="savingInputs()"
[savingErrors]="savingErrors()"
[readOnly]="inputsReadOnly()"
[pendingKeys]="pendingInputKeys()"
[saveError]="globalSaveError()"
[copySources]="inputCopySources()"
[copySummary]="inputCopySummary()"
(authorizationValueChange)="onAuthorizationValueChange($event.requirement, $event.value)"
(authorizationSubmit)="submitAuthorization($event)"
(textInputChange)="onTextInputChange($event.input, $event.value)"
(textInputSubmit)="submitTextInput($event)"
(submitAllInputs)="submitAllTextInputs()"
(copyRequested)="copyInputsFrom($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()) {
<div class="text-xs text-slate-500">Loading execution log...</div>
} @else if (logsError()) {
<div class="rounded-md border border-rose-200 bg-rose-50 px-3 py-2 text-xs text-rose-700">{{ logsError() }}</div>
} @else if (!visibleExecutionLogs().length) {
<div class="text-xs text-slate-500">No execution log available.</div>
} @else {
@for (event of visibleExecutionLogs(); track event.id) {
<div class="execution-log-card">
<div class="execution-log-header">
<div class="execution-log-title-wrap">
<mat-icon class="execution-log-icon" [fontIcon]="logTypeIcon(event.type)"></mat-icon>
<div>
<div class="execution-log-title">{{ event.messageText }}</div>
<div class="execution-log-meta">
<span>{{ event.timestamp | date:'dd/MM/yyyy HH:mm:ss' }}</span>
@if (event.nodeName) {
<span>{{ event.nodeName }}</span>
}
@if (event.type) {
<span>{{ event.type }}</span>
}
@if (event.innerNodeId) {
<span>Inner node: {{ event.innerNodeId }}</span>
}
@if (event.innerStepId) {
<span>Inner step: {{ event.innerStepId }}</span>
}
@if (event.innerExecutionId) {
<span>Inner execution: {{ event.innerExecutionId }}</span>
}
@if (event.iterationIndex) {
<span>Iteration: {{ event.iterationIndex }}</span>
}
@if (event.containerType) {
<span>Container: {{ event.containerType }}</span>
}
</div>
</div>
</div>
<span class="execution-log-level" [ngClass]="logLevelClass(event.level)">
{{ event.levelText }}
</span>
</div>
</div>
}
}
</div>
} @else if (activeAsideTab() === 'output') {
<div class="p-3 space-y-3">
@if (!executionOutputGroups().length) {
<div class="text-xs text-slate-500">No execution output available.</div>
} @else {
@for (group of executionOutputGroups(); track group.nodeTitle) {
<div class="execution-output-group">
<div class="execution-output-group-title">{{ group.nodeTitle }}</div>
<div class="execution-output-group-list">
@for (output of group.outputs; track output.key) {
<div class="execution-output-item">
<div class="execution-output-item-head">
<div>
<div class="execution-output-item-name">{{ output.outputName }}</div>
@if (output.itemLabel) {
<div class="text-[11px] text-slate-500">{{ output.itemLabel }}</div>
}
</div>
@if (output.isLong) {
<button
type="button"
class="execution-output-view-btn"
aria-label="View full output"
(click)="openOutputPreview(output, $event)">
<i class="bi bi-eye"></i>
</button>
}
</div>
<div class="execution-output-preview">{{ output.preview }}</div>
</div>
}
</div>
</div>
}
}
</div>
} @else {
<app-bias-impact-report-list
[executionId]="execution()!.id"
[annotatedNodeCount]="biasAnnotatedNodeCount()"
[blockedReason]="biasExperimentBlockedReason()"
(startExperimentRequested)="openBiasedRerunDialog()" />
}
</div>
</aside>
}
</div>
</div>
@if (outputPreviewModal(); as outputModal) {
<div class="fixed inset-0 z-9999 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-black/45" (click)="closeOutputPreview($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">{{ outputPreviewTitle(outputModal) }}</h3>
<p class="mt-1 break-all text-xs text-slate-500">{{ outputPreviewSubtitle(outputModal) }}</p>
</div>
<button type="button" mat-stroked-button (click)="closeOutputPreview($event)">Close</button>
</div>
<div class="min-h-0 flex-1 overflow-auto px-5 py-4">
<pre class="execution-output-modal">{{ outputModal.value }}</pre>
</div>
</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
</div>
}
</section>