26793b1317
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>
119 lines
4.0 KiB
Python
119 lines
4.0 KiB
Python
"""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)
|