From 017634f7a82a71e3cc808f2b935925e1f8469cef Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Sat, 28 Jun 2025 04:40:30 +0200 Subject: [PATCH] feat: make filters & sorting section collapsible - Add collapsible filters section to save space on sessions page - Show/hide toggle button with chevron icons for better UX - Filters start collapsed by default for cleaner initial view - Improves mobile experience by reducing vertical space usage --- app/dashboard/company/page.tsx | 231 ++++++++----- app/dashboard/sessions/[id]/page.tsx | 290 ++++++++++++---- app/dashboard/sessions/page.tsx | 482 ++++++++++++++++----------- app/globals.css | 16 +- components/SessionDetails.tsx | 298 +++++++++-------- 5 files changed, 814 insertions(+), 503 deletions(-) diff --git a/app/dashboard/company/page.tsx b/app/dashboard/company/page.tsx index a119edd..ef52c8c 100644 --- a/app/dashboard/company/page.tsx +++ b/app/dashboard/company/page.tsx @@ -3,6 +3,12 @@ import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; import { Company } from "../../../lib/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { ShieldX, Settings, Save, Database, Bell } from "lucide-react"; export default function CompanySettingsPage() { const { data: session, status } = useSession(); @@ -74,110 +80,161 @@ export default function CompanySettingsPage() { // Loading state if (loading) { - return
Loading settings...
; + return ( +
+ + +
+ + Company Settings +
+
+ +
+ Loading settings... +
+
+
+
+ ); } // Check for ADMIN access if (session?.user?.role !== "ADMIN") { return ( -
-

Access Denied

-

You don't have permission to view company settings.

+
+ + +
+ + Access Denied +
+
+ +
+

+ You don't have permission to view company settings. +

+
+
+
); } return (
-
-

- Company Settings -

+ + +
+ + Company Settings +
+
+ + {message && ( + + {message} + + )} - {message && ( -
{ + e.preventDefault(); + handleSave(); + }} + autoComplete="off" > - {message} -
- )} +
+ + +
+ + Data Source Configuration +
+
+ +
+ + setCsvUrl(e.target.value)} + placeholder="https://example.com/data.csv" + autoComplete="off" + /> +
-
{ - e.preventDefault(); - handleSave(); - }} - autoComplete="off" - > -
- - setCsvUrl(e.target.value)} - placeholder="https://example.com/data.csv" - autoComplete="off" - /> -
+
+ + setCsvUsername(e.target.value)} + placeholder="Username for CSV access (if needed)" + autoComplete="off" + /> +
-
- - setCsvUsername(e.target.value)} - placeholder="Username for CSV access (if needed)" - autoComplete="off" - /> -
+
+ + setCsvPassword(e.target.value)} + placeholder="Password will be updated only if provided" + autoComplete="new-password" + /> +

+ Leave blank to keep current password +

+
+ + -
- - setCsvPassword(e.target.value)} - placeholder="Password will be updated only if provided" - autoComplete="new-password" - /> -

- Leave blank to keep current password -

-
+ + +
+ + Alert Configuration +
+
+ +
+ + setSentimentThreshold(e.target.value)} + placeholder="Threshold value (0-100)" + min="0" + max="100" + autoComplete="off" + /> +

+ Percentage of negative sentiment sessions to trigger alert (0-100) +

