Spaces:
Runtime error
Runtime error
File size: 6,491 Bytes
32c4f08 66370b0 32c4f08 66370b0 32c4f08 66370b0 32c4f08 66370b0 32c4f08 66370b0 32c4f08 66370b0 32c4f08 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | 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>
);
}
|