Skip to content

Commit e04e60a

Browse files
I've just implemented a comprehensive roadmap to evolve your ML Log Analyzer from a simple client-side tool into a robust, extensible, and scalable platform.
The changes are implemented in three main phases: Phase 1: Foundational Improvements - Implemented session persistence using localStorage to prevent data loss on page refresh. - Expanded the list of metric presets to include common defaults for PyTorch, Keras, and Hugging Face. - Significantly increased unit test coverage for the core data parsing logic (`ValueExtractor`). Phase 2: Platformization with a Backend - Integrated the Firebase SDK to provide backend services. - Implemented user authentication with Google Sign-In. - Added a Firestore database backend to save and load workspaces for authenticated users, while retaining localStorage for anonymous sessions. Phase 3: Extensibility with a Plugin Architecture - Refactored the data extraction logic into a modular plugin system. - Each extraction method (Keyword, Regex) is now a self-contained plugin with its own logic and UI configuration. - A central plugin registry allows for easy addition of new parsing capabilities in the future.
1 parent 58c751b commit e04e60a

12 files changed

Lines changed: 1565 additions & 391 deletions

File tree

package-lock.json

Lines changed: 1037 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"autoprefixer": "^10.4.21",
1818
"chart.js": "^4.5.0",
1919
"chartjs-plugin-zoom": "^2.2.0",
20+
"firebase": "^12.0.0",
2021
"lucide-react": "^0.522.0",
2122
"postcss": "^8.5.6",
2223
"react": "^19.1.0",

src/App.jsx

Lines changed: 142 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,29 @@
11
import React, { useState, useCallback, useEffect } from 'react';
2+
3+
// Helper to load state from localStorage
4+
const loadState = () => {
5+
try {
6+
const serializedState = localStorage.getItem('workspaceV1');
7+
if (serializedState === null) return undefined;
8+
const state = JSON.parse(serializedState);
9+
// Files content is not persisted, just their configs and names.
10+
// User will be prompted to re-upload if they want to see the charts.
11+
if (state.uploadedFiles) {
12+
state.uploadedFiles = state.uploadedFiles.map(file => ({
13+
...file,
14+
file: null,
15+
content: null,
16+
data: null,
17+
}));
18+
}
19+
return state;
20+
} catch (err) {
21+
console.warn("Could not load state from localStorage", err);
22+
return undefined;
23+
}
24+
};
25+
26+
const persistedState = loadState();
227
import { FileUpload } from './components/FileUpload';
328
import { RegexControls } from './components/RegexControls';
429
import { FileList } from './components/FileList';
@@ -8,12 +33,15 @@ import { Header } from './components/Header';
833
import { FileConfigModal } from './components/FileConfigModal';
934
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
1035
import { mergeFilesWithReplacement } from './utils/mergeFiles.js';
36+
import { signInWithGoogle, doSignOut, onAuthChange } from './services/authService.js';
37+
import { saveWorkspace, loadWorkspace } from './services/workspaceService.js';
1138

