Add api settings to settings modal
This commit is contained in:
parent
b978e8c58a
commit
44d26acb8a
4 changed files with 295 additions and 25 deletions
|
|
@ -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 (
|
||||
<ThemeProvider>
|
||||
<TTSProvider>
|
||||
<PDFProvider>
|
||||
{children}
|
||||
</PDFProvider>
|
||||
</TTSProvider>
|
||||
<ConfigProvider>
|
||||
<TTSProvider>
|
||||
<PDFProvider>
|
||||
{children}
|
||||
</PDFProvider>
|
||||
</TTSProvider>
|
||||
</ConfigProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
|
||||
|
|
@ -122,19 +131,59 @@ export function SettingsModal({ isOpen, setIsOpen }: SettingsModalProps) {
|
|||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-foreground">OpenAI API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
value={localApiKey}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-foreground">OpenAI API Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={localBaseUrl}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end">
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-lg bg-accent px-4 py-2 text-sm
|
||||
font-medium text-white hover:bg-accent/90 focus:outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
|
||||
transition-colors"
|
||||
onClick={() => setIsOpen(false)}
|
||||
onClick={async () => {
|
||||
await updateConfig({
|
||||
apiKey: localApiKey,
|
||||
baseUrl: localBaseUrl
|
||||
});
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-lg bg-background px-4 py-2 text-sm
|
||||
font-medium text-foreground hover:bg-background/90 focus:outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
|
||||
transition-colors"
|
||||
onClick={() => {
|
||||
setLocalApiKey(apiKey);
|
||||
setLocalBaseUrl(baseUrl);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
|
|
|
|||
77
src/contexts/ConfigContext.tsx
Normal file
77
src/contexts/ConfigContext.tsx
Normal file
|
|
@ -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<void>;
|
||||
}
|
||||
|
||||
const ConfigContext = createContext<ConfigContextType | undefined>(undefined);
|
||||
|
||||
export function ConfigProvider({ children }: { children: React.ReactNode }) {
|
||||
const [apiKey, setApiKey] = useState<string>('');
|
||||
const [baseUrl, setBaseUrl] = useState<string>('');
|
||||
|
||||
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 (
|
||||
<ConfigContext.Provider value={{ apiKey, baseUrl, updateConfig }}>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useConfig() {
|
||||
const context = useContext(ConfigContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useConfig must be used within a ConfigProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
@ -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<void> {
|
||||
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<PDFDocument | undefined> {
|
||||
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<PDFDocument[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
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<Record<string, string>> {
|
||||
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<string, string> = {};
|
||||
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<void> {
|
||||
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<string | null> {
|
||||
return indexedDBService.getConfigItem(key);
|
||||
}
|
||||
|
||||
export async function setItem(key: string, value: string): Promise<void> {
|
||||
return indexedDBService.setConfigItem(key, value);
|
||||
}
|
||||
Loading…
Reference in a new issue