refactor: optimize chat input layout and enhance drawer functionality with loading indicators

This commit is contained in:
cogwheel0
2025-08-25 15:50:25 +05:30
parent d6f96ff5c5
commit f934c59d19
4 changed files with 153 additions and 117 deletions

View File

@@ -287,8 +287,10 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
tooltip: AppLocalizations.of(
context,
)!.addAttachment,
showBackground: false,
iconSize: IconSize.large,
),
const SizedBox(width: Spacing.sm),
const SizedBox(width: Spacing.xs),
],
// Text input expands to fill
Expanded(
@@ -395,14 +397,16 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
tooltip: AppLocalizations.of(
context,
)!.addAttachment,
showBackground: false,
iconSize: IconSize.large,
),
const SizedBox(width: Spacing.sm),
const SizedBox(width: Spacing.xs),
// Quick pills: no scroll, clip text within fixed max width
Expanded(
child: Row(
children: [
Flexible(
fit: FlexFit.loose,
Expanded(
flex: 2,
child: _buildPillButton(
icon: Platform.isIOS
? CupertinoIcons.search
@@ -425,9 +429,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
),
),
if (imageGenAvailable) ...[
const SizedBox(width: Spacing.sm),
Flexible(
fit: FlexFit.loose,
const SizedBox(width: Spacing.xs),
Expanded(
flex: 3,
child: _buildPillButton(
icon: Platform.isIOS
? CupertinoIcons.photo
@@ -450,92 +454,108 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
),
),
],
const SizedBox(width: Spacing.xs),
_buildRoundButton(
icon: Icons.more_horiz,
onTap: widget.enabled
? _showUnifiedToolsModal
: null,
tooltip: AppLocalizations.of(
context,
)!.tools,
isActive:
ref
.watch(
selectedToolIdsProvider,
)
.isNotEmpty ||
webSearchEnabled ||
imageGenEnabled,
),
],
),
),
const SizedBox(width: Spacing.sm),
_buildRoundButton(
icon: Icons.more_horiz,
onTap: widget.enabled
? _showUnifiedToolsModal
: null,
tooltip: AppLocalizations.of(
context,
)!.tools,
isActive:
ref
.watch(selectedToolIdsProvider)
.isNotEmpty ||
webSearchEnabled ||
imageGenEnabled,
),
const SizedBox(width: Spacing.sm),
// Microphone button: inline voice input toggle with animated intensity ring
Builder(
builder: (context) {
const double buttonSize =
TouchTarget.comfortable;
final double t = _isRecording
? (_intensity.clamp(0, 10) / 10.0)
: 0.0;
final double ringMaxExtra = 16.0;
final double ringSize =
buttonSize + (ringMaxExtra * t);
final double ringOpacity =
0.15 + (0.35 * t);
const SizedBox(width: Spacing.xs),
// Mic + Send cluster pinned to the right
Row(
mainAxisSize: MainAxisSize.min,
children: [
// Microphone button: inline voice input toggle with animated intensity ring
Builder(
builder: (context) {
const double buttonSize =
TouchTarget.comfortable;
final double t = _isRecording
? (_intensity.clamp(0, 10) / 10.0)
: 0.0;
final double ringMaxExtra = 16.0;
final double ringSize =
buttonSize + (ringMaxExtra * t);
final double ringOpacity =
0.15 + (0.35 * t);
return SizedBox(
width: buttonSize,
height: buttonSize,
child: Stack(
alignment: Alignment.center,
children: [
AnimatedContainer(
duration: const Duration(
milliseconds: 120,
),
width: ringSize,
height: ringSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: context
.conduitTheme
.buttonPrimary
.withValues(
alpha: ringOpacity,
),
),
return SizedBox(
width: buttonSize,
height: buttonSize,
child: Stack(
alignment: Alignment.center,
children: [
AnimatedContainer(
duration: const Duration(
milliseconds: 120,
),
width: ringSize,
height: ringSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: context
.conduitTheme
.buttonPrimary
.withValues(
alpha: ringOpacity,
),
),
),
Transform.scale(
scale: _isRecording
? 1.0 +
(_intensity.clamp(
0,
10,
) /
200)
: 1.0,
child: _buildRoundButton(
icon: Platform.isIOS
? CupertinoIcons
.mic_fill
: Icons.mic,
onTap:
(widget.enabled &&
voiceAvailable)
? _toggleVoice
: null,
tooltip:
AppLocalizations.of(
context,
)!.voiceInput,
isActive: _isRecording,
),
),
],
),
Transform.scale(
scale: _isRecording
? 1.0 +
(_intensity.clamp(
0,
10,
) /
200)
: 1.0,
child: _buildRoundButton(
icon: Platform.isIOS
? CupertinoIcons.mic_fill
: Icons.mic,
onTap:
(widget.enabled &&
voiceAvailable)
? _toggleVoice
: null,
tooltip: AppLocalizations.of(
context,
)!.voiceInput,
isActive: _isRecording,
),
),
],
),
);
},
);
},
),
const SizedBox(width: Spacing.xs),
// Primary action button (Send/Stop) when expanded
_buildPrimaryButton(
_hasText,
isGenerating,
stopGeneration,
),
],
),
const SizedBox(width: Spacing.sm),
// Debug button for testing on-device STT (enable by changing false to true)
// ignore: dead_code
if (false) ...[
@@ -565,13 +585,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
tooltip: 'Test On-Device STT',
),
],
const SizedBox(width: Spacing.sm),
// Primary action button (Send/Stop) when expanded
_buildPrimaryButton(
_hasText,
isGenerating,
stopGeneration,
),
// removed duplicate send button; now only in right cluster
],
),
),
@@ -716,6 +730,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
String? tooltip,
bool isActive = false,
bool showBackground = true,
double? iconSize,
}) {
return Tooltip(
message: tooltip ?? '',
@@ -760,7 +775,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
),
child: Icon(
icon,
size: IconSize.medium,
size: iconSize ?? IconSize.medium,
color: widget.enabled
? (isActive
? context.conduitTheme.textPrimary
@@ -814,18 +829,15 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
boxShadow: null,
),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 140),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: AppTypography.labelStyle.copyWith(
color: isActive
? context.conduitTheme.buttonPrimary
: context.conduitTheme.textPrimary,
),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: AppTypography.labelStyle.copyWith(
color: isActive
? context.conduitTheme.buttonPrimary
: context.conduitTheme.textPrimary,
),
),
),

