import React, { useState, useRef, useEffect } from 'react'; import axios from 'axios'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneLight, oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { useTheme } from './ui/theme-provider'; import { Button } from './ui/button'; import { Input } from './ui/input'; import Quiz from './Quiz'; // Helper function to parse thinking and answer sections const parseThinkingAnswer = (text) => { // Try to match the think tag, even if it's incomplete const thinkingRegex = /([\s\S]*?)(?:<\/think>|$)/i; const thinkingMatch = thinkingRegex.exec(text); // If thinking section is found (even partially) if (thinkingMatch) { // Get the text after the thinking section, including if is not yet complete const thinkingEndIndex = thinkingMatch.index + thinkingMatch[0].length; const restOfText = text.substring(thinkingEndIndex).trim(); // Check for explicit answer tag, even if incomplete const answerRegex = /([\s\S]*?)(?:<\/answer>|$)/i; const answerMatch = answerRegex.exec(restOfText); if (answerMatch) { // Both thinking and answer tags found return { thinking: thinkingMatch[1].trim(), answer: answerMatch[1].trim(), hasFormatting: true }; } else { // Only thinking tag found, treat the rest as the answer return { thinking: thinkingMatch[1].trim(), answer: restOfText, hasFormatting: true }; } } // Check if there's just an answer tag, even if incomplete const answerRegex = /([\s\S]*?)(?:<\/answer>|$)/i; const answerMatch = answerRegex.exec(text); if (answerMatch) { return { thinking: "", answer: answerMatch[1].trim(), hasFormatting: true }; } // If no formatting is found, return the original text as the answer return { thinking: '', answer: text, hasFormatting: false }; }; const Chat = ({ sessionId, userId, docDescription, suggestedQuestions, selectedQuestion, onQuestionSelected }) => { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [userQuestionCount, setUserQuestionCount] = useState(0); const [showQuizPrompt, setShowQuizPrompt] = useState(false); const [showQuiz, setShowQuiz] = useState(false); const [quizQuestions, setQuizQuestions] = useState([]); const [quizLoading, setQuizLoading] = useState(false); const [quizResults, setQuizResults] = useState(null); const [expandedThinking, setExpandedThinking] = useState({}); const [activeEventSource, setActiveEventSource] = useState(null); const messagesEndRef = useRef(null); const inputRef = useRef(null); const { theme } = useTheme(); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages]); // Handle when a suggested question is selected useEffect(() => { if (selectedQuestion) { setInput(selectedQuestion); onQuestionSelected(); // Clear the selected question after setting it } }, [selectedQuestion, onQuestionSelected]); const handleInputChange = (e) => { setInput(e.target.value); }; const toggleThinking = (messageId) => { setExpandedThinking(prev => ({ ...prev, [messageId]: !prev[messageId] })); }; const handleSubmit = async (e) => { e.preventDefault(); if (!input.trim()) return; const userMessage = input; setInput(''); // Add user message to chat const userMessageId = Date.now(); console.log('Adding user message:', userMessageId, userMessage); setMessages(prevMessages => [ ...prevMessages, { text: userMessage, sender: 'user', id: userMessageId } ]); // Check if user is explicitly asking for a quiz const quizRequestPatterns = [ /quiz me/i, /take (a|the) quiz/i, /start (a|the) quiz/i, /test (my )?knowledge/i, /give me (a|the) quiz/i, /create (a|the) quiz/i, /can (i|you) (do|have|take) (a|the) quiz/i ]; const isQuizRequest = quizRequestPatterns.some(pattern => pattern.test(userMessage)); if (isQuizRequest && !showQuiz && !quizLoading) { // Add user message acknowledging quiz request const messageId = Date.now() + 1; setMessages(prevMessages => [ ...prevMessages, { text: "I'd be happy to create a quiz based on this document! Generating questions now...", sender: 'ai', id: messageId, hasFormatting: false } ]); // Start quiz generation handleQuizGeneration(); return; } setIsLoading(true); try { // Create placeholder message for streaming content with a guaranteed unique ID const messageId = userMessageId + 100; console.log('Creating AI message placeholder:', messageId); setMessages(prevMessages => [ ...prevMessages, { text: "", sender: 'ai', id: messageId, thinking: "", answer: "", hasFormatting: false, isStreaming: true } ]); // Close any existing EventSource before creating a new one if (activeEventSource) { console.log('Closing existing EventSource'); activeEventSource.close(); } // Create EventSource for streaming connection - Include user ID if available const queryParams = new URLSearchParams({ session_id: sessionId, query: userMessage }); // Add user ID if available if (userId) { queryParams.append('user_id', userId); } const eventSource = new EventSource(`/stream?${queryParams.toString()}`, { withCredentials: true }); // Store the event source in state setActiveEventSource(eventSource); let streamedText = ""; eventSource.onmessage = (event) => { const data = JSON.parse(event.data); streamedText += data.text; // Parse current content const parsedResponse = parseThinkingAnswer(streamedText); console.log('Updating message with chunk, ID:', messageId); // Update message with streamed content setMessages(prevMessages => prevMessages.map(msg => msg.id === messageId ? { ...msg, text: streamedText, thinking: parsedResponse.thinking, answer: parsedResponse.answer, hasFormatting: parsedResponse.hasFormatting, isStreaming: true, sender: 'ai' } : msg )); }; // Listen for stream completion event eventSource.addEventListener('complete', (event) => { console.log('Stream complete, closing EventSource'); eventSource.close(); setActiveEventSource(null); const parsedResponse = parseThinkingAnswer(streamedText); // Update message with final streamed content setMessages(prevMessages => prevMessages.map(msg => msg.id === messageId ? { ...msg, text: streamedText, thinking: parsedResponse.thinking, answer: parsedResponse.answer, hasFormatting: parsedResponse.hasFormatting, isStreaming: false, sender: 'ai' } : msg ) ); // Increment question count after successful response const newCount = userQuestionCount + 1; setUserQuestionCount(newCount); // Check if we should show quiz prompt (after 3+ questions and not already shown) if (newCount >= 3 && !showQuizPrompt && !showQuiz && !quizResults) { setShowQuizPrompt(true); } setIsLoading(false); }); eventSource.onerror = (error) => { console.error('EventSource error:', error); eventSource.close(); setActiveEventSource(null); // If we got a partial response, keep it if (streamedText) { const parsedResponse = parseThinkingAnswer(streamedText); // Update message with final streamed content setMessages(prevMessages => prevMessages.map(msg => msg.id === messageId ? { ...msg, text: streamedText, thinking: parsedResponse.thinking, answer: parsedResponse.answer, hasFormatting: parsedResponse.hasFormatting, isStreaming: false, sender: 'ai' } : msg ) ); } else { // If we got no response, show error setMessages(prevMessages => [ ...prevMessages.filter(msg => msg.id !== messageId), // Remove placeholder { text: 'Sorry, there was an error processing your request.', sender: 'ai', isError: true, id: Date.now() } ]); } setIsLoading(false); }; eventSource.onopen = () => { console.log('EventSource connected'); }; } catch (error) { console.error('Error getting response:', error); // Add error message to chat setMessages(prevMessages => [ ...prevMessages, { text: 'Sorry, there was an error processing your request.', sender: 'ai', isError: true, id: Date.now() } ]); setIsLoading(false); } }; const handleQuizGeneration = async () => { setQuizLoading(true); setShowQuizPrompt(false); try { const response = await axios.post('/generate-quiz', { session_id: sessionId, num_questions: 5, user_id: userId // Include user ID if available }); setQuizQuestions(response.data.questions); setShowQuiz(true); } catch (error) { console.error('Error generating quiz:', error); // Add error message to chat setMessages(prevMessages => [ ...prevMessages, { text: 'Sorry, there was an error generating the quiz. Please try again later.', sender: 'ai', isError: true, id: Date.now(), hasFormatting: false } ]); } finally { setQuizLoading(false); } }; const handleAcceptQuiz = async () => { setShowQuizPrompt(false); setQuizLoading(true); // Add message about starting quiz generation setMessages(prevMessages => [ ...prevMessages, { text: "I'll create a knowledge quiz based on this document. Please wait a moment...", sender: 'ai', id: Date.now(), hasFormatting: false } ]); handleQuizGeneration(); }; const handleDeclineQuiz = () => { setShowQuizPrompt(false); // Add message acknowledging user's choice setMessages(prevMessages => [ ...prevMessages, { text: "No problem! Feel free to continue asking questions about the document.", sender: 'ai', id: Date.now(), hasFormatting: false } ]); }; const handleQuizComplete = (results) => { setQuizResults(results); setShowQuiz(false); // Add quiz results to chat const resultMessage = ` ## Quiz Results 📊 You answered **${results.correctAnswers}** out of **${results.totalQuestions}** questions correctly (**${Math.round(results.score)}%**). ${results.score >= 80 ? "Great job! You have a solid understanding of the material." : results.score >= 60 ? "Good work! You're on the right track." : "Keep learning! Review the document to improve your understanding."} `; setMessages(prevMessages => [ ...prevMessages, { text: resultMessage, sender: 'ai', id: Date.now(), hasFormatting: false } ]); }; const handleCloseQuiz = () => { setShowQuiz(false); }; // Component for rendering markdown with custom styles const MarkdownContent = ({ content }) => (

, h1: ({ node, ...props }) =>

, h2: ({ node, ...props }) =>

, h3: ({ node, ...props }) =>

, ul: ({ node, ...props }) =>