Initial PoC commit
This commit is contained in:
315
journal.js
Normal file
315
journal.js
Normal file
@@ -0,0 +1,315 @@
|
||||
// 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: "<base64>" } 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');
|
||||
}
|
||||
Reference in New Issue
Block a user