feat: last-used persona cookie, emoji dropdown, theme support, auth status move

- cx_last_persona cookie set on serve_ui; root/login/help/settings
  redirects use preferred persona from cookie instead of alphabetically first
- /api/personas returns [{name, emoji}] objects; persona switcher dropdown
  renders emoji + name with flex layout and .pd-emoji span
- Help, Settings, Model Registry pages apply localStorage theme on load
  (no flash); CSS variables for dark/light replacing all hardcoded hex values
- Claude CLI auth status moved from prominent chat banner to Anthropic
  provider block in Model Registry — live dot indicator (ok/warn/err)
- Auth banner removed from main chat UI (index.html, app.js, style.css)
- Add Model collapsed into Models section as <details> to shorten page
- Light-mode overrides for provider icons, model badges, ctx-badge, tags
  (Anthropic/Google/local colors now readable in both themes)
- Help page gains table, pre/code, hr styles for HELP.md rendered content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-28 22:52:34 -04:00
parent 1222f806ce
commit 66cb197de0
9 changed files with 305 additions and 245 deletions

View File

@@ -7,12 +7,28 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
<script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
<style>
:root {
--pg-bg: #0f1117; --pg-surface: #1a1d27;
--pg-border: #2d3148; --pg-border-deep: #1e2030;
--pg-text: #e2e8f0; --pg-muted: #94a3b8;
--pg-dim: #64748b; --pg-dimmer: #475569;
--pg-bright: #cbd5e1; --pg-nav-hover: rgba(255,255,255,0.05);
}
[data-theme="light"] {
--pg-bg: #f4f2fa; --pg-surface: #ffffff;
--pg-border: #d0c8e8; --pg-border-deep: #d0c8e8;
--pg-text: #1a1228; --pg-muted: #5a5478;
--pg-dim: #7a7290; --pg-dimmer: #9e98b0;
--pg-bright: #1a1228; --pg-nav-hover: rgba(0,0,0,0.05);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh; background: #0f1117;
min-height: 100vh; background: var(--pg-bg);
font-family: 'Inter', system-ui, sans-serif; font-weight: 450;
-webkit-font-smoothing: antialiased; color: #e2e8f0;
-webkit-font-smoothing: antialiased; color: var(--pg-text);
padding: 2rem 1.5rem 4rem;
}
.page { max-width: 700px; margin: 0 auto; }
@@ -22,32 +38,32 @@
.nav-link {
display: inline-flex; align-items: center;
padding: 0.3rem 0.6rem; border-radius: 6px;
font-size: 0.8rem; font-weight: 500; color: #64748b;
font-size: 0.8rem; font-weight: 500; color: var(--pg-dim);
text-decoration: none; transition: color 0.15s, background 0.15s; white-space: nowrap;
}
.nav-link:hover { color: #cbd5e1; background: rgba(255,255,255,0.05); }
.nav-link:hover { color: var(--pg-bright); background: var(--pg-nav-hover); }
.nav-link.active { color: #a78bfa; }
.nav-spacer { flex: 1; min-width: 0.5rem; }
.nav-link.nav-logout { color: #475569; }
.nav-link.nav-logout:hover { color: #94a3b8; background: none; }
.nav-link.nav-logout { color: var(--pg-dimmer); }
.nav-link.nav-logout:hover { color: var(--pg-muted); background: none; }
/* Page header */
.page-header { margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #2d3148; }
.page-header { margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid var(--pg-border); }
.page-header h1 { font-size: 1.4rem; font-weight: 700; color: #a78bfa; }
.page-header p { font-size: 0.82rem; color: #94a3b8; margin-top: 0.25rem; }
.page-header p { font-size: 0.82rem; color: var(--pg-muted); margin-top: 0.25rem; }
/* Section cards */
.section {
background: #1a1d27; border: 1px solid #2d3148;
background: var(--pg-surface); border: 1px solid var(--pg-border);
border-radius: 10px; padding: 1.5rem; margin-bottom: 1.25rem;
}
.section h2 {
font-size: 0.85rem; font-weight: 600; color: #94a3b8;
font-size: 0.85rem; font-weight: 600; color: var(--pg-muted);
text-transform: uppercase; letter-spacing: 0.05em;
margin-bottom: 1.1rem; padding-bottom: 0.5rem;
border-bottom: 1px solid #2d3148;
border-bottom: 1px solid var(--pg-border);
}
.section-note { font-size: 0.8rem; color: #64748b; margin-bottom: 1rem; line-height: 1.5; }
.section-note { font-size: 0.8rem; color: var(--pg-dim); margin-bottom: 1rem; line-height: 1.5; }
/* Provider sub-sections */
.provider-block { margin-bottom: 1.25rem; }
@@ -63,26 +79,44 @@
}
.pi-anthropic { background: #1e1b4b; color: #818cf8; }
.pi-google { background: #042f2e; color: #34d399; }
.provider-title { font-size: 0.9rem; font-weight: 600; color: #e2e8f0; }
.provider-subtitle { font-size: 0.78rem; color: #64748b; }
[data-theme="light"] .pi-anthropic { background: #ede9fe; color: #5b21b6; }
[data-theme="light"] .pi-google { background: #d1fae5; color: #065f46; }
.provider-title { font-size: 0.9rem; font-weight: 600; color: var(--pg-text); }
.provider-subtitle { font-size: 0.78rem; color: var(--pg-dim); }
/* Auth status row */
.auth-status {
display: flex; align-items: center; gap: 0.5rem;
font-size: 0.8rem; margin-top: 0.6rem; color: var(--pg-muted);
}
.auth-status .dot {
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
background: var(--pg-dim);
}
.auth-status.ok .dot { background: #4ade80; }
.auth-status.ok { color: #4ade80; }
.auth-status.warn { color: #fbbf24; }
.auth-status.warn .dot { background: #fbbf24; }
.auth-status.err .dot { background: #f87171; }
.auth-status.err { color: #f87171; }
/* Account rows */
.account-row {
display: flex; align-items: center; justify-content: space-between;
padding: 0.6rem 0.9rem; background: #0f1117;
border: 1px solid #2d3148; border-radius: 8px; margin-bottom: 0.5rem;
padding: 0.6rem 0.9rem; background: var(--pg-bg);
border: 1px solid var(--pg-border); border-radius: 8px; margin-bottom: 0.5rem;
}
.account-label { font-size: 0.88rem; font-weight: 600; color: #e2e8f0; }
.account-hint { font-size: 0.73rem; color: #475569; margin-left: 0.5rem; font-family: monospace; }
.account-label { font-size: 0.88rem; font-weight: 600; color: var(--pg-text); }
.account-hint { font-size: 0.73rem; color: var(--pg-dimmer); margin-left: 0.5rem; font-family: monospace; }
/* Form elements */
.field { margin-bottom: 0.9rem; }
label { display: block; font-size: 0.78rem; font-weight: 500; color: #94a3b8; margin-bottom: 0.35rem; }
label { display: block; font-size: 0.78rem; font-weight: 500; color: var(--pg-muted); margin-bottom: 0.35rem; }
input[type="text"], input[type="password"], input[type="url"],
input[type="number"], select {
width: 100%; padding: 0.6rem 0.8rem;
background: #0f1117; border: 1px solid #2d3148; border-radius: 6px;
color: #e2e8f0; font-size: 0.9rem; font-family: inherit;
background: var(--pg-bg); border: 1px solid var(--pg-border); border-radius: 6px;
color: var(--pg-text); font-size: 0.9rem; font-family: inherit;
outline: none; transition: border-color 0.15s;
}
input:focus, select:focus { border-color: #7c3aed; }
@@ -90,7 +124,7 @@
input[type="number"] { width: 6rem; }
.field-row { display: flex; gap: 0.75rem; }
.field-row .field { flex: 1; margin-bottom: 0; }
.key-status { font-size: 0.75rem; color: #94a3b8; margin-top: 0.35rem; }
.key-status { font-size: 0.75rem; color: var(--pg-muted); margin-top: 0.35rem; }
/* Buttons */
.btn {
@@ -100,29 +134,29 @@
}
.btn-primary { background: #7c3aed; color: #fff; }
.btn-primary:hover { background: #6d28d9; }
.btn-secondary { background: #1a1d27; color: #94a3b8; border: 1px solid #2d3148; }
.btn-secondary:hover { border-color: #94a3b8; color: #e2e8f0; }
.btn-secondary { background: var(--pg-surface); color: var(--pg-muted); border: 1px solid var(--pg-border); }
.btn-secondary:hover { border-color: var(--pg-muted); color: var(--pg-text); }
.btn-sm { padding: 0.35rem 0.7rem; font-size: 0.8rem; font-weight: 500; }
.btn-row { display: flex; gap: 0.6rem; align-items: center; margin-top: 0.75rem; flex-wrap: wrap; }
.btn-link {
background: none; border: none; cursor: pointer; font-family: inherit;
font-size: 0.78rem; color: #64748b; padding: 0;
font-size: 0.78rem; color: var(--pg-dim); padding: 0;
text-decoration: underline; text-underline-offset: 2px;
}
.btn-link:hover { color: #94a3b8; }
.btn-link:hover { color: var(--pg-muted); }
.btn-link.danger { color: #7f1d1d; }
.btn-link.danger:hover { color: #f87171; }
/* Provider tabs */
.ptabs { display: flex; gap: 0; margin-bottom: 1.1rem; border-bottom: 1px solid #2d3148; }
.ptabs { display: flex; gap: 0; margin-bottom: 1.1rem; border-bottom: 1px solid var(--pg-border); }
.ptab {
padding: 0.45rem 0.9rem; font-size: 0.82rem; font-weight: 500;
background: none; border: none; cursor: pointer; color: #64748b;
background: none; border: none; cursor: pointer; color: var(--pg-dim);
border-bottom: 2px solid transparent; margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s; font-family: inherit;
}
.ptab.active { color: #a78bfa; border-bottom-color: #a78bfa; }
.ptab:hover:not(.active) { color: #cbd5e1; }
.ptab:hover:not(.active) { color: var(--pg-bright); }
/* Provider badges on model rows */
.pbadge {
@@ -133,39 +167,44 @@
.pb-anthropic { background: #1e1b4b; color: #818cf8; }
.pb-google { background: #042f2e; color: #34d399; }
.pb-local { background: #1e293b; color: #64748b; }
[data-theme="light"] .pb-anthropic { background: #ede9fe; color: #5b21b6; }
[data-theme="light"] .pb-google { background: #d1fae5; color: #065f46; }
[data-theme="light"] .pb-local { background: #e2e8f0; color: #475569; }
/* Host & model rows */
.host-row {
background: #0f1117; border: 1px solid #2d3148; border-radius: 8px;
background: var(--pg-bg); border: 1px solid var(--pg-border); border-radius: 8px;
padding: 1rem; margin-bottom: 0.75rem;
}
.host-form .field-row { margin-bottom: 0.6rem; }
.fetch-status { font-size: 0.78rem; color: #94a3b8; }
.fetch-status { font-size: 0.78rem; color: var(--pg-muted); }
.fetch-status.ok { color: #4ade80; }
.fetch-status.err { color: #f87171; }
.model-row {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 0.75rem; padding: 0.75rem 0.9rem;
background: #0f1117; border: 1px solid #2d3148; border-radius: 8px;
background: var(--pg-bg); border: 1px solid var(--pg-border); border-radius: 8px;
margin-bottom: 0.5rem;
}
.model-info { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.model-label { font-size: 0.9rem; font-weight: 600; color: #e2e8f0; }
.model-name { font-size: 0.75rem; color: #64748b; font-family: monospace; word-break: break-all; }
.model-host { font-size: 0.72rem; color: #475569; }
.model-label { font-size: 0.9rem; font-weight: 600; color: var(--pg-text); }
.model-name { font-size: 0.75rem; color: var(--pg-dim); font-family: monospace; word-break: break-all; }
.model-host { font-size: 0.72rem; color: var(--pg-dimmer); }
.ctx-badge {
display: inline-block; margin-left: 0.35rem;
padding: 0.1rem 0.3rem; border-radius: 3px;
background: #1e293b; color: #64748b; font-size: 0.65rem; font-weight: 600;
vertical-align: middle;
}
[data-theme="light"] .ctx-badge { background: #e2e8f0; color: #475569; }
.tag-row { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.2rem; }
.tag { padding: 0.1rem 0.4rem; border-radius: 3px; background: #1e1b4b; color: #818cf8; font-size: 0.68rem; font-weight: 500; }
[data-theme="light"] .tag { background: #ede9fe; color: #5b21b6; }
.row-btn {
padding: 0.3rem 0.65rem; border-radius: 5px; font-size: 0.78rem;
font-weight: 500; cursor: pointer; font-family: inherit;
border: 1px solid #2d3148; background: #1a1d27; color: #94a3b8;
border: 1px solid var(--pg-border); background: var(--pg-surface); color: var(--pg-muted);
transition: border-color 0.15s, color 0.15s; flex-shrink: 0;
}
.row-btn.danger { color: #f87171; }
@@ -174,17 +213,17 @@
/* Role assignments */
.role-row {
display: flex; align-items: flex-start; gap: 1rem;
padding: 0.6rem 0; border-bottom: 1px solid #1e2030;
padding: 0.6rem 0; border-bottom: 1px solid var(--pg-border-deep);
}
.role-row:last-child { border-bottom: none; }
.role-name { font-size: 0.82rem; font-weight: 600; color: #a78bfa; min-width: 6rem; padding-top: 0.45rem; }
.role-slots { display: flex; flex-wrap: wrap; gap: 0.5rem; flex: 1; }
.role-slot { display: flex; flex-direction: column; gap: 0.2rem; flex: 1; min-width: 8rem; }
.slot-label { font-size: 0.68rem; color: #475569; font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; }
.slot-label { font-size: 0.68rem; color: var(--pg-dimmer); font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; }
.role-select {
padding: 0.4rem 0.6rem; font-size: 0.8rem;
background: #0f1117; border: 1px solid #2d3148; border-radius: 6px;
color: #e2e8f0; font-family: inherit; cursor: pointer; outline: none;
background: var(--pg-bg); border: 1px solid var(--pg-border); border-radius: 6px;
color: var(--pg-text); font-family: inherit; cursor: pointer; outline: none;
transition: border-color 0.15s;
}
.role-select:focus { border-color: #7c3aed; }
@@ -194,7 +233,7 @@
/* Model select picker */
#model-select-wrap { display: none; margin-bottom: 0.75rem; }
.tags-hint { font-size: 0.72rem; color: #475569; margin-top: 0.3rem; }
.tags-hint { font-size: 0.72rem; color: var(--pg-dimmer); margin-top: 0.3rem; }
/* Messages & Toast */
.msg { font-size: 0.85rem; text-align: center; padding: 0.6rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
@@ -202,15 +241,15 @@
.msg.error { color: #f87171; background: #2d0a0a; border: 1px solid #7f1d1d; }
#toast {
position: fixed; bottom: 1.5rem; right: 1.5rem;
background: #1a1d27; border: 1px solid #166534; color: #4ade80;
background: var(--pg-surface); border: 1px solid #166534; color: #4ade80;
padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.82rem;
opacity: 0; transition: opacity 0.2s; pointer-events: none; z-index: 100;
}
#toast.show { opacity: 1; }
#toast.err { border-color: #7f1d1d; color: #f87171; }
.empty-note { font-size: 0.85rem; color: #475569; padding: 0.3rem 0; }
details summary { font-size: 0.82rem; color: #64748b; cursor: pointer; user-select: none; margin-top: 0.75rem; }
.empty-note { font-size: 0.85rem; color: var(--pg-dimmer); padding: 0.3rem 0; }
details summary { font-size: 0.82rem; color: var(--pg-dim); cursor: pointer; user-select: none; margin-top: 0.75rem; }
details > div { margin-top: 0.75rem; }
</style>
</head>
@@ -248,6 +287,9 @@
Claude models are accessed through the Claude CLI using your existing OAuth login.
Run <code style="font-family:monospace;color:#94a3b8">claude auth login</code> to authenticate.
</p>
<div id="claude-auth-status" class="auth-status" style="margin-top:0.6rem">
<span class="dot"></span><span id="claude-auth-msg">Checking…</span>
</div>
</div>
<div class="provider-block" style="border-top:1px solid #2d3148; padding-top:1.25rem">
@@ -335,11 +377,10 @@
<div class="section">
<h2>Models</h2>
{{ model_rows }}
</div>
<!-- ── Add Model ── -->
<div class="section">
<h2>Add Model</h2>
<details>
<summary>+ Add model</summary>
<div>
<div class="ptabs" id="provider-tabs">
<button type="button" class="ptab active" data-p="local">Local</button>
@@ -427,6 +468,9 @@
<span id="fetch-status" class="fetch-status"></span>
</div>
</form>
</div>
</details>
</div>
<!-- ── Role Assignments ── -->
@@ -625,6 +669,28 @@
// Hide fetch button initially if no hosts
if (!HAS_HOSTS) fetchBtn.style.display = 'none';
// ── Claude CLI auth status ─────────────────────────────────────────────
(async function() {
const el = document.getElementById('claude-auth-status');
const msg = document.getElementById('claude-auth-msg');
if (!el || !msg) return;
try {
const d = await fetch('/auth/status').then(r => r.json());
const c = d.claude;
if (!c) return;
if (c.expired) {
el.className = 'auth-status err';
msg.textContent = 'Token expired — run claude auth login on the Cortex host, then restart Cortex.';
} else if (c.warning) {
el.className = 'auth-status warn';
msg.textContent = `Token expires in ${c.access_token_hours_remaining}h — run claude auth login to refresh.`;
} else {
el.className = 'auth-status ok';
msg.textContent = `Authenticated — token valid for ${c.access_token_hours_remaining}h`;
}
} catch { msg.textContent = 'Status unavailable'; }
})();
</script>
</body>
</html>