From 93b6aa6c02ebe0fe443e7f6aacc8d4cc6f0f4d5e Mon Sep 17 00:00:00 2001 From: Rocky Date: Thu, 25 Jun 2026 15:16:04 +0800 Subject: [PATCH] feat(translation-polish): dual-engine translation UX + provider picker filter + topbar cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add LLMProvider.isUserSelectable (default true) and filter ProviderPickerSection on it; next pass can hide non-user presets (e.g. a future DeepSeek key-preset) without changing call sites. - KeyboardRootView: hide TranslationChip when off (matches user's mental model of an opt-in feature), drop the 'warming' branch (Qwen3 download UX was removed with the backend in v0.2.0), unify chip pill height to minHeight 28 + vertical 6 for visual rhythm across all topbar chips. - TranslationChip: drop isLocal warning path — both engines now run the translate-and-polish step (local routes through DeepSeek via ProviderConfig.localModeProviderId). - OnboardingView: cloud engine branch now wraps the translation row in the same surface card chrome as the local branch. - Strings: drop keyboard.models.warming (no longer referenced). DeepSeek key pre-fill deferred to a follow-up. --- OSGKeyboard/Views/OnboardingView.swift | 5 +++ OSGKeyboard/Views/ProviderPickerSection.swift | 9 +++-- OSGKeyboardExt/Views/KeyboardRootView.swift | 29 ++++++++-------- OSGKeyboardExt/Views/TranslationChip.swift | 34 ++++++------------- OSGKeyboardExt/en.lproj/Keyboard.strings | 1 - OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings | 1 - OSGKeyboardShared/Models/LLMProvider.swift | 10 +++++- 7 files changed, 46 insertions(+), 43 deletions(-) diff --git a/OSGKeyboard/Views/OnboardingView.swift b/OSGKeyboard/Views/OnboardingView.swift index 183944a..ce115f6 100644 --- a/OSGKeyboard/Views/OnboardingView.swift +++ b/OSGKeyboard/Views/OnboardingView.swift @@ -742,6 +742,11 @@ private struct APISetupPage: View { // step through DeepSeek, so the constraint is gone). // Wrapped in the same surface card chrome as the // APISettingsCard above for visual symmetry. + // + // v0.2.1 final review (topbar cleanup pass): the + // surface card chrome is owned by `translationSection` + // itself, so both branches get it for free — no + // per-branch duplication. if config.isTranslationRowVisible { translationSection .padding(.horizontal, Spacing.lg) diff --git a/OSGKeyboard/Views/ProviderPickerSection.swift b/OSGKeyboard/Views/ProviderPickerSection.swift index 4a0be00..fbbff9a 100644 --- a/OSGKeyboard/Views/ProviderPickerSection.swift +++ b/OSGKeyboard/Views/ProviderPickerSection.swift @@ -10,15 +10,20 @@ struct ProviderPickerSection: View { @ObservedObject var config: ProviderConfig var body: some View { + // v0.2.1 follow-up: filter out presets marked as + // `isUserSelectable == false` so a future "DeepSeek key + // pre-fill" preset (or similar) can ship in `presets` without + // showing up in the picker. + let visiblePresets = LLMProvider.presets.filter { $0.isUserSelectable } VStack(spacing: 0) { - ForEach(Array(LLMProvider.presets.enumerated()), id: \.element.id) { index, provider in + ForEach(Array(visiblePresets.enumerated()), id: \.element.id) { index, provider in Button { select(provider) } label: { row(provider, selected: provider.id == config.providerId) } .buttonStyle(.plain) - if index < LLMProvider.presets.count - 1 { + if index < visiblePresets.count - 1 { Divider().background(palette.divider) } } diff --git a/OSGKeyboardExt/Views/KeyboardRootView.swift b/OSGKeyboardExt/Views/KeyboardRootView.swift index 230a1e7..5bc287f 100644 --- a/OSGKeyboardExt/Views/KeyboardRootView.swift +++ b/OSGKeyboardExt/Views/KeyboardRootView.swift @@ -80,7 +80,13 @@ public struct KeyboardRootView: View { // and doubles as both the on/off switch and the target- // language picker (Menu pattern matches LocaleChip so the // top bar stays visually consistent). - TranslationChip(state: state) + // v0.2.1 final review: only render the chip when translation + // is actually on. Off-by-default keeps the top bar compact + // for users who don't need translation; the menu still lives + // in onboarding so the feature is discoverable. + if state.translationEnabled { + TranslationChip(state: state) + } Spacer(minLength: 0) StatusBadge(phase: state.phase, onDeviceSupported: state.onDeviceSupported) Button(action: state.openSettings) { @@ -231,13 +237,6 @@ private struct TranscriptLine: View { } .buttonStyle(.plain) .accessibilityHint(ExtL10n.text("keyboard.models.downloadHint")) - } else if isLocalEngine, localModelsReady, !localModelsLoaded { - HStack(spacing: 6) { - ProgressView().controlSize(.mini).tint(palette.textSecondary) - ExtL10n.text("keyboard.models.warming") - .font(TypeStyle.caption) - .foregroundStyle(palette.textSecondary) - } } else if flowSessionActive { ExtL10n.text("keyboard.placeholder.idle") .font(TypeStyle.caption) @@ -418,7 +417,7 @@ private struct StatusBadge: View { .foregroundStyle(palette.textSecondary) } .padding(.horizontal, Spacing.xs) - .padding(.vertical, 3) + .padding(.vertical, 4) .background(palette.surface, in: Capsule()) .overlay(Capsule().stroke(palette.divider, lineWidth: 0.5)) } @@ -437,8 +436,8 @@ private struct CloudEngineChip: View { .font(TypeStyle.caption2) .foregroundStyle(palette.accent) .padding(.horizontal, Spacing.xs + 2) - .padding(.vertical, 5) - .frame(minHeight: 26) + .padding(.vertical, 6) + .frame(minHeight: 28) .background(palette.accent.opacity(0.15), in: Capsule()) .overlay(Capsule().stroke(palette.accent.opacity(0.35), lineWidth: 0.5)) } @@ -457,8 +456,8 @@ private struct LocalEngineChip: View { .font(TypeStyle.caption2) .foregroundStyle(palette.accent) .padding(.horizontal, Spacing.xs + 2) - .padding(.vertical, 5) - .frame(minHeight: 26) + .padding(.vertical, 6) + .frame(minHeight: 28) .background(palette.accent.opacity(0.15), in: Capsule()) .overlay(Capsule().stroke(palette.accent.opacity(0.35), lineWidth: 0.5)) } @@ -504,8 +503,8 @@ private struct LocaleChip: View { .font(TypeStyle.caption2) .foregroundStyle(palette.textPrimary) .padding(.horizontal, Spacing.xs + 2) - .padding(.vertical, 5) - .frame(minHeight: 26) + .padding(.vertical, 6) + .frame(minHeight: 28) .background(palette.surfaceElevated, in: Capsule()) .overlay(Capsule().stroke(palette.divider, lineWidth: 0.5)) } diff --git a/OSGKeyboardExt/Views/TranslationChip.swift b/OSGKeyboardExt/Views/TranslationChip.swift index 0fb8947..7d45cad 100644 --- a/OSGKeyboardExt/Views/TranslationChip.swift +++ b/OSGKeyboardExt/Views/TranslationChip.swift @@ -23,7 +23,7 @@ // • on (any engine) → accent fill, "→ EN" / "→ 日本語" style label // // Stays in the same visual family as `CloudEngineChip` / `LocaleChip` -// (Capsule + 26 pt min height + 5 pt vertical padding) so the top bar +// (Capsule + 28 pt min height + 6 pt vertical padding) so the top bar // doesn't grow when translation is enabled. import SwiftUI @@ -63,22 +63,21 @@ struct TranslationChip: View { @ViewBuilder private var label: some View { let target = TranslationLanguageCatalog.resolve(state.translationTargetLocaleId) - let isLocal = state.isLocalEngine let enabled = state.translationEnabled HStack(spacing: 4) { Image(systemName: enabled ? "character.bubble" : "character.bubble.fill") - Text(chipLabel(target: target, enabled: enabled, isLocal: isLocal)) + Text(chipLabel(target: target, enabled: enabled)) Image(systemName: "chevron.down") .font(.system(size: 8, weight: .bold)) } .font(TypeStyle.caption2) - .foregroundStyle(foreground(enabled: enabled, isLocal: isLocal)) + .foregroundStyle(foreground(enabled: enabled)) .padding(.horizontal, Spacing.xs + 2) - .padding(.vertical, 5) - .frame(minHeight: 26) - .background(background(enabled: enabled, isLocal: isLocal), in: Capsule()) - .overlay(Capsule().stroke(stroke(enabled: enabled, isLocal: isLocal), lineWidth: 0.5)) + .padding(.vertical, 6) + .frame(minHeight: 28) + .background(background(enabled: enabled), in: Capsule()) + .overlay(Capsule().stroke(stroke(enabled: enabled), lineWidth: 0.5)) } /// Active selection id — the chip derives "on" from a non-off @@ -94,11 +93,7 @@ struct TranslationChip: View { return language.nativeName } - private func chipLabel(target: TranslationLanguage, enabled: Bool, isLocal: Bool) -> String { - // v0.2.1 follow-up: with the local engine now routing the - // polish / translate step through DeepSeek, the chip shows - // the same "→EN"-style label on both engines. There's no - // "needs cloud" hint path anymore. + private func chipLabel(target: TranslationLanguage, enabled: Bool) -> String { if !enabled { return ExtL10n.string("keyboard.translation.off") } @@ -125,24 +120,17 @@ struct TranslationChip: View { } } - private func foreground(enabled: Bool, isLocal: Bool) -> Color { - // v0.2.1 follow-up: the chip no longer needs a "warning" path - // for local + on — both engines share the accent treatment - // now. `isLocal` is kept in the signature so callers don't - // need to change; it's intentionally unused below. - _ = isLocal + private func foreground(enabled: Bool) -> Color { if enabled { return palette.accent } return palette.textPrimary } - private func background(enabled: Bool, isLocal: Bool) -> Color { - _ = isLocal + private func background(enabled: Bool) -> Color { if enabled { return palette.accent.opacity(0.15) } return palette.surfaceElevated } - private func stroke(enabled: Bool, isLocal: Bool) -> Color { - _ = isLocal + private func stroke(enabled: Bool) -> Color { if enabled { return palette.accent.opacity(0.35) } return palette.divider } diff --git a/OSGKeyboardExt/en.lproj/Keyboard.strings b/OSGKeyboardExt/en.lproj/Keyboard.strings index 5859671..a288ad1 100644 --- a/OSGKeyboardExt/en.lproj/Keyboard.strings +++ b/OSGKeyboardExt/en.lproj/Keyboard.strings @@ -121,7 +121,6 @@ "keyboard.placeholder.cloudBadge" = "Cloud"; "keyboard.models.notDownloaded" = "On-device models not downloaded"; "keyboard.models.downloadHint" = "Open OSGKeyboard to download models"; -"keyboard.models.warming" = "Loading models…"; "keyboard.rec" = "REC"; "keyboard.space" = "Space"; "keyboard.denied.mic" = "Mic denied"; diff --git a/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings b/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings index 726eca0..c74a1e1 100644 --- a/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings +++ b/OSGKeyboardExt/zh-Hans.lproj/Keyboard.strings @@ -121,7 +121,6 @@ "keyboard.placeholder.cloudBadge" = "云端"; "keyboard.models.notDownloaded" = "本地模型尚未下载"; "keyboard.models.downloadHint" = "打开 OSGKeyboard 下载模型"; -"keyboard.models.warming" = "正在加载模型…"; "keyboard.rec" = "REC"; "keyboard.space" = "空格"; "keyboard.denied.mic" = "麦克风被拒绝"; diff --git a/OSGKeyboardShared/Models/LLMProvider.swift b/OSGKeyboardShared/Models/LLMProvider.swift index 53fe351..7e6fcdd 100644 --- a/OSGKeyboardShared/Models/LLMProvider.swift +++ b/OSGKeyboardShared/Models/LLMProvider.swift @@ -14,6 +14,12 @@ public struct LLMProvider: Identifiable, Codable, Hashable, Sendable { public let apiKeyURL: URL? /// Optional short blurb shown under the provider name in the picker. public let blurb: String? + /// Whether this preset should appear in user-facing provider pickers + /// (settings / onboarding). Defaults to `true` so the existing + /// `presets` array keeps its public surface area; future passes can + /// mark e.g. a DeepSeek key-pre-fill preset as `false` to hide it + /// from the picker without touching call sites. + public let isUserSelectable: Bool public init( id: String, @@ -21,7 +27,8 @@ public struct LLMProvider: Identifiable, Codable, Hashable, Sendable { defaultBaseURL: String, defaultModel: String, apiKeyURL: URL? = nil, - blurb: String? = nil + blurb: String? = nil, + isUserSelectable: Bool = true ) { self.id = id self.name = name @@ -29,6 +36,7 @@ public struct LLMProvider: Identifiable, Codable, Hashable, Sendable { self.defaultModel = defaultModel self.apiKeyURL = apiKeyURL self.blurb = blurb + self.isUserSelectable = isUserSelectable } public static let presets: [LLMProvider] = [