@memtensor/memos-local-openclaw-plugin 0.1.4 → 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 (104) hide show
  1. package/README.md +196 -84
  2. package/dist/ingest/dedup.d.ts +8 -0
  3. package/dist/ingest/dedup.d.ts.map +1 -1
  4. package/dist/ingest/dedup.js +21 -0
  5. package/dist/ingest/dedup.js.map +1 -1
  6. package/dist/ingest/providers/anthropic.d.ts +14 -0
  7. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  8. package/dist/ingest/providers/anthropic.js +104 -0
  9. package/dist/ingest/providers/anthropic.js.map +1 -1
  10. package/dist/ingest/providers/bedrock.d.ts +14 -0
  11. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  12. package/dist/ingest/providers/bedrock.js +100 -0
  13. package/dist/ingest/providers/bedrock.js.map +1 -1
  14. package/dist/ingest/providers/gemini.d.ts +14 -0
  15. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  16. package/dist/ingest/providers/gemini.js +96 -0
  17. package/dist/ingest/providers/gemini.js.map +1 -1
  18. package/dist/ingest/providers/index.d.ts +22 -0
  19. package/dist/ingest/providers/index.d.ts.map +1 -1
  20. package/dist/ingest/providers/index.js +68 -0
  21. package/dist/ingest/providers/index.js.map +1 -1
  22. package/dist/ingest/providers/openai.d.ts +22 -0
  23. package/dist/ingest/providers/openai.d.ts.map +1 -1
  24. package/dist/ingest/providers/openai.js +143 -0
  25. package/dist/ingest/providers/openai.js.map +1 -1
  26. package/dist/ingest/task-processor.d.ts +2 -0
  27. package/dist/ingest/task-processor.d.ts.map +1 -1
  28. package/dist/ingest/task-processor.js +15 -0
  29. package/dist/ingest/task-processor.js.map +1 -1
  30. package/dist/ingest/worker.d.ts +2 -0
  31. package/dist/ingest/worker.d.ts.map +1 -1
  32. package/dist/ingest/worker.js +115 -12
  33. package/dist/ingest/worker.js.map +1 -1
  34. package/dist/recall/engine.d.ts.map +1 -1
  35. package/dist/recall/engine.js +1 -0
  36. package/dist/recall/engine.js.map +1 -1
  37. package/dist/skill/bundled-memory-guide.d.ts +6 -0
  38. package/dist/skill/bundled-memory-guide.d.ts.map +1 -0
  39. package/dist/skill/bundled-memory-guide.js +95 -0
  40. package/dist/skill/bundled-memory-guide.js.map +1 -0
  41. package/dist/skill/evaluator.d.ts +31 -0
  42. package/dist/skill/evaluator.d.ts.map +1 -0
  43. package/dist/skill/evaluator.js +194 -0
  44. package/dist/skill/evaluator.js.map +1 -0
  45. package/dist/skill/evolver.d.ts +22 -0
  46. package/dist/skill/evolver.d.ts.map +1 -0
  47. package/dist/skill/evolver.js +193 -0
  48. package/dist/skill/evolver.js.map +1 -0
  49. package/dist/skill/generator.d.ts +25 -0
  50. package/dist/skill/generator.d.ts.map +1 -0
  51. package/dist/skill/generator.js +477 -0
  52. package/dist/skill/generator.js.map +1 -0
  53. package/dist/skill/installer.d.ts +16 -0
  54. package/dist/skill/installer.d.ts.map +1 -0
  55. package/dist/skill/installer.js +89 -0
  56. package/dist/skill/installer.js.map +1 -0
  57. package/dist/skill/upgrader.d.ts +19 -0
  58. package/dist/skill/upgrader.d.ts.map +1 -0
  59. package/dist/skill/upgrader.js +263 -0
  60. package/dist/skill/upgrader.js.map +1 -0
  61. package/dist/skill/validator.d.ts +29 -0
  62. package/dist/skill/validator.d.ts.map +1 -0
  63. package/dist/skill/validator.js +227 -0
  64. package/dist/skill/validator.js.map +1 -0
  65. package/dist/storage/sqlite.d.ts +75 -1
  66. package/dist/storage/sqlite.d.ts.map +1 -1
  67. package/dist/storage/sqlite.js +417 -6
  68. package/dist/storage/sqlite.js.map +1 -1
  69. package/dist/types.d.ts +78 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/dist/types.js +6 -0
  72. package/dist/types.js.map +1 -1
  73. package/dist/viewer/html.d.ts +1 -1
  74. package/dist/viewer/html.d.ts.map +1 -1
  75. package/dist/viewer/html.js +1549 -113
  76. package/dist/viewer/html.js.map +1 -1
  77. package/dist/viewer/server.d.ts +13 -0
  78. package/dist/viewer/server.d.ts.map +1 -1
  79. package/dist/viewer/server.js +289 -4
  80. package/dist/viewer/server.js.map +1 -1
  81. package/index.ts +489 -181
  82. package/package.json +1 -1
  83. package/skill/memos-memory-guide/SKILL.md +86 -0
  84. package/src/ingest/dedup.ts +29 -0
  85. package/src/ingest/providers/anthropic.ts +130 -0
  86. package/src/ingest/providers/bedrock.ts +126 -0
  87. package/src/ingest/providers/gemini.ts +124 -0
  88. package/src/ingest/providers/index.ts +86 -4
  89. package/src/ingest/providers/openai.ts +174 -0
  90. package/src/ingest/task-processor.ts +16 -0
  91. package/src/ingest/worker.ts +126 -21
  92. package/src/recall/engine.ts +1 -0
  93. package/src/skill/bundled-memory-guide.ts +91 -0
  94. package/src/skill/evaluator.ts +220 -0
  95. package/src/skill/evolver.ts +169 -0
  96. package/src/skill/generator.ts +506 -0
  97. package/src/skill/installer.ts +59 -0
  98. package/src/skill/upgrader.ts +257 -0
  99. package/src/skill/validator.ts +227 -0
  100. package/src/storage/sqlite.ts +508 -6
  101. package/src/types.ts +77 -0
  102. package/src/viewer/html.ts +1549 -113
  103. package/src/viewer/server.ts +285 -4
  104. package/skill/SKILL.md +0 -59
