Prompt suggestions on the new-chat screen
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>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""The cards offered on the new-chat screen.
|
||||
|
||||
A blank composer is the least helpful thing a chat client can show someone who
|
||||
has just installed one. These are three starting points an administrator can
|
||||
replace with their own.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session as DBSession
|
||||
|
||||
from lembas.db.models import Suggestion
|
||||
from lembas.services import settings_store
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# A short list stays a short list. Past a dozen it is a menu, and a menu on the
|
||||
# empty screen is a worse blank page than a blank page.
|
||||
MAX_SUGGESTIONS = 12
|
||||
MAX_SHOWN = 6
|
||||
|
||||
MAX_NAME = 120
|
||||
MAX_DESCRIPTION = 300
|
||||
MAX_PROMPT = 4000
|
||||
|
||||
# Each ends mid-sentence, so the caret lands exactly where the person has to
|
||||
# start typing. No Middle-earth flavour: this is functional UI.
|
||||
DEFAULTS: tuple[tuple[str, str, str], ...] = (
|
||||
(
|
||||
"Explain this",
|
||||
"Paste something confusing and get it back in plain language.",
|
||||
"Explain the following in plain language. Start with one sentence "
|
||||
"summarising it, then the details that actually matter, then anything I "
|
||||
"should watch out for. If I have not pasted anything yet, ask me for it "
|
||||
"rather than guessing.\n\n",
|
||||
),
|
||||
(
|
||||
"Draft a reply",
|
||||
"Turn a message you have received into an answer you can send.",
|
||||
"Help me reply to the message below. If the tone I want and the outcome "
|
||||
"I am after are not obvious from it, ask me before writing. Then give me "
|
||||
"a draft I could send as it stands.\n\n",
|
||||
),
|
||||
(
|
||||
"Find the flaw",
|
||||
"Have a plan argued with before you commit to it.",
|
||||
"I am going to describe a plan. Argue against it: what is most likely to "
|
||||
"go wrong, what am I assuming without evidence, and what would change "
|
||||
"your mind. Do not soften it, and do not agree just because I sound "
|
||||
"confident.\n\nMy plan: ",
|
||||
),
|
||||
)
|
||||
|
||||
# Guards the seed. Not "is the table empty", because an administrator who
|
||||
# deletes all three would get them back on every restart.
|
||||
SEEDED_KEY = "suggestions_seeded"
|
||||
|
||||
|
||||
def visible(db: DBSession) -> list[Suggestion]:
|
||||
"""What the new-chat screen shows, in order."""
|
||||
return list(
|
||||
db.scalars(
|
||||
select(Suggestion)
|
||||
.where(Suggestion.enabled.is_(True))
|
||||
.order_by(Suggestion.position, Suggestion.name)
|
||||
.limit(MAX_SHOWN)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def all_of_them(db: DBSession) -> list[Suggestion]:
|
||||
"""Every suggestion, enabled or not, for the admin page."""
|
||||
return list(
|
||||
db.scalars(select(Suggestion).order_by(Suggestion.position, Suggestion.name))
|
||||
)
|
||||
|
||||
|
||||
def next_position(db: DBSession) -> int:
|
||||
"""New rows land at the end rather than at 0, where they would shuffle.
|
||||
|
||||
No `or -1` after the coalesce: position 0 is falsy, so that idiom sends the
|
||||
second row back to 0 on top of the first.
|
||||
"""
|
||||
highest = db.scalar(select(func.coalesce(func.max(Suggestion.position), -1)))
|
||||
return int(highest if highest is not None else -1) + 1
|
||||
|
||||
|
||||
def create(db: DBSession, *, name: str, description: str = "", prompt: str = "") -> Suggestion:
|
||||
suggestion = Suggestion(
|
||||
name=name.strip()[:MAX_NAME],
|
||||
description=description.strip()[:MAX_DESCRIPTION],
|
||||
prompt=prompt[:MAX_PROMPT],
|
||||
position=next_position(db),
|
||||
)
|
||||
db.add(suggestion)
|
||||
db.commit()
|
||||
return suggestion
|
||||
|
||||
|
||||
def seed_defaults(db: DBSession) -> int:
|
||||
"""Write the built-in suggestions, once ever.
|
||||
|
||||
Runs from the startup housekeeping block. The flag is what makes it once:
|
||||
checking whether the table is empty would restore all three every restart
|
||||
for anyone who decided they did not want them.
|
||||
"""
|
||||
if settings_store.get(db, SEEDED_KEY):
|
||||
return 0
|
||||
|
||||
for name, description, prompt in DEFAULTS:
|
||||
create(db, name=name, description=description, prompt=prompt)
|
||||
|
||||
settings_store.update(db, {SEEDED_KEY: True})
|
||||
log.info("seeded %d default suggestion(s)", len(DEFAULTS))
|
||||
return len(DEFAULTS)
|
||||
Reference in New Issue
Block a user