mindanchor / script.js
timelord7000's picture
# Predictable + Bulletproof Dev Cycle (Recall Anchor)
87b24f4 verified
// ============================================
// 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);