- CGEvent cghidEventTap 全局监听 Fn 键 - Apple Speech 流式语音识别(zh-CN 默认) - 实时音频 RMS 电平驱动波形动画 - NSPanel 胶囊浮动窗口 - 剪贴板 + Cmd+V 文本注入,CJK 输入法自动切换 - OpenAI 兼容 LLM 纠错(DeepSeek 预设) - LSUIElement 菜单栏运行 - SPM + Makefile 构建
208 lines
7.6 KiB
Swift
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 = ""
|
|
}
|
|
}
|
|
}
|