A suggestion card sends its prompt
Filling the composer and waiting for Enter made the card a form to review rather than a thing to press. One click, one reply. That changes what a prompt has to be. The built-ins ended mid-sentence -- "My plan: " -- because nothing was sent until the person finished the thought; sent cold they are a model guessing at material nobody gave it. All three are rewritten to ask for what they need, so the first reply is the right question instead. There is a test that they end as complete sentences, since the failure is silent and only visible in the answer. requestSubmit, not submit: it fires the submit event, which is what htmx listens for. Same call the Enter key already makes. Version bumped because app.js is what changed, and the service worker caches it -- without the bump the first load after this would still only fill the box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "lembas"
|
name = "lembas"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
description = "LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints"
|
description = "LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints."""
|
"""LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints."""
|
||||||
|
|
||||||
__version__ = "0.3.0"
|
__version__ = "0.3.1"
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ class Suggestion(UUIDPrimaryKey, Timestamps, Base):
|
|||||||
|
|
||||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||||
description: Mapped[str] = mapped_column(String(300), default="")
|
description: Mapped[str] = mapped_column(String(300), default="")
|
||||||
# What lands in the composer. Deliberately not sent on its own: it usually
|
# Sent as the first message the moment the card is clicked, so it has to
|
||||||
# ends mid-sentence, because a card is a starting point rather than a
|
# stand on its own -- there is no chance to add anything to it first. The
|
||||||
# question somebody already asked.
|
# built-ins ask for what they need rather than assuming material.
|
||||||
prompt: Mapped[str] = mapped_column(Text, default="")
|
prompt: Mapped[str] = mapped_column(Text, default="")
|
||||||
|
|
||||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||||
|
|||||||
@@ -26,31 +26,33 @@ MAX_NAME = 120
|
|||||||
MAX_DESCRIPTION = 300
|
MAX_DESCRIPTION = 300
|
||||||
MAX_PROMPT = 4000
|
MAX_PROMPT = 4000
|
||||||
|
|
||||||
# Each ends mid-sentence, so the caret lands exactly where the person has to
|
# A card is sent the moment it is clicked, so each of these has to work cold --
|
||||||
# start typing. No Middle-earth flavour: this is functional UI.
|
# with nothing pasted and nothing typed. They are written to ask for what they
|
||||||
|
# need, which turns the first reply into the right question rather than a guess
|
||||||
|
# at material nobody has given yet.
|
||||||
|
#
|
||||||
|
# No Middle-earth flavour: this is functional UI.
|
||||||
DEFAULTS: tuple[tuple[str, str, str], ...] = (
|
DEFAULTS: tuple[tuple[str, str, str], ...] = (
|
||||||
(
|
(
|
||||||
"Explain this",
|
"Explain this",
|
||||||
"Paste something confusing and get it back in plain language.",
|
"Get something confusing back in plain language.",
|
||||||
"Explain the following in plain language. Start with one sentence "
|
"I want something explained in plain language. Ask me what it is, then "
|
||||||
"summarising it, then the details that actually matter, then anything I "
|
"give me one sentence summarising it, the details that actually matter, "
|
||||||
"should watch out for. If I have not pasted anything yet, ask me for it "
|
"and anything I should watch out for.",
|
||||||
"rather than guessing.\n\n",
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"Draft a reply",
|
"Draft a reply",
|
||||||
"Turn a message you have received into an answer you can send.",
|
"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 need to reply to a message. Ask me what it says, what tone I want and "
|
||||||
"I am after are not obvious from it, ask me before writing. Then give me "
|
"what outcome I am after, then write a draft I could send as it stands.",
|
||||||
"a draft I could send as it stands.\n\n",
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"Find the flaw",
|
"Find the flaw",
|
||||||
"Have a plan argued with before you commit to it.",
|
"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 "
|
"I want a plan argued with. Ask me what the plan is, then tell me what is "
|
||||||
"go wrong, what am I assuming without evidence, and what would change "
|
"most likely to go wrong, what I am assuming without evidence, and what "
|
||||||
"your mind. Do not soften it, and do not agree just because I sound "
|
"would change your mind. Do not soften it, and do not agree just because "
|
||||||
"confident.\n\nMy plan: ",
|
"I sound confident.",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -384,9 +384,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A suggestion card fills the composer and stops there. It deliberately
|
/* A suggestion card sends its prompt. One click, one reply -- filling the
|
||||||
does not submit: the prompts end mid-sentence, because a card is a
|
box and waiting for Enter makes the card a form to review rather than a
|
||||||
starting point rather than a question somebody already asked. */
|
thing to press. The built-in prompts are written to work sent cold: each
|
||||||
|
asks for what it needs, so the answer is a question back rather than a
|
||||||
|
guess at material nobody has given yet. */
|
||||||
var suggestion = event.target.closest("[data-suggestion]");
|
var suggestion = event.target.closest("[data-suggestion]");
|
||||||
if (suggestion) {
|
if (suggestion) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -394,8 +396,10 @@
|
|||||||
if (!input) return;
|
if (!input) return;
|
||||||
input.value = suggestion.dataset.suggestion;
|
input.value = suggestion.dataset.suggestion;
|
||||||
autosize(input);
|
autosize(input);
|
||||||
input.focus();
|
var form = input.closest("form");
|
||||||
input.setSelectionRange(input.value.length, input.value.length);
|
/* requestSubmit, not submit(): it fires the submit event, which is what
|
||||||
|
htmx is listening for. Same call the Enter key makes. */
|
||||||
|
if (form) form.requestSubmit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
{% block admin_content %}
|
{% block admin_content %}
|
||||||
<p class="admin-lede">
|
<p class="admin-lede">
|
||||||
Cards on the new-chat screen. Clicking one puts its prompt in the composer
|
Cards on the new-chat screen. Clicking one sends its prompt straight away, so
|
||||||
without sending it — the built-in ones deliberately end mid-sentence, so the
|
each has to work on its own — write one that asks for whatever it needs, and
|
||||||
caret lands where the person has to start typing. The first
|
the first reply becomes the right question rather than a guess. The first
|
||||||
{{ max_shown }} enabled ones are shown, in this order.
|
{{ max_shown }} enabled ones are shown, in this order.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -64,8 +64,8 @@
|
|||||||
<textarea class="textarea" id="prompt-{{ suggestion.id }}" name="prompt"
|
<textarea class="textarea" id="prompt-{{ suggestion.id }}" name="prompt"
|
||||||
rows="4">{{ suggestion.prompt }}</textarea>
|
rows="4">{{ suggestion.prompt }}</textarea>
|
||||||
<p class="field__hint">
|
<p class="field__hint">
|
||||||
Put in the composer, not sent. Ending it mid-sentence is usually right:
|
Sent as the first message. Nothing is added to it, so a prompt that needs
|
||||||
the person still has to say what they are asking about.
|
material should ask for it.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field__label" for="new-prompt">Prompt</label>
|
<label class="field__label" for="new-prompt">Prompt</label>
|
||||||
<textarea class="textarea" id="new-prompt" name="prompt" rows="4"
|
<textarea class="textarea" id="new-prompt" name="prompt" rows="4"
|
||||||
placeholder="What lands in the composer when the card is clicked."></textarea>
|
placeholder="Sent as the first message when the card is clicked."></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
<button class="btn btn--primary" type="submit">Add</button>
|
<button class="btn btn--primary" type="submit">Add</button>
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ def test_the_seed_flag_lives_in_settings(db):
|
|||||||
assert settings_store.get(db, suggestions_service.SEEDED_KEY) is True
|
assert settings_store.get(db, suggestions_service.SEEDED_KEY) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_default_prompt_stands_on_its_own():
|
||||||
|
"""A card is sent the instant it is clicked, with nothing added. One that
|
||||||
|
trails off waiting for material the person has not given is a prompt the
|
||||||
|
model has to guess at."""
|
||||||
|
for name, _description, prompt in suggestions_service.DEFAULTS:
|
||||||
|
assert prompt == prompt.strip(), f"{name} has stray whitespace"
|
||||||
|
assert prompt.endswith("."), f"{name} does not end as a complete sentence"
|
||||||
|
|
||||||
|
|
||||||
# --- What is shown ------------------------------------------------------------
|
# --- What is shown ------------------------------------------------------------
|
||||||
def test_only_enabled_ones_are_shown(db):
|
def test_only_enabled_ones_are_shown(db):
|
||||||
suggestions_service.create(db, name="Shown", prompt="a")
|
suggestions_service.create(db, name="Shown", prompt="a")
|
||||||
|
|||||||
Reference in New Issue
Block a user