google search

This commit is contained in:
陈连辰
2023-04-29 21:50:06 +08:00
parent 56c3d9340c
commit 5a7cb06c18
14 changed files with 638 additions and 150 deletions

View File

@@ -55,6 +55,10 @@
CB1F1DD029DDBA0B008CFD0B /* AIPromptPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB1F1DCF29DDBA0B008CFD0B /* AIPromptPopView.swift */; };
CB2449F829D721F3006EE829 /* SystemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2449F729D721F3006EE829 /* SystemManager.swift */; };
CB2449FA29D7FE38006EE829 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2449F929D7FE38006EE829 /* ServerManager.swift */; };
CB26A2DF29FC1DCF001EF861 /* GoogleSearch+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB26A2DB29FC1DCF001EF861 /* GoogleSearch+CoreDataClass.swift */; };
CB26A2E029FC1DCF001EF861 /* GoogleSearch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB26A2DC29FC1DCF001EF861 /* GoogleSearch+CoreDataProperties.swift */; };
CB26A2E129FC1DCF001EF861 /* GoogleSearchResult+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB26A2DD29FC1DCF001EF861 /* GoogleSearchResult+CoreDataClass.swift */; };
CB26A2E229FC1DCF001EF861 /* GoogleSearchResult+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB26A2DE29FC1DCF001EF861 /* GoogleSearchResult+CoreDataProperties.swift */; };
CB27655C29D1C12C00897E0E /* MarkdownContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB27655B29D1C12C00897E0E /* MarkdownContentView.swift */; };
CB27656629D1DA9800897E0E /* AIPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB27656529D1DA9800897E0E /* AIPromptView.swift */; };
CB27657329D30F1400897E0E /* AIPromptViewMdoel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB27657229D30F1400897E0E /* AIPromptViewMdoel.swift */; };
@@ -62,8 +66,6 @@
CB28A52229C07BE500F0286A /* KeyboardMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB28A52129C07BE500F0286A /* KeyboardMonitor.swift */; };
CB28A52829C1569900F0286A /* ThinkingAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB28A52729C1569900F0286A /* ThinkingAnimationView.swift */; };
CB2A943229F828E500D3A048 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = CB2A943129F828E500D3A048 /* SwiftSoup */; };
CB2A943529F830B300D3A048 /* GoogleSearch+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A943329F830B300D3A048 /* GoogleSearch+CoreDataClass.swift */; };
CB2A943629F830B300D3A048 /* GoogleSearch+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A943429F830B300D3A048 /* GoogleSearch+CoreDataProperties.swift */; };
CB2A943929F830E500D3A048 /* Conversation+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A943729F830E500D3A048 /* Conversation+CoreDataClass.swift */; };
CB2A943A29F830E500D3A048 /* Conversation+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A943829F830E500D3A048 /* Conversation+CoreDataProperties.swift */; };
CB2A944029F973F800D3A048 /* GoogleSearchPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB2A943F29F973F800D3A048 /* GoogleSearchPopView.swift */; };
@@ -133,14 +135,17 @@
CB228EA129CD4949006B3559 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
CB2449F729D721F3006EE829 /* SystemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemManager.swift; sourceTree = "<group>"; };
CB2449F929D7FE38006EE829 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = "<group>"; };
CB26A2DB29FC1DCF001EF861 /* GoogleSearch+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GoogleSearch+CoreDataClass.swift"; sourceTree = "<group>"; };
CB26A2DC29FC1DCF001EF861 /* GoogleSearch+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GoogleSearch+CoreDataProperties.swift"; sourceTree = "<group>"; };
CB26A2DD29FC1DCF001EF861 /* GoogleSearchResult+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GoogleSearchResult+CoreDataClass.swift"; sourceTree = "<group>"; };
CB26A2DE29FC1DCF001EF861 /* GoogleSearchResult+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GoogleSearchResult+CoreDataProperties.swift"; sourceTree = "<group>"; };
CB26A2E329FC399F001EF861 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
CB27655B29D1C12C00897E0E /* MarkdownContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownContentView.swift; sourceTree = "<group>"; };
CB27656529D1DA9800897E0E /* AIPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIPromptView.swift; sourceTree = "<group>"; };
CB27657229D30F1400897E0E /* AIPromptViewMdoel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIPromptViewMdoel.swift; sourceTree = "<group>"; };
CB27657429D33D7A00897E0E /* AIPromptInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIPromptInputView.swift; sourceTree = "<group>"; };
CB28A52129C07BE500F0286A /* KeyboardMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardMonitor.swift; sourceTree = "<group>"; };
CB28A52729C1569900F0286A /* ThinkingAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThinkingAnimationView.swift; sourceTree = "<group>"; };
CB2A943329F830B300D3A048 /* GoogleSearch+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GoogleSearch+CoreDataClass.swift"; sourceTree = "<group>"; };
CB2A943429F830B300D3A048 /* GoogleSearch+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GoogleSearch+CoreDataProperties.swift"; sourceTree = "<group>"; };
CB2A943729F830E500D3A048 /* Conversation+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataClass.swift"; sourceTree = "<group>"; };
CB2A943829F830E500D3A048 /* Conversation+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataProperties.swift"; sourceTree = "<group>"; };
CB2A943F29F973F800D3A048 /* GoogleSearchPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleSearchPopView.swift; sourceTree = "<group>"; };
@@ -270,6 +275,7 @@
CB1DCAC029B4F09D00B1D4E1 /* OSXChatGPT */ = {
isa = PBXGroup;
children = (
CB26A2E329FC399F001EF861 /* Info.plist */,
CBD5AB6729E7078E007B6625 /* Markdown */,
CB0F5A5C29D059C4005B71D2 /* SyntaxHighlighter */,
182B436E29BC5D1B00F06778 /* DataProvider */,
@@ -355,10 +361,12 @@
CBC4B11429B8CB1B00650296 /* Models */ = {
isa = PBXGroup;
children = (
CB26A2DB29FC1DCF001EF861 /* GoogleSearch+CoreDataClass.swift */,
CB26A2DC29FC1DCF001EF861 /* GoogleSearch+CoreDataProperties.swift */,
CB26A2DD29FC1DCF001EF861 /* GoogleSearchResult+CoreDataClass.swift */,
CB26A2DE29FC1DCF001EF861 /* GoogleSearchResult+CoreDataProperties.swift */,
CB2A943729F830E500D3A048 /* Conversation+CoreDataClass.swift */,
CB2A943829F830E500D3A048 /* Conversation+CoreDataProperties.swift */,
CB2A943329F830B300D3A048 /* GoogleSearch+CoreDataClass.swift */,
CB2A943429F830B300D3A048 /* GoogleSearch+CoreDataProperties.swift */,
CB2D438729F0183A007742AE /* ChatGPT+CoreDataClass.swift */,
CB2D438829F0183A007742AE /* ChatGPT+CoreDataProperties.swift */,
CB1F015D29EB9D05009CF942 /* Message+CoreDataClass.swift */,
@@ -485,15 +493,16 @@
CB2449FA29D7FE38006EE829 /* ServerManager.swift in Sources */,
CB1F014829E99B5E009CF942 /* Int+IsOdd.swift in Sources */,
CB2F972029CE1ADC004EBD96 /* OSXChatGPT.xcdatamodeld in Sources */,
CB26A2E029FC1DCF001EF861 /* GoogleSearch+CoreDataProperties.swift in Sources */,
182B436929BC5C8700F06778 /* UserInitializeView.swift in Sources */,
CB2449F829D721F3006EE829 /* SystemManager.swift in Sources */,
CB2A944229F993A200D3A048 /* GoogleSearchSettingView.swift in Sources */,
182B43A429BF730300F06778 /* NSColor.swift in Sources */,
CB1F012D29E99982009CF942 /* Grammar.swift in Sources */,
CB27656629D1DA9800897E0E /* AIPromptView.swift in Sources */,
CB26A2E229FC1DCF001EF861 /* GoogleSearchResult+CoreDataProperties.swift in Sources */,
CB1F014929E99B5E009CF942 /* Sequence+Occurrences.swift in Sources */,
182B436B29BC5CBA00F06778 /* AppDelegate.swift in Sources */,
CB2A943629F830B300D3A048 /* GoogleSearch+CoreDataProperties.swift in Sources */,
CB1F015B29EAFBF5009CF942 /* MessageText+CoreDataClass.swift in Sources */,
182B437329BC5D1B00F06778 /* CoreDataManager.swift in Sources */,
CB2A944029F973F800D3A048 /* GoogleSearchPopView.swift in Sources */,
@@ -504,7 +513,6 @@
CB2A943A29F830E500D3A048 /* Conversation+CoreDataProperties.swift in Sources */,
182B436629BC5C8700F06778 /* View.swift in Sources */,
CB1F012929E995BA009CF942 /* MarkdownTextBuilder.swift in Sources */,
CB2A943529F830B300D3A048 /* GoogleSearch+CoreDataClass.swift in Sources */,
CB1F015F29EB9D05009CF942 /* Message+CoreDataClass.swift in Sources */,
182B437B29BC5FBE00F06778 /* EnterAPIView.swift in Sources */,
CB0F5A5F29D059C4005B71D2 /* TextOutputFormat.swift in Sources */,
@@ -526,6 +534,7 @@
CB1F014429E99B5E009CF942 /* String+PrefixChecking.swift in Sources */,
CB27657529D33D7A00897E0E /* AIPromptInputView.swift in Sources */,
182B437229BC5D1B00F06778 /* HTTPClient.swift in Sources */,
CB26A2DF29FC1DCF001EF861 /* GoogleSearch+CoreDataClass.swift in Sources */,
CB53A3BF29D48C8F00A5B8FC /* Prompt+CoreDataProperties.swift in Sources */,
CB1F012729E9832F009CF942 /* MarkdownTextAttributesReader.swift in Sources */,
CB1F012C29E99982009CF942 /* SwiftGrammar.swift in Sources */,
@@ -534,6 +543,7 @@
CB28A52829C1569900F0286A /* ThinkingAnimationView.swift in Sources */,
CBD5AB6629E6EFE3007B6625 /* MarkdownView.swift in Sources */,
CBD5AB6B29E707F0007B6625 /* MarkdownTokenType.swift in Sources */,
CB26A2E129FC1DCF001EF861 /* GoogleSearchResult+CoreDataClass.swift in Sources */,
182B437529BC5D1B00F06778 /* ChatGPTManager.swift in Sources */,
CB2D438A29F0183A007742AE /* ChatGPT+CoreDataProperties.swift in Sources */,
CB2F972229CED6AE004EBD96 /* ChatRoomInputView.swift in Sources */,
@@ -688,6 +698,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OSXChatGPT/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -716,6 +727,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OSXChatGPT/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -6,7 +6,7 @@
//
import Foundation
import SwiftSoup
private let OSXChatGPTKEY = "OSXChatGPT_apiKey_key"
private let OSXChatGoogleSearchKEY = "OSXChatGPT_GoogleSearch_apiKey_key"
@@ -76,10 +76,14 @@ struct ChatGPTRequest {
get {
let arr = messages.suffix(contextCount.valyeInt * 2 + 1)
var temp: [[String: String]] = []
var tokens: Int64 = 0
arr.forEach { msg in
if msg.type != 1 {
//gpt
if msg.msgType == .normal {
//
temp.append(["role": msg.role ?? "user", "content": msg.text ?? ""])
tokens += Int64("rolecontent".count)
tokens += Int64((msg.role ?? "user").count)
tokens += Int64((msg.text ?? "").count)
}
}
if let system = systemMsg {
@@ -232,11 +236,11 @@ class ChatGPTManager {
let key = UserDefaults.standard.value(forKey: OSXChatGPTKEY) as? String
return key ?? ""
}()
private lazy var googleSearchApiKey : String = {
lazy var googleSearchApiKey : String = {
let key = UserDefaults.standard.value(forKey: OSXChatGoogleSearchKEY) as? String
return key ?? ""
}()
private lazy var googleSearchEngineID : String = {
lazy var googleSearchEngineID : String = {
let key = UserDefaults.standard.value(forKey: OSXChatGoogleEngineKEY) as? String
return key ?? ""
}()
@@ -402,6 +406,16 @@ extension ChatGPTManager {
// }
// }
// }
/*
Web search results:
{web_results}
Current date: {current_date}
Instructions: Using the provided web search results, write a comprehensive reply to the given query. Make sure to cite results using [[number](URL)] notation after the reference. If the provided search results refer to multiple subjects with the same name, write separate answers for each subject.
Query: {query}
Reply in
*/
///
func askChatGPT(messages: [Message], complete:(([String: Any]?, String?) -> ())?) {
@@ -448,58 +462,19 @@ extension ChatGPTManager {
// MARK: - search
extension ChatGPTManager {
func search(_ text: String, callback:@escaping (_ searchResult: GoogleSearchResult?, _ err: String?) -> Void) {
httpClient.googleSearch(text, cx: googleSearchEngineID, key: googleSearchApiKey) { [weak self] result, error in
guard let model = result else {
callback(nil, "error")
return
}
var temp : [GoogleSearch] = []
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInitiated)
model.items.forEach { item in
if let link = item.link {
group.enter()
self?.fetchHTML(link, callback: { content, err in
if let result = content {
}
group.leave()
})
}
}
group.notify(queue: DispatchQueue.main) {
func search(search: GoogleSearch?, callback:@escaping (_ searchResult: GoogleSearchResponse?, _ err: String?) -> Void) {
if let url = search?.url {
httpClient.googleSearch(url) { result, error in
callback(result,error)
}
}else {
callback(nil,"error")
}
}
private func fetchHTML(_ link: String, callback: @escaping (_ content: String?, _ err: String?) -> Void) {
func fetchHTML(_ link: String, callback: @escaping (_ content: String?, _ err: String?) -> Void) {
httpClient.googleSearchFetchHTML(link) { result, err in
if let html = result {
do {
let doc = try SwiftSoup.parse(html)
let pTags = try doc.select("p")
var array: [String] = []
for p in pTags {
let text = try p.text()
if !array.contains(text) && !text.isEmpty {
array.append(text)
}
}
var string: String = ""
array.forEach { str in
string.append(str)
}
callback(string, nil)
print("string:\(string)")
} catch Exception.Error(let type, let message) {
callback(nil, "error")
} catch {
callback(nil, "error")
}
}
callback(result, err)
}
}
}

View File

@@ -9,6 +9,7 @@ import Foundation
import SwiftUI
import CoreData
import Splash
import SwiftSoup
@@ -32,6 +33,13 @@ import Splash
@Environment(\.colorScheme) private var colorScheme
private let searchQueue = DispatchQueue(label: "google.search.queue")
private let semaphore = DispatchSemaphore(value: 1)
private var searchResult: GoogleSearchResponse?
private var searchIndex: Int = 0
private var searchResultMaxCount: Int16 = 0
var theme: Splash.Theme {
switch colorScheme {
case .dark:
@@ -268,6 +276,123 @@ extension ViewModel {
}
}
func addNewMessage(sesstionId: String, text: String, role: String, prompt: String?, updateBlock: @escaping(() -> ())) {
if sesstionId.isEmpty {
return
}
if ChatGPTManager.shared.chatGPTSpeaking {
return
}
let msg = Message(context: CoreDataManager.shared.container.viewContext)
msg.sesstionId = sesstionId
msg.text = text
msg.role = role
msg.id = UUID()
msg.createdDate = Date()
messages.append(msg)
CoreDataManager.shared.saveData()
updateConversation(sesstionId: sesstionId, message:messages.last)
self.scrollID = msg.id!
self.changeMsgText = text
print("发送提问:\(text)")
var sendMsgs = messages
sendMsgs.removeAll(where: {$0.msgType == .waitingReply})
if let search = currentConversation?.search, search.open, !search.searched {
//
search.search = text
let result = search.result?.mutableCopy() as? NSMutableOrderedSet
result?.removeAllObjects()
self.addGptThinkMessage(sesstionId: sesstionId)
if let lastId = self.messages.last?.id {
self.scrollID = lastId
}
self.changeMsgText = ""
self.search(search: search) { [weak self] searchResult, err in
guard let self = self else { return }
if let result = searchResult {
if let index = self.messages.firstIndex(where: { $0.msgType == .searching}) {
let message = self.messages[index]
message.text = self.createSearchText(result)
self.messages[index] = message
self.updateConversation(sesstionId: sesstionId, search: result)
if self.currentConversation?.sesstionId == sesstionId {
self.scrollID = self.messages.last!.id!
self.changeMsgText = ""//
}
}else {
self.removeGptThinkMessage()
let newMsg = Message(context: CoreDataManager.shared.container.viewContext)
newMsg.sesstionId = sesstionId
newMsg.role = ChatGPTManager.shared.gptRoleString
newMsg.id = UUID()
newMsg.createdDate = Date()
newMsg.msgTextType = .text
newMsg.msgType = .searching
newMsg.text = self.createSearchText(result)
if self.currentConversation?.sesstionId == sesstionId {
self.messages.append(newMsg)
self.scrollID = self.messages.last!.id!
self.changeMsgText = ""//
}
self.addGptThinkMessage(sesstionId: sesstionId)
CoreDataManager.shared.saveData()
self.updateConversation(sesstionId: sesstionId, search: result)
}
}else {
self.removeGptThinkMessage()
var searchText: String?
//
if let search = self.currentConversation?.search {
search.searched = true//
searchText = self.getSearchText(search)
self.updateConversation(sesstionId: sesstionId, search: search)
}
//
self.sendMessage(sesstionId: sesstionId, messages: sendMsgs, prompt: searchText, updateBlock: updateBlock)
}
}
}else {
var sendMsgs = messages
sendMsgs.removeAll(where: {$0.msgType == .waitingReply})
sendMessage(sesstionId: sesstionId, messages: sendMsgs, prompt: prompt, updateBlock: updateBlock)
}
}
private func createSearchText(_ search: GoogleSearch) -> String {
var text: String = "#### 搜索结果"
search.result?.array.forEach({ item in
if let result = item as? GoogleSearchResult {
text.append("\n\t")
text.append("标题: \(result.title ?? "") \n\t")
text.append("链接: \(result.link ?? "") \n\t")
text.append("内容: \(result.result ?? "") \n\t")
}
})
return text
}
private func getSearchText(_ search: GoogleSearch) -> String {
var text: String = "{"
search.result?.array.forEach({ item in
if let result = item as? GoogleSearchResult {
text.append("\(result.result ?? "")\n")
}
})
text.append("}")
return text
}
func addSearch(sesstionId: String, text: String, role: String, prompt: String?, updateBlock: @escaping(() -> ())) {
search(search: currentConversation?.search) { searchResult, err in
}
return
if sesstionId.isEmpty {
return
}
@@ -290,6 +415,8 @@ extension ViewModel {
sendMsgs.removeAll(where: {$0.msgType == .waitingReply})
sendMessage(sesstionId: sesstionId, messages: sendMsgs, prompt: prompt, updateBlock: updateBlock)
}
func cancel() {
ChatGPTManager.shared.stopResponse()
self.removeGptThinkMessage()
@@ -648,7 +775,156 @@ extension ViewModel {
}
}
// MARK: - search
extension ViewModel {
func cerateDefaultSearch() -> GoogleSearch {
let search = GoogleSearch(context: CoreDataManager.shared.container.viewContext)
search.id = UUID()
search.maxSearchResult = 3
search.dateType = .unlimited
return search
}
func updateConversation(sesstionId: String, search: GoogleSearch?) {
var con = fetchConversation(sesstionId: sesstionId)
if con == nil {
con = Conversation(context: CoreDataManager.shared.container.viewContext)
con?.sesstionId = sesstionId
con?.id = UUID()
}
con!.search = search
CoreDataManager.shared.saveData()
if let index = conversations.firstIndex(where: { $0.sesstionId == sesstionId}) {
conversations[index] = con!
} else {
conversations.insert(con!, at: 0)
}
}
}
// MARK: - search
extension ViewModel {
func search(search: GoogleSearch?, callback:@escaping (_ searchResult: GoogleSearch?, _ err: String?) -> Void) {
guard let searchModel = search else {
callback(nil, "error")
return
}
ChatGPTManager.shared.search(search: searchModel) { [weak self] searchResult, err in
guard let result = searchResult else {
DispatchQueue.main.async {
callback(nil, "error")
}
return
}
if err != nil {
DispatchQueue.main.async {
callback(nil, "error")
}
return
}
self?.searchResult = result
self?.searchIndex = 0
self?.searchResultMaxCount = searchModel.maxSearchResult
self?.fetchHTML(callback: { content, item, err in
if let result = content {
let model = GoogleSearchResult(context: CoreDataManager.shared.container.viewContext)
model.result = result
model.link = item?.link
model.snippet = item?.snippet
model.title = item?.title
searchModel.addToResult(model)
CoreDataManager.shared.saveData()
DispatchQueue.main.async {
callback(searchModel, nil)
}
}else {
DispatchQueue.main.async {
callback(nil, "error")
}
}
})
}
}
private func fetchHTML(callback: @escaping (_ content: String?, _ item: GoogleSearchItem?, _ err: String?) -> Void) {
guard let result = self.searchResult else {
print("Not searchResult")
callback(nil, nil, "error")
return
}
if result.items.count <= self.searchIndex || self.searchIndex >= self.searchResultMaxCount {
print("exceed max count")
callback(nil, nil, "error")
return
}
let item = result.items[self.searchIndex]
searchQueue.async {
self.semaphore.wait()
self.fetchHTML(item.link) { [weak self] content, err in
self?.semaphore.signal()
guard let self = self else {
print("htmlError Not self")
return
}
guard let html = content else {
print("htmlError Not html")
callback(nil, item, "error")
self.searchIndex += 1
self.fetchHTML(callback: callback)
return
}
// print("+++html:\(html)")
// print("+++htmlNED")
guard let doc = try? SwiftSoup.parse(html), let pTags = try? doc.select("p:lt(2)") else {
self.searchIndex += 1
print("htmlError Not parse")
callback(nil, item, "error")
self.fetchHTML(callback: callback)
return
}
var pArray: [String] = []
for p in pTags {
if let text = try? p.text(), !text.isEmpty {
print("+++htmlP:\(text)")
if !pArray.contains(text) {
pArray.append(text)
}
}
}
var string: String = ""
pArray.forEach { p in
string.append(p)
}
print("htmlString:\(string)")
callback(string, item, nil)
self.searchIndex += 1
self.fetchHTML(callback: callback)
}
}
}
private func fetchHTML(_ link: String?, callback: @escaping (_ content: String?, _ err: String?) -> Void) {
guard let lin = link, let ur = URL(string: lin) else {
callback(nil, "error")
return
}
print("aaaaaaaURL:\(lin)")
URLSession.shared.dataTask(with: ur) { (data, response, error) in
if let error = error {
print("error: \(error.localizedDescription)")
callback(nil, error.localizedDescription)
return
}
if let response = response as? HTTPURLResponse {
print("statusCode: \(response.statusCode)")
}
if let da = data, let html = String(data: da, encoding: .utf8) {
callback(html, nil)
}else {
callback(nil, "error")
}
}.resume()
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -2,14 +2,63 @@
// GoogleSearch+CoreDataClass.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/4/26.
// Created by CoderChan on 2023/4/28.
//
//
import Foundation
import CoreData
enum GoogleSearchDate: Int16 {
case unlimited = 0 //
case oneWeak //
case oneMonth //
case sixMonth //
case oneYears //
var value: String {
switch self {
case .unlimited:
return ""
case .oneWeak:
return "w1"
case .oneMonth:
return "m1"
case .sixMonth:
return "m6"
case .oneYears:
return "y1"
}
}
}
public class GoogleSearch: NSManagedObject {
var dateType: GoogleSearchDate {
get {
return GoogleSearchDate(rawValue: dateRestrict) ?? .unlimited
}
set {
dateRestrict = newValue.rawValue
}
}
var baseUrl: String = "https://customsearch.googleapis.com/customsearch/v1"
var url: String? {
var newUrl = baseUrl
if ChatGPTManager.shared.googleSearchEngineID.isEmpty {
return nil
}
newUrl += "?cx=\(ChatGPTManager.shared.googleSearchEngineID)"
guard let searchText = search else { return nil }
newUrl += "&q=\(searchText)"
newUrl += "&dateRestrict=\(dateType.value)"
if ChatGPTManager.shared.googleSearchApiKey.isEmpty {
return nil
}
newUrl += "&key=\(ChatGPTManager.shared.googleSearchApiKey)"
let allowedCharacters = CharacterSet.urlQueryAllowed
guard let encodedStr = newUrl.addingPercentEncoding(withAllowedCharacters: allowedCharacters) else { return nil}
return encodedStr
}
}

View File

@@ -2,7 +2,7 @@
// GoogleSearch+CoreDataProperties.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/4/26.
// Created by CoderChan on 2023/4/28.
//
//
@@ -16,12 +16,48 @@ extension GoogleSearch {
return NSFetchRequest<GoogleSearch>(entityName: "GoogleSearch")
}
@NSManaged public var maxSearchResult: Int16
@NSManaged public var id: UUID?
@NSManaged public var link: String?
@NSManaged public var title: String?
@NSManaged public var result: String?
@NSManaged public var snippet: String?
@NSManaged public var maxSearchResult: Int16
@NSManaged public var dateRestrict: Int16
@NSManaged public var result: NSOrderedSet?
@NSManaged public var search: String?
@NSManaged public var open: Bool
@NSManaged public var searched: Bool
}
// MARK: Generated accessors for result
extension GoogleSearch {
@objc(insertObject:inResultAtIndex:)
@NSManaged public func insertIntoResult(_ value: GoogleSearchResult, at idx: Int)
@objc(removeObjectFromResultAtIndex:)
@NSManaged public func removeFromResult(at idx: Int)
@objc(insertResult:atIndexes:)
@NSManaged public func insertIntoResult(_ values: [GoogleSearchResult], at indexes: NSIndexSet)
@objc(removeResultAtIndexes:)
@NSManaged public func removeFromResult(at indexes: NSIndexSet)
@objc(replaceObjectInResultAtIndex:withObject:)
@NSManaged public func replaceResult(at idx: Int, with value: GoogleSearchResult)
@objc(replaceResultAtIndexes:withResult:)
@NSManaged public func replaceResult(at indexes: NSIndexSet, with values: [GoogleSearchResult])
@objc(addResultObject:)
@NSManaged public func addToResult(_ value: GoogleSearchResult)
@objc(removeResultObject:)
@NSManaged public func removeFromResult(_ value: GoogleSearchResult)
@objc(addResult:)
@NSManaged public func addToResult(_ values: NSOrderedSet)
@objc(removeResult:)
@NSManaged public func removeFromResult(_ values: NSOrderedSet)
}

View File

@@ -0,0 +1,15 @@
//
// GoogleSearchResult+CoreDataClass.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/4/28.
//
//
import Foundation
import CoreData
public class GoogleSearchResult: NSManagedObject {
}

View File

@@ -0,0 +1,29 @@
//
// GoogleSearchResult+CoreDataProperties.swift
// OSXChatGPT
//
// Created by CoderChan on 2023/4/28.
//
//
import Foundation
import CoreData
extension GoogleSearchResult {
@nonobjc public class func fetchRequest() -> NSFetchRequest<GoogleSearchResult> {
return NSFetchRequest<GoogleSearchResult>(entityName: "GoogleSearchResult")
}
@NSManaged public var title: String?
@NSManaged public var snippet: String?
@NSManaged public var result: String?
@NSManaged public var link: String?
@NSManaged public var id: UUID?
}
extension GoogleSearchResult : Identifiable {
}

View File

@@ -13,6 +13,7 @@ enum MessageType: Int16 {
case normal = 0
case waitingReply = 1//
case fialMsg = 2//
case searching = 3//
}
enum MessageTextType: Int16 {

View File

@@ -40,7 +40,7 @@ struct HTTPResponseMessage: Decodable {
let role: String?
}
struct GoogleSearchResult: Decodable {
struct GoogleSearchResponse: Decodable {
let kind: String
let items: [GoogleSearchItem]
}
@@ -347,18 +347,12 @@ extension HTTPClient {
}
// MARK: - google search
extension HTTPClient {
func googleSearch(_ text: String, cx: String, key: String, callback:@escaping (_ searchResult: GoogleSearchResult?, _ err: String?) -> Void) {
let urlStr = "https://customsearch.googleapis.com/customsearch/v1?cx=\(cx)&q=\(text)&key=\(key)"
let allowedCharacters = CharacterSet.urlQueryAllowed
guard let encodedStr = urlStr.addingPercentEncoding(withAllowedCharacters: allowedCharacters) else {
callback(nil, "error")
return
}
let url = URL(string: encodedStr)!
func googleSearch(_ urlString: String, callback:@escaping (_ searchResult: GoogleSearchResponse?, _ err: String?) -> Void) {
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
print("error: \(error.localizedDescription)")
callback(nil, error.localizedDescription)
@@ -368,7 +362,7 @@ extension HTTPClient {
print("statusCode: \(response.statusCode)")
}
if let da = data,
let response = try? JSONDecoder().decode(GoogleSearchResult.self, from: da){
let response = try? JSONDecoder().decode(GoogleSearchResponse.self, from: da){
callback(response, nil)
}else {
callback(nil, "error")
@@ -377,7 +371,11 @@ extension HTTPClient {
task.resume()
}
func googleSearchFetchHTML(_ url: String, callback:@escaping (_ result: String?, _ err: String?) -> Void) {
let ur = URL(string: url)!
guard let ur = URL(string: url) else {
callback(nil, "error")
return
}
print("aaaaaaaURL:\(url)")
let task = URLSession.shared.dataTask(with: ur) { (data, response, error) in
if let error = error {
print("error: \(error.localizedDescription)")

View File

@@ -17,9 +17,17 @@
<relationship name="search" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GoogleSearch"/>
</entity>
<entity name="GoogleSearch" representedClassName=".GoogleSearch" syncable="YES">
<attribute name="dateRestrict" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="maxSearchResult" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="open" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="search" optional="YES" attributeType="String"/>
<attribute name="searched" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="result" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="GoogleSearchResult"/>
</entity>
<entity name="GoogleSearchResult" representedClassName=".GoogleSearchResult" syncable="YES">
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="link" optional="YES" attributeType="String"/>
<attribute name="maxSearchResult" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="result" optional="YES" attributeType="String"/>
<attribute name="snippet" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>

View File

@@ -83,6 +83,17 @@ struct ChatRoomInputView: View {
return
}
}
// if let search = viewModel.currentConversation?.search, search.open, !search.searched {
// //
// search.search = msg
// let result = search.result?.mutableCopy() as? NSMutableOrderedSet
// result?.removeAllObjects()
// CoreDataManager.shared.saveData()
// viewModel.search(search: search) { searchResult, err in
//
// }
// }else {
// }
viewModel.addNewMessage(sesstionId: viewModel.currentConversation?.sesstionId ?? "", text: msg, role: "user", prompt: viewModel.currentConversation?.prompt?.prompt) {
}

View File

@@ -12,6 +12,7 @@ struct ChatRoomToolBar: View {
@State private var showInputView = false
@State private var showDragView = false
@State private var showSearchView = false
@State private var showSearchSettingView = false
@State private var temperature: String = ""
@State private var model: String = ""
@State private var context: String = ""
@@ -71,17 +72,19 @@ struct ChatRoomToolBar: View {
Button {
showSearchView.toggle()
ChatGPTManager.shared.search("中国新冠开") { searchResult, err in
}
// ChatGPTManager.shared.search("") { searchResult, err in
//
// }
} label: {
Text("谷歌搜索")
}
.popover(isPresented: $showSearchView) {
GoogleSearchPopView(showSearchView: $showSearchView).environmentObject(viewModel)
GoogleSearchPopView(showSearchView: $showSearchView, showSearchSettingView: $showSearchSettingView).environmentObject(viewModel)
}
.popover(isPresented: $showSearchSettingView) {
GoogleSearchSettingView().environmentObject(viewModel)
}
Spacer()
if viewModel.showStopAnswerBtn {

View File

@@ -11,6 +11,7 @@ struct GoogleSearchPopView: View {
@Binding var showSearchView: Bool
@State private var progress: Float = 0.5
@EnvironmentObject var viewModel: ViewModel
@Binding var showSearchSettingView : Bool
@State private var isOn = false
var body: some View {
VStack(alignment: .center) {
@@ -26,48 +27,127 @@ struct GoogleSearchPopView: View {
if isOn {
GoogleSearchCountSliderView()
Divider()
Button {
withAnimation {
showSearchView.toggle()
viewModel.currentConversation = nil
viewModel.showUserInitialize = true
}
} label: {
HStack {
Text("谷歌APIKey")
Text("未配置")
}
}.buttonStyle(PlainButtonStyle())
.padding(EdgeInsets(top: 5, leading: 16, bottom: 5, trailing: 16))
Divider()
Button {
withAnimation {
showSearchView.toggle()
viewModel.currentConversation = nil
viewModel.showUserInitialize = true
}
} label: {
HStack {
Text("搜索引擎ID")
Text("未配置")
}
}.buttonStyle(PlainButtonStyle())
.padding(EdgeInsets(top: 5, leading: 16, bottom: 5, trailing: 16))
GoogleSearchTimeSliderView()
}else {
Text("1、搜索需要配置APIKey与搜索引擎ID")
.lineLimit(3)
.padding(EdgeInsets(top: 15, leading: 16, bottom: 5, trailing: 16))
Text("2、搜索结果超出最大令牌后会自动截取")
.lineLimit(3)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 15, trailing: 16))
HStack {
Text("谷歌APIKey")
Button {
withAnimation {
showSearchView.toggle()
showSearchSettingView = true
}
} label: {
HStack {
if ChatGPTManager.shared.getGoogleSearchMaskApiKey().isEmpty {
Text("[未配置]")
.foregroundColor(.red)
}else {
Text("[更新key]").foregroundColor(.green)
}
}
}.buttonStyle(PlainButtonStyle())
}
.frame(height: 45)
Divider()
HStack {
Text("搜索引擎ID")
Button {
withAnimation {
showSearchView.toggle()
showSearchSettingView = true
}
} label: {
HStack {
if ChatGPTManager.shared.getGoogleSearchEngineMaskApiKey().isEmpty {
Text("[未配置]")
.foregroundColor(.red)
}else {
Text("[更新ID]")
.foregroundColor(.green)
}
}
}.buttonStyle(PlainButtonStyle())
}
.frame(height: 45)
}
Spacer()
}
.frame(width: 160, height: 200)
.onChange(of: isOn) { newValue in
var search = viewModel.currentConversation?.search
if search == nil {
search = viewModel.cerateDefaultSearch()
}
search?.open = newValue
viewModel.currentConversation?.search = search
viewModel.updateConversation(sesstionId: viewModel.currentConversation!.sesstionId, search: search)
}
.onAppear {
if let search = viewModel.currentConversation?.search, search.open {
self.isOn = true
}else {
self.isOn = false
}
}
}
}
struct GoogleSearchTimeSliderView: View {
@State private var progress: Float = 0
@State private var change: String = "无限"
@EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack {
HStack {
Text("时间筛选")
.padding(0)
Text("(\(change))")
.padding(0)
.frame(width: 40)
}.padding(0)
Slider(value: $progress, in: 1...5, step: 1) {
}
.frame(height: 10)
.padding(.leading, 16)
.padding(.trailing, 16)
.padding(.top, 0)
.padding(.bottom, 0)
}
.frame(height: 45)
.onChange(of: progress) { newValue in
let formattedValue = String(format: "%.0f", newValue)
change = formattedValue
if newValue == 2 {
change = "1周"
}else if newValue == 3 {
change = "1月"
}else if newValue == 4 {
change = "6月"
}else if newValue == 5 {
change = "1年"
}else {
change = "无限"
}
}
.onAppear {
if let search = viewModel.currentConversation?.search {
progress = Float(search.dateType.rawValue)
}
}
.onDisappear {
let search = viewModel.currentConversation?.search
search?.dateType = GoogleSearchDate(rawValue: Int16(progress)) ?? .unlimited
viewModel.currentConversation?.search = search
viewModel.updateConversation(sesstionId: viewModel.currentConversation!.sesstionId, search: search)
}
}
}
struct GoogleSearchCountSliderView: View {
@State private var progress: Float = 3
@@ -76,52 +156,36 @@ struct GoogleSearchCountSliderView: View {
var body: some View {
VStack {
HStack {
Text("获取搜索结果数")
Text(change)
.frame(width: 10)
Text("搜索结果数")
Text("(\(change))")
.frame(width: 26)
}.padding(0)
// .background(.orange)
Slider(value: $progress, in: 1...10, step: 1) {
// Text(change)
}
.frame(height: 10)
.padding(.leading, 16)
.padding(.trailing, 16)
.padding(.top, 0)
.padding(.bottom, 0)
// .background(.green)
// Slider(value: $progress) {
// Text(change)
// }
// Slider(value: $progress) {
//
// } minimumValueLabel: {
// Text("0.0")
// } maximumValueLabel: {
// Text("2.0")
// }.padding(.horizontal)
// .frame(width: 160)
// .background(.orange)
}
.padding(.top, 0)
.padding(.bottom, 0)
.frame(height: 45)
.onChange(of: progress) { newValue in
let formattedValue = String(format: "%.0f", newValue)
change = formattedValue
}
.onAppear {
// if let gpt = viewModel.currentConversation?.chatGPT {
// progress = Float(gpt.temperature / 2)
// let formattedValue = String(format: "%.1f", progress * 2)
// change = formattedValue
// }
if let search = viewModel.currentConversation?.search {
progress = Float(search.maxSearchResult)
}
}
.onDisappear {
// viewModel.currentConversation?.chatGPT?.temperature = Double(progress * 2)
// viewModel.updateConversation(sesstion: viewModel.currentConversation)
// temperature = change
let search = viewModel.currentConversation?.search
search?.maxSearchResult = Int16(progress)
viewModel.currentConversation?.search = search
viewModel.updateConversation(sesstionId: viewModel.currentConversation!.sesstionId, search: search)
}
}
}