Generate a proposal from the 'Proposal Builder' tab.
`;
pdfDownloadBtn.style.display = 'none';
copyBtn.style.display = 'none';
return;
}
let html = `
${escapeHTML(db.content["Title"] || "Untitled Proposal")}
`;
db.sections.forEach(section => {
if (section !== "Title" && db.content[section]?.trim()) {
html += `
${escapeHTML(section)}
`;
// Basic formatting attempt: replace double newlines with paragraph breaks
const paragraphs = escapeHTML(db.content[section]).split(/\n\s*\n/);
html += paragraphs.map(p => `
${p.replace(/\n/g, '
')}
`).join('');
}
});
pdfContent.innerHTML = html;
pdfDownloadBtn.style.display = 'inline-block';
copyBtn.style.display = 'inline-block';
}
// --- Utility Functions ---
function showMessageModal(message) {
let modal = document.getElementById('rpg-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'rpg-modal';
modal.className = 'rpg-pdf-hide fixed inset-0 bg-gray-800 bg-opacity-50 flex items-center justify-center p-4';
modal.style.zIndex = '1000';
modal.innerHTML = `
`;
document.body.appendChild(modal);
modal.querySelector('#rpg-modal-close').addEventListener('click', () => modal.style.display = 'none');
}
modal.querySelector('#rpg-modal-message').textContent = message;
modal.style.display = 'flex';
}
async function downloadPDF() {
// Per user request, ensuring this is robust
pdfTarget.classList.add('rpg-pdf-view');
try {
// Use scale: 1 for text-heavy documents to avoid blurriness
const canvas = await html2canvas(pdfTarget, { scale: 1, logging: false, useCORS: true });
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const pageMargin = 15;
const imgProps = pdf.getImageProperties(imgData);
const imgWidth = pdfWidth - (pageMargin * 2);
const imgHeight = (imgProps.height * imgWidth) / imgProps.width;
let heightLeft = imgHeight;
let position = pageMargin; // Initial top margin
pdf.addImage(imgData, 'PNG', pageMargin, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - (pageMargin * 2));
while (heightLeft > 0) {
position = heightLeft - imgHeight + pageMargin; // Adjust position for next page
pdf.addPage();
pdf.addImage(imgData, 'PNG', pageMargin, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - (pageMargin * 2));
}
pdf.save(`${(db.content["Title"] || 'Research_Proposal').replace(/\s+/g, '_')}.pdf`);
} catch (error) {
console.error("Error generating PDF:", error);
showMessageModal("An error occurred while generating the PDF. Please try again.");
} finally {
pdfTarget.classList.remove('rpg-pdf-view');
}
}
function copyToClipboard() {
// Use document.execCommand for iframe compatibility
const el = document.createElement('textarea');
let text = `${db.content["Title"] || "Untitled Proposal"}\n\n`;
db.sections.forEach(section => {
if (section !== "Title" && db.content[section]?.trim()) {
text += `--- ${section} ---\n`;
text += `${db.content[section]}\n\n`;
}
});
el.value = text;
document.body.appendChild(el);
el.select();
try {
document.execCommand('copy');
showMessageModal('Proposal text copied to clipboard!');
} catch (err) {
showMessageModal('Failed to copy text. Please try again.');
}
document.body.removeChild(el);
}
function renderAll() {
renderConfig();
renderBuilderForm(); // Re-render form to reflect section changes
}
function escapeHTML(str) {
if (typeof str !== 'string') return '';
return str.replace(/[&<>"']/g, m => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[m]));
}
// --- 4. EVENT BINDING & INITIALIZATION ---
navTabs.forEach((tab, index) => {
tab.addEventListener('click', () => showTab(index));
});
navPrev.addEventListener('click', () => showTab(currentTab - 1));
navNext.addEventListener('click', () => showTab(currentTab + 1));
addSectionForm.addEventListener('submit', handleAddSection);
generateBtn.addEventListener('click', handleGenerateProposal);
pdfDownloadBtn.addEventListener('click', downloadPDF);
copyBtn.addEventListener('click', copyToClipboard);
// Delegate delete for sections
sectionsList.addEventListener('click', (e) => {
if (e.target.classList.contains('rpg-section-delete')) {
const index = parseInt(e.target.dataset.index, 10);
const sectionName = db.sections[index];
db.sections.splice(index, 1);
delete db.content[sectionName]; // Remove content when section is deleted
renderAll();
}
});
// Initial Load
renderAll();
showTab(1); // Start on the builder tab
});