Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Folders #75

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a005aa7
Add Folder Datatype
Jul 7, 2024
1ffc939
Added the New Folder button on the tool bar. Haven't added in the act…
Jul 7, 2024
6976fe0
Button connected to model
Jul 7, 2024
826462d
Basic ability to organise chats with drag and drop in to folders
Jul 7, 2024
d5888ef
Can create chats straight inside an existing folder now
Jul 7, 2024
49ac234
Keep folders open on drop
Jul 9, 2024
3f88f14
Sort root folders in to chats by name
Jul 10, 2024
634bc30
Can now rename sub folders but need to reimplement movement controls …
Jul 11, 2024
a5decf9
Updated freechat-server and NavList. NavList now has selection colour…
Jul 14, 2024
1f691db
Delete and rename function menu off to the side
Jul 14, 2024
3c48c01
Additions to schemea
Jul 14, 2024
1af955d
Data
Jul 14, 2024
d18ac44
Context menu fixed and working correctly for Rename and recursive delete
Jul 14, 2024
582afca
15-7-24 checkpoint
Jul 14, 2024
37b7ca6
Added a flag to render folders as Open when refreshing
Jul 14, 2024
61198fc
Redraw the navlist on move, to refresh from data model
Jul 14, 2024
1314ebe
Removal of non required code through refactor
Jul 15, 2024
2cc81b6
Finally worked out the context menu issue. Item id's in a hierarchy a…
Jul 17, 2024
6ddb355
Before refactor to remove stroings from names
Jul 21, 2024
cfdef1f
Ability to create folders in folders
Jul 22, 2024
9366eec
Model Changes for UUID
Jul 22, 2024
546beb4
Recursive deletion of folders and contents
Jul 22, 2024
0735565
Fixed conversation selection logic
Jul 22, 2024
824b1fc
Formatting
Jul 22, 2024
d16e4ee
Fixed renaming functionality for folders and files, much smother now
Jul 22, 2024
3614c84
Better tap alignment, still can be slightly improved
Jul 22, 2024
a9de4ff
Working version before test re-write
Jul 24, 2024
5110367
Default folders to open
Jul 24, 2024
ece7227
Good version of folders but stuffed context menu
Aug 3, 2024
dbc2286
Finally got it all together with the selected item highlighting, rena…
Aug 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
skipped = "YES"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
Expand All @@ -41,7 +41,7 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
skipped = "YES"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
Expand Down
16 changes: 15 additions & 1 deletion mac/FreeChat/Chats.xcdatamodeld/Mantras.xcdatamodel/contents
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23C71" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="Conversation" representedClassName="Conversation" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" attributeType="Date" defaultDateTimeInterval="712519080" usesScalarValueType="NO"/>
<attribute name="lastMessageAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="orderIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="path" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>
<attribute name="uniqueId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="updatedAt" attributeType="Date" defaultDateTimeInterval="713047620" usesScalarValueType="NO"/>
<relationship name="folder" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Folder" inverseName="conversation" inverseEntity="Folder"/>
<relationship name="messages" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Message" inverseName="conversation" inverseEntity="Message"/>
</entity>
<entity name="Folder" representedClassName="Folder" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="open" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="orderIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="path" optional="YES" attributeType="String"/>
<attribute name="sysPrompt" optional="YES" attributeType="String"/>
<relationship name="child" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Folder" inverseName="parent" inverseEntity="Folder"/>
<relationship name="conversation" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Conversation" inverseName="folder" inverseEntity="Conversation"/>
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Folder" inverseName="child" inverseEntity="Folder"/>
</entity>
<entity name="Message" representedClassName="Message" syncable="YES" codeGenerationType="class">
<attribute name="createdAt" attributeType="Date" defaultDateTimeInterval="712518960" usesScalarValueType="NO"/>
<attribute name="fromId" attributeType="String" defaultValueString="npc"/>
Expand Down
42 changes: 35 additions & 7 deletions mac/FreeChat/Models/Conversation+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,31 @@
import Foundation
import CoreData

