diff --git a/app/components/settings/SettingsWindow.tsx b/app/components/settings/SettingsWindow.tsx index 541323f50..301cf5adf 100644 --- a/app/components/settings/SettingsWindow.tsx +++ b/app/components/settings/SettingsWindow.tsx @@ -5,27 +5,27 @@ import { classNames } from '~/utils/classNames'; import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog'; import { IconButton } from '~/components/ui/IconButton'; import styles from './Settings.module.scss'; -import ChatHistoryTab from './chat-history/ChatHistoryTab'; import ProvidersTab from './providers/ProvidersTab'; import { useSettings } from '~/lib/hooks/useSettings'; import FeaturesTab from './features/FeaturesTab'; import DebugTab from './debug/DebugTab'; import EventLogsTab from './event-logs/EventLogsTab'; import ConnectionsTab from './connections/ConnectionsTab'; +import DataTab from './data/DataTab'; interface SettingsProps { open: boolean; onClose: () => void; } -type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection'; +type TabType = 'data' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection'; export const SettingsWindow = ({ open, onClose }: SettingsProps) => { const { debug, eventLogs } = useSettings(); - const [activeTab, setActiveTab] = useState('chat-history'); + const [activeTab, setActiveTab] = useState('data'); const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [ - { id: 'chat-history', label: 'Chat History', icon: 'i-ph:book', component: }, + { id: 'data', label: 'Data', icon: 'i-ph:database', component: }, { id: 'providers', label: 'Providers', icon: 'i-ph:key', component: }, { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: }, { id: 'features', label: 'Features', icon: 'i-ph:star', component: }, diff --git a/app/components/settings/chat-history/ChatHistoryTab.tsx b/app/components/settings/chat-history/ChatHistoryTab.tsx deleted file mode 100644 index ea5187b25..000000000 --- a/app/components/settings/chat-history/ChatHistoryTab.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useNavigate } from '@remix-run/react'; -import React, { useState } from 'react'; -import { toast } from 'react-toastify'; -import { db, deleteById, getAll } from '~/lib/persistence'; -import { classNames } from '~/utils/classNames'; -import styles from '~/components/settings/Settings.module.scss'; -import { logStore } from '~/lib/stores/logs'; // Import logStore for event logging - -export default function ChatHistoryTab() { - const navigate = useNavigate(); - const [isDeleting, setIsDeleting] = useState(false); - const downloadAsJson = (data: any, filename: string) => { - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - }; - - const handleDeleteAllChats = async () => { - const confirmDelete = window.confirm('Are you sure you want to delete all chats? This action cannot be undone.'); - - if (!confirmDelete) { - return; // Exit if the user cancels - } - - if (!db) { - const error = new Error('Database is not available'); - logStore.logError('Failed to delete chats - DB unavailable', error); - toast.error('Database is not available'); - - return; - } - - try { - setIsDeleting(true); - - const allChats = await getAll(db); - await Promise.all(allChats.map((chat) => deleteById(db!, chat.id))); - logStore.logSystem('All chats deleted successfully', { count: allChats.length }); - toast.success('All chats deleted successfully'); - navigate('/', { replace: true }); - } catch (error) { - logStore.logError('Failed to delete chats', error); - toast.error('Failed to delete chats'); - console.error(error); - } finally { - setIsDeleting(false); - } - }; - - const handleExportAllChats = async () => { - if (!db) { - const error = new Error('Database is not available'); - logStore.logError('Failed to export chats - DB unavailable', error); - toast.error('Database is not available'); - - return; - } - - try { - const allChats = await getAll(db); - const exportData = { - chats: allChats, - exportDate: new Date().toISOString(), - }; - - downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`); - logStore.logSystem('Chats exported successfully', { count: allChats.length }); - toast.success('Chats exported successfully'); - } catch (error) { - logStore.logError('Failed to export chats', error); - toast.error('Failed to export chats'); - console.error(error); - } - }; - - return ( - <> -
-

Chat History

- - -
-

Danger Area

-

This action cannot be undone!

