diff --git a/firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj b/firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj index bffe367de..015ff616d 100644 --- a/firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj +++ b/firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj @@ -60,6 +60,14 @@ AEE793DF2E256D3900708F02 /* GoogleSearchSuggestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE793DC2E256D3900708F02 /* GoogleSearchSuggestionView.swift */; }; AEE793E02E256D3900708F02 /* GroundedResponseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE793DD2E256D3900708F02 /* GroundedResponseView.swift */; }; DE26D95F2DBB3E9F007E6668 /* FirebaseAI in Frameworks */ = {isa = PBXBuildFile; productRef = DE26D95E2DBB3E9F007E6668 /* FirebaseAI */; }; + DE907A812EAAE53E00AE56CE /* GenerateContentFromTemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE907A802EAAE53E00AE56CE /* GenerateContentFromTemplateScreen.swift */; }; + DE907A822EAAE53E00AE56CE /* GenerateContentFromTemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE907A802EAAE53E00AE56CE /* GenerateContentFromTemplateScreen.swift */; }; + DE907A842EAAE55600AE56CE /* GenerateContentFromTemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE907A832EAAE55600AE56CE /* GenerateContentFromTemplateViewModel.swift */; }; + DE907A852EAAE55600AE56CE /* GenerateContentFromTemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE907A832EAAE55600AE56CE /* GenerateContentFromTemplateViewModel.swift */; }; + DE907A882EAAEBCC00AE56CE /* ImagenFromTemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE907A872EAAEBCC00AE56CE /* ImagenFromTemplateViewModel.swift */; }; + DE907A892EAAEBCC00AE56CE /* ImagenFromTemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE907A862EAAEBCC00AE56CE /* ImagenFromTemplateScreen.swift */; }; + DE907A8A2EAAEBCC00AE56CE /* ImagenFromTemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE907A872EAAEBCC00AE56CE /* ImagenFromTemplateViewModel.swift */; }; + DE907A8B2EAAEBCC00AE56CE /* ImagenFromTemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE907A862EAAEBCC00AE56CE /* ImagenFromTemplateScreen.swift */; }; 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 */ @@ -94,6 +102,10 @@ 88E10F5C2B11135000C08E95 /* BouncingDots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BouncingDots.swift; sourceTree = ""; }; AEE793DC2E256D3900708F02 /* GoogleSearchSuggestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSearchSuggestionView.swift; sourceTree = ""; }; AEE793DD2E256D3900708F02 /* GroundedResponseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroundedResponseView.swift; sourceTree = ""; }; + DE907A802EAAE53E00AE56CE /* GenerateContentFromTemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateContentFromTemplateScreen.swift; sourceTree = ""; }; + DE907A832EAAE55600AE56CE /* GenerateContentFromTemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateContentFromTemplateViewModel.swift; sourceTree = ""; }; + DE907A862EAAEBCC00AE56CE /* ImagenFromTemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagenFromTemplateScreen.swift; sourceTree = ""; }; + DE907A872EAAEBCC00AE56CE /* ImagenFromTemplateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagenFromTemplateViewModel.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 */ @@ -171,6 +183,7 @@ 88209C1A2B0FBDC300F64795 /* Screens */ = { isa = PBXGroup; children = ( + DE907A802EAAE53E00AE56CE /* GenerateContentFromTemplateScreen.swift */, 88209C1B2B0FBDC300F64795 /* GenerateContentScreen.swift */, ); path = Screens; @@ -179,6 +192,7 @@ 88209C1C2B0FBDC300F64795 /* ViewModels */ = { isa = PBXGroup; children = ( + DE907A832EAAE55600AE56CE /* GenerateContentFromTemplateViewModel.swift */, 88209C1D2B0FBDC300F64795 /* GenerateContentViewModel.swift */, ); path = ViewModels; @@ -342,6 +356,8 @@ DEFECAA82D7B4CCD00EF9621 /* ImagenScreen */ = { isa = PBXGroup; children = ( + DE907A862EAAEBCC00AE56CE /* ImagenFromTemplateScreen.swift */, + DE907A872EAAEBCC00AE56CE /* ImagenFromTemplateViewModel.swift */, DEFECAA62D7B4CCD00EF9621 /* ImagenScreen.swift */, DEFECAA72D7B4CCD00EF9621 /* ImagenViewModel.swift */, ); @@ -468,6 +484,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DE907A852EAAE55600AE56CE /* GenerateContentFromTemplateViewModel.swift in Sources */, + DE907A812EAAE53E00AE56CE /* GenerateContentFromTemplateScreen.swift in Sources */, 86BB55EA2E8B2D6D0054B8B5 /* FunctionCallingScreen.swift in Sources */, 86BB55EB2E8B2D6D0054B8B5 /* BouncingDots.swift in Sources */, 86BB55EC2E8B2D6D0054B8B5 /* FunctionCallingViewModel.swift in Sources */, @@ -481,6 +499,8 @@ 86BB55F42E8B2D6D0054B8B5 /* PhotoReasoningScreen.swift in Sources */, 86BB55F52E8B2D6D0054B8B5 /* ImagenViewModel.swift in Sources */, 86BB55F62E8B2D6D0054B8B5 /* ImagenScreen.swift in Sources */, + DE907A882EAAEBCC00AE56CE /* ImagenFromTemplateViewModel.swift in Sources */, + DE907A892EAAEBCC00AE56CE /* ImagenFromTemplateScreen.swift in Sources */, 86BB55F72E8B2D6D0054B8B5 /* PhotoReasoningViewModel.swift in Sources */, 86BB55F82E8B2D6D0054B8B5 /* ConversationScreen.swift in Sources */, 86BB55F92E8B2D6D0054B8B5 /* ErrorView.swift in Sources */, @@ -494,6 +514,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DE907A842EAAE55600AE56CE /* GenerateContentFromTemplateViewModel.swift in Sources */, + DE907A822EAAE53E00AE56CE /* GenerateContentFromTemplateScreen.swift in Sources */, 86C1F4832BC726150026816F /* FunctionCallingScreen.swift in Sources */, 886F95DF2B17D5010036F07A /* BouncingDots.swift in Sources */, 86C1F4842BC726150026816F /* FunctionCallingViewModel.swift in Sources */, @@ -507,6 +529,8 @@ 886F95DC2B17BAEF0036F07A /* PhotoReasoningScreen.swift in Sources */, DEFECAA92D7B4CCD00EF9621 /* ImagenViewModel.swift in Sources */, DEFECAAA2D7B4CCD00EF9621 /* ImagenScreen.swift in Sources */, + DE907A8A2EAAEBCC00AE56CE /* ImagenFromTemplateViewModel.swift in Sources */, + DE907A8B2EAAEBCC00AE56CE /* ImagenFromTemplateScreen.swift in Sources */, 886F95DB2B17BAEF0036F07A /* PhotoReasoningViewModel.swift in Sources */, 886F95E12B17D5010036F07A /* ConversationScreen.swift in Sources */, 88263BF02B239C09008AB09B /* ErrorView.swift in Sources */, @@ -828,7 +852,7 @@ repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 12.0.0; + minimumVersion = 12.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/firebaseai/FirebaseAIExample/ContentView.swift b/firebaseai/FirebaseAIExample/ContentView.swift index 1714a0653..73bc64fcf 100644 --- a/firebaseai/FirebaseAIExample/ContentView.swift +++ b/firebaseai/FirebaseAIExample/ContentView.swift @@ -55,6 +55,11 @@ struct ContentView: View { } label: { Label("Generate Content", systemImage: "doc.text") } + NavigationLink { + GenerateContentFromTemplateScreen(firebaseService: firebaseService) + } label: { + Label("Generate Content from Template", systemImage: "doc.text.fill") + } NavigationLink { PhotoReasoningScreen(firebaseService: firebaseService) } label: { @@ -84,6 +89,11 @@ struct ContentView: View { } label: { Label("Imagen", systemImage: "camera.circle") } + NavigationLink { + ImagenFromTemplateScreen(firebaseService: firebaseService) + } label: { + Label("Imagen from Template", systemImage: "camera.circle.fill") + } } } .navigationTitle("Generative AI Examples") diff --git a/firebaseai/GenerativeAITextExample/Screens/GenerateContentFromTemplateScreen.swift b/firebaseai/GenerativeAITextExample/Screens/GenerateContentFromTemplateScreen.swift new file mode 100644 index 000000000..1b4b28bbe --- /dev/null +++ b/firebaseai/GenerativeAITextExample/Screens/GenerateContentFromTemplateScreen.swift @@ -0,0 +1,91 @@ +// Copyright 2025 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 +#if canImport(FirebaseAILogic) + import FirebaseAILogic +#else + import FirebaseAI +#endif +import GenerativeAIUIComponents + +struct GenerateContentFromTemplateScreen: View { + let firebaseService: FirebaseAI + @StateObject var viewModel: GenerateContentFromTemplateViewModel + @State var userInput = "" + + init(firebaseService: FirebaseAI) { + self.firebaseService = firebaseService + _viewModel = + StateObject( + wrappedValue: GenerateContentFromTemplateViewModel(firebaseService: firebaseService) + ) + } + + enum FocusedField: Hashable { + case message + } + + @FocusState + var focusedField: FocusedField? + + var body: some View { + VStack { + VStack(alignment: .leading) { + Text("Enter your name, then tap on _Go_ to run generateContent from template on it.") + .padding(.horizontal, 6) + InputField("Enter your name", text: $userInput) { + Text("Go") + } + .focused($focusedField, equals: .message) + .onSubmit { onGenerateContentTapped() } + } + .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) + } + .onTapGesture { + focusedField = nil + } + .navigationTitle("Template Generate Content") + } + + private func onGenerateContentTapped() { + focusedField = nil + + Task { + await viewModel.generateContentFromTemplate(name: userInput) + } + } +} + +#Preview { + NavigationStack { + GenerateContentFromTemplateScreen(firebaseService: FirebaseAI.firebaseAI()) + } +} diff --git a/firebaseai/GenerativeAITextExample/ViewModels/GenerateContentFromTemplateViewModel.swift b/firebaseai/GenerativeAITextExample/ViewModels/GenerateContentFromTemplateViewModel.swift new file mode 100644 index 000000000..d07544f2e --- /dev/null +++ b/firebaseai/GenerativeAITextExample/ViewModels/GenerateContentFromTemplateViewModel.swift @@ -0,0 +1,92 @@ +// Copyright 2025 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. + +#if canImport(FirebaseAILogic) + import FirebaseAILogic +#else + import FirebaseAI +#endif +import Foundation +import OSLog + +// Template Details +// +// Configuration +// +// input: +// default: +// language: english +// schema: +// name: string +// language?: string +// +// Prompt and system instructions +// +// {{role "system"}} +// The user's name is {{name}}. They prefer to communicate in {{language}}. +// {{role "user"}} +// Say hello. +// + +@MainActor +class GenerateContentFromTemplateViewModel: ObservableObject { + private var logger = Logger( + subsystem: Bundle.main.bundleIdentifier ?? "com.google.firebase.quickstart.FirebaseAIExample", + category: "generative-ai" + ) + + @Published + var outputText = "" + + @Published + var errorMessage: String? + + @Published + var inProgress = false + + private var model: TemplateGenerativeModel? + + init(firebaseService: FirebaseAI) { + model = firebaseService.templateGenerativeModel() + } + + func generateContentFromTemplate(name: String) async { + defer { + inProgress = false + } + guard let model else { + return + } + + do { + inProgress = true + errorMessage = nil + outputText = "" + + let response = try await model.generateContent( + templateID: "apple-qs-greeting", + inputs: [ + "name": name, + "language": "Spanish", + ] + ) + if let text = response.text { + outputText = text + } + } catch { + logger.error("\(error.localizedDescription)") + errorMessage = error.localizedDescription + } + } +} diff --git a/firebaseai/ImagenScreen/ImagenFromTemplateScreen.swift b/firebaseai/ImagenScreen/ImagenFromTemplateScreen.swift new file mode 100644 index 000000000..8f2df7d27 --- /dev/null +++ b/firebaseai/ImagenScreen/ImagenFromTemplateScreen.swift @@ -0,0 +1,103 @@ +// Copyright 2025 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 +import GenerativeAIUIComponents +#if canImport(FirebaseAILogic) + import FirebaseAILogic +#else + import FirebaseAI +#endif + +struct ImagenFromTemplateScreen: View { + let firebaseService: FirebaseAI + @StateObject var viewModel: ImagenFromTemplateViewModel + + init(firebaseService: FirebaseAI) { + self.firebaseService = firebaseService + _viewModel = + StateObject(wrappedValue: ImagenFromTemplateViewModel(firebaseService: firebaseService)) + } + + enum FocusedField: Hashable { + case message + } + + @FocusState + var focusedField: FocusedField? + + var body: some View { + ZStack { + ScrollView { + VStack { + InputField( + "Enter a prompt to generate an image from template", + text: $viewModel.userInput + ) { + Image( + systemName: viewModel.inProgress ? "stop.circle.fill" : "paperplane.circle.fill" + ) + .font(.title) + } + .focused($focusedField, equals: .message) + .onSubmit { sendOrStop() } + + let spacing: CGFloat = 10 + LazyVGrid(columns: [ + GridItem(.flexible(), spacing: spacing), + GridItem(.flexible(), spacing: spacing), + ], spacing: spacing) { + ForEach(viewModel.images, id: \.self) { image in + Image(uiImage: image) + .resizable() + .aspectRatio(1, contentMode: .fill) + .cornerRadius(12) + .clipped() + } + } + .padding(.horizontal, spacing) + } + } + if viewModel.inProgress { + ProgressOverlay() + } + } + .onTapGesture { + focusedField = nil + } + .navigationTitle("Imagen Template") + .onAppear { + focusedField = .message + } + } + + private func sendMessage() { + Task { + await viewModel.generateImageFromTemplate(prompt: viewModel.userInput) + focusedField = .message + } + } + + private func sendOrStop() { + if viewModel.inProgress { + viewModel.stop() + } else { + sendMessage() + } + } +} + +#Preview { + ImagenFromTemplateScreen(firebaseService: FirebaseAI.firebaseAI()) +} diff --git a/firebaseai/ImagenScreen/ImagenFromTemplateViewModel.swift b/firebaseai/ImagenScreen/ImagenFromTemplateViewModel.swift new file mode 100644 index 000000000..02bb15bfc --- /dev/null +++ b/firebaseai/ImagenScreen/ImagenFromTemplateViewModel.swift @@ -0,0 +1,93 @@ +// Copyright 2025 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. + +#if canImport(FirebaseAILogic) + import FirebaseAILogic +#else + import FirebaseAI +#endif +import Foundation +import OSLog +import SwiftUI + +// Template Details +// +// Configuration +// +// input: +// schema: +// prompt: 'string' +// +// Prompt and system instructions +// +// Create an image containing {{prompt}} +// + +@MainActor +class ImagenFromTemplateViewModel: ObservableObject { + private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "generative-ai") + + @Published + var userInput: String = "" + + @Published + var images = [UIImage]() + + @Published + var errorMessage: String? + + @Published + var inProgress = false + + private let model: TemplateImagenModel + + private var generateImagesTask: Task? + + init(firebaseService: FirebaseAI) { + model = firebaseService.templateImagenModel() + } + + func generateImageFromTemplate(prompt: String) async { + stop() + + generateImagesTask = Task { + inProgress = true + defer { + inProgress = false + } + + do { + let response = try await model.generateImages( + templateID: "image-generation-basic", + inputs: [ + "prompt": prompt, + ] + ) + + if !Task.isCancelled { + images = response.images.compactMap { UIImage(data: $0.data) } + } + } catch { + if !Task.isCancelled { + logger.error("Error generating images from template: \(error)") + } + } + } + } + + func stop() { + generateImagesTask?.cancel() + generateImagesTask = nil + } +}