11import  'dart:developer' ;
22import  'dart:io' ;
33
4+ import  'package:flutter/cupertino.dart' ;
45import  "package:flutter/material.dart" ;
6+ import  'package:flutter/services.dart' ;
7+ import  'package:flutter_markdown/flutter_markdown.dart' ;
58import  'package:image_picker/image_picker.dart' ;
69import  'package:loading_animation_widget/loading_animation_widget.dart' ;
710import  'package:flutter_chat_ui/flutter_chat_ui.dart' ;
811import  'package:flutter_riverpod/flutter_riverpod.dart' ;
912import  'package:convogen/providers/gemini_chat_provider.dart' ;
1013import  'package:simple_gradient_text/simple_gradient_text.dart' ;
1114import  'package:shimmer/shimmer.dart' ;
15+ import  'package:speech_to_text/speech_recognition_result.dart' ;
16+ import  'package:speech_to_text/speech_to_text.dart'  as  stt;
17+ import  'package:toastification/toastification.dart' ;
1218
1319class  ChatPage  extends  ConsumerStatefulWidget  {
1420  const  ChatPage ({super .key});
@@ -35,6 +41,64 @@ class _ChatPageState extends ConsumerState<ChatPage> {
3541        customStatusBuilder:  (message, {required  context}) {
3642          return  const  SizedBox ();
3743        },
44+         textMessageBuilder:  (p0, {required  messageWidth, required  showName}) {
45+           if  (p0.author.id ==  '1' ) {
46+             return  Padding (
47+               padding:  const  EdgeInsets .all (15.0 ),
48+               child:  Text (p0.text, style:  const  TextStyle (color:  Colors .white)),
49+             );
50+           }
51+           return  Markdown (
52+             data:  p0.text,
53+             shrinkWrap:  true ,
54+             physics:  const  NeverScrollableScrollPhysics (),
55+           );
56+         },
57+         listBottomWidget:  AnimatedContainer (
58+           height:  geminiChat.isTyping ?  0  :  80 ,
59+           duration:  const  Duration (milliseconds:  300 ),
60+           child:  Padding (
61+             padding:  const  EdgeInsets .symmetric (horizontal:  20.0 )
62+                 .copyWith (bottom:  20 ),
63+             child:  Row (
64+               children:  [
65+                 TextButton (
66+                     onPressed:  () {
67+                       ref.read (geminiChatProvider.notifier).reset ();
68+                     },
69+                     child:  Row (
70+                       children:  [
71+                         Icon (CupertinoIcons .bolt_circle,
72+                             color:  Theme .of (context).colorScheme.primary),
73+                         const  SizedBox (width:  5 ),
74+                         const  Text ("Start New Chat" ),
75+                       ],
76+                     )),
77+                 IconButton (
78+                     onPressed:  () {
79+                       var  text = 
80+                           (geminiChat.messages.first.toJson ()["type" ] ==  'text' )
81+                               ?  geminiChat.messages.first.toJson ()["text" ]
82+                               :  '' ;
83+                       log (text);
84+                       Clipboard .setData (ClipboardData (text:  text));
85+                       toastification.show (
86+                         context:  context,
87+                         type:  ToastificationType .success,
88+                         style:  ToastificationStyle .flat,
89+                         title:  const  Text ('Copied' ),
90+                         description:  const  Text ('Copied to clipboard' ),
91+                         alignment:  Alignment .bottomCenter,
92+                         autoCloseDuration:  const  Duration (seconds:  4 ),
93+                         boxShadow:  lowModeShadow,
94+                       );
95+                     },
96+                     icon:  Icon (Icons .copy_rounded,
97+                         color:  Theme .of (context).colorScheme.primary))
98+               ],
99+             ),
100+           ),
101+         ),
38102        emptyState:  EmptyStateWidget (onSendPressed:  (p0) async  {
39103          FocusManager .instance.primaryFocus? .unfocus ();
40104          await  ref
@@ -79,6 +143,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
79143                    .read (geminiChatProvider.notifier)
80144                    .getPrompt (p0, selectedImage);
81145              }
146+               log ("SEND PRESSED: $p0 " );
82147            }),
83148        typingIndicatorOptions:  TypingIndicatorOptions (
84149          customTypingIndicator:  Padding (
@@ -230,7 +295,7 @@ class EmptyStateWidget extends StatelessWidget {
230295  }
231296}
232297
233- class  CustomBottomInputBar  extends  StatelessWidget  {
298+ class  CustomBottomInputBar  extends  StatefulWidget  {
234299  final  bool  collapsed;
235300  final  Function  onSendPressed;
236301  final  Function  setImage;
@@ -242,16 +307,72 @@ class CustomBottomInputBar extends StatelessWidget {
242307      required  this .onSendPressed,
243308      required  this .setImage});
244309
310+   @override 
311+   State <CustomBottomInputBar > createState () =>  _CustomBottomInputBarState ();
312+ }
313+ 
314+ class  _CustomBottomInputBarState  extends  State <CustomBottomInputBar > {
315+   var  inputController =  TextEditingController ();
316+   final  stt.SpeechToText  _speechToText =  stt.SpeechToText ();
317+   bool  _speechEnabled =  false ;
318+   String  _lastWords =  '' ;
319+ 
320+   @override 
321+   void  initState () {
322+     super .initState ();
323+     _initSpeech ();
324+   }
325+ 
326+   /// This has to happen only once per app 
327+ void  _initSpeech () async  {
328+     _speechEnabled =  await  _speechToText.initialize ();
329+     setState (() {});
330+   }
331+ 
332+   /// Each time to start a speech recognition session 
333+ void  _startListening () async  {
334+     await  _speechToText.listen (onResult:  _onSpeechResult);
335+     setState (() {});
336+   }
337+ 
338+   /// Manually stop the active speech recognition session 
339+   /// Note that there are also timeouts that each platform enforces 
340+   /// and the SpeechToText plugin supports setting timeouts on the 
341+   /// listen method. 
342+ void  _stopListening () async  {
343+     await  _speechToText.stop ();
344+     setState (() {});
345+   }
346+ 
347+   /// This is the callback that the SpeechToText plugin calls when 
348+   /// the platform returns recognized words. 
349+ void  _onSpeechResult (SpeechRecognitionResult  result) {
350+     setState (() {
351+       _lastWords =  result.recognizedWords;
352+       if  (result.finalResult) {
353+         inputController.text =  _lastWords;
354+       }
355+     });
356+   }
357+ 
245358  @override 
246359  Widget  build (BuildContext  context) {
247-     var  inputController =  TextEditingController ();
360+     handleMicPress () async  {
361+       log ("Mic Pressed" );
362+       if  (_speechEnabled) {
363+         _startListening ();
364+       } else  {
365+         // show snackbar 
366+         log ("Speech not enabled" );
367+       }
368+     }
248369
249370    handleCameraPressed () {
250371      log ("Camera pressed" );
251372      ImagePicker ().pickImage (source:  ImageSource .gallery).then ((image) {
252373        if  (image !=  null ) {
253374          log ("IMAGE SELECTED: ${image .path }" );
254-           setImage (image);
375+           widget. setImage (image);
255376        }
256377      });
257378    }
@@ -274,18 +395,25 @@ class CustomBottomInputBar extends StatelessWidget {
274395          children:  [
275396            TextField (
276397                onSubmitted:  (value) {
277-                   onSendPressed (inputController.text);
398+                   widget.onSendPressed (inputController.text);
399+                   inputController.clear ();
278400                },
401+                 minLines:  1 ,
402+                 maxLines:  5 ,
279403                controller:  inputController,
404+                 keyboardType:  TextInputType .text,
280405                decoration:  InputDecoration (
281406                    hintStyle:  const  TextStyle (
282407                      fontSize:  18 ,
283408                    ),
284-                     hintText:  'Type, talk, or share \n a photo' ,
409+                     hintText:  _speechToText.isListening
410+                         ?  'Listening...' 
411+                         :  'Type, talk, or share \n a photo' ,
285412                    hintMaxLines:  2 ,
286413                    suffix:  IconButton (
287414                        onPressed:  () {
288-                           onSendPressed (inputController.text);
415+                           widget.onSendPressed (inputController.text);
416+                           inputController.clear ();
289417                        },
290418                        icon:  const  Icon (Icons .send_rounded)),
291419                    border:  InputBorder .none)),
@@ -295,28 +423,30 @@ class CustomBottomInputBar extends StatelessWidget {
295423            Row (
296424              mainAxisAlignment:  MainAxisAlignment .center,
297425              children:  [
298-                 selectedImage !=  null 
426+                 widget. selectedImage !=  null 
299427                    ?  Container (
300428                        margin:  const  EdgeInsets .only (right:  10 ),
301429                        child:  ClipRRect (
302430                          borderRadius:  BorderRadius .circular (10 ),
303431                          child:  Image .file (
304-                             File (selectedImage! .path),
432+                             File (widget. selectedImage! .path),
305433                            width:  50 ,
306434                            height:  50 ,
307435                          ),
308436                        ),
309437                      )
310438                    :  const  SizedBox (),
311-                 selectedImage !=  null 
439+                 widget. selectedImage !=  null 
312440                    ?  IconButton (
313-                         onPressed:  () =>  setImage (null ),
441+                         onPressed:  () =>  widget. setImage (null ),
314442                        icon:  Icon (
315443                          Icons .delete_outline,
316444                          color:  Theme .of (context).colorScheme.error,
317445                        ))
318446                    :  const  SizedBox (),
319-                 selectedImage !=  null  ?  const  Spacer () :  const  SizedBox (),
447+                 widget.selectedImage !=  null 
448+                     ?  const  Spacer ()
449+                     :  const  SizedBox (),
320450                FilledButton (
321451                  // color: Colors.red, 
322452                  style:  ButtonStyle (
@@ -327,9 +457,15 @@ class CustomBottomInputBar extends StatelessWidget {
327457                    crossAxisAlignment:  CrossAxisAlignment .center,
328458                    mainAxisAlignment:  MainAxisAlignment .center,
329459                    children:  [
330-                       IconButton (
331-                           onPressed:  () {},
332-                           icon:  const  Icon (Icons .mic_none_outlined)),
460+                       _speechToText.isListening
461+                           ?  IconButton (
462+                               onPressed:  () async  {
463+                                 _stopListening ();
464+                               },
465+                               icon:  const  Icon (Icons .stop_circle_outlined))
466+                           :  IconButton (
467+                               onPressed:  handleMicPress,
468+                               icon:  const  Icon (Icons .mic_none_outlined)),
333469                      const  SizedBox (
334470                        width:  10 ,
335471                      ),
0 commit comments