1239
function App() {
13-
const [uploadedFiles, setUploadedFiles] = useState([]);
40+
const [currentUser, setCurrentUser] = useState(null);
41+
const [uploadedFiles, setUploadedFiles] = useState(persistedState?.uploadedFiles || []);
1442

1543
// 全局解析配置状态
16-
const [globalParsingConfig, setGlobalParsingConfig] = useState({
44+
const [globalParsingConfig, setGlobalParsingConfig] = useState(persistedState?.globalParsingConfig || {
1745
metrics: [
1846
{
1947
name: 'Loss',
@@ -30,16 +58,107 @@ function App() {
3058
]
3159
});
3260

33-
const [compareMode, setCompareMode] = useState('normal');
34-
const [relativeBaseline, setRelativeBaseline] = useState(0.002);
35-
const [absoluteBaseline, setAbsoluteBaseline] = useState(0.005);
61+
const [compareMode, setCompareMode] = useState(persistedState?.compareMode || 'normal');
62+
const [relativeBaseline, setRelativeBaseline] = useState(persistedState?.relativeBaseline || 0.002);
63+
const [absoluteBaseline, setAbsoluteBaseline] = useState(persistedState?.absoluteBaseline || 0.005);
3664
const [configModalOpen, setConfigModalOpen] = useState(false);
3765
const [configFile, setConfigFile] = useState(null);
3866
const [globalDragOver, setGlobalDragOver] = useState(false);
3967
const [, setDragCounter] = useState(0);
40-
const [xRange, setXRange] = useState({ min: undefined, max: undefined });
68+
const [xRange, setXRange] = useState(persistedState?.xRange || { min: undefined, max: undefined });
4169
const [maxStep, setMaxStep] = useState(0);
42-
const [sidebarVisible, setSidebarVisible] = useState(true);
70+
const [sidebarVisible, setSidebarVisible] = useState(persistedState?.sidebarVisible !== undefined ? persistedState.sidebarVisible : true);
71+
72+
const applyWorkspace = (workspace) => {
73+
if (!workspace) return;
74+
75+
// Clear local storage if we are loading from the cloud
76+
localStorage.removeItem('workspaceV1');
77+
78+
const files = workspace.uploadedFiles || [];
79+
// Ensure files are in the correct format (without content)
80+
setUploadedFiles(files.map(f => ({ ...f, file: null, content: null, data: null })));
81+
82+
setGlobalParsingConfig(workspace.globalParsingConfig || { metrics: [] });
83+
setCompareMode(workspace.compareMode || 'normal');
84+
setRelativeBaseline(workspace.relativeBaseline || 0.002);
85+
setAbsoluteBaseline(workspace.absoluteBaseline || 0.005);
86+
setXRange(workspace.xRange || { min: undefined, max: undefined });
87+
setSidebarVisible(workspace.sidebarVisible !== undefined ? workspace.sidebarVisible : true);
88+
};
89+
90+
// Effect for saving state
91+
useEffect(() => {
92+
const stateToSave = {
93+
uploadedFiles: uploadedFiles.map(f => ({
94+
id: f.id,
95+
name: f.name,
96+
enabled: f.enabled,
97+
config: f.config,
98+
})),
99+
globalParsingConfig,
100+
compareMode,
101+
relativeBaseline,
102+
absoluteBaseline,
103+
xRange,
104+
sidebarVisible,
105+
};
106+
107+
if (currentUser) {
108+
saveWorkspace(currentUser.uid, stateToSave)
109+
.catch(err => console.warn("Could not save workspace to Firestore", err));
110+
} else {
111+
try {
112+
const serializedState = JSON.stringify(stateToSave);
113+
localStorage.setItem('workspaceV1', serializedState);
114+
} catch (err) {
115+
console.warn("Could not save state to localStorage", err);
116+
}
117+
}
118+
}, [
119+
currentUser,
120+
uploadedFiles,
121+
globalParsingConfig,
122+
compareMode,
123+
relativeBaseline,
124+
absoluteBaseline,
125+
xRange,
126+
sidebarVisible
127+
]);
128+
129+
// Effect for handling auth changes and loading data
130+
useEffect(() => {
131+
const unsubscribe = onAuthChange(async (user) => {
132+
if (user) {
133+
const workspace = await loadWorkspace(user.uid);
134+
console.log("User signed in. Workspace from cloud:", workspace);
135+
if (workspace) {
136+
applyWorkspace(workspace);
137+
}
138+
setCurrentUser(user);
139+
} else {
140+
console.log("User signed out.");
141+
setCurrentUser(null);
142+
}
143+
});
144+
return () => unsubscribe();
145+
}, []);
146+
147+
const handleLogin = async () => {
148+
try {
149+
await signInWithGoogle();
150+
} catch (error) {
151+
console.error("Error signing in with Google", error);
152+
}
153+
};
154+
155+
const handleLogout = async () => {
156+
try {
157+
await doSignOut();
158+
} catch (error) {
159+
console.error("Error signing out", error);
160+
}
161+
};
43162

44163
const handleFilesUploaded = useCallback((files) => {
45164
const filesWithDefaults = files.map(file => ({
@@ -300,6 +419,22 @@ function App() {
300419
</svg>
301420
<span>GitHub</span>
302421
</a>
422+
{currentUser ? (
423+
<button
424+
onClick={handleLogout}
425+
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700 hover:bg-red-200 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
426+
title={`以 ${currentUser.displayName} 身份登出`}
427+
>
428+
<span>登出</span>
429+
</button>
430+
) : (
431+
<button
432+
onClick={handleLogin}
433+
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
434+
>
435+
<span>使用Google登录</span>
436+
</button>
437+
)}
303438
</div>
304439
</div>
305440

0 commit comments

Comments
 (0)