Skip to content

页面-心语精灵对话界面

字数
5674 字
阅读时间
30 分钟

版本:v1.0.0 | 更新日期:2025-10-08 页面路径:miniprogram/independent/heartchat/index 分包类型:独立分包 | 依赖云函数:askMessageManager

页面概述

心语精灵对话界面是MindGuard项目的核心AI交互功能,为用户提供7×24小时的智能心理陪伴服务。该页面集成了先进的多Agent对话系统、实时情绪分析、文化洞察和风险评估功能,通过自然语言交互为用户提供个性化的心理支持。

设计目标

  • 自然交互:提供流畅、自然的对话体验,降低用户使用门槛
  • 智能分析:实时分析用户情绪状态,提供深度心理洞察
  • 安全保障:集成风险评估机制,确保用户安全
  • 个性化服务:基于用户特点提供定制化回应和建议
  • 隐私保护:严格的数据隐私保护,让用户放心交流

页面架构

技术架构

┌─────────────────────────────────────────────────────────────┐
│                     心语精灵对话界面                           │
├─────────────────────────────────────────────────────────────┤
│  展示层 (Presentation Layer)                                 │
│  ├─ Agent选择面板 (AgentSelector)                             │
│  ├─ 对话消息列表 (MessageList)                               │
│  ├─ AI分析结果卡片 (AnalysisCards)                           │
│  ├─ 输入区域 (InputArea)                                    │
│  └─ 功能控制栏 (ControlBar)                                  │
├─────────────────────────────────────────────────────────────┤
│  逻辑层 (Logic Layer)                                        │
│  ├─ 对话状态管理 (ConversationManager)                        │
│  ├─ 消息处理 (MessageProcessor)                              │
│  ├─ 分析结果展示 (AnalysisRenderer)                          │
│  ├─ 安全监控 (SafetyMonitor)                                 │
│  └─ 缓存管理 (CacheManager)                                  │
├─────────────────────────────────────────────────────────────┤
│  数据层 (Data Layer)                                         │
│  ├─ 本地存储 (LocalStorage)                                  │
│  ├─ 云函数调用 (CloudFunction)                               │
│  ├─ 分析数据存储 (AnalysisData)                              │
│  └─ 用户配置 (UserPreferences)                              │
└─────────────────────────────────────────────────────────────┘

组件结构

HeartChatPage
├── AgentSelector (Agent选择面板)
│   ├── AgentCard (Agent卡片)
│   ├── AgentDescription (Agent介绍)
│   └── AgentHistory (对话历史)
├── MessageList (消息列表)
│   ├── UserMessage (用户消息)
│   ├── AgentMessage (AI回复)
│   ├── AnalysisCard (分析结果卡片)
│   └── SystemMessage (系统消息)
├── InputArea (输入区域)
│   ├── TextInput (文本输入)
│   ├── VoiceInput (语音输入)
│   ├── EmotionPicker (情绪选择)
│   └── QuickActions (快捷操作)
├── ControlBar (控制栏)
│   ├── SettingsButton (设置按钮)
│   ├── HistoryButton (历史记录)
│   ├── ShareButton (分享功能)
│   └── HelpButton (帮助中心)
└── SafetyPanel (安全面板) [按需显示]
    ├── RiskIndicator (风险指示器)
    ├── EmergencyContacts (紧急联系)
    └── ProfessionalResources (专业资源)

核心功能设计

1. Agent选择系统

Agent类型定义

javascript
const AgentTypes = {
  cbt_specialist: {
    name: 'CBT认知专家',
    avatar: '/assets/agents/cbt-specialist.png',
    description: '擅长认知行为疗法,帮助你识别和改变非理性思维模式',
    specialties: ['认知重构', '情绪管理', '行为激活'],
    suitable_for: ['焦虑情绪', '负面思维', '行为问题']
  },

  emotion_companion: {
    name: '情绪陪伴师',
    avatar: '/assets/agents/emotion-companion.png',
    description: '温暖的倾听者,提供情绪支持和共情理解',
    specialties: ['情绪支持', '共情倾听', '心理陪伴'],
    suitable_for: ['情绪低落', '孤独感', '需要陪伴']
  },

  stress_counselor: {
    name: '压力管理顾问',
    avatar: '/assets/agents/stress-counselor.png',
    description: '专业的压力管理专家,提供实用的减压技巧',
    specialties: ['压力评估', '放松训练', '时间管理'],
    suitable_for: ['学业压力', '工作压力', '生活压力']
  },

  relationship_guide: {
    name: '关系指导师',
    avatar: '/assets/agents/relationship-guide.png',
    description: '人际关系专家,帮助你改善人际沟通技巧',
    specialties: ['沟通技巧', '冲突解决', '关系建立'],
    suitable_for: ['人际困扰', '沟通问题', '关系改善']
  },

  wellness_coach: {
    name: '健康生活教练',
    avatar: '/assets/agents/wellness-coach.png',
    description: '全方位健康生活指导,关注身心协调发展',
    specialties: ['生活习惯', '运动建议', '营养指导'],
    suitable_for: ['生活规划', '健康咨询', '习惯养成']
  }
};

Agent选择界面

