+
Settings
+
+
+
+
+
+
+
+
+ Export All Settings
+
+
+ Export all your settings to a JSON file.
+
+
+
+
+
+
+
- {/* Settings Backup Section */}
-
-
-
- Export your settings to a JSON file or import settings from a previously exported file.
-
-
-
-
- Export Settings
-
-
fileInputRef.current?.click()}
- >
-
- Import Settings
-
-
setShowResetInlineConfirm(true)}
- >
-
- Reset Settings
-
-
-
+
+
+
+
+
+
+
+ Export Selected Settings
+
+
+ Choose specific settings to export.
+
+
+
+
+
+
+
- {/* API Keys Management Section */}
-
-
-
-
API Keys Management
+
+
+
+
+
+
+
+ Import Settings
+
+
+ Import settings from a JSON file.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reset All Settings
+
+
+ Reset all settings to their default values.
+
+
+
+
+
+
+
-
- Import API keys from a JSON file or download a template to fill in your keys.
-
-
-
-
- {isDownloadingTemplate ? (
-
- ) : (
-
- )}
- Download Template
-
-
apiKeyFileInputRef.current?.click()}
- disabled={isImportingKeys}
- >
- {isImportingKeys ? (
-
- ) : (
-
- )}
- Import API Keys
-
+
+
+ {/* API Keys Section */}
+
+
API Keys
+
+
+
+
+
+
+
+
+ Export API Keys
+
+
+ Export your API keys to a JSON file.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Download Template
+
+
+ Download a template file for your API keys.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import API Keys
+
+
+ Import API keys from a JSON file.
+
+
+
+
+
+
+
-
+
+
+ {/* Data Visualization */}
+
+
Data Usage
+
+
+
+
+
+
+
+ {/* Undo Last Operation */}
+ {lastOperation && (
+
+
+ Last action: {lastOperation.type}
+
+
+
+ )}
);
}
diff --git a/app/components/@settings/tabs/data/DataVisualization.tsx b/app/components/@settings/tabs/data/DataVisualization.tsx
new file mode 100644
index 00000000..27d27388
--- /dev/null
+++ b/app/components/@settings/tabs/data/DataVisualization.tsx
@@ -0,0 +1,384 @@
+import { useState, useEffect } from 'react';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+ ArcElement,
+ PointElement,
+ LineElement,
+} from 'chart.js';
+import { Bar, Pie } from 'react-chartjs-2';
+import type { Chat } from '~/lib/persistence/chats';
+import { classNames } from '~/utils/classNames';
+
+// Register ChartJS components
+ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, ArcElement, PointElement, LineElement);
+
+type DataVisualizationProps = {
+ chats: Chat[];
+};
+
+export function DataVisualization({ chats }: DataVisualizationProps) {
+ const [chatsByDate, setChatsByDate] = useState
>({});
+ const [messagesByRole, setMessagesByRole] = useState>({});
+ const [apiKeyUsage, setApiKeyUsage] = useState>([]);
+ const [averageMessagesPerChat, setAverageMessagesPerChat] = useState(0);
+ const [isDarkMode, setIsDarkMode] = useState(false);
+
+ useEffect(() => {
+ const isDark = document.documentElement.classList.contains('dark');
+ setIsDarkMode(isDark);
+
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if (mutation.attributeName === 'class') {
+ setIsDarkMode(document.documentElement.classList.contains('dark'));
+ }
+ });
+ });
+
+ observer.observe(document.documentElement, { attributes: true });
+
+ return () => observer.disconnect();
+ }, []);
+
+ useEffect(() => {
+ if (!chats || chats.length === 0) {
+ return;
+ }
+
+ // Process chat data
+ const chatDates: Record = {};
+ const roleCounts: Record = {};
+ const apiUsage: Record = {};
+ let totalMessages = 0;
+
+ chats.forEach((chat) => {
+ const date = new Date(chat.timestamp).toLocaleDateString();
+ chatDates[date] = (chatDates[date] || 0) + 1;
+
+ chat.messages.forEach((message) => {
+ roleCounts[message.role] = (roleCounts[message.role] || 0) + 1;
+ totalMessages++;
+
+ if (message.role === 'assistant') {
+ const providerMatch = message.content.match(/provider:\s*([\w-]+)/i);
+ const provider = providerMatch ? providerMatch[1] : 'unknown';
+ apiUsage[provider] = (apiUsage[provider] || 0) + 1;
+ }
+ });
+ });
+
+ const sortedDates = Object.keys(chatDates).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
+ const sortedChatsByDate: Record = {};
+ sortedDates.forEach((date) => {
+ sortedChatsByDate[date] = chatDates[date];
+ });
+
+ setChatsByDate(sortedChatsByDate);
+ setMessagesByRole(roleCounts);
+ setApiKeyUsage(Object.entries(apiUsage).map(([provider, count]) => ({ provider, count })));
+ setAverageMessagesPerChat(totalMessages / chats.length);
+ }, [chats]);
+
+ // Get theme colors from CSS variables to ensure theme consistency
+ const getThemeColor = (varName: string): string => {
+ // Get the CSS variable value from document root
+ if (typeof document !== 'undefined') {
+ return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
+ }
+
+ // Fallback for SSR
+ return isDarkMode ? '#FFFFFF' : '#000000';
+ };
+
+ // Theme-aware chart colors with enhanced dark mode visibility using CSS variables
+ const chartColors = {
+ grid: isDarkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.1)',
+ text: getThemeColor('--bolt-elements-textPrimary'),
+ textSecondary: getThemeColor('--bolt-elements-textSecondary'),
+ background: getThemeColor('--bolt-elements-bg-depth-1'),
+ accent: getThemeColor('--bolt-elements-button-primary-text'),
+ border: getThemeColor('--bolt-elements-borderColor'),
+ };
+
+ const getChartColors = (index: number) => {
+ // Define color palettes based on Bolt design tokens
+ const baseColors = [
+ // Indigo
+ {
+ base: getThemeColor('--bolt-elements-button-primary-text'),
+ },
+
+ // Pink
+ {
+ base: isDarkMode ? 'rgb(244, 114, 182)' : 'rgb(236, 72, 153)',
+ },
+
+ // Green
+ {
+ base: getThemeColor('--bolt-elements-icon-success'),
+ },
+
+ // Yellow
+ {
+ base: isDarkMode ? 'rgb(250, 204, 21)' : 'rgb(234, 179, 8)',
+ },
+
+ // Blue
+ {
+ base: isDarkMode ? 'rgb(56, 189, 248)' : 'rgb(14, 165, 233)',
+ },
+ ];
+
+ // Get the base color for this index
+ const color = baseColors[index % baseColors.length].base;
+
+ // Parse color and generate variations with appropriate opacity
+ let r = 0,
+ g = 0,
+ b = 0;
+
+ // Handle rgb/rgba format
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+ const rgbaMatch = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([0-9.]+)\)/);
+
+ if (rgbMatch) {
+ [, r, g, b] = rgbMatch.map(Number);
+ } else if (rgbaMatch) {
+ [, r, g, b] = rgbaMatch.map(Number);
+ } else if (color.startsWith('#')) {
+ // Handle hex format
+ const hex = color.slice(1);
+ const bigint = parseInt(hex, 16);
+ r = (bigint >> 16) & 255;
+ g = (bigint >> 8) & 255;
+ b = bigint & 255;
+ }
+
+ return {
+ bg: `rgba(${r}, ${g}, ${b}, ${isDarkMode ? 0.7 : 0.5})`,
+ border: `rgba(${r}, ${g}, ${b}, ${isDarkMode ? 0.9 : 0.8})`,
+ };
+ };
+
+ const chartData = {
+ history: {
+ labels: Object.keys(chatsByDate),
+ datasets: [
+ {
+ label: 'Chats Created',
+ data: Object.values(chatsByDate),
+ backgroundColor: getChartColors(0).bg,
+ borderColor: getChartColors(0).border,
+ borderWidth: 1,
+ },
+ ],
+ },
+ roles: {
+ labels: Object.keys(messagesByRole),
+ datasets: [
+ {
+ label: 'Messages by Role',
+ data: Object.values(messagesByRole),
+ backgroundColor: Object.keys(messagesByRole).map((_, i) => getChartColors(i).bg),
+ borderColor: Object.keys(messagesByRole).map((_, i) => getChartColors(i).border),
+ borderWidth: 1,
+ },
+ ],
+ },
+ apiUsage: {
+ labels: apiKeyUsage.map((item) => item.provider),
+ datasets: [
+ {
+ label: 'API Usage',
+ data: apiKeyUsage.map((item) => item.count),
+ backgroundColor: apiKeyUsage.map((_, i) => getChartColors(i).bg),
+ borderColor: apiKeyUsage.map((_, i) => getChartColors(i).border),
+ borderWidth: 1,
+ },
+ ],
+ },
+ };
+
+ const baseChartOptions = {
+ responsive: true,
+ maintainAspectRatio: false,
+ color: chartColors.text,
+ plugins: {
+ legend: {
+ position: 'top' as const,
+ labels: {
+ color: chartColors.text,
+ font: {
+ weight: 'bold' as const,
+ size: 12,
+ },
+ padding: 16,
+ usePointStyle: true,
+ },
+ },
+ title: {
+ display: true,
+ color: chartColors.text,
+ font: {
+ size: 16,
+ weight: 'bold' as const,
+ },
+ padding: 16,
+ },
+ tooltip: {
+ titleColor: chartColors.text,
+ bodyColor: chartColors.text,
+ backgroundColor: isDarkMode
+ ? 'rgba(23, 23, 23, 0.8)' // Dark bg using Tailwind gray-900
+ : 'rgba(255, 255, 255, 0.8)', // Light bg
+ borderColor: chartColors.border,
+ borderWidth: 1,
+ },
+ },
+ };
+
+ const chartOptions = {
+ ...baseChartOptions,
+ plugins: {
+ ...baseChartOptions.plugins,
+ title: {
+ ...baseChartOptions.plugins.title,
+ text: 'Chat History',
+ },
+ },
+ scales: {
+ x: {
+ grid: {
+ color: chartColors.grid,
+ drawBorder: false,
+ },
+ border: {
+ display: false,
+ },
+ ticks: {
+ color: chartColors.text,
+ font: {
+ weight: 500,
+ },
+ },
+ },
+ y: {
+ grid: {
+ color: chartColors.grid,
+ drawBorder: false,
+ },
+ border: {
+ display: false,
+ },
+ ticks: {
+ color: chartColors.text,
+ font: {
+ weight: 500,
+ },
+ },
+ },
+ },
+ };
+
+ const pieOptions = {
+ ...baseChartOptions,
+ plugins: {
+ ...baseChartOptions.plugins,
+ title: {
+ ...baseChartOptions.plugins.title,
+ text: 'Message Distribution',
+ },
+ legend: {
+ ...baseChartOptions.plugins.legend,
+ position: 'right' as const,
+ },
+ datalabels: {
+ color: chartColors.text,
+ font: {
+ weight: 'bold' as const,
+ },
+ },
+ },
+ };
+
+ if (chats.length === 0) {
+ return (
+
+
+
No Data Available
+
+ Start creating chats to see your usage statistics and data visualization.
+
+
+ );
+ }
+
+ const cardClasses = classNames(
+ 'p-6 rounded-lg shadow-sm',
+ 'bg-bolt-elements-bg-depth-1',
+ 'border border-bolt-elements-borderColor',
+ );
+
+ const statClasses = classNames('text-3xl font-bold text-bolt-elements-textPrimary', 'flex items-center gap-3');
+
+ return (
+
+
+
+
+
+
Total Messages
+
+
+
{Object.values(messagesByRole).reduce((sum, count) => sum + count, 0)}
+
+
+
+
+
Avg. Messages/Chat
+
+
+
{averageMessagesPerChat.toFixed(1)}
+
+
+
+
+
+
+
+
+
Message Distribution
+
+
+
+
+ {apiKeyUsage.length > 0 && (
+
+
API Usage by Provider
+
+
+ )}
+
+ );
+}
diff --git a/app/components/@settings/tabs/debug/DebugTab.tsx b/app/components/@settings/tabs/debug/DebugTab.tsx
index 25a86623..652c7f5a 100644
--- a/app/components/@settings/tabs/debug/DebugTab.tsx
+++ b/app/components/@settings/tabs/debug/DebugTab.tsx
@@ -342,24 +342,86 @@ export default function DebugTab() {
try {
setLoading((prev) => ({ ...prev, systemInfo: true }));
- // Get browser info
- const ua = navigator.userAgent;
- const browserName = ua.includes('Firefox')
- ? 'Firefox'
- : ua.includes('Chrome')
- ? 'Chrome'
- : ua.includes('Safari')
- ? 'Safari'
- : ua.includes('Edge')
- ? 'Edge'
- : 'Unknown';
- const browserVersion = ua.match(/(Firefox|Chrome|Safari|Edge)\/([0-9.]+)/)?.[2] || 'Unknown';
+ // Get better OS detection
+ const userAgent = navigator.userAgent;
+ let detectedOS = 'Unknown';
+ let detectedArch = 'unknown';
+
+ // Improved OS detection
+ if (userAgent.indexOf('Win') !== -1) {
+ detectedOS = 'Windows';
+ } else if (userAgent.indexOf('Mac') !== -1) {
+ detectedOS = 'macOS';
+ } else if (userAgent.indexOf('Linux') !== -1) {
+ detectedOS = 'Linux';
+ } else if (userAgent.indexOf('Android') !== -1) {
+ detectedOS = 'Android';
+ } else if (/iPhone|iPad|iPod/.test(userAgent)) {
+ detectedOS = 'iOS';
+ }
+
+ // Better architecture detection
+ if (userAgent.indexOf('x86_64') !== -1 || userAgent.indexOf('x64') !== -1 || userAgent.indexOf('WOW64') !== -1) {
+ detectedArch = 'x64';
+ } else if (userAgent.indexOf('x86') !== -1 || userAgent.indexOf('i686') !== -1) {
+ detectedArch = 'x86';
+ } else if (userAgent.indexOf('arm64') !== -1 || userAgent.indexOf('aarch64') !== -1) {
+ detectedArch = 'arm64';
+ } else if (userAgent.indexOf('arm') !== -1) {
+ detectedArch = 'arm';
+ }
+
+ // Get browser info with improved detection
+ const browserName = (() => {
+ if (userAgent.indexOf('Edge') !== -1 || userAgent.indexOf('Edg/') !== -1) {
+ return 'Edge';
+ }
+
+ if (userAgent.indexOf('Chrome') !== -1) {
+ return 'Chrome';
+ }
+
+ if (userAgent.indexOf('Firefox') !== -1) {
+ return 'Firefox';
+ }
+
+ if (userAgent.indexOf('Safari') !== -1) {
+ return 'Safari';
+ }
+
+ return 'Unknown';
+ })();
+
+ const browserVersionMatch = userAgent.match(/(Edge|Edg|Chrome|Firefox|Safari)[\s/](\d+(\.\d+)*)/);
+ const browserVersion = browserVersionMatch ? browserVersionMatch[2] : 'Unknown';
// Get performance metrics
const memory = (performance as any).memory || {};
const timing = performance.timing;
const navigation = performance.navigation;
- const connection = (navigator as any).connection;
+ const connection = (navigator as any).connection || {};
+
+ // Try to use Navigation Timing API Level 2 when available
+ let loadTime = 0;
+ let domReadyTime = 0;
+
+ try {
+ const navEntries = performance.getEntriesByType('navigation');
+
+ if (navEntries.length > 0) {
+ const navTiming = navEntries[0] as PerformanceNavigationTiming;
+ loadTime = navTiming.loadEventEnd - navTiming.startTime;
+ domReadyTime = navTiming.domContentLoadedEventEnd - navTiming.startTime;
+ } else {
+ // Fall back to older API
+ loadTime = timing.loadEventEnd - timing.navigationStart;
+ domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;
+ }
+ } catch {
+ // Fall back to older API if Navigation Timing API Level 2 is not available
+ loadTime = timing.loadEventEnd - timing.navigationStart;
+ domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;
+ }
// Get battery info
let batteryInfo;
@@ -405,9 +467,9 @@ export default function DebugTab() {
const memoryPercentage = totalMemory ? (usedMemory / totalMemory) * 100 : 0;
const systemInfo: SystemInfo = {
- os: navigator.platform,
- arch: navigator.userAgent.includes('x64') ? 'x64' : navigator.userAgent.includes('arm') ? 'arm' : 'unknown',
- platform: navigator.platform,
+ os: detectedOS,
+ arch: detectedArch,
+ platform: navigator.platform || 'unknown',
cpus: navigator.hardwareConcurrency + ' cores',
memory: {
total: formatBytes(totalMemory),
@@ -423,7 +485,7 @@ export default function DebugTab() {
userAgent: navigator.userAgent,
cookiesEnabled: navigator.cookieEnabled,
online: navigator.onLine,
- platform: navigator.platform,
+ platform: navigator.platform || 'unknown',
cores: navigator.hardwareConcurrency,
},
screen: {
@@ -445,8 +507,8 @@ export default function DebugTab() {
usagePercentage: memory.totalJSHeapSize ? (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100 : 0,
},
timing: {
- loadTime: timing.loadEventEnd - timing.navigationStart,
- domReadyTime: timing.domContentLoadedEventEnd - timing.navigationStart,
+ loadTime,
+ domReadyTime,
readyStart: timing.fetchStart - timing.navigationStart,
redirectTime: timing.redirectEnd - timing.redirectStart,
appcacheTime: timing.domainLookupStart - timing.fetchStart,
@@ -483,6 +545,23 @@ export default function DebugTab() {
}
};
+ // Helper function to format bytes to human readable format with better precision
+ const formatBytes = (bytes: number) => {
+ if (bytes === 0) {
+ return '0 B';
+ }
+
+ const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+
+ // Return with proper precision based on unit size
+ if (i === 0) {
+ return `${bytes} ${units[i]}`;
+ }
+
+ return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
+ };
+
const getWebAppInfo = async () => {
try {
setLoading((prev) => ({ ...prev, webAppInfo: true }));
@@ -520,20 +599,6 @@ export default function DebugTab() {
}
};
- // Helper function to format bytes to human readable format
- const formatBytes = (bytes: number) => {
- const units = ['B', 'KB', 'MB', 'GB'];
- let size = bytes;
- let unitIndex = 0;
-
- while (size >= 1024 && unitIndex < units.length - 1) {
- size /= 1024;
- unitIndex++;
- }
-
- return `${Math.round(size)} ${units[unitIndex]}`;
- };
-
const handleLogPerformance = () => {
try {
setLoading((prev) => ({ ...prev, performance: true }));
@@ -1353,9 +1418,7 @@ export default function DebugTab() {