Spaces:
Running
Running
| // ============================================ | |
| // 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 | |
| ? `<svg class="spinner w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Anchoring...` | |
| : `<svg data-feather="anchor" class="w-4 h-4"></svg> 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 = ` | |
| <div class="output-thread">${this.escapeHtml(thread)}</div> | |
| <ul class="output-bullets"> | |
| ${bullets.map(b => `<li>${this.escapeHtml(b)}</li>`).join('')} | |
| </ul> | |
| <div class="output-next-step"> | |
| <strong>Next Step</strong> | |
| ${this.escapeHtml(nextStep)} | |
| </div> | |
| `; | |
| }, | |
| async refreshHistory() { | |
| const anchors = await Storage.getAll(); | |
| this.elements.historySidebar.setHistory(anchors, State.currentAnchor?.id); | |
| }, | |
| getMarkdown(anchor) { | |
| if (!anchor) return ''; | |
| const { rawText, output, createdAt } = anchor; | |
| return `# Anchored Thought\n\n**Date:** ${new Date(createdAt).toLocaleString()}\n\n## Original Thought\n${rawText}\n\n## Thread\n${output.thread}\n\n## Key Insights\n${output.bullets.map(b => `- ${b}`).join('\n')}\n\n## Next Step\n${output.nextStep}`; | |
| }, | |
| copyToClipboard(text) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| this.showToast('Copied to clipboard!', 'success'); | |
| }).catch(() => { | |
| this.showToast('Failed to copy', 'error'); | |
| }); | |
| }, | |
| downloadMarkdown(anchor) { | |
| const md = this.getMarkdown(anchor); | |
| const blob = new Blob([md], { type: 'text/markdown' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `anchor-${anchor.id}.md`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| this.showToast('Markdown exported!', 'success'); | |
| }, | |
| async exportAll() { | |
| const anchors = await Storage.getAll(); | |
| if (anchors.length === 0) { | |
| this.showToast('No anchors to export', 'info'); | |
| return; | |
| } | |
| const data = JSON.stringify(anchors, null, 2); | |
| const blob = new Blob([data], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `mindanchor-backup-${new Date().toISOString().split('T')[0]}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| this.showToast('Backup exported!', 'success'); | |
| }, | |
| showToast(message, type = 'info') { | |
| const { toastContainer } = this.elements; | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type} px-4 py-3 rounded-lg shadow-lg font-medium text-sm`; | |
| toast.textContent = message; | |
| toastContainer.appendChild(toast); | |
| setTimeout(() => { | |
| toast.classList.add('removing'); | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| }, | |
| escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| }; | |
| // ============================================ | |
| // INITIALIZATION | |
| // ============================================ | |
| async function init() { | |
| try { | |
| await Storage.init(); | |
| UI.init(); | |
| await UI.refreshHistory(); | |
| // Initialize feather icons | |
| feather.replace(); | |
| console.log('MindAnchor initialized successfully'); | |
| } catch (error) { | |
| console.error('Initialization error:', error); | |
| UI.showToast('Failed to initialize app', 'error'); | |
| } | |
| } | |
| // Start the app | |
| document.addEventListener('DOMContentLoaded', init); |