import React, { useState, useRef, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; // @ts-ignore import remarkGfm from 'remark-gfm'; import { Icons } from '../constants'; import { sendChatMessage } from '../services/geminiService'; import { ChatMessage, AIModel, AVAILABLE_MODELS } from '../types'; import CompactModelSelector from './CompactModelSelector'; interface ChatInterfaceProps { onNavigateToPage: (page: number) => void; } const ChatInterface: React.FC = ({ onNavigateToPage }) => { const [selectedModel, setSelectedModel] = useState(AVAILABLE_MODELS[0]); const [messages, setMessages] = useState([ { role: 'model', content: "Hello! I've analyzed your document. Ask me anything about it." } ]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [useRLM, setUseRLM] = useState(false); const messagesEndRef = useRef(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [messages]); const handleSend = async () => { if (!input.trim() || loading) return; const userMsg: ChatMessage = { role: 'user', content: input }; setMessages(prev => [...prev, userMsg]); setInput(''); setLoading(true); try { // Build conversation history from previous messages (excluding the current one we just added) // Format: [{role: "user"|"assistant", content: "..."}] // Skip the initial greeting and only include actual conversation const conversationHistory = messages .filter((msg, idx) => idx > 0) // Skip initial AI greeting .map(msg => ({ role: msg.role === 'model' ? 'assistant' : 'user', content: msg.content })); // For RLM mode, we can optionally skip history as deep research handles context differently // But for normal chat, always pass history const historyToSend = useRLM ? [] : conversationHistory; const response = await sendChatMessage(userMsg.content, historyToSend, selectedModel, useRLM); const botMsg: ChatMessage = { role: 'model', content: response.answer, reasoning_trace: response.reasoning_trace }; setMessages(prev => [...prev, botMsg]); } catch (error) { setMessages(prev => [...prev, { role: 'model', content: "Sorry, I encountered an error answering that. Please try again." }]); } finally { setLoading(false); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return (
{/* Header with Model Selector */}
AI Assistant
{/* Messages Area */}
{messages.map((msg, idx) => { const isUser = msg.role === 'user'; // Convert page citations to clickable links // Handle multiple formats: [Page X], [page X], (Page X), Page X, etc. // Match [Page X], (Page X), Page X, p. X formats (case insensitive) let markdownContent = msg.content // Standardize [Page X] -> [Page X](citation:X) .replace(/\[(?:page|p\.?)\s*(\d+)\]/gi, '[Page $1](citation:$1)') // Standardize (Page X) -> [Page X](citation:X) .replace(/\((?:page|p\.?)\s*(\d+)\)/gi, '[Page $1](citation:$1)') // Standardize plain "Page X" if not already linked -> [Page X](citation:X) // Negative lookbehind/ahead would be ideal but complex in JS regex for all cases, // so we use a simpler approach that avoids double-linking .replace(/(?
{isUser ? (
{msg.content}
) : (
{msg.reasoning_trace && msg.reasoning_trace.length > 0 && (
Deep Research ({msg.reasoning_trace.length} steps)
{msg.reasoning_trace.map((step, sIdx) => (
{step.step} {step.thought}
{step.type === 'execute' && step.content && (
{step.content}
)}
))}
)}
{ // Check for citation: prefix if (href?.startsWith('citation:')) { const pageNum = parseInt(href.replace('citation:', ''), 10); return ( ); } // Also catch any remaining page links that slip through const childText = String(children); const pageMatch = childText.match(/page\s*(\d+)/i); if (pageMatch) { const pageNum = parseInt(pageMatch[1], 10); return ( ); } return {children}; }, table: ({ node: _node, ...props }) => (
), thead: ({ node: _node, ...props }) => , th: ({ node: _node, ...props }) =>
, td: ({ node: _node, ...props }) => , code: ({ node: _node, className, children, ...props }: any) => { const match = /language-(\w+)/.exec(className || ''); return match ? ( ) : ( ); }, p: ({ node: _node, ...props }) => (

), }} > {markdownContent} )} ); })} {loading && (

{useRLM ? 'Deep researching...' : 'Thinking...'}
)}
{/* Input Area */}