moved task in a service
This commit is contained in:
parent
36167f13ac
commit
1c5184892d
|
|
@ -1,7 +1,12 @@
|
|||
import { Component, signal } from '@angular/core';
|
||||
import { Component, computed, effect, inject, signal } from '@angular/core';
|
||||
import { TaskExecution } from '@models/task-execution';
|
||||
import { TasksExecutionsListComponent, TaskExecutionListItem } from '@shared/tasks-executions-list/tasks-executions-list';
|
||||
import {
|
||||
TaskExecutionListItem,
|
||||
TaskExecutionStatus,
|
||||
TasksExecutionsListComponent
|
||||
} from '@shared/tasks-executions-list/tasks-executions-list';
|
||||
import { TaskExecutionViewerComponent } from '@shared/task-execution-viewer/task-execution-viewer';
|
||||
import { TaskExecutionsService } from '@services/task-executions/task-executions';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tasks-executor',
|
||||
|
|
@ -10,244 +15,68 @@ import { TaskExecutionViewerComponent } from '@shared/task-execution-viewer/task
|
|||
styleUrl: './tasks-executor.css',
|
||||
})
|
||||
export class TasksExecutor {
|
||||
readonly executionDetails = signal<TaskExecution[]>([
|
||||
{
|
||||
id: 'c106be9d-5467-428c-8992-0b5f40a59aac',
|
||||
name: 'Test Flow',
|
||||
creationTime: 1772619308394,
|
||||
context: {
|
||||
inputs : {
|
||||
'feeb2977-370f-4bf9-aa84-04fa2f11e365:name' : 'Leonardo da Vinci'
|
||||
},
|
||||
result: {},
|
||||
startTime: 1772619308397,
|
||||
endTime: 1772619308398,
|
||||
errors: {
|
||||
'5ceb9b7b-88a0-41bb-afef-76fcb1f57918': 'Provider not found: testProvider'
|
||||
},
|
||||
warnings: {
|
||||
'b2540579-ca7b-4beb-8ed3-65136e7f03d6': 'Response was slower than expected'
|
||||
},
|
||||
steps: {
|
||||
'b2540579-ca7b-4beb-8ed3-65136e7f03d6': {
|
||||
block: {
|
||||
id: 'b2540579-ca7b-4beb-8ed3-65136e7f03d6',
|
||||
position: { x: 120, y: 160 },
|
||||
sink: false,
|
||||
name: 'first',
|
||||
inputs: [{ name: 'name', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'response', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'LLMBlockConfiguration',
|
||||
name: 'first',
|
||||
llmDescriptor: { provider: 'testProvider', model: 'testModel' },
|
||||
prompt: 'When ${{name}} died ? give me only the year as number'
|
||||
},
|
||||
typeName: 'LLMBlock'
|
||||
},
|
||||
id: 'b2540579-ca7b-4beb-8ed3-65136e7f03d6',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: { name: 'name', type: 'TEXT', multiple: false },
|
||||
value: 'Leonardo da Vinci',
|
||||
registered: false,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: { name: 'response', type: 'TEXT', multiple: false },
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
status: 'FAILED',
|
||||
started: true,
|
||||
simulated: false
|
||||
},
|
||||
'5ceb9b7b-88a0-41bb-afef-76fcb1f57918': {
|
||||
block: {
|
||||
id: '5ceb9b7b-88a0-41bb-afef-76fcb1f57918',
|
||||
position: { x: 500, y: 160 },
|
||||
sink: true,
|
||||
name: 'second',
|
||||
inputs: [{ name: 'year', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'response', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'LLMBlockConfiguration',
|
||||
name: 'second',
|
||||
llmDescriptor: { provider: 'testProvider', model: 'testModel' },
|
||||
prompt: 'what was the most important event in year ${{year}}?'
|
||||
},
|
||||
typeName: 'LLMBlock'
|
||||
},
|
||||
id: '5ceb9b7b-88a0-41bb-afef-76fcb1f57918',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: { name: 'year', type: 'TEXT', multiple: false },
|
||||
value: null,
|
||||
registered: true,
|
||||
set: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: { name: 'response', type: 'TEXT', multiple: false },
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
status: 'WAITING_FOR_INPUT',
|
||||
started: true,
|
||||
simulated: false
|
||||
}
|
||||
},
|
||||
status: 'ERROR',
|
||||
waitingSteps: [],
|
||||
executionResult: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'eda4040e-ae58-4ffb-a3b4-16dfef45a6c2',
|
||||
name: 'Test Flow',
|
||||
creationTime: 1772623910033,
|
||||
context: {
|
||||
inputs: {
|
||||
'95ebb03f-80e0-412d-87ee-2d4b7ddef240:name': 'marie curie'
|
||||
},
|
||||
result: {
|
||||
'68c5949c-1c74-400e-a1aa-b5f7739e5bb2:output': 'Based on the provided context, Marie Curie is a remarkable scientist and explorer who made significant contributions to the field of science and exploration. She is often considered a pioneer in the field of radioactivity.\n'
|
||||
},
|
||||
startTime: 1772623910037,
|
||||
endTime: 1772623947755,
|
||||
errors: {},
|
||||
warnings: {},
|
||||
steps: {
|
||||
'95ebb03f-80e0-412d-87ee-2d4b7ddef240': {
|
||||
block: {
|
||||
id: '95ebb03f-80e0-412d-87ee-2d4b7ddef240',
|
||||
position: { x: 120, y: 140 },
|
||||
sink: false,
|
||||
name: 'first',
|
||||
inputs: [{ name: 'name', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'response', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'LLMBlockConfiguration',
|
||||
name: 'first',
|
||||
llmDescriptor: {
|
||||
provider: 'ollamaTestProvider',
|
||||
model: 'sam860/gemma3:270m'
|
||||
},
|
||||
prompt: 'Make a question about ${{name}}'
|
||||
},
|
||||
typeName: 'LLMBlock'
|
||||
},
|
||||
id: '95ebb03f-80e0-412d-87ee-2d4b7ddef240',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'name',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
value: 'marie curie',
|
||||
registered: false,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'response',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
status: 'COMPLETED',
|
||||
started: true,
|
||||
simulated: false
|
||||
},
|
||||
'68c5949c-1c74-400e-a1aa-b5f7739e5bb2': {
|
||||
block: {
|
||||
id: '68c5949c-1c74-400e-a1aa-b5f7739e5bb2',
|
||||
position: { x: 500, y: 140 },
|
||||
sink: true,
|
||||
name: 'interactive',
|
||||
inputs: [{ name: 'input', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'output', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'HumanInteractiveBlockConfiguration',
|
||||
name: 'interactive',
|
||||
actionDescription: 'Answer the question in input',
|
||||
llmDescriptor: {
|
||||
provider: 'ollamaTestProvider',
|
||||
model: 'sam860/gemma3:270m'
|
||||
},
|
||||
},
|
||||
typeName: 'HumanInteractionBlock'
|
||||
},
|
||||
id: '68c5949c-1c74-400e-a1aa-b5f7739e5bb2',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'input',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
value: 'Marie Curie is a remarkable scientist and explorer who made significant contributions to science and exploration. She is often considered a pioneer in the field of radioactivity. What is your opinion about her contributions to the field of science and exploration?',
|
||||
registered: true,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'output',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
status: 'COMPLETED',
|
||||
started: true,
|
||||
simulated: true
|
||||
}
|
||||
},
|
||||
status: 'SUCCESS',
|
||||
waitingSteps: [],
|
||||
executionResult: {
|
||||
'68c5949c-1c74-400e-a1aa-b5f7739e5bb2:output': 'Based on the provided context, Marie Curie is a remarkable scientist and explorer who made significant contributions to the field of science and exploration. She is often considered a pioneer in the field of radioactivity.\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
private taskExecutionsService = inject(TaskExecutionsService);
|
||||
|
||||
readonly executions = signal<TaskExecutionListItem[]>([
|
||||
{
|
||||
id: 'c106be9d-5467-428c-8992-0b5f40a59aac',
|
||||
title: 'Test Flow',
|
||||
flowName: 'Test Flow',
|
||||
status: 'ERROR',
|
||||
startedAt: '2026-03-03 10:35',
|
||||
duration: '00:00:00'
|
||||
},
|
||||
{
|
||||
id: 'eda4040e-ae58-4ffb-a3b4-16dfef45a6c2',
|
||||
title: 'Test Flow',
|
||||
flowName: 'Test Flow',
|
||||
status: 'COMPLETED',
|
||||
startedAt: '2026-03-03 11:51',
|
||||
duration: '00:37:43'
|
||||
}
|
||||
]);
|
||||
readonly executionDetails = this.taskExecutionsService.taskExecutions;
|
||||
|
||||
readonly selectedExecutionId = signal<string | null>(this.executions()[0]?.id ?? null);
|
||||
readonly executions = computed<TaskExecutionListItem[]>(() =>
|
||||
this.executionDetails().map((execution) => ({
|
||||
id: execution.id,
|
||||
title: execution.name,
|
||||
flowName: execution.name,
|
||||
status: this.toListStatus(execution.context.status),
|
||||
startedAt: this.formatDateTime(execution.creationTime),
|
||||
duration: this.formatDuration(execution.context.startTime ?? null, execution.context.endTime ?? null)
|
||||
}))
|
||||
);
|
||||
|
||||
readonly selectedExecution = () =>
|
||||
this.executionDetails().find((execution) => execution.id === this.selectedExecutionId()) ?? null;
|
||||
readonly selectedExecutionId = signal<string | null>(null);
|
||||
|
||||
readonly selectedExecution = computed<TaskExecution | null>(() => {
|
||||
const selectedId = this.selectedExecutionId();
|
||||
const details = this.executionDetails();
|
||||
if (!details.length) return null;
|
||||
if (!selectedId) return details[0];
|
||||
return details.find((execution) => execution.id === selectedId) ?? details[0];
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.taskExecutionsService.init();
|
||||
effect(() => {
|
||||
if (this.selectedExecutionId()) return;
|
||||
const first = this.executions()[0];
|
||||
if (first) this.selectedExecutionId.set(first.id);
|
||||
});
|
||||
}
|
||||
|
||||
selectExecution(id: string) {
|
||||
this.selectedExecutionId.set(id);
|
||||
}
|
||||
|
||||
private toListStatus(status: string): TaskExecutionStatus {
|
||||
if (status === 'COMPLETED') return 'COMPLETED';
|
||||
if (status === 'FAILED' || status === 'ERROR') return 'FAILED';
|
||||
if (status === 'RUNNING' || status === 'WAITING' || status === 'WAITING_FOR_INPUT') return 'RUNNING';
|
||||
return 'QUEUED';
|
||||
}
|
||||
|
||||
private formatDateTime(timestamp: number): string {
|
||||
const date = new Date(timestamp);
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
const hh = String(date.getHours()).padStart(2, '0');
|
||||
const mi = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
|
||||
}
|
||||
|
||||
private formatDuration(startTime: number | null, endTime: number | null): string {
|
||||
if (!startTime || !endTime) return '00:00:00';
|
||||
const diffMs = Math.max(0, endTime - startTime);
|
||||
const totalSeconds = Math.floor(diffMs / 1000);
|
||||
const hh = String(Math.floor(totalSeconds / 3600)).padStart(2, '0');
|
||||
const mm = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0');
|
||||
const ss = String(totalSeconds % 60).padStart(2, '0');
|
||||
return `${hh}:${mm}:${ss}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { TaskExecution } from '@models/task-execution';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export abstract class TaskExecutionsCallServiceBase {
|
||||
abstract retrieveAllTaskExecutions(): Observable<TaskExecution[]>;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
import { TaskExecution } from '@models/task-execution';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { TaskExecutionsCallServiceBase } from './task-executions-call.base';
|
||||
|
||||
export class TaskExecutionsCallServiceFake extends TaskExecutionsCallServiceBase {
|
||||
private readonly data: TaskExecution[] = [
|
||||
{
|
||||
id: 'c106be9d-5467-428c-8992-0b5f40a59aac',
|
||||
name: 'Test Flow',
|
||||
creationTime: 1772619308394,
|
||||
context: {
|
||||
inputs: {
|
||||
'feeb2977-370f-4bf9-aa84-04fa2f11e365:name': 'Leonardo da Vinci'
|
||||
},
|
||||
result: {},
|
||||
startTime: 1772619308397,
|
||||
endTime: 1772619308398,
|
||||
errors: {
|
||||
'5ceb9b7b-88a0-41bb-afef-76fcb1f57918': 'Provider not found: testProvider'
|
||||
},
|
||||
warnings: {
|
||||
'b2540579-ca7b-4beb-8ed3-65136e7f03d6': 'Response was slower than expected'
|
||||
},
|
||||
steps: {
|
||||
'b2540579-ca7b-4beb-8ed3-65136e7f03d6': {
|
||||
block: {
|
||||
id: 'b2540579-ca7b-4beb-8ed3-65136e7f03d6',
|
||||
position: { x: 120, y: 160 },
|
||||
sink: false,
|
||||
name: 'first',
|
||||
inputs: [{ name: 'name', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'response', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'LLMBlockConfiguration',
|
||||
name: 'first',
|
||||
llmDescriptor: { provider: 'testProvider', model: 'testModel' },
|
||||
prompt: 'When ${{name}} died ? give me only the year as number'
|
||||
},
|
||||
typeName: 'LLMBlock'
|
||||
},
|
||||
id: 'b2540579-ca7b-4beb-8ed3-65136e7f03d6',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: { name: 'name', type: 'TEXT', multiple: false },
|
||||
value: 'Leonardo da Vinci',
|
||||
registered: false,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: { name: 'response', type: 'TEXT', multiple: false },
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
status: 'FAILED',
|
||||
started: true,
|
||||
simulated: false
|
||||
},
|
||||
'5ceb9b7b-88a0-41bb-afef-76fcb1f57918': {
|
||||
block: {
|
||||
id: '5ceb9b7b-88a0-41bb-afef-76fcb1f57918',
|
||||
position: { x: 500, y: 160 },
|
||||
sink: true,
|
||||
name: 'second',
|
||||
inputs: [{ name: 'year', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'response', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'LLMBlockConfiguration',
|
||||
name: 'second',
|
||||
llmDescriptor: { provider: 'testProvider', model: 'testModel' },
|
||||
prompt: 'what was the most important event in year ${{year}}?'
|
||||
},
|
||||
typeName: 'LLMBlock'
|
||||
},
|
||||
id: '5ceb9b7b-88a0-41bb-afef-76fcb1f57918',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: { name: 'year', type: 'TEXT', multiple: false },
|
||||
value: null,
|
||||
registered: true,
|
||||
set: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: { name: 'response', type: 'TEXT', multiple: false },
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
status: 'WAITING_FOR_INPUT',
|
||||
started: true,
|
||||
simulated: false
|
||||
}
|
||||
},
|
||||
status: 'ERROR',
|
||||
waitingSteps: [],
|
||||
executionResult: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'eda4040e-ae58-4ffb-a3b4-16dfef45a6c2',
|
||||
name: 'Test Flow',
|
||||
creationTime: 1772623910033,
|
||||
context: {
|
||||
inputs: {
|
||||
'95ebb03f-80e0-412d-87ee-2d4b7ddef240:name': 'marie curie'
|
||||
},
|
||||
result: {
|
||||
'68c5949c-1c74-400e-a1aa-b5f7739e5bb2:output':
|
||||
'Based on the provided context, Marie Curie is a remarkable scientist and explorer who made significant contributions to the field of science and exploration. She is often considered a pioneer in the field of radioactivity.\n'
|
||||
},
|
||||
startTime: 1772623910037,
|
||||
endTime: 1772623947755,
|
||||
errors: {},
|
||||
warnings: {},
|
||||
steps: {
|
||||
'95ebb03f-80e0-412d-87ee-2d4b7ddef240': {
|
||||
block: {
|
||||
id: '95ebb03f-80e0-412d-87ee-2d4b7ddef240',
|
||||
position: { x: 120, y: 140 },
|
||||
sink: false,
|
||||
name: 'first',
|
||||
inputs: [{ name: 'name', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'response', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'LLMBlockConfiguration',
|
||||
name: 'first',
|
||||
llmDescriptor: {
|
||||
provider: 'ollamaTestProvider',
|
||||
model: 'sam860/gemma3:270m'
|
||||
},
|
||||
prompt: 'Make a question about ${{name}}'
|
||||
},
|
||||
typeName: 'LLMBlock'
|
||||
},
|
||||
id: '95ebb03f-80e0-412d-87ee-2d4b7ddef240',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'name',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
value: 'marie curie',
|
||||
registered: false,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'response',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
status: 'COMPLETED',
|
||||
started: true,
|
||||
simulated: false
|
||||
},
|
||||
'68c5949c-1c74-400e-a1aa-b5f7739e5bb2': {
|
||||
block: {
|
||||
id: '68c5949c-1c74-400e-a1aa-b5f7739e5bb2',
|
||||
position: { x: 500, y: 140 },
|
||||
sink: true,
|
||||
name: 'interactive',
|
||||
inputs: [{ name: 'input', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'output', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'HumanInteractiveBlockConfiguration',
|
||||
name: 'interactive',
|
||||
actionDescription: 'Answer the question in input',
|
||||
llmDescriptor: {
|
||||
provider: 'ollamaTestProvider',
|
||||
model: 'sam860/gemma3:270m'
|
||||
}
|
||||
},
|
||||
typeName: 'HumanInteractionBlock'
|
||||
},
|
||||
id: '68c5949c-1c74-400e-a1aa-b5f7739e5bb2',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'input',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
value:
|
||||
'Marie Curie is a remarkable scientist and explorer who made significant contributions to science and exploration. She is often considered a pioneer in the field of radioactivity. What is your opinion about her contributions to the field of science and exploration?',
|
||||
registered: true,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'output',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
status: 'COMPLETED',
|
||||
started: true,
|
||||
simulated: true
|
||||
}
|
||||
},
|
||||
status: 'SUCCESS',
|
||||
waitingSteps: [],
|
||||
executionResult: {
|
||||
'68c5949c-1c74-400e-a1aa-b5f7739e5bb2:output':
|
||||
'Based on the provided context, Marie Curie is a remarkable scientist and explorer who made significant contributions to the field of science and exploration. She is often considered a pioneer in the field of radioactivity.\n'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '74ec477f-b04e-494c-80cc-968a40527bef',
|
||||
name: 'Test Flow',
|
||||
creationTime: 1772705504567,
|
||||
context: {
|
||||
inputs: {
|
||||
'f91ec0f7-03e8-4208-89ac-bd9db46dca8c:name': 'marie curie'
|
||||
},
|
||||
result: {},
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
errors: {},
|
||||
warnings: {},
|
||||
steps: {
|
||||
'ab7e0b08-c653-4d11-b808-e0e51c89d989': {
|
||||
block: {
|
||||
id: 'ab7e0b08-c653-4d11-b808-e0e51c89d989',
|
||||
position: { x: 500, y: 140 },
|
||||
sink: true,
|
||||
name: 'interactive',
|
||||
inputs: [{ name: 'input', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'output', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'HumanInteractiveBlockConfiguration',
|
||||
name: 'interactive',
|
||||
actionDescription: 'Answer the question in input',
|
||||
simulateWith: {
|
||||
provider: 'ollamaTestProvider',
|
||||
model: 'sam860/gemma3:270m'
|
||||
}
|
||||
},
|
||||
typeName: 'HumanInteractionBlock'
|
||||
},
|
||||
id: 'ab7e0b08-c653-4d11-b808-e0e51c89d989',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'input',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
value: null,
|
||||
registered: true,
|
||||
set: false
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'output',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
status: 'WAITING_FOR_INPUT',
|
||||
started: false,
|
||||
simulated: false
|
||||
},
|
||||
'f91ec0f7-03e8-4208-89ac-bd9db46dca8c': {
|
||||
block: {
|
||||
id: 'f91ec0f7-03e8-4208-89ac-bd9db46dca8c',
|
||||
position: { x: 120, y: 140 },
|
||||
sink: false,
|
||||
name: 'first',
|
||||
inputs: [{ name: 'name', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'response', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'LLMBlockConfiguration',
|
||||
name: 'first',
|
||||
llmDescriptor: {
|
||||
provider: 'ollamaTestProvider',
|
||||
model: 'sam860/gemma3:270m'
|
||||
},
|
||||
prompt: 'Make a question about ${{name}}'
|
||||
},
|
||||
typeName: 'LLMBlock'
|
||||
},
|
||||
id: 'f91ec0f7-03e8-4208-89ac-bd9db46dca8c',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'name',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
value: 'marie curie',
|
||||
registered: false,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'response',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
status: 'READY',
|
||||
started: false,
|
||||
simulated: false
|
||||
}
|
||||
},
|
||||
status: 'READY',
|
||||
waitingSteps: [],
|
||||
executionResult: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '228b8689-e53c-4fcc-8426-fcf9f8aad634',
|
||||
name: 'Test Flow Waiting',
|
||||
creationTime: 1772705840477,
|
||||
context: {
|
||||
inputs: {
|
||||
'f80bce81-f1e4-4e03-9982-d35a042b1276:name': 'marie curie'
|
||||
},
|
||||
result: {},
|
||||
startTime: 1772705842371,
|
||||
endTime: null,
|
||||
errors: {},
|
||||
warnings: {},
|
||||
steps: {
|
||||
'82844256-d9c1-4f81-a415-49b18c371a13': {
|
||||
block: {
|
||||
id: '82844256-d9c1-4f81-a415-49b18c371a13',
|
||||
position: { x: 500, y: 140 },
|
||||
sink: true,
|
||||
name: 'interactive',
|
||||
inputs: [{ name: 'input', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'output', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'HumanInteractiveBlockConfiguration',
|
||||
name: 'interactive',
|
||||
actionDescription: 'Answer the question in input',
|
||||
simulateWith: {
|
||||
provider: 'ollamaTestProvider',
|
||||
model: 'sam860/gemma3:270m'
|
||||
}
|
||||
},
|
||||
typeName: 'HumanInteractionBlock'
|
||||
},
|
||||
id: '82844256-d9c1-4f81-a415-49b18c371a13',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'input',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
value:
|
||||
'Marie Curie is a pioneering scientist who revolutionized our understanding of radioactivity and the development of nuclear medicine. Her work has inspired generations of scientists and continues to be studied and understood.\n',
|
||||
registered: true,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'output',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
connected: false
|
||||
}
|
||||
],
|
||||
status: 'WAITING_FOR_INTERACTION',
|
||||
started: true,
|
||||
simulated: false
|
||||
},
|
||||
'f80bce81-f1e4-4e03-9982-d35a042b1276': {
|
||||
block: {
|
||||
id: 'f80bce81-f1e4-4e03-9982-d35a042b1276',
|
||||
position: { x: 120, y: 140 },
|
||||
sink: false,
|
||||
name: 'first',
|
||||
inputs: [{ name: 'name', type: 'TEXT', multiple: false }],
|
||||
outputs: [{ name: 'response', type: 'TEXT', multiple: false }],
|
||||
specificConfiguration: {
|
||||
type: 'LLMBlockConfiguration',
|
||||
name: 'first',
|
||||
llmDescriptor: {
|
||||
provider: 'ollamaTestProvider',
|
||||
model: 'sam860/gemma3:270m'
|
||||
},
|
||||
prompt: 'Make a question about ${{name}}'
|
||||
},
|
||||
typeName: 'LLMBlock'
|
||||
},
|
||||
id: 'f80bce81-f1e4-4e03-9982-d35a042b1276',
|
||||
inputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'name',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
value: 'marie curie',
|
||||
registered: false,
|
||||
set: true
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
descriptor: {
|
||||
name: 'response',
|
||||
type: 'TEXT',
|
||||
multiple: false
|
||||
},
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
status: 'COMPLETED',
|
||||
started: true,
|
||||
simulated: false
|
||||
}
|
||||
},
|
||||
status: 'WAITING',
|
||||
waitingSteps: ['82844256-d9c1-4f81-a415-49b18c371a13'],
|
||||
executionResult: {}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
override retrieveAllTaskExecutions(): Observable<TaskExecution[]> {
|
||||
return of(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { TaskExecution } from '@models/task-execution';
|
||||
import { Observable } from 'rxjs';
|
||||
import { TaskExecutionsCallServiceBase } from './task-executions-call.base';
|
||||
|
||||
export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase {
|
||||
override retrieveAllTaskExecutions(): Observable<TaskExecution[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { Injectable, signal } from '@angular/core';
|
||||
import { environment } from '@environment';
|
||||
import { TaskExecution } from '@models/task-execution';
|
||||
import { TaskExecutionsCallServiceBase } from './task-executions-call.base';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TaskExecutionsService {
|
||||
taskExecutionsCallService: TaskExecutionsCallServiceBase = new environment.taskExecutionsCallService();
|
||||
private initialized = false;
|
||||
private _taskExecutions = signal<TaskExecution[]>([]);
|
||||
|
||||
taskExecutions = this._taskExecutions.asReadonly();
|
||||
|
||||
init() {
|
||||
if (this.initialized) return;
|
||||
this.initialized = true;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.taskExecutionsCallService.retrieveAllTaskExecutions().subscribe((taskExecutions) => {
|
||||
this._taskExecutions.set(taskExecutions);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5,10 +5,19 @@ import { ClassicPreset } from 'rete';
|
|||
import { ReteModule } from 'rete-angular-plugin/21';
|
||||
import { NodeSettingsDialogService } from '@services/dialogs/node-settings-dialog';
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { FieldRetreiver } from '@services/retreiver/field-retreiver';
|
||||
import { FieldRetreiver as FieldRetriever } from '@services/retreiver/field-retreiver';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { ConditionalRequiredField, extractSchemaRequirements, SchemaRequirements } from '../schema-requirements';
|
||||
import {
|
||||
flattenPrimitiveValues,
|
||||
parentPath,
|
||||
pathToLabel,
|
||||
resolveSchemaRef,
|
||||
splitTemplatedTextParts,
|
||||
toStringOrNull,
|
||||
valueToDisplayString
|
||||
} from '../node-utility';
|
||||
|
||||
type FieldType = 'string' | 'number' | 'integer' | 'boolean' | 'unknown';
|
||||
|
||||
|
|
@ -57,7 +66,7 @@ export class GenericNodeComponent {
|
|||
|
||||
private settingsDialog = inject(NodeSettingsDialogService);
|
||||
private editorState = inject(EditorStateHolder);
|
||||
private fieldRetreiver = inject(FieldRetreiver);
|
||||
private fieldRetriever = inject(FieldRetriever);
|
||||
private blocksService = inject(BlocksService);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
|
|
@ -108,7 +117,7 @@ export class GenericNodeComponent {
|
|||
|
||||
const config = this.ensureBlockConfiguration();
|
||||
|
||||
this.name = this.toStringOrNull(config['name']) || this.name;
|
||||
this.name = toStringOrNull(config['name']) || this.name;
|
||||
|
||||
this.refreshValidationState();
|
||||
this.refreshParameterFields();
|
||||
|
|
@ -236,7 +245,7 @@ export class GenericNodeComponent {
|
|||
mainContentLabel(): string {
|
||||
const contentKey = this.mainContentKey();
|
||||
if (!contentKey) return 'Content';
|
||||
return this.pathToLabel(contentKey);
|
||||
return pathToLabel(contentKey);
|
||||
}
|
||||
|
||||
hasMainContent(): boolean {
|
||||
|
|
@ -246,11 +255,11 @@ export class GenericNodeComponent {
|
|||
mainContentParts(): { text: string; isDynamicInput: boolean }[] {
|
||||
const contentKey = this.mainContentKey();
|
||||
if (!contentKey) return [];
|
||||
const content = this.toStringOrNull(this.getByPath(this.blockConfiguration ?? {}, contentKey));
|
||||
const content = toStringOrNull(this.getByPath(this.blockConfiguration ?? {}, contentKey));
|
||||
if (!content) return [];
|
||||
const ui = this.getFieldUiMeta(contentKey);
|
||||
if (ui.widget === 'textarea' && ui.acceptVariableAsPlaceholder) {
|
||||
return this.splitTemplatedTextParts(content);
|
||||
return splitTemplatedTextParts(content);
|
||||
}
|
||||
return [{ text: content, isDynamicInput: false }];
|
||||
}
|
||||
|
|
@ -279,11 +288,6 @@ export class GenericNodeComponent {
|
|||
return this.data.data.specificConfiguration;
|
||||
}
|
||||
|
||||
private toStringOrNull(value: unknown): string | null {
|
||||
if (typeof value === 'string' && value.trim().length > 0) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
private markFlowDirty() {
|
||||
const flow = this.editorState.currentFlow();
|
||||
if (!flow) return;
|
||||
|
|
@ -341,14 +345,14 @@ export class GenericNodeComponent {
|
|||
const contentKey = this.mainContentKey();
|
||||
|
||||
const walk = (node: Record<string, any>, pathPrefix: string) => {
|
||||
const resolved = this.resolveRef(node, schema);
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
||||
const properties = resolved.properties as Record<string, any> | undefined;
|
||||
if (!properties) return;
|
||||
|
||||
for (const [key, childSchema] of Object.entries(properties)) {
|
||||
const childResolved = this.resolveRef(childSchema as Record<string, any>, schema);
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
|
||||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
|
||||
|
||||
|
|
@ -364,7 +368,7 @@ export class GenericNodeComponent {
|
|||
seen.add(path);
|
||||
definitions.push({
|
||||
path,
|
||||
label: this.pathToLabel(path),
|
||||
label: pathToLabel(path),
|
||||
type: this.toFieldType(childResolved?.type),
|
||||
retrieverKey: this.toRetrieverKey(childResolved),
|
||||
retrieverDependsOn: this.toRetrieverDependsOn(childResolved, pathPrefix),
|
||||
|
|
@ -415,10 +419,10 @@ export class GenericNodeComponent {
|
|||
let current: Record<string, any> | null = root;
|
||||
for (const segment of path.split('.')) {
|
||||
if (!current) return null;
|
||||
const resolved = this.resolveRef(current, root);
|
||||
const resolved = resolveSchemaRef(current, root);
|
||||
const properties = resolved?.properties as Record<string, unknown> | undefined;
|
||||
if (!properties || !properties[segment]) return null;
|
||||
current = this.resolveRef(properties[segment] as Record<string, any>, root);
|
||||
current = resolveSchemaRef(properties[segment] as Record<string, any>, root);
|
||||
}
|
||||
|
||||
return current;
|
||||
|
|
@ -468,7 +472,7 @@ export class GenericNodeComponent {
|
|||
|
||||
try {
|
||||
const options = await firstValueFrom(
|
||||
this.fieldRetreiver.retrieveValues(blockType, definition.retrieverKey, context)
|
||||
this.fieldRetriever.retrieveValues(blockType, definition.retrieverKey, context)
|
||||
);
|
||||
this.localEditorOptions = options ?? [];
|
||||
} catch {
|
||||
|
|
@ -489,25 +493,25 @@ export class GenericNodeComponent {
|
|||
return {
|
||||
path: definition.path,
|
||||
label: definition.label,
|
||||
value: this.valueToDisplayString(value)
|
||||
value: valueToDisplayString(value)
|
||||
};
|
||||
});
|
||||
|
||||
for (const field of orderedFields) {
|
||||
const parentPath = this.parentPath(field.path);
|
||||
if (!parentPath) {
|
||||
const parentKey = parentPath(field.path);
|
||||
if (!parentKey) {
|
||||
rootFields.push(field);
|
||||
continue;
|
||||
}
|
||||
if (!grouped.has(parentPath)) {
|
||||
grouped.set(parentPath, []);
|
||||
if (!grouped.has(parentKey)) {
|
||||
grouped.set(parentKey, []);
|
||||
}
|
||||
grouped.get(parentPath)!.push(field);
|
||||
grouped.get(parentKey)!.push(field);
|
||||
}
|
||||
this.parameterFields = rootFields;
|
||||
this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({
|
||||
key,
|
||||
legend: this.pathToLabel(key),
|
||||
legend: pathToLabel(key),
|
||||
fields
|
||||
}));
|
||||
this.refreshView();
|
||||
|
|
@ -515,68 +519,36 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
const contentKey = this.mainContentKey();
|
||||
const fallbackFields = this.flattenPrimitiveValues(config)
|
||||
const fallbackFields = flattenPrimitiveValues(config)
|
||||
.filter((entry) => entry.path !== 'name' && entry.path !== 'type' && entry.path !== contentKey)
|
||||
.map((entry) => ({
|
||||
path: entry.path,
|
||||
label: this.pathToLabel(entry.path),
|
||||
value: this.valueToDisplayString(entry.value)
|
||||
label: pathToLabel(entry.path),
|
||||
value: valueToDisplayString(entry.value)
|
||||
}));
|
||||
|
||||
for (const field of fallbackFields) {
|
||||
const parentPath = this.parentPath(field.path);
|
||||
if (!parentPath) {
|
||||
const parentKey = parentPath(field.path);
|
||||
if (!parentKey) {
|
||||
rootFields.push(field);
|
||||
continue;
|
||||
}
|
||||
if (!grouped.has(parentPath)) {
|
||||
grouped.set(parentPath, []);
|
||||
if (!grouped.has(parentKey)) {
|
||||
grouped.set(parentKey, []);
|
||||
}
|
||||
grouped.get(parentPath)!.push(field);
|
||||
grouped.get(parentKey)!.push(field);
|
||||
}
|
||||
|
||||
this.parameterFields = rootFields;
|
||||
this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({
|
||||
key,
|
||||
legend: this.pathToLabel(key),
|
||||
legend: pathToLabel(key),
|
||||
fields
|
||||
}));
|
||||
|
||||
this.refreshView();
|
||||
}
|
||||
|
||||
private parentPath(path: string): string | null {
|
||||
const index = path.lastIndexOf('.');
|
||||
if (index <= 0) return null;
|
||||
return path.slice(0, index);
|
||||
}
|
||||
|
||||
private flattenPrimitiveValues(source: Record<string, any>, prefix = ''): Array<{ path: string; value: unknown }> {
|
||||
const entries: Array<{ path: string; value: unknown }> = [];
|
||||
for (const [key, value] of Object.entries(source ?? {})) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (key.startsWith('__')) continue;
|
||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
entries.push(...this.flattenPrimitiveValues(value as Record<string, any>, path));
|
||||
} else {
|
||||
entries.push({ path, value });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private valueToDisplayString(value: unknown): string {
|
||||
if (value == null) return '-';
|
||||
if (typeof value === 'string') return value.trim().length ? value : '-';
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
if (Array.isArray(value)) return value.length ? JSON.stringify(value) : '-';
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
private valueToEditorString(value: unknown, type: FieldType): string {
|
||||
if (type === 'boolean') {
|
||||
return value === true ? 'true' : 'false';
|
||||
|
|
@ -633,14 +605,6 @@ export class GenericNodeComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
private pathToLabel(path: string): string {
|
||||
const lastSegment = path.split('.').at(-1) ?? path;
|
||||
return lastSegment
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/^./, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
private getByPath(source: Record<string, any>, path: string): unknown {
|
||||
return path.split('.').reduce<unknown>((acc, key) => {
|
||||
if (acc == null || typeof acc !== 'object') return undefined;
|
||||
|
|
@ -704,60 +668,13 @@ export class GenericNodeComponent {
|
|||
|
||||
try {
|
||||
return await firstValueFrom(
|
||||
this.fieldRetreiver.isFieldRequired(blockType, field.retrieverKey, context)
|
||||
this.fieldRetriever.isFieldRequired(blockType, field.retrieverKey, context)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private resolveRef(node: Record<string, any>, root: Record<string, any>) {
|
||||
if (!node || typeof node !== 'object') return node;
|
||||
const ref = node['$ref'];
|
||||
if (typeof ref !== 'string' || !ref.startsWith('#/')) return node;
|
||||
|
||||
const path = ref.slice(2).split('/');
|
||||
let current: any = root;
|
||||
for (const segment of path) {
|
||||
current = current?.[segment];
|
||||
if (current == null) return node;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private splitTemplatedTextParts(prompt: string | null): { text: string; isDynamicInput: boolean }[] {
|
||||
if (!prompt) return [];
|
||||
|
||||
const parts: { text: string; isDynamicInput: boolean }[] = [];
|
||||
const re = /\$\{\{[^}]+\}\}/g;
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = re.exec(prompt)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({
|
||||
text: prompt.slice(lastIndex, match.index),
|
||||
isDynamicInput: false
|
||||
});
|
||||
}
|
||||
|
||||
parts.push({
|
||||
text: match[0],
|
||||
isDynamicInput: true
|
||||
});
|
||||
lastIndex = re.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < prompt.length) {
|
||||
parts.push({
|
||||
text: prompt.slice(lastIndex),
|
||||
isDynamicInput: false
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private refreshView() {
|
||||
queueMicrotask(() => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
flattenPrimitiveValues,
|
||||
parentPath,
|
||||
pathToLabel,
|
||||
resolveSchemaRef,
|
||||
splitTemplatedTextParts,
|
||||
toStringOrNull,
|
||||
valueToDisplayString
|
||||
} from './node-utility';
|
||||
|
||||
describe('node-utility', () => {
|
||||
it('toStringOrNull returns trimmed non-empty strings and null otherwise', () => {
|
||||
expect(toStringOrNull('hello')).toBe('hello');
|
||||
expect(toStringOrNull(' ')).toBeNull();
|
||||
expect(toStringOrNull(10)).toBeNull();
|
||||
});
|
||||
|
||||
it('flattenPrimitiveValues flattens nested objects and skips internal keys', () => {
|
||||
const result = flattenPrimitiveValues({
|
||||
name: 'Node',
|
||||
nested: { value: 5, __internal: 'skip' },
|
||||
__meta: 'skip'
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ path: 'name', value: 'Node' },
|
||||
{ path: 'nested.value', value: 5 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('valueToDisplayString formats primitive and empty values', () => {
|
||||
expect(valueToDisplayString(null)).toBe('-');
|
||||
expect(valueToDisplayString('')).toBe('-');
|
||||
expect(valueToDisplayString('ok')).toBe('ok');
|
||||
expect(valueToDisplayString(1)).toBe('1');
|
||||
expect(valueToDisplayString(false)).toBe('false');
|
||||
expect(valueToDisplayString([])).toBe('-');
|
||||
});
|
||||
|
||||
it('pathToLabel builds readable labels', () => {
|
||||
expect(pathToLabel('message.actionDescription')).toBe('Action Description');
|
||||
expect(pathToLabel('step_name')).toBe('Step name');
|
||||
});
|
||||
|
||||
it('parentPath returns parent path for nested keys', () => {
|
||||
expect(parentPath('a.b.c')).toBe('a.b');
|
||||
expect(parentPath('root')).toBeNull();
|
||||
});
|
||||
|
||||
it('resolveSchemaRef resolves local schema refs and falls back to input node', () => {
|
||||
const root = {
|
||||
definitions: {
|
||||
Message: { type: 'string' }
|
||||
}
|
||||
};
|
||||
const refNode = { $ref: '#/definitions/Message' };
|
||||
|
||||
expect(resolveSchemaRef(refNode, root)).toEqual({ type: 'string' });
|
||||
expect(resolveSchemaRef({ $ref: '#/definitions/Missing' }, root)).toEqual({ $ref: '#/definitions/Missing' });
|
||||
});
|
||||
|
||||
it('splitTemplatedTextParts identifies dynamic placeholders', () => {
|
||||
const parts = splitTemplatedTextParts('Hello ${{ user.name }}!');
|
||||
expect(parts).toEqual([
|
||||
{ text: 'Hello ', isDynamicInput: false },
|
||||
{ text: '${{ user.name }}', isDynamicInput: true },
|
||||
{ text: '!', isDynamicInput: false }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
export type TemplatedTextPart = { text: string; isDynamicInput: boolean };
|
||||
|
||||
export function toStringOrNull(value: unknown): string | null {
|
||||
if (typeof value === 'string' && value.trim().length > 0) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function flattenPrimitiveValues(
|
||||
source: Record<string, any>,
|
||||
prefix = ''
|
||||
): Array<{ path: string; value: unknown }> {
|
||||
const entries: Array<{ path: string; value: unknown }> = [];
|
||||
for (const [key, value] of Object.entries(source ?? {})) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (key.startsWith('__')) continue;
|
||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
entries.push(...flattenPrimitiveValues(value as Record<string, any>, path));
|
||||
} else {
|
||||
entries.push({ path, value });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function valueToDisplayString(value: unknown): string {
|
||||
if (value == null) return '-';
|
||||
if (typeof value === 'string') return value.trim().length ? value : '-';
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
if (Array.isArray(value)) return value.length ? JSON.stringify(value) : '-';
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function pathToLabel(path: string): string {
|
||||
const lastSegment = path.split('.').at(-1) ?? path;
|
||||
return lastSegment
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/^./, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function parentPath(path: string): string | null {
|
||||
const index = path.lastIndexOf('.');
|
||||
if (index <= 0) return null;
|
||||
return path.slice(0, index);
|
||||
}
|
||||
|
||||
export function resolveSchemaRef(node: Record<string, any>, root: Record<string, any>) {
|
||||
if (!node || typeof node !== 'object') return node;
|
||||
const ref = node['$ref'];
|
||||
if (typeof ref !== 'string' || !ref.startsWith('#/')) return node;
|
||||
|
||||
const path = ref.slice(2).split('/');
|
||||
let current: any = root;
|
||||
for (const segment of path) {
|
||||
current = current?.[segment];
|
||||
if (current == null) return node;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
export function splitTemplatedTextParts(text: string | null): TemplatedTextPart[] {
|
||||
if (!text) return [];
|
||||
|
||||
const parts: TemplatedTextPart[] = [];
|
||||
const re = /\$\{\{[^}]+\}\}/g;
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({
|
||||
text: text.slice(lastIndex, match.index),
|
||||
isDynamicInput: false
|
||||
});
|
||||
}
|
||||
|
||||
parts.push({
|
||||
text: match[0],
|
||||
isDynamicInput: true
|
||||
});
|
||||
lastIndex = re.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
parts.push({
|
||||
text: text.slice(lastIndex),
|
||||
isDynamicInput: false
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { resolveSchemaRef } from './node-utility';
|
||||
|
||||
export type RequiredField = {
|
||||
path: string;
|
||||
label: string;
|
||||
|
|
@ -37,7 +39,7 @@ function walkSchema(
|
|||
seenRequired: Set<string>,
|
||||
seenConditional: Set<string>
|
||||
) {
|
||||
const resolved = resolveRef(node, root);
|
||||
const resolved = resolveSchemaRef(node, root);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
||||
const properties = resolved.properties as Record<string, any> | undefined;
|
||||
|
|
@ -47,7 +49,7 @@ function walkSchema(
|
|||
|
||||
for (const [key, propertySchema] of Object.entries(properties)) {
|
||||
const propertyPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
const propertyResolved = resolveRef(propertySchema as Record<string, any>, root);
|
||||
const propertyResolved = resolveSchemaRef(propertySchema as Record<string, any>, root);
|
||||
const hasChildren = !!propertyResolved?.properties || propertyResolved?.type === 'object';
|
||||
const label = toLabel(key);
|
||||
|
||||
|
|
@ -88,20 +90,6 @@ function walkSchema(
|
|||
}
|
||||
}
|
||||
|
||||
function resolveRef(node: Record<string, any>, root: Record<string, any>) {
|
||||
if (!node || typeof node !== 'object') return node;
|
||||
const ref = node['$ref'];
|
||||
if (typeof ref !== 'string' || !ref.startsWith('#/')) return node;
|
||||
|
||||
const path = ref.slice(2).split('/');
|
||||
let current: any = root;
|
||||
for (const segment of path) {
|
||||
current = current?.[segment];
|
||||
if (current == null) return node;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function toLabel(key: string): string {
|
||||
return key
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@
|
|||
box-shadow: 0 0 0 3px rgba(234, 179, 8, 0.22), 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.llm-node-completed {
|
||||
border: 2px solid #16a34a;
|
||||
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.2), 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.llm-node-attention {
|
||||
border: 2px solid #f97316;
|
||||
animation: llmAttentionBorderPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.llm-error-alert-wrap,
|
||||
.llm-warning-alert-wrap {
|
||||
position: absolute;
|
||||
|
|
@ -35,6 +45,33 @@
|
|||
z-index: 120;
|
||||
}
|
||||
|
||||
.llm-attention-wrap {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -14px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 125;
|
||||
}
|
||||
|
||||
.llm-attention-trigger {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid #fdba74;
|
||||
border-radius: 999px;
|
||||
background: #f97316;
|
||||
color: #fff7ed;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.45);
|
||||
animation: llmAttentionIconPulse 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.llm-attention-trigger i {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.llm-error-alert-wrap {
|
||||
right: -12px;
|
||||
}
|
||||
|
|
@ -226,6 +263,19 @@
|
|||
justify-self: end;
|
||||
}
|
||||
|
||||
.llm-socket-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.llm-socket-wrap-output {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.llm-pill {
|
||||
height: 24px;
|
||||
border-radius: 7px;
|
||||
|
|
@ -236,6 +286,15 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.llm-pill-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.llm-pill-input {
|
||||
|
|
@ -254,28 +313,69 @@
|
|||
border-color: #fecdd3;
|
||||
}
|
||||
|
||||
.llm-input-info-wrap {
|
||||
.llm-input-value-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.llm-input-info {
|
||||
.llm-input-value-icon {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #166534;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: #68ae82;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
cursor: help;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.llm-input-tooltip {
|
||||
.llm-input-value-icon:hover {
|
||||
background: rgba(104, 174, 130, 0.22);
|
||||
}
|
||||
|
||||
.llm-input-value-icon i,
|
||||
.llm-output-value-icon i {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.llm-output-value-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.llm-output-value-icon {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #be123c;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
cursor: help;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.llm-output-value-icon:hover {
|
||||
background: rgba(225, 29, 72, 0.12);
|
||||
}
|
||||
|
||||
.llm-io-tooltip {
|
||||
position: absolute;
|
||||
left: calc(100% + 6px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
min-width: 140px;
|
||||
|
|
@ -296,12 +396,108 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.llm-input-info-wrap:hover .llm-input-tooltip,
|
||||
.llm-input-info-wrap:focus-within .llm-input-tooltip {
|
||||
.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,
|
||||
.llm-socket-wrap:focus-within .llm-io-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.llm-modal-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 16px;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
z-index: 180;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.llm-modal {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.28);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.llm-modal-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.llm-modal-content {
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.llm-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.llm-btn {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.llm-btn-primary {
|
||||
background: #2563eb;
|
||||
color: #eff6ff;
|
||||
}
|
||||
|
||||
@keyframes llmAttentionBorderPulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.38), 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.14), 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.05), 0 10px 24px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes llmAttentionIconPulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.45);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 0 0 7px rgba(249, 115, 22, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(249, 115, 22, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.llm-params {
|
||||
margin: 0 12px 12px;
|
||||
padding: 10px;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
<div class="llm-node"
|
||||
[class.llm-node--human]="isHumanNode()"
|
||||
[class.llm-node-completed]="isCompleted() && !needsAttention() && !hasExecutionErrors() && !hasExecutionWarnings()"
|
||||
[class.llm-node-attention]="needsAttention()"
|
||||
[class.llm-node-error]="hasExecutionErrors()"
|
||||
[class.llm-node-warning]="hasExecutionWarnings() && !hasExecutionErrors()">
|
||||
@if (needsAttention()) {
|
||||
<div class="llm-attention-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
|
||||
<button
|
||||
type="button"
|
||||
class="llm-attention-trigger"
|
||||
aria-label="Open waiting step details"
|
||||
(click)="openInteractionModal($event)">
|
||||
<i class="bi bi-hand-index-thumb-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (hasExecutionErrors()) {
|
||||
<div class="llm-error-alert-wrap">
|
||||
<div class="llm-error-alert">
|
||||
|
|
@ -50,32 +63,38 @@
|
|||
<div class="llm-column-title">Inputs</div>
|
||||
@for (input of inputs; track input.key) {
|
||||
<div class="llm-row llm-row-input">
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'input',
|
||||
key: input.key,
|
||||
nodeId: data.id,
|
||||
payload: input.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="llm-pill llm-pill-input">
|
||||
{{ input.key }}
|
||||
@if (isInputConnected(input.key)) {
|
||||
<span class="llm-socket-wrap llm-socket-wrap-input">
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'input',
|
||||
key: input.key,
|
||||
nodeId: data.id,
|
||||
payload: input.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
@if (executionInputTooltip(input.key); as inputTooltip) {
|
||||
<span class="llm-input-info-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
|
||||
<button
|
||||
type="button"
|
||||
class="llm-input-info"
|
||||
aria-label="Show input value">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</button>
|
||||
<span class="llm-input-tooltip">{{ inputTooltip }}</span>
|
||||
</span>
|
||||
<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">
|
||||
<i class="bi bi-box-arrow-in-right"></i>
|
||||
</button>
|
||||
<span class="llm-io-tooltip llm-io-tooltip-input">{{ inputValueTooltip(input.key) }}</span>
|
||||
</span>
|
||||
}
|
||||
<span class="llm-pill llm-pill-input">
|
||||
<span class="llm-pill-label">{{ input.key }}</span>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
@ -84,19 +103,38 @@
|
|||
<div class="llm-column-title llm-column-title-right">Outputs</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>
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket llm-socket-right"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'output',
|
||||
key: output.key,
|
||||
nodeId: data.id,
|
||||
payload: output.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
<span class="llm-pill llm-pill-output">
|
||||
<span class="llm-pill-label">{{ output.key }}</span>
|
||||
</span>
|
||||
@if (isOutputConnected(output.key)) {
|
||||
<span class="llm-socket-wrap llm-socket-wrap-output">
|
||||
<div
|
||||
refComponent
|
||||
class="llm-socket llm-socket-right"
|
||||
[data]="{
|
||||
type: 'socket',
|
||||
side: 'output',
|
||||
key: output.key,
|
||||
nodeId: data.id,
|
||||
payload: output.socket
|
||||
}"
|
||||
[emit]="emit">
|
||||
</div>
|
||||
@if (executionOutputTooltip(output.key); as outputTooltip) {
|
||||
<span class="llm-io-tooltip llm-io-tooltip-output">{{ outputTooltip }}</span>
|
||||
}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="llm-output-value-wrap" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
|
||||
<button
|
||||
type="button"
|
||||
class="llm-output-value-icon"
|
||||
aria-label="Show output result">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
</button>
|
||||
<span class="llm-io-tooltip llm-io-tooltip-output">{{ outputValueTooltip(output.key) }}</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
@ -153,4 +191,19 @@
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (interactionModalOpen) {
|
||||
<div class="llm-modal-backdrop" (pointerdown)="$event.stopPropagation()" (click)="closeInteractionModal($event)">
|
||||
<div class="llm-modal" (pointerdown)="$event.stopPropagation()" (click)="$event.stopPropagation()">
|
||||
<div class="llm-modal-title">Step Waiting</div>
|
||||
<div class="llm-modal-content">
|
||||
<div><strong>Node:</strong> {{ name }}</div>
|
||||
<div><strong>Status:</strong> {{ stepStatus() || 'UNKNOWN' }}</div>
|
||||
</div>
|
||||
<div class="llm-modal-actions">
|
||||
<button type="button" class="llm-btn llm-btn-primary" (click)="closeInteractionModal($event)">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,15 @@ import { ChangeDetectorRef, Component, HostBinding, Input, inject } from '@angul
|
|||
import { ClassicPreset } from 'rete';
|
||||
import { ReteModule } from 'rete-angular-plugin/21';
|
||||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import {
|
||||
flattenPrimitiveValues,
|
||||
parentPath,
|
||||
pathToLabel,
|
||||
resolveSchemaRef,
|
||||
splitTemplatedTextParts,
|
||||
toStringOrNull,
|
||||
valueToDisplayString
|
||||
} from '../node-utility';
|
||||
|
||||
type DisplayField = {
|
||||
path: string;
|
||||
|
|
@ -50,6 +59,7 @@ export class TaskStepNodeComponent {
|
|||
|
||||
name = 'Step';
|
||||
mainContent: MainContentView | null = null;
|
||||
interactionModalOpen = false;
|
||||
|
||||
private blockSchema: Record<string, any> | null = null;
|
||||
private variablePlaceholderPaths = new Set<string>();
|
||||
|
|
@ -74,14 +84,14 @@ export class TaskStepNodeComponent {
|
|||
|
||||
private rebuildDisplayState() {
|
||||
const config = this.blockConfiguration ?? {};
|
||||
this.name = this.toStringOrNull(config['name']) ?? this.name;
|
||||
const primitiveEntries = this.flattenPrimitiveValues(config);
|
||||
this.name = toStringOrNull(config['name']) ?? this.name;
|
||||
const primitiveEntries = flattenPrimitiveValues(config);
|
||||
const contentEntry = this.pickMainContentEntry(primitiveEntries);
|
||||
|
||||
this.mainContent = contentEntry
|
||||
? {
|
||||
path: contentEntry.path,
|
||||
label: this.pathToLabel(contentEntry.path),
|
||||
label: pathToLabel(contentEntry.path),
|
||||
parts: this.toMainContentParts(contentEntry.path, String(contentEntry.value))
|
||||
}
|
||||
: null;
|
||||
|
|
@ -93,26 +103,26 @@ export class TaskStepNodeComponent {
|
|||
.filter((entry) => entry.path !== contentEntry?.path)
|
||||
.map((entry) => ({
|
||||
path: entry.path,
|
||||
label: this.pathToLabel(entry.path),
|
||||
value: this.valueToDisplayString(entry.value)
|
||||
label: pathToLabel(entry.path),
|
||||
value: valueToDisplayString(entry.value)
|
||||
}));
|
||||
|
||||
for (const field of orderedFields) {
|
||||
const parentPath = this.parentPath(field.path);
|
||||
if (!parentPath) {
|
||||
const parentKey = parentPath(field.path);
|
||||
if (!parentKey) {
|
||||
rootFields.push(field);
|
||||
continue;
|
||||
}
|
||||
if (!grouped.has(parentPath)) {
|
||||
grouped.set(parentPath, []);
|
||||
if (!grouped.has(parentKey)) {
|
||||
grouped.set(parentKey, []);
|
||||
}
|
||||
grouped.get(parentPath)!.push(field);
|
||||
grouped.get(parentKey)!.push(field);
|
||||
}
|
||||
|
||||
this.parameterFields = rootFields;
|
||||
this.parameterFieldGroups = Array.from(grouped.entries()).map(([key, fields]) => ({
|
||||
key,
|
||||
legend: this.pathToLabel(key),
|
||||
legend: pathToLabel(key),
|
||||
fields
|
||||
}));
|
||||
|
||||
|
|
@ -158,6 +168,31 @@ export class TaskStepNodeComponent {
|
|||
if (!values || !Object.prototype.hasOwnProperty.call(values, inputName)) return null;
|
||||
|
||||
const value = values[inputName];
|
||||
if (value == null) return 'not ready yet';
|
||||
if (typeof value === 'string') return value.trim().length > 0 ? value : null;
|
||||
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
isInputConnected(inputName: string): boolean {
|
||||
const connectedInputs = this.blockConfiguration?.['__connectedInputs'];
|
||||
if (!Array.isArray(connectedInputs)) return true;
|
||||
return connectedInputs.includes(inputName);
|
||||
}
|
||||
|
||||
inputValueTooltip(inputName: string): string {
|
||||
return this.executionInputTooltip(inputName) ?? 'not ready yet';
|
||||
}
|
||||
|
||||
executionOutputTooltip(outputName: string): string | null {
|
||||
const values = this.blockConfiguration?.['__executionOutputs'] as Record<string, unknown> | undefined;
|
||||
if (!values || !Object.prototype.hasOwnProperty.call(values, outputName)) return null;
|
||||
|
||||
const value = values[outputName];
|
||||
if (value === undefined) return null;
|
||||
if (typeof value === 'string') return value.trim().length > 0 ? value : null;
|
||||
|
||||
|
|
@ -168,6 +203,16 @@ export class TaskStepNodeComponent {
|
|||
}
|
||||
}
|
||||
|
||||
isOutputConnected(outputName: string): boolean {
|
||||
const connectedOutputs = this.blockConfiguration?.['__connectedOutputs'];
|
||||
if (!Array.isArray(connectedOutputs)) return true;
|
||||
return connectedOutputs.includes(outputName);
|
||||
}
|
||||
|
||||
outputValueTooltip(outputName: string): string {
|
||||
return this.executionOutputTooltip(outputName) ?? 'No output result';
|
||||
}
|
||||
|
||||
executionErrors(): string[] {
|
||||
return this.getExecutionMessages('__executionErrors');
|
||||
}
|
||||
|
|
@ -184,6 +229,31 @@ export class TaskStepNodeComponent {
|
|||
return this.executionWarnings().length > 0;
|
||||
}
|
||||
|
||||
needsAttention(): boolean {
|
||||
return this.blockConfiguration?.['__isWaitingStep'] === true;
|
||||
}
|
||||
|
||||
isCompleted(): boolean {
|
||||
return this.stepStatus() === 'COMPLETED';
|
||||
}
|
||||
|
||||
stepStatus(): string {
|
||||
const status = this.blockConfiguration?.['__stepStatus'];
|
||||
return typeof status === 'string' ? status.toUpperCase() : '';
|
||||
}
|
||||
|
||||
openInteractionModal(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
this.interactionModalOpen = true;
|
||||
}
|
||||
|
||||
closeInteractionModal(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
this.interactionModalOpen = false;
|
||||
}
|
||||
|
||||
private get blockConfiguration(): Record<string, any> | null {
|
||||
return this.data?.data?.specificConfiguration ?? null;
|
||||
}
|
||||
|
|
@ -193,25 +263,6 @@ export class TaskStepNodeComponent {
|
|||
return typeof typeName === 'string' && typeName.length > 0 ? typeName : null;
|
||||
}
|
||||
|
||||
private toStringOrNull(value: unknown): string | null {
|
||||
if (typeof value === 'string' && value.trim().length > 0) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
private flattenPrimitiveValues(source: Record<string, any>, prefix = ''): Array<{ path: string; value: unknown }> {
|
||||
const entries: Array<{ path: string; value: unknown }> = [];
|
||||
for (const [key, value] of Object.entries(source ?? {})) {
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (key.startsWith('__')) continue;
|
||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
entries.push(...this.flattenPrimitiveValues(value as Record<string, any>, path));
|
||||
} else {
|
||||
entries.push({ path, value });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private pickMainContentEntry(entries: Array<{ path: string; value: unknown }>) {
|
||||
const candidates = entries
|
||||
.filter((entry) => !['name', 'type'].includes(entry.path))
|
||||
|
|
@ -235,32 +286,6 @@ export class TaskStepNodeComponent {
|
|||
return { path: chosen.path, value: chosen.text };
|
||||
}
|
||||
|
||||
private valueToDisplayString(value: unknown): string {
|
||||
if (value == null) return '-';
|
||||
if (typeof value === 'string') return value.trim().length ? value : '-';
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
if (Array.isArray(value)) return value.length ? JSON.stringify(value) : '-';
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
private pathToLabel(path: string): string {
|
||||
const lastSegment = path.split('.').at(-1) ?? path;
|
||||
return lastSegment
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/^./, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
private parentPath(path: string): string | null {
|
||||
const index = path.lastIndexOf('.');
|
||||
if (index <= 0) return null;
|
||||
return path.slice(0, index);
|
||||
}
|
||||
|
||||
private getExecutionMessages(key: '__executionErrors' | '__executionWarnings'): string[] {
|
||||
const values = this.blockConfiguration?.[key];
|
||||
if (!Array.isArray(values)) return [];
|
||||
|
|
@ -269,44 +294,11 @@ export class TaskStepNodeComponent {
|
|||
|
||||
private toMainContentParts(path: string, value: string): { text: string; isDynamicInput: boolean }[] {
|
||||
if (this.variablePlaceholderPaths.has(path)) {
|
||||
return this.splitTemplatedTextParts(value);
|
||||
return splitTemplatedTextParts(value);
|
||||
}
|
||||
return [{ text: value, isDynamicInput: false }];
|
||||
}
|
||||
|
||||
private splitTemplatedTextParts(text: string | null): { text: string; isDynamicInput: boolean }[] {
|
||||
if (!text) return [];
|
||||
|
||||
const parts: { text: string; isDynamicInput: boolean }[] = [];
|
||||
const re = /\$\{\{[^}]+\}\}/g;
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = re.exec(text)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
parts.push({
|
||||
text: text.slice(lastIndex, match.index),
|
||||
isDynamicInput: false
|
||||
});
|
||||
}
|
||||
|
||||
parts.push({
|
||||
text: match[0],
|
||||
isDynamicInput: true
|
||||
});
|
||||
lastIndex = re.lastIndex;
|
||||
}
|
||||
|
||||
if (lastIndex < text.length) {
|
||||
parts.push({
|
||||
text: text.slice(lastIndex),
|
||||
isDynamicInput: false
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
private async loadSchemaContext() {
|
||||
const type = this.blockType;
|
||||
if (!type) return;
|
||||
|
|
@ -322,14 +314,14 @@ export class TaskStepNodeComponent {
|
|||
if (!schema) return paths;
|
||||
|
||||
const walk = (node: Record<string, any>, pathPrefix: string) => {
|
||||
const resolved = this.resolveRef(node, schema);
|
||||
const resolved = resolveSchemaRef(node, schema);
|
||||
if (!resolved || typeof resolved !== 'object') return;
|
||||
|
||||
const properties = resolved.properties as Record<string, any> | undefined;
|
||||
if (!properties) return;
|
||||
|
||||
for (const [key, childSchema] of Object.entries(properties)) {
|
||||
const childResolved = this.resolveRef(childSchema as Record<string, any>, schema);
|
||||
const childResolved = resolveSchemaRef(childSchema as Record<string, any>, schema);
|
||||
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
||||
const hasChildren = !!childResolved?.properties || childResolved?.type === 'object';
|
||||
if (hasChildren) {
|
||||
|
|
@ -352,20 +344,6 @@ export class TaskStepNodeComponent {
|
|||
return paths;
|
||||
}
|
||||
|
||||
private resolveRef(node: Record<string, any>, root: Record<string, any>) {
|
||||
if (!node || typeof node !== 'object') return node;
|
||||
const ref = node['$ref'];
|
||||
if (typeof ref !== 'string' || !ref.startsWith('#/')) return node;
|
||||
|
||||
const path = ref.slice(2).split('/');
|
||||
let current: any = root;
|
||||
for (const segment of path) {
|
||||
current = current?.[segment];
|
||||
if (current == null) return node;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private refreshView() {
|
||||
queueMicrotask(() => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
<section class="h-full p-4">
|
||||
<section class="h-full p-1">
|
||||
@if (execution()) {
|
||||
<div class="h-full bg-white border border-slate-200 rounded-md flex flex-col overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 bg-slate-50">
|
||||
<div class="px-5 pt-2 border-b border-slate-200 bg-slate-50">
|
||||
<h2 class="text-base font-semibold text-slate-900">{{ execution()!.name }}</h2>
|
||||
<p class="text-sm text-slate-500">Execution ID: {{ execution()!.id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 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-3">
|
||||
<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-3">
|
||||
<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-3">
|
||||
<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-3">
|
||||
<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">{{ durationMs() != null ? durationMs() + ' ms' : '-' }}</div>
|
||||
</div>
|
||||
<div class="bg-slate-50 border border-slate-200 rounded-md p-3">
|
||||
<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>
|
||||
|
||||
<div class="flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4 p-4">
|
||||
<div class="flex-1 min-h-0 grid grid-cols-1 lg:grid-cols-3 gap-4 p-1">
|
||||
<div class="lg:col-span-2 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="h-[calc(100%-33px)] execution-rete-readonly">
|
||||
|
|
|
|||
|
|
@ -19,14 +19,24 @@ export class TaskExecutionViewerComponent {
|
|||
|
||||
readonly executionFlowData = computed<FlowData>(() => {
|
||||
const contextInputs = this.execution()?.context.inputs ?? {};
|
||||
const contextResults = {
|
||||
...(this.execution()?.context.result ?? {}),
|
||||
...(this.execution()?.context.executionResult ?? {})
|
||||
};
|
||||
const contextErrors = this.execution()?.context.errors ?? {};
|
||||
const contextWarnings = this.execution()?.context.warnings ?? {};
|
||||
const waitingSteps = this.execution()?.context.waitingSteps ?? [];
|
||||
const steps = this.stepsArray();
|
||||
const blocks = steps.map((step, index) => ({
|
||||
...step.block,
|
||||
specificConfiguration: {
|
||||
...(step.block.specificConfiguration ?? {}),
|
||||
__stepStatus: step.status,
|
||||
__isWaitingStep: waitingSteps.includes(step.id),
|
||||
__executionInputs: this.getExecutionInputValues(step, contextInputs),
|
||||
__connectedInputs: this.getConnectedInputs(step),
|
||||
__executionOutputs: this.getExecutionOutputValues(step, contextResults),
|
||||
__connectedOutputs: this.getConnectedOutputs(step),
|
||||
__executionErrors: this.getExecutionErrors(step.id, contextErrors),
|
||||
__executionWarnings: this.getExecutionWarnings(step.id, contextWarnings)
|
||||
},
|
||||
|
|
@ -107,6 +117,39 @@ export class TaskExecutionViewerComponent {
|
|||
return result;
|
||||
}
|
||||
|
||||
private getConnectedInputs(step: TaskExecutionStep): string[] {
|
||||
return (step.inputs ?? [])
|
||||
.filter((input) => input.registered)
|
||||
.map((input) => input.descriptor?.name)
|
||||
.filter((name): name is string => typeof name === 'string' && name.length > 0);
|
||||
}
|
||||
|
||||
private getExecutionOutputValues(
|
||||
step: TaskExecutionStep,
|
||||
contextResults: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const output of step.outputs ?? []) {
|
||||
const outputName = output.descriptor?.name;
|
||||
if (!outputName) continue;
|
||||
|
||||
const key = `${step.id}:${outputName}`;
|
||||
if (Object.prototype.hasOwnProperty.call(contextResults, key)) {
|
||||
result[outputName] = contextResults[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getConnectedOutputs(step: TaskExecutionStep): string[] {
|
||||
return (step.outputs ?? [])
|
||||
.filter((output) => output.connected)
|
||||
.map((output) => output.descriptor?.name)
|
||||
.filter((name): name is string => typeof name === 'string' && name.length > 0);
|
||||
}
|
||||
|
||||
private getExecutionErrors(
|
||||
stepId: string,
|
||||
contextErrors: Record<string, unknown>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { AuthorizationCallFakeService } from "@services/authorization/authorizat
|
|||
import { BlocksCallServiceFake } from "@services/blocks/blocks-call.fake";
|
||||
import { FlowsCallServiceFake } from "@services/flows/flows-call.fake";
|
||||
import { FieldRetreiverCallServiceFake } from "@services/retreiver/field-retreiver-call.fake";
|
||||
import { TaskExecutionsCallServiceFake } from "@services/task-executions/task-executions-call.fake";
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
|
|
@ -9,5 +10,6 @@ export const environment = {
|
|||
authorizationCallService: AuthorizationCallFakeService, // Assign the appropriate service here
|
||||
flowsCallService: FlowsCallServiceFake,
|
||||
blocksCallService: BlocksCallServiceFake,
|
||||
fieldRetreiverCallService: FieldRetreiverCallServiceFake
|
||||
fieldRetreiverCallService: FieldRetreiverCallServiceFake,
|
||||
taskExecutionsCallService: TaskExecutionsCallServiceFake
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { AuthorizationCallService } from "@services/authorization/authorization-
|
|||
import { BlocksCallService } from "@services/blocks/blocks-call";
|
||||
import { FlowsCallService } from "@services/flows/flows-call";
|
||||
import { FieldRetreiverCallService } from "@services/retreiver/field-retreiver-call";
|
||||
import { TaskExecutionsCallService } from "@services/task-executions/task-executions-call";
|
||||
|
||||
export const environment = {
|
||||
apiUrl: '/api',
|
||||
|
|
@ -9,5 +10,6 @@ export const environment = {
|
|||
authorizationCallService: AuthorizationCallService,
|
||||
flowsCallService: FlowsCallService,
|
||||
blocksCallService: BlocksCallService,
|
||||
fieldRetreiverCallService: FieldRetreiverCallService
|
||||
fieldRetreiverCallService: FieldRetreiverCallService,
|
||||
taskExecutionsCallService: TaskExecutionsCallService
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue