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:
2025-05-22 01:00:46 +02:00
parent 4db0104e2c
commit 6d4055c4eb
6 changed files with 475 additions and 65 deletions

View File

@ -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<string, number> = {
'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<string, number> = {
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)

View File

@ -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,
};
}

View File

@ -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<T> {