diff --git a/Demo/App/APIProvidedView.swift b/Demo/App/APIProvidedView.swift index e13c3f8a..7dc61b5d 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..ef6628ff --- /dev/null +++ b/Demo/DemoChat/Sources/UI/Images/ImageCreationView.swift @@ -0,0 +1,75 @@ +// +// ImageCreationView.swift +// DemoChat +// +// Created by Aled Samuel on 24/04/2023. +// + +import SwiftUI +import OpenAI + +public struct ImageCreationView: View { + @ObservedObject var store: ImageStore + + @State private var prompt: String = "" + @State private var n: Int = 1 + @State private var size: String + + 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) { + LinkPreview(previewURL: imageURL) + .aspectRatio(contentMode: .fit) + } else { + Text(urlString) + .foregroundStyle(.secondary) + } + } + } + } + } + .listStyle(.insetGrouped) + .navigationTitle("Create Image") + } +} 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/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 + } + } +} 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 { }