Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e65ea90fe6
|
@@ -16,6 +16,15 @@ for 1.0.0 have something to be assembled from.
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.9.1
|
||||
|
||||
- **A temporary chat can be started on any model.** On the new-chat screen,
|
||||
turning on Temporary switched the model back to the default, and choosing a
|
||||
model switched Temporary off, so a temporary chat could only ever be started
|
||||
on the default model. The Temporary button, the model menu and `/temp` now
|
||||
keep each other's choice, and also keep the folder a chat was started in
|
||||
("New chat here") and whether it is an agent chat.
|
||||
|
||||
## 1.9.0
|
||||
|
||||
The model menu says which model is loaded, and a new chat now matches the
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints."""
|
||||
|
||||
__version__ = "1.9.0"
|
||||
__version__ = "1.9.1"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from zoneinfo import available_timezones
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, status
|
||||
@@ -727,6 +728,24 @@ async def chat_index(
|
||||
if preselected is None and context["models"]:
|
||||
preselected = context["models"][0]
|
||||
|
||||
# Every preselection lives in the URL, so every link that changes one of
|
||||
# them has to carry the rest. The temporary toggle used to link to a bare
|
||||
# `/chat?temporary=1` and the model picker to a bare `/chat?model=`, so
|
||||
# each undid the other: temporary chats could only ever be started on the
|
||||
# default model. The model goes last in the picker's URL because ui.js
|
||||
# appends the chosen id to it.
|
||||
carried = {
|
||||
"model": model if model and preselected and preselected.model_id == model else "",
|
||||
"temporary": "1" if temporary else "",
|
||||
"kind": kind if kind in KINDS and kind != KIND_CHAT else "",
|
||||
"folder": starting_folder.id if starting_folder is not None else "",
|
||||
}
|
||||
|
||||
def new_chat_url(**changes: str) -> str:
|
||||
query = urlencode({k: v for k, v in {**carried, **changes}.items() if v})
|
||||
return f"/chat?{query}" if query else "/chat"
|
||||
|
||||
without_model = new_chat_url(model="")
|
||||
return render(
|
||||
request,
|
||||
"chat/index.html",
|
||||
@@ -749,6 +768,8 @@ async def chat_index(
|
||||
else chat_service.DEFAULT_EFFORTS
|
||||
),
|
||||
"starting_temporary": temporary,
|
||||
"temporary_toggle_url": new_chat_url(temporary="" if temporary else "1"),
|
||||
"model_navigate_url": without_model + ("&" if "?" in without_model else "?") + "model=",
|
||||
"starting_kind": kind if kind in KINDS else KIND_CHAT,
|
||||
"starting_folder": starting_folder,
|
||||
"suggestions": suggestions_service.visible(db),
|
||||
|
||||
@@ -222,7 +222,16 @@
|
||||
{
|
||||
name: "temp",
|
||||
summary: "Start a temporary chat, gone after a day",
|
||||
run: function () { window.location = "/chat?temporary=1"; }
|
||||
run: function () {
|
||||
/* On the new-chat screen the model, folder and kind already chosen are
|
||||
in the query string. Add the flag to them rather than starting over,
|
||||
or the chat is made on the default model. */
|
||||
var query = new URLSearchParams(
|
||||
window.location.pathname === "/chat" ? window.location.search : ""
|
||||
);
|
||||
query.set("temporary", "1");
|
||||
window.location = "/chat?" + query.toString();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "stop",
|
||||
|
||||
@@ -91,7 +91,9 @@
|
||||
</form>
|
||||
{% else %}
|
||||
{# No chat yet: selecting navigates so the whole composer re-renders with the
|
||||
right vision warning and the right hidden model_id. #}
|
||||
<span hidden data-picker-navigate="/chat?model="></span>
|
||||
right vision warning and the right hidden model_id. The URL carries the
|
||||
other preselections -- temporary, folder, kind -- and ends in `model=`,
|
||||
which ui.js completes. #}
|
||||
<span hidden data-picker-navigate="{{ model_navigate_url }}"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -65,11 +65,12 @@
|
||||
<div class="topbar__actions">
|
||||
{#
|
||||
A link, not a script: the flag lives in the URL, so it survives a
|
||||
reload and can be bookmarked.
|
||||
reload and can be bookmarked. The URL is built in `chat_index` so it
|
||||
keeps the chosen model, folder and kind.
|
||||
#}
|
||||
{% if not chat and can.get("chat.create") %}
|
||||
<a class="btn btn--icon {{ 'is-active' if starting_temporary }}"
|
||||
href="{{ '/chat' if starting_temporary else '/chat?temporary=1' }}"
|
||||
href="{{ temporary_toggle_url }}"
|
||||
aria-label="{{ t('Temporary chat') }}"
|
||||
title="{% if starting_temporary %}Starting a temporary chat. Click to go back to a normal one.{% else %}Start a temporary chat: not listed in the sidebar, and removed after a day.{% endif %}">
|
||||
{{ icon("clock") }}
|
||||
|
||||
@@ -58,3 +58,12 @@ def test_send_from_anywhere_never_means_stop():
|
||||
stops."""
|
||||
window = SOURCE[SOURCE.index('event.code === "Enter"') :][:600]
|
||||
assert 'composerAction === "send"' in window
|
||||
|
||||
|
||||
def test_temp_keeps_what_the_new_chat_screen_already_chose():
|
||||
"""`/temp` went to a bare `/chat?temporary=1`, so on a new-chat screen with
|
||||
a model picked it quietly swapped back to the default model."""
|
||||
window = SOURCE[SOURCE.index('name: "temp"') :][:600]
|
||||
assert 'window.location = "/chat?temporary=1"' not in window
|
||||
assert "window.location.search" in window
|
||||
assert 'query.set("temporary", "1")' in window
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import re
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -100,6 +102,59 @@ def test_the_new_chat_screen_carries_the_flag(client: TestClient, db, registered
|
||||
assert 'name="temporary"' in client.get("/chat?temporary=1").text
|
||||
|
||||
|
||||
def _temporary_link(page: str) -> str:
|
||||
match = re.search(r'href="([^"]*)"\s+aria-label="Temporary chat"', page)
|
||||
assert match, "no temporary toggle on the page"
|
||||
return html.unescape(match.group(1))
|
||||
|
||||
|
||||
def _picker_url(page: str) -> str:
|
||||
match = re.search(r'data-picker-navigate="([^"]*)"', page)
|
||||
assert match, "no navigating model picker on the page"
|
||||
return html.unescape(match.group(1))
|
||||
|
||||
|
||||
def test_the_temporary_toggle_keeps_the_chosen_model(client: TestClient, db, registered):
|
||||
"""Temporary chats could only be started on the default model: the toggle
|
||||
linked to a bare `/chat?temporary=1`, so the model picked a moment before
|
||||
was dropped."""
|
||||
connection = _connection(db)
|
||||
db.add(Model(connection_id=connection.id, model_id="other-model"))
|
||||
db.commit()
|
||||
|
||||
on = _temporary_link(client.get("/chat?model=other-model").text)
|
||||
assert "model=other-model" in on and "temporary=1" in on
|
||||
assert 'name="model_id" value="other-model"' in client.get(on).text
|
||||
|
||||
off = _temporary_link(client.get(on).text)
|
||||
assert "model=other-model" in off and "temporary" not in off
|
||||
|
||||
|
||||
def test_choosing_a_model_keeps_the_temporary_flag(client: TestClient, db, registered):
|
||||
"""The other half: the picker navigated to a bare `/chat?model=`, so
|
||||
picking a model after the toggle quietly made the chat an ordinary one."""
|
||||
connection = _connection(db)
|
||||
db.add(Model(connection_id=connection.id, model_id="other-model"))
|
||||
db.commit()
|
||||
|
||||
url = _picker_url(client.get("/chat?temporary=1").text)
|
||||
assert url.endswith("model=")
|
||||
page = client.get(url + "other-model").text
|
||||
assert 'name="temporary"' in page
|
||||
assert 'name="model_id" value="other-model"' in page
|
||||
assert _picker_url(client.get("/chat").text) == "/chat?model="
|
||||
|
||||
|
||||
def test_both_links_keep_the_folder(client: TestClient, db, registered):
|
||||
_connection(db)
|
||||
client.post("/api/folders", data={"name": "Quests"})
|
||||
folder = db.scalar(select(Folder))
|
||||
|
||||
page = client.get(f"/chat?folder={folder.id}").text
|
||||
assert f"folder={folder.id}" in _temporary_link(page)
|
||||
assert f"folder={folder.id}" in _picker_url(page)
|
||||
|
||||
|
||||
def test_starting_a_temporary_chat_sets_the_flag(client: TestClient, db, registered):
|
||||
_connection(db)
|
||||
client.post("/api/chats/start", data={"content": "hello", "temporary": "true"})
|
||||
|
||||
Reference in New Issue
Block a user