From 2538181f8ad006c405951787640c18767bbba773 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:12:29 +0530 Subject: [PATCH] refactor: enhance chat input UI with compact composer and improved styling - Introduced a compact composer mode for the chat input, optimizing space when no quick pills are available. - Refactored the input shell decoration and layout to improve visual consistency and responsiveness. - Enhanced the text field and button styling, ensuring better integration with the current theme and improved user experience. - Streamlined the build method by encapsulating the composer text field logic into a separate method for better readability and maintainability. --- .../chat/widgets/modern_chat_input.dart | 670 ++++++++++-------- 1 file changed, 364 insertions(+), 306 deletions(-) diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index d91b967..66a4308 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -723,22 +723,203 @@ class _ModernChatInputState extends ConsumerState } } + final bool showCompactComposer = quickPills.isEmpty; + + final BorderRadius shellRadius = BorderRadius.circular( + showCompactComposer ? AppBorderRadius.round : _composerRadius, + ); + + final BoxDecoration shellDecoration = BoxDecoration( + color: showCompactComposer ? Colors.transparent : 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), + ), + ], + ); + + final List composerChildren = [ + if (_showPromptOverlay) + Padding( + padding: const EdgeInsets.fromLTRB( + Spacing.sm, + 0, + Spacing.sm, + Spacing.xs, + ), + child: _buildPromptOverlay(context), + ), + if (showCompactComposer) + Padding( + padding: const EdgeInsets.fromLTRB( + Spacing.screenPadding, + Spacing.xs, + Spacing.screenPadding, + Spacing.sm, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildOverflowButton( + tooltip: AppLocalizations.of(context)!.more, + webSearchActive: webSearchEnabled, + imageGenerationActive: imageGenEnabled, + toolsActive: selectedToolIds.isNotEmpty, + ), + const SizedBox(width: Spacing.sm), + Expanded( + 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: brightness == Brightness.dark + ? composerSurface.withValues(alpha: 0.9) + : context.conduitTheme.surfaceContainer, + borderRadius: BorderRadius.circular(AppBorderRadius.round), + border: Border.all( + color: outlineColor.withValues( + alpha: brightness == Brightness.dark ? 0.32 : 0.2, + ), + width: BorderWidth.micro, + ), + boxShadow: [ + BoxShadow( + color: shellShadowColor.withValues( + alpha: brightness == Brightness.dark ? 0.4 : 0.22, + ), + blurRadius: 24, + spreadRadius: -6, + offset: const Offset(0, 12), + ), + ], + ), + child: Align( + alignment: Alignment.centerLeft, + child: _buildComposerTextField( + brightness: brightness, + sendOnEnter: sendOnEnter, + placeholderBase: placeholderBase, + placeholderFocused: placeholderFocused, + contentPadding: const EdgeInsets.symmetric( + vertical: Spacing.xs, + ), + isActive: isActive, + ), + ), + ), + ), + const SizedBox(width: Spacing.sm), + _buildPrimaryButton( + _hasText, + isGenerating, + stopGeneration, + voiceAvailable, + ), + ], + ), + ) + else ...[ + Padding( + padding: const EdgeInsets.fromLTRB( + Spacing.sm, + Spacing.xs, + Spacing.sm, + Spacing.xs, + ), + child: Container( + padding: const EdgeInsets.fromLTRB( + Spacing.sm, + Spacing.xs, + Spacing.sm, + Spacing.xs, + ), + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(_composerRadius), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: _buildComposerTextField( + brightness: brightness, + sendOnEnter: sendOnEnter, + placeholderBase: placeholderBase, + placeholderFocused: placeholderFocused, + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.sm, + vertical: Spacing.xs, + ), + isActive: isActive, + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + Spacing.inputPadding, + 0, + Spacing.inputPadding, + 0, + ), + child: Row( + children: [ + _buildOverflowButton( + tooltip: AppLocalizations.of(context)!.more, + webSearchActive: webSearchEnabled, + imageGenerationActive: imageGenEnabled, + toolsActive: selectedToolIds.isNotEmpty, + ), + const SizedBox(width: Spacing.xs), + Expanded( + child: ClipRect( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: _withHorizontalSpacing(quickPills, Spacing.xxs), + ), + ), + ), + ), + const SizedBox(width: Spacing.sm), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildPrimaryButton( + _hasText, + isGenerating, + stopGeneration, + voiceAvailable, + ), + ], + ), + ], + ), + ), + ], + ]; + Widget shell = AnimatedContainer( duration: const Duration(milliseconds: 180), curve: Curves.easeOutCubic, - decoration: BoxDecoration( - color: composerBackground, - borderRadius: BorderRadius.circular(_composerRadius), - border: Border.all(color: outlineColor, width: BorderWidth.thin), - boxShadow: [ - BoxShadow( - color: shellShadowColor, - blurRadius: 12 + (isActive ? 4 : 0), - spreadRadius: -2, - offset: const Offset(0, -2), - ), - ], - ), + decoration: shellDecoration, width: double.infinity, child: SafeArea( top: false, @@ -756,297 +937,7 @@ class _ModernChatInputState extends ConsumerState child: RepaintBoundary( child: Column( mainAxisSize: MainAxisSize.min, - children: [ - if (_showPromptOverlay) - Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.sm, - 0, - Spacing.sm, - Spacing.xs, - ), - child: _buildPromptOverlay(context), - ), - Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.sm, - Spacing.xs, - Spacing.sm, - Spacing.xs, - ), - child: Container( - padding: const EdgeInsets.fromLTRB( - Spacing.sm, - Spacing.xs, - Spacing.sm, - Spacing.xs, - ), - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(_composerRadius), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (!widget.enabled) return; - _ensureFocusedIfEnabled(); - }, - child: Semantics( - textField: true, - label: AppLocalizations.of( - context, - )!.messageInputLabel, - hint: AppLocalizations.of( - context, - )!.messageInputHint, - child: Shortcuts( - shortcuts: () { - final map = { - LogicalKeySet( - LogicalKeyboardKey.meta, - LogicalKeyboardKey.enter, - ): const _SendMessageIntent(), - LogicalKeySet( - LogicalKeyboardKey.control, - LogicalKeyboardKey.enter, - ): const _SendMessageIntent(), - }; - if (sendOnEnter) { - map[LogicalKeySet( - LogicalKeyboardKey.enter, - )] = - const _SendMessageIntent(); - map[LogicalKeySet( - LogicalKeyboardKey.shift, - LogicalKeyboardKey.enter, - )] = - const _InsertNewlineIntent(); - } - if (_showPromptOverlay) { - map[LogicalKeySet( - LogicalKeyboardKey.arrowDown, - )] = - const _SelectNextPromptIntent(); - map[LogicalKeySet( - LogicalKeyboardKey.arrowUp, - )] = - const _SelectPreviousPromptIntent(); - map[LogicalKeySet( - LogicalKeyboardKey.escape, - )] = - const _DismissPromptIntent(); - } - return map; - }(), - child: Actions( - actions: >{ - _SendMessageIntent: - CallbackAction<_SendMessageIntent>( - onInvoke: (intent) { - if (_showPromptOverlay) { - _confirmPromptSelection(); - return null; - } - _sendMessage(); - return null; - }, - ), - _InsertNewlineIntent: - CallbackAction< - _InsertNewlineIntent - >( - onInvoke: (intent) { - _insertNewline(); - return null; - }, - ), - _SelectNextPromptIntent: - CallbackAction< - _SelectNextPromptIntent - >( - onInvoke: (intent) { - _movePromptSelection(1); - return null; - }, - ), - _SelectPreviousPromptIntent: - CallbackAction< - _SelectPreviousPromptIntent - >( - onInvoke: (intent) { - _movePromptSelection(-1); - return null; - }, - ), - _DismissPromptIntent: - CallbackAction< - _DismissPromptIntent - >( - onInvoke: (intent) { - _hidePromptOverlay(); - return null; - }, - ), - }, - child: TweenAnimationBuilder( - tween: Tween( - begin: 0.0, - end: isActive ? 1.0 : 0.0, - ), - duration: const Duration( - milliseconds: 180, - ), - curve: Curves.easeOutCubic, - builder: (context, factor, child) { - final Color animatedPlaceholder = - Color.lerp( - placeholderBase, - placeholderFocused, - factor, - )!; - final Color animatedTextColor = - Color.lerp( - context.conduitTheme.inputText - .withValues(alpha: 0.88), - context.conduitTheme.inputText, - factor, - )!; - - final FontWeight recordingWeight = - _isRecording - ? FontWeight.w500 - : FontWeight.w400; - final TextStyle baseChatStyle = - AppTypography.chatMessageStyle; - - return TextField( - controller: _controller, - focusNode: _focusNode, - enabled: widget.enabled, - autofocus: false, - minLines: 1, - maxLines: null, - keyboardType: - TextInputType.multiline, - textCapitalization: - TextCapitalization.sentences, - textInputAction: sendOnEnter - ? TextInputAction.send - : TextInputAction.newline, - autofillHints: const [], - showCursor: true, - scrollPadding: - const EdgeInsets.only( - bottom: 80, - ), - keyboardAppearance: brightness, - cursorColor: animatedTextColor, - style: baseChatStyle.copyWith( - color: animatedTextColor, - fontStyle: _isRecording - ? FontStyle.italic - : FontStyle.normal, - fontWeight: recordingWeight, - ), - decoration: InputDecoration( - hintText: AppLocalizations.of( - context, - )!.messageHintText, - hintStyle: baseChatStyle.copyWith( - color: animatedPlaceholder, - fontWeight: recordingWeight, - fontStyle: _isRecording - ? FontStyle.italic - : FontStyle.normal, - ), - filled: false, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - contentPadding: - const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xs, - ), - isDense: true, - alignLabelWithHint: true, - ), - onSubmitted: (_) { - if (sendOnEnter) { - _sendMessage(); - } - }, - onTap: () { - if (!widget.enabled) return; - _ensureFocusedIfEnabled(); - }, - ); - }, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.inputPadding, - 0, - Spacing.inputPadding, - 0, - ), - child: Row( - children: [ - _buildOverflowButton( - tooltip: AppLocalizations.of(context)!.more, - webSearchActive: webSearchEnabled, - imageGenerationActive: imageGenEnabled, - toolsActive: selectedToolIds.isNotEmpty, - ), - const SizedBox(width: Spacing.xs), - Expanded( - child: quickPills.isEmpty - ? const SizedBox.shrink() - : ClipRect( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: _withHorizontalSpacing( - quickPills, - Spacing.xxs, - ), - ), - ), - ), - ), - const SizedBox(width: Spacing.sm), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildPrimaryButton( - _hasText, - isGenerating, - stopGeneration, - voiceAvailable, - ), - ], - ), - ], - ), - ), - ], + children: composerChildren, ), ), ), @@ -1055,9 +946,9 @@ class _ModernChatInputState extends ConsumerState ), ); - if (brightness == Brightness.dark) { + if (brightness == Brightness.dark && !showCompactComposer) { shell = ClipRRect( - borderRadius: BorderRadius.circular(_composerRadius), + borderRadius: shellRadius, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), child: shell, @@ -1088,6 +979,173 @@ class _ModernChatInputState extends ConsumerState return result; } + Widget _buildComposerTextField({ + required Brightness brightness, + required bool sendOnEnter, + required Color placeholderBase, + required Color placeholderFocused, + required EdgeInsetsGeometry contentPadding, + required bool isActive, + }) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (!widget.enabled) return; + _ensureFocusedIfEnabled(); + }, + child: Semantics( + textField: true, + label: AppLocalizations.of(context)!.messageInputLabel, + hint: AppLocalizations.of(context)!.messageInputHint, + child: Shortcuts( + shortcuts: () { + final map = { + LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.enter): + const _SendMessageIntent(), + LogicalKeySet( + LogicalKeyboardKey.control, + LogicalKeyboardKey.enter, + ): const _SendMessageIntent(), + }; + if (sendOnEnter) { + map[LogicalKeySet(LogicalKeyboardKey.enter)] = + const _SendMessageIntent(); + map[LogicalKeySet( + LogicalKeyboardKey.shift, + LogicalKeyboardKey.enter, + )] = + const _InsertNewlineIntent(); + } + if (_showPromptOverlay) { + map[LogicalKeySet(LogicalKeyboardKey.arrowDown)] = + const _SelectNextPromptIntent(); + map[LogicalKeySet(LogicalKeyboardKey.arrowUp)] = + const _SelectPreviousPromptIntent(); + map[LogicalKeySet(LogicalKeyboardKey.escape)] = + const _DismissPromptIntent(); + } + return map; + }(), + child: Actions( + actions: >{ + _SendMessageIntent: CallbackAction<_SendMessageIntent>( + onInvoke: (intent) { + if (_showPromptOverlay) { + _confirmPromptSelection(); + return null; + } + _sendMessage(); + return null; + }, + ), + _InsertNewlineIntent: CallbackAction<_InsertNewlineIntent>( + onInvoke: (intent) { + _insertNewline(); + return null; + }, + ), + _SelectNextPromptIntent: CallbackAction<_SelectNextPromptIntent>( + onInvoke: (intent) { + _movePromptSelection(1); + return null; + }, + ), + _SelectPreviousPromptIntent: + CallbackAction<_SelectPreviousPromptIntent>( + onInvoke: (intent) { + _movePromptSelection(-1); + return null; + }, + ), + _DismissPromptIntent: CallbackAction<_DismissPromptIntent>( + onInvoke: (intent) { + _hidePromptOverlay(); + return null; + }, + ), + }, + child: TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: isActive ? 1.0 : 0.0), + duration: const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + builder: (context, factor, child) { + final Color animatedPlaceholder = Color.lerp( + placeholderBase, + placeholderFocused, + factor, + )!; + final Color animatedTextColor = Color.lerp( + context.conduitTheme.inputText.withValues(alpha: 0.88), + context.conduitTheme.inputText, + factor, + )!; + + final FontWeight recordingWeight = _isRecording + ? FontWeight.w500 + : FontWeight.w400; + final TextStyle baseChatStyle = AppTypography.chatMessageStyle; + + return TextField( + controller: _controller, + focusNode: _focusNode, + enabled: widget.enabled, + autofocus: false, + minLines: 1, + maxLines: null, + keyboardType: TextInputType.multiline, + textCapitalization: TextCapitalization.sentences, + textInputAction: sendOnEnter + ? TextInputAction.send + : TextInputAction.newline, + autofillHints: const [], + showCursor: true, + scrollPadding: const EdgeInsets.only(bottom: 80), + keyboardAppearance: brightness, + cursorColor: animatedTextColor, + style: baseChatStyle.copyWith( + color: animatedTextColor, + fontStyle: _isRecording + ? FontStyle.italic + : FontStyle.normal, + fontWeight: recordingWeight, + ), + decoration: InputDecoration( + hintText: AppLocalizations.of(context)!.messageHintText, + hintStyle: baseChatStyle.copyWith( + color: animatedPlaceholder, + fontWeight: recordingWeight, + fontStyle: _isRecording + ? FontStyle.italic + : FontStyle.normal, + ), + filled: false, + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: contentPadding, + isDense: true, + alignLabelWithHint: true, + ), + onSubmitted: (_) { + if (sendOnEnter) { + _sendMessage(); + } + }, + onTap: () { + if (!widget.enabled) return; + _ensureFocusedIfEnabled(); + }, + ); + }, + ), + ), + ), + ), + ); + } + Widget _buildOverflowButton({ required String tooltip, required bool webSearchActive,