Skip to content

Commit 10e08c3

Browse files
committed
🧑‍💻 Improvements
1 parent 42eae39 commit 10e08c3

File tree

8 files changed

+126
-106
lines changed

8 files changed

+126
-106
lines changed

ChatMLX/Features/Conversation/ConversationDetailView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ struct ConversationDetailView: View {
8787
MessageBubbleView(
8888
message: message,
8989
displayStyle: $displayStyle
90-
).id(message.id)
90+
)
9191
}
9292
}
9393
.padding()

ChatMLX/Features/Conversation/ConversationSidebarItem.swift

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,12 @@ struct ConversationSidebarItem: View {
1111
@ObservedObject var conversation: Conversation
1212

1313
@Environment(\.managedObjectContext) private var viewContext
14+
@Environment(ConversationViewModel.self) private var vm
1415

15-
@Binding var selectedConversation: Conversation?
16-
17-
@State private var isHovering: Bool = false
1816
@State private var isActive: Bool = false
19-
@State private var showIndicator: Bool = false
2017

2118
var body: some View {
22-
Button {
23-
selectedConversation = conversation
24-
} label: {
19+
Button(action: selectConversation) {
2520
VStack(alignment: .leading, spacing: 4) {
2621
Text(LocalizedStringKey(conversation.title))
2722
.font(.headline)
@@ -33,34 +28,40 @@ struct ConversationSidebarItem: View {
3328

3429
Spacer()
3530

36-
Text(conversation.updatedAt.toFormatted())
37-
.font(.caption)
31+
if !(conversation.isFault || conversation.isDeleted) {
32+
Text(conversation.updatedAt.toFormatted())
33+
.font(.caption)
34+
}
3835
}
3936
.foregroundStyle(.white.opacity(0.7))
4037
}
4138
.padding(6)
4239
}
4340
.buttonStyle(UltramanSidebarButtonStyle(isActive: $isActive))
44-
.onAppear {
45-
checkIfSelfIsActiveTab()
46-
}
47-
.onChange(of: selectedConversation) { _, _ in
48-
checkIfSelfIsActiveTab()
49-
}
41+
.onAppear(perform: updateActiveState)
42+
.onChange(of: vm.selectedConversation) { _, _ in updateActiveState() }
5043
.contextMenu {
5144
Button(role: .destructive, action: deleteConversation) {
5245
Label("Delete", systemImage: "trash")
5346
}
5447
}
5548
}
5649

57-
private func checkIfSelfIsActiveTab() {
50+
private func selectConversation() {
51+
vm.selectedConversation = conversation
52+
}
53+
54+
private func updateActiveState() {
5855
withAnimation(.easeOut(duration: 0.1)) {
59-
isActive = selectedConversation == conversation
56+
isActive = vm.selectedConversation == conversation
6057
}
6158
}
6259

6360
private func deleteConversation() {
64-
try? PersistenceController.shared.delete(conversation)
61+
do {
62+
try PersistenceController.shared.delete(conversation)
63+
} catch {
64+
vm.throwError(error, title: "Delete Conversation Failed")
65+
}
6566
}
6667
}

ChatMLX/Features/Conversation/ConversationSidebarView.swift

Lines changed: 61 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Luminare
1010
import SwiftUI
1111

1212
struct ConversationSidebarView: View {
13-
@Environment(ConversationViewModel.self) private var conversationViewModel
13+
@Environment(ConversationViewModel.self) private var vm
1414
@Environment(\.managedObjectContext) private var viewContext
1515

1616
@FetchRequest(
@@ -22,70 +22,82 @@ struct ConversationSidebarView: View {
2222
@Binding var selectedConversation: Conversation?
2323

2424
@State private var keyword = ""
25-
@State private var showingNewConversationAlert = false
26-
@State private var newConversationTitle = ""
27-
@State private var showingClearConfirmation = false
2825

2926
let padding: CGFloat = 8
3027

3128
var body: some View {
3229
VStack(spacing: 0) {
33-
HStack {
34-
Spacer()
35-
Button(action: conversationViewModel.createConversation) {
36-
Image(systemName: "plus")
37-
}
30+
headerView()
31+
logoView()
32+
searchField()
33+
conversationList()
34+
}
35+
.background(.black.opacity(0.4))
36+
}
3837

39-
SettingsLink {
40-
Image(systemName: "gear")
41-
}
38+
@MainActor
39+
@ViewBuilder
40+
private func headerView() -> some View {
41+
HStack {
42+
Spacer()
43+
Button(action: vm.createConversation) {
44+
Image(systemName: "plus")
4245
}
43-
.frame(height: 50)
44-
.padding(.horizontal, padding)
45-
.buttonStyle(.plain)
46-
47-
HStack {
48-
Image("AppLogo")
49-
.resizable()
50-
.aspectRatio(contentMode: .fit)
51-
.frame(width: 60, height: 60)
52-
.shadow(radius: 5)
53-
Text("ChatMLX")
54-
.font(.title)
55-
.fontWeight(.bold)
46+
SettingsLink {
47+
Image(systemName: "gear")
5648
}
49+
}
50+
.frame(height: 50)
51+
.padding(.horizontal, padding)
52+
.buttonStyle(.plain)
53+
}
5754

58-
LuminareSection {
59-
UltramanTextField(
60-
$keyword, placeholder: Text("Search Conversation..."),
61-
onSubmit: updateSearchPredicate
62-
)
55+
@MainActor
56+
@ViewBuilder
57+
private func logoView() -> some View {
58+
HStack {
59+
Image("AppLogo")
60+
.resizable()
61+
.aspectRatio(contentMode: .fit)
62+
.frame(width: 60, height: 60)
63+
.shadow(radius: 5)
64+
Text("ChatMLX")
65+
.font(.title)
66+
.fontWeight(.bold)
67+
}
68+
}
6369

64-
.frame(height: 25)
65-
}.padding(.horizontal, padding)
70+
@MainActor
71+
@ViewBuilder
72+
private func searchField() -> some View {
73+
LuminareSection {
74+
UltramanTextField(
75+
$keyword,
76+
placeholder: Text("Search Conversation..."),
77+
onSubmit: updateSearchPredicate
78+
)
79+
.frame(height: 25)
80+
}
81+
.padding(.horizontal, padding)
82+
}
6683

67-
ScrollView {
68-
LazyVStack(spacing: 0) {
69-
ForEach(conversations) { conversation in
70-
ConversationSidebarItem(
71-
conversation: conversation,
72-
selectedConversation: $selectedConversation
73-
)
74-
}
84+
@MainActor
85+
@ViewBuilder
86+
private func conversationList() -> some View {
87+
ScrollView {
88+
LazyVStack(spacing: 0) {
89+
ForEach(conversations) { conversation in
90+
ConversationSidebarItem(conversation: conversation)
7591
}
7692
}
77-
.padding(.top, 6)
7893
}
79-
.background(.black.opacity(0.4))
94+
.padding(.top, 6)
8095
}
8196

8297
private func updateSearchPredicate() {
83-
if keyword.isEmpty {
84-
conversations.nsPredicate = nil
85-
} else {
86-
conversations.nsPredicate = NSPredicate(
87-
format: "title CONTAINS [cd] %@ OR ANY messages.content CONTAINS [cd] %@", keyword,
88-
keyword)
89-
}
98+
conversations.nsPredicate = keyword.isEmpty ? nil : NSPredicate(
99+
format: "title CONTAINS [cd] %@ OR ANY messages.content CONTAINS [cd] %@",
100+
keyword, keyword
101+
)
90102
}
91103
}

ChatMLX/Features/Conversation/ConversationView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct ConversationView: View {
1919
selectedConversation: $conversationViewModel.selectedConversation)
2020
},
2121
detail: {
22-
Detail()
22+
detailView()
2323
}
2424
)
2525
.foregroundColor(.white)
@@ -28,7 +28,7 @@ struct ConversationView: View {
2828

2929
@MainActor
3030
@ViewBuilder
31-
private func Detail() -> some View {
31+
private func detailView() -> some View {
3232
Group {
3333
if let conversation = conversationViewModel.selectedConversation {
3434
ConversationDetailView(

ChatMLX/Features/Conversation/EmptyConversation.swift

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,15 @@ struct EmptyConversation: View {
1414
var body: some View {
1515
ContentUnavailableView {
1616
Label("No Conversation", systemImage: "tray.fill")
17-
.foregroundColor(.white)
1817
} description: {
1918
Text("Please select a new conversation")
20-
.foregroundColor(.white)
21-
Button(
22-
action: conversationViewModel.createConversation,
23-
label: {
24-
HStack {
25-
Image(systemName: "plus")
26-
.foregroundStyle(.white)
27-
Text("New Conversation")
28-
}
29-
.foregroundColor(.white)
30-
}
31-
).buttonStyle(LuminareCompactButtonStyle())
32-
.fixedSize()
19+
20+
Button(action: conversationViewModel.createConversation) {
21+
Label("New Conversation", systemImage: "plus")
22+
}
23+
.buttonStyle(LuminareCompactButtonStyle())
24+
.fixedSize()
3325
}
26+
.foregroundColor(.white)
3427
}
3528
}

ChatMLX/Features/Conversation/MessageBubbleView.swift

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,6 @@ struct MessageBubbleView: View {
2020

2121
@State private var showToast = false
2222

23-
private func copyText() {
24-
let pasteboard = NSPasteboard.general
25-
pasteboard.clearContents()
26-
pasteboard.setString(message.content, forType: .string)
27-
showToast = true
28-
}
29-
3023
var body: some View {
3124
HStack {
3225
if message.role == .assistant {
@@ -91,9 +84,12 @@ struct MessageBubbleView: View {
9184
Image(systemName: "arrow.clockwise")
9285
.help("Regenerate")
9386
}
87+
.disabled(runner.running)
9488

95-
Text(message.updatedAt.toTimeFormatted())
96-
.font(.caption)
89+
if !(message.isFault || message.isDeleted) {
90+
Text(message.updatedAt.toTimeFormatted())
91+
.font(.caption)
92+
}
9793

9894
if message.role == .assistant, message.inferring {
9995
ProgressView()
@@ -144,12 +140,8 @@ struct MessageBubbleView: View {
144140

145141
private func delete() {
146142
guard message.role == .user else { return }
147-
let conversation = message.conversation
148-
let messages = conversation.messages
149-
if let index = messages.firstIndex(of: message) {
150-
for message in messages[index...] {
151-
viewContext.delete(message)
152-
}
143+
for message in message.suffixMessages() {
144+
viewContext.delete(message)
153145
}
154146

155147
Task(priority: .background) {
@@ -170,16 +162,22 @@ struct MessageBubbleView: View {
170162

171163
Task {
172164
let conversation = message.conversation
173-
let messages = conversation.messages
174-
if let index = messages.firstIndex(of: message) {
175-
for message in messages[index...] {
165+
166+
if conversation.messages.last != message {
167+
for message in message.suffixMessages() {
176168
viewContext.delete(message)
177169
}
178170
}
179-
180171
await MainActor.run {
181172
runner.generate(conversation: conversation, in: viewContext)
182173
}
183174
}
184175
}
176+
177+
private func copyText() {
178+
let pasteboard = NSPasteboard.general
179+
pasteboard.clearContents()
180+
pasteboard.setString(message.content, forType: .string)
181+
showToast = true
182+
}
185183
}

ChatMLX/Models/Message+CoreDataClass.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,15 @@ public class Message: NSManagedObject {
3838
"content": self.content,
3939
]
4040
}
41+
42+
func suffixMessages() -> [Message] {
43+
let conversation = self.conversation
44+
let messages = conversation.messages
45+
46+
guard let index = messages.firstIndex(of: self) else {
47+
return []
48+
}
49+
50+
return Array(messages[index...])
51+
}
4152
}

ChatMLX/Utilities/LLMRunner.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
//
77

88
import Defaults
9+
import Metal
910
import MLX
1011
import MLXLLM
1112
import MLXRandom
12-
import Metal
1313
import SwiftUI
1414
import Tokenizers
1515

@@ -113,13 +113,18 @@ class LLMRunner {
113113
}
114114

115115
func generate(
116-
conversation: Conversation, in context: NSManagedObjectContext,
116+
conversation: Conversation,
117+
in context: NSManagedObjectContext,
117118
progressing: @escaping () -> Void = {}
118119
) {
119120
guard !running else { return }
120121
running = true
121122

122-
let assistantMessage = Message(context: context).assistant(conversation: conversation)
123+
let assistantMessage: Message = if let message = conversation.messages.last, message.role == .assistant {
124+
message
125+
} else {
126+
Message(context: context).assistant(conversation: conversation)
127+
}
123128

124129
let parameters = GenerateParameters(
125130
temperature: conversation.temperature,

0 commit comments

Comments
 (0)