2026-03-30 09:06:10 +08:00
|
|
|
// ignore_for_file: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
|
|
|
|
|
|
|
|
|
part of 'chat_bloc.dart';
|
|
|
|
|
|
|
|
|
|
extension _ChatBlocEvents on ChatBloc {
|
|
|
|
|
void _handleEvent(AgUiEvent event) {
|
|
|
|
|
switch (event.type) {
|
|
|
|
|
case AgUiEventType.runStarted:
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
isSending: false,
|
|
|
|
|
isWaitingFirstToken: true,
|
|
|
|
|
isCancelling: false,
|
|
|
|
|
error: null,
|
|
|
|
|
currentStage: null,
|
|
|
|
|
hasSeenStep: false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
case AgUiEventType.runFinished:
|
|
|
|
|
emit(
|
|
|
|
|
_resetRunState().copyWith(items: _removeToolCallItems(state.items)),
|
|
|
|
|
);
|
|
|
|
|
case AgUiEventType.runError:
|
|
|
|
|
final errorEvent = event as RunErrorEvent;
|
|
|
|
|
final isCanceledByUser = errorEvent.code == 'RUN_CANCELED';
|
|
|
|
|
emit(
|
|
|
|
|
_resetRunState(
|
|
|
|
|
error: isCanceledByUser ? null : errorEvent.message,
|
|
|
|
|
).copyWith(
|
|
|
|
|
items: _markActiveToolCallsFailed(
|
|
|
|
|
state.items,
|
|
|
|
|
reason: isCanceledByUser
|
|
|
|
|
? L10n.current.chatRunCanceled
|
|
|
|
|
: L10n.current.chatRunFailed,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
case AgUiEventType.stepStarted:
|
|
|
|
|
_handleStepStarted(event as StepStartedEvent);
|
|
|
|
|
case AgUiEventType.stepFinished:
|
|
|
|
|
_handleStepFinished(event as StepFinishedEvent);
|
|
|
|
|
case AgUiEventType.textMessageEnd:
|
|
|
|
|
_handleTextMessageEnd(event as TextMessageEndEvent);
|
|
|
|
|
case AgUiEventType.toolCallStart:
|
|
|
|
|
_handleToolCallStart(event as ToolCallStartEvent);
|
|
|
|
|
case AgUiEventType.toolCallArgs:
|
|
|
|
|
_handleToolCallArgs(event as ToolCallArgsEvent);
|
|
|
|
|
case AgUiEventType.toolCallEnd:
|
|
|
|
|
_handleToolCallEnd(event as ToolCallEndEvent);
|
|
|
|
|
case AgUiEventType.toolCallResult:
|
|
|
|
|
_handleToolCallResult(event as ToolCallResultEvent);
|
|
|
|
|
case AgUiEventType.toolCallError:
|
|
|
|
|
_handleToolCallError(event as ToolCallErrorEvent);
|
|
|
|
|
case AgUiEventType.unknown:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleStepStarted(StepStartedEvent event) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
currentStage: stageFromStepName(event.stepName),
|
|
|
|
|
hasSeenStep: true,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleStepFinished(StepFinishedEvent event) {
|
|
|
|
|
if (state.currentStage == stageFromStepName(event.stepName)) {
|
|
|
|
|
emit(state.copyWith(currentStage: null));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleTextMessageEnd(TextMessageEndEvent event) {
|
|
|
|
|
final timestamp = DateTime.now();
|
|
|
|
|
final items = _updateOrAddMessage(
|
|
|
|
|
state.items,
|
|
|
|
|
event.messageId,
|
|
|
|
|
event.answer,
|
|
|
|
|
timestamp,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final uiSchema = event.uiSchema;
|
|
|
|
|
if (uiSchema != null) {
|
|
|
|
|
_upsertUiSchema(items, event.messageId, uiSchema, timestamp);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
items: _removeToolCallItems(items),
|
|
|
|
|
currentMessageId: null,
|
|
|
|
|
isWaitingFirstToken: false,
|
|
|
|
|
isStreaming: false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<ChatListItem> _updateOrAddMessage(
|
|
|
|
|
List<ChatListItem> items,
|
|
|
|
|
String messageId,
|
|
|
|
|
String content,
|
|
|
|
|
DateTime timestamp,
|
|
|
|
|
) {
|
|
|
|
|
final result = List<ChatListItem>.from(items);
|
|
|
|
|
final index = result.indexWhere(
|
|
|
|
|
(item) => item.id == messageId && item is TextMessageItem,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (index >= 0) {
|
|
|
|
|
final existing = result[index] as TextMessageItem;
|
|
|
|
|
result[index] = existing.copyWith(content: content, isStreaming: false);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.add(
|
|
|
|
|
TextMessageItem(
|
|
|
|
|
id: messageId,
|
|
|
|
|
content: content,
|
|
|
|
|
timestamp: timestamp,
|
|
|
|
|
sender: MessageSender.ai,
|
|
|
|
|
isStreaming: false,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _upsertUiSchema(
|
|
|
|
|
List<ChatListItem> items,
|
|
|
|
|
String messageId,
|
|
|
|
|
Map<String, dynamic> uiSchema,
|
|
|
|
|
DateTime timestamp,
|
|
|
|
|
) {
|
|
|
|
|
final uiItemId = '$messageId-ui';
|
|
|
|
|
final uiItem = ToolResultItem(
|
|
|
|
|
id: uiItemId,
|
|
|
|
|
callId: messageId,
|
|
|
|
|
uiSchema: uiSchema,
|
|
|
|
|
timestamp: timestamp,
|
|
|
|
|
sender: MessageSender.ai,
|
|
|
|
|
);
|
|
|
|
|
final existingIndex = items.indexWhere((item) => item.id == uiItemId);
|
|
|
|
|
if (existingIndex >= 0) {
|
|
|
|
|
items[existingIndex] = uiItem;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
items.add(uiItem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleToolCallStart(ToolCallStartEvent event) {
|
|
|
|
|
final exists = state.items.any(
|
|
|
|
|
(item) => item is ToolCallItem && item.id == event.toolCallId,
|
|
|
|
|
);
|
|
|
|
|
if (exists) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
items: [
|
|
|
|
|
...state.items,
|
|
|
|
|
ToolCallItem(
|
|
|
|
|
id: event.toolCallId,
|
|
|
|
|
callId: event.toolCallId,
|
|
|
|
|
toolName: event.toolCallName,
|
|
|
|
|
args: const {},
|
|
|
|
|
status: ToolCallStatus.pending,
|
|
|
|
|
timestamp: DateTime.now(),
|
|
|
|
|
sender: MessageSender.ai,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleToolCallArgs(ToolCallArgsEvent event) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
items: state.items.map((item) {
|
|
|
|
|
if (item is ToolCallItem && item.id == event.toolCallId) {
|
|
|
|
|
return item.copyWith(args: event.args);
|
|
|
|
|
}
|
|
|
|
|
return item;
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleToolCallEnd(ToolCallEndEvent event) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
items: state.items.map((item) {
|
|
|
|
|
if (item is ToolCallItem && item.id == event.toolCallId) {
|
|
|
|
|
return item.copyWith(status: ToolCallStatus.executing);
|
|
|
|
|
}
|
|
|
|
|
return item;
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleToolCallResult(ToolCallResultEvent event) {
|
2026-03-30 18:36:57 +08:00
|
|
|
if (_shouldRefreshCalendarForTool(event)) {
|
|
|
|
|
unawaited(_refreshCalendarAfterToolMutation());
|
|
|
|
|
}
|
2026-03-30 09:06:10 +08:00
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
items: state.items.map((item) {
|
|
|
|
|
if (item is ToolCallItem && item.id == event.toolCallId) {
|
|
|
|
|
return item.copyWith(status: ToolCallStatus.completed);
|
|
|
|
|
}
|
|
|
|
|
return item;
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleToolCallError(ToolCallErrorEvent event) {
|
|
|
|
|
emit(
|
|
|
|
|
state.copyWith(
|
|
|
|
|
items: state.items.map((item) {
|
|
|
|
|
if (item is ToolCallItem && item.id == event.toolCallId) {
|
|
|
|
|
return item.copyWith(
|
|
|
|
|
status: ToolCallStatus.error,
|
|
|
|
|
errorMessage: event.error,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return item;
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<ChatListItem> _removeToolCallItems(List<ChatListItem> items) {
|
|
|
|
|
return items.where((item) => item is! ToolCallItem).toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<ChatListItem> _markActiveToolCallsFailed(
|
|
|
|
|
List<ChatListItem> items, {
|
|
|
|
|
required String reason,
|
|
|
|
|
}) {
|
|
|
|
|
return items.map((item) {
|
|
|
|
|
if (item is! ToolCallItem ||
|
|
|
|
|
item.status == ToolCallStatus.error ||
|
|
|
|
|
item.status == ToolCallStatus.completed) {
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
return item.copyWith(status: ToolCallStatus.error, errorMessage: reason);
|
|
|
|
|
}).toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
List<ChatListItem> _convertHistoryMessages(List<HistoryMessage> messages) {
|
|
|
|
|
final converted = <ChatListItem>[];
|
|
|
|
|
for (final msg in messages) {
|
|
|
|
|
final normalizedRole = msg.role.toLowerCase();
|
|
|
|
|
final isUser = normalizedRole == 'user';
|
|
|
|
|
final isTool = normalizedRole == 'tool' || normalizedRole == 'tools';
|
|
|
|
|
final sender = isUser ? MessageSender.user : MessageSender.ai;
|
|
|
|
|
final attachments = msg.attachments
|
|
|
|
|
.map(
|
|
|
|
|
(attachment) => <String, dynamic>{
|
|
|
|
|
'url': attachment.url,
|
|
|
|
|
'mimeType': attachment.mimeType,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
|
|
if (!isTool && (msg.content.isNotEmpty || isUser)) {
|
|
|
|
|
converted.add(
|
|
|
|
|
TextMessageItem(
|
|
|
|
|
id: msg.id,
|
|
|
|
|
content: msg.content,
|
|
|
|
|
timestamp: msg.timestamp,
|
|
|
|
|
sender: sender,
|
|
|
|
|
isLocalEcho: false,
|
|
|
|
|
attachments: attachments,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isTool && msg.uiSchema != null) {
|
|
|
|
|
converted.add(
|
|
|
|
|
ToolResultItem(
|
|
|
|
|
id: '${msg.id}-ui',
|
|
|
|
|
callId: msg.id,
|
|
|
|
|
uiSchema: msg.uiSchema!,
|
|
|
|
|
timestamp: msg.timestamp,
|
|
|
|
|
sender: MessageSender.ai,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return converted;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|