Skip to content

Commit 156ae04

Browse files
feat: add save/autosave indicator
1 parent c6396d7 commit 156ae04

File tree

4 files changed

+87
-3
lines changed

4 files changed

+87
-3
lines changed

src/components/Analysis/ConfigurableScreens.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ interface Props {
1414
onDeleteCustomGame?: () => void
1515
onAnalyzeEntireGame?: () => void
1616
isAnalysisInProgress?: boolean
17+
autoSave?: {
18+
hasUnsavedChanges: boolean
19+
isSaving: boolean
20+
status: 'saving' | 'unsaved' | 'saved'
21+
}
1722
}
1823

1924
export const ConfigurableScreens: React.FC<Props> = ({
@@ -26,6 +31,7 @@ export const ConfigurableScreens: React.FC<Props> = ({
2631
onDeleteCustomGame,
2732
onAnalyzeEntireGame,
2833
isAnalysisInProgress,
34+
autoSave,
2935
}) => {
3036
const screens = [
3137
{
@@ -82,6 +88,7 @@ export const ConfigurableScreens: React.FC<Props> = ({
8288
onDeleteCustomGame={onDeleteCustomGame}
8389
onAnalyzeEntireGame={onAnalyzeEntireGame}
8490
isAnalysisInProgress={isAnalysisInProgress}
91+
autoSave={autoSave}
8592
/>
8693
) : screen.id === 'export' ? (
8794
<div className="flex w-full flex-col p-3">

src/components/Analysis/ConfigureAnalysis.tsx

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import Link from 'next/link'
21
import React from 'react'
3-
import { useLocalStorage } from 'src/hooks'
42
import { AnalyzedGame } from 'src/types'
53

64
import { ContinueAgainstMaia } from 'src/components'
@@ -14,6 +12,11 @@ interface Props {
1412
onDeleteCustomGame?: () => void
1513
onAnalyzeEntireGame?: () => void
1614
isAnalysisInProgress?: boolean
15+
autoSave?: {
16+
hasUnsavedChanges: boolean
17+
isSaving: boolean
18+
status: 'saving' | 'unsaved' | 'saved'
19+
}
1720
}
1821

1922
export const ConfigureAnalysis: React.FC<Props> = ({
@@ -25,6 +28,7 @@ export const ConfigureAnalysis: React.FC<Props> = ({
2528
onDeleteCustomGame,
2629
onAnalyzeEntireGame,
2730
isAnalysisInProgress = false,
31+
autoSave,
2832
}: Props) => {
2933
const isCustomGame = game.type === 'custom-pgn' || game.type === 'custom-fen'
3034

@@ -66,6 +70,43 @@ export const ConfigureAnalysis: React.FC<Props> = ({
6670
</div>
6771
</button>
6872
)}
73+
{autoSave &&
74+
game.type !== 'custom-pgn' &&
75+
game.type !== 'custom-fen' &&
76+
game.type !== 'tournament' && (
77+
<div className="mt-2 w-full">
78+
<div className="flex items-center gap-1.5">
79+
{autoSave.status === 'saving' && (
80+
<>
81+
<div className="h-2 w-2 animate-spin rounded-full border border-secondary border-t-primary"></div>
82+
<span className="text-xs text-secondary">
83+
Saving analysis...
84+
</span>
85+
</>
86+
)}
87+
{autoSave.status === 'unsaved' && (
88+
<>
89+
<span className="material-symbols-outlined !text-sm text-orange-400">
90+
sync_problem
91+
</span>
92+
<span className="text-xs text-orange-400">
93+
Unsaved analysis. Will auto-save...
94+
</span>
95+
</>
96+
)}
97+
{autoSave.status === 'saved' && (
98+
<>
99+
<span className="material-symbols-outlined !text-sm text-green-400">
100+
cloud_done
101+
</span>
102+
<span className="text-xs text-green-400">
103+
Analysis auto-saved
104+
</span>
105+
</>
106+
)}
107+
</div>
108+
</div>
109+
)}
69110
{isCustomGame && onDeleteCustomGame && (
70111
<div className="mt-2 w-full">
71112
<button

src/hooks/useAnalysisController/useAnalysisController.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export const useAnalysisController = (
8484
})
8585

8686
const [lastSavedCacheKey, setLastSavedCacheKey] = useState<string>('')
87+
const [hasUnsavedAnalysis, setHasUnsavedAnalysis] = useState(false)
88+
const [isAutoSaving, setIsAutoSaving] = useState(false)
8789
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null)
8890

8991
const saveAnalysisToBackend = useCallback(async () => {
@@ -96,10 +98,17 @@ export const useAnalysisController = (
9698
return
9799
}
98100

101+
// Don't save if there are no unsaved changes
102+
if (!hasUnsavedAnalysis) {
103+
return
104+
}
105+
99106
try {
107+
setIsAutoSaving(true)
100108
const analysisData = collectEngineAnalysisData(game.tree)
101109

102110
if (analysisData.length === 0) {
111+
setIsAutoSaving(false)
103112
return
104113
}
105114

@@ -108,16 +117,19 @@ export const useAnalysisController = (
108117
)
109118

110119
if (!hasMeaningfulAnalysis) {
120+
setIsAutoSaving(false)
111121
return
112122
}
113123

114124
const cacheKey = generateAnalysisCacheKey(analysisData)
115125
if (cacheKey === lastSavedCacheKey) {
126+
setIsAutoSaving(false)
116127
return
117128
}
118129

119130
await storeEngineAnalysis(game.id, analysisData)
120131
setLastSavedCacheKey(cacheKey)
132+
setHasUnsavedAnalysis(false) // Mark as saved
121133
console.log(
122134
'Analysis saved to backend:',
123135
analysisData.length,
@@ -126,8 +138,10 @@ export const useAnalysisController = (
126138
} catch (error) {
127139
console.warn('Failed to save analysis to backend:', error)
128140
// Don't show error to user as this is background functionality
141+
} finally {
142+
setIsAutoSaving(false)
129143
}
130-
}, [game.id, game.type, game.tree, lastSavedCacheKey])
144+
}, [game.id, game.type, game.tree, lastSavedCacheKey, hasUnsavedAnalysis])
131145

132146
const saveAnalysisToBackendRef = useRef(saveAnalysisToBackend)
133147
saveAnalysisToBackendRef.current = saveAnalysisToBackend
@@ -174,9 +188,20 @@ export const useAnalysisController = (
174188

175189
// Load stored analysis when game changes
176190
useEffect(() => {
191+
// Reset states for new game
192+
setHasUnsavedAnalysis(false)
193+
setIsAutoSaving(false)
194+
setLastSavedCacheKey('')
177195
loadStoredAnalysis()
178196
}, [loadStoredAnalysis])
179197

198+
// Mark analysis as unsaved when new analysis comes in
199+
useEffect(() => {
200+
if (analysisState > 0) {
201+
setHasUnsavedAnalysis(true)
202+
}
203+
}, [analysisState])
204+
180205
// Setup auto-save timer
181206
useEffect(() => {
182207
// Clear existing timer
@@ -481,6 +506,15 @@ export const useAnalysisController = (
481506
isEnginesReady: stockfish.isReady() && maia.status === 'ready',
482507
saveAnalysis: saveAnalysisToBackend,
483508
loadStoredAnalysis,
509+
autoSave: {
510+
hasUnsavedChanges: hasUnsavedAnalysis,
511+
isSaving: isAutoSaving,
512+
status: isAutoSaving
513+
? 'saving'
514+
: hasUnsavedAnalysis
515+
? 'unsaved'
516+
: 'saved',
517+
},
484518
},
485519
}
486520
}

src/pages/analysis/[...id].tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,7 @@ const Analysis: React.FC<Props> = ({
819819
onDeleteCustomGame={handleDeleteCustomGame}
820820
onAnalyzeEntireGame={handleAnalyzeEntireGame}
821821
isAnalysisInProgress={controller.gameAnalysis.progress.isAnalyzing}
822+
autoSave={controller.gameAnalysis.autoSave}
822823
/>
823824
</motion.div>
824825
<motion.div
@@ -1409,6 +1410,7 @@ const Analysis: React.FC<Props> = ({
14091410
isAnalysisInProgress={
14101411
controller.gameAnalysis.progress.isAnalyzing
14111412
}
1413+
autoSave={controller.gameAnalysis.autoSave}
14121414
currentNode={controller.currentNode as GameNode}
14131415
/>
14141416
</motion.div>

0 commit comments

Comments
 (0)