From 477e86f694611e1cc4eb09bc87775ce0be591309 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 28 Sep 2026 10:46:58 +0900 Subject: [PATCH] feat(insights): add factual weekly and monthly reports --- package.json | 2 +- public/addons.js | 2 +- public/i18n-renderers.js | 48 +++++++++++++++++++++++++++++++++++---- public/index.html | 6 ++--- public/locales/en.json | 33 ++++++++++++++++++++------- public/locales/zh-CN.json | 33 ++++++++++++++++++++------- public/service-worker.js | 2 +- scripts/build.mjs | 2 +- scripts/validate.mjs | 39 +++++++++++++++++++++++++++++-- 9 files changed, 138 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 857621f..f28a32d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "life-os", - "version": "1.5.8", + "version": "1.6.0", "private": true, "type": "module", "scripts": { diff --git a/public/addons.js b/public/addons.js index 7f786aa..688472b 100644 --- a/public/addons.js +++ b/public/addons.js @@ -3,7 +3,7 @@ const DRAFT_KEY = 'lifeos_today_draft_v1'; const localDay = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; }; const escapeHtml = value => String(value || '').replace(/[&<>]/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); - const APP_VERSION = '1.5.8'; + const APP_VERSION = '1.6.0'; window.LifeOSVersion = APP_VERSION; const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system']; const t = (key, values) => LifeI18n.t(key, values); diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js index 6b2a3ff..5e6ed3b 100644 --- a/public/i18n-renderers.js +++ b/public/i18n-renderers.js @@ -61,10 +61,47 @@ window.preserve = id => { const all = museum(), alreadyPreserved = all.some(item => item.memoryId === id); if (alreadyPreserved) { save('lifeos_museum', all.filter(item => item.memoryId !== id)); toast(t('timeline.unpreserved')); } else { save('lifeos_museum', [...all, { id: `artifact_${crypto.randomUUID()}`, memoryId: id, preservedAt: new Date().toISOString() }]); toast(t('timeline.added')); } closeModal(); renderAll(); }; window.confirmDelete = id => { rootModal().innerHTML = ``; }; window.deleteMemory = id => { save('lifeos_memories', mems().filter(item => item.id !== id)); save('lifeos_museum', museum().filter(item => item.memoryId !== id)); rootModal().innerHTML = ''; renderAll(); toast(t('timeline.removed')); }; + let insightView = 'week', insightOffset = 0; + const reportDateKey = value => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; + const reportDayNumber = value => Date.UTC(value.getFullYear(), value.getMonth(), value.getDate()) / 864e5; + const reportEventDate = value => { if (!value) return ''; const parsed = new Date(value); return Number.isNaN(parsed.getTime()) ? '' : reportDateKey(parsed); }; + const reportPeriod = () => { + const today = new Date(), first = insightView === 'week' ? new Date(today.getFullYear(), today.getMonth(), today.getDate(), 12) : new Date(today.getFullYear(), today.getMonth() + insightOffset, 1, 12); + if (insightView === 'week') first.setDate(first.getDate() - (first.getDay() + 6) % 7 + insightOffset * 7); + const next = new Date(first); + if (insightView === 'week') next.setDate(next.getDate() + 7); else next.setMonth(next.getMonth() + 1); + const last = new Date(next); last.setDate(last.getDate() - 1); + const elapsed = Math.min(reportDayNumber(next), reportDayNumber(today) + 1) - reportDayNumber(first); + return { start: reportDateKey(first), end: reportDateKey(next), last: reportDateKey(last), today: reportDateKey(today), elapsed }; + }; + window.setInsightsView = view => { if (!['week', 'month'].includes(view)) return; insightView = view; insightOffset = 0; window.insights(document.querySelector('#content-insights')); }; + window.shiftInsights = delta => { const next = insightOffset + delta; if (next > 0 || !Number.isInteger(next)) return; insightOffset = next; window.insights(document.querySelector('#content-insights')); }; window.insights = container => { - const all = mems(), recent = all.filter(item => Date.now() - new Date(item.date + 'T12:00:00') < 2592e6), counts = { clear: 0, light: 0, cloudy: 0, rain: 0, storm: 0 }; recent.forEach(item => counts[item.mood]++); - const average = all.length ? (all.reduce((sum, item) => sum + item.energy, 0) / all.length).toFixed(1) : '—', dayCounts = {}; all.forEach(item => { const name = weekday(item.date); dayCounts[name] = (dayCounts[name] || 0) + 1; }); const active = Object.entries(dayCounts).sort((a,b) => b[1] - a[1])[0]?.[0] || '—'; - container.innerHTML = `
${t('insights.eyebrow')}

${t('insights.title')}