- -
-
- - ); -} diff --git a/app/components/settings/data/DataTab.tsx b/app/components/settings/data/DataTab.tsx new file mode 100644 index 000000000..756abaadb --- /dev/null +++ b/app/components/settings/data/DataTab.tsx @@ -0,0 +1,304 @@ +import React, { useState } from 'react'; +import { useNavigate } from '@remix-run/react'; +import Cookies from 'js-cookie'; +import { toast } from 'react-toastify'; +import { db, deleteById, getAll } from '~/lib/persistence'; +import { logStore } from '~/lib/stores/logs'; +import { classNames } from '~/utils/classNames'; +import styles from '~/components/settings/Settings.module.scss'; + +// List of supported providers that can have API keys +const API_KEY_PROVIDERS = [ + 'Anthropic', + 'OpenAI', + 'Google', + 'Groq', + 'HuggingFace', + 'OpenRouter', + 'Deepseek', + 'Mistral', + 'OpenAILike', + 'Together', + 'xAI', + 'Perplexity', + 'Cohere', + 'AzureOpenAI', +] as const; + +type Provider = typeof API_KEY_PROVIDERS[number]; + +interface ApiKeys { + [key: string]: string; +} + +export default function DataTab() { + const navigate = useNavigate(); + const [isDeleting, setIsDeleting] = useState(false); + + const downloadAsJson = (data: any, filename: string) => { + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + const handleExportAllChats = async () => { + if (!db) { + const error = new Error('Database is not available'); + logStore.logError('Failed to export chats - DB unavailable', error); + toast.error('Database is not available'); + return; + } + + try { + const allChats = await getAll(db); + const exportData = { + chats: allChats, + exportDate: new Date().toISOString(), + }; + + downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`); + logStore.logSystem('Chats exported successfully', { count: allChats.length }); + toast.success('Chats exported successfully'); + } catch (error) { + logStore.logError('Failed to export chats', error); + toast.error('Failed to export chats'); + console.error(error); + } + }; + + const handleDeleteAllChats = async () => { + const confirmDelete = window.confirm('Are you sure you want to delete all chats? This action cannot be undone.'); + + if (!confirmDelete) { + return; + } + + if (!db) { + const error = new Error('Database is not available'); + logStore.logError('Failed to delete chats - DB unavailable', error); + toast.error('Database is not available'); + return; + } + + try { + setIsDeleting(true); + const allChats = await getAll(db); + await Promise.all(allChats.map((chat) => deleteById(db!, chat.id))); + logStore.logSystem('All chats deleted successfully', { count: allChats.length }); + toast.success('All chats deleted successfully'); + navigate('/', { replace: true }); + } catch (error) { + logStore.logError('Failed to delete chats', error); + toast.error('Failed to delete chats'); + console.error(error); + } finally { + setIsDeleting(false); + } + }; + + const handleExportSettings = () => { + const settings = { + providers: Cookies.get('providers'), + isDebugEnabled: Cookies.get('isDebugEnabled'), + isEventLogsEnabled: Cookies.get('isEventLogsEnabled'), + isLocalModelsEnabled: Cookies.get('isLocalModelsEnabled'), + promptId: Cookies.get('promptId'), + isLatestBranch: Cookies.get('isLatestBranch'), + commitHash: Cookies.get('commitHash'), + eventLogs: Cookies.get('eventLogs'), + selectedModel: Cookies.get('selectedModel'), + selectedProvider: Cookies.get('selectedProvider'), + githubUsername: Cookies.get('githubUsername'), + githubToken: Cookies.get('githubToken'), + bolt_theme: localStorage.getItem('bolt_theme'), + }; + + downloadAsJson(settings, 'bolt-settings.json'); + toast.success('Settings exported successfully'); + }; + + const handleImportSettings = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const settings = JSON.parse(e.target?.result as string); + + Object.entries(settings).forEach(([key, value]) => { + if (key === 'bolt_theme') { + if (value) localStorage.setItem(key, value as string); + } else if (value) { + Cookies.set(key, value as string); + } + }); + + toast.success('Settings imported successfully. Please refresh the page for changes to take effect.'); + } catch (error) { + toast.error('Failed to import settings. Make sure the file is a valid JSON file.'); + console.error('Failed to import settings:', error); + } + }; + reader.readAsText(file); + event.target.value = ''; + }; + + const handleExportApiKeyTemplate = () => { + const template: ApiKeys = {}; + API_KEY_PROVIDERS.forEach(provider => { + template[`${provider}_API_KEY`] = ''; + }); + + template['OPENAI_LIKE_API_BASE_URL'] = ''; + template['LMSTUDIO_API_BASE_URL'] = ''; + template['OLLAMA_API_BASE_URL'] = ''; + template['TOGETHER_API_BASE_URL'] = ''; + + downloadAsJson(template, 'api-keys-template.json'); + toast.success('API keys template exported successfully'); + }; + + const handleImportApiKeys = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const apiKeys = JSON.parse(e.target?.result as string); + let importedCount = 0; + const consolidatedKeys: Record = {}; + + API_KEY_PROVIDERS.forEach(provider => { + const keyName = `${provider}_API_KEY`; + if (apiKeys[keyName]) { + consolidatedKeys[provider] = apiKeys[keyName]; + importedCount++; + } + }); + + if (importedCount > 0) { + // Store all API keys in a single cookie as JSON + Cookies.set('apiKeys', JSON.stringify(consolidatedKeys)); + + // Also set individual cookies for backward compatibility + Object.entries(consolidatedKeys).forEach(([provider, key]) => { + Cookies.set(`${provider}_API_KEY`, key); + }); + + toast.success(`Successfully imported ${importedCount} API keys/URLs. Refreshing page to apply changes...`); + // Reload the page after a short delay to allow the toast to be seen + setTimeout(() => { + window.location.reload(); + }, 1500); + } else { + toast.warn('No valid API keys found in the file'); + } + + // Set base URLs if they exist + ['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach(baseUrl => { + if (apiKeys[baseUrl]) { + Cookies.set(baseUrl, apiKeys[baseUrl]); + } + }); + + } catch (error) { + toast.error('Failed to import API keys. Make sure the file is a valid JSON file.'); + console.error('Failed to import API keys:', error); + } + }; + reader.readAsText(file); + event.target.value = ''; + }; + + return ( +
+
+

Data Management

+
+
+
+

Chat History

+

+ Export or delete all your chat history. +

+
+ + +
+
+ +
+

Settings Backup

+

+ Export your settings to a JSON file or import settings from a previously exported file. +

+
+ + +
+
+ +
+

API Keys Management

+

+ Import API keys from a JSON file or download a template to fill in your keys. +

+
+ + +
+
+
+
+
+
+ ); +} \ No newline at end of file