mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-05-02 11:21:20 +00:00
* Update DataTab.tsx ## API Key Import Fix We identified and fixed an issue with the API key import functionality in the DataTab component. The problem was that API keys were being stored in localStorage instead of cookies, and the key format was being incorrectly processed. ### Changes Made: 1. **Updated `handleImportAPIKeys` function**: - Changed to store API keys in cookies instead of localStorage - Modified to use provider names directly as keys (e.g., "OpenAI", "Google") - Added logic to skip comment fields (keys starting with "_") - Added page reload after successful import to apply changes immediately 2. **Updated `handleDownloadTemplate` function**: - Changed template format to use provider names as keys - Added explanatory comment in the template - Removed URL-related keys that weren't being used properly 3. **Fixed template format**: - Template now uses the correct format with provider names as keys - Added support for all available providers including Hyperbolic These changes ensure that when users download the template, fill it with their API keys, and import it back, the keys are properly stored in cookies with the correct format that the application expects. * backwards compatible old import template * Update the export / import settings Settings Export/Import Improvements We've completely redesigned the settings export and import functionality to ensure all application settings are properly backed up and restored: Key Improvements Comprehensive Export Format: Now captures ALL settings from both localStorage and cookies, organized into logical categories (core, providers, features, UI, connections, debug, updates) Robust Import System: Automatically detects format version and handles both new and legacy formats with detailed error handling Complete Settings Coverage: Properly exports and imports settings from ALL tabs including: Local provider configurations (Ollama, LMStudio, etc.) Cloud provider API keys (OpenAI, Anthropic, etc.) Feature toggles and preferences UI configurations and tab settings Connection settings (GitHub, Netlify) Debug configurations and logs Technical Details Added version tracking to export files for better compatibility Implemented fallback mechanisms if primary import methods fail Added detailed logging for troubleshooting import/export issues Created helper functions for safer data handling Maintained backward compatibility with older export formats Feature Settings: Feature flags and viewed features Developer mode settings Energy saver mode configurations User Preferences: User profile information Theme settings Tab configurations Connection Settings: Netlify connections Git authentication credentials Any other service connections Debug and System Settings: Debug flags and acknowledged issues Error logs and event logs Update settings and preferences * Update DataTab.tsx * Update GithubConnection.tsx revert the code back as asked * feat: enhance style to match the project * feat:small improvements * feat: add major improvements * Update Dialog.tsx * Delete DataTab.tsx.bak * feat: small updates * Update DataVisualization.tsx * feat: dark mode fix
779 lines
31 KiB
TypeScript
779 lines
31 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,
|
|
handleExportAPIKeys,
|
|
handleUndo,
|
|
lastOperation,
|
|
} = 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-filter-duotone 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-download-duotone w-5 h-5" />
|
|
</motion.div>
|
|
<CardTitle className="text-lg group-hover:text-bolt-elements-item-contentAccent transition-colors">
|
|
Export API Keys
|
|
</CardTitle>
|
|
</div>
|
|
<CardDescription>Export your API keys to a JSON file.</CardDescription>
|
|
</CardHeader>
|
|
<CardFooter>
|
|
<motion.div whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} className="w-full">
|
|
<Button
|
|
onClick={handleExportAPIKeys}
|
|
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 Keys'
|
|
)}
|
|
</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-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>
|
|
|
|
{/* Undo Last Operation */}
|
|
{lastOperation && (
|
|
<div className="fixed bottom-4 right-4 bg-bolt-elements-bg-depth-3 text-bolt-elements-textPrimary p-4 rounded-lg shadow-lg flex items-center gap-3 z-50">
|
|
<div className="text-sm">
|
|
<span className="font-medium">Last action:</span> {lastOperation.type}
|
|
</div>
|
|
<Button
|
|
onClick={handleUndo}
|
|
variant="outline"
|
|
size="sm"
|
|
className="border-bolt-elements-borderColor text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundAccent hover:text-bolt-elements-item-contentAccent"
|
|
>
|
|
Undo
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|