bolt.diy/app/components/@settings/tabs/data/DataTab.tsx
KevIsDev 4354ad45b9
Some checks are pending
Docker Publish / docker-build-publish (push) Waiting to run
Update Stable Branch / prepare-release (push) Waiting to run
git push origin mainRevert "fix: fix icon classes for consistency and clarity #release:major"
This reverts commit 870828d551.
2025-05-12 00:15:16 +01:00

722 lines
29 KiB
TypeScript

import { useState, useRef, useCallback, useEffect } from 'react';
import { Button } from '~/components/ui/Button';
import { ConfirmationDialog, SelectionDialog } from '~/components/ui/Dialog';
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '~/components/ui/Card';
import { motion } from 'framer-motion';
import { useDataOperations } from '~/lib/hooks/useDataOperations';
import { openDatabase } from '~/lib/persistence/db';
import { getAllChats, type Chat } from '~/lib/persistence/chats';
import { DataVisualization } from './DataVisualization';
import { classNames } from '~/utils/classNames';
import { toast } from 'react-toastify';
// Create a custom hook to connect to the boltHistory database
function useBoltHistoryDB() {
const [db, setDb] = useState<IDBDatabase | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const initDB = async () => {
try {
setIsLoading(true);
const database = await openDatabase();
setDb(database || null);
setIsLoading(false);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error initializing database'));
setIsLoading(false);
}
};
initDB();
return () => {
if (db) {
db.close();
}
};
}, []);
return { db, isLoading, error };
}
// Extend the Chat interface to include the missing properties
interface ExtendedChat extends Chat {
title?: string;
updatedAt?: number;
}
// Helper function to create a chat label and description
function createChatItem(chat: Chat): ChatItem {
return {
id: chat.id,
// Use description as title if available, or format a short ID
label: (chat as ExtendedChat).title || chat.description || `Chat ${chat.id.slice(0, 8)}`,
// Format the description with message count and timestamp
description: `${chat.messages.length} messages - Last updated: ${new Date((chat as ExtendedChat).updatedAt || Date.parse(chat.timestamp)).toLocaleString()}`,
};
}
interface SettingsCategory {
id: string;
label: string;
description: string;
}
interface ChatItem {
id: string;
label: string;
description: string;
}
export function DataTab() {
// Use our custom hook for the boltHistory database
const { db, isLoading: dbLoading } = useBoltHistoryDB();
const fileInputRef = useRef<HTMLInputElement>(null);
const apiKeyFileInputRef = useRef<HTMLInputElement>(null);
const chatFileInputRef = useRef<HTMLInputElement>(null);
// State for confirmation dialogs
const [showResetInlineConfirm, setShowResetInlineConfirm] = useState(false);
const [showDeleteInlineConfirm, setShowDeleteInlineConfirm] = useState(false);
const [showSettingsSelection, setShowSettingsSelection] = useState(false);
const [showChatsSelection, setShowChatsSelection] = useState(false);
// State for settings categories and available chats
const [settingsCategories] = useState<SettingsCategory[]>([
{ id: 'core', label: 'Core Settings', description: 'User profile and main settings' },
{ id: 'providers', label: 'Providers', description: 'API keys and provider configurations' },
{ id: 'features', label: 'Features', description: 'Feature flags and settings' },
{ id: 'ui', label: 'UI', description: 'UI configuration and preferences' },
{ id: 'connections', label: 'Connections', description: 'External service connections' },
{ id: 'debug', label: 'Debug', description: 'Debug settings and logs' },
{ id: 'updates', label: 'Updates', description: 'Update settings and notifications' },
]);
const [availableChats, setAvailableChats] = useState<ExtendedChat[]>([]);
const [chatItems, setChatItems] = useState<ChatItem[]>([]);
// Data operations hook with boltHistory database
const {
isExporting,
isImporting,
isResetting,
isDownloadingTemplate,
handleExportSettings,
handleExportSelectedSettings,
handleExportAllChats,
handleExportSelectedChats,
handleImportSettings,
handleImportChats,
handleResetSettings,
handleResetChats,
handleDownloadTemplate,
handleImportAPIKeys,
} = useDataOperations({
customDb: db || undefined, // Pass the boltHistory database, converting null to undefined
onReloadSettings: () => window.location.reload(),
onReloadChats: () => {
// Reload chats after reset
if (db) {
getAllChats(db).then((chats) => {
// Cast to ExtendedChat to handle additional properties
const extendedChats = chats as ExtendedChat[];
setAvailableChats(extendedChats);
setChatItems(extendedChats.map((chat) => createChatItem(chat)));
});
}
},
onResetSettings: () => setShowResetInlineConfirm(false),
onResetChats: () => setShowDeleteInlineConfirm(false),
});
// Loading states for operations not provided by the hook
const [isDeleting, setIsDeleting] = useState(false);
const [isImportingKeys, setIsImportingKeys] = useState(false);
// Load available chats
useEffect(() => {
if (db) {
console.log('Loading chats from boltHistory database', {
name: db.name,
version: db.version,
objectStoreNames: Array.from(db.objectStoreNames),
});
getAllChats(db)
.then((chats) => {
console.log('Found chats:', chats.length);
// Cast to ExtendedChat to handle additional properties
const extendedChats = chats as ExtendedChat[];
setAvailableChats(extendedChats);
// Create ChatItems for selection dialog
setChatItems(extendedChats.map((chat) => createChatItem(chat)));
})
.catch((error) => {
console.error('Error loading chats:', error);
toast.error('Failed to load chats: ' + (error instanceof Error ? error.message : 'Unknown error'));
});
}
}, [db]);
// Handle file input changes
const handleFileInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
handleImportSettings(file);
}
},
[handleImportSettings],
);
const handleAPIKeyFileInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
setIsImportingKeys(true);
handleImportAPIKeys(file).finally(() => setIsImportingKeys(false));
}
},
[handleImportAPIKeys],
);
const handleChatFileInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
handleImportChats(file);
}
},
[handleImportChats],
);
// Wrapper for reset chats to handle loading state
const handleResetChatsWithState = useCallback(() => {
setIsDeleting(true);
handleResetChats().finally(() => setIsDeleting(false));
}, [handleResetChats]);
return (
<div className="space-y-12">
{/* Hidden file inputs */}
<input ref={fileInputRef} type="file" accept=".json" onChange={handleFileInputChange} className="hidden" />
<input
ref={apiKeyFileInputRef}
type="file"
accept=".json"
onChange={handleAPIKeyFileInputChange}
className="hidden"
/>
<input
ref={chatFileInputRef}
type="file"
accept=".json"
onChange={handleChatFileInputChange}
className="hidden"
/>
{/* Reset Settings Confirmation Dialog */}
<ConfirmationDialog
isOpen={showResetInlineConfirm}
onClose={() => setShowResetInlineConfirm(false)}
title="Reset All Settings?"
description="This will reset all your settings to their default values. This action cannot be undone."
confirmLabel="Reset Settings"
cancelLabel="Cancel"
variant="destructive"
isLoading={isResetting}
onConfirm={handleResetSettings}
/>
{/* Delete Chats Confirmation Dialog */}
<ConfirmationDialog
isOpen={showDeleteInlineConfirm}
onClose={() => setShowDeleteInlineConfirm(false)}
title="Delete All Chats?"
description="This will permanently delete all your chat history. This action cannot be undone."
confirmLabel="Delete All"
cancelLabel="Cancel"
variant="destructive"
isLoading={isDeleting}
onConfirm={handleResetChatsWithState}
/>
{/* Settings Selection Dialog */}
<SelectionDialog
isOpen={showSettingsSelection}
onClose={() => setShowSettingsSelection(false)}
title="Select Settings to Export"
items={settingsCategories}
onConfirm={(selectedIds) => {
handleExportSelectedSettings(selectedIds);
setShowSettingsSelection(false);
}}
confirmLabel="Export Selected"
/>
{/* Chats Selection Dialog */}
<SelectionDialog
isOpen={showChatsSelection}
onClose={() => setShowChatsSelection(false)}
title="Select Chats to Export"
items={chatItems}
onConfirm={(selectedIds) => {
handleExportSelectedChats(selectedIds);
setShowChatsSelection(false);
}}
confirmLabel="Export Selected"
/>
{/* Chats Section */}
<div>
<h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">Chats</h2>
{dbLoading ? (
<div className="flex items-center justify-center p-4">
<div className="i-ph-spinner-gap-bold animate-spin w-6 h-6 mr-2" />
<span>Loading chats database...</span>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph-download-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Export All Chats
</CardTitle>
</div>
<CardDescription>Export all your chats to a JSON file.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={async () => {
try {
if (!db) {
toast.error('Database not available');
return;
}
console.log('Database information:', {
name: db.name,
version: db.version,
objectStoreNames: Array.from(db.objectStoreNames),
});
if (availableChats.length === 0) {
toast.warning('No chats available to export');
return;
}
await handleExportAllChats();
} catch (error) {
console.error('Error exporting chats:', error);
toast.error(
`Failed to export chats: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
}}
disabled={isExporting || availableChats.length === 0}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isExporting || availableChats.length === 0 ? 'cursor-not-allowed' : '',
)}
>
{isExporting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Exporting...
</>
) : availableChats.length === 0 ? (
'No Chats to Export'
) : (
'Export All'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph:list-checks w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Export Selected Chats
</CardTitle>
</div>
<CardDescription>Choose specific chats to export.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={() => setShowChatsSelection(true)}
disabled={isExporting || chatItems.length === 0}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isExporting || chatItems.length === 0 ? 'cursor-not-allowed' : '',
)}
>
{isExporting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Exporting...
</>
) : (
'Select Chats'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph-upload-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Import Chats
</CardTitle>
</div>
<CardDescription>Import chats from a JSON file.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={() => chatFileInputRef.current?.click()}
disabled={isImporting}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isImporting ? 'cursor-not-allowed' : '',
)}
>
{isImporting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Importing...
</>
) : (
'Import Chats'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div
className="text-red-500 dark:text-red-400 mr-2"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<div className="i-ph-trash-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Delete All Chats
</CardTitle>
</div>
<CardDescription>Delete all your chat history.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={() => setShowDeleteInlineConfirm(true)}
disabled={isDeleting || chatItems.length === 0}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isDeleting || chatItems.length === 0 ? 'cursor-not-allowed' : '',
)}
>
{isDeleting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Deleting...
</>
) : (
'Delete All'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
</div>
)}
</div>
{/* Settings Section */}
<div>
<h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">Settings</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph-download-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Export All Settings
</CardTitle>
</div>
<CardDescription>Export all your settings to a JSON file.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={handleExportSettings}
disabled={isExporting}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isExporting ? 'cursor-not-allowed' : '',
)}
>
{isExporting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Exporting...
</>
) : (
'Export All'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph-filter-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Export Selected Settings
</CardTitle>
</div>
<CardDescription>Choose specific settings to export.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={() => setShowSettingsSelection(true)}
disabled={isExporting || settingsCategories.length === 0}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isExporting || settingsCategories.length === 0 ? 'cursor-not-allowed' : '',
)}
>
{isExporting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Exporting...
</>
) : (
'Select Settings'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph-upload-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Import Settings
</CardTitle>
</div>
<CardDescription>Import settings from a JSON file.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={() => fileInputRef.current?.click()}
disabled={isImporting}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isImporting ? 'cursor-not-allowed' : '',
)}
>
{isImporting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Importing...
</>
) : (
'Import Settings'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div
className="text-red-500 dark:text-red-400 mr-2"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<div className="i-ph-arrow-counter-clockwise-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Reset All Settings
</CardTitle>
</div>
<CardDescription>Reset all settings to their default values.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={() => setShowResetInlineConfirm(true)}
disabled={isResetting}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isResetting ? 'cursor-not-allowed' : '',
)}
>
{isResetting ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Resetting...
</>
) : (
'Reset All'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
</div>
</div>
{/* API Keys Section */}
<div>
<h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">API Keys</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph-file-text-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Download Template
</CardTitle>
</div>
<CardDescription>Download a template file for your API keys.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={handleDownloadTemplate}
disabled={isDownloadingTemplate}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isDownloadingTemplate ? 'cursor-not-allowed' : '',
)}
>
{isDownloadingTemplate ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Downloading...
</>
) : (
'Download'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
<Card>
<CardHeader>
<div className="flex items-center mb-2">
<motion.div className="text-accent-500 mr-2" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
<div className="i-ph-upload-duotone w-5 h-5" />
</motion.div>
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
Import API Keys
</CardTitle>
</div>
<CardDescription>Import API keys from a JSON file.</CardDescription>
</CardHeader>
<CardFooter>
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
<Button
onClick={() => apiKeyFileInputRef.current?.click()}
disabled={isImportingKeys}
variant="outline"
size="sm"
className={classNames(
'hover:text-bolt-elements-item-contentAccent hover:border-bolt-elements-item-backgroundAccent hover:bg-bolt-elements-item-backgroundAccent transition-colors w-full justify-center',
isImportingKeys ? 'cursor-not-allowed' : '',
)}
>
{isImportingKeys ? (
<>
<div className="i-ph-spinner-gap-bold animate-spin w-4 h-4 mr-2" />
Importing...
</>
) : (
'Import Keys'
)}
</Button>
</motion.div>
</CardFooter>
</Card>
</div>
</div>
{/* Data Visualization */}
<div>
<h2 className="text-xl font-semibold mb-4 text-bolt-elements-textPrimary">Data Usage</h2>
<Card>
<CardContent className="p-5">
<DataVisualization chats={availableChats} />
</CardContent>
</Card>
</div>
</div>
);
}