UI/UX: miglioramenti a flow-editor, flow-assistant e rete-editor (stili, layout, interazioni)
This commit is contained in:
parent
f558df3aa8
commit
73312f82c0
|
|
@ -220,6 +220,13 @@
|
|||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.flow-editor-create-ai-window-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.flow-editor-create-ai-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -236,6 +243,19 @@
|
|||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.flow-editor-create-ai-close:disabled {
|
||||
border-color: #e2e8f0;
|
||||
background: #f8fafc;
|
||||
color: #cbd5e1;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.flow-editor-create-ai-close:disabled:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.flow-editor-create-ai-close .mat-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
|
@ -247,6 +267,37 @@
|
|||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.flow-editor-create-ai-floating {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 1299;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: calc(100vw - 48px);
|
||||
min-height: 44px;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid rgba(14, 116, 144, 0.28);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
box-shadow: 0 18px 36px rgba(15, 23, 42, 0.22);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.flow-editor-create-ai-floating:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.flow-editor-create-ai-floating .mat-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
font-size: 18px;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.flow-editor-tour-target {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,25 +108,55 @@
|
|||
</div>
|
||||
|
||||
@if (showCreateAssistantModal()) {
|
||||
<div class="flow-editor-create-ai-backdrop" (click)="closeCreateWithAi()"></div>
|
||||
<div class="flow-editor-create-ai-backdrop"></div>
|
||||
<section class="flow-editor-create-ai-modal" role="dialog" aria-modal="true" aria-labelledby="create-ai-title">
|
||||
<div class="flow-editor-create-ai-header">
|
||||
<div>
|
||||
<div class="flow-editor-create-ai-eyebrow">Workflow Copilot</div>
|
||||
<h2 id="create-ai-title" class="flow-editor-create-ai-title">New flow with AI</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flow-editor-create-ai-close"
|
||||
aria-label="Close AI flow creation"
|
||||
(click)="closeCreateWithAi()">
|
||||
<mat-icon fontIcon="close"></mat-icon>
|
||||
</button>
|
||||
<div class="flow-editor-create-ai-window-actions">
|
||||
<span
|
||||
[matTooltip]="createAssistantCancellable() ? 'Minimize and keep the assistant running' : 'Minimize is available while the assistant is running'">
|
||||
<button
|
||||
type="button"
|
||||
class="flow-editor-create-ai-close"
|
||||
aria-label="Minimize AI flow creation"
|
||||
[disabled]="!createAssistantCancellable()"
|
||||
(click)="minimizeCreateWithAi()">
|
||||
<mat-icon fontIcon="remove"></mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="flow-editor-create-ai-close"
|
||||
aria-label="Close AI flow creation"
|
||||
[matTooltip]="createAssistantCancellable() ? 'Cancel the assistant request and close' : 'Close AI flow creation'"
|
||||
(click)="closeCreateWithAi()">
|
||||
<mat-icon fontIcon="close"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-flow-assistant class="flow-editor-create-ai-assistant" [variant]="'create-modal'"></app-flow-assistant>
|
||||
<app-flow-assistant
|
||||
#createAssistant
|
||||
class="flow-editor-create-ai-assistant"
|
||||
[variant]="'create-modal'"
|
||||
(cancellableCallChange)="createAssistantCancellable.set($event)"></app-flow-assistant>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (showCreateAssistantFloatingButton()) {
|
||||
<button
|
||||
type="button"
|
||||
class="flow-editor-create-ai-floating"
|
||||
aria-label="Reopen AI flow creation"
|
||||
matTooltip="Reopen AI flow creation"
|
||||
(click)="restoreCreateWithAi()">
|
||||
<mat-icon fontIcon="auto_awesome"></mat-icon>
|
||||
<span>AI flow creation</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (tourActive() && activeTourStep(); as step) {
|
||||
<div class="flow-editor-tour-overlay" (click)="skipTour()"></div>
|
||||
<div class="flow-editor-tour-spotlight" [ngStyle]="tourSpotlightStyle()"></div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, HostListener, ViewChild, computed,
|
|||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { environment } from '@environment';
|
||||
import { FlowBlock, FlowData } from '@models/flow';
|
||||
import { EditorSidebar } from "@pages/main/editor-sidebar/editor-sidebar";
|
||||
|
|
@ -11,6 +12,7 @@ import { Authorization } from '@services/authorization/authorization';
|
|||
import { BlocksService } from '@services/blocks/blocks';
|
||||
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
|
||||
import { FlowsService } from '@services/flows/flows';
|
||||
import { AssistantSessionStore } from '@stores/assistant-session-store';
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { FlowAssistant } from '@shared/flow-assistant/flow-assistant';
|
||||
import { FlowValidationPanel } from '@shared/flow-validation-panel/flow-validation-panel';
|
||||
|
|
@ -28,7 +30,7 @@ type TourStep = {
|
|||
|
||||
@Component({
|
||||
selector: 'app-flow-editor',
|
||||
imports: [CommonModule, EditorSidebar, TitleToolbar, ReteEditor, FlowAssistant, FlowValidationPanel, MatButtonModule, MatCardModule, MatIconModule],
|
||||
imports: [CommonModule, EditorSidebar, TitleToolbar, ReteEditor, FlowAssistant, FlowValidationPanel, MatButtonModule, MatCardModule, MatIconModule, MatTooltipModule],
|
||||
templateUrl: './flow-editor.html',
|
||||
styleUrl: './flow-editor.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -37,12 +39,14 @@ export class FlowEditor {
|
|||
private static readonly SIDEBAR_OPEN_DELAY_MS = 320;
|
||||
private static readonly TOUR_SEEN_KEY_PREFIX = 'editor-guided-tour-seen:';
|
||||
@ViewChild(EditorSidebar) editorSidebar?: EditorSidebar;
|
||||
@ViewChild('createAssistant') createAssistant?: FlowAssistant;
|
||||
|
||||
private editorState: EditorStateHolder = inject(EditorStateHolder);
|
||||
private authorization = inject(Authorization);
|
||||
private flowsService = inject(FlowsService);
|
||||
private blocksService = inject(BlocksService);
|
||||
private confirm = inject(ConfirmDialogService);
|
||||
private assistantSessionStore = inject(AssistantSessionStore);
|
||||
private tourBootstrapped = false;
|
||||
private demoFlowId: string | null = null;
|
||||
|
||||
|
|
@ -50,6 +54,8 @@ export class FlowEditor {
|
|||
assistantOpen = signal(true);
|
||||
activeRightPanel = signal<'assistant' | 'errors'>('assistant');
|
||||
aiCreationRequested = signal(false);
|
||||
aiCreationMinimized = signal(false);
|
||||
createAssistantCancellable = signal(false);
|
||||
creatingFlowFromEmpty = signal(false);
|
||||
flow = this.editorState.currentFlow;
|
||||
readonly = this.editorState.isCurrentFlowReadOnly;
|
||||
|
|
@ -59,7 +65,10 @@ export class FlowEditor {
|
|||
this.assistantEnabled && !this.readonly() && !!this.flow()
|
||||
);
|
||||
showCreateAssistantModal = computed(() =>
|
||||
this.assistantEnabled && this.aiCreationRequested() && !this.flow()
|
||||
this.assistantEnabled && this.aiCreationRequested() && !this.aiCreationMinimized() && !this.flow()
|
||||
);
|
||||
showCreateAssistantFloatingButton = computed(() =>
|
||||
this.assistantEnabled && this.aiCreationRequested() && this.aiCreationMinimized() && !this.flow()
|
||||
);
|
||||
tourActive = signal(false);
|
||||
tourStepIndex = signal(0);
|
||||
|
|
@ -152,8 +161,12 @@ export class FlowEditor {
|
|||
effect(() => {
|
||||
if (this.aiCreationRequested() && this.flow()) {
|
||||
this.aiCreationRequested.set(false);
|
||||
this.aiCreationMinimized.set(false);
|
||||
this.createAssistantCancellable.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
queueMicrotask(() => this.restoreMinimizedCreateAssistantIfAvailable());
|
||||
}
|
||||
|
||||
toggleAssistant() {
|
||||
|
|
@ -216,12 +229,37 @@ export class FlowEditor {
|
|||
this.editorState.closeDocument();
|
||||
}
|
||||
this.aiCreationRequested.set(true);
|
||||
this.aiCreationMinimized.set(false);
|
||||
this.activeRightPanel.set('assistant');
|
||||
this.assistantOpen.set(true);
|
||||
}
|
||||
|
||||
closeCreateWithAi() {
|
||||
minimizeCreateWithAi() {
|
||||
if (!this.aiCreationRequested() || !this.createAssistantCancellable()) return;
|
||||
this.aiCreationMinimized.set(true);
|
||||
}
|
||||
|
||||
restoreCreateWithAi() {
|
||||
if (!this.aiCreationRequested()) return;
|
||||
this.aiCreationMinimized.set(false);
|
||||
}
|
||||
|
||||
async closeCreateWithAi() {
|
||||
const assistant = this.createAssistant;
|
||||
if (assistant?.hasCancellableCall()) {
|
||||
const confirmed = await this.confirm.open(
|
||||
'An assistant request is still running. Cancel it and close AI flow creation?'
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
const cancelled = await assistant.cancelActiveCall();
|
||||
if (!cancelled) return;
|
||||
assistant.clearActiveSnapshot();
|
||||
}
|
||||
|
||||
this.aiCreationRequested.set(false);
|
||||
this.aiCreationMinimized.set(false);
|
||||
this.createAssistantCancellable.set(false);
|
||||
}
|
||||
|
||||
async createFlowFromEmpty() {
|
||||
|
|
@ -407,6 +445,14 @@ export class FlowEditor {
|
|||
return localStorage.getItem(`${FlowEditor.TOUR_SEEN_KEY_PREFIX}${username}`) === 'true';
|
||||
}
|
||||
|
||||
private restoreMinimizedCreateAssistantIfAvailable() {
|
||||
if (!this.assistantEnabled || this.flow() || this.aiCreationRequested()) return;
|
||||
if (!this.assistantSessionStore.hasSnapshot(AssistantSessionStore.CREATE_MODAL_FLOW_KEY)) return;
|
||||
|
||||
this.aiCreationRequested.set(true);
|
||||
this.aiCreationMinimized.set(true);
|
||||
}
|
||||
|
||||
private ensureSidebarForStep(): boolean {
|
||||
const section = this.activeTourStep()?.sidebarSection;
|
||||
if (!section) return false;
|
||||
|
|
|
|||
|
|
@ -123,6 +123,36 @@
|
|||
color: #7f1d1d;
|
||||
}
|
||||
|
||||
.assistant-inline-retry {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 8px;
|
||||
padding: 5px 12px;
|
||||
border: 1px solid rgba(15, 118, 110, 0.28);
|
||||
border-radius: 999px;
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
text-transform: lowercase;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.assistant-inline-retry:hover {
|
||||
border-color: rgba(15, 118, 110, 0.46);
|
||||
background: #ccfbf1;
|
||||
color: #115e59;
|
||||
}
|
||||
|
||||
.assistant-inline-retry:disabled {
|
||||
border-color: #cbd5e1;
|
||||
background: #f8fafc;
|
||||
color: #94a3b8;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.assistant-busy-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
|
|||
|
|
@ -38,22 +38,6 @@
|
|||
</section>
|
||||
}
|
||||
|
||||
@if (assistantErrorMessage() && !assistantBusy()) {
|
||||
<section class="assistant-card assistant-error-card" role="status" aria-live="polite">
|
||||
<p class="assistant-error-message">{{ assistantErrorMessage() }}</p>
|
||||
<button
|
||||
type="button"
|
||||
mat-stroked-button
|
||||
class="assistant-retry"
|
||||
[disabled]="!canRetryLastPrompt()"
|
||||
(click)="retryLastPrompt()">
|
||||
Retry
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@if (!progressOnlyMode()) {
|
||||
<section class="assistant-card assistant-starters">
|
||||
<div class="assistant-starters-head">
|
||||
|
|
@ -82,6 +66,16 @@
|
|||
<article class="assistant-message" [class.assistant-message-user]="message.role === 'user'" [class.assistant-message-system]="message.role === 'system'">
|
||||
<div class="assistant-message-role">{{ message.role }}</div>
|
||||
<p class="assistant-message-body">{{ message.content }}</p>
|
||||
@if (canRetryFromMessage(message)) {
|
||||
<button
|
||||
type="button"
|
||||
class="assistant-inline-retry"
|
||||
aria-label="Retry last assistant request"
|
||||
[disabled]="!canRetryLastPrompt()"
|
||||
(click)="retryLastPrompt()">
|
||||
retry
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (message.warnings?.length) {
|
||||
<div class="assistant-list assistant-list-warning">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, input, OnDestroy, OnInit, signal, ViewChild } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, EventEmitter, inject, input, OnDestroy, OnInit, Output, signal, ViewChild } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
|
|
@ -20,7 +20,7 @@ import { AssistantService } from '@services/assistant/assistant';
|
|||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { AssistantSessionStore } from '@stores/assistant-session-store';
|
||||
import { EditorStateHolder } from '@stores/flow-editor';
|
||||
import { finalize, interval, Subscription, switchMap, take } from 'rxjs';
|
||||
import { finalize, firstValueFrom, interval, Subscription, switchMap, take } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-flow-assistant',
|
||||
|
|
@ -30,6 +30,7 @@ import { finalize, interval, Subscription, switchMap, take } from 'rxjs';
|
|||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FlowAssistant implements OnInit, OnDestroy {
|
||||
@Output() cancellableCallChange = new EventEmitter<boolean>();
|
||||
@ViewChild('assistantScroll') assistantScrollElement?: ElementRef<HTMLDivElement>;
|
||||
|
||||
private readonly assistant = inject(AssistantService);
|
||||
|
|
@ -40,7 +41,9 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
private initialized = false;
|
||||
private activeFlowKey: string | null = null;
|
||||
private lastAutoScrollKey = '';
|
||||
private readonly createModalFlowKey = `__assistant:create-modal:${crypto.randomUUID()}`;
|
||||
private lastCancellableCallState = false;
|
||||
private skipDestroySnapshot = false;
|
||||
private readonly createModalFlowKey = AssistantSessionStore.CREATE_MODAL_FLOW_KEY;
|
||||
private static readonly STANDARD_ASSISTANT_ERROR = 'Something went wrong while processing your workflow request. Please try again.';
|
||||
|
||||
readonly assistantConfig = signal<AssistantConfig | null>(null);
|
||||
|
|
@ -119,7 +122,8 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
const baseMessages = this.sessionState()?.messages?.length
|
||||
? this.sessionState()!.messages
|
||||
: [this.systemWelcomeMessage()];
|
||||
return [...baseMessages, ...this.localMessages()];
|
||||
return [...baseMessages, ...this.localMessages()]
|
||||
.filter((message) => !this.isTechnicalAssistantFailureMessage(message));
|
||||
});
|
||||
readonly assistantBusy = computed(() => {
|
||||
const status = this.currentCall()?.status;
|
||||
|
|
@ -201,6 +205,13 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
this.activeFlowKey = flowKey;
|
||||
void this.restoreSessionForFlow(flowKey);
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const nextState = this.hasCancellableCall();
|
||||
if (this.lastCancellableCallState === nextState) return;
|
||||
this.lastCancellableCallState = nextState;
|
||||
this.cancellableCallChange.emit(nextState);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -208,7 +219,7 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.activeFlowKey) {
|
||||
if (this.activeFlowKey && !this.skipDestroySnapshot) {
|
||||
this.persistSnapshot(this.activeFlowKey);
|
||||
}
|
||||
this.stopPolling();
|
||||
|
|
@ -246,6 +257,54 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
this.sendPrompt(failedPrompt);
|
||||
}
|
||||
|
||||
canRetryFromMessage(message: AssistantChatMessage): boolean {
|
||||
return message.role === 'assistant'
|
||||
&& this.canRetryLastPrompt()
|
||||
&& this.canonicalAssistantErrorContent(message.content)
|
||||
=== this.canonicalAssistantErrorContent(FlowAssistant.STANDARD_ASSISTANT_ERROR);
|
||||
}
|
||||
|
||||
hasCancellableCall(): boolean {
|
||||
return this.isActiveCall(this.currentCall());
|
||||
}
|
||||
|
||||
async cancelActiveCall(): Promise<boolean> {
|
||||
const call = this.currentCall();
|
||||
if (!this.isActiveCall(call)) {
|
||||
this.requestPending.set(false);
|
||||
this.createPromptSubmitted.set(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.stopPolling();
|
||||
this.requestPending.set(false);
|
||||
|
||||
try {
|
||||
const cancelledCall = await firstValueFrom(this.assistant.cancelCall(call.id).pipe(take(1)));
|
||||
this.currentCall.set(cancelledCall);
|
||||
this.createPromptSubmitted.set(false);
|
||||
this.persistSnapshot();
|
||||
|
||||
const session = await firstValueFrom(this.assistant.getSession(cancelledCall.sessionId || call.sessionId).pipe(take(1)));
|
||||
this.applySessionState(session, { syncDraftToEditor: false });
|
||||
this.currentCall.set(null);
|
||||
this.persistSnapshot();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Assistant call cancel failed', err);
|
||||
this.currentCall.set(call);
|
||||
this.startPolling(call.id, call.sessionId);
|
||||
this.assistantErrorMessage.set('Unable to cancel the assistant request.');
|
||||
this.persistSnapshot();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
clearActiveSnapshot() {
|
||||
const flowKey = this.activeFlowKey ?? this.resolveFlowKey(this.currentFlow()?.id ?? null);
|
||||
this.clearSnapshot(flowKey);
|
||||
}
|
||||
|
||||
private sendPrompt(content: string) {
|
||||
const normalizedContent = content.trim();
|
||||
const sessionId = this.sessionState()?.id;
|
||||
|
|
@ -325,10 +384,6 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
? this.createModalFlowKey
|
||||
: this.resolveFlowKey(this.currentFlow()?.id ?? null);
|
||||
this.initialized = true;
|
||||
if (this.isCreateModal()) {
|
||||
void this.openSession(selectedModel, this.activeFlowKey);
|
||||
return;
|
||||
}
|
||||
void this.restoreSessionForFlow(this.activeFlowKey);
|
||||
},
|
||||
error: (err) => {
|
||||
|
|
@ -370,11 +425,14 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
next: (callState) => {
|
||||
this.currentCall.set(callState);
|
||||
this.persistSnapshot();
|
||||
if (callState.status === 'COMPLETED' || callState.status === 'FAILED') {
|
||||
if (this.isTerminalCall(callState)) {
|
||||
this.stopPolling();
|
||||
if (callState.status === 'COMPLETED' && callState.flowResult?.flow) {
|
||||
this.syncDraftToEditor(this.flowResultToDraft(callState.flowResult));
|
||||
}
|
||||
if (callState.status === 'CANCELLED') {
|
||||
this.createPromptSubmitted.set(false);
|
||||
}
|
||||
void this.reloadSession(sessionId, callState.status === 'FAILED');
|
||||
}
|
||||
},
|
||||
|
|
@ -449,6 +507,7 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
const nextFlowKey = this.resolveFlowKey(nextFlow.id);
|
||||
this.persistSnapshot(currentFlowKey);
|
||||
this.sessionStore.cloneSnapshot(currentFlowKey, nextFlowKey);
|
||||
const shouldClearCreateModalSnapshot = currentFlowKey === this.createModalFlowKey;
|
||||
|
||||
void this.editorState.openDocument(nextFlow, { skipDirtyCheck: false }).then((opened) => {
|
||||
if (!opened) {
|
||||
|
|
@ -456,6 +515,9 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
this.editorState.loadAssistantFlow(nextFlow, { markDirty: true });
|
||||
if (shouldClearCreateModalSnapshot) {
|
||||
this.clearSnapshot(currentFlowKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -526,6 +588,11 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
return String(content ?? '').trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
private isTechnicalAssistantFailureMessage(message: AssistantChatMessage): boolean {
|
||||
return message.role === 'assistant'
|
||||
&& /^the assistant request failed:/i.test(this.normalizeAssistantMessageContent(message.content));
|
||||
}
|
||||
|
||||
private canonicalAssistantErrorContent(content: string): string {
|
||||
return this.normalizeAssistantMessageContent(content)
|
||||
.replace(/^the assistant request failed:\s*/i, '')
|
||||
|
|
@ -555,6 +622,9 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
this.quickPromptsOpen.set(true);
|
||||
this.sessionState.set(null);
|
||||
this.localMessages.set([]);
|
||||
this.assistantErrorMessage.set(null);
|
||||
this.lastFailedPrompt.set(null);
|
||||
this.lastSubmittedPrompt.set('');
|
||||
const model = this.selectedModel();
|
||||
if (!model) {
|
||||
this.sessionLoading.set(false);
|
||||
|
|
@ -568,7 +638,11 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
this.modelPickerOpen.set(snapshot.modelPickerOpen);
|
||||
this.quickPromptsOpen.set(snapshot.quickPromptsOpen);
|
||||
this.localMessages.set(snapshot.localMessages);
|
||||
this.currentCall.set(null);
|
||||
this.currentCall.set(snapshot.currentCall);
|
||||
this.assistantErrorMessage.set(snapshot.assistantErrorMessage);
|
||||
this.lastFailedPrompt.set(snapshot.lastFailedPrompt);
|
||||
this.lastSubmittedPrompt.set(snapshot.lastSubmittedPrompt);
|
||||
this.createPromptSubmitted.set(this.isCreateModal() && this.isActiveCall(snapshot.currentCall));
|
||||
if (snapshot.selectedModel) {
|
||||
this.selectedModel.set(snapshot.selectedModel);
|
||||
}
|
||||
|
|
@ -588,6 +662,12 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.isActiveCall(snapshot.currentCall)) {
|
||||
this.sessionLoading.set(false);
|
||||
this.startPolling(snapshot.currentCall.id, snapshot.currentCall.sessionId || snapshot.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sessionLoading.set(true);
|
||||
this.assistant.getSession(snapshot.sessionId).pipe(
|
||||
take(1),
|
||||
|
|
@ -612,6 +692,21 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
return this.sessionStore.flowKey(flowId);
|
||||
}
|
||||
|
||||
private isActiveCall(call: AssistantCallState | null): call is AssistantCallState {
|
||||
return call?.status === 'QUEUED' || call?.status === 'RUNNING';
|
||||
}
|
||||
|
||||
private isTerminalCall(call: AssistantCallState): boolean {
|
||||
return call.status === 'COMPLETED' || call.status === 'FAILED' || call.status === 'CANCELLED';
|
||||
}
|
||||
|
||||
private clearSnapshot(flowKey: string) {
|
||||
if (flowKey === this.activeFlowKey) {
|
||||
this.skipDestroySnapshot = true;
|
||||
}
|
||||
this.sessionStore.clearSnapshot(flowKey);
|
||||
}
|
||||
|
||||
private persistSnapshot(flowKey = this.activeFlowKey ?? this.resolveFlowKey(this.currentFlow()?.id ?? null)) {
|
||||
this.sessionStore.setSnapshot(flowKey, {
|
||||
sessionId: this.sessionState()?.id ?? null,
|
||||
|
|
@ -621,7 +716,10 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
quickPromptsOpen: this.quickPromptsOpen(),
|
||||
localMessages: this.localMessages(),
|
||||
currentCall: this.currentCall(),
|
||||
sessionState: this.sessionState()
|
||||
sessionState: this.sessionState(),
|
||||
assistantErrorMessage: this.assistantErrorMessage(),
|
||||
lastFailedPrompt: this.lastFailedPrompt(),
|
||||
lastSubmittedPrompt: this.lastSubmittedPrompt()
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -668,6 +766,8 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
return 'Finalizing flow...';
|
||||
case 'failed':
|
||||
return 'Assistant request failed.';
|
||||
case 'cancelled':
|
||||
return 'Assistant request cancelled.';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
}
|
||||
if (changes['flowData'] && this.rete) {
|
||||
setEditorGlobalInputs(this.rete.editor, this.flowData().globalInputs ?? []);
|
||||
if (!this.readonly() && !this.isEditorGraphInSync()) {
|
||||
void this.reloadEditor();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.readonly() && changes['flowData']) {
|
||||
void this.syncReadonlyFlowData();
|
||||
|
|
@ -535,6 +539,11 @@ export class ReteEditor implements OnChanges, OnDestroy {
|
|||
this.flowChanged.emit(updatedData);
|
||||
}
|
||||
|
||||
private isEditorGraphInSync(): boolean {
|
||||
if (!this.rete) return false;
|
||||
return JSON.stringify(exportGraph(this.rete.editor)) === JSON.stringify(this.flowData());
|
||||
}
|
||||
|
||||
private canStartSelection(target: EventTarget | null) {
|
||||
if (!this.selectionModeActive) return false;
|
||||
const element = target instanceof HTMLElement ? target : null;
|
||||
|
|
|
|||
Loading…
Reference in New Issue