import React, { Suspense, useState, useMemo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { ChevronRight, ChevronDown, ExternalLink, RefreshCw, MessageSquare, Sparkles, CheckCircle2, } from 'lucide-react'; import OverviewCards from './components/overview-cards/OverviewCards'; import MonitoringFilters from './components/filters/MonitoringFilters'; import TokenMonitoring from './components/TokenMonitoring'; import { ExportDropdown } from './components/ExportDropdown'; import { useMonitoringFilters } from './hooks/useMonitoringFilters'; import { useMonitoringData } from './hooks/useMonitoringData'; import { useFeedbackData } from './hooks/useFeedbackData'; import { ConversationTurnList } from './components/ConversationTurnList'; import { FeedbackStatsCards } from './components/FeedbackCard'; import { FeedbackList } from './components/FeedbackList'; import { buildConversationTurns } from './utils/conversationTurns'; import { LoadingSpinner, LoadingPage } from '@/components/ui/loading-spinner'; import { useCurrentWorkspace } from '@/app/infra/http'; function MonitoringPageContent() { const { t } = useTranslation(); const currentWorkspace = useCurrentWorkspace(); const canExport = currentWorkspace?.permissions.includes('data.export') ?? false; const { filterState, setSelectedBots, setSelectedPipelines, setTimeRange } = useMonitoringFilters(); const { data, loading, refetch } = useMonitoringData(filterState); // Counter to force feedbackTimeRange recomputation on manual refresh const [feedbackRefreshKey, setFeedbackRefreshKey] = useState(0); // Get time range for feedback data const feedbackTimeRange = useMemo(() => { const now = new Date(); let startTime: Date | null = null; switch (filterState.timeRange) { case 'lastHour': startTime = new Date(now.getTime() - 60 * 60 * 1000); break; case 'last6Hours': startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000); break; case 'last24Hours': startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); break; case 'last7Days': startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); break; case 'last30Days': startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); break; case 'custom': if (filterState.customDateRange) { startTime = filterState.customDateRange.from; } break; } const endTime = filterState.timeRange === 'custom' && filterState.customDateRange ? filterState.customDateRange.to : now; return { startTime: startTime?.toISOString(), endTime: endTime.toISOString(), }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [filterState.timeRange, filterState.customDateRange, feedbackRefreshKey]); // Feedback data hook const { feedback: feedbackList, stats: feedbackStats, loading: feedbackLoading, } = useFeedbackData({ botIds: filterState.selectedBots.length > 0 ? filterState.selectedBots : undefined, pipelineIds: filterState.selectedPipelines.length > 0 ? filterState.selectedPipelines : undefined, startTime: feedbackTimeRange.startTime, endTime: feedbackTimeRange.endTime, limit: 50, }); // Combined refresh handler for both monitoring and feedback data const handleRefresh = useCallback(() => { refetch(); setFeedbackRefreshKey((k) => k + 1); }, [refetch]); const conversationTurns = useMemo( () => buildConversationTurns( data?.messages || [], data?.llmCalls || [], data?.errors || [], data?.toolCalls || [], ), [data?.messages, data?.llmCalls, data?.errors, data?.toolCalls], ); // State for expanded errors const [expandedErrorId, setExpandedErrorId] = useState(null); const [expandedTurnId, setExpandedTurnId] = useState(null); // State for controlled tabs const [activeTab, setActiveTab] = useState('messages'); // Function to jump to a message record const jumpToMessage = (messageId: string) => { setActiveTab('messages'); setTimeout(() => { const turn = conversationTurns.find((item) => item.messages.some((message) => message.id === messageId), ); setExpandedTurnId(turn?.id ?? messageId); }, 100); }; const toggleTurnExpand = (turnId: string) => { setExpandedTurnId((current) => (current === turnId ? null : turnId)); }; const toggleErrorExpand = (errorId: string) => { if (expandedErrorId === errorId) { setExpandedErrorId(null); } else { setExpandedErrorId(errorId); } }; return (
{/* Filters and Refresh Button - Sticky */}
{canExport && }
{/* Content Area */}
{/* Overview Section */} {/* Tabs Section */}
{t('monitoring.tabs.messages')} {t('monitoring.tabs.modelCalls')} {t('monitoring.tabs.tokens')} {t('monitoring.tabs.feedback')} {t('monitoring.tabs.errors')}
{loading && (
)} {!loading && data && conversationTurns.length > 0 && ( )} {!loading && (!data || conversationTurns.length === 0) && (
{t('monitoring.messageList.noMessages')}
)}
{loading && (
)} {!loading && data && data.modelCalls && data.modelCalls.length > 0 && (
{data.modelCalls.map((call) => (
{/* Query ID - only show if messageId exists */} {call.messageId && (
Query ID: {call.messageId}
)}
{/* Model Type Badge */} {call.modelType === 'llm' ? t('monitoring.modelCalls.llmModel') : t('monitoring.modelCalls.embeddingModel')} {/* Call Type Badge for Embedding */} {call.modelType === 'embedding' && call.callType && ( {call.callType === 'retrieve' ? t( 'monitoring.modelCalls.retrieveCall', ) : t( 'monitoring.modelCalls.embeddingCall', )} )} {/* Status Badge */} {call.status}
{/* Model Name */}
{call.modelName}
{/* Context Info - only for LLM calls */} {call.modelType === 'llm' && call.botName && call.pipelineName && (
{call.botName} → {call.pipelineName}
)} {/* Token Info */}
{call.modelType === 'llm' && call.tokens && ( <> {t('monitoring.llmCalls.inputTokens')}:{' '} {call.tokens.input} {t('monitoring.llmCalls.outputTokens')}:{' '} {call.tokens.output} {t('monitoring.llmCalls.totalTokens')}:{' '} {call.tokens.total} )} {call.modelType === 'embedding' && ( <> {t( 'monitoring.embeddingCalls.promptTokens', )} : {call.promptTokens} {t( 'monitoring.embeddingCalls.totalTokens', )} : {call.totalTokens} {t( 'monitoring.embeddingCalls.inputCount', )} : {call.inputCount} )} {t('monitoring.llmCalls.duration')}:{' '} {call.duration}ms {call.cost && ( {t('monitoring.llmCalls.cost')}: $ {call.cost.toFixed(4)} )}
{/* Knowledge Base Info for Embedding */} {call.modelType === 'embedding' && call.knowledgeBaseId && (
{t( 'monitoring.embeddingCalls.knowledgeBase', )} : {call.knowledgeBaseId}
)} {/* Query Text for Embedding Retrieve */} {call.modelType === 'embedding' && call.queryText && (
{t( 'monitoring.embeddingCalls.queryText', )} :{' '} {call.queryText.length > 100 ? call.queryText.substring(0, 100) + '...' : call.queryText}
)}
{call.errorMessage && (
Error: {call.errorMessage}
)}
{call.timestamp.toLocaleString()}
))}
)} {!loading && (!data || !data.modelCalls || data.modelCalls.length === 0) && (
{t('monitoring.modelCalls.noData')}
)}
0 ? filterState.selectedBots : undefined } pipelineIds={ filterState.selectedPipelines.length > 0 ? filterState.selectedPipelines : undefined } startTime={feedbackTimeRange.startTime} endTime={feedbackTimeRange.endTime} refreshKey={feedbackRefreshKey} />
{loading && (
)} {!loading && ( <> {/* Feedback Stats Cards */}
{/* Feedback List */}

{t('monitoring.feedback.feedbackList')}

)}
{loading && (
)} {!loading && data && data.errors && data.errors.length > 0 && (
{data.errors.map((error) => (
{/* Error Header - Always Visible */}
toggleErrorExpand(error.id)} >
{/* Expand Icon */}
{expandedErrorId === error.id ? ( ) : ( )}
{/* Error Info */}
{/* Query ID */}
Query ID: {error.messageId || '-'} {error.messageId && ( )}
{error.errorType} {error.botName} {error.pipelineName}

{error.errorMessage}

{/* Timestamp */}
{error.timestamp.toLocaleString()}
{/* Expanded Details */} {expandedErrorId === error.id && (
{/* Error Details */}

{t('monitoring.errors.errorMessage')}

{error.errorMessage}
{/* Context Info */}

{t('monitoring.messageList.viewDetails')}

{t('monitoring.messageList.bot')}
{error.botName}
{t('monitoring.messageList.pipeline')}
{error.pipelineName}
{error.sessionId && (
{t('monitoring.sessions.sessionId')}
{error.sessionId}
)}
{/* Stack Trace */} {error.stackTrace && (

{t('monitoring.errors.stackTrace')}

                                    {error.stackTrace}
                                  
)}
)}
))}
)} {!loading && (!data || !data.errors || data.errors.length === 0) && (
{t('monitoring.errors.noErrors')}
)}
); } export default function MonitoringPage() { return ( }> ); }