05e005e9ce
Batch of in-progress app work from the working tree. - feat(keyboard): CursorNavigation + CursorDragPad for caret movement; KeyboardSoundFeedback for system key click sounds - feat(dictionary): DictionaryAliasGenerator + PersonalDictionaryEntrySheet; TranscriptPostProcessor quality gate; retire DictionaryLearner - feat(ui): TabBarVisibility handling; drop PageHeaderRow / PageHeaderConfirmButton; refresh views and localizable strings - fix(security): move the hardcoded DeepSeek key out of PreconfiguredKeys.swift into a gitignored PreconfiguredKeys.local.swift (seeded from .example by generate-xcodeproj.sh) - docs(agents): add Conventional Commits versioning + bilingual changelog rules - chore(gitignore): ignore PreconfiguredKeys.local.swift, .cache/, pycache Custom language model / lexicon work stays on feature/custom-language-model-asr. Changelog bullets added under [Unreleased]; no version bump.
150 lines
5.3 KiB
Swift
150 lines
5.3 KiB
Swift
// EnginePickerSection.swift
|
|
// OSGKeyboard · Main App
|
|
//
|
|
// Engine picker — Local (on-device ASR) vs
|
|
// Cloud (ASR + optional LLM polish via the user's API).
|
|
|
|
import SwiftUI
|
|
import OSGKeyboardShared
|
|
|
|
struct EnginePickerSection: View {
|
|
@Environment(\.themePalette) private var palette: ThemePalette
|
|
|
|
@ObservedObject var config: ProviderConfig
|
|
@State private var showCloudAcknowledgment = false
|
|
@State private var pendingEngineSelection: String?
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: SettingsListMetrics.sectionLabelSpacing) {
|
|
sectionHeader("settings.engine.title")
|
|
VStack(spacing: 0) {
|
|
engineOptionRow(
|
|
id: "local",
|
|
systemIcon: "iphone.badge.checkmark",
|
|
title: AppL10n.string("settings.engine.local.title"),
|
|
subtitle: localSubtitle
|
|
)
|
|
Divider().background(palette.divider)
|
|
engineOptionRow(
|
|
id: "cloud",
|
|
systemIcon: "wand.and.stars",
|
|
title: AppL10n.string("settings.engine.cloud.title"),
|
|
subtitle: AppL10n.string("settings.engine.cloud.subtitle")
|
|
)
|
|
}
|
|
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
|
|
.stroke(palette.divider, lineWidth: 0.5)
|
|
)
|
|
|
|
if config.engineMode == "cloud" {
|
|
CloudPolishDisclosureBanner()
|
|
}
|
|
}
|
|
.cloudSharingAcknowledgment(
|
|
config: config,
|
|
isPresented: $showCloudAcknowledgment,
|
|
onConfirm: { applyPendingEngineSelection() },
|
|
onCancel: { pendingEngineSelection = nil }
|
|
)
|
|
}
|
|
|
|
private var localSubtitle: String {
|
|
AppL10n.string("settings.engine.local.legacy")
|
|
}
|
|
|
|
private func engineOptionRow(
|
|
id: String,
|
|
assetName: String? = nil,
|
|
systemIcon: String? = nil,
|
|
title: String,
|
|
subtitle: String
|
|
) -> some View {
|
|
let isSelected = config.engineMode == id
|
|
return Button {
|
|
guard config.engineMode != id else { return }
|
|
if id == "cloud", !config.hasAcknowledgedCloudSharing {
|
|
pendingEngineSelection = id
|
|
showCloudAcknowledgment = true
|
|
return
|
|
}
|
|
selectEngine(id)
|
|
} label: {
|
|
HStack(spacing: Spacing.sm) {
|
|
engineMark(
|
|
assetName: assetName,
|
|
systemIcon: systemIcon,
|
|
isSelected: isSelected
|
|
)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(title)
|
|
.font(TypeStyle.body)
|
|
.foregroundStyle(isSelected ? palette.accent : palette.textPrimary)
|
|
Text(subtitle)
|
|
.font(TypeStyle.caption2)
|
|
.foregroundStyle(palette.textTertiary)
|
|
}
|
|
Spacer()
|
|
if isSelected {
|
|
Image(systemName: "checkmark")
|
|
.font(.system(size: 13, weight: .semibold))
|
|
.foregroundStyle(palette.accent)
|
|
}
|
|
}
|
|
.padding(.horizontal, Spacing.md)
|
|
.frame(minHeight: SettingsListMetrics.doubleLineMinHeight)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
private func selectEngine(_ id: String) {
|
|
withAnimation(.easeInOut(duration: 0.2)) {
|
|
config.engineMode = id
|
|
if id == "cloud" {
|
|
config.modeId = "polish"
|
|
if config.providerId == "deepseek" {
|
|
config.apply(preset: LLMProvider.provider(id: "openai"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func applyPendingEngineSelection() {
|
|
guard let id = pendingEngineSelection else { return }
|
|
pendingEngineSelection = nil
|
|
selectEngine(id)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func engineMark(assetName: String?, systemIcon: String?, isSelected: Bool) -> some View {
|
|
ZStack {
|
|
Circle()
|
|
.fill(isSelected ? palette.accentMuted : palette.surfaceElevated)
|
|
.frame(width: 32, height: 32)
|
|
if let assetName {
|
|
Image(assetName)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 18, height: 18)
|
|
.foregroundStyle(isSelected ? palette.accent : palette.textPrimary)
|
|
} else if let systemIcon {
|
|
Image(systemName: systemIcon)
|
|
.font(.system(size: 16, weight: .medium))
|
|
.foregroundStyle(isSelected ? palette.accent : palette.textSecondary)
|
|
}
|
|
}
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func sectionHeader(_ title: LocalizedStringKey) -> some View {
|
|
Text(title)
|
|
.font(TypeStyle.caption2)
|
|
.foregroundStyle(palette.textSecondary)
|
|
.textCase(.uppercase)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|