Enhances dashboard metrics and session handling with word cloud data and country metrics

This commit is contained in:
2025-05-22 07:31:24 +02:00
parent 7762061850
commit 2b75c53241
6 changed files with 509 additions and 128 deletions

View File

@ -11,7 +11,7 @@ import {
} from "../../components/Charts"; } 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, WordCloudWord } from "../../lib/types"; // Added WordCloudWord
import MetricCard from "../../components/MetricCard"; import MetricCard from "../../components/MetricCard";
import DonutChart from "../../components/DonutChart"; import DonutChart from "../../components/DonutChart";
import WordCloud from "../../components/WordCloud"; import WordCloud from "../../components/WordCloud";
@ -136,14 +136,10 @@ function DashboardContent() {
return <div className="text-center py-10">Loading dashboard...</div>; return <div className="text-center py-10">Loading dashboard...</div>;
} }
// Function to prepare word cloud data from categories // Function to prepare word cloud data from metrics.wordCloudData
const getWordCloudData = () => { const getWordCloudData = (): WordCloudWord[] => {
if (!metrics || !metrics.categories) return []; if (!metrics || !metrics.wordCloudData) return [];
return Object.entries(metrics.categories) return metrics.wordCloudData;
.map(([text, value]) => ({ text, value }))
.filter((item) => item.text.trim() !== "")
.sort((a, b) => b.value - a.value)
.slice(0, 30);
}; };
// Function to prepare country data for the map - using simulated/dummy data // Function to prepare country data for the map - using simulated/dummy data
@ -380,7 +376,7 @@ function DashboardContent() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-xl shadow overflow-hidden"> <div className="bg-white p-6 rounded-xl shadow overflow-hidden">
<h3 className="font-bold text-lg text-gray-800 mb-4"> <h3 className="font-bold text-lg text-gray-800 mb-4">
Categories Word Cloud Transcript Word Cloud
</h3> </h3>
<WordCloud words={getWordCloudData()} width={500} height={300} /> <WordCloud words={getWordCloudData()} width={500} height={300} />
</div> </div>

View File

