mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
- Move stores/utils/types to their relative directories (i.e chat stores in chat directory) - Move utility files to shared/utils - Move component files to shared/components - Move type definitions to shared/types - Move stores to shared/stores - Update import paths across the project
385 lines
11 KiB
TypeScript
385 lines
11 KiB
TypeScript
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 '~/shared/lib/persistence/chats';
|
|
import { classNames } from '~/shared/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<Record<string, number>>({});
|
|
const [messagesByRole, setMessagesByRole] = useState<Record<string, number>>({});
|
|
const [apiKeyUsage, setApiKeyUsage] = useState<Array<{ provider: string; count: number }>>([]);
|
|
const [averageMessagesPerChat, setAverageMessagesPerChat] = useState<number>(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<string, number> = {};
|
|
const roleCounts: Record<string, number> = {};
|
|
const apiUsage: Record<string, number> = {};
|
|
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<string, number> = {};
|
|
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 (
|
|
<div className="text-center py-8">
|
|
<div className="i-ph-chart-line-duotone w-12 h-12 mx-auto mb-4 text-bolt-elements-textTertiary opacity-80" />
|
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-2">No Data Available</h3>
|
|
<p className="text-bolt-elements-textSecondary">
|
|
Start creating chats to see your usage statistics and data visualization.
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<div className="space-y-8">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className={cardClasses}>
|
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Total Chats</h3>
|
|
<div className={statClasses}>
|
|
<div className="i-ph-chats-duotone w-8 h-8 text-indigo-500 dark:text-indigo-400" />
|
|
<span>{chats.length}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={cardClasses}>
|
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Total Messages</h3>
|
|
<div className={statClasses}>
|
|
<div className="i-ph-chat-text-duotone w-8 h-8 text-pink-500 dark:text-pink-400" />
|
|
<span>{Object.values(messagesByRole).reduce((sum, count) => sum + count, 0)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={cardClasses}>
|
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Avg. Messages/Chat</h3>
|
|
<div className={statClasses}>
|
|
<div className="i-ph-chart-bar-duotone w-8 h-8 text-green-500 dark:text-green-400" />
|
|
<span>{averageMessagesPerChat.toFixed(1)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className={cardClasses}>
|
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-6">Chat History</h3>
|
|
<div className="h-64">
|
|
<Bar data={chartData.history} options={chartOptions} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className={cardClasses}>
|
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-6">Message Distribution</h3>
|
|
<div className="h-64">
|
|
<Pie data={chartData.roles} options={pieOptions} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{apiKeyUsage.length > 0 && (
|
|
<div className={cardClasses}>
|
|
<h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-6">API Usage by Provider</h3>
|
|
<div className="h-64">
|
|
<Pie data={chartData.apiUsage} options={pieOptions} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|