import AppKit import ConjureKit import Security @MainActor final class TranslationController: NSViewController { private let keyField = NSSecureTextField() private let output = NSTextView() private let status = NSTextField(wrappingLabelWithString: "English → Japanese · gpt-6-luna · Text is sent to OpenAI only when you translate.") private let translateButton = NSButton(title: "Translate ↵", target: nil, action: nil) private var task: Task? var input: (() -> String)? var closed: (() -> Void)? private static let credential: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "Conjure.OpenAI", kSecAttrAccount as String: "translation", ] override func loadView() { view = NSView() let back = NSButton(title: "‹ Back", target: self, action: #selector(goBack)) let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese") title.font = .systemFont(ofSize: 13, weight: .semibold) let heading = NSStackView(views: [back, title, NSView()]) keyField.placeholderString = "OpenAI API key (saved in Keychain)" keyField.setAccessibilityLabel("OpenAI API key") keyField.widthAnchor.constraint(greaterThanOrEqualToConstant: 220).isActive = true let save = NSButton(title: "Save Key", target: self, action: #selector(saveKey)) let forget = NSButton(title: "Forget Key", target: self, action: #selector(forgetKey)) let credentials = NSStackView(views: [keyField, save, forget]) credentials.distribution = .fill translateButton.target = self translateButton.action = #selector(translate) translateButton.keyEquivalent = "\r" output.isEditable = false output.isSelectable = true output.drawsBackground = false output.textColor = SunsetPalette.text output.font = .systemFont(ofSize: 20) output.setAccessibilityLabel("Japanese translation") let scroll = NSScrollView() scroll.documentView = output scroll.hasVerticalScroller = true scroll.drawsBackground = false output.isVerticallyResizable = true output.autoresizingMask = [.width] output.textContainer?.widthTracksTextView = true status.textColor = SunsetPalette.secondaryText status.font = .systemFont(ofSize: 12) let stack = NSStackView(views: [heading, credentials, scroll, status, translateButton]) stack.orientation = .vertical stack.alignment = .leading stack.spacing = 12 stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), stack.topAnchor.constraint(equalTo: view.topAnchor), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16), credentials.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), status.widthAnchor.constraint(equalTo: stack.widthAnchor), ]) } @objc func goBack() { task?.cancel() task = nil closed?() } func inputChanged() { task?.cancel() task = nil translateButton.isEnabled = true output.string = "" status.stringValue = "Press Return to translate English → Japanese with gpt-6-luna." } @objc private func saveKey() { let key = keyField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) guard !key.isEmpty, !key.contains(where: { $0.isNewline }) else { status.stringValue = "Enter a valid API key before saving." return } let data = Data(key.utf8) var result = SecItemUpdate(Self.credential as CFDictionary, [kSecValueData as String: data] as CFDictionary) if result == errSecItemNotFound { var item = Self.credential item[kSecValueData as String] = data item[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly result = SecItemAdd(item as CFDictionary, nil) } if result == errSecSuccess { keyField.stringValue = "" status.stringValue = "API key saved in Keychain. Press Return to translate." } else { showKeychainError(result) } } @objc private func forgetKey() { task?.cancel() task = nil translateButton.isEnabled = true let result = SecItemDelete(Self.credential as CFDictionary) if result == errSecSuccess || result == errSecItemNotFound { keyField.stringValue = "" status.stringValue = "API key removed." } else { showKeychainError(result) } } private func showKeychainError(_ code: OSStatus) { status.stringValue = "Keychain: \(SecCopyErrorMessageString(code, nil) as String? ?? String(code))" } @objc func translate() { guard task == nil else { return } let text = input?() ?? "" guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { status.stringValue = "Enter English text in the search bar." return } if !keyField.stringValue.isEmpty { saveKey(); guard keyField.stringValue.isEmpty else { return } } var query = Self.credential query[kSecReturnData as String] = true var item: CFTypeRef? let result = SecItemCopyMatching(query as CFDictionary, &item) guard result == errSecSuccess, let data = item as? Data, let key = String(data: data, encoding: .utf8) else { if result == errSecItemNotFound { status.stringValue = "Enter and save your OpenAI API key first." } else { showKeychainError(result) } return } output.string = "" status.stringValue = "Translating…" translateButton.isEnabled = false task = Task { [weak self] in do { let translation = try await TranslationAPI.translate(text, apiKey: key) guard !Task.isCancelled, let self else { return } self.output.string = translation self.status.stringValue = "Japanese · Select text to copy (⌘C)." } catch { guard !Task.isCancelled, let self else { return } self.status.stringValue = error.localizedDescription } guard let self else { return } self.task = nil self.translateButton.isEnabled = true } } }