Skip to content
Closed
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
39 changes: 34 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@
"chart.js": "^4.5.0",
"chartjs-plugin-zoom": "^2.2.0",
"lucide-react": "^0.522.0",
"lz-string": "^1.5.0",
"postcss": "^8.5.6",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.1.0",
"tailwindcss": "^3.4.4"
"tailwindcss": "^3.4.4",
"zustand": "^5.0.7"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
Expand Down
154 changes: 36 additions & 118 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,148 +1,66 @@
import React, { useState, useCallback, useEffect } from 'react';
import { useStore } from './store';
import { deserializeStateFromURL } from './utils/sharing';
import { FileUpload } from './components/FileUpload';
import { RegexControls } from './components/RegexControls';
import { FileList } from './components/FileList';
import ChartContainer from './components/ChartContainer';
import { ComparisonControls } from './components/ComparisonControls';
import { Header } from './components/Header';
import { FileConfigModal } from './components/FileConfigModal';
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
import { mergeFilesWithReplacement } from './utils/mergeFiles.js';

function App() {
const [uploadedFiles, setUploadedFiles] = useState([]);
const {
uploadedFiles,
globalParsingConfig,
compareMode,
relativeBaseline,
absoluteBaseline,
configModalOpen,
configFile,
xRange,
maxStep,
sidebarVisible,
handleFilesUploaded,
processGlobalFiles,
handleFileRemove,
handleFileToggle,
handleFileConfig,
handleConfigSave,
handleConfigClose,
handleGlobalParsingConfigChange,
setCompareMode,
setRelativeBaseline,
setAbsoluteBaseline,
setXRange,
setMaxStep,
setSidebarVisible,
} = useStore();

// 全局解析配置状态
const [globalParsingConfig, setGlobalParsingConfig] = useState({
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+-]+)'
}
]
});

const [compareMode, setCompareMode] = useState('normal');
const [relativeBaseline, setRelativeBaseline] = useState(0.002);
const [absoluteBaseline, setAbsoluteBaseline] = useState(0.005);
const [configModalOpen, setConfigModalOpen] = useState(false);
const [configFile, setConfigFile] = useState(null);
const [globalDragOver, setGlobalDragOver] = useState(false);
const [, setDragCounter] = useState(0);
const [xRange, setXRange] = useState({ min: undefined, max: undefined });
const [maxStep, setMaxStep] = useState(0);
const [sidebarVisible, setSidebarVisible] = useState(true);

const handleFilesUploaded = useCallback((files) => {
const filesWithDefaults = files.map(file => ({
...file,
enabled: true,
config: {
// 使用全局解析配置作为默认值
metrics: globalParsingConfig.metrics.map(m => ({ ...m })),
dataRange: {
start: 0, // 默认从第一个数据点开始
end: undefined, // 默认到最后一个数据点
useRange: false // 保留这个字段用于向后兼容,但默认不启用
}
}
}));
setUploadedFiles(prev => mergeFilesWithReplacement(prev, filesWithDefaults));
}, [globalParsingConfig]);

// 全局文件处理函数
const processGlobalFiles = useCallback((files) => {
const fileArray = Array.from(files);

if (fileArray.length === 0) return;
const [dragCounter, setDragCounter] = useState(0);

const processedFiles = fileArray.map(file => ({
file,
name: file.name,
id: Math.random().toString(36).substr(2, 9),
data: null,
content: null
}));

// Read file contents
Promise.all(
processedFiles.map(fileObj =>
new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
fileObj.content = e.target.result;
resolve(fileObj);
};
reader.readAsText(fileObj.file);
})
)
).then(files => {
handleFilesUploaded(files);
});
}, [handleFilesUploaded]);

const handleFileRemove = useCallback((index) => {
setUploadedFiles(prev => prev.filter((_, i) => i !== index));
}, []);

const handleFileToggle = useCallback((index, enabled) => {
setUploadedFiles(prev => prev.map((file, i) =>
i === index ? { ...file, enabled } : file
));
}, []);

const handleFileConfig = useCallback((file) => {
setConfigFile(file);
setConfigModalOpen(true);
}, []);

const handleConfigSave = useCallback((fileId, config) => {
setUploadedFiles(prev => prev.map(file =>
file.id === fileId ? { ...file, config } : file
));
}, []);

