fix(chat): fix ChatBloc event callback and test reliability
- Fix onEvent callback initialization in ChatBloc constructor - Add MockAgUiService to isolate test from mock API behavior - Remove unnecessary non-null assertions in tests
This commit is contained in:
@@ -2,17 +2,23 @@ import 'dart:convert';
|
|||||||
|
|
||||||
enum Intent { createEvent, searchEvent, unknown }
|
enum Intent { createEvent, searchEvent, unknown }
|
||||||
|
|
||||||
class AiDecisionEngine {
|
|
||||||
/// 意图匹配规则(顺序敏感:searchEvent 优先级高于 createEvent)
|
/// 意图匹配规则(顺序敏感:searchEvent 优先级高于 createEvent)
|
||||||
static final List<(RegExp, Intent)> _orderedPatterns = [
|
final _orderedPatterns = <(RegExp, Intent)>[
|
||||||
(RegExp(r'^查看|^有什么|^今天.*日程|^明天.*安排|^查询'), Intent.searchEvent),
|
(RegExp(r'^查看|^有什么|^今天.*日程|^明天.*安排|^查询'), Intent.searchEvent),
|
||||||
(RegExp(r'提醒|开会|预约|安排.*时间|创建.*日程'), Intent.createEvent),
|
(RegExp(r'提醒|开会|预约|安排.*时间|创建.*日程'), Intent.createEvent),
|
||||||
(
|
(RegExp(r'明天.*\d|今天.*\d|后天.*\d|\d{1,2}点|\d{1,2}:\d{2}'), Intent.createEvent),
|
||||||
RegExp(r'明天.*\d|今天.*\d|后天.*\d|\d{1,2}点|\d{1,2}:\d{2}'),
|
|
||||||
Intent.createEvent,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// 时区常量
|
||||||
|
const _defaultTimezone = 'Asia/Shanghai';
|
||||||
|
const _dayToday = '今天';
|
||||||
|
const _dayTomorrow = '明天';
|
||||||
|
const _dayAfterTomorrow = '后天';
|
||||||
|
const _tomorrowOffset = 1;
|
||||||
|
const _dayAfterTomorrowOffset = 2;
|
||||||
|
const _defaultMinute = 0;
|
||||||
|
|
||||||
|
class AiDecisionEngine {
|
||||||
Intent matchIntent(String text) {
|
Intent matchIntent(String text) {
|
||||||
for (final (pattern, intent) in _orderedPatterns) {
|
for (final (pattern, intent) in _orderedPatterns) {
|
||||||
if (pattern.hasMatch(text)) return intent;
|
if (pattern.hasMatch(text)) return intent;
|
||||||
@@ -34,31 +40,33 @@ class AiDecisionEngine {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args['title'] == null || (args['title'] as String).isEmpty) return null;
|
final title = args['title'];
|
||||||
|
if (title == null || (title as String).isEmpty) return null;
|
||||||
|
|
||||||
final timeMatch = RegExp(
|
final timeMatch = RegExp(
|
||||||
r'(明天|今天|后天)?\s*(\d{1,2})[:点](\d{2})?',
|
r'(明天|今天|后天)?\s*(\d{1,2})[:点](\d{2})?',
|
||||||
).firstMatch(text);
|
).firstMatch(text);
|
||||||
if (timeMatch != null) {
|
if (timeMatch != null) {
|
||||||
final dayStr = timeMatch.group(1) ?? '今天';
|
final dayStr = timeMatch.group(1) ?? _dayToday;
|
||||||
final hour = int.parse(timeMatch.group(2)!);
|
final hour = int.parse(timeMatch.group(2)!);
|
||||||
final minute = int.parse(timeMatch.group(3) ?? '0');
|
final minute = int.parse(timeMatch.group(3) ?? '$_defaultMinute');
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
DateTime startAt;
|
final dayOffset = switch (dayStr) {
|
||||||
switch (dayStr) {
|
_dayTomorrow => _tomorrowOffset,
|
||||||
case '明天':
|
_dayAfterTomorrow => _dayAfterTomorrowOffset,
|
||||||
startAt = DateTime(now.year, now.month, now.day + 1, hour, minute);
|
_ => 0,
|
||||||
break;
|
};
|
||||||
case '后天':
|
final startAt = DateTime(
|
||||||
startAt = DateTime(now.year, now.month, now.day + 2, hour, minute);
|
now.year,
|
||||||
break;
|
now.month,
|
||||||
default:
|
now.day + dayOffset,
|
||||||
startAt = DateTime(now.year, now.month, now.day, hour, minute);
|
hour,
|
||||||
}
|
minute,
|
||||||
|
);
|
||||||
|
|
||||||
args['startAt'] = startAt.toIso8601String();
|
args['startAt'] = startAt.toIso8601String();
|
||||||
args['timezone'] = 'Asia/Shanghai';
|
args['timezone'] = _defaultTimezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args.containsKey('startAt')) return null;
|
if (!args.containsKey('startAt')) return null;
|
||||||
|
|||||||
@@ -32,63 +32,39 @@ enum AgUiEventType {
|
|||||||
unknown,
|
unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
AgUiEventType agUiEventTypeFromWire(String wire) {
|
const _wireToTypeMap = {
|
||||||
switch (wire) {
|
AgUiEventTypeWire.runStarted: AgUiEventType.runStarted,
|
||||||
case AgUiEventTypeWire.runStarted:
|
AgUiEventTypeWire.runFinished: AgUiEventType.runFinished,
|
||||||
return AgUiEventType.runStarted;
|
AgUiEventTypeWire.runError: AgUiEventType.runError,
|
||||||
case AgUiEventTypeWire.runFinished:
|
AgUiEventTypeWire.textMessageStart: AgUiEventType.textMessageStart,
|
||||||
return AgUiEventType.runFinished;
|
AgUiEventTypeWire.textMessageContent: AgUiEventType.textMessageContent,
|
||||||
case AgUiEventTypeWire.runError:
|
AgUiEventTypeWire.textMessageEnd: AgUiEventType.textMessageEnd,
|
||||||
return AgUiEventType.runError;
|
AgUiEventTypeWire.toolCallStart: AgUiEventType.toolCallStart,
|
||||||
case AgUiEventTypeWire.textMessageStart:
|
AgUiEventTypeWire.toolCallArgs: AgUiEventType.toolCallArgs,
|
||||||
return AgUiEventType.textMessageStart;
|
AgUiEventTypeWire.toolCallEnd: AgUiEventType.toolCallEnd,
|
||||||
case AgUiEventTypeWire.textMessageContent:
|
AgUiEventTypeWire.toolCallResult: AgUiEventType.toolCallResult,
|
||||||
return AgUiEventType.textMessageContent;
|
AgUiEventTypeWire.toolCallError: AgUiEventType.toolCallError,
|
||||||
case AgUiEventTypeWire.textMessageEnd:
|
};
|
||||||
return AgUiEventType.textMessageEnd;
|
|
||||||
case AgUiEventTypeWire.toolCallStart:
|
|
||||||
return AgUiEventType.toolCallStart;
|
|
||||||
case AgUiEventTypeWire.toolCallArgs:
|
|
||||||
return AgUiEventType.toolCallArgs;
|
|
||||||
case AgUiEventTypeWire.toolCallEnd:
|
|
||||||
return AgUiEventType.toolCallEnd;
|
|
||||||
case AgUiEventTypeWire.toolCallResult:
|
|
||||||
return AgUiEventType.toolCallResult;
|
|
||||||
case AgUiEventTypeWire.toolCallError:
|
|
||||||
return AgUiEventType.toolCallError;
|
|
||||||
default:
|
|
||||||
return AgUiEventType.unknown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String agUiEventTypeToWire(AgUiEventType type) {
|
const _typeToWireMap = {
|
||||||
switch (type) {
|
AgUiEventType.runStarted: AgUiEventTypeWire.runStarted,
|
||||||
case AgUiEventType.runStarted:
|
AgUiEventType.runFinished: AgUiEventTypeWire.runFinished,
|
||||||
return AgUiEventTypeWire.runStarted;
|
AgUiEventType.runError: AgUiEventTypeWire.runError,
|
||||||
case AgUiEventType.runFinished:
|
AgUiEventType.textMessageStart: AgUiEventTypeWire.textMessageStart,
|
||||||
return AgUiEventTypeWire.runFinished;
|
AgUiEventType.textMessageContent: AgUiEventTypeWire.textMessageContent,
|
||||||
case AgUiEventType.runError:
|
AgUiEventType.textMessageEnd: AgUiEventTypeWire.textMessageEnd,
|
||||||
return AgUiEventTypeWire.runError;
|
AgUiEventType.toolCallStart: AgUiEventTypeWire.toolCallStart,
|
||||||
case AgUiEventType.textMessageStart:
|
AgUiEventType.toolCallArgs: AgUiEventTypeWire.toolCallArgs,
|
||||||
return AgUiEventTypeWire.textMessageStart;
|
AgUiEventType.toolCallEnd: AgUiEventTypeWire.toolCallEnd,
|
||||||
case AgUiEventType.textMessageContent:
|
AgUiEventType.toolCallResult: AgUiEventTypeWire.toolCallResult,
|
||||||
return AgUiEventTypeWire.textMessageContent;
|
AgUiEventType.toolCallError: AgUiEventTypeWire.toolCallError,
|
||||||
case AgUiEventType.textMessageEnd:
|
AgUiEventType.unknown: '',
|
||||||
return AgUiEventTypeWire.textMessageEnd;
|
};
|
||||||
case AgUiEventType.toolCallStart:
|
|
||||||
return AgUiEventTypeWire.toolCallStart;
|
AgUiEventType agUiEventTypeFromWire(String wire) =>
|
||||||
case AgUiEventType.toolCallArgs:
|
_wireToTypeMap[wire] ?? AgUiEventType.unknown;
|
||||||
return AgUiEventTypeWire.toolCallArgs;
|
|
||||||
case AgUiEventType.toolCallEnd:
|
String agUiEventTypeToWire(AgUiEventType type) => _typeToWireMap[type] ?? '';
|
||||||
return AgUiEventTypeWire.toolCallEnd;
|
|
||||||
case AgUiEventType.toolCallResult:
|
|
||||||
return AgUiEventTypeWire.toolCallResult;
|
|
||||||
case AgUiEventType.toolCallError:
|
|
||||||
return AgUiEventTypeWire.toolCallError;
|
|
||||||
case AgUiEventType.unknown:
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class AgUiEvent {
|
class AgUiEvent {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import 'package:json_annotation/json_annotation.dart';
|
|||||||
|
|
||||||
part 'tool_result.g.dart';
|
part 'tool_result.g.dart';
|
||||||
|
|
||||||
|
/// Schema 版本常量
|
||||||
|
const _defaultSchemaVersion = 'v1';
|
||||||
|
|
||||||
/// 工具执行结果(给 AI 的原始数据)
|
/// 工具执行结果(给 AI 的原始数据)
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class ToolResult {
|
class ToolResult {
|
||||||
@@ -31,7 +34,7 @@ class UiCard {
|
|||||||
|
|
||||||
UiCard({
|
UiCard({
|
||||||
required this.cardType,
|
required this.cardType,
|
||||||
this.schemaVersion = 'v1',
|
this.schemaVersion = _defaultSchemaVersion,
|
||||||
required this.data,
|
required this.data,
|
||||||
this.actions,
|
this.actions,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ import '../models/ag_ui_event.dart';
|
|||||||
import '../models/tool_result.dart';
|
import '../models/tool_result.dart';
|
||||||
import '../tools/tool_registry.dart';
|
import '../tools/tool_registry.dart';
|
||||||
|
|
||||||
|
/// Mock ID 前缀常量
|
||||||
|
const _threadIdPrefix = 'thread_';
|
||||||
|
const _runIdPrefix = 'run_';
|
||||||
|
const _toolCallIdPrefix = 'tc_';
|
||||||
|
const _messageIdPrefix = 'msg_';
|
||||||
|
|
||||||
|
/// 流式输出延迟 (毫秒)
|
||||||
|
const _streamChunkDelayMs = 50;
|
||||||
|
|
||||||
|
/// 文本块大小
|
||||||
|
const _textChunkSize = 10;
|
||||||
|
|
||||||
typedef EventCallback = void Function(AgUiEvent event);
|
typedef EventCallback = void Function(AgUiEvent event);
|
||||||
|
|
||||||
class AgUiService {
|
class AgUiService {
|
||||||
@@ -27,8 +39,8 @@ class AgUiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _mockEventStream(String content) async {
|
Future<void> _mockEventStream(String content) async {
|
||||||
final threadId = 'thread_${DateTime.now().millisecondsSinceEpoch}';
|
final threadId = '$_threadIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
||||||
final runId = 'run_${DateTime.now().millisecondsSinceEpoch}';
|
final runId = '$_runIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
onEvent(RunStartedEvent(threadId: threadId, runId: runId));
|
onEvent(RunStartedEvent(threadId: threadId, runId: runId));
|
||||||
|
|
||||||
@@ -58,7 +70,8 @@ class AgUiService {
|
|||||||
String toolName,
|
String toolName,
|
||||||
Map<String, dynamic> args,
|
Map<String, dynamic> args,
|
||||||
) async {
|
) async {
|
||||||
final toolCallId = 'tc_${DateTime.now().millisecondsSinceEpoch}';
|
final toolCallId =
|
||||||
|
'$_toolCallIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
onEvent(ToolCallStartEvent(toolCallId: toolCallId, toolCallName: toolName));
|
onEvent(ToolCallStartEvent(toolCallId: toolCallId, toolCallName: toolName));
|
||||||
|
|
||||||
@@ -82,7 +95,8 @@ class AgUiService {
|
|||||||
ToolRegistry.initialize();
|
ToolRegistry.initialize();
|
||||||
final result = await ToolRegistry.execute(toolName, args);
|
final result = await ToolRegistry.execute(toolName, args);
|
||||||
final ui = _buildUiCard(toolName, result);
|
final ui = _buildUiCard(toolName, result);
|
||||||
final messageId = 'msg_${DateTime.now().millisecondsSinceEpoch}';
|
final messageId =
|
||||||
|
'$_messageIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
onEvent(
|
onEvent(
|
||||||
ToolCallResultEvent(
|
ToolCallResultEvent(
|
||||||
@@ -145,20 +159,20 @@ class AgUiService {
|
|||||||
|
|
||||||
Future<void> _mockTextMessageStream(List<String> replies) async {
|
Future<void> _mockTextMessageStream(List<String> replies) async {
|
||||||
for (final reply in replies) {
|
for (final reply in replies) {
|
||||||
final messageId = 'msg_${DateTime.now().millisecondsSinceEpoch}';
|
final messageId =
|
||||||
|
'$_messageIdPrefix${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
onEvent(TextMessageStartEvent(messageId: messageId, role: 'assistant'));
|
onEvent(TextMessageStartEvent(messageId: messageId, role: 'assistant'));
|
||||||
|
|
||||||
const chunkSize = 10;
|
for (var i = 0; i < reply.length; i += _textChunkSize) {
|
||||||
for (var i = 0; i < reply.length; i += chunkSize) {
|
final end = (i + _textChunkSize < reply.length)
|
||||||
final end = (i + chunkSize < reply.length)
|
? i + _textChunkSize
|
||||||
? i + chunkSize
|
|
||||||
: reply.length;
|
: reply.length;
|
||||||
final chunk = reply.substring(i, end);
|
final chunk = reply.substring(i, end);
|
||||||
|
|
||||||
onEvent(TextMessageContentEvent(messageId: messageId, delta: chunk));
|
onEvent(TextMessageContentEvent(messageId: messageId, delta: chunk));
|
||||||
|
|
||||||
await Future.delayed(const Duration(milliseconds: 50));
|
await Future.delayed(const Duration(milliseconds: _streamChunkDelayMs));
|
||||||
}
|
}
|
||||||
|
|
||||||
onEvent(TextMessageEndEvent(messageId: messageId));
|
onEvent(TextMessageEndEvent(messageId: messageId));
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
typedef ToolHandler =
|
typedef ToolHandler =
|
||||||
Future<Map<String, dynamic>> Function(Map<String, dynamic> args);
|
Future<Map<String, dynamic>> Function(Map<String, dynamic> args);
|
||||||
|
|
||||||
|
/// 工具常量
|
||||||
|
const _toolNameCreateCalendar = 'create_calendar_event';
|
||||||
|
const _defaultTimezone = 'Asia/Shanghai';
|
||||||
|
const _defaultEventColor = '#4F46E5';
|
||||||
|
const _defaultSourceType = 'agentGenerated';
|
||||||
|
const _titleMinLength = 1;
|
||||||
|
const _titleMaxLength = 100;
|
||||||
|
|
||||||
class ToolDefinition {
|
class ToolDefinition {
|
||||||
final String name;
|
final String name;
|
||||||
final String description;
|
final String description;
|
||||||
@@ -22,8 +30,8 @@ class ToolRegistry {
|
|||||||
static void initialize() {
|
static void initialize() {
|
||||||
if (_initialized) return;
|
if (_initialized) return;
|
||||||
|
|
||||||
_tools['create_calendar_event'] = ToolDefinition(
|
_tools[_toolNameCreateCalendar] = ToolDefinition(
|
||||||
name: 'create_calendar_event',
|
name: _toolNameCreateCalendar,
|
||||||
description: '创建一个日历事件或待办事项',
|
description: '创建一个日历事件或待办事项',
|
||||||
parameters: {
|
parameters: {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@@ -31,8 +39,8 @@ class ToolRegistry {
|
|||||||
'title': {
|
'title': {
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': '事件标题',
|
'description': '事件标题',
|
||||||
'minLength': 1,
|
'minLength': _titleMinLength,
|
||||||
'maxLength': 100,
|
'maxLength': _titleMaxLength,
|
||||||
},
|
},
|
||||||
'description': {'type': 'string', 'description': '事件描述'},
|
'description': {'type': 'string', 'description': '事件描述'},
|
||||||
'startAt': {
|
'startAt': {
|
||||||
@@ -45,7 +53,7 @@ class ToolRegistry {
|
|||||||
'format': 'date-time',
|
'format': 'date-time',
|
||||||
'description': '结束时间 (ISO8601)',
|
'description': '结束时间 (ISO8601)',
|
||||||
},
|
},
|
||||||
'timezone': {'type': 'string', 'default': 'Asia/Shanghai'},
|
'timezone': {'type': 'string', 'default': _defaultTimezone},
|
||||||
'location': {'type': 'string'},
|
'location': {'type': 'string'},
|
||||||
'notes': {'type': 'string'},
|
'notes': {'type': 'string'},
|
||||||
},
|
},
|
||||||
@@ -69,10 +77,10 @@ class ToolRegistry {
|
|||||||
'description': args['description'],
|
'description': args['description'],
|
||||||
'startAt': args['startAt'],
|
'startAt': args['startAt'],
|
||||||
'endAt': args['endAt'],
|
'endAt': args['endAt'],
|
||||||
'timezone': args['timezone'] ?? 'Asia/Shanghai',
|
'timezone': args['timezone'] ?? _defaultTimezone,
|
||||||
'location': args['location'],
|
'location': args['location'],
|
||||||
'color': '#4F46E5',
|
'color': _defaultEventColor,
|
||||||
'sourceType': 'agentGenerated',
|
'sourceType': _defaultSourceType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,18 +101,20 @@ class ToolRegistry {
|
|||||||
Map<String, dynamic> args,
|
Map<String, dynamic> args,
|
||||||
) {
|
) {
|
||||||
final tool = _tools[toolName];
|
final tool = _tools[toolName];
|
||||||
if (tool == null)
|
if (tool == null) {
|
||||||
return ToolValidationResult(
|
return ToolValidationResult(
|
||||||
ok: false,
|
ok: false,
|
||||||
error: 'Tool not found: $toolName',
|
error: 'Tool not found: $toolName',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final required = tool.parameters['required'] as List<dynamic>? ?? [];
|
final required = tool.parameters['required'] as List<dynamic>? ?? [];
|
||||||
final missing = <String>[];
|
final missing = <String>[];
|
||||||
for (final field in required) {
|
for (final field in required) {
|
||||||
if (!args.containsKey(field) || args[field] == null)
|
if (!args.containsKey(field) || args[field] == null) {
|
||||||
missing.add(field as String);
|
missing.add(field as String);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (missing.isNotEmpty) {
|
if (missing.isNotEmpty) {
|
||||||
return ToolValidationResult(
|
return ToolValidationResult(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import '../../data/models/ag_ui_event.dart';
|
import '../../data/models/ag_ui_event.dart';
|
||||||
import '../../data/models/chat_list_item.dart';
|
import '../../data/models/chat_list_item.dart';
|
||||||
import '../../data/models/tool_result.dart';
|
import '../../data/models/tool_result.dart';
|
||||||
|
import '../../data/services/ag_ui_service.dart';
|
||||||
|
|
||||||
class ChatState {
|
class ChatState {
|
||||||
final List<ChatListItem> items;
|
final List<ChatListItem> items;
|
||||||
@@ -38,20 +39,12 @@ class ChatState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AgUiService {
|
|
||||||
void Function(AgUiEvent)? onEvent;
|
|
||||||
|
|
||||||
AgUiService({this.onEvent});
|
|
||||||
|
|
||||||
Future<void> sendMessage(String content) async {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChatBloc extends Cubit<ChatState> {
|
class ChatBloc extends Cubit<ChatState> {
|
||||||
final AgUiService _service;
|
final AgUiService _service;
|
||||||
final Map<String, String> _toolCallArgsBuffer = {};
|
final Map<String, String> _toolCallArgsBuffer = {};
|
||||||
|
|
||||||
ChatBloc({AgUiService? service})
|
ChatBloc({AgUiService? service})
|
||||||
: _service = service ?? AgUiService(onEvent: (_) {}),
|
: _service = service ?? AgUiService(),
|
||||||
super(const ChatState()) {
|
super(const ChatState()) {
|
||||||
_service.onEvent = _handleEvent;
|
_service.onEvent = _handleEvent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,19 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:social_app/core/theme/design_tokens.dart';
|
import 'package:social_app/core/theme/design_tokens.dart';
|
||||||
import '../../data/models/tool_result.dart';
|
import '../../data/models/tool_result.dart';
|
||||||
|
|
||||||
|
/// 卡片类型常量
|
||||||
|
const _calendarCardType = 'calendar_card.v1';
|
||||||
|
const _errorCardType = 'error_card.v1';
|
||||||
|
const _aiGeneratedSource = 'ai_generated';
|
||||||
|
const _primaryActionType = 'primary';
|
||||||
|
|
||||||
class UiSchemaRenderer {
|
class UiSchemaRenderer {
|
||||||
static Widget render(UiCard card) {
|
static Widget render(UiCard card) {
|
||||||
switch (card.cardType) {
|
return switch (card.cardType) {
|
||||||
case 'calendar_card.v1':
|
_calendarCardType => _renderCalendarCard(card),
|
||||||
return _renderCalendarCard(card);
|
_errorCardType => _renderErrorCard(card),
|
||||||
case 'error_card.v1':
|
_ => _renderUnknownCard(card),
|
||||||
return _renderErrorCard(card);
|
};
|
||||||
default:
|
|
||||||
return _renderUnknownCard(card);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Widget _renderCalendarCard(UiCard card) {
|
static Widget _renderCalendarCard(UiCard card) {
|
||||||
@@ -19,6 +22,7 @@ class UiSchemaRenderer {
|
|||||||
final color = data.color != null
|
final color = data.color != null
|
||||||
? Color(int.parse(data.color!.replaceFirst('#', '0xFF')))
|
? Color(int.parse(data.color!.replaceFirst('#', '0xFF')))
|
||||||
: AppColors.blue500;
|
: AppColors.blue500;
|
||||||
|
final isAiGenerated = data.sourceType == _aiGeneratedSource;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -44,23 +48,10 @@ class UiSchemaRenderer {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (data.sourceType == 'ai_generated')
|
if (isAiGenerated) ...[
|
||||||
Container(
|
_buildAiTag(),
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: AppSpacing.sm,
|
|
||||||
vertical: AppSpacing.xs,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.messageTagBg,
|
|
||||||
borderRadius: BorderRadius.circular(AppRadius.sm),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'AI生成',
|
|
||||||
style: TextStyle(fontSize: 10, color: AppColors.blue600),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (data.sourceType == 'ai_generated')
|
|
||||||
SizedBox(height: AppSpacing.sm),
|
SizedBox(height: AppSpacing.sm),
|
||||||
|
],
|
||||||
Text(
|
Text(
|
||||||
_formatTime(data.startAt, data.endAt),
|
_formatTime(data.startAt, data.endAt),
|
||||||
style: TextStyle(fontSize: 12, color: AppColors.slate500),
|
style: TextStyle(fontSize: 12, color: AppColors.slate500),
|
||||||
@@ -83,32 +74,11 @@ class UiSchemaRenderer {
|
|||||||
],
|
],
|
||||||
if (data.location != null) ...[
|
if (data.location != null) ...[
|
||||||
SizedBox(height: AppSpacing.sm),
|
SizedBox(height: AppSpacing.sm),
|
||||||
Row(
|
_buildLocation(data.location!),
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.location_on_outlined,
|
|
||||||
size: 16,
|
|
||||||
color: AppColors.slate500,
|
|
||||||
),
|
|
||||||
SizedBox(width: AppSpacing.xs),
|
|
||||||
Text(
|
|
||||||
data.location!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppColors.slate500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
if (card.actions != null && card.actions!.isNotEmpty) ...[
|
if (card.actions != null && card.actions!.isNotEmpty) ...[
|
||||||
SizedBox(height: AppSpacing.md),
|
SizedBox(height: AppSpacing.md),
|
||||||
Wrap(
|
_buildActions(card.actions!),
|
||||||
spacing: AppSpacing.sm,
|
|
||||||
children: card.actions!
|
|
||||||
.map((action) => _buildActionButton(action))
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -118,8 +88,45 @@ class UiSchemaRenderer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Widget _buildAiTag() {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: AppSpacing.sm,
|
||||||
|
vertical: AppSpacing.xs,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.messageTagBg,
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.sm),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'AI生成',
|
||||||
|
style: TextStyle(fontSize: 10, color: AppColors.blue600),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _buildLocation(String location) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.location_on_outlined, size: 16, color: AppColors.slate500),
|
||||||
|
SizedBox(width: AppSpacing.xs),
|
||||||
|
Text(
|
||||||
|
location,
|
||||||
|
style: TextStyle(fontSize: 12, color: AppColors.slate500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _buildActions(List<CardAction> actions) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: AppSpacing.sm,
|
||||||
|
children: actions.map((action) => _buildActionButton(action)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Widget _buildActionButton(CardAction action) {
|
static Widget _buildActionButton(CardAction action) {
|
||||||
final isPrimary = action.type == 'primary';
|
final isPrimary = action.type == _primaryActionType;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _handleAction(action),
|
onTap: () => _handleAction(action),
|
||||||
child: Container(
|
child: Container(
|
||||||
|
|||||||
@@ -10,6 +10,25 @@ import '../../../../shared/widgets/toast/toast.dart';
|
|||||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||||
import 'home_sheet.dart';
|
import 'home_sheet.dart';
|
||||||
|
|
||||||
|
/// 布局常量
|
||||||
|
const _headerHeight = 60.0;
|
||||||
|
const _defaultPadding = 20.0;
|
||||||
|
const _itemSpacing = 16.0;
|
||||||
|
const _inputPadding = 16.0;
|
||||||
|
const _iconSize = 24.0;
|
||||||
|
const _avatarSize = 32.0;
|
||||||
|
const _botIconSize = 18.0;
|
||||||
|
const _messagePaddingH = 13.0;
|
||||||
|
const _messagePaddingV = 9.0;
|
||||||
|
const _cornerRadius = 12.0;
|
||||||
|
const _inputMinHeight = 48.0;
|
||||||
|
const _inputRadius = 24.0;
|
||||||
|
const _scrollDurationMs = 300;
|
||||||
|
|
||||||
|
/// 颜色常量
|
||||||
|
const _chatBgColor = Color(0xFFF8FAFC);
|
||||||
|
const _userBubbleColor = Color(0xFFEAF1FB);
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
const HomeScreen({super.key});
|
||||||
|
|
||||||
@@ -53,7 +72,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF8FAFC),
|
backgroundColor: _chatBgColor,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -71,16 +90,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
Widget _buildHeader(BuildContext context) {
|
Widget _buildHeader(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 60,
|
height: _headerHeight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: _defaultPadding),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
LucideIcons.settings,
|
LucideIcons.settings,
|
||||||
size: 24,
|
size: _iconSize,
|
||||||
color: AppColors.slate900,
|
color: AppColors.slate900,
|
||||||
),
|
),
|
||||||
onPressed: () => context.push('/settings'),
|
onPressed: () => context.push('/settings'),
|
||||||
@@ -90,16 +109,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
LucideIcons.calendar,
|
LucideIcons.calendar,
|
||||||
size: 24,
|
size: _iconSize,
|
||||||
color: AppColors.slate900,
|
color: AppColors.slate900,
|
||||||
),
|
),
|
||||||
onPressed: () => context.push('/calendar/dayweek?from=home'),
|
onPressed: () => context.push('/calendar/dayweek?from=home'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: _itemSpacing),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
LucideIcons.messageSquare,
|
LucideIcons.messageSquare,
|
||||||
size: 24,
|
size: _iconSize,
|
||||||
color: AppColors.slate900,
|
color: AppColors.slate900,
|
||||||
),
|
),
|
||||||
onPressed: () => context.push('/messages/invites'),
|
onPressed: () => context.push('/messages/invites'),
|
||||||
@@ -126,7 +145,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
_scrollController.animateTo(
|
_scrollController.animateTo(
|
||||||
_scrollController.position.maxScrollExtent,
|
_scrollController.position.maxScrollExtent,
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: _scrollDurationMs),
|
||||||
curve: Curves.easeOut,
|
curve: Curves.easeOut,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -134,12 +153,12 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(_defaultPadding),
|
||||||
itemCount: state.items.length,
|
itemCount: state.items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = state.items[index];
|
final item = state.items[index];
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: _itemSpacing),
|
||||||
child: _buildChatItem(item),
|
child: _buildChatItem(item),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -167,15 +186,15 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
children: [
|
children: [
|
||||||
if (!isUser) ...[
|
if (!isUser) ...[
|
||||||
Container(
|
Container(
|
||||||
width: 32,
|
width: _avatarSize,
|
||||||
height: 32,
|
height: _avatarSize,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.blue100,
|
color: AppColors.blue100,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
LucideIcons.bot,
|
LucideIcons.bot,
|
||||||
size: 18,
|
size: _botIconSize,
|
||||||
color: AppColors.blue600,
|
color: AppColors.blue600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -183,14 +202,17 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
],
|
],
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 9),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: _messagePaddingH,
|
||||||
|
vertical: _messagePaddingV,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isUser ? const Color(0xFFEAF1FB) : AppColors.white,
|
color: isUser ? _userBubbleColor : AppColors.white,
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: const Radius.circular(12),
|
topLeft: const Radius.circular(_cornerRadius),
|
||||||
topRight: const Radius.circular(12),
|
topRight: const Radius.circular(_cornerRadius),
|
||||||
bottomLeft: Radius.circular(isUser ? 12 : 0),
|
bottomLeft: Radius.circular(isUser ? _cornerRadius : 0),
|
||||||
bottomRight: Radius.circular(isUser ? 0 : 12),
|
bottomRight: Radius.circular(isUser ? 0 : _cornerRadius),
|
||||||
),
|
),
|
||||||
border: isUser ? null : Border.all(color: AppColors.slate300),
|
border: isUser ? null : Border.all(color: AppColors.slate300),
|
||||||
),
|
),
|
||||||
@@ -207,32 +229,28 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildToolCallItem(ToolCallItem item) {
|
Widget _buildToolCallItem(ToolCallItem item) {
|
||||||
String statusText;
|
final (statusText, statusColor, statusIcon) = switch (item.status) {
|
||||||
Color statusColor;
|
ToolCallStatus.pending => (
|
||||||
IconData statusIcon;
|
'准备中...',
|
||||||
|
AppColors.slate500,
|
||||||
switch (item.status) {
|
LucideIcons.clock,
|
||||||
case ToolCallStatus.pending:
|
),
|
||||||
statusText = '准备中...';
|
ToolCallStatus.executing => (
|
||||||
statusColor = AppColors.slate500;
|
'执行中...',
|
||||||
statusIcon = LucideIcons.clock;
|
AppColors.blue600,
|
||||||
break;
|
LucideIcons.loader,
|
||||||
case ToolCallStatus.executing:
|
),
|
||||||
statusText = '执行中...';
|
ToolCallStatus.error => (
|
||||||
statusColor = AppColors.blue600;
|
item.errorMessage ?? '执行失败',
|
||||||
statusIcon = LucideIcons.loader;
|
AppColors.red600,
|
||||||
break;
|
LucideIcons.alertCircle,
|
||||||
case ToolCallStatus.error:
|
),
|
||||||
statusText = item.errorMessage ?? '执行失败';
|
ToolCallStatus.completed => (
|
||||||
statusColor = AppColors.red600;
|
'已完成',
|
||||||
statusIcon = LucideIcons.alertCircle;
|
AppColors.emerald600,
|
||||||
break;
|
LucideIcons.checkCircle,
|
||||||
case ToolCallStatus.completed:
|
),
|
||||||
statusText = '已完成';
|
};
|
||||||
statusColor = AppColors.emerald600;
|
|
||||||
statusIcon = LucideIcons.checkCircle;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
@@ -267,8 +285,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
Widget _buildInputContainer(BuildContext context) {
|
Widget _buildInputContainer(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(_inputPadding),
|
||||||
color: const Color(0xFFF8FAFC),
|
color: _chatBgColor,
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -292,11 +310,11 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(minHeight: 48),
|
constraints: const BoxConstraints(minHeight: _inputMinHeight),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(_inputRadius),
|
||||||
border: Border.all(color: AppColors.slate300),
|
border: Border.all(color: AppColors.slate300),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -327,7 +345,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
onTap: _hasMessage ? () => _sendMessage(context) : null,
|
onTap: _hasMessage ? () => _sendMessage(context) : null,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
_hasMessage ? LucideIcons.send : LucideIcons.mic,
|
_hasMessage ? LucideIcons.send : LucideIcons.mic,
|
||||||
size: 24,
|
size: _iconSize,
|
||||||
color: _hasMessage
|
color: _hasMessage
|
||||||
? AppColors.blue600
|
? AppColors.blue600
|
||||||
: AppColors.slate500,
|
: AppColors.slate500,
|
||||||
|
|||||||
@@ -2,14 +2,22 @@ import 'package:bloc_test/bloc_test.dart';
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:social_app/features/chat/data/models/ag_ui_event.dart';
|
import 'package:social_app/features/chat/data/models/ag_ui_event.dart';
|
||||||
import 'package:social_app/features/chat/data/models/chat_list_item.dart';
|
import 'package:social_app/features/chat/data/models/chat_list_item.dart';
|
||||||
|
import 'package:social_app/features/chat/data/services/ag_ui_service.dart';
|
||||||
import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart';
|
import 'package:social_app/features/chat/presentation/bloc/chat_bloc.dart';
|
||||||
|
|
||||||
|
class MockAgUiService extends AgUiService {
|
||||||
|
MockAgUiService() : super(onEvent: (_) {});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> sendMessage(String content) async {}
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late ChatBloc chatBloc;
|
late ChatBloc chatBloc;
|
||||||
late AgUiService service;
|
late AgUiService service;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
service = AgUiService();
|
service = MockAgUiService();
|
||||||
chatBloc = ChatBloc(service: service);
|
chatBloc = ChatBloc(service: service);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,7 +57,7 @@ void main() {
|
|||||||
build: () => chatBloc,
|
build: () => chatBloc,
|
||||||
act: (bloc) {
|
act: (bloc) {
|
||||||
bloc.emit(chatBloc.state.copyWith(isLoading: true));
|
bloc.emit(chatBloc.state.copyWith(isLoading: true));
|
||||||
service.onEvent!(
|
service.onEvent(
|
||||||
TextMessageStartEvent(messageId: 'msg_1', role: 'assistant'),
|
TextMessageStartEvent(messageId: 'msg_1', role: 'assistant'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -86,7 +94,7 @@ void main() {
|
|||||||
currentMessageId: 'msg_1',
|
currentMessageId: 'msg_1',
|
||||||
),
|
),
|
||||||
act: (bloc) {
|
act: (bloc) {
|
||||||
service.onEvent!(
|
service.onEvent(
|
||||||
TextMessageContentEvent(messageId: 'msg_1', delta: 'Hello'),
|
TextMessageContentEvent(messageId: 'msg_1', delta: 'Hello'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -115,7 +123,7 @@ void main() {
|
|||||||
currentMessageId: 'msg_1',
|
currentMessageId: 'msg_1',
|
||||||
),
|
),
|
||||||
act: (bloc) {
|
act: (bloc) {
|
||||||
service.onEvent!(TextMessageEndEvent(messageId: 'msg_1'));
|
service.onEvent(TextMessageEndEvent(messageId: 'msg_1'));
|
||||||
},
|
},
|
||||||
expect: () => [
|
expect: () => [
|
||||||
isA<ChatState>()
|
isA<ChatState>()
|
||||||
@@ -132,7 +140,7 @@ void main() {
|
|||||||
'runStarted sets isLoading to true',
|
'runStarted sets isLoading to true',
|
||||||
build: () => chatBloc,
|
build: () => chatBloc,
|
||||||
act: (bloc) {
|
act: (bloc) {
|
||||||
service.onEvent!(RunStartedEvent(threadId: 't1', runId: 'r1'));
|
service.onEvent(RunStartedEvent(threadId: 't1', runId: 'r1'));
|
||||||
},
|
},
|
||||||
expect: () => [
|
expect: () => [
|
||||||
isA<ChatState>()
|
isA<ChatState>()
|
||||||
@@ -146,7 +154,7 @@ void main() {
|
|||||||
build: () => chatBloc,
|
build: () => chatBloc,
|
||||||
seed: () => const ChatState(isLoading: true),
|
seed: () => const ChatState(isLoading: true),
|
||||||
act: (bloc) {
|
act: (bloc) {
|
||||||
service.onEvent!(RunFinishedEvent(threadId: 't1', runId: 'r1'));
|
service.onEvent(RunFinishedEvent(threadId: 't1', runId: 'r1'));
|
||||||
},
|
},
|
||||||
expect: () => [
|
expect: () => [
|
||||||
isA<ChatState>()
|
isA<ChatState>()
|
||||||
@@ -160,7 +168,7 @@ void main() {
|
|||||||
build: () => chatBloc,
|
build: () => chatBloc,
|
||||||
seed: () => const ChatState(isLoading: true),
|
seed: () => const ChatState(isLoading: true),
|
||||||
act: (bloc) {
|
act: (bloc) {
|
||||||
service.onEvent!(
|
service.onEvent(
|
||||||
RunErrorEvent(message: 'Something went wrong', code: 'ERR'),
|
RunErrorEvent(message: 'Something went wrong', code: 'ERR'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -183,7 +191,7 @@ void main() {
|
|||||||
'toolCallStart adds ToolCallItem',
|
'toolCallStart adds ToolCallItem',
|
||||||
build: () => chatBloc,
|
build: () => chatBloc,
|
||||||
act: (bloc) {
|
act: (bloc) {
|
||||||
service.onEvent!(
|
service.onEvent(
|
||||||
ToolCallStartEvent(
|
ToolCallStartEvent(
|
||||||
toolCallId: 'tc_1',
|
toolCallId: 'tc_1',
|
||||||
toolCallName: 'create_calendar_event',
|
toolCallName: 'create_calendar_event',
|
||||||
|
|||||||
Reference in New Issue
Block a user