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:
parent
877844fca0
commit
f558df3aa8
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue