Mercurial
view mrjunejune/src/public/editor.js @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| parents | e5aed6c36672 |
| children |
line wrap: on
line source
let editor = null; let currentNoteId = 'index'; function getAuthToken() { return localStorage.getItem('notes-auth-token'); } function requireAuth() { if (!getAuthToken()) { const returnUrl = encodeURIComponent(window.location.pathname); window.location.href = '/notes/login?return=' + returnUrl; return false; } return true; } function goHome() { window.location.href = '/notes'; } function logout() { localStorage.removeItem('notes-auth-token'); window.location.href = '/notes/login'; } function getNoteIdFromPath() { const path = window.location.pathname; const match = path.match(/^\/notes\/(.+)$/); if (match && match[1] && match[1] !== 'login') { return decodeURIComponent(match[1]); } return 'index'; } function showNewNoteDialog() { document.getElementById('new-note-dialog').classList.add('show'); document.getElementById('new-note-id').focus(); } function hideNewNoteDialog() { document.getElementById('new-note-dialog').classList.remove('show'); document.getElementById('new-note-id').value = ''; } function createNewNote() { let noteId = document.getElementById('new-note-id').value.trim(); if (!noteId) return; // Sanitize note ID noteId = noteId.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'); hideNewNoteDialog(); window.location.href = '/notes/' + encodeURIComponent(noteId); } // Handle Enter key in new note dialog document.getElementById('new-note-id').addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); createNewNote(); } if (e.key === 'Escape') { hideNewNoteDialog(); } }); // Close dialog on backdrop click document.getElementById('new-note-dialog').addEventListener('click', function(e) { if (e.target === this) { hideNewNoteDialog(); } }); async function uploadFile(file) { const token = getAuthToken(); if (!token) { throw new Error('Not authenticated'); } // 1. Create media record const createResponse = await fetch('/api/media/create', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: file.name, content_type: file.type }) }); if (!createResponse.ok) { const error = await createResponse.json().catch(() => ({})); throw new Error(error.error || 'Failed to create media record'); } const data = await createResponse.json(); // 2. Upload file directly to S3 const uploadResponse = await fetch(data.upload_url, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file }); if (!uploadResponse.ok) { throw new Error('Failed to upload file to S3'); } // 3. Mark as uploaded (triggers processing for images) await fetch(`/api/media/${data.media_id}/uploaded`, { method: 'POST', headers: { 'Authorization': 'Bearer ' + token } }); // 4. Poll for images, return immediately for non-images if (file.type.startsWith('image/')) { return await pollForProcessedImage(data.media_id, token); } else { // For non-images, use the public URL from the server return { url: data.public_url }; } } async function pollForProcessedImage(mediaId, token) { const maxAttempts = 60; // 2 minutes max (60 * 2 seconds) for (let i = 0; i < maxAttempts; i++) { await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second interval const statusResponse = await fetch(`/api/media/${mediaId}/status`, { headers: { 'Authorization': 'Bearer ' + token } }); if (!statusResponse.ok) { console.warn('Status check failed, retrying...'); continue; } const statusData = await statusResponse.json(); if (statusData.status === 'finished') { return { url: statusData.processed_url }; } else if (statusData.status === 'error') { throw new Error(statusData.error_message || 'Processing failed'); } // Status is 'uploaded' or 'processing', continue polling } throw new Error('Processing timeout after 2 minutes'); } async function saveContent(content) { const token = getAuthToken(); if (!token) return; const response = await fetch('/api/editor/save', { method: 'POST', headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }, body: JSON.stringify({ doc_id: currentNoteId, content: content }) }); if (!response.ok) { throw new Error('Failed to save'); } } async function loadNote(noteId) { const token = getAuthToken(); if (!token) return; try { const response = await fetch('/api/editor/load/' + encodeURIComponent(noteId), { headers: { 'Authorization': 'Bearer ' + token } }); if (response.ok) { const data = await response.json(); editor.setContent(data.content || ''); } } catch (error) { console.error('Failed to load note:', error); } } // Initialize document.addEventListener('DOMContentLoaded', function() { if (!requireAuth()) return; currentNoteId = getNoteIdFromPath(); document.getElementById('note-id-display').textContent = currentNoteId; // Update page title document.title = currentNoteId + ' | Notes'; editor = RichEditor.init('editor-container', { uploadCallback: uploadFile, saveCallback: saveContent, debounceMs: 1500, placeholder: 'Start writing... (paste images, drag files, or use /upload)\n\nTip: Click "+ New Note" to create linked notes.' }); loadNote(currentNoteId); });