179 lines
6.4 KiB
Dart
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.
|
|
));
|
|
}
|
|
}
|