diff --git a/src/app/models/assistant.ts b/src/app/models/assistant.ts index 2cc3c96..88d0163 100644 --- a/src/app/models/assistant.ts +++ b/src/app/models/assistant.ts @@ -46,7 +46,7 @@ export type AssistantConfig = { availableModelsRetrieverUrl: string; }; -export type AssistantCallStatus = 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED'; +export type AssistantCallStatus = 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; export type AssistantCallPhase = | 'queued' @@ -58,7 +58,8 @@ export type AssistantCallPhase = | 'fixing' | 'explaining' | 'completed' - | 'failed'; + | 'failed' + | 'cancelled'; export type AssistantCallState = { id: string; diff --git a/src/app/services/assistant/assistant-call.base.ts b/src/app/services/assistant/assistant-call.base.ts index 6fb37e7..20eaf61 100644 --- a/src/app/services/assistant/assistant-call.base.ts +++ b/src/app/services/assistant/assistant-call.base.ts @@ -19,5 +19,7 @@ export abstract class AssistantCallServiceBase { abstract getCall(callId: string): Observable; + abstract cancelCall(callId: string): Observable; + abstract getSession(sessionId: string): Observable; } diff --git a/src/app/services/assistant/assistant-call.fake.ts b/src/app/services/assistant/assistant-call.fake.ts index 4826eb4..1b5511c 100644 --- a/src/app/services/assistant/assistant-call.fake.ts +++ b/src/app/services/assistant/assistant-call.fake.ts @@ -20,6 +20,7 @@ type FakeCallRecord = { phases: AssistantCallPhase[]; completed: boolean; failed: boolean; + cancelled: boolean; }; export class AssistantCallServiceFake extends AssistantCallServiceBase { @@ -83,7 +84,8 @@ export class AssistantCallServiceFake extends AssistantCallServiceBase { phaseIndex: 0, phases, completed: false, - failed: false + failed: false, + cancelled: false }); session.lastCallId = callId; @@ -96,7 +98,7 @@ export class AssistantCallServiceFake extends AssistantCallServiceBase { throw new Error(`Assistant call ${callId} not found`); } - if (!call.completed && !call.failed) { + if (!call.completed && !call.failed && !call.cancelled) { if (call.phaseIndex < call.phases.length - 1) { call.phaseIndex += 1; } else { @@ -109,23 +111,44 @@ export class AssistantCallServiceFake extends AssistantCallServiceBase { ? 'completed' : call.failed ? 'failed' - : call.phases[call.phaseIndex]; + : call.cancelled + ? 'cancelled' + : call.phases[call.phaseIndex]; return of({ id: call.id, sessionId: call.sessionId, status: call.failed ? 'FAILED' + : call.cancelled + ? 'CANCELLED' : call.completed ? 'COMPLETED' : call.phaseIndex === 0 ? 'QUEUED' : 'RUNNING', phase, + progressMessage: call.cancelled ? 'Assistant request cancelled' : undefined, errorMessage: call.failed ? 'Fake assistant call failed.' : undefined }); } + override cancelCall(callId: string): Observable { + const call = this.calls.get(callId); + if (!call) { + throw new Error(`Assistant call ${callId} not found`); + } + + call.cancelled = true; + return of({ + id: call.id, + sessionId: call.sessionId, + status: 'CANCELLED', + phase: 'cancelled', + progressMessage: 'Assistant request cancelled' + }); + } + override getSession(sessionId: string): Observable { const session = this.sessions.get(sessionId); if (!session) { diff --git a/src/app/services/assistant/assistant-call.ts b/src/app/services/assistant/assistant-call.ts index 3a5131c..3560045 100644 --- a/src/app/services/assistant/assistant-call.ts +++ b/src/app/services/assistant/assistant-call.ts @@ -55,6 +55,13 @@ export class AssistantCallService extends AssistantCallServiceBase { .pipe(map((raw) => mapAssistantCallState(raw))); } + override cancelCall(callId: string): Observable { + const encodedId = encodeURIComponent(callId); + return this.http + .put(`${environment.apiUrl}/assistant/calls/${encodedId}/cancel`, {}) + .pipe(map((raw) => mapAssistantCallState(raw))); + } + override getSession(sessionId: string): Observable { const encodedId = encodeURIComponent(sessionId); return this.http @@ -222,7 +229,7 @@ function mapValidationIssues(raw: unknown): AssistantValidationIssue[] { function mapCallStatus(raw: unknown): AssistantCallStatus { const normalized = typeof raw === 'string' ? raw.toUpperCase() : ''; - if (normalized === 'QUEUED' || normalized === 'RUNNING' || normalized === 'FAILED') return normalized; + if (normalized === 'QUEUED' || normalized === 'RUNNING' || normalized === 'FAILED' || normalized === 'CANCELLED') return normalized; return 'COMPLETED'; } @@ -239,6 +246,7 @@ function mapCallPhase(raw: unknown): AssistantCallPhase { case 'explaining': case 'completed': case 'failed': + case 'cancelled': return normalized; default: return 'queued'; diff --git a/src/app/services/assistant/assistant.ts b/src/app/services/assistant/assistant.ts index 6f0a9b9..400a262 100644 --- a/src/app/services/assistant/assistant.ts +++ b/src/app/services/assistant/assistant.ts @@ -29,6 +29,10 @@ export class AssistantService { return this.assistantCall.getCall(callId); } + cancelCall(callId: string) { + return this.assistantCall.cancelCall(callId); + } + getSession(sessionId: string) { return this.assistantCall.getSession(sessionId); } diff --git a/src/app/stores/assistant-session-store.ts b/src/app/stores/assistant-session-store.ts index c03e67f..ec86e8f 100644 --- a/src/app/stores/assistant-session-store.ts +++ b/src/app/stores/assistant-session-store.ts @@ -10,10 +10,14 @@ export type AssistantSessionSnapshot = { localMessages: AssistantChatMessage[]; currentCall: AssistantCallState | null; sessionState: AssistantSessionState | null; + assistantErrorMessage: string | null; + lastFailedPrompt: string | null; + lastSubmittedPrompt: string; }; @Injectable({ providedIn: 'root' }) export class AssistantSessionStore { + static readonly CREATE_MODAL_FLOW_KEY = '__assistant:create-modal'; private static readonly NO_FLOW_KEY = '__assistant:no-flow__'; private static readonly STORAGE_KEY = 'assistant-session-store:v1'; private static readonly STORAGE_TARGETS: Array<'localStorage' | 'sessionStorage'> = ['localStorage', 'sessionStorage']; @@ -32,6 +36,10 @@ export class AssistantSessionStore { return snapshot ? structuredClone(snapshot) : null; } + hasSnapshot(flowKey: string): boolean { + return this.snapshots.has(flowKey); + } + setSnapshot(flowKey: string, snapshot: AssistantSessionSnapshot) { this.snapshots.set(flowKey, structuredClone(snapshot)); this.persistToStorage(); @@ -44,6 +52,11 @@ export class AssistantSessionStore { this.persistToStorage(); } + clearSnapshot(flowKey: string) { + if (!this.snapshots.delete(flowKey)) return; + this.persistToStorage(); + } + private hydrateFromStorage() { const raw = this.readStoredPayload(); if (!raw) return; @@ -83,7 +96,16 @@ export class AssistantSessionStore { : null, sessionState: snapshot['sessionState'] && typeof snapshot['sessionState'] === 'object' ? this.normalizeSessionState(snapshot['sessionState']) - : null + : null, + assistantErrorMessage: typeof snapshot['assistantErrorMessage'] === 'string' + ? snapshot['assistantErrorMessage'] + : null, + lastFailedPrompt: typeof snapshot['lastFailedPrompt'] === 'string' + ? snapshot['lastFailedPrompt'] + : null, + lastSubmittedPrompt: typeof snapshot['lastSubmittedPrompt'] === 'string' + ? snapshot['lastSubmittedPrompt'] + : '' }; }