import 'dart:io'; import 'package:app/components/util/backend.dart'; import 'package:camera/camera.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:flutter/material.dart'; import 'package:app/theme.dart'; import 'package:image_picker/image_picker.dart'; import '../conversation_thread/chat.dart'; import 'prompt_box_model.dart'; export 'prompt_box_model.dart'; class PromptBoxWidget extends StatefulWidget { const PromptBoxWidget({super.key}); @override State createState() => _PromptBoxWidgetState(); } class _PromptBoxWidgetState extends State { late PromptBoxModel _model; String entryText = ""; @override void setState(VoidCallback callback) { super.setState(callback); _model.onUpdate(); } @override void initState() { super.initState(); _model = createModel(context, () => PromptBoxModel()); _model.textController ??= TextEditingController(); _model.textFieldFocusNode ??= FocusNode(); } @override void dispose() { _model.maybeDispose(); super.dispose(); } @override Widget build(BuildContext context) { const double buttonPadding = 4; return SafeArea( child: Container( width: double.infinity, decoration: BoxDecoration( color: AppTheme.secondaryBackground, borderRadius: BorderRadius.circular(10), ), alignment: const AlignmentDirectional(0, 0), child: Column( mainAxisSize: MainAxisSize.max, children: [ Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.end, // TODO: Implement the context upload buttons children: [ Expanded( // TODO: Video context upload child: Padding( padding: const EdgeInsets.all(buttonPadding), child: IconButton( icon: const Icon( Icons.videocam_outlined, color: AppTheme.accent2, size: 30, ), onPressed: () { print('IconButton pressed ...'); }, ), ), ), Expanded( // TODO: Gallery context upload child: Padding( padding: const EdgeInsets.all(buttonPadding), child: IconButton( icon: const Icon( Icons.photo_outlined, color: AppTheme.accent2, size: 30, ), onPressed: () async { final picker = ImagePicker(); final image = await picker.pickImage(source: ImageSource.gallery); // TODO: Allow for multiple pictures/photos to be selected. // final images = await picker.pickMultiImage(); if (image != null) { // TODO: Upload to server and add to conversation.chats String i = Backend.convertToBase64(File(image.path)).toString(); Backend.loadedConversation.add(Chat.image(i, true, 1)); } else { print('User canceled image selection'); } }, ), ), ), Expanded( // TODO: File context upload child: Padding( padding: const EdgeInsets.all(buttonPadding), child: IconButton( icon: const Icon( Icons.file_upload_outlined, color: AppTheme.accent2, size: 30, ), onPressed: () async { final picker = FilePicker.platform; final result = await picker.pickFiles(allowMultiple: true); // Allow multiple files if (result != null) { for (final platformFile in result.files) { // TODO: Upload file to server final file = File(platformFile.path!); } } else { print('User canceled file selection'); } }, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(buttonPadding), child: IconButton( icon: const Icon( Icons.camera_alt_outlined, color: AppTheme.accent2, size: 30, ), onPressed: () async { final picker = ImagePicker(); final image = await picker.pickImage(source: ImageSource.camera); if (image != null) { final i = Backend.convertToBase64(File(image.path)); // TODO: Add to widget rather than display in conversation thread. Backend.loadedConversation.add(Chat.image(i.toString(), true, 1)); } }, ), ), ), Expanded( child: Padding( padding: const EdgeInsets.all(buttonPadding), child: IconButton( icon: Icon( Backend.stt.speechToText.isNotListening ? Icons.mic_off_outlined : Icons.mic_none, color: AppTheme.accent2, size: 30, ), onPressed: () { // TODO: Audio input is still buggy, it appears to only enter the text field once the button is pressed again to stop. if(Backend.stt.speechToText.isNotListening) { print('STT engine starting ...'); Backend.stt.startListening(); updateText(); } else { print('STT engine stopping'); Backend.stt.stopListening(); updateText(); } }, ), ), ), ], ), Row( mainAxisSize: MainAxisSize.max, children: [ Expanded( flex: 7, child: Padding( padding: const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0), child: TextFormField( keyboardAppearance: Brightness.dark, controller: _model.textController, focusNode: _model.textFieldFocusNode, onChanged: (_) => EasyDebounce.debounce( '_model.textController', const Duration(milliseconds: 2000), () => setState(() {}), ), autofocus: true, textCapitalization: TextCapitalization.none, decoration: InputDecoration( isDense: false, counterStyle: AppTheme.bodyMedium, enabledBorder: OutlineInputBorder( borderSide: const BorderSide( color: AppTheme.secondaryText, width: 2, ), borderRadius: BorderRadius.circular(12), ), focusedBorder: OutlineInputBorder( borderSide: const BorderSide( color: Color(0x00000000), width: 2, ), borderRadius: BorderRadius.circular(12), ), errorBorder: OutlineInputBorder( borderSide: const BorderSide( color: Color(0x00000000), width: 2, ), borderRadius: BorderRadius.circular(12), ), focusedErrorBorder: OutlineInputBorder( borderSide: const BorderSide( color: Color(0x00000000), width: 2, ), borderRadius: BorderRadius.circular(12), ), filled: true, fillColor: AppTheme.alternate, suffixIcon: GestureDetector( onTap: () { if(_model.textController.text != "") { // TODO: Connect with multimedia entryText = _model.textController.text; Backend.respondWhenReady(entryText); _model.textController?.clear(); // Clear the text field SystemChannels.textInput.invokeMethod('TextInput.hide'); } }, child: const Icon( Icons.send, color: AppTheme.accent, size: 30, ), ), ), style: AppTheme.displayMedium, textAlign: TextAlign.start, maxLines: 5, minLines: 1, textInputAction: TextInputAction.send, // "Send" on keyboard onFieldSubmitted: (text) { // Don't take any action unless there's an actual input. if(_model.textController.text != "") { entryText = _model.textController.text; Backend.respondWhenReady(entryText); // TODO: Connect with multimedia _model.textController?.clear(); // Clear the text field } }, ), ), ), ], ), ], ), ), ); } void updateText() { _model.textController.text = Backend.stt.text; entryText = _model.textController.text; print(entryText); print(_model.textController.text); Backend.stt.reset(); } }