import { useState, useRef, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; const logo = "https://huggingface.co/spaces/JenishMakwana/RAG/resolve/main/frontend-react/public/logo.png"; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import { Send, Paperclip, Mic, MicOff, PanelLeftClose, PanelLeftOpen, Sparkles, Search, Users, FileText, Volume2, VolumeX, ChevronRight, CheckCircle, MessageSquare, BrainCircuit, ShieldCheck, Scale, AlertCircle } from 'lucide-react'; import { chatQuery, fetchChatHistory, uploadDocument, transcribeVoice, getTtsAudio } from '../api'; // ... (existing functions) export default function ChatWindow({ token, messages, setMessages, sessionId, onFirstMessage, setIsUploading, onUploadSuccess, isSidebarCollapsed, setIsSidebarCollapsed, sessionDocuments, setSessionDocuments, onLogout }) { const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [historyLoading, setHistoryLoading] = useState(true); const [uploading, setUploading] = useState(false); const [isRecording, setIsRecording] = useState(false); const [isProcessingVoice, setIsProcessingVoice] = useState(false); const [speakingIdx, setSpeakingIdx] = useState(null); const messagesEndRef = useRef(null); const audioRef = useRef(null); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); const [selectedFiles, setSelectedFiles] = useState([]); const [pendingSelectionQuery, setPendingSelectionQuery] = useState(null); // TTS Highlighting State const [highlightedInfo, setHighlightedInfo] = useState({ msgIdx: null, sentence: null, wordIdx: -1 }); const highlightTimerRef = useRef(null); /** * Injects tags into raw markdown while preserving formatting. * This handles the core logic of 'Seamless Highlighting' */ const injectHighlighting = (rawText, msgIdx) => { if (highlightedInfo.msgIdx !== msgIdx || !highlightedInfo.sentence) { return rawText; } const { sentence } = highlightedInfo; // Find the exact sentence block. const sentencePos = rawText.indexOf(sentence); if (sentencePos === -1) return rawText; const highlightedSentence = `${sentence}`; const before = rawText.substring(0, sentencePos); const after = rawText.substring(sentencePos + sentence.length); return before + highlightedSentence + after; }; const toggleFileSelection = (filename) => { setSelectedFiles(prev => { const exists = prev.includes(filename); if (exists) return prev.filter(f => f !== filename); return [...prev, filename]; }); }; const renderMessageContent = (text, msgIdx) => { if (!text || typeof text !== 'string') return text; const sourceMarker = '[Source:'; let content = text; let source = ''; if (text.includes(sourceMarker)) { const parts = text.split(sourceMarker); content = parts[0]; source = sourceMarker + parts.slice(1).join(sourceMarker); } const processedContent = injectHighlighting(content, msgIdx); return ( <> {processedContent} {source && (
{source}
)} ); }; const renderErrorMessage = (text) => { if (!text || typeof text !== 'string') return text; const lines = text.split('\n'); const headerText = lines[0]; const detailText = lines.slice(1).join('\n'); return (
{headerText}
{detailText && (
{detailText}
)}
); }; const handleUpload = async (e) => { const files = Array.from(e.target.files); if (files.length === 0) return; setUploading(true); setIsUploading(true); try { const results = await Promise.allSettled( files.map(async (file) => { try { const result = await uploadDocument(token, file, sessionId); return { file, result, success: true }; } catch (err) { return { file, error: err.message, status: err.status, success: false }; } }) ); const successful = results.filter(r => r.value && r.value.success); const failed = results.filter(r => (r.value && !r.value.success) || r.status === 'rejected'); const isUnauthorized = failed.some(r => r.value?.status === 401); if (isUnauthorized && onLogout) { onLogout(); return; } if (successful.length > 0) { if (onUploadSuccess) onUploadSuccess(); const newDocs = successful.map(r => ({ filename: r.value.file.name, chunks: r.value.result.chunks })); setSessionDocuments(prev => [...prev, ...newDocs]); const successMsg = successful.length === 1 ? `✅ Attached document: **${successful[0].value.file.name}**` : `✅ Attached **${successful.length}** documents successfully.`; setMessages(prev => [...prev, { role: 'assistant', text: successMsg }]); } if (failed.length > 0) { const errorMsg = failed.map(r => { const name = r.value?.file?.name || "Unknown file"; const error = r.value?.error || "Upload failed"; return `- ${name}: ${error}`; }).join('\n'); setMessages(prev => [...prev, { role: 'error', text: `Failed to upload some documents:\n${errorMsg}` }]); } } catch (err) { alert("An unexpected error occurred during upload."); } finally { setUploading(false); setIsUploading(false); e.target.value = null; } }; useEffect(() => { const loadHistory = async () => { setHistoryLoading(true); try { const data = await fetchChatHistory(token, sessionId); setMessages(data.history); setSessionDocuments(data.documents || []); setSelectedFiles([]); // Reset selection when switching chats } catch (err) { setMessages([]); setSessionDocuments([]); if (err.status === 401 && onLogout) { onLogout(); } } finally { setHistoryLoading(false); } }; if (token && sessionId) loadHistory(); }, [token, sessionId, setMessages, setSessionDocuments, onLogout]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const handleStartChat = () => { setMessages([ { role: 'assistant', text: 'Hello! I am your Legal Case Assistant. How can I help you with your case analysis or document research today?' } ]); }; const submitQuery = async (queryText, force = false) => { if (!queryText.trim() || loading) return; // Check if we need to "ask" for document focus if (!force && sessionDocuments.length > 1 && selectedFiles.length === 0) { setPendingSelectionQuery(queryText); return; } const isFirstMessage = !messages.some(m => m.role === 'user'); setMessages(prev => [...prev, { role: 'user', text: queryText }]); setLoading(true); try { // Send the list of selected filenames (if any) const filenames = selectedFiles.length > 0 ? selectedFiles : null; const response = await chatQuery(token, queryText, sessionId, null, filenames); const aiMessage = { role: 'assistant', text: '' }; setMessages(prev => [...prev, aiMessage]); const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullText = ''; while (true) { const { done, value } = await reader.read(); if (done) break; fullText += decoder.decode(value, { stream: true }); setMessages(prev => { const newMessages = [...prev]; newMessages[newMessages.length - 1] = { ...newMessages[newMessages.length - 1], text: fullText }; return newMessages; }); } if (isFirstMessage) onFirstMessage(); } catch (err) { setMessages(prev => [...prev, { role: 'error', text: err.message }]); if (err.status === 401 && onLogout) { onLogout(); } } finally { setLoading(false); } }; const handleSubmit = (e) => { e.preventDefault(); if (uploading) return; const queryText = input; if (!queryText.trim()) return; setInput(''); submitQuery(queryText); }; const handleConfirmSelection = () => { if (pendingSelectionQuery) { const queryToSubmit = pendingSelectionQuery; setPendingSelectionQuery(null); submitQuery(queryToSubmit, true); } }; const toggleVoice = async () => { if (isRecording) { if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') { mediaRecorderRef.current.stop(); } } else { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); mediaRecorderRef.current = mediaRecorder; audioChunksRef.current = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { audioChunksRef.current.push(event.data); } }; mediaRecorder.onstop = async () => { const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' }); setIsRecording(false); setIsProcessingVoice(true); // Stop all audio tracks to release the microphone stream.getTracks().forEach(track => track.stop()); try { const result = await transcribeVoice(token, audioBlob); if (result.query) setInput(result.query); } catch (err) { alert('Voice processing failed: ' + err.message); } finally { setIsProcessingVoice(false); } }; mediaRecorder.start(); setIsRecording(true); } catch (err) { setIsRecording(false); alert('Microphone error: ' + (err.name || 'Unknown') + ' - ' + (err.message || err)); } } }; const audioQueueRef = useRef([]); const abortControllerRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); // Play next item in queue const playNextInQueue = () => { if (highlightTimerRef.current) clearInterval(highlightTimerRef.current); if (audioQueueRef.current.length === 0) { setIsPlaying(false); setSpeakingIdx(null); setHighlightedInfo({ msgIdx: null, sentence: null, wordIdx: -1 }); return; } const { url, text: sentenceText } = audioQueueRef.current.shift(); setIsPlaying(true); // Update the single audio element's source (iOS/Mobile compatible) audioRef.current.src = url; audioRef.current.load(); audioRef.current.onplay = () => { setHighlightedInfo(prev => ({ ...prev, sentence: sentenceText, wordIdx: 0 })); }; audioRef.current.onended = () => { URL.revokeObjectURL(url); playNextInQueue(); }; audioRef.current.play().catch(err => { console.error("Playback error", err); playNextInQueue(); }); }; const handleSpeak = async (text, index) => { // If clicking same button while playing, STOP everything if (speakingIdx === index) { if (audioRef.current) { audioRef.current.onplay = null; audioRef.current.onended = null; audioRef.current.pause(); audioRef.current.src = ""; audioRef.current.load(); } if (abortControllerRef.current) { abortControllerRef.current.abort(); } if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current); audioQueueRef.current.forEach(item => URL.revokeObjectURL(item.url)); audioQueueRef.current = []; setSpeakingIdx(null); setIsPlaying(false); setHighlightedInfo({ msgIdx: null, sentence: null, wordIdx: -1 }); return; } // Stop current if any if (audioRef.current) { audioRef.current.onplay = null; audioRef.current.onended = null; audioRef.current.pause(); audioRef.current.src = ""; audioRef.current.load(); } if (abortControllerRef.current) abortControllerRef.current.abort(); if (highlightTimerRef.current) clearTimeout(highlightTimerRef.current); audioQueueRef.current.forEach(item => URL.revokeObjectURL(item.url)); audioQueueRef.current = []; // Create & unlock the single Audio element synchronously in the user gesture context if (!audioRef.current) { audioRef.current = new Audio(); } // Mobile browsers require a synchronous play/pause on a user gesture to lift restriction audioRef.current.play().then(() => { audioRef.current.pause(); }).catch(() => { // Ignore initial play error on empty source }); setSpeakingIdx(index); setHighlightedInfo({ msgIdx: index, sentence: null, wordIdx: -1 }); const controller = new AbortController(); abortControllerRef.current = controller; try { const response = await getTtsAudio(token, text, controller.signal); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let startedPlaying = false; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); // Keep last partial line for (const line of lines) { if (!line.trim()) continue; try { const { audio: b64, text: sentenceText } = JSON.parse(line); const byteCharacters = atob(b64); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); // Queue only the url and text (avoiding creating new Audio elements asynchronously) audioQueueRef.current.push({ url, text: sentenceText }); // If we haven't started playing, start now if (!startedPlaying) { startedPlaying = true; playNextInQueue(); } } catch (e) { console.error("Error parsing audio chunk JSON:", e); } } } } catch (err) { if (err.name !== 'AbortError') { console.error("TTS Stream error:", err); setSpeakingIdx(null); } } }; return (

Legal Case Analysis

{historyLoading ? (

Restoring conversation...

) : ( <> {messages.length === 0 && (
Logo

Legal Case Assistant

Analyze complex case files, retrieve precise legal citations, and verify precedents securely.

Always Online
Smart Search
Ironclad Privacy
)} {messages.map((m, i) => (
{m.role === 'error' ? ( renderErrorMessage(m.text) ) : (
{m.role === 'assistant' ? ( renderMessageContent(m.text, i) ) : ( m.text )}
)} {m.role === 'assistant' && !m.text.startsWith('✅') && !m.text.includes('No documents found') && ( )}
))} {loading && (
Analyzing
)}
)}
{messages.length > 0 && (
{sessionDocuments.length > 0 && pendingSelectionQuery && (

Select focus for your question

{selectedFiles.length === 0 ? 'All Documents' : `${selectedFiles.length} selected`}
setSelectedFiles([])} >
{selectedFiles.length === 0 ? : 1}
All Documents
{sessionDocuments.map((doc, idx) => { const isActive = selectedFiles.includes(doc.filename); return (
toggleFileSelection(doc.filename)} >
{isActive ? : idx + 2}
{doc.filename}
); })}
)} {uploading && (
Processing and embedding documents... Please wait.
)}
setInput(e.target.value)} placeholder={ isProcessingVoice ? "Processing voice..." : selectedFiles.length > 0 ? `Searching ${selectedFiles.length} doc${selectedFiles.length > 1 ? 's' : ''}...` : sessionDocuments.length > 0 ? "Search documents..." : "Ask a question..." } disabled={loading || isProcessingVoice} />
)}
); }