Severian commited on
Commit
1b6fd69
·
verified ·
1 Parent(s): 93a443e

Update App.tsx

Browse files
Files changed (1) hide show
  1. App.tsx +301 -17
App.tsx CHANGED
@@ -28,13 +28,15 @@ const estimateStorageSize = (results: ResultRow[]): number => {
28
  // Maximum storage size per job (500KB)
29
  const MAX_JOB_STORAGE_SIZE = 500 * 1024;
30
 
 
31
  function useLocalStorage<T>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] {
32
  const [storedValue, setStoredValue] = useState<T>(() => {
33
  try {
34
  const item = window.localStorage.getItem(key);
35
  if (item) {
36
- // Basic validation to help clear out old, problematic data.
37
  const parsed = JSON.parse(item);
 
 
38
  if (Array.isArray(parsed)) {
39
  // Filter out old items that had the 'results' property which caused storage errors.
40
  let cleanHistory = parsed.filter(p => typeof p === 'object' && p !== null && !p.hasOwnProperty('results'));
@@ -67,13 +69,86 @@ function useLocalStorage<T>(key: string, initialValue: T): [T, React.Dispatch<Re
67
  try {
68
  setStoredValue(prev => {
69
  const valueToStore = value instanceof Function ? value(prev) : value;
70
- window.localStorage.setItem(key, JSON.stringify(valueToStore));
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  return valueToStore;
72
  });
73
  } catch (error) {
74
  console.error('Error writing to localStorage', error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  }
76
  }, [key]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  return [storedValue, setValue];
78
  }
79
 
@@ -93,22 +168,82 @@ const MAX_HISTORY_ITEMS = 10;
93
 
94
  const App: React.FC = () => {
95
  const [history, setHistory] = useLocalStorage<JobHistoryItem[]>('ace-copywriting-history', []);
 
96
  const [view, setView] = useState<ViewState>(() => history.length > 0 ? { type: 'new_job' } : { type: 'welcome' });
97
 
98
- // State for the currently active processing job
99
- const [jobId, setJobId] = useState<string | null>(null);
100
- const [filename, setFilename] = useState<string>('');
101
- const [queue, setQueue] = useState<QueueItem[]>([]);
102
- const [results, setResults] = useState<ResultRow[]>([]);
103
- const [status, setStatus] = useState<ProcessingStatus>('idle');
104
- const [error, setError] = useState<string | null>(null);
105
  const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
106
 
107
- // New: capture all user-facing errors and show a details modal
108
- const [errors, setErrors] = useState<BatchError[]>([]);
109
  const [showErrorDetails, setShowErrorDetails] = useState(false);
110
  const [expandedDebugIndex, setExpandedDebugIndex] = useState<number | null>(null);
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  const resetNewJobState = useCallback(() => {
113
  setJobId(generateUUID());
114
  setFilename('');
@@ -118,7 +253,10 @@ const App: React.FC = () => {
118
  setError(null);
119
  setErrors([]);
120
  setShowErrorDetails(false);
121
- }, []);
 
 
 
122
 
123
  const handleStartNewJob = () => {
124
  resetNewJobState();
@@ -135,6 +273,7 @@ const App: React.FC = () => {
135
  }
136
  };
137
 
 
138
  const handleFileParsed = useCallback((data: CsvInputRow[], file: File) => {
139
  // Ensure we have a jobId for this processing session
140
  if (!jobId) {
@@ -149,7 +288,23 @@ const App: React.FC = () => {
149
  setQueue(initialQueue);
150
  setError(null);
151
  setErrors([]);
152
- }, [jobId]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
  const handleStartProcessing = async () => {
155
  console.log('Start processing clicked. JobId:', jobId, 'Queue length:', queue.length);
@@ -225,13 +380,47 @@ const App: React.FC = () => {
225
  };
226
 
227
  const updatedHistory = [newHistoryItem, ...prevHistory];
228
- // Prune history to the maximum allowed size
 
 
 
 
229
  console.log('Job saved to history successfully:', newHistoryItem);
230
- return updatedHistory.slice(0, MAX_HISTORY_ITEMS);
231
  });
232
- // Stay on the results view for the current job. The user can navigate to history manually.
 
 
233
  }
234
- }, [status, jobId, filename, queue.length, results, setHistory]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
  // --- RENDER ---
237
 
@@ -462,6 +651,100 @@ const App: React.FC = () => {
462
  </div>
463
  );
464
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  return (
466
  <div className="flex h-screen bg-gray-900 text-gray-100">
467
  {/* --- SIDEBAR --- */}
@@ -551,6 +834,7 @@ const App: React.FC = () => {
551
  </div>
552
  )}
553
  {renderMainContent()}
 
554
  </main>
555
 
556
  {/* --- DELETE CONFIRMATION DIALOG --- */}
 
28
  // Maximum storage size per job (500KB)
29
  const MAX_JOB_STORAGE_SIZE = 500 * 1024;
30
 
31
+ // Enhanced localStorage hook with better error handling and data validation
32
  function useLocalStorage<T>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] {
33
  const [storedValue, setStoredValue] = useState<T>(() => {
34
  try {
35
  const item = window.localStorage.getItem(key);
36
  if (item) {
 
37
  const parsed = JSON.parse(item);
38
+
39
+ // Enhanced validation for different data types
40
  if (Array.isArray(parsed)) {
41
  // Filter out old items that had the 'results' property which caused storage errors.
42
  let cleanHistory = parsed.filter(p => typeof p === 'object' && p !== null && !p.hasOwnProperty('results'));
 
69
  try {
70
  setStoredValue(prev => {
71
  const valueToStore = value instanceof Function ? value(prev) : value;
72
+
73
+ // Check storage quota before saving
74
+ const serialized = JSON.stringify(valueToStore);
75
+ if (serialized.length > 5 * 1024 * 1024) { // 5MB limit
76
+ console.warn('Data too large for localStorage, truncating...');
77
+ // For history, keep only the most recent items
78
+ if (Array.isArray(valueToStore) && key === 'ace-copywriting-history') {
79
+ const truncated = (valueToStore as any[]).slice(0, 5); // Keep only 5 most recent
80
+ window.localStorage.setItem(key, JSON.stringify(truncated));
81
+ return truncated as T;
82
+ }
83
+ }
84
+
85
+ window.localStorage.setItem(key, serialized);
86
  return valueToStore;
87
  });
88
  } catch (error) {
89
  console.error('Error writing to localStorage', error);
90
+ // If quota exceeded, try to clear old data and retry
91
+ if (error instanceof Error && error.name === 'QuotaExceededError') {
92
+ try {
93
+ // Clear old history items to make space
94
+ if (key === 'ace-copywriting-history') {
95
+ const currentHistory = window.localStorage.getItem(key);
96
+ if (currentHistory) {
97
+ const parsed = JSON.parse(currentHistory);
98
+ if (Array.isArray(parsed) && parsed.length > 3) {
99
+ const truncated = parsed.slice(0, 3); // Keep only 3 most recent
100
+ window.localStorage.setItem(key, JSON.stringify(truncated));
101
+ setStoredValue(truncated as T);
102
+ }
103
+ }
104
+ }
105
+ } catch (clearError) {
106
+ console.error('Failed to clear storage for new data', clearError);
107
+ }
108
+ }
109
  }
110
  }, [key]);
111
+
112
+ return [storedValue, setValue];
113
+ }
114
+
115
+ // Hook for user preferences
116
+ function useUserPreferences() {
117
+ const [preferences, setPreferences] = useLocalStorage('ace-copywriting-preferences', {
118
+ autoSave: true,
119
+ maxHistoryItems: 10,
120
+ showDebugInfo: false,
121
+ defaultExportFormat: 'csv' as 'csv' | 'pdf' | 'docx' | 'json',
122
+ theme: 'dark' as 'dark' | 'light'
123
+ });
124
+
125
+ return [preferences, setPreferences] as const;
126
+ }
127
+
128
+ // Hook for session data (cleared on browser close)
129
+ function useSessionStorage<T>(key: string, initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] {
130
+ const [storedValue, setStoredValue] = useState<T>(() => {
131
+ try {
132
+ const item = window.sessionStorage.getItem(key);
133
+ return item ? JSON.parse(item) : initialValue;
134
+ } catch (error) {
135
+ console.error('Error reading from sessionStorage', error);
136
+ return initialValue;
137
+ }
138
+ });
139
+
140
+ const setValue: React.Dispatch<React.SetStateAction<T>> = useCallback((value) => {
141
+ try {
142
+ setStoredValue(prev => {
143
+ const valueToStore = value instanceof Function ? value(prev) : value;
144
+ window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
145
+ return valueToStore;
146
+ });
147
+ } catch (error) {
148
+ console.error('Error writing to sessionStorage', error);
149
+ }
150
+ }, [key]);
151
+
152
  return [storedValue, setValue];
153
  }
154
 
 
168
 
169
  const App: React.FC = () => {
170
  const [history, setHistory] = useLocalStorage<JobHistoryItem[]>('ace-copywriting-history', []);
171
+ const [preferences, setPreferences] = useUserPreferences();
172
  const [view, setView] = useState<ViewState>(() => history.length > 0 ? { type: 'new_job' } : { type: 'welcome' });
173
 
174
+ // State for the currently active processing job - persist in sessionStorage
175
+ const [jobId, setJobId] = useSessionStorage<string | null>('ace-current-job-id', null);
176
+ const [filename, setFilename] = useSessionStorage<string>('ace-current-filename', '');
177
+ const [queue, setQueue] = useSessionStorage<QueueItem[]>('ace-current-queue', []);
178
+ const [results, setResults] = useSessionStorage<ResultRow[]>('ace-current-results', []);
179
+ const [status, setStatus] = useSessionStorage<ProcessingStatus>('ace-current-status', 'idle');
180
+ const [error, setError] = useSessionStorage<string | null>('ace-current-error', null);
181
  const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
182
 
183
+ // New: capture all user-facing errors and show a details modal - persist in sessionStorage
184
+ const [errors, setErrors] = useSessionStorage<BatchError[]>('ace-current-errors', []);
185
  const [showErrorDetails, setShowErrorDetails] = useState(false);
186
  const [expandedDebugIndex, setExpandedDebugIndex] = useState<number | null>(null);
187
 
188
+ // Auto-save current job state when it changes
189
+ useEffect(() => {
190
+ if (preferences.autoSave && jobId) {
191
+ const jobState = {
192
+ jobId,
193
+ filename,
194
+ queue,
195
+ results,
196
+ status,
197
+ error,
198
+ errors,
199
+ timestamp: new Date().toISOString()
200
+ };
201
+
202
+ try {
203
+ window.sessionStorage.setItem('ace-current-job-state', JSON.stringify(jobState));
204
+ } catch (error) {
205
+ console.warn('Failed to auto-save job state:', error);
206
+ }
207
+ }
208
+ }, [preferences.autoSave, jobId, filename, queue, results, status, error, errors]);
209
+
210
+ // Restore job state on app load
211
+ useEffect(() => {
212
+ try {
213
+ const savedState = window.sessionStorage.getItem('ace-current-job-state');
214
+ if (savedState) {
215
+ const jobState = JSON.parse(savedState);
216
+ const stateAge = Date.now() - new Date(jobState.timestamp).getTime();
217
+ const maxAge = 24 * 60 * 60 * 1000; // 24 hours
218
+
219
+ // Only restore if state is recent
220
+ if (stateAge < maxAge) {
221
+ setJobId(jobState.jobId);
222
+ setFilename(jobState.filename);
223
+ setQueue(jobState.queue || []);
224
+ setResults(jobState.results || []);
225
+ setStatus(jobState.status || 'idle');
226
+ setError(jobState.error);
227
+ setErrors(jobState.errors || []);
228
+
229
+ // Set appropriate view based on restored state
230
+ if (jobState.status === 'completed' && jobState.results?.length > 0) {
231
+ setView({ type: 'new_job' }); // Show results view
232
+ } else if (jobState.status === 'processing') {
233
+ setView({ type: 'new_job' }); // Show processing view
234
+ }
235
+ } else {
236
+ // Clear old state
237
+ window.sessionStorage.removeItem('ace-current-job-state');
238
+ }
239
+ }
240
+ } catch (error) {
241
+ console.warn('Failed to restore job state:', error);
242
+ // Clear corrupted state
243
+ window.sessionStorage.removeItem('ace-current-job-state');
244
+ }
245
+ }, []);
246
+
247
  const resetNewJobState = useCallback(() => {
248
  setJobId(generateUUID());
249
  setFilename('');
 
253
  setError(null);
254
  setErrors([]);
255
  setShowErrorDetails(false);
256
+
257
+ // Clear session storage for new job
258
+ window.sessionStorage.removeItem('ace-current-job-state');
259
+ }, [setJobId, setFilename, setQueue, setResults, setStatus, setError, setErrors]);
260
 
261
  const handleStartNewJob = () => {
262
  resetNewJobState();
 
273
  }
274
  };
275
 
276
+ // Enhanced file parsing with better error handling
277
  const handleFileParsed = useCallback((data: CsvInputRow[], file: File) => {
278
  // Ensure we have a jobId for this processing session
279
  if (!jobId) {
 
288
  setQueue(initialQueue);
289
  setError(null);
290
  setErrors([]);
291
+
292
+ // Save file info to history for reference
293
+ const fileInfo = {
294
+ filename: file.name,
295
+ size: file.size,
296
+ lastModified: file.lastModified,
297
+ rowCount: data.length
298
+ };
299
+
300
+ try {
301
+ const fileHistory = JSON.parse(localStorage.getItem('ace-file-history') || '[]');
302
+ const updatedHistory = [fileInfo, ...fileHistory.slice(0, 9)]; // Keep last 10 files
303
+ localStorage.setItem('ace-file-history', JSON.stringify(updatedHistory));
304
+ } catch (error) {
305
+ console.warn('Failed to save file history:', error);
306
+ }
307
+ }, [jobId, setJobId, setFilename, setQueue, setError, setErrors]);
308
 
309
  const handleStartProcessing = async () => {
310
  console.log('Start processing clicked. JobId:', jobId, 'Queue length:', queue.length);
 
380
  };
381
 
382
  const updatedHistory = [newHistoryItem, ...prevHistory];
383
+
384
+ // Prune history to the maximum allowed size from preferences
385
+ const maxItems = preferences.maxHistoryItems || 10;
386
+ const prunedHistory = updatedHistory.slice(0, maxItems);
387
+
388
  console.log('Job saved to history successfully:', newHistoryItem);
389
+ return prunedHistory;
390
  });
391
+
392
+ // Clear session storage after successful save
393
+ window.sessionStorage.removeItem('ace-current-job-state');
394
  }
395
+ }, [status, jobId, filename, queue.length, results, setHistory, preferences.maxHistoryItems]);
396
+
397
+ // Cleanup old history items periodically
398
+ useEffect(() => {
399
+ const cleanupHistory = () => {
400
+ setHistory(prevHistory => {
401
+ const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
402
+ const now = Date.now();
403
+ const filtered = prevHistory.filter(item => {
404
+ const itemAge = now - new Date(item.date).getTime();
405
+ return itemAge < maxAge;
406
+ });
407
+
408
+ if (filtered.length !== prevHistory.length) {
409
+ console.log(`Cleaned up ${prevHistory.length - filtered.length} old history items`);
410
+ }
411
+
412
+ return filtered;
413
+ });
414
+ };
415
+
416
+ // Clean up on app start
417
+ cleanupHistory();
418
+
419
+ // Set up periodic cleanup (every hour)
420
+ const interval = setInterval(cleanupHistory, 60 * 60 * 1000);
421
+
422
+ return () => clearInterval(interval);
423
+ }, [setHistory]);
424
 
425
  // --- RENDER ---
426
 
 
651
  </div>
652
  );
653
 
654
+ // Storage management utilities
655
+ const getStorageInfo = () => {
656
+ try {
657
+ const totalSpace = 5 * 1024 * 1024; // 5MB limit
658
+ const usedSpace = new Blob([JSON.stringify(history)]).size;
659
+ const usedPercentage = (usedSpace / totalSpace) * 100;
660
+
661
+ return {
662
+ usedSpace: Math.round(usedSpace / 1024), // KB
663
+ totalSpace: Math.round(totalSpace / 1024), // KB
664
+ usedPercentage: Math.round(usedPercentage),
665
+ historyCount: history.length,
666
+ maxHistoryItems: preferences.maxHistoryItems
667
+ };
668
+ } catch (error) {
669
+ console.error('Error calculating storage info:', error);
670
+ return null;
671
+ }
672
+ };
673
+
674
+ const clearAllData = () => {
675
+ if (window.confirm('Are you sure you want to clear all stored data? This will remove all job history and cannot be undone.')) {
676
+ try {
677
+ // Clear localStorage
678
+ window.localStorage.removeItem('ace-copywriting-history');
679
+ window.localStorage.removeItem('ace-copywriting-preferences');
680
+ window.localStorage.removeItem('ace-file-history');
681
+
682
+ // Clear sessionStorage
683
+ window.sessionStorage.clear();
684
+
685
+ // Reset state
686
+ setHistory([]);
687
+ setPreferences({
688
+ autoSave: true,
689
+ maxHistoryItems: 10,
690
+ showDebugInfo: false,
691
+ defaultExportFormat: 'csv',
692
+ theme: 'dark'
693
+ });
694
+
695
+ // Reset current job state
696
+ resetNewJobState();
697
+ setView({ type: 'welcome' });
698
+
699
+ alert('All data has been cleared successfully.');
700
+ } catch (error) {
701
+ console.error('Error clearing data:', error);
702
+ alert('Error clearing data. Please try refreshing the page.');
703
+ }
704
+ }
705
+ };
706
+
707
+ const StorageInfoComponent: React.FC = () => {
708
+ const storageInfo = getStorageInfo();
709
+
710
+ if (!storageInfo) return null;
711
+
712
+ return (
713
+ <div className="bg-gray-700 rounded-lg p-4 mb-4">
714
+ <div className="flex items-center justify-between mb-2">
715
+ <h3 className="text-sm font-semibold text-gray-200">Storage Status</h3>
716
+ <button
717
+ onClick={clearAllData}
718
+ className="text-xs px-2 py-1 bg-red-600 hover:bg-red-700 text-white rounded"
719
+ >
720
+ Clear All Data
721
+ </button>
722
+ </div>
723
+ <div className="grid grid-cols-2 gap-4 text-xs">
724
+ <div>
725
+ <div className="text-gray-400">Storage Used</div>
726
+ <div className="text-white">{storageInfo.usedSpace}KB / {storageInfo.totalSpace}KB</div>
727
+ <div className="w-full bg-gray-600 rounded-full h-1 mt-1">
728
+ <div
729
+ className={`h-1 rounded-full ${storageInfo.usedPercentage > 80 ? 'bg-red-500' : storageInfo.usedPercentage > 60 ? 'bg-yellow-500' : 'bg-green-500'}`}
730
+ style={{ width: `${Math.min(storageInfo.usedPercentage, 100)}%` }}
731
+ ></div>
732
+ </div>
733
+ </div>
734
+ <div>
735
+ <div className="text-gray-400">Job History</div>
736
+ <div className="text-white">{storageInfo.historyCount} / {storageInfo.maxHistoryItems}</div>
737
+ </div>
738
+ </div>
739
+ {storageInfo.usedPercentage > 80 && (
740
+ <div className="text-xs text-yellow-400 mt-2">
741
+ ⚠️ Storage is getting full. Consider clearing old jobs.
742
+ </div>
743
+ )}
744
+ </div>
745
+ );
746
+ };
747
+
748
  return (
749
  <div className="flex h-screen bg-gray-900 text-gray-100">
750
  {/* --- SIDEBAR --- */}
 
834
  </div>
835
  )}
836
  {renderMainContent()}
837
+ <StorageInfoComponent />
838
  </main>
839
 
840
  {/* --- DELETE CONFIRMATION DIALOG --- */}