refactor: improve chat input and message list layout, enhance keyboard visibility handling
This commit is contained in:
@@ -9,7 +9,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'dart:io' show Platform, File;
|
import 'dart:io' show Platform, File;
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui' show ImageFilter;
|
|
||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../providers/chat_providers.dart';
|
import '../providers/chat_providers.dart';
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
@@ -941,6 +940,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
// Watch reviewer mode and auto-select model if needed
|
// Watch reviewer mode and auto-select model if needed
|
||||||
final isReviewerMode = ref.watch(reviewerModeProvider);
|
final isReviewerMode = ref.watch(reviewerModeProvider);
|
||||||
|
|
||||||
|
// Keyboard visibility
|
||||||
|
final keyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
|
||||||
|
|
||||||
// Auto-select model when in reviewer mode with no selection
|
// Auto-select model when in reviewer mode with no selection
|
||||||
if (isReviewerMode && selectedModel == null) {
|
if (isReviewerMode && selectedModel == null) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
@@ -1340,7 +1342,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
child: _buildMessagesList(theme),
|
child: RepaintBoundary(
|
||||||
|
child: _buildMessagesList(theme),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1352,17 +1356,19 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
const ChatOfflineOverlay(),
|
const ChatOfflineOverlay(),
|
||||||
|
|
||||||
// Modern Input (root matches input background including safe area)
|
// Modern Input (root matches input background including safe area)
|
||||||
ModernChatInput(
|
RepaintBoundary(
|
||||||
enabled:
|
child: ModernChatInput(
|
||||||
selectedModel != null &&
|
enabled:
|
||||||
(isOnline || ref.watch(reviewerModeProvider)),
|
selectedModel != null &&
|
||||||
onSendMessage: (text) =>
|
(isOnline || ref.watch(reviewerModeProvider)),
|
||||||
_handleMessageSend(text, selectedModel),
|
onSendMessage: (text) =>
|
||||||
onVoiceInput: null,
|
_handleMessageSend(text, selectedModel),
|
||||||
onFileAttachment: _handleFileAttachment,
|
onVoiceInput: null,
|
||||||
onImageAttachment: _handleImageAttachment,
|
onFileAttachment: _handleFileAttachment,
|
||||||
onCameraCapture: () =>
|
onImageAttachment: _handleImageAttachment,
|
||||||
_handleImageAttachment(fromCamera: true),
|
onCameraCapture: () =>
|
||||||
|
_handleImageAttachment(fromCamera: true),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1390,44 +1396,42 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
},
|
},
|
||||||
child:
|
child:
|
||||||
(_showScrollToBottom &&
|
(_showScrollToBottom &&
|
||||||
|
!keyboardVisible &&
|
||||||
ref.watch(chatMessagesProvider).isNotEmpty)
|
ref.watch(chatMessagesProvider).isNotEmpty)
|
||||||
? ClipRRect(
|
? ClipRRect(
|
||||||
key: const ValueKey('scroll_to_bottom_visible'),
|
key: const ValueKey('scroll_to_bottom_visible'),
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
AppBorderRadius.floatingButton,
|
AppBorderRadius.floatingButton,
|
||||||
),
|
),
|
||||||
child: BackdropFilter(
|
child: Container(
|
||||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
decoration: BoxDecoration(
|
||||||
child: Container(
|
color: context
|
||||||
decoration: BoxDecoration(
|
.conduitTheme
|
||||||
color: context
|
.surfaceContainerHighest
|
||||||
.conduitTheme
|
.withValues(alpha: 0.75),
|
||||||
.surfaceContainerHighest
|
border: Border.all(
|
||||||
.withValues(alpha: 0.75),
|
color: context.conduitTheme.cardBorder
|
||||||
border: Border.all(
|
.withValues(alpha: 0.3),
|
||||||
color: context.conduitTheme.cardBorder
|
width: BorderWidth.regular,
|
||||||
.withValues(alpha: 0.3),
|
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
AppBorderRadius.floatingButton,
|
|
||||||
),
|
|
||||||
boxShadow: ConduitShadows.button,
|
|
||||||
),
|
),
|
||||||
child: SizedBox(
|
borderRadius: BorderRadius.circular(
|
||||||
width: TouchTarget.button,
|
AppBorderRadius.floatingButton,
|
||||||
height: TouchTarget.button,
|
),
|
||||||
child: IconButton(
|
boxShadow: ConduitShadows.button,
|
||||||
onPressed: _scrollToBottom,
|
),
|
||||||
splashRadius: 24,
|
child: SizedBox(
|
||||||
icon: Icon(
|
width: TouchTarget.button,
|
||||||
Platform.isIOS
|
height: TouchTarget.button,
|
||||||
? CupertinoIcons.arrow_down
|
child: IconButton(
|
||||||
: Icons.keyboard_arrow_down,
|
onPressed: _scrollToBottom,
|
||||||
size: IconSize.lg,
|
splashRadius: 24,
|
||||||
color: context.conduitTheme.iconPrimary
|
icon: Icon(
|
||||||
.withValues(alpha: 0.9),
|
Platform.isIOS
|
||||||
),
|
? CupertinoIcons.arrow_down
|
||||||
|
: Icons.keyboard_arrow_down,
|
||||||
|
size: IconSize.lg,
|
||||||
|
color: context.conduitTheme.iconPrimary
|
||||||
|
.withValues(alpha: 0.9),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -231,7 +231,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
children: [
|
children: [
|
||||||
// Main input area with unified 2-row design
|
// Main input area with unified 2-row design
|
||||||
Container(
|
Container(
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.conduitTheme.inputBackground,
|
color: context.conduitTheme.inputBackground,
|
||||||
borderRadius: const BorderRadius.vertical(
|
borderRadius: const BorderRadius.vertical(
|
||||||
@@ -269,137 +268,22 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
child: Column(
|
child: RepaintBoundary(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
// Collapsed/Expanded top row: text input with left/right buttons in collapsed
|
children: [
|
||||||
Padding(
|
// Collapsed/Expanded top row: text input with left/right buttons in collapsed
|
||||||
padding: const EdgeInsets.only(
|
Padding(
|
||||||
left: Spacing.inputPadding,
|
|
||||||
right: Spacing.inputPadding,
|
|
||||||
top: Spacing.inputPadding,
|
|
||||||
bottom: Spacing.inputPadding,
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (!_isExpanded) ...[
|
|
||||||
_buildRoundButton(
|
|
||||||
icon: Icons.add,
|
|
||||||
onTap: widget.enabled
|
|
||||||
? _showAttachmentOptions
|
|
||||||
: null,
|
|
||||||
tooltip: AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
)!.addAttachment,
|
|
||||||
showBackground: false,
|
|
||||||
iconSize: IconSize.large + 2.0,
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
] else ...[
|
|
||||||
// When expanded, the left padding was reduced to move the plus button.
|
|
||||||
// Add back spacing so the text field aligns comfortably from the edge.
|
|
||||||
SizedBox(
|
|
||||||
width: Spacing.inputPadding - Spacing.xs,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
// Text input expands to fill
|
|
||||||
Expanded(
|
|
||||||
child: Semantics(
|
|
||||||
textField: true,
|
|
||||||
label: AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
)!.messageInputLabel,
|
|
||||||
hint: AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
)!.messageInputHint,
|
|
||||||
child: TextField(
|
|
||||||
controller: _controller,
|
|
||||||
focusNode: _focusNode,
|
|
||||||
enabled: widget.enabled,
|
|
||||||
autofocus: false,
|
|
||||||
maxLines: _isExpanded ? null : 1,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
textCapitalization:
|
|
||||||
TextCapitalization.sentences,
|
|
||||||
textInputAction: TextInputAction.newline,
|
|
||||||
showCursor: true,
|
|
||||||
cursorColor: context.conduitTheme.inputText,
|
|
||||||
style: AppTypography.chatMessageStyle
|
|
||||||
.copyWith(
|
|
||||||
color: context.conduitTheme.inputText,
|
|
||||||
),
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
)!.messageHintText,
|
|
||||||
hintStyle: TextStyle(
|
|
||||||
color: context
|
|
||||||
.conduitTheme
|
|
||||||
.inputPlaceholder,
|
|
||||||
fontSize: AppTypography.bodyLarge,
|
|
||||||
fontWeight: _isRecording
|
|
||||||
? FontWeight.w500
|
|
||||||
: FontWeight.w400,
|
|
||||||
fontStyle: _isRecording
|
|
||||||
? FontStyle.italic
|
|
||||||
: FontStyle.normal,
|
|
||||||
),
|
|
||||||
// Ensure the text field background matches its parent container
|
|
||||||
// and does not use the global InputDecorationTheme fill
|
|
||||||
filled: false,
|
|
||||||
border: InputBorder.none,
|
|
||||||
enabledBorder: InputBorder.none,
|
|
||||||
focusedBorder: InputBorder.none,
|
|
||||||
errorBorder: InputBorder.none,
|
|
||||||
disabledBorder: InputBorder.none,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
isDense: true,
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
// Removed onChanged setState to reduce rebuilds
|
|
||||||
onSubmitted: (_) => _sendMessage(),
|
|
||||||
onTap: () {
|
|
||||||
if (!widget.enabled) return;
|
|
||||||
if (!_isExpanded) {
|
|
||||||
_setExpanded(true);
|
|
||||||
WidgetsBinding.instance
|
|
||||||
.addPostFrameCallback((_) {
|
|
||||||
if (!mounted) return;
|
|
||||||
_ensureFocusedIfEnabled();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_ensureFocusedIfEnabled();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!_isExpanded) ...[
|
|
||||||
const SizedBox(width: Spacing.sm),
|
|
||||||
// Primary action button (Send/Stop) when collapsed
|
|
||||||
_buildPrimaryButton(
|
|
||||||
_hasText,
|
|
||||||
isGenerating,
|
|
||||||
stopGeneration,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Expanded bottom row with additional options
|
|
||||||
if (_isExpanded) ...[
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: Spacing.inputPadding,
|
left: Spacing.inputPadding,
|
||||||
right: Spacing.inputPadding,
|
right: Spacing.inputPadding,
|
||||||
|
top: Spacing.inputPadding,
|
||||||
bottom: Spacing.inputPadding,
|
bottom: Spacing.inputPadding,
|
||||||
),
|
),
|
||||||
child: FadeTransition(
|
child: Row(
|
||||||
opacity: _expandController,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
if (!_isExpanded) ...[
|
||||||
_buildRoundButton(
|
_buildRoundButton(
|
||||||
icon: Icons.add,
|
icon: Icons.add,
|
||||||
onTap: widget.enabled
|
onTap: widget.enabled
|
||||||
@@ -412,197 +296,318 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
|||||||
iconSize: IconSize.large + 2.0,
|
iconSize: IconSize.large + 2.0,
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
// Quick pills: no scroll, clip text within fixed max width
|
] else ...[
|
||||||
Expanded(
|
// When expanded, the left padding was reduced to move the plus button.
|
||||||
child: Row(
|
// Add back spacing so the text field aligns comfortably from the edge.
|
||||||
children: [
|
SizedBox(
|
||||||
Expanded(
|
width: Spacing.inputPadding - Spacing.xs,
|
||||||
flex: 2,
|
),
|
||||||
child: _buildPillButton(
|
],
|
||||||
icon: Platform.isIOS
|
// Text input expands to fill
|
||||||
? CupertinoIcons.search
|
Expanded(
|
||||||
: Icons.search,
|
child: Semantics(
|
||||||
label: AppLocalizations.of(
|
textField: true,
|
||||||
context,
|
label: AppLocalizations.of(
|
||||||
)!.web,
|
context,
|
||||||
isActive: webSearchEnabled,
|
)!.messageInputLabel,
|
||||||
onTap: widget.enabled
|
hint: AppLocalizations.of(
|
||||||
? () {
|
context,
|
||||||
ref
|
)!.messageInputHint,
|
||||||
.read(
|
child: TextField(
|
||||||
webSearchEnabledProvider
|
controller: _controller,
|
||||||
.notifier,
|
focusNode: _focusNode,
|
||||||
)
|
enabled: widget.enabled,
|
||||||
.state =
|
autofocus: false,
|
||||||
!webSearchEnabled;
|
maxLines: _isExpanded ? null : 1,
|
||||||
}
|
keyboardType: TextInputType.multiline,
|
||||||
: null,
|
textCapitalization:
|
||||||
|
TextCapitalization.sentences,
|
||||||
|
textInputAction: TextInputAction.newline,
|
||||||
|
showCursor: true,
|
||||||
|
cursorColor:
|
||||||
|
context.conduitTheme.inputText,
|
||||||
|
style: AppTypography.chatMessageStyle
|
||||||
|
.copyWith(
|
||||||
|
color:
|
||||||
|
context.conduitTheme.inputText,
|
||||||
),
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.messageHintText,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: context
|
||||||
|
.conduitTheme
|
||||||
|
.inputPlaceholder,
|
||||||
|
fontSize: AppTypography.bodyLarge,
|
||||||
|
fontWeight: _isRecording
|
||||||
|
? FontWeight.w500
|
||||||
|
: FontWeight.w400,
|
||||||
|
fontStyle: _isRecording
|
||||||
|
? FontStyle.italic
|
||||||
|
: FontStyle.normal,
|
||||||
),
|
),
|
||||||
if (imageGenAvailable) ...[
|
// Ensure the text field background matches its parent container
|
||||||
const SizedBox(width: Spacing.xs),
|
// and does not use the global InputDecorationTheme fill
|
||||||
|
filled: false,
|
||||||
|
border: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
errorBorder: InputBorder.none,
|
||||||
|
disabledBorder: InputBorder.none,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
isDense: true,
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
// Removed onChanged setState to reduce rebuilds
|
||||||
|
onSubmitted: (_) => _sendMessage(),
|
||||||
|
onTap: () {
|
||||||
|
if (!widget.enabled) return;
|
||||||
|
if (!_isExpanded) {
|
||||||
|
_setExpanded(true);
|
||||||
|
WidgetsBinding.instance
|
||||||
|
.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
_ensureFocusedIfEnabled();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_ensureFocusedIfEnabled();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!_isExpanded) ...[
|
||||||
|
const SizedBox(width: Spacing.sm),
|
||||||
|
// Primary action button (Send/Stop) when collapsed
|
||||||
|
_buildPrimaryButton(
|
||||||
|
_hasText,
|
||||||
|
isGenerating,
|
||||||
|
stopGeneration,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Expanded bottom row with additional options
|
||||||
|
if (_isExpanded) ...[
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: Spacing.inputPadding,
|
||||||
|
right: Spacing.inputPadding,
|
||||||
|
bottom: Spacing.inputPadding,
|
||||||
|
),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: _expandController,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildRoundButton(
|
||||||
|
icon: Icons.add,
|
||||||
|
onTap: widget.enabled
|
||||||
|
? _showAttachmentOptions
|
||||||
|
: null,
|
||||||
|
tooltip: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.addAttachment,
|
||||||
|
showBackground: false,
|
||||||
|
iconSize: IconSize.large + 2.0,
|
||||||
|
),
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
|
// Quick pills: no scroll, clip text within fixed max width
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 2,
|
||||||
child: _buildPillButton(
|
child: _buildPillButton(
|
||||||
icon: Platform.isIOS
|
icon: Platform.isIOS
|
||||||
? CupertinoIcons.photo
|
? CupertinoIcons.search
|
||||||
: Icons.image,
|
: Icons.search,
|
||||||
label: AppLocalizations.of(
|
label: AppLocalizations.of(
|
||||||
context,
|
context,
|
||||||
)!.imageGen,
|
)!.web,
|
||||||
isActive: imageGenEnabled,
|
isActive: webSearchEnabled,
|
||||||
onTap: widget.enabled
|
onTap: widget.enabled
|
||||||
? () {
|
? () {
|
||||||
ref
|
ref
|
||||||
.read(
|
.read(
|
||||||
imageGenerationEnabledProvider
|
webSearchEnabledProvider
|
||||||
.notifier,
|
.notifier,
|
||||||
)
|
)
|
||||||
.state =
|
.state =
|
||||||
!imageGenEnabled;
|
!webSearchEnabled;
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (imageGenAvailable) ...[
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: _buildPillButton(
|
||||||
|
icon: Platform.isIOS
|
||||||
|
? CupertinoIcons.photo
|
||||||
|
: Icons.image,
|
||||||
|
label: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.imageGen,
|
||||||
|
isActive: imageGenEnabled,
|
||||||
|
onTap: widget.enabled
|
||||||
|
? () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
imageGenerationEnabledProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.state =
|
||||||
|
!imageGenEnabled;
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(width: Spacing.xs),
|
||||||
|
_buildRoundButton(
|
||||||
|
icon: Icons.more_horiz,
|
||||||
|
onTap: widget.enabled
|
||||||
|
? _showUnifiedToolsModal
|
||||||
|
: null,
|
||||||
|
tooltip: AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.tools,
|
||||||
|
isActive:
|
||||||
|
ref
|
||||||
|
.watch(
|
||||||
|
selectedToolIdsProvider,
|
||||||
|
)
|
||||||
|
.isNotEmpty ||
|
||||||
|
webSearchEnabled ||
|
||||||
|
imageGenEnabled,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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) {
|
||||||
|
const double buttonSize =
|
||||||
|
TouchTarget.comfortable;
|
||||||
|
final double t = _isRecording
|
||||||
|
? (_intensity.clamp(0, 10) /
|
||||||
|
10.0)
|
||||||
|
: 0.0;
|
||||||
|
final double ringMaxExtra = 16.0;
|
||||||
|
final double ringSize =
|
||||||
|
buttonSize + (ringMaxExtra * t);
|
||||||
|
final double ringOpacity =
|
||||||
|
0.15 + (0.35 * t);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: buttonSize,
|
||||||
|
height: buttonSize,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(
|
||||||
|
milliseconds: 120,
|
||||||
|
),
|
||||||
|
width: ringSize,
|
||||||
|
height: ringSize,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: context
|
||||||
|
.conduitTheme
|
||||||
|
.buttonPrimary
|
||||||
|
.withValues(
|
||||||
|
alpha: ringOpacity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Transform.scale(
|
||||||
|
scale: _isRecording
|
||||||
|
? 1.0 +
|
||||||
|
(_intensity.clamp(
|
||||||
|
0,
|
||||||
|
10,
|
||||||
|
) /
|
||||||
|
200)
|
||||||
|
: 1.0,
|
||||||
|
child: _buildRoundButton(
|
||||||
|
icon: Platform.isIOS
|
||||||
|
? CupertinoIcons
|
||||||
|
.mic_fill
|
||||||
|
: Icons.mic,
|
||||||
|
onTap:
|
||||||
|
(widget.enabled &&
|
||||||
|
voiceAvailable)
|
||||||
|
? _toggleVoice
|
||||||
|
: null,
|
||||||
|
tooltip:
|
||||||
|
AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
)!.voiceInput,
|
||||||
|
isActive: _isRecording,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
_buildRoundButton(
|
// Primary action button (Send/Stop) when expanded
|
||||||
icon: Icons.more_horiz,
|
_buildPrimaryButton(
|
||||||
onTap: widget.enabled
|
_hasText,
|
||||||
? _showUnifiedToolsModal
|
isGenerating,
|
||||||
: null,
|
stopGeneration,
|
||||||
tooltip: AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
)!.tools,
|
|
||||||
isActive:
|
|
||||||
ref
|
|
||||||
.watch(
|
|
||||||
selectedToolIdsProvider,
|
|
||||||
)
|
|
||||||
.isNotEmpty ||
|
|
||||||
webSearchEnabled ||
|
|
||||||
imageGenEnabled,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
// Debug button for testing on-device STT (enable by changing false to true)
|
||||||
const SizedBox(width: Spacing.xs),
|
// ignore: dead_code
|
||||||
// Mic + Send cluster pinned to the right
|
if (false) ...[
|
||||||
Row(
|
const SizedBox(width: Spacing.sm),
|
||||||
mainAxisSize: MainAxisSize.min,
|
_buildRoundButton(
|
||||||
children: [
|
icon: Icons.bug_report,
|
||||||
// Microphone button: inline voice input toggle with animated intensity ring
|
onTap: widget.enabled
|
||||||
Builder(
|
? () async {
|
||||||
builder: (context) {
|
final result =
|
||||||
const double buttonSize =
|
await _voiceService
|
||||||
TouchTarget.comfortable;
|
.testOnDeviceStt();
|
||||||
final double t = _isRecording
|
if (context.mounted) {
|
||||||
? (_intensity.clamp(0, 10) / 10.0)
|
ScaffoldMessenger.of(
|
||||||
: 0.0;
|
context,
|
||||||
final double ringMaxExtra = 16.0;
|
).showSnackBar(
|
||||||
final double ringSize =
|
SnackBar(
|
||||||
buttonSize + (ringMaxExtra * t);
|
content: Text(
|
||||||
final double ringOpacity =
|
'STT Test: $result',
|
||||||
0.15 + (0.35 * t);
|
),
|
||||||
|
duration: const Duration(
|
||||||
return SizedBox(
|
seconds: 5,
|
||||||
width: buttonSize,
|
),
|
||||||
height: buttonSize,
|
),
|
||||||
child: Stack(
|
);
|
||||||
alignment: Alignment.center,
|
}
|
||||||
children: [
|
}
|
||||||
AnimatedContainer(
|
: null,
|
||||||
duration: const Duration(
|
tooltip: 'Test On-Device STT',
|
||||||
milliseconds: 120,
|
|
||||||
),
|
|
||||||
width: ringSize,
|
|
||||||
height: ringSize,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: context
|
|
||||||
.conduitTheme
|
|
||||||
.buttonPrimary
|
|
||||||
.withValues(
|
|
||||||
alpha: ringOpacity,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Transform.scale(
|
|
||||||
scale: _isRecording
|
|
||||||
? 1.0 +
|
|
||||||
(_intensity.clamp(
|
|
||||||
0,
|
|
||||||
10,
|
|
||||||
) /
|
|
||||||
200)
|
|
||||||
: 1.0,
|
|
||||||
child: _buildRoundButton(
|
|
||||||
icon: Platform.isIOS
|
|
||||||
? CupertinoIcons
|
|
||||||
.mic_fill
|
|
||||||
: Icons.mic,
|
|
||||||
onTap:
|
|
||||||
(widget.enabled &&
|
|
||||||
voiceAvailable)
|
|
||||||
? _toggleVoice
|
|
||||||
: null,
|
|
||||||
tooltip:
|
|
||||||
AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
)!.voiceInput,
|
|
||||||
isActive: _isRecording,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: Spacing.xs),
|
|
||||||
// Primary action button (Send/Stop) when expanded
|
|
||||||
_buildPrimaryButton(
|
|
||||||
_hasText,
|
|
||||||
isGenerating,
|
|
||||||
stopGeneration,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
// removed duplicate send button; now only in right cluster
|
||||||
// Debug button for testing on-device STT (enable by changing false to true)
|
|
||||||
// ignore: dead_code
|
|
||||||
if (false) ...[
|
|
||||||
const SizedBox(width: Spacing.sm),
|
|
||||||
_buildRoundButton(
|
|
||||||
icon: Icons.bug_report,
|
|
||||||
onTap: widget.enabled
|
|
||||||
? () async {
|
|
||||||
final result = await _voiceService
|
|
||||||
.testOnDeviceStt();
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'STT Test: $result',
|
|
||||||
),
|
|
||||||
duration: const Duration(
|
|
||||||
seconds: 5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
tooltip: 'Test On-Device STT',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
// removed duplicate send button; now only in right cluster
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user