feat: persona onboarding — invite tokens, self-service setup, persona creation, switcher
New user flow:
1. Admin: python manage_passwords.py invite <username> → generates URL
2. User visits /setup/<token> → sets own password → logged in
3. User redirected to /setup/persona → fills name/emoji/description
4. persona_template.py generates all starter files → lands at /{user}/{persona}
Multiple personas:
- Header persona name is now a clickable dropdown listing all personas
- "New persona" link at bottom → /setup/persona (available to logged-in users)
- /api/personas endpoint returns persona list for current session user
New files:
- persona_template.py: generates IDENTITY/SOUL/PROTOCOLS/USER/HELP.md + data files
- routers/onboarding.py: /setup/{token}, /setup/persona GET+POST
- static/setup.html: two-step form (password → persona), emoji picker, mobile-friendly
Updated:
- auth_utils.py: create_invite(), validate_invite(), consume_invite()
- manage_passwords.py: invite command with URL output
- auth_middleware.py: /setup/* prefix is public (invite tokens need no auth)
- routers/ui.py: /api/personas endpoint; post-login redirect if no personas
- static/app.js: persona switcher dropdown with navigation + Add persona link
- static/style.css: .persona-switcher, .persona-dropdown, mobile adjustments
Mobile: login/setup pages are card-centered with responsive padding;
dropdown avoids edge-clipping on narrow screens; logout button stays visible.
All 80 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -137,13 +137,58 @@
|
||||
updateInputMode();
|
||||
});
|
||||
|
||||
// ── Persona name in header ───────────────────────────────────
|
||||
const personaNameEl = document.getElementById('persona-name');
|
||||
// ── Persona name + switcher ──────────────────────────────────
|
||||
const personaNameEl = document.getElementById('persona-name');
|
||||
const personaDropEl = document.getElementById('persona-dropdown');
|
||||
const personaSwitcher = document.getElementById('persona-switcher');
|
||||
|
||||
if (personaNameEl && CORTEX_PERSONA) {
|
||||
// Capitalize first letter
|
||||
personaNameEl.textContent = CORTEX_PERSONA.charAt(0).toUpperCase() + CORTEX_PERSONA.slice(1);
|
||||
}
|
||||
|
||||
// Load persona list and build dropdown
|
||||
async function loadPersonaSwitcher() {
|
||||
try {
|
||||
const res = await fetch('/api/personas');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const personas = data.personas || [];
|
||||
if (personas.length === 0) return;
|
||||
|
||||
personaDropEl.innerHTML = '';
|
||||
|
||||
personas.forEach(p => {
|
||||
const a = document.createElement('a');
|
||||
a.href = `/${CORTEX_USER}/${p}`;
|
||||
a.textContent = p.charAt(0).toUpperCase() + p.slice(1);
|
||||
if (p === CORTEX_PERSONA) a.classList.add('active');
|
||||
personaDropEl.appendChild(a);
|
||||
});
|
||||
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'pd-divider';
|
||||
personaDropEl.appendChild(divider);
|
||||
|
||||
const addLink = document.createElement('a');
|
||||
addLink.href = '/setup/persona';
|
||||
addLink.className = 'pd-add';
|
||||
addLink.textContent = '+ New persona';
|
||||
personaDropEl.appendChild(addLink);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
loadPersonaSwitcher();
|
||||
|
||||
// Toggle dropdown on click
|
||||
if (personaSwitcher) {
|
||||
personaSwitcher.addEventListener('click', (e) => {
|
||||
if (personaDropEl.children.length === 0) return;
|
||||
personaDropEl.classList.toggle('open');
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('click', () => personaDropEl.classList.remove('open'));
|
||||
}
|
||||
|
||||
// ── Backend toggle ───────────────────────────────────────────
|
||||
|
||||
fetch('/backend').then(r => r.json()).then(d => setBackendUI(d.primary));
|
||||
|
||||
Reference in New Issue
Block a user