11import 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 ( ) ;
227import { FileUpload } from './components/FileUpload' ;
328import { RegexControls } from './components/RegexControls' ;
429import { FileList } from './components/FileList' ;
@@ -8,12 +33,15 @@ import { Header } from './components/Header';
833import { FileConfigModal } from './components/FileConfigModal' ;
934import { PanelLeftClose , PanelLeftOpen } from 'lucide-react' ;
1035import { mergeFilesWithReplacement } from './utils/mergeFiles.js' ;
36+ import { signInWithGoogle , doSignOut , onAuthChange } from './services/authService.js' ;
37+ import { saveWorkspace , loadWorkspace } from './services/workspaceService.js' ;
1138
1239function 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