From 563c420f685431c0e462aafe9516c4488bc9e7eb Mon Sep 17 00:00:00 2001 From: Chinmay Singh Date: Wed, 22 Oct 2025 12:13:43 +0530 Subject: [PATCH 1/8] feat: XYNE-206 added a chats overview for admins --- frontend/src/components/AdminChatsTable.tsx | 621 ++++++++++++++++++ frontend/src/components/Dashboard.tsx | 107 ++- .../_authenticated/admin/chat-overview.tsx | 273 ++++++++ server/api/admin.ts | 156 +++-- server/db/message.ts | 36 +- server/server.ts | 28 +- 6 files changed, 1145 insertions(+), 76 deletions(-) create mode 100644 frontend/src/components/AdminChatsTable.tsx create mode 100644 frontend/src/routes/_authenticated/admin/chat-overview.tsx diff --git a/frontend/src/components/AdminChatsTable.tsx b/frontend/src/components/AdminChatsTable.tsx new file mode 100644 index 000000000..b70f66eb6 --- /dev/null +++ b/frontend/src/components/AdminChatsTable.tsx @@ -0,0 +1,621 @@ +import { useState, useEffect } from "react" +import { + MessageSquare, + Bot, + ThumbsUp, + ThumbsDown, + Search, + Eye, + Calendar, + Filter, + X, + User, + Clock, + Hash, + Loader2, + Users, +} from "lucide-react" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { formatCostInINR } from "@/lib/utils" +import { api } from "@/api" + +export interface AdminChat { + externalId: string + title: string + createdAt: string + userId: number + userName: string + userEmail: string + agentId?: string | null + agentName?: string | null + messageCount: number + totalCost: number + totalTokens: number + likes: number + dislikes: number + isBookmarked: boolean +} + +export interface AdminUser { + id: number + email: string + name: string + role: string + createdAt: string + lastLogin?: string | null + isActive: boolean + totalChats: number + totalMessages: number + likes: number + dislikes: number + totalCost: number + totalTokens: number +} + +// The API returns a simple array of strings (user message content) +type ChatMessages = string[] + +interface ChatViewDialogProps { + isOpen: boolean + onClose: () => void + chat: AdminChat | null +} + +const ChatViewDialog = ({ isOpen, onClose, chat }: ChatViewDialogProps) => { + const [messages, setMessages] = useState([]) + const [loadingMessages, setLoadingMessages] = useState(false) + const [messagesError, setMessagesError] = useState(null) + + const fetchChatMessages = async (chatId: string) => { + try { + setLoadingMessages(true) + setMessagesError(null) + + const response = await api.admin.chat.queries[chatId].$get() + + if (!response.ok) { + throw new Error("Failed to fetch chat messages") + } + + const data = await response.json() + + if (data.success) { + setMessages(data.data || []) + } else { + setMessagesError(data.message || "Failed to load messages") + } + } catch (error) { + console.error("Error fetching chat messages:", error) + setMessagesError("Failed to load messages. Please try again.") + } finally { + setLoadingMessages(false) + } + } + + // Fetch messages when dialog opens and chat changes + useEffect(() => { + if (isOpen && chat) { + fetchChatMessages(chat.externalId) + } else { + // Clear messages when dialog closes + setMessages([]) + setMessagesError(null) + } + }, [isOpen, chat]) + + if (!chat) return null + + return ( + + + +
+ + Chat Details + + +
+
+ +
+ {/* Chat Title & Status */} +
+

{chat.title}