${t('insights.weather')}
${Object.entries(counts).map(([key, value]) => `
${weather(key)} ${value} ${t('common.days')}
`).join('')}
${t('insights.average')}
${average} / 5
${all.slice(0,10).reverse().map(item => ``).join('') || `${t('insights.noEntries')}`}
${t('insights.active')}
${active}
${t('insights.current')}
${streak(all)} ${t('common.days')}
${t('insights.longest',{count:longest(all)})}
`; + if (!container) return; + const period = reportPeriod(), inPeriod = key => key && key >= period.start && key < period.end && key <= period.today; + const records = mems().filter(item => inPeriod(item.date)).sort((a, b) => a.date.localeCompare(b.date)); + const daysRecorded = new Set(records.map(item => item.date)).size; + const board = sys().tasksBoard || {}, tasks = Array.isArray(board.tasks) ? board.tasks : [], goals = Array.isArray(board.goals) ? board.goals : []; + const tasksCreated = tasks.filter(item => inPeriod(reportEventDate(item.createdAt))).length; + const tasksCompleted = tasks.filter(item => item.done && inPeriod(reportEventDate(item.completedAt))).length; + const goalsCompleted = goals.filter(item => item.done && inPeriod(reportEventDate(item.completedAt))).length; + const weatherCounts = { clear: 0, light: 0, cloudy: 0, rain: 0, storm: 0 }; + records.forEach(item => { if (Object.hasOwn(weatherCounts, item.mood)) weatherCounts[item.mood]++; }); + const commonWeather = Object.entries(weatherCounts).filter(([, count]) => count).sort((a, b) => b[1] - a[1]).slice(0, 3); + const energies = records.map(item => ({ date: item.date, value: Number(item.energy) })).filter(item => Number.isFinite(item.value) && item.value >= 1 && item.value <= 5); + const energyAverage = energies.length ? (energies.reduce((sum, item) => sum + item.value, 0) / energies.length).toFixed(1) : '—'; + const favorites = new Set(museum().map(item => item.memoryId)); + const hasHighlightTag = item => (Array.isArray(item.tags) ? item.tags : []).some(tag => ['高光', 'highlight'].includes(String(tag).replace(/^#/, '').trim().toLowerCase())); + const highlights = records.filter(item => favorites.has(item.id) || hasHighlightTag(item)).reverse().slice(0, 3); + const count = value => LifeI18n.number(value), range = `${LifeI18n.date(period.start, { month: 'short', day: 'numeric', year: 'numeric' })} — ${LifeI18n.date(period.last, { month: 'short', day: 'numeric', year: 'numeric' })}`; + container.innerHTML = `
${t('insights.eyebrow')}

${t('insights.title')}

${t('insights.description')}

+
${['week', 'month'].map(view => ``).join('')}
${range}
+
${t('insights.records')}
${count(daysRecorded)} / ${count(period.elapsed)}

${t('insights.recordedDays', { count: count(records.length), percent: count(period.elapsed ? Math.round(daysRecorded / period.elapsed * 100) : 0) })}

+
${t('insights.tasks')}
${count(tasksCompleted)}

${t('insights.completedTasks')}

${t('insights.taskDetails', { created: count(tasksCreated), goals: count(goalsCompleted) })}
+
${t('insights.weather')}
${commonWeather.length ? `
${commonWeather.map(([key, value]) => `
${weather(key)}${count(value)}
`).join('')}
` : `

${t('insights.noWeather')}

`}
+
${t('insights.energy')}
${energyAverage} / 5

${energies.length > 1 ? t('insights.energyFromTo', { first: energies[0].value, last: energies.at(-1).value }) : energies.length ? t('insights.singleEnergy') : t('insights.noEnergy')}

${energies.length ? `
${energies.map(item => ``).join('')}
` : ''}
+
${t('insights.highlights')}

${t('insights.highlightSource')}

${highlights.length ? `
${highlights.map(item => ``).join('')}
` : `
${t('insights.highlightEmpty')}
`}
+

${t('insights.note')}

