Statement of Purpose (SOP) Writer

Statement of Purpose (SOP) Writer

Craft a compelling SOP by following the steps below.

You haven\'t written anything yet. Go back and fill out the sections.

'; pdfDownloadSection.classList.remove('hidden'); } function handleNavigation(direction) { const newTab = currentTab + direction; if (newTab >= 0 && newTab <= tabsData.length) { if (newTab === tabsData.length) { showFinalReview(); } else { nextBtn.classList.remove('hidden'); showTab(newTab); } } } window.handleRefine = async (button, tabId) => { const textarea = document.getElementById(`textarea-${tabId}`); const aiOutputContainer = document.getElementById(`ai-output-${tabId}`); if (!textarea || !aiOutputContainer) return; const text = textarea.value.trim(); if (text.length < 20) { alert("Please write at least 20 characters before refining."); return; } const buttonText = button.querySelector('.button-text'); const spinner = button.querySelector('.spinner'); button.disabled = true; buttonText.textContent = 'Refining...'; spinner.classList.remove('hidden'); aiOutputContainer.innerHTML = ''; try { const refinedText = await callGeminiAPI(text); aiOutputContainer.innerHTML = `

AI Suggestion:

${refinedText.replace(/\n/g, '
')}

`; // Trigger animation setTimeout(() => { const suggestionBox = aiOutputContainer.querySelector('.ai-suggestion-enter'); if (suggestionBox) { suggestionBox.classList.add('ai-suggestion-enter-active'); } }, 10); } catch (error) { aiOutputContainer.innerHTML = `

Error: ${error.message}

`; } finally { button.disabled = false; buttonText.textContent = 'Refine with AI'; spinner.classList.add('hidden'); } }; async function callGeminiAPI(text) { const apiKey = ""; // API key will be automatically provided in the environment. const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`; const systemPrompt = "You are an expert academic advisor. Refine the following excerpt from a user's Statement of Purpose. Improve clarity, flow, tone, and word choice to make it more professional and compelling. Retain the user's original meaning and voice. Do not add new ideas. Respond only with the refined text."; const payload = { contents: [{ parts: [{ text: text }] }], systemInstruction: { parts: [{ text: systemPrompt }] }, }; const response = await fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!response.ok) { const errorBody = await response.json(); throw new Error(errorBody.error?.message || `API request failed with status ${response.status}`); } const result = await response.json(); const candidate = result.candidates?.[0]; if (candidate && candidate.content?.parts?.[0]?.text) { return candidate.content.parts[0].text; } else { throw new Error("Could not get a valid response from the AI model."); } } // Made function global to prevent reference errors from inline onclick calls window.downloadPDF = function() { const { jsPDF } = window.jspdf; const doc = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' }); const contentSource = document.getElementById('sop-preview-for-pdf'); if (!contentSource) { console.error("PDF content source not found."); return; } // --- PDF Styling Constants --- const pageTitle = "Statement of Purpose"; const leftMargin = 20; const topMargin = 20; const contentWidth = doc.internal.pageSize.getWidth() - leftMargin * 2; const pageHeight = doc.internal.pageSize.getHeight(); const bottomMargin = 20; const lineHeight = 7; const headingFontSize = 14; const bodyFontSize = 11; const font = 'Helvetica'; let cursorY = topMargin; let pageNumber = 1; function addHeader() { doc.setFont(font, 'bold'); doc.setFontSize(16); doc.text(pageTitle, leftMargin, cursorY); doc.setLineWidth(0.5); doc.line(leftMargin, cursorY + 4, leftMargin + contentWidth, cursorY + 4); cursorY += 15; } function addFooter() { const pageCount = doc.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setFont(font, 'normal'); doc.setFontSize(10); const text = `Page ${i} of ${pageCount}`; const textWidth = doc.getStringUnitWidth(text) * doc.getFontSize() / doc.internal.scaleFactor; doc.text(text, (doc.internal.pageSize.getWidth() - textWidth) / 2, pageHeight - 10); } } function checkPageBreak() { if (cursorY + lineHeight > pageHeight - bottomMargin) { doc.addPage(); pageNumber++; cursorY = topMargin; } } addHeader(); const sections = contentSource.children; for (const section of sections) { if (section.tagName.toLowerCase() === 'h3') { checkPageBreak(); cursorY += 5; // Extra space before a heading checkPageBreak(); doc.setFont(font, 'bold'); doc.setFontSize(headingFontSize); const headingText = section.innerText; const splitHeading = doc.splitTextToSize(headingText, contentWidth); doc.text(splitHeading, leftMargin, cursorY); cursorY += (splitHeading.length * lineHeight); cursorY += 2; // Space after heading } if (section.tagName.toLowerCase() === 'p') { checkPageBreak(); doc.setFont(font, 'normal'); doc.setFontSize(bodyFontSize); // jsPDF needs plain text. innerText is better than innerHTML here. const bodyText = section.innerText; const splitBody = doc.splitTextToSize(bodyText, contentWidth); splitBody.forEach(line => { checkPageBreak(); doc.text(line, leftMargin, cursorY); cursorY += lineHeight; }); cursorY += lineHeight; // Add a paragraph break } } addFooter(); doc.save('Statement_of_Purpose.pdf'); } // Attach Event Listeners if (prevBtn && nextBtn && downloadPdfBtn) { prevBtn.addEventListener('click', () => handleNavigation(-1)); nextBtn.addEventListener('click', () => handleNavigation(1)); downloadPdfBtn.addEventListener('click', downloadPDF); } else { console.error("One or more navigation buttons not found."); } // Initial setup initializeUI(); });
Scroll to Top