diff --git a/app/dashboard/company/page.tsx b/app/dashboard/company/page.tsx
index bb2f7e6..c04fea1 100644
--- a/app/dashboard/company/page.tsx
+++ b/app/dashboard/company/page.tsx
@@ -77,7 +77,7 @@ export default function CompanySettingsPage() {
return (
Access Denied
-
You don't have permission to view company settings.
+
You don't have permission to view company settings.
);
}
@@ -103,6 +103,7 @@ export default function CompanySettingsPage() {
e.preventDefault();
handleSave();
}}
+ autoComplete="off"
>
@@ -125,6 +127,7 @@ export default function CompanySettingsPage() {
value={csvUsername}
onChange={(e) => setCsvUsername(e.target.value)}
placeholder="Username for CSV access (if needed)"
+ autoComplete="off"
/>
@@ -136,6 +139,7 @@ export default function CompanySettingsPage() {
value={csvPassword}
onChange={(e) => setCsvPassword(e.target.value)}
placeholder="Password will be updated only if provided"
+ autoComplete="new-password"
/>
Leave blank to keep current password
@@ -154,6 +158,7 @@ export default function CompanySettingsPage() {
placeholder="Threshold value (0-100)"
min="0"
max="100"
+ autoComplete="off"
/>
Percentage of negative sentiment sessions to trigger alert (0-100)
diff --git a/app/dashboard/overview/page.tsx b/app/dashboard/overview/page.tsx
index 83fe7bd..3906f83 100644
--- a/app/dashboard/overview/page.tsx
+++ b/app/dashboard/overview/page.tsx
@@ -246,12 +246,12 @@ function DashboardContent() {
}
trend={{
- value: metrics.sessionTrend,
+ value: metrics.sessionTrend ?? 0,
label:
- metrics.sessionTrend > 0
- ? `${metrics.sessionTrend}% increase`
- : `${Math.abs(metrics.sessionTrend || 0)}% decrease`,
- positive: metrics.sessionTrend >= 0,
+ (metrics.sessionTrend ?? 0) > 0
+ ? `${metrics.sessionTrend ?? 0}% increase`
+ : `${Math.abs(metrics.sessionTrend ?? 0)}% decrease`,
+ isPositive: (metrics.sessionTrend ?? 0) >= 0,
}}
/>
}
trend={{
- value: metrics.usersTrend,
+ value: metrics.usersTrend ?? 0,
label:
metrics.usersTrend > 0
? `${metrics.usersTrend}% increase`
: `${Math.abs(metrics.usersTrend || 0)}% decrease`,
- positive: metrics.usersTrend >= 0,
+ isPositive: metrics.usersTrend >= 0,
}}
/>
}
trend={{
- value: metrics.sessionTimeTrend,
+ value: metrics.sessionTimeTrend ?? 0,
label:
metrics.sessionTimeTrend > 0
? `${metrics.sessionTimeTrend}% increase`
: `${Math.abs(metrics.sessionTimeTrend || 0)}% decrease`,
- positive: metrics.sessionTimeTrend >= 0,
+ isPositive: metrics.sessionTimeTrend >= 0,
}}
/>
}
trend={{
- value: metrics.responseTimeTrend,
+ value: metrics.avgResponseTimeTrend ?? 0,
label:
- metrics.responseTimeTrend > 0
- ? `${metrics.responseTimeTrend}% increase`
- : `${Math.abs(metrics.responseTimeTrend || 0)}% decrease`,
- positive: metrics.responseTimeTrend <= 0, // Lower response time is better
+ (metrics.avgResponseTimeTrend ?? 0) > 0
+ ? `${metrics.avgResponseTimeTrend ?? 0}% increase`
+ : `${Math.abs(metrics.avgResponseTimeTrend ?? 0)}% decrease`,
+ isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0, // Lower response time is better
}}
/>
@@ -345,7 +345,7 @@ function DashboardContent() {
Sessions Over Time
-
+
diff --git a/app/dashboard/users/page.tsx b/app/dashboard/users/page.tsx
index 87a2a0e..c1addcc 100644
--- a/app/dashboard/users/page.tsx
+++ b/app/dashboard/users/page.tsx
@@ -74,7 +74,7 @@ export default function UserManagementPage() {
return (
Access Denied
-
You don't have permission to view user management.
+
You don't have permission to view user management.
);
}
@@ -102,6 +102,7 @@ export default function UserManagementPage() {
e.preventDefault();
inviteUser();
}}
+ autoComplete="off" // Disable autofill for the form
>
@@ -112,6 +113,7 @@ export default function UserManagementPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
required
+ autoComplete="off" // Disable autofill for this input
/>
diff --git a/components/GeographicMap.tsx b/components/GeographicMap.tsx
index ca767ea..a2828b0 100644
--- a/components/GeographicMap.tsx
+++ b/components/GeographicMap.tsx
@@ -44,7 +44,6 @@ const getCountryCoordinates = (): Record => {
return coordinates;
} catch (error) {
- // eslint-disable-next-line no-console
console.error("Error loading country coordinates:", error);
return coordinates;
}
@@ -88,8 +87,6 @@ export default function GeographicMap({
.filter(([code]) => {
// If no coordinates found, log to help with debugging
if (!countryCoordinates[code] && !DEFAULT_COORDINATES[code]) {
- // eslint-disable-next-line no-console
- console.warn(`Missing coordinates for country code: ${code}`);
return false;
}
return true;
@@ -102,14 +99,12 @@ export default function GeographicMap({
}));
// Log for debugging
- // eslint-disable-next-line no-console
console.log(
`Found ${data.length} countries with coordinates out of ${Object.keys(countries).length} total countries`
);
setCountryData(data);
} catch (error) {
- // eslint-disable-next-line no-console
console.error("Error processing geographic data:", error);
setCountryData([]);
}
diff --git a/components/MetricCard.tsx b/components/MetricCard.tsx
index 9388599..a3fe0f2 100644
--- a/components/MetricCard.tsx
+++ b/components/MetricCard.tsx
@@ -4,7 +4,7 @@ interface MetricCardProps {
title: string;
value: string | number | null | undefined;
description?: string;
- icon?: string;
+ icon?: React.ReactNode;
trend?: {
value: number;
label?: string;
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index b50f828..45b83c3 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -2,6 +2,7 @@
import { useState } from "react";
import Link from "next/link";
+import Image from "next/image"; // Import the Next.js Image component
import { usePathname } from "next/navigation";
import { signOut } from "next-auth/react";
@@ -190,10 +191,13 @@ export default function Sidebar() {
-
{isExpanded && (
@@ -213,7 +217,7 @@ export default function Sidebar() {
{isExpanded ? "Collapse sidebar" : "Expand sidebar"}
@@ -292,8 +296,8 @@ export default function Sidebar() {
) : (
Logout
diff --git a/eslint.config.js b/eslint.config.js
index b01b0f6..c033397 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -26,13 +26,13 @@ const eslintConfig = [
"coverage/",
],
rules: {
- "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "warn",
- "react/no-unescaped-entities": "off",
- "no-console": "warn",
- "no-trailing-spaces": "error",
+ "react/no-unescaped-entities": "warn",
+ "no-console": "off",
+ "no-trailing-spaces": "warn",
"prefer-const": "error",
- "no-unused-vars": "off",
+ "no-unused-vars": "warn",
},
},
];
diff --git a/lib/metrics.ts b/lib/metrics.ts
index 56eeac9..d8b07b6 100644
--- a/lib/metrics.ts
+++ b/lib/metrics.ts
@@ -314,7 +314,7 @@ export function sessionMetrics(
const byDay: DayMetrics = {};
const byCategory: CategoryMetrics = {};
const byLanguage: LanguageMetrics = {};
- const byCountry: CountryMetrics = {}; // Added for country data
+ const byCountry: CountryMetrics = {};
const tokensByDay: DayMetrics = {};
const tokensCostByDay: DayMetrics = {};
@@ -322,49 +322,68 @@ export function sessionMetrics(
forwarded = 0;
let totalSentiment = 0,
sentimentCount = 0;
- let totalResponse = 0,
- responseCount = 0;
+ let totalResponseTimeCurrent = 0, // Renamed to avoid conflict
+ responseCountCurrent = 0; // Renamed to avoid conflict
let totalTokens = 0,
totalTokensEur = 0;
- // For sentiment distribution
let sentimentPositive = 0,
sentimentNegative = 0,
sentimentNeutral = 0;
- // Calculate total session duration in minutes
- let totalDuration = 0;
- let durationCount = 0;
+ let totalDurationCurrent = 0, // Renamed to avoid conflict
+ durationCountCurrent = 0; // Renamed to avoid conflict
- const wordCounts: { [key: string]: number } = {}; // For WordCloud
+ const wordCounts: { [key: string]: number } = {};
+ const uniqueUserIdsCurrent = new Set();
+
+ let minDateCurrentPeriod = new Date();
+ if (sessions.length > 0) {
+ minDateCurrentPeriod = new Date(
+ Math.min(...sessions.map((s) => s.startTime.getTime()))
+ );
+ }
+
+ const prevPeriodEndDate = new Date(minDateCurrentPeriod);
+ prevPeriodEndDate.setDate(prevPeriodEndDate.getDate() - 1);
+ const prevPeriodStartDate = new Date(prevPeriodEndDate);
+ prevPeriodStartDate.setDate(prevPeriodStartDate.getDate() - 6); // 7-day previous period
+
+ let prevPeriodSessionsCount = 0;
+ const prevPeriodUniqueUserIds = new Set();
+ let prevPeriodTotalDuration = 0;
+ let prevPeriodDurationCount = 0;
+ let prevPeriodTotalResponseTime = 0;
+ let prevPeriodResponseCount = 0;
sessions.forEach((s) => {
- const day = s.startTime.toISOString().slice(0, 10);
+ const sessionDate = s.startTime;
+ const day = sessionDate.toISOString().slice(0, 10);
+
+ // Aggregate current period data
byDay[day] = (byDay[day] || 0) + 1;
+ if (s.userId) {
+ uniqueUserIdsCurrent.add(s.userId);
+ }
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
+ if (s.country) byCountry[s.country] = (byCountry[s.country] || 0) + 1;
- // Process token usage by day
if (s.tokens) {
tokensByDay[day] = (tokensByDay[day] || 0) + s.tokens;
}
-
- // Process token cost by day
if (s.tokensEur) {
tokensCostByDay[day] = (tokensCostByDay[day] || 0) + s.tokensEur;
}
if (s.endTime) {
const duration =
- (s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
-
- // Sanity check: Only include sessions with reasonable durations (less than 24 hours)
- const MAX_REASONABLE_DURATION = 24 * 60; // 24 hours in minutes
+ (s.endTime.getTime() - sessionDate.getTime()) / (1000 * 60); // minutes
+ const MAX_REASONABLE_DURATION = 24 * 60;
if (duration > 0 && duration < MAX_REASONABLE_DURATION) {
- totalDuration += duration;
- durationCount++;
+ totalDurationCurrent += duration;
+ durationCountCurrent++;
}
}
@@ -374,45 +393,98 @@ export function sessionMetrics(
if (s.sentiment != null) {
totalSentiment += s.sentiment;
sentimentCount++;
-
- // Classify sentiment
- if (s.sentiment > 0.3) {
- sentimentPositive++;
- } else if (s.sentiment < -0.3) {
- sentimentNegative++;
- } else {
- sentimentNeutral++;
- }
+ if (s.sentiment > 0.3) sentimentPositive++;
+ else if (s.sentiment < -0.3) sentimentNegative++;
+ else sentimentNeutral++;
}
if (s.avgResponseTime != null) {
- totalResponse += s.avgResponseTime;
- responseCount++;
+ totalResponseTimeCurrent += s.avgResponseTime;
+ responseCountCurrent++;
}
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
+ const words = s.transcriptContent.toLowerCase().match(/\b\w+\b/g);
if (words) {
words.forEach((word) => {
- const cleanedWord = word.replace(/[^a-z0-9]/gi, ""); // Remove punctuation
+ const cleanedWord = word.replace(/[^a-z0-9]/gi, "");
if (
cleanedWord &&
!stopWords.has(cleanedWord) &&
cleanedWord.length > 2
) {
- // Check if not a stop word and length > 2
wordCounts[cleanedWord] = (wordCounts[cleanedWord] || 0) + 1;
}
});
}
}
+
+ // Aggregate previous period data (if session falls within the previous period range)
+ if (
+ sessionDate >= prevPeriodStartDate &&
+ sessionDate <= prevPeriodEndDate
+ ) {
+ prevPeriodSessionsCount++;
+ if (s.userId) {
+ prevPeriodUniqueUserIds.add(s.userId);
+ }
+ if (s.endTime) {
+ const duration =
+ (s.endTime.getTime() - sessionDate.getTime()) / (1000 * 60);
+ const MAX_REASONABLE_DURATION = 24 * 60;
+ if (duration > 0 && duration < MAX_REASONABLE_DURATION) {
+ prevPeriodTotalDuration += duration;
+ prevPeriodDurationCount++;
+ }
+ }
+ if (s.avgResponseTime != null) {
+ prevPeriodTotalResponseTime += s.avgResponseTime;
+ prevPeriodResponseCount++;
+ }
+ }
});
- // Now add sentiment alert logic:
+ const calculateTrend = (current: number, previous: number): number => {
+ if (previous === 0) {
+ return current > 0 ? 100 : 0;
+ }
+ const trend = ((current - previous) / previous) * 100;
+ return parseFloat(trend.toFixed(1));
+ };
+
+ const sessionTrend = calculateTrend(total, prevPeriodSessionsCount);
+ const usersTrend = calculateTrend(
+ uniqueUserIdsCurrent.size,
+ prevPeriodUniqueUserIds.size
+ );
+
+ const avgSessionLengthCurrent =
+ durationCountCurrent > 0 ? totalDurationCurrent / durationCountCurrent : 0;
+ const avgSessionLengthPrevious =
+ prevPeriodDurationCount > 0
+ ? prevPeriodTotalDuration / prevPeriodDurationCount
+ : 0;
+ const avgSessionTimeTrend = calculateTrend(
+ avgSessionLengthCurrent,
+ avgSessionLengthPrevious
+ );
+
+ const avgResponseTimeCurrentPeriod =
+ responseCountCurrent > 0
+ ? totalResponseTimeCurrent / responseCountCurrent
+ : 0;
+ const avgResponseTimePreviousPeriod =
+ prevPeriodResponseCount > 0
+ ? prevPeriodTotalResponseTime / prevPeriodResponseCount
+ : 0;
+ const avgResponseTimeTrend = calculateTrend(
+ avgResponseTimeCurrentPeriod,
+ avgResponseTimePreviousPeriod
+ );
+
let belowThreshold = 0;
const threshold = companyConfig.sentimentAlert ?? null;
if (threshold != null) {
@@ -421,45 +493,43 @@ export function sessionMetrics(
}
}
- // Calculate average sessions per day
const dayCount = Object.keys(byDay).length;
const avgSessionsPerDay = dayCount > 0 ? total / dayCount : 0;
- // Calculate average session length
- 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
+ .slice(0, 500);
return {
totalSessions: total,
- avgSessionsPerDay,
- avgSessionLength,
+ avgSessionsPerDay: parseFloat(avgSessionsPerDay.toFixed(1)),
+ avgSessionLength: parseFloat(avgSessionLengthCurrent.toFixed(1)),
days: byDay,
languages: byLanguage,
- categories: byCategory, // This will be empty if we are not using categories for word cloud
- countries: byCountry, // Added countries to the result
+ categories: byCategory,
+ countries: byCountry,
belowThresholdCount: belowThreshold,
- // Additional metrics not in the interface - using type assertion
escalatedCount: escalated,
forwardedCount: forwarded,
- avgSentiment: sentimentCount ? totalSentiment / sentimentCount : undefined,
- avgResponseTime: responseCount ? totalResponse / responseCount : undefined,
+ avgSentiment: sentimentCount
+ ? parseFloat((totalSentiment / sentimentCount).toFixed(2))
+ : undefined,
+ avgResponseTime: parseFloat(avgResponseTimeCurrentPeriod.toFixed(2)),
totalTokens,
totalTokensEur,
sentimentThreshold: threshold,
- lastUpdated: Date.now(), // Add current timestamp
-
- // New metrics for enhanced dashboard
+ lastUpdated: Date.now(),
sentimentPositiveCount: sentimentPositive,
sentimentNeutralCount: sentimentNeutral,
sentimentNegativeCount: sentimentNegative,
tokensByDay,
tokensCostByDay,
- wordCloudData, // Added word cloud data
+ wordCloudData,
+ uniqueUsers: uniqueUserIdsCurrent.size,
+ sessionTrend,
+ usersTrend,
+ avgSessionTimeTrend,
+ avgResponseTimeTrend,
};
}
diff --git a/lib/types.ts b/lib/types.ts
index e55da4f..de4ca14 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -131,6 +131,13 @@ export interface MetricsResult {
tokensByDay?: DayMetrics;
tokensCostByDay?: DayMetrics;
wordCloudData?: WordCloudWord[]; // Added for transcript-based word cloud
+
+ // Properties for overview page cards and trends
+ uniqueUsers?: number;
+ sessionTrend?: number; // e.g., percentage change in totalSessions
+ usersTrend?: number; // e.g., percentage change in uniqueUsers
+ avgSessionTimeTrend?: number; // e.g., percentage change in avgSessionLength
+ avgResponseTimeTrend?: number; // e.g., percentage change in avgResponseTime
}
export interface ApiResponse {
diff --git a/package.json b/package.json
index 94ff784..c1cbc4a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "livedash-node",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"type": "module",
"scripts": {
@@ -12,7 +12,8 @@
"format": "prettier --write .",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev",
- "prisma:seed": "node prisma/seed.mjs"
+ "prisma:seed": "node prisma/seed.mjs",
+ "prisma:studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^6.8.2",