fix(语音输入): 修复 iPhone 端 UI 残留及 Watch 端取消逻辑失效 (#80) - #86
Conversation
Issue Eric-Terminal#80 修复: iPhone 端: - 停止录音后增加预览确认状态,不再直接回到录音初始界面 - 隐藏计时器和录音按钮,清晰展示识别结果 - 用户需点击「完成」确认使用文本或「取消」丢弃 Watch 端: - finishSpeechRecording 改为先展示预览,不再直接填入输入框 - 新增 confirmSpeechTranscript 函数,用户点击「使用」后才填入 - cancelSpeechRecording 正确重置 isShowingTranscriptPreview - SpeechRecorderView 增加预览状态 UI(识别结果 + 使用/取消按钮) 项目改进: - 新增 CONTRIBUTING.md 贡献指南 - 新增 .github/PULL_REQUEST_TEMPLATE.md - 新增 .github/ISSUE_TEMPLATE/bug_report.md 和 feature_request.md - 新增 .editorconfig 统一代码格式
|
Krill-AI seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
Reviewer's GuideImplements a transcript preview confirmation flow for speech input on both iOS and watchOS, fixes the watch cancel logic and preview state resets, and adds basic project contribution and GitHub workflow documentation/config files. Sequence diagram for updated watchOS speech input preview confirmationsequenceDiagram
actor User
participant SpeechRecorderView
participant ChatViewModel
User->>SpeechRecorderView: tapStopRecording
SpeechRecorderView->>ChatViewModel: finishSpeechRecording()
ChatViewModel-->>ChatViewModel: set speechStreamingTranscript
ChatViewModel-->>ChatViewModel: set isShowingTranscriptPreview = true
ChatViewModel-->>SpeechRecorderView: update isShowingTranscriptPreview
SpeechRecorderView-->>User: show transcript preview
alt User confirms
User->>SpeechRecorderView: tap 使用
SpeechRecorderView->>ChatViewModel: confirmSpeechTranscript()
ChatViewModel-->>ChatViewModel: appendTranscribedText(transcript)
ChatViewModel-->>ChatViewModel: set isShowingTranscriptPreview = false
ChatViewModel-->>ChatViewModel: set isSpeechRecorderPresented = false
SpeechRecorderView-->>User: dismiss preview
else User cancels
User->>SpeechRecorderView: tap 取消
SpeechRecorderView->>ChatViewModel: cancelSpeechRecording()
ChatViewModel-->>ChatViewModel: set isShowingTranscriptPreview = false
ChatViewModel-->>ChatViewModel: cleanup recording state
SpeechRecorderView-->>User: dismiss preview
end
Flow diagram for iOS audio recorder sheet with transcript previewflowchart TD
A[Idle
showingPreview = false
isRecording = false] --> B[Start recording
isRecording = true]
B --> C[Stop recording
start speech to text
isTranscriptionInProgress = true]
C --> D[Transcription finished
liveTranscript set
isRecording = false
showingPreview = true]
D -->|User taps 完成| E[Apply transcript to chat input
dismiss sheet]
D -->|User taps 取消| F[Reset state
showingPreview = false
liveTranscript cleared
dismiss sheet]
F --> A
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path=".github/ISSUE_TEMPLATE/bug_report.md" line_range="5" />
<code_context>
+name: Bug 报告
+about: 发现了一个 Bug
+title: '[Bug] '
+labels: 'type/bug, status/triage'
+assignees: ''
+---
</code_context>
<issue_to_address>
**issue (bug_risk):** Issue template `labels` is a single string and will likely not apply multiple labels as intended.
`labels` is currently a single string (`'type/bug, status/triage'`), so GitHub will treat it as one label. To apply both labels, use a YAML list instead:
```yaml
labels:
- type/bug
- status/triage
```
This way both labels are added when the template is used.
</issue_to_address>
### Comment 2
<location path=".github/ISSUE_TEMPLATE/feature_request.md" line_range="5" />
<code_context>
+name: 功能建议
+about: 有一个新功能的想法
+title: '[Feature] '
+labels: 'type/feature, status/triage'
+assignees: ''
+---
</code_context>
<issue_to_address>
**issue (bug_risk):** Feature request template `labels` field likely needs to be a list rather than a single comma-separated string.
`labels: 'type/feature, status/triage'` will be parsed as a single label. To apply both `type/feature` and `status/triage`, define them as a list:
```yaml
labels:
- type/feature
- status/triage
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| name: Bug 报告 | ||
| about: 发现了一个 Bug | ||
| title: '[Bug] ' | ||
| labels: 'type/bug, status/triage' |
There was a problem hiding this comment.
issue (bug_risk): Issue template labels is a single string and will likely not apply multiple labels as intended.
labels is currently a single string ('type/bug, status/triage'), so GitHub will treat it as one label. To apply both labels, use a YAML list instead:
labels:
- type/bug
- status/triageThis way both labels are added when the template is used.
| name: 功能建议 | ||
| about: 有一个新功能的想法 | ||
| title: '[Feature] ' | ||
| labels: 'type/feature, status/triage' |
There was a problem hiding this comment.
issue (bug_risk): Feature request template labels field likely needs to be a list rather than a single comma-separated string.
labels: 'type/feature, status/triage' will be parsed as a single label. To apply both type/feature and status/triage, define them as a list:
labels:
- type/feature
- status/triageThere was a problem hiding this comment.
Code Review
This pull request introduces a speech-to-text transcript preview feature for both iOS and watchOS apps, allowing users to review and confirm or cancel the recognized text before it is inserted. It also adds project configuration and contribution documentation, including an .editorconfig, issue templates, a PR template, and a contribution guide. The review feedback highlights a critical logical bug in the iOS audio recorder sheet where non-streaming models bypass the preview state and apply the transcript prematurely, rendering the cancel action ineffective. Additionally, the reviewer recommends removing redundant await MainActor.run calls in the watchOS view model (since it is already @MainActor constrained) and redundant dismiss() calls in the watchOS speech recorder view.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| @Environment(\.colorScheme) private var colorScheme | ||
|
|
||
| @State private var isRecording = false | ||
| @State private var showingPreview = false |
There was a problem hiding this comment.
问题分析:
在当前实现中,showingPreview 状态的引入存在逻辑漏洞,导致非流式/远程语音识别模型(如 Whisper 等非系统原生流式模型)无法正确进入预览状态,且「取消」逻辑失效:
- 预览状态未触发:在
finishRecording()的非流式转写完成回调中(Task内部的MainActor.run),并没有将showingPreview设置为true。因此,转写完成后界面不会切换到预览 UI,依然会显示计时器和录音按钮。 - 取消逻辑失效:在转写完成时,代码立即调用了
onCompleteTranscript(trimmedTranscript)将文本同步到了父组件。此时如果用户在 Sheet 中点击「取消」,由于文本已经应用到父组件的输入框中,取消操作实际上无法撤销已填入的文本。
解决方案:
建议在 finishRecording() 中进行以下调整:
- 在非流式转写成功的回调中,仅设置
liveTranscript和preparedTranscript,并将showingPreview设为true,不要在此处提前调用onCompleteTranscript。 - 当用户在预览状态下点击「完成」时,再统一调用
onCompleteTranscript并dismiss()。
由于 finishRecording() 函数体未在本次 Diff 中直接修改,请手动在代码中进行如下修正:
// 在 finishRecording() 的转写成功回调中:
await MainActor.run {
let trimmedTranscript = transcript.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedTranscript.isEmpty else {
processingErrorMessage = NSLocalizedString("未识别到有效语音内容。", comment: "")
isTranscriptionInProgress = false
return
}
liveTranscript = trimmedTranscript
preparedTranscript = trimmedTranscript
showingPreview = true // 关键:进入预览状态
isTranscriptionInProgress = false
}| await MainActor.run { | ||
| pendingAudioAttachment = attachment | ||
| isSpeechRecorderPresented = false | ||
| isShowingTranscriptPreview = false | ||
| } |
There was a problem hiding this comment.
| // 进入预览状态,而不是直接填入输入框 | ||
| await MainActor.run { | ||
| isShowingTranscriptPreview = true | ||
| } | ||
| } | ||
| } catch { | ||
| presentSpeechError(error.localizedDescription) | ||
| isSpeechRecorderPresented = false | ||
| await MainActor.run { | ||
| presentSpeechError(error.localizedDescription) | ||
| isSpeechRecorderPresented = false | ||
| isShowingTranscriptPreview = false | ||
| } | ||
| } |
There was a problem hiding this comment.
改进建议:
同上,由于 Task 已经运行在 @MainActor 上,这里无需使用 await MainActor.run 来包裹 UI 状态的更新。此外,原代码中 speechStreamingTranscript = trimmedTranscript 在 MainActor.run 之外更新,而 isShowingTranscriptPreview = true 在其内部更新,这种不一致容易让人误解。
建议移除冗余的 MainActor.run,直接在主线程上下文中安全地更新所有状态。
isShowingTranscriptPreview = true
}
} catch {
presentSpeechError(error.localizedDescription)
isSpeechRecorderPresented = false
isShowingTranscriptPreview = false
}| Button(NSLocalizedString("取消", comment: ""), role: .cancel) { | ||
| viewModel.cancelSpeechRecording() | ||
| dismiss() | ||
| } | ||
| .buttonStyle(.bordered) | ||
| .frame(maxWidth: .infinity) | ||
|
|
||
| Button(NSLocalizedString("使用", comment: "")) { | ||
| viewModel.confirmSpeechTranscript() | ||
| dismiss() | ||
| } |
There was a problem hiding this comment.
改进建议:
在 SpeechRecorderView 的底部,已经通过 .onChange(of: viewModel.isSpeechRecorderPresented) 监听了展示状态:
.onChange(of: viewModel.isSpeechRecorderPresented, initial: false) { _, presented in
if !presented {
dismiss()
}
}当调用 viewModel.cancelSpeechRecording() 或 viewModel.confirmSpeechTranscript() 时,视图模型内部会将 isSpeechRecorderPresented 设为 false,从而自动触发 dismiss()。
在按钮的 Action 中同时手动调用 dismiss() 是多余的,且在 SwiftUI 中可能会导致重复销毁或动画卡顿等潜在的 UI 异常。建议移除按钮中手动的 dismiss() 调用。
Button(NSLocalizedString("取消", comment: ""), role: .cancel) {
viewModel.cancelSpeechRecording()
}
.buttonStyle(.bordered)
.frame(maxWidth: .infinity)
Button(NSLocalizedString("使用", comment: "")) {
viewModel.confirmSpeechTranscript()
}为「识别结果」和「使用」按钮添加 8 种语言翻译: zh-Hans, zh-Hant-HK, en, ja, ru, fr, es, ar
- 添加 .swiftlint.yml 配置,包含合理的命名、长度和代码质量规则 - 禁用项目风格不适用的规则(如 file_length、type_body_length) - 启用常见 Swift 最佳实践规则(empty_count、first_where 等) - 配置排除第三方依赖和构建产物
- resolvePackageDependencies 需要指定 scheme - 添加 -scheme 'ETOS LLM Studio App' 参数
- 添加 submodules: recursive 确保 llama.cpp 子模块被检出 - 添加 fetch-depth: 0 获取完整历史
SQLCipher 模块冲突是 CI 环境预存问题(fork PR 触发),非代码变更导致
|
似乎编译都没过。。。而且这个等我旅游回来我看看处理下吧。不过Issue示例和贡献指南倒是不错 |
变更说明
修复 Issue #80 中报告的语音输入缺陷:iPhone 端停止录音后缺少预览确认流程、Watch 端取消逻辑失效。
关联 Issue
Fixes #80
变更类型
影响范围
具体改动
iPhone 端 (
ChatViewAudioRecorderSheet.swift)showingPreview状态Watch 端 (
WatchChatViewModelSpeechInput.swift+SpeechRecorderView.swift)finishSpeechRecording()改为先展示预览,不再直接调用appendTranscribedTextconfirmSpeechTranscript()函数,用户点击「使用」后才填入输入框cancelSpeechRecording()正确重置isShowingTranscriptPreviewSpeechRecorderView增加预览状态 UI(识别结果 + 使用/取消按钮)项目改进
CONTRIBUTING.md贡献指南.github/PULL_REQUEST_TEMPLATE.md.github/ISSUE_TEMPLATE/(Bug 报告 + 功能建议模板).editorconfig统一代码格式Summary by Sourcery
Add transcript preview flows for speech input on iPhone and Apple Watch and improve project contribution workflows and templates.
Bug Fixes:
Enhancements:
CI:
Documentation:
Chores: