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( tooltip: AppLocalizations.of(
context, context,
)!.addAttachment, )!.addAttachment,
showBackground: false,
iconSize: IconSize.large,
), ),
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.xs),
], ],
// Text input expands to fill // Text input expands to fill
Expanded( Expanded(
@@ -395,14 +397,16 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
tooltip: AppLocalizations.of( tooltip: AppLocalizations.of(
context, context,
)!.addAttachment, )!.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 // Quick pills: no scroll, clip text within fixed max width
Expanded( Expanded(
child: Row( child: Row(
children: [ children: [
Flexible( Expanded(
fit: FlexFit.loose, flex: 2,
child: _buildPillButton( child: _buildPillButton(
icon: Platform.isIOS icon: Platform.isIOS
? CupertinoIcons.search ? CupertinoIcons.search
@@ -425,9 +429,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
), ),
), ),
if (imageGenAvailable) ...[ if (imageGenAvailable) ...[
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.xs),
Flexible( Expanded(
fit: FlexFit.loose, flex: 3,
child: _buildPillButton( child: _buildPillButton(
icon: Platform.isIOS icon: Platform.isIOS
? CupertinoIcons.photo ? CupertinoIcons.photo
@@ -450,10 +454,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
), ),
), ),
], ],
], const SizedBox(width: Spacing.xs),
),
),
const SizedBox(width: Spacing.sm),
_buildRoundButton( _buildRoundButton(
icon: Icons.more_horiz, icon: Icons.more_horiz,
onTap: widget.enabled onTap: widget.enabled
@@ -464,12 +465,21 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
)!.tools, )!.tools,
isActive: isActive:
ref ref
.watch(selectedToolIdsProvider) .watch(
selectedToolIdsProvider,
)
.isNotEmpty || .isNotEmpty ||
webSearchEnabled || webSearchEnabled ||
imageGenEnabled, imageGenEnabled,
), ),
const SizedBox(width: Spacing.sm), ],
),
),
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 // Microphone button: inline voice input toggle with animated intensity ring
Builder( Builder(
builder: (context) { builder: (context) {
@@ -517,14 +527,16 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
: 1.0, : 1.0,
child: _buildRoundButton( child: _buildRoundButton(
icon: Platform.isIOS icon: Platform.isIOS
? CupertinoIcons.mic_fill ? CupertinoIcons
.mic_fill
: Icons.mic, : Icons.mic,
onTap: onTap:
(widget.enabled && (widget.enabled &&
voiceAvailable) voiceAvailable)
? _toggleVoice ? _toggleVoice
: null, : null,
tooltip: AppLocalizations.of( tooltip:
AppLocalizations.of(
context, context,
)!.voiceInput, )!.voiceInput,
isActive: _isRecording, isActive: _isRecording,
@@ -535,7 +547,15 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
); );
}, },
), ),
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.xs),
// Primary action button (Send/Stop) when expanded
_buildPrimaryButton(
_hasText,
isGenerating,
stopGeneration,
),
],
),
// Debug button for testing on-device STT (enable by changing false to true) // Debug button for testing on-device STT (enable by changing false to true)
// ignore: dead_code // ignore: dead_code
if (false) ...[ if (false) ...[
@@ -565,13 +585,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
tooltip: 'Test On-Device STT', tooltip: 'Test On-Device STT',
), ),
], ],
const SizedBox(width: Spacing.sm), // removed duplicate send button; now only in right cluster
// Primary action button (Send/Stop) when expanded
_buildPrimaryButton(
_hasText,
isGenerating,
stopGeneration,
),
], ],
), ),
), ),
@@ -716,6 +730,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
String? tooltip, String? tooltip,
bool isActive = false, bool isActive = false,
bool showBackground = true, bool showBackground = true,
double? iconSize,
}) { }) {
return Tooltip( return Tooltip(
message: tooltip ?? '', message: tooltip ?? '',
@@ -760,7 +775,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
), ),
child: Icon( child: Icon(
icon, icon,
size: IconSize.medium, size: iconSize ?? IconSize.medium,
color: widget.enabled color: widget.enabled
? (isActive ? (isActive
? context.conduitTheme.textPrimary ? context.conduitTheme.textPrimary
@@ -814,8 +829,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
boxShadow: null, boxShadow: null,
), ),
child: Center( child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 140),
child: Text( child: Text(
label, label,
maxLines: 1, maxLines: 1,
@@ -830,7 +843,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
), ),
), ),
), ),
),
); );
} }

View File

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

View File

@@ -177,7 +177,8 @@ class OfflineAwareButton extends ConsumerWidget {
return Tooltip( return Tooltip(
message: !enabled message: !enabled
? (offlineTooltip ?? AppLocalizations.of(context)!.featureRequiresInternet) ? (offlineTooltip ??
AppLocalizations.of(context)!.featureRequiresInternet)
: '', : '',
child: FilledButton(onPressed: enabled ? onPressed : null, child: child), child: FilledButton(onPressed: enabled ? onPressed : null, child: child),
); );