Files
Termind/Frameworks/TermindPresentation/ModelProviderVertexFields.swift

165 lines
6.5 KiB
Swift

// SPDX-FileCopyrightText: 2026 Kaede Akino
// SPDX-License-Identifier: GPL-3.0-only
import SwiftUI
import UniformTypeIdentifiers
/// Platform-independent fields; hosts own file access, secret transactions, and request lifetimes.
public struct ModelProviderVertexFields: View {
@Binding private var isEnabled: Bool
@Binding private var projectID: String
@Binding private var location: String
private let clientEmail: String
private let hasCredential: Bool
private let requestURL: String?
private let readFile: (URL) async throws -> String
private let importJSON: (String) throws -> Void
private let remove: () throws -> Void
@State private var importingFile = false
@State private var pasting = false
@State private var json = ""
@State private var error: String?
@State private var importTask: Task<Void, Never>?
public init(
isEnabled: Binding<Bool>, projectID: Binding<String>, location: Binding<String>,
clientEmail: String, hasCredential: Bool, requestURL: String?,
readFile: @escaping (URL) async throws -> String,
importJSON: @escaping (String) throws -> Void, remove: @escaping () throws -> Void
) {
_isEnabled = isEnabled
_projectID = projectID
_location = location
self.clientEmail = clientEmail
self.hasCredential = hasCredential
self.requestURL = requestURL
self.readFile = readFile
self.importJSON = importJSON
self.remove = remove
}
public var body: some View {
Section("Google Vertex AI") {
Toggle(AppStrings.Settings.vertexAi.swiftUIKey, isOn: $isEnabled)
if isEnabled {
LabeledContent("Project ID") {
TextField("Project ID", text: $projectID.singleLineText, axis: .horizontal)
.singleLinePaste().labelsHidden().multilineTextAlignment(.trailing)
.autocorrectionDisabled().lineLimit(1)
}
LabeledContent("Location") {
TextField(
"Location", text: $location.singleLineText, prompt: Text(verbatim: "global"), axis: .horizontal
)
.singleLinePaste().labelsHidden().multilineTextAlignment(.trailing)
.autocorrectionDisabled().lineLimit(1)
}
LabeledContent("Service Account") {
if hasCredential {
Text(clientEmail).textSelection(.enabled)
} else {
Text(AppStrings.Vault.required)
}
}
ViewThatFits(in: .horizontal) {
HStack { credentialActions }
VStack(alignment: .leading, spacing: 12) { credentialActions }
}
.buttonStyle(.bordered)
if let requestURL {
LabeledContent("Full URL") {
Text(requestURL).font(.caption.monospaced()).textSelection(.enabled)
.multilineTextAlignment(.trailing).fixedSize(horizontal: false, vertical: true)
}
} else {
Text("Enter a valid project and location to configure Vertex AI.")
.font(.caption).foregroundStyle(.secondary)
}
Text("Vertex AI requires a service account. Add model IDs manually; model discovery is unavailable.")
.font(.caption).foregroundStyle(.secondary)
if let error { Text(error).foregroundStyle(.red) }
}
}
.fileImporter(isPresented: $importingFile, allowedContentTypes: [.json], allowsMultipleSelection: false) {
result in
switch result {
case .success(let urls):
guard let url = urls.first else { return }
importTask?.cancel()
importTask = Task {
do {
let value = try await readFile(url)
try Task.checkCancellation()
try importJSON(value)
} catch {
if !Task.isCancelled { self.error = error.localizedDescription }
}
}
case .failure(let error): self.error = error.localizedDescription
}
}
.sheet(isPresented: $pasting, onDismiss: { json = "" }) {
VStack(alignment: .leading, spacing: 12) {
Text("Paste Service Account JSON").font(.headline)
TextEditor(text: $json).autohidingScrollers().font(.body.monospaced()).autocorrectionDisabled()
#if os(iOS)
.textInputAutocapitalization(.never)
#endif
.frame(minHeight: 220)
if let error { Text(error).foregroundStyle(.red) }
HStack {
Button(AppStrings.Common.cancel.swiftUIKey) {
json = ""
pasting = false
}
Spacer()
Button(AppStrings.Common.import.swiftUIKey) {
perform {
try importJSON(json)
json = ""
pasting = false
}
}.disabled(json.isEmpty)
}
}
.padding().frame(minWidth: 300, idealWidth: 520)
}
.onDisappear {
importTask?.cancel()
importTask = nil
json = ""
}
.onChange(of: isEnabled) { _, _ in
importTask?.cancel()
importTask = nil
json = ""
pasting = false
}
}
@ViewBuilder private var credentialActions: some View {
Button(AppStrings.Common.import.swiftUIKey) {
importTask?.cancel()
error = nil
importingFile = true
}
Button(AppStrings.Common.paste.swiftUIKey) {
importTask?.cancel()
error = nil
json = ""
pasting = true
}
if hasCredential {
Button("Remove Service Account", role: .destructive) { perform(remove) }
}
}
private func perform(_ action: () throws -> Void) {
importTask?.cancel()
do {
try action()
error = nil
} catch { self.error = error.localizedDescription }
}
}