const handleConfigClose = useCallback(() => {
setConfigModalOpen(false);
setConfigFile(null);
}, []);

// 全局解析配置变更处理
const handleGlobalParsingConfigChange = useCallback((newConfig) => {
setGlobalParsingConfig(newConfig);

// 同步所有文件的解析配置
setUploadedFiles(prev => prev.map(file => ({
...file,
config: {
...file.config,
metrics: newConfig.metrics.map(m => ({ ...m }))
}
})));
useEffect(() => {
const sharedState = deserializeStateFromURL();
if (sharedState) {
useStore.setState(sharedState);
// Clear the hash to avoid re-loading the shared state on every refresh
window.location.hash = '';
}
}, []);

// 全局拖拽事件处理
const handleGlobalDragEnter = useCallback((e) => {
e.preventDefault();
setDragCounter(prev => prev + 1);

// 检查是否包含文件
if (e.dataTransfer.types.includes('Files')) {
setGlobalDragOver(true);
}
}, []);

const handleGlobalDragOver = useCallback((e) => {
e.preventDefault();
// 设置拖拽效果
e.dataTransfer.dropEffect = 'copy';
}, []);

Expand Down
37 changes: 24 additions & 13 deletions src/components/ChartContainer.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useMemo, useRef, useCallback, useEffect } from 'react';
import { Line } from 'react-chartjs-2';
import { ResizablePanel } from './ResizablePanel';
import { movingAverage } from '../utils/smoothing.js';
import {
Chart as ChartJS,
Chart,
Expand Down Expand Up @@ -60,16 +61,20 @@ const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover })
);
};

export default function ChartContainer({
files,
metrics = [],
compareMode,
relativeBaseline = 0.002,
absoluteBaseline = 0.005,
xRange = { min: undefined, max: undefined },
onXRangeChange,
onMaxStepChange
}) {
import { useStore } from '../store';

export default function ChartContainer() {
const files = useStore(state => state.uploadedFiles);
const metrics = useStore(state => state.globalParsingConfig.metrics);
const compareMode = useStore(state => state.compareMode);
const relativeBaseline = useStore(state => state.relativeBaseline);
const absoluteBaseline = useStore(state => state.absoluteBaseline);
const xRange = useStore(state => state.xRange);
const onXRangeChange = useStore(state => state.setXRange);
const onMaxStepChange = useStore(state => state.setMaxStep);
const smoothingEnabled = useStore(state => state.smoothingEnabled);
const smoothingWindow = useStore(state => state.smoothingWindow);

const chartRefs = useRef(new Map());
const registerChart = useCallback((id, inst) => {
chartRefs.current.set(id, inst);
Expand Down Expand Up @@ -153,9 +158,16 @@ export default function ChartContainer({
});
}

// Apply smoothing if enabled
if (smoothingEnabled && smoothingWindow > 1) {
Object.keys(metricsData).forEach(key => {
metricsData[key] = movingAverage(metricsData[key], smoothingWindow);
});
}

return { ...file, metricsData };
});
}, [files, metrics]);
}, [files, metrics, smoothingEnabled, smoothingWindow]);

useEffect(() => {
const maxStep = parsedData.reduce((m, f) => {
Expand All @@ -176,10 +188,9 @@ export default function ChartContainer({
}
}, [parsedData, onXRangeChange]);

const colors = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#f97316'];
const createChartData = dataArray => ({
datasets: dataArray.map((item, index) => {
const color = colors[index % colors.length];
const color = item.color || '#000000'; // Fallback to black
return {
label: item.name?.replace(/\.(log|txt)$/i, '') || `File ${index + 1}`,
data: item.data,
Expand Down
14 changes: 10 additions & 4 deletions src/components/ComparisonControls.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import React from 'react';
import { BarChart2 } from 'lucide-react';
import { useStore } from '../store';

export function ComparisonControls() {
const {
compareMode,
onCompareModeChange
} = useStore(state => ({
compareMode: state.compareMode,
onCompareModeChange: state.setCompareMode
}));

export function ComparisonControls({
compareMode,
onCompareModeChange
}) {
const modes = [
{ value: 'normal', label: '📊 平均误差 (normal)', description: '未取绝对值的平均误差' },
{ value: 'absolute', label: '📈 平均误差 (absolute)', description: '绝对值差值的平均' },
Expand Down
Loading