`; }; window.profileApp = container => { const profileData = profile(), all = mems(), common = Object.entries(all.reduce((out, item) => (out[item.mood] = (out[item.mood] || 0) + 1, out), {})).sort((a,b) => b[1] - a[1])[0]?.[0] || '—', average = all.length ? (all.reduce((sum,item) => sum + item.energy,0) / all.length).toFixed(1) : '—', started = Math.max(1, Math.floor((Date.now() - new Date(profileData.startDate)) / 864e5)); @@ -73,7 +110,7 @@ window.saveProfile = () => { save('lifeos_profile', { name: pname.value, birthday: pbday.value, startDate: pstart.value || day(), motto: pmotto.value }); renderAll(); toast(t('profile.saved')); }; window.systemApp = container => { const state = settings(), preference = LifeI18n.preference(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = browserLanguage() === 'zh' ? t('system.detectedZh') : t('system.detectedEn'); - container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${t('system.data')}
${t('system.storage')}

${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}

${t('system.dataStorage')}

${t('system.stored')}
${t('system.backups')}

${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}

${t('system.version')}

${t('system.release',{version:window.LifeOSVersion||'1.5.8'})}

`; + container.innerHTML = `
${t('system.eyebrow')}

${t('system.title')}

${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}

${detected}

${t('system.data')}
${t('system.storage')}

${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}

${t('system.dataStorage')}

${t('system.stored')}
${t('system.backups')}

${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}

${t('system.version')}

${t('system.release',{version:window.LifeOSVersion||'1.6.0'})}

`; }; window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = ``; }; window.replaceConfirm = () => { rootModal().innerHTML = ``; }; @@ -101,6 +138,9 @@ const memoryStyle = document.createElement('style'); memoryStyle.textContent = `.memory-meta-form{display:grid;gap:9px;margin-top:12px}.memory-title-field{display:grid;gap:6px;margin-top:10px;color:var(--muted);font-size:12px}.memory-title-field input,.memory-edit-form input{width:100%;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;color:var(--ink);outline:none;font:inherit}.memory-title-field input:focus,.memory-edit-form input:focus{border-color:var(--accent)}.memory-meta-form label,.memory-edit-form>label{display:grid;gap:6px;color:var(--muted);font-size:12px}.memory-meta-form input,.memory-meta-form select,.memory-edit-form input,.memory-edit-form textarea{width:100%;min-width:0;border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px 11px;color:var(--ink);outline:none;font:inherit}.memory-meta-form input:focus,.memory-meta-form select:focus,.memory-edit-form input:focus,.memory-edit-form textarea:focus,.timeline-tools input:focus,.timeline-tools select:focus{border-color:var(--accent)}.memory-edit-form{display:grid;gap:12px;max-height:min(80vh,720px);overflow:auto}.memory-edit-form h2{font-size:21px;margin:0}.memory-edit-form textarea{resize:vertical;line-height:1.55}.memory-edit-form .memory-meta-form{margin:0}.memory-edit-form .actions{margin:0}.timeline-tools{display:grid;grid-template-columns:minmax(0,1fr) 190px;gap:8px;margin:17px 0 11px}.timeline-tools input,.timeline-tools select{width:100%;min-width:0;height:40px;border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);padding:0 11px;outline:none;font:13px Manrope,system-ui}.timeline-tools input::placeholder{color:var(--muted)}.memory-open{display:block!important;text-align:left}.memory-badges{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}.memory-badges span,.memory-detail-meta{color:var(--muted);font-size:12px}.memory-badges span{border:1px solid var(--line);background:var(--panel2);border-radius:99px;padding:4px 8px}.detail-badges{margin:0 0 14px}.text-link{border:0;background:none;padding:0;color:var(--accent);text-decoration:underline}.memory-detail-meta{margin:8px 0}.memory-edit-form .sub{margin:0}@media(max-width:600px){.timeline-tools{grid-template-columns:1fr 130px}.memory-badges span{font-size:11px}.memory-meta-form label,.memory-edit-form>label,.memory-title-field{font-size:13px}}`; document.head.append(memoryStyle); + const reportStyle = document.createElement('style'); + reportStyle.textContent = `.report-description{max-width:620px;line-height:1.55}.report-controls{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;margin:20px 0}.report-range{display:flex;align-items:center;gap:10px;min-width:0}.report-range strong{font-size:14px;line-height:1.4;text-align:center}.report-step{width:34px;height:34px;flex:none;border:1px solid var(--line);border-radius:8px;background:var(--panel2);color:var(--ink);cursor:pointer;font-size:22px}.report-step:disabled{opacity:.4;cursor:default}.report-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.report-card{min-width:0;padding:18px;border:1px solid var(--line);border-radius:12px;background:var(--panel2)}.report-card .eyebrow{margin-bottom:12px}.report-card p{font-size:13px;line-height:1.5;color:var(--muted);margin:8px 0 0}.report-big{font-size:32px;font-weight:700;line-height:1.2}.report-big span{font-size:15px;color:var(--muted)}.report-secondary{color:var(--muted);font-size:12px;line-height:1.5;margin-top:8px}.report-weather{display:grid;gap:8px}.report-weather>div{display:flex;justify-content:space-between;gap:10px;font-size:14px}.report-energy-bars{height:50px;display:flex;align-items:end;gap:4px;margin-top:14px}.report-energy-bars i{flex:1;min-width:3px;max-width:24px;background:var(--accent);border-radius:3px 3px 0 0}.report-highlights{margin-top:24px}.report-highlights .sub{line-height:1.5}.report-highlight-list{display:grid;gap:8px}.report-highlight{display:grid;gap:5px;width:100%;padding:14px;text-align:left;border:1px solid var(--line);border-radius:10px;background:var(--panel2);color:var(--ink);cursor:pointer}.report-highlight:hover,.report-highlight:focus-visible{border-color:var(--accent)}.report-highlight small,.report-highlight span{color:var(--muted);line-height:1.4}.report-highlight span{font-size:13px}.report-empty{padding:18px;border:1px dashed var(--line);border-radius:10px;color:var(--muted);font-size:13px}.report-note{margin-top:18px;color:var(--muted);font-size:12px;line-height:1.5}@media(max-width:650px){.report-grid{grid-template-columns:1fr}.report-controls{align-items:flex-start}.report-range{width:100%;justify-content:space-between}}`; + document.head.append(reportStyle); // Render every surface again after the overrides are installed; no DOM walking is involved. window.renderIcons(); window.renderAll(); window.applyLanguage(); })(); diff --git a/public/index.html b/public/index.html index 03b3c09..db1fd1c 100644 --- a/public/index.html +++ b/public/index.html @@ -14,7 +14,7 @@ function browserLanguage(){let ls=navigator.languages?.length?navigator.language function appName(id){return i18n(APPS[id][1])}function moodName(m){return i18n(m.toUpperCase(),cap(m))}function day(){const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`}function fmtDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'})}function shortDate(d){return new Date(d+'T12:00:00').toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function cap(x){return x[0].toUpperCase()+x.slice(1)}function esc(s){return (s||'').replace(/[&<>]/g,x=>({'&':'&','<':'<','>':'>'}[x]))} function applyWallpaper(w){if(!w?.url)return;desktop.style.backgroundImage=`linear-gradient(120deg,color-mix(in srgb,var(--bg) 68%,transparent),color-mix(in srgb,var(--bg) 36%,transparent)),url('${w.url}')`;desktop.style.backgroundSize='cover';desktop.style.backgroundPosition='center';desktop.title=LifeI18n.t('shell.wallpaperTitle',{title:w.title||LifeI18n.t('wallpaper.dailyImage')})}async function loadBingWallpaper(){let s=settings();if(s.wallpaper?.date===day()&&s.wallpaper?.source==='lifeos-proxy'){applyWallpaper(s.wallpaper);return}try{let r=await fetch('/api/bing-wallpaper'),d=await r.json();if(!r.ok||!d.url)throw Error(d.error||'No Bing image');s.wallpaper={date:day(),url:'/api/bing-wallpaper-image?date='+day(),title:d.title||'Bing daily image',source:'lifeos-proxy'};save('lifeos_settings',s);applyWallpaper(s.wallpaper)}catch(e){console.info('Bing wallpaper unavailable; using the calm fallback.',e)}}function init(){let s=settings();document.documentElement.dataset.theme=s.theme==='auto'?(matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'):s.theme;renderIcons();renderTaskbar();loadBingWallpaper();setInterval(clock,1000);clock();setTimeout(()=>window.applyLanguage?.(),0);setTimeout(()=>boot.remove(),650)} function desktopIconClick(id,e){if(innerWidth<650){openApp(id);return}selectedIcon=id;document.querySelectorAll('.app-icon').forEach(x=>x.classList.remove('selected'));e.currentTarget.classList.add('selected')}function desktopIconKey(id,e){if(e.key==='Enter'||e.key===' '){e.preventDefault();openApp(id)}}function renderIcons(){icons.innerHTML=Object.entries(APPS).map(([id,[g,n]])=>``).join('')}function taskClick(id){let w=windows[id];if(!w){openApp(id);return}if(w.classList.contains('min')){openApp(id);return}if(activeWindow===id){minWin(id);return}focus(w)}function renderTaskbar(){let ids=innerWidth<650?Object.keys(windows):['today','timeline','museum','insights','tasks','calendar'];tasks.innerHTML=ids.map(id=>{let w=windows[id],min=w?.classList.contains('min'),fg=activeWindow===id&&!min,name=window.appName(id);return``}).join('')} -function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString(LifeI18n.locale(), {hour:'2-digit',minute:'2-digit'});document.querySelector('#date').textContent=d.toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function toggleLauncher(){launcher.classList.toggle('hidden');calendar.classList.add('hidden');launcher.innerHTML=`
LIFE OS${LifeI18n.t(`shell.${new Date().getHours()<12?'morning':new Date().getHours()<18?'afternoon':'evening'}`)}
${Object.entries(APPS).map(([id,a])=>``).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.5.8'})}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`
${LifeI18n.t('shell.systemDate')}

${fmtDate(day())}

${LifeI18n.t('shell.todaySummary',{memories:mems().length,preserved:museum().length})}

`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let labels={open:LifeI18n.t('shell.open'),min:LifeI18n.t('shell.minimize'),close:LifeI18n.t('shell.menuCloseWindow'),refresh:LifeI18n.t('shell.refresh'),new:LifeI18n.t('shell.newToday'),system:LifeI18n.t('shell.systemSettings')},actions=id?`${windows[id]?``:''}`:`
`;let m=document.createElement('div');m.id='life-context';m.style.cssText=`position:fixed;z-index:7000;left:${Math.min(e.clientX,innerWidth-190)}px;top:${Math.min(e.clientY,innerHeight-150)}px;width:180px;padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)`;m.innerHTML=actions;m.querySelectorAll('button').forEach(b=>b.style.cssText='display:block;width:100%;border:0;background:transparent;color:var(--ink);text-align:left;border-radius:6px;padding:9px 10px;font:12px Manrope');m.querySelectorAll('button').forEach(b=>b.onmouseenter=()=>b.style.background='var(--panel2)');document.body.append(m)}document.addEventListener('contextmenu',e=>{if(e.target.closest('.desktop')&&!e.target.closest('.app-icon')&&!e.target.closest('.window'))showContext(e,null)});document.addEventListener('click',hideContext) +function clock(){let d=new Date();document.querySelector('#clock').textContent=d.toLocaleTimeString(LifeI18n.locale(), {hour:'2-digit',minute:'2-digit'});document.querySelector('#date').textContent=d.toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',lang()==='zh'?{month:'numeric',day:'numeric'}:{month:'short',day:'numeric'}).toUpperCase()}function toggleLauncher(){launcher.classList.toggle('hidden');calendar.classList.add('hidden');launcher.innerHTML=`
LIFE OS${LifeI18n.t(`shell.${new Date().getHours()<12?'morning':new Date().getHours()<18?'afternoon':'evening'}`)}
${Object.entries(APPS).map(([id,a])=>``).join('')}
${LifeI18n.t('shell.systemOnline',{version:window.LifeOSVersion||'1.6.0'})}
`}function toggleCalendar(){calendar.classList.toggle('hidden');launcher.classList.add('hidden');calendar.innerHTML=`
${LifeI18n.t('shell.systemDate')}

