From 5371231438d8e2521ca9bcee3ba7d0b7102e7ebf Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:02:16 +0800 Subject: [PATCH 1/6] feat: support dynamic metric configs --- src/App.jsx | 8 ++- src/components/ChartContainer.jsx | 107 ++++++++++++++++++++++++++---- src/components/RegexControls.jsx | 71 +++++++++++++++++++- 3 files changed, 168 insertions(+), 18 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index c7ba568..6ed23b6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,7 +21,8 @@ function App() { mode: 'keyword', // 'keyword' | 'regex' keyword: 'norm:', regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)' - } + }, + others: [] // 其他自定义指标解析配置 }); // 兼容旧版本的正则表达式状态(供ChartContainer使用) @@ -48,6 +49,7 @@ function App() { // 使用全局解析配置作为默认值 loss: { ...globalParsingConfig.loss }, gradNorm: { ...globalParsingConfig.gradNorm }, + others: globalParsingConfig.others.map(o => ({ ...o })), dataRange: { start: 0, // 默认从第一个数据点开始 end: undefined, // 默认到最后一个数据点 @@ -129,7 +131,8 @@ function App() { config: { ...file.config, loss: { ...newConfig.loss }, - gradNorm: { ...newConfig.gradNorm } + gradNorm: { ...newConfig.gradNorm }, + others: newConfig.others.map(o => ({ ...o })) } }))); }, []); @@ -440,6 +443,7 @@ function App() { files={uploadedFiles} lossRegex={lossRegex} gradNormRegex={gradNormRegex} + otherConfigs={globalParsingConfig.others} compareMode={compareMode} relativeBaseline={relativeBaseline} absoluteBaseline={absoluteBaseline} diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index 1c8f9b4..39e7a70 100644 --- a/src/components/ChartContainer.jsx +++ b/src/components/ChartContainer.jsx @@ -78,9 +78,10 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover }) }; export default function ChartContainer({ - files, - lossRegex, - gradNormRegex, + files, + lossRegex, + gradNormRegex, + otherConfigs = [], compareMode, relativeBaseline = 0.002, absoluteBaseline = 0.005, @@ -138,13 +139,35 @@ export default function ChartContainer({ const enabledFiles = files.filter(file => file.enabled !== false); return enabledFiles.map(file => { - if (!file.content) return { ...file, lossData: [], gradNormData: [] }; + if (!file.content) return { ...file, lossData: [], gradNormData: [], othersData: {} }; const lines = file.content.split('\n'); const lossData = []; const gradNormData = []; + const otherMetricData = {}; try { + // 公用关键词匹配函数 + const extractByKeyword = (content, keyword) => { + const results = []; + const lines = content.split('\n'); + const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; + lines.forEach((line) => { + const keywordIndex = line.toLowerCase().indexOf(keyword.toLowerCase()); + if (keywordIndex !== -1) { + const afterKeyword = line.substring(keywordIndex + keyword.length); + const numberMatch = afterKeyword.match(numberRegex); + if (numberMatch) { + const value = parseFloat(numberMatch[0]); + if (!isNaN(value)) { + results.push(value); + } + } + } + }); + return results; + }; + // 使用新的配置格式,同时保持向后兼容 let fileLossConfig, fileGradNormConfig; @@ -265,6 +288,29 @@ export default function ChartContainer({ } }); } + + // 处理其他自定义指标 + if (Array.isArray(file.config?.others)) { + file.config.others.forEach(metric => { + let values = []; + if (metric.mode === 'keyword') { + values = extractByKeyword(file.content, metric.keyword); + } else { + const regexObj = new RegExp(metric.regex); + lines.forEach(line => { + regexObj.lastIndex = 0; + const match = regexObj.exec(line); + if (match && match[1]) { + const value = parseFloat(match[1]); + if (!isNaN(value)) { + values.push(value); + } + } + }); + } + otherMetricData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v })); + }); + } } catch (error) { console.error('Regex error:', error); } @@ -291,28 +337,34 @@ export default function ChartContainer({ return slicedData; }; + const reindexData = (data) => data.map((point, index) => ({ x: index, y: point.y })); + const filteredLossData = applyRangeFilter(lossData); const filteredGradNormData = applyRangeFilter(gradNormData); + const filteredOthers = {}; + Object.entries(otherMetricData).forEach(([key, data]) => { + filteredOthers[key] = reindexData(applyRangeFilter(data)); + }); - // 重新索引数据点 - const reindexData = (data) => data.map((point, index) => ({ x: index, y: point.y })); - - return { - ...file, - lossData: reindexData(filteredLossData), - gradNormData: reindexData(filteredGradNormData) + return { + ...file, + lossData: reindexData(filteredLossData), + gradNormData: reindexData(filteredGradNormData), + othersData: filteredOthers }; } - return { ...file, lossData, gradNormData }; + return { ...file, lossData, gradNormData, othersData: otherMetricData }; }); - }, [files, lossRegex, gradNormRegex]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [files, lossRegex, gradNormRegex, otherConfigs]); useEffect(() => { const maxStep = parsedData.reduce((max, file) => { const maxLoss = file.lossData.length > 0 ? file.lossData[file.lossData.length - 1].x : 0; const maxGrad = file.gradNormData.length > 0 ? file.gradNormData[file.gradNormData.length - 1].x : 0; - return Math.max(max, maxLoss, maxGrad); + const otherMax = Object.values(file.othersData || {}).reduce((m, data) => Math.max(m, data.length > 0 ? data[data.length - 1].x : 0), 0); + return Math.max(max, maxLoss, maxGrad, otherMax); }, 0); onMaxStepChange(maxStep); }, [parsedData, onMaxStepChange]); @@ -655,6 +707,14 @@ export default function ChartContainer({ .filter(file => file.gradNormData && file.gradNormData.length > 0) .map(file => ({ name: file.name, data: file.gradNormData })); + const otherMetricKeys = otherConfigs.map(c => c.name || c.keyword); + const otherDataArrays = {}; + otherMetricKeys.forEach(key => { + otherDataArrays[key] = parsedData + .filter(file => file.othersData && file.othersData[key] && file.othersData[key].length > 0) + .map(file => ({ name: file.name, data: file.othersData[key] })); + }); + // 计算显示的图表数量来决定布局 const enabledFiles = files.filter(file => file.enabled !== false); const showingLossCharts = showLoss && lossDataArray.length > 0; @@ -837,6 +897,25 @@ export default function ChartContainer({ )} + {otherMetricKeys.length > 0 && ( +
+
+ {otherMetricKeys.map((key, idx) => ( +
+ + + +
+ ))} +
+
+ )} ); } diff --git a/src/components/RegexControls.jsx b/src/components/RegexControls.jsx index 7a5a911..31e1f20 100644 --- a/src/components/RegexControls.jsx +++ b/src/components/RegexControls.jsx @@ -210,7 +210,7 @@ export function RegexControls({ // 预览匹配结果 const previewMatches = useCallback(() => { - const results = { loss: [], gradNorm: [] }; + const results = { loss: [], gradNorm: [], others: {} }; uploadedFiles.forEach(file => { if (file.content) { @@ -247,6 +247,17 @@ export function RegexControls({ format: m.format })) }); + + globalParsingConfig.others.forEach((cfg, idx) => { + const matches = extractValues( + file.content, + cfg.mode, + cfg + ); + const key = cfg.name || `metric${idx+1}`; + if (!results.others[key]) results.others[key] = []; + results.others[key].push({ fileName: file.name, count: matches.length }); + }); } }); @@ -329,6 +340,28 @@ export function RegexControls({ onGlobalParsingConfigChange(newConfig); }; + const handleOtherConfigChange = (index, field, value) => { + const newOthers = [...globalParsingConfig.others]; + newOthers[index] = { ...newOthers[index], [field]: value }; + const newConfig = { ...globalParsingConfig, others: newOthers }; + onGlobalParsingConfigChange(newConfig); + }; + + const addMetric = () => { + const newOthers = [...globalParsingConfig.others, { + name: `metric${globalParsingConfig.others.length + 1}`, + mode: 'keyword', + keyword: '', + regex: '' + }]; + onGlobalParsingConfigChange({ ...globalParsingConfig, others: newOthers }); + }; + + const removeMetric = (index) => { + const newOthers = globalParsingConfig.others.filter((_, i) => i !== index); + onGlobalParsingConfigChange({ ...globalParsingConfig, others: newOthers }); + }; + const handleXRangeChange = (field, value) => { const newRange = { ...xRange, [field]: value === '' ? undefined : Number(value) }; onXRangeChange(newRange); @@ -337,9 +370,20 @@ export function RegexControls({ // 渲染配置项的函数 const renderConfigPanel = (type, config, onConfigChange) => { const ModeIcon = MODE_CONFIG[config.mode].icon; - + return (
+ {type.startsWith('other') && ( +
+ + onConfigChange('name', e.target.value)} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" + /> +
+ )} {/* 模式选择 */}
+ {globalParsingConfig.others.map((cfg, idx) => ( +
+ +

+ + {cfg.name || `Metric ${idx+1}`} 解析配置 +

+ {renderConfigPanel(`other-${idx}`, cfg, (field, value) => handleOtherConfigChange(idx, field, value))} +
+ ))} + +
Date: Tue, 22 Jul 2025 14:20:18 +0800 Subject: [PATCH 2/6] Refactor metrics to generic configuration --- src/App.jsx | 58 +- src/components/ChartContainer.jsx | 966 ++++++----------------------- src/components/FileConfigModal.jsx | 64 +- src/components/RegexControls.jsx | 284 +++------ 4 files changed, 326 insertions(+), 1046 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 6ed23b6..12856b7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -12,23 +12,22 @@ function App() { // 全局解析配置状态 const [globalParsingConfig, setGlobalParsingConfig] = useState({ - loss: { - mode: 'keyword', // 'keyword' | 'regex' - keyword: 'loss:', - regex: 'loss:\\s*([\\d.eE+-]+)' - }, - gradNorm: { - mode: 'keyword', // 'keyword' | 'regex' - keyword: 'norm:', - regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)' - }, - others: [] // 其他自定义指标解析配置 + metrics: [ + { + name: 'Loss', + mode: 'keyword', // 'keyword' | 'regex' + keyword: 'loss:', + regex: 'loss:\\s*([\\d.eE+-]+)' + }, + { + name: 'Grad Norm', + mode: 'keyword', + keyword: 'norm:', + regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)' + } + ] }); - // 兼容旧版本的正则表达式状态(供ChartContainer使用) - const [lossRegex, setLossRegex] = useState('loss:\\s*([\\d.eE+-]+)'); - const [gradNormRegex, setGradNormRegex] = useState('grad[\\s_]norm:\\s*([\\d.eE+-]+)'); - const [compareMode, setCompareMode] = useState('normal'); const [relativeBaseline, setRelativeBaseline] = useState(0.002); const [absoluteBaseline, setAbsoluteBaseline] = useState(0.005); @@ -47,9 +46,7 @@ function App() { enabled: true, config: { // 使用全局解析配置作为默认值 - loss: { ...globalParsingConfig.loss }, - gradNorm: { ...globalParsingConfig.gradNorm }, - others: globalParsingConfig.others.map(o => ({ ...o })), + metrics: globalParsingConfig.metrics.map(m => ({ ...m })), dataRange: { start: 0, // 默认从第一个数据点开始 end: undefined, // 默认到最后一个数据点 @@ -120,31 +117,17 @@ function App() { // 全局解析配置变更处理 const handleGlobalParsingConfigChange = useCallback((newConfig) => { setGlobalParsingConfig(newConfig); - - // 同步更新兼容的正则表达式状态 - setLossRegex(newConfig.loss.mode === 'regex' ? newConfig.loss.regex : 'loss:\\s*([\\d.eE+-]+)'); - setGradNormRegex(newConfig.gradNorm.mode === 'regex' ? newConfig.gradNorm.regex : 'grad[\\s_]norm:\\s*([\\d.eE+-]+)'); - + // 同步所有文件的解析配置 setUploadedFiles(prev => prev.map(file => ({ ...file, config: { ...file.config, - loss: { ...newConfig.loss }, - gradNorm: { ...newConfig.gradNorm }, - others: newConfig.others.map(o => ({ ...o })) + metrics: newConfig.metrics.map(m => ({ ...m })) } }))); }, []); - const handleRegexChange = useCallback((type, value) => { - if (type === 'loss') { - setLossRegex(value); - } else { - setGradNormRegex(value); - } - }, []); - // 全局拖拽事件处理 const handleGlobalDragEnter = useCallback((e) => { e.preventDefault(); @@ -306,9 +289,6 @@ function App() { { const chartRef = useRef(null); - + const handleChartRef = useCallback((ref) => { if (ref) { chartRef.current = ref; @@ -46,571 +45,178 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover }) onSyncHover(null, chartId); } }, - // 添加额外的事件处理确保清除状态 events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], }; - // 添加容器的鼠标离开事件 const handleContainerMouseLeave = useCallback(() => { onSyncHover(null, chartId); }, [onSyncHover, chartId]); - try { - return ( -
- -
- ); - } catch (error) { - console.error('Chart rendering error:', error); - return ( -
-
-

⚠️ 图表渲染错误

-

🔄 请检查数据格式或刷新页面重试

-
-
- ); - } + return ( +
+ +
+ ); }; -export default function ChartContainer({ +export default function ChartContainer({ files, - lossRegex, - gradNormRegex, - otherConfigs = [], + metrics = [], compareMode, relativeBaseline = 0.002, absoluteBaseline = 0.005, showLoss = true, showGradNorm = false, - xRange = { min: undefined, max: undefined }, - onXRangeChange, onMaxStepChange }) { - // 同步hover状态管理 - const chartRefs = useRef(new Map()); // 存储所有图表实例的引用 - - // 注册图表实例 - const registerChart = useCallback((chartId, chartInstance) => { - chartRefs.current.set(chartId, chartInstance); + const chartRefs = useRef(new Map()); + const registerChart = useCallback((id, inst) => { + chartRefs.current.set(id, inst); }, []); - - // 同步所有图表的hover状态 - const syncHoverToAllCharts = useCallback((step, sourceChartId) => { - if (step === null) { - // 清除所有图表的hover状态(包括源图表) - chartRefs.current.forEach((chart) => { - if (chart) { - chart.setActiveElements([]); - chart.tooltip.setActiveElements([]); - chart.update('none'); - } - }); - } else { - // 同步hover到所有图表(不包括源图表,避免重复操作) - chartRefs.current.forEach((chart, chartId) => { - if (chart && chartId !== sourceChartId) { - const activeElements = []; - - // 为每个数据集找到对应step的数据点 - chart.data.datasets.forEach((dataset, datasetIndex) => { - if (dataset.data && dataset.data.length > step) { - activeElements.push({ - datasetIndex, - index: step - }); - } - }); - - chart.setActiveElements(activeElements); - chart.tooltip.setActiveElements(activeElements, { x: 0, y: 0 }); - chart.update('none'); - } - }); - } + + const syncHoverToAllCharts = useCallback((step, sourceId) => { + chartRefs.current.forEach((chart, id) => { + if (!chart) return; + if (step === null) { + chart.setActiveElements([]); + chart.tooltip.setActiveElements([]); + chart.update('none'); + } else if (id !== sourceId) { + const activeElements = []; + chart.data.datasets.forEach((dataset, datasetIndex) => { + if (dataset.data && dataset.data.length > step) { + activeElements.push({ datasetIndex, index: step }); + } + }); + chart.setActiveElements(activeElements); + chart.tooltip.setActiveElements(activeElements, { x: 0, y: 0 }); + chart.update('none'); + } + }); }, []); const parsedData = useMemo(() => { - // 只处理已启用的文件 - const enabledFiles = files.filter(file => file.enabled !== false); - - return enabledFiles.map(file => { - if (!file.content) return { ...file, lossData: [], gradNormData: [], othersData: {} }; - + const enabled = files.filter(f => f.enabled !== false); + return enabled.map(file => { + if (!file.content) return { ...file, metricsData: {} }; const lines = file.content.split('\n'); - const lossData = []; - const gradNormData = []; - const otherMetricData = {}; - - try { - // 公用关键词匹配函数 - const extractByKeyword = (content, keyword) => { - const results = []; - const lines = content.split('\n'); - const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; - lines.forEach((line) => { - const keywordIndex = line.toLowerCase().indexOf(keyword.toLowerCase()); - if (keywordIndex !== -1) { - const afterKeyword = line.substring(keywordIndex + keyword.length); - const numberMatch = afterKeyword.match(numberRegex); - if (numberMatch) { - const value = parseFloat(numberMatch[0]); - if (!isNaN(value)) { - results.push(value); - } - } - } - }); - return results; - }; - - // 使用新的配置格式,同时保持向后兼容 - let fileLossConfig, fileGradNormConfig; - - if (file.config?.loss && file.config?.gradNorm) { - // 新配置格式 - fileLossConfig = file.config.loss; - fileGradNormConfig = file.config.gradNorm; - } else { - // 旧配置格式,转换为新格式 - fileLossConfig = { - mode: 'regex', - regex: file.config?.lossRegex || lossRegex - }; - fileGradNormConfig = { - mode: 'regex', - regex: file.config?.gradNormRegex || gradNormRegex - }; - } - - // 处理Loss数据 - if (fileLossConfig.mode === 'keyword') { - // 关键词匹配 - const extractByKeyword = (content, keyword) => { - const results = []; - const lines = content.split('\n'); - - // 数值正则:支持各种数值格式,包括科学计数法 - const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; - - lines.forEach((line) => { - // 查找关键词(忽略大小写) - const keywordIndex = line.toLowerCase().indexOf(keyword.toLowerCase()); - if (keywordIndex !== -1) { - // 从关键词后开始查找第一个数字 - const afterKeyword = line.substring(keywordIndex + keyword.length); - const numberMatch = afterKeyword.match(numberRegex); - - if (numberMatch) { - const value = parseFloat(numberMatch[0]); - if (!isNaN(value)) { - results.push(value); - } - } - } - }); - - return results; - }; - - const lossValues = extractByKeyword(file.content, fileLossConfig.keyword); - lossValues.forEach((value, index) => { - if (!isNaN(value)) { - lossData.push({ x: index, y: value }); - } - }); - } else { - // 正则表达式匹配 - const lossRegexObj = new RegExp(fileLossConfig.regex); - lines.forEach((line) => { - lossRegexObj.lastIndex = 0; - const lossMatch = lossRegexObj.exec(line); - if (lossMatch && lossMatch[1]) { - const value = parseFloat(lossMatch[1]); - if (!isNaN(value)) { - lossData.push({ x: lossData.length, y: value }); - } - } - }); - } - - // 处理Grad Norm数据 - if (fileGradNormConfig.mode === 'keyword') { - // 关键词匹配 - const extractByKeyword = (content, keyword) => { - const results = []; - const lines = content.split('\n'); - - // 数值正则:支持各种数值格式,包括科学计数法 - const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; - - lines.forEach((line) => { - // 查找关键词(忽略大小写) - const keywordIndex = line.toLowerCase().indexOf(keyword.toLowerCase()); - if (keywordIndex !== -1) { - // 从关键词后开始查找第一个数字 - const afterKeyword = line.substring(keywordIndex + keyword.length); - const numberMatch = afterKeyword.match(numberRegex); - - if (numberMatch) { - const value = parseFloat(numberMatch[0]); - if (!isNaN(value)) { - results.push(value); - } - } - } - }); - - return results; - }; - - const gradNormValues = extractByKeyword(file.content, fileGradNormConfig.keyword); - gradNormValues.forEach((value, index) => { - if (!isNaN(value)) { - gradNormData.push({ x: index, y: value }); + const metricsData = {}; + + const extractByKeyword = (content, keyword) => { + const results = []; + const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; + content.split('\n').forEach(line => { + const idx = line.toLowerCase().indexOf(keyword.toLowerCase()); + if (idx !== -1) { + const after = line.substring(idx + keyword.length); + const match = after.match(numberRegex); + if (match) { + const v = parseFloat(match[0]); + if (!isNaN(v)) results.push(v); } - }); - } else { - // 正则表达式匹配 - const gradNormRegexObj = new RegExp(fileGradNormConfig.regex); - lines.forEach((line) => { - gradNormRegexObj.lastIndex = 0; - const gradNormMatch = gradNormRegexObj.exec(line); - if (gradNormMatch && gradNormMatch[1]) { - const value = parseFloat(gradNormMatch[1]); - if (!isNaN(value)) { - gradNormData.push({ x: gradNormData.length, y: value }); - } - } - }); - } - - // 处理其他自定义指标 - if (Array.isArray(file.config?.others)) { - file.config.others.forEach(metric => { - let values = []; - if (metric.mode === 'keyword') { - values = extractByKeyword(file.content, metric.keyword); - } else { - const regexObj = new RegExp(metric.regex); - lines.forEach(line => { - regexObj.lastIndex = 0; - const match = regexObj.exec(line); - if (match && match[1]) { - const value = parseFloat(match[1]); - if (!isNaN(value)) { - values.push(value); - } - } - }); + } + }); + return results; + }; + + metrics.forEach(metric => { + let values = []; + if (metric.mode === 'keyword') { + values = extractByKeyword(file.content, metric.keyword); + } else if (metric.regex) { + const reg = new RegExp(metric.regex); + lines.forEach(line => { + reg.lastIndex = 0; + const m = reg.exec(line); + if (m && m[1]) { + const v = parseFloat(m[1]); + if (!isNaN(v)) values.push(v); } - otherMetricData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v })); }); } - } catch (error) { - console.error('Regex error:', error); - } + metricsData[metric.name || metric.keyword] = values.map((v, i) => ({ x: i, y: v })); + }); - // 应用数据范围过滤 - const dataRange = file.config?.dataRange; - if (dataRange && (dataRange.start > 0 || dataRange.end !== undefined)) { - const applyRangeFilter = (data) => { + const range = file.config?.dataRange; + if (range && (range.start > 0 || range.end !== undefined)) { + const applyRange = data => { if (data.length === 0) return data; - - const start = Math.max(0, parseInt(dataRange.start) || 0); - const end = dataRange.end !== undefined ? parseInt(dataRange.end) : data.length; - - // 验证范围有效性 - if (start >= data.length || (end !== undefined && start >= end)) { - console.warn(`Invalid range for file ${file.name}: start=${start}, end=${end}, length=${data.length}`); - return data; // 返回原始数据 - } - - // 切片数据(start到end,不包含end) + const start = Math.max(0, parseInt(range.start) || 0); + const end = range.end !== undefined ? parseInt(range.end) : data.length; const endIndex = Math.min(data.length, end); - const slicedData = data.slice(start, endIndex); - - return slicedData; + return data.slice(start, endIndex); }; - - const reindexData = (data) => data.map((point, index) => ({ x: index, y: point.y })); - - const filteredLossData = applyRangeFilter(lossData); - const filteredGradNormData = applyRangeFilter(gradNormData); - const filteredOthers = {}; - Object.entries(otherMetricData).forEach(([key, data]) => { - filteredOthers[key] = reindexData(applyRangeFilter(data)); + const reindex = data => data.map((p, idx) => ({ x: idx, y: p.y })); + Object.keys(metricsData).forEach(k => { + metricsData[k] = reindex(applyRange(metricsData[k])); }); - - return { - ...file, - lossData: reindexData(filteredLossData), - gradNormData: reindexData(filteredGradNormData), - othersData: filteredOthers - }; } - return { ...file, lossData, gradNormData, othersData: otherMetricData }; + return { ...file, metricsData }; }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [files, lossRegex, gradNormRegex, otherConfigs]); + }, [files, metrics]); useEffect(() => { - const maxStep = parsedData.reduce((max, file) => { - const maxLoss = file.lossData.length > 0 ? file.lossData[file.lossData.length - 1].x : 0; - const maxGrad = file.gradNormData.length > 0 ? file.gradNormData[file.gradNormData.length - 1].x : 0; - const otherMax = Object.values(file.othersData || {}).reduce((m, data) => Math.max(m, data.length > 0 ? data[data.length - 1].x : 0), 0); - return Math.max(max, maxLoss, maxGrad, otherMax); + const maxStep = parsedData.reduce((m, f) => { + const localMax = Object.values(f.metricsData).reduce((mm, d) => Math.max(mm, d.length > 0 ? d[d.length - 1].x : 0), 0); + return Math.max(m, localMax); }, 0); onMaxStepChange(maxStep); }, [parsedData, onMaxStepChange]); + const colors = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#f97316']; + const createChartData = dataArray => ({ + datasets: dataArray.map((item, index) => { + const color = colors[index % colors.length]; + return { + label: item.name?.replace(/\.(log|txt)$/i, '') || `File ${index + 1}`, + data: item.data, + borderColor: color, + backgroundColor: `${color}33`, + borderWidth: 2, + fill: false, + tension: 0, + pointRadius: 0, + pointHoverRadius: 4, + pointBackgroundColor: color, + pointBorderColor: color, + pointBorderWidth: 1, + pointHoverBackgroundColor: color, + pointHoverBorderColor: color, + pointHoverBorderWidth: 1, + animation: false, + animations: { colors: false, x: false, y: false }, + }; + }) + }); const getComparisonData = (data1, data2, mode) => { const minLength = Math.min(data1.length, data2.length); const result = []; - for (let i = 0; i < minLength; i++) { - const val1 = data1[i].y; - const val2 = data2[i].y; + const v1 = data1[i].y; + const v2 = data2[i].y; let diff; - switch (mode) { case 'absolute': - diff = Math.abs(val2 - val1); + diff = Math.abs(v2 - v1); break; case 'relative': { - // 相对误差:先计算绝对差值,再计算相对误差(不使用百分号) - const absoluteDiff = Math.abs(val2 - val1); - diff = val1 !== 0 ? absoluteDiff / Math.abs(val1) : 0; + const ad = Math.abs(v2 - v1); + diff = v1 !== 0 ? ad / Math.abs(v1) : 0; break; } - default: // normal - diff = val2 - val1; + default: + diff = v2 - v1; } - result.push({ x: i, y: diff }); } - return result; }; - const chartOptions = { - responsive: true, - maintainAspectRatio: false, - animation: { - duration: 0, // 禁用默认动画 - }, - animations: { - // 禁用所有动画,包括hover动画 - colors: false, - x: false, - y: false, - }, - hover: { - animationDuration: 0, // 禁用hover动画 - }, - responsiveAnimationDuration: 0, // 禁用响应式动画 - interaction: { - mode: 'index', - intersect: false, - }, - plugins: { - zoom: { - pan: { - enabled: true, - mode: 'x', - onPanComplete: ({chart}) => { - const {min, max} = chart.scales.x; - onXRangeChange({min: Math.round(min), max: Math.round(max)}); - } - }, - zoom: { - drag: { - enabled: true, - borderColor: 'rgba(225,225,225,0.2)', - borderWidth: 1, - backgroundColor: 'rgba(225,225,225,0.2)', - modifierKey: 'shift', - }, - wheel: { - enabled: true, - }, - pinch: { - enabled: true - }, - mode: 'x', - onZoomComplete: ({chart}) => { - const {min, max} = chart.scales.x; - onXRangeChange({min: Math.round(min), max: Math.round(max)}); - } - } - }, - legend: { - position: 'top', - labels: { - boxWidth: 40, - boxHeight: 2, - padding: 10, - usePointStyle: false, - generateLabels: function(chart) { - const original = Chart.defaults.plugins.legend.labels.generateLabels; - const labels = original.call(this, chart); - - labels.forEach((label, index) => { - const dataset = chart.data.datasets[index]; - if (dataset && dataset.borderDash && dataset.borderDash.length > 0) { - label.lineDash = dataset.borderDash; - } - }); - - return labels; - } - }, - }, - tooltip: { - mode: 'index', - intersect: false, - animation: false, // 禁用tooltip动画 - backgroundColor: 'rgba(15, 23, 42, 0.92)', - titleColor: '#f1f5f9', - bodyColor: '#cbd5e1', - borderColor: 'rgba(71, 85, 105, 0.2)', - borderWidth: 1, - cornerRadius: 6, - displayColors: true, - usePointStyle: true, - titleFont: { - size: 11, - weight: '600', - family: 'Inter, system-ui, sans-serif' - }, - bodyFont: { - size: 10, - weight: '400', - family: 'Inter, system-ui, sans-serif' - }, - footerFont: { - size: 9, - weight: '300' - }, - padding: { - top: 6, - bottom: 6, - left: 8, - right: 8 - }, - caretPadding: 4, - caretSize: 4, - multiKeyBackground: 'transparent', - callbacks: { - title: function(context) { - return `Step ${context[0].parsed.x}`; - }, - label: function(context) { - const value = Number(context.parsed.y.toPrecision(4)); - return ` ${value}`; - }, - labelColor: function(context) { - return { - borderColor: context.dataset.borderColor, - backgroundColor: context.dataset.borderColor, - borderWidth: 1, - borderRadius: 2 - }; - } - } - }, - }, - scales: { - x: { - type: 'linear', - display: true, - title: { - display: true, - text: 'Step', - }, - min: xRange.min, - max: xRange.max, - bounds: 'data' - }, - y: { - type: 'linear', - display: true, - title: { - display: true, - text: 'Value', - }, - bounds: 'data', - ticks: { - callback: function(value) { - return Number(value.toPrecision(2)); - }, - }, - }, - }, - elements: { - point: { - radius: 0, // 默认不显示数据点 - }, - }, - }; - - const colors = [ - '#ef4444', // red - '#3b82f6', // blue - '#10b981', // green - '#f59e0b', // yellow - '#8b5cf6', // purple - '#f97316', // orange - ]; - - const createChartData = (dataArray) => { - const datasets = []; - - dataArray.forEach((item, index) => { - const color = colors[index % colors.length]; - - datasets.push({ - label: `${item.name?.replace(/\.(log|txt)$/i, '') || `File ${index + 1}`}`, - data: item.data, - borderColor: color, - backgroundColor: `${color}33`, // Add transparency - borderWidth: 2, - fill: false, - tension: 0, // 设置 tension 为 0,绘制直线段 - pointRadius: 0, // 默认不显示数据点 - pointHoverRadius: 4, // hover时显示实心圆点,半径为4 - pointBackgroundColor: color, // 设置点的背景色 - pointBorderColor: color, // 设置点的边框色 - pointBorderWidth: 1, // 设置点的边框宽度 - pointHoverBackgroundColor: color, // hover时的背景色 - pointHoverBorderColor: color, // hover时的边框色 - pointHoverBorderWidth: 1, // hover时的边框宽度 - // 禁用所有动画,确保响应迅速 - animation: false, - animations: { - colors: false, - x: false, - y: false, - }, - }); - }); - - return { - datasets, - }; - }; - const createComparisonChartData = (item1, item2, title) => { const comparisonData = getComparisonData(item1.data, item2.data, compareMode); - const baseline = compareMode === 'relative' ? relativeBaseline : - compareMode === 'absolute' ? absoluteBaseline : 0; - + const baseline = compareMode === 'relative' ? relativeBaseline : compareMode === 'absolute' ? absoluteBaseline : 0; const datasets = [ { label: `${title} 差值`, @@ -619,55 +225,42 @@ export default function ChartContainer({ backgroundColor: '#dc2626', borderWidth: 2, fill: false, - tension: 0, // 设置 tension 为 0,绘制直线段 - pointRadius: 0, // 默认不显示数据点 - pointHoverRadius: 4, // hover时显示实心圆点 - pointBackgroundColor: '#dc2626', // 设置点的背景色 - pointBorderColor: '#dc2626', // 设置点的边框色 - pointBorderWidth: 1, // 设置点的边框宽度 - pointHoverBackgroundColor: '#dc2626', // hover时的背景色 - pointHoverBorderColor: '#dc2626', // hover时的边框色 - pointHoverBorderWidth: 1, // hover时的边框宽度 - // 禁用所有动画,确保响应迅速 + tension: 0, + pointRadius: 0, + pointHoverRadius: 4, + pointBackgroundColor: '#dc2626', + pointBorderColor: '#dc2626', + pointBorderWidth: 1, + pointHoverBackgroundColor: '#dc2626', + pointHoverBorderColor: '#dc2626', + pointHoverBorderWidth: 1, animation: false, - animations: { - colors: false, - x: false, - y: false, - }, - } + animations: { colors: false, x: false, y: false }, + }, ]; - - // Add baseline if it's not zero and we're in relative or absolute mode if (baseline > 0 && (compareMode === 'relative' || compareMode === 'absolute')) { - const baselineData = comparisonData.map(point => ({ x: point.x, y: baseline })); + const baselineData = comparisonData.map(p => ({ x: p.x, y: baseline })); datasets.push({ - label: `Baseline`, - data: baselineData, - borderColor: '#10b981', - backgroundColor: '#10b981', - borderWidth: 2, - borderDash: [5, 5], - fill: false, - tension: 0, - pointRadius: 0, // 默认不显示数据点 - pointHoverRadius: 4, // hover时显示实心圆点 - pointBackgroundColor: '#10b981', // 设置点的背景色 - pointBorderColor: '#10b981', // 设置点的边框色 - pointBorderWidth: 1, // 设置点的边框宽度 - pointHoverBackgroundColor: '#10b981', // hover时的背景色 - pointHoverBorderColor: '#10b981', // hover时的边框色 - pointHoverBorderWidth: 1, // hover时的边框宽度 - // 禁用所有动画,确保响应迅速 - animation: false, - animations: { - colors: false, - x: false, - y: false, - }, - }); + label: 'Baseline', + data: baselineData, + borderColor: '#10b981', + backgroundColor: '#10b981', + borderWidth: 2, + borderDash: [5, 5], + fill: false, + tension: 0, + pointRadius: 0, + pointHoverRadius: 4, + pointBackgroundColor: '#10b981', + pointBorderColor: '#10b981', + pointBorderWidth: 1, + pointHoverBackgroundColor: '#10b981', + pointHoverBorderColor: '#10b981', + pointHoverBorderWidth: 1, + animation: false, + animations: { colors: false, x: false, y: false }, + }); } - return { datasets }; }; @@ -682,240 +275,73 @@ export default function ChartContainer({ ); } - // 检查是否有任何图表可以显示 - if (!showLoss && !showGradNorm) { + const metricNames = metrics.map(m => m.name || m.keyword); + const metricDataArrays = {}; + metricNames.forEach(name => { + metricDataArrays[name] = parsedData + .filter(file => file.metricsData[name] && file.metricsData[name].length > 0) + .map(file => ({ name: file.name, data: file.metricsData[name] })); + }); + + const metricsToShow = metrics.filter((m, idx) => { + if (idx === 0) return showLoss; + if (idx === 1) return showGradNorm; + return true; + }); + + if (metricsToShow.length === 0) { return (
-
- - - -

🎯 请选择要显示的图表

-

👈 在左侧显示选项中勾选 "显示 Loss 函数" 或 "显示 Grad Norm"

); } - const lossDataArray = parsedData - .filter(file => file.lossData && file.lossData.length > 0) - .map(file => ({ name: file.name, data: file.lossData })); - - const gradNormDataArray = parsedData - .filter(file => file.gradNormData && file.gradNormData.length > 0) - .map(file => ({ name: file.name, data: file.gradNormData })); - - const otherMetricKeys = otherConfigs.map(c => c.name || c.keyword); - const otherDataArrays = {}; - otherMetricKeys.forEach(key => { - otherDataArrays[key] = parsedData - .filter(file => file.othersData && file.othersData[key] && file.othersData[key].length > 0) - .map(file => ({ name: file.name, data: file.othersData[key] })); - }); - - // 计算显示的图表数量来决定布局 - const enabledFiles = files.filter(file => file.enabled !== false); - const showingLossCharts = showLoss && lossDataArray.length > 0; - const showingGradNormCharts = showGradNorm && gradNormDataArray.length > 0; - const showingLossComparison = showLoss && enabledFiles.length === 2 && lossDataArray.length === 2; - const showingGradNormComparison = showGradNorm && enabledFiles.length === 2 && gradNormDataArray.length === 2; - - // 计算实际显示的图表列数(不是图表总数) - const showingLossColumn = showingLossCharts || showingLossComparison; - const showingGradNormColumn = showingGradNormCharts || showingGradNormComparison; - const columnsShowing = (showingLossColumn ? 1 : 0) + (showingGradNormColumn ? 1 : 0); - - // 动态决定布局:如果只显示一列,使用全宽;否则使用两列 - const useFullWidth = columnsShowing <= 1; - const gridCols = useFullWidth ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2'; - return ( -
- {/* Loss Charts Column */} - {(showingLossCharts || showingLossComparison) && ( -
- {showingLossCharts && ( - - - - )} - - {/* Loss Comparison Chart (for 2 files) */} - {showingLossComparison && ( - - - - )} -
- )} - - {/* Grad Norm Charts Column */} - {(showingGradNormCharts || showingGradNormComparison) && ( -
- {showingGradNormCharts && ( - - - - )} - - {/* Grad Norm Comparison Chart (for 2 files) */} - {showingGradNormComparison && ( - - - - )} -
- )} - - {/* Statistics for comparison - spans all columns when showing both types */} - {enabledFiles.length === 2 && (showingLossComparison || showingGradNormComparison) && ( -
-

差值分析统计

-
- {showingLossComparison && ( -
-

Loss 差值统计

-
- {(() => { - const normalDiff = getComparisonData(lossDataArray[0], lossDataArray[1], 'normal'); - const absoluteDiff = getComparisonData(lossDataArray[0], lossDataArray[1], 'absolute'); - const relativeDiff = getComparisonData(lossDataArray[0], lossDataArray[1], 'relative'); - - const meanNormal = normalDiff.reduce((sum, p) => sum + p.y, 0) / normalDiff.length; - const meanAbsolute = absoluteDiff.reduce((sum, p) => sum + p.y, 0) / absoluteDiff.length; - const meanRelative = relativeDiff.reduce((sum, p) => sum + p.y, 0) / relativeDiff.length; - - return ( - <> -

Mean Difference: {meanNormal.toFixed(6)}

-

Mean Absolute Error: {meanAbsolute.toFixed(6)}

-

Mean Relative Error: {meanRelative.toFixed(6)}

- - ); - })()} -
-
- )} - {showingGradNormComparison && ( -
-

Grad Norm 差值统计

-
- {(() => { - const normalDiff = getComparisonData(gradNormDataArray[0], gradNormDataArray[1], 'normal'); - const absoluteDiff = getComparisonData(gradNormDataArray[0], gradNormDataArray[1], 'absolute'); - const relativeDiff = getComparisonData(gradNormDataArray[0], gradNormDataArray[1], 'relative'); - - const meanNormal = normalDiff.reduce((sum, p) => sum + p.y, 0) / normalDiff.length; - const meanAbsolute = absoluteDiff.reduce((sum, p) => sum + p.y, 0) / absoluteDiff.length; - const meanRelative = relativeDiff.reduce((sum, p) => sum + p.y, 0) / relativeDiff.length; - - return ( - <> -

Mean Difference: {meanNormal.toFixed(6)}

-

Mean Absolute Error: {meanAbsolute.toFixed(6)}

-

Mean Relative Error: {meanRelative.toFixed(6)}

- - ); - })()} -
-
- )} -
-
- )} - {otherMetricKeys.length > 0 && ( -
-
- {otherMetricKeys.map((key, idx) => ( -
- +
+
+ {metricsToShow.map((metric, idx) => { + const key = metric.name || metric.keyword || `metric${idx+1}`; + const dataArray = metricDataArrays[key] || []; + const showComparison = dataArray.length === 2; + return ( +
+ + + + {showComparison && ( + -
- ))} -
-
- )} + )} +
+ ); + })} +
); } diff --git a/src/components/FileConfigModal.jsx b/src/components/FileConfigModal.jsx index ad3d850..0d0f17c 100644 --- a/src/components/FileConfigModal.jsx +++ b/src/components/FileConfigModal.jsx @@ -25,16 +25,7 @@ const MODE_CONFIG = { export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingConfig }) { const [config, setConfig] = useState({ - loss: { - mode: 'keyword', - keyword: 'loss:', - regex: 'loss:\\s*([\\d.eE+-]+)' - }, - gradNorm: { - mode: 'keyword', - keyword: 'norm:', - regex: 'grad[\\s_]norm:\\s*([\\d.eE+-]+)' - }, + metrics: [], dataRange: { start: 0, // 起始位置,默认为0(第一个数据点) end: undefined, // 结束位置,默认为undefined(最后一个数据点) @@ -47,8 +38,7 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo // 如果文件有配置,使用文件配置,否则使用全局配置 const fileConfig = file.config || {}; setConfig({ - loss: fileConfig.loss || globalParsingConfig.loss, - gradNorm: fileConfig.gradNorm || globalParsingConfig.gradNorm, + metrics: fileConfig.metrics || globalParsingConfig.metrics, dataRange: fileConfig.dataRange || { start: 0, end: undefined, @@ -63,13 +53,12 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo onClose(); }; - const handleConfigChange = (type, field, value) => { + const handleMetricChange = (index, field, value) => { setConfig(prev => ({ ...prev, - [type]: { - ...prev[type], - [field]: value - } + metrics: prev.metrics.map((m, i) => + i === index ? { ...m, [field]: value } : m + ) })); }; @@ -87,13 +76,12 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo const syncFromGlobal = () => { setConfig(prev => ({ ...prev, - loss: { ...globalParsingConfig.loss }, - gradNorm: { ...globalParsingConfig.gradNorm } + metrics: globalParsingConfig.metrics.map(m => ({ ...m })) })); }; // 渲染配置项的函数 - const renderConfigPanel = (type, configItem) => { + const renderConfigPanel = (type, configItem, index) => { const ModeIcon = MODE_CONFIG[configItem.mode].icon; return ( @@ -105,7 +93,7 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo handleConfigChange(type, 'keyword', e.target.value)} + onChange={(e) => handleMetricChange(index, 'keyword', e.target.value)} className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" - placeholder={type === 'loss' ? 'loss:' : 'norm:'} + placeholder="keyword" />

支持模糊匹配,如 "loss" 可匹配 "training_loss" @@ -147,9 +135,9 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo handleConfigChange(type, 'regex', e.target.value)} + onChange={(e) => handleMetricChange(index, 'regex', e.target.value)} className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none font-mono" - placeholder={type === 'loss' ? 'loss:\\s*([\\d.eE+-]+)' : 'grad[\\s_]norm:\\s*([\\d.eE+-]+)'} + placeholder="value:\\s*([\\d.eE+-]+)" />

使用捕获组 () 来提取数值 @@ -210,23 +198,15 @@ export function FileConfigModal({ file, isOpen, onClose, onSave, globalParsingCo

- {/* Loss 配置 */} -
-

-

- {renderConfigPanel('loss', config.loss)} -
- - {/* Grad Norm 配置 */} -
-

-

- {renderConfigPanel('gradNorm', config.gradNorm)} -
+ {config.metrics.map((cfg, idx) => ( +
+

+

+ {renderConfigPanel(`metric-${idx}`, cfg, idx)} +
+ ))}
diff --git a/src/components/RegexControls.jsx b/src/components/RegexControls.jsx index 31e1f20..2844c8a 100644 --- a/src/components/RegexControls.jsx +++ b/src/components/RegexControls.jsx @@ -187,14 +187,13 @@ export class ValueExtractor { export function RegexControls({ globalParsingConfig, onGlobalParsingConfigChange, - onRegexChange, uploadedFiles = [], xRange, onXRangeChange, maxStep }) { const [showPreview, setShowPreview] = useState(false); - const [previewResults, setPreviewResults] = useState({ loss: [], gradNorm: [] }); + const [previewResults, setPreviewResults] = useState({}); // 提取数值的通用函数 const extractValues = useCallback((content, mode, config) => { @@ -210,53 +209,24 @@ export function RegexControls({ // 预览匹配结果 const previewMatches = useCallback(() => { - const results = { loss: [], gradNorm: [], others: {} }; + const results = {}; uploadedFiles.forEach(file => { if (file.content) { - // Loss匹配 - const lossMatches = extractValues( - file.content, - globalParsingConfig.loss.mode, - globalParsingConfig.loss - ); - results.loss.push({ - fileName: file.name, - count: lossMatches.length, - examples: lossMatches.slice(0, 3).map(m => ({ - value: m.value, - line: m.line, - text: m.text, - format: m.format - })) - }); - - // Grad Norm匹配 - const gradNormMatches = extractValues( - file.content, - globalParsingConfig.gradNorm.mode, - globalParsingConfig.gradNorm - ); - results.gradNorm.push({ - fileName: file.name, - count: gradNormMatches.length, - examples: gradNormMatches.slice(0, 3).map(m => ({ - value: m.value, - line: m.line, - text: m.text, - format: m.format - })) - }); - - globalParsingConfig.others.forEach((cfg, idx) => { - const matches = extractValues( - file.content, - cfg.mode, - cfg - ); - const key = cfg.name || `metric${idx+1}`; - if (!results.others[key]) results.others[key] = []; - results.others[key].push({ fileName: file.name, count: matches.length }); + globalParsingConfig.metrics.forEach((cfg, idx) => { + const matches = extractValues(file.content, cfg.mode, cfg); + const key = cfg.name || `metric${idx + 1}`; + if (!results[key]) results[key] = []; + results[key].push({ + fileName: file.name, + count: matches.length, + examples: matches.slice(0, 3).map(m => ({ + value: m.value, + line: m.line, + text: m.text, + format: m.format + })) + }); }); } }); @@ -268,52 +238,33 @@ export function RegexControls({ const smartRecommend = useCallback(() => { if (uploadedFiles.length === 0) return; - let bestLossConfig = null; - let bestGradNormConfig = null; - let maxLossCount = 0; - let maxGradNormCount = 0; - const allContent = uploadedFiles.map(f => f.content).join('\n'); - - // 测试关键词模式 - const lossKeywords = ['loss', 'training_loss', 'train_loss']; - const gradNormKeywords = ['grad_norm', 'gradient_norm', 'gnorm', 'global_norm']; - - lossKeywords.forEach(keyword => { - const matches = ValueExtractor.extractByKeyword(allContent, keyword); - if (matches.length > maxLossCount) { - maxLossCount = matches.length; - bestLossConfig = { mode: MATCH_MODES.KEYWORD, keyword }; - } - }); - - gradNormKeywords.forEach(keyword => { - const matches = ValueExtractor.extractByKeyword(allContent, keyword); - if (matches.length > maxGradNormCount) { - maxGradNormCount = matches.length; - bestGradNormConfig = { mode: MATCH_MODES.KEYWORD, keyword }; - } - }); - // 应用最佳配置到全局配置 - const newConfig = { ...globalParsingConfig }; - if (bestLossConfig) { - newConfig.loss = { - ...newConfig.loss, - mode: bestLossConfig.mode, - keyword: bestLossConfig.keyword - }; + const newMetrics = globalParsingConfig.metrics.map(m => ({ ...m })); + + if (newMetrics[0]) { + let maxCount = 0; + ['loss', 'training_loss', 'train_loss'].forEach(keyword => { + const matches = ValueExtractor.extractByKeyword(allContent, keyword); + if (matches.length > maxCount) { + maxCount = matches.length; + newMetrics[0] = { ...newMetrics[0], mode: MATCH_MODES.KEYWORD, keyword }; + } + }); } - - if (bestGradNormConfig) { - newConfig.gradNorm = { - ...newConfig.gradNorm, - mode: bestGradNormConfig.mode, - keyword: bestGradNormConfig.keyword - }; + + if (newMetrics[1]) { + let maxCount = 0; + ['grad_norm', 'gradient_norm', 'gnorm', 'global_norm'].forEach(keyword => { + const matches = ValueExtractor.extractByKeyword(allContent, keyword); + if (matches.length > maxCount) { + maxCount = matches.length; + newMetrics[1] = { ...newMetrics[1], mode: MATCH_MODES.KEYWORD, keyword }; + } + }); } - - onGlobalParsingConfigChange(newConfig); + + onGlobalParsingConfigChange({ metrics: newMetrics }); }, [uploadedFiles, globalParsingConfig, onGlobalParsingConfigChange]); // 当配置变化时更新预览 @@ -324,42 +275,28 @@ export function RegexControls({ }, [showPreview, previewMatches]); // 处理配置变化 - const handleConfigChange = (type, field, value) => { - const newConfig = { ...globalParsingConfig }; - newConfig[type] = { ...newConfig[type], [field]: value }; - - // 如果是正则表达式模式的变更,同时更新兼容的正则状态 - if (field === 'regex') { - if (type === 'loss') { - onRegexChange('loss', value); - } else { - onRegexChange('gradNorm', value); - } - } - - onGlobalParsingConfigChange(newConfig); - }; - - const handleOtherConfigChange = (index, field, value) => { - const newOthers = [...globalParsingConfig.others]; - newOthers[index] = { ...newOthers[index], [field]: value }; - const newConfig = { ...globalParsingConfig, others: newOthers }; - onGlobalParsingConfigChange(newConfig); + const handleMetricChange = (index, field, value) => { + const newMetrics = [...globalParsingConfig.metrics]; + newMetrics[index] = { ...newMetrics[index], [field]: value }; + onGlobalParsingConfigChange({ metrics: newMetrics }); }; const addMetric = () => { - const newOthers = [...globalParsingConfig.others, { - name: `metric${globalParsingConfig.others.length + 1}`, - mode: 'keyword', - keyword: '', - regex: '' - }]; - onGlobalParsingConfigChange({ ...globalParsingConfig, others: newOthers }); + const newMetrics = [ + ...globalParsingConfig.metrics, + { + name: `metric${globalParsingConfig.metrics.length + 1}`, + mode: 'keyword', + keyword: '', + regex: '' + } + ]; + onGlobalParsingConfigChange({ metrics: newMetrics }); }; const removeMetric = (index) => { - const newOthers = globalParsingConfig.others.filter((_, i) => i !== index); - onGlobalParsingConfigChange({ ...globalParsingConfig, others: newOthers }); + const newMetrics = globalParsingConfig.metrics.filter((_, i) => i !== index); + onGlobalParsingConfigChange({ metrics: newMetrics }); }; const handleXRangeChange = (field, value) => { @@ -373,17 +310,15 @@ export function RegexControls({ return (
- {type.startsWith('other') && ( -
- - onConfigChange('name', e.target.value)} - className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" - /> -
- )} +
+ + onConfigChange('name', e.target.value)} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" + /> +
{/* 模式选择 */}
- {/* Loss 配置 */} -
-

- - Loss 解析配置 -

- {renderConfigPanel('loss', globalParsingConfig.loss, (field, value) => handleConfigChange('loss', field, value))} -
- - {/* Grad Norm 配置 */} -
-

- - Grad Norm 解析配置 -

- {renderConfigPanel('gradnorm', globalParsingConfig.gradNorm, (field, value) => handleConfigChange('gradNorm', field, value))} -
- - {globalParsingConfig.others.map((cfg, idx) => ( + {globalParsingConfig.metrics.map((cfg, idx) => (

- {cfg.name || `Metric ${idx+1}`} 解析配置 + {cfg.name || `Metric ${idx + 1}`} 解析配置

- {renderConfigPanel(`other-${idx}`, cfg, (field, value) => handleOtherConfigChange(idx, field, value))} + {renderConfigPanel(`metric-${idx}`, cfg, (field, value) => handleMetricChange(idx, field, value))}
))}

- {cfg.name || `Metric ${idx + 1}`} 解析配置 + {getMetricTitle(cfg, idx)} 解析配置

- {renderConfigPanel(`metric-${idx}`, cfg, (field, value) => handleMetricChange(idx, field, value))} + {renderConfigPanel(`metric-${idx}`, cfg, (field, value) => handleMetricChange(idx, field, value), idx)}
))}