feat: add error handling and retry functionality to flow assistant

- Added assistant error message state with retry capability
- Introduced assistantErrorMessage, lastFailedPrompt, and lastSubmittedPrompt signals
- Created progressOnlyMode computed signal for unified progress display
- Implemented handleAssistantErrorWithRetry method for consistent error handling
- Added retry button with conditional rendering in template
- Enhanced error states with improved styling for error cards
- Extracted sendPrompt logic to support both new prompts and retries
- Improved session reload with failure state handling
This commit is contained in:
Lucio Lelii 2026-05-13 16:47:13 +02:00
parent 877844fca0
commit f558df3aa8
3 changed files with 81 additions and 16 deletions

View File

@ -105,6 +105,24 @@
linear-gradient(180deg, rgba(240, 253, 250, 0.96), rgba(248, 250, 252, 0.96));
}
.assistant-error-card {
border-color: rgba(185, 28, 28, 0.24);
background: linear-gradient(180deg, rgba(254, 242, 242, 0.96), rgba(255, 255, 255, 0.96));
}
.assistant-error-message {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: #7f1d1d;
}
.assistant-retry {
margin-top: 10px;
border-color: #ef4444;
color: #7f1d1d;
}
.assistant-busy-row {
display: flex;
align-items: flex-start;

View File

@ -11,13 +11,13 @@
@if (activePhaseLabel()) {
@if (activePhaseLabel() && !progressOnlyMode()) {
<div class="assistant-intent-row">
<span class="assistant-intent">{{ activePhaseLabel() }}</span>
</div>
}
@if (assistantBusy() || createModalProgressOnly()) {
@if (assistantBusy() || progressOnlyMode()) {
<section class="assistant-card assistant-busy-card">
<div class="assistant-busy-row">
<span class="assistant-busy-spinner" aria-hidden="true"></span>
@ -38,9 +38,23 @@
</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 (!createModalProgressOnly()) {
@if (!progressOnlyMode()) {
<section class="assistant-card assistant-starters">
<div class="assistant-starters-head">
<p class="assistant-label">Quick prompts</p>
@ -98,7 +112,7 @@
</div>
@if (!createModalProgressOnly()) {
@if (!progressOnlyMode()) {
<form class="assistant-composer" (ngSubmit)="submitPrompt()">
<mat-form-field appearance="outline">
<mat-label>Prompt</mat-label>

View File

@ -41,6 +41,7 @@ export class FlowAssistant implements OnInit, OnDestroy {
private activeFlowKey: string | null = null;
private lastAutoScrollKey = '';
private readonly createModalFlowKey = `__assistant:create-modal:${crypto.randomUUID()}`;
private static readonly STANDARD_ASSISTANT_ERROR = 'Something went wrong while processing your workflow request. Please try again.';
readonly assistantConfig = signal<AssistantConfig | null>(null);
readonly variant = input<'aside' | 'create-modal'>('aside');
@ -57,6 +58,9 @@ export class FlowAssistant implements OnInit, OnDestroy {
readonly selectedModel = signal('');
readonly modelPickerOpen = signal(false);
readonly quickPromptsOpen = signal(true);
readonly assistantErrorMessage = signal<string | null>(null);
readonly lastFailedPrompt = signal<string | null>(null);
readonly lastSubmittedPrompt = signal('');
readonly editorHasOpenFlow = computed(() => !!this.currentFlow());
readonly editorHasNonEmptyFlow = computed(() => this.hasMeaningfulFlow(this.currentFlow()));
readonly sessionHasDraft = computed(() => !!this.currentDraft());
@ -66,6 +70,15 @@ export class FlowAssistant implements OnInit, OnDestroy {
readonly createModalProgressOnly = computed(() =>
this.isCreateModal() && this.createPromptSubmitted()
);
readonly refineProgressOnly = computed(() =>
!this.isCreateModal() && !this.canOfferCreate() && this.assistantBusy()
);
readonly progressOnlyMode = computed(() =>
this.createModalProgressOnly() || this.refineProgressOnly()
);
readonly canRetryLastPrompt = computed(() =>
!this.assistantBusy() && !!this.lastFailedPrompt() && !!this.sessionState()?.id
);
readonly assistantModeLabel = computed(() => this.canOfferCreate() ? 'Create with assistant' : 'Refine with assistant');
readonly assistantModeDescription = computed(() => {
if (this.canOfferCreate()) {
@ -224,22 +237,35 @@ export class FlowAssistant implements OnInit, OnDestroy {
submitPrompt() {
const content = this.prompt().trim();
this.sendPrompt(content);
}
retryLastPrompt() {
const failedPrompt = this.lastFailedPrompt()?.trim() ?? '';
if (!failedPrompt) return;
this.sendPrompt(failedPrompt);
}
private sendPrompt(content: string) {
const normalizedContent = content.trim();
const sessionId = this.sessionState()?.id;
if (!content || this.assistantBusy() || !sessionId) return;
if (!normalizedContent || this.assistantBusy() || !sessionId) return;
if (this.isCreateModal()) this.createPromptSubmitted.set(true);
this.requestPending.set(true);
this.assistantErrorMessage.set(null);
this.lastSubmittedPrompt.set(normalizedContent);
this.prompt.set('');
this.localMessages.set([
{
id: crypto.randomUUID(),
role: 'user',
content
content: normalizedContent
}
]);
this.persistSnapshot();
this.assistant.sendMessage(sessionId, { message: content }).pipe(
this.assistant.sendMessage(sessionId, { message: normalizedContent }).pipe(
take(1)
).subscribe({
next: ({ callId }) => {
@ -256,8 +282,7 @@ export class FlowAssistant implements OnInit, OnDestroy {
error: (err) => {
console.error('Assistant send message failed', err);
this.requestPending.set(false);
this.createPromptSubmitted.set(false);
this.pushLocalAssistantMessage('The assistant request failed.');
this.handleAssistantErrorWithRetry(normalizedContent);
}
});
}
@ -350,18 +375,18 @@ export class FlowAssistant implements OnInit, OnDestroy {
if (callState.status === 'COMPLETED' && callState.flowResult?.flow) {
this.syncDraftToEditor(this.flowResultToDraft(callState.flowResult));
}
void this.reloadSession(sessionId, callState.status === 'FAILED' ? callState.errorMessage : undefined);
void this.reloadSession(sessionId, callState.status === 'FAILED');
}
},
error: (err) => {
console.error('Assistant call polling failed', err);
this.stopPolling();
this.pushLocalAssistantMessage('Polling the assistant call failed.');
this.handleAssistantErrorWithRetry(this.lastSubmittedPrompt());
}
});
}
private async reloadSession(sessionId: string, failureMessage?: string) {
private async reloadSession(sessionId: string, hasFailedCall = false) {
this.assistant.getSession(sessionId).pipe(
take(1)
).subscribe({
@ -369,19 +394,27 @@ export class FlowAssistant implements OnInit, OnDestroy {
this.applySessionState(session);
this.currentCall.set(null);
this.persistSnapshot();
if (failureMessage) {
this.createPromptSubmitted.set(false);
this.pushLocalAssistantMessage(failureMessage);
if (hasFailedCall) {
this.handleAssistantErrorWithRetry(this.lastSubmittedPrompt());
}
},
error: (err) => {
console.error('Assistant session refresh failed', err);
this.currentCall.set(null);
this.pushLocalAssistantMessage('Unable to refresh the assistant session.');
this.handleAssistantErrorWithRetry(this.lastSubmittedPrompt());
}
});
}
private handleAssistantErrorWithRetry(promptForRetry: string) {
const normalizedPrompt = String(promptForRetry ?? '').trim();
this.createPromptSubmitted.set(false);
this.assistantErrorMessage.set(FlowAssistant.STANDARD_ASSISTANT_ERROR);
this.lastFailedPrompt.set(normalizedPrompt || null);
this.pushLocalAssistantMessage(FlowAssistant.STANDARD_ASSISTANT_ERROR);
this.persistSnapshot();
}
private applySessionState(session: AssistantSessionState, options?: { clearLocalMessages?: boolean; syncDraftToEditor?: boolean }) {
const normalizedSession = session.messages.length
? session