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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.env.example +13 -5
  2. package/README.md +283 -91
  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/dedup.d.ts +8 -0
  8. package/dist/ingest/dedup.d.ts.map +1 -1
  9. package/dist/ingest/dedup.js +21 -0
  10. package/dist/ingest/dedup.js.map +1 -1
  11. package/dist/ingest/providers/anthropic.d.ts +16 -0
  12. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  13. package/dist/ingest/providers/anthropic.js +214 -1
  14. package/dist/ingest/providers/anthropic.js.map +1 -1
  15. package/dist/ingest/providers/bedrock.d.ts +16 -5
  16. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  17. package/dist/ingest/providers/bedrock.js +210 -6
  18. package/dist/ingest/providers/bedrock.js.map +1 -1
  19. package/dist/ingest/providers/gemini.d.ts +16 -0
  20. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  21. package/dist/ingest/providers/gemini.js +202 -1
  22. package/dist/ingest/providers/gemini.js.map +1 -1
  23. package/dist/ingest/providers/index.d.ts +31 -0
  24. package/dist/ingest/providers/index.d.ts.map +1 -1
  25. package/dist/ingest/providers/index.js +134 -4
  26. package/dist/ingest/providers/index.js.map +1 -1
  27. package/dist/ingest/providers/openai.d.ts +24 -0
  28. package/dist/ingest/providers/openai.d.ts.map +1 -1
  29. package/dist/ingest/providers/openai.js +255 -1
  30. package/dist/ingest/providers/openai.js.map +1 -1
  31. package/dist/ingest/task-processor.d.ts +65 -0
  32. package/dist/ingest/task-processor.d.ts.map +1 -0
  33. package/dist/ingest/task-processor.js +354 -0
  34. package/dist/ingest/task-processor.js.map +1 -0
  35. package/dist/ingest/worker.d.ts +3 -1
  36. package/dist/ingest/worker.d.ts.map +1 -1
  37. package/dist/ingest/worker.js +131 -23
  38. package/dist/ingest/worker.js.map +1 -1
  39. package/dist/recall/engine.d.ts +1 -0
  40. package/dist/recall/engine.d.ts.map +1 -1
  41. package/dist/recall/engine.js +22 -11
  42. package/dist/recall/engine.js.map +1 -1
  43. package/dist/recall/mmr.d.ts.map +1 -1
  44. package/dist/recall/mmr.js +3 -1
  45. package/dist/recall/mmr.js.map +1 -1
  46. package/dist/skill/bundled-memory-guide.d.ts +6 -0
  47. package/dist/skill/bundled-memory-guide.d.ts.map +1 -0
  48. package/dist/skill/bundled-memory-guide.js +95 -0
  49. package/dist/skill/bundled-memory-guide.js.map +1 -0
  50. package/dist/skill/evaluator.d.ts +31 -0
  51. package/dist/skill/evaluator.d.ts.map +1 -0
  52. package/dist/skill/evaluator.js +194 -0
  53. package/dist/skill/evaluator.js.map +1 -0
  54. package/dist/skill/evolver.d.ts +22 -0
  55. package/dist/skill/evolver.d.ts.map +1 -0
  56. package/dist/skill/evolver.js +193 -0
  57. package/dist/skill/evolver.js.map +1 -0
  58. package/dist/skill/generator.d.ts +25 -0
  59. package/dist/skill/generator.d.ts.map +1 -0
  60. package/dist/skill/generator.js +477 -0
  61. package/dist/skill/generator.js.map +1 -0
  62. package/dist/skill/installer.d.ts +16 -0
  63. package/dist/skill/installer.d.ts.map +1 -0
  64. package/dist/skill/installer.js +89 -0
  65. package/dist/skill/installer.js.map +1 -0
  66. package/dist/skill/upgrader.d.ts +19 -0
  67. package/dist/skill/upgrader.d.ts.map +1 -0
  68. package/dist/skill/upgrader.js +263 -0
  69. package/dist/skill/upgrader.js.map +1 -0
  70. package/dist/skill/validator.d.ts +29 -0
  71. package/dist/skill/validator.d.ts.map +1 -0
  72. package/dist/skill/validator.js +227 -0
  73. package/dist/skill/validator.js.map +1 -0
  74. package/dist/storage/sqlite.d.ts +141 -1
  75. package/dist/storage/sqlite.d.ts.map +1 -1
  76. package/dist/storage/sqlite.js +664 -7
  77. package/dist/storage/sqlite.js.map +1 -1
  78. package/dist/types.d.ts +93 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/types.js +8 -0
  81. package/dist/types.js.map +1 -1
  82. package/dist/viewer/html.d.ts +1 -1
  83. package/dist/viewer/html.d.ts.map +1 -1
  84. package/dist/viewer/html.js +2391 -159
  85. package/dist/viewer/html.js.map +1 -1
  86. package/dist/viewer/server.d.ts +16 -0
  87. package/dist/viewer/server.d.ts.map +1 -1
  88. package/dist/viewer/server.js +346 -3
  89. package/dist/viewer/server.js.map +1 -1
  90. package/index.ts +572 -89
  91. package/openclaw.plugin.json +20 -45
  92. package/package.json +3 -4
  93. package/skill/memos-memory-guide/SKILL.md +86 -0
  94. package/src/capture/index.ts +85 -45
  95. package/src/ingest/dedup.ts +29 -0
  96. package/src/ingest/providers/anthropic.ts +258 -1
  97. package/src/ingest/providers/bedrock.ts +256 -6
  98. package/src/ingest/providers/gemini.ts +252 -1
  99. package/src/ingest/providers/index.ts +156 -8
  100. package/src/ingest/providers/openai.ts +304 -1
  101. package/src/ingest/task-processor.ts +396 -0
  102. package/src/ingest/worker.ts +145 -34
  103. package/src/recall/engine.ts +23 -12
  104. package/src/recall/mmr.ts +3 -1
  105. package/src/skill/bundled-memory-guide.ts +91 -0
  106. package/src/skill/evaluator.ts +220 -0
  107. package/src/skill/evolver.ts +169 -0
  108. package/src/skill/generator.ts +506 -0
  109. package/src/skill/installer.ts +59 -0
  110. package/src/skill/upgrader.ts +257 -0
  111. package/src/skill/validator.ts +227 -0
  112. package/src/storage/sqlite.ts +802 -7
  113. package/src/types.ts +96 -0
  114. package/src/viewer/html.ts +2391 -159
  115. package/src/viewer/server.ts +346 -3
  116. package/SKILL.md +0 -43
  117. package/www/index.html +0 -632
