From b07914936c0c55882fb5cd14111a696969ad50bd Mon Sep 17 00:00:00 2001 From: master <> Date: Tue, 7 Apr 2026 15:33:47 +0300 Subject: [PATCH] feat(stella-helper): long-press close UX + minimum thinking animation Close button now uses quick-click to dismiss and long-press to reveal mute options. Chat stream emits 'start' immediately so the mascot thinking animation plays during the HTTP wait with an 800ms minimum duration. User preferences page gains a tutorial reset button. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/advisory-ai/chat/chat.service.ts | 6 +- .../user-preferences-page.component.ts | 8 ++ .../stella-helper/stella-assistant.service.ts | 15 ++- .../stella-helper/stella-helper.component.ts | 111 ++++++++++++++++-- .../stella-preferences.service.ts | 35 ++++++ 5 files changed, 159 insertions(+), 16 deletions(-) diff --git a/src/Web/StellaOps.Web/src/app/features/advisory-ai/chat/chat.service.ts b/src/Web/StellaOps.Web/src/app/features/advisory-ai/chat/chat.service.ts index abf49df04..77f099965 100644 --- a/src/Web/StellaOps.Web/src/app/features/advisory-ai/chat/chat.service.ts +++ b/src/Web/StellaOps.Web/src/app/features/advisory-ai/chat/chat.service.ts @@ -181,6 +181,10 @@ export class ChatService { // Use the legacy conversation turns endpoint routed via Valkey RPC. // The gateway maps /api/v1/advisory-ai/* to the backend via Valkey. // HandleAddTurn returns JSON (not SSE), so we simulate streaming client-side. + + // Emit 'start' immediately so the mascot thinking animation plays during the HTTP wait + this.streamEvents$.next({ event: 'start', data: {} } as any); + this.http.post(`${API_BASE}/conversations/${conversationId}/turns`, { content: message, }).subscribe({ @@ -190,8 +194,6 @@ export class ChatService { const words = content.split(' '); let accumulated = ''; - this.streamEvents$.next({ event: 'start', data: {} } as any); - let i = 0; const interval = setInterval(() => { if (i < words.length) { diff --git a/src/Web/StellaOps.Web/src/app/features/settings/user-preferences/user-preferences-page.component.ts b/src/Web/StellaOps.Web/src/app/features/settings/user-preferences/user-preferences-page.component.ts index 6417c1a24..92ba20929 100644 --- a/src/Web/StellaOps.Web/src/app/features/settings/user-preferences/user-preferences-page.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/settings/user-preferences/user-preferences-page.component.ts @@ -325,6 +325,14 @@ type LocaleSaveState = 'idle' | 'saving' | 'saved' | 'syncFailed'; + +
+
+ Reset page tutorials + Re-show "About this page" panels on all pages +
+ +
} } diff --git a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-assistant.service.ts b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-assistant.service.ts index 20d217316..3ae4c5413 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-assistant.service.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-assistant.service.ts @@ -123,6 +123,8 @@ export class StellaAssistantService { // ---- Chat animation state ---- readonly chatAnimationState = signal<'idle' | 'thinking' | 'typing' | 'done' | 'error'>('idle'); private chatStreamSub?: Subscription; + private thinkingStartedAt = 0; + private readonly MIN_THINKING_MS = 800; // ---- Session persistence ---- readonly activeConversationId = signal(null); @@ -287,16 +289,25 @@ export class StellaAssistantService { // Connect mascot animation to chat stream events this.chatStreamSub?.unsubscribe(); this.chatAnimationState.set('idle'); + this.thinkingStartedAt = 0; this.chatStreamSub = this.chatService.streamEvents.subscribe((event) => { const type = (event as any).event ?? ''; switch (type) { case 'start': case 'progress': this.chatAnimationState.set('thinking'); + this.thinkingStartedAt = Date.now(); break; - case 'token': - this.chatAnimationState.set('typing'); + case 'token': { + // Enforce minimum thinking duration so the animation is visible + const elapsed = Date.now() - this.thinkingStartedAt; + if (this.chatAnimationState() === 'thinking' && elapsed < this.MIN_THINKING_MS) { + setTimeout(() => this.chatAnimationState.set('typing'), this.MIN_THINKING_MS - elapsed); + } else { + this.chatAnimationState.set('typing'); + } break; + } case 'done': this.chatAnimationState.set('done'); setTimeout(() => this.chatAnimationState.set('idle'), 2000); diff --git a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper.component.ts index 5e7f74768..53678534f 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper.component.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper.component.ts @@ -28,6 +28,7 @@ import { const IDLE_ANIMATION_INTERVAL = 12_000; // wiggle every 12s const AUTO_TIP_INTERVAL = 45_000; // suggest next tip every 45s +const LONG_PRESS_MS = 500; // hold threshold before showing mute menu /** * StellaHelperComponent — Clippy-style contextual help assistant. @@ -92,19 +93,28 @@ const AUTO_TIP_INTERVAL = 45_000; // suggest next tip every 45s @if (bubbleOpen() && !assistantDrawer.isOpen()) {
- +
@if (muteDropdownOpen()) {