mirror of
https://github.com/PragmaticMachineLearning/probly.git
synced 2026-01-10 05:47:56 -05:00
testing indexed db
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@types/react-dom": "18.2.19",
|
||||
"autoprefixer": "10.4.17",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"dexie": "^4.0.11",
|
||||
"dotenv": "^16.4.1",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"handsontable": "^13.1.0",
|
||||
@@ -3313,6 +3314,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/dexie": {
|
||||
"version": "4.0.11",
|
||||
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz",
|
||||
"integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@types/react-dom": "18.2.19",
|
||||
"autoprefixer": "10.4.17",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"dexie": "^4.0.11",
|
||||
"dotenv": "^16.4.1",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"handsontable": "^13.1.0",
|
||||
|
||||
115
src/app/page.tsx
115
src/app/page.tsx
@@ -13,6 +13,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import ChatBox from "@/components/ChatBox";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
import type { SpreadsheetRef } from "@/components/Spreadsheet";
|
||||
import { db } from '@/lib/db';
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const Spreadsheet = dynamic(() => import("@/components/Spreadsheet").then(mod => mod.default), {
|
||||
@@ -24,6 +25,31 @@ const Spreadsheet = dynamic(() => import("@/components/Spreadsheet").then(mod =>
|
||||
),
|
||||
});
|
||||
|
||||
// Helper function to load chat history from IndexedDB
|
||||
const loadChatHistory = async (): Promise<ChatMessage[]> => {
|
||||
try {
|
||||
return await db.getChatHistory();
|
||||
} catch (error) {
|
||||
console.error("Error loading chat history from IndexedDB:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to save chat history to IndexedDB
|
||||
const saveChatHistory = async (history: ChatMessage[]): Promise<void> => {
|
||||
try {
|
||||
// Clear existing chat history
|
||||
await db.clearChatHistory();
|
||||
|
||||
// Add each message to the database
|
||||
for (const message of history) {
|
||||
await db.addChatMessage(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving chat history to IndexedDB:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const SpreadsheetApp = () => {
|
||||
const {
|
||||
setFormulas,
|
||||
@@ -67,41 +93,36 @@ const SpreadsheetApp = () => {
|
||||
return () => window.removeEventListener("keydown", handleKeyPress);
|
||||
}, [isPromptLibraryOpen]);
|
||||
|
||||
// Load chat open state from localStorage
|
||||
// Load chat open state from IndexedDB
|
||||
useEffect(() => {
|
||||
const savedState = localStorage.getItem("chatOpen");
|
||||
if (savedState) {
|
||||
setIsChatOpen(JSON.parse(savedState));
|
||||
}
|
||||
const loadChatOpen = async () => {
|
||||
const isOpen = await loadChatOpenState();
|
||||
setIsChatOpen(isOpen);
|
||||
};
|
||||
|
||||
loadChatOpen();
|
||||
}, []);
|
||||
|
||||
// Save chat open state to localStorage
|
||||
// Save chat open state to IndexedDB when it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem("chatOpen", JSON.stringify(isChatOpen));
|
||||
saveChatOpenState(isChatOpen);
|
||||
}, [isChatOpen]);
|
||||
|
||||
// Load chat history from localStorage
|
||||
// Load chat history from IndexedDB
|
||||
useEffect(() => {
|
||||
const savedHistory = localStorage.getItem("chatHistory");
|
||||
if (savedHistory) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedHistory);
|
||||
setChatHistory(
|
||||
parsed.map((msg: any) => ({
|
||||
...msg,
|
||||
timestamp: new Date(msg.timestamp),
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error loading chat history:", error);
|
||||
localStorage.removeItem("chatHistory");
|
||||
}
|
||||
}
|
||||
const loadHistory = async () => {
|
||||
const history = await loadChatHistory();
|
||||
setChatHistory(history);
|
||||
};
|
||||
|
||||
loadHistory();
|
||||
}, []);
|
||||
|
||||
// Save chat history to localStorage
|
||||
// Save chat history to IndexedDB when it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem("chatHistory", JSON.stringify(chatHistory));
|
||||
if (chatHistory.length > 0) {
|
||||
saveChatHistory(chatHistory);
|
||||
}
|
||||
}, [chatHistory]);
|
||||
|
||||
// Update spreadsheetData when active sheet changes
|
||||
@@ -110,6 +131,23 @@ const SpreadsheetApp = () => {
|
||||
// We can access the active sheet data directly when needed via getActiveSheetData()
|
||||
}, [activeSheetId, getActiveSheetData]);
|
||||
|
||||
// Cleanup database on initial load
|
||||
useEffect(() => {
|
||||
const cleanupDatabase = async () => {
|
||||
try {
|
||||
// Check for and clean up duplicate sheets on startup
|
||||
const removedCount = await db.cleanupDuplicateSheets();
|
||||
if (removedCount > 0) {
|
||||
console.log(`Cleaned up ${removedCount} duplicate sheets on startup.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up database on startup:", error);
|
||||
}
|
||||
};
|
||||
|
||||
cleanupDatabase();
|
||||
}, []);
|
||||
|
||||
const handleStop = () => {
|
||||
if (abortController.current) {
|
||||
abortController.current.abort();
|
||||
@@ -335,11 +373,34 @@ const SpreadsheetApp = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearHistory = () => {
|
||||
const handleClearHistory = async () => {
|
||||
setChatHistory([]);
|
||||
localStorage.removeItem("chatHistory");
|
||||
try {
|
||||
await db.clearChatHistory();
|
||||
} catch (error) {
|
||||
console.error("Error clearing chat history from IndexedDB:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to load chat open state from IndexedDB
|
||||
const loadChatOpenState = async (): Promise<boolean> => {
|
||||
try {
|
||||
const chatOpen = await db.getPreference('chatOpen');
|
||||
return chatOpen === true;
|
||||
} catch (error) {
|
||||
console.error("Error loading chat open state from IndexedDB:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to save chat open state to IndexedDB
|
||||
const saveChatOpenState = async (isOpen: boolean): Promise<void> => {
|
||||
try {
|
||||
await db.setPreference('chatOpen', isOpen);
|
||||
} catch (error) {
|
||||
console.error("Error saving chat open state to IndexedDB:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="h-screen w-screen flex flex-col bg-gray-50">
|
||||
|
||||
@@ -66,12 +66,82 @@ const Spreadsheet = forwardRef<SpreadsheetRef, SpreadsheetProps>(
|
||||
const [editingSheetId, setEditingSheetId] = useState<string | null>(null);
|
||||
const [newSheetName, setNewSheetName] = useState("");
|
||||
const [showSheetMenu, setShowSheetMenu] = useState<string | null>(null);
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// Add a debug effect to monitor sheet menu state
|
||||
useEffect(() => {
|
||||
console.log("Sheet menu state changed:", showSheetMenu);
|
||||
}, [showSheetMenu]);
|
||||
|
||||
// Initialize or update Handsontable when sheets and activeSheetId are available
|
||||
useEffect(() => {
|
||||
// Only proceed if we have valid sheet data
|
||||
if (sheets.length === 0 || !activeSheetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If hot instance already exists, update it
|
||||
if (hotInstanceRef.current) {
|
||||
const activeSheetData = getActiveSheetData();
|
||||
setCurrentData(activeSheetData);
|
||||
hotInstanceRef.current.updateSettings({ data: activeSheetData }, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// If spreadsheetRef is ready but hotInstance is not yet created
|
||||
if (spreadsheetRef.current && !hotInstanceRef.current) {
|
||||
try {
|
||||
// Get the initial data for the active sheet
|
||||
const activeSheetData = getActiveSheetData();
|
||||
setCurrentData(activeSheetData);
|
||||
|
||||
// Get the initial config
|
||||
const config = getInitialConfig(activeSheetData);
|
||||
|
||||
// Add our custom afterChange hook to the config
|
||||
config.afterChange = (changes: any) => {
|
||||
if (changes) {
|
||||
const currentData = hotInstanceRef.current?.getData();
|
||||
if (currentData) {
|
||||
// Use the current activeSheetId from context
|
||||
console.log(`Saving changes to active sheet: ${activeSheetId}`);
|
||||
|
||||
// Update the data for the active sheet only
|
||||
updateSheetData(activeSheetId, currentData);
|
||||
|
||||
// Also update our local state
|
||||
setCurrentData(currentData);
|
||||
|
||||
if (onDataChange) {
|
||||
onDataChange(currentData);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new instance with the config
|
||||
hotInstanceRef.current = new Handsontable(
|
||||
spreadsheetRef.current,
|
||||
config
|
||||
);
|
||||
|
||||
setIsInitialized(true);
|
||||
} catch (error) {
|
||||
console.error("Error initializing spreadsheet:", error);
|
||||
}
|
||||
}
|
||||
}, [sheets, activeSheetId, getActiveSheetData, updateSheetData, onDataChange]);
|
||||
|
||||
// Cleanup Handsontable instance on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hotInstanceRef.current) {
|
||||
hotInstanceRef.current.destroy();
|
||||
hotInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleImport = async (file: File) => {
|
||||
if (!file) return;
|
||||
try {
|
||||
@@ -170,54 +240,10 @@ const Spreadsheet = forwardRef<SpreadsheetRef, SpreadsheetProps>(
|
||||
});
|
||||
}, [formulaQueue, clearFormula, activeSheetId, updateSheetData]);
|
||||
|
||||
// Initialize Handsontable when component mounts
|
||||
useEffect(() => {
|
||||
if (spreadsheetRef.current && !hotInstanceRef.current) {
|
||||
try {
|
||||
// Get the initial config
|
||||
const config = getInitialConfig(currentData);
|
||||
|
||||
// Add our custom afterChange hook to the config
|
||||
config.afterChange = (changes: any) => {
|
||||
if (changes) {
|
||||
const currentData = hotInstanceRef.current?.getData();
|
||||
if (currentData) {
|
||||
// Use the current activeSheetId from context
|
||||
console.log(`Saving changes to active sheet: ${activeSheetId}`);
|
||||
|
||||
// Update the data for the active sheet only
|
||||
updateSheetData(activeSheetId, currentData);
|
||||
|
||||
// Also update our local state
|
||||
setCurrentData(currentData);
|
||||
|
||||
if (onDataChange) {
|
||||
onDataChange(currentData);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a new instance with the config
|
||||
hotInstanceRef.current = new Handsontable(
|
||||
spreadsheetRef.current,
|
||||
config
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error initializing spreadsheet:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (hotInstanceRef.current) {
|
||||
hotInstanceRef.current.destroy();
|
||||
hotInstanceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update Handsontable when active sheet changes
|
||||
useEffect(() => {
|
||||
if (!activeSheetId || sheets.length === 0) return;
|
||||
|
||||
const activeSheet = sheets.find(sheet => sheet.id === activeSheetId);
|
||||
if (activeSheet) {
|
||||
console.log(`Active sheet changed to: ${activeSheet.name} (ID: ${activeSheetId}, HyperFormula ID: ${activeSheet.hyperFormulaId})`);
|
||||
@@ -253,7 +279,7 @@ const Spreadsheet = forwardRef<SpreadsheetRef, SpreadsheetProps>(
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
}, [activeSheetId, sheets]);
|
||||
}, [activeSheetId, sheets, updateSheetData, onDataChange]);
|
||||
|
||||
// Update Handsontable when currentData changes
|
||||
useEffect(() => {
|
||||
@@ -506,6 +532,15 @@ const Spreadsheet = forwardRef<SpreadsheetRef, SpreadsheetProps>(
|
||||
};
|
||||
}, [showSheetMenu]);
|
||||
|
||||
// Display a loading state if sheets aren't loaded yet
|
||||
if (sheets.length === 0 || !activeSheetId) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-gray-500">Loading spreadsheet data...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<SpreadsheetToolbar
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/lib/file/spreadsheet/config";
|
||||
|
||||
import { CellUpdate } from "@/types/api";
|
||||
import { db } from "@/lib/db";
|
||||
|
||||
// Define sheet interface
|
||||
export interface Sheet {
|
||||
@@ -96,20 +97,96 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
new Map(),
|
||||
);
|
||||
|
||||
// Initialize with a default sheet
|
||||
const [sheets, setSheets] = useState<Sheet[]>(() => {
|
||||
// Create first sheet and register with HyperFormula
|
||||
const hyperFormulaId = 0; // First sheet in HyperFormula is 0
|
||||
const defaultSheet: Sheet = {
|
||||
id: generateId(),
|
||||
name: "Sheet 1",
|
||||
data: [["", ""], ["", ""]],
|
||||
hyperFormulaId
|
||||
// Initialize with a default sheet if no data in DB
|
||||
const [sheets, setSheets] = useState<Sheet[]>([]);
|
||||
const [activeSheetId, setActiveSheetId] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load data from IndexedDB on initial render
|
||||
useEffect(() => {
|
||||
const loadFromDB = async () => {
|
||||
try {
|
||||
// Load sheets
|
||||
let storedSheets = await db.getAllSheets();
|
||||
|
||||
// Check for duplicate Sheet 1 and clean up if needed
|
||||
const sheet1Sheets = storedSheets.filter(sheet => sheet.name === "Sheet 1");
|
||||
if (sheet1Sheets.length > 1) {
|
||||
console.log("Found duplicate Sheet 1 entries, cleaning up...");
|
||||
// Keep the sheet with data (if any) or the latest one
|
||||
const sheetToKeep = sheet1Sheets.find(sheet =>
|
||||
sheet.data &&
|
||||
sheet.data.length > 0 &&
|
||||
sheet.data.some(row => row.some(cell => cell !== ""))
|
||||
) || sheet1Sheets[sheet1Sheets.length - 1];
|
||||
|
||||
// Delete all other duplicate Sheet 1 entries
|
||||
for (const sheet of sheet1Sheets) {
|
||||
if (sheet.id !== sheetToKeep.id) {
|
||||
await db.deleteSheet(sheet.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload sheets after cleanup
|
||||
storedSheets = await db.getAllSheets();
|
||||
}
|
||||
|
||||
// Load cell values
|
||||
const storedCellValues = await db.getAllCellValues();
|
||||
|
||||
// Load formulas
|
||||
const storedFormulas = await db.getAllFormulas();
|
||||
|
||||
// If no sheets found, create a default sheet
|
||||
if (storedSheets.length === 0) {
|
||||
// Create first sheet and register with HyperFormula
|
||||
const hyperFormulaId = 0; // First sheet in HyperFormula is 0
|
||||
const defaultSheet: Sheet = {
|
||||
id: generateId(),
|
||||
name: "Sheet 1",
|
||||
data: [["", ""], ["", ""]],
|
||||
hyperFormulaId
|
||||
};
|
||||
|
||||
// Save to the database
|
||||
await db.addSheet(defaultSheet);
|
||||
|
||||
setSheets([defaultSheet]);
|
||||
setActiveSheetId(defaultSheet.id);
|
||||
} else {
|
||||
// Use loaded sheets
|
||||
setSheets(storedSheets);
|
||||
|
||||
// Find active sheet in stored preferences or use the first sheet
|
||||
const lastActiveSheetId = storedSheets[0].id;
|
||||
setActiveSheetId(lastActiveSheetId);
|
||||
}
|
||||
|
||||
// Set cell values and formulas
|
||||
setCellValuesState(storedCellValues);
|
||||
setFormulaQueue(storedFormulas);
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.error("Error loading data from IndexedDB:", error);
|
||||
|
||||
// Fallback to default initialization
|
||||
const hyperFormulaId = 0;
|
||||
const defaultSheet: Sheet = {
|
||||
id: generateId(),
|
||||
name: "Sheet 1",
|
||||
data: [["", ""], ["", ""]],
|
||||
hyperFormulaId
|
||||
};
|
||||
|
||||
setSheets([defaultSheet]);
|
||||
setActiveSheetId(defaultSheet.id);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return [defaultSheet];
|
||||
});
|
||||
|
||||
const [activeSheetId, setActiveSheetId] = useState<string>(() => sheets[0].id);
|
||||
|
||||
loadFromDB();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextEvaluatedValues = new Map(evaluatedValues);
|
||||
@@ -124,12 +201,94 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
setEvaluatedValues(nextEvaluatedValues);
|
||||
}, [formulaQueue, cellValues, activeSheetId, sheets]);
|
||||
|
||||
// Save formula queue changes to IndexedDB
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
const saveFormulas = async () => {
|
||||
try {
|
||||
// Use transactions to ensure this operation is atomic
|
||||
await db.transaction('rw', db.formulaQueue, async () => {
|
||||
// We'll use this set to track which formulas to keep
|
||||
const formulaKeysToKeep = new Set<string>();
|
||||
|
||||
// Process current formulas
|
||||
formulaQueue.forEach((formula, cell) => {
|
||||
formulaKeysToKeep.add(cell);
|
||||
db.setFormula(cell, formula);
|
||||
});
|
||||
|
||||
// Delete any formulas in the DB that are not in our current queue
|
||||
const existingFormulas = await db.getAllFormulas();
|
||||
existingFormulas.forEach((formula, cell) => {
|
||||
if (!formulaKeysToKeep.has(cell)) {
|
||||
db.deleteFormula(cell);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving formulas to IndexedDB:", error);
|
||||
}
|
||||
};
|
||||
|
||||
saveFormulas();
|
||||
}, [formulaQueue, isLoading]);
|
||||
|
||||
// Save cell values changes to IndexedDB
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
const saveCellValues = async () => {
|
||||
try {
|
||||
await db.transaction('rw', db.cellValues, async () => {
|
||||
// We'll use this set to track which cell values to keep
|
||||
const cellKeysToKeep = new Set<string>();
|
||||
|
||||
// Process current cell values
|
||||
cellValues.forEach((value, cell) => {
|
||||
cellKeysToKeep.add(cell);
|
||||
db.setCellValue(cell, value);
|
||||
});
|
||||
|
||||
// Optional: Delete any cell values in the DB that are not in our current set
|
||||
// Only enable this if you want to completely sync DB with memory state
|
||||
// This may cause performance issues if there are many cells
|
||||
/*
|
||||
const existingCellValues = await db.getAllCellValues();
|
||||
existingCellValues.forEach((value, cell) => {
|
||||
if (!cellKeysToKeep.has(cell)) {
|
||||
db.deleteCellValue(cell);
|
||||
}
|
||||
});
|
||||
*/
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving cell values to IndexedDB:", error);
|
||||
}
|
||||
};
|
||||
|
||||
saveCellValues();
|
||||
}, [cellValues, isLoading]);
|
||||
|
||||
// Save active sheet ID to IndexedDB
|
||||
useEffect(() => {
|
||||
if (isLoading || !activeSheetId) return;
|
||||
|
||||
// No explicit save needed for active sheet ID as we're
|
||||
// saving the sheet state independently
|
||||
}, [activeSheetId, isLoading]);
|
||||
|
||||
const setFormula = (target: string, formula: string) => {
|
||||
setFormulaQueue((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(target, formula);
|
||||
return next;
|
||||
});
|
||||
|
||||
// Save to IndexedDB
|
||||
db.setFormula(target, formula).catch(error => {
|
||||
console.error("Error saving formula to IndexedDB:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const setFormulas = (updates: CellUpdate[]) => {
|
||||
@@ -147,6 +306,15 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
});
|
||||
return next;
|
||||
});
|
||||
|
||||
// Save to IndexedDB
|
||||
db.transaction('rw', db.formulaQueue, async () => {
|
||||
for (const update of updates) {
|
||||
await db.setFormula(update.target, update.formula);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error("Error saving formulas to IndexedDB:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const clearFormula = (target: string) => {
|
||||
@@ -155,6 +323,11 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
next.delete(target);
|
||||
return next;
|
||||
});
|
||||
|
||||
// Remove from IndexedDB
|
||||
db.deleteFormula(target).catch(error => {
|
||||
console.error("Error removing formula from IndexedDB:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const setChartData = (chartData: any) => {
|
||||
@@ -163,6 +336,11 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
next.set("chart", JSON.stringify(chartData));
|
||||
return next;
|
||||
});
|
||||
|
||||
// Save to IndexedDB
|
||||
db.setFormula("chart", JSON.stringify(chartData)).catch(error => {
|
||||
console.error("Error saving chart data to IndexedDB:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const setCellValues = (updates: Map<string, any>) => {
|
||||
@@ -173,6 +351,11 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
});
|
||||
return next;
|
||||
});
|
||||
|
||||
// Save to IndexedDB
|
||||
db.setCellValues(updates).catch(error => {
|
||||
console.error("Error saving cell values to IndexedDB:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const clearCellValues = (target: string) => {
|
||||
@@ -181,6 +364,11 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
next.delete(target);
|
||||
return next;
|
||||
});
|
||||
|
||||
// Remove from IndexedDB
|
||||
db.deleteCellValue(target).catch(error => {
|
||||
console.error("Error removing cell value from IndexedDB:", error);
|
||||
});
|
||||
};
|
||||
|
||||
// Sheet management functions
|
||||
@@ -207,6 +395,11 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
// Update state
|
||||
setSheets(prev => [...prev, newSheet]);
|
||||
setActiveSheetId(newSheet.id);
|
||||
|
||||
// Save to IndexedDB
|
||||
db.addSheet(newSheet).catch(error => {
|
||||
console.error("Error saving new sheet to IndexedDB:", error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error adding sheet:", error);
|
||||
}
|
||||
@@ -251,6 +444,11 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
|
||||
// Update our state to reflect this sheet is now "removed" (but actually just hidden)
|
||||
setSheets(prev => prev.filter(sheet => sheet.id !== sheetId));
|
||||
|
||||
// Remove from IndexedDB
|
||||
db.deleteSheet(sheetId).catch(error => {
|
||||
console.error("Error removing sheet from IndexedDB:", error);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// For non-first sheets, we can remove them normally
|
||||
@@ -258,16 +456,31 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
|
||||
// Update our state
|
||||
setSheets(prev => prev.filter(sheet => sheet.id !== sheetId));
|
||||
|
||||
// Remove from IndexedDB
|
||||
db.deleteSheet(sheetId).catch(error => {
|
||||
console.error("Error removing sheet from IndexedDB:", error);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If the hyperFormulaId is invalid, just remove it from our state
|
||||
console.log("Sheet has invalid hyperFormulaId, removing from state only:", sheetToRemove);
|
||||
setSheets(prev => prev.filter(sheet => sheet.id !== sheetId));
|
||||
|
||||
// Remove from IndexedDB
|
||||
db.deleteSheet(sheetId).catch(error => {
|
||||
console.error("Error removing sheet from IndexedDB:", error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error removing sheet:", error);
|
||||
// Even if there's an error with HyperFormula, still remove from our state
|
||||
setSheets(prev => prev.filter(sheet => sheet.id !== sheetId));
|
||||
|
||||
// Remove from IndexedDB
|
||||
db.deleteSheet(sheetId).catch(error => {
|
||||
console.error("Error removing sheet from IndexedDB:", error);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -298,6 +511,12 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
: sheet
|
||||
)
|
||||
);
|
||||
|
||||
// Update in IndexedDB
|
||||
const updatedSheet = { ...sheetToRename, name: finalName };
|
||||
db.updateSheet(updatedSheet).catch(error => {
|
||||
console.error("Error updating sheet name in IndexedDB:", error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error renaming sheet:", error);
|
||||
@@ -330,13 +549,19 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
clearHyperFormulaSheet(sheetToClear.hyperFormulaId);
|
||||
|
||||
// Update our state with empty data
|
||||
setSheets(prev =>
|
||||
prev.map(sheet =>
|
||||
sheet.id === sheetId
|
||||
? { ...sheet, data: [["", ""], ["", ""]] }
|
||||
: sheet
|
||||
)
|
||||
const emptyData = [["", ""], ["", ""]];
|
||||
const updatedSheets = sheets.map(sheet =>
|
||||
sheet.id === sheetId
|
||||
? { ...sheet, data: emptyData }
|
||||
: sheet
|
||||
);
|
||||
setSheets(updatedSheets);
|
||||
|
||||
// Update in IndexedDB
|
||||
const updatedSheet = { ...sheetToClear, data: emptyData };
|
||||
db.updateSheet(updatedSheet).catch(error => {
|
||||
console.error("Error updating cleared sheet in IndexedDB:", error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error clearing sheet:", error);
|
||||
}
|
||||
@@ -353,13 +578,18 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
|
||||
if (changes !== null) {
|
||||
// Update our state with the new data
|
||||
setSheets(prev =>
|
||||
prev.map(sheet =>
|
||||
sheet.id === sheetId
|
||||
? { ...sheet, data }
|
||||
: sheet
|
||||
)
|
||||
const updatedSheets = sheets.map(sheet =>
|
||||
sheet.id === sheetId
|
||||
? { ...sheet, data }
|
||||
: sheet
|
||||
);
|
||||
setSheets(updatedSheets);
|
||||
|
||||
// Update in IndexedDB - ensure we update the existing record
|
||||
const updatedSheet = { ...sheetToUpdate, data };
|
||||
db.updateSheet(updatedSheet).catch(error => {
|
||||
console.error("Error updating sheet data in IndexedDB:", error);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating sheet data:", error);
|
||||
@@ -380,6 +610,11 @@ export const SpreadsheetProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
return sheets.find(sheet => sheet.name === name);
|
||||
};
|
||||
|
||||
// Show loading state if still loading data from IndexedDB
|
||||
if (isLoading) {
|
||||
return <div>Loading spreadsheet data...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SpreadsheetContext.Provider
|
||||
value={{
|
||||
|
||||
245
src/lib/db/index.ts
Normal file
245
src/lib/db/index.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import Dexie, { Table } from 'dexie';
|
||||
|
||||
import { ChatMessage } from '@/types/api';
|
||||
import { Sheet } from '@/context/SpreadsheetContext';
|
||||
|
||||
// Define the database schema
|
||||
class SpreadsheetDatabase extends Dexie {
|
||||
// Tables
|
||||
sheets!: Table<Sheet>;
|
||||
cellValues!: Table<{id: string, cell: string, value: any}>;
|
||||
formulaQueue!: Table<{id: string, cell: string, formula: string}>;
|
||||
chatHistory!: Table<ChatMessage>;
|
||||
preferences!: Table<{id: string, key: string, value: any}>;
|
||||
|
||||
constructor() {
|
||||
super('ProblySpreadsheetDB');
|
||||
|
||||
// Define tables and indexes
|
||||
this.version(1).stores({
|
||||
sheets: '++id, name, hyperFormulaId', // Primary key is id
|
||||
cellValues: '++id, cell', // Store cell values with cell reference as index
|
||||
formulaQueue: '++id, cell', // Store formulas with cell reference as index
|
||||
chatHistory: '++id, timestamp', // Store chat history with timestamp as index
|
||||
preferences: 'id, key' // Store user preferences
|
||||
});
|
||||
}
|
||||
|
||||
// Helper methods for sheets
|
||||
async getAllSheets(): Promise<Sheet[]> {
|
||||
return await this.sheets.toArray();
|
||||
}
|
||||
|
||||
async getSheetById(id: string): Promise<Sheet | undefined> {
|
||||
return await this.sheets.get(id);
|
||||
}
|
||||
|
||||
async getSheetByName(name: string): Promise<Sheet | undefined> {
|
||||
return await this.sheets.where('name').equals(name).first();
|
||||
}
|
||||
|
||||
async addSheet(sheet: Sheet): Promise<string> {
|
||||
return await this.sheets.add(sheet);
|
||||
}
|
||||
|
||||
async updateSheet(sheet: Sheet): Promise<number> {
|
||||
return await this.sheets.update(sheet.id, {
|
||||
name: sheet.name,
|
||||
data: sheet.data,
|
||||
hyperFormulaId: sheet.hyperFormulaId
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSheet(id: string): Promise<void> {
|
||||
await this.sheets.delete(id);
|
||||
}
|
||||
|
||||
// Helper methods for cell values
|
||||
async getCellValue(cell: string): Promise<any> {
|
||||
const record = await this.cellValues.where('cell').equals(cell).first();
|
||||
return record?.value;
|
||||
}
|
||||
|
||||
async getAllCellValues(): Promise<Map<string, any>> {
|
||||
const records = await this.cellValues.toArray();
|
||||
const map = new Map<string, any>();
|
||||
records.forEach(record => {
|
||||
map.set(record.cell, record.value);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
async setCellValue(cell: string, value: any): Promise<void> {
|
||||
const existing = await this.cellValues.where('cell').equals(cell).first();
|
||||
if (existing) {
|
||||
await this.cellValues.update(existing.id, { value });
|
||||
} else {
|
||||
await this.cellValues.add({ id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, cell, value });
|
||||
}
|
||||
}
|
||||
|
||||
async setCellValues(values: Map<string, any>): Promise<void> {
|
||||
await this.transaction('rw', this.cellValues, async () => {
|
||||
const promises: Promise<any>[] = [];
|
||||
values.forEach((value, cell) => {
|
||||
promises.push(this.setCellValue(cell, value));
|
||||
});
|
||||
await Promise.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCellValue(cell: string): Promise<void> {
|
||||
await this.cellValues.where('cell').equals(cell).delete();
|
||||
}
|
||||
|
||||
// Helper methods for formula queue
|
||||
async getFormula(cell: string): Promise<string | undefined> {
|
||||
const record = await this.formulaQueue.where('cell').equals(cell).first();
|
||||
return record?.formula;
|
||||
}
|
||||
|
||||
async getAllFormulas(): Promise<Map<string, string>> {
|
||||
const records = await this.formulaQueue.toArray();
|
||||
const map = new Map<string, string>();
|
||||
records.forEach(record => {
|
||||
map.set(record.cell, record.formula);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
async setFormula(cell: string, formula: string): Promise<void> {
|
||||
const existing = await this.formulaQueue.where('cell').equals(cell).first();
|
||||
if (existing) {
|
||||
await this.formulaQueue.update(existing.id, { formula });
|
||||
} else {
|
||||
await this.formulaQueue.add({ id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, cell, formula });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFormula(cell: string): Promise<void> {
|
||||
await this.formulaQueue.where('cell').equals(cell).delete();
|
||||
}
|
||||
|
||||
// Helper methods for chat history
|
||||
async getChatHistory(): Promise<ChatMessage[]> {
|
||||
return await this.chatHistory.orderBy('timestamp').toArray();
|
||||
}
|
||||
|
||||
async addChatMessage(message: ChatMessage): Promise<string> {
|
||||
return await this.chatHistory.add(message);
|
||||
}
|
||||
|
||||
async updateChatMessage(message: ChatMessage): Promise<number> {
|
||||
return await this.chatHistory.update(message.id, {
|
||||
text: message.text,
|
||||
response: message.response,
|
||||
timestamp: message.timestamp,
|
||||
status: message.status,
|
||||
updates: message.updates,
|
||||
chartData: message.chartData,
|
||||
analysis: message.analysis,
|
||||
hasImage: message.hasImage,
|
||||
documentImage: message.documentImage
|
||||
});
|
||||
}
|
||||
|
||||
async clearChatHistory(): Promise<void> {
|
||||
await this.chatHistory.clear();
|
||||
}
|
||||
|
||||
// Helper methods for preferences
|
||||
async getPreference(key: string): Promise<any> {
|
||||
const record = await this.preferences.where('key').equals(key).first();
|
||||
return record?.value;
|
||||
}
|
||||
|
||||
async setPreference(key: string, value: any): Promise<void> {
|
||||
const id = `pref_${key}`;
|
||||
const existing = await this.preferences.get(id);
|
||||
if (existing) {
|
||||
await this.preferences.update(id, { value });
|
||||
} else {
|
||||
await this.preferences.add({ id, key, value });
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all data for a fresh start
|
||||
async clearDatabase(): Promise<void> {
|
||||
await this.transaction('rw', [this.sheets, this.cellValues, this.formulaQueue, this.chatHistory, this.preferences], async () => {
|
||||
await this.sheets.clear();
|
||||
await this.cellValues.clear();
|
||||
await this.formulaQueue.clear();
|
||||
await this.chatHistory.clear();
|
||||
await this.preferences.clear();
|
||||
});
|
||||
}
|
||||
|
||||
// Special method to clean up duplicate sheets
|
||||
async cleanupDuplicateSheets(): Promise<number> {
|
||||
try {
|
||||
// Get all sheets
|
||||
const sheets = await this.sheets.toArray();
|
||||
|
||||
// Group sheets by name
|
||||
const sheetsByName: Record<string, Sheet[]> = {};
|
||||
sheets.forEach(sheet => {
|
||||
if (!sheetsByName[sheet.name]) {
|
||||
sheetsByName[sheet.name] = [];
|
||||
}
|
||||
sheetsByName[sheet.name].push(sheet);
|
||||
});
|
||||
|
||||
// Find duplicate names
|
||||
const duplicateNames = Object.keys(sheetsByName).filter(name =>
|
||||
sheetsByName[name].length > 1
|
||||
);
|
||||
|
||||
if (duplicateNames.length === 0) {
|
||||
console.log("No duplicate sheets found.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.log(`Found ${duplicateNames.length} sheet names with duplicates: ${duplicateNames.join(', ')}`);
|
||||
|
||||
// Clean up duplicates - we'll keep the sheet with actual data or the most recent one
|
||||
let removedCount = 0;
|
||||
|
||||
await this.transaction('rw', this.sheets, async () => {
|
||||
for (const name of duplicateNames) {
|
||||
const duplicates = sheetsByName[name];
|
||||
|
||||
// Find sheet with actual data (non-empty cells)
|
||||
const sheetWithData = duplicates.find(sheet =>
|
||||
sheet.data &&
|
||||
sheet.data.length > 0 &&
|
||||
sheet.data.some(row => row.some(cell => cell !== ""))
|
||||
);
|
||||
|
||||
// If no sheet has data, keep the most recent one (assuming ID is timestamp-based)
|
||||
const sheetToKeep = sheetWithData ||
|
||||
duplicates.sort((a, b) => b.id.localeCompare(a.id))[0];
|
||||
|
||||
// Delete all duplicates except the one to keep
|
||||
for (const sheet of duplicates) {
|
||||
if (sheet.id !== sheetToKeep.id) {
|
||||
await this.sheets.delete(sheet.id);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Cleaned up ${removedCount} duplicate sheets.`);
|
||||
return removedCount;
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up duplicate sheets:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance of the database
|
||||
export const db = new SpreadsheetDatabase();
|
||||
|
||||
// Export the database class type for type checking
|
||||
export type { SpreadsheetDatabase };
|
||||
Reference in New Issue
Block a user