AldawsariNLP's picture
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;