diff --git a/src/app/providers.tsx b/src/app/providers.tsx index cfb8738..5f14bd2 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -3,16 +3,19 @@ import { PDFProvider } from '@/contexts/PDFContext'; import { TTSProvider } from '@/contexts/TTSContext'; import { ThemeProvider } from '@/contexts/ThemeContext'; +import { ConfigProvider } from '@/contexts/ConfigContext'; import { ReactNode } from 'react'; export function Providers({ children }: { children: ReactNode }) { return ( - - - {children} - - + + + + {children} + + + ); } diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 03af752..2565087 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -1,8 +1,9 @@ 'use client'; -import { Fragment } from 'react'; +import { Fragment, useState, useEffect } from 'react'; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react'; import { useTheme } from '@/contexts/ThemeContext'; +import { useConfig } from '@/contexts/ConfigContext'; interface SettingsModalProps { isOpen: boolean; @@ -17,8 +18,16 @@ const themes = [ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) { const { theme, setTheme } = useTheme(); + const { apiKey, baseUrl, updateConfig } = useConfig(); + const [localApiKey, setLocalApiKey] = useState(apiKey); + const [localBaseUrl, setLocalBaseUrl] = useState(baseUrl); const selectedTheme = themes.find(t => t.id === theme) || themes[0]; + useEffect(() => { + setLocalApiKey(apiKey); + setLocalBaseUrl(baseUrl); + }, [apiKey, baseUrl]); + return ( setIsOpen(false)}> @@ -122,19 +131,59 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) { + +
+ + setLocalApiKey(e.target.value)} + className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
+ +
+ + setLocalBaseUrl(e.target.value)} + className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent" + /> +
-
+
+
diff --git a/src/contexts/ConfigContext.tsx b/src/contexts/ConfigContext.tsx new file mode 100644 index 0000000..44a4c18 --- /dev/null +++ b/src/contexts/ConfigContext.tsx @@ -0,0 +1,77 @@ +'use client'; + +import { createContext, useContext, useEffect, useState } from 'react'; +import { getItem, setItem } from '@/services/indexedDB'; + +interface ConfigContextType { + apiKey: string; + baseUrl: string; + updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => Promise; +} + +const ConfigContext = createContext(undefined); + +export function ConfigProvider({ children }: { children: React.ReactNode }) { + const [apiKey, setApiKey] = useState(''); + const [baseUrl, setBaseUrl] = useState(''); + + useEffect(() => { + const loadConfig = async () => { + try { + // Try to load from IndexedDB first + const cachedApiKey = await getItem('apiKey'); + const cachedBaseUrl = await getItem('baseUrl'); + + // If not in cache, use env variables + const defaultApiKey = process.env.NEXT_PUBLIC_OPENAI_API_KEY || ''; + const defaultBaseUrl = process.env.NEXT_PUBLIC_OPENAI_API_BASE || ''; + + // Set the values + setApiKey(cachedApiKey || defaultApiKey); + setBaseUrl(cachedBaseUrl || defaultBaseUrl); + + // If we used default values and they're not empty, store them in IndexedDB + if (!cachedApiKey && defaultApiKey) { + await setItem('apiKey', defaultApiKey); + } + if (!cachedBaseUrl && defaultBaseUrl) { + await setItem('baseUrl', defaultBaseUrl); + } + } catch (error) { + console.error('Error loading config:', error); + } + }; + + loadConfig(); + }, []); + + const updateConfig = async (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => { + try { + if (newConfig.apiKey !== undefined) { + await setItem('apiKey', newConfig.apiKey); + setApiKey(newConfig.apiKey); + } + if (newConfig.baseUrl !== undefined) { + await setItem('baseUrl', newConfig.baseUrl); + setBaseUrl(newConfig.baseUrl); + } + } catch (error) { + console.error('Error updating config:', error); + throw error; + } + }; + + return ( + + {children} + + ); +} + +export function useConfig() { + const context = useContext(ConfigContext); + if (context === undefined) { + throw new Error('useConfig must be used within a ConfigProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/services/indexedDB.ts b/src/services/indexedDB.ts index 5315235..bcf06e7 100644 --- a/src/services/indexedDB.ts +++ b/src/services/indexedDB.ts @@ -1,6 +1,7 @@ const DB_NAME = 'openreader-db'; -const DB_VERSION = 1; -const STORE_NAME = 'pdf-documents'; +const DB_VERSION = 2; +const PDF_STORE_NAME = 'pdf-documents'; +const CONFIG_STORE_NAME = 'config'; export interface PDFDocument { id: string; @@ -10,6 +11,11 @@ export interface PDFDocument { data: Blob; } +export interface Config { + key: string; + value: string; +} + class IndexedDBService { private db: IDBDatabase | null = null; @@ -37,25 +43,31 @@ class IndexedDBService { request.onupgradeneeded = (event) => { console.log('Upgrading IndexedDB schema...'); const db = (event.target as IDBOpenDBRequest).result; - if (!db.objectStoreNames.contains(STORE_NAME)) { + + if (!db.objectStoreNames.contains(PDF_STORE_NAME)) { console.log('Creating PDF documents store...'); - db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + db.createObjectStore(PDF_STORE_NAME, { keyPath: 'id' }); + } + + if (!db.objectStoreNames.contains(CONFIG_STORE_NAME)) { + console.log('Creating config store...'); + db.createObjectStore(CONFIG_STORE_NAME, { keyPath: 'key' }); } }; }); } + // PDF Document Methods async addDocument(document: PDFDocument): Promise { if (!this.db) { - console.log('Database not initialized, initializing now...'); await this.init(); } return new Promise((resolve, reject) => { try { console.log('Adding document to IndexedDB:', document.name); - const transaction = this.db!.transaction([STORE_NAME], 'readwrite'); - const store = transaction.objectStore(STORE_NAME); + const transaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(PDF_STORE_NAME); const request = store.put(document); request.onerror = (event) => { @@ -77,15 +89,14 @@ class IndexedDBService { async getDocument(id: string): Promise { if (!this.db) { - console.log('Database not initialized, initializing now...'); await this.init(); } return new Promise((resolve, reject) => { try { console.log('Fetching document:', id); - const transaction = this.db!.transaction([STORE_NAME], 'readonly'); - const store = transaction.objectStore(STORE_NAME); + const transaction = this.db!.transaction([PDF_STORE_NAME], 'readonly'); + const store = transaction.objectStore(PDF_STORE_NAME); const request = store.get(id); request.onerror = (event) => { @@ -107,15 +118,14 @@ class IndexedDBService { async getAllDocuments(): Promise { if (!this.db) { - console.log('Database not initialized, initializing now...'); await this.init(); } return new Promise((resolve, reject) => { try { console.log('Fetching all documents'); - const transaction = this.db!.transaction([STORE_NAME], 'readonly'); - const store = transaction.objectStore(STORE_NAME); + const transaction = this.db!.transaction([PDF_STORE_NAME], 'readonly'); + const store = transaction.objectStore(PDF_STORE_NAME); const request = store.getAll(); request.onerror = (event) => { @@ -137,15 +147,14 @@ class IndexedDBService { async removeDocument(id: string): Promise { if (!this.db) { - console.log('Database not initialized, initializing now...'); await this.init(); } return new Promise((resolve, reject) => { try { console.log('Removing document:', id); - const transaction = this.db!.transaction([STORE_NAME], 'readwrite'); - const store = transaction.objectStore(STORE_NAME); + const transaction = this.db!.transaction([PDF_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(PDF_STORE_NAME); const request = store.delete(id); request.onerror = (event) => { @@ -164,6 +173,138 @@ class IndexedDBService { } }); } + + // Config Methods + async setConfigItem(key: string, value: string): Promise { + if (!this.db) { + await this.init(); + } + + return new Promise((resolve, reject) => { + try { + console.log('Setting config item:', key); + const transaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(CONFIG_STORE_NAME); + const request = store.put({ key, value }); + + request.onerror = (event) => { + const error = (event.target as IDBRequest).error; + console.error('Error setting config item:', error); + reject(error); + }; + + transaction.oncomplete = () => { + console.log('Config item set successfully:', key); + resolve(); + }; + } catch (error) { + console.error('Error in setConfigItem transaction:', error); + reject(error); + } + }); + } + + async getConfigItem(key: string): Promise { + if (!this.db) { + await this.init(); + } + + return new Promise((resolve, reject) => { + try { + console.log('Fetching config item:', key); + const transaction = this.db!.transaction([CONFIG_STORE_NAME], 'readonly'); + const store = transaction.objectStore(CONFIG_STORE_NAME); + const request = store.get(key); + + request.onerror = (event) => { + const error = (event.target as IDBRequest).error; + console.error('Error fetching config item:', error); + reject(error); + }; + + request.onsuccess = () => { + const result = request.result as Config | undefined; + console.log('Config item fetch result:', result ? 'found' : 'not found'); + resolve(result ? result.value : null); + }; + } catch (error) { + console.error('Error in getConfigItem transaction:', error); + reject(error); + } + }); + } + + async getAllConfig(): Promise> { + if (!this.db) { + await this.init(); + } + + return new Promise((resolve, reject) => { + try { + console.log('Fetching all config items'); + const transaction = this.db!.transaction([CONFIG_STORE_NAME], 'readonly'); + const store = transaction.objectStore(CONFIG_STORE_NAME); + const request = store.getAll(); + + request.onerror = (event) => { + const error = (event.target as IDBRequest).error; + console.error('Error fetching all config items:', error); + reject(error); + }; + + request.onsuccess = () => { + const result = request.result as Config[]; + const config: Record = {}; + result.forEach((item) => { + config[item.key] = item.value; + }); + console.log('Retrieved config items count:', result.length); + resolve(config); + }; + } catch (error) { + console.error('Error in getAllConfig transaction:', error); + reject(error); + } + }); + } + + async removeConfigItem(key: string): Promise { + if (!this.db) { + await this.init(); + } + + return new Promise((resolve, reject) => { + try { + console.log('Removing config item:', key); + const transaction = this.db!.transaction([CONFIG_STORE_NAME], 'readwrite'); + const store = transaction.objectStore(CONFIG_STORE_NAME); + const request = store.delete(key); + + request.onerror = (event) => { + const error = (event.target as IDBRequest).error; + console.error('Error removing config item:', error); + reject(error); + }; + + transaction.oncomplete = () => { + console.log('Config item removed successfully:', key); + resolve(); + }; + } catch (error) { + console.error('Error in removeConfigItem transaction:', error); + reject(error); + } + }); + } } export const indexedDBService = new IndexedDBService(); + +// Helper functions for the ConfigContext +export async function getItem(key: string): Promise { + return indexedDBService.getConfigItem(key); +} + +export async function setItem(key: string, value: string): Promise { + return indexedDBService.setConfigItem(key, value); +} \ No newline at end of file