html
<!-- Agent选择面板 -->
<view class="agent-selector">
  <view class="selector-header">
    <text class="title">选择你的心语精灵</text>
    <text class="subtitle">每个精灵都有独特的专长,找到最适合你的陪伴</text>
  </view>

  <scroll-view class="agent-list" scroll-x="{{true}}">
    <view class="agent-cards">
      <view
        class="agent-card {{selectedAgent === agent.type ? 'selected' : ''}}"
        wx:for="{{availableAgents}}"
        wx:key="type"
        bindtap="selectAgent"
        data-agent="{{item}}"
      >
        <image class="agent-avatar" src="{{item.avatar}}" />
        <text class="agent-name">{{item.name}}</text>
        <text class="agent-desc">{{item.description}}</text>

        <view class="agent-tags">
          <text
            class="tag"
            wx:for="{{item.specialties}}"
            wx:key="*this"
          >{{item}}</text>
        </view>
      </view>
    </view>
  </scroll-view>

  <view class="selector-actions">
    <button class="btn-primary" bindtap="startConversation">
      开始对话
    </button>
  </view>
</view>

2. 对话消息系统

消息类型定义

javascript
const MessageTypes = {
  USER: 'user',           // 用户消息
  AGENT: 'agent',         // AI回复消息
  SYSTEM: 'system',       // 系统消息
  ANALYSIS: 'analysis',   // 分析结果卡片
  EMERGENCY: 'emergency'  // 紧急提示消息
};

const MessageStatus = {
  SENDING: 'sending',     // 发送中
  SENT: 'sent',           // 已发送
  DELIVERED: 'delivered', // 已送达
  READ: 'read',           // 已读
  FAILED: 'failed'        // 发送失败
};

消息列表组件

html
<!-- 消息列表 -->
<scroll-view
  class="message-list"
  scroll-y="{{true}}"
  scroll-into-view="{{scrollToMessage}}"
  bindscrolltolower="loadMoreMessages"
>
  <view class="message-container">
    <!-- 系统欢迎消息 -->
    <view class="message system-message" wx:if="{{showWelcome}}">
      <view class="message-content">
        <text class="welcome-text">
          欢迎来到心语精灵!我是{{selectedAgent.name}},{{selectedAgent.description}}
        </text>
      </view>
    </view>

    <!-- 消息循环 -->
    <view
      class="message {{item.type}}-message {{item.status}}"
      wx:for="{{messageList}}"
      wx:key="id"
      id="message-{{item.id}}"
    >
      <!-- 用户消息 -->
      <view class="user-message" wx:if="{{item.type === 'user'}}">
        <view class="message-content">
          <text class="message-text">{{item.content}}</text>
          <view class="message-time">{{item.time}}</view>
        </view>
        <image class="user-avatar" src="{{userInfo.avatar}}" />
      </view>

      <!-- AI回复消息 -->
      <view class="agent-message" wx:if="{{item.type === 'agent'}}">
        <image class="agent-avatar" src="{{selectedAgent.avatar}}" />
        <view class="message-content">
          <text class="message-text">{{item.content}}</text>
          <view class="message-actions">
            <button class="btn-text" bindtap="likeMessage" data-id="{{item.id}}">
              👍 有帮助
            </button>
            <button class="btn-text" bindtap="regenerateResponse" data-id="{{item.id}}">
              🔄 重新生成
            </button>
          </view>
          <view class="message-time">{{item.time}}</view>
        </view>
      </view>

      <!-- 分析结果卡片 -->
      <view class="analysis-card" wx:if="{{item.type === 'analysis'}}">
        <AnalysisCard
          analysis-data="{{item.analysisData}}"
          on-expand="onAnalysisCardExpand"
        />
      </view>

      <!-- 系统消息 -->
      <view class="system-message" wx:if="{{item.type === 'system'}}">
        <view class="message-content">
          <text class="system-text">{{item.content}}</text>
        </view>
      </view>

      <!-- 紧急提示 -->
      <view class="emergency-message" wx:if="{{item.type === 'emergency'}}">
        <view class="emergency-content">
          <view class="emergency-icon">⚠️</view>
          <text class="emergency-text">{{item.content}}</text>
          <view class="emergency-actions">
            <button class="btn-emergency" bindtap="handleEmergency">
              立即寻求帮助
            </button>
          </view>
        </view>
      </view>
    </view>

    <!-- 输入状态指示器 -->
    <view class="typing-indicator" wx:if="{{isAgentTyping}}">
      <image class="agent-avatar" src="{{selectedAgent.avatar}}" />
      <view class="typing-dots">
        <view class="dot"></view>
        <view class="dot"></view>
        <view class="dot"></view>
      </view>
    </view>
  </view>
</scroll-view>

3. AI分析结果展示

分析结果卡片组件

