Changeset 0.25.1 (#161)
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
}
|
||||
|
||||
.admin-surface-row:hover {
|
||||
background: var(--bg-tertiary, #1e1e22);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.admin-surface-info {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
.ch-model-pill-remove:hover {
|
||||
opacity: 1;
|
||||
color: var(--error-color, #d32f2f);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.ch-model-add-btn {
|
||||
@@ -151,7 +151,7 @@
|
||||
.mention-ac-handle {
|
||||
font-size: 0.72rem;
|
||||
color: var(--accent, #6c9fff);
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-family: var(--mono);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.mention-ac-model {
|
||||
|
||||
@@ -164,13 +164,6 @@
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
background: none; border: none; color: var(--text-3); cursor: pointer;
|
||||
padding: 4px; border-radius: 4px;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
.icon-btn:hover { color: var(--text); }
|
||||
|
||||
/* Messages — Bubble Layout (matching prototype) */
|
||||
.messages { flex: 1; overflow-y: auto; scroll-behavior: smooth; }
|
||||
|
||||
@@ -650,9 +643,9 @@
|
||||
|
||||
/* ── Chat type indicators (v0.23.0) ────────── */
|
||||
.chat-type-icon { font-size: 11px; opacity: 0.7; }
|
||||
.mono-input { font-family: var(--font-mono, 'JetBrains Mono', monospace) !important; font-size: 12px !important; }
|
||||
.mono-input { font-family: var(--mono) !important; font-size: 12px !important; }
|
||||
|
||||
/* Handle hints in dropdowns and pickers */
|
||||
.item-handle { font-size: 10px; color: var(--accent); font-family: var(--font-mono, monospace); margin-left: 6px; opacity: 0.75; }
|
||||
.group-persona-handle { font-size: 10px; color: var(--accent); font-family: var(--font-mono, monospace); margin-left: 4px; opacity: 0.75; }
|
||||
.item-handle { font-size: 10px; color: var(--accent); font-family: var(--mono); margin-left: 6px; opacity: 0.75; }
|
||||
.group-persona-handle { font-size: 10px; color: var(--accent); font-family: var(--mono); margin-left: 4px; opacity: 0.75; }
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
.editor-topbar-back:hover {
|
||||
color: var(--text, #eee);
|
||||
background: var(--bg-tertiary, #1e1e22);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.editor-topbar-sep {
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
.editor-ws-selector-btn:hover {
|
||||
border-color: var(--border, #2a2a2e);
|
||||
background: var(--bg-tertiary, #1e1e22);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.editor-ws-dropdown {
|
||||
@@ -128,7 +128,7 @@
|
||||
}
|
||||
|
||||
.editor-ws-dropdown-item:hover {
|
||||
background: var(--bg-tertiary, #1e1e22);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.editor-ws-dropdown-item.active {
|
||||
@@ -247,7 +247,7 @@
|
||||
|
||||
.editor-bootstrap-ws-item:hover {
|
||||
border-color: var(--accent, #b38a4e);
|
||||
background: var(--bg-tertiary, #1e1e22);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.editor-bootstrap-ws-name {
|
||||
@@ -307,7 +307,7 @@
|
||||
}
|
||||
|
||||
.surface-editor .file-tree-row:hover {
|
||||
background: var(--bg-tertiary, #1e1e22);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.surface-editor .file-tree-row.active {
|
||||
@@ -361,7 +361,7 @@
|
||||
}
|
||||
|
||||
.file-tree-ctx-item:hover {
|
||||
background: var(--bg-tertiary, #1e1e22);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* ── CodeEditor overrides ──────────────────── */
|
||||
@@ -399,7 +399,7 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.surface-editor .code-editor-tab:hover { background: var(--bg-tertiary, #1e1e22); }
|
||||
.surface-editor .code-editor-tab:hover { background: var(--bg-hover); }
|
||||
.surface-editor .code-editor-tab.active { color: var(--text, #eee); background: var(--bg, #0e0e10); }
|
||||
.surface-editor .code-editor-tab.modified .code-editor-tab-modified { color: var(--warning, #e5a842); }
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background: var(--bg-2);
|
||||
background: var(--bg-raised);
|
||||
border-radius: 8px;
|
||||
min-width: 80px;
|
||||
}
|
||||
@@ -93,7 +93,7 @@
|
||||
/* ── Memory Card ───────────────────────── */
|
||||
|
||||
.memory-card {
|
||||
background: var(--bg-2);
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
|
||||
@@ -110,7 +110,6 @@
|
||||
.provider-name { font-weight: 500; font-size: 13px; }
|
||||
.provider-meta { font-size: 11px; color: var(--text-3); margin-left: 8px; }
|
||||
.provider-actions { display: flex; align-items: center; gap: 8px; }
|
||||
.badge-global { font-size: 10px; color: var(--text-3); background: var(--bg-raised); padding: 2px 8px; border-radius: 4px; }
|
||||
|
||||
/* Admin panel (fullscreen) */
|
||||
.admin-panel {
|
||||
@@ -232,7 +231,6 @@
|
||||
.admin-persona-row .persona-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
|
||||
.admin-persona-row .btn-delete { background: none; border: none; color: var(--text-3); cursor: pointer; font-size: 16px; padding: 2px 6px; }
|
||||
.admin-persona-row .btn-delete:hover { color: var(--danger); }
|
||||
.badge-user { font-size: 10px; padding: 1px 6px; border-radius: 3px; background: var(--accent-dim); color: var(--accent); }
|
||||
|
||||
/* Stats cards */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
|
||||
@@ -247,12 +245,12 @@
|
||||
.audit-row { padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
|
||||
.audit-row:last-child { border-bottom: none; }
|
||||
.audit-main { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.audit-action { font-weight: 600; color: var(--accent); font-family: var(--font-mono, monospace); font-size: 12px; }
|
||||
.audit-action { font-weight: 600; color: var(--accent); font-family: var(--mono); font-size: 12px; }
|
||||
.audit-actor { color: var(--text); }
|
||||
.audit-resource { color: var(--text-3); font-family: var(--font-mono, monospace); font-size: 11px; }
|
||||
.audit-resource { color: var(--text-3); font-family: var(--mono); font-size: 11px; }
|
||||
.audit-meta { display: flex; gap: 10px; margin-top: 2px; font-size: 11px; color: var(--text-3); flex-wrap: wrap; }
|
||||
.audit-details { color: var(--text-2); }
|
||||
.audit-ip { font-family: var(--font-mono, monospace); }
|
||||
.audit-ip { font-family: var(--mono); }
|
||||
.pagination-row { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
||||
|
||||
/* Banner preview */
|
||||
@@ -285,11 +283,6 @@
|
||||
.model-list-item .model-provider { font-size: 12px; color: var(--text-3); }
|
||||
.model-list-item .model-caps-inline { display: flex; gap: 3px; }
|
||||
|
||||
.badge-admin { background: var(--accent); color: var(--text-on-color); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-user { background: var(--bg-raised); color: var(--text-3); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-inactive { background: var(--danger); color: var(--text-on-color); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-pending { background: var(--warning); color: var(--text-on-warning); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-team { background: var(--accent-dim); color: var(--accent-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; margin-left: 2px; }
|
||||
.admin-approve-form { padding: 10px 12px; margin: -4px 0 8px; background: var(--glass); border-radius: 0 0 8px 8px; border-top: 1px solid var(--border); }
|
||||
.admin-approve-form .checkbox-label { display: block; margin: 2px 0; }
|
||||
.btn-approve { background: var(--success-dim); color: var(--success-light); border: 1px solid rgba(34,197,94,0.3); border-radius: 4px; padding: 3px 10px; cursor: pointer; font-size: 12px; }
|
||||
@@ -301,22 +294,12 @@
|
||||
}
|
||||
.team-admin-back:hover { opacity: 1; }
|
||||
.team-tab-content { padding: 0; }
|
||||
.badge-private { background: var(--purple); color: var(--text-on-color); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-success { background: var(--success-dim); color: var(--success-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-warning { background: var(--warning-dim); color: var(--warning-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-danger { background: var(--danger-dim); color: var(--danger-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
|
||||
/* Health + Routing admin (v0.22.3) */
|
||||
.admin-health-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
|
||||
.admin-health-card { background: var(--bg-2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
|
||||
.list-card { background: var(--bg-2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px; }
|
||||
.admin-table { border-collapse: collapse; }
|
||||
.admin-table th, .admin-table td { padding: 6px 10px; text-align: left; border-bottom: 1px solid var(--border); }
|
||||
.admin-table th { font-weight: 600; color: var(--text-2); font-size: 11px; text-transform: uppercase; }
|
||||
.admin-health-card { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
|
||||
.list-card { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 8px; padding: 12px; margin-bottom: 8px; }
|
||||
.admin-user-row.user-inactive { opacity: 0.7; }
|
||||
.loading { color: var(--text-3); font-size: 13px; padding: 0.5rem; }
|
||||
.error-hint { color: var(--danger); font-size: 13px; padding: 0.5rem; }
|
||||
.empty-hint { color: var(--text-3); font-size: 13px; text-align: center; padding: 1rem; }
|
||||
|
||||
|
||||
/* ── Debug Modal ─────────────────────────── */
|
||||
@@ -455,7 +438,7 @@
|
||||
.group-persona-item:hover { background: var(--bg-hover); }
|
||||
.group-persona-item input[type="checkbox"] { accent-color: var(--accent); flex-shrink: 0; }
|
||||
.group-persona-name { flex: 1; color: var(--text); font-weight: 500; }
|
||||
.group-persona-model { font-size: 11px; color: var(--text-3); font-family: var(--font-mono); }
|
||||
.group-persona-model { font-size: 11px; color: var(--text-3); font-family: var(--mono); }
|
||||
.chain-typing-name { font-size: 11px; font-weight: 600; color: var(--accent); margin-right: 6px; }
|
||||
|
||||
/* ── Creation Dialog (project / channel / folder — v0.23.1) ── */
|
||||
@@ -486,11 +469,3 @@
|
||||
.sb-dialog-color-label { font-size: 12px; color: var(--text-2); }
|
||||
.sb-dialog-color-input { width: 32px; height: 26px; border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; padding: 1px; background: none; }
|
||||
.sb-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.sb-dialog-btn {
|
||||
padding: 6px 14px; border-radius: var(--radius); font-size: 12px; font-family: inherit;
|
||||
cursor: pointer; border: 1px solid var(--border);
|
||||
background: var(--bg-hover); color: var(--text-2);
|
||||
}
|
||||
.sb-dialog-btn:hover { background: var(--bg-raised); }
|
||||
.sb-dialog-btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
.sb-dialog-btn-primary:hover { opacity: 0.88; }
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
.notif-pref-table th {
|
||||
text-align: left;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 2px solid var(--border-color, #ddd);
|
||||
border-bottom: 2px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary, #888);
|
||||
color: var(--text-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
.notif-pref-table th:nth-child(2),
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
.notif-pref-table td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--border-color, #eee);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.notif-pref-toggle {
|
||||
text-align: center;
|
||||
@@ -52,10 +52,10 @@
|
||||
}
|
||||
.smtp-test-status {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary, #888);
|
||||
color: var(--text-2);
|
||||
}
|
||||
.smtp-test-status.success { color: var(--success-color, #2e7d32); }
|
||||
.smtp-test-status.error { color: var(--error-color, #d32f2f); }
|
||||
.smtp-test-status.success { color: var(--success); }
|
||||
.smtp-test-status.error { color: var(--danger); }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.smtp-config-grid {
|
||||
|
||||
@@ -314,10 +314,6 @@
|
||||
.notes-select-all input { cursor: pointer; }
|
||||
|
||||
.notes-list { padding: 8px; }
|
||||
.notes-empty, .notes-loading {
|
||||
text-align: center; color: var(--text-3); padding: 2rem;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
display: flex; align-items: flex-start; gap: 8px;
|
||||
@@ -475,7 +471,7 @@
|
||||
background: var(--bg-surface);
|
||||
}
|
||||
.transclusion-header {
|
||||
padding: 6px 10px; background: var(--bg-2);
|
||||
padding: 6px 10px; background: var(--bg-raised);
|
||||
border-bottom: 1px solid var(--border); font-size: 12px;
|
||||
}
|
||||
.transclusion-content {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
.persona-kb-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius, 6px);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -41,14 +41,6 @@ input[type="password"]:focus, textarea:focus {
|
||||
}
|
||||
select option { background: var(--bg-surface); color: var(--text); }
|
||||
|
||||
.form-group { margin-bottom: 0.75rem; }
|
||||
.form-group label { display: block; font-size: 13px; color: var(--text-2); margin-bottom: 4px; font-weight: 500; }
|
||||
.form-group input, .form-group select, .form-group textarea {
|
||||
width: 100%;
|
||||
}
|
||||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
||||
outline: none; border-color: var(--accent);
|
||||
}
|
||||
.form-row { display: flex; gap: 0.75rem; }
|
||||
.form-row .form-group { flex: 1; }
|
||||
.form-hint { font-weight: 400; color: var(--text-3); margin-left: 4px; }
|
||||
@@ -68,25 +60,6 @@ select option { background: var(--bg-surface); color: var(--text); }
|
||||
.range-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-3); }
|
||||
.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; margin: 0.5rem 0; color: var(--text-2); }
|
||||
|
||||
/* ── Theme Toggle ────────────────────────── */
|
||||
.theme-toggle {
|
||||
display: flex; gap: 4px; padding: 3px;
|
||||
background: var(--bg); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); width: fit-content;
|
||||
}
|
||||
.theme-btn {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 6px 14px; border-radius: calc(var(--radius) - 2px);
|
||||
border: none; background: none; color: var(--text-3);
|
||||
font-family: var(--font); font-size: 13px; font-weight: 500;
|
||||
cursor: pointer; transition: all var(--transition);
|
||||
}
|
||||
.theme-btn:hover { color: var(--text); background: var(--bg-hover); }
|
||||
.theme-btn.active {
|
||||
color: var(--text); background: var(--bg-raised);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
|
||||
/* ── Toast ────────────────────────────────── */
|
||||
|
||||
@@ -320,9 +293,6 @@ select option { background: var(--bg-surface); color: var(--text); }
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.kb-popup-meta { font-size: 11px; color: var(--text-3); }
|
||||
.kb-popup-empty, .kb-popup-loading {
|
||||
padding: 16px; text-align: center; font-size: 13px; color: var(--text-3);
|
||||
}
|
||||
|
||||
/* KB management panel (settings tab) */
|
||||
.kb-manage-header {
|
||||
@@ -335,7 +305,7 @@ select option { background: var(--bg-surface); color: var(--text); }
|
||||
.kb-manage-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 10px; border-radius: var(--radius);
|
||||
background: var(--bg-2); transition: background var(--transition);
|
||||
background: var(--bg-raised); transition: background var(--transition);
|
||||
}
|
||||
.kb-manage-item:hover { background: var(--bg-hover); }
|
||||
.kb-manage-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
@@ -344,9 +314,6 @@ select option { background: var(--bg-surface); color: var(--text); }
|
||||
.kb-manage-stats { font-size: 11px; color: var(--text-3); }
|
||||
.kb-manage-desc-block { font-size: 12px; color: var(--text-3); margin-bottom: 12px; }
|
||||
.kb-manage-actions { display: flex; gap: 4px; flex-shrink: 0; }
|
||||
.kb-manage-empty, .kb-manage-loading {
|
||||
padding: 20px; text-align: center; font-size: 13px; color: var(--text-3);
|
||||
}
|
||||
.kb-doc-item[data-doc-status="pending"] .kb-manage-name,
|
||||
.kb-doc-item[data-doc-status="extracting"] .kb-manage-name,
|
||||
.kb-doc-item[data-doc-status="chunking"] .kb-manage-name,
|
||||
@@ -363,20 +330,15 @@ select option { background: var(--bg-surface); color: var(--text); }
|
||||
color: var(--text-2); margin-bottom: 5px;
|
||||
}
|
||||
.form-group input, .form-group select, .form-group textarea {
|
||||
padding: 8px 12px; border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--input-bg, var(--bg)); color: var(--text);
|
||||
font-size: 13px; font-family: inherit;
|
||||
width: 100%; max-width: 360px; outline: none;
|
||||
transition: border-color 0.15s;
|
||||
width: 100%; outline: none;
|
||||
}
|
||||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.form-group input:disabled { opacity: 0.6; }
|
||||
.form-group input[type="range"] { max-width: 100%; border: none; background: none; }
|
||||
.form-group input[type="range"] { border: none; background: none; }
|
||||
.form-group input[type="range"]:focus { box-shadow: none; }
|
||||
.form-group input[type="checkbox"] { width: auto; max-width: none; accent-color: var(--accent); }
|
||||
.form-group input[type="checkbox"] { width: auto; accent-color: var(--accent); }
|
||||
|
||||
.toggle-group {
|
||||
display: flex; gap: 1px; padding: 3px;
|
||||
@@ -392,26 +354,32 @@ select option { background: var(--bg-surface); color: var(--text); }
|
||||
.toggle-btn.active { background: var(--bg-raised); color: var(--text); }
|
||||
.toggle-btn:hover { color: var(--text-2); }
|
||||
|
||||
.btn-primary { background: var(--accent); color: var(--text-on-color); border: none; }
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-danger { background: var(--danger); color: var(--text-on-color); border: none; }
|
||||
.btn-ghost { background: transparent; color: var(--text-2); border: 1px solid var(--border); }
|
||||
.btn-ghost:hover { background: var(--bg-hover); }
|
||||
.btn-subtle { background: var(--accent-dim); color: var(--accent); border: none; }
|
||||
.btn-sm { padding: 5px 11px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 500; font-family: inherit; }
|
||||
.btn-md { padding: 7px 15px; font-size: 13px; border-radius: 8px; cursor: pointer; font-weight: 500; font-family: inherit; }
|
||||
.btn-md { padding: 7px 15px; font-size: 13px; border-radius: var(--radius); cursor: pointer; font-weight: 500; font-family: inherit; }
|
||||
|
||||
/* ── Badge ───────────────────────────────── */
|
||||
.badge {
|
||||
font-size: 10px; padding: 2px 7px; border-radius: 4px;
|
||||
font-size: 10px; padding: 2px 8px; border-radius: 4px;
|
||||
font-weight: 600; letter-spacing: 0.3px; white-space: nowrap;
|
||||
display: inline-flex; align-items: center;
|
||||
}
|
||||
.badge-accent { background: var(--accent-dim); color: var(--accent); }
|
||||
.badge-success { background: var(--success-dim); color: var(--success); }
|
||||
.badge-warning { background: var(--warning-dim); color: var(--warning); }
|
||||
.badge-danger { background: var(--danger-dim); color: var(--danger); }
|
||||
.badge-muted { background: var(--bg-raised); color: var(--text-3); }
|
||||
|
||||
/* Dim variants (colored text on tinted background) */
|
||||
.badge-accent, .badge-models, .badge-team { background: var(--accent-dim); color: var(--accent-light); }
|
||||
.badge-success, .badge-active, .badge-healthy { background: var(--success-dim); color: var(--success-light); }
|
||||
.badge-warning, .badge-degraded { background: var(--warning-dim); color: var(--warning-light); }
|
||||
.badge-danger, .badge-disabled, .badge-unhealthy { background: var(--danger-dim); color: var(--danger-light); }
|
||||
.badge-muted, .badge-unknown, .badge-provider-type, .badge-global, .badge-user { background: var(--bg-raised); color: var(--text-3); }
|
||||
.badge-purple, .badge-service { background: var(--purple-dim); color: var(--purple); }
|
||||
|
||||
/* Solid variants (white text on colored background) */
|
||||
.badge-admin { background: var(--accent); color: var(--text-on-color); }
|
||||
.badge-inactive { background: var(--danger); color: var(--text-on-color); }
|
||||
.badge-pending { background: var(--warning); color: var(--text-on-warning); }
|
||||
.badge-private { background: var(--purple); color: var(--text-on-color); }
|
||||
|
||||
/* ── Status Dot ──────────────────────────── */
|
||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
@@ -429,4 +397,10 @@ select option { background: var(--bg-surface); color: var(--text); }
|
||||
}
|
||||
.icon-btn:hover { background: var(--bg-hover); color: var(--text-2); }
|
||||
.icon-btn.active { color: var(--accent); }
|
||||
.icon-btn-danger:hover { color: var(--danger); }
|
||||
|
||||
/* ── Utility States ─────────────────────── */
|
||||
.loading { color: var(--text-3); font-size: 13px; padding: 0.5rem; }
|
||||
.error-hint { color: var(--danger); font-size: 13px; padding: 0.5rem; }
|
||||
.empty-hint { color: var(--text-3); font-size: 13px; text-align: center; padding: 1rem; }
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
.project-panel-label { font-size: 12px; font-weight: 600; color: var(--text-2); text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.project-panel-textarea {
|
||||
width: 100%; resize: vertical; min-height: 80px; padding: 8px;
|
||||
font-family: var(--font-mono, monospace); font-size: 13px;
|
||||
font-family: var(--mono); font-size: 13px;
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
}
|
||||
@@ -429,7 +429,7 @@
|
||||
/* ── Bar Chart ───────────────────────────── */
|
||||
.bar-chart { display: flex; align-items: flex-end; gap: 8px; height: 140px; }
|
||||
.bar-chart-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||
.bar-chart-val { font-size: 10px; color: var(--text-3); font-family: var(--font-mono, monospace); }
|
||||
.bar-chart-val { font-size: 10px; color: var(--text-3); font-family: var(--mono); }
|
||||
.bar-chart-track { width: 100%; background: var(--accent-dim); border-radius: 4px; position: relative; height: 100px; }
|
||||
.bar-chart-fill { position: absolute; bottom: 0; left: 0; right: 0; background: var(--accent); border-radius: 4px; transition: height 0.3s ease; }
|
||||
.bar-chart-label { font-size: 10px; color: var(--text-3); }
|
||||
@@ -484,18 +484,8 @@
|
||||
}
|
||||
.admin-user-name { font-weight: 600; font-size: 13px; }
|
||||
.admin-user-handle { font-size: 11px; color: var(--text-3); margin-top: 1px; }
|
||||
.admin-user-time { color: var(--text-3); font-size: 12px; font-family: var(--font-mono, monospace); }
|
||||
.admin-user-time { color: var(--text-3); font-size: 12px; font-family: var(--mono); }
|
||||
.admin-actions-cell { white-space: nowrap; text-align: right; }
|
||||
.btn-icon {
|
||||
background: none; border: none; color: var(--text-3); cursor: pointer;
|
||||
padding: 4px; font-size: 14px; border-radius: 4px;
|
||||
transition: color var(--transition), background var(--transition);
|
||||
}
|
||||
.btn-icon:hover { color: var(--text); background: var(--bg-hover); }
|
||||
.btn-icon-danger:hover { color: var(--danger); }
|
||||
.badge-service { background: var(--purple-dim); color: var(--purple); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-disabled { background: var(--danger-dim); color: var(--danger-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
.badge-active { background: var(--success-dim); color: var(--success-light); font-size: 10px; padding: 1px 8px; border-radius: 4px; }
|
||||
|
||||
/* ── Provider Cards ──────────────────────── */
|
||||
.provider-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
|
||||
@@ -507,12 +497,6 @@
|
||||
.provider-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
||||
.provider-card-name { font-weight: 600; font-size: 14px; }
|
||||
.provider-card-meta { display: flex; gap: 6px; align-items: center; margin-top: 4px; flex-wrap: wrap; }
|
||||
.badge-healthy { background: var(--success-dim); color: var(--success-light); font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
||||
.badge-degraded { background: var(--warning-dim); color: var(--warning-light); font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
||||
.badge-unhealthy { background: var(--danger-dim); color: var(--danger-light); font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 500; }
|
||||
.badge-unknown { background: var(--bg-raised); color: var(--text-3); font-size: 10px; padding: 2px 8px; border-radius: 4px; }
|
||||
.badge-models { background: var(--accent-dim); color: var(--accent-light); font-size: 10px; padding: 2px 8px; border-radius: 4px; }
|
||||
.badge-provider-type { background: var(--bg-raised); color: var(--text-3); font-size: 10px; padding: 2px 8px; border-radius: 4px; }
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -775,3 +759,13 @@
|
||||
|
||||
/* Indented chat items inside folders */
|
||||
.chat-item.indented { padding-left: 28px; }
|
||||
|
||||
/* ── Notes Surface ─────────────────────── */
|
||||
.surface-notes { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
||||
.surface-notes-body { flex: 1; display: flex; min-height: 0; overflow: hidden; }
|
||||
.surface-notes-main { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
|
||||
.surface-notes-assist {
|
||||
width: 400px; border-left: 1px solid var(--border);
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
@media (max-width: 768px) { .surface-notes-assist { display: none; } }
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
|
||||
.tool-grants-item:hover {
|
||||
background: var(--bg-tertiary, #1e1e22);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.tool-grants-item input[type="checkbox"] {
|
||||
|
||||
@@ -474,13 +474,6 @@ function _initAdminListeners() {
|
||||
document.querySelectorAll('.admin-cat').forEach(btn => {
|
||||
btn.addEventListener('click', () => UI.switchAdminCategory(btn.dataset.cat));
|
||||
});
|
||||
// Esc closes admin panel
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape' && document.getElementById('adminPanel')?.style.display === 'flex') {
|
||||
UI.closeAdmin();
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
document.getElementById('adminSaveSettings')?.addEventListener('click', handleSaveAdminSettings);
|
||||
|
||||
// Admin — users
|
||||
|
||||
@@ -120,22 +120,22 @@ async function _loadSurfaceList() {
|
||||
row.innerHTML =
|
||||
'<div class="admin-surface-info">' +
|
||||
'<div class="admin-surface-title">' +
|
||||
'<span class="admin-surface-name">' + _surfEsc(s.title) + '</span>' +
|
||||
'<span class="admin-surface-name">' + esc(s.title) + '</span>' +
|
||||
'<span class="admin-surface-badge admin-surface-badge--' + s.source + '">' + sourceLabel + '</span>' +
|
||||
(isProtected ? '<span class="admin-surface-badge admin-surface-badge--required">Required</span>' : '') +
|
||||
'</div>' +
|
||||
'<div class="admin-surface-meta">' +
|
||||
'<span class="admin-surface-id">' + _surfEsc(s.id) + '</span>' +
|
||||
(route ? '<span class="admin-surface-route">' + _surfEsc(route) + '</span>' : '') +
|
||||
'<span class="admin-surface-id">' + esc(s.id) + '</span>' +
|
||||
(route ? '<span class="admin-surface-route">' + esc(route) + '</span>' : '') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="admin-surface-actions">' +
|
||||
'<label class="admin-surface-toggle' + (isProtected ? ' admin-surface-toggle--locked' : '') + '">' +
|
||||
'<input type="checkbox"' + (s.enabled ? ' checked' : '') + (isProtected ? ' disabled' : '') +
|
||||
' data-surface-id="' + _surfEsc(s.id) + '">' +
|
||||
' data-surface-id="' + esc(s.id) + '">' +
|
||||
'<span class="admin-surface-toggle-label">' + (s.enabled ? 'Enabled' : 'Disabled') + '</span>' +
|
||||
'</label>' +
|
||||
(s.source === 'extension' ? '<button class="btn-small btn-danger admin-surface-uninstall" data-surface-id="' + _surfEsc(s.id) + '">Uninstall</button>' : '') +
|
||||
(s.source === 'extension' ? '<button class="btn-small btn-danger admin-surface-uninstall" data-surface-id="' + esc(s.id) + '">Uninstall</button>' : '') +
|
||||
'</div>';
|
||||
|
||||
// Toggle handler
|
||||
@@ -187,12 +187,7 @@ async function _loadSurfaceList() {
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--danger);">Failed to load: ' + _surfEsc(e.message) + '</div>';
|
||||
listEl.innerHTML = '<div style="padding:12px;font-size:12px;color:var(--danger);">Failed to load: ' + esc(e.message) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function _surfEsc(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s || '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
// ChatPane.primary is the backward-compat bridge.
|
||||
|
||||
const ChatPane = {
|
||||
primary: null,
|
||||
_instances: new Map(),
|
||||
...createComponentRegistry('ChatPane'),
|
||||
|
||||
create(opts) {
|
||||
const id = opts.id || 'pane-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
||||
const instance = {
|
||||
const instance = componentMixin({
|
||||
id,
|
||||
messagesEl: opts.messagesEl,
|
||||
inputEl: opts.inputEl,
|
||||
@@ -22,7 +21,6 @@ const ChatPane = {
|
||||
standalone: opts.standalone || false,
|
||||
_cmView: null,
|
||||
_typing: null,
|
||||
_listeners: [],
|
||||
|
||||
renderMessages(messages) {
|
||||
if (!this.messagesEl) return;
|
||||
@@ -34,7 +32,7 @@ const ChatPane = {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'chat-msg chat-msg--' + m.role;
|
||||
div.innerHTML = typeof formatMessage === 'function'
|
||||
? formatMessage(m) : _cpEsc(m.content || '');
|
||||
? formatMessage(m) : esc(m.content || '');
|
||||
this.messagesEl.appendChild(div);
|
||||
});
|
||||
}
|
||||
@@ -127,28 +125,16 @@ const ChatPane = {
|
||||
else { const ta = this.inputEl && this.inputEl.querySelector('textarea'); if (ta) ta.focus(); }
|
||||
},
|
||||
|
||||
on(el, event, handler) {
|
||||
if (!el) return;
|
||||
el.addEventListener(event, handler);
|
||||
this._listeners.push({ el, event, handler });
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||
this._listeners = [];
|
||||
_cleanup() {
|
||||
if (this._cmView) { this._cmView.destroy(); this._cmView = null; }
|
||||
this.removeTyping();
|
||||
ChatPane._instances.delete(this.id);
|
||||
if (ChatPane.primary === this) ChatPane.primary = null;
|
||||
},
|
||||
};
|
||||
}, ChatPane);
|
||||
|
||||
ChatPane._instances.set(id, instance);
|
||||
ChatPane._register(id, instance);
|
||||
return instance;
|
||||
},
|
||||
|
||||
get(id) { return this._instances.get(id) || null; },
|
||||
|
||||
forChannel(channelId) {
|
||||
for (const [, inst] of this._instances) {
|
||||
if (inst.channelId === channelId) return inst;
|
||||
@@ -159,4 +145,3 @@ const ChatPane = {
|
||||
active() { return this.primary; },
|
||||
};
|
||||
|
||||
function _cpEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
// editor.destroy();
|
||||
|
||||
const CodeEditor = {
|
||||
_instances: new Map(),
|
||||
...createComponentRegistry('CodeEditor'),
|
||||
|
||||
create(opts) {
|
||||
const pfx = opts.id || 'editor';
|
||||
const instance = {
|
||||
const instance = componentMixin({
|
||||
id: pfx,
|
||||
tabsEl: document.getElementById(pfx + 'EditorTabs'),
|
||||
contentEl: document.getElementById(pfx + 'EditorContent'),
|
||||
@@ -35,9 +35,8 @@ const CodeEditor = {
|
||||
workspaceId: opts.workspaceId || null,
|
||||
onSave: opts.onSave || null,
|
||||
onActivate: opts.onActivate || null,
|
||||
_openFiles: new Map(), // path → { tab, editorWrap, editor, content, modified, language }
|
||||
_openFiles: new Map(),
|
||||
_activeFile: null,
|
||||
_listeners: [],
|
||||
|
||||
// ── Public API ──────────────────────
|
||||
|
||||
@@ -69,7 +68,7 @@ const CodeEditor = {
|
||||
const fileName = path.split('/').pop();
|
||||
tab.innerHTML =
|
||||
'<span class="code-editor-tab-icon">' + _ceFileIcon(fileName) + '</span>' +
|
||||
'<span class="code-editor-tab-name">' + _ceEsc(fileName) + '</span>' +
|
||||
'<span class="code-editor-tab-name">' + esc(fileName) + '</span>' +
|
||||
'<span class="code-editor-tab-modified" style="display:none">●</span>' +
|
||||
'<button class="code-editor-tab-close" title="Close">✕</button>';
|
||||
|
||||
@@ -235,10 +234,7 @@ const CodeEditor = {
|
||||
});
|
||||
},
|
||||
|
||||
// ── Lifecycle ───────────────────────
|
||||
|
||||
destroy() {
|
||||
// Destroy all CM6 instances
|
||||
_cleanup() {
|
||||
for (const [, f] of this._openFiles) {
|
||||
if (f.editor?.destroy) f.editor.destroy();
|
||||
f.tab.remove();
|
||||
@@ -246,9 +242,6 @@ const CodeEditor = {
|
||||
}
|
||||
this._openFiles.clear();
|
||||
this._activeFile = null;
|
||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||
this._listeners = [];
|
||||
CodeEditor._instances.delete(this.id);
|
||||
},
|
||||
|
||||
// ── Internal ────────────────────────
|
||||
@@ -288,23 +281,15 @@ const CodeEditor = {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
|
||||
_on(el, event, handler) {
|
||||
if (!el) return;
|
||||
el.addEventListener(event, handler);
|
||||
this._listeners.push({ el, event, handler });
|
||||
},
|
||||
};
|
||||
}, CodeEditor);
|
||||
|
||||
CodeEditor._instances.set(pfx, instance);
|
||||
CodeEditor._register(pfx, instance);
|
||||
return instance;
|
||||
},
|
||||
|
||||
get(id) { return this._instances.get(id) || null; },
|
||||
};
|
||||
|
||||
// ── Helpers ─────────────────────────────────
|
||||
|
||||
function _ceEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
function _ceFileIcon(name) {
|
||||
const ext = name.split('.').pop()?.toLowerCase();
|
||||
|
||||
@@ -603,7 +603,7 @@ function copyDebugLog() {
|
||||
const text = DebugLog.exportText();
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => { if (typeof showToast === 'function') showToast('📋 Debug log copied', 'success'); })
|
||||
.catch(() => { alert('Failed to copy'); });
|
||||
.catch(() => { UI.toast('Copy failed', 'error'); });
|
||||
}
|
||||
|
||||
function exportDebugLog() {
|
||||
|
||||
@@ -162,8 +162,8 @@
|
||||
const item = document.createElement('button');
|
||||
item.className = 'editor-bootstrap-ws-item';
|
||||
item.innerHTML =
|
||||
'<span class="editor-bootstrap-ws-name">' + _edEsc(ws.name || ws.id?.slice(0, 8)) + '</span>' +
|
||||
'<span class="editor-bootstrap-ws-date">' + _edEsc(ws.created_at ? new Date(ws.created_at).toLocaleDateString() : '') + '</span>';
|
||||
'<span class="editor-bootstrap-ws-name">' + esc(ws.name || ws.id?.slice(0, 8)) + '</span>' +
|
||||
'<span class="editor-bootstrap-ws-date">' + esc(ws.created_at ? new Date(ws.created_at).toLocaleDateString() : '') + '</span>';
|
||||
item.addEventListener('click', () => {
|
||||
window.location.href = (window.__BASE__ || '') + '/editor/' + ws.id;
|
||||
});
|
||||
@@ -336,7 +336,6 @@
|
||||
|
||||
// ── Helpers ──────────────────────────────
|
||||
|
||||
function _edEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
// ── Assist Chat (channel-based, standalone) ──
|
||||
// Full chat pane for the editor: model selector, chat history,
|
||||
@@ -616,7 +615,7 @@
|
||||
function _appendSystemMsg(text) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'chat-msg chat-msg--system';
|
||||
div.innerHTML = '<div class="chat-msg__content" style="color:var(--danger,#f44336);font-size:12px;">' + _edEsc(text) + '</div>';
|
||||
div.innerHTML = '<div class="chat-msg__content" style="color:var(--danger,#f44336);font-size:12px;">' + esc(text) + '</div>';
|
||||
chatPane.messagesEl?.appendChild(div);
|
||||
chatPane.scrollToBottom();
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
// tree.destroy();
|
||||
|
||||
const FileTree = {
|
||||
_instances: new Map(),
|
||||
...createComponentRegistry('FileTree'),
|
||||
|
||||
create(opts) {
|
||||
const pfx = opts.id || 'editor';
|
||||
const instance = {
|
||||
const instance = componentMixin({
|
||||
id: pfx,
|
||||
itemsEl: document.getElementById(pfx + 'TreeItems'),
|
||||
newFileEl: document.getElementById(pfx + 'TreeNewFile'),
|
||||
@@ -34,7 +34,6 @@ const FileTree = {
|
||||
_expandedDirs: new Set(['']),
|
||||
_gitStatusMap: new Map(),
|
||||
_activeFile: null,
|
||||
_listeners: [],
|
||||
|
||||
// ── Public API ──────────────────────
|
||||
|
||||
@@ -166,7 +165,7 @@ const FileTree = {
|
||||
row.innerHTML =
|
||||
'<span class="file-tree-arrow ' + (expanded ? 'expanded' : '') + '">' + (expanded ? '▾' : '▸') + '</span>' +
|
||||
'<span class="file-tree-icon">📁</span>' +
|
||||
'<span class="file-tree-name">' + _ftEsc(node.name) + '</span>';
|
||||
'<span class="file-tree-name">' + esc(node.name) + '</span>';
|
||||
row.addEventListener('click', () => {
|
||||
if (self._expandedDirs.has(node.path)) {
|
||||
self._expandedDirs.delete(node.path);
|
||||
@@ -182,7 +181,7 @@ const FileTree = {
|
||||
} else {
|
||||
row.innerHTML =
|
||||
'<span class="file-tree-icon">' + _ftFileIcon(node.name) + '</span>' +
|
||||
'<span class="file-tree-name">' + _ftEsc(node.name) + '</span>';
|
||||
'<span class="file-tree-name">' + esc(node.name) + '</span>';
|
||||
row.addEventListener('click', () => {
|
||||
if (self.onSelect) self.onSelect(node.path);
|
||||
});
|
||||
@@ -233,34 +232,20 @@ const FileTree = {
|
||||
});
|
||||
},
|
||||
|
||||
// ── Lifecycle ───────────────────────
|
||||
|
||||
destroy() {
|
||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||
this._listeners = [];
|
||||
_cleanup() {
|
||||
this._treeData = [];
|
||||
this._expandedDirs = new Set(['']);
|
||||
this._gitStatusMap.clear();
|
||||
FileTree._instances.delete(this.id);
|
||||
},
|
||||
}, FileTree);
|
||||
|
||||
_on(el, event, handler) {
|
||||
if (!el) return;
|
||||
el.addEventListener(event, handler);
|
||||
this._listeners.push({ el, event, handler });
|
||||
},
|
||||
};
|
||||
|
||||
FileTree._instances.set(pfx, instance);
|
||||
FileTree._register(pfx, instance);
|
||||
return instance;
|
||||
},
|
||||
|
||||
get(id) { return this._instances.get(id) || null; },
|
||||
};
|
||||
|
||||
// ── Helpers ─────────────────────────────────
|
||||
|
||||
function _ftEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
function _ftFileIcon(name) {
|
||||
const ext = name.split('.').pop()?.toLowerCase();
|
||||
|
||||
@@ -344,8 +344,8 @@ function _renderStrip() {
|
||||
return `<div class="att-chip ${statusClass}" data-local-id="${a.localId}">
|
||||
${a.previewUrl ? `<img class="att-thumb" src="${a.previewUrl}" alt="">` : `<span class="att-icon">${icon}</span>`}
|
||||
<span class="att-info">
|
||||
<span class="att-name" title="${_esc(a.filename)}">${_esc(name)}</span>
|
||||
<span class="att-meta">${_esc(size)}${label ? ' · ' + label : ''}</span>
|
||||
<span class="att-name" title="${esc(a.filename)}">${esc(name)}</span>
|
||||
<span class="att-meta">${esc(size)}${label ? ' · ' + label : ''}</span>
|
||||
</span>
|
||||
<button class="att-remove" onclick="unstageFile('${a.localId}')" title="Remove">✕</button>
|
||||
</div>`;
|
||||
@@ -363,12 +363,6 @@ function _truncFilename(name, max) {
|
||||
return name.slice(0, max - 1) + '…';
|
||||
}
|
||||
|
||||
function _esc(s) {
|
||||
if (!s) return '';
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
|
||||
// ── Channel File Loading ──────────────
|
||||
@@ -426,8 +420,8 @@ function renderMessageFiles(msgId) {
|
||||
const visionHint = !hasVision
|
||||
? '<span class="att-vision-hint" title="Current model does not support images">👁️🗨️</span>'
|
||||
: '';
|
||||
return `<div class="msg-att msg-att-image" onclick="openLightbox('${_esc(att.id)}')">
|
||||
<img class="msg-att-img" data-att-id="${_esc(att.id)}" alt="${_esc(att.filename)}"
|
||||
return `<div class="msg-att msg-att-image" onclick="openLightbox('${esc(att.id)}')">
|
||||
<img class="msg-att-img" data-att-id="${esc(att.id)}" alt="${esc(att.filename)}"
|
||||
style="max-width:${w}px" loading="lazy">
|
||||
${visionHint}
|
||||
</div>`;
|
||||
@@ -436,10 +430,10 @@ function renderMessageFiles(msgId) {
|
||||
// Document chip
|
||||
const icon = _docIcon(att.content_type);
|
||||
const size = formatFileSize(att.size_bytes);
|
||||
return `<a class="msg-att msg-att-doc" href="#" onclick="downloadFile('${_esc(att.id)}','${_esc(att.filename)}');return false">
|
||||
return `<a class="msg-att msg-att-doc" href="#" onclick="downloadFile('${esc(att.id)}','${esc(att.filename)}');return false">
|
||||
<span class="att-doc-icon">${icon}</span>
|
||||
<span class="att-doc-info">
|
||||
<span class="att-doc-name">${_esc(att.filename)}</span>
|
||||
<span class="att-doc-name">${esc(att.filename)}</span>
|
||||
<span class="att-doc-size">${size}</span>
|
||||
</span>
|
||||
<span class="att-doc-dl">⬇</span>
|
||||
@@ -565,7 +559,7 @@ async function loadAdminStorage() {
|
||||
<div class="admin-storage-grid">
|
||||
<div class="admin-storage-item">
|
||||
<span class="admin-storage-label">Backend</span>
|
||||
<span class="admin-storage-value">${_esc(status.backend || 'none')}</span>
|
||||
<span class="admin-storage-value">${esc(status.backend || 'none')}</span>
|
||||
</div>
|
||||
<div class="admin-storage-item">
|
||||
<span class="admin-storage-label">Status</span>
|
||||
@@ -581,15 +575,15 @@ async function loadAdminStorage() {
|
||||
</div>
|
||||
${status.path ? `<div class="admin-storage-item">
|
||||
<span class="admin-storage-label">Path</span>
|
||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${_esc(status.path)}</span>
|
||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${esc(status.path)}</span>
|
||||
</div>` : ''}
|
||||
${status.endpoint ? `<div class="admin-storage-item">
|
||||
<span class="admin-storage-label">Endpoint</span>
|
||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${_esc(status.endpoint)}</span>
|
||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${esc(status.endpoint)}</span>
|
||||
</div>` : ''}
|
||||
${status.bucket ? `<div class="admin-storage-item">
|
||||
<span class="admin-storage-label">Bucket</span>
|
||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${_esc(status.bucket)}</span>
|
||||
<span class="admin-storage-value" style="font-family:monospace;font-size:12px">${esc(status.bucket)}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -640,7 +634,7 @@ async function loadAdminStorage() {
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div class="empty-hint">Failed to load storage status: ${_esc(e.message)}</div>`;
|
||||
el.innerHTML = `<div class="empty-hint">Failed to load storage status: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,12 +29,12 @@ const KnowledgeUI = (() => {
|
||||
if (!popup) return;
|
||||
|
||||
if (!App.activeId) {
|
||||
popup.innerHTML = '<div class="kb-popup-empty">Start a conversation first</div>';
|
||||
popup.innerHTML = '<div class="empty-hint">Start a conversation first</div>';
|
||||
popup.classList.add('open');
|
||||
return;
|
||||
}
|
||||
|
||||
popup.innerHTML = '<div class="kb-popup-loading">Loading…</div>';
|
||||
popup.innerHTML = '<div class="loading">Loading…</div>';
|
||||
popup.classList.add('open');
|
||||
|
||||
try {
|
||||
@@ -46,7 +46,7 @@ const KnowledgeUI = (() => {
|
||||
_channelKBs = chResp.data || [];
|
||||
_renderChannelPopup(popup);
|
||||
} catch (e) {
|
||||
popup.innerHTML = '<div class="kb-popup-empty">Failed to load knowledge bases</div>';
|
||||
popup.innerHTML = '<div class="empty-hint">Failed to load knowledge bases</div>';
|
||||
console.warn('KB popup load failed:', e);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ const KnowledgeUI = (() => {
|
||||
function _renderChannelPopup(popup) {
|
||||
if (_allKBs.length === 0) {
|
||||
popup.innerHTML = `
|
||||
<div class="kb-popup-empty">
|
||||
<div class="empty-hint">
|
||||
No knowledge bases yet.
|
||||
<br><small>Create one in Settings → Knowledge Bases</small>
|
||||
</div>`;
|
||||
@@ -73,7 +73,7 @@ const KnowledgeUI = (() => {
|
||||
<div class="kb-popup-item ${linked ? 'enabled' : ''}" data-kb-id="${kb.id}">
|
||||
<span class="kb-popup-check"></span>
|
||||
<span class="kb-popup-label">
|
||||
<span class="kb-popup-name">${_esc(kb.name)}</span>
|
||||
<span class="kb-popup-name">${esc(kb.name)}</span>
|
||||
<span class="kb-popup-meta">${docs}${scope}${status}</span>
|
||||
</span>
|
||||
</div>`;
|
||||
@@ -130,7 +130,7 @@ const KnowledgeUI = (() => {
|
||||
}
|
||||
|
||||
_panelCtx = { container: containerId, scope, teamId };
|
||||
panel.innerHTML = '<div class="kb-manage-loading">Loading…</div>';
|
||||
panel.innerHTML = '<div class="loading">Loading…</div>';
|
||||
panel.style.display = '';
|
||||
|
||||
try {
|
||||
@@ -146,7 +146,7 @@ const KnowledgeUI = (() => {
|
||||
_renderManagePanel(panel);
|
||||
} catch (e) {
|
||||
console.warn('[KB] openPanel error:', e);
|
||||
panel.innerHTML = '<div class="kb-manage-empty">Failed to load knowledge bases</div>';
|
||||
panel.innerHTML = '<div class="empty-hint">Failed to load knowledge bases</div>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ const KnowledgeUI = (() => {
|
||||
</div>`;
|
||||
|
||||
if (_allKBs.length === 0) {
|
||||
html += '<div class="kb-manage-empty">No knowledge bases yet. Create one to get started.</div>';
|
||||
html += '<div class="empty-hint">No knowledge bases yet. Create one to get started.</div>';
|
||||
} else {
|
||||
// Summary stats
|
||||
const totalDocs = _allKBs.reduce((s, kb) => s + (kb.document_count || 0), 0);
|
||||
@@ -184,12 +184,12 @@ const KnowledgeUI = (() => {
|
||||
html += `
|
||||
<div class="kb-manage-item" data-kb-id="${kb.id}" data-grant-kb="${kb.id}">
|
||||
<div class="kb-manage-info">
|
||||
<span class="kb-manage-name">${scope} ${_esc(kb.name)}</span>
|
||||
<span class="kb-manage-desc">${_esc(kb.description || '')}</span>
|
||||
<span class="kb-manage-name">${scope} ${esc(kb.name)}</span>
|
||||
<span class="kb-manage-desc">${esc(kb.description || '')}</span>
|
||||
<span class="kb-manage-stats">${docs} · ${chunks}${size}${status}</span>
|
||||
</div>
|
||||
<div class="kb-manage-actions">
|
||||
${kb.scope !== 'personal' ? `<button class="btn-small" data-kb-grant="${kb.id}" data-kb-name="${_esc(kb.name)}" title="Access">🔒</button>` : ''}
|
||||
${kb.scope !== 'personal' ? `<button class="btn-small" data-kb-grant="${kb.id}" data-kb-name="${esc(kb.name)}" title="Access">🔒</button>` : ''}
|
||||
<button class="btn-small" data-kb-docs="${kb.id}" title="Documents">📄</button>
|
||||
<button class="btn-small btn-danger" data-kb-del="${kb.id}" title="Delete">✕</button>
|
||||
</div>
|
||||
@@ -273,7 +273,7 @@ const KnowledgeUI = (() => {
|
||||
close();
|
||||
_refreshCurrentPanel();
|
||||
} catch (e) {
|
||||
alert('Failed to create: ' + (e.message || e));
|
||||
UI.toast('Failed to create: ' + (e.message || e), 'error');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -292,12 +292,12 @@ const KnowledgeUI = (() => {
|
||||
|
||||
async function _deleteKB(kbId) {
|
||||
const kb = _allKBs.find(k => k.id === kbId);
|
||||
if (!confirm(`Delete "${kb?.name || kbId}" and all its documents?`)) return;
|
||||
if (!await showConfirm(`Delete "${kb?.name || kbId}" and all its documents?`)) return;
|
||||
try {
|
||||
await API.deleteKnowledgeBase(kbId);
|
||||
_refreshCurrentPanel();
|
||||
} catch (e) {
|
||||
alert('Delete failed: ' + (e.message || e));
|
||||
UI.toast('Delete failed: ' + (e.message || e), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ const KnowledgeUI = (() => {
|
||||
const panel = document.getElementById(containerId);
|
||||
if (!panel) return;
|
||||
|
||||
panel.innerHTML = '<div class="kb-manage-loading">Loading documents…</div>';
|
||||
panel.innerHTML = '<div class="loading">Loading documents…</div>';
|
||||
|
||||
try {
|
||||
const [kbResp, docsResp] = await Promise.all([
|
||||
@@ -333,7 +333,7 @@ const KnowledgeUI = (() => {
|
||||
const docs = docsResp.data || docsResp || [];
|
||||
_renderDocumentsPanel(panel, kb, Array.isArray(docs) ? docs : []);
|
||||
} catch (e) {
|
||||
panel.innerHTML = `<div class="kb-manage-empty">Failed to load documents: ${_esc(e.message || '')}</div>`;
|
||||
panel.innerHTML = `<div class="empty-hint">Failed to load documents: ${esc(e.message || '')}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ const KnowledgeUI = (() => {
|
||||
let html = `
|
||||
<div class="kb-manage-header">
|
||||
<button class="btn-small" id="kbDocsBack">← Back</button>
|
||||
<h3>${_esc(kb.name)}</h3>
|
||||
<h3>${esc(kb.name)}</h3>
|
||||
<label class="btn-small btn-primary" id="kbUploadLabel" title="Upload document">
|
||||
+ Upload
|
||||
<input type="file" id="kbFileInput" accept=".txt,.md,.csv,.html,.htm" style="display:none" multiple>
|
||||
@@ -349,7 +349,7 @@ const KnowledgeUI = (() => {
|
||||
</div>`;
|
||||
|
||||
if (kb.description) {
|
||||
html += `<div class="kb-manage-desc-block">${_esc(kb.description)}</div>`;
|
||||
html += `<div class="kb-manage-desc-block">${esc(kb.description)}</div>`;
|
||||
}
|
||||
|
||||
// Stats line
|
||||
@@ -360,18 +360,18 @@ const KnowledgeUI = (() => {
|
||||
}
|
||||
|
||||
if (docs.length === 0) {
|
||||
html += '<div class="kb-manage-empty">No documents yet. Upload text, markdown, CSV, or HTML files.</div>';
|
||||
html += '<div class="empty-hint">No documents yet. Upload text, markdown, CSV, or HTML files.</div>';
|
||||
} else {
|
||||
html += '<div class="kb-manage-list">';
|
||||
for (const doc of docs) {
|
||||
const statusIcon = _docStatusIcon(doc.status);
|
||||
const size = _formatSize(doc.size_bytes);
|
||||
const chunks = doc.chunk_count > 0 ? ` · ${doc.chunk_count} chunks` : '';
|
||||
const errText = doc.error ? ` · ⚠ ${_esc(doc.error)}` : '';
|
||||
const errText = doc.error ? ` · ⚠ ${esc(doc.error)}` : '';
|
||||
html += `
|
||||
<div class="kb-manage-item kb-doc-item" data-doc-id="${doc.id}" data-doc-status="${doc.status}">
|
||||
<div class="kb-manage-info">
|
||||
<span class="kb-manage-name">${statusIcon} ${_esc(doc.filename)}</span>
|
||||
<span class="kb-manage-name">${statusIcon} ${esc(doc.filename)}</span>
|
||||
<span class="kb-manage-stats">${size}${chunks}${errText}</span>
|
||||
</div>
|
||||
<div class="kb-manage-actions">
|
||||
@@ -417,18 +417,18 @@ const KnowledgeUI = (() => {
|
||||
console.log(`📄 Uploaded ${file.name}, status: ${resp.status}`);
|
||||
if (resp.id) _startPolling(kbId, resp.id);
|
||||
} catch (e) {
|
||||
alert(`Upload failed for ${file.name}: ${e.message || e}`);
|
||||
UI.toast(`Upload failed for ${file.name}: ${e.message || e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function _deleteDocument(kbId, docId) {
|
||||
if (!confirm('Delete this document and all its chunks?')) return;
|
||||
if (!await showConfirm('Delete this document and all its chunks?')) return;
|
||||
try {
|
||||
_stopPolling(docId);
|
||||
await API.deleteKBDocument(kbId, docId);
|
||||
openDocumentsPanel(kbId);
|
||||
} catch (e) {
|
||||
alert('Delete failed: ' + (e.message || e));
|
||||
UI.toast('Delete failed: ' + (e.message || e), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ const KnowledgeUI = (() => {
|
||||
if (nameEl) {
|
||||
const icon = _docStatusIcon(status.status);
|
||||
const filename = nameEl.textContent.replace(/^[^\s]+ /, '');
|
||||
nameEl.innerHTML = `${icon} ${_esc(status.filename || filename)}`;
|
||||
nameEl.innerHTML = `${icon} ${esc(status.filename || filename)}`;
|
||||
}
|
||||
const statsEl = item.querySelector('.kb-manage-stats');
|
||||
if (statsEl && status.chunk_count > 0) {
|
||||
@@ -538,12 +538,6 @@ const KnowledgeUI = (() => {
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function _esc(s) {
|
||||
if (!s) return '';
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════
|
||||
// Public API
|
||||
|
||||
@@ -160,7 +160,7 @@ const MemoryUI = {
|
||||
},
|
||||
|
||||
async deleteMemory(id) {
|
||||
if (!confirm('Delete this memory permanently?')) return;
|
||||
if (!await showConfirm('Delete this memory permanently?')) return;
|
||||
try {
|
||||
await API.deleteMemory(id);
|
||||
UI.toast('Memory deleted', 'success');
|
||||
@@ -213,7 +213,7 @@ const MemoryUI = {
|
||||
const data = await API.listMemories('pending_review', '');
|
||||
const ids = (data.memories || []).map(m => m.id);
|
||||
if (!ids.length) return UI.toast('No pending memories', 'info');
|
||||
if (!confirm(`Approve all ${ids.length} pending memories?`)) return;
|
||||
if (!await showConfirm(`Approve all ${ids.length} pending memories?`)) return;
|
||||
await API.bulkApproveMemories(ids);
|
||||
UI.toast(`${ids.length} memories approved`, 'success');
|
||||
this.openSettingsPanel();
|
||||
@@ -250,7 +250,7 @@ const MemoryUI = {
|
||||
// Wire events
|
||||
document.getElementById('adminBulkApproveBtn')?.addEventListener('click', async () => {
|
||||
const ids = pending.map(m => m.id);
|
||||
if (!confirm(`Approve all ${ids.length} pending memories?`)) return;
|
||||
if (!await showConfirm(`Approve all ${ids.length} pending memories?`)) return;
|
||||
try {
|
||||
await API.bulkApproveMemories(ids);
|
||||
UI.toast(`${ids.length} memories approved`, 'success');
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
// sel.destroy();
|
||||
|
||||
const ModelSelector = {
|
||||
primary: null,
|
||||
_instances: new Map(),
|
||||
...createComponentRegistry('ModelSelector'),
|
||||
|
||||
create(opts) {
|
||||
const pfx = opts.id || '';
|
||||
const instance = {
|
||||
const instance = componentMixin({
|
||||
id: pfx,
|
||||
dropdownEl: document.getElementById(pfx + 'modelDropdown'),
|
||||
btnEl: document.getElementById(pfx + 'modelDropdownBtn'),
|
||||
@@ -28,7 +27,6 @@ const ModelSelector = {
|
||||
onChange: opts.onChange || null,
|
||||
_value: '',
|
||||
_models: [],
|
||||
_listeners: [],
|
||||
|
||||
// ── Selection ───────────────────────
|
||||
|
||||
@@ -100,11 +98,11 @@ const ModelSelector = {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'model-dropdown-item';
|
||||
div.dataset.value = m.id;
|
||||
const avatar = m.personaAvatar ? '<img src="' + _msEsc(m.personaAvatar) + '" class="dropdown-avatar" alt="">' : '';
|
||||
const handle = m.isPersona && m.personaHandle ? '<span class="item-handle">@' + _msEsc(m.personaHandle) + '</span>' : '';
|
||||
const teamBadge = m.source === 'team' && m.teamName ? '<span class="badge-team" style="font-size:9px;padding:0 4px">\uD83D\uDC65 ' + _msEsc(m.teamName) + '</span>' : '';
|
||||
const provider = m.provider ? '<span class="item-provider">' + _msEsc(m.provider) + '</span>' : '';
|
||||
div.innerHTML = avatar + '<span class="item-label">' + _msEsc(m.name || m.id) + handle + '</span>' + teamBadge + provider;
|
||||
const avatar = m.personaAvatar ? '<img src="' + esc(m.personaAvatar) + '" class="dropdown-avatar" alt="">' : '';
|
||||
const handle = m.isPersona && m.personaHandle ? '<span class="item-handle">@' + esc(m.personaHandle) + '</span>' : '';
|
||||
const teamBadge = m.source === 'team' && m.teamName ? '<span class="badge-team" style="font-size:9px;padding:0 4px">\uD83D\uDC65 ' + esc(m.teamName) + '</span>' : '';
|
||||
const provider = m.provider ? '<span class="item-provider">' + esc(m.provider) + '</span>' : '';
|
||||
div.innerHTML = avatar + '<span class="item-label">' + esc(m.name || m.id) + handle + '</span>' + teamBadge + provider;
|
||||
div.addEventListener('click', () => {
|
||||
self.select(m.id, (m.name || m.id) + (m.provider ? ' (' + m.provider + ')' : ''));
|
||||
self.close();
|
||||
@@ -163,29 +161,9 @@ const ModelSelector = {
|
||||
if (!e.target.closest('#' + pfx + 'modelDropdown')) this.close();
|
||||
});
|
||||
},
|
||||
}, ModelSelector);
|
||||
|
||||
// ── Lifecycle ───────────────────────
|
||||
|
||||
destroy() {
|
||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||
this._listeners = [];
|
||||
ModelSelector._instances.delete(this.id);
|
||||
if (ModelSelector.primary === this) ModelSelector.primary = null;
|
||||
},
|
||||
|
||||
_on(el, event, handler) {
|
||||
if (!el) return;
|
||||
el.addEventListener(event, handler);
|
||||
this._listeners.push({ el, event, handler });
|
||||
},
|
||||
};
|
||||
|
||||
ModelSelector._instances.set(pfx, instance);
|
||||
ModelSelector._register(pfx, instance);
|
||||
return instance;
|
||||
},
|
||||
|
||||
get(id) { return this._instances.get(id) || null; },
|
||||
};
|
||||
|
||||
// HTML-escape for model selector content
|
||||
function _msEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
// (editor pane, future notes-studio layout).
|
||||
|
||||
const NoteEditor = {
|
||||
_instances: new Map(),
|
||||
...createComponentRegistry('NoteEditor'),
|
||||
|
||||
create(opts) {
|
||||
const pfx = opts.id || 'main';
|
||||
const instance = {
|
||||
const instance = componentMixin({
|
||||
id: pfx,
|
||||
rootEl: document.getElementById(pfx + 'NoteEditor'),
|
||||
listViewEl: document.getElementById(pfx + 'NotesListView'),
|
||||
@@ -51,7 +51,6 @@ const NoteEditor = {
|
||||
_currentNote: null,
|
||||
_sort: 'updated_desc',
|
||||
_cmEditor: null,
|
||||
_listeners: [],
|
||||
|
||||
// ── View Switching ───────────────────
|
||||
|
||||
@@ -78,7 +77,7 @@ const NoteEditor = {
|
||||
|
||||
async loadNotes(folder, searchQuery) {
|
||||
if (!this.listEl) return;
|
||||
this.listEl.innerHTML = '<div class="notes-loading">Loading…</div>';
|
||||
this.listEl.innerHTML = '<div class="loading">Loading…</div>';
|
||||
|
||||
try {
|
||||
let notes;
|
||||
@@ -92,7 +91,7 @@ const NoteEditor = {
|
||||
}
|
||||
|
||||
if (notes.length === 0) {
|
||||
this.listEl.innerHTML = '<div class="notes-empty">' +
|
||||
this.listEl.innerHTML = '<div class="empty-hint">' +
|
||||
(searchQuery ? 'No results found' : 'No notes yet') + '</div>';
|
||||
return;
|
||||
}
|
||||
@@ -108,15 +107,15 @@ const NoteEditor = {
|
||||
const date = note.updated_at ? new Date(note.updated_at).toLocaleDateString() : '';
|
||||
item.innerHTML =
|
||||
'<div class="note-content-col">' +
|
||||
'<div class="note-title">' + _neEsc(title) + '</div>' +
|
||||
'<div class="note-preview">' + _neEsc(preview) + '</div>' +
|
||||
'<div class="note-date">' + _neEsc(date) + '</div>' +
|
||||
'<div class="note-title">' + esc(title) + '</div>' +
|
||||
'<div class="note-preview">' + esc(preview) + '</div>' +
|
||||
'<div class="note-date">' + esc(date) + '</div>' +
|
||||
'</div>';
|
||||
item.addEventListener('click', () => self.openNote(note.id));
|
||||
self.listEl.appendChild(item);
|
||||
});
|
||||
} catch (e) {
|
||||
this.listEl.innerHTML = '<div class="notes-empty">Failed to load: ' + _neEsc(e.message) + '</div>';
|
||||
this.listEl.innerHTML = '<div class="empty-hint">Failed to load: ' + esc(e.message) + '</div>';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -422,29 +421,15 @@ const NoteEditor = {
|
||||
}
|
||||
},
|
||||
|
||||
// ── Lifecycle ───────────────────────
|
||||
|
||||
destroy() {
|
||||
_cleanup() {
|
||||
this._destroyCmEditor();
|
||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||
this._listeners = [];
|
||||
this._editingNoteId = null;
|
||||
this._currentNote = null;
|
||||
NoteEditor._instances.delete(this.id);
|
||||
},
|
||||
}, NoteEditor);
|
||||
|
||||
_on(el, event, handler) {
|
||||
if (!el) return;
|
||||
el.addEventListener(event, handler);
|
||||
this._listeners.push({ el, event, handler });
|
||||
},
|
||||
};
|
||||
|
||||
NoteEditor._instances.set(pfx, instance);
|
||||
NoteEditor._register(pfx, instance);
|
||||
return instance;
|
||||
},
|
||||
|
||||
get(id) { return this._instances.get(id) || null; },
|
||||
};
|
||||
|
||||
function _neEsc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
@@ -25,7 +25,7 @@ async function openNotes() {
|
||||
|
||||
async function loadNotesList(folder, searchQuery) {
|
||||
const list = document.getElementById('notesList');
|
||||
list.innerHTML = '<div class="notes-loading">Loading…</div>';
|
||||
list.innerHTML = '<div class="loading">Loading…</div>';
|
||||
|
||||
try {
|
||||
let data;
|
||||
@@ -33,7 +33,7 @@ async function loadNotesList(folder, searchQuery) {
|
||||
data = await API.searchNotes(searchQuery);
|
||||
const results = data.data || [];
|
||||
if (results.length === 0) {
|
||||
list.innerHTML = '<div class="notes-empty">No results found</div>';
|
||||
list.innerHTML = '<div class="empty-hint">No results found</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = results.map(n => _noteListItem(n, true)).join('');
|
||||
@@ -42,13 +42,13 @@ async function loadNotesList(folder, searchQuery) {
|
||||
data = await API.listNotes(100, 0, folderVal, '', _notesSort);
|
||||
const notes = data.data || [];
|
||||
if (notes.length === 0) {
|
||||
list.innerHTML = `<div class="notes-empty">${folderVal ? 'No notes in this folder' : 'No notes yet. Create one or ask the AI to save a note.'}</div>`;
|
||||
list.innerHTML = `<div class="empty-hint">${folderVal ? 'No notes in this folder' : 'No notes yet. Create one or ask the AI to save a note.'}</div>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = notes.map(n => _noteListItem(n, false)).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
list.innerHTML = `<div class="notes-empty">Failed to load: ${esc(e.message)}</div>`;
|
||||
list.innerHTML = `<div class="empty-hint">Failed to load: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ function showNotesList() {
|
||||
_destroyNoteEditor();
|
||||
// Reset list to loading state to prevent stale content flash
|
||||
const list = document.getElementById('notesList');
|
||||
if (list) list.innerHTML = '<div class="notes-loading">Loading…</div>';
|
||||
if (list) list.innerHTML = '<div class="loading">Loading…</div>';
|
||||
}
|
||||
|
||||
var _currentNote = null; // cached note for read mode
|
||||
|
||||
@@ -126,8 +126,8 @@ const Notifications = {
|
||||
html += `<div class="${cls}" data-id="${n.id}" data-resource-type="${n.resource_type || ''}" data-resource-id="${n.resource_id || ''}" onclick="Notifications._onItemClick(this)">
|
||||
<span class="notif-dot">${dot}</span>
|
||||
<div class="notif-body">
|
||||
<div class="notif-title">${_esc(n.title)}</div>
|
||||
${n.body ? `<div class="notif-detail">${_esc(n.body)}</div>` : ''}
|
||||
<div class="notif-title">${esc(n.title)}</div>
|
||||
${n.body ? `<div class="notif-detail">${esc(n.body)}</div>` : ''}
|
||||
<div class="notif-time">${ago}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -277,8 +277,8 @@ const Notifications = {
|
||||
<div class="notif-panel-row">
|
||||
<span class="notif-dot">${dot}</span>
|
||||
<div class="notif-body">
|
||||
<div class="notif-title">${_esc(n.title)}</div>
|
||||
${n.body ? `<div class="notif-detail">${_esc(n.body)}</div>` : ''}
|
||||
<div class="notif-title">${esc(n.title)}</div>
|
||||
${n.body ? `<div class="notif-detail">${esc(n.body)}</div>` : ''}
|
||||
<div class="notif-time">${ago}</div>
|
||||
</div>
|
||||
<button class="notif-delete" onclick="event.stopPropagation(); Notifications._deleteItem('${n.id}')" title="Delete">✕</button>
|
||||
@@ -411,12 +411,6 @@ const Notifications = {
|
||||
|
||||
// ── Helpers ──────────────────────────────────
|
||||
|
||||
function _esc(s) {
|
||||
if (!s) return '';
|
||||
const el = document.createElement('span');
|
||||
el.textContent = s;
|
||||
return el.innerHTML;
|
||||
}
|
||||
|
||||
function _timeAgo(isoStr) {
|
||||
if (!isoStr) return '';
|
||||
|
||||
@@ -29,7 +29,7 @@ const Pages = {
|
||||
);
|
||||
modelSelect.innerHTML = '<option value="">— select model —</option>' +
|
||||
filtered.map(m =>
|
||||
`<option value="${m.model_id}">${_esc(m.display_name || m.model_id)}</option>`
|
||||
`<option value="${m.model_id}">${esc(m.display_name || m.model_id)}</option>`
|
||||
).join('');
|
||||
},
|
||||
|
||||
@@ -302,11 +302,6 @@ function _val(id, setVal) {
|
||||
function _show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; }
|
||||
function _hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
||||
|
||||
function _esc(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function _toast(msg, type) {
|
||||
if (typeof UI !== 'undefined' && UI.toast) {
|
||||
|
||||
@@ -271,7 +271,7 @@ const PanelRegistry = {
|
||||
const panel = this._panels[this._active];
|
||||
slot.innerHTML = (panel.actions || [])
|
||||
.filter(a => !a.visible || a.visible())
|
||||
.map(a => `<button class="side-panel-action-btn" id="${a.id || ''}" onclick="${a.onClickName || ''}" title="${_escPanel(a.title || '')}">${a.icon || ''}</button>`)
|
||||
.map(a => `<button class="side-panel-action-btn" id="${a.id || ''}" onclick="${a.onClickName || ''}" title="${esc(a.title || '')}">${a.icon || ''}</button>`)
|
||||
.join('');
|
||||
},
|
||||
|
||||
@@ -282,10 +282,6 @@ const PanelRegistry = {
|
||||
};
|
||||
|
||||
/** Minimal HTML escaping for panel labels/titles. */
|
||||
function _escPanel(s) {
|
||||
if (!s) return '';
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ── Secondary Pane Fullscreen ──────────────
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ function _showCreationDialog({ title, placeholder, withColor = false, defaultCol
|
||||
<input id="sbDialogColor" type="color" value="${defaultColor}" class="sb-dialog-color-input">
|
||||
</div>` : ''}
|
||||
<div class="sb-dialog-actions">
|
||||
<button id="sbDialogCancel" class="sb-dialog-btn">Cancel</button>
|
||||
<button id="sbDialogOK" class="sb-dialog-btn sb-dialog-btn-primary">${btnLabel}</button>
|
||||
<button id="sbDialogCancel" class="btn-small">Cancel</button>
|
||||
<button id="sbDialogOK" class="btn-small btn-primary">${btnLabel}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -142,7 +142,7 @@ async function renameProject(projectId) {
|
||||
async function deleteProject(projectId) {
|
||||
const proj = App.projects.find(p => p.id === projectId);
|
||||
if (!proj) return;
|
||||
if (!confirm(`Delete project "${proj.name}"?\nChats inside will be kept but unassigned.`)) return;
|
||||
if (!await showConfirm(`Delete project "${proj.name}"?\nChats inside will be kept but unassigned.`)) return;
|
||||
try {
|
||||
await API.deleteProject(projectId);
|
||||
App.projects = App.projects.filter(p => p.id !== projectId);
|
||||
|
||||
@@ -465,13 +465,17 @@ function _renderCmdResults(query) {
|
||||
// ── Settings Listeners (extracted from initListeners) ──
|
||||
|
||||
function _initSettingsListeners() {
|
||||
// Settings modal
|
||||
document.getElementById('settingsCloseBtn').addEventListener('click', UI.closeSettings);
|
||||
document.getElementById('settingsSaveBtn').addEventListener('click', handleSaveSettings);
|
||||
// Settings modal elements only exist on the settings surface.
|
||||
// On chat surface, openSettings() navigates to /settings/ instead.
|
||||
const settingsModel = document.getElementById('settingsModel');
|
||||
if (!settingsModel) return;
|
||||
|
||||
document.getElementById('settingsCloseBtn')?.addEventListener('click', UI.closeSettings);
|
||||
document.getElementById('settingsSaveBtn')?.addEventListener('click', handleSaveSettings);
|
||||
document.querySelectorAll('.settings-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => UI.switchSettingsTab(tab.dataset.stab));
|
||||
});
|
||||
document.getElementById('settingsModel').addEventListener('change', function() {
|
||||
settingsModel.addEventListener('change', function() {
|
||||
const model = App.findModel(this.value);
|
||||
const caps = model?.capabilities || {};
|
||||
const hint = document.getElementById('settingsMaxHint');
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* surface-nav.js — Navigation overrides for server-rendered surfaces.
|
||||
*
|
||||
* Replaces SPA modal-based openSettings/openAdmin with page navigation
|
||||
* to the dedicated /settings and /admin surface routes.
|
||||
*
|
||||
* Load AFTER ui-core.js and ui-admin.js so it overrides their methods.
|
||||
*
|
||||
* v0.22.8 — FE refactor: surfaces replace modals
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const base = window.__BASE__ || '';
|
||||
|
||||
// ── Settings: navigate to /settings surface ──────────
|
||||
if (typeof UI !== 'undefined') {
|
||||
UI.openSettings = function(section) {
|
||||
const sec = section || 'general';
|
||||
window.location.href = base + '/settings/' + sec;
|
||||
};
|
||||
UI.closeSettings = function() {
|
||||
// Navigate back to chat if called from settings surface
|
||||
window.location.href = base + '/';
|
||||
};
|
||||
}
|
||||
|
||||
// ── Admin: navigate to /admin surface ────────────────
|
||||
if (typeof UI !== 'undefined') {
|
||||
UI.openAdmin = function(cat, section) {
|
||||
const sec = section || 'users';
|
||||
window.location.href = base + '/admin/' + sec;
|
||||
};
|
||||
UI.closeAdmin = function() {
|
||||
window.location.href = base + '/';
|
||||
};
|
||||
UI.openAdminSection = function(section) {
|
||||
window.location.href = base + '/admin/' + (section || 'users');
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[surface-nav] Navigation overrides active — settings/admin use server surfaces');
|
||||
})();
|
||||
@@ -66,26 +66,19 @@ Object.assign(UI, {
|
||||
|
||||
// ── Open / Close ──────────────────────────
|
||||
openAdmin(cat, section) {
|
||||
UI._adminCat = cat || 'people';
|
||||
UI._adminSection = section || ADMIN_SECTIONS[UI._adminCat][0];
|
||||
const panel = document.getElementById('adminPanel');
|
||||
panel.style.display = 'flex';
|
||||
// Show current user email
|
||||
const userEl = document.getElementById('adminCurrentUser');
|
||||
if (userEl && API.user) userEl.textContent = API.user.email || API.user.username || '';
|
||||
UI._renderAdminNav();
|
||||
UI._switchAdminSection(UI._adminSection);
|
||||
const base = window.__BASE__ || '';
|
||||
window.location.href = base + '/admin/' + (section || 'users');
|
||||
},
|
||||
|
||||
closeAdmin() {
|
||||
document.getElementById('adminPanel').style.display = 'none';
|
||||
const base = window.__BASE__ || '';
|
||||
window.location.href = base + '/';
|
||||
},
|
||||
|
||||
// ── Navigate to specific section ──────────
|
||||
// Called from command palette, deep links, etc.
|
||||
openAdminSection(section) {
|
||||
const cat = _adminCatForSection(section);
|
||||
UI.openAdmin(cat, section);
|
||||
const base = window.__BASE__ || '';
|
||||
window.location.href = base + '/admin/' + (section || 'users');
|
||||
},
|
||||
|
||||
// ── Category switch ───────────────────────
|
||||
@@ -128,21 +121,6 @@ Object.assign(UI, {
|
||||
await ADMIN_LOADERS[section]?.();
|
||||
},
|
||||
|
||||
// ── Backward compat: switchAdminTab still works ──
|
||||
// (called by some inline onclick handlers and admin-handlers.js)
|
||||
async switchAdminTab(tab) {
|
||||
const cat = _adminCatForSection(tab);
|
||||
if (document.getElementById('adminPanel').style.display !== 'flex') {
|
||||
UI.openAdmin(cat, tab);
|
||||
} else {
|
||||
if (cat !== UI._adminCat) {
|
||||
UI._adminCat = cat;
|
||||
UI._renderAdminNav();
|
||||
}
|
||||
await UI._switchAdminSection(tab);
|
||||
}
|
||||
},
|
||||
|
||||
openTeamAdmin() {
|
||||
const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin');
|
||||
if (adminTeams.length === 0) {
|
||||
@@ -233,12 +211,12 @@ Object.assign(UI, {
|
||||
<td class="admin-user-time">${lastSeen}</td>
|
||||
<td class="admin-actions-cell">
|
||||
${isPending
|
||||
? `<button class="btn-icon" onclick="showApproveForm('${u.id}','${esc(u.username)}')" title="Approve">✓</button>`
|
||||
: `<button class="btn-icon" onclick="toggleUserActive('${u.id}',false)" title="Disable">⏸</button>`
|
||||
? `<button class="icon-btn" onclick="showApproveForm('${u.id}','${esc(u.username)}')" title="Approve">✓</button>`
|
||||
: `<button class="icon-btn" onclick="toggleUserActive('${u.id}',false)" title="Disable">⏸</button>`
|
||||
}
|
||||
<button class="btn-icon" onclick="toggleUserRole('${u.id}','${u.role === 'admin' ? 'user' : 'admin'}')" title="${u.role === 'admin' ? 'Demote' : 'Promote'}">⇅</button>
|
||||
<button class="btn-icon" onclick="adminResetUserPassword('${u.id}','${esc(u.username)}')" title="Reset PW">🔑</button>
|
||||
<button class="btn-icon btn-icon-danger" onclick="deleteUser('${u.id}','${esc(u.username)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" onclick="toggleUserRole('${u.id}','${u.role === 'admin' ? 'user' : 'admin'}')" title="${u.role === 'admin' ? 'Demote' : 'Promote'}">⇅</button>
|
||||
<button class="icon-btn" onclick="adminResetUserPassword('${u.id}','${esc(u.username)}')" title="Reset PW">🔑</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteUser('${u.id}','${esc(u.username)}')" title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
@@ -439,9 +417,9 @@ Object.assign(UI, {
|
||||
<td><span class="badge-provider-type">${esc(ext.tier)}</span></td>
|
||||
<td style="font-size:12px;color:var(--text-3);font-family:var(--font-mono,monospace)">v${esc(ext.version)}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="btn-icon" onclick="editAdminExtension('${ext.id}')" title="Edit">✎</button>
|
||||
<button class="btn-icon" onclick="toggleAdminExtension('${ext.id}',${!ext.is_enabled})" title="${ext.is_enabled ? 'Disable' : 'Enable'}">${ext.is_enabled ? '⏸' : '▶'}</button>
|
||||
<button class="btn-icon btn-icon-danger" onclick="deleteAdminExtension('${ext.id}','${esc(ext.name)}')" title="Uninstall">🗑</button>
|
||||
<button class="icon-btn" onclick="editAdminExtension('${ext.id}')" title="Edit">✎</button>
|
||||
<button class="icon-btn" onclick="toggleAdminExtension('${ext.id}',${!ext.is_enabled})" title="${ext.is_enabled ? 'Disable' : 'Enable'}">${ext.is_enabled ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteAdminExtension('${ext.id}','${esc(ext.name)}')" title="Uninstall">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
@@ -746,10 +724,10 @@ Object.assign(UI, {
|
||||
<td style="font-size:12px;color:var(--text-3);font-family:var(--font-mono,monospace)">${esc(p.base_model_id)}</td>
|
||||
<td>${p.is_active ? '<span class="badge-active">active</span>' : '<span class="badge-disabled">inactive</span>'}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="btn-icon" onclick="UI.openGrantPicker('persona','${p.id}','${esc(p.name)}','[data-grant-persona="${p.id}"]')" title="Access">🔒</button>
|
||||
<button class="btn-icon" onclick="editAdminPersona('${p.id}')" title="Edit">✎</button>
|
||||
<button class="btn-icon" onclick="toggleAdminPersona('${p.id}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="btn-icon btn-icon-danger" onclick="deleteAdminPersona('${p.id}','${esc(p.name)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" onclick="UI.openGrantPicker('persona','${p.id}','${esc(p.name)}','[data-grant-persona="${p.id}"]')" title="Access">🔒</button>
|
||||
<button class="icon-btn" onclick="editAdminPersona('${p.id}')" title="Edit">✎</button>
|
||||
<button class="icon-btn" onclick="toggleAdminPersona('${p.id}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteAdminPersona('${p.id}','${esc(p.name)}')" title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
@@ -788,9 +766,9 @@ Object.assign(UI, {
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--text-2)">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="btn-icon" onclick="UI.openTeamDetail('${t.id}','${esc(t.name)}')" title="Members">👥</button>
|
||||
<button class="btn-icon" onclick="toggleTeamActive('${t.id}',${!t.is_active})" title="${t.is_active ? 'Disable' : 'Enable'}">${t.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="btn-icon btn-icon-danger" onclick="deleteTeam('${t.id}','${esc(t.name)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" onclick="UI.openTeamDetail('${t.id}','${esc(t.name)}')" title="Members">👥</button>
|
||||
<button class="icon-btn" onclick="toggleTeamActive('${t.id}',${!t.is_active})" title="${t.is_active ? 'Disable' : 'Enable'}">${t.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteTeam('${t.id}','${esc(t.name)}')" title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
@@ -843,7 +821,7 @@ Object.assign(UI, {
|
||||
${m.user_role === 'admin' ? '<span class="badge-admin" style="margin-left:4px;font-size:9px">sys-admin</span>' : ''}
|
||||
</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="btn-icon btn-icon-danger" onclick="removeTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="removeTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
@@ -898,8 +876,8 @@ Object.assign(UI, {
|
||||
<td>${scopeBadge}</td>
|
||||
<td style="font-size:12px;color:var(--text-2)">${g.member_count} member${g.member_count !== 1 ? 's' : ''}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="btn-icon" onclick="UI.openGroupDetail('${g.id}','${esc(g.name)}')" title="Members">👥</button>
|
||||
<button class="btn-icon btn-icon-danger" onclick="deleteGroup('${g.id}','${esc(g.name)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" onclick="UI.openGroupDetail('${g.id}','${esc(g.name)}')" title="Members">👥</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="deleteGroup('${g.id}','${esc(g.name)}')" title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
@@ -1022,7 +1000,7 @@ Object.assign(UI, {
|
||||
</td>
|
||||
<td class="admin-user-time">${new Date(m.added_at).toLocaleDateString()}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="btn-icon btn-icon-danger" onclick="removeGroupMember('${groupId}','${m.user_id}','${esc(m.username || m.email)}')" title="Remove">🗑</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="removeGroupMember('${groupId}','${m.user_id}','${esc(m.username || m.email)}')" title="Remove">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
@@ -1196,9 +1174,9 @@ Object.assign(UI, {
|
||||
<td class="admin-user-time">${p.priority}</td>
|
||||
<td>${p.is_active ? '<span class="badge-active">active</span>' : '<span class="badge-disabled">inactive</span>'}</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="btn-icon" onclick="UI._showRoutingForm('${esc(p.id)}')" title="Edit">✎</button>
|
||||
<button class="btn-icon" onclick="UI._toggleRoutingPolicy('${esc(p.id)}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="btn-icon btn-icon-danger" onclick="UI._deleteRoutingPolicy('${esc(p.id)}','${esc(p.name)}')" title="Delete">🗑</button>
|
||||
<button class="icon-btn" onclick="UI._showRoutingForm('${esc(p.id)}')" title="Edit">✎</button>
|
||||
<button class="icon-btn" onclick="UI._toggleRoutingPolicy('${esc(p.id)}',${!p.is_active})" title="${p.is_active ? 'Disable' : 'Enable'}">${p.is_active ? '⏸' : '▶'}</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="UI._deleteRoutingPolicy('${esc(p.id)}','${esc(p.name)}')" title="Delete">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1377,34 +1377,14 @@ const UI = {
|
||||
|
||||
// ── Settings Modal ───────────────────────
|
||||
|
||||
openSettings() {
|
||||
document.getElementById('settingsModel').value = App.settings.model;
|
||||
document.getElementById('settingsSystemPrompt').value = App.settings.systemPrompt;
|
||||
document.getElementById('settingsMaxTokens').value = App.settings.maxTokens || '';
|
||||
document.getElementById('settingsTemp').value = App.settings.temperature;
|
||||
document.getElementById('settingsThinking').checked = App.settings.showThinking;
|
||||
document.getElementById('profileChangePwForm').style.display = 'none';
|
||||
|
||||
// Show model's max output in the hint
|
||||
const caps = this.getSelectedModelCaps();
|
||||
const hint = document.getElementById('settingsMaxHint');
|
||||
if (hint) {
|
||||
if (caps.max_output_tokens > 0) {
|
||||
const k = (caps.max_output_tokens / 1000).toFixed(0) + 'K';
|
||||
hint.textContent = `(model max: ${k})`;
|
||||
} else {
|
||||
hint.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
UI.loadProfileIntoSettings();
|
||||
UI.loadMyTeams();
|
||||
UI.checkUserProvidersAllowed();
|
||||
UI.checkUserPersonasAllowed();
|
||||
UI.switchSettingsTab('general');
|
||||
openModal('settingsModal');
|
||||
openSettings(section) {
|
||||
const base = window.__BASE__ || '';
|
||||
window.location.href = base + '/settings/' + (section || 'general');
|
||||
},
|
||||
closeSettings() {
|
||||
const base = window.__BASE__ || '';
|
||||
window.location.href = base + '/';
|
||||
},
|
||||
closeSettings() { closeModal('settingsModal'); },
|
||||
|
||||
switchSettingsTab(tab) {
|
||||
document.querySelectorAll('.settings-tab').forEach(t => t.classList.toggle('active', t.dataset.stab === tab));
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// ==========================================
|
||||
// Chat Switchboard – UI Formatting & Helpers
|
||||
// ==========================================
|
||||
// Pure utility layer: esc(), markdown rendering, code blocks,
|
||||
// preview panel, time formatting. Loaded after panels.js.
|
||||
|
||||
// ── HTML Escaping ───────────────────────────
|
||||
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
// Markdown rendering, code blocks, preview panel, time formatting.
|
||||
// esc() is in ui-primitives.js (loaded globally in base.html).
|
||||
|
||||
// ── Message Formatting ──────────────────────
|
||||
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
// ==========================================
|
||||
// Chat Switchboard - UI Primitives Additions
|
||||
// ==========================================
|
||||
// v0.22.7: Toast, Badge, IconBtn, Theme toggle, confirm dialog, avatar upload.
|
||||
// Load AFTER ui-primitives.js in base.html.
|
||||
|
||||
// -- Toast ------------------------------------------------
|
||||
var Toast = {
|
||||
_container: null,
|
||||
_init: function() {
|
||||
if (this._container) return;
|
||||
this._container = document.getElementById('toastContainer');
|
||||
if (!this._container) {
|
||||
this._container = document.createElement('div');
|
||||
this._container.id = 'toastContainer';
|
||||
this._container.className = 'toast-stack';
|
||||
document.body.appendChild(this._container);
|
||||
}
|
||||
},
|
||||
show: function(msg, type) {
|
||||
type = type || 'info';
|
||||
this._init();
|
||||
var el = document.createElement('div');
|
||||
el.className = 'toast toast-' + type;
|
||||
el.innerHTML = '<span class="toast-icon"></span><span class="toast-msg">' + (typeof esc === 'function' ? esc(msg) : msg) + '</span>';
|
||||
this._container.appendChild(el);
|
||||
setTimeout(function() {
|
||||
el.classList.add('toast-exit');
|
||||
setTimeout(function() { el.remove(); }, 300);
|
||||
}, 3000);
|
||||
},
|
||||
success: function(msg) { this.show(msg, 'success'); },
|
||||
error: function(msg) { this.show(msg, 'error'); },
|
||||
warning: function(msg) { this.show(msg, 'warning'); },
|
||||
info: function(msg) { this.show(msg, 'info'); },
|
||||
};
|
||||
|
||||
// -- Badge ------------------------------------------------
|
||||
function renderBadge(text, color) {
|
||||
color = color || 'muted';
|
||||
var e = typeof esc === 'function' ? esc : function(s) { return s; };
|
||||
return '<span class="badge badge-' + e(color) + '">' + e(text) + '</span>';
|
||||
}
|
||||
|
||||
// -- Icon SVG ---------------------------------------------
|
||||
var _ICONS = {
|
||||
x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||||
back: '<line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/>',
|
||||
plus: '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
|
||||
edit: '<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>',
|
||||
trash: '<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>',
|
||||
search: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
||||
send: '<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
|
||||
user: '<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||||
users: '<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/>',
|
||||
key: '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 010-7.777L12 4l3.5 3.5L18 5l3-3"/>',
|
||||
settings:'<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9c.2.65.7 1.15 1.33 1.34H21a2 2 0 010 4h-.09c-.63.2-1.13.7-1.33 1.34z"/>',
|
||||
cpu: '<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/>',
|
||||
eye: '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>',
|
||||
chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
|
||||
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
|
||||
msg: '<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>',
|
||||
globe: '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>',
|
||||
};
|
||||
|
||||
function renderIcon(name, size) {
|
||||
size = size || 16;
|
||||
var d = _ICONS[name];
|
||||
if (!d) return '';
|
||||
return '<svg width="' + size + '" height="' + size + '" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + d + '</svg>';
|
||||
}
|
||||
|
||||
function renderIconBtn(icon, opts) {
|
||||
opts = opts || {};
|
||||
var title = opts.title || '';
|
||||
var onclick = opts.onclick || '';
|
||||
var cls = opts.cls || '';
|
||||
var size = opts.size || 16;
|
||||
var activeClass = opts.active ? ' active' : '';
|
||||
var e = typeof esc === 'function' ? esc : function(s) { return s; };
|
||||
return '<button class="icon-btn' + activeClass + ' ' + cls + '" title="' + e(title) + '"' +
|
||||
(onclick ? ' onclick="' + onclick + '"' : '') + '>' + renderIcon(icon, size) + '</button>';
|
||||
}
|
||||
|
||||
// -- Theme ------------------------------------------------
|
||||
var Theme = {
|
||||
_key: 'switchboard_theme',
|
||||
init: function() {
|
||||
var saved = localStorage.getItem(this._key) || 'system';
|
||||
this.set(saved);
|
||||
},
|
||||
set: function(mode) {
|
||||
localStorage.setItem(this._key, mode);
|
||||
if (mode === 'system') document.documentElement.removeAttribute('data-theme');
|
||||
else document.documentElement.setAttribute('data-theme', mode);
|
||||
},
|
||||
get: function() { return localStorage.getItem(this._key) || 'system'; },
|
||||
resolved: function() {
|
||||
var mode = this.get();
|
||||
if (mode !== 'system') return mode;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
},
|
||||
renderToggle: function(containerId) {
|
||||
var el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
var current = this.get();
|
||||
var options = [
|
||||
{ id: 'light', label: 'Light', icon: '\u2600' },
|
||||
{ id: 'dark', label: 'Dark', icon: '\u263E' },
|
||||
{ id: 'system',label: 'System',icon: '\u229E' },
|
||||
];
|
||||
var self = this;
|
||||
el.innerHTML = '<div class="theme-toggle">' + options.map(function(o) {
|
||||
return '<button class="theme-toggle__btn' + (current === o.id ? ' active' : '') + '" data-theme="' + o.id + '" title="' + o.label + '">' + o.icon + '</button>';
|
||||
}).join('') + '</div>';
|
||||
el.querySelectorAll('.theme-toggle__btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
self.set(btn.dataset.theme);
|
||||
self.renderToggle(containerId);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// -- Confirm Dialog (enhanced) ----------------------------
|
||||
function showConfirmDialog(opts) {
|
||||
opts = opts || {};
|
||||
var title = opts.title || 'Confirm';
|
||||
var message = opts.message || 'Are you sure?';
|
||||
var confirmLabel = opts.confirmLabel || 'Confirm';
|
||||
var cancelLabel = opts.cancelLabel || 'Cancel';
|
||||
var variant = opts.variant || 'primary';
|
||||
var onConfirm = opts.onConfirm;
|
||||
var onCancel = opts.onCancel;
|
||||
var e = typeof esc === 'function' ? esc : function(s) { return s; };
|
||||
|
||||
var overlay = document.getElementById('confirmOverlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.id = 'confirmOverlay';
|
||||
overlay.className = 'confirm-overlay';
|
||||
overlay.style.display = 'none';
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
overlay.innerHTML =
|
||||
'<div class="confirm-dialog">' +
|
||||
'<div class="confirm-dialog__title">' + e(title) + '</div>' +
|
||||
'<div class="confirm-dialog__msg">' + e(message) + '</div>' +
|
||||
'<div class="confirm-dialog__actions">' +
|
||||
'<button class="btn btn-ghost" id="confirmCancelBtn">' + e(cancelLabel) + '</button>' +
|
||||
'<button class="btn btn-' + variant + '" id="confirmOkBtn">' + e(confirmLabel) + '</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
overlay.style.display = 'flex';
|
||||
|
||||
function close() { overlay.style.display = 'none'; overlay.innerHTML = ''; }
|
||||
document.getElementById('confirmCancelBtn').addEventListener('click', function() { close(); if (onCancel) onCancel(); });
|
||||
document.getElementById('confirmOkBtn').addEventListener('click', function() { close(); if (onConfirm) onConfirm(); });
|
||||
overlay.addEventListener('click', function(ev) { if (ev.target === overlay) { close(); if (onCancel) onCancel(); } });
|
||||
}
|
||||
|
||||
// -- Avatar Upload Component ------------------------------
|
||||
function mountAvatarUpload(containerEl, opts) {
|
||||
opts = opts || {};
|
||||
var current = opts.current;
|
||||
var size = opts.size || 80;
|
||||
var onUpload = opts.onUpload;
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.style.display = 'none';
|
||||
|
||||
containerEl.className = 'avatar-upload';
|
||||
containerEl.style.width = size + 'px';
|
||||
containerEl.style.height = size + 'px';
|
||||
|
||||
function render(src) {
|
||||
var e = typeof esc === 'function' ? esc : function(s) { return s; };
|
||||
if (src) containerEl.innerHTML = '<img src="' + e(src) + '" alt="Avatar">';
|
||||
else containerEl.innerHTML = '<div class="avatar-upload__placeholder">Upload<br>Photo</div>';
|
||||
containerEl.appendChild(input);
|
||||
}
|
||||
|
||||
render(current || null);
|
||||
containerEl.addEventListener('click', function() { input.click(); });
|
||||
input.addEventListener('change', function() {
|
||||
var file = input.files[0];
|
||||
if (!file) return;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(ev) {
|
||||
render(ev.target.result);
|
||||
if (onUpload) onUpload(file, ev.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
return { setImage: render };
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// ── Chat Switchboard — UI Primitives ────────────────────────────────────────
|
||||
// Single source of truth for constants + reusable rendering functions.
|
||||
// Loaded after ui-format.js, before ui-core.js.
|
||||
// Loaded in base.html before all components and surfaces.
|
||||
// Follows the renderPersonaForm() pattern: (container, options) → control object.
|
||||
// Designed for extension hooks: registries are open for .add(), primitives
|
||||
// accept options for customization, and return handles for programmatic control.
|
||||
@@ -8,6 +8,67 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// §0 CORE UTILITIES
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** HTML-escape a string for safe insertion into innerHTML and attributes. */
|
||||
function esc(s) {
|
||||
if (!s) return '';
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// §0b COMPONENT LIFECYCLE
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Create a component registry (factory namespace with instance tracking).
|
||||
*
|
||||
* @param {string} name - Component name (for debug)
|
||||
* @returns {{ primary, _instances, _register, _unregister, get }}
|
||||
*/
|
||||
function createComponentRegistry(name) {
|
||||
return {
|
||||
primary: null,
|
||||
_instances: new Map(),
|
||||
_register(id, instance) { this._instances.set(id, instance); },
|
||||
_unregister(id) {
|
||||
this._instances.delete(id);
|
||||
if (this.primary && this.primary.id === id) this.primary = null;
|
||||
},
|
||||
get(id) { return this._instances.get(id) || null; },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix lifecycle methods into a component instance.
|
||||
* Adds _listeners array, _on() for tracked event binding, and destroy()
|
||||
* for automatic cleanup + registry removal.
|
||||
* Components needing extra teardown define _cleanup() which is called first.
|
||||
*
|
||||
* @param {Object} instance - The instance object (must have .id)
|
||||
* @param {Object} registry - The component registry (from createComponentRegistry)
|
||||
* @returns {Object} The instance, mutated with lifecycle methods
|
||||
*/
|
||||
function componentMixin(instance, registry) {
|
||||
instance._listeners = [];
|
||||
instance._on = function(el, event, handler) {
|
||||
if (!el) return;
|
||||
el.addEventListener(event, handler);
|
||||
this._listeners.push({ el, event, handler });
|
||||
};
|
||||
instance.destroy = function() {
|
||||
if (typeof this._cleanup === 'function') this._cleanup();
|
||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||
this._listeners = [];
|
||||
registry._unregister(this.id);
|
||||
};
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// §1 PROVIDER REGISTRY
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -933,3 +994,98 @@ function updateTabArrows(tabs) {
|
||||
left.classList.toggle('visible', !atStart);
|
||||
right.classList.toggle('visible', !atEnd);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// §9 THEME
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const Theme = {
|
||||
_key: 'switchboard_theme',
|
||||
|
||||
init() {
|
||||
this.set(localStorage.getItem(this._key) || 'system');
|
||||
},
|
||||
|
||||
set(mode) {
|
||||
localStorage.setItem(this._key, mode);
|
||||
if (mode === 'system') document.documentElement.removeAttribute('data-theme');
|
||||
else document.documentElement.setAttribute('data-theme', mode);
|
||||
},
|
||||
|
||||
get() {
|
||||
return localStorage.getItem(this._key) || 'system';
|
||||
},
|
||||
|
||||
resolved() {
|
||||
const mode = this.get();
|
||||
if (mode !== 'system') return mode;
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
},
|
||||
|
||||
renderToggle(containerId) {
|
||||
const el = document.getElementById(containerId);
|
||||
if (!el) return;
|
||||
const current = this.get();
|
||||
const options = [
|
||||
{ id: 'light', label: 'Light', icon: '☀' },
|
||||
{ id: 'dark', label: 'Dark', icon: '☾' },
|
||||
{ id: 'system',label: 'System',icon: '⊞' },
|
||||
];
|
||||
el.innerHTML = '<div class="toggle-group">' + options.map(o =>
|
||||
`<button class="toggle-btn${current === o.id ? ' active' : ''}" data-theme="${o.id}" title="${o.label}">${o.icon}</button>`
|
||||
).join('') + '</div>';
|
||||
el.querySelectorAll('.toggle-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
this.set(btn.dataset.theme);
|
||||
this.renderToggle(containerId);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// §10 AVATAR UPLOAD
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Mount an avatar upload component into a container.
|
||||
*
|
||||
* @param {HTMLElement} containerEl
|
||||
* @param {Object} opts
|
||||
* @param {string} [opts.current] - Current avatar URL
|
||||
* @param {number} [opts.size=80] - Avatar size in px
|
||||
* @param {Function} [opts.onUpload] - Called with (file, dataURL)
|
||||
* @returns {{ setImage }}
|
||||
*/
|
||||
function mountAvatarUpload(containerEl, opts = {}) {
|
||||
const size = opts.size || 80;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.style.display = 'none';
|
||||
|
||||
containerEl.className = 'avatar-upload';
|
||||
containerEl.style.width = size + 'px';
|
||||
containerEl.style.height = size + 'px';
|
||||
|
||||
function render(src) {
|
||||
if (src) containerEl.innerHTML = `<img src="${esc(src)}" alt="Avatar">`;
|
||||
else containerEl.innerHTML = '<div class="avatar-upload__placeholder">Upload<br>Photo</div>';
|
||||
containerEl.appendChild(input);
|
||||
}
|
||||
|
||||
render(opts.current || null);
|
||||
containerEl.addEventListener('click', () => input.click());
|
||||
input.addEventListener('change', () => {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
render(ev.target.result);
|
||||
if (opts.onUpload) opts.onUpload(file, ev.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
return { setImage: render };
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ Object.assign(UI, {
|
||||
if (scaleEl) prefs.scale = parseInt(scaleEl.value);
|
||||
if (msgFontEl) prefs.msgFont = parseInt(msgFontEl.value);
|
||||
// v0.23.2: Theme saved via Theme API (localStorage key: switchboard_theme)
|
||||
const activeThemeBtn = document.querySelector('#themeToggle .theme-btn.active');
|
||||
const activeThemeBtn = document.querySelector('#themeToggle .toggle-btn.active');
|
||||
if (activeThemeBtn && typeof Theme !== 'undefined') {
|
||||
Theme.set(activeThemeBtn.dataset.theme);
|
||||
}
|
||||
@@ -156,17 +156,17 @@ Object.assign(UI, {
|
||||
if (msgFontEl) { msgFontEl.value = msgFont; document.getElementById('msgFontValue').textContent = msgFont + 'px'; }
|
||||
|
||||
// Highlight active theme button and wire click
|
||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => {
|
||||
document.querySelectorAll('#themeToggle .toggle-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.theme === theme);
|
||||
btn.onclick = () => {
|
||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('#themeToggle .toggle-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
if (typeof Theme !== 'undefined') Theme.set(btn.dataset.theme);
|
||||
};
|
||||
});
|
||||
|
||||
// Highlight active keymap button
|
||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => {
|
||||
document.querySelectorAll('#keymapToggle .toggle-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.keymap === keymap);
|
||||
});
|
||||
},
|
||||
@@ -192,11 +192,11 @@ Object.assign(UI, {
|
||||
});
|
||||
|
||||
// Theme toggle buttons
|
||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => {
|
||||
document.querySelectorAll('#themeToggle .toggle-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const theme = btn.dataset.theme;
|
||||
UI.applyTheme(theme);
|
||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(b =>
|
||||
document.querySelectorAll('#themeToggle .toggle-btn').forEach(b =>
|
||||
b.classList.toggle('active', b === btn)
|
||||
);
|
||||
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
@@ -206,10 +206,10 @@ Object.assign(UI, {
|
||||
});
|
||||
|
||||
// Editor keymap toggle buttons
|
||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => {
|
||||
document.querySelectorAll('#keymapToggle .toggle-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const mode = btn.dataset.keymap;
|
||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(b =>
|
||||
document.querySelectorAll('#keymapToggle .toggle-btn').forEach(b =>
|
||||
b.classList.toggle('active', b === btn)
|
||||
);
|
||||
// Persist
|
||||
@@ -395,7 +395,7 @@ Object.assign(UI, {
|
||||
</select>
|
||||
</td>
|
||||
<td class="admin-actions-cell">
|
||||
<button class="btn-icon btn-icon-danger" onclick="settingsRemoveTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
||||
<button class="icon-btn icon-btn-danger" onclick="settingsRemoveTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">🗑</button>
|
||||
</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>
|
||||
|
||||
@@ -12,12 +12,11 @@
|
||||
// menu.destroy();
|
||||
|
||||
const UserMenu = {
|
||||
primary: null,
|
||||
_instances: new Map(),
|
||||
...createComponentRegistry('UserMenu'),
|
||||
|
||||
create(opts) {
|
||||
const pfx = opts.id || '';
|
||||
const instance = {
|
||||
const instance = componentMixin({
|
||||
id: pfx,
|
||||
btnEl: document.getElementById(pfx + 'userMenuBtn'),
|
||||
avatarEl: document.getElementById(pfx + 'userAvatar'),
|
||||
@@ -30,7 +29,6 @@ const UserMenu = {
|
||||
debugEl: document.getElementById(pfx + 'menuDebug'),
|
||||
signoutEl: document.getElementById(pfx + 'menuSignout'),
|
||||
_open: false,
|
||||
_listeners: [],
|
||||
|
||||
// ── User display ────────────────────
|
||||
|
||||
@@ -127,28 +125,9 @@ const UserMenu = {
|
||||
this._on(this.debugEl, 'click', () => { this.close(); if (handlers.onDebug) handlers.onDebug(); });
|
||||
this._on(this.signoutEl, 'click', () => { this.close(); if (handlers.onSignout) handlers.onSignout(); });
|
||||
},
|
||||
}, UserMenu);
|
||||
|
||||
// ── Lifecycle ───────────────────────
|
||||
|
||||
destroy() {
|
||||
this._listeners.forEach(({ el, event, handler }) => el.removeEventListener(event, handler));
|
||||
this._listeners = [];
|
||||
UserMenu._instances.delete(this.id);
|
||||
if (UserMenu.primary === this) UserMenu.primary = null;
|
||||
},
|
||||
|
||||
// ── Internal ────────────────────────
|
||||
|
||||
_on(el, event, handler) {
|
||||
if (!el) return;
|
||||
el.addEventListener(event, handler);
|
||||
this._listeners.push({ el, event, handler });
|
||||
},
|
||||
};
|
||||
|
||||
UserMenu._instances.set(pfx, instance);
|
||||
UserMenu._register(pfx, instance);
|
||||
return instance;
|
||||
},
|
||||
|
||||
get(id) { return this._instances.get(id) || null; },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user