Skip to content

Commit 5289874

Browse files
committed
refactor(ModelSelectionView): improve UI and logic for model selection
1 parent f995a24 commit 5289874

File tree

1 file changed

+46
-44
lines changed

1 file changed

+46
-44
lines changed

Chato/Views/ChatSetting/ModelSelectionView.swift

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// Created for Chato in 2025
22

3-
import os
43
import SwiftData
54
import SwiftUI
5+
import os
66

77
struct ModelSelectionView: View {
88
@Environment(\.dismiss) private var dismiss
9-
9+
1010
@Query(filter: #Predicate<Provider> { $0.enabled }) private var providers: [Provider]
1111
@Query private var allModels: [ModelEntity]
12-
12+
1313
@Bindable var chatOption: ChatOption
1414
@State private var searchText = ""
1515
@State private var expandedProviders: Set<PersistentIdentifier> = []
1616
@State private var favoritesExpanded = true
17-
17+
1818
var body: some View {
1919
ModelSelectionContent(
2020
chatOption: chatOption,
@@ -36,17 +36,17 @@ struct ModelSelectionContent: View {
3636
@Binding var expandedProviders: Set<PersistentIdentifier>
3737
@Binding var favoritesExpanded: Bool
3838
let dismiss: DismissAction
39-
39+
4040
private func favoritedModels() -> [ModelEntity] {
4141
let filtered = allModels.filter { $0.favorited }
4242
let sorted = ModelEntity.smartSort(filtered)
4343
return sorted
4444
}
45-
45+
4646
private func searchKeywords() -> [String] {
4747
return parseSearchText(searchText)
4848
}
49-
49+
5050
private var filteredModels: [ModelEntity] {
5151
let keywords = searchKeywords()
5252
if keywords.isEmpty {
@@ -59,24 +59,24 @@ struct ModelSelectionContent: View {
5959
}
6060
return filtered
6161
}
62-
62+
6363
private func parseSearchText(_ text: String) -> [String] {
6464
let separators = CharacterSet(charactersIn: " ,")
6565
let keywords = text.components(separatedBy: separators)
6666
.map { $0.trimmingCharacters(in: .whitespaces) }
6767
.filter { !$0.isEmpty }
6868
return keywords
6969
}
70-
70+
7171
private func matchesKeywords(text: String, keywords: [String]) -> Bool {
7272
guard !keywords.isEmpty else { return true }
73-
73+
7474
let lowercasedText = text.lowercased()
7575
return keywords.contains { keyword in
7676
lowercasedText.contains(keyword.lowercased())
7777
}
7878
}
79-
79+
8080
private var groupedProviders: [(provider: Provider, models: [ModelEntity])] {
8181
let modelsToGroup: [ModelEntity]
8282
if searchText.isEmpty {
@@ -86,7 +86,7 @@ struct ModelSelectionContent: View {
8686
}
8787
return modelsToGroup.groupedByProvider()
8888
}
89-
89+
9090
var body: some View {
9191
ScrollViewReader { proxy in
9292
List {
@@ -110,7 +110,7 @@ struct ModelSelectionContent: View {
110110
}
111111
}
112112
}
113-
113+
114114
@ViewBuilder
115115
private var favoritesSection: some View {
116116
let favorited = favoritedModels()
@@ -130,20 +130,21 @@ struct ModelSelectionContent: View {
130130
.id(model.id)
131131
}
132132
} label: {
133-
Label("Favorites", systemImage: "star.fill")
134-
.foregroundColor(.yellow)
133+
Text("Favorites").font(.headline)
135134
}
135+
.tint(.secondary)
136136
}
137137
}
138-
138+
139139
@ViewBuilder
140140
private var providerSections: some View {
141141
ForEach(groupedProviders, id: \.provider.id) { group in
142142
providerSection(for: group)
143143
}
144144
}
145-
146-
private func providerSection(for group: (provider: Provider, models: [ModelEntity])) -> some View {
145+
146+
private func providerSection(for group: (provider: Provider, models: [ModelEntity])) -> some View
147+
{
147148
DisclosureGroup(
148149
isExpanded: providerBinding(for: group.provider.id)
149150
) {
@@ -160,13 +161,13 @@ struct ModelSelectionContent: View {
160161
}
161162
} label: {
162163
HStack {
163-
Image(systemName: group.provider.iconName)
164164
Text(group.provider.displayName)
165+
.font(.headline)
165166
}
166167
}
167168
.tint(.secondary)
168169
}
169-
170+
170171
private func providerBinding(for providerId: PersistentIdentifier) -> Binding<Bool> {
171172
Binding(
172173
get: { expandedProviders.contains(providerId) },
@@ -179,18 +180,18 @@ struct ModelSelectionContent: View {
179180
}
180181
)
181182
}
182-
183+
183184
@ViewBuilder
184185
private var emptyStateViews: some View {
185186
if !searchText.isEmpty && filteredModels.isEmpty {
186187
ContentUnavailableView.search
187188
}
188-
189+
189190
if providers.isEmpty && allModels.isEmpty {
190191
noModelsView
191192
}
192193
}
193-
194+
194195
private var noModelsView: some View {
195196
ContentUnavailableView {
196197
Label("No Models Available", systemImage: "cube.box")
@@ -203,14 +204,15 @@ struct ModelSelectionContent: View {
203204
.buttonStyle(.borderedProminent)
204205
}
205206
}
206-
207+
207208
private func expandInitialSections() {
208209
guard expandedProviders.isEmpty else { return }
209-
210+
210211
// Check if selected model exists and is not in favorites
211212
let favorited = favoritedModels()
212213
if let selectedModel = chatOption.model,
213-
!favorited.contains(where: { $0.id == selectedModel.id }) {
214+
!favorited.contains(where: { $0.id == selectedModel.id })
215+
{
214216
// Find and expand only the provider containing the selected model
215217
if let providerGroup = groupedProviders.first(where: { group in
216218
group.models.contains(where: { $0.id == selectedModel.id })
@@ -220,17 +222,17 @@ struct ModelSelectionContent: View {
220222
}
221223
// Otherwise, all providers remain collapsed (only favorites is expanded)
222224
}
223-
225+
224226
private func scrollToSelectedModel(proxy: ScrollViewProxy) {
225227
guard let selectedModel = chatOption.model else { return }
226-
228+
227229
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
228230
withAnimation {
229231
proxy.scrollTo(selectedModel.id, anchor: .center)
230232
}
231233
}
232234
}
233-
235+
234236
private func selectModel(_ model: ModelEntity) {
235237
chatOption.model = model
236238
}
@@ -242,7 +244,7 @@ struct ModelSelectionRow: View {
242244
let showProvider: Bool
243245
let searchKeywords: [String]
244246
let action: () -> Void
245-
247+
246248
var body: some View {
247249
HStack(spacing: 12) {
248250
Button(action: action) {
@@ -254,39 +256,38 @@ struct ModelSelectionRow: View {
254256
)
255257
.font(.body)
256258
.foregroundColor(.primary)
257-
259+
258260
HStack(spacing: 8) {
259261
if showProvider {
260262
Label(model.provider.displayName, systemImage: model.provider.iconName)
261263
.font(.caption)
262264
.foregroundColor(.secondary)
263265
}
264-
266+
265267
if model.isCustom {
266268
Label("Custom", systemImage: "wrench")
267269
.font(.caption2)
268270
.foregroundColor(.blue)
269271
}
270-
272+
271273
if let contextLength = model.contextLength {
272274
Text("\(contextLength)k")
273275
.font(.caption)
274276
.foregroundColor(.secondary)
275277
}
276278
}
277279
}
278-
280+
279281
Spacer()
280-
282+
281283
if isSelected {
282284
Image(systemName: "checkmark")
283-
.foregroundColor(.accentColor)
284285
}
285286
}
286287
.contentShape(Rectangle())
287288
}
288289
.buttonStyle(.plain)
289-
290+
290291
Button {
291292
withAnimation {
292293
model.favorited.toggle()
@@ -303,33 +304,34 @@ struct ModelSelectionRow: View {
303304
struct HighlightedText: View {
304305
let text: String
305306
let keywords: [String]
306-
307+
307308
var body: some View {
308309
if keywords.isEmpty {
309310
Text(text)
310311
} else {
311312
Text(attributedString)
312313
}
313314
}
314-
315+
315316
private var attributedString: AttributedString {
316317
var attributed = AttributedString(text)
317-
318+
318319
let lowercasedText = text.lowercased()
319-
320+
320321
for keyword in keywords {
321322
let lowercasedKeyword = keyword.lowercased()
322323
var searchRange = lowercasedText.startIndex..<lowercasedText.endIndex
323-
324-
while let range = lowercasedText.range(of: lowercasedKeyword, options: [], range: searchRange) {
324+
325+
while let range = lowercasedText.range(of: lowercasedKeyword, options: [], range: searchRange)
326+
{
325327
if let attributedRange = Range(range, in: attributed) {
326328
attributed[attributedRange].backgroundColor = .yellow.opacity(0.3)
327329
attributed[attributedRange].font = .body.bold()
328330
}
329331
searchRange = range.upperBound..<lowercasedText.endIndex
330332
}
331333
}
332-
334+
333335
return attributed
334336
}
335337
}

0 commit comments

Comments
 (0)