diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 65570c5..bff9601 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -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
Loading dashboard...
; } - // 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() {

- Categories Word Cloud + Transcript Word Cloud

diff --git a/lib/metrics.ts b/lib/metrics.ts index a283caf..8cbc199 100644 --- a/lib/metrics.ts +++ b/lib/metrics.ts @@ -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 }; } diff --git a/lib/types.ts b/lib/types.ts index cc92b0d..32d444c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -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 { diff --git a/pages/api/dashboard/metrics.ts b/pages/api/dashboard/metrics.ts index 0c3986c..c34a84c 100644 --- a/pages/api/dashboard/metrics.ts +++ b/pages/api/dashboard/metrics.ts @@ -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, diff --git a/pages/api/dashboard/session/[id].ts b/pages/api/dashboard/session/[id].ts index a9ed1f4..8e2eb73 100644 --- a/pages/api/dashboard/session/[id].ts +++ b/pages/api/dashboard/session/[id].ts @@ -3,60 +3,66 @@ import { prisma } from "../../../../lib/prisma"; import { ChatSession } from "../../../../lib/types"; export default async function handler( - req: NextApiRequest, - res: NextApiResponse + req: NextApiRequest, + res: NextApiResponse ) { - if (req.method !== "GET") { - return res.status(405).json({ error: "Method not allowed" }); + if (req.method !== "GET") { + return res.status(405).json({ error: "Method not allowed" }); + } + + const { id } = req.query; + + if (!id || typeof id !== "string") { + return res.status(400).json({ error: "Session ID is required" }); + } + + try { + const prismaSession = await prisma.session.findUnique({ + where: { id }, + }); + + if (!prismaSession) { + return res.status(404).json({ error: "Session not found" }); } - const { id } = req.query; + // Map Prisma session object to ChatSession type + const session: ChatSession = { + // Spread prismaSession to include all its properties + ...prismaSession, + // 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), + // 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, + }; - if (!id || typeof id !== "string") { - return res.status(400).json({ error: "Session ID is required" }); - } - - try { - const prismaSession = await prisma.session.findUnique({ - where: { id }, - }); - - if (!prismaSession) { - return res.status(404).json({ error: "Session not found" }); - } - - // Map Prisma session object to ChatSession type - const session: ChatSession = { - ...prismaSession, - sessionId: prismaSession.id, // Assuming ChatSession's sessionId is Prisma's 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, - }; - - 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 - .status(500) - .json({ error: "Failed to fetch session", details: errorMessage }); - } + return res.status(200).json({ session }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "An unknown error occurred"; + return res + .status(500) + .json({ error: "Failed to fetch session", details: errorMessage }); + } } diff --git a/pages/api/dashboard/sessions.ts b/pages/api/dashboard/sessions.ts index 1b069a8..d62e7f7 100644 --- a/pages/api/dashboard/sessions.ts +++ b/pages/api/dashboard/sessions.ts @@ -1,73 +1,81 @@ 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"; export default async function handler( - req: NextApiRequest, - res: NextApiResponse + req: NextApiRequest, + res: NextApiResponse ) { - if (req.method !== "GET") { - return res.status(405).json({ error: "Method not allowed" }); + if (req.method !== "GET") { + 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 { + const whereClause: any = { companyId }; + + 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" } }, + ]; + whereClause.OR = searchConditions; } - const { searchTerm } = req.query; + const prismaSessions = await prisma.session.findMany({ + where: whereClause, + orderBy: { + startTime: "desc", + }, + }); - try { - let prismaSessions; + 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.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, + })); - 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", - }, - }); - } - - const sessions: ChatSession[] = prismaSessions.map(ps => ({ - ...ps, - sessionId: ps.id, - 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, - })); - - 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(200).json({ sessions }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "An unknown error occurred"; + return res + .status(500) + .json({ error: "Failed to fetch sessions", details: errorMessage }); + } }