diff --git a/src/app/shared/flow-assistant/flow-assistant.css b/src/app/shared/flow-assistant/flow-assistant.css index 242e05c..374d96b 100644 --- a/src/app/shared/flow-assistant/flow-assistant.css +++ b/src/app/shared/flow-assistant/flow-assistant.css @@ -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; diff --git a/src/app/shared/flow-assistant/flow-assistant.html b/src/app/shared/flow-assistant/flow-assistant.html index 8b952d2..7dc8455 100644 --- a/src/app/shared/flow-assistant/flow-assistant.html +++ b/src/app/shared/flow-assistant/flow-assistant.html @@ -11,13 +11,13 @@ - @if (activePhaseLabel()) { + @if (activePhaseLabel() && !progressOnlyMode()) {
{{ activePhaseLabel() }}
} - @if (assistantBusy() || createModalProgressOnly()) { + @if (assistantBusy() || progressOnlyMode()) {
@@ -38,9 +38,23 @@
} + @if (assistantErrorMessage() && !assistantBusy()) { +
+

{{ assistantErrorMessage() }}

+ +
+ } - @if (!createModalProgressOnly()) { + + @if (!progressOnlyMode()) {

Quick prompts

@@ -98,7 +112,7 @@
- @if (!createModalProgressOnly()) { + @if (!progressOnlyMode()) {
Prompt diff --git a/src/app/shared/flow-assistant/flow-assistant.ts b/src/app/shared/flow-assistant/flow-assistant.ts index 3ebaae6..33fdbf2 100644 --- a/src/app/shared/flow-assistant/flow-assistant.ts +++ b/src/app/shared/flow-assistant/flow-assistant.ts @@ -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(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(null); + readonly lastFailedPrompt = signal(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