diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index a888cb8..7af8cc7 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -709,12 +709,13 @@ class _ChatPageState extends ConsumerState { ?.cast(); final name = meta?['name']?.toString() ?? parsed.host; - final collectionName = - result?['collection_name']?.toString(); + final collectionName = result?['collection_name'] + ?.toString(); // Add as appropriate type - final notifier = - ref.read(contextAttachmentsProvider.notifier); + final notifier = ref.read( + contextAttachmentsProvider.notifier, + ); if (isYoutube) { notifier.addYoutube( displayName: name, @@ -1680,28 +1681,29 @@ class _ChatPageState extends ConsumerState { constraints: BoxConstraints( maxWidth: constraints.maxWidth, ), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.center, - 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: [ - StreamingTitleText( + 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 @@ -1720,96 +1722,45 @@ class _ChatPageState extends ConsumerState { .textPrimary .withValues(alpha: 0.8), ), - const SizedBox( - height: Spacing.xs, - ), - ], - ) - : const SizedBox.shrink( - key: ValueKey( - 'empty-title', ), + 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, - ); + ), + ), + 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( + final row = Row( + mainAxisAlignment: + MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Opacity( + opacity: 0.0, + child: Container( padding: const EdgeInsets.symmetric( horizontal: iconPaddingX, @@ -1843,64 +1794,107 @@ class _ChatPageState extends ConsumerState { 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, + 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: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: 1.0, + child: row, + ); + return hasConversationTitle + ? SizedBox( + height: 24, + child: constrainedRow, + ) + : constrainedRow; + }(), + ), + if (isReviewerMode) + Padding( + padding: const EdgeInsets.only( + top: 2.0, + ), + 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, ), - decoration: BoxDecoration( + border: Border.all( 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, - ), + .withValues(alpha: 0.3), + width: BorderWidth.thin, ), ), + child: Text( + 'REVIEWER MODE', + style: AppTypography.captionStyle + .copyWith( + color: context + .conduitTheme + .success, + fontWeight: FontWeight.w600, + fontSize: 9, + ), + ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/features/chat/widgets/streaming_title_text.dart b/lib/features/chat/widgets/streaming_title_text.dart index c0d0f09..d3fa7e6 100644 --- a/lib/features/chat/widgets/streaming_title_text.dart +++ b/lib/features/chat/widgets/streaming_title_text.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import '../../../shared/widgets/middle_ellipsis_text.dart'; + /// Displays a chat title that reveals characters with a streaming animation /// whenever the title changes. class StreamingTitleText extends StatefulWidget { @@ -141,36 +143,53 @@ class _StreamingTitleTextState extends State ? widget.style.fontSize! * (widget.style.height ?? 1.1) : 18.0); - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - child: Text( - // When the animation completes we fall back to the full string. - revealedGlyphs >= totalGlyphs ? _activeTitle : visibleText, - maxLines: 1, - overflow: TextOverflow.fade, - softWrap: false, - textAlign: TextAlign.center, - style: widget.style, - ), - ), - if (isAnimating) - FadeTransition( - opacity: _cursorOpacity, - child: Container( - width: widget.cursorWidth, - height: cursorHeight, - margin: const EdgeInsets.only(left: 2), - decoration: BoxDecoration( - color: cursorColor, - borderRadius: BorderRadius.circular(widget.cursorWidth), - ), + // When animation is complete, use middle ellipsis for overflow. + // During animation, show partial text with standard Text widget. + final bool animationComplete = revealedGlyphs >= totalGlyphs; + + // Use middle ellipsis when animation is complete + if (animationComplete) { + return MiddleEllipsisText( + _activeTitle, + style: widget.style, + textAlign: TextAlign.center, + semanticsLabel: _activeTitle, + ); + } + + // During animation, use IntrinsicWidth to size the row to the text, + // then clip any overflow from the cursor + return ClipRect( + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: Text( + visibleText, + maxLines: 1, + overflow: TextOverflow.clip, + softWrap: false, + textAlign: TextAlign.center, + style: widget.style, ), ), - ], + if (isAnimating) + FadeTransition( + opacity: _cursorOpacity, + child: Container( + width: widget.cursorWidth, + height: cursorHeight, + margin: const EdgeInsets.only(left: 2), + decoration: BoxDecoration( + color: cursorColor, + borderRadius: BorderRadius.circular(widget.cursorWidth), + ), + ), + ), + ], + ), ); } diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 91ec7da..4117e99 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -29,6 +29,7 @@ import '../../../core/models/folder.dart'; import '../../../core/persistence/persistence_keys.dart'; import '../../../core/persistence/hive_boxes.dart'; import 'package:hive_ce/hive.dart'; +import '../../../shared/widgets/middle_ellipsis_text.dart'; /// Defines the section types that can be collapsed in the chats drawer enum _SectionType { pinned, recent } @@ -418,17 +419,19 @@ class _ChatsDrawerState extends ConsumerState { modelsById: modelsById, ), ); + out.add( + const SliverToBoxAdapter( + child: SizedBox(height: Spacing.sm), + ), + ); + } else { + // Only add spacing after collapsed folders out.add( const SliverToBoxAdapter( child: SizedBox(height: Spacing.xs), ), ); } - out.add( - const SliverToBoxAdapter( - child: SizedBox(height: Spacing.xs), - ), - ); } return out.isEmpty ? [ @@ -665,6 +668,13 @@ class _ChatsDrawerState extends ConsumerState { child: SizedBox(height: Spacing.sm), ), ); + } else { + // Only add spacing after collapsed folders + out.add( + const SliverToBoxAdapter( + child: SizedBox(height: Spacing.xs), + ), + ); } } return out.isEmpty @@ -1903,11 +1913,10 @@ class _ConversationTileContent extends StatelessWidget { ], Flexible( fit: textFit, - child: Text( + child: MiddleEllipsisText( title, - maxLines: 1, - overflow: TextOverflow.ellipsis, style: textStyle, + semanticsLabel: title, ), ), ...trailing,