Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 44 additions & 17 deletions src/components/map-projects/AICandidatesAnalysis.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Tooltip from '@mui/material/Tooltip'
import Skeleton from '@mui/material/Skeleton'
import CloseIcon from '@mui/icons-material/Close'
import DataObjectIcon from '@mui/icons-material/DataObject';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';

import get from 'lodash/get'
import map from 'lodash/map'
Expand All @@ -20,9 +22,24 @@ import compact from 'lodash/compact'
import Comment from './Comment'


const AICandidatesAnalysis = ({ analysis, onClose, sx, isCoreUser }) => {
const AICandidatesAnalysis = ({ analysis: analysisProp, onClose, sx, isCoreUser }) => {
const { t } = useTranslation();
const [openDetails, setOpenDetails] = React.useState(false)
const [page, setPage] = React.useState(0)

const analysisArray = Array.isArray(analysisProp) ? analysisProp : (analysisProp ? [analysisProp] : [])
const total = analysisArray.length

// Jump to latest only when total grows (a new entry was appended);
// don't yank the user back when total holds steady on re-render.
const prevTotalRef = React.useRef(0)
React.useEffect(() => {
if(total > prevTotalRef.current)
setPage(total - 1)
prevTotalRef.current = total
}, [total])

const analysis = analysisArray[page]
let output = analysis?.output || analysis

const getRecommendationTitle = () => {
Expand Down Expand Up @@ -112,13 +129,13 @@ const AICandidatesAnalysis = ({ analysis, onClose, sx, isCoreUser }) => {
</Typography>
</span>
{
analysis?.prompt_template_uri &&
analysis?.output_locale &&
<span style={{marginRight: '4px', display: 'inline-flex'}}>
<Typography gutterBottom sx={{ color: 'text.secondary', fontSize: 12, mb: 0 }} component='span'>
URI:
{t('map_project.output_locale')}:
</Typography>
<Typography gutterBottom sx={{ color: 'text.primary', fontSize: 12, mb: 0 }} component='span'>
{analysis.prompt_template_uri}
{analysis.output_locale}
</Typography>
</span>
}
Expand All @@ -131,19 +148,29 @@ const AICandidatesAnalysis = ({ analysis, onClose, sx, isCoreUser }) => {
</Typography>
</span>
</span>
<span>
<>
{
isCoreUser &&
<Tooltip title={t('map_project.view_raw_json')} placement='right'>
<span>
<IconButton color='primary' size='small' disabled={!analysis} sx={{padding: '4px', marginLeft: '4px', marginTop: '-2px'}} onClick={() => setOpenDetails(!openDetails)}>
<DataObjectIcon fontSize='inherit' />
</IconButton>
</span>
</Tooltip>
}
</>
<span style={{display: 'inline-flex', alignItems: 'center'}}>
{
total > 1 &&
<span style={{display: 'inline-flex', alignItems: 'center', fontSize: '12px', marginRight: '4px'}}>
<IconButton size='small' sx={{padding: '2px', color: 'text.primary'}} onClick={() => setPage(p => Math.max(0, p - 1))} disabled={page === 0}>
<ChevronLeftIcon sx={{fontSize: '1rem'}} />
</IconButton>
<b style={{fontSize: '12px'}}>{page + 1}/{total}</b>
<IconButton size='small' sx={{padding: '2px', color: 'text.primary'}} onClick={() => setPage(p => Math.min(total - 1, p + 1))} disabled={page === total - 1}>
<ChevronRightIcon sx={{fontSize: '1rem'}} />
</IconButton>
</span>
}
{
isCoreUser &&
<Tooltip title={t('map_project.view_raw_json')} placement='right'>
<span>
<IconButton color='primary' size='small' disabled={!analysis} sx={{padding: '4px', marginLeft: '4px', marginTop: '-2px'}} onClick={() => setOpenDetails(!openDetails)}>
<DataObjectIcon fontSize='inherit' />
</IconButton>
</span>
</Tooltip>
}
</span>
</div>
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/map-projects/Candidates.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@ const Candidates = ({rowIndex, alert, setAlert, rowState, conceptCache, targetCa
// v2 concept_key passthrough, then canonical_reference.code (PR2a shim),
// then the legacy concept_id/id. The resolved code is matched against
// ConceptDefinition.reference.code in Concept.jsx for highlighting.
const primary = analysis?.output?.primary_candidate || analysis?.primary_candidate
const latestAnalysis = Array.isArray(analysis) ? analysis[analysis.length - 1] : analysis
const primary = latestAnalysis?.output?.primary_candidate || latestAnalysis?.primary_candidate
const AIRecommendedCandidateId = resolveAICandidateID(primary, conceptCache)

// Quality (score-grouped) view shows ONLY target-repo concepts. Bridge
Expand Down
27 changes: 22 additions & 5 deletions src/components/map-projects/MapProject.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,8 @@ const MapProject = () => {
setProjectPromptTemplateKey(response.data?.prompt_template_key || '')
setPromptOutputLocale(response.data?.prompt_output_locale || null)
setUseLexicalVariants(Boolean(response.data?.use_lexical_variants))
setAnalysis(response.data?.analysis || {})
const rawAnalysis = response.data?.analysis || {}
setAnalysis(Object.fromEntries(Object.entries(rawAnalysis).map(([k, v]) => [k, Array.isArray(v) ? v : [v]])))
setProject(response.data)
setConfigure(false)
})
Expand Down Expand Up @@ -2259,7 +2260,9 @@ const MapProject = () => {
const rowStateLabel = VIEWS[rowState].label
let concept = mapSelected[index]
let _repo = concept?.repo
const aiRecommendation = get(analysis, index)?.output || get(analysis, index)
const rowAnalyses = get(analysis, index) || []
const latestAnalysis = Array.isArray(rowAnalyses) ? rowAnalyses[rowAnalyses.length - 1] : rowAnalyses
const aiRecommendation = latestAnalysis?.output || latestAnalysis
const aiCandidate = get(aiRecommendation, 'primary_candidate')
// v2 response: prefer concept_key (resolves via conceptCache for an
// unambiguous match), then canonical_reference.code (the PR2a shim);
Expand Down Expand Up @@ -3840,7 +3843,13 @@ const MapProject = () => {
})
)
}
if(isNumber(__index) && repoVersion && !analysis[__index] && _candidates?.length > 0) {
// Auto-match (caller supplied resolvedPromptTemplate) fires once per row;
// user-initiated single-row clicks always append a new entry to the
// per-row analysis history.
const isAutoMatch = Boolean(resolvedPromptTemplate)
const existingAnalyses = analysis[__index] || []
const alreadyAnalyzed = isAutoMatch && existingAnalyses.length > 0
if(isNumber(__index) && repoVersion && !alreadyAnalyzed && _candidates?.length > 0) {
Comment thread
snyaggarwal marked this conversation as resolved.
if(!promptTemplate?.key) {
setAlert({message: 'AI Assistant prompt template is not available', severity: 'error'})
markAlgo(__index, 'recommend', -3)
Expand Down Expand Up @@ -3935,7 +3944,15 @@ const MapProject = () => {

markAlgo(__index, 'recommend', 1)
log({created_at: timestamp, action: 'AIRecommendation', description: get(response.data, 'output.rationale') || get(response.data, 'rationale'), extras: {...response.data, model: selectedModel, prompt_template: promptTemplateRef, prompt_template_uri: promptTemplateRef?.uri}}, __index)
setAnalysis(prev => ({...prev, [__index]: {...response.data, model: selectedModel?.id || AIModel, model_name: selectedModel?.name, prompt_template: promptTemplateRef, prompt_template_uri: promptTemplateRef?.uri, timestamp: timestamp, user: user.username || user.id}}))
const resolvedTemplate = response.data?.template || {}
const resolvedVersion = resolvedTemplate.version || promptTemplateRef?.version || null
const resolvedPromptRef = {
...promptTemplateRef,
version: resolvedVersion,
uri: resolvedVersion && promptTemplateRef?.key ? `/prompts/${promptTemplateRef.key}/${resolvedVersion}/` : (promptTemplateRef?.uri || null)
}
const newEntry = {...response.data, model: selectedModel?.id || AIModel, model_name: selectedModel?.name, prompt_template: resolvedPromptRef, prompt_template_uri: resolvedPromptRef.uri, output_locale: promptOutputLocale || null, timestamp: timestamp, user: user.username || user.id}
setAnalysis(prev => ({...prev, [__index]: [...(prev[__index] || []), newEntry]}))
return true
} catch (err) {
markAlgo(__index, 'recommend', -2)
Expand All @@ -3946,7 +3963,7 @@ const MapProject = () => {
return false
}
} else {
markAlgo(__index, 'recommend', analysis[__index] ? 1 : -3)
markAlgo(__index, 'recommend', analysis[__index]?.length > 0 ? 1 : -3)
}
return false
}
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@
"bridge_terminology_search": "Bridge terminology search<0>Premium</0>",
"bridge_terminology_search_description": "Include mappings in the <0>CIEL Interface Terminology</0> to identify additional high quality candidates. Only available for compatible target repositories and matching algorithms.",
"scispacy_loinc_search": "ScispaCy LOINC search<0>Premium</0>",
"ocl_ai_assistant": "OCL AI Assistant",
"ocl_ai_candidates_analysis": "OCL AI Assistant: Candidate Analysis",
"group_by_match_quality": "Match Quality",
"ocl_semantic_algorithm": "OCL Semantic Algorithm",
Expand Down Expand Up @@ -628,6 +629,7 @@
"create_similar_name": "Copy of {{name}}",
"set_ai_assistant_output_language": "Set AI Assistant output language",
"ai_assistant_output_locale": "Output locale",
"output_locale": "Output Locale",
"use_lexical_variants": "Use Lexical Variants",
"use_lexical_variants_description": "Expand $match search to include English spelling variants (e.g. color/colour, leukemia/leukaemia) when matching concept names."
},
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/es/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@
"bridge_terminology_search": "Búsqueda de terminología Bridge<0>Premium</0>",
"bridge_terminology_search_description": "Incluir mapeos en la <0>Terminología de Interfaz CIEL</0> para identificar candidatos adicionales de alta calidad. Disponible solo para repositorios de destino y algoritmos de coincidencia compatibles.",
"scispacy_loinc_search": "Búsqueda ScispaCy LOINC<0>Premium</0>",
"ocl_ai_assistant": "OCL Asistente de IA",
"ocl_ai_candidates_analysis": "OCL AI Assistant: Análisis de Candidatos",
"group_by_match_quality": "Calidad de Coincidencia",
"ocl_semantic_algorithm": "Algoritmo Semántico OCL",
Expand Down Expand Up @@ -556,6 +557,7 @@
"ai_prompt_template_default_model": "Modelo predeterminado",
"set_ai_assistant_output_language": "Establecer el idioma de salida del asistente de IA",
"ai_assistant_output_locale": "Configuración regional de salida",
"output_locale": "Idioma de salida",
"reranker_configuration": "Configuración del reranker",
"reranker_configuration_description": "Elija el modelo de reranker utilizado para calcular las puntuaciones unificadas de este proyecto. El modelo predeterminado se selecciona automáticamente, o puede ingresar un nombre de modelo personalizado.",
"reranker_configuration_model": "Modelo de reranker",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/zh/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,7 @@
"bridge_terminology_search": "Bridge 术语搜索<0>高级版</0>",
"bridge_terminology_search_description": "包含 <0>CIEL 接口术语</0> 中的映射,以识别更多高质量候选项。仅适用于兼容的目标仓库和匹配算法。",
"scispacy_loinc_search": "ScispaCy LOINC 搜索<0>高级版</0>",
"ocl_ai_assistant": "OCL AI 助手",
"ocl_ai_candidates_analysis": "OCL AI 助手:候选分析",
"group_by_match_quality": "匹配质量",
"ocl_semantic_algorithm": "OCL 语义算法",
Expand Down Expand Up @@ -581,6 +582,7 @@
"ai_prompt_template_default_model": "默认模型",
"set_ai_assistant_output_language": "设置 AI 助手输出语言",
"ai_assistant_output_locale": "输出区域设置",
"output_locale": "输出语言",
"reranker_configuration": "重排序器配置",
"reranker_configuration_description": "为此项目选择用于计算统一分数的重排序器模型。默认模型会自动选中,您也可以输入自定义模型名称。",
"reranker_configuration_model": "重排序器模型",
Expand Down