Spaces:
Sleeping
Sleeping
{% extends "base.html" %} | |
{% block title %}ResearchMate - Projects{% endblock %} | |
{% block content %} | |
<div class="row"> | |
<div class="col-12"> | |
<!-- Projects Header --> | |
<div class="d-flex justify-content-between align-items-center mb-4"> | |
<h1 class="h2 text-primary-custom"> | |
<i class="fas fa-project-diagram me-2 text-success"></i>Research Projects | |
</h1> | |
<button class="btn btn-success shadow-sm" data-bs-toggle="modal" data-bs-target="#createProjectModal"> | |
<i class="fas fa-plus me-2"></i>Create New Project | |
</button> | |
</div> | |
<!-- Projects List --> | |
<div id="projects-container"> | |
<div class="text-center py-5"> | |
<div class="spinner-border text-primary" role="status"> | |
<span class="visually-hidden">Loading projects...</span> | |
</div> | |
<p class="mt-3 text-muted">Loading projects...</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Create Project Modal --> | |
<div class="modal fade" id="createProjectModal" tabindex="-1"> | |
<div class="modal-dialog modal-lg"> | |
<div class="modal-content shadow-lg" style="color: #222;"> | |
<div class="modal-header" style="background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); color: #222;"> | |
<h5 class="modal-title"> | |
<i class="fas fa-plus me-2"></i>Create New Project | |
</h5> | |
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<form id="create-project-form"> | |
<div class="mb-3"> | |
<label for="project-name" class="form-label fw-bold">Project Name</label> | |
<input type="text" class="form-control" id="project-name" name="name" | |
placeholder="e.g., Transformer Models in NLP" required> | |
<div class="form-text"> | |
<i class="fas fa-info-circle me-1 text-info"></i> | |
Choose a descriptive name for your research project | |
</div> | |
</div> | |
<div class="mb-3"> | |
<label for="research-question" class="form-label fw-bold">Research Question</label> | |
<textarea class="form-control" id="research-question" name="research_question" rows="3" | |
placeholder="e.g., How do transformer models improve performance in natural language processing tasks?" | |
required></textarea> | |
</div> | |
<div class="mb-3"> | |
<label for="keywords" class="form-label">Keywords (comma-separated)</label> | |
<input type="text" class="form-control" id="keywords" name="keywords" | |
placeholder="e.g., transformer, attention, NLP, neural networks" required> | |
</div> | |
</form> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
<button type="submit" form="create-project-form" class="btn btn-primary"> | |
<i class="fas fa-plus me-2"></i>Create Project | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Project Details Modal --> | |
<div class="modal fade" id="projectDetailsModal" tabindex="-1"> | |
<div class="modal-dialog modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title"> | |
<i class="fas fa-project-diagram me-2"></i>Project Details | |
</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<div id="project-details-content"> | |
<!-- Project details will be loaded here --> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
{% block extra_js %} | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
const createProjectForm = document.getElementById('create-project-form'); | |
const projectsContainer = document.getElementById('projects-container'); | |
// Load projects on page load | |
loadProjects(); | |
// Create project form handler | |
createProjectForm.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
const name = document.getElementById('project-name').value; | |
const researchQuestion = document.getElementById('research-question').value; | |
const keywords = document.getElementById('keywords').value.split(',').map(k => k.trim()); | |
createProject(name, researchQuestion, keywords); | |
}); | |
function loadProjects() { | |
apiRequest('/api/projects', { credentials: 'include' }) | |
.then(data => { | |
console.log('Projects API response:', data); // Debug log | |
if (data.success) { | |
displayProjects(data.projects); | |
} else { | |
displayError(data.error || 'Failed to load projects'); | |
} | |
}) | |
.catch(error => { | |
console.error('Projects loading error:', error); // Debug log | |
displayError('Network error: ' + error.message); | |
}); | |
} | |
function createProject(name, researchQuestion, keywords) { | |
const submitBtn = document.querySelector('button[form="create-project-form"]') || | |
document.querySelector('#create-project-form button[type="submit"]') || | |
document.querySelector('.modal-footer button[type="submit"]'); | |
const originalText = submitBtn ? submitBtn.innerHTML : ''; | |
if (submitBtn) { | |
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Creating...'; | |
submitBtn.disabled = true; | |
} | |
apiRequest('/api/projects', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
credentials: 'include', | |
body: JSON.stringify({ | |
name: name, | |
research_question: researchQuestion, | |
keywords: keywords | |
}) | |
}) | |
.then(data => { | |
if (data.success) { | |
// Close modal and reset form | |
const modal = bootstrap.Modal.getInstance(document.getElementById('createProjectModal')); | |
modal.hide(); | |
createProjectForm.reset(); | |
// Reload projects | |
loadProjects(); | |
// Show success message | |
showAlert('success', 'Project created successfully!'); | |
} else { | |
showAlert('danger', data.error || 'Failed to create project'); | |
} | |
}) | |
.catch(error => { | |
showAlert('danger', 'Network error: ' + error.message); | |
}) | |
.finally(() => { | |
if (submitBtn) { | |
submitBtn.innerHTML = originalText; | |
submitBtn.disabled = false; | |
} | |
}); | |
} | |
function displayProjects(projects) { | |
console.log('Displaying projects:', projects); // Debug log | |
if (!projects || projects.length === 0) { | |
projectsContainer.innerHTML = ` | |
<div class="text-center py-5"> | |
<i class="fas fa-project-diagram fa-3x text-muted mb-3"></i> | |
<h4 class="text-muted">No projects yet</h4> | |
<p class="text-muted">Create your first research project to get started!</p> | |
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createProjectModal"> | |
<i class="fas fa-plus me-2"></i>Create Project | |
</button> | |
</div> | |
`; | |
return; | |
} | |
const html = ` | |
<div class="row"> | |
${projects.map(project => ` | |
<div class="col-md-6 col-lg-4 mb-4"> | |
<div class="card h-100 shadow-sm"> | |
<div class="card-body"> | |
<h5 class="card-title">${escapeHtml(project.name || 'Untitled Project')}</h5> | |
<p class="card-text"> | |
<small class="text-muted">${escapeHtml(project.research_question || 'No research question')}</small> | |
</p> | |
<div class="mb-2"> | |
<span class="badge bg-${project.status === 'active' ? 'success' : 'secondary'} me-2"> | |
${project.status || 'unknown'} | |
</span> | |
<span class="badge bg-info">${(project.papers || []).length} papers</span> | |
</div> | |
<div class="d-flex justify-content-between align-items-center"> | |
<small class="text-muted"> | |
Created: ${new Date(project.created_at).toLocaleDateString()} | |
</small> | |
<div class="btn-group"> | |
<button class="btn btn-sm btn-outline-primary" onclick="viewProject('${project.id}')"> | |
<i class="fas fa-eye me-1"></i>View | |
</button> | |
<button class="btn btn-sm btn-outline-success" onclick="searchLiterature('${project.id}')"> | |
<i class="fas fa-search me-1"></i>Search | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
`).join('')} | |
</div> | |
`; | |
projectsContainer.innerHTML = html; | |
} | |
function displayError(message) { | |
projectsContainer.innerHTML = ` | |
<div class="alert alert-danger" role="alert"> | |
<i class="fas fa-exclamation-triangle me-2"></i> | |
<strong>Error:</strong> ${message} | |
</div> | |
`; | |
} | |
function showAlert(type, message, duration = 10000) { | |
const alert = document.createElement('div'); | |
alert.className = `alert alert-${type} alert-dismissible fade show`; | |
alert.innerHTML = ` | |
<i class="fas fa-${type === 'success' ? 'check' : 'exclamation-triangle'} me-2"></i> | |
${message} | |
<button type="button" class="btn-close" data-bs-dismiss="alert"></button> | |
`; | |
document.querySelector('.container').insertBefore(alert, document.querySelector('.container').firstChild); | |
// Store reference to current alert | |
window.currentActiveAlert = alert; | |
// Only auto-dismiss if duration > 0 | |
if (duration > 0) { | |
setTimeout(() => { | |
if (alert.parentNode) { | |
alert.remove(); | |
if (window.currentActiveAlert === alert) { | |
window.currentActiveAlert = null; | |
} | |
} | |
}, duration); | |
} | |
} | |
// Global functions for project actions | |
window.viewProject = function(projectId) { | |
apiRequest(`/api/projects/${projectId}`, { credentials: 'include' }) | |
.then(data => { | |
if (data.success) { | |
displayProjectDetails(data.project); | |
} else { | |
showAlert('danger', data.error || 'Failed to load project'); | |
} | |
}) | |
.catch(error => { | |
showAlert('danger', 'Network error: ' + error.message); | |
}); | |
}; | |
window.searchLiterature = function(projectId) { | |
// Find the button that was clicked and show loading state | |
const button = event.target.closest('button'); | |
const originalText = button ? button.innerHTML : ''; | |
if (button) { | |
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Searching...'; | |
button.disabled = true; | |
button.classList.add('btn-warning'); | |
button.classList.remove('btn-success', 'btn-outline-success'); | |
} | |
// Show persistent loading message (no auto-dismiss) | |
showAlert('info', 'Literature search in progress... This may take a few minutes.', 0); // 0 = no auto-dismiss | |
apiRequest(`/api/projects/${projectId}/search`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
credentials: 'include' | |
}) | |
.then(data => { | |
// Remove loading alert if present | |
if (window.currentActiveAlert && window.currentActiveAlert.parentNode) { | |
window.currentActiveAlert.remove(); | |
window.currentActiveAlert = null; | |
} | |
if (data.success) { | |
showAlert('success', `Found ${data.papers_found || 0} papers for the project!`); | |
loadProjects(); // Reload to show updated paper count | |
// Display search results if available | |
if (data.papers && data.papers.length > 0) { | |
displaySearchResults(data.papers, projectId); | |
} | |
} else { | |
showAlert('danger', data.error || 'Literature search failed'); | |
} | |
}) | |
.catch(error => { | |
showAlert('danger', 'Network error: ' + error.message); | |
}) | |
.finally(() => { | |
// Restore button state | |
if (button) { | |
button.innerHTML = originalText; | |
button.disabled = false; | |
button.classList.remove('btn-warning'); | |
button.classList.add('btn-success'); | |
} | |
}); | |
}; | |
function displayProjectDetails(project) { | |
const detailsContent = document.getElementById('project-details-content'); | |
detailsContent.innerHTML = ` | |
<div class="mb-3"> | |
<h6 class="text-primary">Project Name:</h6> | |
<p>${project.name}</p> | |
</div> | |
<div class="mb-3"> | |
<h6 class="text-primary">Research Question:</h6> | |
<p>${project.research_question}</p> | |
</div> | |
<div class="mb-3"> | |
<h6 class="text-primary">Keywords:</h6> | |
<div> | |
${project.keywords.map(keyword => `<span class="badge bg-secondary me-1">${keyword}</span>`).join('')} | |
</div> | |
</div> | |
<div class="mb-3"> | |
<h6 class="text-primary">Status:</h6> | |
<span class="badge bg-${project.status === 'active' ? 'success' : 'secondary'}">${project.status}</span> | |
</div> | |
<div class="mb-3"> | |
<h6 class="text-primary">Papers:</h6> | |
<p>${project.papers ? project.papers.length : 0} papers collected</p> | |
</div> | |
<div class="mb-3"> | |
<h6 class="text-primary">Created:</h6> | |
<p>${new Date(project.created_at).toLocaleString()}</p> | |
</div> | |
<div class="d-flex gap-2"> | |
<button class="btn btn-success" onclick="searchLiterature('${project.id}')"> | |
<i class="fas fa-search me-2"></i>Search Literature | |
</button> | |
<button class="btn btn-info" onclick="analyzeProject('${project.id}')"> | |
<i class="fas fa-chart-bar me-2"></i>Analyze | |
</button> | |
<button class="btn btn-warning" onclick="generateReview('${project.id}')"> | |
<i class="fas fa-file-alt me-2"></i>Generate Review | |
</button> | |
</div> | |
`; | |
const modal = new bootstrap.Modal(document.getElementById('projectDetailsModal')); | |
modal.show(); | |
} | |
window.analyzeProject = function(projectId) { | |
// Find the button that was clicked and show loading state | |
const button = event.target.closest('button'); | |
const originalText = button ? button.innerHTML : ''; | |
if (button) { | |
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Analyzing...'; | |
button.disabled = true; | |
button.classList.add('btn-warning'); | |
button.classList.remove('btn-info', 'btn-outline-info'); | |
} | |
showAlert('info', 'Analyzing project data... This may take a few minutes.', 0); | |
apiRequest(`/api/projects/${projectId}/analyze`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
credentials: 'include' | |
}) | |
.then(data => { | |
// Remove loading alert if present | |
if (window.currentActiveAlert && window.currentActiveAlert.parentNode) { | |
window.currentActiveAlert.remove(); | |
window.currentActiveAlert = null; | |
} | |
if (data.success) { | |
showAlert('success', 'Project analysis completed!'); | |
// Display analysis results | |
if (data.analysis) { | |
displayAnalysisResults(data.analysis, projectId); | |
} | |
} else { | |
showAlert('danger', data.error || 'Analysis failed'); | |
} | |
}) | |
.catch(error => { | |
showAlert('danger', 'Network error: ' + error.message); | |
}) | |
.finally(() => { | |
// Restore button state | |
if (button) { | |
button.innerHTML = originalText; | |
button.disabled = false; | |
button.classList.remove('btn-warning'); | |
button.classList.add('btn-info'); | |
} | |
}); | |
}; | |
window.generateReview = function(projectId) { | |
// Find the button that was clicked and show loading state | |
const button = event.target.closest('button'); | |
const originalText = button ? button.innerHTML : ''; | |
if (button) { | |
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Generating...'; | |
button.disabled = true; | |
button.classList.add('btn-info'); | |
button.classList.remove('btn-warning', 'btn-outline-warning'); | |
} | |
showAlert('info', 'Generating literature review... This may take several minutes.', 0); // 0 = no auto-dismiss | |
apiRequest(`/api/projects/${projectId}/review`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
credentials: 'include' | |
}) | |
.then(data => { | |
console.log('Review response:', data); // Debug log | |
// Remove loading alert if present | |
if (window.currentActiveAlert && window.currentActiveAlert.parentNode) { | |
window.currentActiveAlert.remove(); | |
window.currentActiveAlert = null; | |
} | |
if (data.success) { | |
showAlert('success', 'Literature review generated!'); | |
// Display the generated review | |
if (data.review) { | |
console.log('Review data:', data.review); // Debug log | |
displayLiteratureReview(data.review, projectId); | |
} else { | |
showAlert('warning', 'Review generated but no content received'); | |
} | |
} else { | |
showAlert('danger', data.error || 'Review generation failed'); | |
} | |
}) | |
.catch(error => { | |
showAlert('danger', 'Network error: ' + error.message); | |
}) | |
.finally(() => { | |
// Restore button state | |
if (button) { | |
button.innerHTML = originalText; | |
button.disabled = false; | |
button.classList.remove('btn-info'); | |
button.classList.add('btn-warning'); | |
} | |
}); | |
}; | |
// Functions to display results | |
function displaySearchResults(papers, projectId) { | |
// Defensive: avoid nested template literals and ensure all dynamic content is escaped | |
const papersHtml = papers.map(function(paper) { | |
const title = escapeHtml(paper.title || 'Untitled'); | |
const authors = escapeHtml(paper.authors || 'Unknown authors'); | |
const year = escapeHtml(paper.year || 'Unknown year'); | |
const abstract = escapeHtml((paper.abstract || 'No abstract available').substring(0, 150)); | |
let urlLink = ''; | |
if (paper.url) { | |
urlLink = '<a href="' + escapeHtml(paper.url) + '" target="_blank" class="btn btn-sm btn-outline-primary">' + | |
'<i class="fas fa-external-link-alt me-1"></i>View Paper' + | |
'</a>'; | |
} | |
return [ | |
'<div class="col-md-6 mb-3">', | |
' <div class="card h-100">', | |
' <div class="card-body">', | |
' <h6 class="card-title">' + title + '</h6>', | |
' <p class="text-muted small mb-2">', | |
' <i class="fas fa-users me-1"></i>' + authors, | |
' </p>', | |
' <p class="text-muted small mb-2">', | |
' <i class="fas fa-calendar me-1"></i>' + year, | |
' </p>', | |
' <p class="card-text small">' + abstract + '...</p>', | |
' ' + urlLink, | |
' </div>', | |
' </div>', | |
'</div>' | |
].join(''); | |
}).join(''); | |
const modalContent = [ | |
'<div class="mb-3">', | |
' <h6 class="text-primary">Found ' + papers.length + ' papers:</h6>', | |
'</div>', | |
'<div class="row">', | |
papersHtml, | |
'</div>' | |
].join(''); | |
const modal = createResultModal('Literature Search Results', modalContent); | |
modal.show(); | |
} | |
function displayAnalysisResults(analysis, projectId) { | |
// Store analysis data globally for download | |
window.currentAnalysis = analysis; | |
const modal = createResultModal('Project Analysis Results', ` | |
<div class="row"> | |
<div class="col-md-6 mb-3"> | |
<div class="card border-primary"> | |
<div class="card-header bg-primary text-white"> | |
<h6 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Overview</h6> | |
</div> | |
<div class="card-body"> | |
<p><strong>Total Papers:</strong> ${analysis.total_papers || 'N/A'}</p> | |
<p><strong>Key Topics:</strong> ${analysis.key_topics ? analysis.key_topics.join(', ') : 'N/A'}</p> | |
<p><strong>Research Trends:</strong> ${analysis.trends || 'N/A'}</p> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-6 mb-3"> | |
<div class="card border-success"> | |
<div class="card-header bg-success text-white"> | |
<h6 class="mb-0"><i class="fas fa-lightbulb me-2"></i>Key Insights</h6> | |
</div> | |
<div class="card-body"> | |
<div style="max-height: 200px; overflow-y: auto;"> | |
${analysis.insights ? marked.parse(analysis.insights) : 'No insights available'} | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-12 mb-3"> | |
<div class="card border-info"> | |
<div class="card-header bg-info text-white"> | |
<h6 class="mb-0"><i class="fas fa-brain me-2"></i>Summary</h6> | |
</div> | |
<div class="card-body"> | |
<div style="max-height: 300px; overflow-y: auto;"> | |
${analysis.summary ? marked.parse(analysis.summary) : 'No summary available'} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="d-flex justify-content-end"> | |
<button class="btn btn-primary me-2" onclick="downloadAnalysis('${projectId}')"> | |
<i class="fas fa-download me-1"></i>Download Analysis | |
</button> | |
</div> | |
`); | |
modal.show(); | |
} | |
function displayLiteratureReview(review, projectId) { | |
// Store review data globally for download | |
window.currentReview = review; | |
const modal = createResultModal('Literature Review', ` | |
<div class="card"> | |
<div class="card-body"> | |
<div style="max-height: 600px; overflow-y: auto;"> | |
${review.content ? marked.parse(review.content) : 'No review content available'} | |
</div> | |
</div> | |
</div> | |
<div class="d-flex justify-content-end mt-3"> | |
<button class="btn btn-success me-2" onclick="downloadReview('${projectId}')"> | |
<i class="fas fa-download me-1"></i>Download Review | |
</button> | |
<button class="btn btn-info" onclick="copyReview('${projectId}')"> | |
<i class="fas fa-copy me-1"></i>Copy to Clipboard | |
</button> | |
</div> | |
`); | |
modal.show(); | |
} | |
function createResultModal(title, content) { | |
// Remove existing result modal if it exists | |
const existingModal = document.getElementById('resultModal'); | |
if (existingModal) { | |
existingModal.remove(); | |
} | |
// Create new modal | |
const modalHtml = ` | |
<div class="modal fade" id="resultModal" tabindex="-1"> | |
<div class="modal-dialog modal-xl"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">${title}</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
${content} | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
`; | |
document.body.insertAdjacentHTML('beforeend', modalHtml); | |
return new bootstrap.Modal(document.getElementById('resultModal')); | |
} | |
// Global storage for results | |
window.currentAnalysis = null; | |
window.currentReview = null; | |
// Utility function to escape HTML | |
function escapeHtml(text) { | |
const div = document.createElement('div'); | |
div.textContent = text; | |
return div.innerHTML; | |
} | |
// Analysis-specific functions (global scope) | |
window.downloadAnalysis = function(projectId) { | |
if (window.currentAnalysis) { | |
const analysisText = JSON.stringify(window.currentAnalysis, null, 2); | |
window.downloadText(analysisText, `analysis_${projectId}.json`); | |
} else { | |
showAlert('danger', 'No analysis data available to download', 3000); | |
} | |
}; | |
// Review-specific functions (global scope) | |
window.downloadReview = function(projectId) { | |
if (window.currentReview && window.currentReview.content) { | |
window.downloadText(window.currentReview.content, `review_${projectId}.md`); | |
} else { | |
showAlert('danger', 'No review content available to download', 3000); | |
} | |
}; | |
window.copyReview = function(projectId) { | |
if (window.currentReview && window.currentReview.content) { | |
window.copyToClipboard(window.currentReview.content); | |
} else { | |
showAlert('danger', 'No review content available to copy', 3000); | |
} | |
}; | |
}); | |
</script> | |
{% endblock %} | |