refactor: 死代码清理 + 趋势图重构 + 侧边栏交互优化 + 智能体卡片去延迟
- 死代码清理: 删除 AppCard/AppMenuItem/AppTabChip/AppButtons 等 4 个未使用 Widget 文件; 清理 common_widgets/enterprise_widgets/app_status_badge 中未使用 class; 移除 HealthService 等未使用方法; 后端移除 ExerciseService.CreateFromItemsAsync/HealthArchiveService.GetOrCreateAsync/MedicationAgentHandler 死分支/ChatRequest DTO - 趋势图: 直线折线 + 渐变填充 + 阴影; 去网格; X 轴日+月份; Y 轴整数刻度最多 4 个; 点击数据点/录入记录显示上方浮层; 选中点变实心 - 侧边栏: 对话记录改单选删除(灰底高亮); 操作栏浮层修复触摸问题; 常用功能/健康仪表盘间距收紧; _Panel 去外框 - 智能体: 欢迎卡片去掉 400ms 延迟; 胶囊去阴影 - 其他: api_client IP 适配; 多页面 UI 微调; 新增 chat_provider/prelaunch_guardrails 测试
This commit is contained in:
@@ -27,6 +27,7 @@ class ChatMessage {
|
||||
this.confirmed = false,
|
||||
});
|
||||
bool get isUser => role == 'user';
|
||||
bool get isReadOnly => metadata?['readOnly'] == true;
|
||||
}
|
||||
|
||||
enum ActiveAgent {
|
||||
@@ -67,16 +68,6 @@ class ChatState {
|
||||
);
|
||||
}
|
||||
|
||||
class SelectedAgentNotifier extends Notifier<ActiveAgent?> {
|
||||
@override
|
||||
ActiveAgent? build() => null;
|
||||
void select(ActiveAgent? a) => state = a;
|
||||
}
|
||||
|
||||
final selectedAgentProvider =
|
||||
NotifierProvider<SelectedAgentNotifier, ActiveAgent?>(
|
||||
SelectedAgentNotifier.new,
|
||||
);
|
||||
final chatProvider = NotifierProvider<ChatNotifier, ChatState>(
|
||||
ChatNotifier.new,
|
||||
);
|
||||
@@ -85,8 +76,8 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
StreamSubscription<Map<String, dynamic>>? _subscription;
|
||||
Completer<void>? _streamDone;
|
||||
ActiveAgent? _lastTriggeredAgent;
|
||||
|
||||
void markNeedsRebuild() => state = state.copyWith();
|
||||
Timer? _agentWelcomeTimer;
|
||||
String? _pendingAgentTriggerMessageId;
|
||||
|
||||
/// 重置整个会话:取消正在进行的 SSE,清空消息和会话 ID。
|
||||
/// 历史记录页一键清空 / 删除当前会话时调用。
|
||||
@@ -94,7 +85,6 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
await _cancelActiveStream();
|
||||
_lastTriggeredAgent = null;
|
||||
state = const ChatState();
|
||||
ref.read(selectedAgentProvider.notifier).select(null);
|
||||
}
|
||||
|
||||
/// 不可变消息操作方法(供 chat_messages_view 新版代码调用)
|
||||
@@ -102,6 +92,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
final msgs = state.messages.toList();
|
||||
final i = msgs.indexWhere((m) => m.id == id);
|
||||
if (i < 0) return '确认卡片不存在';
|
||||
if (msgs[i].isReadOnly) return '历史记录中的录入卡片仅供查看';
|
||||
|
||||
final rawIds = msgs[i].metadata?['confirmationIds'];
|
||||
final confirmationIds = rawIds is List
|
||||
@@ -142,6 +133,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
ChatState build() {
|
||||
ref.onDispose(() {
|
||||
_subscription?.cancel();
|
||||
_agentWelcomeTimer?.cancel();
|
||||
_subscription = null;
|
||||
if (_streamDone != null && !_streamDone!.isCompleted) {
|
||||
_streamDone!.complete();
|
||||
@@ -170,14 +162,6 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
);
|
||||
}
|
||||
|
||||
void setAgent(ActiveAgent a) {
|
||||
// 流式回复中忽略胶囊切换,防止状态混乱
|
||||
if (state.isStreaming) return;
|
||||
_cancelActiveStream();
|
||||
state = state.copyWith(activeAgent: a);
|
||||
ref.read(selectedAgentProvider.notifier).select(a);
|
||||
}
|
||||
|
||||
/// 根据 AI 调用的工具自动切换智能体胶囊
|
||||
void _switchAgentByTool(String tool) {
|
||||
ActiveAgent? agent;
|
||||
@@ -203,13 +187,13 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
break;
|
||||
}
|
||||
if (agent != null) {
|
||||
ref.read(selectedAgentProvider.notifier).select(agent);
|
||||
state = state.copyWith(activeAgent: agent);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadConversation(String convId) async {
|
||||
Future<String?> loadConversation(String convId) async {
|
||||
await _cancelActiveStream();
|
||||
_cancelPendingAgentWelcome();
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/ai/conversations/$convId');
|
||||
@@ -219,7 +203,8 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
final role = map['role']?.toString().toLowerCase() == 'user'
|
||||
? 'user'
|
||||
: 'assistant';
|
||||
final metadata = _parseMetadata(map['metadataJson']);
|
||||
final metadata = _parseMetadata(map['metadataJson']) ?? {};
|
||||
metadata['readOnly'] = true;
|
||||
return ChatMessage(
|
||||
id: map['id']?.toString() ?? '',
|
||||
role: role,
|
||||
@@ -229,6 +214,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
DateTime.now(),
|
||||
type: _messageTypeFromMetadata(metadata),
|
||||
metadata: metadata,
|
||||
confirmed: metadata['confirmationIds'] is! List,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@@ -237,11 +223,14 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
conversationId: convId,
|
||||
activeAgent: ActiveAgent.default_,
|
||||
);
|
||||
ref.read(selectedAgentProvider.notifier).select(ActiveAgent.default_);
|
||||
} catch (_) {}
|
||||
return null;
|
||||
} catch (_) {
|
||||
return '会话加载失败,请稍后重试';
|
||||
}
|
||||
}
|
||||
|
||||
void insertAgentWelcome(ActiveAgent agent) {
|
||||
_pendingAgentTriggerMessageId = null;
|
||||
state = state.copyWith(
|
||||
messages: [
|
||||
...state.messages,
|
||||
@@ -260,7 +249,12 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
/// 点击胶囊:先出用户标签 → 0.4 秒后出欢迎卡片,不走 AI
|
||||
/// 重复点击同一胶囊不重复弹卡片
|
||||
void triggerAgent(ActiveAgent agent, String label) {
|
||||
if (_lastTriggeredAgent == agent) return;
|
||||
if (_pendingAgentTriggerMessageId != null) {
|
||||
return;
|
||||
}
|
||||
if (_lastTriggeredAgent == agent && _pendingAgentTriggerMessageId == null) {
|
||||
return;
|
||||
}
|
||||
_lastTriggeredAgent = agent;
|
||||
|
||||
final userMsg = ChatMessage(
|
||||
@@ -269,18 +263,35 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
content: label,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
state = state.copyWith(messages: [...state.messages, userMsg]);
|
||||
final messages = state.messages.toList();
|
||||
messages.add(userMsg);
|
||||
_pendingAgentTriggerMessageId = userMsg.id;
|
||||
state = state.copyWith(messages: messages, activeAgent: agent);
|
||||
final expectedConversationId = state.conversationId;
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 400), () {
|
||||
_agentWelcomeTimer = Timer(Duration.zero, () {
|
||||
if (state.conversationId != expectedConversationId ||
|
||||
state.activeAgent != agent ||
|
||||
_lastTriggeredAgent != agent) {
|
||||
return;
|
||||
}
|
||||
_agentWelcomeTimer = null;
|
||||
insertAgentWelcome(agent);
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelPendingAgentWelcome() {
|
||||
_agentWelcomeTimer?.cancel();
|
||||
_agentWelcomeTimer = null;
|
||||
_pendingAgentTriggerMessageId = null;
|
||||
}
|
||||
|
||||
Future<void> sendImage(String imagePath, String text) async {
|
||||
if (state.isStreaming) return;
|
||||
final file = File(imagePath);
|
||||
if (!await file.exists()) return;
|
||||
_lastTriggeredAgent = null;
|
||||
_cancelPendingAgentWelcome();
|
||||
|
||||
// 先显示用户消息(本地显示图片路径)
|
||||
final userMsg = ChatMessage(
|
||||
@@ -290,7 +301,10 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
createdAt: DateTime.now(),
|
||||
metadata: {'localImagePath': imagePath},
|
||||
);
|
||||
state = state.copyWith(messages: [...state.messages, userMsg]);
|
||||
state = state.copyWith(
|
||||
messages: [...state.messages, userMsg],
|
||||
isStreaming: true,
|
||||
);
|
||||
|
||||
// 异步上传图片
|
||||
String? uploadedUrl;
|
||||
@@ -328,6 +342,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
state = state.copyWith(messages: [...state.messages, errorMsg]);
|
||||
state = state.copyWith(isStreaming: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,6 +357,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
final file = File(pdfPath);
|
||||
if (!await file.exists()) return;
|
||||
_lastTriggeredAgent = null;
|
||||
_cancelPendingAgentWelcome();
|
||||
|
||||
final userMsg = ChatMessage(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}',
|
||||
@@ -350,7 +366,10 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
createdAt: DateTime.now(),
|
||||
metadata: {'pdfFileName': fileName},
|
||||
);
|
||||
state = state.copyWith(messages: [...state.messages, userMsg]);
|
||||
state = state.copyWith(
|
||||
messages: [...state.messages, userMsg],
|
||||
isStreaming: true,
|
||||
);
|
||||
|
||||
String? uploadedUrl;
|
||||
try {
|
||||
@@ -382,6 +401,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
state = state.copyWith(messages: [...state.messages, errorMsg]);
|
||||
state = state.copyWith(isStreaming: false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -391,6 +411,7 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
Future<void> sendMessage(String text) async {
|
||||
if (text.trim().isEmpty || state.isStreaming) return;
|
||||
_lastTriggeredAgent = null;
|
||||
_cancelPendingAgentWelcome();
|
||||
|
||||
final userMsg = ChatMessage(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}',
|
||||
|
||||
Reference in New Issue
Block a user