import { useState } from "react"; import CitationPopover from "./CitationPopover"; import SourceExpander from "./SourceExpander"; function WebSourcesList({ sources }) { const webSources = sources?.filter((s) => s.source_type === "web") ?? []; if (webSources.length === 0) return null; return (
{webSources.map((s, i) => ( {s.title || new URL(s.url).hostname} ))}
); } function CitedText({ text, onCitationClick }) { if (!text) return null; const parts = text.split(/(\[\d+\])/g); return ( <> {parts.map((part, i) => { const match = part.match(/^\[(\d+)\]$/); if (match) { const idx = parseInt(match[1], 10); return ( e.stopPropagation()} onClick={(e) => onCitationClick(idx, e.currentTarget)} className="ml-0.5 text-indigo-500 font-semibold cursor-pointer hover:text-indigo-700 text-xs select-none" title={`View source [${idx}]`} > [{idx}] ); } return {part}; })} ); } export default function MessageBubble({ message }) { const isUser = message.role === "user"; const [openCitation, setOpenCitation] = useState(null); // { idx, rect } | null function handleCitationClick(idx, el) { if (openCitation?.idx === idx) { setOpenCitation(null); } else { setOpenCitation({ idx, rect: el.getBoundingClientRect() }); } } const activeSrc = openCitation && message.sources ? message.sources.find((s) => s.citation_index === openCitation.idx) || message.sources[openCitation.idx - 1] : null; return (

{isUser ? ( message.content ) : message.loading && !message.content ? ( ) : ( <> {message.loading && ( )} )}

{!isUser && !message.loading && message.sources && ( )} {!isUser && !message.loading && message.sources && ( )}
{openCitation && activeSrc && ( setOpenCitation(null)} /> )}
); }