Skip to content

Commit aa2397f

Browse files
committed
Add settings view for web search
1 parent b68fd33 commit aa2397f

File tree

4 files changed

+238
-33
lines changed

4 files changed

+238
-33
lines changed

Core/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ let package = Package(
126126
.product(name: "Toast", package: "Tool"),
127127
.product(name: "SharedUIComponents", package: "Tool"),
128128
.product(name: "SuggestionBasic", package: "Tool"),
129+
.product(name: "WebSearchService", package: "Tool"),
129130
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
130131
.product(name: "OpenAIService", package: "Tool"),
131132
.product(name: "Preferences", package: "Tool"),

Core/Sources/HostApp/AccountSettings/WebSearchView.swift

Lines changed: 217 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,242 @@
11
import AppKit
22
import Client
3+
import ComposableArchitecture
34
import OpenAIService
45
import Preferences
56
import SuggestionBasic
67
import SwiftUI
8+
import WebSearchService
79

8-
final class BingSearchViewSettings: ObservableObject {
9-
@AppStorage(\.bingSearchSubscriptionKey) var bingSearchSubscriptionKey: String
10-
@AppStorage(\.bingSearchEndpoint) var bingSearchEndpoint: String
10+
@Reducer
11+
struct WebSearchSettings {
12+
struct TestResult: Identifiable, Equatable {
13+
let id = UUID()
14+
var duration: TimeInterval
15+
var result: Result<WebSearchResult, Error>?
16+
17+
static func == (lhs: Self, rhs: Self) -> Bool {
18+
lhs.id == rhs.id
19+
}
20+
}
21+
22+
@ObservableState
23+
struct State: Equatable {
24+
var apiKeySelection: APIKeySelection.State = .init()
25+
var testResult: TestResult?
26+
}
27+
28+
enum Action: BindableAction {
29+
case binding(BindingAction<State>)
30+
case appear
31+
case test
32+
case bringUpTestResult
33+
case updateTestResult(TimeInterval, Result<WebSearchResult, Error>)
34+
case apiKeySelection(APIKeySelection.Action)
35+
}
36+
37+
var body: some ReducerOf<Self> {
38+
BindingReducer()
39+
40+
Scope(state: \.apiKeySelection, action: \.apiKeySelection) {
41+
APIKeySelection()
42+
}
43+
44+
Reduce { state, action in
45+
switch action {
46+
case .binding:
47+
return .none
48+
case .appear:
49+
state.testResult = nil
50+
state.apiKeySelection.apiKeyName = UserDefaults.shared.value(for: \.serpAPIKeyName)
51+
return .none
52+
case .test:
53+
return .run { send in
54+
let searchService = WebSearchService(provider: .userPreferred)
55+
await send(.bringUpTestResult)
56+
let start = Date()
57+
do {
58+
let result = try await searchService.search(query: "Swift")
59+
let duration = Date().timeIntervalSince(start)
60+
await send(.updateTestResult(duration, .success(result)))
61+
} catch {
62+
let duration = Date().timeIntervalSince(start)
63+
await send(.updateTestResult(duration, .failure(error)))
64+
}
65+
}
66+
case .bringUpTestResult:
67+
state.testResult = .init(duration: 0)
68+
return .none
69+
case let .updateTestResult(duration, result):
70+
state.testResult?.duration = duration
71+
state.testResult?.result = result
72+
return .none
73+
case let .apiKeySelection(action):
74+
switch action {
75+
case .binding(\APIKeySelection.State.apiKeyName):
76+
UserDefaults.shared.set(state.apiKeySelection.apiKeyName, for: \.serpAPIKeyName)
77+
return .none
78+
default:
79+
return .none
80+
}
81+
}
82+
}
83+
}
84+
}
85+
86+
final class WebSearchViewSettings: ObservableObject {
87+
@AppStorage(\.serpAPIEngine) var serpAPIEngine
88+
@AppStorage(\.headlessBrowserEngine) var headlessBrowserEngine
89+
@AppStorage(\.searchProvider) var searchProvider
1190
init() {}
1291
}
1392

14-
struct BingSearchView: View {
93+
struct WebSearchView: View {
94+
@Perception.Bindable var store: StoreOf<WebSearchSettings>
1595
@Environment(\.openURL) var openURL
16-
@StateObject var settings = BingSearchViewSettings()
96+
@StateObject var settings = WebSearchViewSettings()
1797

1898
var body: some View {
19-
Form {
20-
Button(action: {
21-
let url = URL(string: "https://www.microsoft.com/bing/apis/bing-web-search-api")!
22-
openURL(url)
23-
}) {
24-
Text("Apply for Subscription Key for Free")
99+
WithPerceptionTracking {
100+
ScrollView {
101+
Form {
102+
Section(header: Text("Search Provider")) {
103+
Picker("Search Provider", selection: $settings.searchProvider) {
104+
ForEach(UserDefaultPreferenceKeys.SearchProvider.allCases, id: \.self) {
105+
provider in
106+
Text(provider.rawValue).tag(provider)
107+
}
108+
}
109+
.pickerStyle(.segmented)
110+
}
111+
112+
switch settings.searchProvider {
113+
case .serpAPI:
114+
serpAPIForm()
115+
case .headlessBrowser:
116+
headlessBrowserForm()
117+
}
118+
119+
Section {
120+
Button("Test Search") {
121+
store.send(.test)
122+
}
123+
}
124+
}
125+
.padding()
25126
}
26-
27-
SecureField(text: $settings.bingSearchSubscriptionKey, prompt: Text("")) {
28-
Text("Bing Search Subscription Key")
127+
.sheet(item: $store.testResult) { testResult in
128+
testResultView(testResult: testResult)
29129
}
30-
.textFieldStyle(.roundedBorder)
130+
.onAppear {
131+
store.send(.appear)
132+
}
133+
}
134+
}
135+
136+
@ViewBuilder
137+
func serpAPIForm() -> some View {
138+
Section(header: Text("SerpAPI")) {
139+
Picker("Engine", selection: $settings.serpAPIEngine) {
140+
ForEach(
141+
UserDefaultPreferenceKeys.SerpAPIEngine.allCases,
142+
id: \.self
143+
) { engine in
144+
Text(engine.rawValue).tag(engine)
145+
}
146+
}
147+
148+
WithPerceptionTracking {
149+
APIKeyPicker(store: store.scope(
150+
state: \.apiKeySelection,
151+
action: \.apiKeySelection
152+
))
153+
}
154+
}
155+
}
31156

32-
TextField(
33-
text: $settings.bingSearchEndpoint,
34-
prompt: Text("https://api.bing.microsoft.com/***")
35-
) {
36-
Text("Bing Search Endpoint")
37-
}.textFieldStyle(.roundedBorder)
157+
@ViewBuilder
158+
func headlessBrowserForm() -> some View {
159+
Section(header: Text("Headless Browser")) {
160+
Picker("Engine", selection: $settings.headlessBrowserEngine) {
161+
ForEach(
162+
UserDefaultPreferenceKeys.HeadlessBrowserEngine.allCases,
163+
id: \.self
164+
) { engine in
165+
Text(engine.rawValue).tag(engine)
166+
}
167+
}
38168
}
39169
}
170+
171+
@ViewBuilder
172+
func testResultView(testResult: WebSearchSettings.TestResult) -> some View {
173+
VStack {
174+
Text("Test Result")
175+
.font(.headline)
176+
.padding()
177+
178+
if let result = testResult.result {
179+
switch result {
180+
case let .success(webSearchResult):
181+
VStack(alignment: .leading) {
182+
Text("Success (Completed in \(testResult.duration, specifier: "%.2f")s)")
183+
.foregroundColor(.green)
184+
185+
Text("Found \(webSearchResult.webPages.count) results:")
186+
.padding(.top)
187+
188+
ScrollView {
189+
ForEach(webSearchResult.webPages, id: \.urlString) { page in
190+
VStack(alignment: .leading) {
191+
Text(page.title)
192+
.font(.headline)
193+
Text(page.urlString)
194+
.font(.caption)
195+
.foregroundColor(.blue)
196+
Text(page.snippet)
197+
.padding(.top, 2)
198+
}
199+
.padding(.vertical, 4)
200+
Divider()
201+
}
202+
}
203+
}
204+
.padding()
205+
case let .failure(error):
206+
VStack(alignment: .leading) {
207+
Text("Error (Completed in \(testResult.duration, specifier: "%.2f")s)")
208+
.foregroundColor(.red)
209+
Text(error.localizedDescription)
210+
.padding(.top)
211+
}
212+
.padding()
213+
}
214+
} else {
215+
VStack {
216+
ProgressView()
217+
}
218+
.padding()
219+
}
220+
221+
Button("Close") {
222+
store.testResult = nil
223+
}
224+
.padding()
225+
}
226+
.frame(minWidth: 400, minHeight: 300)
227+
}
228+
}
229+
230+
// Helper struct to make TestResult identifiable for sheet presentation
231+
private struct TestResultWrapper: Identifiable {
232+
var id: UUID = .init()
233+
var testResult: WebSearchSettings.TestResult
40234
}
41235

42-
struct BingSearchView_Previews: PreviewProvider {
236+
struct WebSearchView_Previews: PreviewProvider {
43237
static var previews: some View {
44238
VStack(alignment: .leading, spacing: 8) {
45-
BingSearchView()
239+
WebSearchView(store: .init(initialState: .init(), reducer: { WebSearchSettings() }))
46240
}
47241
.frame(height: 800)
48242
.padding(.all, 8)

Core/Sources/HostApp/HostApp.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ struct HostApp {
1818
var general = General.State()
1919
var chatModelManagement = ChatModelManagement.State()
2020
var embeddingModelManagement = EmbeddingModelManagement.State()
21+
var webSearchSettings = WebSearchSettings.State()
2122
}
2223

2324
enum Action {
2425
case appear
2526
case general(General.Action)
2627
case chatModelManagement(ChatModelManagement.Action)
2728
case embeddingModelManagement(EmbeddingModelManagement.Action)
29+
case webSearchSettings(WebSearchSettings.Action)
2830
}
2931

3032
@Dependency(\.toast) var toast
@@ -45,6 +47,10 @@ struct HostApp {
4547
Scope(state: \.embeddingModelManagement, action: \.embeddingModelManagement) {
4648
EmbeddingModelManagement()
4749
}
50+
51+
Scope(state: \.webSearchSettings, action: \.webSearchSettings) {
52+
WebSearchSettings()
53+
}
4854

4955
Reduce { _, action in
5056
switch action {
@@ -62,6 +68,9 @@ struct HostApp {
6268

6369
case .embeddingModelManagement:
6470
return .none
71+
72+
case .webSearchSettings:
73+
return .none
6574
}
6675
}
6776
}

Core/Sources/HostApp/ServiceView.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct ServiceView: View {
1717
subtitle: "Suggestion",
1818
image: "globe"
1919
)
20-
20+
2121
ScrollView {
2222
CodeiumView().padding()
2323
}.sidebarItem(
@@ -26,7 +26,7 @@ struct ServiceView: View {
2626
subtitle: "Suggestion",
2727
image: "globe"
2828
)
29-
29+
3030
ChatModelManagementView(store: store.scope(
3131
state: \.chatModelManagement,
3232
action: \.chatModelManagement
@@ -36,7 +36,7 @@ struct ServiceView: View {
3636
subtitle: "Chat, Modification",
3737
image: "globe"
3838
)
39-
39+
4040
EmbeddingModelManagementView(store: store.scope(
4141
state: \.embeddingModelManagement,
4242
action: \.embeddingModelManagement
@@ -46,16 +46,17 @@ struct ServiceView: View {
4646
subtitle: "Chat, Modification",
4747
image: "globe"
4848
)
49-
50-
ScrollView {
51-
BingSearchView().padding()
52-
}.sidebarItem(
49+
50+
WebSearchView(store: store.scope(
51+
state: \.webSearchSettings,
52+
action: \.webSearchSettings
53+
)).sidebarItem(
5354
tag: 4,
54-
title: "Bing Search",
55-
subtitle: "Search Chat Plugin",
55+
title: "Web Search",
56+
subtitle: "Chat, Modification",
5657
image: "globe"
5758
)
58-
59+
5960
ScrollView {
6061
OtherSuggestionServicesView().padding()
6162
}.sidebarItem(

0 commit comments

Comments
 (0)