Skip to content

Commit 3fadb66

Browse files
committed
Missing files
1 parent 1b7225d commit 3fadb66

File tree

2 files changed

+246
-0
lines changed

2 files changed

+246
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#if canImport(FirebaseAILogic)
16+
import FirebaseAILogic
17+
#else
18+
import FirebaseAI
19+
#endif
20+
import GenerativeAIUIComponents
21+
import SwiftUI
22+
23+
struct ConversationFromTemplateScreen: View {
24+
let firebaseService: FirebaseAI
25+
let title: String
26+
@StateObject var viewModel: ConversationFromTemplateViewModel
27+
28+
@State
29+
private var userPrompt = ""
30+
31+
init(firebaseService: FirebaseAI, title: String) {
32+
self.title = title
33+
self.firebaseService = firebaseService
34+
_viewModel =
35+
StateObject(wrappedValue: ConversationFromTemplateViewModel(firebaseService: firebaseService))
36+
}
37+
38+
enum FocusedField: Hashable {
39+
case message
40+
}
41+
42+
@FocusState
43+
var focusedField: FocusedField?
44+
45+
var body: some View {
46+
VStack {
47+
ScrollViewReader { scrollViewProxy in
48+
List {
49+
ForEach(viewModel.messages) { message in
50+
MessageView(message: message)
51+
}
52+
if let error = viewModel.error {
53+
ErrorView(error: error)
54+
.tag("errorView")
55+
}
56+
}
57+
.listStyle(.plain)
58+
.onChange(of: viewModel.messages, perform: { newValue in
59+
if viewModel.hasError {
60+
// wait for a short moment to make sure we can actually scroll to the bottom
61+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
62+
withAnimation {
63+
scrollViewProxy.scrollTo("errorView", anchor: .bottom)
64+
}
65+
focusedField = .message
66+
}
67+
} else {
68+
guard let lastMessage = viewModel.messages.last else { return }
69+
70+
// wait for a short moment to make sure we can actually scroll to the bottom
71+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
72+
withAnimation {
73+
scrollViewProxy.scrollTo(lastMessage.id, anchor: .bottom)
74+
}
75+
focusedField = .message
76+
}
77+
}
78+
})
79+
}
80+
InputField("Message...", text: $userPrompt) {
81+
Image(systemName: viewModel.busy ? "stop.circle.fill" : "arrow.up.circle.fill")
82+
.font(.title)
83+
}
84+
.focused($focusedField, equals: .message)
85+
.onSubmit { sendOrStop() }
86+
}
87+
.onTapGesture {
88+
focusedField = nil
89+
}
90+
.toolbar {
91+
ToolbarItem(placement: .primaryAction) {
92+
Button(action: newChat) {
93+
Image(systemName: "square.and.pencil")
94+
}
95+
}
96+
}
97+
.navigationTitle(title)
98+
.onAppear {
99+
focusedField = .message
100+
}
101+
}
102+
103+
private func sendMessage() {
104+
Task {
105+
let prompt = userPrompt
106+
userPrompt = ""
107+
await viewModel.sendMessage(prompt)
108+
}
109+
}
110+
111+
private func sendOrStop() {
112+
focusedField = nil
113+
114+
if viewModel.busy {
115+
viewModel.stop()
116+
} else {
117+
sendMessage()
118+
}
119+
}
120+
121+
private func newChat() {
122+
viewModel.startNewChat()
123+
}
124+
}
125+
126+
struct ConversationFromTemplateScreen_Previews: PreviewProvider {
127+
struct ContainerView: View {
128+
@StateObject var viewModel = ConversationFromTemplateViewModel(firebaseService: FirebaseAI
129+
.firebaseAI()) // Example service init
130+
131+
var body: some View {
132+
ConversationFromTemplateScreen(
133+
firebaseService: FirebaseAI.firebaseAI(),
134+
title: "Chat from Template sample"
135+
)
136+
.onAppear {
137+
viewModel.messages = ChatMessage.samples
138+
}
139+
}
140+
}
141+
142+
static var previews: some View {
143+
NavigationStack {
144+
ConversationFromTemplateScreen(
145+
firebaseService: FirebaseAI.firebaseAI(),
146+
title: "Chat from Template sample"
147+
)
148+
}
149+
}
150+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#if canImport(FirebaseAILogic)
16+
import FirebaseAILogic
17+
#else
18+
import FirebaseAI
19+
#endif
20+
import Foundation
21+
import UIKit
22+
23+
@MainActor
24+
class ConversationFromTemplateViewModel: ObservableObject {
25+
/// This array holds both the user's and the system's chat messages
26+
@Published var messages = [ChatMessage]()
27+
28+
/// Indicates we're waiting for the model to finish
29+
@Published var busy = false
30+
31+
@Published var error: Error?
32+
var hasError: Bool {
33+
return error != nil
34+
}
35+
36+
private var model: TemplateGenerativeModel
37+
private var chat: TemplateChatSession
38+
private var stopGenerating = false
39+
40+
private var chatTask: Task<Void, Never>?
41+
42+
init(firebaseService: FirebaseAI) {
43+
model = firebaseService.templateGenerativeModel()
44+
chat = model.startChat(templateID: "chat-history")
45+
}
46+
47+
func sendMessage(_ text: String) async {
48+
error = nil
49+
await internalSendMessage(text)
50+
}
51+
52+
func startNewChat() {
53+
stop()
54+
error = nil
55+
chat = model.startChat(templateID: "chat-history")
56+
messages.removeAll()
57+
}
58+
59+
func stop() {
60+
chatTask?.cancel()
61+
error = nil
62+
}
63+
64+
private func internalSendMessage(_ text: String) async {
65+
chatTask?.cancel()
66+
67+
chatTask = Task {
68+
busy = true
69+
defer {
70+
busy = false
71+
}
72+
73+
// first, add the user's message to the chat
74+
let userMessage = ChatMessage(message: text, participant: .user)
75+
messages.append(userMessage)
76+
77+
// add a pending message while we're waiting for a response from the backend
78+
let systemMessage = ChatMessage.pending(participant: .system)
79+
messages.append(systemMessage)
80+
81+
do {
82+
let response = try await chat.sendMessage(text, inputs: ["message": text])
83+
84+
if let responseText = response.text {
85+
// replace pending message with backend response
86+
messages[messages.count - 1].message = responseText
87+
messages[messages.count - 1].pending = false
88+
}
89+
} catch {
90+
self.error = error
91+
print(error.localizedDescription)
92+
messages.removeLast()
93+
}
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)