import { useState, useRef, useEffect, KeyboardEvent, Dispatch, SetStateAction } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { askQuestionStream } from '../api' import type { Citation, Document, HistoryMessage } from '../api' import type { Message } from '../types' import type { Tr } from '../i18n' import styles from './ChatView.module.css' const STATUS_LABELS: Record = { searching: 'Recherche dans les documents…', querying: 'Analyse des données…', generating: 'Rédaction de la réponse…', } function formatCitation(c: Citation): string { const page = c.page if (typeof page === 'string') return `${c.filename} · ${page}` if (page > 0) return `${c.filename} p.${page}` return c.filename } interface Props { documents: Document[] messages: Message[] setMessages: Dispatch> onSave?: (msgs: Message[]) => void tr: Tr lang: string } export default function ChatView({ documents, messages, setMessages, onSave, tr, lang }: Props) { const [input, setInput] = useState('') const [loading, setLoading] = useState(false) const [streamStatus, setStreamStatus] = useState('') const bottomRef = useRef(null) useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, loading]) const send = async () => { const q = input.trim() if (!q || loading) return setInput('') setStreamStatus('') const userMsg: Message = { role: 'user', text: q } setMessages(prev => [...prev, userMsg]) setLoading(true) const history: HistoryMessage[] = messages.map(m => ({ role: m.role, content: m.text })) setMessages(prev => [...prev, { role: 'assistant', text: '' }]) try { await askQuestionStream( q, history, 6, '', (token) => { setStreamStatus('') setMessages(prev => { const copy = [...prev] copy[copy.length - 1] = { ...copy[copy.length - 1], text: copy[copy.length - 1].text + token, } return copy }) }, (meta) => { setStreamStatus('') setMessages(prev => { const copy = [...prev] const last = copy[copy.length - 1] const text = meta.path === 'sql' && meta.answer ? meta.answer : last.text copy[copy.length - 1] = { ...last, text, meta } onSave?.(copy) return copy }) }, (err) => { setStreamStatus('') setMessages(prev => { const copy = [...prev] copy[copy.length - 1] = { role: 'assistant', text: `Error: ${err}` } return copy }) }, (s) => setStreamStatus(s), lang, ) } finally { setLoading(false) setStreamStatus('') } } const onKey = (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() send() } } const empty = documents.length === 0 return (
{tr.docCount(documents.length)} {messages.length > 0 && ( )}
{messages.length === 0 && (
{empty ? tr.emptyNoDoc : tr.emptyWithDoc}
)} {messages.map((m, i) => (
{m.role === 'assistant' && Julia} {m.role === 'assistant' ? ( loading && i === messages.length - 1 && m.text === '' ? ( {streamStatus ? (STATUS_LABELS[streamStatus] ?? tr.thinking) : tr.thinking} ) : (
{m.text}
) ) : (

{m.text}

)} {m.meta && (
{m.meta.citations.length > 0 && (
{m.meta.citations.map((c, j) => ( {formatCitation(c)} ))}
)} {m.meta.path === 'sql' ? 'SQL' : 'Vector'} · {m.meta.chunks_used} chunks
)}
))}