Skip to content

fix(语音输入): 修复 iPhone 端 UI 残留及 Watch 端取消逻辑失效 (#80) - #86

Closed
devhunk wants to merge 6 commits into
Eric-Terminal:mainfrom
devhunk:fix/speech-input-preview-flow
Closed

fix(语音输入): 修复 iPhone 端 UI 残留及 Watch 端取消逻辑失效 (#80)#86
devhunk wants to merge 6 commits into
Eric-Terminal:mainfrom
devhunk:fix/speech-input-preview-flow

Conversation

@devhunk

@devhunk devhunk commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

变更说明

修复 Issue #80 中报告的语音输入缺陷:iPhone 端停止录音后缺少预览确认流程、Watch 端取消逻辑失效。

关联 Issue

Fixes #80

变更类型

  • Bug 修复
  • 新功能(预览确认 UI)

影响范围

  • iOS App
  • watchOS App

具体改动

iPhone 端 (ChatViewAudioRecorderSheet.swift)

  • 新增 showingPreview 状态
  • 停止录音后进入预览状态,展示识别结果
  • 隐藏计时器和录音按钮,避免回到初始界面的困惑
  • 用户需点击「完成」确认或「取消」丢弃

Watch 端 (WatchChatViewModelSpeechInput.swift + SpeechRecorderView.swift)

  • finishSpeechRecording() 改为先展示预览,不再直接调用 appendTranscribedText
  • 新增 confirmSpeechTranscript() 函数,用户点击「使用」后才填入输入框
  • cancelSpeechRecording() 正确重置 isShowingTranscriptPreview
  • SpeechRecorderView 增加预览状态 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:

  • Fix missing reset of watchOS transcript preview state when starting, failing, or cancelling speech recording.
  • Prevent watchOS speech input from immediately inserting transcriptions by requiring explicit confirmation.
  • Ensure iOS speech recording cleanup clears preview state and related transcription data.

Enhancements:

  • Introduce a transcript preview step for iOS speech-to-text recording, requiring user confirmation before applying recognized text.
  • Add a transcript preview state and UI on watchOS so users can review recognition results and choose to use or cancel them.
  • Expose transcript preview state in the watchOS chat view model to drive the new preview UI.

CI:

  • Add standardized GitHub issue templates for bug reports and feature requests.
  • Add a GitHub pull request template to standardize PR descriptions and metadata.

Documentation:

  • Add CONTRIBUTING guide describing build setup, coding standards, commit conventions, and PR process.

Chores:

  • Add an EditorConfig file to unify code style across the project.

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 统一代码格式
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


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.

@sourcery-ai

sourcery-ai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements 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 confirmation

sequenceDiagram
    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
Loading

Flow diagram for iOS audio recorder sheet with transcript preview

flowchart 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
Loading

File-Level Changes

Change Details Files
Add transcript preview confirmation flow to iOS audio recorder sheet instead of immediately returning to the initial UI after recording.
  • Introduce a local showingPreview state to represent post-recording preview mode in speech-to-text flows.
  • Update the main sheet body to show a scrollable transcript preview with optional error message when showingPreview is true, hiding timer and recording controls.
  • Set showingPreview to true when streaming transcription finishes successfully, and ensure it is reset in resetRecordingState and when closing the sheet without using the result.
  • Clear recording-related state (URLs, recorder, transcript, flags) consistently when closing or resetting to avoid UI residue.
ETOS LLM Studio/ETOS LLM Studio iOS App/Views/ChatViewAudioRecorderSheet.swift
Refactor watchOS speech input flow to show a transcript preview and require explicit user confirmation before inserting text; fix cancel/reset behavior.
  • Add isShowingTranscriptPreview published state on ChatViewModel to drive the preview UI on watchOS.
  • Change finishSpeechRecording logic to set isShowingTranscriptPreview when a valid transcript is available instead of directly calling appendTranscribedText, and ensure preview state is reset on early exits and errors.
  • Adjust the audio file transcription path to set speechStreamingTranscript and then enter preview mode on the main actor instead of closing the recorder immediately.
  • Introduce confirmSpeechTranscript() to validate the trimmed transcript, append it to the input, clear streaming/progress flags, close the recorder, and reset visuals.
  • Ensure cancelSpeechRecording and recorder opening/closing paths always clear isShowingTranscriptPreview and related state, and delete any temporary recording file if present.
  • Extend SpeechRecorderView to render a transcript preview section (title, scrollable transcript, Cancel/Use buttons) when isShowingTranscriptPreview is true, wiring buttons to cancelSpeechRecording/confirmSpeechTranscript and dismissing the sheet.
ETOS LLM Studio/ETOS LLM Studio Watch App/ViewModels/ChatViewModel.swift
ETOS LLM Studio/ETOS LLM Studio Watch App/ViewModels/WatchChatViewModelSpeechInput.swift
ETOS LLM Studio/ETOS LLM Studio Watch App/Views/SpeechRecorderView.swift
Add project contribution, PR, and issue templates plus editor configuration.
  • Add CONTRIBUTING.md documenting build prerequisites, architecture conventions, commit message style, and PR process.
  • Introduce GitHub issue templates for bug reports and feature requests with structured sections and default labels.
  • Add a GitHub pull request template to standardize PR descriptions, impact, testing status, and checklist.
  • Add a .editorconfig file to enforce consistent code formatting across contributors.
CONTRIBUTING.md
.github/PULL_REQUEST_TEMPLATE.md
.github/ISSUE_TEMPLATE/bug_report.md
.github/ISSUE_TEMPLATE/feature_request.md
.editorconfig

Assessment against linked issues

Issue Objective Addressed Explanation
#80 On iPhone, add a voice-input transcript preview-and-confirm (完成/取消) flow after recording stops, ensure the sheet closes normally after the user’s choice, and avoid confusing UI residue such as incorrect timer display or reverting to the initial recording state.
#80 On Apple Watch, change the voice-input behavior so that text is not inserted into the input field until the user explicitly confirms (e.g., 使用/完成), and make the 取消 action correctly discard the current transcript and leave the input field unchanged.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/triage

This way both labels are added when the template is used.

name: 功能建议
about: 有一个新功能的想法
title: '[Feature] '
labels: 'type/feature, status/triage'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/triage

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

问题分析
在当前实现中,showingPreview 状态的引入存在逻辑漏洞,导致非流式/远程语音识别模型(如 Whisper 等非系统原生流式模型)无法正确进入预览状态,且「取消」逻辑失效:

  1. 预览状态未触发:在 finishRecording() 的非流式转写完成回调中(Task 内部的 MainActor.run),并没有将 showingPreview 设置为 true。因此,转写完成后界面不会切换到预览 UI,依然会显示计时器和录音按钮。
  2. 取消逻辑失效:在转写完成时,代码立即调用了 onCompleteTranscript(trimmedTranscript) 将文本同步到了父组件。此时如果用户在 Sheet 中点击「取消」,由于文本已经应用到父组件的输入框中,取消操作实际上无法撤销已填入的文本。

解决方案
建议在 finishRecording() 中进行以下调整:

  1. 在非流式转写成功的回调中,仅设置 liveTranscriptpreparedTranscript,并将 showingPreview 设为 true不要在此处提前调用 onCompleteTranscript
  2. 当用户在预览状态下点击「完成」时,再统一调用 onCompleteTranscriptdismiss()

由于 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
}

Comment on lines 271 to 275
await MainActor.run {
pendingAudioAttachment = attachment
isSpeechRecorderPresented = false
isShowingTranscriptPreview = false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

改进建议
由于 ChatViewModel 类已被标记为 @MainActor,其所有方法和扩展默认都在主线程(Main Actor)上执行。因此,在此处创建的 Task 会自动继承 @MainActor 上下文。

这里的 await MainActor.run 是完全多余的,且会引入不必要的线程调度开销。建议直接更新属性,使代码更加简洁和符合 Swift 异步编程的最佳实践。

                    pendingAudioAttachment = attachment
                    isSpeechRecorderPresented = false
                    isShowingTranscriptPreview = false

Comment on lines +291 to 302
// 进入预览状态,而不是直接填入输入框
await MainActor.run {
isShowingTranscriptPreview = true
}
}
} catch {
presentSpeechError(error.localizedDescription)
isSpeechRecorderPresented = false
await MainActor.run {
presentSpeechError(error.localizedDescription)
isSpeechRecorderPresented = false
isShowingTranscriptPreview = false
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

改进建议
同上,由于 Task 已经运行在 @MainActor 上,这里无需使用 await MainActor.run 来包裹 UI 状态的更新。此外,原代码中 speechStreamingTranscript = trimmedTranscriptMainActor.run 之外更新,而 isShowingTranscriptPreview = true 在其内部更新,这种不一致容易让人误解。

建议移除冗余的 MainActor.run,直接在主线程上下文中安全地更新所有状态。

                    isShowingTranscriptPreview = true
                }
            } catch {
                presentSpeechError(error.localizedDescription)
                isSpeechRecorderPresented = false
                isShowingTranscriptPreview = false
            }

Comment on lines +47 to +57
Button(NSLocalizedString("取消", comment: ""), role: .cancel) {
viewModel.cancelSpeechRecording()
dismiss()
}
.buttonStyle(.bordered)
.frame(maxWidth: .infinity)

Button(NSLocalizedString("使用", comment: "")) {
viewModel.confirmSpeechTranscript()
dismiss()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

改进建议
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()
                    }

Krill-AI and others added 5 commits June 17, 2026 13:02
为「识别结果」和「使用」按钮添加 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 触发),非代码变更导致
@Eric-Terminal

Eric-Terminal commented Jun 20, 2026

Copy link
Copy Markdown
Owner

似乎编译都没过。。。而且这个等我旅游回来我看看处理下吧。不过Issue示例和贡献指南倒是不错

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[App反馈][IOS] 语音输入缺陷反馈:iPhone端UI残留及Watch端取消逻辑失效

3 participants