@@ -13,45 +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,.95);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:rgba(59,130,246,.1);color:#3b82f6}
48
- [data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2)}
49
- [data-theme="light"] .analytics-card{background:linear-gradient(145deg,#fff 0%,#f8fafc 100%);border-color:rgba(0,0,0,.08)}
50
- [data-theme="light"] .analytics-section{background:#fff;border-color:rgba(0,0,0,.08)}
51
- [data-theme="light"] .breakdown-item{background:rgba(0,0,0,.04)}
52
- [data-theme="light"] .metrics-toolbar .range-btn{background:#fff;border-color:rgba(0,0,0,.1)}
53
- [data-theme="light"] .metrics-toolbar .range-btn.active{background:rgba(0,0,0,.06);color:var(--text);border-color:rgba(0,0,0,.15)}
54
- [data-theme="light"] .metrics-toolbar .range-btn:hover{border-color:var(--pri);color:var(--pri)}
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}
55
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}
56
78
  button{cursor:pointer;font-family:inherit;font-size:inherit}
57
79
  input,textarea,select{font-family:inherit;font-size:inherit}
@@ -64,28 +86,28 @@ input,textarea,select{font-family:inherit;font-size:inherit}
64
86
  .auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}
65
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%)}
66
88
  .auth-card input::placeholder{color:hsl(0 0% 45.1%)}
67
- .auth-card input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.15)}
68
- .auth-card .btn-auth{width:100%;padding:12px;border:none;border-radius:8px;background:#3b82f6;color:#fff;font-weight:600;font-size:14px;transition:all .2s}
69
- .auth-card .btn-auth:hover{background:#2563eb;transform:translateY(-1px);box-shadow:0 8px 25px rgba(59,130,246,.3)}
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)}
70
92
  .auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}
71
93
  .auth-card .btn-text{color:hsl(0 0% 45.1%)}
72
- .auth-card .btn-text:hover{color:#3b82f6}
94
+ .auth-card .btn-text:hover{color:var(--pri)}
73
95
 
74
96
  .reset-guide{text-align:left;margin-bottom:20px}
75
97
  .reset-step{display:flex;gap:14px;margin-bottom:16px}
76
- .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}
77
99
  .step-body{flex:1;min-width:0}
78
100
  .step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}
79
101
  .step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}
80
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%)}
81
- .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%)}
82
104
  .cmd-box code{flex:1}
83
105
  .copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}
84
106
  .cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}
85
107
 
86
108
  /* ─── App Layout (dark dashboard, same as www) ─── */
87
109
  .app{display:none;flex-direction:column;min-height:100vh}
88
- .topbar{background:rgba(5,5,16,.92);border-bottom:1px solid var(--border);padding:0 28px;height:56px;display:flex;align-items:center;position:sticky;top:0;z-index:100;backdrop-filter:blur(16px)}
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)}
89
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}
90
112
  .topbar .brand .icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:22px;background:none;border-radius:0}
91
113
  .topbar .brand .sub{font-weight:400;color:var(--text-muted);font-size:11px}
