diff --git a/.github/workflows/firebaseai.yml b/.github/workflows/firebaseai.yml new file mode 100644 index 000000000..5b95aeb30 --- /dev/null +++ b/.github/workflows/firebaseai.yml @@ -0,0 +1,48 @@ +name: firebaseai + +on: + pull_request: + paths: + - 'firebaseai/**' + schedule: + # Run every day at 11pm (PST) - cron uses UTC times + - cron: '0 7 * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +env: + SAMPLE: FirebaseAI + +jobs: + spm: + name: spm (Xcode ${{ matrix.xcode }} - ${{ matrix.os }}) + runs-on: macOS-15 + strategy: + matrix: + xcode: ["16.3"] + os: [iOS] + include: + - os: iOS + device: iPhone 16 + env: + SETUP: firebaseai + SPM: true + DIR: firebaseai + OS: ${{ matrix.os }} + DEVICE: ${{ matrix.device }} + TEST: false + XCODE_VERSION: ${{ matrix.xcode }} + DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer + steps: + - name: Checkout + uses: actions/checkout@master + - name: Setup + run: | + gem install xcpretty + - name: Placeholder GoogleService-Info.plist good enough for build only testing. + run: cp ./mock-GoogleService-Info.plist ./firebaseai/GoogleService-Info.plist + - name: Build and Test SwiftUI (${{ matrix.os }}) + run: ./scripts/test.sh diff --git a/firebaseai/ChatSample/Assets.xcassets/AccentColor.colorset/Contents.json b/firebaseai/ChatSample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/firebaseai/ChatSample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/ChatSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebaseai/ChatSample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..13613e3ee --- /dev/null +++ b/firebaseai/ChatSample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/ChatSample/Assets.xcassets/Contents.json b/firebaseai/ChatSample/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/firebaseai/ChatSample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/ChatSample/Models/ChatMessage.swift b/firebaseai/ChatSample/Models/ChatMessage.swift new file mode 100644 index 000000000..6f7ab321b --- /dev/null +++ b/firebaseai/ChatSample/Models/ChatMessage.swift @@ -0,0 +1,64 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +enum Participant { + case system + case user +} + +struct ChatMessage: Identifiable, Equatable { + let id = UUID().uuidString + var message: String + let participant: Participant + var pending = false + + static func pending(participant: Participant) -> ChatMessage { + Self(message: "", participant: participant, pending: true) + } +} + +extension ChatMessage { + static var samples: [ChatMessage] = [ + .init(message: "Hello. What can I do for you today?", participant: .system), + .init(message: "Show me a simple loop in Swift.", participant: .user), + .init(message: """ + Sure, here is a simple loop in Swift: + + # Example 1 + ``` + for i in 1...5 { + print("Hello, world!") + } + ``` + + This loop will print the string "Hello, world!" five times. The for loop iterates over a range of numbers, + in this case the numbers from 1 to 5. The variable i is assigned each number in the range, and the code inside the loop is executed. + + **Here is another example of a simple loop in Swift:** + ```swift + var sum = 0 + for i in 1...100 { + sum += i + } + print("The sum of the numbers from 1 to 100 is \\(sum).") + ``` + + This loop calculates the sum of the numbers from 1 to 100. The variable sum is initialized to 0, and then the for loop iterates over the range of numbers from 1 to 100. The variable i is assigned each number in the range, and the value of i is added to the sum variable. After the loop has finished executing, the value of sum is printed to the console. + """, participant: .system), + ] + + static var sample = samples[0] +} diff --git a/firebaseai/ChatSample/Preview Content/Preview Assets.xcassets/Contents.json b/firebaseai/ChatSample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/firebaseai/ChatSample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/ChatSample/Screens/ConversationScreen.swift b/firebaseai/ChatSample/Screens/ConversationScreen.swift new file mode 100644 index 000000000..8d7c3c1a8 --- /dev/null +++ b/firebaseai/ChatSample/Screens/ConversationScreen.swift @@ -0,0 +1,129 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAI +import GenerativeAIUIComponents +import SwiftUI + +struct ConversationScreen: View { + @EnvironmentObject + var viewModel: ConversationViewModel + + @State + private var userPrompt = "" + + enum FocusedField: Hashable { + case message + } + + @FocusState + var focusedField: FocusedField? + + var body: some View { + VStack { + ScrollViewReader { scrollViewProxy in + List { + ForEach(viewModel.messages) { message in + MessageView(message: message) + } + if let error = viewModel.error { + ErrorView(error: error) + .tag("errorView") + } + } + .listStyle(.plain) + .onChange(of: viewModel.messages, perform: { newValue in + if viewModel.hasError { + // wait for a short moment to make sure we can actually scroll to the bottom + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation { + scrollViewProxy.scrollTo("errorView", anchor: .bottom) + } + focusedField = .message + } + } else { + guard let lastMessage = viewModel.messages.last else { return } + + // wait for a short moment to make sure we can actually scroll to the bottom + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation { + scrollViewProxy.scrollTo(lastMessage.id, anchor: .bottom) + } + focusedField = .message + } + } + }) + } + InputField("Message...", text: $userPrompt) { + Image(systemName: viewModel.busy ? "stop.circle.fill" : "arrow.up.circle.fill") + .font(.title) + } + .focused($focusedField, equals: .message) + .onSubmit { sendOrStop() } + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(action: newChat) { + Image(systemName: "square.and.pencil") + } + } + } + .navigationTitle("Chat sample") + .onAppear { + focusedField = .message + } + } + + private func sendMessage() { + Task { + let prompt = userPrompt + userPrompt = "" + await viewModel.sendMessage(prompt, streaming: true) + } + } + + private func sendOrStop() { + focusedField = nil + + if viewModel.busy { + viewModel.stop() + } else { + sendMessage() + } + } + + private func newChat() { + viewModel.startNewChat() + } +} + +struct ConversationScreen_Previews: PreviewProvider { + struct ContainerView: View { + @StateObject var viewModel = ConversationViewModel() + + var body: some View { + ConversationScreen() + .environmentObject(viewModel) + .onAppear { + viewModel.messages = ChatMessage.samples + } + } + } + + static var previews: some View { + NavigationStack { + ConversationScreen() + } + } +} diff --git a/firebaseai/ChatSample/ViewModels/ConversationViewModel.swift b/firebaseai/ChatSample/ViewModels/ConversationViewModel.swift new file mode 100644 index 000000000..1f77b8ff9 --- /dev/null +++ b/firebaseai/ChatSample/ViewModels/ConversationViewModel.swift @@ -0,0 +1,132 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAI +import Foundation +import UIKit + +@MainActor +class ConversationViewModel: ObservableObject { + /// This array holds both the user's and the system's chat messages + @Published var messages = [ChatMessage]() + + /// Indicates we're waiting for the model to finish + @Published var busy = false + + @Published var error: Error? + var hasError: Bool { + return error != nil + } + + private var model: GenerativeModel + private var chat: Chat + private var stopGenerating = false + + private var chatTask: Task? + + init() { + // model = FirebaseAI.firebaseAI(backend: .vertexAI()).generativeModel(modelName: "gemini-2.0-flash-001") + model = FirebaseAI.firebaseAI(backend: .googleAI()) + .generativeModel(modelName: "gemini-2.0-flash-001") + chat = model.startChat() + } + + func sendMessage(_ text: String, streaming: Bool = true) async { + error = nil + if streaming { + await internalSendMessageStreaming(text) + } else { + await internalSendMessage(text) + } + } + + func startNewChat() { + stop() + error = nil + chat = model.startChat() + messages.removeAll() + } + + func stop() { + chatTask?.cancel() + error = nil + } + + private func internalSendMessageStreaming(_ text: String) async { + chatTask?.cancel() + + chatTask = Task { + busy = true + defer { + busy = false + } + + // first, add the user's message to the chat + let userMessage = ChatMessage(message: text, participant: .user) + messages.append(userMessage) + + // add a pending message while we're waiting for a response from the backend + let systemMessage = ChatMessage.pending(participant: .system) + messages.append(systemMessage) + + do { + let responseStream = try chat.sendMessageStream(text) + for try await chunk in responseStream { + messages[messages.count - 1].pending = false + if let text = chunk.text { + messages[messages.count - 1].message += text + } + } + } catch { + self.error = error + print(error.localizedDescription) + messages.removeLast() + } + } + } + + private func internalSendMessage(_ text: String) async { + chatTask?.cancel() + + chatTask = Task { + busy = true + defer { + busy = false + } + + // first, add the user's message to the chat + let userMessage = ChatMessage(message: text, participant: .user) + messages.append(userMessage) + + // add a pending message while we're waiting for a response from the backend + let systemMessage = ChatMessage.pending(participant: .system) + messages.append(systemMessage) + + do { + var response: GenerateContentResponse? + response = try await chat.sendMessage(text) + + if let responseText = response?.text { + // replace pending message with backend response + messages[messages.count - 1].message = responseText + messages[messages.count - 1].pending = false + } + } catch { + self.error = error + print(error.localizedDescription) + messages.removeLast() + } + } + } +} diff --git a/firebaseai/ChatSample/Views/BouncingDots.swift b/firebaseai/ChatSample/Views/BouncingDots.swift new file mode 100644 index 000000000..6895e6723 --- /dev/null +++ b/firebaseai/ChatSample/Views/BouncingDots.swift @@ -0,0 +1,77 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI + +struct BouncingDots: View { + @State + private var dot1YOffset: CGFloat = 0.0 + + @State + private var dot2YOffset: CGFloat = 0.0 + + @State + private var dot3YOffset: CGFloat = 0.0 + + let animation = Animation.easeInOut(duration: 0.8) + .repeatForever(autoreverses: true) + + var body: some View { + HStack(spacing: 8) { + Circle() + .fill(Color.white) + .frame(width: 10, height: 10) + .offset(y: dot1YOffset) + .onAppear { + withAnimation(self.animation.delay(0.0)) { + self.dot1YOffset = -5 + } + } + Circle() + .fill(Color.white) + .frame(width: 10, height: 10) + .offset(y: dot2YOffset) + .onAppear { + withAnimation(self.animation.delay(0.2)) { + self.dot2YOffset = -5 + } + } + Circle() + .fill(Color.white) + .frame(width: 10, height: 10) + .offset(y: dot3YOffset) + .onAppear { + withAnimation(self.animation.delay(0.4)) { + self.dot3YOffset = -5 + } + } + } + .onAppear { + let baseOffset: CGFloat = -2 + + self.dot1YOffset = baseOffset + self.dot2YOffset = baseOffset + self.dot3YOffset = baseOffset + } + } +} + +struct BouncingDots_Previews: PreviewProvider { + static var previews: some View { + BouncingDots() + .frame(width: 200, height: 50) + .background(.blue) + .roundedCorner(10, corners: [.allCorners]) + } +} diff --git a/firebaseai/ChatSample/Views/ErrorDetailsView.swift b/firebaseai/ChatSample/Views/ErrorDetailsView.swift new file mode 100644 index 000000000..e333f2896 --- /dev/null +++ b/firebaseai/ChatSample/Views/ErrorDetailsView.swift @@ -0,0 +1,260 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAI +import MarkdownUI +import SwiftUI + +private extension HarmCategory { + /// Returns a description of the `HarmCategory` suitable for displaying in the UI. + var displayValue: String { + switch self { + case .dangerousContent: "Dangerous content" + case .harassment: "Harassment" + case .hateSpeech: "Hate speech" + case .sexuallyExplicit: "Sexually explicit" + case .civicIntegrity: "Civic integrity" + default: "Unknown HarmCategory: \(rawValue)" + } + } +} + +private extension SafetyRating.HarmProbability { + /// Returns a description of the `HarmProbability` suitable for displaying in the UI. + var displayValue: String { + switch self { + case .high: "High" + case .low: "Low" + case .medium: "Medium" + case .negligible: "Negligible" + default: "Unknown HarmProbability: \(rawValue)" + } + } +} + +private struct SubtitleFormRow: View { + var title: String + var value: String + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.subheadline) + Text(value) + } + } +} + +private struct SubtitleMarkdownFormRow: View { + var title: String + var value: String + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.subheadline) + Markdown(value) + } + } +} + +private struct SafetyRatingsSection: View { + var ratings: [SafetyRating] + + var body: some View { + Section("Safety ratings") { + List(ratings, id: \.self) { rating in + HStack { + Text(rating.category.displayValue).font(.subheadline) + Spacer() + Text(rating.probability.displayValue) + } + } + } + } +} + +struct ErrorDetailsView: View { + var error: Error + + var body: some View { + NavigationView { + Form { + switch error { + case let GenerateContentError.internalError(underlying: underlyingError): + Section("Error Type") { + Text("Internal error") + } + + Section("Details") { + SubtitleFormRow(title: "Error description", + value: underlyingError.localizedDescription) + } + + case let GenerateContentError.promptBlocked(response: generateContentResponse): + Section("Error Type") { + Text("Your prompt was blocked") + } + + Section("Details") { + if let reason = generateContentResponse.promptFeedback?.blockReason { + SubtitleFormRow(title: "Reason for blocking", value: reason.rawValue) + } + + if let text = generateContentResponse.text { + SubtitleMarkdownFormRow(title: "Last chunk for the response", value: text) + } + } + + if let ratings = generateContentResponse.candidates.first?.safetyRatings { + SafetyRatingsSection(ratings: ratings) + } + + case let GenerateContentError.responseStoppedEarly( + reason: finishReason, + response: generateContentResponse + ): + + Section("Error Type") { + Text("Response stopped early") + } + + Section("Details") { + SubtitleFormRow(title: "Reason for finishing early", value: finishReason.rawValue) + + if let text = generateContentResponse.text { + SubtitleMarkdownFormRow(title: "Last chunk for the response", value: text) + } + } + + if let ratings = generateContentResponse.candidates.first?.safetyRatings { + SafetyRatingsSection(ratings: ratings) + } + + default: + Section("Error Type") { + Text("Some other error") + } + + Section("Details") { + SubtitleFormRow(title: "Error description", value: error.localizedDescription) + } + } + } + .navigationTitle("Error details") + .navigationBarTitleDisplayMode(.inline) + } + } +} + +#Preview("Response Stopped Early") { + let error = GenerateContentError.responseStoppedEarly( + reason: .maxTokens, + response: GenerateContentResponse(candidates: [ + Candidate(content: ModelContent(role: "model", parts: + """ + A _hypothetical_ model response. + Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. + """), + safetyRatings: [ + SafetyRating( + category: .dangerousContent, + probability: .medium, + probabilityScore: 0.8, + severity: .medium, + severityScore: 0.9, + blocked: false + ), + SafetyRating( + category: .harassment, + probability: .low, + probabilityScore: 0.5, + severity: .low, + severityScore: 0.6, + blocked: false + ), + SafetyRating( + category: .hateSpeech, + probability: .low, + probabilityScore: 0.3, + severity: .medium, + severityScore: 0.2, + blocked: false + ), + SafetyRating( + category: .sexuallyExplicit, + probability: .low, + probabilityScore: 0.2, + severity: .negligible, + severityScore: 0.5, + blocked: false + ), + ], + finishReason: FinishReason.maxTokens, + citationMetadata: nil), + ]) + ) + + return ErrorDetailsView(error: error) +} + +#Preview("Prompt Blocked") { + let error = GenerateContentError.promptBlocked( + response: GenerateContentResponse(candidates: [ + Candidate(content: ModelContent(role: "model", parts: + """ + A _hypothetical_ model response. + Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. + """), + safetyRatings: [ + SafetyRating( + category: .dangerousContent, + probability: .low, + probabilityScore: 0.8, + severity: .medium, + severityScore: 0.9, + blocked: false + ), + SafetyRating( + category: .harassment, + probability: .low, + probabilityScore: 0.5, + severity: .low, + severityScore: 0.6, + blocked: false + ), + SafetyRating( + category: .hateSpeech, + probability: .low, + probabilityScore: 0.3, + severity: .medium, + severityScore: 0.2, + blocked: false + ), + SafetyRating( + category: .sexuallyExplicit, + probability: .low, + probabilityScore: 0.2, + severity: .negligible, + severityScore: 0.5, + blocked: false + ), + ], + finishReason: FinishReason.other, + citationMetadata: nil), + ]) + ) + + return ErrorDetailsView(error: error) +} diff --git a/firebaseai/ChatSample/Views/ErrorView.swift b/firebaseai/ChatSample/Views/ErrorView.swift new file mode 100644 index 000000000..09ed3f870 --- /dev/null +++ b/firebaseai/ChatSample/Views/ErrorView.swift @@ -0,0 +1,96 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAI +import SwiftUI + +struct ErrorView: View { + var error: Error + @State private var isDetailsSheetPresented = false + var body: some View { + HStack { + Text("An error occurred.") + Button(action: { isDetailsSheetPresented.toggle() }) { + Image(systemName: "info.circle") + } + } + .frame(maxWidth: .infinity, alignment: .center) + .listRowSeparator(.hidden) + .sheet(isPresented: $isDetailsSheetPresented) { + ErrorDetailsView(error: error) + } + } +} + +#Preview { + NavigationView { + let errorPromptBlocked = GenerateContentError.promptBlocked( + response: GenerateContentResponse( + candidates: [ + Candidate( + content: ModelContent(role: "model", parts: [ + """ + A _hypothetical_ model response. + Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. + """, + ]), + safetyRatings: [ + SafetyRating( + category: .dangerousContent, + probability: .high, + probabilityScore: 0.8, + severity: .medium, + severityScore: 0.9, + blocked: true + ), + SafetyRating( + category: .harassment, + probability: .low, + probabilityScore: 0.5, + severity: .low, + severityScore: 0.6, + blocked: false + ), + SafetyRating( + category: .hateSpeech, + probability: .low, + probabilityScore: 0.3, + severity: .medium, + severityScore: 0.2, + blocked: false + ), + SafetyRating( + category: .sexuallyExplicit, + probability: .low, + probabilityScore: 0.2, + severity: .negligible, + severityScore: 0.5, + blocked: false + ), + ], + finishReason: FinishReason.other, + citationMetadata: nil + ), + ] + ) + ) + List { + MessageView(message: ChatMessage.samples[0]) + MessageView(message: ChatMessage.samples[1]) + ErrorView(error: errorPromptBlocked) + } + .listStyle(.plain) + .navigationTitle("Chat sample") + } +} diff --git a/firebaseai/ChatSample/Views/MessageView.swift b/firebaseai/ChatSample/Views/MessageView.swift new file mode 100644 index 000000000..79894503f --- /dev/null +++ b/firebaseai/ChatSample/Views/MessageView.swift @@ -0,0 +1,108 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import MarkdownUI +import SwiftUI + +struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) + } +} + +extension View { + func roundedCorner(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape(RoundedCorner(radius: radius, corners: corners)) + } +} + +struct MessageContentView: View { + var message: ChatMessage + + var body: some View { + if message.pending { + BouncingDots() + } else { + Markdown(message.message) + .markdownTextStyle { + FontFamilyVariant(.normal) + FontSize(.em(0.85)) + ForegroundColor(message.participant == .system ? Color(UIColor.label) : .white) + } + .markdownBlockStyle(\.codeBlock) { configuration in + configuration.label + .relativeLineSpacing(.em(0.25)) + .markdownTextStyle { + FontFamilyVariant(.monospaced) + FontSize(.em(0.85)) + ForegroundColor(Color(.label)) + } + .padding() + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .markdownMargin(top: .zero, bottom: .em(0.8)) + } + } + } +} + +struct MessageView: View { + var message: ChatMessage + + var body: some View { + HStack { + if message.participant == .user { + Spacer() + } + MessageContentView(message: message) + .padding(10) + .background(message.participant == .system + ? Color(UIColor.systemFill) + : Color(UIColor.systemBlue)) + .roundedCorner(10, + corners: [ + .topLeft, + .topRight, + message.participant == .system ? .bottomRight : .bottomLeft, + ]) + if message.participant == .system { + Spacer() + } + } + .listRowSeparator(.hidden) + } +} + +struct MessageView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + List { + MessageView(message: ChatMessage.samples[0]) + MessageView(message: ChatMessage.samples[1]) + MessageView(message: ChatMessage.samples[2]) + MessageView(message: ChatMessage(message: "Hello!", participant: .system, pending: true)) + } + .listStyle(.plain) + .navigationTitle("Chat sample") + } + } +} diff --git a/firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj b/firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..1749d3a5a --- /dev/null +++ b/firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj @@ -0,0 +1,673 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 869200B32B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; }; + 86C1F4832BC726150026816F /* FunctionCallingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86C1F47E2BC726150026816F /* FunctionCallingScreen.swift */; }; + 86C1F4842BC726150026816F /* FunctionCallingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86C1F4802BC726150026816F /* FunctionCallingViewModel.swift */; }; + 86D9CA8B2BED3EE1007D939E /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 86D9CA8A2BED3EE1007D939E /* FirebaseAppCheck */; }; + 86D9CA8F2BED3EE1007D939E /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 86D9CA8E2BED3EE1007D939E /* FirebaseAuth */; }; + 86D9CAB52BED3EE1007D939E /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 86D9CAB42BED3EE1007D939E /* FirebaseStorage */; }; + 88263BF02B239C09008AB09B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88263BEE2B239BFE008AB09B /* ErrorView.swift */; }; + 88263BF12B239C11008AB09B /* ErrorDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889873842B208563005B4896 /* ErrorDetailsView.swift */; }; + 8848C8332B0D04BC007B434F /* FirebaseAISampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8848C8322B0D04BC007B434F /* FirebaseAISampleApp.swift */; }; + 8848C8352B0D04BC007B434F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8848C8342B0D04BC007B434F /* ContentView.swift */; }; + 8848C8372B0D04BD007B434F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8848C8362B0D04BD007B434F /* Assets.xcassets */; }; + 8848C83A2B0D04BD007B434F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8848C8392B0D04BD007B434F /* Preview Assets.xcassets */; }; + 886F95D52B17BA010036F07A /* SummarizeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88209C1B2B0FBDC300F64795 /* SummarizeScreen.swift */; }; + 886F95D62B17BA010036F07A /* SummarizeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88209C1D2B0FBDC300F64795 /* SummarizeViewModel.swift */; }; + 886F95D82B17BA420036F07A /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 886F95D72B17BA420036F07A /* MarkdownUI */; }; + 886F95DB2B17BAEF0036F07A /* PhotoReasoningViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8802666F2B0FC39000CF7CB6 /* PhotoReasoningViewModel.swift */; }; + 886F95DC2B17BAEF0036F07A /* PhotoReasoningScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880266752B0FC39000CF7CB6 /* PhotoReasoningScreen.swift */; }; + 886F95DD2B17D5010036F07A /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F5A2B11133E00C08E95 /* MessageView.swift */; }; + 886F95DE2B17D5010036F07A /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F582B11131900C08E95 /* ChatMessage.swift */; }; + 886F95DF2B17D5010036F07A /* BouncingDots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F5C2B11135000C08E95 /* BouncingDots.swift */; }; + 886F95E02B17D5010036F07A /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F562B1112F600C08E95 /* ConversationViewModel.swift */; }; + 886F95E12B17D5010036F07A /* ConversationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F542B1112CA00C08E95 /* ConversationScreen.swift */; }; + 886F95E32B17D6630036F07A /* GenerativeAIUIComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 886F95E22B17D6630036F07A /* GenerativeAIUIComponents */; }; + DE26D95F2DBB3E9F007E6668 /* FirebaseAI in Frameworks */ = {isa = PBXBuildFile; productRef = DE26D95E2DBB3E9F007E6668 /* FirebaseAI */; }; + DEFECAA92D7B4CCD00EF9621 /* ImagenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFECAA72D7B4CCD00EF9621 /* ImagenViewModel.swift */; }; + DEFECAAA2D7B4CCD00EF9621 /* ImagenScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFECAA62D7B4CCD00EF9621 /* ImagenScreen.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 869200B22B879C4F00482873 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 86C1F47E2BC726150026816F /* FunctionCallingScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionCallingScreen.swift; sourceTree = ""; }; + 86C1F4802BC726150026816F /* FunctionCallingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionCallingViewModel.swift; sourceTree = ""; }; + 8802666F2B0FC39000CF7CB6 /* PhotoReasoningViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoReasoningViewModel.swift; sourceTree = ""; }; + 880266752B0FC39000CF7CB6 /* PhotoReasoningScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoReasoningScreen.swift; sourceTree = ""; }; + 88209C1B2B0FBDC300F64795 /* SummarizeScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SummarizeScreen.swift; sourceTree = ""; }; + 88209C1D2B0FBDC300F64795 /* SummarizeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SummarizeViewModel.swift; sourceTree = ""; }; + 88263BEE2B239BFE008AB09B /* ErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + 8848C82F2B0D04BC007B434F /* FirebaseAISample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FirebaseAISample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8848C8322B0D04BC007B434F /* FirebaseAISampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseAISampleApp.swift; sourceTree = ""; }; + 8848C8342B0D04BC007B434F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 8848C8362B0D04BD007B434F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8848C8392B0D04BD007B434F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 8848C84A2B0D051F007B434F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8848C84D2B0D051F007B434F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 8848C85C2B0D056D007B434F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 8848C85F2B0D056D007B434F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 889873842B208563005B4896 /* ErrorDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsView.swift; sourceTree = ""; }; + 88B8A9352B0FCBA700424728 /* GenerativeAIUIComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = GenerativeAIUIComponents; sourceTree = ""; }; + 88E10F482B110D5400C08E95 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 88E10F4B2B110D5400C08E95 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 88E10F542B1112CA00C08E95 /* ConversationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationScreen.swift; sourceTree = ""; }; + 88E10F562B1112F600C08E95 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = ""; }; + 88E10F582B11131900C08E95 /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = ""; }; + 88E10F5A2B11133E00C08E95 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; + 88E10F5C2B11135000C08E95 /* BouncingDots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BouncingDots.swift; sourceTree = ""; }; + DEFECAA62D7B4CCD00EF9621 /* ImagenScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagenScreen.swift; sourceTree = ""; }; + DEFECAA72D7B4CCD00EF9621 /* ImagenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagenViewModel.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8848C82C2B0D04BC007B434F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE26D95F2DBB3E9F007E6668 /* FirebaseAI in Frameworks */, + 886F95D82B17BA420036F07A /* MarkdownUI in Frameworks */, + 86D9CAB52BED3EE1007D939E /* FirebaseStorage in Frameworks */, + 86D9CA8F2BED3EE1007D939E /* FirebaseAuth in Frameworks */, + 86D9CA8B2BED3EE1007D939E /* FirebaseAppCheck in Frameworks */, + 886F95E32B17D6630036F07A /* GenerativeAIUIComponents in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 86C1F47F2BC726150026816F /* Screens */ = { + isa = PBXGroup; + children = ( + 86C1F47E2BC726150026816F /* FunctionCallingScreen.swift */, + ); + path = Screens; + sourceTree = ""; + }; + 86C1F4812BC726150026816F /* ViewModels */ = { + isa = PBXGroup; + children = ( + 86C1F4802BC726150026816F /* FunctionCallingViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 86C1F4822BC726150026816F /* FunctionCallingSample */ = { + isa = PBXGroup; + children = ( + 86C1F4812BC726150026816F /* ViewModels */, + 86C1F47F2BC726150026816F /* Screens */, + ); + path = FunctionCallingSample; + sourceTree = ""; + }; + 8802666E2B0FC39000CF7CB6 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 8802666F2B0FC39000CF7CB6 /* PhotoReasoningViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 880266742B0FC39000CF7CB6 /* Screens */ = { + isa = PBXGroup; + children = ( + 880266752B0FC39000CF7CB6 /* PhotoReasoningScreen.swift */, + ); + path = Screens; + sourceTree = ""; + }; + 88209C1A2B0FBDC300F64795 /* Screens */ = { + isa = PBXGroup; + children = ( + 88209C1B2B0FBDC300F64795 /* SummarizeScreen.swift */, + ); + path = Screens; + sourceTree = ""; + }; + 88209C1C2B0FBDC300F64795 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 88209C1D2B0FBDC300F64795 /* SummarizeViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 88209C222B0FBE1700F64795 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 8848C8262B0D04BC007B434F = { + isa = PBXGroup; + children = ( + DEFECAA82D7B4CCD00EF9621 /* ImagenScreen */, + 88B8A9352B0FCBA700424728 /* GenerativeAIUIComponents */, + 869200B22B879C4F00482873 /* GoogleService-Info.plist */, + 8848C8312B0D04BC007B434F /* FirebaseAISample */, + 8848C8452B0D051E007B434F /* GenerativeAITextSample */, + 8848C8572B0D056C007B434F /* GenerativeAIMultimodalSample */, + 88E10F432B110D5300C08E95 /* ChatSample */, + 86C1F4822BC726150026816F /* FunctionCallingSample */, + 8848C8302B0D04BC007B434F /* Products */, + 88209C222B0FBE1700F64795 /* Frameworks */, + ); + sourceTree = ""; + }; + 8848C8302B0D04BC007B434F /* Products */ = { + isa = PBXGroup; + children = ( + 8848C82F2B0D04BC007B434F /* FirebaseAISample.app */, + ); + name = Products; + sourceTree = ""; + }; + 8848C8312B0D04BC007B434F /* FirebaseAISample */ = { + isa = PBXGroup; + children = ( + 8848C8322B0D04BC007B434F /* FirebaseAISampleApp.swift */, + 8848C8342B0D04BC007B434F /* ContentView.swift */, + 8848C8362B0D04BD007B434F /* Assets.xcassets */, + 8848C8382B0D04BD007B434F /* Preview Content */, + ); + path = FirebaseAISample; + sourceTree = ""; + }; + 8848C8382B0D04BD007B434F /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8848C8392B0D04BD007B434F /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 8848C8452B0D051E007B434F /* GenerativeAITextSample */ = { + isa = PBXGroup; + children = ( + 88209C1C2B0FBDC300F64795 /* ViewModels */, + 88209C1A2B0FBDC300F64795 /* Screens */, + 8848C84A2B0D051F007B434F /* Assets.xcassets */, + 8848C84C2B0D051F007B434F /* Preview Content */, + ); + path = GenerativeAITextSample; + sourceTree = ""; + }; + 8848C84C2B0D051F007B434F /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8848C84D2B0D051F007B434F /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 8848C8572B0D056C007B434F /* GenerativeAIMultimodalSample */ = { + isa = PBXGroup; + children = ( + 8802666E2B0FC39000CF7CB6 /* ViewModels */, + 880266742B0FC39000CF7CB6 /* Screens */, + 8848C85C2B0D056D007B434F /* Assets.xcassets */, + 8848C85E2B0D056D007B434F /* Preview Content */, + ); + path = GenerativeAIMultimodalSample; + sourceTree = ""; + }; + 8848C85E2B0D056D007B434F /* Preview Content */ = { + isa = PBXGroup; + children = ( + 8848C85F2B0D056D007B434F /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 88E10F432B110D5300C08E95 /* ChatSample */ = { + isa = PBXGroup; + children = ( + 88E10F522B11124A00C08E95 /* Models */, + 88E10F502B11123600C08E95 /* ViewModels */, + 88E10F512B11124100C08E95 /* Views */, + 88E10F532B1112B900C08E95 /* Screens */, + 88E10F482B110D5400C08E95 /* Assets.xcassets */, + 88E10F4A2B110D5400C08E95 /* Preview Content */, + ); + path = ChatSample; + sourceTree = ""; + }; + 88E10F4A2B110D5400C08E95 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 88E10F4B2B110D5400C08E95 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 88E10F502B11123600C08E95 /* ViewModels */ = { + isa = PBXGroup; + children = ( + 88E10F562B1112F600C08E95 /* ConversationViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + 88E10F512B11124100C08E95 /* Views */ = { + isa = PBXGroup; + children = ( + 88263BEE2B239BFE008AB09B /* ErrorView.swift */, + 88E10F5A2B11133E00C08E95 /* MessageView.swift */, + 88E10F5C2B11135000C08E95 /* BouncingDots.swift */, + 889873842B208563005B4896 /* ErrorDetailsView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 88E10F522B11124A00C08E95 /* Models */ = { + isa = PBXGroup; + children = ( + 88E10F582B11131900C08E95 /* ChatMessage.swift */, + ); + path = Models; + sourceTree = ""; + }; + 88E10F532B1112B900C08E95 /* Screens */ = { + isa = PBXGroup; + children = ( + 88E10F542B1112CA00C08E95 /* ConversationScreen.swift */, + ); + path = Screens; + sourceTree = ""; + }; + DEFECAA82D7B4CCD00EF9621 /* ImagenScreen */ = { + isa = PBXGroup; + children = ( + DEFECAA62D7B4CCD00EF9621 /* ImagenScreen.swift */, + DEFECAA72D7B4CCD00EF9621 /* ImagenViewModel.swift */, + ); + path = ImagenScreen; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8848C82E2B0D04BC007B434F /* FirebaseAISample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8848C83D2B0D04BD007B434F /* Build configuration list for PBXNativeTarget "FirebaseAISample" */; + buildPhases = ( + 8848C82B2B0D04BC007B434F /* Sources */, + 8848C82C2B0D04BC007B434F /* Frameworks */, + 8848C82D2B0D04BC007B434F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FirebaseAISample; + packageProductDependencies = ( + 886F95D72B17BA420036F07A /* MarkdownUI */, + 886F95E22B17D6630036F07A /* GenerativeAIUIComponents */, + 86D9CA8A2BED3EE1007D939E /* FirebaseAppCheck */, + 86D9CA8E2BED3EE1007D939E /* FirebaseAuth */, + 86D9CAB42BED3EE1007D939E /* FirebaseStorage */, + 86D9CAB82BED3EE1007D939E /* FirebaseVertexAI */, + DEFECAAC2D7BB49700EF9621 /* FirebaseVertexAI */, + DE26D95E2DBB3E9F007E6668 /* FirebaseAI */, + ); + productName = GenerativeAISample; + productReference = 8848C82F2B0D04BC007B434F /* FirebaseAISample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8848C8272B0D04BC007B434F /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1510; + TargetAttributes = { + 8848C82E2B0D04BC007B434F = { + CreatedOnToolsVersion = 15.1; + }; + }; + }; + buildConfigurationList = 8848C82A2B0D04BC007B434F /* Build configuration list for PBXProject "FirebaseAIExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8848C8262B0D04BC007B434F; + packageReferences = ( + 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, + DEA09AC32B1FCE22001962D9 /* XCRemoteSwiftPackageReference "NetworkImage" */, + DEFECAAB2D7BB49700EF9621 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + ); + productRefGroup = 8848C8302B0D04BC007B434F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8848C82E2B0D04BC007B434F /* FirebaseAISample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8848C82D2B0D04BC007B434F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8848C83A2B0D04BD007B434F /* Preview Assets.xcassets in Resources */, + 8848C8372B0D04BD007B434F /* Assets.xcassets in Resources */, + 869200B32B879C4F00482873 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8848C82B2B0D04BC007B434F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 86C1F4832BC726150026816F /* FunctionCallingScreen.swift in Sources */, + 886F95DF2B17D5010036F07A /* BouncingDots.swift in Sources */, + 86C1F4842BC726150026816F /* FunctionCallingViewModel.swift in Sources */, + 886F95DE2B17D5010036F07A /* ChatMessage.swift in Sources */, + 88263BF12B239C11008AB09B /* ErrorDetailsView.swift in Sources */, + 8848C8352B0D04BC007B434F /* ContentView.swift in Sources */, + 886F95D52B17BA010036F07A /* SummarizeScreen.swift in Sources */, + 8848C8332B0D04BC007B434F /* FirebaseAISampleApp.swift in Sources */, + 886F95E02B17D5010036F07A /* ConversationViewModel.swift in Sources */, + 886F95DD2B17D5010036F07A /* MessageView.swift in Sources */, + 886F95DC2B17BAEF0036F07A /* PhotoReasoningScreen.swift in Sources */, + DEFECAA92D7B4CCD00EF9621 /* ImagenViewModel.swift in Sources */, + DEFECAAA2D7B4CCD00EF9621 /* ImagenScreen.swift in Sources */, + 886F95DB2B17BAEF0036F07A /* PhotoReasoningViewModel.swift in Sources */, + 886F95E12B17D5010036F07A /* ConversationScreen.swift in Sources */, + 88263BF02B239C09008AB09B /* ErrorView.swift in Sources */, + 886F95D62B17BA010036F07A /* SummarizeViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8848C83B2B0D04BD007B434F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8848C83C2B0D04BD007B434F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8848C83E2B0D04BD007B434F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"FirebaseAISample/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.VertexAISample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8848C83F2B0D04BD007B434F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"FirebaseAISample/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.VertexAISample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8848C82A2B0D04BC007B434F /* Build configuration list for PBXProject "FirebaseAIExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8848C83B2B0D04BD007B434F /* Debug */, + 8848C83C2B0D04BD007B434F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8848C83D2B0D04BD007B434F /* Build configuration list for PBXNativeTarget "FirebaseAISample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8848C83E2B0D04BD007B434F /* Debug */, + 8848C83F2B0D04BD007B434F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui"; + requirement = { + kind = revision; + revision = 55441810c0f678c78ed7e2ebd46dde89228e02fc; + }; + }; + DEA09AC32B1FCE22001962D9 /* XCRemoteSwiftPackageReference "NetworkImage" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/NetworkImage"; + requirement = { + kind = revision; + revision = 7aff8d1b31148d32c5933d75557d42f6323ee3d1; + }; + }; + DEFECAAB2D7BB49700EF9621 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; + requirement = { + branch = "firebase-ai"; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 86D9CA8A2BED3EE1007D939E /* FirebaseAppCheck */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAppCheck; + }; + 86D9CA8E2BED3EE1007D939E /* FirebaseAuth */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAuth; + }; + 86D9CAB42BED3EE1007D939E /* FirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseStorage; + }; + 86D9CAB82BED3EE1007D939E /* FirebaseVertexAI */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseVertexAI; + }; + 886F95D72B17BA420036F07A /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; + productName = MarkdownUI; + }; + 886F95E22B17D6630036F07A /* GenerativeAIUIComponents */ = { + isa = XCSwiftPackageProductDependency; + productName = GenerativeAIUIComponents; + }; + DE26D95E2DBB3E9F007E6668 /* FirebaseAI */ = { + isa = XCSwiftPackageProductDependency; + package = DEFECAAB2D7BB49700EF9621 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAI; + }; + DEFECAAC2D7BB49700EF9621 /* FirebaseVertexAI */ = { + isa = XCSwiftPackageProductDependency; + package = DEFECAAB2D7BB49700EF9621 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseVertexAI; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 8848C8272B0D04BC007B434F /* Project object */; +} diff --git a/firebaseai/FirebaseAIExample.xcodeproj/xcshareddata/xcschemes/FirebaseAIExample (iOS).xcscheme b/firebaseai/FirebaseAIExample.xcodeproj/xcshareddata/xcschemes/FirebaseAIExample (iOS).xcscheme new file mode 100644 index 000000000..6a66b5471 --- /dev/null +++ b/firebaseai/FirebaseAIExample.xcodeproj/xcshareddata/xcschemes/FirebaseAIExample (iOS).xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/firebaseai/FirebaseAISample/Assets.xcassets/AccentColor.colorset/Contents.json b/firebaseai/FirebaseAISample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/firebaseai/FirebaseAISample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/FirebaseAISample/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebaseai/FirebaseAISample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..13613e3ee --- /dev/null +++ b/firebaseai/FirebaseAISample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/FirebaseAISample/Assets.xcassets/Contents.json b/firebaseai/FirebaseAISample/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/firebaseai/FirebaseAISample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/FirebaseAISample/ContentView.swift b/firebaseai/FirebaseAISample/ContentView.swift new file mode 100644 index 000000000..2c13af833 --- /dev/null +++ b/firebaseai/FirebaseAISample/ContentView.swift @@ -0,0 +1,61 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI + +struct ContentView: View { + @StateObject + var viewModel = ConversationViewModel() + + @StateObject + var functionCallingViewModel = FunctionCallingViewModel() + + var body: some View { + NavigationStack { + List { + NavigationLink { + SummarizeScreen() + } label: { + Label("Text", systemImage: "doc.text") + } + NavigationLink { + PhotoReasoningScreen() + } label: { + Label("Multi-modal", systemImage: "doc.richtext") + } + NavigationLink { + ConversationScreen() + .environmentObject(viewModel) + } label: { + Label("Chat", systemImage: "ellipsis.message.fill") + } + NavigationLink { + FunctionCallingScreen().environmentObject(functionCallingViewModel) + } label: { + Label("Function Calling", systemImage: "function") + } + NavigationLink { + ImagenScreen() + } label: { + Label("Imagen", systemImage: "camera.circle") + } + } + .navigationTitle("Generative AI Samples") + } + } +} + +#Preview { + ContentView() +} diff --git a/firebaseai/FirebaseAISample/FirebaseAISampleApp.swift b/firebaseai/FirebaseAISample/FirebaseAISampleApp.swift new file mode 100644 index 000000000..861a9d9ad --- /dev/null +++ b/firebaseai/FirebaseAISample/FirebaseAISampleApp.swift @@ -0,0 +1,67 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAppCheck +import FirebaseCore +import SwiftUI + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication + .LaunchOptionsKey: Any]? = nil) -> Bool { + // Recommendation: Protect your Vertex AI API resources from abuse by preventing unauthorized + // clients using App Check; see https://firebase.google.com/docs/app-check#get_started. + AppCheck.setAppCheckProviderFactory(AppCheckNotConfiguredFactory()) + + FirebaseApp.configure() + + if let firebaseApp = FirebaseApp.app(), firebaseApp.options.projectID == "mockproject-1234" { + guard let bundleID = Bundle.main.bundleIdentifier else { fatalError() } + fatalError(""" + You must create and/or download a valid `GoogleService-Info.plist` file for \(bundleID) from \ + https://console.firebase.google.com to run this sample. Replace the existing \ + `GoogleService-Info.plist` file in the `firebaseai` directory with this new file. + """) + } + + return true + } +} + +@main +struct FirebaseAISampleApp: App { + @UIApplicationDelegateAdaptor var appDelegate: AppDelegate + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} + +/// Placeholder App Check provider factory that returns a simple ``AppCheckNotConfigured`` error. +private class AppCheckNotConfiguredFactory: NSObject, AppCheckProviderFactory { + private class AppCheckNotConfiguredProvider: NSObject, AppCheckProvider { + func getToken() async throws -> AppCheckToken { + throw AppCheckNotConfigured() + } + } + + func createProvider(with app: FirebaseApp) -> (any AppCheckProvider)? { + return AppCheckNotConfiguredProvider() + } +} + +/// Error indicating that App Check is not configured in the sample app. +struct AppCheckNotConfigured: Error {} diff --git a/firebaseai/FirebaseAISample/Preview Content/Preview Assets.xcassets/Contents.json b/firebaseai/FirebaseAISample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/firebaseai/FirebaseAISample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/FunctionCallingSample/Screens/FunctionCallingScreen.swift b/firebaseai/FunctionCallingSample/Screens/FunctionCallingScreen.swift new file mode 100644 index 000000000..71de0c2cf --- /dev/null +++ b/firebaseai/FunctionCallingSample/Screens/FunctionCallingScreen.swift @@ -0,0 +1,131 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAI +import GenerativeAIUIComponents +import SwiftUI + +struct FunctionCallingScreen: View { + @EnvironmentObject + var viewModel: FunctionCallingViewModel + + @State + private var userPrompt = "What is 100 Euros in U.S. Dollars?" + + enum FocusedField: Hashable { + case message + } + + @FocusState + var focusedField: FocusedField? + + var body: some View { + VStack { + ScrollViewReader { scrollViewProxy in + List { + Text("Interact with a currency conversion API using function calling in Gemini.") + ForEach(viewModel.messages) { message in + MessageView(message: message) + } + if let error = viewModel.error { + ErrorView(error: error) + .tag("errorView") + } + } + .listStyle(.plain) + .onChange(of: viewModel.messages, perform: { newValue in + if viewModel.hasError { + // Wait for a short moment to make sure we can actually scroll to the bottom. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation { + scrollViewProxy.scrollTo("errorView", anchor: .bottom) + } + focusedField = .message + } + } else { + guard let lastMessage = viewModel.messages.last else { return } + + // Wait for a short moment to make sure we can actually scroll to the bottom. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation { + scrollViewProxy.scrollTo(lastMessage.id, anchor: .bottom) + } + focusedField = .message + } + } + }) + .onTapGesture { + focusedField = nil + } + } + InputField("Message...", text: $userPrompt) { + Image(systemName: viewModel.busy ? "stop.circle.fill" : "arrow.up.circle.fill") + .font(.title) + } + .focused($focusedField, equals: .message) + .onSubmit { sendOrStop() } + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(action: newChat) { + Image(systemName: "square.and.pencil") + } + } + } + .navigationTitle("Function Calling") + .onAppear { + focusedField = .message + } + } + + private func sendMessage() { + Task { + let prompt = userPrompt + userPrompt = "" + await viewModel.sendMessage(prompt, streaming: true) + } + } + + private func sendOrStop() { + if viewModel.busy { + viewModel.stop() + } else { + sendMessage() + } + } + + private func newChat() { + viewModel.startNewChat() + } +} + +struct FunctionCallingScreen_Previews: PreviewProvider { + struct ContainerView: View { + @EnvironmentObject + var viewModel: FunctionCallingViewModel + + var body: some View { + FunctionCallingScreen() + .onAppear { + viewModel.messages = ChatMessage.samples + } + } + } + + static var previews: some View { + NavigationStack { + FunctionCallingScreen().environmentObject(FunctionCallingViewModel()) + } + } +} diff --git a/firebaseai/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift b/firebaseai/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift new file mode 100644 index 000000000..2c04c6a6d --- /dev/null +++ b/firebaseai/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift @@ -0,0 +1,256 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAI +import Foundation +import UIKit + +@MainActor +class FunctionCallingViewModel: ObservableObject { + /// This array holds both the user's and the system's chat messages + @Published var messages = [ChatMessage]() + + /// Indicates we're waiting for the model to finish + @Published var busy = false + + @Published var error: Error? + var hasError: Bool { + return error != nil + } + + /// Function calls pending processing + private var functionCalls = [FunctionCallPart]() + + private var model: GenerativeModel + private var chat: Chat + + private var chatTask: Task? + + init() { + // model = FirebaseAI.firebaseAI(backend: .vertexAI()).generativeModel( + model = FirebaseAI.firebaseAI(backend: .googleAI()).generativeModel( + modelName: "gemini-2.0-flash-001", + tools: [.functionDeclarations([ + FunctionDeclaration( + name: "get_exchange_rate", + description: "Get the exchange rate for currencies between countries", + parameters: [ + "currency_from": .enumeration( + values: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"], + description: "The currency to convert from in ISO 4217 format" + ), + "currency_to": .enumeration( + values: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"], + description: "The currency to convert to in ISO 4217 format" + ), + ] + ), + ])] + ) + chat = model.startChat() + } + + func sendMessage(_ text: String, streaming: Bool = true) async { + error = nil + chatTask?.cancel() + + chatTask = Task { + busy = true + defer { + busy = false + } + + // first, add the user's message to the chat + let userMessage = ChatMessage(message: text, participant: .user) + messages.append(userMessage) + + // add a pending message while we're waiting for a response from the backend + let systemMessage = ChatMessage.pending(participant: .system) + messages.append(systemMessage) + + print(messages) + do { + repeat { + if streaming { + try await internalSendMessageStreaming(text) + } else { + try await internalSendMessage(text) + } + } while !functionCalls.isEmpty + } catch { + self.error = error + print(error.localizedDescription) + messages.removeLast() + } + } + } + + func startNewChat() { + stop() + error = nil + chat = model.startChat() + messages.removeAll() + } + + func stop() { + chatTask?.cancel() + error = nil + } + + private func internalSendMessageStreaming(_ text: String) async throws { + let functionResponses = try await processFunctionCalls() + let responseStream: AsyncThrowingStream + if functionResponses.isEmpty { + responseStream = try chat.sendMessageStream(text) + } else { + for functionResponse in functionResponses { + messages.insert(functionResponse.chatMessage(), at: messages.count - 1) + } + responseStream = try chat.sendMessageStream([functionResponses.modelContent()]) + } + for try await chunk in responseStream { + processResponseContent(content: chunk) + } + } + + private func internalSendMessage(_ text: String) async throws { + let functionResponses = try await processFunctionCalls() + let response: GenerateContentResponse + if functionResponses.isEmpty { + response = try await chat.sendMessage(text) + } else { + for functionResponse in functionResponses { + messages.insert(functionResponse.chatMessage(), at: messages.count - 1) + } + response = try await chat.sendMessage([functionResponses.modelContent()]) + } + processResponseContent(content: response) + } + + func processResponseContent(content: GenerateContentResponse) { + guard let candidate = content.candidates.first else { + fatalError("No candidate.") + } + + for part in candidate.content.parts { + switch part { + case let textPart as TextPart: + // replace pending message with backend response + messages[messages.count - 1].message += textPart.text + messages[messages.count - 1].pending = false + case let functionCallPart as FunctionCallPart: + messages.insert(functionCallPart.chatMessage(), at: messages.count - 1) + functionCalls.append(functionCallPart) + default: + fatalError("Unsupported response part: \(part)") + } + } + } + + func processFunctionCalls() async throws -> [FunctionResponsePart] { + var functionResponses = [FunctionResponsePart]() + for functionCall in functionCalls { + switch functionCall.name { + case "get_exchange_rate": + let exchangeRates = getExchangeRate(args: functionCall.args) + functionResponses.append(FunctionResponsePart( + name: "get_exchange_rate", + response: exchangeRates + )) + default: + fatalError("Unknown function named \"\(functionCall.name)\".") + } + } + functionCalls = [] + + return functionResponses + } + + // MARK: - Callable Functions + + func getExchangeRate(args: JSONObject) -> JSONObject { + // 1. Validate and extract the parameters provided by the model (from a `FunctionCall`) + guard case let .string(from) = args["currency_from"] else { + fatalError("Missing `currency_from` parameter.") + } + guard case let .string(to) = args["currency_to"] else { + fatalError("Missing `currency_to` parameter.") + } + + // 2. Get the exchange rate + let allRates: [String: [String: Double]] = [ + "AUD": ["CAD": 0.89265, "EUR": 0.6072, "GBP": 0.51714, "JPY": 97.75, "USD": 0.66379], + "CAD": ["AUD": 1.1203, "EUR": 0.68023, "GBP": 0.57933, "JPY": 109.51, "USD": 0.74362], + "EUR": ["AUD": 1.6469, "CAD": 1.4701, "GBP": 0.85168, "JPY": 160.99, "USD": 1.0932], + "GBP": ["AUD": 1.9337, "CAD": 1.7261, "EUR": 1.1741, "JPY": 189.03, "USD": 1.2836], + "JPY": ["AUD": 0.01023, "CAD": 0.00913, "EUR": 0.00621, "GBP": 0.00529, "USD": 0.00679], + "USD": ["AUD": 1.5065, "CAD": 1.3448, "EUR": 0.91475, "GBP": 0.77907, "JPY": 147.26], + ] + guard let fromRates = allRates[from] else { + return ["error": .string("No data for currency \(from).")] + } + guard let toRate = fromRates[to] else { + return ["error": .string("No data for currency \(to).")] + } + + // 3. Return the exchange rates as a JSON object (returned to the model in a `FunctionResponse`) + return ["rates": .number(toRate)] + } +} + +private extension FunctionCallPart { + func chatMessage() -> ChatMessage { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + let jsonData: Data + do { + jsonData = try encoder.encode(self) + } catch { + fatalError("JSON Encoding Failed: \(error.localizedDescription)") + } + guard let json = String(data: jsonData, encoding: .utf8) else { + fatalError("Failed to convert JSON data to a String.") + } + let messageText = "Function call requested by model:\n```\n\(json)\n```" + + return ChatMessage(message: messageText, participant: .system) + } +} + +private extension FunctionResponsePart { + func chatMessage() -> ChatMessage { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + + let jsonData: Data + do { + jsonData = try encoder.encode(self) + } catch { + fatalError("JSON Encoding Failed: \(error.localizedDescription)") + } + guard let json = String(data: jsonData, encoding: .utf8) else { + fatalError("Failed to convert JSON data to a String.") + } + let messageText = "Function response returned by app:\n```\n\(json)\n```" + + return ChatMessage(message: messageText, participant: .user) + } +} + +private extension [FunctionResponsePart] { + func modelContent() -> ModelContent { + return ModelContent(role: "function", parts: self) + } +} diff --git a/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/AccentColor.colorset/Contents.json b/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..13613e3ee --- /dev/null +++ b/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/Contents.json b/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/firebaseai/GenerativeAIMultimodalSample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/GenerativeAIMultimodalSample/Preview Content/Preview Assets.xcassets/Contents.json b/firebaseai/GenerativeAIMultimodalSample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/firebaseai/GenerativeAIMultimodalSample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/GenerativeAIMultimodalSample/Screens/PhotoReasoningScreen.swift b/firebaseai/GenerativeAIMultimodalSample/Screens/PhotoReasoningScreen.swift new file mode 100644 index 000000000..930214770 --- /dev/null +++ b/firebaseai/GenerativeAIMultimodalSample/Screens/PhotoReasoningScreen.swift @@ -0,0 +1,78 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import GenerativeAIUIComponents +import MarkdownUI +import PhotosUI +import SwiftUI + +struct PhotoReasoningScreen: View { + @StateObject var viewModel = PhotoReasoningViewModel() + + enum FocusedField: Hashable { + case message + } + + @FocusState + var focusedField: FocusedField? + + var body: some View { + VStack { + MultimodalInputField(text: $viewModel.userInput, selection: $viewModel.selectedItems) + .focused($focusedField, equals: .message) + .onSubmit { + onSendTapped() + } + + ScrollViewReader { scrollViewProxy in + List { + if let outputText = viewModel.outputText { + HStack(alignment: .top) { + if viewModel.inProgress { + ProgressView() + } else { + Image(systemName: "cloud.circle.fill") + .font(.title2) + } + + Markdown("\(outputText)") + } + .listRowSeparator(.hidden) + } + } + .listStyle(.plain) + } + } + .navigationTitle("Multimodal sample") + .onAppear { + focusedField = .message + } + } + + // MARK: - Actions + + private func onSendTapped() { + focusedField = nil + + Task { + await viewModel.reason() + } + } +} + +#Preview { + NavigationStack { + PhotoReasoningScreen() + } +} diff --git a/firebaseai/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift b/firebaseai/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift new file mode 100644 index 000000000..b64d167b9 --- /dev/null +++ b/firebaseai/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift @@ -0,0 +1,121 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAI +import Foundation +import OSLog +import PhotosUI +import SwiftUI + +@MainActor +class PhotoReasoningViewModel: ObservableObject { + // Maximum value for the larger of the two image dimensions (height and width) in pixels. This is + // being used to reduce the image size in bytes. + private static let largestImageDimension = 768.0 + + private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "generative-ai") + + @Published + var userInput: String = "" + + @Published + var selectedItems = [PhotosPickerItem]() + + @Published + var outputText: String? = nil + + @Published + var errorMessage: String? + + @Published + var inProgress = false + + private var model: GenerativeModel? + + init() { + // model = FirebaseAI.firebaseAI(backend: .vertexAI()).generativeModel(modelName: "gemini-2.0-flash-001") + model = FirebaseAI.firebaseAI(backend: .googleAI()) + .generativeModel(modelName: "gemini-2.0-flash-001") + } + + func reason() async { + defer { + inProgress = false + } + guard let model else { + return + } + + do { + inProgress = true + errorMessage = nil + outputText = "" + + let prompt = "Look at the image(s), and then answer the following question: \(userInput)" + + var images = [any PartsRepresentable]() + for item in selectedItems { + if let data = try? await item.loadTransferable(type: Data.self) { + guard let image = UIImage(data: data) else { + logger.error("Failed to parse data as an image, skipping.") + continue + } + if image.size.fits(largestDimension: PhotoReasoningViewModel.largestImageDimension) { + images.append(image) + } else { + guard let resizedImage = image + .preparingThumbnail(of: image.size + .aspectFit(largestDimension: PhotoReasoningViewModel.largestImageDimension)) else { + logger.error("Failed to resize image: \(image)") + continue + } + + images.append(resizedImage) + } + } + } + + let outputContentStream = try model.generateContentStream(prompt, images) + + // stream response + for try await outputContent in outputContentStream { + guard let line = outputContent.text else { + return + } + + outputText = (outputText ?? "") + line + } + } catch { + logger.error("\(error.localizedDescription)") + errorMessage = error.localizedDescription + } + } +} + +private extension CGSize { + func fits(largestDimension length: CGFloat) -> Bool { + return width <= length && height <= length + } + + func aspectFit(largestDimension length: CGFloat) -> CGSize { + let aspectRatio = width / height + if width > height { + let width = min(self.width, length) + return CGSize(width: width, height: round(width / aspectRatio)) + } else { + let height = min(self.height, length) + return CGSize(width: round(height * aspectRatio), height: height) + } + } +} diff --git a/firebaseai/GenerativeAITextSample/Assets.xcassets/AccentColor.colorset/Contents.json b/firebaseai/GenerativeAITextSample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/firebaseai/GenerativeAITextSample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/GenerativeAITextSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/firebaseai/GenerativeAITextSample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..13613e3ee --- /dev/null +++ b/firebaseai/GenerativeAITextSample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/GenerativeAITextSample/Assets.xcassets/Contents.json b/firebaseai/GenerativeAITextSample/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/firebaseai/GenerativeAITextSample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/GenerativeAITextSample/Preview Content/Preview Assets.xcassets/Contents.json b/firebaseai/GenerativeAITextSample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/firebaseai/GenerativeAITextSample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firebaseai/GenerativeAITextSample/Screens/SummarizeScreen.swift b/firebaseai/GenerativeAITextSample/Screens/SummarizeScreen.swift new file mode 100644 index 000000000..748c1addd --- /dev/null +++ b/firebaseai/GenerativeAITextSample/Screens/SummarizeScreen.swift @@ -0,0 +1,80 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import MarkdownUI +import SwiftUI + +struct SummarizeScreen: View { + @StateObject var viewModel = SummarizeViewModel() + @State var userInput = "" + + enum FocusedField: Hashable { + case message + } + + @FocusState + var focusedField: FocusedField? + + var body: some View { + VStack { + VStack(alignment: .leading) { + Text("Enter some text, then tap on _Go_ to summarize it.") + .padding(.horizontal, 6) + HStack(alignment: .top) { + TextField("Enter text summarize", text: $userInput, axis: .vertical) + .focused($focusedField, equals: .message) + .textFieldStyle(.roundedBorder) + .onSubmit { + onSummarizeTapped() + } + Button("Go") { + onSummarizeTapped() + } + .padding(.top, 4) + } + } + .padding(.horizontal, 16) + + List { + HStack(alignment: .top) { + if viewModel.inProgress { + ProgressView() + } else { + Image(systemName: "cloud.circle.fill") + .font(.title2) + } + + Markdown("\(viewModel.outputText)") + } + .listRowSeparator(.hidden) + } + .listStyle(.plain) + } + .navigationTitle("Text sample") + } + + private func onSummarizeTapped() { + focusedField = nil + + Task { + await viewModel.summarize(inputText: userInput) + } + } +} + +#Preview { + NavigationStack { + SummarizeScreen() + } +} diff --git a/firebaseai/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift b/firebaseai/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift new file mode 100644 index 000000000..512ee1c80 --- /dev/null +++ b/firebaseai/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift @@ -0,0 +1,70 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import FirebaseAI +import Foundation +import OSLog + +@MainActor +class SummarizeViewModel: ObservableObject { + private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "generative-ai") + + @Published + var outputText = "" + + @Published + var errorMessage: String? + + @Published + var inProgress = false + + private var model: GenerativeModel? + + init() { + // model = FirebaseAI.firebaseAI(backend: .vertexAI()).generativeModel(modelName: "gemini-2.0-flash-001") + model = FirebaseAI.firebaseAI(backend: .googleAI()) + .generativeModel(modelName: "gemini-2.0-flash-001") + } + + func summarize(inputText: String) async { + defer { + inProgress = false + } + guard let model else { + return + } + + do { + inProgress = true + errorMessage = nil + outputText = "" + + let prompt = "Summarize the following text for me: \(inputText)" + + let outputContentStream = try model.generateContentStream(prompt) + + // stream response + for try await outputContent in outputContentStream { + guard let line = outputContent.text else { + return + } + + outputText = outputText + line + } + } catch { + logger.error("\(error.localizedDescription)") + errorMessage = error.localizedDescription + } + } +} diff --git a/firebaseai/GenerativeAIUIComponents/Package.swift b/firebaseai/GenerativeAIUIComponents/Package.swift new file mode 100644 index 000000000..808f5f42a --- /dev/null +++ b/firebaseai/GenerativeAIUIComponents/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import PackageDescription + +let package = Package( + name: "GenerativeAIUIComponents", + platforms: [ + .iOS(.v16), + ], + products: [ + .library( + name: "GenerativeAIUIComponents", + targets: ["GenerativeAIUIComponents"] + ), + ], + targets: [ + .target( + name: "GenerativeAIUIComponents" + ), + ] +) diff --git a/firebaseai/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift b/firebaseai/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift new file mode 100644 index 000000000..67941c370 --- /dev/null +++ b/firebaseai/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift @@ -0,0 +1,83 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import SwiftUI + +public struct InputField