// Clarity1 — Smart Journal (Local) const storeKey = 'clarity1_entries_v1'; const chatKey = 'clarity1_chat_v1'; const els = { sidebar: document.getElementById('sidebar'), sidebarToggle: document.getElementById('sidebarToggle'), searchInput: document.getElementById('searchInput'), composeBtn: document.getElementById('composeBtn'), composeModal: document.getElementById('composeModal'), composeForm: document.getElementById('composeForm'), entryTitle: document.getElementById('entryTitle'), entryMood: document.getElementById('entryMood'), entryContent: document.getElementById('entryContent'), entryTags: document.getElementById('entryTags'), aiAssistBtn: document.getElementById('aiAssistBtn'), closeCompose: document.getElementById('closeCompose'), entriesGrid: document.getElementById('entriesGrid'), entriesList: document.getElementById('entriesList'), viewModeGrid: document.getElementById('viewModeGrid'), viewModeList: document.getElementById('viewModeList'), viewJournal: document.getElementById('view-journal'), viewPrompts: document.getElementById('view-prompts'), viewChat: document.getElementById('view-chat'), viewAnalytics: document.getElementById('view-analytics'), viewSettings: document.getElementById('view-settings'), promptsGrid: document.getElementById('promptsGrid'), chatMessages: document.getElementById('chatMessages'), chatForm: document.getElementById('chatForm'), chatInput: document.getElementById('chatInput'), statTotal: document.getElementById('statTotal'), statWeek: document.getElementById('statWeek'), statStreak: document.getElementById('statStreak'), entryModal: document.getElementById('entryModal'), entryModalTitle: document.getElementById('entryModalTitle'), entryModalDate: document.getElementById('entryModalDate'), entryModalMood: document.getElementById('entryModalMood'), entryModalTags: document.getElementById('entryModalTags'), entryModalContent: document.getElementById('entryModalContent'), deleteEntryBtn: document.getElementById('deleteEntryBtn'), closeEntry: document.getElementById('closeEntry'), }; let state = { entries: [], selectedEntryId: null, view: 'journal', mode: 'grid', // or 'list' chat: [], }; const promptsCatalog = [ { title: 'Values Check', body: 'What matters most to me this week?', tags: ['reflection', 'values'] }, { title: 'Peak Moment', body: 'Describe a high point today and why it resonated.', tags: ['gratitude'] }, { title: 'Friction Map', body: 'What’s one friction point I can reduce tomorrow?', tags: ['action'] }, { title: 'Anchor Habit', body: 'If I had to pick one habit to do daily, what is it?', tags: ['habit'] }, { title: 'Perspective Shift', body: 'What assumption am I ready to challenge?', tags: ['growth'] }, { title: 'Energy Audit', body: 'When do I feel most alive?', tags: ['energy'] }, ]; function loadEntries() { try { const data = JSON.parse(localStorage.getItem(storeKey) || '[]'); if (Array.isArray(data)) state.entries = data; } catch (e) { console.warn('Failed to load entries', e); state.entries = []; } } function saveEntries() { localStorage.setItem(storeKey, JSON.stringify(state.entries)); } function loadChat() { try { const data = JSON.parse(localStorage.getItem(chatKey) || '[]'); if (Array.isArray(data)) state.chat = data; } catch { state.chat = []; } } function saveChat() { localStorage.setItem(chatKey, JSON.stringify(state.chat)); } function uid() { return Math.random().toString(36).slice(2) + Date.now().toString(36); } function formatDate(ts) { const d = new Date(ts); return d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); } function renderNav(active) { document.querySelectorAll('.nav-item').forEach(btn => { btn.classList.toggle('bg-primary', btn.dataset.nav === active); btn.classList.toggle('text-onPrimary', btn.dataset.nav === active); }); } function showView(name) { state.view = name; els.viewJournal.classList.toggle('hidden', name !== 'journal'); els.viewPrompts.classList.toggle('hidden', name !== 'prompts'); els.viewChat.classList.toggle('hidden', name !== 'chat'); els.viewAnalytics.classList.toggle('hidden', name !== 'analytics'); els.viewSettings.classList.toggle('hidden', name !== 'settings'); renderNav(name); if (name === 'journal') { renderEntries(); updateAnalytics(); } if (name === 'prompts') { renderPrompts(); } if (name === 'chat') { renderChat(); } if (name === 'analytics') { updateAnalytics(); } } function entryCard(e) { const tags = (e.tags || []).slice(0, 3).map(t => `#${t}`).join(' '); return `

${escapeHtml(e.title || 'Untitled')}

${e.mood}

${escapeHtml((e.content || '').slice(0, 200))}

${formatDate(e.createdAt)}
${tags}
`; } function entryRow(e) { const tags = (e.tags || []).map(t => `#${t}`).join(' '); return `

${escapeHtml(e.title || 'Untitled')}

${e.mood}

${escapeHtml(e.content || '')}

