Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions firebaseai/ChatExample/Models/ChatMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,21 @@ extension ChatMessage {

static var sample = samples[0]
}

extension ChatMessage {
static func from(_ modelContent: ModelContent) -> ChatMessage? {
// TODO: add non-text parts to message when multi-model support is added
let text = modelContent.parts.compactMap { ($0 as? TextPart)?.text }.joined()
guard !text.isEmpty else {
return nil
}

let participant: Participant = (modelContent.role == "user") ? .user : .system

return ChatMessage(message: text, participant: participant)
}

static func from(_ modelContents: [ModelContent]) -> [ChatMessage] {
return modelContents.compactMap { from($0) }
}
}
23 changes: 11 additions & 12 deletions firebaseai/ChatExample/Screens/ConversationScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,16 @@ import SwiftUI

struct ConversationScreen: View {
let firebaseService: FirebaseAI
let title: String
@StateObject var viewModel: ConversationViewModel

@State
private var userPrompt = ""

init(firebaseService: FirebaseAI, title: String, searchGroundingEnabled: Bool = false) {
let model = firebaseService.generativeModel(
modelName: "gemini-2.0-flash-001",
tools: searchGroundingEnabled ? [.googleSearch()] : []
)
self.title = title
init(firebaseService: FirebaseAI, sample: Sample? = nil) {
self.firebaseService = firebaseService
_viewModel =
StateObject(wrappedValue: ConversationViewModel(firebaseService: firebaseService,
model: model))
sample: sample))
}

enum FocusedField: Hashable {
Expand Down Expand Up @@ -95,9 +89,13 @@ struct ConversationScreen: View {
}
}
}
.navigationTitle(title)
.navigationTitle(viewModel.title)
.onAppear {
focusedField = .message
// Set initial prompt from viewModel if available
if userPrompt.isEmpty && !viewModel.initialPrompt.isEmpty {
userPrompt = viewModel.initialPrompt
}
}
}

Expand All @@ -121,16 +119,17 @@ struct ConversationScreen: View {

private func newChat() {
viewModel.startNewChat()
userPrompt = ""
}
}

