595 lines
30 KiB
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>
|