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 Chats

{chats.length}

Total Messages

{Object.values(messagesByRole).reduce((sum, count) => sum + count, 0)}

Avg. Messages/Chat

{averageMessagesPerChat.toFixed(1)}

Chat History

Message Distribution

{apiKeyUsage.length > 0 && (

API Usage by Provider

)}
); }