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.
@@ -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
- { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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>