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