ui: move translation credentials into settings
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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 = SunsetPalette.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 is sent to OpenAI only when you explicitly translate. Saving a key does not send your text. Your API key is stored in Keychain."
|
||||
)
|
||||
for label in [status, myMemory, openAI] {
|
||||
label.textColor = SunsetPalette.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 ? .systemRed : SunsetPalette.secondaryText
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user