Extend execution graph and schema-driven node rendering
This commit is contained in:
parent
53a045a6ef
commit
514339599b
|
|
@ -1,4 +1,4 @@
|
|||
import { FlowBlock, FlowPort } from './flow';
|
||||
import { FlowBlock, FlowBlockConnection, FlowPort } from './flow';
|
||||
|
||||
export type TaskExecutionStatus = 'CREATED' | 'READY' | 'RUNNING' | 'WAITING' | 'SUCCESS' | 'ERROR';
|
||||
export type TaskExecutionStatusGroup = 'INIT' | 'RUNNING' | 'FINAL';
|
||||
|
|
@ -10,6 +10,7 @@ export type TaskExecution = {
|
|||
name: string;
|
||||
creationTime: number;
|
||||
context: TaskExecutionContext;
|
||||
stepConnections?: FlowBlockConnection[];
|
||||
};
|
||||
|
||||
export type TaskExecutionContext = {
|
||||
|
|
|
|||
|
|
@ -452,6 +452,18 @@
|
|||
border-color: #fecdd3;
|
||||
}
|
||||
|
||||
.llm-pill-output-true {
|
||||
color: #166534;
|
||||
background: #ecfdf3;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
|
||||
.llm-pill-output-false {
|
||||
color: #9f1239;
|
||||
background: #fff1f2;
|
||||
border-color: #fecdd3;
|
||||
}
|
||||
|
||||
.llm-input-info {
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
|
|
|||
|
|
@ -82,10 +82,10 @@
|
|||
</div>
|
||||
|
||||
<div class="llm-column">
|
||||
<div class="llm-column-title llm-column-title-right">Outputs</div>
|
||||
<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">{{ output.key }}</span>
|
||||
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)">{{ output.key }}</span>
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket llm-socket-right"
|
||||
|
|
@ -154,18 +154,19 @@
|
|||
}
|
||||
|
||||
@if (hasMainContent()) {
|
||||
@for (contentField of richContentFields; track contentField.path) {
|
||||
<div class="llm-param-block">
|
||||
<div class="llm-param-row-head">
|
||||
<div class="llm-param-key">{{ mainContentLabel() }}</div>
|
||||
<button type="button" class="llm-edit-btn" [attr.title]="'Edit ' + mainContentLabel().toLowerCase()" (pointerdown)="$event.stopPropagation()" (click)="openMainContentEditor($event)">
|
||||
<div class="llm-param-key">{{ contentField.label }}</div>
|
||||
<button type="button" class="llm-edit-btn" [attr.title]="'Edit ' + contentField.label.toLowerCase()" (pointerdown)="$event.stopPropagation()" (click)="openMainContentEditor(contentField.path, $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="llm-param-text">
|
||||
@if (!mainContentParts().length) {
|
||||
@if (!contentField.parts.length) {
|
||||
-
|
||||
} @else {
|
||||
@for (part of mainContentParts(); track $index) {
|
||||
@for (part of contentField.parts; track $index) {
|
||||
@if (part.isDynamicInput) {
|
||||
<span class="llm-inline-token">{{ formatDynamicInputToken(part.text) }}</span>
|
||||
} @else {
|
||||
|
|
@ -175,6 +176,7 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@ type EditableFieldGroupView = {
|
|||
fields: EditableFieldView[];
|
||||
};
|
||||
|
||||
type RichContentView = {
|
||||
path: string;
|
||||
label: string;
|
||||
parts: { text: string; isDynamicInput: boolean }[];
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-generic-node',
|
||||
imports: [CommonModule, FormsModule, ReteModule],
|
||||
|
|
@ -92,6 +98,7 @@ export class GenericNodeComponent {
|
|||
inputs: { key: string; socket: ClassicPreset.Socket }[] = [];
|
||||
parameterFields: EditableFieldView[] = [];
|
||||
parameterFieldGroups: EditableFieldGroupView[] = [];
|
||||
richContentFields: RichContentView[] = [];
|
||||
name = 'noName';
|
||||
|
||||
localEditorOpen = false;
|
||||
|
|
@ -116,6 +123,7 @@ export class GenericNodeComponent {
|
|||
this.inputs = [];
|
||||
this.parameterFields = [];
|
||||
this.parameterFieldGroups = [];
|
||||
this.richContentFields = [];
|
||||
|
||||
Object.entries(this.data.outputs).forEach(([key, output]) => {
|
||||
this.outputs.push({ key, socket: (output as any).socket });
|
||||
|
|
@ -241,18 +249,16 @@ export class GenericNodeComponent {
|
|||
this.closeSimpleParamEditor();
|
||||
}
|
||||
|
||||
async openMainContentEditor(event?: Event) {
|
||||
async openMainContentEditor(path: string, event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
const contentKey = this.mainContentKey();
|
||||
if (!contentKey) return;
|
||||
if (!this.isPathVisible(contentKey)) return;
|
||||
if (!this.isPathVisible(path)) return;
|
||||
|
||||
const contentLabel = this.mainContentLabel();
|
||||
const ui = this.getFieldUiMeta(contentKey);
|
||||
const currentValue = String(this.getByPath(this.blockConfiguration ?? {}, contentKey) ?? '');
|
||||
await this.openTextareaEditor(contentKey, contentLabel, currentValue, ui);
|
||||
const contentLabel = pathToLabel(path);
|
||||
const ui = this.getFieldUiMeta(path);
|
||||
const currentValue = String(this.getByPath(this.blockConfiguration ?? {}, path) ?? '');
|
||||
await this.openTextareaEditor(path, contentLabel, currentValue, ui);
|
||||
}
|
||||
|
||||
async confirmDelete(event?: Event) {
|
||||
|
|
@ -272,6 +278,10 @@ export class GenericNodeComponent {
|
|||
return this.blockType === 'HumanInteractionBlock';
|
||||
}
|
||||
|
||||
isConditionalNode(): boolean {
|
||||
return this.blockType === 'ConditionalBlock';
|
||||
}
|
||||
|
||||
nodeTitle(): string {
|
||||
const type = this.blockType;
|
||||
if (!type) return 'Node';
|
||||
|
|
@ -282,27 +292,21 @@ export class GenericNodeComponent {
|
|||
.trim();
|
||||
}
|
||||
|
||||
mainContentLabel(): string {
|
||||
const contentKey = this.mainContentKey();
|
||||
if (!contentKey) return 'Content';
|
||||
return pathToLabel(contentKey);
|
||||
outputsTitle(): string {
|
||||
return this.isConditionalNode() ? 'On Condition' : 'Outputs';
|
||||
}
|
||||
|
||||
outputPillClass(outputKey: string): string | null {
|
||||
if (!this.isConditionalNode()) return null;
|
||||
|
||||
const normalized = outputKey.trim().toLowerCase();
|
||||
if (normalized === 'true') return 'llm-pill-output-true';
|
||||
if (normalized === 'false') return 'llm-pill-output-false';
|
||||
return null;
|
||||
}
|
||||
|
||||
hasMainContent(): boolean {
|
||||
const contentKey = this.mainContentKey();
|
||||
return !!contentKey && this.isPathVisible(contentKey);
|
||||
}
|
||||
|
||||
mainContentParts(): { text: string; isDynamicInput: boolean }[] {
|
||||
const contentKey = this.mainContentKey();
|
||||
if (!contentKey || !this.isPathVisible(contentKey)) return [];
|
||||
const content = toStringOrNull(this.getByPath(this.blockConfiguration ?? {}, contentKey));
|
||||
if (!content) return [];
|
||||
const ui = this.getFieldUiMeta(contentKey);
|
||||
if (ui.widget === 'textarea' && ui.acceptVariableAsPlaceholder) {
|
||||
return splitTemplatedTextParts(content);
|
||||
}
|
||||
return [{ text: content, isDynamicInput: false }];
|
||||
return this.richContentFields.length > 0;
|
||||
}
|
||||
|
||||
formatDynamicInputToken(token: string): string {
|
||||
|
|
@ -619,10 +623,18 @@ export class GenericNodeComponent {
|
|||
|
||||
private refreshParameterFields() {
|
||||
const config = this.blockConfiguration ?? {};
|
||||
const contentKey = this.mainContentKey();
|
||||
const richContentPaths = new Set(this.richContentPaths());
|
||||
const grouped = new Map<string, EditableFieldView[]>();
|
||||
const rootFields: EditableFieldView[] = [];
|
||||
|
||||
this.richContentFields = this.richContentPaths()
|
||||
.filter((path) => this.isPathVisible(path))
|
||||
.map((path) => ({
|
||||
path,
|
||||
label: pathToLabel(path),
|
||||
parts: this.toRichContentParts(path)
|
||||
}));
|
||||
|
||||
if (this.editableFieldDefinitions.length) {
|
||||
const orderedFields = this.editableFieldDefinitions.map((definition) => {
|
||||
const value = this.getByPath(config, definition.path);
|
||||
|
|
@ -631,7 +643,7 @@ export class GenericNodeComponent {
|
|||
label: definition.label,
|
||||
value: valueToDisplayString(value)
|
||||
};
|
||||
}).filter((field) => field.path !== contentKey)
|
||||
}).filter((field) => !richContentPaths.has(field.path))
|
||||
.filter((field) => this.isPathVisible(field.path));
|
||||
|
||||
for (const field of orderedFields) {
|
||||
|
|
@ -657,7 +669,7 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
const fallbackFields = flattenPrimitiveValues(config)
|
||||
.filter((entry) => entry.path !== 'name' && entry.path !== 'type' && entry.path !== contentKey)
|
||||
.filter((entry) => entry.path !== 'name' && entry.path !== 'type' && !richContentPaths.has(entry.path))
|
||||
.filter((entry) => this.isPathVisible(entry.path))
|
||||
.map((entry) => ({
|
||||
path: entry.path,
|
||||
|
|
@ -751,22 +763,27 @@ export class GenericNodeComponent {
|
|||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
private mainContentKey(): string | null {
|
||||
const preferredPath = this.editableFieldDefinitions.find((field) =>
|
||||
field.ui.widget === 'textarea'
|
||||
&& field.ui.acceptVariableAsPlaceholder
|
||||
&& this.isPathVisible(field.path)
|
||||
)?.path;
|
||||
if (preferredPath) {
|
||||
return preferredPath;
|
||||
private richContentPaths(): string[] {
|
||||
const preferredPaths = this.editableFieldDefinitions
|
||||
.filter((field) => field.ui.widget === 'textarea' && field.ui.acceptVariableAsPlaceholder)
|
||||
.map((field) => field.path);
|
||||
if (preferredPaths.length) {
|
||||
return preferredPaths;
|
||||
}
|
||||
|
||||
const schemaCandidates = this.findMainContentCandidatePaths(this.blockSchema);
|
||||
for (const candidate of schemaCandidates) {
|
||||
if (this.isPathVisible(candidate)) return candidate;
|
||||
return this.findMainContentCandidatePaths(this.blockSchema);
|
||||
}
|
||||
|
||||
private toRichContentParts(path: string): { text: string; isDynamicInput: boolean }[] {
|
||||
const content = toStringOrNull(this.getByPath(this.blockConfiguration ?? {}, path));
|
||||
if (!content) return [];
|
||||
|
||||
const ui = this.getFieldUiMeta(path);
|
||||
if (ui.widget === 'textarea' && ui.acceptVariableAsPlaceholder) {
|
||||
return splitTemplatedTextParts(content);
|
||||
}
|
||||
|
||||
return null;
|
||||
return [{ text: content, isDynamicInput: false }];
|
||||
}
|
||||
|
||||
private findMainContentCandidatePaths(schema: Record<string, any> | null): string[] {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,18 @@
|
|||
animation: llmAttentionBorderPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.llm-node-skipped {
|
||||
border: 2px solid #94a3b8;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
box-shadow: 0 8px 20px rgba(100, 116, 139, 0.16);
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
.llm-node-skipped .llm-header {
|
||||
background: linear-gradient(135deg, #94a3b8 0%, #64748b 100%);
|
||||
border-bottom-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.llm-error-alert-wrap,
|
||||
.llm-warning-alert-wrap {
|
||||
position: absolute;
|
||||
|
|
@ -330,6 +342,18 @@
|
|||
border-color: #fecdd3;
|
||||
}
|
||||
|
||||
.llm-pill-output-true {
|
||||
color: #166534;
|
||||
background: #ecfdf3;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
|
||||
.llm-pill-output-false {
|
||||
color: #9f1239;
|
||||
background: #fff1f2;
|
||||
border-color: #fecdd3;
|
||||
}
|
||||
|
||||
.llm-input-value-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<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-completed]="isCompleted() && !needsAttention() && !hasExecutionErrors() && !hasExecutionWarnings()"
|
||||
[class.llm-node-attention]="needsAttention()"
|
||||
|
|
@ -103,10 +104,10 @@
|
|||
</div>
|
||||
|
||||
<div class="llm-column">
|
||||
<div class="llm-column-title llm-column-title-right">Outputs</div>
|
||||
<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">
|
||||
<span class="llm-pill llm-pill-output" [ngClass]="outputPillClass(output.key)">
|
||||
<span class="llm-pill-label">{{ output.key }}</span>
|
||||
</span>
|
||||
@if (isOutputConnected(output.key)) {
|
||||
|
|
@ -180,12 +181,13 @@
|
|||
}
|
||||
|
||||
@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">{{ mainContentLabel() }}</div>
|
||||
<div class="llm-param-key">{{ contentField.label }}</div>
|
||||
</div>
|
||||
<div class="llm-param-text">
|
||||
@for (part of mainContentParts(); track $index) {
|
||||
@for (part of contentField.parts; track $index) {
|
||||
@if (part.isDynamicInput) {
|
||||
<span class="llm-inline-token">{{ formatDynamicInputToken(part.text) }}</span>
|
||||
} @else {
|
||||
|
|
@ -194,6 +196,7 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class TaskStepNodeComponent {
|
|||
parameterFieldGroups: DisplayFieldGroup[] = [];
|
||||
|
||||
name = 'Step';
|
||||
mainContent: MainContentView | null = null;
|
||||
mainContentFields: MainContentView[] = [];
|
||||
|
||||
private blockSchema: Record<string, any> | null = null;
|
||||
private variablePlaceholderPaths = new Set<string>();
|
||||
|
|
@ -92,21 +92,18 @@ export class TaskStepNodeComponent {
|
|||
this.name = toStringOrNull(config['name']) ?? this.name;
|
||||
const primitiveEntries = flattenPrimitiveValues(config);
|
||||
const visibleEntries = primitiveEntries.filter((entry) => this.isPathVisible(entry.path));
|
||||
const contentEntry = this.pickMainContentEntry(visibleEntries);
|
||||
|
||||
this.mainContent = contentEntry
|
||||
? {
|
||||
path: contentEntry.path,
|
||||
label: pathToLabel(contentEntry.path),
|
||||
parts: this.toMainContentParts(contentEntry.path, String(contentEntry.value))
|
||||
}
|
||||
: null;
|
||||
const richContentPaths = this.pickMainContentEntries(visibleEntries).map((entry) => entry.path);
|
||||
this.mainContentFields = this.pickMainContentEntries(visibleEntries).map((entry) => ({
|
||||
path: entry.path,
|
||||
label: pathToLabel(entry.path),
|
||||
parts: this.toMainContentParts(entry.path, String(entry.value))
|
||||
}));
|
||||
|
||||
const grouped = new Map<string, DisplayField[]>();
|
||||
const rootFields: DisplayField[] = [];
|
||||
const orderedFields = visibleEntries
|
||||
.filter((entry) => !['name', 'type'].includes(entry.path))
|
||||
.filter((entry) => entry.path !== contentEntry?.path)
|
||||
.filter((entry) => !richContentPaths.includes(entry.path))
|
||||
.map((entry) => ({
|
||||
path: entry.path,
|
||||
label: pathToLabel(entry.path),
|
||||
|
|
@ -144,6 +141,10 @@ export class TaskStepNodeComponent {
|
|||
return this.blockType === 'HumanInteractionBlock';
|
||||
}
|
||||
|
||||
isConditionalNode(): boolean {
|
||||
return this.blockType === 'ConditionalBlock';
|
||||
}
|
||||
|
||||
nodeTitle(): string {
|
||||
const type = this.blockType;
|
||||
if (!type) return 'Task Step';
|
||||
|
|
@ -153,16 +154,21 @@ export class TaskStepNodeComponent {
|
|||
.trim();
|
||||
}
|
||||
|
||||
outputsTitle(): string {
|
||||
return this.isConditionalNode() ? 'On Condition' : 'Outputs';
|
||||
}
|
||||
|
||||
outputPillClass(outputKey: string): string | null {
|
||||
if (!this.isConditionalNode()) return null;
|
||||
|
||||
const normalized = outputKey.trim().toLowerCase();
|
||||
if (normalized === 'true') return 'llm-pill-output-true';
|
||||
if (normalized === 'false') return 'llm-pill-output-false';
|
||||
return null;
|
||||
}
|
||||
|
||||
hasMainContent(): boolean {
|
||||
return (this.mainContent?.parts.length ?? 0) > 0;
|
||||
}
|
||||
|
||||
mainContentLabel(): string {
|
||||
return this.mainContent?.label ?? 'Content';
|
||||
}
|
||||
|
||||
mainContentParts(): { text: string; isDynamicInput: boolean }[] {
|
||||
return this.mainContent?.parts ?? [];
|
||||
return this.mainContentFields.length > 0;
|
||||
}
|
||||
|
||||
formatDynamicInputToken(token: string): string {
|
||||
|
|
@ -257,6 +263,10 @@ export class TaskStepNodeComponent {
|
|||
return this.stepStatus() === 'RUNNING';
|
||||
}
|
||||
|
||||
isSkipped(): boolean {
|
||||
return this.stepStatus() === 'SKIPPED';
|
||||
}
|
||||
|
||||
stepStatus(): string {
|
||||
const status = this.blockConfiguration?.['__stepStatus'];
|
||||
return typeof status === 'string' ? status.toUpperCase() : '';
|
||||
|
|
@ -324,27 +334,23 @@ export class TaskStepNodeComponent {
|
|||
return this.executionOutputTooltip(outputKey) ?? '';
|
||||
}
|
||||
|
||||
private pickMainContentEntry(entries: Array<{ path: string; value: unknown }>) {
|
||||
private pickMainContentEntries(entries: Array<{ path: string; value: unknown }>) {
|
||||
const candidates = entries
|
||||
.filter((entry) => !['name', 'type'].includes(entry.path))
|
||||
.filter((entry) => typeof entry.value === 'string')
|
||||
.map((entry) => ({ ...entry, text: String(entry.value).trim() }))
|
||||
.filter((entry) => entry.text.length > 0);
|
||||
|
||||
if (!candidates.length) return null;
|
||||
if (!candidates.length) return [];
|
||||
|
||||
const placeholderCandidates = candidates.filter((entry) =>
|
||||
this.variablePlaceholderPaths.has(entry.path)
|
||||
);
|
||||
const scope = placeholderCandidates.length ? placeholderCandidates : candidates;
|
||||
|
||||
scope.sort((a, b) => {
|
||||
if (b.text.length !== a.text.length) return b.text.length - a.text.length;
|
||||
return a.path.localeCompare(b.path);
|
||||
});
|
||||
scope.sort((a, b) => a.path.localeCompare(b.path));
|
||||
|
||||
const chosen = scope[0];
|
||||
return { path: chosen.path, value: chosen.text };
|
||||
return scope.map((chosen) => ({ path: chosen.path, value: chosen.text }));
|
||||
}
|
||||
|
||||
private getExecutionMessages(key: '__executionErrors' | '__executionWarnings'): string[] {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { Component, computed, inject, input, OnDestroy, signal } from '@angular/core';
|
||||
import { FlowData } from '@models/flow';
|
||||
import { FlowBlockConnection, FlowData } from '@models/flow';
|
||||
import { getExecutionStatusGroup, TaskExecution, TaskExecutionStep } from '@models/task-execution';
|
||||
import {
|
||||
EditableExecutionInput,
|
||||
|
|
@ -61,7 +61,7 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
}
|
||||
}));
|
||||
|
||||
const connections = this.inferConnections(steps);
|
||||
const connections = this.getExecutionConnections(steps);
|
||||
return { blocks, connections };
|
||||
});
|
||||
|
||||
|
|
@ -266,25 +266,28 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
|
||||
private inferConnections(steps: TaskExecutionStep[]) {
|
||||
const connections: FlowData['connections'] = [];
|
||||
const indexedSteps = steps.map((step, index) => ({ step, index }));
|
||||
|
||||
for (const targetStep of steps) {
|
||||
for (const targetEntry of indexedSteps) {
|
||||
const targetStep = targetEntry.step;
|
||||
for (const input of targetStep.inputs ?? []) {
|
||||
if (!input.registered) continue;
|
||||
|
||||
const candidates = steps
|
||||
.filter((step) => step.id !== targetStep.id)
|
||||
.flatMap((sourceStep) =>
|
||||
(sourceStep.outputs ?? [])
|
||||
const candidates = indexedSteps
|
||||
.filter(({ step }) => step.id !== targetStep.id)
|
||||
.flatMap((sourceEntry) =>
|
||||
(sourceEntry.step.outputs ?? [])
|
||||
.filter((output) => output.connected && output.descriptor.type === input.descriptor.type)
|
||||
.map((output) => ({
|
||||
sourceStep,
|
||||
sourceStep: sourceEntry.step,
|
||||
sourceIndex: sourceEntry.index,
|
||||
sourceOutputName: output.descriptor.name
|
||||
}))
|
||||
);
|
||||
|
||||
if (candidates.length !== 1) continue;
|
||||
if (!candidates.length) continue;
|
||||
|
||||
const source = candidates[0];
|
||||
const source = this.pickBestConnectionCandidate(candidates, targetEntry.index);
|
||||
const id = `${source.sourceStep.id}_${source.sourceOutputName}_${targetStep.id}_${input.descriptor.name}`;
|
||||
if (connections.some((connection) => connection.id === id)) continue;
|
||||
|
||||
|
|
@ -301,6 +304,36 @@ export class TaskExecutionViewerComponent implements OnDestroy {
|
|||
return connections;
|
||||
}
|
||||
|
||||
private getExecutionConnections(steps: TaskExecutionStep[]): FlowBlockConnection[] {
|
||||
const explicitConnections = this.execution()?.stepConnections;
|
||||
if (explicitConnections?.length) {
|
||||
return explicitConnections.map((connection) => ({
|
||||
id: String(connection.id),
|
||||
sourceId: String(connection.sourceId),
|
||||
sourceName: String(connection.sourceName),
|
||||
targetId: String(connection.targetId),
|
||||
targetName: String(connection.targetName)
|
||||
}));
|
||||
}
|
||||
|
||||
return this.inferConnections(steps);
|
||||
}
|
||||
|
||||
private pickBestConnectionCandidate(
|
||||
candidates: Array<{ sourceStep: TaskExecutionStep; sourceIndex: number; sourceOutputName: string }>,
|
||||
targetIndex: number
|
||||
) {
|
||||
const previousCandidates = candidates
|
||||
.filter((candidate) => candidate.sourceIndex < targetIndex)
|
||||
.sort((left, right) => right.sourceIndex - left.sourceIndex);
|
||||
|
||||
if (previousCandidates.length) {
|
||||
return previousCandidates[0];
|
||||
}
|
||||
|
||||
return [...candidates].sort((left, right) => left.sourceIndex - right.sourceIndex)[0];
|
||||
}
|
||||
|
||||
private getExecutionInputValues(
|
||||
step: TaskExecutionStep,
|
||||
contextInputs: Record<string, unknown>
|
||||
|
|
|
|||
Loading…
Reference in New Issue