ai-growth 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/AGENTS.md +232 -0
- package/IMPLEMENTATION_PLAN.md +207 -0
- package/LICENSE +21 -0
- package/PRODUCT_SPEC.md +1776 -0
- package/README.md +384 -0
- package/SKILL.md +121 -0
- package/START-HERE.md +156 -0
- package/dist/api/app.js +206 -0
- package/dist/api/server.js +42 -0
- package/dist/cli/main.js +799 -0
- package/dist/core/actions.js +264 -0
- package/dist/core/context.js +7 -0
- package/dist/core/directions.js +26 -0
- package/dist/core/evidence.js +155 -0
- package/dist/core/goals.js +188 -0
- package/dist/core/lifeContext.js +41 -0
- package/dist/core/memory.js +35 -0
- package/dist/core/metrics.js +74 -0
- package/dist/core/profile.js +95 -0
- package/dist/core/progress.js +244 -0
- package/dist/core/prompts/index.js +40 -0
- package/dist/core/replan.js +100 -0
- package/dist/core/rollover.js +68 -0
- package/dist/core/rolloverTypes.js +1 -0
- package/dist/core/stateMachine.js +61 -0
- package/dist/core/status.js +57 -0
- package/dist/core/types.js +9 -0
- package/dist/daemon/main.js +64 -0
- package/dist/db/cli.js +19 -0
- package/dist/db/client.js +36 -0
- package/dist/db/migrate.js +34 -0
- package/dist/db/migrations/V1__init.sql +249 -0
- package/dist/device/card.js +257 -0
- package/dist/device/deck.js +157 -0
- package/dist/device/fontCoverage.js +162 -0
- package/dist/device/http.js +170 -0
- package/dist/device/png.js +111 -0
- package/dist/device/provision.js +185 -0
- package/dist/device/render.js +194 -0
- package/dist/device/renderCard.js +48 -0
- package/dist/device/report.js +146 -0
- package/dist/device/serial.js +201 -0
- package/dist/device/token.js +78 -0
- package/dist/device/types.js +2 -0
- package/dist/mcp/ctx.js +48 -0
- package/dist/mcp/server.js +15 -0
- package/dist/mcp/tools.js +586 -0
- package/dist/passport/builder.js +83 -0
- package/dist/passport/state.js +81 -0
- package/dist/passport/sync.js +61 -0
- package/dist/reminder/notify.js +30 -0
- package/dist/reminder/scheduler.js +87 -0
- package/dist/reminder/service.js +102 -0
- package/dist/shared/config.js +50 -0
- package/dist/shared/result.js +27 -0
- package/dist/shared/time.js +67 -0
- package/dist/shared/util.js +21 -0
- package/dist/ui/app.js +500 -0
- package/dist/ui/index.html +38 -0
- package/dist/ui/style.css +367 -0
- package/docs/images/cards.png +0 -0
- package/docs/images/cover.png +0 -0
- package/launchd/com.lairey.ai-growth.daemon.plist +45 -0
- package/llms.txt +52 -0
- package/package.json +79 -0
- package/scripts/check-device-cards.mjs +177 -0
- package/scripts/doctor.sh +194 -0
- package/scripts/gen-device-assets.mjs +188 -0
- package/scripts/install.sh +291 -0
- package/scripts/preview-cards.mjs +120 -0
- package/scripts/reset-dev-data.sh +92 -0
- package/scripts/smoke-device.mjs +401 -0
- package/scripts/uninstall.sh +63 -0
- package/server.json +56 -0
package/dist/ui/app.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
AI Growth — 游戏面板式 HUD
|
|
3
|
+
原则 6 / §16.3:不产生虚构属性值、掌握度百分比、自律分。
|
|
4
|
+
面板上出现的每个数字都必须是可核对事实。
|
|
5
|
+
============================================================ */
|
|
6
|
+
|
|
7
|
+
const app = document.getElementById('app');
|
|
8
|
+
const statusText = document.getElementById('status-text');
|
|
9
|
+
const statusDate = document.getElementById('status-date');
|
|
10
|
+
const linkDot = document.getElementById('link-dot');
|
|
11
|
+
|
|
12
|
+
// 桌面小组件 / 打包成桌面应用时的紧凑布局
|
|
13
|
+
if (new URLSearchParams(location.search).get('layout') === 'widget') {
|
|
14
|
+
document.body.classList.add('widget');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let page = 'today';
|
|
18
|
+
/** todayKey 由服务端按配置时区给出(不信任客户端时钟/时区) */
|
|
19
|
+
const state = { todayKey: null };
|
|
20
|
+
|
|
21
|
+
/** 标签页与 URL hash 同步:刷新/书签/桌面组件重启后能回到同一页 */
|
|
22
|
+
const PAGES = ['today', 'goals', 'timeline', 'passport', 'settings'];
|
|
23
|
+
|
|
24
|
+
function setPage(next, { push = true } = {}) {
|
|
25
|
+
page = PAGES.includes(next) ? next : 'today';
|
|
26
|
+
document.querySelectorAll('.tab').forEach((x) => x.classList.toggle('is-active', x.dataset.page === page));
|
|
27
|
+
if (push && location.hash.slice(1) !== page) location.hash = page;
|
|
28
|
+
render();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
document.getElementById('tabs').addEventListener('click', (e) => {
|
|
32
|
+
const btn = e.target.closest('.tab');
|
|
33
|
+
if (btn) setPage(btn.dataset.page);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
window.addEventListener('hashchange', () => setPage(location.hash.slice(1), { push: false }));
|
|
37
|
+
|
|
38
|
+
document.addEventListener('keydown', (e) => {
|
|
39
|
+
if (e.key === 'r' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); render(); }
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function api(path, opts) {
|
|
43
|
+
const res = await fetch(path, opts);
|
|
44
|
+
return res.json();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function esc(s) {
|
|
48
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setLink(ok, text) {
|
|
52
|
+
linkDot.className = 'dot ' + (ok ? 'ok' : 'bad');
|
|
53
|
+
statusText.textContent = text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fmtMin(m) {
|
|
57
|
+
if (!m && m !== 0) return '';
|
|
58
|
+
return m >= 60 ? `${Math.floor(m / 60)}h${m % 60 ? (m % 60) + 'm' : ''}` : `${m}m`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 相对日期。key 必须是服务端按配置时区算好的 YYYY-MM-DD。
|
|
63
|
+
* state.todayKey 由任意一个接口返回(含 /api/timeline),因此直接打开 #timeline 也正确。
|
|
64
|
+
* 注意:绝不要在客户端对 ISO 串做 slice(0,10) —— 那是按 UTC 截断,东八区会差一天。
|
|
65
|
+
*/
|
|
66
|
+
function relDate(key) {
|
|
67
|
+
if (!key) return '';
|
|
68
|
+
const today = state.todayKey;
|
|
69
|
+
if (today && key === today) return '今天';
|
|
70
|
+
const d = new Date(`${key}T12:00:00`);
|
|
71
|
+
if (!today) return `${d.getMonth() + 1}月${d.getDate()}日`;
|
|
72
|
+
const t = new Date(`${today}T12:00:00`);
|
|
73
|
+
const diff = Math.round((t - d) / 86400000);
|
|
74
|
+
if (diff === 0) return '今天';
|
|
75
|
+
if (diff === 1) return '昨天';
|
|
76
|
+
if (diff > 1 && diff < 7) return `${diff} 天前`;
|
|
77
|
+
return `${d.getMonth() + 1}月${d.getDate()}日`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------- 渲染入口
|
|
81
|
+
|
|
82
|
+
async function render() {
|
|
83
|
+
app.innerHTML = '<div class="boot">读取面板…</div>';
|
|
84
|
+
try {
|
|
85
|
+
if (page === 'today') await renderToday();
|
|
86
|
+
else if (page === 'goals') await renderGoals();
|
|
87
|
+
else if (page === 'timeline') await renderTimeline();
|
|
88
|
+
else if (page === 'passport') await renderPassport();
|
|
89
|
+
else if (page === 'settings') await renderSettings();
|
|
90
|
+
setLink(true, '已连接');
|
|
91
|
+
} catch (e) {
|
|
92
|
+
setLink(false, '连接失败');
|
|
93
|
+
app.innerHTML = `<div class="card hl-gold"><div class="empty">读不到数据:${esc(e.message)}<br><br>
|
|
94
|
+
确认服务在跑:<br><code style="color:var(--cyan)">npm run api</code><br>
|
|
95
|
+
或先自检:<code style="color:var(--cyan)">bash scripts/doctor.sh</code></div></div>`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------- 面板页
|
|
100
|
+
|
|
101
|
+
async function renderToday() {
|
|
102
|
+
const r = await api('/api/today');
|
|
103
|
+
const d = r.data ?? {};
|
|
104
|
+
state.todayKey = d.today ?? state.todayKey;
|
|
105
|
+
statusDate.textContent = d.today ?? '';
|
|
106
|
+
|
|
107
|
+
const st = d.status ?? {};
|
|
108
|
+
const facts = d.facts ?? {};
|
|
109
|
+
const main = (d.actions ?? []).find((a) => a.kind === 'MAIN_QUEST') ?? null;
|
|
110
|
+
const dailies = (d.actions ?? []).filter((a) => a.kind === 'DAILY');
|
|
111
|
+
|
|
112
|
+
// 首次使用:不展示任务,先引导访谈(原则 2)
|
|
113
|
+
if (st.onboarding !== 'COMPLETED') {
|
|
114
|
+
app.innerHTML = `
|
|
115
|
+
<div class="grid">
|
|
116
|
+
<div class="card hl-cyan col-6">
|
|
117
|
+
<div class="onboard">
|
|
118
|
+
<h2>系统还没开始认识你</h2>
|
|
119
|
+
<p>这个面板不是任务清单,它是<strong>替你维护计划</strong>的成长系统。
|
|
120
|
+
开始前需要先和你的 Agent 聊一次——它会问你想去哪、每天有多少时间、想过什么样的生活。</p>
|
|
121
|
+
<p style="margin-top:10px;color:var(--ink-faint)">没有访谈就不会有任务。这是刻意设计的:AI 管路线,你决定目的地。</p>
|
|
122
|
+
<code>对 Agent 说:「开启今日任务」</code>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>`;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const stagePct = d.stageProgress ? (d.stageProgress.order / d.stageProgress.total) : 0;
|
|
130
|
+
const trackHtml = renderStageTrack(d.stages ?? []);
|
|
131
|
+
|
|
132
|
+
app.innerHTML = `
|
|
133
|
+
<div class="grid">
|
|
134
|
+
|
|
135
|
+
<!-- 主线定位 -->
|
|
136
|
+
<div class="card hl-violet col-4">
|
|
137
|
+
<div class="card-head">当前主线<span class="rule"></span></div>
|
|
138
|
+
${d.primaryGoal
|
|
139
|
+
? `<div class="quest-title" style="font-size:16px">${esc(d.primaryGoal.title)}</div>
|
|
140
|
+
<div class="field"><div class="field-k">Why</div><div class="field-v">${esc(d.primaryGoal.why)}</div></div>
|
|
141
|
+
${d.primaryGoal.targetDate ? `<div class="quest-meta"><span class="chip">目标 ${esc(d.primaryGoal.targetDate)}</span></div>` : ''}`
|
|
142
|
+
: '<div class="empty">还没有 Active Goal</div>'}
|
|
143
|
+
${d.lifeContext && d.lifeContext.mode !== 'NORMAL'
|
|
144
|
+
? `<div class="quest-meta"><span class="chip chip-amber">${esc(modeLabel(d.lifeContext.mode))}${d.lifeContext.availableMinutesPerDay ? ' · 每天 ' + d.lifeContext.availableMinutesPerDay + ' 分钟' : ''}</span></div>`
|
|
145
|
+
: ''}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- 阶段轨道:Stage 3 / 6 是可核对事实 -->
|
|
149
|
+
<div class="card hl-cyan col-2">
|
|
150
|
+
<div class="card-head">阶段进度<span class="rule"></span></div>
|
|
151
|
+
${d.stageProgress
|
|
152
|
+
? `<div class="fact cyan" style="border:none;background:none;padding:0">
|
|
153
|
+
<div class="fact-n">Stage ${d.stageProgress.order}<small> / ${d.stageProgress.total}</small></div>
|
|
154
|
+
<div class="fact-k">${esc(d.stageProgress.title)}</div>
|
|
155
|
+
</div>`
|
|
156
|
+
: '<div class="empty">尚未分阶段</div>'}
|
|
157
|
+
${(d.stages ?? []).length > 1 ? `<div style="margin-top:14px">${trackHtml}</div>` : ''}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- 今天最重要的一步 -->
|
|
161
|
+
<div class="col-6">
|
|
162
|
+
${main ? mainQuestCard(main) : `
|
|
163
|
+
<div class="card hl-cyan">
|
|
164
|
+
<div class="card-head">今天最重要的一步<span class="rule"></span></div>
|
|
165
|
+
<div class="empty">今天还没有 Main Quest。<br>让 Agent 帮你规划,或直接对它说你想推进什么。</div>
|
|
166
|
+
</div>`}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<!-- 每日维护 + 今日提醒(高度相近,同排更紧凑) -->
|
|
170
|
+
<div class="card col-4">
|
|
171
|
+
<div class="card-head">每日维护<span class="rule"></span>
|
|
172
|
+
${facts.todayDaily ? `<span class="chip">${facts.todayDaily.done}/${facts.todayDaily.total} 完成</span>` : ''}
|
|
173
|
+
</div>
|
|
174
|
+
${dailies.length
|
|
175
|
+
? `<div class="task-cols">${dailies.map(dailyRow).join('')}</div>`
|
|
176
|
+
: '<div class="empty">今天没有每日任务<br><span style="font-size:12px">这也是合理的安排</span></div>'}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="card col-2">
|
|
180
|
+
<div class="card-head">今日提醒<span class="rule"></span></div>
|
|
181
|
+
${(d.reminders ?? []).length
|
|
182
|
+
? d.reminders.map((x) => `
|
|
183
|
+
<div class="entry">
|
|
184
|
+
<div class="entry-date">${esc(new Date(x.scheduledAt).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }))}</div>
|
|
185
|
+
<div><div class="entry-title">${x.status === 'SENT' ? '已提醒' : '待提醒'}</div>
|
|
186
|
+
<div class="entry-sub">${esc(x.channel === 'LOCAL_NOTIFICATION' ? '系统通知' : x.channel)}</div></div>
|
|
187
|
+
</div>`).join('')
|
|
188
|
+
: '<div class="empty">没有安排提醒</div>'}
|
|
189
|
+
<div class="hint">提醒会随任务一起失效:任务完成或过期,对应提醒自动取消,不会反复催。</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<!-- 事实统计(可核对,非虚构属性) -->
|
|
193
|
+
<div class="card col-6">
|
|
194
|
+
<div class="card-head">累计事实<span class="rule"></span>
|
|
195
|
+
<span class="chip" style="font-size:10px">可核对</span></div>
|
|
196
|
+
<div class="facts">
|
|
197
|
+
<div class="fact gold"><div class="fact-n">${facts.mainQuestCompletedTotal ?? 0}</div><div class="fact-k">已完成主任务</div></div>
|
|
198
|
+
<div class="fact cyan"><div class="fact-n">${facts.evidenceTotal ?? 0}</div><div class="fact-k">证据总数</div></div>
|
|
199
|
+
<div class="fact violet"><div class="fact-n">${facts.growthEventTotal ?? 0}</div><div class="fact-k">成长记录</div></div>
|
|
200
|
+
<div class="fact green"><div class="fact-n">${facts.todayMain?.done ?? 0}<small>/${facts.todayMain?.total ?? 0}</small></div><div class="fact-k">今日主任务</div></div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- 最近进展 -->
|
|
205
|
+
<div class="card col-6">
|
|
206
|
+
<div class="card-head">最近进展<span class="rule"></span>
|
|
207
|
+
${(d.recentEvents ?? []).length ? `<span class="chip">${d.recentEvents.length}</span>` : ''}</div>
|
|
208
|
+
${(d.recentEvents ?? []).length
|
|
209
|
+
? `<div class="task-cols">${d.recentEvents.map((e) => `
|
|
210
|
+
<div class="entry">
|
|
211
|
+
<div class="entry-date">${esc(relDate(e.date))}</div>
|
|
212
|
+
<div>
|
|
213
|
+
<div class="entry-title">${esc(e.title)}</div>
|
|
214
|
+
<div class="entry-sub">${chipDomain(e.domain)}${e.evidenceCount ? `<span class="chip" style="font-size:10.5px">${e.evidenceCount} 条证据</span>` : ''}</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>`).join('')}</div>`
|
|
217
|
+
: '<div class="empty">还没有成长记录<br><span style="font-size:12px">完成真实任务并记下证据后会出现</span></div>'}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
</div>`;
|
|
221
|
+
|
|
222
|
+
bindOps();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 阶段节点轨道:只表达"第几个 / 共几个",不换算百分比 */
|
|
226
|
+
function renderStageTrack(stages) {
|
|
227
|
+
if (!stages.length) return '';
|
|
228
|
+
const done = new Set(['COMPLETED']);
|
|
229
|
+
const skip = new Set(['SKIPPED']);
|
|
230
|
+
const now = new Set(['ACTIVE', 'DELAYED']);
|
|
231
|
+
const items = stages.map((s, i) => {
|
|
232
|
+
const cls = done.has(s.status) ? 'past' : now.has(s.status) ? 'now' : skip.has(s.status) ? 'skip' : '';
|
|
233
|
+
const node = `<div class="node ${cls}" title="${esc(s.title)}">${done.has(s.status) ? '✓' : s.order}</div>`;
|
|
234
|
+
const link = i < stages.length - 1 ? `<div class="link ${done.has(s.status) ? 'past' : ''}"></div>` : '';
|
|
235
|
+
return `<div class="node-wrap">${node}${link}</div>`;
|
|
236
|
+
}).join('');
|
|
237
|
+
const current = stages.find((s) => now.has(s.status));
|
|
238
|
+
// 图例加圆点标记,避免被误读成"节点 1 的标签"
|
|
239
|
+
return `<div class="track">${items}</div>
|
|
240
|
+
${current ? `<div class="node-legend">
|
|
241
|
+
<span><i class="dotmark"></i>当前:${esc(current.title)}</span>
|
|
242
|
+
<span>${esc(current.end ?? '')}</span>
|
|
243
|
+
</div>` : ''}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function mainQuestCard(a) {
|
|
247
|
+
const open = isOpen(a);
|
|
248
|
+
return `
|
|
249
|
+
<div class="card quest-card">
|
|
250
|
+
<div class="card-head" style="color:var(--gold)">今天最重要的一步<span class="rule"></span></div>
|
|
251
|
+
<div class="quest-title">${esc(a.title)}</div>
|
|
252
|
+
<div class="quest-meta">
|
|
253
|
+
${a.estimatedMinutes ? `<span class="chip chip-gold">预计 ${fmtMin(a.estimatedMinutes)}</span>` : ''}
|
|
254
|
+
<span class="chip chip-${a.status === 'COMPLETED' ? 'green' : 'gold'}">${esc(statusLabel(a.status))}</span>
|
|
255
|
+
${chipDomain(a.domain)}
|
|
256
|
+
</div>
|
|
257
|
+
${a.whyToday ? `<div class="field"><div class="field-k">为什么今天做</div><div class="field-v">${esc(a.whyToday)}</div></div>` : ''}
|
|
258
|
+
${a.completionCriteria ? `<div class="field"><div class="field-k">完成标准</div><div class="field-v">${esc(a.completionCriteria)}</div></div>` : ''}
|
|
259
|
+
${open ? `<div class="ops">
|
|
260
|
+
<button class="btn btn-primary" data-id="${esc(a.id)}" data-op="complete">已完成</button>
|
|
261
|
+
<button class="btn" data-id="${esc(a.id)}" data-op="delay">延期</button>
|
|
262
|
+
<button class="btn" data-id="${esc(a.id)}" data-op="skip">跳过</button>
|
|
263
|
+
</div>` : ''}
|
|
264
|
+
</div>`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function dailyRow(a) {
|
|
268
|
+
const open = isOpen(a);
|
|
269
|
+
const cls = a.status === 'COMPLETED' ? 'done' : a.status === 'EXPIRED' ? 'miss' : '';
|
|
270
|
+
return `
|
|
271
|
+
<div class="task">
|
|
272
|
+
<div class="bullet ${cls}">${a.status === 'COMPLETED' ? '✓' : a.status === 'EXPIRED' ? '×' : ''}</div>
|
|
273
|
+
<div class="task-body">
|
|
274
|
+
<div class="task-title">${esc(a.title)}</div>
|
|
275
|
+
<div class="task-sub">${esc(dailySub(a))}</div>
|
|
276
|
+
</div>
|
|
277
|
+
${open ? `<div class="task-ops">
|
|
278
|
+
<button class="btn btn-go" data-id="${esc(a.id)}" data-op="complete">完成</button>
|
|
279
|
+
<button class="btn" data-id="${esc(a.id)}" data-op="skip">跳过</button>
|
|
280
|
+
</div>` : ''}
|
|
281
|
+
</div>`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function dailySub(a) {
|
|
285
|
+
if (a.status === 'COMPLETED') return '已完成';
|
|
286
|
+
if (a.status === 'EXPIRED') return '已过期(不会累到明天)';
|
|
287
|
+
if (a.status === 'SKIPPED') return '已跳过';
|
|
288
|
+
return `${a.estimatedMinutes ? '约 ' + fmtMin(a.estimatedMinutes) + ' · ' : ''}${a.basis ? esc(a.basis) : '每日维护'}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isOpen(a) {
|
|
292
|
+
return !['COMPLETED', 'SKIPPED', 'CANCELLED', 'EXPIRED'].includes(a.status);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function statusLabel(s) {
|
|
296
|
+
return { DRAFT: '待确认', PLANNED: '待进行', IN_PROGRESS: '进行中', COMPLETED: '已完成',
|
|
297
|
+
SKIPPED: '已跳过', DELAYED: '已延期', PENDING_REPLAN: '待重排', EXPIRED: '已过期', CANCELLED: '已取消' }[s] ?? s;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function domainLabel(d) {
|
|
301
|
+
return { LEARN: '学习', BUILD: '构建', TRAIN: '训练', HABIT: '习惯', CREATE: '创作',
|
|
302
|
+
RELATIONSHIP: '关系', EXPLORE: '探索', LIFE: '生活', OTHER: '其他' }[d] ?? d;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** 领域标签:OTHER 不显示(避免"其他"这种噪音标签) */
|
|
306
|
+
function chipDomain(d) {
|
|
307
|
+
if (!d || d === 'OTHER') return '';
|
|
308
|
+
return `<span class="chip chip-cyan" style="font-size:10.5px">${esc(domainLabel(d))}</span>`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function modeLabel(m) {
|
|
312
|
+
return { NORMAL: '常规', BUSY: '忙碌', RECOVERY: '恢复', TRAVEL: '在外', FOCUS: '冲刺', CUSTOM: '自定义' }[m] ?? m;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function bindOps() {
|
|
316
|
+
app.querySelectorAll('.btn[data-op]').forEach((btn) => {
|
|
317
|
+
btn.addEventListener('click', async () => {
|
|
318
|
+
const id = btn.dataset.id;
|
|
319
|
+
const op = btn.dataset.op;
|
|
320
|
+
let reason = '';
|
|
321
|
+
if (op !== 'complete') {
|
|
322
|
+
reason = prompt(op === 'skip' ? '跳过原因(可留空)' : '延期原因(可留空)') ?? '';
|
|
323
|
+
}
|
|
324
|
+
btn.disabled = true;
|
|
325
|
+
await api(`/api/action/${id}/${op}`, {
|
|
326
|
+
method: 'POST',
|
|
327
|
+
headers: { 'Content-Type': 'application/json' },
|
|
328
|
+
body: JSON.stringify({ reason }),
|
|
329
|
+
});
|
|
330
|
+
render();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------- 主线页
|
|
336
|
+
|
|
337
|
+
async function renderGoals() {
|
|
338
|
+
const r = await api('/api/goals');
|
|
339
|
+
const goals = r.data ?? [];
|
|
340
|
+
if (!goals.length) {
|
|
341
|
+
app.innerHTML = '<div class="card hl-violet"><div class="empty">还没有 Goal</div></div>';
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
app.innerHTML = `<div class="grid">${goals.map((g) => `
|
|
345
|
+
<div class="card ${g.status === 'ACTIVE' ? 'hl-violet' : ''} col-6">
|
|
346
|
+
<div class="card-head">${esc(goalStatusLabel(g.status))}<span class="rule"></span>
|
|
347
|
+
<span class="chip chip-${g.status === 'ACTIVE' ? 'violet' : ''}">${g.stages?.length ?? 0} 个阶段</span></div>
|
|
348
|
+
<div class="quest-title" style="font-size:17px">${esc(g.title)}</div>
|
|
349
|
+
<div class="field"><div class="field-k">为什么</div><div class="field-v">${esc(g.why)}</div></div>
|
|
350
|
+
<div class="field"><div class="field-k">期望结果</div><div class="field-v">${esc(g.desiredOutcome)}</div></div>
|
|
351
|
+
<div class="field"><div class="field-k">成功标准</div><div class="field-v">
|
|
352
|
+
${(g.successCriteria ?? []).map((s) => `· ${esc(s)}`).join('<br>')}</div></div>
|
|
353
|
+
${g.targetDate ? `<div class="quest-meta"><span class="chip">目标日期 ${esc(g.targetDate)}</span>${g.dailyTimeBudgetMinutes ? `<span class="chip">每天 ${g.dailyTimeBudgetMinutes} 分钟</span>` : ''}</div>` : ''}
|
|
354
|
+
<div style="margin-top:16px">${renderStageTrack(g.stages ?? [])}</div>
|
|
355
|
+
<div style="margin-top:14px">
|
|
356
|
+
${(g.stages ?? []).map((s) => `
|
|
357
|
+
<div class="task">
|
|
358
|
+
<div class="bullet ${s.status === 'COMPLETED' ? 'done' : s.status === 'ACTIVE' ? 'main' : ''}">${s.status === 'COMPLETED' ? '✓' : ''}</div>
|
|
359
|
+
<div class="task-body">
|
|
360
|
+
<div class="task-title">${s.order}. ${esc(s.title)}</div>
|
|
361
|
+
<div class="task-sub">${esc(s.objective ?? '')}
|
|
362
|
+
${s.plannedStartDate || s.plannedEndDate ? `<br>窗口 ${esc(s.plannedStartDate ?? '?')} → ${esc(s.plannedEndDate ?? '?')}` : ''}</div>
|
|
363
|
+
</div>
|
|
364
|
+
<span class="chip">${esc(stageStatusLabel(s.status))}</span>
|
|
365
|
+
</div>`).join('')}
|
|
366
|
+
</div>
|
|
367
|
+
</div>`).join('')}</div>`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function goalStatusLabel(s) {
|
|
371
|
+
return { DRAFT: '草案', CONFIRMED: '已确认', ACTIVE: '进行中', PAUSED: '已暂停', COMPLETED: '已完成', ABANDONED: '已放弃' }[s] ?? s;
|
|
372
|
+
}
|
|
373
|
+
function stageStatusLabel(s) {
|
|
374
|
+
return { PLANNED: '未开始', ACTIVE: '进行中', COMPLETED: '已完成', DELAYED: '已顺延', SKIPPED: '已跳过' }[s] ?? s;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------- 记录页
|
|
378
|
+
|
|
379
|
+
async function renderTimeline() {
|
|
380
|
+
const r = await api('/api/timeline');
|
|
381
|
+
const d = r.data ?? {};
|
|
382
|
+
state.todayKey = d.today ?? state.todayKey;
|
|
383
|
+
const events = d.events ?? [];
|
|
384
|
+
const evidence = d.evidence ?? [];
|
|
385
|
+
app.innerHTML = `
|
|
386
|
+
<div class="grid">
|
|
387
|
+
<div class="card hl-violet col-3">
|
|
388
|
+
<div class="card-head">成长记录<span class="rule"></span><span class="chip">${events.length}</span></div>
|
|
389
|
+
${events.length ? events.map((e) => `
|
|
390
|
+
<div class="entry">
|
|
391
|
+
<div class="entry-date">${esc(relDate(e.dateKey))}</div>
|
|
392
|
+
<div><div class="entry-title">${esc(e.title)}</div>
|
|
393
|
+
<div class="entry-sub">${chipDomain(e.domain)}${(e.evidenceIds?.length ?? e.evidenceCount) ? `<span class="chip" style="font-size:10.5px">${e.evidenceIds?.length ?? e.evidenceCount} 条证据</span>` : ''}</div>
|
|
394
|
+
${e.description ? `<div class="entry-sub">${esc(e.description)}</div>` : ''}</div>
|
|
395
|
+
</div>`).join('')
|
|
396
|
+
: '<div class="empty">还没有成长记录</div>'}
|
|
397
|
+
</div>
|
|
398
|
+
<div class="card hl-cyan col-3">
|
|
399
|
+
<div class="card-head">证据流<span class="rule"></span><span class="chip">${evidence.length}</span></div>
|
|
400
|
+
${evidence.length ? evidence.map((e) => `
|
|
401
|
+
<div class="entry">
|
|
402
|
+
<div class="entry-date">${esc(relDate(e.dateKey))}</div>
|
|
403
|
+
<div><div class="entry-title" style="font-weight:500">${esc(clip(e.content, 120))}</div>
|
|
404
|
+
<div class="entry-sub">
|
|
405
|
+
<span class="chip" style="font-size:10.5px">${esc(evidenceTypeLabel(e.type))}</span>
|
|
406
|
+
<span class="chip chip-${strengthTone(e.strength)}" style="font-size:10.5px">${esc(strengthLabel(e.strength))}</span>
|
|
407
|
+
</div></div>
|
|
408
|
+
</div>`).join('')
|
|
409
|
+
: '<div class="empty">还没有证据</div>'}
|
|
410
|
+
</div>
|
|
411
|
+
</div>`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function clip(s, n) { return String(s ?? '').length > n ? String(s).slice(0, n) + '…' : String(s ?? ''); }
|
|
415
|
+
function evidenceTypeLabel(t) {
|
|
416
|
+
return { TEXT: '文字', FILE: '文件', GIT: '代码提交', METRIC: '客观数据', QUIZ: '考核',
|
|
417
|
+
ARTIFACT: '产出物', REFLECTION: '反思', MANUAL: '手动' }[t] ?? t;
|
|
418
|
+
}
|
|
419
|
+
function strengthLabel(s) {
|
|
420
|
+
return { AUTO_VERIFIED: '自动验证', USER_CONFIRMED: '本人确认', AGENT_OBSERVED: 'Agent 观察', UNVERIFIED: '未验证' }[s] ?? s;
|
|
421
|
+
}
|
|
422
|
+
function strengthTone(s) {
|
|
423
|
+
return { AUTO_VERIFIED: 'green', USER_CONFIRMED: 'cyan', AGENT_OBSERVED: 'amber', UNVERIFIED: '' }[s] ?? '';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ---------------------------------------------------------------- 护照页
|
|
427
|
+
|
|
428
|
+
async function renderPassport() {
|
|
429
|
+
const r = await api('/api/passport');
|
|
430
|
+
const d = r.data ?? {};
|
|
431
|
+
const p = d.snapshot;
|
|
432
|
+
app.innerHTML = `
|
|
433
|
+
<div class="grid">
|
|
434
|
+
<div class="card hl-cyan col-6">
|
|
435
|
+
<div class="card-head">AI Passport<span class="rule"></span>
|
|
436
|
+
${d.dirty ? '<span class="chip chip-amber">待同步</span>' : '<span class="chip chip-green">已同步</span>'}</div>
|
|
437
|
+
${p ? `
|
|
438
|
+
<div class="kv"><div class="kv-k">生成时间</div><div>${esc(new Date(d.generatedAt).toLocaleString('zh-CN'))}</div></div>
|
|
439
|
+
<div class="kv"><div class="kv-k">当前章节</div><div>${esc(p.currentState?.currentChapter ?? '—')}</div></div>
|
|
440
|
+
<div class="kv"><div class="kv-k">活跃目标</div><div>${(p.currentState?.activeGoals ?? []).map((g) => esc(g.title)).join('、') || '—'}</div></div>
|
|
441
|
+
<div class="kv"><div class="kv-k">生活情境</div><div>${esc(p.currentState?.lifeContext ?? '常规')}</div></div>
|
|
442
|
+
<div class="kv"><div class="kv-k">能力证据</div><div>${(p.capabilities ?? []).map((c) => esc(`${c.topic}(${c.level})`)).join('、') || '—'}</div></div>
|
|
443
|
+
<div style="margin-top:14px"><div class="field-k">原始快照</div>
|
|
444
|
+
<pre class="json">${esc(JSON.stringify(p, null, 2))}</pre></div>`
|
|
445
|
+
: '<div class="empty">还没有生成过 Passport。<br><span style="font-size:12px">完成第一个真实任务后会自动生成</span></div>'}
|
|
446
|
+
</div>
|
|
447
|
+
</div>`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------- 设置页
|
|
451
|
+
|
|
452
|
+
async function renderSettings() {
|
|
453
|
+
const r = await api('/api/settings');
|
|
454
|
+
const d = r.data ?? {};
|
|
455
|
+
app.innerHTML = `
|
|
456
|
+
<div class="grid">
|
|
457
|
+
<div class="card hl-cyan col-3">
|
|
458
|
+
<div class="card-head">设置<span class="rule"></span></div>
|
|
459
|
+
<form class="form" id="settings-form">
|
|
460
|
+
<div><label>时区(决定"哪一天"怎么算)</label>
|
|
461
|
+
<input name="timezone" value="${esc(d.timezone)}"></div>
|
|
462
|
+
<div><label>提醒渠道</label>
|
|
463
|
+
<select name="reminderChannel">
|
|
464
|
+
<option value="macos-local" ${d.reminderChannel === 'macos-local' ? 'selected' : ''}>系统通知</option>
|
|
465
|
+
<option value="none" ${d.reminderChannel === 'none' ? 'selected' : ''}>关闭提醒</option>
|
|
466
|
+
</select></div>
|
|
467
|
+
<div><button class="btn btn-go" type="submit">保存</button></div>
|
|
468
|
+
</form>
|
|
469
|
+
<div class="ops"><button class="btn" id="run-rollover">手动执行跨天检查</button></div>
|
|
470
|
+
<div class="field"><div class="field-v" style="font-size:12px;color:var(--ink-faint)">
|
|
471
|
+
跨天检查会把昨天没完成的每日任务标记为过期(不会累到明天),未完成的主线转入待重排。</div></div>
|
|
472
|
+
</div>
|
|
473
|
+
<div class="card col-3">
|
|
474
|
+
<div class="card-head">关于<span class="rule"></span></div>
|
|
475
|
+
<div class="kv"><div class="kv-k">数据位置</div><div>~/.ai-growth/growth.db</div></div>
|
|
476
|
+
<div class="kv"><div class="kv-k">访问范围</div><div>仅本机 127.0.0.1(无账号体系)</div></div>
|
|
477
|
+
<div class="kv"><div class="kv-k">紧凑面板</div><div><code style="color:var(--cyan)">?layout=widget</code></div></div>
|
|
478
|
+
<div class="field"><div class="field-v" style="font-size:12px;color:var(--ink-faint)">
|
|
479
|
+
面板不显示"掌握度百分比""自律分"这类东西——它们没有可信计算依据。
|
|
480
|
+
进度只用可核对事实表达:阶段序号、证据条数、成长记录条数。</div></div>
|
|
481
|
+
</div>
|
|
482
|
+
</div>`;
|
|
483
|
+
document.getElementById('settings-form').addEventListener('submit', async (e) => {
|
|
484
|
+
e.preventDefault();
|
|
485
|
+
const fd = new FormData(e.target);
|
|
486
|
+
await api('/api/settings', {
|
|
487
|
+
method: 'POST',
|
|
488
|
+
headers: { 'Content-Type': 'application/json' },
|
|
489
|
+
body: JSON.stringify({ timezone: fd.get('timezone'), reminderChannel: fd.get('reminderChannel') }),
|
|
490
|
+
});
|
|
491
|
+
render();
|
|
492
|
+
});
|
|
493
|
+
document.getElementById('run-rollover').addEventListener('click', async () => {
|
|
494
|
+
await api('/api/rollover', { method: 'POST' });
|
|
495
|
+
render();
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// 初始页由 URL hash 决定(支持 ?layout=widget#goals 这类组合)
|
|
500
|
+
setPage(location.hash.slice(1) || 'today', { push: false });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AI Growth · 面板</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="hud">
|
|
11
|
+
|
|
12
|
+
<header class="topbar">
|
|
13
|
+
<div class="brand">
|
|
14
|
+
<span class="brand-mark" aria-hidden="true"></span>
|
|
15
|
+
<span class="brand-text">AI GROWTH</span>
|
|
16
|
+
</div>
|
|
17
|
+
<nav class="tabs" id="tabs">
|
|
18
|
+
<button class="tab is-active" data-page="today">面板</button>
|
|
19
|
+
<button class="tab" data-page="goals">主线</button>
|
|
20
|
+
<button class="tab" data-page="timeline">记录</button>
|
|
21
|
+
<button class="tab" data-page="passport">护照</button>
|
|
22
|
+
<button class="tab" data-page="settings">设置</button>
|
|
23
|
+
</nav>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<main id="app"><div class="boot">加载中…</div></main>
|
|
27
|
+
|
|
28
|
+
<footer class="statusbar">
|
|
29
|
+
<span class="dot" id="link-dot"></span>
|
|
30
|
+
<span id="status-text">连接中</span>
|
|
31
|
+
<span class="spacer"></span>
|
|
32
|
+
<span id="status-date"></span>
|
|
33
|
+
</footer>
|
|
34
|
+
|
|
35
|
+
</div>
|
|
36
|
+
<script src="/app.js"></script>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|