Phase 4 Stage 6: chat-room countdown UX + voice-call mode pill
Customer chat screen:
- Voice-call header pill (mode == 'call' renders accent-colored pill;
chat mode renders no pill).
- HaloSnackbar fires once per session at 180s remaining ('sisa 3 menit
lagi ya 🤍'), driven by the backend session_warning WS event.
- Last-2-min danger styling: timer pill flips to HaloTokens.danger +
bold JetBrainsMono when remaining <= 120s.
- Floating ChatExpiredBanner widget injected above the input bar when
remaining hits 0 in closing-grace state. perpanjang -> existing
pricing bottom sheet.
- pricing_bottom_sheet.dart rewritten to the 5-option layout with
chat|call mode toggle (mirrors duration-pick from Stage 3).
Mitra chat screen: voice-call header pill only (no countdown UX per PRD).
Backend:
- session.service.js getSessionById JOINs payment_sessions so mode +
expires_at ship in /api/shared/chat/:id/info.
- session-timer.service.js onThreeMinuteWarning now emits expires_at +
remaining_seconds for client resync.
- Dev-only POST /internal/_test/force-session-expires-at clears the
3-min flag, reschedules the timer, and broadcasts WS resync. Lets
the Maestro flow drive 175s -> 90s -> 0s without waiting live.
New chatRemainingSeconds StreamProvider derived from expiresAt, fed by
session_warning / session_timer / session_expired resync messages
(plan referenced a secondsLeftProvider that didn't actually exist).
Maestro 06_chat_countdown.yaml + force_session_expires_at.js helper.
Out of scope: meet.google.com URL launching - url_launcher isn't a
client_app dependency and message bubbles render plain Text. Defer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,21 @@ class ExtensionStatus {
|
||||
ExtensionStatus._();
|
||||
}
|
||||
|
||||
/// Session mode — chat or voice call. Mirrors backend `payment_sessions.mode`
|
||||
/// (added in Phase 4 stage 1). A `call` session is functionally a chat with a
|
||||
/// "voice call" badge and (eventually) a Meet link the mitra pastes manually;
|
||||
/// no real audio transport is built yet.
|
||||
enum SessionMode {
|
||||
chat('chat'),
|
||||
call('call');
|
||||
|
||||
final String value;
|
||||
const SessionMode(this.value);
|
||||
|
||||
static SessionMode fromString(String? v) =>
|
||||
values.firstWhere((e) => e.value == v, orElse: () => SessionMode.chat);
|
||||
}
|
||||
|
||||
/// Session topic sensitivity
|
||||
enum TopicSensitivity {
|
||||
regular('regular'),
|
||||
@@ -101,6 +116,9 @@ class WsMessage {
|
||||
static const sessionCompleted = 'session_completed';
|
||||
static const sessionPaused = 'session_paused';
|
||||
static const sessionResumed = 'session_resumed';
|
||||
// Phase 4 — soft countdown warning (`kind: 'three_minutes_left'`).
|
||||
// Customer-only: mitra never sees a countdown.
|
||||
static const sessionWarning = 'session_warning';
|
||||
|
||||
// Extension
|
||||
static const extensionRequest = 'extension_request';
|
||||
|
||||
Reference in New Issue
Block a user