feat(translation-polish-2): local-engine dedicated section + PreconfiguredKeys for DeepSeek

- Add PreconfiguredKeys.swift: placeholder constant for the DeepSeek
  API key the local engine uses. DEBUG build asserts at launch when
  the placeholder is still in place so nobody ships an always-401
  build by accident. Replace TODO_FILL_LATER_DEEPSEEK_KEY before
  distributing.

- PolishingService.polishRemote:
  * Adopted (A) path: when effective provider is DeepSeek, take the
    apiKey from PreconfiguredKeys.deepseek instead of store.apiKey.
    Refuse the round-trip with PolishError.missingAPIKey when the
    placeholder is still in place.
  * Adopted fix for pre-existing typo: store.model.isEmpty ?
    preset.defaultModel : store.model (the right-hand side was
    defaultModel on both branches, silently ignoring the user's
    custom model field).

- LocalEngineSettingsRows.LocalModelsGroup:
  * Dropped the inline 'uses DeepSeek' caption — it added visual
    weight without telling the user anything they couldn't infer.
  * Translation row now lives inside the group so the local engine
    reads as one cohesive card.
  * Group owns its surface card chrome (palette.surface + rounded
    border) — callers no longer need to wrap it externally.

- SettingsView:
  * languageAndModelsSection: dropped the trailing TranslationPickerRow
    and the inner LocalModelsGroup (now lifted to its own section).
  * New localEngineSettingsSection: renders only when
    engineMode == 'local', wrapping LocalModelsGroup with a
    'settings.localEngine.title' header.
  * body: added the new section between languageAndModelsSection and
    the cloud-only systemPromptLinkSection.

- Localizable.strings (en + zh-Hans):
  * Removed settings.localModels.cloudPolish.caption (no references).
  * Added settings.localEngine.title = 'Local engine' / '本地引擎'.

