Skip to content

Commit ad6bc0a

Browse files
authored
Merge pull request #51 from OpenConceptLab/issues#2355
OpenConceptLab/ocl_issues#2355 | added target metadata columns
2 parents 846ce1f + 5292f8a commit ad6bc0a

8 files changed

Lines changed: 245 additions & 62 deletions

File tree

src/components/map-projects/Candidates.jsx

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import {
5454
conceptBelongsToTargetRepo,
5555
sortRowViews,
5656
conceptForMapping,
57-
resolveAICandidateID
57+
getAIAnalysisCandidateIDs
5858
} from './viewBuilders.js'
5959

6060
const getRowProgressLabel = (stageMap, algos) => {
@@ -452,21 +452,8 @@ const Candidates = ({rowIndex, alert, setAlert, rowState, conceptCache, targetCa
452452
? undefined
453453
: analysisArray[analysisPage]
454454
const selectedAnalysisForGrouping = selectedAnalysis || analysisArray[analysisCount - 1]
455-
const primary = selectedAnalysis?.output?.primary_candidate || selectedAnalysis?.primary_candidate
456-
const AIRecommendedCandidateId = resolveAICandidateID(primary, conceptCache)
457-
const alternateCandidates = selectedAnalysis?.output?.alternative_candidates || selectedAnalysis?.alternative_candidates || []
458-
const AIAlternateCandidateIds = [...new Set(alternateCandidates
459-
.map(candidate => resolveAICandidateID(candidate, conceptCache))
460-
.filter(id => id && id !== AIRecommendedCandidateId))]
461-
const groupedAnalysisPrimaryId = resolveAICandidateID(
462-
selectedAnalysisForGrouping?.output?.primary_candidate || selectedAnalysisForGrouping?.primary_candidate,
463-
conceptCache
464-
)
465-
const groupedAnalysisAlternateIds = [...new Set(
466-
(selectedAnalysisForGrouping?.output?.alternative_candidates || selectedAnalysisForGrouping?.alternative_candidates || [])
467-
.map(candidate => resolveAICandidateID(candidate, conceptCache))
468-
.filter(id => id && id !== groupedAnalysisPrimaryId)
469-
)]
455+
const { primaryCandidateId: AIRecommendedCandidateId, alternateCandidateIds: AIAlternateCandidateIds } = getAIAnalysisCandidateIDs(selectedAnalysis, conceptCache)
456+
const { primaryCandidateId: groupedAnalysisPrimaryId, alternateCandidateIds: groupedAnalysisAlternateIds } = getAIAnalysisCandidateIDs(selectedAnalysisForGrouping, conceptCache)
470457

471458
// Quality (score-grouped) view shows ONLY target-repo concepts. Bridge
472459
// intermediaries live in algorithm view as metadata-about-the-target;

src/components/map-projects/MapProject.jsx

Lines changed: 173 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ import { OperationsContext } from '../app/LayoutContext';
7676
import APIService, { isTransientNetworkError, retryWithBackoff } from '../../services/APIService';
7777
import { buildAttributionHeaders, buildConfigSnapshot, summarizeRunCompletion } from '../../services/attribution'
7878
import { highlightTexts, dropVersion, getCurrentUser, hasAuthGroup, downloadObject, currentUserToken } from '../../common/utils';
79-
import { WHITE, SURFACE_COLORS } from '../../common/colors';
79+
import { WHITE, SURFACE_COLORS, TEXT_GRAY } from '../../common/colors';
8080

8181
import { useDoubleClick } from '../common/useDoubleClick'
8282
import CloseIconButton from '../common/CloseIconButton';
8383
import ConceptDetailsPanel from './ConceptDetailsPanel'
8484
import { buildPanelPayload } from './openConceptPanel'
8585
import LoaderDialog from '../common/LoaderDialog'
8686
import Error403 from '../errors/Error403'
87-
import { HEADERS, SEMANTIC_SEARCH_HEADERS, ROW_STATES, VIEWS, DECISION_TABS, ROW_STAGES, PROMPTS_KEY_DEFAULT, PROMPTS_ACTION_TYPE_DEFAULT } from './constants'
87+
import { HEADERS, SEMANTIC_SEARCH_HEADERS, ROW_STATES, VIEWS, DECISION_TABS, ROW_STAGES, PROMPTS_KEY_DEFAULT, PROMPTS_ACTION_TYPE_DEFAULT, SCORES_COLOR } from './constants'
8888
import MapProjectDeleteConfirmDialog from './MapProjectDeleteConfirmDialog';
8989
import ConfigurationForm from './ConfigurationForm'
9090
import Controls from './Controls'
@@ -108,7 +108,7 @@ import { DEFAULT_ENCODER_MODEL } from './rerankerModels'
108108
import { normalizeAlgorithmInvocation, lookupStatusRank, buildRecommendableConceptEntry, stripConstantClassAndDatatype, buildLookupConceptUrl } from './normalizers'
109109
import { parseConceptKey } from './conceptKey'
110110
import { getDefaultTargetRepoVersion, getProjectTargetRepoVersion, getTargetRepoVersionFromUrl, getTargetRepoVersionId } from './projectTargetRepo'
111-
import { buildBridgeTargetDownloadEntries, buildQualityRowViews, conceptBelongsToTargetRepo, conceptForMapping, formatBridgeTargetDownloadEntry, resolveAICandidateID } from './viewBuilders.js'
111+
import { buildBridgeTargetDownloadEntries, buildQualityRowViews, conceptBelongsToTargetRepo, conceptForMapping, formatBridgeTargetDownloadEntry, resolveAICandidateID, getScoreDetails, getAIAnalysisCandidateIDs } from './viewBuilders.js'
112112

113113
import './MapProject.scss'
114114
import '../common/ResizablePanel.scss'
@@ -823,6 +823,25 @@ const MapProject = () => {
823823
return _columns
824824
}
825825

826+
const getRowStateLabel = index => VIEWS[getStateFromIndex(index)]?.label || ''
827+
const getRowScoreDetails = index => getScoreDetails({search_meta: mapSelected[index]?.search_meta}, candidatesScore)
828+
const getRowMatchQualityLabel = index => {
829+
const qualityBucket = getRowScoreDetails(index).qualityBucket
830+
return qualityBucket ? startCase(qualityBucket) : t('map_project.unranked')
831+
}
832+
833+
const getRowAIIndicatorMeta = (index, targetConcept) => {
834+
const status = rowStageRef.current[index]?.recommend
835+
const { latestAnalysis, primaryCandidateId, alternateCandidateIds } = getAIAnalysisCandidateIDs(analysis[index], conceptCacheRef.current)
836+
const targetCode = targetConcept?.id || targetConcept?.reference?.code
837+
return {
838+
status,
839+
latestAnalysis,
840+
isPrimaryMatch: Boolean(targetCode && primaryCandidateId && targetCode === primaryCandidateId),
841+
isAlternateMatch: Boolean(targetCode && alternateCandidateIds.includes(targetCode)),
842+
}
843+
}
844+
826845
const getColumnsForTable = () => {
827846
let cols = []
828847
forEach(columns, (column, idx) => {
@@ -898,42 +917,156 @@ const MapProject = () => {
898917
cols.push({
899918
field: '_targetCode_',
900919
headerName: t('map_project.target_code'),
901-
width: columnWidth['_targetCode_'] || 300,
920+
width: columnWidth['_targetCode_'] || 360,
902921
renderCell: params => {
903922
const targetConcept = getConcept(mapSelected[params.row.__index])
923+
const aiMeta = AI_ASSISTANT_API_URL ? getRowAIIndicatorMeta(params.row.__index, targetConcept) : null
904924
if(targetConcept?.id) {
905-
return <Concept key={`${params.row.__index}-${targetConcept.id}`} sx={{padding: 0}} repoVersion={repoVersion} notClickable firstChild concept={targetConcept} noScore onCardClick={false} noSynonymPrefix asTarget />
925+
return (
926+
<span style={{display: 'flex', alignItems: 'flex-start', gap: '6px', width: '100%'}}>
927+
{
928+
aiMeta?.status === 0 ?
929+
<Tooltip title={t('map_project.ai_recommendation_running')}>
930+
<span style={{display: 'inline-flex', paddingTop: '7px'}}>
931+
<CircularProgress size={14} />
932+
</span>
933+
</Tooltip> :
934+
aiMeta?.status === -2 ?
935+
<Tooltip title={t('map_project.ai_recommendation_failed_retry')}>
936+
<IconButton size='small' sx={{padding: '4px', marginTop: '3px'}} onClick={e => { e.stopPropagation(); fetchRecommendation(params.row) }}>
937+
<AssistantIcon color='error' fontSize='small' />
938+
</IconButton>
939+
</Tooltip> :
940+
aiMeta?.latestAnalysis ?
941+
<Tooltip title={aiMeta.isPrimaryMatch ? t('map_project.ai_recommended') : (aiMeta.isAlternateMatch ? t('map_project.ai_alternate') : t('map_project.ai_analysis'))}>
942+
<span style={{display: 'inline-flex', paddingTop: '7px'}}>
943+
<AssistantIcon color={aiMeta.isPrimaryMatch ? 'primary' : 'warning'} fontSize='small' />
944+
</span>
945+
</Tooltip> :
946+
null
947+
}
948+
<span style={{minWidth: 0, flex: 1}}>
949+
<Concept key={`${params.row.__index}-${targetConcept.id}`} sx={{padding: 0}} repoVersion={repoVersion} notClickable firstChild concept={targetConcept} noScore onCardClick={false} noSynonymPrefix asTarget />
950+
</span>
951+
</span>
952+
)
906953
}
907954
}
908955
})
909-
if(AI_ASSISTANT_API_URL) {
910-
cols.push({
911-
field: '_aiRecommendStatus_',
912-
headerName: '',
913-
width: 48,
914-
sortable: false,
915-
filterable: false,
916-
disableColumnMenu: true,
917-
renderCell: params => {
918-
const status = rowStageRef.current[params.row.__index]?.recommend
919-
if(status === 0)
920-
return (
921-
<Tooltip title={t('map_project.ai_recommendation_running')}>
922-
<CircularProgress size={16} />
923-
</Tooltip>
924-
)
925-
if(status === -2)
926-
return (
927-
<Tooltip title={t('map_project.ai_recommendation_failed_retry')}>
928-
<IconButton size='small' onClick={e => { e.stopPropagation(); fetchRecommendation(params.row) }}>
929-
<AssistantIcon color='error' fontSize='small' />
930-
</IconButton>
931-
</Tooltip>
932-
)
956+
cols.push({
957+
field: '_rowState_',
958+
headerName: t('map_project.state'),
959+
width: columnWidth['_rowState_'] || 130,
960+
valueGetter: (_, row) => getRowStateLabel(row.__index),
961+
renderCell: params => {
962+
const state = VIEWS[getStateFromIndex(params.row.__index)]
963+
return (
964+
<Chip
965+
size='small'
966+
icon={state?.icon}
967+
label={state?.label || ''}
968+
color={state?.color || 'default'}
969+
variant='outlined'
970+
/>
971+
)
972+
}
973+
})
974+
cols.push({
975+
field: '_matchQuality_',
976+
headerName: t('map_project.group_by_match_quality'),
977+
width: columnWidth['_matchQuality_'] || 160,
978+
align: 'left',
979+
headerAlign: 'left',
980+
valueGetter: (_, row) => getRowMatchQualityLabel(row.__index),
981+
renderCell: params => {
982+
const details = getRowScoreDetails(params.row.__index)
983+
const label = getRowMatchQualityLabel(params.row.__index)
984+
return (
985+
<Chip
986+
size='small'
987+
label={label}
988+
sx={details.qualityBucket ? {backgroundColor: SCORES_COLOR[details.qualityBucket], color: TEXT_GRAY} : undefined}
989+
variant={details.qualityBucket ? 'filled' : 'outlined'}
990+
/>
991+
)
992+
}
993+
})
994+
cols.push({
995+
field: '_bestScore_',
996+
headerName: t('search.search_score'),
997+
width: columnWidth['_bestScore_'] || 130,
998+
align: 'left',
999+
headerAlign: 'left',
1000+
type: 'number',
1001+
valueGetter: (_, row) => getRowScoreDetails(row.__index).percentile,
1002+
renderCell: params => {
1003+
const details = getRowScoreDetails(params.row.__index)
1004+
return details.rerankScore || ''
1005+
}
1006+
})
1007+
cols.push({
1008+
field: '_rawScore_',
1009+
headerName: t('search.search_raw_score'),
1010+
width: columnWidth['_rawScore_'] || 130,
1011+
align: 'left',
1012+
headerAlign: 'left',
1013+
type: 'number',
1014+
valueGetter: (_, row) => getRowScoreDetails(row.__index).score,
1015+
renderCell: params => {
1016+
const details = getRowScoreDetails(params.row.__index)
1017+
return details.algoScore || ''
1018+
}
1019+
})
1020+
cols.push({
1021+
field: '_mapType_',
1022+
headerName: t('map_project.map_type'),
1023+
width: columnWidth['_mapType_'] || 150,
1024+
valueGetter: (_, row) => mapSelected[row.__index] ? (mapTypes[row.__index] || '') : '',
1025+
renderCell: params => {
1026+
if(!mapSelected[params.row.__index])
9331027
return null
934-
}
935-
})
936-
}
1028+
const mapType = mapTypes[params.row.__index]
1029+
return mapType ? (
1030+
<Chip
1031+
size='small'
1032+
label={mapType}
1033+
variant='outlined'
1034+
color='primary'
1035+
/>
1036+
) : null
1037+
}
1038+
})
1039+
cols.push({
1040+
field: '_algorithms_',
1041+
headerName: t('map_project.algorithms'),
1042+
width: columnWidth['_algorithms_'] || 220,
1043+
sortable: false,
1044+
valueGetter: (_, row) => {
1045+
const targetConcept = getConcept(mapSelected[row.__index])
1046+
const contributingAlgorithms = targetConcept?.search_meta?.contributing_algorithms || []
1047+
const contributingAlgorithmIds = targetConcept?.search_meta?.contributing_algorithm_ids || targetConcept?.contributingAlgorithmIds || []
1048+
return compact([
1049+
...contributingAlgorithms.map(algo => algo?.algorithm_id),
1050+
...contributingAlgorithmIds,
1051+
targetConcept?.search_meta?.algorithm
1052+
]).join(', ')
1053+
},
1054+
renderCell: params => {
1055+
const targetConcept = getConcept(mapSelected[params.row.__index])
1056+
const contributingAlgorithms = targetConcept?.search_meta?.contributing_algorithms || []
1057+
const contributingAlgorithmIds = targetConcept?.search_meta?.contributing_algorithm_ids || targetConcept?.contributingAlgorithmIds || []
1058+
const algoIds = uniq(compact([
1059+
...contributingAlgorithms.map(algo => algo?.algorithm_id),
1060+
...contributingAlgorithmIds,
1061+
targetConcept?.search_meta?.algorithm
1062+
]))
1063+
return (
1064+
<span style={{display: 'inline-flex', gap: '4px', flexWrap: 'wrap', padding: '4px 0'}}>
1065+
{algoIds.map(algoId => <Chip key={algoId} size='small' label={algoId} variant='outlined' color='warning' />)}
1066+
</span>
1067+
)
1068+
}
1069+
})
9371070
return cols
9381071
}
9391072

@@ -3469,19 +3602,14 @@ const MapProject = () => {
34693602
// AI chip state as the current row. Fall back to the latest analysis
34703603
// available for this row when callers don't supply explicit ids.
34713604
const getCurrentRowAIIds = () => {
3472-
const rowAnalysis = analysis?.[rowIndex]
3473-
const analysisArray = Array.isArray(rowAnalysis) ? rowAnalysis : (rowAnalysis ? [rowAnalysis] : [])
3474-
const selectedAnalysis = analysisArray[analysisArray.length - 1]
3475-
if(!selectedAnalysis) return { recommendedId: undefined, alternateIds: [] }
3476-
3477-
const primary = selectedAnalysis?.output?.primary_candidate || selectedAnalysis?.primary_candidate
3478-
const recommendedId = resolveAICandidateID(primary, conceptCacheRef.current)
3479-
const alternateCandidates = selectedAnalysis?.output?.alternative_candidates || selectedAnalysis?.alternative_candidates || []
3480-
const alternateIds = [...new Set(alternateCandidates
3481-
.map(candidate => resolveAICandidateID(candidate, conceptCacheRef.current))
3482-
.filter(id => id && id !== recommendedId))]
3483-
3484-
return { recommendedId, alternateIds }
3605+
const { primaryCandidateId, alternateCandidateIds } = getAIAnalysisCandidateIDs(
3606+
analysis?.[rowIndex],
3607+
conceptCacheRef.current
3608+
)
3609+
return {
3610+
recommendedId: primaryCandidateId ?? undefined,
3611+
alternateIds: alternateCandidateIds
3612+
}
34853613
}
34863614

34873615
const openConceptPanel = (concept, options = {}) => {

src/components/map-projects/MapProject.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,16 @@
1717
.unmatched-row {
1818
border-left: 8px solid #bdbdbd;
1919
}
20+
21+
.MuiDataGrid-panel .MuiDataGrid-filterFormValueInput .MuiChip-root {
22+
background-color: #e5e1e6 !important;
23+
color: #47464f !important;
24+
}
25+
26+
.MuiDataGrid-panel .MuiDataGrid-filterFormValueInput .MuiChip-root .MuiChip-label {
27+
color: #47464f !important;
28+
}
29+
30+
.MuiDataGrid-panel .MuiDataGrid-filterFormValueInput .MuiChip-root .MuiChip-deleteIcon {
31+
color: #76758b !important;
32+
}

src/components/map-projects/__tests__/viewHelpers.test.js

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
conceptBelongsToTargetRepo,
1616
getScoreDetails,
1717
conceptForMapping,
18-
resolveAICandidateID
18+
resolveAICandidateID,
19+
getLatestAnalysisEntry,
20+
getAIAnalysisCandidateIDs
1921
} from '../viewBuilders.js'
2022

2123
const candidatesScore = { recommended: 80, available: 60 }
@@ -225,3 +227,34 @@ test('resolveAICandidateID: concept_key present but not in cache falls through t
225227
test('resolveAICandidateID: completely unidentified candidate returns null', () => {
226228
assert.equal(resolveAICandidateID({}, {}), null)
227229
})
230+
231+
test('getLatestAnalysisEntry: returns the last entry from analysis history', () => {
232+
const analysis = [{output: {recommendation: 'first'}}, {output: {recommendation: 'latest'}}]
233+
assert.deepEqual(getLatestAnalysisEntry(analysis), analysis[1])
234+
assert.equal(getLatestAnalysisEntry(null), null)
235+
})
236+
237+
test('getAIAnalysisCandidateIDs: resolves primary + alternate ids from latest analysis entry', () => {
238+
const analysis = [
239+
{output: {primary_candidate: {canonical_reference: {code: 'OLD'}}}},
240+
{
241+
output: {
242+
primary_candidate: {concept_key: 'k1'},
243+
alternative_candidates: [
244+
{canonical_reference: {code: 'ALT-1'}},
245+
{concept_key: 'k2'},
246+
{canonical_reference: {code: 'ALT-1'}},
247+
]
248+
}
249+
}
250+
]
251+
const conceptCache = {
252+
k1: {reference: {code: 'PRIMARY'}},
253+
k2: {reference: {code: 'ALT-2'}},
254+
}
255+
256+
const out = getAIAnalysisCandidateIDs(analysis, conceptCache)
257+
assert.equal(out.primaryCandidateId, 'PRIMARY')
258+
assert.deepEqual(out.alternateCandidateIds, ['ALT-1', 'ALT-2'])
259+
assert.equal(out.latestAnalysis, analysis[1])
260+
})

src/components/map-projects/viewBuilders.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,3 +488,22 @@ export const resolveAICandidateID = (candidate, conceptCache) => {
488488
return conceptCache[candidate.concept_key].reference.code
489489
return candidate.canonical_reference?.code || null
490490
}
491+
492+
export const getLatestAnalysisEntry = (analysisProp) => {
493+
if(Array.isArray(analysisProp)) return analysisProp[analysisProp.length - 1]
494+
return analysisProp || null
495+
}
496+
497+
export const getAIAnalysisCandidateIDs = (analysisProp, conceptCache) => {
498+
const latestAnalysis = getLatestAnalysisEntry(analysisProp)
499+
const output = latestAnalysis?.output || latestAnalysis
500+
const primaryCandidateId = resolveAICandidateID(output?.primary_candidate, conceptCache)
501+
const alternateCandidateIds = uniq((output?.alternative_candidates || [])
502+
.map(candidate => resolveAICandidateID(candidate, conceptCache))
503+
.filter(id => id && id !== primaryCandidateId))
504+
return {
505+
latestAnalysis,
506+
primaryCandidateId,
507+
alternateCandidateIds
508+
}
509+
}

src/i18n/locales/en/translations.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@
442442
"not_recommended": "Not Recommended",
443443
"available": "Available",
444444
"recommended": "Recommended",
445+
"state": "State",
445446
"unranked": "Unranked",
446447
"low_ranked": "Low Ranked",
447448
"field_configuration": "Field Configuration",

0 commit comments

Comments
 (0)