VoiceInput/Sources/UI/SettingsWindow.swift
JOJO 1d9c9d5fa0 feat: macOS 菜单栏语音输入应用初始版本
- CGEvent cghidEventTap 全局监听 Fn 键
- Apple Speech 流式语音识别(zh-CN 默认)
- 实时音频 RMS 电平驱动波形动画
- NSPanel 胶囊浮动窗口
- 剪贴板 + Cmd+V 文本注入,CJK 输入法自动切换
- OpenAI 兼容 LLM 纠错(DeepSeek 预设)
- LSUIElement 菜单栏运行
- SPM + Makefile 构建
2026-04-24 16:17:57 +08:00

208 lines
7.6 KiB
Swift

import Cocoa
/// LLM
final class SettingsWindow: NSWindow {
private let baseURLField: NSTextField
private let apiKeyField: NSSecureTextField
private let modelField: NSTextField
private let testButton: NSButton
private let saveButton: NSButton
private let statusLabel: NSTextField
init() {
//
baseURLField = NSTextField(frame: .zero)
apiKeyField = NSSecureTextField(frame: .zero)
modelField = NSTextField(frame: .zero)
testButton = NSButton(title: "Test", target: nil, action: nil)
saveButton = NSButton(title: "Save", target: nil, action: nil)
statusLabel = NSTextField(labelWithString: "")
let windowRect = NSRect(x: 0, y: 0, width: 420, height: 280)
super.init(
contentRect: windowRect,
styleMask: [.titled, .closable, .miniaturizable],
backing: .buffered,
defer: false
)
configureWindow()
buildUI()
loadCurrentConfig()
}
private func configureWindow() {
title = "LLM Settings"
isReleasedWhenClosed = false
center()
}
private func buildUI() {
guard let content = contentView else { return }
//
func makeLabel(_ text: String) -> NSTextField {
let label = NSTextField(labelWithString: text)
label.font = .systemFont(ofSize: 11, weight: .semibold)
label.textColor = .secondaryLabelColor
label.alignment = .right
label.translatesAutoresizingMaskIntoConstraints = false
return label
}
func makeField(_ placeholder: String, isSecure: Bool = false) -> NSTextField {
let field = isSecure ? NSSecureTextField(frame: .zero) : NSTextField(frame: .zero)
field.placeholderString = placeholder
field.font = .systemFont(ofSize: 13)
field.bezelStyle = .roundedBezel
field.translatesAutoresizingMaskIntoConstraints = false
return field
}
let baseURLLabel = makeLabel("API Base URL:")
let apiKeyLabel = makeLabel("API Key:")
let modelLabel = makeLabel("Model:")
// 使
baseURLField.placeholderString = "https://api.openai.com"
baseURLField.font = .systemFont(ofSize: 13)
baseURLField.bezelStyle = .roundedBezel
baseURLField.translatesAutoresizingMaskIntoConstraints = false
apiKeyField.placeholderString = "sk-..."
apiKeyField.font = .systemFont(ofSize: 13)
apiKeyField.bezelStyle = .roundedBezel
apiKeyField.translatesAutoresizingMaskIntoConstraints = false
modelField.placeholderString = "gpt-3.5-turbo"
modelField.font = .systemFont(ofSize: 13)
modelField.bezelStyle = .roundedBezel
modelField.translatesAutoresizingMaskIntoConstraints = false
testButton.bezelStyle = .rounded
testButton.translatesAutoresizingMaskIntoConstraints = false
testButton.target = self
testButton.action = #selector(testConnection)
saveButton.bezelStyle = .rounded
saveButton.translatesAutoresizingMaskIntoConstraints = false
saveButton.keyEquivalent = "\r"
saveButton.target = self
saveButton.action = #selector(saveConfig)
statusLabel.font = .systemFont(ofSize: 11)
statusLabel.textColor = .secondaryLabelColor
statusLabel.alignment = .center
statusLabel.maximumNumberOfLines = 2
statusLabel.translatesAutoresizingMaskIntoConstraints = false
//
let grid = NSGridView(views: [
[baseURLLabel, baseURLField],
[apiKeyLabel, apiKeyField],
[modelLabel, modelField]
])
grid.translatesAutoresizingMaskIntoConstraints = false
grid.column(at: 0).xPlacement = .trailing
grid.column(at: 1).width = 260
grid.rowSpacing = 12
grid.columnSpacing = 8
let buttonStack = NSStackView(views: [testButton, saveButton])
buttonStack.translatesAutoresizingMaskIntoConstraints = false
buttonStack.orientation = .horizontal
buttonStack.spacing = 12
buttonStack.distribution = .fillEqually
content.addSubview(grid)
content.addSubview(buttonStack)
content.addSubview(statusLabel)
NSLayoutConstraint.activate([
grid.topAnchor.constraint(equalTo: content.topAnchor, constant: 24),
grid.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
grid.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
buttonStack.topAnchor.constraint(equalTo: grid.bottomAnchor, constant: 20),
buttonStack.centerXAnchor.constraint(equalTo: content.centerXAnchor),
buttonStack.widthAnchor.constraint(equalToConstant: 220),
statusLabel.topAnchor.constraint(equalTo: buttonStack.bottomAnchor, constant: 12),
statusLabel.centerXAnchor.constraint(equalTo: content.centerXAnchor),
statusLabel.widthAnchor.constraint(equalToConstant: 380)
])
}
private func loadCurrentConfig() {
baseURLField.stringValue = Config.shared.llmBaseURL
apiKeyField.stringValue = Config.shared.llmAPIKey
modelField.stringValue = Config.shared.llmModel
}
@objc private func testConnection() {
// 使
statusLabel.stringValue = "Testing..."
statusLabel.textColor = .systemBlue
testButton.isEnabled = false
saveButton.isEnabled = false
let url = baseURLField.stringValue.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let key = apiKeyField.stringValue
let model = modelField.stringValue
guard !url.isEmpty, !key.isEmpty, !model.isEmpty else {
statusLabel.stringValue = "Please fill all fields"
statusLabel.textColor = .systemRed
testButton.isEnabled = true
saveButton.isEnabled = true
return
}
Task { @MainActor in
let refiner = LLMRefiner()
//
let original = (Config.shared.llmBaseURL, Config.shared.llmAPIKey, Config.shared.llmModel)
Config.shared.llmBaseURL = url
Config.shared.llmAPIKey = key
Config.shared.llmModel = model
do {
let result = try await refiner.testConnection(config: Config.shared)
statusLabel.stringValue = "✅ Connected! Response: \(result)"
statusLabel.textColor = .systemGreen
} catch {
statusLabel.stringValue = "❌ Error: \(error.localizedDescription)"
statusLabel.textColor = .systemRed
}
//
Config.shared.llmBaseURL = original.0
Config.shared.llmAPIKey = original.1
Config.shared.llmModel = original.2
testButton.isEnabled = true
saveButton.isEnabled = true
}
}
@objc private func saveConfig() {
let url = baseURLField.stringValue.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let key = apiKeyField.stringValue
let model = modelField.stringValue
Config.shared.llmBaseURL = url
Config.shared.llmAPIKey = key
Config.shared.llmModel = model
statusLabel.stringValue = "✅ Saved"
statusLabel.textColor = .systemGreen
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
self?.statusLabel.stringValue = ""
}
}
}