diff --git a/app/ChatMasterMind.xcodeproj/project.pbxproj b/app/ChatMasterMind.xcodeproj/project.pbxproj index ab22070..7d2e974 100644 --- a/app/ChatMasterMind.xcodeproj/project.pbxproj +++ b/app/ChatMasterMind.xcodeproj/project.pbxproj @@ -231,7 +231,6 @@ ); mainGroup = 4F772AF82A4706F600D3266B; packageReferences = ( - 4F50EF3F2A49CA5E009BD94E /* XCRemoteSwiftPackageReference "Down" */, 4F50EF422A49CE31009BD94E /* XCRemoteSwiftPackageReference "MarkdownView" */, 4F50EF452A49D012009BD94E /* XCRemoteSwiftPackageReference "Highlightr" */, ); @@ -444,6 +443,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ChatMasterMind/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -485,6 +485,7 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ChatMasterMind/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -644,14 +645,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 4F50EF3F2A49CA5E009BD94E /* XCRemoteSwiftPackageReference "Down" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/iwasrobbed/Down"; - requirement = { - branch = master; - kind = branch; - }; - }; 4F50EF422A49CE31009BD94E /* XCRemoteSwiftPackageReference "MarkdownView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/LiYanan2004/MarkdownView.git"; diff --git a/app/ChatMasterMind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/app/ChatMasterMind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 083cf4f..29a051f 100644 --- a/app/ChatMasterMind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/app/ChatMasterMind.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "down", - "kind" : "remoteSourceControl", - "location" : "https://github.com/iwasrobbed/Down", - "state" : { - "branch" : "master", - "revision" : "e754ab1c80920dd51a8e08290c912ac1c2ac8b58" - } - }, { "identity" : "highlightr", "kind" : "remoteSourceControl", diff --git a/app/ChatMasterMind.xcodeproj/project.xcworkspace/xcuserdata/ok.xcuserdatad/UserInterfaceState.xcuserstate b/app/ChatMasterMind.xcodeproj/project.xcworkspace/xcuserdata/ok.xcuserdatad/UserInterfaceState.xcuserstate index 5db0577..d658991 100644 Binary files a/app/ChatMasterMind.xcodeproj/project.xcworkspace/xcuserdata/ok.xcuserdatad/UserInterfaceState.xcuserstate and b/app/ChatMasterMind.xcodeproj/project.xcworkspace/xcuserdata/ok.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/app/ChatMasterMind/ChatData.swift b/app/ChatMasterMind/ChatData.swift index 9f27cd1..09c90e5 100644 --- a/app/ChatMasterMind/ChatData.swift +++ b/app/ChatMasterMind/ChatData.swift @@ -9,30 +9,26 @@ import Foundation import SwiftData @Model -final class ChatPair: Identifiable { - let id: UUID +final class ChatPair { var timestamp: Date var question: String var answer: String? - var previousVersions: [ChatPair] + var disabled: Bool - init(question: String, answer: String? = nil, timestamp: Date = Date(), previousVersions: [ChatPair] = []) { - self.id = UUID() + init(question: String, answer: String? = nil, timestamp: Date = Date(), disabled: Bool = false) { self.question = question self.answer = answer self.timestamp = timestamp - self.previousVersions = previousVersions + self.disabled = disabled } } @Model -final class ChatHistory: Identifiable { - let id: UUID +final class ChatHistory { var name: String var chatPairs: [ChatPair] init(name: String, chatPairs: [ChatPair] = []) { - self.id = UUID() self.name = name self.chatPairs = chatPairs } @@ -42,7 +38,7 @@ final class ChatHistory: Identifiable { chatPairs.append(newPair) } - func editChatPair(withId id: UUID, question: String? = nil, answer: String? = nil) { + func editChatPair(withId id: PersistentIdentifier, question: String? = nil, answer: String? = nil) { guard let index = chatPairs.firstIndex(where: { $0.id == id }) else { return } let newChatPair = chatPairs[index] newChatPair.previousVersions.append(chatPairs[index]) @@ -55,4 +51,8 @@ final class ChatHistory: Identifiable { newChatPair.timestamp = Date() chatPairs[index] = newChatPair } + + func moveChatPair(from source: IndexSet, to destination: Int) { + chatPairs.move(fromOffsets: source, toOffset: destination) + } } diff --git a/app/ChatMasterMind/ContentView.swift b/app/ChatMasterMind/ContentView.swift index 59550e1..4518c64 100644 --- a/app/ChatMasterMind/ContentView.swift +++ b/app/ChatMasterMind/ContentView.swift @@ -55,9 +55,73 @@ struct ContentView: View { } } +struct ChatPairView: View { + let chatPair: ChatPair + let editAction: () -> Void + let toggleAction: (Bool) -> Void + let dateFormatter: DateFormatter + + var body: some View { + VStack(alignment: .leading) { + GroupBox { + VStack(alignment: .leading) { + MarkdownView(text: chatPair.question) + Divider() + if let answer = chatPair.answer { + MarkdownView(text: answer) + } + } + } + .onTapGesture { + editAction() + } + .opacity(chatPair.disabled ? 0.5 : 1) + HStack { + Toggle("", isOn: Binding( + get: { !chatPair.disabled }, + set: { toggleAction($0) })) + .toggleStyle(CheckboxToggleStyle()) + .labelsHidden() + Text("\(chatPair.timestamp, formatter: dateFormatter)") + .foregroundColor(.secondary) + .font(.footnote) + } + } + .padding(.vertical) + } +} + +struct ChatPairEditor: View { + @Binding var chatPair: ChatPair? + @Binding var question: String + @Binding var answer: String + let saveAction: () -> Void + let cancelAction: () -> Void + + var body: some View { + VStack { + TextEditor(text: $question) + .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.gray)) + .frame(maxHeight: .infinity) + TextEditor(text: $answer) + .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.gray)) + .frame(maxHeight: .infinity) + HStack { + Button(action: cancelAction) { + Image(systemName: "xmark.circle.fill") + } + Button(action: saveAction) { + Image(systemName: "checkmark.circle.fill") + } + } + } + .padding() + } +} + struct ChatHistoryDetailView: View { @Environment(\.modelContext) private var modelContext - var chatHistory: ChatHistory + @State var chatHistory: ChatHistory @State private var newQuestion: String = "" @State private var pairToEdit: ChatPair? = nil @State private var newAnswer: String = "" @@ -65,59 +129,75 @@ struct ChatHistoryDetailView: View { var body: some View { VStack { List { - ForEach(chatHistory.chatPairs) { chatPair in - VStack(alignment: .leading) { - MarkdownView(text: chatPair.question) - if let answer = chatPair.answer { - MarkdownView(text: answer) - .tint(.secondary) - } - Button(action: { - pairToEdit = chatPair - newAnswer = chatPair.answer ?? "" - }) { - Text("Edit") - .foregroundColor(.blue) - } - } - Text("Timestamp: \(chatPair.timestamp)") - .foregroundColor(.secondary) - .font(.footnote) + ForEach(Array(chatHistory.chatPairs.enumerated()), id: \.element) { index, chatPair in + ChatPairView(chatPair: chatPair, + editAction: { + pairToEdit = chatPair + newQuestion = chatPair.question + newAnswer = chatPair.answer ?? "" + }, + toggleAction: { isEnabled in + chatPair.disabled = !isEnabled + saveContext() + }, + dateFormatter: itemFormatter) } + .onDelete(perform: deleteChatPair) + .onMove(perform: moveChatPair) } HStack { TextEditor(text: $newQuestion) - .frame(height: 100) + .frame(height: 50) .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.gray)) Button(action: { addChatPair() }) { - Text("Add") - } - Button(action: { - cancelEdit() - }) { - Text("Cancel") + Image(systemName: "plus.circle.fill") } } .padding() } .navigationTitle(chatHistory.name) .sheet(item: $pairToEdit) { pairToEdit in - VStack { - Text(pairToEdit.question) - TextEditor(text: $newAnswer) - .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.gray)) - .frame(maxHeight: .infinity) - Button(action: { - editChatPair(pairToEdit) - }) { - Text("Save") - } - } - .padding() + ChatPairEditor(chatPair: $pairToEdit, + question: $newQuestion, + answer: $newAnswer, + saveAction: { editChatPair(pairToEdit) }, + cancelAction: cancelEdit) } } + + private func saveContext() { + do { + try modelContext.save() + } catch { + print("Error saving model context: \(error)") + } + } + + private func deleteChatPair(at offsets: IndexSet) { + withAnimation { + offsets.forEach { index in + let chatPair = chatHistory.chatPairs[index] + modelContext.delete(chatPair) + } + saveContext() + } + } + + func moveChatPair(from source: IndexSet, to destination: Int) { + withAnimation { + chatHistory.moveChatPair(from: source, to: destination) + saveContext() + } + } + + private var itemFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + return formatter + } private func addChatPair() { guard !newQuestion.isEmpty else { return } @@ -125,34 +205,41 @@ struct ChatHistoryDetailView: View { let newPair = ChatPair(question: newQuestion) chatHistory.chatPairs.append(newPair) newQuestion = "" - do { - try modelContext.save() - } catch { - print("Error saving model context: \(error)") - } + saveContext() } } private func editChatPair(_ chatPair: ChatPair) { guard !newAnswer.isEmpty else { return } withAnimation { - chatHistory.editChatPair(withId: chatPair.id, question: nil, answer: newAnswer) + chatHistory.editChatPair(withId: chatPair.id, question: newQuestion, answer: newAnswer) newAnswer = "" pairToEdit = nil - do { - try modelContext.save() - } catch { - print("Error saving model context: \(error)") - } + saveContext() } } private func cancelEdit() { + newQuestion = "" newAnswer = "" pairToEdit = nil } } + + +struct CheckboxToggleStyle: ToggleStyle { + func makeBody(configuration: Configuration) -> some View { + HStack { + configuration.label + Spacer() + Image(systemName: configuration.isOn ? "eye" : "eye.slash") + .foregroundColor(configuration.isOn ? .blue : .gray) + .onTapGesture { configuration.isOn.toggle() } + } + } +} + #Preview { ContentView() .modelContainer(for: ChatHistory.self, inMemory: true)