${fmtDate(day())}

${LifeI18n.t('shell.todaySummary',{memories:mems().length,preserved:museum().length})}

`}function hideContext(){document.querySelector('#life-context')?.remove()}function showContext(e,id){e.preventDefault();e.stopPropagation();hideContext();let labels={open:LifeI18n.t('shell.open'),min:LifeI18n.t('shell.minimize'),close:LifeI18n.t('shell.menuCloseWindow'),refresh:LifeI18n.t('shell.refresh'),new:LifeI18n.t('shell.newToday'),system:LifeI18n.t('shell.systemSettings')},actions=id?`${windows[id]?``:''}`:`
`;let m=document.createElement('div');m.id='life-context';m.style.cssText=`position:fixed;z-index:7000;left:${Math.min(e.clientX,innerWidth-190)}px;top:${Math.min(e.clientY,innerHeight-150)}px;width:180px;padding:6px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow)`;m.innerHTML=actions;m.querySelectorAll('button').forEach(b=>b.style.cssText='display:block;width:100%;border:0;background:transparent;color:var(--ink);text-align:left;border-radius:6px;padding:9px 10px;font:12px Manrope');m.querySelectorAll('button').forEach(b=>b.onmouseenter=()=>b.style.background='var(--panel2)');document.body.append(m)}document.addEventListener('contextmenu',e=>{if(e.target.closest('.desktop')&&!e.target.closest('.app-icon')&&!e.target.closest('.window'))showContext(e,null)});document.addEventListener('click',hideContext) function openApp(id){launcher.classList.add('hidden');let w=windows[id];if(w){w.classList.remove('min');focus(w);return}w=document.createElement('section');w.className='window';w.dataset.id=id;w.style.left=(innerWidth<650?0:90+Object.keys(windows).length*36)+'px';w.style.top=(innerWidth<650?0:72+Object.keys(windows).length*28)+'px';w.innerHTML=`
${APPS[id][0]}${appName(id)}
`;windows[id]=w;document.querySelector('#windows').append(w);let controls=w.querySelector('.controls'),bar=w.querySelector('.titlebar');controls.addEventListener('pointerdown',e=>{e.preventDefault();e.stopPropagation()});controls.addEventListener('click',e=>{let action=e.target.closest('button')?.dataset.windowAction;if(!action)return;e.preventDefault();e.stopPropagation();if(action==='min')minWin(id);if(action==='max')maxWin(id);if(action==='close')closeWin(id)});bar.addEventListener('dblclick',e=>{if(!e.target.closest('.controls'))maxWin(id)});w.addEventListener('pointerdown',()=>focus(w));drag(w);renderApp(id);focus(w)}function focus(w){if(!w)return;activeWindow=w.dataset.id;w.style.zIndex=++z;document.querySelectorAll('.window').forEach(x=>x.classList.toggle('active-window',x===w));renderTaskbar()}function closeWin(id){windows[id]?.remove();delete windows[id];if(activeWindow===id){activeWindow=Object.keys(windows).find(k=>!windows[k].classList.contains('min'))||null;if(activeWindow)focus(windows[activeWindow])}renderTaskbar()}function minWin(id){let w=windows[id];if(!w)return;w.classList.add('min');if(activeWindow===id){activeWindow=Object.keys(windows).find(k=>k!==id&&!windows[k].classList.contains('min'))||null;if(activeWindow)focus(windows[activeWindow])}renderTaskbar();toast(i18n('MINIMIZED — TAP THE APP ICON TO RESTORE'))}function maxWin(id){let w=windows[id];if(!w)return;w.classList.toggle('max');focus(w)}function snapWindow(w){if(innerWidth<650||w.classList.contains('max'))return;let r=w.getBoundingClientRect(),edge=22;if(r.topinnerWidth-edge){w.style.left='calc(50vw + 9px)';w.style.top='18px';w.style.width='calc(50vw - 27px)';w.style.height='calc(100vh - 98px)'}}function drag(w){let bar=w.querySelector('.titlebar'),x,y,l,t;bar.onpointerdown=e=>{if(e.target.closest('.controls')||innerWidth<650)return;if(w.classList.contains('max'))w.classList.remove('max');x=e.clientX;y=e.clientY;l=w.offsetLeft;t=w.offsetTop;bar.setPointerCapture(e.pointerId);bar.onpointermove=e=>{w.style.left=Math.max(0,l+e.clientX-x)+'px';w.style.top=Math.max(0,t+e.clientY-y)+'px'};bar.onpointerup=()=>{bar.onpointermove=null;snapWindow(w)}}}document.addEventListener('keydown',e=>{if(e.altKey&&e.key==='F4'&&activeWindow){e.preventDefault();closeWin(activeWindow)}if(e.key==='Escape'&&activeWindow&&!document.querySelector('.modal')&&!document.querySelector('#lifeos-search')){let w=windows[activeWindow];if(w?.classList.contains('max')){e.preventDefault();maxWin(activeWindow)}}}) function renderApp(id){let c=document.querySelector('#content-'+id);if(!c)return;if(id==='today')window.today(c);if(id==='timeline')window.timeline(c);if(id==='museum')window.museumApp(c);if(id==='insights')window.insights(c);if(id==='profile')window.profileApp(c);if(id==='tasks')window.tasksApp(c);if(id==='calendar')window.calendarApp(c);if(id==='system')window.systemApp(c);setTimeout(()=>window.applyLanguage?.(),0)}function renderAll(){Object.keys(windows).forEach(renderApp);renderTaskbar();window.renderDesktopGoalCountdowns?.();setTimeout(()=>window.applyLanguage?.(),0)}document.addEventListener('click',e=>{if(e.target.closest('#content-today .chip,#content-today .energy button'))setTimeout(()=>window.applyLanguage?.(),0)}) let todayDraft=null,todayTitleDraft=null,todayTargetDate=null;function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft;c.innerHTML=`
${new Date().toLocaleDateString('en-US',{weekday:'long'}).toUpperCase()}