//extension Conversation: Hashable {
extension Conversation {
public var id: UUID {
if uniqueId == nil {
uniqueId = UUID()
}
return uniqueId!
}



static func create(ctx: NSManagedObjectContext) throws -> Self {
let record = self.init(context: ctx)
record.createdAt = Date()
record.lastMessageAt = record.createdAt

try ctx.save()
return record
}
let record = self.init(context: ctx)
record.createdAt = Date()
record.lastMessageAt = record.createdAt
record.uniqueId = UUID() // Set the uniqueId here
try ctx.save()
return record
}

func moveToFolder(_ folder: Folder?) {
self.folder = folder
try? self.managedObjectContext?.save()
}

var orderedMessages: [Message] {
let set = messages as? Set<Message> ?? []
return set.sorted {
Expand Down Expand Up @@ -60,4 +75,17 @@ extension Conversation {
self.setValue(Date(), forKey: "updatedAt")
}
}

/*
public func hash(into hasher: inout Hasher) {
hasher.combine(objectID)
}*/

public static func == (lhs: Conversation, rhs: Conversation) -> Bool {
return lhs.objectID == rhs.objectID
}



}

93 changes: 93 additions & 0 deletions mac/FreeChat/Models/ConversationHierarchy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Foundation
import CoreData

class ConversationHierarchy {
private let viewContext: NSManagedObjectContext

@Published var folderHierarchy: [FolderNode] = []
@Published var rootConversations: [Conversation] = []


init(viewContext: NSManagedObjectContext) {
self.viewContext = viewContext
refreshHierarchy()
}

func refreshHierarchy() {
(folderHierarchy, rootConversations) = getHierarchy()
}

func getHierarchy() -> ([FolderNode], [Conversation]) {
do {
let folderFetchRequest: NSFetchRequest<Folder> = Folder.fetchRequest()
folderFetchRequest.predicate = NSPredicate(format: "parent == nil")
let rootFolders = try viewContext.fetch(folderFetchRequest)

let folderNodes = rootFolders
.map { createFolderNode(from: $0) }
.sorted { $0.folder.name?.lowercased() ?? "" < $1.folder.name?.lowercased() ?? "" }

let conversationFetchRequest: NSFetchRequest<Conversation> = Conversation.fetchRequest()
conversationFetchRequest.predicate = NSPredicate(format: "folder == nil")
let rootConversations = try viewContext.fetch(conversationFetchRequest)
.sorted { $0.titleWithDefault.lowercased() < $1.titleWithDefault.lowercased() }

// Combine and sort folders and root conversations
let combinedItems = (folderNodes as [Any] + rootConversations as [Any]).sorted {
let title1 = ($0 as? FolderNode)?.folder.name?.lowercased() ?? ($0 as? Conversation)?.titleWithDefault.lowercased() ?? ""
let title2 = ($1 as? FolderNode)?.folder.name?.lowercased() ?? ($1 as? Conversation)?.titleWithDefault.lowercased() ?? ""
return title1 < title2
}

// Separate sorted items back into folders and conversations
let sortedFolderNodes = combinedItems.compactMap { $0 as? FolderNode }
let sortedRootConversations = combinedItems.compactMap { $0 as? Conversation }

return (sortedFolderNodes, sortedRootConversations)
} catch {
print("Failed to fetch root items: \(error)")
return ([], [])
}
}

private func createFolderNode(from folder: Folder) -> FolderNode {
let subfolders = folder.subfolders.map { createFolderNode(from: $0) }
let conversations = fetchConversations(for: folder)

// Combine and sort subfolders and conversations
let combinedItems = (subfolders as [Any] + conversations as [Any]).sorted {
let title1 = ($0 as? FolderNode)?.folder.name?.lowercased() ?? ($0 as? Conversation)?.titleWithDefault.lowercased() ?? ""
let title2 = ($1 as? FolderNode)?.folder.name?.lowercased() ?? ($1 as? Conversation)?.titleWithDefault.lowercased() ?? ""
return title1 < title2
}

// Separate sorted items back into subfolders and conversations
let sortedSubfolders = combinedItems.compactMap { $0 as? FolderNode }
let sortedConversations = combinedItems.compactMap { $0 as? Conversation }

return FolderNode(folder: folder, subfolders: sortedSubfolders, conversations: sortedConversations, isOpen: folder.open)
}

private func fetchConversations(for folder: Folder) -> [Conversation] {
let fetchRequest: NSFetchRequest<Conversation> = Conversation.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "folder == %@", folder)

do {
return try viewContext.fetch(fetchRequest)
} catch {
print("Failed to fetch conversations for folder \(folder.name ?? ""): \(error)")
return []
}
}

