// Journal UI (Figma skin) + Prometheus HTR extraction wiring // - Camera button => opens system image chooser, posts to /api/extract // - Clear => clears text + resets page counter // - Save => saves to localStorage // - Share => native share or clipboard // // Assumes your server accepts JSON: { image: "" } at POST /api/extract // (matches your original working prototype). // ------------------------- // Config // ------------------------- const API_BASE = window.location.origin; const MAX_IMAGE_DIMENSION = 1600; // ------------------------- // DOM // ------------------------- const noteTextarea = document.getElementById('noteTextarea'); const shareBtn = document.getElementById('shareBtn'); const menuBtn = document.getElementById('menuBtn'); const cameraBtn = document.getElementById('cameraBtn'); const clearBtn = document.getElementById('clearBtn'); const saveBtn = document.getElementById('saveBtn'); const toastContainer = document.getElementById('toastContainer'); const fileInput = document.getElementById('fileInput'); // ------------------------- // State // ------------------------- let pageCount = 0; let isProcessing = false; // ------------------------- // Toasts // ------------------------- function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; toastContainer.appendChild(toast); // Trigger animation setTimeout(() => toast.classList.add('show'), 10); // Remove toast after 3 seconds setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, 3000); } // ------------------------- // Helpers // ------------------------- function setBusy(busy) { isProcessing = busy; cameraBtn.disabled = busy; clearBtn.disabled = busy; saveBtn.disabled = busy; shareBtn.disabled = busy; cameraBtn.style.opacity = busy ? '0.6' : ''; } function appendExtractedText(newText) { const text = (newText || '').trim(); if (!text) { showToast('No text returned', 'info'); return; } pageCount += 1; const current = noteTextarea.value.trim(); if (current) { noteTextarea.value = current + `\n\n--- Page ${pageCount} ---\n\n` + text; } else { noteTextarea.value = text; } // Keep cursor at end noteTextarea.scrollTop = noteTextarea.scrollHeight; } // Resize image -> base64 (no data: prefix) function resizeImageToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); let { width, height } = img; if (width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) { if (width > height) { height = Math.round((height * MAX_IMAGE_DIMENSION) / width); width = MAX_IMAGE_DIMENSION; } else { width = Math.round((width * MAX_IMAGE_DIMENSION) / height); height = MAX_IMAGE_DIMENSION; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); const dataUrl = canvas.toDataURL('image/jpeg', 0.85); const base64 = dataUrl.split(',')[1]; resolve(base64); }; img.onerror = () => reject(new Error('Failed to load image')); img.src = e.target.result; }; reader.onerror = () => reject(new Error('Failed to read file')); reader.readAsDataURL(file); }); } // ------------------------- // Extraction pipeline // ------------------------- async function extractFromImageFile(file) { if (isProcessing) return; setBusy(true); showToast('Extracting…', 'info'); try { const resizedBase64 = await resizeImageToBase64(file); const response = await fetch(`${API_BASE}/api/extract`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ image: resizedBase64 }) }); if (!response.ok) { let msg = `HTTP ${response.status}`; try { const err = await response.json(); msg = err?.detail?.message || err?.detail || err?.message || msg; } catch (_) { // ignore json parse try { msg = await response.text(); } catch (_) {} } throw new Error(msg || 'Extraction failed'); } const result = await response.json(); appendExtractedText(result.text); showToast(`Page extracted (${pageCount})`, 'success'); } catch (err) { console.error(err); showToast(err?.message ? `Extract failed: ${err.message}` : 'Extract failed', 'error'); } finally { setBusy(false); } } // ------------------------- // Button wiring // ------------------------- // Share shareBtn.addEventListener('click', async () => { const text = noteTextarea.value.trim(); if (!text) { showToast('Nothing to share', 'error'); return; } if (navigator.share) { try { await navigator.share({ text }); // no toast needed; iOS share sheet is the feedback } catch (err) { // user cancelled is fine; only show for real failures if (err?.name !== 'AbortError') showToast('Failed to share', 'error'); } return; } try { await navigator.clipboard.writeText(text); showToast('Copied to clipboard', 'success'); } catch (err) { showToast('Clipboard blocked (long-press to copy)', 'error'); } }); // Menu (Hamburger Menu - potential future addition skin-picker?) menuBtn.addEventListener('click', () => { window.location.href = 'menu.html'; }); // Extract from image cameraBtn.addEventListener('click', () => { if (isProcessing) return; // Allow selecting same photo twice fileInput.value = ''; fileInput.click(); }); fileInput.addEventListener('change', async (e) => { const file = e.target.files?.[0]; if (!file) return; // reset input so selecting the same file again works e.target.value = ''; await extractFromImageFile(file); }); // Clear clearBtn.addEventListener('click', () => { const text = noteTextarea.value.trim(); if (!text) { // Nothing to clear; no need to warn. return; } const ok = window.confirm( 'Clear all text?\n\nThis cannot be undone. Make sure you have saved or shared anything important first.' ); if (!ok) { return; } noteTextarea.value = ''; pageCount = 0; localStorage.removeItem('savedNote'); localStorage.removeItem('savedNotePageCount'); showToast('Cleared', 'success'); }); // Save - localStorage saveBtn.addEventListener('click', () => { const text = noteTextarea.value.trim(); if (!text) { showToast('Nothing to save', 'error'); return; } localStorage.setItem('savedNote', text); localStorage.setItem('savedNotePageCount', String(pageCount)); showToast('Saved', 'success'); }); // Load saved note on page load window.addEventListener('DOMContentLoaded', () => { const savedNote = localStorage.getItem('savedNote'); const savedCount = parseInt(localStorage.getItem('savedNotePageCount') || '0', 10); if (savedNote) { noteTextarea.value = savedNote; pageCount = Number.isFinite(savedCount) ? savedCount : 0; showToast('Loaded saved note', 'info'); } }); // Navigation back to main app function goBackToApp() { window.location.href = "index.html"; // change if your main file has a different name/path } // Credits & Rewards actions function onWatchAd() { // TODO: integrate with your AdMob rewarded flow // Example: AdMob.showRewardedAd().then(refreshCredits); console.log("Watch-ad clicked – call AdMob flow here."); } function onBuyPack(packId) { // TODO: integrate with your in-app purchase / RevenueCat / store kit flow console.log("Buy-pack clicked:", packId); // Example: Purchases.purchasePackage(packId); } // Feedback handling function submitFeedback() { const messageEl = document.getElementById('feedbackMessage'); const emailEl = document.getElementById('feedbackEmail'); if (!messageEl) return; const message = messageEl.value.trim(); const email = emailEl ? emailEl.value.trim() : ''; if (!message) { showToast('Please add a short message before sending.', 'error'); return; } // For MVP: open mail client with a prefilled email. const to = 'support@prometheus.cafe'; // change to your actual inbox const subject = encodeURIComponent('Pen2Post feedback'); const bodyLines = [ message, '', email ? `Reply email (optional): ${email}` : '', ]; const body = encodeURIComponent(bodyLines.join('\n')); window.location.href = `mailto:${to}?subject=${subject}&body=${body}`; showToast('Opening your mail app to send feedback…', 'info'); }