// 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(' ')}
Use
`).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' ? `
` : `
`}
`).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);