agim-cli 1.2.70 → 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 +62 -0
- package/dist/web/public/assets/{a2a-CzOCrC9J.js → a2a-BbtW9q1L.js} +2 -2
- package/dist/web/public/assets/{a2a-CzOCrC9J.js.map → a2a-BbtW9q1L.js.map} +1 -1
- package/dist/web/public/assets/{activity-BlasCFZq.js → activity-CJmQLZ6_.js} +2 -2
- package/dist/web/public/assets/{activity-BlasCFZq.js.map → activity-CJmQLZ6_.js.map} +1 -1
- package/dist/web/public/assets/{admins-CQOShfRl.js → admins-BJP9koxS.js} +2 -2
- package/dist/web/public/assets/{admins-CQOShfRl.js.map → admins-BJP9koxS.js.map} +1 -1
- package/dist/web/public/assets/{agents-UstP4DoK.js → agents-DC0XFC6e.js} +2 -2
- package/dist/web/public/assets/{agents-UstP4DoK.js.map → agents-DC0XFC6e.js.map} +1 -1
- package/dist/web/public/assets/{approvals-BvfcdYLU.js → approvals-DiPkpPN4.js} +2 -2
- package/dist/web/public/assets/{approvals-BvfcdYLU.js.map → approvals-DiPkpPN4.js.map} +1 -1
- package/dist/web/public/assets/{asks-DAJ1edZu.js → asks-CG9chQgT.js} +2 -2
- package/dist/web/public/assets/{asks-DAJ1edZu.js.map → asks-CG9chQgT.js.map} +1 -1
- package/dist/web/public/assets/{audit-CAOadRDl.js → audit-B40mxlW1.js} +2 -2
- package/dist/web/public/assets/{audit-CAOadRDl.js.map → audit-B40mxlW1.js.map} +1 -1
- package/dist/web/public/assets/{bell-BuZoWBMK.js → bell-BC8Jqt9f.js} +2 -2
- package/dist/web/public/assets/{bell-BuZoWBMK.js.map → bell-BC8Jqt9f.js.map} +1 -1
- package/dist/web/public/assets/{bgjobs-C5jF3BWG.js → bgjobs-ClrQ2jzx.js} +2 -2
- package/dist/web/public/assets/{bgjobs-C5jF3BWG.js.map → bgjobs-ClrQ2jzx.js.map} +1 -1
- package/dist/web/public/assets/{brain-DaFGi4GZ.js → brain-BBx7RF2j.js} +2 -2
- package/dist/web/public/assets/{brain-DaFGi4GZ.js.map → brain-BBx7RF2j.js.map} +1 -1
- package/dist/web/public/assets/{briefcase-BXKiYFAz.js → briefcase-TihsRkSy.js} +2 -2
- package/dist/web/public/assets/{briefcase-BXKiYFAz.js.map → briefcase-TihsRkSy.js.map} +1 -1
- package/dist/web/public/assets/{chevron-right-Bi2RmsHt.js → chevron-right-DTLZLXDP.js} +2 -2
- package/dist/web/public/assets/{chevron-right-Bi2RmsHt.js.map → chevron-right-DTLZLXDP.js.map} +1 -1
- package/dist/web/public/assets/{circle-check-NYJSuZMK.js → circle-check-EHM2OMD5.js} +2 -2
- package/dist/web/public/assets/{circle-check-NYJSuZMK.js.map → circle-check-EHM2OMD5.js.map} +1 -1
- package/dist/web/public/assets/{circle-check-big-wxQV9Apf.js → circle-check-big-DJXNAZ3u.js} +2 -2
- package/dist/web/public/assets/{circle-check-big-wxQV9Apf.js.map → circle-check-big-DJXNAZ3u.js.map} +1 -1
- package/dist/web/public/assets/{circle-x-D7EpTdJQ.js → circle-x-DsaHSb_C.js} +2 -2
- package/dist/web/public/assets/{circle-x-D7EpTdJQ.js.map → circle-x-DsaHSb_C.js.map} +1 -1
- package/dist/web/public/assets/{confirm-dialog-BecM1NAT.js → confirm-dialog-DezSK6wB.js} +2 -2
- package/dist/web/public/assets/{confirm-dialog-BecM1NAT.js.map → confirm-dialog-DezSK6wB.js.map} +1 -1
- package/dist/web/public/assets/{data-table-Bh5TFhXw.js → data-table-DdSnbgyt.js} +2 -2
- package/dist/web/public/assets/{data-table-Bh5TFhXw.js.map → data-table-DdSnbgyt.js.map} +1 -1
- package/dist/web/public/assets/{dialog-8m5Uihhi.js → dialog-45x-p0sR.js} +2 -2
- package/dist/web/public/assets/{dialog-8m5Uihhi.js.map → dialog-45x-p0sR.js.map} +1 -1
- package/dist/web/public/assets/{download-DWvNllY2.js → download-BL4jeu4M.js} +2 -2
- package/dist/web/public/assets/{download-DWvNllY2.js.map → download-BL4jeu4M.js.map} +1 -1
- package/dist/web/public/assets/{email-Cr1x3fdt.js → email-BGUO-gme.js} +2 -2
- package/dist/web/public/assets/{email-Cr1x3fdt.js.map → email-BGUO-gme.js.map} +1 -1
- package/dist/web/public/assets/{empty-state-BESUGVnV.js → empty-state-n-LfHo6N.js} +2 -2
- package/dist/web/public/assets/{empty-state-BESUGVnV.js.map → empty-state-n-LfHo6N.js.map} +1 -1
- package/dist/web/public/assets/{external-link-BwscsXby.js → external-link-CZd6ma6Y.js} +2 -2
- package/dist/web/public/assets/{external-link-BwscsXby.js.map → external-link-CZd6ma6Y.js.map} +1 -1
- package/dist/web/public/assets/{eye-C7gaSIZv.js → eye-LsErAgqh.js} +2 -2
- package/dist/web/public/assets/{eye-C7gaSIZv.js.map → eye-LsErAgqh.js.map} +1 -1
- package/dist/web/public/assets/{facts-B9hm9627.js → facts-CxZ3L_7a.js} +2 -2
- package/dist/web/public/assets/{facts-B9hm9627.js.map → facts-CxZ3L_7a.js.map} +1 -1
- package/dist/web/public/assets/{goals-D3f1Dw2M.js → goals-BjzgqVnW.js} +2 -2
- package/dist/web/public/assets/{goals-D3f1Dw2M.js.map → goals-BjzgqVnW.js.map} +1 -1
- package/dist/web/public/assets/{health-BLwrXebg.js → health-BYX5IK98.js} +2 -2
- package/dist/web/public/assets/{health-BLwrXebg.js.map → health-BYX5IK98.js.map} +1 -1
- package/dist/web/public/assets/{heart-pulse-BtioxtCU.js → heart-pulse-D_xUK9r0.js} +2 -2
- package/dist/web/public/assets/{heart-pulse-BtioxtCU.js.map → heart-pulse-D_xUK9r0.js.map} +1 -1
- package/dist/web/public/assets/{heartbeat-BHPx1z33.js → heartbeat-DnSegIYS.js} +2 -2
- package/dist/web/public/assets/{heartbeat-BHPx1z33.js.map → heartbeat-DnSegIYS.js.map} +1 -1
- package/dist/web/public/assets/{hot-CuaGMYzV.js → hot-C5TxCTVL.js} +2 -2
- package/dist/web/public/assets/{hot-CuaGMYzV.js.map → hot-C5TxCTVL.js.map} +1 -1
- package/dist/web/public/assets/{index-Cc8hxJWT.js → index-CbWhcThC.js} +3 -3
- package/dist/web/public/assets/{index-Cc8hxJWT.js.map → index-CbWhcThC.js.map} +1 -1
- package/dist/web/public/assets/{installed-D_BoGzel.js → installed-D_Hjs7uj.js} +2 -2
- package/dist/web/public/assets/{installed-D_BoGzel.js.map → installed-D_Hjs7uj.js.map} +1 -1
- package/dist/web/public/assets/{jobs-D0eeD9ye.js → jobs-DWtjJIAD.js} +2 -2
- package/dist/web/public/assets/{jobs-D0eeD9ye.js.map → jobs-DWtjJIAD.js.map} +1 -1
- package/dist/web/public/assets/{layout-DPxfAgBW.js → layout-54-PzplU.js} +2 -2
- package/dist/web/public/assets/{layout-DPxfAgBW.js.map → layout-54-PzplU.js.map} +1 -1
- package/dist/web/public/assets/{layout-bRkGiGF-.js → layout-BLK9XuQB.js} +2 -2
- package/dist/web/public/assets/{layout-bRkGiGF-.js.map → layout-BLK9XuQB.js.map} +1 -1
- package/dist/web/public/assets/{layout-CvRW3Bn_.js → layout-CTyy2kgf.js} +2 -2
- package/dist/web/public/assets/{layout-CvRW3Bn_.js.map → layout-CTyy2kgf.js.map} +1 -1
- package/dist/web/public/assets/{layout-Wl7Pr1-s.js → layout-CvvZcaiO.js} +2 -2
- package/dist/web/public/assets/{layout-Wl7Pr1-s.js.map → layout-CvvZcaiO.js.map} +1 -1
- package/dist/web/public/assets/{layout-CQe9ncYF.js → layout-D1-LnIA_.js} +2 -2
- package/dist/web/public/assets/{layout-CQe9ncYF.js.map → layout-D1-LnIA_.js.map} +1 -1
- package/dist/web/public/assets/{llm-CJGxq9Ru.js → llm-D59Tv6ne.js} +2 -2
- package/dist/web/public/assets/{llm-CJGxq9Ru.js.map → llm-D59Tv6ne.js.map} +1 -1
- package/dist/web/public/assets/{loader-circle-D4WrM89N.js → loader-circle-U64eR4W2.js} +2 -2
- package/dist/web/public/assets/{loader-circle-D4WrM89N.js.map → loader-circle-U64eR4W2.js.map} +1 -1
- package/dist/web/public/assets/{map-pin-DGSzw_UH.js → map-pin-D08YlPs3.js} +2 -2
- package/dist/web/public/assets/{map-pin-DGSzw_UH.js.map → map-pin-D08YlPs3.js.map} +1 -1
- package/dist/web/public/assets/{mcp-DYp7QbX8.js → mcp-Br9fBIWu.js} +2 -2
- package/dist/web/public/assets/{mcp-DYp7QbX8.js.map → mcp-Br9fBIWu.js.map} +1 -1
- package/dist/web/public/assets/{memos-Dz0jD4R5.js → memos-nZKV3SLq.js} +2 -2
- package/dist/web/public/assets/{memos-Dz0jD4R5.js.map → memos-nZKV3SLq.js.map} +1 -1
- package/dist/web/public/assets/messengers-C8ss4WVx.js +7 -0
- package/dist/web/public/assets/messengers-C8ss4WVx.js.map +1 -0
- package/dist/web/public/assets/{native-agent-ChgK3ceB.js → native-agent-BiiYK91m.js} +2 -2
- package/dist/web/public/assets/{native-agent-ChgK3ceB.js.map → native-agent-BiiYK91m.js.map} +1 -1
- package/dist/web/public/assets/{network-D8uD8vuE.js → network-D9rhTHz5.js} +2 -2
- package/dist/web/public/assets/{network-D8uD8vuE.js.map → network-D9rhTHz5.js.map} +1 -1
- package/dist/web/public/assets/{outbox-CRcmD2P1.js → outbox-BS_gla4W.js} +2 -2
- package/dist/web/public/assets/{outbox-CRcmD2P1.js.map → outbox-BS_gla4W.js.map} +1 -1
- package/dist/web/public/assets/{pagination-BK8pXuOn.js → pagination-CSuYj-hv.js} +2 -2
- package/dist/web/public/assets/{pagination-BK8pXuOn.js.map → pagination-CSuYj-hv.js.map} +1 -1
- package/dist/web/public/assets/{persona-Dk4Y24TF.js → persona-BByKd8Zd.js} +2 -2
- package/dist/web/public/assets/{persona-Dk4Y24TF.js.map → persona-BByKd8Zd.js.map} +1 -1
- package/dist/web/public/assets/{play-BAwY0kZY.js → play-B_11jKz_.js} +2 -2
- package/dist/web/public/assets/{play-BAwY0kZY.js.map → play-B_11jKz_.js.map} +1 -1
- package/dist/web/public/assets/{plus-Qg7H_hBC.js → plus-BxhCliGV.js} +2 -2
- package/dist/web/public/assets/{plus-Qg7H_hBC.js.map → plus-BxhCliGV.js.map} +1 -1
- package/dist/web/public/assets/{policy-bd1_X4KT.js → policy-CHbUM0MU.js} +2 -2
- package/dist/web/public/assets/{policy-bd1_X4KT.js.map → policy-CHbUM0MU.js.map} +1 -1
- package/dist/web/public/assets/{refresh-ccw-DjeeSzbz.js → refresh-ccw-BKw5Agez.js} +2 -2
- package/dist/web/public/assets/{refresh-ccw-DjeeSzbz.js.map → refresh-ccw-BKw5Agez.js.map} +1 -1
- package/dist/web/public/assets/{reminders-CyNYrPCo.js → reminders-_MsAKlS7.js} +2 -2
- package/dist/web/public/assets/{reminders-CyNYrPCo.js.map → reminders-_MsAKlS7.js.map} +1 -1
- package/dist/web/public/assets/{save-B6_ssx8c.js → save-DznSJZGN.js} +2 -2
- package/dist/web/public/assets/{save-B6_ssx8c.js.map → save-DznSJZGN.js.map} +1 -1
- package/dist/web/public/assets/{schedules-DVxENvba.js → schedules-CiRZXonB.js} +2 -2
- package/dist/web/public/assets/{schedules-DVxENvba.js.map → schedules-CiRZXonB.js.map} +1 -1
- package/dist/web/public/assets/{search-BJwBDf0Q.js → search-CXr_r7L8.js} +2 -2
- package/dist/web/public/assets/{search-BJwBDf0Q.js.map → search-CXr_r7L8.js.map} +1 -1
- package/dist/web/public/assets/{security-Co1823U6.js → security-CjySIq-k.js} +2 -2
- package/dist/web/public/assets/{security-Co1823U6.js.map → security-CjySIq-k.js.map} +1 -1
- package/dist/web/public/assets/{service-BJ2kE446.js → service-LXlgJooI.js} +2 -2
- package/dist/web/public/assets/{service-BJ2kE446.js.map → service-LXlgJooI.js.map} +1 -1
- package/dist/web/public/assets/{status-badge-3nnQTZXz.js → status-badge-BsPU9NHK.js} +2 -2
- package/dist/web/public/assets/{status-badge-3nnQTZXz.js.map → status-badge-BsPU9NHK.js.map} +1 -1
- package/dist/web/public/assets/{subtasks-BgHm-5dB.js → subtasks-DM8tJkC7.js} +2 -2
- package/dist/web/public/assets/{subtasks-BgHm-5dB.js.map → subtasks-DM8tJkC7.js.map} +1 -1
- package/dist/web/public/assets/{table-CNXI0HD-.js → table-CQbO75bn.js} +2 -2
- package/dist/web/public/assets/{table-CNXI0HD-.js.map → table-CQbO75bn.js.map} +1 -1
- package/dist/web/public/assets/{topn-C1PyG8Ns.js → topn-CaCYQ1NI.js} +2 -2
- package/dist/web/public/assets/{topn-C1PyG8Ns.js.map → topn-CaCYQ1NI.js.map} +1 -1
- package/dist/web/public/assets/{trash-2-CjkttCxU.js → trash-2-CdgBbY1Z.js} +2 -2
- package/dist/web/public/assets/{trash-2-CjkttCxU.js.map → trash-2-CdgBbY1Z.js.map} +1 -1
- package/dist/web/public/assets/{use-background-tasks-CLf78_yn.js → use-background-tasks-DUDxcbbl.js} +2 -2
- package/dist/web/public/assets/{use-background-tasks-CLf78_yn.js.map → use-background-tasks-DUDxcbbl.js.map} +1 -1
- package/dist/web/public/assets/{use-llm-admin-CzL0zcIi.js → use-llm-admin-Bpy7nDu-.js} +2 -2
- package/dist/web/public/assets/{use-llm-admin-CzL0zcIi.js.map → use-llm-admin-Bpy7nDu-.js.map} +1 -1
- package/dist/web/public/assets/{use-memory-DrJjdPYo.js → use-memory-B00AYXi0.js} +2 -2
- package/dist/web/public/assets/{use-memory-DrJjdPYo.js.map → use-memory-B00AYXi0.js.map} +1 -1
- package/dist/web/public/assets/{use-observability-1j7-vMy5.js → use-observability-7ETfQyEP.js} +2 -2
- package/dist/web/public/assets/{use-observability-1j7-vMy5.js.map → use-observability-7ETfQyEP.js.map} +1 -1
- package/dist/web/public/assets/{use-settings-BLcy2mrv.js → use-settings-tzqyzhw3.js} +2 -2
- package/dist/web/public/assets/{use-settings-BLcy2mrv.js.map → use-settings-tzqyzhw3.js.map} +1 -1
- package/dist/web/public/assets/{use-workspace-BLXvLTDF.js → use-workspace-B5gbK4s5.js} +2 -2
- package/dist/web/public/assets/{use-workspace-BLXvLTDF.js.map → use-workspace-B5gbK4s5.js.map} +1 -1
- package/dist/web/public/assets/{useQuery-xB5dCeu-.js → useQuery-wnQifGeZ.js} +2 -2
- package/dist/web/public/assets/{useQuery-xB5dCeu-.js.map → useQuery-wnQifGeZ.js.map} +1 -1
- package/dist/web/public/assets/{vector-EOjQsr7r.js → vector-or1-MfRX.js} +2 -2
- package/dist/web/public/assets/{vector-EOjQsr7r.js.map → vector-or1-MfRX.js.map} +1 -1
- package/dist/web/public/assets/{viewer-D_I2ikzf.js → viewer-C0f5b5eT.js} +2 -2
- package/dist/web/public/assets/{viewer-D_I2ikzf.js.map → viewer-C0f5b5eT.js.map} +1 -1
- package/dist/web/public/assets/{workspace-lMCzYJAB.js → workspace-CGE46-O2.js} +2 -2
- package/dist/web/public/assets/{workspace-lMCzYJAB.js.map → workspace-CGE46-O2.js.map} +1 -1
- package/dist/web/public/assets/{workspaces-Ciyjiowm.js → workspaces-CqDIcIXo.js} +2 -2
- package/dist/web/public/assets/{workspaces-Ciyjiowm.js.map → workspaces-CqDIcIXo.js.map} +1 -1
- package/dist/web/public/assets/{x-DP0VnoG9.js → x-DBZPbfu3.js} +2 -2
- package/dist/web/public/assets/{x-DP0VnoG9.js.map → x-DBZPbfu3.js.map} +1 -1
- package/dist/web/public/index.html +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +21 -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/assets/messengers-Bewhby2_.js +0 -7
- package/dist/web/public/assets/messengers-Bewhby2_.js.map +0 -1
- 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,352 +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 — Memos</title>
|
|
7
|
-
<script src="/_app.js"></script>
|
|
8
|
-
<script>
|
|
9
|
-
(function () {
|
|
10
|
-
const LANGS = { en: 'en', zh: 'zh' };
|
|
11
|
-
const savedLang = localStorage.getItem('im-hub-lang');
|
|
12
|
-
const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
|
|
13
|
-
window.__lang = savedLang && LANGS[savedLang] ? savedLang : browserLang;
|
|
14
|
-
document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
|
|
15
|
-
const T = {
|
|
16
|
-
en: {
|
|
17
|
-
title: 'Agim — Memos', h1: '📋 Memos',
|
|
18
|
-
backToChat: '↩ Chat', toTasks: 'Tasks', toReminders: 'Reminders', toSettings: 'Settings',
|
|
19
|
-
filterAll: 'All', filterGeo: 'With location', filterExpired: 'Expired',
|
|
20
|
-
searchPlaceholder: 'search (what / memo / who / address …)',
|
|
21
|
-
loading: 'Loading…',
|
|
22
|
-
loadFailed: 'Load failed: {err}',
|
|
23
|
-
emptyNoMatch: 'No memos match your search.',
|
|
24
|
-
emptyNone: 'No memos yet. Just tell an agent in IM "记下…" / "remember that…" and they will save it.',
|
|
25
|
-
delete: 'Delete', edit: 'Edit',
|
|
26
|
-
confirmDelete: 'Really delete memo #{id}?',
|
|
27
|
-
deleted: 'Deleted #{id}',
|
|
28
|
-
deleteFailed: 'Delete failed: {err}',
|
|
29
|
-
mapBaidu: 'Baidu Map', mapAmap: 'Amap', mapGoogle: 'Google Maps',
|
|
30
|
-
},
|
|
31
|
-
zh: {
|
|
32
|
-
title: 'Agim — 备忘', h1: '📋 备忘',
|
|
33
|
-
backToChat: '↩ 对话', toTasks: '任务', toReminders: '提醒', toSettings: '设置',
|
|
34
|
-
filterAll: '全部', filterGeo: '有位置', filterExpired: '已过期',
|
|
35
|
-
searchPlaceholder: '搜索内容(what / memo / who / 地址 …)',
|
|
36
|
-
loading: '加载中…',
|
|
37
|
-
loadFailed: '加载失败:{err}',
|
|
38
|
-
emptyNoMatch: '没匹配的 memo',
|
|
39
|
-
emptyNone: '还没记下任何 memo。在 IM 里说「记下…」让 agent 帮你存。',
|
|
40
|
-
delete: '删除', edit: '编辑',
|
|
41
|
-
confirmDelete: '确定删除 memo #{id}?',
|
|
42
|
-
deleted: '已删除 #{id}',
|
|
43
|
-
deleteFailed: '删除失败:{err}',
|
|
44
|
-
mapBaidu: '百度地图', mapAmap: '高德地图', mapGoogle: 'Google',
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
window.__t = T[window.__lang];
|
|
48
|
-
document.title = T[window.__lang].title;
|
|
49
|
-
})();
|
|
50
|
-
</script>
|
|
51
|
-
<style>
|
|
52
|
-
:root {
|
|
53
|
-
--bg: #fafafa; --fg: #222; --muted: #666; --border: #e5e7eb;
|
|
54
|
-
--card: #fff; --accent: #2563eb; --danger: #dc2626; --warn: #d97706;
|
|
55
|
-
--geo: #059669;
|
|
56
|
-
}
|
|
57
|
-
[data-theme="dark"] {
|
|
58
|
-
--bg: #1a1a1a; --fg: #e5e5e5; --muted: #999; --border: #333;
|
|
59
|
-
--card: #242424; --accent: #60a5fa; --danger: #f87171; --warn: #fbbf24;
|
|
60
|
-
--geo: #34d399;
|
|
61
|
-
}
|
|
62
|
-
body {
|
|
63
|
-
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif;
|
|
64
|
-
background: var(--bg); color: var(--fg); margin: 0; padding: 0;
|
|
65
|
-
}
|
|
66
|
-
/* Header — kept in sync with tasks.html / reminders.html so all
|
|
67
|
-
dashboard pages share the same layout. */
|
|
68
|
-
header {
|
|
69
|
-
display: flex;
|
|
70
|
-
align-items: center;
|
|
71
|
-
gap: 16px;
|
|
72
|
-
padding: 14px 24px;
|
|
73
|
-
border-bottom: 1px solid var(--border);
|
|
74
|
-
background: var(--card);
|
|
75
|
-
}
|
|
76
|
-
header h1 { margin: 0; font-size: 18px; font-weight: 600; flex: 1; }
|
|
77
|
-
header a, header button, header select {
|
|
78
|
-
color: var(--accent);
|
|
79
|
-
text-decoration: none;
|
|
80
|
-
font-size: 14px;
|
|
81
|
-
background: none;
|
|
82
|
-
border: 1px solid var(--border);
|
|
83
|
-
padding: 6px 12px;
|
|
84
|
-
border-radius: 4px;
|
|
85
|
-
cursor: pointer;
|
|
86
|
-
}
|
|
87
|
-
header select { color: var(--fg); }
|
|
88
|
-
header a:hover, header button:hover { border-color: var(--accent); }
|
|
89
|
-
main { padding: 20px; max-width: 980px; margin: 0 auto; }
|
|
90
|
-
.toolbar {
|
|
91
|
-
display: flex; gap: 8px; margin-bottom: 18px; flex-wrap: wrap;
|
|
92
|
-
align-items: center;
|
|
93
|
-
}
|
|
94
|
-
.toolbar input[type="text"] {
|
|
95
|
-
flex: 1; min-width: 200px;
|
|
96
|
-
padding: 7px 12px; border: 1px solid var(--border); border-radius: 6px;
|
|
97
|
-
background: var(--card); color: var(--fg); font-size: 14px;
|
|
98
|
-
}
|
|
99
|
-
.toolbar input[type="text"]:focus { outline: none; border-color: var(--accent); }
|
|
100
|
-
.filter-btn {
|
|
101
|
-
padding: 6px 12px; border: 1px solid var(--border); border-radius: 6px;
|
|
102
|
-
background: transparent; color: var(--muted); cursor: pointer;
|
|
103
|
-
font-size: 13px; transition: all 0.15s;
|
|
104
|
-
}
|
|
105
|
-
.filter-btn:hover { color: var(--fg); border-color: var(--fg); }
|
|
106
|
-
.filter-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
|
|
107
|
-
.stats { color: var(--muted); font-size: 12px; }
|
|
108
|
-
.memo {
|
|
109
|
-
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
|
110
|
-
padding: 12px 16px; margin-bottom: 10px;
|
|
111
|
-
display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
|
|
112
|
-
}
|
|
113
|
-
.memo .body { flex: 1; min-width: 0; }
|
|
114
|
-
.memo .what {
|
|
115
|
-
font-weight: 600; margin-bottom: 4px;
|
|
116
|
-
display: flex; align-items: center; gap: 8px;
|
|
117
|
-
}
|
|
118
|
-
.memo .icon { font-size: 16px; }
|
|
119
|
-
.memo .id { color: var(--muted); font-size: 11px; font-weight: normal; }
|
|
120
|
-
.memo .original {
|
|
121
|
-
color: var(--muted); margin-bottom: 6px; font-size: 13px;
|
|
122
|
-
border-left: 2px solid var(--border); padding-left: 8px;
|
|
123
|
-
}
|
|
124
|
-
.memo .meta {
|
|
125
|
-
color: var(--muted); font-size: 12px; margin-top: 4px;
|
|
126
|
-
display: flex; gap: 12px; flex-wrap: wrap;
|
|
127
|
-
}
|
|
128
|
-
.memo .meta .who::before { content: '👤 '; }
|
|
129
|
-
.memo .meta .when::before { content: '🗓 '; }
|
|
130
|
-
.memo .meta .geo { color: var(--geo); }
|
|
131
|
-
.memo .meta .geo::before { content: '📍 '; }
|
|
132
|
-
.memo .meta .platform { opacity: 0.7; }
|
|
133
|
-
.memo .meta .expires { color: var(--warn); }
|
|
134
|
-
.memo .meta .expires::before { content: '⏳ '; }
|
|
135
|
-
.memo .maps { margin-top: 6px; font-size: 12px; display: flex; gap: 12px; flex-wrap: wrap; }
|
|
136
|
-
.memo .maps a { color: var(--accent); text-decoration: none; }
|
|
137
|
-
.memo .maps a:hover { text-decoration: underline; }
|
|
138
|
-
.actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
139
|
-
.btn {
|
|
140
|
-
padding: 5px 10px; border: 1px solid var(--border); border-radius: 5px;
|
|
141
|
-
background: transparent; color: var(--fg); cursor: pointer; font-size: 12px;
|
|
142
|
-
}
|
|
143
|
-
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
144
|
-
.btn.danger:hover { border-color: var(--danger); color: var(--danger); }
|
|
145
|
-
.empty { color: var(--muted); text-align: center; padding: 60px 0; }
|
|
146
|
-
.toast {
|
|
147
|
-
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
|
148
|
-
background: var(--card); border: 1px solid var(--border); padding: 10px 16px;
|
|
149
|
-
border-radius: 6px; font-size: 13px;
|
|
150
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
151
|
-
transition: opacity 0.2s;
|
|
152
|
-
}
|
|
153
|
-
.toast.error { color: var(--danger); border-color: var(--danger); }
|
|
154
|
-
@media (max-width: 600px) {
|
|
155
|
-
.memo { flex-direction: column; }
|
|
156
|
-
.actions { width: 100%; justify-content: flex-end; }
|
|
157
|
-
}
|
|
158
|
-
</style>
|
|
159
|
-
</head>
|
|
160
|
-
<body>
|
|
161
|
-
<header>
|
|
162
|
-
<h1 id="page-title"></h1>
|
|
163
|
-
<button id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
|
|
164
|
-
<select id="langSelect" title="Language / 语言">
|
|
165
|
-
<option value="en">EN</option>
|
|
166
|
-
<option value="zh">中文</option>
|
|
167
|
-
</select>
|
|
168
|
-
<a href="/" id="lnk-chat"></a>
|
|
169
|
-
<a href="/tasks" id="lnk-tasks"></a>
|
|
170
|
-
<a href="/reminders" id="lnk-reminders"></a>
|
|
171
|
-
<a href="/settings" id="lnk-settings"></a>
|
|
172
|
-
</header>
|
|
173
|
-
<main>
|
|
174
|
-
<div class="toolbar">
|
|
175
|
-
<input type="text" id="q" />
|
|
176
|
-
<button type="button" class="filter-btn active" data-filter="all" id="btn-filter-all"></button>
|
|
177
|
-
<button type="button" class="filter-btn" data-filter="geo" id="btn-filter-geo"></button>
|
|
178
|
-
<button type="button" class="filter-btn" data-filter="expired" id="btn-filter-expired"></button>
|
|
179
|
-
<span class="stats" id="stats"></span>
|
|
180
|
-
</div>
|
|
181
|
-
<div id="list"></div>
|
|
182
|
-
</main>
|
|
183
|
-
<div id="toast" class="toast" style="display:none"></div>
|
|
184
|
-
|
|
185
|
-
<script>
|
|
186
|
-
(() => {
|
|
187
|
-
const T = window.__t;
|
|
188
|
-
const $list = document.getElementById('list')
|
|
189
|
-
const $toast = document.getElementById('toast')
|
|
190
|
-
const $q = document.getElementById('q')
|
|
191
|
-
const $stats = document.getElementById('stats')
|
|
192
|
-
let currentFilter = 'all'
|
|
193
|
-
let queryStr = ''
|
|
194
|
-
let debounceTimer = null
|
|
195
|
-
|
|
196
|
-
// i18n bind
|
|
197
|
-
document.getElementById('page-title').textContent = T.h1;
|
|
198
|
-
document.getElementById('lnk-chat').textContent = T.backToChat;
|
|
199
|
-
document.getElementById('lnk-tasks').textContent = T.toTasks;
|
|
200
|
-
document.getElementById('lnk-reminders').textContent = T.toReminders;
|
|
201
|
-
document.getElementById('lnk-settings').textContent = T.toSettings;
|
|
202
|
-
document.getElementById('btn-filter-all').textContent = T.filterAll;
|
|
203
|
-
document.getElementById('btn-filter-geo').textContent = T.filterGeo;
|
|
204
|
-
document.getElementById('btn-filter-expired').textContent = T.filterExpired;
|
|
205
|
-
$q.placeholder = T.searchPlaceholder;
|
|
206
|
-
if (window.imhub) imhub.theme.bindToggle(document.getElementById('theme-toggle'));
|
|
207
|
-
(function setupLangSwitcher() {
|
|
208
|
-
const sel = document.getElementById('langSelect');
|
|
209
|
-
if (!sel) return;
|
|
210
|
-
sel.value = window.__lang;
|
|
211
|
-
sel.addEventListener('change', () => {
|
|
212
|
-
if (sel.value === window.__lang) return;
|
|
213
|
-
localStorage.setItem('im-hub-lang', sel.value);
|
|
214
|
-
window.location.reload();
|
|
215
|
-
});
|
|
216
|
-
})();
|
|
217
|
-
|
|
218
|
-
function toast(msg, isErr = false) {
|
|
219
|
-
$toast.textContent = msg
|
|
220
|
-
$toast.className = 'toast' + (isErr ? ' error' : '')
|
|
221
|
-
$toast.style.display = 'block'
|
|
222
|
-
setTimeout(() => { $toast.style.display = 'none' }, 2400)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function escapeHtml(s) {
|
|
226
|
-
if (s == null) return ''
|
|
227
|
-
return String(s)
|
|
228
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
229
|
-
.replace(/"/g, '"').replace(/'/g, ''')
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function fmtTime(iso) {
|
|
233
|
-
if (!iso) return ''
|
|
234
|
-
// memos use 'YYYY-MM-DD HH:MM:SS' (UTC+8 bare-local).
|
|
235
|
-
const d = new Date(iso.replace(' ', 'T') + '+08:00')
|
|
236
|
-
if (Number.isNaN(d.getTime())) return iso
|
|
237
|
-
const isZh = window.__lang === 'zh'
|
|
238
|
-
const now = new Date()
|
|
239
|
-
const diffMs = now.getTime() - d.getTime()
|
|
240
|
-
const absMin = Math.abs(Math.round(diffMs / 60000))
|
|
241
|
-
let rel = ''
|
|
242
|
-
if (Math.abs(diffMs) < 60_000) rel = isZh ? '刚才' : 'just now'
|
|
243
|
-
else if (absMin < 60) rel = isZh ? `${absMin} 分钟前` : `${absMin} min ago`
|
|
244
|
-
else if (absMin < 1440) rel = isZh ? `${(absMin / 60).toFixed(1)} 小时前` : `${(absMin / 60).toFixed(1)} h ago`
|
|
245
|
-
else rel = isZh ? `${(absMin / 1440).toFixed(1)} 天前` : `${(absMin / 1440).toFixed(1)} d ago`
|
|
246
|
-
return `${d.toLocaleString()} · ${rel}`
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function load() {
|
|
250
|
-
$list.innerHTML = `<div class="empty">${T.loading}</div>`
|
|
251
|
-
try {
|
|
252
|
-
const params = new URLSearchParams()
|
|
253
|
-
if (queryStr) params.set('query', queryStr)
|
|
254
|
-
if (currentFilter === 'geo') params.set('has_location', 'true')
|
|
255
|
-
if (currentFilter === 'expired') params.set('include_expired', 'true')
|
|
256
|
-
params.set('limit', '200')
|
|
257
|
-
const data = await window.imhub.api(`/api/memos?${params.toString()}`)
|
|
258
|
-
let items = data.memos || []
|
|
259
|
-
if (currentFilter === 'expired') {
|
|
260
|
-
// Only the rows that have an expires_at in the past.
|
|
261
|
-
const now = new Date()
|
|
262
|
-
items = items.filter(m => m.expiresAt && new Date(m.expiresAt.replace(' ', 'T') + '+08:00') < now)
|
|
263
|
-
}
|
|
264
|
-
render(items)
|
|
265
|
-
$stats.textContent = items.length ? (window.__lang === 'zh' ? `${items.length} 条` : `${items.length} memos`) : ''
|
|
266
|
-
} catch (err) {
|
|
267
|
-
$list.innerHTML = ''
|
|
268
|
-
toast(T.loadFailed.replace('{err}', err.message), true)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function render(items) {
|
|
273
|
-
if (!items.length) {
|
|
274
|
-
$list.innerHTML = `<div class="empty">${queryStr ? T.emptyNoMatch : T.emptyNone}</div>`
|
|
275
|
-
return
|
|
276
|
-
}
|
|
277
|
-
$list.innerHTML = items.map(m => {
|
|
278
|
-
const icon = m.where_lat != null ? '📍' : '📝'
|
|
279
|
-
const idLbl = `<span class="id">#${m.id}</span>`
|
|
280
|
-
const what = `<div class="what"><span class="icon">${icon}</span>${escapeHtml(m.what)} ${idLbl}</div>`
|
|
281
|
-
const original = (m.memo && m.memo !== m.what)
|
|
282
|
-
? `<div class="original">${escapeHtml(m.memo)}</div>` : ''
|
|
283
|
-
const metaBits = []
|
|
284
|
-
if (m.who) metaBits.push(`<span class="who">${escapeHtml(m.who)}</span>`)
|
|
285
|
-
if (m.whenAt) metaBits.push(`<span class="when">${escapeHtml(m.whenAt)}</span>`)
|
|
286
|
-
else if (m.whenText) metaBits.push(`<span class="when">${escapeHtml(m.whenText)}</span>`)
|
|
287
|
-
if (m.where_label) metaBits.push(`<span class="geo">${escapeHtml(m.where_label)}</span>`)
|
|
288
|
-
else if (m.where_lat != null) metaBits.push(`<span class="geo">${m.where_lat.toFixed(5)}, ${m.where_lng.toFixed(5)}</span>`)
|
|
289
|
-
if (m.platform) metaBits.push(`<span class="platform">${escapeHtml(m.platform)}</span>`)
|
|
290
|
-
if (m.expiresAt) metaBits.push(`<span class="expires">${escapeHtml(m.expiresAt)}</span>`)
|
|
291
|
-
metaBits.push(`<span class="when">${fmtTime(m.createdAt)}</span>`)
|
|
292
|
-
const meta = `<div class="meta">${metaBits.join('')}</div>`
|
|
293
|
-
|
|
294
|
-
let maps = ''
|
|
295
|
-
if (m.mapUrls) {
|
|
296
|
-
maps = `<div class="maps">
|
|
297
|
-
<a href="${m.mapUrls.baidu}" target="_blank">${T.mapBaidu}</a>
|
|
298
|
-
<a href="${m.mapUrls.amap}" target="_blank">${T.mapAmap}</a>
|
|
299
|
-
<a href="${m.mapUrls.google}" target="_blank">${T.mapGoogle}</a>
|
|
300
|
-
</div>`
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return `<div class="memo" data-id="${m.id}">
|
|
304
|
-
<div class="body">${what}${original}${meta}${maps}</div>
|
|
305
|
-
<div class="actions">
|
|
306
|
-
<button type="button" class="btn danger" data-action="delete">${T.delete}</button>
|
|
307
|
-
</div>
|
|
308
|
-
</div>`
|
|
309
|
-
}).join('')
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
$list.addEventListener('click', async (e) => {
|
|
313
|
-
const btn = e.target.closest('button[data-action]')
|
|
314
|
-
if (!btn) return
|
|
315
|
-
const card = btn.closest('.memo')
|
|
316
|
-
if (!card) return
|
|
317
|
-
const id = card.dataset.id
|
|
318
|
-
const action = btn.dataset.action
|
|
319
|
-
if (action === 'delete') {
|
|
320
|
-
if (!confirm(T.confirmDelete.replace('{id}', id))) return
|
|
321
|
-
try {
|
|
322
|
-
await window.imhub.api(`/api/memos/${id}`, { method: 'DELETE' })
|
|
323
|
-
toast(T.deleted.replace('{id}', id))
|
|
324
|
-
load()
|
|
325
|
-
} catch (err) { toast(T.deleteFailed.replace('{err}', err.message), true) }
|
|
326
|
-
}
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
330
|
-
btn.addEventListener('click', () => {
|
|
331
|
-
document.querySelectorAll('.filter-btn').forEach(b => { b.classList.remove('active') })
|
|
332
|
-
btn.classList.add('active')
|
|
333
|
-
currentFilter = btn.dataset.filter
|
|
334
|
-
load()
|
|
335
|
-
})
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
$q.addEventListener('input', () => {
|
|
339
|
-
clearTimeout(debounceTimer)
|
|
340
|
-
debounceTimer = setTimeout(() => {
|
|
341
|
-
queryStr = $q.value.trim()
|
|
342
|
-
load()
|
|
343
|
-
}, 280)
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
load()
|
|
347
|
-
// Auto-refresh every 30s so newly-saved memos show up.
|
|
348
|
-
setInterval(load, 30_000)
|
|
349
|
-
})()
|
|
350
|
-
</script>
|
|
351
|
-
</body>
|
|
352
|
-
</html>
|
|
@@ -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>
|