From 60437eff7d83c57fa3b6e9d72724dd7f286d6aba Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 10 Nov 2026 18:42:13 -0500 Subject: [PATCH 1/2] closes OpenConceptLab/ocl_issues#2934 | The orange AI icon's tooltip names the code the AI recommends In the grid's Target Code cell, the orange AI icon (the mapped code is neither the OCL AI Assistant's pick nor one of its alternates) said only "AI Analysis". It now says "The AI recommends ", naming the AI's pick. The purple icon and the alternate case keep their wording, and a row whose analysis recommends no code still says "AI Analysis". The choice of wording moves into getAIIndicatorTooltip in viewBuilders.js so it can be unit-tested; the new map_project.ai_recommends_code key is in every catalog (en, es, pt-BR, zh). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AaZ4qTi7WutsgsbAJ48PTj --- src/components/map-projects/MapProject.jsx | 5 +- .../__tests__/viewHelpers.test.js | 56 ++++++++++++++++++- src/components/map-projects/viewBuilders.js | 13 +++++ src/i18n/locales/en/translations.json | 1 + src/i18n/locales/es/translations.json | 1 + src/i18n/locales/pt-BR/translations.json | 1 + src/i18n/locales/zh/translations.json | 1 + 7 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/components/map-projects/MapProject.jsx b/src/components/map-projects/MapProject.jsx index e426f76c..53f18f4d 100644 --- a/src/components/map-projects/MapProject.jsx +++ b/src/components/map-projects/MapProject.jsx @@ -122,7 +122,7 @@ import { createLookupSession, LOOKUP_NOT_FOUND } from './lookupSession' import { parseConceptKey } from './conceptKey' import { normalizeLookupConfig } from './lookupConfigFields' import { getDeclaredLocales, getDefaultTargetRepoVersion, getProjectTargetRepoVersion, getSavedTargetRepoVersion, getTargetRepoVersionFromUrl, getTargetRepoVersionId } from './projectTargetRepo' -import { buildBridgeTargetDownloadEntries, buildQualityRowViews, conceptBelongsToTargetRepo, conceptForMapping, findTargetRowView, formatBridgeTargetDownloadEntry, resolveAICandidateID, getScoreDetails, getAIAnalysisCandidateIDs, withCachedConcept, withCandidateNames, buildMatchEntries, restoredNote, withOwnNames, withSavedMatch } from './viewBuilders.js' +import { buildBridgeTargetDownloadEntries, buildQualityRowViews, conceptBelongsToTargetRepo, conceptForMapping, findTargetRowView, formatBridgeTargetDownloadEntry, resolveAICandidateID, getScoreDetails, getAIAnalysisCandidateIDs, getAIIndicatorTooltip, withCachedConcept, withCandidateNames, buildMatchEntries, restoredNote, withOwnNames, withSavedMatch } from './viewBuilders.js' import { formatClockTime, getCapacityWaitLabel, mergeCapacityWaits } from './rowProgress.js' import './MapProject.scss' @@ -1100,6 +1100,7 @@ const MapProject = () => { return { status, latestAnalysis, + primaryCandidateId, isPrimaryMatch: Boolean(targetCode && primaryCandidateId && targetCode === primaryCandidateId), isAlternateMatch: Boolean(targetCode && alternateCandidateIds.includes(targetCode)), } @@ -1203,7 +1204,7 @@ const MapProject = () => { : aiMeta?.latestAnalysis ? - + diff --git a/src/components/map-projects/__tests__/viewHelpers.test.js b/src/components/map-projects/__tests__/viewHelpers.test.js index 2412bfb2..8c29c78b 100644 --- a/src/components/map-projects/__tests__/viewHelpers.test.js +++ b/src/components/map-projects/__tests__/viewHelpers.test.js @@ -4,12 +4,15 @@ * - conceptForMapping (the tuple -> legacy concept projection at the * onMap / isSelectedForMap boundary) * - resolveAICandidateID (AI Assistant response resolution chain) + * - getAIIndicatorTooltip (the AI icon's tooltip in the Target Code cell) * * Run with: npm test */ import test from 'node:test' import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import i18next from 'i18next' import { conceptBelongsToTargetRepo, @@ -17,7 +20,8 @@ import { conceptForMapping, resolveAICandidateID, getLatestAnalysisEntry, - getAIAnalysisCandidateIDs + getAIAnalysisCandidateIDs, + getAIIndicatorTooltip } from '../viewBuilders.js' const candidatesScore = { recommended: 80, available: 60 } @@ -258,3 +262,53 @@ test('getAIAnalysisCandidateIDs: resolves primary + alternate ids from latest an assert.deepEqual(out.alternateCandidateIds, ['ALT-1', 'ALT-2']) assert.equal(out.latestAnalysis, analysis[1]) }) + +// ---------- getAIIndicatorTooltip (OpenConceptLab/ocl_issues#2934) ---------- + +const translator = async locale => { + const catalog = JSON.parse(readFileSync(new URL(`../../../i18n/locales/${locale}/translations.json`, import.meta.url), 'utf8')) + const i18n = i18next.createInstance() + await i18n.init({lng: locale, resources: {[locale]: {translations: catalog}}, ns: ['translations'], defaultNS: 'translations', interpolation: {escapeValue: false}}) + return i18n.t +} + +test('getAIIndicatorTooltip: the orange icon names the code the AI recommends instead', async () => { + const t = await translator('en') + assert.equal( + getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: false, primaryCandidateId: 'BA00.Z'}, t), + 'The AI recommends BA00.Z' + ) +}) + +test('getAIIndicatorTooltip: a mapped alternate keeps its own wording', async () => { + const t = await translator('en') + assert.equal( + getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: true, primaryCandidateId: 'BA00.Z'}, t), + 'AI Alternate Candidate Suggestion' + ) +}) + +test('getAIIndicatorTooltip: the purple icon still says AI Recommended', async () => { + const t = await translator('en') + assert.equal( + getAIIndicatorTooltip({isPrimaryMatch: true, isAlternateMatch: false, primaryCandidateId: 'BA00.Z'}, t), + 'AI Recommended' + ) +}) + +test('getAIIndicatorTooltip: with no recommended code it falls back to AI Analysis', async () => { + const t = await translator('en') + assert.equal( + getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: false, primaryCandidateId: null}, t), + 'AI Analysis' + ) +}) + +test('getAIIndicatorTooltip: every catalog names the recommended code', async () => { + for (const locale of ['en', 'es', 'pt-BR', 'zh']) { + const t = await translator(locale) + const tooltip = getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: false, primaryCandidateId: 'BA00.Z'}, t) + assert.ok(tooltip.includes('BA00.Z'), `${locale}: ${tooltip}`) + assert.notEqual(tooltip, t('map_project.ai_analysis'), locale) + } +}) diff --git a/src/components/map-projects/viewBuilders.js b/src/components/map-projects/viewBuilders.js index af49291a..61991458 100644 --- a/src/components/map-projects/viewBuilders.js +++ b/src/components/map-projects/viewBuilders.js @@ -618,3 +618,16 @@ export const getAIAnalysisCandidateIDs = (analysisProp, conceptCache) => { alternateCandidateIds } } + +/** + * Tooltip for the AI icon in the grid's Target Code cell: purple when the + * mapped code is the OCL AI Assistant's pick, orange otherwise. The orange + * one names the code the AI recommends instead, so a reviewer needn't open + * the row to find it (OpenConceptLab/ocl_issues#2934). + */ +export const getAIIndicatorTooltip = ({isPrimaryMatch, isAlternateMatch, primaryCandidateId}, t) => { + if(isPrimaryMatch) return t('map_project.ai_recommended') + if(isAlternateMatch) return t('map_project.ai_alternate') + if(primaryCandidateId) return t('map_project.ai_recommends_code', {code: primaryCandidateId}) + return t('map_project.ai_analysis') +} diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index d929e493..bf6b226b 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -309,6 +309,7 @@ "source": "Source", "ai_recommended": "AI Recommended", "ai_alternate": "AI Alternate Candidate Suggestion", + "ai_recommends_code": "The AI recommends {{code}}", "ai_assistant": "OCL AI Assistant", "ai_prompt_template_unavailable": "OCL AI Assistant prompt template is not available", "view_raw_json": "View Raw JSON", diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index 0cdb0208..e69a4e6a 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -293,6 +293,7 @@ "source": "Fuente", "ai_recommended": "Recomendado por IA", "ai_alternate": "Sugerencia de candidato alternativo de IA", + "ai_recommends_code": "La IA recomienda {{code}}", "ai_assistant": "Asistente de IA de OCL", "ai_prompt_template_unavailable": "La plantilla de prompt del asistente de IA de OCL no está disponible", "view_raw_json": "Ver JSON Sin Procesar", diff --git a/src/i18n/locales/pt-BR/translations.json b/src/i18n/locales/pt-BR/translations.json index 213b0a21..947521b0 100644 --- a/src/i18n/locales/pt-BR/translations.json +++ b/src/i18n/locales/pt-BR/translations.json @@ -309,6 +309,7 @@ "source": "Source", "ai_recommended": "Recomendado por IA", "ai_alternate": "Sugestão de Candidato Alternativo por IA", + "ai_recommends_code": "A IA recomenda {{code}}", "ai_assistant": "Assistente de IA do OCL", "ai_prompt_template_unavailable": "O modelo de prompt do Assistente de IA do OCL não está disponível", "view_raw_json": "Ver JSON Bruto", diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index ba755cc9..9d1b42ca 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -305,6 +305,7 @@ "source": "来源", "ai_recommended": "AI 推荐", "ai_alternate": "AI 备选候选建议", + "ai_recommends_code": "AI 推荐的是 {{code}}", "ai_assistant": "OCL AI 助手", "ai_prompt_template_unavailable": "OCL AI 助手提示模板不可用", "view_raw_json": "查看原始 JSON", From 662ce174c3f6da29e890150fe3c07a645f351cfc Mon Sep 17 00:00:00 2001 From: Jonathan Payne Date: Tue, 10 Nov 2026 18:47:46 -0500 Subject: [PATCH 2/2] closes OpenConceptLab/ocl_issues#2934 | Trim the AI tooltip tests and comment From the simplification review: the four English tooltip cases become one table-style test with the same assertions, the every-catalog loop drops an assertion its includes() check already implies, and the helper's comment shrinks to one line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AaZ4qTi7WutsgsbAJ48PTj --- .../__tests__/viewHelpers.test.js | 36 ++++--------------- src/components/map-projects/viewBuilders.js | 7 +--- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/src/components/map-projects/__tests__/viewHelpers.test.js b/src/components/map-projects/__tests__/viewHelpers.test.js index 8c29c78b..85c78a32 100644 --- a/src/components/map-projects/__tests__/viewHelpers.test.js +++ b/src/components/map-projects/__tests__/viewHelpers.test.js @@ -272,36 +272,13 @@ const translator = async locale => { return i18n.t } -test('getAIIndicatorTooltip: the orange icon names the code the AI recommends instead', async () => { +test('getAIIndicatorTooltip: orange names the AI\'s pick; the alternate, purple and no-pick cases keep their wording', async () => { const t = await translator('en') - assert.equal( - getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: false, primaryCandidateId: 'BA00.Z'}, t), - 'The AI recommends BA00.Z' - ) -}) - -test('getAIIndicatorTooltip: a mapped alternate keeps its own wording', async () => { - const t = await translator('en') - assert.equal( - getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: true, primaryCandidateId: 'BA00.Z'}, t), - 'AI Alternate Candidate Suggestion' - ) -}) - -test('getAIIndicatorTooltip: the purple icon still says AI Recommended', async () => { - const t = await translator('en') - assert.equal( - getAIIndicatorTooltip({isPrimaryMatch: true, isAlternateMatch: false, primaryCandidateId: 'BA00.Z'}, t), - 'AI Recommended' - ) -}) - -test('getAIIndicatorTooltip: with no recommended code it falls back to AI Analysis', async () => { - const t = await translator('en') - assert.equal( - getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: false, primaryCandidateId: null}, t), - 'AI Analysis' - ) + const tooltip = meta => getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: false, primaryCandidateId: 'BA00.Z', ...meta}, t) + assert.equal(tooltip({}), 'The AI recommends BA00.Z') + assert.equal(tooltip({isAlternateMatch: true}), 'AI Alternate Candidate Suggestion') + assert.equal(tooltip({isPrimaryMatch: true}), 'AI Recommended') + assert.equal(tooltip({primaryCandidateId: null}), 'AI Analysis') }) test('getAIIndicatorTooltip: every catalog names the recommended code', async () => { @@ -309,6 +286,5 @@ test('getAIIndicatorTooltip: every catalog names the recommended code', async () const t = await translator(locale) const tooltip = getAIIndicatorTooltip({isPrimaryMatch: false, isAlternateMatch: false, primaryCandidateId: 'BA00.Z'}, t) assert.ok(tooltip.includes('BA00.Z'), `${locale}: ${tooltip}`) - assert.notEqual(tooltip, t('map_project.ai_analysis'), locale) } }) diff --git a/src/components/map-projects/viewBuilders.js b/src/components/map-projects/viewBuilders.js index 61991458..e921b097 100644 --- a/src/components/map-projects/viewBuilders.js +++ b/src/components/map-projects/viewBuilders.js @@ -619,12 +619,7 @@ export const getAIAnalysisCandidateIDs = (analysisProp, conceptCache) => { } } -/** - * Tooltip for the AI icon in the grid's Target Code cell: purple when the - * mapped code is the OCL AI Assistant's pick, orange otherwise. The orange - * one names the code the AI recommends instead, so a reviewer needn't open - * the row to find it (OpenConceptLab/ocl_issues#2934). - */ +// The AI icon's tooltip in the grid's Target Code cell; the orange one names the AI's pick (OpenConceptLab/ocl_issues#2934). export const getAIIndicatorTooltip = ({isPrimaryMatch, isAlternateMatch, primaryCandidateId}, t) => { if(isPrimaryMatch) return t('map_project.ai_recommended') if(isAlternateMatch) return t('map_project.ai_alternate')