refactor(chat): 重构聊天模块并集成历史消息加载功能
- 删除冗余的 chat_history_repository 和 home_mock_data - 简化 ag_ui_event fromJson 使用工厂映射表 - 提取 ChatBloc 事件处理方法,添加 loadHistory/loadMoreHistory - HomeScreen 集成 ChatBloc 实现历史消息加载和下拉刷新 - 更新 AGENTS.md 文档约束
This commit is contained in:
@@ -63,126 +63,218 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
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;
|
||||
_handleTextMessageStart(event as TextMessageStartEvent);
|
||||
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;
|
||||
_handleTextMessageContent(event as TextMessageContentEvent);
|
||||
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;
|
||||
_handleTextMessageEnd(event as TextMessageEndEvent);
|
||||
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;
|
||||
_handleToolCallStart(event as ToolCallStartEvent);
|
||||
case AgUiEventType.toolCallArgs:
|
||||
final argsEvent = event as ToolCallArgsEvent;
|
||||
_toolCallArgsBuffer[argsEvent.toolCallId] =
|
||||
(_toolCallArgsBuffer[argsEvent.toolCallId] ?? '') + argsEvent.delta;
|
||||
break;
|
||||
_handleToolCallArgs(event as ToolCallArgsEvent);
|
||||
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;
|
||||
_handleToolCallEnd(event as ToolCallEndEvent);
|
||||
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;
|
||||
_handleToolCallResult(event as ToolCallResultEvent);
|
||||
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;
|
||||
_handleToolCallError(event as ToolCallErrorEvent);
|
||||
case AgUiEventType.messagesSnapshot:
|
||||
_handleMessagesSnapshot(event as MessagesSnapshotEvent);
|
||||
case AgUiEventType.unknown:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTextMessageStart(TextMessageStartEvent startEvent) {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTextMessageContent(TextMessageContentEvent contentEvent) {
|
||||
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));
|
||||
}
|
||||
|
||||
void _handleTextMessageEnd(TextMessageEndEvent endEvent) {
|
||||
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));
|
||||
}
|
||||
|
||||
void _handleToolCallStart(ToolCallStartEvent startEvent) {
|
||||
_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]));
|
||||
}
|
||||
|
||||
void _handleToolCallArgs(ToolCallArgsEvent argsEvent) {
|
||||
_toolCallArgsBuffer[argsEvent.toolCallId] =
|
||||
(_toolCallArgsBuffer[argsEvent.toolCallId] ?? '') + argsEvent.delta;
|
||||
}
|
||||
|
||||
void _handleToolCallEnd(ToolCallEndEvent endEvent) {
|
||||
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));
|
||||
}
|
||||
|
||||
void _handleToolCallResult(ToolCallResultEvent resultEvent) {
|
||||
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]));
|
||||
}
|
||||
|
||||
void _handleToolCallError(ToolCallErrorEvent errorEvent) {
|
||||
_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));
|
||||
}
|
||||
|
||||
void _handleMessagesSnapshot(MessagesSnapshotEvent snapshotEvent) {
|
||||
final newItems = _convertSnapshotMessages(snapshotEvent.messages);
|
||||
final allItems = [...newItems, ...state.items];
|
||||
|
||||
// Determine oldest date and history availability
|
||||
DateTime? newOldestDate = state.oldestLoadedDate;
|
||||
bool newHasEarlierHistory = false;
|
||||
|
||||
if (newItems.isNotEmpty) {
|
||||
newOldestDate = _extractDateFromItems(newItems);
|
||||
if (newOldestDate != null) {
|
||||
newHasEarlierHistory = _service.hasEarlierHistory(newOldestDate);
|
||||
}
|
||||
} else if (newOldestDate != null) {
|
||||
newHasEarlierHistory = _service.hasEarlierHistory(newOldestDate);
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
items: allItems,
|
||||
oldestLoadedDate: newOldestDate,
|
||||
hasEarlierHistory: newHasEarlierHistory,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<ChatListItem> _convertSnapshotMessages(List<SnapshotMessage> messages) {
|
||||
return messages.map((msg) {
|
||||
final timestamp = msg.timestamp ?? DateTime.now();
|
||||
switch (msg.role) {
|
||||
case 'user':
|
||||
return TextMessageItem(
|
||||
id: msg.id,
|
||||
content: msg.content ?? '',
|
||||
timestamp: timestamp,
|
||||
sender: MessageSender.user,
|
||||
);
|
||||
case 'assistant':
|
||||
return TextMessageItem(
|
||||
id: msg.id,
|
||||
content: msg.content ?? '',
|
||||
timestamp: timestamp,
|
||||
sender: MessageSender.ai,
|
||||
);
|
||||
case 'tool' when msg.ui != null:
|
||||
return ToolResultItem(
|
||||
id: msg.id,
|
||||
callId: msg.toolCallId ?? '',
|
||||
uiCard: msg.ui!,
|
||||
timestamp: timestamp,
|
||||
sender: MessageSender.ai,
|
||||
);
|
||||
default:
|
||||
return TextMessageItem(
|
||||
id: msg.id,
|
||||
content: msg.content ?? '',
|
||||
timestamp: timestamp,
|
||||
sender: MessageSender.ai,
|
||||
);
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
DateTime? _extractDateFromItems(List<ChatListItem> items) {
|
||||
if (items.isEmpty) return null;
|
||||
|
||||
return items
|
||||
.map(
|
||||
(item) => DateTime(
|
||||
item.timestamp.year,
|
||||
item.timestamp.month,
|
||||
item.timestamp.day,
|
||||
),
|
||||
)
|
||||
.reduce((a, b) => a.isBefore(b) ? a : b);
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String content) async {
|
||||
final userMessage = TextMessageItem(
|
||||
id: 'user-${DateTime.now().millisecondsSinceEpoch}',
|
||||
@@ -194,6 +286,18 @@ class ChatBloc extends Cubit<ChatState> {
|
||||
await _service.sendMessage(content);
|
||||
}
|
||||
|
||||
Future<void> loadHistory() async {
|
||||
if (state.isLoading) return;
|
||||
await _service.loadHistory();
|
||||
}
|
||||
|
||||
Future<void> loadMoreHistory() async {
|
||||
if (state.isLoading || !state.hasEarlierHistory) return;
|
||||
if (state.oldestLoadedDate == null) return;
|
||||
|
||||
await _service.loadHistory(beforeDate: state.oldestLoadedDate);
|
||||
}
|
||||
|
||||
void clearError() {
|
||||
emit(state.copyWith(error: null));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user