refactor: more logs

This commit is contained in:
cogwheel0
2025-09-25 23:22:48 +05:30
parent 9210b2155a
commit 3124bccfeb
20 changed files with 937 additions and 846 deletions

View File

@@ -16,11 +16,6 @@ import '../../tools/providers/tools_providers.dart';
import 'dart:async';
import '../../../core/utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'chat/providers');
}
const bool kSocketVerboseLogging = false;
// Chat messages for current conversation
@@ -105,7 +100,10 @@ class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
previous,
next,
) {
debugPrint('Conversation changed: ${previous?.id} -> ${next?.id}');
DebugLogger.log(
'Conversation changed: ${previous?.id} -> ${next?.id}',
scope: 'chat/providers',
);
// Only react when the conversation actually changes
if (previous?.id == next?.id) {
@@ -1886,7 +1884,10 @@ Please try sending the message again, or try without attachments.''',
);
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
} else if (e.toString().contains('404')) {
debugPrint('DEBUG: Model or endpoint not found (404)');
DebugLogger.log(
'Model or endpoint not found (404)',
scope: 'chat/providers',
);
final errorMessage = ChatMessage(
id: const Uuid().v4(),
role: 'assistant',
@@ -2005,7 +2006,10 @@ Future<void> pinConversation(
.set(activeConversation!.copyWith(pinned: pinned));
}
} catch (e) {
debugPrint('Error ${pinned ? 'pinning' : 'unpinning'} conversation: $e');
DebugLogger.log(
'Error ${pinned ? 'pinning' : 'unpinning'} conversation: $e',
scope: 'chat/providers',
);
rethrow;
}
}
@@ -2033,8 +2037,9 @@ Future<void> archiveConversation(
// Refresh conversations list to reflect the change
ref.invalidate(conversationsProvider);
} catch (e) {
debugPrint(
DebugLogger.log(
'Error ${archived ? 'archiving' : 'unarchiving'} conversation: $e',
scope: 'chat/providers',
);
// If server operation failed and we archived locally, restore the conversation
@@ -2060,7 +2065,7 @@ Future<String?> shareConversation(WidgetRef ref, String conversationId) async {
return shareId;
} catch (e) {
debugPrint('Error sharing conversation: $e');
DebugLogger.log('Error sharing conversation: $e', scope: 'chat/providers');
rethrow;
}
}
@@ -2081,7 +2086,7 @@ Future<void> cloneConversation(WidgetRef ref, String conversationId) async {
// Refresh conversations list to show the new conversation
ref.invalidate(conversationsProvider);
} catch (e) {
debugPrint('Error cloning conversation: $e');
DebugLogger.log('Error cloning conversation: $e', scope: 'chat/providers');
rethrow;
}
}

View File

@@ -1,4 +1,4 @@
import 'package:flutter/material.dart' hide debugPrint;
import 'package:flutter/material.dart';
import 'package:conduit/l10n/app_localizations.dart';
import '../../../core/widgets/error_boundary.dart';
import '../../../shared/widgets/optimized_list.dart';
@@ -48,11 +48,6 @@ import '../../../shared/widgets/model_avatar.dart';
import '../../../core/services/platform_service.dart' as ps;
import 'package:flutter/gestures.dart' show DragStartBehavior;
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'chat/page');
}
class ChatPage extends ConsumerStatefulWidget {
const ChatPage({super.key});
@@ -251,7 +246,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
if (!mounted) return;
ref.read(activeConversationProvider.notifier).set(welcomeConv);
debugPrint('Auto-loaded demo conversation');
DebugLogger.log('Auto-loaded demo conversation', scope: 'chat/page');
return;
}
@@ -266,7 +261,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
break;
}
debugPrint('Failed to auto-load demo conversation');
DebugLogger.log(
'Failed to auto-load demo conversation',
scope: 'chat/page',
);
}
@override
@@ -439,18 +437,19 @@ class _ChatPageState extends ConsumerState<ChatPage> {
);
} catch (e) {
if (!mounted) return;
debugPrint('Enqueue upload failed: $e');
DebugLogger.log('Enqueue upload failed: $e', scope: 'chat/page');
}
}
} catch (e) {
if (!mounted) return;
debugPrint('File selection failed: $e');
DebugLogger.log('File selection failed: $e', scope: 'chat/page');
}
}
void _handleImageAttachment({bool fromCamera = false}) async {
debugPrint(
'DEBUG: Starting image attachment process - fromCamera: $fromCamera',
DebugLogger.log(
'Starting image attachment process - fromCamera: $fromCamera',
scope: 'chat/page',
);
// Check if selected model supports vision
@@ -462,23 +461,26 @@ class _ChatPageState extends ConsumerState<ChatPage> {
final fileService = ref.read(fileAttachmentServiceProvider);
if (fileService == null) {
debugPrint('DEBUG: File service is null - cannot proceed');
DebugLogger.log(
'File service is null - cannot proceed',
scope: 'chat/page',
);
return;
}
try {
debugPrint('DEBUG: Picking image...');
DebugLogger.log('Picking image...', scope: 'chat/page');
final image = fromCamera
? await fileService.takePhoto()
: await fileService.pickImage();
if (image == null) {
debugPrint('DEBUG: No image selected');
DebugLogger.log('No image selected', scope: 'chat/page');
return;
}
debugPrint('DEBUG: Image selected: ${image.path}');
DebugLogger.log('Image selected: ${image.path}', scope: 'chat/page');
final imageSize = await image.length();
debugPrint('DEBUG: Image size: $imageSize bytes');
DebugLogger.log('Image size: $imageSize bytes', scope: 'chat/page');
// Validate file size (default 20MB limit like OpenWebUI)
if (!validateFileSize(imageSize, 20)) {
@@ -495,10 +497,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// Add image to the attachment list
ref.read(attachedFilesProvider.notifier).addFiles([image]);
debugPrint('DEBUG: Image added to attachment list');
DebugLogger.log('Image added to attachment list', scope: 'chat/page');
// Enqueue upload via task queue for unified retry/progress
debugPrint('DEBUG: Enqueueing image upload...');
DebugLogger.log('Enqueueing image upload...', scope: 'chat/page');
final activeConv = ref.read(activeConversationProvider);
try {
await ref
@@ -510,10 +512,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
fileSize: imageSize,
);
} catch (e) {
debugPrint('DEBUG: Enqueue image upload failed: $e');
DebugLogger.log('Enqueue image upload failed: $e', scope: 'chat/page');
}
} catch (e) {
debugPrint('DEBUG: Image attachment error: $e');
DebugLogger.log('Image attachment error: $e', scope: 'chat/page');
if (!mounted) return;
}
}
@@ -886,7 +888,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
userMessage.attachmentIds,
);
} catch (e) {
debugPrint('Regenerate failed: $e');
DebugLogger.log('Regenerate failed: $e', scope: 'chat/page');
}
}
@@ -1389,8 +1391,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
.read(activeConversationProvider.notifier)
.set(full);
} catch (e) {
debugPrint(
'DEBUG: Failed to refresh conversation: $e',
DebugLogger.log(
'Failed to refresh conversation: $e',
scope: 'chat/page',
);
}
}
@@ -2101,7 +2104,7 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
});
},
onDone: () {
debugPrint('DEBUG: VoiceInputSheet stream done');
DebugLogger.log('VoiceInputSheet stream done', scope: 'chat/page');
setState(() {
_isListening = false;
});
@@ -2112,7 +2115,10 @@ class _VoiceInputSheetState extends ConsumerState<_VoiceInputSheet> {
}
},
onError: (error) {
debugPrint('DEBUG: VoiceInputSheet stream error: $error');
DebugLogger.log(
'VoiceInputSheet stream error: $error',
scope: 'chat/page',
);
setState(() {
_isListening = false;
});

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart' hide debugPrint;
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show listEquals;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'dart:convert';
@@ -22,11 +21,6 @@ import 'package:url_launcher/url_launcher_string.dart';
import '../providers/chat_providers.dart' show sendMessage;
import '../../../core/utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'chat/assistant');
}
class AssistantMessageWidget extends ConsumerStatefulWidget {
final dynamic message;
final bool isStreaming;
@@ -76,7 +70,10 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
try {
await sendMessage(ref, trimmed, null);
} catch (err, stack) {
debugPrint('Failed to send follow-up: $err');
DebugLogger.log(
'Failed to send follow-up: $err',
scope: 'chat/assistant',
);
debugPrintStack(stackTrace: stack);
}
}
@@ -660,15 +657,6 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
const SizedBox(height: Spacing.md),
CitationListView(sources: widget.message.sources),
],
if (hasFollowUps) ...[
const SizedBox(height: Spacing.md),
FollowUpSuggestionBar(
suggestions: widget.message.followUps,
onSelected: _handleFollowUpTap,
isBusy: widget.isStreaming,
),
],
],
),
),
@@ -677,6 +665,14 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
if (!widget.isStreaming) ...[
const SizedBox(height: Spacing.sm),
_buildActionButtons(),
if (hasFollowUps) ...[
const SizedBox(height: Spacing.md),
FollowUpSuggestionBar(
suggestions: widget.message.followUps,
onSelected: _handleFollowUpTap,
isBusy: widget.isStreaming,
),
],
],
],
),
@@ -1283,6 +1279,124 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
}
}
class _AssistantResponseSection extends StatelessWidget {
const _AssistantResponseSection({
required this.title,
required this.child,
this.icon,
});
final String title;
final Widget child;
final IconData? icon;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (icon != null) ...[
Icon(icon, size: 16, color: theme.buttonPrimary),
const SizedBox(width: Spacing.xs),
],
Text(
title,
style: TextStyle(
color: theme.textSecondary,
fontSize: AppTypography.bodySmall,
fontWeight: FontWeight.w600,
letterSpacing: 0.15,
),
),
],
),
const SizedBox(height: Spacing.xs),
Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: theme.cardBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.card),
border: Border.all(
color: theme.cardBorder.withValues(alpha: 0.6),
width: BorderWidth.thin,
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: child,
),
],
);
}
}
class _AssistantSuggestionChip extends StatelessWidget {
const _AssistantSuggestionChip({
required this.label,
this.icon,
this.onPressed,
this.enabled = true,
});
final String label;
final IconData? icon;
final VoidCallback? onPressed;
final bool enabled;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
final effectiveOnPressed = enabled ? onPressed : null;
final iconColor = enabled
? theme.textSecondary
: theme.textSecondary.withValues(alpha: 0.5);
final background = theme.cardBackground.withValues(
alpha: enabled ? 0.95 : 0.85,
);
final borderColor = theme.cardBorder.withValues(
alpha: enabled ? 0.6 : 0.35,
);
return RawChip(
avatar: icon != null ? Icon(icon, size: 16, color: iconColor) : null,
label: Text(
label,
style: TextStyle(
color: enabled ? theme.textPrimary : theme.textSecondary,
fontSize: AppTypography.labelMedium,
fontWeight: FontWeight.w500,
letterSpacing: 0.2,
),
),
onPressed: effectiveOnPressed,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.sm,
vertical: Spacing.xxs,
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
backgroundColor: background,
disabledColor: background,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.pill),
side: BorderSide(color: borderColor, width: BorderWidth.thin),
),
);
}
}
class StatusHistoryTimeline extends StatelessWidget {
const StatusHistoryTimeline({super.key, required this.updates});
@@ -1290,39 +1404,24 @@ class StatusHistoryTimeline extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
if (updates.isEmpty) {
return const SizedBox.shrink();
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: theme.surfaceContainer.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: theme.dividerColor.withValues(alpha: 0.6),
width: BorderWidth.thin,
),
),
return _AssistantResponseSection(
title: 'Status updates',
icon: Icons.sync_alt,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Status updates',
style: TextStyle(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
fontSize: AppTypography.bodyLarge,
const SizedBox(height: Spacing.xs),
for (var index = 0; index < updates.length; index++)
Padding(
padding: EdgeInsets.only(
bottom: index == updates.length - 1 ? 0 : Spacing.xs,
),
child: _StatusHistoryEntry(update: updates[index]),
),
),
const SizedBox(height: Spacing.sm),
...List.generate(updates.length, (index) {
final update = updates[index];
final isLast = index == updates.length - 1;
return _StatusHistoryEntry(update: update, isLast: isLast);
}),
],
),
);
@@ -1330,10 +1429,9 @@ class StatusHistoryTimeline extends StatelessWidget {
}
class _StatusHistoryEntry extends StatelessWidget {
const _StatusHistoryEntry({required this.update, required this.isLast});
const _StatusHistoryEntry({required this.update});
final ChatStatusUpdate update;
final bool isLast;
Color _indicatorColor(ConduitThemeExtension theme) {
if (update.done == false) {
@@ -1372,142 +1470,159 @@ class _StatusHistoryEntry extends StatelessWidget {
}
}
return Padding(
padding: const EdgeInsets.only(bottom: Spacing.sm),
child: Row(
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: Spacing.sm,
vertical: Spacing.sm,
),
decoration: BoxDecoration(
color: theme.cardBackground.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(AppBorderRadius.md),
border: Border.all(
color: theme.cardBorder.withValues(alpha: 0.5),
width: BorderWidth.thin,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(_indicatorIcon(), size: 18, color: indicatorColor),
if (!isLast)
Container(
margin: const EdgeInsets.only(top: Spacing.xxs),
width: 2,
height: 32,
color: theme.dividerColor.withValues(alpha: 0.5),
Icon(_indicatorIcon(), size: 16, color: indicatorColor),
const SizedBox(width: Spacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
description,
style: TextStyle(
fontSize: AppTypography.bodySmall,
color: theme.textSecondary,
fontWeight: update.done == true
? FontWeight.w600
: FontWeight.w500,
),
),
if (update.count != null)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Text(
update.count == 1
? 'Retrieved 1 source'
: 'Retrieved ${update.count} sources',
style: TextStyle(
color: theme.textSecondary,
fontSize: AppTypography.labelSmall,
fontWeight: FontWeight.w500,
),
),
),
if (timestamp != null)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Text(
_formatTimestamp(timestamp),
style: TextStyle(
color: theme.textSecondary.withValues(alpha: 0.8),
fontSize: AppTypography.labelSmall,
),
),
),
],
),
),
],
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
description,
style: TextStyle(
fontSize: AppTypography.bodyMedium,
color: theme.textPrimary,
fontWeight: update.done == true
? FontWeight.w600
: FontWeight.w500,
),
),
if (update.count != null)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Text(
update.count == 1
? 'Retrieved 1 source'
: 'Retrieved ${update.count} sources',
style: TextStyle(
color: theme.textSecondary,
fontSize: AppTypography.labelSmall,
),
),
),
if (timestamp != null)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Text(
_formatTimestamp(timestamp),
style: TextStyle(
color: theme.textSecondary,
fontSize: AppTypography.labelSmall,
),
),
),
if (queries.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Wrap(
spacing: Spacing.xs,
runSpacing: Spacing.xs,
children: queries.map((query) {
return ActionChip(
label: Text(query),
avatar: const Icon(Icons.search, size: 16),
onPressed: () {
_launchUri(
'https://www.google.com/search?q=${Uri.encodeComponent(query)}',
);
},
);
}).toList(),
),
),
if (update.urls.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Wrap(
spacing: Spacing.xs,
runSpacing: Spacing.xs,
children: update.urls.map((url) {
return OutlinedButton.icon(
onPressed: () => _launchUri(url),
icon: const Icon(Icons.open_in_new, size: 16),
label: Text(
Uri.tryParse(url)?.host ?? 'Link',
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
),
),
if (update.items.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: Spacing.xxs),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: update.items.map((item) {
final title = item.title?.isNotEmpty == true
? item.title!
: item.link ?? 'Result';
return Padding(
padding: const EdgeInsets.only(bottom: Spacing.xxs),
child: InkWell(
onTap: item.link != null
? () => _launchUri(item.link!)
: null,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.link, size: 16),
const SizedBox(width: Spacing.xxs),
Expanded(
child: Text(
title,
style: TextStyle(
color: item.link != null
? theme.buttonPrimary
: theme.textSecondary,
decoration: item.link != null
? TextDecoration.underline
: TextDecoration.none,
),
),
),
],
),
),
);
}).toList(),
),
),
],
if (queries.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: Spacing.sm),
child: Wrap(
spacing: Spacing.xs,
runSpacing: Spacing.xs,
children: queries.map((query) {
return _AssistantSuggestionChip(
label: query,
icon: Icons.search,
onPressed: () {
_launchUri(
'https://www.google.com/search?q=${Uri.encodeComponent(query)}',
);
},
);
}).toList(),
),
),
if (update.urls.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: Spacing.sm),
child: Wrap(
spacing: Spacing.xs,
runSpacing: Spacing.xs,
children: update.urls.map((url) {
final host = Uri.tryParse(url)?.host ?? 'Link';
return _AssistantSuggestionChip(
label: host,
icon: Icons.open_in_new,
onPressed: () => _launchUri(url),
);
}).toList(),
),
),
if (update.items.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: Spacing.sm),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: update.items.map((item) {
final title = item.title?.isNotEmpty == true
? item.title!
: item.link ?? 'Result';
return Padding(
padding: const EdgeInsets.only(bottom: Spacing.xs),
child: InkWell(
onTap: item.link != null
? () => _launchUri(item.link!)
: null,
borderRadius: BorderRadius.circular(AppBorderRadius.sm),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: Spacing.xxs,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.link,
size: 16,
color: theme.textSecondary,
),
const SizedBox(width: Spacing.xs),
Expanded(
child: Text(
title,
style: TextStyle(
color: item.link != null
? theme.buttonPrimary
: theme.textSecondary,
decoration: item.link != null
? TextDecoration.underline
: TextDecoration.none,
fontSize: AppTypography.bodySmall,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
);
}).toList(),
),
),
),
],
),
);
@@ -1772,7 +1887,7 @@ class CitationListView extends StatelessWidget {
}
}
class FollowUpSuggestionBar extends StatefulWidget {
class FollowUpSuggestionBar extends StatelessWidget {
const FollowUpSuggestionBar({
super.key,
required this.suggestions,
@@ -1784,149 +1899,37 @@ class FollowUpSuggestionBar extends StatefulWidget {
final ValueChanged<String> onSelected;
final bool isBusy;
@override
State<FollowUpSuggestionBar> createState() => _FollowUpSuggestionBarState();
}
class _FollowUpSuggestionBarState extends State<FollowUpSuggestionBar>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 520),
);
if (widget.suggestions.isNotEmpty) {
_controller.forward();
}
}
@override
void didUpdateWidget(covariant FollowUpSuggestionBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (!listEquals(oldWidget.suggestions, widget.suggestions)) {
if (widget.suggestions.isEmpty) {
_controller.reset();
} else {
_controller.forward(from: 0);
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
if (widget.suggestions.isEmpty) {
final trimmedSuggestions = suggestions
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList(growable: false);
if (trimmedSuggestions.isEmpty) {
return const SizedBox.shrink();
}
final Animation<double> headerAnimation = CurvedAnimation(
parent: _controller,
curve: const Interval(0, 0.35, curve: Curves.easeOutCubic),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnimatedBuilder(
animation: headerAnimation,
builder: (context, child) {
return Opacity(
opacity: headerAnimation.value,
child: Transform.translate(
offset: Offset(0, (1 - headerAnimation.value) * 10),
child: child,
),
);
},
child: Text(
'Try next',
style: TextStyle(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
fontSize: AppTypography.bodyLarge,
),
return _AssistantResponseSection(
title: 'Suggested next steps',
icon: Icons.auto_awesome,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: Spacing.xs),
Wrap(
spacing: Spacing.xs,
runSpacing: Spacing.xs,
children: [
for (final suggestion in trimmedSuggestions)
_AssistantSuggestionChip(
label: suggestion,
onPressed: isBusy ? null : () => onSelected(suggestion),
enabled: !isBusy,
),
],
),
),
const SizedBox(height: Spacing.xs),
Wrap(
spacing: Spacing.xs,
runSpacing: Spacing.xs,
children: [
for (var i = 0; i < widget.suggestions.length; i++)
_AnimatedSuggestionChip(
controller: _controller,
index: i,
total: widget.suggestions.length,
isBusy: widget.isBusy,
suggestion: widget.suggestions[i],
onSelected: widget.onSelected,
),
],
),
],
);
}
}
class _AnimatedSuggestionChip extends StatelessWidget {
const _AnimatedSuggestionChip({
required this.controller,
required this.index,
required this.total,
required this.isBusy,
required this.suggestion,
required this.onSelected,
});
final AnimationController controller;
final int index;
final int total;
final bool isBusy;
final String suggestion;
final ValueChanged<String> onSelected;
Interval _intervalForIndex() {
if (total <= 1) {
return const Interval(0.0, 0.8, curve: Curves.easeOutCubic);
}
final double step = 0.6 / total;
final double start = (index * step).clamp(0.0, 0.8);
final double end = (start + 0.4).clamp(0.2, 1.0);
return Interval(start, end, curve: Curves.easeOutCubic);
}
@override
Widget build(BuildContext context) {
final animation = CurvedAnimation(
parent: controller,
curve: _intervalForIndex(),
);
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
final double t = animation.value;
return Opacity(
opacity: t,
child: Transform.translate(
offset: Offset(0, (1 - t) * 12),
child: Transform.scale(scale: 0.95 + (t * 0.05), child: child),
),
);
},
child: FilledButton.tonal(
onPressed: isBusy ? null : () => onSelected(suggestion),
child: Text(suggestion),
],
),
);
}
@@ -1937,6 +1940,6 @@ Future<void> _launchUri(String url) async {
try {
await launchUrlString(url, mode: LaunchMode.externalApplication);
} catch (err) {
debugPrint('Unable to open url $url: $err');
DebugLogger.log('Unable to open url $url: $err', scope: 'chat/assistant');
}
}

View File

@@ -1,7 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart' hide debugPrint;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_animate/flutter_animate.dart';
@@ -14,11 +14,6 @@ import '../../../core/providers/app_providers.dart';
import '../../auth/providers/unified_auth_providers.dart';
import '../../../core/utils/debug_logger.dart';
void debugPrint(String? message, {int? wrapWidth}) {
if (message == null) return;
DebugLogger.fromLegacy(message, scope: 'chat/image-attachment');
}
// Simple global cache to prevent reloading
final _globalImageCache = <String, String>{};
final _globalLoadingStates = <String, bool>{};
@@ -696,7 +691,10 @@ class FullScreenImageViewer extends ConsumerWidget {
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)]));
} catch (e) {
// Swallowing UI feedback per requirements; keep a log for debugging
debugPrint('Failed to share image: $e');
DebugLogger.log(
'Failed to share image: $e',
scope: 'chat/image-attachment',
);
}
}
}