@memtensor/memos-local-openclaw-plugin 0.1.3 → 0.1.4

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.
Files changed (77) hide show
  1. package/.env.example +13 -5
  2. package/README.md +177 -97
  3. package/dist/capture/index.d.ts +5 -7
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +72 -43
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/ingest/providers/anthropic.d.ts +2 -0
  8. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  9. package/dist/ingest/providers/anthropic.js +110 -1
  10. package/dist/ingest/providers/anthropic.js.map +1 -1
  11. package/dist/ingest/providers/bedrock.d.ts +2 -5
  12. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  13. package/dist/ingest/providers/bedrock.js +110 -6
  14. package/dist/ingest/providers/bedrock.js.map +1 -1
  15. package/dist/ingest/providers/gemini.d.ts +2 -0
  16. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  17. package/dist/ingest/providers/gemini.js +106 -1
  18. package/dist/ingest/providers/gemini.js.map +1 -1
  19. package/dist/ingest/providers/index.d.ts +9 -0
  20. package/dist/ingest/providers/index.d.ts.map +1 -1
  21. package/dist/ingest/providers/index.js +66 -4
  22. package/dist/ingest/providers/index.js.map +1 -1
  23. package/dist/ingest/providers/openai.d.ts +2 -0
  24. package/dist/ingest/providers/openai.d.ts.map +1 -1
  25. package/dist/ingest/providers/openai.js +112 -1
  26. package/dist/ingest/providers/openai.js.map +1 -1
  27. package/dist/ingest/task-processor.d.ts +63 -0
  28. package/dist/ingest/task-processor.d.ts.map +1 -0
  29. package/dist/ingest/task-processor.js +339 -0
  30. package/dist/ingest/task-processor.js.map +1 -0
  31. package/dist/ingest/worker.d.ts +1 -1
  32. package/dist/ingest/worker.d.ts.map +1 -1
  33. package/dist/ingest/worker.js +18 -13
  34. package/dist/ingest/worker.js.map +1 -1
  35. package/dist/recall/engine.d.ts +1 -0
  36. package/dist/recall/engine.d.ts.map +1 -1
  37. package/dist/recall/engine.js +21 -11
  38. package/dist/recall/engine.js.map +1 -1
  39. package/dist/recall/mmr.d.ts.map +1 -1
  40. package/dist/recall/mmr.js +3 -1
  41. package/dist/recall/mmr.js.map +1 -1
  42. package/dist/storage/sqlite.d.ts +67 -1
  43. package/dist/storage/sqlite.d.ts.map +1 -1
  44. package/dist/storage/sqlite.js +251 -5
  45. package/dist/storage/sqlite.js.map +1 -1
  46. package/dist/types.d.ts +15 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/types.js +2 -0
  49. package/dist/types.js.map +1 -1
  50. package/dist/viewer/html.d.ts +1 -1
  51. package/dist/viewer/html.d.ts.map +1 -1
  52. package/dist/viewer/html.js +919 -123
  53. package/dist/viewer/html.js.map +1 -1
  54. package/dist/viewer/server.d.ts +3 -0
  55. package/dist/viewer/server.d.ts.map +1 -1
  56. package/dist/viewer/server.js +59 -1
  57. package/dist/viewer/server.js.map +1 -1
  58. package/index.ts +217 -42
  59. package/openclaw.plugin.json +20 -45
  60. package/package.json +3 -4
  61. package/skill/SKILL.md +59 -0
  62. package/src/capture/index.ts +85 -45
  63. package/src/ingest/providers/anthropic.ts +128 -1
  64. package/src/ingest/providers/bedrock.ts +130 -6
  65. package/src/ingest/providers/gemini.ts +128 -1
  66. package/src/ingest/providers/index.ts +74 -8
  67. package/src/ingest/providers/openai.ts +130 -1
  68. package/src/ingest/task-processor.ts +380 -0
  69. package/src/ingest/worker.ts +21 -15
  70. package/src/recall/engine.ts +22 -12
  71. package/src/recall/mmr.ts +3 -1
  72. package/src/storage/sqlite.ts +298 -5
  73. package/src/types.ts +19 -0
  74. package/src/viewer/html.ts +919 -123
  75. package/src/viewer/server.ts +63 -1
  76. package/SKILL.md +0 -43
  77. package/www/index.html +0 -632
