import { useEffect, useRef, useState } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { queryDocuments } from '../api/api' import CitationPanel from './CitationPanel' import { useToast } from './Toast' import { useAuth } from '../contexts/AuthContext' const WELCOME = { role: 'assistant', content: `Hello! I'm **ASK Docs**, your intelligent document assistant.\n\nUpload a document using the sidebar, then ask me anything about it — I'll answer with contextual accuracy, grounded in your document content.`, citations: [], } function mdToHtml(raw) { const escaped = raw .replace(/&/g, '&') .replace(//g, '>') const lines = escaped.split('\n') const out = [] let inUl = false, inOl = false, inPre = false for (let i = 0; i < lines.length; i++) { let l = lines[i] // fenced code blocks if (l.trim().startsWith('```')) { if (!inPre) { out.push('
'); inPre = true }
      else         { out.push('
'); inPre = false } continue } if (inPre) { out.push(l + '\n'); continue } // close lists on blank line if (!l.trim()) { if (inUl) { out.push(''); inUl = false } if (inOl) { out.push(''); inOl = false } out.push('
') continue } // headings if (/^### /.test(l)) { out.push(`

${l.slice(4)}

`); continue } if (/^## /.test(l)) { out.push(`

${l.slice(3)}

`); continue } if (/^# /.test(l)) { out.push(`

${l.slice(2)}

`); continue } // unordered list if (/^[-*] /.test(l)) { if (!inUl) { out.push(''); inUl = false } if (inOl) { out.push(''); inOl = false } out.push('

' + inlineMd(l) + '

') } if (inUl) out.push('') if (inOl) out.push('') return out.join('') } function inlineMd(s) { return s .replace(/\*\*\*(.+?)\*\*\*/g, '$1') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/~~(.+?)~~/g, '$1') } function buildChatHtml(messages, userName, userPhoto) { const count = messages.length - 1 const date = new Date().toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'short' }) const rows = messages.slice(1).map((msg) => { const isUser = msg.role === 'user' const avatar = isUser ? (userPhoto ? `` : `
${(userName || 'U')[0].toUpperCase()}
`) : `
` const name = isUser ? (userName || 'You') : 'ASK Docs' const body = isUser ? `

${msg.content.replace(/&/g,'&').replace(//g,'>')}

` : mdToHtml(msg.content) const citBlock = (!isUser && msg.citations?.length) ? `

Sources

${msg.citations.map((c, ci) => `
${ci+1} ${c.documentName}  ·  Page ${c.pageNumber}  ·  ${Math.round(c.score * 100)}% match
`).join('')}
` : '' return `
${avatar}

${name}

${body}
${citBlock}
` }).join('') return ` ASK Docs — Chat Export

ASK Docs

AI Document Assistant

📅 ${date} 💬 ${count} message${count !== 1 ? 's' : ''} ${userName ? `👤 ${userName}` : ''}
${rows}
` } const CHAT_KEY = 'askdocs_chat' export default function ChatInterface() { const toast = useToast() const { user } = useAuth() // Load from sessionStorage so chat survives F5 reloads. // sessionStorage is cleared automatically when the tab is closed. const [messages, setMessages] = useState(() => { try { const stored = sessionStorage.getItem(CHAT_KEY) if (stored) return JSON.parse(stored) } catch {} return [WELCOME] }) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const bottomRef = useRef(null) const textareaRef = useRef(null) // Persist messages to sessionStorage whenever they change useEffect(() => { try { sessionStorage.setItem(CHAT_KEY, JSON.stringify(messages)) } catch {} }, [messages]) const downloadPDF = () => { if (messages.length <= 1) { toast.error('No chat to export yet.'); return } const win = window.open('', '_blank') if (!win) { toast.error('Allow pop-ups to download the chat PDF.'); return } const userName = user?.firebaseUser?.displayName || '' const userPhoto = user?.firebaseUser?.photoURL || '' win.document.open() win.document.write(buildChatHtml(messages, userName, userPhoto)) win.document.close() win.focus() setTimeout(() => { win.print(); win.close() }, 500) } useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, loading]) useEffect(() => { const ta = textareaRef.current if (!ta) return ta.style.height = 'auto' ta.style.height = `${Math.min(ta.scrollHeight, 160)}px` }, [input]) const send = async () => { const question = input.trim() if (!question || loading) return setMessages(m => [...m, { role: 'user', content: question, citations: [] }]) setInput('') setLoading(true) try { const data = await queryDocuments(question) setMessages(m => [...m, { role: 'assistant', content: data.answer, citations: data.citations ?? [] }]) } catch (err) { const msg = err.response?.data?.error ?? err.message ?? 'Something went wrong.' toast.error(msg) setMessages(m => [...m, { role: 'assistant', content: `**Error:** ${msg}`, citations: [] }]) } finally { setLoading(false) } } const handleKey = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send() } } return (
{/* Toolbar */}

{messages.length - 1} message{messages.length - 1 !== 1 ? 's' : ''}

{messages.length > 1 && (
)}
{/* Messages */}
{messages.map((msg, i) => ( ))} {loading && } {/* Nudge shown only when no conversation has started yet */} {messages.length === 1 && !loading && (

Upload a document first

Use the sidebar to upload a PDF, DOCX, image, or spreadsheet, then ask away.

)}
{/* Input area */}