@@ -10,39 +10,67 @@ export const viewerHTML = `<!DOCTYPE html>
10
10
  <style>
11
11
  *{margin:0;padding:0;box-sizing:border-box}
12
12
  :root{
13
- --bg:#050510;--bg-card:rgba(255,255,255,.04);--bg-card-hover:rgba(255,255,255,.07);
14
- --border:rgba(255,255,255,.08);--border-glow:rgba(0,187,238,.25);
15
- --text:#f0f4f8;--text-sec:#8b95a5;--text-muted:#5a6373;
16
- --pri:#00bbee;--pri-glow:rgba(0,187,238,.15);--pri-dark:#0088aa;
17
- --pri-grad:linear-gradient(135deg,#00bbee,#00a0cc);
18
- --accent:#e63946;--accent-glow:rgba(230,57,70,.15);
19
- --green:#10b981;--green-bg:rgba(16,185,129,.12);
20
- --amber:#f59e0b;--amber-bg:rgba(245,158,11,.12);
21
- --violet:#8b5cf6;--rose:#f43f5e;--rose-bg:rgba(244,63,94,.12);
22
- --shadow-sm:0 1px 2px rgba(0,0,0,.2);--shadow:0 4px 12px rgba(0,0,0,.25);
23
- --shadow-lg:0 20px 40px rgba(0,0,0,.35);
13
+ --bg:#0b0d11;--bg-card:#12141a;--bg-card-hover:#1a1d25;
14
+ --border:rgba(255,255,255,.08);--border-glow:rgba(255,255,255,.14);
15
+ --text:#e8eaed;--text-sec:#8b8fa4;--text-muted:#555a6e;
16
+ --pri:#818cf8;--pri-glow:rgba(129,140,248,.1);--pri-dark:#6366f1;
17
+ --pri-grad:linear-gradient(135deg,#818cf8,#6366f1);
18
+ --accent:#ef4444;--accent-glow:rgba(239,68,68,.1);
19
+ --green:#34d399;--green-bg:rgba(52,211,153,.08);
20
+ --amber:#fbbf24;--amber-bg:rgba(251,191,36,.08);
21
+ --violet:#818cf8;--rose:#ef4444;--rose-bg:rgba(239,68,68,.08);
22
+ --shadow-sm:0 1px 2px rgba(0,0,0,.3);--shadow:0 4px 12px rgba(0,0,0,.35);
23
+ --shadow-lg:0 20px 40px rgba(0,0,0,.45);
24
24
  --radius:12px;--radius-lg:14px;--radius-xl:18px;
25
25
  }
26
26
  [data-theme="light"]{
27
- --bg:#f1f5f9;--bg-card:#fff;--bg-card-hover:#f8fafc;
28
- --border:rgba(0,0,0,.08);--border-glow:rgba(0,187,238,.4);
29
- --text:#0f172a;--text-sec:#475569;--text-muted:#64748b;
30
- --pri:#0891b2;--pri-glow:rgba(8,145,178,.12);--pri-dark:#0e7490;
31
- --pri-grad:linear-gradient(135deg,#0891b2,#0e7490);
32
- --accent:#dc2626;--accent-glow:rgba(220,38,38,.1);
33
- --green:#059669;--green-bg:rgba(5,150,105,.1);
34
- --amber:#d97706;--amber-bg:rgba(217,119,6,.1);
35
- --violet:#7c3aed;--rose:#e11d48;--rose-bg:rgba(225,29,72,.1);
36
- --shadow-sm:0 1px 2px rgba(0,0,0,.06);--shadow:0 4px 12px rgba(0,0,0,.08);
37
- --shadow-lg:0 20px 40px rgba(0,0,0,.12);
38
- }
39
- [data-theme="light"] .auth-screen{background:linear-gradient(135deg,#e0f2fe 0%,#f0f9ff 50%,#e0e7ff 100%)}
40
- [data-theme="light"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.12)}
41
- [data-theme="light"] .topbar{background:rgba(255,255,255,.9);border-bottom-color:var(--border)}
42
- [data-theme="light"] .session-item .count,[data-theme="light"] .kind-tag,[data-theme="light"] .session-tag{background:rgba(0,0,0,.06)}
43
- [data-theme="light"] .card-content pre{background:rgba(0,0,0,.05);border-color:var(--border)}
44
- [data-theme="light"] .vscore-badge{background:linear-gradient(135deg,var(--pri),var(--violet))}
45
- [data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2)}
27
+ --bg:#f8f9fb;--bg-card:#fff;--bg-card-hover:#f3f4f6;
28
+ --border:#e2e4e9;--border-glow:#cbd0d8;
29
+ --text:#111827;--text-sec:#4b5563;--text-muted:#9ca3af;
30
+ --pri:#4f46e5;--pri-glow:rgba(79,70,229,.06);--pri-dark:#4338ca;
31
+ --pri-grad:linear-gradient(135deg,#4f46e5,#4338ca);
32
+ --accent:#dc2626;--accent-glow:rgba(220,38,38,.06);
33
+ --green:#059669;--green-bg:rgba(5,150,105,.06);
34
+ --amber:#d97706;--amber-bg:rgba(217,119,6,.06);
35
+ --violet:#4f46e5;--rose:#dc2626;--rose-bg:rgba(220,38,38,.06);
36
+ --shadow-sm:0 1px 2px rgba(0,0,0,.04);--shadow:0 4px 12px rgba(0,0,0,.06);
37
+ --shadow-lg:0 20px 40px rgba(0,0,0,.1);
38
+ }
39
+ [data-theme="light"] .auth-screen{background:linear-gradient(135deg,#f0f4ff 0%,#f8f9fb 50%,#eef2ff 100%)}
40
+ [data-theme="light"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.08)}
41
+ [data-theme="light"] .topbar{background:rgba(255,255,255,.92);border-bottom-color:var(--border);backdrop-filter:blur(8px)}
42
+ [data-theme="light"] .session-item .count,[data-theme="light"] .kind-tag,[data-theme="light"] .session-tag{background:rgba(0,0,0,.05)}
43
+ [data-theme="light"] .card-content pre{background:#f3f4f6;border-color:var(--border)}
44
+ [data-theme="light"] .vscore-badge{background:rgba(79,70,229,.06);color:#4f46e5}
45
+ [data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15)}
46
+ [data-theme="light"] .analytics-card{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.06);border:1px solid var(--border)}
47
+ [data-theme="light"] .analytics-card::before{background:none}
48
+ [data-theme="light"] .analytics-card::after{display:none}
49
+ [data-theme="light"] .analytics-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.08);transform:translateY(-2px)}
50
+ [data-theme="light"] .analytics-card.green{background:#fff;border-color:var(--border)}
51
+ [data-theme="light"] .analytics-card.green::before{background:none}
52
+ [data-theme="light"] .analytics-card.amber{background:#fff;border-color:var(--border)}
53
+ [data-theme="light"] .analytics-card.amber::before{background:none}
54
+ [data-theme="light"] .analytics-card .ac-value{-webkit-text-fill-color:unset;background:none;color:#111827}
55
+ [data-theme="light"] .analytics-card.green .ac-value{color:#059669}
56
+ [data-theme="light"] .analytics-card.amber .ac-value{color:#d97706}
57
+ [data-theme="light"] .analytics-section{background:#fff;border-color:var(--border);box-shadow:0 1px 3px rgba(0,0,0,.04)}
58
+ [data-theme="light"] .analytics-section::before{background:none}
59
+ [data-theme="light"] .chart-bar{box-shadow:none}
60
+ [data-theme="light"] .chart-bar:hover{box-shadow:0 2px 8px rgba(79,70,229,.15)}
61
+ [data-theme="light"] .tool-chart-tooltip{background:rgba(17,24,39,.92);color:#e8eaed;border-color:rgba(99,102,241,.3);box-shadow:0 8px 24px rgba(0,0,0,.2)}
62
+ [data-theme="light"] .tool-chart-tooltip .tt-time{color:#a5b4fc}
63
+ [data-theme="light"] .tool-chart-tooltip .tt-val{color:#e8eaed}
64
+ [data-theme="light"] .tool-agg-table td{background:transparent}
65
+ [data-theme="light"] .tool-agg-table tr:hover td{background:rgba(79,70,229,.03)}
66
+ [data-theme="light"] .tool-agg-table th{color:#9ca3af}
67
+ [data-theme="light"] .breakdown-item{background:#f9fafb;border-color:var(--border)}
68
+ [data-theme="light"] .breakdown-item:hover{background:#f3f4f6;border-color:#cbd5e1}
69
+ [data-theme="light"] .breakdown-bar-wrap{background:#e5e7eb}
70
+ [data-theme="light"] .breakdown-bar{background:linear-gradient(90deg,#4f46e5,#6366f1);box-shadow:none}
71
+ [data-theme="light"] .range-btn{background:transparent;border-color:var(--border);color:var(--text-sec)}
72
+ [data-theme="light"] .range-btn.active{background:rgba(79,70,229,.06);color:#4f46e5;border-color:rgba(79,70,229,.2)}
73
+ [data-theme="light"] .range-btn:hover{border-color:#4f46e5;color:#4f46e5}
46
74
  body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .2s,color .2s}
47
75
  button{cursor:pointer;font-family:inherit;font-size:inherit}
48
76
  input,textarea,select{font-family:inherit;font-size:inherit}
@@ -55,31 +83,33 @@ input,textarea,select{font-family:inherit;font-size:inherit}
55
83
  .auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}
56
84
  .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%)}
57
85
  .auth-card input::placeholder{color:hsl(0 0% 45.1%)}
58
- .auth-card input:focus{border-color:rgb(168,85,247);box-shadow:0 0 0 3px rgba(168,85,247,.2)}
59
- .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}
60
- .auth-card .btn-auth:hover{background:hsl(0 0% 14%);transform:translateY(-1px);box-shadow:0 8px 25px rgba(0,0,0,.2)}
86
+ .auth-card input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}
87
+ .auth-card .btn-auth{width:100%;padding:11px;border:1px solid var(--pri);border-radius:8px;background:rgba(99,102,241,.06);color:var(--pri);font-weight:600;font-size:14px;transition:all .15s}
88
+ .auth-card .btn-auth:hover{background:rgba(99,102,241,.12);border-color:var(--pri-dark)}
61
89
  .auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}
62
90
  .auth-card .btn-text{color:hsl(0 0% 45.1%)}
63
- .auth-card .btn-text:hover{color:rgb(168,85,247)}
91
+ .auth-card .btn-text:hover{color:var(--pri)}
64
92
 
65
93
  .reset-guide{text-align:left;margin-bottom:20px}
66
94
  .reset-step{display:flex;gap:14px;margin-bottom:16px}
67
- .step-num{width:28px;height:28px;border-radius:50%;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}
95
+ .step-num{width:28px;height:28px;border-radius:50%;background:var(--pri);color:#fff;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}
68
96
  .step-body{flex:1;min-width:0}
69
97
  .step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}
70
98
  .step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}
71
99
  .cmd-box{margin-top:8px;background:hsl(0 0% 96.1%);border:1px solid hsl(0 0% 89.8%);border-radius:8px;padding:12px 14px;font-size:12px;font-family:ui-monospace,monospace;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:space-between;gap:8px;word-break:break-all;color:hsl(0 0% 3.9%)}
72
- .cmd-box:hover{border-color:rgb(168,85,247);background:rgba(168,85,247,.08)}
100
+ .cmd-box:hover{border-color:hsl(0 0% 70%);background:hsl(0 0% 96.1%)}
73
101
  .cmd-box code{flex:1}
74
102
  .copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}
75
103
  .cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}
76
104
 
77
105
  /* ─── App Layout (dark dashboard, same as www) ─── */
78
106
  .app{display:none;flex-direction:column;min-height:100vh}
79
- .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)}
80
- .topbar .brand{display:flex;align-items:center;gap:12px;font-weight:700;font-size:17px;color:var(--text);letter-spacing:-.02em}
81
- .topbar .brand .icon{width:38px;height:38px;display:flex;align-items:center;justify-content:center;font-size:26px;background:none;border-radius:0}
82
- .topbar .actions{display:flex;align-items:center;gap:10px}
107
+ .topbar{background:rgba(11,13,17,.88);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(12px)}
108
+ .topbar .brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}
109
+ .topbar .brand .icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:22px;background:none;border-radius:0}
110
+ .topbar .brand .sub{font-weight:400;color:var(--text-muted);font-size:11px}
111
+ .topbar-center{flex:1;display:flex;justify-content:center}
112
+ .topbar .actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
83
113
 
84
114
  .main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}
85
115
 
@@ -107,17 +137,17 @@ input,textarea,select{font-family:inherit;font-size:inherit}
107
137
 
108
138
  /* ─── Feed ─── */
109
139
  .feed{flex:1;min-width:0}
110
- .search-bar{display:flex;gap:12px;margin-bottom:16px;position:relative}
111
- .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}
140
+ .search-bar{display:flex;gap:10px;margin-bottom:16px;position:relative;align-items:center}
141
+ .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}
112
142
  .search-bar input::placeholder{color:var(--text-muted)}
113
143
  .search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}
114
- .search-bar .search-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:15px;pointer-events:none}
144
+ .search-bar .search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:14px;pointer-events:none}
115
145
  .search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}
116
146
 
117
147
  .filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
118
- .filter-chip{padding:6px 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;transition:all .15s}
148
+ .filter-chip{padding:5px 14px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-sec);font-size:12px;font-weight:500;transition:all .15s}
119
149
  .filter-chip:hover{border-color:var(--pri);color:var(--pri)}
120
- .filter-chip.active{background:var(--pri);color:#000;border-color:var(--pri)}
150
+ .filter-chip.active{background:rgba(99,102,241,.08);color:var(--pri);border-color:rgba(99,102,241,.25)}
121
151
 
122
152
  .memory-list{display:flex;flex-direction:column;gap:16px}
123
153
  .memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}
@@ -125,7 +155,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
125
155
  .memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}
126
156
  .memory-card .meta{display:flex;align-items:center;gap:8px}
127
157
  .role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}
128
- .role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(0,187,238,.2)}
158
+ .role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(99,102,241,.12)}
129
159
  .role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}
130
160
  .role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}
131
161
  .kind-tag{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-sec);background:rgba(0,0,0,.2);font-weight:500}
@@ -136,19 +166,55 @@ input,textarea,select{font-family:inherit;font-size:inherit}
136
166
  .card-content.show{max-height:600px;overflow-y:auto}
137
167
  .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)}
138
168
  .card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}
139
- .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}
169
+ .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}
170
+ .merge-badge{display:inline-flex;align-items:center;gap:4px;background:rgba(16,185,129,.12);color:#10b981;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
171
+ .merge-history{margin-top:12px;padding:12px 14px;background:rgba(0,0,0,.15);border-radius:10px;border:1px solid var(--border);font-size:12px;line-height:1.7;color:var(--text-sec);max-height:200px;overflow-y:auto}
172
+ .merge-history-item{padding:6px 0;border-bottom:1px dashed rgba(255,255,255,.06)}
173
+ .merge-history-item:last-child{border-bottom:none}
174
+ .merge-action{font-weight:600;font-size:11px;padding:2px 6px;border-radius:4px}
175
+ .merge-action.UPDATE{background:rgba(59,130,246,.15);color:#60a5fa}
176
+ .merge-action.DUPLICATE{background:rgba(245,158,11,.15);color:#f59e0b}
177
+ .card-updated{font-size:11px;color:var(--text-muted);margin-left:6px}
178
+ .dedup-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
179
+ .dedup-badge.duplicate{background:rgba(245,158,11,.12);color:#f59e0b}
180
+ .dedup-badge.merged{background:rgba(59,130,246,.12);color:#60a5fa}
181
+ .memory-card.dedup-inactive{opacity:.55;border-style:dashed}
182
+ .memory-card.dedup-inactive:hover{opacity:.85}
183
+ .dedup-target-link{font-size:11px;color:var(--pri);cursor:pointer;text-decoration:underline;margin-left:4px}
184
+ .memory-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9999;display:none;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
185
+ .memory-modal-overlay.show{display:flex}
186
+ .memory-modal{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;width:min(600px,90vw);max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.4);animation:modalIn .2s ease-out}
187
+ @keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}
188
+ .memory-modal-title{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);font-size:14px;font-weight:700}
189
+ .memory-modal-body{padding:20px;overflow-y:auto;flex:1}
190
+ .modal-memory-card{display:flex;flex-direction:column;gap:14px}
191
+ .modal-header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
192
+ .modal-field{display:flex;flex-direction:column;gap:4px}
193
+ .modal-field-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec)}
194
+ .modal-field-val{font-size:13px;color:var(--text);line-height:1.5}
195
+ .modal-field-content{font-family:'SF Mono',Consolas,monospace;font-size:12px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.15);border-radius:8px;padding:12px;max-height:240px;overflow-y:auto;margin:0}
196
+ [data-theme="light"] .modal-field-content{background:rgba(0,0,0,.04)}
197
+ .modal-meta-row{display:flex;flex-wrap:wrap;gap:12px;font-size:11px;color:var(--text-sec);padding:8px 0;border-top:1px dashed var(--border)}
198
+ [data-theme="light"] .merge-history{background:rgba(0,0,0,.04)}
199
+ [data-theme="light"] .merge-history-item{border-bottom-color:rgba(0,0,0,.06)}
140
200
 
141
201
  /* ─── Buttons ─── */
142
- .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}
202
+ .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}
143
203
  .btn:hover{border-color:var(--pri);color:var(--pri)}
144
- .btn-primary{background:var(--pri);color:#000;border:none}
145
- .btn-primary:hover{background:#4dd9ff;transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,187,238,.25)}
146
- .btn-danger{color:var(--accent);border-color:var(--accent)}
147
- .btn-danger:hover{background:var(--accent);color:#fff;border-color:var(--accent)}
148
- .btn-sm{padding:6px 12px;font-size:12px}
149
- .btn-icon{padding:6px 8px;font-size:14px}
150
- .btn-text{border:none;background:none;color:var(--text-sec);font-size:13px;padding:4px 8px}
204
+ .btn-primary{background:rgba(255,255,255,.08);color:var(--text);border:1px solid var(--border);font-weight:600}
205
+ .btn-primary:hover{background:rgba(255,255,255,.14);transform:translateY(-1px);border-color:var(--pri);color:var(--pri)}
206
+ .btn-ghost{border-color:transparent;background:transparent;color:var(--text-sec)}
207
+ .btn-ghost:hover{background:rgba(255,255,255,.06);color:var(--text)}
208
+ .btn-danger{color:var(--accent);border-color:rgba(230,57,70,.25)}
209
+ .btn-danger:hover{background:rgba(230,57,70,.1);color:var(--accent)}
210
+ .btn-sm{padding:5px 12px;font-size:12px}
211
+ .btn-icon{padding:5px 7px;font-size:15px;border-radius:8px}
212
+ .btn-text{border:none;background:none;color:var(--text-muted);font-size:12px;padding:4px 8px}
151
213
  .btn-text:hover{color:var(--pri)}
214
+ [data-theme="light"] .btn-primary{background:rgba(0,0,0,.05);color:var(--text);border-color:rgba(0,0,0,.12)}
215
+ [data-theme="light"] .btn-primary:hover{background:rgba(0,0,0,.08);border-color:var(--pri);color:var(--pri)}
216
+ [data-theme="light"] .btn-ghost{color:var(--text-sec)}
217
+ [data-theme="light"] .btn-ghost:hover{background:rgba(0,0,0,.04);color:var(--text)}
152
218
 
153
219
  /* ─── Modal ─── */
154
220
  .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)}
@@ -168,7 +234,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
168
234
  .toast{padding:14px 20px;border-radius:10px;font-size:13px;font-weight:500;box-shadow:var(--shadow-lg);animation:slideIn .3s ease;display:flex;align-items:center;gap:10px;max-width:360px;border:1px solid}
169
235
  .toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}
170
236
  .toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}
171
- .toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(0,187,238,.3)}
237
+ .toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(99,102,241,.15)}
172
238
  @keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}
173
239
 
174
240
  .empty{text-align:center;padding:64px 20px;color:var(--text-sec)}
@@ -198,89 +264,406 @@ input,textarea,select{font-family:inherit;font-size:inherit}
198
264
  .pagination .pg-btn.disabled{opacity:.4;pointer-events:none}
199
265
  .pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}
200
266
 
201
- .theme-toggle{position:relative;width:40px;height:40px;padding:0;display:flex;align-items:center;justify-content:center;font-size:18px}
267
+ /* ─── Tasks 视图 ─── */
268
+ .tasks-view{display:none;flex:1;min-width:0;flex-direction:column;gap:16px}
269
+ .tasks-view.show{display:flex}
270
+ .tasks-header{display:flex;flex-direction:column;gap:14px}
271
+ .tasks-stats{display:flex;gap:16px}
272
+ .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}
273
+ .tasks-stat:hover{border-color:var(--border-glow)}
274
+ .tasks-stat-value{font-size:22px;font-weight:700;color:var(--text)}
275
+ .tasks-stat-label{font-size:12px;color:var(--text-sec);font-weight:500}
276
+ .tasks-filters{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
277
+ .tasks-list{display:flex;flex-direction:column;gap:10px}
278
+ .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}
279
+ .task-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover);transform:translateY(-1px);box-shadow:var(--shadow)}
280
+ .task-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px}
281
+ .task-card.status-active::before{background:var(--green)}
282
+ .task-card.status-completed::before{background:var(--pri)}
283
+ .task-card.status-skipped::before{background:var(--text-muted)}
284
+ .task-card.status-skipped{opacity:.6}
285
+ .task-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:8px}
286
+ .task-card-title{font-size:14px;font-weight:600;color:var(--text);line-height:1.4;flex:1;word-break:break-word}
287
+ .task-card-title:empty::after{content:'Untitled Task';color:var(--text-muted);font-style:italic}
288
+ .task-status-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:3px 10px;border-radius:20px;flex-shrink:0}
289
+ .task-status-badge.active{color:var(--green);background:var(--green-bg)}
290
+ .task-status-badge.completed{color:var(--pri);background:var(--pri-glow)}
291
+ .task-status-badge.skipped{color:var(--text-muted);background:rgba(128,128,128,.15)}
292
+ .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}
293
+ .task-card-summary:empty{display:none}
294
+ .task-card-summary.skipped-reason{background:rgba(128,128,128,.08);border-radius:6px;padding:6px 10px;border-left:3px solid var(--text-muted)}
295
+ .task-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted)}
296
+ .task-card-bottom .tag{display:flex;align-items:center;gap:4px}
297
+ .task-card-bottom .tag .icon{font-size:12px}
298
+
299
+ /* ─── Task Detail Overlay ─── */
300
+ .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)}
301
+ .task-detail-overlay.show{display:flex}
302
+ .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}
303
+ .task-detail-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}
304
+ .task-detail-header h2{font-size:18px;font-weight:700;color:var(--text);line-height:1.4;flex:1}
305
+ .task-detail-meta{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;font-size:12px;color:var(--text-sec)}
306
+ .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}
307
+ .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}
308
+ .task-detail-summary:empty::after{content:'Summary not yet generated (task still active)';color:var(--text-muted);font-style:italic}
309
+ .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)}
310
+ .task-detail-summary .summary-section-title:first-child{margin-top:0}
311
+ .task-detail-summary ul{margin:4px 0 8px 0;padding-left:20px}
312
+ .task-detail-summary li{margin:3px 0;color:var(--text-sec);line-height:1.6}
313
+ .task-detail-chunks-title{font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px}
314
+ .task-detail-chunks{display:flex;flex-direction:column;gap:14px;padding:8px 0}
315
+ .task-chunk-item{display:flex;flex-direction:column;max-width:82%;font-size:13px;line-height:1.6}
316
+ .task-chunk-item.role-user{align-self:flex-end;align-items:flex-end}
317
+ .task-chunk-item.role-assistant,.task-chunk-item.role-tool{align-self:flex-start;align-items:flex-start}
318
+ .task-chunk-role{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px;padding:0 4px}
319
+ .task-chunk-role.user{color:var(--pri)}
320
+ .task-chunk-role.assistant{color:var(--green)}
321
+ .task-chunk-role.tool{color:var(--amber)}
322
+ .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}
323
+ .task-chunk-bubble.expanded{max-height:none}
324
+ .role-user .task-chunk-bubble{background:var(--pri);color:#000;border-bottom-right-radius:4px}
325
+ .role-assistant .task-chunk-bubble{background:var(--bg-card);border:1px solid var(--border);color:var(--text-sec);border-bottom-left-radius:4px}
326
+ .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}
327
+ .task-chunk-bubble:hover{filter:brightness(1.05)}
328
+ .task-chunk-time{font-size:10px;color:var(--text-muted);margin-top:3px;padding:0 4px}
329
+ [data-theme="light"] .role-user .task-chunk-bubble{background:var(--pri);color:#fff}
330
+ [data-theme="light"] .role-assistant .task-chunk-bubble{background:#f0f0f0;border:none;color:#333}
331
+ [data-theme="light"] .task-detail-panel{background:#fff}
332
+ [data-theme="light"] .task-card{background:#fff}
333
+ [data-theme="light"] .tasks-stat{background:#fff}
334
+
335
+ /* ─── Skills ─── */
336
+ .skills-view{display:none;flex:1;min-width:0;flex-direction:column;gap:16px}
337
+ .skills-view.show{display:flex}
338
+ .skill-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}
339
+ .skill-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover);transform:translateY(-1px);box-shadow:var(--shadow)}
340
+ .skill-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px;background:var(--violet)}
341
+ .skill-card.installed::before{background:var(--green)}
342
+ .skill-card.archived{opacity:.5}
343
+ .skill-card.archived::before{background:var(--text-muted)}
344
+ .skill-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:6px}
345
+ .skill-card-name{font-size:15px;font-weight:700;color:var(--text);flex:1}
346
+ .skill-card-badges{display:flex;gap:6px;align-items:center}
347
+ .skill-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:3px 10px;border-radius:20px}
348
+ .skill-badge.version{color:var(--violet);background:rgba(139,92,246,.15)}
349
+ .skill-badge.installed{color:var(--green);background:var(--green-bg)}
350
+ .skill-badge.status-active{color:var(--pri);background:var(--pri-glow)}
351
+ .skill-badge.status-archived{color:var(--text-muted);background:rgba(128,128,128,.15)}
352
+ .skill-badge.status-draft{color:var(--amber);background:var(--amber-bg)}
353
+ .skill-badge.quality{font-size:10px;font-weight:700;padding:3px 10px;border-radius:20px}
354
+ .skill-badge.quality.high{color:var(--green);background:var(--green-bg)}
355
+ .skill-badge.quality.mid{color:var(--amber);background:var(--amber-bg)}
356
+ .skill-badge.quality.low{color:var(--rose);background:var(--rose-bg)}
357
+ .skill-card.draft{opacity:.75}
358
+ .skill-card.draft::before{background:var(--amber)}
359
+ .skill-card-desc{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}
360
+ .skill-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted);flex-wrap:wrap}
361
+ .skill-card-bottom .tag{display:flex;align-items:center;gap:4px}
362
+ .skill-card-tags{display:flex;gap:4px;flex-wrap:wrap}
363
+ .skill-tag{font-size:10px;padding:2px 8px;border-radius:10px;background:rgba(139,92,246,.1);color:var(--violet);font-weight:500}
364
+ .skill-detail-desc{font-size:13px;color:var(--text-sec);line-height:1.6;margin-bottom:16px;padding:12px 16px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius)}
365
+ .skill-version-item{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px}
366
+ .skill-version-header{display:flex;align-items:center;gap:10px;margin-bottom:6px}
367
+ .skill-version-badge{font-size:11px;font-weight:700;color:var(--violet);background:rgba(139,92,246,.12);padding:2px 8px;border-radius:8px}
368
+ .skill-version-type{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em}
369
+ .skill-version-changelog{font-size:12px;color:var(--text);line-height:1.5;font-weight:600}
370
+ .skill-version-summary{font-size:12px;color:var(--text-sec);line-height:1.6;margin-top:6px;padding:8px 12px;background:rgba(139,92,246,.04);border-left:2px solid rgba(139,92,246,.2);border-radius:0 6px 6px 0}
371
+ .skill-version-time{font-size:10px;color:var(--text-muted);margin-top:4px}
372
+ .skill-related-task{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:all .2s}
373
+ .skill-related-task:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}
374
+ .skill-related-task .relation{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em;min-width:80px}
375
+ .skill-related-task .task-title{font-size:13px;color:var(--text);flex:1}
376
+ .skill-files-list{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}
377
+ .skill-file-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;font-size:12px}
378
+ .skill-file-icon{font-size:14px;width:20px;text-align:center}
379
+ .skill-file-name{flex:1;color:var(--text);font-family:SF Mono,Monaco,Consolas,monospace}
380
+ .skill-file-type{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em}
381
+ .skill-file-size{font-size:10px;color:var(--text-muted)}
382
+ .skill-download-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:8px;background:var(--pri-grad);color:#fff;font-size:12px;font-weight:600;border:none;cursor:pointer;transition:all .2s}
383
+ .skill-download-btn:hover{opacity:.85;transform:translateY(-1px)}
384
+ .task-skill-section{margin-bottom:16px;padding:14px 16px;border-radius:var(--radius);border:1px solid var(--border)}
385
+ .task-skill-section.status-generated{border-color:var(--green);background:var(--green-bg)}
386
+ .task-skill-section.status-generating{border-color:var(--amber);background:var(--amber-bg)}
387
+ .task-skill-section.status-not_generated,.task-skill-section.status-skipped{border-color:var(--border);background:var(--bg-card)}
388
+ .task-skill-section .skill-status-header{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:13px;font-weight:600;color:var(--text)}
389
+ .task-skill-section .skill-status-reason{font-size:12px;color:var(--text-sec);line-height:1.5}
390
+ .task-skill-section .skill-link-card{margin-top:10px;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:all .2s}
391
+ .task-skill-section .skill-link-card:hover{border-color:var(--pri);background:var(--bg-card-hover)}
392
+ .task-skill-section .skill-link-name{font-size:13px;font-weight:600;color:var(--pri)}
393
+ .task-skill-section .skill-link-meta{font-size:11px;color:var(--text-sec);margin-top:4px}
394
+ .task-id-full{font-family:monospace;font-size:11px;color:var(--text-muted);word-break:break-all;user-select:all;cursor:text;padding:2px 6px;background:var(--bg-card);border-radius:4px;border:1px solid var(--border)}
395
+ [data-theme="light"] .skill-card{background:#fff}
396
+ [data-theme="light"] .skill-detail-desc{background:#f8fafc}
397
+ [data-theme="light"] .skill-version-item{background:#f8fafc}
398
+
399
+ /* ─── Analytics / 统计 ─── */
400
+ .nav-tabs{display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.06);border-radius:10px;padding:3px}
401
+ .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}
402
+ .nav-tabs .tab:hover{color:var(--text)}
403
+ .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)}
404
+ [data-theme="light"] .nav-tabs{background:rgba(0,0,0,.05)}
405
+ [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)}
406
+ .analytics-view,.settings-view,.logs-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
407
+ .analytics-view.show,.settings-view.show,.logs-view.show{display:flex}
408
+
409
+ /* ─── Logs ─── */
410
+ .logs-toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 0}
411
+ .logs-toolbar-left{display:flex;align-items:center;gap:8px}
412
+ .logs-toolbar-right{display:flex;align-items:center;gap:8px}
413
+ .logs-list{display:flex;flex-direction:column;gap:8px;overflow-y:auto;flex:1;min-height:0}
414
+ .log-entry{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;transition:border-color .2s}
415
+ .log-entry:hover{border-color:var(--border-glow)}
416
+ .log-header{display:flex;align-items:center;gap:10px;padding:12px 16px;cursor:pointer;user-select:none;transition:background .15s}
417
+ .log-header:hover{background:rgba(255,255,255,.03)}
418
+ [data-theme="light"] .log-header:hover{background:rgba(0,0,0,.02)}
419
+ .log-tool-badge{font-family:'SF Mono',Consolas,monospace;font-size:11px;font-weight:700;padding:3px 8px;border-radius:4px;white-space:nowrap;letter-spacing:.3px}
420
+ .log-tool-badge.memory_search{background:rgba(59,130,246,.15);color:#60a5fa}
421
+ .log-tool-badge.memory_add{background:rgba(168,85,247,.15);color:#c084fc}
422
+ .log-tool-badge.auto_recall{background:rgba(168,85,247,.15);color:#c084fc}
423
+ .log-tool-badge.memory_timeline{background:rgba(34,197,94,.15);color:#4ade80}
424
+ .log-tool-badge.memory_get{background:rgba(251,146,60,.15);color:#fb923c}
425
+ .log-tool-badge.task_summary{background:rgba(245,158,11,.15);color:#fbbf24}
426
+ .log-tool-badge.skill_get{background:rgba(236,72,153,.15);color:#f472b6}
427
+ .log-tool-badge.skill_install{background:rgba(14,165,233,.15);color:#38bdf8}
428
+ .log-tool-badge.memory_viewer{background:rgba(100,116,139,.15);color:#94a3b8}
429
+ .log-dur{font-family:'SF Mono',Consolas,monospace;font-size:10px;color:var(--text-sec);opacity:.7}
430
+ .log-time{margin-left:auto;font-size:11px;color:var(--text-sec);font-family:'SF Mono',Consolas,monospace;white-space:nowrap}
431
+ .log-status{width:7px;height:7px;border-radius:50%;flex-shrink:0}
432
+ .log-status.ok{background:#4ade80;box-shadow:0 0 4px rgba(74,222,128,.5)}
433
+ .log-status.fail{background:#f87171;box-shadow:0 0 4px rgba(248,113,113,.5)}
434
+ .log-summary{padding:8px 16px 10px;font-size:12px;color:var(--text-sec);line-height:1.5}
435
+ .log-summary-kv{display:inline-flex;align-items:center;gap:4px;margin-right:12px;font-size:11px}
436
+ .log-summary-kv .kv-label{color:var(--text-sec);opacity:.7}
437
+ .log-summary-kv .kv-val{color:var(--text);font-family:'SF Mono',Consolas,monospace;font-size:11px}
438
+ .log-summary-query{margin-top:4px;padding:6px 10px;background:rgba(59,130,246,.08);border-radius:6px;font-size:12px;color:var(--text);border-left:3px solid rgba(59,130,246,.4);line-height:1.4}
439
+ .log-summary-stats{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
440
+ .log-stat-chip{display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600;font-family:'SF Mono',Consolas,monospace}
441
+ .log-stat-chip.stored{background:rgba(74,222,128,.12);color:#4ade80}
442
+ .log-stat-chip.skipped{background:rgba(100,116,139,.12);color:#94a3b8}
443
+ .log-stat-chip.dedup{background:rgba(251,146,60,.12);color:#fb923c}
444
+ .log-stat-chip.merged{background:rgba(168,85,247,.12);color:#c084fc}
445
+ .log-stat-chip.errors{background:rgba(248,113,113,.12);color:#f87171}
446
+ .log-msg-list{margin-top:8px;display:flex;flex-direction:column;gap:4px}
447
+ .log-msg-item{display:flex;gap:8px;align-items:flex-start;font-size:11.5px;line-height:1.5;padding:4px 10px;border-radius:6px;background:rgba(255,255,255,.02)}
448
+ [data-theme="light"] .log-msg-item{background:rgba(0,0,0,.02)}
449
+ .log-msg-role{flex-shrink:0;font-size:10px;font-weight:600;padding:1px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.3px}
450
+ .log-msg-role.user{background:rgba(59,130,246,.12);color:#60a5fa}
451
+ .log-msg-role.assistant{background:rgba(168,85,247,.12);color:#c084fc}
452
+ .log-msg-role.system{background:rgba(100,116,139,.12);color:#94a3b8}
453
+ .log-msg-action{flex-shrink:0;font-size:10px;font-weight:600;padding:1px 6px;border-radius:4px}
454
+ .log-msg-action.stored{color:#4ade80}
455
+ .log-msg-action.exact-dup{color:#94a3b8}
456
+ .log-msg-action.dedup{color:#fb923c}
457
+ .log-msg-action.merged{color:#c084fc}
458
+ .log-msg-action.error{color:#f87171}
459
+ .log-msg-text{color:var(--text);opacity:.85;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
460
+ .log-detail{display:none;border-top:1px solid var(--border);padding:0}
461
+ .log-detail.open{display:block}
462
+ .log-expand-btn{font-size:10px;color:var(--text-sec);opacity:.5;margin-left:auto;transition:transform .2s,opacity .15s;display:inline-block}
463
+ .log-entry.expanded .log-expand-btn{transform:rotate(180deg);opacity:.8}
464
+ .logs-pagination{display:flex;align-items:center;justify-content:center;gap:4px;padding:12px 0;flex-wrap:wrap}
465
+ .logs-pagination .btn{min-width:32px;padding:4px 8px;font-size:12px}
466
+ .logs-pagination .btn-primary{background:var(--primary);color:#fff;border-color:var(--primary)}
467
+ .logs-pagination .page-ellipsis{color:var(--text-sec);font-size:12px;padding:0 4px}
468
+ .logs-pagination .page-total{font-size:11px;color:var(--text-sec);margin-left:8px}
469
+ .log-io-section{padding:10px 14px}
470
+ .log-io-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec);margin-bottom:6px}
471
+ .log-io-content{font-family:'SF Mono',Consolas,monospace;font-size:11px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.2);border-radius:6px;padding:10px 12px;max-height:300px;overflow-y:auto}
472
+ .log-io-section+.log-io-section{border-top:1px dashed var(--border)}
473
+ [data-theme="light"] .log-io-content{background:rgba(0,0,0,.04)}
474
+ [data-theme="light"] .log-summary-query{background:rgba(59,130,246,.06)}
475
+ .settings-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px 28px}
476
+ .settings-section h3{font-size:13px;font-weight:700;color:var(--text);margin-bottom:16px;display:flex;align-items:center;gap:8px}
477
+ .settings-section h3 .icon{font-size:16px;opacity:.8}
478
+ .settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
479
+ @media(max-width:800px){.settings-grid{grid-template-columns:1fr}}
480
+ .settings-field{display:flex;flex-direction:column;gap:4px}
481
+ .settings-field label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}
482
+ .settings-field input,.settings-field select{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 12px;color:var(--text);font-size:13px;font-family:inherit;transition:border-color .15s}
483
+ .settings-field input:focus,.settings-field select:focus{outline:none;border-color:var(--pri)}
484
+ .settings-field input[type="password"]{font-family:'Courier New',monospace;letter-spacing:.05em}
485
+ .settings-field .field-hint{font-size:10px;color:var(--text-muted);margin-top:2px}
486
+ .settings-field.full-width{grid-column:1/-1}
487
+ .settings-toggle{display:flex;align-items:center;gap:10px;padding:4px 0}
488
+ .settings-toggle label{font-size:12px;font-weight:500;color:var(--text-sec);text-transform:none;letter-spacing:0}
489
+ .toggle-switch{position:relative;width:36px;height:20px;cursor:pointer}
490
+ .toggle-switch input{opacity:0;width:0;height:0}
491
+ .toggle-slider{position:absolute;inset:0;background:var(--border);border-radius:20px;transition:.2s}
492
+ .toggle-slider::before{content:'';position:absolute;height:14px;width:14px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s}
493
+ .toggle-switch input:checked+.toggle-slider{background:var(--pri)}
494
+ .toggle-switch input:checked+.toggle-slider::before{transform:translateX(16px)}
495
+ .settings-actions{display:flex;gap:12px;justify-content:flex-end;align-items:center;margin-top:16px;padding-top:16px;border-top:1px solid var(--border)}
496
+ .settings-actions .btn{min-width:110px;padding:10px 20px;font-size:13px}
497
+ .settings-actions .btn-primary{background:rgba(99,102,241,.08);color:var(--pri);border:1px solid rgba(99,102,241,.25);font-weight:600}
498
+ .settings-actions .btn-primary:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}
499
+ [data-theme="light"] .settings-actions .btn-primary{background:rgba(79,70,229,.06);color:#4f46e5;border:1px solid rgba(79,70,229,.2)}
500
+ [data-theme="light"] .settings-actions .btn-primary:hover{background:rgba(79,70,229,.1);border-color:#4f46e5}
501
+ .settings-saved{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px;font-weight:600;opacity:0;transition:opacity .3s}
502
+ .settings-saved.show{opacity:1}
503
+ .feed-wrap{flex:1;min-width:0;display:flex;flex-direction:column}
504
+ .feed-wrap.hide{display:none}
505
+ .analytics-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
506
+ .analytics-card{position:relative;overflow:hidden;border-radius:var(--radius-lg);padding:22px 20px;transition:all .2s ease;border:1px solid var(--border);background:var(--bg-card)}
507
+ .analytics-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--pri);opacity:.5}
508
+ .analytics-card::after{display:none}
509
+ .analytics-card:hover{transform:translateY(-2px);box-shadow:var(--shadow);border-color:var(--border-glow)}
510
+ .analytics-card.green::before{background:var(--green)}
511
+ .analytics-card.amber::before{background:var(--amber)}
512
+ .analytics-card .ac-value{font-size:28px;font-weight:700;letter-spacing:-.03em;color:var(--text);line-height:1;-webkit-text-fill-color:unset;background:none}
513
+ .analytics-card.green .ac-value{color:var(--green);background:none}
514
+ .analytics-card.amber .ac-value{color:var(--amber);background:none}
515
+ .analytics-card .ac-label{font-size:11px;color:var(--text-muted);margin-top:6px;font-weight:500;text-transform:uppercase;letter-spacing:.06em}
516
+ .analytics-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:22px 24px;position:relative;overflow:hidden}
517
+ .analytics-section::before{display:none}
518
+ .analytics-section h3{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:16px;display:flex;align-items:center;gap:8px}
519
+ .analytics-section h3 .icon{font-size:14px;opacity:.6}
520
+ .chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto;justify-content:center}
521
+ .chart-bar-wrap{flex:1;min-width:28px;max-width:80px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}
522
+ .chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}
523
+ .chart-bar-wrap:hover .chart-bar{opacity:1}
524
+ .chart-bar-wrap:hover .chart-bar-label{color:var(--text)}
525
+ .chart-bar-wrap:hover .chart-tip{opacity:1;transform:translateX(-50%) translateY(0)}
526
+ .chart-tip{position:absolute;top:-6px;left:50%;transform:translateX(-50%) translateY(4px);background:var(--bg-card);border:1px solid var(--border-glow);color:var(--text);padding:2px 8px;border-radius:6px;font-size:10px;font-weight:600;white-space:nowrap;z-index:5;pointer-events:none;box-shadow:var(--shadow);opacity:0;transition:all .15s ease}
527
+ .chart-bar{width:100%;border-radius:3px 3px 1px 1px;background:#818cf8;opacity:.75;transition:all .2s ease}
528
+ .chart-bar.violet{background:#6366f1}
529
+ .chart-bar.green{background:var(--green)}
530
+ .chart-bar.zero{background:var(--border);opacity:.3;border-radius:2px}
531
+ .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}
532
+ .chart-legend{display:flex;gap:14px;margin-top:12px;flex-wrap:wrap;font-size:11px;color:var(--text-sec);font-weight:500}
533
+ .chart-legend span{display:inline-flex;align-items:center;gap:5px}
534
+ .chart-legend .dot{width:8px;height:8px;border-radius:2px}
535
+ .chart-legend .dot.pri{background:var(--pri)}
536
+ .tool-chart-svg{width:100%;height:100%;display:block}
537
+ .tool-chart-svg .grid-line{stroke:var(--border);stroke-dasharray:3 3;stroke-width:0.5}
538
+ .tool-chart-svg .axis-label{fill:var(--text-muted);font-size:10px;font-family:var(--mono)}
539
+ .tool-chart-svg .data-line{fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:2000;stroke-dashoffset:2000;animation:lineIn .6s ease forwards}
540
+ @keyframes lineIn{to{stroke-dashoffset:0}}
541
+ .tool-chart-svg .data-area{opacity:1}
542
+ .tool-chart-svg .hover-dot{r:3.5;stroke-width:2;stroke:var(--bg);opacity:0;transition:opacity .1s}
543
+ .tool-chart-svg .hover-dot.show{opacity:1}
544
+ .tool-chart-tooltip{position:absolute;top:0;left:0;background:var(--bg-card);border:1px solid var(--border-glow);color:var(--text);padding:8px 12px;border-radius:8px;font-size:11px;font-family:var(--mono);pointer-events:none;opacity:0;transition:opacity .1s;z-index:10;box-shadow:var(--shadow-lg);white-space:nowrap}
545
+ .tool-chart-tooltip.show{opacity:1}
546
+ .tool-chart-tooltip .tt-time{color:var(--text-muted);font-size:10px;margin-bottom:4px;font-weight:500}
547
+ .tool-chart-tooltip .tt-row{display:flex;align-items:center;gap:6px;margin:2px 0}
548
+ .tool-chart-tooltip .tt-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
549
+ .tool-chart-tooltip .tt-val{font-weight:600;margin-left:auto;padding-left:12px}
550
+ .tool-agg-table{width:100%;border-collapse:collapse;font-size:12px}
551
+ .tool-agg-table th{text-align:left;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;font-size:10px;padding:8px 12px;border-bottom:1px solid var(--border)}
552
+ .tool-agg-table td{padding:8px 12px;color:var(--text-sec);border-bottom:1px solid var(--border)}
553
+ .tool-agg-table tr:hover td{background:rgba(99,102,241,.04);color:var(--text)}
554
+ .tool-agg-table .tool-name{font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
555
+ .tool-agg-table .tool-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}
556
+ .tool-agg-table .ms-val{font-family:var(--mono);font-weight:600}
557
+ .tool-agg-table .ms-val.fast{color:var(--green)}
558
+ .tool-agg-table .ms-val.medium{color:var(--amber)}
559
+ .tool-agg-table .ms-val.slow{color:var(--accent)}
560
+ .chart-legend .dot.violet{background:var(--violet)}
561
+ .chart-legend .dot.green{background:var(--green)}
562
+ .breakdown-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}
563
+ .breakdown-item{display:flex;flex-direction:column;gap:5px;padding:10px 12px;background:rgba(255,255,255,.02);border-radius:8px;border:1px solid var(--border);transition:all .15s}
564
+ .breakdown-item:hover{border-color:var(--border-glow);background:rgba(255,255,255,.04)}
565
+ .breakdown-item .bd-top{display:flex;align-items:center;justify-content:space-between}
566
+ .breakdown-item .label{font-size:12px;color:var(--text-sec);font-weight:500;text-transform:capitalize}
567
+ .breakdown-item .value{font-size:13px;font-weight:600;color:var(--text)}
568
+ .breakdown-bar-wrap{height:3px;background:rgba(255,255,255,.06);border-radius:2px;overflow:hidden}
569
+ .breakdown-bar{height:100%;border-radius:2px;background:var(--pri);transition:width .5s ease}
570
+ .metrics-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}
571
+ .range-btn{padding:5px 12px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-sec);font-size:12px;font-weight:500;cursor:pointer;transition:all .15s}
572
+ .range-btn:hover{border-color:var(--pri);color:var(--pri)}
573
+ .range-btn.active{background:rgba(99,102,241,.08);color:var(--pri);border-color:rgba(99,102,241,.25)}
574
+
575
+ .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}
202
576
  .theme-toggle .theme-icon-light{display:none}
203
577
  .theme-toggle .theme-icon-dark{display:inline}
204
578
  [data-theme="light"] .theme-toggle .theme-icon-light{display:inline}
205
579
  [data-theme="light"] .theme-toggle .theme-icon-dark{display:none}
206
580
 
207
- .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}
208
- .auth-theme-toggle:hover{background:rgba(255,255,255,.4)}
581
+ .auth-top-actions{position:absolute;top:16px;right:16px;z-index:10;display:flex;align-items:center;gap:2px}
582
+ .auth-theme-toggle{min-width:28px;height:28px;border:none;border-radius:14px;background:rgba(255,255,255,.12);color:rgba(255,255,255,.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;transition:all .2s;padding:0 8px;font-weight:600}
583
+ .auth-theme-toggle:hover{background:rgba(255,255,255,.25);color:#fff}
209
584
  .auth-theme-toggle .theme-icon-light{display:none}
210
585
  .auth-theme-toggle .theme-icon-dark{display:inline}
211
- [data-theme="light"] .auth-theme-toggle{background:rgba(0,0,0,.08);color:#0f172a}
212
- [data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.12)}
586
+ [data-theme="light"] .auth-theme-toggle{color:rgba(0,0,0,.4);background:rgba(0,0,0,.05)}
587
+ [data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.1);color:#0f172a}
588
+ [data-theme="light"] .auth-top-actions{background:none}
213
589
  [data-theme="light"] .auth-theme-toggle .theme-icon-light{display:inline}
214
590
  [data-theme="light"] .auth-theme-toggle .theme-icon-dark{display:none}
215
591
 
216
- @media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}}
592
+ @media(max-width:1100px){.analytics-cards{grid-template-columns:repeat(3,1fr)}}
593
+ @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}}
217
594
  </style>
218
595
  </head>
219
596
  <body>
220
597
 
221
598
  <!-- ─── Auth: Setup Password ─── -->
222
599
  <div id="setupScreen" class="auth-screen" style="display:none">
223
- <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>
600
+ <div class="auth-top-actions">
601
+ <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>
602
+ <button class="auth-theme-toggle" onclick="toggleLang()" aria-label="Switch language"><span data-i18n="lang.switch">EN</span></button>
603
+ </div>
224
604
  <div class="auth-card">
225
605
  <div class="logo">\u{1F99E}</div>
226
- <h1>OpenClaw Memory</h1>
227
- <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px">Powered by MemOS</p>
228
- <p>Set a password to protect your memories</p>
229
- <input type="password" id="setupPw" placeholder="Enter a password (4+ characters)" autofocus>
230
- <input type="password" id="setupPw2" placeholder="Confirm password">
231
- <button class="btn-auth" onclick="doSetup()">Set Password & Enter</button>
606
+ <h1 data-i18n="title">OpenClaw Memory</h1>
607
+ <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px" data-i18n="subtitle">Powered by MemOS</p>
608
+ <p data-i18n="setup.desc">Set a password to protect your memories</p>
609
+ <input type="password" id="setupPw" data-i18n-ph="setup.pw" placeholder="Enter a password (4+ characters)" autofocus>
610
+ <input type="password" id="setupPw2" data-i18n-ph="setup.pw2" placeholder="Confirm password">
611
+ <button class="btn-auth" onclick="doSetup()" data-i18n="setup.btn">Set Password & Enter</button>
232
612
  <div class="error-msg" id="setupErr"></div>
233
613
  </div>
234
614
  </div>
235
615
 
236
616
  <!-- ─── Auth: Login ─── -->
237
617
  <div id="loginScreen" class="auth-screen" style="display:none">
238
- <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>
618
+ <div class="auth-top-actions">
619
+ <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>
620
+ <button class="auth-theme-toggle" onclick="toggleLang()" aria-label="Switch language"><span data-i18n="lang.switch">EN</span></button>
621
+ </div>
239
622
  <div class="auth-card">
240
623
  <div class="logo">\u{1F99E}</div>
241
- <h1>OpenClaw Memory</h1>
242
- <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px">Powered by MemOS</p>
243
- <p>Enter your password to access memories</p>
624
+ <h1 data-i18n="title">OpenClaw Memory</h1>
625
+ <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px" data-i18n="subtitle">Powered by MemOS</p>
626
+ <p data-i18n="login.desc">Enter your password to access memories</p>
244
627
  <div id="loginForm">
245
- <input type="password" id="loginPw" placeholder="Password" autofocus>
246
- <button class="btn-auth" onclick="doLogin()">Unlock</button>
628
+ <input type="password" id="loginPw" data-i18n-ph="login.pw" placeholder="Password" autofocus>
629
+ <button class="btn-auth" onclick="doLogin()" data-i18n="login.btn">Unlock</button>
247
630
  <div class="error-msg" id="loginErr"></div>
248
- <button class="btn-text" style="margin-top:12px;font-size:13px;color:var(--text-sec)" onclick="showResetForm()">Forgot password?</button>
631
+ <button class="btn-text" style="margin-top:12px;font-size:13px;color:var(--text-sec)" onclick="showResetForm()" data-i18n="login.forgot">Forgot password?</button>
249
632
  </div>
250
633
  <div id="resetForm" style="display:none">
251
634
  <div class="reset-guide">
252
635
  <div class="reset-step">
253
636
  <div class="step-num">1</div>
254
637
  <div class="step-body">
255
- <div class="step-title">Open Terminal</div>
256
- <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>
638
+ <div class="step-title" data-i18n="reset.step1.title">Open Terminal</div>
639
+ <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>
257
640
  <div class="cmd-box" onclick="copyCmd(this)">
258
641
  <code>grep "password reset token:" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>
259
- <span class="copy-hint">Click to copy</span>
642
+ <span class="copy-hint" data-i18n="copy.hint">Click to copy</span>
260
643
  </div>
261
644
  </div>
262
645
  </div>
263
646
  <div class="reset-step">
264
647
  <div class="step-num">2</div>
265
648
  <div class="step-body">
266
- <div class="step-title">Find the token</div>
267
- <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>
649
+ <div class="step-title" data-i18n="reset.step2.title">Find the token</div>
650
+ <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>
268
651
  </div>
269
652
  </div>
270
653
  <div class="reset-step">
271
654
  <div class="step-num">3</div>
272
655
  <div class="step-body">
273
- <div class="step-title">Paste & reset</div>
274
- <div class="step-desc">Paste the token below and set your new password.</div>
656
+ <div class="step-title" data-i18n="reset.step3.title">Paste & reset</div>
657
+ <div class="step-desc" data-i18n="reset.step3.desc">Paste the token below and set your new password.</div>
275
658
  </div>
276
659
  </div>
277
660
  </div>
278
- <input type="text" id="resetToken" placeholder="Paste reset token here" style="margin-bottom:8px;font-family:monospace">
279
- <input type="password" id="resetNewPw" placeholder="New password (4+ characters)">
280
- <input type="password" id="resetNewPw2" placeholder="Confirm new password">
281
- <button class="btn-auth" onclick="doReset()">Reset Password</button>
661
+ <input type="text" id="resetToken" data-i18n-ph="reset.token" placeholder="Paste reset token here" style="margin-bottom:8px;font-family:monospace">
662
+ <input type="password" id="resetNewPw" data-i18n-ph="reset.newpw" placeholder="New password (4+ characters)">
663
+ <input type="password" id="resetNewPw2" data-i18n-ph="reset.newpw2" placeholder="Confirm new password">
664
+ <button class="btn-auth" onclick="doReset()" data-i18n="reset.btn">Reset Password</button>
282
665
  <div class="error-msg" id="resetErr"></div>
283
- <button class="btn-text" style="margin-top:12px;font-size:13px;color:var(--text-sec)" onclick="showLoginForm()">\u2190 Back to login</button>
666
+ <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>
284
667
  </div>
285
668
  </div>
286
669
  </div>
@@ -290,78 +673,364 @@ input,textarea,select{font-family:inherit;font-size:inherit}
290
673
  <div class="topbar">
291
674
  <div class="brand">
292
675
  <div class="icon">\u{1F99E}</div>
293
- <span>OpenClaw Memory <span style="font-weight:400;color:var(--text-sec);font-size:12px">by MemOS</span></span>
676
+ <span data-i18n="title">OpenClaw Memory</span>
677
+ </div>
678
+ <div class="topbar-center">
679
+ <nav class="nav-tabs">
680
+ <button class="tab active" data-view="memories" onclick="switchView('memories')" data-i18n="tab.memories">\u{1F4DA} Memories</button>
681
+ <button class="tab" data-view="tasks" onclick="switchView('tasks')" data-i18n="tab.tasks">\u{1F4CB} Tasks</button>
682
+ <button class="tab" data-view="skills" onclick="switchView('skills')" data-i18n="tab.skills">\u{1F9E0} Skills</button>
683
+ <button class="tab" data-view="analytics" onclick="switchView('analytics')" data-i18n="tab.analytics">\u{1F4CA} Analytics</button>
684
+ <button class="tab" data-view="logs" onclick="switchView('logs')" data-i18n="tab.logs">\u{1F4DD} Logs</button>
685
+ <button class="tab" data-view="settings" onclick="switchView('settings')" data-i18n="tab.settings">\u2699 Settings</button>
686
+ </nav>
294
687
  </div>
295
688
  <div class="actions">
689
+ <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>
296
690
  <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>
297
- <button class="btn btn-primary" onclick="openCreateModal()">+ New Memory</button>
298
- <button class="btn" onclick="loadAll()">Refresh</button>
299
- <button class="btn btn-danger" onclick="clearAll()">Clear All</button>
300
- <button class="btn btn-text" onclick="doLogout()">Logout</button>
691
+ <button class="btn btn-ghost btn-sm" onclick="loadAll()" data-i18n="refresh">\u21BB Refresh</button>
692
+ <button class="btn btn-ghost btn-sm" onclick="doLogout()" data-i18n="logout">Logout</button>
301
693
  </div>
302
694
  </div>
303
695
 
304
696
  <div class="main-content">
305
697
  <div class="sidebar" id="sidebar">
306
698
  <div class="stats-grid" id="statsGrid">
307
- <div class="stat-card pri"><div class="stat-value" id="statTotal">-</div><div class="stat-label">Memories</div></div>
308
- <div class="stat-card green"><div class="stat-value" id="statSessions">-</div><div class="stat-label">Sessions</div></div>
309
- <div class="stat-card amber"><div class="stat-value" id="statEmbeddings">-</div><div class="stat-label">Embeddings</div></div>
310
- <div class="stat-card rose"><div class="stat-value" id="statTimeSpan">-</div><div class="stat-label">Days</div></div>
699
+ <div class="stat-card pri"><div class="stat-value" id="statTotal">-</div><div class="stat-label" data-i18n="stat.memories">Memories</div></div>
700
+ <div class="stat-card green"><div class="stat-value" id="statSessions">-</div><div class="stat-label" data-i18n="stat.sessions">Sessions</div></div>
701
+ <div class="stat-card amber"><div class="stat-value" id="statEmbeddings">-</div><div class="stat-label" data-i18n="stat.embeddings">Embeddings</div></div>
702
+ <div class="stat-card rose"><div class="stat-value" id="statTimeSpan">-</div><div class="stat-label" data-i18n="stat.days">Days</div></div>
311
703
  </div>
312
704
  <div id="embeddingStatus"></div>
313
- <div class="section-title">Sessions</div>
705
+ <div class="section-title" data-i18n="sidebar.sessions">Sessions</div>
314
706
  <div class="session-list" id="sessionList"></div>
707
+ <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>
315
708
  </div>
316
709
 
710
+ <div class="feed-wrap" id="feedWrap">
317
711
  <div class="feed">
318
712
  <div class="search-bar">
319
713
  <span class="search-icon">\u{1F50D}</span>
320
- <input type="text" id="searchInput" placeholder="Search memories (supports semantic search)..." oninput="debounceSearch()">
714
+ <input type="text" id="searchInput" data-i18n-ph="search.placeholder" placeholder="Search memories (supports semantic search)..." oninput="debounceSearch()">
321
715
  </div>
322
716
  <div class="search-meta" id="searchMeta"></div>
323
717
  <div class="filter-bar" id="filterBar">
324
- <button class="filter-chip active" data-role="" onclick="setRoleFilter(this,'')">All</button>
718
+ <button class="filter-chip active" data-role="" onclick="setRoleFilter(this,'')" data-i18n="filter.all">All</button>
325
719
  <button class="filter-chip" data-role="user" onclick="setRoleFilter(this,'user')">User</button>
326
720
  <button class="filter-chip" data-role="assistant" onclick="setRoleFilter(this,'assistant')">Assistant</button>
327
721
  <button class="filter-chip" data-role="system" onclick="setRoleFilter(this,'system')">System</button>
328
722
  <span class="filter-sep"></span>
329
723
  <select id="filterKind" class="filter-select" onchange="applyFilters()">
330
- <option value="">All kinds</option>
331
- <option value="paragraph">Paragraph</option>
332
- <option value="code_block">Code</option>
333
- <option value="dialog">Dialog</option>
334
- <option value="list">List</option>
335
- <option value="error_stack">Error</option>
336
- <option value="command">Command</option>
724
+ <option value="" data-i18n="filter.allkinds">All kinds</option>
725
+ <option value="paragraph" data-i18n="filter.paragraph">Paragraph</option>
726
+ <option value="code_block" data-i18n="filter.code">Code</option>
727
+ <option value="dialog" data-i18n="filter.dialog">Dialog</option>
728
+ <option value="list" data-i18n="filter.list">List</option>
729
+ <option value="error_stack" data-i18n="filter.error">Error</option>
730
+ <option value="command" data-i18n="filter.command">Command</option>
337
731
  </select>
338
732
  <select id="filterSort" class="filter-select" onchange="applyFilters()">
339
- <option value="newest">Newest first</option>
340
- <option value="oldest">Oldest first</option>
733
+ <option value="newest" data-i18n="filter.newest">Newest first</option>
734
+ <option value="oldest" data-i18n="filter.oldest">Oldest first</option>
341
735
  </select>
342
736
  </div>
343
737
  <div class="date-filter">
344
- <label>From</label><input type="datetime-local" id="dateFrom" step="1" onchange="applyFilters()">
345
- <label>To</label><input type="datetime-local" id="dateTo" step="1" onchange="applyFilters()">
346
- <button class="btn btn-sm btn-text" onclick="clearDateFilter()">Clear</button>
738
+ <label data-i18n="filter.from">From</label><input type="datetime-local" id="dateFrom" step="1" onchange="applyFilters()">
739
+ <label data-i18n="filter.to">To</label><input type="datetime-local" id="dateTo" step="1" onchange="applyFilters()">
740
+ <button class="btn btn-sm btn-text" onclick="clearDateFilter()" data-i18n="filter.clear">Clear</button>
347
741
  </div>
348
742
  <div class="memory-list" id="memoryList"><div class="spinner"></div></div>
349
743
  <div class="pagination" id="pagination"></div>
350
744
  </div>
745
+ </div>
746
+ <div class="tasks-view" id="tasksView">
747
+ <div class="tasks-header">
748
+ <div class="tasks-stats">
749
+ <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>
750
+ <div class="tasks-stat"><span class="tasks-stat-value" id="tasksActiveCount">-</span><span class="tasks-stat-label" data-i18n="tasks.active">Active</span></div>
751
+ <div class="tasks-stat"><span class="tasks-stat-value" id="tasksCompletedCount">-</span><span class="tasks-stat-label" data-i18n="tasks.completed">Completed</span></div>
752
+ <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>
753
+ </div>
754
+ <div class="tasks-filters">
755
+ <button class="filter-chip active" data-task-status="" onclick="setTaskStatusFilter(this,'')" data-i18n="filter.all">All</button>
756
+ <button class="filter-chip" data-task-status="active" onclick="setTaskStatusFilter(this,'active')" data-i18n="tasks.status.active">Active</button>
757
+ <button class="filter-chip" data-task-status="completed" onclick="setTaskStatusFilter(this,'completed')" data-i18n="tasks.status.completed">Completed</button>
758
+ <button class="filter-chip" data-task-status="skipped" onclick="setTaskStatusFilter(this,'skipped')" data-i18n="tasks.status.skipped">Skipped</button>
759
+ <button class="btn btn-sm btn-ghost" onclick="loadTasks()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
760
+ </div>
761
+ </div>
762
+ <div class="tasks-list" id="tasksList"><div class="spinner"></div></div>
763
+ <div class="pagination" id="tasksPagination"></div>
764
+ <div class="task-detail-overlay" id="taskDetailOverlay" onclick="closeTaskDetail(event)">
765
+ <div class="task-detail-panel" onclick="event.stopPropagation()">
766
+ <div class="task-detail-header">
767
+ <h2 id="taskDetailTitle"></h2>
768
+ <button class="btn btn-icon" onclick="closeTaskDetail()" title="Close">\u2715</button>
769
+ </div>
770
+ <div class="task-detail-meta" id="taskDetailMeta"></div>
771
+ <div class="task-skill-section" id="taskSkillSection"></div>
772
+ <div class="task-detail-summary" id="taskDetailSummary"></div>
773
+ <div class="task-detail-chunks-title" data-i18n="tasks.chunks">Related Memories</div>
774
+ <div class="task-detail-chunks" id="taskDetailChunks"></div>
775
+ </div>
776
+ </div>
777
+ </div>
778
+ <div class="skills-view" id="skillsView">
779
+ <div class="tasks-header">
780
+ <div class="tasks-stats">
781
+ <div class="tasks-stat"><span class="tasks-stat-value" id="skillsTotalCount">-</span><span class="tasks-stat-label" data-i18n="skills.total">Total Skills</span></div>
782
+ <div class="tasks-stat" style="border-left:3px solid var(--green)"><span class="tasks-stat-value" id="skillsActiveCount">-</span><span class="tasks-stat-label" data-i18n="skills.active">Active</span></div>
783
+ <div class="tasks-stat" style="border-left:3px solid var(--amber)"><span class="tasks-stat-value" id="skillsDraftCount">-</span><span class="tasks-stat-label" data-i18n="skills.draft">Draft</span></div>
784
+ <div class="tasks-stat" style="border-left:3px solid var(--violet)"><span class="tasks-stat-value" id="skillsInstalledCount">-</span><span class="tasks-stat-label" data-i18n="skills.installed">Installed</span></div>
785
+ </div>
786
+ <div class="tasks-filters">
787
+ <button class="filter-chip active" data-skill-status="" onclick="setSkillStatusFilter(this,'')" data-i18n="filter.all">All</button>
788
+ <button class="filter-chip" data-skill-status="active" onclick="setSkillStatusFilter(this,'active')" data-i18n="skills.filter.active">Active</button>
789
+ <button class="filter-chip" data-skill-status="draft" onclick="setSkillStatusFilter(this,'draft')" data-i18n="skills.filter.draft">Draft</button>
790
+ <button class="filter-chip" data-skill-status="archived" onclick="setSkillStatusFilter(this,'archived')" data-i18n="skills.filter.archived">Archived</button>
791
+ <button class="btn btn-sm btn-ghost" onclick="loadSkills()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
792
+ </div>
793
+ </div>
794
+ <div class="tasks-list" id="skillsList"><div class="spinner"></div></div>
795
+ </div>
796
+ <div class="task-detail-overlay" id="skillDetailOverlay" onclick="closeSkillDetail(event)">
797
+ <div class="task-detail-panel" onclick="event.stopPropagation()">
798
+ <div class="task-detail-header">
799
+ <h2 id="skillDetailTitle"></h2>
800
+ <div style="display:flex;gap:8px;align-items:center">
801
+ <button class="skill-download-btn" id="skillDownloadBtn" onclick="downloadSkill()" data-i18n="skills.download">\u2B07 Download</button>
802
+ <button class="btn btn-icon" onclick="closeSkillDetail()" title="Close">\u2715</button>
803
+ </div>
804
+ </div>
805
+ <div class="task-detail-meta" id="skillDetailMeta"></div>
806
+ <div class="skill-detail-desc" id="skillDetailDesc"></div>
807
+ <div class="task-detail-chunks-title" data-i18n="skills.files">Skill Files</div>
808
+ <div class="skill-files-list" id="skillFilesList"></div>
809
+ <div class="task-detail-chunks-title" id="skillContentTitle" data-i18n="skills.content">SKILL.md Content</div>
810
+ <div class="task-detail-summary" id="skillDetailContent" style="max-height:50vh;overflow-y:auto"></div>
811
+ <div class="task-detail-chunks-title" data-i18n="skills.versions">Version History</div>
812
+ <div class="task-detail-chunks" id="skillVersionsList" style="gap:10px"></div>
813
+ <div class="task-detail-chunks-title" style="margin-top:16px" data-i18n="skills.related">Related Tasks</div>
814
+ <div class="task-detail-chunks" id="skillRelatedTasks" style="gap:8px"></div>
815
+ </div>
816
+ </div>
817
+ <div class="analytics-view" id="analyticsView">
818
+ <div class="metrics-toolbar">
819
+ <span style="font-size:12px;color:var(--text-sec);font-weight:600" data-i18n="range">Range</span>
820
+ <button class="range-btn" data-days="7" onclick="setMetricsDays(7)">7 <span data-i18n="range.days">days</span></button>
821
+ <button class="range-btn active" data-days="30" onclick="setMetricsDays(30)">30 <span data-i18n="range.days">days</span></button>
822
+ <button class="range-btn" data-days="90" onclick="setMetricsDays(90)">90 <span data-i18n="range.days">days</span></button>
823
+ <button class="btn btn-sm" onclick="loadMetrics()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
824
+ </div>
825
+ <div class="analytics-cards" id="analyticsCards">
826
+ <div class="analytics-card"><div class="ac-value" id="mTotal">-</div><div class="ac-label" data-i18n="analytics.total">Total Memories</div></div>
827
+ <div class="analytics-card green"><div class="ac-value" id="mTodayWrites">-</div><div class="ac-label" data-i18n="analytics.writes">Writes Today</div></div>
828
+ <div class="analytics-card"><div class="ac-value" id="mSessions">-</div><div class="ac-label" data-i18n="analytics.sessions">Sessions</div></div>
829
+ <div class="analytics-card amber"><div class="ac-value" id="mEmbeddings">-</div><div class="ac-label" data-i18n="analytics.embeddings">Embeddings</div></div>
830
+ </div>
831
+ <div class="analytics-section">
832
+ <h3><span class="icon">\u{1F4CA}</span> <span data-i18n="chart.writes">Memory Writes per Day</span></h3>
833
+ <div class="chart-bars" id="chartWrites"></div>
834
+ </div>
835
+
836
+ <div class="analytics-section" id="toolPerfSection" style="position:relative">
837
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
838
+ <h3 style="margin-bottom:0"><span class="icon">\u26A1</span> <span data-i18n="chart.toolperf">Tool Response Time</span> <span style="font-size:10px;color:var(--text-muted);font-weight:500;text-transform:none;letter-spacing:0;margin-left:4px">(per minute avg)</span></h3>
839
+ <div style="display:flex;gap:6px;align-items:center">
840
+ <button class="range-btn tool-range active" data-mins="60" onclick="setToolMinutes(60)">1h</button>
841
+ <button class="range-btn tool-range" data-mins="360" onclick="setToolMinutes(360)">6h</button>
842
+ <button class="range-btn tool-range" data-mins="1440" onclick="setToolMinutes(1440)">24h</button>
843
+ </div>
844
+ </div>
845
+ <div id="toolChart" style="width:100%;height:280px;position:relative;overflow:hidden;border-radius:12px"></div>
846
+ <div id="toolLegend" class="chart-legend" style="margin-top:14px;padding:0 4px"></div>
847
+ <div id="toolAggTable" style="margin-top:20px"></div>
848
+ </div>
849
+
850
+ <div class="breakdown-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
851
+ <div class="analytics-section">
852
+ <h3><span class="icon">\u{1F464}</span> <span data-i18n="breakdown.role">By Role</span></h3>
853
+ <div id="breakdownRole"></div>
854
+ </div>
855
+ <div class="analytics-section">
856
+ <h3><span class="icon">\u{1F4DD}</span> <span data-i18n="breakdown.kind">By Kind</span></h3>
857
+ <div id="breakdownKind"></div>
858
+ </div>
859
+ </div>
860
+ </div>
861
+
862
+ <!-- ─── Logs View ─── -->
863
+ <div class="logs-view" id="logsView">
864
+ <div class="logs-toolbar">
865
+ <div class="logs-toolbar-left">
866
+ <select id="logToolFilter" onchange="onLogFilterChange()" style="font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--text);min-width:120px">
867
+ <option value="" data-i18n="logs.allTools">All Tools</option>
868
+ </select>
869
+ <button class="btn btn-sm btn-ghost" onclick="loadLogs()" style="font-size:12px">\u21BB <span data-i18n="logs.refresh">Refresh</span></button>
870
+ </div>
871
+ <div class="logs-toolbar-right">
872
+ <input type="checkbox" id="logAutoRefresh" checked style="display:none">
873
+ </div>
874
+ </div>
875
+ <div class="logs-list" id="logsList"></div>
876
+ <div id="logsPagination"></div>
877
+ </div>
878
+
879
+ <!-- ─── Settings View ─── -->
880
+ <div class="settings-view" id="settingsView">
881
+ <div class="settings-section">
882
+ <h3><span class="icon">\u{1F4E1}</span> <span data-i18n="settings.embedding">Embedding Model</span></h3>
883
+ <div class="settings-grid">
884
+ <div class="settings-field">
885
+ <label data-i18n="settings.provider">Provider</label>
886
+ <select id="cfgEmbProvider">
887
+ <option value="openai_compatible">OpenAI Compatible</option>
888
+ <option value="openai">OpenAI</option>
889
+ <option value="gemini">Gemini</option>
890
+ <option value="azure_openai">Azure OpenAI</option>
891
+ <option value="cohere">Cohere</option>
892
+ <option value="mistral">Mistral</option>
893
+ <option value="voyage">Voyage</option>
894
+ <option value="local">Local</option>
895
+ </select>
896
+ </div>
897
+ <div class="settings-field">
898
+ <label data-i18n="settings.model">Model</label>
899
+ <input type="text" id="cfgEmbModel" placeholder="e.g. bge-m3">
900
+ </div>
901
+ <div class="settings-field full-width">
902
+ <label>Endpoint</label>
903
+ <input type="text" id="cfgEmbEndpoint" placeholder="https://...">
904
+ </div>
905
+ <div class="settings-field">
906
+ <label>API Key</label>
907
+ <input type="password" id="cfgEmbApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
908
+ </div>
909
+ </div>
910
+ </div>
911
+
912
+ <div class="settings-section">
913
+ <h3><span class="icon">\u{1F9E0}</span> <span data-i18n="settings.summarizer">Summarizer Model</span></h3>
914
+ <div class="settings-grid">
915
+ <div class="settings-field">
916
+ <label data-i18n="settings.provider">Provider</label>
917
+ <select id="cfgSumProvider">
918
+ <option value="openai_compatible">OpenAI Compatible</option>
919
+ <option value="openai">OpenAI</option>
920
+ <option value="anthropic">Anthropic</option>
921
+ <option value="gemini">Gemini</option>
922
+ <option value="azure_openai">Azure OpenAI</option>
923
+ <option value="bedrock">Bedrock</option>
924
+ </select>
925
+ </div>
926
+ <div class="settings-field">
927
+ <label data-i18n="settings.model">Model</label>
928
+ <input type="text" id="cfgSumModel" placeholder="e.g. gpt-4o-mini">
929
+ </div>
930
+ <div class="settings-field full-width">
931
+ <label>Endpoint</label>
932
+ <input type="text" id="cfgSumEndpoint" placeholder="https://...">
933
+ </div>
934
+ <div class="settings-field">
935
+ <label>API Key</label>
936
+ <input type="password" id="cfgSumApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
937
+ </div>
938
+ <div class="settings-field">
939
+ <label data-i18n="settings.temperature">Temperature</label>
940
+ <input type="number" id="cfgSumTemp" step="0.1" min="0" max="2" placeholder="0">
941
+ </div>
942
+ </div>
943
+ </div>
944
+
945
+ <div class="settings-section">
946
+ <h3><span class="icon">\u{1F527}</span> <span data-i18n="settings.skill">Skill Evolution</span></h3>
947
+ <div class="settings-grid">
948
+ <div class="settings-toggle">
949
+ <label class="toggle-switch"><input type="checkbox" id="cfgSkillEnabled"><span class="toggle-slider"></span></label>
950
+ <label data-i18n="settings.skill.enabled">Enable Skill Evolution</label>
951
+ </div>
952
+ <div class="settings-toggle">
953
+ <label class="toggle-switch"><input type="checkbox" id="cfgSkillAutoInstall"><span class="toggle-slider"></span></label>
954
+ <label data-i18n="settings.skill.autoinstall">Auto Install Skills</label>
955
+ </div>
956
+ <div class="settings-field">
957
+ <label data-i18n="settings.skill.confidence">Min Confidence</label>
958
+ <input type="number" id="cfgSkillConfidence" step="0.1" min="0" max="1" placeholder="0.7">
959
+ </div>
960
+ <div class="settings-field">
961
+ <label data-i18n="settings.skill.minchunks">Min Chunks</label>
962
+ <input type="number" id="cfgSkillMinChunks" placeholder="6">
963
+ </div>
964
+ </div>
965
+ <div style="margin-top:16px;padding-top:16px;border-top:1px dashed var(--border)">
966
+ <h3 style="font-size:12px;color:var(--text-muted);margin-bottom:4px;display:flex;align-items:center;gap:8px"><span class="icon">\u{1F680}</span> <span data-i18n="settings.skill.model">Skill Dedicated Model</span><span style="font-size:10px;font-weight:500;color:var(--amber);background:var(--amber-bg);padding:2px 8px;border-radius:10px" data-i18n="settings.optional">Optional</span></h3>
967
+ <div style="font-size:11px;color:var(--text-muted);margin-bottom:12px;line-height:1.5" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.</div>
968
+ <div class="settings-grid">
969
+ <div class="settings-field">
970
+ <label data-i18n="settings.provider">Provider</label>
971
+ <select id="cfgSkillProvider">
972
+ <option value="" id="cfgSkillProviderDefault">\u2014</option>
973
+ <option value="openai_compatible">OpenAI Compatible</option>
974
+ <option value="openai">OpenAI</option>
975
+ <option value="anthropic">Anthropic</option>
976
+ <option value="gemini">Gemini</option>
977
+ <option value="azure_openai">Azure OpenAI</option>
978
+ <option value="bedrock">Bedrock</option>
979
+ </select>
980
+ </div>
981
+ <div class="settings-field">
982
+ <label data-i18n="settings.model">Model</label>
983
+ <input type="text" id="cfgSkillModel" placeholder="e.g. claude-4.6-opus">
984
+ </div>
985
+ <div class="settings-field full-width">
986
+ <label>Endpoint</label>
987
+ <input type="text" id="cfgSkillEndpoint" placeholder="https://...">
988
+ </div>
989
+ <div class="settings-field">
990
+ <label>API Key</label>
991
+ <input type="password" id="cfgSkillApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
992
+ </div>
993
+ <div class="settings-field">
994
+ <label data-i18n="settings.temperature">Temperature</label>
995
+ <input type="number" id="cfgSkillTemp" step="0.1" min="0" max="2" placeholder="0.2">
996
+ </div>
997
+ </div>
998
+ </div>
999
+ </div>
1000
+
1001
+ <div class="settings-section">
1002
+ <h3><span class="icon">\u{1F4BE}</span> <span data-i18n="settings.general">General</span></h3>
1003
+ <div class="settings-grid">
1004
+ <div class="settings-field">
1005
+ <label data-i18n="settings.viewerport">Viewer Port</label>
1006
+ <input type="number" id="cfgViewerPort" placeholder="18799">
1007
+ <div class="field-hint" data-i18n="settings.viewerport.hint">Requires restart to take effect</div>
1008
+ </div>
1009
+ </div>
1010
+ </div>
1011
+
1012
+ <div class="settings-actions">
1013
+ <span class="settings-saved" id="settingsSaved">\u2713 <span data-i18n="settings.saved">Saved</span></span>
1014
+ <button class="btn btn-ghost" onclick="loadConfig()" data-i18n="settings.reset">Reset</button>
1015
+ <button class="btn btn-primary" onclick="saveConfig()" data-i18n="settings.save">Save Settings</button>
1016
+ </div>
1017
+ <div style="font-size:11px;color:var(--text-muted);text-align:right;margin-top:4px" data-i18n="settings.restart.hint">Some changes require restarting the OpenClaw gateway to take effect.</div>
1018
+ </div>
1019
+
351
1020
  </div>
352
1021
  </div>
353
1022
 
354
1023
  <!-- ─── Memory Modal ─── -->
355
1024
  <div class="modal-overlay" id="modalOverlay">
356
1025
  <div class="modal">
357
- <h2 id="modalTitle">New Memory</h2>
358
- <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>
359
- <div class="form-group"><label>Content</label><textarea id="mContent" rows="4" placeholder="Memory content..."></textarea></div>
360
- <div class="form-group"><label>Summary</label><input type="text" id="mSummary" placeholder="Brief summary (optional)"></div>
361
- <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>
1026
+ <h2 id="modalTitle" data-i18n="modal.new">New Memory</h2>
1027
+ <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>
1028
+ <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>
1029
+ <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>
1030
+ <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>
362
1031
  <div class="modal-actions">
363
- <button class="btn" onclick="closeModal()">Cancel</button>
364
- <button class="btn btn-primary" id="modalSubmit" onclick="submitModal()">Create</button>
1032
+ <button class="btn btn-ghost" onclick="closeModal()" data-i18n="modal.cancel">Cancel</button>
1033
+ <button class="btn btn-primary" id="modalSubmit" onclick="submitModal()" data-i18n="modal.create">Create</button>
365
1034
  </div>
366
1035
  </div>
367
1036
  </div>
@@ -370,7 +1039,434 @@ input,textarea,select{font-family:inherit;font-size:inherit}
370
1039
  <div class="toast-container" id="toasts"></div>
371
1040
 
372
1041
  <script>
373
- let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30;
1042
+ let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=40,metricsDays=30;
1043
+
1044
+ /* ─── i18n ─── */
1045
+ const I18N={
1046
+ en:{
1047
+ 'title':'OpenClaw Memory',
1048
+ 'subtitle':'Powered by MemOS',
1049
+ 'setup.desc':'Set a password to protect your memories',
1050
+ 'setup.pw':'Enter a password (4+ characters)',
1051
+ 'setup.pw2':'Confirm password',
1052
+ 'setup.btn':'Set Password & Enter',
1053
+ 'setup.err.short':'Password must be at least 4 characters',
1054
+ 'setup.err.mismatch':'Passwords do not match',
1055
+ 'setup.err.fail':'Setup failed',
1056
+ 'login.desc':'Enter your password to access memories',
1057
+ 'login.pw':'Password',
1058
+ 'login.btn':'Unlock',
1059
+ 'login.err':'Incorrect password',
1060
+ 'login.forgot':'Forgot password?',
1061
+ 'reset.step1.title':'Open Terminal',
1062
+ '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):',
1063
+ 'reset.step2.title':'Find the token',
1064
+ 'reset.step2.desc.pre':'In the output, find ',
1065
+ 'reset.step2.desc.post':' (plain line or inside JSON). Copy the 32-character hex string after the colon.',
1066
+ 'reset.step3.title':'Paste & reset',
1067
+ 'reset.step3.desc':'Paste the token below and set your new password.',
1068
+ 'reset.token':'Paste reset token here',
1069
+ 'reset.newpw':'New password (4+ characters)',
1070
+ 'reset.newpw2':'Confirm new password',
1071
+ 'reset.btn':'Reset Password',
1072
+ 'reset.err.token':'Please enter the reset token',
1073
+ 'reset.err.short':'Password must be at least 4 characters',
1074
+ 'reset.err.mismatch':'Passwords do not match',
1075
+ 'reset.err.fail':'Reset failed',
1076
+ 'reset.back':'\\u2190 Back to login',
1077
+ 'copy.hint':'Click to copy',
1078
+ 'copy.done':'Copied!',
1079
+ 'tab.memories':'\\u{1F4DA} Memories',
1080
+ 'tab.tasks':'\\u{1F4CB} Tasks',
1081
+ 'tab.skills':'\\u{1F9E0} Skills',
1082
+ 'tab.analytics':'\\u{1F4CA} Analytics',
1083
+ 'skills.total':'Total Skills',
1084
+ 'skills.active':'Active',
1085
+ 'skills.installed':'Installed',
1086
+ 'tasks.total':'Total Tasks',
1087
+ 'tasks.active':'Active',
1088
+ 'tasks.completed':'Completed',
1089
+ 'tasks.status.active':'Active',
1090
+ 'tasks.status.completed':'Completed',
1091
+ 'tasks.status.skipped':'Skipped',
1092
+ 'tasks.empty':'No tasks yet. Tasks are automatically created as you converse.',
1093
+ 'tasks.loading':'Loading...',
1094
+ 'tasks.untitled':'Untitled Task',
1095
+ 'tasks.chunks':'Related Memories',
1096
+ 'tasks.nochunks':'No memories in this task yet.',
1097
+ 'tasks.skipped.default':'This conversation was too brief to generate a summary. It will not appear in search results.',
1098
+ 'refresh':'\\u21BB Refresh',
1099
+ 'logout':'Logout',
1100
+ 'stat.memories':'Memories',
1101
+ 'stat.sessions':'Sessions',
1102
+ 'stat.embeddings':'Embeddings',
1103
+ 'stat.days':'Days',
1104
+ 'stat.active':'active',
1105
+ 'stat.deduped':'deduped',
1106
+ 'sidebar.sessions':'Sessions',
1107
+ 'sidebar.allsessions':'All Sessions',
1108
+ 'sidebar.clear':'\\u{1F5D1} Clear All Data',
1109
+ 'search.placeholder':'Search memories (supports semantic search)...',
1110
+ 'search.meta.total':' memories total',
1111
+ 'search.meta.semantic':' semantic',
1112
+ 'search.meta.text':' text',
1113
+ 'search.meta.results':' results',
1114
+ 'filter.all':'All',
1115
+ 'filter.allkinds':'All kinds',
1116
+ 'filter.paragraph':'Paragraph',
1117
+ 'filter.code':'Code',
1118
+ 'filter.dialog':'Dialog',
1119
+ 'filter.list':'List',
1120
+ 'filter.error':'Error',
1121
+ 'filter.command':'Command',
1122
+ 'filter.newest':'Newest first',
1123
+ 'filter.oldest':'Oldest first',
1124
+ 'filter.from':'From',
1125
+ 'filter.to':'To',
1126
+ 'filter.clear':'Clear',
1127
+ 'empty.text':'No memories found',
1128
+ 'card.expand':'Expand',
1129
+ 'card.edit':'Edit',
1130
+ 'card.delete':'Delete',
1131
+ 'card.evolved':'Evolved',
1132
+ 'card.times':'times',
1133
+ 'card.updated':'updated',
1134
+ 'card.evolveHistory':'Evolution History',
1135
+ 'card.oldSummary':'Old',
1136
+ 'card.dedupDuplicate':'Duplicate',
1137
+ 'card.dedupMerged':'Merged',
1138
+ 'card.dedupTarget':'Target: ',
1139
+ 'card.dedupReason':'Reason: ',
1140
+ 'card.newSummary':'New',
1141
+ 'pagination.total':' total',
1142
+ 'range':'Range',
1143
+ 'range.days':'days',
1144
+ 'analytics.total':'Total Memories',
1145
+ 'analytics.writes':'Writes Today',
1146
+ 'analytics.calls':'Viewer Calls Today',
1147
+ 'analytics.sessions':'Sessions',
1148
+ 'analytics.embeddings':'Embeddings',
1149
+ 'chart.writes':'Memory Writes per Day',
1150
+ 'chart.calls':'Viewer API Calls per Day (List / Search)',
1151
+ 'chart.nodata':'No data in this range',
1152
+ 'chart.nocalls':'No viewer calls in this range',
1153
+ 'chart.toolperf':'Tool Response Time',
1154
+ 'chart.list':'List',
1155
+ 'chart.search':'Search',
1156
+ 'breakdown.role':'By Role',
1157
+ 'breakdown.kind':'By Kind',
1158
+ 'modal.new':'New Memory',
1159
+ 'modal.edit':'Edit Memory',
1160
+ 'modal.role':'Role',
1161
+ 'modal.content':'Content',
1162
+ 'modal.content.ph':'Memory content...',
1163
+ 'modal.summary':'Summary',
1164
+ 'modal.summary.ph':'Brief summary (optional)',
1165
+ 'modal.kind':'Kind',
1166
+ 'modal.cancel':'Cancel',
1167
+ 'modal.create':'Create',
1168
+ 'modal.save':'Save',
1169
+ 'modal.err.empty':'Please enter content',
1170
+ 'toast.created':'Memory created',
1171
+ 'toast.updated':'Memory updated',
1172
+ 'toast.deleted':'Memory deleted',
1173
+ 'toast.opfail':'Operation failed',
1174
+ 'toast.delfail':'Delete failed',
1175
+ 'toast.cleared':'All memories cleared',
1176
+ 'toast.clearfail':'Clear failed',
1177
+ 'toast.notfound':'Memory not found in cache',
1178
+ 'confirm.delete':'Delete this memory?',
1179
+ 'confirm.clearall':'Delete ALL memories? This cannot be undone.',
1180
+ 'confirm.clearall2':'Are you absolutely sure?',
1181
+ 'embed.on':'Embedding: ',
1182
+ 'embed.off':'No embedding model',
1183
+ 'lang.switch':'中',
1184
+ 'tab.logs':'\u{1F4DD} Logs',
1185
+ 'logs.allTools':'All Tools',
1186
+ 'logs.refresh':'Refresh',
1187
+ 'logs.autoRefresh':'Auto-refresh',
1188
+ 'logs.input':'INPUT',
1189
+ 'logs.output':'OUTPUT',
1190
+ 'logs.empty':'No logs yet. Logs will appear here when tools are called.',
1191
+ 'logs.ago':'ago',
1192
+ 'tab.settings':'\u2699 Settings',
1193
+ 'settings.embedding':'Embedding Model',
1194
+ 'settings.summarizer':'Summarizer Model',
1195
+ 'settings.skill':'Skill Evolution',
1196
+ 'settings.general':'General',
1197
+ 'settings.provider':'Provider',
1198
+ 'settings.model':'Model',
1199
+ 'settings.temperature':'Temperature',
1200
+ 'settings.skill.enabled':'Enable Skill Evolution',
1201
+ 'settings.skill.autoinstall':'Auto Install Skills',
1202
+ 'settings.skill.confidence':'Min Confidence',
1203
+ 'settings.skill.minchunks':'Min Chunks',
1204
+ 'settings.skill.model':'Skill Dedicated Model',
1205
+ 'settings.skill.model.hint':'If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.',
1206
+ 'settings.optional':'Optional',
1207
+ 'settings.skill.usemain':'Use Main Summarizer',
1208
+ 'settings.viewerport':'Viewer Port',
1209
+ 'settings.viewerport.hint':'Requires restart to take effect',
1210
+ 'settings.save':'Save Settings',
1211
+ 'settings.reset':'Reset',
1212
+ 'settings.saved':'Saved',
1213
+ 'settings.restart.hint':'Some changes require restarting the OpenClaw gateway to take effect.',
1214
+ 'settings.save.fail':'Failed to save settings',
1215
+ 'skills.draft':'Draft',
1216
+ 'skills.filter.active':'Active',
1217
+ 'skills.filter.draft':'Draft',
1218
+ 'skills.filter.archived':'Archived',
1219
+ 'skills.files':'Skill Files',
1220
+ 'skills.content':'SKILL.md Content',
1221
+ 'skills.versions':'Version History',
1222
+ 'skills.related':'Related Tasks',
1223
+ 'skills.download':'\u2B07 Download',
1224
+ 'skills.installed.badge':'Installed',
1225
+ 'skills.empty':'No skills yet. Skills are automatically generated from completed tasks that contain reusable experience.',
1226
+ 'skills.loading':'Loading...',
1227
+ 'skills.error':'Error loading skill',
1228
+ 'skills.error.detail':'Failed to load skill: ',
1229
+ 'skills.nofiles':'No files found',
1230
+ 'skills.noversions':'No versions recorded',
1231
+ 'skills.norelated':'No related tasks',
1232
+ 'skills.nocontent':'No content available',
1233
+ 'skills.nochangelog':'No changelog',
1234
+ 'skills.status.active':'Active',
1235
+ 'skills.status.draft':'Draft',
1236
+ 'skills.status.archived':'Archived',
1237
+ 'skills.updated':'Updated: ',
1238
+ 'skills.task.prefix':'Task: ',
1239
+ 'tasks.chunks.label':'chunks',
1240
+ 'tasks.taskid':'Task ID: ',
1241
+ 'tasks.role.user':'You',
1242
+ 'tasks.role.assistant':'Assistant',
1243
+ 'tasks.error':'Error',
1244
+ 'tasks.error.detail':'Failed to load task details',
1245
+ 'tasks.untitled.related':'Untitled'
1246
+ },
1247
+ zh:{
1248
+ 'title':'OpenClaw 记忆',
1249
+ 'subtitle':'由 MemOS 驱动',
1250
+ 'setup.desc':'设置密码以保护你的记忆数据',
1251
+ 'setup.pw':'输入密码(至少4位)',
1252
+ 'setup.pw2':'确认密码',
1253
+ 'setup.btn':'设置密码并进入',
1254
+ 'setup.err.short':'密码至少需要4个字符',
1255
+ 'setup.err.mismatch':'两次密码不一致',
1256
+ 'setup.err.fail':'设置失败',
1257
+ 'login.desc':'输入密码以访问记忆',
1258
+ 'login.pw':'密码',
1259
+ 'login.btn':'解锁',
1260
+ 'login.err':'密码错误',
1261
+ 'login.forgot':'忘记密码?',
1262
+ 'reset.step1.title':'打开终端',
1263
+ 'reset.step1.desc':'运行以下命令获取重置令牌:',
1264
+ 'reset.step2.title':'找到令牌',
1265
+ 'reset.step2.desc.pre':'在输出中找到 ',
1266
+ 'reset.step2.desc.post':'(纯文本行或 JSON 内)。复制冒号后的32位十六进制字符串。',
1267
+ 'reset.step3.title':'粘贴并重置',
1268
+ 'reset.step3.desc':'将令牌粘贴到下方并设置新密码。',
1269
+ 'reset.token':'在此粘贴重置令牌',
1270
+ 'reset.newpw':'新密码(至少4位)',
1271
+ 'reset.newpw2':'确认新密码',
1272
+ 'reset.btn':'重置密码',
1273
+ 'reset.err.token':'请输入重置令牌',
1274
+ 'reset.err.short':'密码至少需要4个字符',
1275
+ 'reset.err.mismatch':'两次密码不一致',
1276
+ 'reset.err.fail':'重置失败',
1277
+ 'reset.back':'\\u2190 返回登录',
1278
+ 'copy.hint':'点击复制',
1279
+ 'copy.done':'已复制!',
1280
+ 'tab.memories':'\\u{1F4DA} 记忆',
1281
+ 'tab.tasks':'\\u{1F4CB} 任务',
1282
+ 'tab.skills':'\\u{1F9E0} 技能',
1283
+ 'tab.analytics':'\\u{1F4CA} 分析',
1284
+ 'skills.total':'技能总数',
1285
+ 'skills.active':'生效中',
1286
+ 'skills.installed':'已安装',
1287
+ 'tasks.total':'任务总数',
1288
+ 'tasks.active':'进行中',
1289
+ 'tasks.completed':'已完成',
1290
+ 'tasks.status.active':'进行中',
1291
+ 'tasks.status.completed':'已完成',
1292
+ 'tasks.status.skipped':'已跳过',
1293
+ 'tasks.empty':'暂无任务。任务会随着对话自动创建。',
1294
+ 'tasks.loading':'加载中...',
1295
+ 'tasks.untitled':'未命名任务',
1296
+ 'tasks.chunks':'关联记忆',
1297
+ 'tasks.nochunks':'此任务暂无关联记忆。',
1298
+ 'tasks.skipped.default':'对话内容过少,未生成摘要。该任务不会出现在检索结果中。',
1299
+ 'refresh':'\\u21BB 刷新',
1300
+ 'logout':'退出',
1301
+ 'stat.memories':'记忆',
1302
+ 'stat.sessions':'会话',
1303
+ 'stat.embeddings':'嵌入',
1304
+ 'stat.days':'天数',
1305
+ 'stat.active':'活跃',
1306
+ 'stat.deduped':'已去重',
1307
+ 'sidebar.sessions':'会话列表',
1308
+ 'sidebar.allsessions':'全部会话',
1309
+ 'sidebar.clear':'\\u{1F5D1} 清除所有数据',
1310
+ 'search.placeholder':'搜索记忆(支持语义搜索)...',
1311
+ 'search.meta.total':' 条记忆',
1312
+ 'search.meta.semantic':' 语义',
1313
+ 'search.meta.text':' 文本',
1314
+ 'search.meta.results':' 条结果',
1315
+ 'filter.all':'全部',
1316
+ 'filter.allkinds':'所有类型',
1317
+ 'filter.paragraph':'段落',
1318
+ 'filter.code':'代码',
1319
+ 'filter.dialog':'对话',
1320
+ 'filter.list':'列表',
1321
+ 'filter.error':'错误',
1322
+ 'filter.command':'命令',
1323
+ 'filter.newest':'最新优先',
1324
+ 'filter.oldest':'最早优先',
1325
+ 'filter.from':'起始',
1326
+ 'filter.to':'截止',
1327
+ 'filter.clear':'清除',
1328
+ 'empty.text':'暂无记忆',
1329
+ 'card.expand':'展开',
1330
+ 'card.edit':'编辑',
1331
+ 'card.delete':'删除',
1332
+ 'card.evolved':'已演化',
1333
+ 'card.times':'次',
1334
+ 'card.updated':'更新于',
1335
+ 'card.evolveHistory':'演化记录',
1336
+ 'card.oldSummary':'旧摘要',
1337
+ 'card.dedupDuplicate':'重复',
1338
+ 'card.dedupMerged':'已合并',
1339
+ 'card.dedupTarget':'关联: ',
1340
+ 'card.dedupReason':'原因: ',
1341
+ 'card.newSummary':'新摘要',
1342
+ 'pagination.total':' 条',
1343
+ 'range':'范围',
1344
+ 'range.days':'天',
1345
+ 'analytics.total':'总记忆数',
1346
+ 'analytics.writes':'今日写入',
1347
+ 'analytics.calls':'今日查看器调用',
1348
+ 'analytics.sessions':'会话数',
1349
+ 'analytics.embeddings':'嵌入数',
1350
+ 'chart.writes':'每日记忆写入',
1351
+ 'chart.calls':'每日查看器 API 调用(列表 / 搜索)',
1352
+ 'chart.nodata':'此范围内暂无数据',
1353
+ 'chart.nocalls':'此范围内暂无查看器调用',
1354
+ 'chart.toolperf':'工具响应耗时',
1355
+ 'chart.list':'列表',
1356
+ 'chart.search':'搜索',
1357
+ 'breakdown.role':'按角色',
1358
+ 'breakdown.kind':'按类型',
1359
+ 'modal.new':'新建记忆',
1360
+ 'modal.edit':'编辑记忆',
1361
+ 'modal.role':'角色',
1362
+ 'modal.content':'内容',
1363
+ 'modal.content.ph':'记忆内容...',
1364
+ 'modal.summary':'摘要',
1365
+ 'modal.summary.ph':'简要摘要(可选)',
1366
+ 'modal.kind':'类型',
1367
+ 'modal.cancel':'取消',
1368
+ 'modal.create':'创建',
1369
+ 'modal.save':'保存',
1370
+ 'modal.err.empty':'请输入内容',
1371
+ 'toast.created':'记忆已创建',
1372
+ 'toast.updated':'记忆已更新',
1373
+ 'toast.deleted':'记忆已删除',
1374
+ 'toast.opfail':'操作失败',
1375
+ 'toast.delfail':'删除失败',
1376
+ 'toast.cleared':'所有记忆已清除',
1377
+ 'toast.clearfail':'清除失败',
1378
+ 'toast.notfound':'缓存中未找到此记忆',
1379
+ 'confirm.delete':'确定要删除这条记忆吗?',
1380
+ 'confirm.clearall':'确定要删除所有记忆?此操作不可撤销。',
1381
+ 'confirm.clearall2':'你真的确定吗?',
1382
+ 'embed.on':'嵌入模型:',
1383
+ 'embed.off':'无嵌入模型',
1384
+ 'lang.switch':'EN',
1385
+ 'tab.logs':'\u{1F4DD} 日志',
1386
+ 'logs.allTools':'全部工具',
1387
+ 'logs.refresh':'刷新',
1388
+ 'logs.autoRefresh':'自动刷新',
1389
+ 'logs.input':'输入',
1390
+ 'logs.output':'输出',
1391
+ 'logs.empty':'暂无日志。当工具被调用时日志会显示在这里。',
1392
+ 'logs.ago':'前',
1393
+ 'tab.settings':'\u2699 设置',
1394
+ 'settings.embedding':'嵌入模型',
1395
+ 'settings.summarizer':'摘要模型',
1396
+ 'settings.skill':'技能进化',
1397
+ 'settings.general':'通用设置',
1398
+ 'settings.provider':'服务商',
1399
+ 'settings.model':'模型',
1400
+ 'settings.temperature':'温度',
1401
+ 'settings.skill.enabled':'启用技能进化',
1402
+ 'settings.skill.autoinstall':'自动安装技能',
1403
+ 'settings.skill.confidence':'最低置信度',
1404
+ 'settings.skill.minchunks':'最少记忆片段',
1405
+ 'settings.skill.model':'技能专用模型',
1406
+ 'settings.skill.model.hint':'不配置时默认使用上方的摘要模型进行技能生成。如需更高质量的技能输出,可在此单独配置一个更强的模型。',
1407
+ 'settings.optional':'可选',
1408
+ 'settings.skill.usemain':'使用主摘要模型',
1409
+ 'settings.viewerport':'Viewer 端口',
1410
+ 'settings.viewerport.hint':'修改后需重启网关生效',
1411
+ 'settings.save':'保存设置',
1412
+ 'settings.reset':'重置',
1413
+ 'settings.saved':'已保存',
1414
+ 'settings.restart.hint':'部分设置修改后需要重启 OpenClaw 网关才能生效。',
1415
+ 'settings.save.fail':'保存设置失败',
1416
+ 'skills.draft':'草稿',
1417
+ 'skills.filter.active':'生效中',
1418
+ 'skills.filter.draft':'草稿',
1419
+ 'skills.filter.archived':'已归档',
1420
+ 'skills.files':'技能文件',
1421
+ 'skills.content':'SKILL.md 内容',
1422
+ 'skills.versions':'版本历史',
1423
+ 'skills.related':'关联任务',
1424
+ 'skills.download':'\u2B07 下载',
1425
+ 'skills.installed.badge':'已安装',
1426
+ 'skills.empty':'暂无技能。技能会从已完成的、包含可复用经验的任务中自动生成。',
1427
+ 'skills.loading':'加载中...',
1428
+ 'skills.error':'加载技能失败',
1429
+ 'skills.error.detail':'加载技能失败:',
1430
+ 'skills.nofiles':'暂无文件',
1431
+ 'skills.noversions':'暂无版本记录',
1432
+ 'skills.norelated':'暂无关联任务',
1433
+ 'skills.nocontent':'暂无内容',
1434
+ 'skills.nochangelog':'暂无变更记录',
1435
+ 'skills.status.active':'生效中',
1436
+ 'skills.status.draft':'草稿',
1437
+ 'skills.status.archived':'已归档',
1438
+ 'skills.updated':'更新于:',
1439
+ 'skills.task.prefix':'任务:',
1440
+ 'tasks.chunks.label':'条记忆',
1441
+ 'tasks.taskid':'任务 ID:',
1442
+ 'tasks.role.user':'你',
1443
+ 'tasks.role.assistant':'助手',
1444
+ 'tasks.error':'出错了',
1445
+ 'tasks.error.detail':'加载任务详情失败',
1446
+ 'tasks.untitled.related':'未命名'
1447
+ }
1448
+ };
1449
+ const LANG_KEY='memos-viewer-lang';
1450
+ let curLang=localStorage.getItem(LANG_KEY)||(navigator.language.startsWith('zh')?'zh':'en');
1451
+ function t(key){return (I18N[curLang]||I18N.en)[key]||key;}
1452
+ function setLang(lang){curLang=lang;localStorage.setItem(LANG_KEY,lang);applyI18n();}
1453
+ function toggleLang(){setLang(curLang==='zh'?'en':'zh');}
1454
+
1455
+ function applyI18n(){
1456
+ document.querySelectorAll('[data-i18n]').forEach(el=>{
1457
+ const key=el.getAttribute('data-i18n');
1458
+ if(key) el.textContent=t(key);
1459
+ });
1460
+ document.querySelectorAll('[data-i18n-ph]').forEach(el=>{
1461
+ const key=el.getAttribute('data-i18n-ph');
1462
+ if(key) el.placeholder=t(key);
1463
+ });
1464
+ const step2=document.getElementById('resetStep2Desc');
1465
+ 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');
1466
+ document.title=t('title')+' - MemOS';
1467
+ if(typeof loadStats==='function' && document.getElementById('app').style.display==='flex'){loadStats();}
1468
+ if(document.querySelector('.analytics-view.show') && typeof loadMetrics==='function'){loadMetrics();}
1469
+ }
374
1470
 
375
1471
  /* ─── Auth flow ─── */
376
1472
  async function checkAuth(){
@@ -392,12 +1488,12 @@ async function doSetup(){
392
1488
  const pw=document.getElementById('setupPw').value;
393
1489
  const pw2=document.getElementById('setupPw2').value;
394
1490
  const err=document.getElementById('setupErr');
395
- if(pw.length<4){err.textContent='Password must be at least 4 characters';return}
396
- if(pw!==pw2){err.textContent='Passwords do not match';return}
1491
+ if(pw.length<4){err.textContent=t('setup.err.short');return}
1492
+ if(pw!==pw2){err.textContent=t('setup.err.mismatch');return}
397
1493
  const r=await fetch('/api/auth/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});
398
1494
  const d=await r.json();
399
1495
  if(d.ok){document.getElementById('setupScreen').style.display='none';enterApp();}
400
- else{err.textContent=d.error||'Setup failed'}
1496
+ else{err.textContent=d.error||t('setup.err.fail')}
401
1497
  }
402
1498
 
403
1499
  async function doLogin(){
@@ -406,7 +1502,7 @@ async function doLogin(){
406
1502
  const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});
407
1503
  const d=await r.json();
408
1504
  if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}
409
- else{err.textContent='Incorrect password';document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}
1505
+ else{err.textContent=t('login.err');document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}
410
1506
  }
411
1507
 
412
1508
  async function doLogout(){
@@ -430,8 +1526,8 @@ function copyCmd(el){
430
1526
  const code=el.querySelector('code').textContent;
431
1527
  navigator.clipboard.writeText(code).then(()=>{
432
1528
  el.classList.add('copied');
433
- el.querySelector('.copy-hint').textContent='Copied!';
434
- setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent='Click to copy'},2000);
1529
+ el.querySelector('.copy-hint').textContent=t('copy.done');
1530
+ setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent=t('copy.hint')},2000);
435
1531
  });
436
1532
  }
437
1533
 
@@ -440,13 +1536,13 @@ async function doReset(){
440
1536
  const pw=document.getElementById('resetNewPw').value;
441
1537
  const pw2=document.getElementById('resetNewPw2').value;
442
1538
  const err=document.getElementById('resetErr');
443
- if(!token){err.textContent='Please enter the reset token';return}
444
- if(pw.length<4){err.textContent='Password must be at least 4 characters';return}
445
- if(pw!==pw2){err.textContent='Passwords do not match';return}
1539
+ if(!token){err.textContent=t('reset.err.token');return}
1540
+ if(pw.length<4){err.textContent=t('reset.err.short');return}
1541
+ if(pw!==pw2){err.textContent=t('reset.err.mismatch');return}
446
1542
  const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,newPassword:pw})});
447
1543
  const d=await r.json();
448
1544
  if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}
449
- else{err.textContent=d.error||'Reset failed'}
1545
+ else{err.textContent=d.error||t('reset.err.fail')}
450
1546
  }
451
1547
 
452
1548
  function enterApp(){
@@ -454,6 +1550,1003 @@ function enterApp(){
454
1550
  loadAll();
455
1551
  }
456
1552
 
1553
+ function switchView(view){
1554
+ document.querySelectorAll('.nav-tabs .tab').forEach(t=>t.classList.toggle('active',t.dataset.view===view));
1555
+ const feedWrap=document.getElementById('feedWrap');
1556
+ const analyticsView=document.getElementById('analyticsView');
1557
+ const tasksView=document.getElementById('tasksView');
1558
+ const skillsView=document.getElementById('skillsView');
1559
+ const logsView=document.getElementById('logsView');
1560
+ const settingsView=document.getElementById('settingsView');
1561
+ feedWrap.classList.add('hide');
1562
+ analyticsView.classList.remove('show');
1563
+ tasksView.classList.remove('show');
1564
+ skillsView.classList.remove('show');
1565
+ logsView.classList.remove('show');
1566
+ settingsView.classList.remove('show');
1567
+ if(view==='analytics'){
1568
+ analyticsView.classList.add('show');
1569
+ loadMetrics();
1570
+ } else if(view==='tasks'){
1571
+ tasksView.classList.add('show');
1572
+ loadTasks();
1573
+ } else if(view==='skills'){
1574
+ skillsView.classList.add('show');
1575
+ loadSkills();
1576
+ } else if(view==='logs'){
1577
+ logsView.classList.add('show');
1578
+ loadLogs();
1579
+ } else if(view==='settings'){
1580
+ settingsView.classList.add('show');
1581
+ loadConfig();
1582
+ } else {
1583
+ feedWrap.classList.remove('hide');
1584
+ }
1585
+ }
1586
+
1587
+ // ─── Logs ───
1588
+ let logAutoTimer=null;
1589
+ let logPage=1;
1590
+ const LOG_PAGE_SIZE=20;
1591
+ async function loadLogs(page){
1592
+ if(typeof page==='number') logPage=page;
1593
+ try{
1594
+ const toolFilter=document.getElementById('logToolFilter').value;
1595
+ const offset=(logPage-1)*LOG_PAGE_SIZE;
1596
+ const url='/api/logs?limit='+LOG_PAGE_SIZE+'&offset='+offset+(toolFilter?'&tool='+encodeURIComponent(toolFilter):'');
1597
+ const [logsRes,toolsRes]=await Promise.all([fetch(url),fetch('/api/log-tools')]);
1598
+ if(!logsRes.ok) return;
1599
+ const logsData=await logsRes.json();
1600
+ const toolsData=await toolsRes.json();
1601
+ renderLogToolFilter(toolsData.tools||[],toolFilter);
1602
+ renderLogs(logsData.logs||[]);
1603
+ renderLogPagination(logsData.page||1,logsData.totalPages||1,logsData.total||0);
1604
+ startLogAutoRefresh();
1605
+ }catch(e){console.error('loadLogs',e)}
1606
+ }
1607
+ function onLogFilterChange(){logPage=1;loadLogs(1);}
1608
+ function renderLogPagination(page,totalPages,total){
1609
+ const el=document.getElementById('logsPagination');
1610
+ if(!el||totalPages<=1){if(el)el.innerHTML='';return;}
1611
+ const pages=[];
1612
+ const range=2;
1613
+ for(let i=1;i<=totalPages;i++){
1614
+ if(i===1||i===totalPages||Math.abs(i-page)<=range){
1615
+ pages.push(i);
1616
+ }else if(pages[pages.length-1]!=='...'){
1617
+ pages.push('...');
1618
+ }
1619
+ }
1620
+ let html='<div class="logs-pagination">';
1621
+ html+='<button class="btn btn-sm btn-ghost" '+(page<=1?'disabled':'')+' onclick="loadLogs('+(page-1)+')">\u2039</button>';
1622
+ pages.forEach(p=>{
1623
+ if(p==='...'){html+='<span class="page-ellipsis">\u2026</span>';}
1624
+ else{html+='<button class="btn btn-sm '+(p===page?'btn-primary':'btn-ghost')+'" onclick="loadLogs('+p+')">'+p+'</button>';}
1625
+ });
1626
+ html+='<button class="btn btn-sm btn-ghost" '+(page>=totalPages?'disabled':'')+' onclick="loadLogs('+(page+1)+')">\u203A</button>';
1627
+ html+='<span class="page-total">'+total+' total</span>';
1628
+ html+='</div>';
1629
+ el.innerHTML=html;
1630
+ }
1631
+
1632
+ function renderLogToolFilter(tools,current){
1633
+ const sel=document.getElementById('logToolFilter');
1634
+ const opts=['<option value="">'+t('logs.allTools')+'</option>'];
1635
+ tools.forEach(tn=>{
1636
+ opts.push('<option value="'+tn+'"'+(tn===current?' selected':'')+'>'+tn+'</option>');
1637
+ });
1638
+ sel.innerHTML=opts.join('');
1639
+ }
1640
+
1641
+ function formatLogTime(ts){
1642
+ const d=new Date(ts);
1643
+ const time=d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false});
1644
+ const y=d.getFullYear();
1645
+ const m=String(d.getMonth()+1).padStart(2,'0');
1646
+ const day=String(d.getDate()).padStart(2,'0');
1647
+ return y+'-'+m+'-'+day+' '+time;
1648
+ }
1649
+
1650
+ function buildLogSummary(lg){
1651
+ let inputObj=null;
1652
+ try{inputObj=JSON.parse(lg.input);}catch(_){}
1653
+ let html='';
1654
+ const tn=lg.toolName;
1655
+ if(tn==='memory_search'&&inputObj){
1656
+ const q=inputObj.query||'';
1657
+ if(q) html+='<div class="log-summary-query">'+escapeHtml(q.length>200?q.slice(0,200)+'...':q)+'</div>';
1658
+ const outLines=(lg.output||'').split('\\n');
1659
+ const memCount=outLines.filter(l=>l.match(/^\\d+\\.\\s*\\[/)).length;
1660
+ if(memCount>0) html+='<div style="margin-top:4px;font-size:11px;color:var(--text-sec)">\u{1F4CE} '+memCount+' memories retrieved</div>';
1661
+ else if(lg.output&&lg.output.includes('no hits')) html+='<div style="margin-top:4px;font-size:11px;color:var(--text-sec)">\u2205 No matching memories</div>';
1662
+ }else if(tn==='memory_add'&&inputObj){
1663
+ const out=lg.output||'';
1664
+ const statsMatch=out.match(/^([^\\n]+)/);
1665
+ if(statsMatch){
1666
+ html+='<div class="log-summary-stats">';
1667
+ const pairs=statsMatch[1].split(',').map(s=>s.trim());
1668
+ pairs.forEach(p=>{
1669
+ const m=p.match(/^(\\w+)=(\\d+)/);
1670
+ if(m){html+='<span class="log-stat-chip '+m[1]+'">'+m[1]+' '+m[2]+'</span>';}
1671
+ });
1672
+ html+='</div>';
1673
+ }
1674
+ const outLines=out.split('\\n').filter(l=>l.startsWith('['));
1675
+ if(outLines.length>0){
1676
+ html+='<div class="log-msg-list">';
1677
+ outLines.forEach(function(l){
1678
+ var rm=l.match(/^\\[(\\w+)\\]\\s*([^\u2192]+)\u2192\\s*(.*)/);
1679
+ if(rm){
1680
+ var role=rm[1],actionRaw=rm[2].trim(),text=rm[3].trim();
1681
+ var actionCls='stored';
1682
+ if(actionRaw.indexOf('exact-dup')>=0||actionRaw.indexOf('\u23ED')>=0) actionCls='exact-dup';
1683
+ else if(actionRaw.indexOf('dedup')>=0||actionRaw.indexOf('\uD83D\uDD01')>=0) actionCls='dedup';
1684
+ else if(actionRaw.indexOf('merged')>=0||actionRaw.indexOf('\uD83D\uDD00')>=0) actionCls='merged';
1685
+ else if(actionRaw.indexOf('error')>=0||actionRaw.indexOf('\u274C')>=0) actionCls='error';
1686
+ var actionLabel={'stored':'\u2713 stored','exact-dup':'\u23ED skip','dedup':'\uD83D\uDD01 dedup','merged':'\uD83D\uDD00 merged','error':'\u2717 error'}[actionCls]||actionCls;
1687
+ html+='<div class="log-msg-item">'+
1688
+ '<span class="log-msg-role '+role+'">'+role+'</span>'+
1689
+ '<span class="log-msg-action '+actionCls+'">'+actionLabel+'</span>'+
1690
+ '<span class="log-msg-text">'+escapeHtml(text.length>150?text.slice(0,150)+'...':text)+'</span>'+
1691
+ '</div>';
1692
+ }else{
1693
+ html+='<div class="log-msg-item"><span class="log-msg-text">'+escapeHtml(l.length>200?l.slice(0,200)+'...':l)+'</span></div>';
1694
+ }
1695
+ });
1696
+ html+='</div>';
1697
+ }else if(inputObj.details&&Array.isArray(inputObj.details)&&inputObj.details.length>0){
1698
+ html+='<div class="log-msg-list">';
1699
+ inputObj.details.forEach(function(d){
1700
+ var s=typeof d==='string'?d:String(d);
1701
+ var dm=s.match(/^\\[(\\w+)\\]\\s*(.*)/);
1702
+ if(dm){
1703
+ html+='<div class="log-msg-item"><span class="log-msg-role '+dm[1]+'">'+dm[1]+'</span><span class="log-msg-text">'+escapeHtml(dm[2].length>150?dm[2].slice(0,150)+'...':dm[2])+'</span></div>';
1704
+ }else{
1705
+ html+='<div class="log-msg-item"><span class="log-msg-text">'+escapeHtml(s.length>150?s.slice(0,150)+'...':s)+'</span></div>';
1706
+ }
1707
+ });
1708
+ html+='</div>';
1709
+ }
1710
+ }else if(inputObj){
1711
+ const keys=Object.keys(inputObj);
1712
+ keys.slice(0,4).forEach(k=>{
1713
+ const v=String(inputObj[k]);
1714
+ html+='<span class="log-summary-kv"><span class="kv-label">'+escapeHtml(k)+':</span><span class="kv-val">'+escapeHtml(v.length>60?v.slice(0,60)+'...':v)+'</span></span>';
1715
+ });
1716
+ }
1717
+ return html;
1718
+ }
1719
+ function renderLogs(logs){
1720
+ const el=document.getElementById('logsList');
1721
+ if(!logs.length){
1722
+ el.innerHTML='<div style="text-align:center;padding:60px 20px;color:var(--text-sec)">'+
1723
+ '<div style="font-size:32px;margin-bottom:12px;opacity:.5">\u{1F4CB}</div>'+
1724
+ '<div style="font-size:13px">'+t('logs.empty')+'</div></div>';
1725
+ return;
1726
+ }
1727
+ el.innerHTML=logs.map((lg,i)=>{
1728
+ const toolCls=lg.toolName.replace(/[^a-zA-Z0-9_]/g,'_');
1729
+ const dur=lg.durationMs<1000?Math.round(lg.durationMs)+'ms':(lg.durationMs/1000).toFixed(1)+'s';
1730
+ let inputDisplay='';
1731
+ try{const parsed=JSON.parse(lg.input);inputDisplay=JSON.stringify(parsed,null,2);}catch(_){inputDisplay=lg.input;}
1732
+ const summary=buildLogSummary(lg);
1733
+ return '<div class="log-entry" id="log-'+i+'">'+
1734
+ '<div class="log-header" onclick="toggleLog('+i+')">'+
1735
+ '<span class="log-status '+(lg.success?'ok':'fail')+'"></span>'+
1736
+ '<span class="log-tool-badge '+toolCls+'">'+lg.toolName+'</span>'+
1737
+ '<span class="log-dur">'+dur+'</span>'+
1738
+ '<span class="log-expand-btn" style="margin-left:4px">\u25BC</span>'+
1739
+ '<span class="log-time">'+formatLogTime(lg.calledAt)+'</span>'+
1740
+ '</div>'+
1741
+ (summary?'<div class="log-summary">'+summary+'</div>':'')+
1742
+ '<div class="log-detail" id="log-detail-'+i+'">'+
1743
+ '<div class="log-io-section">'+
1744
+ '<div class="log-io-label">\u25B6 '+t('logs.input')+'</div>'+
1745
+ '<pre class="log-io-content">'+escapeHtml(inputDisplay)+'</pre>'+
1746
+ '</div>'+
1747
+ '<div class="log-io-section">'+
1748
+ '<div class="log-io-label">\u25C0 '+t('logs.output')+'</div>'+
1749
+ '<pre class="log-io-content">'+escapeHtml(lg.output)+'</pre>'+
1750
+ '</div>'+
1751
+ '</div>'+
1752
+ '</div>';
1753
+ }).join('');
1754
+ }
1755
+
1756
+ function toggleLog(i){
1757
+ const entry=document.getElementById('log-'+i);
1758
+ const d=document.getElementById('log-detail-'+i);
1759
+ if(d) d.classList.toggle('open');
1760
+ if(entry) entry.classList.toggle('expanded');
1761
+ }
1762
+
1763
+ function startLogAutoRefresh(){
1764
+ if(logAutoTimer) clearInterval(logAutoTimer);
1765
+ logAutoTimer=setInterval(()=>{
1766
+ const cb=document.getElementById('logAutoRefresh');
1767
+ const logsView=document.getElementById('logsView');
1768
+ if(cb&&cb.checked&&logsView&&logsView.classList.contains('show')){
1769
+ loadLogs();
1770
+ }
1771
+ },5000);
1772
+ }
1773
+
1774
+ function escapeHtml(s){
1775
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1776
+ }
1777
+
1778
+ function setMetricsDays(d){
1779
+ metricsDays=d;
1780
+ document.querySelectorAll('.metrics-toolbar .range-btn').forEach(btn=>btn.classList.toggle('active',Number(btn.dataset.days)===d));
1781
+ loadMetrics();
1782
+ }
1783
+
1784
+ async function loadMetrics(){
1785
+ const r=await fetch('/api/metrics?days='+metricsDays);
1786
+ const d=await r.json();
1787
+ document.getElementById('mTotal').textContent=formatNum(d.totals.memories);
1788
+ document.getElementById('mTodayWrites').textContent=formatNum(d.totals.todayWrites);
1789
+ document.getElementById('mSessions').textContent=formatNum(d.totals.sessions);
1790
+ document.getElementById('mEmbeddings').textContent=formatNum(d.totals.embeddings);
1791
+ renderChartWrites(d.writesPerDay);
1792
+ renderBreakdown(d.roleBreakdown,'breakdownRole');
1793
+ renderBreakdown(d.kindBreakdown,'breakdownKind');
1794
+ loadToolMetrics();
1795
+ }
1796
+
1797
+ function formatNum(n){return n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'k':String(n);}
1798
+
1799
+ /* ─── Tasks View Logic ─── */
1800
+ let tasksStatusFilter='';
1801
+ let tasksPage=0;
1802
+ const TASKS_PER_PAGE=20;
1803
+
1804
+ function setTaskStatusFilter(btn,status){
1805
+ document.querySelectorAll('.tasks-filters .filter-chip').forEach(c=>c.classList.remove('active'));
1806
+ btn.classList.add('active');
1807
+ tasksStatusFilter=status;
1808
+ tasksPage=0;
1809
+ loadTasks();
1810
+ }
1811
+
1812
+ async function loadTasks(){
1813
+ const list=document.getElementById('tasksList');
1814
+ list.innerHTML='<div class="spinner"></div>';
1815
+ try{
1816
+ const params=new URLSearchParams({limit:String(TASKS_PER_PAGE),offset:String(tasksPage*TASKS_PER_PAGE)});
1817
+ if(tasksStatusFilter) params.set('status',tasksStatusFilter);
1818
+ const r=await fetch('/api/tasks?'+params);
1819
+ const data=await r.json();
1820
+
1821
+ // stats
1822
+ const allR=await fetch('/api/tasks?limit=1&offset=0');
1823
+ const allD=await allR.json();
1824
+ document.getElementById('tasksTotalCount').textContent=formatNum(allD.total);
1825
+
1826
+ const activeR=await fetch('/api/tasks?status=active&limit=1&offset=0');
1827
+ const activeD=await activeR.json();
1828
+ document.getElementById('tasksActiveCount').textContent=formatNum(activeD.total);
1829
+
1830
+ const compR=await fetch('/api/tasks?status=completed&limit=1&offset=0');
1831
+ const compD=await compR.json();
1832
+ document.getElementById('tasksCompletedCount').textContent=formatNum(compD.total);
1833
+
1834
+ const skipR=await fetch('/api/tasks?status=skipped&limit=1&offset=0');
1835
+ const skipD=await skipR.json();
1836
+ document.getElementById('tasksSkippedCount').textContent=formatNum(skipD.total);
1837
+
1838
+ if(!data.tasks||data.tasks.length===0){
1839
+ list.innerHTML='<div style="text-align:center;padding:48px;color:var(--text-muted);font-size:14px" data-i18n="tasks.empty">'+t('tasks.empty')+'</div>';
1840
+ document.getElementById('tasksPagination').innerHTML='';
1841
+ return;
1842
+ }
1843
+
1844
+ list.innerHTML=data.tasks.map(task=>{
1845
+ const timeStr=formatTime(task.startedAt);
1846
+ const endStr=task.endedAt?formatTime(task.endedAt):'';
1847
+ const durationStr=task.endedAt?formatDuration(task.endedAt-task.startedAt):'';
1848
+ return '<div class="task-card status-'+task.status+'" onclick="openTaskDetail(\\''+task.id+'\\')">'+
1849
+ '<div class="task-card-top">'+
1850
+ '<div class="task-card-title">'+esc(task.title)+'</div>'+
1851
+ '<span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span>'+
1852
+ '</div>'+
1853
+ (task.summary?'<div class="task-card-summary'+(task.status==='skipped'?' skipped-reason':'')+'">'+esc(task.summary)+'</div>':'')+
1854
+ '<div class="task-card-bottom">'+
1855
+ '<span class="tag"><span class="icon">\\u{1F4C5}</span> '+timeStr+'</span>'+
1856
+ (durationStr?'<span class="tag"><span class="icon">\\u23F1</span> '+durationStr+'</span>':'')+
1857
+ '<span class="tag"><span class="icon">\\u{1F4DD}</span> '+task.chunkCount+' '+t('tasks.chunks.label')+'</span>'+
1858
+ '<span class="tag"><span class="icon">\\u{1F4C2}</span> '+(task.sessionKey||'').slice(0,12)+'</span>'+
1859
+ '</div>'+
1860
+ '</div>';
1861
+ }).join('');
1862
+
1863
+ renderTasksPagination(data.total);
1864
+ }catch(e){
1865
+ console.error('loadTasks error:',e);
1866
+ list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">Failed to load tasks: '+String(e)+'</div>';
1867
+ }
1868
+ }
1869
+
1870
+ function renderTasksPagination(total){
1871
+ const el=document.getElementById('tasksPagination');
1872
+ const pages=Math.ceil(total/TASKS_PER_PAGE);
1873
+ if(pages<=1){el.innerHTML='';return;}
1874
+ let html='<button class="pg-btn'+(tasksPage===0?' disabled':'')+'" onclick="tasksPage=Math.max(0,tasksPage-1);loadTasks()">\\u2190</button>';
1875
+ const start=Math.max(0,tasksPage-2),end=Math.min(pages,tasksPage+3);
1876
+ for(let i=start;i<end;i++){
1877
+ html+='<button class="pg-btn'+(i===tasksPage?' active':'')+'" onclick="tasksPage='+i+';loadTasks()">'+(i+1)+'</button>';
1878
+ }
1879
+ html+='<button class="pg-btn'+(tasksPage>=pages-1?' disabled':'')+'" onclick="tasksPage=Math.min('+(pages-1)+',tasksPage+1);loadTasks()">\\u2192</button>';
1880
+ html+='<span class="pg-info">'+total+' '+t('pagination.total')+'</span>';
1881
+ el.innerHTML=html;
1882
+ }
1883
+
1884
+ async function openTaskDetail(taskId){
1885
+ const overlay=document.getElementById('taskDetailOverlay');
1886
+ overlay.classList.add('show');
1887
+ document.getElementById('taskDetailTitle').textContent=t('tasks.loading');
1888
+ document.getElementById('taskDetailMeta').innerHTML='';
1889
+ document.getElementById('taskSkillSection').innerHTML='';
1890
+ document.getElementById('taskSkillSection').className='task-skill-section';
1891
+ document.getElementById('taskDetailSummary').textContent='';
1892
+ document.getElementById('taskDetailChunks').innerHTML='<div class="spinner"></div>';
1893
+
1894
+ try{
1895
+ const r=await fetch('/api/task/'+taskId);
1896
+ const task=await r.json();
1897
+
1898
+ document.getElementById('taskDetailTitle').textContent=task.title||t('tasks.untitled');
1899
+
1900
+ const meta=[
1901
+ '<span class="meta-item"><span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span></span>',
1902
+ '<span class="meta-item">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>',
1903
+ ];
1904
+ if(task.endedAt) meta.push('<span class="meta-item">\\u2192 '+formatTime(task.endedAt)+'</span>');
1905
+ meta.push('<span class="meta-item">\\u{1F4C2} '+task.sessionKey+'</span>');
1906
+ meta.push('<span class="meta-item">\\u{1F4DD} '+task.chunks.length+' '+t('tasks.chunks.label')+'</span>');
1907
+ meta.push('<div style="width:100%;margin-top:4px"><span class="meta-item" style="width:100%">'+t('tasks.taskid')+'<span class="task-id-full">'+esc(task.id)+'</span></span></div>');
1908
+ document.getElementById('taskDetailMeta').innerHTML=meta.join('');
1909
+
1910
+ // ── Skill status section ──
1911
+ renderTaskSkillSection(task);
1912
+
1913
+ var summaryEl=document.getElementById('taskDetailSummary');
1914
+ if(task.status==='skipped'){
1915
+ 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>';
1916
+ }else{
1917
+ summaryEl.innerHTML=renderSummaryHtml(task.summary);
1918
+ }
1919
+
1920
+ if(task.chunks.length===0){
1921
+ document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--text-muted);padding:12px;font-size:13px">'+t('tasks.nochunks')+'</div>';
1922
+ }else{
1923
+ document.getElementById('taskDetailChunks').innerHTML=task.chunks.map(c=>{
1924
+ var roleLabel=c.role==='user'?t('tasks.role.user'):c.role==='assistant'?t('tasks.role.assistant'):c.role.toUpperCase();
1925
+ return '<div class="task-chunk-item role-'+c.role+'">'+
1926
+ '<div class="task-chunk-role '+c.role+'">'+roleLabel+'</div>'+
1927
+ '<div class="task-chunk-bubble" onclick="this.classList.toggle(\\\'expanded\\\')">'+esc(c.content)+'</div>'+
1928
+ '<div class="task-chunk-time">'+formatTime(c.createdAt)+'</div>'+
1929
+ '</div>';
1930
+ }).join('');
1931
+ }
1932
+ }catch(e){
1933
+ document.getElementById('taskDetailTitle').textContent=t('tasks.error');
1934
+ document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--rose)">'+t('tasks.error.detail')+'</div>';
1935
+ }
1936
+ }
1937
+
1938
+ function renderTaskSkillSection(task){
1939
+ const section=document.getElementById('taskSkillSection');
1940
+ const ss=task.skillStatus;
1941
+ const links=task.skillLinks||[];
1942
+
1943
+ if(links.length>0){
1944
+ section.className='task-skill-section status-generated';
1945
+ var html='<div class="skill-status-header">\\u{1F527} \u5DF2\u751F\u6210\u6280\u80FD</div>';
1946
+ html+=links.map(function(lk){
1947
+ var relLabel={'generated_from':'\u7531\u6B64\u4EFB\u52A1\u751F\u6210','evolved_from':'\u7531\u6B64\u4EFB\u52A1\u5347\u7EA7','applied_to':'\u5173\u8054\u4F7F\u7528'}[lk.relation]||lk.relation;
1948
+ var statusLabel={'active':'\u6D3B\u8DC3','draft':'\u8349\u7A3F','archived':'\u5DF2\u5F52\u6863'}[lk.status]||lk.status;
1949
+ return '<div class="skill-link-card" onclick="event.stopPropagation();closeTaskDetail();switchView(\\'skills\\');setTimeout(function(){openSkillDetail(\\''+lk.skillId+'\\')},300)">'+
1950
+ '<div class="skill-link-name">'+esc(lk.skillName)+' <span style="font-size:11px;color:var(--text-sec)">('+relLabel+', v'+lk.versionAt+')</span></div>'+
1951
+ '<div class="skill-link-meta">'+
1952
+ '\u72B6\u6001: <span class="task-status-badge '+(lk.status||'active')+'">'+statusLabel+'</span>'+
1953
+ (lk.qualityScore!=null?' &middot; \u8D28\u91CF\u5206: '+lk.qualityScore+'/10':'')+
1954
+ '</div>'+
1955
+ '<div style="margin-top:4px"><span class="task-id-full">Skill ID: '+esc(lk.skillId)+'</span></div>'+
1956
+ '</div>';
1957
+ }).join('');
1958
+ section.innerHTML=html;
1959
+ }else if(ss==='generating'){
1960
+ section.className='task-skill-section status-generating';
1961
+ section.innerHTML='<div class="skill-status-header">\\u23F3 \u6280\u80FD\u751F\u6210\u4E2D...</div>'+
1962
+ '<div class="skill-status-reason">'+esc(task.skillReason||'')+'</div>';
1963
+ }else if(ss==='not_generated'){
1964
+ section.className='task-skill-section status-not_generated';
1965
+ section.innerHTML='<div class="skill-status-header">\\u274C \u672A\u751F\u6210\u6280\u80FD</div>'+
1966
+ '<div class="skill-status-reason">\u539F\u56E0\uFF1A'+esc(task.skillReason||'\u7ECF LLM \u8BC4\u4F30\uFF0C\u8BE5\u4EFB\u52A1\u4E0D\u9002\u5408\u63D0\u70BC\u4E3A\u53EF\u590D\u7528\u6280\u80FD\u3002')+'</div>';
1967
+ }else if(ss==='skipped'){
1968
+ section.className='task-skill-section status-skipped';
1969
+ section.innerHTML='<div class="skill-status-header">\\u23ED \u8DF3\u8FC7\u6280\u80FD\u8BC4\u4F30</div>'+
1970
+ '<div class="skill-status-reason">\u539F\u56E0\uFF1A'+esc(task.skillReason||'')+'</div>';
1971
+ }else if(task.status==='active'){
1972
+ section.className='task-skill-section status-skipped';
1973
+ section.innerHTML='<div class="skill-status-header">\\u23F8 \u4EFB\u52A1\u8FDB\u884C\u4E2D</div>'+
1974
+ '<div class="skill-status-reason">\u6280\u80FD\u8BC4\u4F30\u5728\u4EFB\u52A1\u5B8C\u6210\u540E\u81EA\u52A8\u8FD0\u884C\u3002</div>';
1975
+ }else{
1976
+ section.className='task-skill-section status-skipped';
1977
+ section.innerHTML='<div class="skill-status-header">\\u2014 \u65E0\u6280\u80FD\u4FE1\u606F</div>'+
1978
+ '<div class="skill-status-reason">\u8BE5\u4EFB\u52A1\u5728\u6280\u80FD\u8FDB\u5316\u7CFB\u7EDF\u542F\u7528\u4E4B\u524D\u5B8C\u6210\uFF0C\u65E0\u6280\u80FD\u8BC4\u4F30\u8BB0\u5F55\u3002</div>';
1979
+ }
1980
+ }
1981
+
1982
+ function closeTaskDetail(event){
1983
+ if(event && event.target!==document.getElementById('taskDetailOverlay')) return;
1984
+ document.getElementById('taskDetailOverlay').classList.remove('show');
1985
+ }
1986
+
1987
+ /* ─── Skills View Logic ─── */
1988
+ let skillsStatusFilter='';
1989
+
1990
+ function setSkillStatusFilter(btn,status){
1991
+ document.querySelectorAll('.skills-view .tasks-filters .filter-chip').forEach(c=>c.classList.remove('active'));
1992
+ btn.classList.add('active');
1993
+ skillsStatusFilter=status;
1994
+ loadSkills();
1995
+ }
1996
+
1997
+ async function loadSkills(){
1998
+ const list=document.getElementById('skillsList');
1999
+ list.innerHTML='<div class="spinner"></div>';
2000
+ try{
2001
+ const params=new URLSearchParams();
2002
+ if(skillsStatusFilter) params.set('status',skillsStatusFilter);
2003
+ const r=await fetch('/api/skills?'+params);
2004
+ const data=await r.json();
2005
+
2006
+ document.getElementById('skillsTotalCount').textContent=formatNum(data.skills.length);
2007
+ document.getElementById('skillsActiveCount').textContent=formatNum(data.skills.filter(s=>s.status==='active').length);
2008
+ document.getElementById('skillsDraftCount').textContent=formatNum(data.skills.filter(s=>s.status==='draft').length);
2009
+ document.getElementById('skillsInstalledCount').textContent=formatNum(data.skills.filter(s=>s.installed).length);
2010
+
2011
+ if(!data.skills||data.skills.length===0){
2012
+ list.innerHTML='<div style="text-align:center;padding:48px;color:var(--text-muted);font-size:14px">'+t('skills.empty')+'</div>';
2013
+ return;
2014
+ }
2015
+
2016
+ list.innerHTML=data.skills.map(skill=>{
2017
+ const timeStr=formatTime(skill.createdAt);
2018
+ const tags=parseTags(skill.tags);
2019
+ const installedClass=skill.installed?'installed':'';
2020
+ const statusClass=skill.status==='archived'?'archived':(skill.status==='draft'?'draft':'');
2021
+ const qs=skill.qualityScore;
2022
+ const qsBadge=qs!==null&&qs!==undefined?'<span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+qs.toFixed(1)+'</span>':'';
2023
+ return '<div class="skill-card '+installedClass+' '+statusClass+'" onclick="openSkillDetail(\\''+skill.id+'\\')">'+
2024
+ '<div class="skill-card-top">'+
2025
+ '<div class="skill-card-name">\\u{1F9E0} '+esc(skill.name)+'</div>'+
2026
+ '<div class="skill-card-badges">'+
2027
+ qsBadge+
2028
+ '<span class="skill-badge version">v'+skill.version+'</span>'+
2029
+ (skill.installed?'<span class="skill-badge installed">'+t('skills.installed.badge')+'</span>':'')+
2030
+ '<span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span>'+
2031
+ '</div>'+
2032
+ '</div>'+
2033
+ '<div class="skill-card-desc">'+esc(skill.description)+'</div>'+
2034
+ '<div class="skill-card-bottom">'+
2035
+ '<span class="tag"><span class="icon">\\u{1F4C5}</span> '+timeStr+'</span>'+
2036
+ '<span class="tag"><span class="icon">\\u{1F4E6}</span> '+skill.sourceType+'</span>'+
2037
+ (tags.length>0?'<div class="skill-card-tags">'+tags.map(t=>'<span class="skill-tag">'+esc(t)+'</span>').join('')+'</div>':'')+
2038
+ '</div>'+
2039
+ '</div>';
2040
+ }).join('');
2041
+ }catch(e){
2042
+ list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">Failed to load skills: '+esc(String(e))+'</div>';
2043
+ }
2044
+ }
2045
+
2046
+ function parseTags(tagsStr){
2047
+ try{ const arr=JSON.parse(tagsStr||'[]'); return Array.isArray(arr)?arr:[]; }catch{ return []; }
2048
+ }
2049
+
2050
+ let currentSkillId='';
2051
+
2052
+ async function openSkillDetail(skillId){
2053
+ currentSkillId=skillId;
2054
+ const overlay=document.getElementById('skillDetailOverlay');
2055
+ overlay.classList.add('show');
2056
+ document.getElementById('skillDetailTitle').textContent=t('skills.loading');
2057
+ document.getElementById('skillDetailMeta').innerHTML='';
2058
+ document.getElementById('skillDetailDesc').textContent='';
2059
+ document.getElementById('skillFilesList').innerHTML='';
2060
+ document.getElementById('skillDetailContent').innerHTML='<div class="spinner"></div>';
2061
+ document.getElementById('skillVersionsList').innerHTML='<div class="spinner"></div>';
2062
+ document.getElementById('skillRelatedTasks').innerHTML='';
2063
+
2064
+ try{
2065
+ const r=await fetch('/api/skill/'+skillId);
2066
+ if(!r.ok){
2067
+ const errText=await r.text();
2068
+ throw new Error('API '+r.status+': '+errText);
2069
+ }
2070
+ const data=await r.json();
2071
+ if(!data.skill){
2072
+ throw new Error('No skill data in response: '+JSON.stringify(data).slice(0,200));
2073
+ }
2074
+ const skill=data.skill;
2075
+ const versions=data.versions||[];
2076
+ const relatedTasks=data.relatedTasks||[];
2077
+ const files=data.files||[];
2078
+
2079
+ document.getElementById('skillDetailTitle').textContent='\\u{1F9E0} '+skill.name;
2080
+
2081
+ const qs=skill.qualityScore;
2082
+ const qsBadge=qs!==null&&qs!==undefined?'<span class="meta-item"><span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+qs.toFixed(1)+'/10</span></span>':'';
2083
+ document.getElementById('skillDetailMeta').innerHTML=[
2084
+ '<span class="meta-item"><span class="skill-badge version">v'+skill.version+'</span></span>',
2085
+ '<span class="meta-item"><span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span></span>',
2086
+ qsBadge,
2087
+ skill.installed?'<span class="meta-item"><span class="skill-badge installed">'+t('skills.installed.badge')+'</span></span>':'',
2088
+ '<span class="meta-item">\\u{1F4C5} '+formatTime(skill.createdAt)+'</span>',
2089
+ '<span class="meta-item">\\u270F '+t('skills.updated')+formatTime(skill.updatedAt)+'</span>',
2090
+ ].filter(Boolean).join('');
2091
+
2092
+ document.getElementById('skillDetailDesc').textContent=skill.description;
2093
+
2094
+ if(files.length>0){
2095
+ const fileIcons={'skill':'\\u{1F4D6}','script':'\\u{2699}','reference':'\\u{1F4CE}','file':'\\u{1F4C4}'};
2096
+ document.getElementById('skillFilesList').innerHTML=files.map(f=>
2097
+ '<div class="skill-file-item">'+
2098
+ '<span class="skill-file-icon">'+(fileIcons[f.type]||'\\u{1F4C4}')+'</span>'+
2099
+ '<span class="skill-file-name">'+esc(f.path)+'</span>'+
2100
+ '<span class="skill-file-type">'+f.type+'</span>'+
2101
+ '<span class="skill-file-size">'+(f.size>1024?(f.size/1024).toFixed(1)+'KB':f.size+'B')+'</span>'+
2102
+ '</div>'
2103
+ ).join('');
2104
+ } else {
2105
+ document.getElementById('skillFilesList').innerHTML='<div style="color:var(--text-muted);font-size:12px">'+t('skills.nofiles')+'</div>';
2106
+ }
2107
+
2108
+ const latestVersion=versions[0];
2109
+ document.getElementById('skillContentTitle').textContent=latestVersion?'SKILL.md (v'+latestVersion.version+')':t('skills.content');
2110
+ document.getElementById('skillDetailContent').innerHTML=latestVersion?renderSkillMarkdown(latestVersion.content):'<span style="color:var(--text-muted)">'+t('skills.nocontent')+'</span>';
2111
+
2112
+ if(versions.length===0){
2113
+ document.getElementById('skillVersionsList').innerHTML='<div style="color:var(--text-muted);font-size:13px">'+t('skills.noversions')+'</div>';
2114
+ } else {
2115
+ document.getElementById('skillVersionsList').innerHTML=versions.map(v=>{
2116
+ const vqs=v.qualityScore;
2117
+ const vqsBadge=vqs!==null&&vqs!==undefined?'<span class="skill-badge quality '+(vqs>=7?'high':vqs>=5?'mid':'low')+'">\\u2605 '+vqs.toFixed(1)+'</span>':'';
2118
+ const summaryHtml=v.changeSummary?'<div class="skill-version-summary">'+esc(v.changeSummary)+'</div>':'';
2119
+ return '<div class="skill-version-item">'+
2120
+ '<div class="skill-version-header">'+
2121
+ '<span class="skill-version-badge">v'+v.version+'</span>'+
2122
+ '<span class="skill-version-type">'+v.upgradeType+'</span>'+
2123
+ vqsBadge+
2124
+ '</div>'+
2125
+ '<div class="skill-version-changelog">'+esc(v.changelog||t('skills.nochangelog'))+'</div>'+
2126
+ summaryHtml+
2127
+ '<div class="skill-version-time">'+formatTime(v.createdAt)+(v.sourceTaskId?' \\u2022 '+t('skills.task.prefix')+v.sourceTaskId.slice(0,8)+'...':'')+'</div>'+
2128
+ '</div>';
2129
+ }).join('');
2130
+ }
2131
+
2132
+ if(relatedTasks.length===0){
2133
+ document.getElementById('skillRelatedTasks').innerHTML='<div style="color:var(--text-muted);font-size:13px">'+t('skills.norelated')+'</div>';
2134
+ } else {
2135
+ document.getElementById('skillRelatedTasks').innerHTML=relatedTasks.map(rt=>
2136
+ '<div class="skill-related-task" onclick="event.stopPropagation();closeSkillDetail();switchView(\\'tasks\\');setTimeout(()=>openTaskDetail(\\''+rt.task.id+'\\'),300)">'+
2137
+ '<span class="relation">'+rt.relation+'</span>'+
2138
+ '<span class="task-title">'+esc(rt.task.title||t('tasks.untitled.related'))+'</span>'+
2139
+ '<span style="font-size:11px;color:var(--text-muted)">'+formatTime(rt.task.startedAt)+'</span>'+
2140
+ '</div>'
2141
+ ).join('');
2142
+ }
2143
+
2144
+ }catch(e){
2145
+ document.getElementById('skillDetailTitle').textContent=t('skills.error');
2146
+ document.getElementById('skillDetailContent').innerHTML='<div style="color:var(--rose);padding:16px">'+t('skills.error.detail')+esc(String(e))+'</div>';
2147
+ document.getElementById('skillFilesList').innerHTML='';
2148
+ document.getElementById('skillVersionsList').innerHTML='';
2149
+ document.getElementById('skillRelatedTasks').innerHTML='';
2150
+ }
2151
+ }
2152
+
2153
+ function downloadSkill(){
2154
+ if(!currentSkillId) return;
2155
+ window.open('/api/skill/'+currentSkillId+'/download','_blank');
2156
+ }
2157
+
2158
+ /* ─── Settings / Config ─── */
2159
+ async function loadConfig(){
2160
+ try{
2161
+ const r=await fetch('/api/config');
2162
+ if(!r.ok) return;
2163
+ const cfg=await r.json();
2164
+ const emb=cfg.embedding||{};
2165
+ document.getElementById('cfgEmbProvider').value=emb.provider||'openai_compatible';
2166
+ document.getElementById('cfgEmbModel').value=emb.model||'';
2167
+ document.getElementById('cfgEmbEndpoint').value=emb.endpoint||'';
2168
+ document.getElementById('cfgEmbApiKey').value=emb.apiKey||'';
2169
+
2170
+ const sum=cfg.summarizer||{};
2171
+ document.getElementById('cfgSumProvider').value=sum.provider||'openai_compatible';
2172
+ document.getElementById('cfgSumModel').value=sum.model||'';
2173
+ document.getElementById('cfgSumEndpoint').value=sum.endpoint||'';
2174
+ document.getElementById('cfgSumApiKey').value=sum.apiKey||'';
2175
+ document.getElementById('cfgSumTemp').value=sum.temperature!=null?sum.temperature:'';
2176
+
2177
+ const sk=cfg.skillEvolution||{};
2178
+ document.getElementById('cfgSkillEnabled').checked=sk.enabled!==false;
2179
+ document.getElementById('cfgSkillAutoInstall').checked=!!sk.autoInstall;
2180
+ document.getElementById('cfgSkillConfidence').value=sk.minConfidence||'';
2181
+ document.getElementById('cfgSkillMinChunks').value=sk.minChunksForEval||'';
2182
+
2183
+ const skSum=sk.summarizer||{};
2184
+ document.getElementById('cfgSkillProviderDefault').textContent='\u2014 '+t('settings.skill.usemain');
2185
+ document.getElementById('cfgSkillProvider').value=skSum.provider||'';
2186
+ document.getElementById('cfgSkillModel').value=skSum.model||'';
2187
+ document.getElementById('cfgSkillEndpoint').value=skSum.endpoint||'';
2188
+ document.getElementById('cfgSkillApiKey').value=skSum.apiKey||'';
2189
+ document.getElementById('cfgSkillTemp').value=skSum.temperature!=null?skSum.temperature:'';
2190
+
2191
+ document.getElementById('cfgViewerPort').value=cfg.viewerPort||'';
2192
+ }catch(e){
2193
+ console.error('loadConfig error',e);
2194
+ }
2195
+ }
2196
+
2197
+ async function saveConfig(){
2198
+ const cfg={};
2199
+ const embP=document.getElementById('cfgEmbProvider').value;
2200
+ if(embP){
2201
+ cfg.embedding={provider:embP};
2202
+ const v=document.getElementById('cfgEmbModel').value.trim();if(v) cfg.embedding.model=v;
2203
+ const e=document.getElementById('cfgEmbEndpoint').value.trim();if(e) cfg.embedding.endpoint=e;
2204
+ const k=document.getElementById('cfgEmbApiKey').value.trim();if(k) cfg.embedding.apiKey=k;
2205
+ }
2206
+ const sumP=document.getElementById('cfgSumProvider').value;
2207
+ if(sumP){
2208
+ cfg.summarizer={provider:sumP};
2209
+ const v=document.getElementById('cfgSumModel').value.trim();if(v) cfg.summarizer.model=v;
2210
+ const e=document.getElementById('cfgSumEndpoint').value.trim();if(e) cfg.summarizer.endpoint=e;
2211
+ const k=document.getElementById('cfgSumApiKey').value.trim();if(k) cfg.summarizer.apiKey=k;
2212
+ const tp=document.getElementById('cfgSumTemp').value.trim();if(tp!=='') cfg.summarizer.temperature=Number(tp);
2213
+ }
2214
+ cfg.skillEvolution={
2215
+ enabled:document.getElementById('cfgSkillEnabled').checked,
2216
+ autoInstall:document.getElementById('cfgSkillAutoInstall').checked
2217
+ };
2218
+ const mc=document.getElementById('cfgSkillConfidence').value.trim();if(mc) cfg.skillEvolution.minConfidence=Number(mc);
2219
+ const mk=document.getElementById('cfgSkillMinChunks').value.trim();if(mk) cfg.skillEvolution.minChunksForEval=Number(mk);
2220
+
2221
+ const skP=document.getElementById('cfgSkillProvider').value;
2222
+ if(skP){
2223
+ cfg.skillEvolution.summarizer={provider:skP};
2224
+ const v=document.getElementById('cfgSkillModel').value.trim();if(v) cfg.skillEvolution.summarizer.model=v;
2225
+ const e=document.getElementById('cfgSkillEndpoint').value.trim();if(e) cfg.skillEvolution.summarizer.endpoint=e;
2226
+ const k=document.getElementById('cfgSkillApiKey').value.trim();if(k) cfg.skillEvolution.summarizer.apiKey=k;
2227
+ const tp=document.getElementById('cfgSkillTemp').value.trim();if(tp!=='') cfg.skillEvolution.summarizer.temperature=Number(tp);
2228
+ }
2229
+
2230
+ const vp=document.getElementById('cfgViewerPort').value.trim();
2231
+ if(vp) cfg.viewerPort=Number(vp);
2232
+
2233
+ try{
2234
+ const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});
2235
+ if(!r.ok) throw new Error(await r.text());
2236
+ const el=document.getElementById('settingsSaved');
2237
+ el.classList.add('show');
2238
+ setTimeout(()=>el.classList.remove('show'),2500);
2239
+ }catch(e){
2240
+ showToast(t('settings.save.fail')+': '+e.message,'error');
2241
+ }
2242
+ }
2243
+
2244
+ function renderSkillMarkdown(md){
2245
+ let content=md;
2246
+ // Strip YAML frontmatter
2247
+ content=content.replace(/^---[\\s\\S]*?---\\s*/,'');
2248
+ // Code blocks
2249
+ content=content.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g,function(_,lang,code){
2250
+ return '<pre style="background:rgba(0,0,0,.3);border:1px solid var(--border);border-radius:8px;padding:12px 16px;overflow-x:auto;font-size:12px;line-height:1.5;font-family:SF Mono,Monaco,Consolas,monospace"><code>'+esc(code.trim())+'</code></pre>';
2251
+ });
2252
+ // Inline code
2253
+ content=content.replace(/\`([^\`]+)\`/g,'<code style="background:rgba(139,92,246,.1);color:var(--violet);padding:1px 6px;border-radius:4px;font-size:12px">$1</code>');
2254
+ // Headers
2255
+ content=content.replace(/^### (.+)$/gm,'<div class="summary-section-title" style="font-size:13px;margin-top:12px">$1</div>');
2256
+ content=content.replace(/^## (.+)$/gm,'<div class="summary-section-title">$1</div>');
2257
+ content=content.replace(/^# (.+)$/gm,'<div style="font-size:16px;font-weight:700;color:var(--text);margin:8px 0">$1</div>');
2258
+ // Bold
2259
+ content=content.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
2260
+ // List items
2261
+ content=content.replace(/^- (.+)$/gm,'<div style="padding-left:16px;position:relative;margin:3px 0"><span style="position:absolute;left:4px;color:var(--text-muted)">•</span>$1</div>');
2262
+ // HTML comments (version markers)
2263
+ content=content.replace(/<!--[\\s\\S]*?-->/g,'');
2264
+ // Line breaks
2265
+ content=content.replace(/\\n\\n/g,'<div style="height:10px"></div>');
2266
+ content=content.replace(/\\n/g,'<br>');
2267
+ return content;
2268
+ }
2269
+
2270
+ function closeSkillDetail(event){
2271
+ if(event && event.target!==document.getElementById('skillDetailOverlay')) return;
2272
+ document.getElementById('skillDetailOverlay').classList.remove('show');
2273
+ }
2274
+
2275
+ function formatDuration(ms){
2276
+ const s=Math.floor(ms/1000);
2277
+ if(s<60) return s+'s';
2278
+ const m=Math.floor(s/60);
2279
+ if(m<60) return m+'min';
2280
+ const h=Math.floor(m/60);
2281
+ if(h<24) return h+'h '+((m%60)>0?(m%60)+'min':'');
2282
+ const d=Math.floor(h/24);
2283
+ return d+'d '+((h%24)>0?(h%24)+'h':'');
2284
+ }
2285
+
2286
+ function formatTime(ts){
2287
+ if(!ts) return '-';
2288
+ return new Date(ts).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
2289
+ }
2290
+
2291
+ function fillDays(rows,days){
2292
+ const map=new Map((rows||[]).map(r=>[r.date,{...r}]));
2293
+ const out=[];const now=new Date();
2294
+ for(let i=days-1;i>=0;i--){
2295
+ const d=new Date(now);d.setDate(d.getDate()-i);
2296
+ const dateStr=d.toISOString().slice(0,10);
2297
+ const row=map.get(dateStr)||{};
2298
+ out.push({date:dateStr,count:row.count??0,list:row.list??0,search:row.search??0,total:(row.list??0)+(row.search??0)});
2299
+ }
2300
+ if(days>21){
2301
+ const weeks=[];let i=0;
2302
+ while(i<out.length){
2303
+ const chunk=out.slice(i,i+7);
2304
+ const first=chunk[0].date,last=chunk[chunk.length-1].date;
2305
+ const c=chunk.reduce((s,r)=>s+r.count,0);
2306
+ const l=chunk.reduce((s,r)=>s+r.list,0);
2307
+ const se=chunk.reduce((s,r)=>s+r.search,0);
2308
+ const label=first.slice(5,10)+'~'+last.slice(8,10);
2309
+ weeks.push({date:label,count:c,list:l,search:se,total:l+se});
2310
+ i+=7;
2311
+ }
2312
+ return weeks;
2313
+ }
2314
+ return out;
2315
+ }
2316
+
2317
+ function renderBars(el,data,valueKey,H){
2318
+ const vals=data.map(d=>d[valueKey]??0);
2319
+ 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;}
2320
+ const max=Math.max(1,...vals);
2321
+ const nonZero=vals.filter(v=>v>0).length;
2322
+ const barStyle=data.length<=7?'min-width:40px;max-width:120px':'';
2323
+ el.innerHTML=data.map(r=>{
2324
+ const v=r[valueKey]??0;
2325
+ const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
2326
+ if(v===0){
2327
+ return '<div class="chart-bar-wrap" style="'+barStyle+'"><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>';
2328
+ }
2329
+ const h=Math.max(8,Math.round((v/max)*H));
2330
+ return '<div class="chart-bar-wrap" style="'+barStyle+'"><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>';
2331
+ }).join('');
2332
+ }
2333
+
2334
+ function renderChartWrites(rows){
2335
+ const el=document.getElementById('chartWrites');
2336
+ const filled=fillDays(rows?.map(r=>({date:r.date,count:r.count})),metricsDays);
2337
+ renderBars(el,filled,'count',160);
2338
+ }
2339
+
2340
+ function renderChartCalls(rows){
2341
+ const el=document.getElementById('chartCalls');
2342
+ const filled=fillDays(rows?.map(r=>({date:r.date,list:r.list,search:r.search})),metricsDays);
2343
+ const vals=filled.map(f=>f.total);
2344
+ 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;}
2345
+ const max=Math.max(1,...vals);
2346
+ const H=160;
2347
+ el.innerHTML=filled.map(r=>{
2348
+ const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
2349
+ if(r.total===0){
2350
+ 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>';
2351
+ }
2352
+ const totalH=Math.max(8,Math.round((r.total/max)*H));
2353
+ const listH=r.list?Math.max(3,Math.round((r.list/r.total)*totalH)):0;
2354
+ const searchH=r.search?totalH-listH:0;
2355
+ const tip='List: '+r.list+', Search: '+r.search;
2356
+ let bars='';
2357
+ if(searchH>0) bars+='<div class="chart-bar violet" style="height:'+searchH+'px"></div>';
2358
+ if(listH>0) bars+='<div class="chart-bar" style="height:'+listH+'px"></div>';
2359
+ 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>';
2360
+ }).join('');
2361
+ }
2362
+
2363
+ /* ─── Tool Performance Chart ─── */
2364
+ let toolMinutes=60;
2365
+ const TOOL_COLORS=['#818cf8','#34d399','#fbbf24','#f87171','#38bdf8','#a78bfa','#fb923c'];
2366
+
2367
+ function setToolMinutes(m){
2368
+ toolMinutes=m;
2369
+ document.querySelectorAll('.tool-range').forEach(b=>{
2370
+ b.classList.toggle('active',Number(b.dataset.mins)===m);
2371
+ });
2372
+ loadToolMetrics();
2373
+ }
2374
+
2375
+ async function loadToolMetrics(){
2376
+ try{
2377
+ const r=await fetch('/api/tool-metrics?minutes='+toolMinutes);
2378
+ if(!r.ok) return;
2379
+ const d=await r.json();
2380
+ if(d.error) return;
2381
+ renderToolChart(d);
2382
+ renderToolAgg(d);
2383
+ }catch(e){
2384
+ console.warn('loadToolMetrics error:',e);
2385
+ }
2386
+ }
2387
+
2388
+ function renderToolChart(data){
2389
+ const container=document.getElementById('toolChart');
2390
+ const legend=document.getElementById('toolLegend');
2391
+ const {tools,series}=data;
2392
+
2393
+ if(!series||series.length===0||tools.length===0){
2394
+ container.innerHTML='<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:var(--text-muted)"><div style="font-size:36px;opacity:.25">\u{1F4CA}</div><div style="font-size:13px;font-weight:500">Waiting for tool calls...</div><div style="font-size:11px;opacity:.6">Charts will render once the agent uses memory tools</div></div>';
2395
+ legend.innerHTML='';
2396
+ return;
2397
+ }
2398
+
2399
+ const W=container.clientWidth||800;
2400
+ const H=280;
2401
+ const pad={t:20,r:20,b:36,l:52};
2402
+ const cw=W-pad.l-pad.r;
2403
+ const ch=H-pad.t-pad.b;
2404
+
2405
+ let maxVal=0;
2406
+ for(const s of series){for(const t of tools){const v=s[t]||0;if(v>maxVal)maxVal=v;}}
2407
+ if(maxVal===0)maxVal=100;
2408
+ maxVal=Math.ceil(maxVal*1.15);
2409
+
2410
+ const gridLines=5;
2411
+ let gridHtml='';
2412
+ for(let i=0;i<=gridLines;i++){
2413
+ const y=pad.t+ch-(ch/gridLines)*i;
2414
+ const val=Math.round((maxVal/gridLines)*i);
2415
+ gridHtml+='<line class="grid-line" x1="'+pad.l+'" y1="'+y+'" x2="'+(W-pad.r)+'" y2="'+y+'"/>';
2416
+ gridHtml+='<text class="axis-label" x="'+(pad.l-8)+'" y="'+(y+3)+'" text-anchor="end">'+val+'ms</text>';
2417
+ }
2418
+
2419
+ const step=cw/(series.length-1||1);
2420
+ const labelEvery=Math.max(1,Math.floor(series.length/8));
2421
+ let labelsHtml='';
2422
+ series.forEach((s,i)=>{
2423
+ if(i%labelEvery===0||i===series.length-1){
2424
+ const x=pad.l+i*step;
2425
+ const time=s.minute.slice(11);
2426
+ labelsHtml+='<text class="axis-label" x="'+x+'" y="'+(H-4)+'" text-anchor="middle">'+time+'</text>';
2427
+ }
2428
+ });
2429
+
2430
+ let pathsHtml='';
2431
+ let dotsHtml='';
2432
+ tools.forEach((toolName,ti)=>{
2433
+ const color=TOOL_COLORS[ti%TOOL_COLORS.length];
2434
+ const pts=series.map((s,i)=>{
2435
+ const x=pad.l+i*step;
2436
+ const v=s[toolName]||0;
2437
+ const y=pad.t+ch-((v/maxVal)*ch);
2438
+ return {x,y,v};
2439
+ });
2440
+ let line='M'+pts[0].x.toFixed(1)+' '+pts[0].y.toFixed(1);
2441
+ for(let i=1;i<pts.length;i++){
2442
+ const p0=pts[Math.max(0,i-2)],p1=pts[i-1],p2=pts[i],p3=pts[Math.min(pts.length-1,i+1)];
2443
+ const cp1x=(p1.x+(p2.x-p0.x)/6).toFixed(1),cp1y=(p1.y+(p2.y-p0.y)/6).toFixed(1);
2444
+ const cp2x=(p2.x-(p3.x-p1.x)/6).toFixed(1),cp2y=(p2.y-(p3.y-p1.y)/6).toFixed(1);
2445
+ line+=' C'+cp1x+' '+cp1y+','+cp2x+' '+cp2y+','+p2.x.toFixed(1)+' '+p2.y.toFixed(1);
2446
+ }
2447
+ pathsHtml+='<path class="data-line" d="'+line+'" stroke="'+color+'" />';
2448
+ const area=line+' L'+pts[pts.length-1].x.toFixed(1)+' '+(pad.t+ch)+' L'+pts[0].x.toFixed(1)+' '+(pad.t+ch)+' Z';
2449
+ pathsHtml+='<path class="data-area" d="'+area+'" fill="url(#tg'+ti+')" />';
2450
+ pts.forEach((p,i)=>{
2451
+ dotsHtml+='<circle class="hover-dot" cx="'+p.x.toFixed(1)+'" cy="'+p.y.toFixed(1)+'" fill="'+color+'" data-tool="'+toolName+'" data-idx="'+i+'" data-val="'+p.v+'" />';
2452
+ });
2453
+ });
2454
+
2455
+ const svg='<svg class="tool-chart-svg" viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet">'+
2456
+ '<defs>'+
2457
+ tools.map((t,i)=>{
2458
+ const c=TOOL_COLORS[i%TOOL_COLORS.length];
2459
+ return '<linearGradient id="tg'+i+'" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="'+c+'" stop-opacity=".08"/><stop offset="1" stop-color="'+c+'" stop-opacity="0"/></linearGradient>'+
2460
+ '';
2461
+ }).join('')+'</defs>'+
2462
+
2463
+ gridHtml+labelsHtml+pathsHtml+dotsHtml+
2464
+ '<line class="crosshair" x1="0" y1="'+pad.t+'" x2="0" y2="'+(pad.t+ch)+'" stroke="var(--text-muted)" stroke-width="0.5" stroke-dasharray="3 3" opacity="0" />'+
2465
+ '<rect class="hover-rect" x="'+pad.l+'" y="'+pad.t+'" width="'+cw+'" height="'+ch+'" fill="transparent" />'+
2466
+ '</svg><div class="tool-chart-tooltip" id="toolTooltip"></div>';
2467
+
2468
+ container.innerHTML=svg;
2469
+
2470
+ legend.innerHTML=tools.map((t,i)=>{
2471
+ const c=TOOL_COLORS[i%TOOL_COLORS.length];
2472
+ return '<span><span class="dot" style="background:'+c+'"></span>'+t+'</span>';
2473
+ }).join('');
2474
+
2475
+ const svgEl=container.querySelector('svg');
2476
+ const tooltip=document.getElementById('toolTooltip');
2477
+ const rect=svgEl.querySelector('.hover-rect');
2478
+
2479
+ rect.addEventListener('mousemove',function(e){
2480
+ const r=svgEl.getBoundingClientRect();
2481
+ const mx=e.clientX-r.left;
2482
+ const scale=W/r.width;
2483
+ const dataX=(mx*scale-pad.l)/step;
2484
+ const idx=Math.max(0,Math.min(series.length-1,Math.round(dataX)));
2485
+ const s=series[idx];
2486
+ if(!s)return;
2487
+
2488
+ svgEl.querySelectorAll('.hover-dot').forEach(d=>{
2489
+ d.classList.toggle('show',Number(d.dataset.idx)===idx);
2490
+ });
2491
+ const crosshair=svgEl.querySelector('.crosshair');
2492
+ const cx=pad.l+idx*step;
2493
+ crosshair.setAttribute('x1',cx);crosshair.setAttribute('x2',cx);crosshair.setAttribute('opacity','0.5');
2494
+
2495
+ let rows='<div class="tt-time">'+s.minute+'</div>';
2496
+ tools.forEach((t,ti)=>{
2497
+ const v=s[t]||0;
2498
+ const c=TOOL_COLORS[ti%TOOL_COLORS.length];
2499
+ rows+='<div class="tt-row"><span class="tt-dot" style="background:'+c+'"></span>'+t+'<span class="tt-val">'+v+'ms</span></div>';
2500
+ });
2501
+ tooltip.innerHTML=rows;
2502
+ tooltip.classList.add('show');
2503
+
2504
+ const tx=e.clientX-container.getBoundingClientRect().left;
2505
+ const ty=e.clientY-container.getBoundingClientRect().top;
2506
+ tooltip.style.left=(tx+15)+'px';
2507
+ tooltip.style.top=(ty-10)+'px';
2508
+ if(tx>container.clientWidth*0.7) tooltip.style.left=(tx-tooltip.offsetWidth-15)+'px';
2509
+ });
2510
+
2511
+ rect.addEventListener('mouseleave',function(){
2512
+ svgEl.querySelectorAll('.hover-dot').forEach(d=>d.classList.remove('show'));
2513
+ svgEl.querySelector('.crosshair').setAttribute('opacity','0');
2514
+ tooltip.classList.remove('show');
2515
+ });
2516
+ }
2517
+
2518
+ function renderToolAgg(data){
2519
+ const el=document.getElementById('toolAggTable');
2520
+ const {aggregated}=data;
2521
+ if(!aggregated||aggregated.length===0){el.innerHTML='';return;}
2522
+
2523
+ const msClass=v=>v<100?'fast':v<500?'medium':'slow';
2524
+
2525
+ el.innerHTML='<table class="tool-agg-table"><thead><tr><th>Tool</th><th>Calls</th><th>Avg</th><th>P95</th><th>Errors</th></tr></thead><tbody>'+
2526
+ aggregated.map((a,i)=>{
2527
+ const c=TOOL_COLORS[i%TOOL_COLORS.length];
2528
+ return '<tr>'+
2529
+ '<td><span class="tool-name"><span class="tool-dot" style="background:'+c+'"></span>'+a.tool+'</span></td>'+
2530
+ '<td>'+a.totalCalls+'</td>'+
2531
+ '<td><span class="ms-val '+msClass(a.avgMs)+'">'+a.avgMs+'ms</span></td>'+
2532
+ '<td><span class="ms-val '+msClass(a.p95Ms)+'">'+a.p95Ms+'ms</span></td>'+
2533
+ '<td>'+(a.errorCount>0?'<span style="color:var(--accent)">'+a.errorCount+'</span>':'<span style="color:var(--text-muted)">0</span>')+'</td>'+
2534
+ '</tr>';
2535
+ }).join('')+
2536
+ '</tbody></table>';
2537
+ }
2538
+
2539
+ function renderBreakdown(obj,containerId){
2540
+ const el=document.getElementById(containerId);
2541
+ if(!el)return;
2542
+ const entries=Object.entries(obj||{}).sort((a,b)=>b[1]-a[1]);
2543
+ const total=entries.reduce((s,[,v])=>s+v,0)||1;
2544
+ el.innerHTML=entries.map(([label,value])=>{
2545
+ const pct=Math.round((value/total)*100);
2546
+ 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>';
2547
+ }).join('');
2548
+ }
2549
+
457
2550
  /* ─── Data loading ─── */
458
2551
  async function loadAll(){
459
2552
  await Promise.all([loadStats(),loadMemories()]);
@@ -462,7 +2555,13 @@ async function loadAll(){
462
2555
  async function loadStats(){
463
2556
  const r=await fetch('/api/stats');
464
2557
  const d=await r.json();
2558
+ const dedupB=d.dedupBreakdown||{};
2559
+ const activeCount=dedupB.active||d.totalMemories;
2560
+ const inactiveCount=(dedupB.duplicate||0)+(dedupB.merged||0);
465
2561
  document.getElementById('statTotal').textContent=d.totalMemories;
2562
+ if(inactiveCount>0){
2563
+ document.getElementById('statTotal').title=activeCount+' '+t('stat.active')+', '+inactiveCount+' '+t('stat.deduped');
2564
+ }
466
2565
  document.getElementById('statSessions').textContent=d.totalSessions;
467
2566
  document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;
468
2567
  const days=d.timeRange.earliest?Math.max(1,Math.round((new Date(d.timeRange.latest)-new Date(d.timeRange.earliest))/(86400000))):0;
@@ -470,13 +2569,13 @@ async function loadStats(){
470
2569
 
471
2570
  const provEl=document.getElementById('embeddingStatus');
472
2571
  if(d.embeddingProvider && d.embeddingProvider!=='none'){
473
- provEl.innerHTML='<div class="provider-badge"><span>\\u2713</span> Embedding: '+d.embeddingProvider+'</div>';
2572
+ provEl.innerHTML='<div class="provider-badge"><span>\\u2713</span> '+t('embed.on')+d.embeddingProvider+'</div>';
474
2573
  } else {
475
- provEl.innerHTML='<div class="provider-badge offline"><span>\\u26A0</span> No embedding model</div>';
2574
+ provEl.innerHTML='<div class="provider-badge offline"><span>\\u26A0</span> '+t('embed.off')+'</div>';
476
2575
  }
477
2576
 
478
2577
  const sl=document.getElementById('sessionList');
479
- sl.innerHTML='<div class="session-item'+(activeSession===null?' active':'')+'" onclick="filterSession(null)"><span>All Sessions</span><span class="count">'+d.totalMemories+'</span></div>';
2578
+ sl.innerHTML='<div class="session-item'+(activeSession===null?' active':'')+'" onclick="filterSession(null)"><span>'+t('sidebar.allsessions')+'</span><span class="count">'+d.totalMemories+'</span></div>';
480
2579
  (d.sessions||[]).forEach(s=>{
481
2580
  const isActive=activeSession===s.session_key;
482
2581
  const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;
@@ -510,7 +2609,7 @@ async function loadMemories(page){
510
2609
  const d=await r.json();
511
2610
  totalPages=d.totalPages||1;
512
2611
  totalCount=d.total||0;
513
- document.getElementById('searchMeta').textContent=totalCount+' memories total';
2612
+ document.getElementById('searchMeta').textContent=totalCount+t('search.meta.total');
514
2613
  renderMemories(d.memories||[]);
515
2614
  renderPagination();
516
2615
  }
@@ -524,9 +2623,9 @@ async function doSearch(q){
524
2623
  const r=await fetch('/api/search?'+p.toString());
525
2624
  const d=await r.json();
526
2625
  const meta=[];
527
- if(d.vectorCount>0) meta.push(d.vectorCount+' semantic');
528
- if(d.ftsCount>0) meta.push(d.ftsCount+' text');
529
- meta.push(d.total+' results');
2626
+ if(d.vectorCount>0) meta.push(d.vectorCount+t('search.meta.semantic'));
2627
+ if(d.ftsCount>0) meta.push(d.ftsCount+t('search.meta.text'));
2628
+ meta.push(d.total+t('search.meta.results'));
530
2629
  document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');
531
2630
  renderMemories(d.results||[]);
532
2631
  document.getElementById('pagination').innerHTML='';
@@ -570,7 +2669,7 @@ function clearDateFilter(){
570
2669
  function renderMemories(items){
571
2670
  const list=document.getElementById('memoryList');
572
2671
  if(!items.length){
573
- list.innerHTML='<div class="empty"><div class="icon">\\u{1F4ED}</div><p>No memories found</p></div>';
2672
+ list.innerHTML='<div class="empty"><div class="icon">\\u{1F4ED}</div><p>'+t('empty.text')+'</p></div>';
574
2673
  return;
575
2674
  }
576
2675
  items.forEach(m=>{memoryCache[m.id]=m});
@@ -584,14 +2683,46 @@ function renderMemories(items){
584
2683
  const vscore=m._vscore?'<span class="vscore-badge">'+Math.round(m._vscore*100)+'%</span>':'';
585
2684
  const sid=m.session_key||'';
586
2685
  const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;
587
- return '<div class="memory-card">'+
588
- '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span><span class="kind-tag">'+kind+'</span></div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+'</span></div>'+
2686
+ const mc=m.merge_count||0;
2687
+ const mergeBadge=mc>0?'<span class="merge-badge">\\u{1F504} '+t('card.evolved')+' '+mc+t('card.times')+'</span>':'';
2688
+ const updatedAt=(m.updated_at&&m.updated_at>m.created_at)?'<span class="card-updated">'+t('card.updated')+' '+new Date(m.updated_at).toLocaleString('zh-CN')+'</span>':'';
2689
+ const ds=m.dedup_status||'active';
2690
+ const isInactive=ds==='duplicate'||ds==='merged';
2691
+ const dedupBadge=ds==='duplicate'?'<span class="dedup-badge duplicate">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class="dedup-badge merged">'+t('card.dedupMerged')+'</span>':'';
2692
+ let dedupInfo='';
2693
+ if(isInactive){
2694
+ const reason=m.dedup_reason?'<span style="font-size:11px;color:var(--text-muted)">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';
2695
+ const target=m.dedup_target?'<span class="dedup-target-link" onclick="scrollToMemory(\\''+m.dedup_target+'\\')">'+t('card.dedupTarget')+m.dedup_target.slice(0,8)+'...</span>':'';
2696
+ dedupInfo='<div style="margin-top:6px;font-size:11px">'+target+' '+reason+'</div>';
2697
+ }
2698
+ let historyHtml='';
2699
+ if(mc>0){
2700
+ try{
2701
+ const hist=JSON.parse(m.merge_history||'[]');
2702
+ if(hist.length>0){
2703
+ historyHtml='<div class="merge-history" id="history-'+id+'" style="display:none"><div style="font-weight:600;margin-bottom:8px;font-size:12px">'+t('card.evolveHistory')+' ('+hist.length+')</div>';
2704
+ hist.forEach(function(h){
2705
+ const ht=h.at?new Date(h.at).toLocaleString('zh-CN'):'';
2706
+ historyHtml+='<div class="merge-history-item"><span class="merge-action '+h.action+'">'+h.action+'</span> <span style="color:var(--text-muted)">'+ht+'</span><br>'+esc(h.reason||'');
2707
+ if(h.from) historyHtml+='<br><span style="opacity:.6">'+t('card.oldSummary')+':</span> '+esc(h.from);
2708
+ if(h.to) historyHtml+='<br><span style="opacity:.6">'+t('card.newSummary')+':</span> '+esc(h.to);
2709
+ historyHtml+='</div>';
2710
+ });
2711
+ historyHtml+='</div>';
2712
+ }
2713
+ }catch(e){}
2714
+ }
2715
+ return '<div class="memory-card'+(isInactive?' dedup-inactive':'')+'">'+
2716
+ '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span><span class="kind-tag">'+kind+'</span>'+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
589
2717
  '<div class="card-summary">'+summary+'</div>'+
2718
+ dedupInfo+
590
2719
  '<div class="card-content" id="content-'+id+'"><pre>'+content+'</pre></div>'+
2720
+ historyHtml+
591
2721
  '<div class="card-actions">'+
592
- '<button class="btn btn-sm btn-text" onclick="toggleContent(\\''+id+'\\')">Expand</button>'+
593
- '<button class="btn btn-sm" onclick="openEditModal(\\''+id+'\\')">Edit</button>'+
594
- '<button class="btn btn-sm btn-danger" onclick="deleteMemory(\\''+id+'\\')">Delete</button>'+
2722
+ '<button class="btn btn-sm btn-ghost" onclick="toggleContent(\\''+id+'\\')">'+t('card.expand')+'</button>'+
2723
+ (mc>0?'<button class="btn btn-sm btn-ghost" onclick="toggleHistory(\\''+id+'\\')">'+t('card.evolveHistory')+'</button>':'')+
2724
+ '<button class="btn btn-sm btn-ghost" onclick="openEditModal(\\''+id+'\\')">'+t('card.edit')+'</button>'+
2725
+ '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteMemory(\\''+id+'\\')">'+t('card.delete')+'</button>'+
595
2726
  vscore+
596
2727
  '</div></div>';
597
2728
  }).join('');
@@ -614,7 +2745,7 @@ function renderPagination(){
614
2745
  prev=p;
615
2746
  }
616
2747
  h+='<button class="pg-btn'+(currentPage>=totalPages?' disabled':'')+'" onclick="goPage('+(currentPage+1)+')">\u203A</button>';
617
- h+='<span class="pg-info">'+totalCount+' total</span>';
2748
+ h+='<span class="pg-info">'+totalCount+t('pagination.total')+'</span>';
618
2749
  el.innerHTML=h;
619
2750
  }
620
2751
 
@@ -625,21 +2756,109 @@ function goPage(p){
625
2756
  document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});
626
2757
  }
627
2758
 
2759
+ function toggleHistory(id){
2760
+ const el=document.getElementById('history-'+id);
2761
+ if(el) el.style.display=el.style.display==='none'?'block':'none';
2762
+ }
2763
+
628
2764
  function toggleContent(id){
629
2765
  const el=document.getElementById('content-'+id);
630
2766
  el.classList.toggle('show');
631
2767
  }
632
2768
 
2769
+ function scrollToMemory(targetId){
2770
+ const cards=document.querySelectorAll('.memory-card');
2771
+ for(const card of cards){
2772
+ const contentEl=card.querySelector('[id^="content-"]');
2773
+ if(contentEl&&contentEl.id==='content-'+targetId){
2774
+ card.scrollIntoView({behavior:'smooth',block:'center'});
2775
+ card.style.transition='box-shadow .3s';
2776
+ card.style.boxShadow='0 0 0 2px var(--pri)';
2777
+ setTimeout(()=>{card.style.boxShadow='';},2000);
2778
+ return;
2779
+ }
2780
+ }
2781
+ showMemoryModal(targetId);
2782
+ }
2783
+ async function showMemoryModal(chunkId){
2784
+ const overlay=document.getElementById('memoryModal');
2785
+ const body=document.getElementById('memoryModalBody');
2786
+ body.innerHTML='<div style="text-align:center;padding:40px;color:var(--text-sec)">Loading...</div>';
2787
+ overlay.classList.add('show');
2788
+ try{
2789
+ const res=await fetch('/api/memory/'+encodeURIComponent(chunkId));
2790
+ if(!res.ok){body.innerHTML='<div style="text-align:center;padding:40px;color:#f87171">Memory not found</div>';return;}
2791
+ const data=await res.json();
2792
+ const m=data.memory;
2793
+ const role=(m.role||'unknown').toUpperCase();
2794
+ const roleCls=(m.role||'').toLowerCase();
2795
+ const kind=m.kind||'paragraph';
2796
+ const ds=m.dedup_status||'active';
2797
+ const time=new Date(m.created_at).toLocaleString('zh-CN');
2798
+ const updated=m.updated_at?new Date(m.updated_at).toLocaleString('zh-CN'):'';
2799
+ let html='<div class="modal-memory-card">';
2800
+ html+='<div class="modal-header-row"><span class="role-tag '+roleCls+'">'+role+'</span><span class="kind-tag">'+kind+'</span>';
2801
+ if(ds!=='active') html+='<span class="dedup-badge '+(ds==='duplicate'?'duplicate':'merged')+'">'+ds+'</span>';
2802
+ html+='</div>';
2803
+ html+='<div class="modal-field"><div class="modal-field-label">ID</div><div class="modal-field-val" style="font-family:monospace;font-size:11px">'+esc(m.id)+'</div></div>';
2804
+ html+='<div class="modal-field"><div class="modal-field-label">Summary</div><div class="modal-field-val" style="font-size:14px;font-weight:600">'+esc(m.summary||'')+'</div></div>';
2805
+ html+='<div class="modal-field"><div class="modal-field-label">Content</div><pre class="modal-field-content">'+esc(m.content||'')+'</pre></div>';
2806
+ html+='<div class="modal-meta-row">';
2807
+ html+='<span><strong>Session:</strong> '+esc(m.session_key||'')+'</span>';
2808
+ html+='<span><strong>Created:</strong> '+time+'</span>';
2809
+ if(updated) html+='<span><strong>Updated:</strong> '+updated+'</span>';
2810
+ html+='</div>';
2811
+ if(m.dedup_reason) html+='<div class="modal-field"><div class="modal-field-label">Dedup Reason</div><div class="modal-field-val">'+esc(m.dedup_reason)+'</div></div>';
2812
+ if(m.dedup_target&&m.dedup_target!==chunkId) html+='<div class="modal-field"><span class="dedup-target-link" onclick="closeMemoryModal();scrollToMemory(\\''+m.dedup_target+'\\')">View target: '+m.dedup_target.slice(0,8)+'...</span></div>';
2813
+ html+='</div>';
2814
+ body.innerHTML=html;
2815
+ }catch(e){body.innerHTML='<div style="text-align:center;padding:40px;color:#f87171">Error: '+esc(String(e))+'</div>';}
2816
+ }
2817
+ function closeMemoryModal(){document.getElementById('memoryModal').classList.remove('show');}
2818
+
2819
+
633
2820
  function esc(s){
634
2821
  if(!s)return'';
635
2822
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
636
2823
  }
637
2824
 
2825
+ function renderSummaryHtml(raw){
2826
+ if(!raw)return'';
2827
+ var lines=raw.split('\\n');
2828
+ var html=[];
2829
+ var inList=false;
2830
+ var sectionRe=new RegExp('^(\u{1F3AF}|\u{1F4CB}|\u2705|\u{1F4A1})\\\\s+(.+)$');
2831
+ var listRe=new RegExp('^- (.+)$');
2832
+ for(var i=0;i<lines.length;i++){
2833
+ var line=lines[i];
2834
+ var hm=line.match(sectionRe);
2835
+ if(hm){
2836
+ if(inList){html.push('</ul>');inList=false;}
2837
+ html.push('<div class="summary-section-title">'+esc(line)+'</div>');
2838
+ continue;
2839
+ }
2840
+ var lm=line.match(listRe);
2841
+ if(lm){
2842
+ if(!inList){html.push('<ul>');inList=true;}
2843
+ html.push('<li>'+esc(lm[1])+'</li>');
2844
+ continue;
2845
+ }
2846
+ if(line.trim()===''){
2847
+ if(inList){html.push('</ul>');inList=false;}
2848
+ continue;
2849
+ }
2850
+ if(inList){html.push('</ul>');inList=false;}
2851
+ html.push('<p style="margin:4px 0">'+esc(line)+'</p>');
2852
+ }
2853
+ if(inList)html.push('</ul>');
2854
+ return html.join('');
2855
+ }
2856
+
638
2857
  /* ─── CRUD ─── */
639
2858
  function openCreateModal(){
640
2859
  editingId=null;
641
- document.getElementById('modalTitle').textContent='New Memory';
642
- document.getElementById('modalSubmit').textContent='Create';
2860
+ document.getElementById('modalTitle').textContent=t('modal.new');
2861
+ document.getElementById('modalSubmit').textContent=t('modal.create');
643
2862
  document.getElementById('mRole').value='user';
644
2863
  document.getElementById('mContent').value='';
645
2864
  document.getElementById('mSummary').value='';
@@ -649,10 +2868,10 @@ function openCreateModal(){
649
2868
 
650
2869
  function openEditModal(id){
651
2870
  const m=memoryCache[id];
652
- if(!m){toast('Memory not found in cache','error');return}
2871
+ if(!m){toast(t('toast.notfound'),'error');return}
653
2872
  editingId=id;
654
- document.getElementById('modalTitle').textContent='Edit Memory';
655
- document.getElementById('modalSubmit').textContent='Save';
2873
+ document.getElementById('modalTitle').textContent=t('modal.edit');
2874
+ document.getElementById('modalSubmit').textContent=t('modal.save');
656
2875
  document.getElementById('mRole').value=m.role||'user';
657
2876
  document.getElementById('mContent').value=m.content||'';
658
2877
  document.getElementById('mSummary').value=m.summary||'';
@@ -671,7 +2890,7 @@ async function submitModal(){
671
2890
  summary:document.getElementById('mSummary').value,
672
2891
  kind:document.getElementById('mKind').value,
673
2892
  };
674
- if(!data.content.trim()){toast('Please enter content','error');return}
2893
+ if(!data.content.trim()){toast(t('modal.err.empty'),'error');return}
675
2894
  let r;
676
2895
  if(editingId){
677
2896
  r=await fetch('/api/memory/'+editingId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
@@ -679,25 +2898,25 @@ async function submitModal(){
679
2898
  r=await fetch('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
680
2899
  }
681
2900
  const d=await r.json();
682
- if(d.ok){toast(editingId?'Memory updated':'Memory created','success');closeModal();loadAll();}
683
- else{toast(d.error||'Operation failed','error')}
2901
+ if(d.ok){toast(editingId?t('toast.updated'):t('toast.created'),'success');closeModal();loadAll();}
2902
+ else{toast(d.error||t('toast.opfail'),'error')}
684
2903
  }
685
2904
 
686
2905
  async function deleteMemory(id){
687
- if(!confirm('Delete this memory?'))return;
2906
+ if(!confirm(t('confirm.delete')))return;
688
2907
  const r=await fetch('/api/memory/'+id,{method:'DELETE'});
689
2908
  const d=await r.json();
690
- if(d.ok){toast('Memory deleted','success');loadAll();}
691
- else{toast('Delete failed','error')}
2909
+ if(d.ok){toast(t('toast.deleted'),'success');loadAll();}
2910
+ else{toast(t('toast.delfail'),'error')}
692
2911
  }
693
2912
 
694
2913
  async function clearAll(){
695
- if(!confirm('Delete ALL memories? This cannot be undone.'))return;
696
- if(!confirm('Are you absolutely sure?'))return;
2914
+ if(!confirm(t('confirm.clearall')))return;
2915
+ if(!confirm(t('confirm.clearall2')))return;
697
2916
  const r=await fetch('/api/memories',{method:'DELETE'});
698
2917
  const d=await r.json();
699
- if(d.ok){toast('All memories cleared','success');loadAll();}
700
- else{toast('Clear failed','error')}
2918
+ if(d.ok){toast(t('toast.cleared'),'success');loadAll();}
2919
+ else{toast(t('toast.clearfail'),'error')}
701
2920
  }
702
2921
 
703
2922
  /* ─── Toast ─── */
@@ -720,7 +2939,20 @@ initViewerTheme();
720
2939
  /* ─── Init ─── */
721
2940
  document.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});
722
2941
  document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});
2942
+ applyI18n();
723
2943
  checkAuth();
724
2944
  </script>
2945
+
2946
+ <!-- Memory Detail Modal -->
2947
+ <div class="memory-modal-overlay" id="memoryModal" onclick="if(event.target===this)closeMemoryModal()">
2948
+ <div class="memory-modal">
2949
+ <div class="memory-modal-title">
2950
+ <span>Memory Detail</span>
2951
+ <button class="btn btn-sm btn-ghost" onclick="closeMemoryModal()" style="font-size:16px;padding:2px 8px">&times;</button>
2952
+ </div>
2953
+ <div class="memory-modal-body" id="memoryModalBody"></div>
2954
+ </div>
2955
+ </div>
2956
+
725
2957
  </body>
726
2958
  </html>`;