struct ConversationScreen_Previews: PreviewProvider {
struct ContainerView: View {
@StateObject var viewModel = ConversationViewModel(firebaseService: FirebaseAI
.firebaseAI()) // Example service init
.firebaseAI(), sample: nil) // Example service init

var body: some View {
ConversationScreen(firebaseService: FirebaseAI.firebaseAI(), title: "Chat sample")
ConversationScreen(firebaseService: FirebaseAI.firebaseAI())
.onAppear {
viewModel.messages = ChatMessage.samples
}
Expand All @@ -139,7 +138,7 @@ struct ConversationScreen_Previews: PreviewProvider {

static var previews: some View {
NavigationStack {
ConversationScreen(firebaseService: FirebaseAI.firebaseAI(), title: "Chat sample")
ConversationScreen(firebaseService: FirebaseAI.firebaseAI())
}
}
}
32 changes: 25 additions & 7 deletions firebaseai/ChatExample/ViewModels/ConversationViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import FirebaseAI
import Foundation
import UIKit
import GenerativeAIUIComponents

@MainActor
class ConversationViewModel: ObservableObject {
Expand All @@ -29,21 +30,37 @@ class ConversationViewModel: ObservableObject {
return error != nil
}

@Published var initialPrompt: String = ""
@Published var title: String = ""

private var model: GenerativeModel
private var chat: Chat
private var stopGenerating = false

private var chatTask: Task<Void, Never>?

init(firebaseService: FirebaseAI, model: GenerativeModel? = nil) {
if let model {
self.model = model
private var sample: Sample?

init(firebaseService: FirebaseAI, sample: Sample? = nil) {
self.sample = sample

// create a generative model with sample data
model = firebaseService.generativeModel(
modelName: "gemini-2.0-flash-001",
tools: sample?.tools,
systemInstruction: sample?.systemInstruction
)

if let chatHistory = sample?.chatHistory, !chatHistory.isEmpty {
// Initialize with sample chat history if it's available
messages = ChatMessage.from(chatHistory)
chat = model.startChat(history: chatHistory)
} else {
self.model = firebaseService.generativeModel(
modelName: "gemini-2.0-flash-001"
)
chat = model.startChat()
}
chat = self.model.startChat()

initialPrompt = sample?.initialPrompt ?? ""
title = sample?.title ?? ""
}

func sendMessage(_ text: String, streaming: Bool = true) async {
Expand All @@ -60,6 +77,7 @@ class ConversationViewModel: ObservableObject {
error = nil
chat = model.startChat()
messages.removeAll()
initialPrompt = ""
}

func stop() {
Expand Down
62 changes: 44 additions & 18 deletions firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 60;
objects = {

/* Begin PBXBuildFile section */
7200F3082E3A054300CDC51C /* GenerativeAIUIComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 7200F3072E3A054300CDC51C /* GenerativeAIUIComponents */; };
72DA044F2E385DF3004FED7D /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72DA044E2E385DF3004FED7D /* ChatMessage.swift */; };
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 */; };
Expand All @@ -22,11 +24,11 @@
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 */; };
A5E8E3C92C3B4F388A7A4A19 /* FilterChipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E8E3C52C3B4F388A7A4A15 /* FilterChipView.swift */; };
A5E8E3CA2C3B4F388A7A4A1A /* SampleCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E8E3C62C3B4F388A7A4A16 /* SampleCardView.swift */; };
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 */; };
Expand All @@ -35,6 +37,8 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
726634072E37011C00554974 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Package.swift; path = GenerativeAIUIComponents/Package.swift; sourceTree = "<group>"; };
72DA044E2E385DF3004FED7D /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = "<group>"; };
869200B22B879C4F00482873 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
86C1F47E2BC726150026816F /* FunctionCallingScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionCallingScreen.swift; sourceTree = "<group>"; };
86C1F4802BC726150026816F /* FunctionCallingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionCallingViewModel.swift; sourceTree = "<group>"; };
Expand All @@ -58,9 +62,10 @@
88E10F4B2B110D5400C08E95 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
88E10F542B1112CA00C08E95 /* ConversationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationScreen.swift; sourceTree = "<group>"; };
88E10F562B1112F600C08E95 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; };
88E10F582B11131900C08E95 /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = "<group>"; };
88E10F5A2B11133E00C08E95 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = "<group>"; };
88E10F5C2B11135000C08E95 /* BouncingDots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BouncingDots.swift; sourceTree = "<group>"; };
A5E8E3C52C3B4F388A7A4A15 /* FilterChipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterChipView.swift; sourceTree = "<group>"; };
A5E8E3C62C3B4F388A7A4A16 /* SampleCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleCardView.swift; sourceTree = "<group>"; };
AEE793DC2E256D3900708F02 /* GoogleSearchSuggestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSearchSuggestionView.swift; sourceTree = "<group>"; };
AEE793DD2E256D3900708F02 /* GroundedResponseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroundedResponseView.swift; sourceTree = "<group>"; };
DEFECAA62D7B4CCD00EF9621 /* ImagenScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagenScreen.swift; sourceTree = "<group>"; };
Expand All @@ -74,13 +79,21 @@
files = (
DE26D95F2DBB3E9F007E6668 /* FirebaseAI in Frameworks */,
886F95D82B17BA420036F07A /* MarkdownUI in Frameworks */,
886F95E32B17D6630036F07A /* GenerativeAIUIComponents in Frameworks */,
7200F3082E3A054300CDC51C /* GenerativeAIUIComponents in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
72DA044D2E385DED004FED7D /* Models */ = {
isa = PBXGroup;
children = (
72DA044E2E385DF3004FED7D /* ChatMessage.swift */,
);
path = Models;
sourceTree = "<group>";
};
86C1F47F2BC726150026816F /* Screens */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -141,6 +154,7 @@
88209C222B0FBE1700F64795 /* Frameworks */ = {
isa = PBXGroup;
children = (
726634072E37011C00554974 /* Package.swift */,
);
name = Frameworks;
sourceTree = "<group>";
Expand Down Expand Up @@ -176,6 +190,7 @@
8848C8342B0D04BC007B434F /* ContentView.swift */,
8848C8362B0D04BD007B434F /* Assets.xcassets */,
8848C8382B0D04BD007B434F /* Preview Content */,
A5E8E3C22C3B4F388A7A4A12 /* Views */,
);
path = FirebaseAIExample;
sourceTree = "<group>";
Expand Down Expand Up @@ -229,7 +244,7 @@
88E10F432B110D5300C08E95 /* ChatExample */ = {
isa = PBXGroup;
children = (
88E10F522B11124A00C08E95 /* Models */,
72DA044D2E385DED004FED7D /* Models */,
88E10F502B11123600C08E95 /* ViewModels */,
88E10F512B11124100C08E95 /* Views */,
88E10F532B1112B900C08E95 /* Screens */,
Expand Down Expand Up @@ -267,20 +282,21 @@
path = Views;
sourceTree = "<group>";
};
88E10F522B11124A00C08E95 /* Models */ = {
88E10F532B1112B900C08E95 /* Screens */ = {
isa = PBXGroup;
children = (
88E10F582B11131900C08E95 /* ChatMessage.swift */,
88E10F542B1112CA00C08E95 /* ConversationScreen.swift */,
);
path = Models;
path = Screens;
sourceTree = "<group>";
};
88E10F532B1112B900C08E95 /* Screens */ = {
A5E8E3C22C3B4F388A7A4A12 /* Views */ = {
isa = PBXGroup;
children = (
88E10F542B1112CA00C08E95 /* ConversationScreen.swift */,
A5E8E3C52C3B4F388A7A4A15 /* FilterChipView.swift */,
A5E8E3C62C3B4F388A7A4A16 /* SampleCardView.swift */,
);
path = Screens;
path = Views;
sourceTree = "<group>";
};
AEE793DE2E256D3900708F02 /* Grounding */ = {
Expand Down Expand Up @@ -319,8 +335,8 @@
name = FirebaseAIExample;
packageProductDependencies = (
886F95D72B17BA420036F07A /* MarkdownUI */,
886F95E22B17D6630036F07A /* GenerativeAIUIComponents */,
DE26D95E2DBB3E9F007E6668 /* FirebaseAI */,
7200F3072E3A054300CDC51C /* GenerativeAIUIComponents */,
);
productName = GenerativeAIExample;
productReference = 8848C82F2B0D04BC007B434F /* FirebaseAIExample.app */;
Expand Down Expand Up @@ -354,6 +370,7 @@
88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
DEA09AC32B1FCE22001962D9 /* XCRemoteSwiftPackageReference "NetworkImage" */,
DEFECAAB2D7BB49700EF9621 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
7200F3062E3A054300CDC51C /* XCLocalSwiftPackageReference "GenerativeAIUIComponents" */,
);
productRefGroup = 8848C8302B0D04BC007B434F /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -385,7 +402,6 @@
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 /* GenerateContentScreen.swift in Sources */,
Expand All @@ -398,7 +414,10 @@
886F95DB2B17BAEF0036F07A /* PhotoReasoningViewModel.swift in Sources */,
886F95E12B17D5010036F07A /* ConversationScreen.swift in Sources */,
88263BF02B239C09008AB09B /* ErrorView.swift in Sources */,
72DA044F2E385DF3004FED7D /* ChatMessage.swift in Sources */,
886F95D62B17BA010036F07A /* GenerateContentViewModel.swift in Sources */,
A5E8E3C92C3B4F388A7A4A19 /* FilterChipView.swift in Sources */,
A5E8E3CA2C3B4F388A7A4A1A /* SampleCardView.swift in Sources */,
AEE793DF2E256D3900708F02 /* GoogleSearchSuggestionView.swift in Sources */,
AEE793E02E256D3900708F02 /* GroundedResponseView.swift in Sources */,
);
Expand Down Expand Up @@ -609,6 +628,13 @@
};
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
7200F3062E3A054300CDC51C /* XCLocalSwiftPackageReference "GenerativeAIUIComponents" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = GenerativeAIUIComponents;
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCRemoteSwiftPackageReference section */
88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
isa = XCRemoteSwiftPackageReference;
Expand Down Expand Up @@ -637,15 +663,15 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
7200F3072E3A054300CDC51C /* GenerativeAIUIComponents */ = {
isa = XCSwiftPackageProductDependency;
productName = GenerativeAIUIComponents;
};
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" */;
Expand Down
Loading