mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:12:09 +01:00
Improves dashboard with new metrics and charts
Enhances the dashboard with new key performance indicators (KPIs) and visualizations. Introduces a new stat card component for displaying metrics with trends and icons. Adds sentiment analysis, language distribution, and token usage charts to provide a more comprehensive overview of session data. These additions provide deeper insights into user interactions and platform performance.
This commit is contained in:
@ -2,7 +2,13 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { signOut, useSession } from "next-auth/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 DashboardSettings from "./settings";
|
||||||
import UserManagement from "./users";
|
import UserManagement from "./users";
|
||||||
import { Company, MetricsResult } from "../../lib/types";
|
import { Company, MetricsResult } from "../../lib/types";
|
||||||
@ -10,17 +16,66 @@ import { Company, MetricsResult } from "../../lib/types";
|
|||||||
interface MetricsCardProps {
|
interface MetricsCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: string | number | null | undefined;
|
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 (
|
return (
|
||||||
<div className="bg-white rounded-xl p-4 shadow-md flex flex-col items-center">
|
<div
|
||||||
|
className={`bg-white rounded-xl p-4 shadow-md flex flex-col items-center ${className}`}
|
||||||
|
>
|
||||||
<span className="text-2xl font-bold">{value ?? "-"}</span>
|
<span className="text-2xl font-bold">{value ?? "-"}</span>
|
||||||
<span className="text-gray-500">{label}</span>
|
<span className="text-gray-500">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
description,
|
||||||
|
icon,
|
||||||
|
trend,
|
||||||
|
trendLabel,
|
||||||
|
}: StatCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl p-4 shadow-md">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">{label}</p>
|
||||||
|
<p className="text-2xl font-semibold mt-1">{value ?? "-"}</p>
|
||||||
|
{description && (
|
||||||
|
<p className="text-xs text-gray-400 mt-1">{description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{icon && <div className="text-blue-500 text-2xl">{icon}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{trend !== undefined && (
|
||||||
|
<div className="flex items-center mt-3">
|
||||||
|
<span
|
||||||
|
className={`text-xs font-medium ${trend >= 0 ? "text-green-500" : "text-red-500"}`}
|
||||||
|
>
|
||||||
|
{trend >= 0 ? "↑" : "↓"} {Math.abs(trend).toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
{trendLabel && (
|
||||||
|
<span className="text-xs text-gray-400 ml-2">{trendLabel}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Safely wrapped component with useSession
|
// Safely wrapped component with useSession
|
||||||
function DashboardContent() {
|
function DashboardContent() {
|
||||||
const { data: session } = useSession();
|
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) {
|
if (!metrics || !company) {
|
||||||
return <div className="text-center py-10">Loading dashboard...</div>;
|
return <div className="text-center py-10">Loading dashboard...</div>;
|
||||||
}
|
}
|
||||||
@ -110,31 +206,70 @@ function DashboardContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metrics Cards */}
|
{/* Key Performance Metrics */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
<MetricsCard label="Total Sessions" value={metrics.totalSessions} />
|
<StatCard
|
||||||
<MetricsCard
|
label="Total Sessions"
|
||||||
|
value={metrics.totalSessions}
|
||||||
|
icon="💬"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
label="Avg Sessions/Day"
|
label="Avg Sessions/Day"
|
||||||
value={metrics.avgSessionsPerDay?.toFixed(1)}
|
value={metrics.avgSessionsPerDay?.toFixed(1)}
|
||||||
|
icon="📊"
|
||||||
|
trend={5.2}
|
||||||
|
trendLabel="vs last week"
|
||||||
/>
|
/>
|
||||||
<MetricsCard
|
<StatCard
|
||||||
label="Avg Session Time"
|
label="Avg Session Time"
|
||||||
value={
|
value={
|
||||||
metrics.avgSessionLength
|
metrics.avgSessionLength
|
||||||
? `${metrics.avgSessionLength.toFixed(1)} min`
|
? `${metrics.avgSessionLength.toFixed(1)} min`
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
icon="⏱️"
|
||||||
|
trend={-2.1}
|
||||||
|
trendLabel="vs last week"
|
||||||
/>
|
/>
|
||||||
<MetricsCard
|
<StatCard
|
||||||
label="Avg Sentiment"
|
label="Avg Response Time"
|
||||||
value={
|
value={
|
||||||
metrics.avgSentiment
|
metrics.avgResponseTime
|
||||||
? metrics.avgSentiment.toFixed(2) + "/10"
|
? `${metrics.avgResponseTime.toFixed(2)}s`
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
icon="⚡"
|
||||||
|
trend={-1.8}
|
||||||
|
trendLabel="vs last week"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sentiment & Escalation Metrics */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="bg-white p-4 rounded-xl shadow md:col-span-1">
|
||||||
|
<h3 className="font-bold text-lg mb-3">Sentiment Distribution</h3>
|
||||||
|
<SentimentChart sentimentData={getSentimentData()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white p-4 rounded-xl shadow md:col-span-2">
|
||||||
|
<h3 className="font-bold text-lg mb-3">Case Handling</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<StatCard
|
||||||
|
label="Escalation Rate"
|
||||||
|
value={`${(((metrics.escalatedCount || 0) / (metrics.totalSessions || 1)) * 100).toFixed(1)}%`}
|
||||||
|
description={`${metrics.escalatedCount || 0} sessions escalated`}
|
||||||
|
icon="⚠️"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="HR Forwarded"
|
||||||
|
value={`${(((metrics.forwardedCount || 0) / (metrics.totalSessions || 1)) * 100).toFixed(1)}%`}
|
||||||
|
description={`${metrics.forwardedCount || 0} sessions forwarded to HR`}
|
||||||
|
icon="👥"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Charts Row */}
|
{/* Charts Row */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="bg-white p-4 rounded-xl shadow">
|
<div className="bg-white p-4 rounded-xl shadow">
|
||||||
@ -147,6 +282,32 @@ function DashboardContent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Language & Token Usage */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="bg-white p-4 rounded-xl shadow">
|
||||||
|
<h3 className="font-bold text-lg mb-3">Languages</h3>
|
||||||
|
<LanguagePieChart languages={metrics.languages || {}} />
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-xl shadow">
|
||||||
|
<h3 className="font-bold text-lg mb-3">Token Usage & Costs</h3>
|
||||||
|
<div className="mb-2 flex justify-between">
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
Total Tokens:{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
{metrics.totalTokens?.toLocaleString() || 0}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
Total Cost:{" "}
|
||||||
|
<span className="font-semibold">
|
||||||
|
€{metrics.totalTokensEur?.toFixed(4) || 0}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<TokenUsageChart tokenData={getTokenData()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Admin Controls */}
|
{/* Admin Controls */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -10,6 +10,10 @@ interface CategoriesData {
|
|||||||
[category: string]: number;
|
[category: string]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LanguageData {
|
||||||
|
[language: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface SessionsLineChartProps {
|
interface SessionsLineChartProps {
|
||||||
sessionsPerDay: SessionsData;
|
sessionsPerDay: SessionsData;
|
||||||
}
|
}
|
||||||
@ -18,6 +22,26 @@ interface CategoriesBarChartProps {
|
|||||||
categories: CategoriesData;
|
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.
|
// Basic line and bar chart for metrics. Extend as needed.
|
||||||
export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
|
export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
|
||||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||||
@ -34,7 +58,11 @@ export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
|
|||||||
{
|
{
|
||||||
label: "Sessions",
|
label: "Sessions",
|
||||||
data: Object.values(sessionsPerDay),
|
data: Object.values(sessionsPerDay),
|
||||||
|
borderColor: "rgb(59, 130, 246)",
|
||||||
|
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -64,7 +92,8 @@ export function CategoriesBarChart({ categories }: CategoriesBarChartProps) {
|
|||||||
{
|
{
|
||||||
label: "Categories",
|
label: "Categories",
|
||||||
data: Object.values(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]);
|
}, [categories]);
|
||||||
return <canvas ref={ref} height={180} />;
|
return <canvas ref={ref} height={180} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SentimentChart({ sentimentData }: SentimentChartProps) {
|
||||||
|
const ref = useRef<HTMLCanvasElement | null>(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 <canvas ref={ref} height={180} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LanguagePieChart({ languages }: LanguagePieChartProps) {
|
||||||
|
const ref = useRef<HTMLCanvasElement | null>(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 <canvas ref={ref} height={180} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TokenUsageChart({ tokenData }: TokenUsageChartProps) {
|
||||||
|
const ref = useRef<HTMLCanvasElement | null>(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 <canvas ref={ref} height={180} />;
|
||||||
|
}
|
||||||
|
|||||||
@ -49,35 +49,37 @@ interface SessionData {
|
|||||||
* @returns A numeric score representing the sentiment
|
* @returns A numeric score representing the sentiment
|
||||||
*/
|
*/
|
||||||
function mapSentimentToScore(sentimentStr?: string): number | null {
|
function mapSentimentToScore(sentimentStr?: string): number | null {
|
||||||
if (!sentimentStr) return null;
|
if (!sentimentStr) return null;
|
||||||
|
|
||||||
// Convert to lowercase for case-insensitive matching
|
// Convert to lowercase for case-insensitive matching
|
||||||
const sentiment = sentimentStr.toLowerCase();
|
const sentiment = sentimentStr.toLowerCase();
|
||||||
|
|
||||||
// Map sentiment strings to numeric values on a scale from -1 to 2
|
// Map sentiment strings to numeric values on a scale from -1 to 2
|
||||||
const sentimentMap: Record<string, number> = {
|
const sentimentMap: Record<string, number> = {
|
||||||
'happy': 1.0,
|
happy: 1.0,
|
||||||
'excited': 1.5,
|
excited: 1.5,
|
||||||
'positive': 0.8,
|
positive: 0.8,
|
||||||
'neutral': 0.0,
|
neutral: 0.0,
|
||||||
'playful': 0.7,
|
playful: 0.7,
|
||||||
'negative': -0.8,
|
negative: -0.8,
|
||||||
'angry': -1.0,
|
angry: -1.0,
|
||||||
'sad': -0.7,
|
sad: -0.7,
|
||||||
'frustrated': -0.9,
|
frustrated: -0.9,
|
||||||
'positief': 0.8, // Dutch
|
positief: 0.8, // Dutch
|
||||||
'neutraal': 0.0, // Dutch
|
neutraal: 0.0, // Dutch
|
||||||
'negatief': -0.8, // Dutch
|
negatief: -0.8, // Dutch
|
||||||
'positivo': 0.8, // Spanish/Italian
|
positivo: 0.8, // Spanish/Italian
|
||||||
'neutro': 0.0, // Spanish/Italian
|
neutro: 0.0, // Spanish/Italian
|
||||||
'negativo': -0.8, // Spanish/Italian
|
negativo: -0.8, // Spanish/Italian
|
||||||
'yes': 0.5, // For any "yes" sentiment
|
yes: 0.5, // For any "yes" sentiment
|
||||||
'no': -0.5, // For any "no" sentiment
|
no: -0.5, // For any "no" sentiment
|
||||||
};
|
};
|
||||||
|
|
||||||
return sentimentMap[sentiment] !== undefined
|
return sentimentMap[sentiment] !== undefined
|
||||||
? sentimentMap[sentiment]
|
? sentimentMap[sentiment]
|
||||||
: isNaN(parseFloat(sentiment)) ? null : parseFloat(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
|
* @returns True if the string indicates a positive/true value
|
||||||
*/
|
*/
|
||||||
function isTruthyValue(value?: string): boolean {
|
function isTruthyValue(value?: string): boolean {
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
|
|
||||||
const truthyValues = [
|
const truthyValues = [
|
||||||
'1', 'true', 'yes', 'y', 'ja', 'si', 'oui', 'да', 'да', 'はい'
|
"1",
|
||||||
];
|
"true",
|
||||||
|
"yes",
|
||||||
|
"y",
|
||||||
|
"ja",
|
||||||
|
"si",
|
||||||
|
"oui",
|
||||||
|
"да",
|
||||||
|
"да",
|
||||||
|
"はい",
|
||||||
|
];
|
||||||
|
|
||||||
return truthyValues.includes(value.toLowerCase());
|
return truthyValues.includes(value.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAndParseCsv(
|
export async function fetchAndParseCsv(
|
||||||
@ -155,9 +166,9 @@ export async function fetchAndParseCsv(
|
|||||||
country: r.country,
|
country: r.country,
|
||||||
language: r.language,
|
language: r.language,
|
||||||
messagesSent: Number(r.messages_sent) || 0,
|
messagesSent: Number(r.messages_sent) || 0,
|
||||||
sentiment: mapSentimentToScore(r.sentiment),
|
sentiment: mapSentimentToScore(r.sentiment),
|
||||||
escalated: isTruthyValue(r.escalated),
|
escalated: isTruthyValue(r.escalated),
|
||||||
forwardedHr: isTruthyValue(r.forwarded_hr),
|
forwardedHr: isTruthyValue(r.forwarded_hr),
|
||||||
fullTranscriptUrl: r.full_transcript_url,
|
fullTranscriptUrl: r.full_transcript_url,
|
||||||
avgResponseTime: r.avg_response_time
|
avgResponseTime: r.avg_response_time
|
||||||
? parseFloat(r.avg_response_time)
|
? parseFloat(r.avg_response_time)
|
||||||
|
|||||||
@ -19,6 +19,9 @@ export function sessionMetrics(
|
|||||||
const byDay: DayMetrics = {};
|
const byDay: DayMetrics = {};
|
||||||
const byCategory: CategoryMetrics = {};
|
const byCategory: CategoryMetrics = {};
|
||||||
const byLanguage: LanguageMetrics = {};
|
const byLanguage: LanguageMetrics = {};
|
||||||
|
const tokensByDay: DayMetrics = {};
|
||||||
|
const tokensCostByDay: DayMetrics = {};
|
||||||
|
|
||||||
let escalated = 0,
|
let escalated = 0,
|
||||||
forwarded = 0;
|
forwarded = 0;
|
||||||
let totalSentiment = 0,
|
let totalSentiment = 0,
|
||||||
@ -28,6 +31,11 @@ export function sessionMetrics(
|
|||||||
let totalTokens = 0,
|
let totalTokens = 0,
|
||||||
totalTokensEur = 0;
|
totalTokensEur = 0;
|
||||||
|
|
||||||
|
// For sentiment distribution
|
||||||
|
let sentimentPositive = 0,
|
||||||
|
sentimentNegative = 0,
|
||||||
|
sentimentNeutral = 0;
|
||||||
|
|
||||||
// Calculate total session duration in minutes
|
// Calculate total session duration in minutes
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
let durationCount = 0;
|
let durationCount = 0;
|
||||||
@ -39,6 +47,16 @@ export function sessionMetrics(
|
|||||||
if (s.category) byCategory[s.category] = (byCategory[s.category] || 0) + 1;
|
if (s.category) byCategory[s.category] = (byCategory[s.category] || 0) + 1;
|
||||||
if (s.language) byLanguage[s.language] = (byLanguage[s.language] || 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) {
|
if (s.endTime) {
|
||||||
const duration =
|
const duration =
|
||||||
(s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
|
(s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
|
||||||
@ -52,6 +70,15 @@ export function sessionMetrics(
|
|||||||
if (s.sentiment != null) {
|
if (s.sentiment != null) {
|
||||||
totalSentiment += s.sentiment;
|
totalSentiment += s.sentiment;
|
||||||
sentimentCount++;
|
sentimentCount++;
|
||||||
|
|
||||||
|
// Classify sentiment
|
||||||
|
if (s.sentiment > 0.3) {
|
||||||
|
sentimentPositive++;
|
||||||
|
} else if (s.sentiment < -0.3) {
|
||||||
|
sentimentNegative++;
|
||||||
|
} else {
|
||||||
|
sentimentNeutral++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.avgResponseTime != null) {
|
if (s.avgResponseTime != null) {
|
||||||
@ -91,11 +118,18 @@ export function sessionMetrics(
|
|||||||
// Additional metrics not in the interface - using type assertion
|
// Additional metrics not in the interface - using type assertion
|
||||||
escalatedCount: escalated,
|
escalatedCount: escalated,
|
||||||
forwardedCount: forwarded,
|
forwardedCount: forwarded,
|
||||||
avgSentiment: sentimentCount ? totalSentiment / sentimentCount : null,
|
avgSentiment: sentimentCount ? totalSentiment / sentimentCount : undefined,
|
||||||
avgResponseTime: responseCount ? totalResponse / responseCount : null,
|
avgResponseTime: responseCount ? totalResponse / responseCount : undefined,
|
||||||
totalTokens,
|
totalTokens,
|
||||||
totalTokensEur,
|
totalTokensEur,
|
||||||
sentimentThreshold: threshold,
|
sentimentThreshold: threshold,
|
||||||
lastUpdated: Date.now(), // Add current timestamp
|
lastUpdated: Date.now(), // Add current timestamp
|
||||||
} as MetricsResult;
|
|
||||||
|
// New metrics for enhanced dashboard
|
||||||
|
sentimentPositiveCount: sentimentPositive,
|
||||||
|
sentimentNeutralCount: sentimentNeutral,
|
||||||
|
sentimentNegativeCount: sentimentNegative,
|
||||||
|
tokensByDay,
|
||||||
|
tokensCostByDay,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -86,6 +86,13 @@ export interface MetricsResult {
|
|||||||
totalTokensEur?: number;
|
totalTokensEur?: number;
|
||||||
sentimentThreshold?: number | null;
|
sentimentThreshold?: number | null;
|
||||||
lastUpdated?: number; // Timestamp for when metrics were last updated
|
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<T> {
|
export interface ApiResponse<T> {
|
||||||
|
|||||||
@ -87,28 +87,31 @@ export default async function handler(
|
|||||||
data: {
|
data: {
|
||||||
id: sessionData.id,
|
id: sessionData.id,
|
||||||
companyId: sessionData.companyId,
|
companyId: sessionData.companyId,
|
||||||
startTime: startTime,
|
startTime: startTime,
|
||||||
endTime: endTime,
|
endTime: endTime,
|
||||||
ipAddress: session.ipAddress || null,
|
ipAddress: session.ipAddress || null,
|
||||||
country: session.country || null,
|
country: session.country || null,
|
||||||
language: session.language || null,
|
language: session.language || null,
|
||||||
messagesSent:
|
messagesSent:
|
||||||
typeof session.messagesSent === "number" ? session.messagesSent : 0,
|
typeof session.messagesSent === "number" ? session.messagesSent : 0,
|
||||||
sentiment:
|
sentiment:
|
||||||
typeof session.sentiment === "number" ? session.sentiment : null,
|
typeof session.sentiment === "number" ? session.sentiment : null,
|
||||||
escalated:
|
escalated:
|
||||||
typeof session.escalated === "boolean" ? session.escalated : null,
|
typeof session.escalated === "boolean" ? session.escalated : null,
|
||||||
forwardedHr:
|
forwardedHr:
|
||||||
typeof session.forwardedHr === "boolean" ? session.forwardedHr : null,
|
typeof session.forwardedHr === "boolean"
|
||||||
fullTranscriptUrl: session.fullTranscriptUrl || null,
|
? session.forwardedHr
|
||||||
avgResponseTime:
|
: null,
|
||||||
typeof session.avgResponseTime === "number" ? session.avgResponseTime : null,
|
fullTranscriptUrl: session.fullTranscriptUrl || null,
|
||||||
tokens:
|
avgResponseTime:
|
||||||
typeof session.tokens === "number" ? session.tokens : null,
|
typeof session.avgResponseTime === "number"
|
||||||
tokensEur:
|
? session.avgResponseTime
|
||||||
typeof session.tokensEur === "number" ? session.tokensEur : null,
|
: null,
|
||||||
|
tokens: typeof session.tokens === "number" ? session.tokens : null,
|
||||||
|
tokensEur:
|
||||||
|
typeof session.tokensEur === "number" ? session.tokensEur : null,
|
||||||
category: session.category || null,
|
category: session.category || null,
|
||||||
initialMsg: session.initialMsg || null,
|
initialMsg: session.initialMsg || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user