Skip to content

Commit 5388546

Browse files
committed
Refactor
1 parent 99c1362 commit 5388546

File tree

5 files changed

+166
-90
lines changed

5 files changed

+166
-90
lines changed

firebaseai/ChatExample/Views/WebView.swift renamed to firebaseai/ChatExample/Views/GoogleSearchSuggestionView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import SwiftUI
1616
import WebKit
1717

18-
struct WebView: UIViewRepresentable {
18+
struct GoogleSearchSuggestionView: UIViewRepresentable {
1919
let htmlString: String
2020

2121
// This Coordinator class will act as the web view's navigation delegate.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2025 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+
import SwiftUI
16+
import WebKit
17+
18+
struct GoogleSearchSuggestionView: UIViewRepresentable {
19+
let htmlString: String
20+
21+
// This Coordinator class will act as the web view's navigation delegate.
22+
class Coordinator: NSObject, WKNavigationDelegate {
23+
func webView(_ webView: WKWebView,
24+
decidePolicyFor navigationAction: WKNavigationAction,
25+
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
26+
// Check if the navigation was triggered by a user clicking a link.
27+
if navigationAction.navigationType == .linkActivated {
28+
if let url = navigationAction.request.url {
29+
// Open the URL in the system's default browser (e.g., Safari).
30+
UIApplication.shared.open(url)
31+
}
32+
// Cancel the navigation inside our small web view.
33+
decisionHandler(.cancel)
34+
return
35+
}
36+
// For all other navigation types (like the initial HTML load), allow it.
37+
decisionHandler(.allow)
38+
}
39+
}
40+
41+
func makeCoordinator() -> Coordinator {
42+
Coordinator()
43+
}
44+
45+
func makeUIView(context: Context) -> WKWebView {
46+
let webView = WKWebView()
47+
webView.isOpaque = false
48+
webView.backgroundColor = .clear
49+
webView.scrollView.backgroundColor = .clear
50+
webView.scrollView.isScrollEnabled = false
51+
// Set the coordinator as the navigation delegate.
52+
webView.navigationDelegate = context.coordinator
53+
return webView
54+
}
55+
56+
func updateUIView(_ uiView: WKWebView, context: Context) {
57+
// The renderedContent is an HTML snippet with CSS.
58+
// For it to render correctly, we wrap it in a basic HTML document structure.
59+
let fullHTML = """
60+
<!DOCTYPE html>
61+
<html>
62+
<head>
63+
<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=no'>
64+
<style>
65+
body { margin: 0; padding: 0; }
66+
</style>
67+
</head>
68+
<body>
69+
\(htmlString)
70+
</body>
71+
</html>
72+
"""
73+
uiView.loadHTMLString(fullHTML, baseURL: nil)
74+
}
75+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
import FirebaseAI
16+
import SwiftUI
17+
18+
/// A view that displays a chat message that is grounded in Google Search.
19+
struct GroundedResponseView: View {
20+
var message: ChatMessage
21+
var groundingMetadata: GroundingMetadata
22+
23+
var body: some View {
24+
// We can only display a response grounded in Google Search if the searchEntrypoint is non-nil.
25+
let isCompliant = (groundingMetadata.groundingChunks.isEmpty || groundingMetadata
26+
.searchEntryPoint != nil)
27+
if isCompliant {
28+
HStack(alignment: .top, spacing: 8) {
29+
VStack(alignment: .leading, spacing: 8) {
30+
// Message text
31+
ResponseTextView(message: message)
32+
33+
if !groundingMetadata.groundingChunks.isEmpty {
34+
Divider()
35+
// Source links
36+
ForEach(0 ..< groundingMetadata.groundingChunks.count, id: \.self) { index in
37+
if let webChunk = groundingMetadata.groundingChunks[index].web {
38+
SourceLinkView(
39+
title: webChunk.title ?? "Untitled Source",
40+
uri: webChunk.uri
41+
)
42+
}
43+
}
44+
}
45+
// Search suggestions
46+
if let searchEntryPoint = groundingMetadata.searchEntryPoint {
47+
Divider()
48+
GoogleSearchSuggestionView(htmlString: searchEntryPoint.renderedContent)
49+
.frame(height: 44)
50+
.clipShape(RoundedRectangle(cornerRadius: 22))
51+
}
52+
}
53+
}
54+
.frame(maxWidth: UIScreen.main.bounds.width * 0.8, alignment: .leading)
55+
}
56+
}
57+
}
58+
59+
/// A view for a single, clickable source link.
60+
struct SourceLinkView: View {
61+
let title: String
62+
let uri: String?
63+
64+
var body: some View {
65+
if let uri, let url = URL(string: uri) {
66+
Link(destination: url) {
67+
HStack(spacing: 4) {
68+
Image(systemName: "link")
69+
.font(.caption)
70+
.foregroundColor(.secondary)
71+
Text(title)
72+
.font(.footnote)
73+
.underline()
74+
.lineLimit(1)
75+
.multilineTextAlignment(.leading)
76+
}
77+
}
78+
.buttonStyle(.plain)
79+
}
80+
}
81+
}

firebaseai/ChatExample/Views/MessageView.swift

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -80,49 +80,6 @@ struct ResponseTextView: View {
8080
}
8181
}
8282

83-
struct GroundedResponseView: View {
84-
var message: ChatMessage
85-
var groundingMetadata: GroundingMetadata
86-
87-
var body: some View {
88-
// We can only display a grounded response if the searchEntrypoint is non-nil.
89-
// If the searchEntrypoint is nil, we can only display the response if it's not grounded.
90-
let isNonCompliant = (!groundingMetadata.groundingChunks.isEmpty && groundingMetadata
91-
.searchEntryPoint == nil)
92-
if isNonCompliant {
93-
ComplianceErrorView()
94-
} else {
95-
HStack(alignment: .top, spacing: 8) {
96-
VStack(alignment: .leading, spacing: 8) {
97-
// Message text
98-
ResponseTextView(message: message)
99-
100-
if !groundingMetadata.groundingChunks.isEmpty {
101-
Divider()
102-
// Source links
103-
ForEach(0 ..< groundingMetadata.groundingChunks.count, id: \.self) { index in
104-
if let webChunk = groundingMetadata.groundingChunks[index].web {
105-
SourceLinkView(
106-
title: webChunk.title ?? "Untitled Source",
107-
uri: webChunk.uri
108-
)
109-
}
110-
}
111-
}
112-
// Search suggestions
113-
if let searchEntryPoint = groundingMetadata.searchEntryPoint {
114-
Divider()
115-
WebView(htmlString: searchEntryPoint.renderedContent)
116-
.frame(height: 44)
117-
.clipShape(RoundedRectangle(cornerRadius: 22))
118-
}
119-
}
120-
}
121-
.frame(maxWidth: UIScreen.main.bounds.width * 0.8, alignment: .leading)
122-
}
123-
}
124-
}
125-
12683
struct MessageView: View {
12784
var message: ChatMessage
12885

@@ -164,44 +121,3 @@ struct MessageView_Previews: PreviewProvider {
164121
}
165122
}
166123
}
167-
168-
/// A simplified view for a single, clickable source link.
169-
struct SourceLinkView: View {
170-
let title: String
171-
let uri: String?
172-
173-
var body: some View {
174-
if let uri, let url = URL(string: uri) {
175-
Link(destination: url) {
176-
HStack(spacing: 4) {
177-
Image(systemName: "link")
178-
.font(.caption)
179-
.foregroundColor(.secondary)
180-
Text(title)
181-
.font(.footnote)
182-
.underline()
183-
.lineLimit(1)
184-
.multilineTextAlignment(.leading)
185-
}
186-
}
187-
.buttonStyle(.plain)
188-
}
189-
}
190-
}
191-
192-
/// A view to show when a response cannot be displayed due to compliance or other errors.
193-
struct ComplianceErrorView: View {
194-
var message =
195-
"Could not display the response because it was missing required attribution components."
196-
197-
var body: some View {
198-
HStack {
199-
Image(systemName: "exclamationmark.triangle.fill")
200-
.foregroundColor(.orange)
201-
Text(message)
202-
}
203-
.padding()
204-
.background(Color(.secondarySystemBackground))
205-
.clipShape(RoundedRectangle(cornerRadius: 12))
206-
}
207-
}

firebaseai/FirebaseAIExample.xcodeproj/project.pbxproj

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 56;
6+
objectVersion = 70;
77
objects = {
88

99
/* Begin PBXBuildFile section */
10-
8611DB362E21AA1600132740 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8611DB352E21AA1600132740 /* WebView.swift */; };
1110
869200B32B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; };
1211
86C1F4832BC726150026816F /* FunctionCallingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86C1F47E2BC726150026816F /* FunctionCallingScreen.swift */; };
1312
86C1F4842BC726150026816F /* FunctionCallingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86C1F4802BC726150026816F /* FunctionCallingViewModel.swift */; };
@@ -34,7 +33,6 @@
3433
/* End PBXBuildFile section */
3534

