11import 'dart:async' ;
22import 'dart:ui' ;
33import 'package:flutter/material.dart' ;
4+ import 'package:flutter_markdown_plus/flutter_markdown_plus.dart' ;
45
56class GlassAppBar extends StatelessWidget implements PreferredSizeWidget {
67 final String name;
@@ -104,11 +105,35 @@ class ChatBubble extends StatelessWidget {
104105 crossAxisAlignment: CrossAxisAlignment .end,
105106 mainAxisSize: MainAxisSize .min,
106107 children: [
107- Text (
108- message,
109- style: TextStyle (
110- color: isMe ? Colors .white : Colors .black87,
111- fontSize: 15 ,
108+ MarkdownBody (
109+ data: message,
110+ selectable: false ,
111+ styleSheet: MarkdownStyleSheet (
112+ p: TextStyle (
113+ color: isMe ? Colors .white : Colors .black87,
114+ fontSize: 15 ,
115+ ),
116+ strong: TextStyle (
117+ color: isMe ? Colors .white : Colors .black87,
118+ fontWeight: FontWeight .bold,
119+ ),
120+ em: TextStyle (
121+ color: isMe ? Colors .white : Colors .black87,
122+ fontStyle: FontStyle .italic,
123+ ),
124+ del: TextStyle (
125+ color: isMe ? Colors .white70 : Colors .black54,
126+ decoration: TextDecoration .lineThrough,
127+ ),
128+ code: TextStyle (
129+ backgroundColor: isMe ? Colors .blue[800 ] : Colors .grey[300 ],
130+ color: isMe ? Colors .blue[50 ] : Colors .red[800 ],
131+ fontFamily: 'monospace' ,
132+ ),
133+ a: TextStyle (
134+ color: isMe ? Colors .blue[100 ] : Colors .blue[700 ],
135+ decoration: TextDecoration .underline,
136+ ),
112137 ),
113138 ),
114139 const SizedBox (height: 4 ),
@@ -155,8 +180,21 @@ class ChatInputArea extends StatefulWidget {
155180
156181class _ChatInputAreaState extends State <ChatInputArea > {
157182 final _controller = TextEditingController ();
183+ final FocusNode _focusNode = FocusNode ();
184+
158185 bool _isTyping = false ;
159186 Timer ? _typingTimer;
187+ bool _showFormattingToolbar = false ;
188+
189+ @override
190+ void initState () {
191+ super .initState ();
192+ _focusNode.addListener (() {
193+ setState (() {
194+ _showFormattingToolbar = _focusNode.hasFocus;
195+ });
196+ });
197+ }
160198
161199 @override
162200 void dispose () {
@@ -165,6 +203,7 @@ class _ChatInputAreaState extends State<ChatInputArea> {
165203 widget.onTypingChanged? .call (false );
166204 }
167205 _controller.dispose ();
206+ _focusNode.dispose ();
168207 super .dispose ();
169208 }
170209
@@ -203,6 +242,35 @@ class _ChatInputAreaState extends State<ChatInputArea> {
203242 }
204243 }
205244
245+ void _insertMarkdown (String prefix, String suffix) {
246+ final text = _controller.text;
247+ final selection = _controller.selection;
248+
249+ if (! selection.isValid || selection.start == selection.end) {
250+ final newText = text + prefix + suffix;
251+ _controller.value = TextEditingValue (
252+ text: newText,
253+ selection: TextSelection .collapsed (
254+ offset: newText.length - suffix.length,
255+ ),
256+ );
257+ } else {
258+ final selectedText = text.substring (selection.start, selection.end);
259+ final newText = text.replaceRange (
260+ selection.start,
261+ selection.end,
262+ '$prefix $selectedText $suffix ' ,
263+ );
264+ _controller.value = TextEditingValue (
265+ text: newText,
266+ selection: TextSelection .collapsed (
267+ offset: selection.start + prefix.length + selectedText.length,
268+ ),
269+ );
270+ }
271+ _handleOnChange (_controller.text);
272+ }
273+
206274 void _submit () {
207275 _isOverLimit = false ;
208276 final text = _controller.text.trim ();
@@ -230,74 +298,115 @@ class _ChatInputAreaState extends State<ChatInputArea> {
230298 child: Container (
231299 color: Colors .transparent,
232300 padding: const EdgeInsets .symmetric (horizontal: 12.0 , vertical: 20 ),
233- child: Row (
234- mainAxisAlignment: MainAxisAlignment .center,
235- crossAxisAlignment: CrossAxisAlignment .start,
301+ child: Column (
302+ mainAxisSize: MainAxisSize .min,
236303 children: [
237- Expanded (
238- child: TextField (
239- controller: _controller,
240- onChanged: _handleOnChange,
241- keyboardType: TextInputType .multiline,
242- minLines: 1 ,
243- maxLines: 5 ,
244- maxLength: _charLimit,
245- maxLengthEnforcement: .none,
246- // cursorColor: Colors.blue,
247- style: TextStyle (
248- color: _isOverLimit ? Colors .red : Colors .black87,
249- fontSize: 15 ,
250- ),
251- decoration: InputDecoration (
252- hintText: "Write a message..." ,
253- hintStyle: const TextStyle (color: Colors .black38),
254-
255- fillColor: _isOverLimit
256- ? Colors .red.withValues (alpha: 0.1 )
257- : const Color (0xFFF5F5F5 ),
258- filled: true ,
259- errorText: _isOverLimit
260- ? 'character limit exceeded'
261- : null ,
262- counterStyle: TextStyle (
263- color: _isOverLimit ? Colors .red : Colors .black54,
264- fontWeight: _isOverLimit
265- ? FontWeight .bold
266- : FontWeight .normal,
267- ),
268-
269- border: OutlineInputBorder (
270- borderRadius: BorderRadius .circular (24 ),
271- borderSide: _isOverLimit
272- ? const BorderSide (color: Colors .red, width: 1.5 )
273- : BorderSide .none,
274- ),
275- focusedBorder: OutlineInputBorder (
276- borderRadius: BorderRadius .circular (24 ),
277- borderSide: _isOverLimit
278- ? const BorderSide (color: Colors .red, width: 2 )
279- : BorderSide .none,
280- ),
281- enabledBorder: OutlineInputBorder (
282- borderRadius: BorderRadius .circular (24 ),
283- borderSide: _isOverLimit
284- ? const BorderSide (color: Colors .red, width: 1.5 )
285- : BorderSide .none,
286- ),
287-
288- contentPadding: const EdgeInsets .symmetric (
289- horizontal: 16 ,
290- vertical: 10 ,
304+ AnimatedSize (
305+ duration: const Duration (milliseconds: 200 ),
306+ child: _showFormattingToolbar
307+ ? Container (
308+ padding: const EdgeInsets .only (bottom: 8 ),
309+ child: Row (
310+ mainAxisAlignment: MainAxisAlignment .start,
311+ children: [
312+ _FormatButton (
313+ icon: Icons .format_bold,
314+ onPressed: () => _insertMarkdown ('**' , '**' ),
315+ ),
316+ _FormatButton (
317+ icon: Icons .format_italic,
318+ onPressed: () => _insertMarkdown ('*' , '*' ),
319+ ),
320+ _FormatButton (
321+ icon: Icons .format_strikethrough,
322+ onPressed: () => _insertMarkdown ('~~' , '~~' ),
323+ ),
324+ _FormatButton (
325+ icon: Icons .code,
326+ onPressed: () => _insertMarkdown ('`' , '`' ),
327+ ),
328+ _FormatButton (
329+ icon: Icons .link,
330+ onPressed: () => _insertMarkdown ('[' , '](url)' ),
331+ ),
332+ ],
333+ ),
334+ )
335+ : const SizedBox .shrink (),
336+ ),
337+ Row (
338+ mainAxisAlignment: MainAxisAlignment .center,
339+ crossAxisAlignment: CrossAxisAlignment .start,
340+ children: [
341+ Expanded (
342+ child: TextField (
343+ controller: _controller,
344+ focusNode: _focusNode,
345+ onChanged: _handleOnChange,
346+ keyboardType: TextInputType .multiline,
347+ minLines: 1 ,
348+ maxLines: 5 ,
349+ maxLength: _charLimit,
350+ maxLengthEnforcement: .none,
351+ style: TextStyle (
352+ color: _isOverLimit ? Colors .red : Colors .black87,
353+ fontSize: 15 ,
354+ ),
355+ decoration: InputDecoration (
356+ hintText: "Write a message..." ,
357+ hintStyle: const TextStyle (color: Colors .black38),
358+ fillColor: _isOverLimit
359+ ? Colors .red.withValues (alpha: 0.1 )
360+ : const Color (0xFFF5F5F5 ),
361+ filled: true ,
362+ errorText: _isOverLimit
363+ ? 'character limit exceeded'
364+ : null ,
365+ counterStyle: TextStyle (
366+ color: _isOverLimit ? Colors .red : Colors .black54,
367+ fontWeight: _isOverLimit
368+ ? FontWeight .bold
369+ : FontWeight .normal,
370+ ),
371+ border: OutlineInputBorder (
372+ borderRadius: BorderRadius .circular (24 ),
373+ borderSide: _isOverLimit
374+ ? const BorderSide (
375+ color: Colors .red,
376+ width: 1.5 ,
377+ )
378+ : BorderSide .none,
379+ ),
380+ focusedBorder: OutlineInputBorder (
381+ borderRadius: BorderRadius .circular (24 ),
382+ borderSide: _isOverLimit
383+ ? const BorderSide (color: Colors .red, width: 2 )
384+ : BorderSide .none,
385+ ),
386+ enabledBorder: OutlineInputBorder (
387+ borderRadius: BorderRadius .circular (24 ),
388+ borderSide: _isOverLimit
389+ ? const BorderSide (
390+ color: Colors .red,
391+ width: 1.5 ,
392+ )
393+ : BorderSide .none,
394+ ),
395+ contentPadding: const EdgeInsets .symmetric (
396+ horizontal: 16 ,
397+ vertical: 10 ,
398+ ),
399+ ),
291400 ),
292401 ),
293- ),
294- ),
295- const SizedBox (width : 8 ),
296- IconButton (
297- icon : _isOverLimit
298- ? const Icon ( Icons .not_interested)
299- : const Icon ( Icons .send, color : Color ( 0xFF1890FF ) ),
300- onPressed : _isOverLimit ? null : _submit ,
402+ const SizedBox (width : 8 ),
403+ IconButton (
404+ icon : _isOverLimit
405+ ? const Icon ( Icons .not_interested)
406+ : const Icon ( Icons .send, color : Color ( 0xFF1890FF )),
407+ onPressed : _isOverLimit ? null : _submit,
408+ ),
409+ ] ,
301410 ),
302411 ],
303412 ),
@@ -307,3 +416,21 @@ class _ChatInputAreaState extends State<ChatInputArea> {
307416 );
308417 }
309418}
419+
420+ class _FormatButton extends StatelessWidget {
421+ final IconData icon;
422+ final VoidCallback onPressed;
423+
424+ const _FormatButton ({required this .icon, required this .onPressed});
425+
426+ @override
427+ Widget build (BuildContext context) {
428+ return IconButton (
429+ icon: Icon (icon, size: 20 , color: Colors .black54),
430+ onPressed: onPressed,
431+ visualDensity: VisualDensity .compact,
432+ padding: const EdgeInsets .symmetric (horizontal: 4 ),
433+ constraints: const BoxConstraints (),
434+ );
435+ }
436+ }
0 commit comments