mirror of
https://github.com/we-promise/sure.git
synced 2026-04-09 15:24:48 +00:00
* feat(mobile): Add animated TypingIndicator widget for AI chat responses Replaces the static CircularProgressIndicator + "AI is thinking..." text with an animated TypingIndicator showing pulsing dots while the AI generates a response. Respects the app color scheme so it works in light and dark themes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix: Normalize stagger progress to [0,1) in TypingIndicator to prevent negative opacity Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(mobile): fix typing indicator visibility and run pub get The typing indicator was only visible for the duration of the HTTP POST (~instant) because it was tied to `isSendingMessage`. It now tracks the full AI response lifecycle via a new `isWaitingForResponse` state that stays true through polling until the response stabilises. - Add `isWaitingForResponse` to ChatProvider; set on poll start, clear on poll stop with notifyListeners so the UI reacts correctly - Move TypingIndicator inside the ListView as an assistant bubble so it scrolls naturally with the conversation - Add provider listener that auto-scrolls on every update while waiting for a response - Redesign TypingIndicator: 3-dot sequential bounce animation (classic chat style) replacing the simultaneous fade * feat(mobile): overhaul new-chat flow and fix typing indicator bugs chat is created lazily on first send, eliminating all pre-conversation flashes and crashes - Inject user message locally into _currentChat immediately on createChat so it renders before the first poll completes - Hide thinking indicator the moment the first assistant content arrives (was waiting one extra 2s poll cycle before disappearing) - Fix double-spinner on new chat: remove manual showDialog spinner and use a local _isCreating flag on the FAB instead * fix(mboile) : address PR review — widget lifecycle safety and new-chat regression * Fic(mobile): Add mounted check in post-frame callback --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.0 KiB
Dart
100 lines
3.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Animated 3-dot "Thinking..." indicator shown while the AI generates a response.
|
|
/// Each dot bounces up in sequence, giving the classic chat typing indicator feel.
|
|
class TypingIndicator extends StatefulWidget {
|
|
const TypingIndicator({super.key});
|
|
|
|
@override
|
|
State<TypingIndicator> createState() => _TypingIndicatorState();
|
|
}
|
|
|
|
class _TypingIndicatorState extends State<TypingIndicator>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: const Duration(milliseconds: 1200),
|
|
vsync: this,
|
|
)..repeat();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final dotColor = colorScheme.onSurfaceVariant;
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'Thinking',
|
|
style: TextStyle(
|
|
color: colorScheme.onSurfaceVariant,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
SizedBox(
|
|
height: 20,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: List.generate(3, (index) {
|
|
return AnimatedBuilder(
|
|
animation: _controller,
|
|
builder: (context, child) {
|
|
final offset = _dotOffset(index, _controller.value);
|
|
return Padding(
|
|
padding: EdgeInsets.only(right: index < 2 ? 5 : 0),
|
|
child: Transform.translate(
|
|
offset: Offset(0, offset),
|
|
child: Container(
|
|
width: 7,
|
|
height: 7,
|
|
decoration: BoxDecoration(
|
|
color: dotColor.withValues(alpha: 0.75),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Returns the vertical offset (px) for a dot at [index] given the
|
|
/// controller's current [value] in [0, 1).
|
|
/// Each dot is delayed by 1/3 of the cycle so they bounce in sequence.
|
|
double _dotOffset(int index, double value) {
|
|
const bounceHeight = 5.0;
|
|
const dotCount = 3;
|
|
final phase = (value - index / dotCount + 1.0) % 1.0;
|
|
|
|
// Bounce occupies the first 40% of each dot's phase; rest is idle.
|
|
if (phase < 0.2) {
|
|
// Rising: 0 → peak
|
|
return -bounceHeight * (phase / 0.2);
|
|
} else if (phase < 0.4) {
|
|
// Falling: peak → 0
|
|
return -bounceHeight * (1.0 - (phase - 0.2) / 0.2);
|
|
}
|
|
return 0.0;
|
|
}
|
|
}
|