@ -4,13 +4,308 @@ import {
DayMetrics, DayMetrics,
CategoryMetrics, CategoryMetrics,
LanguageMetrics, LanguageMetrics,
CountryMetrics, // Added CountryMetrics
MetricsResult, MetricsResult,
WordCloudWord, // Added WordCloudWord
} from "./types"; } from "./types";
interface CompanyConfig { interface CompanyConfig {
sentimentAlert?: number; sentimentAlert?: number;
} }
// List of common stop words - this can be expanded
const stopWords = new Set([
"assistant",
"user",
// Web
"com",
"www",
"http",
"https",
"www2",
"href",
"html",
"php",
"js",
"css",
"xml",
"json",
"txt",
"jpg",
"jpeg",
"png",
"gif",
"bmp",
"svg",
"org",
"net",
"co",
"io",
// English stop words
"a",
"an",
"the",
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"have",
"has",
"had",
"do",
"does",
"did",
"will",
"would",
"should",
"can",
"could",
"may",
"might",
"must",
"am",
"i",
"you",
"he",
"she",
"it",
"we",
"they",
"me",
"him",
"her",
"us",
"them",
"my",
"your",
"his",
"its",
"our",
"their",
"mine",
"yours",
"hers",
"ours",
"theirs",
"to",
"of",
"in",
"on",
"at",
"by",
"for",
"with",
"about",
"against",
"between",
"into",
"through",
"during",
"before",
"after",
"above",
"below",
"from",
"up",
"down",
"out",
"off",
"over",
"under",
"again",
"further",
"then",
"once",
"here",
"there",
"when",
"where",
"why",
"how",
"all",
"any",
"both",
"each",
"few",
"more",
"most",
"other",
"some",
"such",
"no",
"nor",
"not",
"only",
"own",
"same",
"so",
"than",
"too",
"very",
"s",
"t",
"just",
"don",
"shouldve",
"now",
"d",
"ll",
"m",
"o",
"re",
"ve",
"y",
"ain",
"aren",
"couldn",
"didn",
"doesn",
"hadn",
"hasn",
"haven",
"isn",
"ma",
"mightn",
"mustn",
"needn",
"shan",
"shouldn",
"wasn",
"weren",
"won",
"wouldn",
"hi",
"hello",
"thanks",
"thank",
"please",
"ok",
"okay",
"yes",
"yeah",
"bye",
"goodbye",
// French stop words
"la",
"le",
"les",
"un",
"une",
"des",
"et",
"ou",
"mais",
"donc",
// Dutch stop words
"dit",
"ben",
"de",
"het",
"ik",
"jij",
"hij",
"zij",
"wij",
"jullie",
"deze",
"dit",
"dat",
"die",
"een",
"en",
"of",
"maar",
"want",
"omdat",
"dus",
"als",
"ook",
"dan",
"nu",
"nog",
"al",
"naar",
"voor",
"van",
"door",
"met",
"bij",
"tot",
"om",
"over",
"tussen",
"onder",
"boven",
"tegen",
"aan",
"uit",
"sinds",
"tijdens",
"binnen",
"buiten",
"zonder",
"volgens",
"dankzij",
"ondanks",
"behalve",
"mits",
"tenzij",
"hoewel",
"alhoewel",
"toch",
"anders",
"echter",
"wel",
"niet",
"geen",
"iets",
"niets",
"veel",
"weinig",
"meer",
"meest",
"elk",
"ieder",
"sommige",
"hoe",
"wat",
"waar",
"wie",
"wanneer",
"waarom",
"welke",
"wordt",
"worden",
"werd",
"werden",
"geworden",
"zijn",
"ben",
"bent",
"was",
"waren",
"geweest",
"hebben",
"heb",
"hebt",
"heeft",
"had",
"hadden",
"gehad",
"kunnen",
"kan",
"kunt",
"kon",
"konden",
"zullen",
"zal",
"zult",
// Add more domain-specific stop words if necessary
]);
export function sessionMetrics( export function sessionMetrics(
sessions: ChatSession[], sessions: ChatSession[],
companyConfig: CompanyConfig = {} companyConfig: CompanyConfig = {}
@ -19,6 +314,7 @@ export function sessionMetrics(
const byDay: DayMetrics = {}; const byDay: DayMetrics = {};
const byCategory: CategoryMetrics = {}; const byCategory: CategoryMetrics = {};
const byLanguage: LanguageMetrics = {}; const byLanguage: LanguageMetrics = {};
const byCountry: CountryMetrics = {}; // Added for country data
const tokensByDay: DayMetrics = {}; const tokensByDay: DayMetrics = {};
const tokensCostByDay: DayMetrics = {}; const tokensCostByDay: DayMetrics = {};
@ -40,12 +336,15 @@ export function sessionMetrics(
let totalDuration = 0; let totalDuration = 0;
let durationCount = 0; let durationCount = 0;
const wordCounts: { [key: string]: number } = {}; // For WordCloud
sessions.forEach((s) => { sessions.forEach((s) => {
const day = s.startTime.toISOString().slice(0, 10); const day = s.startTime.toISOString().slice(0, 10);
byDay[day] = (byDay[day] || 0) + 1; byDay[day] = (byDay[day] || 0) + 1;
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;
if (s.country) byCountry[s.country] = (byCountry[s.country] || 0) + 1; // Populate byCountry
// Process token usage by day // Process token usage by day
if (s.tokens) { if (s.tokens) {
@ -88,6 +387,24 @@ export function sessionMetrics(
totalTokens += s.tokens || 0; totalTokens += s.tokens || 0;
totalTokensEur += s.tokensEur || 0; totalTokensEur += s.tokensEur || 0;
// Process transcript for WordCloud
if (s.transcriptContent) {
const words = s.transcriptContent.toLowerCase().match(/\b\w+\b/g); // Split into words, lowercase
if (words) {
words.forEach((word) => {
const cleanedWord = word.replace(/[^a-z0-9]/gi, ""); // Remove punctuation
if (
cleanedWord &&
!stopWords.has(cleanedWord) &&
cleanedWord.length > 2
) {
// Check if not a stop word and length > 2
wordCounts[cleanedWord] = (wordCounts[cleanedWord] || 0) + 1;
}
});
}
}
}); });
// Now add sentiment alert logic: // Now add sentiment alert logic:
@ -107,13 +424,20 @@ export function sessionMetrics(
const avgSessionLength = const avgSessionLength =
durationCount > 0 ? totalDuration / durationCount : null; durationCount > 0 ? totalDuration / durationCount : null;
// Prepare wordCloudData
const wordCloudData: WordCloudWord[] = Object.entries(wordCounts)
.map(([text, value]) => ({ text, value }))
.sort((a, b) => b.value - a.value)
.slice(0, 500); // Take top 500 words
return { return {
totalSessions: total, totalSessions: total,
avgSessionsPerDay, avgSessionsPerDay,
avgSessionLength, avgSessionLength,
days: byDay, days: byDay,
languages: byLanguage, languages: byLanguage,
categories: byCategory, categories: byCategory, // This will be empty if we are not using categories for word cloud
countries: byCountry, // Added countries to the result
belowThresholdCount: belowThreshold, belowThresholdCount: belowThreshold,
// Additional metrics not in the interface - using type assertion // Additional metrics not in the interface - using type assertion
escalatedCount: escalated, escalatedCount: escalated,
@ -131,5 +455,6 @@ export function sessionMetrics(
sentimentNegativeCount: sentimentNegative, sentimentNegativeCount: sentimentNegative,
tokensByDay, tokensByDay,
tokensCostByDay, tokensCostByDay,
wordCloudData, // Added word cloud data
}; };
} }

View File

@ -74,6 +74,15 @@ export interface LanguageMetrics {
[language: string]: number; [language: string]: number;
} }
export interface CountryMetrics {
[country: string]: number;
}
export interface WordCloudWord {
text: string;
value: number;
}
export interface MetricsResult { export interface MetricsResult {
totalSessions: number; totalSessions: number;
avgSessionsPerDay: number; avgSessionsPerDay: number;
@ -81,6 +90,7 @@ export interface MetricsResult {
days: DayMetrics; days: DayMetrics;
languages: LanguageMetrics; languages: LanguageMetrics;
categories: CategoryMetrics; categories: CategoryMetrics;
countries: CountryMetrics; // Added for geographic distribution
belowThresholdCount: number; belowThresholdCount: number;
// Additional properties for dashboard // Additional properties for dashboard
escalatedCount?: number; escalatedCount?: number;
@ -98,6 +108,7 @@ export interface MetricsResult {
sentimentNegativeCount?: number; sentimentNegativeCount?: number;
tokensByDay?: DayMetrics; tokensByDay?: DayMetrics;
tokensCostByDay?: DayMetrics; tokensCostByDay?: DayMetrics;
wordCloudData?: WordCloudWord[]; // Added for transcript-based word cloud
} }
export interface ApiResponse<T> { export interface ApiResponse<T> {

View File

@ -4,6 +4,7 @@ import { getServerSession } from "next-auth";
import { prisma } from "../../../lib/prisma"; import { prisma } from "../../../lib/prisma";
import { sessionMetrics } from "../../../lib/metrics"; import { sessionMetrics } from "../../../lib/metrics";
import { authOptions } from "../auth/[...nextauth]"; import { authOptions } from "../auth/[...nextauth]";
import { ChatSession } from "../../../lib/types"; // Import ChatSession
interface SessionUser { interface SessionUser {
email: string; email: string;
@ -32,13 +33,47 @@ export default async function handler(
if (!user) return res.status(401).json({ error: "No user" }); if (!user) return res.status(401).json({ error: "No user" });
const sessions = await prisma.session.findMany({ const prismaSessions = await prisma.session.findMany({
where: { companyId: user.companyId }, where: { companyId: user.companyId },
}); });
// Convert Prisma sessions to ChatSession[] type for sessionMetrics
const chatSessions: ChatSession[] = prismaSessions.map((ps) => ({
id: ps.id, // Map Prisma's id to ChatSession.id
sessionId: ps.id, // Map Prisma's id to ChatSession.sessionId
companyId: ps.companyId,
startTime: new Date(ps.startTime), // Ensure startTime is a Date object
endTime: ps.endTime ? new Date(ps.endTime) : null, // Ensure endTime is a Date object or null
transcriptContent: ps.transcriptContent || "", // Ensure transcriptContent is a string
createdAt: new Date(ps.createdAt), // Map Prisma's createdAt
updatedAt: new Date(ps.createdAt), // Use createdAt for updatedAt as Session model doesn't have updatedAt
category: ps.category || undefined,
language: ps.language || undefined,
country: ps.country || undefined,
ipAddress: ps.ipAddress || undefined,
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent, // Handle null messagesSent
avgResponseTime:
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
tokens: ps.tokens === null ? undefined : ps.tokens,
tokensEur: ps.tokensEur === null ? undefined : ps.tokensEur,
escalated: ps.escalated || false,
forwardedHr: ps.forwardedHr || false,
initialMsg: ps.initialMsg || undefined,
fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
// userId is missing in Prisma Session model, assuming it's not strictly needed for metrics or can be null
userId: undefined, // Or some other default/mapping if available
}));
// Pass company config to metrics // Pass company config to metrics
// @ts-expect-error - Type conversion is needed between prisma session and ChatSession const companyConfigForMetrics = {
const metrics = sessionMetrics(sessions, user.company); sentimentAlert:
user.company.sentimentAlert === null
? undefined
: user.company.sentimentAlert,
};
const metrics = sessionMetrics(chatSessions, companyConfigForMetrics);
res.json({ res.json({
metrics, metrics,

View File

@ -27,32 +27,38 @@ export default async function handler(
// Map Prisma session object to ChatSession type // Map Prisma session object to ChatSession type
const session: ChatSession = { const session: ChatSession = {
// Spread prismaSession to include all its properties
...prismaSession, ...prismaSession,
sessionId: prismaSession.id, // Assuming ChatSession's sessionId is Prisma's id // Override properties that need conversion or specific mapping
id: prismaSession.id, // ChatSession.id from Prisma.Session.id
sessionId: prismaSession.id, // ChatSession.sessionId from Prisma.Session.id
startTime: new Date(prismaSession.startTime), startTime: new Date(prismaSession.startTime),
endTime: prismaSession.endTime ? new Date(prismaSession.endTime) : null, endTime: prismaSession.endTime ? new Date(prismaSession.endTime) : null,
createdAt: new Date(prismaSession.createdAt), createdAt: new Date(prismaSession.createdAt),
updatedAt: new Date(prismaSession.updatedAt), // Prisma.Session does not have an `updatedAt` field. We'll use `createdAt` as a fallback.
userId: prismaSession.userId === undefined ? null : prismaSession.userId, // Or, if your business logic implies an update timestamp elsewhere, use that.
category: prismaSession.category === undefined ? null : prismaSession.category, updatedAt: new Date(prismaSession.createdAt), // Fallback to createdAt
language: prismaSession.language === undefined ? null : prismaSession.language, // Prisma.Session does not have a `userId` field.
country: prismaSession.country === undefined ? null : prismaSession.country, userId: null, // Explicitly set to null or map if available from another source
ipAddress: prismaSession.ipAddress === undefined ? null : prismaSession.ipAddress, // Ensure nullable fields from Prisma are correctly mapped to ChatSession's optional or nullable fields
sentiment: prismaSession.sentiment === undefined ? null : prismaSession.sentiment, category: prismaSession.category ?? null,
messagesSent: prismaSession.messagesSent === undefined ? undefined : prismaSession.messagesSent, language: prismaSession.language ?? null,
avgResponseTime: prismaSession.avgResponseTime === undefined ? null : prismaSession.avgResponseTime, country: prismaSession.country ?? null,
escalated: prismaSession.escalated === undefined ? undefined : prismaSession.escalated, ipAddress: prismaSession.ipAddress ?? null,
forwardedHr: prismaSession.forwardedHr === undefined ? undefined : prismaSession.forwardedHr, sentiment: prismaSession.sentiment ?? null,
tokens: prismaSession.tokens === undefined ? undefined : prismaSession.tokens, messagesSent: prismaSession.messagesSent ?? undefined, // Use undefined if ChatSession expects number | undefined
tokensEur: prismaSession.tokensEur === undefined ? undefined : prismaSession.tokensEur, avgResponseTime: prismaSession.avgResponseTime ?? null,
initialMsg: prismaSession.initialMsg === undefined ? null : prismaSession.initialMsg, escalated: prismaSession.escalated ?? undefined,
fullTranscriptUrl: prismaSession.fullTranscriptUrl === undefined ? null : prismaSession.fullTranscriptUrl, forwardedHr: prismaSession.forwardedHr ?? undefined,
transcriptContent: prismaSession.transcriptContent === undefined ? null : prismaSession.transcriptContent, tokens: prismaSession.tokens ?? undefined,
tokensEur: prismaSession.tokensEur ?? undefined,
initialMsg: prismaSession.initialMsg ?? undefined,
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
transcriptContent: prismaSession.transcriptContent ?? null,
}; };
return res.status(200).json({ session }); return res.status(200).json({ session });
} catch (error) { } catch (error) {
console.error(`Failed to fetch session ${id}:`, error);
const errorMessage = const errorMessage =
error instanceof Error ? error.message : "An unknown error occurred"; error instanceof Error ? error.message : "An unknown error occurred";
return res return res

View File

@ -1,4 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../auth/[...nextauth]";
import { prisma } from "../../../lib/prisma"; import { prisma } from "../../../lib/prisma";
import { ChatSession } from "../../../lib/types"; import { ChatSession } from "../../../lib/types";
@ -10,64 +12,70 @@ export default async function handler(
return res.status(405).json({ error: "Method not allowed" }); return res.status(405).json({ error: "Method not allowed" });
} }
const authSession = await getServerSession(req, res, authOptions);
if (!authSession || !authSession.user?.companyId) {
return res.status(401).json({ error: "Unauthorized" });
}
const companyId = authSession.user.companyId;
const { searchTerm } = req.query; const { searchTerm } = req.query;
try { try {
let prismaSessions; const whereClause: any = { companyId };
if (searchTerm && typeof searchTerm === "string" && searchTerm.trim() !== "") { if (
searchTerm &&
typeof searchTerm === "string" &&
searchTerm.trim() !== ""
) {
const searchConditions = [ const searchConditions = [
{ id: { contains: searchTerm, mode: "insensitive" } }, { id: { contains: searchTerm, mode: "insensitive" } },
{ category: { contains: searchTerm, mode: "insensitive" } }, { category: { contains: searchTerm, mode: "insensitive" } },
{ initialMsg: { contains: searchTerm, mode: "insensitive" } }, { initialMsg: { contains: searchTerm, mode: "insensitive" } },
{ transcriptContent: { contains: searchTerm, mode: "insensitive" } }, { transcriptContent: { contains: searchTerm, mode: "insensitive" } },
]; ];
whereClause.OR = searchConditions;
prismaSessions = await prisma.session.findMany({
where: {
OR: searchConditions,
},
orderBy: {
startTime: "desc",
},
});
} else {
prismaSessions = await prisma.session.findMany({
orderBy: {
startTime: "desc",
},
});
} }
const sessions: ChatSession[] = prismaSessions.map(ps => ({ const prismaSessions = await prisma.session.findMany({
...ps, where: whereClause,
orderBy: {
startTime: "desc",
},
});
const sessions: ChatSession[] = prismaSessions.map((ps) => ({
id: ps.id,
sessionId: ps.id, sessionId: ps.id,
companyId: ps.companyId,
startTime: new Date(ps.startTime), startTime: new Date(ps.startTime),
endTime: ps.endTime ? new Date(ps.endTime) : null, endTime: ps.endTime ? new Date(ps.endTime) : null,
createdAt: new Date(ps.createdAt), createdAt: new Date(ps.createdAt),
updatedAt: new Date(ps.updatedAt), updatedAt: new Date(ps.createdAt),
userId: ps.userId === undefined ? null : ps.userId, userId: null,
category: ps.category === undefined ? null : ps.category, category: ps.category ?? null,
language: ps.language === undefined ? null : ps.language, language: ps.language ?? null,
country: ps.country === undefined ? null : ps.country, country: ps.country ?? null,
ipAddress: ps.ipAddress === undefined ? null : ps.ipAddress, ipAddress: ps.ipAddress ?? null,
sentiment: ps.sentiment === undefined ? null : ps.sentiment, sentiment: ps.sentiment ?? null,
messagesSent: ps.messagesSent === undefined ? undefined : ps.messagesSent, messagesSent: ps.messagesSent ?? undefined,
avgResponseTime: ps.avgResponseTime === undefined ? null : ps.avgResponseTime, avgResponseTime: ps.avgResponseTime ?? null,
escalated: ps.escalated === undefined ? undefined : ps.escalated, escalated: ps.escalated ?? undefined,
forwardedHr: ps.forwardedHr === undefined ? undefined : ps.forwardedHr, forwardedHr: ps.forwardedHr ?? undefined,
tokens: ps.tokens === undefined ? undefined : ps.tokens, tokens: ps.tokens ?? undefined,
tokensEur: ps.tokensEur === undefined ? undefined : ps.tokensEur, tokensEur: ps.tokensEur ?? undefined,
initialMsg: ps.initialMsg === undefined ? null : ps.initialMsg, initialMsg: ps.initialMsg ?? undefined,
fullTranscriptUrl: ps.fullTranscriptUrl === undefined ? null : ps.fullTranscriptUrl, fullTranscriptUrl: ps.fullTranscriptUrl ?? null,
transcriptContent: ps.transcriptContent === undefined ? null : ps.transcriptContent, transcriptContent: ps.transcriptContent ?? null,
})); }));
return res.status(200).json({ sessions }); return res.status(200).json({ sessions });
} catch (error) { } catch (error) {
console.error("Failed to fetch sessions:", error);
const errorMessage = const errorMessage =
error instanceof Error ? error.message : "An unknown error occurred"; error instanceof Error ? error.message : "An unknown error occurred";
return res.status(500).json({ error: "Failed to fetch sessions", details: errorMessage }); return res
.status(500)
.json({ error: "Failed to fetch sessions", details: errorMessage });
} }
} }