Files
OSGKeyboard/OSGKeyboard/Views/ProviderPickerSection.swift
T
rocky 93b6aa6c02 feat(translation-polish): dual-engine translation UX + provider picker filter + topbar cleanup
- 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.
2026-06-25 15:16:33 +08:00

100 lines
3.4 KiB
Swift

// ProviderPickerSection.swift
// OSGKeyboard · Main App
import SwiftUI
import OSGKeyboardShared
struct ProviderPickerSection: View {
@Environment(\.themePalette) private var palette: ThemePalette
@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(visiblePresets.enumerated()), id: \.element.id) { index, provider in
Button {
select(provider)
} label: {
row(provider, selected: provider.id == config.providerId)
}
.buttonStyle(.plain)
if index < visiblePresets.count - 1 {
Divider().background(palette.divider)
}
}
}
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: Radius.large, style: .continuous)
.stroke(palette.divider, lineWidth: 0.5)
)
}
private func select(_ provider: LLMProvider) {
withAnimation(Motion.quick) {
config.apply(preset: provider)
}
}
@ViewBuilder
private func row(_ provider: LLMProvider, selected: Bool) -> some View {
HStack(spacing: Spacing.sm) {
providerMark(provider, selected: selected)
Text(ProviderDisplayName.name(for: provider.id))
.font(TypeStyle.body)
.foregroundStyle(palette.textPrimary)
Spacer(minLength: Spacing.xs)
if selected {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 17, weight: .medium))
.foregroundStyle(palette.accent)
}
}
.padding(.horizontal, Spacing.md)
.frame(minHeight: SettingsListMetrics.singleLineMinHeight)
.contentShape(Rectangle())
}
@ViewBuilder
private func providerMark(_ provider: LLMProvider, selected: Bool) -> some View {
ZStack {
Circle()
.fill(selected ? palette.accentMuted : palette.surfaceElevated)
.frame(width: 32, height: 32)
if let asset = ProviderLogo.assetName(for: provider.id) {
Image(asset)
.resizable()
.scaledToFit()
.frame(width: 18, height: 18)
.foregroundStyle(selected ? palette.accent : palette.textPrimary)
} else {
Text(String(provider.name.prefix(1)))
.font(TypeStyle.caption)
.foregroundStyle(selected ? palette.accent : palette.textSecondary)
}
}
}
}
enum ProviderLogo {
static func assetName(for providerId: String) -> String? {
switch providerId {
case "openai": return "openai"
case "deepseek": return "deepseek"
case "qwen": return "qwen"
case "moonshot": return "moonshot"
case "zhipu": return "zhipu"
case "custom": return "custom"
default: return nil
}
}
}