DeepSeek key pre-fill tripwire + local-engine UI cohesion. No main
merge, no PR.
This commit is contained in:
2026-06-25 17:34:02 +08:00
parent 93b6aa6c02
commit 4fec0da7f0
6 changed files with 118 additions and 35 deletions
+26 -19
View File
@@ -27,11 +27,26 @@ struct LocalModelsGroup: View {
@ObservedObject var config: ProviderConfig
var body: some View {
// v0.2.1 follow-up: the LocalEngineGroup now owns the
// translation row so the local-engine Settings tab reads as
// one cohesive card. The same surface chrome
// (`palette.surface` + rounded border) the cloud branch uses
// on its own card wraps the whole group so it sits flush with
// the language tab above.
VStack(spacing: 0) {
speechRow
Divider().background(palette.divider)
cloudPolishRow
if config.isTranslationRowVisible {
Divider().background(palette.divider)
TranslationPickerRow(config: config, isVisible: true)
}
}
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
.stroke(palette.divider, lineWidth: 0.5)
)
}
// MARK: Speech row
@@ -58,28 +73,20 @@ struct LocalModelsGroup: View {
/// key yet), but the polish call short-circuits with an Alert if
/// the Keychain is empty when it fires.
///
/// v0.2.1 follow-up: added a one-line caption under the toggle
/// that names the default cloud vendor (DeepSeek) so the user
/// knows where the transcript is going when they flip the switch.
/// The toggle row is now a two-line layout title + caption
/// so we drop the explicit `singleLineMinHeight` here and let
/// `SettingsListMetrics` provide enough vertical room.
/// v0.2.1 follow-up: dropped the inline "uses DeepSeek" caption
/// (the user already opted into cloud mode by switching engines,
/// and the vendor name surfaces when they tap the row's helper
/// text in onboarding / deep links). Title + switch is enough.
private var cloudPolishRow: some View {
VStack(alignment: .leading, spacing: 4) {
Toggle(isOn: $config.localModeCloudPolishEnabled) {
Text("settings.localModels.cloudPolish.title")
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
}
.toggleStyle(.switch)
.tint(palette.accent)
Text("settings.localModels.cloudPolish.caption")
.font(TypeStyle.caption2)
.foregroundStyle(palette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
Toggle(isOn: $config.localModeCloudPolishEnabled) {
Text("settings.localModels.cloudPolish.title")
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
}
.toggleStyle(.switch)
.tint(palette.accent)
.padding(.horizontal, Spacing.md)
.padding(.vertical, Spacing.xs)
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
}
// MARK: Helpers
+19 -10
View File
@@ -74,6 +74,9 @@ struct SettingsView: View {
apiSection
}
languageAndModelsSection
if config.engineMode == "local" {
localEngineSettingsSection
}
if config.engineMode == "cloud" {
systemPromptLinkSection
}
@@ -131,8 +134,9 @@ struct SettingsView: View {
// now sits above the cloud-polish toggle / local models
// block so the row that maps to microphone input comes
// first, the row that maps to post-processing comes
// second, and translation (post-post-processing) sits at
// the bottom.
// second. Translation moved into the local-engine
// group (see `localEngineSettingsSection`) so the local
// engine reads as one cohesive card on its own.
LocalePickerRow(
locales: effectiveLocales,
selection: Binding(
@@ -140,14 +144,6 @@ struct SettingsView: View {
set: { config.localeId = $0 }
)
)
if config.engineMode == "local" {
Divider().background(palette.divider)
LocalModelsGroup(config: config)
}
Divider().background(palette.divider)
if config.isTranslationRowVisible {
TranslationPickerRow(config: config, isVisible: true)
}
}
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.overlay(
@@ -178,6 +174,19 @@ struct SettingsView: View {
}
}
/// v0.2.1 follow-up: dedicated section for the local engine's
/// settings (cloud-polish toggle + translation row). Renders only
/// when `engineMode == "local"` so the cloud-engine user doesn't
/// see rows that are inert for them. The translation row lives
/// inside `LocalModelsGroup` so it shares the group's surface card
/// chrome see `LocalEngineSettingsRows.swift` for the layout.
private var localEngineSettingsSection: some View {
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
sectionHeader("settings.localEngine.title")
LocalModelsGroup(config: config)
}
}
private var providerSection: some View {
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
sectionHeader("settings.provider.title")
+1 -1
View File
@@ -119,7 +119,7 @@
"settings.localModels.readiness %lld %lld" = "%lld/%lld ready";
"settings.localModels.cloudPolish.title" = "Cloud polish after ASR";
"settings.localModels.cloudPolish.subtitle" = "Sends the transcript to your configured cloud LLM (DeepSeek by default) for cleanup. Enable only when iOS speech recognition struggles — noisy far-field audio, strong accents, etc. Requires a DeepSeek API key.";
"settings.localModels.cloudPolish.caption" = "Uses DeepSeek to polish transcripts in local mode.";
"settings.localEngine.title" = "Local engine";
"settings.language.subtitle.cloud" = "Recognition language and text processing mode.";
"settings.language.subtitle.local" = "Recognition language.";
"settings.mode.title" = "Mode";
@@ -119,7 +119,7 @@
"settings.localModels.readiness %lld %lld" = "%lld/%lld 已就绪";
"settings.localModels.cloudPolish.title" = "识别后云端润色";
"settings.localModels.cloudPolish.subtitle" = "将识别文本发送给已配置的云端大模型(默认 DeepSeek)进行润色。仅在 iOS 语音识别效果不理想时(远场、噪声、方言)开启,需提前在设置中填入 DeepSeek API Key。";
"settings.localModels.cloudPolish.caption" = "本地模式下,使用 DeepSeek 进行云端润色。";
"settings.localEngine.title" = "本地引擎";
"settings.language.subtitle.cloud" = "选择识别语言和文字处理模式。";
"settings.language.subtitle.local" = "选择识别语言。";
"settings.mode.title" = "模式";