mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 11:52:09 +01:00
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.
182 lines
4.6 KiB
TypeScript
182 lines
4.6 KiB
TypeScript
// Fetches, parses, and returns chat session data for a company from a CSV URL
|
|
import fetch from "node-fetch";
|
|
import { parse } from "csv-parse/sync";
|
|
|
|
// This type is used internally for parsing the CSV records
|
|
interface CSVRecord {
|
|
session_id: string;
|
|
start_time: string;
|
|
end_time?: string;
|
|
ip_address?: string;
|
|
country?: string;
|
|
language?: string;
|
|
messages_sent?: string;
|
|
sentiment?: string;
|
|
escalated?: string;
|
|
forwarded_hr?: string;
|
|
full_transcript_url?: string;
|
|
avg_response_time?: string;
|
|
tokens?: string;
|
|
tokens_eur?: string;
|
|
category?: string;
|
|
initial_msg?: string;
|
|
[key: string]: string | undefined;
|
|
}
|
|
|
|
interface SessionData {
|
|
id: string;
|
|
sessionId: string;
|
|
startTime: Date;
|
|
endTime: Date | null;
|
|
ipAddress?: string;
|
|
country?: string;
|
|
language?: string | null;
|
|
messagesSent: number;
|
|
sentiment: number | null;
|
|
escalated: boolean;
|
|
forwardedHr: boolean;
|
|
fullTranscriptUrl?: string | null;
|
|
avgResponseTime: number | null;
|
|
tokens: number;
|
|
tokensEur: number;
|
|
category?: string | null;
|
|
initialMsg?: string;
|
|
}
|
|
|
|
/**
|
|
* Converts sentiment string values to numeric scores
|
|
* @param sentimentStr The sentiment string from the CSV
|
|
* @returns A numeric score representing the sentiment
|
|
*/
|
|
function mapSentimentToScore(sentimentStr?: string): number | null {
|
|
if (!sentimentStr) return null;
|
|
|
|
// 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
|
|
};
|
|
|
|
return sentimentMap[sentiment] !== undefined
|
|
? sentimentMap[sentiment]
|
|
: isNaN(parseFloat(sentiment))
|
|
? null
|
|
: parseFloat(sentiment);
|
|
}
|
|
|
|
/**
|
|
* Checks if a string value should be considered as boolean true
|
|
* @param value The string value to check
|
|
* @returns True if the string indicates a positive/true value
|
|
*/
|
|
function isTruthyValue(value?: string): boolean {
|
|
if (!value) return false;
|
|
|
|
const truthyValues = [
|
|
"1",
|
|
"true",
|
|
"yes",
|
|
"y",
|
|
"ja",
|
|
"si",
|
|
"oui",
|
|
"да",
|
|
"да",
|
|
"はい",
|
|
];
|
|
|
|
return truthyValues.includes(value.toLowerCase());
|
|
}
|
|
|
|
export async function fetchAndParseCsv(
|
|
url: string,
|
|
username?: string,
|
|
password?: string,
|
|
): Promise<Partial<SessionData>[]> {
|
|
const authHeader =
|
|
username && password
|
|
? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
|
|
: undefined;
|
|
|
|
const res = await fetch(url, {
|
|
headers: authHeader ? { Authorization: authHeader } : {},
|
|
});
|
|
if (!res.ok) throw new Error("Failed to fetch CSV: " + res.statusText);
|
|
|
|
const text = await res.text();
|
|
|
|
// Parse without expecting headers, using known order
|
|
const records: CSVRecord[] = parse(text, {
|
|
delimiter: ",",
|
|
columns: [
|
|
"session_id",
|
|
"start_time",
|
|
"end_time",
|
|
"ip_address",
|
|
"country",
|
|
"language",
|
|
"messages_sent",
|
|
"sentiment",
|
|
"escalated",
|
|
"forwarded_hr",
|
|
"full_transcript_url",
|
|
"avg_response_time",
|
|
"tokens",
|
|
"tokens_eur",
|
|
"category",
|
|
"initial_msg",
|
|
],
|
|
from_line: 1,
|
|
relax_column_count: true,
|
|
skip_empty_lines: true,
|
|
trim: true,
|
|
});
|
|
|
|
// Helper function to safely parse dates
|
|
function safeParseDate(dateStr?: string): Date | null {
|
|
if (!dateStr) return null;
|
|
const date = new Date(dateStr);
|
|
return !isNaN(date.getTime()) ? date : null;
|
|
}
|
|
|
|
// Coerce types for relevant columns
|
|
return records.map((r) => ({
|
|
id: r.session_id,
|
|
startTime: safeParseDate(r.start_time) || new Date(), // Fallback to current date if invalid
|
|
endTime: safeParseDate(r.end_time),
|
|
ipAddress: r.ip_address,
|
|
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),
|
|
fullTranscriptUrl: r.full_transcript_url,
|
|
avgResponseTime: r.avg_response_time
|
|
? parseFloat(r.avg_response_time)
|
|
: null,
|
|
tokens: Number(r.tokens) || 0,
|
|
tokensEur: r.tokens_eur ? parseFloat(r.tokens_eur) : 0,
|
|
category: r.category,
|
|
initialMsg: r.initial_msg,
|
|
}));
|
|
}
|