59739cc7fd
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>
135 lines
4.7 KiB
Python
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)
|