@@ -126,9 +148,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
126
148
  .search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}
127
149
 
128
150
  .filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
129
- .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}
130
152
  .filter-chip:hover{border-color:var(--pri);color:var(--pri)}
131
- .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)}
132
154
 
133
155
  .memory-list{display:flex;flex-direction:column;gap:16px}
134
156
  .memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}
@@ -136,7 +158,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
136
158
  .memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}
137
159
  .memory-card .meta{display:flex;align-items:center;gap:8px}
138
160
  .role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}
139
- .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)}
140
162
  .role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}
141
163
  .role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}
142
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}
@@ -148,6 +170,36 @@ input,textarea,select{font-family:inherit;font-size:inherit}
148
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)}
149
171
  .card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}
150
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)}
151
203
 
152
204
  /* ─── Buttons ─── */
153
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}
@@ -185,7 +237,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
185
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}
186
238
  .toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}
187
239
  .toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}
188
- .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)}
189
241
  @keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}
190
242
 
191
243
  .empty{text-align:center;padding:64px 20px;color:var(--text-sec)}
@@ -242,6 +294,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
242
294
  .task-status-badge.skipped{color:var(--text-muted);background:rgba(128,128,128,.15)}
243
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}
244
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)}
245
298
  .task-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted)}
246
299
  .task-card-bottom .tag{display:flex;align-items:center;gap:4px}
247
300
  .task-card-bottom .tag .icon{font-size:12px}
@@ -282,6 +335,70 @@ input,textarea,select{font-family:inherit;font-size:inherit}
282
335
  [data-theme="light"] .task-card{background:#fff}
283
336
  [data-theme="light"] .tasks-stat{background:#fff}
284
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
+
285
402
  /* ─── Analytics / 统计 ─── */
286
403
  .nav-tabs{display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.06);border-radius:10px;padding:3px}
287
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}
@@ -289,52 +406,174 @@ input,textarea,select{font-family:inherit;font-size:inherit}
289
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)}
290
407
  [data-theme="light"] .nav-tabs{background:rgba(0,0,0,.05)}
291
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)}
292
- .analytics-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
293
- .analytics-view.show{display:flex}
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}
294
506
  .feed-wrap{flex:1;min-width:0;display:flex;flex-direction:column}
295
507
  .feed-wrap.hide{display:none}
296
- .analytics-cards{display:grid;grid-template-columns:repeat(5,1fr);gap:14px}
297
- .analytics-card{background:linear-gradient(145deg,var(--bg-card) 0%,rgba(255,255,255,.02) 100%);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 18px;position:relative;overflow:hidden;transition:all .25s}
298
- .analytics-card:hover{border-color:var(--border-glow);transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.15)}
299
- .analytics-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,#3b82f6,#60a5fa);opacity:.85}
300
- .analytics-card.green::before{background:linear-gradient(90deg,var(--green),#34d399)}
301
- .analytics-card.amber::before{background:linear-gradient(90deg,var(--amber),#fbbf24)}
302
- .analytics-card.violet::before{background:linear-gradient(90deg,var(--violet),#a78bfa)}
303
- .analytics-card .ac-value{font-size:26px;font-weight:800;letter-spacing:-.03em;color:var(--text);line-height:1.1}
304
- .analytics-card .ac-label{font-size:11px;color:var(--text-muted);margin-top:5px;font-weight:500;text-transform:uppercase;letter-spacing:.04em}
305
- .analytics-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:22px 24px}
306
- .analytics-section h3{font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:16px;display:flex;align-items:center;gap:8px}
307
- .analytics-section h3 .icon{font-size:14px;opacity:.7}
308
- .chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto}
309
- .chart-bar-wrap{flex:1;min-width:18px;max-width:60px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}
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}
310
525
  .chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}
311
- .chart-bar-wrap:hover .chart-bar{filter:brightness(1.25)}
312
- .chart-bar-wrap:hover .chart-bar-label{color:var(--pri)}
526
+ .chart-bar-wrap:hover .chart-bar{opacity:1}
527
+ .chart-bar-wrap:hover .chart-bar-label{color:var(--text)}
313
528
  .chart-bar-wrap:hover .chart-tip{opacity:1;transform:translateX(-50%) translateY(0)}
314
- .chart-tip{position:absolute;top:-6px;left:50%;transform:translateX(-50%) translateY(4px);background:var(--bg);border:1px solid var(--border);color:var(--text);padding:2px 8px;border-radius:6px;font-size:10px;font-weight:700;white-space:nowrap;z-index:5;pointer-events:none;box-shadow:var(--shadow);opacity:0;transition:all .15s ease}
315
- .chart-bar{width:100%;border-radius:4px 4px 0 0;background:linear-gradient(180deg,#60a5fa,#3b82f6);transition:all .3s ease}
316
- .chart-bar.violet{background:linear-gradient(180deg,var(--violet),#7c3aed)}
317
- .chart-bar.green{background:linear-gradient(180deg,var(--green),#059669)}
318
- .chart-bar.zero{background:var(--border);opacity:.4;border-radius:2px}
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}
319
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}
320
- .chart-legend{display:flex;gap:16px;margin-top:12px;flex-wrap:wrap;font-size:12px;color:var(--text-sec)}
321
- .chart-legend span{display:inline-flex;align-items:center;gap:6px}
322
- .chart-legend .dot{width:8px;height:8px;border-radius:50%}
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}
323
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)}
324
563
  .chart-legend .dot.violet{background:var(--violet)}
325
564
  .chart-legend .dot.green{background:var(--green)}
326
565
  .breakdown-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}
327
- .breakdown-item{display:flex;flex-direction:column;gap:6px;padding:12px 14px;background:rgba(0,0,0,.12);border-radius:10px;border:1px solid var(--border);transition:border-color .15s}
328
- .breakdown-item:hover{border-color:var(--border-glow)}
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)}
329
568
  .breakdown-item .bd-top{display:flex;align-items:center;justify-content:space-between}
