VoiceInput/Sources/UI/SettingsWindow.swift

213 lines
7.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Cocoa
/// LLM
final class SettingsWindow: NSWindow {
private let mode: LLMMode
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(mode: LLMMode) {
self.mode = mode
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 = mode == .input ? "Input LLM Settings (Fn)" : "Assist LLM Settings (Control)"
isReleasedWhenClosed = false
center()
}
// MARK: - LSUIElement Edit
override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Cmd
guard event.modifierFlags.contains(.command) else {
return super.performKeyEquivalent(with: event)
}
let selector: Selector?
switch event.charactersIgnoringModifiers?.lowercased() {
case "a": selector = #selector(NSText.selectAll(_:))
case "c": selector = Selector(("copy:"))
case "v": selector = Selector(("paste:"))
case "x": selector = Selector(("cut:"))
default: selector = nil
}
guard let sel = selector else {
return super.performKeyEquivalent(with: event)
}
//
if NSApp.sendAction(sel, to: nil, from: self) {
return true
}
return super.performKeyEquivalent(with: event)
}
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
}
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 = mode == .input ? "deepseek-v4-flash" : "gpt-4"
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() {
let config = Config.shared.getLLMConfig(for: mode)
baseURLField.stringValue = config.baseURL
apiKeyField.stringValue = config.apiKey
modelField.stringValue = config.model
}
@objc private func testConnection() {
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
return
}
statusLabel.stringValue = "Testing..."
statusLabel.textColor = .systemBlue
testButton.isEnabled = false
saveButton.isEnabled = false
Task { @MainActor in
let refiner = LLMRefiner()
let testConfig = LLMConfig(enabled: true, baseURL: url, apiKey: key, model: model)
do {
let result = try await refiner.testConnection(config: testConfig)
statusLabel.stringValue = "✅ Connected! Response: \(result)"
statusLabel.textColor = .systemGreen
} catch {
statusLabel.stringValue = "❌ Error: \(error.localizedDescription)"
statusLabel.textColor = .systemRed
}
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
let config = LLMConfig(enabled: true, baseURL: url, apiKey: key, model: model)
Config.shared.setLLMConfig(config, for: mode)
statusLabel.stringValue = "✅ Saved"
statusLabel.textColor = .systemGreen
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in
self?.statusLabel.stringValue = ""
}
}
}