09cfde4de8
A blank composer is the least helpful thing a chat client can show someone who has just installed one. Three cards now sit under the empty state, and an administrator manages them at /admin/suggestions. Clicking a card fills the composer and stops there. It deliberately does not send: every default ends mid-sentence, because a card is a starting point rather than a question somebody already asked, and the caret lands where the person has to start typing. Seeding is guarded by a settings flag, not by "is the table empty" -- otherwise an administrator who decided against them would get all three back on every restart. Capped at twelve, six shown: past a dozen this is a menu, and a menu on the empty screen is a worse blank page than a blank page. The cards are gated on there being no chat at all, not on the thread being empty. An empty chat someone opened on purpose already has a model and a prompt chosen. Also fixes a pre-existing bug the position test caught. Both this and _refresh_models wrote `coalesce(max(position), -1) or -1`, and position 0 is falsy -- so the second row landed back on 0 on top of the first. The coalesce was already doing that job; the `or` was undoing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
"""Administration for the cards offered on the new-chat screen."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Form, HTTPException, Request, Response, status
|
|
from fastapi.responses import RedirectResponse
|
|
|
|
from lembas.api.deps import AdminUser, Db
|
|
from lembas.db.models import Suggestion
|
|
from lembas.services import suggestions as suggestions_service
|
|
from lembas.web.templating import render
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/admin/suggestions", tags=["admin-suggestions"])
|
|
|
|
|
|
def _suggestion(db: Db, suggestion_id: str) -> Suggestion:
|
|
suggestion = db.get(Suggestion, suggestion_id)
|
|
if suggestion is None:
|
|
raise HTTPException(status.HTTP_404_NOT_FOUND, "That suggestion no longer exists.")
|
|
return suggestion
|
|
|
|
|
|
def _back(message: str = "") -> Response:
|
|
target = f"/admin/suggestions?saved={message}" if message else "/admin/suggestions"
|
|
return RedirectResponse(target, status_code=status.HTTP_303_SEE_OTHER)
|
|
|
|
|
|
@router.get("")
|
|
async def suggestions_page(request: Request, db: Db, user: AdminUser, saved: str = ""):
|
|
rows = suggestions_service.all_of_them(db)
|
|
return render(
|
|
request,
|
|
"admin/suggestions.html",
|
|
{
|
|
"suggestions": rows,
|
|
"at_limit": len(rows) >= suggestions_service.MAX_SUGGESTIONS,
|
|
"max_suggestions": suggestions_service.MAX_SUGGESTIONS,
|
|
"max_shown": suggestions_service.MAX_SHOWN,
|
|
"saved": saved,
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("")
|
|
async def create_suggestion(
|
|
db: Db,
|
|
user: AdminUser,
|
|
name: str = Form(""),
|
|
description: str = Form(""),
|
|
prompt: str = Form(""),
|
|
) -> Response:
|
|
name = name.strip()
|
|
if not name:
|
|
return _back("A suggestion needs a name.")
|
|
if len(suggestions_service.all_of_them(db)) >= suggestions_service.MAX_SUGGESTIONS:
|
|
return _back(f"That is already {suggestions_service.MAX_SUGGESTIONS}, which is plenty.")
|
|
|
|
suggestions_service.create(db, name=name, description=description, prompt=prompt)
|
|
log.info("%s added suggestion %s", user.email, name)
|
|
return _back(f"Added {name}.")
|
|
|
|
|
|
# Registered before /{suggestion_id}: FastAPI matches in registration order, so
|
|
# with the parameterised route first any literal segment added later would be
|
|
# captured as an id. That has already been a bug once, in /admin/models.
|
|
@router.post("/{suggestion_id}/delete")
|
|
async def delete_suggestion(db: Db, user: AdminUser, suggestion_id: str) -> Response:
|
|
suggestion = _suggestion(db, suggestion_id)
|
|
name = suggestion.name
|
|
db.delete(suggestion)
|
|
db.commit()
|
|
log.info("%s deleted suggestion %s", user.email, name)
|
|
return _back(f"Deleted {name}.")
|
|
|
|
|
|
@router.post("/{suggestion_id}")
|
|
async def update_suggestion(
|
|
request: Request,
|
|
db: Db,
|
|
user: AdminUser,
|
|
suggestion_id: str,
|
|
) -> Response:
|
|
"""Save one row.
|
|
|
|
The raw form is read rather than declared parameters because `enabled` is a
|
|
checkbox: FastAPI cannot tell an unticked box from an absent field, and an
|
|
absent one is exactly what an unticked box sends.
|
|
"""
|
|
suggestion = _suggestion(db, suggestion_id)
|
|
form = await request.form()
|
|
|
|
suggestion.name = (
|
|
str(form.get("name") or "").strip()[: suggestions_service.MAX_NAME] or suggestion.name
|
|
)
|
|
suggestion.description = str(form.get("description") or "").strip()[
|
|
: suggestions_service.MAX_DESCRIPTION
|
|
]
|
|
suggestion.prompt = str(form.get("prompt") or "").replace("\r\n", "\n")[
|
|
: suggestions_service.MAX_PROMPT
|
|
]
|
|
suggestion.enabled = "enabled" in form
|
|
|
|
position = str(form.get("position") or "").strip()
|
|
if position.isdigit():
|
|
suggestion.position = min(max(int(position) - 1, 0), 999)
|
|
|
|
db.commit()
|
|
log.info("%s updated suggestion %s", user.email, suggestion.name)
|
|
return _back(f"Saved {suggestion.name}.")
|