Files
eryao/docs/bugs/2026-04-08-followup-loading-and-voice-bug.md
T
qzl e80a82bef4 docs: 更新协议文档,删除废弃计划文档
- 更新 http-error-codes, user-points-chat-data-protocol
- 更新 divination-run-protocol, profile-protocol
- 删除废弃的后端和前端设计计划文档
2026-04-08 17:23:02 +08:00

7.6 KiB
Raw Blame History

Bug: 追问页面 UI 问题

日期:2026-04-08 状态:已确认(未修复)

Bug 1: 追问页发送时顶部和消息下方重复加载UI

问题描述

在追问页面,用户输入信息发送后,顶部和消息下方同时出现加载UI,但顶部的加载UI是多余的。

根因分析

文件apps/lib/features/divination/presentation/screens/follow_up_chat_screen.dart

问题代码

  1. 顶部 step 指示器(第 85-124 行):
if (_sending && _currentStepName != null)
  Container(
    // 显示 step 进度,如 "解读中..."、"推理中..."
    child: Row(
      children: [
        CircularProgressIndicator(...),
        Text(_stepLabel(_currentStepName!)),
      ],
    ),
  )
  1. 消息下方 streaming placeholder(第 565-584 行):
isStreamingPlaceholder
    ? Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          CircularProgressIndicator(...),
          Text(l10n.followUpGenerating),  // "生成中..."
        ],
      )

事件触发流程

顺序 事件 顶部指示器 消息下方
1 _submitText() 调用 streaming placeholder 显示 "生成中..."
2 STEP_STARTED(stepName='divination') 显示 "解读中..." 继续显示
3 STEP_FINISHED 消失 继续显示
4 STEP_STARTED(stepName='worker') 显示 "推理中..." 同时显示 "生成中..."
5 STEP_FINISHED 消失 继续显示
6 TEXT_MESSAGE_END placeholder 消失,显示真实内容

问题:在 worker 阶段(步骤 4),顶部显示 "推理中..." 同时消息下方显示 "生成中...",造成重复反馈。用户只需要一个加载反馈即可。

修复方向

  1. 方案A:只在 divination 阶段显示顶部 step 指示器,worker 阶段隐藏(因为 streaming placeholder 已经提供反馈)

    // 第 85 行修改
    if (_sending && _currentStepName != null && _currentStepName != 'worker')
    
  2. 方案B:移除顶部 step 指示器,完全依赖 streaming placeholder

  3. 方案C:在 worker 阶段隐藏 streaming placeholder,只显示顶部指示器

推荐 方案A,因为:

  • divination 阶段没有 streaming placeholder,需要顶部指示器提供反馈
  • worker 阶段有 streaming placeholder,不需要顶部指示器
  • 改动最小,只加一个条件

Bug 2: 语音模式录制时缺少动画UI

问题描述

根据 social-app 的实现,按住说话模式下录音时应该有动画效果,但 eryao 的实现只有文字,没有动画。

根因分析

对比

功能 social-app eryao
录音中动画 recordingAnimation widget(外部传入) 只有文字 "录音中..."
录音提示文字 带动画的 widget 只有文字
录音中隐藏输入区域 IgnorePointer + Opacity 未实现

eryao 当前实现message_composer.dart:159-172):

if (_isRecording) {
  if (!showRecordingInlineFeedback) {
    return Text(recordingText, ...);
  }
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      const SizedBox(width: 16, height: 16),  // 空白占位
      const SizedBox(height: AppSpacing.xs),
      Text(recordingText, ...),               // "录音中..."
      const SizedBox(height: AppSpacing.xs),
      Text(recordingHintText, ...),           // "上滑取消"
    ],
  );
}

social-app 实现message_composer.dart:222-239):

if (_isRecording) {
  if (!showRecordingInlineFeedback) {
    return Text(resolvedRecordingText, ...);
  }
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      recordingAnimation,                      // 动画widget(外部传入)
      const SizedBox(height: AppSpacing.xs),
      Text(resolvedRecordingText, ...),
      const SizedBox(height: AppSpacing.xs),
      Text(resolvedRecordingHintText, ...),
    ],
  );
}

差异

  1. recordingAnimation widget 缺失 - 需要外部传入,eryao 直接用空白占位
  2. 录音中图标/按钮没有动画效果

修复方向

  1. FollowUpChatScreen 或其父组件中创建 recordingAnimation widget
  2. recordingAnimation 通过 MessageComposer 传入
  3. 参考 social-app 的实现,使用脉冲动画或波形动画

参考实现(需要添加 recording_animation.dart):

class RecordingAnimation extends StatefulWidget {
  final double size;
  final Color color;
  
  @override
  State<RecordingAnimation> createState() => _RecordingAnimationState();
}

class _RecordingAnimationState extends State<RecordingAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 1000),
      vsync: this,
    )..repeat(reverse: true);
    _scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _scaleAnimation,
      builder: (context, child) {
        return Transform.scale(
          scale: _scaleAnimation.value,
          child: Container(
            width: widget.size,
            height: widget.size,
            decoration: BoxDecoration(
              shape: BoxShape.circle,
              color: widget.color.withValues(alpha: 0.3),
            ),
            child: Icon(Icons.mic, color: widget.color, size: widget.size * 0.6),
          ),
        );
      },
    );
  }
}

Bug 2 & 3: 直接复用 social-app 输入框(不需要 plus 按钮)

需求

直接复用 social-app 的 MessageComposer,但不需要 plus 按钮

复用清单

功能 social-app eryao 处理
recordingAnimation required Widget 空白占位 需要添加
双层阴影 需要添加
AppRadius.xxl 用于圆角 无此值 需要添加
Plus 按钮 不需要,保持无
图标 LucideIcons Material Icons 保持 Material Icons

实现步骤

1. 添加 AppRadius.xxldesign_tokens.dart

class AppRadius {
  static const double sm = 8;
  static const double md = 12;
  static const double lg = 16;
  static const double xl = 20;
  static const double xxl = 32;  // 新增
  static const double full = 999;
}

2. 创建 RecordingAnimation widget

放在 apps/lib/shared/widgets/ 下,参考 social-app 的脉冲动画效果。

3. 修改 MessageComposer

  • 添加 recordingAnimation 参数(required
  • 添加双层阴影
  • 使用 AppRadius.xxl 替代 AppRadius.full
  • 移除 plus 按钮(eryao 原有的无 plus 逻辑保持不变)
  • 保持 Material Icons

4. 修改 FollowUpChatScreen

  • 创建 RecordingAnimation 实例
  • 传入 MessageComposer

相关文件

  • apps/lib/features/divination/presentation/screens/follow_up_chat_screen.dart
  • apps/lib/shared/widgets/message_composer.dart
  • apps/lib/shared/widgets/recording_animation.dart(新建)
  • apps/lib/shared/theme/design_tokens.dart
  • /home/qzl/Code/social-app/apps/lib/shared/widgets/message_composer.dart

修复优先级

  1. 高优先级Bug 1(重复加载UI
  2. 高优先级Bug 2 & 3(直接复用 social-app 输入框)