agim-cli 1.1.3 → 1.1.5

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.
@@ -5,6 +5,48 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Agim — Reminders</title>
7
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>
8
50
  <style>
9
51
  :root {
10
52
  --bg: #fafafa; --fg: #222; --muted: #666; --border: #e5e7eb;
@@ -21,17 +63,30 @@
21
63
  background: var(--bg); color: var(--fg);
22
64
  margin: 0; padding: 0;
23
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>. */
24
69
  header {
25
- display: flex; align-items: center; justify-content: space-between;
26
- padding: 14px 20px; border-bottom: 1px solid var(--border);
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 16px;
73
+ padding: 14px 24px;
74
+ border-bottom: 1px solid var(--border);
27
75
  background: var(--card);
28
76
  }
29
- header h1 { margin: 0; font-size: 18px; font-weight: 600; }
30
- header nav a {
31
- margin-left: 12px; color: var(--muted); text-decoration: none;
32
- font-size: 13px;
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;
33
87
  }
34
- header nav a:hover { color: var(--accent); }
88
+ header select { color: var(--fg); }
89
+ header a:hover, header button:hover { border-color: var(--accent); }
35
90
  main { max-width: 880px; margin: 24px auto; padding: 0 16px; }
36
91
  .filters {
37
92
  display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;
@@ -86,20 +141,23 @@
86
141
  </head>
87
142
  <body>
88
143
  <header>
89
- <h1>🔔 Reminders</h1>
90
- <nav>
91
- <a href="/">Chat</a>
92
- <a href="/tasks">Tasks</a>
93
- <a href="/memos">Memos</a>
94
- <a href="/settings">Settings</a>
95
- </nav>
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>
96
154
  </header>
97
155
  <main>
98
156
  <div class="filters" id="filters">
99
- <button type="button" class="filter-btn active" data-status="pending">待发 (Pending)</button>
100
- <button type="button" class="filter-btn" data-status="fired">已发</button>
101
- <button type="button" class="filter-btn" data-status="cancelled">已取消</button>
102
- <button type="button" class="filter-btn" data-status="failed">失败</button>
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>
103
161
  </div>
104
162
  <div id="list"></div>
105
163
  </main>
@@ -107,11 +165,34 @@
107
165
 
108
166
  <script>
109
167
  (() => {
168
+ const T = window.__t;
110
169
  const $list = document.getElementById('list')
111
170
  const $toast = document.getElementById('toast')
112
171
  const $filters = document.getElementById('filters')
113
172
  let currentStatus = 'pending'
114
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
+
115
196
  function toast(msg, isErr = false) {
116
197
  $toast.textContent = msg
117
198
  $toast.className = `toast${isErr ? ' error' : ''}`
@@ -119,34 +200,48 @@
119
200
  setTimeout(() => { $toast.style.display = 'none' }, 2500)
120
201
  }
121
202
 
122
- function fmtTime(iso) {
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'
123
210
  const d = new Date(iso)
124
- if (Number.isNaN(d.getTime())) return iso
211
+ if (Number.isNaN(d.getTime())) return s
212
+ const isZh = window.__lang === 'zh'
125
213
  const now = new Date()
126
214
  const diffMs = d.getTime() - now.getTime()
127
215
  const absMin = Math.abs(Math.round(diffMs / 60000))
216
+ const past = diffMs <= 0
128
217
  let rel = ''
129
- if (Math.abs(diffMs) < 60_000) rel = '刚才'
130
- else if (absMin < 60) rel = `${(diffMs > 0 ? '' : '已过 ') + absMin} 分钟${diffMs > 0 ? '后' : ''}`
131
- else if (absMin < 1440) rel = `${(diffMs > 0 ? '' : '已过 ') + (absMin / 60).toFixed(1)} 小时${diffMs > 0 ? '' : ''}`
132
- else rel = `${(diffMs > 0 ? '' : '已过 ') + (absMin / 1440).toFixed(1)} 天${diffMs > 0 ? '' : ''}`
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'}`
133
228
  return `${d.toLocaleString()} · ${rel}`
134
229
  }
135
230
 
136
231
  async function load() {
137
- $list.innerHTML = '<div class="empty">加载中…</div>'
232
+ $list.innerHTML = `<div class="empty">${T.loading}</div>`
138
233
  try {
139
234
  const data = await window.imhub.api(`/api/reminders?status=${encodeURIComponent(currentStatus)}`)
140
235
  render(data.reminders || [])
141
236
  } catch (err) {
142
237
  $list.innerHTML = ''
143
- toast(`加载失败:${err.message}`, true)
238
+ toast(T.loadFailed.replace('{err}', err.message), true)
144
239
  }
145
240
  }
146
241
 
147
242
  function render(items) {
148
243
  if (!items.length) {
149
- $list.innerHTML = `<div class="empty">没有${statusLabel(currentStatus)}的提醒</div>`
244
+ $list.innerHTML = `<div class="empty">${T.emptyStatus.replace('{status}', statusLabel(currentStatus))}</div>`
150
245
  return
151
246
  }
152
247
  $list.innerHTML = items.map((r) => {
@@ -154,14 +249,14 @@
154
249
  const recurMeta = r.recurrence_label
155
250
  ? `<span class="recur">↻ ${escapeHtml(r.recurrence_label)}</span>` : ''
156
251
  const literalMeta = r.prompt_mode === 'literal'
157
- ? '<span class="literal">字面文本</span>' : ''
252
+ ? `<span class="literal">${T.literalText}</span>` : ''
158
253
  const platformMeta = r.platform
159
254
  ? `<span>${escapeHtml(r.platform)}</span>` : ''
160
255
  const showActions = currentStatus === 'pending'
161
256
  const actions = showActions
162
257
  ? `<div class="actions">
163
- <button class="btn" data-act="snooze" data-id="${r.id}">延 5 分钟</button>
164
- <button class="btn danger" data-act="cancel" data-id="${r.id}">取消</button>
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>
165
260
  </div>`
166
261
  : ''
167
262
  return `<div class="reminder">
@@ -188,7 +283,10 @@
188
283
  }
189
284
 
190
285
  function statusLabel(s) {
191
- return ({ pending: '待发', fired: '已发', cancelled: '已取消', failed: '失败' })[s] || s
286
+ return ({
287
+ pending: T.statusPending, fired: T.statusFired,
288
+ cancelled: T.statusCancelled, failed: T.statusFailed,
289
+ })[s] || s
192
290
  }
193
291
 
194
292
  $filters.addEventListener('click', (e) => {
@@ -207,19 +305,19 @@
207
305
  btn.disabled = true
208
306
  try {
209
307
  if (act === 'cancel') {
210
- if (!confirm(`取消提醒 #${id}?(循环提醒会终止整条循环)`)) return
308
+ if (!confirm(T.confirmCancel.replace('{id}', id))) return
211
309
  await window.imhub.api(`/api/reminders/${id}/cancel`, { method: 'POST' })
212
- toast(`✅ 已取消 #${id}`)
310
+ toast(T.cancelled.replace('{id}', id))
213
311
  } else if (act === 'snooze') {
214
312
  await window.imhub.api(`/api/reminders/${id}/snooze`, {
215
313
  method: 'POST',
216
314
  body: JSON.stringify({ duration: '5m' }),
217
315
  })
218
- toast(`✅ #${id} 已延后 5 分钟`)
316
+ toast(T.snoozed.replace('{id}', id))
219
317
  }
220
318
  await load()
221
319
  } catch (err) {
222
- toast(`操作失败:${err.message}`, true)
320
+ toast(T.loadFailed.replace('{err}', err.message), true)
223
321
  } finally {
224
322
  btn.disabled = false
225
323
  }