feat(chat): add ChatBloc for state management

This commit is contained in:
qzl
2026-02-28 13:40:46 +08:00
parent d12f846cc0
commit e1973a9868
@@ -0,0 +1,197 @@
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';
class ChatState {
final List<ChatListItem> 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<ChatListItem>? 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 AgUiService {
void Function(AgUiEvent)? onEvent;
AgUiService({this.onEvent});
Future<void> sendMessage(String content) async {}
}
class ChatBloc extends Cubit<ChatState> {
final AgUiService _service;
final Map<String, String> _toolCallArgsBuffer = {};
ChatBloc({AgUiService? service})
: _service = service ?? AgUiService(onEvent: (_) {}),
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<String, dynamic> parsedArgs = {};
if (argsBuffer.isNotEmpty) {
try {
parsedArgs = jsonDecode(argsBuffer) as Map<String, dynamic>;
} 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<void> 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));
}
}