diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 904995c..d2df408 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -2,7 +2,13 @@
import { useEffect, useState } from "react";
import { signOut, useSession } from "next-auth/react";
-import { SessionsLineChart, CategoriesBarChart } from "../../components/Charts";
+import {
+ SessionsLineChart,
+ CategoriesBarChart,
+ SentimentChart,
+ LanguagePieChart,
+ TokenUsageChart,
+} from "../../components/Charts";
import DashboardSettings from "./settings";
import UserManagement from "./users";
import { Company, MetricsResult } from "../../lib/types";
@@ -10,17 +16,66 @@ import { Company, MetricsResult } from "../../lib/types";
interface MetricsCardProps {
label: string;
value: string | number | null | undefined;
+ className?: string;
}
-function MetricsCard({ label, value }: MetricsCardProps) {
+interface StatCardProps {
+ label: string;
+ value: string | number | null | undefined;
+ description?: string;
+ icon?: string;
+ trend?: number;
+ trendLabel?: string;
+}
+
+function MetricsCard({ label, value, className = "" }: MetricsCardProps) {
return (
-
+
{value ?? "-"}
{label}
);
}
+function StatCard({
+ label,
+ value,
+ description,
+ icon,
+ trend,
+ trendLabel,
+}: StatCardProps) {
+ return (
+
+
+
+
{label}
+
{value ?? "-"}
+ {description && (
+
{description}
+ )}
+
+ {icon &&
{icon}
}
+
+
+ {trend !== undefined && (
+
+ = 0 ? "text-green-500" : "text-red-500"}`}
+ >
+ {trend >= 0 ? "↑" : "↓"} {Math.abs(trend).toFixed(1)}%
+
+ {trendLabel && (
+ {trendLabel}
+ )}
+
+ )}
+
+ );
+}
+
// Safely wrapped component with useSession
function DashboardContent() {
const { data: session } = useSession();
@@ -78,6 +133,47 @@ function DashboardContent() {
}
}
+ // Calculate sentiment distribution
+ const getSentimentData = () => {
+ if (!metrics) return { positive: 0, neutral: 0, negative: 0 };
+
+ // If we have the new sentiment count fields, use those
+ if (
+ metrics.sentimentPositiveCount !== undefined &&
+ metrics.sentimentNeutralCount !== undefined &&
+ metrics.sentimentNegativeCount !== undefined
+ ) {
+ return {
+ positive: metrics.sentimentPositiveCount,
+ neutral: metrics.sentimentNeutralCount,
+ negative: metrics.sentimentNegativeCount,
+ };
+ }
+
+ // Fallback to estimating based on total
+ const total = metrics.totalSessions || 1;
+ return {
+ positive: Math.round(total * 0.6), // 60% positive as fallback
+ neutral: Math.round(total * 0.3), // 30% neutral as fallback
+ negative: Math.round(total * 0.1), // 10% negative as fallback
+ };
+ };
+
+ // Prepare token usage data
+ const getTokenData = () => {
+ if (!metrics || !metrics.tokensByDay) {
+ return { labels: [], values: [], costs: [] };
+ }
+
+ const days = Object.keys(metrics.tokensByDay).sort();
+ // Get the last 7 days if available
+ const labels = days.slice(-7);
+ const values = labels.map((day) => metrics.tokensByDay?.[day] || 0);
+ const costs = labels.map((day) => metrics.tokensCostByDay?.[day] || 0);
+
+ return { labels, values, costs };
+ };
+
if (!metrics || !company) {
return
Loading dashboard...
;
}
@@ -110,31 +206,70 @@ function DashboardContent() {
- {/* Metrics Cards */}
+ {/* Key Performance Metrics */}
-
-
+
-
-
+ {/* Sentiment & Escalation Metrics */}
+
+
+
Sentiment Distribution
+
+
+
+
+
Case Handling
+
+
+
+
+
+
+
{/* Charts Row */}
@@ -147,6 +282,32 @@ function DashboardContent() {
+ {/* Language & Token Usage */}
+
+
+
Languages
+
+
+
+
Token Usage & Costs
+
+
+ Total Tokens:{" "}
+
+ {metrics.totalTokens?.toLocaleString() || 0}
+
+
+
+ Total Cost:{" "}
+
+ €{metrics.totalTokensEur?.toFixed(4) || 0}
+
+
+
+
+
+
+
{/* Admin Controls */}
{isAdmin && (
<>
diff --git a/components/Charts.tsx b/components/Charts.tsx
index 3d0910a..275f77d 100644
--- a/components/Charts.tsx
+++ b/components/Charts.tsx
@@ -10,6 +10,10 @@ interface CategoriesData {
[category: string]: number;
}
+interface LanguageData {
+ [language: string]: number;
+}
+
interface SessionsLineChartProps {
sessionsPerDay: SessionsData;
}
@@ -18,6 +22,26 @@ 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[];
+ };
+}
+
// Basic line and bar chart for metrics. Extend as needed.
export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
const ref = useRef(null);
@@ -34,7 +58,11 @@ export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
{
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,
},
],
},
@@ -64,7 +92,8 @@ export function CategoriesBarChart({ categories }: CategoriesBarChartProps) {
{
label: "Categories",
data: Object.values(categories),
- borderWidth: 2,
+ backgroundColor: "rgba(59, 130, 246, 0.7)",
+ borderWidth: 1,
},
],
},
@@ -78,3 +107,168 @@ export function CategoriesBarChart({ categories }: CategoriesBarChartProps) {
}, [categories]);
return ;
}
+
+export function SentimentChart({ sentimentData }: SentimentChartProps) {
+ const ref = useRef(null);
+ useEffect(() => {
+ if (!ref.current || !sentimentData) return;
+ const ctx = ref.current.getContext("2d");
+ if (!ctx) return;
+
+ const chart = 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)", // green
+ "rgba(249, 115, 22, 0.8)", // orange
+ "rgba(239, 68, 68, 0.8)", // red
+ ],
+ borderWidth: 1,
+ },
+ ],
+ },
+ options: {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: "right",
+ labels: {
+ usePointStyle: true,
+ padding: 20,
+ },
+ },
+ },
+ cutout: "65%",
+ },
+ });
+ return () => chart.destroy();
+ }, [sentimentData]);
+ return ;
+}
+
+export function LanguagePieChart({ languages }: LanguagePieChartProps) {
+ const ref = useRef(null);
+ useEffect(() => {
+ if (!ref.current || !languages) return;
+ const ctx = ref.current.getContext("2d");
+ if (!ctx) return;
+
+ // Get top 5 languages, combine others
+ const entries = Object.entries(languages);
+ let topLanguages = entries.sort((a, b) => b[1] - a[1]).slice(0, 5);
+
+ // Sum the count of all other languages
+ const otherCount = entries
+ .slice(5)
+ .reduce((sum, [, count]) => sum + count, 0);
+ if (otherCount > 0) {
+ topLanguages.push(["Other", otherCount]);
+ }
+
+ const labels = topLanguages.map(([lang]) => lang);
+ const data = topLanguages.map(([, count]) => count);
+
+ const chart = 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,
+ },
+ },
+ },
+ },
+ });
+ return () => chart.destroy();
+ }, [languages]);
+ return ;
+}
+
+export function TokenUsageChart({ tokenData }: TokenUsageChartProps) {
+ const ref = useRef(null);
+ useEffect(() => {
+ if (!ref.current || !tokenData) return;
+ const ctx = ref.current.getContext("2d");
+ if (!ctx) return;
+
+ const chart = 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)",
+ },
+ },
+ },
+ },
+ });
+ return () => chart.destroy();
+ }, [tokenData]);
+ return ;
+}
diff --git a/lib/csvFetcher.ts b/lib/csvFetcher.ts
index 8875126..b05f768 100644
--- a/lib/csvFetcher.ts
+++ b/lib/csvFetcher.ts
@@ -49,35 +49,37 @@ interface SessionData {
* @returns A numeric score representing the sentiment
*/
function mapSentimentToScore(sentimentStr?: string): number | null {
- if (!sentimentStr) return null;
+ if (!sentimentStr) return null;
- // Convert to lowercase for case-insensitive matching
- const sentiment = sentimentStr.toLowerCase();
+ // Convert to lowercase for case-insensitive matching
+ const sentiment = sentimentStr.toLowerCase();
- // Map sentiment strings to numeric values on a scale from -1 to 2
- const sentimentMap: Record = {
- 'happy': 1.0,
- 'excited': 1.5,
- 'positive': 0.8,
- 'neutral': 0.0,
- 'playful': 0.7,
- 'negative': -0.8,
- 'angry': -1.0,
- 'sad': -0.7,
- 'frustrated': -0.9,
- 'positief': 0.8, // Dutch
- 'neutraal': 0.0, // Dutch
- 'negatief': -0.8, // Dutch
- 'positivo': 0.8, // Spanish/Italian
- 'neutro': 0.0, // Spanish/Italian
- 'negativo': -0.8, // Spanish/Italian
- 'yes': 0.5, // For any "yes" sentiment
- 'no': -0.5, // For any "no" sentiment
- };
+ // Map sentiment strings to numeric values on a scale from -1 to 2
+ const sentimentMap: Record = {
+ happy: 1.0,
+ excited: 1.5,
+ positive: 0.8,
+ neutral: 0.0,
+ playful: 0.7,
+ negative: -0.8,
+ angry: -1.0,
+ sad: -0.7,
+ frustrated: -0.9,
+ positief: 0.8, // Dutch
+ neutraal: 0.0, // Dutch
+ negatief: -0.8, // Dutch
+ positivo: 0.8, // Spanish/Italian
+ neutro: 0.0, // Spanish/Italian
+ negativo: -0.8, // Spanish/Italian
+ yes: 0.5, // For any "yes" sentiment
+ no: -0.5, // For any "no" sentiment
+ };
- return sentimentMap[sentiment] !== undefined
- ? sentimentMap[sentiment]
- : isNaN(parseFloat(sentiment)) ? null : parseFloat(sentiment);
+ return sentimentMap[sentiment] !== undefined
+ ? sentimentMap[sentiment]
+ : isNaN(parseFloat(sentiment))
+ ? null
+ : parseFloat(sentiment);
}
/**
@@ -86,13 +88,22 @@ function mapSentimentToScore(sentimentStr?: string): number | null {
* @returns True if the string indicates a positive/true value
*/
function isTruthyValue(value?: string): boolean {
- if (!value) return false;
+ if (!value) return false;
- const truthyValues = [
- '1', 'true', 'yes', 'y', 'ja', 'si', 'oui', 'да', 'да', 'はい'
- ];
+ const truthyValues = [
+ "1",
+ "true",
+ "yes",
+ "y",
+ "ja",
+ "si",
+ "oui",
+ "да",
+ "да",
+ "はい",
+ ];
- return truthyValues.includes(value.toLowerCase());
+ return truthyValues.includes(value.toLowerCase());
}
export async function fetchAndParseCsv(
@@ -155,9 +166,9 @@ export async function fetchAndParseCsv(
country: r.country,
language: r.language,
messagesSent: Number(r.messages_sent) || 0,
- sentiment: mapSentimentToScore(r.sentiment),
- escalated: isTruthyValue(r.escalated),
- forwardedHr: isTruthyValue(r.forwarded_hr),
+ sentiment: mapSentimentToScore(r.sentiment),
+ escalated: isTruthyValue(r.escalated),
+ forwardedHr: isTruthyValue(r.forwarded_hr),
fullTranscriptUrl: r.full_transcript_url,
avgResponseTime: r.avg_response_time
? parseFloat(r.avg_response_time)
diff --git a/lib/metrics.ts b/lib/metrics.ts
index fc8bb94..9cf955f 100644
--- a/lib/metrics.ts
+++ b/lib/metrics.ts
@@ -19,6 +19,9 @@ export function sessionMetrics(
const byDay: DayMetrics = {};
const byCategory: CategoryMetrics = {};
const byLanguage: LanguageMetrics = {};
+ const tokensByDay: DayMetrics = {};
+ const tokensCostByDay: DayMetrics = {};
+
let escalated = 0,
forwarded = 0;
let totalSentiment = 0,
@@ -28,6 +31,11 @@ export function sessionMetrics(
let totalTokens = 0,
totalTokensEur = 0;
+ // For sentiment distribution
+ let sentimentPositive = 0,
+ sentimentNegative = 0,
+ sentimentNeutral = 0;
+
// Calculate total session duration in minutes
let totalDuration = 0;
let durationCount = 0;
@@ -39,6 +47,16 @@ export function sessionMetrics(
if (s.category) byCategory[s.category] = (byCategory[s.category] || 0) + 1;
if (s.language) byLanguage[s.language] = (byLanguage[s.language] || 0) + 1;
+ // Process token usage by day
+ if (s.tokens) {
+ tokensByDay[day] = (tokensByDay[day] || 0) + s.tokens;
+ }
+
+ // Process token cost by day
+ if (s.tokensEur) {
+ tokensCostByDay[day] = (tokensCostByDay[day] || 0) + s.tokensEur;
+ }
+
if (s.endTime) {
const duration =
(s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
@@ -52,6 +70,15 @@ export function sessionMetrics(
if (s.sentiment != null) {
totalSentiment += s.sentiment;
sentimentCount++;
+
+ // Classify sentiment
+ if (s.sentiment > 0.3) {
+ sentimentPositive++;
+ } else if (s.sentiment < -0.3) {
+ sentimentNegative++;
+ } else {
+ sentimentNeutral++;
+ }
}
if (s.avgResponseTime != null) {
@@ -91,11 +118,18 @@ export function sessionMetrics(
// Additional metrics not in the interface - using type assertion
escalatedCount: escalated,
forwardedCount: forwarded,
- avgSentiment: sentimentCount ? totalSentiment / sentimentCount : null,
- avgResponseTime: responseCount ? totalResponse / responseCount : null,
+ avgSentiment: sentimentCount ? totalSentiment / sentimentCount : undefined,
+ avgResponseTime: responseCount ? totalResponse / responseCount : undefined,
totalTokens,
totalTokensEur,
sentimentThreshold: threshold,
lastUpdated: Date.now(), // Add current timestamp
- } as MetricsResult;
+
+ // New metrics for enhanced dashboard
+ sentimentPositiveCount: sentimentPositive,
+ sentimentNeutralCount: sentimentNeutral,
+ sentimentNegativeCount: sentimentNegative,
+ tokensByDay,
+ tokensCostByDay,
+ };
}
diff --git a/lib/types.ts b/lib/types.ts
index 80a9c24..c8c70c0 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -86,6 +86,13 @@ export interface MetricsResult {
totalTokensEur?: number;
sentimentThreshold?: number | null;
lastUpdated?: number; // Timestamp for when metrics were last updated
+
+ // New metrics for enhanced dashboard
+ sentimentPositiveCount?: number;
+ sentimentNeutralCount?: number;
+ sentimentNegativeCount?: number;
+ tokensByDay?: DayMetrics;
+ tokensCostByDay?: DayMetrics;
}
export interface ApiResponse {
diff --git a/pages/api/admin/refresh-sessions.ts b/pages/api/admin/refresh-sessions.ts
index 36e11a1..9524955 100644
--- a/pages/api/admin/refresh-sessions.ts
+++ b/pages/api/admin/refresh-sessions.ts
@@ -87,28 +87,31 @@ export default async function handler(
data: {
id: sessionData.id,
companyId: sessionData.companyId,
- startTime: startTime,
+ startTime: startTime,
endTime: endTime,
ipAddress: session.ipAddress || null,
country: session.country || null,
- language: session.language || null,
+ language: session.language || null,
messagesSent:
typeof session.messagesSent === "number" ? session.messagesSent : 0,
- sentiment:
- typeof session.sentiment === "number" ? session.sentiment : null,
- escalated:
- typeof session.escalated === "boolean" ? session.escalated : null,
- forwardedHr:
- typeof session.forwardedHr === "boolean" ? session.forwardedHr : null,
- fullTranscriptUrl: session.fullTranscriptUrl || null,
- avgResponseTime:
- typeof session.avgResponseTime === "number" ? session.avgResponseTime : null,
- tokens:
- typeof session.tokens === "number" ? session.tokens : null,
- tokensEur:
- typeof session.tokensEur === "number" ? session.tokensEur : null,
+ sentiment:
+ typeof session.sentiment === "number" ? session.sentiment : null,
+ escalated:
+ typeof session.escalated === "boolean" ? session.escalated : null,
+ forwardedHr:
+ typeof session.forwardedHr === "boolean"
+ ? session.forwardedHr
+ : null,
+ fullTranscriptUrl: session.fullTranscriptUrl || null,
+ avgResponseTime:
+ typeof session.avgResponseTime === "number"
+ ? session.avgResponseTime
+ : null,
+ tokens: typeof session.tokens === "number" ? session.tokens : null,
+ tokensEur:
+ typeof session.tokensEur === "number" ? session.tokensEur : null,
category: session.category || null,
- initialMsg: session.initialMsg || null,
+ initialMsg: session.initialMsg || null,
},
});
}