mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:52:08 +01:00
Refactor components and enhance metrics calculations:
- Update access denied messages to use HTML entities. - Add autoComplete attributes to forms for better user experience. - Improve trend calculations in sessionMetrics function. - Update MetricCard props to accept React nodes for icons. - Integrate Next.js Image component in Sidebar for optimization. - Adjust ESLint rules for better code quality. - Add new properties for trends in MetricsResult interface. - Bump version to 0.2.0 in package.json.
This commit is contained in:
@ -77,7 +77,7 @@ export default function CompanySettingsPage() {
|
||||
return (
|
||||
<div className="text-center py-10 bg-white rounded-xl shadow p-6">
|
||||
<h2 className="font-bold text-xl text-red-600 mb-2">Access Denied</h2>
|
||||
<p>You don't have permission to view company settings.</p>
|
||||
<p>You don't have permission to view company settings.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -103,6 +103,7 @@ export default function CompanySettingsPage() {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
}}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<label className="font-medium text-gray-700">
|
||||
@ -114,6 +115,7 @@ export default function CompanySettingsPage() {
|
||||
value={csvUrl}
|
||||
onChange={(e) => setCsvUrl(e.target.value)}
|
||||
placeholder="https://example.com/data.csv"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
<p className="text-sm text-gray-500">
|
||||
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"
|
||||
/>
|
||||
<p className="text-sm text-gray-500">
|
||||
Percentage of negative sentiment sessions to trigger alert (0-100)
|
||||
|
||||
@ -246,12 +246,12 @@ function DashboardContent() {
|
||||
</svg>
|
||||
}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
<MetricCard
|
||||
@ -274,12 +274,12 @@ function DashboardContent() {
|
||||
</svg>
|
||||
}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
<MetricCard
|
||||
@ -302,12 +302,12 @@ function DashboardContent() {
|
||||
</svg>
|
||||
}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
<MetricCard
|
||||
@ -330,12 +330,12 @@ function DashboardContent() {
|
||||
</svg>
|
||||
}
|
||||
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
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -345,7 +345,7 @@ function DashboardContent() {
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Sessions Over Time
|
||||
</h3>
|
||||
<SessionsLineChart sessions={metrics.sessionsByDate || {}} />
|
||||
<SessionsLineChart sessionsPerDay={metrics.days} />
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
|
||||
@ -74,7 +74,7 @@ export default function UserManagementPage() {
|
||||
return (
|
||||
<div className="text-center py-10 bg-white rounded-xl shadow p-6">
|
||||
<h2 className="font-bold text-xl text-red-600 mb-2">Access Denied</h2>
|
||||
<p>You don't have permission to view user management.</p>
|
||||
<p>You don't have permission to view user management.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -102,6 +102,7 @@ export default function UserManagementPage() {
|
||||
e.preventDefault();
|
||||
inviteUser();
|
||||
}}
|
||||
autoComplete="off" // Disable autofill for the form
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<label className="font-medium text-gray-700">Email</label>
|
||||
@ -112,6 +113,7 @@ export default function UserManagementPage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoComplete="off" // Disable autofill for this input
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -44,7 +44,6 @@ const getCountryCoordinates = (): Record<string, [number, number]> => {
|
||||
|
||||
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([]);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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() {
|
||||
<div
|
||||
className={`relative ${isExpanded ? "w-16" : "w-10"} aspect-square mb-1`}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src="/favicon.svg"
|
||||
alt="LiveDash Logo"
|
||||
className="w-full h-full transition-all duration-300"
|
||||
fill
|
||||
style={{ objectFit: "contain" }}
|
||||
className="transition-all duration-300"
|
||||
priority // Added priority prop for LCP optimization
|
||||
/>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
@ -213,7 +217,7 @@ export default function Sidebar() {
|
||||
<div
|
||||
className="fixed ml-6 w-auto p-2 min-w-max rounded-md shadow-md text-xs font-medium
|
||||
text-white bg-gray-800 z-50
|
||||
invisible opacity-0 -translate-x-3 transition-all
|
||||
invisible opacity-0 -translate-x-3 transition-all
|
||||
group-hover:visible group-hover:opacity-100 group-hover:translate-x-0"
|
||||
>
|
||||
{isExpanded ? "Collapse sidebar" : "Expand sidebar"}
|
||||
@ -292,8 +296,8 @@ export default function Sidebar() {
|
||||
) : (
|
||||
<div
|
||||
className="fixed ml-6 w-auto p-2 min-w-max rounded-md shadow-md text-xs font-medium
|
||||
text-white bg-gray-800 z-50
|
||||
invisible opacity-0 -translate-x-3 transition-all
|
||||
text-white bg-gray-800 z-50
|
||||
invisible opacity-0 -translate-x-3 transition-all
|
||||
group-hover:visible group-hover:opacity-100 group-hover:translate-x-0"
|
||||
>
|
||||
Logout
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
176
lib/metrics.ts
176
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<string>();
|
||||
|
||||
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<string>();
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<T> {
|
||||
|
||||
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user