// // ContentView.swift // ChatMasterMind // // Created by Oleksandr Kozachuk on 2023-06-24. // import SwiftUI import SwiftData import MarkdownView struct ContentView: View { @Environment(\.modelContext) private var modelContext @Query private var chatHistoryList: [ChatHistory] var body: some View { NavigationView { List { ForEach(chatHistoryList) { chatHistory in NavigationLink(destination: ChatHistoryDetailView(chatHistory: chatHistory)) { Text(chatHistory.name) } } .onDelete(perform: deleteItems) } .toolbar { #if os(iOS) ToolbarItem(placement: .navigationBarTrailing) { EditButton() } #endif ToolbarItem { Button(action: newChat) { Label("New chat", systemImage: "plus") } } } Text("Select an chat") } } private func newChat() { withAnimation { let newChatHistory = ChatHistory(name: "test1") modelContext.insert(newChatHistory) } } private func deleteItems(offsets: IndexSet) { withAnimation { for index in offsets { modelContext.delete(chatHistoryList[index]) } } } } 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 @State var chatHistory: ChatHistory @State private var newQuestion: String = "" @State private var pairToEdit: ChatPair? = nil @State private var newAnswer: String = "" var body: some View { VStack { List { 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: 50) .overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.gray)) Button(action: { addChatPair() }) { Image(systemName: "plus.circle.fill") } } .padding() } .navigationTitle(chatHistory.name) .sheet(item: $pairToEdit) { pairToEdit in 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 } withAnimation { let newPair = ChatPair(question: newQuestion) chatHistory.chatPairs.append(newPair) newQuestion = "" saveContext() } } private func editChatPair(_ chatPair: ChatPair) { guard !newAnswer.isEmpty else { return } withAnimation { chatHistory.editChatPair(withId: chatPair.id, question: newQuestion, answer: newAnswer) newAnswer = "" pairToEdit = nil 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) }