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 copyFromOtherButton: 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) copyFromOtherButton = NSButton(title: "", target: nil, action: nil) statusLabel = NSTextField(labelWithString: "") let windowRect = NSRect(x: 0, y: 0, width: 420, height: 320) 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) let otherModeName = mode == .input ? "AI 助手" : "语音输入" copyFromOtherButton.title = "从「\(otherModeName)」复制配置" copyFromOtherButton.bezelStyle = .rounded copyFromOtherButton.translatesAutoresizingMaskIntoConstraints = false copyFromOtherButton.target = self copyFromOtherButton.action = #selector(copyFromOther) 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(copyFromOtherButton) 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), copyFromOtherButton.topAnchor.constraint(equalTo: buttonStack.bottomAnchor, constant: 10), copyFromOtherButton.centerXAnchor.constraint(equalTo: content.centerXAnchor), statusLabel.topAnchor.constraint(equalTo: copyFromOtherButton.bottomAnchor, constant: 10), 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 copyFromOther() { let otherMode: LLMMode = mode == .input ? .assist : .input let config = Config.shared.getLLMConfig(for: otherMode) baseURLField.stringValue = config.baseURL apiKeyField.stringValue = config.apiKey modelField.stringValue = config.model statusLabel.stringValue = "✅ 已复制「\(otherMode == .input ? "语音输入" : "AI 助手")」的配置" statusLabel.textColor = .systemGreen DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in self?.statusLabel.stringValue = "" } } @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 = "" } } }