feat: 优化前端 UI 组件与交互体验

- 优化日历、待办、消息等页面交互
- 更新 ChatBloc 与 UI Schema 渲染
- 优化联系人、首页、设置页面体验
This commit is contained in:
qzl
2026-03-16 16:11:28 +08:00
parent a75c868bca
commit 4b92772535
18 changed files with 1591 additions and 1780 deletions
@@ -1,26 +1,15 @@
import 'dart:convert';
import 'package:json_annotation/json_annotation.dart';
import 'tool_result.dart';
part 'ag_ui_event.g.dart';
class AgUiEventTypeWire {
static const runStarted = 'RUN_STARTED';
static const runFinished = 'RUN_FINISHED';
static const runError = 'RUN_ERROR';
static const stepStarted = 'STEP_STARTED';
static const stepFinished = 'STEP_FINISHED';
static const textMessageStart = 'TEXT_MESSAGE_START';
static const textMessageContent = 'TEXT_MESSAGE_CONTENT';
static const textMessageEnd = 'TEXT_MESSAGE_END';
static const toolCallStart = 'TOOL_CALL_START';
static const toolCallArgs = 'TOOL_CALL_ARGS';
static const toolCallEnd = 'TOOL_CALL_END';
static const toolCallResult = 'TOOL_CALL_RESULT';
static const toolCallError = 'TOOL_CALL_ERROR';
static const stateSnapshot = 'STATE_SNAPSHOT';
static const messagesSnapshot = 'MESSAGES_SNAPSHOT';
}
enum AgUiEventType {
@@ -29,55 +18,41 @@ enum AgUiEventType {
runError,
stepStarted,
stepFinished,
textMessageStart,
textMessageContent,
textMessageEnd,
toolCallStart,
toolCallArgs,
toolCallEnd,
toolCallResult,
toolCallError,
stateSnapshot,
messagesSnapshot,
unknown,
}
// wire 类型到枚举的映射
const _wireToTypeMap = {
AgUiEventTypeWire.runStarted: AgUiEventType.runStarted,
AgUiEventTypeWire.runFinished: AgUiEventType.runFinished,
AgUiEventTypeWire.runError: AgUiEventType.runError,
AgUiEventTypeWire.stepStarted: AgUiEventType.stepStarted,
AgUiEventTypeWire.stepFinished: AgUiEventType.stepFinished,
AgUiEventTypeWire.textMessageStart: AgUiEventType.textMessageStart,
AgUiEventTypeWire.textMessageContent: AgUiEventType.textMessageContent,
AgUiEventTypeWire.textMessageEnd: AgUiEventType.textMessageEnd,
AgUiEventTypeWire.toolCallStart: AgUiEventType.toolCallStart,
AgUiEventTypeWire.toolCallArgs: AgUiEventType.toolCallArgs,
AgUiEventTypeWire.toolCallEnd: AgUiEventType.toolCallEnd,
AgUiEventTypeWire.toolCallResult: AgUiEventType.toolCallResult,
AgUiEventTypeWire.toolCallError: AgUiEventType.toolCallError,
AgUiEventTypeWire.stateSnapshot: AgUiEventType.stateSnapshot,
AgUiEventTypeWire.messagesSnapshot: AgUiEventType.messagesSnapshot,
};
// 枚举到 wire 类型的映射
const _typeToWireMap = {
AgUiEventType.runStarted: AgUiEventTypeWire.runStarted,
AgUiEventType.runFinished: AgUiEventTypeWire.runFinished,
AgUiEventType.runError: AgUiEventTypeWire.runError,
AgUiEventType.stepStarted: AgUiEventTypeWire.stepStarted,
AgUiEventType.stepFinished: AgUiEventTypeWire.stepFinished,
AgUiEventType.textMessageStart: AgUiEventTypeWire.textMessageStart,
AgUiEventType.textMessageContent: AgUiEventTypeWire.textMessageContent,
AgUiEventType.textMessageEnd: AgUiEventTypeWire.textMessageEnd,
AgUiEventType.toolCallStart: AgUiEventTypeWire.toolCallStart,
AgUiEventType.toolCallArgs: AgUiEventTypeWire.toolCallArgs,
AgUiEventType.toolCallEnd: AgUiEventTypeWire.toolCallEnd,
AgUiEventType.toolCallResult: AgUiEventTypeWire.toolCallResult,
AgUiEventType.toolCallError: AgUiEventTypeWire.toolCallError,
AgUiEventType.stateSnapshot: AgUiEventTypeWire.stateSnapshot,
AgUiEventType.messagesSnapshot: AgUiEventTypeWire.messagesSnapshot,
AgUiEventType.unknown: '',
};
@@ -86,383 +61,310 @@ AgUiEventType agUiEventTypeFromWire(String wire) =>
String agUiEventTypeToWire(AgUiEventType type) => _typeToWireMap[type] ?? '';
// 类型到工厂函数的映射,用于简化 fromJson
final _typeToFactory = {
AgUiEventType.runStarted: RunStartedEvent.fromJson,
AgUiEventType.runFinished: RunFinishedEvent.fromJson,
AgUiEventType.runError: RunErrorEvent.fromJson,
AgUiEventType.stepStarted: StepStartedEvent.fromJson,
AgUiEventType.stepFinished: StepFinishedEvent.fromJson,
AgUiEventType.textMessageStart: TextMessageStartEvent.fromJson,
AgUiEventType.textMessageContent: TextMessageContentEvent.fromJson,
AgUiEventType.textMessageEnd: TextMessageEndEvent.fromJson,
AgUiEventType.toolCallStart: ToolCallStartEvent.fromJson,
AgUiEventType.toolCallArgs: ToolCallArgsEvent.fromJson,
AgUiEventType.toolCallEnd: ToolCallEndEvent.fromJson,
AgUiEventType.toolCallResult: ToolCallResultEvent.fromJson,
AgUiEventType.toolCallError: ToolCallErrorEvent.fromJson,
AgUiEventType.stateSnapshot: StateSnapshotEvent.fromJson,
AgUiEventType.messagesSnapshot: MessagesSnapshotEvent.fromJson,
AgUiEventType.unknown: UnknownAgUiEvent.fromJson,
};
abstract class AgUiEvent {
const AgUiEvent({required this.type});
@JsonSerializable(createFactory: false)
class AgUiEvent {
final AgUiEventType type;
AgUiEvent({required this.type});
factory AgUiEvent.fromJson(Map<String, dynamic> json) {
final typeStr = json['type'] as String? ?? '';
final type = agUiEventTypeFromWire(typeStr);
return _typeToFactory[type]?.call(json) ?? UnknownAgUiEvent.fromJson(json);
final wireType = json['type'];
final type = wireType is String
? agUiEventTypeFromWire(wireType)
: AgUiEventType.unknown;
return switch (type) {
AgUiEventType.runStarted => RunStartedEvent.fromJson(json),
AgUiEventType.runFinished => RunFinishedEvent.fromJson(json),
AgUiEventType.runError => RunErrorEvent.fromJson(json),
AgUiEventType.stepStarted => StepStartedEvent.fromJson(json),
AgUiEventType.stepFinished => StepFinishedEvent.fromJson(json),
AgUiEventType.textMessageEnd => TextMessageEndEvent.fromJson(json),
AgUiEventType.toolCallStart => ToolCallStartEvent.fromJson(json),
AgUiEventType.toolCallArgs => ToolCallArgsEvent.fromJson(json),
AgUiEventType.toolCallEnd => ToolCallEndEvent.fromJson(json),
AgUiEventType.toolCallResult => ToolCallResultEvent.fromJson(json),
AgUiEventType.toolCallError => ToolCallErrorEvent.fromJson(json),
AgUiEventType.unknown => UnknownAgUiEvent(rawJson: json),
};
}
Map<String, dynamic> toJson() => _$AgUiEventToJson(this);
}
@JsonSerializable(createFactory: false, createToJson: false)
class UnknownAgUiEvent extends AgUiEvent {
final Map<String, dynamic> rawJson;
UnknownAgUiEvent({required this.rawJson})
const UnknownAgUiEvent({required this.rawJson})
: super(type: AgUiEventType.unknown);
factory UnknownAgUiEvent.fromJson(Map<String, dynamic> json) =>
UnknownAgUiEvent(rawJson: json);
@override
Map<String, dynamic> toJson() => rawJson;
final Map<String, dynamic> rawJson;
}
@JsonSerializable()
class RunStartedEvent extends AgUiEvent {
final String threadId;
final String runId;
RunStartedEvent({required this.threadId, required this.runId})
: super(type: AgUiEventType.runStarted);
factory RunStartedEvent.fromJson(Map<String, dynamic> json) =>
_$RunStartedEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$RunStartedEventToJson(this);
}
@JsonSerializable()
class RunFinishedEvent extends AgUiEvent {
final String threadId;
final String runId;
factory RunStartedEvent.fromJson(Map<String, dynamic> json) =>
RunStartedEvent(
threadId: _asString(json['threadId']),
runId: _asString(json['runId']),
);
}
class RunFinishedEvent extends AgUiEvent {
RunFinishedEvent({required this.threadId, required this.runId})
: super(type: AgUiEventType.runFinished);
factory RunFinishedEvent.fromJson(Map<String, dynamic> json) =>
_$RunFinishedEventFromJson(json);
final String threadId;
final String runId;
@override
Map<String, dynamic> toJson() => _$RunFinishedEventToJson(this);
factory RunFinishedEvent.fromJson(Map<String, dynamic> json) =>
RunFinishedEvent(
threadId: _asString(json['threadId']),
runId: _asString(json['runId']),
);
}
@JsonSerializable()
class RunErrorEvent extends AgUiEvent {
final String message;
final String? code;
RunErrorEvent({required this.message, this.code})
: super(type: AgUiEventType.runError);
factory RunErrorEvent.fromJson(Map<String, dynamic> json) =>
_$RunErrorEventFromJson(json);
final String message;
final String? code;
@override
Map<String, dynamic> toJson() => _$RunErrorEventToJson(this);
factory RunErrorEvent.fromJson(Map<String, dynamic> json) => RunErrorEvent(
message: _asString(json['message'], fallback: 'Unknown error'),
code: json['code'] as String?,
);
}
@JsonSerializable()
class StepStartedEvent extends AgUiEvent {
final String stepName;
StepStartedEvent({required this.stepName})
: super(type: AgUiEventType.stepStarted);
factory StepStartedEvent.fromJson(Map<String, dynamic> json) =>
_$StepStartedEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$StepStartedEventToJson(this);
}
@JsonSerializable()
class StepFinishedEvent extends AgUiEvent {
final String stepName;
factory StepStartedEvent.fromJson(Map<String, dynamic> json) =>
StepStartedEvent(stepName: _asString(json['stepName']));
}
class StepFinishedEvent extends AgUiEvent {
StepFinishedEvent({required this.stepName})
: super(type: AgUiEventType.stepFinished);
final String stepName;
factory StepFinishedEvent.fromJson(Map<String, dynamic> json) =>
_$StepFinishedEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$StepFinishedEventToJson(this);
StepFinishedEvent(stepName: _asString(json['stepName']));
}
@JsonSerializable()
class TextMessageStartEvent extends AgUiEvent {
final String messageId;
final String role;
TextMessageStartEvent({required this.messageId, required this.role})
: super(type: AgUiEventType.textMessageStart);
factory TextMessageStartEvent.fromJson(Map<String, dynamic> json) =>
_$TextMessageStartEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$TextMessageStartEventToJson(this);
}
@JsonSerializable()
class TextMessageContentEvent extends AgUiEvent {
final String messageId;
final String delta;
TextMessageContentEvent({required this.messageId, required this.delta})
: super(type: AgUiEventType.textMessageContent);
factory TextMessageContentEvent.fromJson(Map<String, dynamic> json) =>
_$TextMessageContentEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$TextMessageContentEventToJson(this);
}
@JsonSerializable()
class TextMessageEndEvent extends AgUiEvent {
final String messageId;
TextMessageEndEvent({
required this.messageId,
required this.answer,
required this.role,
required this.status,
required this.uiSchema,
}) : super(type: AgUiEventType.textMessageEnd);
TextMessageEndEvent({required this.messageId})
: super(type: AgUiEventType.textMessageEnd);
final String messageId;
final String answer;
final String role;
final String status;
final Map<String, dynamic>? uiSchema;
factory TextMessageEndEvent.fromJson(Map<String, dynamic> json) =>
_$TextMessageEndEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$TextMessageEndEventToJson(this);
TextMessageEndEvent(
messageId: _asString(json['messageId']),
answer: _asString(json['answer']),
role: _asString(json['role'], fallback: 'assistant'),
status: _asString(json['status'], fallback: 'success'),
uiSchema: _asMap(json['ui_schema']) ?? _asMap(json['uiSchema']),
);
}
@JsonSerializable()
class ToolCallStartEvent extends AgUiEvent {
ToolCallStartEvent({required this.toolCallId, required this.toolCallName})
: super(type: AgUiEventType.toolCallStart);
final String toolCallId;
final String toolCallName;
final String? parentMessageId;
ToolCallStartEvent({
required this.toolCallId,
required this.toolCallName,
this.parentMessageId,
}) : super(type: AgUiEventType.toolCallStart);
factory ToolCallStartEvent.fromJson(Map<String, dynamic> json) =>
_$ToolCallStartEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$ToolCallStartEventToJson(this);
ToolCallStartEvent(
toolCallId: _asString(json['toolCallId']),
toolCallName: _asString(json['toolCallName']),
);
}
@JsonSerializable()
class ToolCallArgsEvent extends AgUiEvent {
final String toolCallId;
final String delta;
ToolCallArgsEvent({required this.toolCallId, required this.delta})
ToolCallArgsEvent({required this.toolCallId, required this.args})
: super(type: AgUiEventType.toolCallArgs);
factory ToolCallArgsEvent.fromJson(Map<String, dynamic> json) =>
_$ToolCallArgsEventFromJson(json);
final String toolCallId;
final Map<String, dynamic> args;
@override
Map<String, dynamic> toJson() => _$ToolCallArgsEventToJson(this);
factory ToolCallArgsEvent.fromJson(Map<String, dynamic> json) =>
ToolCallArgsEvent(
toolCallId: _asString(json['toolCallId']),
args: _asMap(json['args']) ?? const {},
);
}
@JsonSerializable()
class ToolCallEndEvent extends AgUiEvent {
final String toolCallId;
ToolCallEndEvent({required this.toolCallId})
: super(type: AgUiEventType.toolCallEnd);
factory ToolCallEndEvent.fromJson(Map<String, dynamic> json) =>
_$ToolCallEndEventFromJson(json);
final String toolCallId;
@override
Map<String, dynamic> toJson() => _$ToolCallEndEventToJson(this);
factory ToolCallEndEvent.fromJson(Map<String, dynamic> json) =>
ToolCallEndEvent(toolCallId: _asString(json['toolCallId']));
}
@JsonSerializable(createFactory: false, createToJson: false)
class ToolCallResultEvent extends AgUiEvent {
final String messageId;
final String toolCallId;
final String content;
ToolCallResultEvent({
required this.messageId,
required this.toolCallId,
required this.content,
required this.toolName,
required this.resultSummary,
required this.status,
required this.uiSchema,
}) : super(type: AgUiEventType.toolCallResult);
Map<String, dynamic> get payload {
try {
final decoded = jsonDecode(content);
if (decoded is Map<String, dynamic>) {
return decoded;
}
} catch (_) {}
return {'content': content};
}
final String messageId;
final String toolCallId;
final String toolName;
final String resultSummary;
final String status;
final Map<String, dynamic>? uiSchema;
Map<String, dynamic> get result {
final rawResult = payload['result'];
if (rawResult is Map<String, dynamic>) {
return rawResult;
}
return payload;
}
UiCard? get ui {
final rawUi = payload['ui'];
if (rawUi is Map<String, dynamic>) {
return UiCard.fromJson(rawUi);
}
final rawResult = payload['result'];
if (rawResult is Map<String, dynamic>) {
final type = rawResult['type'];
final data = rawResult['data'];
if (type is String && data is Map<String, dynamic>) {
return UiCard.fromJson(rawResult);
}
}
return null;
}
factory ToolCallResultEvent.fromJson(Map<String, dynamic> json) {
final rawContent = json['content'];
final hasStructuredFields =
json['ui'] != null || json['result'] != null || json['error'] != null;
final content = switch (rawContent) {
String value when value.trim().startsWith('{') => value,
String value when value.trim().startsWith('[') => value,
String value when hasStructuredFields => jsonEncode({
'toolName': json['toolName'],
'result': json['result'],
'error': json['error'],
'ui': json['ui'],
'content': value,
}),
String value => value,
_ => jsonEncode({
'toolName': json['toolName'],
'result': json['result'],
'error': json['error'],
'ui': json['ui'],
'content': json['content'],
}),
};
final toolCallId =
json['toolCallId'] as String? ?? json['callId'] as String? ?? '';
final messageId = json['messageId'] as String? ?? 'tool-result-$toolCallId';
return ToolCallResultEvent(
messageId: messageId,
toolCallId: toolCallId,
content: content,
);
}
@override
Map<String, dynamic> toJson() => {
'type': agUiEventTypeToWire(type),
'messageId': messageId,
'toolCallId': toolCallId,
'content': content,
};
factory ToolCallResultEvent.fromJson(Map<String, dynamic> json) =>
ToolCallResultEvent(
messageId: _asString(
json['messageId'],
fallback: 'tool-${_asString(json['tool_call_id'])}',
),
toolCallId: _asString(json['tool_call_id'] ?? json['toolCallId']),
toolName: _asString(json['tool_name'] ?? json['toolName']),
resultSummary: _asString(
json['result_summary'] ?? json['resultSummary'],
),
status: _asString(json['status'], fallback: 'success'),
uiSchema: _asMap(json['ui_schema']) ?? _asMap(json['uiSchema']),
);
}
@JsonSerializable()
class ToolCallErrorEvent extends AgUiEvent {
ToolCallErrorEvent({required this.toolCallId, required this.error, this.code})
: super(type: AgUiEventType.toolCallError);
final String toolCallId;
final String error;
final String? code;
ToolCallErrorEvent({required this.toolCallId, required this.error, this.code})
: super(type: AgUiEventType.toolCallError);
factory ToolCallErrorEvent.fromJson(Map<String, dynamic> json) =>
_$ToolCallErrorEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$ToolCallErrorEventToJson(this);
ToolCallErrorEvent(
toolCallId: _asString(json['toolCallId']),
error: _asString(json['error'], fallback: 'Tool call failed'),
code: json['code'] as String?,
);
}
@JsonSerializable(createFactory: false, createToJson: false)
class StateSnapshotEvent extends AgUiEvent {
final Map<String, dynamic> snapshot;
StateSnapshotEvent({required this.snapshot})
: super(type: AgUiEventType.stateSnapshot);
factory StateSnapshotEvent.fromJson(Map<String, dynamic> json) {
final rawSnapshot = json['snapshot'];
return StateSnapshotEvent(
snapshot: rawSnapshot is Map<String, dynamic>
? rawSnapshot
: <String, dynamic>{},
);
}
@override
Map<String, dynamic> toJson() => {
'type': agUiEventTypeToWire(type),
'snapshot': snapshot,
};
}
@JsonSerializable()
class MessagesSnapshotEvent extends AgUiEvent {
final List<SnapshotMessage> messages;
MessagesSnapshotEvent({required this.messages})
: super(type: AgUiEventType.messagesSnapshot);
factory MessagesSnapshotEvent.fromJson(Map<String, dynamic> json) =>
_$MessagesSnapshotEventFromJson(json);
@override
Map<String, dynamic> toJson() => _$MessagesSnapshotEventToJson(this);
}
@JsonSerializable()
class SnapshotMessage {
final String id;
final String role;
final String? content;
final String? toolCallId;
final UiCard? ui;
final DateTime? timestamp;
final List<Map<String, dynamic>>? attachments;
SnapshotMessage({
required this.id,
required this.role,
this.content,
this.toolCallId,
this.ui,
this.timestamp,
this.attachments,
class HistorySnapshot {
const HistorySnapshot({
required this.scope,
required this.threadId,
required this.day,
required this.hasMore,
required this.messages,
});
factory SnapshotMessage.fromJson(Map<String, dynamic> json) =>
_$SnapshotMessageFromJson(json);
final String scope;
final String? threadId;
final String? day;
final bool hasMore;
final List<HistoryMessage> messages;
Map<String, dynamic> toJson() => _$SnapshotMessageToJson(this);
factory HistorySnapshot.fromJson(Map<String, dynamic> json) {
final rawMessages = json['messages'];
final messages = rawMessages is List
? rawMessages
.whereType<Map<String, dynamic>>()
.map(HistoryMessage.fromJson)
.toList()
: const <HistoryMessage>[];
return HistorySnapshot(
scope: _asString(json['scope'], fallback: 'history_day'),
threadId: json['threadId'] as String?,
day: json['day'] as String?,
hasMore: json['hasMore'] == true,
messages: messages,
);
}
}
class HistoryMessage {
const HistoryMessage({
required this.id,
required this.seq,
required this.role,
required this.content,
required this.timestamp,
this.url,
this.uiSchema,
});
final String id;
final int seq;
final String role;
final String content;
final DateTime timestamp;
final String? url;
final Map<String, dynamic>? uiSchema;
factory HistoryMessage.fromJson(Map<String, dynamic> json) => HistoryMessage(
id: _asString(json['id']),
seq: _asInt(json['seq']),
role: _asString(json['role']),
content: _asString(json['content']),
timestamp:
DateTime.tryParse(_asString(json['timestamp'])) ?? DateTime.now(),
url: json['url'] as String?,
uiSchema: _asMap(json['ui_schema']) ?? _asMap(json['uiSchema']),
);
}
String _asString(Object? value, {String fallback = ''}) {
if (value is String) {
return value;
}
return fallback;
}
int _asInt(Object? value) {
if (value is int) {
return value;
}
if (value is double) {
return value.toInt();
}
if (value is String) {
return int.tryParse(value) ?? 0;
}
return 0;
}
Map<String, dynamic>? _asMap(Object? value) {
if (value is Map<String, dynamic>) {
return value;
}
if (value is Map) {
final result = <String, dynamic>{};
for (final entry in value.entries) {
final key = entry.key;
if (key is String) {
result[key] = entry.value;
}
}
return result;
}
return null;
}