${formatDate(e.createdAt)} ${tags}
`; } function renderEntries() { const q = (els.searchInput.value || '').toLowerCase().trim(); const list = state.entries .slice() .sort((a, b) => b.createdAt - a.createdAt) .filter(e => { if (!q) return true; return ( (e.title || '').toLowerCase().includes(q) || (e.content || '').toLowerCase().includes(q) || (e.tags || []).some(t => t.toLowerCase().includes(q)) ); }); if (state.mode === 'grid') { els.entriesGrid.classList.remove('hidden'); els.entriesList.classList.add('hidden'); els.entriesGrid.innerHTML = list.map(entryCard).join('') || `
No entries yet. Create your first one!
`; } else { els.entriesGrid.classList.add('hidden'); els.entriesList.classList.remove('hidden'); els.entriesList.innerHTML = list.map(entryRow).join('') || `
No entries yet. Create your first one!
`; } // Re-bind icons in dynamically injected DOM if (window.feather) feather.replace(); // Bind view buttons document.querySelectorAll('[data-view]').forEach(btn => { btn.addEventListener('click', () => openEntry(btn.getAttribute('data-view'))); }); } function renderPrompts() { els.promptsGrid.innerHTML = promptsCatalog.map(p => `

${p.title}

${p.body}

${(p.tags || []).map(t => `#${t}`).join(' ')}
`).join(''); if (window.feather) feather.replace(); document.querySelectorAll('[data-use-prompt]').forEach(btn => { btn.addEventListener('click', () => { const p = JSON.parse(decodeURIComponent(btn.getAttribute('data-use-prompt'))); openCompose({ preset: p }); }); }); } function openCompose({ preset } = {}) { if (preset) { els.entryTitle.value = preset.title || ''; els.entryContent.value = `${preset.body}\n\n`; els.entryTags.value = (preset.tags || []).join(', '); } else { els.composeForm.reset(); } els.composeModal.classList.remove('hidden'); els.composeModal.classList.add('flex'); } function closeCompose() { els.composeModal.classList.add('hidden'); els.composeModal.classList.remove('flex'); } function openEntry(id) { const entry = state.entries.find(e => e.id === id); if (!entry) return; state.selectedEntryId = id; els.entryModalTitle.textContent = entry.title || 'Untitled'; els.entryModalDate.textContent = formatDate(entry.createdAt); els.entryModalMood.textContent = entry.mood; els.entryModalTags.textContent = (entry.tags || []).map(t => `#${t}`).join(' '); els.entryModalContent.innerHTML = markdownToHtml(entry.content || ''); els.entryModal.classList.remove('hidden'); els.entryModal.classList.add('flex'); } function closeEntry() { els.entryModal.classList.add('hidden'); els.entryModal.classList.remove('flex'); state.selectedEntryId = null; } function renderChat() { els.chatMessages.innerHTML = state.chat.map(m => `
${m.role === 'assistant' ? `
` : `
`}
${escapeHtml(m.content)}
`).join('') || `
Ask Clarity1 to reflect on your journal.
`; if (window.feather) feather.replace(); els.chatMessages.scrollTop = els.chatMessages.scrollHeight; } function aiAssistCompose(content, mood, tags) { // Lightweight local "AI" assistant for compose window const base = content.trim(); const suggestions = [ 'You might expand this with a concrete next step.', 'Try adding an example that illustrates your point.', 'Consider noting one thing you’re grateful for related to this.', ]; const pick = () => suggestions[Math.floor(Math.random() * suggestions.length)]; return ` Here’s a polished version of your entry: ${base} Suggestions: - ${pick()} - ${pick()} - ${pick()} Mood matched: ${mood}. Tags suggested: ${(tags || []).slice(0, 4).join(', ') || 'journal, reflection'} `.trim(); } function aiChatRespond(message) { const m = message.trim(); if (!m) return 'Please enter a message.'; if (/summar/i.test(m)) { return summarizeEntries(); } if (/prompt/i.test(m)) { return 'Here are three prompts to help you reflect:\n1) What energized me today?\n2) What did I avoid, and why?\n3) What assumption am I willing to revisit?'; } if (/procrastinat/i.test(m)) { return 'A quick approach:\n- Name the task in one sentence.\n- Identify the tiniest next action.\n- Timebox 10 minutes and start.'; } if (/action|next|steps/i.test(m)) { return 'Turn your draft into steps:\n1) Define the outcome.\n2) List the smallest actionable task.\n3) Assign a time window.'; } if (/last week|week/i.test(m)) { return summarizeEntries(); } return localFallback(m); } function localFallback(msg) { const rules = [ { k: /hello|hi/i, r: 'Hello! Ask me to summarize your week, generate prompts, or refine a draft.' }, { k: /help/i, r: 'Try: “Summarize my entries from last week”, “Give me three prompts”, or “Turn my draft into steps”.' }, ]; for (const { k, r } of rules) if (k.test(msg)) return r; return `I heard: “${msg}”. I can help summarize your entries or turn thoughts into steps.`; } function summarizeEntries() { const total = state.entries.length; if (!total) return 'No entries to summarize yet.'; const last7 = state.entries.filter(e => Date.now() - e.createdAt <= 7 * 86400000); const words = state.entries.reduce((acc, e) => acc + (e.content || '').split(/\s+/).filter(Boolean).length, 0); const moods = state.entries.reduce((acc, e) => { const m = (e.mood || '').split(' ')[0]; acc[m] = (acc[m] || 0) + 1; return acc; }, {}); const moodTop = Object.entries(moods).sort((a,b) => b[1]-a[1])[0]?.[0] || 'Unknown'; return `Summary: - ${total} total entries - ${last7.length} entries in the last 7 days - Common mood: ${moodTop} - Estimated total words: ${words}`; } function updateAnalytics() { const total = state.entries.length; const week = state.entries.filter(e => Date.now() - e.createdAt <= 7 * 86400000).length; // Simple streak: count consecutive days with entries ending today const days = new Set(state.entries.map(e => new Date(e.createdAt).toDateString())); let streak = 0; let cursor = new Date(); while (days.has(cursor.toDateString())) { streak += 1; cursor.setDate(cursor.getDate() - 1); } els.statTotal.textContent = total; els.statWeek.textContent = week; els.statStreak.textContent = streak; } function escapeHtml(s) { return (s || '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function markdownToHtml(s) { // Very small subset: headings, bold, italics, lists, line breaks return escapeHtml(s) .replace(/^### (.*$)/gim, '

$1

') .replace(/^## (.*$)/gim, '

$1

') .replace(/^# (.*$)/gim, '

$1

') .replace(/\*\*(.*?)\*\*/gim, '$1') .replace(/\*(.*?)\*/gim, '$1') .replace(/\n$/gim, '
') .replace(/\n/gim, '
'); } // Theme toggle function setTheme(t) { document.documentElement.classList.toggle('dark', t === 'dark'); localStorage.setItem('clarity1_theme', t); } function initTheme() { const saved = localStorage.getItem('clarity1_theme'); if (saved) { setTheme(saved); } else { setTheme('dark'); // enforce dark as requested } } function bindEvents() { els.sidebarToggle.addEventListener('click', () => { els.sidebar.classList.toggle('hidden'); }); document.querySelectorAll('.nav-item').forEach(btn => { btn.addEventListener('click', () => showView(btn.dataset.nav)); }); els.searchInput.addEventListener('input', renderEntries); els.viewModeGrid.addEventListener('click', () => { state.mode = 'grid'; renderEntries(); }); els.viewModeList.addEventListener('click', () => { state.mode = 'list'; renderEntries(); }); els.composeBtn.addEventListener('click', () => openCompose()); els.closeCompose.addEventListener('click', closeCompose); els.composeForm.addEventListener('submit', (e) => { e.preventDefault(); const entry = { id: uid(), title: els.entryTitle.value.trim(), mood: els.entryMood.value, content: els.entryContent.value, tags: els.entryTags.value.split(',').map(s => s.trim()).filter(Boolean), createdAt: Date.now(), }; state.entries.unshift(entry); saveEntries(); closeCompose(); renderEntries(); updateAnalytics(); }); els.aiAssistBtn.addEventListener('click', () => { const suggestion = aiAssistCompose( els.entryContent.value, els.entryMood.value, els.entryTags.value.split(',').map(s => s.trim()).filter(Boolean) ); els.entryContent.value = suggestion; }); els.chatForm.addEventListener('submit', (e) => { e.preventDefault(); const text = els.chatInput.value.trim(); if (!text) return; state.chat.push({ role: 'user', content: text }); els.chatInput.value = ''; saveChat(); renderChat(); setTimeout(() => { const reply = aiChatRespond(text); state.chat.push({ role: 'assistant', content: reply }); saveChat(); renderChat(); }, 300); }); document.querySelectorAll('.chat-example').forEach(btn => { btn.addEventListener('click', () => { els.chatInput.value = btn.textContent.trim(); els.chatForm.dispatchEvent(new Event('submit')); }); }); document.querySelectorAll('.theme-toggle').forEach(btn => { btn.addEventListener('click', () => setTheme(btn.dataset.theme)); }); els.deleteEntryBtn.addEventListener('click', () => { if (!state.selectedEntryId) return; if (confirm('Delete this entry?')) { state.entries = state.entries.filter(e => e.id !== state.selectedEntryId); saveEntries(); closeEntry(); renderEntries(); updateAnalytics(); } }); els.closeEntry.addEventListener('click', closeEntry); } function boot() { initTheme(); loadEntries(); loadChat(); bindEvents(); renderNav('journal'); renderEntries(); renderPrompts(); renderChat(); updateAnalytics(); } document.addEventListener('DOMContentLoaded', boot);