Changeset 0.13.0 (#64)

This commit is contained in:
2026-02-25 22:52:19 +00:00
parent 88216ec4cb
commit 8292a6efa8
18 changed files with 360 additions and 9363 deletions

View File

@@ -6,6 +6,7 @@
========================================== */
:root {
/* ── Core palette ────────────────────────── */
--bg: #0e0e10;
--bg-surface: #18181b;
--bg-raised: #222227;
@@ -23,6 +24,27 @@
--warning: #eab308;
--purple: #a78bfa;
--purple-dim: rgba(167,139,250,0.10);
/* ── Contrast text for solid-color backgrounds ── */
--text-on-color: #fff;
--text-on-warning: #000;
/* ── Light variants (badge/pill text on dim backgrounds) ── */
--accent-light: #93c5fd;
--danger-light: #f87171;
--success-light: #4ade80;
--warning-light: #fbbf24;
/* ── Dim variants (badge/pill backgrounds) ── */
--danger-dim: rgba(239,68,68,0.2);
--success-dim: rgba(34,197,94,0.2);
--warning-dim: rgba(234,179,8,0.2);
/* ── Surfaces ────────────────────────────── */
--overlay: rgba(0,0,0,0.6);
--glass: rgba(255,255,255,0.03);
/* ── Layout ──────────────────────────────── */
--radius: 8px;
--radius-lg: 12px;
--sidebar-w: 260px;
@@ -406,7 +428,7 @@ a:hover { text-decoration: underline; }
.messages { flex: 1; overflow-y: auto; scroll-behavior: smooth; }
.message { padding: 1.25rem 0; }
.message:not(:last-child) { border-bottom: 1px solid rgba(255,255,255,0.03); }
.message:not(:last-child) { border-bottom: 1px solid var(--glass); }
.msg-inner { max-width: 768px; margin: 0 auto; padding: 0 1.5rem; display: flex; gap: 1rem; }
@@ -506,7 +528,7 @@ a:hover { text-decoration: underline; }
.msg-edit-cancel:hover { background: var(--bg-hover); color: var(--text); }
.msg-edit-submit {
background: var(--accent);
color: #fff;
color: var(--text-on-color);
border-color: var(--accent);
}
.msg-edit-submit:hover { background: var(--accent-hover); }
@@ -701,7 +723,7 @@ a:hover { text-decoration: underline; }
/* Thinking blocks */
.thinking-block {
border: 1px solid rgba(108,159,255,0.12); border-radius: var(--radius);
border: 1px solid var(--accent-dim); border-radius: var(--radius);
margin: 0.5rem 0; background: rgba(108,159,255,0.03);
}
.thinking-block summary {
@@ -831,15 +853,15 @@ a:hover { text-decoration: underline; }
.context-warning-icon { flex-shrink: 0; }
.context-warning-text { flex: 1; }
.context-warning-dismiss { background: none; border: none; color: var(--text-3); cursor: pointer; padding: 0 2px; font-size: 0.85rem; }
.context-warning-dismiss:hover { color: var(--text-1); }
.context-warning-dismiss:hover { color: var(--text); }
.context-warning-action {
flex-shrink: 0; padding: 3px 10px; border-radius: 6px; border: 1px solid var(--border);
background: var(--bg-surface); color: var(--text-1); cursor: pointer;
background: var(--bg-surface); color: var(--text); cursor: pointer;
font-size: 0.75rem; white-space: nowrap; transition: all var(--transition);
}
.context-warning-action:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
.context-warning-action:hover { background: var(--accent); color: var(--text-on-color); border-color: var(--accent); }
.context-warning-action:disabled { opacity: 0.5; cursor: not-allowed; }
.context-warning-action:disabled:hover { background: var(--bg-surface); color: var(--text-1); border-color: var(--border); }
.context-warning-action:disabled:hover { background: var(--bg-surface); color: var(--text); border-color: var(--border); }
/* Summary message node */
.message-summary {
border: 1px dashed color-mix(in srgb, var(--accent) 40%, transparent);
@@ -855,7 +877,7 @@ a:hover { text-decoration: underline; }
background: none; border: none; color: var(--accent); cursor: pointer;
font-size: 0.75rem; padding: 0; text-decoration: underline;
}
.message-summary .summary-toggle:hover { color: var(--text-1); }
.message-summary .summary-toggle:hover { color: var(--text); }
.msg-dimmed { opacity: 0.5; }
.message-summary-divider {
text-align: center; padding: 8px 0; margin: 4px 0;
@@ -918,7 +940,7 @@ a:hover { text-decoration: underline; }
font-size: 12px; max-width: 220px; position: relative;
}
.att-chip.att-ready { border-color: color-mix(in srgb, var(--success) 40%, var(--border)); }
.att-chip.att-error { border-color: color-mix(in srgb, var(--error) 40%, var(--border)); }
.att-chip.att-error { border-color: color-mix(in srgb, var(--danger) 40%, var(--border)); }
.att-chip.att-pending { border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); }
.att-thumb {
@@ -1005,7 +1027,7 @@ a:hover { text-decoration: underline; }
}
.lightbox-close {
position: absolute; top: 16px; right: 20px;
background: rgba(255,255,255,0.15); border: none; color: #fff;
background: rgba(255,255,255,0.15); border: none; color: var(--text-on-color);
font-size: 22px; width: 40px; height: 40px; border-radius: 50%;
cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: background 0.15s;
@@ -1032,7 +1054,7 @@ a:hover { text-decoration: underline; }
button { font-family: var(--font); cursor: pointer; }
.btn-primary {
background: var(--accent); color: #fff; border: none;
background: var(--accent); color: var(--text-on-color); border: none;
padding: 8px 16px; border-radius: var(--radius);
font-weight: 500; font-size: 13px;
transition: background var(--transition);
@@ -1046,8 +1068,8 @@ button { font-family: var(--font); cursor: pointer; }
font-size: 12px; transition: all var(--transition);
}
.btn-small:hover { background: var(--bg-hover); color: var(--text); }
.btn-small.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-danger { background: var(--danger); color: #fff; border: none; border-radius: var(--radius); }
.btn-small.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--text-on-color); }
.btn-danger { background: var(--danger); color: var(--text-on-color); border: none; border-radius: var(--radius); }
/* ── Auth Splash ─────────────────────────── */
@@ -1147,7 +1169,7 @@ button { font-family: var(--font); cursor: pointer; }
.auth-error { color: var(--danger); font-size: 0.78rem; min-height: 1.2em; margin: 0.5rem 0; }
.splash-error { color: var(--danger); font-size: 0.78rem; text-align: center; margin-top: 0.5rem; line-height: 1.5; }
.splash-error strong { display: block; font-size: 0.85rem; margin-bottom: 0.25rem; }
.splash-error-hint { color: var(--text-muted); font-size: 0.72rem; }
.splash-error-hint { color: var(--text-3); font-size: 0.72rem; }
.splash-error-hint a { color: var(--accent); text-decoration: underline; cursor: pointer; }
.auth-actions { margin-top: 1.5rem; }
.auth-footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border); text-align: center; }
@@ -1200,14 +1222,26 @@ button { font-family: var(--font); cursor: pointer; }
/* ── Forms ────────────────────────────────── */
.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%; background: var(--bg-raised); border: 1px solid var(--border);
/* Base form element styling — ensures selects/inputs match the dark theme
even when not wrapped in .form-group (e.g. role config, inline toolbars) */
select, input[type="text"], input[type="email"], input[type="password"],
input[type="number"], input[type="search"], textarea {
background: var(--bg-raised); border: 1px solid var(--border);
color: var(--text); padding: 8px 12px; border-radius: var(--radius);
font-family: var(--font); font-size: 13px;
transition: border-color var(--transition);
}
select:focus, input[type="text"]:focus, input[type="email"]:focus,
input[type="password"]:focus, textarea:focus {
outline: none; border-color: var(--accent);
}
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);
}
@@ -1233,7 +1267,7 @@ button { font-family: var(--font); cursor: pointer; }
/* ── Modal ────────────────────────────────── */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.6);
position: fixed; inset: 0; background: var(--overlay);
display: none; align-items: center; justify-content: center;
z-index: 1000; backdrop-filter: blur(2px);
padding: 2rem; /* keeps modal away from viewport edges at any zoom */
@@ -1324,7 +1358,7 @@ button { font-family: var(--font); cursor: pointer; }
.settings-tabs .settings-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.settings-notice {
background: rgba(234,179,8,0.08); border: 1px solid rgba(234,179,8,0.2);
background: rgba(234,179,8,0.08); border: 1px solid var(--warning-dim);
border-radius: var(--radius); padding: 10px 14px; font-size: 13px;
color: var(--warning); display: flex; align-items: center; gap: 8px;
margin-top: 1rem;
@@ -1340,17 +1374,66 @@ button { font-family: var(--font); cursor: pointer; }
.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 tabs */
.admin-tabs { }
/* Admin panel (fullscreen) */
.admin-panel {
position: fixed; z-index: 1000;
top: var(--banner-top-height); right: 0;
bottom: var(--banner-bottom-height); left: 0;
background: var(--bg); display: flex; flex-direction: column;
}
.admin-topbar {
display: flex; align-items: center; padding: 0 1rem; height: 48px;
border-bottom: 1px solid var(--border); background: var(--bg-surface); flex-shrink: 0;
}
.admin-back {
background: none; border: none; color: var(--text-3); cursor: pointer;
font-size: 14px; padding: 4px 10px; border-radius: 4px; font-family: var(--font);
}
.admin-back:hover { color: var(--accent); background: var(--bg-raised); }
.admin-title { font-size: 16px; font-weight: 600; margin-left: 1rem; flex: 1; }
.admin-user { font-size: 12px; color: var(--text-3); }
.admin-categories {
display: flex; gap: 0; border-bottom: 1px solid var(--border);
background: var(--bg-surface); padding: 0 1rem; flex-shrink: 0;
}
.admin-cat {
background: none; border: none; border-bottom: 2px solid transparent;
color: var(--text-3); padding: 10px 20px; cursor: pointer;
font-size: 14px; font-weight: 500; font-family: var(--font);
transition: color 0.15s, border-color 0.15s; outline: none;
}
.admin-cat:hover { color: var(--text-2); }
.admin-cat.active { color: var(--accent); border-bottom-color: var(--accent); }
.admin-body { display: flex; flex: 1; overflow: hidden; }
.admin-sidebar {
width: 160px; flex-shrink: 0; border-right: 1px solid var(--border);
background: var(--bg-surface); padding: 0.75rem 0; overflow-y: auto;
}
.admin-sidebar button {
display: block; width: 100%; text-align: left; background: none; border: none;
padding: 8px 20px; color: var(--text-3); cursor: pointer;
font-size: 13px; font-family: var(--font); border-left: 3px solid transparent;
transition: color 0.1s, background 0.1s;
}
.admin-sidebar button:hover { color: var(--text-2); background: var(--bg-raised); }
.admin-sidebar button.active { color: var(--accent); border-left-color: var(--accent); background: var(--bg-raised); }
.admin-main { flex: 1; overflow-y: auto; padding: 1.5rem 2rem; }
.admin-section-content { /* replaces .admin-tab-content */ }
/* Admin shared */
/* admin-tab: still used by team admin modal */
.admin-tab {
padding: 10px 14px; background: none; border: none; color: var(--text-3);
cursor: pointer; font-size: 13px; border-bottom: 2px solid transparent;
white-space: nowrap; flex-shrink: 0; transition: color var(--transition);
outline: none;
outline: none; font-family: var(--font);
}
.admin-tab:hover { color: var(--text-2); }
.admin-tab:focus { color: var(--text-3); }
.admin-tab.active, .admin-tab.active:focus { color: var(--accent); border-bottom-color: var(--accent); }
.admin-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.admin-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 1rem; }
.admin-hint { font-size: 12px; color: var(--text-3); }
@@ -1387,7 +1470,7 @@ button { font-family: var(--font); cursor: pointer; }
.admin-model-toggle { background: none; border: 1px solid var(--border); border-radius: 4px; padding: 4px 10px; font-size: 12px; cursor: pointer; color: var(--text-2); min-width: 82px; text-align: center; }
.admin-model-toggle:hover { border-color: var(--accent); color: var(--accent); }
.admin-model-toggle.enabled { border-color: var(--success); color: var(--success); }
.admin-model-toggle.team { border-color: #60a5fa; color: #60a5fa; }
.admin-model-toggle.team { border-color: var(--accent); color: var(--accent); }
.model-list-item.model-hidden { opacity: 0.5; }
.model-list-item.model-hidden .model-name { text-decoration: line-through; }
@@ -1411,7 +1494,7 @@ button { font-family: var(--font); cursor: pointer; }
.admin-preset-row .preset-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
.admin-preset-row .btn-delete { background: none; border: none; color: var(--text-3); cursor: pointer; font-size: 16px; padding: 2px 6px; }
.admin-preset-row .btn-delete:hover { color: var(--danger); }
.badge-user { font-size: 10px; padding: 1px 6px; border-radius: 3px; background: var(--accent-bg); color: var(--accent); }
.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; }
@@ -1464,15 +1547,15 @@ button { font-family: var(--font); cursor: pointer; }
.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: #fff; font-size: 10px; padding: 1px 8px; border-radius: 4px; }
.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: #fff; font-size: 10px; padding: 1px 8px; border-radius: 4px; }
.badge-pending { background: rgba(234,179,8,0.85); color: #000; font-size: 10px; padding: 1px 8px; border-radius: 4px; }
.badge-team { background: rgba(59,130,246,0.2); color: #93c5fd; font-size: 10px; padding: 1px 8px; border-radius: 4px; margin-left: 2px; }
.admin-approve-form { padding: 10px 12px; margin: -4px 0 8px; background: rgba(255,255,255,0.03); border-radius: 0 0 8px 8px; border-top: 1px solid var(--border); }
.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: rgba(34,197,94,0.15); color: #4ade80; border: 1px solid rgba(34,197,94,0.3); border-radius: 4px; padding: 3px 10px; cursor: pointer; font-size: 12px; }
.team-card { padding: 8px 10px; background: rgba(255,255,255,0.03); border-radius: 6px; margin-bottom: 6px; }
.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; }
.team-card { padding: 8px 10px; background: var(--glass); border-radius: 6px; margin-bottom: 6px; }
.team-card-info { display: flex; align-items: center; gap: 8px; }
.team-admin-back {
cursor: pointer; margin-right: 4px; opacity: 0.5;
@@ -1480,10 +1563,10 @@ button { font-family: var(--font); cursor: pointer; }
}
.team-admin-back:hover { opacity: 1; }
.team-tab-content { padding: 0; }
.badge-private { background: rgba(139,92,246,0.85); color: #fff; font-size: 10px; padding: 1px 8px; border-radius: 4px; }
.badge-success { background: rgba(34,197,94,0.2); color: #4ade80; font-size: 10px; padding: 1px 8px; border-radius: 4px; }
.badge-warning { background: rgba(234,179,8,0.2); color: #fbbf24; font-size: 10px; padding: 1px 8px; border-radius: 4px; }
.badge-danger { background: rgba(239,68,68,0.2); color: #f87171; font-size: 10px; padding: 1px 8px; border-radius: 4px; }
.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; }
.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; }
@@ -1687,8 +1770,8 @@ button { font-family: var(--font); cursor: pointer; }
.notes-editor input, .notes-editor textarea {
width: 100%; background: var(--bg-surface); color: var(--text);
border: 1px solid var(--border); border-radius: var(--radius);
padding: 6px 10px; font-family: var(--font); outline: none;
transition: border-color var(--transition);
padding: 6px 10px; font-family: var(--font); font-size: var(--msg-font, 14px);
outline: none; transition: border-color var(--transition);
}
.notes-editor input:focus, .notes-editor textarea:focus {
border-color: var(--accent);
@@ -1730,7 +1813,7 @@ button { font-family: var(--font); cursor: pointer; }
/* ── Command Palette ─────────────────────── */
.cmd-palette-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
position: fixed; inset: 0; background: var(--overlay);
display: none; align-items: flex-start; justify-content: center;
z-index: 2000; backdrop-filter: blur(4px);
padding-top: min(20vh, 160px);
@@ -1817,7 +1900,7 @@ button { font-family: var(--font); cursor: pointer; }
.confirm-overlay {
position: fixed; inset: 0; z-index: 9999;
display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.55);
background: var(--overlay);
animation: fade-in 0.12s ease;
}
.confirm-dialog {
@@ -1876,7 +1959,7 @@ button { font-family: var(--font); cursor: pointer; }
.mobile-menu-btn:hover { color: var(--text); }
.sidebar-overlay {
display: none; position: fixed; inset: 0; z-index: 99;
background: rgba(0,0,0,0.5);
background: var(--overlay);
}
@media (max-width: 768px) {
@@ -1891,4 +1974,9 @@ button { font-family: var(--font); cursor: pointer; }
.modal { width: 100%; }
.tab-arrow { width: 36px; font-size: 20px; } /* larger touch target */
.modal-tabs { padding: 0 12px; }
.admin-sidebar { width: 48px; padding: 0.5rem 0; }
.admin-sidebar button { padding: 8px 0; text-align: center; font-size: 11px; border-left: none; border-bottom: 2px solid transparent; }
.admin-sidebar button.active { border-bottom-color: var(--accent); border-left: none; }
.admin-main { padding: 1rem; }
.admin-cat { padding: 10px 12px; font-size: 13px; }
}

View File

@@ -425,7 +425,7 @@
<button class="btn-small" onclick="bulkSetUserModelVisibility(false)" title="Hide all models from selector">Hide All</button>
</div>
</div>
<p class="section-hint" style="margin-bottom:8px">Toggle visibility in your model selector. Hidden models are still accessible through presets.</p>
<p class="section-hint" style="margin-bottom:8px">Toggle visibility in your model selector. Hidden models are still accessible through personas.</p>
<div id="userModelList" class="model-list-grid"><div class="empty-hint">Loading models...</div></div>
</section>
</div>
@@ -433,7 +433,7 @@
<div class="settings-tab-content" id="settingsPersonasTab" style="display:none">
<section class="settings-section">
<h3 style="font-size:14px;margin-bottom:4px">My Personas</h3>
<p class="section-hint" style="margin-bottom:8px">Personal model presets with custom system prompts and settings.</p>
<p class="section-hint" style="margin-bottom:8px">Personal model personas with custom system prompts and settings.</p>
<div id="userPresetList"><div class="empty-hint">Loading...</div></div>
<div id="userAddPresetForm" style="display:none;margin-top:8px" class="admin-inline-form"></div>
<button class="btn-small" id="userAddPresetBtn" style="margin-top:6px">+ New Persona</button>
@@ -491,7 +491,7 @@
<div class="modal-tabs admin-tabs" id="teamAdminTabs">
<button class="admin-tab active" data-ttab="members" onclick="UI.switchTeamTab('members')">Members</button>
<button class="admin-tab" data-ttab="providers" onclick="UI.switchTeamTab('providers')">Providers</button>
<button class="admin-tab" data-ttab="presets" onclick="UI.switchTeamTab('presets')">Presets</button>
<button class="admin-tab" data-ttab="presets" onclick="UI.switchTeamTab('presets')">Personas</button>
<button class="admin-tab" data-ttab="usage" onclick="UI.switchTeamTab('usage')">Usage</button>
<button class="admin-tab" data-ttab="activity" onclick="UI.switchTeamTab('activity')">Activity</button>
</div>
@@ -558,25 +558,24 @@
</div>
<!-- ── Admin Modal ─────────────────────────── -->
<div class="modal-overlay" id="adminModal">
<div class="modal modal-wide">
<div class="modal-header"><h2>Admin Panel</h2><button class="modal-close" id="adminCloseBtn"></button></div>
<div class="modal-tabs admin-tabs">
<button class="admin-tab active" data-tab="users">Users</button>
<button class="admin-tab" data-tab="providers">Providers</button>
<button class="admin-tab" data-tab="models">Models</button>
<button class="admin-tab" data-tab="presets">Presets</button>
<button class="admin-tab" data-tab="teams">Teams</button>
<button class="admin-tab" data-tab="settings">Settings</button>
<button class="admin-tab" data-tab="roles">Roles</button>
<button class="admin-tab" data-tab="usage">Usage</button>
<button class="admin-tab" data-tab="extensions">Extensions</button>
<button class="admin-tab" data-tab="storage">Storage</button>
<button class="admin-tab" data-tab="stats">Stats</button>
<button class="admin-tab" data-tab="audit">Audit</button>
</div>
<div class="modal-body">
<div class="admin-tab-content" id="adminUsersTab">
<!-- ── Admin Panel (fullscreen overlay) ──── -->
<div class="admin-panel" id="adminPanel" style="display:none">
<div class="admin-topbar">
<button class="admin-back" id="adminBackBtn">← Chat</button>
<h1 class="admin-title">Admin Panel</h1>
<span class="admin-user" id="adminCurrentUser"></span>
</div>
<div class="admin-categories" id="adminCategories">
<button class="admin-cat active" data-cat="people">People</button>
<button class="admin-cat" data-cat="ai">AI</button>
<button class="admin-cat" data-cat="system">System</button>
<button class="admin-cat" data-cat="monitoring">Monitoring</button>
</div>
<div class="admin-body">
<nav class="admin-sidebar" id="adminSidebar"></nav>
<main class="admin-main" id="adminMain">
<!-- People -->
<div class="admin-section-content" id="adminUsersTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddUserBtn">+ Add User</button>
</div>
@@ -593,32 +592,7 @@
</div>
<div id="adminUserList"></div>
</div>
<div class="admin-tab-content" id="adminProvidersTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddProviderBtn">+ Add Global Provider</button>
</div>
<div id="adminAddProviderForm" style="display:none" class="admin-inline-form">
</div>
<div id="adminProviderList"></div>
</div>
<div class="admin-tab-content" id="adminModelsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminFetchModelsBtn">⟳ Fetch Models</button>
<button class="btn-small" onclick="bulkSetVisibility('enabled')">All Enabled</button>
<button class="btn-small" onclick="bulkSetVisibility('team')">All Team</button>
<button class="btn-small" onclick="bulkSetVisibility('disabled')">All Disabled</button>
<span class="admin-hint" id="adminModelsHint"></span>
</div>
<div id="adminModelList"></div>
</div>
<div class="admin-tab-content" id="adminPresetsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddPresetBtn">+ New Global Preset</button>
</div>
<div id="adminAddPresetForm" style="display:none" class="admin-inline-form"></div>
<div id="adminPresetList"></div>
</div>
<div class="admin-tab-content" id="adminTeamsTab" style="display:none">
<div class="admin-section-content" id="adminTeamsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddTeamBtn">+ New Team</button>
</div>
@@ -630,7 +604,6 @@
<div class="form-row"><button class="btn-small btn-primary" id="adminCreateTeamBtn">Create</button><button class="btn-small" id="adminCancelTeamBtn">Cancel</button></div>
</div>
<div id="adminTeamList"></div>
<!-- Team detail / member management (shown when editing a team) -->
<div id="adminTeamDetail" style="display:none">
<button class="notes-back-btn" id="adminTeamBackBtn">← Back to teams</button>
<h3 id="adminTeamDetailName" style="margin:8px 0 12px;font-size:15px"></h3>
@@ -653,7 +626,42 @@
<div id="adminMemberList"></div>
</div>
</div>
<div class="admin-tab-content" id="adminSettingsTab" style="display:none">
<div class="admin-section-content" id="adminRolesTab" style="display:none">
<p style="color:var(--text-muted);margin:0 0 12px">Configure system model roles. Each role has a primary and optional fallback model.</p>
<div id="adminRolesContent">
<div class="loading">Loading roles...</div>
</div>
</div>
<!-- AI -->
<div class="admin-section-content" id="adminProvidersTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddProviderBtn">+ Add Global Provider</button>
</div>
<div id="adminAddProviderForm" style="display:none" class="admin-inline-form">
</div>
<div id="adminProviderList"></div>
</div>
<div class="admin-section-content" id="adminModelsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminFetchModelsBtn">⟳ Fetch Models</button>
<button class="btn-small" onclick="bulkSetVisibility('enabled')">All Enabled</button>
<button class="btn-small" onclick="bulkSetVisibility('team')">All Team</button>
<button class="btn-small" onclick="bulkSetVisibility('disabled')">All Disabled</button>
<span class="admin-hint" id="adminModelsHint"></span>
</div>
<div id="adminModelList"></div>
</div>
<div class="admin-section-content" id="adminPresetsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminAddPresetBtn">+ New Global Persona</button>
</div>
<div id="adminAddPresetForm" style="display:none" class="admin-inline-form"></div>
<div id="adminPresetList"></div>
</div>
<!-- System -->
<div class="admin-section-content" id="adminSettingsTab" style="display:none">
<section class="settings-section">
<h3>System Prompt</h3>
<p class="section-hint" style="margin-bottom:6px">Injected into every conversation before user/preset prompts. Users cannot override or disable this.</p>
@@ -678,9 +686,9 @@
<p class="section-hint">When disabled, users can only use admin-configured global providers.</p>
</section>
<section class="settings-section">
<h3>User Presets</h3>
<label class="checkbox-label"><input type="checkbox" id="adminUserPresetsToggle" checked> Allow users to create personal presets</label>
<p class="section-hint">When disabled, users can only use admin-created global presets.</p>
<h3>User Personas</h3>
<label class="checkbox-label"><input type="checkbox" id="adminUserPresetsToggle" checked> Allow users to create personal personas</label>
<p class="section-hint">When disabled, users can only use admin-created global personas.</p>
</section>
<section class="settings-section">
<h3>Default Model</h3>
@@ -718,38 +726,8 @@
</section>
<button class="btn-primary btn-small" id="adminSaveSettings">Save Settings</button>
</div>
<!-- ── Roles Tab ─────────────────── -->
<div class="admin-tab-content" id="adminRolesTab" style="display:none">
<p style="color:var(--text-muted);margin:0 0 12px">Configure system model roles. Each role has a primary and optional fallback model.</p>
<div id="adminRolesContent">
<div class="loading">Loading roles...</div>
</div>
</div>
<!-- ── Usage Tab ─────────────────── -->
<div class="admin-tab-content" id="adminUsageTab" style="display:none">
<div class="admin-toolbar" style="margin-bottom:12px">
<select id="usagePeriod" style="min-width:100px">
<option value="7d">Last 7 days</option>
<option value="30d" selected>Last 30 days</option>
<option value="90d">Last 90 days</option>
</select>
<select id="usageGroupBy" style="min-width:100px">
<option value="model">By Model</option>
<option value="user">By User</option>
<option value="day">By Day</option>
<option value="provider">By Provider</option>
</select>
<button class="btn-secondary btn-small" id="usageRefreshBtn">Refresh</button>
</div>
<div id="adminUsageTotals"></div>
<div id="adminUsageResults"></div>
<h3 style="margin-top:16px;font-size:14px">Model Pricing</h3>
<div id="adminPricingTable"></div>
</div>
<div class="admin-tab-content" id="adminExtensionsTab" style="display:none">
<div class="admin-section-content" id="adminStorageTab" style="display:none"><div id="adminStorageContent"></div></div>
<div class="admin-section-content" id="adminExtensionsTab" style="display:none">
<div class="admin-toolbar">
<button class="btn-small btn-primary" id="adminInstallExtBtn">+ Install Extension</button>
</div>
@@ -776,9 +754,29 @@
</div>
</div>
</div>
<div class="admin-tab-content" id="adminStorageTab" style="display:none"><div id="adminStorageContent"></div></div>
<div class="admin-tab-content" id="adminStatsTab" style="display:none"><div id="adminStats"></div></div>
<div class="admin-tab-content" id="adminAuditTab" style="display:none">
<!-- Monitoring -->
<div class="admin-section-content" id="adminUsageTab" style="display:none">
<div class="admin-toolbar" style="margin-bottom:12px">
<select id="usagePeriod" style="min-width:100px">
<option value="7d">Last 7 days</option>
<option value="30d" selected>Last 30 days</option>
<option value="90d">Last 90 days</option>
</select>
<select id="usageGroupBy" style="min-width:100px">
<option value="model">By Model</option>
<option value="user">By User</option>
<option value="day">By Day</option>
<option value="provider">By Provider</option>
</select>
<button class="btn-secondary btn-small" id="usageRefreshBtn">Refresh</button>
</div>
<div id="adminUsageTotals"></div>
<div id="adminUsageResults"></div>
<h3 style="margin-top:16px;font-size:14px">Model Pricing</h3>
<div id="adminPricingTable"></div>
</div>
<div class="admin-section-content" id="adminAuditTab" style="display:none">
<div class="admin-toolbar" style="margin-bottom:8px">
<select id="auditFilterAction" style="min-width:140px"><option value="">All actions</option></select>
<select id="auditFilterResource" style="min-width:120px">
@@ -798,7 +796,8 @@
<button class="btn-small" id="auditNextBtn">Next →</button>
</div>
</div>
</div>
<div class="admin-section-content" id="adminStatsTab" style="display:none"><div id="adminStats"></div></div>
</main>
</div>
</div>

View File

@@ -6,7 +6,7 @@
// ── Admin Actions ────────────────────────────
function _adminScroll() { return document.querySelector('#adminModal .modal-body'); }
function _adminScroll() { return document.getElementById('adminMain'); }
function _restoreScroll(el, pos) { if (el) requestAnimationFrame(() => { el.scrollTop = pos; }); }
async function toggleUserActive(id, active) {
try {
@@ -403,10 +403,17 @@ async function settingsDeleteTeamPreset(teamId, presetId, name) {
// ── Admin Listeners (extracted from initListeners) ──
function _initAdminListeners() {
// Admin modal (elements may not exist for non-admin users)
document.getElementById('adminCloseBtn')?.addEventListener('click', UI.closeAdmin);
document.querySelectorAll('.admin-tab').forEach(tab => {
tab.addEventListener('click', () => UI.switchAdminTab(tab.dataset.tab));
// Admin panel navigation (elements may not exist for non-admin users)
document.getElementById('adminBackBtn')?.addEventListener('click', UI.closeAdmin);
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);

View File

@@ -1,14 +1,131 @@
// ==========================================
// Chat Switchboard UI Admin
// ==========================================
// Extends UI with admin modal tabs: users, stats, roles,
// usage, audit, providers, models, presets, teams, settings.
// Fullscreen admin panel with category + section navigation.
// All loadAdmin*() functions unchanged — same endpoints, same DOM IDs.
// ── Category → Section mapping ────────────
const ADMIN_SECTIONS = {
people: ['users', 'teams'],
ai: ['providers', 'models', 'presets', 'roles'],
system: ['settings', 'storage', 'extensions'],
monitoring: ['usage', 'audit', 'stats'],
};
const ADMIN_LABELS = {
users: 'Users', teams: 'Teams',
providers: 'Providers', models: 'Models', presets: 'Personas', roles: 'Roles',
settings: 'Settings', storage: 'Storage', extensions: 'Extensions',
usage: 'Usage', audit: 'Audit', stats: 'Stats',
};
// Section → loader mapping (reuses all existing loadAdmin* functions)
const ADMIN_LOADERS = {
users: () => UI.loadAdminUsers(),
teams: () => UI.loadAdminTeams(),
roles: () => UI.loadAdminRoles(),
providers: () => UI.loadAdminProviders(),
models: () => UI.loadAdminModels(),
presets: () => UI.loadAdminPresets(),
settings: () => UI.loadAdminSettings(),
storage: () => typeof loadAdminStorage === 'function' ? loadAdminStorage() : null,
extensions: () => UI.loadAdminExtensions(),
usage: () => UI.loadAdminUsage(),
audit: () => UI.loadAuditLog(),
stats: () => UI.loadAdminStats(),
};
// Find which category a section belongs to
function _adminCatForSection(section) {
for (const [cat, sections] of Object.entries(ADMIN_SECTIONS)) {
if (sections.includes(section)) return cat;
}
return 'people';
}
Object.assign(UI, {
// ── Admin Modal ──────────────────────────
// ── Admin Panel State ─────────────────────
_adminCat: 'people',
_adminSection: 'users',
openAdmin() { openModal('adminModal'); UI.switchAdminTab('users'); },
closeAdmin() { closeModal('adminModal'); },
// ── 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);
},
closeAdmin() {
document.getElementById('adminPanel').style.display = 'none';
},
// ── Navigate to specific section ──────────
// Called from command palette, deep links, etc.
openAdminSection(section) {
const cat = _adminCatForSection(section);
UI.openAdmin(cat, section);
},
// ── Category switch ───────────────────────
switchAdminCategory(cat) {
UI._adminCat = cat;
UI._adminSection = ADMIN_SECTIONS[cat][0];
UI._renderAdminNav();
UI._switchAdminSection(UI._adminSection);
},
// ── Render navigation state ───────────────
_renderAdminNav() {
// Highlight active category
document.querySelectorAll('.admin-cat').forEach(btn => {
btn.classList.toggle('active', btn.dataset.cat === UI._adminCat);
});
// Populate sidebar
const sidebar = document.getElementById('adminSidebar');
sidebar.innerHTML = ADMIN_SECTIONS[UI._adminCat].map(sec =>
`<button class="${sec === UI._adminSection ? 'active' : ''}" data-section="${sec}">${ADMIN_LABELS[sec]}</button>`
).join('');
// Wire sidebar clicks
sidebar.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => UI._switchAdminSection(btn.dataset.section));
});
},
// ── Section switch (shows content + lazy-loads) ──
async _switchAdminSection(section) {
UI._adminSection = section;
// Update sidebar highlight
document.querySelectorAll('.admin-sidebar button').forEach(btn => {
btn.classList.toggle('active', btn.dataset.section === section);
});
// Hide all, show target
document.querySelectorAll('.admin-section-content').forEach(c => c.style.display = 'none');
const panel = document.getElementById(`admin${section.charAt(0).toUpperCase() + section.slice(1)}Tab`);
if (panel) panel.style.display = '';
// Lazy-load data
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');
@@ -59,26 +176,6 @@ Object.assign(UI, {
if (tab === 'activity') UI.loadTeamAuditLog(1);
},
async switchAdminTab(tab) {
document.querySelectorAll('#adminModal .admin-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
document.querySelectorAll('.admin-tab-content').forEach(c => c.style.display = 'none');
const panel = document.getElementById(`admin${tab.charAt(0).toUpperCase() + tab.slice(1)}Tab`);
if (panel) panel.style.display = '';
if (tab === 'users') await this.loadAdminUsers();
if (tab === 'stats') await this.loadAdminStats();
if (tab === 'audit') await this.loadAuditLog();
if (tab === 'providers') await this.loadAdminProviders();
if (tab === 'models') await this.loadAdminModels();
if (tab === 'presets') await this.loadAdminPresets();
if (tab === 'teams') await this.loadAdminTeams();
if (tab === 'settings') await this.loadAdminSettings();
if (tab === 'roles') await this.loadAdminRoles();
if (tab === 'usage') await this.loadAdminUsage();
if (tab === 'extensions') await this.loadAdminExtensions();
if (tab === 'storage' && typeof loadAdminStorage === 'function') await loadAdminStorage();
},
async loadAdminUsers(quiet) {
const el = document.getElementById('adminUserList');
if (!quiet) el.innerHTML = '<div class="loading">Loading...</div>';

View File

@@ -441,14 +441,14 @@ function renderRoleConfig(containerEl, opts = {}) {
// Reload after short delay so dropdowns reflect saved state
setTimeout(() => refresh(), 500);
} catch (e) {
if (statusEl) { statusEl.textContent = '✗ ' + e.message; statusEl.style.color = 'var(--error)'; }
if (statusEl) { statusEl.textContent = '✗ ' + e.message; statusEl.style.color = 'var(--danger)'; }
}
}
async function handleTest(roleId) {
if (!opts.onTest) return;
const statusEl = containerEl.querySelector(`[data-role-status="${roleId}"]`);
if (statusEl) { statusEl.textContent = 'Testing...'; statusEl.style.color = 'var(--text-muted)'; }
if (statusEl) { statusEl.textContent = 'Testing...'; statusEl.style.color = 'var(--text-3)'; }
try {
const result = await opts.onTest(roleId);
if (statusEl) {
@@ -457,7 +457,7 @@ function renderRoleConfig(containerEl, opts = {}) {
statusEl.style.color = 'var(--success)';
}
} catch (e) {
if (statusEl) { statusEl.textContent = '✗ ' + e.message; statusEl.style.color = 'var(--error)'; }
if (statusEl) { statusEl.textContent = '✗ ' + e.message; statusEl.style.color = 'var(--danger)'; }
}
}

View File

@@ -40,8 +40,8 @@ Object.assign(UI, {
applyAppearance(scale, msgFont) {
const z = scale === 100 ? '' : scale / 100;
// Zoom content areas + modals (but NOT .app or banners)
document.querySelectorAll('.sidebar, .chat-area, .modal-overlay').forEach(el => el.style.zoom = z);
// Zoom content areas + modals + panels (but NOT .app or banners)
document.querySelectorAll('.sidebar, .chat-area, .modal-overlay, .side-panel, .admin-panel').forEach(el => el.style.zoom = z);
const splash = document.getElementById('splashGate');
if (splash) splash.style.zoom = z;
document.documentElement.style.setProperty('--msg-font', msgFont + 'px');

File diff suppressed because it is too large Load Diff