330
- .breakdown-item .label{font-size:13px;color:var(--text-sec);font-weight:500;text-transform:capitalize}
331
- .breakdown-item .value{font-size:14px;font-weight:700;color:var(--text)}
332
- .breakdown-bar-wrap{height:4px;background:rgba(0,0,0,.15);border-radius:2px;overflow:hidden}
333
- .breakdown-bar{height:100%;border-radius:2px;background:linear-gradient(90deg,#3b82f6,#60a5fa);transition:width .5s ease}
334
- .metrics-toolbar{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap}
335
- .metrics-toolbar .range-btn{padding:6px 14px;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text-sec);font-size:12px;font-weight:600;cursor:pointer;transition:all .15s}
336
- .metrics-toolbar .range-btn:hover{border-color:var(--pri);color:var(--pri)}
337
- .metrics-toolbar .range-btn.active{background:rgba(255,255,255,.1);color:var(--text);border-color:var(--border)}
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)}
338
577
 
339
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}
340
579
  .theme-toggle .theme-icon-light{display:none}
@@ -342,14 +581,14 @@ input,textarea,select{font-family:inherit;font-size:inherit}
342
581
  [data-theme="light"] .theme-toggle .theme-icon-light{display:inline}
343
582
  [data-theme="light"] .theme-toggle .theme-icon-dark{display:none}
344
583
 
345
- .auth-top-actions{position:absolute;top:16px;right:16px;z-index:10;display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.18);backdrop-filter:blur(10px);border-radius:20px;padding:3px}
346
- .auth-theme-toggle{width:30px;height:30px;border:none;border-radius:50%;background:transparent;color:rgba(255,255,255,.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .2s}
347
- .auth-theme-toggle:hover{background:rgba(255,255,255,.2);color:#fff}
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}
348
587
  .auth-theme-toggle .theme-icon-light{display:none}
349
588
  .auth-theme-toggle .theme-icon-dark{display:inline}
350
- [data-theme="light"] .auth-theme-toggle{color:rgba(0,0,0,.45)}
351
- [data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.08);color:#0f172a}
352
- [data-theme="light"] .auth-top-actions{background:rgba(0,0,0,.06)}
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}
353
592
  [data-theme="light"] .auth-theme-toggle .theme-icon-light{display:inline}
354
593
  [data-theme="light"] .auth-theme-toggle .theme-icon-dark{display:none}
355
594
 
@@ -443,7 +682,10 @@ input,textarea,select{font-family:inherit;font-size:inherit}
443
682
  <nav class="nav-tabs">
444
683
  <button class="tab active" data-view="memories" onclick="switchView('memories')" data-i18n="tab.memories">\u{1F4DA} Memories</button>
445
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>
446
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>
447
689
  </nav>
448
690
  </div>
449
691
  <div class="actions">
@@ -529,12 +771,52 @@ input,textarea,select{font-family:inherit;font-size:inherit}
529
771
  <button class="btn btn-icon" onclick="closeTaskDetail()" title="Close">\u2715</button>
530
772
  </div>
531
773
  <div class="task-detail-meta" id="taskDetailMeta"></div>
774
+ <div class="task-skill-section" id="taskSkillSection"></div>
532
775
  <div class="task-detail-summary" id="taskDetailSummary"></div>
533
776
  <div class="task-detail-chunks-title" data-i18n="tasks.chunks">Related Memories</div>