html
<!-- 分析结果卡片 -->
<view class="analysis-card {{isExpanded ? 'expanded' : 'collapsed'}}">
  <!-- 卡片头部 -->
  <view class="card-header" bindtap="toggleCard">
    <view class="analysis-icon">
      <image
        class="icon"
        src="/assets/icons/analysis-{{analysisData.type}}.png"
      />
    </view>
    <view class="analysis-title">
      <text class="title">{{getAnalysisTitle(analysisData.type)}}</text>
      <text class="subtitle">{{getAnalysisSubtitle(analysisData.type)}}</text>
    </view>
    <view class="expand-icon">
      <text class="icon">{{isExpanded ? '▲' : '▼'}}</text>
    </view>
  </view>

  <!-- 卡片内容 -->
  <view class="card-content" wx:if="{{isExpanded}}">
    <!-- 情感分析结果 -->
    <view class="emotion-analysis" wx:if="{{analysisData.type === 'emotion'}}">
      <view class="emotion-summary">
        <view class="primary-emotion">
          <text class="emotion-emoji">{{getEmotionEmoji(analysisData.primary_emotion)}}</text>
          <text class="emotion-text">{{analysisData.primary_emotion}}</text>
          <text class="emotion-intensity">{{analysisData.intensity * 100}}%</text>
        </view>

        <view class="secondary-emotions" wx:if="{{analysisData.secondary_emotions.length > 0}}">
          <text class="label">其他情绪:</text>
          <view class="emotion-tags">
            <text
              class="emotion-tag"
              wx:for="{{analysisData.secondary_emotions}}"
              wx:key="*this"
            >{{item}}</text>
          </view>
        </view>
      </view>

      <!-- 情绪雷达图 -->
      <view class="radar-chart-container">
        <canvas
          class="emotion-radar"
          canvas-id="emotionRadar{{cardId}}"
          bind:tap="showRadarDetail"
        />
      </view>

      <!-- 情绪建议 -->
      <view class="emotion-suggestions">
        <text class="suggestions-title">💡 情绪建议</text>
        <view class="suggestions-list">
          <text
            class="suggestion-item"
            wx:for="{{getEmotionSuggestions(analysisData)}}"
            wx:key="*this"
          >• {{item}}</text>
        </view>
      </view>
    </view>

    <!-- 文化洞察结果 -->
    <view class="culture-analysis" wx:if="{{analysisData.type === 'culture'}}">
      <view class="culture-keywords">
        <text class="keywords-title">🏮 文化关键词</text>
        <view class="keywords-list">
          <text
            class="keyword-item"
            wx:for="{{analysisData.topic_keywords}}"
            wx:key="*this"
          >{{item}}</text>
        </view>
      </view>

      <view class="culture-triggers">
        <text class="triggers-title">💭 情绪触发点</text>
        <view class="triggers-list">
          <text
            class="trigger-item"
            wx:for="{{analysisData.emotion_triggers}}"
            wx:key="*this"
          >{{item}}</text>
        </view>
      </view>

      <view class="culture-context">
        <text class="context-title">📖 文化背景解读</text>
        <text class="context-text">{{analysisData.cultural_context_notes}}</text>
      </view>

      <view class="culture-advice">
        <text class="advice-title">🎯 个性化建议</text>
        <view class="advice-list">
          <text
            class="advice-item"
            wx:for="{{getCulturalSuggestions(analysisData)}}"
            wx:key="*this"
          >• {{item}}</text>
        </view>
      </view>
    </view>

    <!-- 风险评估结果 -->
    <view class="risk-analysis" wx:if="{{analysisData.type === 'risk'}}">
      <view class="risk-level {{getRiskLevelClass(analysisData.risk_level)}}">
        <view class="risk-icon">{{getRiskIcon(analysisData.risk_level)}}</view>
        <view class="risk-info">
          <text class="risk-title">{{getRiskTitle(analysisData.risk_level)}}</text>
          <text class="risk-description">{{getRiskDescription(analysisData.risk_level)}}</text>
        </view>
      </view>

      <view class="risk-summary" wx:if="{{analysisData.summary}}">
        <text class="summary-title">📋 风险评估摘要</text>
        <text class="summary-text">{{analysisData.summary}}</text>
      </view>

      <!-- 高风险时显示紧急资源 -->
      <view class="emergency-resources" wx:if="{{analysisData.risk_level >= 3}}">
        <text class="resources-title">🚨 紧急支持资源</text>
        <view class="resources-list">
          <view
            class="resource-item"
            wx:for="{{getEmergencyResources()}}"
            wx:key="name"
            bindtap="contactResource"
            data-resource="{{item}}"
          >
            <text class="resource-name">{{item.name}}</text>
            <text class="resource-contact">{{item.contact}}</text>
          </view>
        </view>
      </view>
    </view>
  </view>
</view>

情绪雷达图实现

