diff --git a/src/App.jsx b/src/App.jsx
index 4a942e6..b12b4ee 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -171,17 +171,25 @@ function App() {
const handleGlobalParsingConfigChange = useCallback((newConfig) => {
setGlobalParsingConfig(newConfig);
- // Sync parsing config to all files
- setUploadedFiles(prev => prev.map(file => ({
- ...file,
- config: {
- ...file.config,
- metrics: newConfig.metrics.map(m => ({ ...m })),
- useStepKeyword: newConfig.useStepKeyword,
- stepKeyword: newConfig.stepKeyword
- }
- })));
- }, []);
+ // Sync parsing config to files that still use the global metrics
+ setUploadedFiles(prev => prev.map(file => {
+ const fileConfig = file.config || {};
+ const usesGlobalMetrics = !fileConfig.metrics ||
+ JSON.stringify(fileConfig.metrics) === JSON.stringify(globalParsingConfig.metrics);
+
+ return {
+ ...file,
+ config: {
+ ...fileConfig,
+ ...(usesGlobalMetrics && {
+ metrics: newConfig.metrics.map(m => ({ ...m }))
+ }),
+ useStepKeyword: newConfig.useStepKeyword,
+ stepKeyword: newConfig.stepKeyword
+ }
+ };
+ }));
+ }, [globalParsingConfig]);
// Reset configuration
const handleResetConfig = useCallback(() => {
diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx
index 8a6c5e3..c24e04d 100644
--- a/src/components/ChartContainer.jsx
+++ b/src/components/ChartContainer.jsx
@@ -260,12 +260,13 @@ export default function ChartContainer({
return results;
};
- metrics.forEach(metric => {
+ metrics.forEach((metric, idx) => {
+ const fileMetric = file.config?.metrics?.[idx] || metric;
let points = [];
- if (metric.mode === 'keyword') {
- points = extractByKeyword(lines, metric.keyword);
- } else if (metric.regex) {
- const reg = new RegExp(metric.regex);
+ if (fileMetric.mode === 'keyword') {
+ points = extractByKeyword(lines, fileMetric.keyword);
+ } else if (fileMetric.regex) {
+ const reg = new RegExp(fileMetric.regex);
lines.forEach(line => {
reg.lastIndex = 0;
const m = reg.exec(line);
@@ -278,7 +279,20 @@ export default function ChartContainer({
}
});
}
- metricsData[metric.name || metric.keyword] = points;
+
+ let key = '';
+ if (metric.name && metric.name.trim()) {
+ key = metric.name.trim();
+ } else if (metric.keyword) {
+ key = metric.keyword.replace(/[::]/g, '').trim();
+ } else if (metric.regex) {
+ const sanitized = metric.regex.replace(/[^a-zA-Z0-9_]/g, '').trim();
+ key = sanitized || `metric${idx + 1}`;
+ } else {
+ key = `metric${idx + 1}`;
+ }
+
+ metricsData[key] = points;
});
const range = file.config?.dataRange;
diff --git a/src/components/__tests__/ChartContainer.test.jsx b/src/components/__tests__/ChartContainer.test.jsx
index 378703a..04b1785 100644
--- a/src/components/__tests__/ChartContainer.test.jsx
+++ b/src/components/__tests__/ChartContainer.test.jsx
@@ -175,4 +175,38 @@ describe('ChartContainer', () => {
opts.plugins.zoom.pan.onPanComplete({ chart: { scales: { x: { min: 0, max: 10 } } } });
opts.plugins.zoom.zoom.onZoomComplete({ chart: { scales: { x: { min: 2, max: 4 } } } });
});
+
+ it('uses per-file metric configuration when provided', () => {
+ const onXRangeChange = vi.fn();
+ const onMaxStepChange = vi.fn();
+ const files = [
+ { name: 'a.log', enabled: true, content: 'loss: 1\nloss: 2' },
+ {
+ name: 'b.log',
+ enabled: true,
+ content: 'train_loss: 3\ntrain_loss: 4',
+ config: { metrics: [{ mode: 'keyword', keyword: 'train_loss:' }] }
+ }
+ ];
+ const metrics = [{ name: 'loss', mode: 'keyword', keyword: 'loss:' }];
+
+ render(
+