refactor: optimize chat input layout and enhance drawer functionality with loading indicators
This commit is contained in:
@@ -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,10 +454,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Spacing.sm),
|
||||
const SizedBox(width: Spacing.xs),
|
||||
_buildRoundButton(
|
||||
icon: Icons.more_horiz,
|
||||
onTap: widget.enabled
|
||||
@@ -464,12 +465,21 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
)!.tools,
|
||||
isActive:
|
||||
ref
|
||||
.watch(selectedToolIdsProvider)
|
||||
.watch(
|
||||
selectedToolIdsProvider,
|
||||
)
|
||||
.isNotEmpty ||
|
||||
webSearchEnabled ||
|
||||
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
|
||||
Builder(
|
||||
builder: (context) {
|
||||
@@ -517,14 +527,16 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
: 1.0,
|
||||
child: _buildRoundButton(
|
||||
icon: Platform.isIOS
|
||||
? CupertinoIcons.mic_fill
|
||||
? CupertinoIcons
|
||||
.mic_fill
|
||||
: Icons.mic,
|
||||
onTap:
|
||||
(widget.enabled &&
|
||||
voiceAvailable)
|
||||
? _toggleVoice
|
||||
: null,
|
||||
tooltip: AppLocalizations.of(
|
||||
tooltip:
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.voiceInput,
|
||||
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)
|
||||
// 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,8 +829,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
boxShadow: null,
|
||||
),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 140),
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
@@ -830,7 +843,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -177,7 +177,8 @@ class OfflineAwareButton extends ConsumerWidget {
|
||||
|
||||
return Tooltip(
|
||||
message: !enabled
|
||||
? (offlineTooltip ?? AppLocalizations.of(context)!.featureRequiresInternet)
|
||||
? (offlineTooltip ??
|
||||
AppLocalizations.of(context)!.featureRequiresInternet)
|
||||
: '',
|
||||
child: FilledButton(onPressed: enabled ? onPressed : null, child: child),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user