diff --git a/Ollamac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Ollamac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9f3211b..417cd6d 100644 --- a/Ollamac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Ollamac.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kevinhermawan/ViewCondition", "state" : { - "revision" : "f69a0ebf8cb6630c171a8bc284c6b9273590cc81", - "version" : "1.0.0" + "revision" : "3d6ff8a51f31890e091eac6872d966859d11ed76", + "version" : "1.0.1" } }, { diff --git a/Ollamac/ViewModels/ChatViewModel.swift b/Ollamac/ViewModels/ChatViewModel.swift index 853d8de..c570ade 100644 --- a/Ollamac/ViewModels/ChatViewModel.swift +++ b/Ollamac/ViewModels/ChatViewModel.swift @@ -19,7 +19,7 @@ final class ChatViewModel { } func fetch() throws { - let sortDescriptor = SortDescriptor(\Chat.createdAt, order: .reverse) + let sortDescriptor = SortDescriptor(\Chat.modifiedAt, order: .reverse) let fetchDescriptor = FetchDescriptor(sortBy: [sortDescriptor]) self.chats = try self.modelContext.fetch(fetchDescriptor) @@ -46,4 +46,15 @@ final class ChatViewModel { try self.modelContext.saveChanges() } + + func modify(_ chat: Chat) throws { + chat.modifiedAt = .now + + if let index = self.chats.firstIndex(where: { $0.id == chat.id }) { + self.chats.remove(at: index) + self.chats.insert(chat, at: 0) + } + + try self.modelContext.saveChanges() + } } diff --git a/Ollamac/Views/ChatViews/ChatSidebarListView.swift b/Ollamac/Views/ChatViews/ChatSidebarListView.swift index 40f274c..db9c355 100644 --- a/Ollamac/Views/ChatViews/ChatSidebarListView.swift +++ b/Ollamac/Views/ChatViews/ChatSidebarListView.swift @@ -6,22 +6,77 @@ // import SwiftUI +import ViewCondition struct ChatSidebarListView: View { @Environment(CommandViewModel.self) private var commandViewModel @Environment(ChatViewModel.self) private var chatViewModel + private var todayChats: [Chat] { + let calendar = Calendar.current + let today = calendar.startOfDay(for: Date()) + + return chatViewModel.chats.filter { + calendar.isDate($0.modifiedAt, inSameDayAs: today) + } + } + + private var yesterdayChats: [Chat] { + let calendar = Calendar.current + let today = calendar.startOfDay(for: Date()) + let yesterday = calendar.date(byAdding: .day, value: -1, to: today)! + + return chatViewModel.chats.filter { + calendar.isDate($0.modifiedAt, inSameDayAs: yesterday) + } + } + + private var previousDays: [Chat] { + let calendar = Calendar.current + let today = calendar.startOfDay(for: Date()) + let yesterday = calendar.date(byAdding: .day, value: -1, to: today)! + + return chatViewModel.chats.filter { + !calendar.isDate($0.modifiedAt, inSameDayAs: today) && !calendar.isDate($0.modifiedAt, inSameDayAs: yesterday) + } + } + var body: some View { @Bindable var commandViewModelBindable = commandViewModel List(selection: $commandViewModelBindable.selectedChat) { - ForEach(chatViewModel.chats) { chat in - Label(chat.name, systemImage: "bubble") - .contextMenu { - ChatContextMenu(commandViewModel, for: chat) - } - .tag(chat) + Section(header: Text("Today")) { + ForEach(todayChats) { chat in + Label(chat.name, systemImage: "bubble") + .contextMenu { + ChatContextMenu(commandViewModel, for: chat) + } + .tag(chat) + } + } + .hide(if: todayChats.isEmpty, removeCompletely: true) + + Section(header: Text("Yesterday")) { + ForEach(yesterdayChats) { chat in + Label(chat.name, systemImage: "bubble") + .contextMenu { + ChatContextMenu(commandViewModel, for: chat) + } + .tag(chat) + } + } + .hide(if: yesterdayChats.isEmpty, removeCompletely: true) + + Section(header: Text("Previous Days")) { + ForEach(previousDays) { chat in + Label(chat.name, systemImage: "bubble") + .contextMenu { + ChatContextMenu(commandViewModel, for: chat) + } + .tag(chat) + } } + .hide(if: previousDays.isEmpty, removeCompletely: true) } .listStyle(.sidebar) .task { @@ -67,6 +122,7 @@ struct ChatSidebarListView: View { .dialogSeverity(.critical) } + // MARK: - Actions func deleteAction() { guard let chatToDelete = commandViewModel.chatToDelete else { return } try? chatViewModel.delete(chatToDelete) diff --git a/Ollamac/Views/MessageViews/MessageView.swift b/Ollamac/Views/MessageViews/MessageView.swift index 4f68942..e251a8f 100644 --- a/Ollamac/Views/MessageViews/MessageView.swift +++ b/Ollamac/Views/MessageViews/MessageView.swift @@ -8,12 +8,14 @@ import OptionalKit import SwiftUI import SwiftUIIntrospect +import ViewCondition import ViewState struct MessageView: View { private var chat: Chat @Environment(\.modelContext) private var modelContext + @Environment(ChatViewModel.self) private var chatViewModel @Environment(MessageViewModel.self) private var messageViewModel @Environment(OllamaViewModel.self) private var ollamaViewModel @@ -71,58 +73,58 @@ struct MessageView: View { scrollToBottom(scrollViewProxy) } - if !isEditorExpanded { - VStack(spacing: 8) { - HStack(alignment: .bottom, spacing: 16) { - PromptEditor(prompt: $prompt) - .frame(minHeight: 32, maxHeight: 256) - .fixedSize(horizontal: false, vertical: true) - .overlay(alignment: .topTrailing) { - Button(action: { isEditorExpanded = true }) { - Label("Expand", systemImage: "arrow.up.left.and.arrow.down.right") - .labelStyle(.iconOnly) - } - .padding(8) - .buttonStyle(.plain) - .keyboardShortcut("e", modifiers: .command) - .help("Expand editor (⌘ + E)") + VStack(spacing: 8) { + HStack(alignment: .bottom, spacing: 16) { + PromptEditor(prompt: $prompt) + .frame(minHeight: 32, maxHeight: 256) + .fixedSize(horizontal: false, vertical: true) + .overlay(alignment: .topTrailing) { + Button(action: { isEditorExpanded = true }) { + Label("Expand", systemImage: "arrow.up.left.and.arrow.down.right") + .labelStyle(.iconOnly) } - .focused($isEditorFocused) - .onChange(of: messageViewModel.sendViewState) { - isEditorFocused = messageViewModel.sendViewState.isNil - } - .onChange(of: prompt) { - directSendAction() - } - - Button(action: sendAction) { - Label("Send", systemImage: "paperplane") - .padding(8) - .foregroundStyle(.white) - .help("Send message (Return)") + .padding(8) + .buttonStyle(.plain) + .keyboardShortcut("e", modifiers: .command) + .help("Expand editor (⌘ + E)") } - .buttonStyle(.borderedProminent) - .disabled(sendButtonDisabled) - } - .padding(.horizontal) - - if let errorMessage { - HStack(alignment: .center) { - Text(errorMessage) + .focused($isEditorFocused) + .disabled(promptInputDisabled) + .onChange(of: messageViewModel.sendViewState) { + isEditorFocused = messageViewModel.sendViewState.isNil } - .foregroundStyle(.red) - } - - if messageViewModel.sendViewState == .error { - HStack(alignment: .center) { - Text(AppMessages.generalErrorMessage) + .onChange(of: prompt) { + directSendAction() } - .foregroundStyle(.red) + + Button(action: sendAction) { + Label("Send", systemImage: "paperplane") + .padding(8) + .foregroundStyle(.white) + .help("Send message (Return)") } + .buttonStyle(.borderedProminent) + .disabled(sendButtonDisabled) + } + .padding(.horizontal) + + if let errorMessage { + HStack(alignment: .center) { + Text(errorMessage) + } + .foregroundStyle(.red) + } + + if messageViewModel.sendViewState == .error { + HStack(alignment: .center) { + Text(AppMessages.generalErrorMessage) + } + .foregroundStyle(.red) } - .padding(.top, 8) - .padding(.bottom, 16) } + .padding(.top, 8) + .padding(.bottom, 16) + .hide(if: isEditorExpanded, removeCompletely: true) } .navigationTitle(chat.name) .navigationSubtitle(chat.model?.name ?? "") @@ -150,6 +152,8 @@ struct MessageView: View { } private func sendAction() { + try? chatViewModel.modify(chat) + let message = Message(prompt: prompt, response: nil) message.context = messageViewModel.messages.last?.context ?? [] message.chat = chat