javascript
// emotion-radar.js
Component({
  properties: {
    analysisData: {
      type: Object,
      value: {}
    }
  },

  data: {
    radarData: null,
    animationProgress: 0
  },

  lifetimes: {
    attached() {
      this.initRadarChart();
    }
  },

  methods: {
    initRadarChart() {
      const { radar_dimensions } = this.properties.analysisData;

      if (!radar_dimensions) return;

      // 转换数据格式
      this.radarData = {
        trust: radar_dimensions.trust * 100,
        openness: radar_dimensions.openness * 100,
        resistance: radar_dimensions.resistance * 100,
        stress: radar_dimensions.stress * 100,
        control: radar_dimensions.control * 100
      };

      // 启动动画
      this.animateRadar();
    },

    animateRadar() {
      const animationDuration = 1000; // 1秒动画
      const startTime = Date.now();

      const animate = () => {
        const elapsed = Date.now() - startTime;
        const progress = Math.min(elapsed / animationDuration, 1);

        // 使用缓动函数
        const easeProgress = this.easeInOutCubic(progress);
        this.setData({ animationProgress: easeProgress });

        if (progress < 1) {
          this.animationFrameId = requestAnimationFrame(animate);
        } else {
          this.drawRadarChart();
        }
      };

      animate();
    },

    drawRadarChart() {
      const ctx = wx.createCanvasContext('emotionRadar');
      const centerX = 100;
      const centerY = 100;
      const radius = 80;

      // 绘制网格
      this.drawGrid(ctx, centerX, centerY, radius);

      // 绘制数据
      this.drawData(ctx, centerX, centerY, radius, this.radarData);

      ctx.draw();
    },

    drawGrid(ctx, centerX, centerY, radius) {
      const levels = 5;
      const dimensions = 5;

      // 绘制同心圆
      for (let i = 1; i <= levels; i++) {
        const levelRadius = (radius / levels) * i;

        ctx.beginPath();
        for (let j = 0; j <= dimensions; j++) {
          const angle = (Math.PI * 2 / dimensions) * j - Math.PI / 2;
          const x = centerX + Math.cos(angle) * levelRadius;
          const y = centerY + Math.sin(angle) * levelRadius;

          if (j === 0) {
            ctx.moveTo(x, y);
          } else {
            ctx.lineTo(x, y);
          }
        }
        ctx.closePath();
        ctx.setStrokeStyle('rgba(200, 200, 200, 0.3)');
        ctx.stroke();
      }

      // 绘制轴线
      for (let i = 0; i < dimensions; i++) {
        const angle = (Math.PI * 2 / dimensions) * i - Math.PI / 2;
        const x = centerX + Math.cos(angle) * radius;
        const y = centerY + Math.sin(angle) * radius;

        ctx.beginPath();
        ctx.moveTo(centerX, centerY);
        ctx.lineTo(x, y);
        ctx.setStrokeStyle('rgba(150, 150, 150, 0.5)');
        ctx.stroke();
      }
    },

    drawData(ctx, centerX, centerY, radius, data) {
      const dimensions = Object.keys(data);
      const angleStep = (Math.PI * 2) / dimensions;

      // 绘制数据多边形
      ctx.beginPath();
      dimensions.forEach((dimension, index) => {
        const angle = angleStep * index - Math.PI / 2;
        const value = data[dimension] / 100; // 归一化到0-1
        const distance = value * radius * this.data.animationProgress;
        const x = centerX + Math.cos(angle) * distance;
        const y = centerY + Math.sin(angle) * distance;

        if (index === 0) {
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      });
      ctx.closePath();

      // 填充和描边
      ctx.setFillStyle('rgba(74, 144, 226, 0.3)');
      ctx.fill();
      ctx.setStrokeStyle('#4a90e2');
      ctx.stroke();

      // 绘制数据点
      dimensions.forEach((dimension, index) => {
        const angle = angleStep * index - Math.PI / 2;
        const value = data[dimension] / 100;
        const distance = value * radius * this.data.animationProgress;
        const x = centerX + Math.cos(angle) * distance;
        const y = centerY + Math.sin(angle) * distance;

        ctx.beginPath();
        ctx.arc(x, y, 4, 0, Math.PI * 2);
        ctx.setFillStyle('#4a90e2');
        ctx.fill();
        ctx.setStrokeStyle('white');
        ctx.stroke();
      });
    },

    easeInOutCubic(t) {
      return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
    },

    showRadarDetail() {
      this.triggerEvent('showDetail', {
        radarData: this.radarData
      });
    }
  }
});

4. 输入系统设计

多模态输入组件

html
<!-- 输入区域 -->
<view class="input-area">
  <!-- 快捷情绪选择 -->
  <view class="emotion-picker" wx:if="{{showEmotionPicker}}">
    <scroll-view class="emotion-list" scroll-x="{{true}}">
      <view class="emotion-items">
        <view
          class="emotion-item {{selectedEmotion === item.type ? 'selected' : ''}}"
          wx:for="{{emotionOptions}}"
          wx:key="type"
          bindtap="selectEmotion"
          data-emotion="{{item}}"
        >
          <text class="emotion-emoji">{{item.emoji}}</text>
          <text class="emotion-label">{{item.label}}</text>
        </view>
      </view>
    </scroll-view>
  </view>

  <!-- 主输入框 -->
  <view class="input-container">
    <!-- 语音输入按钮 -->
    <button
      class="voice-btn {{isRecording ? 'recording' : ''}}"
      bindtouchstart="startRecording"
      bindtouchend="stopRecording"
      wx:if="{{!inputText}}"
    >
      <text class="voice-icon">🎤</text>
    </button>

    <!-- 文本输入框 -->
    <input
      class="text-input"
      type="text"
      placeholder="分享你的想法..."
      value="{{inputText}}"
      bindinput="onInputChange"
      bindconfirm="sendMessage"
      focus="{{inputFocused}}"
      maxlength="500"
    />

    <!-- 表情按钮 -->
    <button
      class="emoji-btn"
      bindtap="toggleEmotionPicker"
      wx:if="{{!inputText}}"
    >
      <text class="emoji-icon">😊</text>
    </button>

    <!-- 发送按钮 -->
    <button
      class="send-btn {{inputText ? 'active' : ''}}"
      bindtap="sendMessage"
      disabled="{{!inputText}}"
    >
      <text class="send-icon">➤</text>
    </button>
  </view>

  <!-- 快捷操作栏 -->
  <view class="quick-actions">
    <button
      class="quick-action"
      wx:for="{{quickActions}}"
      wx:key="type"
      bindtap="sendQuickAction"
      data-action="{{item}}"
    >
      <text class="action-icon">{{item.icon}}</text>
      <text class="action-label">{{item.label}}</text>
    </button>
  </view>
</view>

输入处理逻辑

javascript
// input-handler.js
Page({
  data: {
    inputText: '',
    isRecording: false,
    showEmotionPicker: false,
    selectedEmotion: null,
    emotionOptions: [
      { type: 'happy', emoji: '😊', label: '开心' },
      { type: 'sad', emoji: '😢', label: '难过' },
      { type: 'angry', emoji: '😠', label: '生气' },
      { type: 'anxious', emoji: '😰', label: '焦虑' },
      { type: 'confused', emoji: '😕', label: '困惑' },
      { type: 'calm', emoji: '😌', label: '平静' }
    ],
    quickActions: [
      { type: 'mood_report', icon: '📊', label: '心情报告' },
      { type: 'breathing', icon: '🫁', label: '呼吸练习' },
      { type: 'gratitude', icon: '🙏', label: '感恩日记' },
      { type: 'goal_setting', icon: '🎯', label: '目标设定' }
    ]
  },

  // 文本输入处理
  onInputChange(e) {
    this.setData({
      inputText: e.detail.value
    });
  },

  // 发送消息
  async sendMessage() {
    const { inputText, selectedEmotion } = this.data;

    if (!inputText.trim()) return;

    // 构建消息内容
    const messageContent = this.buildMessageContent(inputText, selectedEmotion);

    // 添加用户消息到列表
    this.addUserMessage(messageContent);

    // 清空输入
    this.setData({
      inputText: '',
      selectedEmotion: null,
      showEmotionPicker: false
    });

    // 发送到AI处理
    await this.processMessage(messageContent);
  },

  // 构建消息内容
  buildMessageContent(text, emotion) {
    let content = text;

    if (emotion) {
      content = `[${emotion.label}] ${content}`;
    }

    return content;
  },

  // 处理消息(调用云函数)
  async processMessage(content) {
    try {
      // 显示AI正在输入
      this.setData({ isAgentTyping: true });

      // 调用云函数
      const result = await wx.cloud.callFunction({
        name: 'askMessageManager',
        data: {
          action: 'sendMessage',
          message: content,
          sessionId: this.data.currentSessionId,
          agentType: this.data.selectedAgent.type
        }
      });

      const { response, analysis } = result.result;

      // 添加AI回复消息
      this.addAgentMessage(response);

      // 添加分析结果卡片
      if (analysis && analysis.length > 0) {
        this.addAnalysisCards(analysis);
      }

    } catch (error) {
      console.error('消息处理失败:', error);
      this.addSystemMessage('抱歉,我现在无法回复。请稍后再试。');
    } finally {
      this.setData({ isAgentTyping: false });
    }
  },

  // 语音输入处理
  startRecording(e) {
    if (!this.checkRecordPermission()) {
      return;
    }

    this.setData({ isRecording: true });

    // 开始录音
    wx.startRecord({
      success: (res) => {
        console.log('录音开始');
      },
      fail: (err) => {
        console.error('录音失败:', err);
        this.setData({ isRecording: false });
      }
    });
  },

  stopRecording() {
    if (!this.data.isRecording) return;

    wx.stopRecord({
      success: (res) => {
        const tempFilePath = res.tempFilePath;
        this.processVoiceMessage(tempFilePath);
      },
      fail: (err) => {
        console.error('录音结束失败:', err);
      }
    });

    this.setData({ isRecording: false });
  },

  // 处理语音消息
  async processVoiceMessage(filePath) {
    try {
      // 语音转文字
      const transcription = await this.transcribeVoice(filePath);

      if (transcription) {
        this.setData({ inputText: transcription });
        // 自动发送语音转文字的结果
        await this.sendMessage();
      }
    } catch (error) {
      console.error('语音处理失败:', error);
      this.addSystemMessage('语音识别失败,请重试或使用文字输入。');
    }
  },

  // 语音转文字
  transcribeVoice(filePath) {
    return new Promise((resolve, reject) => {
      wx.cloud.callFunction({
        name: 'askMessageManager',
        data: {
          action: 'transcribeVoice',
          filePath: filePath
        },
        success: (res) => {
          resolve(res.result.text);
        },
        fail: (err) => {
          reject(err);
        }
      });
    });
  },

  // 情绪选择处理
  selectEmotion(e) {
    const emotion = e.currentTarget.dataset.emotion;
    this.setData({
      selectedEmotion: emotion,
      showEmotionPicker: false
    });
  },

  // 快捷操作处理
  sendQuickAction(e) {
    const action = e.currentTarget.dataset.action;

    const quickMessages = {
      mood_report: '我想了解一下我的情绪状态',
      breathing: '我现在压力很大,想做一些呼吸练习',
      gratitude: '今天有什么值得感恩的事情吗?',
      goal_setting: '帮我设定一个小目标'
    };

    const message = quickMessages[action.type];
    if (message) {
      this.setData({ inputText: message });
      this.sendMessage();
    }
  }
});

5. 安全监控系统

风险监控组件

html
<!-- 安全监控面板 -->
<view class="safety-panel {{riskLevel > 0 ? 'visible' : 'hidden'}}">
  <!-- 风险指示器 -->
  <view class="risk-indicator level-{{riskLevel}}">
    <view class="risk-icon">{{getRiskIcon(riskLevel)}}</view>
    <view class="risk-info">
      <text class="risk-title">{{getRiskTitle(riskLevel)}}</text>
      <text class="risk-description">{{getRiskDescription(riskLevel)}}</text>
    </view>
  </view>

  <!-- 紧急操作(高风险时显示) -->
  <view class="emergency-actions" wx:if="{{riskLevel >= 3}}">
    <button
      class="emergency-contact"
      bindtap="contactEmergencyService"
    >
      📞 联系心理热线
    </button>
    <button
      class="emergency-location"
      bindtap="shareLocation"
    >
      📍 分享位置
    </button>
    <button
      class="emergency-contact-person"
      bindtap="contactEmergencyPerson"
    >
      👥 联系紧急联系人
    </button>
  </view>

  <!-- 安抚资源(低中风险时显示) -->
  <view class="calming-resources" wx:if="{{riskLevel < 3}}">
    <button
      class="calming-action"
      bindtap="startBreathingExercise"
    >
      🫁 呼吸练习
    </button>
    <button
      class="calming-action"
      bindtap="playCalmingMusic"
    >
      🎵 放松音乐
    </button>
    <button
      class="calming-action"
      bindtap="showPositiveThoughts"
    >
      💭 积极思考
    </button>
  </view>
</view>

安全监控逻辑

javascript
// safety-monitor.js
const SafetyMonitor = {
  // 风险等级评估
  assessRiskLevel(analysisResults) {
    let maxRiskLevel = 0;
    let riskFactors = [];

    analysisResults.forEach(analysis => {
      if (analysis.type === 'risk') {
        maxRiskLevel = Math.max(maxRiskLevel, analysis.risk_level);

        if (analysis.risk_level >= 2) {
          riskFactors.push({
            type: 'risk_assessment',
            level: analysis.risk_level,
            details: analysis.summary
          });
        }
      }
    });

    return {
      level: maxRiskLevel,
      factors: riskFactors,
      requiresIntervention: maxRiskLevel >= 3
    };
  },

  // 触发安全响应
  triggerSafetyResponse(riskAssessment) {
    const { level, factors, requiresIntervention } = riskAssessment;

    switch (level) {
      case 0:
        // 无风险,正常监控
        this.startNormalMonitoring();
        break;

      case 1:
        // 轻度风险,增加关注
        this.startEnhancedMonitoring();
        break;

      case 2:
        // 中度风险,主动干预
        this.startProactiveIntervention();
        break;

      case 3:
        // 高度风险,紧急干预
        this.startEmergencyIntervention();
        break;

      case 4:
        // 紧急危机,立即响应
        this.triggerCrisisResponse();
        break;
    }
  },

  // 紧急干预流程
  startEmergencyIntervention() {
    // 显示紧急资源面板
    this.showEmergencyPanel();

    // 记录安全事件
    this.logSafetyEvent({
      level: 3,
      timestamp: new Date().toISOString(),
      actions: ['show_emergency_resources', 'increase_monitoring']
    });

    // 通知安全团队(如果是生产环境)
    if (this.isProduction()) {
      this.notifySafetyTeam();
    }
  },

  // 危机响应流程
  triggerCrisisResponse() {
    // 立即联系紧急服务
    this.contactEmergencyServices();

    // 通知紧急联系人
    this.notifyEmergencyContacts();

    // 保持用户在线
    this.keepUserOnline();

    // 记录危机事件
    this.logCrisisEvent({
      level: 4,
      timestamp: new Date().toISOString(),
      actions: ['contact_emergency_services', 'notify_contacts', 'keep_online']
    });
  },

  // 联系紧急服务
  async contactEmergencyServices() {
    const emergencyNumbers = [
      { name: '心理援助热线', phone: '400-161-9995' },
      { name: '危机干预中心', phone: '010-82951332' }
    ];

    for (const service of emergencyNumbers) {
      try {
        await wx.makePhoneCall({
          phoneNumber: service.phone
        });
        break; // 成功拨打电话后退出循环
      } catch (error) {
        console.error(`拨打${service.name}失败:`, error);
      }
    }
  },

  // 分享位置信息
  async shareLocation() {
    try {
      const location = await this.getCurrentLocation();

      // 发送位置给紧急联系人
      await this.sendLocationToContacts(location);

      // 显示位置信息给用户
      this.showLocationInfo(location);

    } catch (error) {
      console.error('获取位置失败:', error);
      wx.showToast({
        title: '位置获取失败',
        icon: 'none'
      });
    }
  },

  // 获取当前位置
  getCurrentLocation() {
    return new Promise((resolve, reject) => {
      wx.getLocation({
        type: 'gcj02',
        success: (res) => {
          resolve({
            latitude: res.latitude,
            longitude: res.longitude,
            accuracy: res.accuracy,
            timestamp: new Date().toISOString()
          });
        },
        fail: reject
      });
    });
  }
};

样式设计

主要样式规范

scss
// 心语精灵对话界面样式
.heartchat-page {
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

  // Agent选择面板
  .agent-selector {
    background: rgba(255, 255, 255, 0.95);
    border-radius: 16px;
    margin: 16px;
    padding: 20px;

    .selector-header {
      text-align: center;
      margin-bottom: 20px;

      .title {
        font-size: 18px;
        font-weight: 600;
        color: #2c3e50;
        margin-bottom: 8px;
      }

      .subtitle {
        font-size: 14px;
        color: #7f8c8d;
        line-height: 1.4;
      }
    }

    .agent-cards {
      display: flex;
      gap: 16px;
      padding: 0 8px;

      .agent-card {
        min-width: 120px;
        padding: 16px;
        border-radius: 12px;
        background: white;
        border: 2px solid transparent;
        transition: all 0.3s ease;

        &.selected {
          border-color: #4a90e2;
          background: #f0f8ff;
        }

        .agent-avatar {
          width: 48px;
          height: 48px;
          border-radius: 50%;
          margin: 0 auto 12px;
        }

        .agent-name {
          font-size: 14px;
          font-weight: 600;
          color: #2c3e50;
          text-align: center;
          display: block;
          margin-bottom: 8px;
        }

        .agent-desc {
          font-size: 12px;
          color: #7f8c8d;
          line-height: 1.3;
          text-align: center;
          display: block;
          margin-bottom: 12px;
        }

        .agent-tags {
          display: flex;
          flex-wrap: wrap;
          gap: 4px;

          .tag {
            font-size: 10px;
            padding: 2px 6px;
            background: #e8f4fd;
            color: #4a90e2;
            border-radius: 4px;
          }
        }
      }
    }
  }

  // 消息列表
  .message-list {
    flex: 1;
    padding: 0 16px;

    .message {
      margin-bottom: 16px;

      &.user-message {
        display: flex;
        justify-content: flex-end;

        .message-content {
          max-width: 70%;
          background: white;
          border-radius: 16px 16px 4px 16px;
          padding: 12px 16px;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

          .message-text {
            font-size: 15px;
            color: #2c3e50;
            line-height: 1.4;
          }

          .message-time {
            font-size: 11px;
            color: #bdc3c7;
            margin-top: 4px;
            text-align: right;
          }
        }

        .user-avatar {
          width: 32px;
          height: 32px;
          border-radius: 50%;
          margin-left: 8px;
        }
      }

      &.agent-message {
        display: flex;
        justify-content: flex-start;

        .agent-avatar {
          width: 32px;
          height: 32px;
          border-radius: 50%;
          margin-right: 8px;
        }

        .message-content {
          max-width: 70%;
          background: #f8f9fa;
          border-radius: 16px 16px 16px 4px;
          padding: 12px 16px;
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

          .message-text {
            font-size: 15px;
            color: #2c3e50;
            line-height: 1.4;
          }

          .message-actions {
            margin-top: 8px;
            display: flex;
            gap: 12px;

            .btn-text {
              font-size: 12px;
              color: #4a90e2;
              background: transparent;
              border: none;
              padding: 0;

              &:active {
                opacity: 0.6;
              }
            }
          }

          .message-time {
            font-size: 11px;
            color: #bdc3c7;
            margin-top: 8px;
          }
        }
      }

      &.analysis-card {
        margin: 20px 0;
      }
    }

    // 输入状态指示器
    .typing-indicator {
      display: flex;
      align-items: center;
      margin-bottom: 16px;

      .agent-avatar {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        margin-right: 8px;
      }

      .typing-dots {
        background: #f8f9fa;
        border-radius: 16px;
        padding: 12px 16px;
        display: flex;
        gap: 4px;

        .dot {
          width: 6px;
          height: 6px;
          border-radius: 50%;
          background: #bdc3c7;
          animation: typing 1.4s infinite;

          &:nth-child(2) {
            animation-delay: 0.2s;
          }

          &:nth-child(3) {
            animation-delay: 0.4s;
          }
        }
      }
    }
  }

  // 输入区域
  .input-area {
    background: white;
    border-radius: 20px 20px 0 0;
    padding: 16px;
    box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);

    .emotion-picker {
      margin-bottom: 12px;

      .emotion-items {
        display: flex;
        gap: 12px;

        .emotion-item {
          display: flex;
          flex-direction: column;
          align-items: center;
          padding: 8px;
          border-radius: 8px;
          background: #f8f9fa;
          transition: all 0.2s ease;

          &.selected {
            background: #e8f4fd;
            border: 1px solid #4a90e2;
          }

          .emotion-emoji {
            font-size: 20px;
            margin-bottom: 4px;
          }

          .emotion-label {
            font-size: 10px;
            color: #7f8c8d;
          }
        }
      }
    }

    .input-container {
      display: flex;
      align-items: center;
      gap: 8px;
      background: #f8f9fa;
      border-radius: 25px;
      padding: 8px 16px;

      .voice-btn, .emoji-btn {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        background: transparent;
        border: none;
        display: flex;
        align-items: center;
        justify-content: center;

        &.recording {
          background: #e74c3c;
          animation: pulse 1s infinite;
        }
      }

      .text-input {
        flex: 1;
        background: transparent;
        border: none;
        font-size: 15px;
        color: #2c3e50;
        padding: 8px 0;
      }

      .send-btn {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        background: #4a90e2;
        border: none;
        color: white;
        display: flex;
        align-items: center;
        justify-content: center;

        &:not(.active) {
          background: #bdc3c7;
        }
      }
    }

    .quick-actions {
      display: flex;
      gap: 8px;
      margin-top: 12px;

      .quick-action {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        padding: 8px;
        background: #f8f9fa;
        border-radius: 8px;
        border: none;

        .action-icon {
          font-size: 16px;
          margin-bottom: 4px;
        }

        .action-label {
          font-size: 10px;
          color: #7f8c8d;
        }
      }
    }
  }

  // 安全面板
  .safety-panel {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: white;
    border-radius: 16px;
    padding: 20px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
    z-index: 1000;
    min-width: 300px;

    .risk-indicator {
      display: flex;
      align-items: center;
      margin-bottom: 16px;

      .risk-icon {
        font-size: 24px;
        margin-right: 12px;
      }

      .risk-info {
        .risk-title {
          font-size: 16px;
          font-weight: 600;
          color: #2c3e50;
          margin-bottom: 4px;
        }

        .risk-description {
          font-size: 14px;
          color: #7f8c8d;
        }
      }
    }

    .emergency-actions, .calming-resources {
      display: flex;
      flex-direction: column;
      gap: 8px;

      button {
        padding: 12px 16px;
        border-radius: 8px;
        border: none;
        font-size: 14px;
        transition: all 0.2s ease;

        &:active {
          opacity: 0.8;
        }
      }
    }

    .emergency-actions {
      button {
        background: #e74c3c;
        color: white;
      }
    }

    .calming-resources {
      button {
        background: #3498db;
        color: white;
      }
    }
  }
}