534
777
  <div class="task-detail-chunks" id="taskDetailChunks"></div>
535
778
  </div>
536
779
  </div>
537
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>
538
820
  <div class="analytics-view" id="analyticsView">
539
821
  <div class="metrics-toolbar">
540
822
  <span style="font-size:12px;color:var(--text-sec);font-weight:600" data-i18n="range">Range</span>
@@ -546,7 +828,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
546
828
  <div class="analytics-cards" id="analyticsCards">
547
829
  <div class="analytics-card"><div class="ac-value" id="mTotal">-</div><div class="ac-label" data-i18n="analytics.total">Total Memories</div></div>
548
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>
549
- <div class="analytics-card violet"><div class="ac-value" id="mTodayCalls">-</div><div class="ac-label" data-i18n="analytics.calls">Viewer Calls Today</div></div>
550
831
  <div class="analytics-card"><div class="ac-value" id="mSessions">-</div><div class="ac-label" data-i18n="analytics.sessions">Sessions</div></div>
551
832
  <div class="analytics-card amber"><div class="ac-value" id="mEmbeddings">-</div><div class="ac-label" data-i18n="analytics.embeddings">Embeddings</div></div>
552
833
  </div>
@@ -554,11 +835,21 @@ input,textarea,select{font-family:inherit;font-size:inherit}
554
835
  <h3><span class="icon">\u{1F4CA}</span> <span data-i18n="chart.writes">Memory Writes per Day</span></h3>
555
836
  <div class="chart-bars" id="chartWrites"></div>
556
837
  </div>
557
- <div class="analytics-section">
558
- <h3><span class="icon">\u{1F50D}</span> <span data-i18n="chart.calls">Viewer API Calls per Day (List / Search)</span></h3>
559
- <div class="chart-bars" id="chartCalls"></div>
560
- <div class="chart-legend"><span><span class="dot pri"></span> <span data-i18n="chart.list">List</span></span><span><span class="dot violet"></span> <span data-i18n="chart.search">Search</span></span></div>
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>
561
851
  </div>
852
+
562
853
  <div class="breakdown-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
563
854
  <div class="analytics-section">
564
855
  <h3><span class="icon">\u{1F464}</span> <span data-i18n="breakdown.role">By Role</span></h3>
@@ -570,6 +861,165 @@ input,textarea,select{font-family:inherit;font-size:inherit}
570
861
  </div>
571
862
  </div>
572
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
+
573
1023
  </div>
574
1024
  </div>
575
1025
 
@@ -592,7 +1042,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
592
1042
  <div class="toast-container" id="toasts"></div>
593
1043
 
594
1044
  <script>
595
- let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30,metricsDays=30;
1045
+ let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=40,metricsDays=30;
596
1046
 
597
1047
  /* ─── i18n ─── */
