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

View File

@ -4,13 +4,308 @@ import {
DayMetrics,
CategoryMetrics,
LanguageMetrics,
CountryMetrics, // Added CountryMetrics
MetricsResult,
WordCloudWord, // Added WordCloudWord
} from "./types";
interface CompanyConfig {
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(
sessions: ChatSession[],
companyConfig: CompanyConfig = {}
@ -19,6 +314,7 @@ export function sessionMetrics(
const byDay: DayMetrics = {};
const byCategory: CategoryMetrics = {};
const byLanguage: LanguageMetrics = {};
const byCountry: CountryMetrics = {}; // Added for country data
const tokensByDay: DayMetrics = {};
const tokensCostByDay: DayMetrics = {};
@ -40,12 +336,15 @@ export function sessionMetrics(
let totalDuration = 0;
let durationCount = 0;
const wordCounts: { [key: string]: number } = {}; // For WordCloud
sessions.forEach((s) => {
const day = s.startTime.toISOString().slice(0, 10);
byDay[day] = (byDay[day] || 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.country) byCountry[s.country] = (byCountry[s.country] || 0) + 1; // Populate byCountry
// Process token usage by day
if (s.tokens) {
@ -88,6 +387,24 @@ export function sessionMetrics(
totalTokens += s.tokens || 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:
@ -107,13 +424,20 @@ export function sessionMetrics(
const avgSessionLength =
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 {
totalSessions: total,
avgSessionsPerDay,
avgSessionLength,
days: byDay,
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,
// Additional metrics not in the interface - using type assertion
escalatedCount: escalated,
@ -131,5 +455,6 @@ export function sessionMetrics(
sentimentNegativeCount: sentimentNegative,
tokensByDay,
tokensCostByDay,
wordCloudData, // Added word cloud data
};
}

View File

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

View File

@ -4,6 +4,7 @@ import { getServerSession } from "next-auth";
import { prisma } from "../../../lib/prisma";
import { sessionMetrics } from "../../../lib/metrics";
import { authOptions } from "../auth/[...nextauth]";
import { ChatSession } from "../../../lib/types"; // Import ChatSession
interface SessionUser {
email: string;
@ -32,13 +33,47 @@ export default async function handler(
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 },
});
// 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
// @ts-expect-error - Type conversion is needed between prisma session and ChatSession
const metrics = sessionMetrics(sessions, user.company);
const companyConfigForMetrics = {
sentimentAlert:
user.company.sentimentAlert === null
? undefined
: user.company.sentimentAlert,
};
const metrics = sessionMetrics(chatSessions, companyConfigForMetrics);
res.json({
metrics,

View File

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

View File

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