// 动画定义
@keyframes typing {
  0%, 60%, 100% {
    opacity: 0.3;
  }
  30% {
    opacity: 1;
  }
}

@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7);
  }
  70% {
    box-shadow: 0 0 0 10px rgba(231, 76, 60, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(231, 76, 60, 0);
  }
}

数据流设计

页面数据结构

javascript
// 页面数据结构
const pageData = {
  // Agent相关
  selectedAgent: null,
  availableAgents: [],
  agentHistory: [],

  // 会话相关
  currentSessionId: null,
  messageList: [],
  isLoading: false,
  isAgentTyping: false,

  // 输入相关
  inputText: '',
  selectedEmotion: null,
  showEmotionPicker: false,
  isRecording: false,

  // 分析结果
  analysisResults: [],
  currentRiskLevel: 0,
  safetyPanelVisible: false,

  // 用户信息
  userInfo: {
    avatar: '',
    nickname: ''
  },

  // 配置
  scrollToMessage: '',
  showWelcome: true,

  // 缓存
  messageCache: new Map(),
  analysisCache: new Map()
};

消息数据结构

javascript
// 消息数据结构
const messageStructure = {
  id: 'unique_message_id',
  type: 'user|agent|system|analysis|emergency',
  content: 'message_content',
  timestamp: '2025-10-08T15:30:00Z',
  status: 'sending|sent|delivered|read|failed',

  // 用户消息特有
  userInfo: {
    avatar: 'avatar_url',
    nickname: 'user_nickname'
  },

  // Agent消息特有
  agentInfo: {
    type: 'cbt_specialist',
    name: 'CBT认知专家',
    avatar: 'agent_avatar_url'
  },

  // 分析结果特有
  analysisData: {
    type: 'emotion|culture|risk',
    data: { /* 分析结果数据 */ },
    confidence: 0.95
  },

  // 元数据
  metadata: {
    sessionId: 'session_id',
    messageId: 'original_message_id',
    processingTime: 250,
    modelVersion: 'v2.1.0'
  }
};

