import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:island/models/thought.dart'; import 'package:island/screens/posts/compose.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/post/compose_dialog.dart'; import 'package:island/widgets/thought/function_calls_section.dart'; import 'package:island/widgets/thought/proposals_section.dart'; import 'package:island/widgets/thought/reasoning_section.dart'; import 'package:island/widgets/thought/thought_content.dart'; import 'package:island/widgets/thought/thought_header.dart'; import 'package:island/widgets/thought/token_info.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; List> _extractProposals(String content) { final proposalRegex = RegExp( r'(.*?)<\/proposal>', dotAll: true, ); final matches = proposalRegex.allMatches(content); return matches.map((match) { return {'type': match.group(1)!, 'content': match.group(2)!}; }).toList(); } void _handleProposalAction(BuildContext context, Map proposal) { switch (proposal['type']) { case 'post_create': // Show post creation dialog with the proposal content PostComposeDialog.show( context, initialState: PostComposeInitialState( content: (proposal['content'] ?? '').trim(), ), ); break; default: // Show a snackbar for unsupported proposal types showSnackBar('Unsupported proposal type: ${proposal['type']}'); } } class ThoughtInput extends HookWidget { final TextEditingController messageController; final bool isStreaming; final VoidCallback onSend; final List>? attachedMessages; final List? attachedPosts; const ThoughtInput({ super.key, required this.messageController, required this.isStreaming, required this.onSend, this.attachedMessages, this.attachedPosts, }); @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.only( left: 16, right: 16, bottom: 16 + MediaQuery.of(context).padding.bottom, ), child: Material( elevation: 2, color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(32), child: Padding( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), child: Column( children: [ if ((attachedMessages?.isNotEmpty ?? false) || (attachedPosts?.isNotEmpty ?? false)) Container( key: ValueKey( 'attachments-${attachedMessages?.length ?? 0}-${attachedPosts?.length ?? 0}', ), padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), margin: const EdgeInsets.only( left: 4, right: 4, top: 4, bottom: 4, ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(16), border: Border.all( color: Theme.of( context, ).colorScheme.outline.withOpacity(0.2), width: 1, ), ), child: Row( mainAxisSize: MainAxisSize.max, children: [ Icon( Symbols.attach_file, size: 16, color: Theme.of(context).colorScheme.primary, ), const Gap(4), Text( [ if (attachedMessages?.isNotEmpty ?? false) '${attachedMessages!.length} message${attachedMessages!.length > 1 ? 's' : ''}', if (attachedPosts?.isNotEmpty ?? false) '${attachedPosts!.length} post${attachedPosts!.length > 1 ? 's' : ''}', ].join(', '), style: Theme.of(context).textTheme.bodySmall!.copyWith( fontWeight: FontWeight.w500, fontSize: 12, ), ), const Spacer(), SizedBox( width: 24, height: 24, child: IconButton( padding: EdgeInsets.zero, icon: const Icon(Icons.close, size: 14), onPressed: () { // Note: Since these are final parameters, we can't modify them directly // This would require making the sheet stateful or using a callback // For now, just show the indicator without remove functionality }, tooltip: 'clear', ), ), ], ), ), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: TextField( controller: messageController, keyboardType: TextInputType.multiline, enabled: !isStreaming, decoration: InputDecoration( hintText: (isStreaming ? 'thoughtStreamingHint' : 'thoughtInputHint') .tr(), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 12, ), ), maxLines: 5, minLines: 1, textInputAction: TextInputAction.send, onSubmitted: (_) => onSend(), ), ), IconButton( icon: Icon(isStreaming ? Symbols.stop : Icons.send), color: Theme.of(context).colorScheme.primary, onPressed: onSend, ), ], ), ], ), ), ), ); } } // Unified thought item widget class ThoughtItem extends StatelessWidget { const ThoughtItem({ super.key, this.thought, this.thoughtIndex, this.isStreaming = false, this.streamingText = '', this.reasoningChunks = const [], this.streamingFunctionCalls = const [], }) : assert( (thought != null && !isStreaming) || (thought == null && isStreaming), 'Either thought or streaming parameters must be provided', ); final SnThinkingThought? thought; final int? thoughtIndex; final bool isStreaming; final String streamingText; final List reasoningChunks; final List streamingFunctionCalls; @override Widget build(BuildContext context) { final isUser = !isStreaming && thought!.role == ThinkingThoughtRole.user; final isAI = isStreaming || (!isStreaming && thought!.role == ThinkingThoughtRole.assistant); final List> proposals = !isStreaming && thought!.content != null ? _extractProposals(thought!.content!) : []; return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header ThoughtHeader(isStreaming: isStreaming, isUser: isUser), const Gap(8), // Content Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), border: Border.all( color: Theme.of(context).colorScheme.outline.withOpacity(0.2), width: 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Main content ThoughtContent( isStreaming: isStreaming, streamingText: streamingText, thought: thought, ), // Reasoning chunks (streaming only) ReasoningSection(reasoningChunks: reasoningChunks), // Function calls FunctionCallsSection( isStreaming: isStreaming, streamingFunctionCalls: streamingFunctionCalls, thought: thought, ), // Token count and model name (for completed AI thoughts only) if (!isStreaming && isAI && thought != null) TokenInfo(thought: thought!), // Proposals (for completed AI thoughts only) if (!isStreaming && proposals.isNotEmpty && isAI) ProposalsSection( proposals: proposals, onProposalAction: _handleProposalAction, ), ], ), ), ], ), ); } }