3635
/* Begin PBXFileReference section */
37-
8611DB352E21AA1600132740 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
3836
869200B22B879C4F00482873 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
3937
86C1F47E2BC726150026816F /* FunctionCallingScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionCallingScreen.swift; sourceTree = "<group>"; };
4038
86C1F4802BC726150026816F /* FunctionCallingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FunctionCallingViewModel.swift; sourceTree = "<group>"; };
@@ -65,6 +63,10 @@
6563
DEFECAA72D7B4CCD00EF9621 /* ImagenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagenViewModel.swift; sourceTree = "<group>"; };
6664
/* End PBXFileReference section */
6765

66+
/* Begin PBXFileSystemSynchronizedRootGroup section */
67+
AE7230862E2567B50037E50A /* Grounding */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Grounding; sourceTree = "<group>"; };
68+
/* End PBXFileSystemSynchronizedRootGroup section */
69+
6870
/* Begin PBXFrameworksBuildPhase section */
6971
8848C82C2B0D04BC007B434F /* Frameworks */ = {
7072
isa = PBXFrameworksBuildPhase;
@@ -256,11 +258,11 @@
256258
88E10F512B11124100C08E95 /* Views */ = {
257259
isa = PBXGroup;
258260
children = (
261+
AE7230862E2567B50037E50A /* Grounding */,
259262
88263BEE2B239BFE008AB09B /* ErrorView.swift */,
260263
88E10F5A2B11133E00C08E95 /* MessageView.swift */,
261264
88E10F5C2B11135000C08E95 /* BouncingDots.swift */,
262265
889873842B208563005B4896 /* ErrorDetailsView.swift */,
263-
8611DB352E21AA1600132740 /* WebView.swift */,
264266
);
265267
path = Views;
266268
sourceTree = "<group>";
@@ -305,6 +307,9 @@
305307
);
306308
dependencies = (
307309
);
310+
fileSystemSynchronizedGroups = (
311+
AE7230862E2567B50037E50A /* Grounding */,
312+
);
308313
name = FirebaseAIExample;
309314
packageProductDependencies = (
310315
886F95D72B17BA420036F07A /* MarkdownUI */,
@@ -378,7 +383,6 @@
378383
88263BF12B239C11008AB09B /* ErrorDetailsView.swift in Sources */,
379384
8848C8352B0D04BC007B434F /* ContentView.swift in Sources */,
380385
886F95D52B17BA010036F07A /* GenerateContentScreen.swift in Sources */,
381-
8611DB362E21AA1600132740 /* WebView.swift in Sources */,
382386
8848C8332B0D04BC007B434F /* FirebaseAIExampleApp.swift in Sources */,
383387
886F95E02B17D5010036F07A /* ConversationViewModel.swift in Sources */,
384388
886F95DD2B17D5010036F07A /* MessageView.swift in Sources */,

0 commit comments

Comments
 (0)