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:
@@ -13,6 +13,9 @@ import '../../../core/constants.dart';
|
||||
const _kUserBubbleColor = Color(0xFFD4929A);
|
||||
const _kBannerColor = Color(0xFFC4868F);
|
||||
const _kAccentPink = Color(0xFFBE7C8A);
|
||||
// Phase 4 — voice-call mode badge background. Mirrors `HaloTokens.accent`
|
||||
// from the customer app palette so both apps render the same pill color.
|
||||
const _kVoiceCallPillColor = Color(0xFFF7B26A);
|
||||
|
||||
class MitraChatScreen extends ConsumerStatefulWidget {
|
||||
final String sessionId;
|
||||
@@ -117,7 +120,23 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
icon: const Icon(Icons.chevron_left, size: 28),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: Text(widget.customerName),
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.customerName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (chatState is MitraChatConnectedData &&
|
||||
chatState.mode == SessionMode.call) ...[
|
||||
const SizedBox(width: 8),
|
||||
_buildVoiceCallPill(),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (chatState is MitraChatConnectedData) _buildTopicToggle(chatState),
|
||||
if (chatState is MitraChatConnectedData && chatState.remainingSeconds != null)
|
||||
@@ -145,6 +164,24 @@ class _MitraChatScreenState extends ConsumerState<MitraChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVoiceCallPill() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: const BoxDecoration(
|
||||
color: _kVoiceCallPillColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(9999)),
|
||||
),
|
||||
child: const Text(
|
||||
'📞 Voice Call',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSensitivityHeader() {
|
||||
const theme = SensitivityTheme.sensitive;
|
||||
return Container(
|
||||
|
||||
Reference in New Issue
Block a user