import { useState, useRef, useEffect } from 'react';
import './ChatArea.css';
const SendIcon = () => (
);
const UploadIcon = () => (
);
const PinIcon = () => (
);
const ChevronIcon = ({ open }) => (
);
const BotIcon = () => (
);
function VerdictBadge({ verdict }) {
const map = {
supported: ['badge-success', '✓ Supported'],
partially_supported: ['badge-warning', '⚬ Partial'],
refused: ['badge-error', '✕ Refused'],
loading: ['badge-muted', '…'],
};
const [cls, label] = map[verdict] || ['badge-muted', verdict];
return {label};
}
function ConfidenceBar({ value }) {
const pct = Math.round((value || 0) * 100);
return (
Confidence
75 ? 'linear-gradient(90deg,#10b981,#34d399)'
: pct > 45 ? 'linear-gradient(90deg,#f59e0b,#fbbf24)'
: 'linear-gradient(90deg,#ef4444,#f87171)' }} />
{pct}%
);
}
function CitationList({ citations }) {
if (!citations?.length) return null;
return (
{citations.map((c, i) => (
[{c.evidence_id}] p.{c.pages}
))}
);
}
function MessageBubble({ turn, onTurnClick, onPin }) {
const [showEvidence, setShowEvidence] = useState(false);
const isLoading = turn.verdict === 'loading';
const handlePin = (e) => {
e.stopPropagation();
const text = window.getSelection()?.toString() || turn.answer?.slice(0, 120) + '…';
onPin(text, turn.question);
};
return (
onTurnClick(turn)}>
{/* User */}
{turn.question}
{turn.rewritten_question && (
↻ Context-expanded: "{turn.rewritten_question.slice(0, 60)}…"
)}
{/* AI */}
{isLoading ? (
Analysing evidence…
) : (
<>
{turn.verdict !== 'refused' && turn.confidence != null && (
)}
{turn.timestamp}
{(turn.answer || '').split('\n').map((line, i) => (
{highlightCitations(line)}
))}
{turn.citations?.length > 0 && (
<>
{showEvidence && (
{turn.citations.map((c, i) => (
[{c.evidence_id}]
{c.text_preview}
p.{c.pages}
))}
)}
>
)}
{turn.citation_coverage != null && turn.verdict !== 'refused' && (
Citation coverage
{Math.round(turn.citation_coverage * 100)}%
)}
>
)}
);
}
function highlightCitations(text) {
const parts = text.split(/(\[E\d+\])/g);
return parts.map((p, i) =>
/\[E\d+\]/.test(p)
?
{p}
: p
);
}
function EmptyState({ onUpload, uploadLoading }) {
const fileRef = useRef(null);
return (
Upload a research paper to begin
Upload any academic PDF and ask questions. Answers are grounded in evidence with inline citations and hallucination detection.
{ const f = e.target.files?.[0]; if (f) onUpload(f); e.target.value = ''; }} />
Supports multi-document indexing • Conversation memory • Citation tracking
);
}
export default function ChatArea({
sessionName, turns, indexStatus, activeDocs, isLoading, error,
onAsk, onTurnClick, onPin, onClearSession, onUpload, uploadLoading,
}) {
const [question, setQuestion] = useState('');
const [useMemory, setUseMemory] = useState(true);
const [enableNli, setEnableNli] = useState(false);
const fileRef = useRef(null);
const bottomRef = useRef(null);
const inputRef = useRef(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [turns.length, isLoading]);
const submit = () => {
const q = question.trim();
if (!q || isLoading) return;
setQuestion('');
onAsk(q, { useMemory, enableNli });
};
const handleKey = (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); }
};
const hasIndex = indexStatus.chunk_count > 0;
return (
{/* Top Bar */}
{/* Error Banner */}
{error && (
⚠ {error}
)}
{/* Messages */}
{!hasIndex && turns.length === 0 ? (
) : (
<>
{turns.map((turn) => (
))}
{isLoading && turns[turns.length - 1]?.verdict !== 'loading' && (
)}
>
)}
{/* Input Bar */}
{ const f = e.target.files?.[0]; if (f) onUpload(f); e.target.value = ''; }}
/>
);
}