import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/app_colors.dart'; import '../../../core/app_theme.dart'; import '../../../core/navigation_provider.dart'; import '../../../providers/auth_provider.dart'; import '../../../providers/chat_provider.dart'; import '../../../providers/data_providers.dart'; /// 对话消息列表 class ChatMessagesView extends ConsumerWidget { final ScrollController scrollCtrl; final List messages; const ChatMessagesView({super.key, required this.scrollCtrl, required this.messages}); @override Widget build(BuildContext context, WidgetRef ref) { final chatState = ref.watch(chatProvider); if (messages.isEmpty) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(40), ), child: const Icon(Icons.health_and_safety, size: 40, color: AppTheme.primary), ), const SizedBox(height: 16), Text('开始和 AI 健康管家对话吧', style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 8), const Text('记录健康数据,获取专业建议', style: TextStyle(fontSize: 17, color: AppColors.textHint)), ], ), ); } return ListView.builder( controller: scrollCtrl, reverse: true, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[messages.length - 1 - index]; return _buildMessageContent(context, ref, msg, chatState); }, ); } // ─── 消息分发 ───────────────────────────────────────────── Widget _buildMessageContent(BuildContext context, WidgetRef ref, ChatMessage msg, ChatState chatState) { switch (msg.type) { case MessageType.agentWelcome: final storedAgent = _parseAgentFromName(msg.metadata?['agent'] as String?); return _buildAgentWelcomeCard(context, ref, msg, storedAgent); case MessageType.taskCard: return _buildTaskCardInChat(context, ref); case MessageType.dataConfirm: return _buildDataConfirmCard(context, ref, msg); default: if (!msg.isUser && chatState.isStreaming && msg.content.length < 5) { return _buildThinkingBubble(context, chatState.thinkingText); } return _buildTextBubble(context, msg, chatState); } } // ═══════════════════════════════════════════════════════════ // 逐字淡入文本(蚂蚁阿福风格) // ═══════════════════════════════════════════════════════════ Widget _buildFadeInText(String text, TextStyle style, String stableId, {bool enabled = true}) { if (!enabled || text.length <= 2) return Text(text, style: style); return Wrap( children: List.generate(text.length, (i) { return TweenAnimationBuilder( key: ValueKey('fade_${stableId}_$i'), tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 200), curve: Curves.easeOut, builder: (_, value, child) => Opacity(opacity: value, child: child), child: Text(text[i], style: style), ); }), ); } // ═══════════════════════════════════════════════════════════ // 1. AgentWelcomeCard — 智能体欢迎卡片(蚂蚁阿福风格 + 美化版) // ═══════════════════════════════════════════════════════════ Widget _buildAgentWelcomeCard(BuildContext context, WidgetRef ref, ChatMessage msg, ActiveAgent agent) { final info = _agentInfo(agent); final actions = agent.actions; final features = _getAgentFeatures(agent); final screenWidth = MediaQuery.of(context).size.width; return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 16), constraints: BoxConstraints(maxWidth: screenWidth * 0.95), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(24), boxShadow: AppColors.cardShadow, ), clipBehavior: Clip.antiAlias, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── 标题区域(清爽蓝紫渐变背景) ── Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 22, 20, 20), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.purpleGradientStart.withAlpha(40), AppColors.blueGradientEnd.withAlpha(30), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 大图标容器 Container( width: 64, height: 64, decoration: BoxDecoration( gradient: AppColors.purpleBlueGradient, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: AppColors.primaryPurple.withAlpha(30), blurRadius: 14, offset: const Offset(0, 5), ), ], ), child: Icon(info.$1, size: 32, color: Colors.white), ), const SizedBox(width: 18), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // AI 助手标签 Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: Colors.white.withAlpha(160), borderRadius: BorderRadius.circular(10), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.auto_awesome, size: 14, color: AppColors.iconColor), const SizedBox(width: 4), Text('AI 智能助手', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: AppColors.iconColor)), ]), ), const SizedBox(height: 10), Text(info.$2, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w800, color: AppColors.textPrimary, letterSpacing: -0.3)), const SizedBox(height: 8), Text(info.$3, style: const TextStyle(fontSize: 16, color: AppColors.textSecondary, height: 1.4)), ], ), ), ], ), ), // ── 功能特性区 ── if (features.isNotEmpty) ...[ Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: AppColors.backgroundSecondary, borderRadius: BorderRadius.circular(18), border: Border.all(color: AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( width: 6, height: 18, decoration: BoxDecoration( gradient: AppColors.purpleBlueGradient, borderRadius: BorderRadius.circular(3), ), ), const SizedBox(width: 8), Text('核心功能', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), ]), const SizedBox(height: 14), Row( children: features.map((feature) { return Expanded( child: Container( margin: EdgeInsets.only( left: features.indexOf(feature) == 0 ? 0 : 6, right: features.indexOf(feature) == features.length - 1 ? 0 : 6, ), padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.borderLight), ), child: Column( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( gradient: AppColors.lightGradient, borderRadius: BorderRadius.circular(14), ), child: Icon(feature.icon, size: 24, color: AppColors.iconColor), ), const SizedBox(height: 8), Text(feature.title, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.textPrimary), textAlign: TextAlign.center), const SizedBox(height: 2), Text(feature.desc, style: const TextStyle(fontSize: 13, color: AppColors.textHint), textAlign: TextAlign.center), ], ), ), ); }).toList(), ), ], ), ), ), ], // ── 快捷操作按钮区 ── if (actions.isNotEmpty) ...[ const SizedBox(height: 14), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Container( width: 6, height: 18, decoration: BoxDecoration( gradient: AppColors.purpleBlueGradient, borderRadius: BorderRadius.circular(3), ), ), const SizedBox(width: 8), Text('快捷操作', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), ]), const SizedBox(height: 12), Wrap( spacing: 10, runSpacing: 10, children: actions.map((a) => _buildActionButton(a, context, ref)).toList(), ), ], ), ), ], // ── 底部提示 ── const SizedBox(height: 16), Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: AppColors.iconBackground, borderRadius: BorderRadius.circular(12), ), child: Row(children: [ Icon(Icons.lightbulb_outline, size: 18, color: AppColors.iconColor), const SizedBox(width: 8), Expanded(child: Text('直接描述您的需求,AI 会自动为您记录和分析', style: TextStyle(fontSize: 14, color: AppColors.iconColor, fontWeight: FontWeight.w500))), ]), ), ), ], ), ), ); } Widget _buildActionButton(_AgentAction a, BuildContext context, WidgetRef ref) { final screenWidth = MediaQuery.of(context).size.width; return InkWell( onTap: () { if (a.label == '服药打卡') { _medicationCheckIn(ref, context); } else if (a.action == 'pickFoodCamera' || a.action == 'pickFoodGallery') { ref.read(dietActionProvider.notifier).trigger(a.action!); } else if (a.route != null) { if (a.route == 'camera' || a.route == 'gallery') { ref.read(cameraActionProvider.notifier).trigger(a.route!); } else { pushRoute(ref, a.route!); } } }, borderRadius: BorderRadius.circular(16), child: Container( width: ((screenWidth - 40 - 10) / 2) - 5, padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.borderLight, width: 1.5), boxShadow: AppColors.cardShadow, ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( gradient: AppColors.lightGradient, borderRadius: BorderRadius.circular(12), ), child: Icon(a.icon, size: 22, color: AppColors.iconColor), ), const SizedBox(width: 12), Expanded(child: Text(a.label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary))), Icon(Icons.chevron_right, size: 18, color: AppColors.border), ], ), ), ); } List<_AgentFeature> _getAgentFeatures(ActiveAgent agent) { return switch (agent) { ActiveAgent.health => [ _AgentFeature(Icons.trending_up, '趋势分析', '查看历史数据'), _AgentFeature(Icons.add, '快速录入', '血压心率血糖'), ], ActiveAgent.diet => [ _AgentFeature(Icons.camera_alt, '拍照识别', '自动识别食物'), _AgentFeature(Icons.bar_chart, '营养分析', '热量营养成分'), ], ActiveAgent.medication => [ _AgentFeature(Icons.list, '用药清单', '管理所有药品'), _AgentFeature(Icons.alarm, '智能提醒', '按时服药提醒'), ], ActiveAgent.consultation => [ _AgentFeature(Icons.message, '在线问诊', '专业医生解答'), _AgentFeature(Icons.local_hospital, '科室选择', '精准匹配'), ], ActiveAgent.report => [ _AgentFeature(Icons.upload, '上传报告', '支持多种格式'), _AgentFeature(Icons.search, 'AI解读', '智能分析报告'), ], ActiveAgent.exercise => [ _AgentFeature(Icons.calendar_month, '运动计划', '科学规划'), _AgentFeature(Icons.check_circle, '打卡记录', '坚持完成'), ], _ => [ _AgentFeature(Icons.health_and_safety, '健康管理', '全方位呵护'), _AgentFeature(Icons.chat, '智能咨询', '随时答疑'), ], }; } Widget _buildDoctorCards(WidgetRef ref) { const doctors = [ {'name': '张明', 'title': '主任医师', 'dept': '心脏康复科', 'desc': '冠心病、高血压术后管理', 'id': '468b82e2-d95a-4436-bff6-a50eecf99a66'}, {'name': '李芳', 'title': '副主任医师', 'dept': '营养科', 'desc': '糖尿病、甲状腺疾病管理', 'id': 'd4148733-b538-4398-af17-0c7592fc0c2d'}, {'name': '王建国', 'title': '主任医师', 'dept': '心血管内科', 'desc': '术后营养指导、饮食方案制定', 'id': 'ef0953c9-eb63-4d03-b6d7-050a1897d4a3'}, ]; return Row( children: [ for (var i = 0; i < doctors.length; i++) ...[ if (i > 0) const SizedBox(width: 8), Expanded(child: _doctorCard(doctors[i], ref)), ], ], ); } Widget _doctorCard(Map doc, WidgetRef ref) { return InkWell( onTap: () => pushRoute(ref, 'consultation', params: {'id': doc['id']!}), borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: AppTheme.primaryLight), ), child: Column(mainAxisSize: MainAxisSize.min, children: [ CircleAvatar( radius: 22, backgroundColor: AppTheme.primaryLight, child: Text(doc['name']![0], style: const TextStyle(fontSize: 21, fontWeight: FontWeight.w600, color: AppTheme.primary)), ), const SizedBox(height: 6), Text(doc['name']!, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), Text(doc['title']!, style: const TextStyle(fontSize: 14, color: AppColors.textHint)), const SizedBox(height: 2), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(4), ), child: Text(doc['dept']!, style: const TextStyle(fontSize: 13, color: AppTheme.primary)), ), const SizedBox(height: 4), Text( doc['desc']!, style: const TextStyle(fontSize: 13, color: AppColors.textSecondary, height: 1.3), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ]), ), ); } // ═══════════════════════════════════════════════════════════ // 2. DataConfirmCard — 统一确认卡片(紫白蓝风格) // ═══════════════════════════════════════════════════════════ Widget _buildDataConfirmCard(BuildContext context, WidgetRef ref, ChatMessage msg) { final meta = msg.metadata ?? {}; final backendType = meta['type'] as String? ?? ''; final screenWidth = MediaQuery.of(context).size.width; // 识别是哪种数据类型 final isMedication = backendType == 'medication' || (meta['name'] != null && meta['name'].toString().isNotEmpty && backendType != 'exercise'); final isExercise = backendType == 'exercise'; final isHealth = !isMedication && !isExercise; // 根据类型获取显示字段 String title = '数据确认'; IconData titleIcon = Icons.assignment; String mainLabel = ''; String mainValue = ''; String mainUnit = ''; bool abnormal = meta['abnormal'] as bool? ?? false; String recordTime = meta['recordTime'] as String? ?? ''; List<_ConfirmField> fields = []; if (isMedication) { // 药品录入 title = '药品录入确认'; titleIcon = Icons.medication_liquid_outlined; mainLabel = meta['name'] as String? ?? ''; mainValue = meta['dosage'] as String? ?? ''; mainUnit = ''; if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt); final nameValue = meta['药品名称'] as String? ?? mainLabel; fields.add(_ConfirmField(label: '药品名称', value: nameValue, icon: Icons.medication_outlined, editable: true)); if (mainValue.isNotEmpty) { final dosageValue = meta['剂量'] as String? ?? mainValue; fields.add(_ConfirmField(label: '剂量', value: dosageValue, icon: Icons.local_pharmacy_outlined, editable: true)); } final time = meta['time'] as String? ?? ''; if (time.isNotEmpty) { final timeValue = meta['服药时间'] as String? ?? time; fields.add(_ConfirmField(label: '服药时间', value: timeValue, icon: Icons.access_time, editable: true)); } final frequency = meta['frequency'] as String? ?? ''; if (frequency.isNotEmpty) { final freqValue = meta['频率'] as String? ?? _freqLabel(frequency); fields.add(_ConfirmField(label: '频率', value: freqValue, icon: Icons.repeat_outlined, editable: true)); } final duration = meta['duration_days'] as int?; if (duration != null && duration > 0) { final durationValue = meta['服用天数'] as String? ?? '$duration 天'; fields.add(_ConfirmField(label: '服用天数', value: durationValue, icon: Icons.event_available_outlined, editable: true)); } } else if (isExercise) { // 运动计划 title = '运动计划确认'; titleIcon = Icons.directions_run_outlined; mainLabel = meta['name'] as String? ?? meta['activity'] as String? ?? '运动计划'; mainValue = meta['duration'] as String? ?? ''; mainUnit = meta['duration_unit'] as String? ?? '分钟'; if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt); final typeValue = meta['运动类型'] as String? ?? mainLabel; fields.add(_ConfirmField(label: '运动类型', value: typeValue, icon: Icons.fitness_center_outlined, editable: true)); if (mainValue.isNotEmpty) { final durationValue = meta['运动时长'] as String? ?? '$mainValue $mainUnit'; fields.add(_ConfirmField(label: '运动时长', value: durationValue, icon: Icons.timer_outlined, editable: true)); } final intensity = meta['intensity'] as String? ?? ''; if (intensity.isNotEmpty) { final intensityValue = meta['运动强度'] as String? ?? intensity; fields.add(_ConfirmField(label: '运动强度', value: intensityValue, icon: Icons.local_fire_department_outlined, editable: true)); } final calories = meta['calories'] as String? ?? meta['calorie'] as String? ?? ''; if (calories.isNotEmpty) { final caloriesValue = meta['消耗热量'] as String? ?? '$calories kcal'; fields.add(_ConfirmField(label: '消耗热量', value: caloriesValue, icon: Icons.local_fire_department, editable: true)); } final schedule = meta['schedule'] as String? ?? ''; if (schedule.isNotEmpty) { final scheduleValue = meta['计划时间'] as String? ?? schedule; fields.add(_ConfirmField(label: '计划时间', value: scheduleValue, icon: Icons.calendar_month_outlined, editable: true)); } } else { // 健康指标 title = '健康数据确认'; titleIcon = Icons.monitor_heart_outlined; mainLabel = _getMetricName(backendType); mainValue = meta['value'] as String? ?? ''; mainUnit = meta['unit'] as String? ?? _getMetricUnit(backendType); if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt); final metricValue = meta[mainLabel] as String? ?? '$mainValue $mainUnit'; fields.add(_ConfirmField(label: mainLabel, value: metricValue, icon: Icons.favorite_outline, editable: true)); final timeValue = meta['记录时间'] as String? ?? recordTime; fields.add(_ConfirmField(label: '记录时间', value: timeValue, icon: Icons.access_time, editable: true)); final note = meta['note'] as String? ?? ''; if (note.isNotEmpty) { final noteValue = meta['备注'] as String? ?? note; fields.add(_ConfirmField(label: '备注', value: noteValue, icon: Icons.edit_note_outlined, editable: true)); } } return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 16), constraints: BoxConstraints(maxWidth: screenWidth * 0.95), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(24), boxShadow: AppColors.cardShadow, ), clipBehavior: Clip.antiAlias, child: Column( mainAxisSize: MainAxisSize.min, children: [ // ── 标题区域(清爽蓝紫渐变背景) ── Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 20, 20, 18), decoration: BoxDecoration( gradient: LinearGradient( colors: [ AppColors.purpleGradientStart.withAlpha(45), AppColors.blueGradientEnd.withAlpha(25), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 56, height: 56, decoration: BoxDecoration( gradient: AppColors.purpleBlueGradient, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow(color: AppColors.primaryPurple.withAlpha(35), blurRadius: 10, offset: const Offset(0, 4)), ], ), child: Icon(titleIcon, size: 28, color: Colors.white), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary)), const SizedBox(height: 6), Row( children: [ Icon(Icons.access_time, size: 15, color: AppColors.iconColor), const SizedBox(width: 4), Text(recordTime, style: const TextStyle(fontSize: 15, color: AppColors.textSecondary)), ], ), ], ), ), ], ), ), // ── 主要数据展示区(大卡片) ── Padding( padding: const EdgeInsets.fromLTRB(20, 14, 20, 0), child: Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.backgroundSecondary, borderRadius: BorderRadius.circular(18), border: Border.all(color: AppColors.border), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 68, height: 68, decoration: BoxDecoration( gradient: AppColors.lightGradient, borderRadius: BorderRadius.circular(18), ), child: Center( child: isHealth ? Text(_getMetricIcon(backendType), style: const TextStyle(fontSize: 36)) : Icon( isMedication ? Icons.medication_liquid_outlined : Icons.directions_run_outlined, size: 36, color: AppColors.iconColor, ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(mainLabel, style: const TextStyle(fontSize: 17, color: AppColors.textSecondary)), const SizedBox(height: 8), RichText( text: TextSpan( children: [ TextSpan( text: mainValue.isNotEmpty ? mainValue : '—', style: TextStyle( fontSize: 36, fontWeight: FontWeight.w800, color: abnormal ? AppColors.error : AppColors.textPrimary, letterSpacing: -0.5, ), ), if (mainUnit.isNotEmpty) TextSpan( text: ' $mainUnit', style: TextStyle( fontSize: 19, color: abnormal ? AppColors.error : AppColors.iconColor, fontWeight: FontWeight.w500, ), ), ], ), ), ], ), ), ], ), ), ), // ── 异常提示 ── if (abnormal) ...[ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: AppColors.error.withAlpha(10), borderRadius: BorderRadius.circular(14), border: Border.all(color: AppColors.error.withAlpha(30)), ), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration(color: AppColors.error.withAlpha(15), borderRadius: BorderRadius.circular(10)), child: Icon(Icons.warning_amber_rounded, size: 20, color: AppColors.error), ), const SizedBox(width: 10), Expanded(child: Text('数值偏高,建议关注并咨询医生', style: TextStyle(fontSize: 16, color: AppColors.error, fontWeight: FontWeight.w500))), ], ), ), ), ], // ── 详细字段列表(可点击修改) ── if (fields.isNotEmpty) ...[ const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.border), ), child: Column( children: [ for (int i = 0; i < fields.length; i++) ...[ if (i > 0) Container(height: 1, color: AppColors.borderLight, margin: const EdgeInsets.symmetric(horizontal: 16)), _buildFieldRow(fields[i], msg, ref), ], ], ), ), ), ], // ── 确认按钮(白色底+渐变边框) ── const SizedBox(height: 18), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( width: double.infinity, height: 56, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.borderLight, width: 1.5), boxShadow: AppColors.cardShadow, ), child: msg.confirmed ? Container( decoration: BoxDecoration( gradient: AppColors.successButtonGradient, borderRadius: BorderRadius.circular(16), boxShadow: AppColors.buttonShadow, ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.check_circle_outline, size: 22, color: Colors.white), const SizedBox(width: 8), const Text('录入成功', style: TextStyle(fontSize: 19, fontWeight: FontWeight.w600, color: Colors.white)), ]), ) : Material( color: Colors.transparent, child: InkWell( onTap: () { msg.confirmed = true; if (isMedication) ref.invalidate(medicationListProvider); if (isHealth) ref.invalidate(latestHealthProvider); ref.read(chatProvider.notifier).markNeedsRebuild(); }, borderRadius: BorderRadius.circular(16), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 32, height: 32, decoration: BoxDecoration( gradient: AppColors.purpleBlueGradient, borderRadius: BorderRadius.circular(8), ), child: const Icon(Icons.check, size: 18, color: Colors.white), ), const SizedBox(width: 10), Text('确认录入', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), ], ), ), ), ), ), const SizedBox(height: 20), ], ), ), ); } Widget _buildFieldRow(_ConfirmField field, ChatMessage msg, WidgetRef ref) { final isEditing = field.label == msg.metadata?['_editingField']; return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration(color: AppColors.iconBackground, borderRadius: BorderRadius.circular(12)), child: Icon(field.icon, size: 22, color: AppColors.iconColor), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(field.label, style: const TextStyle(fontSize: 15, color: AppColors.textHint)), const SizedBox(height: 4), if (!field.editable) Text(field.value.isNotEmpty ? field.value : '未设置', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)) else if (isEditing) _buildEditableTextField(field, msg, ref) else InkWell( onTap: () { msg.metadata?['_editingField'] = field.label; ref.read(chatProvider.notifier).markNeedsRebuild(); }, child: Row( children: [ Expanded(child: Text(field.value.isNotEmpty ? field.value : '未设置', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary))), const SizedBox(width: 8), Icon(Icons.edit_outlined, size: 18, color: AppColors.border), ], ), ), ], ), ), ], ), ); } Widget _buildEditableTextField(_ConfirmField field, ChatMessage msg, WidgetRef ref) { return StatefulBuilder( builder: (context, setState) { final controller = TextEditingController(text: field.value); return Container( decoration: BoxDecoration( border: Border.all(color: AppColors.primaryBlue.withAlpha(150), width: 1.5), borderRadius: BorderRadius.circular(8), color: AppColors.backgroundSecondary, ), child: TextField( controller: controller, autofocus: true, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary), decoration: const InputDecoration( contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), border: InputBorder.none, ), onSubmitted: (value) { msg.metadata?[field.label] = value; msg.metadata?['_editingField'] = null; ref.read(chatProvider.notifier).markNeedsRebuild(); }, onTapOutside: (_) { msg.metadata?[field.label] = controller.text; msg.metadata?['_editingField'] = null; ref.read(chatProvider.notifier).markNeedsRebuild(); }, ), ); }, ); } void _editField(_ConfirmField field, ChatMessage msg, WidgetRef ref) { // 字段可点击修改的入口,后续可接入 showDialog 实现 } // ═══════════════════════════════════════════════════════════ // 3. ThinkingBubble — 思考动画 // ═══════════════════════════════════════════════════════════ Widget _buildThinkingBubble(BuildContext context, String? thinkingText) { return Align( alignment: Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: const BorderRadius.only(topLeft: Radius.circular(4), topRight: Radius.circular(20), bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), border: Border.all(color: AppColors.border, width: 1.5), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 26, height: 26, padding: const EdgeInsets.all(5), decoration: BoxDecoration( color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(13), ), child: const CircularProgressIndicator(strokeWidth: 2.2, color: AppTheme.primary), ), const SizedBox(width: 10), Text(thinkingText?.isNotEmpty == true ? thinkingText! : '正在分析...', style: const TextStyle(fontSize: 17, color: AppColors.textHint)), ], ), ), ); } Widget _buildTextBubble(BuildContext context, ChatMessage msg, ChatState? chatState) { final isUser = msg.isUser; final imageUrl = msg.metadata?['imageUrl'] as String?; final localPath = msg.metadata?['localImagePath'] as String?; final hasImage = imageUrl != null || localPath != null; return Align( alignment: isUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 12), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.82), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: isUser ? AppTheme.primary : AppColors.cardBackground, borderRadius: BorderRadius.only( topLeft: Radius.circular(isUser ? 20 : 4), topRight: Radius.circular(isUser ? 4 : 20), bottomLeft: const Radius.circular(20), bottomRight: const Radius.circular(20), ), border: isUser ? null : Border.all(color: AppColors.border, width: 1.5), boxShadow: isUser ? [] : [BoxShadow(color: AppTheme.primary.withAlpha(12), blurRadius: 10, offset: const Offset(0, 3))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 文字内容 if (isUser) SelectableText(msg.content, style: const TextStyle(fontSize: 19, color: Colors.white, height: 1.5)) else if (chatState?.isStreaming ?? false) // 流式输出中:逐字淡入 _buildFadeInText(_stripMd(msg.content), const TextStyle(fontSize: 19, color: AppColors.textPrimary, height: 1.5), msg.id) else SelectableText(_stripMd(msg.content), style: const TextStyle(fontSize: 19, color: AppColors.textPrimary, height: 1.5)), // 图片缩略图(在文字下方) if (hasImage) Padding( padding: const EdgeInsets.only(top: 8), child: GestureDetector( onTap: () => _showFullImage(context, localPath ?? imageUrl), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 160, maxHeight: 120), child: localPath != null ? Image.file(File(localPath), fit: BoxFit.cover) : imageUrl != null ? Image.network(imageUrl, fit: BoxFit.cover, errorBuilder: (_, e, s) => Container( width: 80, height: 60, decoration: BoxDecoration(color: AppColors.backgroundSecondary, borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.image, size: 28, color: AppColors.textHint), )) : const SizedBox.shrink(), ), ), ), ), if (!isUser && msg.content.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 10), child: Row(children: [ const CircleAvatar(radius: 10, backgroundColor: AppTheme.primaryLight, child: Icon(Icons.chat_bubble_outline, size: 17, color: AppTheme.primary)), const SizedBox(width: 6), const Text('健康管家', style: TextStyle(fontSize: 15, color: AppColors.textHint)), const SizedBox(width: 4), const Text('仅供参考', style: TextStyle(fontSize: 14, color: AppColors.textHint)), ]), ), ], ), ), ); } // ═══════════════════════════════════════════════════════════ // 公共组件:通用按钮 // ═══════════════════════════════════════════════════════════ Widget _cardFilledBtn(String label, IconData icon, {VoidCallback? onTap}) { return ElevatedButton( onPressed: onTap ?? () {}, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primary, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 11), ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 19), const SizedBox(width: 5), Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), ]), ); } Widget _cardOutlineBtn(String label, IconData icon, {VoidCallback? onTap}) { return OutlinedButton( onPressed: onTap ?? () {}, style: OutlinedButton.styleFrom( foregroundColor: AppTheme.primary, side: const BorderSide(color: AppTheme.primary, width: 1.2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 11), ), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 18), const SizedBox(width: 4), Text(label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), ]), ); } // ═══════════════════════════════════════════════════════════ // 工具方法 // ═══════════════════════════════════════════════════════════ static void _showFullImage(BuildContext context, String? path) { if (path == null) return; showDialog( context: context, builder: (ctx) => Dialog( backgroundColor: Colors.black, insetPadding: const EdgeInsets.all(24), child: Stack(alignment: Alignment.center, children: [ ClipRRect(borderRadius: BorderRadius.circular(12), child: InteractiveViewer(child: path.startsWith('http') ? Image.network(path, fit: BoxFit.contain) : Image.file(File(path), fit: BoxFit.contain))), Positioned(top: 8, right: 8, child: GestureDetector( onTap: () => Navigator.pop(ctx), child: Container(padding: const EdgeInsets.all(6), decoration: BoxDecoration(color: Colors.white54, shape: BoxShape.circle), child: const Icon(Icons.close, size: 21)), )), ]), ), ); } /// 去掉 AI 返回的 Markdown 标记(**加粗**、*斜体*、`代码`、标题、列表符号等) static String _stripMd(String text) { return text .replaceAll(RegExp(r'\*\*(.+?)\*\*'), r'$1') .replaceAll(RegExp(r'\*(.+?)\*'), r'$1') .replaceAll(RegExp(r'^#{1,6}\s+', multiLine: true), '') .replaceAll(RegExp(r'^[\-\*]\s+', multiLine: true), '') .replaceAll(RegExp(r'`(.+?)`'), r'$1') .replaceAll(RegExp(r'\[([^\]]+)\]\([^)]+\)'), r'$1') .replaceAll(RegExp(r'\n{3,}'), '\n\n') .trim(); } String _freqLabel(String freq) { switch (freq.toLowerCase()) { case 'daily': return '每天'; case 'everyotherday': return '隔天'; case 'weekly': return '每周'; default: return freq; } } String _formatTime(DateTime dt) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final thatDay = DateTime(dt.year, dt.month, dt.day); if (thatDay == today) { return '今天 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } if (thatDay == today.subtract(const Duration(days: 1))) { return '昨天 ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } return '${dt.month}/${dt.day} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } String _getMetricUnit(String type) { switch (type.toLowerCase()) { case 'blood_pressure': return 'mmHg'; case 'heart_rate': return 'bpm'; case 'glucose': return 'mmol/L'; case 'spo2': return '%'; case 'weight': return 'kg'; case 'exercise': return '分钟/天'; default: return ''; } } String _getMetricIcon(String type) { switch (type.toLowerCase()) { case 'blood_pressure': return '🩺'; case 'heart_rate': return '💓'; case 'glucose': return '💉'; case 'spo2': return '🫁'; case 'weight': return '⚖️'; case 'exercise': return '🏃'; default: return '📊'; } } String _getMetricName(String type) { switch (type.toLowerCase()) { case 'blood_pressure': return '血压'; case 'heart_rate': return '心率'; case 'glucose': return '血糖'; case 'spo2': return '血氧'; case 'weight': return '体重'; case 'exercise': return '运动计划'; default: return '健康指标'; } } static void _medicationCheckIn(WidgetRef ref, BuildContext context) async { try { final api = ref.read(apiClientProvider); final reminders = await ref.read(medicationReminderProvider.future); if (reminders.isEmpty) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('暂无待服药记录'), backgroundColor: AppColors.warning), ); return; } for (final m in reminders) { final id = m['id']?.toString() ?? ''; final time = m['scheduledTime']?.toString() ?? ''; if (id.isEmpty) continue; await api.post('/api/medications/$id/confirm-dose', data: {'scheduledTime': time, 'status': 'taken'}); } ref.invalidate(medicationReminderProvider); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('打卡成功 已记录 ${reminders.length} 项服药'), backgroundColor: AppTheme.success, ), ); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('打卡失败:$e'), backgroundColor: Colors.red), ); } } static _AgentColors _agentColors(ActiveAgent agent) { return switch (agent) { ActiveAgent.consultation => _AgentColors( gradient: [const Color(0xFFC5D5F8), const Color(0xFFA0B8F0), const Color(0xFF7B98E0)], bg: const Color(0xFFF0F4FF), border: const Color(0xFFD8E0FA), iconBg: const Color(0xFFE4ECFC), accent: const Color(0xFF7B98E0), ), ActiveAgent.health => _AgentColors( gradient: [const Color(0xFFB8E6CF), const Color(0xFF8ED4AE), const Color(0xFF5FB88D)], bg: const Color(0xFFF0FAF4), border: const Color(0xFFD0ECD8), iconBg: const Color(0xFFE4F8EC), accent: const Color(0xFF5FB88D), ), ActiveAgent.diet => _AgentColors( gradient: [const Color(0xFFFFD8B8), const Color(0xFFFFC896), const Color(0xFFF0A060)], bg: const Color(0xFFFFF6F0), border: const Color(0xFFFFE8D4), iconBg: const Color(0xFFFFEEDC), accent: const Color(0xFFF0A060), ), ActiveAgent.medication => _AgentColors( gradient: [const Color(0xFFFFD4E0), const Color(0xFFFFB8CC), const Color(0xFFE898A8)], bg: const Color(0xFFFFF0F4), border: const Color(0xFFFFE0E8), iconBg: const Color(0xFFFFE8EE), accent: const Color(0xFFE898A8), ), ActiveAgent.report => _AgentColors( gradient: [const Color(0xFFD8D0F0), const Color(0xFFC4B8EC), const Color(0xFFA898D8)], bg: const Color(0xFFF8F4FF), border: const Color(0xFFECE4F8), iconBg: const Color(0xFFF0E8FC), accent: const Color(0xFFA898D8), ), ActiveAgent.exercise => _AgentColors( gradient: [const Color(0xFFB8E0E0), const Color(0xFF90D0D0), const Color(0xFF68B4B4)], bg: const Color(0xFFF0FAFA), border: const Color(0xFFD4ECEC), iconBg: const Color(0xFFE4F4F4), accent: const Color(0xFF68B4B4), ), _ => _AgentColors( gradient: [const Color(0xFFC5D0F8), const Color(0xFFA0B0F0), const Color(0xFF7B90E0)], bg: const Color(0xFFF5F5FF), border: const Color(0xFFE0E0F8), iconBg: const Color(0xFFEDEDFC), accent: const Color(0xFF7B90E0), ), }; } static (_AgentIcon, String, String) _agentInfo(ActiveAgent agent) { return switch (agent) { ActiveAgent.health => (Icons.favorite_border, '记数据', '录入血压、血糖、心率等日常指标'), ActiveAgent.diet => (Icons.restaurant, '拍饮食', '拍照识别食物热量和营养成分'), ActiveAgent.medication => (Icons.medication, '药管家', '管理药品、提醒服药、追踪用量'), ActiveAgent.consultation => (Icons.local_hospital, '问诊', '在线咨询医生,描述症状获取建议'), ActiveAgent.report => (Icons.assignment, '看报告', '上传体检报告,AI 辅助解读'), ActiveAgent.exercise => (Icons.directions_run, '运动', '制定运动计划,打卡记录进度'), _ => (Icons.smart_toy, 'AI 助手', '您的智能健康管家'), }; } static ActiveAgent _parseAgentFromName(String? name) { switch (name) { case 'consultation': return ActiveAgent.consultation; case 'health': return ActiveAgent.health; case 'diet': return ActiveAgent.diet; case 'medication': return ActiveAgent.medication; case 'report': return ActiveAgent.report; case 'exercise': return ActiveAgent.exercise; default: return ActiveAgent.default_; } } Widget _buildTaskCardInChat(BuildContext context, WidgetRef ref) { final health = ref.watch(latestHealthProvider); final reminders = ref.watch(medicationReminderProvider); return health.when( data: (data) => _taskCardBubble(context, ref, data, reminders), loading: () => _taskCardBubble(context, ref, {}, const AsyncValue.loading()), error: (_, e) => _taskCardBubble(context, ref, {}, const AsyncValue.loading()), ); } Widget _taskCardBubble(BuildContext context, WidgetRef ref, Map healthData, AsyncValue>> reminders) { final now = DateTime.now(); final tasks = []; // 1. 健康数据摘要行 final bp = healthData['BloodPressure']; final bpText = bp is Map ? '${bp['systolic'] ?? '--'}/${bp['diastolic'] ?? '--'}' : null; final hr = healthData['HeartRate']; final hrText = hr is int ? '$hr' : null; final bs = healthData['BloodSugar']; final bsText = bs is num ? '$bs' : null; final bo = healthData['BloodOxygen']; final boText = bo is num ? '$bo' : null; final wt = healthData['Weight']; final wtText = wt is num ? '$wt' : null; final allNull = bpText == null && hrText == null && bsText == null && boText == null && wtText == null; if (!allNull) { tasks.add(_taskRow(context, Icons.check_circle, '今日已记录', trailing: [ if (bpText != null) '血压 $bpText', if (hrText != null) '心率 $hrText', if (bsText != null) '血糖 $bsText', if (boText != null) '血氧 $boText', if (wtText != null) '体重 $wtText', ].join(' · '), status: 'done', onTap: () => pushRoute(ref, 'trend'))); } // 2. 用药提醒 — 一行汇总 reminders.whenOrNull(data: (meds) { final overdue = meds.where((m) => m['status'] == 'overdue').length; final upcoming = meds.where((m) => m['status'] == 'upcoming').length; final taken = meds.where((m) => m['status'] == 'taken').length; final total = overdue + upcoming + taken; if (total > 0) { final parts = []; if (overdue > 0) parts.add('$overdue项过期'); if (upcoming > 0) parts.add('$upcoming项待服'); if (taken > 0) parts.add('$taken项已服'); tasks.add(_taskRow( context, Icons.medication_rounded, '用药打卡 ($total项)', trailing: parts.join(' · '), status: overdue > 0 ? 'overdue' : upcoming > 0 ? 'pending' : 'done', onTap: () => pushRoute(ref, 'medCheckIn'), )); } }); final hasMeds = reminders.asData?.value != null && (reminders.asData!.value).isNotEmpty; if (!hasMeds) { tasks.add(_taskRow(context, Icons.medication_rounded, '暂无用药提醒', status: 'pending')); } // 3. 运动 — 使用 currentExercisePlanProvider 同步读取 final exercisePlan = ref.watch(currentExercisePlanProvider); exercisePlan.whenOrNull(data: (plan) { if (plan != null) { final items = (plan['items'] as List?)?.cast>() ?? []; final today = now.weekday % 7; // Dart: Mon=1...Sun=7 → C#: Sun=0 Mon=1...Sat=6 final todayItem = items.cast?>().firstWhere( (i) => i?['dayOfWeek'] == today, orElse: () => null); if (todayItem != null && todayItem['isRestDay'] != true) { final done = todayItem['isCompleted'] == true; final name = items.isNotEmpty ? (items.first['exerciseType']?.toString() ?? '运动') : '运动'; final dur = items.isNotEmpty ? (items.first['durationMinutes']?.toString() ?? '30') : '30'; tasks.add(_taskRow(context, Icons.directions_run, '$name $dur分钟', status: done ? 'done' : (now.hour >= 18 ? 'overdue' : 'pending'), onTap: () => pushRoute(ref, 'exercisePlan'), )); } } }); // 4. 异常指标 if (bp is Map) { final s = bp['systolic']; if (s is int && s >= 140) { tasks.add(_taskRow( context, Icons.warning_amber_rounded, '血压 $s/${bp['diastolic'] ?? '--'} 偏高', status: 'warning', onTap: () => pushRoute(ref, 'trend', params: {'type': 'blood_pressure'}), )); } } return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: AppTheme.primary.withAlpha(10), blurRadius: 8, offset: const Offset(0, 2))], ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.today, size: 21, color: AppTheme.primary), const SizedBox(width: 8), const Text('今日任务', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimary)), ]), const SizedBox(height: 10), ...tasks, ]), ); } Widget _taskRow(BuildContext context, IconData icon, String label, {String status = 'pending', String? trailing, VoidCallback? onTap}) { final colors = {'done': AppTheme.success, 'warning': AppColors.warning, 'pending': AppColors.textHint, 'overdue': AppTheme.error}; final icons = {'done': Icons.check_circle, 'warning': Icons.warning, 'pending': Icons.circle_outlined, 'overdue': Icons.error}; final isOverdue = status == 'overdue'; return Padding( padding: const EdgeInsets.only(bottom: 8), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: isOverdue ? const Color(0xFFFFEBEE) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ Container(width: 30, height: 30, decoration: BoxDecoration(color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(8)), child: Icon(icon, size: 18, color: AppTheme.primary)), const SizedBox(width: 10), Expanded(child: Text(trailing ?? label, style: const TextStyle(fontSize: 16, color: AppColors.textPrimary))), Icon(icons[status] ?? Icons.circle_outlined, size: 21, color: colors[status] ?? Colors.grey), ]), ), ), ); } } // ════════════════════════════════════════════════════════════════ // 内部数据类 // ════════════════════════════════════════════════════════════════ typedef _AgentIcon = IconData; class _AgentColors { final List gradient; final Color bg; final Color border; final Color iconBg; final Color accent; const _AgentColors({ required this.gradient, required this.bg, required this.border, required this.iconBg, required this.accent, }); } class _AgentAction { final String label; final IconData icon; final bool isWide; final String? route; final String? action; const _AgentAction({required this.label, required this.icon, this.isWide = false, this.route, this.action}); } class _AgentFeature { final IconData icon; final String title; final String desc; const _AgentFeature(this.icon, this.title, this.desc); } class _ConfirmField { final String label; final String value; final IconData icon; final bool editable; const _ConfirmField({ required this.label, required this.value, this.icon = Icons.info_outline, this.editable = false, }); } final _agentActions = >{ ActiveAgent.health: [ _AgentAction(label: '查看趋势', icon: Icons.trending_up, isWide: true, route: 'trend'), ], ActiveAgent.diet: [ _AgentAction(label: '拍照识别', icon: Icons.camera_alt_outlined, isWide: true, action: 'pickFoodCamera'), _AgentAction(label: '上传照片', icon: Icons.photo_library_outlined, isWide: true, action: 'pickFoodGallery'), ], ActiveAgent.medication: [ _AgentAction(label: '用药管理', icon: Icons.medication_liquid_outlined, isWide: true, route: 'medications'), _AgentAction(label: '服药打卡', icon: Icons.check_circle_outline, isWide: true), ], ActiveAgent.consultation: [], ActiveAgent.report: [ _AgentAction(label: '上传报告', icon: Icons.upload_file_outlined, isWide: true, route: 'reports'), _AgentAction(label: '查看历史', icon: Icons.history_outlined, isWide: true, route: 'reports'), ], ActiveAgent.exercise: [ _AgentAction(label: '查看计划', icon: Icons.calendar_month_outlined, isWide: true, route: 'exercisePlan'), _AgentAction(label: '新建计划', icon: Icons.add_task_outlined, isWide: true, route: 'exerciseCreate'), ], }; extension _AgentActionsExt on ActiveAgent { List<_AgentAction> get actions => _agentActions[this] ?? [const _AgentAction(label: '开始对话', icon: Icons.chat_outlined)]; } // ════════════════════════════════════════════════════════════════ // 可展开的 AI 建议小组件 // ════════════════════════════════════════════════════════════════ class _ExpandableAdvice extends StatefulWidget { final String advice; const _ExpandableAdvice({required this.advice}); @override State<_ExpandableAdvice> createState() => _ExpandableAdviceState(); } class _ExpandableAdviceState extends State<_ExpandableAdvice> { bool _expanded = false; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => setState(() => _expanded = !_expanded), child: Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.backgroundSecondary, borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFE8E4FF), width: 0.8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.lightbulb_outline, size: 19, color: AppTheme.primary), const SizedBox(width: 6), const Text('AI 建议', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: AppTheme.primary)), const Spacer(), Icon(_expanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 21, color: AppColors.textHint), ], ), if (_expanded) ...[ const SizedBox(height: 10), Text(widget.advice, style: const TextStyle(fontSize: 16, color: Color(0xFF555555), height: 1.6)), ], ], ), ), ); } }