${fmtDate(day()).replace(/^[^,]+, /,'')}

How was your day?

`}function filmFor(m,e){const dict=LifeI18n.dictionaries[lang()==='zh'?'zh':'en'],seed=[...`${day()}|${m}|${e}`].reduce((sum,char,index)=>sum+char.charCodeAt(0)*(index+1),0),n=seed%dict.prompt.films.length;return{filmTitle:dict.prompt.films[n].title,filmSubtitle:dict.prompt.films[n].subtitle,filmLine:dict.prompt.lines[(n*3+e)%dict.prompt.lines.length]}}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';window.today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)}function setMood(x){selectedMood=x;rerenderToday()}function setEnergy(x){selectedEnergy=x;rerenderToday()}function archiveToday(){let text=document.querySelector('#entry').value.trim(),a=mems(),old=a.find(x=>x.date===day()),f=old||filmFor(selectedMood,selectedEnergy),now=new Date().toISOString(),m={id:old?.id||'memory_'+crypto.randomUUID(),date:day(),text,mood:selectedMood,energy:selectedEnergy,filmTitle:f.filmTitle,filmSubtitle:f.filmSubtitle,filmLine:f.filmLine,createdAt:old?.createdAt||now,updatedAt:now};let status=document.querySelector('#saveStatus');status.textContent='ARCHIVING MEMORY...';setTimeout(()=>{save('lifeos_memories',[...a.filter(x=>x.date!==day()),m].sort((x,y)=>y.date.localeCompare(x.date)));todayDraft=null;status.textContent='MEMORY ARCHIVED';renderAll()},360)} @@ -23,6 +23,6 @@ function museumApp(c){let a=museum(),ms=mems();c.innerHTML=`
m.date===d.toISOString().slice(0,10))){s++;d.setDate(d.getDate()-1)}return s}function longest(a){let ds=a.map(x=>x.date).sort(),best=0,run=0,last='';ds.forEach(d=>{run=last&&((new Date(d)-new Date(last))/864e5===1)?run+1:1;best=Math.max(best,run);last=d});return best}function insights(c){let a=mems(),last=a.filter(m=>(Date.now()-new Date(m.date+'T12:00:00'))<2592e6),cnt={clear:0,light:0,cloudy:0,rain:0,storm:0};last.forEach(x=>cnt[x.mood]++);let avg=a.length?(a.reduce((s,x)=>s+x.energy,0)/a.length).toFixed(1):'—',days={};a.forEach(x=>{let d=new Date(x.date+'T12:00:00').toLocaleDateString('en-US',{weekday:'long'});days[d]=(days[d]||0)+1});let active=Object.entries(days).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—';c.innerHTML=`
LAST 30 DAYS

Insights, quietly.

WEATHER
${Object.entries(cnt).map(([k,v])=>`
${cap(k)} ${v} DAYS
`).join('')}
AVERAGE ENERGY
${avg} / 5
${a.slice(0,10).reverse().map(x=>``).join('')||'No entries yet'}
MOST ACTIVE DAY
${active}
CURRENT STREAK
${streak(a)} DAYS
Longest streak: ${longest(a)} days
`} function profileApp(c){let p=profile(),a=mems(),common=Object.entries(a.reduce((o,m)=>(o[m.mood]=(o[m.mood]||0)+1,o),{})).sort((x,y)=>y[1]-x[1])[0]?.[0]||'—',avg=a.length?(a.reduce((x,m)=>x+m.energy,0)/a.length).toFixed(1):'—',started=Math.max(1,Math.floor((Date.now()-new Date(p.startDate))/864e5));c.innerHTML=`
SYSTEM PROFILE

${p.name?esc(p.name)+' · ':''}LIFE OS ONLINE

${started}DAYS ONLINE
${a.length}MEMORIES
${museum().length}MUSEUM ITEMS
${streak(a)}CURRENT STREAK
${longest(a)}LONGEST STREAK
${cap(common)}COMMON WEATHER
PERSONAL SYSTEM
AVERAGE ENERGY ${avg} / 5
`}function saveProfile(){save('lifeos_profile',{name:pname.value,birthday:pbday.value,startDate:pstart.value||day(),motto:pmotto.value});renderAll();toast('PROFILE SAVED')} function today(c){let m=mems().find(x=>x.date===day());if(todayDraft===null){selectedMood=m?.mood||'cloudy';selectedEnergy=m?.energy||3}let f=m||filmFor(selectedMood,selectedEnergy),text=todayDraft===null?(m?.text||''):todayDraft,cWeek=new Date().toLocaleDateString(lang()==='zh'?'zh-CN':'en-US',{weekday:'long'}).toUpperCase();c.innerHTML=`
${cWeek}

${fmtDate(day()).replace(/^[^,]+, /,'')}

${i18n('How was your day?')}

`}function rerenderToday(){let old=document.querySelector('#entry');todayDraft=old?old.value:todayDraft||'';window.today(document.querySelector('#content-today'));let entry=document.querySelector('#entry');entry?.focus();entry?.setSelectionRange(entry.value.length,entry.value.length)} -function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS

System, local and yours.

APPEARANCE
${['dark','light','auto'].map(x=>``).join('')}
LANGUAGE
DATA
STORAGE

Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB

DATA STORAGE

Your data is stored locally in this browser.
Create regular backups to prevent data loss.

${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'
BACKUP RECOMMENDED':''}

VERSION

Life OS
Version ${window.LifeOSVersion||'1.5.8'} · Schema v2

`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:window.LifeOSVersion||'1.5.8',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||![1,2].includes(d.schemaVersion)||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=``}function replaceConfirm(){modal.innerHTML=``}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=``}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)} +function systemApp(c){let s=settings(),bytes=new Blob([JSON.stringify({mems:mems(),museum:museum(),profile:profile()})]).size;let rec=s.lastBackup&&(Date.now()-new Date(s.lastBackup)>12096e5);c.innerHTML=`
SYSTEM SETTINGS

System, local and yours.

APPEARANCE
${['dark','light','auto'].map(x=>``).join('')}
LANGUAGE
DATA
STORAGE

Local Storage
${mems().length} memories · ${museum().length} museum items · Approx. ${Math.ceil(bytes/1024)} KB

DATA STORAGE

Your data is stored locally in this browser.
Create regular backups to prevent data loss.

${s.lastBackup?'LAST BACKUP · '+fmtDate(s.lastBackup.slice(0,10)):'LAST BACKUP · NEVER'} ${rec?'
BACKUP RECOMMENDED':''}

VERSION

Life OS
Version ${window.LifeOSVersion||'1.6.0'} · Schema v2

`}function theme(x){let s=settings();s.theme=x;save('lifeos_settings',s);init();renderAll()}function exportData(){let data={app:'Life OS',version:window.LifeOSVersion||'1.6.0',schemaVersion:1,exportedAt:new Date().toISOString(),profile:profile(),memories:mems(),museum:museum(),settings:settings()};let a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'}));a.download='life-os-backup-'+day()+'.json';a.click();let s=settings();s.lastBackup=new Date().toISOString();save('lifeos_settings',s);renderAll();toast('BACKUP CREATED')}function readImport(el){let f=el.files[0];if(!f)return;let r=new FileReader;r.onload=()=>{try{let d=JSON.parse(r.result);if(d.app!=='Life OS'||![1,2].includes(d.schemaVersion)||!Array.isArray(d.memories)||!Array.isArray(d.museum))throw 0;importData=d;importPreview()}catch{toast('INVALID BACKUP FILE')}};r.readAsText(f)}function importPreview(){let d=importData;modal.innerHTML=``}function replaceConfirm(){modal.innerHTML=``}function applyImport(mode){let d=importData;if(mode==='replace'){save('lifeos_memories',d.memories);save('lifeos_museum',d.museum);save('lifeos_profile',d.profile||{});save('lifeos_settings',d.settings||{})}else{let a=mems(), ids=new Set(a.map(x=>x.id)),dates=new Set(a.map(x=>x.date));save('lifeos_memories',[...a,...d.memories.filter(x=>!ids.has(x.id)&&!dates.has(x.date))]);let mu=museum(),mi=new Set(mu.map(x=>x.id));save('lifeos_museum',[...mu,...d.museum.filter(x=>!mi.has(x.id))])}modal.innerHTML='';toast('RESTORING SYSTEM...');setTimeout(()=>{renderAll();toast('SYSTEM RESTORED')},500)}function resetConfirm(){modal.innerHTML=``}function toast(t){let x=document.createElement('div');x.className='toast';x.textContent=t;document.body.append(x);setTimeout(()=>x.remove(),2000)} window.renderIcons=renderIcons;window.renderAll=renderAll;window.toast=toast; - + diff --git a/public/locales/en.json b/public/locales/en.json index fffd6a1..985ceac 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -92,14 +92,31 @@ "collection": "COLLECTION" }, "insights": { - "eyebrow": "LAST 30 DAYS", - "title": "Insights, quietly.", - "weather": "WEATHER", - "average": "AVERAGE ENERGY", - "active": "MOST ACTIVE DAY", - "current": "CURRENT STREAK", - "longest": "Longest streak: {count} days", - "noEntries": "No entries yet" + "eyebrow": "PERIOD REPORTS", + "title": "Insights", + "description": "A factual view of what you recorded and completed.", + "period": "Report period", + "week": "WEEK", + "month": "MONTH", + "previous": "Previous period", + "next": "Next period", + "records": "RECORDING FREQUENCY", + "recordedDays": "{count} entries · {percent}% of elapsed days", + "tasks": "TASKS COMPLETED", + "completedTasks": "Tasks marked complete in this period", + "taskDetails": "{created} tasks created · {goals} goals completed", + "weather": "COMMON WEATHER", + "noWeather": "No weather recorded in this period", + "energy": "RECORDED ENERGY", + "energyFromTo": "First entry {first}/5 → latest {last}/5", + "singleEnergy": "One energy entry in this period", + "noEnergy": "No energy recorded in this period", + "highlights": "MOMENTS TO REVISIT", + "highlightSource": "Saved memories or entries tagged #highlight or #高光.", + "highlightFavorite": "SAVED", + "highlightTag": "TAGGED", + "highlightEmpty": "No saved or tagged memories in this period.", + "note": "Only saved records are counted. Missing dates carry no inferred weather or energy." }, "profile": { "eyebrow": "SYSTEM PROFILE", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index d1784aa..39ea0c8 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -92,14 +92,31 @@ "collection": "收藏" }, "insights": { - "eyebrow": "最近 30 天", - "title": "安静地看见时间。", - "weather": "天气", - "average": "平均能量", - "active": "最活跃日", - "current": "当前连续记录", - "longest": "最长连续记录:{count} 天", - "noEntries": "暂无记录" + "eyebrow": "周期回顾", + "title": "洞察", + "description": "回顾已记录和已完成的事实。", + "period": "报告周期", + "week": "周报", + "month": "月报", + "previous": "上一期", + "next": "下一期", + "records": "记录频率", + "recordedDays": "{count} 条记忆 · 已过天数的 {percent}%", + "tasks": "任务完成", + "completedTasks": "本期标记完成的任务", + "taskDetails": "新建任务 {created} · 完成目标 {goals}", + "weather": "常见天气", + "noWeather": "本期暂无天气记录", + "energy": "记录时的能量", + "energyFromTo": "首条 {first}/5 → 末条 {last}/5", + "singleEnergy": "本期只有一条能量记录", + "noEnergy": "本期暂无能量记录", + "highlights": "值得回看的片段", + "highlightSource": "来自本期收藏或标记 #高光、#highlight 的记忆。", + "highlightFavorite": "已收藏", + "highlightTag": "已标记", + "highlightEmpty": "本期暂无收藏或高光标签记忆。", + "note": "只统计已保存的记录;未记录的日期不推断天气或能量。" }, "profile": { "eyebrow": "系统档案", diff --git a/public/service-worker.js b/public/service-worker.js index a55aaad..4431559 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const CACHE = 'life-os-shell-v1.5.8'; +const CACHE = 'life-os-shell-v1.6.0'; const SHELL = [ '/', '/index.html', '/manifest.webmanifest', '/life-os-icon.svg' ]; diff --git a/scripts/build.mjs b/scripts/build.mjs index 1bff109..20acda2 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -8,7 +8,7 @@ const meta = new URL("../dist/.openai/", import.meta.url); await rm(dist, { recursive: true, force: true }); await Promise.all([mkdir(server, { recursive: true }), mkdir(meta, { recursive: true })]); const [rawIndexHtml, addonsJs, bootGuardJs, i18nJs, tasksJs, calendarJs, i18nRenderersJs, manifest, serviceWorker, icon, localeEn, localeZh] = await Promise.all([text("index.html"), text("addons.js"), text("boot-guard.js"), text("i18n.js"), text("tasks.js"), text("calendar.js"), text("i18n-renderers.js"), text("manifest.webmanifest"), text("service-worker.js"), text("life-os-icon.svg"), readFile(new URL("../public/locales/en.json", import.meta.url), "utf8"), readFile(new URL("../public/locales/zh-CN.json", import.meta.url), "utf8")]); -const release = "1.5.8"; +const release = "1.6.0"; const localeEnJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales.en=${localeEn};`; const localeZhJs = `window.LifeOSLocales=window.LifeOSLocales||{};window.LifeOSLocales['zh-CN']=${localeZh};`; let indexHtml = rawIndexHtml.replace('src="addons.js"', `src="addons.js?v=${release}"`).replace('src="tasks.js"', `src="tasks.js?v=${release}"`).replace('src="calendar.js"', `src="calendar.js?v=${release}"`); diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 87ab1f9..ca91d3b 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -115,14 +115,15 @@ assert.match(assets.i18nRenderersJs, /onclick=\"window\.setLanguage\('zh'\)\"/); assert.doesNotMatch(assets.addonsJs, /lifeos_language_preference_v1/, "the device locale preference stays outside cloud-synced keys"); const uiNodes = new Map(); const uiNode = () => ({ innerHTML: "", textContent: "", style: {}, dataset: {}, classList: { add() {}, remove() {}, toggle() {}, contains: () => false }, remove() {}, append() {}, querySelector: () => null, querySelectorAll: () => [] }); -for (const id of ["icons", "tasks", "launcher", "calendar", "desktop", "boot", "modal", "windows", "clock", "date", "desktop-brand", "life-launcher", "content-system"]) uiNodes.set(`#${id}`, uiNode()); +for (const id of ["icons", "tasks", "launcher", "calendar", "desktop", "boot", "modal", "windows", "clock", "date", "desktop-brand", "life-launcher", "content-system", "content-insights"]) uiNodes.set(`#${id}`, uiNode()); const uiDocument = { documentElement: { dataset: {}, lang: "en" }, title: "", head: { insertAdjacentHTML() {}, append() {} }, body: { append() {} }, querySelector: selector => uiNodes.get(selector) || null, getElementById: id => uiNodes.get(`#${id}`) || null, createElement: uiNode, addEventListener() {}, querySelectorAll: () => [] }; +const uiStorage = makeStorage(); const uiContext = vm.createContext({ - document: uiDocument, navigator: { languages: ["en-US"], language: "en-US" }, localStorage: makeStorage(), + document: uiDocument, navigator: { languages: ["en-US"], language: "en-US" }, localStorage: uiStorage, Blob, crypto: globalThis.crypto, innerWidth: 1000, innerHeight: 800, matchMedia: () => ({ matches: false }), setTimeout: () => 0, setInterval: () => 0, fetch: async () => ({ ok: false, json: async () => ({}) }), console, @@ -143,6 +144,40 @@ uiContext.navigator.languages = ["zh-CN"]; uiContext.setLanguage("auto"); assert.match(uiNodes.get("#content-system").innerHTML, /系统设置/); assert.equal(uiContext.LifeI18n.preference(), "auto"); +uiContext.setLanguage("en"); +const calendarKey = date => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; +const current = new Date(), previousWeek = new Date(current.getFullYear(), current.getMonth(), current.getDate() - 7, 12); +const currentKey = calendarKey(current), previousKey = calendarKey(previousWeek), currentStamp = new Date(current.getFullYear(), current.getMonth(), current.getDate(), 12).toISOString(); +uiStorage.setItem("lifeos_memories", JSON.stringify([ + { id: "current-highlight", date: currentKey, title: "Current highlight", text: "A real saved moment", mood: "clear", energy: 4, tags: ["高光"] }, + { id: "previous-highlight", date: previousKey, title: "Previous highlight", text: "An older moment", mood: "rain", energy: 2, tags: [] } +])); +uiStorage.setItem("lifeos_museum", JSON.stringify([{ id: "artifact-prev", memoryId: "previous-highlight" }])); +uiStorage.setItem("lifeos_system", JSON.stringify({ schemaVersion: 2, tasksBoard: { + tasks: [{ id: "done-now", done: true, createdAt: currentStamp, completedAt: currentStamp }, { id: "done-before", done: true, createdAt: previousWeek.toISOString(), completedAt: previousWeek.toISOString() }], + goals: [{ id: "goal-now", done: true, completedAt: currentStamp }] +} })); +uiContext.insights(uiNodes.get("#content-insights")); +let report = uiNodes.get("#content-insights").innerHTML; +assert.match(report, /Current highlight/); +assert.doesNotMatch(report, /Previous highlight/); +assert.match(report, /1 tasks created · 1 goals completed/); +assert.match(report, /First entry|One energy entry/); +uiContext.shiftInsights(-1); +report = uiNodes.get("#content-insights").innerHTML; +assert.match(report, /Previous highlight/); +assert.doesNotMatch(report, /Current highlight/); +assert.match(report, /SAVED/); +uiContext.setInsightsView("month"); +report = uiNodes.get("#content-insights").innerHTML; +assert.match(report, /Current highlight/); +assert.match(report, /MONTH/); +uiStorage.setItem("lifeos_memories", "[]"); +uiStorage.setItem("lifeos_museum", "[]"); +uiStorage.setItem("lifeos_system", JSON.stringify({ schemaVersion: 2, tasksBoard: { tasks: [], goals: [] } })); +uiContext.insights(uiNodes.get("#content-insights")); +assert.match(uiNodes.get("#content-insights").innerHTML, /No weather recorded in this period/); +assert.match(uiNodes.get("#content-insights").innerHTML, /No saved or tagged memories in this period/); const zhLogin = await worker.default.fetch(new Request("https://life-os.test/", { headers: { "accept-language": "zh-CN,zh;q=0.9" } }), {}); assert.match(await zhLogin.text(), /登录 ChatGPT/); const enLogin = await worker.default.fetch(new Request("https://life-os.test/", { headers: { "accept-language": "en-US,en;q=0.9" } }), {});