From 7762061850e7e82e6aa914eb1542a3ffbe814058 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 22 May 2025 06:28:25 +0200 Subject: [PATCH] Enhances session and dashboard components with authentication checks and loading states --- app/dashboard/page.tsx | 100 +++++++-------- app/dashboard/sessions/[id]/page.tsx | 41 ++++-- components/TranscriptViewer.tsx | 5 +- package-lock.json | 180 ++++++++++++++++++++++++++- package.json | 3 +- 5 files changed, 261 insertions(+), 68 deletions(-) diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index cc48f5b..65570c5 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { signOut, useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; // Import useRouter import { SessionsLineChart, CategoriesBarChart, @@ -20,29 +21,36 @@ import WelcomeBanner from "../../components/WelcomeBanner"; // Safely wrapped component with useSession function DashboardContent() { - const { data: session } = useSession(); + const { data: session, status } = useSession(); // Add status from useSession + const router = useRouter(); // Initialize useRouter const [metrics, setMetrics] = useState(null); const [company, setCompany] = useState(null); const [, setLoading] = useState(false); - // Remove unused csvUrl state variable const [refreshing, setRefreshing] = useState(false); const isAdmin = session?.user?.role === "admin"; const isAuditor = session?.user?.role === "auditor"; useEffect(() => { - // Fetch metrics and company on mount - const fetchData = async () => { - setLoading(true); - const res = await fetch("/api/dashboard/metrics"); - const data = await res.json(); - setMetrics(data.metrics); - setCompany(data.company); - // Removed unused csvUrl assignment - setLoading(false); - }; - fetchData(); - }, []); + // Redirect if not authenticated + if (status === "unauthenticated") { + router.push("/login"); + return; // Stop further execution in this effect + } + + // Fetch metrics and company on mount if authenticated + if (status === "authenticated") { + const fetchData = async () => { + setLoading(true); + const res = await fetch("/api/dashboard/metrics"); + const data = await res.json(); + setMetrics(data.metrics); + setCompany(data.company); + setLoading(false); + }; + fetchData(); + } + }, [status, router]); // Add status and router to dependency array async function handleRefresh() { if (isAuditor) return; // Prevent auditors from refreshing @@ -51,7 +59,6 @@ function DashboardContent() { // Make sure we have a company ID to send if (!company?.id) { - // Use a more appropriate error handling approach setRefreshing(false); alert("Cannot refresh: Company ID is missing"); return; @@ -70,7 +77,6 @@ function DashboardContent() { setMetrics(data.metrics); } else { const errorData = await res.json(); - // Use alert instead of console.error for user feedback alert(`Failed to refresh sessions: ${errorData.error}`); } } finally { @@ -82,7 +88,6 @@ function DashboardContent() { const getSentimentData = () => { if (!metrics) return { positive: 0, neutral: 0, negative: 0 }; - // If we have the new sentiment count fields, use those if ( metrics.sentimentPositiveCount !== undefined && metrics.sentimentNeutralCount !== undefined && @@ -95,12 +100,11 @@ function DashboardContent() { }; } - // Fallback to estimating based on total const total = metrics.totalSessions || 1; return { - positive: Math.round(total * 0.6), // 60% positive as fallback - neutral: Math.round(total * 0.3), // 30% neutral as fallback - negative: Math.round(total * 0.1), // 10% negative as fallback + positive: Math.round(total * 0.6), + neutral: Math.round(total * 0.3), + negative: Math.round(total * 0.1), }; }; @@ -111,7 +115,6 @@ function DashboardContent() { } const days = Object.keys(metrics.tokensByDay).sort(); - // Get the last 7 days if available const labels = days.slice(-7); const values = labels.map((day) => metrics.tokensByDay?.[day] || 0); const costs = labels.map((day) => metrics.tokensCostByDay?.[day] || 0); @@ -119,6 +122,16 @@ function DashboardContent() { return { labels, values, costs }; }; + // Show loading state while session status is being determined + if (status === "loading") { + return
Loading session...
; + } + + // If unauthenticated and not redirected yet (should be handled by useEffect, but as a fallback) + if (status === "unauthenticated") { + return
Redirecting to login...
; + } + if (!metrics || !company) { return
Loading dashboard...
; } @@ -130,12 +143,11 @@ function DashboardContent() { .map(([text, value]) => ({ text, value })) .filter((item) => item.text.trim() !== "") .sort((a, b) => b.value - a.value) - .slice(0, 30); // Limit to top 30 categories + .slice(0, 30); }; // Function to prepare country data for the map - using simulated/dummy data const getCountryData = () => { - // Use dummy country data as the actual metrics doesn't contain session-level country data return { US: 42, GB: 25, @@ -156,14 +168,10 @@ function DashboardContent() { // Function to prepare response time distribution data const getResponseTimeData = () => { - // Since we have aggregated avgResponseTime, we'll create a simulated distribution - // based on the average response time const avgTime = metrics.avgResponseTime || 1.5; const simulatedData: number[] = []; - // Generate response times that average to our avgResponseTime for (let i = 0; i < 50; i++) { - // Random value that's mostly close to the average const randomFactor = 0.5 + Math.random(); simulatedData.push(avgTime * randomFactor); } @@ -173,30 +181,20 @@ function DashboardContent() { return (
- {" "} - {/* Increased spacing */} - {/* Welcome Banner */} - {/* Header with company info */}

{company.name}

- {" "} - {/* Adjusted text color and margin */} Dashboard updated{" "} - {" "} - {/* Adjusted text color */} {new Date(metrics.lastUpdated || Date.now()).toLocaleString()}

- {" "} - {/* Adjusted gap and responsive margin */}
- {/* Key Performance Metrics */}
- {/* Sentiment & Escalation Metrics */}

@@ -318,9 +314,9 @@ function DashboardContent() { getSentimentData().negative, ], colors: [ - "rgba(34, 197, 94, 0.8)", // green - "rgba(249, 115, 22, 0.8)", // orange - "rgba(239, 68, 68, 0.8)", // red + "rgba(34, 197, 94, 0.8)", + "rgba(249, 115, 22, 0.8)", + "rgba(239, 68, 68, 0.8)", ], }} centerText={{ @@ -367,7 +363,6 @@ function DashboardContent() {

- {/* Charts Row */}

@@ -382,7 +377,6 @@ function DashboardContent() {

- {/* Word Cloud and World Map */}

@@ -397,7 +391,6 @@ function DashboardContent() {

- {/* Response Time Distribution and Language Distribution */}

@@ -413,7 +406,6 @@ function DashboardContent() {

- {/* Token Usage */}

@@ -432,7 +424,6 @@ function DashboardContent() {

- {/* Admin Controls */} {isAdmin && ( <> @@ -445,14 +436,9 @@ function DashboardContent() { // Our exported component export default function DashboardPage() { - // We don't use useSession here to avoid the error outside the provider return (
- {" "} - {/* Added gradient background */}
- {" "} - {/* Added inner container for content alignment */}
diff --git a/app/dashboard/sessions/[id]/page.tsx b/app/dashboard/sessions/[id]/page.tsx index 68dc6b5..e29f192 100644 --- a/app/dashboard/sessions/[id]/page.tsx +++ b/app/dashboard/sessions/[id]/page.tsx @@ -1,7 +1,8 @@ "use client"; import { useEffect, useState } from "react"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; // Import useRouter +import { useSession } from "next-auth/react"; // Import useSession import SessionDetails from "../../../../components/SessionDetails"; import TranscriptViewer from "../../../../components/TranscriptViewer"; import { ChatSession } from "../../../../lib/types"; @@ -9,15 +10,22 @@ import Link from "next/link"; export default function SessionViewPage() { const params = useParams(); + const router = useRouter(); // Initialize useRouter + const { status } = useSession(); // Get session status, removed unused sessionData const id = params?.id as string; const [session, setSession] = useState(null); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(true); // This will now primarily be for data fetching const [error, setError] = useState(null); useEffect(() => { - if (id) { + if (status === "unauthenticated") { + router.push("/login"); + return; + } + + if (status === "authenticated" && id) { const fetchSession = async () => { - setLoading(true); + if (!session) setLoading(true); setError(null); try { const response = await fetch(`/api/dashboard/session/${id}`); @@ -40,10 +48,29 @@ export default function SessionViewPage() { } }; fetchSession(); + } else if (status === "authenticated" && !id) { + setError("Session ID is missing."); + setLoading(false); } - }, [id]); + }, [id, status, router, session]); - if (loading) { + if (status === "loading") { + return ( +
+

Loading session...

+
+ ); + } + + if (status === "unauthenticated") { + return ( +
+

Redirecting to login...

+
+ ); + } + + if (loading && status === "authenticated") { return (

Loading session details...

@@ -112,8 +139,6 @@ export default function SessionViewPage() { {session.transcriptContent && session.transcriptContent.trim() !== "" ? (
- {" "} - {/* Adjusted margin, TranscriptViewer has its own top margin */} =10.13.0" } }, + "node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", @@ -4896,6 +4909,64 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -4923,6 +4994,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -4936,6 +5036,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -4946,6 +5063,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/i18n-iso-countries": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", @@ -7169,6 +7296,18 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7523,6 +7662,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -8680,6 +8834,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", @@ -8694,6 +8862,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", diff --git a/package.json b/package.json index f9333f7..a9f5aa1 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "react-chartjs-2": "^5.0.0", "react-dom": "^19.1.0", "react-leaflet": "^5.0.0", - "react-markdown": "^10.1.0" + "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1",