Files
Cortex-Inara/cortex/static/settings.html
Scott Idem 8baab874f1 feat: replace backend/slot toggle with role selector
The backend toggle now cycles through configured roles (chat, coder,
research, distill, etc.) instead of backup model slots within the chat
role. Each role uses its own primary→backup chain from the registry.

- ChatRequest.slot replaced by chat_role (default "chat")
- GET /backend returns available_roles instead of chat_models
- _available_roles_for_toggle() builds list from defined_roles, excluding
  orchestrator (which has its own Agent mode)
- Model label on responses now reflects the actual role's assigned model
- Toggle is inert when only one role is configured (avoids useless cycling)
- Add "Clear browser cache" button to Account Settings (Connected Accounts)
- Add _role_model_label() helper for cleaner response tag labeling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:23:18 -04:00

398 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cortex — Account Settings</title>
<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">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #0f1117;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-weight: 450;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #e2e8f0;
padding: 1.5rem;
}
.card {
background: #1a1d27;
border: 1px solid #2d3148;
border-radius: 12px;
padding: 2.5rem 2rem;
width: 100%;
max-width: 480px;
}
.page-nav {
display: flex;
align-items: center;
gap: 0.25rem;
margin-bottom: 1.75rem;
flex-wrap: wrap;
}
.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;
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.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; }
.logo {
margin-bottom: 1.75rem;
}
.logo h1 { font-size: 1.4rem; font-weight: 700; color: #a78bfa; }
.logo p { font-size: 0.8rem; color: #94a3b8; margin-top: 0.2rem; }
h2 {
font-size: 0.9rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 1rem;
padding-bottom: 0.4rem;
border-bottom: 1px solid #2d3148;
}
.section { margin-bottom: 2rem; }
label {
display: block;
font-size: 0.8rem;
font-weight: 500;
color: #94a3b8;
margin-bottom: 0.4rem;
}
input {
width: 100%;
padding: 0.65rem 0.85rem;
background: #0f1117;
border: 1px solid #2d3148;
border-radius: 6px;
color: #e2e8f0;
font-size: 0.95rem;
outline: none;
transition: border-color 0.15s;
}
input:focus { border-color: #7c3aed; }
input[readonly] { color: #94a3b8; cursor: default; }
.field { margin-bottom: 1rem; }
button[type="submit"] {
width: 100%;
padding: 0.7rem;
margin-top: 0.25rem;
background: #7c3aed;
border: none;
border-radius: 6px;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
button[type="submit"]:hover { background: #6d28d9; }
.error {
color: #f87171;
font-size: 0.85rem;
text-align: center;
margin-bottom: 1rem;
}
.success {
color: #4ade80;
font-size: 0.85rem;
text-align: center;
margin-bottom: 1rem;
}
.persona-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
.persona-list li {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.persona-link {
display: inline-block;
padding: 0.3rem 0.75rem;
background: #0f1117;
border: 1px solid #2d3148;
border-radius: 20px;
color: #a78bfa;
font-size: 0.85rem;
text-decoration: none;
transition: border-color 0.15s;
}
.persona-link:hover { border-color: #7c3aed; }
.persona-list li em { color: #94a3b8; font-size: 0.85rem; }
.persona-rename-toggle {
background: none;
border: none;
color: #94a3b8;
font-size: 0.85rem;
cursor: pointer;
padding: 0.2rem 0.4rem;
border-radius: 4px;
opacity: 0.7;
transition: opacity 0.15s, color 0.15s;
}
.persona-rename-toggle:hover { opacity: 1; color: #a78bfa; }
.persona-rename-form { display: flex; align-items: center; gap: 0.4rem; }
.persona-rename-form input[type="text"] {
width: 12rem;
padding: 0.3rem 0.6rem;
background: #0f1117;
border: 1px solid #7c3aed;
border-radius: 6px;
color: #e2e8f0;
font-size: 0.9rem;
outline: none;
}
.persona-rename-form button[type="submit"] {
width: auto;
padding: 0.3rem 0.75rem;
font-size: 0.85rem;
margin-top: 0;
}
.persona-rename-cancel {
background: none;
border: 1px solid #2d3148;
border-radius: 6px;
color: #94a3b8;
font-size: 0.85rem;
padding: 0.3rem 0.6rem;
cursor: pointer;
}
.persona-rename-cancel:hover { border-color: #94a3b8; color: #e2e8f0; }
.add-persona {
display: inline-block;
margin-top: 0.75rem;
font-size: 0.8rem;
color: #94a3b8;
text-decoration: none;
}
.add-persona:hover { color: #a78bfa; }
</style>
</head>
<body>
<div class="card">
<nav class="page-nav">
<a href="{{ back_href }}" class="nav-link">← Chat</a>
<a href="{{ help_href }}" class="nav-link">Help</a>
<a href="/settings" class="nav-link active">Settings</a>
<span class="nav-spacer"></span>
<a href="/logout" class="nav-link nav-logout">Sign out</a>
</nav>
<div class="logo">
<h1>Account Settings</h1>
<p>Manage your account and personas.</p>
</div>
<!-- SUCCESS -->
<!-- ERROR -->
<!-- Account info -->
<div class="section">
<h2>Account</h2>
<div class="field">
<label>Username</label>
<input type="text" value="{{ username }}" readonly>
</div>
<button type="button" id="show-rename-user" class="persona-rename-toggle"
style="opacity:0.7; font-size:0.8rem; padding:0.3rem 0.6rem; border:1px solid #2d3148; border-radius:6px; margin-top:0.25rem;">
✏ Change username
</button>
<form id="rename-user-form" method="POST" action="/settings/username"
style="display:none; margin-top:0.75rem;">
<div class="field">
<label for="new_username">New username</label>
<input type="text" id="new_username" name="new_username"
value="{{ username }}"
pattern="[a-z_][a-z0-9_\-]{0,31}" required autofocus
autocomplete="off" data-form-type="other">
<p style="font-size:0.75rem; color:#94a3b8; margin-top:0.3rem;">
Lowercase letters, digits, _ or - only. You will be logged out after renaming.
</p>
</div>
<div style="display:flex; gap:0.5rem;">
<button type="submit" style="flex:1; padding:0.5rem; background:#7c3aed; border:none; border-radius:6px; color:#fff; font-size:0.9rem; font-weight:600; cursor:pointer;">Save</button>
<button type="button" id="cancel-rename-user"
style="padding:0.5rem 0.9rem; background:none; border:1px solid #2d3148; border-radius:6px; color:#94a3b8; font-size:0.9rem; cursor:pointer;">Cancel</button>
</div>
</form>
</div>
<!-- Connected accounts -->
<div class="section">
<h2>Connected Accounts</h2>
<div class="field">
<label>Google Account</label>
<input type="text" value="{{ google_email }}" readonly
placeholder="No Google account linked"
style="{{ google_email == '' and 'color:#475569' or '' }}">
</div>
<p style="font-size:0.75rem; color:#94a3b8; margin-top:-0.5rem;">
To link or change your Google account, contact Scott.
</p>
</div>
<!-- Browser cache -->
<div class="section">
<h2>Browser Cache</h2>
<p style="font-size:0.8rem; color:#94a3b8; margin-bottom:0.85rem; line-height:1.55;">
Clears UI preferences stored in this browser: active mode, session ID, memory toggles,
theme, font size, and context tier. Does not sign you out.
</p>
<button type="button" id="clear-ls-btn"
style="padding:0.5rem 1rem; background:none; border:1px solid #2d3148; border-radius:6px;
color:#94a3b8; font-size:0.88rem; font-weight:500; cursor:pointer;
transition:border-color 0.15s, color 0.15s;">
Clear browser cache
</button>
<span id="clear-ls-ok" style="display:none; margin-left:0.75rem; font-size:0.8rem; color:#4ade80;">
Cleared.
</span>
</div>
<!-- Model Registry link -->
<div class="section">
<h2>Model Registry</h2>
<p style="font-size:0.8rem; color:#94a3b8; margin-bottom:0.85rem; line-height:1.55;">
Configure AI providers (Anthropic, Google), local hosts (Open WebUI, Ollama, OpenRouter, etc.),
and assign models to roles — chat, orchestrator, distill, and more.
</p>
<a href="/settings/models"
style="display:inline-block; padding:0.55rem 1rem; background:#7c3aed; border-radius:6px;
color:#fff; font-size:0.88rem; font-weight:600; text-decoration:none;
transition:background 0.15s;">
Manage models →
</a>
</div>
<!-- Change password -->
<div class="section">
<h2>Change Password</h2>
<form method="POST" action="/settings/password" id="password-form">
<div class="field">
<label for="current_password">Current password</label>
<input type="password" id="current_password" name="current_password"
autocomplete="current-password" required>
</div>
<div class="field">
<label for="new_password">New password</label>
<input type="password" id="new_password" name="new_password"
autocomplete="new-password" required minlength="8">
</div>
<div class="field">
<label for="confirm_password">Confirm new password</label>
<input type="password" id="confirm_password" name="confirm_password"
autocomplete="new-password" required>
</div>
<button type="submit">Update password</button>
</form>
</div>
<!-- Personas -->
<div class="section">
<h2>Personas</h2>
<ul class="persona-list">
{{ persona_items }}
</ul>
<a href="/setup/persona" class="add-persona">+ Add new persona</a>
</div>
</div>
<script>
// Password confirmation check
document.getElementById('password-form').addEventListener('submit', e => {
const np = document.getElementById('new_password').value;
const cfm = document.getElementById('confirm_password').value;
if (np !== cfm) {
e.preventDefault();
alert('New passwords do not match.');
}
});
// Username rename toggle
document.getElementById('show-rename-user').addEventListener('click', () => {
document.getElementById('show-rename-user').style.display = 'none';
document.getElementById('rename-user-form').style.display = 'block';
document.getElementById('new_username').focus();
});
document.getElementById('cancel-rename-user').addEventListener('click', () => {
document.getElementById('rename-user-form').style.display = 'none';
document.getElementById('show-rename-user').style.display = '';
});
// Gemini key — "remove" link clears the input and submits the form
const geminiRemove = document.getElementById('gemini-remove-link');
if (geminiRemove) {
geminiRemove.addEventListener('click', e => {
e.preventDefault();
document.getElementById('gemini_api_key').value = '';
document.querySelector('form[action="/settings/gemini-key"]').submit();
});
}
// Clear localStorage (keeps JWT cookie — no sign-out)
document.getElementById('clear-ls-btn').addEventListener('click', () => {
localStorage.clear();
document.getElementById('clear-ls-ok').style.display = 'inline';
});
// Persona rename toggle
document.querySelectorAll('.persona-rename-toggle').forEach(btn => {
btn.addEventListener('click', () => {
const p = btn.dataset.persona;
const form = document.querySelector(`.persona-rename-form[data-persona="${p}"]`);
btn.style.display = 'none';
form.style.display = 'flex';
form.querySelector('input[type="text"]').focus();
});
});
document.querySelectorAll('.persona-rename-cancel').forEach(btn => {
btn.addEventListener('click', () => {
const form = btn.closest('.persona-rename-form');
const p = form.dataset.persona;
const toggle = document.querySelector(`.persona-rename-toggle[data-persona="${p}"]`);
form.style.display = 'none';
toggle.style.display = '';
});
});
</script>
</body>
</html>