@@ -163,6 +163,119 @@ const SearchResultsDisplay = React.memo(({cohortsData, searchTerms, searchMode,
163163
164164SearchResultsDisplay . displayName = 'SearchResultsDisplay' ;
165165
166+ // Component to show equivalent variable names based on shared concept_codes
167+ const EquivalentVariableNames = React . memo ( ( { cohortsData, searchTerms, searchMode, searchScope} : {
168+ cohortsData : Record < string , Cohort > ,
169+ searchTerms : string [ ] ,
170+ searchMode : 'or' | 'and' | 'exact' ,
171+ searchScope : 'cohorts' | 'variables' | 'all'
172+ } ) => {
173+ const [ expanded , setExpanded ] = useState ( false ) ;
174+
175+ const equivalentNames = useMemo ( ( ) => {
176+ if ( searchTerms . length === 0 ) return null ;
177+ // Only relevant when searching variables or all
178+ if ( searchScope === 'cohorts' ) return null ;
179+
180+ // Step 1: Find variables whose var_name matches the query and collect their concept_codes
181+ const matchedConceptCodes = new Map < string , Set < string > > ( ) ; // concept_code -> set of matched var_names
182+
183+ Object . entries ( cohortsData ) . forEach ( ( [ _cohortId , cohortData ] ) => {
184+ Object . entries ( cohortData . variables || { } ) . forEach ( ( [ varName , varData ] ) => {
185+ const nameMatches = matchesSearchTerms ( varName , searchTerms , searchMode ) ;
186+ if ( nameMatches && varData . concept_code ) {
187+ const code = varData . concept_code . trim ( ) ;
188+ if ( code ) {
189+ if ( ! matchedConceptCodes . has ( code ) ) {
190+ matchedConceptCodes . set ( code , new Set ( ) ) ;
191+ }
192+ matchedConceptCodes . get ( code ) ! . add ( varName ) ;
193+ }
194+ }
195+ } ) ;
196+ } ) ;
197+
198+ if ( matchedConceptCodes . size === 0 ) return null ;
199+
200+ // Step 2: For each concept_code, find ALL variable names across all cohorts, grouped by cohort
201+ const result : { conceptCode : string ; conceptName : string | null ; namesByCohort : { cohortId : string ; names : string [ ] ; isMatched : boolean } [ ] } [ ] = [ ] ;
202+
203+ matchedConceptCodes . forEach ( ( matchedVarNames , conceptCode ) => {
204+ let conceptName : string | null = null ;
205+ const cohortEntries : { cohortId : string ; names : string [ ] ; isMatched : boolean } [ ] = [ ] ;
206+
207+ Object . entries ( cohortsData ) . forEach ( ( [ cohortId , cohortData ] ) => {
208+ const namesInCohort : string [ ] = [ ] ;
209+ Object . entries ( cohortData . variables || { } ) . forEach ( ( [ varName , varData ] ) => {
210+ if ( varData . concept_code && varData . concept_code . trim ( ) === conceptCode ) {
211+ namesInCohort . push ( varName ) ;
212+ if ( ! conceptName && varData . concept_name ) {
213+ conceptName = varData . concept_name ;
214+ }
215+ }
216+ } ) ;
217+ if ( namesInCohort . length > 0 ) {
218+ // A cohort is "matched" if any of its names were the ones that triggered the search hit
219+ const hasMatchedName = namesInCohort . some ( n => matchedVarNames . has ( n ) ) ;
220+ cohortEntries . push ( { cohortId, names : namesInCohort . sort ( ) , isMatched : hasMatchedName } ) ;
221+ }
222+ } ) ;
223+
224+ // Sort: cohorts with non-matched (equivalent) names first, matched cohorts last
225+ cohortEntries . sort ( ( a , b ) => {
226+ if ( a . isMatched === b . isMatched ) return a . cohortId . localeCompare ( b . cohortId ) ;
227+ return a . isMatched ? 1 : - 1 ;
228+ } ) ;
229+
230+ // Only show if there's at least one name beyond the matched ones
231+ const hasEquivalent = cohortEntries . some ( e =>
232+ e . names . some ( n => ! matchedVarNames . has ( n ) )
233+ ) ;
234+ if ( hasEquivalent ) {
235+ result . push ( { conceptCode, conceptName, namesByCohort : cohortEntries } ) ;
236+ }
237+ } ) ;
238+
239+ return result . length > 0 ? result : null ;
240+ } , [ cohortsData , searchTerms , searchMode , searchScope ] ) ;
241+
242+ if ( ! equivalentNames ) return null ;
243+
244+ return (
245+ < div className = "mt-2" >
246+ < button
247+ onClick = { ( ) => setExpanded ( ! expanded ) }
248+ className = "btn btn-xs btn-outline btn-info"
249+ >
250+ { expanded ? '▾ Hide' : '▸ Show' } equivalent variable names
251+ </ button >
252+ { expanded && (
253+ < div className = "mt-2 space-y-2" >
254+ { equivalentNames . map ( ( { conceptCode, conceptName, namesByCohort} ) => (
255+ < div key = { conceptCode } className = "p-2 bg-base-100 rounded-lg border border-base-300 text-sm" >
256+ < div className = "font-semibold text-gray-700 dark:text-gray-300 mb-1" >
257+ Standard code: < span className = "text-primary" > { conceptCode } </ span >
258+ { conceptName && < span className = "ml-1 text-gray-500" > ({ conceptName } )</ span > }
259+ </ div >
260+ < div className = "space-y-1" >
261+ { namesByCohort . map ( ( { cohortId, names, isMatched} ) => (
262+ < div key = { cohortId } className = { isMatched ? 'text-gray-400 dark:text-gray-500' : '' } >
263+ < span className = "font-medium" > { cohortId } :</ span > { ' ' }
264+ { names . join ( ', ' ) }
265+ { isMatched && < span className = "ml-1 text-xs italic" > (matched)</ span > }
266+ </ div >
267+ ) ) }
268+ </ div >
269+ </ div >
270+ ) ) }
271+ </ div >
272+ ) }
273+ </ div >
274+ ) ;
275+ } ) ;
276+
277+ EquivalentVariableNames . displayName = 'EquivalentVariableNames' ;
278+
166279// Helper function to format participants value for display in tags
167280const formatParticipantsForTag = ( value : string | number | null | undefined ) : string => {
168281 if ( ! value ) return '' ;
@@ -650,7 +763,7 @@ export default function CohortsList() {
650763
651764 { /* Search Results Display */ }
652765 { searchInput . trim ( ) && (
653- < div className = "mt-2 p-2 bg-base-200 rounded-lg text-sm " >
766+ < div className = "mt-2 p-2 bg-base-200 rounded-lg text-base " >
654767 < div className = "flex items-start gap-3" >
655768 < span className = "text-gray-600 dark:text-gray-400 mt-0.5" > 🔍</ span >
656769 < div className = "flex-1" >
@@ -660,6 +773,12 @@ export default function CohortsList() {
660773 searchMode = { searchMode }
661774 searchScope = { searchScope }
662775 />
776+ < EquivalentVariableNames
777+ cohortsData = { cohortsData as Record < string , Cohort > }
778+ searchTerms = { searchTerms }
779+ searchMode = { searchMode }
780+ searchScope = { searchScope }
781+ />
663782 </ div >
664783 < button
665784 onClick = { ( ) => {
0 commit comments