@memtensor/memos-local-openclaw-plugin 0.3.7 → 0.3.8

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.
@@ -1,2 +1,2 @@
1
- export declare const viewerHTML = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>OpenClaw Memory - Powered by MemOS</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\n:root{\n --bg:#0b0d11;--bg-card:#12141a;--bg-card-hover:#1a1d25;\n --border:rgba(255,255,255,.08);--border-glow:rgba(255,255,255,.14);\n --text:#e8eaed;--text-sec:#8b8fa4;--text-muted:#555a6e;\n --pri:#818cf8;--pri-glow:rgba(129,140,248,.1);--pri-dark:#6366f1;\n --pri-grad:linear-gradient(135deg,#818cf8,#6366f1);\n --accent:#ef4444;--accent-glow:rgba(239,68,68,.1);\n --green:#34d399;--green-bg:rgba(52,211,153,.08);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.08);\n --violet:#818cf8;--rose:#ef4444;--rose-bg:rgba(239,68,68,.08);\n --shadow-sm:0 1px 2px rgba(0,0,0,.3);--shadow:0 4px 12px rgba(0,0,0,.35);\n --shadow-lg:0 20px 40px rgba(0,0,0,.45);\n --radius:12px;--radius-lg:14px;--radius-xl:18px;\n}\n[data-theme=\"light\"]{\n --bg:#f8f9fb;--bg-card:#fff;--bg-card-hover:#f3f4f6;\n --border:#e2e4e9;--border-glow:#cbd0d8;\n --text:#111827;--text-sec:#4b5563;--text-muted:#9ca3af;\n --pri:#4f46e5;--pri-glow:rgba(79,70,229,.06);--pri-dark:#4338ca;\n --pri-grad:linear-gradient(135deg,#4f46e5,#4338ca);\n --accent:#dc2626;--accent-glow:rgba(220,38,38,.06);\n --green:#059669;--green-bg:rgba(5,150,105,.06);\n --amber:#d97706;--amber-bg:rgba(217,119,6,.06);\n --violet:#4f46e5;--rose:#dc2626;--rose-bg:rgba(220,38,38,.06);\n --shadow-sm:0 1px 2px rgba(0,0,0,.04);--shadow:0 4px 12px rgba(0,0,0,.06);\n --shadow-lg:0 20px 40px rgba(0,0,0,.1);\n}\n[data-theme=\"light\"] .auth-screen{background:linear-gradient(135deg,#f0f4ff 0%,#f8f9fb 50%,#eef2ff 100%)}\n[data-theme=\"light\"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.08)}\n[data-theme=\"light\"] .topbar{background:rgba(255,255,255,.92);border-bottom-color:var(--border);backdrop-filter:blur(8px)}\n[data-theme=\"light\"] .session-item .count,[data-theme=\"light\"] .kind-tag,[data-theme=\"light\"] .session-tag{background:rgba(0,0,0,.05)}\n[data-theme=\"light\"] .card-content pre{background:#f3f4f6;border-color:var(--border)}\n[data-theme=\"light\"] .vscore-badge{background:rgba(79,70,229,.06);color:#4f46e5}\n[data-theme=\"light\"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15)}\n[data-theme=\"light\"] .analytics-card{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.06);border:1px solid var(--border)}\n[data-theme=\"light\"] .analytics-card::before{background:none}\n[data-theme=\"light\"] .analytics-card::after{display:none}\n[data-theme=\"light\"] .analytics-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.08);transform:translateY(-2px)}\n[data-theme=\"light\"] .analytics-card.green{background:#fff;border-color:var(--border)}\n[data-theme=\"light\"] .analytics-card.green::before{background:none}\n[data-theme=\"light\"] .analytics-card.amber{background:#fff;border-color:var(--border)}\n[data-theme=\"light\"] .analytics-card.amber::before{background:none}\n[data-theme=\"light\"] .analytics-card .ac-value{-webkit-text-fill-color:unset;background:none;color:#111827}\n[data-theme=\"light\"] .analytics-card.green .ac-value{color:#059669}\n[data-theme=\"light\"] .analytics-card.amber .ac-value{color:#d97706}\n[data-theme=\"light\"] .analytics-section{background:#fff;border-color:var(--border);box-shadow:0 1px 3px rgba(0,0,0,.04)}\n[data-theme=\"light\"] .analytics-section::before{background:none}\n[data-theme=\"light\"] .chart-bar{box-shadow:none}\n[data-theme=\"light\"] .chart-bar:hover{box-shadow:0 2px 8px rgba(79,70,229,.15)}\n[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)}\n[data-theme=\"light\"] .tool-chart-tooltip .tt-time{color:#a5b4fc}\n[data-theme=\"light\"] .tool-chart-tooltip .tt-val{color:#e8eaed}\n[data-theme=\"light\"] .tool-agg-table td{background:transparent}\n[data-theme=\"light\"] .tool-agg-table tr:hover td{background:rgba(79,70,229,.03)}\n[data-theme=\"light\"] .tool-agg-table th{color:#9ca3af}\n[data-theme=\"light\"] .breakdown-item{background:#f9fafb;border-color:var(--border)}\n[data-theme=\"light\"] .breakdown-item:hover{background:#f3f4f6;border-color:#cbd5e1}\n[data-theme=\"light\"] .breakdown-bar-wrap{background:#e5e7eb}\n[data-theme=\"light\"] .breakdown-bar{background:linear-gradient(90deg,#4f46e5,#6366f1);box-shadow:none}\n[data-theme=\"light\"] .range-btn{background:transparent;border-color:var(--border);color:var(--text-sec)}\n[data-theme=\"light\"] .range-btn.active{background:rgba(79,70,229,.06);color:#4f46e5;border-color:rgba(79,70,229,.2)}\n[data-theme=\"light\"] .range-btn:hover{border-color:#4f46e5;color:#4f46e5}\nbody{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .2s,color .2s}\nbutton{cursor:pointer;font-family:inherit;font-size:inherit}\ninput,textarea,select{font-family:inherit;font-size:inherit}\n\n/* \u2500\u2500\u2500 Auth (Linkify \u914D\u8272: globals.css .dark + \u84DD\u7D2B\u6E10\u53D8) \u2500\u2500\u2500 */\n.auth-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;background:linear-gradient(135deg,rgb(36,0,255) 0%,rgb(0,135,255) 35%,rgb(108,39,157) 70%,rgb(105,30,255) 100%);position:relative;overflow:hidden}\n.auth-card{background:hsl(0 0% 100%);border:none;border-radius:8px;padding:48px 40px;width:100%;max-width:420px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);text-align:center;position:relative;z-index:1}\n.auth-card .logo{margin:0 auto 20px;text-align:center;line-height:0;background:none;border-radius:0}\n.auth-card .logo svg{filter:drop-shadow(0 0 16px rgba(255,77,77,.35))}\n.auth-card h1{font-size:22px;font-weight:700;margin-bottom:4px;color:hsl(0 0% 3.9%);letter-spacing:-.02em}\n.auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}\n.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%)}\n.auth-card input::placeholder{color:hsl(0 0% 45.1%)}\n.auth-card input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.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}\n.auth-card .btn-auth:hover{background:rgba(99,102,241,.12);border-color:var(--pri-dark)}\n.auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}\n.auth-card .btn-text{color:hsl(0 0% 45.1%)}\n.auth-card .btn-text:hover{color:var(--pri)}\n\n.reset-guide{text-align:left;margin-bottom:20px}\n.reset-step{display:flex;gap:14px;margin-bottom:16px}\n.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}\n.step-body{flex:1;min-width:0}\n.step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}\n.step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}\n.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%)}\n.cmd-box:hover{border-color:hsl(0 0% 70%);background:hsl(0 0% 96.1%)}\n.cmd-box code{flex:1}\n.copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}\n.cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}\n\n/* \u2500\u2500\u2500 App Layout (dark dashboard, same as www) \u2500\u2500\u2500 */\n.app{display:none;flex-direction:column;min-height:100vh}\n.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)}\n.topbar .brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}\n.topbar .brand .icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:22px;background:none;border-radius:0}\n.topbar .brand .sub{font-weight:400;color:var(--text-muted);font-size:11px}\n.topbar-center{flex:1;display:flex;justify-content:center}\n.topbar .actions{display:flex;align-items:center;gap:6px;flex-shrink:0}\n\n.main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}\n\n/* \u2500\u2500\u2500 Sidebar \u2500\u2500\u2500 */\n.sidebar{width:260px;flex-shrink:0}\n.sidebar .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px}\n.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;transition:all .2s}\n.stat-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.stat-card .stat-value{font-size:22px;font-weight:700;color:var(--text);letter-spacing:-.02em}\n.stat-card .stat-label{font-size:12px;color:var(--text-sec);margin-top:4px;font-weight:500}\n.stat-card.pri .stat-value{color:var(--pri)}\n.stat-card.green .stat-value{color:var(--green)}\n.stat-card.amber .stat-value{color:var(--amber)}\n.stat-card.rose .stat-value{color:var(--rose)}\n\n.sidebar .section-title{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin:24px 0 12px;padding:0 2px}\n.sidebar .session-list{display:flex;flex-direction:column;gap:6px;max-height:280px;overflow-y:auto}\n.session-item{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;font-size:13px;color:var(--text)}\n.session-item:hover{border-color:var(--pri);background:var(--pri-glow)}\n.session-item.active{border-color:var(--pri);background:var(--pri-glow);font-weight:600;color:var(--pri)}\n.session-item .count{color:var(--text-sec);font-size:11px;font-weight:600;background:rgba(0,0,0,.2);padding:3px 8px;border-radius:8px}\n\n.provider-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--green-bg);color:var(--green);border-radius:999px;font-size:11px;font-weight:600;margin-top:10px}\n.provider-badge.offline{background:var(--amber-bg);color:var(--amber)}\n\n/* \u2500\u2500\u2500 Feed \u2500\u2500\u2500 */\n.feed{flex:1;min-width:0}\n.search-bar{display:flex;gap:10px;margin-bottom:16px;position:relative;align-items:center}\n.search-bar input{flex:1;padding:10px 16px 10px 40px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;background:var(--bg-card);color:var(--text);transition:all .2s}\n.search-bar input::placeholder{color:var(--text-muted)}\n.search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.search-bar .search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:14px;pointer-events:none}\n.search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}\n\n.filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}\n.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}\n.filter-chip:hover{border-color:var(--pri);color:var(--pri)}\n.filter-chip.active{background:rgba(99,102,241,.08);color:var(--pri);border-color:rgba(99,102,241,.25)}\n\n.memory-list{display:flex;flex-direction:column;gap:16px}\n.memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}\n.memory-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}\n.memory-card .meta{display:flex;align-items:center;gap:8px}\n.role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}\n.role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(99,102,241,.12)}\n.role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}\n.role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}\n.kind-tag{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-sec);background:rgba(0,0,0,.2);font-weight:500}\n.card-time{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:8px}\n.session-tag{font-size:11px;font-family:ui-monospace,monospace;color:var(--text-muted);background:rgba(0,0,0,.2);padding:3px 8px;border-radius:6px;cursor:default}\n.card-summary{font-size:15px;font-weight:600;color:var(--text);margin-bottom:10px;line-height:1.5;letter-spacing:-.01em}\n.card-content{font-size:13px;color:var(--text-sec);line-height:1.65;max-height:0;overflow:hidden;transition:max-height .3s ease}\n.card-content.show{max-height:600px;overflow-y:auto}\n.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)}\n.card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}\n.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}\n.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}\n.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}\n.merge-history-item{padding:6px 0;border-bottom:1px dashed rgba(255,255,255,.06)}\n.merge-history-item:last-child{border-bottom:none}\n.merge-action{font-weight:600;font-size:11px;padding:2px 6px;border-radius:4px}\n.merge-action.UPDATE{background:rgba(59,130,246,.15);color:#60a5fa}\n.merge-action.DUPLICATE{background:rgba(245,158,11,.15);color:#f59e0b}\n.card-updated{font-size:11px;color:var(--text-muted);margin-left:6px}\n.dedup-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}\n.dedup-badge.duplicate{background:rgba(245,158,11,.12);color:#f59e0b}\n.dedup-badge.merged{background:rgba(59,130,246,.12);color:#60a5fa}\n.import-badge{display:inline-flex;align-items:center;gap:4px;background:rgba(236,72,153,.1);color:#ec4899;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}\n[data-theme=\"light\"] .import-badge{background:rgba(219,39,119,.08);color:#db2777}\n.memory-card.dedup-inactive{opacity:.55;border-style:dashed}\n.memory-card.dedup-inactive:hover{opacity:.85}\n.dedup-target-link{font-size:11px;color:var(--pri);cursor:pointer;text-decoration:underline;margin-left:4px}\n.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)}\n.memory-modal-overlay.show{display:flex}\n.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}\n@keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}\n.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}\n.memory-modal-body{padding:20px;overflow-y:auto;flex:1}\n.modal-memory-card{display:flex;flex-direction:column;gap:14px}\n.modal-header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.modal-field{display:flex;flex-direction:column;gap:4px}\n.modal-field-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec)}\n.modal-field-val{font-size:13px;color:var(--text);line-height:1.5}\n.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}\n[data-theme=\"light\"] .modal-field-content{background:rgba(0,0,0,.04)}\n.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)}\n[data-theme=\"light\"] .merge-history{background:rgba(0,0,0,.04)}\n[data-theme=\"light\"] .merge-history-item{border-bottom-color:rgba(0,0,0,.06)}\n\n/* \u2500\u2500\u2500 Buttons \u2500\u2500\u2500 */\n.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}\n.btn:hover{border-color:var(--pri);color:var(--pri)}\n.btn-primary{background:rgba(255,255,255,.08);color:var(--text);border:1px solid var(--border);font-weight:600}\n.btn-primary:hover{background:rgba(255,255,255,.14);transform:translateY(-1px);border-color:var(--pri);color:var(--pri)}\n.btn-ghost{border-color:transparent;background:transparent;color:var(--text-sec)}\n.btn-ghost:hover{background:rgba(255,255,255,.06);color:var(--text)}\n.btn-danger{color:var(--accent);border-color:rgba(230,57,70,.25)}\n.btn-danger:hover{background:rgba(230,57,70,.1);color:var(--accent)}\n.btn-sm{padding:5px 12px;font-size:12px}\n.btn-icon{padding:5px 7px;font-size:15px;border-radius:8px}\n.btn-text{border:none;background:none;color:var(--text-muted);font-size:12px;padding:4px 8px}\n.btn-text:hover{color:var(--pri)}\n[data-theme=\"light\"] .btn-primary{background:rgba(0,0,0,.05);color:var(--text);border-color:rgba(0,0,0,.12)}\n[data-theme=\"light\"] .btn-primary:hover{background:rgba(0,0,0,.08);border-color:var(--pri);color:var(--pri)}\n[data-theme=\"light\"] .btn-ghost{color:var(--text-sec)}\n[data-theme=\"light\"] .btn-ghost:hover{background:rgba(0,0,0,.04);color:var(--text)}\n\n/* \u2500\u2500\u2500 Modal \u2500\u2500\u2500 */\n.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:500;align-items:center;justify-content:center;backdrop-filter:blur(8px)}\n.modal-overlay.show{display:flex}\n.modal{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);padding:32px;width:100%;max-width:520px;box-shadow:var(--shadow-lg);max-height:85vh;overflow-y:auto}\n.modal h2{font-size:20px;font-weight:700;margin-bottom:24px;color:var(--text);letter-spacing:-.02em}\n.form-group{margin-bottom:18px}\n.form-group label{display:block;font-size:13px;font-weight:600;color:var(--text-sec);margin-bottom:6px}\n.form-group input,.form-group textarea,.form-group select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;transition:all .2s;background:var(--bg-card);color:var(--text)}\n.form-group input::placeholder,.form-group textarea::placeholder{color:var(--text-muted)}\n.form-group input:focus,.form-group textarea:focus,.form-group select:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.form-group textarea{min-height:100px;resize:vertical}\n.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:28px}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\n.toast-container{position:fixed;top:80px;right:24px;z-index:1000;display:flex;flex-direction:column;gap:8px}\n.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}\n.toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}\n.toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}\n.toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(99,102,241,.15)}\n@keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}\n\n.empty{text-align:center;padding:64px 20px;color:var(--text-sec)}\n.empty .icon{font-size:52px;margin-bottom:16px;opacity:.5}\n.empty p{font-size:15px;font-weight:500}\n\n.spinner{width:40px;height:40px;border:3px solid var(--border);border-top-color:var(--pri);border-radius:50%;animation:spin .8s linear infinite;margin:48px auto}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n::-webkit-scrollbar{width:6px;height:6px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.25)}\n\n.filter-sep{width:1px;height:20px;background:var(--border);margin:0 4px}\n.filter-select{padding:6px 12px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;outline:none;cursor:pointer}\n.filter-select:focus{border-color:var(--pri)}\n.date-filter{display:flex;align-items:center;gap:10px;margin-bottom:18px;font-size:13px;color:var(--text-sec)}\n.date-filter input[type=\"datetime-local\"]{padding:6px 10px;border:1px solid var(--border);border-radius:8px;font-size:12px;outline:none;background:var(--bg-card);color:var(--text)}\n.date-filter input[type=\"datetime-local\"]:focus{border-color:var(--pri)}\n.date-filter label{font-weight:500}\n\n.pagination{display:flex;align-items:center;justify-content:center;gap:6px;padding:28px 0;flex-wrap:wrap}\n.pagination .pg-btn{min-width:38px;height:38px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s}\n.pagination .pg-btn:hover{border-color:var(--pri);color:var(--pri)}\n.pagination .pg-btn.active{background:var(--pri);color:#000;border-color:var(--pri)}\n.pagination .pg-btn.disabled{opacity:.4;pointer-events:none}\n.pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}\n\n/* \u2500\u2500\u2500 Tasks \u89C6\u56FE \u2500\u2500\u2500 */\n.tasks-view{display:none;flex:1;min-width:0;flex-direction:column;gap:16px}\n.tasks-view.show{display:flex}\n.tasks-header{display:flex;flex-direction:column;gap:14px}\n.tasks-stats{display:flex;gap:16px}\n.tasks-stat{display:flex;align-items:center;gap:8px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 18px;flex:1;transition:all .2s}\n.tasks-stat:hover{border-color:var(--border-glow)}\n.tasks-stat-value{font-size:22px;font-weight:700;color:var(--text)}\n.tasks-stat-label{font-size:12px;color:var(--text-sec);font-weight:500}\n.tasks-filters{display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n.tasks-list{display:flex;flex-direction:column;gap:10px}\n.task-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:18px 20px;cursor:pointer;transition:all .25s;position:relative;overflow:hidden}\n.task-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover);transform:translateY(-1px);box-shadow:var(--shadow)}\n.task-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px}\n.task-card.status-active::before{background:var(--green)}\n.task-card.status-completed::before{background:var(--pri)}\n.task-card.status-skipped::before{background:var(--text-muted)}\n.task-card.status-skipped{opacity:.6}\n.task-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:8px}\n.task-card-title{font-size:14px;font-weight:600;color:var(--text);line-height:1.4;flex:1;word-break:break-word}\n.task-card-title:empty::after{content:'Untitled Task';color:var(--text-muted);font-style:italic}\n.task-status-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:3px 10px;border-radius:20px;flex-shrink:0}\n.task-status-badge.active{color:var(--green);background:var(--green-bg)}\n.task-status-badge.completed{color:var(--pri);background:var(--pri-glow)}\n.task-status-badge.skipped{color:var(--text-muted);background:rgba(128,128,128,.15)}\n.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}\n.task-card-summary:empty{display:none}\n.task-card-summary.skipped-reason{background:rgba(128,128,128,.08);border-radius:6px;padding:6px 10px;border-left:3px solid var(--text-muted)}\n.task-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted)}\n.task-card-bottom .tag{display:flex;align-items:center;gap:4px}\n.task-card-bottom .tag .icon{font-size:12px}\n\n/* \u2500\u2500\u2500 Task Detail Overlay \u2500\u2500\u2500 */\n.task-detail-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:200;align-items:center;justify-content:center;padding:24px;backdrop-filter:blur(4px)}\n.task-detail-overlay.show{display:flex}\n.task-detail-panel{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);width:100%;max-width:780px;max-height:85vh;overflow-y:auto;box-shadow:var(--shadow-lg);padding:28px 32px}\n.task-detail-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}\n.task-detail-header h2{font-size:18px;font-weight:700;color:var(--text);line-height:1.4;flex:1}\n.task-detail-meta{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;font-size:12px;color:var(--text-sec)}\n.task-detail-meta .meta-item{display:flex;align-items:center;gap:5px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:5px 12px}\n.task-detail-summary{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:20px;font-size:13px;line-height:1.7;color:var(--text);word-break:break-word}\n.task-detail-summary:empty::after{content:'Summary not yet generated (task still active)';color:var(--text-muted);font-style:italic}\n.task-detail-summary .summary-section-title{font-size:14px;font-weight:700;color:var(--text);margin:14px 0 6px 0;padding-bottom:4px;border-bottom:1px solid var(--border)}\n.task-detail-summary .summary-section-title:first-child{margin-top:0}\n.task-detail-summary ul{margin:4px 0 8px 0;padding-left:20px}\n.task-detail-summary li{margin:3px 0;color:var(--text-sec);line-height:1.6}\n.task-detail-chunks-title{font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px}\n.task-detail-chunks{display:flex;flex-direction:column;gap:14px;padding:8px 0}\n.task-chunk-item{display:flex;flex-direction:column;max-width:82%;font-size:13px;line-height:1.6}\n.task-chunk-item.role-user{align-self:flex-end;align-items:flex-end}\n.task-chunk-item.role-assistant,.task-chunk-item.role-tool{align-self:flex-start;align-items:flex-start}\n.task-chunk-role{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px;padding:0 4px}\n.task-chunk-role.user{color:var(--pri)}\n.task-chunk-role.assistant{color:var(--green)}\n.task-chunk-role.tool{color:var(--amber)}\n.task-chunk-bubble{padding:12px 16px;border-radius:16px;white-space:pre-wrap;word-break:break-word;max-height:200px;overflow:hidden;position:relative;transition:all .2s}\n.task-chunk-bubble.expanded{max-height:none}\n.role-user .task-chunk-bubble{background:var(--pri);color:#000;border-bottom-right-radius:4px}\n.role-assistant .task-chunk-bubble{background:var(--bg-card);border:1px solid var(--border);color:var(--text-sec);border-bottom-left-radius:4px}\n.role-tool .task-chunk-bubble{background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);color:var(--text-sec);border-bottom-left-radius:4px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}\n.task-chunk-bubble:hover{filter:brightness(1.05)}\n.task-chunk-time{font-size:10px;color:var(--text-muted);margin-top:3px;padding:0 4px}\n[data-theme=\"light\"] .role-user .task-chunk-bubble{background:var(--pri);color:#fff}\n[data-theme=\"light\"] .role-assistant .task-chunk-bubble{background:#f0f0f0;border:none;color:#333}\n[data-theme=\"light\"] .task-detail-panel{background:#fff}\n[data-theme=\"light\"] .task-card{background:#fff}\n[data-theme=\"light\"] .tasks-stat{background:#fff}\n\n/* \u2500\u2500\u2500 Skills \u2500\u2500\u2500 */\n.skills-view{display:none;flex:1;min-width:0;flex-direction:column;gap:16px}\n.skills-view.show{display:flex}\n.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}\n.skill-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover);transform:translateY(-1px);box-shadow:var(--shadow)}\n.skill-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px;background:var(--violet)}\n.skill-card.installed::before{background:var(--green)}\n.skill-card.archived{opacity:.5}\n.skill-card.archived::before{background:var(--text-muted)}\n.skill-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:6px}\n.skill-card-name{font-size:15px;font-weight:700;color:var(--text);flex:1}\n.skill-card-badges{display:flex;gap:6px;align-items:center}\n.skill-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:3px 10px;border-radius:20px}\n.skill-badge.version{color:var(--violet);background:rgba(139,92,246,.15)}\n.skill-badge.installed{color:var(--green);background:var(--green-bg)}\n.skill-badge.status-active{color:var(--pri);background:var(--pri-glow)}\n.skill-badge.status-archived{color:var(--text-muted);background:rgba(128,128,128,.15)}\n.skill-badge.status-draft{color:var(--amber);background:var(--amber-bg)}\n.skill-badge.quality{font-size:10px;font-weight:700;padding:3px 10px;border-radius:20px}\n.skill-badge.quality.high{color:var(--green);background:var(--green-bg)}\n.skill-badge.quality.mid{color:var(--amber);background:var(--amber-bg)}\n.skill-badge.quality.low{color:var(--rose);background:var(--rose-bg)}\n.skill-card.draft{opacity:.75}\n.skill-card.draft::before{background:var(--amber)}\n.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}\n.skill-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted);flex-wrap:wrap}\n.skill-card-bottom .tag{display:flex;align-items:center;gap:4px}\n.skill-card-tags{display:flex;gap:4px;flex-wrap:wrap}\n.skill-tag{font-size:10px;padding:2px 8px;border-radius:10px;background:rgba(139,92,246,.1);color:var(--violet);font-weight:500}\n.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)}\n.skill-version-item{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px}\n.skill-version-header{display:flex;align-items:center;gap:10px;margin-bottom:6px}\n.skill-version-badge{font-size:11px;font-weight:700;color:var(--violet);background:rgba(139,92,246,.12);padding:2px 8px;border-radius:8px}\n.skill-version-type{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em}\n.skill-version-changelog{font-size:12px;color:var(--text);line-height:1.5;font-weight:600}\n.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}\n.skill-version-time{font-size:10px;color:var(--text-muted);margin-top:4px}\n.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}\n.skill-related-task:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.skill-related-task .relation{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em;min-width:80px}\n.skill-related-task .task-title{font-size:13px;color:var(--text);flex:1}\n.skill-files-list{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}\n.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}\n.skill-file-icon{font-size:14px;width:20px;text-align:center}\n.skill-file-name{flex:1;color:var(--text);font-family:SF Mono,Monaco,Consolas,monospace}\n.skill-file-type{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em}\n.skill-file-size{font-size:10px;color:var(--text-muted)}\n.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}\n.skill-download-btn:hover{opacity:.85;transform:translateY(-1px)}\n.task-skill-section{margin-bottom:16px;padding:14px 16px;border-radius:var(--radius);border:1px solid var(--border)}\n.task-skill-section.status-generated{border-color:var(--green);background:var(--green-bg)}\n.task-skill-section.status-generating{border-color:var(--amber);background:var(--amber-bg)}\n.task-skill-section.status-not_generated,.task-skill-section.status-skipped{border-color:var(--border);background:var(--bg-card)}\n.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)}\n.task-skill-section .skill-status-reason{font-size:12px;color:var(--text-sec);line-height:1.5}\n.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}\n.task-skill-section .skill-link-card:hover{border-color:var(--pri);background:var(--bg-card-hover)}\n.task-skill-section .skill-link-name{font-size:13px;font-weight:600;color:var(--pri)}\n.task-skill-section .skill-link-meta{font-size:11px;color:var(--text-sec);margin-top:4px}\n.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)}\n[data-theme=\"light\"] .skill-card{background:#fff}\n[data-theme=\"light\"] .skill-detail-desc{background:#f8fafc}\n[data-theme=\"light\"] .skill-version-item{background:#f8fafc}\n\n/* \u2500\u2500\u2500 Analytics / \u7EDF\u8BA1 \u2500\u2500\u2500 */\n.nav-tabs{display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.06);border-radius:10px;padding:3px}\n.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}\n.nav-tabs .tab:hover{color:var(--text)}\n.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)}\n[data-theme=\"light\"] .nav-tabs{background:rgba(0,0,0,.05)}\n[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)}\n.analytics-view,.settings-view,.logs-view,.migrate-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}\n.analytics-view.show,.settings-view.show,.logs-view.show,.migrate-view.show{display:flex}\n\n/* \u2500\u2500\u2500 Logs \u2500\u2500\u2500 */\n.logs-toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 0}\n.logs-toolbar-left{display:flex;align-items:center;gap:8px}\n.logs-toolbar-right{display:flex;align-items:center;gap:8px}\n.logs-list{display:flex;flex-direction:column;gap:8px;overflow-y:auto;flex:1;min-height:0}\n.log-entry{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;transition:border-color .2s}\n.log-entry:hover{border-color:var(--border-glow)}\n.log-header{display:flex;align-items:center;gap:10px;padding:12px 16px;cursor:pointer;user-select:none;transition:background .15s}\n.log-header:hover{background:rgba(255,255,255,.03)}\n[data-theme=\"light\"] .log-header:hover{background:rgba(0,0,0,.02)}\n.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}\n.log-tool-badge.memory_search{background:rgba(59,130,246,.15);color:#60a5fa}\n.log-tool-badge.memory_add{background:rgba(168,85,247,.15);color:#c084fc}\n.log-tool-badge.auto_recall{background:rgba(168,85,247,.15);color:#c084fc}\n.log-tool-badge.memory_timeline{background:rgba(34,197,94,.15);color:#4ade80}\n.log-tool-badge.memory_get{background:rgba(251,146,60,.15);color:#fb923c}\n.log-tool-badge.task_summary{background:rgba(245,158,11,.15);color:#fbbf24}\n.log-tool-badge.skill_get{background:rgba(236,72,153,.15);color:#f472b6}\n.log-tool-badge.skill_install{background:rgba(14,165,233,.15);color:#38bdf8}\n.log-tool-badge.memory_viewer{background:rgba(100,116,139,.15);color:#94a3b8}\n.log-dur{font-family:'SF Mono',Consolas,monospace;font-size:10px;color:var(--text-sec);opacity:.7}\n.log-time{margin-left:auto;font-size:11px;color:var(--text-sec);font-family:'SF Mono',Consolas,monospace;white-space:nowrap}\n.log-status{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n.log-status.ok{background:#4ade80;box-shadow:0 0 4px rgba(74,222,128,.5)}\n.log-status.fail{background:#f87171;box-shadow:0 0 4px rgba(248,113,113,.5)}\n.log-summary{padding:8px 16px 10px;font-size:12px;color:var(--text-sec);line-height:1.5}\n.log-summary-kv{display:inline-flex;align-items:center;gap:4px;margin-right:12px;font-size:11px}\n.log-summary-kv .kv-label{color:var(--text-sec);opacity:.7}\n.log-summary-kv .kv-val{color:var(--text);font-family:'SF Mono',Consolas,monospace;font-size:11px}\n.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}\n.log-summary-stats{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}\n.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}\n.log-stat-chip.stored{background:rgba(74,222,128,.12);color:#4ade80}\n.log-stat-chip.skipped{background:rgba(100,116,139,.12);color:#94a3b8}\n.log-stat-chip.dedup{background:rgba(251,146,60,.12);color:#fb923c}\n.log-stat-chip.merged{background:rgba(168,85,247,.12);color:#c084fc}\n.log-stat-chip.errors{background:rgba(248,113,113,.12);color:#f87171}\n.log-msg-list{margin-top:8px;display:flex;flex-direction:column;gap:4px}\n.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)}\n[data-theme=\"light\"] .log-msg-item{background:rgba(0,0,0,.02)}\n.log-msg-role{flex-shrink:0;font-size:10px;font-weight:600;padding:1px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.3px}\n.log-msg-role.user{background:rgba(59,130,246,.12);color:#60a5fa}\n.log-msg-role.assistant{background:rgba(168,85,247,.12);color:#c084fc}\n.log-msg-role.system{background:rgba(100,116,139,.12);color:#94a3b8}\n.log-msg-action{flex-shrink:0;font-size:10px;font-weight:600;padding:1px 6px;border-radius:4px}\n.log-msg-action.stored{color:#4ade80}\n.log-msg-action.exact-dup{color:#94a3b8}\n.log-msg-action.dedup{color:#fb923c}\n.log-msg-action.merged{color:#c084fc}\n.log-msg-action.error{color:#f87171}\n.log-msg-text{color:var(--text);opacity:.85;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}\n.log-detail{display:none;border-top:1px solid var(--border);padding:0}\n.log-detail.open{display:block}\n.log-expand-btn{font-size:10px;color:var(--text-sec);opacity:.5;margin-left:auto;transition:transform .2s,opacity .15s;display:inline-block}\n.log-entry.expanded .log-expand-btn{transform:rotate(180deg);opacity:.8}\n.logs-pagination{display:flex;align-items:center;justify-content:center;gap:4px;padding:12px 0;flex-wrap:wrap}\n.logs-pagination .btn{min-width:32px;padding:4px 8px;font-size:12px}\n.logs-pagination .btn-primary{background:var(--primary);color:#fff;border-color:var(--primary)}\n.logs-pagination .page-ellipsis{color:var(--text-sec);font-size:12px;padding:0 4px}\n.logs-pagination .page-total{font-size:11px;color:var(--text-sec);margin-left:8px}\n.log-io-section{padding:10px 14px}\n.log-io-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec);margin-bottom:6px}\n.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}\n.log-io-section+.log-io-section{border-top:1px dashed var(--border)}\n[data-theme=\"light\"] .log-io-content{background:rgba(0,0,0,.04)}\n[data-theme=\"light\"] .log-summary-query{background:rgba(59,130,246,.06)}\n.settings-group{margin-bottom:8px}\n.settings-group-title{font-size:15px;font-weight:700;color:var(--text);margin:0 0 12px 0;padding:0;letter-spacing:.02em}\n.settings-group .settings-section{margin-bottom:16px}\n.settings-group .settings-section:last-child{margin-bottom:0}\n.settings-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px 28px}\n.settings-section h3{font-size:13px;font-weight:700;color:var(--text);margin-bottom:16px;display:flex;align-items:center;gap:8px}\n.settings-section h3 .icon{font-size:16px;opacity:.8}\n.settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}\n@media(max-width:800px){.settings-grid{grid-template-columns:1fr}}\n.settings-field{display:flex;flex-direction:column;gap:4px}\n.settings-field label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}\n.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}\n.settings-field input:focus,.settings-field select:focus{outline:none;border-color:var(--pri)}\n.settings-field input[type=\"password\"]{font-family:'Courier New',monospace;letter-spacing:.05em}\n.settings-field .field-hint{font-size:10px;color:var(--text-muted);margin-top:2px}\n.settings-field.full-width{grid-column:1/-1}\n.settings-toggle{display:flex;align-items:center;gap:10px;padding:4px 0}\n.settings-toggle label{font-size:12px;font-weight:500;color:var(--text-sec);text-transform:none;letter-spacing:0}\n.toggle-switch{position:relative;width:36px;height:20px;cursor:pointer}\n.toggle-switch input{opacity:0;width:0;height:0}\n.toggle-slider{position:absolute;inset:0;background:var(--border);border-radius:20px;transition:.2s}\n.toggle-slider::before{content:'';position:absolute;height:14px;width:14px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s}\n.toggle-switch input:checked+.toggle-slider{background:var(--pri)}\n.toggle-switch input:checked+.toggle-slider::before{transform:translateX(16px)}\n.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)}\n.settings-actions .btn{min-width:110px;padding:10px 20px;font-size:13px}\n.settings-actions .btn-primary{background:rgba(99,102,241,.08);color:var(--pri);border:1px solid rgba(99,102,241,.25);font-weight:600}\n.settings-actions .btn-primary:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}\n[data-theme=\"light\"] .settings-actions .btn-primary{background:rgba(79,70,229,.06);color:#4f46e5;border:1px solid rgba(79,70,229,.2)}\n[data-theme=\"light\"] .settings-actions .btn-primary:hover{background:rgba(79,70,229,.1);border-color:#4f46e5}\n.settings-saved{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px;font-weight:600;opacity:0;transition:opacity .3s}\n.settings-saved.show{opacity:1}\n.migrate-log-item{display:flex;align-items:flex-start;gap:10px;padding:8px 14px;border-bottom:1px solid var(--border);animation:migrateFadeIn .3s ease}\n.migrate-log-item:last-child{border-bottom:none}\n.migrate-log-item .log-icon{flex-shrink:0;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;margin-top:2px}\n.migrate-log-item .log-icon.stored{background:rgba(34,197,94,.12);color:#22c55e}\n.migrate-log-item .log-icon.skipped{background:rgba(245,158,11,.12);color:#f59e0b}\n.migrate-log-item .log-icon.merged{background:rgba(59,130,246,.12);color:#3b82f6}\n.migrate-log-item .log-icon.error{background:rgba(239,68,68,.12);color:#ef4444}\n.migrate-log-item .log-icon.duplicate{background:rgba(245,158,11,.12);color:#f59e0b}\n.migrate-log-item .log-body{flex:1;min-width:0}\n.migrate-log-item .log-preview{color:var(--text);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}\n.migrate-log-item .log-meta{display:flex;gap:8px;font-size:9px;color:var(--text-muted);margin-top:2px}\n.migrate-log-item .log-meta .tag{padding:1px 6px;border-radius:4px;font-weight:600;letter-spacing:.02em}\n.migrate-log-item .log-meta .tag.stored{background:rgba(34,197,94,.1);color:#22c55e}\n.migrate-log-item .log-meta .tag.skipped{background:rgba(245,158,11,.1);color:#f59e0b}\n.migrate-log-item .log-meta .tag.merged{background:rgba(59,130,246,.1);color:#3b82f6}\n.migrate-log-item .log-meta .tag.error{background:rgba(239,68,68,.1);color:#ef4444}\n.migrate-log-item .log-meta .tag.duplicate{background:rgba(245,158,11,.1);color:#f59e0b}\n@keyframes migrateFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}\n.feed-wrap{flex:1;min-width:0;display:flex;flex-direction:column}\n.feed-wrap.hide{display:none}\n.analytics-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}\n.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)}\n.analytics-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--pri);opacity:.5}\n.analytics-card::after{display:none}\n.analytics-card:hover{transform:translateY(-2px);box-shadow:var(--shadow);border-color:var(--border-glow)}\n.analytics-card.green::before{background:var(--green)}\n.analytics-card.amber::before{background:var(--amber)}\n.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}\n.analytics-card.green .ac-value{color:var(--green);background:none}\n.analytics-card.amber .ac-value{color:var(--amber);background:none}\n.analytics-card .ac-label{font-size:11px;color:var(--text-muted);margin-top:6px;font-weight:500;text-transform:uppercase;letter-spacing:.06em}\n.analytics-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:22px 24px;position:relative;overflow:hidden}\n.analytics-section::before{display:none}\n.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}\n.analytics-section h3 .icon{font-size:14px;opacity:.6}\n.chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto;justify-content:center}\n.chart-bar-wrap{flex:1;min-width:28px;max-width:80px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}\n.chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}\n.chart-bar-wrap:hover .chart-bar{opacity:1}\n.chart-bar-wrap:hover .chart-bar-label{color:var(--text)}\n.chart-bar-wrap:hover .chart-tip{opacity:1;transform:translateX(-50%) translateY(0)}\n.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}\n.chart-bar{width:100%;border-radius:3px 3px 1px 1px;background:#818cf8;opacity:.75;transition:all .2s ease}\n.chart-bar.violet{background:#6366f1}\n.chart-bar.green{background:var(--green)}\n.chart-bar.zero{background:var(--border);opacity:.3;border-radius:2px}\n.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}\n.chart-legend{display:flex;gap:14px;margin-top:12px;flex-wrap:wrap;font-size:11px;color:var(--text-sec);font-weight:500}\n.chart-legend span{display:inline-flex;align-items:center;gap:5px}\n.chart-legend .dot{width:8px;height:8px;border-radius:2px}\n.chart-legend .dot.pri{background:var(--pri)}\n.tool-chart-svg{width:100%;height:100%;display:block}\n.tool-chart-svg .grid-line{stroke:var(--border);stroke-dasharray:3 3;stroke-width:0.5}\n.tool-chart-svg .axis-label{fill:var(--text-muted);font-size:10px;font-family:var(--mono)}\n.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}\n@keyframes lineIn{to{stroke-dashoffset:0}}\n.tool-chart-svg .data-area{opacity:1}\n.tool-chart-svg .hover-dot{r:3.5;stroke-width:2;stroke:var(--bg);opacity:0;transition:opacity .1s}\n.tool-chart-svg .hover-dot.show{opacity:1}\n.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}\n.tool-chart-tooltip.show{opacity:1}\n.tool-chart-tooltip .tt-time{color:var(--text-muted);font-size:10px;margin-bottom:4px;font-weight:500}\n.tool-chart-tooltip .tt-row{display:flex;align-items:center;gap:6px;margin:2px 0}\n.tool-chart-tooltip .tt-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}\n.tool-chart-tooltip .tt-val{font-weight:600;margin-left:auto;padding-left:12px}\n.tool-agg-table{width:100%;border-collapse:collapse;font-size:12px}\n.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)}\n.tool-agg-table td{padding:8px 12px;color:var(--text-sec);border-bottom:1px solid var(--border)}\n.tool-agg-table tr:hover td{background:rgba(99,102,241,.04);color:var(--text)}\n.tool-agg-table .tool-name{font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}\n.tool-agg-table .tool-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}\n.tool-agg-table .ms-val{font-family:var(--mono);font-weight:600}\n.tool-agg-table .ms-val.fast{color:var(--green)}\n.tool-agg-table .ms-val.medium{color:var(--amber)}\n.tool-agg-table .ms-val.slow{color:var(--accent)}\n.chart-legend .dot.violet{background:var(--violet)}\n.chart-legend .dot.green{background:var(--green)}\n.breakdown-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}\n.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}\n.breakdown-item:hover{border-color:var(--border-glow);background:rgba(255,255,255,.04)}\n.breakdown-item .bd-top{display:flex;align-items:center;justify-content:space-between}\n.breakdown-item .label{font-size:12px;color:var(--text-sec);font-weight:500;text-transform:capitalize}\n.breakdown-item .value{font-size:13px;font-weight:600;color:var(--text)}\n.breakdown-bar-wrap{height:3px;background:rgba(255,255,255,.06);border-radius:2px;overflow:hidden}\n.breakdown-bar{height:100%;border-radius:2px;background:var(--pri);transition:width .5s ease}\n.metrics-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}\n.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}\n.range-btn:hover{border-color:var(--pri);color:var(--pri)}\n.range-btn.active{background:rgba(99,102,241,.08);color:var(--pri);border-color:rgba(99,102,241,.25)}\n\n.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}\n.theme-toggle .theme-icon-light{display:none}\n.theme-toggle .theme-icon-dark{display:inline}\n[data-theme=\"light\"] .theme-toggle .theme-icon-light{display:inline}\n[data-theme=\"light\"] .theme-toggle .theme-icon-dark{display:none}\n\n.auth-top-actions{position:absolute;top:16px;right:16px;z-index:10;display:flex;align-items:center;gap:2px}\n.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}\n.auth-theme-toggle:hover{background:rgba(255,255,255,.25);color:#fff}\n.auth-theme-toggle .theme-icon-light{display:none}\n.auth-theme-toggle .theme-icon-dark{display:inline}\n[data-theme=\"light\"] .auth-theme-toggle{color:rgba(0,0,0,.4);background:rgba(0,0,0,.05)}\n[data-theme=\"light\"] .auth-theme-toggle:hover{background:rgba(0,0,0,.1);color:#0f172a}\n[data-theme=\"light\"] .auth-top-actions{background:none}\n[data-theme=\"light\"] .auth-theme-toggle .theme-icon-light{display:inline}\n[data-theme=\"light\"] .auth-theme-toggle .theme-icon-dark{display:none}\n\n@media(max-width:1100px){.analytics-cards{grid-template-columns:repeat(3,1fr)}}\n@media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}.analytics-cards{grid-template-columns:repeat(2,1fr)}.topbar{padding:0 16px;gap:8px}.topbar .brand span{display:none}.topbar-center{justify-content:flex-start}}\n</style>\n</head>\n<body>\n\n<!-- \u2500\u2500\u2500 Auth: Setup Password \u2500\u2500\u2500 -->\n<div id=\"setupScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-top-actions\">\n <button class=\"auth-theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <button class=\"auth-theme-toggle\" onclick=\"toggleLang()\" aria-label=\"Switch language\"><span data-i18n=\"lang.switch\">EN</span></button>\n </div>\n <div class=\"auth-card\">\n <div class=\"logo\"><svg width=\"60\" height=\"60\" viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"aLG\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"><stop offset=\"0%\" stop-color=\"#ff4d4d\"/><stop offset=\"100%\" stop-color=\"#991b1b\"/></linearGradient></defs><path d=\"M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z\" fill=\"url(#aLG)\"/><path d=\"M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z\" fill=\"url(#aLG)\"/><path d=\"M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z\" fill=\"url(#aLG)\"/><path d=\"M45 15Q35 5 30 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><path d=\"M75 15Q85 5 90 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><circle cx=\"45\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"75\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"46\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/><circle cx=\"76\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/></svg></div>\n <h1 data-i18n=\"title\">OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\" data-i18n=\"subtitle\">Powered by MemOS</p>\n <p data-i18n=\"setup.desc\">Set a password to protect your memories</p>\n <input type=\"password\" id=\"setupPw\" data-i18n-ph=\"setup.pw\" placeholder=\"Enter a password (4+ characters)\" autofocus>\n <input type=\"password\" id=\"setupPw2\" data-i18n-ph=\"setup.pw2\" placeholder=\"Confirm password\">\n <button class=\"btn-auth\" onclick=\"doSetup()\" data-i18n=\"setup.btn\">Set Password & Enter</button>\n <div class=\"error-msg\" id=\"setupErr\"></div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Auth: Login \u2500\u2500\u2500 -->\n<div id=\"loginScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-top-actions\">\n <button class=\"auth-theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <button class=\"auth-theme-toggle\" onclick=\"toggleLang()\" aria-label=\"Switch language\"><span data-i18n=\"lang.switch\">EN</span></button>\n </div>\n <div class=\"auth-card\">\n <div class=\"logo\"><svg width=\"60\" height=\"60\" viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"bLG\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"><stop offset=\"0%\" stop-color=\"#ff4d4d\"/><stop offset=\"100%\" stop-color=\"#991b1b\"/></linearGradient></defs><path d=\"M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z\" fill=\"url(#bLG)\"/><path d=\"M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z\" fill=\"url(#bLG)\"/><path d=\"M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z\" fill=\"url(#bLG)\"/><path d=\"M45 15Q35 5 30 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><path d=\"M75 15Q85 5 90 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><circle cx=\"45\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"75\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"46\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/><circle cx=\"76\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/></svg></div>\n <h1 data-i18n=\"title\">OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\" data-i18n=\"subtitle\">Powered by MemOS</p>\n <p data-i18n=\"login.desc\">Enter your password to access memories</p>\n <div id=\"loginForm\">\n <input type=\"password\" id=\"loginPw\" data-i18n-ph=\"login.pw\" placeholder=\"Password\" autofocus>\n <button class=\"btn-auth\" onclick=\"doLogin()\" data-i18n=\"login.btn\">Unlock</button>\n <div class=\"error-msg\" id=\"loginErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showResetForm()\" data-i18n=\"login.forgot\">Forgot password?</button>\n </div>\n <div id=\"resetForm\" style=\"display:none\">\n <div class=\"reset-guide\">\n <div class=\"reset-step\">\n <div class=\"step-num\">1</div>\n <div class=\"step-body\">\n <div class=\"step-title\" data-i18n=\"reset.step1.title\">Open Terminal</div>\n <div class=\"step-desc\" data-i18n=\"reset.step1.desc\">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>\n <div class=\"cmd-box\" onclick=\"copyCmd(this)\">\n <code>grep \"password reset token:\" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>\n <span class=\"copy-hint\" data-i18n=\"copy.hint\">Click to copy</span>\n </div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">2</div>\n <div class=\"step-body\">\n <div class=\"step-title\" data-i18n=\"reset.step2.title\">Find the token</div>\n <div class=\"step-desc\" id=\"resetStep2Desc\">In the output, find <span style=\"font-family:monospace;font-size:12px;color:var(--pri)\">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">3</div>\n <div class=\"step-body\">\n <div class=\"step-title\" data-i18n=\"reset.step3.title\">Paste & reset</div>\n <div class=\"step-desc\" data-i18n=\"reset.step3.desc\">Paste the token below and set your new password.</div>\n </div>\n </div>\n </div>\n <input type=\"text\" id=\"resetToken\" data-i18n-ph=\"reset.token\" placeholder=\"Paste reset token here\" style=\"margin-bottom:8px;font-family:monospace\">\n <input type=\"password\" id=\"resetNewPw\" data-i18n-ph=\"reset.newpw\" placeholder=\"New password (4+ characters)\">\n <input type=\"password\" id=\"resetNewPw2\" data-i18n-ph=\"reset.newpw2\" placeholder=\"Confirm new password\">\n <button class=\"btn-auth\" onclick=\"doReset()\" data-i18n=\"reset.btn\">Reset Password</button>\n <div class=\"error-msg\" id=\"resetErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showLoginForm()\" data-i18n=\"reset.back\">\u2190 Back to login</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Main App \u2500\u2500\u2500 -->\n<div class=\"app\" id=\"app\">\n <div class=\"topbar\">\n <div class=\"brand\">\n <div class=\"icon\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" style=\"filter:drop-shadow(0 0 8px rgba(255,77,77,.3))\"><defs><linearGradient id=\"tLG\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"><stop offset=\"0%\" stop-color=\"#ff4d4d\"/><stop offset=\"100%\" stop-color=\"#991b1b\"/></linearGradient></defs><path d=\"M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z\" fill=\"url(#tLG)\"/><path d=\"M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z\" fill=\"url(#tLG)\"/><path d=\"M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z\" fill=\"url(#tLG)\"/><path d=\"M45 15Q35 5 30 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><path d=\"M75 15Q85 5 90 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><circle cx=\"45\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"75\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"46\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/><circle cx=\"76\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/></svg></div>\n <span data-i18n=\"title\">OpenClaw Memory</span>\n </div>\n <div class=\"topbar-center\">\n <nav class=\"nav-tabs\">\n <button class=\"tab active\" data-view=\"memories\" onclick=\"switchView('memories')\" data-i18n=\"tab.memories\">\uD83D\uDCDA Memories</button>\n <button class=\"tab\" data-view=\"tasks\" onclick=\"switchView('tasks')\" data-i18n=\"tab.tasks\">\uD83D\uDCCB Tasks</button>\n <button class=\"tab\" data-view=\"skills\" onclick=\"switchView('skills')\" data-i18n=\"tab.skills\">\uD83E\uDDE0 Skills</button>\n <button class=\"tab\" data-view=\"analytics\" onclick=\"switchView('analytics')\" data-i18n=\"tab.analytics\">\uD83D\uDCCA Analytics</button>\n <button class=\"tab\" data-view=\"logs\" onclick=\"switchView('logs')\" data-i18n=\"tab.logs\">\uD83D\uDCDD Logs</button>\n <button class=\"tab\" data-view=\"import\" onclick=\"switchView('import')\" data-i18n=\"tab.import\">\uD83D\uDCE5 Import</button>\n <button class=\"tab\" data-view=\"settings\" onclick=\"switchView('settings')\" data-i18n=\"tab.settings\">\u2699 Settings</button>\n </nav>\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-icon\" onclick=\"toggleLang()\" aria-label=\"Switch language\" style=\"font-size:12px;font-weight:700;padding:4px 8px\"><span data-i18n=\"lang.switch\">EN</span></button>\n <button class=\"btn btn-icon theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"loadAll()\" data-i18n=\"refresh\">\u21BB Refresh</button>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"doLogout()\" data-i18n=\"logout\">Logout</button>\n </div>\n </div>\n\n <div class=\"main-content\">\n <div class=\"sidebar\" id=\"sidebar\">\n <div class=\"stats-grid\" id=\"statsGrid\">\n <div class=\"stat-card pri\"><div class=\"stat-value\" id=\"statTotal\">-</div><div class=\"stat-label\" data-i18n=\"stat.memories\">Memories</div></div>\n <div class=\"stat-card green\"><div class=\"stat-value\" id=\"statSessions\">-</div><div class=\"stat-label\" data-i18n=\"stat.sessions\">Sessions</div></div>\n <div class=\"stat-card amber\"><div class=\"stat-value\" id=\"statEmbeddings\">-</div><div class=\"stat-label\" data-i18n=\"stat.embeddings\">Embeddings</div></div>\n <div class=\"stat-card rose\"><div class=\"stat-value\" id=\"statTimeSpan\">-</div><div class=\"stat-label\" data-i18n=\"stat.days\">Days</div></div>\n </div>\n <div id=\"embeddingStatus\"></div>\n <div class=\"section-title\" data-i18n=\"sidebar.sessions\">Sessions</div>\n <div class=\"session-list\" id=\"sessionList\"></div>\n <button class=\"btn btn-sm btn-ghost\" style=\"width:100%;margin-top:20px;justify-content:center;color:var(--text-muted);font-size:11px\" onclick=\"clearAll()\" data-i18n=\"sidebar.clear\">\uD83D\uDDD1 Clear All Data</button>\n </div>\n\n <div class=\"feed-wrap\" id=\"feedWrap\">\n <div class=\"feed\">\n <div class=\"search-bar\">\n <span class=\"search-icon\">\uD83D\uDD0D</span>\n <input type=\"text\" id=\"searchInput\" data-i18n-ph=\"search.placeholder\" placeholder=\"Search memories (supports semantic search)...\" oninput=\"debounceSearch()\">\n </div>\n <div class=\"search-meta\" id=\"searchMeta\"></div>\n <div class=\"filter-bar\" id=\"filterBar\">\n <button class=\"filter-chip active\" data-role=\"\" onclick=\"setRoleFilter(this,'')\" data-i18n=\"filter.all\">All</button>\n <button class=\"filter-chip\" data-role=\"user\" onclick=\"setRoleFilter(this,'user')\">User</button>\n <button class=\"filter-chip\" data-role=\"assistant\" onclick=\"setRoleFilter(this,'assistant')\">Assistant</button>\n <button class=\"filter-chip\" data-role=\"system\" onclick=\"setRoleFilter(this,'system')\">System</button>\n <span class=\"filter-sep\"></span>\n <select id=\"filterKind\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"\" data-i18n=\"filter.allkinds\">All kinds</option>\n <option value=\"paragraph\" data-i18n=\"filter.paragraph\">Paragraph</option>\n <option value=\"code_block\" data-i18n=\"filter.code\">Code</option>\n <option value=\"dialog\" data-i18n=\"filter.dialog\">Dialog</option>\n <option value=\"list\" data-i18n=\"filter.list\">List</option>\n <option value=\"error_stack\" data-i18n=\"filter.error\">Error</option>\n <option value=\"command\" data-i18n=\"filter.command\">Command</option>\n </select>\n <select id=\"filterSort\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"newest\" data-i18n=\"filter.newest\">Newest first</option>\n <option value=\"oldest\" data-i18n=\"filter.oldest\">Oldest first</option>\n </select>\n </div>\n <div class=\"date-filter\">\n <label data-i18n=\"filter.from\">From</label><input type=\"datetime-local\" id=\"dateFrom\" step=\"1\" onchange=\"applyFilters()\">\n <label data-i18n=\"filter.to\">To</label><input type=\"datetime-local\" id=\"dateTo\" step=\"1\" onchange=\"applyFilters()\">\n <button class=\"btn btn-sm btn-text\" onclick=\"clearDateFilter()\" data-i18n=\"filter.clear\">Clear</button>\n </div>\n <div class=\"memory-list\" id=\"memoryList\"><div class=\"spinner\"></div></div>\n <div class=\"pagination\" id=\"pagination\"></div>\n </div>\n </div>\n <div class=\"tasks-view\" id=\"tasksView\">\n <div class=\"tasks-header\">\n <div class=\"tasks-stats\">\n <div class=\"tasks-stat\"><span class=\"tasks-stat-value\" id=\"tasksTotalCount\">-</span><span class=\"tasks-stat-label\" data-i18n=\"tasks.total\">Total Tasks</span></div>\n <div class=\"tasks-stat\"><span class=\"tasks-stat-value\" id=\"tasksActiveCount\">-</span><span class=\"tasks-stat-label\" data-i18n=\"tasks.active\">Active</span></div>\n <div class=\"tasks-stat\"><span class=\"tasks-stat-value\" id=\"tasksCompletedCount\">-</span><span class=\"tasks-stat-label\" data-i18n=\"tasks.completed\">Completed</span></div>\n <div class=\"tasks-stat\"><span class=\"tasks-stat-value\" id=\"tasksSkippedCount\">-</span><span class=\"tasks-stat-label\" data-i18n=\"tasks.status.skipped\">Skipped</span></div>\n </div>\n <div class=\"tasks-filters\">\n <button class=\"filter-chip active\" data-task-status=\"\" onclick=\"setTaskStatusFilter(this,'')\" data-i18n=\"filter.all\">All</button>\n <button class=\"filter-chip\" data-task-status=\"active\" onclick=\"setTaskStatusFilter(this,'active')\" data-i18n=\"tasks.status.active\">Active</button>\n <button class=\"filter-chip\" data-task-status=\"completed\" onclick=\"setTaskStatusFilter(this,'completed')\" data-i18n=\"tasks.status.completed\">Completed</button>\n <button class=\"filter-chip\" data-task-status=\"skipped\" onclick=\"setTaskStatusFilter(this,'skipped')\" data-i18n=\"tasks.status.skipped\">Skipped</button>\n <button class=\"btn btn-sm btn-ghost\" onclick=\"loadTasks()\" style=\"margin-left:auto\" data-i18n=\"refresh\">\u21BB Refresh</button>\n </div>\n </div>\n <div class=\"tasks-list\" id=\"tasksList\"><div class=\"spinner\"></div></div>\n <div class=\"pagination\" id=\"tasksPagination\"></div>\n <div class=\"task-detail-overlay\" id=\"taskDetailOverlay\" onclick=\"closeTaskDetail(event)\">\n <div class=\"task-detail-panel\" onclick=\"event.stopPropagation()\">\n <div class=\"task-detail-header\">\n <h2 id=\"taskDetailTitle\"></h2>\n <button class=\"btn btn-icon\" onclick=\"closeTaskDetail()\" title=\"Close\">\u2715</button>\n </div>\n <div class=\"task-detail-meta\" id=\"taskDetailMeta\"></div>\n <div class=\"task-skill-section\" id=\"taskSkillSection\"></div>\n <div class=\"task-detail-summary\" id=\"taskDetailSummary\"></div>\n <div class=\"task-detail-chunks-title\" data-i18n=\"tasks.chunks\">Related Memories</div>\n <div class=\"task-detail-chunks\" id=\"taskDetailChunks\"></div>\n </div>\n </div>\n </div>\n <div class=\"skills-view\" id=\"skillsView\">\n <div class=\"tasks-header\">\n <div class=\"tasks-stats\">\n <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>\n <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>\n <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>\n <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>\n </div>\n <div class=\"tasks-filters\">\n <button class=\"filter-chip active\" data-skill-status=\"\" onclick=\"setSkillStatusFilter(this,'')\" data-i18n=\"filter.all\">All</button>\n <button class=\"filter-chip\" data-skill-status=\"active\" onclick=\"setSkillStatusFilter(this,'active')\" data-i18n=\"skills.filter.active\">Active</button>\n <button class=\"filter-chip\" data-skill-status=\"draft\" onclick=\"setSkillStatusFilter(this,'draft')\" data-i18n=\"skills.filter.draft\">Draft</button>\n <button class=\"filter-chip\" data-skill-status=\"archived\" onclick=\"setSkillStatusFilter(this,'archived')\" data-i18n=\"skills.filter.archived\">Archived</button>\n <button class=\"btn btn-sm btn-ghost\" onclick=\"loadSkills()\" style=\"margin-left:auto\" data-i18n=\"refresh\">\u21BB Refresh</button>\n </div>\n </div>\n <div class=\"tasks-list\" id=\"skillsList\"><div class=\"spinner\"></div></div>\n </div>\n <div class=\"task-detail-overlay\" id=\"skillDetailOverlay\" onclick=\"closeSkillDetail(event)\">\n <div class=\"task-detail-panel\" onclick=\"event.stopPropagation()\">\n <div class=\"task-detail-header\">\n <h2 id=\"skillDetailTitle\"></h2>\n <div style=\"display:flex;gap:8px;align-items:center\">\n <button class=\"skill-download-btn\" id=\"skillDownloadBtn\" onclick=\"downloadSkill()\" data-i18n=\"skills.download\">\u2B07 Download</button>\n <button class=\"btn btn-icon\" onclick=\"closeSkillDetail()\" title=\"Close\">\u2715</button>\n </div>\n </div>\n <div class=\"task-detail-meta\" id=\"skillDetailMeta\"></div>\n <div class=\"skill-detail-desc\" id=\"skillDetailDesc\"></div>\n <div class=\"task-detail-chunks-title\" data-i18n=\"skills.files\">Skill Files</div>\n <div class=\"skill-files-list\" id=\"skillFilesList\"></div>\n <div class=\"task-detail-chunks-title\" id=\"skillContentTitle\" data-i18n=\"skills.content\">SKILL.md Content</div>\n <div class=\"task-detail-summary\" id=\"skillDetailContent\" style=\"max-height:50vh;overflow-y:auto\"></div>\n <div class=\"task-detail-chunks-title\" data-i18n=\"skills.versions\">Version History</div>\n <div class=\"task-detail-chunks\" id=\"skillVersionsList\" style=\"gap:10px\"></div>\n <div class=\"task-detail-chunks-title\" style=\"margin-top:16px\" data-i18n=\"skills.related\">Related Tasks</div>\n <div class=\"task-detail-chunks\" id=\"skillRelatedTasks\" style=\"gap:8px\"></div>\n </div>\n </div>\n <div class=\"analytics-view\" id=\"analyticsView\">\n <div class=\"metrics-toolbar\">\n <span style=\"font-size:12px;color:var(--text-sec);font-weight:600\" data-i18n=\"range\">Range</span>\n <button class=\"range-btn\" data-days=\"7\" onclick=\"setMetricsDays(7)\">7 <span data-i18n=\"range.days\">days</span></button>\n <button class=\"range-btn active\" data-days=\"30\" onclick=\"setMetricsDays(30)\">30 <span data-i18n=\"range.days\">days</span></button>\n <button class=\"range-btn\" data-days=\"90\" onclick=\"setMetricsDays(90)\">90 <span data-i18n=\"range.days\">days</span></button>\n <button class=\"btn btn-sm\" onclick=\"loadMetrics()\" style=\"margin-left:auto\" data-i18n=\"refresh\">\u21BB Refresh</button>\n </div>\n <div class=\"analytics-cards\" id=\"analyticsCards\">\n <div class=\"analytics-card\"><div class=\"ac-value\" id=\"mTotal\">-</div><div class=\"ac-label\" data-i18n=\"analytics.total\">Total Memories</div></div>\n <div class=\"analytics-card green\"><div class=\"ac-value\" id=\"mTodayWrites\">-</div><div class=\"ac-label\" data-i18n=\"analytics.writes\">Writes Today</div></div>\n <div class=\"analytics-card\"><div class=\"ac-value\" id=\"mSessions\">-</div><div class=\"ac-label\" data-i18n=\"analytics.sessions\">Sessions</div></div>\n <div class=\"analytics-card amber\"><div class=\"ac-value\" id=\"mEmbeddings\">-</div><div class=\"ac-label\" data-i18n=\"analytics.embeddings\">Embeddings</div></div>\n </div>\n <div class=\"analytics-section\">\n <h3><span class=\"icon\">\uD83D\uDCCA</span> <span data-i18n=\"chart.writes\">Memory Writes per Day</span></h3>\n <div class=\"chart-bars\" id=\"chartWrites\"></div>\n </div>\n \n <div class=\"analytics-section\" id=\"toolPerfSection\" style=\"position:relative\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;margin-bottom:20px\">\n <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>\n <div style=\"display:flex;gap:6px;align-items:center\">\n <button class=\"range-btn tool-range active\" data-mins=\"60\" onclick=\"setToolMinutes(60)\">1h</button>\n <button class=\"range-btn tool-range\" data-mins=\"360\" onclick=\"setToolMinutes(360)\">6h</button>\n <button class=\"range-btn tool-range\" data-mins=\"1440\" onclick=\"setToolMinutes(1440)\">24h</button>\n </div>\n </div>\n <div id=\"toolChart\" style=\"width:100%;height:280px;position:relative;overflow:hidden;border-radius:12px\"></div>\n <div id=\"toolLegend\" class=\"chart-legend\" style=\"margin-top:14px;padding:0 4px\"></div>\n <div id=\"toolAggTable\" style=\"margin-top:20px\"></div>\n </div>\n\n <div class=\"breakdown-grid\" style=\"display:grid;grid-template-columns:1fr 1fr;gap:24px\">\n <div class=\"analytics-section\">\n <h3><span class=\"icon\">\uD83D\uDC64</span> <span data-i18n=\"breakdown.role\">By Role</span></h3>\n <div id=\"breakdownRole\"></div>\n </div>\n <div class=\"analytics-section\">\n <h3><span class=\"icon\">\uD83D\uDCDD</span> <span data-i18n=\"breakdown.kind\">By Kind</span></h3>\n <div id=\"breakdownKind\"></div>\n </div>\n </div>\n </div>\n\n <!-- \u2500\u2500\u2500 Logs View \u2500\u2500\u2500 -->\n <div class=\"logs-view\" id=\"logsView\">\n <div class=\"logs-toolbar\">\n <div class=\"logs-toolbar-left\">\n <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\">\n <option value=\"\" data-i18n=\"logs.allTools\">All Tools</option>\n </select>\n <button class=\"btn btn-sm btn-ghost\" onclick=\"loadLogs()\" style=\"font-size:12px\">\u21BB <span data-i18n=\"logs.refresh\">Refresh</span></button>\n </div>\n <div class=\"logs-toolbar-right\">\n <input type=\"checkbox\" id=\"logAutoRefresh\" style=\"display:none\">\n </div>\n </div>\n <div class=\"logs-list\" id=\"logsList\"></div>\n <div id=\"logsPagination\"></div>\n </div>\n\n <!-- \u2500\u2500\u2500 Settings View \u2500\u2500\u2500 -->\n <div class=\"settings-view\" id=\"settingsView\">\n <div class=\"settings-group\" id=\"settingsModelConfig\">\n <h2 class=\"settings-group-title\"><span data-i18n=\"settings.modelconfig\">Model Configuration</span></h2>\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83D\uDCE1</span> <span data-i18n=\"settings.embedding\">Embedding Model</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-field\">\n <label data-i18n=\"settings.provider\">Provider</label>\n <select id=\"cfgEmbProvider\">\n <option value=\"openai_compatible\">OpenAI Compatible</option>\n <option value=\"openai\">OpenAI</option>\n <option value=\"gemini\">Gemini</option>\n <option value=\"azure_openai\">Azure OpenAI</option>\n <option value=\"cohere\">Cohere</option>\n <option value=\"mistral\">Mistral</option>\n <option value=\"voyage\">Voyage</option>\n <option value=\"local\">Local</option>\n </select>\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.model\">Model</label>\n <input type=\"text\" id=\"cfgEmbModel\" placeholder=\"e.g. bge-m3\">\n </div>\n <div class=\"settings-field full-width\">\n <label>Endpoint</label>\n <input type=\"text\" id=\"cfgEmbEndpoint\" placeholder=\"https://...\">\n </div>\n <div class=\"settings-field\">\n <label>API Key</label>\n <input type=\"password\" id=\"cfgEmbApiKey\" placeholder=\"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\">\n </div>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83E\uDDE0</span> <span data-i18n=\"settings.summarizer\">Summarizer Model</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-field\">\n <label data-i18n=\"settings.provider\">Provider</label>\n <select id=\"cfgSumProvider\">\n <option value=\"openai_compatible\">OpenAI Compatible</option>\n <option value=\"openai\">OpenAI</option>\n <option value=\"anthropic\">Anthropic</option>\n <option value=\"gemini\">Gemini</option>\n <option value=\"azure_openai\">Azure OpenAI</option>\n <option value=\"bedrock\">Bedrock</option>\n </select>\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.model\">Model</label>\n <input type=\"text\" id=\"cfgSumModel\" placeholder=\"e.g. gpt-4o-mini\">\n </div>\n <div class=\"settings-field full-width\">\n <label>Endpoint</label>\n <input type=\"text\" id=\"cfgSumEndpoint\" placeholder=\"https://...\">\n </div>\n <div class=\"settings-field\">\n <label>API Key</label>\n <input type=\"password\" id=\"cfgSumApiKey\" placeholder=\"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\">\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.temperature\">Temperature</label>\n <input type=\"number\" id=\"cfgSumTemp\" step=\"0.1\" min=\"0\" max=\"2\" placeholder=\"0\">\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83D\uDD27</span> <span data-i18n=\"settings.skill\">Skill Evolution</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-toggle\">\n <label class=\"toggle-switch\"><input type=\"checkbox\" id=\"cfgSkillEnabled\"><span class=\"toggle-slider\"></span></label>\n <label data-i18n=\"settings.skill.enabled\">Enable Skill Evolution</label>\n </div>\n <div class=\"settings-toggle\">\n <label class=\"toggle-switch\"><input type=\"checkbox\" id=\"cfgSkillAutoInstall\"><span class=\"toggle-slider\"></span></label>\n <label data-i18n=\"settings.skill.autoinstall\">Auto Install Skills</label>\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.skill.confidence\">Min Confidence</label>\n <input type=\"number\" id=\"cfgSkillConfidence\" step=\"0.1\" min=\"0\" max=\"1\" placeholder=\"0.7\">\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.skill.minchunks\">Min Chunks</label>\n <input type=\"number\" id=\"cfgSkillMinChunks\" placeholder=\"6\">\n </div>\n </div>\n <div style=\"margin-top:16px;padding-top:16px;border-top:1px solid var(--border)\">\n <h4 style=\"font-size:12px;font-weight:600;color:var(--text-sec);margin-bottom:12px\"><span data-i18n=\"settings.skill.model\">Skill Dedicated Model</span></h4>\n <div class=\"field-hint\" style=\"margin-bottom:12px\" 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>\n <div class=\"settings-grid\">\n <div class=\"settings-field\">\n <label data-i18n=\"settings.provider\">Provider</label>\n <select id=\"cfgSkillProvider\">\n <option value=\"\">\u2014 <span data-i18n=\"settings.skill.usemain\">Use main summarizer</span> \u2014</option>\n <option value=\"openai_compatible\">OpenAI Compatible</option>\n <option value=\"openai\">OpenAI</option>\n <option value=\"anthropic\">Anthropic</option>\n <option value=\"gemini\">Gemini</option>\n <option value=\"azure_openai\">Azure OpenAI</option>\n <option value=\"bedrock\">Bedrock</option>\n </select>\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.model\">Model</label>\n <input type=\"text\" id=\"cfgSkillModel\" placeholder=\"e.g. claude-4.6-opus\">\n </div>\n <div class=\"settings-field full-width\">\n <label>Endpoint</label>\n <input type=\"text\" id=\"cfgSkillEndpoint\" placeholder=\"https://...\">\n </div>\n <div class=\"settings-field\">\n <label>API Key</label>\n <input type=\"password\" id=\"cfgSkillApiKey\" placeholder=\"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\">\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83D\uDCCA</span> <span data-i18n=\"settings.telemetry\">Telemetry</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-toggle\">\n <label class=\"toggle-switch\"><input type=\"checkbox\" id=\"cfgTelemetryEnabled\" checked><span class=\"toggle-slider\"></span></label>\n <label data-i18n=\"settings.telemetry.enabled\">Enable Anonymous Telemetry</label>\n </div>\n <div class=\"settings-field full-width\">\n <div class=\"field-hint\" data-i18n=\"settings.telemetry.hint\">Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.</div>\n </div>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83D\uDCBE</span> <span data-i18n=\"settings.general\">General</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-field\">\n <label data-i18n=\"settings.viewerport\">Viewer Port</label>\n <input type=\"number\" id=\"cfgViewerPort\" placeholder=\"18799\">\n <div class=\"field-hint\" data-i18n=\"settings.viewerport.hint\">Requires restart to take effect</div>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <span class=\"settings-saved\" id=\"settingsSaved\">\u2713 <span data-i18n=\"settings.saved\">Saved</span></span>\n <button class=\"btn btn-ghost\" onclick=\"loadConfig()\" data-i18n=\"settings.reset\">Reset</button>\n <button class=\"btn btn-primary\" onclick=\"saveConfig()\" data-i18n=\"settings.save\">Save Settings</button>\n </div>\n <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>\n </div>\n\n <!-- \u2500\u2500\u2500 Import Page \u2500\u2500\u2500 -->\n <div class=\"migrate-view\" id=\"migrateView\">\n <div class=\"settings-section\" style=\"border:1px solid rgba(99,102,241,.15)\">\n <h3><span class=\"icon\">\uD83D\uDCE5</span> <span data-i18n=\"migrate.title\">Import OpenClaw Memory</span></h3>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:12px;line-height:1.6\" data-i18n=\"migrate.desc\">Migrate your existing OpenClaw built-in memories and conversation history into this plugin. The import process uses smart deduplication to avoid duplicates.</p>\n\n <div style=\"background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:12px;line-height:1.7;color:var(--text-sec)\">\n <div style=\"font-weight:700;color:var(--text);margin-bottom:8px\" data-i18n=\"migrate.modes.title\">Three ways to use:</div>\n <div style=\"display:flex;flex-direction:column;gap:6px\">\n <div><span style=\"font-weight:600;color:var(--accent)\" data-i18n=\"migrate.mode1.label\">\u2460 Import memories only (fast)</span><span data-i18n=\"migrate.mode1.desc\"> \u2014 Click \"Start Import\" to quickly migrate all memory chunks and conversations. No task/skill generation. Suitable when you just need the raw data.</span></div>\n <div><span style=\"font-weight:600;color:var(--accent)\" data-i18n=\"migrate.mode2.label\">\u2461 Import + generate tasks & skills (slow, serial)</span><span data-i18n=\"migrate.mode2.desc\"> \u2014 After importing memories, enable \"Generate Tasks\" and/or \"Trigger Skill Evolution\" below to analyze conversations one by one. This takes longer as each session is processed by LLM sequentially.</span></div>\n <div><span style=\"font-weight:600;color:var(--accent)\" data-i18n=\"migrate.mode3.label\">\u2462 Import first, generate later (flexible)</span><span data-i18n=\"migrate.mode3.desc\"> \u2014 Import memories now, then come back anytime to start task/skill generation. You can pause the generation at any point and resume later \u2014 it will pick up where you left off, only processing sessions that haven't been handled yet.</span></div>\n </div>\n </div>\n\n <div id=\"migrateConfigWarn\" style=\"display:none;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.3);border-radius:10px;padding:14px 18px;margin-bottom:16px\">\n <div style=\"font-size:12px;font-weight:600;color:#f59e0b;margin-bottom:6px\">\u26A0 <span data-i18n=\"migrate.config.warn\">Configuration Required</span></div>\n <div style=\"font-size:11px;color:var(--text-sec);line-height:1.5\" data-i18n=\"migrate.config.warn.desc\">Please configure both Embedding Model and Summarizer Model in Settings before importing. These are required for processing memories.</div>\n </div>\n\n <div id=\"migrateScanResult\" style=\"display:none;margin-bottom:16px\">\n <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:12px\">\n <div style=\"background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px\">\n <div style=\"font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:6px\" data-i18n=\"migrate.sqlite.label\">Memory Index (SQLite)</div>\n <div style=\"font-size:22px;font-weight:700;color:var(--text)\" id=\"migrateSqliteCount\">0</div>\n <div style=\"font-size:10px;color:var(--text-muted);margin-top:2px\" id=\"migrateSqliteFiles\"></div>\n </div>\n <div style=\"background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px\">\n <div style=\"font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:6px\" data-i18n=\"migrate.sessions.label\">Conversation History</div>\n <div style=\"font-size:22px;font-weight:700;color:var(--text)\" id=\"migrateSessionCount\">0</div>\n <div style=\"font-size:10px;color:var(--text-muted);margin-top:2px\" id=\"migrateSessionFiles\"></div>\n </div>\n </div>\n </div>\n\n <div id=\"migrateActions\" style=\"display:flex;gap:12px;align-items:center;flex-wrap:wrap\">\n <button class=\"btn btn-ghost\" onclick=\"migrateScan()\" id=\"migrateScanBtn\" data-i18n=\"migrate.scan\">Scan Data Sources</button>\n <button class=\"btn btn-primary\" onclick=\"migrateStart()\" id=\"migrateStartBtn\" style=\"display:none\" data-i18n=\"migrate.start\">Start Import</button>\n <span id=\"migrateStatus\" style=\"font-size:11px;color:var(--text-muted)\"></span>\n </div>\n\n <!-- Post-process section: shown after import completes -->\n <div id=\"postprocessSection\" style=\"display:none;margin-top:16px\">\n <div class=\"settings-section\" style=\"border:1px solid var(--border)\">\n <div style=\"font-size:14px;font-weight:700;color:var(--text);margin-bottom:6px\" data-i18n=\"pp.title\">\uD83E\uDDE0 Optional: Generate Tasks & Skills</div>\n <div style=\"font-size:12px;color:var(--text-sec);margin-bottom:14px;line-height:1.6\" data-i18n=\"pp.desc\">This step is completely optional. The import above has already stored raw memory data. Here you can further analyze imported conversations to generate structured task summaries and evolve reusable skills. Processing is serial (one session at a time) and may take a while. You can stop at any time and resume later \u2014 it will only process sessions not yet handled.</div>\n <div style=\"display:flex;flex-direction:column;gap:8px;margin-bottom:14px\">\n <label style=\"display:flex;align-items:flex-start;gap:8px;cursor:pointer\">\n <input type=\"checkbox\" id=\"ppEnableTasks\" checked style=\"accent-color:var(--accent);margin-top:2px\">\n <div>\n <div style=\"font-size:12px;font-weight:600;color:var(--text)\" data-i18n=\"pp.tasks.label\">Generate task summaries</div>\n <div style=\"font-size:11px;color:var(--text-sec);line-height:1.4\" data-i18n=\"pp.tasks.hint\">Group imported messages into tasks and generate a structured summary (title, goal, steps, result) for each one. Makes it easier to search and recall past work.</div>\n </div>\n </label>\n <label style=\"display:flex;align-items:flex-start;gap:8px;cursor:pointer\">\n <input type=\"checkbox\" id=\"ppEnableSkills\" style=\"accent-color:var(--accent);margin-top:2px\">\n <div>\n <div style=\"font-size:12px;font-weight:600;color:var(--text)\" data-i18n=\"pp.skills.label\">Trigger skill evolution</div>\n <div style=\"font-size:11px;color:var(--text-sec);line-height:1.4\" data-i18n=\"pp.skills.hint\">Analyze completed tasks and automatically create or upgrade reusable skills (SKILL.md). Requires task summaries to be enabled. May take longer due to LLM evaluation.</div>\n </div>\n </label>\n </div>\n <div style=\"display:flex;gap:10px;align-items:center\">\n <button class=\"btn btn-primary\" id=\"ppStartBtn\" onclick=\"ppStart()\" data-i18n=\"pp.start\">Start Processing</button>\n <button class=\"btn btn-sm\" id=\"ppStopBtn\" onclick=\"ppStop()\" style=\"display:none;background:rgba(239,68,68,.12);color:#ef4444;border:1px solid rgba(239,68,68,.3);font-size:12px;padding:5px 16px;font-weight:600\" data-i18n=\"migrate.stop\">\u25A0 Stop</button>\n <span id=\"ppStatus\" style=\"font-size:11px;color:var(--text-muted)\"></span>\n </div>\n <div id=\"ppProgress\" style=\"display:none;margin-top:12px\">\n <div style=\"display:flex;align-items:center;gap:12px;margin-bottom:8px\">\n <div style=\"font-size:12px;font-weight:600;color:var(--text)\" id=\"ppPhaseLabel\"></div>\n <div style=\"font-size:11px;color:var(--text-muted);flex:1\" id=\"ppCounter\"></div>\n </div>\n <div style=\"position:relative;height:5px;background:var(--bg);border-radius:3px;overflow:hidden;margin-bottom:12px\">\n <div id=\"ppBar\" style=\"position:absolute;left:0;top:0;height:100%;width:0%;background:linear-gradient(90deg,#f59e0b,#fbbf24);border-radius:3px;transition:width .3s ease\"></div>\n </div>\n <div style=\"display:flex;gap:16px;margin-bottom:12px\" id=\"ppStatsRow\">\n <div style=\"display:flex;align-items:center;gap:5px;font-size:11px\">\n <span style=\"width:7px;height:7px;border-radius:50%;background:#22c55e;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"pp.stat.tasks\">Tasks</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"ppStatTasks\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:5px;font-size:11px\">\n <span style=\"width:7px;height:7px;border-radius:50%;background:#8b5cf6;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"pp.stat.skills\">Skills</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"ppStatSkills\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:5px;font-size:11px\">\n <span style=\"width:7px;height:7px;border-radius:50%;background:#ef4444;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"pp.stat.errors\">Errors</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"ppStatErrors\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:5px;font-size:11px\" id=\"ppSkippedInfo\" style=\"display:none\">\n <span style=\"width:7px;height:7px;border-radius:50%;background:#3b82f6;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"pp.stat.skipped\">Skipped</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"ppStatSkipped\">0</span>\n </div>\n </div>\n <div id=\"ppLiveLog\" style=\"background:var(--bg);border:1px solid var(--border);border-radius:8px;max-height:320px;overflow-y:auto;font-family:'SF Mono','Fira Code',monospace;font-size:11px;line-height:1.7;padding:0\"></div>\n </div>\n <div id=\"ppDone\" style=\"display:none;margin-top:12px;padding:10px 14px;border-radius:8px;font-size:12px;color:var(--text-sec);line-height:1.5\"></div>\n </div>\n </div>\n </div>\n\n <!-- Progress Area -->\n <div id=\"migrateProgress\" style=\"display:none\">\n <div class=\"settings-section\">\n <div style=\"display:flex;align-items:center;gap:12px;margin-bottom:12px\">\n <div style=\"font-size:13px;font-weight:600;color:var(--text)\" id=\"migratePhaseLabel\"></div>\n <div style=\"font-size:12px;color:var(--text-muted);flex:1\" id=\"migrateCounter\"></div>\n <button class=\"btn btn-sm\" id=\"migrateStopBtn\" onclick=\"migrateStop()\" style=\"background:rgba(239,68,68,.12);color:#ef4444;border:1px solid rgba(239,68,68,.3);font-size:12px;padding:5px 16px;font-weight:600;cursor:pointer\" data-i18n=\"migrate.stop\">\u25A0 Stop</button>\n </div>\n\n <div style=\"position:relative;height:6px;background:var(--bg);border-radius:3px;overflow:hidden;margin-bottom:16px\">\n <div id=\"migrateBar\" style=\"position:absolute;left:0;top:0;height:100%;width:0%;background:linear-gradient(90deg,#6366f1,#8b5cf6);border-radius:3px;transition:width .3s ease\"></div>\n </div>\n\n <div style=\"display:flex;gap:20px;margin-bottom:16px\" id=\"migrateStatsRow\">\n <div style=\"display:flex;align-items:center;gap:6px;font-size:12px\">\n <span style=\"width:8px;height:8px;border-radius:50%;background:#22c55e;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"migrate.stat.stored\">Stored</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"migrateStatStored\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:6px;font-size:12px\">\n <span style=\"width:8px;height:8px;border-radius:50%;background:#f59e0b;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"migrate.stat.skipped\">Skipped</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"migrateStatSkipped\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:6px;font-size:12px\">\n <span style=\"width:8px;height:8px;border-radius:50%;background:#3b82f6;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"migrate.stat.merged\">Merged</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"migrateStatMerged\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:6px;font-size:12px\">\n <span style=\"width:8px;height:8px;border-radius:50%;background:#ef4444;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"migrate.stat.errors\">Errors</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"migrateStatErrors\">0</span>\n </div>\n </div>\n\n <div id=\"migrateLiveLog\" style=\"background:var(--bg);border:1px solid var(--border);border-radius:10px;max-height:480px;overflow-y:auto;font-family:'SF Mono','Fira Code',monospace;font-size:11px;line-height:1.7;padding:0\">\n </div>\n </div>\n </div>\n\n </div>\n\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Memory Modal \u2500\u2500\u2500 -->\n<div class=\"modal-overlay\" id=\"modalOverlay\">\n <div class=\"modal\">\n <h2 id=\"modalTitle\" data-i18n=\"modal.new\">New Memory</h2>\n <div class=\"form-group\"><label data-i18n=\"modal.role\">Role</label><select id=\"mRole\"><option value=\"user\">User</option><option value=\"assistant\">Assistant</option><option value=\"system\">System</option></select></div>\n <div class=\"form-group\"><label data-i18n=\"modal.content\">Content</label><textarea id=\"mContent\" rows=\"4\" data-i18n-ph=\"modal.content.ph\" placeholder=\"Memory content...\"></textarea></div>\n <div class=\"form-group\"><label data-i18n=\"modal.summary\">Summary</label><input type=\"text\" id=\"mSummary\" data-i18n-ph=\"modal.summary.ph\" placeholder=\"Brief summary (optional)\"></div>\n <div class=\"form-group\"><label data-i18n=\"modal.kind\">Kind</label><select id=\"mKind\"><option value=\"paragraph\" data-i18n=\"filter.paragraph\">Paragraph</option><option value=\"code\" data-i18n=\"filter.code\">Code</option><option value=\"dialog\" data-i18n=\"filter.dialog\">Dialog</option></select></div>\n <div class=\"modal-actions\">\n <button class=\"btn btn-ghost\" onclick=\"closeModal()\" data-i18n=\"modal.cancel\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"modalSubmit\" onclick=\"submitModal()\" data-i18n=\"modal.create\">Create</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Toast \u2500\u2500\u2500 -->\n<div class=\"toast-container\" id=\"toasts\"></div>\n\n<script>\nlet activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=40,metricsDays=30;\n\n/* \u2500\u2500\u2500 i18n \u2500\u2500\u2500 */\nconst I18N={\n en:{\n 'title':'OpenClaw Memory',\n 'subtitle':'Powered by MemOS',\n 'setup.desc':'Set a password to protect your memories',\n 'setup.pw':'Enter a password (4+ characters)',\n 'setup.pw2':'Confirm password',\n 'setup.btn':'Set Password & Enter',\n 'setup.err.short':'Password must be at least 4 characters',\n 'setup.err.mismatch':'Passwords do not match',\n 'setup.err.fail':'Setup failed',\n 'login.desc':'Enter your password to access memories',\n 'login.pw':'Password',\n 'login.btn':'Unlock',\n 'login.err':'Incorrect password',\n 'login.forgot':'Forgot password?',\n 'reset.step1.title':'Open Terminal',\n 'reset.step1.desc':'Run the following command to get your reset token (use the pattern below so you get the line that contains the token):',\n 'reset.step2.title':'Find the token',\n 'reset.step2.desc.pre':'In the output, find ',\n 'reset.step2.desc.post':' (plain line or inside JSON). Copy the 32-character hex string after the colon.',\n 'reset.step3.title':'Paste & reset',\n 'reset.step3.desc':'Paste the token below and set your new password.',\n 'reset.token':'Paste reset token here',\n 'reset.newpw':'New password (4+ characters)',\n 'reset.newpw2':'Confirm new password',\n 'reset.btn':'Reset Password',\n 'reset.err.token':'Please enter the reset token',\n 'reset.err.short':'Password must be at least 4 characters',\n 'reset.err.mismatch':'Passwords do not match',\n 'reset.err.fail':'Reset failed',\n 'reset.back':'\\u2190 Back to login',\n 'copy.hint':'Click to copy',\n 'copy.done':'Copied!',\n 'tab.memories':'\\u{1F4DA} Memories',\n 'tab.tasks':'\\u{1F4CB} Tasks',\n 'tab.skills':'\\u{1F9E0} Skills',\n 'tab.analytics':'\\u{1F4CA} Analytics',\n 'skills.total':'Total Skills',\n 'skills.active':'Active',\n 'skills.installed':'Installed',\n 'tasks.total':'Total Tasks',\n 'tasks.active':'Active',\n 'tasks.completed':'Completed',\n 'tasks.status.active':'Active',\n 'tasks.status.completed':'Completed',\n 'tasks.status.skipped':'Skipped',\n 'tasks.empty':'No tasks yet. Tasks are automatically created as you converse.',\n 'tasks.loading':'Loading...',\n 'tasks.untitled':'Untitled Task',\n 'tasks.chunks':'Related Memories',\n 'tasks.nochunks':'No memories in this task yet.',\n 'tasks.skipped.default':'This conversation was too brief to generate a summary. It will not appear in search results.',\n 'refresh':'\\u21BB Refresh',\n 'logout':'Logout',\n 'stat.memories':'Memories',\n 'stat.sessions':'Sessions',\n 'stat.embeddings':'Embeddings',\n 'stat.days':'Days',\n 'stat.active':'active',\n 'stat.deduped':'deduped',\n 'sidebar.sessions':'Sessions',\n 'sidebar.allsessions':'All Sessions',\n 'sidebar.clear':'\\u{1F5D1} Clear All Data',\n 'search.placeholder':'Search memories (supports semantic search)...',\n 'search.meta.total':' memories total',\n 'search.meta.semantic':' semantic',\n 'search.meta.text':' text',\n 'search.meta.results':' results',\n 'filter.all':'All',\n 'filter.allkinds':'All kinds',\n 'filter.paragraph':'Paragraph',\n 'filter.code':'Code',\n 'filter.dialog':'Dialog',\n 'filter.list':'List',\n 'filter.error':'Error',\n 'filter.command':'Command',\n 'filter.newest':'Newest first',\n 'filter.oldest':'Oldest first',\n 'filter.from':'From',\n 'filter.to':'To',\n 'filter.clear':'Clear',\n 'empty.text':'No memories found',\n 'card.expand':'Expand',\n 'card.edit':'Edit',\n 'card.delete':'Delete',\n 'card.evolved':'Evolved',\n 'card.times':'times',\n 'card.updated':'updated',\n 'card.evolveHistory':'Evolution History',\n 'card.oldSummary':'Old',\n 'card.dedupDuplicate':'Duplicate',\n 'card.dedupMerged':'Merged',\n 'card.dedupTarget':'Target: ',\n 'card.dedupReason':'Reason: ',\n 'card.newSummary':'New',\n 'pagination.total':' total',\n 'range':'Range',\n 'range.days':'days',\n 'analytics.total':'Total Memories',\n 'analytics.writes':'Writes Today',\n 'analytics.calls':'Viewer Calls Today',\n 'analytics.sessions':'Sessions',\n 'analytics.embeddings':'Embeddings',\n 'chart.writes':'Memory Writes per Day',\n 'chart.calls':'Viewer API Calls per Day (List / Search)',\n 'chart.nodata':'No data in this range',\n 'chart.nocalls':'No viewer calls in this range',\n 'chart.toolperf':'Tool Response Time',\n 'chart.list':'List',\n 'chart.search':'Search',\n 'breakdown.role':'By Role',\n 'breakdown.kind':'By Kind',\n 'modal.new':'New Memory',\n 'modal.edit':'Edit Memory',\n 'modal.role':'Role',\n 'modal.content':'Content',\n 'modal.content.ph':'Memory content...',\n 'modal.summary':'Summary',\n 'modal.summary.ph':'Brief summary (optional)',\n 'modal.kind':'Kind',\n 'modal.cancel':'Cancel',\n 'modal.create':'Create',\n 'modal.save':'Save',\n 'modal.err.empty':'Please enter content',\n 'toast.created':'Memory created',\n 'toast.updated':'Memory updated',\n 'toast.deleted':'Memory deleted',\n 'toast.opfail':'Operation failed',\n 'toast.delfail':'Delete failed',\n 'toast.cleared':'All memories cleared',\n 'toast.clearfail':'Clear failed',\n 'toast.notfound':'Memory not found in cache',\n 'confirm.delete':'Delete this memory?',\n 'confirm.clearall':'Delete ALL memories? This cannot be undone.',\n 'confirm.clearall2':'Are you absolutely sure?',\n 'embed.on':'Embedding: ',\n 'embed.off':'No embedding model',\n 'lang.switch':'\u4E2D',\n 'tab.logs':'\uD83D\uDCDD Logs',\n 'logs.allTools':'All Tools',\n 'logs.refresh':'Refresh',\n 'logs.autoRefresh':'Auto-refresh',\n 'logs.input':'INPUT',\n 'logs.output':'OUTPUT',\n 'logs.empty':'No logs yet. Logs will appear here when tools are called.',\n 'logs.ago':'ago',\n 'tab.import':'\uD83D\uDCE5 Import',\n 'tab.settings':'\u2699 Settings',\n 'settings.modelconfig':'Model Configuration',\n 'settings.embedding':'Embedding Model',\n 'settings.summarizer':'Summarizer Model',\n 'settings.skill':'Skill Evolution',\n 'settings.general':'General',\n 'settings.provider':'Provider',\n 'settings.model':'Model',\n 'settings.temperature':'Temperature',\n 'settings.skill.enabled':'Enable Skill Evolution',\n 'settings.skill.autoinstall':'Auto Install Skills',\n 'settings.skill.confidence':'Min Confidence',\n 'settings.skill.minchunks':'Min Chunks',\n 'settings.skill.model':'Skill Dedicated Model',\n '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.',\n 'settings.optional':'Optional',\n 'settings.skill.usemain':'Use Main Summarizer',\n 'settings.telemetry':'Telemetry',\n 'settings.telemetry.enabled':'Enable Anonymous Telemetry',\n 'settings.telemetry.hint':'Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.',\n 'settings.viewerport':'Viewer Port',\n 'settings.viewerport.hint':'Requires restart to take effect',\n 'settings.save':'Save Settings',\n 'settings.reset':'Reset',\n 'settings.saved':'Saved',\n 'settings.restart.hint':'Some changes require restarting the OpenClaw gateway to take effect.',\n 'settings.save.fail':'Failed to save settings',\n 'migrate.title':'Import OpenClaw Memory',\n 'migrate.desc':'Migrate your existing OpenClaw built-in memories and conversation history into this plugin. The import process uses smart deduplication to avoid duplicates.',\n 'migrate.modes.title':'Three ways to use:',\n 'migrate.mode1.label':'\\u2460 Import memories only (fast)',\n 'migrate.mode1.desc':' \u2014 Click \"Start Import\" to quickly migrate all memory chunks and conversations. No task/skill generation. Suitable when you just need the raw data.',\n 'migrate.mode2.label':'\\u2461 Import + generate tasks & skills (slow, serial)',\n 'migrate.mode2.desc':' \u2014 After importing memories, enable \"Generate Tasks\" and/or \"Trigger Skill Evolution\" below to analyze conversations one by one. This takes longer as each session is processed by LLM sequentially.',\n 'migrate.mode3.label':'\\u2462 Import first, generate later (flexible)',\n 'migrate.mode3.desc':' \u2014 Import memories now, then come back anytime to start task/skill generation. You can pause the generation at any point and resume later \u2014 it will pick up where you left off, only processing sessions that haven\\'t been handled yet.',\n 'migrate.config.warn':'Configuration Required',\n 'migrate.config.warn.desc':'Please configure both Embedding Model and Summarizer Model above before importing. These are required for processing memories.',\n 'migrate.sqlite.label':'Memory Index (SQLite)',\n 'migrate.sessions.label':'Conversation History',\n 'migrate.scan':'Scan Data Sources',\n 'migrate.start':'Start Import',\n 'migrate.scanning':'Scanning...',\n 'migrate.stat.stored':'Stored',\n 'migrate.stat.skipped':'Skipped',\n 'migrate.stat.merged':'Merged',\n 'migrate.stat.errors':'Errors',\n 'migrate.phase.sqlite':'Importing memory index...',\n 'migrate.phase.sessions':'Importing conversation history...',\n 'migrate.chunks':'chunks',\n 'migrate.sessions.count':'sessions, {n} messages',\n 'migrate.nodata':'No OpenClaw data found to import.',\n 'migrate.running':'Import in progress...',\n 'migrate.error.running':'A migration is already in progress.',\n 'migrate.stop':'\\u25A0 Stop',\n 'migrate.stopping':'Stopping...',\n 'migrate.resume':'Continue Import',\n 'pp.title':'\\u{1F9E0} Optional: Generate Tasks & Skills',\n 'pp.desc':'This step is completely optional. The import above has already stored raw memory data. Here you can further analyze imported conversations to generate structured task summaries and evolve reusable skills. Processing is serial (one session at a time) and may take a while. You can stop at any time and resume later \u2014 it will only process sessions not yet handled.',\n 'pp.tasks.label':'Generate task summaries',\n 'pp.tasks.hint':'Group imported messages into tasks and generate a structured summary (title, goal, steps, result) for each one. Makes it easier to search and recall past work.',\n 'pp.skills.label':'Trigger skill evolution',\n 'pp.skills.hint':'Analyze completed tasks and automatically create or upgrade reusable skills (SKILL.md). Requires task summaries to be enabled. May take longer due to LLM evaluation.',\n 'pp.start':'Start Processing',\n 'pp.resume':'Resume Processing',\n 'pp.running':'Processing',\n 'pp.stopped':'Processing stopped. You can resume anytime.',\n 'pp.failed':'Processing failed \u2014 see error message above.',\n 'pp.done':'Task & skill generation complete!',\n 'pp.select.warn':'Please select at least one option.',\n 'pp.skill.created':'Skill created',\n 'pp.stat.tasks':'Tasks',\n 'pp.stat.skills':'Skills',\n 'pp.stat.errors':'Errors',\n 'pp.stat.skipped':'Skipped',\n 'pp.info.skipped':'{n} sessions already processed, skipping.',\n 'pp.info.pending':'Processing {n} sessions...',\n 'pp.info.allDone':'All sessions have been processed already. Nothing to do.',\n 'pp.action.full':'Task+Skill',\n 'pp.action.skillOnly':'Skill only (task exists)',\n 'card.imported':'OpenClaw Native',\n 'skills.draft':'Draft',\n 'skills.filter.active':'Active',\n 'skills.filter.draft':'Draft',\n 'skills.filter.archived':'Archived',\n 'skills.files':'Skill Files',\n 'skills.content':'SKILL.md Content',\n 'skills.versions':'Version History',\n 'skills.related':'Related Tasks',\n 'skills.download':'\u2B07 Download',\n 'skills.installed.badge':'Installed',\n 'skills.empty':'No skills yet. Skills are automatically generated from completed tasks that contain reusable experience.',\n 'skills.loading':'Loading...',\n 'skills.error':'Error loading skill',\n 'skills.error.detail':'Failed to load skill: ',\n 'skills.nofiles':'No files found',\n 'skills.noversions':'No versions recorded',\n 'skills.norelated':'No related tasks',\n 'skills.nocontent':'No content available',\n 'skills.nochangelog':'No changelog',\n 'skills.status.active':'Active',\n 'skills.status.draft':'Draft',\n 'skills.status.archived':'Archived',\n 'skills.updated':'Updated: ',\n 'skills.task.prefix':'Task: ',\n 'tasks.chunks.label':'chunks',\n 'tasks.taskid':'Task ID: ',\n 'tasks.role.user':'You',\n 'tasks.role.assistant':'Assistant',\n 'tasks.error':'Error',\n 'tasks.error.detail':'Failed to load task details',\n 'tasks.untitled.related':'Untitled'\n },\n zh:{\n 'title':'OpenClaw \u8BB0\u5FC6',\n 'subtitle':'\u7531 MemOS \u9A71\u52A8',\n 'setup.desc':'\u8BBE\u7F6E\u5BC6\u7801\u4EE5\u4FDD\u62A4\u4F60\u7684\u8BB0\u5FC6\u6570\u636E',\n 'setup.pw':'\u8F93\u5165\u5BC6\u7801\uFF08\u81F3\u5C114\u4F4D\uFF09',\n 'setup.pw2':'\u786E\u8BA4\u5BC6\u7801',\n 'setup.btn':'\u8BBE\u7F6E\u5BC6\u7801\u5E76\u8FDB\u5165',\n 'setup.err.short':'\u5BC6\u7801\u81F3\u5C11\u9700\u89814\u4E2A\u5B57\u7B26',\n 'setup.err.mismatch':'\u4E24\u6B21\u5BC6\u7801\u4E0D\u4E00\u81F4',\n 'setup.err.fail':'\u8BBE\u7F6E\u5931\u8D25',\n 'login.desc':'\u8F93\u5165\u5BC6\u7801\u4EE5\u8BBF\u95EE\u8BB0\u5FC6',\n 'login.pw':'\u5BC6\u7801',\n 'login.btn':'\u89E3\u9501',\n 'login.err':'\u5BC6\u7801\u9519\u8BEF',\n 'login.forgot':'\u5FD8\u8BB0\u5BC6\u7801\uFF1F',\n 'reset.step1.title':'\u6253\u5F00\u7EC8\u7AEF',\n 'reset.step1.desc':'\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u83B7\u53D6\u91CD\u7F6E\u4EE4\u724C\uFF1A',\n 'reset.step2.title':'\u627E\u5230\u4EE4\u724C',\n 'reset.step2.desc.pre':'\u5728\u8F93\u51FA\u4E2D\u627E\u5230 ',\n 'reset.step2.desc.post':'\uFF08\u7EAF\u6587\u672C\u884C\u6216 JSON \u5185\uFF09\u3002\u590D\u5236\u5192\u53F7\u540E\u768432\u4F4D\u5341\u516D\u8FDB\u5236\u5B57\u7B26\u4E32\u3002',\n 'reset.step3.title':'\u7C98\u8D34\u5E76\u91CD\u7F6E',\n 'reset.step3.desc':'\u5C06\u4EE4\u724C\u7C98\u8D34\u5230\u4E0B\u65B9\u5E76\u8BBE\u7F6E\u65B0\u5BC6\u7801\u3002',\n 'reset.token':'\u5728\u6B64\u7C98\u8D34\u91CD\u7F6E\u4EE4\u724C',\n 'reset.newpw':'\u65B0\u5BC6\u7801\uFF08\u81F3\u5C114\u4F4D\uFF09',\n 'reset.newpw2':'\u786E\u8BA4\u65B0\u5BC6\u7801',\n 'reset.btn':'\u91CD\u7F6E\u5BC6\u7801',\n 'reset.err.token':'\u8BF7\u8F93\u5165\u91CD\u7F6E\u4EE4\u724C',\n 'reset.err.short':'\u5BC6\u7801\u81F3\u5C11\u9700\u89814\u4E2A\u5B57\u7B26',\n 'reset.err.mismatch':'\u4E24\u6B21\u5BC6\u7801\u4E0D\u4E00\u81F4',\n 'reset.err.fail':'\u91CD\u7F6E\u5931\u8D25',\n 'reset.back':'\\u2190 \u8FD4\u56DE\u767B\u5F55',\n 'copy.hint':'\u70B9\u51FB\u590D\u5236',\n 'copy.done':'\u5DF2\u590D\u5236\uFF01',\n 'tab.memories':'\\u{1F4DA} \u8BB0\u5FC6',\n 'tab.tasks':'\\u{1F4CB} \u4EFB\u52A1',\n 'tab.skills':'\\u{1F9E0} \u6280\u80FD',\n 'tab.analytics':'\\u{1F4CA} \u5206\u6790',\n 'skills.total':'\u6280\u80FD\u603B\u6570',\n 'skills.active':'\u751F\u6548\u4E2D',\n 'skills.installed':'\u5DF2\u5B89\u88C5',\n 'tasks.total':'\u4EFB\u52A1\u603B\u6570',\n 'tasks.active':'\u8FDB\u884C\u4E2D',\n 'tasks.completed':'\u5DF2\u5B8C\u6210',\n 'tasks.status.active':'\u8FDB\u884C\u4E2D',\n 'tasks.status.completed':'\u5DF2\u5B8C\u6210',\n 'tasks.status.skipped':'\u5DF2\u8DF3\u8FC7',\n 'tasks.empty':'\u6682\u65E0\u4EFB\u52A1\u3002\u4EFB\u52A1\u4F1A\u968F\u7740\u5BF9\u8BDD\u81EA\u52A8\u521B\u5EFA\u3002',\n 'tasks.loading':'\u52A0\u8F7D\u4E2D...',\n 'tasks.untitled':'\u672A\u547D\u540D\u4EFB\u52A1',\n 'tasks.chunks':'\u5173\u8054\u8BB0\u5FC6',\n 'tasks.nochunks':'\u6B64\u4EFB\u52A1\u6682\u65E0\u5173\u8054\u8BB0\u5FC6\u3002',\n 'tasks.skipped.default':'\u5BF9\u8BDD\u5185\u5BB9\u8FC7\u5C11\uFF0C\u672A\u751F\u6210\u6458\u8981\u3002\u8BE5\u4EFB\u52A1\u4E0D\u4F1A\u51FA\u73B0\u5728\u68C0\u7D22\u7ED3\u679C\u4E2D\u3002',\n 'refresh':'\\u21BB \u5237\u65B0',\n 'logout':'\u9000\u51FA',\n 'stat.memories':'\u8BB0\u5FC6',\n 'stat.sessions':'\u4F1A\u8BDD',\n 'stat.embeddings':'\u5D4C\u5165',\n 'stat.days':'\u5929\u6570',\n 'stat.active':'\u6D3B\u8DC3',\n 'stat.deduped':'\u5DF2\u53BB\u91CD',\n 'sidebar.sessions':'\u4F1A\u8BDD\u5217\u8868',\n 'sidebar.allsessions':'\u5168\u90E8\u4F1A\u8BDD',\n 'sidebar.clear':'\\u{1F5D1} \u6E05\u9664\u6240\u6709\u6570\u636E',\n 'search.placeholder':'\u641C\u7D22\u8BB0\u5FC6\uFF08\u652F\u6301\u8BED\u4E49\u641C\u7D22\uFF09...',\n 'search.meta.total':' \u6761\u8BB0\u5FC6',\n 'search.meta.semantic':' \u8BED\u4E49',\n 'search.meta.text':' \u6587\u672C',\n 'search.meta.results':' \u6761\u7ED3\u679C',\n 'filter.all':'\u5168\u90E8',\n 'filter.allkinds':'\u6240\u6709\u7C7B\u578B',\n 'filter.paragraph':'\u6BB5\u843D',\n 'filter.code':'\u4EE3\u7801',\n 'filter.dialog':'\u5BF9\u8BDD',\n 'filter.list':'\u5217\u8868',\n 'filter.error':'\u9519\u8BEF',\n 'filter.command':'\u547D\u4EE4',\n 'filter.newest':'\u6700\u65B0\u4F18\u5148',\n 'filter.oldest':'\u6700\u65E9\u4F18\u5148',\n 'filter.from':'\u8D77\u59CB',\n 'filter.to':'\u622A\u6B62',\n 'filter.clear':'\u6E05\u9664',\n 'empty.text':'\u6682\u65E0\u8BB0\u5FC6',\n 'card.expand':'\u5C55\u5F00',\n 'card.edit':'\u7F16\u8F91',\n 'card.delete':'\u5220\u9664',\n 'card.evolved':'\u5DF2\u6F14\u5316',\n 'card.times':'\u6B21',\n 'card.updated':'\u66F4\u65B0\u4E8E',\n 'card.evolveHistory':'\u6F14\u5316\u8BB0\u5F55',\n 'card.oldSummary':'\u65E7\u6458\u8981',\n 'card.dedupDuplicate':'\u91CD\u590D',\n 'card.dedupMerged':'\u5DF2\u5408\u5E76',\n 'card.dedupTarget':'\u5173\u8054: ',\n 'card.dedupReason':'\u539F\u56E0: ',\n 'card.newSummary':'\u65B0\u6458\u8981',\n 'pagination.total':' \u6761',\n 'range':'\u8303\u56F4',\n 'range.days':'\u5929',\n 'analytics.total':'\u603B\u8BB0\u5FC6\u6570',\n 'analytics.writes':'\u4ECA\u65E5\u5199\u5165',\n 'analytics.calls':'\u4ECA\u65E5\u67E5\u770B\u5668\u8C03\u7528',\n 'analytics.sessions':'\u4F1A\u8BDD\u6570',\n 'analytics.embeddings':'\u5D4C\u5165\u6570',\n 'chart.writes':'\u6BCF\u65E5\u8BB0\u5FC6\u5199\u5165',\n 'chart.calls':'\u6BCF\u65E5\u67E5\u770B\u5668 API \u8C03\u7528\uFF08\u5217\u8868 / \u641C\u7D22\uFF09',\n 'chart.nodata':'\u6B64\u8303\u56F4\u5185\u6682\u65E0\u6570\u636E',\n 'chart.nocalls':'\u6B64\u8303\u56F4\u5185\u6682\u65E0\u67E5\u770B\u5668\u8C03\u7528',\n 'chart.toolperf':'\u5DE5\u5177\u54CD\u5E94\u8017\u65F6',\n 'chart.list':'\u5217\u8868',\n 'chart.search':'\u641C\u7D22',\n 'breakdown.role':'\u6309\u89D2\u8272',\n 'breakdown.kind':'\u6309\u7C7B\u578B',\n 'modal.new':'\u65B0\u5EFA\u8BB0\u5FC6',\n 'modal.edit':'\u7F16\u8F91\u8BB0\u5FC6',\n 'modal.role':'\u89D2\u8272',\n 'modal.content':'\u5185\u5BB9',\n 'modal.content.ph':'\u8BB0\u5FC6\u5185\u5BB9...',\n 'modal.summary':'\u6458\u8981',\n 'modal.summary.ph':'\u7B80\u8981\u6458\u8981\uFF08\u53EF\u9009\uFF09',\n 'modal.kind':'\u7C7B\u578B',\n 'modal.cancel':'\u53D6\u6D88',\n 'modal.create':'\u521B\u5EFA',\n 'modal.save':'\u4FDD\u5B58',\n 'modal.err.empty':'\u8BF7\u8F93\u5165\u5185\u5BB9',\n 'toast.created':'\u8BB0\u5FC6\u5DF2\u521B\u5EFA',\n 'toast.updated':'\u8BB0\u5FC6\u5DF2\u66F4\u65B0',\n 'toast.deleted':'\u8BB0\u5FC6\u5DF2\u5220\u9664',\n 'toast.opfail':'\u64CD\u4F5C\u5931\u8D25',\n 'toast.delfail':'\u5220\u9664\u5931\u8D25',\n 'toast.cleared':'\u6240\u6709\u8BB0\u5FC6\u5DF2\u6E05\u9664',\n 'toast.clearfail':'\u6E05\u9664\u5931\u8D25',\n 'toast.notfound':'\u7F13\u5B58\u4E2D\u672A\u627E\u5230\u6B64\u8BB0\u5FC6',\n 'confirm.delete':'\u786E\u5B9A\u8981\u5220\u9664\u8FD9\u6761\u8BB0\u5FC6\u5417\uFF1F',\n 'confirm.clearall':'\u786E\u5B9A\u8981\u5220\u9664\u6240\u6709\u8BB0\u5FC6\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002',\n 'confirm.clearall2':'\u4F60\u771F\u7684\u786E\u5B9A\u5417\uFF1F',\n 'embed.on':'\u5D4C\u5165\u6A21\u578B\uFF1A',\n 'embed.off':'\u65E0\u5D4C\u5165\u6A21\u578B',\n 'lang.switch':'EN',\n 'tab.logs':'\uD83D\uDCDD \u65E5\u5FD7',\n 'logs.allTools':'\u5168\u90E8\u5DE5\u5177',\n 'logs.refresh':'\u5237\u65B0',\n 'logs.autoRefresh':'\u81EA\u52A8\u5237\u65B0',\n 'logs.input':'\u8F93\u5165',\n 'logs.output':'\u8F93\u51FA',\n 'logs.empty':'\u6682\u65E0\u65E5\u5FD7\u3002\u5F53\u5DE5\u5177\u88AB\u8C03\u7528\u65F6\u65E5\u5FD7\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\u3002',\n 'logs.ago':'\u524D',\n 'tab.import':'\uD83D\uDCE5 \u5BFC\u5165',\n 'tab.settings':'\u2699 \u8BBE\u7F6E',\n 'settings.modelconfig':'\u6A21\u578B\u914D\u7F6E',\n 'settings.embedding':'\u5D4C\u5165\u6A21\u578B',\n 'settings.summarizer':'\u6458\u8981\u6A21\u578B',\n 'settings.skill':'\u6280\u80FD\u8FDB\u5316',\n 'settings.general':'\u901A\u7528\u8BBE\u7F6E',\n 'settings.provider':'\u670D\u52A1\u5546',\n 'settings.model':'\u6A21\u578B',\n 'settings.temperature':'\u6E29\u5EA6',\n 'settings.skill.enabled':'\u542F\u7528\u6280\u80FD\u8FDB\u5316',\n 'settings.skill.autoinstall':'\u81EA\u52A8\u5B89\u88C5\u6280\u80FD',\n 'settings.skill.confidence':'\u6700\u4F4E\u7F6E\u4FE1\u5EA6',\n 'settings.skill.minchunks':'\u6700\u5C11\u8BB0\u5FC6\u7247\u6BB5',\n 'settings.skill.model':'\u6280\u80FD\u4E13\u7528\u6A21\u578B',\n 'settings.skill.model.hint':'\u4E0D\u914D\u7F6E\u65F6\u9ED8\u8BA4\u4F7F\u7528\u4E0A\u65B9\u7684\u6458\u8981\u6A21\u578B\u8FDB\u884C\u6280\u80FD\u751F\u6210\u3002\u5982\u9700\u66F4\u9AD8\u8D28\u91CF\u7684\u6280\u80FD\u8F93\u51FA\uFF0C\u53EF\u5728\u6B64\u5355\u72EC\u914D\u7F6E\u4E00\u4E2A\u66F4\u5F3A\u7684\u6A21\u578B\u3002',\n 'settings.optional':'\u53EF\u9009',\n 'settings.skill.usemain':'\u4F7F\u7528\u4E3B\u6458\u8981\u6A21\u578B',\n 'settings.telemetry':'\u6570\u636E\u7EDF\u8BA1',\n 'settings.telemetry.enabled':'\u542F\u7528\u533F\u540D\u6570\u636E\u7EDF\u8BA1',\n 'settings.telemetry.hint':'\u533F\u540D\u4F7F\u7528\u7EDF\u8BA1\uFF0C\u5E2E\u52A9\u6539\u8FDB\u63D2\u4EF6\u3002\u4EC5\u53D1\u9001\u5DE5\u5177\u540D\u79F0\u3001\u54CD\u5E94\u65F6\u95F4\u548C\u7248\u672C\u4FE1\u606F\uFF0C\u4E0D\u4F1A\u53D1\u9001\u4EFB\u4F55\u8BB0\u5FC6\u5185\u5BB9\u3001\u641C\u7D22\u67E5\u8BE2\u6216\u4E2A\u4EBA\u6570\u636E\u3002',\n 'settings.viewerport':'Viewer \u7AEF\u53E3',\n 'settings.viewerport.hint':'\u4FEE\u6539\u540E\u9700\u91CD\u542F\u7F51\u5173\u751F\u6548',\n 'settings.save':'\u4FDD\u5B58\u8BBE\u7F6E',\n 'settings.reset':'\u91CD\u7F6E',\n 'settings.saved':'\u5DF2\u4FDD\u5B58',\n 'settings.restart.hint':'\u90E8\u5206\u8BBE\u7F6E\u4FEE\u6539\u540E\u9700\u8981\u91CD\u542F OpenClaw \u7F51\u5173\u624D\u80FD\u751F\u6548\u3002',\n 'settings.save.fail':'\u4FDD\u5B58\u8BBE\u7F6E\u5931\u8D25',\n 'migrate.title':'\u5BFC\u5165 OpenClaw \u8BB0\u5FC6',\n 'migrate.desc':'\u5C06 OpenClaw \u5185\u7F6E\u7684\u8BB0\u5FC6\u6570\u636E\u548C\u5BF9\u8BDD\u5386\u53F2\u8FC1\u79FB\u5230\u672C\u63D2\u4EF6\u4E2D\u3002\u5BFC\u5165\u8FC7\u7A0B\u4F7F\u7528\u667A\u80FD\u53BB\u91CD\uFF0C\u907F\u514D\u91CD\u590D\u5BFC\u5165\u3002',\n 'migrate.modes.title':'\u4E09\u79CD\u4F7F\u7528\u65B9\u5F0F\uFF1A',\n 'migrate.mode1.label':'\u2460 \u4EC5\u5BFC\u5165\u8BB0\u5FC6\uFF08\u5FEB\u901F\uFF09',\n 'migrate.mode1.desc':'\u2014\u2014\u70B9\u51FB\u300C\u5F00\u59CB\u5BFC\u5165\u300D\u5373\u53EF\u5FEB\u901F\u8FC1\u79FB\u6240\u6709\u8BB0\u5FC6\u7247\u6BB5\u548C\u5BF9\u8BDD\u5386\u53F2\uFF0C\u4E0D\u8FDB\u884C\u4EFB\u52A1/\u6280\u80FD\u751F\u6210\u3002\u9002\u5408\u53EA\u9700\u8981\u539F\u59CB\u6570\u636E\u7684\u573A\u666F\u3002',\n 'migrate.mode2.label':'\u2461 \u5BFC\u5165 + \u751F\u6210\u4EFB\u52A1\u4E0E\u6280\u80FD\uFF08\u8F83\u6162\uFF0C\u4E32\u884C\uFF09',\n 'migrate.mode2.desc':'\u2014\u2014\u5BFC\u5165\u8BB0\u5FC6\u540E\uFF0C\u5728\u4E0B\u65B9\u52FE\u9009\u300C\u751F\u6210\u4EFB\u52A1\u6458\u8981\u300D\u548C/\u6216\u300C\u89E6\u53D1\u6280\u80FD\u8FDB\u5316\u300D\uFF0C\u7CFB\u7EDF\u4F1A\u9010\u4E2A\u4F1A\u8BDD\u5206\u6790\u3002\u7531\u4E8E\u6BCF\u4E2A\u4F1A\u8BDD\u90FD\u9700\u8981 LLM \u5904\u7406\uFF0C\u8017\u65F6\u8F83\u957F\u3002',\n 'migrate.mode3.label':'\u2462 \u5148\u5BFC\u5165\uFF0C\u968F\u65F6\u518D\u751F\u6210\uFF08\u7075\u6D3B\uFF09',\n 'migrate.mode3.desc':'\u2014\u2014\u5148\u5BFC\u5165\u8BB0\u5FC6\uFF0C\u4E4B\u540E\u968F\u65F6\u53EF\u4EE5\u56DE\u6765\u5F00\u542F\u4EFB\u52A1/\u6280\u80FD\u751F\u6210\u3002\u751F\u6210\u8FC7\u7A0B\u53EF\u4EE5\u968F\u65F6\u6682\u505C\uFF0C\u4E0B\u6B21\u7EE7\u7EED\u65F6\u4F1A\u4ECE\u4E0A\u6B21\u505C\u4E0B\u7684\u5730\u65B9\u63A5\u7740\u5904\u7406\uFF0C\u5DF2\u5904\u7406\u7684\u4F1A\u8BDD\u4F1A\u81EA\u52A8\u8DF3\u8FC7\u3002',\n 'migrate.config.warn':'\u9700\u8981\u914D\u7F6E',\n 'migrate.config.warn.desc':'\u8BF7\u5148\u5728\u4E0A\u65B9\u914D\u7F6E\u597D Embedding \u6A21\u578B\u548C Summarizer \u6A21\u578B\uFF0C\u8FD9\u4E24\u9879\u662F\u5904\u7406\u8BB0\u5FC6\u6240\u5FC5\u9700\u7684\u3002',\n 'migrate.sqlite.label':'\u8BB0\u5FC6\u7D22\u5F15 (SQLite)',\n 'migrate.sessions.label':'\u5BF9\u8BDD\u5386\u53F2',\n 'migrate.scan':'\u626B\u63CF\u6570\u636E\u6E90',\n 'migrate.start':'\u5F00\u59CB\u5BFC\u5165',\n 'migrate.scanning':'\u626B\u63CF\u4E2D...',\n 'migrate.stat.stored':'\u5DF2\u5B58\u50A8',\n 'migrate.stat.skipped':'\u5DF2\u8DF3\u8FC7',\n 'migrate.stat.merged':'\u5DF2\u5408\u5E76',\n 'migrate.stat.errors':'\u9519\u8BEF',\n 'migrate.phase.sqlite':'\u6B63\u5728\u5BFC\u5165\u8BB0\u5FC6\u7D22\u5F15...',\n 'migrate.phase.sessions':'\u6B63\u5728\u5BFC\u5165\u5BF9\u8BDD\u5386\u53F2...',\n 'migrate.chunks':'\u6761\u8BB0\u5FC6',\n 'migrate.sessions.count':'\u4E2A\u4F1A\u8BDD\uFF0C{n} \u6761\u6D88\u606F',\n 'migrate.nodata':'\u672A\u627E\u5230\u53EF\u5BFC\u5165\u7684 OpenClaw \u6570\u636E\u3002',\n 'migrate.running':'\u5BFC\u5165\u8FDB\u884C\u4E2D...',\n 'migrate.error.running':'\u5DF2\u6709\u8FC1\u79FB\u4EFB\u52A1\u6B63\u5728\u8FDB\u884C\u3002',\n 'migrate.stop':'\\u25A0 \u505C\u6B62',\n 'migrate.stopping':'\u6B63\u5728\u505C\u6B62...',\n 'migrate.resume':'\u7EE7\u7EED\u5BFC\u5165',\n 'pp.title':'\\u{1F9E0} \u53EF\u9009\uFF1A\u751F\u6210\u4EFB\u52A1\u4E0E\u6280\u80FD',\n 'pp.desc':'\u6B64\u6B65\u9AA4\u5B8C\u5168\u53EF\u9009\u3002\u4E0A\u9762\u7684\u5BFC\u5165\u5DF2\u7ECF\u5B58\u50A8\u4E86\u539F\u59CB\u8BB0\u5FC6\u6570\u636E\u3002\u5728\u8FD9\u91CC\u53EF\u4EE5\u8FDB\u4E00\u6B65\u5206\u6790\u5DF2\u5BFC\u5165\u7684\u5BF9\u8BDD\uFF0C\u751F\u6210\u7ED3\u6784\u5316\u7684\u4EFB\u52A1\u6458\u8981\u6216\u8FDB\u5316\u53EF\u590D\u7528\u7684\u6280\u80FD\u3002\u5904\u7406\u8FC7\u7A0B\u662F\u4E32\u884C\u7684\uFF08\u9010\u4E2A\u4F1A\u8BDD\uFF09\uFF0C\u53EF\u80FD\u9700\u8981\u8F83\u957F\u65F6\u95F4\u3002\u4F60\u53EF\u4EE5\u968F\u65F6\u505C\u6B62\uFF0C\u4E0B\u6B21\u7EE7\u7EED\u65F6\u53EA\u4F1A\u5904\u7406\u5C1A\u672A\u5B8C\u6210\u7684\u4F1A\u8BDD\u3002',\n 'pp.tasks.label':'\u751F\u6210\u4EFB\u52A1\u6458\u8981',\n 'pp.tasks.hint':'\u5C06\u5BFC\u5165\u7684\u6D88\u606F\u6309\u4EFB\u52A1\u5206\u7EC4\uFF0C\u4E3A\u6BCF\u4E2A\u4EFB\u52A1\u751F\u6210\u7ED3\u6784\u5316\u6458\u8981\uFF08\u6807\u9898\u3001\u76EE\u6807\u3001\u6B65\u9AA4\u3001\u7ED3\u679C\uFF09\uFF0C\u65B9\u4FBF\u65E5\u540E\u641C\u7D22\u548C\u56DE\u5FC6\u3002',\n 'pp.skills.label':'\u89E6\u53D1\u6280\u80FD\u8FDB\u5316',\n 'pp.skills.hint':'\u5206\u6790\u5DF2\u5B8C\u6210\u7684\u4EFB\u52A1\uFF0C\u81EA\u52A8\u521B\u5EFA\u6216\u5347\u7EA7\u53EF\u590D\u7528\u7684\u6280\u80FD\uFF08SKILL.md\uFF09\u3002\u9700\u8981\u5148\u542F\u7528\u4EFB\u52A1\u6458\u8981\u3002\u7531\u4E8E\u9700\u8981 LLM \u8BC4\u4F30\uFF0C\u8017\u65F6\u8F83\u957F\u3002',\n 'pp.start':'\u5F00\u59CB\u5904\u7406',\n 'pp.resume':'\u7EE7\u7EED\u5904\u7406',\n 'pp.running':'\u6B63\u5728\u5904\u7406',\n 'pp.stopped':'\u5904\u7406\u5DF2\u505C\u6B62\uFF0C\u4F60\u53EF\u4EE5\u968F\u65F6\u7EE7\u7EED\u3002',\n 'pp.failed':'\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u4E0A\u65B9\u7684\u9519\u8BEF\u63D0\u793A\u3002',\n 'pp.done':'\u4EFB\u52A1\u4E0E\u6280\u80FD\u751F\u6210\u5B8C\u6210\uFF01',\n 'pp.select.warn':'\u8BF7\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A\u9009\u9879\u3002',\n 'pp.skill.created':'\u6280\u80FD\u5DF2\u521B\u5EFA',\n 'pp.stat.tasks':'\u4EFB\u52A1',\n 'pp.stat.skills':'\u6280\u80FD',\n 'pp.stat.errors':'\u9519\u8BEF',\n 'pp.stat.skipped':'\u5DF2\u8DF3\u8FC7',\n 'pp.info.skipped':'\u5DF2\u6709 {n} \u4E2A\u4F1A\u8BDD\u5904\u7406\u8FC7\uFF0C\u81EA\u52A8\u8DF3\u8FC7\u3002',\n 'pp.info.pending':'\u6B63\u5728\u5904\u7406 {n} \u4E2A\u4F1A\u8BDD...',\n 'pp.info.allDone':'\u6240\u6709\u4F1A\u8BDD\u5747\u5DF2\u5904\u7406\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u5904\u7406\u3002',\n 'pp.action.full':'\u4EFB\u52A1+\u6280\u80FD',\n 'pp.action.skillOnly':'\u4EC5\u6280\u80FD\uFF08\u4EFB\u52A1\u5DF2\u5B58\u5728\uFF09',\n 'card.imported':'OpenClaw \u539F\u751F\u8BB0\u5FC6',\n 'skills.draft':'\u8349\u7A3F',\n 'skills.filter.active':'\u751F\u6548\u4E2D',\n 'skills.filter.draft':'\u8349\u7A3F',\n 'skills.filter.archived':'\u5DF2\u5F52\u6863',\n 'skills.files':'\u6280\u80FD\u6587\u4EF6',\n 'skills.content':'SKILL.md \u5185\u5BB9',\n 'skills.versions':'\u7248\u672C\u5386\u53F2',\n 'skills.related':'\u5173\u8054\u4EFB\u52A1',\n 'skills.download':'\u2B07 \u4E0B\u8F7D',\n 'skills.installed.badge':'\u5DF2\u5B89\u88C5',\n 'skills.empty':'\u6682\u65E0\u6280\u80FD\u3002\u6280\u80FD\u4F1A\u4ECE\u5DF2\u5B8C\u6210\u7684\u3001\u5305\u542B\u53EF\u590D\u7528\u7ECF\u9A8C\u7684\u4EFB\u52A1\u4E2D\u81EA\u52A8\u751F\u6210\u3002',\n 'skills.loading':'\u52A0\u8F7D\u4E2D...',\n 'skills.error':'\u52A0\u8F7D\u6280\u80FD\u5931\u8D25',\n 'skills.error.detail':'\u52A0\u8F7D\u6280\u80FD\u5931\u8D25\uFF1A',\n 'skills.nofiles':'\u6682\u65E0\u6587\u4EF6',\n 'skills.noversions':'\u6682\u65E0\u7248\u672C\u8BB0\u5F55',\n 'skills.norelated':'\u6682\u65E0\u5173\u8054\u4EFB\u52A1',\n 'skills.nocontent':'\u6682\u65E0\u5185\u5BB9',\n 'skills.nochangelog':'\u6682\u65E0\u53D8\u66F4\u8BB0\u5F55',\n 'skills.status.active':'\u751F\u6548\u4E2D',\n 'skills.status.draft':'\u8349\u7A3F',\n 'skills.status.archived':'\u5DF2\u5F52\u6863',\n 'skills.updated':'\u66F4\u65B0\u4E8E\uFF1A',\n 'skills.task.prefix':'\u4EFB\u52A1\uFF1A',\n 'tasks.chunks.label':'\u6761\u8BB0\u5FC6',\n 'tasks.taskid':'\u4EFB\u52A1 ID\uFF1A',\n 'tasks.role.user':'\u4F60',\n 'tasks.role.assistant':'\u52A9\u624B',\n 'tasks.error':'\u51FA\u9519\u4E86',\n 'tasks.error.detail':'\u52A0\u8F7D\u4EFB\u52A1\u8BE6\u60C5\u5931\u8D25',\n 'tasks.untitled.related':'\u672A\u547D\u540D'\n }\n};\nconst LANG_KEY='memos-viewer-lang';\nlet curLang=localStorage.getItem(LANG_KEY)||(navigator.language.startsWith('zh')?'zh':'en');\nfunction t(key){return (I18N[curLang]||I18N.en)[key]||key;}\nfunction setLang(lang){curLang=lang;localStorage.setItem(LANG_KEY,lang);applyI18n();}\nfunction toggleLang(){setLang(curLang==='zh'?'en':'zh');}\n\nfunction applyI18n(){\n document.querySelectorAll('[data-i18n]').forEach(el=>{\n const key=el.getAttribute('data-i18n');\n if(key) el.textContent=t(key);\n });\n document.querySelectorAll('[data-i18n-ph]').forEach(el=>{\n const key=el.getAttribute('data-i18n-ph');\n if(key) el.placeholder=t(key);\n });\n const step2=document.getElementById('resetStep2Desc');\n if(step2) step2.innerHTML=t('reset.step2.desc.pre')+'<span style=\"font-family:monospace;font-size:12px;color:var(--pri)\">password reset token: <strong>a1b2c3d4e5f6...</strong></span>'+t('reset.step2.desc.post');\n document.title=t('title')+' - MemOS';\n if(typeof loadStats==='function' && document.getElementById('app').style.display==='flex'){loadStats();}\n if(document.querySelector('.analytics-view.show') && typeof loadMetrics==='function'){loadMetrics();}\n}\n\n/* \u2500\u2500\u2500 Auth flow \u2500\u2500\u2500 */\nasync function checkAuth(){\n const r=await fetch('/api/auth/status');\n const d=await r.json();\n if(d.needsSetup){\n document.getElementById('setupScreen').style.display='flex';\n document.getElementById('setupPw').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('setupPw2').focus()});\n document.getElementById('setupPw2').addEventListener('keydown',e=>{if(e.key==='Enter')doSetup()});\n } else if(!d.loggedIn){\n document.getElementById('loginScreen').style.display='flex';\n document.getElementById('loginPw').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});\n } else {\n enterApp();\n }\n}\n\nasync function doSetup(){\n const pw=document.getElementById('setupPw').value;\n const pw2=document.getElementById('setupPw2').value;\n const err=document.getElementById('setupErr');\n if(pw.length<4){err.textContent=t('setup.err.short');return}\n if(pw!==pw2){err.textContent=t('setup.err.mismatch');return}\n const r=await fetch('/api/auth/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('setupScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||t('setup.err.fail')}\n}\n\nasync function doLogin(){\n const pw=document.getElementById('loginPw').value;\n const err=document.getElementById('loginErr');\n const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent=t('login.err');document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}\n}\n\nasync function doLogout(){\n await fetch('/api/auth/logout',{method:'POST'});\n location.reload();\n}\n\nfunction showResetForm(){\n document.getElementById('loginForm').style.display='none';\n document.getElementById('resetForm').style.display='block';\n document.getElementById('resetToken').focus();\n}\n\nfunction showLoginForm(){\n document.getElementById('resetForm').style.display='none';\n document.getElementById('loginForm').style.display='block';\n document.getElementById('loginPw').focus();\n}\n\nfunction copyCmd(el){\n const code=el.querySelector('code').textContent;\n navigator.clipboard.writeText(code).then(()=>{\n el.classList.add('copied');\n el.querySelector('.copy-hint').textContent=t('copy.done');\n setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent=t('copy.hint')},2000);\n });\n}\n\nasync function doReset(){\n const token=document.getElementById('resetToken').value.trim();\n const pw=document.getElementById('resetNewPw').value;\n const pw2=document.getElementById('resetNewPw2').value;\n const err=document.getElementById('resetErr');\n if(!token){err.textContent=t('reset.err.token');return}\n if(pw.length<4){err.textContent=t('reset.err.short');return}\n if(pw!==pw2){err.textContent=t('reset.err.mismatch');return}\n const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,newPassword:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||t('reset.err.fail')}\n}\n\nfunction enterApp(){\n document.getElementById('app').style.display='flex';\n loadAll();\n}\n\nfunction switchView(view){\n document.querySelectorAll('.nav-tabs .tab').forEach(t=>t.classList.toggle('active',t.dataset.view===view));\n const feedWrap=document.getElementById('feedWrap');\n const analyticsView=document.getElementById('analyticsView');\n const tasksView=document.getElementById('tasksView');\n const skillsView=document.getElementById('skillsView');\n const logsView=document.getElementById('logsView');\n const settingsView=document.getElementById('settingsView');\n const migrateView=document.getElementById('migrateView');\n feedWrap.classList.add('hide');\n analyticsView.classList.remove('show');\n tasksView.classList.remove('show');\n skillsView.classList.remove('show');\n logsView.classList.remove('show');\n settingsView.classList.remove('show');\n migrateView.classList.remove('show');\n if(view==='analytics'){\n analyticsView.classList.add('show');\n loadMetrics();\n } else if(view==='tasks'){\n tasksView.classList.add('show');\n loadTasks();\n } else if(view==='skills'){\n skillsView.classList.add('show');\n loadSkills();\n } else if(view==='logs'){\n logsView.classList.add('show');\n loadLogs();\n } else if(view==='settings'){\n settingsView.classList.add('show');\n loadConfig();\n } else if(view==='import'){\n migrateView.classList.add('show');\n if(!window._migrateRunning) migrateScan();\n } else {\n feedWrap.classList.remove('hide');\n }\n}\n\n// \u2500\u2500\u2500 Logs \u2500\u2500\u2500\nlet logAutoTimer=null;\nlet logPage=1;\nconst LOG_PAGE_SIZE=20;\nasync function loadLogs(page){\n if(typeof page==='number') logPage=page;\n try{\n const toolFilter=document.getElementById('logToolFilter').value;\n const offset=(logPage-1)*LOG_PAGE_SIZE;\n const url='/api/logs?limit='+LOG_PAGE_SIZE+'&offset='+offset+(toolFilter?'&tool='+encodeURIComponent(toolFilter):'');\n const [logsRes,toolsRes]=await Promise.all([fetch(url),fetch('/api/log-tools')]);\n if(!logsRes.ok) return;\n const logsData=await logsRes.json();\n const toolsData=await toolsRes.json();\n renderLogToolFilter(toolsData.tools||[],toolFilter);\n renderLogs(logsData.logs||[]);\n renderLogPagination(logsData.page||1,logsData.totalPages||1,logsData.total||0);\n startLogAutoRefresh();\n }catch(e){console.error('loadLogs',e)}\n}\nfunction onLogFilterChange(){logPage=1;loadLogs(1);}\nfunction renderLogPagination(page,totalPages,total){\n const el=document.getElementById('logsPagination');\n if(!el||totalPages<=1){if(el)el.innerHTML='';return;}\n const pages=[];\n const range=2;\n for(let i=1;i<=totalPages;i++){\n if(i===1||i===totalPages||Math.abs(i-page)<=range){\n pages.push(i);\n }else if(pages[pages.length-1]!=='...'){\n pages.push('...');\n }\n }\n let html='<div class=\"logs-pagination\">';\n html+='<button class=\"btn btn-sm btn-ghost\" '+(page<=1?'disabled':'')+' onclick=\"loadLogs('+(page-1)+')\">\u2039</button>';\n pages.forEach(p=>{\n if(p==='...'){html+='<span class=\"page-ellipsis\">\u2026</span>';}\n else{html+='<button class=\"btn btn-sm '+(p===page?'btn-primary':'btn-ghost')+'\" onclick=\"loadLogs('+p+')\">'+p+'</button>';}\n });\n html+='<button class=\"btn btn-sm btn-ghost\" '+(page>=totalPages?'disabled':'')+' onclick=\"loadLogs('+(page+1)+')\">\u203A</button>';\n html+='<span class=\"page-total\">'+total+' total</span>';\n html+='</div>';\n el.innerHTML=html;\n}\n\nfunction renderLogToolFilter(tools,current){\n const sel=document.getElementById('logToolFilter');\n const opts=['<option value=\"\">'+t('logs.allTools')+'</option>'];\n tools.forEach(tn=>{\n opts.push('<option value=\"'+tn+'\"'+(tn===current?' selected':'')+'>'+tn+'</option>');\n });\n sel.innerHTML=opts.join('');\n}\n\nfunction formatLogTime(ts){\n const d=new Date(ts);\n const time=d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false});\n const y=d.getFullYear();\n const m=String(d.getMonth()+1).padStart(2,'0');\n const day=String(d.getDate()).padStart(2,'0');\n return y+'-'+m+'-'+day+' '+time;\n}\n\nfunction buildLogSummary(lg){\n let inputObj=null;\n try{inputObj=JSON.parse(lg.input);}catch(_){}\n let html='';\n const tn=lg.toolName;\n if(tn==='memory_search'&&inputObj){\n const q=inputObj.query||'';\n if(q) html+='<div class=\"log-summary-query\">'+escapeHtml(q.length>200?q.slice(0,200)+'...':q)+'</div>';\n const outLines=(lg.output||'').split('\\n');\n const memCount=outLines.filter(l=>l.match(/^\\d+\\.\\s*\\[/)).length;\n if(memCount>0) html+='<div style=\"margin-top:4px;font-size:11px;color:var(--text-sec)\">\uD83D\uDCCE '+memCount+' memories retrieved</div>';\n 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>';\n }else if(tn==='memory_add'&&inputObj){\n const out=lg.output||'';\n const statsMatch=out.match(/^([^\\n]+)/);\n if(statsMatch){\n html+='<div class=\"log-summary-stats\">';\n const pairs=statsMatch[1].split(',').map(s=>s.trim());\n pairs.forEach(p=>{\n const m=p.match(/^(\\w+)=(\\d+)/);\n if(m){html+='<span class=\"log-stat-chip '+m[1]+'\">'+m[1]+' '+m[2]+'</span>';}\n });\n html+='</div>';\n }\n const outLines=out.split('\\n').filter(l=>l.startsWith('['));\n if(outLines.length>0){\n html+='<div class=\"log-msg-list\">';\n outLines.forEach(function(l){\n var rm=l.match(/^\\[(\\w+)\\]\\s*([^\u2192]+)\u2192\\s*(.*)/);\n if(rm){\n var role=rm[1],actionRaw=rm[2].trim(),text=rm[3].trim();\n var actionCls='stored';\n if(actionRaw.indexOf('exact-dup')>=0||actionRaw.indexOf('\u23ED')>=0) actionCls='exact-dup';\n else if(actionRaw.indexOf('dedup')>=0||actionRaw.indexOf('\uD83D\uDD01')>=0) actionCls='dedup';\n else if(actionRaw.indexOf('merged')>=0||actionRaw.indexOf('\uD83D\uDD00')>=0) actionCls='merged';\n else if(actionRaw.indexOf('error')>=0||actionRaw.indexOf('\u274C')>=0) actionCls='error';\n var actionLabel={'stored':'\u2713 stored','exact-dup':'\u23ED skip','dedup':'\uD83D\uDD01 dedup','merged':'\uD83D\uDD00 merged','error':'\u2717 error'}[actionCls]||actionCls;\n html+='<div class=\"log-msg-item\">'+\n '<span class=\"log-msg-role '+role+'\">'+role+'</span>'+\n '<span class=\"log-msg-action '+actionCls+'\">'+actionLabel+'</span>'+\n '<span class=\"log-msg-text\">'+escapeHtml(text.length>150?text.slice(0,150)+'...':text)+'</span>'+\n '</div>';\n }else{\n html+='<div class=\"log-msg-item\"><span class=\"log-msg-text\">'+escapeHtml(l.length>200?l.slice(0,200)+'...':l)+'</span></div>';\n }\n });\n html+='</div>';\n }else if(inputObj.details&&Array.isArray(inputObj.details)&&inputObj.details.length>0){\n html+='<div class=\"log-msg-list\">';\n inputObj.details.forEach(function(d){\n var s=typeof d==='string'?d:String(d);\n var dm=s.match(/^\\[(\\w+)\\]\\s*(.*)/);\n if(dm){\n 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>';\n }else{\n html+='<div class=\"log-msg-item\"><span class=\"log-msg-text\">'+escapeHtml(s.length>150?s.slice(0,150)+'...':s)+'</span></div>';\n }\n });\n html+='</div>';\n }\n }else if(inputObj){\n const keys=Object.keys(inputObj);\n keys.slice(0,4).forEach(k=>{\n const v=String(inputObj[k]);\n 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>';\n });\n }\n return html;\n}\nfunction renderLogs(logs){\n const el=document.getElementById('logsList');\n if(!logs.length){\n el.innerHTML='<div style=\"text-align:center;padding:60px 20px;color:var(--text-sec)\">'+\n '<div style=\"font-size:32px;margin-bottom:12px;opacity:.5\">\uD83D\uDCCB</div>'+\n '<div style=\"font-size:13px\">'+t('logs.empty')+'</div></div>';\n return;\n }\n el.innerHTML=logs.map((lg,i)=>{\n const toolCls=lg.toolName.replace(/[^a-zA-Z0-9_]/g,'_');\n const dur=lg.durationMs<1000?Math.round(lg.durationMs)+'ms':(lg.durationMs/1000).toFixed(1)+'s';\n let inputDisplay='';\n try{const parsed=JSON.parse(lg.input);inputDisplay=JSON.stringify(parsed,null,2);}catch(_){inputDisplay=lg.input;}\n const summary=buildLogSummary(lg);\n return '<div class=\"log-entry\" id=\"log-'+i+'\">'+\n '<div class=\"log-header\" onclick=\"toggleLog('+i+')\">'+\n '<span class=\"log-status '+(lg.success?'ok':'fail')+'\"></span>'+\n '<span class=\"log-tool-badge '+toolCls+'\">'+lg.toolName+'</span>'+\n '<span class=\"log-dur\">'+dur+'</span>'+\n '<span class=\"log-expand-btn\" style=\"margin-left:4px\">\u25BC</span>'+\n '<span class=\"log-time\">'+formatLogTime(lg.calledAt)+'</span>'+\n '</div>'+\n (summary?'<div class=\"log-summary\">'+summary+'</div>':'')+\n '<div class=\"log-detail\" id=\"log-detail-'+i+'\">'+\n '<div class=\"log-io-section\">'+\n '<div class=\"log-io-label\">\u25B6 '+t('logs.input')+'</div>'+\n '<pre class=\"log-io-content\">'+escapeHtml(inputDisplay)+'</pre>'+\n '</div>'+\n '<div class=\"log-io-section\">'+\n '<div class=\"log-io-label\">\u25C0 '+t('logs.output')+'</div>'+\n '<pre class=\"log-io-content\">'+escapeHtml(lg.output)+'</pre>'+\n '</div>'+\n '</div>'+\n '</div>';\n }).join('');\n}\n\nfunction toggleLog(i){\n const entry=document.getElementById('log-'+i);\n const d=document.getElementById('log-detail-'+i);\n if(d) d.classList.toggle('open');\n if(entry) entry.classList.toggle('expanded');\n}\n\nfunction startLogAutoRefresh(){\n if(logAutoTimer) clearInterval(logAutoTimer);\n logAutoTimer=setInterval(()=>{\n const cb=document.getElementById('logAutoRefresh');\n const logsView=document.getElementById('logsView');\n if(cb&&cb.checked&&logsView&&logsView.classList.contains('show')){\n loadLogs();\n }\n },5000);\n}\n\nfunction escapeHtml(s){\n return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n}\n\nfunction setMetricsDays(d){\n metricsDays=d;\n document.querySelectorAll('.metrics-toolbar .range-btn').forEach(btn=>btn.classList.toggle('active',Number(btn.dataset.days)===d));\n loadMetrics();\n}\n\nasync function loadMetrics(){\n const r=await fetch('/api/metrics?days='+metricsDays);\n const d=await r.json();\n document.getElementById('mTotal').textContent=formatNum(d.totals.memories);\n document.getElementById('mTodayWrites').textContent=formatNum(d.totals.todayWrites);\n document.getElementById('mSessions').textContent=formatNum(d.totals.sessions);\n document.getElementById('mEmbeddings').textContent=formatNum(d.totals.embeddings);\n renderChartWrites(d.writesPerDay);\n renderBreakdown(d.roleBreakdown,'breakdownRole');\n renderBreakdown(d.kindBreakdown,'breakdownKind');\n loadToolMetrics();\n}\n\nfunction formatNum(n){return n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'k':String(n);}\n\n/* \u2500\u2500\u2500 Tasks View Logic \u2500\u2500\u2500 */\nlet tasksStatusFilter='';\nlet tasksPage=0;\nconst TASKS_PER_PAGE=20;\n\nfunction setTaskStatusFilter(btn,status){\n document.querySelectorAll('.tasks-filters .filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n tasksStatusFilter=status;\n tasksPage=0;\n loadTasks();\n}\n\nasync function loadTasks(){\n const list=document.getElementById('tasksList');\n list.innerHTML='<div class=\"spinner\"></div>';\n try{\n const params=new URLSearchParams({limit:String(TASKS_PER_PAGE),offset:String(tasksPage*TASKS_PER_PAGE)});\n if(tasksStatusFilter) params.set('status',tasksStatusFilter);\n const r=await fetch('/api/tasks?'+params);\n const data=await r.json();\n\n // stats\n const allR=await fetch('/api/tasks?limit=1&offset=0');\n const allD=await allR.json();\n document.getElementById('tasksTotalCount').textContent=formatNum(allD.total);\n\n const activeR=await fetch('/api/tasks?status=active&limit=1&offset=0');\n const activeD=await activeR.json();\n document.getElementById('tasksActiveCount').textContent=formatNum(activeD.total);\n\n const compR=await fetch('/api/tasks?status=completed&limit=1&offset=0');\n const compD=await compR.json();\n document.getElementById('tasksCompletedCount').textContent=formatNum(compD.total);\n\n const skipR=await fetch('/api/tasks?status=skipped&limit=1&offset=0');\n const skipD=await skipR.json();\n document.getElementById('tasksSkippedCount').textContent=formatNum(skipD.total);\n\n if(!data.tasks||data.tasks.length===0){\n list.innerHTML='<div style=\"text-align:center;padding:48px;color:var(--text-muted);font-size:14px\" data-i18n=\"tasks.empty\">'+t('tasks.empty')+'</div>';\n document.getElementById('tasksPagination').innerHTML='';\n return;\n }\n\n list.innerHTML=data.tasks.map(task=>{\n const timeStr=formatTime(task.startedAt);\n const endStr=task.endedAt?formatTime(task.endedAt):'';\n const durationStr=task.endedAt?formatDuration(task.endedAt-task.startedAt):'';\n return '<div class=\"task-card status-'+task.status+'\" onclick=\"openTaskDetail(\\''+task.id+'\\')\">'+\n '<div class=\"task-card-top\">'+\n '<div class=\"task-card-title\">'+esc(task.title)+'</div>'+\n '<span class=\"task-status-badge '+task.status+'\">'+t('tasks.status.'+task.status)+'</span>'+\n '</div>'+\n (task.summary?'<div class=\"task-card-summary'+(task.status==='skipped'?' skipped-reason':'')+'\">'+esc(task.summary)+'</div>':'')+\n '<div class=\"task-card-bottom\">'+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4C5}</span> '+timeStr+'</span>'+\n (durationStr?'<span class=\"tag\"><span class=\"icon\">\\u23F1</span> '+durationStr+'</span>':'')+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4DD}</span> '+task.chunkCount+' '+t('tasks.chunks.label')+'</span>'+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4C2}</span> '+(task.sessionKey||'').slice(0,12)+'</span>'+\n '</div>'+\n '</div>';\n }).join('');\n\n renderTasksPagination(data.total);\n }catch(e){\n console.error('loadTasks error:',e);\n list.innerHTML='<div style=\"text-align:center;padding:24px;color:var(--rose)\">Failed to load tasks: '+String(e)+'</div>';\n }\n}\n\nfunction renderTasksPagination(total){\n const el=document.getElementById('tasksPagination');\n const pages=Math.ceil(total/TASKS_PER_PAGE);\n if(pages<=1){el.innerHTML='';return;}\n let html='<button class=\"pg-btn'+(tasksPage===0?' disabled':'')+'\" onclick=\"tasksPage=Math.max(0,tasksPage-1);loadTasks()\">\\u2190</button>';\n const start=Math.max(0,tasksPage-2),end=Math.min(pages,tasksPage+3);\n for(let i=start;i<end;i++){\n html+='<button class=\"pg-btn'+(i===tasksPage?' active':'')+'\" onclick=\"tasksPage='+i+';loadTasks()\">'+(i+1)+'</button>';\n }\n html+='<button class=\"pg-btn'+(tasksPage>=pages-1?' disabled':'')+'\" onclick=\"tasksPage=Math.min('+(pages-1)+',tasksPage+1);loadTasks()\">\\u2192</button>';\n html+='<span class=\"pg-info\">'+total+' '+t('pagination.total')+'</span>';\n el.innerHTML=html;\n}\n\nasync function openTaskDetail(taskId){\n const overlay=document.getElementById('taskDetailOverlay');\n overlay.classList.add('show');\n document.getElementById('taskDetailTitle').textContent=t('tasks.loading');\n document.getElementById('taskDetailMeta').innerHTML='';\n document.getElementById('taskSkillSection').innerHTML='';\n document.getElementById('taskSkillSection').className='task-skill-section';\n document.getElementById('taskDetailSummary').textContent='';\n document.getElementById('taskDetailChunks').innerHTML='<div class=\"spinner\"></div>';\n\n try{\n const r=await fetch('/api/task/'+taskId);\n const task=await r.json();\n\n document.getElementById('taskDetailTitle').textContent=task.title||t('tasks.untitled');\n\n const meta=[\n '<span class=\"meta-item\"><span class=\"task-status-badge '+task.status+'\">'+t('tasks.status.'+task.status)+'</span></span>',\n '<span class=\"meta-item\">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>',\n ];\n if(task.endedAt) meta.push('<span class=\"meta-item\">\\u2192 '+formatTime(task.endedAt)+'</span>');\n meta.push('<span class=\"meta-item\">\\u{1F4C2} '+task.sessionKey+'</span>');\n meta.push('<span class=\"meta-item\">\\u{1F4DD} '+task.chunks.length+' '+t('tasks.chunks.label')+'</span>');\n 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>');\n document.getElementById('taskDetailMeta').innerHTML=meta.join('');\n\n // \u2500\u2500 Skill status section \u2500\u2500\n renderTaskSkillSection(task);\n\n var summaryEl=document.getElementById('taskDetailSummary');\n if(task.status==='skipped'){\n 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>';\n }else{\n summaryEl.innerHTML=renderSummaryHtml(task.summary);\n }\n\n if(task.chunks.length===0){\n document.getElementById('taskDetailChunks').innerHTML='<div style=\"color:var(--text-muted);padding:12px;font-size:13px\">'+t('tasks.nochunks')+'</div>';\n }else{\n document.getElementById('taskDetailChunks').innerHTML=task.chunks.map(c=>{\n var roleLabel=c.role==='user'?t('tasks.role.user'):c.role==='assistant'?t('tasks.role.assistant'):c.role.toUpperCase();\n return '<div class=\"task-chunk-item role-'+c.role+'\">'+\n '<div class=\"task-chunk-role '+c.role+'\">'+roleLabel+'</div>'+\n '<div class=\"task-chunk-bubble\" onclick=\"this.classList.toggle(\\'expanded\\')\">'+esc(c.content)+'</div>'+\n '<div class=\"task-chunk-time\">'+formatTime(c.createdAt)+'</div>'+\n '</div>';\n }).join('');\n }\n }catch(e){\n document.getElementById('taskDetailTitle').textContent=t('tasks.error');\n document.getElementById('taskDetailChunks').innerHTML='<div style=\"color:var(--rose)\">'+t('tasks.error.detail')+'</div>';\n }\n}\n\nfunction renderTaskSkillSection(task){\n const section=document.getElementById('taskSkillSection');\n const ss=task.skillStatus;\n const links=task.skillLinks||[];\n\n if(links.length>0){\n section.className='task-skill-section status-generated';\n var html='<div class=\"skill-status-header\">\\u{1F527} \u5DF2\u751F\u6210\u6280\u80FD</div>';\n html+=links.map(function(lk){\n 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;\n var statusLabel={'active':'\u6D3B\u8DC3','draft':'\u8349\u7A3F','archived':'\u5DF2\u5F52\u6863'}[lk.status]||lk.status;\n return '<div class=\"skill-link-card\" onclick=\"event.stopPropagation();closeTaskDetail();switchView(\\'skills\\');setTimeout(function(){openSkillDetail(\\''+lk.skillId+'\\')},300)\">'+\n '<div class=\"skill-link-name\">'+esc(lk.skillName)+' <span style=\"font-size:11px;color:var(--text-sec)\">('+relLabel+', v'+lk.versionAt+')</span></div>'+\n '<div class=\"skill-link-meta\">'+\n '\u72B6\u6001: <span class=\"task-status-badge '+(lk.status||'active')+'\">'+statusLabel+'</span>'+\n (lk.qualityScore!=null?' &middot; \u8D28\u91CF\u5206: '+lk.qualityScore+'/10':'')+\n '</div>'+\n '<div style=\"margin-top:4px\"><span class=\"task-id-full\">Skill ID: '+esc(lk.skillId)+'</span></div>'+\n '</div>';\n }).join('');\n section.innerHTML=html;\n }else if(ss==='generating'){\n section.className='task-skill-section status-generating';\n section.innerHTML='<div class=\"skill-status-header\">\\u23F3 \u6280\u80FD\u751F\u6210\u4E2D...</div>'+\n '<div class=\"skill-status-reason\">'+esc(task.skillReason||'')+'</div>';\n }else if(ss==='not_generated'){\n section.className='task-skill-section status-not_generated';\n section.innerHTML='<div class=\"skill-status-header\">\\u274C \u672A\u751F\u6210\u6280\u80FD</div>'+\n '<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>';\n }else if(ss==='skipped'){\n section.className='task-skill-section status-skipped';\n section.innerHTML='<div class=\"skill-status-header\">\\u23ED \u8DF3\u8FC7\u6280\u80FD\u8BC4\u4F30</div>'+\n '<div class=\"skill-status-reason\">\u539F\u56E0\uFF1A'+esc(task.skillReason||'')+'</div>';\n }else if(task.status==='active'){\n section.className='task-skill-section status-skipped';\n section.innerHTML='<div class=\"skill-status-header\">\\u23F8 \u4EFB\u52A1\u8FDB\u884C\u4E2D</div>'+\n '<div class=\"skill-status-reason\">\u6280\u80FD\u8BC4\u4F30\u5728\u4EFB\u52A1\u5B8C\u6210\u540E\u81EA\u52A8\u8FD0\u884C\u3002</div>';\n }else{\n section.className='task-skill-section status-skipped';\n section.innerHTML='<div class=\"skill-status-header\">\\u2014 \u65E0\u6280\u80FD\u4FE1\u606F</div>'+\n '<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>';\n }\n}\n\nfunction closeTaskDetail(event){\n if(event && event.target!==document.getElementById('taskDetailOverlay')) return;\n document.getElementById('taskDetailOverlay').classList.remove('show');\n}\n\n/* \u2500\u2500\u2500 Skills View Logic \u2500\u2500\u2500 */\nlet skillsStatusFilter='';\n\nfunction setSkillStatusFilter(btn,status){\n document.querySelectorAll('.skills-view .tasks-filters .filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n skillsStatusFilter=status;\n loadSkills();\n}\n\nasync function loadSkills(){\n const list=document.getElementById('skillsList');\n list.innerHTML='<div class=\"spinner\"></div>';\n try{\n const params=new URLSearchParams();\n if(skillsStatusFilter) params.set('status',skillsStatusFilter);\n const r=await fetch('/api/skills?'+params);\n const data=await r.json();\n\n document.getElementById('skillsTotalCount').textContent=formatNum(data.skills.length);\n document.getElementById('skillsActiveCount').textContent=formatNum(data.skills.filter(s=>s.status==='active').length);\n document.getElementById('skillsDraftCount').textContent=formatNum(data.skills.filter(s=>s.status==='draft').length);\n document.getElementById('skillsInstalledCount').textContent=formatNum(data.skills.filter(s=>s.installed).length);\n\n if(!data.skills||data.skills.length===0){\n list.innerHTML='<div style=\"text-align:center;padding:48px;color:var(--text-muted);font-size:14px\">'+t('skills.empty')+'</div>';\n return;\n }\n\n list.innerHTML=data.skills.map(skill=>{\n const timeStr=formatTime(skill.createdAt);\n const tags=parseTags(skill.tags);\n const installedClass=skill.installed?'installed':'';\n const statusClass=skill.status==='archived'?'archived':(skill.status==='draft'?'draft':'');\n const qs=skill.qualityScore;\n const qsBadge=qs!==null&&qs!==undefined?'<span class=\"skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'\">\\u2605 '+qs.toFixed(1)+'</span>':'';\n return '<div class=\"skill-card '+installedClass+' '+statusClass+'\" onclick=\"openSkillDetail(\\''+skill.id+'\\')\">'+\n '<div class=\"skill-card-top\">'+\n '<div class=\"skill-card-name\">\\u{1F9E0} '+esc(skill.name)+'</div>'+\n '<div class=\"skill-card-badges\">'+\n qsBadge+\n '<span class=\"skill-badge version\">v'+skill.version+'</span>'+\n (skill.installed?'<span class=\"skill-badge installed\">'+t('skills.installed.badge')+'</span>':'')+\n '<span class=\"skill-badge status-'+skill.status+'\">'+t('skills.status.'+skill.status)+'</span>'+\n '</div>'+\n '</div>'+\n '<div class=\"skill-card-desc\">'+esc(skill.description)+'</div>'+\n '<div class=\"skill-card-bottom\">'+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4C5}</span> '+timeStr+'</span>'+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4E6}</span> '+skill.sourceType+'</span>'+\n (tags.length>0?'<div class=\"skill-card-tags\">'+tags.map(t=>'<span class=\"skill-tag\">'+esc(t)+'</span>').join('')+'</div>':'')+\n '</div>'+\n '</div>';\n }).join('');\n }catch(e){\n list.innerHTML='<div style=\"text-align:center;padding:24px;color:var(--rose)\">Failed to load skills: '+esc(String(e))+'</div>';\n }\n}\n\nfunction parseTags(tagsStr){\n try{ const arr=JSON.parse(tagsStr||'[]'); return Array.isArray(arr)?arr:[]; }catch{ return []; }\n}\n\nlet currentSkillId='';\n\nasync function openSkillDetail(skillId){\n currentSkillId=skillId;\n const overlay=document.getElementById('skillDetailOverlay');\n overlay.classList.add('show');\n document.getElementById('skillDetailTitle').textContent=t('skills.loading');\n document.getElementById('skillDetailMeta').innerHTML='';\n document.getElementById('skillDetailDesc').textContent='';\n document.getElementById('skillFilesList').innerHTML='';\n document.getElementById('skillDetailContent').innerHTML='<div class=\"spinner\"></div>';\n document.getElementById('skillVersionsList').innerHTML='<div class=\"spinner\"></div>';\n document.getElementById('skillRelatedTasks').innerHTML='';\n\n try{\n const r=await fetch('/api/skill/'+skillId);\n if(!r.ok){\n const errText=await r.text();\n throw new Error('API '+r.status+': '+errText);\n }\n const data=await r.json();\n if(!data.skill){\n throw new Error('No skill data in response: '+JSON.stringify(data).slice(0,200));\n }\n const skill=data.skill;\n const versions=data.versions||[];\n const relatedTasks=data.relatedTasks||[];\n const files=data.files||[];\n\n document.getElementById('skillDetailTitle').textContent='\\u{1F9E0} '+skill.name;\n\n const qs=skill.qualityScore;\n 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>':'';\n document.getElementById('skillDetailMeta').innerHTML=[\n '<span class=\"meta-item\"><span class=\"skill-badge version\">v'+skill.version+'</span></span>',\n '<span class=\"meta-item\"><span class=\"skill-badge status-'+skill.status+'\">'+t('skills.status.'+skill.status)+'</span></span>',\n qsBadge,\n skill.installed?'<span class=\"meta-item\"><span class=\"skill-badge installed\">'+t('skills.installed.badge')+'</span></span>':'',\n '<span class=\"meta-item\">\\u{1F4C5} '+formatTime(skill.createdAt)+'</span>',\n '<span class=\"meta-item\">\\u270F '+t('skills.updated')+formatTime(skill.updatedAt)+'</span>',\n ].filter(Boolean).join('');\n\n document.getElementById('skillDetailDesc').textContent=skill.description;\n\n if(files.length>0){\n const fileIcons={'skill':'\\u{1F4D6}','script':'\\u{2699}','reference':'\\u{1F4CE}','file':'\\u{1F4C4}'};\n document.getElementById('skillFilesList').innerHTML=files.map(f=>\n '<div class=\"skill-file-item\">'+\n '<span class=\"skill-file-icon\">'+(fileIcons[f.type]||'\\u{1F4C4}')+'</span>'+\n '<span class=\"skill-file-name\">'+esc(f.path)+'</span>'+\n '<span class=\"skill-file-type\">'+f.type+'</span>'+\n '<span class=\"skill-file-size\">'+(f.size>1024?(f.size/1024).toFixed(1)+'KB':f.size+'B')+'</span>'+\n '</div>'\n ).join('');\n } else {\n document.getElementById('skillFilesList').innerHTML='<div style=\"color:var(--text-muted);font-size:12px\">'+t('skills.nofiles')+'</div>';\n }\n\n const latestVersion=versions[0];\n document.getElementById('skillContentTitle').textContent=latestVersion?'SKILL.md (v'+latestVersion.version+')':t('skills.content');\n document.getElementById('skillDetailContent').innerHTML=latestVersion?renderSkillMarkdown(latestVersion.content):'<span style=\"color:var(--text-muted)\">'+t('skills.nocontent')+'</span>';\n\n if(versions.length===0){\n document.getElementById('skillVersionsList').innerHTML='<div style=\"color:var(--text-muted);font-size:13px\">'+t('skills.noversions')+'</div>';\n } else {\n document.getElementById('skillVersionsList').innerHTML=versions.map(v=>{\n const vqs=v.qualityScore;\n const vqsBadge=vqs!==null&&vqs!==undefined?'<span class=\"skill-badge quality '+(vqs>=7?'high':vqs>=5?'mid':'low')+'\">\\u2605 '+vqs.toFixed(1)+'</span>':'';\n const summaryHtml=v.changeSummary?'<div class=\"skill-version-summary\">'+esc(v.changeSummary)+'</div>':'';\n return '<div class=\"skill-version-item\">'+\n '<div class=\"skill-version-header\">'+\n '<span class=\"skill-version-badge\">v'+v.version+'</span>'+\n '<span class=\"skill-version-type\">'+v.upgradeType+'</span>'+\n vqsBadge+\n '</div>'+\n '<div class=\"skill-version-changelog\">'+esc(v.changelog||t('skills.nochangelog'))+'</div>'+\n summaryHtml+\n '<div class=\"skill-version-time\">'+formatTime(v.createdAt)+(v.sourceTaskId?' \\u2022 '+t('skills.task.prefix')+v.sourceTaskId.slice(0,8)+'...':'')+'</div>'+\n '</div>';\n }).join('');\n }\n\n if(relatedTasks.length===0){\n document.getElementById('skillRelatedTasks').innerHTML='<div style=\"color:var(--text-muted);font-size:13px\">'+t('skills.norelated')+'</div>';\n } else {\n document.getElementById('skillRelatedTasks').innerHTML=relatedTasks.map(rt=>\n '<div class=\"skill-related-task\" onclick=\"event.stopPropagation();closeSkillDetail();switchView(\\'tasks\\');setTimeout(()=>openTaskDetail(\\''+rt.task.id+'\\'),300)\">'+\n '<span class=\"relation\">'+rt.relation+'</span>'+\n '<span class=\"task-title\">'+esc(rt.task.title||t('tasks.untitled.related'))+'</span>'+\n '<span style=\"font-size:11px;color:var(--text-muted)\">'+formatTime(rt.task.startedAt)+'</span>'+\n '</div>'\n ).join('');\n }\n\n }catch(e){\n document.getElementById('skillDetailTitle').textContent=t('skills.error');\n document.getElementById('skillDetailContent').innerHTML='<div style=\"color:var(--rose);padding:16px\">'+t('skills.error.detail')+esc(String(e))+'</div>';\n document.getElementById('skillFilesList').innerHTML='';\n document.getElementById('skillVersionsList').innerHTML='';\n document.getElementById('skillRelatedTasks').innerHTML='';\n }\n}\n\nfunction downloadSkill(){\n if(!currentSkillId) return;\n window.open('/api/skill/'+currentSkillId+'/download','_blank');\n}\n\n/* \u2500\u2500\u2500 Settings / Config \u2500\u2500\u2500 */\nasync function loadConfig(){\n try{\n const r=await fetch('/api/config');\n if(!r.ok) return;\n const cfg=await r.json();\n const emb=cfg.embedding||{};\n document.getElementById('cfgEmbProvider').value=emb.provider||'openai_compatible';\n document.getElementById('cfgEmbModel').value=emb.model||'';\n document.getElementById('cfgEmbEndpoint').value=emb.endpoint||'';\n document.getElementById('cfgEmbApiKey').value=emb.apiKey||'';\n\n const sum=cfg.summarizer||{};\n document.getElementById('cfgSumProvider').value=sum.provider||'openai_compatible';\n document.getElementById('cfgSumModel').value=sum.model||'';\n document.getElementById('cfgSumEndpoint').value=sum.endpoint||'';\n document.getElementById('cfgSumApiKey').value=sum.apiKey||'';\n document.getElementById('cfgSumTemp').value=sum.temperature!=null?sum.temperature:'';\n\n const sk=cfg.skillEvolution||{};\n document.getElementById('cfgSkillEnabled').checked=sk.enabled!==false;\n document.getElementById('cfgSkillAutoInstall').checked=!!sk.autoInstall;\n document.getElementById('cfgSkillConfidence').value=sk.minConfidence||'';\n document.getElementById('cfgSkillMinChunks').value=sk.minChunksForEval||'';\n\n const skSum=sk.summarizer||{};\n document.getElementById('cfgSkillProvider').value=skSum.provider||'';\n document.getElementById('cfgSkillModel').value=skSum.model||'';\n document.getElementById('cfgSkillEndpoint').value=skSum.endpoint||'';\n document.getElementById('cfgSkillApiKey').value=skSum.apiKey||'';\n\n document.getElementById('cfgViewerPort').value=cfg.viewerPort||'';\n\n const tel=cfg.telemetry||{};\n document.getElementById('cfgTelemetryEnabled').checked=tel.enabled!==false;\n }catch(e){\n console.error('loadConfig error',e);\n }\n}\n\nasync function saveConfig(){\n const cfg={};\n const embP=document.getElementById('cfgEmbProvider').value;\n if(embP){\n cfg.embedding={provider:embP};\n const v=document.getElementById('cfgEmbModel').value.trim();if(v) cfg.embedding.model=v;\n const e=document.getElementById('cfgEmbEndpoint').value.trim();if(e) cfg.embedding.endpoint=e;\n const k=document.getElementById('cfgEmbApiKey').value.trim();if(k) cfg.embedding.apiKey=k;\n }\n const sumP=document.getElementById('cfgSumProvider').value;\n if(sumP){\n cfg.summarizer={provider:sumP};\n const v=document.getElementById('cfgSumModel').value.trim();if(v) cfg.summarizer.model=v;\n const e=document.getElementById('cfgSumEndpoint').value.trim();if(e) cfg.summarizer.endpoint=e;\n const k=document.getElementById('cfgSumApiKey').value.trim();if(k) cfg.summarizer.apiKey=k;\n const tp=document.getElementById('cfgSumTemp').value.trim();if(tp!=='') cfg.summarizer.temperature=Number(tp);\n }\n cfg.skillEvolution={\n enabled:document.getElementById('cfgSkillEnabled').checked,\n autoInstall:document.getElementById('cfgSkillAutoInstall').checked\n };\n const mc=document.getElementById('cfgSkillConfidence').value.trim();if(mc) cfg.skillEvolution.minConfidence=Number(mc);\n const mk=document.getElementById('cfgSkillMinChunks').value.trim();if(mk) cfg.skillEvolution.minChunksForEval=Number(mk);\n\n const skP=document.getElementById('cfgSkillProvider').value;\n if(skP){\n cfg.skillEvolution.summarizer={provider:skP};\n const sv=document.getElementById('cfgSkillModel').value.trim();if(sv) cfg.skillEvolution.summarizer.model=sv;\n const se=document.getElementById('cfgSkillEndpoint').value.trim();if(se) cfg.skillEvolution.summarizer.endpoint=se;\n const sk=document.getElementById('cfgSkillApiKey').value.trim();if(sk) cfg.skillEvolution.summarizer.apiKey=sk;\n }\n\n const vp=document.getElementById('cfgViewerPort').value.trim();\n if(vp) cfg.viewerPort=Number(vp);\n\n cfg.telemetry={\n enabled:document.getElementById('cfgTelemetryEnabled').checked\n };\n\n try{\n const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});\n if(!r.ok) throw new Error(await r.text());\n const el=document.getElementById('settingsSaved');\n el.classList.add('show');\n setTimeout(()=>el.classList.remove('show'),2500);\n }catch(e){\n showToast(t('settings.save.fail')+': '+e.message,'error');\n }\n}\n\nfunction renderSkillMarkdown(md){\n let content=md;\n // Strip YAML frontmatter\n content=content.replace(/^---[\\s\\S]*?---\\s*/,'');\n // Code blocks\n content=content.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(_,lang,code){\n 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>';\n });\n // Inline code\n 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>');\n // Headers\n content=content.replace(/^### (.+)$/gm,'<div class=\"summary-section-title\" style=\"font-size:13px;margin-top:12px\">$1</div>');\n content=content.replace(/^## (.+)$/gm,'<div class=\"summary-section-title\">$1</div>');\n content=content.replace(/^# (.+)$/gm,'<div style=\"font-size:16px;font-weight:700;color:var(--text);margin:8px 0\">$1</div>');\n // Bold\n content=content.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');\n // List items\n content=content.replace(/^- (.+)$/gm,'<div style=\"padding-left:16px;position:relative;margin:3px 0\"><span style=\"position:absolute;left:4px;color:var(--text-muted)\">\u2022</span>$1</div>');\n // HTML comments (version markers)\n content=content.replace(/<!--[\\s\\S]*?-->/g,'');\n // Line breaks\n content=content.replace(/\\n\\n/g,'<div style=\"height:10px\"></div>');\n content=content.replace(/\\n/g,'<br>');\n return content;\n}\n\nfunction closeSkillDetail(event){\n if(event && event.target!==document.getElementById('skillDetailOverlay')) return;\n document.getElementById('skillDetailOverlay').classList.remove('show');\n}\n\nfunction formatDuration(ms){\n const s=Math.floor(ms/1000);\n if(s<60) return s+'s';\n const m=Math.floor(s/60);\n if(m<60) return m+'min';\n const h=Math.floor(m/60);\n if(h<24) return h+'h '+((m%60)>0?(m%60)+'min':'');\n const d=Math.floor(h/24);\n return d+'d '+((h%24)>0?(h%24)+'h':'');\n}\n\nfunction formatTime(ts){\n if(!ts) return '-';\n return new Date(ts).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});\n}\n\nfunction fillDays(rows,days){\n const map=new Map((rows||[]).map(r=>[r.date,{...r}]));\n const out=[];const now=new Date();\n for(let i=days-1;i>=0;i--){\n const d=new Date(now);d.setDate(d.getDate()-i);\n const dateStr=d.toISOString().slice(0,10);\n const row=map.get(dateStr)||{};\n out.push({date:dateStr,count:row.count??0,list:row.list??0,search:row.search??0,total:(row.list??0)+(row.search??0)});\n }\n if(days>21){\n const weeks=[];let i=0;\n while(i<out.length){\n const chunk=out.slice(i,i+7);\n const first=chunk[0].date,last=chunk[chunk.length-1].date;\n const c=chunk.reduce((s,r)=>s+r.count,0);\n const l=chunk.reduce((s,r)=>s+r.list,0);\n const se=chunk.reduce((s,r)=>s+r.search,0);\n const label=first.slice(5,10)+'~'+last.slice(8,10);\n weeks.push({date:label,count:c,list:l,search:se,total:l+se});\n i+=7;\n }\n return weeks;\n }\n return out;\n}\n\nfunction renderBars(el,data,valueKey,H){\n const vals=data.map(d=>d[valueKey]??0);\n 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;}\n const max=Math.max(1,...vals);\n const nonZero=vals.filter(v=>v>0).length;\n const barStyle=data.length<=7?'min-width:40px;max-width:120px':'';\n el.innerHTML=data.map(r=>{\n const v=r[valueKey]??0;\n const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);\n if(v===0){\n 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>';\n }\n const h=Math.max(8,Math.round((v/max)*H));\n 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>';\n }).join('');\n}\n\nfunction renderChartWrites(rows){\n const el=document.getElementById('chartWrites');\n const filled=fillDays(rows?.map(r=>({date:r.date,count:r.count})),metricsDays);\n renderBars(el,filled,'count',160);\n}\n\nfunction renderChartCalls(rows){\n const el=document.getElementById('chartCalls');\n const filled=fillDays(rows?.map(r=>({date:r.date,list:r.list,search:r.search})),metricsDays);\n const vals=filled.map(f=>f.total);\n if(vals.every(v=>v===0)){el.innerHTML='<div style=\"color:var(--text-muted);font-size:13px;padding:20px;text-align:center\">'+t('chart.nocalls')+'</div>';return;}\n const max=Math.max(1,...vals);\n const H=160;\n el.innerHTML=filled.map(r=>{\n const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);\n if(r.total===0){\n 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>';\n }\n const totalH=Math.max(8,Math.round((r.total/max)*H));\n const listH=r.list?Math.max(3,Math.round((r.list/r.total)*totalH)):0;\n const searchH=r.search?totalH-listH:0;\n const tip='List: '+r.list+', Search: '+r.search;\n let bars='';\n if(searchH>0) bars+='<div class=\"chart-bar violet\" style=\"height:'+searchH+'px\"></div>';\n if(listH>0) bars+='<div class=\"chart-bar\" style=\"height:'+listH+'px\"></div>';\n return '<div class=\"chart-bar-wrap\"><div class=\"chart-tip\">'+tip+'</div><div class=\"chart-bar-col\"><div style=\"display:flex;flex-direction:column;gap:1px\">'+bars+'</div></div><div class=\"chart-bar-label\">'+label+'</div></div>';\n }).join('');\n}\n\n/* \u2500\u2500\u2500 Tool Performance Chart \u2500\u2500\u2500 */\nlet toolMinutes=60;\nconst TOOL_COLORS=['#818cf8','#34d399','#fbbf24','#f87171','#38bdf8','#a78bfa','#fb923c'];\n\nfunction setToolMinutes(m){\n toolMinutes=m;\n document.querySelectorAll('.tool-range').forEach(b=>{\n b.classList.toggle('active',Number(b.dataset.mins)===m);\n });\n loadToolMetrics();\n}\n\nasync function loadToolMetrics(){\n try{\n const r=await fetch('/api/tool-metrics?minutes='+toolMinutes);\n if(!r.ok) return;\n const d=await r.json();\n if(d.error) return;\n renderToolChart(d);\n renderToolAgg(d);\n }catch(e){\n console.warn('loadToolMetrics error:',e);\n }\n}\n\nfunction renderToolChart(data){\n const container=document.getElementById('toolChart');\n const legend=document.getElementById('toolLegend');\n const {tools,series}=data;\n\n if(!series||series.length===0||tools.length===0){\n 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\">\uD83D\uDCCA</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>';\n legend.innerHTML='';\n return;\n }\n\n const W=container.clientWidth||800;\n const H=280;\n const pad={t:20,r:20,b:36,l:52};\n const cw=W-pad.l-pad.r;\n const ch=H-pad.t-pad.b;\n\n let maxVal=0;\n for(const s of series){for(const t of tools){const v=s[t]||0;if(v>maxVal)maxVal=v;}}\n if(maxVal===0)maxVal=100;\n maxVal=Math.ceil(maxVal*1.15);\n\n const gridLines=5;\n let gridHtml='';\n for(let i=0;i<=gridLines;i++){\n const y=pad.t+ch-(ch/gridLines)*i;\n const val=Math.round((maxVal/gridLines)*i);\n gridHtml+='<line class=\"grid-line\" x1=\"'+pad.l+'\" y1=\"'+y+'\" x2=\"'+(W-pad.r)+'\" y2=\"'+y+'\"/>';\n gridHtml+='<text class=\"axis-label\" x=\"'+(pad.l-8)+'\" y=\"'+(y+3)+'\" text-anchor=\"end\">'+val+'ms</text>';\n }\n\n const step=cw/(series.length-1||1);\n const labelEvery=Math.max(1,Math.floor(series.length/8));\n let labelsHtml='';\n series.forEach((s,i)=>{\n if(i%labelEvery===0||i===series.length-1){\n const x=pad.l+i*step;\n const time=s.minute.slice(11);\n labelsHtml+='<text class=\"axis-label\" x=\"'+x+'\" y=\"'+(H-4)+'\" text-anchor=\"middle\">'+time+'</text>';\n }\n });\n\n let pathsHtml='';\n let dotsHtml='';\n tools.forEach((toolName,ti)=>{\n const color=TOOL_COLORS[ti%TOOL_COLORS.length];\n const pts=series.map((s,i)=>{\n const x=pad.l+i*step;\n const v=s[toolName]||0;\n const y=pad.t+ch-((v/maxVal)*ch);\n return {x,y,v};\n });\n let line='M'+pts[0].x.toFixed(1)+' '+pts[0].y.toFixed(1);\n for(let i=1;i<pts.length;i++){\n const p0=pts[Math.max(0,i-2)],p1=pts[i-1],p2=pts[i],p3=pts[Math.min(pts.length-1,i+1)];\n const cp1x=(p1.x+(p2.x-p0.x)/6).toFixed(1),cp1y=(p1.y+(p2.y-p0.y)/6).toFixed(1);\n const cp2x=(p2.x-(p3.x-p1.x)/6).toFixed(1),cp2y=(p2.y-(p3.y-p1.y)/6).toFixed(1);\n line+=' C'+cp1x+' '+cp1y+','+cp2x+' '+cp2y+','+p2.x.toFixed(1)+' '+p2.y.toFixed(1);\n }\n pathsHtml+='<path class=\"data-line\" d=\"'+line+'\" stroke=\"'+color+'\" />';\n const area=line+' L'+pts[pts.length-1].x.toFixed(1)+' '+(pad.t+ch)+' L'+pts[0].x.toFixed(1)+' '+(pad.t+ch)+' Z';\n pathsHtml+='<path class=\"data-area\" d=\"'+area+'\" fill=\"url(#tg'+ti+')\" />';\n pts.forEach((p,i)=>{\n 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+'\" />';\n });\n });\n\n const svg='<svg class=\"tool-chart-svg\" viewBox=\"0 0 '+W+' '+H+'\" preserveAspectRatio=\"xMidYMid meet\">'+\n '<defs>'+\n tools.map((t,i)=>{\n const c=TOOL_COLORS[i%TOOL_COLORS.length];\n 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>'+\n '';\n }).join('')+'</defs>'+\n \n gridHtml+labelsHtml+pathsHtml+dotsHtml+\n '<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\" />'+\n '<rect class=\"hover-rect\" x=\"'+pad.l+'\" y=\"'+pad.t+'\" width=\"'+cw+'\" height=\"'+ch+'\" fill=\"transparent\" />'+\n '</svg><div class=\"tool-chart-tooltip\" id=\"toolTooltip\"></div>';\n\n container.innerHTML=svg;\n\n legend.innerHTML=tools.map((t,i)=>{\n const c=TOOL_COLORS[i%TOOL_COLORS.length];\n return '<span><span class=\"dot\" style=\"background:'+c+'\"></span>'+t+'</span>';\n }).join('');\n\n const svgEl=container.querySelector('svg');\n const tooltip=document.getElementById('toolTooltip');\n const rect=svgEl.querySelector('.hover-rect');\n\n rect.addEventListener('mousemove',function(e){\n const r=svgEl.getBoundingClientRect();\n const mx=e.clientX-r.left;\n const scale=W/r.width;\n const dataX=(mx*scale-pad.l)/step;\n const idx=Math.max(0,Math.min(series.length-1,Math.round(dataX)));\n const s=series[idx];\n if(!s)return;\n\n svgEl.querySelectorAll('.hover-dot').forEach(d=>{\n d.classList.toggle('show',Number(d.dataset.idx)===idx);\n });\n const crosshair=svgEl.querySelector('.crosshair');\n const cx=pad.l+idx*step;\n crosshair.setAttribute('x1',cx);crosshair.setAttribute('x2',cx);crosshair.setAttribute('opacity','0.5');\n\n let rows='<div class=\"tt-time\">'+s.minute+'</div>';\n tools.forEach((t,ti)=>{\n const v=s[t]||0;\n const c=TOOL_COLORS[ti%TOOL_COLORS.length];\n rows+='<div class=\"tt-row\"><span class=\"tt-dot\" style=\"background:'+c+'\"></span>'+t+'<span class=\"tt-val\">'+v+'ms</span></div>';\n });\n tooltip.innerHTML=rows;\n tooltip.classList.add('show');\n\n const tx=e.clientX-container.getBoundingClientRect().left;\n const ty=e.clientY-container.getBoundingClientRect().top;\n tooltip.style.left=(tx+15)+'px';\n tooltip.style.top=(ty-10)+'px';\n if(tx>container.clientWidth*0.7) tooltip.style.left=(tx-tooltip.offsetWidth-15)+'px';\n });\n\n rect.addEventListener('mouseleave',function(){\n svgEl.querySelectorAll('.hover-dot').forEach(d=>d.classList.remove('show'));\n svgEl.querySelector('.crosshair').setAttribute('opacity','0');\n tooltip.classList.remove('show');\n });\n}\n\nfunction renderToolAgg(data){\n const el=document.getElementById('toolAggTable');\n const {aggregated}=data;\n if(!aggregated||aggregated.length===0){el.innerHTML='';return;}\n\n const msClass=v=>v<100?'fast':v<500?'medium':'slow';\n\n 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>'+\n aggregated.map((a,i)=>{\n const c=TOOL_COLORS[i%TOOL_COLORS.length];\n return '<tr>'+\n '<td><span class=\"tool-name\"><span class=\"tool-dot\" style=\"background:'+c+'\"></span>'+a.tool+'</span></td>'+\n '<td>'+a.totalCalls+'</td>'+\n '<td><span class=\"ms-val '+msClass(a.avgMs)+'\">'+a.avgMs+'ms</span></td>'+\n '<td><span class=\"ms-val '+msClass(a.p95Ms)+'\">'+a.p95Ms+'ms</span></td>'+\n '<td>'+(a.errorCount>0?'<span style=\"color:var(--accent)\">'+a.errorCount+'</span>':'<span style=\"color:var(--text-muted)\">0</span>')+'</td>'+\n '</tr>';\n }).join('')+\n '</tbody></table>';\n}\n\nfunction renderBreakdown(obj,containerId){\n const el=document.getElementById(containerId);\n if(!el)return;\n const entries=Object.entries(obj||{}).sort((a,b)=>b[1]-a[1]);\n const total=entries.reduce((s,[,v])=>s+v,0)||1;\n el.innerHTML=entries.map(([label,value])=>{\n const pct=Math.round((value/total)*100);\n return '<div class=\"breakdown-item\"><div class=\"bd-top\"><span class=\"label\">'+esc(label)+'</span><span class=\"value\">'+value+' <span style=\"font-size:11px;font-weight:500;color:var(--text-muted)\">('+pct+'%)</span></span></div><div class=\"breakdown-bar-wrap\"><div class=\"breakdown-bar\" style=\"width:'+pct+'%\"></div></div></div>';\n }).join('');\n}\n\n/* \u2500\u2500\u2500 Data loading \u2500\u2500\u2500 */\nasync function loadAll(){\n await Promise.all([loadStats(),loadMemories()]);\n checkMigrateStatus();\n connectPPSSE();\n}\n\nasync function loadStats(){\n const r=await fetch('/api/stats');\n const d=await r.json();\n const dedupB=d.dedupBreakdown||{};\n const activeCount=dedupB.active||d.totalMemories;\n const inactiveCount=(dedupB.duplicate||0)+(dedupB.merged||0);\n document.getElementById('statTotal').textContent=d.totalMemories;\n if(inactiveCount>0){\n document.getElementById('statTotal').title=activeCount+' '+t('stat.active')+', '+inactiveCount+' '+t('stat.deduped');\n }\n document.getElementById('statSessions').textContent=d.totalSessions;\n document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;\n let days=0;\n if(d.timeRange&&d.timeRange.earliest!=null&&d.timeRange.latest!=null){\n let e=Number(d.timeRange.earliest), l=Number(d.timeRange.latest);\n if(Number.isFinite(e)&&Number.isFinite(l)){\n if(e<1e12) e*=1000;\n if(l<1e12) l*=1000;\n days=Math.round((l-e)/86400000);\n days=Math.max(0,Math.min(36500,days));\n if(days===0) days=1;\n }\n }\n document.getElementById('statTimeSpan').textContent=days;\n\n const provEl=document.getElementById('embeddingStatus');\n if(d.embeddingProvider && d.embeddingProvider!=='none'){\n provEl.innerHTML='<div class=\"provider-badge\"><span>\\u2713</span> '+t('embed.on')+d.embeddingProvider+'</div>';\n } else {\n provEl.innerHTML='<div class=\"provider-badge offline\"><span>\\u26A0</span> '+t('embed.off')+'</div>';\n }\n\n const sl=document.getElementById('sessionList');\n sl.innerHTML='<div class=\"session-item'+(activeSession===null?' active':'')+'\" onclick=\"filterSession(null)\"><span>'+t('sidebar.allsessions')+'</span><span class=\"count\">'+d.totalMemories+'</span></div>';\n (d.sessions||[]).forEach(s=>{\n const isActive=activeSession===s.session_key;\n const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;\n sl.innerHTML+='<div class=\"session-item'+(isActive?' active':'')+'\" onclick=\"filterSession(\\''+s.session_key.replace(/'/g,\"\\\\'\")+'\\')\"><span title=\"'+s.session_key+'\">'+name+'</span><span class=\"count\">'+s.count+'</span></div>';\n });\n}\n\nfunction getFilterParams(){\n const p=new URLSearchParams();\n if(activeSession) p.set('session',activeSession);\n if(activeRole) p.set('role',activeRole);\n const kind=document.getElementById('filterKind').value;\n if(kind) p.set('kind',kind);\n const df=document.getElementById('dateFrom').value;\n if(df) p.set('dateFrom',df);\n const dt=document.getElementById('dateTo').value;\n if(dt) p.set('dateTo',dt);\n const sort=document.getElementById('filterSort').value;\n if(sort==='oldest') p.set('sort','oldest');\n return p;\n}\n\nasync function loadMemories(page){\n if(page) currentPage=page;\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('limit',PAGE_SIZE);\n p.set('page',currentPage);\n const r=await fetch('/api/memories?'+p.toString());\n const d=await r.json();\n totalPages=d.totalPages||1;\n totalCount=d.total||0;\n document.getElementById('searchMeta').textContent=totalCount+t('search.meta.total');\n renderMemories(d.memories||[]);\n renderPagination();\n}\n\nasync function doSearch(q){\n if(!q.trim()){currentPage=1;loadMemories();return}\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('q',q);\n const r=await fetch('/api/search?'+p.toString());\n const d=await r.json();\n const meta=[];\n if(d.vectorCount>0) meta.push(d.vectorCount+t('search.meta.semantic'));\n if(d.ftsCount>0) meta.push(d.ftsCount+t('search.meta.text'));\n meta.push(d.total+t('search.meta.results'));\n document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');\n renderMemories(d.results||[]);\n document.getElementById('pagination').innerHTML='';\n}\n\nfunction debounceSearch(){\n clearTimeout(searchTimer);\n searchTimer=setTimeout(()=>doSearch(document.getElementById('searchInput').value),350);\n}\n\nfunction filterSession(key){\n activeSession=key;\n currentPage=1;\n loadAll();\n}\n\nfunction setRoleFilter(btn,role){\n activeRole=role;\n currentPage=1;\n document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n applyFilters();\n}\n\nfunction applyFilters(){\n currentPage=1;\n if(document.getElementById('searchInput').value.trim()){\n doSearch(document.getElementById('searchInput').value);\n } else {\n loadMemories();\n }\n}\n\nfunction clearDateFilter(){\n document.getElementById('dateFrom').value='';\n document.getElementById('dateTo').value='';\n applyFilters();\n}\n\n/* \u2500\u2500\u2500 Rendering \u2500\u2500\u2500 */\nfunction renderMemories(items){\n const list=document.getElementById('memoryList');\n if(!items.length){\n list.innerHTML='<div class=\"empty\"><div class=\"icon\">\\u{1F4ED}</div><p>'+t('empty.text')+'</p></div>';\n return;\n }\n items.forEach(m=>{memoryCache[m.id]=m});\n list.innerHTML=items.map(m=>{\n const time=m.created_at?new Date(typeof m.created_at==='number'?m.created_at:m.created_at).toLocaleString('zh-CN'):'';\n const role=m.role||'user';\n const kind=m.kind||'paragraph';\n const summary=esc(m.summary||m.content?.slice(0,120)||'');\n const content=esc(m.content||'');\n const id=m.id;\n const vscore=m._vscore?'<span class=\"vscore-badge\">'+Math.round(m._vscore*100)+'%</span>':'';\n const sid=m.session_key||'';\n const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;\n const mc=m.merge_count||0;\n const mergeBadge=mc>0?'<span class=\"merge-badge\">\\u{1F504} '+t('card.evolved')+' '+mc+t('card.times')+'</span>':'';\n 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>':'';\n const ds=m.dedup_status||'active';\n const isInactive=ds==='duplicate'||ds==='merged';\n const dedupBadge=ds==='duplicate'?'<span class=\"dedup-badge duplicate\">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class=\"dedup-badge merged\">'+t('card.dedupMerged')+'</span>':'';\n const isImported=sid.startsWith('openclaw-import-')||sid.startsWith('openclaw-session-');\n const importBadge=isImported?'<span class=\"import-badge\">\uD83E\uDD90 '+t('card.imported')+'</span>':'';\n let dedupInfo='';\n if(isInactive){\n const reason=m.dedup_reason?'<span style=\"font-size:11px;color:var(--text-muted)\">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';\n 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>':'';\n dedupInfo='<div style=\"margin-top:6px;font-size:11px\">'+target+' '+reason+'</div>';\n }\n let historyHtml='';\n if(mc>0){\n try{\n const hist=JSON.parse(m.merge_history||'[]');\n if(hist.length>0){\n 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>';\n hist.forEach(function(h){\n const ht=h.at?new Date(h.at).toLocaleString('zh-CN'):'';\n 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||'');\n if(h.from) historyHtml+='<br><span style=\"opacity:.6\">'+t('card.oldSummary')+':</span> '+esc(h.from);\n if(h.to) historyHtml+='<br><span style=\"opacity:.6\">'+t('card.newSummary')+':</span> '+esc(h.to);\n historyHtml+='</div>';\n });\n historyHtml+='</div>';\n }\n }catch(e){}\n }\n return '<div class=\"memory-card'+(isInactive?' dedup-inactive':'')+'\">'+\n '<div class=\"card-header\"><div class=\"meta\"><span class=\"role-tag '+role+'\">'+role+'</span><span class=\"kind-tag\">'+kind+'</span>'+importBadge+dedupBadge+mergeBadge+'</div><span class=\"card-time\"><span class=\"session-tag\" title=\"'+esc(sid)+'\">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+\n '<div class=\"card-summary\">'+summary+'</div>'+\n dedupInfo+\n '<div class=\"card-content\" id=\"content-'+id+'\"><pre>'+content+'</pre></div>'+\n historyHtml+\n '<div class=\"card-actions\">'+\n '<button class=\"btn btn-sm btn-ghost\" onclick=\"toggleContent(\\''+id+'\\')\">'+t('card.expand')+'</button>'+\n (mc>0?'<button class=\"btn btn-sm btn-ghost\" onclick=\"toggleHistory(\\''+id+'\\')\">'+t('card.evolveHistory')+'</button>':'')+\n '<button class=\"btn btn-sm btn-ghost\" onclick=\"openEditModal(\\''+id+'\\')\">'+t('card.edit')+'</button>'+\n '<button class=\"btn btn-sm btn-ghost\" style=\"color:var(--accent)\" onclick=\"deleteMemory(\\''+id+'\\')\">'+t('card.delete')+'</button>'+\n vscore+\n '</div></div>';\n }).join('');\n}\n\nfunction renderPagination(){\n const el=document.getElementById('pagination');\n if(totalPages<=1){el.innerHTML='';return}\n let h='';\n h+='<button class=\"pg-btn'+(currentPage<=1?' disabled':'')+'\" onclick=\"goPage('+(currentPage-1)+')\">\u2039</button>';\n const range=[];\n range.push(1);\n for(let i=Math.max(2,currentPage-2);i<=Math.min(totalPages-1,currentPage+2);i++) range.push(i);\n if(totalPages>1) range.push(totalPages);\n const unique=[...new Set(range)].sort((a,b)=>a-b);\n let prev=0;\n for(const p of unique){\n if(p-prev>1) h+='<span class=\"pg-info\">...</span>';\n h+='<button class=\"pg-btn'+(p===currentPage?' active':'')+'\" onclick=\"goPage('+p+')\">'+p+'</button>';\n prev=p;\n }\n h+='<button class=\"pg-btn'+(currentPage>=totalPages?' disabled':'')+'\" onclick=\"goPage('+(currentPage+1)+')\">\u203A</button>';\n h+='<span class=\"pg-info\">'+totalCount+t('pagination.total')+'</span>';\n el.innerHTML=h;\n}\n\nfunction goPage(p){\n if(p<1||p>totalPages||p===currentPage) return;\n currentPage=p;\n loadMemories();\n document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});\n}\n\nfunction toggleHistory(id){\n const el=document.getElementById('history-'+id);\n if(el) el.style.display=el.style.display==='none'?'block':'none';\n}\n\nfunction toggleContent(id){\n const el=document.getElementById('content-'+id);\n el.classList.toggle('show');\n}\n\nfunction scrollToMemory(targetId){\n const cards=document.querySelectorAll('.memory-card');\n for(const card of cards){\n const contentEl=card.querySelector('[id^=\"content-\"]');\n if(contentEl&&contentEl.id==='content-'+targetId){\n card.scrollIntoView({behavior:'smooth',block:'center'});\n card.style.transition='box-shadow .3s';\n card.style.boxShadow='0 0 0 2px var(--pri)';\n setTimeout(()=>{card.style.boxShadow='';},2000);\n return;\n }\n }\n showMemoryModal(targetId);\n}\nasync function showMemoryModal(chunkId){\n const overlay=document.getElementById('memoryModal');\n const body=document.getElementById('memoryModalBody');\n body.innerHTML='<div style=\"text-align:center;padding:40px;color:var(--text-sec)\">Loading...</div>';\n overlay.classList.add('show');\n try{\n const res=await fetch('/api/memory/'+encodeURIComponent(chunkId));\n if(!res.ok){body.innerHTML='<div style=\"text-align:center;padding:40px;color:#f87171\">Memory not found</div>';return;}\n const data=await res.json();\n const m=data.memory;\n const role=(m.role||'unknown').toUpperCase();\n const roleCls=(m.role||'').toLowerCase();\n const kind=m.kind||'paragraph';\n const ds=m.dedup_status||'active';\n const time=new Date(m.created_at).toLocaleString('zh-CN');\n const updated=m.updated_at?new Date(m.updated_at).toLocaleString('zh-CN'):'';\n let html='<div class=\"modal-memory-card\">';\n html+='<div class=\"modal-header-row\"><span class=\"role-tag '+roleCls+'\">'+role+'</span><span class=\"kind-tag\">'+kind+'</span>';\n if(ds!=='active') html+='<span class=\"dedup-badge '+(ds==='duplicate'?'duplicate':'merged')+'\">'+ds+'</span>';\n html+='</div>';\n 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>';\n 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>';\n html+='<div class=\"modal-field\"><div class=\"modal-field-label\">Content</div><pre class=\"modal-field-content\">'+esc(m.content||'')+'</pre></div>';\n html+='<div class=\"modal-meta-row\">';\n html+='<span><strong>Session:</strong> '+esc(m.session_key||'')+'</span>';\n html+='<span><strong>Created:</strong> '+time+'</span>';\n if(updated) html+='<span><strong>Updated:</strong> '+updated+'</span>';\n html+='</div>';\n 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>';\n 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>';\n html+='</div>';\n body.innerHTML=html;\n }catch(e){body.innerHTML='<div style=\"text-align:center;padding:40px;color:#f87171\">Error: '+esc(String(e))+'</div>';}\n}\nfunction closeMemoryModal(){document.getElementById('memoryModal').classList.remove('show');}\n\n\nfunction esc(s){\n if(!s)return'';\n return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n}\n\nfunction renderSummaryHtml(raw){\n if(!raw)return'';\n var lines=raw.split('\\n');\n var html=[];\n var inList=false;\n var sectionRe=new RegExp('^(\uD83C\uDFAF|\uD83D\uDCCB|\u2705|\uD83D\uDCA1)\\\\s+(.+)$');\n var listRe=new RegExp('^- (.+)$');\n for(var i=0;i<lines.length;i++){\n var line=lines[i];\n var hm=line.match(sectionRe);\n if(hm){\n if(inList){html.push('</ul>');inList=false;}\n html.push('<div class=\"summary-section-title\">'+esc(line)+'</div>');\n continue;\n }\n var lm=line.match(listRe);\n if(lm){\n if(!inList){html.push('<ul>');inList=true;}\n html.push('<li>'+esc(lm[1])+'</li>');\n continue;\n }\n if(line.trim()===''){\n if(inList){html.push('</ul>');inList=false;}\n continue;\n }\n if(inList){html.push('</ul>');inList=false;}\n html.push('<p style=\"margin:4px 0\">'+esc(line)+'</p>');\n }\n if(inList)html.push('</ul>');\n return html.join('');\n}\n\n/* \u2500\u2500\u2500 CRUD \u2500\u2500\u2500 */\nfunction openCreateModal(){\n editingId=null;\n document.getElementById('modalTitle').textContent=t('modal.new');\n document.getElementById('modalSubmit').textContent=t('modal.create');\n document.getElementById('mRole').value='user';\n document.getElementById('mContent').value='';\n document.getElementById('mSummary').value='';\n document.getElementById('mKind').value='paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction openEditModal(id){\n const m=memoryCache[id];\n if(!m){toast(t('toast.notfound'),'error');return}\n editingId=id;\n document.getElementById('modalTitle').textContent=t('modal.edit');\n document.getElementById('modalSubmit').textContent=t('modal.save');\n document.getElementById('mRole').value=m.role||'user';\n document.getElementById('mContent').value=m.content||'';\n document.getElementById('mSummary').value=m.summary||'';\n document.getElementById('mKind').value=m.kind||'paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction closeModal(){\n document.getElementById('modalOverlay').classList.remove('show');\n}\n\nasync function submitModal(){\n const data={\n role:document.getElementById('mRole').value,\n content:document.getElementById('mContent').value,\n summary:document.getElementById('mSummary').value,\n kind:document.getElementById('mKind').value,\n };\n if(!data.content.trim()){toast(t('modal.err.empty'),'error');return}\n let r;\n if(editingId){\n r=await fetch('/api/memory/'+editingId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n } else {\n r=await fetch('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n }\n const d=await r.json();\n if(d.ok){toast(editingId?t('toast.updated'):t('toast.created'),'success');closeModal();loadAll();}\n else{toast(d.error||t('toast.opfail'),'error')}\n}\n\nasync function deleteMemory(id){\n if(!confirm(t('confirm.delete')))return;\n const r=await fetch('/api/memory/'+id,{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast(t('toast.deleted'),'success');loadAll();}\n else{toast(t('toast.delfail'),'error')}\n}\n\nasync function clearAll(){\n if(!confirm(t('confirm.clearall')))return;\n if(!confirm(t('confirm.clearall2')))return;\n const r=await fetch('/api/memories',{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast(t('toast.cleared'),'success');loadAll();}\n else{toast(t('toast.clearfail'),'error')}\n}\n\n/* \u2500\u2500\u2500 Migration \u2500\u2500\u2500 */\nlet migrateScanData=null;\nlet migrateStats={stored:0,skipped:0,merged:0,errors:0};\n\nasync function migrateScan(){\n const btn=document.getElementById('migrateScanBtn');\n btn.disabled=true;\n btn.textContent=t('migrate.scanning');\n document.getElementById('migrateStartBtn').style.display='none';\n document.getElementById('migrateScanResult').style.display='none';\n document.getElementById('migrateConfigWarn').style.display='none';\n document.getElementById('migrateProgress').style.display='none';\n\n try{\n const r=await fetch('/api/migrate/scan');\n if(!r.ok){ const err=await r.text(); throw new Error(err); }\n const d=await r.json();\n migrateScanData=d;\n\n const files=d.sqliteFiles||[];\n const sess=d.sessions||{count:0,messages:0};\n const sqliteTotal=files.reduce((s,f)=>s+f.chunks,0);\n document.getElementById('migrateSqliteCount').textContent=sqliteTotal;\n document.getElementById('migrateSqliteFiles').textContent=files.map(f=>f.file+' ('+f.chunks+')').join(', ')||'\u2014';\n document.getElementById('migrateSessionCount').textContent=sess.messages;\n document.getElementById('migrateSessionFiles').textContent=sess.count+' '+t('migrate.sessions.count').replace('{n}',sess.messages);\n document.getElementById('migrateScanResult').style.display='block';\n\n if(!d.configReady){\n document.getElementById('migrateConfigWarn').style.display='block';\n const parts=[];\n if(!d.hasEmbedding) parts.push('Embedding');\n if(!d.hasSummarizer) parts.push('Summarizer');\n document.getElementById('migrateConfigWarn').querySelector('div:last-child').textContent=\n t('migrate.config.warn.desc')+' ('+parts.join(', ')+')';\n }\n\n if(d.totalItems>0 && d.configReady){\n document.getElementById('migrateStartBtn').style.display='inline-flex';\n }\n\n if(d.totalItems===0){\n document.getElementById('migrateStatus').textContent=t('migrate.nodata');\n }\n\n if(d.hasImportedData){\n document.getElementById('postprocessSection').style.display='block';\n }\n }catch(e){\n toast('Scan failed: '+e.message,'error');\n }finally{\n btn.disabled=false;\n btn.textContent=t('migrate.scan');\n }\n}\n\nfunction migrateStart(){\n if(!migrateScanData||!migrateScanData.configReady)return;\n if(!confirm(t('migrate.start')+'?'))return;\n\n window._migrateRunning=true;\n _migrateStatusChecked=false;\n document.getElementById('migrateStartBtn').style.display='none';\n document.getElementById('migrateScanBtn').disabled=true;\n document.getElementById('migrateProgress').style.display='block';\n document.getElementById('migrateLiveLog').innerHTML='';\n migrateStats={stored:0,skipped:0,merged:0,errors:0};\n updateMigrateStats();\n\n document.getElementById('migrateStopBtn').disabled=false;\n document.getElementById('migrateBar').style.width='0%';\n document.getElementById('migrateBar').style.background='linear-gradient(90deg,#6366f1,#8b5cf6)';\n document.getElementById('migrateCounter').textContent='';\n const body=JSON.stringify({sources:['sqlite','sessions']});\n connectMigrateSSE('/api/migrate/start','POST',body);\n}\n\nasync function migrateStop(){\n const btn=document.getElementById('migrateStopBtn');\n btn.disabled=true;\n btn.textContent=t('migrate.stopping');\n try{\n await fetch('/api/migrate/stop',{method:'POST'});\n }catch(e){\n toast('Stop failed: '+e.message,'error');\n btn.disabled=false;\n btn.textContent=t('migrate.stop');\n }\n}\n\nfunction connectMigrateSSE(url,method,body){\n const opts={method:method||'GET'};\n if(body){opts.headers={'Content-Type':'application/json'};opts.body=body;}\n fetch(url,opts)\n .then(r=>{\n if(!r.ok){toast('Migration request failed: '+r.status,'error');onMigrateDone(false);return;}\n readSSEStream(r);\n })\n .catch(e=>{toast('Migration failed: '+e.message,'error');onMigrateDone(false);});\n}\n\nfunction readSSEStream(r){\n const reader=r.body.getReader();\n const decoder=new TextDecoder();\n let buf='';\n let migrateDoneCalled=false;\n const NL=String.fromCharCode(10);\n function pump(){\n reader.read().then(({done,value})=>{\n if(done){if(!migrateDoneCalled&&!window._migrateRunning)onMigrateDone(false);return;}\n buf+=decoder.decode(value,{stream:true});\n const lines=buf.split(NL);\n buf=lines.pop()||'';\n let evtType='';\n for(const line of lines){\n if(line.startsWith('event: ')){evtType=line.slice(7).trim();}\n else if(line.startsWith('data: ')){\n try{\n const data=JSON.parse(line.slice(6));\n if(evtType==='done'||evtType==='stopped') migrateDoneCalled=true;\n handleMigrateEvent(evtType,data);\n }catch{}\n }\n }\n pump();\n });\n }\n pump();\n}\n\nvar _migrateStatusChecked=false;\nasync function checkMigrateStatus(){\n if(_migrateStatusChecked) return;\n _migrateStatusChecked=true;\n try{\n const r=await fetch('/api/migrate/status');\n if(!r.ok)return;\n const s=await r.json();\n if(s.running){\n window._migrateRunning=true;\n switchView('import');\n migrateStats={stored:s.stored,skipped:s.skipped,merged:s.merged,errors:s.errors};\n updateMigrateStats();\n const progEl=document.getElementById('migrateProgress');\n if(!progEl)return;\n progEl.style.display='block';\n document.getElementById('migrateStartBtn').style.display='none';\n document.getElementById('migrateScanBtn').disabled=true;\n document.getElementById('migrateStopBtn').disabled=false;\n const pct=s.total>0?Math.round((s.processed/s.total)*100):0;\n document.getElementById('migrateBar').style.width=pct+'%';\n document.getElementById('migrateCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';\n const label=s.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');\n document.getElementById('migratePhaseLabel').textContent=label;\n connectMigrateSSE('/api/migrate/stream','GET',null);\n }else if(s.done&&(s.stored>0||s.skipped>0||s.stopped)){\n migrateStats={stored:s.stored,skipped:s.skipped,merged:s.merged,errors:s.errors};\n updateMigrateStats();\n const progEl=document.getElementById('migrateProgress');\n if(!progEl)return;\n progEl.style.display='block';\n const pct=s.total>0?Math.round((s.processed/s.total)*100):0;\n document.getElementById('migrateBar').style.width=pct+'%';\n document.getElementById('migrateCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';\n onMigrateDone(!!s.stopped,true);\n }\n }catch(e){console.log('checkMigrateStatus error',e);}\n}\n\nfunction handleMigrateEvent(evtType,data){\n if(evtType==='phase'){\n const label=data.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');\n document.getElementById('migratePhaseLabel').textContent=label;\n }\n else if(evtType==='progress'){\n document.getElementById('migrateCounter').textContent=data.processed+' / '+data.total;\n }\n else if(evtType==='item'){\n if(data.status==='stored')migrateStats.stored++;\n else if(data.status==='skipped'||data.status==='duplicate')migrateStats.skipped++;\n else if(data.status==='merged')migrateStats.merged++;\n else if(data.status==='error')migrateStats.errors++;\n updateMigrateStats();\n\n const pct=data.total>0?Math.round((data.index/data.total)*100):0;\n document.getElementById('migrateBar').style.width=pct+'%';\n document.getElementById('migrateCounter').textContent=data.index+' / '+data.total+' ('+pct+'%)';\n\n appendMigrateLogItem(data);\n }\n else if(evtType==='error'){\n migrateStats.errors++;\n updateMigrateStats();\n appendMigrateLogItem({status:'error',preview:data.error||data.file,source:data.file});\n }\n else if(evtType==='summary'){\n document.getElementById('migrateBar').style.width='100%';\n }\n else if(evtType==='done'){\n onMigrateDone(false);\n }\n else if(evtType==='stopped'){\n onMigrateDone(true);\n }\n else if(evtType==='state'){\n migrateStats={stored:data.stored||0,skipped:data.skipped||0,merged:data.merged||0,errors:data.errors||0};\n updateMigrateStats();\n const pct=data.total>0?Math.round((data.processed/data.total)*100):0;\n document.getElementById('migrateBar').style.width=pct+'%';\n document.getElementById('migrateCounter').textContent=data.processed+' / '+data.total+' ('+pct+'%)';\n if(data.phase){\n const label=data.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');\n document.getElementById('migratePhaseLabel').textContent=label;\n }\n }\n}\n\nfunction updateMigrateStats(){\n document.getElementById('migrateStatStored').textContent=migrateStats.stored;\n document.getElementById('migrateStatSkipped').textContent=migrateStats.skipped;\n document.getElementById('migrateStatMerged').textContent=migrateStats.merged;\n document.getElementById('migrateStatErrors').textContent=migrateStats.errors;\n}\n\nfunction appendMigrateLogItem(data){\n const log=document.getElementById('migrateLiveLog');\n const icons={stored:'\\u2705',skipped:'\\u23ED',merged:'\\u{1F500}',error:'\\u274C',duplicate:'\\u23ED'};\n const statusClass=data.status==='duplicate'?'skipped':data.status;\n const el=document.createElement('div');\n el.className='migrate-log-item';\n el.innerHTML=\n '<div class=\"log-icon '+statusClass+'\">'+( icons[data.status]||'\\u2022')+'</div>'+\n '<div class=\"log-body\">'+\n '<div class=\"log-preview\">'+esc(data.preview||'')+'</div>'+\n '<div class=\"log-meta\">'+\n '<span class=\"tag '+statusClass+'\">'+(data.status||'').toUpperCase()+'</span>'+\n (data.source?'<span>'+esc(data.source)+'</span>':'')+\n (data.role?'<span>'+data.role+'</span>':'')+\n (data.summary?'<span style=\"opacity:.7\">'+esc(data.summary)+'</span>':'')+\n '</div>'+\n '</div>';\n log.appendChild(el);\n log.scrollTop=log.scrollHeight;\n}\n\nfunction onMigrateDone(wasStopped,skipReload){\n window._migrateRunning=false;\n document.getElementById('migrateScanBtn').disabled=false;\n document.getElementById('migrateStopBtn').disabled=true;\n document.getElementById('migrateStopBtn').textContent=t('migrate.stop');\n if(wasStopped){\n document.getElementById('migrateBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';\n document.getElementById('migrateStartBtn').style.display='inline-flex';\n document.getElementById('migrateStartBtn').textContent=t('migrate.resume');\n }else{\n document.getElementById('migrateBar').style.width='100%';\n document.getElementById('migrateBar').style.background='linear-gradient(90deg,#22c55e,#16a34a)';\n }\n fetch('/api/migrate/scan').then(r=>{if(!r.ok)throw new Error();return r.json()}).then(d=>{\n if(d&&d.hasImportedData) document.getElementById('postprocessSection').style.display='block';\n }).catch(()=>{});\n if(!skipReload) loadAll();\n}\n\n/* \u2500\u2500\u2500 Post-processing: tasks & skills \u2500\u2500\u2500 */\n\nvar ppStats={tasks:0,skills:0,errors:0,skipped:0};\nwindow._ppRunning=false;\n\nfunction ppStart(){\n var enableTasks=document.getElementById('ppEnableTasks').checked;\n var enableSkills=document.getElementById('ppEnableSkills').checked;\n if(!enableTasks&&!enableSkills){toast(t('pp.select.warn'),'error');return;}\n\n window._ppRunning=true;\n _ppSSEConnected=false;\n ppStats={tasks:0,skills:0,errors:0,skipped:0};\n document.getElementById('ppStartBtn').style.display='none';\n document.getElementById('ppStopBtn').style.display='inline-flex';\n document.getElementById('ppStopBtn').disabled=false;\n document.getElementById('ppStopBtn').textContent=t('migrate.stop');\n document.getElementById('ppProgress').style.display='block';\n document.getElementById('ppDone').style.display='none';\n document.getElementById('ppBar').style.width='0%';\n document.getElementById('ppBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';\n document.getElementById('ppPhaseLabel').textContent=t('pp.running');\n document.getElementById('ppCounter').textContent='';\n document.getElementById('ppLiveLog').innerHTML='';\n updatePPStats();\n\n var body=JSON.stringify({enableTasks:enableTasks,enableSkills:enableSkills});\n fetch('/api/migrate/postprocess',{method:'POST',headers:{'Content-Type':'application/json'},body:body})\n .then(function(r){\n if(!r.ok){\n r.json().then(function(j){toast(j.error||('Postprocess failed: '+r.status),'error');}).catch(function(){toast('Postprocess failed: '+r.status,'error');});\n ppDone(false,true);\n return;\n }\n readPPStream(r.body.getReader());\n })\n .catch(function(e){toast('Postprocess failed: '+e.message,'error');ppDone(false,true);});\n}\n\nfunction updatePPStats(){\n document.getElementById('ppStatTasks').textContent=ppStats.tasks;\n document.getElementById('ppStatSkills').textContent=ppStats.skills;\n document.getElementById('ppStatErrors').textContent=ppStats.errors;\n document.getElementById('ppStatSkipped').textContent=ppStats.skipped;\n}\n\nfunction appendPPLogItem(data){\n var log=document.getElementById('ppLiveLog');\n var el=document.createElement('div');\n el.style.cssText='display:flex;align-items:flex-start;gap:8px;padding:6px 12px;border-bottom:1px solid var(--border)';\n var icon='\\u2022';var color='var(--text-muted)';\n if(data.step==='done'){icon='\\u2705';color='#22c55e';}\n else if(data.step==='error'){icon='\\u274C';color='#ef4444';}\n else if(data.step==='processing'){icon='\\u23F3';color='#f59e0b';}\n else if(data.step==='skipped'){icon='\\u23ED';color='#3b82f6';}\n else if(data.step==='skill'){icon='\\u{1F9E0}';color='#8b5cf6';}\n var label=data.taskTitle||data.session||data.title||'';\n if(label.length>60)label=label.slice(0,57)+'...';\n el.innerHTML='<span style=\"color:'+color+';min-width:18px\">'+icon+'</span>'+\n '<span style=\"flex:1;color:var(--text-sec)\">'+esc(label)+'</span>'+\n '<span style=\"color:var(--text-muted);font-size:10px\">'+(data.index||'')+' / '+(data.total||'')+'</span>';\n if(data.error) el.innerHTML+='<span style=\"color:#ef4444;font-size:10px\">'+esc(data.error)+'</span>';\n log.appendChild(el);\n log.scrollTop=log.scrollHeight;\n}\n\nfunction readPPStream(reader){\n var NL=String.fromCharCode(10);\n var dec=new TextDecoder();\n var buf='';\n var ppDoneCalled=false;\n function pump(){\n reader.read().then(function(result){\n if(result.done){if(!ppDoneCalled)ppDone(false);return;}\n buf+=dec.decode(result.value,{stream:true});\n var lines=buf.split(NL);\n buf=lines.pop()||'';\n var evtType='';\n for(var i=0;i<lines.length;i++){\n var line=lines[i];\n if(line.startsWith('event: '))evtType=line.slice(7).trim();\n else if(line.startsWith('data: ')&&evtType){\n try{\n if(evtType==='done'||evtType==='stopped')ppDoneCalled=true;\n handlePPEvent(evtType,JSON.parse(line.slice(6)));\n }catch(e){}\n evtType='';\n }\n }\n pump();\n }).catch(function(){if(!ppDoneCalled)ppDone(false);});\n }\n pump();\n}\n\nvar _ppSSEConnected=false;\nfunction connectPPSSE(){\n if(_ppSSEConnected) return;\n _ppSSEConnected=true;\n fetch('/api/migrate/postprocess/status').then(function(r){return r.json();}).then(function(s){\n if(s.running){\n window._ppRunning=true;\n document.getElementById('postprocessSection').style.display='block';\n document.getElementById('ppStartBtn').style.display='none';\n document.getElementById('ppStopBtn').style.display='inline-flex';\n document.getElementById('ppStopBtn').disabled=false;\n document.getElementById('ppStopBtn').textContent=t('migrate.stop');\n document.getElementById('ppProgress').style.display='block';\n document.getElementById('ppDone').style.display='none';\n ppStats={tasks:s.tasksCreated||0,skills:s.skillsCreated||0,errors:s.errors||0,skipped:0};\n updatePPStats();\n var pct=s.total>0?Math.round((s.processed/s.total)*100):0;\n document.getElementById('ppBar').style.width=pct+'%';\n document.getElementById('ppCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';\n document.getElementById('ppPhaseLabel').textContent=t('pp.running');\n fetch('/api/migrate/postprocess/stream',{method:'GET'}).then(function(r){\n if(r.ok&&r.body)readPPStream(r.body.getReader());\n }).catch(function(){});\n }else if(s.done){\n document.getElementById('postprocessSection').style.display='block';\n ppStats={tasks:s.tasksCreated||0,skills:s.skillsCreated||0,errors:s.errors||0,skipped:0};\n updatePPStats();\n document.getElementById('ppProgress').style.display='block';\n var pct2=s.total>0?Math.round((s.processed/s.total)*100):0;\n document.getElementById('ppBar').style.width=pct2+'%';\n document.getElementById('ppCounter').textContent=s.processed+' / '+s.total+' ('+pct2+'%)';\n ppDone(!!s.stopped,false,true);\n }\n }).catch(function(){});\n}\n\nfunction handlePPEvent(evtType,data){\n if(evtType==='progress'){\n var pct=data.total>0?Math.round((data.processed/data.total)*100):0;\n document.getElementById('ppBar').style.width=pct+'%';\n document.getElementById('ppCounter').textContent=data.processed+' / '+data.total+' ('+pct+'%)';\n }else if(evtType==='info'){\n if(data.alreadyProcessed>0){\n ppStats.skipped=data.alreadyProcessed;\n updatePPStats();\n appendPPLogItem({step:'skipped',session:t('pp.info.skipped').replace('{n}',data.alreadyProcessed),index:'',total:''});\n }\n if(data.pending===0){\n appendPPLogItem({step:'done',session:t('pp.info.allDone'),index:'',total:''});\n }else{\n document.getElementById('ppPhaseLabel').textContent=t('pp.info.pending').replace('{n}',data.pending);\n }\n }else if(evtType==='item'){\n var label=data.session||'';\n if(label.length>40)label=label.slice(0,37)+'...';\n if(data.step==='processing'){\n var actionLabel=data.action==='skill-only'?t('pp.action.skillOnly'):t('pp.action.full');\n document.getElementById('ppPhaseLabel').textContent=t('pp.running')+' \u2014 '+actionLabel+' \u2014 '+label;\n }\n if(data.step==='done'){\n if(data.action==='skill-only'){\n ppStats.skills++;\n }else{\n ppStats.tasks++;\n }\n updatePPStats();\n }else if(data.step==='error'){\n ppStats.errors++;\n updatePPStats();\n }\n appendPPLogItem(data);\n }else if(evtType==='skill'){\n ppStats.skills++;\n updatePPStats();\n appendPPLogItem({step:'skill',title:data.title,index:'',total:''});\n }else if(evtType==='done'){\n ppDone(false);\n }else if(evtType==='stopped'){\n ppDone(true);\n }\n}\n\nfunction ppStop(){\n document.getElementById('ppStopBtn').disabled=true;\n document.getElementById('ppStopBtn').textContent=t('migrate.stopping');\n fetch('/api/migrate/postprocess/stop',{method:'POST'}).catch(function(){});\n}\n\nfunction ppDone(wasStopped,wasFailed,skipReload){\n window._ppRunning=false;\n document.getElementById('ppStopBtn').style.display='none';\n document.getElementById('ppStartBtn').style.display='inline-flex';\n document.getElementById('ppStartBtn').textContent=wasStopped?t('pp.resume'):t('pp.start');\n var doneEl=document.getElementById('ppDone');\n doneEl.style.display='block';\n if(wasFailed){\n doneEl.style.background='rgba(239,68,68,.06)';\n doneEl.style.color='#ef4444';\n doneEl.textContent=t('pp.failed')||'Processing failed \u2014 check error above';\n document.getElementById('ppBar').style.background='linear-gradient(90deg,#ef4444,#dc2626)';\n }else if(wasStopped){\n doneEl.style.background='rgba(245,158,11,.06)';\n doneEl.style.color='#f59e0b';\n doneEl.textContent=t('pp.stopped');\n document.getElementById('ppBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';\n }else{\n doneEl.style.background='rgba(34,197,94,.06)';\n doneEl.style.color='#22c55e';\n doneEl.textContent=t('pp.done')+' ('+t('pp.stat.tasks')+': '+ppStats.tasks+', '+t('pp.stat.skills')+': '+ppStats.skills+')';\n document.getElementById('ppBar').style.width='100%';\n document.getElementById('ppBar').style.background='linear-gradient(90deg,#22c55e,#16a34a)';\n }\n if(!skipReload) loadAll();\n}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\nfunction toast(msg,type='info'){\n const c=document.getElementById('toasts');\n const t=document.createElement('div');\n t.className='toast '+type;\n const icons={success:'\\u2705',error:'\\u274C',info:'\\u2139\\uFE0F'};\n t.innerHTML=(icons[type]||'')+' '+esc(msg);\n c.appendChild(t);\n setTimeout(()=>t.remove(),3500);\n}\n\n/* \u2500\u2500\u2500 Theme \u2500\u2500\u2500 */\nconst VIEWER_THEME_KEY='memos-viewer-theme';\nfunction initViewerTheme(){const s=localStorage.getItem(VIEWER_THEME_KEY);const theme=(s==='light'||s==='dark')?s:'dark';document.documentElement.setAttribute('data-theme',theme);}\nfunction toggleViewerTheme(){const el=document.documentElement;const cur=el.getAttribute('data-theme')||'dark';const next=cur==='dark'?'light':'dark';el.setAttribute('data-theme',next);localStorage.setItem(VIEWER_THEME_KEY,next);}\ninitViewerTheme();\n\n/* \u2500\u2500\u2500 Init \u2500\u2500\u2500 */\ndocument.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});\ndocument.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});\napplyI18n();\ncheckAuth();\n</script>\n\n<!-- Memory Detail Modal -->\n<div class=\"memory-modal-overlay\" id=\"memoryModal\" onclick=\"if(event.target===this)closeMemoryModal()\">\n <div class=\"memory-modal\">\n <div class=\"memory-modal-title\">\n <span>Memory Detail</span>\n <button class=\"btn btn-sm btn-ghost\" onclick=\"closeMemoryModal()\" style=\"font-size:16px;padding:2px 8px\">&times;</button>\n </div>\n <div class=\"memory-modal-body\" id=\"memoryModalBody\"></div>\n </div>\n</div>\n\n</body>\n</html>";
1
+ export declare const viewerHTML = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>OpenClaw Memory - Powered by MemOS</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\n:root{\n --bg:#0b0d11;--bg-card:#12141a;--bg-card-hover:#1a1d25;\n --border:rgba(255,255,255,.08);--border-glow:rgba(255,255,255,.14);\n --text:#e8eaed;--text-sec:#8b8fa4;--text-muted:#555a6e;\n --pri:#818cf8;--pri-glow:rgba(129,140,248,.1);--pri-dark:#6366f1;\n --pri-grad:linear-gradient(135deg,#818cf8,#6366f1);\n --accent:#ef4444;--accent-glow:rgba(239,68,68,.1);\n --green:#34d399;--green-bg:rgba(52,211,153,.08);\n --amber:#fbbf24;--amber-bg:rgba(251,191,36,.08);\n --violet:#818cf8;--rose:#ef4444;--rose-bg:rgba(239,68,68,.08);\n --shadow-sm:0 1px 2px rgba(0,0,0,.3);--shadow:0 4px 12px rgba(0,0,0,.35);\n --shadow-lg:0 20px 40px rgba(0,0,0,.45);\n --radius:12px;--radius-lg:14px;--radius-xl:18px;\n}\n[data-theme=\"light\"]{\n --bg:#f8f9fb;--bg-card:#fff;--bg-card-hover:#f3f4f6;\n --border:#e2e4e9;--border-glow:#cbd0d8;\n --text:#111827;--text-sec:#4b5563;--text-muted:#9ca3af;\n --pri:#4f46e5;--pri-glow:rgba(79,70,229,.06);--pri-dark:#4338ca;\n --pri-grad:linear-gradient(135deg,#4f46e5,#4338ca);\n --accent:#dc2626;--accent-glow:rgba(220,38,38,.06);\n --green:#059669;--green-bg:rgba(5,150,105,.06);\n --amber:#d97706;--amber-bg:rgba(217,119,6,.06);\n --violet:#4f46e5;--rose:#dc2626;--rose-bg:rgba(220,38,38,.06);\n --shadow-sm:0 1px 2px rgba(0,0,0,.04);--shadow:0 4px 12px rgba(0,0,0,.06);\n --shadow-lg:0 20px 40px rgba(0,0,0,.1);\n}\n[data-theme=\"light\"] .auth-screen{background:linear-gradient(135deg,#f0f4ff 0%,#f8f9fb 50%,#eef2ff 100%)}\n[data-theme=\"light\"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.08)}\n[data-theme=\"light\"] .topbar{background:rgba(255,255,255,.92);border-bottom-color:var(--border);backdrop-filter:blur(8px)}\n[data-theme=\"light\"] .session-item .count,[data-theme=\"light\"] .kind-tag,[data-theme=\"light\"] .session-tag{background:rgba(0,0,0,.05)}\n[data-theme=\"light\"] .card-content pre{background:#f3f4f6;border-color:var(--border)}\n[data-theme=\"light\"] .vscore-badge{background:rgba(79,70,229,.06);color:#4f46e5}\n[data-theme=\"light\"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15)}\n[data-theme=\"light\"] .analytics-card{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.06);border:1px solid var(--border)}\n[data-theme=\"light\"] .analytics-card::before{background:none}\n[data-theme=\"light\"] .analytics-card::after{display:none}\n[data-theme=\"light\"] .analytics-card:hover{box-shadow:0 4px 16px rgba(0,0,0,.08);transform:translateY(-2px)}\n[data-theme=\"light\"] .analytics-card.green{background:#fff;border-color:var(--border)}\n[data-theme=\"light\"] .analytics-card.green::before{background:none}\n[data-theme=\"light\"] .analytics-card.amber{background:#fff;border-color:var(--border)}\n[data-theme=\"light\"] .analytics-card.amber::before{background:none}\n[data-theme=\"light\"] .analytics-card .ac-value{-webkit-text-fill-color:unset;background:none;color:#111827}\n[data-theme=\"light\"] .analytics-card.green .ac-value{color:#059669}\n[data-theme=\"light\"] .analytics-card.amber .ac-value{color:#d97706}\n[data-theme=\"light\"] .analytics-section{background:#fff;border-color:var(--border);box-shadow:0 1px 3px rgba(0,0,0,.04)}\n[data-theme=\"light\"] .analytics-section::before{background:none}\n[data-theme=\"light\"] .chart-bar{box-shadow:none}\n[data-theme=\"light\"] .chart-bar:hover{box-shadow:0 2px 8px rgba(79,70,229,.15)}\n[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)}\n[data-theme=\"light\"] .tool-chart-tooltip .tt-time{color:#a5b4fc}\n[data-theme=\"light\"] .tool-chart-tooltip .tt-val{color:#e8eaed}\n[data-theme=\"light\"] .tool-agg-table td{background:transparent}\n[data-theme=\"light\"] .tool-agg-table tr:hover td{background:rgba(79,70,229,.03)}\n[data-theme=\"light\"] .tool-agg-table th{color:#9ca3af}\n[data-theme=\"light\"] .breakdown-item{background:#f9fafb;border-color:var(--border)}\n[data-theme=\"light\"] .breakdown-item:hover{background:#f3f4f6;border-color:#cbd5e1}\n[data-theme=\"light\"] .breakdown-bar-wrap{background:#e5e7eb}\n[data-theme=\"light\"] .breakdown-bar{background:linear-gradient(90deg,#4f46e5,#6366f1);box-shadow:none}\n[data-theme=\"light\"] .range-btn{background:transparent;border-color:var(--border);color:var(--text-sec)}\n[data-theme=\"light\"] .range-btn.active{background:rgba(79,70,229,.06);color:#4f46e5;border-color:rgba(79,70,229,.2)}\n[data-theme=\"light\"] .range-btn:hover{border-color:#4f46e5;color:#4f46e5}\nbody{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .2s,color .2s}\nbutton{cursor:pointer;font-family:inherit;font-size:inherit}\ninput,textarea,select{font-family:inherit;font-size:inherit}\n\n/* \u2500\u2500\u2500 Auth (Linkify \u914D\u8272: globals.css .dark + \u84DD\u7D2B\u6E10\u53D8) \u2500\u2500\u2500 */\n.auth-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;background:linear-gradient(135deg,rgb(36,0,255) 0%,rgb(0,135,255) 35%,rgb(108,39,157) 70%,rgb(105,30,255) 100%);position:relative;overflow:hidden}\n.auth-card{background:hsl(0 0% 100%);border:none;border-radius:8px;padding:48px 40px;width:100%;max-width:420px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);text-align:center;position:relative;z-index:1}\n.auth-card .logo{margin:0 auto 20px;text-align:center;line-height:0;background:none;border-radius:0}\n.auth-card .logo svg{filter:drop-shadow(0 0 16px rgba(255,77,77,.35))}\n.auth-card h1{font-size:22px;font-weight:700;margin-bottom:4px;color:hsl(0 0% 3.9%);letter-spacing:-.02em}\n.auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}\n.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%)}\n.auth-card input::placeholder{color:hsl(0 0% 45.1%)}\n.auth-card input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.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}\n.auth-card .btn-auth:hover{background:rgba(99,102,241,.12);border-color:var(--pri-dark)}\n.auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}\n.auth-card .btn-text{color:hsl(0 0% 45.1%)}\n.auth-card .btn-text:hover{color:var(--pri)}\n\n.reset-guide{text-align:left;margin-bottom:20px}\n.reset-step{display:flex;gap:14px;margin-bottom:16px}\n.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}\n.step-body{flex:1;min-width:0}\n.step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}\n.step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}\n.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%)}\n.cmd-box:hover{border-color:hsl(0 0% 70%);background:hsl(0 0% 96.1%)}\n.cmd-box code{flex:1}\n.copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}\n.cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}\n\n/* \u2500\u2500\u2500 App Layout (dark dashboard, same as www) \u2500\u2500\u2500 */\n.app{display:none;flex-direction:column;min-height:100vh}\n.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)}\n.topbar .brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}\n.topbar .brand .icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:22px;background:none;border-radius:0}\n.topbar .brand .sub{font-weight:400;color:var(--text-muted);font-size:11px}\n.topbar-center{flex:1;display:flex;justify-content:center}\n.topbar .actions{display:flex;align-items:center;gap:6px;flex-shrink:0}\n\n.main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}\n\n/* \u2500\u2500\u2500 Sidebar \u2500\u2500\u2500 */\n.sidebar{width:260px;flex-shrink:0}\n.sidebar .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px}\n.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;transition:all .2s}\n.stat-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.stat-card .stat-value{font-size:22px;font-weight:700;color:var(--text);letter-spacing:-.02em}\n.stat-card .stat-label{font-size:12px;color:var(--text-sec);margin-top:4px;font-weight:500}\n.stat-card.pri .stat-value{color:var(--pri)}\n.stat-card.green .stat-value{color:var(--green)}\n.stat-card.amber .stat-value{color:var(--amber)}\n.stat-card.rose .stat-value{color:var(--rose)}\n\n.sidebar .section-title{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin:24px 0 12px;padding:0 2px}\n.sidebar .session-list{display:flex;flex-direction:column;gap:6px;max-height:280px;overflow-y:auto}\n.session-item{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;font-size:13px;color:var(--text)}\n.session-item:hover{border-color:var(--pri);background:var(--pri-glow)}\n.session-item.active{border-color:var(--pri);background:var(--pri-glow);font-weight:600;color:var(--pri)}\n.session-item .count{color:var(--text-sec);font-size:11px;font-weight:600;background:rgba(0,0,0,.2);padding:3px 8px;border-radius:8px}\n\n.provider-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--green-bg);color:var(--green);border-radius:999px;font-size:11px;font-weight:600;margin-top:10px}\n.provider-badge.offline{background:var(--amber-bg);color:var(--amber)}\n\n/* \u2500\u2500\u2500 Feed \u2500\u2500\u2500 */\n.feed{flex:1;min-width:0}\n.search-bar{display:flex;gap:10px;margin-bottom:16px;position:relative;align-items:center}\n.search-bar input{flex:1;padding:10px 16px 10px 40px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;background:var(--bg-card);color:var(--text);transition:all .2s}\n.search-bar input::placeholder{color:var(--text-muted)}\n.search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.search-bar .search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:14px;pointer-events:none}\n.search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}\n\n.filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}\n.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}\n.filter-chip:hover{border-color:var(--pri);color:var(--pri)}\n.filter-chip.active{background:rgba(99,102,241,.08);color:var(--pri);border-color:rgba(99,102,241,.25)}\n\n.memory-list{display:flex;flex-direction:column;gap:16px}\n.memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}\n.memory-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}\n.memory-card .meta{display:flex;align-items:center;gap:8px}\n.role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}\n.role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(99,102,241,.12)}\n.role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}\n.role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}\n.kind-tag{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-sec);background:rgba(0,0,0,.2);font-weight:500}\n.card-time{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:8px}\n.session-tag{font-size:11px;font-family:ui-monospace,monospace;color:var(--text-muted);background:rgba(0,0,0,.2);padding:3px 8px;border-radius:6px;cursor:default}\n.card-summary{font-size:15px;font-weight:600;color:var(--text);margin-bottom:10px;line-height:1.5;letter-spacing:-.01em}\n.card-content{font-size:13px;color:var(--text-sec);line-height:1.65;max-height:0;overflow:hidden;transition:max-height .3s ease}\n.card-content.show{max-height:600px;overflow-y:auto}\n.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)}\n.card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}\n.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}\n.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}\n.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}\n.merge-history-item{padding:6px 0;border-bottom:1px dashed rgba(255,255,255,.06)}\n.merge-history-item:last-child{border-bottom:none}\n.merge-action{font-weight:600;font-size:11px;padding:2px 6px;border-radius:4px}\n.merge-action.UPDATE{background:rgba(59,130,246,.15);color:#60a5fa}\n.merge-action.DUPLICATE{background:rgba(245,158,11,.15);color:#f59e0b}\n.card-updated{font-size:11px;color:var(--text-muted);margin-left:6px}\n.dedup-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}\n.dedup-badge.duplicate{background:rgba(245,158,11,.12);color:#f59e0b}\n.dedup-badge.merged{background:rgba(59,130,246,.12);color:#60a5fa}\n.import-badge{display:inline-flex;align-items:center;gap:4px;background:rgba(236,72,153,.1);color:#ec4899;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}\n[data-theme=\"light\"] .import-badge{background:rgba(219,39,119,.08);color:#db2777}\n.memory-card.dedup-inactive{opacity:.55;border-style:dashed}\n.memory-card.dedup-inactive:hover{opacity:.85}\n.dedup-target-link{font-size:11px;color:var(--pri);cursor:pointer;text-decoration:underline;margin-left:4px}\n.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)}\n.memory-modal-overlay.show{display:flex}\n.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}\n@keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}\n.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}\n.memory-modal-body{padding:20px;overflow-y:auto;flex:1}\n.modal-memory-card{display:flex;flex-direction:column;gap:14px}\n.modal-header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.modal-field{display:flex;flex-direction:column;gap:4px}\n.modal-field-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec)}\n.modal-field-val{font-size:13px;color:var(--text);line-height:1.5}\n.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}\n[data-theme=\"light\"] .modal-field-content{background:rgba(0,0,0,.04)}\n.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)}\n[data-theme=\"light\"] .merge-history{background:rgba(0,0,0,.04)}\n[data-theme=\"light\"] .merge-history-item{border-bottom-color:rgba(0,0,0,.06)}\n\n/* \u2500\u2500\u2500 Buttons \u2500\u2500\u2500 */\n.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}\n.btn:hover{border-color:var(--pri);color:var(--pri)}\n.btn-primary{background:rgba(255,255,255,.08);color:var(--text);border:1px solid var(--border);font-weight:600}\n.btn-primary:hover{background:rgba(255,255,255,.14);transform:translateY(-1px);border-color:var(--pri);color:var(--pri)}\n.btn-ghost{border-color:transparent;background:transparent;color:var(--text-sec)}\n.btn-ghost:hover{background:rgba(255,255,255,.06);color:var(--text)}\n.btn-danger{color:var(--accent);border-color:rgba(230,57,70,.25)}\n.btn-danger:hover{background:rgba(230,57,70,.1);color:var(--accent)}\n.btn-sm{padding:5px 12px;font-size:12px}\n.btn-icon{padding:5px 7px;font-size:15px;border-radius:8px}\n.btn-text{border:none;background:none;color:var(--text-muted);font-size:12px;padding:4px 8px}\n.btn-text:hover{color:var(--pri)}\n[data-theme=\"light\"] .btn-primary{background:rgba(0,0,0,.05);color:var(--text);border-color:rgba(0,0,0,.12)}\n[data-theme=\"light\"] .btn-primary:hover{background:rgba(0,0,0,.08);border-color:var(--pri);color:var(--pri)}\n[data-theme=\"light\"] .btn-ghost{color:var(--text-sec)}\n[data-theme=\"light\"] .btn-ghost:hover{background:rgba(0,0,0,.04);color:var(--text)}\n\n/* \u2500\u2500\u2500 Modal \u2500\u2500\u2500 */\n.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:500;align-items:center;justify-content:center;backdrop-filter:blur(8px)}\n.modal-overlay.show{display:flex}\n.modal{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);padding:32px;width:100%;max-width:520px;box-shadow:var(--shadow-lg);max-height:85vh;overflow-y:auto}\n.modal h2{font-size:20px;font-weight:700;margin-bottom:24px;color:var(--text);letter-spacing:-.02em}\n.form-group{margin-bottom:18px}\n.form-group label{display:block;font-size:13px;font-weight:600;color:var(--text-sec);margin-bottom:6px}\n.form-group input,.form-group textarea,.form-group select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;transition:all .2s;background:var(--bg-card);color:var(--text)}\n.form-group input::placeholder,.form-group textarea::placeholder{color:var(--text-muted)}\n.form-group input:focus,.form-group textarea:focus,.form-group select:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.form-group textarea{min-height:100px;resize:vertical}\n.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:28px}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\n.toast-container{position:fixed;top:80px;right:24px;z-index:1000;display:flex;flex-direction:column;gap:8px}\n.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}\n.toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}\n.toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}\n.toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(99,102,241,.15)}\n@keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}\n\n.empty{text-align:center;padding:64px 20px;color:var(--text-sec)}\n.empty .icon{font-size:52px;margin-bottom:16px;opacity:.5}\n.empty p{font-size:15px;font-weight:500}\n\n.spinner{width:40px;height:40px;border:3px solid var(--border);border-top-color:var(--pri);border-radius:50%;animation:spin .8s linear infinite;margin:48px auto}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n::-webkit-scrollbar{width:6px;height:6px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.25)}\n\n.filter-sep{width:1px;height:20px;background:var(--border);margin:0 4px}\n.filter-select{padding:6px 12px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;outline:none;cursor:pointer}\n.filter-select:focus{border-color:var(--pri)}\n.date-filter{display:flex;align-items:center;gap:10px;margin-bottom:18px;font-size:13px;color:var(--text-sec)}\n.date-filter input[type=\"datetime-local\"]{padding:6px 10px;border:1px solid var(--border);border-radius:8px;font-size:12px;outline:none;background:var(--bg-card);color:var(--text)}\n.date-filter input[type=\"datetime-local\"]:focus{border-color:var(--pri)}\n.date-filter label{font-weight:500}\n\n.pagination{display:flex;align-items:center;justify-content:center;gap:6px;padding:28px 0;flex-wrap:wrap}\n.pagination .pg-btn{min-width:38px;height:38px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s}\n.pagination .pg-btn:hover{border-color:var(--pri);color:var(--pri)}\n.pagination .pg-btn.active{background:var(--pri);color:#000;border-color:var(--pri)}\n.pagination .pg-btn.disabled{opacity:.4;pointer-events:none}\n.pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}\n\n/* \u2500\u2500\u2500 Tasks \u89C6\u56FE \u2500\u2500\u2500 */\n.tasks-view{display:none;flex:1;min-width:0;flex-direction:column;gap:16px}\n.tasks-view.show{display:flex}\n.tasks-header{display:flex;flex-direction:column;gap:14px}\n.tasks-stats{display:flex;gap:16px}\n.tasks-stat{display:flex;align-items:center;gap:8px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 18px;flex:1;transition:all .2s}\n.tasks-stat:hover{border-color:var(--border-glow)}\n.tasks-stat-value{font-size:22px;font-weight:700;color:var(--text)}\n.tasks-stat-label{font-size:12px;color:var(--text-sec);font-weight:500}\n.tasks-filters{display:flex;align-items:center;gap:6px;flex-wrap:wrap}\n.tasks-list{display:flex;flex-direction:column;gap:10px}\n.task-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:18px 20px;cursor:pointer;transition:all .25s;position:relative;overflow:hidden}\n.task-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover);transform:translateY(-1px);box-shadow:var(--shadow)}\n.task-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px}\n.task-card.status-active::before{background:var(--green)}\n.task-card.status-completed::before{background:var(--pri)}\n.task-card.status-skipped::before{background:var(--text-muted)}\n.task-card.status-skipped{opacity:.6}\n.task-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:8px}\n.task-card-title{font-size:14px;font-weight:600;color:var(--text);line-height:1.4;flex:1;word-break:break-word}\n.task-card-title:empty::after{content:'Untitled Task';color:var(--text-muted);font-style:italic}\n.task-status-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;padding:3px 10px;border-radius:20px;flex-shrink:0}\n.task-status-badge.active{color:var(--green);background:var(--green-bg)}\n.task-status-badge.completed{color:var(--pri);background:var(--pri-glow)}\n.task-status-badge.skipped{color:var(--text-muted);background:rgba(128,128,128,.15)}\n.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}\n.task-card-summary:empty{display:none}\n.task-card-summary.skipped-reason{background:rgba(128,128,128,.08);border-radius:6px;padding:6px 10px;border-left:3px solid var(--text-muted)}\n.task-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted)}\n.task-card-bottom .tag{display:flex;align-items:center;gap:4px}\n.task-card-bottom .tag .icon{font-size:12px}\n\n/* \u2500\u2500\u2500 Task Detail Overlay \u2500\u2500\u2500 */\n.task-detail-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:200;align-items:center;justify-content:center;padding:24px;backdrop-filter:blur(4px)}\n.task-detail-overlay.show{display:flex}\n.task-detail-panel{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);width:100%;max-width:780px;max-height:85vh;overflow-y:auto;box-shadow:var(--shadow-lg);padding:28px 32px}\n.task-detail-header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}\n.task-detail-header h2{font-size:18px;font-weight:700;color:var(--text);line-height:1.4;flex:1}\n.task-detail-meta{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;font-size:12px;color:var(--text-sec)}\n.task-detail-meta .meta-item{display:flex;align-items:center;gap:5px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:5px 12px}\n.task-detail-summary{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:20px;font-size:13px;line-height:1.7;color:var(--text);word-break:break-word}\n.task-detail-summary:empty::after{content:'Summary not yet generated (task still active)';color:var(--text-muted);font-style:italic}\n.task-detail-summary .summary-section-title{font-size:14px;font-weight:700;color:var(--text);margin:14px 0 6px 0;padding-bottom:4px;border-bottom:1px solid var(--border)}\n.task-detail-summary .summary-section-title:first-child{margin-top:0}\n.task-detail-summary ul{margin:4px 0 8px 0;padding-left:20px}\n.task-detail-summary li{margin:3px 0;color:var(--text-sec);line-height:1.6}\n.task-detail-chunks-title{font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px}\n.task-detail-chunks{display:flex;flex-direction:column;gap:14px;padding:8px 0}\n.task-chunk-item{display:flex;flex-direction:column;max-width:82%;font-size:13px;line-height:1.6}\n.task-chunk-item.role-user{align-self:flex-end;align-items:flex-end}\n.task-chunk-item.role-assistant,.task-chunk-item.role-tool{align-self:flex-start;align-items:flex-start}\n.task-chunk-role{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px;padding:0 4px}\n.task-chunk-role.user{color:var(--pri)}\n.task-chunk-role.assistant{color:var(--green)}\n.task-chunk-role.tool{color:var(--amber)}\n.task-chunk-bubble{padding:12px 16px;border-radius:16px;white-space:pre-wrap;word-break:break-word;max-height:200px;overflow:hidden;position:relative;transition:all .2s}\n.task-chunk-bubble.expanded{max-height:none}\n.role-user .task-chunk-bubble{background:var(--pri);color:#000;border-bottom-right-radius:4px}\n.role-assistant .task-chunk-bubble{background:var(--bg-card);border:1px solid var(--border);color:var(--text-sec);border-bottom-left-radius:4px}\n.role-tool .task-chunk-bubble{background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);color:var(--text-sec);border-bottom-left-radius:4px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}\n.task-chunk-bubble:hover{filter:brightness(1.05)}\n.task-chunk-time{font-size:10px;color:var(--text-muted);margin-top:3px;padding:0 4px}\n[data-theme=\"light\"] .role-user .task-chunk-bubble{background:var(--pri);color:#fff}\n[data-theme=\"light\"] .role-assistant .task-chunk-bubble{background:#f0f0f0;border:none;color:#333}\n[data-theme=\"light\"] .task-detail-panel{background:#fff}\n[data-theme=\"light\"] .task-card{background:#fff}\n[data-theme=\"light\"] .tasks-stat{background:#fff}\n\n/* \u2500\u2500\u2500 Skills \u2500\u2500\u2500 */\n.skills-view{display:none;flex:1;min-width:0;flex-direction:column;gap:16px}\n.skills-view.show{display:flex}\n.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}\n.skill-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover);transform:translateY(-1px);box-shadow:var(--shadow)}\n.skill-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px;background:var(--violet)}\n.skill-card.installed::before{background:var(--green)}\n.skill-card.archived{opacity:.5}\n.skill-card.archived::before{background:var(--text-muted)}\n.skill-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:6px}\n.skill-card-name{font-size:15px;font-weight:700;color:var(--text);flex:1}\n.skill-card-badges{display:flex;gap:6px;align-items:center}\n.skill-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:3px 10px;border-radius:20px}\n.skill-badge.version{color:var(--violet);background:rgba(139,92,246,.15)}\n.skill-badge.installed{color:var(--green);background:var(--green-bg)}\n.skill-badge.status-active{color:var(--pri);background:var(--pri-glow)}\n.skill-badge.status-archived{color:var(--text-muted);background:rgba(128,128,128,.15)}\n.skill-badge.status-draft{color:var(--amber);background:var(--amber-bg)}\n.skill-badge.quality{font-size:10px;font-weight:700;padding:3px 10px;border-radius:20px}\n.skill-badge.quality.high{color:var(--green);background:var(--green-bg)}\n.skill-badge.quality.mid{color:var(--amber);background:var(--amber-bg)}\n.skill-badge.quality.low{color:var(--rose);background:var(--rose-bg)}\n.skill-card.draft{opacity:.75}\n.skill-card.draft::before{background:var(--amber)}\n.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}\n.skill-card-bottom{display:flex;align-items:center;gap:14px;font-size:11px;color:var(--text-muted);flex-wrap:wrap}\n.skill-card-bottom .tag{display:flex;align-items:center;gap:4px}\n.skill-card-tags{display:flex;gap:4px;flex-wrap:wrap}\n.skill-tag{font-size:10px;padding:2px 8px;border-radius:10px;background:rgba(139,92,246,.1);color:var(--violet);font-weight:500}\n.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)}\n.skill-version-item{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:12px 16px}\n.skill-version-header{display:flex;align-items:center;gap:10px;margin-bottom:6px}\n.skill-version-badge{font-size:11px;font-weight:700;color:var(--violet);background:rgba(139,92,246,.12);padding:2px 8px;border-radius:8px}\n.skill-version-type{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em}\n.skill-version-changelog{font-size:12px;color:var(--text);line-height:1.5;font-weight:600}\n.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}\n.skill-version-time{font-size:10px;color:var(--text-muted);margin-top:4px}\n.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}\n.skill-related-task:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.skill-related-task .relation{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em;min-width:80px}\n.skill-related-task .task-title{font-size:13px;color:var(--text);flex:1}\n.skill-files-list{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}\n.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}\n.skill-file-icon{font-size:14px;width:20px;text-align:center}\n.skill-file-name{flex:1;color:var(--text);font-family:SF Mono,Monaco,Consolas,monospace}\n.skill-file-type{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.04em}\n.skill-file-size{font-size:10px;color:var(--text-muted)}\n.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}\n.skill-download-btn:hover{opacity:.85;transform:translateY(-1px)}\n.task-skill-section{margin-bottom:16px;padding:14px 16px;border-radius:var(--radius);border:1px solid var(--border)}\n.task-skill-section.status-generated{border-color:var(--green);background:var(--green-bg)}\n.task-skill-section.status-generating{border-color:var(--amber);background:var(--amber-bg)}\n.task-skill-section.status-not_generated,.task-skill-section.status-skipped{border-color:var(--border);background:var(--bg-card)}\n.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)}\n.task-skill-section .skill-status-reason{font-size:12px;color:var(--text-sec);line-height:1.5}\n.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}\n.task-skill-section .skill-link-card:hover{border-color:var(--pri);background:var(--bg-card-hover)}\n.task-skill-section .skill-link-name{font-size:13px;font-weight:600;color:var(--pri)}\n.task-skill-section .skill-link-meta{font-size:11px;color:var(--text-sec);margin-top:4px}\n.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)}\n[data-theme=\"light\"] .skill-card{background:#fff}\n[data-theme=\"light\"] .skill-detail-desc{background:#f8fafc}\n[data-theme=\"light\"] .skill-version-item{background:#f8fafc}\n\n/* \u2500\u2500\u2500 Analytics / \u7EDF\u8BA1 \u2500\u2500\u2500 */\n.nav-tabs{display:flex;align-items:center;gap:2px;background:rgba(255,255,255,.06);border-radius:10px;padding:3px}\n.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}\n.nav-tabs .tab:hover{color:var(--text)}\n.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)}\n[data-theme=\"light\"] .nav-tabs{background:rgba(0,0,0,.05)}\n[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)}\n.analytics-view,.settings-view,.logs-view,.migrate-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}\n.analytics-view.show,.settings-view.show,.logs-view.show,.migrate-view.show{display:flex}\n\n/* \u2500\u2500\u2500 Logs \u2500\u2500\u2500 */\n.logs-toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 0}\n.logs-toolbar-left{display:flex;align-items:center;gap:8px}\n.logs-toolbar-right{display:flex;align-items:center;gap:8px}\n.logs-list{display:flex;flex-direction:column;gap:8px;overflow-y:auto;flex:1;min-height:0}\n.log-entry{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;transition:border-color .2s}\n.log-entry:hover{border-color:var(--border-glow)}\n.log-header{display:flex;align-items:center;gap:10px;padding:12px 16px;cursor:pointer;user-select:none;transition:background .15s}\n.log-header:hover{background:rgba(255,255,255,.03)}\n[data-theme=\"light\"] .log-header:hover{background:rgba(0,0,0,.02)}\n.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}\n.log-tool-badge.memory_search{background:rgba(59,130,246,.15);color:#60a5fa}\n.log-tool-badge.memory_add{background:rgba(168,85,247,.15);color:#c084fc}\n.log-tool-badge.auto_recall{background:rgba(168,85,247,.15);color:#c084fc}\n.log-tool-badge.memory_timeline{background:rgba(34,197,94,.15);color:#4ade80}\n.log-tool-badge.memory_get{background:rgba(251,146,60,.15);color:#fb923c}\n.log-tool-badge.task_summary{background:rgba(245,158,11,.15);color:#fbbf24}\n.log-tool-badge.skill_get{background:rgba(236,72,153,.15);color:#f472b6}\n.log-tool-badge.skill_install{background:rgba(14,165,233,.15);color:#38bdf8}\n.log-tool-badge.memory_viewer{background:rgba(100,116,139,.15);color:#94a3b8}\n.log-dur{font-family:'SF Mono',Consolas,monospace;font-size:10px;color:var(--text-sec);opacity:.7}\n.log-time{margin-left:auto;font-size:11px;color:var(--text-sec);font-family:'SF Mono',Consolas,monospace;white-space:nowrap}\n.log-status{width:7px;height:7px;border-radius:50%;flex-shrink:0}\n.log-status.ok{background:#4ade80;box-shadow:0 0 4px rgba(74,222,128,.5)}\n.log-status.fail{background:#f87171;box-shadow:0 0 4px rgba(248,113,113,.5)}\n.log-summary{padding:8px 16px 10px;font-size:12px;color:var(--text-sec);line-height:1.5}\n.log-summary-kv{display:inline-flex;align-items:center;gap:4px;margin-right:12px;font-size:11px}\n.log-summary-kv .kv-label{color:var(--text-sec);opacity:.7}\n.log-summary-kv .kv-val{color:var(--text);font-family:'SF Mono',Consolas,monospace;font-size:11px}\n.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}\n.log-summary-stats{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}\n.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}\n.log-stat-chip.stored{background:rgba(74,222,128,.12);color:#4ade80}\n.log-stat-chip.skipped{background:rgba(100,116,139,.12);color:#94a3b8}\n.log-stat-chip.dedup{background:rgba(251,146,60,.12);color:#fb923c}\n.log-stat-chip.merged{background:rgba(168,85,247,.12);color:#c084fc}\n.log-stat-chip.errors{background:rgba(248,113,113,.12);color:#f87171}\n.log-msg-list{margin-top:8px;display:flex;flex-direction:column;gap:4px}\n.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)}\n[data-theme=\"light\"] .log-msg-item{background:rgba(0,0,0,.02)}\n.log-msg-role{flex-shrink:0;font-size:10px;font-weight:600;padding:1px 6px;border-radius:4px;text-transform:uppercase;letter-spacing:.3px}\n.log-msg-role.user{background:rgba(59,130,246,.12);color:#60a5fa}\n.log-msg-role.assistant{background:rgba(168,85,247,.12);color:#c084fc}\n.log-msg-role.system{background:rgba(100,116,139,.12);color:#94a3b8}\n.log-msg-action{flex-shrink:0;font-size:10px;font-weight:600;padding:1px 6px;border-radius:4px}\n.log-msg-action.stored{color:#4ade80}\n.log-msg-action.exact-dup{color:#94a3b8}\n.log-msg-action.dedup{color:#fb923c}\n.log-msg-action.merged{color:#c084fc}\n.log-msg-action.error{color:#f87171}\n.log-msg-text{color:var(--text);opacity:.85;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}\n.log-detail{display:none;border-top:1px solid var(--border);padding:0}\n.log-detail.open{display:block}\n.log-expand-btn{font-size:10px;color:var(--text-sec);opacity:.5;margin-left:auto;transition:transform .2s,opacity .15s;display:inline-block}\n.log-entry.expanded .log-expand-btn{transform:rotate(180deg);opacity:.8}\n.logs-pagination{display:flex;align-items:center;justify-content:center;gap:4px;padding:12px 0;flex-wrap:wrap}\n.logs-pagination .btn{min-width:32px;padding:4px 8px;font-size:12px}\n.logs-pagination .btn-primary{background:var(--primary);color:#fff;border-color:var(--primary)}\n.logs-pagination .page-ellipsis{color:var(--text-sec);font-size:12px;padding:0 4px}\n.logs-pagination .page-total{font-size:11px;color:var(--text-sec);margin-left:8px}\n.log-io-section{padding:10px 14px}\n.log-io-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec);margin-bottom:6px}\n.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}\n.log-io-section+.log-io-section{border-top:1px dashed var(--border)}\n[data-theme=\"light\"] .log-io-content{background:rgba(0,0,0,.04)}\n[data-theme=\"light\"] .log-summary-query{background:rgba(59,130,246,.06)}\n.settings-group{margin-bottom:8px}\n.settings-group-title{font-size:15px;font-weight:700;color:var(--text);margin:0 0 12px 0;padding:0;letter-spacing:.02em}\n.settings-group .settings-section{margin-bottom:16px}\n.settings-group .settings-section:last-child{margin-bottom:0}\n.settings-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px 28px}\n.settings-section h3{font-size:13px;font-weight:700;color:var(--text);margin-bottom:16px;display:flex;align-items:center;gap:8px}\n.settings-section h3 .icon{font-size:16px;opacity:.8}\n.settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}\n@media(max-width:800px){.settings-grid{grid-template-columns:1fr}}\n.settings-field{display:flex;flex-direction:column;gap:4px}\n.settings-field label{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em}\n.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}\n.settings-field input:focus,.settings-field select:focus{outline:none;border-color:var(--pri)}\n.settings-field input[type=\"password\"]{font-family:'Courier New',monospace;letter-spacing:.05em}\n.settings-field .field-hint{font-size:10px;color:var(--text-muted);margin-top:2px}\n.settings-field.full-width{grid-column:1/-1}\n.settings-toggle{display:flex;align-items:center;gap:10px;padding:4px 0}\n.settings-toggle label{font-size:12px;font-weight:500;color:var(--text-sec);text-transform:none;letter-spacing:0}\n.toggle-switch{position:relative;width:36px;height:20px;cursor:pointer}\n.toggle-switch input{opacity:0;width:0;height:0}\n.toggle-slider{position:absolute;inset:0;background:var(--border);border-radius:20px;transition:.2s}\n.toggle-slider::before{content:'';position:absolute;height:14px;width:14px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s}\n.toggle-switch input:checked+.toggle-slider{background:var(--pri)}\n.toggle-switch input:checked+.toggle-slider::before{transform:translateX(16px)}\n.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)}\n.settings-actions .btn{min-width:110px;padding:10px 20px;font-size:13px}\n.settings-actions .btn-primary{background:rgba(99,102,241,.08);color:var(--pri);border:1px solid rgba(99,102,241,.25);font-weight:600}\n.settings-actions .btn-primary:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}\n[data-theme=\"light\"] .settings-actions .btn-primary{background:rgba(79,70,229,.06);color:#4f46e5;border:1px solid rgba(79,70,229,.2)}\n[data-theme=\"light\"] .settings-actions .btn-primary:hover{background:rgba(79,70,229,.1);border-color:#4f46e5}\n.settings-saved{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px;font-weight:600;opacity:0;transition:opacity .3s}\n.settings-saved.show{opacity:1}\n.migrate-log-item{display:flex;align-items:flex-start;gap:10px;padding:8px 14px;border-bottom:1px solid var(--border);animation:migrateFadeIn .3s ease}\n.migrate-log-item:last-child{border-bottom:none}\n.migrate-log-item .log-icon{flex-shrink:0;width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;margin-top:2px}\n.migrate-log-item .log-icon.stored{background:rgba(34,197,94,.12);color:#22c55e}\n.migrate-log-item .log-icon.skipped{background:rgba(245,158,11,.12);color:#f59e0b}\n.migrate-log-item .log-icon.merged{background:rgba(59,130,246,.12);color:#3b82f6}\n.migrate-log-item .log-icon.error{background:rgba(239,68,68,.12);color:#ef4444}\n.migrate-log-item .log-icon.duplicate{background:rgba(245,158,11,.12);color:#f59e0b}\n.migrate-log-item .log-body{flex:1;min-width:0}\n.migrate-log-item .log-preview{color:var(--text);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}\n.migrate-log-item .log-meta{display:flex;gap:8px;font-size:9px;color:var(--text-muted);margin-top:2px}\n.migrate-log-item .log-meta .tag{padding:1px 6px;border-radius:4px;font-weight:600;letter-spacing:.02em}\n.migrate-log-item .log-meta .tag.stored{background:rgba(34,197,94,.1);color:#22c55e}\n.migrate-log-item .log-meta .tag.skipped{background:rgba(245,158,11,.1);color:#f59e0b}\n.migrate-log-item .log-meta .tag.merged{background:rgba(59,130,246,.1);color:#3b82f6}\n.migrate-log-item .log-meta .tag.error{background:rgba(239,68,68,.1);color:#ef4444}\n.migrate-log-item .log-meta .tag.duplicate{background:rgba(245,158,11,.1);color:#f59e0b}\n@keyframes migrateFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}\n.feed-wrap{flex:1;min-width:0;display:flex;flex-direction:column}\n.feed-wrap.hide{display:none}\n.analytics-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}\n.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)}\n.analytics-card::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--pri);opacity:.5}\n.analytics-card::after{display:none}\n.analytics-card:hover{transform:translateY(-2px);box-shadow:var(--shadow);border-color:var(--border-glow)}\n.analytics-card.green::before{background:var(--green)}\n.analytics-card.amber::before{background:var(--amber)}\n.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}\n.analytics-card.green .ac-value{color:var(--green);background:none}\n.analytics-card.amber .ac-value{color:var(--amber);background:none}\n.analytics-card .ac-label{font-size:11px;color:var(--text-muted);margin-top:6px;font-weight:500;text-transform:uppercase;letter-spacing:.06em}\n.analytics-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:22px 24px;position:relative;overflow:hidden}\n.analytics-section::before{display:none}\n.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}\n.analytics-section h3 .icon{font-size:14px;opacity:.6}\n.chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto;justify-content:center}\n.chart-bar-wrap{flex:1;min-width:28px;max-width:80px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}\n.chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}\n.chart-bar-wrap:hover .chart-bar{opacity:1}\n.chart-bar-wrap:hover .chart-bar-label{color:var(--text)}\n.chart-bar-wrap:hover .chart-tip{opacity:1;transform:translateX(-50%) translateY(0)}\n.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}\n.chart-bar{width:100%;border-radius:3px 3px 1px 1px;background:#818cf8;opacity:.75;transition:all .2s ease}\n.chart-bar.violet{background:#6366f1}\n.chart-bar.green{background:var(--green)}\n.chart-bar.zero{background:var(--border);opacity:.3;border-radius:2px}\n.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}\n.chart-legend{display:flex;gap:14px;margin-top:12px;flex-wrap:wrap;font-size:11px;color:var(--text-sec);font-weight:500}\n.chart-legend span{display:inline-flex;align-items:center;gap:5px}\n.chart-legend .dot{width:8px;height:8px;border-radius:2px}\n.chart-legend .dot.pri{background:var(--pri)}\n.tool-chart-svg{width:100%;height:100%;display:block}\n.tool-chart-svg .grid-line{stroke:var(--border);stroke-dasharray:3 3;stroke-width:0.5}\n.tool-chart-svg .axis-label{fill:var(--text-muted);font-size:10px;font-family:var(--mono)}\n.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}\n@keyframes lineIn{to{stroke-dashoffset:0}}\n.tool-chart-svg .data-area{opacity:1}\n.tool-chart-svg .hover-dot{r:3.5;stroke-width:2;stroke:var(--bg);opacity:0;transition:opacity .1s}\n.tool-chart-svg .hover-dot.show{opacity:1}\n.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}\n.tool-chart-tooltip.show{opacity:1}\n.tool-chart-tooltip .tt-time{color:var(--text-muted);font-size:10px;margin-bottom:4px;font-weight:500}\n.tool-chart-tooltip .tt-row{display:flex;align-items:center;gap:6px;margin:2px 0}\n.tool-chart-tooltip .tt-dot{width:6px;height:6px;border-radius:2px;flex-shrink:0}\n.tool-chart-tooltip .tt-val{font-weight:600;margin-left:auto;padding-left:12px}\n.tool-agg-table{width:100%;border-collapse:collapse;font-size:12px}\n.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)}\n.tool-agg-table td{padding:8px 12px;color:var(--text-sec);border-bottom:1px solid var(--border)}\n.tool-agg-table tr:hover td{background:rgba(99,102,241,.04);color:var(--text)}\n.tool-agg-table .tool-name{font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}\n.tool-agg-table .tool-dot{width:8px;height:8px;border-radius:2px;flex-shrink:0}\n.tool-agg-table .ms-val{font-family:var(--mono);font-weight:600}\n.tool-agg-table .ms-val.fast{color:var(--green)}\n.tool-agg-table .ms-val.medium{color:var(--amber)}\n.tool-agg-table .ms-val.slow{color:var(--accent)}\n.chart-legend .dot.violet{background:var(--violet)}\n.chart-legend .dot.green{background:var(--green)}\n.breakdown-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:20px}\n.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}\n.breakdown-item:hover{border-color:var(--border-glow);background:rgba(255,255,255,.04)}\n.breakdown-item .bd-top{display:flex;align-items:center;justify-content:space-between}\n.breakdown-item .label{font-size:12px;color:var(--text-sec);font-weight:500;text-transform:capitalize}\n.breakdown-item .value{font-size:13px;font-weight:600;color:var(--text)}\n.breakdown-bar-wrap{height:3px;background:rgba(255,255,255,.06);border-radius:2px;overflow:hidden}\n.breakdown-bar{height:100%;border-radius:2px;background:var(--pri);transition:width .5s ease}\n.metrics-toolbar{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}\n.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}\n.range-btn:hover{border-color:var(--pri);color:var(--pri)}\n.range-btn.active{background:rgba(99,102,241,.08);color:var(--pri);border-color:rgba(99,102,241,.25)}\n\n.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}\n.theme-toggle .theme-icon-light{display:none}\n.theme-toggle .theme-icon-dark{display:inline}\n[data-theme=\"light\"] .theme-toggle .theme-icon-light{display:inline}\n[data-theme=\"light\"] .theme-toggle .theme-icon-dark{display:none}\n\n.auth-top-actions{position:absolute;top:16px;right:16px;z-index:10;display:flex;align-items:center;gap:2px}\n.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}\n.auth-theme-toggle:hover{background:rgba(255,255,255,.25);color:#fff}\n.auth-theme-toggle .theme-icon-light{display:none}\n.auth-theme-toggle .theme-icon-dark{display:inline}\n[data-theme=\"light\"] .auth-theme-toggle{color:rgba(0,0,0,.4);background:rgba(0,0,0,.05)}\n[data-theme=\"light\"] .auth-theme-toggle:hover{background:rgba(0,0,0,.1);color:#0f172a}\n[data-theme=\"light\"] .auth-top-actions{background:none}\n[data-theme=\"light\"] .auth-theme-toggle .theme-icon-light{display:inline}\n[data-theme=\"light\"] .auth-theme-toggle .theme-icon-dark{display:none}\n\n@media(max-width:1100px){.analytics-cards{grid-template-columns:repeat(3,1fr)}}\n@media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}.analytics-cards{grid-template-columns:repeat(2,1fr)}.topbar{padding:0 16px;gap:8px}.topbar .brand span{display:none}.topbar-center{justify-content:flex-start}}\n</style>\n</head>\n<body>\n\n<!-- \u2500\u2500\u2500 Auth: Setup Password \u2500\u2500\u2500 -->\n<div id=\"setupScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-top-actions\">\n <button class=\"auth-theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <button class=\"auth-theme-toggle\" onclick=\"toggleLang()\" aria-label=\"Switch language\"><span data-i18n=\"lang.switch\">EN</span></button>\n </div>\n <div class=\"auth-card\">\n <div class=\"logo\"><svg width=\"60\" height=\"60\" viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"aLG\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"><stop offset=\"0%\" stop-color=\"#ff4d4d\"/><stop offset=\"100%\" stop-color=\"#991b1b\"/></linearGradient></defs><path d=\"M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z\" fill=\"url(#aLG)\"/><path d=\"M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z\" fill=\"url(#aLG)\"/><path d=\"M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z\" fill=\"url(#aLG)\"/><path d=\"M45 15Q35 5 30 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><path d=\"M75 15Q85 5 90 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><circle cx=\"45\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"75\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"46\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/><circle cx=\"76\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/></svg></div>\n <h1 data-i18n=\"title\">OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\" data-i18n=\"subtitle\">Powered by MemOS</p>\n <p data-i18n=\"setup.desc\">Set a password to protect your memories</p>\n <input type=\"password\" id=\"setupPw\" data-i18n-ph=\"setup.pw\" placeholder=\"Enter a password (4+ characters)\" autofocus>\n <input type=\"password\" id=\"setupPw2\" data-i18n-ph=\"setup.pw2\" placeholder=\"Confirm password\">\n <button class=\"btn-auth\" onclick=\"doSetup()\" data-i18n=\"setup.btn\">Set Password & Enter</button>\n <div class=\"error-msg\" id=\"setupErr\"></div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Auth: Login \u2500\u2500\u2500 -->\n<div id=\"loginScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-top-actions\">\n <button class=\"auth-theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <button class=\"auth-theme-toggle\" onclick=\"toggleLang()\" aria-label=\"Switch language\"><span data-i18n=\"lang.switch\">EN</span></button>\n </div>\n <div class=\"auth-card\">\n <div class=\"logo\"><svg width=\"60\" height=\"60\" viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"bLG\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"><stop offset=\"0%\" stop-color=\"#ff4d4d\"/><stop offset=\"100%\" stop-color=\"#991b1b\"/></linearGradient></defs><path d=\"M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z\" fill=\"url(#bLG)\"/><path d=\"M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z\" fill=\"url(#bLG)\"/><path d=\"M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z\" fill=\"url(#bLG)\"/><path d=\"M45 15Q35 5 30 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><path d=\"M75 15Q85 5 90 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><circle cx=\"45\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"75\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"46\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/><circle cx=\"76\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/></svg></div>\n <h1 data-i18n=\"title\">OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\" data-i18n=\"subtitle\">Powered by MemOS</p>\n <p data-i18n=\"login.desc\">Enter your password to access memories</p>\n <div id=\"loginForm\">\n <input type=\"password\" id=\"loginPw\" data-i18n-ph=\"login.pw\" placeholder=\"Password\" autofocus>\n <button class=\"btn-auth\" onclick=\"doLogin()\" data-i18n=\"login.btn\">Unlock</button>\n <div class=\"error-msg\" id=\"loginErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showResetForm()\" data-i18n=\"login.forgot\">Forgot password?</button>\n </div>\n <div id=\"resetForm\" style=\"display:none\">\n <div class=\"reset-guide\">\n <div class=\"reset-step\">\n <div class=\"step-num\">1</div>\n <div class=\"step-body\">\n <div class=\"step-title\" data-i18n=\"reset.step1.title\">Open Terminal</div>\n <div class=\"step-desc\" data-i18n=\"reset.step1.desc\">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>\n <div class=\"cmd-box\" onclick=\"copyCmd(this)\">\n <code>grep \"password reset token:\" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>\n <span class=\"copy-hint\" data-i18n=\"copy.hint\">Click to copy</span>\n </div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">2</div>\n <div class=\"step-body\">\n <div class=\"step-title\" data-i18n=\"reset.step2.title\">Find the token</div>\n <div class=\"step-desc\" id=\"resetStep2Desc\">In the output, find <span style=\"font-family:monospace;font-size:12px;color:var(--pri)\">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">3</div>\n <div class=\"step-body\">\n <div class=\"step-title\" data-i18n=\"reset.step3.title\">Paste & reset</div>\n <div class=\"step-desc\" data-i18n=\"reset.step3.desc\">Paste the token below and set your new password.</div>\n </div>\n </div>\n </div>\n <input type=\"text\" id=\"resetToken\" data-i18n-ph=\"reset.token\" placeholder=\"Paste reset token here\" style=\"margin-bottom:8px;font-family:monospace\">\n <input type=\"password\" id=\"resetNewPw\" data-i18n-ph=\"reset.newpw\" placeholder=\"New password (4+ characters)\">\n <input type=\"password\" id=\"resetNewPw2\" data-i18n-ph=\"reset.newpw2\" placeholder=\"Confirm new password\">\n <button class=\"btn-auth\" onclick=\"doReset()\" data-i18n=\"reset.btn\">Reset Password</button>\n <div class=\"error-msg\" id=\"resetErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showLoginForm()\" data-i18n=\"reset.back\">\u2190 Back to login</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Main App \u2500\u2500\u2500 -->\n<div class=\"app\" id=\"app\">\n <div class=\"topbar\">\n <div class=\"brand\">\n <div class=\"icon\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 120 120\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" style=\"filter:drop-shadow(0 0 8px rgba(255,77,77,.3))\"><defs><linearGradient id=\"tLG\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"><stop offset=\"0%\" stop-color=\"#ff4d4d\"/><stop offset=\"100%\" stop-color=\"#991b1b\"/></linearGradient></defs><path d=\"M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z\" fill=\"url(#tLG)\"/><path d=\"M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z\" fill=\"url(#tLG)\"/><path d=\"M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z\" fill=\"url(#tLG)\"/><path d=\"M45 15Q35 5 30 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><path d=\"M75 15Q85 5 90 8\" stroke=\"#ff4d4d\" stroke-width=\"2\" stroke-linecap=\"round\"/><circle cx=\"45\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"75\" cy=\"35\" r=\"6\" fill=\"#050810\"/><circle cx=\"46\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/><circle cx=\"76\" cy=\"34\" r=\"2\" fill=\"#00e5cc\"/></svg></div>\n <span data-i18n=\"title\">OpenClaw Memory</span>\n </div>\n <div class=\"topbar-center\">\n <nav class=\"nav-tabs\">\n <button class=\"tab active\" data-view=\"memories\" onclick=\"switchView('memories')\" data-i18n=\"tab.memories\">\uD83D\uDCDA Memories</button>\n <button class=\"tab\" data-view=\"tasks\" onclick=\"switchView('tasks')\" data-i18n=\"tab.tasks\">\uD83D\uDCCB Tasks</button>\n <button class=\"tab\" data-view=\"skills\" onclick=\"switchView('skills')\" data-i18n=\"tab.skills\">\uD83E\uDDE0 Skills</button>\n <button class=\"tab\" data-view=\"analytics\" onclick=\"switchView('analytics')\" data-i18n=\"tab.analytics\">\uD83D\uDCCA Analytics</button>\n <button class=\"tab\" data-view=\"logs\" onclick=\"switchView('logs')\" data-i18n=\"tab.logs\">\uD83D\uDCDD Logs</button>\n <button class=\"tab\" data-view=\"import\" onclick=\"switchView('import')\" data-i18n=\"tab.import\">\uD83D\uDCE5 Import</button>\n <button class=\"tab\" data-view=\"settings\" onclick=\"switchView('settings')\" data-i18n=\"tab.settings\">\u2699 Settings</button>\n </nav>\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-icon\" onclick=\"toggleLang()\" aria-label=\"Switch language\" style=\"font-size:12px;font-weight:700;padding:4px 8px\"><span data-i18n=\"lang.switch\">EN</span></button>\n <button class=\"btn btn-icon theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"loadAll()\" data-i18n=\"refresh\">\u21BB Refresh</button>\n <button class=\"btn btn-ghost btn-sm\" onclick=\"doLogout()\" data-i18n=\"logout\">Logout</button>\n </div>\n </div>\n\n <div class=\"main-content\">\n <div class=\"sidebar\" id=\"sidebar\">\n <div class=\"stats-grid\" id=\"statsGrid\">\n <div class=\"stat-card pri\"><div class=\"stat-value\" id=\"statTotal\">-</div><div class=\"stat-label\" data-i18n=\"stat.memories\">Memories</div></div>\n <div class=\"stat-card green\"><div class=\"stat-value\" id=\"statSessions\">-</div><div class=\"stat-label\" data-i18n=\"stat.sessions\">Sessions</div></div>\n <div class=\"stat-card amber\"><div class=\"stat-value\" id=\"statEmbeddings\">-</div><div class=\"stat-label\" data-i18n=\"stat.embeddings\">Embeddings</div></div>\n <div class=\"stat-card rose\"><div class=\"stat-value\" id=\"statTimeSpan\">-</div><div class=\"stat-label\" data-i18n=\"stat.days\">Days</div></div>\n </div>\n <div id=\"embeddingStatus\"></div>\n <div class=\"section-title\" data-i18n=\"sidebar.sessions\">Sessions</div>\n <div class=\"session-list\" id=\"sessionList\"></div>\n <button class=\"btn btn-sm btn-ghost\" style=\"width:100%;margin-top:20px;justify-content:center;color:var(--text-muted);font-size:11px\" onclick=\"clearAll()\" data-i18n=\"sidebar.clear\">\uD83D\uDDD1 Clear All Data</button>\n </div>\n\n <div class=\"feed-wrap\" id=\"feedWrap\">\n <div class=\"feed\">\n <div class=\"search-bar\">\n <span class=\"search-icon\">\uD83D\uDD0D</span>\n <input type=\"text\" id=\"searchInput\" data-i18n-ph=\"search.placeholder\" placeholder=\"Search memories (supports semantic search)...\" oninput=\"debounceSearch()\">\n </div>\n <div class=\"search-meta\" id=\"searchMeta\"></div>\n <div class=\"filter-bar\" id=\"filterBar\">\n <button class=\"filter-chip active\" data-role=\"\" onclick=\"setRoleFilter(this,'')\" data-i18n=\"filter.all\">All</button>\n <button class=\"filter-chip\" data-role=\"user\" onclick=\"setRoleFilter(this,'user')\">User</button>\n <button class=\"filter-chip\" data-role=\"assistant\" onclick=\"setRoleFilter(this,'assistant')\">Assistant</button>\n <button class=\"filter-chip\" data-role=\"system\" onclick=\"setRoleFilter(this,'system')\">System</button>\n <span class=\"filter-sep\"></span>\n <select id=\"filterKind\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"\" data-i18n=\"filter.allkinds\">All kinds</option>\n <option value=\"paragraph\" data-i18n=\"filter.paragraph\">Paragraph</option>\n <option value=\"code_block\" data-i18n=\"filter.code\">Code</option>\n <option value=\"dialog\" data-i18n=\"filter.dialog\">Dialog</option>\n <option value=\"list\" data-i18n=\"filter.list\">List</option>\n <option value=\"error_stack\" data-i18n=\"filter.error\">Error</option>\n <option value=\"command\" data-i18n=\"filter.command\">Command</option>\n </select>\n <select id=\"filterSort\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"newest\" data-i18n=\"filter.newest\">Newest first</option>\n <option value=\"oldest\" data-i18n=\"filter.oldest\">Oldest first</option>\n </select>\n </div>\n <div class=\"date-filter\">\n <label data-i18n=\"filter.from\">From</label><input type=\"datetime-local\" id=\"dateFrom\" step=\"1\" onchange=\"applyFilters()\">\n <label data-i18n=\"filter.to\">To</label><input type=\"datetime-local\" id=\"dateTo\" step=\"1\" onchange=\"applyFilters()\">\n <button class=\"btn btn-sm btn-text\" onclick=\"clearDateFilter()\" data-i18n=\"filter.clear\">Clear</button>\n </div>\n <div class=\"memory-list\" id=\"memoryList\"><div class=\"spinner\"></div></div>\n <div class=\"pagination\" id=\"pagination\"></div>\n </div>\n </div>\n <div class=\"tasks-view\" id=\"tasksView\">\n <div class=\"tasks-header\">\n <div class=\"tasks-stats\">\n <div class=\"tasks-stat\"><span class=\"tasks-stat-value\" id=\"tasksTotalCount\">-</span><span class=\"tasks-stat-label\" data-i18n=\"tasks.total\">Total Tasks</span></div>\n <div class=\"tasks-stat\"><span class=\"tasks-stat-value\" id=\"tasksActiveCount\">-</span><span class=\"tasks-stat-label\" data-i18n=\"tasks.active\">Active</span></div>\n <div class=\"tasks-stat\"><span class=\"tasks-stat-value\" id=\"tasksCompletedCount\">-</span><span class=\"tasks-stat-label\" data-i18n=\"tasks.completed\">Completed</span></div>\n <div class=\"tasks-stat\"><span class=\"tasks-stat-value\" id=\"tasksSkippedCount\">-</span><span class=\"tasks-stat-label\" data-i18n=\"tasks.status.skipped\">Skipped</span></div>\n </div>\n <div class=\"tasks-filters\">\n <button class=\"filter-chip active\" data-task-status=\"\" onclick=\"setTaskStatusFilter(this,'')\" data-i18n=\"filter.all\">All</button>\n <button class=\"filter-chip\" data-task-status=\"active\" onclick=\"setTaskStatusFilter(this,'active')\" data-i18n=\"tasks.status.active\">Active</button>\n <button class=\"filter-chip\" data-task-status=\"completed\" onclick=\"setTaskStatusFilter(this,'completed')\" data-i18n=\"tasks.status.completed\">Completed</button>\n <button class=\"filter-chip\" data-task-status=\"skipped\" onclick=\"setTaskStatusFilter(this,'skipped')\" data-i18n=\"tasks.status.skipped\">Skipped</button>\n <button class=\"btn btn-sm btn-ghost\" onclick=\"loadTasks()\" style=\"margin-left:auto\" data-i18n=\"refresh\">\u21BB Refresh</button>\n </div>\n </div>\n <div class=\"tasks-list\" id=\"tasksList\"><div class=\"spinner\"></div></div>\n <div class=\"pagination\" id=\"tasksPagination\"></div>\n <div class=\"task-detail-overlay\" id=\"taskDetailOverlay\" onclick=\"closeTaskDetail(event)\">\n <div class=\"task-detail-panel\" onclick=\"event.stopPropagation()\">\n <div class=\"task-detail-header\">\n <h2 id=\"taskDetailTitle\"></h2>\n <button class=\"btn btn-icon\" onclick=\"closeTaskDetail()\" title=\"Close\">\u2715</button>\n </div>\n <div class=\"task-detail-meta\" id=\"taskDetailMeta\"></div>\n <div class=\"task-skill-section\" id=\"taskSkillSection\"></div>\n <div class=\"task-detail-summary\" id=\"taskDetailSummary\"></div>\n <div class=\"task-detail-chunks-title\" data-i18n=\"tasks.chunks\">Related Memories</div>\n <div class=\"task-detail-chunks\" id=\"taskDetailChunks\"></div>\n </div>\n </div>\n </div>\n <div class=\"skills-view\" id=\"skillsView\">\n <div class=\"tasks-header\">\n <div class=\"tasks-stats\">\n <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>\n <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>\n <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>\n <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>\n </div>\n <div class=\"tasks-filters\">\n <button class=\"filter-chip active\" data-skill-status=\"\" onclick=\"setSkillStatusFilter(this,'')\" data-i18n=\"filter.all\">All</button>\n <button class=\"filter-chip\" data-skill-status=\"active\" onclick=\"setSkillStatusFilter(this,'active')\" data-i18n=\"skills.filter.active\">Active</button>\n <button class=\"filter-chip\" data-skill-status=\"draft\" onclick=\"setSkillStatusFilter(this,'draft')\" data-i18n=\"skills.filter.draft\">Draft</button>\n <button class=\"filter-chip\" data-skill-status=\"archived\" onclick=\"setSkillStatusFilter(this,'archived')\" data-i18n=\"skills.filter.archived\">Archived</button>\n <button class=\"btn btn-sm btn-ghost\" onclick=\"loadSkills()\" style=\"margin-left:auto\" data-i18n=\"refresh\">\u21BB Refresh</button>\n </div>\n </div>\n <div class=\"tasks-list\" id=\"skillsList\"><div class=\"spinner\"></div></div>\n </div>\n <div class=\"task-detail-overlay\" id=\"skillDetailOverlay\" onclick=\"closeSkillDetail(event)\">\n <div class=\"task-detail-panel\" onclick=\"event.stopPropagation()\">\n <div class=\"task-detail-header\">\n <h2 id=\"skillDetailTitle\"></h2>\n <div style=\"display:flex;gap:8px;align-items:center\">\n <button class=\"skill-download-btn\" id=\"skillDownloadBtn\" onclick=\"downloadSkill()\" data-i18n=\"skills.download\">\u2B07 Download</button>\n <button class=\"btn btn-icon\" onclick=\"closeSkillDetail()\" title=\"Close\">\u2715</button>\n </div>\n </div>\n <div class=\"task-detail-meta\" id=\"skillDetailMeta\"></div>\n <div class=\"skill-detail-desc\" id=\"skillDetailDesc\"></div>\n <div class=\"task-detail-chunks-title\" data-i18n=\"skills.files\">Skill Files</div>\n <div class=\"skill-files-list\" id=\"skillFilesList\"></div>\n <div class=\"task-detail-chunks-title\" id=\"skillContentTitle\" data-i18n=\"skills.content\">SKILL.md Content</div>\n <div class=\"task-detail-summary\" id=\"skillDetailContent\" style=\"max-height:50vh;overflow-y:auto\"></div>\n <div class=\"task-detail-chunks-title\" data-i18n=\"skills.versions\">Version History</div>\n <div class=\"task-detail-chunks\" id=\"skillVersionsList\" style=\"gap:10px\"></div>\n <div class=\"task-detail-chunks-title\" style=\"margin-top:16px\" data-i18n=\"skills.related\">Related Tasks</div>\n <div class=\"task-detail-chunks\" id=\"skillRelatedTasks\" style=\"gap:8px\"></div>\n </div>\n </div>\n <div class=\"analytics-view\" id=\"analyticsView\">\n <div class=\"metrics-toolbar\">\n <span style=\"font-size:12px;color:var(--text-sec);font-weight:600\" data-i18n=\"range\">Range</span>\n <button class=\"range-btn\" data-days=\"7\" onclick=\"setMetricsDays(7)\">7 <span data-i18n=\"range.days\">days</span></button>\n <button class=\"range-btn active\" data-days=\"30\" onclick=\"setMetricsDays(30)\">30 <span data-i18n=\"range.days\">days</span></button>\n <button class=\"range-btn\" data-days=\"90\" onclick=\"setMetricsDays(90)\">90 <span data-i18n=\"range.days\">days</span></button>\n <button class=\"btn btn-sm\" onclick=\"loadMetrics()\" style=\"margin-left:auto\" data-i18n=\"refresh\">\u21BB Refresh</button>\n </div>\n <div class=\"analytics-cards\" id=\"analyticsCards\">\n <div class=\"analytics-card\"><div class=\"ac-value\" id=\"mTotal\">-</div><div class=\"ac-label\" data-i18n=\"analytics.total\">Total Memories</div></div>\n <div class=\"analytics-card green\"><div class=\"ac-value\" id=\"mTodayWrites\">-</div><div class=\"ac-label\" data-i18n=\"analytics.writes\">Writes Today</div></div>\n <div class=\"analytics-card\"><div class=\"ac-value\" id=\"mSessions\">-</div><div class=\"ac-label\" data-i18n=\"analytics.sessions\">Sessions</div></div>\n <div class=\"analytics-card amber\"><div class=\"ac-value\" id=\"mEmbeddings\">-</div><div class=\"ac-label\" data-i18n=\"analytics.embeddings\">Embeddings</div></div>\n </div>\n <div class=\"analytics-section\">\n <h3><span class=\"icon\">\uD83D\uDCCA</span> <span data-i18n=\"chart.writes\">Memory Writes per Day</span></h3>\n <div class=\"chart-bars\" id=\"chartWrites\"></div>\n </div>\n \n <div class=\"analytics-section\" id=\"toolPerfSection\" style=\"position:relative\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;margin-bottom:20px\">\n <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>\n <div style=\"display:flex;gap:6px;align-items:center\">\n <button class=\"range-btn tool-range active\" data-mins=\"60\" onclick=\"setToolMinutes(60)\">1h</button>\n <button class=\"range-btn tool-range\" data-mins=\"360\" onclick=\"setToolMinutes(360)\">6h</button>\n <button class=\"range-btn tool-range\" data-mins=\"1440\" onclick=\"setToolMinutes(1440)\">24h</button>\n </div>\n </div>\n <div id=\"toolChart\" style=\"width:100%;height:280px;position:relative;overflow:hidden;border-radius:12px\"></div>\n <div id=\"toolLegend\" class=\"chart-legend\" style=\"margin-top:14px;padding:0 4px\"></div>\n <div id=\"toolAggTable\" style=\"margin-top:20px\"></div>\n </div>\n\n <div class=\"breakdown-grid\" style=\"display:grid;grid-template-columns:1fr 1fr;gap:24px\">\n <div class=\"analytics-section\">\n <h3><span class=\"icon\">\uD83D\uDC64</span> <span data-i18n=\"breakdown.role\">By Role</span></h3>\n <div id=\"breakdownRole\"></div>\n </div>\n <div class=\"analytics-section\">\n <h3><span class=\"icon\">\uD83D\uDCDD</span> <span data-i18n=\"breakdown.kind\">By Kind</span></h3>\n <div id=\"breakdownKind\"></div>\n </div>\n </div>\n </div>\n\n <!-- \u2500\u2500\u2500 Logs View \u2500\u2500\u2500 -->\n <div class=\"logs-view\" id=\"logsView\">\n <div class=\"logs-toolbar\">\n <div class=\"logs-toolbar-left\">\n <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\">\n <option value=\"\" data-i18n=\"logs.allTools\">All Tools</option>\n </select>\n <button class=\"btn btn-sm btn-ghost\" onclick=\"loadLogs()\" style=\"font-size:12px\">\u21BB <span data-i18n=\"logs.refresh\">Refresh</span></button>\n </div>\n <div class=\"logs-toolbar-right\">\n <input type=\"checkbox\" id=\"logAutoRefresh\" style=\"display:none\">\n </div>\n </div>\n <div class=\"logs-list\" id=\"logsList\"></div>\n <div id=\"logsPagination\"></div>\n </div>\n\n <!-- \u2500\u2500\u2500 Settings View \u2500\u2500\u2500 -->\n <div class=\"settings-view\" id=\"settingsView\">\n <div class=\"settings-group\" id=\"settingsModelConfig\">\n <h2 class=\"settings-group-title\"><span data-i18n=\"settings.modelconfig\">Model Configuration</span></h2>\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83D\uDCE1</span> <span data-i18n=\"settings.embedding\">Embedding Model</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-field\">\n <label data-i18n=\"settings.provider\">Provider</label>\n <select id=\"cfgEmbProvider\">\n <option value=\"openai_compatible\">OpenAI Compatible</option>\n <option value=\"openai\">OpenAI</option>\n <option value=\"gemini\">Gemini</option>\n <option value=\"azure_openai\">Azure OpenAI</option>\n <option value=\"cohere\">Cohere</option>\n <option value=\"mistral\">Mistral</option>\n <option value=\"voyage\">Voyage</option>\n <option value=\"local\">Local</option>\n </select>\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.model\">Model</label>\n <input type=\"text\" id=\"cfgEmbModel\" placeholder=\"e.g. bge-m3\">\n </div>\n <div class=\"settings-field full-width\">\n <label>Endpoint</label>\n <input type=\"text\" id=\"cfgEmbEndpoint\" placeholder=\"https://...\">\n </div>\n <div class=\"settings-field\">\n <label>API Key</label>\n <input type=\"password\" id=\"cfgEmbApiKey\" placeholder=\"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\">\n </div>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83E\uDDE0</span> <span data-i18n=\"settings.summarizer\">Summarizer Model</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-field\">\n <label data-i18n=\"settings.provider\">Provider</label>\n <select id=\"cfgSumProvider\">\n <option value=\"openai_compatible\">OpenAI Compatible</option>\n <option value=\"openai\">OpenAI</option>\n <option value=\"anthropic\">Anthropic</option>\n <option value=\"gemini\">Gemini</option>\n <option value=\"azure_openai\">Azure OpenAI</option>\n <option value=\"bedrock\">Bedrock</option>\n </select>\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.model\">Model</label>\n <input type=\"text\" id=\"cfgSumModel\" placeholder=\"e.g. gpt-4o-mini\">\n </div>\n <div class=\"settings-field full-width\">\n <label>Endpoint</label>\n <input type=\"text\" id=\"cfgSumEndpoint\" placeholder=\"https://...\">\n </div>\n <div class=\"settings-field\">\n <label>API Key</label>\n <input type=\"password\" id=\"cfgSumApiKey\" placeholder=\"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\">\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.temperature\">Temperature</label>\n <input type=\"number\" id=\"cfgSumTemp\" step=\"0.1\" min=\"0\" max=\"2\" placeholder=\"0\">\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83D\uDD27</span> <span data-i18n=\"settings.skill\">Skill Evolution</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-toggle\">\n <label class=\"toggle-switch\"><input type=\"checkbox\" id=\"cfgSkillEnabled\"><span class=\"toggle-slider\"></span></label>\n <label data-i18n=\"settings.skill.enabled\">Enable Skill Evolution</label>\n </div>\n <div class=\"settings-toggle\">\n <label class=\"toggle-switch\"><input type=\"checkbox\" id=\"cfgSkillAutoInstall\"><span class=\"toggle-slider\"></span></label>\n <label data-i18n=\"settings.skill.autoinstall\">Auto Install Skills</label>\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.skill.confidence\">Min Confidence</label>\n <input type=\"number\" id=\"cfgSkillConfidence\" step=\"0.1\" min=\"0\" max=\"1\" placeholder=\"0.7\">\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.skill.minchunks\">Min Chunks</label>\n <input type=\"number\" id=\"cfgSkillMinChunks\" placeholder=\"6\">\n </div>\n </div>\n <div style=\"margin-top:16px;padding-top:16px;border-top:1px solid var(--border)\">\n <h4 style=\"font-size:12px;font-weight:600;color:var(--text-sec);margin-bottom:12px\"><span data-i18n=\"settings.skill.model\">Skill Dedicated Model</span></h4>\n <div class=\"field-hint\" style=\"margin-bottom:12px\" 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>\n <div class=\"settings-grid\">\n <div class=\"settings-field\">\n <label data-i18n=\"settings.provider\">Provider</label>\n <select id=\"cfgSkillProvider\">\n <option value=\"\">\u2014 <span data-i18n=\"settings.skill.usemain\">Use main summarizer</span> \u2014</option>\n <option value=\"openai_compatible\">OpenAI Compatible</option>\n <option value=\"openai\">OpenAI</option>\n <option value=\"anthropic\">Anthropic</option>\n <option value=\"gemini\">Gemini</option>\n <option value=\"azure_openai\">Azure OpenAI</option>\n <option value=\"bedrock\">Bedrock</option>\n </select>\n </div>\n <div class=\"settings-field\">\n <label data-i18n=\"settings.model\">Model</label>\n <input type=\"text\" id=\"cfgSkillModel\" placeholder=\"e.g. claude-4.6-opus\">\n </div>\n <div class=\"settings-field full-width\">\n <label>Endpoint</label>\n <input type=\"text\" id=\"cfgSkillEndpoint\" placeholder=\"https://...\">\n </div>\n <div class=\"settings-field\">\n <label>API Key</label>\n <input type=\"password\" id=\"cfgSkillApiKey\" placeholder=\"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\">\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83D\uDCCA</span> <span data-i18n=\"settings.telemetry\">Telemetry</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-toggle\">\n <label class=\"toggle-switch\"><input type=\"checkbox\" id=\"cfgTelemetryEnabled\" checked><span class=\"toggle-slider\"></span></label>\n <label data-i18n=\"settings.telemetry.enabled\">Enable Anonymous Telemetry</label>\n </div>\n <div class=\"settings-field full-width\">\n <div class=\"field-hint\" data-i18n=\"settings.telemetry.hint\">Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.</div>\n </div>\n </div>\n </div>\n\n <div class=\"settings-section\">\n <h3><span class=\"icon\">\uD83D\uDCBE</span> <span data-i18n=\"settings.general\">General</span></h3>\n <div class=\"settings-grid\">\n <div class=\"settings-field\">\n <label data-i18n=\"settings.viewerport\">Viewer Port</label>\n <input type=\"number\" id=\"cfgViewerPort\" placeholder=\"18799\">\n <div class=\"field-hint\" data-i18n=\"settings.viewerport.hint\">Requires restart to take effect</div>\n </div>\n </div>\n </div>\n\n <div class=\"settings-actions\">\n <span class=\"settings-saved\" id=\"settingsSaved\">\u2713 <span data-i18n=\"settings.saved\">Saved</span></span>\n <button class=\"btn btn-ghost\" onclick=\"loadConfig()\" data-i18n=\"settings.reset\">Reset</button>\n <button class=\"btn btn-primary\" onclick=\"saveConfig()\" data-i18n=\"settings.save\">Save Settings</button>\n </div>\n <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>\n </div>\n\n <!-- \u2500\u2500\u2500 Import Page \u2500\u2500\u2500 -->\n <div class=\"migrate-view\" id=\"migrateView\">\n <div class=\"settings-section\" style=\"border:1px solid rgba(99,102,241,.15)\">\n <h3><span class=\"icon\">\uD83D\uDCE5</span> <span data-i18n=\"migrate.title\">Import OpenClaw Memory</span></h3>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:12px;line-height:1.6\" data-i18n=\"migrate.desc\">Migrate your existing OpenClaw built-in memories and conversation history into this plugin. The import process uses smart deduplication to avoid duplicates.</p>\n\n <div style=\"background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px;margin-bottom:16px;font-size:12px;line-height:1.7;color:var(--text-sec)\">\n <div style=\"font-weight:700;color:var(--text);margin-bottom:8px\" data-i18n=\"migrate.modes.title\">Three ways to use:</div>\n <div style=\"display:flex;flex-direction:column;gap:6px\">\n <div><span style=\"font-weight:600;color:var(--accent)\" data-i18n=\"migrate.mode1.label\">\u2460 Import memories only (fast)</span><span data-i18n=\"migrate.mode1.desc\"> \u2014 Click \"Start Import\" to quickly migrate all memory chunks and conversations. No task/skill generation. Suitable when you just need the raw data.</span></div>\n <div><span style=\"font-weight:600;color:var(--accent)\" data-i18n=\"migrate.mode2.label\">\u2461 Import + generate tasks & skills (slow, serial)</span><span data-i18n=\"migrate.mode2.desc\"> \u2014 After importing memories, enable \"Generate Tasks\" and/or \"Trigger Skill Evolution\" below to analyze conversations one by one. This takes longer as each session is processed by LLM sequentially.</span></div>\n <div><span style=\"font-weight:600;color:var(--accent)\" data-i18n=\"migrate.mode3.label\">\u2462 Import first, generate later (flexible)</span><span data-i18n=\"migrate.mode3.desc\"> \u2014 Import memories now, then come back anytime to start task/skill generation. You can pause the generation at any point and resume later \u2014 it will pick up where you left off, only processing sessions that haven't been handled yet.</span></div>\n </div>\n </div>\n\n <div id=\"migrateConfigWarn\" style=\"display:none;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.3);border-radius:10px;padding:14px 18px;margin-bottom:16px\">\n <div style=\"font-size:12px;font-weight:600;color:#f59e0b;margin-bottom:6px\">\u26A0 <span data-i18n=\"migrate.config.warn\">Configuration Required</span></div>\n <div style=\"font-size:11px;color:var(--text-sec);line-height:1.5\" data-i18n=\"migrate.config.warn.desc\">Please configure both Embedding Model and Summarizer Model in Settings before importing. These are required for processing memories.</div>\n </div>\n\n <div id=\"migrateScanResult\" style=\"display:none;margin-bottom:16px\">\n <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:12px\">\n <div style=\"background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px\">\n <div style=\"font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:6px\" data-i18n=\"migrate.sqlite.label\">Memory Index (SQLite)</div>\n <div style=\"font-size:22px;font-weight:700;color:var(--text)\" id=\"migrateSqliteCount\">0</div>\n <div style=\"font-size:10px;color:var(--text-muted);margin-top:2px\" id=\"migrateSqliteFiles\"></div>\n </div>\n <div style=\"background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px\">\n <div style=\"font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:6px\" data-i18n=\"migrate.sessions.label\">Conversation History</div>\n <div style=\"font-size:22px;font-weight:700;color:var(--text)\" id=\"migrateSessionCount\">0</div>\n <div style=\"font-size:10px;color:var(--text-muted);margin-top:2px\" id=\"migrateSessionFiles\"></div>\n </div>\n </div>\n </div>\n\n <div id=\"migrateActions\" style=\"display:flex;gap:12px;align-items:center;flex-wrap:wrap\">\n <button class=\"btn btn-ghost\" onclick=\"migrateScan()\" id=\"migrateScanBtn\" data-i18n=\"migrate.scan\">Scan Data Sources</button>\n <button class=\"btn btn-primary\" onclick=\"migrateStart()\" id=\"migrateStartBtn\" style=\"display:none\" data-i18n=\"migrate.start\">Start Import</button>\n <span id=\"migrateStatus\" style=\"font-size:11px;color:var(--text-muted)\"></span>\n </div>\n\n <!-- Post-process section: shown after import completes -->\n <div id=\"postprocessSection\" style=\"display:none;margin-top:16px\">\n <div class=\"settings-section\" style=\"border:1px solid var(--border)\">\n <div style=\"font-size:14px;font-weight:700;color:var(--text);margin-bottom:6px\" data-i18n=\"pp.title\">\uD83E\uDDE0 Optional: Generate Tasks & Skills</div>\n <div style=\"font-size:12px;color:var(--text-sec);margin-bottom:14px;line-height:1.6\" data-i18n=\"pp.desc\">This step is completely optional. The import above has already stored raw memory data. Here you can further analyze imported conversations to generate structured task summaries and evolve reusable skills. Processing is serial (one session at a time) and may take a while. You can stop at any time and resume later \u2014 it will only process sessions not yet handled.</div>\n <div style=\"display:flex;flex-direction:column;gap:8px;margin-bottom:14px\">\n <label style=\"display:flex;align-items:flex-start;gap:8px;cursor:pointer\">\n <input type=\"checkbox\" id=\"ppEnableTasks\" checked style=\"accent-color:var(--accent);margin-top:2px\">\n <div>\n <div style=\"font-size:12px;font-weight:600;color:var(--text)\" data-i18n=\"pp.tasks.label\">Generate task summaries</div>\n <div style=\"font-size:11px;color:var(--text-sec);line-height:1.4\" data-i18n=\"pp.tasks.hint\">Group imported messages into tasks and generate a structured summary (title, goal, steps, result) for each one. Makes it easier to search and recall past work.</div>\n </div>\n </label>\n <label style=\"display:flex;align-items:flex-start;gap:8px;cursor:pointer\">\n <input type=\"checkbox\" id=\"ppEnableSkills\" style=\"accent-color:var(--accent);margin-top:2px\">\n <div>\n <div style=\"font-size:12px;font-weight:600;color:var(--text)\" data-i18n=\"pp.skills.label\">Trigger skill evolution</div>\n <div style=\"font-size:11px;color:var(--text-sec);line-height:1.4\" data-i18n=\"pp.skills.hint\">Analyze completed tasks and automatically create or upgrade reusable skills (SKILL.md). Requires task summaries to be enabled. May take longer due to LLM evaluation.</div>\n </div>\n </label>\n </div>\n <div style=\"display:flex;gap:10px;align-items:center\">\n <button class=\"btn btn-primary\" id=\"ppStartBtn\" onclick=\"ppStart()\" data-i18n=\"pp.start\">Start Processing</button>\n <button class=\"btn btn-sm\" id=\"ppStopBtn\" onclick=\"ppStop()\" style=\"display:none;background:rgba(239,68,68,.12);color:#ef4444;border:1px solid rgba(239,68,68,.3);font-size:12px;padding:5px 16px;font-weight:600\" data-i18n=\"migrate.stop\">\u25A0 Stop</button>\n <span id=\"ppStatus\" style=\"font-size:11px;color:var(--text-muted)\"></span>\n </div>\n <div id=\"ppProgress\" style=\"display:none;margin-top:12px\">\n <div style=\"display:flex;align-items:center;gap:12px;margin-bottom:8px\">\n <div style=\"font-size:12px;font-weight:600;color:var(--text)\" id=\"ppPhaseLabel\"></div>\n <div style=\"font-size:11px;color:var(--text-muted);flex:1\" id=\"ppCounter\"></div>\n </div>\n <div style=\"position:relative;height:5px;background:var(--bg);border-radius:3px;overflow:hidden;margin-bottom:12px\">\n <div id=\"ppBar\" style=\"position:absolute;left:0;top:0;height:100%;width:0%;background:linear-gradient(90deg,#f59e0b,#fbbf24);border-radius:3px;transition:width .3s ease\"></div>\n </div>\n <div style=\"display:flex;gap:16px;margin-bottom:12px\" id=\"ppStatsRow\">\n <div style=\"display:flex;align-items:center;gap:5px;font-size:11px\">\n <span style=\"width:7px;height:7px;border-radius:50%;background:#22c55e;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"pp.stat.tasks\">Tasks</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"ppStatTasks\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:5px;font-size:11px\">\n <span style=\"width:7px;height:7px;border-radius:50%;background:#8b5cf6;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"pp.stat.skills\">Skills</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"ppStatSkills\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:5px;font-size:11px\">\n <span style=\"width:7px;height:7px;border-radius:50%;background:#ef4444;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"pp.stat.errors\">Errors</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"ppStatErrors\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:5px;font-size:11px\" id=\"ppSkippedInfo\" style=\"display:none\">\n <span style=\"width:7px;height:7px;border-radius:50%;background:#3b82f6;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"pp.stat.skipped\">Skipped</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"ppStatSkipped\">0</span>\n </div>\n </div>\n <div id=\"ppLiveLog\" style=\"background:var(--bg);border:1px solid var(--border);border-radius:8px;max-height:320px;overflow-y:auto;font-family:'SF Mono','Fira Code',monospace;font-size:11px;line-height:1.7;padding:0\"></div>\n </div>\n <div id=\"ppDone\" style=\"display:none;margin-top:12px;padding:10px 14px;border-radius:8px;font-size:12px;color:var(--text-sec);line-height:1.5\"></div>\n </div>\n </div>\n </div>\n\n <!-- Progress Area -->\n <div id=\"migrateProgress\" style=\"display:none\">\n <div class=\"settings-section\">\n <div style=\"display:flex;align-items:center;gap:12px;margin-bottom:12px\">\n <div style=\"font-size:13px;font-weight:600;color:var(--text)\" id=\"migratePhaseLabel\"></div>\n <div style=\"font-size:12px;color:var(--text-muted);flex:1\" id=\"migrateCounter\"></div>\n <button class=\"btn btn-sm\" id=\"migrateStopBtn\" onclick=\"migrateStop()\" style=\"background:rgba(239,68,68,.12);color:#ef4444;border:1px solid rgba(239,68,68,.3);font-size:12px;padding:5px 16px;font-weight:600;cursor:pointer\" data-i18n=\"migrate.stop\">\u25A0 Stop</button>\n </div>\n\n <div style=\"position:relative;height:6px;background:var(--bg);border-radius:3px;overflow:hidden;margin-bottom:16px\">\n <div id=\"migrateBar\" style=\"position:absolute;left:0;top:0;height:100%;width:0%;background:linear-gradient(90deg,#6366f1,#8b5cf6);border-radius:3px;transition:width .3s ease\"></div>\n </div>\n\n <div style=\"display:flex;gap:20px;margin-bottom:16px\" id=\"migrateStatsRow\">\n <div style=\"display:flex;align-items:center;gap:6px;font-size:12px\">\n <span style=\"width:8px;height:8px;border-radius:50%;background:#22c55e;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"migrate.stat.stored\">Stored</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"migrateStatStored\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:6px;font-size:12px\">\n <span style=\"width:8px;height:8px;border-radius:50%;background:#f59e0b;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"migrate.stat.skipped\">Skipped</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"migrateStatSkipped\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:6px;font-size:12px\">\n <span style=\"width:8px;height:8px;border-radius:50%;background:#3b82f6;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"migrate.stat.merged\">Merged</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"migrateStatMerged\">0</span>\n </div>\n <div style=\"display:flex;align-items:center;gap:6px;font-size:12px\">\n <span style=\"width:8px;height:8px;border-radius:50%;background:#ef4444;display:inline-block\"></span>\n <span style=\"color:var(--text-sec)\" data-i18n=\"migrate.stat.errors\">Errors</span>\n <span style=\"font-weight:700;color:var(--text)\" id=\"migrateStatErrors\">0</span>\n </div>\n </div>\n\n <div id=\"migrateLiveLog\" style=\"background:var(--bg);border:1px solid var(--border);border-radius:10px;max-height:480px;overflow-y:auto;font-family:'SF Mono','Fira Code',monospace;font-size:11px;line-height:1.7;padding:0\">\n </div>\n </div>\n </div>\n\n </div>\n\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Memory Modal \u2500\u2500\u2500 -->\n<div class=\"modal-overlay\" id=\"modalOverlay\">\n <div class=\"modal\">\n <h2 id=\"modalTitle\" data-i18n=\"modal.new\">New Memory</h2>\n <div class=\"form-group\"><label data-i18n=\"modal.role\">Role</label><select id=\"mRole\"><option value=\"user\">User</option><option value=\"assistant\">Assistant</option><option value=\"system\">System</option></select></div>\n <div class=\"form-group\"><label data-i18n=\"modal.content\">Content</label><textarea id=\"mContent\" rows=\"4\" data-i18n-ph=\"modal.content.ph\" placeholder=\"Memory content...\"></textarea></div>\n <div class=\"form-group\"><label data-i18n=\"modal.summary\">Summary</label><input type=\"text\" id=\"mSummary\" data-i18n-ph=\"modal.summary.ph\" placeholder=\"Brief summary (optional)\"></div>\n <div class=\"form-group\"><label data-i18n=\"modal.kind\">Kind</label><select id=\"mKind\"><option value=\"paragraph\" data-i18n=\"filter.paragraph\">Paragraph</option><option value=\"code\" data-i18n=\"filter.code\">Code</option><option value=\"dialog\" data-i18n=\"filter.dialog\">Dialog</option></select></div>\n <div class=\"modal-actions\">\n <button class=\"btn btn-ghost\" onclick=\"closeModal()\" data-i18n=\"modal.cancel\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"modalSubmit\" onclick=\"submitModal()\" data-i18n=\"modal.create\">Create</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Toast \u2500\u2500\u2500 -->\n<div class=\"toast-container\" id=\"toasts\"></div>\n\n<script>\nlet activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=40,metricsDays=30;\n\n/* \u2500\u2500\u2500 i18n \u2500\u2500\u2500 */\nconst I18N={\n en:{\n 'title':'OpenClaw Memory',\n 'subtitle':'Powered by MemOS',\n 'setup.desc':'Set a password to protect your memories',\n 'setup.pw':'Enter a password (4+ characters)',\n 'setup.pw2':'Confirm password',\n 'setup.btn':'Set Password & Enter',\n 'setup.err.short':'Password must be at least 4 characters',\n 'setup.err.mismatch':'Passwords do not match',\n 'setup.err.fail':'Setup failed',\n 'login.desc':'Enter your password to access memories',\n 'login.pw':'Password',\n 'login.btn':'Unlock',\n 'login.err':'Incorrect password',\n 'login.forgot':'Forgot password?',\n 'reset.step1.title':'Open Terminal',\n 'reset.step1.desc':'Run the following command to get your reset token (use the pattern below so you get the line that contains the token):',\n 'reset.step2.title':'Find the token',\n 'reset.step2.desc.pre':'In the output, find ',\n 'reset.step2.desc.post':' (plain line or inside JSON). Copy the 32-character hex string after the colon.',\n 'reset.step3.title':'Paste & reset',\n 'reset.step3.desc':'Paste the token below and set your new password.',\n 'reset.token':'Paste reset token here',\n 'reset.newpw':'New password (4+ characters)',\n 'reset.newpw2':'Confirm new password',\n 'reset.btn':'Reset Password',\n 'reset.err.token':'Please enter the reset token',\n 'reset.err.short':'Password must be at least 4 characters',\n 'reset.err.mismatch':'Passwords do not match',\n 'reset.err.fail':'Reset failed',\n 'reset.back':'\\u2190 Back to login',\n 'copy.hint':'Click to copy',\n 'copy.done':'Copied!',\n 'tab.memories':'\\u{1F4DA} Memories',\n 'tab.tasks':'\\u{1F4CB} Tasks',\n 'tab.skills':'\\u{1F9E0} Skills',\n 'tab.analytics':'\\u{1F4CA} Analytics',\n 'skills.total':'Total Skills',\n 'skills.active':'Active',\n 'skills.installed':'Installed',\n 'tasks.total':'Total Tasks',\n 'tasks.active':'Active',\n 'tasks.completed':'Completed',\n 'tasks.status.active':'Active',\n 'tasks.status.completed':'Completed',\n 'tasks.status.skipped':'Skipped',\n 'tasks.empty':'No tasks yet. Tasks are automatically created as you converse.',\n 'tasks.loading':'Loading...',\n 'tasks.untitled':'Untitled Task',\n 'tasks.chunks':'Related Memories',\n 'tasks.nochunks':'No memories in this task yet.',\n 'tasks.skipped.default':'This conversation was too brief to generate a summary. It will not appear in search results.',\n 'refresh':'\\u21BB Refresh',\n 'logout':'Logout',\n 'stat.memories':'Memories',\n 'stat.sessions':'Sessions',\n 'stat.embeddings':'Embeddings',\n 'stat.days':'Days',\n 'stat.active':'active',\n 'stat.deduped':'deduped',\n 'sidebar.sessions':'Sessions',\n 'sidebar.allsessions':'All Sessions',\n 'sidebar.clear':'\\u{1F5D1} Clear All Data',\n 'search.placeholder':'Search memories (supports semantic search)...',\n 'search.meta.total':' memories total',\n 'search.meta.semantic':' semantic',\n 'search.meta.text':' text',\n 'search.meta.results':' results',\n 'filter.all':'All',\n 'filter.allkinds':'All kinds',\n 'filter.paragraph':'Paragraph',\n 'filter.code':'Code',\n 'filter.dialog':'Dialog',\n 'filter.list':'List',\n 'filter.error':'Error',\n 'filter.command':'Command',\n 'filter.newest':'Newest first',\n 'filter.oldest':'Oldest first',\n 'filter.from':'From',\n 'filter.to':'To',\n 'filter.clear':'Clear',\n 'empty.text':'No memories found',\n 'card.expand':'Expand',\n 'card.edit':'Edit',\n 'card.delete':'Delete',\n 'card.evolved':'Evolved',\n 'card.times':'times',\n 'card.updated':'updated',\n 'card.evolveHistory':'Evolution History',\n 'card.oldSummary':'Old',\n 'card.dedupDuplicate':'Duplicate',\n 'card.dedupMerged':'Merged',\n 'card.dedupTarget':'Target: ',\n 'card.dedupReason':'Reason: ',\n 'card.newSummary':'New',\n 'pagination.total':' total',\n 'range':'Range',\n 'range.days':'days',\n 'analytics.total':'Total Memories',\n 'analytics.writes':'Writes Today',\n 'analytics.calls':'Viewer Calls Today',\n 'analytics.sessions':'Sessions',\n 'analytics.embeddings':'Embeddings',\n 'chart.writes':'Memory Writes per Day',\n 'chart.calls':'Viewer API Calls per Day (List / Search)',\n 'chart.nodata':'No data in this range',\n 'chart.nocalls':'No viewer calls in this range',\n 'chart.toolperf':'Tool Response Time',\n 'chart.list':'List',\n 'chart.search':'Search',\n 'breakdown.role':'By Role',\n 'breakdown.kind':'By Kind',\n 'modal.new':'New Memory',\n 'modal.edit':'Edit Memory',\n 'modal.role':'Role',\n 'modal.content':'Content',\n 'modal.content.ph':'Memory content...',\n 'modal.summary':'Summary',\n 'modal.summary.ph':'Brief summary (optional)',\n 'modal.kind':'Kind',\n 'modal.cancel':'Cancel',\n 'modal.create':'Create',\n 'modal.save':'Save',\n 'modal.err.empty':'Please enter content',\n 'toast.created':'Memory created',\n 'toast.updated':'Memory updated',\n 'toast.deleted':'Memory deleted',\n 'toast.opfail':'Operation failed',\n 'toast.delfail':'Delete failed',\n 'toast.cleared':'All memories cleared',\n 'toast.clearfail':'Clear failed',\n 'toast.notfound':'Memory not found in cache',\n 'confirm.delete':'Delete this memory?',\n 'confirm.clearall':'Delete ALL memories? This cannot be undone.',\n 'confirm.clearall2':'Are you absolutely sure?',\n 'embed.on':'Embedding: ',\n 'embed.off':'No embedding model',\n 'lang.switch':'\u4E2D',\n 'tab.logs':'\uD83D\uDCDD Logs',\n 'logs.allTools':'All Tools',\n 'logs.refresh':'Refresh',\n 'logs.autoRefresh':'Auto-refresh',\n 'logs.input':'INPUT',\n 'logs.output':'OUTPUT',\n 'logs.empty':'No logs yet. Logs will appear here when tools are called.',\n 'logs.ago':'ago',\n 'tab.import':'\uD83D\uDCE5 Import',\n 'tab.settings':'\u2699 Settings',\n 'settings.modelconfig':'Model Configuration',\n 'settings.embedding':'Embedding Model',\n 'settings.summarizer':'Summarizer Model',\n 'settings.skill':'Skill Evolution',\n 'settings.general':'General',\n 'settings.provider':'Provider',\n 'settings.model':'Model',\n 'settings.temperature':'Temperature',\n 'settings.skill.enabled':'Enable Skill Evolution',\n 'settings.skill.autoinstall':'Auto Install Skills',\n 'settings.skill.confidence':'Min Confidence',\n 'settings.skill.minchunks':'Min Chunks',\n 'settings.skill.model':'Skill Dedicated Model',\n '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.',\n 'settings.optional':'Optional',\n 'settings.skill.usemain':'Use Main Summarizer',\n 'settings.telemetry':'Telemetry',\n 'settings.telemetry.enabled':'Enable Anonymous Telemetry',\n 'settings.telemetry.hint':'Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.',\n 'settings.viewerport':'Viewer Port',\n 'settings.viewerport.hint':'Requires restart to take effect',\n 'settings.save':'Save Settings',\n 'settings.reset':'Reset',\n 'settings.saved':'Saved',\n 'settings.restart.hint':'Some changes require restarting the OpenClaw gateway to take effect.',\n 'settings.save.fail':'Failed to save settings',\n 'migrate.title':'Import OpenClaw Memory',\n 'migrate.desc':'Migrate your existing OpenClaw built-in memories and conversation history into this plugin. The import process uses smart deduplication to avoid duplicates.',\n 'migrate.modes.title':'Three ways to use:',\n 'migrate.mode1.label':'\\u2460 Import memories only (fast)',\n 'migrate.mode1.desc':' \u2014 Click \"Start Import\" to quickly migrate all memory chunks and conversations. No task/skill generation. Suitable when you just need the raw data.',\n 'migrate.mode2.label':'\\u2461 Import + generate tasks & skills (slow, serial)',\n 'migrate.mode2.desc':' \u2014 After importing memories, enable \"Generate Tasks\" and/or \"Trigger Skill Evolution\" below to analyze conversations one by one. This takes longer as each session is processed by LLM sequentially.',\n 'migrate.mode3.label':'\\u2462 Import first, generate later (flexible)',\n 'migrate.mode3.desc':' \u2014 Import memories now, then come back anytime to start task/skill generation. You can pause the generation at any point and resume later \u2014 it will pick up where you left off, only processing sessions that haven\\'t been handled yet.',\n 'migrate.config.warn':'Configuration Required',\n 'migrate.config.warn.desc':'Please configure both Embedding Model and Summarizer Model above before importing. These are required for processing memories.',\n 'migrate.sqlite.label':'Memory Index (SQLite)',\n 'migrate.sessions.label':'Conversation History',\n 'migrate.scan':'Scan Data Sources',\n 'migrate.start':'Start Import',\n 'migrate.scanning':'Scanning...',\n 'migrate.stat.stored':'Stored',\n 'migrate.stat.skipped':'Skipped',\n 'migrate.stat.merged':'Merged',\n 'migrate.stat.errors':'Errors',\n 'migrate.phase.sqlite':'Importing memory index...',\n 'migrate.phase.sessions':'Importing conversation history...',\n 'migrate.chunks':'chunks',\n 'migrate.sessions.count':'sessions, {n} messages',\n 'migrate.nodata':'No OpenClaw data found to import.',\n 'migrate.running':'Import in progress...',\n 'migrate.error.running':'A migration is already in progress.',\n 'migrate.stop':'\\u25A0 Stop',\n 'migrate.stopping':'Stopping...',\n 'migrate.resume':'Continue Import',\n 'pp.title':'\\u{1F9E0} Optional: Generate Tasks & Skills',\n 'pp.desc':'This step is completely optional. The import above has already stored raw memory data. Here you can further analyze imported conversations to generate structured task summaries and evolve reusable skills. Processing is serial (one session at a time) and may take a while. You can stop at any time and resume later \u2014 it will only process sessions not yet handled.',\n 'pp.tasks.label':'Generate task summaries',\n 'pp.tasks.hint':'Group imported messages into tasks and generate a structured summary (title, goal, steps, result) for each one. Makes it easier to search and recall past work.',\n 'pp.skills.label':'Trigger skill evolution',\n 'pp.skills.hint':'Analyze completed tasks and automatically create or upgrade reusable skills (SKILL.md). Requires task summaries to be enabled. May take longer due to LLM evaluation.',\n 'pp.start':'Start Processing',\n 'pp.resume':'Resume Processing',\n 'pp.running':'Processing',\n 'pp.stopped':'Processing stopped. You can resume anytime.',\n 'pp.failed':'Processing failed \u2014 see error message above.',\n 'pp.done':'Task & skill generation complete!',\n 'pp.select.warn':'Please select at least one option.',\n 'pp.skill.created':'Skill created',\n 'pp.stat.tasks':'Tasks',\n 'pp.stat.skills':'Skills',\n 'pp.stat.errors':'Errors',\n 'pp.stat.skipped':'Skipped',\n 'pp.info.skipped':'{n} sessions already processed, skipping.',\n 'pp.info.pending':'Processing {n} sessions...',\n 'pp.info.allDone':'All sessions have been processed already. Nothing to do.',\n 'pp.action.full':'Task+Skill',\n 'pp.action.skillOnly':'Skill only (task exists)',\n 'card.imported':'OpenClaw Native',\n 'skills.draft':'Draft',\n 'skills.filter.active':'Active',\n 'skills.filter.draft':'Draft',\n 'skills.filter.archived':'Archived',\n 'skills.files':'Skill Files',\n 'skills.content':'SKILL.md Content',\n 'skills.versions':'Version History',\n 'skills.related':'Related Tasks',\n 'skills.download':'\u2B07 Download',\n 'skills.installed.badge':'Installed',\n 'skills.empty':'No skills yet. Skills are automatically generated from completed tasks that contain reusable experience.',\n 'skills.loading':'Loading...',\n 'skills.error':'Error loading skill',\n 'skills.error.detail':'Failed to load skill: ',\n 'skills.nofiles':'No files found',\n 'skills.noversions':'No versions recorded',\n 'skills.norelated':'No related tasks',\n 'skills.nocontent':'No content available',\n 'skills.nochangelog':'No changelog',\n 'skills.status.active':'Active',\n 'skills.status.draft':'Draft',\n 'skills.status.archived':'Archived',\n 'skills.updated':'Updated: ',\n 'skills.task.prefix':'Task: ',\n 'tasks.chunks.label':'chunks',\n 'tasks.taskid':'Task ID: ',\n 'tasks.role.user':'You',\n 'tasks.role.assistant':'Assistant',\n 'tasks.error':'Error',\n 'tasks.error.detail':'Failed to load task details',\n 'tasks.untitled.related':'Untitled'\n },\n zh:{\n 'title':'OpenClaw \u8BB0\u5FC6',\n 'subtitle':'\u7531 MemOS \u9A71\u52A8',\n 'setup.desc':'\u8BBE\u7F6E\u5BC6\u7801\u4EE5\u4FDD\u62A4\u4F60\u7684\u8BB0\u5FC6\u6570\u636E',\n 'setup.pw':'\u8F93\u5165\u5BC6\u7801\uFF08\u81F3\u5C114\u4F4D\uFF09',\n 'setup.pw2':'\u786E\u8BA4\u5BC6\u7801',\n 'setup.btn':'\u8BBE\u7F6E\u5BC6\u7801\u5E76\u8FDB\u5165',\n 'setup.err.short':'\u5BC6\u7801\u81F3\u5C11\u9700\u89814\u4E2A\u5B57\u7B26',\n 'setup.err.mismatch':'\u4E24\u6B21\u5BC6\u7801\u4E0D\u4E00\u81F4',\n 'setup.err.fail':'\u8BBE\u7F6E\u5931\u8D25',\n 'login.desc':'\u8F93\u5165\u5BC6\u7801\u4EE5\u8BBF\u95EE\u8BB0\u5FC6',\n 'login.pw':'\u5BC6\u7801',\n 'login.btn':'\u89E3\u9501',\n 'login.err':'\u5BC6\u7801\u9519\u8BEF',\n 'login.forgot':'\u5FD8\u8BB0\u5BC6\u7801\uFF1F',\n 'reset.step1.title':'\u6253\u5F00\u7EC8\u7AEF',\n 'reset.step1.desc':'\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u83B7\u53D6\u91CD\u7F6E\u4EE4\u724C\uFF1A',\n 'reset.step2.title':'\u627E\u5230\u4EE4\u724C',\n 'reset.step2.desc.pre':'\u5728\u8F93\u51FA\u4E2D\u627E\u5230 ',\n 'reset.step2.desc.post':'\uFF08\u7EAF\u6587\u672C\u884C\u6216 JSON \u5185\uFF09\u3002\u590D\u5236\u5192\u53F7\u540E\u768432\u4F4D\u5341\u516D\u8FDB\u5236\u5B57\u7B26\u4E32\u3002',\n 'reset.step3.title':'\u7C98\u8D34\u5E76\u91CD\u7F6E',\n 'reset.step3.desc':'\u5C06\u4EE4\u724C\u7C98\u8D34\u5230\u4E0B\u65B9\u5E76\u8BBE\u7F6E\u65B0\u5BC6\u7801\u3002',\n 'reset.token':'\u5728\u6B64\u7C98\u8D34\u91CD\u7F6E\u4EE4\u724C',\n 'reset.newpw':'\u65B0\u5BC6\u7801\uFF08\u81F3\u5C114\u4F4D\uFF09',\n 'reset.newpw2':'\u786E\u8BA4\u65B0\u5BC6\u7801',\n 'reset.btn':'\u91CD\u7F6E\u5BC6\u7801',\n 'reset.err.token':'\u8BF7\u8F93\u5165\u91CD\u7F6E\u4EE4\u724C',\n 'reset.err.short':'\u5BC6\u7801\u81F3\u5C11\u9700\u89814\u4E2A\u5B57\u7B26',\n 'reset.err.mismatch':'\u4E24\u6B21\u5BC6\u7801\u4E0D\u4E00\u81F4',\n 'reset.err.fail':'\u91CD\u7F6E\u5931\u8D25',\n 'reset.back':'\\u2190 \u8FD4\u56DE\u767B\u5F55',\n 'copy.hint':'\u70B9\u51FB\u590D\u5236',\n 'copy.done':'\u5DF2\u590D\u5236\uFF01',\n 'tab.memories':'\\u{1F4DA} \u8BB0\u5FC6',\n 'tab.tasks':'\\u{1F4CB} \u4EFB\u52A1',\n 'tab.skills':'\\u{1F9E0} \u6280\u80FD',\n 'tab.analytics':'\\u{1F4CA} \u5206\u6790',\n 'skills.total':'\u6280\u80FD\u603B\u6570',\n 'skills.active':'\u751F\u6548\u4E2D',\n 'skills.installed':'\u5DF2\u5B89\u88C5',\n 'tasks.total':'\u4EFB\u52A1\u603B\u6570',\n 'tasks.active':'\u8FDB\u884C\u4E2D',\n 'tasks.completed':'\u5DF2\u5B8C\u6210',\n 'tasks.status.active':'\u8FDB\u884C\u4E2D',\n 'tasks.status.completed':'\u5DF2\u5B8C\u6210',\n 'tasks.status.skipped':'\u5DF2\u8DF3\u8FC7',\n 'tasks.empty':'\u6682\u65E0\u4EFB\u52A1\u3002\u4EFB\u52A1\u4F1A\u968F\u7740\u5BF9\u8BDD\u81EA\u52A8\u521B\u5EFA\u3002',\n 'tasks.loading':'\u52A0\u8F7D\u4E2D...',\n 'tasks.untitled':'\u672A\u547D\u540D\u4EFB\u52A1',\n 'tasks.chunks':'\u5173\u8054\u8BB0\u5FC6',\n 'tasks.nochunks':'\u6B64\u4EFB\u52A1\u6682\u65E0\u5173\u8054\u8BB0\u5FC6\u3002',\n 'tasks.skipped.default':'\u5BF9\u8BDD\u5185\u5BB9\u8FC7\u5C11\uFF0C\u672A\u751F\u6210\u6458\u8981\u3002\u8BE5\u4EFB\u52A1\u4E0D\u4F1A\u51FA\u73B0\u5728\u68C0\u7D22\u7ED3\u679C\u4E2D\u3002',\n 'refresh':'\\u21BB \u5237\u65B0',\n 'logout':'\u9000\u51FA',\n 'stat.memories':'\u8BB0\u5FC6',\n 'stat.sessions':'\u4F1A\u8BDD',\n 'stat.embeddings':'\u5D4C\u5165',\n 'stat.days':'\u5929\u6570',\n 'stat.active':'\u6D3B\u8DC3',\n 'stat.deduped':'\u5DF2\u53BB\u91CD',\n 'sidebar.sessions':'\u4F1A\u8BDD\u5217\u8868',\n 'sidebar.allsessions':'\u5168\u90E8\u4F1A\u8BDD',\n 'sidebar.clear':'\\u{1F5D1} \u6E05\u9664\u6240\u6709\u6570\u636E',\n 'search.placeholder':'\u641C\u7D22\u8BB0\u5FC6\uFF08\u652F\u6301\u8BED\u4E49\u641C\u7D22\uFF09...',\n 'search.meta.total':' \u6761\u8BB0\u5FC6',\n 'search.meta.semantic':' \u8BED\u4E49',\n 'search.meta.text':' \u6587\u672C',\n 'search.meta.results':' \u6761\u7ED3\u679C',\n 'filter.all':'\u5168\u90E8',\n 'filter.allkinds':'\u6240\u6709\u7C7B\u578B',\n 'filter.paragraph':'\u6BB5\u843D',\n 'filter.code':'\u4EE3\u7801',\n 'filter.dialog':'\u5BF9\u8BDD',\n 'filter.list':'\u5217\u8868',\n 'filter.error':'\u9519\u8BEF',\n 'filter.command':'\u547D\u4EE4',\n 'filter.newest':'\u6700\u65B0\u4F18\u5148',\n 'filter.oldest':'\u6700\u65E9\u4F18\u5148',\n 'filter.from':'\u8D77\u59CB',\n 'filter.to':'\u622A\u6B62',\n 'filter.clear':'\u6E05\u9664',\n 'empty.text':'\u6682\u65E0\u8BB0\u5FC6',\n 'card.expand':'\u5C55\u5F00',\n 'card.edit':'\u7F16\u8F91',\n 'card.delete':'\u5220\u9664',\n 'card.evolved':'\u5DF2\u6F14\u5316',\n 'card.times':'\u6B21',\n 'card.updated':'\u66F4\u65B0\u4E8E',\n 'card.evolveHistory':'\u6F14\u5316\u8BB0\u5F55',\n 'card.oldSummary':'\u65E7\u6458\u8981',\n 'card.dedupDuplicate':'\u91CD\u590D',\n 'card.dedupMerged':'\u5DF2\u5408\u5E76',\n 'card.dedupTarget':'\u5173\u8054: ',\n 'card.dedupReason':'\u539F\u56E0: ',\n 'card.newSummary':'\u65B0\u6458\u8981',\n 'pagination.total':' \u6761',\n 'range':'\u8303\u56F4',\n 'range.days':'\u5929',\n 'analytics.total':'\u603B\u8BB0\u5FC6\u6570',\n 'analytics.writes':'\u4ECA\u65E5\u5199\u5165',\n 'analytics.calls':'\u4ECA\u65E5\u67E5\u770B\u5668\u8C03\u7528',\n 'analytics.sessions':'\u4F1A\u8BDD\u6570',\n 'analytics.embeddings':'\u5D4C\u5165\u6570',\n 'chart.writes':'\u6BCF\u65E5\u8BB0\u5FC6\u5199\u5165',\n 'chart.calls':'\u6BCF\u65E5\u67E5\u770B\u5668 API \u8C03\u7528\uFF08\u5217\u8868 / \u641C\u7D22\uFF09',\n 'chart.nodata':'\u6B64\u8303\u56F4\u5185\u6682\u65E0\u6570\u636E',\n 'chart.nocalls':'\u6B64\u8303\u56F4\u5185\u6682\u65E0\u67E5\u770B\u5668\u8C03\u7528',\n 'chart.toolperf':'\u5DE5\u5177\u54CD\u5E94\u8017\u65F6',\n 'chart.list':'\u5217\u8868',\n 'chart.search':'\u641C\u7D22',\n 'breakdown.role':'\u6309\u89D2\u8272',\n 'breakdown.kind':'\u6309\u7C7B\u578B',\n 'modal.new':'\u65B0\u5EFA\u8BB0\u5FC6',\n 'modal.edit':'\u7F16\u8F91\u8BB0\u5FC6',\n 'modal.role':'\u89D2\u8272',\n 'modal.content':'\u5185\u5BB9',\n 'modal.content.ph':'\u8BB0\u5FC6\u5185\u5BB9...',\n 'modal.summary':'\u6458\u8981',\n 'modal.summary.ph':'\u7B80\u8981\u6458\u8981\uFF08\u53EF\u9009\uFF09',\n 'modal.kind':'\u7C7B\u578B',\n 'modal.cancel':'\u53D6\u6D88',\n 'modal.create':'\u521B\u5EFA',\n 'modal.save':'\u4FDD\u5B58',\n 'modal.err.empty':'\u8BF7\u8F93\u5165\u5185\u5BB9',\n 'toast.created':'\u8BB0\u5FC6\u5DF2\u521B\u5EFA',\n 'toast.updated':'\u8BB0\u5FC6\u5DF2\u66F4\u65B0',\n 'toast.deleted':'\u8BB0\u5FC6\u5DF2\u5220\u9664',\n 'toast.opfail':'\u64CD\u4F5C\u5931\u8D25',\n 'toast.delfail':'\u5220\u9664\u5931\u8D25',\n 'toast.cleared':'\u6240\u6709\u8BB0\u5FC6\u5DF2\u6E05\u9664',\n 'toast.clearfail':'\u6E05\u9664\u5931\u8D25',\n 'toast.notfound':'\u7F13\u5B58\u4E2D\u672A\u627E\u5230\u6B64\u8BB0\u5FC6',\n 'confirm.delete':'\u786E\u5B9A\u8981\u5220\u9664\u8FD9\u6761\u8BB0\u5FC6\u5417\uFF1F',\n 'confirm.clearall':'\u786E\u5B9A\u8981\u5220\u9664\u6240\u6709\u8BB0\u5FC6\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002',\n 'confirm.clearall2':'\u4F60\u771F\u7684\u786E\u5B9A\u5417\uFF1F',\n 'embed.on':'\u5D4C\u5165\u6A21\u578B\uFF1A',\n 'embed.off':'\u65E0\u5D4C\u5165\u6A21\u578B',\n 'lang.switch':'EN',\n 'tab.logs':'\uD83D\uDCDD \u65E5\u5FD7',\n 'logs.allTools':'\u5168\u90E8\u5DE5\u5177',\n 'logs.refresh':'\u5237\u65B0',\n 'logs.autoRefresh':'\u81EA\u52A8\u5237\u65B0',\n 'logs.input':'\u8F93\u5165',\n 'logs.output':'\u8F93\u51FA',\n 'logs.empty':'\u6682\u65E0\u65E5\u5FD7\u3002\u5F53\u5DE5\u5177\u88AB\u8C03\u7528\u65F6\u65E5\u5FD7\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\u3002',\n 'logs.ago':'\u524D',\n 'tab.import':'\uD83D\uDCE5 \u5BFC\u5165',\n 'tab.settings':'\u2699 \u8BBE\u7F6E',\n 'settings.modelconfig':'\u6A21\u578B\u914D\u7F6E',\n 'settings.embedding':'\u5D4C\u5165\u6A21\u578B',\n 'settings.summarizer':'\u6458\u8981\u6A21\u578B',\n 'settings.skill':'\u6280\u80FD\u8FDB\u5316',\n 'settings.general':'\u901A\u7528\u8BBE\u7F6E',\n 'settings.provider':'\u670D\u52A1\u5546',\n 'settings.model':'\u6A21\u578B',\n 'settings.temperature':'\u6E29\u5EA6',\n 'settings.skill.enabled':'\u542F\u7528\u6280\u80FD\u8FDB\u5316',\n 'settings.skill.autoinstall':'\u81EA\u52A8\u5B89\u88C5\u6280\u80FD',\n 'settings.skill.confidence':'\u6700\u4F4E\u7F6E\u4FE1\u5EA6',\n 'settings.skill.minchunks':'\u6700\u5C11\u8BB0\u5FC6\u7247\u6BB5',\n 'settings.skill.model':'\u6280\u80FD\u4E13\u7528\u6A21\u578B',\n 'settings.skill.model.hint':'\u4E0D\u914D\u7F6E\u65F6\u9ED8\u8BA4\u4F7F\u7528\u4E0A\u65B9\u7684\u6458\u8981\u6A21\u578B\u8FDB\u884C\u6280\u80FD\u751F\u6210\u3002\u5982\u9700\u66F4\u9AD8\u8D28\u91CF\u7684\u6280\u80FD\u8F93\u51FA\uFF0C\u53EF\u5728\u6B64\u5355\u72EC\u914D\u7F6E\u4E00\u4E2A\u66F4\u5F3A\u7684\u6A21\u578B\u3002',\n 'settings.optional':'\u53EF\u9009',\n 'settings.skill.usemain':'\u4F7F\u7528\u4E3B\u6458\u8981\u6A21\u578B',\n 'settings.telemetry':'\u6570\u636E\u7EDF\u8BA1',\n 'settings.telemetry.enabled':'\u542F\u7528\u533F\u540D\u6570\u636E\u7EDF\u8BA1',\n 'settings.telemetry.hint':'\u533F\u540D\u4F7F\u7528\u7EDF\u8BA1\uFF0C\u5E2E\u52A9\u6539\u8FDB\u63D2\u4EF6\u3002\u4EC5\u53D1\u9001\u5DE5\u5177\u540D\u79F0\u3001\u54CD\u5E94\u65F6\u95F4\u548C\u7248\u672C\u4FE1\u606F\uFF0C\u4E0D\u4F1A\u53D1\u9001\u4EFB\u4F55\u8BB0\u5FC6\u5185\u5BB9\u3001\u641C\u7D22\u67E5\u8BE2\u6216\u4E2A\u4EBA\u6570\u636E\u3002',\n 'settings.viewerport':'Viewer \u7AEF\u53E3',\n 'settings.viewerport.hint':'\u4FEE\u6539\u540E\u9700\u91CD\u542F\u7F51\u5173\u751F\u6548',\n 'settings.save':'\u4FDD\u5B58\u8BBE\u7F6E',\n 'settings.reset':'\u91CD\u7F6E',\n 'settings.saved':'\u5DF2\u4FDD\u5B58',\n 'settings.restart.hint':'\u90E8\u5206\u8BBE\u7F6E\u4FEE\u6539\u540E\u9700\u8981\u91CD\u542F OpenClaw \u7F51\u5173\u624D\u80FD\u751F\u6548\u3002',\n 'settings.save.fail':'\u4FDD\u5B58\u8BBE\u7F6E\u5931\u8D25',\n 'migrate.title':'\u5BFC\u5165 OpenClaw \u8BB0\u5FC6',\n 'migrate.desc':'\u5C06 OpenClaw \u5185\u7F6E\u7684\u8BB0\u5FC6\u6570\u636E\u548C\u5BF9\u8BDD\u5386\u53F2\u8FC1\u79FB\u5230\u672C\u63D2\u4EF6\u4E2D\u3002\u5BFC\u5165\u8FC7\u7A0B\u4F7F\u7528\u667A\u80FD\u53BB\u91CD\uFF0C\u907F\u514D\u91CD\u590D\u5BFC\u5165\u3002',\n 'migrate.modes.title':'\u4E09\u79CD\u4F7F\u7528\u65B9\u5F0F\uFF1A',\n 'migrate.mode1.label':'\u2460 \u4EC5\u5BFC\u5165\u8BB0\u5FC6\uFF08\u5FEB\u901F\uFF09',\n 'migrate.mode1.desc':'\u2014\u2014\u70B9\u51FB\u300C\u5F00\u59CB\u5BFC\u5165\u300D\u5373\u53EF\u5FEB\u901F\u8FC1\u79FB\u6240\u6709\u8BB0\u5FC6\u7247\u6BB5\u548C\u5BF9\u8BDD\u5386\u53F2\uFF0C\u4E0D\u8FDB\u884C\u4EFB\u52A1/\u6280\u80FD\u751F\u6210\u3002\u9002\u5408\u53EA\u9700\u8981\u539F\u59CB\u6570\u636E\u7684\u573A\u666F\u3002',\n 'migrate.mode2.label':'\u2461 \u5BFC\u5165 + \u751F\u6210\u4EFB\u52A1\u4E0E\u6280\u80FD\uFF08\u8F83\u6162\uFF0C\u4E32\u884C\uFF09',\n 'migrate.mode2.desc':'\u2014\u2014\u5BFC\u5165\u8BB0\u5FC6\u540E\uFF0C\u5728\u4E0B\u65B9\u52FE\u9009\u300C\u751F\u6210\u4EFB\u52A1\u6458\u8981\u300D\u548C/\u6216\u300C\u89E6\u53D1\u6280\u80FD\u8FDB\u5316\u300D\uFF0C\u7CFB\u7EDF\u4F1A\u9010\u4E2A\u4F1A\u8BDD\u5206\u6790\u3002\u7531\u4E8E\u6BCF\u4E2A\u4F1A\u8BDD\u90FD\u9700\u8981 LLM \u5904\u7406\uFF0C\u8017\u65F6\u8F83\u957F\u3002',\n 'migrate.mode3.label':'\u2462 \u5148\u5BFC\u5165\uFF0C\u968F\u65F6\u518D\u751F\u6210\uFF08\u7075\u6D3B\uFF09',\n 'migrate.mode3.desc':'\u2014\u2014\u5148\u5BFC\u5165\u8BB0\u5FC6\uFF0C\u4E4B\u540E\u968F\u65F6\u53EF\u4EE5\u56DE\u6765\u5F00\u542F\u4EFB\u52A1/\u6280\u80FD\u751F\u6210\u3002\u751F\u6210\u8FC7\u7A0B\u53EF\u4EE5\u968F\u65F6\u6682\u505C\uFF0C\u4E0B\u6B21\u7EE7\u7EED\u65F6\u4F1A\u4ECE\u4E0A\u6B21\u505C\u4E0B\u7684\u5730\u65B9\u63A5\u7740\u5904\u7406\uFF0C\u5DF2\u5904\u7406\u7684\u4F1A\u8BDD\u4F1A\u81EA\u52A8\u8DF3\u8FC7\u3002',\n 'migrate.config.warn':'\u9700\u8981\u914D\u7F6E',\n 'migrate.config.warn.desc':'\u8BF7\u5148\u5728\u4E0A\u65B9\u914D\u7F6E\u597D Embedding \u6A21\u578B\u548C Summarizer \u6A21\u578B\uFF0C\u8FD9\u4E24\u9879\u662F\u5904\u7406\u8BB0\u5FC6\u6240\u5FC5\u9700\u7684\u3002',\n 'migrate.sqlite.label':'\u8BB0\u5FC6\u7D22\u5F15 (SQLite)',\n 'migrate.sessions.label':'\u5BF9\u8BDD\u5386\u53F2',\n 'migrate.scan':'\u626B\u63CF\u6570\u636E\u6E90',\n 'migrate.start':'\u5F00\u59CB\u5BFC\u5165',\n 'migrate.scanning':'\u626B\u63CF\u4E2D...',\n 'migrate.stat.stored':'\u5DF2\u5B58\u50A8',\n 'migrate.stat.skipped':'\u5DF2\u8DF3\u8FC7',\n 'migrate.stat.merged':'\u5DF2\u5408\u5E76',\n 'migrate.stat.errors':'\u9519\u8BEF',\n 'migrate.phase.sqlite':'\u6B63\u5728\u5BFC\u5165\u8BB0\u5FC6\u7D22\u5F15...',\n 'migrate.phase.sessions':'\u6B63\u5728\u5BFC\u5165\u5BF9\u8BDD\u5386\u53F2...',\n 'migrate.chunks':'\u6761\u8BB0\u5FC6',\n 'migrate.sessions.count':'\u4E2A\u4F1A\u8BDD\uFF0C{n} \u6761\u6D88\u606F',\n 'migrate.nodata':'\u672A\u627E\u5230\u53EF\u5BFC\u5165\u7684 OpenClaw \u6570\u636E\u3002',\n 'migrate.running':'\u5BFC\u5165\u8FDB\u884C\u4E2D...',\n 'migrate.error.running':'\u5DF2\u6709\u8FC1\u79FB\u4EFB\u52A1\u6B63\u5728\u8FDB\u884C\u3002',\n 'migrate.stop':'\\u25A0 \u505C\u6B62',\n 'migrate.stopping':'\u6B63\u5728\u505C\u6B62...',\n 'migrate.resume':'\u7EE7\u7EED\u5BFC\u5165',\n 'pp.title':'\\u{1F9E0} \u53EF\u9009\uFF1A\u751F\u6210\u4EFB\u52A1\u4E0E\u6280\u80FD',\n 'pp.desc':'\u6B64\u6B65\u9AA4\u5B8C\u5168\u53EF\u9009\u3002\u4E0A\u9762\u7684\u5BFC\u5165\u5DF2\u7ECF\u5B58\u50A8\u4E86\u539F\u59CB\u8BB0\u5FC6\u6570\u636E\u3002\u5728\u8FD9\u91CC\u53EF\u4EE5\u8FDB\u4E00\u6B65\u5206\u6790\u5DF2\u5BFC\u5165\u7684\u5BF9\u8BDD\uFF0C\u751F\u6210\u7ED3\u6784\u5316\u7684\u4EFB\u52A1\u6458\u8981\u6216\u8FDB\u5316\u53EF\u590D\u7528\u7684\u6280\u80FD\u3002\u5904\u7406\u8FC7\u7A0B\u662F\u4E32\u884C\u7684\uFF08\u9010\u4E2A\u4F1A\u8BDD\uFF09\uFF0C\u53EF\u80FD\u9700\u8981\u8F83\u957F\u65F6\u95F4\u3002\u4F60\u53EF\u4EE5\u968F\u65F6\u505C\u6B62\uFF0C\u4E0B\u6B21\u7EE7\u7EED\u65F6\u53EA\u4F1A\u5904\u7406\u5C1A\u672A\u5B8C\u6210\u7684\u4F1A\u8BDD\u3002',\n 'pp.tasks.label':'\u751F\u6210\u4EFB\u52A1\u6458\u8981',\n 'pp.tasks.hint':'\u5C06\u5BFC\u5165\u7684\u6D88\u606F\u6309\u4EFB\u52A1\u5206\u7EC4\uFF0C\u4E3A\u6BCF\u4E2A\u4EFB\u52A1\u751F\u6210\u7ED3\u6784\u5316\u6458\u8981\uFF08\u6807\u9898\u3001\u76EE\u6807\u3001\u6B65\u9AA4\u3001\u7ED3\u679C\uFF09\uFF0C\u65B9\u4FBF\u65E5\u540E\u641C\u7D22\u548C\u56DE\u5FC6\u3002',\n 'pp.skills.label':'\u89E6\u53D1\u6280\u80FD\u8FDB\u5316',\n 'pp.skills.hint':'\u5206\u6790\u5DF2\u5B8C\u6210\u7684\u4EFB\u52A1\uFF0C\u81EA\u52A8\u521B\u5EFA\u6216\u5347\u7EA7\u53EF\u590D\u7528\u7684\u6280\u80FD\uFF08SKILL.md\uFF09\u3002\u9700\u8981\u5148\u542F\u7528\u4EFB\u52A1\u6458\u8981\u3002\u7531\u4E8E\u9700\u8981 LLM \u8BC4\u4F30\uFF0C\u8017\u65F6\u8F83\u957F\u3002',\n 'pp.start':'\u5F00\u59CB\u5904\u7406',\n 'pp.resume':'\u7EE7\u7EED\u5904\u7406',\n 'pp.running':'\u6B63\u5728\u5904\u7406',\n 'pp.stopped':'\u5904\u7406\u5DF2\u505C\u6B62\uFF0C\u4F60\u53EF\u4EE5\u968F\u65F6\u7EE7\u7EED\u3002',\n 'pp.failed':'\u5904\u7406\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u4E0A\u65B9\u7684\u9519\u8BEF\u63D0\u793A\u3002',\n 'pp.done':'\u4EFB\u52A1\u4E0E\u6280\u80FD\u751F\u6210\u5B8C\u6210\uFF01',\n 'pp.select.warn':'\u8BF7\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A\u9009\u9879\u3002',\n 'pp.skill.created':'\u6280\u80FD\u5DF2\u521B\u5EFA',\n 'pp.stat.tasks':'\u4EFB\u52A1',\n 'pp.stat.skills':'\u6280\u80FD',\n 'pp.stat.errors':'\u9519\u8BEF',\n 'pp.stat.skipped':'\u5DF2\u8DF3\u8FC7',\n 'pp.info.skipped':'\u5DF2\u6709 {n} \u4E2A\u4F1A\u8BDD\u5904\u7406\u8FC7\uFF0C\u81EA\u52A8\u8DF3\u8FC7\u3002',\n 'pp.info.pending':'\u6B63\u5728\u5904\u7406 {n} \u4E2A\u4F1A\u8BDD...',\n 'pp.info.allDone':'\u6240\u6709\u4F1A\u8BDD\u5747\u5DF2\u5904\u7406\u8FC7\uFF0C\u65E0\u9700\u91CD\u590D\u5904\u7406\u3002',\n 'pp.action.full':'\u4EFB\u52A1+\u6280\u80FD',\n 'pp.action.skillOnly':'\u4EC5\u6280\u80FD\uFF08\u4EFB\u52A1\u5DF2\u5B58\u5728\uFF09',\n 'card.imported':'OpenClaw \u539F\u751F\u8BB0\u5FC6',\n 'skills.draft':'\u8349\u7A3F',\n 'skills.filter.active':'\u751F\u6548\u4E2D',\n 'skills.filter.draft':'\u8349\u7A3F',\n 'skills.filter.archived':'\u5DF2\u5F52\u6863',\n 'skills.files':'\u6280\u80FD\u6587\u4EF6',\n 'skills.content':'SKILL.md \u5185\u5BB9',\n 'skills.versions':'\u7248\u672C\u5386\u53F2',\n 'skills.related':'\u5173\u8054\u4EFB\u52A1',\n 'skills.download':'\u2B07 \u4E0B\u8F7D',\n 'skills.installed.badge':'\u5DF2\u5B89\u88C5',\n 'skills.empty':'\u6682\u65E0\u6280\u80FD\u3002\u6280\u80FD\u4F1A\u4ECE\u5DF2\u5B8C\u6210\u7684\u3001\u5305\u542B\u53EF\u590D\u7528\u7ECF\u9A8C\u7684\u4EFB\u52A1\u4E2D\u81EA\u52A8\u751F\u6210\u3002',\n 'skills.loading':'\u52A0\u8F7D\u4E2D...',\n 'skills.error':'\u52A0\u8F7D\u6280\u80FD\u5931\u8D25',\n 'skills.error.detail':'\u52A0\u8F7D\u6280\u80FD\u5931\u8D25\uFF1A',\n 'skills.nofiles':'\u6682\u65E0\u6587\u4EF6',\n 'skills.noversions':'\u6682\u65E0\u7248\u672C\u8BB0\u5F55',\n 'skills.norelated':'\u6682\u65E0\u5173\u8054\u4EFB\u52A1',\n 'skills.nocontent':'\u6682\u65E0\u5185\u5BB9',\n 'skills.nochangelog':'\u6682\u65E0\u53D8\u66F4\u8BB0\u5F55',\n 'skills.status.active':'\u751F\u6548\u4E2D',\n 'skills.status.draft':'\u8349\u7A3F',\n 'skills.status.archived':'\u5DF2\u5F52\u6863',\n 'skills.updated':'\u66F4\u65B0\u4E8E\uFF1A',\n 'skills.task.prefix':'\u4EFB\u52A1\uFF1A',\n 'tasks.chunks.label':'\u6761\u8BB0\u5FC6',\n 'tasks.taskid':'\u4EFB\u52A1 ID\uFF1A',\n 'tasks.role.user':'\u4F60',\n 'tasks.role.assistant':'\u52A9\u624B',\n 'tasks.error':'\u51FA\u9519\u4E86',\n 'tasks.error.detail':'\u52A0\u8F7D\u4EFB\u52A1\u8BE6\u60C5\u5931\u8D25',\n 'tasks.untitled.related':'\u672A\u547D\u540D'\n }\n};\nconst LANG_KEY='memos-viewer-lang';\nlet curLang=localStorage.getItem(LANG_KEY)||(navigator.language.startsWith('zh')?'zh':'en');\nfunction t(key){return (I18N[curLang]||I18N.en)[key]||key;}\nfunction setLang(lang){curLang=lang;localStorage.setItem(LANG_KEY,lang);applyI18n();}\nfunction toggleLang(){setLang(curLang==='zh'?'en':'zh');}\n\nfunction applyI18n(){\n document.querySelectorAll('[data-i18n]').forEach(el=>{\n const key=el.getAttribute('data-i18n');\n if(key) el.textContent=t(key);\n });\n document.querySelectorAll('[data-i18n-ph]').forEach(el=>{\n const key=el.getAttribute('data-i18n-ph');\n if(key) el.placeholder=t(key);\n });\n const step2=document.getElementById('resetStep2Desc');\n if(step2) step2.innerHTML=t('reset.step2.desc.pre')+'<span style=\"font-family:monospace;font-size:12px;color:var(--pri)\">password reset token: <strong>a1b2c3d4e5f6...</strong></span>'+t('reset.step2.desc.post');\n document.title=t('title')+' - MemOS';\n if(typeof loadStats==='function' && document.getElementById('app').style.display==='flex'){loadStats();}\n if(document.querySelector('.analytics-view.show') && typeof loadMetrics==='function'){loadMetrics();}\n}\n\n/* \u2500\u2500\u2500 Auth flow \u2500\u2500\u2500 */\nasync function checkAuth(){\n const r=await fetch('/api/auth/status');\n const d=await r.json();\n if(d.needsSetup){\n document.getElementById('setupScreen').style.display='flex';\n document.getElementById('setupPw').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('setupPw2').focus()});\n document.getElementById('setupPw2').addEventListener('keydown',e=>{if(e.key==='Enter')doSetup()});\n } else if(!d.loggedIn){\n document.getElementById('loginScreen').style.display='flex';\n document.getElementById('loginPw').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});\n } else {\n enterApp();\n }\n}\n\nasync function doSetup(){\n const pw=document.getElementById('setupPw').value;\n const pw2=document.getElementById('setupPw2').value;\n const err=document.getElementById('setupErr');\n if(pw.length<4){err.textContent=t('setup.err.short');return}\n if(pw!==pw2){err.textContent=t('setup.err.mismatch');return}\n const r=await fetch('/api/auth/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('setupScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||t('setup.err.fail')}\n}\n\nasync function doLogin(){\n const pw=document.getElementById('loginPw').value;\n const err=document.getElementById('loginErr');\n const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent=t('login.err');document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}\n}\n\nasync function doLogout(){\n await fetch('/api/auth/logout',{method:'POST'});\n location.reload();\n}\n\nfunction showResetForm(){\n document.getElementById('loginForm').style.display='none';\n document.getElementById('resetForm').style.display='block';\n document.getElementById('resetToken').focus();\n}\n\nfunction showLoginForm(){\n document.getElementById('resetForm').style.display='none';\n document.getElementById('loginForm').style.display='block';\n document.getElementById('loginPw').focus();\n}\n\nfunction copyCmd(el){\n const code=el.querySelector('code').textContent;\n navigator.clipboard.writeText(code).then(()=>{\n el.classList.add('copied');\n el.querySelector('.copy-hint').textContent=t('copy.done');\n setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent=t('copy.hint')},2000);\n });\n}\n\nasync function doReset(){\n const token=document.getElementById('resetToken').value.trim();\n const pw=document.getElementById('resetNewPw').value;\n const pw2=document.getElementById('resetNewPw2').value;\n const err=document.getElementById('resetErr');\n if(!token){err.textContent=t('reset.err.token');return}\n if(pw.length<4){err.textContent=t('reset.err.short');return}\n if(pw!==pw2){err.textContent=t('reset.err.mismatch');return}\n const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,newPassword:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||t('reset.err.fail')}\n}\n\nfunction enterApp(){\n document.getElementById('app').style.display='flex';\n loadAll();\n}\n\nfunction switchView(view){\n document.querySelectorAll('.nav-tabs .tab').forEach(t=>t.classList.toggle('active',t.dataset.view===view));\n const feedWrap=document.getElementById('feedWrap');\n const analyticsView=document.getElementById('analyticsView');\n const tasksView=document.getElementById('tasksView');\n const skillsView=document.getElementById('skillsView');\n const logsView=document.getElementById('logsView');\n const settingsView=document.getElementById('settingsView');\n const migrateView=document.getElementById('migrateView');\n feedWrap.classList.add('hide');\n analyticsView.classList.remove('show');\n tasksView.classList.remove('show');\n skillsView.classList.remove('show');\n logsView.classList.remove('show');\n settingsView.classList.remove('show');\n migrateView.classList.remove('show');\n if(view==='analytics'){\n analyticsView.classList.add('show');\n loadMetrics();\n } else if(view==='tasks'){\n tasksView.classList.add('show');\n loadTasks();\n } else if(view==='skills'){\n skillsView.classList.add('show');\n loadSkills();\n } else if(view==='logs'){\n logsView.classList.add('show');\n loadLogs();\n } else if(view==='settings'){\n settingsView.classList.add('show');\n loadConfig();\n } else if(view==='import'){\n migrateView.classList.add('show');\n if(!window._migrateRunning) migrateScan();\n } else {\n feedWrap.classList.remove('hide');\n }\n}\n\n// \u2500\u2500\u2500 Logs \u2500\u2500\u2500\nlet logAutoTimer=null;\nlet logPage=1;\nconst LOG_PAGE_SIZE=20;\nasync function loadLogs(page){\n if(typeof page==='number') logPage=page;\n try{\n const toolFilter=document.getElementById('logToolFilter').value;\n const offset=(logPage-1)*LOG_PAGE_SIZE;\n const url='/api/logs?limit='+LOG_PAGE_SIZE+'&offset='+offset+(toolFilter?'&tool='+encodeURIComponent(toolFilter):'');\n const [logsRes,toolsRes]=await Promise.all([fetch(url),fetch('/api/log-tools')]);\n if(!logsRes.ok) return;\n const logsData=await logsRes.json();\n const toolsData=await toolsRes.json();\n renderLogToolFilter(toolsData.tools||[],toolFilter);\n renderLogs(logsData.logs||[]);\n renderLogPagination(logsData.page||1,logsData.totalPages||1,logsData.total||0);\n startLogAutoRefresh();\n }catch(e){console.error('loadLogs',e)}\n}\nfunction onLogFilterChange(){logPage=1;loadLogs(1);}\nfunction renderLogPagination(page,totalPages,total){\n const el=document.getElementById('logsPagination');\n if(!el||totalPages<=1){if(el)el.innerHTML='';return;}\n const pages=[];\n const range=2;\n for(let i=1;i<=totalPages;i++){\n if(i===1||i===totalPages||Math.abs(i-page)<=range){\n pages.push(i);\n }else if(pages[pages.length-1]!=='...'){\n pages.push('...');\n }\n }\n let html='<div class=\"logs-pagination\">';\n html+='<button class=\"btn btn-sm btn-ghost\" '+(page<=1?'disabled':'')+' onclick=\"loadLogs('+(page-1)+')\">\u2039</button>';\n pages.forEach(p=>{\n if(p==='...'){html+='<span class=\"page-ellipsis\">\u2026</span>';}\n else{html+='<button class=\"btn btn-sm '+(p===page?'btn-primary':'btn-ghost')+'\" onclick=\"loadLogs('+p+')\">'+p+'</button>';}\n });\n html+='<button class=\"btn btn-sm btn-ghost\" '+(page>=totalPages?'disabled':'')+' onclick=\"loadLogs('+(page+1)+')\">\u203A</button>';\n html+='<span class=\"page-total\">'+total+' total</span>';\n html+='</div>';\n el.innerHTML=html;\n}\n\nfunction renderLogToolFilter(tools,current){\n const sel=document.getElementById('logToolFilter');\n const opts=['<option value=\"\">'+t('logs.allTools')+'</option>'];\n tools.forEach(tn=>{\n opts.push('<option value=\"'+tn+'\"'+(tn===current?' selected':'')+'>'+tn+'</option>');\n });\n sel.innerHTML=opts.join('');\n}\n\nfunction formatLogTime(ts){\n const d=new Date(ts);\n const time=d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false});\n const y=d.getFullYear();\n const m=String(d.getMonth()+1).padStart(2,'0');\n const day=String(d.getDate()).padStart(2,'0');\n return y+'-'+m+'-'+day+' '+time;\n}\n\nfunction buildLogSummary(lg){\n let inputObj=null;\n try{inputObj=JSON.parse(lg.input);}catch(_){}\n let html='';\n const tn=lg.toolName;\n if(tn==='memory_search'&&inputObj){\n const q=inputObj.query||'';\n if(q) html+='<div class=\"log-summary-query\">'+escapeHtml(q.length>200?q.slice(0,200)+'...':q)+'</div>';\n const outLines=(lg.output||'').split('\\n');\n const memCount=outLines.filter(l=>l.match(/^\\d+\\.\\s*\\[/)).length;\n if(memCount>0) html+='<div style=\"margin-top:4px;font-size:11px;color:var(--text-sec)\">\uD83D\uDCCE '+memCount+' memories retrieved</div>';\n 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>';\n }else if(tn==='memory_add'&&inputObj){\n const out=lg.output||'';\n const statsMatch=out.match(/^([^\\n]+)/);\n if(statsMatch){\n html+='<div class=\"log-summary-stats\">';\n const pairs=statsMatch[1].split(',').map(s=>s.trim());\n pairs.forEach(p=>{\n const m=p.match(/^(\\w+)=(\\d+)/);\n if(m){html+='<span class=\"log-stat-chip '+m[1]+'\">'+m[1]+' '+m[2]+'</span>';}\n });\n html+='</div>';\n }\n const outLines=out.split('\\n').filter(l=>l.startsWith('['));\n if(outLines.length>0){\n html+='<div class=\"log-msg-list\">';\n outLines.forEach(function(l){\n var rm=l.match(/^\\[(\\w+)\\]\\s*([^\u2192]+)\u2192\\s*(.*)/);\n if(rm){\n var role=rm[1],actionRaw=rm[2].trim(),text=rm[3].trim();\n var actionCls='stored';\n if(actionRaw.indexOf('exact-dup')>=0||actionRaw.indexOf('\u23ED')>=0) actionCls='exact-dup';\n else if(actionRaw.indexOf('dedup')>=0||actionRaw.indexOf('\uD83D\uDD01')>=0) actionCls='dedup';\n else if(actionRaw.indexOf('merged')>=0||actionRaw.indexOf('\uD83D\uDD00')>=0) actionCls='merged';\n else if(actionRaw.indexOf('error')>=0||actionRaw.indexOf('\u274C')>=0) actionCls='error';\n var actionLabel={'stored':'\u2713 stored','exact-dup':'\u23ED skip','dedup':'\uD83D\uDD01 dedup','merged':'\uD83D\uDD00 merged','error':'\u2717 error'}[actionCls]||actionCls;\n html+='<div class=\"log-msg-item\">'+\n '<span class=\"log-msg-role '+role+'\">'+role+'</span>'+\n '<span class=\"log-msg-action '+actionCls+'\">'+actionLabel+'</span>'+\n '<span class=\"log-msg-text\">'+escapeHtml(text.length>150?text.slice(0,150)+'...':text)+'</span>'+\n '</div>';\n }else{\n html+='<div class=\"log-msg-item\"><span class=\"log-msg-text\">'+escapeHtml(l.length>200?l.slice(0,200)+'...':l)+'</span></div>';\n }\n });\n html+='</div>';\n }else if(inputObj.details&&Array.isArray(inputObj.details)&&inputObj.details.length>0){\n html+='<div class=\"log-msg-list\">';\n inputObj.details.forEach(function(d){\n var s=typeof d==='string'?d:String(d);\n var dm=s.match(/^\\[(\\w+)\\]\\s*(.*)/);\n if(dm){\n 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>';\n }else{\n html+='<div class=\"log-msg-item\"><span class=\"log-msg-text\">'+escapeHtml(s.length>150?s.slice(0,150)+'...':s)+'</span></div>';\n }\n });\n html+='</div>';\n }\n }else if(inputObj){\n const keys=Object.keys(inputObj);\n keys.slice(0,4).forEach(k=>{\n const v=String(inputObj[k]);\n 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>';\n });\n }\n return html;\n}\nfunction renderLogs(logs){\n const el=document.getElementById('logsList');\n if(!logs.length){\n el.innerHTML='<div style=\"text-align:center;padding:60px 20px;color:var(--text-sec)\">'+\n '<div style=\"font-size:32px;margin-bottom:12px;opacity:.5\">\uD83D\uDCCB</div>'+\n '<div style=\"font-size:13px\">'+t('logs.empty')+'</div></div>';\n return;\n }\n el.innerHTML=logs.map((lg,i)=>{\n const toolCls=lg.toolName.replace(/[^a-zA-Z0-9_]/g,'_');\n const dur=lg.durationMs<1000?Math.round(lg.durationMs)+'ms':(lg.durationMs/1000).toFixed(1)+'s';\n let inputDisplay='';\n try{const parsed=JSON.parse(lg.input);inputDisplay=JSON.stringify(parsed,null,2);}catch(_){inputDisplay=lg.input;}\n const summary=buildLogSummary(lg);\n return '<div class=\"log-entry\" id=\"log-'+i+'\">'+\n '<div class=\"log-header\" onclick=\"toggleLog('+i+')\">'+\n '<span class=\"log-status '+(lg.success?'ok':'fail')+'\"></span>'+\n '<span class=\"log-tool-badge '+toolCls+'\">'+lg.toolName+'</span>'+\n '<span class=\"log-dur\">'+dur+'</span>'+\n '<span class=\"log-expand-btn\" style=\"margin-left:4px\">\u25BC</span>'+\n '<span class=\"log-time\">'+formatLogTime(lg.calledAt)+'</span>'+\n '</div>'+\n (summary?'<div class=\"log-summary\">'+summary+'</div>':'')+\n '<div class=\"log-detail\" id=\"log-detail-'+i+'\">'+\n '<div class=\"log-io-section\">'+\n '<div class=\"log-io-label\">\u25B6 '+t('logs.input')+'</div>'+\n '<pre class=\"log-io-content\">'+escapeHtml(inputDisplay)+'</pre>'+\n '</div>'+\n '<div class=\"log-io-section\">'+\n '<div class=\"log-io-label\">\u25C0 '+t('logs.output')+'</div>'+\n '<pre class=\"log-io-content\">'+escapeHtml(lg.output)+'</pre>'+\n '</div>'+\n '</div>'+\n '</div>';\n }).join('');\n}\n\nfunction toggleLog(i){\n const entry=document.getElementById('log-'+i);\n const d=document.getElementById('log-detail-'+i);\n if(d) d.classList.toggle('open');\n if(entry) entry.classList.toggle('expanded');\n}\n\nfunction startLogAutoRefresh(){\n if(logAutoTimer) clearInterval(logAutoTimer);\n logAutoTimer=setInterval(()=>{\n const cb=document.getElementById('logAutoRefresh');\n const logsView=document.getElementById('logsView');\n if(cb&&cb.checked&&logsView&&logsView.classList.contains('show')){\n loadLogs();\n }\n },5000);\n}\n\nfunction escapeHtml(s){\n return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n}\n\nfunction setMetricsDays(d){\n metricsDays=d;\n document.querySelectorAll('.metrics-toolbar .range-btn').forEach(btn=>btn.classList.toggle('active',Number(btn.dataset.days)===d));\n loadMetrics();\n}\n\nasync function loadMetrics(){\n const r=await fetch('/api/metrics?days='+metricsDays);\n const d=await r.json();\n document.getElementById('mTotal').textContent=formatNum(d.totals.memories);\n document.getElementById('mTodayWrites').textContent=formatNum(d.totals.todayWrites);\n document.getElementById('mSessions').textContent=formatNum(d.totals.sessions);\n document.getElementById('mEmbeddings').textContent=formatNum(d.totals.embeddings);\n renderChartWrites(d.writesPerDay);\n renderBreakdown(d.roleBreakdown,'breakdownRole');\n renderBreakdown(d.kindBreakdown,'breakdownKind');\n loadToolMetrics();\n}\n\nfunction formatNum(n){return n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'k':String(n);}\n\n/* \u2500\u2500\u2500 Tasks View Logic \u2500\u2500\u2500 */\nlet tasksStatusFilter='';\nlet tasksPage=0;\nconst TASKS_PER_PAGE=20;\n\nfunction setTaskStatusFilter(btn,status){\n document.querySelectorAll('.tasks-filters .filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n tasksStatusFilter=status;\n tasksPage=0;\n loadTasks();\n}\n\nasync function loadTasks(){\n const list=document.getElementById('tasksList');\n list.innerHTML='<div class=\"spinner\"></div>';\n try{\n const params=new URLSearchParams({limit:String(TASKS_PER_PAGE),offset:String(tasksPage*TASKS_PER_PAGE)});\n if(tasksStatusFilter) params.set('status',tasksStatusFilter);\n const r=await fetch('/api/tasks?'+params);\n const data=await r.json();\n\n // stats\n const allR=await fetch('/api/tasks?limit=1&offset=0');\n const allD=await allR.json();\n document.getElementById('tasksTotalCount').textContent=formatNum(allD.total);\n\n const activeR=await fetch('/api/tasks?status=active&limit=1&offset=0');\n const activeD=await activeR.json();\n document.getElementById('tasksActiveCount').textContent=formatNum(activeD.total);\n\n const compR=await fetch('/api/tasks?status=completed&limit=1&offset=0');\n const compD=await compR.json();\n document.getElementById('tasksCompletedCount').textContent=formatNum(compD.total);\n\n const skipR=await fetch('/api/tasks?status=skipped&limit=1&offset=0');\n const skipD=await skipR.json();\n document.getElementById('tasksSkippedCount').textContent=formatNum(skipD.total);\n\n if(!data.tasks||data.tasks.length===0){\n list.innerHTML='<div style=\"text-align:center;padding:48px;color:var(--text-muted);font-size:14px\" data-i18n=\"tasks.empty\">'+t('tasks.empty')+'</div>';\n document.getElementById('tasksPagination').innerHTML='';\n return;\n }\n\n list.innerHTML=data.tasks.map(task=>{\n const timeStr=formatTime(task.startedAt);\n const endStr=task.endedAt?formatTime(task.endedAt):'';\n const durationStr=task.endedAt?formatDuration(task.endedAt-task.startedAt):'';\n return '<div class=\"task-card status-'+task.status+'\" onclick=\"openTaskDetail(\\''+task.id+'\\')\">'+\n '<div class=\"task-card-top\">'+\n '<div class=\"task-card-title\">'+esc(task.title)+'</div>'+\n '<span class=\"task-status-badge '+task.status+'\">'+t('tasks.status.'+task.status)+'</span>'+\n '</div>'+\n (task.summary?'<div class=\"task-card-summary'+(task.status==='skipped'?' skipped-reason':'')+'\">'+esc(task.summary)+'</div>':'')+\n '<div class=\"task-card-bottom\">'+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4C5}</span> '+timeStr+'</span>'+\n (durationStr?'<span class=\"tag\"><span class=\"icon\">\\u23F1</span> '+durationStr+'</span>':'')+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4DD}</span> '+task.chunkCount+' '+t('tasks.chunks.label')+'</span>'+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4C2}</span> '+(task.sessionKey||'').slice(0,12)+'</span>'+\n '</div>'+\n '</div>';\n }).join('');\n\n renderTasksPagination(data.total);\n }catch(e){\n console.error('loadTasks error:',e);\n list.innerHTML='<div style=\"text-align:center;padding:24px;color:var(--rose)\">Failed to load tasks: '+String(e)+'</div>';\n }\n}\n\nfunction renderTasksPagination(total){\n const el=document.getElementById('tasksPagination');\n const pages=Math.ceil(total/TASKS_PER_PAGE);\n if(pages<=1){el.innerHTML='';return;}\n let html='<button class=\"pg-btn'+(tasksPage===0?' disabled':'')+'\" onclick=\"tasksPage=Math.max(0,tasksPage-1);loadTasks()\">\\u2190</button>';\n const start=Math.max(0,tasksPage-2),end=Math.min(pages,tasksPage+3);\n for(let i=start;i<end;i++){\n html+='<button class=\"pg-btn'+(i===tasksPage?' active':'')+'\" onclick=\"tasksPage='+i+';loadTasks()\">'+(i+1)+'</button>';\n }\n html+='<button class=\"pg-btn'+(tasksPage>=pages-1?' disabled':'')+'\" onclick=\"tasksPage=Math.min('+(pages-1)+',tasksPage+1);loadTasks()\">\\u2192</button>';\n html+='<span class=\"pg-info\">'+total+' '+t('pagination.total')+'</span>';\n el.innerHTML=html;\n}\n\nasync function openTaskDetail(taskId){\n const overlay=document.getElementById('taskDetailOverlay');\n overlay.classList.add('show');\n document.getElementById('taskDetailTitle').textContent=t('tasks.loading');\n document.getElementById('taskDetailMeta').innerHTML='';\n document.getElementById('taskSkillSection').innerHTML='';\n document.getElementById('taskSkillSection').className='task-skill-section';\n document.getElementById('taskDetailSummary').textContent='';\n document.getElementById('taskDetailChunks').innerHTML='<div class=\"spinner\"></div>';\n\n try{\n const r=await fetch('/api/task/'+taskId);\n const task=await r.json();\n\n document.getElementById('taskDetailTitle').textContent=task.title||t('tasks.untitled');\n\n const meta=[\n '<span class=\"meta-item\"><span class=\"task-status-badge '+task.status+'\">'+t('tasks.status.'+task.status)+'</span></span>',\n '<span class=\"meta-item\">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>',\n ];\n if(task.endedAt) meta.push('<span class=\"meta-item\">\\u2192 '+formatTime(task.endedAt)+'</span>');\n meta.push('<span class=\"meta-item\">\\u{1F4C2} '+task.sessionKey+'</span>');\n meta.push('<span class=\"meta-item\">\\u{1F4DD} '+task.chunks.length+' '+t('tasks.chunks.label')+'</span>');\n 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>');\n document.getElementById('taskDetailMeta').innerHTML=meta.join('');\n\n // \u2500\u2500 Skill status section \u2500\u2500\n renderTaskSkillSection(task);\n\n var summaryEl=document.getElementById('taskDetailSummary');\n if(task.status==='skipped'){\n 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>';\n }else{\n summaryEl.innerHTML=renderSummaryHtml(task.summary);\n }\n\n if(task.chunks.length===0){\n document.getElementById('taskDetailChunks').innerHTML='<div style=\"color:var(--text-muted);padding:12px;font-size:13px\">'+t('tasks.nochunks')+'</div>';\n }else{\n document.getElementById('taskDetailChunks').innerHTML=task.chunks.map(c=>{\n var roleLabel=c.role==='user'?t('tasks.role.user'):c.role==='assistant'?t('tasks.role.assistant'):c.role.toUpperCase();\n return '<div class=\"task-chunk-item role-'+c.role+'\">'+\n '<div class=\"task-chunk-role '+c.role+'\">'+roleLabel+'</div>'+\n '<div class=\"task-chunk-bubble\" onclick=\"this.classList.toggle(\\'expanded\\')\">'+esc(c.content)+'</div>'+\n '<div class=\"task-chunk-time\">'+formatTime(c.createdAt)+'</div>'+\n '</div>';\n }).join('');\n }\n }catch(e){\n document.getElementById('taskDetailTitle').textContent=t('tasks.error');\n document.getElementById('taskDetailChunks').innerHTML='<div style=\"color:var(--rose)\">'+t('tasks.error.detail')+'</div>';\n }\n}\n\nfunction renderTaskSkillSection(task){\n const section=document.getElementById('taskSkillSection');\n const ss=task.skillStatus;\n const links=task.skillLinks||[];\n\n if(links.length>0){\n section.className='task-skill-section status-generated';\n var html='<div class=\"skill-status-header\">\\u{1F527} \u5DF2\u751F\u6210\u6280\u80FD</div>';\n html+=links.map(function(lk){\n 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;\n var statusLabel={'active':'\u6D3B\u8DC3','draft':'\u8349\u7A3F','archived':'\u5DF2\u5F52\u6863'}[lk.status]||lk.status;\n return '<div class=\"skill-link-card\" onclick=\"event.stopPropagation();closeTaskDetail();switchView(\\'skills\\');setTimeout(function(){openSkillDetail(\\''+lk.skillId+'\\')},300)\">'+\n '<div class=\"skill-link-name\">'+esc(lk.skillName)+' <span style=\"font-size:11px;color:var(--text-sec)\">('+relLabel+', v'+lk.versionAt+')</span></div>'+\n '<div class=\"skill-link-meta\">'+\n '\u72B6\u6001: <span class=\"task-status-badge '+(lk.status||'active')+'\">'+statusLabel+'</span>'+\n (lk.qualityScore!=null?' &middot; \u8D28\u91CF\u5206: '+lk.qualityScore+'/10':'')+\n '</div>'+\n '<div style=\"margin-top:4px\"><span class=\"task-id-full\">Skill ID: '+esc(lk.skillId)+'</span></div>'+\n '</div>';\n }).join('');\n section.innerHTML=html;\n }else if(ss==='generating'){\n section.className='task-skill-section status-generating';\n section.innerHTML='<div class=\"skill-status-header\">\\u23F3 \u6280\u80FD\u751F\u6210\u4E2D...</div>'+\n '<div class=\"skill-status-reason\">'+esc(task.skillReason||'')+'</div>';\n }else if(ss==='not_generated'){\n section.className='task-skill-section status-not_generated';\n section.innerHTML='<div class=\"skill-status-header\">\\u274C \u672A\u751F\u6210\u6280\u80FD</div>'+\n '<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>';\n }else if(ss==='skipped'){\n section.className='task-skill-section status-skipped';\n section.innerHTML='<div class=\"skill-status-header\">\\u23ED \u8DF3\u8FC7\u6280\u80FD\u8BC4\u4F30</div>'+\n '<div class=\"skill-status-reason\">\u539F\u56E0\uFF1A'+esc(task.skillReason||'')+'</div>';\n }else if(task.status==='active'){\n section.className='task-skill-section status-skipped';\n section.innerHTML='<div class=\"skill-status-header\">\\u23F8 \u4EFB\u52A1\u8FDB\u884C\u4E2D</div>'+\n '<div class=\"skill-status-reason\">\u6280\u80FD\u8BC4\u4F30\u5728\u4EFB\u52A1\u5B8C\u6210\u540E\u81EA\u52A8\u8FD0\u884C\u3002</div>';\n }else{\n section.className='task-skill-section status-skipped';\n section.innerHTML='<div class=\"skill-status-header\">\\u2014 \u65E0\u6280\u80FD\u4FE1\u606F</div>'+\n '<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>';\n }\n}\n\nfunction closeTaskDetail(event){\n if(event && event.target!==document.getElementById('taskDetailOverlay')) return;\n document.getElementById('taskDetailOverlay').classList.remove('show');\n}\n\n/* \u2500\u2500\u2500 Skills View Logic \u2500\u2500\u2500 */\nlet skillsStatusFilter='';\n\nfunction setSkillStatusFilter(btn,status){\n document.querySelectorAll('.skills-view .tasks-filters .filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n skillsStatusFilter=status;\n loadSkills();\n}\n\nasync function loadSkills(){\n const list=document.getElementById('skillsList');\n list.innerHTML='<div class=\"spinner\"></div>';\n try{\n const params=new URLSearchParams();\n if(skillsStatusFilter) params.set('status',skillsStatusFilter);\n const r=await fetch('/api/skills?'+params);\n const data=await r.json();\n\n document.getElementById('skillsTotalCount').textContent=formatNum(data.skills.length);\n document.getElementById('skillsActiveCount').textContent=formatNum(data.skills.filter(s=>s.status==='active').length);\n document.getElementById('skillsDraftCount').textContent=formatNum(data.skills.filter(s=>s.status==='draft').length);\n document.getElementById('skillsInstalledCount').textContent=formatNum(data.skills.filter(s=>s.installed).length);\n\n if(!data.skills||data.skills.length===0){\n list.innerHTML='<div style=\"text-align:center;padding:48px;color:var(--text-muted);font-size:14px\">'+t('skills.empty')+'</div>';\n return;\n }\n\n list.innerHTML=data.skills.map(skill=>{\n const timeStr=formatTime(skill.createdAt);\n const tags=parseTags(skill.tags);\n const installedClass=skill.installed?'installed':'';\n const statusClass=skill.status==='archived'?'archived':(skill.status==='draft'?'draft':'');\n const qs=skill.qualityScore;\n const qsBadge=qs!==null&&qs!==undefined?'<span class=\"skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'\">\\u2605 '+qs.toFixed(1)+'</span>':'';\n return '<div class=\"skill-card '+installedClass+' '+statusClass+'\" onclick=\"openSkillDetail(\\''+skill.id+'\\')\">'+\n '<div class=\"skill-card-top\">'+\n '<div class=\"skill-card-name\">\\u{1F9E0} '+esc(skill.name)+'</div>'+\n '<div class=\"skill-card-badges\">'+\n qsBadge+\n '<span class=\"skill-badge version\">v'+skill.version+'</span>'+\n (skill.installed?'<span class=\"skill-badge installed\">'+t('skills.installed.badge')+'</span>':'')+\n '<span class=\"skill-badge status-'+skill.status+'\">'+t('skills.status.'+skill.status)+'</span>'+\n '</div>'+\n '</div>'+\n '<div class=\"skill-card-desc\">'+esc(skill.description)+'</div>'+\n '<div class=\"skill-card-bottom\">'+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4C5}</span> '+timeStr+'</span>'+\n '<span class=\"tag\"><span class=\"icon\">\\u{1F4E6}</span> '+skill.sourceType+'</span>'+\n (tags.length>0?'<div class=\"skill-card-tags\">'+tags.map(t=>'<span class=\"skill-tag\">'+esc(t)+'</span>').join('')+'</div>':'')+\n '</div>'+\n '</div>';\n }).join('');\n }catch(e){\n list.innerHTML='<div style=\"text-align:center;padding:24px;color:var(--rose)\">Failed to load skills: '+esc(String(e))+'</div>';\n }\n}\n\nfunction parseTags(tagsStr){\n try{ const arr=JSON.parse(tagsStr||'[]'); return Array.isArray(arr)?arr:[]; }catch{ return []; }\n}\n\nlet currentSkillId='';\n\nasync function openSkillDetail(skillId){\n currentSkillId=skillId;\n const overlay=document.getElementById('skillDetailOverlay');\n overlay.classList.add('show');\n document.getElementById('skillDetailTitle').textContent=t('skills.loading');\n document.getElementById('skillDetailMeta').innerHTML='';\n document.getElementById('skillDetailDesc').textContent='';\n document.getElementById('skillFilesList').innerHTML='';\n document.getElementById('skillDetailContent').innerHTML='<div class=\"spinner\"></div>';\n document.getElementById('skillVersionsList').innerHTML='<div class=\"spinner\"></div>';\n document.getElementById('skillRelatedTasks').innerHTML='';\n\n try{\n const r=await fetch('/api/skill/'+skillId);\n if(!r.ok){\n const errText=await r.text();\n throw new Error('API '+r.status+': '+errText);\n }\n const data=await r.json();\n if(!data.skill){\n throw new Error('No skill data in response: '+JSON.stringify(data).slice(0,200));\n }\n const skill=data.skill;\n const versions=data.versions||[];\n const relatedTasks=data.relatedTasks||[];\n const files=data.files||[];\n\n document.getElementById('skillDetailTitle').textContent='\\u{1F9E0} '+skill.name;\n\n const qs=skill.qualityScore;\n 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>':'';\n document.getElementById('skillDetailMeta').innerHTML=[\n '<span class=\"meta-item\"><span class=\"skill-badge version\">v'+skill.version+'</span></span>',\n '<span class=\"meta-item\"><span class=\"skill-badge status-'+skill.status+'\">'+t('skills.status.'+skill.status)+'</span></span>',\n qsBadge,\n skill.installed?'<span class=\"meta-item\"><span class=\"skill-badge installed\">'+t('skills.installed.badge')+'</span></span>':'',\n '<span class=\"meta-item\">\\u{1F4C5} '+formatTime(skill.createdAt)+'</span>',\n '<span class=\"meta-item\">\\u270F '+t('skills.updated')+formatTime(skill.updatedAt)+'</span>',\n ].filter(Boolean).join('');\n\n document.getElementById('skillDetailDesc').textContent=skill.description;\n\n if(files.length>0){\n const fileIcons={'skill':'\\u{1F4D6}','script':'\\u{2699}','reference':'\\u{1F4CE}','file':'\\u{1F4C4}'};\n document.getElementById('skillFilesList').innerHTML=files.map(f=>\n '<div class=\"skill-file-item\">'+\n '<span class=\"skill-file-icon\">'+(fileIcons[f.type]||'\\u{1F4C4}')+'</span>'+\n '<span class=\"skill-file-name\">'+esc(f.path)+'</span>'+\n '<span class=\"skill-file-type\">'+f.type+'</span>'+\n '<span class=\"skill-file-size\">'+(f.size>1024?(f.size/1024).toFixed(1)+'KB':f.size+'B')+'</span>'+\n '</div>'\n ).join('');\n } else {\n document.getElementById('skillFilesList').innerHTML='<div style=\"color:var(--text-muted);font-size:12px\">'+t('skills.nofiles')+'</div>';\n }\n\n const latestVersion=versions[0];\n document.getElementById('skillContentTitle').textContent=latestVersion?'SKILL.md (v'+latestVersion.version+')':t('skills.content');\n document.getElementById('skillDetailContent').innerHTML=latestVersion?renderSkillMarkdown(latestVersion.content):'<span style=\"color:var(--text-muted)\">'+t('skills.nocontent')+'</span>';\n\n if(versions.length===0){\n document.getElementById('skillVersionsList').innerHTML='<div style=\"color:var(--text-muted);font-size:13px\">'+t('skills.noversions')+'</div>';\n } else {\n document.getElementById('skillVersionsList').innerHTML=versions.map(v=>{\n const vqs=v.qualityScore;\n const vqsBadge=vqs!==null&&vqs!==undefined?'<span class=\"skill-badge quality '+(vqs>=7?'high':vqs>=5?'mid':'low')+'\">\\u2605 '+vqs.toFixed(1)+'</span>':'';\n const summaryHtml=v.changeSummary?'<div class=\"skill-version-summary\">'+esc(v.changeSummary)+'</div>':'';\n return '<div class=\"skill-version-item\">'+\n '<div class=\"skill-version-header\">'+\n '<span class=\"skill-version-badge\">v'+v.version+'</span>'+\n '<span class=\"skill-version-type\">'+v.upgradeType+'</span>'+\n vqsBadge+\n '</div>'+\n '<div class=\"skill-version-changelog\">'+esc(v.changelog||t('skills.nochangelog'))+'</div>'+\n summaryHtml+\n '<div class=\"skill-version-time\">'+formatTime(v.createdAt)+(v.sourceTaskId?' \\u2022 '+t('skills.task.prefix')+v.sourceTaskId.slice(0,8)+'...':'')+'</div>'+\n '</div>';\n }).join('');\n }\n\n if(relatedTasks.length===0){\n document.getElementById('skillRelatedTasks').innerHTML='<div style=\"color:var(--text-muted);font-size:13px\">'+t('skills.norelated')+'</div>';\n } else {\n document.getElementById('skillRelatedTasks').innerHTML=relatedTasks.map(rt=>\n '<div class=\"skill-related-task\" onclick=\"event.stopPropagation();closeSkillDetail();switchView(\\'tasks\\');setTimeout(()=>openTaskDetail(\\''+rt.task.id+'\\'),300)\">'+\n '<span class=\"relation\">'+rt.relation+'</span>'+\n '<span class=\"task-title\">'+esc(rt.task.title||t('tasks.untitled.related'))+'</span>'+\n '<span style=\"font-size:11px;color:var(--text-muted)\">'+formatTime(rt.task.startedAt)+'</span>'+\n '</div>'\n ).join('');\n }\n\n }catch(e){\n document.getElementById('skillDetailTitle').textContent=t('skills.error');\n document.getElementById('skillDetailContent').innerHTML='<div style=\"color:var(--rose);padding:16px\">'+t('skills.error.detail')+esc(String(e))+'</div>';\n document.getElementById('skillFilesList').innerHTML='';\n document.getElementById('skillVersionsList').innerHTML='';\n document.getElementById('skillRelatedTasks').innerHTML='';\n }\n}\n\nfunction downloadSkill(){\n if(!currentSkillId) return;\n window.open('/api/skill/'+currentSkillId+'/download','_blank');\n}\n\n/* \u2500\u2500\u2500 Settings / Config \u2500\u2500\u2500 */\nasync function loadConfig(){\n try{\n const r=await fetch('/api/config');\n if(!r.ok) return;\n const cfg=await r.json();\n const emb=cfg.embedding||{};\n document.getElementById('cfgEmbProvider').value=emb.provider||'openai_compatible';\n document.getElementById('cfgEmbModel').value=emb.model||'';\n document.getElementById('cfgEmbEndpoint').value=emb.endpoint||'';\n document.getElementById('cfgEmbApiKey').value=emb.apiKey||'';\n\n const sum=cfg.summarizer||{};\n document.getElementById('cfgSumProvider').value=sum.provider||'openai_compatible';\n document.getElementById('cfgSumModel').value=sum.model||'';\n document.getElementById('cfgSumEndpoint').value=sum.endpoint||'';\n document.getElementById('cfgSumApiKey').value=sum.apiKey||'';\n document.getElementById('cfgSumTemp').value=sum.temperature!=null?sum.temperature:'';\n\n const sk=cfg.skillEvolution||{};\n document.getElementById('cfgSkillEnabled').checked=sk.enabled!==false;\n document.getElementById('cfgSkillAutoInstall').checked=!!sk.autoInstall;\n document.getElementById('cfgSkillConfidence').value=sk.minConfidence||'';\n document.getElementById('cfgSkillMinChunks').value=sk.minChunksForEval||'';\n\n const skSum=sk.summarizer||{};\n document.getElementById('cfgSkillProvider').value=skSum.provider||'';\n document.getElementById('cfgSkillModel').value=skSum.model||'';\n document.getElementById('cfgSkillEndpoint').value=skSum.endpoint||'';\n document.getElementById('cfgSkillApiKey').value=skSum.apiKey||'';\n\n document.getElementById('cfgViewerPort').value=cfg.viewerPort||'';\n\n const tel=cfg.telemetry||{};\n document.getElementById('cfgTelemetryEnabled').checked=tel.enabled!==false;\n }catch(e){\n console.error('loadConfig error',e);\n }\n}\n\nasync function saveConfig(){\n const cfg={};\n const embP=document.getElementById('cfgEmbProvider').value;\n if(embP){\n cfg.embedding={provider:embP};\n const v=document.getElementById('cfgEmbModel').value.trim();if(v) cfg.embedding.model=v;\n const e=document.getElementById('cfgEmbEndpoint').value.trim();if(e) cfg.embedding.endpoint=e;\n const k=document.getElementById('cfgEmbApiKey').value.trim();if(k) cfg.embedding.apiKey=k;\n }\n const sumP=document.getElementById('cfgSumProvider').value;\n if(sumP){\n cfg.summarizer={provider:sumP};\n const v=document.getElementById('cfgSumModel').value.trim();if(v) cfg.summarizer.model=v;\n const e=document.getElementById('cfgSumEndpoint').value.trim();if(e) cfg.summarizer.endpoint=e;\n const k=document.getElementById('cfgSumApiKey').value.trim();if(k) cfg.summarizer.apiKey=k;\n const tp=document.getElementById('cfgSumTemp').value.trim();if(tp!=='') cfg.summarizer.temperature=Number(tp);\n }\n cfg.skillEvolution={\n enabled:document.getElementById('cfgSkillEnabled').checked,\n autoInstall:document.getElementById('cfgSkillAutoInstall').checked\n };\n const mc=document.getElementById('cfgSkillConfidence').value.trim();if(mc) cfg.skillEvolution.minConfidence=Number(mc);\n const mk=document.getElementById('cfgSkillMinChunks').value.trim();if(mk) cfg.skillEvolution.minChunksForEval=Number(mk);\n\n const skP=document.getElementById('cfgSkillProvider').value;\n if(skP){\n cfg.skillEvolution.summarizer={provider:skP};\n const sv=document.getElementById('cfgSkillModel').value.trim();if(sv) cfg.skillEvolution.summarizer.model=sv;\n const se=document.getElementById('cfgSkillEndpoint').value.trim();if(se) cfg.skillEvolution.summarizer.endpoint=se;\n const sk=document.getElementById('cfgSkillApiKey').value.trim();if(sk) cfg.skillEvolution.summarizer.apiKey=sk;\n }\n\n const vp=document.getElementById('cfgViewerPort').value.trim();\n if(vp) cfg.viewerPort=Number(vp);\n\n cfg.telemetry={\n enabled:document.getElementById('cfgTelemetryEnabled').checked\n };\n\n try{\n const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});\n if(!r.ok) throw new Error(await r.text());\n const el=document.getElementById('settingsSaved');\n el.classList.add('show');\n setTimeout(()=>el.classList.remove('show'),2500);\n }catch(e){\n showToast(t('settings.save.fail')+': '+e.message,'error');\n }\n}\n\nfunction renderSkillMarkdown(md){\n let content=md;\n // Strip YAML frontmatter\n content=content.replace(/^---[\\s\\S]*?---\\s*/,'');\n // Code blocks\n content=content.replace(/```(\\w*)\\n([\\s\\S]*?)```/g,function(_,lang,code){\n 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>';\n });\n // Inline code\n 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>');\n // Headers\n content=content.replace(/^### (.+)$/gm,'<div class=\"summary-section-title\" style=\"font-size:13px;margin-top:12px\">$1</div>');\n content=content.replace(/^## (.+)$/gm,'<div class=\"summary-section-title\">$1</div>');\n content=content.replace(/^# (.+)$/gm,'<div style=\"font-size:16px;font-weight:700;color:var(--text);margin:8px 0\">$1</div>');\n // Bold\n content=content.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');\n // List items\n content=content.replace(/^- (.+)$/gm,'<div style=\"padding-left:16px;position:relative;margin:3px 0\"><span style=\"position:absolute;left:4px;color:var(--text-muted)\">\u2022</span>$1</div>');\n // HTML comments (version markers)\n content=content.replace(/<!--[\\s\\S]*?-->/g,'');\n // Line breaks\n content=content.replace(/\\n\\n/g,'<div style=\"height:10px\"></div>');\n content=content.replace(/\\n/g,'<br>');\n return content;\n}\n\nfunction closeSkillDetail(event){\n if(event && event.target!==document.getElementById('skillDetailOverlay')) return;\n document.getElementById('skillDetailOverlay').classList.remove('show');\n}\n\nfunction formatDuration(ms){\n const s=Math.floor(ms/1000);\n if(s<60) return s+'s';\n const m=Math.floor(s/60);\n if(m<60) return m+'min';\n const h=Math.floor(m/60);\n if(h<24) return h+'h '+((m%60)>0?(m%60)+'min':'');\n const d=Math.floor(h/24);\n return d+'d '+((h%24)>0?(h%24)+'h':'');\n}\n\nfunction formatTime(ts){\n if(!ts) return '-';\n return new Date(ts).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});\n}\n\nfunction fillDays(rows,days){\n const map=new Map((rows||[]).map(r=>[r.date,{...r}]));\n const out=[];const now=new Date();\n for(let i=days-1;i>=0;i--){\n const d=new Date(now);d.setDate(d.getDate()-i);\n const dateStr=d.toISOString().slice(0,10);\n const row=map.get(dateStr)||{};\n out.push({date:dateStr,count:row.count??0,list:row.list??0,search:row.search??0,total:(row.list??0)+(row.search??0)});\n }\n if(days>21){\n const weeks=[];let i=0;\n while(i<out.length){\n const chunk=out.slice(i,i+7);\n const first=chunk[0].date,last=chunk[chunk.length-1].date;\n const c=chunk.reduce((s,r)=>s+r.count,0);\n const l=chunk.reduce((s,r)=>s+r.list,0);\n const se=chunk.reduce((s,r)=>s+r.search,0);\n const label=first.slice(5,10)+'~'+last.slice(8,10);\n weeks.push({date:label,count:c,list:l,search:se,total:l+se});\n i+=7;\n }\n return weeks;\n }\n return out;\n}\n\nfunction renderBars(el,data,valueKey,H){\n const vals=data.map(d=>d[valueKey]??0);\n 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;}\n const max=Math.max(1,...vals);\n const nonZero=vals.filter(v=>v>0).length;\n const barStyle=data.length<=7?'min-width:40px;max-width:120px':'';\n el.innerHTML=data.map(r=>{\n const v=r[valueKey]??0;\n const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);\n if(v===0){\n 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>';\n }\n const h=Math.max(8,Math.round((v/max)*H));\n 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>';\n }).join('');\n}\n\nfunction renderChartWrites(rows){\n const el=document.getElementById('chartWrites');\n const filled=fillDays(rows?.map(r=>({date:r.date,count:r.count})),metricsDays);\n renderBars(el,filled,'count',160);\n}\n\nfunction renderChartCalls(rows){\n const el=document.getElementById('chartCalls');\n const filled=fillDays(rows?.map(r=>({date:r.date,list:r.list,search:r.search})),metricsDays);\n const vals=filled.map(f=>f.total);\n if(vals.every(v=>v===0)){el.innerHTML='<div style=\"color:var(--text-muted);font-size:13px;padding:20px;text-align:center\">'+t('chart.nocalls')+'</div>';return;}\n const max=Math.max(1,...vals);\n const H=160;\n el.innerHTML=filled.map(r=>{\n const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);\n if(r.total===0){\n 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>';\n }\n const totalH=Math.max(8,Math.round((r.total/max)*H));\n const listH=r.list?Math.max(3,Math.round((r.list/r.total)*totalH)):0;\n const searchH=r.search?totalH-listH:0;\n const tip='List: '+r.list+', Search: '+r.search;\n let bars='';\n if(searchH>0) bars+='<div class=\"chart-bar violet\" style=\"height:'+searchH+'px\"></div>';\n if(listH>0) bars+='<div class=\"chart-bar\" style=\"height:'+listH+'px\"></div>';\n return '<div class=\"chart-bar-wrap\"><div class=\"chart-tip\">'+tip+'</div><div class=\"chart-bar-col\"><div style=\"display:flex;flex-direction:column;gap:1px\">'+bars+'</div></div><div class=\"chart-bar-label\">'+label+'</div></div>';\n }).join('');\n}\n\n/* \u2500\u2500\u2500 Tool Performance Chart \u2500\u2500\u2500 */\nlet toolMinutes=60;\nconst TOOL_COLORS=['#818cf8','#34d399','#fbbf24','#f87171','#38bdf8','#a78bfa','#fb923c'];\n\nfunction setToolMinutes(m){\n toolMinutes=m;\n document.querySelectorAll('.tool-range').forEach(b=>{\n b.classList.toggle('active',Number(b.dataset.mins)===m);\n });\n loadToolMetrics();\n}\n\nasync function loadToolMetrics(){\n try{\n const r=await fetch('/api/tool-metrics?minutes='+toolMinutes);\n if(!r.ok) return;\n const d=await r.json();\n if(d.error) return;\n renderToolChart(d);\n renderToolAgg(d);\n }catch(e){\n console.warn('loadToolMetrics error:',e);\n }\n}\n\nfunction renderToolChart(data){\n const container=document.getElementById('toolChart');\n const legend=document.getElementById('toolLegend');\n const {tools,series}=data;\n\n if(!series||series.length===0||tools.length===0){\n 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\">\uD83D\uDCCA</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>';\n legend.innerHTML='';\n return;\n }\n\n const W=container.clientWidth||800;\n const H=280;\n const pad={t:20,r:20,b:36,l:52};\n const cw=W-pad.l-pad.r;\n const ch=H-pad.t-pad.b;\n\n let maxVal=0;\n for(const s of series){for(const t of tools){const v=s[t]||0;if(v>maxVal)maxVal=v;}}\n if(maxVal===0)maxVal=100;\n maxVal=Math.ceil(maxVal*1.15);\n\n const gridLines=5;\n let gridHtml='';\n for(let i=0;i<=gridLines;i++){\n const y=pad.t+ch-(ch/gridLines)*i;\n const val=Math.round((maxVal/gridLines)*i);\n gridHtml+='<line class=\"grid-line\" x1=\"'+pad.l+'\" y1=\"'+y+'\" x2=\"'+(W-pad.r)+'\" y2=\"'+y+'\"/>';\n gridHtml+='<text class=\"axis-label\" x=\"'+(pad.l-8)+'\" y=\"'+(y+3)+'\" text-anchor=\"end\">'+val+'ms</text>';\n }\n\n const step=cw/(series.length-1||1);\n const labelEvery=Math.max(1,Math.floor(series.length/8));\n let labelsHtml='';\n series.forEach((s,i)=>{\n if(i%labelEvery===0||i===series.length-1){\n const x=pad.l+i*step;\n const time=s.minute.slice(11);\n labelsHtml+='<text class=\"axis-label\" x=\"'+x+'\" y=\"'+(H-4)+'\" text-anchor=\"middle\">'+time+'</text>';\n }\n });\n\n let pathsHtml='';\n let dotsHtml='';\n tools.forEach((toolName,ti)=>{\n const color=TOOL_COLORS[ti%TOOL_COLORS.length];\n const pts=series.map((s,i)=>{\n const x=pad.l+i*step;\n const v=s[toolName]||0;\n const y=pad.t+ch-((v/maxVal)*ch);\n return {x,y,v};\n });\n let line='M'+pts[0].x.toFixed(1)+' '+pts[0].y.toFixed(1);\n for(let i=1;i<pts.length;i++){\n const p0=pts[Math.max(0,i-2)],p1=pts[i-1],p2=pts[i],p3=pts[Math.min(pts.length-1,i+1)];\n const cp1x=(p1.x+(p2.x-p0.x)/6).toFixed(1),cp1y=(p1.y+(p2.y-p0.y)/6).toFixed(1);\n const cp2x=(p2.x-(p3.x-p1.x)/6).toFixed(1),cp2y=(p2.y-(p3.y-p1.y)/6).toFixed(1);\n line+=' C'+cp1x+' '+cp1y+','+cp2x+' '+cp2y+','+p2.x.toFixed(1)+' '+p2.y.toFixed(1);\n }\n pathsHtml+='<path class=\"data-line\" d=\"'+line+'\" stroke=\"'+color+'\" />';\n const area=line+' L'+pts[pts.length-1].x.toFixed(1)+' '+(pad.t+ch)+' L'+pts[0].x.toFixed(1)+' '+(pad.t+ch)+' Z';\n pathsHtml+='<path class=\"data-area\" d=\"'+area+'\" fill=\"url(#tg'+ti+')\" />';\n pts.forEach((p,i)=>{\n 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+'\" />';\n });\n });\n\n const svg='<svg class=\"tool-chart-svg\" viewBox=\"0 0 '+W+' '+H+'\" preserveAspectRatio=\"xMidYMid meet\">'+\n '<defs>'+\n tools.map((t,i)=>{\n const c=TOOL_COLORS[i%TOOL_COLORS.length];\n 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>'+\n '';\n }).join('')+'</defs>'+\n \n gridHtml+labelsHtml+pathsHtml+dotsHtml+\n '<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\" />'+\n '<rect class=\"hover-rect\" x=\"'+pad.l+'\" y=\"'+pad.t+'\" width=\"'+cw+'\" height=\"'+ch+'\" fill=\"transparent\" />'+\n '</svg><div class=\"tool-chart-tooltip\" id=\"toolTooltip\"></div>';\n\n container.innerHTML=svg;\n\n legend.innerHTML=tools.map((t,i)=>{\n const c=TOOL_COLORS[i%TOOL_COLORS.length];\n return '<span><span class=\"dot\" style=\"background:'+c+'\"></span>'+t+'</span>';\n }).join('');\n\n const svgEl=container.querySelector('svg');\n const tooltip=document.getElementById('toolTooltip');\n const rect=svgEl.querySelector('.hover-rect');\n\n rect.addEventListener('mousemove',function(e){\n const r=svgEl.getBoundingClientRect();\n const mx=e.clientX-r.left;\n const scale=W/r.width;\n const dataX=(mx*scale-pad.l)/step;\n const idx=Math.max(0,Math.min(series.length-1,Math.round(dataX)));\n const s=series[idx];\n if(!s)return;\n\n svgEl.querySelectorAll('.hover-dot').forEach(d=>{\n d.classList.toggle('show',Number(d.dataset.idx)===idx);\n });\n const crosshair=svgEl.querySelector('.crosshair');\n const cx=pad.l+idx*step;\n crosshair.setAttribute('x1',cx);crosshair.setAttribute('x2',cx);crosshair.setAttribute('opacity','0.5');\n\n let rows='<div class=\"tt-time\">'+s.minute+'</div>';\n tools.forEach((t,ti)=>{\n const v=s[t]||0;\n const c=TOOL_COLORS[ti%TOOL_COLORS.length];\n rows+='<div class=\"tt-row\"><span class=\"tt-dot\" style=\"background:'+c+'\"></span>'+t+'<span class=\"tt-val\">'+v+'ms</span></div>';\n });\n tooltip.innerHTML=rows;\n tooltip.classList.add('show');\n\n const tx=e.clientX-container.getBoundingClientRect().left;\n const ty=e.clientY-container.getBoundingClientRect().top;\n tooltip.style.left=(tx+15)+'px';\n tooltip.style.top=(ty-10)+'px';\n if(tx>container.clientWidth*0.7) tooltip.style.left=(tx-tooltip.offsetWidth-15)+'px';\n });\n\n rect.addEventListener('mouseleave',function(){\n svgEl.querySelectorAll('.hover-dot').forEach(d=>d.classList.remove('show'));\n svgEl.querySelector('.crosshair').setAttribute('opacity','0');\n tooltip.classList.remove('show');\n });\n}\n\nfunction renderToolAgg(data){\n const el=document.getElementById('toolAggTable');\n const {aggregated}=data;\n if(!aggregated||aggregated.length===0){el.innerHTML='';return;}\n\n const msClass=v=>v<100?'fast':v<500?'medium':'slow';\n\n 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>'+\n aggregated.map((a,i)=>{\n const c=TOOL_COLORS[i%TOOL_COLORS.length];\n return '<tr>'+\n '<td><span class=\"tool-name\"><span class=\"tool-dot\" style=\"background:'+c+'\"></span>'+a.tool+'</span></td>'+\n '<td>'+a.totalCalls+'</td>'+\n '<td><span class=\"ms-val '+msClass(a.avgMs)+'\">'+a.avgMs+'ms</span></td>'+\n '<td><span class=\"ms-val '+msClass(a.p95Ms)+'\">'+a.p95Ms+'ms</span></td>'+\n '<td>'+(a.errorCount>0?'<span style=\"color:var(--accent)\">'+a.errorCount+'</span>':'<span style=\"color:var(--text-muted)\">0</span>')+'</td>'+\n '</tr>';\n }).join('')+\n '</tbody></table>';\n}\n\nfunction renderBreakdown(obj,containerId){\n const el=document.getElementById(containerId);\n if(!el)return;\n const entries=Object.entries(obj||{}).sort((a,b)=>b[1]-a[1]);\n const total=entries.reduce((s,[,v])=>s+v,0)||1;\n el.innerHTML=entries.map(([label,value])=>{\n const pct=Math.round((value/total)*100);\n return '<div class=\"breakdown-item\"><div class=\"bd-top\"><span class=\"label\">'+esc(label)+'</span><span class=\"value\">'+value+' <span style=\"font-size:11px;font-weight:500;color:var(--text-muted)\">('+pct+'%)</span></span></div><div class=\"breakdown-bar-wrap\"><div class=\"breakdown-bar\" style=\"width:'+pct+'%\"></div></div></div>';\n }).join('');\n}\n\n/* \u2500\u2500\u2500 Data loading \u2500\u2500\u2500 */\nasync function loadAll(){\n await Promise.all([loadStats(),loadMemories()]);\n checkMigrateStatus();\n connectPPSSE();\n}\n\nasync function loadStats(){\n const r=await fetch('/api/stats');\n const d=await r.json();\n const dedupB=d.dedupBreakdown||{};\n const activeCount=dedupB.active||d.totalMemories;\n const inactiveCount=(dedupB.duplicate||0)+(dedupB.merged||0);\n document.getElementById('statTotal').textContent=d.totalMemories;\n if(inactiveCount>0){\n document.getElementById('statTotal').title=activeCount+' '+t('stat.active')+', '+inactiveCount+' '+t('stat.deduped');\n }\n document.getElementById('statSessions').textContent=d.totalSessions;\n document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;\n let days=0;\n if(d.timeRange&&d.timeRange.earliest!=null&&d.timeRange.latest!=null){\n let e=Number(d.timeRange.earliest), l=Number(d.timeRange.latest);\n if(Number.isFinite(e)&&Number.isFinite(l)){\n if(e<1e12) e*=1000;\n if(l<1e12) l*=1000;\n days=Math.round((l-e)/86400000);\n days=Math.max(0,Math.min(36500,days));\n if(days===0) days=1;\n }\n }\n document.getElementById('statTimeSpan').textContent=days;\n\n const provEl=document.getElementById('embeddingStatus');\n if(d.embeddingProvider && d.embeddingProvider!=='none'){\n provEl.innerHTML='<div class=\"provider-badge\"><span>\\u2713</span> '+t('embed.on')+d.embeddingProvider+'</div>';\n } else {\n provEl.innerHTML='<div class=\"provider-badge offline\"><span>\\u26A0</span> '+t('embed.off')+'</div>';\n }\n\n const sl=document.getElementById('sessionList');\n sl.innerHTML='<div class=\"session-item'+(activeSession===null?' active':'')+'\" onclick=\"filterSession(null)\"><span>'+t('sidebar.allsessions')+'</span><span class=\"count\">'+d.totalMemories+'</span></div>';\n (d.sessions||[]).forEach(s=>{\n const isActive=activeSession===s.session_key;\n const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;\n sl.innerHTML+='<div class=\"session-item'+(isActive?' active':'')+'\" onclick=\"filterSession(\\''+s.session_key.replace(/'/g,\"\\\\'\")+'\\')\"><span title=\"'+s.session_key+'\">'+name+'</span><span class=\"count\">'+s.count+'</span></div>';\n });\n}\n\nfunction getFilterParams(){\n const p=new URLSearchParams();\n if(activeSession) p.set('session',activeSession);\n if(activeRole) p.set('role',activeRole);\n const kind=document.getElementById('filterKind').value;\n if(kind) p.set('kind',kind);\n const df=document.getElementById('dateFrom').value;\n if(df) p.set('dateFrom',df);\n const dt=document.getElementById('dateTo').value;\n if(dt) p.set('dateTo',dt);\n const sort=document.getElementById('filterSort').value;\n if(sort==='oldest') p.set('sort','oldest');\n return p;\n}\n\nasync function loadMemories(page){\n if(page) currentPage=page;\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('limit',PAGE_SIZE);\n p.set('page',currentPage);\n const r=await fetch('/api/memories?'+p.toString());\n const d=await r.json();\n totalPages=d.totalPages||1;\n totalCount=d.total||0;\n document.getElementById('searchMeta').textContent=totalCount+t('search.meta.total');\n renderMemories(d.memories||[]);\n renderPagination();\n}\n\nasync function doSearch(q){\n if(!q.trim()){currentPage=1;loadMemories();return}\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('q',q);\n const r=await fetch('/api/search?'+p.toString());\n const d=await r.json();\n const meta=[];\n if(d.vectorCount>0) meta.push(d.vectorCount+t('search.meta.semantic'));\n if(d.ftsCount>0) meta.push(d.ftsCount+t('search.meta.text'));\n meta.push(d.total+t('search.meta.results'));\n document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');\n renderMemories(d.results||[]);\n document.getElementById('pagination').innerHTML='';\n}\n\nfunction debounceSearch(){\n clearTimeout(searchTimer);\n searchTimer=setTimeout(()=>doSearch(document.getElementById('searchInput').value),350);\n}\n\nfunction filterSession(key){\n activeSession=key;\n currentPage=1;\n loadAll();\n}\n\nfunction setRoleFilter(btn,role){\n activeRole=role;\n currentPage=1;\n document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n applyFilters();\n}\n\nfunction applyFilters(){\n currentPage=1;\n if(document.getElementById('searchInput').value.trim()){\n doSearch(document.getElementById('searchInput').value);\n } else {\n loadMemories();\n }\n}\n\nfunction clearDateFilter(){\n document.getElementById('dateFrom').value='';\n document.getElementById('dateTo').value='';\n applyFilters();\n}\n\n/* \u2500\u2500\u2500 Rendering \u2500\u2500\u2500 */\nfunction renderMemories(items){\n const list=document.getElementById('memoryList');\n if(!items.length){\n list.innerHTML='<div class=\"empty\"><div class=\"icon\">\\u{1F4ED}</div><p>'+t('empty.text')+'</p></div>';\n return;\n }\n items.forEach(m=>{memoryCache[m.id]=m});\n list.innerHTML=items.map(m=>{\n const time=m.created_at?new Date(typeof m.created_at==='number'?m.created_at:m.created_at).toLocaleString('zh-CN'):'';\n const role=m.role||'user';\n const kind=m.kind||'paragraph';\n const summary=esc(m.summary||m.content?.slice(0,120)||'');\n const content=esc(m.content||'');\n const id=m.id;\n const vscore=m._vscore?'<span class=\"vscore-badge\">'+Math.round(m._vscore*100)+'%</span>':'';\n const sid=m.session_key||'';\n const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;\n const mc=m.merge_count||0;\n const mergeBadge=mc>0?'<span class=\"merge-badge\">\\u{1F504} '+t('card.evolved')+' '+mc+t('card.times')+'</span>':'';\n 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>':'';\n const ds=m.dedup_status||'active';\n const isInactive=ds==='duplicate'||ds==='merged';\n const dedupBadge=ds==='duplicate'?'<span class=\"dedup-badge duplicate\">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class=\"dedup-badge merged\">'+t('card.dedupMerged')+'</span>':'';\n const isImported=sid.startsWith('openclaw-import-')||sid.startsWith('openclaw-session-');\n const importBadge=isImported?'<span class=\"import-badge\">\uD83E\uDD90 '+t('card.imported')+'</span>':'';\n let dedupInfo='';\n if(isInactive){\n const reason=m.dedup_reason?'<span style=\"font-size:11px;color:var(--text-muted)\">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';\n 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>':'';\n dedupInfo='<div style=\"margin-top:6px;font-size:11px\">'+target+' '+reason+'</div>';\n }\n let historyHtml='';\n if(mc>0){\n try{\n const hist=JSON.parse(m.merge_history||'[]');\n if(hist.length>0){\n 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>';\n hist.forEach(function(h){\n const ht=h.at?new Date(h.at).toLocaleString('zh-CN'):'';\n 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||'');\n if(h.from) historyHtml+='<br><span style=\"opacity:.6\">'+t('card.oldSummary')+':</span> '+esc(h.from);\n if(h.to) historyHtml+='<br><span style=\"opacity:.6\">'+t('card.newSummary')+':</span> '+esc(h.to);\n historyHtml+='</div>';\n });\n historyHtml+='</div>';\n }\n }catch(e){}\n }\n return '<div class=\"memory-card'+(isInactive?' dedup-inactive':'')+'\">'+\n '<div class=\"card-header\"><div class=\"meta\"><span class=\"role-tag '+role+'\">'+role+'</span><span class=\"kind-tag\">'+kind+'</span>'+importBadge+dedupBadge+mergeBadge+'</div><span class=\"card-time\"><span class=\"session-tag\" title=\"'+esc(sid)+'\">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+\n '<div class=\"card-summary\">'+summary+'</div>'+\n dedupInfo+\n '<div class=\"card-content\" id=\"content-'+id+'\"><pre>'+content+'</pre></div>'+\n historyHtml+\n '<div class=\"card-actions\">'+\n '<button class=\"btn btn-sm btn-ghost\" onclick=\"toggleContent(\\''+id+'\\')\">'+t('card.expand')+'</button>'+\n (mc>0?'<button class=\"btn btn-sm btn-ghost\" onclick=\"toggleHistory(\\''+id+'\\')\">'+t('card.evolveHistory')+'</button>':'')+\n '<button class=\"btn btn-sm btn-ghost\" onclick=\"openEditModal(\\''+id+'\\')\">'+t('card.edit')+'</button>'+\n '<button class=\"btn btn-sm btn-ghost\" style=\"color:var(--accent)\" onclick=\"deleteMemory(\\''+id+'\\')\">'+t('card.delete')+'</button>'+\n vscore+\n '</div></div>';\n }).join('');\n}\n\nfunction renderPagination(){\n const el=document.getElementById('pagination');\n if(totalPages<=1){el.innerHTML='';return}\n let h='';\n h+='<button class=\"pg-btn'+(currentPage<=1?' disabled':'')+'\" onclick=\"goPage('+(currentPage-1)+')\">\u2039</button>';\n const range=[];\n range.push(1);\n for(let i=Math.max(2,currentPage-2);i<=Math.min(totalPages-1,currentPage+2);i++) range.push(i);\n if(totalPages>1) range.push(totalPages);\n const unique=[...new Set(range)].sort((a,b)=>a-b);\n let prev=0;\n for(const p of unique){\n if(p-prev>1) h+='<span class=\"pg-info\">...</span>';\n h+='<button class=\"pg-btn'+(p===currentPage?' active':'')+'\" onclick=\"goPage('+p+')\">'+p+'</button>';\n prev=p;\n }\n h+='<button class=\"pg-btn'+(currentPage>=totalPages?' disabled':'')+'\" onclick=\"goPage('+(currentPage+1)+')\">\u203A</button>';\n h+='<span class=\"pg-info\">'+totalCount+t('pagination.total')+'</span>';\n el.innerHTML=h;\n}\n\nfunction goPage(p){\n if(p<1||p>totalPages||p===currentPage) return;\n currentPage=p;\n loadMemories();\n document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});\n}\n\nfunction toggleHistory(id){\n const el=document.getElementById('history-'+id);\n if(el) el.style.display=el.style.display==='none'?'block':'none';\n}\n\nfunction toggleContent(id){\n const el=document.getElementById('content-'+id);\n el.classList.toggle('show');\n}\n\nfunction scrollToMemory(targetId){\n const cards=document.querySelectorAll('.memory-card');\n for(const card of cards){\n const contentEl=card.querySelector('[id^=\"content-\"]');\n if(contentEl&&contentEl.id==='content-'+targetId){\n card.scrollIntoView({behavior:'smooth',block:'center'});\n card.style.transition='box-shadow .3s';\n card.style.boxShadow='0 0 0 2px var(--pri)';\n setTimeout(()=>{card.style.boxShadow='';},2000);\n return;\n }\n }\n showMemoryModal(targetId);\n}\nasync function showMemoryModal(chunkId){\n const overlay=document.getElementById('memoryModal');\n const body=document.getElementById('memoryModalBody');\n body.innerHTML='<div style=\"text-align:center;padding:40px;color:var(--text-sec)\">Loading...</div>';\n overlay.classList.add('show');\n try{\n const res=await fetch('/api/memory/'+encodeURIComponent(chunkId));\n if(!res.ok){body.innerHTML='<div style=\"text-align:center;padding:40px;color:#f87171\">Memory not found</div>';return;}\n const data=await res.json();\n const m=data.memory;\n const role=(m.role||'unknown').toUpperCase();\n const roleCls=(m.role||'').toLowerCase();\n const kind=m.kind||'paragraph';\n const ds=m.dedup_status||'active';\n const time=new Date(m.created_at).toLocaleString('zh-CN');\n const updated=m.updated_at?new Date(m.updated_at).toLocaleString('zh-CN'):'';\n let html='<div class=\"modal-memory-card\">';\n html+='<div class=\"modal-header-row\"><span class=\"role-tag '+roleCls+'\">'+role+'</span><span class=\"kind-tag\">'+kind+'</span>';\n if(ds!=='active') html+='<span class=\"dedup-badge '+(ds==='duplicate'?'duplicate':'merged')+'\">'+ds+'</span>';\n html+='</div>';\n 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>';\n 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>';\n html+='<div class=\"modal-field\"><div class=\"modal-field-label\">Content</div><pre class=\"modal-field-content\">'+esc(m.content||'')+'</pre></div>';\n html+='<div class=\"modal-meta-row\">';\n html+='<span><strong>Session:</strong> '+esc(m.session_key||'')+'</span>';\n html+='<span><strong>Created:</strong> '+time+'</span>';\n if(updated) html+='<span><strong>Updated:</strong> '+updated+'</span>';\n html+='</div>';\n 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>';\n 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>';\n html+='</div>';\n body.innerHTML=html;\n }catch(e){body.innerHTML='<div style=\"text-align:center;padding:40px;color:#f87171\">Error: '+esc(String(e))+'</div>';}\n}\nfunction closeMemoryModal(){document.getElementById('memoryModal').classList.remove('show');}\n\n\nfunction esc(s){\n if(!s)return'';\n return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n}\n\nfunction renderSummaryHtml(raw){\n if(!raw)return'';\n var lines=raw.split('\\n');\n var html=[];\n var inList=false;\n var sectionRe=new RegExp('^(\uD83C\uDFAF|\uD83D\uDCCB|\u2705|\uD83D\uDCA1)\\\\s+(.+)$');\n var listRe=new RegExp('^- (.+)$');\n for(var i=0;i<lines.length;i++){\n var line=lines[i];\n var hm=line.match(sectionRe);\n if(hm){\n if(inList){html.push('</ul>');inList=false;}\n html.push('<div class=\"summary-section-title\">'+esc(line)+'</div>');\n continue;\n }\n var lm=line.match(listRe);\n if(lm){\n if(!inList){html.push('<ul>');inList=true;}\n html.push('<li>'+esc(lm[1])+'</li>');\n continue;\n }\n if(line.trim()===''){\n if(inList){html.push('</ul>');inList=false;}\n continue;\n }\n if(inList){html.push('</ul>');inList=false;}\n html.push('<p style=\"margin:4px 0\">'+esc(line)+'</p>');\n }\n if(inList)html.push('</ul>');\n return html.join('');\n}\n\n/* \u2500\u2500\u2500 CRUD \u2500\u2500\u2500 */\nfunction openCreateModal(){\n editingId=null;\n document.getElementById('modalTitle').textContent=t('modal.new');\n document.getElementById('modalSubmit').textContent=t('modal.create');\n document.getElementById('mRole').value='user';\n document.getElementById('mContent').value='';\n document.getElementById('mSummary').value='';\n document.getElementById('mKind').value='paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction openEditModal(id){\n const m=memoryCache[id];\n if(!m){toast(t('toast.notfound'),'error');return}\n editingId=id;\n document.getElementById('modalTitle').textContent=t('modal.edit');\n document.getElementById('modalSubmit').textContent=t('modal.save');\n document.getElementById('mRole').value=m.role||'user';\n document.getElementById('mContent').value=m.content||'';\n document.getElementById('mSummary').value=m.summary||'';\n document.getElementById('mKind').value=m.kind||'paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction closeModal(){\n document.getElementById('modalOverlay').classList.remove('show');\n}\n\nasync function submitModal(){\n const data={\n role:document.getElementById('mRole').value,\n content:document.getElementById('mContent').value,\n summary:document.getElementById('mSummary').value,\n kind:document.getElementById('mKind').value,\n };\n if(!data.content.trim()){toast(t('modal.err.empty'),'error');return}\n let r;\n if(editingId){\n r=await fetch('/api/memory/'+editingId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n } else {\n r=await fetch('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n }\n const d=await r.json();\n if(d.ok){toast(editingId?t('toast.updated'):t('toast.created'),'success');closeModal();loadAll();}\n else{toast(d.error||t('toast.opfail'),'error')}\n}\n\nasync function deleteMemory(id){\n if(!confirm(t('confirm.delete')))return;\n const r=await fetch('/api/memory/'+id,{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast(t('toast.deleted'),'success');loadAll();}\n else{toast(t('toast.delfail'),'error')}\n}\n\nasync function clearAll(){\n if(!confirm(t('confirm.clearall')))return;\n if(!confirm(t('confirm.clearall2')))return;\n const r=await fetch('/api/memories',{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast(t('toast.cleared'),'success');loadAll();}\n else{toast(t('toast.clearfail'),'error')}\n}\n\n/* \u2500\u2500\u2500 Migration \u2500\u2500\u2500 */\nlet migrateScanData=null;\nlet migrateStats={stored:0,skipped:0,merged:0,errors:0};\n\nasync function migrateScan(){\n const btn=document.getElementById('migrateScanBtn');\n btn.disabled=true;\n btn.textContent=t('migrate.scanning');\n document.getElementById('migrateStartBtn').style.display='none';\n document.getElementById('migrateScanResult').style.display='none';\n document.getElementById('migrateConfigWarn').style.display='none';\n document.getElementById('migrateProgress').style.display='none';\n\n try{\n const r=await fetch('/api/migrate/scan');\n if(!r.ok){ const err=await r.text(); throw new Error(err); }\n const d=await r.json();\n migrateScanData=d;\n\n const files=d.sqliteFiles||[];\n const sess=d.sessions||{count:0,messages:0};\n const sqliteTotal=files.reduce((s,f)=>s+f.chunks,0);\n document.getElementById('migrateSqliteCount').textContent=sqliteTotal;\n document.getElementById('migrateSqliteFiles').textContent=files.map(f=>f.file+' ('+f.chunks+')').join(', ')||'\u2014';\n document.getElementById('migrateSessionCount').textContent=sess.messages;\n document.getElementById('migrateSessionFiles').textContent=sess.count+' '+t('migrate.sessions.count').replace('{n}',sess.messages);\n document.getElementById('migrateScanResult').style.display='block';\n\n if(!d.configReady){\n document.getElementById('migrateConfigWarn').style.display='block';\n const parts=[];\n if(!d.hasEmbedding) parts.push('Embedding');\n if(!d.hasSummarizer) parts.push('Summarizer');\n document.getElementById('migrateConfigWarn').querySelector('div:last-child').textContent=\n t('migrate.config.warn.desc')+' ('+parts.join(', ')+')';\n }\n\n if(d.totalItems>0 && d.configReady){\n document.getElementById('migrateStartBtn').style.display='inline-flex';\n }\n\n if(d.totalItems===0){\n document.getElementById('migrateStatus').textContent=t('migrate.nodata');\n }\n\n if(d.hasImportedData){\n document.getElementById('postprocessSection').style.display='block';\n }\n }catch(e){\n toast('Scan failed: '+e.message,'error');\n }finally{\n btn.disabled=false;\n btn.textContent=t('migrate.scan');\n }\n}\n\nfunction migrateStart(){\n if(!migrateScanData||!migrateScanData.configReady)return;\n if(!confirm(t('migrate.start')+'?'))return;\n\n window._migrateRunning=true;\n _migrateStatusChecked=false;\n document.getElementById('migrateStartBtn').style.display='none';\n document.getElementById('migrateScanBtn').disabled=true;\n document.getElementById('migrateProgress').style.display='block';\n document.getElementById('migrateLiveLog').innerHTML='';\n migrateStats={stored:0,skipped:0,merged:0,errors:0};\n updateMigrateStats();\n\n document.getElementById('migrateStopBtn').disabled=false;\n document.getElementById('migrateBar').style.width='0%';\n document.getElementById('migrateBar').style.background='linear-gradient(90deg,#6366f1,#8b5cf6)';\n document.getElementById('migrateCounter').textContent='';\n const body=JSON.stringify({sources:['sqlite','sessions']});\n connectMigrateSSE('/api/migrate/start','POST',body);\n}\n\nasync function migrateStop(){\n const btn=document.getElementById('migrateStopBtn');\n btn.disabled=true;\n btn.textContent=t('migrate.stopping');\n try{\n await fetch('/api/migrate/stop',{method:'POST'});\n }catch(e){\n toast('Stop failed: '+e.message,'error');\n btn.disabled=false;\n btn.textContent=t('migrate.stop');\n }\n}\n\nfunction connectMigrateSSE(url,method,body){\n const opts={method:method||'GET'};\n if(body){opts.headers={'Content-Type':'application/json'};opts.body=body;}\n fetch(url,opts)\n .then(r=>{\n if(!r.ok){toast('Migration request failed: '+r.status,'error');onMigrateDone(false);return;}\n readSSEStream(r);\n })\n .catch(e=>{toast('Migration failed: '+e.message,'error');onMigrateDone(false);});\n}\n\nfunction readSSEStream(r){\n const reader=r.body.getReader();\n const decoder=new TextDecoder();\n let buf='';\n let migrateDoneCalled=false;\n const NL=String.fromCharCode(10);\n function pump(){\n reader.read().then(({done,value})=>{\n if(done){if(!migrateDoneCalled&&!window._migrateRunning)onMigrateDone(false);return;}\n buf+=decoder.decode(value,{stream:true});\n const lines=buf.split(NL);\n buf=lines.pop()||'';\n let evtType='';\n for(const line of lines){\n if(line.startsWith('event: ')){evtType=line.slice(7).trim();}\n else if(line.startsWith('data: ')){\n try{\n const data=JSON.parse(line.slice(6));\n if(evtType==='done'||evtType==='stopped') migrateDoneCalled=true;\n handleMigrateEvent(evtType,data);\n }catch{}\n }\n }\n pump();\n });\n }\n pump();\n}\n\nvar _migrateStatusChecked=false;\nasync function checkMigrateStatus(){\n if(_migrateStatusChecked) return;\n _migrateStatusChecked=true;\n try{\n const r=await fetch('/api/migrate/status');\n if(!r.ok)return;\n const s=await r.json();\n if(s.running){\n window._migrateRunning=true;\n switchView('import');\n migrateStats={stored:s.stored,skipped:s.skipped,merged:s.merged,errors:s.errors};\n updateMigrateStats();\n const progEl=document.getElementById('migrateProgress');\n if(!progEl)return;\n progEl.style.display='block';\n document.getElementById('migrateStartBtn').style.display='none';\n document.getElementById('migrateScanBtn').disabled=true;\n document.getElementById('migrateStopBtn').disabled=false;\n const pct=s.total>0?Math.round((s.processed/s.total)*100):0;\n document.getElementById('migrateBar').style.width=pct+'%';\n document.getElementById('migrateCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';\n const label=s.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');\n document.getElementById('migratePhaseLabel').textContent=label;\n connectMigrateSSE('/api/migrate/stream','GET',null);\n }else if(s.done&&(s.stored>0||s.skipped>0||s.stopped)){\n migrateStats={stored:s.stored,skipped:s.skipped,merged:s.merged,errors:s.errors};\n updateMigrateStats();\n const progEl=document.getElementById('migrateProgress');\n if(!progEl)return;\n progEl.style.display='block';\n const pct=s.total>0?Math.round((s.processed/s.total)*100):0;\n document.getElementById('migrateBar').style.width=pct+'%';\n document.getElementById('migrateCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';\n onMigrateDone(!!s.stopped,true);\n }\n }catch(e){console.log('checkMigrateStatus error',e);}\n}\n\nfunction handleMigrateEvent(evtType,data){\n if(evtType==='phase'){\n const label=data.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');\n document.getElementById('migratePhaseLabel').textContent=label;\n }\n else if(evtType==='progress'){\n document.getElementById('migrateCounter').textContent=data.processed+' / '+data.total;\n }\n else if(evtType==='item'){\n if(data.status==='stored')migrateStats.stored++;\n else if(data.status==='skipped'||data.status==='duplicate')migrateStats.skipped++;\n else if(data.status==='merged')migrateStats.merged++;\n else if(data.status==='error')migrateStats.errors++;\n updateMigrateStats();\n\n const pct=data.total>0?Math.round((data.index/data.total)*100):0;\n document.getElementById('migrateBar').style.width=pct+'%';\n document.getElementById('migrateCounter').textContent=data.index+' / '+data.total+' ('+pct+'%)';\n\n appendMigrateLogItem(data);\n }\n else if(evtType==='error'){\n migrateStats.errors++;\n updateMigrateStats();\n appendMigrateLogItem({status:'error',preview:data.error||data.file,source:data.file});\n }\n else if(evtType==='summary'){\n document.getElementById('migrateBar').style.width='100%';\n const tp=data.totalProcessed||0;\n document.getElementById('migrateCounter').textContent=tp+' / '+tp+' (100%)';\n }\n else if(evtType==='done'){\n onMigrateDone(false);\n }\n else if(evtType==='stopped'){\n onMigrateDone(true);\n }\n else if(evtType==='state'){\n migrateStats={stored:data.stored||0,skipped:data.skipped||0,merged:data.merged||0,errors:data.errors||0};\n updateMigrateStats();\n const pct=data.total>0?Math.round((data.processed/data.total)*100):0;\n document.getElementById('migrateBar').style.width=pct+'%';\n document.getElementById('migrateCounter').textContent=data.processed+' / '+data.total+' ('+pct+'%)';\n if(data.phase){\n const label=data.phase==='sqlite'?t('migrate.phase.sqlite'):t('migrate.phase.sessions');\n document.getElementById('migratePhaseLabel').textContent=label;\n }\n }\n}\n\nfunction updateMigrateStats(){\n document.getElementById('migrateStatStored').textContent=migrateStats.stored;\n document.getElementById('migrateStatSkipped').textContent=migrateStats.skipped;\n document.getElementById('migrateStatMerged').textContent=migrateStats.merged;\n document.getElementById('migrateStatErrors').textContent=migrateStats.errors;\n}\n\nfunction appendMigrateLogItem(data){\n const log=document.getElementById('migrateLiveLog');\n const icons={stored:'\\u2705',skipped:'\\u23ED',merged:'\\u{1F500}',error:'\\u274C',duplicate:'\\u23ED'};\n const statusClass=data.status==='duplicate'?'skipped':data.status;\n const el=document.createElement('div');\n el.className='migrate-log-item';\n el.innerHTML=\n '<div class=\"log-icon '+statusClass+'\">'+( icons[data.status]||'\\u2022')+'</div>'+\n '<div class=\"log-body\">'+\n '<div class=\"log-preview\">'+esc(data.preview||'')+'</div>'+\n '<div class=\"log-meta\">'+\n '<span class=\"tag '+statusClass+'\">'+(data.status||'').toUpperCase()+'</span>'+\n (data.source?'<span>'+esc(data.source)+'</span>':'')+\n (data.role?'<span>'+data.role+'</span>':'')+\n (data.summary?'<span style=\"opacity:.7\">'+esc(data.summary)+'</span>':'')+\n '</div>'+\n '</div>';\n log.appendChild(el);\n log.scrollTop=log.scrollHeight;\n}\n\nfunction onMigrateDone(wasStopped,skipReload){\n window._migrateRunning=false;\n document.getElementById('migrateScanBtn').disabled=false;\n document.getElementById('migrateStopBtn').disabled=true;\n document.getElementById('migrateStopBtn').textContent=t('migrate.stop');\n if(wasStopped){\n document.getElementById('migrateBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';\n document.getElementById('migrateStartBtn').style.display='inline-flex';\n document.getElementById('migrateStartBtn').textContent=t('migrate.resume');\n }else{\n document.getElementById('migrateBar').style.width='100%';\n document.getElementById('migrateBar').style.background='linear-gradient(90deg,#22c55e,#16a34a)';\n const total=migrateStats.stored+migrateStats.skipped+migrateStats.merged+migrateStats.errors;\n if(total>0) document.getElementById('migrateCounter').textContent=total+' / '+total+' (100%)';\n }\n fetch('/api/migrate/scan').then(r=>{if(!r.ok)throw new Error();return r.json()}).then(d=>{\n if(d&&d.hasImportedData) document.getElementById('postprocessSection').style.display='block';\n }).catch(()=>{});\n if(!skipReload) loadAll();\n}\n\n/* \u2500\u2500\u2500 Post-processing: tasks & skills \u2500\u2500\u2500 */\n\nvar ppStats={tasks:0,skills:0,errors:0,skipped:0};\nwindow._ppRunning=false;\n\nfunction ppStart(){\n var enableTasks=document.getElementById('ppEnableTasks').checked;\n var enableSkills=document.getElementById('ppEnableSkills').checked;\n if(!enableTasks&&!enableSkills){toast(t('pp.select.warn'),'error');return;}\n\n window._ppRunning=true;\n _ppSSEConnected=false;\n ppStats={tasks:0,skills:0,errors:0,skipped:0};\n document.getElementById('ppStartBtn').style.display='none';\n document.getElementById('ppStopBtn').style.display='inline-flex';\n document.getElementById('ppStopBtn').disabled=false;\n document.getElementById('ppStopBtn').textContent=t('migrate.stop');\n document.getElementById('ppProgress').style.display='block';\n document.getElementById('ppDone').style.display='none';\n document.getElementById('ppBar').style.width='0%';\n document.getElementById('ppBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';\n document.getElementById('ppPhaseLabel').textContent=t('pp.running');\n document.getElementById('ppCounter').textContent='';\n document.getElementById('ppLiveLog').innerHTML='';\n updatePPStats();\n\n var body=JSON.stringify({enableTasks:enableTasks,enableSkills:enableSkills});\n fetch('/api/migrate/postprocess',{method:'POST',headers:{'Content-Type':'application/json'},body:body})\n .then(function(r){\n if(!r.ok){\n r.json().then(function(j){toast(j.error||('Postprocess failed: '+r.status),'error');}).catch(function(){toast('Postprocess failed: '+r.status,'error');});\n ppDone(false,true);\n return;\n }\n readPPStream(r.body.getReader());\n })\n .catch(function(e){toast('Postprocess failed: '+e.message,'error');ppDone(false,true);});\n}\n\nfunction updatePPStats(){\n document.getElementById('ppStatTasks').textContent=ppStats.tasks;\n document.getElementById('ppStatSkills').textContent=ppStats.skills;\n document.getElementById('ppStatErrors').textContent=ppStats.errors;\n document.getElementById('ppStatSkipped').textContent=ppStats.skipped;\n}\n\nfunction appendPPLogItem(data){\n var log=document.getElementById('ppLiveLog');\n var el=document.createElement('div');\n el.style.cssText='display:flex;align-items:flex-start;gap:8px;padding:6px 12px;border-bottom:1px solid var(--border)';\n var icon='\\u2022';var color='var(--text-muted)';\n if(data.step==='done'){icon='\\u2705';color='#22c55e';}\n else if(data.step==='error'){icon='\\u274C';color='#ef4444';}\n else if(data.step==='processing'){icon='\\u23F3';color='#f59e0b';}\n else if(data.step==='skipped'){icon='\\u23ED';color='#3b82f6';}\n else if(data.step==='skill'){icon='\\u{1F9E0}';color='#8b5cf6';}\n var label=data.taskTitle||data.session||data.title||'';\n if(label.length>60)label=label.slice(0,57)+'...';\n el.innerHTML='<span style=\"color:'+color+';min-width:18px\">'+icon+'</span>'+\n '<span style=\"flex:1;color:var(--text-sec)\">'+esc(label)+'</span>'+\n '<span style=\"color:var(--text-muted);font-size:10px\">'+(data.index||'')+' / '+(data.total||'')+'</span>';\n if(data.error) el.innerHTML+='<span style=\"color:#ef4444;font-size:10px\">'+esc(data.error)+'</span>';\n log.appendChild(el);\n log.scrollTop=log.scrollHeight;\n}\n\nfunction readPPStream(reader){\n var NL=String.fromCharCode(10);\n var dec=new TextDecoder();\n var buf='';\n var ppDoneCalled=false;\n function pump(){\n reader.read().then(function(result){\n if(result.done){if(!ppDoneCalled)ppDone(false);return;}\n buf+=dec.decode(result.value,{stream:true});\n var lines=buf.split(NL);\n buf=lines.pop()||'';\n var evtType='';\n for(var i=0;i<lines.length;i++){\n var line=lines[i];\n if(line.startsWith('event: '))evtType=line.slice(7).trim();\n else if(line.startsWith('data: ')&&evtType){\n try{\n if(evtType==='done'||evtType==='stopped')ppDoneCalled=true;\n handlePPEvent(evtType,JSON.parse(line.slice(6)));\n }catch(e){}\n evtType='';\n }\n }\n pump();\n }).catch(function(){if(!ppDoneCalled)ppDone(false);});\n }\n pump();\n}\n\nvar _ppSSEConnected=false;\nfunction connectPPSSE(){\n if(_ppSSEConnected) return;\n _ppSSEConnected=true;\n fetch('/api/migrate/postprocess/status').then(function(r){return r.json();}).then(function(s){\n if(s.running){\n window._ppRunning=true;\n document.getElementById('postprocessSection').style.display='block';\n document.getElementById('ppStartBtn').style.display='none';\n document.getElementById('ppStopBtn').style.display='inline-flex';\n document.getElementById('ppStopBtn').disabled=false;\n document.getElementById('ppStopBtn').textContent=t('migrate.stop');\n document.getElementById('ppProgress').style.display='block';\n document.getElementById('ppDone').style.display='none';\n ppStats={tasks:s.tasksCreated||0,skills:s.skillsCreated||0,errors:s.errors||0,skipped:0};\n updatePPStats();\n var pct=s.total>0?Math.round((s.processed/s.total)*100):0;\n document.getElementById('ppBar').style.width=pct+'%';\n document.getElementById('ppCounter').textContent=s.processed+' / '+s.total+' ('+pct+'%)';\n document.getElementById('ppPhaseLabel').textContent=t('pp.running');\n fetch('/api/migrate/postprocess/stream',{method:'GET'}).then(function(r){\n if(r.ok&&r.body)readPPStream(r.body.getReader());\n }).catch(function(){});\n }else if(s.done){\n document.getElementById('postprocessSection').style.display='block';\n ppStats={tasks:s.tasksCreated||0,skills:s.skillsCreated||0,errors:s.errors||0,skipped:0};\n updatePPStats();\n document.getElementById('ppProgress').style.display='block';\n var pct2=s.total>0?Math.round((s.processed/s.total)*100):0;\n document.getElementById('ppBar').style.width=pct2+'%';\n document.getElementById('ppCounter').textContent=s.processed+' / '+s.total+' ('+pct2+'%)';\n ppDone(!!s.stopped,false,true);\n }\n }).catch(function(){});\n}\n\nfunction handlePPEvent(evtType,data){\n if(evtType==='progress'){\n var pct=data.total>0?Math.round((data.processed/data.total)*100):0;\n document.getElementById('ppBar').style.width=pct+'%';\n document.getElementById('ppCounter').textContent=data.processed+' / '+data.total+' ('+pct+'%)';\n }else if(evtType==='info'){\n if(data.alreadyProcessed>0){\n ppStats.skipped=data.alreadyProcessed;\n updatePPStats();\n appendPPLogItem({step:'skipped',session:t('pp.info.skipped').replace('{n}',data.alreadyProcessed),index:'',total:''});\n }\n if(data.pending===0){\n appendPPLogItem({step:'done',session:t('pp.info.allDone'),index:'',total:''});\n }else{\n document.getElementById('ppPhaseLabel').textContent=t('pp.info.pending').replace('{n}',data.pending);\n }\n }else if(evtType==='item'){\n var label=data.session||'';\n if(label.length>40)label=label.slice(0,37)+'...';\n if(data.step==='processing'){\n var actionLabel=data.action==='skill-only'?t('pp.action.skillOnly'):t('pp.action.full');\n document.getElementById('ppPhaseLabel').textContent=t('pp.running')+' \u2014 '+actionLabel+' \u2014 '+label;\n }\n if(data.step==='done'){\n if(data.action==='skill-only'){\n ppStats.skills++;\n }else{\n ppStats.tasks++;\n }\n updatePPStats();\n }else if(data.step==='error'){\n ppStats.errors++;\n updatePPStats();\n }\n appendPPLogItem(data);\n }else if(evtType==='skill'){\n ppStats.skills++;\n updatePPStats();\n appendPPLogItem({step:'skill',title:data.title,index:'',total:''});\n }else if(evtType==='done'){\n ppDone(false);\n }else if(evtType==='stopped'){\n ppDone(true);\n }\n}\n\nfunction ppStop(){\n document.getElementById('ppStopBtn').disabled=true;\n document.getElementById('ppStopBtn').textContent=t('migrate.stopping');\n fetch('/api/migrate/postprocess/stop',{method:'POST'}).catch(function(){});\n}\n\nfunction ppDone(wasStopped,wasFailed,skipReload){\n window._ppRunning=false;\n document.getElementById('ppStopBtn').style.display='none';\n document.getElementById('ppStartBtn').style.display='inline-flex';\n document.getElementById('ppStartBtn').textContent=wasStopped?t('pp.resume'):t('pp.start');\n var doneEl=document.getElementById('ppDone');\n doneEl.style.display='block';\n if(wasFailed){\n doneEl.style.background='rgba(239,68,68,.06)';\n doneEl.style.color='#ef4444';\n doneEl.textContent=t('pp.failed')||'Processing failed \u2014 check error above';\n document.getElementById('ppBar').style.background='linear-gradient(90deg,#ef4444,#dc2626)';\n }else if(wasStopped){\n doneEl.style.background='rgba(245,158,11,.06)';\n doneEl.style.color='#f59e0b';\n doneEl.textContent=t('pp.stopped');\n document.getElementById('ppBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';\n }else{\n doneEl.style.background='rgba(34,197,94,.06)';\n doneEl.style.color='#22c55e';\n doneEl.textContent=t('pp.done')+' ('+t('pp.stat.tasks')+': '+ppStats.tasks+', '+t('pp.stat.skills')+': '+ppStats.skills+')';\n document.getElementById('ppBar').style.width='100%';\n document.getElementById('ppBar').style.background='linear-gradient(90deg,#22c55e,#16a34a)';\n }\n if(!skipReload) loadAll();\n}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\nfunction toast(msg,type='info'){\n const c=document.getElementById('toasts');\n const t=document.createElement('div');\n t.className='toast '+type;\n const icons={success:'\\u2705',error:'\\u274C',info:'\\u2139\\uFE0F'};\n t.innerHTML=(icons[type]||'')+' '+esc(msg);\n c.appendChild(t);\n setTimeout(()=>t.remove(),3500);\n}\n\n/* \u2500\u2500\u2500 Theme \u2500\u2500\u2500 */\nconst VIEWER_THEME_KEY='memos-viewer-theme';\nfunction initViewerTheme(){const s=localStorage.getItem(VIEWER_THEME_KEY);const theme=(s==='light'||s==='dark')?s:'dark';document.documentElement.setAttribute('data-theme',theme);}\nfunction toggleViewerTheme(){const el=document.documentElement;const cur=el.getAttribute('data-theme')||'dark';const next=cur==='dark'?'light':'dark';el.setAttribute('data-theme',next);localStorage.setItem(VIEWER_THEME_KEY,next);}\ninitViewerTheme();\n\n/* \u2500\u2500\u2500 Init \u2500\u2500\u2500 */\ndocument.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});\ndocument.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});\napplyI18n();\ncheckAuth();\n</script>\n\n<!-- Memory Detail Modal -->\n<div class=\"memory-modal-overlay\" id=\"memoryModal\" onclick=\"if(event.target===this)closeMemoryModal()\">\n <div class=\"memory-modal\">\n <div class=\"memory-modal-title\">\n <span>Memory Detail</span>\n <button class=\"btn btn-sm btn-ghost\" onclick=\"closeMemoryModal()\" style=\"font-size:16px;padding:2px 8px\">&times;</button>\n </div>\n <div class=\"memory-modal-body\" id=\"memoryModalBody\"></div>\n </div>\n</div>\n\n</body>\n</html>";
2
2
  //# sourceMappingURL=html.d.ts.map