Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 195 additions & 74 deletions lib/components/enhanced_loading_indicator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,124 +5,187 @@ import 'package:vide_cli/constants/text_opacity.dart';
import 'package:vide_cli/theme/theme.dart';

class EnhancedLoadingIndicator extends StatefulComponent {
const EnhancedLoadingIndicator({super.key});
/// When the current response started (for elapsed time display)
final DateTime? responseStartTime;

/// Current output token count (for token counter display)
final int? outputTokens;

/// Dynamic loading words generated by Haiku based on user's message.
/// If provided and non-empty, cycles through these instead of static messages.
final List<String>? dynamicWords;

const EnhancedLoadingIndicator({
super.key,
this.responseStartTime,
this.outputTokens,
this.dynamicWords,
});

@override
State<EnhancedLoadingIndicator> createState() =>
_EnhancedLoadingIndicatorState();
}

class _EnhancedLoadingIndicatorState extends State<EnhancedLoadingIndicator> {
static final _activities = [
'Calibrating quantum flux capacitors',
'Teaching neurons to dance',
'Counting electrons backwards',
'Negotiating with the GPU',
'Consulting the ancient scrolls',
'Reticulating splines',
'Downloading more RAM',
'Asking the rubber duck for advice',
'Warming up the hamster wheel',
'Aligning chakras with CPU cores',
'Bribing the cache',
'Summoning the algorithm spirits',
'Untangling virtual spaghetti',
'Polishing the bits',
'Feeding the neural network',
'Optimizing the optimization',
'Reversing entropy temporarily',
'Borrowing cycles from the future',
'Debugging the debugger',
'Compiling thoughts into words',
'Defragmenting consciousness',
'Garbage collecting bad ideas',
'Spinning up the thinking wheels',
'Caffeinating the processors',
'Consulting my digital crystal ball',
'Performing ritual sacrifices to the memory gods',
'Translating binary to feelings',
'Mining for the perfect response',
'Charging up the synaptic batteries',
'Dusting off old neural pathways',
'Waking up sleeping threads',
'Organizing the chaos matrix',
'Calibrating sarcasm levels',
'Loading witty responses',
'Searching the void for answers',
'Petting the server hamsters',
'Adjusting reality parameters',
'Synchronizing with the cosmos',
'Downloading wisdom from the cloud',
'Recursively thinking about thinking',
'Contemplating the meaning of bits',
'Herding digital cats',
'Shaking the magic 8-ball',
'Tickling the silicon',
'Whispering sweet nothings to the ALU',
'Parsing the unparseable',
'Finding the missing semicolon',
'Dividing by zero carefully',
'Counting to infinity twice',
'Unscrambling quantum eggs',
static final _brailleFrames = [
'\u280b', // ⠋
'\u2819', // ⠙
'\u2839', // ⠹
'\u2838', // ⠸
'\u283c', // ⠼
'\u2834', // ⠴
'\u2826', // ⠦
'\u2827', // ⠧
'\u2807', // ⠇
'\u280f', // ⠏
];

static final _brailleFrames = [
'⠋',
'⠙',
'⠹',
'⠸',
'⠼',
'⠴',
'⠦',
'⠧',
'⠇',
'⠏',
static final _fallbackMessages = [
'Reticulating splines',
'Consulting the oracle',
'Herding electrons',
'Polishing the bits',
'Warming up the neurons',
'Untangling the logic',
'Feeding the hamsters',
'Brewing some code',
'Thinking really hard',
'Consulting ancient scrolls',
'Channeling the machine spirit',
'Waking up the minions',
'Spinning up the gerbils',
'Pondering the imponderables',
'Aligning the chakras',
'Summoning the algorithms',
'Charging the flux capacitor',
'Parsing the cosmos',
'Calibrating the quantum',
'Assembling the thoughts',
];

final _random = Random();
Timer? _animationTimer;
Timer? _activityTimer;
Timer? _messageTimer;
int _frameIndex = 0;
int _activityIndex = 0;
int _shimmerPosition = 0;
String _currentMessage = '';
int _dynamicWordIndex = 0;
bool _hasDynamicWords = false;

@override
void initState() {
super.initState();
_activityIndex = _random.nextInt(_activities.length);
_initializeMessage();

// Animation timer for braille and shimmer
_animationTimer = Timer.periodic(Duration(milliseconds: 100), (_) {
setState(() {
_frameIndex = (_frameIndex + 1) % _brailleFrames.length;
_shimmerPosition = (_shimmerPosition + 1);
if (_shimmerPosition >= _activities[_activityIndex].length + 5) {
if (_shimmerPosition >= _currentMessage.length + 5) {
_shimmerPosition = -5;
}
});
});
}

void _initializeMessage() {
final dynamicWords = component.dynamicWords;
if (dynamicWords != null && dynamicWords.isNotEmpty) {
// We have dynamic words - use them and start cycling
_hasDynamicWords = true;
_dynamicWordIndex = 0;
_currentMessage = dynamicWords[0];
_startMessageTimer();
} else {
// No dynamic words yet - pick one random static message and stick with it
_hasDynamicWords = false;
final random = Random();
_currentMessage = _fallbackMessages[random.nextInt(_fallbackMessages.length)];
// Don't start timer - we stay on this message until dynamic words arrive
}
}

// Activity change timer
_activityTimer = Timer.periodic(Duration(seconds: 4), (_) {
void _startMessageTimer() {
_messageTimer?.cancel();
// Cycle through dynamic words every 10 seconds
_messageTimer = Timer.periodic(Duration(seconds: 10), (_) {
setState(() {
_activityIndex = _random.nextInt(_activities.length);
_pickNextDynamicWord();
_shimmerPosition = -5;
});
});
}

@override
void didUpdateComponent(EnhancedLoadingIndicator oldComponent) {
super.didUpdateComponent(oldComponent);

// If dynamic words just became available, start using them
if (component.dynamicWords != null &&
component.dynamicWords!.isNotEmpty &&
!_hasDynamicWords) {
_hasDynamicWords = true;
_dynamicWordIndex = 0;
_currentMessage = component.dynamicWords![0];
_shimmerPosition = -5;
_startMessageTimer();
}
}

void _pickNextDynamicWord() {
final dynamicWords = component.dynamicWords;
if (dynamicWords != null && dynamicWords.isNotEmpty) {
_dynamicWordIndex = (_dynamicWordIndex + 1) % dynamicWords.length;
_currentMessage = dynamicWords[_dynamicWordIndex];
}
}

@override
void dispose() {
_animationTimer?.cancel();
_activityTimer?.cancel();
_messageTimer?.cancel();
super.dispose();
}

String _formatElapsedTime() {
if (component.responseStartTime == null) return '';
final elapsed = DateTime.now().difference(component.responseStartTime!);
final seconds = elapsed.inSeconds;
if (seconds < 60) {
return '${seconds}s';
} else {
final minutes = elapsed.inMinutes;
final remainingSeconds = seconds % 60;
return '${minutes}m ${remainingSeconds}s';
}
}

String _formatTokens(int tokens) {
if (tokens >= 1000) {
final k = tokens / 1000;
return '${k.toStringAsFixed(1)}k';
}
return tokens.toString();
}

@override
Component build(BuildContext context) {
final theme = VideTheme.of(context);
final braille = _brailleFrames[_frameIndex];
final activity = _activities[_activityIndex];

// Build the status info parts
final statusParts = <String>[];

// Add elapsed time if we have a start time
if (component.responseStartTime != null) {
statusParts.add(_formatElapsedTime());
}

// Add token count if available
if (component.outputTokens != null && component.outputTokens! > 0) {
statusParts.add('\u2193 ${_formatTokens(component.outputTokens!)} tokens');
}

return Row(
children: [
Expand All @@ -135,7 +198,17 @@ class _EnhancedLoadingIndicatorState extends State<EnhancedLoadingIndicator> {
),
SizedBox(width: 1),
// Activity text with shimmer
_buildShimmerText(context, activity),
_buildShimmerText(context, _currentMessage),
// Show elapsed time and tokens
if (statusParts.isNotEmpty) ...[
SizedBox(width: 1),
Text(
'(${statusParts.join(' \u00b7 ')})',
style: TextStyle(
color: Colors.white.withOpacity(TextOpacity.tertiary),
),
),
],
],
);
}
Expand All @@ -160,3 +233,51 @@ class _EnhancedLoadingIndicatorState extends State<EnhancedLoadingIndicator> {
return Row(mainAxisSize: MainAxisSize.min, children: components);
}
}

/// A simpler thinking indicator that just shows duration
class ThinkingIndicator extends StatefulComponent {
final DateTime? startTime;

const ThinkingIndicator({super.key, this.startTime});

@override
State<ThinkingIndicator> createState() => _ThinkingIndicatorState();
}

class _ThinkingIndicatorState extends State<ThinkingIndicator> {
Timer? _timer;
int _seconds = 0;

@override
void initState() {
super.initState();
_updateSeconds();
_timer = Timer.periodic(Duration(seconds: 1), (_) {
setState(() {
_updateSeconds();
});
});
}

void _updateSeconds() {
if (component.startTime != null) {
_seconds = DateTime.now().difference(component.startTime!).inSeconds;
}
}

@override
void dispose() {
_timer?.cancel();
super.dispose();
}

@override
Component build(BuildContext context) {
return Text(
'Thought for ${_seconds}s',
style: TextStyle(
color: Colors.white.withOpacity(TextOpacity.tertiary),
),
);
}
}
Loading
Loading