@@ -20,17 +20,24 @@ const HighlightedText = ({text, searchTerms, searchMode}: {text: string, searchT
2020 return < span dangerouslySetInnerHTML = { { __html : highlightedHtml } } /> ;
2121} ;
2222
23+ // Normalize text for fuzzy matching: split camelCase and replace common separators with spaces
24+ const normalizeText = ( text : string ) : string =>
25+ text
26+ . replace ( / ( [ a - z ] ) ( [ A - Z ] ) / g, '$1 $2' ) // camelCase: aB → a B
27+ . replace ( / ( [ A - Z ] + ) ( [ A - Z ] [ a - z ] ) / g, '$1 $2' ) // acronym boundary: ABc → A Bc
28+ . replace ( / [ _ \- . , — ] / g, ' ' ) ;
29+
2330// Helper function to check if text matches search terms (reusable)
2431const matchesSearchTerms = ( text : string | null | undefined , searchTerms : string [ ] , searchMode : 'or' | 'and' | 'exact' ) : boolean => {
2532 if ( ! text || searchTerms . length === 0 ) return false ;
26- const textLower = String ( text ) . toLowerCase ( ) ;
33+ const textNorm = normalizeText ( String ( text ) . toLowerCase ( ) ) ;
2734
2835 if ( searchMode === 'exact' ) {
29- return textLower . includes ( searchTerms . join ( ' ' ) . toLowerCase ( ) ) ;
36+ return textNorm . includes ( normalizeText ( searchTerms . join ( ' ' ) . toLowerCase ( ) ) ) ;
3037 } else if ( searchMode === 'and' ) {
31- return searchTerms . every ( term => textLower . includes ( term . toLowerCase ( ) ) ) ;
38+ return searchTerms . every ( term => textNorm . includes ( normalizeText ( term . toLowerCase ( ) ) ) ) ;
3239 } else { // 'or' mode
33- return searchTerms . some ( term => textLower . includes ( term . toLowerCase ( ) ) ) ;
40+ return searchTerms . some ( term => textNorm . includes ( normalizeText ( term . toLowerCase ( ) ) ) ) ;
3441 }
3542} ;
3643
@@ -119,9 +126,21 @@ const SearchResultsDisplay = React.memo(({cohortsData, searchTerms, searchMode,
119126
120127 // Format cohort metadata results: "CohortA (study objective, morbidity), CohortB (institution)"
121128 const formatCohortResults = ( ) => {
122- return results . matchedCohorts
123- . map ( ( { cohortId, sections} ) => `${ cohortId } (${ sections . join ( ', ' ) } )` )
124- . join ( ', ' ) ;
129+ return (
130+ < span >
131+ { results . matchedCohorts . map ( ( { cohortId, sections} , idx ) => (
132+ < span key = { cohortId } >
133+ { cohortId } ({ sections . map ( ( s , i ) => (
134+ < span key = { i } >
135+ < em > { s } </ em >
136+ { i < sections . length - 1 && ', ' }
137+ </ span >
138+ ) ) } )
139+ { idx < results . matchedCohorts . length - 1 && ', ' }
140+ </ span >
141+ ) ) }
142+ </ span >
143+ ) ;
125144 } ;
126145
127146 // Format variable results: "var1, var2 (CohortA); var3 (CohortB)"
@@ -138,7 +157,7 @@ const SearchResultsDisplay = React.memo(({cohortsData, searchTerms, searchMode,
138157 Search matched < strong className = "text-primary" > { results . matchedCohorts . length } </ strong > cohort{ results . matchedCohorts . length !== 1 ? 's' : '' } metadata
139158 </ span >
140159 { results . matchedCohorts . length > 0 && (
141- < div className = "mt-1 text-xs text- gray-600 dark:text-gray-400" >
160+ < div className = "mt-1 text-gray-600 dark:text-gray-400" >
142161 < strong > Studies metadata:</ strong > { formatCohortResults ( ) }
143162 </ div >
144163 ) }
@@ -153,7 +172,7 @@ const SearchResultsDisplay = React.memo(({cohortsData, searchTerms, searchMode,
153172 Search matched < strong className = "text-primary" > { results . totalVariables } </ strong > variable description{ results . totalVariables !== 1 ? 's' : '' } in < strong className = "text-primary" > { cohortsWithVarMatches } </ strong > cohort{ cohortsWithVarMatches !== 1 ? 's' : '' }
154173 </ span >
155174 { results . totalVariables > 0 && (
156- < div className = "mt-1 text-xs text- gray-600 dark:text-gray-400 max-h-20 overflow-y-auto" >
175+ < div className = "mt-1 text-gray-600 dark:text-gray-400 max-h-20 overflow-y-auto" >
157176 < strong > Variables:</ strong > { formatVariableResults ( ) }
158177 </ div >
159178 ) }
@@ -168,7 +187,7 @@ const SearchResultsDisplay = React.memo(({cohortsData, searchTerms, searchMode,
168187 Search matched < strong className = "text-primary" > { results . matchedCohorts . length } </ strong > cohort{ results . matchedCohorts . length !== 1 ? 's' : '' } metadata and < strong className = "text-primary" > { results . totalVariables } </ strong > variable description{ results . totalVariables !== 1 ? 's' : '' } in < strong className = "text-primary" > { cohortsWithVarMatches } </ strong > cohort{ cohortsWithVarMatches !== 1 ? 's' : '' }
169188 </ span >
170189 { ( results . matchedCohorts . length > 0 || results . totalVariables > 0 ) && (
171- < div className = "mt-1 text-xs text- gray-600 dark:text-gray-400 max-h-24 overflow-y-auto" >
190+ < div className = "mt-1 text-gray-600 dark:text-gray-400 max-h-24 overflow-y-auto" >
172191 { results . matchedCohorts . length > 0 && (
173192 < div > < strong > Studies metadata:</ strong > { formatCohortResults ( ) } </ div >
174193 ) }
@@ -202,7 +221,7 @@ const EquivalentVariableNames = React.memo(({cohortsData, searchTerms, searchMod
202221
203222 Object . entries ( cohortsData ) . forEach ( ( [ _cohortId , cohortData ] ) => {
204223 Object . entries ( cohortData . variables || { } ) . forEach ( ( [ varName , varData ] ) => {
205- const nameMatches = matchesSearchTerms ( varName , searchTerms , 'and' ) ;
224+ const nameMatches = matchesSearchTerms ( varName , searchTerms , 'and' ) || matchesSearchTerms ( varData . concept_name , searchTerms , 'and' ) ;
206225 if ( nameMatches && varData . concept_code ) {
207226 const code = varData . concept_code . trim ( ) . toUpperCase ( ) ;
208227 if ( code ) {
@@ -787,7 +806,7 @@ export default function CohortsList() {
787806
788807 { /* Search Results Display */ }
789808 { searchInput . trim ( ) && (
790- < div className = "mt-2 p-2 bg-base-200 rounded-lg text-lg " >
809+ < div className = "mt-2 p-2 bg-base-200 rounded-lg text-base " >
791810 < div className = "flex items-start gap-3" >
792811 < span className = "text-gray-600 dark:text-gray-400 mt-0.5" > 🔍</ span >
793812 < div className = "flex-1" >
0 commit comments