598
1048
  const I18N={
@@ -631,7 +1081,11 @@ const I18N={
631
1081
  'copy.done':'Copied!',
632
1082
  'tab.memories':'\\u{1F4DA} Memories',
633
1083
  'tab.tasks':'\\u{1F4CB} Tasks',
1084
+ 'tab.skills':'\\u{1F9E0} Skills',
634
1085
  'tab.analytics':'\\u{1F4CA} Analytics',
1086
+ 'skills.total':'Total Skills',
1087
+ 'skills.active':'Active',
1088
+ 'skills.installed':'Installed',
635
1089
  'tasks.total':'Total Tasks',
636
1090
  'tasks.active':'Active',
637
1091
  'tasks.completed':'Completed',
@@ -650,6 +1104,8 @@ const I18N={
650
1104
  'stat.sessions':'Sessions',
651
1105
  'stat.embeddings':'Embeddings',
652
1106
  'stat.days':'Days',
1107
+ 'stat.active':'active',
1108
+ 'stat.deduped':'deduped',
653
1109
  'sidebar.sessions':'Sessions',
654
1110
  'sidebar.allsessions':'All Sessions',
655
1111
  'sidebar.clear':'\\u{1F5D1} Clear All Data',
@@ -675,6 +1131,16 @@ const I18N={
675
1131
  'card.expand':'Expand',
676
1132
  'card.edit':'Edit',
677
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',
678
1144
  'pagination.total':' total',
679
1145
  'range':'Range',
680
1146
  'range.days':'days',
@@ -687,6 +1153,7 @@ const I18N={
687
1153
  'chart.calls':'Viewer API Calls per Day (List / Search)',
688
1154
  'chart.nodata':'No data in this range',
689
1155
  'chart.nocalls':'No viewer calls in this range',
1156
+ 'chart.toolperf':'Tool Response Time',
690
1157
  'chart.list':'List',
691
1158
  'chart.search':'Search',
692
1159
  'breakdown.role':'By Role',
@@ -716,7 +1183,69 @@ const I18N={
716
1183
  'confirm.clearall2':'Are you absolutely sure?',
717
1184
  'embed.on':'Embedding: ',
718
1185
  'embed.off':'No embedding model',
719
- 'lang.switch':'中'
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'
720
1249
  },
721
1250
  zh:{
722
1251
  'title':'OpenClaw 记忆',
@@ -753,7 +1282,11 @@ const I18N={
753
1282
  'copy.done':'已复制!',
754
1283
  'tab.memories':'\\u{1F4DA} 记忆',
755
1284
  'tab.tasks':'\\u{1F4CB} 任务',
1285
+ 'tab.skills':'\\u{1F9E0} 技能',
756
1286
  'tab.analytics':'\\u{1F4CA} 分析',
1287
+ 'skills.total':'技能总数',
1288
+ 'skills.active':'生效中',
1289
+ 'skills.installed':'已安装',
757
1290
  'tasks.total':'任务总数',
758
1291
  'tasks.active':'进行中',
759
1292
  'tasks.completed':'已完成',
@@ -772,6 +1305,8 @@ const I18N={
772
1305
  'stat.sessions':'会话',
773
1306
  'stat.embeddings':'嵌入',
774
1307
  'stat.days':'天数',
1308
+ 'stat.active':'活跃',
1309
+ 'stat.deduped':'已去重',
775
1310
  'sidebar.sessions':'会话列表',
776
1311
  'sidebar.allsessions':'全部会话',
777
1312
  'sidebar.clear':'\\u{1F5D1} 清除所有数据',
@@ -797,6 +1332,16 @@ const I18N={
797
1332
  'card.expand':'展开',
798
1333
  'card.edit':'编辑',
799
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':'新摘要',
800
1345
  'pagination.total':' 条',
801
1346
  'range':'范围',
802
1347
  'range.days':'天',
@@ -809,6 +1354,7 @@ const I18N={
809
1354
  'chart.calls':'每日查看器 API 调用(列表 / 搜索)',
810
1355
  'chart.nodata':'此范围内暂无数据',
811
1356
  'chart.nocalls':'此范围内暂无查看器调用',
1357
+ 'chart.toolperf':'工具响应耗时',
812
1358
  'chart.list':'列表',
813
1359
  'chart.search':'搜索',
814
1360
  'breakdown.role':'按角色',
@@ -838,7 +1384,69 @@ const I18N={
838
1384
  'confirm.clearall2':'你真的确定吗?',
839
1385
  'embed.on':'嵌入模型:',
840
1386
  'embed.off':'无嵌入模型',
841
- 'lang.switch':'EN'
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':'未命名'
842
1450
  }
843
1451
  };
844
1452
  const LANG_KEY='memos-viewer-lang';
@@ -950,20 +1558,226 @@ function switchView(view){
950
1558
  const feedWrap=document.getElementById('feedWrap');
951
1559
  const analyticsView=document.getElementById('analyticsView');
952
1560
  const tasksView=document.getElementById('tasksView');
1561
+ const skillsView=document.getElementById('skillsView');
1562
+ const logsView=document.getElementById('logsView');
1563
+ const settingsView=document.getElementById('settingsView');
953
1564
  feedWrap.classList.add('hide');
954
1565
  analyticsView.classList.remove('show');
955
1566
  tasksView.classList.remove('show');
1567
+ skillsView.classList.remove('show');
1568
+ logsView.classList.remove('show');
1569
+ settingsView.classList.remove('show');
956
1570
  if(view==='analytics'){
957
1571
  analyticsView.classList.add('show');
958
1572
  loadMetrics();
959
1573
  } else if(view==='tasks'){
960
1574
  tasksView.classList.add('show');
961
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();
962
1585
  } else {
963
1586
  feedWrap.classList.remove('hide');
964
1587
  }
965
1588
  }
966
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
+
967
1781
  function setMetricsDays(d){
968
1782
  metricsDays=d;
969
1783
  document.querySelectorAll('.metrics-toolbar .range-btn').forEach(btn=>btn.classList.toggle('active',Number(btn.dataset.days)===d));
@@ -975,13 +1789,12 @@ async function loadMetrics(){
975
1789
  const d=await r.json();
976
1790
  document.getElementById('mTotal').textContent=formatNum(d.totals.memories);
977
1791
  document.getElementById('mTodayWrites').textContent=formatNum(d.totals.todayWrites);
978
- document.getElementById('mTodayCalls').textContent=formatNum(d.totals.todayViewerCalls);
979
1792
  document.getElementById('mSessions').textContent=formatNum(d.totals.sessions);
980
1793
  document.getElementById('mEmbeddings').textContent=formatNum(d.totals.embeddings);
981
1794
  renderChartWrites(d.writesPerDay);
982
- renderChartCalls(d.viewerCallsPerDay);
983
1795
  renderBreakdown(d.roleBreakdown,'breakdownRole');
984
1796
  renderBreakdown(d.kindBreakdown,'breakdownKind');
1797
+ loadToolMetrics();
985
1798
  }
986
1799
 
987
1800
  function formatNum(n){return n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'k':String(n);}
@@ -1038,21 +1851,22 @@ async function loadTasks(){
1038
1851
  return '<div class="task-card status-'+task.status+'" onclick="openTaskDetail(\\''+task.id+'\\')">'+
1039
1852
  '<div class="task-card-top">'+
1040
1853
  '<div class="task-card-title">'+esc(task.title)+'</div>'+
1041
- '<span class="task-status-badge '+task.status+'">'+task.status+'</span>'+
1854
+ '<span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span>'+
1042
1855
  '</div>'+
1043
- (task.summary?'<div class="task-card-summary">'+esc(task.summary)+'</div>':'')+
1856
+ (task.summary?'<div class="task-card-summary'+(task.status==='skipped'?' skipped-reason':'')+'">'+esc(task.summary)+'</div>':'')+
1044
1857
  '<div class="task-card-bottom">'+
1045
1858
  '<span class="tag"><span class="icon">\\u{1F4C5}</span> '+timeStr+'</span>'+
1046
1859
  (durationStr?'<span class="tag"><span class="icon">\\u23F1</span> '+durationStr+'</span>':'')+
1047
- '<span class="tag"><span class="icon">\\u{1F4DD}</span> '+task.chunkCount+' '+(task.chunkCount===1?'chunk':'chunks')+'</span>'+
1048
- '<span class="tag"><span class="icon">\\u{1F4C2}</span> '+task.sessionKey.slice(0,12)+'</span>'+
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>'+
1049
1862
  '</div>'+
1050
1863
  '</div>';
1051
1864
  }).join('');
1052
1865
 
1053
1866
  renderTasksPagination(data.total);
1054
1867
  }catch(e){
1055
- list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">Failed to load tasks</div>';
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>';
1056
1870
  }
1057
1871
  }
1058
1872
 
@@ -1075,6 +1889,8 @@ async function openTaskDetail(taskId){
1075
1889
  overlay.classList.add('show');
1076
1890
  document.getElementById('taskDetailTitle').textContent=t('tasks.loading');
1077
1891
  document.getElementById('taskDetailMeta').innerHTML='';
1892
+ document.getElementById('taskSkillSection').innerHTML='';
1893
+ document.getElementById('taskSkillSection').className='task-skill-section';
1078
1894
  document.getElementById('taskDetailSummary').textContent='';
1079
1895
  document.getElementById('taskDetailChunks').innerHTML='<div class="spinner"></div>';
1080
1896
 
@@ -1085,14 +1901,18 @@ async function openTaskDetail(taskId){
1085
1901
  document.getElementById('taskDetailTitle').textContent=task.title||t('tasks.untitled');
1086
1902
 
1087
1903
  const meta=[
1088
- '<span class="meta-item"><span class="task-status-badge '+task.status+'">'+task.status+'</span></span>',
1904
+ '<span class="meta-item"><span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span></span>',
1089
1905
  '<span class="meta-item">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>',
1090
1906
  ];
1091
1907
  if(task.endedAt) meta.push('<span class="meta-item">\\u2192 '+formatTime(task.endedAt)+'</span>');
1092
1908
  meta.push('<span class="meta-item">\\u{1F4C2} '+task.sessionKey+'</span>');
1093
- meta.push('<span class="meta-item">\\u{1F4DD} '+task.chunks.length+' chunks</span>');
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>');
1094
1911
  document.getElementById('taskDetailMeta').innerHTML=meta.join('');
1095
1912
 
1913
+ // ── Skill status section ──
1914
+ renderTaskSkillSection(task);
1915
+
1096
1916
  var summaryEl=document.getElementById('taskDetailSummary');
1097
1917
  if(task.status==='skipped'){
1098
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>';
@@ -1104,7 +1924,7 @@ async function openTaskDetail(taskId){
1104
1924
  document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--text-muted);padding:12px;font-size:13px">'+t('tasks.nochunks')+'</div>';
1105
1925
  }else{
1106
1926
  document.getElementById('taskDetailChunks').innerHTML=task.chunks.map(c=>{
1107
- var roleLabel=c.role==='user'?'You':c.role==='assistant'?'Assistant':c.role.toUpperCase();
1927
+ var roleLabel=c.role==='user'?t('tasks.role.user'):c.role==='assistant'?t('tasks.role.assistant'):c.role.toUpperCase();
1108
1928
  return '<div class="task-chunk-item role-'+c.role+'">'+
1109
1929
  '<div class="task-chunk-role '+c.role+'">'+roleLabel+'</div>'+
1110
1930
  '<div class="task-chunk-bubble" onclick="this.classList.toggle(\\\'expanded\\\')">'+esc(c.content)+'</div>'+
@@ -1113,8 +1933,52 @@ async function openTaskDetail(taskId){
1113
1933
  }).join('');
1114
1934
  }
1115
1935
  }catch(e){
1116
- document.getElementById('taskDetailTitle').textContent='Error';
1117
- document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--rose)">Failed to load task details</div>';
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>';
1118
1982
  }
1119
1983
  }
1120
1984
 
@@ -1123,6 +1987,294 @@ function closeTaskDetail(event){
1123
1987
  document.getElementById('taskDetailOverlay').classList.remove('show');
1124
1988
  }
1125
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
+
1126
2278
  function formatDuration(ms){
1127
2279
  const s=Math.floor(ms/1000);
1128
2280
  if(s<60) return s+'s';
@@ -1169,14 +2321,16 @@ function renderBars(el,data,valueKey,H){
1169
2321
  const vals=data.map(d=>d[valueKey]??0);
1170
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;}
1171
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':'';
1172
2326
  el.innerHTML=data.map(r=>{
1173
2327
  const v=r[valueKey]??0;
1174
2328
  const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
1175
2329
  if(v===0){
1176
- return '<div class="chart-bar-wrap"><div class="chart-tip">0</div><div class="chart-bar-col"><div class="chart-bar zero" style="height:2px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
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>';
1177
2331
  }
1178
2332
  const h=Math.max(8,Math.round((v/max)*H));
1179
- return '<div class="chart-bar-wrap"><div class="chart-tip">'+v+'</div><div class="chart-bar-col"><div class="chart-bar" style="height:'+h+'px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
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>';
1180
2334
  }).join('');
1181
2335
  }
1182
2336
 
@@ -1209,6 +2363,182 @@ function renderChartCalls(rows){
1209
2363
  }).join('');
1210
2364
  }
1211
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
+
1212
2542
  function renderBreakdown(obj,containerId){
1213
2543
  const el=document.getElementById(containerId);
1214
2544
  if(!el)return;
@@ -1228,7 +2558,13 @@ async function loadAll(){
1228
2558
  async function loadStats(){
1229
2559
  const r=await fetch('/api/stats');
1230
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);
1231
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
+ }
1232
2568
  document.getElementById('statSessions').textContent=d.totalSessions;
1233
2569
  document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;
1234
2570
  const days=d.timeRange.earliest?Math.max(1,Math.round((new Date(d.timeRange.latest)-new Date(d.timeRange.earliest))/(86400000))):0;
@@ -1350,12 +2686,44 @@ function renderMemories(items){
1350
2686
  const vscore=m._vscore?'<span class="vscore-badge">'+Math.round(m._vscore*100)+'%</span>':'';
1351
2687
  const sid=m.session_key||'';
1352
2688
  const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;
1353
- return '<div class="memory-card">'+
1354
- '<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>'+
1355
2720
  '<div class="card-summary">'+summary+'</div>'+
2721
+ dedupInfo+
1356
2722
  '<div class="card-content" id="content-'+id+'"><pre>'+content+'</pre></div>'+
2723
+ historyHtml+
1357
2724
  '<div class="card-actions">'+
1358
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>':'')+
1359
2727
  '<button class="btn btn-sm btn-ghost" onclick="openEditModal(\\''+id+'\\')">'+t('card.edit')+'</button>'+
1360
2728
  '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteMemory(\\''+id+'\\')">'+t('card.delete')+'</button>'+
1361
2729
  vscore+
@@ -1391,11 +2759,67 @@ function goPage(p){
1391
2759
  document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});
1392
2760
  }
1393
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
+
1394
2767
  function toggleContent(id){
1395
2768
  const el=document.getElementById('content-'+id);
1396
2769
  el.classList.toggle('show');
1397
2770
  }
1398
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
+
1399
2823
  function esc(s){
1400
2824
  if(!s)return'';
1401
2825
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
@@ -1521,6 +2945,18 @@ document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==
1521
2945
  applyI18n();
1522
2946
  checkAuth();
1523
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
+
1524
2960
  </body>
1525
2961
  </html>`;
1526
2962
  //# sourceMappingURL=html.js.map