@@ -76,15 +76,15 @@ import { OperationsContext } from '../app/LayoutContext';
7676import APIService , { isTransientNetworkError , retryWithBackoff } from '../../services/APIService' ;
7777import { buildAttributionHeaders , buildConfigSnapshot , summarizeRunCompletion } from '../../services/attribution'
7878import { 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
8181import { useDoubleClick } from '../common/useDoubleClick'
8282import CloseIconButton from '../common/CloseIconButton' ;
8383import ConceptDetailsPanel from './ConceptDetailsPanel'
8484import { buildPanelPayload } from './openConceptPanel'
8585import LoaderDialog from '../common/LoaderDialog'
8686import 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'
8888import MapProjectDeleteConfirmDialog from './MapProjectDeleteConfirmDialog' ;
8989import ConfigurationForm from './ConfigurationForm'
9090import Controls from './Controls'
@@ -108,7 +108,7 @@ import { DEFAULT_ENCODER_MODEL } from './rerankerModels'
108108import { normalizeAlgorithmInvocation , lookupStatusRank , buildRecommendableConceptEntry , stripConstantClassAndDatatype , buildLookupConceptUrl } from './normalizers'
109109import { parseConceptKey } from './conceptKey'
110110import { 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
113113import './MapProject.scss'
114114import '../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 = { } ) => {
0 commit comments