.breath-circle-container.hold { animation: holdPulse 1.6s ease-in-out infinite; }
#bridge-phase { transition: opacity .25s ease; opacity:1; }
.breath-circle-container.hold #bridge-ring { animation: glowPulse 1.6s ease-in-out infinite; }
@keyframes glowPulse { 0%,100% { filter:drop-shadow(0 0 6px var(--gold)); } 50% { filter:drop-shadow(0 0 22px var(--gold)); } }
#bridge-ring { transition:none; }
#bridge-ring.ring-inhale {
stroke:#5BC0EB;
filter:drop-shadow(0 0 12px #5BC0EB);
animation: inhaleFill 4s cubic-bezier(.25,.46,.45,.94) forwards, pulseInhale 2s ease-in-out infinite;
}
#bridge-ring.ring-hold {
stroke:var(--gold);
filter:drop-shadow(0 0 14px var(--gold));
animation: glowPulse 1.6s ease-in-out infinite;
stroke-dashoffset:0;
}
#bridge-ring.ring-exhale {
stroke:#56C596;
filter:drop-shadow(0 0 12px #56C596);
animation: exhaleEmpty 8s cubic-bezier(.25,.46,.45,.94) forwards, pulseExhale 2.5s ease-in-out infinite;
}
@keyframes inhaleFill { from { stroke-dashoffset:502; } to { stroke-dashoffset:0; } }
@keyframes exhaleEmpty { from { stroke-dashoffset:0; } to { stroke-dashoffset:502; } }
@keyframes pulseInhale { 0%,100% { filter:drop-shadow(0 0 8px #5BC0EB); } 50% { filter:drop-shadow(0 0 20px #5BC0EB); } }
@keyframes pulseExhale { 0%,100% { filter:drop-shadow(0 0 8px #56C596); } 50% { filter:drop-shadow(0 0 18px #56C596); } }
#bridge-overlay { transition: background .6s ease; }
#bridge-overlay .breath-circle-container { transition: transform .6s cubic-bezier(.22,1,.36,1); }
@keyframes holdPulse { 0%{transform:scale(1)} 50%{transform:scale(1.08)} 100%{transform:scale(1)} }
.breath-circle-svg { width:100%; height:100%; transform:rotate(-90deg); }
.breath-circle-ring { fill:none; stroke:#E5E5E5; stroke-width:8; } .breath-circle-active { fill:none; stroke:var(--gold); stroke-width:8; stroke-linecap:round; transition:stroke-dashoffset 1s ease; }
#breath-ring { transition:stroke-dashoffset .3s ease, stroke-color .3s ease, filter .3s ease; }
#breath-ring.ring-inhale { stroke:#5BC0EB; filter:drop-shadow(0 0 8px #5BC0EB); }
#breath-ring.ring-hold { stroke:var(--gold); filter:drop-shadow(0 0 12px var(--gold)); }
#breath-ring.ring-exhale { stroke:#56C596; filter:drop-shadow(0 0 8px #56C596); }
.breath-phase-text { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); font-size:24px; font-weight:600; color:var(--main); }
.breath-timer { font-size:24px; font-weight:700; color:var(--gold); margin-bottom:10px; }
.breath-btn { width:100%; height:44px; background:var(--active-bg); color:var(--active-text); border:none; border-radius:22px; font-size:15px; font-weight:600; cursor:pointer; } .breath-btn.stop { background:var(--red); }
.coach-card { background:var(--card); border-radius:16px; padding:16px; margin-top:16px; border:1px dashed var(--gold); animation:fadeIn .6s ease both; animation-delay:.3s; }
.coach-header { display:flex; align-items:center; gap:8px; margin-bottom:8px; font-size:13px; color:var(--gold); font-weight:600; }
.coach-q { font-family:'Noto Serif SC',serif; font-size:16px; color:var(--main); line-height:1.6; margin-bottom:12px; }
.coach-reply-box { display:flex; gap:8px; }
.coach-input { flex:1; padding:10px; border:1px solid var(--border); border-radius:12px; background:var(--input-bg); color:var(--main); outline:none; font-size:14px; }
.coach-send-btn { padding:0 16px; background:var(--gold); color:#1A1A1A; border-radius:12px; border:none; font-weight:600; cursor:pointer; }
/* V2.2: Config File Cloud UI */
.cloud-card { background:var(--card); border:1px solid var(--border); border-radius:16px; padding:16px; margin-bottom:16px; }
.cloud-card.synced { border-color:var(--green); }
.cloud-status-row { font-size:14px; font-weight:600; margin-bottom:8px; display:flex; align-items:center; gap:8px; }
.cloud-dot { width:8px; height:8px; border-radius:50%; background:#CCC; } .cloud-dot.ok { background:var(--green); box-shadow:0 0 8px var(--green); }
.cloud-actions { display:flex; gap:8px; margin-top:12px; }
.cloud-btn { flex:1; height:40px; border:none; border-radius:12px; font-size:13px; font-weight:600; cursor:pointer; position:relative; overflow:hidden; }
.cloud-btn.primary { background:var(--active-bg); color:var(--active-text); }
.cloud-btn.secondary { background:var(--input-bg); color:var(--main); }
.cloud-btn.loading { pointer-events:none; opacity:.75; }
.cloud-btn.loading::after { content:""; position:absolute; inset:0; background:rgba(0,0,0,.08); border-radius:inherit; }
.cloud-btn.loading::before { content:""; position:absolute; width:18px; height:18px; top:50%; left:50%; margin:-9px 0 0 -9px; border:2px solid currentColor; border-right-color:transparent; border-radius:50%; animation:spin .7s linear infinite; z-index:1; }
@keyframes spin { to { transform: rotate(360deg); } }
/* V2.0 Insights */
.insight-section { margin-top:16px; margin-bottom:16px; }
.tag-cloud { display:flex; flex-wrap:wrap; gap:6px; }
.tag-item { padding:4px 8px; background:var(--input-bg); border-radius:8px; font-size:12px; color:var(--sub); }
.tag-item.hot { background:var(--gold); color:#1A1A1A; }
.pattern-item { padding:12px; background:var(--input-bg); border-radius:12px; margin-bottom:8px; font-size:13px; }
.resonance-card { background:var(--card); border-radius:20px; padding:20px; margin-bottom:16px; }
.resonance-text { font-family:'Noto Serif SC',serif; font-size:16px; color:var(--gold); line-height:1.6; margin-bottom:12px; }
.resonance-meta { display:flex; justify-content:space-between; align-items:center; }
.resonance-btn { display:flex; align-items:center; gap:4px; padding:4px 10px; background:transparent; border:1px solid var(--border); border-radius:14px; color:var(--sub); cursor:pointer; font-size:11px; } .resonance-btn.active { background:rgba(201,169,110,.15); border-color:var(--gold); color:var(--gold); }
.resonance-count { font-size:14px; font-weight:600; }
.resonance-btn.active { box-shadow:0 0 0 3px var(--gold), 0 0 16px rgba(201,169,110,.35); transition:box-shadow .5s; }
/* V2.4 — 播种与觉察 */
.seed-section { margin-top:20px; padding:20px; background:linear-gradient(135deg,#F7F5F1,#F0EFEB); border:1px solid #E8E3D8; border-radius:16px; animation:seedFadeIn .6s cubic-bezier(.22,1,.36,1) both; }
@keyframes seedFadeIn { from{opacity:0;transform:translateY(12px)} to{opacity:1;transform:translateY(0)} }
.seed-heading { font-size:15px; font-weight:600; color:#5A4A3A; margin-bottom:4px; }
.seed-sub { font-size:12px; color:#A89880; margin-bottom:14px; line-height:1.6; }
.seed-input-wrapper { display:flex; gap:8px; }
.seed-input { flex:1; padding:12px 14px; background:#FFF; border:1px solid #E0D5C8; border-radius:12px; font-size:15px; outline:none; transition:border-color .2s; }
.seed-input:focus { border-color:var(--gold); box-shadow:0 0 0 3px rgba(201,169,110,.15); }
.seed-btn { padding:12px 20px; background:var(--gold); color:#1A1A1A; border:none; border-radius:12px; font-size:14px; font-weight:600; cursor:pointer; transition:transform .15s; } .seed-btn:active { transform:scale(.95); }
.seed-btn.done { background:#56C596; }
.seed-hint { display:inline-block; margin-top:10px; font-size:12px; color:#B0A090; cursor:pointer; text-decoration:underline; text-decoration-style:dotted; text-underline-offset:3px; }
.seed-hint:active { color:var(--gold); }
.seed-old-pattern { margin-top:12px; padding:10px 14px; background:rgba(201,169,110,.08); border-left:3px solid var(--gold); border-radius:8px; font-size:13px; color:#5A4A3A; line-height:1.5; animation:fadeIn .4s ease both; }
.seed-result { margin-top:12px; display:flex; align-items:center; gap:8px; padding:10px 14px; background:rgba(86,197,150,.1); border-radius:10px; font-size:14px; color:#2A6B4A; animation:fadeIn .4s ease both; }
.seed-result-icon { font-size:18px; }
/* 种子花园视图 */
.garden-tabs { display:flex; gap:0; margin-bottom:16px; background:var(--input-bg); border-radius:12px; padding:3px; }
.garden-tab { flex:1; text-align:center; padding:8px; font-size:13px; color:var(--sub); cursor:pointer; border-radius:10px; transition:all .2s; }
.garden-tab.active { background:var(--card); color:var(--main); font-weight:600; box-shadow:0 2px 8px rgba(0,0,0,.06); }
.garden-item { display:flex; gap:12px; align-items:flex-start; padding:14px; background:var(--card); border-radius:16px; margin-bottom:10px; box-shadow:0 2px 8px rgba(0,0,0,.02); }
.garden-icon { font-size:24px; flex-shrink:0; margin-top:2px; }
.garden-content { flex:1; min-width:0; }
.garden-seed { font-size:14px; font-weight:600; color:var(--main); margin-bottom:2px; }
.garden-meta { font-size:11px; color:var(--sub); }
.garden-old { font-size:12px; color:#B0A090; margin-top:4px; padding:4px 8px; background:var(--input-bg); border-radius:6px; display:inline-block; }
.garden-sub { display:flex; gap:6px; margin:0 0 12px 0; justify-content:center; }
.garden-sub-btn { padding:4px 14px; font-size:12px; color:var(--sub); background:transparent; border:1px solid var(--border); border-radius:20px; cursor:pointer; transition:all .2s; }
.garden-sub-btn.active { background:var(--gold); color:#1A1A1A; border-color:var(--gold); }
.garden-level { display:inline-flex; align-items:center; justify-content:center; width:32px; height:32px; font-size:20px; }
.garden-stat { text-align:center; padding:12px; margin-bottom:12px; background:rgba(201,169,110,.06); border-radius:12px; font-size:12px; color:var(--sub); line-height:1.6; }
.water-btn { display:inline-flex; align-items:center; gap:3px; padding:4px 10px; background:transparent; border:1px solid var(--border); border-radius:14px; color:var(--sub); cursor:pointer; font-size:11px; margin-left:6px; } .water-btn.active { background:rgba(86,197,150,.12); border-color:#56C596; color:#56C596; }
🌱 为今日播下一颗种子
今天的洞察是一面镜子。你想为明天种下一颗怎样的新种子?
种下 →
先看看想替换的旧种子?
4-7-8 放松
🧘 觉知呼吸
4-7-8
吸气 4 秒 → 憋气 7 秒 → 呼气 8 秒
开始练习
⚙️ 设置与数据
📤 导出 📥 导入
🗑️ 清空数据
☁️ 数据已连接云端
📥 云端恢复 ☁️ 立即同步
退出云端
版本 V2.4.1
✅ 已记录
关闭
长按图片保存到相册
window.DASHSCOPE_KEY='sk-sp-9f51b4617ae94a5781b7027935dfa78c';
window.App = {
quotes: [{text:"你未曾见过的那一面,才是你。",author:"荣格"},{text:"未经审视的人生是不值得过的。",author:"苏格拉底"},{text:"真正的发现之旅,不在于寻找新风景,而在于拥有新的眼睛。",author:"普鲁斯特"},{text:"知人者智,自知者明。",author:"老子"},{text:"所谓无底深渊,下去,也是前程万里。",author:"木心"},{text:"我们都在阴沟里,但仍有人仰望星空。",author:"王尔德"}],
themes: {
general:{q1:"今天最重要的事是什么?",p1:"一桩事,足矣。",q2:"这件事,说明我是一个怎样的人?",p2:"一个词或一句话。"},
work:{q1:"今天在工作中,我最在意的是什么?",p1:"一个项目、一次沟通...",q2:"这种在意,说明我对工作的态度是?",p2:"责任、焦虑、成就..."},
relation:{q1:"今天在和谁的互动中,我最有情绪?",p1:"家人、伴侣、同事...",q2:"这种情绪背后,是我怎样的需求?",p2:"被理解、被尊重、控制..."},
body:{q1:"今天我的身体在对我说什么?",p1:"疲惫、紧绷、轻松...",q2:"我听到它的声音了吗?我回应了吗?",p2:"忽视、照顾、接纳..."}
},
currentTheme:'general', lastItem:null, reminderInterval:null, recognition:null, isRecording:false, targetFieldId:null, resonanceData:[], breathMode:'478',
voiceGuideEnabled: false,
startVoiceGuide: function() {
if (!('speechSynthesis' in window)) return;
this.voiceGuideEnabled = true;
this.speak('吸气...');
},
speak: function(text) {
if (!this.voiceGuideEnabled) return;
if (!window.speechSynthesis) return;
var u = new SpeechSynthesisUtterance(text);
u.lang = 'zh-CN';
u.rate = 0.9;
speechSynthesis.speak(u);
},
stopVoiceGuide: function() {
this.voiceGuideEnabled = false;
if ('speechSynthesis' in window) speechSynthesis.cancel();
},
breathInterval:null, isBreathing:false, isMeditating:false, medTime:600, medInterval:null,
audioCtx:null, whiteNoiseNode:null, isSoundOn:false, noiseGain:null,
cloudWorkerUrl: '',
cloudUsername: '', // V2.3: Username as KV Key
bindEvents: function() {
var tabs = document.querySelectorAll('.tab');
tabs.forEach(function(tab) {
tab.addEventListener('click', function() {
tabs.forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
var idx = tab.getAttribute('data-idx');
document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); });
var target = document.querySelector('.page[data-idx="' + idx + '"]');
if (target) target.classList.add('active');
});
});
var remBtn = document.getElementById('btn-toggle-reminder');
if (remBtn) {
remBtn.addEventListener('click', function() {
if (window.App && window.App.toggleReminder) window.App.toggleReminder();
});
}
var clearBtn = document.getElementById('btn-clear-data');
if (clearBtn) {
clearBtn.addEventListener('click', function() {
if (window.App && window.App.store && window.App.store.clear) window.App.store.clear();
});
}
var soundBtn = document.getElementById('sound-toggle');
if (soundBtn) {
soundBtn.addEventListener('click', function() {
if (window.App && window.App.toggleSound) App.toggleSound();
});
}
var darkBtn = document.getElementById('dark-mode-toggle');
if (darkBtn) {
darkBtn.addEventListener('click', function() {
if (window.App && window.App.initDarkMode) App.initDarkMode();
});
}
var usernameInput = document.getElementById('cloud-username');
if (usernameInput) {
usernameInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && window.App && window.App.saveUsername) App.saveUsername();
});
}
},
init: async function() {
this.bindEvents();
// V2.3: Use Worker URL from inline meta tag (injected by Pages at deploy time)
this.cloudWorkerUrl = (document.querySelector('meta[name="whoami-worker-url"]')?.content || '').replace(/\/$/, '');
// V2.3: Load username from localStorage (never auto-generate)
this.cloudUsername = localStorage.getItem('whoami_username') || '';
this.renderCloudUI();
this.store.init();
this.setDailyQuote(); this.initDarkMode(); this.initSwipe(); this.initReminder(); this.initVoice(); this.loadResonance(); this.updateStreak();
this.renderHistory(); this.renderProfile();
if (this.cloudWorkerUrl && this.cloudUsername) { this._pullCloudRaw().then(d=>{if(d&&d.entries&&d.entries.length){this.store.save(d.entries);if(d.streak){localStorage.setItem('whoami_streak',JSON.stringify(d.streak));}this.renderProfile();this.updateStreak();}else this.pushCloud(true);}); }
},
showToast: function(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 3000);
},
fetchWithTimeout: function(url, opts, ms) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), ms || 15000);
return fetch(url, Object.assign({}, opts, { signal: ctrl.signal }))
.finally(() => clearTimeout(timer));
},
showPrompt: function(msg) {
return new Promise(resolve => {
const el = document.getElementById('prompt-overlay');
document.getElementById('prompt-text').textContent = msg;
el.classList.add('active');
const input = document.getElementById('prompt-input');
input.value = '';
setTimeout(() => input.focus(), 50);
const done = (v) => {
el.classList.remove('active');
resolve(v);
};
document.getElementById('prompt-ok').onclick = () => done(input.value);
document.getElementById('prompt-cancel').onclick = () => done(null);
input.onkeydown = (e) => {
if (e.key === 'Enter') done(input.value);
if (e.key === 'Escape') done(null);
};
});
},
vibrate: function(ms) {
if (navigator.vibrate) {
navigator.vibrate(ms || 10);
}
},
// V2.3: Username as Key - strict validation
_validUsername: function(u) { return /^[a-zA-Z0-9_-]{3,32}$/.test(u); },
// V2.3: Username-based Cloud UI
renderCloudUI: function() {
const card = document.getElementById('cloud-card');
const dot = document.getElementById('cloud-dot');
const text = document.getElementById('cloud-text');
const connected = document.getElementById('cloud-connected');
const disconnected = document.getElementById('cloud-disconnected');
const loginPanel = document.getElementById('cloud-login');
const usernameInput = document.getElementById('cloud-username');
const usernameStatus = document.getElementById('username-status');
if (this.cloudWorkerUrl && this.cloudUsername) {
// Connected state
card.classList.add('synced');
dot.classList.add('ok');
text.textContent = '已连接云端 (' + this.cloudUsername + ')';
connected.style.display = 'block';
disconnected.style.display = 'none';
loginPanel.style.display = 'none';
} else if (this.cloudWorkerUrl) {
// Worker URL exists but no username set - show login panel
card.classList.remove('synced');
dot.classList.remove('ok');
text.textContent = '等待登录';
connected.style.display = 'none';
disconnected.style.display = 'none';
loginPanel.style.display = 'block';
if (usernameInput) usernameInput.value = this.cloudUsername || '';
} else {
// No Worker URL configured
card.classList.remove('synced');
dot.classList.remove('ok');
text.textContent = '本地模式 (未配置 config.json)';
connected.style.display = 'none';
disconnected.style.display = 'block';
loginPanel.style.display = 'none';
}
},
saveUsername: async function() {
const input = document.getElementById('cloud-username');
const status = document.getElementById('username-status');
const username = input.value.trim();
if (!this._validUsername(username)) {
status.textContent = '⚠️ 仅允许 3-32 位字母、数字、下划线或连字符';
status.style.color = 'var(--red)';
return;
}
this.cloudUsername = username;
localStorage.setItem('whoami_username', username);
status.textContent = '✅ 用户名已保存';
status.style.color = 'var(--green)';
this.renderCloudUI();
// 先尝试从云端恢复
const cloudData = await this._pullCloudRaw();
if (cloudData && cloudData.entries && cloudData.entries.length > 0) {
await this.store.save(cloudData.entries);
if (cloudData.streak) localStorage.setItem('whoami_streak', JSON.stringify(cloudData.streak));
this.renderProfile();
this.renderHistory();
this.updateStreak();
} else {
// 云端空 → 把本地数据推上去
await this.pushCloud(true);
}
},
logoutCloud: async function() {
if(!(await App.showConfirm('确定退出云端吗?(本地数据保留)'))) return;
this.cloudUsername = '';
localStorage.removeItem('whoami_username');
this.renderCloudUI();
this.showToast('🔌 已退出云端');
},
_pullCloudRaw: async function() {
if (!this.cloudWorkerUrl || !this.cloudUsername) return null;
try {
const res = await this.fetchWithTimeout(`${this.cloudWorkerUrl}/sync?k=${encodeURIComponent(this.cloudUsername)}`, {}, 10000);
if (!res || !res.ok) return null;
const text = await res.text();
if (!text) return null;
const data = JSON.parse(text);
// Support both old format (array) and new format ({entries, streak})
if (Array.isArray(data)) return {entries: data, streak: null};
if (data && data.entries) return data;
return null;
} catch { return null; }
},
pushCloud: function(silent) {
if (!this.cloudWorkerUrl) { if(!silent) this.showToast('❌ 云端未配置,无法同步'); return; }
if (!this.cloudUsername) { if(!silent) this.showToast('⚠️ 请先在云端登录'); return; }
const btn = document.getElementById('btn-push');
if (btn) btn.classList.add('loading');
if (!silent) this.showToast('☁️ 正在同步...');
this.store.get().then(data => {
const payload = {entries: Array.isArray(data) ? data : [], streak: JSON.parse(localStorage.getItem('whoami_streak')||'{"count":0,"lastDate":""}')};
return this.fetchWithTimeout(`${this.cloudWorkerUrl}/sync?k=${encodeURIComponent(this.cloudUsername)}`, {
method:'POST', headers:{'content-type':'application/json'}, body:JSON.stringify(payload)
}, 15000);
}).then(res => {
if (res && res.ok && !silent) this.showToast('✅ 同步成功');
else if (res && !res.ok) this.showToast('❌ 同步失败: HTTP ' + res.status);
}).catch(e => {
const err = typeof e === 'string' ? e : (e && e.message) ? e.message : JSON.stringify(e);
if(!silent) this.showToast('❌ 同步失败: ' + err);
}).finally(() => { if (btn) btn.classList.remove('loading'); });
},
pullCloud: function() {
if (!this.cloudWorkerUrl) { this.showToast('❌ 云端未配置,无法恢复'); return; }
if (!this.cloudUsername) { this.showToast('⚠️ 请先在云端登录'); return; }
const btn = document.getElementById('btn-pull');
if (btn) btn.classList.add('loading');
this.showToast('📥 正在从云端恢复...');
this.fetchWithTimeout(`${this.cloudWorkerUrl}/sync?k=${encodeURIComponent(this.cloudUsername)}`, {}, 15000)
.then(res => {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text().then(t => { try { return JSON.parse(t); } catch { return []; } });
})
.then(data => {
// Support both old format (array) and new format ({entries, streak})
const entries = Array.isArray(data) ? data : (data && data.entries ? data.entries : null);
if (entries && entries.length > 0) {
return this.store.save(entries).then(() => {
if (data && data.streak) localStorage.setItem('whoami_streak', JSON.stringify(data.streak));
return {count: entries.length};
});
}
this.showToast('☁️ 云端暂无数据');
return null;
}).then(out => {
if (out && out.count) {
this.showToast(`✅ 已恢复 ${out.count} 条记录`);
this.renderProfile();
this.renderHistory();
this.updateStreak();
}
})
.catch(e => { const err = typeof e === 'string' ? e : (e && e.message) ? e.message : JSON.stringify(e); this.showToast('❌ 恢复失败: ' + err); })
.finally(() => { if (btn) btn.classList.remove('loading'); });
},
setDailyQuote: function() { const d=Math.floor((new Date()-new Date(new Date().getFullYear(),0,0))/864e5), q=this.quotes[d%this.quotes.length]; document.getElementById('quote-text').textContent=q.text; document.getElementById('quote-author').textContent='—— '+q.author; },
initDarkMode: function() {
const isDark = localStorage.getItem('whoami_theme') === 'dark';
const btn = document.getElementById('dark-mode-toggle');
if (isDark) {
document.body.classList.add('dark');
btn.textContent = '☀️';
} else {
document.body.classList.remove('dark');
btn.textContent = '🌙';
}
btn.onclick = () => {
const d = document.body.classList.toggle('dark');
localStorage.setItem('whoami_theme', d ? 'dark' : 'light');
btn.textContent = d ? '☀️' : '🌙';
};
},
initSwipe: function() { const slider=document.getElementById('slider'), tabs=document.querySelectorAll('.tab'); let sx=0,sy=0; slider.addEventListener('touchstart',e=>{sx=e.touches[0].clientX;sy=e.touches[0].clientY;},{passive:true}); slider.addEventListener('touchend',e=>{const dx=sx-e.changedTouches[0].clientX,dy=sy-e.changedTouches[0].clientY;if(Math.abs(dx)>30&&Math.abs(dx)>Math.abs(dy)){let n=Math.round(slider.scrollLeft/window.innerWidth)+(dx>0?1:-1);if(n>=0&&n<=4){slider.scrollTo({left:n*window.innerWidth,behavior:'smooth'});this.updateTabs(n);}}},{passive:true}); slider.addEventListener('scroll',()=>{const i=Math.round(slider.scrollLeft/window.innerWidth);if(!tabs[i].classList.contains('active'))this.updateTabs(i);},{passive:true}); tabs.forEach(t=>t.addEventListener('click',()=>{const idx=parseInt(t.dataset.idx);slider.scrollTo({left:idx*window.innerWidth,behavior:'smooth'});this.updateTabs(idx);})); },
updateTabs: function(idx) { const tabs=document.querySelectorAll('.tab'); tabs.forEach(t=>t.classList.remove('active'));tabs[idx].classList.add('active'); document.activeElement&&document.activeElement.blur(); if(idx===1){this.renderHistory();this.updateStreak();this.bindSelectAll();} if(idx===2)this.renderResonance(); if(idx===3)this.renderBreathStats(); if(idx===4)this.renderProfile(); },
wizard: {
_busy: false,
showStep: function(id, anim) { document.querySelectorAll('.wizard-step').forEach(el=>{el.classList.remove('active');el.style.display='none';}); const el=document.getElementById(id); el.style.display='block'; void el.offsetWidth; if(anim!==false)el.classList.add('active'); },
nextStep: function(n) {
if(this._busy) return;
if(n===2&&!document.getElementById('q1').value.trim()){App.showToast('🤔 先想一下...');return;}
if(n===3&&!document.getElementById('q2').value.trim()){App.showToast('🤔 先想一下...');return;}
this._busy = true;
this.showStep('step-'+n);
setTimeout(() => { this._busy = false; }, 400);
document.activeElement && document.activeElement.blur();
},
prevStep: function(n) { this.showStep('step-'+n,false); document.activeElement && document.activeElement.blur(); }
},
selectTheme: function(theme) { this.vibrate(15); this.currentTheme=theme; const t=this.themes[theme]; document.getElementById('q1-label').textContent='1. '+t.q1; document.getElementById('q1').placeholder=t.p1; document.getElementById('q2-label').textContent='2. '+t.q2; document.getElementById('q2').placeholder=t.p2; document.querySelectorAll('.theme-btn').forEach(b=>b.classList.remove('active')); document.querySelector(`.theme-btn[onclick="App.selectTheme('${theme}')"]`).classList.add('active'); },
switchBreathMode: function(mode) {
this.breathMode = mode;
document.getElementById('mode-478').classList.toggle('active', mode === '478');
document.getElementById('mode-awareness').classList.toggle('active', mode === 'awareness');
const timer = document.getElementById('breath-timer');
const desc = document.getElementById('breath-desc');
if (mode === 'awareness') {
if (timer && !this['_breathe']) { timer.textContent = '4-4-6'; }
if (desc) desc.textContent = '吸气 4 秒 → 憋气 4 秒 → 呼气 6 秒';
} else {
if (timer && !this['_breathe']) { timer.textContent = '4-7-8'; }
if (desc) desc.textContent = '吸气 4 秒 → 憋气 7 秒 → 呼气 8 秒';
}
},
selectQ4: function(val) {
document.getElementById('step4-btns').style.display = 'flex';
},
startBridgeBreath: function() {
this._bridgeActive = true;
const ring = document.getElementById('bridge-ring');
const phase = document.getElementById('bridge-phase');
const timer = document.getElementById('bridge-timer');
const ct = document.querySelector('#bridge-ring')?.closest('.breath-circle-container');
if (!ring || !phase || !timer) return;
const showPhase = (text, color, cls, ringCls) => {
phase.style.opacity = '0';
setTimeout(() => {
phase.textContent = text;
phase.style.color = color;
phase.style.opacity = '1';
}, 120);
timer.style.color = color;
if (ct) { if (cls) ct.classList.add(cls); else ct.classList.remove('hold'); }
ring.classList.remove('ring-inhale','ring-hold','ring-exhale');
if (ringCls) ring.classList.add(ringCls);
};
// Inhale 4s — ring fills, ease-in-out
showPhase('吸气...', '#5BC0EB', null, 'ring-inhale');
let n = 4; timer.textContent = n;
this._breathIntervals = this._breathIntervals || [];
const i1 = setInterval(() => { n--; if (n <= 0) clearInterval(i1); timer.textContent = n; }, 1000);
this._breathIntervals.push(i1);
// Hold 7s — pulse glow
setTimeout(() => {
clearInterval(i1); if (!this._bridgeActive) return;
showPhase('屏息...', 'var(--gold)', 'hold', 'ring-hold');
n = 7; timer.textContent = n;
const i2 = setInterval(() => { n--; if (n <= 0) clearInterval(i2); timer.textContent = n; }, 1000);
// Exhale 8s — ring empties, smooth
setTimeout(() => {
clearInterval(i2); if (!this._bridgeActive) return;
showPhase('呼气...', '#56C596', null, 'ring-exhale');
n = 8; timer.textContent = n;
const i3 = setInterval(() => { n--; if (n <= 0) clearInterval(i3); timer.textContent = n; }, 1000);
setTimeout(() => {
clearInterval(i3);
this._bridgeActive = false;
document.getElementById('bridge-overlay').style.display = 'none';
this.wizard.showStep('step-4');
}, 8000);
}, 7000);
}, 4000);
},
enterBreathing: function() { if(!document.querySelector('input[name=q3]:checked')){App.showToast('🤔 选一下吧...');return;} document.getElementById('bridge-overlay').style.display='flex'; App.startBridgeBreath(); },
submitAll: async function() {
if(!document.querySelector('input[name=q4]:checked')){App.showToast('🤔 选一下吧...');return;}
const q4=document.querySelector('input[name=q4]:checked').value;
const item={id:Date.now(),date:new Date().toISOString(),theme:this.currentTheme,q1:document.getElementById('q1').value.trim(),q2:document.getElementById('q2').value.trim(),q3:document.querySelector('input[name=q3]:checked').value,q4:q4,new_seed:'',old_pattern:''};
try{await this.store.add(item);}catch(e){this.showToast('⚠️ 存储失败');return;}
this.lastItem=item; this.checkInStreak(); this.showToast('✅ 已照见此刻'); this.generateInsight(item); this.wizard.showStep('step-result');
if(this.cloudWorkerUrl && this.cloudUsername) this.pushCloud(true);
// V2.4 展示播种
this._resetSeedUI();
setTimeout(()=>{ const ss=document.getElementById('seed-section'); if(ss) ss.style.display='block'; }, 600);
},
generateInsight: function(item) {
const t1=item.q1, t2=item.q2, c=(t1+' '+t2).toLowerCase();
const neg=/焦虑|愤怒|痛苦|害怕|恐惧|恨|烦|累|疲惫|压力|紧张|担心|不安|难过|失落|孤独|寂寞|迷茫|困惑/.test(c);
const pos=/感恩|感谢|幸福|开心|喜悦|满足|爱|温暖|期待|希望|轻松/.test(c);
let lines=[];
// Layer 1: Q4 awareness state
if(item.q4==='觉知') lines.push('你带着觉知在经历这一切。看着念头升起又落下,不评判,只看着。这就是「参」。');
else if(item.q4==='感受') lines.push('感受在告诉你什么?情绪不是敌人,它是一封信,写着你现在真正在意的。');
else lines.push('你在思考这件事。头脑在运转是正常的——只要知道那是头脑,而不是你,就够了。');
// Layer 2: Theme context
if(item.theme==='work') { if(neg) lines.push('工作中的压力,往往来自我们赋予事情的意义超过了事情本身。'); else lines.push('在工作中觉察自己,是最高效的修行。'); }
else if(item.theme==='relation') lines.push('关系的镜子最明亮——你在对方身上看到的,往往是你自己的一部分。');
else if(item.theme==='body') lines.push('身体从不说谎。它一直在替你的心说话。');
// Layer 3: Keyword matching - richer dictionary
const kw={
'焦虑':'焦虑背后,总是藏着一个未被满足的期待。找到它,就找到了出口。',
'担心':'你担心的事,99%不会发生。那1%,你也有能力面对。',
'紧张':'紧张的背后是你在乎。在乎不是弱点,是你在意。',
'愤怒':'愤怒是一层外衣,里面包着的通常是受伤或无力感。',
'害怕':'害怕的不是事情本身,而是你对这件事的解读。',
'压力':'压力不是事情给的,是你对事情的期待给的。',
'累':'疲惫是身体在敲门,告诉你该停下来听听自己了。',
'忙':'忙是最大的借口——忙到你连自己是谁都忘了问。',
'工作':'工作只是你做的事,不是你这个人。别忘了。',
'孩子':'孩子是你的一面镜子——你希望他成为的,往往是你还没成为的。',
'家人':'最亲密的关系最容不下伪装。家人面前摘下面具,就是修行。',
'伴侣':'你与伴侣的关系,就是你与世界的关系的缩影。',
'感恩':'能看见美好,本身就是一种美好。',
'幸福':'幸福不是拥有更多,而是对已有的感到满足。',
'迷茫':'迷茫不是迷路,是你正在蜕变的信号。',
'信心':'信心不是等来的,是走一步长一点的。',
'放弃':'想放弃的时候,恰恰是离突破最近的时候。'
};
const found=Object.keys(kw).find(k=>c.includes(k));
if(found) lines.push(kw[found]);
// Layer 4: Sentiment + consistency combo
if(neg && item.q3==='矛盾') lines.push('你一边抗拒它,一边认同它。看见这个矛盾,就是松动的开始。');
else if(neg && item.q3==='一致') lines.push('你清楚地知道这让你不舒服,但你没有逃避——这本身就是勇气。');
else if(pos && item.q3==='一致') lines.push('身心一致的状态,是最好的状态。记住这种感觉。');
else if(pos && item.q3==='矛盾') lines.push('即使感到美好,内心仍有矛盾——允许这种张力存在,它也是你的一部分。');
// Layer 5: Q1+Q2 relationship
const matchQ2=t2.match(/我是(.+)的人/);
if(matchQ2) {
const identity=matchQ2[1];
if(item.q3==='一致') lines.push('你认同「'+identity+'」这个描述,说明你对自己有清晰的认知。');
else lines.push('你说自己是'+identity+',但又不完全认同——这个缝隙里,藏着成长的空间。');
}
// Layer 6: q2 length/type analysis
if(t2.length<=3 && !neg && !pos) lines.push('一个词的回答往往藏着一整片海——愿意的话,下次可以多写一点。');
// Deduplicate and assemble
const unique=[...new Set(lines)];
const insight=unique.join('\n\n');
document.getElementById('insight-text').textContent=insight; this.generateCoachQuestion(item);
},
coachQuestions:{
'焦虑':'你写到了焦虑。如果这个焦虑是身体里的一个位置,它在哪?它在说什么?',
'担心':'你写到了担心。最坏的情况如果真的发生了——那个处境里的你,会怎么面对?',
'愤怒':'你写到了愤怒。愤怒指向的那个人或那件事,真正触动你的是什么?是失望、伤害,还是无能为力?',
'累':'你写到了累。你一直在满足谁的期待?如果停下来,你会让谁失望?',
'压力':'你写到了压力。这压力是事情本身的重量,还是你对自己设下的标准?',
'迷茫':'你写到了迷茫。你在寻找一个答案,还是害怕面对答案本身?',
'工作':'你写到了工作。如果这些事明天起没人知道是你做的——你还会做吗?',
'责任':'你写到了责任。责任是你选择的,还是你觉得自己「应该」承担的?',
'家人':'你写到了家人。家人最让你放不下的是什么——是爱,还是某种「应该」?',
'孩子':'你写到了孩子。你希望孩子成为什么样的人——还是希望他成为你没成为的人?',
'伴侣':'你写到了关系。你在关系中给出去的和想要得到的,哪个更难开口?',
'怕':'你写到了怕。如果你不怕了,你会怎么做?那个答案里的你和现在有什么不同?',
'恨':'你写到了恨。恨的对面是什么?你敢看它一眼吗?',
'放下':'你写到了放下。放不下,是这件事太重,还是你抓着它的手不想松开?',
'意义':'你写到了意义。你做的事和你这个人之间,隔着多远?',
'时间':'你写到了时间。你是没有时间,还是没有为自己留时间?',
'自己':'你写到了「自己」。你说「自己」的时候,指的是哪个你?',
'角色':'你写到了角色。这个角色如果卸下,剩下的那个你,还认识自己吗?',
'安全感':'你写到了安全感。安全感从哪里来——从外面得到的东西,还是从里面安放的能力?'
},
_coachRound: 0,
generateCoachQuestion: async function(item) {
this._coachRound = 0;
let question = '';
if (window.DASHSCOPE_KEY && window.DASHSCOPE_KEY.length > 10) {
try {
let ac = new AbortController();
setTimeout(() => ac.abort(), 6000);
const p = '你是觉察教练。用一句话问一个开放式的参究问题(不超过25字,引用户原文,不评判不建议)';
const c = '用户今天写了:\nQ1:' + item.q1 + '\nQ2:' + item.q2 + '\nQ3:' + item.q3 + '\nQ4:' + item.q4;
const b = JSON.stringify({ model: 'qwen-turbo', messages: [{ role: 'system', content: p }, { role: 'user', content: c }] });
const res = await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', { signal: ac.signal, method: 'POST', headers: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + window.DASHSCOPE_KEY }, body: b });
if (res && res.ok) { const d = await res.json(); if (d && d.choices && d.choices[0] && d.choices[0].message) question = d.choices[0].message.content.trim(); }
} catch (e) { }
}
if (!question) {
const combined = (item.q1 + " " + item.q2).toLowerCase();
for (const [key, q] of Object.entries(this.coachQuestions)) { if (combined.includes(key)) { question = q; break; } }
if (!question) { const defs = ['你刚才写的那句话里,哪个词最重?看着它,不要解释。', '如果刚才的回答不是用头脑,而是用心来回答——答案会变吗?', '你写下的这些东西,如果只能说一句给明天的自己,你会说什么?']; question = defs[Math.floor(Math.random() * defs.length)]; }
}
document.getElementById('coach-q').textContent = question; document.getElementById('coach-card').style.display = 'block';
},
replyCoach: async function() {
const input=document.getElementById('coach-input'), reply=input.value.trim(); if(!reply)return; this.vibrate(10);
const btn=document.querySelector('.coach-send-btn');if(btn){btn.style.transform='scale(.92)';btn.style.transition='transform .12s';setTimeout(()=>{btn.textContent='✓';btn.style.background='var(--green)';btn.style.color='#FFF';btn.style.transform='';setTimeout(()=>{btn.textContent='发送';btn.style.background='';btn.style.color='';},400);},120);}
input.style.transition='opacity .15s, transform .15s';input.style.opacity='0';input.style.transform='translateX(10px)';setTimeout(()=>{input.value='';input.style.opacity='1';input.style.transform='';},150);
const followUp={q:document.getElementById('coach-q').textContent,a:reply};
if(this.lastItem){if(!this.lastItem.coachReply)this.lastItem.coachReply=[]; this.lastItem.coachReply.push(followUp); const store=await this.store.get(); const idx=store.findIndex(i=>i.id===this.lastItem.id); if(idx!==-1){store[idx].coachReply=this.lastItem.coachReply;await this.store.save(store);}}
input.value='';
this._coachRound = (this._coachRound || 0) + 1;
const round = this._coachRound;
let followQ = '';
if(round >= 3) {
followQ = '你已经探到这里了。不用再回答——停在这里,呼吸三次。这个觉察,是属于你的。';
document.getElementById('coach-card').style.display='none';
this.showToast('🧘 参究已完成');
return;
}
// Try AI for follow-up via DashScope
if (!followQ && window.DASHSCOPE_KEY && window.DASHSCOPE_KEY.length > 10 && this.lastItem) {
try {
let ac = new AbortController();
setTimeout(() => ac.abort(), 6000);
const historyText = (this.lastItem.coachReply || []).map((h, i) => '追问' + (i + 1) + ':' + h.q + '\n回答:' + h.a).join('\n');
const p = '你是觉察教练。基于对话历史问一个更深的参究问题(不超过25字,引用户原文)';
const c = '用户今天写了:\nQ1:' + this.lastItem.q1 + '\nQ2:' + this.lastItem.q2 + '\nQ3:' + this.lastItem.q3 + '\nQ4:' + this.lastItem.q4 + '\n\n之前的对话:\n' + historyText + '\n\n用户刚刚又回答了:' + reply.slice(0, 50);
const b = JSON.stringify({ model: 'qwen-turbo', messages: [{ role: 'system', content: p }, { role: 'user', content: c }] });
const res = await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', { signal: ac.signal, method: 'POST', headers: { 'content-type': 'application/json', 'Authorization': 'Bearer ' + window.DASHSCOPE_KEY }, body: b });
if (res && res.ok) { const d = await res.json(); if (d && d.choices && d.choices[0] && d.choices[0].message) followQ = d.choices[0].message.content.trim(); }
} catch (e) { }
}
// Fallback to rule-based
if (!followQ) {
const r = reply.toLowerCase();
const hasFeel = /感觉|觉得|知道|认为/.test(r);
const hasEmotion = /难过|开心|愤怒|害怕|焦虑|平静|不安|委屈/.test(r);
const hasPerson = /他|她|他们|老板|同事|家人|孩子/.test(r);
const hasI = /我/.test(r);
if(round === 1) {
if(hasEmotion && hasI) followQ = '你刚才说「'+reply.slice(0,12)+'」——那个感受在被你说出来之前,它在身体的哪个位置?';
else if(hasFeel) followQ = '你说的「感觉」,是头脑里的判断,还是身体里的感受?两者很不一样。';
else if(!hasI) followQ = '你刚才说的这件事里——你在哪里?试着把答案里加上「我」,看看感受会不会不一样。';
else followQ = '看着你写的这个回答——它是从你心里流出来的,还是从习惯里跑出来的?';
} else {
if(hasEmotion) followQ = '这个情绪不是你,它只是经过你。你能看着它,而不被它卷走吗?';
else if(hasPerson) followQ = '你提到的那个人——你在乎的是他/她本身,还是你在他/她面前的自己?';
else followQ = '你刚才的回答,如果用一个画面来形容——那是什么样的画面?';
}
}
document.getElementById('coach-q').textContent=followQ; document.getElementById('coach-card').style.display='block';
},
// V2.4 — 播种与觉察
_resetSeedUI: function() {
const sec=document.getElementById('seed-section'), input=document.getElementById('seed-input'), btn=document.getElementById('seed-btn'), hint=document.getElementById('seed-hint'), op=document.getElementById('seed-old-pattern'), res=document.getElementById('seed-result');
if(sec) sec.style.display='none';
if(input) input.value='';
if(btn) { btn.textContent='种下 →'; btn.className='seed-btn'; btn.disabled=false; }
if(hint) hint.style.display='inline-block';
if(op) op.style.display='none';
if(res) res.style.display='none';
},
saveSeed: function() {
const input=document.getElementById('seed-input'), btn=document.getElementById('seed-btn'), hint=document.getElementById('seed-hint'), res=document.getElementById('seed-result');
const val=input.value.trim();
if(!val) { this.showToast('🌱 写一颗种子吧...'); return; }
this.lastItem.new_seed = val;
// Save to store
this.store.get().then(list=>{
const idx=list.findIndex(i=>i.id===this.lastItem.id);
if(idx!==-1){ list[idx].new_seed=val; this.store.save(list); }
});
btn.textContent='✓';
btn.className='seed-btn done';
btn.disabled=true;
input.disabled=true;
if(hint) hint.style.display='none';
res.innerHTML='🌱 已种下「'+val+'」的种子。每天照见,它就会生长。';
res.style.display='flex';
this.vibrate(10);
if(this.cloudWorkerUrl && this.cloudUsername) this.pushCloud(true);
},
showOldPattern: async function() {
const box=document.getElementById('seed-old-pattern');
if(box.style.display==='block'){ box.style.display='none'; return; }
box.innerHTML='🔍 正在回顾你的记录... ';
box.style.display='block';
// Try AI for old pattern summary
if(window.DASHSCOPE_KEY && window.DASHSCOPE_KEY.length>10 && this.lastItem){
try{
const list=await this.store.get();
const recent=list.filter(i=>i.id!==this.lastItem.id).sort((a,b)=>new Date(b.date)-new Date(a.date)).slice(0,5);
const historyText=recent.map(i=>'Q1:'+i.q1+' Q2:'+i.q2+' Q3:'+i.q3+' Q4:'+i.q4+(i.new_seed?' 播种:'+i.new_seed:'')).join('\n');
const prompt='你是正念教练。从以下近期记录中,总结一条该用户可能携带的旧模式/习惯性反应(一句话,不超过30字,不加评判,用「我注意到你」开头)。记录:\n'+historyText+'\n今天的记录:Q1:'+this.lastItem.q1+' Q2:'+this.lastItem.q2;
const body=JSON.stringify({model:'qwen-turbo',messages:[{role:'system',content:'你是一位正念教练,用一句话总结用户的旧模式,不评判,用「我注意到你」开头。'},{role:'user',content:prompt}]});
const ac=new AbortController();setTimeout(()=>ac.abort(),6000);
const res=await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',{signal:ac.signal,method:'POST',headers:{'content-type':'application/json','Authorization':'Bearer '+window.DASHSCOPE_KEY},body});
if(res&&res.ok){const d=await res.json();if(d&&d.choices&&d.choices[0]&&d.choices[0].message){const pattern=d.choices[0].message.content.trim();if(pattern){box.innerHTML='🌰 旧模式觉知 '+pattern+'
';this.lastItem.old_pattern=pattern;this.store.get().then(list=>{const idx=list.findIndex(i=>i.id===this.lastItem.id);if(idx!==-1){list[idx].old_pattern=pattern;this.store.save(list);}});return;}}}
}catch(e){}
}
// Fallback: use recent patterns
box.innerHTML='🌰 旧模式觉知 请留意你今天的回答中反复出现的模式。觉察它,就是改变的开始。
';
},
_gardenView: 'timeline',
_gardenFilter: 'all',
switchGardenView: function(view) {
this._gardenView = view;
const tabs=document.querySelectorAll('.garden-tab');
tabs.forEach(t=>t.classList.toggle('active',t.getAttribute('onclick').includes(view)));
if(view==='garden') { this.renderSeedGarden(); }
else { this.renderHistory(document.getElementById('search-input')?.value); }
},
_seedCategory: function(text) {
if(!text) return '';
var stop=['的','了','在','是','我','不','也','就','把','被','让','有','和','与','为','对','从','上','下','中'];
var w=text.replace(/[^\u4e00-\u9fa5\w\-]/g,' ').split(/\s+/).filter(Boolean);
for(var i=0;i🌱还没有播下种子
完成照见后,在洞察下方播下一颗种子吧
';
box.innerHTML=''+msg+'
';
return;
}
// Calculate seed levels by category
var cats={};
seeded.forEach(function(i){if(i.new_seed){var c=App._seedCategory(i.new_seed);if(c){cats[c]=(cats[c]||0)+1;}}});
// Filter bar
box.innerHTML='🌱 全部 '+seeded.length+' 🌱 新种子 🌰 旧模式
';
// Stats line
var catEntries=Object.entries(cats).sort(function(a,b){return b[1]-a[1];});
if(catEntries.length){
box.insertAdjacentHTML('beforeend','🌱 '+catEntries.length+' 种种子类型 · 最多:「'+catEntries[0][0]+'」 ('+catEntries[0][1]+'次)
');
}
// Render items
seeded.forEach(function(item){
var date=App.formatDate(item.date);
var icon, levelText;
if(item.new_seed){
var cat=App._seedCategory(item.new_seed);
var cnt=cats[cat]||1;
if(cnt>=4){ icon='🌸'; levelText='🌸 开花 · 种了 '+cnt+'次'; }
else if(cnt>=2){ icon='🌿'; levelText='🌿 发芽 · 种了 '+cnt+'次'; }
else { icon='🌱'; levelText='🌱 种子 · 第1次'; }
} else {
icon='🌰';
levelText='旧模式';
}
var seedText=item.new_seed||'旧模式觉察';
var oldPattern=item.old_pattern?'🌰 '+item.old_pattern+'
':'';
var themeIcon={work:'💼',relation:'❤️',body:'🧘',general:'🌿'}[item.theme]||'🌿';
box.insertAdjacentHTML('beforeend',
''
+'
'+icon+'
'
+'
'
+'
'+seedText+'
'
+'
'+levelText+'
'
+'
'+themeIcon+' '+date+'
'
+oldPattern
+'
'
+'
');
});
},
store: {
db:null,
init:function(){if(!window.indexedDB)return;const req=indexedDB.open('whoami_db',1);req.onupgradeneeded=e=>e.target.result.createObjectStore('entries',{keyPath:'id'});req.onsuccess=e=>this.db=e.target.result;},
async get(){if(this.db)return new Promise(r=>{const tx=this.db.transaction('entries','readonly');tx.objectStore('entries').getAll().onsuccess=e=>r(e.target.result);}); return this._getLocal();},
_getLocal(){try{return JSON.parse(localStorage.getItem('whoami_entries')||'[]');}catch(e){return[];}},
async save(entries){if(this.db){const tx=this.db.transaction('entries','readwrite');tx.objectStore('entries').clear();entries.forEach(e=>tx.objectStore('entries').put(e));} localStorage.setItem('whoami_entries',JSON.stringify(entries));},
async add(entry){const list=await this.get();list.unshift(entry);await this.save(list);return list;},
remove:async function(id){const ok=await App.showConfirm('确定删除这条记录吗?');if(!ok)return;const list=await this.get();await this.save(list.filter(i=>i.id!==id));App.renderHistory();App.renderProfile();App._syncResonanceDelete(id);},
clear:async function(){const code=(''+Math.random()).slice(2,6);const input=await App.showPrompt('⚠️ 清空前请输入验证码:'+code);if(input!==code){App.showToast('验证码错误,已取消。');return;}if(this.db){const tx=this.db.transaction('entries','readwrite');tx.objectStore('entries').clear();}localStorage.removeItem('whoami_entries');App.showToast('已清空');App.renderHistory();App.renderProfile();},
importData:function(file){const reader=new FileReader();reader.onload=e=>{try{const data=JSON.parse(e.target.result);if(Array.isArray(data)){this.save(data);App.showToast('已导入 '+data.length+' 条记录');setTimeout(()=>window.location.reload(),1000);}else{App.showToast('格式不正确。');}}catch(err){App.showToast('解析失败。');}};reader.readAsText(file);}
},
batchDelete: async function() {
const cbs=document.querySelectorAll('.del-cb:checked');const ids=[];cbs.forEach(cb=>ids.push(Number(cb.dataset.id)));
if(!ids.length){this.showToast('请先选择要删除的记录');return;}
if(!(await App.showConfirm('确定删除选中的 '+ids.length+' 条记录吗?'))) return;
const list=await this.store.get();await this.store.save(list.filter(i=>!ids.includes(i.id)));
ids.forEach(id=>App._syncResonanceDelete(id));
this.renderHistory();this.renderProfile();this.showToast('✅ 已删除 '+ids.length+' 条');
},
_syncResonanceDelete: function(recordId) {
const saved=localStorage.getItem('whoami_resonance');if(!saved)return;
const data=JSON.parse(saved);const before=data.length;
const filtered=data.filter(i=>i.recordId!==recordId);
if(filtered.lengthcb.checked=checked);};
},
formatDate: function(iso){const d=new Date(iso);return `${d.getMonth()+1}月${d.getDate()}日`;},
renderHistory: async function(keyword){const list=await this.store.get(),box=document.getElementById('timeline-list');box.innerHTML='';let filtered=keyword?list.filter(i=>i.q1.toLowerCase().includes(keyword)||i.q2.toLowerCase().includes(keyword)):list;filtered.sort((a,b)=>new Date(b.date)-new Date(a.date));if(!filtered.length){box.innerHTML='🔍 没有找到记录
换个关键词试试,或者去照见完成新记录
';return;}filtered.forEach(item=>{const themeIcon={work:'💼',relation:'❤️',body:'🧘',general:'🌿'}[item.theme]||'🌿';const seedBadge=item.new_seed?` 🌱 ${item.new_seed} `:'';box.insertAdjacentHTML('beforeend',`${themeIcon} ${this.formatDate(item.date)} ×
${item.q1}
${item.q2}
状态:${item.q4}${seedBadge}
`);});},
renderProfile: async function(){this.bindSelectAll();const list=await this.store.get();document.getElementById('stat-total').textContent=list.length;const aware=list.filter(i=>i.q4==='觉知').length,ratio=list.length?Math.round(aware/list.length*100):0;document.getElementById('stat-awared').textContent=ratio+'%';if(list.length>0){const c=list.filter(i=>i.q3==='一致').length;document.getElementById('bar-consistent').style.width=(c/list.length*100)+'%';document.getElementById('bar-other').style.width=(100-c/list.length*100)+'%';document.getElementById('bar-awareness').style.width=ratio+'%';const cc=document.getElementById('chart-container');cc.innerHTML='';const now=new Date();for(let i=6;i>=0;i--){const d=new Date(now);d.setDate(d.getDate()-i);const de=list.filter(e=>new Date(e.date).toDateString()===d.toDateString());let cls='low',h='10%';if(de.length>0){const ac=de.filter(e=>e.q4==='觉知').length;if(ac===de.length){cls='active';h='100%';}else if(ac>0){cls='mixed';h='50%';}}const bar=document.createElement('div');bar.className='chart-bar '+cls;bar.style.height=h;cc.appendChild(bar);}this.renderInsights(list);App.renderSeedStats();}},
renderInsights: function(list){
if(!list||!list.length){document.getElementById('deep-insights').style.display='none';return;}
const counts={q1:0,q2:0,q3:0,q4:0};const themes={};const q3Counts={一致:0,矛盾:0};
list.forEach(i=>{
counts[i.q4]=(counts[i.q4]||0)+1;
themes[i.theme]=(themes[i.theme]||0)+1;
q3Counts[i.q3]=(q3Counts[i.q3]||0)+1;
});
const aware=counts['觉知']||0;
const ratio=Math.round(aware/list.length*100);
const segments=`觉知 ${ratio}% · 非觉知 ${100-ratio}%`;
const topTheme=Object.entries(themes).sort((a,b)=>b[1]-a[1])[0];
const themeLabel={work:'工作/事业',relation:'关系/情感',body:'身体/健康',general:'综合觉察'}[topTheme[0]]||topTheme[0];
// Generate insight lines based on patterns
const lines=[];
lines.push(`${segments}
`);
lines.push(`📌 高频主题:${themeLabel}(${topTheme[1]} 次)
`);
// Q4 pattern
const nian=counts['念头']||0,gan=counts['感受']||0;
if(nian>gan*1.5) lines.push(`💭 你更多在头脑层面回应(念头 ${Math.round(nian/list.length*100)}%),可以多让身体参与进来
`);
else if(gan>nian*1.5) lines.push(`❤️ 你对感受很敏锐,这是很好的觉察起点
`);
// Consistency pattern
const conRate=Math.round((q3Counts['一致']||0)/list.length*100);
if(conRate>70) lines.push(`🔑 你的自我认知较一致(${conRate}%),一致性是你的力量
`);
else if(conRate<40) lines.push(`🔑 你经常觉察到「矛盾」——这正是松动认知僵化的入口
`);
// Theme-specific insight
const insights={
work:['你在工作中的觉察,往往指向对效率与意义的追问。效率是手段,意义才是终点。','你记录工作最频繁的时候,往往是你最需要被看见的时候。','工作中的矛盾感,可能不是你不够好,而是标准太完美。'],
relation:['你在关系中的觉察,最真实也最脆弱。关系是一面镜子——你看到的对方,往往是你自己的一部分。','你对关系的敏感,说明你在乎。在乎本身就是一种力量。','关系中的矛盾,常常是边界模糊的信号。看见边界,就看见了自由。'],
body:['身体的觉察是最诚实的。头脑会骗人,身体不会。','你开始关照身体,这是从「向外求」转向「向内安」的标志。','身体的疲惫,有时是心在喊停。听见它。'],
general:['综合觉察是最基础的功夫。每天看自己一眼,就是最大的善意。','你对自己的觉察越细,你对世界的理解就越深。','每一次「照见」,都是一次重新认识自己的机会。']
};
const themeInsights=insights[topTheme[0]]||insights.general;
lines.push(`💡 ${themeLabel}洞察
${themeInsights[Math.floor(Math.random()*themeInsights.length)]}
`);
document.getElementById('insight-pattern').innerHTML=lines.join('');
document.getElementById('deep-insights').style.display='block';
},
renderSeedStats: async function() {
var sec=document.getElementById('seed-stats-section'),box=document.getElementById('seed-stats');if(!sec||!box)return;
var list=await this.store.get();
var seeded=list.filter(function(i){return i.new_seed;}).sort(function(a,b){return new Date(b.date)-new Date(a.date);});
if(!seeded.length){sec.style.display='none';return;}
sec.style.display='block';
var cats={};var totalSeeds=seeded.length;var thisMonth=0;var now=new Date();
seeded.forEach(function(i){
var d=new Date(i.date);if(d.getMonth()===now.getMonth()&&d.getFullYear()===now.getFullYear())thisMonth++;
if(i.new_seed){var c=App._seedCategory(i.new_seed);if(c){cats[c]=(cats[c]||0)+1;}}
});
var catEntries=Object.entries(cats).sort(function(a,b){return b[1]-a[1];});
var top=catEntries[0];
var lines=[];
lines.push('🌱 共播种 '+totalSeeds+'颗种子,'+Object.keys(cats).length+'种类型');
if(thisMonth>0)lines.push('📅 本月播种 '+thisMonth+'颗');
if(top)lines.push('🌸 最多:「'+top[0]+'」 ('+top[1]+'次)');
var tags='';catEntries.slice(0,5).forEach(function(e,i){var icons=['🌱','🌿','🌸','🌼','🍂'];tags+=''+(icons[i]||'🌱')+e[0]+' '+e[1]+' ';});
box.innerHTML=''+lines.join(' ')+'
'+''+tags+'
';
},
generateWeeklyReport: async function(){const list=await this.store.get();const now=new Date(),weekAgo=new Date(now);weekAgo.setDate(now.getDate()-7);const weekData=list.filter(i=>new Date(i.date)>=weekAgo);if(weekData.length<1){this.showToast('📊 本周记录不足');return;}this.showToast('📊 正在生成...');setTimeout(()=>{const canvas=document.createElement('canvas'),ctx=canvas.getContext('2d');const w=750,h=1400;canvas.width=w;canvas.height=h;ctx.fillStyle=document.body.classList.contains('dark')?'#1A1A1A':'#FFFFFF';ctx.fillRect(0,0,w,h);ctx.fillStyle='#C9A96E';ctx.fillRect(0,0,16,h);ctx.fillStyle=document.body.classList.contains('dark')?'#EAEAEA':'#1A1A1A';ctx.font='bold 64px serif';ctx.textAlign='center';ctx.fillText('觉察周报',395,120);ctx.fillStyle='#999';ctx.font='24px sans-serif';ctx.fillText(`${weekAgo.getMonth()+1}.${weekAgo.getDate()} — ${now.getMonth()+1}.${now.getDate()}`,395,170);const total=weekData.length,aware=weekData.filter(i=>i.q4==='觉知').length,awareRatio=total>0?Math.round(aware/total*100):0;const cx=375,cy=350,r=110;ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.fillStyle=document.body.classList.contains('dark')?'#333':'#F0F0F0';ctx.fill();ctx.beginPath();ctx.moveTo(cx,cy);ctx.arc(cx,cy,r,-Math.PI/2,(-Math.PI/2)+(Math.PI*2*awareRatio/100));ctx.fillStyle='#C9A96E';ctx.fill();ctx.fillStyle=document.body.classList.contains('dark')?'#EAEAEA':'#1A1A1A';ctx.font='bold 72px sans-serif';ctx.textAlign='center';ctx.fillText(awareRatio+'%',cx,cy+20);ctx.fillStyle='#999';ctx.font='24px sans-serif';ctx.fillText('觉知占比',cx,cy+60);const quote=this.quotes[Math.floor(Math.random()*this.quotes.length)];ctx.fillStyle='#999';ctx.font='italic 30px serif';ctx.textAlign='center';ctx.fillText('"'+quote.text+'"',375,1100);ctx.fillStyle='#666';ctx.font='24px sans-serif';ctx.fillText('—— '+quote.author,375,1150);ctx.fillStyle='#C9A96E';ctx.font='bold 22px sans-serif';ctx.fillText('Whoami · V2.3.10',375,1250);document.getElementById('share-img').src=canvas.toDataURL('image/png');document.getElementById('share-modal').classList.add('active');},200);},
generateShareImage: function(){if(!this.lastItem)return;this.showToast('生成中...');var self=this;setTimeout(function(){
var c=document.createElement('canvas'),x=c.getContext('2d');c.width=750;c.height=1300;
// 暖色渐变背景
var grad=x.createLinearGradient(0,0,0,1300);
grad.addColorStop(0,'#FDF8F0');grad.addColorStop(0.5,'#F7F0E3');grad.addColorStop(1,'#EDE5D5');
x.fillStyle=grad;x.fillRect(0,0,750,1300);
// 顶部装饰细线
x.strokeStyle='#D4C4A8';x.lineWidth=1;x.beginPath();x.moveTo(100,70);x.lineTo(650,70);x.stroke();
x.fillStyle='#C9A96E';x.beginPath();x.arc(375,70,3,0,Math.PI*2);x.fill();
// 标题 - 居中
x.textAlign='center';
x.fillStyle='#1A1A1A';x.font='bold 32px system-ui,-apple-system,sans-serif';
x.fillText('「 我 」我是谁',375,55);
// 日期
var d=new Date(self.lastItem.date);
var dateStr=d.getFullYear()+'.'+(d.getMonth()+1)+'.'+d.getDate();
x.fillStyle='#999';x.font='12px system-ui,-apple-system,sans-serif';
x.fillText(dateStr,375,85);
// 主题标签
var themeLabels=['💼 工作/事业','❤️ 关系/情感','🧘 身体/健康','🌿 综合觉察'];
var themeIdx={work:0,relation:1,body:2,general:3}[self.lastItem.theme]||3;
var tag=themeLabels[themeIdx];
var tagW=x.measureText(tag).width+40;
x.fillStyle='#F5F0E5';x.beginPath();x.roundRect(375-tagW/2,100,tagW,26,13);x.fill();
x.fillStyle='#888';x.font='12px system-ui,-apple-system,sans-serif';x.textAlign='center';
x.fillText(tag,375,118);
// 内容
var x0=75,y=165;
var items=[['今天最重要的事',self.lastItem.q1],['这说明我是怎样的人',self.lastItem.q2],['是否与自我认知一致',self.lastItem.q3],['此刻是谁在回答',self.lastItem.q4]];
for(var i=0;i570){x.font='18px system-ui,-apple-system,sans-serif';var ln='';for(var j=0;j570){x.fillText(ln,x0,y);y+=26;ln=val[j];}else ln=nxt;}if(ln)x.fillText(ln,x0,y);y+=16;}
else{x.fillText(val,x0,y);y+=28;}
if(i<3)y+=14;}
// 分隔线
y+=4;x.strokeStyle='#D4C4A8';x.lineWidth=1;x.beginPath();x.moveTo(100,y);x.lineTo(650,y);x.stroke();
x.fillStyle='#C9A96E';x.beginPath();x.arc(375,y,3,0,Math.PI*2);x.fill();
// 今日洞察
y+=26;var insightEl=document.getElementById('insight-text');var insightTxt=insightEl?insightEl.textContent:'';
if(insightTxt){
x.textAlign='left';x.fillStyle='#A89880';x.font='12px system-ui,-apple-system,sans-serif';x.fillText('今日洞察',x0,y);y+=22;
x.fillStyle='#4A4030';x.font='15px system-ui,-apple-system,sans-serif';
var ds=insightTxt.split('\n').filter(Boolean);
for(var k=0;k570){x.font='14px system-ui,-apple-system,sans-serif';var ln='';for(var j=0;j570){x.fillText(ln,x0,y);y+=24;ln=t[j];}else ln=nxt;}if(ln)x.fillText(ln,x0,y);y+=18;}
else{x.fillText(t,x0,y);y+=24;}}
y+=10;}
// 分隔线
x.strokeStyle='#D4C4A8';x.lineWidth=1;x.beginPath();x.moveTo(100,y);x.lineTo(650,y);x.stroke();
x.fillStyle='#C9A96E';x.beginPath();x.arc(375,y,3,0,Math.PI*2);x.fill();
// 金句 - 居中
y+=30;x.textAlign='center';
var quote=self.quotes[Math.floor(Math.random()*self.quotes.length)];
var qt='「'+quote.text+'」';
x.fillStyle='#5A5040';x.font='italic 18px serif';
if(x.measureText(qt).width>580){x.font='16px serif';
var words=qt.split('');var line='';var lines=[];
for(var j=0;j580){lines.push(line);line=words[j];}else line=test;}
if(line)lines.push(line);
for(var li=0;li{const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([JSON.stringify(d,null,2)]));a.download='whoami_backup_'+new Date().toISOString().slice(0,10)+'.json';a.click();this.showToast('📤 备份已生成');});},
toggleReminder: function() { const enabled=localStorage.getItem('whoami_reminder_enabled')==='true',btn=document.getElementById('btn-toggle-reminder'),status=document.getElementById('reminder-status'),time=document.getElementById('reminder-time').value||'21:00'; if(enabled){localStorage.setItem('whoami_reminder_enabled','false');btn.textContent='开启提醒';btn.style.background='';btn.style.color='';if(this.reminderInterval)clearInterval(this.reminderInterval);status.textContent='';this.showToast('⏰ 提醒已关闭');}else{if(Notification&&Notification.permission==='default')Notification.requestPermission();localStorage.setItem('whoami_reminder_enabled','true');localStorage.setItem('whoami_reminder_time',time);btn.textContent='关闭提醒';btn.style.background='var(--red)';btn.style.color='#fff';status.textContent='✅ 已开启 (每日 '+time+')';this.startReminderCheck();this.showToast('⏰ 提醒已开启');} },
saveReminderTime: function() { const time=document.getElementById('reminder-time').value||'21:00';localStorage.setItem('whoami_reminder_time',time);const enabled=localStorage.getItem('whoami_reminder_enabled')==='true';if(enabled)document.getElementById('reminder-status').textContent='✅ 已开启 (每日 '+time+')';else{document.getElementById('reminder-status').textContent='⏰ 已保存: '+time;setTimeout(()=>{const s=document.getElementById('reminder-status');if(s&&s.textContent.includes('已保存'))s.textContent='';},2000);}this.showToast('⏰ 提醒时间已保存'); },
initReminder: function() { if(localStorage.getItem('whoami_reminder_enabled')==='true'){const t=localStorage.getItem('whoami_reminder_time')||'21:00';document.getElementById('reminder-time').value=t;document.getElementById('btn-toggle-reminder').textContent='关闭提醒';document.getElementById('reminder-status').textContent='✅ 已开启 (每日 '+t+')';this.startReminderCheck();} },
startReminderCheck: function() { if(this.reminderInterval)clearInterval(this.reminderInterval);this.reminderInterval=setInterval(()=>this.checkReminder(),30000);this.checkReminder(); },
checkReminder: function() { if(localStorage.getItem('whoami_reminder_enabled')!=='true')return;const ts=document.getElementById('reminder-time').value||'21:00',now=new Date(),[h,m]=ts.split(':').map(Number);const rt=new Date(now.getFullYear(),now.getMonth(),now.getDate(),h,m),diff=now-rt;if(diff>=0&&diff<60000){const today=now.toDateString();if(localStorage.getItem('whoami_reminder_last')===today)return;localStorage.setItem('whoami_reminder_last',today);this.triggerReminder();} },
triggerReminder: function() { if(Notification&&Notification.permission==='granted')new Notification('「 我 」我是谁',{body:'今天最重要的事是什么?来照见此刻吧。'});this.showToast('🌙 提醒:今天最重要的事是什么?'); },
initVoice: function() { const SR=window.SpeechRecognition||window.webkitSpeechRecognition; if(SR){this.recognition=new SR();this.recognition.lang='zh-CN';this.recognition.continuous=false;this.recognition.interimResults=false;this.recognition.onresult=event=>{const r=event.results[0][0].transcript;if(this.targetFieldId){document.getElementById(this.targetFieldId).value+=r;this.showToast('🎤 识别成功');}this.isRecording=false;this.updateMicUI();};this.recognition.onerror=()=>{this.showToast('🎤 识别失败');this.isRecording=false;this.updateMicUI();};this.recognition.onend=()=>{this.isRecording=false;this.updateMicUI();};}else{document.querySelectorAll('.mic-btn').forEach(b=>b.style.display='none');} },
startVoice: function(id) { if(this.isRecording){this.recognition.stop();this.isRecording=false;}else{this.targetFieldId=id;this.recognition.start();this.isRecording=true;this.showToast('🎤 正在聆听...');}this.updateMicUI(); },
updateMicUI: function() { const btn=this.targetFieldId?document.querySelector(`button[onclick="App.startVoice('${this.targetFieldId}')"]`):null;if(btn){if(this.isRecording)btn.classList.add('recording');else btn.classList.remove('recording');} },
loadResonance: async function() {
const def=[{id:1,text:"今天最重要的事是陪孩子搭积木。这说明我是个愿意慢下来的人。",q3:"一致",time:"2小时前",resonances:12,userResonated:false},{id:2,text:"最重要的事是完成那份报告。说明我是个有责任感的人。",q3:"矛盾",time:"5小时前",resonances:8,userResonated:false},{id:3,text:"今天什么都没做,只是好好呼吸了一次。说明我还活着。",q3:"一致",time:"昨天",resonances:23,userResonated:false}];
// Initialize with local data first
var saved=localStorage.getItem('whoami_resonance');this.resonanceData=saved?JSON.parse(saved):def;if(this.resonanceData){var seen={};this.resonanceData=this.resonanceData.filter(function(i){var key=i.recordId||i.text+i.q3;if(seen[key])return false;seen[key]=1;return true;});}this.renderResonance();
// Then try API to update - dedup by recordId
if(this.cloudWorkerUrl){try{const res=await this.fetchWithTimeout(this.cloudWorkerUrl+'/resonance',{},8000);if(res&&res.ok){const data=await res.json();if(data&&data.items&&data.items.length){const existing=this.resonanceData||[];const seen={};existing.forEach(i=>{if(i.recordId)seen[i.recordId]=1;});const merged=data.items.slice();existing.forEach(i=>{if(i.recordId&&!seen[i.recordId])merged.push(i);});this.resonanceData=merged;localStorage.setItem('whoami_resonance',JSON.stringify(merged));this.renderResonance();return;}}}catch(e){/* fallback to local */}}},
_timeAgo: function(iso) {
if(!iso) return '刚刚';
var now=Date.now(),then=new Date(iso).getTime(),diff=now-then;
if(diff<60000) return '刚刚';
if(diff<3600000) return Math.floor(diff/60000)+'分钟前';
if(diff<86400000) return Math.floor(diff/3600000)+'小时前';
return Math.floor(diff/86400000)+'天前';
},
_resonanceFilter: 'all',
switchResonanceFilter: function(filter) {
this._resonanceFilter = filter;
var tabs=document.querySelectorAll('#page-resonance .garden-tab');
tabs.forEach(function(t){t.classList.toggle('active',t.getAttribute('onclick').includes(filter));});
this.renderResonance();
},
renderResonance: function() { var box=document.getElementById('resonance-list');box.innerHTML='';var data=this.resonanceData||[];var filtered=this._resonanceFilter==='seed'?data.filter(function(i){return i.seed;}):data;if(!filtered.length){var emptyMsg=this._resonanceFilter==='seed'?'':'';box.innerHTML=emptyMsg;return;}filtered.forEach(function(item){var seedTag=item.seed?'🌱 大家一起来种下「'+item.seed+'」的种子
':'';var waterBtn=item.seed?'💧 '+(item.waters||0)+'
':'';box.insertAdjacentHTML('beforeend','💫 匿名觉察 · '+(item.time||App._timeAgo(item.created_at))+'
"'+item.text+'"
'+seedTag+'
');});box.insertAdjacentHTML('afterbegin','📮 匿名发布我的觉察
');},
waterSeed: function(id) { var item=this.resonanceData.find(function(i){return i.id===id;});if(!item||!item.seed)return;item.userWatered=!item.userWatered;item.waters=(item.waters||0)+(item.userWatered?1:-1);if(item.waters<0)item.waters=0;localStorage.setItem('whoami_resonance',JSON.stringify(this.resonanceData));this.renderResonance(); },
toggleResonance: function(id) { const item=this.resonanceData.find(i=>i.id===id);if(!item)return;item.userResonated=!item.userResonated;item.resonances=(item.resonances||0)+(item.userResonated?1:-1);if(item.resonances<0)item.resonances=0;localStorage.setItem('whoami_resonance',JSON.stringify(this.resonanceData));this.renderResonance(); },
publishResonance: async function() { if(!this.lastItem){this.showToast('请先完成今日记录');return;}if(!(await App.showConfirm('💫 匿名发布觉察?')))return;var rid=this.lastItem.id;if((this.resonanceData||[]).some(function(i){return i.recordId===rid;})){this.showToast('⚠️ 这条记录已发布过');return;}var text='📍 '+this.lastItem.q1+' · 这说明我是:'+this.lastItem.q2;var seed=this.lastItem.new_seed||'';var payload={recordId:rid,text:text,q3:this.lastItem.q3};if(seed)payload.seed=seed;if(this.cloudWorkerUrl){this.fetchWithTimeout(this.cloudWorkerUrl+'/resonance',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)},5000).then(function(res){if(res&&res.ok)App.showToast('✅ 已发布到共鸣广场');else App.showToast('⚠️ 云端发布失败,已保存本地');}).catch(function(){App.showToast('⚠️ 网络异常,已保存本地');});}var item={id:Date.now(),recordId:rid,text:text,q3:this.lastItem.q3,time:'刚刚',resonances:0,userResonated:false};if(seed)item.seed=seed;this.resonanceData.unshift(item);localStorage.setItem('whoami_resonance',JSON.stringify(this.resonanceData));this.renderResonance();if(!this.cloudWorkerUrl)this.showToast('已匿名发布');},
checkInStreak: async function() { const today=new Date().toDateString();let streak=JSON.parse(localStorage.getItem('whoami_streak')||'{"count":0,"lastDate":""}');const yesterday=new Date();yesterday.setDate(yesterday.getDate()-1);if(streak.lastDate===today)return;if(streak.lastDate===yesterday.toDateString()){streak.count++;}else{streak.count=1;}streak.lastDate=today;localStorage.setItem('whoami_streak',JSON.stringify(streak));await this.updateStreak(); },
updateStreak: async function() { const streak=JSON.parse(localStorage.getItem('whoami_streak')||'{"count":0,"lastDate":""}');const el=document.getElementById('streak-display');if(!el)return;const today=new Date().toDateString(),yesterday=new Date();yesterday.setDate(yesterday.getDate()-1);let dc=streak.count;if(streak.lastDate!==today&&streak.lastDate!==yesterday.toDateString())dc=0;if(dc>0){const total=await App.store.get();const tc=total.length;el.innerHTML=`🔥
${dc} 天
连续觉察中,保持!
累计 ${tc} 次
`}else el.innerHTML=''; },
showConfirm: function(text, onConfirm) {
return new Promise(resolve => {
const el = document.getElementById('confirm-overlay');
document.getElementById('confirm-text').textContent = text;
document.getElementById('confirm-ok').onclick = () => { el.style.display = 'none'; resolve(true); };
document.getElementById('confirm-cancel').onclick = () => { el.style.display = 'none'; resolve(false); };
el.style.display = 'flex';
});
},
};
App.breathExerciseAllowed = true;
App.init();
// breath exercise patch
if (window.App && !window.App.toggleBreathExercise) {
window.App.toggleBreathExercise = function(suffix) {
suffix = suffix || '';
if (this['_breathe' + suffix]) this.stopBreathExercise(suffix);
else this.startBreathExercise(suffix);
};
window.App.startBreathExercise = function(suffix) {
suffix = suffix || '';
if (!this.breathExerciseAllowed) return;
this.isBreathing = true;
this.breathExerciseAllowed = false;
this['_breathe' + suffix] = true;
const btn = document.getElementById('breath-start-btn' + suffix);
if (btn) { btn.textContent = '停止练习'; btn.classList.add('stop'); }
this.runBreathCycle(0, suffix);
};
window.App.stopBreathExercise = function(suffix) {
suffix = suffix || '';
this.isBreathing = false;
// Clear all timers
if (this.breathInterval) { clearTimeout(this.breathInterval); this.breathInterval = null; }
// Also clear any running interval timers inside runBreathCycle
const phase = document.getElementById('breath-phase' + suffix);
const timer = document.getElementById('breath-timer' + suffix);
const ring = document.getElementById('breath-ring' + suffix);
const container = ring ? ring.closest('.breath-circle-container') : null;
// Reset to initial state
if (container) container.classList.remove('hold');
if (ring) {
ring.style.transition = 'none';
ring.style.strokeDashoffset = '502';
ring.classList.remove('ring-inhale','ring-hold','ring-exhale');
// Force reflow
void ring.offsetWidth;
}
if (phase) { phase.textContent = '准备好了吗?'; phase.style.color = ''; }
if (timer) { timer.textContent = (this.breathMode === 'awareness') ? '4-4-6' : '4-7-8'; timer.style.color = ''; }
const btn = document.getElementById('breath-start-btn' + suffix);
if (btn) { btn.textContent = '开始练习'; btn.classList.remove('stop'); }
this['_breathe' + suffix] = false;
// Clear any stray intervals from the breath cycle
if (this._breathIntervals) { this._breathIntervals.forEach(id=>clearInterval(id)); this._breathIntervals=[]; }
setTimeout(() => { this.breathExerciseAllowed = true; }, 0);
};
window.App.runBreathCycle = function(cycle, suffix) {
suffix = suffix || '';
if (!this.isBreathing || cycle >= 4) { this.stopBreathExercise(suffix); this.showToast('练习完成'); this.trackBreathCompletion(); return; }
const phase = document.getElementById('breath-phase' + suffix);
const timer = document.getElementById('breath-timer' + suffix);
const ring = document.getElementById('breath-ring' + suffix);
if (!phase || !timer || !ring) return;
const container = document.querySelector('#breath-ring' + suffix).closest('.breath-circle-container');
const mode = this.breathMode || '478';
const isAware = mode === 'awareness';
const inhaleDur = isAware ? 4 : 4;
const holdDur = isAware ? 4 : 7;
const exhaleDur = isAware ? 6 : 8;
const inhaleText = isAware ? '杯子是空的' : '吸气...';
const holdText = isAware ? '水在流动' : '屏息...';
const exhaleText = isAware ? '我是空杯' : '呼气...';
phase.textContent = inhaleText;
phase.style.color = '#5BC0EB';
if (container) container.classList.remove('hold');
ring.style.transition = 'none';
ring.style.strokeDashoffset = '502';
ring.classList.remove('ring-inhale','ring-hold','ring-exhale');
ring.classList.add('ring-inhale');
void ring.offsetWidth;
ring.style.transition = 'stroke-dashoffset ' + inhaleDur + 's linear';
ring.style.strokeDashoffset = '0';
let count = inhaleDur;
timer.textContent = count;
const ii = setInterval(() => { count--; if (count <= 0) clearInterval(ii); timer.textContent = count; }, 1000);
if (!this._breathIntervals) this._breathIntervals = [];
this._breathIntervals.push(ii);
this.breathInterval = setTimeout(() => {
clearInterval(ii);
if (!this.isBreathing) return;
phase.textContent = holdText;
phase.style.color = 'var(--gold)';
if (container) container.classList.add('hold');
ring.classList.remove('ring-inhale','ring-hold','ring-exhale');
ring.classList.add('ring-hold');
count = holdDur;
timer.textContent = count;
const hi = setInterval(() => { count--; if (count <= 0) clearInterval(hi); timer.textContent = count; }, 1000);
this.breathInterval = setTimeout(() => {
clearInterval(hi);
if (!this.isBreathing) return;
if (container) container.classList.remove('hold');
phase.textContent = exhaleText;
phase.style.color = '#56C596';
ring.classList.remove('ring-inhale','ring-hold','ring-exhale');
ring.classList.add('ring-exhale');
ring.style.transition = 'none';
ring.style.strokeDashoffset = '0';
void ring.offsetWidth;
ring.style.transition = 'stroke-dashoffset ' + exhaleDur + 's linear';
ring.style.strokeDashoffset = '502';
count = exhaleDur;
timer.textContent = count;
const ei = setInterval(() => { count--; if (count <= 0) clearInterval(ei); timer.textContent = count; }, 1000);
this.breathInterval = setTimeout(() => {
clearInterval(ei);
if (this.isBreathing) this.runBreathCycle(cycle + 1, suffix);
}, exhaleDur * 1000);
}, holdDur * 1000);
}, inhaleDur * 1000);
};
window.App.trackBreathCompletion = function() {
const key = 'whoami_breath_count';
const count = parseInt(localStorage.getItem(key) || '0') + 1;
localStorage.setItem(key, String(count));
};
window.App.renderBreathStats = function() {
const el = document.getElementById('breath-stats');
if (!el) return;
const count = parseInt(localStorage.getItem('whoami_breath_count') || '0');
el.innerHTML = count > 0 ? '🧘 已完成 ' + count + ' 次呼吸练习' : '开始你的第一次呼吸练习吧';
};
}
document.getElementById('search-input').addEventListener('input',e=>App.renderHistory(e.target.value));
document.getElementById('btn-share').addEventListener('click',()=>App.generateShareImage());