View File

@@ -15,6 +15,7 @@ import '../../../shared/utils/ui_utils.dart';
import '../../../core/auth/auth_state_manager.dart';
import 'package:conduit/l10n/app_localizations.dart';
import '../../../core/models/user.dart' as models;
import '../../../shared/widgets/skeleton_loader.dart';
class ChatsDrawer extends ConsumerStatefulWidget {
const ChatsDrawer({super.key});
@@ -30,6 +31,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
Timer? _debounce;
String _query = '';
bool _isLoadingConversation = false;
String? _pendingConversationId;
String? _dragHoverFolderId;
bool _isDragging = false;
bool _draggingHasFolder = false;
@@ -88,7 +90,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
? CupertinoIcons.bubble_left
: Icons.add_comment,
color: theme.iconPrimary,
size: IconSize.listItem,
size: IconSize.lg,
),
onPressed: () {
chat.startNewChat(ref);
@@ -96,8 +98,8 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
},
tooltip: AppLocalizations.of(context)!.newChat,
constraints: const BoxConstraints(
minWidth: TouchTarget.listItem,
minHeight: TouchTarget.listItem,
minWidth: TouchTarget.comfortable,
minHeight: TouchTarget.comfortable,
),
),
],
@@ -272,8 +274,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
...convs.map(
(c) => _buildTileFor(c, inFolder: true),
),
const SizedBox(height: Spacing.sm),
const SizedBox(height: Spacing.xs),
],
const SizedBox(height: Spacing.xs),
],
);
}).toList();
@@ -659,7 +662,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
Expanded(
child: Text(
name,
style: AppTypography.bodyLargeStyle.copyWith(
style: AppTypography.standard.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
),
@@ -935,10 +938,14 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
final isActive = ref.watch(activeConversationProvider)?.id == conv.id;
final title = conv.title?.isEmpty == true ? 'Chat' : (conv.title ?? 'Chat');
final theme = context.conduitTheme;
final bool isLoadingSelected =
(_pendingConversationId == conv.id) &&
(ref.watch(chat.isLoadingConversationProvider) == true);
final tile = _ConversationTile(
title: title,
pinned: conv.pinned == true,
selected: isActive,
isLoading: isLoadingSelected,
onTap: _isLoadingConversation
? null
: () => _selectConversation(context, conv.id),
@@ -1095,6 +1102,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
try {
// Mark global loading to show skeletons in chat
ref.read(chat.isLoadingConversationProvider.notifier).state = true;
_pendingConversationId = id;
final api = ref.read(apiServiceProvider);
if (api != null) {
@@ -1109,10 +1117,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
// Clear global loading before closing drawer
ref.read(chat.isLoadingConversationProvider.notifier).state = false;
_pendingConversationId = null;
if (mounted) navigator.maybePop();
} catch (_) {
ref.read(chat.isLoadingConversationProvider.notifier).state = false;
_pendingConversationId = null;
if (mounted) navigator.maybePop();
} finally {
if (mounted) setState(() => _isLoadingConversation = false);
@@ -1225,7 +1235,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTypography.bodyLargeStyle.copyWith(
style: AppTypography.standard.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
),
@@ -1495,6 +1505,7 @@ class _ConversationTile extends StatelessWidget {
final String title;
final bool pinned;
final bool selected;
final bool isLoading;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final VoidCallback? onMorePressed;
@@ -1503,6 +1514,7 @@ class _ConversationTile extends StatelessWidget {
required this.title,
required this.pinned,
required this.selected,
required this.isLoading,
required this.onTap,
this.onLongPress,
this.onMorePressed,
@@ -1524,7 +1536,7 @@ class _ConversationTile extends StatelessWidget {
),
child: InkWell(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
onTap: onTap,
onTap: isLoading ? null : onTap,
onLongPress: onLongPress,
child: Padding(
padding: const EdgeInsets.symmetric(
@@ -1545,7 +1557,18 @@ class _ConversationTile extends StatelessWidget {
),
),
const SizedBox(width: Spacing.xs),
if (onMorePressed != null)
if (isLoading)
SizedBox(
width: 72,
height: TouchTarget.small,
child: SkeletonLoader(
width: 72,
height: TouchTarget.small,
borderRadius: BorderRadius.circular(AppBorderRadius.chip),
isCompact: true,
),
)
else if (onMorePressed != null)
IconButton(
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,

View File

@@ -96,7 +96,7 @@ class _UnifiedToolsModalState extends ConsumerState<UnifiedToolsModal> {
),
],
),
const SizedBox(height: Spacing.lg),
// Removed extra spacing between feature tiles and tools list
// All tools as selectable tiles (model selector style)
toolsAsync.when(