testing indexed db

This commit is contained in:
tobiadefami
2025-04-30 01:12:45 +01:00
parent fa15f8ac8a
commit 1a0f008ba4
6 changed files with 683 additions and 99 deletions

7
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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">

View File

@@ -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

View File

@@ -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
View 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 };