Remove chunks from API response and frontend display, and updating most of the files for final
3d910e2 | import React, { useState, useRef, useEffect } from 'react'; | |
| import './App.css'; | |
| import axios from 'axios'; | |
| import ReactMarkdown from 'react-markdown'; | |
| import remarkGfm from 'remark-gfm'; | |
| // Use relative URL for production (Hugging Face Spaces), absolute for local dev | |
| const API_URL = process.env.REACT_APP_API_URL || | |
| (process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:8000'); | |
| const DOCS_URL = process.env.REACT_APP_DOCS_URL || `${API_URL}/documents`; | |
| function App() { | |
| const [messages, setMessages] = useState([]); | |
| const [input, setInput] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [previewUrl, setPreviewUrl] = useState(null); | |
| const [previewFilename, setPreviewFilename] = useState(''); | |
| const [previewError, setPreviewError] = useState(null); | |
| const [previewLoading, setPreviewLoading] = useState(false); | |
| const messagesEndRef = useRef(null); | |
| const previewUrlRef = useRef(null); | |
| const scrollToBottom = () => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }; | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages]); | |
| useEffect(() => { | |
| return () => { | |
| if (previewUrlRef.current) { | |
| URL.revokeObjectURL(previewUrlRef.current); | |
| } | |
| }; | |
| }, []); | |
| const handleSend = async (e) => { | |
| e.preventDefault(); | |
| if (!input.trim() || loading) return; | |
| // Use unique IDs to prevent collision | |
| const baseTime = Date.now(); | |
| const userMessageId = baseTime; | |
| const assistantMessageId = baseTime + 1; // Ensure different ID | |
| const userMessage = { id: userMessageId, role: 'user', content: input }; | |
| setMessages(prev => [...prev, userMessage]); | |
| setInput(''); | |
| setLoading(true); | |
| try { | |
| const response = await axios.post(`${API_URL}/ask`, { | |
| question: input | |
| }); | |
| const assistantMessage = { | |
| id: assistantMessageId, | |
| role: 'assistant', | |
| content: response.data.answer, | |
| sources: response.data.sources || [] | |
| }; | |
| setMessages(prev => [...prev, assistantMessage]); | |
| } catch (error) { | |
| const errorMessage = { | |
| id: assistantMessageId, | |
| role: 'assistant', | |
| content: error.response?.data?.detail || error.message || 'عذراً، حدث خطأ. يرجى المحاولة مرة أخرى.', | |
| error: true | |
| }; | |
| setMessages(prev => [...prev, errorMessage]); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const handleClearHistory = async () => { | |
| if (window.confirm('هل أنت متأكد من أنك تريد مسح سجل المحادثة؟')) { | |
| try { | |
| await axios.post(`${API_URL}/clear-history`); | |
| setMessages([]); // Clear local messages state | |
| } catch (error) { | |
| console.error('Error clearing history:', error); | |
| alert('فشل مسح السجل. يرجى المحاولة مرة أخرى.'); | |
| } | |
| } | |
| }; | |
| const handleDownload = (source) => { | |
| if (!source) return; | |
| const filename = source.split('/').pop() || source; | |
| const url = `${DOCS_URL}/${encodeURIComponent(filename)}?mode=download`; | |
| window.open(url, '_blank'); | |
| }; | |
| const handleClosePreview = () => { | |
| if (previewUrlRef.current) { | |
| URL.revokeObjectURL(previewUrlRef.current); | |
| previewUrlRef.current = null; | |
| } | |
| setPreviewUrl(null); | |
| setPreviewError(null); | |
| setPreviewFilename(''); | |
| setPreviewLoading(false); | |
| }; | |
| const getDisplaySourceName = (source) => { | |
| if (!source) return ''; | |
| const fullName = source.split('/').pop() || source; | |
| const lastDot = fullName.lastIndexOf('.'); | |
| if (lastDot > 0) { | |
| return fullName.substring(0, lastDot); | |
| } | |
| return fullName; | |
| }; | |
| const handleSourceClick = async (source) => { | |
| if (!source) return; | |
| const filename = source.split('/').pop() || source; | |
| const extension = filename.split('.').pop()?.toLowerCase(); | |
| console.log('[Preview] Requesting preview for:', filename); | |
| setPreviewFilename(filename); | |
| setPreviewError(null); | |
| setPreviewLoading(true); | |
| if (previewUrlRef.current) { | |
| URL.revokeObjectURL(previewUrlRef.current); | |
| previewUrlRef.current = null; | |
| } | |
| setPreviewUrl(null); | |
| if (extension !== 'pdf') { | |
| const errorMsg = 'المعاينة متاحة فقط لملفات PDF.'; | |
| console.error('[Preview] Error:', errorMsg); | |
| setPreviewError(errorMsg); | |
| setPreviewLoading(false); | |
| return; | |
| } | |
| try { | |
| const url = `${DOCS_URL}/${encodeURIComponent(filename)}?mode=preview`; | |
| console.log('[Preview] Requesting URL:', url); | |
| const response = await axios.get(url, { | |
| responseType: 'blob', | |
| timeout: 30000 // 30 second timeout | |
| }); | |
| console.log('[Preview] Response received, status:', response.status, 'size:', response.data.size); | |
| if (!response.data || response.data.size === 0) { | |
| throw new Error('Received empty file'); | |
| } | |
| const blob = new Blob([response.data], { type: 'application/pdf' }); | |
| const objectUrl = URL.createObjectURL(blob); | |
| previewUrlRef.current = objectUrl; | |
| setPreviewUrl(objectUrl); | |
| console.log('[Preview] Successfully created object URL'); | |
| } catch (error) { | |
| console.error('[Preview] Error details:', { | |
| message: error.message, | |
| response: error.response?.data, | |
| status: error.response?.status, | |
| statusText: error.response?.statusText, | |
| url: error.config?.url | |
| }); | |
| let errorMsg = 'تعذر تحميل المعاينة.'; | |
| if (error.response?.data?.detail) { | |
| errorMsg = `خطأ: ${error.response.data.detail}`; | |
| } else if (error.response?.status === 404) { | |
| errorMsg = 'الملف غير موجود.'; | |
| } else if (error.response?.status === 403) { | |
| errorMsg = 'غير مسموح بالوصول إلى هذا الملف.'; | |
| } else if (error.message) { | |
| errorMsg = `خطأ: ${error.message}`; | |
| } | |
| setPreviewError(errorMsg); | |
| } finally { | |
| setPreviewLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div className="App" dir="rtl"> | |
| <div className="chat-container"> | |
| <div className="chat-header"> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |
| <div> | |
| <h1>⚖️ المساعد القانوني الذكي لنظام الأحوال الشخصية</h1> | |
| {/* <p>اطرح أسئلة حول وثائقك القانونية</p> */} | |
| </div> | |
| {messages.length > 0 && ( | |
| <button | |
| onClick={handleClearHistory} | |
| className="clear-history-button" | |
| title="مسح سجل المحادثة" | |
| style={{ | |
| background: 'rgba(255, 255, 255, 0.2)', | |
| border: '1px solid rgba(255, 255, 255, 0.3)', | |
| cursor: 'pointer', | |
| fontSize: '14px', | |
| padding: '8px 16px', | |
| borderRadius: '20px', | |
| color: 'white', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '6px', | |
| transition: 'background-color 0.2s', | |
| fontWeight: '500' | |
| }} | |
| onMouseEnter={(e) => e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.3)'} | |
| onMouseLeave={(e) => e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.2)'} | |
| > | |
| <span>🗑️</span> | |
| <span>مسح المحادثة</span> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <div className="messages-container"> | |
| {messages.length === 0 && ( | |
| <div className="welcome-message"> | |
| <h2>مرحباً!</h2> | |
| <p>ابدأ بطرح سؤالا قانونيا...</p> | |
| </div> | |
| )} | |
| {messages.map((msg, idx) => { | |
| const isAssistant = msg.role === 'assistant'; | |
| const contentIsString = typeof msg.content === 'string'; | |
| const renderContent = () => { | |
| if (isAssistant && contentIsString) { | |
| return ( | |
| <ReactMarkdown remarkPlugins={[remarkGfm]}> | |
| {msg.content} | |
| </ReactMarkdown> | |
| ); | |
| } | |
| if (contentIsString) { | |
| return msg.content; | |
| } | |
| try { | |
| return JSON.stringify(msg.content, null, 2); | |
| } catch (err) { | |
| return 'محتوى غير قابل للعرض'; | |
| } | |
| }; | |
| return ( | |
| <div key={msg.id || idx} className={`message ${msg.role}`}> | |
| <div className="message-content"> | |
| <div className="message-header"> | |
| {msg.role === 'user' ? '👤 أنت' : '🤖 المساعد القانوني'} | |
| </div> | |
| <div className={`message-text ${msg.error ? 'error' : ''}`}> | |
| {renderContent()} | |
| </div> | |
| {msg.sources && msg.sources.length > 0 && ( | |
| <div className="sources"> | |
| <strong>المصادر:</strong> | |
| <ul> | |
| {msg.sources.map((source, i) => ( | |
| <li key={i}> | |
| <span className="source-name">{getDisplaySourceName(source)}</span> | |
| <div className="source-actions"> | |
| <button | |
| type="button" | |
| className="source-link" | |
| onClick={() => handleSourceClick(source)} | |
| > | |
| معاينة | |
| </button> | |
| <button | |
| type="button" | |
| className="source-link download" | |
| onClick={() => handleDownload(source)} | |
| > | |
| تحميل | |
| </button> | |
| </div> | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {loading && ( | |
| <div className="message assistant"> | |
| <div className="message-content"> | |
| <div className="message-header">🤖 المساعد القانوني</div> | |
| <div className="message-text"> | |
| <span className="typing-indicator">جاري التفكير...</span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <div ref={messagesEndRef} /> | |
| </div> | |
| <form className="input-container" onSubmit={handleSend}> | |
| <input | |
| type="text" | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| placeholder="اطرح سؤالاً قانونيا ..." | |
| disabled={loading} | |
| className="message-input" | |
| /> | |
| <button | |
| type="submit" | |
| disabled={loading || !input.trim()} | |
| className="send-button" | |
| > | |
| إرسال | |
| </button> | |
| </form> | |
| {(previewUrl || previewError || previewLoading) && ( | |
| <div className="preview-panel"> | |
| <div className="preview-header"> | |
| <h3>معاينة المصدر</h3> | |
| {previewFilename && <span className="preview-filename">{previewFilename}</span>} | |
| <button className="close-preview" onClick={handleClosePreview}> | |
| ✕ | |
| </button> | |
| </div> | |
| <div className="preview-content"> | |
| {previewLoading ? ( | |
| <p>جاري تحميل المعاينة...</p> | |
| ) : previewError ? ( | |
| <p className="error">{previewError}</p> | |
| ) : ( | |
| <iframe | |
| src={previewUrl} | |
| title={`معاينة ${previewFilename}`} | |
| className="pdf-frame" | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default App; | |