性能优化策略

1. 消息列表优化

  • 虚拟滚动:大量消息时使用虚拟滚动技术
  • 图片懒加载:头像和图片懒加载
  • 消息缓存:本地缓存消息数据,减少网络请求

2. 分析结果优化

  • 异步加载:分析结果异步加载和渲染
  • 数据压缩:压缩分析数据传输量
  • 缓存策略:智能缓存常用分析结果

3. 输入体验优化

  • 输入预测:基于历史数据提供输入建议
  • 语音识别优化:优化语音识别准确率
  • 快捷操作:提供常用短语快捷输入

测试策略

功能测试

javascript
// Agent选择测试
describe('Agent Selection', () => {
  test('should select agent successfully', () => {
    const agent = { type: 'cbt_specialist', name: 'CBT认知专家' };
    page.selectAgent(agent);

    expect(page.data.selectedAgent).toEqual(agent);
  });

  test('should load agent history', async () => {
    await page.loadAgentHistory();
    expect(page.data.agentHistory.length).toBeGreaterThan(0);
  });
});

// 消息发送测试
describe('Message Sending', () => {
  test('should send text message successfully', async () => {
    page.setData({ inputText: 'Hello, I need help' });
    await page.sendMessage();

    const lastMessage = page.data.messageList[page.data.messageList.length - 1];
    expect(lastMessage.type).toBe('user');
    expect(lastMessage.content).toContain('Hello, I need help');
  });

  test('should handle voice message input', async () => {
    // Mock voice recording
    const mockVoiceData = 'mock_voice_data';
    await page.processVoiceMessage(mockVoiceData);

    expect(page.data.inputText).toBeDefined();
  });
});

