Files
flutterApp/lib/features/ai/presentation/bloc/ai_cubit.dart
2026-02-03 10:13:59 +08:00

179 lines
6.4 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:image_picker/image_picker.dart';
import 'ai_state.dart';
import '../../data/models/chat_message.dart';
import '../../data/models/session.dart';
class AiCubit extends Cubit<AiState> {
AiCubit() : super(const AiState()) {
createNewSession();
fetchSessions();
}
final Dio _dio = Dio();
final ImagePicker _picker = ImagePicker();
static const String BASE_URL = 'http://1.95.137.212:30009';
final String _userId = '1';
Future<void> fetchSessions() async {
try {
final response = await _dio.get('$BASE_URL/sessions', queryParameters: {'userId': _userId});
if (response.statusCode == 200 && response.data['code'] == 0) {
final sessionsData = response.data['sessions'] as List;
emit(state.copyWith(sessions: sessionsData.map((data) => Session.fromJson(data)).toList()));
}
} catch (e) {
emit(state.copyWith(error: "Failed to fetch sessions: $e"));
}
}
void createNewSession() {
emit(AiState(
sessionId: DateTime.now().millisecondsSinceEpoch.toString(),
messages: [ChatMessage(text: "Welcome to the AI chat!", isUserMessage: false)],
status: AiStatus.initial,
selectedImageFile: null, // Explicitly set to null
sessions: state.sessions, // Preserve existing sessions
));
}
Future<void> loadSessionHistory(String sessionId) async {
emit(state.copyWith(status: AiStatus.loading, messages: [], sessionId: sessionId));
try {
final response = await _dio.get('$BASE_URL/history', queryParameters: {'sessionId': sessionId});
if (response.statusCode == 200 && response.data['code'] == 0) {
final messagesData = response.data['messages'] as List;
final historyMessages = messagesData.map((data) {
return ChatMessage(
text: data['content'],
thinkingText: data['role'] == 'assistant' ? data['thinking'] : null,
imageBase64: data['role'] == 'user' ? data['image_base64'] : null,
isUserMessage: data['role'] == 'user',
);
}).toList();
emit(state.copyWith(status: AiStatus.success, messages: historyMessages.reversed.toList()));
}
} catch (e) {
emit(state.copyWith(status: AiStatus.failure, error: "Failed to load session history: $e"));
}
}
Future<void> analyze({String? prompt, File? image}) async {
if (state.status == AiStatus.loading) return;
String? imageBase64;
if (image != null) {
final bytes = await image.readAsBytes();
imageBase64 = 'data:image/png;base64,${base64Encode(bytes)}';
}
final userMessage = ChatMessage(text: prompt, image: image, isUserMessage: true);
final aiPlaceholder = ChatMessage(text: '', thinkingText: '', isUserMessage: false);
// Create a new state to ensure selectedImageFile is set to null
final newState = AiState(
status: AiStatus.loading,
messages: [aiPlaceholder, userMessage, ...state.messages],
selectedImageFile: null, // Clear the selected image
sessions: state.sessions,
sessionId: state.sessionId,
error: state.error,
);
emit(newState);
try {
final response = await _dio.post(
'$BASE_URL/analyze',
data: {
'userId': _userId,
'sessionId': state.sessionId,
'userPrompt': prompt,
'imageBase64': imageBase64,
},
options: Options(responseType: ResponseType.stream),
);
String currentPhase = 'thinking';
StringBuffer thinkingBuffer = StringBuffer();
StringBuffer answerBuffer = StringBuffer();
final streamSubscription = response.data.stream.listen(
(chunk) {
final decodedChunk = utf8.decode(chunk);
final events = decodedChunk.split('\n\n').where((s) => s.isNotEmpty);
for (final event in events) {
if (event.startsWith('data: ')) {
final dataString = event.substring(6);
if (dataString == '[DONE]') continue;
try {
final data = jsonDecode(dataString);
final type = data['type'];
final content = data['content'] as String;
if (type == 'phase') {
currentPhase = content;
} else if (type == 'thinking' && currentPhase == 'thinking') {
thinkingBuffer.write(content);
} else if (type == 'answer' && currentPhase == 'answer') {
answerBuffer.write(content);
}
final updatedMessages = List<ChatMessage>.from(state.messages);
updatedMessages[0] = ChatMessage(
thinkingText: thinkingBuffer.toString(),
text: answerBuffer.toString(),
isUserMessage: false,
);
emit(state.copyWith(messages: updatedMessages));
} catch (e) {
print("Error parsing stream data: $e");
}
}
}
},
onDone: () {
emit(state.copyWith(status: AiStatus.success));
fetchSessions();
},
onError: (error) {
final updatedMessages = List<ChatMessage>.from(state.messages);
updatedMessages[0] = ChatMessage(text: "Error: $error", isUserMessage: false);
emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages));
},
);
} catch (e) {
final updatedMessages = List<ChatMessage>.from(state.messages);
updatedMessages[0] = ChatMessage(text: "Failed to connect to the server: $e", isUserMessage: false);
emit(state.copyWith(status: AiStatus.failure, messages: updatedMessages));
}
}
void selectImage(ImageSource source) async {
final pickedFile = await _picker.pickImage(source: source);
if (pickedFile != null) {
emit(state.copyWith(selectedImageFile: File(pickedFile.path)));
}
}
void deselectImage() {
// Manually create a new state to ensure selectedImageFile is set to null,
// bypassing the limitation of the existing copyWith method.
emit(AiState(
status: state.status,
messages: state.messages,
sessions: state.sessions,
sessionId: state.sessionId,
error: state.error,
selectedImageFile: null, // This will now correctly clear the file.
));
}
}