private func sortFolderNodesAlphabetically(_ nodes: [FolderNode]) -> [FolderNode] {
return nodes.sorted { $0.folder.name?.lowercased() ?? "" < $1.folder.name?.lowercased() ?? "" }.map { node in
var sortedNode = node
sortedNode.subfolders = sortFolderNodesAlphabetically(node.subfolders)
sortedNode.conversations = node.conversations.sorted { $0.titleWithDefault.lowercased() < $1.titleWithDefault.lowercased() }
return sortedNode
}
}
}


57 changes: 57 additions & 0 deletions mac/FreeChat/Models/Folder+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Folder+Extensions.swift
// FreeChat
//
// Created by Sebastian Gray on 5/7/2024.
//

import Foundation
import CoreData


extension Folder {

static func create(ctx: NSManagedObjectContext, name: String, parent: Folder? = nil) throws -> Self {
let folder = self.init(context: ctx)
folder.name = name
if let parent = parent {
parent.addSubfolder(folder) // Assuming you have a 'children' relationship
}
try ctx.save()
return folder
}

func addConversation(_ conversation: Conversation) {
conversation.moveToFolder(self)
}

var subfolders: [Folder] {
let childFolders = self.child as? Set<Folder> ?? []

return Array(childFolders).sorted { $0.name ?? "" < $1.name ?? "" }
}

func addSubfolder(_ subfolder: Folder) {
addToChild(subfolder)
}

func setSysPrompt(_ prompt: String?) {
self.sysPrompt = prompt
}

func rename(to newName: String) {
if !newName.isEmpty && newName != self.name {
self.name = newName
try? self.managedObjectContext?.save()
}
}

public override func willSave() {
super.willSave()

//if !isDeleted, changedValues()["updatedAt"] == nil {
// self.setValue(Date(), forKey: "updatedAt")
//}
}
}

42 changes: 42 additions & 0 deletions mac/FreeChat/Models/FolderNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// FolderNode.swift
// FreeChat
//
// Created by Sebastian Gray on 10/7/2024.
//

import Foundation
import CoreData

public struct FolderNode: Identifiable, Hashable {
public let id = UUID()
public let folder: Folder
public var subfolders: [FolderNode]
public var conversations: [Conversation]
public var isOpen: Bool

public static func == (lhs: FolderNode, rhs: FolderNode) -> Bool {
return lhs.id == rhs.id
}

public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

enum NavItem: Identifiable {
case folder(FolderNode)
case conversation(Conversation)




var id: AnyHashable {
switch self {
case .folder(let folderNode):
return AnyHashable(folderNode.folder.objectID)
case .conversation(let conversation):
return AnyHashable(conversation.id)
}
}
}
Binary file modified mac/FreeChat/Models/NPC/freechat-server
Binary file not shown.
6 changes: 4 additions & 2 deletions mac/FreeChat/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ struct ContentView: View {
var body: some View {
NavigationSplitView {
if setInitialSelection {
NavList(selection: $selection, showDeleteConfirmation: $showDeleteConfirmation)
.navigationSplitViewColumnWidth(min: 160, ideal: 160)
HierarchyView(selection: $selection,
showDeleteConfirmation: $showDeleteConfirmation,
viewContext: viewContext)
.navigationSplitViewColumnWidth(min: 160, ideal: 160)
}
} detail: {
if selection.count > 1 {
Expand Down
Loading