# 「身体档案」输入框语音输入 设计

> 2026-06-10 · 在「身体档案」(`HealthExportSheet`)底部聊天输入框加端侧语音听写,复用 `SpeechDictationService`,识别文字实时流进输入框。

## 背景

「身体档案」composer 是聊天式输入(提问/诉求 → 发送 → LLM 对话/生成报告)。与日记不同,这里输入的内容马上交给 LLM,**不需要"整理"加工**;口述原话直接进输入框即正确行为(类似系统键盘听写)。

## 决策(已与用户确认)

| 维度 | 决定 |
|---|---|
| 交互 | 听写直接流进输入框:点 mic 开始,实时上屏;再点停止;用户自查后手动发送 |
| LLM | 不调用(无整理步骤、不自动发送) |
| 复用 | `SpeechDictationService`(**@State 持有**,防视图重建丢实例)、权限 alert 文案、3 分钟看门狗、onDisappear abort |
| UI | mic 按钮放 TextField 与发送键之间;`isAvailable == false` 隐藏;录音中变红色停止态(脉冲动画) |

## 组件

### 1. `SpeechDictationService.merge(prefix:partial:)`(新,static 纯函数)

听写文本拼接规则,唯一可单测的逻辑:
- `prefix` 为空 → 返回 `partial`
- `prefix` 以空白/换行结尾 → `prefix + partial`
- 其余 → `prefix + " " + partial`

### 2. `HealthExportSheet` 改动

- `@State dictation` + `isDictating` + `dictationPrefix` + 看门狗 Task
- 点 mic:申请权限(拒绝 → alert 跳设置,与日记同文案)→ 记录 `dictationPrefix = draftQuestion` → start,每个 partial:`draftQuestion = merge(prefix:partial:)`
- 再点:`stop()`,最终稿同 merge 落定;**stop 返回空时保留输入框现状**(partial 已实时在框里,天然兜底,不提示「没听清」)
- 3 分钟看门狗自动停(防麦克风悬挂)

## 冲突防护

- 录音中:TextField 与发送按钮、「生成整理报告」按钮禁用(防手输与 partial 互相覆盖、防录音中发送)
- `isAnswering / isGeneratingReport` 时 mic 禁用
- `onDisappear` abort

## 测试

- `merge(prefix:partial:)` 3 个单测(空前缀 / 空白结尾前缀 / 普通前缀)
- 真机手测:听写上屏、停止落定、已有文字保留、权限拒绝、3 分钟自动停

## 不做(YAGNI)

快捷问答弹窗 / 个人资料 Form 等其他输入处的语音;自动发送;录音面板;LLM 整理。
