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:
@@ -21,6 +21,9 @@ router = APIRouter()
|
||||
_STATIC = Path(__file__).parent.parent / "static"
|
||||
|
||||
|
||||
_LAST_PERSONA_COOKIE = "cx_last_persona"
|
||||
|
||||
|
||||
def _get_session_user(request: Request) -> str | None:
|
||||
token = request.cookies.get(COOKIE_NAME)
|
||||
if not token:
|
||||
@@ -31,6 +34,16 @@ def _get_session_user(request: Request) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _preferred_persona(request: Request, username: str) -> str:
|
||||
names = list_user_personas(username)
|
||||
if not names:
|
||||
return ""
|
||||
cookie_val = request.cookies.get(_LAST_PERSONA_COOKIE, "")
|
||||
if cookie_val in names:
|
||||
return cookie_val
|
||||
return names[0]
|
||||
|
||||
|
||||
@router.get("/help", include_in_schema=False)
|
||||
async def help_page(request: Request, persona: str = ""):
|
||||
username = _get_session_user(request)
|
||||
@@ -38,11 +51,11 @@ async def help_page(request: Request, persona: str = ""):
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
personas = list_user_personas(username)
|
||||
# Use persona from query param if valid, else fall back to first
|
||||
# Use persona from query param if valid, else prefer last-visited from cookie
|
||||
if persona and persona in personas:
|
||||
back_persona = persona
|
||||
else:
|
||||
back_persona = personas[0] if personas else ""
|
||||
back_persona = _preferred_persona(request, username)
|
||||
back_href = f"/{username}/{back_persona}" if back_persona else "/"
|
||||
|
||||
html = (_STATIC / "help.html").read_text()
|
||||
|
||||
@@ -28,6 +28,9 @@ router = APIRouter()
|
||||
_STATIC = Path(__file__).parent.parent / "static"
|
||||
|
||||
|
||||
_LAST_PERSONA_COOKIE = "cx_last_persona"
|
||||
|
||||
|
||||
def _get_session_user(request: Request) -> str | None:
|
||||
token = request.cookies.get(COOKIE_NAME)
|
||||
if not token:
|
||||
@@ -38,7 +41,17 @@ def _get_session_user(request: Request) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _settings_page(username: str, personas: list[str], success: str = "", error: str = "") -> str:
|
||||
def _preferred_persona(request: Request, username: str) -> str:
|
||||
names = list_user_personas(username)
|
||||
if not names:
|
||||
return ""
|
||||
cookie_val = request.cookies.get(_LAST_PERSONA_COOKIE, "")
|
||||
if cookie_val in names:
|
||||
return cookie_val
|
||||
return names[0]
|
||||
|
||||
|
||||
def _settings_page(username: str, personas: list[str], back_persona: str = "", success: str = "", error: str = "") -> str:
|
||||
html = (_STATIC / "settings.html").read_text()
|
||||
html = html.replace("{{ username }}", username)
|
||||
|
||||
@@ -62,7 +75,8 @@ def _settings_page(username: str, personas: list[str], success: str = "", error:
|
||||
</li>''' for p in personas
|
||||
)
|
||||
html = html.replace("{{ persona_items }}", persona_items or "<li><em>No personas yet.</em></li>")
|
||||
back_persona = personas[0] if personas else ""
|
||||
if not back_persona:
|
||||
back_persona = personas[0] if personas else ""
|
||||
html = html.replace("{{ back_href }}", f"/{username}/{back_persona}" if back_persona else "/")
|
||||
html = html.replace("{{ help_href }}", f"/help?persona={back_persona}" if back_persona else "/help")
|
||||
if success:
|
||||
@@ -78,7 +92,8 @@ async def settings_page(request: Request):
|
||||
if not username:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
personas = list_user_personas(username)
|
||||
return HTMLResponse(_settings_page(username, personas))
|
||||
back_persona = _preferred_persona(request, username)
|
||||
return HTMLResponse(_settings_page(username, personas, back_persona=back_persona))
|
||||
|
||||
|
||||
@router.post("/settings/password", include_in_schema=False)
|
||||
@@ -93,19 +108,20 @@ async def change_password(
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
personas = list_user_personas(username)
|
||||
back_persona = _preferred_persona(request, username)
|
||||
|
||||
if not check_credentials(username, current_password):
|
||||
return HTMLResponse(_settings_page(username, personas, error="Current password is incorrect."))
|
||||
return HTMLResponse(_settings_page(username, personas, back_persona, error="Current password is incorrect."))
|
||||
|
||||
if len(new_password) < 8:
|
||||
return HTMLResponse(_settings_page(username, personas, error="New password must be at least 8 characters."))
|
||||
return HTMLResponse(_settings_page(username, personas, back_persona, error="New password must be at least 8 characters."))
|
||||
|
||||
if new_password != confirm_password:
|
||||
return HTMLResponse(_settings_page(username, personas, error="New passwords do not match."))
|
||||
return HTMLResponse(_settings_page(username, personas, back_persona, error="New passwords do not match."))
|
||||
|
||||
set_password(username, new_password)
|
||||
logger.info("password changed: %s", username)
|
||||
return HTMLResponse(_settings_page(username, personas, success="Password updated successfully."))
|
||||
return HTMLResponse(_settings_page(username, personas, back_persona, success="Password updated successfully."))
|
||||
|
||||
|
||||
@router.post("/settings/username", include_in_schema=False)
|
||||
@@ -118,11 +134,12 @@ async def rename_username(
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
personas = list_user_personas(username)
|
||||
back_persona = _preferred_persona(request, username)
|
||||
new_username = new_username.strip().lower()
|
||||
|
||||
if not _SLUG_RE.match(new_username):
|
||||
return HTMLResponse(_settings_page(
|
||||
username, personas,
|
||||
username, personas, back_persona,
|
||||
error="Invalid username. Use lowercase letters, digits, _ or - only."))
|
||||
|
||||
if new_username == username:
|
||||
@@ -134,7 +151,7 @@ async def rename_username(
|
||||
|
||||
if new_dir.exists():
|
||||
return HTMLResponse(_settings_page(
|
||||
username, personas,
|
||||
username, personas, back_persona,
|
||||
error=f"Username '{new_username}' is already taken."))
|
||||
|
||||
old_dir.rename(new_dir)
|
||||
@@ -156,6 +173,7 @@ async def save_gemini_key(
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
personas = list_user_personas(username)
|
||||
back_persona = _preferred_persona(request, username)
|
||||
gemini_api_key = gemini_api_key.strip()
|
||||
|
||||
data = _read_auth(username)
|
||||
@@ -167,7 +185,7 @@ async def save_gemini_key(
|
||||
msg = "Gemini API key removed — using server key."
|
||||
_write_auth(username, data)
|
||||
logger.info("gemini key updated: %s", username)
|
||||
return HTMLResponse(_settings_page(username, personas, success=msg))
|
||||
return HTMLResponse(_settings_page(username, personas, back_persona, success=msg))
|
||||
|
||||
|
||||
@router.post("/settings/persona/rename", include_in_schema=False)
|
||||
@@ -181,11 +199,12 @@ async def rename_persona(
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
personas = list_user_personas(username)
|
||||
back_persona = _preferred_persona(request, username)
|
||||
new_name = new_name.strip().lower()
|
||||
|
||||
if not _SLUG_RE.match(new_name):
|
||||
return HTMLResponse(_settings_page(
|
||||
username, personas,
|
||||
username, personas, back_persona,
|
||||
error="Invalid name. Use lowercase letters, digits, _ or - only."))
|
||||
|
||||
if new_name == old_name:
|
||||
@@ -196,11 +215,11 @@ async def rename_persona(
|
||||
new_dir = persona_root / new_name
|
||||
|
||||
if not old_dir.exists():
|
||||
return HTMLResponse(_settings_page(username, personas, error=f"Persona '{old_name}' not found."))
|
||||
return HTMLResponse(_settings_page(username, personas, back_persona, error=f"Persona '{old_name}' not found."))
|
||||
|
||||
if new_dir.exists():
|
||||
return HTMLResponse(_settings_page(
|
||||
username, personas,
|
||||
username, personas, back_persona,
|
||||
error=f"A persona named '{new_name}' already exists."))
|
||||
|
||||
old_dir.rename(new_dir)
|
||||
|
||||
@@ -56,12 +56,26 @@ def _set_cookie(response: Response, username: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
_LAST_PERSONA_COOKIE = "cx_last_persona"
|
||||
|
||||
|
||||
def _first_persona(username: str) -> str | None:
|
||||
"""Return the first available persona for a user, or None."""
|
||||
names = list_user_personas(username)
|
||||
return names[0] if names else None
|
||||
|
||||
|
||||
def _preferred_persona(request: Request, username: str) -> str | None:
|
||||
"""Return the last-visited persona from cookie if valid, else the first available."""
|
||||
names = list_user_personas(username)
|
||||
if not names:
|
||||
return None
|
||||
cookie_val = request.cookies.get(_LAST_PERSONA_COOKIE, "")
|
||||
if cookie_val in names:
|
||||
return cookie_val
|
||||
return names[0]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Favicon — default sparkle; persona pages override via JS
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -85,7 +99,7 @@ async def root(request: Request):
|
||||
user = _get_session_user(request)
|
||||
if not user:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
persona = _first_persona(user)
|
||||
persona = _preferred_persona(request, user)
|
||||
if not persona:
|
||||
return HTMLResponse("<h1>No personas configured for your account.</h1>", status_code=500)
|
||||
return RedirectResponse(f"/{user}/{persona}", status_code=302)
|
||||
@@ -100,7 +114,7 @@ async def login_page(request: Request):
|
||||
user = _get_session_user(request)
|
||||
if user:
|
||||
# Already logged in — redirect home
|
||||
persona = _first_persona(user)
|
||||
persona = _preferred_persona(request, user)
|
||||
if persona:
|
||||
return RedirectResponse(f"/{user}/{persona}", status_code=302)
|
||||
return HTMLResponse((_STATIC / "login.html").read_text())
|
||||
@@ -254,7 +268,16 @@ async def api_personas(request: Request) -> dict:
|
||||
if not user:
|
||||
from fastapi import HTTPException
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
return {"user": user, "personas": list_user_personas(user)}
|
||||
personas_with_emoji = []
|
||||
for p in list_user_personas(user):
|
||||
emoji = "✨"
|
||||
identity_path = persona_path(user, p) / "IDENTITY.md"
|
||||
if identity_path.exists():
|
||||
m = re.search(r"\|\s*Emoji\s*\|\s*(.+?)\s*\|", identity_path.read_text())
|
||||
if m:
|
||||
emoji = m.group(1).strip()
|
||||
personas_with_emoji.append({"name": p, "emoji": emoji})
|
||||
return {"user": user, "personas": personas_with_emoji}
|
||||
|
||||
|
||||
@router.get("/{username}/{persona}", include_in_schema=False)
|
||||
@@ -288,4 +311,6 @@ async def serve_ui(username: str, persona: str, request: Request):
|
||||
f'{{user: "{username}", persona: "{persona}", emoji: "{emoji}"}};</script>'
|
||||
)
|
||||
html = html.replace("</head>", f"{config_tag}\n</head>", 1)
|
||||
return HTMLResponse(html)
|
||||
resp = HTMLResponse(html)
|
||||
resp.set_cookie(_LAST_PERSONA_COOKIE, persona, max_age=365 * 86400, httponly=False, samesite="lax")
|
||||
return resp
|
||||
|
||||
Reference in New Issue
Block a user