Files
LLeMbas/src/lembas/services/suggestions.py
T
Homer 59739cc7fd Files that outlived the chats that held them, and a page that led with its footnotes
The second audit pass. Four things, and the first two were reported.

The Prompts page put a screen of variables and a screen of preview above
the editor, so the tabs began two screens down and switching one had to
drag the whole page to be any use -- and on a short tab it could not drag
far enough, leaving the panel stranded above a screenful of nothing.
Editor first, reference after, bar sticky. Custom themes were three fixed
slots: fifty-seven empty colour boxes on a fresh instance and no way to
make a fourth theme. One block per theme plus a blank one, colours behind
a disclosure. Both measured rather than argued about -- rendered through
TestClient and driven under headless Chromium, where the tab bar moved
385->642px before and does not move now, and the themes page went from
5495px to 2820px.

Asking where generated images go found the other two. Deleting a chat
cascades to the attachment rows and leaves every file on disk; the helper
written for exactly that was called from one place, and it was not the
delete button, a schedule's chat, a helper's chat or deleting an account.
Underneath it, `claim` bound message_id and never chat_id, so anything
picked before a chat existed kept an empty chat_id forever -- which six
readers filter on, so those files were also unnamed in the prompt,
unopenable in the canvas, and invisible to the one caller the cleanup had.

And folders nest now. The route has handled parent_id since folders
existed, with a cycle guard and a depth cap the move path never applied;
the sidebar has always drawn a tree. Nothing could ask for one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:20:59 +02:00

135 lines
4.7 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
# A card is sent the moment it is clicked, so each of these has to work cold --
# with nothing pasted and nothing typed. These four are *self-contained* rather
# than question-asking: they are what somebody clicks on an instance they have
# just stood up, to find out whether the thing works and what the model behind
# it is. That is the honest first use, and it is a different job from the task
# cards that preceded them, which opened by asking for material the reader had
# not given.
#
# The name and description are read by somebody who has never seen this
# instance; the prompt is read by the model. They are allowed to differ, and
# here they do -- "Start a session" says what the card is for, and its prompt is
# the bare line that produces a clean opening turn.
#
# Only a fresh install gets these: `SEEDED_KEY` means an instance that has
# already seeded keeps whatever its administrator has since made of the list.
#
# No Middle-earth flavour: this is functional UI.
DEFAULTS: tuple[tuple[str, str, str], ...] = (
(
"Start a session",
"Open a fresh conversation with nothing assumed.",
"Initializing a new session.",
),
(
"What can you do?",
"Have the model say which tools, instructions and limits it has.",
"Who are you and what are you capable of? What are your available tools? "
"What do you already know from your instructions? Do you have any set "
"personality?",
),
(
"Show me some code",
"See how the model writes, in a language of its own choosing.",
"Pick a random programming language and produce a coding example showing "
"your coding capabilities.",
),
(
"Tell me something",
"A fact worth knowing, from a subject picked at random.",
"Give me a random fun fact from a random topic of your choosing. "
"Available topics are science, history, sociology, theology etc.",
),
)
# 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)