+ {chat.agentId && ( + + + {chat.agentName || "Agent Chat"} + + )} + {chat.isBookmarked && Bookmarked} +
+ + {/* User Information */} + + + + + User Information + + + +
+ Name: {chat.userName} |{" "} + Email: {chat.userEmail} +
+
+
+ {/* Agent Information (if applicable) */} + {chat.agentId && ( + + + + + Agent Information + + + +
+ Agent Name: + + {chat.agentName || "Unknown Agent"} + +
+
+ Agent ID: + + {chat.agentId} + +
+
+
+ )} + + {/* Chat Messages */} + + + + + Messages + + + + {loadingMessages ? ( +
+ + + Loading messages... + +
+ ) : messagesError ? ( +
+
+ Error loading messages +
+
+ {messagesError} +
+ +
+ ) : messages.length === 0 ? ( +
+ +

No messages found in this chat

+
+ ) : ( +
+
+ + Showing user messages only ({messages.length} messages) +
+ {messages.map((message, index) => ( +
+
+
+ {message} +
+
+
+ ))} +
+ )} +
+
+
+
+
+ ) +} + +interface AdminChatsTableProps { + chats: AdminChat[] + loading?: boolean + onChatView?: (chat: AdminChat) => void +} + +export const AdminChatsTable = ({ + chats, + loading = false, + onChatView, +}: AdminChatsTableProps) => { + const [searchQuery, setSearchQuery] = useState("") + const [filterType, setFilterType] = useState<"all" | "agent" | "normal">( + "all", + ) + const [userFilter, setUserFilter] = useState<"all" | number>("all") + const [sortBy, setSortBy] = useState< + "created" | "messages" | "cost" | "tokens" + >("created") + const [selectedChat, setSelectedChat] = useState(null) + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [users, setUsers] = useState([]) + const [loadingUsers, setLoadingUsers] = useState(false) + + const handleViewChat = (chat: AdminChat) => { + setSelectedChat(chat) + setIsDialogOpen(true) + onChatView?.(chat) + } + + const handleCloseDialog = () => { + setIsDialogOpen(false) + setSelectedChat(null) + } + + // Fetch users on component mount + useEffect(() => { + const fetchUsers = async () => { + try { + setLoadingUsers(true) + const response = await api.admin.users.$get() + + if (!response.ok) { + throw new Error("Failed to fetch users") + } + + const usersData = await response.json() + setUsers(usersData) + } catch (error) { + console.error("Error fetching users:", error) + } finally { + setLoadingUsers(false) + } + } + + fetchUsers() + }, []) + + if (loading) { + return ( + + + + + All Chats + + System-wide chat conversations + + +
+
Loading chats data...
+
+
+
+ ) + } + + // Filter chats based on search query, filter type, and user filter + const filteredChats = chats.filter((chat) => { + const matchesSearch = + chat.title.toLowerCase().includes(searchQuery.toLowerCase()) || + chat.userName.toLowerCase().includes(searchQuery.toLowerCase()) || + chat.userEmail.toLowerCase().includes(searchQuery.toLowerCase()) || + (chat.agentName && + chat.agentName.toLowerCase().includes(searchQuery.toLowerCase())) + + const matchesFilter = + filterType === "all" || + (filterType === "agent" && chat.agentId) || + (filterType === "normal" && !chat.agentId) + + const matchesUser = userFilter === "all" || chat.userId === userFilter + + return matchesSearch && matchesFilter && matchesUser + }) + + // Sort chats + const sortedChats = [...filteredChats].sort((a, b) => { + switch (sortBy) { + case "created": + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + case "messages": + return b.messageCount - a.messageCount + case "cost": + return b.totalCost - a.totalCost + case "tokens": + return b.totalTokens - a.totalTokens + default: + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + } + }) + + return ( + <> + + +
+
+ + + All Chats + + + System-wide chat conversations with viewing capability + +
+ + {chats.length} total chats + +
+ + {/* Search and Filter Controls */} +
+ {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 text-sm border border-input bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" + /> +
+ + {/* Filter Dropdown */} +
+ +
+ +
+
+ + {/* User Filter Dropdown */} +
+ +
+ {loadingUsers ? ( + + ) : ( + + )} +
+
+ + {/* Sort Dropdown */} +
+ +
+ + + +
+
+
+
+ + {chats.length === 0 ? ( +
+ +

No chat data available

+
+ ) : ( +
+ {/* Chats List */} +
+ {sortedChats.length > 0 ? ( + sortedChats.map((chat, index) => ( +
+
+
+ {index + 1} +
+
+
+

+ {chat.title} +

+ {chat.agentId && ( + + + {chat.agentName || "Agent"} + + )} + {chat.isBookmarked && ( + + Bookmarked + + )} +
+

+ User: {chat.userName} ({chat.userEmail}) +

+

+ + {new Date(chat.createdAt).toLocaleDateString( + "en-US", + { + month: "short", + day: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }, + )} +

+
+
+ +
+
+ + {chat.messageCount} + + + messages + +
+
+ + {formatCostInINR(chat.totalCost)} + + + cost + +
+
+ + {chat.totalTokens.toLocaleString()} + + + tokens + +
+
+
+ + {chat.likes} +
+
+ + {chat.dislikes} +
+
+ +
+
+ )) + ) : ( +
+ +

+ No chats found matching your criteria +

+
+ )} +
+ + {/* Results Summary */} + {filteredChats.length > 0 && ( +
+ Showing {sortedChats.length} of {chats.length} chats + {searchQuery && ` (filtered from ${chats.length})`} + {filterType !== "all" && ` • ${filterType} chats only`} + {userFilter !== "all" && + ` • User: ${users.find((u) => u.id === userFilter)?.name || "Unknown"}`} + {sortBy !== "created" && ` • Sorted by ${sortBy}`} +
+ )} +
+ )} +
+
+ + {/* Chat View Dialog */} + + + ) +} diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index ea472bd15..3c441c4db 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -37,6 +37,8 @@ import { Area, AreaChart, } from "recharts" +import { AdminChatsTable } from "@/components/AdminChatsTable" +import type { AdminChat } from "@/components/AdminChatsTable" interface Chat { externalId: string @@ -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("") const [feedbackModal, setFeedbackModal] = useState<{ @@ -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, @@ -1579,11 +1590,24 @@ const AdminUsersLeaderboard = ({ return ( - - - Users Leaderboard - - All active users by message count +
+
+ + + Users Leaderboard + + All active users by message count +
+ {onAllChatsClick && ( + + )} +
@@ -1687,15 +1711,26 @@ const AdminUsersLeaderboard = ({ {user.dislikes}
- {(user.likes > 0 || user.dislikes > 0) && ( - - )} +
+ {onUserChatsClick && ( + + )} + {(user.likes > 0 || user.dislikes > 0) && ( + + )} +
) @@ -2667,6 +2702,7 @@ export const Dashboard = ({ feedbackMessages: [], }, }) + const [adminChats, setAdminChats] = useState([]) const [adminLoading, setAdminLoading] = useState(false) const [adminError, setAdminError] = useState(null) const [selectedUser, setSelectedUser] = useState(null) @@ -2855,6 +2891,7 @@ export const Dashboard = ({ const adminChats = await adminChatsResponse.json() const adminAgents = await adminAgentsResponse.json() + // Process admin stats const processedAdminStats = processAdminChatsData( adminChats, @@ -2863,6 +2900,31 @@ export const Dashboard = ({ ) setAdminStats(processedAdminStats) + // Set the raw admin chats data for the table + setAdminChats( + adminChats.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) @@ -3974,6 +4036,14 @@ export const Dashboard = ({ users={adminStats.topUsers} loading={adminLoading} onUserClick={handleAdminUserSelect} + onAllChatsClick={() => { + // Navigate to all chats view + window.location.href = "/admin/chat-overview" + }} + onUserChatsClick={(userId: number, userName: string) => { + // Navigate to user-specific chats view + window.location.href = `/admin/chat-overview?userId=${userId}&userName=${encodeURIComponent(userName)}` + }} /> {/* Top Agents System-wide */} @@ -3982,6 +4052,15 @@ export const Dashboard = ({ showAll={true} onAgentClick={handleAdminAgentSelect} /> + + {/* All Chats Table */} + { + console.log("Viewing chat:", chat.externalId) + }} + /> )} diff --git a/frontend/src/routes/_authenticated/admin/chat-overview.tsx b/frontend/src/routes/_authenticated/admin/chat-overview.tsx new file mode 100644 index 000000000..3930d83a1 --- /dev/null +++ b/frontend/src/routes/_authenticated/admin/chat-overview.tsx @@ -0,0 +1,273 @@ +import { + createFileRoute, + redirect, + useRouterState, + useNavigate, + useSearch, +} from "@tanstack/react-router" +import { useEffect, useState } from "react" +import { api } from "@/api" +import { errorComponent } from "@/components/error" +import { Sidebar } from "@/components/Sidebar" +import { AdminChatsTable } from "@/components/AdminChatsTable" +import type { AdminChat } from "@/components/AdminChatsTable" +import { ArrowLeft, ChevronLeft, ChevronRight } from "lucide-react" +import { Button } from "@/components/ui/button" +import { z } from "zod" + +const searchSchema = z.object({ + userId: z.string().optional(), + userName: z.string().optional(), + page: z.string().optional(), + offset: z.string().optional(), + search: z.string().optional(), +}) + +export const Route = createFileRoute("/_authenticated/admin/chat-overview")({ + beforeLoad: ({ context }) => { + if ( + !(context.user.role === "Admin" || context.user.role === "SuperAdmin") + ) { + throw redirect({ to: "/" }) + } + }, + validateSearch: searchSchema, + component: () => { + const matches = useRouterState({ select: (s) => s.matches }) + const { user, workspace, agentWhiteList } = + matches[matches.length - 1].context + return ( + + ) + }, + errorComponent: errorComponent, +}) + +interface ChatOverviewPageProps { + user: any + workspace: any + agentWhiteList: boolean +} + +function ChatOverviewPage({ + user, + workspace, + agentWhiteList, +}: ChatOverviewPageProps) { + const navigate = useNavigate() + const search = useSearch({ from: "/_authenticated/admin/chat-overview" }) + const [adminChats, setAdminChats] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [currentPage, setCurrentPage] = useState(1) + const [pageSize, setPageSize] = useState(20) + + // Reset to page 1 when userId filter changes + useEffect(() => { + setCurrentPage(1) + }, [search.userId]) + + useEffect(() => { + const fetchAdminChats = async () => { + try { + setLoading(true) + setError(null) + + // Build query with pagination and optional userId filter + const query: any = { + page: currentPage.toString(), + offset: pageSize.toString(), + } + if (search.userId) { + query.userId = search.userId + } + if (search.search) { + query.search = search.search + } + + const [adminChatsResponse, adminAgentsResponse] = await Promise.all([ + api.admin.chats.$get({ query }), + api.admin.agents.$get(), + ]) + + if (!adminChatsResponse.ok || !adminAgentsResponse.ok) { + throw new Error("Failed to fetch admin data") + } + + const adminChats = await adminChatsResponse.json() + const adminAgents = await adminAgentsResponse.json() + + // Process and set the admin chats data + setAdminChats( + adminChats.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, + })), + ) + } catch (err) { + console.error("Error fetching admin chats:", err) + setError( + err instanceof Error ? err.message : "Failed to fetch admin data", + ) + } finally { + setLoading(false) + } + } + + fetchAdminChats() + }, [search.userId, currentPage, pageSize]) + + const handleBackToDashboard = () => { + navigate({ to: "/dashboard" }) + } + + // Determine page title and description based on filter + const getPageInfo = () => { + if (search.userId && search.userName) { + return { + title: `Chats for ${search.userName}`, + description: `All chat conversations for user: ${search.userName}`, + } + } + return { + title: "All Chats Overview", + description: + "Complete overview of all chat conversations across the platform", + } + } + + const { title, description } = getPageInfo() + + return ( +
+ +
+
+ {/* Header with Back Button */} +
+ +
+ +
+
+

{title}

+

{description}

+
+
+ + {/* Error State */} + {error && ( +
+

Error loading chats

+

{error}

+
+ )} + + {/* Debug Info */} + {!loading && ( +
+ Debug: userId={search.userId}, userName={search.userName}, chats= + {adminChats.length}, page={currentPage}, pageSize={pageSize} +
+ )} + + {/* Chat Overview Table */} + { + console.log("Viewing chat:", chat.externalId) + // You can implement chat viewing functionality here if needed + }} + /> + + {/* Pagination Controls */} + {!loading && ( +
+
+ Show + + + per page (Total: {adminChats.length}) + +
+ +
+ + + + Page {currentPage} + + + +
+
+ )} +
+
+
+ ) +} diff --git a/server/api/admin.ts b/server/api/admin.ts index 6d7069131..c8df85e32 100644 --- a/server/api/admin.ts +++ b/server/api/admin.ts @@ -141,6 +141,7 @@ import { KbItemsSchema, type VespaSchema } from "@xyne/vespa-ts" import { GetDocument } from "@/search/vespa" import { getCollectionFilesVespaIds } from "@/db/knowledgeBase" import { replaceSheetIndex } from "@/search/utils" +import { fetchUserQueriesForChat } from "@/db/message" const Logger = getLogger(Subsystem.Api).child({ module: "admin" }) const loggerWithChild = getLoggerWithChild(Subsystem.Api, { module: "admin" }) @@ -165,6 +166,24 @@ export const adminQuerySchema = z.object({ .string() .optional() .transform((val) => (val ? Number(val) : undefined)), + page: z + .string() + .optional() + .refine((val) => !val || (!isNaN(Number(val)) && Number(val) > 0), { + message: "Page must be a positive number", + }) + .transform((val) => (val ? Number(val) : 1)), + offset: z + .string() + .optional() + .refine((val) => !val || (!isNaN(Number(val)) && Number(val) >= 0), { + message: "Offset must be a non-negative number", + }) + .transform((val) => (val ? Number(val) : 20)), + search: z + .string() + .optional() + .transform((val) => (val?.trim() ? val.trim() : undefined)), }) // Schema for user agent leaderboard query @@ -1584,48 +1603,51 @@ export const IngestMoreChannelApi = async (c: Context) => { } // Import ingestion functions for database operations - const { createIngestion, hasActiveIngestion } = await import("@/db/ingestion") + const { createIngestion, hasActiveIngestion } = await import( + "@/db/ingestion" + ) // Prevent concurrent ingestions using database transaction to avoid race conditions // This atomically checks for active ingestions and creates new one if none exist const ingestion = await db.transaction(async (trx) => { const hasActive = await hasActiveIngestion(trx, user.id, connector.id) if (hasActive) { - throw new HTTPException(409, { - message: "An ingestion is already in progress for this connector. Please wait for it to complete or cancel it first." + throw new HTTPException(409, { + message: + "An ingestion is already in progress for this connector. Please wait for it to complete or cancel it first.", }) } // Create ingestion record with initial metadata for resumability // All state needed for resuming is stored in the metadata field return await createIngestion(trx, { - userId: user.id, - connectorId: connector.id, - workspaceId: connector.workspaceId, - status: "pending", - metadata: { - slack: { - // Data sent to frontend via WebSocket for progress display - websocketData: { - connectorId: connector.externalId, - progress: { - totalChannels: payload.channelsToIngest.length, - processedChannels: 0, - totalMessages: 0, - processedMessages: 0, + userId: user.id, + connectorId: connector.id, + workspaceId: connector.workspaceId, + status: "pending", + metadata: { + slack: { + // Data sent to frontend via WebSocket for progress display + websocketData: { + connectorId: connector.externalId, + progress: { + totalChannels: payload.channelsToIngest.length, + processedChannels: 0, + totalMessages: 0, + processedMessages: 0, + }, + }, + // Internal state data for resumability + ingestionState: { + channelsToIngest: payload.channelsToIngest, + startDate: payload.startDate, + endDate: payload.endDate, + includeBotMessage: payload.includeBotMessage, + currentChannelIndex: 0, // Resume from this channel + lastUpdated: new Date().toISOString(), }, - }, - // Internal state data for resumability - ingestionState: { - channelsToIngest: payload.channelsToIngest, - startDate: payload.startDate, - endDate: payload.endDate, - includeBotMessage: payload.includeBotMessage, - currentChannelIndex: 0, // Resume from this channel - lastUpdated: new Date().toISOString(), }, }, - }, }) }) @@ -1639,7 +1661,7 @@ export const IngestMoreChannelApi = async (c: Context) => { payload.endDate, email, payload.includeBotMessage, - ingestion.id, // Pass ingestion ID for progress tracking + ingestion.id, // Pass ingestion ID for progress tracking ).catch((error) => { loggerWithChild({ email: sub }).error( error, @@ -1672,7 +1694,7 @@ export const GetAdminChats = async (c: Context) => { try { // Get validated query parameters // @ts-ignore - const { from, to, userId } = c.req.valid("query") + const { from, to, userId, page, offset, search } = c.req.valid("query") // Build the conditions array const conditions = [] @@ -1686,6 +1708,16 @@ export const GetAdminChats = async (c: Context) => { conditions.push(eq(chats.userId, userId)) } + // Add search functionality across email, chat title, and user name + if (search) { + const searchCondition = sql`( + LOWER(${chats.title}) LIKE LOWER(${"%" + search + "%"}) OR + LOWER(${users.email}) LIKE LOWER(${"%" + search + "%"}) OR + LOWER(${users.name}) LIKE LOWER(${"%" + search + "%"}) + )` + conditions.push(searchCondition) + } + // Build the query with feedback aggregation and cost tracking const baseQuery = db .select({ @@ -1709,6 +1741,7 @@ export const GetAdminChats = async (c: Context) => { .leftJoin(users, eq(chats.userId, users.id)) .leftJoin(messages, eq(chats.id, messages.chatId)) + // Execute the query const result = conditions.length > 0 ? await baseQuery @@ -1720,16 +1753,27 @@ export const GetAdminChats = async (c: Context) => { users.role, users.createdAt, ) - : await baseQuery.groupBy( - chats.id, - users.email, - users.name, - users.role, - users.createdAt, - ) + .orderBy(chats.createdAt) + : await baseQuery + .groupBy( + chats.id, + users.email, + users.name, + users.role, + users.createdAt, + ) + .orderBy(chats.createdAt) + + // Apply pagination in JavaScript for now + let paginatedResult = result + if (offset && offset > 0) { + const startIndex = (page - 1) * offset + const endIndex = startIndex + offset + paginatedResult = result.slice(startIndex, endIndex) + } // Convert totalCost from string to number and totalTokens from bigint to number - const processedResult = result.map((chat) => ({ + const processedResult = paginatedResult.map((chat) => ({ ...chat, totalCost: Number(chat.totalCost) || 0, // numeric → string at runtime totalTokens: Number(chat.totalTokens) || 0, // bigint → string at runtime @@ -2265,10 +2309,7 @@ export const GetKbVespaContent = async (c: Context) => { // console.log("Fetched Vespa Doc ID:", vespaDocId) const schema = rawSchema as VespaSchema - const documentData = await GetDocument( - schema, - vespaDocId, - ) + const documentData = await GetDocument(schema, vespaDocId) if (!documentData || !("fields" in documentData) || !documentData.fields) { loggerWithChild({ email: userEmail }).warn( @@ -2318,3 +2359,36 @@ export const GetKbVespaContent = async (c: Context) => { }) } } + +export const GetChatQueriesApi = async (c: Context) => { + try { + const chatId = c.req.param("chatId") + if (!chatId) { + return c.json( + { + success: false, + message: "chatId is required", + }, + 400, + ) + } + const queries = await fetchUserQueriesForChat(db, chatId) + + return c.json( + { + success: true, + data: queries, + }, + 200, + ) + } catch (error) { + Logger.error(error, "Error fetching chat queries") + return c.json( + { + success: false, + message: getErrorMessage(error), + }, + 500, + ) + } +} diff --git a/server/db/message.ts b/server/db/message.ts index 441b48310..06ae7a74d 100644 --- a/server/db/message.ts +++ b/server/db/message.ts @@ -291,13 +291,15 @@ export const getMessagesWithAttachmentsByChatId = async ( chatExternalId: string, limit: number, offset: number, -): Promise> => { +): Promise< + Array<{ + id: number + externalId: string + attachments: unknown + sources: unknown + email: string + }> +> => { const chatMessages = await trx .select({ id: messages.id, @@ -339,3 +341,23 @@ export const updateMessageAttachmentsAndSources = async ( ), ) } + +export const fetchUserQueriesForChat = async ( + trx: TxnOrClient, + chatExternalId: string, +): Promise => { + const queries = await trx + .select({ + content: messages.message, + }) + .from(messages) + .where( + and( + eq(messages.chatExternalId, chatExternalId), + eq(messages.messageRole, MessageRole.User), + isNull(messages.deletedAt), + ), + ) + .orderBy(asc(messages.createdAt)) + return queries.map((q) => q.content) +} diff --git a/server/server.ts b/server/server.ts index fceff41d1..33465aa71 100644 --- a/server/server.ts +++ b/server/server.ts @@ -79,6 +79,7 @@ import { ListAllLoggedInUsers, ListAllIngestedUsers, GetKbVespaContent, + GetChatQueriesApi, } from "@/api/admin" import { ProxyUrl } from "@/api/proxy" import { initApiServerQueue } from "@/queue/api-server-queue" @@ -1180,20 +1181,14 @@ export const AppRoutes = app zValidator("query", getIngestionStatusSchema), (c) => proxyToSyncServer(c, "/ingestion/status", "GET"), ) - .post( - "/ingestion/cancel", - zValidator("json", cancelIngestionSchema), - (c) => proxyToSyncServer(c, "/ingestion/cancel"), + .post("/ingestion/cancel", zValidator("json", cancelIngestionSchema), (c) => + proxyToSyncServer(c, "/ingestion/cancel"), ) - .post( - "/ingestion/pause", - zValidator("json", pauseIngestionSchema), - (c) => proxyToSyncServer(c, "/ingestion/pause"), + .post("/ingestion/pause", zValidator("json", pauseIngestionSchema), (c) => + proxyToSyncServer(c, "/ingestion/pause"), ) - .post( - "/ingestion/resume", - zValidator("json", resumeIngestionSchema), - (c) => proxyToSyncServer(c, "/ingestion/resume"), + .post("/ingestion/resume", zValidator("json", resumeIngestionSchema), (c) => + proxyToSyncServer(c, "/ingestion/resume"), ) .delete( "/oauth/connector/delete", @@ -1323,6 +1318,7 @@ export const AppRoutes = app zValidator("query", userAgentLeaderboardQuerySchema), GetUserAgentLeaderboard, ) + .get("/chat/queries/:chatId", GetChatQueriesApi) .get( "/agents/:agentId/analysis", @@ -1444,7 +1440,11 @@ app .post("/cl/poll-status", PollCollectionsStatusApi) // Poll collection items status // Proxy function to forward ingestion API calls to sync server -const proxyToSyncServer = async (c: Context, endpoint: string, method: string = "POST") => { +const proxyToSyncServer = async ( + c: Context, + endpoint: string, + method: string = "POST", +) => { try { // Get JWT token from cookie const token = getCookie(c, AccessTokenCookieName) @@ -1457,7 +1457,7 @@ const proxyToSyncServer = async (c: Context, endpoint: string, method: string = if (method === "GET") { const urlObj = new URL(url) const queryParams = c.req.query() - Object.keys(queryParams).forEach(key => { + Object.keys(queryParams).forEach((key) => { if (queryParams[key]) { urlObj.searchParams.set(key, queryParams[key]) } From 8aa031e4ca63d4ecc8b36ebe6646bbba9bfebec9 Mon Sep 17 00:00:00 2001 From: Chinmay Singh Date: Wed, 22 Oct 2025 14:11:40 +0530 Subject: [PATCH 2/8] feat: XYNE-206 resolved comment, improved efficiency and debug logs --- frontend/src/components/AdminChatsTable.tsx | 188 ++++++++++-------- frontend/src/components/Dashboard.tsx | 18 +- .../_authenticated/admin/chat-overview.tsx | 86 ++++++-- server/api/admin.ts | 109 ++++++---- 4 files changed, 244 insertions(+), 157 deletions(-) diff --git a/frontend/src/components/AdminChatsTable.tsx b/frontend/src/components/AdminChatsTable.tsx index b70f66eb6..a45c1a0dd 100644 --- a/frontend/src/components/AdminChatsTable.tsx +++ b/frontend/src/components/AdminChatsTable.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react" +import { useState, useEffect, useCallback, useMemo } from "react" import { MessageSquare, Bot, @@ -10,8 +10,6 @@ import { Filter, X, User, - Clock, - Hash, Loader2, Users, } from "lucide-react" @@ -26,7 +24,6 @@ import { Badge } from "@/components/ui/badge" import { Dialog, DialogContent, - DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" @@ -80,7 +77,7 @@ const ChatViewDialog = ({ isOpen, onClose, chat }: ChatViewDialogProps) => { const [loadingMessages, setLoadingMessages] = useState(false) const [messagesError, setMessagesError] = useState(null) - const fetchChatMessages = async (chatId: string) => { + const fetchChatMessages = useCallback(async (chatId: string) => { try { setLoadingMessages(true) setMessagesError(null) @@ -104,7 +101,7 @@ const ChatViewDialog = ({ isOpen, onClose, chat }: ChatViewDialogProps) => { } finally { setLoadingMessages(false) } - } + }, []) // Fetch messages when dialog opens and chat changes useEffect(() => { @@ -115,7 +112,7 @@ const ChatViewDialog = ({ isOpen, onClose, chat }: ChatViewDialogProps) => { setMessages([]) setMessagesError(null) } - }, [isOpen, chat]) + }, [isOpen, chat, fetchChatMessages]) if (!chat) return null @@ -255,21 +252,39 @@ interface AdminChatsTableProps { chats: AdminChat[] loading?: boolean onChatView?: (chat: AdminChat) => void + // Controlled component props for filters + searchInput: string + searchQuery: string + onSearchInputChange: (input: string) => void + onSearch: () => void + onClearSearch: () => void + filterType: "all" | "agent" | "normal" + onFilterTypeChange: (type: "all" | "agent" | "normal") => void + userFilter: "all" | number + onUserFilterChange: (filter: "all" | number) => void + sortBy: "created" | "messages" | "cost" | "tokens" + onSortByChange: (sortBy: "created" | "messages" | "cost" | "tokens") => void + // Props to control visibility of user dropdown + showUserFilter?: boolean } export const AdminChatsTable = ({ chats, loading = false, onChatView, + searchInput, + searchQuery, + onSearchInputChange, + onSearch, + onClearSearch, + filterType, + onFilterTypeChange, + userFilter, + onUserFilterChange, + sortBy, + onSortByChange, + showUserFilter = true, }: AdminChatsTableProps) => { - const [searchQuery, setSearchQuery] = useState("") - const [filterType, setFilterType] = useState<"all" | "agent" | "normal">( - "all", - ) - const [userFilter, setUserFilter] = useState<"all" | number>("all") - const [sortBy, setSortBy] = useState< - "created" | "messages" | "cost" | "tokens" - >("created") const [selectedChat, setSelectedChat] = useState(null) const [isDialogOpen, setIsDialogOpen] = useState(false) const [users, setUsers] = useState([]) @@ -286,6 +301,12 @@ export const AdminChatsTable = ({ setSelectedChat(null) } + // Memoize the display chats calculation to prevent unnecessary re-renders + const displayChats = useMemo(() => { + // Since filtering and sorting are now handled server-side, we just display the chats as received + return chats + }, [chats]) + // Fetch users on component mount useEffect(() => { const fetchUsers = async () => { @@ -328,41 +349,6 @@ export const AdminChatsTable = ({ ) } - // Filter chats based on search query, filter type, and user filter - const filteredChats = chats.filter((chat) => { - const matchesSearch = - chat.title.toLowerCase().includes(searchQuery.toLowerCase()) || - chat.userName.toLowerCase().includes(searchQuery.toLowerCase()) || - chat.userEmail.toLowerCase().includes(searchQuery.toLowerCase()) || - (chat.agentName && - chat.agentName.toLowerCase().includes(searchQuery.toLowerCase())) - - const matchesFilter = - filterType === "all" || - (filterType === "agent" && chat.agentId) || - (filterType === "normal" && !chat.agentId) - - const matchesUser = userFilter === "all" || chat.userId === userFilter - - return matchesSearch && matchesFilter && matchesUser - }) - - // Sort chats - const sortedChats = [...filteredChats].sort((a, b) => { - switch (sortBy) { - case "created": - return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - case "messages": - return b.messageCount - a.messageCount - case "cost": - return b.totalCost - a.totalCost - case "tokens": - return b.totalTokens - a.totalTokens - default: - return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - } - }) - return ( <> @@ -386,14 +372,38 @@ export const AdminChatsTable = ({
{/* Search */}
- setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-2 text-sm border border-input bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" + value={searchInput} + onChange={(e) => onSearchInputChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + onSearch() + } + }} + className="w-full pl-2 pr-16 py-2 text-sm border border-input bg-background rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" /> +
+ {searchInput && ( + + )} + +
{/* Filter Dropdown */} @@ -401,7 +411,9 @@ export const AdminChatsTable = ({ - setUserFilter( - e.target.value === "all" ? "all" : Number(e.target.value), - ) - } - className="appearance-none bg-background border border-input rounded-md px-3 py-2 pr-8 text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent min-w-[150px]" - disabled={loadingUsers} - > - - {users.map((user) => ( - - ))} - -
- {loadingUsers ? ( - - ) : ( - - )} + {/* User Filter Dropdown - Only show if not filtering by specific user */} + {showUserFilter && ( +
+ +
+ {loadingUsers ? ( + + ) : ( + + )} +
-
+ )} {/* Sort Dropdown */}
- per page (Total: {adminChats.length}) + per page + {paginationMetadata?.totalCount !== undefined && ( + <> (Total: {paginationMetadata.totalCount}) + )}
@@ -292,7 +333,11 @@ function ChatOverviewPage({ variant="outline" size="sm" onClick={() => setCurrentPage(Math.max(1, currentPage - 1))} - disabled={currentPage === 1} + disabled={ + paginationMetadata + ? !paginationMetadata.hasPreviousPage + : currentPage === 1 + } className="flex items-center gap-1" > @@ -307,7 +352,11 @@ function ChatOverviewPage({ variant="outline" size="sm" onClick={() => setCurrentPage(currentPage + 1)} - disabled={adminChats.length < pageSize} + disabled={ + paginationMetadata + ? !paginationMetadata.hasNextPage + : adminChats.length < pageSize + } className="flex items-center gap-1" > Next diff --git a/server/api/admin.ts b/server/api/admin.ts index ea498a840..730a02090 100644 --- a/server/api/admin.ts +++ b/server/api/admin.ts @@ -1722,7 +1722,7 @@ export const GetAdminChats = async (c: Context) => { const to = query.to ? new Date(query.to) : undefined const userId = query.userId ? Number(query.userId) : undefined const page = query.page ? Number(query.page) : 1 - const offset = query.offset ? Number(query.offset) : 20 + const pageSize = query.offset ? Number(query.offset) : 20 const search = query.search?.trim() || undefined const filterType = query.filterType || "all" const sortBy = query.sortBy || "created" @@ -1766,6 +1766,19 @@ export const GetAdminChats = async (c: Context) => { conditions.push(searchCondition) } + // First, get the total count using Drizzle count function + const totalCountResult = await db + .select({ + count: count(), + }) + .from(chats) + .leftJoin(users, eq(chats.userId, users.id)) + .leftJoin(messages, eq(chats.id, messages.chatId)) + .where(and(...conditions)) + .groupBy(chats.id, users.email, users.name, users.role, users.createdAt) + + const totalCount = totalCountResult.length + // Build the query with feedback aggregation and cost tracking const baseQuery = db .select({ @@ -1778,12 +1791,20 @@ export const GetAdminChats = async (c: Context) => { userEmail: users.email, userName: users.name, userRole: users.role, - userCreatedAt: users.createdAt, // Add user's actual creation date + userCreatedAt: users.createdAt, messageCount: count(messages.id), - likes: sql`COUNT(CASE WHEN ${messages.feedback}->>'type' = 'like' AND ${messages.deletedAt} IS NULL THEN 1 END)::int`, - dislikes: sql`COUNT(CASE WHEN ${messages.feedback}->>'type' = 'dislike' AND ${messages.deletedAt} IS NULL THEN 1 END)::int`, - totalCost: sql`COALESCE(SUM(CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.cost} ELSE 0 END), 0)::numeric`, - totalTokens: sql`COALESCE(SUM(CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.tokensUsed} ELSE 0 END), 0)::bigint`, + likes: count( + sql`CASE WHEN ${messages.feedback}->>'type' = 'like' AND ${messages.deletedAt} IS NULL THEN 1 END`, + ), + dislikes: count( + sql`CASE WHEN ${messages.feedback}->>'type' = 'dislike' AND ${messages.deletedAt} IS NULL THEN 1 END`, + ), + totalCost: sum( + sql`CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.cost} ELSE 0 END`, + ), + totalTokens: sum( + sql`CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.tokensUsed} ELSE 0 END`, + ), }) .from(chats) .leftJoin(users, eq(chats.userId, users.id)) @@ -1793,13 +1814,25 @@ export const GetAdminChats = async (c: Context) => { let orderByClause switch (sortBy) { case "messages": - orderByClause = sql`COUNT(CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.id} END) DESC` + orderByClause = desc( + count( + sql`CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.id} END`, + ), + ) break case "cost": - orderByClause = sql`COALESCE(SUM(CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.cost} ELSE 0 END), 0) DESC` + orderByClause = desc( + sum( + sql`CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.cost} ELSE 0 END`, + ), + ) break case "tokens": - orderByClause = sql`COALESCE(SUM(CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.tokensUsed} ELSE 0 END), 0) DESC` + orderByClause = desc( + sum( + sql`CASE WHEN ${messages.deletedAt} IS NULL THEN ${messages.tokensUsed} ELSE 0 END`, + ), + ) break case "created": default: @@ -1808,8 +1841,8 @@ export const GetAdminChats = async (c: Context) => { } // Calculate pagination offsets - const limit = offset || 20 - const offsetValue = ((page || 1) - 1) * limit + const limit = pageSize + const offsetValue = (page - 1) * limit // Execute the query with SQL-based pagination const result = await baseQuery @@ -1826,7 +1859,23 @@ export const GetAdminChats = async (c: Context) => { totalTokens: Number(chat.totalTokens) || 0, // bigint → string at runtime })) - return c.json(processedResult) + // Calculate pagination metadata + const hasNextPage = page * pageSize < totalCount + const hasPreviousPage = page > 1 + + // Return data with pagination metadata + const response = { + data: processedResult, + pagination: { + totalCount, + currentPage: page, + pageSize, + hasNextPage, + hasPreviousPage, + }, + } + + return c.json(response) } catch (error) { Logger.error(error, "Error fetching admin chats") return c.json( diff --git a/server/types.ts b/server/types.ts index d2d2e72f1..3685a82a3 100644 --- a/server/types.ts +++ b/server/types.ts @@ -601,6 +601,22 @@ export const UserRoleChangeSchema = z.object({ export type userRoleChange = z.infer +// Admin pagination response schema +export const AdminChatsPaginationResponseSchema = z.object({ + data: z.array(z.any()), // Chat data array + pagination: z.object({ + totalCount: z.number(), + currentPage: z.number(), + pageSize: z.number(), + hasNextPage: z.boolean(), + hasPreviousPage: z.boolean(), + }), +}) + +export type AdminChatsPaginationResponse = z.infer< + typeof AdminChatsPaginationResponseSchema +> + export const UserMetadata = z.object({ userTimezone: z.string(), dateForAI: z.string(), From 4c8198314a82f93b6794911f2176977a83dc4817 Mon Sep 17 00:00:00 2001 From: Chinmay Singh Date: Wed, 22 Oct 2025 20:29:53 +0530 Subject: [PATCH 7/8] feat: XYNE-206 resolving ts build error --- frontend/src/components/Dashboard.tsx | 4 ++-- frontend/src/routes/_authenticated/admin/chat-overview.tsx | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 235e3d591..607003f12 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -4056,12 +4056,12 @@ export const Dashboard = ({ onUserClick={handleAdminUserSelect} onAllChatsClick={() => { // Navigate to all chats view - navigate({ to: "/admin/chat-overview" }) + navigate({ to: "/admin/chat-overview" as const }) }} onUserChatsClick={(userId: number, userName: string) => { // Navigate to user-specific chats view navigate({ - to: "/admin/chat-overview", + to: "/admin/chat-overview" as const, search: { userName: userName }, }) }} diff --git a/frontend/src/routes/_authenticated/admin/chat-overview.tsx b/frontend/src/routes/_authenticated/admin/chat-overview.tsx index 7a746a9bb..b93ac7c60 100644 --- a/frontend/src/routes/_authenticated/admin/chat-overview.tsx +++ b/frontend/src/routes/_authenticated/admin/chat-overview.tsx @@ -20,6 +20,9 @@ const chatOverviewSearchSchema = z.object({ offset: z.string().optional(), search: z.string().optional(), }) + +type ChatOverviewSearch = z.infer + export const Route = createFileRoute("/_authenticated/admin/chat-overview")({ beforeLoad: ({ context }) => { if ( @@ -28,7 +31,9 @@ export const Route = createFileRoute("/_authenticated/admin/chat-overview")({ throw redirect({ to: "/" }) } }, - validateSearch: chatOverviewSearchSchema, + validateSearch: (search: Record): ChatOverviewSearch => { + return chatOverviewSearchSchema.parse(search) + }, component: () => { const matches = useRouterState({ select: (s) => s.matches }) const { user, workspace, agentWhiteList } = From a0988dca8bb73cd15a1e1c6917cc32add4693e8a Mon Sep 17 00:00:00 2001 From: Chinmay Singh Date: Wed, 22 Oct 2025 20:51:07 +0530 Subject: [PATCH 8/8] feat: XYNE-206 adding routegen tree --- frontend/src/routeTree.gen.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 8ffc16937..8848cf387 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -32,6 +32,7 @@ import { Route as AuthenticatedIntegrationsApiKeyImport } from './routes/_authen import { Route as AuthenticatedDataSourceDocIdImport } from './routes/_authenticated/dataSource.$docId' import { Route as AuthenticatedChatChatIdImport } from './routes/_authenticated/chat.$chatId' import { Route as AuthenticatedAdminUserManagementImport } from './routes/_authenticated/admin/userManagement' +import { Route as AuthenticatedAdminChatOverviewImport } from './routes/_authenticated/admin/chat-overview' import { Route as AuthenticatedAdminIntegrationsIndexImport } from './routes/_authenticated/admin/integrations/index' import { Route as AuthenticatedTraceChatIdMsgIdImport } from './routes/_authenticated/trace.$chatId.$msgId' import { Route as AuthenticatedAdminIntegrationsSlackImport } from './routes/_authenticated/admin/integrations/slack' @@ -175,6 +176,13 @@ const AuthenticatedAdminUserManagementRoute = getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedAdminChatOverviewRoute = + AuthenticatedAdminChatOverviewImport.update({ + id: '/admin/chat-overview', + path: '/admin/chat-overview', + getParentRoute: () => AuthenticatedRoute, + } as any) + const AuthenticatedAdminIntegrationsIndexRoute = AuthenticatedAdminIntegrationsIndexImport.update({ id: '/admin/integrations/', @@ -305,6 +313,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedIndexImport parentRoute: typeof AuthenticatedImport } + '/_authenticated/admin/chat-overview': { + id: '/_authenticated/admin/chat-overview' + path: '/admin/chat-overview' + fullPath: '/admin/chat-overview' + preLoaderRoute: typeof AuthenticatedAdminChatOverviewImport + parentRoute: typeof AuthenticatedImport + } '/_authenticated/admin/userManagement': { id: '/_authenticated/admin/userManagement' path: '/admin/userManagement' @@ -435,6 +450,7 @@ interface AuthenticatedRouteChildren { AuthenticatedTuningRoute: typeof AuthenticatedTuningRoute AuthenticatedWorkflowRoute: typeof AuthenticatedWorkflowRoute AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute + AuthenticatedAdminChatOverviewRoute: typeof AuthenticatedAdminChatOverviewRoute AuthenticatedAdminUserManagementRoute: typeof AuthenticatedAdminUserManagementRoute AuthenticatedDataSourceDocIdRoute: typeof AuthenticatedDataSourceDocIdRoute AuthenticatedIntegrationsApiKeyRoute: typeof AuthenticatedIntegrationsApiKeyRoute @@ -460,6 +476,7 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { AuthenticatedTuningRoute: AuthenticatedTuningRoute, AuthenticatedWorkflowRoute: AuthenticatedWorkflowRoute, AuthenticatedIndexRoute: AuthenticatedIndexRoute, + AuthenticatedAdminChatOverviewRoute: AuthenticatedAdminChatOverviewRoute, AuthenticatedAdminUserManagementRoute: AuthenticatedAdminUserManagementRoute, AuthenticatedDataSourceDocIdRoute: AuthenticatedDataSourceDocIdRoute, AuthenticatedIntegrationsApiKeyRoute: AuthenticatedIntegrationsApiKeyRoute, @@ -499,6 +516,7 @@ export interface FileRoutesByFullPath { '/workflow': typeof AuthenticatedWorkflowRoute '/oauth/success': typeof OauthSuccessRoute '/': typeof AuthenticatedIndexRoute + '/admin/chat-overview': typeof AuthenticatedAdminChatOverviewRoute '/admin/userManagement': typeof AuthenticatedAdminUserManagementRoute '/chat/$chatId': typeof AuthenticatedChatChatIdRoute '/dataSource/$docId': typeof AuthenticatedDataSourceDocIdRoute @@ -528,6 +546,7 @@ export interface FileRoutesByTo { '/workflow': typeof AuthenticatedWorkflowRoute '/oauth/success': typeof OauthSuccessRoute '/': typeof AuthenticatedIndexRoute + '/admin/chat-overview': typeof AuthenticatedAdminChatOverviewRoute '/admin/userManagement': typeof AuthenticatedAdminUserManagementRoute '/chat/$chatId': typeof AuthenticatedChatChatIdRoute '/dataSource/$docId': typeof AuthenticatedDataSourceDocIdRoute @@ -559,6 +578,7 @@ export interface FileRoutesById { '/_authenticated/workflow': typeof AuthenticatedWorkflowRoute '/oauth/success': typeof OauthSuccessRoute '/_authenticated/': typeof AuthenticatedIndexRoute + '/_authenticated/admin/chat-overview': typeof AuthenticatedAdminChatOverviewRoute '/_authenticated/admin/userManagement': typeof AuthenticatedAdminUserManagementRoute '/_authenticated/chat/$chatId': typeof AuthenticatedChatChatIdRoute '/_authenticated/dataSource/$docId': typeof AuthenticatedDataSourceDocIdRoute @@ -591,6 +611,7 @@ export interface FileRouteTypes { | '/workflow' | '/oauth/success' | '/' + | '/admin/chat-overview' | '/admin/userManagement' | '/chat/$chatId' | '/dataSource/$docId' @@ -619,6 +640,7 @@ export interface FileRouteTypes { | '/workflow' | '/oauth/success' | '/' + | '/admin/chat-overview' | '/admin/userManagement' | '/chat/$chatId' | '/dataSource/$docId' @@ -648,6 +670,7 @@ export interface FileRouteTypes { | '/_authenticated/workflow' | '/oauth/success' | '/_authenticated/' + | '/_authenticated/admin/chat-overview' | '/_authenticated/admin/userManagement' | '/_authenticated/chat/$chatId' | '/_authenticated/dataSource/$docId' @@ -707,6 +730,7 @@ export const routeTree = rootRoute "/_authenticated/tuning", "/_authenticated/workflow", "/_authenticated/", + "/_authenticated/admin/chat-overview", "/_authenticated/admin/userManagement", "/_authenticated/dataSource/$docId", "/_authenticated/integrations/apiKey", @@ -767,6 +791,10 @@ export const routeTree = rootRoute "filePath": "_authenticated/index.tsx", "parent": "/_authenticated" }, + "/_authenticated/admin/chat-overview": { + "filePath": "_authenticated/admin/chat-overview.tsx", + "parent": "/_authenticated" + }, "/_authenticated/admin/userManagement": { "filePath": "_authenticated/admin/userManagement.tsx", "parent": "/_authenticated"