@@ -41,11 +41,17 @@ exports.viewerHTML = `<!DOCTYPE html>
41
41
  }
42
42
  [data-theme="light"] .auth-screen{background:linear-gradient(135deg,#e0f2fe 0%,#f0f9ff 50%,#e0e7ff 100%)}
43
43
  [data-theme="light"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.12)}
44
- [data-theme="light"] .topbar{background:rgba(255,255,255,.9);border-bottom-color:var(--border)}
44
+ [data-theme="light"] .topbar{background:rgba(255,255,255,.95);border-bottom-color:var(--border)}
45
45
  [data-theme="light"] .session-item .count,[data-theme="light"] .kind-tag,[data-theme="light"] .session-tag{background:rgba(0,0,0,.06)}
46
46
  [data-theme="light"] .card-content pre{background:rgba(0,0,0,.05);border-color:var(--border)}
47
- [data-theme="light"] .vscore-badge{background:linear-gradient(135deg,var(--pri),var(--violet))}
47
+ [data-theme="light"] .vscore-badge{background:rgba(59,130,246,.1);color:#3b82f6}
48
48
  [data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2)}
49
+ [data-theme="light"] .analytics-card{background:linear-gradient(145deg,#fff 0%,#f8fafc 100%);border-color:rgba(0,0,0,.08)}
50
+ [data-theme="light"] .analytics-section{background:#fff;border-color:rgba(0,0,0,.08)}
51
+ [data-theme="light"] .breakdown-item{background:rgba(0,0,0,.04)}
52
+ [data-theme="light"] .metrics-toolbar .range-btn{background:#fff;border-color:rgba(0,0,0,.1)}
53
+ [data-theme="light"] .metrics-toolbar .range-btn.active{background:rgba(0,0,0,.06);color:var(--text);border-color:rgba(0,0,0,.15)}
54
+ [data-theme="light"] .metrics-toolbar .range-btn:hover{border-color:var(--pri);color:var(--pri)}
49
55
  body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .2s,color .2s}
50
56
  button{cursor:pointer;font-family:inherit;font-size:inherit}
51
57
  input,textarea,select{font-family:inherit;font-size:inherit}
@@ -58,12 +64,12 @@ input,textarea,select{font-family:inherit;font-size:inherit}
58
64
  .auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}
59
65
  .auth-card input{width:100%;padding:12px 16px;border:1px solid hsl(0 0% 89.8%);border-radius:8px;font-size:14px;transition:all .2s;margin-bottom:10px;outline:none;background:#fff;color:hsl(0 0% 3.9%)}
60
66
  .auth-card input::placeholder{color:hsl(0 0% 45.1%)}
61
- .auth-card input:focus{border-color:rgb(168,85,247);box-shadow:0 0 0 3px rgba(168,85,247,.2)}
62
- .auth-card .btn-auth{width:100%;padding:12px;border:none;border-radius:8px;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-weight:600;font-size:14px;transition:all .2s}
63
- .auth-card .btn-auth:hover{background:hsl(0 0% 14%);transform:translateY(-1px);box-shadow:0 8px 25px rgba(0,0,0,.2)}
67
+ .auth-card input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.15)}
68
+ .auth-card .btn-auth{width:100%;padding:12px;border:none;border-radius:8px;background:#3b82f6;color:#fff;font-weight:600;font-size:14px;transition:all .2s}
69
+ .auth-card .btn-auth:hover{background:#2563eb;transform:translateY(-1px);box-shadow:0 8px 25px rgba(59,130,246,.3)}
64
70
  .auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}
65
71
  .auth-card .btn-text{color:hsl(0 0% 45.1%)}
66
- .auth-card .btn-text:hover{color:rgb(168,85,247)}
72
+ .auth-card .btn-text:hover{color:#3b82f6}
67
73
 
68
74
  .reset-guide{text-align:left;margin-bottom:20px}
69
75
  .reset-step{display:flex;gap:14px;margin-bottom:16px}
@@ -79,10 +85,12 @@ input,textarea,select{font-family:inherit;font-size:inherit}
79
85
 
80
86
  /* ─── App Layout (dark dashboard, same as www) ─── */
81
87
  .app{display:none;flex-direction:column;min-height:100vh}
82
- .topbar{background:rgba(5,5,16,.85);border-bottom:1px solid var(--border);padding:0 28px;height:64px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px)}
83
- .topbar .brand{display:flex;align-items:center;gap:12px;font-weight:700;font-size:17px;color:var(--text);letter-spacing:-.02em}
84
- .topbar .brand .icon{width:38px;height:38px;display:flex;align-items:center;justify-content:center;font-size:26px;background:none;border-radius:0}
85
- .topbar .actions{display:flex;align-items:center;gap:10px}
88
+ .topbar{background:rgba(5,5,16,.92);border-bottom:1px solid var(--border);padding:0 28px;height:56px;display:flex;align-items:center;position:sticky;top:0;z-index:100;backdrop-filter:blur(16px)}
89
+ .topbar .brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}
90
+ .topbar .brand .icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:22px;background:none;border-radius:0}
91
+ .topbar .brand .sub{font-weight:400;color:var(--text-muted);font-size:11px}
92
+ .topbar-center{flex:1;display:flex;justify-content:center}
93
+ .topbar .actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
86
94
 
87
95
  .main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}
88
96
 
@@ -110,11 +118,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
110
118
 
111
119
  /* ─── Feed ─── */
112
120
  .feed{flex:1;min-width:0}
113
- .search-bar{display:flex;gap:12px;margin-bottom:16px;position:relative}
114
- .search-bar input{flex:1;padding:12px 16px 12px 44px;border:1px solid var(--border);border-radius:12px;font-size:14px;outline:none;background:var(--bg-card);color:var(--text);transition:all .2s}
121
+ .search-bar{display:flex;gap:10px;margin-bottom:16px;position:relative;align-items:center}
122
+ .search-bar input{flex:1;padding:10px 16px 10px 40px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;background:var(--bg-card);color:var(--text);transition:all .2s}
115
123
  .search-bar input::placeholder{color:var(--text-muted)}
116
124
  .search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}
117
- .search-bar .search-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:15px;pointer-events:none}
125
+ .search-bar .search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:14px;pointer-events:none}
118
126
  .search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}
119
127
 
120
128
  .filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
@@ -139,19 +147,25 @@ input,textarea,select{font-family:inherit;font-size:inherit}
139
147
  .card-content.show{max-height:600px;overflow-y:auto}
140
148
  .card-content pre{white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.25);padding:14px;border-radius:10px;font-size:12px;font-family:ui-monospace,monospace;margin-top:10px;border:1px solid var(--border);color:var(--text-sec)}
141
149
  .card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}
142
- .vscore-badge{display:inline-flex;align-items:center;background:linear-gradient(135deg,var(--pri),var(--violet));color:#fff;font-size:10px;font-weight:700;padding:4px 10px;border-radius:8px;margin-left:auto}
150
+ .vscore-badge{display:inline-flex;align-items:center;background:rgba(59,130,246,.15);color:#60a5fa;font-size:10px;font-weight:700;padding:4px 10px;border-radius:8px;margin-left:auto}
143
151
 
144
152
  /* ─── Buttons ─── */
145
- .btn{padding:8px 16px;border-radius:10px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;font-weight:500;transition:all .15s;display:inline-flex;align-items:center;gap:6px}
153
+ .btn{padding:7px 14px;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;font-weight:500;transition:all .18s ease;display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
146
154
  .btn:hover{border-color:var(--pri);color:var(--pri)}
147
- .btn-primary{background:var(--pri);color:#000;border:none}
148
- .btn-primary:hover{background:#4dd9ff;transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,187,238,.25)}
149
- .btn-danger{color:var(--accent);border-color:var(--accent)}
150
- .btn-danger:hover{background:var(--accent);color:#fff;border-color:var(--accent)}
151
- .btn-sm{padding:6px 12px;font-size:12px}
152
- .btn-icon{padding:6px 8px;font-size:14px}
153
- .btn-text{border:none;background:none;color:var(--text-sec);font-size:13px;padding:4px 8px}
155
+ .btn-primary{background:rgba(255,255,255,.08);color:var(--text);border:1px solid var(--border);font-weight:600}
156
+ .btn-primary:hover{background:rgba(255,255,255,.14);transform:translateY(-1px);border-color:var(--pri);color:var(--pri)}
157
+ .btn-ghost{border-color:transparent;background:transparent;color:var(--text-sec)}
158
+ .btn-ghost:hover{background:rgba(255,255,255,.06);color:var(--text)}
159
+ .btn-danger{color:var(--accent);border-color:rgba(230,57,70,.25)}
160
+ .btn-danger:hover{background:rgba(230,57,70,.1);color:var(--accent)}
161
+ .btn-sm{padding:5px 12px;font-size:12px}
162
+ .btn-icon{padding:5px 7px;font-size:15px;border-radius:8px}
163
+ .btn-text{border:none;background:none;color:var(--text-muted);font-size:12px;padding:4px 8px}
154
164
  .btn-text:hover{color:var(--pri)}
165
+ [data-theme="light"] .btn-primary{background:rgba(0,0,0,.05);color:var(--text);border-color:rgba(0,0,0,.12)}
166
+ [data-theme="light"] .btn-primary:hover{background:rgba(0,0,0,.08);border-color:var(--pri);color:var(--pri)}
167
+ [data-theme="light"] .btn-ghost{color:var(--text-sec)}
168
+ [data-theme="light"] .btn-ghost:hover{background:rgba(0,0,0,.04);color:var(--text)}
155
169
 
156
170
  /* ─── Modal ─── */
157
171
  .modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:500;align-items:center;justify-content:center;backdrop-filter:blur(8px)}
@@ -201,89 +215,219 @@ input,textarea,select{font-family:inherit;font-size:inherit}
201
215
  .pagination .pg-btn.disabled{opacity:.4;pointer-events:none}
202
216
  .pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}
203
217
 
204
- .theme-toggle{position:relative;width:40px;height:40px;padding:0;display:flex;align-items:center;justify-content:center;font-size:18px}
218
+ /* ─── Tasks 视图 ─── */
219
+ .tasks-view{display:none;flex:1;min-width:0;flex-direction:column;gap:16px}
220
+ .tasks-view.show{display:flex}
221
+ .tasks-header{display:flex;flex-direction:column;gap:14px}
222
+ .tasks-stats{display:flex;gap:16px}
223
+ .tasks-stat{display:flex;align-items:center;gap:8px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 18px;flex:1;transition:all .2s}
224
+ .tasks-stat:hover{border-color:var(--border-glow)}
225
+ .tasks-stat-value{font-size:22px;font-weight:700;color:var(--text)}
226
+ .tasks-stat-label{font-size:12px;color:var(--text-sec);font-weight:500}
227
+ .tasks-filters{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
228
+ .tasks-list{display:flex;flex-direction:column;gap:10px}
229
+ .task-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:18px 20px;cursor:pointer;transition:all .25s;position:relative;overflow:hidden}
230
+ .task-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover);transform:translateY(-1px);box-shadow:var(--shadow)}
231
+ .task-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px}
232
+ .task-card.status-active::before{background:var(--green)}
233
+ .task-card.status-completed::before{background:var(--pri)}
234
+ .task-card.status-skipped::before{background:var(--text-muted)}
235
+ .task-card.status-skipped{opacity:.6}
236
+ .task-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:8px}
237
+ .task-card-title{font-size:14px;font-weight:600;color:var(--text);line-height:1.4;flex:1;word-break:break-word}
238
+ .task-card-title:empty::after{content:'Untitled Task';color:var(--text-muted);font-style:italic}
239
+ .task-status-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:3px 10px;border-radius:20px;flex-shrink:0}
240
+ .task-status-badge.active{color:var(--green);background:var(--green-bg)}
241
+ .task-status-badge.completed{color:var(--pri);background:var(--pri-glow)}
242
+ .task-status-badge.skipped{color:var(--text-muted);background:rgba(128,128,128,.15)}
243
+ .task-card-summary{font-size:13px;color:var(--text-sec);line-height:1.5;margin-bottom:10px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
244
+ .task-card-summary:empty{display:none}
245
+ .task-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted)}
246
+ .task-card-bottom .tag{display:flex;align-items:center;gap:4px}
247
+ .task-card-bottom .tag .icon{font-size:12px}
248
+
249
+ /* ─── Task Detail Overlay ─── */
250
+ .task-detail-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:200;align-items:center;justify-content:center;padding:24px;backdrop-filter:blur(4px)}
251
+ .task-detail-overlay.show{display:flex}
252
+ .task-detail-panel{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);width:100%;max-width:780px;max-height:85vh;overflow-y:auto;box-shadow:var(--shadow-lg);padding:28px 32px}
253
+ .task-detail-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}
254
+ .task-detail-header h2{font-size:18px;font-weight:700;color:var(--text);line-height:1.4;flex:1}
255
+ .task-detail-meta{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;font-size:12px;color:var(--text-sec)}
256
+ .task-detail-meta .meta-item{display:flex;align-items:center;gap:5px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:5px 12px}
257
+ .task-detail-summary{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:20px;font-size:13px;line-height:1.7;color:var(--text);word-break:break-word}
258
+ .task-detail-summary:empty::after{content:'Summary not yet generated (task still active)';color:var(--text-muted);font-style:italic}
259
+ .task-detail-summary .summary-section-title{font-size:14px;font-weight:700;color:var(--text);margin:14px 0 6px 0;padding-bottom:4px;border-bottom:1px solid var(--border)}
260
+ .task-detail-summary .summary-section-title:first-child{margin-top:0}
261
+ .task-detail-summary ul{margin:4px 0 8px 0;padding-left:20px}
262
+ .task-detail-summary li{margin:3px 0;color:var(--text-sec);line-height:1.6}
263
+ .task-detail-chunks-title{font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px}
264
+ .task-detail-chunks{display:flex;flex-direction:column;gap:14px;padding:8px 0}
265
+ .task-chunk-item{display:flex;flex-direction:column;max-width:82%;font-size:13px;line-height:1.6}
266
+ .task-chunk-item.role-user{align-self:flex-end;align-items:flex-end}
267
+ .task-chunk-item.role-assistant,.task-chunk-item.role-tool{align-self:flex-start;align-items:flex-start}
268
+ .task-chunk-role{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px;padding:0 4px}
269
+ .task-chunk-role.user{color:var(--pri)}
270
+ .task-chunk-role.assistant{color:var(--green)}
271
+ .task-chunk-role.tool{color:var(--amber)}
272
+ .task-chunk-bubble{padding:12px 16px;border-radius:16px;white-space:pre-wrap;word-break:break-word;max-height:200px;overflow:hidden;position:relative;transition:all .2s}
273
+ .task-chunk-bubble.expanded{max-height:none}
274
+ .role-user .task-chunk-bubble{background:var(--pri);color:#000;border-bottom-right-radius:4px}
275
+ .role-assistant .task-chunk-bubble{background:var(--bg-card);border:1px solid var(--border);color:var(--text-sec);border-bottom-left-radius:4px}
276
+ .role-tool .task-chunk-bubble{background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);color:var(--text-sec);border-bottom-left-radius:4px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
277
+ .task-chunk-bubble:hover{filter:brightness(1.05)}
278
+ .task-chunk-time{font-size:10px;color:var(--text-muted);margin-top:3px;padding:0 4px}
279
+ [data-theme="light"] .role-user .task-chunk-bubble{background:var(--pri);color:#fff}
280
+ [data-theme="light"] .role-assistant .task-chunk-bubble{background:#f0f0f0;border:none;color:#333}
281
+ [data-theme="light"] .task-detail-panel{background:#fff}
282
+ [data-theme="light"] .task-card{background:#fff}
283
+ [data-theme="light"] .tasks-stat{background:#fff}
284
+
285
+ /* ─── Analytics / 统计 ─── */
286
+ .nav-tabs{display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.06);border-radius:10px;padding:3px}
287
+ .nav-tabs .tab{padding:6px 20px;border-radius:8px;font-size:13px;font-weight:600;color:var(--text-sec);background:transparent;border:1px solid transparent;cursor:pointer;transition:all .2s;white-space:nowrap}
288
+ .nav-tabs .tab:hover{color:var(--text)}
289
+ .nav-tabs .tab.active{color:var(--text);background:rgba(255,255,255,.1);border-color:var(--border);box-shadow:0 1px 4px rgba(0,0,0,.15)}
290
+ [data-theme="light"] .nav-tabs{background:rgba(0,0,0,.05)}
291
+ [data-theme="light"] .nav-tabs .tab.active{background:#fff;border-color:rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.08);color:var(--text)}
292
+ .analytics-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
293
+ .analytics-view.show{display:flex}
294
+ .feed-wrap{flex:1;min-width:0;display:flex;flex-direction:column}
295
+ .feed-wrap.hide{display:none}
296
+ .analytics-cards{display:grid;grid-template-columns:repeat(5,1fr);gap:14px}
297
+ .analytics-card{background:linear-gradient(145deg,var(--bg-card) 0%,rgba(255,255,255,.02) 100%);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 18px;position:relative;overflow:hidden;transition:all .25s}
298
+ .analytics-card:hover{border-color:var(--border-glow);transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.15)}
299
+ .analytics-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,#3b82f6,#60a5fa);opacity:.85}
300
+ .analytics-card.green::before{background:linear-gradient(90deg,var(--green),#34d399)}
301
+ .analytics-card.amber::before{background:linear-gradient(90deg,var(--amber),#fbbf24)}
302
+ .analytics-card.violet::before{background:linear-gradient(90deg,var(--violet),#a78bfa)}
303
+ .analytics-card .ac-value{font-size:26px;font-weight:800;letter-spacing:-.03em;color:var(--text);line-height:1.1}
304
+ .analytics-card .ac-label{font-size:11px;color:var(--text-muted);margin-top:5px;font-weight:500;text-transform:uppercase;letter-spacing:.04em}
305
+ .analytics-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:22px 24px}
306
+ .analytics-section h3{font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:16px;display:flex;align-items:center;gap:8px}
307
+ .analytics-section h3 .icon{font-size:14px;opacity:.7}
308
+ .chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto}
309
+ .chart-bar-wrap{flex:1;min-width:18px;max-width:60px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}
310
+ .chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}
311
+ .chart-bar-wrap:hover .chart-bar{filter:brightness(1.25)}
312
+ .chart-bar-wrap:hover .chart-bar-label{color:var(--pri)}
313
+ .chart-bar-wrap:hover .chart-tip{opacity:1;transform:translateX(-50%) translateY(0)}
314
+ .chart-tip{position:absolute;top:-6px;left:50%;transform:translateX(-50%) translateY(4px);background:var(--bg);border:1px solid var(--border);color:var(--text);padding:2px 8px;border-radius:6px;font-size:10px;font-weight:700;white-space:nowrap;z-index:5;pointer-events:none;box-shadow:var(--shadow);opacity:0;transition:all .15s ease}
315
+ .chart-bar{width:100%;border-radius:4px 4px 0 0;background:linear-gradient(180deg,#60a5fa,#3b82f6);transition:all .3s ease}
316
+ .chart-bar.violet{background:linear-gradient(180deg,var(--violet),#7c3aed)}
317
+ .chart-bar.green{background:linear-gradient(180deg,var(--green),#059669)}
318
+ .chart-bar.zero{background:var(--border);opacity:.4;border-radius:2px}
319
+ .chart-bar-label{font-size:9px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;text-align:center;transition:color .15s}
320
+ .chart-legend{display:flex;gap:16px;margin-top:12px;flex-wrap:wrap;font-size:12px;color:var(--text-sec)}
321
+ .chart-legend span{display:inline-flex;align-items:center;gap:6px}
322
+ .chart-legend .dot{width:8px;height:8px;border-radius:50%}
323
+ .chart-legend .dot.pri{background:var(--pri)}
324
+ .chart-legend .dot.violet{background:var(--violet)}
325
+ .chart-legend .dot.green{background:var(--green)}
326
+ .breakdown-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}
327
+ .breakdown-item{display:flex;flex-direction:column;gap:6px;padding:12px 14px;background:rgba(0,0,0,.12);border-radius:10px;border:1px solid var(--border);transition:border-color .15s}
328
+ .breakdown-item:hover{border-color:var(--border-glow)}
329
+ .breakdown-item .bd-top{display:flex;align-items:center;justify-content:space-between}
330
+ .breakdown-item .label{font-size:13px;color:var(--text-sec);font-weight:500;text-transform:capitalize}
331
+ .breakdown-item .value{font-size:14px;font-weight:700;color:var(--text)}
332
+ .breakdown-bar-wrap{height:4px;background:rgba(0,0,0,.15);border-radius:2px;overflow:hidden}
333
+ .breakdown-bar{height:100%;border-radius:2px;background:linear-gradient(90deg,#3b82f6,#60a5fa);transition:width .5s ease}
334
+ .metrics-toolbar{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap}
335
+ .metrics-toolbar .range-btn{padding:6px 14px;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text-sec);font-size:12px;font-weight:600;cursor:pointer;transition:all .15s}
336
+ .metrics-toolbar .range-btn:hover{border-color:var(--pri);color:var(--pri)}
337
+ .metrics-toolbar .range-btn.active{background:rgba(255,255,255,.1);color:var(--text);border-color:var(--border)}
338
+
339
+ .theme-toggle{position:relative;width:28px;height:28px;padding:0;display:flex;align-items:center;justify-content:center;font-size:14px;border:none;background:transparent}
205
340
  .theme-toggle .theme-icon-light{display:none}
206
341
  .theme-toggle .theme-icon-dark{display:inline}
207
342
  [data-theme="light"] .theme-toggle .theme-icon-light{display:inline}
208
343
  [data-theme="light"] .theme-toggle .theme-icon-dark{display:none}
209
344
 
210
- .auth-theme-toggle{position:absolute;top:16px;right:16px;z-index:10;width:40px;height:40px;border:none;border-radius:10px;background:rgba(255,255,255,.25);backdrop-filter:blur(8px);color:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;transition:background .2s}
211
- .auth-theme-toggle:hover{background:rgba(255,255,255,.4)}
345
+ .auth-top-actions{position:absolute;top:16px;right:16px;z-index:10;display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.18);backdrop-filter:blur(10px);border-radius:20px;padding:3px}
346
+ .auth-theme-toggle{width:30px;height:30px;border:none;border-radius:50%;background:transparent;color:rgba(255,255,255,.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s}
347
+ .auth-theme-toggle:hover{background:rgba(255,255,255,.2);color:#fff}
212
348
  .auth-theme-toggle .theme-icon-light{display:none}
213
349
  .auth-theme-toggle .theme-icon-dark{display:inline}
214
- [data-theme="light"] .auth-theme-toggle{background:rgba(0,0,0,.08);color:#0f172a}
215
- [data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.12)}
350
+ [data-theme="light"] .auth-theme-toggle{color:rgba(0,0,0,.45)}
351
+ [data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.08);color:#0f172a}
352
+ [data-theme="light"] .auth-top-actions{background:rgba(0,0,0,.06)}
216
353
  [data-theme="light"] .auth-theme-toggle .theme-icon-light{display:inline}
217
354
  [data-theme="light"] .auth-theme-toggle .theme-icon-dark{display:none}
218
355
 
219
- @media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}}
356
+ @media(max-width:1100px){.analytics-cards{grid-template-columns:repeat(3,1fr)}}
357
+ @media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}.analytics-cards{grid-template-columns:repeat(2,1fr)}.topbar{padding:0 16px;gap:8px}.topbar .brand span{display:none}.topbar-center{justify-content:flex-start}}
220
358
  </style>
221
359
  </head>
222
360
  <body>
223
361
 
224
362
  <!-- ─── Auth: Setup Password ─── -->
225
363
  <div id="setupScreen" class="auth-screen" style="display:none">
226
- <button class="auth-theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
364
+ <div class="auth-top-actions">
365
+ <button class="auth-theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
366
+ <button class="auth-theme-toggle" onclick="toggleLang()" aria-label="Switch language"><span data-i18n="lang.switch">EN</span></button>
367
+ </div>
227
368
  <div class="auth-card">
228
369
  <div class="logo">\u{1F99E}</div>
229
- <h1>OpenClaw Memory</h1>
230
- <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px">Powered by MemOS</p>
231
- <p>Set a password to protect your memories</p>
232
- <input type="password" id="setupPw" placeholder="Enter a password (4+ characters)" autofocus>
233
- <input type="password" id="setupPw2" placeholder="Confirm password">
234
- <button class="btn-auth" onclick="doSetup()">Set Password & Enter</button>
370
+ <h1 data-i18n="title">OpenClaw Memory</h1>
371
+ <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px" data-i18n="subtitle">Powered by MemOS</p>
372
+ <p data-i18n="setup.desc">Set a password to protect your memories</p>
373
+ <input type="password" id="setupPw" data-i18n-ph="setup.pw" placeholder="Enter a password (4+ characters)" autofocus>
374
+ <input type="password" id="setupPw2" data-i18n-ph="setup.pw2" placeholder="Confirm password">
375
+ <button class="btn-auth" onclick="doSetup()" data-i18n="setup.btn">Set Password & Enter</button>
235
376
  <div class="error-msg" id="setupErr"></div>
236
377
  </div>
237
378
  </div>
238
379
 
239
380
  <!-- ─── Auth: Login ─── -->
240
381
  <div id="loginScreen" class="auth-screen" style="display:none">
241
- <button class="auth-theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
382
+ <div class="auth-top-actions">
383
+ <button class="auth-theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
384
+ <button class="auth-theme-toggle" onclick="toggleLang()" aria-label="Switch language"><span data-i18n="lang.switch">EN</span></button>
385
+ </div>
242
386
  <div class="auth-card">
243
387
  <div class="logo">\u{1F99E}</div>
244
- <h1>OpenClaw Memory</h1>
245
- <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px">Powered by MemOS</p>
246
- <p>Enter your password to access memories</p>
388
+ <h1 data-i18n="title">OpenClaw Memory</h1>
389
+ <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px" data-i18n="subtitle">Powered by MemOS</p>
390
+ <p data-i18n="login.desc">Enter your password to access memories</p>
247
391
  <div id="loginForm">
248
- <input type="password" id="loginPw" placeholder="Password" autofocus>
249
- <button class="btn-auth" onclick="doLogin()">Unlock</button>
392
+ <input type="password" id="loginPw" data-i18n-ph="login.pw" placeholder="Password" autofocus>
393
+ <button class="btn-auth" onclick="doLogin()" data-i18n="login.btn">Unlock</button>
250
394
  <div class="error-msg" id="loginErr"></div>
251
- <button class="btn-text" style="margin-top:12px;font-size:13px;color:var(--text-sec)" onclick="showResetForm()">Forgot password?</button>
395
+ <button class="btn-text" style="margin-top:12px;font-size:13px;color:var(--text-sec)" onclick="showResetForm()" data-i18n="login.forgot">Forgot password?</button>
252
396
  </div>
253
397
  <div id="resetForm" style="display:none">
254
398
  <div class="reset-guide">
255
399
  <div class="reset-step">
256
400
  <div class="step-num">1</div>
257
401
  <div class="step-body">
258
- <div class="step-title">Open Terminal</div>
259
- <div class="step-desc">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>
402
+ <div class="step-title" data-i18n="reset.step1.title">Open Terminal</div>
403
+ <div class="step-desc" data-i18n="reset.step1.desc">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>
260
404
  <div class="cmd-box" onclick="copyCmd(this)">
261
405
  <code>grep "password reset token:" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>
262
- <span class="copy-hint">Click to copy</span>
406
+ <span class="copy-hint" data-i18n="copy.hint">Click to copy</span>
263
407
  </div>
264
408
  </div>
265
409
  </div>
266
410
  <div class="reset-step">
267
411
  <div class="step-num">2</div>
268
412
  <div class="step-body">
269
- <div class="step-title">Find the token</div>
270
- <div class="step-desc">In the output, find <span style="font-family:monospace;font-size:12px;color:var(--pri)">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>
413
+ <div class="step-title" data-i18n="reset.step2.title">Find the token</div>
414
+ <div class="step-desc" id="resetStep2Desc">In the output, find <span style="font-family:monospace;font-size:12px;color:var(--pri)">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>
271
415
  </div>
272
416
  </div>
273
417
  <div class="reset-step">
274
418
  <div class="step-num">3</div>
275
419
  <div class="step-body">
276
- <div class="step-title">Paste & reset</div>
277
- <div class="step-desc">Paste the token below and set your new password.</div>
420
+ <div class="step-title" data-i18n="reset.step3.title">Paste & reset</div>
421
+ <div class="step-desc" data-i18n="reset.step3.desc">Paste the token below and set your new password.</div>
278
422
  </div>
279
423
  </div>
280
424
  </div>
281
- <input type="text" id="resetToken" placeholder="Paste reset token here" style="margin-bottom:8px;font-family:monospace">
282
- <input type="password" id="resetNewPw" placeholder="New password (4+ characters)">
283
- <input type="password" id="resetNewPw2" placeholder="Confirm new password">
284
- <button class="btn-auth" onclick="doReset()">Reset Password</button>
425
+ <input type="text" id="resetToken" data-i18n-ph="reset.token" placeholder="Paste reset token here" style="margin-bottom:8px;font-family:monospace">
426
+ <input type="password" id="resetNewPw" data-i18n-ph="reset.newpw" placeholder="New password (4+ characters)">
427
+ <input type="password" id="resetNewPw2" data-i18n-ph="reset.newpw2" placeholder="Confirm new password">
428
+ <button class="btn-auth" onclick="doReset()" data-i18n="reset.btn">Reset Password</button>
285
429
  <div class="error-msg" id="resetErr"></div>
286
- <button class="btn-text" style="margin-top:12px;font-size:13px;color:var(--text-sec)" onclick="showLoginForm()">\u2190 Back to login</button>
430
+ <button class="btn-text" style="margin-top:12px;font-size:13px;color:var(--text-sec)" onclick="showLoginForm()" data-i18n="reset.back">\u2190 Back to login</button>
287
431
  </div>
288
432
  </div>
289
433
  </div>
@@ -293,78 +437,153 @@ input,textarea,select{font-family:inherit;font-size:inherit}
293
437
  <div class="topbar">
294
438
  <div class="brand">
295
439
  <div class="icon">\u{1F99E}</div>
296
- <span>OpenClaw Memory <span style="font-weight:400;color:var(--text-sec);font-size:12px">by MemOS</span></span>
440
+ <span data-i18n="title">OpenClaw Memory</span>
441
+ </div>
442
+ <div class="topbar-center">
443
+ <nav class="nav-tabs">
444
+ <button class="tab active" data-view="memories" onclick="switchView('memories')" data-i18n="tab.memories">\u{1F4DA} Memories</button>
445
+ <button class="tab" data-view="tasks" onclick="switchView('tasks')" data-i18n="tab.tasks">\u{1F4CB} Tasks</button>
446
+ <button class="tab" data-view="analytics" onclick="switchView('analytics')" data-i18n="tab.analytics">\u{1F4CA} Analytics</button>
447
+ </nav>
297
448
  </div>
298
449
  <div class="actions">
450
+ <button class="btn btn-icon" onclick="toggleLang()" aria-label="Switch language" style="font-size:12px;font-weight:700;padding:4px 8px"><span data-i18n="lang.switch">EN</span></button>
299
451
  <button class="btn btn-icon theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
300
- <button class="btn btn-primary" onclick="openCreateModal()">+ New Memory</button>
301
- <button class="btn" onclick="loadAll()">Refresh</button>
302
- <button class="btn btn-danger" onclick="clearAll()">Clear All</button>
303
- <button class="btn btn-text" onclick="doLogout()">Logout</button>
452
+ <button class="btn btn-ghost btn-sm" onclick="loadAll()" data-i18n="refresh">\u21BB Refresh</button>
453
+ <button class="btn btn-ghost btn-sm" onclick="doLogout()" data-i18n="logout">Logout</button>
304
454
  </div>
305
455
  </div>
306
456
 
307
457
  <div class="main-content">
308
458
  <div class="sidebar" id="sidebar">
309
459
  <div class="stats-grid" id="statsGrid">
310
- <div class="stat-card pri"><div class="stat-value" id="statTotal">-</div><div class="stat-label">Memories</div></div>
311
- <div class="stat-card green"><div class="stat-value" id="statSessions">-</div><div class="stat-label">Sessions</div></div>
312
- <div class="stat-card amber"><div class="stat-value" id="statEmbeddings">-</div><div class="stat-label">Embeddings</div></div>
313
- <div class="stat-card rose"><div class="stat-value" id="statTimeSpan">-</div><div class="stat-label">Days</div></div>
460
+ <div class="stat-card pri"><div class="stat-value" id="statTotal">-</div><div class="stat-label" data-i18n="stat.memories">Memories</div></div>
461
+ <div class="stat-card green"><div class="stat-value" id="statSessions">-</div><div class="stat-label" data-i18n="stat.sessions">Sessions</div></div>
462
+ <div class="stat-card amber"><div class="stat-value" id="statEmbeddings">-</div><div class="stat-label" data-i18n="stat.embeddings">Embeddings</div></div>
463
+ <div class="stat-card rose"><div class="stat-value" id="statTimeSpan">-</div><div class="stat-label" data-i18n="stat.days">Days</div></div>
314
464
  </div>
315
465
  <div id="embeddingStatus"></div>
316
- <div class="section-title">Sessions</div>
466
+ <div class="section-title" data-i18n="sidebar.sessions">Sessions</div>
317
467
  <div class="session-list" id="sessionList"></div>
468
+ <button class="btn btn-sm btn-ghost" style="width:100%;margin-top:20px;justify-content:center;color:var(--text-muted);font-size:11px" onclick="clearAll()" data-i18n="sidebar.clear">\u{1F5D1} Clear All Data</button>
318
469
  </div>
319
470
 
471
+ <div class="feed-wrap" id="feedWrap">
320
472
  <div class="feed">
321
473
  <div class="search-bar">
322
474
  <span class="search-icon">\u{1F50D}</span>
323
- <input type="text" id="searchInput" placeholder="Search memories (supports semantic search)..." oninput="debounceSearch()">
475
+ <input type="text" id="searchInput" data-i18n-ph="search.placeholder" placeholder="Search memories (supports semantic search)..." oninput="debounceSearch()">
324
476
  </div>
325
477
  <div class="search-meta" id="searchMeta"></div>
326
478
  <div class="filter-bar" id="filterBar">
327
- <button class="filter-chip active" data-role="" onclick="setRoleFilter(this,'')">All</button>
479
+ <button class="filter-chip active" data-role="" onclick="setRoleFilter(this,'')" data-i18n="filter.all">All</button>
328
480
  <button class="filter-chip" data-role="user" onclick="setRoleFilter(this,'user')">User</button>
329
481
  <button class="filter-chip" data-role="assistant" onclick="setRoleFilter(this,'assistant')">Assistant</button>
330
482
  <button class="filter-chip" data-role="system" onclick="setRoleFilter(this,'system')">System</button>
331
483
  <span class="filter-sep"></span>
332
484
  <select id="filterKind" class="filter-select" onchange="applyFilters()">
333
- <option value="">All kinds</option>
334
- <option value="paragraph">Paragraph</option>
335
- <option value="code_block">Code</option>
336
- <option value="dialog">Dialog</option>
337
- <option value="list">List</option>
338
- <option value="error_stack">Error</option>
339
- <option value="command">Command</option>
485
+ <option value="" data-i18n="filter.allkinds">All kinds</option>
486
+ <option value="paragraph" data-i18n="filter.paragraph">Paragraph</option>
487
+ <option value="code_block" data-i18n="filter.code">Code</option>
488
+ <option value="dialog" data-i18n="filter.dialog">Dialog</option>
489
+ <option value="list" data-i18n="filter.list">List</option>
490
+ <option value="error_stack" data-i18n="filter.error">Error</option>
491
+ <option value="command" data-i18n="filter.command">Command</option>
340
492
  </select>
341
493
  <select id="filterSort" class="filter-select" onchange="applyFilters()">
342
- <option value="newest">Newest first</option>
343
- <option value="oldest">Oldest first</option>
494
+ <option value="newest" data-i18n="filter.newest">Newest first</option>
495
+ <option value="oldest" data-i18n="filter.oldest">Oldest first</option>
344
496
  </select>
345
497
  </div>
346
498
  <div class="date-filter">
347
- <label>From</label><input type="datetime-local" id="dateFrom" step="1" onchange="applyFilters()">
348
- <label>To</label><input type="datetime-local" id="dateTo" step="1" onchange="applyFilters()">
349
- <button class="btn btn-sm btn-text" onclick="clearDateFilter()">Clear</button>
499
+ <label data-i18n="filter.from">From</label><input type="datetime-local" id="dateFrom" step="1" onchange="applyFilters()">
500
+ <label data-i18n="filter.to">To</label><input type="datetime-local" id="dateTo" step="1" onchange="applyFilters()">
501
+ <button class="btn btn-sm btn-text" onclick="clearDateFilter()" data-i18n="filter.clear">Clear</button>
350
502
  </div>
351
503
  <div class="memory-list" id="memoryList"><div class="spinner"></div></div>
352
504
  <div class="pagination" id="pagination"></div>
353
505
  </div>
506
+ </div>
507
+ <div class="tasks-view" id="tasksView">
508
+ <div class="tasks-header">
509
+ <div class="tasks-stats">
510
+ <div class="tasks-stat"><span class="tasks-stat-value" id="tasksTotalCount">-</span><span class="tasks-stat-label" data-i18n="tasks.total">Total Tasks</span></div>
511
+ <div class="tasks-stat"><span class="tasks-stat-value" id="tasksActiveCount">-</span><span class="tasks-stat-label" data-i18n="tasks.active">Active</span></div>
512
+ <div class="tasks-stat"><span class="tasks-stat-value" id="tasksCompletedCount">-</span><span class="tasks-stat-label" data-i18n="tasks.completed">Completed</span></div>
513
+ <div class="tasks-stat"><span class="tasks-stat-value" id="tasksSkippedCount">-</span><span class="tasks-stat-label" data-i18n="tasks.status.skipped">Skipped</span></div>
514
+ </div>
515
+ <div class="tasks-filters">
516
+ <button class="filter-chip active" data-task-status="" onclick="setTaskStatusFilter(this,'')" data-i18n="filter.all">All</button>
517
+ <button class="filter-chip" data-task-status="active" onclick="setTaskStatusFilter(this,'active')" data-i18n="tasks.status.active">Active</button>
518
+ <button class="filter-chip" data-task-status="completed" onclick="setTaskStatusFilter(this,'completed')" data-i18n="tasks.status.completed">Completed</button>
519
+ <button class="filter-chip" data-task-status="skipped" onclick="setTaskStatusFilter(this,'skipped')" data-i18n="tasks.status.skipped">Skipped</button>
520
+ <button class="btn btn-sm btn-ghost" onclick="loadTasks()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
521
+ </div>
522
+ </div>
523
+ <div class="tasks-list" id="tasksList"><div class="spinner"></div></div>
524
+ <div class="pagination" id="tasksPagination"></div>
525
+ <div class="task-detail-overlay" id="taskDetailOverlay" onclick="closeTaskDetail(event)">
526
+ <div class="task-detail-panel" onclick="event.stopPropagation()">
527
+ <div class="task-detail-header">
528
+ <h2 id="taskDetailTitle"></h2>
529
+ <button class="btn btn-icon" onclick="closeTaskDetail()" title="Close">\u2715</button>
530
+ </div>
531
+ <div class="task-detail-meta" id="taskDetailMeta"></div>
532
+ <div class="task-detail-summary" id="taskDetailSummary"></div>
533
+ <div class="task-detail-chunks-title" data-i18n="tasks.chunks">Related Memories</div>
534
+ <div class="task-detail-chunks" id="taskDetailChunks"></div>
535
+ </div>
536
+ </div>
537
+ </div>
538
+ <div class="analytics-view" id="analyticsView">
539
+ <div class="metrics-toolbar">
540
+ <span style="font-size:12px;color:var(--text-sec);font-weight:600" data-i18n="range">Range</span>
541
+ <button class="range-btn" data-days="7" onclick="setMetricsDays(7)">7 <span data-i18n="range.days">days</span></button>
542
+ <button class="range-btn active" data-days="30" onclick="setMetricsDays(30)">30 <span data-i18n="range.days">days</span></button>
543
+ <button class="range-btn" data-days="90" onclick="setMetricsDays(90)">90 <span data-i18n="range.days">days</span></button>
544
+ <button class="btn btn-sm" onclick="loadMetrics()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
545
+ </div>
546
+ <div class="analytics-cards" id="analyticsCards">
547
+ <div class="analytics-card"><div class="ac-value" id="mTotal">-</div><div class="ac-label" data-i18n="analytics.total">Total Memories</div></div>
548
+ <div class="analytics-card green"><div class="ac-value" id="mTodayWrites">-</div><div class="ac-label" data-i18n="analytics.writes">Writes Today</div></div>
549
+ <div class="analytics-card violet"><div class="ac-value" id="mTodayCalls">-</div><div class="ac-label" data-i18n="analytics.calls">Viewer Calls Today</div></div>
550
+ <div class="analytics-card"><div class="ac-value" id="mSessions">-</div><div class="ac-label" data-i18n="analytics.sessions">Sessions</div></div>
551
+ <div class="analytics-card amber"><div class="ac-value" id="mEmbeddings">-</div><div class="ac-label" data-i18n="analytics.embeddings">Embeddings</div></div>
552
+ </div>
553
+ <div class="analytics-section">
554
+ <h3><span class="icon">\u{1F4CA}</span> <span data-i18n="chart.writes">Memory Writes per Day</span></h3>
555
+ <div class="chart-bars" id="chartWrites"></div>
556
+ </div>
557
+ <div class="analytics-section">
558
+ <h3><span class="icon">\u{1F50D}</span> <span data-i18n="chart.calls">Viewer API Calls per Day (List / Search)</span></h3>
559
+ <div class="chart-bars" id="chartCalls"></div>
560
+ <div class="chart-legend"><span><span class="dot pri"></span> <span data-i18n="chart.list">List</span></span><span><span class="dot violet"></span> <span data-i18n="chart.search">Search</span></span></div>
561
+ </div>
562
+ <div class="breakdown-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
563
+ <div class="analytics-section">
564
+ <h3><span class="icon">\u{1F464}</span> <span data-i18n="breakdown.role">By Role</span></h3>
565
+ <div id="breakdownRole"></div>
566
+ </div>
567
+ <div class="analytics-section">
568
+ <h3><span class="icon">\u{1F4DD}</span> <span data-i18n="breakdown.kind">By Kind</span></h3>
569
+ <div id="breakdownKind"></div>
570
+ </div>
571
+ </div>
572
+ </div>
354
573
  </div>
355
574
  </div>
356
575
 
357
576
  <!-- ─── Memory Modal ─── -->
358
577
  <div class="modal-overlay" id="modalOverlay">
359
578
  <div class="modal">
360
- <h2 id="modalTitle">New Memory</h2>
361
- <div class="form-group"><label>Role</label><select id="mRole"><option value="user">User</option><option value="assistant">Assistant</option><option value="system">System</option></select></div>
362
- <div class="form-group"><label>Content</label><textarea id="mContent" rows="4" placeholder="Memory content..."></textarea></div>
363
- <div class="form-group"><label>Summary</label><input type="text" id="mSummary" placeholder="Brief summary (optional)"></div>
364
- <div class="form-group"><label>Kind</label><select id="mKind"><option value="paragraph">Paragraph</option><option value="code">Code</option><option value="dialog">Dialog</option></select></div>
579
+ <h2 id="modalTitle" data-i18n="modal.new">New Memory</h2>
580
+ <div class="form-group"><label data-i18n="modal.role">Role</label><select id="mRole"><option value="user">User</option><option value="assistant">Assistant</option><option value="system">System</option></select></div>
581
+ <div class="form-group"><label data-i18n="modal.content">Content</label><textarea id="mContent" rows="4" data-i18n-ph="modal.content.ph" placeholder="Memory content..."></textarea></div>
582
+ <div class="form-group"><label data-i18n="modal.summary">Summary</label><input type="text" id="mSummary" data-i18n-ph="modal.summary.ph" placeholder="Brief summary (optional)"></div>
583
+ <div class="form-group"><label data-i18n="modal.kind">Kind</label><select id="mKind"><option value="paragraph" data-i18n="filter.paragraph">Paragraph</option><option value="code" data-i18n="filter.code">Code</option><option value="dialog" data-i18n="filter.dialog">Dialog</option></select></div>
365
584
  <div class="modal-actions">
366
- <button class="btn" onclick="closeModal()">Cancel</button>
367
- <button class="btn btn-primary" id="modalSubmit" onclick="submitModal()">Create</button>
585
+ <button class="btn btn-ghost" onclick="closeModal()" data-i18n="modal.cancel">Cancel</button>
586
+ <button class="btn btn-primary" id="modalSubmit" onclick="submitModal()" data-i18n="modal.create">Create</button>
368
587
  </div>
369
588
  </div>
370
589
  </div>
@@ -373,7 +592,276 @@ input,textarea,select{font-family:inherit;font-size:inherit}
373
592
  <div class="toast-container" id="toasts"></div>
374
593
 
375
594
  <script>
376
- let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30;
595
+ let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30,metricsDays=30;
596
+
597
+ /* ─── i18n ─── */
598
+ const I18N={
599
+ en:{
600
+ 'title':'OpenClaw Memory',
601
+ 'subtitle':'Powered by MemOS',
602
+ 'setup.desc':'Set a password to protect your memories',
603
+ 'setup.pw':'Enter a password (4+ characters)',
604
+ 'setup.pw2':'Confirm password',
605
+ 'setup.btn':'Set Password & Enter',
606
+ 'setup.err.short':'Password must be at least 4 characters',
607
+ 'setup.err.mismatch':'Passwords do not match',
608
+ 'setup.err.fail':'Setup failed',
609
+ 'login.desc':'Enter your password to access memories',
610
+ 'login.pw':'Password',
611
+ 'login.btn':'Unlock',
612
+ 'login.err':'Incorrect password',
613
+ 'login.forgot':'Forgot password?',
614
+ 'reset.step1.title':'Open Terminal',
615
+ 'reset.step1.desc':'Run the following command to get your reset token (use the pattern below so you get the line that contains the token):',
616
+ 'reset.step2.title':'Find the token',
617
+ 'reset.step2.desc.pre':'In the output, find ',
618
+ 'reset.step2.desc.post':' (plain line or inside JSON). Copy the 32-character hex string after the colon.',
619
+ 'reset.step3.title':'Paste & reset',
620
+ 'reset.step3.desc':'Paste the token below and set your new password.',
621
+ 'reset.token':'Paste reset token here',
622
+ 'reset.newpw':'New password (4+ characters)',
623
+ 'reset.newpw2':'Confirm new password',
624
+ 'reset.btn':'Reset Password',
625
+ 'reset.err.token':'Please enter the reset token',
626
+ 'reset.err.short':'Password must be at least 4 characters',
627
+ 'reset.err.mismatch':'Passwords do not match',
628
+ 'reset.err.fail':'Reset failed',
629
+ 'reset.back':'\\u2190 Back to login',
630
+ 'copy.hint':'Click to copy',
631
+ 'copy.done':'Copied!',
632
+ 'tab.memories':'\\u{1F4DA} Memories',
633
+ 'tab.tasks':'\\u{1F4CB} Tasks',
634
+ 'tab.analytics':'\\u{1F4CA} Analytics',
635
+ 'tasks.total':'Total Tasks',
636
+ 'tasks.active':'Active',
637
+ 'tasks.completed':'Completed',
638
+ 'tasks.status.active':'Active',
639
+ 'tasks.status.completed':'Completed',
640
+ 'tasks.status.skipped':'Skipped',
641
+ 'tasks.empty':'No tasks yet. Tasks are automatically created as you converse.',
642
+ 'tasks.loading':'Loading...',
643
+ 'tasks.untitled':'Untitled Task',
644
+ 'tasks.chunks':'Related Memories',
645
+ 'tasks.nochunks':'No memories in this task yet.',
646
+ 'tasks.skipped.default':'This conversation was too brief to generate a summary. It will not appear in search results.',
647
+ 'refresh':'\\u21BB Refresh',
648
+ 'logout':'Logout',
649
+ 'stat.memories':'Memories',
650
+ 'stat.sessions':'Sessions',
651
+ 'stat.embeddings':'Embeddings',
652
+ 'stat.days':'Days',
653
+ 'sidebar.sessions':'Sessions',
654
+ 'sidebar.allsessions':'All Sessions',
655
+ 'sidebar.clear':'\\u{1F5D1} Clear All Data',
656
+ 'search.placeholder':'Search memories (supports semantic search)...',
657
+ 'search.meta.total':' memories total',
658
+ 'search.meta.semantic':' semantic',
659
+ 'search.meta.text':' text',
660
+ 'search.meta.results':' results',
661
+ 'filter.all':'All',
662
+ 'filter.allkinds':'All kinds',
663
+ 'filter.paragraph':'Paragraph',
664
+ 'filter.code':'Code',
665
+ 'filter.dialog':'Dialog',
666
+ 'filter.list':'List',
667
+ 'filter.error':'Error',
668
+ 'filter.command':'Command',
669
+ 'filter.newest':'Newest first',
670
+ 'filter.oldest':'Oldest first',
671
+ 'filter.from':'From',
672
+ 'filter.to':'To',
673
+ 'filter.clear':'Clear',
674
+ 'empty.text':'No memories found',
675
+ 'card.expand':'Expand',
676
+ 'card.edit':'Edit',
677
+ 'card.delete':'Delete',
678
+ 'pagination.total':' total',
679
+ 'range':'Range',
680
+ 'range.days':'days',
681
+ 'analytics.total':'Total Memories',
682
+ 'analytics.writes':'Writes Today',
683
+ 'analytics.calls':'Viewer Calls Today',
684
+ 'analytics.sessions':'Sessions',
685
+ 'analytics.embeddings':'Embeddings',
686
+ 'chart.writes':'Memory Writes per Day',
687
+ 'chart.calls':'Viewer API Calls per Day (List / Search)',
688
+ 'chart.nodata':'No data in this range',
689
+ 'chart.nocalls':'No viewer calls in this range',
690
+ 'chart.list':'List',
691
+ 'chart.search':'Search',
692
+ 'breakdown.role':'By Role',
693
+ 'breakdown.kind':'By Kind',
694
+ 'modal.new':'New Memory',
695
+ 'modal.edit':'Edit Memory',
696
+ 'modal.role':'Role',
697
+ 'modal.content':'Content',
698
+ 'modal.content.ph':'Memory content...',
699
+ 'modal.summary':'Summary',
700
+ 'modal.summary.ph':'Brief summary (optional)',
701
+ 'modal.kind':'Kind',
702
+ 'modal.cancel':'Cancel',
703
+ 'modal.create':'Create',
704
+ 'modal.save':'Save',
705
+ 'modal.err.empty':'Please enter content',
706
+ 'toast.created':'Memory created',
707
+ 'toast.updated':'Memory updated',
708
+ 'toast.deleted':'Memory deleted',
709
+ 'toast.opfail':'Operation failed',
710
+ 'toast.delfail':'Delete failed',
711
+ 'toast.cleared':'All memories cleared',
712
+ 'toast.clearfail':'Clear failed',
713
+ 'toast.notfound':'Memory not found in cache',
714
+ 'confirm.delete':'Delete this memory?',
715
+ 'confirm.clearall':'Delete ALL memories? This cannot be undone.',
716
+ 'confirm.clearall2':'Are you absolutely sure?',
717
+ 'embed.on':'Embedding: ',
718
+ 'embed.off':'No embedding model',
719
+ 'lang.switch':'中'
720
+ },
721
+ zh:{
722
+ 'title':'OpenClaw 记忆',
723
+ 'subtitle':'由 MemOS 驱动',
724
+ 'setup.desc':'设置密码以保护你的记忆数据',
725
+ 'setup.pw':'输入密码(至少4位)',
726
+ 'setup.pw2':'确认密码',
727
+ 'setup.btn':'设置密码并进入',
728
+ 'setup.err.short':'密码至少需要4个字符',
729
+ 'setup.err.mismatch':'两次密码不一致',
730
+ 'setup.err.fail':'设置失败',
731
+ 'login.desc':'输入密码以访问记忆',
732
+ 'login.pw':'密码',
733
+ 'login.btn':'解锁',
734
+ 'login.err':'密码错误',
735
+ 'login.forgot':'忘记密码?',
736
+ 'reset.step1.title':'打开终端',
737
+ 'reset.step1.desc':'运行以下命令获取重置令牌:',
738
+ 'reset.step2.title':'找到令牌',
739
+ 'reset.step2.desc.pre':'在输出中找到 ',
740
+ 'reset.step2.desc.post':'(纯文本行或 JSON 内)。复制冒号后的32位十六进制字符串。',
741
+ 'reset.step3.title':'粘贴并重置',
742
+ 'reset.step3.desc':'将令牌粘贴到下方并设置新密码。',
743
+ 'reset.token':'在此粘贴重置令牌',
744
+ 'reset.newpw':'新密码(至少4位)',
745
+ 'reset.newpw2':'确认新密码',
746
+ 'reset.btn':'重置密码',
747
+ 'reset.err.token':'请输入重置令牌',
748
+ 'reset.err.short':'密码至少需要4个字符',
749
+ 'reset.err.mismatch':'两次密码不一致',
750
+ 'reset.err.fail':'重置失败',
751
+ 'reset.back':'\\u2190 返回登录',
752
+ 'copy.hint':'点击复制',
753
+ 'copy.done':'已复制!',
754
+ 'tab.memories':'\\u{1F4DA} 记忆',
755
+ 'tab.tasks':'\\u{1F4CB} 任务',
756
+ 'tab.analytics':'\\u{1F4CA} 分析',
757
+ 'tasks.total':'任务总数',
758
+ 'tasks.active':'进行中',
759
+ 'tasks.completed':'已完成',
760
+ 'tasks.status.active':'进行中',
761
+ 'tasks.status.completed':'已完成',
762
+ 'tasks.status.skipped':'已跳过',
763
+ 'tasks.empty':'暂无任务。任务会随着对话自动创建。',
764
+ 'tasks.loading':'加载中...',
765
+ 'tasks.untitled':'未命名任务',
766
+ 'tasks.chunks':'关联记忆',
767
+ 'tasks.nochunks':'此任务暂无关联记忆。',
768
+ 'tasks.skipped.default':'对话内容过少,未生成摘要。该任务不会出现在检索结果中。',
769
+ 'refresh':'\\u21BB 刷新',
770
+ 'logout':'退出',
771
+ 'stat.memories':'记忆',
772
+ 'stat.sessions':'会话',
773
+ 'stat.embeddings':'嵌入',
774
+ 'stat.days':'天数',
775
+ 'sidebar.sessions':'会话列表',
776
+ 'sidebar.allsessions':'全部会话',
777
+ 'sidebar.clear':'\\u{1F5D1} 清除所有数据',
778
+ 'search.placeholder':'搜索记忆(支持语义搜索)...',
779
+ 'search.meta.total':' 条记忆',
780
+ 'search.meta.semantic':' 语义',
781
+ 'search.meta.text':' 文本',
782
+ 'search.meta.results':' 条结果',
783
+ 'filter.all':'全部',
784
+ 'filter.allkinds':'所有类型',
785
+ 'filter.paragraph':'段落',
786
+ 'filter.code':'代码',
787
+ 'filter.dialog':'对话',
788
+ 'filter.list':'列表',
789
+ 'filter.error':'错误',
790
+ 'filter.command':'命令',
791
+ 'filter.newest':'最新优先',
792
+ 'filter.oldest':'最早优先',
793
+ 'filter.from':'起始',
794
+ 'filter.to':'截止',
795
+ 'filter.clear':'清除',
796
+ 'empty.text':'暂无记忆',
797
+ 'card.expand':'展开',
798
+ 'card.edit':'编辑',
799
+ 'card.delete':'删除',
800
+ 'pagination.total':' 条',
801
+ 'range':'范围',
802
+ 'range.days':'天',
803
+ 'analytics.total':'总记忆数',
804
+ 'analytics.writes':'今日写入',
805
+ 'analytics.calls':'今日查看器调用',
806
+ 'analytics.sessions':'会话数',
807
+ 'analytics.embeddings':'嵌入数',
808
+ 'chart.writes':'每日记忆写入',
809
+ 'chart.calls':'每日查看器 API 调用(列表 / 搜索)',
810
+ 'chart.nodata':'此范围内暂无数据',
811
+ 'chart.nocalls':'此范围内暂无查看器调用',
812
+ 'chart.list':'列表',
813
+ 'chart.search':'搜索',
814
+ 'breakdown.role':'按角色',
815
+ 'breakdown.kind':'按类型',
816
+ 'modal.new':'新建记忆',
817
+ 'modal.edit':'编辑记忆',
818
+ 'modal.role':'角色',
819
+ 'modal.content':'内容',
820
+ 'modal.content.ph':'记忆内容...',
821
+ 'modal.summary':'摘要',
822
+ 'modal.summary.ph':'简要摘要(可选)',
823
+ 'modal.kind':'类型',
824
+ 'modal.cancel':'取消',
825
+ 'modal.create':'创建',
826
+ 'modal.save':'保存',
827
+ 'modal.err.empty':'请输入内容',
828
+ 'toast.created':'记忆已创建',
829
+ 'toast.updated':'记忆已更新',
830
+ 'toast.deleted':'记忆已删除',
831
+ 'toast.opfail':'操作失败',
832
+ 'toast.delfail':'删除失败',
833
+ 'toast.cleared':'所有记忆已清除',
834
+ 'toast.clearfail':'清除失败',
835
+ 'toast.notfound':'缓存中未找到此记忆',
836
+ 'confirm.delete':'确定要删除这条记忆吗?',
837
+ 'confirm.clearall':'确定要删除所有记忆?此操作不可撤销。',
838
+ 'confirm.clearall2':'你真的确定吗?',
839
+ 'embed.on':'嵌入模型:',
840
+ 'embed.off':'无嵌入模型',
841
+ 'lang.switch':'EN'
842
+ }
843
+ };
844
+ const LANG_KEY='memos-viewer-lang';
845
+ let curLang=localStorage.getItem(LANG_KEY)||(navigator.language.startsWith('zh')?'zh':'en');
846
+ function t(key){return (I18N[curLang]||I18N.en)[key]||key;}
847
+ function setLang(lang){curLang=lang;localStorage.setItem(LANG_KEY,lang);applyI18n();}
848
+ function toggleLang(){setLang(curLang==='zh'?'en':'zh');}
849
+
850
+ function applyI18n(){
851
+ document.querySelectorAll('[data-i18n]').forEach(el=>{
852
+ const key=el.getAttribute('data-i18n');
853
+ if(key) el.textContent=t(key);
854
+ });
855
+ document.querySelectorAll('[data-i18n-ph]').forEach(el=>{
856
+ const key=el.getAttribute('data-i18n-ph');
857
+ if(key) el.placeholder=t(key);
858
+ });
859
+ const step2=document.getElementById('resetStep2Desc');
860
+ if(step2) step2.innerHTML=t('reset.step2.desc.pre')+'<span style="font-family:monospace;font-size:12px;color:var(--pri)">password reset token: <strong>a1b2c3d4e5f6...</strong></span>'+t('reset.step2.desc.post');
861
+ document.title=t('title')+' - MemOS';
862
+ if(typeof loadStats==='function' && document.getElementById('app').style.display==='flex'){loadStats();}
863
+ if(document.querySelector('.analytics-view.show') && typeof loadMetrics==='function'){loadMetrics();}
864
+ }
377
865
 
378
866
  /* ─── Auth flow ─── */
379
867
  async function checkAuth(){
@@ -395,12 +883,12 @@ async function doSetup(){
395
883
  const pw=document.getElementById('setupPw').value;
396
884
  const pw2=document.getElementById('setupPw2').value;
397
885
  const err=document.getElementById('setupErr');
398
- if(pw.length<4){err.textContent='Password must be at least 4 characters';return}
399
- if(pw!==pw2){err.textContent='Passwords do not match';return}
886
+ if(pw.length<4){err.textContent=t('setup.err.short');return}
887
+ if(pw!==pw2){err.textContent=t('setup.err.mismatch');return}
400
888
  const r=await fetch('/api/auth/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});
401
889
  const d=await r.json();
402
890
  if(d.ok){document.getElementById('setupScreen').style.display='none';enterApp();}
403
- else{err.textContent=d.error||'Setup failed'}
891
+ else{err.textContent=d.error||t('setup.err.fail')}
404
892
  }
405
893
 
406
894
  async function doLogin(){
@@ -409,7 +897,7 @@ async function doLogin(){
409
897
  const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});
410
898
  const d=await r.json();
411
899
  if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}
412
- else{err.textContent='Incorrect password';document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}
900
+ else{err.textContent=t('login.err');document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}
413
901
  }
414
902
 
415
903
  async function doLogout(){
@@ -433,8 +921,8 @@ function copyCmd(el){
433
921
  const code=el.querySelector('code').textContent;
434
922
  navigator.clipboard.writeText(code).then(()=>{
435
923
  el.classList.add('copied');
436
- el.querySelector('.copy-hint').textContent='Copied!';
437
- setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent='Click to copy'},2000);
924
+ el.querySelector('.copy-hint').textContent=t('copy.done');
925
+ setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent=t('copy.hint')},2000);
438
926
  });
439
927
  }
440
928
 
@@ -443,13 +931,13 @@ async function doReset(){
443
931
  const pw=document.getElementById('resetNewPw').value;
444
932
  const pw2=document.getElementById('resetNewPw2').value;
445
933
  const err=document.getElementById('resetErr');
446
- if(!token){err.textContent='Please enter the reset token';return}
447
- if(pw.length<4){err.textContent='Password must be at least 4 characters';return}
448
- if(pw!==pw2){err.textContent='Passwords do not match';return}
934
+ if(!token){err.textContent=t('reset.err.token');return}
935
+ if(pw.length<4){err.textContent=t('reset.err.short');return}
936
+ if(pw!==pw2){err.textContent=t('reset.err.mismatch');return}
449
937
  const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,newPassword:pw})});
450
938
  const d=await r.json();
451
939
  if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}
452
- else{err.textContent=d.error||'Reset failed'}
940
+ else{err.textContent=d.error||t('reset.err.fail')}
453
941
  }
454
942
 
455
943
  function enterApp(){
@@ -457,6 +945,281 @@ function enterApp(){
457
945
  loadAll();
458
946
  }
459
947
 
948
+ function switchView(view){
949
+ document.querySelectorAll('.nav-tabs .tab').forEach(t=>t.classList.toggle('active',t.dataset.view===view));
950
+ const feedWrap=document.getElementById('feedWrap');
951
+ const analyticsView=document.getElementById('analyticsView');
952
+ const tasksView=document.getElementById('tasksView');
953
+ feedWrap.classList.add('hide');
954
+ analyticsView.classList.remove('show');
955
+ tasksView.classList.remove('show');
956
+ if(view==='analytics'){
957
+ analyticsView.classList.add('show');
958
+ loadMetrics();
959
+ } else if(view==='tasks'){
960
+ tasksView.classList.add('show');
961
+ loadTasks();
962
+ } else {
963
+ feedWrap.classList.remove('hide');
964
+ }
965
+ }
966
+
967
+ function setMetricsDays(d){
968
+ metricsDays=d;
969
+ document.querySelectorAll('.metrics-toolbar .range-btn').forEach(btn=>btn.classList.toggle('active',Number(btn.dataset.days)===d));
970
+ loadMetrics();
971
+ }
972
+
973
+ async function loadMetrics(){
974
+ const r=await fetch('/api/metrics?days='+metricsDays);
975
+ const d=await r.json();
976
+ document.getElementById('mTotal').textContent=formatNum(d.totals.memories);
977
+ document.getElementById('mTodayWrites').textContent=formatNum(d.totals.todayWrites);
978
+ document.getElementById('mTodayCalls').textContent=formatNum(d.totals.todayViewerCalls);
979
+ document.getElementById('mSessions').textContent=formatNum(d.totals.sessions);
980
+ document.getElementById('mEmbeddings').textContent=formatNum(d.totals.embeddings);
981
+ renderChartWrites(d.writesPerDay);
982
+ renderChartCalls(d.viewerCallsPerDay);
983
+ renderBreakdown(d.roleBreakdown,'breakdownRole');
984
+ renderBreakdown(d.kindBreakdown,'breakdownKind');
985
+ }
986
+
987
+ function formatNum(n){return n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'k':String(n);}
988
+
989
+ /* ─── Tasks View Logic ─── */
990
+ let tasksStatusFilter='';
991
+ let tasksPage=0;
992
+ const TASKS_PER_PAGE=20;
993
+
994
+ function setTaskStatusFilter(btn,status){
995
+ document.querySelectorAll('.tasks-filters .filter-chip').forEach(c=>c.classList.remove('active'));
996
+ btn.classList.add('active');
997
+ tasksStatusFilter=status;
998
+ tasksPage=0;
999
+ loadTasks();
1000
+ }
1001
+
1002
+ async function loadTasks(){
1003
+ const list=document.getElementById('tasksList');
1004
+ list.innerHTML='<div class="spinner"></div>';
1005
+ try{
1006
+ const params=new URLSearchParams({limit:String(TASKS_PER_PAGE),offset:String(tasksPage*TASKS_PER_PAGE)});
1007
+ if(tasksStatusFilter) params.set('status',tasksStatusFilter);
1008
+ const r=await fetch('/api/tasks?'+params);
1009
+ const data=await r.json();
1010
+
1011
+ // stats
1012
+ const allR=await fetch('/api/tasks?limit=1&offset=0');
1013
+ const allD=await allR.json();
1014
+ document.getElementById('tasksTotalCount').textContent=formatNum(allD.total);
1015
+
1016
+ const activeR=await fetch('/api/tasks?status=active&limit=1&offset=0');
1017
+ const activeD=await activeR.json();
1018
+ document.getElementById('tasksActiveCount').textContent=formatNum(activeD.total);
1019
+
1020
+ const compR=await fetch('/api/tasks?status=completed&limit=1&offset=0');
1021
+ const compD=await compR.json();
1022
+ document.getElementById('tasksCompletedCount').textContent=formatNum(compD.total);
1023
+
1024
+ const skipR=await fetch('/api/tasks?status=skipped&limit=1&offset=0');
1025
+ const skipD=await skipR.json();
1026
+ document.getElementById('tasksSkippedCount').textContent=formatNum(skipD.total);
1027
+
1028
+ if(!data.tasks||data.tasks.length===0){
1029
+ list.innerHTML='<div style="text-align:center;padding:48px;color:var(--text-muted);font-size:14px" data-i18n="tasks.empty">'+t('tasks.empty')+'</div>';
1030
+ document.getElementById('tasksPagination').innerHTML='';
1031
+ return;
1032
+ }
1033
+
1034
+ list.innerHTML=data.tasks.map(task=>{
1035
+ const timeStr=formatTime(task.startedAt);
1036
+ const endStr=task.endedAt?formatTime(task.endedAt):'';
1037
+ const durationStr=task.endedAt?formatDuration(task.endedAt-task.startedAt):'';
1038
+ return '<div class="task-card status-'+task.status+'" onclick="openTaskDetail(\\''+task.id+'\\')">'+
1039
+ '<div class="task-card-top">'+
1040
+ '<div class="task-card-title">'+esc(task.title)+'</div>'+
1041
+ '<span class="task-status-badge '+task.status+'">'+task.status+'</span>'+
1042
+ '</div>'+
1043
+ (task.summary?'<div class="task-card-summary">'+esc(task.summary)+'</div>':'')+
1044
+ '<div class="task-card-bottom">'+
1045
+ '<span class="tag"><span class="icon">\\u{1F4C5}</span> '+timeStr+'</span>'+
1046
+ (durationStr?'<span class="tag"><span class="icon">\\u23F1</span> '+durationStr+'</span>':'')+
1047
+ '<span class="tag"><span class="icon">\\u{1F4DD}</span> '+task.chunkCount+' '+(task.chunkCount===1?'chunk':'chunks')+'</span>'+
1048
+ '<span class="tag"><span class="icon">\\u{1F4C2}</span> '+task.sessionKey.slice(0,12)+'</span>'+
1049
+ '</div>'+
1050
+ '</div>';
1051
+ }).join('');
1052
+
1053
+ renderTasksPagination(data.total);
1054
+ }catch(e){
1055
+ list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">Failed to load tasks</div>';
1056
+ }
1057
+ }
1058
+
1059
+ function renderTasksPagination(total){
1060
+ const el=document.getElementById('tasksPagination');
1061
+ const pages=Math.ceil(total/TASKS_PER_PAGE);
1062
+ if(pages<=1){el.innerHTML='';return;}
1063
+ let html='<button class="pg-btn'+(tasksPage===0?' disabled':'')+'" onclick="tasksPage=Math.max(0,tasksPage-1);loadTasks()">\\u2190</button>';
1064
+ const start=Math.max(0,tasksPage-2),end=Math.min(pages,tasksPage+3);
1065
+ for(let i=start;i<end;i++){
1066
+ html+='<button class="pg-btn'+(i===tasksPage?' active':'')+'" onclick="tasksPage='+i+';loadTasks()">'+(i+1)+'</button>';
1067
+ }
1068
+ html+='<button class="pg-btn'+(tasksPage>=pages-1?' disabled':'')+'" onclick="tasksPage=Math.min('+(pages-1)+',tasksPage+1);loadTasks()">\\u2192</button>';
1069
+ html+='<span class="pg-info">'+total+' '+t('pagination.total')+'</span>';
1070
+ el.innerHTML=html;
1071
+ }
1072
+
1073
+ async function openTaskDetail(taskId){
1074
+ const overlay=document.getElementById('taskDetailOverlay');
1075
+ overlay.classList.add('show');
1076
+ document.getElementById('taskDetailTitle').textContent=t('tasks.loading');
1077
+ document.getElementById('taskDetailMeta').innerHTML='';
1078
+ document.getElementById('taskDetailSummary').textContent='';
1079
+ document.getElementById('taskDetailChunks').innerHTML='<div class="spinner"></div>';
1080
+
1081
+ try{
1082
+ const r=await fetch('/api/task/'+taskId);
1083
+ const task=await r.json();
1084
+
1085
+ document.getElementById('taskDetailTitle').textContent=task.title||t('tasks.untitled');
1086
+
1087
+ const meta=[
1088
+ '<span class="meta-item"><span class="task-status-badge '+task.status+'">'+task.status+'</span></span>',
1089
+ '<span class="meta-item">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>',
1090
+ ];
1091
+ if(task.endedAt) meta.push('<span class="meta-item">\\u2192 '+formatTime(task.endedAt)+'</span>');
1092
+ meta.push('<span class="meta-item">\\u{1F4C2} '+task.sessionKey+'</span>');
1093
+ meta.push('<span class="meta-item">\\u{1F4DD} '+task.chunks.length+' chunks</span>');
1094
+ document.getElementById('taskDetailMeta').innerHTML=meta.join('');
1095
+
1096
+ var summaryEl=document.getElementById('taskDetailSummary');
1097
+ if(task.status==='skipped'){
1098
+ summaryEl.innerHTML='<div style="color:var(--text-muted);font-style:italic;display:flex;align-items:flex-start;gap:8px"><span style="font-size:18px">\\u26A0\\uFE0F</span><span>'+esc(task.summary||t('tasks.skipped.default'))+'</span></div>';
1099
+ }else{
1100
+ summaryEl.innerHTML=renderSummaryHtml(task.summary);
1101
+ }
1102
+
1103
+ if(task.chunks.length===0){
1104
+ document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--text-muted);padding:12px;font-size:13px">'+t('tasks.nochunks')+'</div>';
1105
+ }else{
1106
+ document.getElementById('taskDetailChunks').innerHTML=task.chunks.map(c=>{
1107
+ var roleLabel=c.role==='user'?'You':c.role==='assistant'?'Assistant':c.role.toUpperCase();
1108
+ return '<div class="task-chunk-item role-'+c.role+'">'+
1109
+ '<div class="task-chunk-role '+c.role+'">'+roleLabel+'</div>'+
1110
+ '<div class="task-chunk-bubble" onclick="this.classList.toggle(\\\'expanded\\\')">'+esc(c.content)+'</div>'+
1111
+ '<div class="task-chunk-time">'+formatTime(c.createdAt)+'</div>'+
1112
+ '</div>';
1113
+ }).join('');
1114
+ }
1115
+ }catch(e){
1116
+ document.getElementById('taskDetailTitle').textContent='Error';
1117
+ document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--rose)">Failed to load task details</div>';
1118
+ }
1119
+ }
1120
+
1121
+ function closeTaskDetail(event){
1122
+ if(event && event.target!==document.getElementById('taskDetailOverlay')) return;
1123
+ document.getElementById('taskDetailOverlay').classList.remove('show');
1124
+ }
1125
+
1126
+ function formatDuration(ms){
1127
+ const s=Math.floor(ms/1000);
1128
+ if(s<60) return s+'s';
1129
+ const m=Math.floor(s/60);
1130
+ if(m<60) return m+'min';
1131
+ const h=Math.floor(m/60);
1132
+ if(h<24) return h+'h '+((m%60)>0?(m%60)+'min':'');
1133
+ const d=Math.floor(h/24);
1134
+ return d+'d '+((h%24)>0?(h%24)+'h':'');
1135
+ }
1136
+
1137
+ function formatTime(ts){
1138
+ if(!ts) return '-';
1139
+ return new Date(ts).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
1140
+ }
1141
+
1142
+ function fillDays(rows,days){
1143
+ const map=new Map((rows||[]).map(r=>[r.date,{...r}]));
1144
+ const out=[];const now=new Date();
1145
+ for(let i=days-1;i>=0;i--){
1146
+ const d=new Date(now);d.setDate(d.getDate()-i);
1147
+ const dateStr=d.toISOString().slice(0,10);
1148
+ const row=map.get(dateStr)||{};
1149
+ out.push({date:dateStr,count:row.count??0,list:row.list??0,search:row.search??0,total:(row.list??0)+(row.search??0)});
1150
+ }
1151
+ if(days>21){
1152
+ const weeks=[];let i=0;
1153
+ while(i<out.length){
1154
+ const chunk=out.slice(i,i+7);
1155
+ const first=chunk[0].date,last=chunk[chunk.length-1].date;
1156
+ const c=chunk.reduce((s,r)=>s+r.count,0);
1157
+ const l=chunk.reduce((s,r)=>s+r.list,0);
1158
+ const se=chunk.reduce((s,r)=>s+r.search,0);
1159
+ const label=first.slice(5,10)+'~'+last.slice(8,10);
1160
+ weeks.push({date:label,count:c,list:l,search:se,total:l+se});
1161
+ i+=7;
1162
+ }
1163
+ return weeks;
1164
+ }
1165
+ return out;
1166
+ }
1167
+
1168
+ function renderBars(el,data,valueKey,H){
1169
+ const vals=data.map(d=>d[valueKey]??0);
1170
+ if(vals.every(v=>v===0)){el.innerHTML='<div style="color:var(--text-muted);font-size:13px;padding:20px;text-align:center">'+t('chart.nodata')+'</div>';return;}
1171
+ const max=Math.max(1,...vals);
1172
+ el.innerHTML=data.map(r=>{
1173
+ const v=r[valueKey]??0;
1174
+ const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
1175
+ if(v===0){
1176
+ return '<div class="chart-bar-wrap"><div class="chart-tip">0</div><div class="chart-bar-col"><div class="chart-bar zero" style="height:2px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
1177
+ }
1178
+ const h=Math.max(8,Math.round((v/max)*H));
1179
+ return '<div class="chart-bar-wrap"><div class="chart-tip">'+v+'</div><div class="chart-bar-col"><div class="chart-bar" style="height:'+h+'px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
1180
+ }).join('');
1181
+ }
1182
+
1183
+ function renderChartWrites(rows){
1184
+ const el=document.getElementById('chartWrites');
1185
+ const filled=fillDays(rows?.map(r=>({date:r.date,count:r.count})),metricsDays);
1186
+ renderBars(el,filled,'count',160);
1187
+ }
1188
+
1189
+ function renderChartCalls(rows){
1190
+ const el=document.getElementById('chartCalls');
1191
+ const filled=fillDays(rows?.map(r=>({date:r.date,list:r.list,search:r.search})),metricsDays);
1192
+ const vals=filled.map(f=>f.total);
1193
+ if(vals.every(v=>v===0)){el.innerHTML='<div style="color:var(--text-muted);font-size:13px;padding:20px;text-align:center">'+t('chart.nocalls')+'</div>';return;}
1194
+ const max=Math.max(1,...vals);
1195
+ const H=160;
1196
+ el.innerHTML=filled.map(r=>{
1197
+ const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
1198
+ if(r.total===0){
1199
+ return '<div class="chart-bar-wrap"><div class="chart-tip">0</div><div class="chart-bar-col"><div class="chart-bar zero" style="height:2px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
1200
+ }
1201
+ const totalH=Math.max(8,Math.round((r.total/max)*H));
1202
+ const listH=r.list?Math.max(3,Math.round((r.list/r.total)*totalH)):0;
1203
+ const searchH=r.search?totalH-listH:0;
1204
+ const tip='List: '+r.list+', Search: '+r.search;
1205
+ let bars='';
1206
+ if(searchH>0) bars+='<div class="chart-bar violet" style="height:'+searchH+'px"></div>';
1207
+ if(listH>0) bars+='<div class="chart-bar" style="height:'+listH+'px"></div>';
1208
+ return '<div class="chart-bar-wrap"><div class="chart-tip">'+tip+'</div><div class="chart-bar-col"><div style="display:flex;flex-direction:column;gap:1px">'+bars+'</div></div><div class="chart-bar-label">'+label+'</div></div>';
1209
+ }).join('');
1210
+ }
1211
+
1212
+ function renderBreakdown(obj,containerId){
1213
+ const el=document.getElementById(containerId);
1214
+ if(!el)return;
1215
+ const entries=Object.entries(obj||{}).sort((a,b)=>b[1]-a[1]);
1216
+ const total=entries.reduce((s,[,v])=>s+v,0)||1;
1217
+ el.innerHTML=entries.map(([label,value])=>{
1218
+ const pct=Math.round((value/total)*100);
1219
+ return '<div class="breakdown-item"><div class="bd-top"><span class="label">'+esc(label)+'</span><span class="value">'+value+' <span style="font-size:11px;font-weight:500;color:var(--text-muted)">('+pct+'%)</span></span></div><div class="breakdown-bar-wrap"><div class="breakdown-bar" style="width:'+pct+'%"></div></div></div>';
1220
+ }).join('');
1221
+ }
1222
+
460
1223
  /* ─── Data loading ─── */
461
1224
  async function loadAll(){
462
1225
  await Promise.all([loadStats(),loadMemories()]);
@@ -473,13 +1236,13 @@ async function loadStats(){
473
1236
 
474
1237
  const provEl=document.getElementById('embeddingStatus');
475
1238
  if(d.embeddingProvider && d.embeddingProvider!=='none'){
476
- provEl.innerHTML='<div class="provider-badge"><span>\\u2713</span> Embedding: '+d.embeddingProvider+'</div>';
1239
+ provEl.innerHTML='<div class="provider-badge"><span>\\u2713</span> '+t('embed.on')+d.embeddingProvider+'</div>';
477
1240
  } else {
478
- provEl.innerHTML='<div class="provider-badge offline"><span>\\u26A0</span> No embedding model</div>';
1241
+ provEl.innerHTML='<div class="provider-badge offline"><span>\\u26A0</span> '+t('embed.off')+'</div>';
479
1242
  }
480
1243
 
481
1244
  const sl=document.getElementById('sessionList');
482
- sl.innerHTML='<div class="session-item'+(activeSession===null?' active':'')+'" onclick="filterSession(null)"><span>All Sessions</span><span class="count">'+d.totalMemories+'</span></div>';
1245
+ sl.innerHTML='<div class="session-item'+(activeSession===null?' active':'')+'" onclick="filterSession(null)"><span>'+t('sidebar.allsessions')+'</span><span class="count">'+d.totalMemories+'</span></div>';
483
1246
  (d.sessions||[]).forEach(s=>{
484
1247
  const isActive=activeSession===s.session_key;
485
1248
  const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;
@@ -513,7 +1276,7 @@ async function loadMemories(page){
513
1276
  const d=await r.json();
514
1277
  totalPages=d.totalPages||1;
515
1278
  totalCount=d.total||0;
516
- document.getElementById('searchMeta').textContent=totalCount+' memories total';
1279
+ document.getElementById('searchMeta').textContent=totalCount+t('search.meta.total');
517
1280
  renderMemories(d.memories||[]);
518
1281
  renderPagination();
519
1282
  }
@@ -527,9 +1290,9 @@ async function doSearch(q){
527
1290
  const r=await fetch('/api/search?'+p.toString());
528
1291
  const d=await r.json();
529
1292
  const meta=[];
530
- if(d.vectorCount>0) meta.push(d.vectorCount+' semantic');
531
- if(d.ftsCount>0) meta.push(d.ftsCount+' text');
532
- meta.push(d.total+' results');
1293
+ if(d.vectorCount>0) meta.push(d.vectorCount+t('search.meta.semantic'));
1294
+ if(d.ftsCount>0) meta.push(d.ftsCount+t('search.meta.text'));
1295
+ meta.push(d.total+t('search.meta.results'));
533
1296
  document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');
534
1297
  renderMemories(d.results||[]);
535
1298
  document.getElementById('pagination').innerHTML='';
@@ -573,7 +1336,7 @@ function clearDateFilter(){
573
1336
  function renderMemories(items){
574
1337
  const list=document.getElementById('memoryList');
575
1338
  if(!items.length){
576
- list.innerHTML='<div class="empty"><div class="icon">\\u{1F4ED}</div><p>No memories found</p></div>';
1339
+ list.innerHTML='<div class="empty"><div class="icon">\\u{1F4ED}</div><p>'+t('empty.text')+'</p></div>';
577
1340
  return;
578
1341
  }
579
1342
  items.forEach(m=>{memoryCache[m.id]=m});
@@ -592,9 +1355,9 @@ function renderMemories(items){
592
1355
  '<div class="card-summary">'+summary+'</div>'+
593
1356
  '<div class="card-content" id="content-'+id+'"><pre>'+content+'</pre></div>'+
594
1357
  '<div class="card-actions">'+
595
- '<button class="btn btn-sm btn-text" onclick="toggleContent(\\''+id+'\\')">Expand</button>'+
596
- '<button class="btn btn-sm" onclick="openEditModal(\\''+id+'\\')">Edit</button>'+
597
- '<button class="btn btn-sm btn-danger" onclick="deleteMemory(\\''+id+'\\')">Delete</button>'+
1358
+ '<button class="btn btn-sm btn-ghost" onclick="toggleContent(\\''+id+'\\')">'+t('card.expand')+'</button>'+
1359
+ '<button class="btn btn-sm btn-ghost" onclick="openEditModal(\\''+id+'\\')">'+t('card.edit')+'</button>'+
1360
+ '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteMemory(\\''+id+'\\')">'+t('card.delete')+'</button>'+
598
1361
  vscore+
599
1362
  '</div></div>';
600
1363
  }).join('');
@@ -617,7 +1380,7 @@ function renderPagination(){
617
1380
  prev=p;
618
1381
  }
619
1382
  h+='<button class="pg-btn'+(currentPage>=totalPages?' disabled':'')+'" onclick="goPage('+(currentPage+1)+')">\u203A</button>';
620
- h+='<span class="pg-info">'+totalCount+' total</span>';
1383
+ h+='<span class="pg-info">'+totalCount+t('pagination.total')+'</span>';
621
1384
  el.innerHTML=h;
622
1385
  }
623
1386
 
@@ -638,11 +1401,43 @@ function esc(s){
638
1401
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
639
1402
  }
640
1403
 
1404
+ function renderSummaryHtml(raw){
1405
+ if(!raw)return'';
1406
+ var lines=raw.split('\\n');
1407
+ var html=[];
1408
+ var inList=false;
1409
+ var sectionRe=new RegExp('^(\u{1F3AF}|\u{1F4CB}|\u2705|\u{1F4A1})\\\\s+(.+)$');
1410
+ var listRe=new RegExp('^- (.+)$');
1411
+ for(var i=0;i<lines.length;i++){
1412
+ var line=lines[i];
1413
+ var hm=line.match(sectionRe);
1414
+ if(hm){
1415
+ if(inList){html.push('</ul>');inList=false;}
1416
+ html.push('<div class="summary-section-title">'+esc(line)+'</div>');
1417
+ continue;
1418
+ }
1419
+ var lm=line.match(listRe);
1420
+ if(lm){
1421
+ if(!inList){html.push('<ul>');inList=true;}
1422
+ html.push('<li>'+esc(lm[1])+'</li>');
1423
+ continue;
1424
+ }
1425
+ if(line.trim()===''){
1426
+ if(inList){html.push('</ul>');inList=false;}
1427
+ continue;
1428
+ }
1429
+ if(inList){html.push('</ul>');inList=false;}
1430
+ html.push('<p style="margin:4px 0">'+esc(line)+'</p>');
1431
+ }
1432
+ if(inList)html.push('</ul>');
1433
+ return html.join('');
1434
+ }
1435
+
641
1436
  /* ─── CRUD ─── */
642
1437
  function openCreateModal(){
643
1438
  editingId=null;
644
- document.getElementById('modalTitle').textContent='New Memory';
645
- document.getElementById('modalSubmit').textContent='Create';
1439
+ document.getElementById('modalTitle').textContent=t('modal.new');
1440
+ document.getElementById('modalSubmit').textContent=t('modal.create');
646
1441
  document.getElementById('mRole').value='user';
647
1442
  document.getElementById('mContent').value='';
648
1443
  document.getElementById('mSummary').value='';
@@ -652,10 +1447,10 @@ function openCreateModal(){
652
1447
 
653
1448
  function openEditModal(id){
654
1449
  const m=memoryCache[id];
655
- if(!m){toast('Memory not found in cache','error');return}
1450
+ if(!m){toast(t('toast.notfound'),'error');return}
656
1451
  editingId=id;
657
- document.getElementById('modalTitle').textContent='Edit Memory';
658
- document.getElementById('modalSubmit').textContent='Save';
1452
+ document.getElementById('modalTitle').textContent=t('modal.edit');
1453
+ document.getElementById('modalSubmit').textContent=t('modal.save');
659
1454
  document.getElementById('mRole').value=m.role||'user';
660
1455
  document.getElementById('mContent').value=m.content||'';
661
1456
  document.getElementById('mSummary').value=m.summary||'';
@@ -674,7 +1469,7 @@ async function submitModal(){
674
1469
  summary:document.getElementById('mSummary').value,
675
1470
  kind:document.getElementById('mKind').value,
676
1471
  };
677
- if(!data.content.trim()){toast('Please enter content','error');return}
1472
+ if(!data.content.trim()){toast(t('modal.err.empty'),'error');return}
678
1473
  let r;
679
1474
  if(editingId){
680
1475
  r=await fetch('/api/memory/'+editingId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
@@ -682,25 +1477,25 @@ async function submitModal(){
682
1477
  r=await fetch('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
683
1478
  }
684
1479
  const d=await r.json();
685
- if(d.ok){toast(editingId?'Memory updated':'Memory created','success');closeModal();loadAll();}
686
- else{toast(d.error||'Operation failed','error')}
1480
+ if(d.ok){toast(editingId?t('toast.updated'):t('toast.created'),'success');closeModal();loadAll();}
1481
+ else{toast(d.error||t('toast.opfail'),'error')}
687
1482
  }
688
1483
 
689
1484
  async function deleteMemory(id){
690
- if(!confirm('Delete this memory?'))return;
1485
+ if(!confirm(t('confirm.delete')))return;
691
1486
  const r=await fetch('/api/memory/'+id,{method:'DELETE'});
692
1487
  const d=await r.json();
693
- if(d.ok){toast('Memory deleted','success');loadAll();}
694
- else{toast('Delete failed','error')}
1488
+ if(d.ok){toast(t('toast.deleted'),'success');loadAll();}
1489
+ else{toast(t('toast.delfail'),'error')}
695
1490
  }
696
1491
 
697
1492
  async function clearAll(){
698
- if(!confirm('Delete ALL memories? This cannot be undone.'))return;
699
- if(!confirm('Are you absolutely sure?'))return;
1493
+ if(!confirm(t('confirm.clearall')))return;
1494
+ if(!confirm(t('confirm.clearall2')))return;
700
1495
  const r=await fetch('/api/memories',{method:'DELETE'});
701
1496
  const d=await r.json();
702
- if(d.ok){toast('All memories cleared','success');loadAll();}
703
- else{toast('Clear failed','error')}
1497
+ if(d.ok){toast(t('toast.cleared'),'success');loadAll();}
1498
+ else{toast(t('toast.clearfail'),'error')}
704
1499
  }
705
1500
 
706
1501
  /* ─── Toast ─── */
@@ -723,6 +1518,7 @@ initViewerTheme();
723
1518
  /* ─── Init ─── */
724
1519
  document.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});
725
1520
  document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});
1521
+ applyI18n();
726
1522
  checkAuth();
727
1523
  </script>
728
1524
  </body>