refactor: chat input

This commit is contained in:
cogwheel0
2025-09-18 15:01:21 +05:30
parent ac12eca6b5
commit dea14dfdcf
3 changed files with 377 additions and 526 deletions

View File

@@ -145,19 +145,11 @@ class BackgroundStreamingHandler: NSObject {
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
// Setup background streaming handler with scene-safe rootViewController access // Setup background streaming handler using the plugin registry messenger
var controller: FlutterViewController? if let registrar = self.registrar(forPlugin: "BackgroundStreamingHandler") {
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let root = scene.windows.first?.rootViewController as? FlutterViewController {
controller = root
} else if let legacy = window?.rootViewController as? FlutterViewController {
controller = legacy
}
if let controller {
let channel = FlutterMethodChannel( let channel = FlutterMethodChannel(
name: "conduit/background_streaming", name: "conduit/background_streaming",
binaryMessenger: controller.binaryMessenger binaryMessenger: registrar.messenger()
) )
backgroundStreamingHandler = BackgroundStreamingHandler() backgroundStreamingHandler = BackgroundStreamingHandler()

View File

@@ -901,7 +901,6 @@ class _ChatPageState extends ConsumerState<ChatPage> {
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
).animate().fadeIn(delay: const Duration(milliseconds: 150)), ).animate().fadeIn(delay: const Duration(milliseconds: 150)),
], ],
), ),
), ),
@@ -1042,7 +1041,9 @@ class _ChatPageState extends ConsumerState<ChatPage> {
Scaffold.of(ctx).openDrawer(); Scaffold.of(ctx).openDrawer();
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.all(4.0), padding: const EdgeInsets.only(
left: Spacing.inputPadding,
),
child: Icon( child: Icon(
Platform.isIOS Platform.isIOS
? CupertinoIcons.line_horizontal_3 ? CupertinoIcons.line_horizontal_3
@@ -1331,14 +1332,19 @@ class _ChatPageState extends ConsumerState<ChatPage> {
), ),
actions: [ actions: [
if (!_isSelectionMode) ...[ if (!_isSelectionMode) ...[
IconButton( Padding(
icon: Icon( padding: const EdgeInsets.only(right: Spacing.inputPadding),
Platform.isIOS ? CupertinoIcons.create : Icons.add_comment, child: IconButton(
color: context.conduitTheme.textPrimary, icon: Icon(
size: IconSize.appBar, Platform.isIOS
? CupertinoIcons.create
: Icons.add_comment,
color: context.conduitTheme.textPrimary,
size: IconSize.appBar,
),
onPressed: _handleNewChat,
tooltip: AppLocalizations.of(context)!.newChat,
), ),
onPressed: _handleNewChat,
tooltip: AppLocalizations.of(context)!.newChat,
), ),
] else ...[ ] else ...[
IconButton( IconButton(

View File

@@ -150,14 +150,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
final TextEditingController _controller = TextEditingController(); final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
bool _isRecording = false; bool _isRecording = false;
bool _isExpanded = true; // Start expanded for better UX
// final String _voiceInputText = ''; // final String _voiceInputText = '';
bool _hasText = false; // track locally without rebuilding on each keystroke bool _hasText = false; // track locally without rebuilding on each keystroke
StreamSubscription<String>? _voiceStreamSubscription; StreamSubscription<String>? _voiceStreamSubscription;
late AnimationController _expandController;
late AnimationController _pulseController;
Timer? _blurCollapseTimer;
bool _pendingFocusAfterExpand = false;
late VoiceInputService _voiceService; late VoiceInputService _voiceService;
StreamSubscription<int>? _intensitySub; StreamSubscription<int>? _intensitySub;
StreamSubscription<String>? _textSub; StreamSubscription<String>? _textSub;
@@ -170,29 +165,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
void initState() { void initState() {
super.initState(); super.initState();
_voiceService = ref.read(voiceInputServiceProvider); _voiceService = ref.read(voiceInputServiceProvider);
_expandController = AnimationController(
duration:
AnimationDuration.fast, // Faster animation for better responsiveness
vsync: this,
value: 1.0, // Start expanded
);
_expandController.addStatusListener((status) {
if (!mounted || _isDeactivated) return;
if (_pendingFocusAfterExpand && status == AnimationStatus.completed) {
_pendingFocusAfterExpand = false;
// Focus and ensure IME shows reliably after expansion finishes
_ensureFocusedIfEnabled();
// Let platform show IME naturally when focus is active. Avoid manual
// TextInput.show here to prevent race conditions on Android.
// If a device/IME requires a nudge, the TextField's onTap path covers it.
}
});
_pulseController = AnimationController(
duration: AnimationDuration.slow,
vsync: this,
);
// Apply any prefilled text on first frame (focus/expand handled via inputFocusTrigger) // Apply any prefilled text on first frame (focus handled via inputFocusTrigger)
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _isDeactivated) return; if (!mounted || _isDeactivated) return;
final text = ref.read(prefilledInputTextProvider); final text = ref.read(prefilledInputTextProvider);
@@ -213,19 +187,12 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _isDeactivated) return; if (!mounted || _isDeactivated) return;
setState(() => _hasText = has); setState(() => _hasText = has);
// Intelligent expansion: expand when user starts typing
if (has && !_isExpanded) {
_setExpanded(true);
}
}); });
} }
}); });
// Intelligent expand/collapse around focus changes // Publish focus changes to listeners
_focusNode.addListener(() { _focusNode.addListener(() {
// Cancel any pending blur-driven collapse
_blurCollapseTimer?.cancel();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _isDeactivated) return; if (!mounted || _isDeactivated) return;
final hasFocus = _focusNode.hasFocus; final hasFocus = _focusNode.hasFocus;
@@ -233,25 +200,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
try { try {
ref.read(composerHasFocusProvider.notifier).state = hasFocus; ref.read(composerHasFocusProvider.notifier).state = hasFocus;
} catch (_) {} } catch (_) {}
if (hasFocus) {
if (!_isExpanded) _setExpanded(true);
} else {
// A blur occurred: ensure no pending auto-focus remains
_pendingFocusAfterExpand = false;
// Defer collapse slightly to avoid IME show/hide race conditions
_blurCollapseTimer = Timer(const Duration(milliseconds: 160), () {
if (!mounted || _isDeactivated) return;
if (_focusNode.hasFocus) return; // focus came back
// Collapse only when keyboard is fully hidden to avoid flicker
final keyboardVisible =
MediaQuery.of(context).viewInsets.bottom > 0;
if (keyboardVisible) return;
final has = _controller.text.trim().isNotEmpty;
if (!has && _isExpanded) {
_setExpanded(false);
}
});
}
}); });
}); });
@@ -265,9 +213,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
} catch (_) {} } catch (_) {}
_controller.dispose(); _controller.dispose();
_focusNode.dispose(); _focusNode.dispose();
_expandController.dispose();
_pulseController.dispose();
_blurCollapseTimer?.cancel();
_voiceStreamSubscription?.cancel(); _voiceStreamSubscription?.cancel();
_intensitySub?.cancel(); _intensitySub?.cancel();
_textSub?.cancel(); _textSub?.cancel();
@@ -286,9 +231,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
@override @override
void deactivate() { void deactivate() {
_isDeactivated = true; _isDeactivated = true;
_blurCollapseTimer?.cancel();
_expandController.stop();
_pulseController.stop();
super.deactivate(); super.deactivate();
} }
@@ -303,10 +245,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
// Avoid auto-focusing when becoming enabled; wait for user intent // Avoid auto-focusing when becoming enabled; wait for user intent
if (!widget.enabled && oldWidget.enabled) { if (!widget.enabled && oldWidget.enabled) {
// Became disabled → collapse and hide keyboard
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || _isDeactivated) return; if (!mounted || _isDeactivated) return;
if (_isExpanded) _setExpanded(false);
if (_focusNode.hasFocus) { if (_focusNode.hasFocus) {
_focusNode.unfocus(); _focusNode.unfocus();
} }
@@ -324,18 +264,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
// Keep focus and keyboard open; do not collapse automatically // Keep focus and keyboard open; do not collapse automatically
} }
void _setExpanded(bool expanded) {
if (!mounted || _isDeactivated || _isExpanded == expanded) return;
setState(() {
_isExpanded = expanded;
});
if (expanded) {
_expandController.forward();
} else {
_expandController.reverse();
}
}
void _insertNewline() { void _insertNewline() {
final text = _controller.text; final text = _controller.text;
TextSelection sel = _controller.selection; TextSelection sel = _controller.selection;
@@ -408,7 +336,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
if (!mounted || _isDeactivated) return; if (!mounted || _isDeactivated) return;
// Explicit request: always try to focus and show the keyboard // Explicit request: always try to focus and show the keyboard
_ensureFocusedIfEnabled(); _ensureFocusedIfEnabled();
if (!_isExpanded) _setExpanded(true);
_lastHandledFocusTick = focusTick; _lastHandledFocusTick = focusTick;
}); });
} }
@@ -467,13 +394,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
.fast, // Faster for better responsiveness .fast, // Faster for better responsiveness
curve: Curves.fastOutSlowIn, // More efficient curve curve: Curves.fastOutSlowIn, // More efficient curve
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
onEnd: () {
if (!mounted || _isDeactivated) return;
if (_pendingFocusAfterExpand) {
_pendingFocusAfterExpand = false;
_ensureFocusedIfEnabled();
}
},
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
child: RepaintBoundary( child: RepaintBoundary(
@@ -485,7 +405,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
Spacing.sm, Spacing.sm,
Spacing.sm, Spacing.sm,
Spacing.sm, Spacing.xs,
Spacing.sm, Spacing.sm,
), ),
child: Container( child: Container(
@@ -507,12 +427,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
if (!widget.enabled) return; if (!widget.enabled) return;
if (!_isExpanded) { _ensureFocusedIfEnabled();
_pendingFocusAfterExpand = true;
_setExpanded(true);
} else {
_ensureFocusedIfEnabled();
}
}, },
child: Semantics( child: Semantics(
textField: true, textField: true,
@@ -573,7 +488,8 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
focusNode: _focusNode, focusNode: _focusNode,
enabled: widget.enabled, enabled: widget.enabled,
autofocus: false, autofocus: false,
maxLines: _isExpanded ? null : 1, minLines: 1,
maxLines: null,
keyboardType: keyboardType:
TextInputType.multiline, TextInputType.multiline,
textCapitalization: textCapitalization:
@@ -642,22 +558,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
}, },
onTap: () { onTap: () {
if (!widget.enabled) return; if (!widget.enabled) return;
if (!_isExpanded) { _ensureFocusedIfEnabled();
_pendingFocusAfterExpand =
true;
_setExpanded(true);
WidgetsBinding.instance
.addPostFrameCallback((
_,
) {
if (!mounted) return;
if (_pendingFocusAfterExpand) {
_ensureFocusedIfEnabled();
}
});
} else {
_ensureFocusedIfEnabled();
}
}, },
), ),
), ),
@@ -665,423 +566,377 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
), ),
), ),
), ),
if (!_isExpanded) ...[
const SizedBox(width: Spacing.sm),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (voiceAvailable) ...[
_buildVoiceButton(voiceAvailable),
const SizedBox(width: Spacing.xs),
],
_buildPrimaryButton(
_hasText,
isGenerating,
stopGeneration,
),
],
),
],
], ],
), ),
), ),
), ),
// Expanded bottom row with additional options Container(
if (_isExpanded) ...[ padding: const EdgeInsets.only(
Container( left: Spacing.inputPadding,
padding: const EdgeInsets.only( right: Spacing.inputPadding,
left: Spacing.inputPadding, top: Spacing.xs,
right: Spacing.inputPadding, bottom: Spacing.sm,
top: Spacing.xs, ),
bottom: Spacing.sm, child: Row(
), children: [
child: FadeTransition( _buildRoundButton(
opacity: _expandController, icon: Icons.add,
child: Row( onTap: widget.enabled && !_isRecording
children: [ ? _showAttachmentOptions
_buildRoundButton( : null,
icon: Icons.add, tooltip: AppLocalizations.of(
onTap: widget.enabled && !_isRecording context,
? _showAttachmentOptions )!.addAttachment,
: null, showBackground: false,
tooltip: AppLocalizations.of( iconSize: IconSize.large + 2.0,
context, ),
)!.addAttachment, const SizedBox(width: Spacing.xs),
showBackground: false, // Quick pills: expand to full text when space allows
iconSize: IconSize.large + 2.0, Expanded(
), child: LayoutBuilder(
const SizedBox(width: Spacing.xs), builder: (context, constraints) {
// Quick pills: expand to full text when space allows final double total = constraints.maxWidth;
Expanded( final bool showImage =
child: LayoutBuilder( imageGenAvailable &&
builder: (context, constraints) { showImagePillPref;
final double total = final bool showWeb = showWebPill;
constraints.maxWidth; // Tools button is always shown
final bool showImage = final double toolsWidth =
imageGenAvailable && TouchTarget.minimum;
showImagePillPref; final double gapBeforeTools = Spacing.xs;
final bool showWeb = showWebPill;
// Tools button is always shown
final double toolsWidth =
TouchTarget.minimum;
final double gapBeforeTools =
Spacing.xs;
final double availableForPills = math final double availableForPills = math.max(
.max( 0.0,
0.0, total - toolsWidth - gapBeforeTools,
total - );
toolsWidth -
gapBeforeTools,
);
// Compose selected pill entries in order // Compose selected pill entries in order
final List<Map<String, dynamic>> final List<Map<String, dynamic>> entries =
entries = []; [];
final textStyle = final textStyle =
AppTypography.labelStyle; AppTypography.labelStyle;
const double horizontalPadding = const double horizontalPadding =
Spacing.md * 2; Spacing.md * 2;
for (final id in selectedQuickPills) { for (final id in selectedQuickPills) {
if (id == 'web' && showWeb) { if (id == 'web' && showWeb) {
final lbl = AppLocalizations.of( final lbl = AppLocalizations.of(
context, context,
)!.web; )!.web;
final tp = TextPainter( final tp = TextPainter(
text: TextSpan( text: TextSpan(
text: lbl, text: lbl,
style: textStyle, style: textStyle,
), ),
maxLines: 1, maxLines: 1,
textDirection: textDirection: Directionality.of(
Directionality.of(context), context,
)..layout(); ),
entries.add({ )..layout();
'id': id, entries.add({
'label': lbl, 'id': id,
'width': 'label': lbl,
tp.width + 'width':
horizontalPadding, tp.width + horizontalPadding,
'widgetBuilder': () => _buildPillButton( 'widgetBuilder': () => _buildPillButton(
icon: Platform.isIOS
? CupertinoIcons.search
: Icons.search,
label: lbl,
isActive: webSearchEnabled,
onTap:
widget.enabled &&
!_isRecording
? () {
ref
.read(
webSearchEnabledProvider
.notifier,
)
.state =
!webSearchEnabled;
}
: null,
),
});
} else if (id == 'image' &&
showImage) {
final lbl = AppLocalizations.of(
context,
)!.imageGen;
final tp = TextPainter(
text: TextSpan(
text: lbl,
style: textStyle,
),
maxLines: 1,
textDirection:
Directionality.of(context),
)..layout();
entries.add({
'id': id,
'label': lbl,
'width':
tp.width +
horizontalPadding,
'widgetBuilder': () => _buildPillButton(
icon: Platform.isIOS
? CupertinoIcons.photo
: Icons.image,
label: lbl,
isActive: imageGenEnabled,
onTap:
widget.enabled &&
!_isRecording
? () {
ref
.read(
imageGenerationEnabledProvider
.notifier,
)
.state =
!imageGenEnabled;
}
: null,
),
});
} else {
// Tool ID from server
Tool? tool;
for (final t in availableTools) {
if (t.id == id) {
tool = t;
break;
}
}
if (tool != null) {
final lbl = tool.name;
final tp = TextPainter(
text: TextSpan(
text: lbl,
style: textStyle,
),
maxLines: 1,
textDirection:
Directionality.of(
context,
),
)..layout();
final selectedIds = ref.watch(
selectedToolIdsProvider,
);
final isActive = selectedIds
.contains(id);
entries.add({
'id': id,
'label': lbl,
'width':
tp.width +
horizontalPadding,
'widgetBuilder': () => _buildPillButton(
icon: Platform.isIOS
? CupertinoIcons.wrench
: Icons.build,
label: lbl,
isActive: isActive,
onTap:
widget.enabled &&
!_isRecording
? () {
final current =
List<
String
>.from(
ref.read(
selectedToolIdsProvider,
),
);
if (current
.contains(id)) {
current.remove(
id,
);
} else {
current.add(id);
}
ref
.read(
selectedToolIdsProvider
.notifier,
)
.state =
current;
}
: null,
),
});
}
}
}
// Build rowChildren according to measured widths and available space
final List<Widget> rowChildren = [];
if (entries.isEmpty) {
// no quick pills, will just show tools later
} else if (entries.length == 1) {
final e = entries.first;
final pill =
e['widgetBuilder']() as Widget;
final w = (e['width'] as double);
if (w <= availableForPills) {
rowChildren.add(pill);
} else {
rowChildren.add(
Flexible(
fit: FlexFit.loose,
child: pill,
),
);
}
} else {
// up to 2 based on settings enforcement; if more, take first 2
final e1 = entries[0];
final e2 = entries[1];
final w1 = (e1['width'] as double);
final w2 = (e2['width'] as double);
const double gapBetweenPills =
Spacing.xs;
final combined =
w1 + gapBetweenPills + w2;
final pill1 =
e1['widgetBuilder']() as Widget;
final pill2 =
e2['widgetBuilder']() as Widget;
if (combined <= availableForPills) {
rowChildren
..add(pill1)
..add(
const SizedBox(
width: Spacing.xs,
),
)
..add(pill2);
} else if (w1 < availableForPills) {
rowChildren
..add(pill1)
..add(
const SizedBox(
width: Spacing.xs,
),
)
..add(
Flexible(
fit: FlexFit.loose,
child: pill2,
),
);
} else if (w2 < availableForPills) {
rowChildren
..add(
Flexible(
fit: FlexFit.loose,
child: pill1,
),
)
..add(
const SizedBox(
width: Spacing.xs,
),
)
..add(pill2);
} else {
final int f1 = math.max(
1,
w1.round(),
);
final int f2 = math.max(
1,
w2.round(),
);
rowChildren
..add(
Flexible(
fit: FlexFit.loose,
flex: f1,
child: pill1,
),
)
..add(
const SizedBox(
width: Spacing.xs,
),
)
..add(
Flexible(
fit: FlexFit.loose,
flex: f2,
child: pill2,
),
);
}
}
// Append tools button at the end (always visible)
rowChildren.add(
_buildIconButton(
icon: Platform.isIOS icon: Platform.isIOS
? CupertinoIcons.wrench ? CupertinoIcons.search
: Icons.build, : Icons.search,
label: lbl,
isActive: webSearchEnabled,
onTap: onTap:
widget.enabled && widget.enabled &&
!_isRecording !_isRecording
? _showUnifiedToolsModal ? () {
ref
.read(
webSearchEnabledProvider
.notifier,
)
.state =
!webSearchEnabled;
}
: null, : null,
tooltip: AppLocalizations.of( ),
});
} else if (id == 'image' && showImage) {
final lbl = AppLocalizations.of(
context,
)!.imageGen;
final tp = TextPainter(
text: TextSpan(
text: lbl,
style: textStyle,
),
maxLines: 1,
textDirection: Directionality.of(
context,
),
)..layout();
entries.add({
'id': id,
'label': lbl,
'width':
tp.width + horizontalPadding,
'widgetBuilder': () => _buildPillButton(
icon: Platform.isIOS
? CupertinoIcons.photo
: Icons.image,
label: lbl,
isActive: imageGenEnabled,
onTap:
widget.enabled &&
!_isRecording
? () {
ref
.read(
imageGenerationEnabledProvider
.notifier,
)
.state =
!imageGenEnabled;
}
: null,
),
});
} else {
// Tool ID from server
Tool? tool;
for (final t in availableTools) {
if (t.id == id) {
tool = t;
break;
}
}
if (tool != null) {
final lbl = tool.name;
final tp = TextPainter(
text: TextSpan(
text: lbl,
style: textStyle,
),
maxLines: 1,
textDirection: Directionality.of(
context, context,
)!.tools, ),
isActive: )..layout();
ref final selectedIds = ref.watch(
.watch( selectedToolIdsProvider,
selectedToolIdsProvider, );
) final isActive = selectedIds
.isNotEmpty || .contains(id);
webSearchEnabled || entries.add({
imageGenEnabled, 'id': id,
'label': lbl,
'width':
tp.width + horizontalPadding,
'widgetBuilder': () => _buildPillButton(
icon: Platform.isIOS
? CupertinoIcons.wrench
: Icons.build,
label: lbl,
isActive: isActive,
onTap:
widget.enabled &&
!_isRecording
? () {
final current =
List<String>.from(
ref.read(
selectedToolIdsProvider,
),
);
if (current.contains(
id,
)) {
current.remove(id);
} else {
current.add(id);
}
ref
.read(
selectedToolIdsProvider
.notifier,
)
.state =
current;
}
: null,
),
});
}
}
}
// Build rowChildren according to measured widths and available space
final List<Widget> rowChildren = [];
if (entries.isEmpty) {
// no quick pills, will just show tools later
} else if (entries.length == 1) {
final e = entries.first;
final pill =
e['widgetBuilder']() as Widget;
final w = (e['width'] as double);
if (w <= availableForPills) {
rowChildren.add(pill);
} else {
rowChildren.add(
Flexible(
fit: FlexFit.loose,
child: pill,
), ),
); );
}
} else {
// up to 2 based on settings enforcement; if more, take first 2
final e1 = entries[0];
final e2 = entries[1];
final w1 = (e1['width'] as double);
final w2 = (e2['width'] as double);
const double gapBetweenPills =
Spacing.xs;
final combined =
w1 + gapBetweenPills + w2;
final pill1 =
e1['widgetBuilder']() as Widget;
final pill2 =
e2['widgetBuilder']() as Widget;
return Row(children: rowChildren); if (combined <= availableForPills) {
}, rowChildren
), ..add(pill1)
), ..add(
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.xs),
Row( )
mainAxisSize: MainAxisSize.min, ..add(pill2);
children: [ } else if (w1 < availableForPills) {
if (voiceAvailable) ...[ rowChildren
_buildVoiceButton(voiceAvailable), ..add(pill1)
const SizedBox(width: Spacing.xs), ..add(
], const SizedBox(width: Spacing.xs),
_buildPrimaryButton( )
_hasText, ..add(
isGenerating, Flexible(
stopGeneration, fit: FlexFit.loose,
child: pill2,
),
);
} else if (w2 < availableForPills) {
rowChildren
..add(
Flexible(
fit: FlexFit.loose,
child: pill1,
),
)
..add(
const SizedBox(width: Spacing.xs),
)
..add(pill2);
} else {
final int f1 = math.max(
1,
w1.round(),
);
final int f2 = math.max(
1,
w2.round(),
);
rowChildren
..add(
Flexible(
fit: FlexFit.loose,
flex: f1,
child: pill1,
),
)
..add(
const SizedBox(width: Spacing.xs),
)
..add(
Flexible(
fit: FlexFit.loose,
flex: f2,
child: pill2,
),
);
}
}
// Append tools button at the end (always visible)
rowChildren.add(
_buildIconButton(
icon: Platform.isIOS
? CupertinoIcons.wrench
: Icons.build,
onTap: widget.enabled && !_isRecording
? _showUnifiedToolsModal
: null,
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) return Row(children: rowChildren);
// ignore: dead_code },
if (false) ...[ ),
const SizedBox(width: Spacing.sm), ),
_buildRoundButton( const SizedBox(width: Spacing.sm),
icon: Icons.bug_report, Row(
onTap: widget.enabled mainAxisSize: MainAxisSize.min,
? () async { children: [
final result = if (voiceAvailable) ...[
await _voiceService _buildVoiceButton(voiceAvailable),
.testOnDeviceStt(); const SizedBox(width: Spacing.xs),
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
'STT Test: $result',
),
duration: const Duration(
seconds: 5,
),
),
);
}
}
: null,
tooltip: 'Test On-Device STT',
),
], ],
_buildPrimaryButton(
_hasText,
isGenerating,
stopGeneration,
),
], ],
), ),
), // 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',
),
],
],
), ),
], ),
], ],
), ),
), ),
@@ -1539,7 +1394,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
if (mounted) { if (mounted) {
_focusNode.canRequestFocus = prevCanRequest; _focusNode.canRequestFocus = prevCanRequest;
if (wasFocused && widget.enabled) { if (wasFocused && widget.enabled) {
if (!_isExpanded) _setExpanded(true);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
_ensureFocusedIfEnabled(); _ensureFocusedIfEnabled();
@@ -1568,7 +1422,6 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
if (mounted) { if (mounted) {
_focusNode.canRequestFocus = prevCanRequest; _focusNode.canRequestFocus = prevCanRequest;
if (wasFocused && widget.enabled) { if (wasFocused && widget.enabled) {
if (!_isExpanded) _setExpanded(true);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
_ensureFocusedIfEnabled(); _ensureFocusedIfEnabled();