diff --git a/src/components/map-projects/AutoMatchDialog.jsx b/src/components/map-projects/AutoMatchDialog.jsx index 3059513..8d051c9 100644 --- a/src/components/map-projects/AutoMatchDialog.jsx +++ b/src/components/map-projects/AutoMatchDialog.jsx @@ -28,9 +28,10 @@ import AIAssistantSelectorPanel from './AIAssistantSelectorPanel' const AutoMatchDialog = ({ open, onClose, - autoMatchUnmappedOnly, - setAutoMatchUnmappedOnly, + autoMatchScope, + setAutoMatchScope, rowStatuses, + selectedRowCount, autoRunAIAnalysis, setAutoRunAIAnalysis, AIModels, @@ -47,11 +48,31 @@ const AutoMatchDialog = ({ }) => { const { t } = useTranslation() const [algos, setAlgos] = React.useState(true) - const selectedRows = autoMatchUnmappedOnly ? rowStatuses.unmapped.length : (rowStatuses.unmapped.length + rowStatuses.readyForReview.length) + const allRowsCount = rowStatuses.unmapped.length + rowStatuses.readyForReview.length + const rowsInSelectedScope = { + unmapped: rowStatuses.unmapped.length, + all: allRowsCount, + selected: selectedRowCount + } + const rowsToMatchCount = rowsInSelectedScope[autoMatchScope] || 0 const totalRows = rowStatuses.unmapped.length + rowStatuses.readyForReview.length + rowStatuses.reviewed.length + const hasSelectedRows = selectedRowCount > 0 + const hasUnmappedRows = rowStatuses.unmapped.length > 0 + + React.useEffect(() => { + if (autoMatchScope === 'unmapped' && !hasUnmappedRows) { + setAutoMatchScope('all') + } + }, [autoMatchScope, hasUnmappedRows, setAutoMatchScope]) const getHelperTextForAutoMatchUnmapped = () => { - if (autoMatchUnmappedOnly) { + if (autoMatchScope === 'selected') { + if (hasSelectedRows) { + return t('map_project.auto_match_selected_rows_note', {count: selectedRowCount.toLocaleString()}); + } + return ''; + } + if (autoMatchScope === 'unmapped') { const count = rowStatuses.unmapped.length; if (count > 0) { return t('map_project.auto_match_unmapped_only_note', {count: count.toLocaleString()}); @@ -69,7 +90,7 @@ const AutoMatchDialog = ({ return t('map_project.auto_match_note_no_counts'); }; - const isDisabled = !repoVersion?.version_url || selectedRows === 0 || (!algos && !autoRunAIAnalysis) + const isDisabled = !repoVersion?.version_url || rowsToMatchCount === 0 || (!algos && !autoRunAIAnalysis) return ( - {`${t('map_project.selected_rows')}: ${selectedRows.toLocaleString()} ${t('map_project.out_of')} ${totalRows.toLocaleString()}` } + {`${t('map_project.rows_to_match')}: ${rowsToMatchCount.toLocaleString()} ${t('map_project.out_of')} ${totalRows.toLocaleString()}` } setAutoMatchUnmappedOnly(!autoMatchUnmappedOnly)} + value={autoMatchScope} + onChange={event => setAutoMatchScope(event.target.value)} > + } + label={`${t('map_project.all_rows')} (${allRowsCount.toLocaleString()})`} + /> } - label={t('map_project.unmapped_only')} + disabled={!hasUnmappedRows} + control={} + label={`${t('map_project.unmapped_only')} (${rowStatuses.unmapped.length.toLocaleString()})`} /> } - label={t('map_project.all_rows')} + value="selected" + disabled={!hasSelectedRows} + control={} + label={`${t('map_project.selected_rows')} (${selectedRowCount.toLocaleString()})`} /> diff --git a/src/components/map-projects/MapProject.jsx b/src/components/map-projects/MapProject.jsx index 0fb22b7..74e7ec9 100644 --- a/src/components/map-projects/MapProject.jsx +++ b/src/components/map-projects/MapProject.jsx @@ -224,7 +224,7 @@ const MapProject = () => { const [matchDialog, setMatchDialog] = React.useState(false) const [showItem, setShowItem] = React.useState(false) - const [autoMatchUnmappedOnly, setAutoMatchUnmappedOnly] = React.useState(true) + const [autoMatchScope, setAutoMatchScope] = React.useState('unmapped') const [autoRunAIAnalysis, setAutoRunAIAnalysis] = React.useState(false) const [alert, setAlert] = React.useState(false) const [columnVisibilityModel, setColumnVisibilityModel] = React.useState({}) @@ -1008,7 +1008,7 @@ const MapProject = () => { setDecisionTab('candidates') setSearchText('') setShowItem(false) - setAutoMatchUnmappedOnly(true) + setAutoMatchScope('unmapped') setAlert(false) setSelectedCandidatesScoreBucket(false) setScoreBucketSortBy('desc') @@ -1591,6 +1591,16 @@ const MapProject = () => { const getRowsResults = async (rows, selectedAlgos) => { abortRef.current = false; + const selectedRowIndexes = getSelectedRowIndexes(rows) + const selectedRowIndexSet = new Set(selectedRowIndexes) + const isAutoMatchUnmappedOnly = autoMatchScope === 'unmapped' + const isAutoMatchAllRows = autoMatchScope === 'all' + const isAutoMatchSelectedRows = autoMatchScope === 'selected' + const selectedRowsLogExtras = isAutoMatchSelectedRows ? { + selected_rows_count: selectedRowIndexes.length, + row_indexes: selectedRowIndexes, + selected_row_indexes: selectedRowIndexes + } : {} // Function to process a single batch const processBatch = async (_repo, rowBatch, algo) => { @@ -1729,8 +1739,10 @@ const MapProject = () => { let _selectedAlgos = filter(algosSelected, algo => selectedAlgos.includes(algo.id)) let subActions = [...map(_selectedAlgos, algo => algo.name || algo.id)] subActions.push('reranker') - if(autoMatchUnmappedOnly) + if(isAutoMatchUnmappedOnly) subActions.push('unmatched_only') + if(isAutoMatchSelectedRows) + subActions.push('selected_rows') if(inAIAssistantGroup && autoRunAIAnalysis) subActions.push('with_ai_analysis') @@ -1738,6 +1750,7 @@ const MapProject = () => { action: 'auto_match_started', extras: { sub_actions: subActions, + ...selectedRowsLogExtras, ...(inAIAssistantGroup && autoRunAIAnalysis ? { ai_assistant: { model: getSelectedAIModel(), @@ -1747,13 +1760,25 @@ const MapProject = () => { } }) - if(!autoMatchUnmappedOnly) + if(isAutoMatchAllRows) setRowStatuses(prev => ({...prev, readyForReview: []})) + if(isAutoMatchSelectedRows) + setRowStatuses(prev => ({ + ...prev, + readyForReview: without(prev.readyForReview, ...selectedRowIndexes), + reviewed: without(prev.reviewed, ...selectedRowIndexes) + })) setTimeout(async () => { - const rowsToProcess = autoMatchUnmappedOnly + const rowsToProcess = isAutoMatchUnmappedOnly ? filter(rows, row => rowStatuses.unmapped.includes(row.__index)) - : filter(rows, row => !rowStatuses.reviewed.includes(row.__index)) + : filter(rows, row => { + if(isAutoMatchSelectedRows) + return selectedRowIndexSet.has(row.__index) + if(rowStatuses.reviewed.includes(row.__index)) + return false + return true + }) // ocl_online#105 Phase 5: open the run record, then guarantee it is // closed out (completed / partial / failed / cancelled) via the finally, @@ -1805,6 +1830,7 @@ const MapProject = () => { action: 'auto_match_finished', extras: { sub_actions: subActions, + ...selectedRowsLogExtras, ...(inAIAssistantGroup && autoRunAIAnalysis ? { ai_assistant: { model: getSelectedAIModel(), @@ -2043,6 +2069,7 @@ const MapProject = () => { const onGetCandidates = event => { event.stopPropagation() event.preventDefault() + setAutoMatchScope(getSelectedRowIndexes().length ? 'selected' : (rowStatuses.unmapped.length ? 'unmapped' : 'all')) setMatchDialog(true) } @@ -2677,7 +2704,10 @@ const MapProject = () => { }) } - const getSelectedRowIndexes = () => selectedRowIds.map(id => parseInt(id)).filter(Number.isFinite) + const getSelectedRowIndexes = (_rows = data) => { + const selectedIds = new Set(selectedRowIds.map(id => id?.toString())) + return _rows.filter(_row => selectedIds.has(_row.__index?.toString())).map(_row => _row.__index) + } const openBulkConfirm = action => { const indexes = getSelectedRowIndexes() @@ -3934,10 +3964,11 @@ const MapProject = () => { const rows = getRows() const visibleRowIds = rows.map(_row => _row.__index) const visibleRowIdKey = visibleRowIds.join(',') - const selectedRowsCount = selectedRowIds.length + const selectedRowsCount = getSelectedRowIndexes(rows).length React.useEffect(() => { setSelectedRowIds(prev => { - const next = prev.filter(id => visibleRowIds.includes(parseInt(id))) + const visibleRowIdSet = new Set(visibleRowIds.map(id => id?.toString())) + const next = prev.filter(id => visibleRowIdSet.has(id?.toString())) return next.length === prev.length ? prev : next }) }, [visibleRowIdKey]) @@ -4965,8 +4996,9 @@ const MapProject = () => { onSubmit={onGetCandidatesSubmit} {...{ rowStatuses, - autoMatchUnmappedOnly, - setAutoMatchUnmappedOnly, + autoMatchScope, + setAutoMatchScope, + selectedRowCount: selectedRowsCount, autoRunAIAnalysis, setAutoRunAIAnalysis, AIModels, diff --git a/src/components/map-projects/ProjectLogs.jsx b/src/components/map-projects/ProjectLogs.jsx index 4889153..17f996b 100644 --- a/src/components/map-projects/ProjectLogs.jsx +++ b/src/components/map-projects/ProjectLogs.jsx @@ -72,12 +72,15 @@ const ProjectLogs = ({onClose, logs, project}) => { if(['auto_match_started', 'auto_match_finished', 'auto_matched'].includes(log.action)) { + const subActions = map(log.extras?.sub_actions || [], formatSubAction).filter(subAction => log.extras?.selected_rows_count ? subAction !== 'Selected Rows' : true) + if(log.extras?.selected_rows_count) + subActions.push(`${log.extras.selected_rows_count.toLocaleString()} Selected Rows`) return {startCase(log.action)} { - log.extras?.sub_actions?.length ? + subActions.length ? - {`(${map(log.extras.sub_actions, formatSubAction).join(', ')})`} + {`(${subActions.join(', ')})`} : null } diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 3c887d5..067348d 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -518,6 +518,8 @@ "auto_match_unmapped_only_note_no_count": "Note: Skip input rows that are already proposed", "auto_match_note": "Note: This will not affect {{approvedCount}} approved input rows but will override {{proposedCount}} proposed input rows", "auto_match_note_no_counts": "Note: This will not affect approved input rows but will override proposed input rows", + "auto_match_selected_rows_note": "Note: This will run Auto Match for {{count}} selected input rows", + "auto_match_selected_rows_note_no_count": "Note: Select rows in the left panel to enable this option", "run_ai_analysis": "Run AI Analysis", "run_ai_analysis_note": "Note: Enabling this feature will run AI Analysis on results of each row. This has direct cost implications.", "decision": "Decision", @@ -632,6 +634,7 @@ "select_an_algo": "Select an algorithm", "selected_rows": "Selected Rows", "all_rows": "All Rows", + "rows_to_match": "Rows to Match", "out_of": "out of", "retrieve_candidates": "Retrieve Candidates", "retrieve_candidates_helper_text": "Your project is configured to use the following match algorithms:", diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index deee65d..fa26a35 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -485,6 +485,8 @@ "auto_match_unmapped_only_note_no_count": "Nota: Omitir filas de entrada que ya están propuestas", "auto_match_note": "Nota: Esto no afectará {{approvedCount}} filas de entrada aprobadas pero sobrescribirá {{proposedCount}} filas de entrada propuestas", "auto_match_note_no_counts": "Nota: Esto no afectará las filas de entrada aprobadas pero sobrescribirá las filas de entrada propuestas", + "auto_match_selected_rows_note": "Nota: Esto ejecutará Auto Match para {{count}} fila(s) seleccionada(s)", + "auto_match_selected_rows_note_no_count": "Nota: Seleccione filas en el panel izquierdo para habilitar esta opción", "run_ai_analysis": "Ejecutar Análisis de IA", "run_ai_analysis_note": "Nota: Habilitar esta función ejecutará Análisis de IA en los resultados de cada fila. Esto tiene implicaciones de costo directas.", "decision": "Decisión", @@ -609,6 +611,7 @@ "select_an_algo": "Seleccione un algoritmo", "selected_rows": "Filas seleccionadas", "all_rows": "Todas las filas", + "rows_to_match": "Filas para coincidir", "out_of": "de", "retrieve_candidates": "Recuperar candidatos", "retrieve_candidates_helper_text": "Su proyecto está configurado para usar los siguientes algoritmos de coincidencia:", diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index 6aaafab..53ad37c 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -510,6 +510,8 @@ "auto_match_unmapped_only_note_no_count": "注意:跳过已提议的输入行", "auto_match_note": "注意:这不会影响 {{approvedCount}} 个已批准的输入行,但会覆盖 {{proposedCount}} 个已提议的输入行", "auto_match_note_no_counts": "注意:这不会影响已批准的输入行,但会覆盖已提议的输入行", + "auto_match_selected_rows_note": "注意:这将对 {{count}} 个已选输入行运行自动匹配", + "auto_match_selected_rows_note_no_count": "注意:请在左侧面板选择行以启用此选项", "run_ai_analysis": "运行 AI 分析", "run_ai_analysis_note": "注意:启用此功能将对每行的结果运行 AI 分析。这会产生直接的成本影响。", "decision": "决策", @@ -634,6 +636,7 @@ "select_an_algo": "选择算法", "selected_rows": "已选行", "all_rows": "所有行", + "rows_to_match": "要匹配的行", "out_of": "共", "retrieve_candidates": "获取候选项", "retrieve_candidates_helper_text": "你的项目已配置为使用以下匹配算法:",