Improve execution handoff and node loading feedback
This commit is contained in:
parent
d508ccf6d1
commit
bb1cdd52ac
|
|
@ -2,3 +2,64 @@
|
|||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tasks-executor-shell {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tasks-executor-viewer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tasks-executor-loader {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(243, 244, 246, 0.82);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.tasks-executor-loader-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
max-width: 360px;
|
||||
padding: 24px 28px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 20px 44px rgba(15, 23, 42, 0.12);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tasks-executor-spinner {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 3px solid rgba(15, 23, 42, 0.12);
|
||||
border-top-color: #2563eb;
|
||||
border-radius: 999px;
|
||||
animation: tasks-executor-spin 0.75s linear infinite;
|
||||
}
|
||||
|
||||
.tasks-executor-loader-title {
|
||||
color: #0f172a;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tasks-executor-loader-text {
|
||||
color: #475569;
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@keyframes tasks-executor-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="flex flex-col flex-1 h-full">
|
||||
<div class="tasks-executor-shell flex flex-col flex-1 h-full">
|
||||
<div class="flex flex-row flex-1 overflow-hidden gap-2">
|
||||
<app-tasks-executions-list
|
||||
class="w-[360px] shrink-0"
|
||||
|
|
@ -8,7 +8,16 @@
|
|||
(executionDeleteRequested)="removeExecution($event)">
|
||||
</app-tasks-executions-list>
|
||||
|
||||
<mat-card class="flex w-full flex-col overflow-hidden !rounded-md !bg-gray-100">
|
||||
<mat-card class="tasks-executor-viewer flex w-full flex-col overflow-hidden !rounded-md !bg-gray-100">
|
||||
@if (showExecutionCreationLoader()) {
|
||||
<div class="tasks-executor-loader">
|
||||
<div class="tasks-executor-loader-card">
|
||||
<span class="tasks-executor-spinner" aria-hidden="true"></span>
|
||||
<div class="tasks-executor-loader-title">Creating execution...</div>
|
||||
<div class="tasks-executor-loader-text">The task view is ready. Waiting for the new execution to appear.</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<app-task-execution-viewer [execution]="selectedExecution()"></app-task-execution-viewer>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export class TasksExecutor {
|
|||
);
|
||||
|
||||
readonly executionDetails = this.taskExecutionsService.taskExecutions;
|
||||
readonly pendingExecutionCreation = this.taskExecutionsService.pendingExecutionCreation;
|
||||
|
||||
readonly executions = computed<TaskExecutionListItem[]>(() =>
|
||||
this.executionDetails().map((execution) => ({
|
||||
|
|
@ -56,6 +57,10 @@ export class TasksExecutor {
|
|||
return details.find((execution) => execution.id === selectedId) ?? null;
|
||||
});
|
||||
|
||||
readonly showExecutionCreationLoader = computed(() =>
|
||||
this.pendingExecutionCreation() && !this.requestedExecutionId()
|
||||
);
|
||||
|
||||
constructor() {
|
||||
void this.blocksService.getAllBlocksTypes().catch((err) => {
|
||||
console.error('Error preloading block types for task executor', err);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export class BlocksService {
|
|||
|
||||
toInit: boolean = true;
|
||||
private loadingPromise: Promise<void> | null = null;
|
||||
private readonly _catalogLoading = signal(false);
|
||||
private readonly emptyBlockCache = new Map<string, FlowBlock>();
|
||||
private readonly pendingEmptyBlockRequests = new Map<string, Observable<FlowBlock>>();
|
||||
private readonly pendingServerSyncCount = signal(0);
|
||||
|
|
@ -19,6 +20,7 @@ export class BlocksService {
|
|||
private _blockTypes = signal<BlockType[]>([]);
|
||||
readonly hasPendingServerSync = computed(() => this.pendingServerSyncCount() > 0);
|
||||
readonly blockTypes = this._blockTypes.asReadonly();
|
||||
readonly catalogLoading = this._catalogLoading.asReadonly();
|
||||
|
||||
hasLoadedBlockTypes() {
|
||||
return this._blockTypes().length > 0 || !this.toInit;
|
||||
|
|
@ -39,6 +41,9 @@ export class BlocksService {
|
|||
}
|
||||
|
||||
this.loadingPromise = firstValueFrom(this.blocksCallService.retrieveAllBlocksTypes())
|
||||
.finally(() => {
|
||||
this._catalogLoading.set(false);
|
||||
})
|
||||
.then((blockTypes) => {
|
||||
this._blockTypes.set(blockTypes);
|
||||
this.clearEmptyBlockCache();
|
||||
|
|
@ -51,6 +56,8 @@ export class BlocksService {
|
|||
this.loadingPromise = null;
|
||||
});
|
||||
|
||||
this._catalogLoading.set(true);
|
||||
|
||||
return this.loadingPromise;
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +65,11 @@ export class BlocksService {
|
|||
const current = this._blockTypes().find((blockType) => blockType.type === typeName);
|
||||
if (current) return current;
|
||||
|
||||
const blockTypes = await firstValueFrom(this.blocksCallService.retrieveAllBlocksTypes());
|
||||
this._catalogLoading.set(true);
|
||||
const blockTypes = await firstValueFrom(this.blocksCallService.retrieveAllBlocksTypes())
|
||||
.finally(() => {
|
||||
this._catalogLoading.set(false);
|
||||
});
|
||||
this._blockTypes.set(blockTypes);
|
||||
this.clearEmptyBlockCache();
|
||||
return blockTypes.find((blockType) => blockType.type === typeName);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export class ContainersService {
|
|||
|
||||
toInit = true;
|
||||
private loadingPromise: Promise<void> | null = null;
|
||||
private readonly _catalogLoading = signal(false);
|
||||
private readonly emptyContainerCache = new Map<string, FlowNode>();
|
||||
private readonly pendingEmptyContainerRequests = new Map<string, Observable<FlowNode>>();
|
||||
private readonly pendingServerSyncCount = signal(0);
|
||||
|
|
@ -19,6 +20,7 @@ export class ContainersService {
|
|||
private _containerTypes = signal<BlockType[]>([]);
|
||||
readonly hasPendingServerSync = computed(() => this.pendingServerSyncCount() > 0);
|
||||
readonly containerTypes = this._containerTypes.asReadonly();
|
||||
readonly catalogLoading = this._catalogLoading.asReadonly();
|
||||
|
||||
hasLoadedContainerTypes() {
|
||||
return this._containerTypes().length > 0 || !this.toInit;
|
||||
|
|
@ -39,6 +41,9 @@ export class ContainersService {
|
|||
}
|
||||
|
||||
this.loadingPromise = firstValueFrom(this.containersCallService.retrieveAllContainerTypes())
|
||||
.finally(() => {
|
||||
this._catalogLoading.set(false);
|
||||
})
|
||||
.then((containerTypes) => {
|
||||
this._containerTypes.set(containerTypes);
|
||||
this.clearEmptyContainerCache();
|
||||
|
|
@ -51,6 +56,8 @@ export class ContainersService {
|
|||
this.loadingPromise = null;
|
||||
});
|
||||
|
||||
this._catalogLoading.set(true);
|
||||
|
||||
return this.loadingPromise;
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +65,11 @@ export class ContainersService {
|
|||
const current = this._containerTypes().find((containerType) => containerType.type === typeName);
|
||||
if (current) return current;
|
||||
|
||||
const containerTypes = await firstValueFrom(this.containersCallService.retrieveAllContainerTypes());
|
||||
this._catalogLoading.set(true);
|
||||
const containerTypes = await firstValueFrom(this.containersCallService.retrieveAllContainerTypes())
|
||||
.finally(() => {
|
||||
this._catalogLoading.set(false);
|
||||
});
|
||||
this._containerTypes.set(containerTypes);
|
||||
this.clearEmptyContainerCache();
|
||||
return containerTypes.find((containerType) => containerType.type === typeName);
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ export class TaskExecutionsService {
|
|||
private refreshInFlight = false;
|
||||
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private _taskExecutions = signal<TaskExecution[]>([]);
|
||||
private _pendingExecutionCreation = signal(false);
|
||||
|
||||
taskExecutions = this._taskExecutions.asReadonly();
|
||||
pendingExecutionCreation = this._pendingExecutionCreation.asReadonly();
|
||||
|
||||
init() {
|
||||
if (this.initialized) return;
|
||||
|
|
@ -48,7 +50,9 @@ export class TaskExecutionsService {
|
|||
}
|
||||
|
||||
createExecution(flowId: string) {
|
||||
this._pendingExecutionCreation.set(true);
|
||||
return this.taskExecutionsCallService.createTaskExecution(flowId).pipe(
|
||||
finalize(() => this._pendingExecutionCreation.set(false)),
|
||||
tap(() => this.refresh()),
|
||||
catchError((err) => {
|
||||
console.error('Create execution failed', err);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@if (loading()) {
|
||||
@if (showLoading()) {
|
||||
<div class="blocks-list-loading">
|
||||
<mat-spinner diameter="32"></mat-spinner>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export class BlocksList extends ListStateViewHolder<BlockType> {
|
|||
private blocksService = inject(BlocksService);
|
||||
|
||||
loading: WritableSignal<boolean> = signal(true);
|
||||
readonly serviceLoading = this.blocksService.catalogLoading;
|
||||
|
||||
/*
|
||||
get orderView() {
|
||||
|
|
@ -75,6 +76,8 @@ export class BlocksList extends ListStateViewHolder<BlockType> {
|
|||
);
|
||||
});
|
||||
|
||||
readonly showLoading = computed(() => this.loading() || this.serviceLoading());
|
||||
|
||||
onDragStart(event: DragEvent, block: BlockType) {
|
||||
if (!event.dataTransfer) return;
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@if (loading()) {
|
||||
@if (showLoading()) {
|
||||
<div class="blocks-list-loading">
|
||||
<mat-spinner diameter="32"></mat-spinner>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export class ContainersList extends ListStateViewHolder<BlockType> {
|
|||
private containersService = inject(ContainersService);
|
||||
|
||||
loading: WritableSignal<boolean> = signal(true);
|
||||
readonly serviceLoading = this.containersService.catalogLoading;
|
||||
containerTypes?: Signal<BlockType[]>;
|
||||
|
||||
constructor() {
|
||||
|
|
@ -67,6 +68,8 @@ export class ContainersList extends ListStateViewHolder<BlockType> {
|
|||
);
|
||||
});
|
||||
|
||||
readonly showLoading = computed(() => this.loading() || this.serviceLoading());
|
||||
|
||||
onDragStart(event: DragEvent, container: BlockType) {
|
||||
if (!event.dataTransfer) return;
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
readonly modelPickerOpen = signal(false);
|
||||
readonly quickPromptsOpen = signal(true);
|
||||
readonly editorHasOpenFlow = computed(() => !!this.currentFlow());
|
||||
readonly editorHasNonEmptyFlow = computed(() => this.hasMeaningfulFlow(this.currentFlow()));
|
||||
readonly sessionHasDraft = computed(() => !!this.currentDraft());
|
||||
readonly canOfferCreate = computed(() => !this.editorHasOpenFlow() && !this.sessionHasDraft());
|
||||
readonly canOfferCreate = computed(() => !this.editorHasNonEmptyFlow() && !this.sessionHasDraft());
|
||||
readonly canOfferFix = computed(() => !this.canOfferCreate() && (this.sessionState()?.lastValidationErrors?.length ?? 0) > 0);
|
||||
readonly assistantModeLabel = computed(() => this.canOfferCreate() ? 'Create with assistant' : 'Refine with assistant');
|
||||
readonly assistantModeDescription = computed(() => {
|
||||
|
|
@ -420,4 +421,13 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
const normalizedBase = apiBase.startsWith('/') ? apiBase : `/${apiBase}`;
|
||||
return new URL(url, `${origin}${normalizedBase.replace(/\/+$/, '')}/`).toString();
|
||||
}
|
||||
|
||||
private hasMeaningfulFlow(flow: Flow | null): boolean {
|
||||
if (!flow) return false;
|
||||
const data = flow.data;
|
||||
return (data.blocks?.length ?? 0) > 0
|
||||
|| (data.containers?.length ?? 0) > 0
|
||||
|| (data.connections?.length ?? 0) > 0
|
||||
|| (data.dependencies?.length ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export class HumanInteractionDialogHostComponent {
|
|||
confirmInput(event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
this.closeWith({ mode: 'complete', value: this.state()?.currentInput ?? '' });
|
||||
this.dialog.submit({ mode: 'complete', value: this.state()?.currentInput ?? '' });
|
||||
}
|
||||
|
||||
sendEditedOutput(event?: Event) {
|
||||
|
|
@ -125,7 +125,7 @@ export class HumanInteractionDialogHostComponent {
|
|||
event?.stopPropagation();
|
||||
const value = this.draftValue.trim();
|
||||
if (!value) return;
|
||||
this.closeWith({ mode: 'complete', value: this.draftValue });
|
||||
this.dialog.submit({ mode: 'complete', value: this.draftValue });
|
||||
}
|
||||
|
||||
sendChatMessage(event?: Event) {
|
||||
|
|
@ -165,8 +165,4 @@ export class HumanInteractionDialogHostComponent {
|
|||
}
|
||||
return deduplicated;
|
||||
}
|
||||
|
||||
private closeWith(value: HumanInteractionDialogResult) {
|
||||
this.dialog.close(value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@if (state()) {
|
||||
<div class="fixed inset-0 z-9999">
|
||||
<div class="fixed inset-0" style="z-index: 10010;">
|
||||
<div class="absolute inset-0 bg-black/50" (click)="cancel($event)"></div>
|
||||
|
||||
<div class="absolute left-1/2 top-1/2 flex max-h-[min(80vh,720px)] w-[min(92vw,560px)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 shadow-2xl">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
<div class="container-node" [class.container-node-assigning]="isAssigning" [class.container-node-delete-pending]="deleteConfirmOpen">
|
||||
@if (isSchemaLoading && !isAssigning) {
|
||||
<div class="llm-loading-overlay">
|
||||
<div class="llm-loading-chip">
|
||||
<span class="container-node__spinner" aria-hidden="true"></span>
|
||||
<span>Loading container...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (deleteConfirmOpen) {
|
||||
<div class="container-node__delete-overlay"></div>
|
||||
}
|
||||
|
|
@ -222,6 +230,15 @@
|
|||
(click)="openParameterEditor(field.path, $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
} @else if (field.expandable) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-param-view-btn"
|
||||
aria-label="View full value"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openFieldPreview(field, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<span class="container-node__param-value" [class.container-node__param-value--clamped]="field.expandable">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
|
||||
|
|
@ -231,6 +248,16 @@
|
|||
<div class="container-node__param-chip container-node__param-chip--wide">
|
||||
<div class="container-node__param-head">
|
||||
<span class="container-node__param-key">{{ contentField.label }}</span>
|
||||
@if (contentField.expandable && isReadonly) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-param-view-btn"
|
||||
aria-label="View full value"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openMainContentPreview(contentField, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="container-node__param-value" [class.container-node__param-value--clamped]="contentField.expandable">
|
||||
@for (part of contentField.parts; track $index) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, HostBinding, Input, inject } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, HostBinding, HostListener, Input, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { currentFlowPortValueKind, flowValueKindLabel, FlowBlock, FlowContainer, FlowData, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY } from '@models/flow';
|
||||
|
|
@ -87,9 +87,14 @@ export class ContainerNodeComponent {
|
|||
parameterFields: ContainerFieldView[] = [];
|
||||
richContentFields: RichContentView[] = [];
|
||||
schemaReady = false;
|
||||
private schemaLoading = false;
|
||||
nameEditorOpen = false;
|
||||
draftName = '';
|
||||
|
||||
get isSchemaLoading() {
|
||||
return this.schemaLoading;
|
||||
}
|
||||
|
||||
@Input() data!: any;
|
||||
@Input() emit!: (data: any) => void;
|
||||
@Input() rendered!: () => void;
|
||||
|
|
@ -114,6 +119,12 @@ export class ContainerNodeComponent {
|
|||
void this.loadSchemaContext();
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onNodeClick() {
|
||||
if (!this.shouldRetrySchemaLoad()) return;
|
||||
void this.loadSchemaContext();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.rendered();
|
||||
}
|
||||
|
|
@ -535,6 +546,20 @@ export class ContainerNodeComponent {
|
|||
this.subflowPreview.open(this.subFlow, `${this.name} subflow`);
|
||||
}
|
||||
|
||||
async openFieldPreview(field: ContainerFieldView, event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (!field.expandable) return;
|
||||
await this.openReadonlyTextDialog(field.label, field.value);
|
||||
}
|
||||
|
||||
async openMainContentPreview(field: RichContentView, event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (!field.expandable) return;
|
||||
await this.openReadonlyTextDialog(field.label, field.rawValue);
|
||||
}
|
||||
|
||||
private get configuration(): Record<string, unknown> | null {
|
||||
const value = this.data?.data?.specificConfiguration;
|
||||
return value && typeof value === 'object' ? value as Record<string, unknown> : null;
|
||||
|
|
@ -553,6 +578,25 @@ export class ContainerNodeComponent {
|
|||
return !this.isAssigning && !this.replaceConfirmOpen && this.selectedCount > 0;
|
||||
}
|
||||
|
||||
private async openReadonlyTextDialog(label: string, value: string) {
|
||||
await this.settingsDialog.open({
|
||||
title: label,
|
||||
previewOnly: true,
|
||||
fields: [
|
||||
{
|
||||
key: 'value',
|
||||
label,
|
||||
type: 'textarea',
|
||||
readonly: true,
|
||||
rows: 18
|
||||
}
|
||||
],
|
||||
initial: {
|
||||
value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private assignSelectionToContainer(payload: string[]) {
|
||||
this.importErrorMessage = null;
|
||||
const assign = this.data?.data?.assignSelectedBlocksToContainer;
|
||||
|
|
@ -664,6 +708,9 @@ export class ContainerNodeComponent {
|
|||
}
|
||||
|
||||
private async loadSchemaContext() {
|
||||
if (this.schemaLoading) return;
|
||||
this.schemaLoading = true;
|
||||
this.schemaReady = false;
|
||||
try {
|
||||
const containerType = this.containersService.peekContainerType(this.typeName) ?? await this.containersService.getContainerType(this.typeName);
|
||||
this.containerSchema = (containerType?.schema ?? null) as Record<string, any> | null;
|
||||
|
|
@ -671,6 +718,7 @@ export class ContainerNodeComponent {
|
|||
this.containerFieldDefinitions = this.buildContainerFieldDefinitions(this.containerSchema);
|
||||
this.refreshParameterFields();
|
||||
} finally {
|
||||
this.schemaLoading = false;
|
||||
this.schemaReady = true;
|
||||
queueMicrotask(() => {
|
||||
try {
|
||||
|
|
@ -682,6 +730,11 @@ export class ContainerNodeComponent {
|
|||
}
|
||||
}
|
||||
|
||||
private shouldRetrySchemaLoad(): boolean {
|
||||
if (this.schemaLoading) return false;
|
||||
return !this.containerSchema || this.containerFieldDefinitions.length === 0;
|
||||
}
|
||||
|
||||
private isMissingValue(value: unknown): boolean {
|
||||
if (value == null) return true;
|
||||
if (typeof value === 'string') return value.trim().length === 0;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (isSchemaLoading && !isCreatingOnServer()) {
|
||||
<div class="llm-loading-overlay">
|
||||
<div class="llm-loading-chip">
|
||||
<span class="llm-spinner" aria-hidden="true"></span>
|
||||
<span>Loading block...</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (deleteConfirmOpen) {
|
||||
<div class="llm-delete-overlay"></div>
|
||||
}
|
||||
|
|
@ -252,6 +260,15 @@
|
|||
(click)="openParameterEditor(field.path, $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
} @else if (field.expandable) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-param-view-btn"
|
||||
aria-label="View full value"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openFieldPreview(field, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<span class="llm-param-value" [class.llm-param-value-clamped]="field.expandable">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
|
||||
|
|
@ -291,6 +308,15 @@
|
|||
(click)="openParameterEditor(field.path, $event)">
|
||||
<i class="bi bi-pen"></i>
|
||||
</button>
|
||||
} @else if (field.expandable) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-param-view-btn"
|
||||
aria-label="View full value"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openFieldPreview(field, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<span class="llm-param-value" [class.llm-param-value-clamped]="field.expandable">{{ field.type === 'boolean' ? (field.booleanValue ? 'Enabled' : 'Disabled') : field.value }}</span>
|
||||
|
|
@ -308,6 +334,15 @@
|
|||
<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>
|
||||
} @else if (contentField.expandable) {
|
||||
<button
|
||||
type="button"
|
||||
class="llm-param-view-btn"
|
||||
aria-label="View full value"
|
||||
(pointerdown)="$event.stopPropagation()"
|
||||
(click)="openMainContentPreview(contentField, $event)">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="llm-param-text" [class.llm-param-text-clamped]="contentField.expandable">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, HostBinding, inject, Input } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, HostBinding, HostListener, inject, Input } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BlockType, currentFlowPortValueKind, flowValueKindLabel, FlowData, FlowPort, FlowValueKind, FLOW_DEPENDANT_PORT_KEY, FLOW_DEPENDENCY_PORT_KEY, normalizeFlowPortValueKinds } from '@models/flow';
|
||||
|
|
@ -199,6 +199,11 @@ export class GenericNodeComponent {
|
|||
localEditorBindableInputName: string | null = null;
|
||||
deleteConfirmOpen = false;
|
||||
schemaReady = false;
|
||||
private schemaLoading = false;
|
||||
|
||||
get isSchemaLoading() {
|
||||
return this.schemaLoading;
|
||||
}
|
||||
|
||||
missingRequiredParams: string[] = [];
|
||||
private blockSchema: Record<string, any> | null = null;
|
||||
|
|
@ -244,6 +249,12 @@ export class GenericNodeComponent {
|
|||
void this.loadSchemaContext();
|
||||
}
|
||||
|
||||
@HostListener('click')
|
||||
onNodeClick() {
|
||||
if (!this.shouldRetrySchemaLoad()) return;
|
||||
void this.loadSchemaContext();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.rendered();
|
||||
}
|
||||
|
|
@ -578,12 +589,15 @@ export class GenericNodeComponent {
|
|||
}
|
||||
|
||||
private async loadSchemaContext() {
|
||||
if (this.schemaLoading) return;
|
||||
const type = this.blockType;
|
||||
if (!type) {
|
||||
this.schemaReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.schemaLoading = true;
|
||||
this.schemaReady = false;
|
||||
try {
|
||||
const blockType = this.blocksService.peekBlockType(type) ?? await this.blocksService.getBlockType(type);
|
||||
this.blockDescriptor = blockType ?? null;
|
||||
|
|
@ -598,10 +612,17 @@ export class GenericNodeComponent {
|
|||
this.refreshValidationState();
|
||||
this.maybeCreateBlockOnServer();
|
||||
} finally {
|
||||
this.schemaLoading = false;
|
||||
this.schemaReady = true;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRetrySchemaLoad(): boolean {
|
||||
if (this.schemaLoading) return false;
|
||||
if (!this.blockType) return false;
|
||||
return !this.blockSchema || (!this.editableFieldDefinitions.length && !this.arrayFieldDefinitions.length);
|
||||
}
|
||||
|
||||
private async openTextareaEditor(
|
||||
path: string,
|
||||
label: string,
|
||||
|
|
@ -1093,6 +1114,20 @@ export class GenericNodeComponent {
|
|||
}
|
||||
}
|
||||
|
||||
async openFieldPreview(field: EditableFieldView, event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (!field.expandable) return;
|
||||
await this.openReadonlyTextDialog(field.label, field.value);
|
||||
}
|
||||
|
||||
async openMainContentPreview(field: RichContentView, event?: Event) {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
if (!field.expandable) return;
|
||||
await this.openReadonlyTextDialog(field.label, field.rawValue);
|
||||
}
|
||||
|
||||
private resolveSelectableOptions(definition: EditableFieldDefinition): NodeSettingOption[] {
|
||||
if (definition.nodeOptionsSource) {
|
||||
return this.resolveNodeOptions(definition.nodeOptionsSource);
|
||||
|
|
@ -1122,6 +1157,25 @@ export class GenericNodeComponent {
|
|||
return String(value ?? '').trim().length > 80;
|
||||
}
|
||||
|
||||
private async openReadonlyTextDialog(label: string, value: string) {
|
||||
await this.settingsDialog.open({
|
||||
title: label,
|
||||
previewOnly: true,
|
||||
fields: [
|
||||
{
|
||||
key: 'value',
|
||||
label,
|
||||
type: 'textarea',
|
||||
readonly: true,
|
||||
rows: 18
|
||||
}
|
||||
],
|
||||
initial: {
|
||||
value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private refreshParameterFields() {
|
||||
const config = this.blockConfiguration ?? {};
|
||||
const richContentPaths = new Set(this.richContentPaths());
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ export class TitleToolbar {
|
|||
if (!flow || !this.canExecute() || this.executeLoading()) return;
|
||||
|
||||
this.executeLoading.set(true);
|
||||
void this.router.navigate(['/tasks']);
|
||||
this.taskExecutionsService.createExecution(flow.id).pipe(
|
||||
take(1)
|
||||
).subscribe({
|
||||
|
|
|
|||
Loading…
Reference in New Issue