feat: initial release v0.1.0
- Custom Keyboard Extension with push-to-talk UI - iOS 26 SpeechAnalyzer + DictationTranscriber (iOS 18 SF fallback) - OpenAI-compatible LLM client (4 built-in providers + custom) - 3-page onboarding flow + provider config UI - App Group shared storage for cross-process config - 8s LLM timeout with raw-transcript fallback - App Store privacy manifests for both targets - SwiftLint + XcodeGen + GitHub Actions CI - Unit tests (4/4 passing)
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// APISettingsCard.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// Editable fields for the four OpenAI-compatible config values:
|
||||
// Base URL, API Key, Model, System Prompt.
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
struct APISettingsCard: View {
|
||||
@ObservedObject var config: ProviderConfig
|
||||
@State private var showKey: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
field("Base URL", text: $config.baseURL, isSecure: false,
|
||||
keyboard: .URL, autocap: false)
|
||||
keyField
|
||||
field("Model", text: $config.model, isSecure: false,
|
||||
keyboard: .default, autocap: false)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("System Prompt")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textSecondary)
|
||||
TextEditor(text: $config.systemPrompt)
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.scrollContentBackground(.hidden)
|
||||
.frame(minHeight: 110)
|
||||
.padding(8)
|
||||
.background(Color.white.opacity(0.05), in: RoundedRectangle(cornerRadius: 8))
|
||||
Button("Reset to default") {
|
||||
config.systemPrompt = config.defaultSystemPrompt
|
||||
}
|
||||
.font(.caption2)
|
||||
.foregroundStyle(Theme.accent)
|
||||
}
|
||||
|
||||
if let url = LLMProvider.provider(id: config.providerId).apiKeyURL {
|
||||
Link(destination: url) {
|
||||
Label("Get an API key", systemImage: "key.fill")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
.cardStyle()
|
||||
}
|
||||
|
||||
private var keyField: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("API Key")
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textSecondary)
|
||||
Spacer()
|
||||
Button(action: { showKey.toggle() }) {
|
||||
Image(systemName: showKey ? "eye.slash.fill" : "eye.fill")
|
||||
.foregroundStyle(Theme.textSecondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
Group {
|
||||
if showKey {
|
||||
TextField("sk-...", text: $config.apiKey)
|
||||
} else {
|
||||
SecureField("sk-...", text: $config.apiKey)
|
||||
}
|
||||
}
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled(true)
|
||||
.padding(10)
|
||||
.background(Color.white.opacity(0.05), in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func field(
|
||||
_ title: String,
|
||||
text: Binding<String>,
|
||||
isSecure: Bool,
|
||||
keyboard: UIKeyboardType,
|
||||
autocap: Bool
|
||||
) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Theme.textSecondary)
|
||||
Group {
|
||||
if isSecure {
|
||||
SecureField("", text: text)
|
||||
} else {
|
||||
TextField("", text: text)
|
||||
.keyboardType(keyboard)
|
||||
.autocorrectionDisabled(true)
|
||||
}
|
||||
}
|
||||
.textInputAutocapitalization(autocap ? .sentences : .never)
|
||||
.padding(10)
|
||||
.background(Color.white.opacity(0.05), in: RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user