"use client"; import type { Chart as ChartType } from "chart.js"; import dynamic from "next/dynamic"; import { useEffect, useRef } from "react"; import { getLocalizedLanguageName } from "../lib/localization"; interface SessionsData { [date: string]: number; } interface CategoriesData { [category: string]: number; } interface LanguageData { [language: string]: number; } interface SessionsLineChartProps { sessionsPerDay: SessionsData; } interface CategoriesBarChartProps { categories: CategoriesData; } interface LanguagePieChartProps { languages: LanguageData; } interface SentimentChartProps { sentimentData: { positive: number; neutral: number; negative: number; }; } interface TokenUsageChartProps { tokenData: { labels: string[]; values: number[]; costs: number[]; }; } // Loading skeleton for charts function ChartSkeleton({ height = 180 }: { height?: number }) { return (
); } function DonutChartSkeleton() { return (
{[...Array(3)].map((_, i) => (
))}
); } // Inner components that do the actual rendering function SessionsLineChartInner({ sessionsPerDay }: SessionsLineChartProps) { const ref = useRef(null); const chartRef = useRef(null); useEffect(() => { let isMounted = true; async function initChart() { if (!ref.current || !sessionsPerDay) return; const ctx = ref.current.getContext("2d"); if (!ctx) return; const { default: Chart } = await import("chart.js/auto"); if (!isMounted) return; if (chartRef.current) chartRef.current.destroy(); chartRef.current = new Chart(ctx, { type: "line", data: { labels: Object.keys(sessionsPerDay), datasets: [ { label: "Sessions", data: Object.values(sessionsPerDay), borderColor: "rgb(59, 130, 246)", backgroundColor: "rgba(59, 130, 246, 0.1)", borderWidth: 2, tension: 0.3, fill: true, }, ], }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } }, }, }); } initChart(); return () => { isMounted = false; if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } }; }, [sessionsPerDay]); return ; } function CategoriesBarChartInner({ categories }: CategoriesBarChartProps) { const ref = useRef(null); const chartRef = useRef(null); useEffect(() => { let isMounted = true; async function initChart() { if (!ref.current || !categories) return; const ctx = ref.current.getContext("2d"); if (!ctx) return; const { default: Chart } = await import("chart.js/auto"); if (!isMounted) return; if (chartRef.current) chartRef.current.destroy(); chartRef.current = new Chart(ctx, { type: "bar", data: { labels: Object.keys(categories), datasets: [ { label: "Categories", data: Object.values(categories), backgroundColor: "rgba(59, 130, 246, 0.7)", borderWidth: 1, }, ], }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } }, }, }); } initChart(); return () => { isMounted = false; if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } }; }, [categories]); return ; } function SentimentChartInner({ sentimentData }: SentimentChartProps) { const ref = useRef(null); const chartRef = useRef(null); useEffect(() => { let isMounted = true; async function initChart() { if (!ref.current || !sentimentData) return; const ctx = ref.current.getContext("2d"); if (!ctx) return; const { default: Chart } = await import("chart.js/auto"); if (!isMounted) return; if (chartRef.current) chartRef.current.destroy(); chartRef.current = new Chart(ctx, { type: "doughnut", data: { labels: ["Positive", "Neutral", "Negative"], datasets: [ { data: [ sentimentData.positive, sentimentData.neutral, sentimentData.negative, ], backgroundColor: [ "rgba(34, 197, 94, 0.8)", "rgba(249, 115, 22, 0.8)", "rgba(239, 68, 68, 0.8)", ], borderWidth: 1, }, ], }, options: { responsive: true, plugins: { legend: { position: "right", labels: { usePointStyle: true, padding: 20, }, }, }, cutout: "65%", }, }); } initChart(); return () => { isMounted = false; if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } }; }, [sentimentData]); return ; } function LanguagePieChartInner({ languages }: LanguagePieChartProps) { const ref = useRef(null); const chartRef = useRef(null); useEffect(() => { let isMounted = true; async function initChart() { if (!ref.current || !languages) return; const ctx = ref.current.getContext("2d"); if (!ctx) return; const { default: Chart } = await import("chart.js/auto"); if (!isMounted) return; // Get top 5 languages, combine others const entries = Object.entries(languages); const topLanguages = entries.sort((a, b) => b[1] - a[1]).slice(0, 5); const otherCount = entries .slice(5) .reduce((sum, [, count]) => sum + count, 0); if (otherCount > 0) { topLanguages.push(["Other", otherCount]); } const isoCodes = topLanguages.map(([lang]) => lang); const labels = topLanguages.map(([lang]) => { if (lang === "Other") return "Other"; return getLocalizedLanguageName(lang, "en"); }); const data = topLanguages.map(([, count]) => count); if (chartRef.current) chartRef.current.destroy(); chartRef.current = new Chart(ctx, { type: "pie", data: { labels, datasets: [ { data, backgroundColor: [ "rgba(59, 130, 246, 0.8)", "rgba(16, 185, 129, 0.8)", "rgba(249, 115, 22, 0.8)", "rgba(236, 72, 153, 0.8)", "rgba(139, 92, 246, 0.8)", "rgba(107, 114, 128, 0.8)", ], borderWidth: 1, }, ], }, options: { responsive: true, plugins: { legend: { position: "right", labels: { usePointStyle: true, padding: 20, }, }, tooltip: { callbacks: { label: (context) => { const label = context.label || ""; const value = context.formattedValue || ""; const index = context.dataIndex; const originalIsoCode = isoCodes[index]; if ( originalIsoCode && originalIsoCode !== "Other" && /^[a-z]{2}$/.test(originalIsoCode.toLowerCase()) ) { return `${label} (${originalIsoCode.toUpperCase()}): ${value}`; } return `${label}: ${value}`; }, }, }, }, }, }); } initChart(); return () => { isMounted = false; if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } }; }, [languages]); return ; } function TokenUsageChartInner({ tokenData }: TokenUsageChartProps) { const ref = useRef(null); const chartRef = useRef(null); useEffect(() => { let isMounted = true; async function initChart() { if (!ref.current || !tokenData) return; const ctx = ref.current.getContext("2d"); if (!ctx) return; const { default: Chart } = await import("chart.js/auto"); if (!isMounted) return; if (chartRef.current) chartRef.current.destroy(); chartRef.current = new Chart(ctx, { type: "bar", data: { labels: tokenData.labels, datasets: [ { label: "Tokens", data: tokenData.values, backgroundColor: "rgba(59, 130, 246, 0.7)", borderWidth: 1, yAxisID: "y", }, { label: "Cost (EUR)", data: tokenData.costs, backgroundColor: "rgba(16, 185, 129, 0.7)", borderWidth: 1, type: "line", yAxisID: "y1", }, ], }, options: { responsive: true, plugins: { legend: { display: true } }, scales: { y: { beginAtZero: true, position: "left", title: { display: true, text: "Token Count", }, }, y1: { beginAtZero: true, position: "right", grid: { drawOnChartArea: false, }, title: { display: true, text: "Cost (EUR)", }, }, }, }, }); } initChart(); return () => { isMounted = false; if (chartRef.current) { chartRef.current.destroy(); chartRef.current = null; } }; }, [tokenData]); return ; } // Dynamic exports with SSR disabled export const SessionsLineChart = dynamic( () => Promise.resolve(SessionsLineChartInner), { ssr: false, loading: () => , } ); export const CategoriesBarChart = dynamic( () => Promise.resolve(CategoriesBarChartInner), { ssr: false, loading: () => , } ); export const SentimentChart = dynamic( () => Promise.resolve(SentimentChartInner), { ssr: false, loading: () => , } ); export const LanguagePieChart = dynamic( () => Promise.resolve(LanguagePieChartInner), { ssr: false, loading: () => , } ); export const TokenUsageChart = dynamic( () => Promise.resolve(TokenUsageChartInner), { ssr: false, loading: () => , } );