From fbc912904a25a69041c9791cef860a16d75f4f61 Mon Sep 17 00:00:00 2001 From: Aled Samuel Date: Mon, 24 Apr 2023 17:14:55 +0100 Subject: [PATCH 1/2] Add Create Image example to Demo app --- Demo/App/APIProvidedView.swift | 7 ++ Demo/App/ContentView.swift | 9 +- Demo/DemoChat/Sources/ImageStore.swift | 33 +++++++ .../Sources/UI/Images/ImageCreationView.swift | 93 +++++++++++++++++++ .../Sources/UI/Images/ImageView.swift | 63 +++++++++++++ .../OpenAI/Public/Models/ImagesResult.swift | 2 + 6 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 Demo/DemoChat/Sources/ImageStore.swift create mode 100644 Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift create mode 100644 Demo/DemoChat/Sources/UI/Images/ImageView.swift diff --git a/Demo/App/APIProvidedView.swift b/Demo/App/APIProvidedView.swift index 25e7ad87..b726e3a1 100644 --- a/Demo/App/APIProvidedView.swift +++ b/Demo/App/APIProvidedView.swift @@ -12,6 +12,7 @@ import SwiftUI struct APIProvidedView: View { @Binding var apiKey: String @StateObject var chatStore: ChatStore + @StateObject var imageStore: ImageStore @StateObject var miscStore: MiscStore @State var isShowingAPIConfigModal: Bool = true @@ -29,6 +30,11 @@ struct APIProvidedView: View { idProvider: idProvider ) ) + self._imageStore = StateObject( + wrappedValue: ImageStore( + openAIClient: OpenAI(apiToken: apiKey.wrappedValue) + ) + ) self._miscStore = StateObject( wrappedValue: MiscStore( openAIClient: OpenAI(apiToken: apiKey.wrappedValue) @@ -39,6 +45,7 @@ struct APIProvidedView: View { var body: some View { ContentView( chatStore: chatStore, + imageStore: imageStore, miscStore: miscStore ) .onChange(of: apiKey) { newApiKey in diff --git a/Demo/App/ContentView.swift b/Demo/App/ContentView.swift index 1e2283cf..2826e6bc 100644 --- a/Demo/App/ContentView.swift +++ b/Demo/App/ContentView.swift @@ -11,6 +11,7 @@ import SwiftUI struct ContentView: View { @ObservedObject var chatStore: ChatStore + @ObservedObject var imageStore: ImageStore @ObservedObject var miscStore: MiscStore @State private var selectedTab = 0 @Environment(\.idProviderValue) var idProvider @@ -33,6 +34,7 @@ struct ContentView: View { .tag(1) ImageView( + store: imageStore ) .tabItem { Label("Image", systemImage: "photo") @@ -63,10 +65,3 @@ struct TranscribeView: View { .font(.largeTitle) } } - -struct ImageView: View { - var body: some View { - Text("Image: TBD") - .font(.largeTitle) - } -} diff --git a/Demo/DemoChat/Sources/ImageStore.swift b/Demo/DemoChat/Sources/ImageStore.swift new file mode 100644 index 00000000..48ca2967 --- /dev/null +++ b/Demo/DemoChat/Sources/ImageStore.swift @@ -0,0 +1,33 @@ +// +// ImageStore.swift +// DemoChat +// +// Created by Aled Samuel on 24/04/2023. +// + +import Foundation +import OpenAI + +public final class ImageStore: ObservableObject { + public var openAIClient: OpenAIProtocol + + @Published var images: [ImagesResult.URLResult] = [] + + public init( + openAIClient: OpenAIProtocol + ) { + self.openAIClient = openAIClient + } + + @MainActor + func images(query: ImagesQuery) async { + images.removeAll() + do { + let response = try await openAIClient.images(query: query) + images = response.data + } catch { + // TODO: Better error handling + print(error.localizedDescription) + } + } +} diff --git a/Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift b/Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift new file mode 100644 index 00000000..2f6ee16e --- /dev/null +++ b/Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift @@ -0,0 +1,93 @@ +// +// ImageCreationView.swift +// DemoChat +// +// Created by Aled Samuel on 24/04/2023. +// + +import SwiftUI +import OpenAI +import SafariServices + +public struct ImageCreationView: View { + @ObservedObject var store: ImageStore + + @State private var prompt: String = "" + @State private var n: Int = 1 + @State private var size: String + @State private var showSafari = false + + private var sizes = ["256x256", "512x512", "1024x1024"] + + public init(store: ImageStore) { + self.store = store + size = sizes[0] + } + + public var body: some View { + List { + Section { + HStack { + Text("Prompt") + Spacer() + TextEditor(text: $prompt) + .multilineTextAlignment(.trailing) + } + HStack { + Stepper("Amount: \(n)", value: $n, in: 1...10) + } + HStack { + Picker("Size", selection: $size) { + ForEach(sizes, id: \.self) { + Text($0) + } + } + } + } + Section { + HStack { + Button("Create Image" + (n == 1 ? "" : "s")) { + Task { + let query = ImagesQuery(prompt: prompt, n: n, size: size) + await store.images(query: query) + } + } + .foregroundColor(.accentColor) + Spacer() + } + } + if !$store.images.isEmpty { + Section("Images") { + ForEach($store.images, id: \.self) { image in + let urlString = image.wrappedValue.url + if let imageURL = URL(string: urlString), UIApplication.shared.canOpenURL(imageURL) { + Button { + showSafari.toggle() + } label: { + Text(urlString) + .foregroundStyle(.foreground) + }.fullScreenCover(isPresented: $showSafari, content: { + SafariView(url: imageURL) + }) + } else { + Text(urlString) + .foregroundStyle(.secondary) + } + } + } + } + } + .listStyle(.insetGrouped) + .navigationTitle("Create Image") + } +} + +private struct SafariView: UIViewControllerRepresentable { + var url: URL + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { + SFSafariViewController(url: url) + } + + func updateUIViewController(_ safariViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { } +} diff --git a/Demo/DemoChat/Sources/UI/Images/ImageView.swift b/Demo/DemoChat/Sources/UI/Images/ImageView.swift new file mode 100644 index 00000000..a509c5f3 --- /dev/null +++ b/Demo/DemoChat/Sources/UI/Images/ImageView.swift @@ -0,0 +1,63 @@ +// +// ImageView.swift +// DemoChat +// +// Created by Aled Samuel on 24/04/2023. +// + +import SwiftUI + +public struct ImageView: View { + @ObservedObject var store: ImageStore + + public init(store: ImageStore) { + self.store = store + } + + public var body: some View { + NavigationStack { + List { + NavigationLink("Create Image", destination: ImageCreationView(store: store)) + NavigationLink("Create Image Edit", destination: ImageEditView(store: store)) + .disabled(true) + NavigationLink("Create Image Variation", destination: ImageVariationView(store: store)) + .disabled(true) + + } + .listStyle(.insetGrouped) + .navigationTitle("Image") + } + } +} + +public struct ImageEditView: View { + @ObservedObject var store: ImageStore + + public init(store: ImageStore) { + self.store = store + } + + public var body: some View { + List { + + } + .listStyle(.insetGrouped) + .navigationTitle("Create Image Edit") + } +} + +public struct ImageVariationView: View { + @ObservedObject var store: ImageStore + + public init(store: ImageStore) { + self.store = store + } + + public var body: some View { + List { + + } + .listStyle(.insetGrouped) + .navigationTitle("Create Image Variation") + } +} diff --git a/Sources/OpenAI/Public/Models/ImagesResult.swift b/Sources/OpenAI/Public/Models/ImagesResult.swift index 524f5ecc..f1990750 100644 --- a/Sources/OpenAI/Public/Models/ImagesResult.swift +++ b/Sources/OpenAI/Public/Models/ImagesResult.swift @@ -15,3 +15,5 @@ public struct ImagesResult: Codable, Equatable { public let created: TimeInterval public let data: [URLResult] } + +extension ImagesResult.URLResult: Hashable { } From 98980589f72f834760345f62829eddd55ebf2d11 Mon Sep 17 00:00:00 2001 From: Aled Samuel Date: Tue, 25 Apr 2023 12:50:53 +0100 Subject: [PATCH 2/2] ImageCreationView now uses LinkPresentation --- .../Sources/UI/Images/ImageCreationView.swift | 22 ++----------- .../Sources/UI/Images/LinkPreview.swift | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 20 deletions(-) create mode 100644 Demo/DemoChat/Sources/UI/Images/LinkPreview.swift diff --git a/Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift b/Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift index 2f6ee16e..ef6628ff 100644 --- a/Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift +++ b/Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift @@ -7,7 +7,6 @@ import SwiftUI import OpenAI -import SafariServices public struct ImageCreationView: View { @ObservedObject var store: ImageStore @@ -15,7 +14,6 @@ public struct ImageCreationView: View { @State private var prompt: String = "" @State private var n: Int = 1 @State private var size: String - @State private var showSafari = false private var sizes = ["256x256", "512x512", "1024x1024"] @@ -61,14 +59,8 @@ public struct ImageCreationView: View { ForEach($store.images, id: \.self) { image in let urlString = image.wrappedValue.url if let imageURL = URL(string: urlString), UIApplication.shared.canOpenURL(imageURL) { - Button { - showSafari.toggle() - } label: { - Text(urlString) - .foregroundStyle(.foreground) - }.fullScreenCover(isPresented: $showSafari, content: { - SafariView(url: imageURL) - }) + LinkPreview(previewURL: imageURL) + .aspectRatio(contentMode: .fit) } else { Text(urlString) .foregroundStyle(.secondary) @@ -81,13 +73,3 @@ public struct ImageCreationView: View { .navigationTitle("Create Image") } } - -private struct SafariView: UIViewControllerRepresentable { - var url: URL - - func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { - SFSafariViewController(url: url) - } - - func updateUIViewController(_ safariViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { } -} diff --git a/Demo/DemoChat/Sources/UI/Images/LinkPreview.swift b/Demo/DemoChat/Sources/UI/Images/LinkPreview.swift new file mode 100644 index 00000000..f4feceba --- /dev/null +++ b/Demo/DemoChat/Sources/UI/Images/LinkPreview.swift @@ -0,0 +1,33 @@ +// +// LinkPreview.swift +// DemoChat +// +// Created by Aled Samuel on 25/04/2023. +// + +import SwiftUI +import LinkPresentation + +struct LinkPreview: UIViewRepresentable { + typealias UIViewType = LPLinkView + + var previewURL: URL + + func makeUIView(context: Context) -> LPLinkView { + LPLinkView(url: previewURL) + } + + func updateUIView(_ uiView: UIViewType, context: Context) { + LPMetadataProvider().startFetchingMetadata(for: previewURL) { metadata, error in + if let error = error { + print(error.localizedDescription) + return + } + guard let metadata = metadata else { + print("Metadata missing for \(previewURL.absoluteString)") + return + } + uiView.metadata = metadata + } + } +}