import React, { useState, useEffect, useRef, useCallback, forwardRef, useImperativeHandle, } from 'react'; import { useTranslation } from 'react-i18next'; import { httpClient } from '@/app/infra/http/HttpClient'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; import { Copy, Check } from 'lucide-react'; import { MessageChainComponent, Plain, At, Image, Quote, Voice, } from '@/app/infra/entities/message'; interface SessionInfo { session_id: string; bot_id: string; bot_name: string; pipeline_id: string; pipeline_name: string; message_count: number; start_time: string; last_activity: string; is_active: boolean; platform?: string | null; user_id?: string | null; user_name?: string | null; } interface SessionMessage { id: string; timestamp: string; bot_id: string; bot_name: string; pipeline_id: string; pipeline_name: string; message_content: string; session_id: string; status: string; level: string; platform?: string | null; user_id?: string | null; runner_name?: string | null; variables?: string | null; role?: string | null; } export interface BotSessionMonitorHandle { refreshSessions: () => Promise; } interface BotSessionMonitorProps { botId: string; } const BotSessionMonitor = forwardRef< BotSessionMonitorHandle, BotSessionMonitorProps >(function BotSessionMonitor({ botId }, ref) { const { t } = useTranslation(); const [sessions, setSessions] = useState([]); const [selectedSessionId, setSelectedSessionId] = useState( null, ); const [messages, setMessages] = useState([]); const [loadingSessions, setLoadingSessions] = useState(false); const [loadingMessages, setLoadingMessages] = useState(false); const [copiedUserId, setCopiedUserId] = useState(false); const messagesContainerRef = useRef(null); const parseSessionType = (sessionId: string): string | null => { const idx = sessionId.indexOf('_'); if (idx === -1) return null; const type = sessionId.slice(0, idx); if (type === 'person' || type === 'group') return type; return null; }; const abbreviateId = (id: string): string => { if (id.length <= 10) return id; return `${id.slice(0, 4)}..${id.slice(-4)}`; }; const copyUserId = (userId: string) => { navigator.clipboard.writeText(userId).then(() => { setCopiedUserId(true); setTimeout(() => setCopiedUserId(false), 2000); }); }; const loadSessions = useCallback(async () => { setLoadingSessions(true); try { const response = await httpClient.getBotSessions(botId); setSessions(response.sessions ?? []); } catch (error) { console.error('Failed to load sessions:', error); } finally { setLoadingSessions(false); } }, [botId]); useImperativeHandle( ref, () => ({ refreshSessions: loadSessions, }), [loadSessions], ); const loadMessages = useCallback(async (sessionId: string) => { setLoadingMessages(true); try { const response = await httpClient.getSessionMessages(sessionId); const sorted = (response.messages ?? []).sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(), ); setMessages(sorted); } catch (error) { console.error('Failed to load session messages:', error); } finally { setLoadingMessages(false); } }, []); useEffect(() => { loadSessions(); }, [loadSessions]); useEffect(() => { if (selectedSessionId) { loadMessages(selectedSessionId); } else { setMessages([]); } }, [selectedSessionId, loadMessages]); useEffect(() => { const container = messagesContainerRef.current; if (container) { const viewport = container.querySelector( '[data-radix-scroll-area-viewport]', ); const scrollTarget = viewport || container; scrollTarget.scrollTop = scrollTarget.scrollHeight; } }, [messages]); const parseMessageChain = (content: string): MessageChainComponent[] => { try { const parsed = JSON.parse(content); if (Array.isArray(parsed)) { return parsed as MessageChainComponent[]; } } catch { // Not JSON, return as plain text } return [{ type: 'Plain', text: content } as Plain]; }; const isUserMessage = (msg: SessionMessage): boolean => { if (msg.role === 'assistant') return false; if (msg.role === 'user') return true; return !msg.runner_name; }; const renderMessageComponent = ( component: MessageChainComponent, index: number, ) => { switch (component.type) { case 'Plain': return {(component as Plain).text}; case 'At': { const atComponent = component as At; const displayName = atComponent.display || atComponent.target?.toString() || ''; return ( @{displayName} ); } case 'AtAll': return ( @All ); case 'Image': { const img = component as Image; const imageUrl = img.url || (img.base64 ? img.base64 : ''); if (!imageUrl) { return ( [Image] ); } return (
Image
); } case 'Voice': { const voice = component as Voice; const voiceUrl = voice.url || (voice.base64 ? voice.base64 : ''); if (!voiceUrl) { return ( 🎙 [Voice] ); } return (
); } case 'Quote': { const quote = component as Quote; return (
{quote.origin?.map((comp, idx) => renderMessageComponent(comp as MessageChainComponent, idx), )}
); } case 'Source': return null; case 'File': { const file = component as MessageChainComponent & { name?: string }; return ( 📎 {file.name || 'File'} ); } default: return ( [{component.type}] ); } }; const renderMessageContent = (msg: SessionMessage) => { const chain = parseMessageChain(msg.message_content); return (
{chain.map((component, index) => renderMessageComponent(component, index), )}
); }; // Backend timestamps may lack timezone indicator; treat as UTC const parseTimestamp = (timestamp: string): Date => { if (!timestamp) return new Date(0); const hasTimezone = timestamp.endsWith('Z') || /[+-]\d{2}:?\d{2}$/.test(timestamp); return new Date(hasTimezone ? timestamp : timestamp + 'Z'); }; const formatTime = (timestamp: string): string => { if (!timestamp) return ''; const date = parseTimestamp(timestamp); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); return `${hours}:${minutes}`; }; const formatRelativeTime = (timestamp: string): string => { if (!timestamp) return ''; const date = parseTimestamp(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return '<1m'; if (diffMins < 60) return `${diffMins}m`; if (diffHours < 24) return `${diffHours}h`; return `${diffDays}d`; }; const selectedSession = sessions.find( (s) => s.session_id === selectedSessionId, ); return (
{/* Left Panel: Session List */}
{/* Session List */} {loadingSessions && sessions.length === 0 ? (
{t('bots.sessionMonitor.loading')}
) : sessions.length === 0 ? (
{t('bots.sessionMonitor.noSessions')}
) : (
{sessions.map((session) => { const isSelected = selectedSessionId === session.session_id; return ( ); })}
)}
{/* Right Panel: Messages */}
{!selectedSessionId ? (
{t('bots.sessionMonitor.selectSession')}
) : ( <> {/* Chat Header */}
{selectedSession?.user_name || selectedSession?.user_id || selectedSessionId.slice(0, 20)}
{parseSessionType(selectedSessionId) && ( {parseSessionType(selectedSessionId)} )} {selectedSession?.platform && ( <> {parseSessionType(selectedSessionId) && ·} {selectedSession.platform} )} {selectedSession?.user_id && ( <> · {selectedSession.user_id} )} {selectedSession?.pipeline_name && ( <> · {selectedSession.pipeline_name} )} {selectedSession?.is_active && ( <> · Active )}
{/* Messages Area */}
{loadingMessages ? (
{t('bots.sessionMonitor.loading')}
) : messages.length === 0 ? (
{t('bots.sessionMonitor.noMessages')}
) : ( messages.map((msg) => { const isUser = isUserMessage(msg); return (
{renderMessageContent(msg)} {/* Role label + timestamp */}
{isUser ? t('bots.sessionMonitor.userMessage', { defaultValue: 'User', }) : t('bots.sessionMonitor.botMessage', { defaultValue: 'Assistant', })} {formatTime(msg.timestamp)} {msg.status === 'error' && ( error )} {msg.runner_name && ( {msg.runner_name} )}
); }) )}
)}
); }); export default BotSessionMonitor;