Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
629 changes: 629 additions & 0 deletions frontend/src/components/AdminChatsTable.tsx

Large diffs are not rendered by default.

220 changes: 165 additions & 55 deletions frontend/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react"
import { useNavigate } from "@tanstack/react-router"
import {
Users,
Activity,
Expand Down Expand Up @@ -37,6 +38,7 @@ import {
Area,
AreaChart,
} from "recharts"
import type { AdminChat } from "@/components/AdminChatsTable"

interface Chat {
externalId: string
Expand Down Expand Up @@ -1507,10 +1509,14 @@ const AdminUsersLeaderboard = ({
users,
loading = false,
onUserClick,
onAllChatsClick,
onUserChatsClick,
}: {
users: AdminUserUsage[]
loading?: boolean
onUserClick?: (user: AdminUserUsage) => void
onAllChatsClick?: () => void
onUserChatsClick?: (userId: number, userName: string) => void
}) => {
const [searchQuery, setSearchQuery] = useState<string>("")
const [feedbackModal, setFeedbackModal] = useState<{
Expand All @@ -1535,6 +1541,11 @@ const AdminUsersLeaderboard = ({
})
}

const handleViewChatsClick = (e: React.MouseEvent, user: AdminUserUsage) => {
e.stopPropagation()
onUserChatsClick?.(user.userId, user.userName)
}

const closeFeedbackModal = () => {
setFeedbackModal({
isOpen: false,
Expand Down Expand Up @@ -1579,11 +1590,24 @@ const AdminUsersLeaderboard = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Users Leaderboard
</CardTitle>
<CardDescription>All active users by message count</CardDescription>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Users Leaderboard
</CardTitle>
<CardDescription>All active users by message count</CardDescription>
</div>
{onAllChatsClick && (
<button
onClick={onAllChatsClick}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors flex items-center gap-2"
>
<MessageSquare className="h-4 w-4" />
All Chats
</button>
)}
</div>
<div className="mt-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
Expand Down Expand Up @@ -1687,15 +1711,26 @@ const AdminUsersLeaderboard = ({
<span>{user.dislikes}</span>
</div>
</div>
{(user.likes > 0 || user.dislikes > 0) && (
<button
onClick={(e) => handleFeedbackClick(e, user)}
className="ml-3 px-3 py-1.5 text-xs text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50 hover:border-blue-300 transition-colors font-medium flex items-center gap-1"
>
<MessageSquare className="h-3 w-3" />
Feedbacks
</button>
)}
<div className="flex items-center gap-2">
{onUserChatsClick && (
<button
onClick={(e) => handleViewChatsClick(e, user)}
className="px-3 py-1.5 text-xs text-green-600 border border-green-200 rounded-lg hover:bg-green-50 hover:border-green-300 transition-colors font-medium flex items-center gap-1"
>
<MessageSquare className="h-3 w-3" />
View Chats
</button>
)}
{(user.likes > 0 || user.dislikes > 0) && (
<button
onClick={(e) => handleFeedbackClick(e, user)}
className="px-3 py-1.5 text-xs text-blue-600 border border-blue-200 rounded-lg hover:bg-blue-50 hover:border-blue-300 transition-colors font-medium flex items-center gap-1"
>
<MessageSquare className="h-3 w-3" />
Feedbacks
</button>
)}
</div>
</div>
</div>
)
Expand Down Expand Up @@ -2155,10 +2190,12 @@ const UserDetailPage = ({
user,
onBack,
timeRange,
navigate,
}: {
user: AdminUserUsage
onBack: () => void
timeRange: "today" | "1w" | "1m" | "3m" | "all"
navigate: ReturnType<typeof useNavigate>
}) => {
const [userChats, setUserChats] = useState<any[]>([])
const [agentLeaderboard, setAgentLeaderboard] = useState<
Expand Down Expand Up @@ -2216,7 +2253,7 @@ const UserDetailPage = ({
const leaderboardData = await leaderboardResponse.json()

setUserChats(
chats.filter(
chats.data.filter(
(chat: any) =>
chat.userId === user.userId || chat.user?.id === user.userId,
),
Expand Down Expand Up @@ -2391,48 +2428,64 @@ const UserDetailPage = ({
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4 mb-10">
<button
onClick={onBack}
className="flex items-center justify-center w-10 h-10 text-muted-foreground hover:text-foreground bg-muted hover:bg-muted/80 rounded-lg transition-all duration-200"
title="Back to Users"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>

<div className="flex items-center justify-between mb-10">
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-12 h-12 text-xl font-bold text-white bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl">
{user.userName.charAt(0).toUpperCase()}
</div>
<div>
<h1 className="text-2xl font-bold">{user.userName}</h1>
<p className="text-muted-foreground">{user.userEmail}</p>
<Badge
variant={
user.role === "SuperAdmin"
? "destructive"
: user.role === "Admin"
? "default"
: "secondary"
}
className="mt-1"
<button
onClick={onBack}
className="flex items-center justify-center w-10 h-10 text-muted-foreground hover:text-foreground bg-muted hover:bg-muted/80 rounded-lg transition-all duration-200"
title="Back to Users"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{user.role}
</Badge>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>

<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-12 h-12 text-xl font-bold text-white bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl">
{user.userName.charAt(0).toUpperCase()}
</div>
<div>
<h1 className="text-2xl font-bold">{user.userName}</h1>
<p className="text-muted-foreground">{user.userEmail}</p>
<Badge
variant={
user.role === "SuperAdmin"
? "destructive"
: user.role === "Admin"
? "default"
: "secondary"
}
className="mt-1"
>
{user.role}
</Badge>
</div>
</div>
</div>

{/* View Chats Button */}
<button
onClick={() => {
navigate({
to: "/admin/chat-overview" as const,
search: { userName: user.userName },
})
}}
className="px-4 py-2 text-sm font-medium text-green-600 border border-green-200 rounded-lg hover:bg-green-50 hover:border-green-300 transition-colors flex items-center gap-2"
>
<MessageSquare className="h-4 w-4" />
View Chats
</button>
</div>

{/* User Stats */}
Expand Down Expand Up @@ -2604,6 +2657,7 @@ export const Dashboard = ({
role?: string
isAgentMode?: boolean
} = {}) => {
const navigate = useNavigate()
const [stats, setStats] = useState<DashboardStats>({
totalChats: 0,
totalMessages: 0,
Expand Down Expand Up @@ -2667,6 +2721,7 @@ export const Dashboard = ({
feedbackMessages: [],
},
})
const [_, setAdminChats] = useState<AdminChat[]>([])
const [adminLoading, setAdminLoading] = useState(false)
const [adminError, setAdminError] = useState<string | null>(null)
const [selectedUser, setSelectedUser] = useState<AdminUserUsage | null>(null)
Expand Down Expand Up @@ -2853,16 +2908,59 @@ export const Dashboard = ({
throw new Error("Failed to fetch admin data")
}

const adminChats = await adminChatsResponse.json()
const adminChatsData = await adminChatsResponse.json()
const adminAgents = await adminAgentsResponse.json()

// Handle both new pagination format and old format for backward compatibility
let chatsArray: any[]
if (
adminChatsData &&
typeof adminChatsData === "object" &&
"data" in adminChatsData &&
"pagination" in adminChatsData
) {
// New format with pagination metadata
chatsArray = adminChatsData.data
} else if (Array.isArray(adminChatsData)) {
// Old format - direct array
chatsArray = adminChatsData
} else {
throw new Error("Invalid response format from admin chats API")
}

// Process admin stats
const processedAdminStats = processAdminChatsData(
adminChats,
chatsArray,
adminAgents,
timeRange,
)
setAdminStats(processedAdminStats)

// Set the raw admin chats data for the table
setAdminChats(
chatsArray.map((chat: any) => ({
externalId: chat.externalId,
title: chat.title || "Untitled Chat",
createdAt: chat.createdAt,
userId: chat.userId || chat.user?.id,
userName: chat.userName || chat.user?.name || "Unknown User",
userEmail: chat.userEmail || chat.user?.email || "",
agentId: chat.agentId,
agentName:
chat.agentName ||
(chat.agentId
? adminAgents.find((a: any) => a.externalId === chat.agentId)
?.name
: null),
messageCount: chat.messageCount || chat.messages?.length || 0,
totalCost: chat.totalCost || 0,
totalTokens: chat.totalTokens || 0,
likes: chat.likes || 0,
dislikes: chat.dislikes || 0,
isBookmarked: chat.isBookmarked || false,
})),
)

setAdminError(null)
} catch (err) {
console.error("Error fetching admin data:", err)
Expand Down Expand Up @@ -3816,6 +3914,7 @@ export const Dashboard = ({
user={selectedUser}
onBack={() => handleAdminUserSelect(selectedUser)}
timeRange={timeRange}
navigate={navigate}
/>
</div>
) : selectedAgent ? (
Expand Down Expand Up @@ -3974,6 +4073,17 @@ export const Dashboard = ({
users={adminStats.topUsers}
loading={adminLoading}
onUserClick={handleAdminUserSelect}
onAllChatsClick={() => {
// Navigate to all chats view
navigate({ to: "/admin/chat-overview" as const })
}}
onUserChatsClick={(userId: number, userName: string) => {
// Navigate to user-specific chats view
navigate({
to: "/admin/chat-overview" as const,
search: { userName: userName },
})
}}
/>

{/* Top Agents System-wide */}
Expand Down
Loading