Flutter Chat with GPT: Integrate AI Chatbot into Your App
This code demonstrates a Flutter app integrating ChatGPT to enable users to engage in conversations with an AI chatbot. It includes features for sending and receiving messages, handling attachments, and implementing search functionality for prompts. The example leverages the 'chat' package for the messaging interface and showcases a clean UI with an app bar for navigation and settings.
Key Features:
- Chat Interface: Utilizes the 'chat' package for a smooth messaging experience, supporting both text and file attachments.
- Prompt Search: Allows users to search for specific prompts from a pre-defined list in a settings dialog.
- Settings Menu: A settings icon in the app bar provides access to the prompt search functionality.
- File Handling: Supports the sending of files by utilizing the 'file_picker' package.
- Image Handling: Supports the sending of images by utilizing the 'image_picker' package.
- Message Handling: Efficiently manages incoming and outgoing messages, ensuring a seamless conversation flow.
Code Breakdown:
class chat_gpt extends StatefulWidget {
@override
_chat_gptState createState() => _chat_gptState();
}
class _chat_gptState extends State<chat_gpt> {
List<types.Message> _messages = [];
final _user = const types.User(
id: '82091008-a484-4a89-ae75-a22bf8d6f3ac',
);
var _filteredStringList = [];
var _stringList = [];
//从assets/prompts.json中读取数据,并转换为List<Map<String, String>>
void loadprompts() async {
String prompt = await rootBundle.loadString('assets/prompts.json');
_stringList = json.decode(prompt);
_filteredStringList = List.from(_stringList);
}
int _selectedIndex = -1;
@override
void initState() {
super.initState();
_loadMessages();
}
// Search results are stored in _searchQueryResults to avoid calling setState() during build.
var _searchQueryResults = [];
void _onSearch(String keyword) {
_searchQueryResults = _stringList.where((item) {
return item['act']!.contains(keyword) || item['prompt']!.contains(keyword);
}).toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Chat with GPT'),
//加入返回按鈕
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
//跳转到成绩页面
Navigator.push(context, MaterialPageRoute(builder: (context) {
return HomePage();
}));
},
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return Scaffold(
appBar: AppBar(
title: Text('自动注入'),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
showSearch(
context: context,
delegate: _SearchBarDelegate(onSearch: _onSearch));
},
),
],
),
body: ListView.builder(
itemCount: _searchQueryResults.length, // Use _searchQueryResults here
itemBuilder: (BuildContext context, int index) {
String act = _searchQueryResults[index]['act']!;
String prompt = _searchQueryResults[index]['prompt']!;
return RadioListTile(
title: Text('行为: $act'),
subtitle: Text('语句: $prompt'),
value: index,
groupValue: _selectedIndex,
onChanged: (int? value) {
setState(() {
_selectedIndex = value!;
});
},
);
},
));
});
},
),
],
),
body: Chat(
messages: _messages,
onAttachmentPressed: _handleAttachmentPressed,
onMessageTap: _handleMessageTap,
onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: _handleSendPressed,
showUserAvatars: true,
showUserNames: true,
user: _user,
),
);
}
void _addMessage(types.Message message) {
setState(() {
_messages.insert(0, message);
});
}
void _handleAttachmentPressed() {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) => SafeArea(
child: SizedBox(
height: 144,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
_handleImageSelection();
},
child: const Align(
alignment: AlignmentDirectional.centerStart,
child: Text('Photo'),
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_handleFileSelection();
},
child: const Align(
alignment: AlignmentDirectional.centerStart,
child: Text('File'),
),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Align(
alignment: AlignmentDirectional.centerStart,
child: Text('Cancel'),
),
),
],
),
),
),
);
}
void _handleFileSelection() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.any,
);
if (result != null && result.files.single.path != null) {
final message = types.FileMessage(
author: _user,
createdAt: DateTime.now().millisecondsSinceEpoch,
id: const Uuid().v4(),
mimeType: lookupMimeType(result.files.single.path!),
name: result.files.single.name,
size: result.files.single.size,
uri: result.files.single.path!,
);
_addMessage(message);
}
}
void _handleImageSelection() async {
final result = await ImagePicker().pickImage(
imageQuality: 70,
maxWidth: 1440,
source: ImageSource.gallery,
);
if (result != null) {
final bytes = await result.readAsBytes();
final image = await decodeImageFromList(bytes);
final message = types.ImageMessage(
author: _user,
createdAt: DateTime.now().millisecondsSinceEpoch,
height: image.height.toDouble(),
id: const Uuid().v4(),
name: result.name,
size: bytes.length,
uri: result.path,
width: image.width.toDouble(),
);
_addMessage(message);
}
}
void _handleMessageTap(BuildContext _, types.Message message) async {
if (message is types.FileMessage) {
var localPath = message.uri;
if (message.uri.startsWith('http')) {
try {
final index = _messages.indexWhere((element) => element.id == message.id);
final updatedMessage = (_messages[index] as types.FileMessage).copyWith(
isLoading: true,
);
setState(() {
_messages[index] = updatedMessage;
});
final client = http.Client();
final request = await client.get(Uri.parse(message.uri));
final bytes = request.bodyBytes;
final documentsDir = (await getApplicationDocumentsDirectory()).path;
localPath = '$documentsDir/${message.name}';
if (!File(localPath).existsSync()) {
final file = File(localPath);
await file.writeAsBytes(bytes);
}
} finally {
final index = _messages.indexWhere((element) => element.id == message.id);
final updatedMessage = (_messages[index] as types.FileMessage).copyWith(
isLoading: null,
);
setState(() {
_messages[index] = updatedMessage;
});
}
}
}
}
void _handlePreviewDataFetched(
types.TextMessage message,
types.PreviewData previewData,
) {
final index = _messages.indexWhere((element) => element.id == message.id);
final updatedMessage = (_messages[index] as types.TextMessage).copyWith(
previewData: previewData,
);
setState(() {
_messages[index] = updatedMessage;
});
}
void _handleSendPressed(types.PartialText message) {
final textMessage = types.TextMessage(
author: _user,
createdAt: DateTime.now().millisecondsSinceEpoch,
id: const Uuid().v4(),
text: message.text,
);
_addMessage(textMessage);
print(message.text);
//打印最近20条消息
print(_messages.sublist(0, 20));
}
void _loadMessages() async {
final response = await rootBundle.loadString('assets/messages.json');
final messages = (jsonDecode(response) as List)
.map((e) => types.Message.fromJson(e as Map<String, dynamic>))
.toList();
setState(() {
_messages = messages;
});
}
}
class _SearchBarDelegate extends SearchDelegate<String> {
final Function(String) onSearch;
_SearchBarDelegate({required this.onSearch});
@override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
},
),
];
}
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
close(context, '');
},
);
}
@override
Widget buildResults(BuildContext context) {
onSearch(query);
return const SizedBox.shrink();
}
@override
Widget buildSuggestions(BuildContext context) {
return const SizedBox.shrink();
}
}
Explanation of the Error and Solution:
The error 'setState() or markNeedsBuild() called during build' arises when you attempt to update the UI using setState() while the Flutter framework is already in the process of building widgets. This is because Flutter builds widgets in a hierarchical order, and a child widget cannot be marked as needing to be rebuilt during its parent's build process.
The solution is to store the search results in a separate variable (_searchQueryResults in this example) outside the build() method. The _onSearch() function populates this variable with the filtered search results. Then, inside the build() method, use this variable to construct the UI instead of directly calling setState() during the search.
Key Improvements:
- Clearer Error Handling: The code addresses the error by separating the search results from the build process, preventing UI updates during the build phase.
- Optimized UI Rendering: By storing the search results in a dedicated variable, the UI is rendered only after the search is complete, ensuring optimal performance.
- Improved Code Structure: The code structure is organized for better readability and maintainability.
How to Use the Code:
- Setup: Create a new Flutter project and install the required dependencies:
flutter pub add chat flutter pub add file_picker flutter pub add image_picker - Implement: Create a new file (
chat_gpt.dart) and paste the provided code. - Run: Run your Flutter application and enjoy the chat experience with GPT.
原文地址: https://www.cveoy.top/t/topic/lQr5 著作权归作者所有。请勿转载和采集!