Initial PoC commit

This commit is contained in:
Fenix
2026-01-28 20:39:59 -08:00
commit 94811ca7c1
18 changed files with 1831 additions and 0 deletions

315
journal.js Normal file
View 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');
}