Skip to content

Commit eafb76d

Browse files
committed
feat: add file configuration modal and enhance file list functionality
1 parent 0f74770 commit eafb76d

File tree

4 files changed

+394
-27
lines changed

4 files changed

+394
-27
lines changed

src/App.jsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FileList } from './components/FileList';
55
import ChartContainer from './components/ChartContainer';
66
import { ComparisonControls } from './components/ComparisonControls';
77
import { Header } from './components/Header';
8+
import { FileConfigModal } from './components/FileConfigModal';
89

910
function App() {
1011
const [uploadedFiles, setUploadedFiles] = useState([]);
@@ -16,15 +17,52 @@ function App() {
1617
const [absoluteBaseline, setAbsoluteBaseline] = useState(0.005);
1718
const [showLoss, setShowLoss] = useState(true);
1819
const [showGradNorm, setShowGradNorm] = useState(false);
20+
const [configModalOpen, setConfigModalOpen] = useState(false);
21+
const [configFile, setConfigFile] = useState(null);
1922

2023
const handleFilesUploaded = useCallback((files) => {
21-
setUploadedFiles(prev => [...prev, ...files]);
24+
const filesWithDefaults = files.map(file => ({
25+
...file,
26+
enabled: true,
27+
config: {
28+
lossRegex: 'loss:\\s*([\\d.]+)',
29+
gradNormRegex: 'grad norm:\\s*([\\d.]+)',
30+
dataRange: {
31+
start: '',
32+
end: '',
33+
useRange: false
34+
}
35+
}
36+
}));
37+
setUploadedFiles(prev => [...prev, ...filesWithDefaults]);
2238
}, []);
2339

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

44+
const handleFileToggle = useCallback((index, enabled) => {
45+
setUploadedFiles(prev => prev.map((file, i) =>
46+
i === index ? { ...file, enabled } : file
47+
));
48+
}, []);
49+
50+
const handleFileConfig = useCallback((file) => {
51+
setConfigFile(file);
52+
setConfigModalOpen(true);
53+
}, []);
54+
55+
const handleConfigSave = useCallback((fileId, config) => {
56+
setUploadedFiles(prev => prev.map(file =>
57+
file.id === fileId ? { ...file, config } : file
58+
));
59+
}, []);
60+
61+
const handleConfigClose = useCallback(() => {
62+
setConfigModalOpen(false);
63+
setConfigFile(null);
64+
}, []);
65+
2866
const handleRegexChange = useCallback((type, value) => {
2967
if (type === 'loss') {
3068
setLossRegex(value);
@@ -59,6 +97,8 @@ function App() {
5997
<FileList
6098
files={uploadedFiles}
6199
onFileRemove={handleFileRemove}
100+
onFileToggle={handleFileToggle}
101+
onFileConfig={handleFileConfig}
62102
/>
63103

64104
{uploadedFiles.length === 2 && (
@@ -212,6 +252,13 @@ function App() {
212252
</section>
213253
</main>
214254
</div>
255+
256+
<FileConfigModal
257+
file={configFile}
258+
isOpen={configModalOpen}
259+
onClose={handleConfigClose}
260+
onSave={handleConfigSave}
261+
/>
215262
</div>
216263
);
217264
}

src/components/ChartContainer.jsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,23 @@ export default function ChartContainer({
5252
showGradNorm = false
5353
}) {
5454
const parsedData = useMemo(() => {
55-
return files.map(file => {
55+
// 只处理已启用的文件
56+
const enabledFiles = files.filter(file => file.enabled !== false);
57+
58+
return enabledFiles.map(file => {
5659
if (!file.content) return { ...file, lossData: [], gradNormData: [] };
5760

5861
const lines = file.content.split('\n');
5962
const lossData = [];
6063
const gradNormData = [];
6164

6265
try {
63-
const lossRegexObj = new RegExp(lossRegex);
64-
const gradNormRegexObj = new RegExp(gradNormRegex);
66+
// 使用文件的独立配置,如果没有则使用全局配置
67+
const fileLossRegex = file.config?.lossRegex || lossRegex;
68+
const fileGradNormRegex = file.config?.gradNormRegex || gradNormRegex;
69+
70+
const lossRegexObj = new RegExp(fileLossRegex);
71+
const gradNormRegexObj = new RegExp(fileGradNormRegex);
6572

6673
lines.forEach((line, index) => {
6774
// Reset regex lastIndex for global flag
@@ -89,6 +96,41 @@ export default function ChartContainer({
8996
console.error('Regex error:', error);
9097
}
9198

99+
// 应用数据范围过滤
100+
const dataRange = file.config?.dataRange;
101+
if (dataRange && dataRange.useRange) {
102+
const applyRangeFilter = (data) => {
103+
if (data.length === 0) return data;
104+
105+
const start = dataRange.start ? Math.max(1, parseInt(dataRange.start)) : 1;
106+
const end = dataRange.end ? Math.max(1, parseInt(dataRange.end)) : data.length;
107+
108+
// 验证范围有效性
109+
if (start > end || start > data.length) {
110+
console.warn(`Invalid range for file ${file.name}: start=${start}, end=${end}, length=${data.length}`);
111+
return data; // 返回原始数据
112+
}
113+
114+
// 转换为0基索引并切片 (start-1 到 end,包含end)
115+
const startIndex = Math.max(0, start - 1);
116+
const endIndex = Math.min(data.length, end);
117+
118+
return data.slice(startIndex, endIndex);
119+
};
120+
121+
const filteredLossData = applyRangeFilter(lossData);
122+
const filteredGradNormData = applyRangeFilter(gradNormData);
123+
124+
// 重新索引数据点
125+
const reindexData = (data) => data.map((point, index) => ({ x: index, y: point.y }));
126+
127+
return {
128+
...file,
129+
lossData: reindexData(filteredLossData),
130+
gradNormData: reindexData(filteredGradNormData)
131+
};
132+
}
133+
92134
return { ...file, lossData, gradNormData };
93135
});
94136
}, [files, lossRegex, gradNormRegex]);

src/components/FileConfigModal.jsx

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { X, Settings, TrendingDown, TrendingUp, Sliders, BarChart3, Play, Square } from 'lucide-react';
3+
4+
export function FileConfigModal({ file, isOpen, onClose, onSave }) {
5+
const [config, setConfig] = useState({
6+
lossRegex: '',
7+
gradNormRegex: '',
8+
dataRange: {
9+
start: '', // 起始位置 (可选,留空表示从开头)
10+
end: '', // 结束位置 (可选,留空表示到结尾)
11+
useRange: false // 是否启用范围限制
12+
}
13+
});
14+
15+
useEffect(() => {
16+
if (file && isOpen) {
17+
setConfig({
18+
lossRegex: file.config?.lossRegex || 'loss:\\s*([\\d.]+)',
19+
gradNormRegex: file.config?.gradNormRegex || 'grad_norm:\\s*([\\d.]+)',
20+
dataRange: file.config?.dataRange || {
21+
start: '',
22+
end: '',
23+
useRange: false
24+
}
25+
});
26+
}
27+
}, [file, isOpen]);
28+
29+
const handleSave = () => {
30+
onSave(file.id, config);
31+
onClose();
32+
};
33+
34+
const handleRangeChange = (field, value) => {
35+
setConfig(prev => ({
36+
...prev,
37+
dataRange: {
38+
...prev.dataRange,
39+
[field]: value
40+
}
41+
}));
42+
};
43+
44+
const handleUseRangeToggle = (useRange) => {
45+
setConfig(prev => ({
46+
...prev,
47+
dataRange: {
48+
...prev.dataRange,
49+
useRange,
50+
// 如果禁用范围,清空输入值
51+
start: useRange ? prev.dataRange.start : '',
52+
end: useRange ? prev.dataRange.end : ''
53+
}
54+
}));
55+
};
56+
57+
if (!isOpen || !file) return null;
58+
59+
return (
60+
<div
61+
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
62+
role="dialog"
63+
aria-labelledby="config-modal-title"
64+
aria-modal="true"
65+
onClick={onClose}
66+
>
67+
<div
68+
className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
69+
onClick={(e) => e.stopPropagation()}
70+
>
71+
<div className="flex items-center justify-between p-4 border-b bg-gray-50">
72+
<div className="flex items-center gap-2">
73+
<Settings size={20} className="text-blue-600" aria-hidden="true" />
74+
<h2 id="config-modal-title" className="text-lg font-semibold text-gray-800">
75+
配置文件: {file.name}
76+
</h2>
77+
</div>
78+
<button
79+
onClick={onClose}
80+
className="p-1 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
81+
aria-label="关闭配置对话框"
82+
>
83+
<X size={20} />
84+
</button>
85+
</div>
86+
87+
<div className="p-6 space-y-6">
88+
{/* 正则表达式配置 */}
89+
<section>
90+
<h3 className="text-base font-medium text-gray-800 mb-4 flex items-center gap-2">
91+
<BarChart3 size={16} className="text-indigo-600" aria-hidden="true" />
92+
正则表达式配置
93+
</h3>
94+
95+
<div className="space-y-4">
96+
<div>
97+
<label
98+
htmlFor="config-loss-regex"
99+
className="block text-sm font-medium text-gray-700 mb-2 flex items-center gap-1"
100+
>
101+
<TrendingDown size={16} className="text-red-500" aria-hidden="true" />
102+
Loss 匹配规则
103+
</label>
104+
<input
105+
id="config-loss-regex"
106+
type="text"
107+
value={config.lossRegex}
108+
onChange={(e) => setConfig(prev => ({ ...prev, lossRegex: e.target.value }))}
109+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none text-sm font-mono"
110+
placeholder="loss:\\s*([\\d.]+)"
111+
/>
112+
<p className="text-xs text-gray-500 mt-1">
113+
用于匹配日志中的损失函数值,括号内为捕获组
114+
</p>
115+
</div>
116+
117+
<div>
118+
<label
119+
htmlFor="config-gradnorm-regex"
120+
className="block text-sm font-medium text-gray-700 mb-2 flex items-center gap-1"
121+
>
122+
<TrendingUp size={16} className="text-green-500" aria-hidden="true" />
123+
Grad Norm 匹配规则
124+
</label>
125+
<input
126+
id="config-gradnorm-regex"
127+
type="text"
128+
value={config.gradNormRegex}
129+
onChange={(e) => setConfig(prev => ({ ...prev, gradNormRegex: e.target.value }))}
130+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none text-sm font-mono"
131+
placeholder="grad_norm:\\s*([\\d.]+)"
132+
/>
133+
<p className="text-xs text-gray-500 mt-1">
134+
用于匹配日志中的梯度范数值,括号内为捕获组
135+
</p>
136+
</div>
137+
</div>
138+
</section>
139+
140+
{/* 数据范围配置 */}
141+
<section>
142+
<h3 className="text-base font-medium text-gray-800 mb-4 flex items-center gap-2">
143+
<Sliders size={16} className="text-purple-600" aria-hidden="true" />
144+
数据范围配置
145+
</h3>
146+
147+
<div className="space-y-4">
148+
<div>
149+
<label className="flex items-center cursor-pointer hover:bg-gray-50 p-2 rounded">
150+
<input
151+
type="checkbox"
152+
checked={config.dataRange.useRange}
153+
onChange={(e) => handleUseRangeToggle(e.target.checked)}
154+
className="rounded border-gray-300 text-blue-600 focus:ring-2 focus:ring-blue-500"
155+
/>
156+
<span className="ml-2 text-sm font-medium">启用数据范围限制</span>
157+
</label>
158+
<p className="text-xs text-gray-500 mt-1 ml-6">
159+
勾选后可以指定要显示的数据点范围
160+
</p>
161+
</div>
162+
163+
{config.dataRange.useRange && (
164+
<div className="bg-gray-50 p-4 rounded-lg border">
165+
<div className="grid grid-cols-2 gap-4">
166+
<div>
167+
<label
168+
htmlFor="range-start"
169+
className="block text-sm font-medium text-gray-700 mb-2 flex items-center gap-1"
170+
>
171+
<Play size={14} className="text-green-600" aria-hidden="true" />
172+
起始位置
173+
</label>
174+
<input
175+
id="range-start"
176+
type="number"
177+
min="1"
178+
placeholder="留空表示从开头"
179+
value={config.dataRange.start}
180+
onChange={(e) => handleRangeChange('start', e.target.value)}
181+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none text-sm"
182+
/>
183+
<p className="text-xs text-gray-500 mt-1">
184+
第几个数据点开始(从1开始)
185+
</p>
186+
</div>
187+
188+
<div>
189+
<label
190+
htmlFor="range-end"
191+
className="block text-sm font-medium text-gray-700 mb-2 flex items-center gap-1"
192+
>
193+
<Square size={14} className="text-red-600" aria-hidden="true" />
194+
结束位置
195+
</label>
196+
<input
197+
id="range-end"
198+
type="number"
199+
min="1"
200+
placeholder="留空表示到结尾"
201+
value={config.dataRange.end}
202+
onChange={(e) => handleRangeChange('end', e.target.value)}
203+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none text-sm"
204+
/>
205+
<p className="text-xs text-gray-500 mt-1">
206+
第几个数据点结束(包含该点)
207+
</p>
208+
</div>
209+
</div>
210+
211+
<div className="mt-3 p-3 bg-blue-50 rounded border border-blue-200">
212+
<p className="text-sm text-blue-800 font-medium">
213+
示例说明:
214+
</p>
215+
<ul className="text-xs text-blue-700 mt-2 space-y-1">
216+
<li>• 起始: 1, 结束: 100 → 显示第1到第100个数据点</li>
217+
<li>• 起始: 50, 结束: 留空 → 显示第50个数据点到结尾</li>
218+
<li>• 起始: 留空, 结束: 200 → 显示开头到第200个数据点</li>
219+
<li>• 起始: 100, 结束: 50 → 无效范围,将显示所有数据</li>
220+
</ul>
221+
</div>
222+
</div>
223+
)}
224+
</div>
225+
</section>
226+
</div>
227+
228+
<div className="flex justify-end gap-3 p-4 border-t bg-gray-50">
229+
<button
230+
onClick={onClose}
231+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
232+
>
233+
取消
234+
</button>
235+
<button
236+
onClick={handleSave}
237+
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
238+
>
239+
保存配置
240+
</button>
241+
</div>
242+
</div>
243+
</div>
244+
);
245+
}

0 commit comments

Comments
 (0)