Rag-Assistant / frontend /src /components /ChatWindow.jsx
AsrorAsr's picture
Upload folder using huggingface_hub
66370b0 verified
Raw
History Blame Contribute Delete
6.49 kB
import { useState, useRef, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { sendChatMessage } from '../services/api';
export default function ChatWindow({ hasDocuments }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const messagesEndRef = useRef(null);
const inputRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(scrollToBottom, [messages]);
const handleSend = async () => {
const question = input.trim();
if (!question || isStreaming) return;
const userMessage = { role: 'user', content: question };
setMessages((prev) => [...prev, userMessage]);
setInput('');
setIsStreaming(true);
// Add empty assistant message that we'll fill via streaming
const assistantMessage = { role: 'assistant', content: '', sources: [] };
setMessages((prev) => [...prev, assistantMessage]);
const chatHistory = messages.map((m) => ({
role: m.role,
content: m.content,
}));
await sendChatMessage(
question,
chatHistory,
// onChunk
(chunk) => {
setMessages((prev) => {
const updated = [...prev];
const last = { ...updated[updated.length - 1] };
last.content += chunk;
updated[updated.length - 1] = last;
return updated;
});
},
// onSources
(sources) => {
setMessages((prev) => {
const updated = [...prev];
const last = { ...updated[updated.length - 1] };
last.sources = sources;
updated[updated.length - 1] = last;
return updated;
});
},
// onDone
() => setIsStreaming(false),
// onError
(error) => {
setMessages((prev) => {
const updated = [...prev];
const last = updated[updated.length - 1];
last.content = `❌ Error: ${error}`;
last.isError = true;
return [...updated];
});
setIsStreaming(false);
}
);
};
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className="chat-window">
<div className="chat-messages">
{messages.length === 0 ? (
<div className="chat-empty">
<div className="chat-empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1">
<circle cx="12" cy="12" r="10" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</svg>
</div>
<h2>RAG Assistant</h2>
<p>Upload a PDF document and ask questions about its content.</p>
<p className="hint">Your answers will be grounded in the uploaded documents.</p>
</div>
) : (
messages.map((msg, i) => (
<div key={i} className={`message ${msg.role} ${msg.isError ? 'error' : ''}`}>
<div className="message-avatar">
{msg.role === 'user' ? (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M12 2a4 4 0 0 1 4 4v2a4 4 0 0 1-8 0V6a4 4 0 0 1 4-4z" />
<path d="M6 10a6 6 0 0 0 12 0" />
<rect x="9" y="14" width="6" height="4" rx="1" />
<path d="M8 18h8v2a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2z" />
</svg>
)}
</div>
<div className="message-content">
<div className="message-role">
{msg.role === 'user' ? 'You' : 'Assistant'}
</div>
<div className="message-text markdown-body">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{msg.content}
</ReactMarkdown>
{isStreaming && i === messages.length - 1 && msg.role === 'assistant' && (
<span className="cursor-blink"></span>
)}
</div>
{msg.sources && msg.sources.length > 0 && (
<div className="message-sources">
<span className="sources-label">Sources:</span>
{msg.sources.map((src, j) => (
<span key={j} className="source-tag">
📄 {src.filename}
<span className="relevance">({(src.relevance * 100).toFixed(0)}%)</span>
</span>
))}
</div>
)}
</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
<div className="chat-input-area">
{!hasDocuments && (
<div className="no-docs-warning">
⚠️ Upload a document first to get answers based on your content
</div>
)}
<div className="chat-input-wrapper">
<textarea
ref={inputRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={hasDocuments ? 'Ask a question about your documents...' : 'Upload a document first...'}
disabled={isStreaming}
rows={1}
className="chat-input"
/>
<button
className="send-btn"
onClick={handleSend}
disabled={!input.trim() || isStreaming}
>
{isStreaming ? (
<div className="send-spinner"></div>
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="22" y1="2" x2="11" y2="13" />
<polygon points="22 2 15 22 11 13 2 9 22 2" />
</svg>
)}
</button>
</div>
</div>
</div>
);
}