v2.11.1flutter_gen_ai_chat_ui
स्ट्रीमिंग टेक्स्ट, मार्कडाउन सपोर्ट, फ़ाइल अटैचमेंट और कस्टमाइज़ेबल थीम वाला आधुनिक Flutter चैट UI। AI असिस्टेंट्स, कस्टमर सपोर्ट और संदेश प्रेषण ऐप्स के लिए आदर्श।
फ्लटर एप्लिकेशन्स के लिए आधुनिक, कस्टमाइज़ेबल चैट UI पैकेज, एआई इंटरैक्शन के लिए अनुकूलित।
{"sdk":"flutter"}^1.1.2^1.0.3^8.1.0^1.10.1^6.3.1^0.7.2{"sdk":"flutter"}^6.0.0^7.2.0^2.1.1{"sdk":"flutter"}^6.1.2^1.3.3यह अंग्रेज़ी मूल स्नैपशॉट है। नवीनतम सामग्री GitHub पर देखें।
pub package pub likes pub points License: MIT Flutter Platform GitHub stars GitHub issues
A modern, high-performance Flutter chat UI kit for building beautiful messaging interfaces. Features streaming text animations, markdown support, file attachments, and extensive customization options. Perfect for AI assistants, customer support, team chat, social messaging, and any conversational application.
🚀 Production Ready | 📱 Cross-Platform | ⚡ High Performance | 🎨 Fully Customizable
|
Dark Mode
Dark Mode |
Chat Demo
Chat Demo |
Add this to your package's pubspec.yaml file:
dependencies:
flutter_gen_ai_chat_ui: ^2.19.1
Then run:
flutter pub get
📖 New: task-focused Cookbook — streaming, stop-generating, thinking→answer, custom bubbles, attachments, localization/RTL.
import 'package:flutter_gen_ai_chat_ui/flutter_gen_ai_chat_ui.dart';
class ChatScreen extends StatefulWidget {
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final _controller = ChatMessagesController();
final _currentUser = ChatUser(id: 'user', firstName: 'User');
final _aiUser = ChatUser(id: 'ai', firstName: 'AI Assistant');
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('AI Chat')),
body: AiChatWidget(
// Required parameters
currentUser: _currentUser,
aiUser: _aiUser,
controller: _controller,
onSendMessage: _handleSendMessage,
// Optional parameters
loadingConfig: LoadingConfig(isLoading: _isLoading),
inputOptions: InputOptions(
hintText: 'Ask me anything...',
sendOnEnter: true,
),
welcomeMessageConfig: WelcomeMessageConfig(
title: 'Welcome to AI Chat',
questionsSectionTitle: 'Try asking me:',
),
exampleQuestions: [
ExampleQuestion(question: "What can you help me with?"),
ExampleQuestion(question: "Tell me about your features"),
],
),
);
}
Future<void> _handleSendMessage(ChatMessage message) async {
setState(() => _isLoading = true);
try {
// Your AI service logic here
await Future.delayed(Duration(seconds: 1)); // Simulating API call
// Add AI response
_controller.addMessage(ChatMessage(
text: "This is a response to: ${message.text}",
user: _aiUser,
createdAt: DateTime.now(),
));
} finally {
setState(() => _isLoading = false);
}
}
}
Three Flutter chat UI packages dominate searches: flutter_gen_ai_chat_ui, flutter_chat_ui, and dash_chat_2. They target different shapes of app. If you have an LLM in the loop, this package was designed for that shape.
| Concern | flutter_gen_ai_chat_ui |
flutter_chat_ui |
dash_chat_2 |
|---|---|---|---|
| Word-by-word streaming animation | Built-in | No | No |
| Markdown + code highlight in messages | Built-in | Manual | Manual |
| LaTeX / math rendering | Opt-in flag | No | No |
| Rich inline widget messages (full-width, no bubble) | ChatMessage.rich() |
Custom bubble only | No |
| AI tool-use / function-calling UI | AiActionProvider |
No | No |
| Human-in-the-loop confirmation | Built-in | No | No |
| Mic/send toggle (ChatGPT-style) | sendOrMicBuilder |
No | No |
| RTL out of the box | Yes | Partial | Partial |
| AI welcome screen + example questions | Built-in | No | No |
| Cross-platform incl. desktop hardware Enter | All 6 | All 6 | Mobile-focused |
For general-purpose peer-to-peer chat with no AI features, flutter_chat_ui is a lighter choice. The features above are this package's reason to exist.
ChatGPT/Claude-style word-by-word streaming is built in. It is gated by two flags — both must be on:
enableMarkdownStreaming: true — master gate for the animation pipeline.streamingWordByWord: true — word-vs-character animation (default is character).Push an empty message with a stable id, then call updateMessage with a fresh ChatMessage carrying that same id each time your LLM emits a chunk. The controller matches on id and replaces the entry in place.
AiChatWidget(
currentUser: me,
aiUser: ai,
controller: controller,
onSendMessage: handleSend,
enableMarkdownStreaming: true,
streamingWordByWord: true,
streamingDuration: const Duration(milliseconds: 30),
);
Future<void> streamReply(String prompt) async {
final id = DateTime.now().microsecondsSinceEpoch.toString();
controller.addMessage(ChatMessage(
text: '',
user: ai,
createdAt: DateTime.now(),
customProperties: {'id': id, 'isStreaming': true},
));
final buffer = StringBuffer();
await for (final chunk in myStreamingLlm(prompt)) {
buffer.write(chunk);
controller.updateMessage(ChatMessage(
text: buffer.toString(),
user: ai,
createdAt: DateTime.now(),
customProperties: {'id': id, 'isStreaming': true},
));
}
controller.stopStreamingMessage(id);
}
Full runnable screen: example/lib/examples/streaming_chat.dart. Before v2.4.2 the two flags were silently ignored — set both, or leave both default.
Try the live web demo → — no install needed, runs the actual example app in your browser. Redeployed automatically on every push to main that touches the package or the example app (see .github/workflows/deploy-web-demo.yml).
Explore all features with our comprehensive example app:
FileUploadOptions)VoiceSendButton widget (InputOptions.sendOrMicBuilder)To run the example app:
cd example/
flutter run
ChatMessage.rich() - Render custom widgets (cards, forms, charts) inline in chat — full-width, no bubbleChatMessage.widget() - One-off inline widgets without a registrysendOrMicBuilder - Mic/send toggle that auto-switches based on text field empty state (ChatGPT-style)inputLeadingBuilder - Icons inside the input row, left of text field (attach, mic, etc.)attachmentPreviewBuilder - File/image preview strip above the input areaChatMessage.loading() - Shimmer placeholder that morphs into rich widget via controller.updateMessage()loadingKind - Per-kind custom loading widgets (e.g., contract spinner, lawyer search animation)resultLoadingRenderers - Register custom loading UIs per widget typeChatPersistence hook to restore/save a long-running conversation across app restarts — see the persistence cookbook recipeBuilds and runs cleanly under Flutter's --wasm web compile target (flutter build web --wasm) — no dart:html/dart:js legacy interop or other wasm-incompatible APIs in this package. CI builds the example app with --wasm on every push so a regression is caught immediately (.github/workflows/ci.yml, wasm-build job).
The package ships first-class support for right-to-left scripts (Arabic, Hebrew, Persian, Urdu, Kurdish) — no extra dependency, no extra widget.
Directionality(textDirection: TextDirection.rtl, ...) (or rely on your app's Localizations) and every surface mirrors: input row, send button, scroll, bubble alignment, copy button.TextDirection is inferred from its content (Arabic chars → RTL, ASCII → LTR), so mixed conversations render correctly in a single thread without per-message config.flutter_streaming_text_markdown 1.7.0+).Minimal RTL setup:
Directionality(
textDirection: TextDirection.rtl,
child: AiChatWidget(
currentUser: ChatUser(id: 'user', name: 'أنت'),
aiUser: ChatUser(id: 'ai', name: 'المساعد'),
controller: controller,
onSendMessage: handleSend,
enableMarkdownStreaming: true,
exampleQuestions: const [
ExampleQuestion(question: 'ما هي عاصمة العراق؟'),
ExampleQuestion(question: 'اكتب لي قصيدة قصيرة'),
],
),
)
Full working screen with streaming Arabic markdown and example questions: example/lib/examples/rtl_chat.dart.
AiChatWidget(
// Required parameters
currentUser: ChatUser(...), // The current user
aiUser: ChatUser(...), // The AI assistant
controller: ChatMessagesController(), // Message controller
onSendMessage: (message) { // Message handler
// Handle user messages here
},
// ... optional parameters
)
AiChatWidget(
// ... required parameters
// Message display options
messages: [], // Optional list of messages (if not using controller)
messageOptions: MessageOptions(...), // Message bubble styling
messageListOptions: MessageListOptions(...), // Message list behavior
// Input field customization
inputOptions: InputOptions(...), // Input field styling and behavior
readOnly: false, // Whether the chat is read-only
// AI-specific features
exampleQuestions: [ // Suggested questions for users
ExampleQuestion(question: 'What is AI?'),
],
persistentExampleQuestions: true, // Keep questions visible after welcome
enableAnimation: true, // Enable message animations
enableMarkdownStreaming: true, // Enable streaming text (FIXED in v2.4.2+)
streamingWordByWord: false, // Control word-by-word vs character animation
streamingDuration: Duration(milliseconds: 30), // Stream speed
welcomeMessageConfig: WelcomeMessageConfig(...), // Welcome message styling
// Loading states
loadingConfig: LoadingConfig( // Loading configuration
isLoading: false,
showCenteredIndicator: true,
),
// Pagination
paginationConfig: PaginationConfig( // Pagination configuration
enabled: true,
reverseOrder: true, // Newest messages at bottom
),
// Layout
maxWidth: 800, // Maximum width
padding: EdgeInsets.all(16), // Overall padding
// Scroll behavior
scrollBehaviorConfig: ScrollBehaviorConfig(
// Control auto-scrolling behavior
autoScrollBehavior: AutoScrollBehavior.onUserMessageOnly,
// Scroll to first message of a response instead of the last (for long responses)
scrollToFirstResponseMessage: true,
// Hold the start of a streaming answer at the top of the viewport (2.16.0+)
pinDuringStreaming: StreamingPinAnchor.responseStart,
),
// Custom bubble styling lives in messageOptions. Use `bubbleBuilder` to wrap
// the default bubble, or `customBubbleBuilder` (3-arg) to replace it entirely.
messageOptions: MessageOptions(
bubbleBuilder: (context, message, isCurrentUser, defaultBubble) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: defaultBubble, // wrap the default, or return your own UI
);
},
),
)
The package offers multiple ways to style the input field:
InputOptions(
// Basic properties
sendOnEnter: true,
// Focus behavior (NEW in v2.4.2+)
autofocus: true, // Automatically focus the input field
focusNode: myFocusNode, // Custom focus node for external control
// Styling
textStyle: TextStyle(...),
decoration: InputDecoration(...),
)
InputOptions.minimal(
hintText: 'Ask a question...',
textColor: Colors.black,
hintColor: Colors.grey,
backgroundColor: Colors.white,
borderRadius: 24.0,
autofocus: true, // Available in factory constructors too
focusNode: myFocusNode, // Custom focus node support
)
InputOptions.glassmorphic(
colors: [Colors.blue.withOpacityCompat(0.2), Colors.purple.withOpacityCompat(0.2)],
borderRadius: 24.0,
blurStrength: 10.0,
hintText: 'Ask me anything...',
textColor: Colors.white,
autofocus: false, // Control autofocus behavior
focusNode: myFocusNode, // Optional custom focus node
)
InputOptions.custom(
decoration: yourCustomDecoration,
textStyle: yourCustomTextStyle,
sendButtonBuilder: (onSend) => CustomSendButton(onSend: onSend),
)
The send button is now hardcoded to always be visible by design, regardless of text content. This removes the need for an explicit setting and ensures a consistent experience across the package.
By default:
// Configure input options to ensure a consistent typing experience
InputOptions(
// Prevent losing focus when tapping outside
unfocusOnTapOutside: false,
// Use newline for Enter key to prevent keyboard focus issues on mobile.
// Hardware Enter on desktop/web still sends — see "Enter key behavior" below.
textInputAction: TextInputAction.newline,
)
With sendOnEnter: true (the default), pressing Enter on a hardware keyboard sends the message. This works on macOS, Windows, Linux, web, and on iOS/Android devices with an attached physical keyboard.
sendOnEnter: false to disable; Enter then always inserts a newline.Provide an onCancelGenerating callback and the send button automatically turns into a stop button whenever loadingConfig.isLoading is true. The package doesn't run the generation itself, so the callback is where you cancel your own stream / HTTP request.
StreamSubscription<String>? _streamSub;
String? _currentId;
bool _isLoading = false;
AiChatWidget(
// ...
loadingConfig: LoadingConfig(isLoading: _isLoading),
onCancelGenerating: () {
_streamSub?.cancel(); // stop your own work
if (_currentId != null) {
_controller.stopStreamingMessage(_currentId!); // finalize the partial bubble
}
setState(() => _isLoading = false);
},
)
onCancelGenerating != null and isLoading == true. Otherwise the normal send button is shown.InputOptions(stopButtonIcon: ..., stopButtonColor: ...), or replace it entirely with InputOptions(cancelButtonBuilder: (onCancel) => ...).See example/lib/examples/streaming_chat.dart for a full working implementation.
Control how the chat widget scrolls when new messages are added:
// Default configuration with manual parameters
ScrollBehaviorConfig(
// When to auto-scroll (one of: always, onNewMessage, onUserMessageOnly, never)
autoScrollBehavior: AutoScrollBehavior.onUserMessageOnly,
// Fix for long responses: scroll to first message of response instead of the last message
// This prevents the top part of long AI responses from being pushed out of view
scrollToFirstResponseMessage: true,
// Customize animation
scrollAnimationDuration: Duration(milliseconds: 300),
scrollAnimationCurve: Curves.easeOut,
// 2.16.0+: hold the start of the answer (or the user's question) at the top
// of the viewport WHILE the answer streams — see the use case below
pinDuringStreaming: StreamingPinAnchor.responseStart,
)
// Or use convenient preset configurations:
ScrollBehaviorConfig.smooth() // Smooth easeInOutCubic curve
ScrollBehaviorConfig.bouncy() // Bouncy elasticOut curve
ScrollBehaviorConfig.fast() // Quick scrolling with minimal animation
ScrollBehaviorConfig.decelerate() // Starts fast, slows down
ScrollBehaviorConfig.accelerate() // Starts slow, speeds up
When an AI returns a long response in multiple parts, scrollToFirstResponseMessage ensures users see the beginning of the response rather than being automatically scrolled to the end. This is crucial for readability, especially with complex information.
For optimal scroll behavior with long responses:
'isStartOfResponse': true'responseId' propertyscrollToFirstResponseMessage: true in your configurationpinDuringStreaming)scrollToFirstResponseMessage acts once the answer has finished. For a single long
answer that is still streaming, the classic behaviour is that the bottom of the list
keeps following the new text, so the beginning scrolls out of view and you have to
wait for the end to start reading. pinDuringStreaming (2.16.0+) fixes that:
ScrollBehaviorConfig(
// responseStart: hold the first line of the answer at the top of the viewport
// userMessage: hold your own question there instead, so Q and A read together
pinDuringStreaming: StreamingPinAnchor.responseStart,
)
What the reader sees:
The default is StreamingPinAnchor.none (unchanged behaviour). The pin arms whenever a
message streams: addMessage/updateMessage with 'isStreaming': true, or
addStreamingMessage + updateMessage + stopStreamingMessage.
MessageOptions(
// Basic options
showTime: true,
showUserName: true,
// Timestamp styling. `timeTextStyle` applies to both bubbles; the per-bubble
// overrides (2.12.0+) take precedence when set — handy when a colored user
// bubble makes the shared timestamp hard to read.
timeTextStyle: const TextStyle(fontSize: 11, color: Colors.grey),
userTimeTextStyle: const TextStyle(fontSize: 11, color: Colors.white70),
aiTimeTextStyle: const TextStyle(fontSize: 11, color: Colors.black45),
// Styling
bubbleStyle: BubbleStyle(
userBubbleColor: Colors.blue.withOpacityCompat(0.1),
aiBubbleColor: Colors.white,
userNameColor: Colors.blue.shade700,
aiNameColor: Colors.purple.shade700,
bottomLeftRadius: 22,
bottomRightRadius: 22,
enableShadow: true,
),
)
Create completely custom message bubbles with full control over styling and behavior:
AiChatWidget(
// ... other parameters
messageOptions: MessageOptions(
// Wrapper approach: enhance the default bubble (e.g. add a report button).
// `bubbleBuilder` hands you the fully-styled default bubble to wrap.
bubbleBuilder: (context, message, isCurrentUser, defaultBubble) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: defaultBubble,
);
},
// Full-replacement approach (3-arg): rebuild the bubble from scratch.
// customBubbleBuilder: (context, message, isCurrentUser) =>
// MyCustomBubble(message: message, isCurrentUser: isCurrentUser),
),
)
Apply a ChatGPT/Claude/Gemini-style look with one line, via a CustomThemeExtension on your app's ThemeData — no need to set colors on every individual AiChatWidget:
MaterialApp(
theme: ThemeData(
extensions: [CustomThemeExtension.chatgpt()], // or .claude() / .gemini()
),
// For dark mode: CustomThemeExtension.chatgpt(dark: true)
home: MyChatScreen(),
)
Two more presets derive their colors from your app's existing ColorScheme instead of a fixed brand palette — handy when you want a theme-consistent look without picking your own bubble colors:
CustomThemeExtension.modern(Theme.of(context).colorScheme) // richer surfaces
CustomThemeExtension.minimal(Theme.of(context).colorScheme) // flat, low-contrast
CustomThemeExtension sets chatBackground, messageBubbleColor/userBubbleColor/messageTextColor, inputBackgroundColor/inputBorderColor/inputTextColor/hintTextColor, and sendButtonColor/backToBottomButtonColor — all consulted only when the more specific BubbleStyle/MessageOptions/InputOptions value is left unset, so any explicit per-widget color you already set keeps winning. Build a fully custom one with CustomThemeExtension(chatBackground: ..., messageBubbleColor: ..., ...).
Transform your chat into a powerful AI agent platform! The AI Actions System allows your AI to execute real functions, display rich results, and maintain human oversight - taking your chat beyond simple text exchanges.
import 'package:flutter_gen_ai_chat_ui/flutter_gen_ai_chat_ui.dart';
class MyAiChat extends StatefulWidget {
@override
_MyAiChatState createState() => _MyAiChatState();
}
class _MyAiChatState extends State<MyAiChat> {
late ChatMessagesController _controller;
@override
Widget build(BuildContext context) {
return AiActionProvider(
config: AiActionConfig(
actions: [
// Define what your AI can do
AiAction(
name: 'calculate',
description: 'Perform mathematical calculations',
parameters: [
ActionParameter.number(name: 'a', description: 'First number', required: true),
ActionParameter.number(name: 'b', description: 'Second number', required: true),
ActionParameter.string(
name: 'operation',
description: 'Math operation',
required: true,
enumValues: ['add', 'subtract', 'multiply', 'divide']
),
],
handler: (params) async {
final a = params['a'] as num;
final b = params['b'] as num;
final op = params['operation'] as String;
double result;
switch (op) {
case 'add': result = a + b; break;
case 'subtract': result = a - b; break;
case 'multiply': result = a * b; break;
case 'divide': result = a / b; break;
default: throw 'Unknown operation';
}
return ActionResult.createSuccess({
'result': result,
'equation': '$a $op $b = $result'
});
},
// Custom UI for results
render: (context, status, params, {result, error}) {
if (status == ActionStatus.completed && result?.data != null) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
result!.data['equation'],
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
);
}
return SizedBox.shrink();
},
),
],
),
child: AiChatWidget(
// Your existing chat configuration
currentUser: currentUser,
aiUser: aiUser,
controller: _controller,
onSendMessage: _handleMessage,
),
);
}
void _handleMessage(ChatMessage message) {
// Add user message
_controller.addMessage(message);
// Simulate AI deciding to use an action
if (message.text.contains('calculate')) {
_executeCalculation(message.text);
}
}
void _executeCalculation(String userMessage) async {
final actionHook = AiActionHook.of(context);
// AI parses user message and calls action
final result = await actionHook.executeAction('calculate', {
'a': 15,
'b': 3,
'operation': 'multiply'
});
// Add AI response with result
_controller.addMessage(ChatMessage(
text: result.success ?
'I calculated that for you: ${result.data['equation']}' :
'Sorry, calculation failed: ${result.error}',
user: aiUser,
));
}
}
AiAction(
name: 'send_email',
description: 'Send an email to a contact',
parameters: [
ActionParameter.string(
name: 'to',
description: 'Recipient email address',
required: true,
validator: (email) => email.contains('@'), // Custom validation
),
ActionParameter.string(
name: 'subject',
description: 'Email subject',
required: true,
),
ActionParameter.string(
name: 'priority',
description: 'Email priority level',
enumValues: ['low', 'normal', 'high'], // Constrained options
defaultValue: 'normal',
),
],
handler: (params) async {
// Your email sending logic
await sendEmailService(params);
return ActionResult.createSuccess({'sent': true});
},
)
AiAction(
name: 'delete_file',
description: 'Delete a file from storage',
parameters: [...],
confirmationConfig: ActionConfirmationConfig(
title: 'Delete File',
message: 'This action cannot be undone. Continue?',
required: true, // Always ask for confirmation
),
handler: (params) async {
// Only executed after user confirms
await deleteFile(params['filename']);
return ActionResult.createSuccess();
},
)
AiAction(
name: 'generate_report',
description: 'Generate a comprehensive report',
render: (context, status, params, {result, error}) {
switch (status) {
case ActionStatus.executing:
return Card(
child: Row(children: [
CircularProgressIndicator(),
Text('Generating report...'),
]),
);
case ActionStatus.completed:
return ReportWidget(data: result!.data);
case ActionStatus.failed:
return ErrorWidget(error: error!);
default:
return SizedBox.shrink();
}
},
handler: (params) async {
// Long-running operation with progress updates
return await generateComplexReport(params);
},
)
class MyAiChat extends StatefulWidget {
@override
_MyAiChatState createState() => _MyAiChatState();
}
class _MyAiChatState extends State<MyAiChat> {
late StreamSubscription<ActionEvent> _actionSubscription;
@override
void initState() {
super.initState();
// Listen to all action events
_actionSubscription = AiActionProvider.of(context).events.listen((event) {
switch (event.type) {
case ActionEventType.started:
print('Action ${event.actionName} started');
break;
case ActionEventType.completed:
print('Action ${event.actionName} completed: ${event.result?.data}');
break;
case ActionEventType.failed:
print('Action ${event.actionName} failed: ${event.error}');
break;
}
});
}
@override
void dispose() {
_actionSubscription.cancel();
super.dispose();
}
}
// Convert actions to OpenAI function format
final actionHook = AiActionHook.of(context);
final functions = actionHook.getActionsForFunctionCalling();
// Send to OpenAI with functions
final response = await openAI.createChatCompletion(
messages: messages,
functions: functions,
functionCall: 'auto',
);
// Execute function if AI wants to call one
if (response.functionCall != null) {
final result = await actionHook.handleFunctionCall(
response.functionCall.name,
json.decode(response.functionCall.arguments),
);
}
void _processAIMessage(String userMessage) async {
// Your AI logic decides which action to call
if (_shouldCalculate(userMessage)) {
final actionHook = AiActionHook.of(context);
// Extract parameters from user message
final params = _parseCalculationParams(userMessage);
// Execute action
final result = await actionHook.executeAction('calculate', params);
// Show result in chat
_controller.addMessage(ChatMessage(
text: 'Result: ${result.data}',
user: aiUser,
));
}
}
The package includes complete working examples:
Run the example app to see AI Actions in action:
cd example/
flutter run
Want your app featured? Submit a showcase request
Fixed Streaming Animation Disable: The enableAnimation: false, enableMarkdownStreaming: false, and streamingWordByWord: false parameters now work correctly. Previously, markdown messages would always stream regardless of these settings.
Added Focus Control: New autofocus and focusNode support in InputOptions for better input field control.
InputOptions(
autofocus: true, // Auto-focus input on widget load
focusNode: myFocusNode, // External focus control
)
Enhanced Factory Constructors: InputOptions.minimal() and InputOptions.glassmorphic() now support the new focus parameters.
"The streaming text animation is incredibly smooth and the file attachment system saved us weeks of development." - Sarah Chen, Senior Flutter Developer
"Best chat UI package I've used. The performance with large message lists is outstanding." - Ahmed Hassan, Mobile Team Lead
"Finally, a chat package that actually works well for AI applications. The streaming feature is exactly what we needed." - Maria Rodriguez, Product Manager
Made with ❤️ by the Flutter community | Star ⭐ this repo if it helped you!