feat: translate with situational context in OpenAI
This commit is contained in:
@@ -12,9 +12,18 @@ final class TranslationController: NSViewController {
|
||||
private(set) var sourceLanguage = "en"
|
||||
private(set) var targetLanguage = "ja"
|
||||
var focusSearch: (() -> Void)?
|
||||
var languagesChanged: (() -> Void)?
|
||||
var inputPlaceholder: String { "Enter \(languageName(sourceLanguage)) text to translate…" }
|
||||
var inputAccessibilityLabel: String { "\(languageName(sourceLanguage)) text to translate into \(languageName(targetLanguage))" }
|
||||
var inputPresentationChanged: (() -> Void)?
|
||||
var inputPlaceholder: String {
|
||||
textToTranslate == nil ? "Enter \(languageName(sourceLanguage)) text to translate…"
|
||||
: "Describe where or how it was used (optional)…"
|
||||
}
|
||||
var inputAccessibilityLabel: String {
|
||||
textToTranslate == nil ? "\(languageName(sourceLanguage)) text to translate into \(languageName(targetLanguage))"
|
||||
: "Context or situation for the translation (optional)"
|
||||
}
|
||||
var backAccessibilityLabel: String {
|
||||
textToTranslate == nil ? "Back to applications" : "Edit original text"
|
||||
}
|
||||
private static let languages = Locale.LanguageCode.isoLanguageCodes.map(\.identifier).filter { $0.count == 2 }
|
||||
.sorted { languageName($0).localizedCaseInsensitiveCompare(languageName($1)) == .orderedAscending }
|
||||
private static func languageName(_ code: String) -> String {
|
||||
@@ -22,19 +31,22 @@ final class TranslationController: NSViewController {
|
||||
}
|
||||
private func languageName(_ code: String) -> String { Self.languageName(code) }
|
||||
private var usesMyMemory: Bool
|
||||
private var textToTranslate: String?
|
||||
private let sourcePreview = NSTextField(wrappingLabelWithString: "")
|
||||
private let output = NSTextView()
|
||||
private let settingsButton = NSButton(title: "Open Translate Settings", target: nil, action: nil)
|
||||
private let status = NSTextField(wrappingLabelWithString: "")
|
||||
private let translateButton = NSButton(title: "Translate", target: nil, action: nil)
|
||||
private var task: Task<Void, Never>?
|
||||
var input: (() -> String)?
|
||||
var replaceInput: ((String) -> Void)?
|
||||
var closed: (() -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
private let pasteboard: NSPasteboard
|
||||
private let defaults: UserDefaults
|
||||
private let credentialsStore: Credentials
|
||||
private let myMemory: @MainActor (String, String, String) async throws -> String
|
||||
private let openAI: @MainActor (String, String, String, String) async throws -> String
|
||||
private let openAI: @MainActor (String, String, String, String, String) async throws -> String
|
||||
|
||||
init(
|
||||
defaults: UserDefaults = .standard,
|
||||
@@ -42,8 +54,8 @@ final class TranslationController: NSViewController {
|
||||
myMemory: @escaping @MainActor (String, String, String) async throws -> String = {
|
||||
try await MyMemoryAPI.translate($0, sourceLanguage: $1, targetLanguage: $2)
|
||||
},
|
||||
openAI: @escaping @MainActor (String, String, String, String) async throws -> String = {
|
||||
try await TranslationAPI.translate($0, apiKey: $3, sourceLanguage: $1, targetLanguage: $2)
|
||||
openAI: @escaping @MainActor (String, String, String, String, String) async throws -> String = {
|
||||
try await TranslationAPI.translate($0, apiKey: $3, sourceLanguage: $1, targetLanguage: $2, context: $4)
|
||||
},
|
||||
pasteboard: NSPasteboard = .general
|
||||
) {
|
||||
@@ -141,13 +153,24 @@ final class TranslationController: NSViewController {
|
||||
status.font = .systemFont(ofSize: 12)
|
||||
status.setAccessibilityLabel("Translation status and errors")
|
||||
status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
scroll.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(scroll)
|
||||
sourcePreview.font = .systemFont(ofSize: 14)
|
||||
sourcePreview.textColor = SunsetPalette.secondaryText
|
||||
sourcePreview.maximumNumberOfLines = 3
|
||||
sourcePreview.lineBreakMode = .byTruncatingTail
|
||||
sourcePreview.setAccessibilityLabel("Original text to translate")
|
||||
let body = NSStackView(views: [sourcePreview, scroll])
|
||||
body.orientation = .vertical
|
||||
body.alignment = .leading
|
||||
body.spacing = 12
|
||||
body.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(body)
|
||||
NSLayoutConstraint.activate([
|
||||
scroll.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
||||
scroll.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
||||
scroll.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
scroll.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -12),
|
||||
body.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
||||
body.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
||||
body.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
body.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -12),
|
||||
sourcePreview.widthAnchor.constraint(equalTo: body.widthAnchor),
|
||||
scroll.widthAnchor.constraint(equalTo: body.widthAnchor),
|
||||
footer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
||||
footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8),
|
||||
footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8),
|
||||
@@ -221,7 +244,7 @@ final class TranslationController: NSViewController {
|
||||
defaults.set(source, forKey: "translationSourceLanguage")
|
||||
defaults.set(target, forKey: "translationTargetLanguage")
|
||||
inputChanged()
|
||||
languagesChanged?()
|
||||
inputPresentationChanged?()
|
||||
}
|
||||
|
||||
func swapLanguages() { setLanguages(source: targetLanguage, target: sourceLanguage) }
|
||||
@@ -233,17 +256,36 @@ final class TranslationController: NSViewController {
|
||||
}
|
||||
|
||||
@objc func goBack() {
|
||||
if actions.isShown { actions.close() } else { close() }
|
||||
if actions.isShown {
|
||||
actions.close()
|
||||
} else if let text = textToTranslate {
|
||||
textToTranslate = nil
|
||||
replaceInput?(text)
|
||||
inputChanged()
|
||||
inputPresentationChanged?()
|
||||
focusSearch?()
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
func inputChanged() {
|
||||
cancelRequest()
|
||||
output.string = ""
|
||||
let isEmpty = (input?() ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
translateButton.title = isEmpty ? "Swap languages" : "Translate"
|
||||
translateButton.toolTip = isEmpty
|
||||
? "Swap source and target languages (Return)."
|
||||
: "Translate the input (Return). Escape dismisses Conjure."
|
||||
if textToTranslate != nil {
|
||||
translateButton.title = "Translate"
|
||||
translateButton.toolTip = "Translate using the context, or leave it blank (Return)."
|
||||
} else if isEmpty {
|
||||
translateButton.title = "Swap languages"
|
||||
translateButton.toolTip = "Swap source and target languages (Return)."
|
||||
} else {
|
||||
translateButton.title = usesMyMemory ? "Translate" : "Add context"
|
||||
translateButton.toolTip = usesMyMemory ? "Translate the input (Return)."
|
||||
: "Continue to context without sending the text (Return)."
|
||||
}
|
||||
sourcePreview.stringValue = textToTranslate.map { "Text to translate: \($0)" } ?? ""
|
||||
sourcePreview.isHidden = textToTranslate == nil
|
||||
settingsButton.isHidden = true
|
||||
languageButton.title = "\(languageName(sourceLanguage)) → \(languageName(targetLanguage))"
|
||||
output.setAccessibilityLabel("\(languageName(targetLanguage)) translation")
|
||||
@@ -256,7 +298,12 @@ final class TranslationController: NSViewController {
|
||||
usesMyMemory = newUsesMyMemory
|
||||
providerButton.title = usesMyMemory ? "MyMemory (no key)" : "OpenAI · gpt-6-luna"
|
||||
defaults.set(openAI ? "openai" : "mymemory", forKey: "translationProvider")
|
||||
if let text = textToTranslate {
|
||||
textToTranslate = nil
|
||||
replaceInput?(text)
|
||||
}
|
||||
inputChanged()
|
||||
inputPresentationChanged?()
|
||||
}
|
||||
|
||||
private func cancelRequest() {
|
||||
@@ -272,11 +319,22 @@ final class TranslationController: NSViewController {
|
||||
|
||||
@objc func performPrimaryAction() {
|
||||
guard task == nil else { return }
|
||||
if (input?() ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if textToTranslate == nil && (input?() ?? "").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
swapLanguages()
|
||||
return
|
||||
}
|
||||
guard !output.string.isEmpty else { translate(); return }
|
||||
if output.string.isEmpty {
|
||||
if !usesMyMemory && textToTranslate == nil {
|
||||
textToTranslate = input?() ?? ""
|
||||
replaceInput?("")
|
||||
inputChanged()
|
||||
inputPresentationChanged?()
|
||||
focusSearch?()
|
||||
} else {
|
||||
translate()
|
||||
}
|
||||
return
|
||||
}
|
||||
pasteboard.clearContents()
|
||||
if pasteboard.setString(output.string, forType: .string) {
|
||||
dismiss?()
|
||||
@@ -287,7 +345,8 @@ final class TranslationController: NSViewController {
|
||||
|
||||
@objc func translate() {
|
||||
guard task == nil else { return }
|
||||
let text = input?() ?? ""
|
||||
let text = textToTranslate ?? input?() ?? ""
|
||||
let context = textToTranslate == nil ? "" : input?() ?? ""
|
||||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
setStatus("Enter \(languageName(sourceLanguage)) text in the search bar.", isError: true)
|
||||
return
|
||||
@@ -319,7 +378,7 @@ final class TranslationController: NSViewController {
|
||||
do {
|
||||
let translation = try await myMemory
|
||||
? myMemoryTranslate(text, source, target)
|
||||
: openAITranslate(text, source, target, key)
|
||||
: openAITranslate(text, source, target, key, context)
|
||||
guard !Task.isCancelled, let self else { return }
|
||||
self.output.string = translation
|
||||
self.setStatus("")
|
||||
|
||||
Reference in New Issue
Block a user