diff --git a/README.md b/README.md index 7ed9667..7b47c78 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ OpenAI ChatGPT app for iOS, iPadOS, macoS #### 更新说明 -最新版本 v2.3 -- Create new conversation. (创建新的对话) -- Save conversation records. (保存对话记录) -- Switch to historical conversations. (切换历史对话) -- Scroll to the top of the conversation list. (滚动到对话列表的顶部) -- Auto-scroll to the bottom of the conversation list. (自动滚动到对话列表的底部) -- Option to request conversations without historical records (click on the icon on the left side of the input box to toggle). (请求时可不带历史对话记录) -- Support for additional languages (Traditional Chinese, Korean, Japanese, French, German, Russian, etc.). (支持更多语言(繁体中文、韩文、日文、法语、德语、俄语等)) +最新版本 v2.5 + +- Added chat room settings with Prompt and Temperature parameter configuration. (新增聊天室设置功能,支持 Prompt 和 Temperature 参数配置。) +- Display current conversation identifier in the history list. (历史对话列表增加显示当前对话标识。) +- Fixed potential crash when sending conversations. (修复发送对话时可能会崩溃的问题。) +- Removed restrictions on creating new conversations and switching history when requesting a conversation. (请求对话时,取消创建新对话和切换历史对话的禁用限制。) +- Improved multi-language translations for sharing feature. (完善分享功能的多语言翻译。) + **支持功能** @@ -101,6 +101,19 @@ TestFlight 下载地址:[https://testflight.apple.com/join/GR4BOt2M](https://t #### 3.4 历史更新功能 +v2.4: +- Add sharing function(增加分享功能) +- Fix the history list is too high(修复历史列表过高) + +v2.3: +- Create new conversation. (创建新的对话) +- Save conversation records. (保存对话记录) +- Switch to historical conversations. (切换历史对话) +- Scroll to the top of the conversation list. (滚动到对话列表的顶部) +- Auto-scroll to the bottom of the conversation list. (自动滚动到对话列表的底部) +- Option to request conversations without historical records (click on the icon on the left side of the input box to toggle). (请求时可不带历史对话记录) +- Support for additional languages (Traditional Chinese, Korean, Japanese, French, German, Russian, etc.). (支持更多语言(繁体中文、韩文、日文、法语、德语、俄语等)) + v2.2: - Increased request timeout from 30 seconds to 60 seconds.(请求超时从 30 秒增加到 60 秒。) - When sending dialog context, only send the first three Q&A rounds, and submit only the first 100 characters of the answer.(发送对话上下文时,只发送提问的前三轮问答,且答案只提交前100个字。) diff --git a/iChatGPT.xcodeproj/project.pbxproj b/iChatGPT.xcodeproj/project.pbxproj index 645a045..f8ed5b7 100644 --- a/iChatGPT.xcodeproj/project.pbxproj +++ b/iChatGPT.xcodeproj/project.pbxproj @@ -569,7 +569,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2023.04.05; + CURRENT_PROJECT_VERSION = 2023.04.12; DEVELOPMENT_ASSET_PATHS = "\"iChatGPT/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -587,7 +587,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4; + MARKETING_VERSION = 2.5; PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.iChatGPT; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -609,7 +609,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2023.04.05; + CURRENT_PROJECT_VERSION = 2023.04.12; DEVELOPMENT_ASSET_PATHS = "\"iChatGPT/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -627,7 +627,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4; + MARKETING_VERSION = 2.5; PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.iChatGPT; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/iChatGPT/AIChatView.swift b/iChatGPT/AIChatView.swift index fa79939..d7f6769 100644 --- a/iChatGPT/AIChatView.swift +++ b/iChatGPT/AIChatView.swift @@ -47,7 +47,7 @@ struct AIChatView: View { }) } .sheet(isPresented: $inputModel.isConfigChatRoom) { - ChatRoomConfigView(isKeyPresented: $inputModel.isConfigChatRoom) + ChatRoomConfigView(isKeyPresented: $inputModel.isConfigChatRoom, chatModel: chatModel) } .sheet(isPresented: $isSharing) { ActivityView(activityItems: $shareContent.activityItems) @@ -192,12 +192,12 @@ extension AIChatView { } func ShareContents() -> Alert { - Alert(title: Text("Share"), - message: Text("Choose a sharing format"), - primaryButton: .default(Text("Image")) { + Alert(title: Text("Share".localized()), + message: Text("Choose a sharing format".localized()), + primaryButton: .default(Text("Image".localized())) { screenshotAndShare(isImage: true) }, - secondaryButton: .default(Text("PDF")) { + secondaryButton: .default(Text("PDF".localized())) { screenshotAndShare(isImage: false) } ) diff --git a/iChatGPT/ChatHistoryListView.swift b/iChatGPT/ChatHistoryListView.swift index b4b2c03..3d8044d 100644 --- a/iChatGPT/ChatHistoryListView.swift +++ b/iChatGPT/ChatHistoryListView.swift @@ -83,11 +83,21 @@ struct ChatHistoryListView: View { VStack(alignment: .leading) { HStack() { - Text(item.roomID.formatTimestamp()) + Text(item.roomName ?? item.roomID.formatTimestamp()) .font(.headline) Spacer() + if item.roomID == chatModel.roomID { + Text(" \("Current Chat".localized()) ") + .font(.footnote) + .foregroundColor(.white) + .padding([.top, .bottom], 3) + .padding([.leading, .trailing], 4) + .background(Color.red.opacity(0.8)) + .clipShape(Capsule()) + } + Text(" \(ChatMessageStore.shared.messages(forRoom: item.roomID).count) ") .font(.footnote) .foregroundColor(.white) diff --git a/iChatGPT/ChatInputView.swift b/iChatGPT/ChatInputView.swift index bc0ba79..6331107 100644 --- a/iChatGPT/ChatInputView.swift +++ b/iChatGPT/ChatInputView.swift @@ -30,7 +30,6 @@ struct ChatInputView: View { .padding(.trailing, 5) .foregroundColor(.lightGray) .buttonStyle(PlainButtonStyle()) - .disabled(!chatModel.contents.filter({ $0.isResponse == false }).isEmpty) if !chatModel.contents.isEmpty { Button(action: { @@ -72,7 +71,6 @@ struct ChatInputView: View { .padding(.trailing, 5) .foregroundColor(.lightGray) .buttonStyle(PlainButtonStyle()) - .disabled(!chatModel.contents.filter({ $0.isResponse == false }).isEmpty) Button(action: { model.isConfigChatRoom.toggle() @@ -82,7 +80,6 @@ struct ChatInputView: View { .padding(.trailing, 8) .foregroundColor(.lightGray) .buttonStyle(PlainButtonStyle()) - .disabled(!chatModel.contents.filter({ $0.isResponse == false }).isEmpty) if !chatModel.contents.isEmpty { Button(action: { diff --git a/iChatGPT/ChatRoomConfigView.swift b/iChatGPT/ChatRoomConfigView.swift index 7549079..ad64178 100644 --- a/iChatGPT/ChatRoomConfigView.swift +++ b/iChatGPT/ChatRoomConfigView.swift @@ -7,34 +7,156 @@ // import SwiftUI +import Combine +import SwiftUIX struct ChatRoomConfigView: View { @Binding var isKeyPresented: Bool + @StateObject var chatModel: AIChatModel + + @State var roomName: String = "" + @State var prompt: String = "" + @State var temperature: String = "" + @State var historyCount: String = "" + @State var model: String = "" + @State var isDirty: Bool = false + @State var showingAlert: Bool = false + @State var alertMessage: String = "" + + init(isKeyPresented: Binding, chatModel: AIChatModel) { + _isKeyPresented = isKeyPresented + _chatModel = StateObject(wrappedValue: chatModel) + + let room = ChatRoomStore.shared.chatRoom(chatModel.roomID) + _roomName = State(initialValue: room?.roomName ?? room?.roomID.formatTimestamp() ?? "") + _prompt = State(initialValue: room?.prompt ?? "") + _temperature = State(initialValue: "\(room?.temperature ?? 0.7)") + _historyCount = State(initialValue: "\(room?.historyCount ?? 0)") + _model = State(initialValue: room?.model ?? "") + _isDirty = State(initialValue: false) + } var body: some View { NavigationView { - VStack { - Text("Coming soon..".localized()).font(.title2) + List { + ConfigCellView(title: "Room Name".localized(), + subtitle: "", + value: $roomName, + description: "The name of the room".localized()) + ConfigCellView(title: "Prompt".localized(), subtitle: "Prompt description.".localized(), value: $prompt, description: "Prompt text to generate contextual information for the corresponding text.".localized()) + ConfigCellView(title: "Temperature".localized(), subtitle: "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.".localized(), value: $temperature, description: "The default temperature is 0.7".localized()) + ConfigCellView(title: "Chat History".localized(), subtitle: "How much context information is carried when sending a dialog.".localized(), value: $historyCount, description: "Default is the last 3 conversations.".localized()) + //ConfigCellView(title: "Model", subtitle: "ChatGPT 模型", value: $model, description: "The model used for processing") } - .navigationTitle("Room Settings".localized()) - .toolbar { - Button(action: onCloseButtonTapped) { - Image(systemName: "xmark.circle").imageScale(.large) - } + .navigationBarTitle(Text("Room Settings".localized())) + .navigationBarItems( + trailing: + HStack { + Button(action: onSaveButtonTapped, label: { + Text("Save".localized()).bold() + }).disabled(!isDirty) + + Button(action: onCloseButtonTapped) { + Image(systemName: "xmark.circle").imageScale(.large) + } + } + ) + .alert(isPresented: $showingAlert) { + ShowAlterView() } + .onChange(of: [roomName, prompt, temperature, historyCount, model]) { _ in + self.isDirty = true + } + .gesture( + TapGesture().onEnded { + hideKeyboard() + } + ) } } + private func onSaveButtonTapped() { + // 检查 temperature 数据格式是否符合 + guard let tempValue = Double(temperature), 0.0 <= tempValue && tempValue <= 2.0 else { + alertMessage = "Temperature is between 0 and 2.".localized() + showingAlert = true + return + } + + // 检查 historyCount 数据格式是否符合 + guard let histCountValue = Int(historyCount), histCountValue >= 0 else { + alertMessage = "History message count must be an integer greater than or equal to 0.".localized() + showingAlert = true + return + } + + let room = ChatRoom(roomID: chatModel.roomID, roomName: roomName, model: model, prompt: prompt.isEmpty ? nil : prompt, temperature: tempValue, historyCount: histCountValue) + ChatRoomStore.shared.updateChatRoom(for: chatModel.roomID, room: room) + self.isDirty = false + + alertMessage = "Settings have been updated~".localized() + showingAlert = true + } + + func ShowAlterView() -> Alert { + Alert( + title: Text("Tips".localized()), + message: Text(alertMessage), + dismissButton: .default(Text("OK".localized())) + ) + } + private func onCloseButtonTapped() { isKeyPresented = false } + + /// 当用户点击其他区域时隐藏软键盘 + private func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } } +struct ConfigCellView: View { + let title: String + let subtitle: String + @Binding var value: String + let description: String + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.headline) + .padding(.top, 10) + + if !subtitle.isEmpty { + Text(subtitle) + .font(.body) + .foregroundColor(.secondaryLabel) + .padding(.top, 0.5) + .padding(.bottom, 10) + .fixedSize(horizontal: false, vertical: true) + } + + TextView(description, text: $value, onEditingChanged: {_ in + + }, onCommit: { + + }) + .returnKeyType(.default) + .padding(10) + .maxHeight(90) + .border(.blue.opacity(0.8), cornerRadius: 10) + + Spacer() + .height(15) + } + } +} struct ChatRoomConfigView_Previews: PreviewProvider { static var previews: some View { - ChatRoomConfigView(isKeyPresented: .constant(true)) + ChatRoomConfigView(isKeyPresented: .constant(true), chatModel: AIChatModel(roomID: nil)) } } diff --git a/iChatGPT/Models/AIChatModel.swift b/iChatGPT/Models/AIChatModel.swift index 5a5196b..c8214f5 100644 --- a/iChatGPT/Models/AIChatModel.swift +++ b/iChatGPT/Models/AIChatModel.swift @@ -32,12 +32,6 @@ class AIChatModel: ObservableObject { } /// room id var roomID: String - /// api model - var apiModel: String { - didSet { - saveRoomConfigData() - } - } /// 更新 token 时更新请求的会话 var isRefreshSession: Bool = false private var bot: Chatbot? @@ -45,13 +39,12 @@ class AIChatModel: ObservableObject { init(roomID: String?) { let roomID = roomID ?? String(Int(Date().timeIntervalSince1970)) self.roomID = roomID - if let room = ChatRoomStore.shared.chatRoom(roomID) { - apiModel = room.model ?? UserDefaults.standard.string(forKey: ChatGPTModelName) ?? "gpt-3.5-turbo" + if ChatRoomStore.shared.chatRoom(roomID) != nil { let messages = ChatMessageStore.shared.messages(forRoom: roomID) contents.append(contentsOf: messages) } else { - apiModel = UserDefaults.standard.string(forKey: ChatGPTModelName) ?? "gpt-3.5-turbo" - ChatRoomStore.shared.addChatRoom(ChatRoom(roomID: roomID)) + let model = UserDefaults.standard.string(forKey: ChatGPTModelName) ?? "gpt-3.5-turbo" + ChatRoomStore.shared.addChatRoom(ChatRoom(roomID: roomID, model: model)) } loadChatbot() } @@ -60,8 +53,8 @@ class AIChatModel: ObservableObject { let newRoomID = roomID ?? String(Int(Date().timeIntervalSince1970)) self.roomID = newRoomID self.contents = ChatMessageStore.shared.messages(forRoom: newRoomID) - apiModel = UserDefaults.standard.string(forKey: ChatGPTModelName) ?? "gpt-3.5-turbo" - let room = ChatRoomStore.shared.chatRoom(newRoomID) ?? ChatRoom(roomID: newRoomID) + let model = UserDefaults.standard.string(forKey: ChatGPTModelName) ?? "gpt-3.5-turbo" + let room = ChatRoomStore.shared.chatRoom(newRoomID) ?? ChatRoom(roomID: newRoomID, model: model) ChatRoomStore.shared.addChatRoom(room) loadChatbot() } @@ -70,19 +63,22 @@ class AIChatModel: ObservableObject { if isRefreshSession { loadChatbot() } - let index = contents.count let userAvatarUrl = self.bot?.getUserAvatar() ?? "" + let roomModel = ChatRoomStore.shared.chatRoom(roomID) let model = UserDefaults.standard.string(forKey: ChatGPTModelName) ?? "gpt-3.5-turbo" var chat = AIChat(datetime: Date().currentDateString(), issue: prompt, model: model, userAvatarUrl: userAvatarUrl) contents.append(chat) isScrollListBottom.toggle() - self.bot?.getChatGPTAnswer(prompts: contents, sendContext: isSendContext) { answer in + self.bot?.getChatGPTAnswer(prompts: contents, sendContext: isSendContext, roomModel: roomModel) { answer in let content = answer DispatchQueue.main.async { [self] in chat.answer = content chat.isResponse = true - contents[index] = chat + // 找到要替换的元素在数组中的索引位置 + if let index = contents.lastIndex(where: { $0.datetime == chat.datetime && $0.issue == chat.issue }) { + contents[index] = chat + } } } } @@ -90,7 +86,7 @@ class AIChatModel: ObservableObject { func loadChatbot() { isRefreshSession = false let chatGPTOpenAIKey = UserDefaults.standard.string(forKey: ChatGPTOpenAIKey) ?? "" - bot = Chatbot( openAIKey: chatGPTOpenAIKey) + bot = Chatbot(openAIKey: chatGPTOpenAIKey) } func saveMessagesData() { diff --git a/iChatGPT/Models/ChatGPT.swift b/iChatGPT/Models/ChatGPT.swift index 4e6f5ba..2fdca2a 100644 --- a/iChatGPT/Models/ChatGPT.swift +++ b/iChatGPT/Models/ChatGPT.swift @@ -26,14 +26,15 @@ class Chatbot { userAvatarUrl } - func getChatGPTAnswer(prompts: [AIChat], sendContext: Bool, completion: @escaping (String) -> Void) { + func getChatGPTAnswer(prompts: [AIChat], sendContext: Bool, roomModel: ChatRoom?, completion: @escaping (String) -> Void) { // 构建对话记录 print("prompts") print(prompts) var messages: [OpenAI.Chat] = [] if sendContext { // 每次只放此次提问之前三轮问答,且答案只放前面100字,已经足够AI推理了 - let prompts = Array(prompts.suffix(4)) + let historyCount = roomModel?.historyCount ?? 3 + let prompts = Array(prompts.suffix(historyCount + 1)) for i in 0..