// ============================================ // MindAnchor - Bulletproof MVP // Loops 0-2: Scaffold + Storage + Mock Generation // ============================================ // ============================================ // STATE MANAGEMENT // ============================================ const State = { anchors: [], currentAnchor: null, isGenerating: false }; // ============================================ // STORAGE LAYER (Loop 1) // ============================================ const Storage = { DB_NAME: 'MindAnchorDB', STORE_NAME: 'anchors', DB_VERSION: 1, async init() { return new Promise((resolve, reject) => { const request = indexedDB.open(this.DB_NAME, this.DB_VERSION); request.onerror = () => { console.warn('IndexedDB failed, falling back to localStorage'); resolve(); }; request.onsuccess = () => { this.db = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(this.STORE_NAME)) { const store = db.createObjectStore(this.STORE_NAME, { keyPath: 'id' }); store.createIndex('createdAt', 'createdAt', { unique: false }); } }; }); }, async getAll() { // Try IndexedDB first if (this.db) { return new Promise((resolve, reject) => { const transaction = this.db.transaction([this.STORE_NAME], 'readonly'); const store = transaction.objectStore(this.STORE_NAME); const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }).catch(() => this.getFromLocalStorage()); } return this.getFromLocalStorage(); }, getFromLocalStorage() { const data = localStorage.getItem('mindanchor_anchors'); return data ? JSON.parse(data) : []; }, async save(anchor) { const anchors = await this.getAll(); const existingIndex = anchors.findIndex(a => a.id === anchor.id); if (existingIndex >= 0) { anchors[existingIndex] = anchor; } else { anchors.unshift(anchor); } // Try IndexedDB first if (this.db) { const transaction = this.db.transaction([this.STORE_NAME], 'readwrite'); const store = transaction.objectStore(this.STORE_NAME); store.put(anchor); } // Always sync to localStorage as backup localStorage.setItem('mindanchor_anchors', JSON.stringify(anchors)); return anchor; }, async delete(id) { let anchors = await this.getAll(); anchors = anchors.filter(a => a.id !== id); if (this.db) { const transaction = this.db.transaction([this.STORE_NAME], 'readwrite'); const store = transaction.objectStore(this.STORE_NAME); store.delete(id); } localStorage.setItem('mindanchor_anchors', JSON.stringify(anchors)); }, async clear() { if (this.db) { const transaction = this.db.transaction([this.STORE_NAME], 'readwrite'); transaction.objectStore(this.STORE_NAME).clear(); } localStorage.removeItem('mindanchor_anchors'); } }; // ============================================ // MOCK ANCHOR GENERATOR (Loop 2) // ============================================ const MockGenerator = { generate(text) { const words = text.split(' ').filter(w => w.length > 3); const themes = [...new Set(words.filter(w => ['think', 'idea', 'plan', 'goal', 'work', 'life', 'learn', 'create', 'build', 'focus'].some(t => w.toLowerCase().includes(t)) ))]; const threadTemplates = [ `Your thought about ${themes[0] || 'this topic'} reflects a deeper desire for growth.`, `The core insight here is about ${themes[0] || 'progress'} - you're ready to take the next step.`, `This thinking pattern shows you're aligning with your values around ${themes[0] || 'meaningful work'}.`, `There's clarity emerging around ${themes[0] || 'your goals'} - trust this direction.`, `Your perspective on ${themes[0] || 'this matter'} is evolving in a positive way.` ]; const bulletTemplates = [ (t) => `Focus on breaking down your ${t || 'idea'} into actionable pieces.`, (t) => `Consider what resources you need to move forward with ${t || 'this'}.`, (t) => `Reflect on past successes with similar ${t || 'challenges'}.`, (t) => `Identify one small win you can achieve today related to ${t || 'this'}.`, (t) => `Think about who might support your journey with ${t || 'this project'}.`, (t) => `Set a clear timeline for your ${t || 'next steps'}.`, (t) => `What would make this ${t || 'endeavor'} feel more meaningful?`, (t) => `Consider the impact you want to create through ${t || 'this work'}.` ]; const nextStepTemplates = [ (t) => `Write down three specific actions for your ${t || 'project'} by tomorrow.`, (t) => `Schedule 15 minutes to brainstorm more ideas about ${t || 'this topic'}.`, (t) => `Share your ${t || 'thoughts'} with someone you trust for feedback.`, (t) => `Create a simple prototype or outline for your ${t || 'idea'}.`, (t) => `Research one example of successful ${t || 'implementation'}.` ]; const thread = threadTemplates[Math.floor(Math.random() * threadTemplates.length)]; const bullets = this.shuffleArray(bulletTemplates) .slice(0, 3) .map(t => t(themes[0])); const nextStep = nextStepTemplates[Math.floor(Math.random() * nextStepTemplates.length)](themes[0]); return { thread, bullets, nextStep }; }, shuffleArray(array) { const newArray = [...array]; for (let i = newArray.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; } return newArray; } }; // ============================================ // UI CONTROLLER // ============================================ const UI = { elements: {}, init() { // Cache elements this.elements = { inputText: document.getElementById('input-text'), charCount: document.getElementById('char-count'), anchorBtn: document.getElementById('anchor-btn'), clearBtn: document.getElementById('clear-btn'), outputSection: document.getElementById('output-section'), outputContent: document.getElementById('output-content'), emptyState: document.getElementById('empty-state'), copyBtn: document.getElementById('copy-btn'), exportMdBtn: document.getElementById('export-md-btn'), deleteBtn: document.getElementById('delete-btn'), saveBtn: document.getElementById('save-btn'), historySidebar: document.querySelector('custom-history-sidebar'), toastContainer: document.getElementById('toast-container') }; this.bindEvents(); }, bindEvents() { const { inputText, charCount, anchorBtn, clearBtn, copyBtn, exportMdBtn, deleteBtn, saveBtn } = this.elements; // Input handling inputText.addEventListener('input', () => { charCount.textContent = `${inputText.value.length} / 5000`; }); clearBtn.addEventListener('click', () => { inputText.value = ''; charCount.textContent = '0 / 5000'; this.showOutput(null); }); // Anchor button anchorBtn.addEventListener('click', async () => { const text = inputText.value.trim(); if (!text) { this.showToast('Please enter some text first', 'error'); return; } this.setGenerating(true); // Simulate processing delay for UX await new Promise(resolve => setTimeout(resolve, 800)); const output = MockGenerator.generate(text); State.currentAnchor = { id: Date.now().toString(), createdAt: new Date().toISOString(), rawText: text, output, mode: 'mock' }; this.renderOutput(State.currentAnchor); this.setGenerating(false); this.showToast('Anchor generated! (Mock Mode)', 'success'); }); // Output actions copyBtn.addEventListener('click', () => { this.copyToClipboard(this.getMarkdown(State.currentAnchor)); }); exportMdBtn.addEventListener('click', () => { this.downloadMarkdown(State.currentAnchor); }); deleteBtn.addEventListener('click', async () => { if (confirm('Delete this anchor?')) { await Storage.delete(State.currentAnchor.id); State.currentAnchor = null; this.showOutput(null); await this.refreshHistory(); this.showToast('Anchor deleted', 'info'); } }); saveBtn.addEventListener('click', async () => { if (!State.currentAnchor) return; await Storage.save(State.currentAnchor); await this.refreshHistory(); this.showToast('Anchor saved!', 'success'); }); // Custom events document.addEventListener('select-anchor', async (e) => { const anchors = await Storage.getAll(); const anchor = anchors.find(a => a.id === e.detail.id); if (anchor) { State.currentAnchor = anchor; this.elements.inputText.value = anchor.rawText; this.elements.charCount.textContent = `${anchor.rawText.length} / 5000`; this.renderOutput(anchor); } }); document.addEventListener('search-history', async (e) => { const anchors = await Storage.getAll(); const query = e.detail.query.toLowerCase(); const filtered = anchors.filter(a => a.rawText.toLowerCase().includes(query) || (a.output && a.output.thread?.toLowerCase().includes(query)) ); this.elements.historySidebar.setHistory(filtered, State.currentAnchor?.id); }); document.addEventListener('export-all', () => this.exportAll()); document.addEventListener('clear-all', async () => { await Storage.clear(); State.anchors = []; State.currentAnchor = null; this.showOutput(null); await this.refreshHistory(); this.showToast('All anchors cleared', 'info'); }); }, setGenerating(generating) { const { anchorBtn, outputContent } = this.elements; State.isGenerating = generating; anchorBtn.disabled = generating; anchorBtn.innerHTML = generating ? ` Anchoring...` : ` Anchor`; feather.replace(); }, showOutput(anchor) { const { outputSection, emptyState } = this.elements; if (anchor) { outputSection.classList.remove('hidden'); emptyState.classList.add('hidden'); this.renderOutput(anchor); } else { outputSection.classList.add('hidden'); emptyState.classList.remove('hidden'); } }, renderOutput(anchor) { const { outputContent } = this.elements; const { thread, bullets, nextStep } = anchor.output; outputContent.innerHTML = `