agim-cli 1.2.71 → 1.2.72
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/CHANGELOG.md +37 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +16 -41
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/web/public/_app.js +0 -248
- package/dist/web/public/memos.html +0 -352
- package/dist/web/public/reminders.html +0 -332
- package/dist/web/public/settings.html +0 -2488
- package/dist/web/public/tasks.html +0 -3724
|
@@ -1,332 +0,0 @@
|
|
|
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>Agim — Reminders</title>
|
|
7
|
-
<script src="/_app.js"></script>
|
|
8
|
-
<script>
|
|
9
|
-
// i18n bootstrap — mirrors tasks.html. _app.js reads window.__lang.
|
|
10
|
-
(function () {
|
|
11
|
-
const LANGS = { en: 'en', zh: 'zh' };
|
|
12
|
-
const savedLang = localStorage.getItem('im-hub-lang');
|
|
13
|
-
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
|
|
14
|
-
window.__lang = savedLang && LANGS[savedLang] ? savedLang : browserLang;
|
|
15
|
-
document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
|
|
16
|
-
const T = {
|
|
17
|
-
en: {
|
|
18
|
-
title: 'Agim — Reminders', h1: '🔔 Reminders',
|
|
19
|
-
backToChat: '↩ Chat', toTasks: 'Tasks', toMemos: 'Memos', toSettings: 'Settings',
|
|
20
|
-
statusPending: 'Pending', statusFired: 'Fired', statusCancelled: 'Cancelled', statusFailed: 'Failed',
|
|
21
|
-
loading: 'Loading…',
|
|
22
|
-
loadFailed: 'Load failed: {err}',
|
|
23
|
-
emptyStatus: 'No {status} reminders.',
|
|
24
|
-
cancel: 'Cancel', snooze: 'Snooze +5min', delete: 'Delete',
|
|
25
|
-
fireAt: 'Fires at', recur: 'Repeat',
|
|
26
|
-
literalText: 'literal',
|
|
27
|
-
confirmCancel: 'Cancel reminder #{id}? (Recurring reminders will stop the whole loop.)',
|
|
28
|
-
cancelled: '✅ Cancelled #{id}',
|
|
29
|
-
snoozed: '⏰ Snoozed #{id} +5min',
|
|
30
|
-
},
|
|
31
|
-
zh: {
|
|
32
|
-
title: 'Agim — 提醒', h1: '🔔 提醒',
|
|
33
|
-
backToChat: '↩ 对话', toTasks: '任务', toMemos: '备忘', toSettings: '设置',
|
|
34
|
-
statusPending: '待发', statusFired: '已发', statusCancelled: '已取消', statusFailed: '失败',
|
|
35
|
-
loading: '加载中…',
|
|
36
|
-
loadFailed: '加载失败:{err}',
|
|
37
|
-
emptyStatus: '没有{status}的提醒',
|
|
38
|
-
cancel: '取消', snooze: '延 5 分钟', delete: '删除',
|
|
39
|
-
fireAt: '触发时间', recur: '重复',
|
|
40
|
-
literalText: '字面文本',
|
|
41
|
-
confirmCancel: '取消提醒 #{id}?(循环提醒会终止整条循环)',
|
|
42
|
-
cancelled: '✅ 已取消 #{id}',
|
|
43
|
-
snoozed: '⏰ 已延后 #{id} 5 分钟',
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
window.__t = T[window.__lang];
|
|
47
|
-
document.title = T[window.__lang].title;
|
|
48
|
-
})();
|
|
49
|
-
</script>
|
|
50
|
-
<style>
|
|
51
|
-
:root {
|
|
52
|
-
--bg: #fafafa; --fg: #222; --muted: #666; --border: #e5e7eb;
|
|
53
|
-
--card: #fff; --accent: #2563eb; --danger: #dc2626; --warn: #d97706;
|
|
54
|
-
--recur: #7c3aed;
|
|
55
|
-
}
|
|
56
|
-
[data-theme="dark"] {
|
|
57
|
-
--bg: #1a1a1a; --fg: #e5e5e5; --muted: #999; --border: #333;
|
|
58
|
-
--card: #242424; --accent: #60a5fa; --danger: #f87171; --warn: #fbbf24;
|
|
59
|
-
--recur: #a78bfa;
|
|
60
|
-
}
|
|
61
|
-
body {
|
|
62
|
-
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif;
|
|
63
|
-
background: var(--bg); color: var(--fg);
|
|
64
|
-
margin: 0; padding: 0;
|
|
65
|
-
}
|
|
66
|
-
/* Header — kept in sync with tasks.html so all dashboard pages share
|
|
67
|
-
the same layout. Flex with h1 flex:1 to push toggles + nav to the
|
|
68
|
-
right; uniform border-button look across <a> and <button>. */
|
|
69
|
-
header {
|
|
70
|
-
display: flex;
|
|
71
|
-
align-items: center;
|
|
72
|
-
gap: 16px;
|
|
73
|
-
padding: 14px 24px;
|
|
74
|
-
border-bottom: 1px solid var(--border);
|
|
75
|
-
background: var(--card);
|
|
76
|
-
}
|
|
77
|
-
header h1 { margin: 0; font-size: 18px; font-weight: 600; flex: 1; }
|
|
78
|
-
header a, header button, header select {
|
|
79
|
-
color: var(--accent);
|
|
80
|
-
text-decoration: none;
|
|
81
|
-
font-size: 14px;
|
|
82
|
-
background: none;
|
|
83
|
-
border: 1px solid var(--border);
|
|
84
|
-
padding: 6px 12px;
|
|
85
|
-
border-radius: 4px;
|
|
86
|
-
cursor: pointer;
|
|
87
|
-
}
|
|
88
|
-
header select { color: var(--fg); }
|
|
89
|
-
header a:hover, header button:hover { border-color: var(--accent); }
|
|
90
|
-
main { max-width: 880px; margin: 24px auto; padding: 0 16px; }
|
|
91
|
-
.filters {
|
|
92
|
-
display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;
|
|
93
|
-
}
|
|
94
|
-
.filter-btn {
|
|
95
|
-
padding: 6px 12px; border: 1px solid var(--border); border-radius: 6px;
|
|
96
|
-
background: var(--card); color: var(--fg); cursor: pointer;
|
|
97
|
-
font-size: 13px;
|
|
98
|
-
}
|
|
99
|
-
.filter-btn.active {
|
|
100
|
-
background: var(--accent); color: #fff; border-color: var(--accent);
|
|
101
|
-
}
|
|
102
|
-
.empty {
|
|
103
|
-
text-align: center; color: var(--muted); padding: 60px 20px;
|
|
104
|
-
background: var(--card); border-radius: 8px; border: 1px dashed var(--border);
|
|
105
|
-
}
|
|
106
|
-
.reminder {
|
|
107
|
-
display: flex; align-items: flex-start; gap: 12px;
|
|
108
|
-
padding: 14px 16px; margin-bottom: 8px;
|
|
109
|
-
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
|
110
|
-
}
|
|
111
|
-
.icon { font-size: 20px; flex-shrink: 0; line-height: 1; padding-top: 1px; }
|
|
112
|
-
.body { flex: 1; min-width: 0; }
|
|
113
|
-
.text { font-weight: 500; word-break: break-word; }
|
|
114
|
-
.meta {
|
|
115
|
-
color: var(--muted); font-size: 12px; margin-top: 4px;
|
|
116
|
-
display: flex; gap: 12px; flex-wrap: wrap;
|
|
117
|
-
}
|
|
118
|
-
.meta .recur { color: var(--recur); }
|
|
119
|
-
.meta .literal { color: var(--warn); }
|
|
120
|
-
.actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
121
|
-
.btn {
|
|
122
|
-
padding: 5px 10px; border: 1px solid var(--border); border-radius: 5px;
|
|
123
|
-
background: transparent; color: var(--fg); cursor: pointer;
|
|
124
|
-
font-size: 12px;
|
|
125
|
-
}
|
|
126
|
-
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
127
|
-
.btn.danger:hover { border-color: var(--danger); color: var(--danger); }
|
|
128
|
-
.toast {
|
|
129
|
-
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
|
130
|
-
background: var(--card); border: 1px solid var(--border); padding: 10px 16px;
|
|
131
|
-
border-radius: 6px; font-size: 13px;
|
|
132
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
133
|
-
transition: opacity 0.2s;
|
|
134
|
-
}
|
|
135
|
-
.toast.error { color: var(--danger); border-color: var(--danger); }
|
|
136
|
-
@media (max-width: 600px) {
|
|
137
|
-
.reminder { flex-direction: column; }
|
|
138
|
-
.actions { width: 100%; justify-content: flex-end; }
|
|
139
|
-
}
|
|
140
|
-
</style>
|
|
141
|
-
</head>
|
|
142
|
-
<body>
|
|
143
|
-
<header>
|
|
144
|
-
<h1 id="page-title"></h1>
|
|
145
|
-
<button id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
|
|
146
|
-
<select id="langSelect" title="Language / 语言">
|
|
147
|
-
<option value="en">EN</option>
|
|
148
|
-
<option value="zh">中文</option>
|
|
149
|
-
</select>
|
|
150
|
-
<a href="/" id="lnk-chat"></a>
|
|
151
|
-
<a href="/tasks" id="lnk-tasks"></a>
|
|
152
|
-
<a href="/memos" id="lnk-memos"></a>
|
|
153
|
-
<a href="/settings" id="lnk-settings"></a>
|
|
154
|
-
</header>
|
|
155
|
-
<main>
|
|
156
|
-
<div class="filters" id="filters">
|
|
157
|
-
<button type="button" class="filter-btn active" data-status="pending" id="btn-status-pending"></button>
|
|
158
|
-
<button type="button" class="filter-btn" data-status="fired" id="btn-status-fired"></button>
|
|
159
|
-
<button type="button" class="filter-btn" data-status="cancelled" id="btn-status-cancelled"></button>
|
|
160
|
-
<button type="button" class="filter-btn" data-status="failed" id="btn-status-failed"></button>
|
|
161
|
-
</div>
|
|
162
|
-
<div id="list"></div>
|
|
163
|
-
</main>
|
|
164
|
-
<div id="toast" class="toast" style="display:none"></div>
|
|
165
|
-
|
|
166
|
-
<script>
|
|
167
|
-
(() => {
|
|
168
|
-
const T = window.__t;
|
|
169
|
-
const $list = document.getElementById('list')
|
|
170
|
-
const $toast = document.getElementById('toast')
|
|
171
|
-
const $filters = document.getElementById('filters')
|
|
172
|
-
let currentStatus = 'pending'
|
|
173
|
-
|
|
174
|
-
// i18n bind — header labels, status filter labels, lang switcher.
|
|
175
|
-
document.getElementById('page-title').textContent = T.h1;
|
|
176
|
-
document.getElementById('lnk-chat').textContent = T.backToChat;
|
|
177
|
-
document.getElementById('lnk-tasks').textContent = T.toTasks;
|
|
178
|
-
document.getElementById('lnk-memos').textContent = T.toMemos;
|
|
179
|
-
document.getElementById('lnk-settings').textContent = T.toSettings;
|
|
180
|
-
document.getElementById('btn-status-pending').textContent = T.statusPending;
|
|
181
|
-
document.getElementById('btn-status-fired').textContent = T.statusFired;
|
|
182
|
-
document.getElementById('btn-status-cancelled').textContent = T.statusCancelled;
|
|
183
|
-
document.getElementById('btn-status-failed').textContent = T.statusFailed;
|
|
184
|
-
if (window.imhub) imhub.theme.bindToggle(document.getElementById('theme-toggle'));
|
|
185
|
-
(function setupLangSwitcher() {
|
|
186
|
-
const sel = document.getElementById('langSelect');
|
|
187
|
-
if (!sel) return;
|
|
188
|
-
sel.value = window.__lang;
|
|
189
|
-
sel.addEventListener('change', () => {
|
|
190
|
-
if (sel.value === window.__lang) return;
|
|
191
|
-
localStorage.setItem('im-hub-lang', sel.value);
|
|
192
|
-
window.location.reload();
|
|
193
|
-
});
|
|
194
|
-
})();
|
|
195
|
-
|
|
196
|
-
function toast(msg, isErr = false) {
|
|
197
|
-
$toast.textContent = msg
|
|
198
|
-
$toast.className = `toast${isErr ? ' error' : ''}`
|
|
199
|
-
$toast.style.display = 'block'
|
|
200
|
-
setTimeout(() => { $toast.style.display = 'none' }, 2500)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function fmtTime(s) {
|
|
204
|
-
if (!s) return '-'
|
|
205
|
-
// Normalize SQLite "YYYY-MM-DD HH:MM:SS" to RFC3339 — Safari/WebView
|
|
206
|
-
// reject the space-separated form (Invalid Date).
|
|
207
|
-
let iso = String(s).trim()
|
|
208
|
-
if (iso && !iso.includes('T')) iso = iso.replace(' ', 'T')
|
|
209
|
-
if (iso && !/[Z+]/.test(iso.slice(-6))) iso = iso + 'Z'
|
|
210
|
-
const d = new Date(iso)
|
|
211
|
-
if (Number.isNaN(d.getTime())) return s
|
|
212
|
-
const isZh = window.__lang === 'zh'
|
|
213
|
-
const now = new Date()
|
|
214
|
-
const diffMs = d.getTime() - now.getTime()
|
|
215
|
-
const absMin = Math.abs(Math.round(diffMs / 60000))
|
|
216
|
-
const past = diffMs <= 0
|
|
217
|
-
let rel = ''
|
|
218
|
-
if (Math.abs(diffMs) < 60_000) rel = isZh ? '刚才' : 'just now'
|
|
219
|
-
else if (absMin < 60) rel = isZh
|
|
220
|
-
? `${past ? '已过 ' : ''}${absMin} 分钟${past ? '' : '后'}`
|
|
221
|
-
: `${absMin} min ${past ? 'ago' : 'from now'}`
|
|
222
|
-
else if (absMin < 1440) rel = isZh
|
|
223
|
-
? `${past ? '已过 ' : ''}${(absMin / 60).toFixed(1)} 小时${past ? '' : '后'}`
|
|
224
|
-
: `${(absMin / 60).toFixed(1)} h ${past ? 'ago' : 'from now'}`
|
|
225
|
-
else rel = isZh
|
|
226
|
-
? `${past ? '已过 ' : ''}${(absMin / 1440).toFixed(1)} 天${past ? '' : '后'}`
|
|
227
|
-
: `${(absMin / 1440).toFixed(1)} d ${past ? 'ago' : 'from now'}`
|
|
228
|
-
return `${d.toLocaleString()} · ${rel}`
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function load() {
|
|
232
|
-
$list.innerHTML = `<div class="empty">${T.loading}</div>`
|
|
233
|
-
try {
|
|
234
|
-
const data = await window.imhub.api(`/api/reminders?status=${encodeURIComponent(currentStatus)}`)
|
|
235
|
-
render(data.reminders || [])
|
|
236
|
-
} catch (err) {
|
|
237
|
-
$list.innerHTML = ''
|
|
238
|
-
toast(T.loadFailed.replace('{err}', err.message), true)
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function render(items) {
|
|
243
|
-
if (!items.length) {
|
|
244
|
-
$list.innerHTML = `<div class="empty">${T.emptyStatus.replace('{status}', statusLabel(currentStatus))}</div>`
|
|
245
|
-
return
|
|
246
|
-
}
|
|
247
|
-
$list.innerHTML = items.map((r) => {
|
|
248
|
-
const icon = r.recurrence ? '🔁' : '🔔'
|
|
249
|
-
const recurMeta = r.recurrence_label
|
|
250
|
-
? `<span class="recur">↻ ${escapeHtml(r.recurrence_label)}</span>` : ''
|
|
251
|
-
const literalMeta = r.prompt_mode === 'literal'
|
|
252
|
-
? `<span class="literal">${T.literalText}</span>` : ''
|
|
253
|
-
const platformMeta = r.platform
|
|
254
|
-
? `<span>${escapeHtml(r.platform)}</span>` : ''
|
|
255
|
-
const showActions = currentStatus === 'pending'
|
|
256
|
-
const actions = showActions
|
|
257
|
-
? `<div class="actions">
|
|
258
|
-
<button class="btn" data-act="snooze" data-id="${r.id}">${T.snooze}</button>
|
|
259
|
-
<button class="btn danger" data-act="cancel" data-id="${r.id}">${T.cancel}</button>
|
|
260
|
-
</div>`
|
|
261
|
-
: ''
|
|
262
|
-
return `<div class="reminder">
|
|
263
|
-
<div class="icon">${icon}</div>
|
|
264
|
-
<div class="body">
|
|
265
|
-
<div class="text">${escapeHtml(r.text)}</div>
|
|
266
|
-
<div class="meta">
|
|
267
|
-
<span>#${r.id}</span>
|
|
268
|
-
<span>${escapeHtml(fmtTime(r.fire_at))}</span>
|
|
269
|
-
${recurMeta}
|
|
270
|
-
${literalMeta}
|
|
271
|
-
${platformMeta}
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
${actions}
|
|
275
|
-
</div>`
|
|
276
|
-
}).join('')
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function escapeHtml(s) {
|
|
280
|
-
return String(s).replace(/[&<>"']/g, (c) => (
|
|
281
|
-
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] || c
|
|
282
|
-
))
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function statusLabel(s) {
|
|
286
|
-
return ({
|
|
287
|
-
pending: T.statusPending, fired: T.statusFired,
|
|
288
|
-
cancelled: T.statusCancelled, failed: T.statusFailed,
|
|
289
|
-
})[s] || s
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
$filters.addEventListener('click', (e) => {
|
|
293
|
-
const t = e.target.closest('.filter-btn')
|
|
294
|
-
if (!t) return
|
|
295
|
-
currentStatus = t.dataset.status
|
|
296
|
-
document.querySelectorAll('.filter-btn').forEach((b) => { b.classList.toggle('active', b === t) })
|
|
297
|
-
load()
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
$list.addEventListener('click', async (e) => {
|
|
301
|
-
const btn = e.target.closest('button[data-act]')
|
|
302
|
-
if (!btn) return
|
|
303
|
-
const id = btn.dataset.id
|
|
304
|
-
const act = btn.dataset.act
|
|
305
|
-
btn.disabled = true
|
|
306
|
-
try {
|
|
307
|
-
if (act === 'cancel') {
|
|
308
|
-
if (!confirm(T.confirmCancel.replace('{id}', id))) return
|
|
309
|
-
await window.imhub.api(`/api/reminders/${id}/cancel`, { method: 'POST' })
|
|
310
|
-
toast(T.cancelled.replace('{id}', id))
|
|
311
|
-
} else if (act === 'snooze') {
|
|
312
|
-
await window.imhub.api(`/api/reminders/${id}/snooze`, {
|
|
313
|
-
method: 'POST',
|
|
314
|
-
body: JSON.stringify({ duration: '5m' }),
|
|
315
|
-
})
|
|
316
|
-
toast(T.snoozed.replace('{id}', id))
|
|
317
|
-
}
|
|
318
|
-
await load()
|
|
319
|
-
} catch (err) {
|
|
320
|
-
toast(T.loadFailed.replace('{err}', err.message), true)
|
|
321
|
-
} finally {
|
|
322
|
-
btn.disabled = false
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
// Initial load + auto-refresh every 30s
|
|
327
|
-
load()
|
|
328
|
-
setInterval(load, 30_000)
|
|
329
|
-
})()
|
|
330
|
-
</script>
|
|
331
|
-
</body>
|
|
332
|
-
</html>
|