@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
@@ -10,45 +10,67 @@ export const viewerHTML = `<!DOCTYPE html>
10
10
  <style>
11
11
  *{margin:0;padding:0;box-sizing:border-box}
12
12
  :root{
13
- --bg:#050510;--bg-card:rgba(255,255,255,.04);--bg-card-hover:rgba(255,255,255,.07);
14
- --border:rgba(255,255,255,.08);--border-glow:rgba(0,187,238,.25);
15
- --text:#f0f4f8;--text-sec:#8b95a5;--text-muted:#5a6373;
16
- --pri:#00bbee;--pri-glow:rgba(0,187,238,.15);--pri-dark:#0088aa;
17
- --pri-grad:linear-gradient(135deg,#00bbee,#00a0cc);
18
- --accent:#e63946;--accent-glow:rgba(230,57,70,.15);
19
- --green:#10b981;--green-bg:rgba(16,185,129,.12);
20
- --amber:#f59e0b;--amber-bg:rgba(245,158,11,.12);
21
- --violet:#8b5cf6;--rose:#f43f5e;--rose-bg:rgba(244,63,94,.12);
22
- --shadow-sm:0 1px 2px rgba(0,0,0,.2);--shadow:0 4px 12px rgba(0,0,0,.25);
23
- --shadow-lg:0 20px 40px rgba(0,0,0,.35);
13
+ --bg:#0b0d11;--bg-card:#12141a;--bg-card-hover:#1a1d25;
14
+ --border:rgba(255,255,255,.08);--border-glow:rgba(255,255,255,.14);
15
+ --text:#e8eaed;--text-sec:#8b8fa4;--text-muted:#555a6e;
16
+ --pri:#818cf8;--pri-glow:rgba(129,140,248,.1);--pri-dark:#6366f1;
17
+ --pri-grad:linear-gradient(135deg,#818cf8,#6366f1);
18
+ --accent:#ef4444;--accent-glow:rgba(239,68,68,.1);
19
+ --green:#34d399;--green-bg:rgba(52,211,153,.08);
20
+ --amber:#fbbf24;--amber-bg:rgba(251,191,36,.08);
21
+ --violet:#818cf8;--rose:#ef4444;--rose-bg:rgba(239,68,68,.08);
22
+ --shadow-sm:0 1px 2px rgba(0,0,0,.3);--shadow:0 4px 12px rgba(0,0,0,.35);
23
+ --shadow-lg:0 20px 40px rgba(0,0,0,.45);
24
24
  --radius:12px;--radius-lg:14px;--radius-xl:18px;
25
25
  }
26
26
  [data-theme="light"]{
27
- --bg:#f1f5f9;--bg-card:#fff;--bg-card-hover:#f8fafc;
28
- --border:rgba(0,0,0,.08);--border-glow:rgba(0,187,238,.4);
29
- --text:#0f172a;--text-sec:#475569;--text-muted:#64748b;
30
- --pri:#0891b2;--pri-glow:rgba(8,145,178,.12);--pri-dark:#0e7490;
31
- --pri-grad:linear-gradient(135deg,#0891b2,#0e7490);
32
- --accent:#dc2626;--accent-glow:rgba(220,38,38,.1);
33
- --green:#059669;--green-bg:rgba(5,150,105,.1);
34
- --amber:#d97706;--amber-bg:rgba(217,119,6,.1);
35
- --violet:#7c3aed;--rose:#e11d48;--rose-bg:rgba(225,29,72,.1);
36
- --shadow-sm:0 1px 2px rgba(0,0,0,.06);--shadow:0 4px 12px rgba(0,0,0,.08);
37
- --shadow-lg:0 20px 40px rgba(0,0,0,.12);
38
- }
39
- [data-theme="light"] .auth-screen{background:linear-gradient(135deg,#e0f2fe 0%,#f0f9ff 50%,#e0e7ff 100%)}
40
- [data-theme="light"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.12)}
41
- [data-theme="light"] .topbar{background:rgba(255,255,255,.95);border-bottom-color:var(--border)}
42
- [data-theme="light"] .session-item .count,[data-theme="light"] .kind-tag,[data-theme="light"] .session-tag{background:rgba(0,0,0,.06)}
43
- [data-theme="light"] .card-content pre{background:rgba(0,0,0,.05);border-color:var(--border)}
44
- [data-theme="light"] .vscore-badge{background:rgba(59,130,246,.1);color:#3b82f6}
45
- [data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2)}
46
- [data-theme="light"] .analytics-card{background:linear-gradient(145deg,#fff 0%,#f8fafc 100%);border-color:rgba(0,0,0,.08)}
47
- [data-theme="light"] .analytics-section{background:#fff;border-color:rgba(0,0,0,.08)}
48
- [data-theme="light"] .breakdown-item{background:rgba(0,0,0,.04)}
49
- [data-theme="light"] .metrics-toolbar .range-btn{background:#fff;border-color:rgba(0,0,0,.1)}
50
- [data-theme="light"] .metrics-toolbar .range-btn.active{background:rgba(0,0,0,.06);color:var(--text);border-color:rgba(0,0,0,.15)}
51
- [data-theme="light"] .metrics-toolbar .range-btn:hover{border-color:var(--pri);color:var(--pri)}
27
+ --bg:#f8f9fb;--bg-card:#fff;--bg-card-hover:#f3f4f6;
28
+ --border:#e2e4e9;--border-glow:#cbd0d8;
29
+ --text:#111827;--text-sec:#4b5563;--text-muted:#9ca3af;
30
+ --pri:#4f46e5;--pri-glow:rgba(79,70,229,.06);--pri-dark:#4338ca;
31
+ --pri-grad:linear-gradient(135deg,#4f46e5,#4338ca);
32
+ --accent:#dc2626;--accent-glow:rgba(220,38,38,.06);
33
+ --green:#059669;--green-bg:rgba(5,150,105,.06);
34
+ --amber:#d97706;--amber-bg:rgba(217,119,6,.06);
35
+ --violet:#4f46e5;--rose:#dc2626;--rose-bg:rgba(220,38,38,.06);
36
+ --shadow-sm:0 1px 2px rgba(0,0,0,.04);--shadow:0 4px 12px rgba(0,0,0,.06);
37
+ --shadow-lg:0 20px 40px rgba(0,0,0,.1);
38
+ }
39
+ [data-theme="light"] .auth-screen{background:linear-gradient(135deg,#f0f4ff 0%,#f8f9fb 50%,#eef2ff 100%)}
40
+ [data-theme="light"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.08)}
41
+ [data-theme="light"] .topbar{background:rgba(255,255,255,.92);border-bottom-color:var(--border);backdrop-filter:blur(8px)}
42
+ [data-theme="light"] .session-item .count,[data-theme="light"] .kind-tag,[data-theme="light"] .session-tag{background:rgba(0,0,0,.05)}
43
+ [data-theme="light"] .card-content pre{background:#f3f4f6;border-color:var(--border)}
44
+ [data-theme="light"] .vscore-badge{background:rgba(79,70,229,.06);color:#4f46e5}
45
+ [data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15)}
46
+ [data-theme="light"] .analytics-card{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.06);border:1px solid var(--border)}
47
+ [data-theme="light"] .analytics-card::before{background:none}
48
+ [data-theme="light"] .analytics-card::after{display:none}
49
+ [data-theme="light"] .analytics-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.08);transform:translateY(-2px)}
50
+ [data-theme="light"] .analytics-card.green{background:#fff;border-color:var(--border)}
51
+ [data-theme="light"] .analytics-card.green::before{background:none}
52
+ [data-theme="light"] .analytics-card.amber{background:#fff;border-color:var(--border)}
53
+ [data-theme="light"] .analytics-card.amber::before{background:none}
54
+ [data-theme="light"] .analytics-card .ac-value{-webkit-text-fill-color:unset;background:none;color:#111827}
55
+ [data-theme="light"] .analytics-card.green .ac-value{color:#059669}
56
+ [data-theme="light"] .analytics-card.amber .ac-value{color:#d97706}
57
+ [data-theme="light"] .analytics-section{background:#fff;border-color:var(--border);box-shadow:0 1px 3px rgba(0,0,0,.04)}
58
+ [data-theme="light"] .analytics-section::before{background:none}
59
+ [data-theme="light"] .chart-bar{box-shadow:none}
60
+ [data-theme="light"] .chart-bar:hover{box-shadow:0 2px 8px rgba(79,70,229,.15)}
61
+ [data-theme="light"] .tool-chart-tooltip{background:rgba(17,24,39,.92);color:#e8eaed;border-color:rgba(99,102,241,.3);box-shadow:0 8px 24px rgba(0,0,0,.2)}
62
+ [data-theme="light"] .tool-chart-tooltip .tt-time{color:#a5b4fc}
63
+ [data-theme="light"] .tool-chart-tooltip .tt-val{color:#e8eaed}
64
+ [data-theme="light"] .tool-agg-table td{background:transparent}
65
+ [data-theme="light"] .tool-agg-table tr:hover td{background:rgba(79,70,229,.03)}
66
+ [data-theme="light"] .tool-agg-table th{color:#9ca3af}
67
+ [data-theme="light"] .breakdown-item{background:#f9fafb;border-color:var(--border)}
68
+ [data-theme="light"] .breakdown-item:hover{background:#f3f4f6;border-color:#cbd5e1}
69
+ [data-theme="light"] .breakdown-bar-wrap{background:#e5e7eb}
70
+ [data-theme="light"] .breakdown-bar{background:linear-gradient(90deg,#4f46e5,#6366f1);box-shadow:none}
71
+ [data-theme="light"] .range-btn{background:transparent;border-color:var(--border);color:var(--text-sec)}
72
+ [data-theme="light"] .range-btn.active{background:rgba(79,70,229,.06);color:#4f46e5;border-color:rgba(79,70,229,.2)}
73
+ [data-theme="light"] .range-btn:hover{border-color:#4f46e5;color:#4f46e5}
52
74
  body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .2s,color .2s}
53
75
  button{cursor:pointer;font-family:inherit;font-size:inherit}
54
76
  input,textarea,select{font-family:inherit;font-size:inherit}
@@ -61,28 +83,28 @@ input,textarea,select{font-family:inherit;font-size:inherit}
61
83
  .auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}
62
84
  .auth-card input{width:100%;padding:12px 16px;border:1px solid hsl(0 0% 89.8%);border-radius:8px;font-size:14px;transition:all .2s;margin-bottom:10px;outline:none;background:#fff;color:hsl(0 0% 3.9%)}
63
85
  .auth-card input::placeholder{color:hsl(0 0% 45.1%)}
64
- .auth-card input:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.15)}
65
- .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}
66
- .auth-card .btn-auth:hover{background:#2563eb;transform:translateY(-1px);box-shadow:0 8px 25px rgba(59,130,246,.3)}
86
+ .auth-card input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}
87
+ .auth-card .btn-auth{width:100%;padding:11px;border:1px solid var(--pri);border-radius:8px;background:rgba(99,102,241,.06);color:var(--pri);font-weight:600;font-size:14px;transition:all .15s}
88
+ .auth-card .btn-auth:hover{background:rgba(99,102,241,.12);border-color:var(--pri-dark)}
67
89
  .auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}
68
90
  .auth-card .btn-text{color:hsl(0 0% 45.1%)}
69
- .auth-card .btn-text:hover{color:#3b82f6}
91
+ .auth-card .btn-text:hover{color:var(--pri)}
70
92
 
71
93
  .reset-guide{text-align:left;margin-bottom:20px}
72
94
  .reset-step{display:flex;gap:14px;margin-bottom:16px}
73
- .step-num{width:28px;height:28px;border-radius:50%;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}
95
+ .step-num{width:28px;height:28px;border-radius:50%;background:var(--pri);color:#fff;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}
74
96
  .step-body{flex:1;min-width:0}
75
97
  .step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}
76
98
  .step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}
77
99
  .cmd-box{margin-top:8px;background:hsl(0 0% 96.1%);border:1px solid hsl(0 0% 89.8%);border-radius:8px;padding:12px 14px;font-size:12px;font-family:ui-monospace,monospace;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:space-between;gap:8px;word-break:break-all;color:hsl(0 0% 3.9%)}
78
- .cmd-box:hover{border-color:rgb(168,85,247);background:rgba(168,85,247,.08)}
100
+ .cmd-box:hover{border-color:hsl(0 0% 70%);background:hsl(0 0% 96.1%)}
79
101
  .cmd-box code{flex:1}
80
102
  .copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}
81
103
  .cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}
82
104
 
83
105
  /* ─── App Layout (dark dashboard, same as www) ─── */
84
106
  .app{display:none;flex-direction:column;min-height:100vh}
85
- .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)}
107
+ .topbar{background:rgba(11,13,17,.88);border-bottom:1px solid var(--border);padding:0 28px;height:56px;display:flex;align-items:center;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px)}
86
108
  .topbar .brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}
87
109
  .topbar .brand .icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:22px;background:none;border-radius:0}
88
110
  .topbar .brand .sub{font-weight:400;color:var(--text-muted);font-size:11px}
@@ -123,9 +145,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
123
145
  .search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}
124
146
 
125
147
  .filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
126
- .filter-chip{padding:6px 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;transition:all .15s}
148
+ .filter-chip{padding:5px 14px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-sec);font-size:12px;font-weight:500;transition:all .15s}
127
149
  .filter-chip:hover{border-color:var(--pri);color:var(--pri)}
128
- .filter-chip.active{background:var(--pri);color:#000;border-color:var(--pri)}
150
+ .filter-chip.active{background:rgba(99,102,241,.08);color:var(--pri);border-color:rgba(99,102,241,.25)}
129
151
 
130
152
  .memory-list{display:flex;flex-direction:column;gap:16px}
131
153
  .memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}
@@ -133,7 +155,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
133
155
  .memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}
134
156
  .memory-card .meta{display:flex;align-items:center;gap:8px}
135
157
  .role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}
136
- .role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(0,187,238,.2)}
158
+ .role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(99,102,241,.12)}
137
159
  .role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}
138
160
  .role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}
139
161
  .kind-tag{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-sec);background:rgba(0,0,0,.2);font-weight:500}
@@ -145,6 +167,36 @@ input,textarea,select{font-family:inherit;font-size:inherit}
145
167
  .card-content pre{white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.25);padding:14px;border-radius:10px;font-size:12px;font-family:ui-monospace,monospace;margin-top:10px;border:1px solid var(--border);color:var(--text-sec)}
146
168
  .card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}
147
169
  .vscore-badge{display:inline-flex;align-items:center;background:rgba(59,130,246,.15);color:#60a5fa;font-size:10px;font-weight:700;padding:4px 10px;border-radius:8px;margin-left:auto}
170
+ .merge-badge{display:inline-flex;align-items:center;gap:4px;background:rgba(16,185,129,.12);color:#10b981;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
171
+ .merge-history{margin-top:12px;padding:12px 14px;background:rgba(0,0,0,.15);border-radius:10px;border:1px solid var(--border);font-size:12px;line-height:1.7;color:var(--text-sec);max-height:200px;overflow-y:auto}
172
+ .merge-history-item{padding:6px 0;border-bottom:1px dashed rgba(255,255,255,.06)}
173
+ .merge-history-item:last-child{border-bottom:none}
174
+ .merge-action{font-weight:600;font-size:11px;padding:2px 6px;border-radius:4px}
175
+ .merge-action.UPDATE{background:rgba(59,130,246,.15);color:#60a5fa}
176
+ .merge-action.DUPLICATE{background:rgba(245,158,11,.15);color:#f59e0b}
177
+ .card-updated{font-size:11px;color:var(--text-muted);margin-left:6px}
178
+ .dedup-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
179
+ .dedup-badge.duplicate{background:rgba(245,158,11,.12);color:#f59e0b}
180
+ .dedup-badge.merged{background:rgba(59,130,246,.12);color:#60a5fa}
181
+ .memory-card.dedup-inactive{opacity:.55;border-style:dashed}
182
+ .memory-card.dedup-inactive:hover{opacity:.85}
183
+ .dedup-target-link{font-size:11px;color:var(--pri);cursor:pointer;text-decoration:underline;margin-left:4px}
184
+ .memory-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9999;display:none;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
185
+ .memory-modal-overlay.show{display:flex}
186
+ .memory-modal{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;width:min(600px,90vw);max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.4);animation:modalIn .2s ease-out}
187
+ @keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}
188
+ .memory-modal-title{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);font-size:14px;font-weight:700}
189
+ .memory-modal-body{padding:20px;overflow-y:auto;flex:1}
190
+ .modal-memory-card{display:flex;flex-direction:column;gap:14px}
191
+ .modal-header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
192
+ .modal-field{display:flex;flex-direction:column;gap:4px}
193
+ .modal-field-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec)}
194
+ .modal-field-val{font-size:13px;color:var(--text);line-height:1.5}
195
+ .modal-field-content{font-family:'SF Mono',Consolas,monospace;font-size:12px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.15);border-radius:8px;padding:12px;max-height:240px;overflow-y:auto;margin:0}
196
+ [data-theme="light"] .modal-field-content{background:rgba(0,0,0,.04)}
197
+ .modal-meta-row{display:flex;flex-wrap:wrap;gap:12px;font-size:11px;color:var(--text-sec);padding:8px 0;border-top:1px dashed var(--border)}
198
+ [data-theme="light"] .merge-history{background:rgba(0,0,0,.04)}
199
+ [data-theme="light"] .merge-history-item{border-bottom-color:rgba(0,0,0,.06)}
148
200
 
149
201
  /* ─── Buttons ─── */
150
202
  .btn{padding:7px 14px;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;font-weight:500;transition:all .18s ease;display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
@@ -182,7 +234,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
182
234
  .toast{padding:14px 20px;border-radius:10px;font-size:13px;font-weight:500;box-shadow:var(--shadow-lg);animation:slideIn .3s ease;display:flex;align-items:center;gap:10px;max-width:360px;border:1px solid}
183
235
  .toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}
184
236
  .toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}
185
- .toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(0,187,238,.3)}
237
+ .toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(99,102,241,.15)}
186
238
  @keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}
187
239
 
188
240
  .empty{text-align:center;padding:64px 20px;color:var(--text-sec)}
@@ -239,6 +291,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
239
291
  .task-status-badge.skipped{color:var(--text-muted);background:rgba(128,128,128,.15)}
240
292
  .task-card-summary{font-size:13px;color:var(--text-sec);line-height:1.5;margin-bottom:10px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
241
293
  .task-card-summary:empty{display:none}
294
+ .task-card-summary.skipped-reason{background:rgba(128,128,128,.08);border-radius:6px;padding:6px 10px;border-left:3px solid var(--text-muted)}
242
295
  .task-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted)}
243
296
  .task-card-bottom .tag{display:flex;align-items:center;gap:4px}
244
297
  .task-card-bottom .tag .icon{font-size:12px}
@@ -279,6 +332,70 @@ input,textarea,select{font-family:inherit;font-size:inherit}
279
332
  [data-theme="light"] .task-card{background:#fff}
280
333
  [data-theme="light"] .tasks-stat{background:#fff}
281
334
 
335
+ /* ─── Skills ─── */
336
+ .skills-view{display:none;flex:1;min-width:0;flex-direction:column;gap:16px}
337
+ .skills-view.show{display:flex}
338
+ .skill-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:18px 20px;cursor:pointer;transition:all .25s;position:relative;overflow:hidden}
339
+ .skill-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover);transform:translateY(-1px);box-shadow:var(--shadow)}
340
+ .skill-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px;background:var(--violet)}
341
+ .skill-card.installed::before{background:var(--green)}
342
+ .skill-card.archived{opacity:.5}
343
+ .skill-card.archived::before{background:var(--text-muted)}
344
+ .skill-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:6px}
345
+ .skill-card-name{font-size:15px;font-weight:700;color:var(--text);flex:1}
346
+ .skill-card-badges{display:flex;gap:6px;align-items:center}
347
+ .skill-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:3px 10px;border-radius:20px}
348
+ .skill-badge.version{color:var(--violet);background:rgba(139,92,246,.15)}
349
+ .skill-badge.installed{color:var(--green);background:var(--green-bg)}
350
+ .skill-badge.status-active{color:var(--pri);background:var(--pri-glow)}
351
+ .skill-badge.status-archived{color:var(--text-muted);background:rgba(128,128,128,.15)}
352
+ .skill-badge.status-draft{color:var(--amber);background:var(--amber-bg)}
353
+ .skill-badge.quality{font-size:10px;font-weight:700;padding:3px 10px;border-radius:20px}
354
+ .skill-badge.quality.high{color:var(--green);background:var(--green-bg)}
355
+ .skill-badge.quality.mid{color:var(--amber);background:var(--amber-bg)}
356
+ .skill-badge.quality.low{color:var(--rose);background:var(--rose-bg)}
357
+ .skill-card.draft{opacity:.75}
358
+ .skill-card.draft::before{background:var(--amber)}
359
+ .skill-card-desc{font-size:13px;color:var(--text-sec);line-height:1.5;margin-bottom:10px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
360
+ .skill-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted);flex-wrap:wrap}
361
+ .skill-card-bottom .tag{display:flex;align-items:center;gap:4px}
362
+ .skill-card-tags{display:flex;gap:4px;flex-wrap:wrap}
363
+ .skill-tag{font-size:10px;padding:2px 8px;border-radius:10px;background:rgba(139,92,246,.1);color:var(--violet);font-weight:500}
364
+ .skill-detail-desc{font-size:13px;color:var(--text-sec);line-height:1.6;margin-bottom:16px;padding:12px 16px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius)}
365
+ .skill-version-item{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px}
366
+ .skill-version-header{display:flex;align-items:center;gap:10px;margin-bottom:6px}
367
+ .skill-version-badge{font-size:11px;font-weight:700;color:var(--violet);background:rgba(139,92,246,.12);padding:2px 8px;border-radius:8px}
368
+ .skill-version-type{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em}
369
+ .skill-version-changelog{font-size:12px;color:var(--text);line-height:1.5;font-weight:600}
370
+ .skill-version-summary{font-size:12px;color:var(--text-sec);line-height:1.6;margin-top:6px;padding:8px 12px;background:rgba(139,92,246,.04);border-left:2px solid rgba(139,92,246,.2);border-radius:0 6px 6px 0}
371
+ .skill-version-time{font-size:10px;color:var(--text-muted);margin-top:4px}
372
+ .skill-related-task{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:all .2s}
373
+ .skill-related-task:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}
374
+ .skill-related-task .relation{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em;min-width:80px}
375
+ .skill-related-task .task-title{font-size:13px;color:var(--text);flex:1}
376
+ .skill-files-list{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}
377
+ .skill-file-item{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;font-size:12px}
378
+ .skill-file-icon{font-size:14px;width:20px;text-align:center}
379
+ .skill-file-name{flex:1;color:var(--text);font-family:SF Mono,Monaco,Consolas,monospace}
380
+ .skill-file-type{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em}
381
+ .skill-file-size{font-size:10px;color:var(--text-muted)}
382
+ .skill-download-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:8px;background:var(--pri-grad);color:#fff;font-size:12px;font-weight:600;border:none;cursor:pointer;transition:all .2s}
383
+ .skill-download-btn:hover{opacity:.85;transform:translateY(-1px)}
384
+ .task-skill-section{margin-bottom:16px;padding:14px 16px;border-radius:var(--radius);border:1px solid var(--border)}
385
+ .task-skill-section.status-generated{border-color:var(--green);background:var(--green-bg)}
386
+ .task-skill-section.status-generating{border-color:var(--amber);background:var(--amber-bg)}
387
+ .task-skill-section.status-not_generated,.task-skill-section.status-skipped{border-color:var(--border);background:var(--bg-card)}
388
+ .task-skill-section .skill-status-header{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:13px;font-weight:600;color:var(--text)}
389
+ .task-skill-section .skill-status-reason{font-size:12px;color:var(--text-sec);line-height:1.5}
390
+ .task-skill-section .skill-link-card{margin-top:10px;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;cursor:pointer;transition:all .2s}
391
+ .task-skill-section .skill-link-card:hover{border-color:var(--pri);background:var(--bg-card-hover)}
392
+ .task-skill-section .skill-link-name{font-size:13px;font-weight:600;color:var(--pri)}
393
+ .task-skill-section .skill-link-meta{font-size:11px;color:var(--text-sec);margin-top:4px}
394
+ .task-id-full{font-family:monospace;font-size:11px;color:var(--text-muted);word-break:break-all;user-select:all;cursor:text;padding:2px 6px;background:var(--bg-card);border-radius:4px;border:1px solid var(--border)}
395
+ [data-theme="light"] .skill-card{background:#fff}
396
+ [data-theme="light"] .skill-detail-desc{background:#f8fafc}
397
+ [data-theme="light"] .skill-version-item{background:#f8fafc}
398
+
282
399
  /* ─── Analytics / 统计 ─── */
283
400
  .nav-tabs{display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.06);border-radius:10px;padding:3px}
284
401
  .nav-tabs .tab{padding:6px 20px;border-radius:8px;font-size:13px;font-weight:600;color:var(--text-sec);background:transparent;border:1px solid transparent;cursor:pointer;transition:all .2s;white-space:nowrap}
@@ -286,52 +403,174 @@ input,textarea,select{font-family:inherit;font-size:inherit}
286
403
  .nav-tabs .tab.active{color:var(--text);background:rgba(255,255,255,.1);border-color:var(--border);box-shadow:0 1px 4px rgba(0,0,0,.15)}
287
404
  [data-theme="light"] .nav-tabs{background:rgba(0,0,0,.05)}
288
405
  [data-theme="light"] .nav-tabs .tab.active{background:#fff;border-color:rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.08);color:var(--text)}
289
- .analytics-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
290
- .analytics-view.show{display:flex}
406
+ .analytics-view,.settings-view,.logs-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
407
+ .analytics-view.show,.settings-view.show,.logs-view.show{display:flex}
408
+
409
+ /* ─── Logs ─── */
410
+ .logs-toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 0}
411
+ .logs-toolbar-left{display:flex;align-items:center;gap:8px}
412
+ .logs-toolbar-right{display:flex;align-items:center;gap:8px}
413
+ .logs-list{display:flex;flex-direction:column;gap:8px;overflow-y:auto;flex:1;min-height:0}
414
+ .log-entry{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;transition:border-color .2s}
415
+ .log-entry:hover{border-color:var(--border-glow)}
416
+ .log-header{display:flex;align-items:center;gap:10px;padding:12px 16px;cursor:pointer;user-select:none;transition:background .15s}
417
+ .log-header:hover{background:rgba(255,255,255,.03)}
418
+ [data-theme="light"] .log-header:hover{background:rgba(0,0,0,.02)}
419
+ .log-tool-badge{font-family:'SF Mono',Consolas,monospace;font-size:11px;font-weight:700;padding:3px 8px;border-radius:4px;white-space:nowrap;letter-spacing:.3px}
420
+ .log-tool-badge.memory_search{background:rgba(59,130,246,.15);color:#60a5fa}
421
+ .log-tool-badge.memory_add{background:rgba(168,85,247,.15);color:#c084fc}
422
+ .log-tool-badge.auto_recall{background:rgba(168,85,247,.15);color:#c084fc}
423
+ .log-tool-badge.memory_timeline{background:rgba(34,197,94,.15);color:#4ade80}
424
+ .log-tool-badge.memory_get{background:rgba(251,146,60,.15);color:#fb923c}
425
+ .log-tool-badge.task_summary{background:rgba(245,158,11,.15);color:#fbbf24}
426
+ .log-tool-badge.skill_get{background:rgba(236,72,153,.15);color:#f472b6}
427
+ .log-tool-badge.skill_install{background:rgba(14,165,233,.15);color:#38bdf8}
428
+ .log-tool-badge.memory_viewer{background:rgba(100,116,139,.15);color:#94a3b8}
429
+ .log-dur{font-family:'SF Mono',Consolas,monospace;font-size:10px;color:var(--text-sec);opacity:.7}
430
+ .log-time{margin-left:auto;font-size:11px;color:var(--text-sec);font-family:'SF Mono',Consolas,monospace;white-space:nowrap}
431
+ .log-status{width:7px;height:7px;border-radius:50%;flex-shrink:0}
432
+ .log-status.ok{background:#4ade80;box-shadow:0 0 4px rgba(74,222,128,.5)}
433
+ .log-status.fail{background:#f87171;box-shadow:0 0 4px rgba(248,113,113,.5)}
434
+ .log-summary{padding:8px 16px 10px;font-size:12px;color:var(--text-sec);line-height:1.5}
435
+ .log-summary-kv{display:inline-flex;align-items:center;gap:4px;margin-right:12px;font-size:11px}
436
+ .log-summary-kv .kv-label{color:var(--text-sec);opacity:.7}
437
+ .log-summary-kv .kv-val{color:var(--text);font-family:'SF Mono',Consolas,monospace;font-size:11px}
438
+ .log-summary-query{margin-top:4px;padding:6px 10px;background:rgba(59,130,246,.08);border-radius:6px;font-size:12px;color:var(--text);border-left:3px solid rgba(59,130,246,.4);line-height:1.4}
439
+ .log-summary-stats{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}
440
+ .log-stat-chip{display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:600;font-family:'SF Mono',Consolas,monospace}
441
+ .log-stat-chip.stored{background:rgba(74,222,128,.12);color:#4ade80}
442
+ .log-stat-chip.skipped{background:rgba(100,116,139,.12);color:#94a3b8}
443
+ .log-stat-chip.dedup{background:rgba(251,146,60,.12);color:#fb923c}
444
+ .log-stat-chip.merged{background:rgba(168,85,247,.12);color:#c084fc}
445
+ .log-stat-chip.errors{background:rgba(248,113,113,.12);color:#f87171}
446
+ .log-msg-list{margin-top:8px;display:flex;flex-direction:column;gap:4px}
447
+ .log-msg-item{display:flex;gap:8px;align-items:flex-start;font-size:11.5px;line-height:1.5;padding:4px 10px;border-radius:6px;background:rgba(255,255,255,.02)}
448
+ [data-theme="light"] .log-msg-item{background:rgba(0,0,0,.02)}
449
+ .log-msg-role{flex-shrink:0;font-size:10px;font-weight:600;padding:1px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.3px}
450
+ .log-msg-role.user{background:rgba(59,130,246,.12);color:#60a5fa}
451
+ .log-msg-role.assistant{background:rgba(168,85,247,.12);color:#c084fc}
452
+ .log-msg-role.system{background:rgba(100,116,139,.12);color:#94a3b8}
453
+ .log-msg-action{flex-shrink:0;font-size:10px;font-weight:600;padding:1px 6px;border-radius:4px}
454
+ .log-msg-action.stored{color:#4ade80}
455
+ .log-msg-action.exact-dup{color:#94a3b8}
456
+ .log-msg-action.dedup{color:#fb923c}
457
+ .log-msg-action.merged{color:#c084fc}
458
+ .log-msg-action.error{color:#f87171}
459
+ .log-msg-text{color:var(--text);opacity:.85;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}
460
+ .log-detail{display:none;border-top:1px solid var(--border);padding:0}
461
+ .log-detail.open{display:block}
462
+ .log-expand-btn{font-size:10px;color:var(--text-sec);opacity:.5;margin-left:auto;transition:transform .2s,opacity .15s;display:inline-block}
463
+ .log-entry.expanded .log-expand-btn{transform:rotate(180deg);opacity:.8}
464
+ .logs-pagination{display:flex;align-items:center;justify-content:center;gap:4px;padding:12px 0;flex-wrap:wrap}
465
+ .logs-pagination .btn{min-width:32px;padding:4px 8px;font-size:12px}
466
+ .logs-pagination .btn-primary{background:var(--primary);color:#fff;border-color:var(--primary)}
467
+ .logs-pagination .page-ellipsis{color:var(--text-sec);font-size:12px;padding:0 4px}
468
+ .logs-pagination .page-total{font-size:11px;color:var(--text-sec);margin-left:8px}
469
+ .log-io-section{padding:10px 14px}
470
+ .log-io-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec);margin-bottom:6px}
471
+ .log-io-content{font-family:'SF Mono',Consolas,monospace;font-size:11px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.2);border-radius:6px;padding:10px 12px;max-height:300px;overflow-y:auto}
472
+ .log-io-section+.log-io-section{border-top:1px dashed var(--border)}
473
+ [data-theme="light"] .log-io-content{background:rgba(0,0,0,.04)}
474
+ [data-theme="light"] .log-summary-query{background:rgba(59,130,246,.06)}
475
+ .settings-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px 28px}
476
+ .settings-section h3{font-size:13px;font-weight:700;color:var(--text);margin-bottom:16px;display:flex;align-items:center;gap:8px}
477
+ .settings-section h3 .icon{font-size:16px;opacity:.8}
478
+ .settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
479
+ @media(max-width:800px){.settings-grid{grid-template-columns:1fr}}
480
+ .settings-field{display:flex;flex-direction:column;gap:4px}
481
+ .settings-field label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}
482
+ .settings-field input,.settings-field select{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 12px;color:var(--text);font-size:13px;font-family:inherit;transition:border-color .15s}
483
+ .settings-field input:focus,.settings-field select:focus{outline:none;border-color:var(--pri)}
484
+ .settings-field input[type="password"]{font-family:'Courier New',monospace;letter-spacing:.05em}
485
+ .settings-field .field-hint{font-size:10px;color:var(--text-muted);margin-top:2px}
486
+ .settings-field.full-width{grid-column:1/-1}
487
+ .settings-toggle{display:flex;align-items:center;gap:10px;padding:4px 0}
488
+ .settings-toggle label{font-size:12px;font-weight:500;color:var(--text-sec);text-transform:none;letter-spacing:0}
489
+ .toggle-switch{position:relative;width:36px;height:20px;cursor:pointer}
490
+ .toggle-switch input{opacity:0;width:0;height:0}
491
+ .toggle-slider{position:absolute;inset:0;background:var(--border);border-radius:20px;transition:.2s}
492
+ .toggle-slider::before{content:'';position:absolute;height:14px;width:14px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s}
493
+ .toggle-switch input:checked+.toggle-slider{background:var(--pri)}
494
+ .toggle-switch input:checked+.toggle-slider::before{transform:translateX(16px)}
495
+ .settings-actions{display:flex;gap:12px;justify-content:flex-end;align-items:center;margin-top:16px;padding-top:16px;border-top:1px solid var(--border)}
496
+ .settings-actions .btn{min-width:110px;padding:10px 20px;font-size:13px}
497
+ .settings-actions .btn-primary{background:rgba(99,102,241,.08);color:var(--pri);border:1px solid rgba(99,102,241,.25);font-weight:600}
498
+ .settings-actions .btn-primary:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}
499
+ [data-theme="light"] .settings-actions .btn-primary{background:rgba(79,70,229,.06);color:#4f46e5;border:1px solid rgba(79,70,229,.2)}
500
+ [data-theme="light"] .settings-actions .btn-primary:hover{background:rgba(79,70,229,.1);border-color:#4f46e5}
501
+ .settings-saved{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px;font-weight:600;opacity:0;transition:opacity .3s}
502
+ .settings-saved.show{opacity:1}
291
503
  .feed-wrap{flex:1;min-width:0;display:flex;flex-direction:column}
292
504
  .feed-wrap.hide{display:none}
293
- .analytics-cards{display:grid;grid-template-columns:repeat(5,1fr);gap:14px}
294
- .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}
295
- .analytics-card:hover{border-color:var(--border-glow);transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.15)}
296
- .analytics-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,#3b82f6,#60a5fa);opacity:.85}
297
- .analytics-card.green::before{background:linear-gradient(90deg,var(--green),#34d399)}
298
- .analytics-card.amber::before{background:linear-gradient(90deg,var(--amber),#fbbf24)}
299
- .analytics-card.violet::before{background:linear-gradient(90deg,var(--violet),#a78bfa)}
300
- .analytics-card .ac-value{font-size:26px;font-weight:800;letter-spacing:-.03em;color:var(--text);line-height:1.1}
301
- .analytics-card .ac-label{font-size:11px;color:var(--text-muted);margin-top:5px;font-weight:500;text-transform:uppercase;letter-spacing:.04em}
302
- .analytics-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:22px 24px}
303
- .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}
304
- .analytics-section h3 .icon{font-size:14px;opacity:.7}
305
- .chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto}
306
- .chart-bar-wrap{flex:1;min-width:18px;max-width:60px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}
505
+ .analytics-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
506
+ .analytics-card{position:relative;overflow:hidden;border-radius:var(--radius-lg);padding:22px 20px;transition:all .2s ease;border:1px solid var(--border);background:var(--bg-card)}
507
+ .analytics-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--pri);opacity:.5}
508
+ .analytics-card::after{display:none}
509
+ .analytics-card:hover{transform:translateY(-2px);box-shadow:var(--shadow);border-color:var(--border-glow)}
510
+ .analytics-card.green::before{background:var(--green)}
511
+ .analytics-card.amber::before{background:var(--amber)}
512
+ .analytics-card .ac-value{font-size:28px;font-weight:700;letter-spacing:-.03em;color:var(--text);line-height:1;-webkit-text-fill-color:unset;background:none}
513
+ .analytics-card.green .ac-value{color:var(--green);background:none}
514
+ .analytics-card.amber .ac-value{color:var(--amber);background:none}
515
+ .analytics-card .ac-label{font-size:11px;color:var(--text-muted);margin-top:6px;font-weight:500;text-transform:uppercase;letter-spacing:.06em}
516
+ .analytics-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:22px 24px;position:relative;overflow:hidden}
517
+ .analytics-section::before{display:none}
518
+ .analytics-section h3{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:16px;display:flex;align-items:center;gap:8px}
519
+ .analytics-section h3 .icon{font-size:14px;opacity:.6}
520
+ .chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto;justify-content:center}
521
+ .chart-bar-wrap{flex:1;min-width:28px;max-width:80px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}
307
522
  .chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}
308
- .chart-bar-wrap:hover .chart-bar{filter:brightness(1.25)}
309
- .chart-bar-wrap:hover .chart-bar-label{color:var(--pri)}
523
+ .chart-bar-wrap:hover .chart-bar{opacity:1}
524
+ .chart-bar-wrap:hover .chart-bar-label{color:var(--text)}
310
525
  .chart-bar-wrap:hover .chart-tip{opacity:1;transform:translateX(-50%) translateY(0)}
311
- .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}
312
- .chart-bar{width:100%;border-radius:4px 4px 0 0;background:linear-gradient(180deg,#60a5fa,#3b82f6);transition:all .3s ease}
313
- .chart-bar.violet{background:linear-gradient(180deg,var(--violet),#7c3aed)}
314
- .chart-bar.green{background:linear-gradient(180deg,var(--green),#059669)}
315
- .chart-bar.zero{background:var(--border);opacity:.4;border-radius:2px}
526
+ .chart-tip{position:absolute;top:-6px;left:50%;transform:translateX(-50%) translateY(4px);background:var(--bg-card);border:1px solid var(--border-glow);color:var(--text);padding:2px 8px;border-radius:6px;font-size:10px;font-weight:600;white-space:nowrap;z-index:5;pointer-events:none;box-shadow:var(--shadow);opacity:0;transition:all .15s ease}
527
+ .chart-bar{width:100%;border-radius:3px 3px 1px 1px;background:#818cf8;opacity:.75;transition:all .2s ease}
528
+ .chart-bar.violet{background:#6366f1}
529
+ .chart-bar.green{background:var(--green)}
530
+ .chart-bar.zero{background:var(--border);opacity:.3;border-radius:2px}
316
531
  .chart-bar-label{font-size:9px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;text-align:center;transition:color .15s}
317
- .chart-legend{display:flex;gap:16px;margin-top:12px;flex-wrap:wrap;font-size:12px;color:var(--text-sec)}
318
- .chart-legend span{display:inline-flex;align-items:center;gap:6px}
319
- .chart-legend .dot{width:8px;height:8px;border-radius:50%}
532
+ .chart-legend{display:flex;gap:14px;margin-top:12px;flex-wrap:wrap;font-size:11px;color:var(--text-sec);font-weight:500}
533
+ .chart-legend span{display:inline-flex;align-items:center;gap:5px}
534
+ .chart-legend .dot{width:8px;height:8px;border-radius:2px}
320
535
  .chart-legend .dot.pri{background:var(--pri)}
536
+ .tool-chart-svg{width:100%;height:100%;display:block}
537
+ .tool-chart-svg .grid-line{stroke:var(--border);stroke-dasharray:3 3;stroke-width:0.5}
538
+ .tool-chart-svg .axis-label{fill:var(--text-muted);font-size:10px;font-family:var(--mono)}
539
+ .tool-chart-svg .data-line{fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:2000;stroke-dashoffset:2000;animation:lineIn .6s ease forwards}
540
+ @keyframes lineIn{to{stroke-dashoffset:0}}
541
+ .tool-chart-svg .data-area{opacity:1}
542
+ .tool-chart-svg .hover-dot{r:3.5;stroke-width:2;stroke:var(--bg);opacity:0;transition:opacity .1s}
543
+ .tool-chart-svg .hover-dot.show{opacity:1}
544
+ .tool-chart-tooltip{position:absolute;top:0;left:0;background:var(--bg-card);border:1px solid var(--border-glow);color:var(--text);padding:8px 12px;border-radius:8px;font-size:11px;font-family:var(--mono);pointer-events:none;opacity:0;transition:opacity .1s;z-index:10;box-shadow:var(--shadow-lg);white-space:nowrap}
545
+ .tool-chart-tooltip.show{opacity:1}
546
+ .tool-chart-tooltip .tt-time{color:var(--text-muted);font-size:10px;margin-bottom:4px;font-weight:500}
547
+ .tool-chart-tooltip .tt-row{display:flex;align-items:center;gap:6px;margin:2px 0}
548
+ .tool-chart-tooltip .tt-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}
549
+ .tool-chart-tooltip .tt-val{font-weight:600;margin-left:auto;padding-left:12px}
550
+ .tool-agg-table{width:100%;border-collapse:collapse;font-size:12px}
551
+ .tool-agg-table th{text-align:left;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;font-size:10px;padding:8px 12px;border-bottom:1px solid var(--border)}
552
+ .tool-agg-table td{padding:8px 12px;color:var(--text-sec);border-bottom:1px solid var(--border)}
553
+ .tool-agg-table tr:hover td{background:rgba(99,102,241,.04);color:var(--text)}
554
+ .tool-agg-table .tool-name{font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
555
+ .tool-agg-table .tool-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}
556
+ .tool-agg-table .ms-val{font-family:var(--mono);font-weight:600}
557
+ .tool-agg-table .ms-val.fast{color:var(--green)}
558
+ .tool-agg-table .ms-val.medium{color:var(--amber)}
559
+ .tool-agg-table .ms-val.slow{color:var(--accent)}
321
560
  .chart-legend .dot.violet{background:var(--violet)}
322
561
  .chart-legend .dot.green{background:var(--green)}
323
562
  .breakdown-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}
324
- .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}
325
- .breakdown-item:hover{border-color:var(--border-glow)}
563
+ .breakdown-item{display:flex;flex-direction:column;gap:5px;padding:10px 12px;background:rgba(255,255,255,.02);border-radius:8px;border:1px solid var(--border);transition:all .15s}
564
+ .breakdown-item:hover{border-color:var(--border-glow);background:rgba(255,255,255,.04)}
326
565
  .breakdown-item .bd-top{display:flex;align-items:center;justify-content:space-between}
327
- .breakdown-item .label{font-size:13px;color:var(--text-sec);font-weight:500;text-transform:capitalize}
328
- .breakdown-item .value{font-size:14px;font-weight:700;color:var(--text)}
329
- .breakdown-bar-wrap{height:4px;background:rgba(0,0,0,.15);border-radius:2px;overflow:hidden}
330
- .breakdown-bar{height:100%;border-radius:2px;background:linear-gradient(90deg,#3b82f6,#60a5fa);transition:width .5s ease}
331
- .metrics-toolbar{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap}
332
- .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}
333
- .metrics-toolbar .range-btn:hover{border-color:var(--pri);color:var(--pri)}
334
- .metrics-toolbar .range-btn.active{background:rgba(255,255,255,.1);color:var(--text);border-color:var(--border)}
566
+ .breakdown-item .label{font-size:12px;color:var(--text-sec);font-weight:500;text-transform:capitalize}
567
+ .breakdown-item .value{font-size:13px;font-weight:600;color:var(--text)}
568
+ .breakdown-bar-wrap{height:3px;background:rgba(255,255,255,.06);border-radius:2px;overflow:hidden}
569
+ .breakdown-bar{height:100%;border-radius:2px;background:var(--pri);transition:width .5s ease}
570
+ .metrics-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}
571
+ .range-btn{padding:5px 12px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-sec);font-size:12px;font-weight:500;cursor:pointer;transition:all .15s}
572
+ .range-btn:hover{border-color:var(--pri);color:var(--pri)}
573
+ .range-btn.active{background:rgba(99,102,241,.08);color:var(--pri);border-color:rgba(99,102,241,.25)}
335
574
 
336
575
  .theme-toggle{position:relative;width:28px;height:28px;padding:0;display:flex;align-items:center;justify-content:center;font-size:14px;border:none;background:transparent}
337
576
  .theme-toggle .theme-icon-light{display:none}
@@ -339,14 +578,14 @@ input,textarea,select{font-family:inherit;font-size:inherit}
339
578
  [data-theme="light"] .theme-toggle .theme-icon-light{display:inline}
340
579
  [data-theme="light"] .theme-toggle .theme-icon-dark{display:none}
341
580
 
342
- .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}
343
- .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}
344
- .auth-theme-toggle:hover{background:rgba(255,255,255,.2);color:#fff}
581
+ .auth-top-actions{position:absolute;top:16px;right:16px;z-index:10;display:flex;align-items:center;gap:2px}
582
+ .auth-theme-toggle{min-width:28px;height:28px;border:none;border-radius:14px;background:rgba(255,255,255,.12);color:rgba(255,255,255,.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;transition:all .2s;padding:0 8px;font-weight:600}
583
+ .auth-theme-toggle:hover{background:rgba(255,255,255,.25);color:#fff}
345
584
  .auth-theme-toggle .theme-icon-light{display:none}
346
585
  .auth-theme-toggle .theme-icon-dark{display:inline}
347
- [data-theme="light"] .auth-theme-toggle{color:rgba(0,0,0,.45)}
348
- [data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.08);color:#0f172a}
349
- [data-theme="light"] .auth-top-actions{background:rgba(0,0,0,.06)}
586
+ [data-theme="light"] .auth-theme-toggle{color:rgba(0,0,0,.4);background:rgba(0,0,0,.05)}
587
+ [data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.1);color:#0f172a}
588
+ [data-theme="light"] .auth-top-actions{background:none}
350
589
  [data-theme="light"] .auth-theme-toggle .theme-icon-light{display:inline}
351
590
  [data-theme="light"] .auth-theme-toggle .theme-icon-dark{display:none}
352
591
 
@@ -440,7 +679,10 @@ input,textarea,select{font-family:inherit;font-size:inherit}
440
679
  <nav class="nav-tabs">
441
680
  <button class="tab active" data-view="memories" onclick="switchView('memories')" data-i18n="tab.memories">\u{1F4DA} Memories</button>
442
681
  <button class="tab" data-view="tasks" onclick="switchView('tasks')" data-i18n="tab.tasks">\u{1F4CB} Tasks</button>
682
+ <button class="tab" data-view="skills" onclick="switchView('skills')" data-i18n="tab.skills">\u{1F9E0} Skills</button>
443
683
  <button class="tab" data-view="analytics" onclick="switchView('analytics')" data-i18n="tab.analytics">\u{1F4CA} Analytics</button>
684
+ <button class="tab" data-view="logs" onclick="switchView('logs')" data-i18n="tab.logs">\u{1F4DD} Logs</button>
685
+ <button class="tab" data-view="settings" onclick="switchView('settings')" data-i18n="tab.settings">\u2699 Settings</button>
444
686
  </nav>
445
687
  </div>
446
688
  <div class="actions">
@@ -526,12 +768,52 @@ input,textarea,select{font-family:inherit;font-size:inherit}
526
768
  <button class="btn btn-icon" onclick="closeTaskDetail()" title="Close">\u2715</button>
527
769
  </div>
528
770
  <div class="task-detail-meta" id="taskDetailMeta"></div>
771
+ <div class="task-skill-section" id="taskSkillSection"></div>
529
772
  <div class="task-detail-summary" id="taskDetailSummary"></div>
530
773
  <div class="task-detail-chunks-title" data-i18n="tasks.chunks">Related Memories</div>
531
774
  <div class="task-detail-chunks" id="taskDetailChunks"></div>
532
775
  </div>
533
776
  </div>
534
777
  </div>
778
+ <div class="skills-view" id="skillsView">
779
+ <div class="tasks-header">
780
+ <div class="tasks-stats">
781
+ <div class="tasks-stat"><span class="tasks-stat-value" id="skillsTotalCount">-</span><span class="tasks-stat-label" data-i18n="skills.total">Total Skills</span></div>
782
+ <div class="tasks-stat" style="border-left:3px solid var(--green)"><span class="tasks-stat-value" id="skillsActiveCount">-</span><span class="tasks-stat-label" data-i18n="skills.active">Active</span></div>
783
+ <div class="tasks-stat" style="border-left:3px solid var(--amber)"><span class="tasks-stat-value" id="skillsDraftCount">-</span><span class="tasks-stat-label" data-i18n="skills.draft">Draft</span></div>
784
+ <div class="tasks-stat" style="border-left:3px solid var(--violet)"><span class="tasks-stat-value" id="skillsInstalledCount">-</span><span class="tasks-stat-label" data-i18n="skills.installed">Installed</span></div>
785
+ </div>
786
+ <div class="tasks-filters">
787
+ <button class="filter-chip active" data-skill-status="" onclick="setSkillStatusFilter(this,'')" data-i18n="filter.all">All</button>
788
+ <button class="filter-chip" data-skill-status="active" onclick="setSkillStatusFilter(this,'active')" data-i18n="skills.filter.active">Active</button>
789
+ <button class="filter-chip" data-skill-status="draft" onclick="setSkillStatusFilter(this,'draft')" data-i18n="skills.filter.draft">Draft</button>
790
+ <button class="filter-chip" data-skill-status="archived" onclick="setSkillStatusFilter(this,'archived')" data-i18n="skills.filter.archived">Archived</button>
791
+ <button class="btn btn-sm btn-ghost" onclick="loadSkills()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
792
+ </div>
793
+ </div>
794
+ <div class="tasks-list" id="skillsList"><div class="spinner"></div></div>
795
+ </div>
796
+ <div class="task-detail-overlay" id="skillDetailOverlay" onclick="closeSkillDetail(event)">
797
+ <div class="task-detail-panel" onclick="event.stopPropagation()">
798
+ <div class="task-detail-header">
799
+ <h2 id="skillDetailTitle"></h2>
800
+ <div style="display:flex;gap:8px;align-items:center">
801
+ <button class="skill-download-btn" id="skillDownloadBtn" onclick="downloadSkill()" data-i18n="skills.download">\u2B07 Download</button>
802
+ <button class="btn btn-icon" onclick="closeSkillDetail()" title="Close">\u2715</button>
803
+ </div>
804
+ </div>
805
+ <div class="task-detail-meta" id="skillDetailMeta"></div>
806
+ <div class="skill-detail-desc" id="skillDetailDesc"></div>
807
+ <div class="task-detail-chunks-title" data-i18n="skills.files">Skill Files</div>
808
+ <div class="skill-files-list" id="skillFilesList"></div>
809
+ <div class="task-detail-chunks-title" id="skillContentTitle" data-i18n="skills.content">SKILL.md Content</div>
810
+ <div class="task-detail-summary" id="skillDetailContent" style="max-height:50vh;overflow-y:auto"></div>
811
+ <div class="task-detail-chunks-title" data-i18n="skills.versions">Version History</div>
812
+ <div class="task-detail-chunks" id="skillVersionsList" style="gap:10px"></div>
813
+ <div class="task-detail-chunks-title" style="margin-top:16px" data-i18n="skills.related">Related Tasks</div>
814
+ <div class="task-detail-chunks" id="skillRelatedTasks" style="gap:8px"></div>
815
+ </div>
816
+ </div>
535
817
  <div class="analytics-view" id="analyticsView">
536
818
  <div class="metrics-toolbar">
537
819
  <span style="font-size:12px;color:var(--text-sec);font-weight:600" data-i18n="range">Range</span>
@@ -543,7 +825,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
543
825
  <div class="analytics-cards" id="analyticsCards">
544
826
  <div class="analytics-card"><div class="ac-value" id="mTotal">-</div><div class="ac-label" data-i18n="analytics.total">Total Memories</div></div>
545
827
  <div class="analytics-card green"><div class="ac-value" id="mTodayWrites">-</div><div class="ac-label" data-i18n="analytics.writes">Writes Today</div></div>
546
- <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>
547
828
  <div class="analytics-card"><div class="ac-value" id="mSessions">-</div><div class="ac-label" data-i18n="analytics.sessions">Sessions</div></div>
548
829
  <div class="analytics-card amber"><div class="ac-value" id="mEmbeddings">-</div><div class="ac-label" data-i18n="analytics.embeddings">Embeddings</div></div>
549
830
  </div>
@@ -551,11 +832,21 @@ input,textarea,select{font-family:inherit;font-size:inherit}
551
832
  <h3><span class="icon">\u{1F4CA}</span> <span data-i18n="chart.writes">Memory Writes per Day</span></h3>
552
833
  <div class="chart-bars" id="chartWrites"></div>
553
834
  </div>
554
- <div class="analytics-section">
555
- <h3><span class="icon">\u{1F50D}</span> <span data-i18n="chart.calls">Viewer API Calls per Day (List / Search)</span></h3>
556
- <div class="chart-bars" id="chartCalls"></div>
557
- <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>
835
+
836
+ <div class="analytics-section" id="toolPerfSection" style="position:relative">
837
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
838
+ <h3 style="margin-bottom:0"><span class="icon">\u26A1</span> <span data-i18n="chart.toolperf">Tool Response Time</span> <span style="font-size:10px;color:var(--text-muted);font-weight:500;text-transform:none;letter-spacing:0;margin-left:4px">(per minute avg)</span></h3>
839
+ <div style="display:flex;gap:6px;align-items:center">
840
+ <button class="range-btn tool-range active" data-mins="60" onclick="setToolMinutes(60)">1h</button>
841
+ <button class="range-btn tool-range" data-mins="360" onclick="setToolMinutes(360)">6h</button>
842
+ <button class="range-btn tool-range" data-mins="1440" onclick="setToolMinutes(1440)">24h</button>
843
+ </div>
844
+ </div>
845
+ <div id="toolChart" style="width:100%;height:280px;position:relative;overflow:hidden;border-radius:12px"></div>
846
+ <div id="toolLegend" class="chart-legend" style="margin-top:14px;padding:0 4px"></div>
847
+ <div id="toolAggTable" style="margin-top:20px"></div>
558
848
  </div>
849
+
559
850
  <div class="breakdown-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:24px">
560
851
  <div class="analytics-section">
561
852
  <h3><span class="icon">\u{1F464}</span> <span data-i18n="breakdown.role">By Role</span></h3>
@@ -567,6 +858,165 @@ input,textarea,select{font-family:inherit;font-size:inherit}
567
858
  </div>
568
859
  </div>
569
860
  </div>
861
+
862
+ <!-- ─── Logs View ─── -->
863
+ <div class="logs-view" id="logsView">
864
+ <div class="logs-toolbar">
865
+ <div class="logs-toolbar-left">
866
+ <select id="logToolFilter" onchange="onLogFilterChange()" style="font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--text);min-width:120px">
867
+ <option value="" data-i18n="logs.allTools">All Tools</option>
868
+ </select>
869
+ <button class="btn btn-sm btn-ghost" onclick="loadLogs()" style="font-size:12px">\u21BB <span data-i18n="logs.refresh">Refresh</span></button>
870
+ </div>
871
+ <div class="logs-toolbar-right">
872
+ <input type="checkbox" id="logAutoRefresh" checked style="display:none">
873
+ </div>
874
+ </div>
875
+ <div class="logs-list" id="logsList"></div>
876
+ <div id="logsPagination"></div>
877
+ </div>
878
+
879
+ <!-- ─── Settings View ─── -->
880
+ <div class="settings-view" id="settingsView">
881
+ <div class="settings-section">
882
+ <h3><span class="icon">\u{1F4E1}</span> <span data-i18n="settings.embedding">Embedding Model</span></h3>
883
+ <div class="settings-grid">
884
+ <div class="settings-field">
885
+ <label data-i18n="settings.provider">Provider</label>
886
+ <select id="cfgEmbProvider">
887
+ <option value="openai_compatible">OpenAI Compatible</option>
888
+ <option value="openai">OpenAI</option>
889
+ <option value="gemini">Gemini</option>
890
+ <option value="azure_openai">Azure OpenAI</option>
891
+ <option value="cohere">Cohere</option>
892
+ <option value="mistral">Mistral</option>
893
+ <option value="voyage">Voyage</option>
894
+ <option value="local">Local</option>
895
+ </select>
896
+ </div>
897
+ <div class="settings-field">
898
+ <label data-i18n="settings.model">Model</label>
899
+ <input type="text" id="cfgEmbModel" placeholder="e.g. bge-m3">
900
+ </div>
901
+ <div class="settings-field full-width">
902
+ <label>Endpoint</label>
903
+ <input type="text" id="cfgEmbEndpoint" placeholder="https://...">
904
+ </div>
905
+ <div class="settings-field">
906
+ <label>API Key</label>
907
+ <input type="password" id="cfgEmbApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
908
+ </div>
909
+ </div>
910
+ </div>
911
+
912
+ <div class="settings-section">
913
+ <h3><span class="icon">\u{1F9E0}</span> <span data-i18n="settings.summarizer">Summarizer Model</span></h3>
914
+ <div class="settings-grid">
915
+ <div class="settings-field">
916
+ <label data-i18n="settings.provider">Provider</label>
917
+ <select id="cfgSumProvider">
918
+ <option value="openai_compatible">OpenAI Compatible</option>
919
+ <option value="openai">OpenAI</option>
920
+ <option value="anthropic">Anthropic</option>
921
+ <option value="gemini">Gemini</option>
922
+ <option value="azure_openai">Azure OpenAI</option>
923
+ <option value="bedrock">Bedrock</option>
924
+ </select>
925
+ </div>
926
+ <div class="settings-field">
927
+ <label data-i18n="settings.model">Model</label>
928
+ <input type="text" id="cfgSumModel" placeholder="e.g. gpt-4o-mini">
929
+ </div>
930
+ <div class="settings-field full-width">
931
+ <label>Endpoint</label>
932
+ <input type="text" id="cfgSumEndpoint" placeholder="https://...">
933
+ </div>
934
+ <div class="settings-field">
935
+ <label>API Key</label>
936
+ <input type="password" id="cfgSumApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
937
+ </div>
938
+ <div class="settings-field">
939
+ <label data-i18n="settings.temperature">Temperature</label>
940
+ <input type="number" id="cfgSumTemp" step="0.1" min="0" max="2" placeholder="0">
941
+ </div>
942
+ </div>
943
+ </div>
944
+
945
+ <div class="settings-section">
946
+ <h3><span class="icon">\u{1F527}</span> <span data-i18n="settings.skill">Skill Evolution</span></h3>
947
+ <div class="settings-grid">
948
+ <div class="settings-toggle">
949
+ <label class="toggle-switch"><input type="checkbox" id="cfgSkillEnabled"><span class="toggle-slider"></span></label>
950
+ <label data-i18n="settings.skill.enabled">Enable Skill Evolution</label>
951
+ </div>
952
+ <div class="settings-toggle">
953
+ <label class="toggle-switch"><input type="checkbox" id="cfgSkillAutoInstall"><span class="toggle-slider"></span></label>
954
+ <label data-i18n="settings.skill.autoinstall">Auto Install Skills</label>
955
+ </div>
956
+ <div class="settings-field">
957
+ <label data-i18n="settings.skill.confidence">Min Confidence</label>
958
+ <input type="number" id="cfgSkillConfidence" step="0.1" min="0" max="1" placeholder="0.7">
959
+ </div>
960
+ <div class="settings-field">
961
+ <label data-i18n="settings.skill.minchunks">Min Chunks</label>
962
+ <input type="number" id="cfgSkillMinChunks" placeholder="6">
963
+ </div>
964
+ </div>
965
+ <div style="margin-top:16px;padding-top:16px;border-top:1px dashed var(--border)">
966
+ <h3 style="font-size:12px;color:var(--text-muted);margin-bottom:4px;display:flex;align-items:center;gap:8px"><span class="icon">\u{1F680}</span> <span data-i18n="settings.skill.model">Skill Dedicated Model</span><span style="font-size:10px;font-weight:500;color:var(--amber);background:var(--amber-bg);padding:2px 8px;border-radius:10px" data-i18n="settings.optional">Optional</span></h3>
967
+ <div style="font-size:11px;color:var(--text-muted);margin-bottom:12px;line-height:1.5" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.</div>
968
+ <div class="settings-grid">
969
+ <div class="settings-field">
970
+ <label data-i18n="settings.provider">Provider</label>
971
+ <select id="cfgSkillProvider">
972
+ <option value="" id="cfgSkillProviderDefault">\u2014</option>
973
+ <option value="openai_compatible">OpenAI Compatible</option>
974
+ <option value="openai">OpenAI</option>
975
+ <option value="anthropic">Anthropic</option>
976
+ <option value="gemini">Gemini</option>
977
+ <option value="azure_openai">Azure OpenAI</option>
978
+ <option value="bedrock">Bedrock</option>
979
+ </select>
980
+ </div>
981
+ <div class="settings-field">
982
+ <label data-i18n="settings.model">Model</label>
983
+ <input type="text" id="cfgSkillModel" placeholder="e.g. claude-4.6-opus">
984
+ </div>
985
+ <div class="settings-field full-width">
986
+ <label>Endpoint</label>
987
+ <input type="text" id="cfgSkillEndpoint" placeholder="https://...">
988
+ </div>
989
+ <div class="settings-field">
990
+ <label>API Key</label>
991
+ <input type="password" id="cfgSkillApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
992
+ </div>
993
+ <div class="settings-field">
994
+ <label data-i18n="settings.temperature">Temperature</label>
995
+ <input type="number" id="cfgSkillTemp" step="0.1" min="0" max="2" placeholder="0.2">
996
+ </div>
997
+ </div>
998
+ </div>
999
+ </div>
1000
+
1001
+ <div class="settings-section">
1002
+ <h3><span class="icon">\u{1F4BE}</span> <span data-i18n="settings.general">General</span></h3>
1003
+ <div class="settings-grid">
1004
+ <div class="settings-field">
1005
+ <label data-i18n="settings.viewerport">Viewer Port</label>
1006
+ <input type="number" id="cfgViewerPort" placeholder="18799">
1007
+ <div class="field-hint" data-i18n="settings.viewerport.hint">Requires restart to take effect</div>
1008
+ </div>
1009
+ </div>
1010
+ </div>
1011
+
1012
+ <div class="settings-actions">
1013
+ <span class="settings-saved" id="settingsSaved">\u2713 <span data-i18n="settings.saved">Saved</span></span>
1014
+ <button class="btn btn-ghost" onclick="loadConfig()" data-i18n="settings.reset">Reset</button>
1015
+ <button class="btn btn-primary" onclick="saveConfig()" data-i18n="settings.save">Save Settings</button>
1016
+ </div>
1017
+ <div style="font-size:11px;color:var(--text-muted);text-align:right;margin-top:4px" data-i18n="settings.restart.hint">Some changes require restarting the OpenClaw gateway to take effect.</div>
1018
+ </div>
1019
+
570
1020
  </div>
571
1021
  </div>
572
1022
 
@@ -589,7 +1039,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
589
1039
  <div class="toast-container" id="toasts"></div>
590
1040
 
591
1041
  <script>
592
- let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30,metricsDays=30;
1042
+ let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=40,metricsDays=30;
593
1043
 
594
1044
  /* ─── i18n ─── */
595
1045
  const I18N={
@@ -628,7 +1078,11 @@ const I18N={
628
1078
  'copy.done':'Copied!',
629
1079
  'tab.memories':'\\u{1F4DA} Memories',
630
1080
  'tab.tasks':'\\u{1F4CB} Tasks',
1081
+ 'tab.skills':'\\u{1F9E0} Skills',
631
1082
  'tab.analytics':'\\u{1F4CA} Analytics',
1083
+ 'skills.total':'Total Skills',
1084
+ 'skills.active':'Active',
1085
+ 'skills.installed':'Installed',
632
1086
  'tasks.total':'Total Tasks',
633
1087
  'tasks.active':'Active',
634
1088
  'tasks.completed':'Completed',
@@ -647,6 +1101,8 @@ const I18N={
647
1101
  'stat.sessions':'Sessions',
648
1102
  'stat.embeddings':'Embeddings',
649
1103
  'stat.days':'Days',
1104
+ 'stat.active':'active',
1105
+ 'stat.deduped':'deduped',
650
1106
  'sidebar.sessions':'Sessions',
651
1107
  'sidebar.allsessions':'All Sessions',
652
1108
  'sidebar.clear':'\\u{1F5D1} Clear All Data',
@@ -672,6 +1128,16 @@ const I18N={
672
1128
  'card.expand':'Expand',
673
1129
  'card.edit':'Edit',
674
1130
  'card.delete':'Delete',
1131
+ 'card.evolved':'Evolved',
1132
+ 'card.times':'times',
1133
+ 'card.updated':'updated',
1134
+ 'card.evolveHistory':'Evolution History',
1135
+ 'card.oldSummary':'Old',
1136
+ 'card.dedupDuplicate':'Duplicate',
1137
+ 'card.dedupMerged':'Merged',
1138
+ 'card.dedupTarget':'Target: ',
1139
+ 'card.dedupReason':'Reason: ',
1140
+ 'card.newSummary':'New',
675
1141
  'pagination.total':' total',
676
1142
  'range':'Range',
677
1143
  'range.days':'days',
@@ -684,6 +1150,7 @@ const I18N={
684
1150
  'chart.calls':'Viewer API Calls per Day (List / Search)',
685
1151
  'chart.nodata':'No data in this range',
686
1152
  'chart.nocalls':'No viewer calls in this range',
1153
+ 'chart.toolperf':'Tool Response Time',
687
1154
  'chart.list':'List',
688
1155
  'chart.search':'Search',
689
1156
  'breakdown.role':'By Role',
@@ -713,7 +1180,69 @@ const I18N={
713
1180
  'confirm.clearall2':'Are you absolutely sure?',
714
1181
  'embed.on':'Embedding: ',
715
1182
  'embed.off':'No embedding model',
716
- 'lang.switch':'中'
1183
+ 'lang.switch':'中',
1184
+ 'tab.logs':'\u{1F4DD} Logs',
1185
+ 'logs.allTools':'All Tools',
1186
+ 'logs.refresh':'Refresh',
1187
+ 'logs.autoRefresh':'Auto-refresh',
1188
+ 'logs.input':'INPUT',
1189
+ 'logs.output':'OUTPUT',
1190
+ 'logs.empty':'No logs yet. Logs will appear here when tools are called.',
1191
+ 'logs.ago':'ago',
1192
+ 'tab.settings':'\u2699 Settings',
1193
+ 'settings.embedding':'Embedding Model',
1194
+ 'settings.summarizer':'Summarizer Model',
1195
+ 'settings.skill':'Skill Evolution',
1196
+ 'settings.general':'General',
1197
+ 'settings.provider':'Provider',
1198
+ 'settings.model':'Model',
1199
+ 'settings.temperature':'Temperature',
1200
+ 'settings.skill.enabled':'Enable Skill Evolution',
1201
+ 'settings.skill.autoinstall':'Auto Install Skills',
1202
+ 'settings.skill.confidence':'Min Confidence',
1203
+ 'settings.skill.minchunks':'Min Chunks',
1204
+ 'settings.skill.model':'Skill Dedicated Model',
1205
+ 'settings.skill.model.hint':'If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.',
1206
+ 'settings.optional':'Optional',
1207
+ 'settings.skill.usemain':'Use Main Summarizer',
1208
+ 'settings.viewerport':'Viewer Port',
1209
+ 'settings.viewerport.hint':'Requires restart to take effect',
1210
+ 'settings.save':'Save Settings',
1211
+ 'settings.reset':'Reset',
1212
+ 'settings.saved':'Saved',
1213
+ 'settings.restart.hint':'Some changes require restarting the OpenClaw gateway to take effect.',
1214
+ 'settings.save.fail':'Failed to save settings',
1215
+ 'skills.draft':'Draft',
1216
+ 'skills.filter.active':'Active',
1217
+ 'skills.filter.draft':'Draft',
1218
+ 'skills.filter.archived':'Archived',
1219
+ 'skills.files':'Skill Files',
1220
+ 'skills.content':'SKILL.md Content',
1221
+ 'skills.versions':'Version History',
1222
+ 'skills.related':'Related Tasks',
1223
+ 'skills.download':'\u2B07 Download',
1224
+ 'skills.installed.badge':'Installed',
1225
+ 'skills.empty':'No skills yet. Skills are automatically generated from completed tasks that contain reusable experience.',
1226
+ 'skills.loading':'Loading...',
1227
+ 'skills.error':'Error loading skill',
1228
+ 'skills.error.detail':'Failed to load skill: ',
1229
+ 'skills.nofiles':'No files found',
1230
+ 'skills.noversions':'No versions recorded',
1231
+ 'skills.norelated':'No related tasks',
1232
+ 'skills.nocontent':'No content available',
1233
+ 'skills.nochangelog':'No changelog',
1234
+ 'skills.status.active':'Active',
1235
+ 'skills.status.draft':'Draft',
1236
+ 'skills.status.archived':'Archived',
1237
+ 'skills.updated':'Updated: ',
1238
+ 'skills.task.prefix':'Task: ',
1239
+ 'tasks.chunks.label':'chunks',
1240
+ 'tasks.taskid':'Task ID: ',
1241
+ 'tasks.role.user':'You',
1242
+ 'tasks.role.assistant':'Assistant',
1243
+ 'tasks.error':'Error',
1244
+ 'tasks.error.detail':'Failed to load task details',
1245
+ 'tasks.untitled.related':'Untitled'
717
1246
  },
718
1247
  zh:{
719
1248
  'title':'OpenClaw 记忆',
@@ -750,7 +1279,11 @@ const I18N={
750
1279
  'copy.done':'已复制!',
751
1280
  'tab.memories':'\\u{1F4DA} 记忆',
752
1281
  'tab.tasks':'\\u{1F4CB} 任务',
1282
+ 'tab.skills':'\\u{1F9E0} 技能',
753
1283
  'tab.analytics':'\\u{1F4CA} 分析',
1284
+ 'skills.total':'技能总数',
1285
+ 'skills.active':'生效中',
1286
+ 'skills.installed':'已安装',
754
1287
  'tasks.total':'任务总数',
755
1288
  'tasks.active':'进行中',
756
1289
  'tasks.completed':'已完成',
@@ -769,6 +1302,8 @@ const I18N={
769
1302
  'stat.sessions':'会话',
770
1303
  'stat.embeddings':'嵌入',
771
1304
  'stat.days':'天数',
1305
+ 'stat.active':'活跃',
1306
+ 'stat.deduped':'已去重',
772
1307
  'sidebar.sessions':'会话列表',
773
1308
  'sidebar.allsessions':'全部会话',
774
1309
  'sidebar.clear':'\\u{1F5D1} 清除所有数据',
@@ -794,6 +1329,16 @@ const I18N={
794
1329
  'card.expand':'展开',
795
1330
  'card.edit':'编辑',
796
1331
  'card.delete':'删除',
1332
+ 'card.evolved':'已演化',
1333
+ 'card.times':'次',
1334
+ 'card.updated':'更新于',
1335
+ 'card.evolveHistory':'演化记录',
1336
+ 'card.oldSummary':'旧摘要',
1337
+ 'card.dedupDuplicate':'重复',
1338
+ 'card.dedupMerged':'已合并',
1339
+ 'card.dedupTarget':'关联: ',
1340
+ 'card.dedupReason':'原因: ',
1341
+ 'card.newSummary':'新摘要',
797
1342
  'pagination.total':' 条',
798
1343
  'range':'范围',
799
1344
  'range.days':'天',
@@ -806,6 +1351,7 @@ const I18N={
806
1351
  'chart.calls':'每日查看器 API 调用(列表 / 搜索)',
807
1352
  'chart.nodata':'此范围内暂无数据',
808
1353
  'chart.nocalls':'此范围内暂无查看器调用',
1354
+ 'chart.toolperf':'工具响应耗时',
809
1355
  'chart.list':'列表',
810
1356
  'chart.search':'搜索',
811
1357
  'breakdown.role':'按角色',
@@ -835,7 +1381,69 @@ const I18N={
835
1381
  'confirm.clearall2':'你真的确定吗?',
836
1382
  'embed.on':'嵌入模型:',
837
1383
  'embed.off':'无嵌入模型',
838
- 'lang.switch':'EN'
1384
+ 'lang.switch':'EN',
1385
+ 'tab.logs':'\u{1F4DD} 日志',
1386
+ 'logs.allTools':'全部工具',
1387
+ 'logs.refresh':'刷新',
1388
+ 'logs.autoRefresh':'自动刷新',
1389
+ 'logs.input':'输入',
1390
+ 'logs.output':'输出',
1391
+ 'logs.empty':'暂无日志。当工具被调用时日志会显示在这里。',
1392
+ 'logs.ago':'前',
1393
+ 'tab.settings':'\u2699 设置',
1394
+ 'settings.embedding':'嵌入模型',
1395
+ 'settings.summarizer':'摘要模型',
1396
+ 'settings.skill':'技能进化',
1397
+ 'settings.general':'通用设置',
1398
+ 'settings.provider':'服务商',
1399
+ 'settings.model':'模型',
1400
+ 'settings.temperature':'温度',
1401
+ 'settings.skill.enabled':'启用技能进化',
1402
+ 'settings.skill.autoinstall':'自动安装技能',
1403
+ 'settings.skill.confidence':'最低置信度',
1404
+ 'settings.skill.minchunks':'最少记忆片段',
1405
+ 'settings.skill.model':'技能专用模型',
1406
+ 'settings.skill.model.hint':'不配置时默认使用上方的摘要模型进行技能生成。如需更高质量的技能输出,可在此单独配置一个更强的模型。',
1407
+ 'settings.optional':'可选',
1408
+ 'settings.skill.usemain':'使用主摘要模型',
1409
+ 'settings.viewerport':'Viewer 端口',
1410
+ 'settings.viewerport.hint':'修改后需重启网关生效',
1411
+ 'settings.save':'保存设置',
1412
+ 'settings.reset':'重置',
1413
+ 'settings.saved':'已保存',
1414
+ 'settings.restart.hint':'部分设置修改后需要重启 OpenClaw 网关才能生效。',
1415
+ 'settings.save.fail':'保存设置失败',
1416
+ 'skills.draft':'草稿',
1417
+ 'skills.filter.active':'生效中',
1418
+ 'skills.filter.draft':'草稿',
1419
+ 'skills.filter.archived':'已归档',
1420
+ 'skills.files':'技能文件',
1421
+ 'skills.content':'SKILL.md 内容',
1422
+ 'skills.versions':'版本历史',
1423
+ 'skills.related':'关联任务',
1424
+ 'skills.download':'\u2B07 下载',
1425
+ 'skills.installed.badge':'已安装',
1426
+ 'skills.empty':'暂无技能。技能会从已完成的、包含可复用经验的任务中自动生成。',
1427
+ 'skills.loading':'加载中...',
1428
+ 'skills.error':'加载技能失败',
1429
+ 'skills.error.detail':'加载技能失败:',
1430
+ 'skills.nofiles':'暂无文件',
1431
+ 'skills.noversions':'暂无版本记录',
1432
+ 'skills.norelated':'暂无关联任务',
1433
+ 'skills.nocontent':'暂无内容',
1434
+ 'skills.nochangelog':'暂无变更记录',
1435
+ 'skills.status.active':'生效中',
1436
+ 'skills.status.draft':'草稿',
1437
+ 'skills.status.archived':'已归档',
1438
+ 'skills.updated':'更新于:',
1439
+ 'skills.task.prefix':'任务:',
1440
+ 'tasks.chunks.label':'条记忆',
1441
+ 'tasks.taskid':'任务 ID:',
1442
+ 'tasks.role.user':'你',
1443
+ 'tasks.role.assistant':'助手',
1444
+ 'tasks.error':'出错了',
1445
+ 'tasks.error.detail':'加载任务详情失败',
1446
+ 'tasks.untitled.related':'未命名'
839
1447
  }
840
1448
  };
841
1449
  const LANG_KEY='memos-viewer-lang';
@@ -947,20 +1555,226 @@ function switchView(view){
947
1555
  const feedWrap=document.getElementById('feedWrap');
948
1556
  const analyticsView=document.getElementById('analyticsView');
949
1557
  const tasksView=document.getElementById('tasksView');
1558
+ const skillsView=document.getElementById('skillsView');
1559
+ const logsView=document.getElementById('logsView');
1560
+ const settingsView=document.getElementById('settingsView');
950
1561
  feedWrap.classList.add('hide');
951
1562
  analyticsView.classList.remove('show');
952
1563
  tasksView.classList.remove('show');
1564
+ skillsView.classList.remove('show');
1565
+ logsView.classList.remove('show');
1566
+ settingsView.classList.remove('show');
953
1567
  if(view==='analytics'){
954
1568
  analyticsView.classList.add('show');
955
1569
  loadMetrics();
956
1570
  } else if(view==='tasks'){
957
1571
  tasksView.classList.add('show');
958
1572
  loadTasks();
1573
+ } else if(view==='skills'){
1574
+ skillsView.classList.add('show');
1575
+ loadSkills();
1576
+ } else if(view==='logs'){
1577
+ logsView.classList.add('show');
1578
+ loadLogs();
1579
+ } else if(view==='settings'){
1580
+ settingsView.classList.add('show');
1581
+ loadConfig();
959
1582
  } else {
960
1583
  feedWrap.classList.remove('hide');
961
1584
  }
962
1585
  }
963
1586
 
1587
+ // ─── Logs ───
1588
+ let logAutoTimer=null;
1589
+ let logPage=1;
1590
+ const LOG_PAGE_SIZE=20;
1591
+ async function loadLogs(page){
1592
+ if(typeof page==='number') logPage=page;
1593
+ try{
1594
+ const toolFilter=document.getElementById('logToolFilter').value;
1595
+ const offset=(logPage-1)*LOG_PAGE_SIZE;
1596
+ const url='/api/logs?limit='+LOG_PAGE_SIZE+'&offset='+offset+(toolFilter?'&tool='+encodeURIComponent(toolFilter):'');
1597
+ const [logsRes,toolsRes]=await Promise.all([fetch(url),fetch('/api/log-tools')]);
1598
+ if(!logsRes.ok) return;
1599
+ const logsData=await logsRes.json();
1600
+ const toolsData=await toolsRes.json();
1601
+ renderLogToolFilter(toolsData.tools||[],toolFilter);
1602
+ renderLogs(logsData.logs||[]);
1603
+ renderLogPagination(logsData.page||1,logsData.totalPages||1,logsData.total||0);
1604
+ startLogAutoRefresh();
1605
+ }catch(e){console.error('loadLogs',e)}
1606
+ }
1607
+ function onLogFilterChange(){logPage=1;loadLogs(1);}
1608
+ function renderLogPagination(page,totalPages,total){
1609
+ const el=document.getElementById('logsPagination');
1610
+ if(!el||totalPages<=1){if(el)el.innerHTML='';return;}
1611
+ const pages=[];
1612
+ const range=2;
1613
+ for(let i=1;i<=totalPages;i++){
1614
+ if(i===1||i===totalPages||Math.abs(i-page)<=range){
1615
+ pages.push(i);
1616
+ }else if(pages[pages.length-1]!=='...'){
1617
+ pages.push('...');
1618
+ }
1619
+ }
1620
+ let html='<div class="logs-pagination">';
1621
+ html+='<button class="btn btn-sm btn-ghost" '+(page<=1?'disabled':'')+' onclick="loadLogs('+(page-1)+')">\u2039</button>';
1622
+ pages.forEach(p=>{
1623
+ if(p==='...'){html+='<span class="page-ellipsis">\u2026</span>';}
1624
+ else{html+='<button class="btn btn-sm '+(p===page?'btn-primary':'btn-ghost')+'" onclick="loadLogs('+p+')">'+p+'</button>';}
1625
+ });
1626
+ html+='<button class="btn btn-sm btn-ghost" '+(page>=totalPages?'disabled':'')+' onclick="loadLogs('+(page+1)+')">\u203A</button>';
1627
+ html+='<span class="page-total">'+total+' total</span>';
1628
+ html+='</div>';
1629
+ el.innerHTML=html;
1630
+ }
1631
+
1632
+ function renderLogToolFilter(tools,current){
1633
+ const sel=document.getElementById('logToolFilter');
1634
+ const opts=['<option value="">'+t('logs.allTools')+'</option>'];
1635
+ tools.forEach(tn=>{
1636
+ opts.push('<option value="'+tn+'"'+(tn===current?' selected':'')+'>'+tn+'</option>');
1637
+ });
1638
+ sel.innerHTML=opts.join('');
1639
+ }
1640
+
1641
+ function formatLogTime(ts){
1642
+ const d=new Date(ts);
1643
+ const time=d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false});
1644
+ const y=d.getFullYear();
1645
+ const m=String(d.getMonth()+1).padStart(2,'0');
1646
+ const day=String(d.getDate()).padStart(2,'0');
1647
+ return y+'-'+m+'-'+day+' '+time;
1648
+ }
1649
+
1650
+ function buildLogSummary(lg){
1651
+ let inputObj=null;
1652
+ try{inputObj=JSON.parse(lg.input);}catch(_){}
1653
+ let html='';
1654
+ const tn=lg.toolName;
1655
+ if(tn==='memory_search'&&inputObj){
1656
+ const q=inputObj.query||'';
1657
+ if(q) html+='<div class="log-summary-query">'+escapeHtml(q.length>200?q.slice(0,200)+'...':q)+'</div>';
1658
+ const outLines=(lg.output||'').split('\\n');
1659
+ const memCount=outLines.filter(l=>l.match(/^\\d+\\.\\s*\\[/)).length;
1660
+ if(memCount>0) html+='<div style="margin-top:4px;font-size:11px;color:var(--text-sec)">\u{1F4CE} '+memCount+' memories retrieved</div>';
1661
+ else if(lg.output&&lg.output.includes('no hits')) html+='<div style="margin-top:4px;font-size:11px;color:var(--text-sec)">\u2205 No matching memories</div>';
1662
+ }else if(tn==='memory_add'&&inputObj){
1663
+ const out=lg.output||'';
1664
+ const statsMatch=out.match(/^([^\\n]+)/);
1665
+ if(statsMatch){
1666
+ html+='<div class="log-summary-stats">';
1667
+ const pairs=statsMatch[1].split(',').map(s=>s.trim());
1668
+ pairs.forEach(p=>{
1669
+ const m=p.match(/^(\\w+)=(\\d+)/);
1670
+ if(m){html+='<span class="log-stat-chip '+m[1]+'">'+m[1]+' '+m[2]+'</span>';}
1671
+ });
1672
+ html+='</div>';
1673
+ }
1674
+ const outLines=out.split('\\n').filter(l=>l.startsWith('['));
1675
+ if(outLines.length>0){
1676
+ html+='<div class="log-msg-list">';
1677
+ outLines.forEach(function(l){
1678
+ var rm=l.match(/^\\[(\\w+)\\]\\s*([^\u2192]+)\u2192\\s*(.*)/);
1679
+ if(rm){
1680
+ var role=rm[1],actionRaw=rm[2].trim(),text=rm[3].trim();
1681
+ var actionCls='stored';
1682
+ if(actionRaw.indexOf('exact-dup')>=0||actionRaw.indexOf('\u23ED')>=0) actionCls='exact-dup';
1683
+ else if(actionRaw.indexOf('dedup')>=0||actionRaw.indexOf('\uD83D\uDD01')>=0) actionCls='dedup';
1684
+ else if(actionRaw.indexOf('merged')>=0||actionRaw.indexOf('\uD83D\uDD00')>=0) actionCls='merged';
1685
+ else if(actionRaw.indexOf('error')>=0||actionRaw.indexOf('\u274C')>=0) actionCls='error';
1686
+ var actionLabel={'stored':'\u2713 stored','exact-dup':'\u23ED skip','dedup':'\uD83D\uDD01 dedup','merged':'\uD83D\uDD00 merged','error':'\u2717 error'}[actionCls]||actionCls;
1687
+ html+='<div class="log-msg-item">'+
1688
+ '<span class="log-msg-role '+role+'">'+role+'</span>'+
1689
+ '<span class="log-msg-action '+actionCls+'">'+actionLabel+'</span>'+
1690
+ '<span class="log-msg-text">'+escapeHtml(text.length>150?text.slice(0,150)+'...':text)+'</span>'+
1691
+ '</div>';
1692
+ }else{
1693
+ html+='<div class="log-msg-item"><span class="log-msg-text">'+escapeHtml(l.length>200?l.slice(0,200)+'...':l)+'</span></div>';
1694
+ }
1695
+ });
1696
+ html+='</div>';
1697
+ }else if(inputObj.details&&Array.isArray(inputObj.details)&&inputObj.details.length>0){
1698
+ html+='<div class="log-msg-list">';
1699
+ inputObj.details.forEach(function(d){
1700
+ var s=typeof d==='string'?d:String(d);
1701
+ var dm=s.match(/^\\[(\\w+)\\]\\s*(.*)/);
1702
+ if(dm){
1703
+ html+='<div class="log-msg-item"><span class="log-msg-role '+dm[1]+'">'+dm[1]+'</span><span class="log-msg-text">'+escapeHtml(dm[2].length>150?dm[2].slice(0,150)+'...':dm[2])+'</span></div>';
1704
+ }else{
1705
+ html+='<div class="log-msg-item"><span class="log-msg-text">'+escapeHtml(s.length>150?s.slice(0,150)+'...':s)+'</span></div>';
1706
+ }
1707
+ });
1708
+ html+='</div>';
1709
+ }
1710
+ }else if(inputObj){
1711
+ const keys=Object.keys(inputObj);
1712
+ keys.slice(0,4).forEach(k=>{
1713
+ const v=String(inputObj[k]);
1714
+ html+='<span class="log-summary-kv"><span class="kv-label">'+escapeHtml(k)+':</span><span class="kv-val">'+escapeHtml(v.length>60?v.slice(0,60)+'...':v)+'</span></span>';
1715
+ });
1716
+ }
1717
+ return html;
1718
+ }
1719
+ function renderLogs(logs){
1720
+ const el=document.getElementById('logsList');
1721
+ if(!logs.length){
1722
+ el.innerHTML='<div style="text-align:center;padding:60px 20px;color:var(--text-sec)">'+
1723
+ '<div style="font-size:32px;margin-bottom:12px;opacity:.5">\u{1F4CB}</div>'+
1724
+ '<div style="font-size:13px">'+t('logs.empty')+'</div></div>';
1725
+ return;
1726
+ }
1727
+ el.innerHTML=logs.map((lg,i)=>{
1728
+ const toolCls=lg.toolName.replace(/[^a-zA-Z0-9_]/g,'_');
1729
+ const dur=lg.durationMs<1000?Math.round(lg.durationMs)+'ms':(lg.durationMs/1000).toFixed(1)+'s';
1730
+ let inputDisplay='';
1731
+ try{const parsed=JSON.parse(lg.input);inputDisplay=JSON.stringify(parsed,null,2);}catch(_){inputDisplay=lg.input;}
1732
+ const summary=buildLogSummary(lg);
1733
+ return '<div class="log-entry" id="log-'+i+'">'+
1734
+ '<div class="log-header" onclick="toggleLog('+i+')">'+
1735
+ '<span class="log-status '+(lg.success?'ok':'fail')+'"></span>'+
1736
+ '<span class="log-tool-badge '+toolCls+'">'+lg.toolName+'</span>'+
1737
+ '<span class="log-dur">'+dur+'</span>'+
1738
+ '<span class="log-expand-btn" style="margin-left:4px">\u25BC</span>'+
1739
+ '<span class="log-time">'+formatLogTime(lg.calledAt)+'</span>'+
1740
+ '</div>'+
1741
+ (summary?'<div class="log-summary">'+summary+'</div>':'')+
1742
+ '<div class="log-detail" id="log-detail-'+i+'">'+
1743
+ '<div class="log-io-section">'+
1744
+ '<div class="log-io-label">\u25B6 '+t('logs.input')+'</div>'+
1745
+ '<pre class="log-io-content">'+escapeHtml(inputDisplay)+'</pre>'+
1746
+ '</div>'+
1747
+ '<div class="log-io-section">'+
1748
+ '<div class="log-io-label">\u25C0 '+t('logs.output')+'</div>'+
1749
+ '<pre class="log-io-content">'+escapeHtml(lg.output)+'</pre>'+
1750
+ '</div>'+
1751
+ '</div>'+
1752
+ '</div>';
1753
+ }).join('');
1754
+ }
1755
+
1756
+ function toggleLog(i){
1757
+ const entry=document.getElementById('log-'+i);
1758
+ const d=document.getElementById('log-detail-'+i);
1759
+ if(d) d.classList.toggle('open');
1760
+ if(entry) entry.classList.toggle('expanded');
1761
+ }
1762
+
1763
+ function startLogAutoRefresh(){
1764
+ if(logAutoTimer) clearInterval(logAutoTimer);
1765
+ logAutoTimer=setInterval(()=>{
1766
+ const cb=document.getElementById('logAutoRefresh');
1767
+ const logsView=document.getElementById('logsView');
1768
+ if(cb&&cb.checked&&logsView&&logsView.classList.contains('show')){
1769
+ loadLogs();
1770
+ }
1771
+ },5000);
1772
+ }
1773
+
1774
+ function escapeHtml(s){
1775
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1776
+ }
1777
+
964
1778
  function setMetricsDays(d){
965
1779
  metricsDays=d;
966
1780
  document.querySelectorAll('.metrics-toolbar .range-btn').forEach(btn=>btn.classList.toggle('active',Number(btn.dataset.days)===d));
@@ -972,13 +1786,12 @@ async function loadMetrics(){
972
1786
  const d=await r.json();
973
1787
  document.getElementById('mTotal').textContent=formatNum(d.totals.memories);
974
1788
  document.getElementById('mTodayWrites').textContent=formatNum(d.totals.todayWrites);
975
- document.getElementById('mTodayCalls').textContent=formatNum(d.totals.todayViewerCalls);
976
1789
  document.getElementById('mSessions').textContent=formatNum(d.totals.sessions);
977
1790
  document.getElementById('mEmbeddings').textContent=formatNum(d.totals.embeddings);
978
1791
  renderChartWrites(d.writesPerDay);
979
- renderChartCalls(d.viewerCallsPerDay);
980
1792
  renderBreakdown(d.roleBreakdown,'breakdownRole');
981
1793
  renderBreakdown(d.kindBreakdown,'breakdownKind');
1794
+ loadToolMetrics();
982
1795
  }
983
1796
 
984
1797
  function formatNum(n){return n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'k':String(n);}
@@ -1035,21 +1848,22 @@ async function loadTasks(){
1035
1848
  return '<div class="task-card status-'+task.status+'" onclick="openTaskDetail(\\''+task.id+'\\')">'+
1036
1849
  '<div class="task-card-top">'+
1037
1850
  '<div class="task-card-title">'+esc(task.title)+'</div>'+
1038
- '<span class="task-status-badge '+task.status+'">'+task.status+'</span>'+
1851
+ '<span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span>'+
1039
1852
  '</div>'+
1040
- (task.summary?'<div class="task-card-summary">'+esc(task.summary)+'</div>':'')+
1853
+ (task.summary?'<div class="task-card-summary'+(task.status==='skipped'?' skipped-reason':'')+'">'+esc(task.summary)+'</div>':'')+
1041
1854
  '<div class="task-card-bottom">'+
1042
1855
  '<span class="tag"><span class="icon">\\u{1F4C5}</span> '+timeStr+'</span>'+
1043
1856
  (durationStr?'<span class="tag"><span class="icon">\\u23F1</span> '+durationStr+'</span>':'')+
1044
- '<span class="tag"><span class="icon">\\u{1F4DD}</span> '+task.chunkCount+' '+(task.chunkCount===1?'chunk':'chunks')+'</span>'+
1045
- '<span class="tag"><span class="icon">\\u{1F4C2}</span> '+task.sessionKey.slice(0,12)+'</span>'+
1857
+ '<span class="tag"><span class="icon">\\u{1F4DD}</span> '+task.chunkCount+' '+t('tasks.chunks.label')+'</span>'+
1858
+ '<span class="tag"><span class="icon">\\u{1F4C2}</span> '+(task.sessionKey||'').slice(0,12)+'</span>'+
1046
1859
  '</div>'+
1047
1860
  '</div>';
1048
1861
  }).join('');
1049
1862
 
1050
1863
  renderTasksPagination(data.total);
1051
1864
  }catch(e){
1052
- list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">Failed to load tasks</div>';
1865
+ console.error('loadTasks error:',e);
1866
+ list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">Failed to load tasks: '+String(e)+'</div>';
1053
1867
  }
1054
1868
  }
1055
1869
 
@@ -1072,6 +1886,8 @@ async function openTaskDetail(taskId){
1072
1886
  overlay.classList.add('show');
1073
1887
  document.getElementById('taskDetailTitle').textContent=t('tasks.loading');
1074
1888
  document.getElementById('taskDetailMeta').innerHTML='';
1889
+ document.getElementById('taskSkillSection').innerHTML='';
1890
+ document.getElementById('taskSkillSection').className='task-skill-section';
1075
1891
  document.getElementById('taskDetailSummary').textContent='';
1076
1892
  document.getElementById('taskDetailChunks').innerHTML='<div class="spinner"></div>';
1077
1893
 
@@ -1082,14 +1898,18 @@ async function openTaskDetail(taskId){
1082
1898
  document.getElementById('taskDetailTitle').textContent=task.title||t('tasks.untitled');
1083
1899
 
1084
1900
  const meta=[
1085
- '<span class="meta-item"><span class="task-status-badge '+task.status+'">'+task.status+'</span></span>',
1901
+ '<span class="meta-item"><span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span></span>',
1086
1902
  '<span class="meta-item">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>',
1087
1903
  ];
1088
1904
  if(task.endedAt) meta.push('<span class="meta-item">\\u2192 '+formatTime(task.endedAt)+'</span>');
1089
1905
  meta.push('<span class="meta-item">\\u{1F4C2} '+task.sessionKey+'</span>');
1090
- meta.push('<span class="meta-item">\\u{1F4DD} '+task.chunks.length+' chunks</span>');
1906
+ meta.push('<span class="meta-item">\\u{1F4DD} '+task.chunks.length+' '+t('tasks.chunks.label')+'</span>');
1907
+ meta.push('<div style="width:100%;margin-top:4px"><span class="meta-item" style="width:100%">'+t('tasks.taskid')+'<span class="task-id-full">'+esc(task.id)+'</span></span></div>');
1091
1908
  document.getElementById('taskDetailMeta').innerHTML=meta.join('');
1092
1909
 
1910
+ // ── Skill status section ──
1911
+ renderTaskSkillSection(task);
1912
+
1093
1913
  var summaryEl=document.getElementById('taskDetailSummary');
1094
1914
  if(task.status==='skipped'){
1095
1915
  summaryEl.innerHTML='<div style="color:var(--text-muted);font-style:italic;display:flex;align-items:flex-start;gap:8px"><span style="font-size:18px">\\u26A0\\uFE0F</span><span>'+esc(task.summary||t('tasks.skipped.default'))+'</span></div>';
@@ -1101,7 +1921,7 @@ async function openTaskDetail(taskId){
1101
1921
  document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--text-muted);padding:12px;font-size:13px">'+t('tasks.nochunks')+'</div>';
1102
1922
  }else{
1103
1923
  document.getElementById('taskDetailChunks').innerHTML=task.chunks.map(c=>{
1104
- var roleLabel=c.role==='user'?'You':c.role==='assistant'?'Assistant':c.role.toUpperCase();
1924
+ var roleLabel=c.role==='user'?t('tasks.role.user'):c.role==='assistant'?t('tasks.role.assistant'):c.role.toUpperCase();
1105
1925
  return '<div class="task-chunk-item role-'+c.role+'">'+
1106
1926
  '<div class="task-chunk-role '+c.role+'">'+roleLabel+'</div>'+
1107
1927
  '<div class="task-chunk-bubble" onclick="this.classList.toggle(\\\'expanded\\\')">'+esc(c.content)+'</div>'+
@@ -1110,8 +1930,52 @@ async function openTaskDetail(taskId){
1110
1930
  }).join('');
1111
1931
  }
1112
1932
  }catch(e){
1113
- document.getElementById('taskDetailTitle').textContent='Error';
1114
- document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--rose)">Failed to load task details</div>';
1933
+ document.getElementById('taskDetailTitle').textContent=t('tasks.error');
1934
+ document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--rose)">'+t('tasks.error.detail')+'</div>';
1935
+ }
1936
+ }
1937
+
1938
+ function renderTaskSkillSection(task){
1939
+ const section=document.getElementById('taskSkillSection');
1940
+ const ss=task.skillStatus;
1941
+ const links=task.skillLinks||[];
1942
+
1943
+ if(links.length>0){
1944
+ section.className='task-skill-section status-generated';
1945
+ var html='<div class="skill-status-header">\\u{1F527} \u5DF2\u751F\u6210\u6280\u80FD</div>';
1946
+ html+=links.map(function(lk){
1947
+ var relLabel={'generated_from':'\u7531\u6B64\u4EFB\u52A1\u751F\u6210','evolved_from':'\u7531\u6B64\u4EFB\u52A1\u5347\u7EA7','applied_to':'\u5173\u8054\u4F7F\u7528'}[lk.relation]||lk.relation;
1948
+ var statusLabel={'active':'\u6D3B\u8DC3','draft':'\u8349\u7A3F','archived':'\u5DF2\u5F52\u6863'}[lk.status]||lk.status;
1949
+ return '<div class="skill-link-card" onclick="event.stopPropagation();closeTaskDetail();switchView(\\'skills\\');setTimeout(function(){openSkillDetail(\\''+lk.skillId+'\\')},300)">'+
1950
+ '<div class="skill-link-name">'+esc(lk.skillName)+' <span style="font-size:11px;color:var(--text-sec)">('+relLabel+', v'+lk.versionAt+')</span></div>'+
1951
+ '<div class="skill-link-meta">'+
1952
+ '\u72B6\u6001: <span class="task-status-badge '+(lk.status||'active')+'">'+statusLabel+'</span>'+
1953
+ (lk.qualityScore!=null?' &middot; \u8D28\u91CF\u5206: '+lk.qualityScore+'/10':'')+
1954
+ '</div>'+
1955
+ '<div style="margin-top:4px"><span class="task-id-full">Skill ID: '+esc(lk.skillId)+'</span></div>'+
1956
+ '</div>';
1957
+ }).join('');
1958
+ section.innerHTML=html;
1959
+ }else if(ss==='generating'){
1960
+ section.className='task-skill-section status-generating';
1961
+ section.innerHTML='<div class="skill-status-header">\\u23F3 \u6280\u80FD\u751F\u6210\u4E2D...</div>'+
1962
+ '<div class="skill-status-reason">'+esc(task.skillReason||'')+'</div>';
1963
+ }else if(ss==='not_generated'){
1964
+ section.className='task-skill-section status-not_generated';
1965
+ section.innerHTML='<div class="skill-status-header">\\u274C \u672A\u751F\u6210\u6280\u80FD</div>'+
1966
+ '<div class="skill-status-reason">\u539F\u56E0\uFF1A'+esc(task.skillReason||'\u7ECF LLM \u8BC4\u4F30\uFF0C\u8BE5\u4EFB\u52A1\u4E0D\u9002\u5408\u63D0\u70BC\u4E3A\u53EF\u590D\u7528\u6280\u80FD\u3002')+'</div>';
1967
+ }else if(ss==='skipped'){
1968
+ section.className='task-skill-section status-skipped';
1969
+ section.innerHTML='<div class="skill-status-header">\\u23ED \u8DF3\u8FC7\u6280\u80FD\u8BC4\u4F30</div>'+
1970
+ '<div class="skill-status-reason">\u539F\u56E0\uFF1A'+esc(task.skillReason||'')+'</div>';
1971
+ }else if(task.status==='active'){
1972
+ section.className='task-skill-section status-skipped';
1973
+ section.innerHTML='<div class="skill-status-header">\\u23F8 \u4EFB\u52A1\u8FDB\u884C\u4E2D</div>'+
1974
+ '<div class="skill-status-reason">\u6280\u80FD\u8BC4\u4F30\u5728\u4EFB\u52A1\u5B8C\u6210\u540E\u81EA\u52A8\u8FD0\u884C\u3002</div>';
1975
+ }else{
1976
+ section.className='task-skill-section status-skipped';
1977
+ section.innerHTML='<div class="skill-status-header">\\u2014 \u65E0\u6280\u80FD\u4FE1\u606F</div>'+
1978
+ '<div class="skill-status-reason">\u8BE5\u4EFB\u52A1\u5728\u6280\u80FD\u8FDB\u5316\u7CFB\u7EDF\u542F\u7528\u4E4B\u524D\u5B8C\u6210\uFF0C\u65E0\u6280\u80FD\u8BC4\u4F30\u8BB0\u5F55\u3002</div>';
1115
1979
  }
1116
1980
  }
1117
1981
 
@@ -1120,6 +1984,294 @@ function closeTaskDetail(event){
1120
1984
  document.getElementById('taskDetailOverlay').classList.remove('show');
1121
1985
  }
1122
1986
 
1987
+ /* ─── Skills View Logic ─── */
1988
+ let skillsStatusFilter='';
1989
+
1990
+ function setSkillStatusFilter(btn,status){
1991
+ document.querySelectorAll('.skills-view .tasks-filters .filter-chip').forEach(c=>c.classList.remove('active'));
1992
+ btn.classList.add('active');
1993
+ skillsStatusFilter=status;
1994
+ loadSkills();
1995
+ }
1996
+
1997
+ async function loadSkills(){
1998
+ const list=document.getElementById('skillsList');
1999
+ list.innerHTML='<div class="spinner"></div>';
2000
+ try{
2001
+ const params=new URLSearchParams();
2002
+ if(skillsStatusFilter) params.set('status',skillsStatusFilter);
2003
+ const r=await fetch('/api/skills?'+params);
2004
+ const data=await r.json();
2005
+
2006
+ document.getElementById('skillsTotalCount').textContent=formatNum(data.skills.length);
2007
+ document.getElementById('skillsActiveCount').textContent=formatNum(data.skills.filter(s=>s.status==='active').length);
2008
+ document.getElementById('skillsDraftCount').textContent=formatNum(data.skills.filter(s=>s.status==='draft').length);
2009
+ document.getElementById('skillsInstalledCount').textContent=formatNum(data.skills.filter(s=>s.installed).length);
2010
+
2011
+ if(!data.skills||data.skills.length===0){
2012
+ list.innerHTML='<div style="text-align:center;padding:48px;color:var(--text-muted);font-size:14px">'+t('skills.empty')+'</div>';
2013
+ return;
2014
+ }
2015
+
2016
+ list.innerHTML=data.skills.map(skill=>{
2017
+ const timeStr=formatTime(skill.createdAt);
2018
+ const tags=parseTags(skill.tags);
2019
+ const installedClass=skill.installed?'installed':'';
2020
+ const statusClass=skill.status==='archived'?'archived':(skill.status==='draft'?'draft':'');
2021
+ const qs=skill.qualityScore;
2022
+ const qsBadge=qs!==null&&qs!==undefined?'<span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+qs.toFixed(1)+'</span>':'';
2023
+ return '<div class="skill-card '+installedClass+' '+statusClass+'" onclick="openSkillDetail(\\''+skill.id+'\\')">'+
2024
+ '<div class="skill-card-top">'+
2025
+ '<div class="skill-card-name">\\u{1F9E0} '+esc(skill.name)+'</div>'+
2026
+ '<div class="skill-card-badges">'+
2027
+ qsBadge+
2028
+ '<span class="skill-badge version">v'+skill.version+'</span>'+
2029
+ (skill.installed?'<span class="skill-badge installed">'+t('skills.installed.badge')+'</span>':'')+
2030
+ '<span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span>'+
2031
+ '</div>'+
2032
+ '</div>'+
2033
+ '<div class="skill-card-desc">'+esc(skill.description)+'</div>'+
2034
+ '<div class="skill-card-bottom">'+
2035
+ '<span class="tag"><span class="icon">\\u{1F4C5}</span> '+timeStr+'</span>'+
2036
+ '<span class="tag"><span class="icon">\\u{1F4E6}</span> '+skill.sourceType+'</span>'+
2037
+ (tags.length>0?'<div class="skill-card-tags">'+tags.map(t=>'<span class="skill-tag">'+esc(t)+'</span>').join('')+'</div>':'')+
2038
+ '</div>'+
2039
+ '</div>';
2040
+ }).join('');
2041
+ }catch(e){
2042
+ list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">Failed to load skills: '+esc(String(e))+'</div>';
2043
+ }
2044
+ }
2045
+
2046
+ function parseTags(tagsStr){
2047
+ try{ const arr=JSON.parse(tagsStr||'[]'); return Array.isArray(arr)?arr:[]; }catch{ return []; }
2048
+ }
2049
+
2050
+ let currentSkillId='';
2051
+
2052
+ async function openSkillDetail(skillId){
2053
+ currentSkillId=skillId;
2054
+ const overlay=document.getElementById('skillDetailOverlay');
2055
+ overlay.classList.add('show');
2056
+ document.getElementById('skillDetailTitle').textContent=t('skills.loading');
2057
+ document.getElementById('skillDetailMeta').innerHTML='';
2058
+ document.getElementById('skillDetailDesc').textContent='';
2059
+ document.getElementById('skillFilesList').innerHTML='';
2060
+ document.getElementById('skillDetailContent').innerHTML='<div class="spinner"></div>';
2061
+ document.getElementById('skillVersionsList').innerHTML='<div class="spinner"></div>';
2062
+ document.getElementById('skillRelatedTasks').innerHTML='';
2063
+
2064
+ try{
2065
+ const r=await fetch('/api/skill/'+skillId);
2066
+ if(!r.ok){
2067
+ const errText=await r.text();
2068
+ throw new Error('API '+r.status+': '+errText);
2069
+ }
2070
+ const data=await r.json();
2071
+ if(!data.skill){
2072
+ throw new Error('No skill data in response: '+JSON.stringify(data).slice(0,200));
2073
+ }
2074
+ const skill=data.skill;
2075
+ const versions=data.versions||[];
2076
+ const relatedTasks=data.relatedTasks||[];
2077
+ const files=data.files||[];
2078
+
2079
+ document.getElementById('skillDetailTitle').textContent='\\u{1F9E0} '+skill.name;
2080
+
2081
+ const qs=skill.qualityScore;
2082
+ const qsBadge=qs!==null&&qs!==undefined?'<span class="meta-item"><span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+qs.toFixed(1)+'/10</span></span>':'';
2083
+ document.getElementById('skillDetailMeta').innerHTML=[
2084
+ '<span class="meta-item"><span class="skill-badge version">v'+skill.version+'</span></span>',
2085
+ '<span class="meta-item"><span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span></span>',
2086
+ qsBadge,
2087
+ skill.installed?'<span class="meta-item"><span class="skill-badge installed">'+t('skills.installed.badge')+'</span></span>':'',
2088
+ '<span class="meta-item">\\u{1F4C5} '+formatTime(skill.createdAt)+'</span>',
2089
+ '<span class="meta-item">\\u270F '+t('skills.updated')+formatTime(skill.updatedAt)+'</span>',
2090
+ ].filter(Boolean).join('');
2091
+
2092
+ document.getElementById('skillDetailDesc').textContent=skill.description;
2093
+
2094
+ if(files.length>0){
2095
+ const fileIcons={'skill':'\\u{1F4D6}','script':'\\u{2699}','reference':'\\u{1F4CE}','file':'\\u{1F4C4}'};
2096
+ document.getElementById('skillFilesList').innerHTML=files.map(f=>
2097
+ '<div class="skill-file-item">'+
2098
+ '<span class="skill-file-icon">'+(fileIcons[f.type]||'\\u{1F4C4}')+'</span>'+
2099
+ '<span class="skill-file-name">'+esc(f.path)+'</span>'+
2100
+ '<span class="skill-file-type">'+f.type+'</span>'+
2101
+ '<span class="skill-file-size">'+(f.size>1024?(f.size/1024).toFixed(1)+'KB':f.size+'B')+'</span>'+
2102
+ '</div>'
2103
+ ).join('');
2104
+ } else {
2105
+ document.getElementById('skillFilesList').innerHTML='<div style="color:var(--text-muted);font-size:12px">'+t('skills.nofiles')+'</div>';
2106
+ }
2107
+
2108
+ const latestVersion=versions[0];
2109
+ document.getElementById('skillContentTitle').textContent=latestVersion?'SKILL.md (v'+latestVersion.version+')':t('skills.content');
2110
+ document.getElementById('skillDetailContent').innerHTML=latestVersion?renderSkillMarkdown(latestVersion.content):'<span style="color:var(--text-muted)">'+t('skills.nocontent')+'</span>';
2111
+
2112
+ if(versions.length===0){
2113
+ document.getElementById('skillVersionsList').innerHTML='<div style="color:var(--text-muted);font-size:13px">'+t('skills.noversions')+'</div>';
2114
+ } else {
2115
+ document.getElementById('skillVersionsList').innerHTML=versions.map(v=>{
2116
+ const vqs=v.qualityScore;
2117
+ const vqsBadge=vqs!==null&&vqs!==undefined?'<span class="skill-badge quality '+(vqs>=7?'high':vqs>=5?'mid':'low')+'">\\u2605 '+vqs.toFixed(1)+'</span>':'';
2118
+ const summaryHtml=v.changeSummary?'<div class="skill-version-summary">'+esc(v.changeSummary)+'</div>':'';
2119
+ return '<div class="skill-version-item">'+
2120
+ '<div class="skill-version-header">'+
2121
+ '<span class="skill-version-badge">v'+v.version+'</span>'+
2122
+ '<span class="skill-version-type">'+v.upgradeType+'</span>'+
2123
+ vqsBadge+
2124
+ '</div>'+
2125
+ '<div class="skill-version-changelog">'+esc(v.changelog||t('skills.nochangelog'))+'</div>'+
2126
+ summaryHtml+
2127
+ '<div class="skill-version-time">'+formatTime(v.createdAt)+(v.sourceTaskId?' \\u2022 '+t('skills.task.prefix')+v.sourceTaskId.slice(0,8)+'...':'')+'</div>'+
2128
+ '</div>';
2129
+ }).join('');
2130
+ }
2131
+
2132
+ if(relatedTasks.length===0){
2133
+ document.getElementById('skillRelatedTasks').innerHTML='<div style="color:var(--text-muted);font-size:13px">'+t('skills.norelated')+'</div>';
2134
+ } else {
2135
+ document.getElementById('skillRelatedTasks').innerHTML=relatedTasks.map(rt=>
2136
+ '<div class="skill-related-task" onclick="event.stopPropagation();closeSkillDetail();switchView(\\'tasks\\');setTimeout(()=>openTaskDetail(\\''+rt.task.id+'\\'),300)">'+
2137
+ '<span class="relation">'+rt.relation+'</span>'+
2138
+ '<span class="task-title">'+esc(rt.task.title||t('tasks.untitled.related'))+'</span>'+
2139
+ '<span style="font-size:11px;color:var(--text-muted)">'+formatTime(rt.task.startedAt)+'</span>'+
2140
+ '</div>'
2141
+ ).join('');
2142
+ }
2143
+
2144
+ }catch(e){
2145
+ document.getElementById('skillDetailTitle').textContent=t('skills.error');
2146
+ document.getElementById('skillDetailContent').innerHTML='<div style="color:var(--rose);padding:16px">'+t('skills.error.detail')+esc(String(e))+'</div>';
2147
+ document.getElementById('skillFilesList').innerHTML='';
2148
+ document.getElementById('skillVersionsList').innerHTML='';
2149
+ document.getElementById('skillRelatedTasks').innerHTML='';
2150
+ }
2151
+ }
2152
+
2153
+ function downloadSkill(){
2154
+ if(!currentSkillId) return;
2155
+ window.open('/api/skill/'+currentSkillId+'/download','_blank');
2156
+ }
2157
+
2158
+ /* ─── Settings / Config ─── */
2159
+ async function loadConfig(){
2160
+ try{
2161
+ const r=await fetch('/api/config');
2162
+ if(!r.ok) return;
2163
+ const cfg=await r.json();
2164
+ const emb=cfg.embedding||{};
2165
+ document.getElementById('cfgEmbProvider').value=emb.provider||'openai_compatible';
2166
+ document.getElementById('cfgEmbModel').value=emb.model||'';
2167
+ document.getElementById('cfgEmbEndpoint').value=emb.endpoint||'';
2168
+ document.getElementById('cfgEmbApiKey').value=emb.apiKey||'';
2169
+
2170
+ const sum=cfg.summarizer||{};
2171
+ document.getElementById('cfgSumProvider').value=sum.provider||'openai_compatible';
2172
+ document.getElementById('cfgSumModel').value=sum.model||'';
2173
+ document.getElementById('cfgSumEndpoint').value=sum.endpoint||'';
2174
+ document.getElementById('cfgSumApiKey').value=sum.apiKey||'';
2175
+ document.getElementById('cfgSumTemp').value=sum.temperature!=null?sum.temperature:'';
2176
+
2177
+ const sk=cfg.skillEvolution||{};
2178
+ document.getElementById('cfgSkillEnabled').checked=sk.enabled!==false;
2179
+ document.getElementById('cfgSkillAutoInstall').checked=!!sk.autoInstall;
2180
+ document.getElementById('cfgSkillConfidence').value=sk.minConfidence||'';
2181
+ document.getElementById('cfgSkillMinChunks').value=sk.minChunksForEval||'';
2182
+
2183
+ const skSum=sk.summarizer||{};
2184
+ document.getElementById('cfgSkillProviderDefault').textContent='\u2014 '+t('settings.skill.usemain');
2185
+ document.getElementById('cfgSkillProvider').value=skSum.provider||'';
2186
+ document.getElementById('cfgSkillModel').value=skSum.model||'';
2187
+ document.getElementById('cfgSkillEndpoint').value=skSum.endpoint||'';
2188
+ document.getElementById('cfgSkillApiKey').value=skSum.apiKey||'';
2189
+ document.getElementById('cfgSkillTemp').value=skSum.temperature!=null?skSum.temperature:'';
2190
+
2191
+ document.getElementById('cfgViewerPort').value=cfg.viewerPort||'';
2192
+ }catch(e){
2193
+ console.error('loadConfig error',e);
2194
+ }
2195
+ }
2196
+
2197
+ async function saveConfig(){
2198
+ const cfg={};
2199
+ const embP=document.getElementById('cfgEmbProvider').value;
2200
+ if(embP){
2201
+ cfg.embedding={provider:embP};
2202
+ const v=document.getElementById('cfgEmbModel').value.trim();if(v) cfg.embedding.model=v;
2203
+ const e=document.getElementById('cfgEmbEndpoint').value.trim();if(e) cfg.embedding.endpoint=e;
2204
+ const k=document.getElementById('cfgEmbApiKey').value.trim();if(k) cfg.embedding.apiKey=k;
2205
+ }
2206
+ const sumP=document.getElementById('cfgSumProvider').value;
2207
+ if(sumP){
2208
+ cfg.summarizer={provider:sumP};
2209
+ const v=document.getElementById('cfgSumModel').value.trim();if(v) cfg.summarizer.model=v;
2210
+ const e=document.getElementById('cfgSumEndpoint').value.trim();if(e) cfg.summarizer.endpoint=e;
2211
+ const k=document.getElementById('cfgSumApiKey').value.trim();if(k) cfg.summarizer.apiKey=k;
2212
+ const tp=document.getElementById('cfgSumTemp').value.trim();if(tp!=='') cfg.summarizer.temperature=Number(tp);
2213
+ }
2214
+ cfg.skillEvolution={
2215
+ enabled:document.getElementById('cfgSkillEnabled').checked,
2216
+ autoInstall:document.getElementById('cfgSkillAutoInstall').checked
2217
+ };
2218
+ const mc=document.getElementById('cfgSkillConfidence').value.trim();if(mc) cfg.skillEvolution.minConfidence=Number(mc);
2219
+ const mk=document.getElementById('cfgSkillMinChunks').value.trim();if(mk) cfg.skillEvolution.minChunksForEval=Number(mk);
2220
+
2221
+ const skP=document.getElementById('cfgSkillProvider').value;
2222
+ if(skP){
2223
+ cfg.skillEvolution.summarizer={provider:skP};
2224
+ const v=document.getElementById('cfgSkillModel').value.trim();if(v) cfg.skillEvolution.summarizer.model=v;
2225
+ const e=document.getElementById('cfgSkillEndpoint').value.trim();if(e) cfg.skillEvolution.summarizer.endpoint=e;
2226
+ const k=document.getElementById('cfgSkillApiKey').value.trim();if(k) cfg.skillEvolution.summarizer.apiKey=k;
2227
+ const tp=document.getElementById('cfgSkillTemp').value.trim();if(tp!=='') cfg.skillEvolution.summarizer.temperature=Number(tp);
2228
+ }
2229
+
2230
+ const vp=document.getElementById('cfgViewerPort').value.trim();
2231
+ if(vp) cfg.viewerPort=Number(vp);
2232
+
2233
+ try{
2234
+ const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});
2235
+ if(!r.ok) throw new Error(await r.text());
2236
+ const el=document.getElementById('settingsSaved');
2237
+ el.classList.add('show');
2238
+ setTimeout(()=>el.classList.remove('show'),2500);
2239
+ }catch(e){
2240
+ showToast(t('settings.save.fail')+': '+e.message,'error');
2241
+ }
2242
+ }
2243
+
2244
+ function renderSkillMarkdown(md){
2245
+ let content=md;
2246
+ // Strip YAML frontmatter
2247
+ content=content.replace(/^---[\\s\\S]*?---\\s*/,'');
2248
+ // Code blocks
2249
+ content=content.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g,function(_,lang,code){
2250
+ return '<pre style="background:rgba(0,0,0,.3);border:1px solid var(--border);border-radius:8px;padding:12px 16px;overflow-x:auto;font-size:12px;line-height:1.5;font-family:SF Mono,Monaco,Consolas,monospace"><code>'+esc(code.trim())+'</code></pre>';
2251
+ });
2252
+ // Inline code
2253
+ content=content.replace(/\`([^\`]+)\`/g,'<code style="background:rgba(139,92,246,.1);color:var(--violet);padding:1px 6px;border-radius:4px;font-size:12px">$1</code>');
2254
+ // Headers
2255
+ content=content.replace(/^### (.+)$/gm,'<div class="summary-section-title" style="font-size:13px;margin-top:12px">$1</div>');
2256
+ content=content.replace(/^## (.+)$/gm,'<div class="summary-section-title">$1</div>');
2257
+ content=content.replace(/^# (.+)$/gm,'<div style="font-size:16px;font-weight:700;color:var(--text);margin:8px 0">$1</div>');
2258
+ // Bold
2259
+ content=content.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
2260
+ // List items
2261
+ content=content.replace(/^- (.+)$/gm,'<div style="padding-left:16px;position:relative;margin:3px 0"><span style="position:absolute;left:4px;color:var(--text-muted)">•</span>$1</div>');
2262
+ // HTML comments (version markers)
2263
+ content=content.replace(/<!--[\\s\\S]*?-->/g,'');
2264
+ // Line breaks
2265
+ content=content.replace(/\\n\\n/g,'<div style="height:10px"></div>');
2266
+ content=content.replace(/\\n/g,'<br>');
2267
+ return content;
2268
+ }
2269
+
2270
+ function closeSkillDetail(event){
2271
+ if(event && event.target!==document.getElementById('skillDetailOverlay')) return;
2272
+ document.getElementById('skillDetailOverlay').classList.remove('show');
2273
+ }
2274
+
1123
2275
  function formatDuration(ms){
1124
2276
  const s=Math.floor(ms/1000);
1125
2277
  if(s<60) return s+'s';
@@ -1166,14 +2318,16 @@ function renderBars(el,data,valueKey,H){
1166
2318
  const vals=data.map(d=>d[valueKey]??0);
1167
2319
  if(vals.every(v=>v===0)){el.innerHTML='<div style="color:var(--text-muted);font-size:13px;padding:20px;text-align:center">'+t('chart.nodata')+'</div>';return;}
1168
2320
  const max=Math.max(1,...vals);
2321
+ const nonZero=vals.filter(v=>v>0).length;
2322
+ const barStyle=data.length<=7?'min-width:40px;max-width:120px':'';
1169
2323
  el.innerHTML=data.map(r=>{
1170
2324
  const v=r[valueKey]??0;
1171
2325
  const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
1172
2326
  if(v===0){
1173
- 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>';
2327
+ return '<div class="chart-bar-wrap" style="'+barStyle+'"><div class="chart-tip">0</div><div class="chart-bar-col"><div class="chart-bar zero" style="height:2px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
1174
2328
  }
1175
2329
  const h=Math.max(8,Math.round((v/max)*H));
1176
- 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>';
2330
+ return '<div class="chart-bar-wrap" style="'+barStyle+'"><div class="chart-tip">'+v+'</div><div class="chart-bar-col"><div class="chart-bar" style="height:'+h+'px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
1177
2331
  }).join('');
1178
2332
  }
1179
2333
 
@@ -1206,6 +2360,182 @@ function renderChartCalls(rows){
1206
2360
  }).join('');
1207
2361
  }
1208
2362
 
2363
+ /* ─── Tool Performance Chart ─── */
2364
+ let toolMinutes=60;
2365
+ const TOOL_COLORS=['#818cf8','#34d399','#fbbf24','#f87171','#38bdf8','#a78bfa','#fb923c'];
2366
+
2367
+ function setToolMinutes(m){
2368
+ toolMinutes=m;
2369
+ document.querySelectorAll('.tool-range').forEach(b=>{
2370
+ b.classList.toggle('active',Number(b.dataset.mins)===m);
2371
+ });
2372
+ loadToolMetrics();
2373
+ }
2374
+
2375
+ async function loadToolMetrics(){
2376
+ try{
2377
+ const r=await fetch('/api/tool-metrics?minutes='+toolMinutes);
2378
+ if(!r.ok) return;
2379
+ const d=await r.json();
2380
+ if(d.error) return;
2381
+ renderToolChart(d);
2382
+ renderToolAgg(d);
2383
+ }catch(e){
2384
+ console.warn('loadToolMetrics error:',e);
2385
+ }
2386
+ }
2387
+
2388
+ function renderToolChart(data){
2389
+ const container=document.getElementById('toolChart');
2390
+ const legend=document.getElementById('toolLegend');
2391
+ const {tools,series}=data;
2392
+
2393
+ if(!series||series.length===0||tools.length===0){
2394
+ container.innerHTML='<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:var(--text-muted)"><div style="font-size:36px;opacity:.25">\u{1F4CA}</div><div style="font-size:13px;font-weight:500">Waiting for tool calls...</div><div style="font-size:11px;opacity:.6">Charts will render once the agent uses memory tools</div></div>';
2395
+ legend.innerHTML='';
2396
+ return;
2397
+ }
2398
+
2399
+ const W=container.clientWidth||800;
2400
+ const H=280;
2401
+ const pad={t:20,r:20,b:36,l:52};
2402
+ const cw=W-pad.l-pad.r;
2403
+ const ch=H-pad.t-pad.b;
2404
+
2405
+ let maxVal=0;
2406
+ for(const s of series){for(const t of tools){const v=s[t]||0;if(v>maxVal)maxVal=v;}}
2407
+ if(maxVal===0)maxVal=100;
2408
+ maxVal=Math.ceil(maxVal*1.15);
2409
+
2410
+ const gridLines=5;
2411
+ let gridHtml='';
2412
+ for(let i=0;i<=gridLines;i++){
2413
+ const y=pad.t+ch-(ch/gridLines)*i;
2414
+ const val=Math.round((maxVal/gridLines)*i);
2415
+ gridHtml+='<line class="grid-line" x1="'+pad.l+'" y1="'+y+'" x2="'+(W-pad.r)+'" y2="'+y+'"/>';
2416
+ gridHtml+='<text class="axis-label" x="'+(pad.l-8)+'" y="'+(y+3)+'" text-anchor="end">'+val+'ms</text>';
2417
+ }
2418
+
2419
+ const step=cw/(series.length-1||1);
2420
+ const labelEvery=Math.max(1,Math.floor(series.length/8));
2421
+ let labelsHtml='';
2422
+ series.forEach((s,i)=>{
2423
+ if(i%labelEvery===0||i===series.length-1){
2424
+ const x=pad.l+i*step;
2425
+ const time=s.minute.slice(11);
2426
+ labelsHtml+='<text class="axis-label" x="'+x+'" y="'+(H-4)+'" text-anchor="middle">'+time+'</text>';
2427
+ }
2428
+ });
2429
+
2430
+ let pathsHtml='';
2431
+ let dotsHtml='';
2432
+ tools.forEach((toolName,ti)=>{
2433
+ const color=TOOL_COLORS[ti%TOOL_COLORS.length];
2434
+ const pts=series.map((s,i)=>{
2435
+ const x=pad.l+i*step;
2436
+ const v=s[toolName]||0;
2437
+ const y=pad.t+ch-((v/maxVal)*ch);
2438
+ return {x,y,v};
2439
+ });
2440
+ let line='M'+pts[0].x.toFixed(1)+' '+pts[0].y.toFixed(1);
2441
+ for(let i=1;i<pts.length;i++){
2442
+ const p0=pts[Math.max(0,i-2)],p1=pts[i-1],p2=pts[i],p3=pts[Math.min(pts.length-1,i+1)];
2443
+ const cp1x=(p1.x+(p2.x-p0.x)/6).toFixed(1),cp1y=(p1.y+(p2.y-p0.y)/6).toFixed(1);
2444
+ const cp2x=(p2.x-(p3.x-p1.x)/6).toFixed(1),cp2y=(p2.y-(p3.y-p1.y)/6).toFixed(1);
2445
+ line+=' C'+cp1x+' '+cp1y+','+cp2x+' '+cp2y+','+p2.x.toFixed(1)+' '+p2.y.toFixed(1);
2446
+ }
2447
+ pathsHtml+='<path class="data-line" d="'+line+'" stroke="'+color+'" />';
2448
+ const area=line+' L'+pts[pts.length-1].x.toFixed(1)+' '+(pad.t+ch)+' L'+pts[0].x.toFixed(1)+' '+(pad.t+ch)+' Z';
2449
+ pathsHtml+='<path class="data-area" d="'+area+'" fill="url(#tg'+ti+')" />';
2450
+ pts.forEach((p,i)=>{
2451
+ dotsHtml+='<circle class="hover-dot" cx="'+p.x.toFixed(1)+'" cy="'+p.y.toFixed(1)+'" fill="'+color+'" data-tool="'+toolName+'" data-idx="'+i+'" data-val="'+p.v+'" />';
2452
+ });
2453
+ });
2454
+
2455
+ const svg='<svg class="tool-chart-svg" viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="xMidYMid meet">'+
2456
+ '<defs>'+
2457
+ tools.map((t,i)=>{
2458
+ const c=TOOL_COLORS[i%TOOL_COLORS.length];
2459
+ return '<linearGradient id="tg'+i+'" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="'+c+'" stop-opacity=".08"/><stop offset="1" stop-color="'+c+'" stop-opacity="0"/></linearGradient>'+
2460
+ '';
2461
+ }).join('')+'</defs>'+
2462
+
2463
+ gridHtml+labelsHtml+pathsHtml+dotsHtml+
2464
+ '<line class="crosshair" x1="0" y1="'+pad.t+'" x2="0" y2="'+(pad.t+ch)+'" stroke="var(--text-muted)" stroke-width="0.5" stroke-dasharray="3 3" opacity="0" />'+
2465
+ '<rect class="hover-rect" x="'+pad.l+'" y="'+pad.t+'" width="'+cw+'" height="'+ch+'" fill="transparent" />'+
2466
+ '</svg><div class="tool-chart-tooltip" id="toolTooltip"></div>';
2467
+
2468
+ container.innerHTML=svg;
2469
+
2470
+ legend.innerHTML=tools.map((t,i)=>{
2471
+ const c=TOOL_COLORS[i%TOOL_COLORS.length];
2472
+ return '<span><span class="dot" style="background:'+c+'"></span>'+t+'</span>';
2473
+ }).join('');
2474
+
2475
+ const svgEl=container.querySelector('svg');
2476
+ const tooltip=document.getElementById('toolTooltip');
2477
+ const rect=svgEl.querySelector('.hover-rect');
2478
+
2479
+ rect.addEventListener('mousemove',function(e){
2480
+ const r=svgEl.getBoundingClientRect();
2481
+ const mx=e.clientX-r.left;
2482
+ const scale=W/r.width;
2483
+ const dataX=(mx*scale-pad.l)/step;
2484
+ const idx=Math.max(0,Math.min(series.length-1,Math.round(dataX)));
2485
+ const s=series[idx];
2486
+ if(!s)return;
2487
+
2488
+ svgEl.querySelectorAll('.hover-dot').forEach(d=>{
2489
+ d.classList.toggle('show',Number(d.dataset.idx)===idx);
2490
+ });
2491
+ const crosshair=svgEl.querySelector('.crosshair');
2492
+ const cx=pad.l+idx*step;
2493
+ crosshair.setAttribute('x1',cx);crosshair.setAttribute('x2',cx);crosshair.setAttribute('opacity','0.5');
2494
+
2495
+ let rows='<div class="tt-time">'+s.minute+'</div>';
2496
+ tools.forEach((t,ti)=>{
2497
+ const v=s[t]||0;
2498
+ const c=TOOL_COLORS[ti%TOOL_COLORS.length];
2499
+ rows+='<div class="tt-row"><span class="tt-dot" style="background:'+c+'"></span>'+t+'<span class="tt-val">'+v+'ms</span></div>';
2500
+ });
2501
+ tooltip.innerHTML=rows;
2502
+ tooltip.classList.add('show');
2503
+
2504
+ const tx=e.clientX-container.getBoundingClientRect().left;
2505
+ const ty=e.clientY-container.getBoundingClientRect().top;
2506
+ tooltip.style.left=(tx+15)+'px';
2507
+ tooltip.style.top=(ty-10)+'px';
2508
+ if(tx>container.clientWidth*0.7) tooltip.style.left=(tx-tooltip.offsetWidth-15)+'px';
2509
+ });
2510
+
2511
+ rect.addEventListener('mouseleave',function(){
2512
+ svgEl.querySelectorAll('.hover-dot').forEach(d=>d.classList.remove('show'));
2513
+ svgEl.querySelector('.crosshair').setAttribute('opacity','0');
2514
+ tooltip.classList.remove('show');
2515
+ });
2516
+ }
2517
+
2518
+ function renderToolAgg(data){
2519
+ const el=document.getElementById('toolAggTable');
2520
+ const {aggregated}=data;
2521
+ if(!aggregated||aggregated.length===0){el.innerHTML='';return;}
2522
+
2523
+ const msClass=v=>v<100?'fast':v<500?'medium':'slow';
2524
+
2525
+ el.innerHTML='<table class="tool-agg-table"><thead><tr><th>Tool</th><th>Calls</th><th>Avg</th><th>P95</th><th>Errors</th></tr></thead><tbody>'+
2526
+ aggregated.map((a,i)=>{
2527
+ const c=TOOL_COLORS[i%TOOL_COLORS.length];
2528
+ return '<tr>'+
2529
+ '<td><span class="tool-name"><span class="tool-dot" style="background:'+c+'"></span>'+a.tool+'</span></td>'+
2530
+ '<td>'+a.totalCalls+'</td>'+
2531
+ '<td><span class="ms-val '+msClass(a.avgMs)+'">'+a.avgMs+'ms</span></td>'+
2532
+ '<td><span class="ms-val '+msClass(a.p95Ms)+'">'+a.p95Ms+'ms</span></td>'+
2533
+ '<td>'+(a.errorCount>0?'<span style="color:var(--accent)">'+a.errorCount+'</span>':'<span style="color:var(--text-muted)">0</span>')+'</td>'+
2534
+ '</tr>';
2535
+ }).join('')+
2536
+ '</tbody></table>';
2537
+ }
2538
+
1209
2539
  function renderBreakdown(obj,containerId){
1210
2540
  const el=document.getElementById(containerId);
1211
2541
  if(!el)return;
@@ -1225,7 +2555,13 @@ async function loadAll(){
1225
2555
  async function loadStats(){
1226
2556
  const r=await fetch('/api/stats');
1227
2557
  const d=await r.json();
2558
+ const dedupB=d.dedupBreakdown||{};
2559
+ const activeCount=dedupB.active||d.totalMemories;
2560
+ const inactiveCount=(dedupB.duplicate||0)+(dedupB.merged||0);
1228
2561
  document.getElementById('statTotal').textContent=d.totalMemories;
2562
+ if(inactiveCount>0){
2563
+ document.getElementById('statTotal').title=activeCount+' '+t('stat.active')+', '+inactiveCount+' '+t('stat.deduped');
2564
+ }
1229
2565
  document.getElementById('statSessions').textContent=d.totalSessions;
1230
2566
  document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;
1231
2567
  const days=d.timeRange.earliest?Math.max(1,Math.round((new Date(d.timeRange.latest)-new Date(d.timeRange.earliest))/(86400000))):0;
@@ -1347,12 +2683,44 @@ function renderMemories(items){
1347
2683
  const vscore=m._vscore?'<span class="vscore-badge">'+Math.round(m._vscore*100)+'%</span>':'';
1348
2684
  const sid=m.session_key||'';
1349
2685
  const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;
1350
- return '<div class="memory-card">'+
1351
- '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span><span class="kind-tag">'+kind+'</span></div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+'</span></div>'+
2686
+ const mc=m.merge_count||0;
2687
+ const mergeBadge=mc>0?'<span class="merge-badge">\\u{1F504} '+t('card.evolved')+' '+mc+t('card.times')+'</span>':'';
2688
+ const updatedAt=(m.updated_at&&m.updated_at>m.created_at)?'<span class="card-updated">'+t('card.updated')+' '+new Date(m.updated_at).toLocaleString('zh-CN')+'</span>':'';
2689
+ const ds=m.dedup_status||'active';
2690
+ const isInactive=ds==='duplicate'||ds==='merged';
2691
+ const dedupBadge=ds==='duplicate'?'<span class="dedup-badge duplicate">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class="dedup-badge merged">'+t('card.dedupMerged')+'</span>':'';
2692
+ let dedupInfo='';
2693
+ if(isInactive){
2694
+ const reason=m.dedup_reason?'<span style="font-size:11px;color:var(--text-muted)">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';
2695
+ const target=m.dedup_target?'<span class="dedup-target-link" onclick="scrollToMemory(\\''+m.dedup_target+'\\')">'+t('card.dedupTarget')+m.dedup_target.slice(0,8)+'...</span>':'';
2696
+ dedupInfo='<div style="margin-top:6px;font-size:11px">'+target+' '+reason+'</div>';
2697
+ }
2698
+ let historyHtml='';
2699
+ if(mc>0){
2700
+ try{
2701
+ const hist=JSON.parse(m.merge_history||'[]');
2702
+ if(hist.length>0){
2703
+ historyHtml='<div class="merge-history" id="history-'+id+'" style="display:none"><div style="font-weight:600;margin-bottom:8px;font-size:12px">'+t('card.evolveHistory')+' ('+hist.length+')</div>';
2704
+ hist.forEach(function(h){
2705
+ const ht=h.at?new Date(h.at).toLocaleString('zh-CN'):'';
2706
+ historyHtml+='<div class="merge-history-item"><span class="merge-action '+h.action+'">'+h.action+'</span> <span style="color:var(--text-muted)">'+ht+'</span><br>'+esc(h.reason||'');
2707
+ if(h.from) historyHtml+='<br><span style="opacity:.6">'+t('card.oldSummary')+':</span> '+esc(h.from);
2708
+ if(h.to) historyHtml+='<br><span style="opacity:.6">'+t('card.newSummary')+':</span> '+esc(h.to);
2709
+ historyHtml+='</div>';
2710
+ });
2711
+ historyHtml+='</div>';
2712
+ }
2713
+ }catch(e){}
2714
+ }
2715
+ return '<div class="memory-card'+(isInactive?' dedup-inactive':'')+'">'+
2716
+ '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span><span class="kind-tag">'+kind+'</span>'+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
1352
2717
  '<div class="card-summary">'+summary+'</div>'+
2718
+ dedupInfo+
1353
2719
  '<div class="card-content" id="content-'+id+'"><pre>'+content+'</pre></div>'+
2720
+ historyHtml+
1354
2721
  '<div class="card-actions">'+
1355
2722
  '<button class="btn btn-sm btn-ghost" onclick="toggleContent(\\''+id+'\\')">'+t('card.expand')+'</button>'+
2723
+ (mc>0?'<button class="btn btn-sm btn-ghost" onclick="toggleHistory(\\''+id+'\\')">'+t('card.evolveHistory')+'</button>':'')+
1356
2724
  '<button class="btn btn-sm btn-ghost" onclick="openEditModal(\\''+id+'\\')">'+t('card.edit')+'</button>'+
1357
2725
  '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteMemory(\\''+id+'\\')">'+t('card.delete')+'</button>'+
1358
2726
  vscore+
@@ -1388,11 +2756,67 @@ function goPage(p){
1388
2756
  document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});
1389
2757
  }
1390
2758
 
2759
+ function toggleHistory(id){
2760
+ const el=document.getElementById('history-'+id);
2761
+ if(el) el.style.display=el.style.display==='none'?'block':'none';
2762
+ }
2763
+
1391
2764
  function toggleContent(id){
1392
2765
  const el=document.getElementById('content-'+id);
1393
2766
  el.classList.toggle('show');
1394
2767
  }
1395
2768
 
2769
+ function scrollToMemory(targetId){
2770
+ const cards=document.querySelectorAll('.memory-card');
2771
+ for(const card of cards){
2772
+ const contentEl=card.querySelector('[id^="content-"]');
2773
+ if(contentEl&&contentEl.id==='content-'+targetId){
2774
+ card.scrollIntoView({behavior:'smooth',block:'center'});
2775
+ card.style.transition='box-shadow .3s';
2776
+ card.style.boxShadow='0 0 0 2px var(--pri)';
2777
+ setTimeout(()=>{card.style.boxShadow='';},2000);
2778
+ return;
2779
+ }
2780
+ }
2781
+ showMemoryModal(targetId);
2782
+ }
2783
+ async function showMemoryModal(chunkId){
2784
+ const overlay=document.getElementById('memoryModal');
2785
+ const body=document.getElementById('memoryModalBody');
2786
+ body.innerHTML='<div style="text-align:center;padding:40px;color:var(--text-sec)">Loading...</div>';
2787
+ overlay.classList.add('show');
2788
+ try{
2789
+ const res=await fetch('/api/memory/'+encodeURIComponent(chunkId));
2790
+ if(!res.ok){body.innerHTML='<div style="text-align:center;padding:40px;color:#f87171">Memory not found</div>';return;}
2791
+ const data=await res.json();
2792
+ const m=data.memory;
2793
+ const role=(m.role||'unknown').toUpperCase();
2794
+ const roleCls=(m.role||'').toLowerCase();
2795
+ const kind=m.kind||'paragraph';
2796
+ const ds=m.dedup_status||'active';
2797
+ const time=new Date(m.created_at).toLocaleString('zh-CN');
2798
+ const updated=m.updated_at?new Date(m.updated_at).toLocaleString('zh-CN'):'';
2799
+ let html='<div class="modal-memory-card">';
2800
+ html+='<div class="modal-header-row"><span class="role-tag '+roleCls+'">'+role+'</span><span class="kind-tag">'+kind+'</span>';
2801
+ if(ds!=='active') html+='<span class="dedup-badge '+(ds==='duplicate'?'duplicate':'merged')+'">'+ds+'</span>';
2802
+ html+='</div>';
2803
+ html+='<div class="modal-field"><div class="modal-field-label">ID</div><div class="modal-field-val" style="font-family:monospace;font-size:11px">'+esc(m.id)+'</div></div>';
2804
+ html+='<div class="modal-field"><div class="modal-field-label">Summary</div><div class="modal-field-val" style="font-size:14px;font-weight:600">'+esc(m.summary||'')+'</div></div>';
2805
+ html+='<div class="modal-field"><div class="modal-field-label">Content</div><pre class="modal-field-content">'+esc(m.content||'')+'</pre></div>';
2806
+ html+='<div class="modal-meta-row">';
2807
+ html+='<span><strong>Session:</strong> '+esc(m.session_key||'')+'</span>';
2808
+ html+='<span><strong>Created:</strong> '+time+'</span>';
2809
+ if(updated) html+='<span><strong>Updated:</strong> '+updated+'</span>';
2810
+ html+='</div>';
2811
+ if(m.dedup_reason) html+='<div class="modal-field"><div class="modal-field-label">Dedup Reason</div><div class="modal-field-val">'+esc(m.dedup_reason)+'</div></div>';
2812
+ if(m.dedup_target&&m.dedup_target!==chunkId) html+='<div class="modal-field"><span class="dedup-target-link" onclick="closeMemoryModal();scrollToMemory(\\''+m.dedup_target+'\\')">View target: '+m.dedup_target.slice(0,8)+'...</span></div>';
2813
+ html+='</div>';
2814
+ body.innerHTML=html;
2815
+ }catch(e){body.innerHTML='<div style="text-align:center;padding:40px;color:#f87171">Error: '+esc(String(e))+'</div>';}
2816
+ }
2817
+ function closeMemoryModal(){document.getElementById('memoryModal').classList.remove('show');}
2818
+
2819
+
1396
2820
  function esc(s){
1397
2821
  if(!s)return'';
1398
2822
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
@@ -1518,5 +2942,17 @@ document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==
1518
2942
  applyI18n();
1519
2943
  checkAuth();
1520
2944
  </script>
2945
+
2946
+ <!-- Memory Detail Modal -->
2947
+ <div class="memory-modal-overlay" id="memoryModal" onclick="if(event.target===this)closeMemoryModal()">
2948
+ <div class="memory-modal">
2949
+ <div class="memory-modal-title">
2950
+ <span>Memory Detail</span>
2951
+ <button class="btn btn-sm btn-ghost" onclick="closeMemoryModal()" style="font-size:16px;padding:2px 8px">&times;</button>
2952
+ </div>
2953
+ <div class="memory-modal-body" id="memoryModalBody"></div>
2954
+ </div>
2955
+ </div>
2956
+
1521
2957
  </body>
1522
2958
  </html>`;