From 8f3c1e0f7c7210db418a92b1f0ef65ff5ec8d2a0 Mon Sep 17 00:00:00 2001 From: Max Kowalski Date: Thu, 26 Jun 2025 12:04:51 +0200 Subject: [PATCH] feat: Enhance dashboard metrics with new calculations and add Top Questions Chart component --- app/dashboard/overview/page.tsx | 70 ++++++++++++++++++++++- components/TopQuestionsChart.tsx | 74 +++++++++++++++++++++++++ lib/metrics.ts | 95 +++++++++++++++++++++++++++++++- lib/types.ts | 11 ++++ pages/api/dashboard/metrics.ts | 6 ++ 5 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 components/TopQuestionsChart.tsx diff --git a/app/dashboard/overview/page.tsx b/app/dashboard/overview/page.tsx index 57beb61..10e1790 100644 --- a/app/dashboard/overview/page.tsx +++ b/app/dashboard/overview/page.tsx @@ -17,6 +17,7 @@ import GeographicMap from "../../../components/GeographicMap"; import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution"; import WelcomeBanner from "../../../components/WelcomeBanner"; import DateRangePicker from "../../../components/DateRangePicker"; +import TopQuestionsChart from "../../../components/TopQuestionsChart"; // Safely wrapped component with useSession function DashboardContent() { @@ -268,7 +269,7 @@ function DashboardContent() { /> )} -
+
+ + + + } + /> + + + + } + /> + + + + } + trend={{ + value: metrics.resolvedChatsPercentage ?? 0, + isPositive: (metrics.resolvedChatsPercentage ?? 0) >= 80, // 80%+ resolution rate is good + }} + />
@@ -429,6 +494,9 @@ function DashboardContent() {
+ {/* Top Questions Chart */} + +

Response Time Distribution diff --git a/components/TopQuestionsChart.tsx b/components/TopQuestionsChart.tsx new file mode 100644 index 0000000..716a17e --- /dev/null +++ b/components/TopQuestionsChart.tsx @@ -0,0 +1,74 @@ +'use client'; + +import React from 'react'; +import { TopQuestion } from '../lib/types'; + +interface TopQuestionsChartProps { + data: TopQuestion[]; + title?: string; +} + +export default function TopQuestionsChart({ data, title = "Top 5 Asked Questions" }: TopQuestionsChartProps) { + if (!data || data.length === 0) { + return ( +
+

{title}

+
+ No questions data available +
+
+ ); + } + + // Find the maximum count to calculate relative bar widths + const maxCount = Math.max(...data.map(q => q.count)); + + return ( +
+

{title}

+ +
+ {data.map((question, index) => { + const percentage = maxCount > 0 ? (question.count / maxCount) * 100 : 0; + + return ( +
+ {/* Question text */} +
+

+ {question.question} +

+ + {question.count} + +
+ + {/* Progress bar */} +
+
+
+ + {/* Rank indicator */} +
+ {index + 1} +
+
+ ); + })} +
+ + {/* Summary */} +
+
+ Total questions analyzed + + {data.reduce((sum, q) => sum + q.count, 0)} + +
+
+
+ ); +} diff --git a/lib/metrics.ts b/lib/metrics.ts index 4e2b69e..1d03f73 100644 --- a/lib/metrics.ts +++ b/lib/metrics.ts @@ -7,6 +7,7 @@ import { CountryMetrics, // Added CountryMetrics MetricsResult, WordCloudWord, // Added WordCloudWord + TopQuestion, // Added TopQuestion } from "./types"; interface CompanyConfig { @@ -348,8 +349,24 @@ export function sessionMetrics( let totalTokensEur = 0; const wordCounts: { [key: string]: number } = {}; let alerts = 0; + + // New metrics variables + const hourlySessionCounts: { [hour: string]: number } = {}; + let resolvedChatsCount = 0; + const questionCounts: { [question: string]: number } = {}; for (const session of sessions) { + // Track hourly usage for peak time calculation + if (session.startTime) { + const hour = new Date(session.startTime).getHours(); + const hourKey = `${hour.toString().padStart(2, '0')}:00`; + hourlySessionCounts[hourKey] = (hourlySessionCounts[hourKey] || 0) + 1; + } + + // Count resolved chats (sessions that have ended and are not escalated) + if (session.endTime && !session.escalated) { + resolvedChatsCount++; + } // Unique Users: Prefer non-empty ipAddress, fallback to non-empty sessionId let identifierAdded = false; if (session.ipAddress && session.ipAddress.trim() !== "") { @@ -487,6 +504,51 @@ export function sessionMetrics( byCountry[session.country] = (byCountry[session.country] || 0) + 1; } + // Extract questions from session + const extractQuestions = () => { + // 1. Extract from questions JSON field + if (session.questions) { + try { + const questionsArray = JSON.parse(session.questions); + if (Array.isArray(questionsArray)) { + questionsArray.forEach((question: string) => { + if (question && question.trim().length > 0) { + const cleanQuestion = question.trim(); + questionCounts[cleanQuestion] = (questionCounts[cleanQuestion] || 0) + 1; + } + }); + } + } catch (error) { + console.warn(`[metrics] Failed to parse questions JSON for session ${session.id}: ${error}`); + } + } + + // 2. Extract questions from user messages (if available) + if (session.messages) { + session.messages + .filter(msg => msg.role === 'User') + .forEach(msg => { + const content = msg.content.trim(); + // Simple heuristic: if message ends with ? or contains question words, treat as question + if (content.endsWith('?') || + /\b(what|when|where|why|how|who|which|can|could|would|will|is|are|do|does|did)\b/i.test(content)) { + questionCounts[content] = (questionCounts[content] || 0) + 1; + } + }); + } + + // 3. Extract questions from initial message as fallback + if (session.initialMsg) { + const content = session.initialMsg.trim(); + if (content.endsWith('?') || + /\b(what|when|where|why|how|who|which|can|could|would|will|is|are|do|does|did)\b/i.test(content)) { + questionCounts[content] = (questionCounts[content] || 0) + 1; + } + } + }; + + extractQuestions(); + // Word Cloud Data (from initial message and transcript content) const processTextForWordCloud = (text: string | undefined | null) => { if (!text) return; @@ -506,7 +568,8 @@ export function sessionMetrics( } }; processTextForWordCloud(session.initialMsg); - processTextForWordCloud(session.transcriptContent); + // Note: transcriptContent is not available in ChatSession type + // Could be added later if transcript parsing is implemented } const uniqueUsers = uniqueUserIds.size; @@ -547,6 +610,30 @@ export function sessionMetrics( mockPreviousPeriodData.avgResponseTime ); + // Calculate new metrics + + // 1. Average Daily Costs (euros) + const avgDailyCosts = numDaysWithSessions > 0 ? totalTokensEur / numDaysWithSessions : 0; + + // 2. Peak Usage Time + let peakUsageTime = "N/A"; + if (Object.keys(hourlySessionCounts).length > 0) { + const peakHour = Object.entries(hourlySessionCounts) + .sort(([, a], [, b]) => b - a)[0][0]; + const peakHourNum = parseInt(peakHour.split(':')[0]); + const endHour = (peakHourNum + 1) % 24; + peakUsageTime = `${peakHour}-${endHour.toString().padStart(2, '0')}:00`; + } + + // 3. Resolved Chats Percentage + const resolvedChatsPercentage = totalSessions > 0 ? (resolvedChatsCount / totalSessions) * 100 : 0; + + // 4. Top 5 Asked Questions + const topQuestions: TopQuestion[] = Object.entries(questionCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 5) // Top 5 questions + .map(([question, count]) => ({ question, count })); + // console.log("Debug metrics calculation:", { // totalSessionDuration, // validSessionsForDuration, @@ -585,5 +672,11 @@ export function sessionMetrics( lastUpdated: Date.now(), totalSessionDuration, validSessionsForDuration, + + // New metrics + avgDailyCosts, + peakUsageTime, + resolvedChatsPercentage, + topQuestions, }; } diff --git a/lib/types.ts b/lib/types.ts index 2f04ff0..1800191 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -119,6 +119,11 @@ export interface WordCloudWord { value: number; } +export interface TopQuestion { + question: string; + count: number; +} + export interface MetricsResult { totalSessions: number; avgSessionsPerDay: number; @@ -152,6 +157,12 @@ export interface MetricsResult { usersTrend?: number; // e.g., percentage change in uniqueUsers avgSessionTimeTrend?: number; // e.g., percentage change in avgSessionLength avgResponseTimeTrend?: number; // e.g., percentage change in avgResponseTime + + // New metrics for enhanced dashboard + avgDailyCosts?: number; // Average daily costs in euros + peakUsageTime?: string; // Peak usage time (e.g., "14:00-15:00") + resolvedChatsPercentage?: number; // Percentage of resolved chats + topQuestions?: TopQuestion[]; // Top 5 most asked questions // Debug properties totalSessionDuration?: number; diff --git a/pages/api/dashboard/metrics.ts b/pages/api/dashboard/metrics.ts index 487f0a8..e4a27a5 100644 --- a/pages/api/dashboard/metrics.ts +++ b/pages/api/dashboard/metrics.ts @@ -48,6 +48,9 @@ export default async function handler( const prismaSessions = await prisma.session.findMany({ where: whereClause, + include: { + messages: true, // Include messages for question extraction + }, }); // Convert Prisma sessions to ChatSession[] type for sessionMetrics @@ -74,6 +77,9 @@ export default async function handler( forwardedHr: ps.forwardedHr || false, initialMsg: ps.initialMsg || undefined, fullTranscriptUrl: ps.fullTranscriptUrl || undefined, + questions: ps.questions || undefined, // Include questions field + summary: ps.summary || undefined, // Include summary field + messages: ps.messages || [], // Include messages for question extraction // 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 }));