11// Created for Chato in 2025
22
3- import os
43import SwiftData
54import SwiftUI
5+ import os
66
77struct 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 {
303304struct 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