From 8d8ad8478bfcf16611d60e46c563e36a9b679139 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:35:56 +0530 Subject: [PATCH 1/4] feat(chat): Refactor chat input styling and remove compact mode --- lib/features/chat/views/chat_page.dart | 292 +++++++++++------- .../chat/widgets/modern_chat_input.dart | 240 +++++++------- 2 files changed, 286 insertions(+), 246 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 19d979f..457fcc6 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -8,6 +8,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'dart:io' show Platform; +import 'dart:ui' show ImageFilter; import '../../../shared/widgets/responsive_drawer_layout.dart'; import '../../navigation/widgets/chats_drawer.dart'; import 'dart:async'; @@ -968,6 +969,8 @@ class _ChatPageState extends ConsumerState { // Use slivers to align with the actual messages view. // Do not attach the primary scroll controller here to avoid // AnimatedSwitcher attaching the same controller twice. + // Add bottom padding to account for floating input overlay. + final bottomPadding = Spacing.lg + _inputHeight; return CustomScrollView( key: const ValueKey('loading_messages'), controller: null, @@ -976,11 +979,11 @@ class _ChatPageState extends ConsumerState { cacheExtent: 300, slivers: [ SliverPadding( - padding: const EdgeInsets.fromLTRB( + padding: EdgeInsets.fromLTRB( Spacing.lg, Spacing.md, Spacing.lg, - Spacing.lg, + bottomPadding, ), sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { @@ -1097,6 +1100,8 @@ class _ChatPageState extends ConsumerState { }); } + // Add bottom padding to account for floating input overlay. + final bottomPadding = Spacing.lg + _inputHeight; return CustomScrollView( key: const ValueKey('actual_messages'), controller: _scrollController, @@ -1105,11 +1110,11 @@ class _ChatPageState extends ConsumerState { cacheExtent: 600, slivers: [ SliverPadding( - padding: const EdgeInsets.fromLTRB( + padding: EdgeInsets.fromLTRB( Spacing.lg, Spacing.md, Spacing.lg, - Spacing.lg, + bottomPadding, ), sliver: OptimizedSliverList( items: messages, @@ -1349,6 +1354,8 @@ class _ChatPageState extends ConsumerState { final greetingText = resolvedGreetingName != null ? l10n.onboardStartTitle(resolvedGreetingName) : null; + // Add bottom padding to account for floating input overlay. + final bottomPadding = _inputHeight; return LayoutBuilder( builder: (context, constraints) { final greetingDisplay = greetingText ?? ''; @@ -1360,7 +1367,12 @@ class _ChatPageState extends ConsumerState { width: double.infinity, height: constraints.maxHeight, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: Spacing.lg), + padding: EdgeInsets.fromLTRB( + Spacing.lg, + 0, + Spacing.lg, + bottomPadding, + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, @@ -1964,94 +1976,120 @@ class _ChatPageState extends ConsumerState { }, child: Stack( children: [ - Column( - children: [ - // Messages Area with pull-to-refresh - Expanded( - child: ConduitRefreshIndicator( - onRefresh: () async { - // Reload active conversation messages from server - final api = ref.read(apiServiceProvider); - final active = ref.read( - activeConversationProvider, + // Messages Area fills entire space with pull-to-refresh + Positioned.fill( + child: ConduitRefreshIndicator( + onRefresh: () async { + // Reload active conversation messages from server + final api = ref.read(apiServiceProvider); + final active = ref.read(activeConversationProvider); + if (api != null && active != null) { + try { + final full = await api.getConversation( + active.id, ); - if (api != null && active != null) { - try { - final full = await api.getConversation( - active.id, - ); - ref - .read( - activeConversationProvider.notifier, - ) - .set(full); - } catch (e) { - DebugLogger.log( - 'Failed to refresh conversation: $e', - scope: 'chat/page', - ); - } - } - - // Also refresh the conversations list to reconcile missed events - // and keep timestamps/order in sync with the server. - try { - refreshConversationsCache(ref); - // Best-effort await to stabilize UI; ignore errors. - await ref.read(conversationsProvider.future); - } catch (_) {} - - // Add small delay for better UX feedback - await Future.delayed( - const Duration(milliseconds: 300), + ref + .read(activeConversationProvider.notifier) + .set(full); + } catch (e) { + DebugLogger.log( + 'Failed to refresh conversation: $e', + scope: 'chat/page', ); - }, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - FocusManager.instance.primaryFocus?.unfocus(); - try { - SystemChannels.textInput.invokeMethod( - 'TextInput.hide', - ); - } catch (_) {} - }, - child: RepaintBoundary( - child: _buildMessagesList(theme), + } + } + + // Also refresh the conversations list to reconcile missed events + // and keep timestamps/order in sync with the server. + try { + refreshConversationsCache(ref); + // Best-effort await to stabilize UI; ignore errors. + await ref.read(conversationsProvider.future); + } catch (_) {} + + // Add small delay for better UX feedback + await Future.delayed( + const Duration(milliseconds: 300), + ); + }, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + FocusManager.instance.primaryFocus?.unfocus(); + try { + SystemChannels.textInput.invokeMethod( + 'TextInput.hide', + ); + } catch (_) {} + }, + child: RepaintBoundary( + child: _buildMessagesList(theme), + ), + ), + ), + ), + + // Floating input area with attachments and blur background + Positioned( + left: 0, + right: 0, + bottom: 0, + child: RepaintBoundary( + child: MeasureSize( + onChange: (size) { + if (mounted) { + setState(() { + _inputHeight = size.height; + }); + } + }, + child: Container( + decoration: BoxDecoration( + // Gradient fade from transparent to solid background + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, 0.4, 1.0], + colors: [ + theme.scaffoldBackgroundColor.withValues( + alpha: 0.0, + ), + theme.scaffoldBackgroundColor.withValues( + alpha: 0.85, + ), + theme.scaffoldBackgroundColor, + ], ), ), - ), - ), - - // File attachments - const FileAttachmentWidget(), - const ContextAttachmentWidget(), - - // Modern Input (root matches input background including safe area) - RepaintBoundary( - child: MeasureSize( - onChange: (size) { - if (mounted) { - setState(() { - _inputHeight = size.height; - }); - } - }, - child: ModernChatInput( - onSendMessage: (text) => - _handleMessageSend(text, selectedModel), - onVoiceInput: null, - onVoiceCall: _handleVoiceCall, - onFileAttachment: _handleFileAttachment, - onImageAttachment: _handleImageAttachment, - onCameraCapture: () => - _handleImageAttachment(fromCamera: true), - onWebAttachment: _promptAttachWebpage, - onPastedAttachments: _handlePastedAttachments, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Top padding for gradient fade area + const SizedBox(height: Spacing.xl), + // File attachments + const FileAttachmentWidget(), + const ContextAttachmentWidget(), + // Modern Input + ModernChatInput( + onSendMessage: (text) => + _handleMessageSend(text, selectedModel), + onVoiceInput: null, + onVoiceCall: _handleVoiceCall, + onFileAttachment: _handleFileAttachment, + onImageAttachment: _handleImageAttachment, + onCameraCapture: () => + _handleImageAttachment( + fromCamera: true, + ), + onWebAttachment: _promptAttachWebpage, + onPastedAttachments: + _handlePastedAttachments, + ), + ], ), ), ), - ], + ), ), // Floating Scroll to Bottom Button with smooth appear/disappear @@ -2093,39 +2131,61 @@ class _ChatPageState extends ConsumerState { borderRadius: BorderRadius.circular( AppBorderRadius.floatingButton, ), - child: Container( - decoration: BoxDecoration( - color: context - .conduitTheme - .surfaceContainerHighest - .withValues(alpha: 0.75), - border: Border.all( - color: context.conduitTheme.cardBorder - .withValues(alpha: 0.3), - width: BorderWidth.regular, - ), - borderRadius: BorderRadius.circular( - AppBorderRadius.floatingButton, - ), - boxShadow: ConduitShadows.button( - context, - ), + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 16, + sigmaY: 16, ), - child: SizedBox( - width: TouchTarget.button, - height: TouchTarget.button, - child: IconButton( - onPressed: _scrollToBottom, - splashRadius: 24, - icon: Icon( - Platform.isIOS - ? CupertinoIcons.arrow_down - : Icons.keyboard_arrow_down, - size: IconSize.lg, + child: Container( + decoration: BoxDecoration( + // Use same high-contrast colors as floating input + color: + theme.brightness == + Brightness.dark + ? Color.lerp( + context + .conduitTheme + .cardBackground, + Colors.white, + 0.08, + )!.withValues(alpha: 0.85) + : Color.lerp( + context + .conduitTheme + .inputBackground, + Colors.black, + 0.06, + )!.withValues(alpha: 0.85), + border: Border.all( color: context .conduitTheme - .iconPrimary - .withValues(alpha: 0.9), + .cardBorder + .withValues(alpha: 0.55), + width: BorderWidth.thin, + ), + borderRadius: BorderRadius.circular( + AppBorderRadius.floatingButton, + ), + boxShadow: ConduitShadows.button( + context, + ), + ), + child: SizedBox( + width: TouchTarget.button, + height: TouchTarget.button, + child: IconButton( + onPressed: _scrollToBottom, + splashRadius: 24, + icon: Icon( + Platform.isIOS + ? CupertinoIcons.arrow_down + : Icons.keyboard_arrow_down, + size: IconSize.lg, + color: context + .conduitTheme + .iconPrimary + .withValues(alpha: 0.9), + ), ), ), ), diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index 836459b..dd7ec04 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -9,7 +9,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io' show Platform; import 'dart:async'; -import 'dart:ui'; import 'dart:math' as math; import '../providers/chat_providers.dart'; import '../services/clipboard_attachment_service.dart'; @@ -1073,10 +1072,10 @@ class _ModernChatInputState extends ConsumerState final Brightness brightness = Theme.of(context).brightness; final bool isActive = _focusNode.hasFocus || _hasText; - final Color composerSurface = context.conduitTheme.inputBackground; + // Use high-contrast background for floating input final Color composerBackground = brightness == Brightness.dark - ? composerSurface.withValues(alpha: 0.78) - : context.conduitTheme.surfaceContainerHighest; + ? Color.lerp(context.conduitTheme.cardBackground, Colors.white, 0.08)! + : Color.lerp(context.conduitTheme.inputBackground, Colors.black, 0.06)!; final Color placeholderBase = context.conduitTheme.inputText.withValues( alpha: 0.64, ); @@ -1087,7 +1086,7 @@ class _ModernChatInputState extends ConsumerState context.conduitTheme.inputBorder, context.conduitTheme.inputBorderFocused, isActive ? 1.0 : 0.0, - )!.withValues(alpha: brightness == Brightness.dark ? 0.55 : 0.45); + )!.withValues(alpha: brightness == Brightness.dark ? 0.65 : 0.55); final Color shellShadowColor = context.conduitTheme.cardShadow.withValues( alpha: brightness == Brightness.dark ? 0.22 + (isActive ? 0.08 : 0.0) @@ -1209,21 +1208,17 @@ class _ModernChatInputState extends ConsumerState ); final BoxDecoration shellDecoration = BoxDecoration( - color: showCompactComposer ? Colors.transparent : composerBackground, + color: composerBackground, borderRadius: shellRadius, - border: showCompactComposer - ? null - : Border.all(color: outlineColor, width: BorderWidth.thin), - boxShadow: showCompactComposer - ? const [] - : [ - BoxShadow( - color: shellShadowColor, - blurRadius: 12 + (isActive ? 4 : 0), - spreadRadius: -2, - offset: const Offset(0, -2), - ), - ], + border: Border.all(color: outlineColor, width: BorderWidth.thin), + boxShadow: [ + BoxShadow( + color: shellShadowColor, + blurRadius: 12 + (isActive ? 4 : 0), + spreadRadius: -2, + offset: const Offset(0, -2), + ), + ], ); final List composerChildren = [ @@ -1238,82 +1233,7 @@ class _ModernChatInputState extends ConsumerState ), child: _buildPromptOverlay(context), ), - if (showCompactComposer) - Padding( - key: const ValueKey('composer-compact'), - padding: const EdgeInsets.fromLTRB( - Spacing.screenPadding, - Spacing.xs, - Spacing.screenPadding, - Spacing.sm, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - _buildOverflowButton( - tooltip: AppLocalizations.of(context)!.more, - webSearchActive: webSearchEnabled, - imageGenerationActive: imageGenEnabled, - toolsActive: selectedToolIds.isNotEmpty, - filtersActive: selectedFilterIds.isNotEmpty, - ), - const SizedBox(width: Spacing.sm), - Expanded( - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.25, - ), - child: AnimatedContainer( - duration: const Duration(milliseconds: 180), - curve: Curves.easeOutCubic, - padding: const EdgeInsets.symmetric(horizontal: Spacing.md), - constraints: const BoxConstraints( - minHeight: TouchTarget.input, - ), - decoration: BoxDecoration( - color: composerSurface.withValues( - alpha: brightness == Brightness.dark ? 0.9 : 0.2, - ), - borderRadius: BorderRadius.circular(_composerRadius), - border: Border.all( - color: outlineColor.withValues( - alpha: brightness == Brightness.dark ? 0.32 : 0.2, - ), - width: BorderWidth.micro, - ), - ), - child: Row( - children: [ - Expanded( - child: _buildComposerTextField( - brightness: brightness, - sendOnEnter: sendOnEnter, - placeholderBase: placeholderBase, - placeholderFocused: placeholderFocused, - contentPadding: const EdgeInsets.symmetric( - vertical: Spacing.xs, - ), - isActive: isActive, - ), - ), - if (!_hasText && voiceAvailable && !isGenerating) - _buildInlineMicIcon(voiceAvailable), - ], - ), - ), - ), - ), - const SizedBox(width: Spacing.sm), - _buildPrimaryButton( - _hasText, - isGenerating, - stopGeneration, - voiceAvailable, - ), - ], - ), - ) - else ...[ + if (!showCompactComposer) ...[ Padding( key: const ValueKey('composer-expanded-input'), padding: const EdgeInsets.fromLTRB( @@ -1405,29 +1325,91 @@ class _ModernChatInputState extends ConsumerState ], ]; + // For compact mode, render text field shell with floating buttons on sides + if (showCompactComposer) { + // Build the text field shell + Widget textFieldShell = AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + padding: const EdgeInsets.symmetric(horizontal: Spacing.md), + constraints: const BoxConstraints(minHeight: TouchTarget.input), + decoration: shellDecoration, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.25, + ), + child: Row( + children: [ + Expanded( + child: _buildComposerTextField( + brightness: brightness, + sendOnEnter: sendOnEnter, + placeholderBase: placeholderBase, + placeholderFocused: placeholderFocused, + contentPadding: const EdgeInsets.symmetric( + vertical: Spacing.xs, + ), + isActive: isActive, + ), + ), + if (!_hasText && voiceAvailable && !isGenerating) + _buildInlineMicIcon(voiceAvailable), + ], + ), + ), + ); + + final bottomPadding = MediaQuery.of(context).viewPadding.bottom; + return Padding( + padding: EdgeInsets.fromLTRB( + Spacing.screenPadding, + 0, + Spacing.screenPadding, + bottomPadding + Spacing.md, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildOverflowButton( + tooltip: AppLocalizations.of(context)!.more, + webSearchActive: webSearchEnabled, + imageGenerationActive: imageGenEnabled, + toolsActive: selectedToolIds.isNotEmpty, + filtersActive: selectedFilterIds.isNotEmpty, + ), + const SizedBox(width: Spacing.sm), + Expanded(child: textFieldShell), + const SizedBox(width: Spacing.sm), + _buildPrimaryButton( + _hasText, + isGenerating, + stopGeneration, + voiceAvailable, + ), + ], + ), + ); + } + + // For expanded mode with quick pills, use the full shell Widget shell = AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeOutCubic, decoration: shellDecoration, - width: double.infinity, - child: SafeArea( - top: false, - bottom: true, - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.4, - ), - child: AnimatedSize( - duration: const Duration(milliseconds: 160), - curve: Curves.easeOutCubic, - alignment: Alignment.topCenter, - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - child: RepaintBoundary( - child: Column( - mainAxisSize: MainAxisSize.min, - children: composerChildren, - ), + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.4, + ), + child: AnimatedSize( + duration: const Duration(milliseconds: 160), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: RepaintBoundary( + child: Column( + mainAxisSize: MainAxisSize.min, + children: composerChildren, ), ), ), @@ -1435,20 +1417,16 @@ class _ModernChatInputState extends ConsumerState ), ); - if (brightness == Brightness.dark && !showCompactComposer) { - shell = ClipRRect( - borderRadius: shellRadius, - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), - child: shell, - ), - ); - } - - return Container( - color: Colors.transparent, - padding: EdgeInsets.zero, - child: Column(mainAxisSize: MainAxisSize.min, children: [shell]), + // Wrap with padding for floating effect, accounting for safe area + final bottomPadding = MediaQuery.of(context).viewPadding.bottom; + return Padding( + padding: EdgeInsets.fromLTRB( + Spacing.sm, + 0, + Spacing.sm, + bottomPadding + Spacing.md, + ), + child: shell, ); } @@ -1688,9 +1666,11 @@ class _ModernChatInputState extends ConsumerState : (activeColor ?? context.conduitTheme.textPrimary.withValues(alpha: Alpha.strong)); + // Use high-contrast background for floating button final Brightness brightness = Theme.of(context).brightness; - final Color baseBackground = context.conduitTheme.inputBackground - .withValues(alpha: brightness == Brightness.dark ? 0.9 : 0.2); + final Color baseBackground = brightness == Brightness.dark + ? Color.lerp(context.conduitTheme.cardBackground, Colors.white, 0.08)! + : Color.lerp(context.conduitTheme.inputBackground, Colors.black, 0.06)!; final Color backgroundColor = !enabled ? baseBackground.withValues(alpha: Alpha.disabled) : isActive From a2c20f7d1f226090d3596e6e5d2a61ee9c0c4229 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 11 Dec 2025 11:57:52 +0530 Subject: [PATCH 2/4] feat(chat): Add floating app bar with blurred background styling --- lib/features/chat/views/chat_page.dart | 631 +++++++++++++------------ 1 file changed, 333 insertions(+), 298 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 457fcc6..65e2a86 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -938,6 +938,67 @@ class _ChatPageState extends ConsumerState { return messages.where((m) => _selectedMessageIds.contains(m.id)).toList(); } + /// Builds a styled container with high-contrast background for app bar + /// widgets, matching the floating chat input styling. + Widget _buildAppBarPill({ + required BuildContext context, + required Widget child, + bool isCircular = false, + }) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + // Use same high-contrast colors as the floating chat input + final backgroundColor = isDark + ? Color.lerp(context.conduitTheme.cardBackground, Colors.white, 0.08)! + : Color.lerp(context.conduitTheme.inputBackground, Colors.black, 0.06)!; + + final borderColor = context.conduitTheme.cardBorder.withValues( + alpha: isDark ? 0.65 : 0.55, + ); + + final borderRadius = isCircular + ? BorderRadius.circular(100) + : BorderRadius.circular(AppBorderRadius.pill); + + // For circular buttons, ensure the entire widget is constrained to a square + if (isCircular) { + return SizedBox( + width: 44, + height: 44, + child: ClipRRect( + borderRadius: borderRadius, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + decoration: BoxDecoration( + color: backgroundColor.withValues(alpha: 0.85), + borderRadius: borderRadius, + border: Border.all(color: borderColor, width: BorderWidth.thin), + ), + child: Center(child: child), + ), + ), + ), + ); + } + + return ClipRRect( + borderRadius: borderRadius, + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), + child: Container( + decoration: BoxDecoration( + color: backgroundColor.withValues(alpha: 0.85), + borderRadius: borderRadius, + border: Border.all(color: borderColor, width: BorderWidth.thin), + ), + child: child, + ), + ), + ); + } + Widget _buildMessagesList(ThemeData theme) { // Use select to watch only the messages list to reduce rebuilds final messages = ref.watch( @@ -969,7 +1030,9 @@ class _ChatPageState extends ConsumerState { // Use slivers to align with the actual messages view. // Do not attach the primary scroll controller here to avoid // AnimatedSwitcher attaching the same controller twice. - // Add bottom padding to account for floating input overlay. + // Add top padding for floating app bar, bottom padding for floating input. + final topPadding = + MediaQuery.of(context).padding.top + kToolbarHeight + Spacing.md; final bottomPadding = Spacing.lg + _inputHeight; return CustomScrollView( key: const ValueKey('loading_messages'), @@ -981,7 +1044,7 @@ class _ChatPageState extends ConsumerState { SliverPadding( padding: EdgeInsets.fromLTRB( Spacing.lg, - Spacing.md, + topPadding, Spacing.lg, bottomPadding, ), @@ -1100,7 +1163,9 @@ class _ChatPageState extends ConsumerState { }); } - // Add bottom padding to account for floating input overlay. + // Add top padding for floating app bar, bottom padding for floating input. + final topPadding = + MediaQuery.of(context).padding.top + kToolbarHeight + Spacing.md; final bottomPadding = Spacing.lg + _inputHeight; return CustomScrollView( key: const ValueKey('actual_messages'), @@ -1112,7 +1177,7 @@ class _ChatPageState extends ConsumerState { SliverPadding( padding: EdgeInsets.fromLTRB( Spacing.lg, - Spacing.md, + topPadding, Spacing.lg, bottomPadding, ), @@ -1354,7 +1419,9 @@ class _ChatPageState extends ConsumerState { final greetingText = resolvedGreetingName != null ? l10n.onboardStartTitle(resolvedGreetingName) : null; - // Add bottom padding to account for floating input overlay. + // Add top padding for floating app bar, bottom padding for floating input. + final topPadding = + MediaQuery.of(context).padding.top + kToolbarHeight + Spacing.md; final bottomPadding = _inputHeight; return LayoutBuilder( builder: (context, constraints) { @@ -1369,7 +1436,7 @@ class _ChatPageState extends ConsumerState { child: Padding( padding: EdgeInsets.fromLTRB( Spacing.lg, - 0, + topPadding, Spacing.lg, bottomPadding, ), @@ -1584,84 +1651,160 @@ class _ChatPageState extends ConsumerState { // Replace Scaffold drawer with a tunable slide drawer for gentler snap behavior. drawerEnableOpenDragGesture: false, drawerDragStartBehavior: DragStartBehavior.down, + extendBodyBehindAppBar: true, appBar: AppBar( - backgroundColor: context.conduitTheme.surfaceBackground, + backgroundColor: Colors.transparent, elevation: Elevation.none, surfaceTintColor: Colors.transparent, shadowColor: Colors.transparent, toolbarHeight: kToolbarHeight + 8, centerTitle: true, titleSpacing: 0.0, + leadingWidth: 44 + Spacing.inputPadding + Spacing.xs, leading: _isSelectionMode - ? IconButton( - icon: Icon( - Platform.isIOS ? CupertinoIcons.xmark : Icons.close, - color: context.conduitTheme.textPrimary, - size: IconSize.appBar, + ? Padding( + padding: const EdgeInsets.only( + left: Spacing.inputPadding, + ), + child: Center( + child: GestureDetector( + onTap: _clearSelection, + child: _buildAppBarPill( + context: context, + isCircular: true, + child: Icon( + Platform.isIOS + ? CupertinoIcons.xmark + : Icons.close, + color: context.conduitTheme.textPrimary, + size: IconSize.appBar, + ), + ), + ), ), - onPressed: _clearSelection, ) : Builder( builder: (ctx) => Padding( padding: const EdgeInsets.only( left: Spacing.inputPadding, ), - child: IconButton( - onPressed: () { - final layout = ResponsiveDrawerLayout.of(ctx); - if (layout == null) return; + child: Center( + child: GestureDetector( + onTap: () { + final layout = ResponsiveDrawerLayout.of(ctx); + if (layout == null) return; - final isDrawerOpen = layout.isOpen; - if (!isDrawerOpen) { - try { - ref - .read( - composerAutofocusEnabledProvider - .notifier, - ) - .set(false); - FocusManager.instance.primaryFocus - ?.unfocus(); - SystemChannels.textInput.invokeMethod( - 'TextInput.hide', - ); - } catch (_) {} - } - layout.toggle(); - }, - icon: Icon( - Platform.isIOS - ? CupertinoIcons.line_horizontal_3 - : Icons.menu, - color: context.conduitTheme.textPrimary, - size: IconSize.appBar, + final isDrawerOpen = layout.isOpen; + if (!isDrawerOpen) { + try { + ref + .read( + composerAutofocusEnabledProvider + .notifier, + ) + .set(false); + FocusManager.instance.primaryFocus + ?.unfocus(); + SystemChannels.textInput.invokeMethod( + 'TextInput.hide', + ); + } catch (_) {} + } + layout.toggle(); + }, + child: _buildAppBarPill( + context: ctx, + isCircular: true, + child: Icon( + Platform.isIOS + ? CupertinoIcons.line_horizontal_3 + : Icons.menu, + color: context.conduitTheme.textPrimary, + size: IconSize.appBar, + ), + ), ), ), ), ), title: _isSelectionMode - ? Text( - '${_selectedMessageIds.length} selected', - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w500, + ? _buildAppBarPill( + context: context, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + child: Text( + '${_selectedMessageIds.length} selected', + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w500, + ), + ), ), ) : LayoutBuilder( builder: (context, constraints) { - return GestureDetector( + // Build title pill (tappable for context menu) + Widget? titlePill; + if (displayConversationTitle != null) { + titlePill = GestureDetector( + onTap: () { + final conversation = ref.read( + activeConversationProvider, + ); + if (conversation == null) return; + showConversationContextMenu( + context: context, + ref: ref, + conversation: conversation, + ); + }, + child: _buildAppBarPill( + context: context, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.xs, + ), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: + constraints.maxWidth - Spacing.xxxl, + ), + child: StreamingTitleText( + title: displayConversationTitle, + style: AppTypography.headlineSmallStyle + .copyWith( + color: context + .conduitTheme + .textPrimary, + fontWeight: FontWeight.w600, + fontSize: 16, + height: 1.3, + ), + cursorColor: context + .conduitTheme + .textPrimary + .withValues(alpha: 0.8), + ), + ), + ), + ), + ); + } + + // Build model selector pill + final modelPill = GestureDetector( onTap: () async { final modelsAsync = ref.read(modelsProvider); - // Handle all async states properly if (modelsAsync.isLoading) { - // If still loading, wait for it to complete try { final models = await ref.read( modelsProvider.future, ); - // Check mounted and use context immediately - // together if (!mounted) return; // ignore: use_build_context_synchronously _showModelDropdown(context, ref, models); @@ -1673,23 +1816,17 @@ class _ChatPageState extends ConsumerState { ); } } else if (modelsAsync.hasValue) { - // If we have data, show immediately (no async - // gap) _showModelDropdown( context, ref, modelsAsync.value!, ); } else if (modelsAsync.hasError) { - // If there's an error, try to refresh and - // load try { ref.invalidate(modelsProvider); final models = await ref.read( modelsProvider.future, ); - // Check mounted and use context immediately - // together if (!mounted) return; // ignore: use_build_context_synchronously _showModelDropdown(context, ref, models); @@ -1702,238 +1839,88 @@ class _ChatPageState extends ConsumerState { } } }, - onLongPress: () { - final conversation = ref.read( - activeConversationProvider, - ); - if (conversation == null) return; - showConversationContextMenu( - context: context, - ref: ref, - conversation: conversation, - ); - }, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: constraints.maxWidth, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AnimatedSwitcher( - duration: const Duration( - milliseconds: 250, - ), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - child: displayConversationTitle != null - ? Column( - key: ValueKey( - displayConversationTitle, - ), - mainAxisSize: MainAxisSize.min, - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: - constraints.maxWidth, - ), - child: StreamingTitleText( - title: - displayConversationTitle, - style: AppTypography - .headlineSmallStyle - .copyWith( - color: context - .conduitTheme - .textPrimary, - fontWeight: - FontWeight.w600, - fontSize: 18, - height: 1.3, - ), - cursorColor: context - .conduitTheme - .textPrimary - .withValues(alpha: 0.8), - ), - ), - const SizedBox( - height: Spacing.xs, - ), - ], - ) - : const SizedBox.shrink( - key: ValueKey( - 'empty-title', - ), - ), - ), - Transform.translate( - offset: const Offset(0, 0), - child: () { - const double iconPaddingX = Spacing.xs; - const double iconPaddingY = Spacing.xxs; - const double iconWidth = IconSize.small; - const double iconBoxWidth = - (iconPaddingX * 2) + - (BorderWidth.thin * 2) + - iconWidth; - final double maxLabelWidth = - (constraints.maxWidth - - (iconBoxWidth * 2) - - (Spacing.xs * 2)) - .clamp( - 48.0, - constraints.maxWidth, - ); - - final row = Row( - mainAxisAlignment: - MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Opacity( - opacity: 0.0, - child: Container( - padding: - const EdgeInsets.symmetric( - horizontal: iconPaddingX, - vertical: iconPaddingY, - ), - decoration: BoxDecoration( - color: context - .conduitTheme - .surfaceBackground - .withValues(alpha: 0.3), - borderRadius: - BorderRadius.circular( - AppBorderRadius.badge, - ), - border: Border.all( - color: context - .conduitTheme - .dividerColor, - width: BorderWidth.thin, - ), - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons - .chevron_down - : Icons - .keyboard_arrow_down, - color: context - .conduitTheme - .iconSecondary, - size: iconWidth, - ), - ), - ), - const SizedBox(width: Spacing.xs), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxLabelWidth, - ), - child: MiddleEllipsisText( - modelLabel, - style: modelTextStyle, - textAlign: TextAlign.center, - semanticsLabel: modelLabel, - ), - ), - const SizedBox(width: Spacing.xs), - Container( - padding: - const EdgeInsets.symmetric( - horizontal: iconPaddingX, - vertical: iconPaddingY, - ), - decoration: BoxDecoration( - color: context - .conduitTheme - .surfaceBackground - .withValues(alpha: 0.3), - borderRadius: - BorderRadius.circular( - AppBorderRadius.badge, - ), - border: Border.all( - color: context - .conduitTheme - .dividerColor, - width: BorderWidth.thin, - ), - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons - .chevron_down - : Icons.keyboard_arrow_down, - color: context - .conduitTheme - .iconSecondary, - size: iconWidth, - ), - ), - ], - ); - final constrainedRow = ConstrainedBox( - constraints: BoxConstraints( - maxWidth: constraints.maxWidth, - ), - child: row, - ); - return hasConversationTitle - ? SizedBox( - height: 24, - child: constrainedRow, - ) - : constrainedRow; - }(), - ), - if (isReviewerMode) - Padding( - padding: const EdgeInsets.only( - top: 2.0, + child: _buildAppBarPill( + context: context, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: + constraints.maxWidth - + Spacing.xxl, ), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: 1.0, - ), - decoration: BoxDecoration( - color: context.conduitTheme.success - .withValues(alpha: 0.1), - borderRadius: BorderRadius.circular( - AppBorderRadius.badge, - ), - border: Border.all( - color: context - .conduitTheme - .success - .withValues(alpha: 0.3), - width: BorderWidth.thin, - ), - ), - child: Text( - 'REVIEWER MODE', - style: AppTypography.captionStyle - .copyWith( - color: context - .conduitTheme - .success, - fontWeight: FontWeight.w600, - fontSize: 9, - ), - ), + child: MiddleEllipsisText( + modelLabel, + style: modelTextStyle, + textAlign: TextAlign.center, + semanticsLabel: modelLabel, ), ), - ], + const SizedBox(width: Spacing.xs), + Icon( + Platform.isIOS + ? CupertinoIcons.chevron_down + : Icons.keyboard_arrow_down, + color: + context.conduitTheme.iconSecondary, + size: IconSize.small, + ), + ], + ), ), ), ); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (titlePill != null) ...[ + titlePill, + const SizedBox(height: Spacing.xs), + ], + modelPill, + if (isReviewerMode) + Padding( + padding: const EdgeInsets.only( + top: Spacing.xs, + ), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: 1.0, + ), + decoration: BoxDecoration( + color: context.conduitTheme.success + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular( + AppBorderRadius.badge, + ), + border: Border.all( + color: context.conduitTheme.success + .withValues(alpha: 0.3), + width: BorderWidth.thin, + ), + ), + child: Text( + 'REVIEWER MODE', + style: AppTypography.captionStyle + .copyWith( + color: + context.conduitTheme.success, + fontWeight: FontWeight.w600, + fontSize: 9, + ), + ), + ), + ), + ], + ); }, ), actions: [ @@ -1942,26 +1929,43 @@ class _ChatPageState extends ConsumerState { padding: const EdgeInsets.only( right: Spacing.inputPadding, ), - child: IconButton( - icon: Icon( - Platform.isIOS - ? CupertinoIcons.create - : Icons.add_comment, - color: context.conduitTheme.textPrimary, - size: IconSize.appBar, + child: Tooltip( + message: AppLocalizations.of(context)!.newChat, + child: GestureDetector( + onTap: _handleNewChat, + child: _buildAppBarPill( + context: context, + isCircular: true, + child: Icon( + Platform.isIOS + ? CupertinoIcons.create + : Icons.add_comment, + color: context.conduitTheme.textPrimary, + size: IconSize.appBar, + ), + ), ), - onPressed: _handleNewChat, - tooltip: AppLocalizations.of(context)!.newChat, ), ), ] else ...[ - IconButton( - icon: Icon( - Platform.isIOS ? CupertinoIcons.delete : Icons.delete, - color: context.conduitTheme.error, - size: IconSize.appBar, + Padding( + padding: const EdgeInsets.only( + right: Spacing.inputPadding, + ), + child: GestureDetector( + onTap: _deleteSelectedMessages, + child: _buildAppBarPill( + context: context, + isCircular: true, + child: Icon( + Platform.isIOS + ? CupertinoIcons.delete + : Icons.delete, + color: context.conduitTheme.error, + size: IconSize.appBar, + ), + ), ), - onPressed: _deleteSelectedMessages, ), ], ], @@ -2092,6 +2096,37 @@ class _ChatPageState extends ConsumerState { ), ), + // Floating app bar gradient overlay + Positioned( + top: 0, + left: 0, + right: 0, + child: IgnorePointer( + child: Container( + height: + MediaQuery.of(context).padding.top + + kToolbarHeight + + Spacing.xl, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, 0.6, 1.0], + colors: [ + theme.scaffoldBackgroundColor, + theme.scaffoldBackgroundColor.withValues( + alpha: 0.85, + ), + theme.scaffoldBackgroundColor.withValues( + alpha: 0.0, + ), + ], + ), + ), + ), + ), + ), + // Floating Scroll to Bottom Button with smooth appear/disappear Positioned( bottom: From 4c80d9f521fbedeb08f3369cb2b7937814b61c80 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:57:53 +0530 Subject: [PATCH 3/4] fix(chat): Adjust input and padding spacing for better UI layout --- lib/features/chat/views/chat_page.dart | 2 +- lib/features/chat/widgets/modern_chat_input.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 65e2a86..f9e5f56 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -2111,7 +2111,7 @@ class _ChatPageState extends ConsumerState { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - stops: const [0.0, 0.6, 1.0], + stops: const [0.0, 0.4, 1.0], colors: [ theme.scaffoldBackgroundColor, theme.scaffoldBackgroundColor.withValues( diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index dd7ec04..27154dd 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -1279,7 +1279,7 @@ class _ModernChatInputState extends ConsumerState Spacing.inputPadding, 0, Spacing.inputPadding, - 0, + Spacing.sm, ), child: Row( children: [ @@ -1421,9 +1421,9 @@ class _ModernChatInputState extends ConsumerState final bottomPadding = MediaQuery.of(context).viewPadding.bottom; return Padding( padding: EdgeInsets.fromLTRB( - Spacing.sm, + Spacing.screenPadding, 0, - Spacing.sm, + Spacing.screenPadding, bottomPadding + Spacing.md, ), child: shell, From 289fbcb49eba0e5adbbe27e299d7db7d3dd4df46 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:38:52 +0530 Subject: [PATCH 4/4] feat(theme): Add system overlay style for consistent UI appearance --- lib/features/chat/views/chat_page.dart | 8 +++----- lib/shared/theme/app_theme.dart | 8 ++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index f9e5f56..8ed8a88 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -2129,11 +2129,9 @@ class _ChatPageState extends ConsumerState { // Floating Scroll to Bottom Button with smooth appear/disappear Positioned( - bottom: - ((_inputHeight > 0) - ? _inputHeight - : (Spacing.xxl + Spacing.xxxl)) + - Spacing.sm, + bottom: (_inputHeight > 0) + ? _inputHeight + : (Spacing.xxl + Spacing.xxxl), left: 0, right: 0, child: AnimatedSwitcher( diff --git a/lib/shared/theme/app_theme.dart b/lib/shared/theme/app_theme.dart index d7f9684..ccd8877 100644 --- a/lib/shared/theme/app_theme.dart +++ b/lib/shared/theme/app_theme.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'theme_extensions.dart'; import 'tweakcn_themes.dart'; @@ -115,6 +116,13 @@ class AppTheme { elevation: Elevation.none, backgroundColor: surfaces.background, foregroundColor: tokens.neutralOnSurface, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarBrightness: brightness, + statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, + systemNavigationBarIconBrightness: isDark + ? Brightness.light + : Brightness.dark, + ), ), bottomSheetTheme: BottomSheetThemeData( backgroundColor: surfaces.card,