// 分析结果展示测试
describe('Analysis Display', () => {
  test('should display emotion analysis correctly', () => {
    const analysisData = {
      type: 'emotion',
      primary_emotion: 'happy',
      radar_dimensions: {
        trust: 0.8,
        openness: 0.7,
        resistance: 0.2,
        stress: 0.3,
        control: 0.9
      }
    };

    page.addAnalysisCards([analysisData]);

    const analysisCard = page.data.messageList.find(msg => msg.type === 'analysis');
    expect(analysisCard).toBeDefined();
    expect(analysisCard.analysisData.type).toBe('emotion');
  });
});

性能测试

javascript
// 性能测试
describe('Performance Tests', () => {
  test('should handle large message list efficiently', () => {
    const startTime = performance.now();

    // 添加1000条消息
    for (let i = 0; i < 1000; i++) {
      page.addMockMessage();
    }

    const endTime = performance.now();
    const renderTime = endTime - startTime;

    expect(renderTime).toBeLessThan(1000); // 1秒内完成渲染
  });

  test('should cache analysis results effectively', () => {
    const analysisData = { type: 'emotion', data: {} };

    const startTime = performance.now();
    page.cacheAnalysisResult('test_key', analysisData);
    const cachedResult = page.getCachedAnalysisResult('test_key');
    const endTime = performance.now();

    const cacheTime = endTime - startTime;
    expect(cacheTime).toBeLessThan(10); // 10ms内完成缓存操作
    expect(cachedResult).toEqual(analysisData);
  });
});

相关文档

版本记录

版本日期变更内容作者
v1.0.02025-10-08初始版本,完成心语精灵对话界面核心设计哈雷酱

贡献者

The avatar of contributor named as Cai Hongyu Cai Hongyu

文件历史

撰写