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);
});