feat: 统一智能体路由 + 删除 onboarding
- 新增 Unified 智能体(合并 health/diet/medication/exercise/default 全部工具) - AI 自动判断意图并调用对应工具,前端胶囊自动切换 - 删除 onboarding 智能体及所有建档引导 UI - 前端始终发 unified 类型,通过 tool_result 事件同步胶囊状态
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 557 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 558 KiB |
@@ -69,8 +69,6 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
return _buildDietAnalysisCard(context, msg);
|
||||
case MessageType.reportAnalysis:
|
||||
return _buildReportAnalysisCard(context, msg);
|
||||
case MessageType.onboarding:
|
||||
return _buildOnboardingCard(context, ref);
|
||||
case MessageType.quickOptions:
|
||||
return _buildQuickOptionsCard(context, msg);
|
||||
default:
|
||||
@@ -749,66 +747,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 6. OnboardingCard — 首次建档引导
|
||||
Widget _buildOnboardingCard(BuildContext context, WidgetRef ref) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.88),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [BoxShadow(color: const Color(0xFF8B9CF7).withAlpha(25), blurRadius: 14, offset: const Offset(0, 4))],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 16),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(colors: [Color(0xFF8B9CF7), Color(0xFFA78BFA)], begin: Alignment.topLeft, end: Alignment.bottomRight),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(width: 48, height: 48, decoration: BoxDecoration(color: Colors.white.withAlpha(30), borderRadius: BorderRadius.circular(14)), child: const Icon(Icons.health_and_safety, size: 28, color: Colors.white)),
|
||||
const SizedBox(width: 14),
|
||||
const Text('欢迎来到健康管家!', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.white)),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Text('我是您的AI健康管家。为了给您更精准的建议,\n我先了解一些基本信息好吗?大约2-3分钟。',
|
||||
style: TextStyle(fontSize: 14, color: Colors.white.withAlpha(220), height: 1.5)),
|
||||
]),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 18, 18, 20),
|
||||
child: Column(children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(chatProvider.notifier).startOnboarding();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF8B9CF7), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14)),
|
||||
child: const Text('开始建档', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () => ref.read(chatProvider.notifier).dismissOnboarding(),
|
||||
child: const Text('以后再说', style: TextStyle(fontSize: 14, color: Color(0xFF999999)))),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 7. QuickOptionsCard — 优化样式
|
||||
// 6. QuickOptionsCard — 优化样式
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
Widget _buildQuickOptionsCard(BuildContext context, ChatMessage msg) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'auth_provider.dart';
|
||||
import 'data_providers.dart';
|
||||
import '../utils/sse_handler.dart';
|
||||
|
||||
enum MessageType { text, dataConfirm, medicationConfirm, dietAnalysis, reportAnalysis, quickOptions, agentWelcome, taskCard, onboarding }
|
||||
enum MessageType { text, dataConfirm, medicationConfirm, dietAnalysis, reportAnalysis, quickOptions, agentWelcome, taskCard }
|
||||
|
||||
class ChatMessage {
|
||||
final String id;
|
||||
@@ -27,7 +27,7 @@ class ChatMessage {
|
||||
bool get isUser => role == 'user';
|
||||
}
|
||||
|
||||
enum ActiveAgent { default_, consultation, health, diet, medication, report, exercise, onboarding }
|
||||
enum ActiveAgent { default_, consultation, health, diet, medication, report, exercise }
|
||||
|
||||
class ChatState {
|
||||
final ActiveAgent activeAgent;
|
||||
@@ -122,49 +122,10 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
ChatState build() {
|
||||
Future.microtask(() {
|
||||
insertTaskCard();
|
||||
_checkOnboarding();
|
||||
});
|
||||
return const ChatState();
|
||||
}
|
||||
|
||||
void _checkOnboarding() async {
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final res = await api.get('/api/user/health-archive');
|
||||
final archive = res.data['data'];
|
||||
// 档案全空 → 触发建档引导
|
||||
final isEmpty = archive == null ||
|
||||
(archive['diagnosis'] == null && archive['surgeryType'] == null &&
|
||||
(archive['allergies'] == null || (archive['allergies'] as List).isEmpty) &&
|
||||
(archive['chronicDiseases'] == null || (archive['chronicDiseases'] as List).isEmpty) &&
|
||||
(archive['dietRestrictions'] == null || (archive['dietRestrictions'] as List).isEmpty));
|
||||
if (isEmpty && !state.messages.any((m) => m.type == MessageType.onboarding)) {
|
||||
final db = ref.read(localDbProvider);
|
||||
final skipped = await db.read('onboarding_skipped');
|
||||
if (skipped == null) {
|
||||
state = state.copyWith(messages: [
|
||||
...state.messages,
|
||||
ChatMessage(id: 'onboarding_card', role: 'assistant', content: '', createdAt: DateTime.now(), type: MessageType.onboarding),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void startOnboarding() {
|
||||
setAgent(ActiveAgent.onboarding);
|
||||
// Remove onboarding card
|
||||
state = state.copyWith(messages: state.messages.where((m) => m.type != MessageType.onboarding).toList());
|
||||
// Send trigger message
|
||||
Future.microtask(() => _sendToAI('开始建档'));
|
||||
}
|
||||
|
||||
void dismissOnboarding() async {
|
||||
final db = ref.read(localDbProvider);
|
||||
await db.write('onboarding_skipped', DateTime.now().toIso8601String());
|
||||
state = state.copyWith(messages: state.messages.where((m) => m.type != MessageType.onboarding).toList());
|
||||
}
|
||||
|
||||
void insertTaskCard() {
|
||||
if (state.messages.any((m) => m.type == MessageType.taskCard)) return;
|
||||
state = state.copyWith(messages: [ChatMessage(
|
||||
@@ -179,6 +140,37 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
void setAgent(ActiveAgent a) {
|
||||
_subscription?.cancel();
|
||||
state = state.copyWith(activeAgent: a);
|
||||
ref.read(selectedAgentProvider.notifier).select(a);
|
||||
}
|
||||
|
||||
/// 根据 AI 调用的工具自动切换智能体胶囊
|
||||
void _switchAgentByTool(String tool) {
|
||||
ActiveAgent? agent;
|
||||
switch (tool) {
|
||||
case 'record_health_data':
|
||||
case 'query_health_records':
|
||||
agent = ActiveAgent.health;
|
||||
break;
|
||||
case 'estimate_food_text':
|
||||
agent = ActiveAgent.diet;
|
||||
break;
|
||||
case 'manage_medication':
|
||||
agent = ActiveAgent.medication;
|
||||
break;
|
||||
case 'manage_exercise':
|
||||
agent = ActiveAgent.exercise;
|
||||
break;
|
||||
case 'request_doctor':
|
||||
agent = ActiveAgent.consultation;
|
||||
break;
|
||||
case 'analyze_report':
|
||||
agent = ActiveAgent.report;
|
||||
break;
|
||||
}
|
||||
if (agent != null) {
|
||||
ref.read(selectedAgentProvider.notifier).select(agent);
|
||||
state = state.copyWith(activeAgent: agent);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadConversation(String convId) async {
|
||||
@@ -298,10 +290,9 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
return;
|
||||
}
|
||||
|
||||
final agentPath =
|
||||
state.activeAgent.name.replaceFirst('default_', 'default');
|
||||
// 始终用 unified 智能体,AI 自动判断意图分配工具
|
||||
final stream = SseHandler.connect(
|
||||
agentType: agentPath,
|
||||
agentType: 'unified',
|
||||
message: text,
|
||||
conversationId: state.conversationId,
|
||||
token: token,
|
||||
@@ -346,6 +337,8 @@ class ChatNotifier extends Notifier<ChatState> {
|
||||
state = state.copyWith(thinkingText: j['message'] as String?);
|
||||
case 'tool_result':
|
||||
final tool = j['tool'] as String? ?? '';
|
||||
// 根据 AI 调用的工具自动切换智能体胶囊
|
||||
_switchAgentByTool(tool);
|
||||
if (tool == 'record_health_data') {
|
||||
ref.invalidate(latestHealthProvider);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user