Spaces:
Sleeping
Sleeping
| import { useState } from 'react' | |
| import './App.css' | |
| const API_URL = 'https://k2mar-docuresume-backend.hf.space' | |
| function App() { | |
| const [page, setPage] = useState('login') | |
| const [user, setUser] = useState(null) | |
| // Login/Register form states | |
| const [email, setEmail] = useState('') | |
| const [username, setUsername] = useState('') | |
| const [password, setPassword] = useState('') | |
| const [message, setMessage] = useState('') | |
| // Upload states | |
| const [file, setFile] = useState(null) | |
| const [documents, setDocuments] = useState([]) | |
| // Search states | |
| const [searchQuery, setSearchQuery] = useState('') | |
| const [searchResults, setSearchResults] = useState([]) | |
| const [searching, setSearching] = useState(false) | |
| // Summary states | |
| const [summarizing, setSummarizing] = useState(false) | |
| const [summarizingAll, setSummarizingAll] = useState(false) | |
| const [summaries, setSummaries] = useState({}) | |
| const handleRegister = async (e) => { | |
| e.preventDefault() | |
| setMessage('') | |
| try { | |
| const response = await fetch(`${API_URL}/users/register`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email, username, password }) | |
| }) | |
| const data = await response.json() | |
| if (response.ok) { | |
| setMessage('✓ Inscription réussie! Connectez-vous.') | |
| setPage('login') | |
| } else { | |
| setMessage('✗ ' + data.detail) | |
| } | |
| } catch (error) { | |
| setMessage('✗ Erreur: ' + error.message) | |
| } | |
| } | |
| const handleLogin = async (e) => { | |
| e.preventDefault() | |
| setMessage('') | |
| try { | |
| const response = await fetch(`${API_URL}/users/login`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ email, password }) | |
| }) | |
| const data = await response.json() | |
| if (response.ok) { | |
| setUser(data) | |
| setMessage('✓ Connexion réussie!') | |
| setPage('dashboard') | |
| loadDocuments(data.user_id) | |
| } else { | |
| setMessage('✗ ' + data.detail) | |
| } | |
| } catch (error) { | |
| setMessage('✗ Erreur: ' + error.message) | |
| } | |
| } | |
| const loadDocuments = async (userId) => { | |
| try { | |
| const response = await fetch(`${API_URL}/documents/user/${userId}`) | |
| const data = await response.json() | |
| setDocuments(data.documents || []) | |
| } catch (error) { | |
| console.error('Erreur chargement documents:', error) | |
| } | |
| } | |
| const handleUpload = async (e) => { | |
| e.preventDefault() | |
| if (!file) { | |
| setMessage('✗ Sélectionnez un fichier') | |
| return | |
| } | |
| setMessage('⏳ Upload en cours...') | |
| const formData = new FormData() | |
| formData.append('file', file) | |
| formData.append('user_id', user.user_id) | |
| formData.append('tags', JSON.stringify([])) | |
| try { | |
| const response = await fetch(`${API_URL}/documents/upload`, { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| const data = await response.json() | |
| if (response.ok) { | |
| setMessage('✓ Document uploadé!') | |
| setFile(null) | |
| loadDocuments(user.user_id) | |
| } else { | |
| setMessage('✗ ' + data.detail) | |
| } | |
| } catch (error) { | |
| setMessage('✗ Erreur: ' + error.message) | |
| } | |
| } | |
| const logout = () => { | |
| setUser(null) | |
| setPage('login') | |
| setEmail('') | |
| setPassword('') | |
| setUsername('') | |
| setMessage('') | |
| setSearchQuery('') | |
| setSearchResults([]) | |
| setSummaries({}) | |
| } | |
| const handleSummarizeDocument = async (documentId, filename) => { | |
| setSummarizing(documentId) | |
| setMessage(`📝 Résumé de "${filename}" en cours...`) | |
| try { | |
| // 1. Récupérer le contenu du document | |
| const contentResponse = await fetch(`${API_URL}/documents/${documentId}/content`) | |
| const contentData = await contentResponse.json() | |
| if (!contentResponse.ok) { | |
| throw new Error(contentData.detail || 'Erreur récupération contenu') | |
| } | |
| // 2. Générer le résumé avec le modèle T5 sur le backend | |
| const generateResponse = await fetch(`${API_URL}/generate/summary`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| text: contentData.content, | |
| max_length: 150, | |
| min_length: 30 | |
| }) | |
| }) | |
| const generateData = await generateResponse.json() | |
| if (!generateResponse.ok) { | |
| throw new Error(generateData.detail || 'Erreur génération résumé') | |
| } | |
| const summary = generateData.summary | |
| const generationTime = generateData.generation_time | |
| // 3. Envoyer le résumé au serveur pour sauvegarde | |
| const summaryResponse = await fetch(`${API_URL}/documents/${documentId}/summaries`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| content: summary, | |
| summary_type: 'abstractive', | |
| summary_level: 'medium', | |
| model_name: 't5-small-finetuned' | |
| }) | |
| }) | |
| const summaryData = await summaryResponse.json() | |
| if (summaryResponse.ok) { | |
| setMessage(`✓ Résumé de "${filename}" créé en ${generationTime}s!`) | |
| setSummaries(prev => ({ ...prev, [documentId]: summary })) | |
| } else { | |
| throw new Error(summaryData.detail || 'Erreur création résumé') | |
| } | |
| } catch (error) { | |
| setMessage(`✗ Erreur résumé: ${error.message}`) | |
| } finally { | |
| setSummarizing(false) | |
| } | |
| } | |
| const handleSummarizeAll = async () => { | |
| if (documents.length === 0) { | |
| setMessage('✗ Aucun document à résumer') | |
| return | |
| } | |
| setSummarizingAll(true) | |
| setMessage(`📝 Résumé de ${documents.length} documents en cours...`) | |
| try { | |
| // 1. Récupérer tous les contenus | |
| const response = await fetch(`${API_URL}/documents/user/${user.user_id}/contents?include_content=true`) | |
| const data = await response.json() | |
| if (!response.ok) { | |
| throw new Error(data.detail || 'Erreur récupération contenus') | |
| } | |
| // 2. Générer les résumés avec le modèle T5 pour chaque document | |
| const summariesBatch = [] | |
| for (const doc of data.documents) { | |
| try { | |
| const generateResponse = await fetch(`${API_URL}/generate/summary`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| text: doc.content, | |
| max_length: 150, | |
| min_length: 30 | |
| }) | |
| }) | |
| const generateData = await generateResponse.json() | |
| if (generateResponse.ok) { | |
| summariesBatch.push({ | |
| document_id: doc.document_id, | |
| content: generateData.summary, | |
| summary_type: 'abstractive', | |
| summary_level: 'medium', | |
| model_name: 't5-small-finetuned' | |
| }) | |
| } | |
| } catch (err) { | |
| console.error(`Erreur résumé ${doc.filename}:`, err) | |
| } | |
| } | |
| if (summariesBatch.length === 0) { | |
| throw new Error('Aucun résumé généré') | |
| } | |
| // 3. Envoyer tous les résumés en batch | |
| const batchResponse = await fetch(`${API_URL}/summaries/batch`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(summariesBatch) | |
| }) | |
| const batchData = await batchResponse.json() | |
| if (batchResponse.ok) { | |
| setMessage(`✓ ${batchData.count} résumés créés avec succès!`) | |
| // Mettre à jour les résumés localement | |
| const newSummaries = {} | |
| summariesBatch.forEach(s => { | |
| newSummaries[s.document_id] = s.content | |
| }) | |
| setSummaries(newSummaries) | |
| } else { | |
| throw new Error(batchData.detail || 'Erreur création résumés') | |
| } | |
| } catch (error) { | |
| setMessage(`✗ Erreur résumé batch: ${error.message}`) | |
| } finally { | |
| setSummarizingAll(false) | |
| } | |
| } | |
| const handleSearch = async (e) => { | |
| e.preventDefault() | |
| if (!searchQuery.trim()) { | |
| setMessage('✗ Entrez une requête de recherche') | |
| return | |
| } | |
| setSearching(true) | |
| setMessage('🔍 Recherche en cours...') | |
| try { | |
| const response = await fetch( | |
| `${API_URL}/search/semantic/multi?user_id=${user.user_id}&query=${encodeURIComponent(searchQuery)}&k=5`, | |
| { method: 'POST' } | |
| ) | |
| const data = await response.json() | |
| if (response.ok) { | |
| setSearchResults(data.results || []) | |
| setMessage(`✓ ${data.count} résultats trouvés dans ${data.searched_documents} documents`) | |
| } else { | |
| setMessage('✗ ' + data.detail) | |
| setSearchResults([]) | |
| } | |
| } catch (error) { | |
| setMessage('✗ Erreur: ' + error.message) | |
| setSearchResults([]) | |
| } finally { | |
| setSearching(false) | |
| } | |
| } | |
| if (page === 'register') { | |
| return ( | |
| <div className="container"> | |
| <div className="card"> | |
| <h1>📝 Inscription</h1> | |
| <form onSubmit={handleRegister}> | |
| <input | |
| type="email" | |
| placeholder="Email" | |
| value={email} | |
| onChange={(e) => setEmail(e.target.value)} | |
| required | |
| /> | |
| <input | |
| type="text" | |
| placeholder="Username" | |
| value={username} | |
| onChange={(e) => setUsername(e.target.value)} | |
| required | |
| /> | |
| <input | |
| type="password" | |
| placeholder="Mot de passe" | |
| value={password} | |
| onChange={(e) => setPassword(e.target.value)} | |
| required | |
| /> | |
| <button type="submit">S'inscrire</button> | |
| </form> | |
| {message && <div className="message">{message}</div>} | |
| <p className="link" onClick={() => setPage('login')}> | |
| Déjà un compte? Se connecter | |
| </p> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| if (page === 'login') { | |
| return ( | |
| <div className="container"> | |
| <div className="card"> | |
| <h1>🔐 Connexion</h1> | |
| <form onSubmit={handleLogin}> | |
| <input | |
| type="email" | |
| placeholder="Email" | |
| value={email} | |
| onChange={(e) => setEmail(e.target.value)} | |
| required | |
| /> | |
| <input | |
| type="password" | |
| placeholder="Mot de passe" | |
| value={password} | |
| onChange={(e) => setPassword(e.target.value)} | |
| required | |
| /> | |
| <button type="submit">Se connecter</button> | |
| </form> | |
| {message && <div className="message">{message}</div>} | |
| <p className="link" onClick={() => setPage('register')}> | |
| Pas de compte? S'inscrire | |
| </p> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| if (page === 'dashboard' && user) { | |
| return ( | |
| <div className="container"> | |
| <div className="card large"> | |
| <div className="header"> | |
| <div> | |
| <h1>📄 DocuResume Pro</h1> | |
| <p>Bienvenue {user.username} ({user.email})</p> | |
| </div> | |
| <button onClick={logout} className="btn-secondary">Déconnexion</button> | |
| </div> | |
| <div className="section"> | |
| <h2>📤 Upload Document</h2> | |
| <form onSubmit={handleUpload}> | |
| <input | |
| type="file" | |
| accept=".pdf" | |
| onChange={(e) => setFile(e.target.files[0])} | |
| /> | |
| <button type="submit">Uploader PDF</button> | |
| </form> | |
| </div> | |
| {message && <div className="message">{message}</div>} | |
| <div className="section"> | |
| <h2>� Recherche Sémantique</h2> | |
| <form onSubmit={handleSearch}> | |
| <input | |
| type="text" | |
| placeholder="Posez une question sur vos documents..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| /> | |
| <button type="submit" disabled={searching}> | |
| {searching ? '⏳ Recherche...' : '🔍 Rechercher'} | |
| </button> | |
| </form> | |
| {searchResults.length > 0 && ( | |
| <div className="search-results"> | |
| <h3>Résultats ({searchResults.length})</h3> | |
| {searchResults.map((result, idx) => ( | |
| <div key={idx} className="search-result"> | |
| <div className="result-header"> | |
| <strong>Score: {(result.score * 100).toFixed(1)}%</strong> | |
| <small>Document ID: {result.document_id.slice(0, 8)}...</small> | |
| </div> | |
| <p className="result-text">{result.text}</p> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| <div className="section"> | |
| <div className="header"> | |
| <h2>📚 Mes Documents ({documents.length})</h2> | |
| {documents.length > 0 && ( | |
| <button | |
| onClick={handleSummarizeAll} | |
| disabled={summarizingAll} | |
| className="btn-primary" | |
| > | |
| {summarizingAll ? '⏳ Résumé en cours...' : '📝 Résumer tous les documents'} | |
| </button> | |
| )} | |
| </div> | |
| <div className="documents"> | |
| {documents.length === 0 ? ( | |
| <p className="empty">Aucun document uploadé</p> | |
| ) : ( | |
| documents.map((doc) => ( | |
| <div key={doc.id} className="document"> | |
| <div className="doc-info"> | |
| <strong>{doc.original_filename}</strong> | |
| <small>{doc.processing_status}</small> | |
| </div> | |
| <div className="doc-actions"> | |
| <div className="doc-meta"> | |
| <span>{(doc.file_size / 1024).toFixed(1)} KB</span> | |
| <span>{doc.page_count || '?'} pages</span> | |
| </div> | |
| <button | |
| onClick={() => handleSummarizeDocument(doc.id, doc.original_filename)} | |
| disabled={summarizing === doc.id} | |
| className="btn-summary" | |
| > | |
| {summarizing === doc.id ? '⏳' : '📝 Résumer'} | |
| </button> | |
| <a | |
| href={`${API_URL}/documents/${doc.id}/download`} | |
| download={doc.original_filename} | |
| className="btn-download" | |
| > | |
| ⬇️ Télécharger | |
| </a> | |
| </div> | |
| {summaries[doc.id] && ( | |
| <div className="summary-box"> | |
| <strong>📄 Résumé:</strong> | |
| <p>{summaries[doc.id]}</p> | |
| </div> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| return null | |
| } | |
| export default App | |