+
+
+
+
-
- - setSentimentThreshold(e.target.value)} - 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/sessions/[id]/page.tsx b/app/dashboard/sessions/[id]/page.tsx index 9348c83..a24742c 100644 --- a/app/dashboard/sessions/[id]/page.tsx +++ b/app/dashboard/sessions/[id]/page.tsx @@ -1,13 +1,29 @@ "use client"; import { useEffect, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; // Import useRouter -import { useSession } from "next-auth/react"; // Import useSession +import { useParams, useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; import SessionDetails from "../../../../components/SessionDetails"; import TranscriptViewer from "../../../../components/TranscriptViewer"; import MessageViewer from "../../../../components/MessageViewer"; import { ChatSession } from "../../../../lib/types"; import Link from "next/link"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { + ArrowLeft, + MessageSquare, + Clock, + Globe, + ExternalLink, + User, + Bot, + AlertCircle, + FileText, + Activity +} from "lucide-react"; export default function SessionViewPage() { const params = useParams(); @@ -57,110 +73,242 @@ export default function SessionViewPage() { if (status === "loading") { return ( -
-

Loading session...

+
+ + +
+ Loading session... +
+
+
); } if (status === "unauthenticated") { return ( -
-

Redirecting to login...

+
+ + +
+ Redirecting to login... +
+
+
); } if (loading && status === "authenticated") { return ( -
-

Loading session details...

+
+ + +
+ Loading session details... +
+
+
); } if (error) { return ( -
-

Error: {error}

- - Back to Sessions List - +
+ + +
+ +

Error: {error}

+ + + +
+
+
); } if (!session) { return ( -
-

Session not found.

- - Back to Sessions List - +
+ + +
+ +

Session not found.

+ + + +
+
+
); } return ( -
-
-
- - - - - Back to Sessions List - -
-

- Session: {session.sessionId || session.id} -

-
-
- +
+ {/* Header */} + + +
+
+ + + +
+

Session Details

+
+ + ID + + + {(session.sessionId || session.id).slice(0, 8)}... + +
+
+
+
+ {session.category && session.category !== 'UNRECOGNIZED_OTHER' && session.category !== 'ACCESS_LOGIN' && ( + + + {session.category.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())} + + )} + {session.language && ( + + + {session.language.toUpperCase()} + + )} + {session.sentiment && ( + + {session.sentiment.charAt(0).toUpperCase() + session.sentiment.slice(1)} + + )} +
+
+
- {/* Show parsed messages if available */} - {session.messages && session.messages.length > 0 && ( -
- + {/* Session Overview */} +
+ + +
+ +
+

Start Time

+

+ {new Date(session.startTime).toLocaleString()} +

+
- )} +
+
- {/* Show transcript URL if available */} - {session.fullTranscriptUrl && ( -
-

Source Transcript

- - View Original Transcript - + + +
+ +
+

Messages

+

+ {session.messages?.length || 0} +

+
- )} -
+ + + + + +
+ +
+

User ID

+

+ {session.userId || 'N/A'} +

+
+
+
+
+ + + +
+ +
+

Duration

+

+ {session.endTime && session.startTime + ? `${Math.round( + (new Date(session.endTime).getTime() - new Date(session.startTime).getTime()) / 60000 + )} min` + : 'N/A'} +

+
+
+
+
+ + {/* Session Details */} + + + {/* Messages */} + {session.messages && session.messages.length > 0 && ( + + + + + Conversation ({session.messages.length} messages) + + + + + + + )} + + {/* Transcript URL */} + {session.fullTranscriptUrl && ( + + + + + Source Transcript + + + + + + View Original Transcript + + + + )}
); } diff --git a/app/dashboard/sessions/page.tsx b/app/dashboard/sessions/page.tsx index 628d306..5625145 100644 --- a/app/dashboard/sessions/page.tsx +++ b/app/dashboard/sessions/page.tsx @@ -3,6 +3,24 @@ import { useState, useEffect, useCallback } from "react"; import { ChatSession } from "../../../lib/types"; import Link from "next/link"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { + MessageSquare, + Search, + Filter, + Calendar, + ChevronLeft, + ChevronRight, + Clock, + Globe, + Eye, + ChevronDown, + ChevronUp +} from "lucide-react"; // Placeholder for a SessionListItem component to be created later // For now, we'll display some basic info directly. @@ -43,6 +61,9 @@ export default function SessionsPage() { // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars const [pageSize, setPageSize] = useState(10); // Or make this configurable + // UI states + const [filtersExpanded, setFiltersExpanded] = useState(false); + useEffect(() => { const timerId = setTimeout(() => { setDebouncedSearchTerm(searchTerm); @@ -120,227 +141,282 @@ export default function SessionsPage() { }, [fetchFilterOptions]); return ( -
-

- Chat Sessions -

