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 = "" } } }