# 语音助手报错展示与首页提示恢复实现方案

## 一、需求来源

本次补充 `voice-assistant` 的错误展示规则：

| 需求编号 | 描述 |
|---------|------|
| 1 | AI 回答气泡中显示错误时，错误文字需要显示为红色 |
| 2 | 首页 `ui->label_tip` 位置也要显示错误信息，且为红色 |
| 3 | 首页错误文案一行放不下时允许换行，最大高度 42px |
| 4 | 首页错误 6 秒后自动消失，并恢复为当前状态应显示的正常文字 |
| 5 | 点击字幕按钮切换到字幕页后，首页错误立即消失，恢复为当前状态应显示的正常文字 |

## 二、改动前现状

已有错误处理入口：

- Gateway 错误走 `MainWindow::handleChatError()`。
- ASR 启动/识别错误走 `handleRecognitionStartFailed()` / `handleRecognitionError()`。
- TTS 错误走 `SpeechSynthesisController::errorOccurred` 连接的 lambda。
- `presentErrorToUser()` 根据上下文决定显示气泡错误、首页错误或系统通知。

已有问题：

- 首页 `label_tip` 红字错误缺少明确的“错误态”保护，状态刷新可能覆盖错误文字。
- 首页错误没有 42px 最大高度和自动换行约束。
- 点击字幕按钮进入字幕页时，首页错误不会立即清除。

## 三、设计方案

### 3.1 错误展示分流

```
错误发生
  → presentErrorToUser(message, useChatBubble)
    ├─ useChatBubble && isVisible()
    │    → appendStandaloneAiBubble()
    │    → ChatWidget::setLatestAIBubbleErrorText()
    │    → AI 气泡红色错误样式
    ├─ isVisible() && !m_callbackMode
    │    → showInlineError()
    │    → 首页 label_tip 红字错误
    └─ 窗口不可见或回拨中
         → 系统通知
```

### 3.2 AI 气泡错误样式

`ChatWidget::setLatestAIBubbleErrorText()` 会复用最近一条 AI 气泡或创建新的 AI 气泡，并切换到错误态。当前实现不再依赖 `setStyleSheet`，而是沿用字幕页自绘气泡控件，仅修改错误文字颜色：

- 气泡背景继续保持 AI 气泡默认背景 `#F7F7F7`
- 错误文字颜色切换为 `#D92D20`
- 未显示复制操作栏时维持四角 10px 圆角
- 若操作栏已显示，底部圆角仍由操作栏承接

错误气泡仍使用 `Qt::PlainText`，避免错误字符串被当作富文本解析。

### 3.3 首页 label_tip 错误态

新增状态：

```cpp
int m_inlineErrorGeneration = 0;
bool m_inlineErrorActive = false;
```

`showInlineError()` 进入错误态：

```cpp
++m_inlineErrorGeneration;
m_inlineErrorActive = true;
ui->interruptButton->hide();
ui->label_tip->setText(text);
ui->label_tip->setStyleSheet("color: #D92D20;");
ui->label_tip->setAlignment(Qt::AlignCenter);
ui->label_tip->setWordWrap(true);
ui->label_tip->setMaximumWidth(std::max(120, ui->page_listening->width() - 32));
ui->label_tip->setMaximumHeight(42);
ui->label_tip->show();
```

`updateVoiceStateUi()` 在错误态直接返回，避免 Listening、Muted、Reading 等状态刷新覆盖错误文案：

```cpp
if (m_inlineErrorActive) {
    return;
}
```

### 3.4 6 秒自动恢复

`showInlineError()` 启动 6 秒 single-shot 计时器，计时器携带当前 generation：

```cpp
QTimer::singleShot(6000, this, [this, generation]() {
    if (generation != m_inlineErrorGeneration) {
        return;
    }
    clearInlineError(true);
});
```

generation 用于处理连续错误：新错误出现时旧计时器失效，避免旧计时器提前清除新错误。

### 3.5 字幕按钮立即清除首页错误

字幕按钮信号中增加清理：

```cpp
connect(ui->bottomWidget, &MiniPanel::subtitleToggled, this, [this](bool checked) {
    if (checked) {
        clearInlineError(true);
    }
    m_showChatPage = checked;
    updateMiddlePage();
});
```

`clearInlineError(true)` 会：

1. 递增 `m_inlineErrorGeneration`，使旧 6 秒计时器失效。
2. 清空 `m_inlineErrorActive`。
3. 取消换行和最大宽高约束。
4. 清除红色样式。
5. 调用 `updateVoiceStateUi()` 恢复当前状态文案。

## 四、涉及文件

| 文件 | 改动内容 |
|------|---------|
| `voice-assistant/chatwidget.cpp` | AI 错误气泡复用自绘气泡结构并切换到红色错误文字 |
| `voice-assistant/chatwidget.h` | 暴露 `setLatestAIBubbleErrorText()` |
| `voice-assistant/mainwindow.h` | 新增 `m_inlineErrorActive` 和 `clearInlineError()` |
| `voice-assistant/mainwindow.cpp` | 首页红字错误、6 秒恢复、字幕页切换立即清除 |

## 五、测试场景

| 场景 | 预期 |
|------|------|
| Gateway 错误发生且当前在字幕/对话上下文 | AI 气泡显示红色错误文字 |
| Gateway/ASR/TTS 错误发生且当前在首页 | `label_tip` 显示红色错误文字 |
| 首页错误文字较长 | 文案换行显示，最高 42px |
| 首页错误显示满 6 秒 | 自动恢复为当前状态文字，例如“请开始说话”“正在聆听”“已静音” |
| 首页错误显示期间点击字幕按钮 | 首页错误立即清除，切换到字幕页 |
| 连续发生两个首页错误 | 第二个错误重新计时，旧计时器不会提前清除新错误 |

## 六、风险与缓解

| 风险 | 缓解措施 |
|------|----------|
| 错误显示期间状态刷新覆盖错误文字 | `m_inlineErrorActive` 使 `updateVoiceStateUi()` 直接返回 |
| 旧错误的 6 秒计时器清除新错误 | `m_inlineErrorGeneration` 做代际校验 |
| 长错误文字撑高首页布局 | `label_tip` 开启换行并限制最大高度 42px |
| 切换字幕页后首页错误残留 | 字幕按钮 checked 时立即调用 `clearInlineError(true)` |
