Skip to content

Commit 72745c5

Browse files
committed
Merge branch 'hotfix/0.35.9'
2 parents ca0bd7a + 66a28d0 commit 72745c5

File tree

22 files changed

+1365
-170
lines changed

22 files changed

+1365
-170
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/ChatContextCollectors/WebChatContextCollector/SearchFunction.swift

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import BingSearchService
21
import ChatBasic
32
import Foundation
43
import OpenAIService
54
import Preferences
5+
import WebSearchService
66

77
struct SearchFunction: ChatGPTFunction {
88
static let dateFormatter = {
@@ -17,13 +17,13 @@ struct SearchFunction: ChatGPTFunction {
1717
}
1818

1919
struct Result: ChatGPTFunctionResult {
20-
var result: BingSearchResult
20+
var result: WebSearchResult
2121

2222
var botReadableContent: String {
23-
result.webPages.value.enumerated().map {
23+
result.webPages.enumerated().map {
2424
let (index, page) = $0
2525
return """
26-
\(index + 1). \(page.name) \(page.url)
26+
\(index + 1). \(page.title) \(page.urlString)
2727
\(page.snippet)
2828
"""
2929
}.joined(separator: "\n")
@@ -72,22 +72,15 @@ struct SearchFunction: ChatGPTFunction {
7272
await reportProgress("Searching \(arguments.query)")
7373

7474
do {
75-
let bingSearch = BingSearchService(
76-
subscriptionKey: UserDefaults.shared.value(for: \.bingSearchSubscriptionKey),
77-
searchURL: UserDefaults.shared.value(for: \.bingSearchEndpoint)
78-
)
75+
let search = WebSearchService(provider: .userPreferred)
7976

80-
let result = try await bingSearch.search(
81-
query: arguments.query,
82-
numberOfResult: maxTokens > 5000 ? 5 : 3,
83-
freshness: arguments.freshness
84-
)
77+
let result = try await search.search(query: arguments.query)
8578

8679
await reportProgress("""
8780
Finish searching \(arguments.query)
8881
\(
89-
result.webPages.value
90-
.map { "- [\($0.name)](\($0.url))" }
82+
result.webPages
83+
.map { "- [\($0.title)](\($0.urlString))" }
9184
.joined(separator: "\n")
9285
)
9386
""")

Core/Sources/HostApp/AccountSettings/BingSearchView.swift

Lines changed: 0 additions & 51 deletions
This file was deleted.
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import AppKit
2+
import Client
3+
import ComposableArchitecture
4+
import OpenAIService
5+
import Preferences
6+
import SuggestionBasic
7+
import SwiftUI
8+
import WebSearchService
9+
import SharedUIComponents
10+
11+
@Reducer
12+
struct WebSearchSettings {
13+
struct TestResult: Identifiable, Equatable {
14+
let id = UUID()
15+
var duration: TimeInterval
16+
var result: Result<WebSearchResult, Error>?
17+
18+
static func == (lhs: Self, rhs: Self) -> Bool {
19+
lhs.id == rhs.id
20+
}
21+
}
22+
23+
@ObservableState
24+
struct State: Equatable {
25+
var apiKeySelection: APIKeySelection.State = .init()
26+
var testResult: TestResult?
27+
}
28+
29+
enum Action: BindableAction {
30+
case binding(BindingAction<State>)
31+
case appear
32+
case test
33+
case bringUpTestResult
34+
case updateTestResult(TimeInterval, Result<WebSearchResult, Error>)
35+
case apiKeySelection(APIKeySelection.Action)
36+
}
37+
38+
var body: some ReducerOf<Self> {
39+
BindingReducer()
40+
41+
Scope(state: \.apiKeySelection, action: \.apiKeySelection) {
42+
APIKeySelection()
43+
}
44+
45+
Reduce { state, action in
46+
switch action {
47+
case .binding:
48+
return .none
49+
case .appear:
50+
state.testResult = nil
51+
state.apiKeySelection.apiKeyName = UserDefaults.shared.value(for: \.serpAPIKeyName)
52+
return .none
53+
case .test:
54+
return .run { send in
55+
let searchService = WebSearchService(provider: .userPreferred)
56+
await send(.bringUpTestResult)
57+
let start = Date()
58+
do {
59+
let result = try await searchService.search(query: "Swift")
60+
let duration = Date().timeIntervalSince(start)
61+
await send(.updateTestResult(duration, .success(result)))
62+
} catch {
63+
let duration = Date().timeIntervalSince(start)
64+
await send(.updateTestResult(duration, .failure(error)))
65+
}
66+
}
67+
case .bringUpTestResult:
68+
state.testResult = .init(duration: 0)
69+
return .none
70+
case let .updateTestResult(duration, result):
71+
state.testResult?.duration = duration
72+
state.testResult?.result = result
73+
return .none
74+
case let .apiKeySelection(action):
75+
switch action {
76+
case .binding(\APIKeySelection.State.apiKeyName):
77+
UserDefaults.shared.set(state.apiKeySelection.apiKeyName, for: \.serpAPIKeyName)
78+
return .none
79+
default:
80+
return .none
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
final class WebSearchViewSettings: ObservableObject {
88+
@AppStorage(\.serpAPIEngine) var serpAPIEngine
89+
@AppStorage(\.headlessBrowserEngine) var headlessBrowserEngine
90+
@AppStorage(\.searchProvider) var searchProvider
91+
init() {}
92+
}
93+
94+
struct WebSearchView: View {
95+
@Perception.Bindable var store: StoreOf<WebSearchSettings>
96+
@Environment(\.openURL) var openURL
97+
@StateObject var settings = WebSearchViewSettings()
98+
99+
var body: some View {
100+
WithPerceptionTracking {
101+
ScrollView {
102+
VStack(alignment: .leading) {
103+
Form {
104+
Picker("Search Provider", selection: $settings.searchProvider) {
105+
ForEach(UserDefaultPreferenceKeys.SearchProvider.allCases, id: \.self) {
106+
provider in
107+
switch provider {
108+
case .serpAPI:
109+
Text("Serp API").tag(provider)
110+
case .headlessBrowser:
111+
Text("Headless Browser").tag(provider)
112+
}
113+
114+
}
115+
}
116+
.pickerStyle(.segmented)
117+
}
118+
119+
switch settings.searchProvider {
120+
case .serpAPI:
121+
serpAPIForm()
122+
case .headlessBrowser:
123+
headlessBrowserForm()
124+
}
125+
}
126+
.padding()
127+
}
128+
.safeAreaInset(edge: .bottom) {
129+
VStack(spacing: 0) {
130+
Divider()
131+
HStack {
132+
Button("Test Search") {
133+
store.send(.test)
134+
}
135+
Spacer()
136+
}
137+
.padding()
138+
}
139+
.background(.regularMaterial)
140+
}
141+
.sheet(item: $store.testResult) { testResult in
142+
testResultView(testResult: testResult)
143+
}
144+
.onAppear {
145+
store.send(.appear)
146+
}
147+
}
148+
}
149+
150+
@ViewBuilder
151+
func serpAPIForm() -> some View {
152+
SubSection(
153+
title: Text("Serp API Settings"),
154+
description: """
155+
Use Serp API to do web search. Serp API is more reliable and faster than headless browser. But you need to provide an API key for it.
156+
"""
157+
) {
158+
Picker("Engine", selection: $settings.serpAPIEngine) {
159+
ForEach(
160+
UserDefaultPreferenceKeys.SerpAPIEngine.allCases,
161+
id: \.self
162+
) { engine in
163+
Text(engine.rawValue).tag(engine)
164+
}
165+
}
166+
167+
WithPerceptionTracking {
168+
APIKeyPicker(store: store.scope(
169+
state: \.apiKeySelection,
170+
action: \.apiKeySelection
171+
))
172+
}
173+
}
174+
}
175+
176+
@ViewBuilder
177+
func headlessBrowserForm() -> some View {
178+
SubSection(
179+
title: Text("Headless Browser Settings"),
180+
description: """
181+
The app will open a webview in the background to do web search. This method uses a set of rules to extract information from the web page, if you notice that it stops working, please submit an issue to the developer.
182+
"""
183+
) {
184+
Picker("Engine", selection: $settings.headlessBrowserEngine) {
185+
ForEach(
186+
UserDefaultPreferenceKeys.HeadlessBrowserEngine.allCases,
187+
id: \.self
188+
) { engine in
189+
Text(engine.rawValue).tag(engine)
190+
}
191+
}
192+
}
193+
}
194+
195+
@ViewBuilder
196+
func testResultView(testResult: WebSearchSettings.TestResult) -> some View {
197+
VStack {
198+
Text("Test Result")
199+
.padding(.top)
200+
.font(.headline)
201+
202+
if let result = testResult.result {
203+
switch result {
204+
case let .success(webSearchResult):
205+
VStack(alignment: .leading) {
206+
Text("Success (Completed in \(testResult.duration, specifier: "%.2f")s)")
207+
.foregroundColor(.green)
208+
209+
Text("Found \(webSearchResult.webPages.count) results:")
210+
211+
ScrollView {
212+
ForEach(webSearchResult.webPages, id: \.urlString) { page in
213+
HStack {
214+
VStack(alignment: .leading) {
215+
Text(page.title)
216+
.font(.headline)
217+
Text(page.urlString)
218+
.font(.caption)
219+
.foregroundColor(.blue)
220+
Text(page.snippet)
221+
.padding(.top, 2)
222+
}
223+
Spacer(minLength: 0)
224+
}
225+
.padding(.vertical, 4)
226+
Divider()
227+
}
228+
}
229+
}
230+
.padding()
231+
case let .failure(error):
232+
VStack(alignment: .leading) {
233+
Text("Error (Completed in \(testResult.duration, specifier: "%.2f")s)")
234+
.foregroundColor(.red)
235+
Text(error.localizedDescription)
236+
}
237+
}
238+
} else {
239+
ProgressView().padding()
240+
}
241+
242+
Spacer()
243+
244+
VStack(spacing: 0) {
245+
Divider()
246+
247+
HStack {
248+
Spacer()
249+
250+
Button("Close") {
251+
store.testResult = nil
252+
}
253+
.keyboardShortcut(.cancelAction)
254+
}
255+
.padding()
256+
}
257+
}
258+
.frame(minWidth: 400, minHeight: 300)
259+
}
260+
}
261+
262+
// Helper struct to make TestResult identifiable for sheet presentation
263+
private struct TestResultWrapper: Identifiable {
264+
var id: UUID = .init()
265+
var testResult: WebSearchSettings.TestResult
266+
}
267+
268+
struct WebSearchView_Previews: PreviewProvider {
269+
static var previews: some View {
270+
VStack(alignment: .leading, spacing: 8) {
271+
WebSearchView(store: .init(initialState: .init(), reducer: { WebSearchSettings() }))
272+
}
273+
.frame(height: 800)
274+
.padding(.all, 8)
275+
}
276+
}
277+

0 commit comments

Comments
 (0)