import 'dart:convert'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../data/models/ag_ui_event.dart'; import '../../data/models/chat_list_item.dart'; import '../../data/models/tool_result.dart'; import '../../data/services/ag_ui_service.dart'; class ChatState { final List items; final bool isLoading; final String? currentMessageId; final String? error; const ChatState({ this.items = const [], this.isLoading = false, this.currentMessageId, this.error, }); static const _unset = Object(); ChatState copyWith({ List? items, bool? isLoading, Object? currentMessageId = _unset, Object? error = _unset, }) { return ChatState( items: items ?? this.items, isLoading: isLoading ?? this.isLoading, currentMessageId: currentMessageId == _unset ? this.currentMessageId : currentMessageId as String?, error: error == _unset ? this.error : error as String?, ); } } class ChatBloc extends Cubit { final AgUiService _service; final Map _toolCallArgsBuffer = {}; ChatBloc({AgUiService? service}) : _service = service ?? AgUiService(), super(const ChatState()) { _service.onEvent = _handleEvent; } void _handleEvent(AgUiEvent event) { switch (event.type) { case AgUiEventType.runStarted: emit(state.copyWith(isLoading: true, error: null)); break; case AgUiEventType.runFinished: emit(state.copyWith(isLoading: false, currentMessageId: null)); break; case AgUiEventType.runError: final errorEvent = event as RunErrorEvent; emit(state.copyWith(isLoading: false, error: errorEvent.message)); break; case AgUiEventType.textMessageStart: final startEvent = event as TextMessageStartEvent; final newMessage = TextMessageItem( id: startEvent.messageId, content: '', timestamp: DateTime.now(), sender: MessageSender.ai, isStreaming: true, ); emit( state.copyWith( items: [...state.items, newMessage], currentMessageId: startEvent.messageId, ), ); break; case AgUiEventType.textMessageContent: final contentEvent = event as TextMessageContentEvent; final updatedItems = state.items.map((item) { if (item.id == contentEvent.messageId && item is TextMessageItem) { return item.copyWith(content: item.content + contentEvent.delta); } return item; }).toList(); emit(state.copyWith(items: updatedItems)); break; case AgUiEventType.textMessageEnd: final endEvent = event as TextMessageEndEvent; final updatedItems = state.items.map((item) { if (item.id == endEvent.messageId && item is TextMessageItem) { return item.copyWith(isStreaming: false); } return item; }).toList(); emit(state.copyWith(items: updatedItems, currentMessageId: null)); break; case AgUiEventType.toolCallStart: final startEvent = event as ToolCallStartEvent; _toolCallArgsBuffer[startEvent.toolCallId] = ''; final newToolCall = ToolCallItem( id: startEvent.toolCallId, callId: startEvent.toolCallId, toolName: startEvent.toolCallName, args: {}, status: ToolCallStatus.pending, timestamp: DateTime.now(), sender: MessageSender.ai, ); emit(state.copyWith(items: [...state.items, newToolCall])); break; case AgUiEventType.toolCallArgs: final argsEvent = event as ToolCallArgsEvent; _toolCallArgsBuffer[argsEvent.toolCallId] = (_toolCallArgsBuffer[argsEvent.toolCallId] ?? '') + argsEvent.delta; break; case AgUiEventType.toolCallEnd: final endEvent = event as ToolCallEndEvent; final argsBuffer = _toolCallArgsBuffer[endEvent.toolCallId] ?? ''; Map parsedArgs = {}; if (argsBuffer.isNotEmpty) { try { parsedArgs = jsonDecode(argsBuffer) as Map; } catch (_) {} } _toolCallArgsBuffer.remove(endEvent.toolCallId); final updatedItems = state.items.map((item) { if (item.id == endEvent.toolCallId && item is ToolCallItem) { return item.copyWith( args: parsedArgs, status: ToolCallStatus.executing, ); } return item; }).toList(); emit(state.copyWith(items: updatedItems)); break; case AgUiEventType.toolCallResult: final resultEvent = event as ToolCallResultEvent; final filteredItems = state.items.where((item) { if (item.id == resultEvent.toolCallId && item is ToolCallItem) { return false; } return true; }).toList(); final resultItem = ToolResultItem( id: resultEvent.messageId, callId: resultEvent.toolCallId, uiCard: resultEvent.ui ?? UiCard(cardType: 'empty', data: {}), timestamp: DateTime.now(), sender: MessageSender.ai, ); emit(state.copyWith(items: [...filteredItems, resultItem])); break; case AgUiEventType.toolCallError: final errorEvent = event as ToolCallErrorEvent; _toolCallArgsBuffer.remove(errorEvent.toolCallId); final updatedItems = state.items.map((item) { if (item.id == errorEvent.toolCallId && item is ToolCallItem) { return item.copyWith( status: ToolCallStatus.error, errorMessage: errorEvent.error, ); } return item; }).toList(); emit(state.copyWith(items: updatedItems)); break; case AgUiEventType.unknown: break; } } Future sendMessage(String content) async { final userMessage = TextMessageItem( id: 'user-${DateTime.now().millisecondsSinceEpoch}', content: content, timestamp: DateTime.now(), sender: MessageSender.user, ); emit(state.copyWith(items: [...state.items, userMessage])); await _service.sendMessage(content); } void clearError() { emit(state.copyWith(error: null)); } }