@@ -37,24 +37,54 @@ struct ModelSelectionContent: View {
3737 @Binding var favoritesExpanded : Bool
3838 let dismiss : DismissAction
3939
40- private var favoritedModels : [ ModelEntity ] {
40+ private func favoritedModels( ) -> [ ModelEntity ] {
4141 let filtered = allModels. filter { $0. favorited }
42- return ModelEntity . smartSort ( filtered)
42+ let sorted = ModelEntity . smartSort ( filtered)
43+ return sorted
44+ }
45+
46+ private func searchKeywords( ) -> [ String ] {
47+ return parseSearchText ( searchText)
4348 }
4449
4550 private var filteredModels : [ ModelEntity ] {
46- if searchText. isEmpty {
51+ let keywords = searchKeywords ( )
52+ if keywords. isEmpty {
4753 return allModels
4854 }
49- return allModels. filter { model in
50- model. resolvedName. localizedStandardContains ( searchText) ||
51- model. modelId. localizedStandardContains ( searchText)
55+ let filtered = allModels. filter { model in
56+ let nameMatches = matchesKeywords ( text: model. resolvedName, keywords: keywords)
57+ let idMatches = matchesKeywords ( text: model. modelId, keywords: keywords)
58+ return nameMatches || idMatches
59+ }
60+ return filtered
61+ }
62+
63+ private func parseSearchText( _ text: String ) -> [ String ] {
64+ let separators = CharacterSet ( charactersIn: " , " )
65+ let keywords = text. components ( separatedBy: separators)
66+ . map { $0. trimmingCharacters ( in: . whitespaces) }
67+ . filter { !$0. isEmpty }
68+ return keywords
69+ }
70+
71+ private func matchesKeywords( text: String , keywords: [ String ] ) -> Bool {
72+ guard !keywords. isEmpty else { return true }
73+
74+ let lowercasedText = text. lowercased ( )
75+ return keywords. contains { keyword in
76+ lowercasedText. contains ( keyword. lowercased ( ) )
5277 }
5378 }
5479
5580 private var groupedProviders : [ ( provider: Provider , models: [ ModelEntity ] ) ] {
56- let filtered = searchText. isEmpty ? allModels : filteredModels
57- return filtered. groupedByProvider ( )
81+ let modelsToGroup : [ ModelEntity ]
82+ if searchText. isEmpty {
83+ modelsToGroup = allModels
84+ } else {
85+ modelsToGroup = filteredModels
86+ }
87+ return modelsToGroup. groupedByProvider ( )
5888 }
5989
6090 var body : some View {
@@ -83,28 +113,26 @@ struct ModelSelectionContent: View {
83113
84114 @ViewBuilder
85115 private var favoritesSection : some View {
86- if !favoritedModels. isEmpty && searchText. isEmpty {
87- favoritesSectionContent
88- }
89- }
90-
91- private var favoritesSectionContent : some View {
92- DisclosureGroup (
93- isExpanded: $favoritesExpanded
94- ) {
95- ForEach ( favoritedModels) { model in
96- ModelSelectionRow (
97- model: model,
98- isSelected: model. id == chatOption. model? . id,
99- showProvider: true
100- ) {
101- selectModel ( model)
116+ let favorited = favoritedModels ( )
117+ if !favorited. isEmpty && searchText. isEmpty {
118+ DisclosureGroup (
119+ isExpanded: $favoritesExpanded
120+ ) {
121+ ForEach ( favorited) { model in
122+ ModelSelectionRow (
123+ model: model,
124+ isSelected: model. id == chatOption. model? . id,
125+ showProvider: true ,
126+ searchKeywords: searchKeywords ( )
127+ ) {
128+ selectModel ( model)
129+ }
130+ . id ( model. id)
102131 }
103- . id ( model. id)
132+ } label: {
133+ Label ( " Favorites " , systemImage: " star.fill " )
134+ . foregroundColor ( . yellow)
104135 }
105- } label: {
106- Label ( " Favorites " , systemImage: " star.fill " )
107- . foregroundColor ( . yellow)
108136 }
109137 }
110138
@@ -123,7 +151,8 @@ struct ModelSelectionContent: View {
123151 ModelSelectionRow (
124152 model: model,
125153 isSelected: model. id == chatOption. model? . id,
126- showProvider: false
154+ showProvider: false ,
155+ searchKeywords: searchKeywords ( )
127156 ) {
128157 selectModel ( model)
129158 }
@@ -179,8 +208,9 @@ struct ModelSelectionContent: View {
179208 guard expandedProviders. isEmpty else { return }
180209
181210 // Check if selected model exists and is not in favorites
211+ let favorited = favoritedModels ( )
182212 if let selectedModel = chatOption. model,
183- !favoritedModels . contains ( where: { $0. id == selectedModel. id } ) {
213+ !favorited . contains ( where: { $0. id == selectedModel. id } ) {
184214 // Find and expand only the provider containing the selected model
185215 if let providerGroup = groupedProviders. first ( where: { group in
186216 group. models. contains ( where: { $0. id == selectedModel. id } )
@@ -210,52 +240,96 @@ struct ModelSelectionRow: View {
210240 let model : ModelEntity
211241 let isSelected : Bool
212242 let showProvider : Bool
243+ let searchKeywords : [ String ]
213244 let action : ( ) -> Void
214245
215246 var body : some View {
216- Button ( action: action) {
217- HStack ( spacing: 12 ) {
218- if model. favorited {
219- Image ( systemName: " star.fill " )
220- . foregroundColor ( . yellow)
221- . font ( . caption)
222- }
223-
224- VStack ( alignment: . leading, spacing: 4 ) {
225- Text ( model. resolvedName)
247+ HStack ( spacing: 12 ) {
248+ Button ( action: action) {
249+ HStack ( spacing: 12 ) {
250+ VStack ( alignment: . leading, spacing: 4 ) {
251+ HighlightedText (
252+ text: model. resolvedName,
253+ keywords: searchKeywords
254+ )
226255 . font ( . body)
227256 . foregroundColor ( . primary)
228-
229- HStack ( spacing: 8 ) {
230- if showProvider {
231- Label ( model. provider. displayName, systemImage: model. provider. iconName)
232- . font ( . caption)
233- . foregroundColor ( . secondary)
234- }
235257
236- if model. isCustom {
237- Label ( " Custom " , systemImage: " wrench " )
238- . font ( . caption2)
239- . foregroundColor ( . blue)
240- }
241-
242- if let contextLength = model. contextLength {
243- Text ( " \( contextLength) k " )
244- . font ( . caption)
245- . foregroundColor ( . secondary)
258+ HStack ( spacing: 8 ) {
259+ if showProvider {
260+ Label ( model. provider. displayName, systemImage: model. provider. iconName)
261+ . font ( . caption)
262+ . foregroundColor ( . secondary)
263+ }
264+
265+ if model. isCustom {
266+ Label ( " Custom " , systemImage: " wrench " )
267+ . font ( . caption2)
268+ . foregroundColor ( . blue)
269+ }
270+
271+ if let contextLength = model. contextLength {
272+ Text ( " \( contextLength) k " )
273+ . font ( . caption)
274+ . foregroundColor ( . secondary)
275+ }
246276 }
247277 }
278+
279+ Spacer ( )
280+
281+ if isSelected {
282+ Image ( systemName: " checkmark " )
283+ . foregroundColor ( . accentColor)
284+ }
248285 }
249-
250- Spacer ( )
251-
252- if isSelected {
253- Image ( systemName: " checkmark " )
254- . foregroundColor ( . accentColor)
286+ . contentShape ( Rectangle ( ) )
287+ }
288+ . buttonStyle ( . plain)
289+
290+ Button {
291+ withAnimation {
292+ model. favorited. toggle ( )
255293 }
294+ } label: {
295+ Image ( systemName: model. favorited ? " star.fill " : " star " )
296+ . foregroundColor ( model. favorited ? . yellow : . gray)
256297 }
257- . contentShape ( Rectangle ( ) )
298+ . buttonStyle ( . plain )
258299 }
259- . buttonStyle ( . plain)
300+ }
301+ }
302+
303+ struct HighlightedText : View {
304+ let text : String
305+ let keywords : [ String ]
306+
307+ var body : some View {
308+ if keywords. isEmpty {
309+ Text ( text)
310+ } else {
311+ Text ( attributedString)
312+ }
313+ }
314+
315+ private var attributedString : AttributedString {
316+ var attributed = AttributedString ( text)
317+
318+ let lowercasedText = text. lowercased ( )
319+
320+ for keyword in keywords {
321+ let lowercasedKeyword = keyword. lowercased ( )
322+ var searchRange = lowercasedText. startIndex..< lowercasedText. endIndex
323+
324+ while let range = lowercasedText. range ( of: lowercasedKeyword, options: [ ] , range: searchRange) {
325+ if let attributedRange = Range ( range, in: attributed) {
326+ attributed [ attributedRange] . backgroundColor = . yellow. opacity ( 0.3 )
327+ attributed [ attributedRange] . font = . body. bold ( )
328+ }
329+ searchRange = range. upperBound..< lowercasedText. endIndex
330+ }
331+ }
332+
333+ return attributed
260334 }
261335}
0 commit comments