diff --git a/Selected.xcodeproj/project.pbxproj b/Selected.xcodeproj/project.pbxproj index 5605ea8..bac7670 100644 --- a/Selected.xcodeproj/project.pbxproj +++ b/Selected.xcodeproj/project.pbxproj @@ -65,7 +65,6 @@ 38CC18B62B9C7B7C0023DF18 /* PopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38CC18B52B9C7B7C0023DF18 /* PopBarView.swift */; }; 38CC554C2BA9B6E900069553 /* RunCommandAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38CC554B2BA9B6E900069553 /* RunCommandAction.swift */; }; 38DEBE322B9D67F800CD2A35 /* OpenAI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DEBE312B9D67F800CD2A35 /* OpenAI.swift */; }; - 38DEBE342B9DD7CA00CD2A35 /* Gemini.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DEBE332B9DD7CA00CD2A35 /* Gemini.swift */; }; 38DEBE372B9DD80300CD2A35 /* GoogleGenerativeAI in Frameworks */ = {isa = PBXBuildFile; productRef = 38DEBE362B9DD80300CD2A35 /* GoogleGenerativeAI */; }; 38DEBE392B9DDD0500CD2A35 /* AI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DEBE382B9DDD0500CD2A35 /* AI.swift */; }; 38DF1D4A2BB7A3BC0063A879 /* Option.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38DF1D492BB7A3BC0063A879 /* Option.swift */; }; @@ -168,7 +167,6 @@ 38CC18B52B9C7B7C0023DF18 /* PopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopBarView.swift; sourceTree = ""; }; 38CC554B2BA9B6E900069553 /* RunCommandAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunCommandAction.swift; sourceTree = ""; }; 38DEBE312B9D67F800CD2A35 /* OpenAI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAI.swift; sourceTree = ""; }; - 38DEBE332B9DD7CA00CD2A35 /* Gemini.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Gemini.swift; sourceTree = ""; }; 38DEBE382B9DDD0500CD2A35 /* AI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AI.swift; sourceTree = ""; }; 38DF1D492BB7A3BC0063A879 /* Option.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Option.swift; sourceTree = ""; }; 38DF1D4C2BB94DCD0063A879 /* ApplicationSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationSettingView.swift; sourceTree = ""; }; @@ -380,7 +378,6 @@ isa = PBXGroup; children = ( 38DEBE312B9D67F800CD2A35 /* OpenAI.swift */, - 38DEBE332B9DD7CA00CD2A35 /* Gemini.swift */, 38DEBE382B9DDD0500CD2A35 /* AI.swift */, 38230CA22BA1DBA8002A52E9 /* Utils.swift */, 38E20A4A2BB5A7E20035A3AA /* TTS.swift */, @@ -602,7 +599,6 @@ 3860106F2C425CD600EB9AE7 /* Claude.swift in Sources */, 3860105A2C2FA07600EB9AE7 /* MessageViewModel.swift in Sources */, 386010682C3AE24000EB9AE7 /* Template.swift in Sources */, - 38DEBE342B9DD7CA00CD2A35 /* Gemini.swift in Sources */, 3888D8032BA8615900A7B75D /* IconImage.swift in Sources */, 38049D632B9DF8EA00E02ECA /* FloatingPanel.swift in Sources */, 386AD74E2B8F7A16008C346D /* MenuItemView.swift in Sources */, diff --git a/Selected/Service/AI.swift b/Selected/Service/AI.swift index 3be1b8b..00e790b 100644 --- a/Selected/Service/AI.swift +++ b/Selected/Service/AI.swift @@ -56,12 +56,6 @@ struct Translation { let OpenAITrans2Chinese = OpenAIPrompt(prompt:"你是一位精通简体中文的专业翻译。翻译指定的内容到中文。规则:请直接回复翻译后的内容。内容为:{selected.text}", model: Defaults[.openAITranslationModel]) await OpenAITrans2Chinese.chatOne(selectedText: content, completion: completion) } - case "Gemini": - if isWord(str: content) { - await GeminiWordTrans.chatOne(selectedText: content, completion: completion) - } else { - await GeminiTrans2Chinese.chatOne(selectedText: content, completion: completion) - } case "Claude": if isWord(str: content) { await ClaudeWordTrans.chatOne(selectedText: content, completion: completion) @@ -78,8 +72,6 @@ struct Translation { case "OpenAI": let OpenAITrans2English = OpenAIPrompt(prompt:"You are a professional translator proficient in English. Translate the following content into English. Rule: reply with the translated content directly. The content is:{selected.text}", model: Defaults[.openAITranslationModel]) await OpenAITrans2English.chatOne(selectedText: content, completion: completion) - case "Gemini": - await GeminiTrans2English.chatOne(selectedText: content, completion: completion) case "Claude": await ClaudeTrans2English.chatOne(selectedText: content, completion: completion) default: @@ -99,8 +91,6 @@ struct ChatService: AIChatService{ switch Defaults[.aiService] { case "OpenAI": chatService = OpenAIService(prompt: prompt, options: options) - case "Gemini": - chatService = GeminiPrompt(prompt: prompt, options: options) case "Claude": chatService = ClaudeService(prompt: prompt, options: options) default: @@ -241,3 +231,5 @@ func openSVGInBrowser(svgData: String) -> Bool { return false } } + +let MAX_CHAT_ROUNDS = 20 diff --git a/Selected/Service/Claude.swift b/Selected/Service/Claude.swift index 6928765..e2b8e70 100644 --- a/Selected/Service/Claude.swift +++ b/Selected/Service/Claude.swift @@ -107,7 +107,7 @@ class ClaudeService: AIChatService{ completion(newIndex, message) return } - if newIndex-index >= 10 { + if newIndex-index >= MAX_CHAT_ROUNDS { newIndex += 1 let localMsg = NSLocalizedString("Too much rounds, please start a new chat", comment: "system info") let message = ResponseMessage(message: localMsg, role: .system, new: true, status:.failure) @@ -133,7 +133,7 @@ class ClaudeService: AIChatService{ completion(index, message) return } - if index >= 10 { + if index >= MAX_CHAT_ROUNDS { index += 1 let localMsg = NSLocalizedString("Too much rounds, please start a new chat", comment: "system info") let message = ResponseMessage(message: localMsg, role: .system, new: true, status:.failure) diff --git a/Selected/Service/Gemini.swift b/Selected/Service/Gemini.swift deleted file mode 100644 index 06ca949..0000000 --- a/Selected/Service/Gemini.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Gemini.swift -// Selected -// -// Created by sake on 2024/3/10. -// - -import SwiftUI -import Defaults -import GoogleGenerativeAI - -struct GeminiPrompt: AIChatService{ - let prompt: String - let options: [String:String] - - init(prompt: String, options: [String:String] = [String:String]()) { - self.prompt = prompt - self.options = options - } - - func chat( - ctx: ChatContext, - completion: @escaping (_ : Int, _: ResponseMessage) -> Void) async -> Void { - - let model = GenerativeModel(name: "gemini-1.5-pro-latest", apiKey: Defaults[.geminiAPIKey], requestOptions: RequestOptions(apiVersion: "v1beta") ) - var message = renderChatContent(content: prompt, chatCtx: ctx, options: options) - message = replaceOptions(content: message, selectedText: ctx.text, options: options) - - let contentStream = model.generateContentStream(message) - var resp = "" - completion(0, ResponseMessage(message: NSLocalizedString("waiting", comment: "system info"), role: .system, new: true, status: .initial)) - do { - for try await chunk in contentStream { - if let text = chunk.text { - NSLog(text) - completion(0, ResponseMessage(message: text, role: .assistant, new: resp == "" ,status: .updating)) - resp += text - } - } - } catch { - NSLog("Unexpected error: \(error).") - return - } - completion(0, ResponseMessage(message: "", role: .assistant, new: false, status: .finished)) - } - - func chatOne( - selectedText: String, - completion: @escaping (_: String) -> Void) async -> Void { - - let model = GenerativeModel(name: "gemini-1.5-pro-latest", apiKey: Defaults[.geminiAPIKey], requestOptions: RequestOptions(apiVersion: "v1beta") ) - let message = replaceOptions(content: prompt, selectedText: selectedText, options: options) - - let contentStream = model.generateContentStream(message) - do { - for try await chunk in contentStream { - if let text = chunk.text { - NSLog(text) - completion(text) - } - } - } catch { - NSLog("Unexpected error: \(error).") - } - } - - func chatFollow( - index: Int, - userMessage: String, - completion: @escaping (_: Int, _: ResponseMessage) -> Void) async -> Void { - } -} - -let GeminiWordTrans = GeminiPrompt(prompt: "翻译以下单词到中文,详细说明单词的不同意思,并且给出原语言的例句与翻译。使用 markdown 的格式回复,要求第一行标题为单词。单词为:{selected.text}") - -let GeminiTrans2Chinese = GeminiPrompt(prompt:"你是一位精通简体中文的专业翻译。翻译指定的内容到中文。规则: 请直接回复翻译后的内容。内容为:{selected.text}") - -let GeminiTrans2English = GeminiPrompt(prompt:"You are a professional translator proficient in English. Translate the following content into English. Rule: reply with the translated content directly. The content is:{selected.text}") diff --git a/Selected/Service/OpenAI.swift b/Selected/Service/OpenAI.swift index fcae4ca..95d7c8f 100644 --- a/Selected/Service/OpenAI.swift +++ b/Selected/Service/OpenAI.swift @@ -199,7 +199,7 @@ struct OpenAIPrompt { completion(index, message) return } - if index >= 10 { + if index >= MAX_CHAT_ROUNDS { index += 1 let localMsg = NSLocalizedString("Too much rounds, please start a new chat", comment: "system info") let message = ResponseMessage(message: localMsg, role: .system, new: true, status: .failure) @@ -225,7 +225,7 @@ struct OpenAIPrompt { completion(newIndex, message) return } - if newIndex-index >= 10 { + if newIndex-index >= MAX_CHAT_ROUNDS { newIndex += 1 let localMsg = NSLocalizedString("Too much rounds, please start a new chat", comment: "system info") let message = ResponseMessage(message: localMsg, role: .system, new: true, status: .failure) diff --git a/Selected/View/ClipView/ClipView.swift b/Selected/View/ClipView/ClipView.swift index 4a1e5ab..153ecc3 100644 --- a/Selected/View/ClipView/ClipView.swift +++ b/Selected/View/ClipView/ClipView.swift @@ -11,7 +11,7 @@ import PDFKit struct ClipDataView: View { var data: ClipHistoryData - + var body: some View { VStack(alignment: .leading){ let item = data.getItems().first! @@ -31,17 +31,17 @@ struct ClipDataView: View { } else if data.plainText != nil { TextView(text: data.plainText!) } - + Spacer() Divider() - + HStack { Text("Application:") Spacer() getIcon(data.application!) Text(getAppName(data.application!)) }.frame(height: 17) - + HStack { Text("Content type:") Spacer() @@ -52,13 +52,13 @@ struct ClipDataView: View { Text(NSLocalizedString(str, comment: "")) } }.frame(height: 17) - + HStack { Text("Date:") Spacer() Text("\(format(data.firstCopiedAt!))") }.frame(height: 17) - + if data.numberOfCopies > 1 { HStack { Text("Last copied:") @@ -71,7 +71,7 @@ struct ClipDataView: View { Text("\(data.numberOfCopies) times") }.frame(height: 17) } - + if let url = data.url { if type == .fileURL { let url = URL(string: String(decoding: item.data!, as: UTF8.self))! @@ -92,14 +92,18 @@ struct ClipDataView: View { } }.padding().frame(width: 550) } - + private func getAppName(_ bundleID: String) -> String { - let bundleURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)! + guard let bundleURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else { + return "Unknown" + } return FileManager.default.displayName(atPath: bundleURL.path) } - + private func getIcon(_ bundleID: String) -> some View { - let bundleURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID)! + guard let bundleURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else{ + return AnyView(EmptyView()) + } return AnyView( Image(nsImage: NSWorkspace.shared.icon(forFile: bundleURL.path)).resizable().aspectRatio(contentMode: .fit).frame(width: 15, height: 15) ) @@ -117,11 +121,11 @@ func isValidHttpUrl(_ string: String) -> Bool { guard let url = URL(string: string) else { return false } - + guard let scheme = url.scheme, scheme == "http" || scheme == "https" else { return false } - + return url.host != nil } @@ -135,14 +139,14 @@ class ClipViewModel: ObservableObject { struct ClipView: View { @Environment(\.managedObjectContext) private var viewContext - + // 维护一个 FetchRequest 实例 @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \ClipHistoryData.lastCopiedAt, ascending: false)], animation: .default) private var clips: FetchedResults - - + + // 默认选择第一条,必须同时设置 List 和 NavigationLink 的 selection // @State var selected : ClipData? @ObservedObject var viewModel = ClipViewModel.shared @@ -217,12 +221,12 @@ struct ClipView: View { } }.frame(width: 800, height: 400) } - + func delete(_ clipData: ClipHistoryData) { if let selectedItem = viewModel.selectedItem { let selectedItemIdx = clips.firstIndex(of: selectedItem)! let idx = clips.firstIndex(of: clipData)! - + // 计算删除后,需要选中的新条目的索引 let newIndexAfterDeletion: Int? if selectedItem == clipData { @@ -238,9 +242,9 @@ struct ClipView: View { } else { newIndexAfterDeletion = selectedItemIdx } - + PersistenceController.shared.delete(item: clipData) - + // 在删除后更新选中项 DispatchQueue.main.async { if let newIndex = newIndexAfterDeletion, clips.indices.contains(newIndex) { diff --git a/Selected/View/SettingsView.swift b/Selected/View/SettingsView.swift index 46d6dcb..cb4c645 100644 --- a/Selected/View/SettingsView.swift +++ b/Selected/View/SettingsView.swift @@ -18,7 +18,7 @@ struct SettingsView: View { @Environment(\.colorScheme) var colorScheme @Default(.aiService) var aiService - let aiServicePickerValues = ["OpenAI", "Claude", "Gemini"] + let aiServicePickerValues = ["OpenAI", "Claude"] @Default(.openAIAPIKey) var openAIAPIKey