+
+ {/* Header */} + + +
+ + Chat Sessions +
+
+
{/* Search Input */} -
- setSearchTerm(e.target.value)} - /> -
+ + +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+
{/* Filter and Sort Controls */} -
- {/* Category Filter */} -
- - -
+ + +
+
+ + Filters & Sorting +
+ +
+
+ {filtersExpanded && ( + +
+ {/* Category Filter */} +
+ + +
- {/* Language Filter */} -
- - -
+ {/* Language Filter */} +
+ + +
- {/* Start Date Filter */} -
- - setStartDate(e.target.value)} - /> -
+ {/* Start Date Filter */} +
+ + setStartDate(e.target.value)} + /> +
- {/* End Date Filter */} -
- - setEndDate(e.target.value)} - /> -
+ {/* End Date Filter */} +
+ + setEndDate(e.target.value)} + /> +
- {/* Sort Key */} -
- - -
+ {/* Sort Key */} +
+ + +
- {/* Sort Order */} -
- - -
-
+ {/* Sort Order */} +
+ + +
+
+ + )} + - {loading &&

Loading sessions...

} - {error &&

Error: {error}

} - - {!loading && !error && sessions.length === 0 && ( -

- {debouncedSearchTerm - ? `No sessions found for "${debouncedSearchTerm}".` - : "No sessions found."} -

+ {/* Loading State */} + {loading && ( + + +
+ Loading sessions... +
+
+
)} - {!loading && !error && sessions.length > 0 && ( -
- {sessions.map((session) => ( -
-

- Session ID: {session.sessionId || session.id} -

-

- Start Time{/* (Local) */}:{" "} - {new Date(session.startTime).toLocaleString()} -

- {/*

- Start Time (Raw API): {session.startTime.toString()} -

*/} - {session.category && ( -

- Category:{" "} - {session.category} -

- )} - {session.language && ( -

- Language:{" "} - - {session.language.toUpperCase()} - -

- )} - {session.initialMsg && ( -

- Initial Message: {session.initialMsg} -

- )} - - View Details - + {/* Error State */} + {error && ( + + +
+ Error: {error}
+
+
+ )} + + {/* Empty State */} + {!loading && !error && sessions.length === 0 && ( + + +
+ {debouncedSearchTerm + ? `No sessions found for "${debouncedSearchTerm}".` + : "No sessions found."} +
+
+
+ )} + + {/* Sessions List */} + {!loading && !error && sessions.length > 0 && ( +
+ {sessions.map((session) => ( + + +
+
+
+ + ID + + + {(session.sessionId || session.id).slice(0, 8)}... + +
+
+ + {new Date(session.startTime).toLocaleString()} +
+
+ + + +
+ +
+ {session.category && session.category !== 'UNRECOGNIZED_OTHER' && session.category !== 'ACCESS_LOGIN' && ( + + + {session.category.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase())} + + )} + {session.language && ( + + + {session.language.toUpperCase()} + + )} +
+ + {session.summary ? ( +

+ {session.summary} +

+ ) : session.initialMsg ? ( +

+ {session.initialMsg} +

+ ) : null} +
+
))}
)} + {/* Pagination */} {totalPages > 0 && ( -
- - - Page {currentPage} of {totalPages} - - -
+ + +
+ + + Page {currentPage} of {totalPages} + + +
+
+
)}
); diff --git a/app/globals.css b/app/globals.css index d5134eb..2ef545f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -171,12 +171,20 @@ /* Custom text selection colors */ ::selection { - background-color: hsl(var(--primary) / 0.2); - color: hsl(var(--foreground)); + background-color: hsl(var(--primary) / 0.3); + color: hsl(var(--primary-foreground)); } ::-moz-selection { - background-color: hsl(var(--primary) / 0.2); - color: hsl(var(--foreground)); + background-color: hsl(var(--primary) / 0.3); + color: hsl(var(--primary-foreground)); + } + + /* Line clamp utility */ + .line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } } \ No newline at end of file diff --git a/components/SessionDetails.tsx b/components/SessionDetails.tsx index acdd646..103add5 100644 --- a/components/SessionDetails.tsx +++ b/components/SessionDetails.tsx @@ -3,6 +3,10 @@ import { ChatSession } from "../lib/types"; import LanguageDisplay from "./LanguageDisplay"; import CountryDisplay from "./CountryDisplay"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { ExternalLink } from "lucide-react"; interface SessionDetailsProps { session: ChatSession; @@ -12,157 +16,175 @@ interface SessionDetailsProps { * Component to display session details with formatted country and language names */ export default function SessionDetails({ session }: SessionDetailsProps) { + // Helper function to format category names + const formatCategory = (category: string) => { + if (category === 'UNRECOGNIZED_OTHER' || category === 'ACCESS_LOGIN') { + return null; // Don't show these internal enum values + } + return category.replace(/_/g, ' ').toLowerCase().replace(/\b\w/g, l => l.toUpperCase()); + }; + return ( -
-

Session Details

-
-
- Session ID: - {session.id} -
- -
- Start Time: - - {new Date(session.startTime).toLocaleString()} - -
- - {session.endTime && ( -
- End Time: - - {new Date(session.endTime).toLocaleString()} - -
- )} - - {session.category && ( -
- Category: - {session.category} -
- )} - - {session.language && ( -
- Language: - - - - ({session.language.toUpperCase()}) - - -
- )} - - {session.country && ( -
- Country: - - - - ({session.country}) - - -
- )} - - {session.sentiment !== null && session.sentiment !== undefined && ( -
- Sentiment: - - {session.sentiment.toLowerCase()} - -
- )} - -
- Messages Sent: - {session.messagesSent || 0} -
- - {session.avgResponseTime !== null && - session.avgResponseTime !== undefined && ( -
- Avg Response Time: - - {session.avgResponseTime.toFixed(2)}s - + + + Session Information + + +
+
+
+

Session ID

+ + {session.id.slice(0, 8)}... +
- )} - {session.escalated !== null && session.escalated !== undefined && ( -
- Escalated: - - {session.escalated ? "Yes" : "No"} - -
- )} - - {session.forwardedHr !== null && session.forwardedHr !== undefined && ( -
- Forwarded to HR: - - {session.forwardedHr ? "Yes" : "No"} - -
- )} - - {session.ipAddress && ( -
- IP Address: - - {session.ipAddress} - -
- )} - - {session.initialMsg && ( -
- Initial Message: -
- "{session.initialMsg}" +
+

Start Time

+

+ {new Date(session.startTime).toLocaleString()} +

+ + {session.endTime && ( +
+

End Time

+

+ {new Date(session.endTime).toLocaleString()} +

+
+ )} + + {session.category && formatCategory(session.category) && ( +
+

Category

+ + {formatCategory(session.category)} + +
+ )} + + {session.language && ( +
+

Language

+
+ + + {session.language.toUpperCase()} + +
+
+ )} + + {session.country && ( +
+

Country

+
+ + + {session.country} + +
+
+ )}
- )} + +
+ {session.sentiment !== null && session.sentiment !== undefined && ( +
+

Sentiment

+ + {session.sentiment.charAt(0).toUpperCase() + session.sentiment.slice(1)} + +
+ )} + +
+

Messages Sent

+

{session.messagesSent || 0}

+
+ + {session.avgResponseTime !== null && session.avgResponseTime !== undefined && ( +
+

Avg Response Time

+

{session.avgResponseTime.toFixed(2)}s

+
+ )} + + {session.escalated !== null && session.escalated !== undefined && ( +
+

Escalated

+ + {session.escalated ? "Yes" : "No"} + +
+ )} + + {session.forwardedHr !== null && session.forwardedHr !== undefined && ( +
+

Forwarded to HR

+ + {session.forwardedHr ? "Yes" : "No"} + +
+ )} + + {session.ipAddress && ( +
+

IP Address

+ + {session.ipAddress} + +
+ )} +
+
+ + {(session.summary || session.initialMsg) && } {session.summary && ( -
- AI Summary: -
+
+

AI Summary

+
{session.summary}
)} - {session.fullTranscriptUrl && ( -
- Transcript: - - View Full Transcript - + {!session.summary && session.initialMsg && ( +
+

Initial Message

+
+ "{session.initialMsg}" +
)} -
-
+ + {session.fullTranscriptUrl && ( + <> + + + + )} + + ); }