Files
conjure/Sources/Conjure/TranslationSettingsController.swift
T

91 lines
4.3 KiB
Swift

import AppKit
@MainActor
final class TranslationSettingsController: NSViewController {
private let keyField = NSSecureTextField()
private let status = NSTextField(wrappingLabelWithString: "")
private let credentials: TranslationController.Credentials
init(credentials: TranslationController.Credentials = .keychain) {
self.credentials = credentials
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("Conjure creates Translate settings programmatically") }
override func loadView() {
view = NSView()
keyField.placeholderString = "OpenAI API key (saved in Keychain)"
keyField.setAccessibilityLabel("OpenAI API key")
let save = NSButton(title: "Save Key", target: self, action: #selector(saveKey))
let forget = NSButton(title: "Forget Key", target: self, action: #selector(forgetKey))
for button in [save, forget] {
button.bezelStyle = .rounded
button.contentTintColor = ThemePalette.accent
}
let controls = NSStackView(views: [keyField, save, forget])
controls.orientation = .horizontal
controls.alignment = .centerY
controls.spacing = 8
keyField.setContentHuggingPriority(.defaultLow, for: .horizontal)
keyField.widthAnchor.constraint(greaterThanOrEqualToConstant: 220).isActive = true
status.setAccessibilityLabel("Translation credential status and errors")
let myMemory = NSTextField(wrappingLabelWithString:
"MyMemory · No key · 500 bytes/request · 5,000 characters/day. Text is sent to MyMemory only when you translate; avoid sensitive content."
)
let openAI = NSTextField(wrappingLabelWithString:
"OpenAI · gpt-6-luna · Text and optional situation/context are sent to OpenAI only when you explicitly translate, not when advancing to context. Saving a key does not send your text. Your API key is stored in Keychain."
)
for label in [status, myMemory, openAI] {
label.textColor = ThemePalette.secondaryText
}
let stack = NSStackView(views: [controls, status, myMemory, openAI])
stack.orientation = .vertical
stack.alignment = .leading
stack.spacing = 12
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
stack.topAnchor.constraint(equalTo: view.topAnchor),
stack.bottomAnchor.constraint(equalTo: view.bottomAnchor),
controls.widthAnchor.constraint(equalTo: stack.widthAnchor),
])
for label in [status, myMemory, openAI] {
label.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
}
do {
setStatus(try credentials.load() == nil ? "No OpenAI API key saved." : "OpenAI API key saved in Keychain.")
} catch { setStatus(error.localizedDescription, isError: true) }
}
@objc private func saveKey() {
let key = keyField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty, !key.contains(where: { $0.isNewline }) else {
setStatus("Enter a valid API key before saving.", isError: true)
return
}
do {
try credentials.save(key)
keyField.stringValue = ""
setStatus("API key saved in Keychain.")
NotificationCenter.default.post(name: Notification.Name("ConjureTranslationCredentialsChanged"), object: self)
} catch { setStatus(error.localizedDescription, isError: true) }
}
@objc private func forgetKey() {
do {
try credentials.forget()
keyField.stringValue = ""
setStatus("API key removed.")
NotificationCenter.default.post(name: Notification.Name("ConjureTranslationCredentialsChanged"), object: self)
} catch { setStatus(error.localizedDescription, isError: true) }
}
private func setStatus(_ message: String, isError: Bool = false) {
status.stringValue = message
status.textColor = isError ? ThemePalette.error : ThemePalette.secondaryText
}
}