Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a16510aba8
|
@@ -16,6 +16,36 @@ for 1.0.0 have something to be assembled from.
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.7.0
|
||||
|
||||
- **The interface speaks Slovak.** Pick a language under **Appearance** in your
|
||||
own settings, or set what everybody else gets under Admin → General. Your own
|
||||
choice wins over the instance's, it is saved to your account rather than to one
|
||||
browser, and `<html lang>` finally says what the page is actually in.
|
||||
|
||||
All 969 translatable strings are translated, including the long explanatory
|
||||
paragraphs on the admin pages — there is no half-done corner where a Slovak
|
||||
instance falls back to English. Dates follow too: a month name is a month name
|
||||
in the language you are reading, which `strftime` cannot do without a process
|
||||
locale that this application must not set.
|
||||
|
||||
**What is deliberately still in English**: everything a *model* reads. The
|
||||
prompt fragments under Admin → Prompts are instructions written for models, and
|
||||
translating them would change what the models are told rather than what you
|
||||
see. Models answer in whatever language you write to them in — they already
|
||||
did, and that line is editable where all the others are.
|
||||
|
||||
An English instance is byte-for-byte what shipped in 1.6.0. That is a property
|
||||
of the design rather than a claim: a string with no translation renders the
|
||||
English it was written in, so a language added later cannot leave holes in a
|
||||
page.
|
||||
|
||||
- Fixed: **every page was rendered in the instance's language, whatever anybody
|
||||
had chosen.** Found while building the above and worth naming because it would
|
||||
have been invisible: the language was resolved in a dependency that FastAPI runs
|
||||
in a threadpool, and the context it was set in is discarded on the way out. Now
|
||||
it is resolved in the request's own task.
|
||||
|
||||
## 1.6.0
|
||||
|
||||
- **A chat can have a crowd.** Switch it on under Admin → Agents, and each chat's
|
||||
|
||||
@@ -81,6 +81,13 @@ src = ["src", "tests"]
|
||||
select = ["E", "F", "I", "UP", "B", "SIM", "C4"]
|
||||
ignore = ["B008"] # FastAPI Depends() in defaults is idiomatic
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
# A translation catalogue is keyed by the English sentence, and a sentence cannot
|
||||
# be rewrapped without becoming a different key. Wrapping them would mean every
|
||||
# key spelled as an implicit concatenation, which is both unreadable and one
|
||||
# stray space away from a silent miss.
|
||||
"src/lembas/web/i18n/*.py" = ["E501"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
# Registered so `-m "not slow"` works and an unknown-marker warning does not
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Find every translatable string, and say what the catalogues are missing.
|
||||
|
||||
Run it:
|
||||
|
||||
python scripts/i18n_extract.py # a report
|
||||
python scripts/i18n_extract.py --write sk # fill sk.py with what is missing
|
||||
|
||||
A development instrument, like `shoot.py` and `fetch_vendor.py`: nothing in `src/`
|
||||
imports it. What it knows is the one thing a catalogue keyed on source text cannot
|
||||
know for itself -- that an English sentence has been edited, leaving its
|
||||
translation stranded under the old wording. `tests/test_translations.py` asserts
|
||||
the same property from the other side, so a catalogue cannot rot quietly.
|
||||
|
||||
The scan is deliberately simple: `t("…")` and `t('…')`, in templates and in
|
||||
Python. A string built by concatenation or an f-string is not found, and that is
|
||||
the point -- a sentence assembled from pieces cannot be translated, because the
|
||||
order of the pieces is not the same in every language. Use `t("… %(name)s …",
|
||||
name=…)`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SRC = REPO / "src/lembas"
|
||||
TEMPLATES = SRC / "web/templates"
|
||||
CATALOGUES = SRC / "web/i18n"
|
||||
|
||||
# `t("…")` with either quote, allowing escaped quotes inside. Multi-line, because
|
||||
# a paragraph in a template is wrapped for the width of the file.
|
||||
CALL = re.compile(r"""\bt\(\s*(?P<q>["'])(?P<text>(?:\\.|(?!\1).)*?)\1""", re.S)
|
||||
|
||||
# `i18n.stamp(value, "%d %B %Y")` -- the *format* is translated too, so a language
|
||||
# that puts the day first, or wants a full stop after it, says so in the
|
||||
# catalogue. A second pattern rather than a looser first one: widening `t(` to
|
||||
# "any call with a string in it" would sweep up every `select("…")` in the
|
||||
# codebase.
|
||||
STAMP = re.compile(
|
||||
# One level of nesting allowed in the first argument, because it is usually a
|
||||
# call: `stamp(clock.now_for(user), "…")`.
|
||||
r"""\bstamp\((?:[^()"']|\([^()]*\))*,\s*(?P<q>["'])(?P<text>(?:\\.|(?!\1).)*?)\1""",
|
||||
re.S,
|
||||
)
|
||||
|
||||
|
||||
def normalise(text: str) -> str:
|
||||
"""The key: whitespace collapsed, escapes resolved.
|
||||
|
||||
A template wraps one sentence across three lines, and the same sentence in a
|
||||
Python file across two. Keying on the exact bytes would need an entry per
|
||||
wrapping, so the key is the text with its runs of whitespace flattened -- which
|
||||
is exactly what `i18n.translate` looks up.
|
||||
"""
|
||||
text = text.replace('\\"', '"').replace("\\'", "'").replace("\\n", " ")
|
||||
return " ".join(text.split())
|
||||
|
||||
|
||||
def sources() -> list[Path]:
|
||||
files = sorted(TEMPLATES.rglob("*.html"))
|
||||
files += [
|
||||
path
|
||||
for path in sorted(SRC.rglob("*.py"))
|
||||
if "web/i18n" not in str(path) and "__pycache__" not in str(path)
|
||||
]
|
||||
return files
|
||||
|
||||
|
||||
JINJA_COMMENT = re.compile(r"\{#.*?#\}", re.S)
|
||||
PY_COMMENT = re.compile(r"^[ \t]*#.*$", re.M)
|
||||
|
||||
|
||||
def strip_comments(path: Path, text: str) -> str:
|
||||
"""Comments are not strings. This file's own docstrings quote `t("Save")`, and
|
||||
so does `web/templating.py`'s comment explaining the global -- both would
|
||||
otherwise arrive in the catalogue as things to translate."""
|
||||
if path.suffix == ".html":
|
||||
return JINJA_COMMENT.sub("", text)
|
||||
return PY_COMMENT.sub("", text)
|
||||
|
||||
|
||||
def found() -> dict[str, list[str]]:
|
||||
"""Every string, with the files it appears in."""
|
||||
out: dict[str, list[str]] = {}
|
||||
for path in sources():
|
||||
text = strip_comments(path, path.read_text(encoding="utf-8"))
|
||||
for match in list(CALL.finditer(text)) + list(STAMP.finditer(text)):
|
||||
key = normalise(match.group("text"))
|
||||
if not key:
|
||||
continue
|
||||
out.setdefault(key, [])
|
||||
where = str(path.relative_to(REPO))
|
||||
if where not in out[key]:
|
||||
out[key].append(where)
|
||||
return out
|
||||
|
||||
|
||||
def catalogue(code: str) -> dict[str, str]:
|
||||
path = CATALOGUES / f"{code}.py"
|
||||
if not path.exists():
|
||||
return {}
|
||||
namespace: dict[str, object] = {}
|
||||
exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), namespace)
|
||||
return dict(namespace.get("MESSAGES", {})) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def report() -> int:
|
||||
strings = found()
|
||||
print(f"{len(strings)} translatable strings in {len(sources())} files")
|
||||
for code in ("sk",):
|
||||
have = catalogue(code)
|
||||
missing = [key for key in strings if key not in have]
|
||||
orphans = [key for key in have if key not in strings]
|
||||
done = len(strings) - len(missing)
|
||||
print(
|
||||
f" {code}: {done}/{len(strings)} translated"
|
||||
f" ({len(missing)} missing, {len(orphans)} orphaned)"
|
||||
)
|
||||
for key in orphans[:10]:
|
||||
print(f" orphan: {key[:80]!r}")
|
||||
return 0
|
||||
|
||||
|
||||
def write(code: str) -> int:
|
||||
"""Append the missing keys to a catalogue, each mapped to itself.
|
||||
|
||||
Mapped to the English rather than to "" on purpose: an empty translation would
|
||||
render as an empty paragraph, while the English renders as what it already
|
||||
said. A catalogue half-filled is a page half-translated, never a page with
|
||||
holes in it.
|
||||
"""
|
||||
strings = found()
|
||||
have = catalogue(code)
|
||||
missing = [key for key in strings if key not in have]
|
||||
if not missing:
|
||||
print(f"{code}: nothing missing")
|
||||
return 0
|
||||
path = CATALOGUES / f"{code}.py"
|
||||
with path.open("a", encoding="utf-8") as handle:
|
||||
handle.write(f"\n# --- {len(missing)} added by scripts/i18n_extract.py ---\n")
|
||||
for key in missing:
|
||||
handle.write(f"MESSAGES[{key!r}] = {key!r}\n")
|
||||
print(f"{code}: added {len(missing)} keys to {path.relative_to(REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if "--write" in sys.argv:
|
||||
return write(sys.argv[sys.argv.index("--write") + 1])
|
||||
return report()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,3 +1,3 @@
|
||||
"""LLeMbas - a Middle-earth themed web UI for OpenAI-compatible LLM endpoints."""
|
||||
|
||||
__version__ = "1.6.0"
|
||||
__version__ = "1.7.0"
|
||||
|
||||
@@ -16,6 +16,7 @@ from lembas.db.models import Connection, Model, User
|
||||
from lembas.services import settings_store
|
||||
from lembas.services.crypto import UNCHANGED_SENTINEL, decrypt, encrypt, mask
|
||||
from lembas.services.llm.openai_client import Endpoint, LLMError, context_from, list_models
|
||||
from lembas.web import i18n
|
||||
from lembas.web.templating import render
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -46,6 +47,7 @@ async def general_page(request: Request, db: Db, user: AdminUser, saved: bool =
|
||||
"admin/general.html",
|
||||
{
|
||||
"values": settings_store.get_group(db),
|
||||
"languages": i18n.LANGUAGES,
|
||||
"saved": saved,
|
||||
"user_count": db.scalar(select(func.count()).select_from(User)),
|
||||
},
|
||||
@@ -57,6 +59,7 @@ async def save_general(
|
||||
db: Db,
|
||||
user: AdminUser,
|
||||
allow_signup: bool = Form(False),
|
||||
language: str = Form(""),
|
||||
system_prompt: str = Form(""),
|
||||
compact_threshold: int = Form(95),
|
||||
max_chat_rounds: int = Form(5),
|
||||
@@ -70,6 +73,9 @@ async def save_general(
|
||||
db,
|
||||
{
|
||||
"allow_signup": allow_signup,
|
||||
# Validated rather than trusted: a code this release does not have
|
||||
# would leave every page in a language nobody chose.
|
||||
"language": i18n.known(language),
|
||||
"system_prompt": system_prompt.strip()[:8000],
|
||||
# 0 is "never"; anything else is clamped into a band where it can
|
||||
# do some good. 100 is useless -- you cannot compact after
|
||||
@@ -82,6 +88,9 @@ async def save_general(
|
||||
"max_chat_rounds": min(max(max_chat_rounds, 0), 100),
|
||||
},
|
||||
)
|
||||
# The instance default is cached at process level, exactly as branding is, so
|
||||
# the one module that writes it is the one that drops the cache.
|
||||
i18n.forget()
|
||||
log.info("registration %s by %s", "opened" if allow_signup else "closed", user.email)
|
||||
return RedirectResponse("/admin/general?saved=1", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
+19
-1
@@ -13,6 +13,7 @@ from starlette.requests import HTTPConnection
|
||||
from lembas.db.models import User
|
||||
from lembas.db.session import get_session_factory
|
||||
from lembas.security.sessions import COOKIE_NAME, resolve_session
|
||||
from lembas.web import i18n
|
||||
|
||||
|
||||
def get_db() -> Iterator[DBSession]:
|
||||
@@ -27,9 +28,21 @@ def get_db() -> Iterator[DBSession]:
|
||||
Db = Annotated[DBSession, Depends(get_db)]
|
||||
|
||||
|
||||
def get_current_user(conn: HTTPConnection, db: Db) -> User | None:
|
||||
async def get_current_user(conn: HTTPConnection, db: Db) -> User | None:
|
||||
"""Resolve the session cookie to a user, or None when signed out.
|
||||
|
||||
⚠ `async def`, and that is load-bearing rather than tidy. FastAPI runs a
|
||||
*sync* dependency in a threadpool, and `i18n.activate` below sets a
|
||||
`ContextVar` -- which anyio copies **into** the thread and discards on the way
|
||||
out, so the language was set in a context nothing else could see and every
|
||||
page rendered in English however anybody's preference was stored. An async
|
||||
dependency is awaited in the request's own task, where the value survives to
|
||||
the render.
|
||||
|
||||
What it costs is one indexed SELECT on the event loop rather than in a
|
||||
thread, which is what every route in this application already does with its
|
||||
session.
|
||||
|
||||
Cached on the connection's state so several dependencies in one request do
|
||||
not each hit the sessions table.
|
||||
|
||||
@@ -44,6 +57,11 @@ def get_current_user(conn: HTTPConnection, db: Db) -> User | None:
|
||||
return cached
|
||||
user = resolve_session(db, conn.cookies.get(COOKIE_NAME))
|
||||
conn.state.user = user
|
||||
# The language this request renders in, set here because this is where the
|
||||
# person is already known -- no second session and no second cookie read. A
|
||||
# request that never resolves a user keeps whatever `LanguageMiddleware` set,
|
||||
# which is the instance default.
|
||||
i18n.activate(i18n.for_user(user))
|
||||
return user
|
||||
|
||||
|
||||
|
||||
+12
-1
@@ -33,6 +33,7 @@ from lembas.services import settings_store
|
||||
from lembas.services import suggestions as suggestions_service
|
||||
from lembas.services.library import documents as documents_service
|
||||
from lembas.services.schedule import clock
|
||||
from lembas.web import i18n
|
||||
from lembas.web.templating import STATIC_DIR, render
|
||||
|
||||
router = APIRouter(tags=["pages"])
|
||||
@@ -909,10 +910,20 @@ async def settings_page(
|
||||
"impressions": personas_service.impressions_for(db, user),
|
||||
# Sorted rather than left in set order, because a list of six
|
||||
# hundred zones that is not alphabetical is one nobody can use.
|
||||
"languages": i18n.LANGUAGES,
|
||||
# Their own choice, and what "follow the instance" currently means --
|
||||
# named rather than left blank, because "follow the instance" is only a
|
||||
# useful option if you can see what you would be following.
|
||||
"chosen_language": str((user.settings_json or {}).get("language") or ""),
|
||||
"instance_language": dict(i18n.LANGUAGES).get(
|
||||
i18n.instance_default(), i18n.instance_default()
|
||||
),
|
||||
"timezones": sorted(available_timezones()),
|
||||
"timezone": clock.name_for(user),
|
||||
"server_timezone": str(clock.server_zone()),
|
||||
"local_now": clock.now_for(user).strftime("%H:%M on %A %-d %B"),
|
||||
# Through `i18n.stamp`, not `strftime`: `%A` and `%B` are C-locale
|
||||
# English whatever the page is in, and this one is read by a person.
|
||||
"local_now": i18n.stamp(clock.now_for(user), "%H:%M on %A %-d %B"),
|
||||
**context,
|
||||
**sidebar_context(db, user),
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ from lembas.config import settings
|
||||
from lembas.security.passwords import hash_password, validate_password, verify_password
|
||||
from lembas.security.sessions import COOKIE_NAME, create_session, revoke_all_for_user
|
||||
from lembas.services.schedule import clock
|
||||
from lembas.web import i18n
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -66,6 +67,29 @@ async def set_timezone(db: Db, user: RequiredUser, timezone: str = Form("")) ->
|
||||
return RedirectResponse("/settings?saved=timezone", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post("/language")
|
||||
async def set_language(db: Db, user: RequiredUser, language: str = Form("")) -> Response:
|
||||
"""Which language this person sees the interface in.
|
||||
|
||||
Empty is a real answer -- "whatever the instance is set to" -- rather than an
|
||||
unset field, which is why it is stored as "" rather than removed. The same
|
||||
shape the timezone above uses, and for the same reason: absent and "follow the
|
||||
default" are different states, and a form cannot tell them apart otherwise.
|
||||
|
||||
Unlike the theme and the layout this needs no `localStorage` tier. Those two
|
||||
exist there because a paint that starts in the wrong theme flashes; text is
|
||||
rendered on the server and cannot.
|
||||
"""
|
||||
chosen = (language or "").strip().lower()
|
||||
if chosen and chosen not in i18n.LANGUAGE_IDS:
|
||||
return RedirectResponse(
|
||||
"/settings?error=language", status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
user.settings_json = {**(user.settings_json or {}), "language": chosen}
|
||||
db.commit()
|
||||
return RedirectResponse("/settings?saved=language", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
# Which CSS variables a browser is allowed to set from here, and how far. An
|
||||
# open dict would let a page store anything under somebody's account and have
|
||||
# it read back on every load; a width outside these bounds would hand them a
|
||||
|
||||
@@ -49,6 +49,7 @@ from lembas.api.deps import RedirectToLogin, is_htmx, login_redirect
|
||||
from lembas.config import settings
|
||||
from lembas.db.session import init_db
|
||||
from lembas.services.library import indexing
|
||||
from lembas.web import i18n
|
||||
from lembas.web.templating import STATIC_DIR, render
|
||||
|
||||
log = logging.getLogger("lembas")
|
||||
@@ -177,6 +178,15 @@ def create_app() -> FastAPI:
|
||||
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
|
||||
# The language every request starts in. A `ContextVar` is per task, and a task
|
||||
# is reused between requests -- so without resetting it here, a signed-out page
|
||||
# would inherit whichever person was served last on that worker. The user's own
|
||||
# choice is applied later, by `get_current_user`, where they are already known.
|
||||
@app.middleware("http")
|
||||
async def _language(request, call_next):
|
||||
i18n.activate(i18n.instance_default())
|
||||
return await call_next(request)
|
||||
|
||||
# One place that notices a library record changing, rather than a call in
|
||||
# each of the ten writers that touch those tables. Idempotent, because the
|
||||
# factory is called per test. See services/library/indexing.py:install.
|
||||
|
||||
@@ -40,6 +40,11 @@ EXTRACTION = "extraction"
|
||||
def _general_defaults() -> dict[str, Any]:
|
||||
return {
|
||||
"allow_signup": env_settings.allow_signup,
|
||||
# What the interface is rendered in when a person has not chosen. Empty
|
||||
# and "en" mean the same thing; `web/i18n.known` is what decides, so a
|
||||
# value from a release that offered more languages than this one cannot
|
||||
# leave somebody with a page nobody can read.
|
||||
"language": "",
|
||||
# When on, new accounts land in the `pending` role and cannot sign in
|
||||
# until an administrator approves them. Reserved for the users pass.
|
||||
"require_approval": False,
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
"""Translating the interface, without a build step.
|
||||
|
||||
## Why not gettext
|
||||
|
||||
`.po` files compiled to `.mo` are a build step, and this project does not have
|
||||
one (hard rule 1). So a catalogue is a committed Python module: a dict, keyed by
|
||||
**the English source text**, loaded at import.
|
||||
|
||||
Keying on the source has one large advantage and one cost, and the advantage is
|
||||
what decides it: **a missing entry renders the key**, which is the English. An
|
||||
untranslated string therefore looks exactly as it did before, an instance running
|
||||
in English is byte-for-byte what shipped, and a half-finished catalogue degrades
|
||||
into a half-translated page rather than into `settings.appearance.theme.label`
|
||||
written across somebody's screen. The cost is that editing an English sentence
|
||||
orphans its translation silently -- which is what
|
||||
`tests/test_translations.py` exists to catch, in both directions.
|
||||
|
||||
This is the shape `branding.FLAVOUR` and `services/prompts.py` already use:
|
||||
defaults in code, overrides beside them, and a test that the two agree.
|
||||
|
||||
## Why a ContextVar
|
||||
|
||||
`t()` has to be reachable from a Jinja **global**, not from the template context.
|
||||
`web/templating.render` is bypassed by 25 direct `TemplateResponse` calls and 8
|
||||
`get_template().render()` calls, and the second group is the SSE frame path, which
|
||||
has no `Request` object at all -- so threading a language through the context
|
||||
would leave a third of the application untranslated, and `brand`'s docstring
|
||||
records that lesson already.
|
||||
|
||||
But a global is bound once at import and the language is **per person**, so the
|
||||
active language cannot live in the global. It lives in a `ContextVar` that
|
||||
`LanguageMiddleware` sets per request. That is the one piece of genuinely new
|
||||
machinery here; `brand` avoids needing it only because instance branding is the
|
||||
same for everybody.
|
||||
|
||||
⚠ A `ContextVar` is per *task*, and a background reply is a task of its own. It
|
||||
therefore does not inherit a request's language, which is correct rather than
|
||||
unfortunate: nothing a generation writes is interface text, and the one place it
|
||||
matters -- a fragment telling a model which language to answer in -- is a prompt
|
||||
variable, not a `t()` call.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from contextvars import ContextVar
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# The language every string is written in, and the key every catalogue uses.
|
||||
SOURCE = "en"
|
||||
|
||||
# What an instance may be set to. Ordered, because it is also the order the
|
||||
# settings screens offer.
|
||||
LANGUAGES: tuple[tuple[str, str], ...] = (
|
||||
("en", "English"),
|
||||
("sk", "Slovenčina"),
|
||||
)
|
||||
|
||||
LANGUAGE_IDS = tuple(code for code, _name in LANGUAGES)
|
||||
|
||||
# Text direction, so `<html dir>` is answered from one place when a
|
||||
# right-to-left language is added rather than being forgotten.
|
||||
DIRECTIONS: Mapping[str, str] = {"en": "ltr", "sk": "ltr"}
|
||||
|
||||
_active: ContextVar[str] = ContextVar("lembas_language", default=SOURCE)
|
||||
|
||||
# Loaded lazily and cached: a catalogue is a module, and importing every language
|
||||
# at start would read files an instance in English never needs.
|
||||
_catalogues: dict[str, Mapping[str, str]] = {}
|
||||
|
||||
|
||||
# The instance default, cached at process level exactly as `branding.snapshot()`
|
||||
# is and invalidated the same way -- by the one module that writes it calling
|
||||
# `forget()`. Without the cache, every request would need a settings read before
|
||||
# it could decide what language to render in, including the ones that never touch
|
||||
# the database otherwise.
|
||||
_DEFAULT: str | None = None
|
||||
|
||||
|
||||
def instance_default() -> str:
|
||||
"""What this instance renders in when nobody has said otherwise.
|
||||
|
||||
Never raises: a language that cannot be read is not a reason to fail a page,
|
||||
and English is a usable answer. The same argument `branding.snapshot` makes.
|
||||
"""
|
||||
global _DEFAULT
|
||||
if _DEFAULT is not None:
|
||||
return _DEFAULT
|
||||
try:
|
||||
from lembas.db.session import session_scope
|
||||
from lembas.services import settings_store
|
||||
|
||||
with session_scope() as db:
|
||||
_DEFAULT = known(settings_store.get(db, "language"))
|
||||
except Exception: # noqa: BLE001 - defaults are a usable answer
|
||||
log.debug("could not read the instance language; using %s", SOURCE, exc_info=True)
|
||||
return SOURCE
|
||||
return _DEFAULT
|
||||
|
||||
|
||||
def forget() -> None:
|
||||
"""Drop the cached instance default. Called by whoever saves it."""
|
||||
global _DEFAULT
|
||||
_DEFAULT = None
|
||||
|
||||
|
||||
def for_user(user) -> str:
|
||||
"""The language one person sees: their own choice, else the instance's.
|
||||
|
||||
A `User` or None, so a signed-out page -- the sign-in screen, an error page --
|
||||
is rendered in the instance's language rather than in English by accident.
|
||||
"""
|
||||
chosen = ""
|
||||
if user is not None:
|
||||
chosen = str((getattr(user, "settings_json", None) or {}).get("language") or "")
|
||||
return known(chosen) if chosen else instance_default()
|
||||
|
||||
|
||||
def known(code: str | None) -> str:
|
||||
"""A language this application has, from whatever was stored or requested."""
|
||||
value = (code or "").strip().lower()
|
||||
if value in LANGUAGE_IDS:
|
||||
return value
|
||||
# A stored value from a release that offered more languages than this one, or
|
||||
# a hand-edited row. English rather than an error: a preference nobody can
|
||||
# satisfy is not a reason to refuse somebody their settings page.
|
||||
return SOURCE
|
||||
|
||||
|
||||
def catalogue(code: str) -> Mapping[str, str]:
|
||||
"""Every translation for one language, keyed by its English source."""
|
||||
code = known(code)
|
||||
if code == SOURCE:
|
||||
return {}
|
||||
if code not in _catalogues:
|
||||
try:
|
||||
module = __import__(f"lembas.web.i18n.{code}", fromlist=["MESSAGES"])
|
||||
_catalogues[code] = dict(getattr(module, "MESSAGES", {}))
|
||||
except Exception: # noqa: BLE001 - a broken catalogue must not break the page
|
||||
log.exception("could not load the %s catalogue", code)
|
||||
_catalogues[code] = {}
|
||||
return _catalogues[code]
|
||||
|
||||
|
||||
def active() -> str:
|
||||
return _active.get()
|
||||
|
||||
|
||||
def activate(code: str | None) -> str:
|
||||
"""Set the language for this request. Returns what was actually set."""
|
||||
code = known(code)
|
||||
_active.set(code)
|
||||
return code
|
||||
|
||||
|
||||
def direction(code: str | None = None) -> str:
|
||||
return DIRECTIONS.get(known(code) if code else active(), "ltr")
|
||||
|
||||
|
||||
def translate(text: str, code: str | None = None) -> str:
|
||||
"""One string in the active language, or the English it was written in.
|
||||
|
||||
Whitespace is collapsed for the *lookup* and not for the output. A template
|
||||
wraps its prose across lines for the width of the file, so the same sentence
|
||||
reaches here with different newlines in it depending on where it sits -- and a
|
||||
catalogue keyed on the exact bytes would need an entry per wrapping. What is
|
||||
returned is the translation as written in the catalogue, or the original text
|
||||
untouched.
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
entries = catalogue(code or active())
|
||||
if not entries:
|
||||
return text
|
||||
return entries.get(" ".join(text.split()), text)
|
||||
|
||||
|
||||
def t(text: str, **fields: object) -> str:
|
||||
"""The function templates and routes call.
|
||||
|
||||
`t("Saved %(name)s", name=x)` rather than an f-string, because a translator
|
||||
needs the whole sentence and because the order of the parts is not the same in
|
||||
every language. Percent-named rather than `str.format`, so a stray brace in a
|
||||
translation cannot raise.
|
||||
"""
|
||||
out = translate(text)
|
||||
if not fields:
|
||||
return out
|
||||
try:
|
||||
return out % fields
|
||||
except (KeyError, TypeError, ValueError):
|
||||
# A catalogue whose placeholders do not match the source is a bug in the
|
||||
# catalogue, and the English sentence is a better answer than a traceback
|
||||
# in the middle of somebody's page.
|
||||
log.warning("placeholder mismatch translating %r", text[:60])
|
||||
try:
|
||||
return text % fields
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return text
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DIRECTIONS",
|
||||
"LANGUAGES",
|
||||
"LANGUAGE_IDS",
|
||||
"SOURCE",
|
||||
"activate",
|
||||
"active",
|
||||
"catalogue",
|
||||
"direction",
|
||||
"forget",
|
||||
"for_user",
|
||||
"instance_default",
|
||||
"known",
|
||||
"stamp",
|
||||
"t",
|
||||
"translate",
|
||||
]
|
||||
|
||||
|
||||
# --- Dates ---------------------------------------------------------------------
|
||||
#
|
||||
# `strftime("%A")` and `%B` emit C-locale English whatever the page is in, which is
|
||||
# invisible until the day a second language ships and then wrong on every screen
|
||||
# showing a date. Setting a process locale is not an option: it is global, it is
|
||||
# not thread-safe, and this application renders two people's pages at once.
|
||||
#
|
||||
# So the names are a table and the *format* is itself translatable -- Slovak wants
|
||||
# "26. septembra 2026", not "26 September 2026", and that is a different pattern
|
||||
# rather than different words in the same one.
|
||||
#
|
||||
# ⚠ Only for what a **person** reads. `harness.py`, `schedule/runner.py` and
|
||||
# `schedule/compile.py` all put dates in front of a *model*, and those stay
|
||||
# English: the prompts are written in English, a model is not the reader, and a
|
||||
# background task has no request language anyway.
|
||||
MONTHS: Mapping[str, Mapping[str, str]] = {
|
||||
"sk": {
|
||||
"January": "januára",
|
||||
"February": "februára",
|
||||
"March": "marca",
|
||||
"April": "apríla",
|
||||
"May": "mája",
|
||||
"June": "júna",
|
||||
"July": "júla",
|
||||
"August": "augusta",
|
||||
"September": "septembra",
|
||||
"October": "októbra",
|
||||
"November": "novembra",
|
||||
"December": "decembra",
|
||||
}
|
||||
}
|
||||
|
||||
DAYS: Mapping[str, Mapping[str, str]] = {
|
||||
"sk": {
|
||||
"Monday": "pondelok",
|
||||
"Tuesday": "utorok",
|
||||
"Wednesday": "streda",
|
||||
"Thursday": "štvrtok",
|
||||
"Friday": "piatok",
|
||||
"Saturday": "sobota",
|
||||
"Sunday": "nedeľa",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def stamp(value, fmt: str) -> str:
|
||||
"""One date, in the reader's language.
|
||||
|
||||
`fmt` is an English `strftime` pattern and is translated like any other
|
||||
string, so a language that puts the day before the month -- or wants a full
|
||||
stop after it -- says so in the catalogue rather than here.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
code = active()
|
||||
text = value.strftime(translate(fmt, code))
|
||||
for table in (MONTHS.get(code, {}), DAYS.get(code, {})):
|
||||
for english, local in table.items():
|
||||
text = text.replace(english, local)
|
||||
return text
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
<span class="badge">{{ model_count }} model{{ '' if model_count == 1 else 's' }}</span>
|
||||
{% endif %}
|
||||
{% if not connection.enabled %}
|
||||
<span class="badge">disabled</span>
|
||||
<span class="badge">{{ t("disabled") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
formnovalidate>
|
||||
{{ icon("refresh", "icon--sm") }} Test & refresh
|
||||
</button>
|
||||
<button class="btn btn--sm btn--primary" type="submit">Save</button>
|
||||
<button class="btn btn--sm btn--primary" type="submit">{{ t("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,23 +45,23 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="name-{{ connection.id }}">Name</label>
|
||||
<label class="field__label" for="name-{{ connection.id }}">{{ t("Name") }}</label>
|
||||
<input class="input" id="name-{{ connection.id }}" name="name"
|
||||
value="{{ connection.name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="url-{{ connection.id }}">Base URL</label>
|
||||
<label class="field__label" for="url-{{ connection.id }}">{{ t("Base URL") }}</label>
|
||||
<input class="input input--mono" id="url-{{ connection.id }}" name="base_url"
|
||||
value="{{ connection.base_url }}" required>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="key-{{ connection.id }}">API key</label>
|
||||
<label class="field__label" for="key-{{ connection.id }}">{{ t("API key") }}</label>
|
||||
<input class="input input--mono" id="key-{{ connection.id }}" name="api_key"
|
||||
type="password" autocomplete="off"
|
||||
value="{{ unchanged if connection.api_key_encrypted else '' }}"
|
||||
placeholder="No key set">
|
||||
placeholder="{{ t('No key set') }}">
|
||||
<p class="field__hint">
|
||||
{% if connection.api_key_encrypted %}
|
||||
Currently <code>{{ masked }}</code>. Leave the dots alone to keep it,
|
||||
@@ -73,14 +73,14 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="unload-{{ connection.id }}">Unload URL</label>
|
||||
<label class="field__label" for="unload-{{ connection.id }}">{{ t("Unload URL") }}</label>
|
||||
<div class="btn-row">
|
||||
<input class="input input--mono" id="unload-{{ connection.id }}" name="unload_url"
|
||||
value="{{ connection.unload_url }}" placeholder="No unload call"
|
||||
value="{{ connection.unload_url }}" placeholder="{{ t('No unload call') }}"
|
||||
style="flex: 1; min-width: 0">
|
||||
<select class="select" name="unload_method" aria-label="How to ask it to unload" style="flex: none">
|
||||
<option value="POST" {{ 'selected' if connection.unload_method != 'GET' }}>POST</option>
|
||||
<option value="GET" {{ 'selected' if connection.unload_method == 'GET' }}>GET</option>
|
||||
<select class="select" name="unload_method" aria-label="{{ t('How to ask it to unload') }}" style="flex: none">
|
||||
<option value="POST" {{ 'selected' if connection.unload_method != 'GET' }}>{{ t("POST") }}</option>
|
||||
<option value="GET" {{ 'selected' if connection.unload_method == 'GET' }}>{{ t("GET") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
@@ -92,10 +92,10 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="headers-{{ connection.id }}">Extra headers</label>
|
||||
<label class="field__label" for="headers-{{ connection.id }}">{{ t("Extra headers") }}</label>
|
||||
<textarea class="textarea input--mono" id="headers-{{ connection.id }}"
|
||||
name="extra_headers" rows="2"
|
||||
placeholder="HTTP-Referer: https://example.org">{% for name, value in (connection.extra_headers_json or {}).items() %}{{ name }}: {{ value }}
|
||||
placeholder="{{ t('HTTP-Referer: https://example.org') }}">{% for name, value in (connection.extra_headers_json or {}).items() %}{{ name }}: {{ value }}
|
||||
{% endfor %}</textarea>
|
||||
<p class="field__hint">
|
||||
One <code>Name: value</code> per line, sent with every request to this
|
||||
@@ -109,7 +109,7 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true"
|
||||
{{ 'checked' if connection.enabled }}>
|
||||
<span>Enabled — its models are offered in chats</span>
|
||||
<span>{{ t("Enabled — its models are offered in chats") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -29,76 +29,76 @@
|
||||
{% include "partials/_sidebar_close.html" %}
|
||||
</header>
|
||||
|
||||
<nav class="sidebar__scroll" aria-label="Administration">
|
||||
<nav class="sidebar__scroll" aria-label="{{ t('Administration') }}">
|
||||
<div class="nav-group">
|
||||
<div class="nav-group__label">Administration</div>
|
||||
<a class="nav-item {{ 'is-active' if section == 'general' }}" href="/admin/general">
|
||||
{{ icon("gear", "icon--sm") }}
|
||||
<span class="nav-item__label">General</span>
|
||||
<span class="nav-item__label">{{ t("General") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'customization' }}"
|
||||
href="/admin/customization">
|
||||
{{ icon("sun", "icon--sm") }}
|
||||
<span class="nav-item__label">Customization</span>
|
||||
<span class="nav-item__label">{{ t("Customization") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'connections' }}"
|
||||
href="/admin/connections">
|
||||
{{ icon("server", "icon--sm") }}
|
||||
<span class="nav-item__label">Connections</span>
|
||||
<span class="nav-item__label">{{ t("Connections") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'models' }}" href="/admin/models">
|
||||
{{ icon("sliders", "icon--sm") }}
|
||||
<span class="nav-item__label">Models</span>
|
||||
<span class="nav-item__label">{{ t("Models") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'audio' }}" href="/admin/audio">
|
||||
{{ icon("speaker", "icon--sm") }}
|
||||
<span class="nav-item__label">Audio</span>
|
||||
<span class="nav-item__label">{{ t("Audio") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'search' }}" href="/admin/search">
|
||||
{{ icon("globe", "icon--sm") }}
|
||||
<span class="nav-item__label">Web search</span>
|
||||
<span class="nav-item__label">{{ t("Web search") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'images' }}" href="/admin/images">
|
||||
{{ icon("image", "icon--sm") }}
|
||||
<span class="nav-item__label">Image generation</span>
|
||||
<span class="nav-item__label">{{ t("Image generation") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'extraction' }}"
|
||||
href="/admin/extraction">
|
||||
{{ icon("file-text", "icon--sm") }}
|
||||
<span class="nav-item__label">Extraction</span>
|
||||
<span class="nav-item__label">{{ t("Extraction") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'tools' }}" href="/admin/tools">
|
||||
{{ icon("link", "icon--sm") }}
|
||||
<span class="nav-item__label">Tools</span>
|
||||
<span class="nav-item__label">{{ t("Tools") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'agents' }}" href="/admin/agents">
|
||||
{{ icon("sparkle", "icon--sm") }}
|
||||
<span class="nav-item__label">Agents</span>
|
||||
<span class="nav-item__label">{{ t("Agents") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'schedules' }}" href="/admin/schedules">
|
||||
{{ icon("clock", "icon--sm") }}
|
||||
<span class="nav-item__label">Scheduling</span>
|
||||
<span class="nav-item__label">{{ t("Scheduling") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'mcp' }}" href="/admin/mcp">
|
||||
{{ icon("server", "icon--sm") }}
|
||||
<span class="nav-item__label">MCP servers</span>
|
||||
<span class="nav-item__label">{{ t("MCP servers") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'prompts' }}" href="/admin/prompts">
|
||||
{{ icon("sparkle", "icon--sm") }}
|
||||
<span class="nav-item__label">Prompts</span>
|
||||
<span class="nav-item__label">{{ t("Prompts") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'suggestions' }}"
|
||||
href="/admin/suggestions">
|
||||
{{ icon("star", "icon--sm") }}
|
||||
<span class="nav-item__label">Suggestions</span>
|
||||
<span class="nav-item__label">{{ t("Suggestions") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'users' }}" href="/admin/users">
|
||||
{{ icon("user", "icon--sm") }}
|
||||
<span class="nav-item__label">Users</span>
|
||||
<span class="nav-item__label">{{ t("Users") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'updates' }}" href="/admin/updates">
|
||||
{{ icon("refresh", "icon--sm") }}
|
||||
<span class="nav-item__label">Updates</span>
|
||||
<span class="nav-item__label">{{ t("Updates") }}</span>
|
||||
</a>
|
||||
<a class="nav-item {{ 'is-active' if section == 'groups' }}" href="/admin/groups">
|
||||
{{ icon("users", "icon--sm") }}
|
||||
@@ -111,7 +111,7 @@
|
||||
<div class="sidebar__footer">
|
||||
<a class="nav-item" href="/chat">
|
||||
{{ icon("chat", "icon--sm") }}
|
||||
<span class="nav-item__label">Back to chats</span>
|
||||
<span class="nav-item__label">{{ t("Back to chats") }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -122,7 +122,7 @@
|
||||
<header class="topbar">
|
||||
{% include "partials/_sidebar_toggle.html" %}
|
||||
<h1 class="topbar__title">{% block heading %}Administration{% endblock %}</h1>
|
||||
<button class="btn btn--icon" type="button" data-theme-toggle aria-label="Switch theme">
|
||||
<button class="btn btn--icon" type="button" data-theme-toggle aria-label="{{ t('Switch theme') }}">
|
||||
<span class="theme-icon theme-icon--dark">{{ icon("moon") }}</span>
|
||||
<span class="theme-icon theme-icon--light">{{ icon("sun") }}</span>
|
||||
</button>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<div class="model-row__title">
|
||||
<a class="model-row__name" href="/admin/mcp/{{ server.id }}/edit">{{ server.name }}</a>
|
||||
<span class="badge">{{ tool_count }} tool{{ '' if tool_count == 1 else 's' }}</span>
|
||||
{% if not server.enabled %}<span class="badge badge--danger">disabled</span>{% endif %}
|
||||
{% if not server.public %}<span class="badge">restricted</span>{% endif %}
|
||||
{% if server.allow_private %}<span class="badge">private network</span>{% endif %}
|
||||
{% if not server.enabled %}<span class="badge badge--danger">{{ t("disabled") }}</span>{% endif %}
|
||||
{% if not server.public %}<span class="badge">{{ t("restricted") }}</span>{% endif %}
|
||||
{% if server.allow_private %}<span class="badge">{{ t("private network") }}</span>{% endif %}
|
||||
{% if server.protocol_version %}
|
||||
<span class="badge badge--leaf">MCP {{ server.protocol_version }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,17 +7,15 @@
|
||||
<div class="card__header">
|
||||
<h2 class="card__title">
|
||||
{{ fragment.label }}
|
||||
{% if overridden %}<span class="badge badge--leaf">edited</span>{% endif %}
|
||||
{% if overridden %}<span class="badge badge--leaf">{{ t("edited") }}</span>{% endif %}
|
||||
{% for family in fragment.families %}<span class="badge">{{ family }}</span>{% endfor %}
|
||||
{% if fragment.when_tools %}<span class="badge">with tools</span>{% endif %}
|
||||
{% if fragment.when_tools %}<span class="badge">{{ t("with tools") }}</span>{% endif %}
|
||||
</h2>
|
||||
<button class="btn btn--sm" type="button"
|
||||
hx-post="/admin/prompts/default"
|
||||
hx-vals='{"key": "{{ fragment.key }}"}'
|
||||
hx-target="#{{ field_id }}" hx-swap="outerHTML"
|
||||
hx-confirm="Put the built-in wording back in this box? Your edit is lost, but nothing is saved until you press Save settings.">
|
||||
Use default
|
||||
</button>
|
||||
hx-confirm="Put the built-in wording back in this box? Your edit is lost, but nothing is saved until you press Save settings.">{{ t("Use default") }}</button>
|
||||
</div>
|
||||
|
||||
{% if fragment.hint %}<p class="card__lede">{{ fragment.hint }}</p>{% endif %}
|
||||
|
||||
@@ -27,15 +27,10 @@
|
||||
</p>
|
||||
|
||||
{% if title_prompt %}
|
||||
<h3 class="admin-section-title">Chat title request</h3>
|
||||
<p class="card__lede">
|
||||
Sent on its own after the first reply, not as part of any conversation.
|
||||
</p>
|
||||
<h3 class="admin-section-title">{{ t("Chat title request") }}</h3>
|
||||
<p class="card__lede">{{ t("Sent on its own after the first reply, not as part of any conversation.") }}</p>
|
||||
<pre class="prompt-preview"><code>{{ title_prompt }}</code></pre>
|
||||
{% else %}
|
||||
<h3 class="admin-section-title">Chat title request</h3>
|
||||
<p class="card__lede">
|
||||
Empty, so no model is asked to name a chat. Chats are named from the first
|
||||
thing said in them.
|
||||
</p>
|
||||
<h3 class="admin-section-title">{{ t("Chat title request") }}</h3>
|
||||
<p class="card__lede">{{ t("Empty, so no model is asked to name a chat. Chats are named from the first thing said in them.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
{% endif %}
|
||||
|
||||
{% if outcome %}
|
||||
<p class="field__hint">This is what the model would read back:</p>
|
||||
<p class="field__hint">{{ t("This is what the model would read back:") }}</p>
|
||||
<pre class="tool-result__text">{{ outcome.content }}</pre>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
An <strong>Agent</strong> chat can read files, write files and run commands on
|
||||
An <strong>{{ t("Agent") }}</strong> chat can read files, write files and run commands on
|
||||
a machine reached over SSH. Nothing runs on this server. People add their own
|
||||
connections under <strong>Connections</strong>; what you decide here is
|
||||
connections under <strong>{{ t("Connections") }}</strong>; what you decide here is
|
||||
whether the feature exists and what one reply may spend.
|
||||
</p>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
whatever host somebody points a connection at. A container built for the
|
||||
job is a very different thing from a key to a live server, and {{ brand.name }}
|
||||
cannot tell them apart. What a model reads — a web page, a file, the output
|
||||
of the last command — is untrusted, and in <strong>Auto</strong> mode
|
||||
of the last command — is untrusted, and in <strong>{{ t("Auto") }}</strong> mode
|
||||
nothing stands between that and a command running.
|
||||
</span>
|
||||
</div>
|
||||
@@ -30,17 +30,17 @@
|
||||
{% endif %}
|
||||
|
||||
{% if saved %}
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>Saved.</span></div>
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ t("Saved.") }}</span></div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/agents" class="form-grid">
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Switch</h2>
|
||||
<h2 class="card__title">{{ t("Switch") }}</h2>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if values.enabled }}>
|
||||
<span>Allow agent chats</span>
|
||||
<span>{{ t("Allow agent chats") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off, nobody can start one and no agent tool is offered, whatever
|
||||
@@ -49,13 +49,13 @@
|
||||
</p>
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
People also need the <strong>Run commands</strong> permission, a model
|
||||
flagged <strong>Agent execution</strong>, and a connection of their own.
|
||||
People also need the <strong>{{ t("Run commands") }}</strong> permission, a model
|
||||
flagged <strong>{{ t("Agent execution") }}</strong>, and a connection of their own.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Connections to this machine</h2>
|
||||
<h2 class="card__title">{{ t("Connections to this machine") }}</h2>
|
||||
<p class="card__lede">
|
||||
Agent chats reach a machine over SSH, and the point of that is that it is
|
||||
not this one — nothing runs on the host holding the database and the
|
||||
@@ -76,11 +76,11 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="loopback_port">The allowed port</label>
|
||||
<label class="field__label" for="loopback_port">{{ t("The allowed port") }}</label>
|
||||
<input class="input" type="number" id="loopback_port" name="loopback_port"
|
||||
min="0" max="65535" step="1" value="{{ values.loopback_port or 0 }}">
|
||||
<p class="field__hint">
|
||||
Only read when the position above is <strong>Only on one port</strong>.
|
||||
Only read when the position above is <strong>{{ t("Only on one port") }}</strong>.
|
||||
Port 22 is refused whatever is typed here — that one is this host's own
|
||||
sshd, not a container that published its port on the loopback interface.
|
||||
</p>
|
||||
@@ -88,11 +88,8 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">The modes</h2>
|
||||
<p class="field__hint">
|
||||
Set per chat and switchable at any time. This is what each one means; the
|
||||
two lists below adjust them.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("The modes") }}</h2>
|
||||
<p class="field__hint">{{ t("Set per chat and switchable at any time. This is what each one means; the two lists below adjust them.") }}</p>
|
||||
<dl class="mode-list">
|
||||
{% for value, label, hint in modes %}
|
||||
<div class="mode-list__row">
|
||||
@@ -104,9 +101,9 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">What never needs asking</h2>
|
||||
<h2 class="card__title">{{ t("What never needs asking") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="allow_default">Always allow</label>
|
||||
<label class="field__label" for="allow_default">{{ t("Always allow") }}</label>
|
||||
<textarea class="textarea input--mono" id="allow_default" name="allow_default" rows="5"
|
||||
spellcheck="false">{{ allow_text }}</textarea>
|
||||
<p class="field__hint">
|
||||
@@ -120,18 +117,18 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">What always needs asking</h2>
|
||||
<h2 class="card__title">{{ t("What always needs asking") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="deny_default">Always ask</label>
|
||||
<label class="field__label" for="deny_default">{{ t("Always ask") }}</label>
|
||||
<textarea class="textarea input--mono" id="deny_default" name="deny_default" rows="5"
|
||||
spellcheck="false">{{ deny_text }}</textarea>
|
||||
<p class="field__hint">
|
||||
Checked before everything, including <strong>Auto</strong>. Treat it as
|
||||
Checked before everything, including <strong>{{ t("Auto") }}</strong>. Treat it as
|
||||
a guard against an accident rather than against an adversary:
|
||||
<code>rm -rf /*</code> here does not stop <code>/bin/rm -rf /</code>, and
|
||||
nothing pattern-shaped could. The same limit as above applies, and it
|
||||
cuts the other way here: a command line that runs more than one thing
|
||||
matches none of these, so in <strong>Auto</strong>
|
||||
matches none of these, so in <strong>{{ t("Auto") }}</strong>
|
||||
<code>shutdown -h now</code> asks and <code>shutdown -h now &</code>
|
||||
runs. Anything that must never happen belongs on the far side, in that
|
||||
account’s own permissions.
|
||||
@@ -140,40 +137,34 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">What one command may spend</h2>
|
||||
<h2 class="card__title">{{ t("What one command may spend") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_timeout">Timeout (seconds)</label>
|
||||
<label class="field__label" for="default_timeout">{{ t("Timeout (seconds)") }}</label>
|
||||
<input class="input" id="default_timeout" name="default_timeout"
|
||||
value="{{ values.default_timeout }}" inputmode="numeric">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_timeout">Longest a command may ask for</label>
|
||||
<label class="field__label" for="max_timeout">{{ t("Longest a command may ask for") }}</label>
|
||||
<input class="input" id="max_timeout" name="max_timeout"
|
||||
value="{{ values.max_timeout }}" inputmode="numeric">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_output_bytes">Most output to keep</label>
|
||||
<label class="field__label" for="max_output_bytes">{{ t("Most output to keep") }}</label>
|
||||
<input class="input" id="max_output_bytes" name="max_output_bytes"
|
||||
value="{{ values.max_output_bytes }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
Characters. The rest is cut off and the model is told so.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Characters. The rest is cut off and the model is told so.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Background commands</h2>
|
||||
<p class="card__lede">
|
||||
A command that would outlast its timeout can be left running instead of
|
||||
killed — detached on the far side, checked on later. It is how a long
|
||||
install, build or download becomes possible at all.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Background commands") }}</h2>
|
||||
<p class="card__lede">{{ t("A command that would outlast its timeout can be left running instead of killed — detached on the far side, checked on later. It is how a long install, build or download becomes possible at all.") }}</p>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="background_enabled"
|
||||
{{ 'checked' if values.background_enabled }}>
|
||||
<span>Allow commands to run in the background</span>
|
||||
<span>{{ t("Allow commands to run in the background") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off means byte-for-byte the old behaviour: a command that hits its
|
||||
@@ -188,63 +179,44 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="background_on_timeout"
|
||||
{{ 'checked' if values.background_on_timeout }}>
|
||||
<span>Keep a timed-out command running instead of killing it</span>
|
||||
<span>{{ t("Keep a timed-out command running instead of killing it") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off leaves the timeout a hard stop; the model can still choose to
|
||||
background a command up front.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Off leaves the timeout a hard stop; the model can still choose to background a command up front.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="background_notify"
|
||||
{{ 'checked' if values.background_notify }}>
|
||||
<span>Wake the model when a background job finishes</span>
|
||||
<span>{{ t("Wake the model when a background job finishes") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
On, a finished job starts (or joins) a reply carrying its result. Off,
|
||||
the model only sees it the next time it runs of its own accord.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("On, a finished job starts (or joins) a reply carrying its result. Off, the model only sees it the next time it runs of its own accord.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="background_max_jobs">Most jobs watched at once</label>
|
||||
<label class="field__label" for="background_max_jobs">{{ t("Most jobs watched at once") }}</label>
|
||||
<input class="input" id="background_max_jobs" name="background_max_jobs"
|
||||
value="{{ values.background_max_jobs }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
Each is a periodic reconnect to the machine. Jobs past this still run;
|
||||
they are simply not watched, and the model is not woken for them.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Each is a periodic reconnect to the machine. Jobs past this still run; they are simply not watched, and the model is not woken for them.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">What one reply may spend</h2>
|
||||
<p class="field__hint">
|
||||
Four separate bounds, because they fail differently: the clock stops one
|
||||
slow command eating an afternoon, tool output stops a model filling its own
|
||||
context with build logs and having no room to answer, written tokens stop
|
||||
one that keeps going, and the step count is a backstop against a runaway.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("What one reply may spend") }}</h2>
|
||||
<p class="field__hint">{{ t("Four separate bounds, because they fail differently: the clock stops one slow command eating an afternoon, tool output stops a model filling its own context with build logs and having no room to answer, written tokens stop one that keeps going, and the step count is a backstop against a runaway.") }}</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_completion_tokens">
|
||||
Most a reply may write
|
||||
</label>
|
||||
<label class="field__label" for="max_completion_tokens">{{ t("Most a reply may write") }}</label>
|
||||
<input class="input" id="max_completion_tokens" name="max_completion_tokens"
|
||||
value="{{ values.max_completion_tokens }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
In tokens, across every round of one reply. This is the bound that
|
||||
normally ends a long piece of work. Zero means no ceiling.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("In tokens, across every round of one reply. This is the bound that normally ends a long piece of work. Zero means no ceiling.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_wall_seconds">Longest a reply may take</label>
|
||||
<label class="field__label" for="max_wall_seconds">{{ t("Longest a reply may take") }}</label>
|
||||
<input class="input" id="max_wall_seconds" name="max_wall_seconds"
|
||||
value="{{ values.max_wall_seconds }}" inputmode="numeric">
|
||||
<p class="field__hint">Time spent waiting for you to answer does not count.</p>
|
||||
<p class="field__hint">{{ t("Time spent waiting for you to answer does not count.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_total_output_bytes">Most output across a reply</label>
|
||||
<label class="field__label" for="max_total_output_bytes">{{ t("Most output across a reply") }}</label>
|
||||
<input class="input" id="max_total_output_bytes" name="max_total_output_bytes"
|
||||
value="{{ values.max_total_output_bytes }}" inputmode="numeric">
|
||||
</div>
|
||||
@@ -252,61 +224,44 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="nudge_unfinished"
|
||||
{{ 'checked' if values.nudge_unfinished }}>
|
||||
<span>Ask it to carry on when it stops with tasks outstanding</span>
|
||||
<span>{{ t("Ask it to carry on when it stops with tasks outstanding") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Only ever against a plan, and only while tasks on it are still open —
|
||||
that is the one thing there is to be objectively wrong about. A reply
|
||||
with no plan that says it has finished is believed. It is asked at most
|
||||
twice in a row, and if it stops a third time that is recorded in the
|
||||
transcript rather than argued with.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Only ever against a plan, and only while tasks on it are still open — that is the one thing there is to be objectively wrong about. A reply with no plan that says it has finished is believed. It is asked at most twice in a row, and if it stops a third time that is recorded in the transcript rather than argued with.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_steps">Most rounds of tool calls</label>
|
||||
<label class="field__label" for="max_steps">{{ t("Most rounds of tool calls") }}</label>
|
||||
<input class="input" id="max_steps" name="max_steps"
|
||||
value="{{ values.max_steps }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
A backstop, not a working budget. An agent reply is meant to run until
|
||||
the task is done, so a number low enough to be what stops it is a number
|
||||
that stops it halfway. Use the token ceiling above for a real limit.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("A backstop, not a working budget. An agent reply is meant to run until the task is done, so a number low enough to be what stops it is a number that stops it halfway. Use the token ceiling above for a real limit.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Asking you things</h2>
|
||||
<h2 class="card__title">{{ t("Asking you things") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="approval_timeout">How long a question waits</label>
|
||||
<label class="field__label" for="approval_timeout">{{ t("How long a question waits") }}</label>
|
||||
<input class="input" id="approval_timeout" name="approval_timeout"
|
||||
value="{{ values.approval_timeout }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
Seconds. After this the reply carries on without an answer and says so.
|
||||
At least a minute, whatever is typed here.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Seconds. After this the reply carries on without an answer and says so. At least a minute, whatever is typed here.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="ask_free_text" value="true"
|
||||
{{ 'checked' if values.ask_free_text }}>
|
||||
<span>Let people write their own answer</span>
|
||||
<span>{{ t("Let people write their own answer") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
When a model asks a question it can offer answers to pick from, and by
|
||||
default a box to write something else. Turn this off if you would rather
|
||||
nobody typed free text into a prompt a model composed.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("When a model asks a question it can offer answers to pick from, and by default a box to write something else. Turn this off if you would rather nobody typed free text into a prompt a model composed.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">The terminal</h2>
|
||||
<h2 class="card__title">{{ t("The terminal") }}</h2>
|
||||
<p class="field__hint">
|
||||
A panel beside an agent chat holding an interactive shell on that chat's
|
||||
own connection. What somebody types there is <em>theirs</em>: the modes and
|
||||
own connection. What somebody types there is <em>{{ t("theirs") }}</em>: the modes and
|
||||
the two lists above govern the model, not the person at the keyboard, who
|
||||
could open the same shell with an ssh client. The model cannot see the
|
||||
panel; sending it something is a button they press.
|
||||
@@ -316,47 +271,45 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="terminal_enabled" value="true"
|
||||
{{ 'checked' if values.terminal_enabled }}>
|
||||
<span>Allow the terminal panel</span>
|
||||
<span>{{ t("Allow the terminal panel") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
People also need the <strong>Open a terminal</strong> permission.
|
||||
People also need the <strong>{{ t("Open a terminal") }}</strong> permission.
|
||||
{{ terminal_count }} shell{{ '' if terminal_count == 1 else 's' }} open right now.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="terminal_idle_timeout">Close a shell after</label>
|
||||
<label class="field__label" for="terminal_idle_timeout">{{ t("Close a shell after") }}</label>
|
||||
<input class="input" id="terminal_idle_timeout" name="terminal_idle_timeout"
|
||||
value="{{ values.terminal_idle_timeout }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
Seconds with nobody watching <em>and</em> nothing typed. Closing the
|
||||
Seconds with nobody watching <em>{{ t("and") }}</em> nothing typed. Closing the
|
||||
panel does not end the session — a build carries on and is still there
|
||||
on the way back — so this is what eventually ends one.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="terminal_max_sessions">Most shells at once</label>
|
||||
<label class="field__label" for="terminal_max_sessions">{{ t("Most shells at once") }}</label>
|
||||
<input class="input" id="terminal_max_sessions" name="terminal_max_sessions"
|
||||
value="{{ values.terminal_max_sessions }}" inputmode="numeric">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="terminal_max_per_user">Most shells per person</label>
|
||||
<label class="field__label" for="terminal_max_per_user">{{ t("Most shells per person") }}</label>
|
||||
<input class="input" id="terminal_max_per_user" name="terminal_max_per_user"
|
||||
value="{{ values.terminal_max_per_user }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
One per chat. Each holds an SSH connection open on the far machine.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("One per chat. Each holds an SSH connection open on the far machine.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="terminal_integration"
|
||||
{{ 'checked' if values.terminal_integration }}>
|
||||
<span>Mark where commands begin and end</span>
|
||||
<span>{{ t("Mark where commands begin and end") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Gives bash and zsh the same invisible markers VS Code and WezTerm use,
|
||||
so <strong>Copy</strong>, <strong>Send</strong> and the automatic
|
||||
so <strong>{{ t("Copy") }}</strong>, <strong>{{ t("Send") }}</strong> and the automatic
|
||||
toggle know which output belongs to which command. Written by the shell
|
||||
into a temporary file it deletes itself, and any other shell is started
|
||||
exactly as it was before. Off means those buttons fall back to copying
|
||||
@@ -366,7 +319,7 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="section-title">The project directory</h2>
|
||||
<h2 class="section-title">{{ t("The project directory") }}</h2>
|
||||
<p class="muted">
|
||||
A listing of the directory a chat works in, so a reply does not spend its
|
||||
first rounds finding out what is there — and so files can be attached by
|
||||
@@ -379,20 +332,17 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="index_enabled"
|
||||
{{ 'checked' if values.index_enabled }}>
|
||||
<span>List the project directory</span>
|
||||
<span>{{ t("List the project directory") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off means no listing is built at all, and the file picker offers only
|
||||
what is in the library.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Off means no listing is built at all, and the file picker offers only what is in the library.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="index_chars">Characters of it in the prompt</label>
|
||||
<label class="field__label" for="index_chars">{{ t("Characters of it in the prompt") }}</label>
|
||||
<input class="input" id="index_chars" name="index_chars"
|
||||
value="{{ values.index_chars }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
This is spent on <em>every</em> request in an agent chat, so it is a
|
||||
This is spent on <em>{{ t("every") }}</em> request in an agent chat, so it is a
|
||||
budget rather than a limit: directories that will not fit are shown as
|
||||
a count and the model is told to look inside them itself.
|
||||
<strong>0</strong> keeps the listing for the file picker and puts none
|
||||
@@ -404,7 +354,7 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="instructions_enabled"
|
||||
{{ 'checked' if values.instructions_enabled }}>
|
||||
<span>Read the project's own instructions</span>
|
||||
<span>{{ t("Read the project's own instructions") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Looks for <code>AGENTS.md</code> or <code>CLAUDE.md</code> in the root
|
||||
@@ -412,14 +362,14 @@
|
||||
the conventions of the project it is working in. The file is written by
|
||||
whoever works on that project, so it is treated as untrusted: it can say
|
||||
how to work, and cannot grant permission for anything. The exact wording
|
||||
around it is the <em>The project's own instructions</em> fragment on
|
||||
around it is the <em>{{ t("The project's own instructions") }}</em> fragment on
|
||||
<a href="/admin/prompts">Prompts</a>, and clearing that fragment removes
|
||||
the only path by which the file reaches a model.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="instructions_chars">Characters of it to use</label>
|
||||
<label class="field__label" for="instructions_chars">{{ t("Characters of it to use") }}</label>
|
||||
<input class="input" id="instructions_chars" name="instructions_chars"
|
||||
value="{{ values.instructions_chars }}" inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
@@ -430,7 +380,7 @@
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save changes</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save changes") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -450,10 +400,10 @@
|
||||
page somebody opens to find out what one turn may set going. #}
|
||||
<form method="post" action="/admin/agents/crowd" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">A crowd</h2>
|
||||
<h2 class="card__title">{{ t("A crowd") }}</h2>
|
||||
<p class="field__hint">
|
||||
A chat can have more than one model in it. The chat's own model answers, then
|
||||
each of the others in turn; then the order runs <strong>backwards</strong>,
|
||||
each of the others in turn; then the order runs <strong>{{ t("backwards") }}</strong>,
|
||||
each one asked whether it disagrees with anything; and it ends back at the
|
||||
first, which either closes or sends them round again.
|
||||
</p>
|
||||
@@ -461,7 +411,7 @@
|
||||
<div class="alert">
|
||||
{{ icon("warning", "icon--sm") }}
|
||||
<span>
|
||||
One turn costs <strong>models × rounds × 2 − 1</strong> replies — four
|
||||
One turn costs <strong>{{ t("models × rounds × 2 − 1") }}</strong> replies — four
|
||||
models over two rounds is fifteen — and on a single local endpoint every
|
||||
change of speaker also loads a different model. Larger crowds of smaller
|
||||
models, and sometimes of bigger ones, start going round in circles: that is
|
||||
@@ -474,78 +424,55 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true"
|
||||
{{ 'checked' if crowd.enabled }}>
|
||||
<span>Let a chat have a crowd</span>
|
||||
<span>{{ t("Let a chat have a crowd") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off by default. With it on, each chat's settings panel offers the other
|
||||
models; a chat with none ticked behaves exactly as it always has.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Off by default. With it on, each chat's settings panel offers the other models; a chat with none ticked behaves exactly as it always has.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="crowd_max_models">Most models besides the chat's own</label>
|
||||
<label class="field__label" for="crowd_max_models">{{ t("Most models besides the chat's own") }}</label>
|
||||
<input class="input" id="crowd_max_models" name="max_models"
|
||||
type="number" min="1" max="8" step="1" value="{{ crowd.max_models }}">
|
||||
<p class="field__hint">
|
||||
Four is already eight replies a turn at one round each. More voices past
|
||||
that tend to repeat each other rather than add anything.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Four is already eight replies a turn at one round each. More voices past that tend to repeat each other rather than add anything.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="crowd_max_rounds">Most rounds</label>
|
||||
<label class="field__label" for="crowd_max_rounds">{{ t("Most rounds") }}</label>
|
||||
<input class="input" id="crowd_max_rounds" name="max_rounds"
|
||||
type="number" min="1" max="5" step="1" value="{{ crowd.max_rounds }}">
|
||||
<p class="field__hint">
|
||||
A round is out and back. Two gives the first model one chance to change its
|
||||
mind after hearing the objections, which is the point of the whole thing;
|
||||
three is where going in circles starts.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("A round is out and back. Two gives the first model one chance to change its mind after hearing the objections, which is the point of the whole thing; three is where going in circles starts.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="crowd_wall_seconds">Longest a turn may take</label>
|
||||
<label class="field__label" for="crowd_wall_seconds">{{ t("Longest a turn may take") }}</label>
|
||||
<input class="input" id="crowd_wall_seconds" name="wall_seconds"
|
||||
type="number" min="60" max="7200" step="30" value="{{ crowd.wall_seconds }}">
|
||||
<p class="field__hint">
|
||||
Across every speaker, not each. A member whose endpoint has stalled cannot
|
||||
then hold the round open all afternoon.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Across every speaker, not each. A member whose endpoint has stalled cannot then hold the round open all afternoon.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="collapse_agreement" value="true"
|
||||
{{ 'checked' if crowd.collapse_agreement }}>
|
||||
<span>Fold away a short "I agree" on the way back</span>
|
||||
<span>{{ t('Fold away a short "I agree" on the way back') }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
The disagreements are what a crowd is for; a column of bubbles saying
|
||||
nothing is what makes somebody switch it off. The text is still there
|
||||
behind a disclosure.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("The disagreements are what a crowd is for; a column of bubbles saying nothing is what makes somebody switch it off. The text is still there behind a disclosure.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/admin/agents/subagents" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Helpers</h2>
|
||||
<h2 class="card__title">{{ t("Helpers") }}</h2>
|
||||
<p class="field__hint">{{ t("A reply can hand a self-contained piece of work to a second model that runs on its own and reports back — several at once, which is what makes research fan out instead of queueing. This applies to ordinary chats as much as agent ones.") }}</p>
|
||||
<p class="field__hint">
|
||||
A reply can hand a self-contained piece of work to a second model that
|
||||
runs on its own and reports back — several at once, which is what makes
|
||||
research fan out instead of queueing. This applies to ordinary chats as
|
||||
much as agent ones.
|
||||
</p>
|
||||
<p class="field__hint">
|
||||
<strong>Asking another model a question uses the same switch and the same
|
||||
allowance below</strong>, because it costs the same thing: one reply
|
||||
<strong>{{ t("Asking another model a question uses the same switch and the same allowance below") }}</strong>, because it costs the same thing: one reply
|
||||
setting another reply going. Which people may do it is a separate
|
||||
permission — <strong>Ask another model</strong> — and which models may is a
|
||||
permission — <strong>{{ t("Ask another model") }}</strong> — and which models may is a
|
||||
switch on each model's own page.
|
||||
</p>
|
||||
|
||||
@@ -554,9 +481,9 @@
|
||||
<span>
|
||||
A helper cannot ask anybody anything, so nothing in its chat can stop
|
||||
for approval. It therefore gets only what this chat could already do
|
||||
<em>without</em> asking: it reads, it searches, and on a machine it runs
|
||||
<em>{{ t("without") }}</em> asking: it reads, it searches, and on a machine it runs
|
||||
a short fixed list of read-only commands and nothing else, in every mode
|
||||
including <strong>Auto</strong>. It cannot send helpers of its own.
|
||||
including <strong>{{ t("Auto") }}</strong>. It cannot send helpers of its own.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -564,11 +491,11 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true"
|
||||
{{ 'checked' if subagents.enabled }}>
|
||||
<span>Let a model delegate</span>
|
||||
<span>{{ t("Let a model delegate") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
People also need the <strong>Delegate to a helper</strong> permission,
|
||||
and the model needs the <strong>Tools</strong> capability. Off by
|
||||
People also need the <strong>{{ t("Delegate to a helper") }}</strong> permission,
|
||||
and the model needs the <strong>{{ t("Tools") }}</strong> capability. Off by
|
||||
default: a reply that spawns helpers spends model time multiplicatively,
|
||||
and on one local endpoint four at once is four times the queue rather
|
||||
than four times the speed.
|
||||
@@ -576,83 +503,60 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="sub_max_per_reply">
|
||||
Most helpers one reply may send
|
||||
</label>
|
||||
<label class="field__label" for="sub_max_per_reply">{{ t("Most helpers one reply may send") }}</label>
|
||||
<input class="input" id="sub_max_per_reply" name="max_per_reply"
|
||||
type="number" min="1" max="20" step="1"
|
||||
value="{{ subagents.max_per_reply }}">
|
||||
<p class="field__hint">
|
||||
Fanning out across a handful of independent questions is what this is
|
||||
for. A reply that wants twenty has misread the tool. Questions put to
|
||||
other models count against this same number, so one reply cannot spend
|
||||
the allowance twice.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Fanning out across a handful of independent questions is what this is for. A reply that wants twenty has misread the tool. Questions put to other models count against this same number, so one reply cannot spend the allowance twice.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="sub_max_concurrent">Running at once, instance-wide</label>
|
||||
<label class="field__label" for="sub_max_concurrent">{{ t("Running at once, instance-wide") }}</label>
|
||||
<input class="input" id="sub_max_concurrent" name="max_concurrent"
|
||||
type="number" min="1" max="50" step="1"
|
||||
value="{{ subagents.max_concurrent }}">
|
||||
<p class="field__hint">
|
||||
Each is a whole generation against the same endpoint the reply that
|
||||
asked for it is waiting on. Past this a model is told to do the work
|
||||
itself rather than made to wait.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Each is a whole generation against the same endpoint the reply that asked for it is waiting on. Past this a model is told to do the work itself rather than made to wait.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="sub_max_completion_tokens">Most a helper may write</label>
|
||||
<label class="field__label" for="sub_max_completion_tokens">{{ t("Most a helper may write") }}</label>
|
||||
<input class="input" id="sub_max_completion_tokens" name="max_completion_tokens"
|
||||
type="number" min="0" max="5000000" step="1000"
|
||||
value="{{ subagents.max_completion_tokens }}">
|
||||
<p class="field__hint">
|
||||
In tokens, across every round. A helper answers one question, so this
|
||||
should run out well before the reply that asked does. Zero means no
|
||||
ceiling.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("In tokens, across every round. A helper answers one question, so this should run out well before the reply that asked does. Zero means no ceiling.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="sub_wall_seconds">Longest a helper may take</label>
|
||||
<label class="field__label" for="sub_wall_seconds">{{ t("Longest a helper may take") }}</label>
|
||||
<input class="input" id="sub_wall_seconds" name="wall_seconds"
|
||||
type="number" min="30" max="7200" step="30"
|
||||
value="{{ subagents.wall_seconds }}">
|
||||
<p class="field__hint">
|
||||
Seconds. Past it the helper is <em>stopped</em>, not abandoned: what it
|
||||
Seconds. Past it the helper is <em>{{ t("stopped") }}</em>, not abandoned: what it
|
||||
had written is kept and handed back with a note saying it is partial.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="sub_max_rounds">Most rounds of tool calls</label>
|
||||
<label class="field__label" for="sub_max_rounds">{{ t("Most rounds of tool calls") }}</label>
|
||||
<input class="input" id="sub_max_rounds" name="max_rounds"
|
||||
type="number" min="1" max="200" step="1"
|
||||
value="{{ subagents.max_rounds }}">
|
||||
<p class="field__hint">
|
||||
A backstop, as it is above. The clock and the token ceiling are what
|
||||
normally end one.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("A backstop, as it is above. The clock and the token ceiling are what normally end one.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="keep_transcript" value="true"
|
||||
{{ 'checked' if subagents.keep_transcript }}>
|
||||
<span>Keep a helper's own chat afterwards</span>
|
||||
<span>{{ t("Keep a helper's own chat afterwards") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off means it is deleted once its answer has been handed over, which is
|
||||
what keeps this cheap to use. Turn it on to work out why one came back
|
||||
with something odd. Kept chats are temporary either way and are swept a
|
||||
day later, and neither appears in anybody's sidebar.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Off means it is deleted once its answer has been handed over, which is what keeps this cheap to use. Turn it on to work out why one came back with something odd. Kept chats are temporary either way and are swept a day later, and neither appears in anybody's sidebar.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save changes</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save changes") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,42 +6,34 @@
|
||||
{% block heading %}Audio{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
Two endpoints speaking the OpenAI audio API: one that turns speech into text
|
||||
so a message can be dictated, one that reads a reply out. They are configured
|
||||
separately because they usually are separate servers — whisper.cpp and Kokoro,
|
||||
say, or Speaches for both.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Two endpoints speaking the OpenAI audio API: one that turns speech into text so a message can be dictated, one that reads a reply out. They are configured separately because they usually are separate servers — whisper.cpp and Kokoro, say, or Speaches for both.") }}</p>
|
||||
|
||||
{% if saved %}
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>Audio settings saved.</span></div>
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ t("Audio settings saved.") }}</span></div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/audio">
|
||||
<section class="card">
|
||||
<h2 class="card__title">
|
||||
Dictation
|
||||
{% if values.stt_enabled %}<span class="badge badge--success">on</span>
|
||||
{% else %}<span class="badge">off</span>{% endif %}
|
||||
{% if values.stt_enabled %}<span class="badge badge--success">{{ t("on") }}</span>
|
||||
{% else %}<span class="badge">{{ t("off") }}</span>{% endif %}
|
||||
</h2>
|
||||
<p class="card__lede">
|
||||
Adds a microphone to the composer. Recordings are sent to this endpoint
|
||||
and never written to disk.
|
||||
</p>
|
||||
<p class="card__lede">{{ t("Adds a microphone to the composer. Recordings are sent to this endpoint and never written to disk.") }}</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="stt_enabled" value="true"
|
||||
{{ 'checked' if values.stt_enabled }}>
|
||||
<span>Allow messages to be dictated</span>
|
||||
<span>{{ t("Allow messages to be dictated") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid--2">
|
||||
<div class="field">
|
||||
<label class="field__label" for="stt-base-url">Base URL</label>
|
||||
<label class="field__label" for="stt-base-url">{{ t("Base URL") }}</label>
|
||||
<input class="input" id="stt-base-url" name="stt_base_url" type="url"
|
||||
value="{{ values.stt_base_url }}" placeholder="http://127.0.0.1:8081">
|
||||
value="{{ values.stt_base_url }}" placeholder="{{ t('http://127.0.0.1:8081') }}">
|
||||
<p class="field__hint">
|
||||
Where <code>/v1/audio/transcriptions</code> lives — whisper.cpp's
|
||||
<code>whisper-server</code>, Speaches, or anything else speaking it.
|
||||
@@ -49,27 +41,27 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="stt-api-key">API key</label>
|
||||
<label class="field__label" for="stt-api-key">{{ t("API key") }}</label>
|
||||
<input class="input" id="stt-api-key" name="stt_api_key" type="password"
|
||||
value="{{ unchanged if masked.stt else '' }}"
|
||||
placeholder="{{ masked.stt or 'None needed for a local server' }}"
|
||||
autocomplete="off">
|
||||
<p class="field__hint">Encrypted at rest. Clear the field to remove it.</p>
|
||||
<p class="field__hint">{{ t("Encrypted at rest. Clear the field to remove it.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="stt-model">Model</label>
|
||||
<label class="field__label" for="stt-model">{{ t("Model") }}</label>
|
||||
<input class="input" id="stt-model" name="stt_model"
|
||||
value="{{ values.stt_model }}" placeholder="whisper-1">
|
||||
value="{{ values.stt_model }}" placeholder="{{ t('whisper-1') }}">
|
||||
<p class="field__hint">
|
||||
A server hosting one model <strong>ignores this</strong> and uses
|
||||
A server hosting one model <strong>{{ t("ignores this") }}</strong> and uses
|
||||
whatever it was started with — <code>whisper-1</code> is then just a
|
||||
label. It only selects anything on a server that hosts several.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="stt-language">Language</label>
|
||||
<label class="field__label" for="stt-language">{{ t("Language") }}</label>
|
||||
<input class="input" id="stt-language" name="stt_language" maxlength="16"
|
||||
value="{{ values.stt_language }}" placeholder="detect">
|
||||
value="{{ values.stt_language }}" placeholder="{{ t('detect') }}">
|
||||
<p class="field__hint">
|
||||
An ISO code such as <code>en</code> or <code>sk</code>. Leave empty to
|
||||
let the server detect it, which is what whisper does best.
|
||||
@@ -89,46 +81,43 @@
|
||||
<section class="card">
|
||||
<h2 class="card__title">
|
||||
Read aloud
|
||||
{% if values.tts_enabled %}<span class="badge badge--success">on</span>
|
||||
{% else %}<span class="badge">off</span>{% endif %}
|
||||
{% if values.tts_enabled %}<span class="badge badge--success">{{ t("on") }}</span>
|
||||
{% else %}<span class="badge">{{ t("off") }}</span>{% endif %}
|
||||
</h2>
|
||||
<p class="card__lede">
|
||||
Adds a speaker button to every reply. Each reader can pick their own voice
|
||||
in their settings; what is chosen here is the default.
|
||||
</p>
|
||||
<p class="card__lede">{{ t("Adds a speaker button to every reply. Each reader can pick their own voice in their settings; what is chosen here is the default.") }}</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="tts_enabled" value="true"
|
||||
{{ 'checked' if values.tts_enabled }}>
|
||||
<span>Allow replies to be read out</span>
|
||||
<span>{{ t("Allow replies to be read out") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid--2">
|
||||
<div class="field">
|
||||
<label class="field__label" for="tts-base-url">Base URL</label>
|
||||
<label class="field__label" for="tts-base-url">{{ t("Base URL") }}</label>
|
||||
<input class="input" id="tts-base-url" name="tts_base_url" type="url"
|
||||
value="{{ values.tts_base_url }}" placeholder="http://127.0.0.1:8880">
|
||||
value="{{ values.tts_base_url }}" placeholder="{{ t('http://127.0.0.1:8880') }}">
|
||||
<p class="field__hint">
|
||||
Where <code>/v1/audio/speech</code> lives — Kokoro-FastAPI, OpenAI, or
|
||||
anything else speaking it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="tts-api-key">API key</label>
|
||||
<label class="field__label" for="tts-api-key">{{ t("API key") }}</label>
|
||||
<input class="input" id="tts-api-key" name="tts_api_key" type="password"
|
||||
value="{{ unchanged if masked.tts else '' }}"
|
||||
placeholder="{{ masked.tts or 'None needed for a local server' }}"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="tts-model">Model</label>
|
||||
<label class="field__label" for="tts-model">{{ t("Model") }}</label>
|
||||
<input class="input" id="tts-model" name="tts_model"
|
||||
value="{{ values.tts_model }}" placeholder="tts-1">
|
||||
value="{{ values.tts_model }}" placeholder="{{ t('tts-1') }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="tts-voice">Default voice</label>
|
||||
<label class="field__label" for="tts-voice">{{ t("Default voice") }}</label>
|
||||
<select class="select" id="tts-voice" name="tts_voice">
|
||||
{% with selected = values.tts_voice %}
|
||||
{% include "partials/_voice_options.html" %}
|
||||
@@ -143,7 +132,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="tts-format">Format</label>
|
||||
<label class="field__label" for="tts-format">{{ t("Format") }}</label>
|
||||
<select class="select" id="tts-format" name="tts_format">
|
||||
{% for format in formats %}
|
||||
<option value="{{ format }}" {{ 'selected' if format == values.tts_format }}>
|
||||
@@ -153,7 +142,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="tts-speed">Speed</label>
|
||||
<label class="field__label" for="tts-speed">{{ t("Speed") }}</label>
|
||||
<input class="input" id="tts-speed" name="tts_speed" type="number"
|
||||
min="0.25" max="4" step="0.05" value="{{ values.tts_speed }}">
|
||||
</div>
|
||||
@@ -163,12 +152,9 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="tts_autoplay" value="true"
|
||||
{{ 'checked' if values.tts_autoplay }}>
|
||||
<span>Read new replies aloud as they finish, by default</span>
|
||||
<span>{{ t("Read new replies aloud as they finish, by default") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Only the starting value for each account — anyone can turn it off in
|
||||
their own settings, and nobody is made to listen.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Only the starting value for each account — anyone can turn it off in their own settings, and nobody is made to listen.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
@@ -180,6 +166,6 @@
|
||||
<div id="audio-test-tts"></div>
|
||||
</section>
|
||||
|
||||
<div class="form-actions"><button class="btn btn--primary" type="submit">Save settings</button></div>
|
||||
<div class="form-actions"><button class="btn btn--primary" type="submit">{{ t("Save settings") }}</button></div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -17,30 +17,27 @@
|
||||
{% endif %}
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Add a connection</h2>
|
||||
<h2 class="card__title">{{ t("Add a connection") }}</h2>
|
||||
<form method="post" action="/admin/connections" >
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-name">Name</label>
|
||||
<input class="input" id="new-name" name="name" required placeholder="Local LM Studio">
|
||||
<label class="field__label" for="new-name">{{ t("Name") }}</label>
|
||||
<input class="input" id="new-name" name="name" required placeholder="{{ t('Local LM Studio') }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-url">Base URL</label>
|
||||
<label class="field__label" for="new-url">{{ t("Base URL") }}</label>
|
||||
<input class="input input--mono" id="new-url" name="base_url" required
|
||||
placeholder="http://localhost:1234/v1">
|
||||
placeholder="{{ t('http://localhost:1234/v1') }}">
|
||||
<p class="field__hint">
|
||||
With or without the trailing <code>/v1</code> — both are accepted.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-key">API key</label>
|
||||
<label class="field__label" for="new-key">{{ t("API key") }}</label>
|
||||
<input class="input input--mono" id="new-key" name="api_key" type="password"
|
||||
autocomplete="off" placeholder="sk-…">
|
||||
<p class="field__hint">
|
||||
Encrypted before it is stored, and never sent back to the browser.
|
||||
Leave empty for endpoints that need no key.
|
||||
</p>
|
||||
autocomplete="off" placeholder="{{ t('sk-…') }}">
|
||||
<p class="field__hint">{{ t("Encrypted before it is stored, and never sent back to the browser. Leave empty for endpoints that need no key.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="btn-row" style="margin-top: var(--sp-5)">
|
||||
@@ -59,9 +56,7 @@
|
||||
{% if not connections %}
|
||||
<div class="empty" style="padding: var(--sp-10) 0">
|
||||
{{ icon("server", "empty__mark") }}
|
||||
<p class="empty__text">
|
||||
Nothing configured yet. Add a connection above and its models appear here.
|
||||
</p>
|
||||
<p class="empty__text">{{ t("Nothing configured yet. Add a connection above and its models appear here.") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
What this installation is called and what it looks like. Every box below ships
|
||||
with a default; leaving one alone means it is <em>not</em> stored, so a later
|
||||
with a default; leaving one alone means it is <em>{{ t("not") }}</em> stored, so a later
|
||||
release can still improve the wording. Clearing a box gives you the shipped
|
||||
version back rather than nothing.
|
||||
</p>
|
||||
@@ -24,38 +24,38 @@
|
||||
<form method="post" action="/admin/customization/identity" enctype="multipart/form-data"
|
||||
class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Identity</h2>
|
||||
<h2 class="card__title">{{ t("Identity") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="instance-name">Name</label>
|
||||
<label class="field__label" for="instance-name">{{ t("Name") }}</label>
|
||||
<input class="input" id="instance-name" name="instance_name"
|
||||
value="{{ values.instance_name }}" maxlength="120" placeholder="LLeMbas">
|
||||
value="{{ values.instance_name }}" maxlength="120" placeholder="{{ t('LLeMbas') }}">
|
||||
<p class="field__hint">
|
||||
Shown in the sidebar, in every page title, in the launcher when this is
|
||||
installed as an app, and to the model — it is told which installation it
|
||||
is answering in. Empty means <strong>LLeMbas</strong>.
|
||||
is answering in. Empty means <strong>{{ t("LLeMbas") }}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="tagline">Tagline</label>
|
||||
<label class="field__label" for="tagline">{{ t("Tagline") }}</label>
|
||||
<input class="input" id="tagline" name="tagline"
|
||||
value="{{ values.tagline }}" maxlength="200">
|
||||
<p class="field__hint">
|
||||
One line, used in the page description. The sign-in page has a line of
|
||||
its own under <strong>Wording</strong> below.
|
||||
its own under <strong>{{ t("Wording") }}</strong> below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="logo">Logo</label>
|
||||
<label class="field__label" for="logo">{{ t("Logo") }}</label>
|
||||
{% if current.logo_path %}
|
||||
<p class="field__hint" style="margin-bottom: var(--sp-2)">
|
||||
<img src="/branding/{{ current.logo_path }}" alt="" width="48" height="48"
|
||||
style="vertical-align: middle; border-radius: var(--radius)">
|
||||
<label class="checkbox" style="display: inline-flex; margin-left: var(--sp-3)">
|
||||
<input type="checkbox" name="remove_logo" value="true">
|
||||
<span>Remove it</span>
|
||||
<span>{{ t("Remove it") }}</span>
|
||||
</label>
|
||||
</p>
|
||||
{% endif %}
|
||||
@@ -63,47 +63,40 @@
|
||||
accept="image/png,image/jpeg,image/webp,image/gif">
|
||||
<p class="field__hint">
|
||||
Replaces the leaf mark everywhere it appears. PNG, JPEG, WEBP or GIF,
|
||||
under 2 MB, square. <strong>SVG is deliberately not accepted</strong>:
|
||||
under 2 MB, square. <strong>{{ t("SVG is deliberately not accepted") }}</strong>:
|
||||
these files are served to people who are not signed in, and an SVG can
|
||||
carry a script. The launcher icons are made from this automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="favicon">Favicon</label>
|
||||
<label class="field__label" for="favicon">{{ t("Favicon") }}</label>
|
||||
{% if current.favicon_path %}
|
||||
<p class="field__hint" style="margin-bottom: var(--sp-2)">
|
||||
<img src="/branding/{{ current.favicon_path }}" alt="" width="16" height="16"
|
||||
style="vertical-align: middle">
|
||||
<label class="checkbox" style="display: inline-flex; margin-left: var(--sp-3)">
|
||||
<input type="checkbox" name="remove_favicon" value="true">
|
||||
<span>Remove it</span>
|
||||
<span>{{ t("Remove it") }}</span>
|
||||
</label>
|
||||
</p>
|
||||
{% endif %}
|
||||
<input class="input" id="favicon" name="favicon" type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif">
|
||||
<p class="field__hint">
|
||||
Optional. Without one, a logo you upload is used at 32px, and without
|
||||
that the shipped leaf.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Optional. Without one, a logo you upload is used at 32px, and without that the shipped leaf.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save identity</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save identity") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# --- Wording -------------------------------------------------------------- #}
|
||||
<form method="post" action="/admin/customization/flavour" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Wording</h2>
|
||||
<p class="field__hint">
|
||||
The lines with a bit of character in them. They live in the empty states,
|
||||
the error pages and the sign-in screen — never in the functional interface,
|
||||
where a button says what it does. Replace them with your own, or leave them.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Wording") }}</h2>
|
||||
<p class="field__hint">{{ t("The lines with a bit of character in them. They live in the empty states, the error pages and the sign-in screen — never in the functional interface, where a button says what it does. Replace them with your own, or leave them.") }}</p>
|
||||
|
||||
{% for entry in flavour %}
|
||||
<div class="field">
|
||||
@@ -117,7 +110,7 @@
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save wording</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save wording") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -145,14 +138,8 @@
|
||||
#}
|
||||
<form method="post" action="/admin/customization/themes" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Themes</h2>
|
||||
<p class="field__hint">
|
||||
A theme is a set of colours, not a stylesheet — nothing in this interface
|
||||
hard-codes one, so a third palette composes with everything. Pick which
|
||||
built-in it starts from and change only what you want; everything left
|
||||
empty is inherited. The soft variants behind focus rings and selected rows
|
||||
are worked out from the accent, so you do not have to.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Themes") }}</h2>
|
||||
<p class="field__hint">{{ t("A theme is a set of colours, not a stylesheet — nothing in this interface hard-codes one, so a third palette composes with everything. Pick which built-in it starts from and change only what you want; everything left empty is inherited. The soft variants behind focus rings and selected rows are worked out from the accent, so you do not have to.") }}</p>
|
||||
|
||||
{# The spare block is offered until the cap, so the form cannot grow without
|
||||
end and the page cannot stop offering one while there is room. #}
|
||||
@@ -166,10 +153,10 @@
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="theme-{{ index }}-id">Id</label>
|
||||
<label class="field__label" for="theme-{{ index }}-id">{{ t("Id") }}</label>
|
||||
<input class="input" id="theme-{{ index }}-id" name="theme_{{ index }}_id"
|
||||
value="{{ existing.id if existing else '' }}" maxlength="24"
|
||||
pattern="[a-z][a-z0-9-]*" placeholder="dusk">
|
||||
pattern="[a-z][a-z0-9-]*" placeholder="{{ t('dusk') }}">
|
||||
<p class="field__hint">
|
||||
{% if existing %}
|
||||
Lowercase letters, digits and hyphens. Clear it to remove this theme.
|
||||
@@ -179,13 +166,13 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="theme-{{ index }}-label">Name</label>
|
||||
<label class="field__label" for="theme-{{ index }}-label">{{ t("Name") }}</label>
|
||||
<input class="input" id="theme-{{ index }}-label" name="theme_{{ index }}_label"
|
||||
value="{{ existing.label if existing else '' }}" maxlength="60"
|
||||
placeholder="Dusk">
|
||||
placeholder="{{ t('Dusk') }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="theme-{{ index }}-base">Starts from</label>
|
||||
<label class="field__label" for="theme-{{ index }}-base">{{ t("Starts from") }}</label>
|
||||
<select class="input" id="theme-{{ index }}-base" name="theme_{{ index }}_base">
|
||||
{% for base in bases %}
|
||||
<option value="{{ base }}"
|
||||
@@ -203,7 +190,7 @@
|
||||
{% if existing and existing.tokens %}
|
||||
<span class="badge">{{ existing.tokens | length }} set</span>
|
||||
{% else %}
|
||||
<span class="faint">all inherited</span>
|
||||
<span class="faint">{{ t("all inherited") }}</span>
|
||||
{% endif %}
|
||||
</summary>
|
||||
<div class="field-row">
|
||||
@@ -215,7 +202,7 @@
|
||||
<input class="input" id="theme-{{ index }}-{{ name }}"
|
||||
name="theme_{{ index }}_{{ name }}" type="text"
|
||||
value="{{ existing.tokens.get(name, '') if existing else '' }}"
|
||||
maxlength="40" placeholder="inherited"
|
||||
maxlength="40" placeholder="{{ t('inherited') }}"
|
||||
spellcheck="false">
|
||||
<p class="field__hint"><code>--{{ name }}</code></p>
|
||||
</div>
|
||||
@@ -234,14 +221,14 @@
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save themes</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save themes") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# --- Stylesheet ----------------------------------------------------------- #}
|
||||
<form method="post" action="/admin/customization/css" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Stylesheet</h2>
|
||||
<h2 class="card__title">{{ t("Stylesheet") }}</h2>
|
||||
<p class="field__hint">
|
||||
Served as <code>/branding.css</code> after everything else, so these rules
|
||||
win. It is a file rather than a block inside the page on purpose: a
|
||||
@@ -251,21 +238,17 @@
|
||||
</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="custom-css">CSS</label>
|
||||
<label class="field__label" for="custom-css">{{ t("CSS") }}</label>
|
||||
<textarea class="input textarea" id="custom-css" name="custom_css" rows="10"
|
||||
spellcheck="false"
|
||||
placeholder=".sidebar__brand { letter-spacing: 0.02em; }"
|
||||
>{{ values.custom_css }}</textarea>
|
||||
<p class="field__hint">
|
||||
Up to 40,000 characters. Nothing here is validated: a rule that does not
|
||||
parse is dropped by the browser, quietly, as it would be in any
|
||||
stylesheet.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Up to 40,000 characters. Nothing here is validated: a rule that does not parse is dropped by the browser, quietly, as it would be in any stylesheet.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save stylesheet</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save stylesheet") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
{% block heading %}Extraction and search{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
What happens to a file between the upload and the model, and how anything is
|
||||
found again afterwards. The two are the same pipeline: what is extracted
|
||||
decides what there is to search.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("What happens to a file between the upload and the model, and how anything is found again afterwards. The two are the same pipeline: what is extracted decides what there is to search.") }}</p>
|
||||
|
||||
{% if saved %}
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ saved }}</span></div>
|
||||
@@ -19,75 +15,58 @@
|
||||
{# --- Extraction ----------------------------------------------------------- #}
|
||||
<form method="post" action="/admin/extraction" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">What a file may cost</h2>
|
||||
<p class="field__hint">
|
||||
Every number here is a trade, and a large one usually breaks a request
|
||||
rather than being slow — the text of a whole book does not fit in a
|
||||
context window, and a model handed it fails the request outright rather
|
||||
than reading the first half.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("What a file may cost") }}</h2>
|
||||
<p class="field__hint">{{ t("Every number here is a trade, and a large one usually breaks a request rather than being slow — the text of a whole book does not fit in a context window, and a model handed it fails the request outright rather than reading the first half.") }}</p>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_upload_mb">Largest upload</label>
|
||||
<label class="field__label" for="max_upload_mb">{{ t("Largest upload") }}</label>
|
||||
<input class="input" id="max_upload_mb" name="max_upload_mb"
|
||||
type="number" min="1" max="512" step="1" value="{{ values.max_upload_mb }}">
|
||||
<p class="field__hint">Megabytes, before anything is done to it.</p>
|
||||
<p class="field__hint">{{ t("Megabytes, before anything is done to it.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_extracted_chars">Text kept per file</label>
|
||||
<label class="field__label" for="max_extracted_chars">{{ t("Text kept per file") }}</label>
|
||||
<input class="input" id="max_extracted_chars" name="max_extracted_chars"
|
||||
type="number" min="1000" max="5000000" step="1000"
|
||||
value="{{ values.max_extracted_chars }}">
|
||||
<p class="field__hint">
|
||||
Characters. Roughly four to a token, so 120,000 is about 30,000 tokens
|
||||
— already most of a small context window. The rest is cut and the
|
||||
model is told so.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Characters. Roughly four to a token, so 120,000 is about 30,000 tokens — already most of a small context window. The rest is cut and the model is told so.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_pdf_pages">Pages read from a PDF</label>
|
||||
<label class="field__label" for="max_pdf_pages">{{ t("Pages read from a PDF") }}</label>
|
||||
<input class="input" id="max_pdf_pages" name="max_pdf_pages"
|
||||
type="number" min="1" max="5000" step="1" value="{{ values.max_pdf_pages }}">
|
||||
<p class="field__hint">
|
||||
Extraction is slow and happens once, at upload. Beyond this the file
|
||||
is still stored; only its text stops.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Extraction is slow and happens once, at upload. Beyond this the file is still stored; only its text stops.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_image_edge">Longest image edge</label>
|
||||
<label class="field__label" for="max_image_edge">{{ t("Longest image edge") }}</label>
|
||||
<input class="input" id="max_image_edge" name="max_image_edge"
|
||||
type="number" min="128" max="8192" step="16"
|
||||
value="{{ values.max_image_edge }}">
|
||||
<p class="field__hint">
|
||||
Pixels. Images are re-encoded before they are sent, because a phone
|
||||
photo is several megabytes of base64.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Pixels. Images are re-encoded before they are sent, because a phone photo is several megabytes of base64.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="jpeg_quality">JPEG quality</label>
|
||||
<label class="field__label" for="jpeg_quality">{{ t("JPEG quality") }}</label>
|
||||
<input class="input" id="jpeg_quality" name="jpeg_quality"
|
||||
type="number" min="30" max="100" step="1" value="{{ values.jpeg_quality }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="orphan_hours">Keep abandoned uploads for</label>
|
||||
<label class="field__label" for="orphan_hours">{{ t("Keep abandoned uploads for") }}</label>
|
||||
<input class="input" id="orphan_hours" name="orphan_hours"
|
||||
type="number" min="1" max="8760" step="1" value="{{ values.orphan_hours }}">
|
||||
<p class="field__hint">
|
||||
Hours. A file chosen in the composer and never sent. Swept at startup.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Hours. A file chosen in the composer and never sent. Swept at startup.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="extra_text_extensions">Also treat as text</label>
|
||||
<label class="field__label" for="extra_text_extensions">{{ t("Also treat as text") }}</label>
|
||||
<textarea class="textarea input--mono" id="extra_text_extensions"
|
||||
name="extra_text_extensions" rows="4" spellcheck="false"
|
||||
>{{ extensions_text }}</textarea>
|
||||
@@ -102,28 +81,24 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="reject_unreadable_pdf" value="true"
|
||||
{{ 'checked' if values.reject_unreadable_pdf }}>
|
||||
<span>Refuse a PDF whose text cannot be read</span>
|
||||
<span>{{ t("Refuse a PDF whose text cannot be read") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off, a scanned PDF is stored with an explanation saying why it
|
||||
contributes nothing — there is no OCR here. That is usually what
|
||||
somebody wants: the file is still attached and still downloadable.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Off, a scanned PDF is stored with an explanation saying why it contributes nothing — there is no OCR here. That is usually what somebody wants: the file is still attached and still downloadable.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save extraction</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save extraction") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# --- Semantic search ------------------------------------------------------ #}
|
||||
<form method="post" action="/admin/extraction/search" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Searching by meaning</h2>
|
||||
<h2 class="card__title">{{ t("Searching by meaning") }}</h2>
|
||||
<p class="field__hint">
|
||||
Keyword search finds a document that uses your words. This finds one that
|
||||
means the same thing — <em>"how do I get in"</em> reaching a note about
|
||||
means the same thing — <em>{{ t('"how do I get in"') }}</em> reaching a note about
|
||||
passwords. Both run and the two rankings are fused, so nothing that
|
||||
keyword search found is lost.
|
||||
</p>
|
||||
@@ -133,7 +108,7 @@
|
||||
{{ icon("warning", "icon--sm") }}
|
||||
<span>
|
||||
No model is marked as producing embeddings. Tick
|
||||
<strong>embeddings</strong> on one under
|
||||
<strong>{{ t("embeddings") }}</strong> on one under
|
||||
<a href="/admin/models">Models</a> — usually a small dedicated model
|
||||
such as <code>nomic-embed-text</code> or <code>bge-m3</code>, not a chat
|
||||
model.
|
||||
@@ -153,9 +128,9 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="embedding_model_id">Embedding model</label>
|
||||
<label class="field__label" for="embedding_model_id">{{ t("Embedding model") }}</label>
|
||||
<select class="input" id="embedding_model_id" name="embedding_model_id">
|
||||
<option value="">None — keyword search only</option>
|
||||
<option value="">{{ t("None — keyword search only") }}</option>
|
||||
{% for model in models %}
|
||||
<option value="{{ model.model_id }}"
|
||||
{{ 'selected' if values.embedding_model_id == model.model_id }}>
|
||||
@@ -164,7 +139,7 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="field__hint">
|
||||
<strong>None</strong> is not a degraded mode: it is the keyword search
|
||||
<strong>{{ t("None") }}</strong> is not a degraded mode: it is the keyword search
|
||||
this has always had, with nothing stored and nothing sent anywhere.
|
||||
Changing the model changes what a vector means, so anything already
|
||||
indexed is ignored until it is rebuilt.
|
||||
@@ -173,53 +148,37 @@
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="chunk_chars">Piece size</label>
|
||||
<label class="field__label" for="chunk_chars">{{ t("Piece size") }}</label>
|
||||
<input class="input" id="chunk_chars" name="chunk_chars"
|
||||
type="number" min="200" max="8000" step="50" value="{{ values.chunk_chars }}">
|
||||
<p class="field__hint">
|
||||
Characters. A record is split on paragraph boundaries into pieces of
|
||||
about this size, and each is embedded separately — a document is found
|
||||
by its best piece, not by its average.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Characters. A record is split on paragraph boundaries into pieces of about this size, and each is embedded separately — a document is found by its best piece, not by its average.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="chunk_overlap">Overlap</label>
|
||||
<label class="field__label" for="chunk_overlap">{{ t("Overlap") }}</label>
|
||||
<input class="input" id="chunk_overlap" name="chunk_overlap"
|
||||
type="number" min="0" max="4000" step="10" value="{{ values.chunk_overlap }}">
|
||||
<p class="field__hint">
|
||||
How much of each piece is repeated at the start of the next, so a
|
||||
sentence across a boundary is whole somewhere. Capped at half the
|
||||
piece size.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("How much of each piece is repeated at the start of the next, so a sentence across a boundary is whole somewhere. Capped at half the piece size.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="embed_batch">Pieces per request</label>
|
||||
<label class="field__label" for="embed_batch">{{ t("Pieces per request") }}</label>
|
||||
<input class="input" id="embed_batch" name="embed_batch"
|
||||
type="number" min="1" max="256" step="1" value="{{ values.embed_batch }}">
|
||||
<p class="field__hint">
|
||||
Lower this if the endpoint refuses large requests; raise it if a
|
||||
rebuild is slow and the far side has room.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Lower this if the endpoint refuses large requests; raise it if a rebuild is slow and the far side has room.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save search</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save search") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# --- The index ------------------------------------------------------------ #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">The index</h2>
|
||||
<p class="field__hint">
|
||||
Documents, notes, skills and reports are indexed as they are written. A
|
||||
rebuild is for everything that already existed — or for after changing the
|
||||
model or the piece size, both of which make what is stored stop meaning
|
||||
anything. It runs in the background and can be left.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("The index") }}</h2>
|
||||
<p class="field__hint">{{ t("Documents, notes, skills and reports are indexed as they are written. A rebuild is for everything that already existed — or for after changing the model or the piece size, both of which make what is stored stop meaning anything. It runs in the background and can be left.") }}</p>
|
||||
|
||||
{% include "admin/_index_progress.html" %}
|
||||
</section>
|
||||
|
||||
@@ -6,13 +6,10 @@
|
||||
{% block heading %}General{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
Instance-wide settings. These are stored in the database and take effect
|
||||
immediately — no restart, and they survive one.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Instance-wide settings. These are stored in the database and take effect immediately — no restart, and they survive one.") }}</p>
|
||||
|
||||
{% if saved %}
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>Settings saved.</span></div>
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ t("Settings saved.") }}</span></div>
|
||||
{% endif %}
|
||||
|
||||
{#
|
||||
@@ -29,30 +26,40 @@
|
||||
<form method="post" action="/admin/general">
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Default system prompt</h2>
|
||||
<p class="card__lede">
|
||||
Applied to every chat that does not have a prompt of its own. A model's
|
||||
prompt overrides this, and a chat's prompt overrides both — most specific
|
||||
wins outright rather than the three being stacked together.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Language") }}</h2>
|
||||
<p class="card__lede">{{ t("What the interface is in for anybody who has not chosen for themselves. Everyone can pick their own under Appearance in their settings, and that choice wins here.") }}</p>
|
||||
<div class="field">
|
||||
<label class="field__label visually-hidden" for="system-prompt">System prompt</label>
|
||||
<textarea class="textarea" id="system-prompt" name="system_prompt" rows="5"
|
||||
placeholder="You are a helpful assistant.">{{ values.system_prompt }}</textarea>
|
||||
<p class="field__hint">Leave empty to send no system prompt at all.</p>
|
||||
<label class="field__label" for="language">{{ t("Interface language") }}</label>
|
||||
<select class="select" id="language" name="language">
|
||||
{% for code, name in languages %}
|
||||
<option value="{{ code }}" {{ 'selected' if values.language == code
|
||||
or (not values.language and code == "en") }}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="field__hint">
|
||||
This is the interface only. Models answer in whatever language you write
|
||||
to them in, which is a line you can edit under
|
||||
<a href="/admin/prompts">Prompts</a>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Compaction</h2>
|
||||
<p class="card__lede">
|
||||
A long conversation eventually fills the model's context. When it gets
|
||||
close, the earlier turns are summarised and the summary is sent in their
|
||||
place. The messages themselves are kept and stay readable in the
|
||||
transcript — they simply stop being sent.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Default system prompt") }}</h2>
|
||||
<p class="card__lede">{{ t("Applied to every chat that does not have a prompt of its own. A model's prompt overrides this, and a chat's prompt overrides both — most specific wins outright rather than the three being stacked together.") }}</p>
|
||||
<div class="field">
|
||||
<label class="field__label" for="compact-threshold">Compact at</label>
|
||||
<label class="field__label visually-hidden" for="system-prompt">{{ t("System prompt") }}</label>
|
||||
<textarea class="textarea" id="system-prompt" name="system_prompt" rows="5"
|
||||
placeholder="{{ t('You are a helpful assistant.') }}">{{ values.system_prompt }}</textarea>
|
||||
<p class="field__hint">{{ t("Leave empty to send no system prompt at all.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">{{ t("Compaction") }}</h2>
|
||||
<p class="card__lede">{{ t("A long conversation eventually fills the model's context. When it gets close, the earlier turns are summarised and the summary is sent in their place. The messages themselves are kept and stay readable in the transcript — they simply stop being sent.") }}</p>
|
||||
<div class="field">
|
||||
<label class="field__label" for="compact-threshold">{{ t("Compact at") }}</label>
|
||||
<input class="input" id="compact-threshold" name="compact_threshold" type="number"
|
||||
min="0" max="99" value="{{ values.compact_threshold }}">
|
||||
<p class="field__hint">
|
||||
@@ -67,20 +74,16 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Tool calls in an ordinary chat</h2>
|
||||
<p class="card__lede">
|
||||
A model ends its own turn the moment it stops asking for tools — that is
|
||||
it saying it has what it needs, and nothing here overrides it. This is a
|
||||
ceiling for the case where it never says so.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Tool calls in an ordinary chat") }}</h2>
|
||||
<p class="card__lede">{{ t("A model ends its own turn the moment it stops asking for tools — that is it saying it has what it needs, and nothing here overrides it. This is a ceiling for the case where it never says so.") }}</p>
|
||||
<div class="field">
|
||||
<label class="field__label" for="max-chat-rounds">Most rounds of tool calls</label>
|
||||
<label class="field__label" for="max-chat-rounds">{{ t("Most rounds of tool calls") }}</label>
|
||||
<input class="input" id="max-chat-rounds" name="max_chat_rounds" type="number"
|
||||
min="0" max="100" value="{{ values.max_chat_rounds }}">
|
||||
<p class="field__hint">
|
||||
Several tools can be called in one round, so this is not a count of
|
||||
tools. Leave room for at least two: <code>knowledge_get</code> and
|
||||
<code>notes_get</code> read a document by an id a <em>search</em>
|
||||
<code>notes_get</code> read a document by an id a <em>{{ t("search") }}</em>
|
||||
returned, so a ceiling of one leaves the library searchable and not
|
||||
readable. <code>0</code> means no ceiling, which is how an agent chat
|
||||
already works — those are bounded under
|
||||
@@ -93,9 +96,9 @@
|
||||
<h2 class="card__title">
|
||||
Registration
|
||||
{% if values.allow_signup %}
|
||||
<span class="badge badge--success">open</span>
|
||||
<span class="badge badge--success">{{ t("open") }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge--danger">closed</span>
|
||||
<span class="badge badge--danger">{{ t("closed") }}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
@@ -103,20 +106,16 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="allow_signup" value="true"
|
||||
{{ 'checked' if values.allow_signup }}>
|
||||
<span>Anyone who can reach this instance may create an account</span>
|
||||
<span>{{ t("Anyone who can reach this instance may create an account") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Turn this off once your users exist. Sign-in is unaffected — existing
|
||||
accounts keep working, and the "Create one" link disappears from the
|
||||
sign-in page.
|
||||
</p>
|
||||
<p class="field__hint">{{ t('Turn this off once your users exist. Sign-in is unaffected — existing accounts keep working, and the "Create one" link disappears from the sign-in page.') }}</p>
|
||||
</div>
|
||||
|
||||
{% if values.allow_signup and user_count > 0 %}
|
||||
<div class="alert alert--warning">
|
||||
{{ icon("warning", "alert__icon") }}
|
||||
<div>
|
||||
<strong>Registration is open.</strong>
|
||||
<strong>{{ t("Registration is open.") }}</strong>
|
||||
<div class="text-sm" style="margin-top: var(--sp-1)">
|
||||
This instance has {{ user_count }} account{{ '' if user_count == 1 else 's' }}.
|
||||
Anyone who can reach it can add another, and every account can use your
|
||||
@@ -132,6 +131,6 @@
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="form-actions"><button class="btn btn--primary" type="submit">Save settings</button></div>
|
||||
<div class="form-actions"><button class="btn btn--primary" type="submit">{{ t("Save settings") }}</button></div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
<a href="/admin/groups">{{ icon("chevron-left", "icon--sm") }} All groups</a>
|
||||
· A group only ever <em>adds</em>. Anything already in the baseline is shown
|
||||
· A group only ever <em>{{ t("adds") }}</em>. Anything already in the baseline is shown
|
||||
below as such, so a tick here that changes nothing looks like one.
|
||||
</p>
|
||||
|
||||
@@ -18,20 +18,20 @@
|
||||
|
||||
<form method="post" action="/admin/groups/{{ group.id }}" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Name</h2>
|
||||
<h2 class="card__title">{{ t("Name") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input" id="name" name="name" value="{{ group.name }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="description">What it is for</label>
|
||||
<label class="field__label" for="description">{{ t("What it is for") }}</label>
|
||||
<input class="input" id="description" name="description"
|
||||
value="{{ group.description }}" maxlength="1000">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Permissions this group adds</h2>
|
||||
<h2 class="card__title">{{ t("Permissions this group adds") }}</h2>
|
||||
{% for section_name, defs in permission_groups.items() %}
|
||||
<div class="field">
|
||||
<span class="field__label">{{ section_name }}</span>
|
||||
@@ -42,7 +42,7 @@
|
||||
<span>
|
||||
<strong>{{ definition.label }}</strong>
|
||||
{% if baseline[definition.key] %}
|
||||
<span class="badge">already in the baseline</span>
|
||||
<span class="badge">{{ t("already in the baseline") }}</span>
|
||||
{% endif %}
|
||||
<span class="perm-row__desc">{{ definition.description }}</span>
|
||||
</span>
|
||||
@@ -59,11 +59,11 @@
|
||||
only place somebody will read it.
|
||||
#}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Quotas</h2>
|
||||
<h2 class="card__title">{{ t("Quotas") }}</h2>
|
||||
<p class="card__lede">
|
||||
Resolved across a person's groups by <strong>maximum</strong> — the union
|
||||
Resolved across a person's groups by <strong>{{ t("maximum") }}</strong> — the union
|
||||
rule applied to numbers, so a second group can only grant more.
|
||||
<strong>Leave a box empty</strong> for “no opinion”, and use
|
||||
<strong>{{ t("Leave a box empty") }}</strong> for “no opinion”, and use
|
||||
<strong>0</strong> for “no limit”, which beats any number another group
|
||||
sets. Administrators are unlimited whatever is here.
|
||||
</p>
|
||||
@@ -73,7 +73,7 @@
|
||||
<label class="field__label" for="limit-{{ key }}">{{ label }}</label>
|
||||
<input class="input" id="limit-{{ key }}" name="limit_{{ key }}"
|
||||
type="number" min="0" step="1"
|
||||
value="{{ limits.get(key, '') }}" placeholder="no opinion">
|
||||
value="{{ limits.get(key, '') }}" placeholder="{{ t('no opinion') }}">
|
||||
<p class="field__hint">{{ description }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -85,11 +85,8 @@
|
||||
and a full-form POST from either side overwrote what the other had shown.
|
||||
#}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Members</h2>
|
||||
<p class="card__lede">
|
||||
The one place membership is edited. A user's own page links here rather
|
||||
than offering a second control for the same value.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Members") }}</h2>
|
||||
<p class="card__lede">{{ t("The one place membership is edited. A user's own page links here rather than offering a second control for the same value.") }}</p>
|
||||
<div class="checkbox-row">
|
||||
{% for person in users %}
|
||||
<label class="checkbox">
|
||||
@@ -102,12 +99,8 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Models this group unlocks</h2>
|
||||
<p class="card__lede">
|
||||
A model marked public is available to everyone; one that is not is
|
||||
available to the groups named here. Model access is separate from
|
||||
permissions — one says what somebody may do, the other what with.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Models this group unlocks") }}</h2>
|
||||
<p class="card__lede">{{ t("A model marked public is available to everyone; one that is not is available to the groups named here. Model access is separate from permissions — one says what somebody may do, the other what with.") }}</p>
|
||||
<div class="checkbox-row">
|
||||
{% for model in models %}
|
||||
<label class="checkbox">
|
||||
@@ -120,22 +113,18 @@
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save group</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save group") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Remove</h2>
|
||||
<h2 class="card__title">{{ t("Remove") }}</h2>
|
||||
<form method="post" action="/admin/groups/{{ group.id }}/delete"
|
||||
data-confirm="Delete {{ group.name }}? Its members keep their accounts.">
|
||||
<button class="btn btn--danger btn--sm" type="submit">
|
||||
{{ icon("trash", "icon--sm") }} Delete this group
|
||||
</button>
|
||||
</form>
|
||||
<p class="field__hint">
|
||||
Members keep their accounts and lose whatever this group granted them. Every
|
||||
share naming this group goes too — nothing cascades to those, so they are
|
||||
deleted explicitly.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Members keep their accounts and lose whatever this group granted them. Every share naming this group goes too — nothing cascades to those, so they are deleted explicitly.") }}</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
Permissions are a <strong>union</strong>: everyone starts with the baseline
|
||||
Permissions are a <strong>{{ t("union") }}</strong>: everyone starts with the baseline
|
||||
below, and each group they belong to can add more. A group never takes
|
||||
something away, so being in a second group can only widen what someone can do —
|
||||
which is what keeps “why can this person not do X?” answerable without
|
||||
@@ -19,11 +19,8 @@
|
||||
{% endif %}
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Baseline permissions</h2>
|
||||
<p class="card__lede">
|
||||
What every signed-in user can do before any group is considered. Turn
|
||||
something off here and grant it through a group to make it opt-in.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Baseline permissions") }}</h2>
|
||||
<p class="card__lede">{{ t("What every signed-in user can do before any group is considered. Turn something off here and grant it through a group to make it opt-in.") }}</p>
|
||||
|
||||
<form method="post" action="/admin/permissions/defaults">
|
||||
{% for section_name, defs in permission_groups.items() %}
|
||||
@@ -41,7 +38,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="btn-row"><button class="btn btn--primary" type="submit">Save baseline</button></div>
|
||||
<div class="btn-row"><button class="btn btn--primary" type="submit">{{ t("Save baseline") }}</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -69,22 +66,20 @@
|
||||
{% if granted[group.id] %}
|
||||
<span class="badge badge--leaf">+{{ granted[group.id] }} permission{{ '' if granted[group.id] == 1 else 's' }}</span>
|
||||
{% endif %}
|
||||
{% if group.limits_json %}<span class="badge">quotas</span>{% endif %}
|
||||
{% if group.limits_json %}<span class="badge">{{ t("quotas") }}</span>{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="empty" style="padding: var(--sp-8) 0">
|
||||
<p class="empty__text">
|
||||
No groups yet. Everybody gets the baseline above and nothing more.
|
||||
</p>
|
||||
<p class="empty__text">{{ t("No groups yet. Everybody gets the baseline above and nothing more.") }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<section class="card" style="margin-top: var(--sp-6)">
|
||||
<form method="post" action="/admin/groups" class="btn-row">
|
||||
<input class="input" name="name" placeholder="New group name" required
|
||||
aria-label="New group name">
|
||||
<input class="input" name="name" placeholder="{{ t('New group name') }}" required
|
||||
aria-label="{{ t('New group name') }}">
|
||||
<button class="btn btn--primary" type="submit">
|
||||
{{ icon("plus", "icon--sm") }} Create group
|
||||
</button>
|
||||
|
||||
@@ -6,13 +6,7 @@
|
||||
{% block heading %}Image generation{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
Lets a model draw a picture and show it in the conversation, on a ComfyUI you
|
||||
are running. It is offered as a tool the model chooses to call, so nothing
|
||||
changes for a conversation that never asks for one — and it is only offered
|
||||
once there is a ComfyUI, a workflow and at least one checkpoint, because a
|
||||
tool that fails on its first call is worse than a tool nobody was given.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Lets a model draw a picture and show it in the conversation, on a ComfyUI you are running. It is offered as a tool the model chooses to call, so nothing changes for a conversation that never asks for one — and it is only offered once there is a ComfyUI, a workflow and at least one checkpoint, because a tool that fails on its first call is worse than a tool nobody was given.") }}</p>
|
||||
|
||||
{% if saved %}
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ saved }}</span></div>
|
||||
@@ -22,13 +16,13 @@
|
||||
<section class="card">
|
||||
<h2 class="card__title">
|
||||
Image generation
|
||||
{% if values.enabled %}<span class="badge badge--success">on</span>
|
||||
{% else %}<span class="badge">off</span>{% endif %}
|
||||
{% if values.enabled %}<span class="badge badge--success">{{ t("on") }}</span>
|
||||
{% else %}<span class="badge">{{ t("off") }}</span>{% endif %}
|
||||
</h2>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if values.enabled }}>
|
||||
<span>Offer image generation to models that support tools</span>
|
||||
<span>{{ t("Offer image generation to models that support tools") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Who may use it is a permission — <code>tools.image</code> under
|
||||
@@ -38,22 +32,18 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">ComfyUI</h2>
|
||||
<h2 class="card__title">{{ t("ComfyUI") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="base_url">Base URL</label>
|
||||
<label class="field__label" for="base_url">{{ t("Base URL") }}</label>
|
||||
<input class="input input--mono" id="base_url" name="base_url" type="url"
|
||||
placeholder="http://127.0.0.1:8188" value="{{ values.base_url }}">
|
||||
<p class="field__hint">
|
||||
Where ComfyUI is listening. An address on this machine or this network is
|
||||
fine here and is not checked against the request-forgery rules — you
|
||||
typed it, unlike an address a model asks for.
|
||||
</p>
|
||||
placeholder="{{ t('http://127.0.0.1:8188') }}" value="{{ values.base_url }}">
|
||||
<p class="field__hint">{{ t("Where ComfyUI is listening. An address on this machine or this network is fine here and is not checked against the request-forgery rules — you typed it, unlike an address a model asks for.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="api_key">API key</label>
|
||||
<label class="field__label" for="api_key">{{ t("API key") }}</label>
|
||||
<input class="input input--mono" id="api_key" name="api_key" type="password"
|
||||
autocomplete="off" placeholder="No key set"
|
||||
autocomplete="off" placeholder="{{ t('No key set') }}"
|
||||
value="{{ unchanged if values.api_key_encrypted else '' }}">
|
||||
<p class="field__hint">
|
||||
{% if values.api_key_encrypted %}
|
||||
@@ -66,29 +56,21 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="timeout">Timeout (seconds)</label>
|
||||
<label class="field__label" for="timeout">{{ t("Timeout (seconds)") }}</label>
|
||||
<input class="input" id="timeout" name="timeout" type="number"
|
||||
min="10" max="3600" step="10" value="{{ values.timeout | int }}">
|
||||
<p class="field__hint">
|
||||
How long to wait for one image, queue included. Far longer than any other
|
||||
timeout here, because the thing being waited for genuinely takes that long.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("How long to wait for one image, queue included. Far longer than any other timeout here, because the thing being waited for genuinely takes that long.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Checkpoints</h2>
|
||||
<h2 class="card__title">{{ t("Checkpoints") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="checkpoints">Available checkpoints</label>
|
||||
<label class="field__label" for="checkpoints">{{ t("Available checkpoints") }}</label>
|
||||
<textarea class="textarea input--mono" id="checkpoints" name="checkpoints" rows="4"
|
||||
spellcheck="false"
|
||||
placeholder="sd_xl_base_1.0.safetensors">{{ checkpoints_text }}</textarea>
|
||||
<p class="field__hint">
|
||||
One filename per line, exactly as ComfyUI spells it. This is the list the
|
||||
model chooses from, so leaving out a checkpoint is how you stop it being
|
||||
used. Press Test below to read them off ComfyUI — that fills this in the
|
||||
first time and never overwrites it afterwards.
|
||||
</p>
|
||||
placeholder="{{ t('sd_xl_base_1.0.safetensors') }}">{{ checkpoints_text }}</textarea>
|
||||
<p class="field__hint">{{ t("One filename per line, exactly as ComfyUI spells it. This is the list the model chooses from, so leaving out a checkpoint is how you stop it being used. Press Test below to read them off ComfyUI — that fills this in the first time and never overwrites it afterwards.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
@@ -101,21 +83,16 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">What a generation uses by default</h2>
|
||||
<p class="card__lede">
|
||||
What every picture is drawn with unless the model names otherwise. Leave a
|
||||
box empty to use the built-in value shown beside it — the built-ins are
|
||||
SD1.5-era, and 512×512 on an SDXL checkpoint is what produces the
|
||||
duplicated limbs.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("What a generation uses by default") }}</h2>
|
||||
<p class="card__lede">{{ t("What every picture is drawn with unless the model names otherwise. Leave a box empty to use the built-in value shown beside it — the built-ins are SD1.5-era, and 512×512 on an SDXL checkpoint is what produces the duplicated limbs.") }}</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_checkpoint">Checkpoint</label>
|
||||
<label class="field__label" for="default_checkpoint">{{ t("Checkpoint") }}</label>
|
||||
{# A select rather than a box, from the list above. Typing a name that is
|
||||
not there produced a picture drawn with whatever happened to be first,
|
||||
with nothing anywhere saying so. #}
|
||||
<select class="select" id="default_checkpoint" name="default_checkpoint">
|
||||
<option value="">The first in the list above</option>
|
||||
<option value="">{{ t("The first in the list above") }}</option>
|
||||
{% for name in values.checkpoints or [] %}
|
||||
<option value="{{ name }}" {{ 'selected' if values.default_checkpoint == name }}>
|
||||
{{ name }}
|
||||
@@ -126,13 +103,13 @@
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_width">Width</label>
|
||||
<label class="field__label" for="default_width">{{ t("Width") }}</label>
|
||||
<input class="input" type="number" id="default_width" name="default_width"
|
||||
min="64" max="2048" step="64" placeholder="512"
|
||||
value="{{ values.default_width }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_height">Height</label>
|
||||
<label class="field__label" for="default_height">{{ t("Height") }}</label>
|
||||
<input class="input" type="number" id="default_height" name="default_height"
|
||||
min="64" max="2048" step="64" placeholder="512"
|
||||
value="{{ values.default_height }}">
|
||||
@@ -153,19 +130,19 @@
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_steps">Steps</label>
|
||||
<label class="field__label" for="default_steps">{{ t("Steps") }}</label>
|
||||
<input class="input" type="number" id="default_steps" name="default_steps"
|
||||
min="1" max="150" step="1" placeholder="20"
|
||||
value="{{ values.default_steps }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_cfg">Guidance (cfg)</label>
|
||||
<label class="field__label" for="default_cfg">{{ t("Guidance (cfg)") }}</label>
|
||||
<input class="input" type="number" id="default_cfg" name="default_cfg"
|
||||
min="0" max="30" step="0.5" placeholder="8"
|
||||
value="{{ values.default_cfg }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_denoise">Denoise</label>
|
||||
<label class="field__label" for="default_denoise">{{ t("Denoise") }}</label>
|
||||
<input class="input" type="number" id="default_denoise" name="default_denoise"
|
||||
min="0" max="1" step="0.05" placeholder="1"
|
||||
value="{{ values.default_denoise }}">
|
||||
@@ -174,13 +151,13 @@
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_sampler">Sampler</label>
|
||||
<label class="field__label" for="default_sampler">{{ t("Sampler") }}</label>
|
||||
{# From what the Test button read off ComfyUI. There are forty-odd and
|
||||
spelling one wrong is a refused workflow, so it is picked rather than
|
||||
typed — and the current value is kept as an option even when the list
|
||||
has not been read, or saving this page would quietly clear it. #}
|
||||
<select class="select" id="default_sampler" name="default_sampler">
|
||||
<option value="">euler (built-in)</option>
|
||||
<option value="">{{ t("euler (built-in)") }}</option>
|
||||
{% for name in values.samplers or [] %}
|
||||
<option value="{{ name }}" {{ 'selected' if values.default_sampler == name }}>
|
||||
{{ name }}
|
||||
@@ -194,9 +171,9 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_scheduler">Scheduler</label>
|
||||
<label class="field__label" for="default_scheduler">{{ t("Scheduler") }}</label>
|
||||
<select class="select" id="default_scheduler" name="default_scheduler">
|
||||
<option value="">normal (built-in)</option>
|
||||
<option value="">{{ t("normal (built-in)") }}</option>
|
||||
{% for name in values.schedulers or [] %}
|
||||
<option value="{{ name }}" {{ 'selected' if values.default_scheduler == name }}>
|
||||
{{ name }}
|
||||
@@ -211,7 +188,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_batch">Images per run</label>
|
||||
<label class="field__label" for="default_batch">{{ t("Images per run") }}</label>
|
||||
<input class="input" type="number" id="default_batch" name="default_batch"
|
||||
min="1" max="8" step="1" placeholder="1"
|
||||
value="{{ values.default_batch }}">
|
||||
@@ -225,38 +202,28 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_negative">Negative prompt</label>
|
||||
<label class="field__label" for="default_negative">{{ t("Negative prompt") }}</label>
|
||||
<input class="input" type="text" id="default_negative" name="default_negative"
|
||||
placeholder="text, watermark" value="{{ values.default_negative }}">
|
||||
<p class="field__hint">
|
||||
Used when the model does not write one of its own. It writes one often,
|
||||
so this is a floor rather than something always applied — “always add
|
||||
these words” belongs in the instructions below, where the model is told
|
||||
to include them.
|
||||
</p>
|
||||
placeholder="{{ t('text, watermark') }}" value="{{ values.default_negative }}">
|
||||
<p class="field__hint">{{ t("Used when the model does not write one of its own. It writes one often, so this is a floor rather than something always applied — “always add these words” belongs in the instructions below, where the model is told to include them.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Checking the result</h2>
|
||||
<h2 class="card__title">{{ t("Checking the result") }}</h2>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="review_enabled" value="true"
|
||||
{{ 'checked' if values.review_enabled }}>
|
||||
<span>Look at each image before showing it, and try again if it is wrong</span>
|
||||
<span>{{ t("Look at each image before showing it, and try again if it is wrong") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
A vision model is shown the picture and the request it came from, and says
|
||||
keep or retry. Only clearly wrong images are retried — a missing subject, a
|
||||
mangled picture — because taste is not a fault and the next attempt is not
|
||||
promised to be better. The reader sees only the image that was kept.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("A vision model is shown the picture and the request it came from, and says keep or retry. Only clearly wrong images are retried — a missing subject, a mangled picture — because taste is not a fault and the next attempt is not promised to be better. The reader sees only the image that was kept.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="review_model_id">Model that reviews</label>
|
||||
<label class="field__label" for="review_model_id">{{ t("Model that reviews") }}</label>
|
||||
<select class="select" id="review_model_id" name="review_model_id">
|
||||
<option value="">The chat's own model, when it has vision</option>
|
||||
<option value="">{{ t("The chat's own model, when it has vision") }}</option>
|
||||
{% for model in vision_models %}
|
||||
{% if model.capabilities_json.get("vision") %}
|
||||
{# The model's own id, not the row's primary key: "Test & refresh"
|
||||
@@ -270,38 +237,26 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="field__hint">
|
||||
Only models marked as having vision are listed. If there is nothing to ask,
|
||||
the first image is kept and nothing fails.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Only models marked as having vision are listed. If there is nothing to ask, the first image is kept and nothing fails.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_tries">Attempts per image</label>
|
||||
<label class="field__label" for="max_tries">{{ t("Attempts per image") }}</label>
|
||||
<input class="input" id="max_tries" name="max_tries" type="number"
|
||||
min="1" max="10" value="{{ values.max_tries }}">
|
||||
<p class="field__hint">
|
||||
Including the first. The last attempt is kept whatever the review says, so
|
||||
a request always produces a picture.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Including the first. The last attempt is kept whatever the review says, so a request always produces a picture.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Memory</h2>
|
||||
<h2 class="card__title">{{ t("Memory") }}</h2>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="preserve_vram" value="true"
|
||||
{{ 'checked' if values.preserve_vram }}>
|
||||
<span>Preserve VRAM: unload the language model while ComfyUI works</span>
|
||||
<span>{{ t("Preserve VRAM: unload the language model while ComfyUI works") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
For a machine that cannot hold both at once. Before generating, the chat's
|
||||
own connection is asked to unload — set an unload URL on it under
|
||||
Connections, or nothing happens. Afterwards ComfyUI is asked to free its
|
||||
own models, and the language model loads again by itself on the next
|
||||
request.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("For a machine that cannot hold both at once. Before generating, the chat's own connection is asked to unload — set an unload URL on it under Connections, or nothing happens. Afterwards ComfyUI is asked to free its own models, and the language model loads again by itself on the next request.") }}</p>
|
||||
</div>
|
||||
|
||||
{% if values.preserve_vram and values.review_enabled %}
|
||||
@@ -317,28 +272,28 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Extra instructions</h2>
|
||||
<h2 class="card__title">{{ t("Extra instructions") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label visually-hidden" for="instructions">Extra instructions</label>
|
||||
<label class="field__label visually-hidden" for="instructions">{{ t("Extra instructions") }}</label>
|
||||
<textarea class="textarea" id="instructions" name="instructions" rows="4"
|
||||
placeholder="Prefer the SDXL template for anything photographic.">{{ values.instructions }}</textarea>
|
||||
placeholder="{{ t('Prefer the SDXL template for anything photographic.') }}">{{ values.instructions }}</textarea>
|
||||
<p class="field__hint">
|
||||
Added to what every model is told about image generation, on this instance.
|
||||
Where “always put <em>text, watermark</em> in the negative prompt” lives.
|
||||
Where “always put <em>{{ t("text, watermark") }}</em> in the negative prompt” lives.
|
||||
Leave it empty and nothing is added at all.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save changes</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save changes") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Workflows</h2>
|
||||
<h2 class="card__title">{{ t("Workflows") }}</h2>
|
||||
<p class="field__hint">
|
||||
A workflow is a ComfyUI graph exported with <strong>Export (API)</strong>, with
|
||||
A workflow is a ComfyUI graph exported with <strong>{{ t("Export (API)") }}</strong>, with
|
||||
<code>{{ '{{prompt}}' }}</code> and the other placeholders where the values go.
|
||||
The model picks between them by their descriptions, so write the description
|
||||
for a reader who cannot see the graph.
|
||||
@@ -353,10 +308,7 @@
|
||||
{% if not workflows %}
|
||||
<div class="empty" style="padding: var(--sp-8) 0">
|
||||
{{ icon("image", "empty__mark") }}
|
||||
<p class="empty__text">
|
||||
No workflows yet. One is needed before anything can be drawn; the default
|
||||
one is filled in for you when you add the first.
|
||||
</p>
|
||||
<p class="empty__text">{{ t("No workflows yet. One is needed before anything can be drawn; the default one is filled in for you when you add the first.") }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="model-rows">
|
||||
@@ -365,9 +317,9 @@
|
||||
<div class="model-row__main">
|
||||
<span class="model-row__name">
|
||||
{{ item.name }}
|
||||
{% if not item.enabled %}<span class="badge badge--danger">disabled</span>{% endif %}
|
||||
{% if not item.enabled %}<span class="badge badge--danger">{{ t("disabled") }}</span>{% endif %}
|
||||
{% if values.default_workflow_id == item.id %}
|
||||
<span class="badge">default</span>
|
||||
<span class="badge">{{ t("default") }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<code class="model-row__id">{{ item.slug }}</code>
|
||||
@@ -395,20 +347,18 @@
|
||||
<input type="hidden" name="max_tries" value="{{ values.max_tries }}">
|
||||
<input type="hidden" name="preserve_vram" value="{{ 'true' if values.preserve_vram }}">
|
||||
<input type="hidden" name="instructions" value="{{ values.instructions }}">
|
||||
<label class="field__label" for="default_workflow_id">Default workflow</label>
|
||||
<label class="field__label" for="default_workflow_id">{{ t("Default workflow") }}</label>
|
||||
<div class="btn-row">
|
||||
<select class="select" id="default_workflow_id" name="default_workflow_id">
|
||||
<option value="">The first one</option>
|
||||
<option value="">{{ t("The first one") }}</option>
|
||||
{% for item in workflows %}
|
||||
<option value="{{ item.id }}"
|
||||
{{ 'selected' if values.default_workflow_id == item.id }}>{{ item.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn" type="submit">Set default</button>
|
||||
<button class="btn" type="submit">{{ t("Set default") }}</button>
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
Used when the model names no template and the chat has no preference.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Used when the model names no template and the chat has no preference.") }}</p>
|
||||
</form>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -8,18 +8,14 @@
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
Remote servers speaking the Model Context Protocol over HTTP. Their tools are
|
||||
offered beside the built-in ones to models marked <strong>MCP servers</strong>.
|
||||
offered beside the built-in ones to models marked <strong>{{ t("MCP servers") }}</strong>.
|
||||
The list of tools is discovered and cached — press <strong>Test &
|
||||
refresh</strong> after adding one, and again whenever the server changes.
|
||||
</p>
|
||||
|
||||
<div class="alert">
|
||||
{{ icon("shield", "icon--sm") }}
|
||||
<span>
|
||||
A server's tool names and descriptions are sent to the model as
|
||||
instructions, and what it returns is read back as fact. Add servers you
|
||||
trust, the way you would a dependency.
|
||||
</span>
|
||||
<span>{{ t("A server's tool names and descriptions are sent to the model as instructions, and what it returns is read back as fact. Add servers you trust, the way you would a dependency.") }}</span>
|
||||
</div>
|
||||
|
||||
{% if saved %}
|
||||
@@ -35,10 +31,7 @@
|
||||
{% if not servers %}
|
||||
<div class="empty" style="padding: var(--sp-10) 0">
|
||||
{{ icon("server", "empty__mark") }}
|
||||
<p class="empty__text">
|
||||
No servers yet. You will need the URL of an MCP endpoint that speaks
|
||||
streamable HTTP — local ones launched as a subprocess are not supported.
|
||||
</p>
|
||||
<p class="empty__text">{{ t("No servers yet. You will need the URL of an MCP endpoint that speaks streamable HTTP — local ones launched as a subprocess are not supported.") }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
|
||||
@@ -27,18 +27,18 @@
|
||||
class="form-grid">
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">The server</h2>
|
||||
<h2 class="card__title">{{ t("The server") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input" id="name" name="name" value="{{ server.name }}" required
|
||||
maxlength="120" placeholder="GitHub">
|
||||
maxlength="120" placeholder="{{ t('GitHub') }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="slug">Identifier</label>
|
||||
<label class="field__label" for="slug">{{ t("Identifier") }}</label>
|
||||
<input class="input input--mono" id="slug" name="slug" value="{{ server.slug }}" required
|
||||
maxlength="24" pattern="[a-z0-9][a-z0-9_\-]*" placeholder="github">
|
||||
maxlength="24" pattern="[a-z0-9][a-z0-9_\-]*" placeholder="{{ t('github') }}">
|
||||
<p class="field__hint">
|
||||
Prefixed onto every tool name this server offers, so that two servers
|
||||
both exposing <code>search</code> do not collide.
|
||||
@@ -46,40 +46,37 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="url">Endpoint URL</label>
|
||||
<label class="field__label" for="url">{{ t("Endpoint URL") }}</label>
|
||||
<input class="input input--mono" id="url" name="url" value="{{ server.url }}" required
|
||||
placeholder="https://mcp.example.com/mcp">
|
||||
<p class="field__hint">
|
||||
The streamable-HTTP endpoint itself, the one that accepts a POST. A
|
||||
server that answers with a redirect to somewhere else will be refused.
|
||||
</p>
|
||||
placeholder="{{ t('https://mcp.example.com/mcp') }}">
|
||||
<p class="field__hint">{{ t("The streamable-HTTP endpoint itself, the one that accepts a POST. A server that answers with a redirect to somewhere else will be refused.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="headers">Extra headers</label>
|
||||
<label class="field__label" for="headers">{{ t("Extra headers") }}</label>
|
||||
<textarea class="textarea input--mono" id="headers" name="headers" rows="3"
|
||||
spellcheck="false">{{ headers_text }}</textarea>
|
||||
<p class="field__hint">One <code>Name: value</code> per line.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="timeout">Timeout (seconds)</label>
|
||||
<label class="field__label" for="timeout">{{ t("Timeout (seconds)") }}</label>
|
||||
<input class="input" id="timeout" name="timeout" value="{{ server.timeout }}"
|
||||
inputmode="numeric">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_chars">Most characters to keep per call</label>
|
||||
<label class="field__label" for="max_chars">{{ t("Most characters to keep per call") }}</label>
|
||||
<input class="input" id="max_chars" name="max_chars" value="{{ server.max_chars }}"
|
||||
inputmode="numeric">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Credential</h2>
|
||||
<h2 class="card__title">{{ t("Credential") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="secret_placement">How it is sent</label>
|
||||
<label class="field__label" for="secret_placement">{{ t("How it is sent") }}</label>
|
||||
<select class="select" id="secret_placement" name="secret_placement">
|
||||
{% for value, label in secret_placements %}
|
||||
<option value="{{ value }}" {{ 'selected' if value == server.secret_placement }}>
|
||||
@@ -90,15 +87,15 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="secret_name">Header or parameter name</label>
|
||||
<label class="field__label" for="secret_name">{{ t("Header or parameter name") }}</label>
|
||||
<input class="input input--mono" id="secret_name" name="secret_name"
|
||||
value="{{ server.secret_name }}" maxlength="120">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="secret">Secret</label>
|
||||
<label class="field__label" for="secret">{{ t("Secret") }}</label>
|
||||
<input class="input input--mono" id="secret" name="secret" type="password"
|
||||
autocomplete="off" placeholder="No secret set"
|
||||
autocomplete="off" placeholder="{{ t('No secret set') }}"
|
||||
value="{{ unchanged if server.secret_encrypted else '' }}">
|
||||
<p class="field__hint">
|
||||
{% if server.secret_encrypted %}
|
||||
@@ -113,11 +110,8 @@
|
||||
|
||||
{% if tools %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Tools it offers</h2>
|
||||
<p class="field__hint">
|
||||
Discovered at the last refresh. Untick one to withhold it — a tool this
|
||||
server adds later is offered by default.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Tools it offers") }}</h2>
|
||||
<p class="field__hint">{{ t("Discovered at the last refresh. Untick one to withhold it — a tool this server adds later is offered by default.") }}</p>
|
||||
|
||||
<input type="hidden" name="tool_choices" value="1">
|
||||
<div class="checkbox-row checkbox-row--stacked">
|
||||
@@ -141,7 +135,7 @@
|
||||
</section>
|
||||
{% elif not is_new %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Tools it offers</h2>
|
||||
<h2 class="card__title">{{ t("Tools it offers") }}</h2>
|
||||
<p class="field__hint">
|
||||
Nothing discovered yet. Save, then press <strong>Test & refresh</strong>
|
||||
on the <a href="/admin/mcp">list</a>.
|
||||
@@ -150,11 +144,11 @@
|
||||
{% endif %}
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Guidance</h2>
|
||||
<h2 class="card__title">{{ t("Guidance") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<textarea class="textarea" name="guidance" rows="4"
|
||||
placeholder="- Use the GitHub tools for anything about our repositories."
|
||||
placeholder="{{ t('- Use the GitHub tools for anything about our repositories.') }}"
|
||||
>{{ server.guidance }}</textarea>
|
||||
<p class="field__hint">
|
||||
Added to the system message whenever any tool from this server is
|
||||
@@ -170,18 +164,18 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Availability</h2>
|
||||
<h2 class="card__title">{{ t("Availability") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<div class="checkbox-row">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if server.enabled }}>
|
||||
<span>Enabled — offered in chats</span>
|
||||
<span>{{ t("Enabled — offered in chats") }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="allow_private" value="true"
|
||||
{{ 'checked' if server.allow_private }}>
|
||||
<span>May reach private and loopback addresses</span>
|
||||
<span>{{ t("May reach private and loopback addresses") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
@@ -193,12 +187,12 @@
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="public" value="true" {{ 'checked' if server.public }}>
|
||||
<span>Available to everyone</span>
|
||||
<span>{{ t("Available to everyone") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="field__label">Groups with access</span>
|
||||
<span class="field__label">{{ t("Groups with access") }}</span>
|
||||
{% if groups %}
|
||||
<div class="checkbox-row">
|
||||
{% for group in groups %}
|
||||
@@ -209,7 +203,7 @@
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="field__hint">Ignored while the server is available to everyone.</p>
|
||||
<p class="field__hint">{{ t("Ignored while the server is available to everyone.") }}</p>
|
||||
{% else %}
|
||||
<p class="field__hint">
|
||||
No groups yet — <a href="/admin/groups">create one</a> to restrict access.
|
||||
@@ -218,7 +212,7 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="position">Position</label>
|
||||
<label class="field__label" for="position">{{ t("Position") }}</label>
|
||||
<input class="input" id="position" name="position" value="{{ server.position }}"
|
||||
inputmode="numeric">
|
||||
</div>
|
||||
@@ -232,9 +226,7 @@
|
||||
{% if not is_new %}
|
||||
<button class="btn btn--danger" type="submit" formnovalidate
|
||||
formaction="/admin/mcp/{{ server.id }}/delete"
|
||||
data-confirm-button="Delete the server “{{ server.name }}”? Chats that used its tools keep their transcripts.">
|
||||
Delete
|
||||
</button>
|
||||
data-confirm-button="Delete the server “{{ server.name }}”? Chats that used its tools keep their transcripts.">{{ t("Delete") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -54,19 +54,16 @@
|
||||
enctype="multipart/form-data" class="btn-row">
|
||||
<input class="input input--file" type="file" name="image"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif" required
|
||||
aria-label="Model image">
|
||||
aria-label="{{ t('Model image') }}">
|
||||
<button class="btn btn--sm" type="submit">{{ icon("image", "icon--sm") }} Upload image</button>
|
||||
</form>
|
||||
{% if model.image_path %}
|
||||
<form method="post" action="/admin/models/{{ model.id }}/image/delete">
|
||||
<button class="btn btn--sm btn--danger" type="submit">Remove image</button>
|
||||
<button class="btn btn--sm btn--danger" type="submit">{{ t("Remove image") }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
PNG, JPEG, WEBP or GIF, under 2 MB. Without one, the model gets a generated
|
||||
badge whose colour is derived from its id.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("PNG, JPEG, WEBP or GIF, under 2 MB. Without one, the model gets a generated badge whose colour is derived from its id.") }}</p>
|
||||
</section>
|
||||
|
||||
{# Empty, hidden, and outside every other form: the Detect button further down
|
||||
@@ -77,18 +74,18 @@
|
||||
|
||||
<form method="post" action="/admin/models/{{ model.id }}">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Presentation</h2>
|
||||
<h2 class="card__title">{{ t("Presentation") }}</h2>
|
||||
|
||||
<div class="grid grid--2">
|
||||
<div class="field">
|
||||
<label class="field__label" for="display-name">Display name</label>
|
||||
<label class="field__label" for="display-name">{{ t("Display name") }}</label>
|
||||
<input class="input" id="display-name" name="display_name"
|
||||
value="{{ model.display_name }}" placeholder="{{ model.model_id }}">
|
||||
<p class="field__hint">Shown instead of the raw id. Empty uses the id.</p>
|
||||
<p class="field__hint">{{ t("Shown instead of the raw id. Empty uses the id.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="position">Position</label>
|
||||
<label class="field__label" for="position">{{ t("Position") }}</label>
|
||||
<input class="input" id="position" name="position" type="number" min="1"
|
||||
max="{{ total }}" value="{{ position_of }}">
|
||||
<p class="field__hint">
|
||||
@@ -99,19 +96,15 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="context-length">Context length</label>
|
||||
<label class="field__label" for="context-length">{{ t("Context length") }}</label>
|
||||
<input class="input" id="context-length" name="context_length" type="number"
|
||||
min="0" step="1" placeholder="unknown"
|
||||
min="0" step="1" placeholder="{{ t('unknown') }}"
|
||||
value="{{ model.context_length or '' }}">
|
||||
<p class="field__hint">
|
||||
How many tokens this model can hold, filled in from the endpoint where
|
||||
it says. Leave it empty if you do not know: the context percentage and
|
||||
automatic compaction both stay off rather than working from a guess.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("How many tokens this model can hold, filled in from the endpoint where it says. Leave it empty if you do not know: the context percentage and automatic compaction both stay off rather than working from a guess.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="field__label">Reasoning efforts this model accepts</span>
|
||||
<span class="field__label">{{ t("Reasoning efforts this model accepts") }}</span>
|
||||
<div class="btn-row">
|
||||
{% for value in efforts %}
|
||||
<label class="checkbox">
|
||||
@@ -162,14 +155,14 @@
|
||||
</button>
|
||||
|
||||
<p class="field__hint">
|
||||
The vocabulary is <strong>not the same for every model</strong>, and
|
||||
The vocabulary is <strong>{{ t("not the same for every model") }}</strong>, and
|
||||
sending one a model does not know is not ignored — it is rendered into
|
||||
the model's chat template, which raises and fails the whole reply.
|
||||
gpt-oss takes <span class="mono">low/medium/high</span>; Bonsai takes
|
||||
<span class="mono">low/medium/xhigh</span> and refuses
|
||||
<span class="mono">high</span>; OpenAI has added
|
||||
<span class="mono">minimal</span>, <span class="mono">xhigh</span> and
|
||||
<span class="mono">max</span> at various points.
|
||||
gpt-oss takes <span class="mono">{{ t("low/medium/high") }}</span>; Bonsai takes
|
||||
<span class="mono">{{ t("low/medium/xhigh") }}</span> and refuses
|
||||
<span class="mono">{{ t("high") }}</span>; OpenAI has added
|
||||
<span class="mono">{{ t("minimal") }}</span>, <span class="mono">{{ t("xhigh") }}</span> and
|
||||
<span class="mono">{{ t("max") }}</span> at various points.
|
||||
<br>
|
||||
Tick none and the common three are offered, which is right for almost
|
||||
everything. If an endpoint ever refuses one anyway, that reply is
|
||||
@@ -179,9 +172,9 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="default-effort">Default reasoning effort</label>
|
||||
<label class="field__label" for="default-effort">{{ t("Default reasoning effort") }}</label>
|
||||
<select class="select" id="default-effort" name="default_effort">
|
||||
<option value="">None — send nothing</option>
|
||||
<option value="">{{ t("None — send nothing") }}</option>
|
||||
{% for value in model_efforts %}
|
||||
<option value="{{ value }}"
|
||||
{{ 'selected' if model.params_json.get('reasoning_effort') == value }}>
|
||||
@@ -190,19 +183,19 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="field__hint">
|
||||
A <em>seed</em>, not a per-request setting: it is copied onto a chat when
|
||||
A <em>{{ t("seed") }}</em>, not a per-request setting: it is copied onto a chat when
|
||||
the chat is created and when somebody switches to this model, and from
|
||||
then on the chat's own value is what is sent. Changing it here therefore
|
||||
does nothing to chats that already exist. The composer's picker shows
|
||||
whichever level is actually in force, so what somebody sees there is
|
||||
what goes out. Anyone can change it per chat with
|
||||
<span class="mono">/effort</span>, and the control only appears on a
|
||||
model marked <strong>Reasoning</strong> above.
|
||||
<span class="mono">{{ t("/effort") }}</span>, and the control only appears on a
|
||||
model marked <strong>{{ t("Reasoning") }}</strong> above.
|
||||
<br>
|
||||
Sent two ways at once, because there is no one field that works: OpenAI
|
||||
and vLLM read <span class="mono">reasoning_effort</span>, while
|
||||
and vLLM read <span class="mono">{{ t("reasoning_effort") }}</span>, while
|
||||
llama.cpp drops it silently and reads only
|
||||
<span class="mono">chat_template_kwargs</span> — which is the route by
|
||||
<span class="mono">{{ t("chat_template_kwargs") }}</span> — which is the route by
|
||||
which it reaches gpt-oss. Both go out, and only on a chat that has an
|
||||
effort set, so an endpoint strict about unknown parameters is untouched
|
||||
until somebody chooses one.
|
||||
@@ -210,9 +203,9 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="description">Description</label>
|
||||
<label class="field__label" for="description">{{ t("Description") }}</label>
|
||||
<textarea class="textarea" id="description" name="description" rows="2"
|
||||
placeholder="What is this model good at?">{{ model.description }}</textarea>
|
||||
placeholder="{{ t('What is this model good at?') }}">{{ model.description }}</textarea>
|
||||
<p class="field__hint">
|
||||
Shown in the chat settings panel and your users' settings — and, if any
|
||||
model here may ask another one a question, read by those models too.
|
||||
@@ -220,26 +213,18 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="notes">Facts for other models</label>
|
||||
<label class="field__label" for="notes">{{ t("Facts for other models") }}</label>
|
||||
<textarea class="textarea" id="notes" name="notes" rows="3"
|
||||
placeholder="Parameters, quantisation, a benchmark figure, what it is bad at">{{ model.notes }}</textarea>
|
||||
<p class="field__hint">
|
||||
Never shown to a person. It goes into the list of the other models that a
|
||||
model sees when it is allowed to ask one of them a question, so write what
|
||||
would help it choose: size, what this one is good and bad at, a score you
|
||||
trust. Leave it empty and the description above carries that on its own.
|
||||
</p>
|
||||
placeholder="{{ t('Parameters, quantisation, a benchmark figure, what it is bad at') }}">{{ model.notes }}</textarea>
|
||||
<p class="field__hint">{{ t("Never shown to a person. It goes into the list of the other models that a model sees when it is allowed to ask one of them a question, so write what would help it choose: size, what this one is good and bad at, a score you trust. Leave it empty and the description above carries that on its own.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">System prompt</h2>
|
||||
<p class="card__lede">
|
||||
Used by chats on this model that have no prompt of their own. A chat's own
|
||||
prompt overrides this; this overrides the instance prompt.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("System prompt") }}</h2>
|
||||
<p class="card__lede">{{ t("Used by chats on this model that have no prompt of their own. A chat's own prompt overrides this; this overrides the instance prompt.") }}</p>
|
||||
<div class="field">
|
||||
<label class="field__label visually-hidden" for="system-prompt">System prompt</label>
|
||||
<label class="field__label visually-hidden" for="system-prompt">{{ t("System prompt") }}</label>
|
||||
<textarea class="textarea" id="system-prompt" name="system_prompt" rows="5"
|
||||
placeholder="{{ instance_prompt or 'No instance prompt is set.' }}"
|
||||
>{{ model.system_prompt }}</textarea>
|
||||
@@ -254,14 +239,14 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Capabilities</h2>
|
||||
<h2 class="card__title">{{ t("Capabilities") }}</h2>
|
||||
<p class="card__lede">
|
||||
What this endpoint can do. Endpoints rarely advertise it reliably, so it
|
||||
is your call. <strong>reasoning</strong> shows the thinking block,
|
||||
<strong>vision</strong> lets images be sent, and <strong>tools</strong> is
|
||||
is your call. <strong>{{ t("reasoning") }}</strong> shows the thinking block,
|
||||
<strong>{{ t("vision") }}</strong> lets images be sent, and <strong>{{ t("tools") }}</strong> is
|
||||
whether a tool list may be sent at all — turn it on for a model that does
|
||||
not support tool calling and every one of its replies fails.
|
||||
<strong>embeddings</strong> is the odd one out: it says this is not a chat
|
||||
<strong>{{ t("embeddings") }}</strong> is the odd one out: it says this is not a chat
|
||||
model at all, but one that turns text into vectors, and it is what
|
||||
<a href="/admin/extraction">Extraction</a> picks from.
|
||||
</p>
|
||||
@@ -280,14 +265,14 @@
|
||||
<h2 class="card__title">
|
||||
Built-in tools
|
||||
{% if not (model.capabilities_json or {}).get("tools") %}
|
||||
<span class="badge">needs tools</span>
|
||||
<span class="badge">{{ t("needs tools") }}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<p class="card__lede">
|
||||
What this model is given, as opposed to what it is capable of. None of it
|
||||
applies unless <strong>tools</strong> is ticked above. Each one is also
|
||||
applies unless <strong>{{ t("tools") }}</strong> is ticked above. Each one is also
|
||||
subject to the instance being configured for it and to the reader's own
|
||||
permissions — this only decides whether it is offered to <em>this</em>
|
||||
permissions — this only decides whether it is offered to <em>{{ t("this") }}</em>
|
||||
model.
|
||||
</p>
|
||||
<div class="checkbox-row">
|
||||
@@ -306,17 +291,17 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Availability</h2>
|
||||
<h2 class="card__title">{{ t("Availability") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<div class="checkbox-row">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if model.enabled }}>
|
||||
<span>Enabled — offered in chats</span>
|
||||
<span>{{ t("Enabled — offered in chats") }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="pinned" value="true" {{ 'checked' if model.pinned }}>
|
||||
<span>Pinned — shortcut in the chat sidebar</span>
|
||||
<span>{{ t("Pinned — shortcut in the chat sidebar") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,16 +309,13 @@
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="public" value="true" {{ 'checked' if model.public }}>
|
||||
<span>Available to everyone</span>
|
||||
<span>{{ t("Available to everyone") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Uncheck to restrict this model to chosen groups. Administrators always
|
||||
have access.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Uncheck to restrict this model to chosen groups. Administrators always have access.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="field__label">Groups with access</span>
|
||||
<span class="field__label">{{ t("Groups with access") }}</span>
|
||||
{% if groups %}
|
||||
<div class="checkbox-row">
|
||||
{% for group in groups %}
|
||||
@@ -344,7 +326,7 @@
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="field__hint">Ignored while the model is available to everyone.</p>
|
||||
<p class="field__hint">{{ t("Ignored while the model is available to everyone.") }}</p>
|
||||
{% else %}
|
||||
<p class="field__hint">
|
||||
No groups yet — <a href="/admin/groups">create one</a> to restrict access.
|
||||
@@ -354,7 +336,7 @@
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save changes</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save changes") }}</button>
|
||||
<a class="btn btn--ghost" href="/admin/models">Back to all models</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -362,28 +344,28 @@
|
||||
{# Outside the form above, and it has to be: two forms cannot nest, and this one
|
||||
posts somewhere else. See the note beside the Detect button. #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Default personality</h2>
|
||||
<h2 class="card__title">{{ t("Default personality") }}</h2>
|
||||
<p class="card__lede">
|
||||
Who this model is before it has worked out who it is with somebody. Different
|
||||
from the system prompt above: that is an instruction you write, this is a
|
||||
character it can be — and, with <strong>Edit its own personality</strong>
|
||||
character it can be — and, with <strong>{{ t("Edit its own personality") }}</strong>
|
||||
ticked, one it rewrites for itself.
|
||||
</p>
|
||||
<p class="card__lede">
|
||||
<strong>A personality belongs to a person.</strong> Each account gets its own
|
||||
<strong>{{ t("A personality belongs to a person.") }}</strong> Each account gets its own
|
||||
version of this model's character, starting from what you write here and
|
||||
diverging from it the first time the model writes its own. Changing this
|
||||
afterwards does not reach anybody who already has one, and it is not stacked
|
||||
underneath theirs — two personalities at once would contradict each other and
|
||||
nobody could tell which was losing. What the model *is*, as opposed to who it
|
||||
has become with somebody, belongs in <strong>Description</strong> and
|
||||
<strong>Facts for other models</strong> above, which are the same for everyone.
|
||||
has become with somebody, belongs in <strong>{{ t("Description") }}</strong> and
|
||||
<strong>{{ t("Facts for other models") }}</strong> above, which are the same for everyone.
|
||||
</p>
|
||||
<form method="post" action="/admin/models/{{ model.id }}/persona">
|
||||
<div class="field">
|
||||
<label class="field__label visually-hidden" for="persona">Default personality</label>
|
||||
<label class="field__label visually-hidden" for="persona">{{ t("Default personality") }}</label>
|
||||
<textarea class="textarea" id="persona" name="content" rows="6"
|
||||
placeholder="Nothing yet. Write one, or let the model write its own."
|
||||
placeholder="{{ t('Nothing yet. Write one, or let the model write its own.') }}"
|
||||
>{{ persona.content if persona else "" }}</textarea>
|
||||
<p class="field__hint">
|
||||
Up to {{ persona_limit }} characters, in the first person. Empty removes it
|
||||
@@ -392,11 +374,11 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit">Save personality</button>
|
||||
<button class="btn" type="submit">{{ t("Save personality") }}</button>
|
||||
{% if persona and persona.author == "model" %}
|
||||
<span class="badge badge--leaf">last written by the model</span>
|
||||
<span class="badge badge--leaf">{{ t("last written by the model") }}</span>
|
||||
{% elif persona %}
|
||||
<span class="badge">last written here</span>
|
||||
<span class="badge">{{ t("last written here") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
@@ -407,20 +389,16 @@
|
||||
<h2 class="card__title">
|
||||
Earlier defaults <span class="badge">{{ persona.revisions|length }}</span>
|
||||
</h2>
|
||||
<p class="card__lede">
|
||||
What this default said before each change. Each person's own personality keeps
|
||||
its own history, which they can see and restore in their own settings — this is
|
||||
the starting point's history, not theirs.
|
||||
</p>
|
||||
<p class="card__lede">{{ t("What this default said before each change. Each person's own personality keeps its own history, which they can see and restore in their own settings — this is the starting point's history, not theirs.") }}</p>
|
||||
<ul class="model-list">
|
||||
{% for revision in persona.revisions %}
|
||||
<li class="model-list__item">
|
||||
<div style="min-width: 0">
|
||||
<strong>{{ revision.created_at.strftime("%Y-%m-%d %H:%M") }}</strong>
|
||||
{% if revision.author == "model" %}
|
||||
<span class="badge badge--leaf">model</span>
|
||||
<span class="badge badge--leaf">{{ t("model") }}</span>
|
||||
{% else %}
|
||||
<span class="badge">you</span>
|
||||
<span class="badge">{{ t("you") }}</span>
|
||||
{% endif %}
|
||||
{% if revision.note %}<div class="text-xs faint">{{ revision.note }}</div>{% endif %}
|
||||
<div class="text-xs faint">
|
||||
@@ -428,8 +406,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/admin/models/{{ model.id }}/persona/revert"
|
||||
data-confirm="Put this version back? The current one is kept in the history."
|
||||
data-confirm-label="Restore" data-confirm-danger="false">
|
||||
data-confirm="{{ t('Put this version back? The current one is kept in the history.') }}"
|
||||
data-confirm-label="{{ t('Restore') }}" data-confirm-danger="false">
|
||||
<input type="hidden" name="revision_id" value="{{ revision.id }}">
|
||||
<button class="btn btn--sm" type="submit">
|
||||
{{ icon("refresh", "icon--sm") }} Restore
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
Every model discovered across your connections. The order here is the order
|
||||
users see in the picker. <strong>Pinning</strong> does not reorder anything —
|
||||
it puts a shortcut in the chat sidebar. The <strong>default</strong> is what a
|
||||
users see in the picker. <strong>{{ t("Pinning") }}</strong> does not reorder anything —
|
||||
it puts a shortcut in the chat sidebar. The <strong>{{ t("default") }}</strong> is what a
|
||||
new chat starts with.
|
||||
</p>
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
<form class="filter-form" method="get" action="/admin/models">
|
||||
<input type="hidden" name="filter" value="{{ active_filter }}">
|
||||
<input class="input" type="search" name="q" value="{{ q }}"
|
||||
placeholder="Search models…" aria-label="Search models">
|
||||
<select class="select" name="connection" aria-label="Connection">
|
||||
<option value="">All connections</option>
|
||||
placeholder="{{ t('Search models…') }}" aria-label="{{ t('Search models') }}">
|
||||
<select class="select" name="connection" aria-label="{{ t('Connection') }}">
|
||||
<option value="">{{ t("All connections") }}</option>
|
||||
{% for conn in connections %}
|
||||
<option value="{{ conn.id }}" {{ 'selected' if conn.id == connection_id }}>
|
||||
{{ conn.name }}
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
{% if not models %}
|
||||
<div class="empty" style="padding: var(--sp-8) 0">
|
||||
<p class="empty__text">Nothing matches that filter.</p>
|
||||
<p class="empty__text">{{ t("Nothing matches that filter.") }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -67,15 +67,15 @@
|
||||
<div class="bulk-bar">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" data-select-all="#model-rows"
|
||||
aria-label="Select every model on this page">
|
||||
<span class="bulk-bar__label">Select all</span>
|
||||
aria-label="{{ t('Select every model on this page') }}">
|
||||
<span class="bulk-bar__label">{{ t("Select all") }}</span>
|
||||
</label>
|
||||
<span class="spacer"></span>
|
||||
<span class="bulk-bar__label">With selected:</span>
|
||||
<button class="btn btn--sm" name="action" value="enable" type="submit">Enable</button>
|
||||
<button class="btn btn--sm" name="action" value="disable" type="submit">Disable</button>
|
||||
<button class="btn btn--sm" name="action" value="public" type="submit">Make public</button>
|
||||
<button class="btn btn--sm" name="action" value="private" type="submit">Restrict</button>
|
||||
<span class="bulk-bar__label">{{ t("With selected:") }}</span>
|
||||
<button class="btn btn--sm" name="action" value="enable" type="submit">{{ t("Enable") }}</button>
|
||||
<button class="btn btn--sm" name="action" value="disable" type="submit">{{ t("Disable") }}</button>
|
||||
<button class="btn btn--sm" name="action" value="public" type="submit">{{ t("Make public") }}</button>
|
||||
<button class="btn btn--sm" name="action" value="private" type="submit">{{ t("Restrict") }}</button>
|
||||
</div>
|
||||
|
||||
<div class="model-rows" id="model-rows">
|
||||
@@ -92,11 +92,11 @@
|
||||
<div class="model-row__title">
|
||||
<a class="model-row__name" href="/admin/models/{{ model.id }}/edit">{{ model.label }}</a>
|
||||
{% if model.model_id == default_model %}
|
||||
<span class="badge badge--leaf">default</span>
|
||||
<span class="badge badge--leaf">{{ t("default") }}</span>
|
||||
{% endif %}
|
||||
{% if model.pinned %}<span class="badge">pinned</span>{% endif %}
|
||||
{% if not model.enabled %}<span class="badge badge--danger">disabled</span>{% endif %}
|
||||
{% if not model.public %}<span class="badge">restricted</span>{% endif %}
|
||||
{% if model.pinned %}<span class="badge">{{ t("pinned") }}</span>{% endif %}
|
||||
{% if not model.enabled %}<span class="badge badge--danger">{{ t("disabled") }}</span>{% endif %}
|
||||
{% if not model.public %}<span class="badge">{{ t("restricted") }}</span>{% endif %}
|
||||
{% for name, on in (model.capabilities_json or {}).items() %}
|
||||
{% if on %}<span class="badge badge--leaf">{{ name }}</span>{% endif %}
|
||||
{% endfor %}
|
||||
@@ -106,12 +106,12 @@
|
||||
|
||||
<div class="model-row__actions">
|
||||
{# formaction lets these post elsewhere without nesting a second form. #}
|
||||
<button class="btn btn--icon btn--sm" type="submit" aria-label="Move up"
|
||||
<button class="btn btn--icon btn--sm" type="submit" aria-label="{{ t('Move up') }}"
|
||||
formaction="/admin/models/{{ model.id }}/move" name="direction" value="up"
|
||||
{{ 'disabled' if page == 1 and loop.first }}>
|
||||
{{ icon("arrow-up", "icon--sm") }}
|
||||
</button>
|
||||
<button class="btn btn--icon btn--sm" type="submit" aria-label="Move down"
|
||||
<button class="btn btn--icon btn--sm" type="submit" aria-label="{{ t('Move down') }}"
|
||||
formaction="/admin/models/{{ model.id }}/move" name="direction" value="down"
|
||||
{{ 'disabled' if page == pages and loop.last }}>
|
||||
{{ icon("arrow-down", "icon--sm") }}
|
||||
@@ -135,13 +135,13 @@
|
||||
{% if page > 1 %}
|
||||
<a class="btn btn--sm" href="{{ base }}&page={{ page - 1 }}">Previous</a>
|
||||
{% else %}
|
||||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">Previous</span>
|
||||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">{{ t("Previous") }}</span>
|
||||
{% endif %}
|
||||
<span class="text-xs faint">Page {{ page }} of {{ pages }}</span>
|
||||
{% if page < pages %}
|
||||
<a class="btn btn--sm" href="{{ base }}&page={{ page + 1 }}">Next</a>
|
||||
{% else %}
|
||||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">Next</span>
|
||||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">{{ t("Next") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
use each tool, what it has been asked to remember. These sit above whichever
|
||||
system prompt was authored for the instance, the model or the chat — that
|
||||
prompt still wins where the two disagree. Clear a box to leave that piece out
|
||||
altogether; press <strong>Use default</strong> to put the built-in wording back.
|
||||
altogether; press <strong>{{ t("Use default") }}</strong> to put the built-in wording back.
|
||||
</p>
|
||||
|
||||
{% if saved %}
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>Prompts saved.</span></div>
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ t("Prompts saved.") }}</span></div>
|
||||
{% endif %}
|
||||
|
||||
{#
|
||||
@@ -39,7 +39,7 @@
|
||||
#}
|
||||
<form method="post" action="/admin/prompts" id="prompt-form">
|
||||
<div class="tabs">
|
||||
<div class="tabs__bar" role="radiogroup" aria-label="Prompt groups">
|
||||
<div class="tabs__bar" role="radiogroup" aria-label="{{ t('Prompt groups') }}">
|
||||
{% for key, label, fragments in groups %}
|
||||
<input class="visually-hidden" type="radio" name="prompts-tab"
|
||||
id="tab-{{ key }}" {{ 'checked' if loop.first }}>
|
||||
@@ -61,9 +61,9 @@
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Length</h2>
|
||||
<h2 class="card__title">{{ t("Length") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="max-harness-chars">Preamble character cap</label>
|
||||
<label class="field__label" for="max-harness-chars">{{ t("Preamble character cap") }}</label>
|
||||
<input class="input" id="max-harness-chars" name="max_harness_chars" type="number"
|
||||
min="0" max="100000" value="{{ max_harness_chars }}">
|
||||
<p class="field__hint">
|
||||
@@ -75,22 +75,20 @@
|
||||
</section>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="btn btn--primary" type="submit">Save settings</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save settings") }}</button>
|
||||
{#
|
||||
data-confirm-button, not data-confirm: this button acts on its own through
|
||||
formaction, and confirming the whole form would also catch plain Save.
|
||||
#}
|
||||
<button class="btn" type="submit" formaction="/admin/prompts/reset"
|
||||
data-confirm-button="Put every prompt back to its built-in wording? Everything you have edited here is lost."
|
||||
data-confirm-title="Restore defaults" data-confirm-label="Restore">
|
||||
Restore all defaults
|
||||
</button>
|
||||
data-confirm-button="{{ t('Put every prompt back to its built-in wording? Everything you have edited here is lost.') }}"
|
||||
data-confirm-title="{{ t('Restore defaults') }}" data-confirm-label="{{ t('Restore') }}">{{ t("Restore all defaults") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Variables</h2>
|
||||
<h2 class="card__title">{{ t("Variables") }}</h2>
|
||||
<p class="card__lede">
|
||||
Write these in double braces. Anything in double braces that is not on this
|
||||
list is left exactly as you typed it — variable names are lowercase letters,
|
||||
@@ -105,11 +103,11 @@
|
||||
<span>{{ variable.description }}</span>
|
||||
<span class="ref-row__value">
|
||||
{% if variable.name not in resolved %}
|
||||
<em>only in the title request</em>
|
||||
<em>{{ t("only in the title request") }}</em>
|
||||
{% elif resolved[variable.name] %}
|
||||
{{ resolved[variable.name] | truncate(80) }}
|
||||
{% else %}
|
||||
<em>empty</em>
|
||||
<em>{{ t("empty") }}</em>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
@@ -124,32 +122,27 @@
|
||||
replaced -- an outerHTML swap would re-fire it and loop forever.
|
||||
#}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Preview</h2>
|
||||
<p class="card__lede">
|
||||
The whole system message, assembled from what is in the boxes below —
|
||||
including changes you have not saved yet. Your own memories and skills are
|
||||
used, because a preview against invented ones cannot tell you whether it
|
||||
reads well against what is actually there.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Preview") }}</h2>
|
||||
<p class="card__lede">{{ t("The whole system message, assembled from what is in the boxes below — including changes you have not saved yet. Your own memories and skills are used, because a preview against invented ones cannot tell you whether it reads well against what is actually there.") }}</p>
|
||||
|
||||
<div class="grid grid--2" id="preview-controls">
|
||||
<div class="field">
|
||||
<label class="field__label" for="preview-model">Answering model</label>
|
||||
<label class="field__label" for="preview-model">{{ t("Answering model") }}</label>
|
||||
<select class="select" id="preview-model" name="preview_model">
|
||||
{% for model in models %}
|
||||
<option value="{{ model.label }}">{{ model.label }}</option>
|
||||
{% else %}
|
||||
<option value="">No models configured</option>
|
||||
<option value="">{{ t("No models configured") }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="preview-bases">Knowledge bases in scope</label>
|
||||
<label class="field__label" for="preview-bases">{{ t("Knowledge bases in scope") }}</label>
|
||||
<input class="input" id="preview-bases" name="preview_bases" value="Contracts, Recipes">
|
||||
<p class="field__hint">Empty means a chat that can see everything.</p>
|
||||
<p class="field__hint">{{ t("Empty means a chat that can see everything.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="preview-documents">Attached files</label>
|
||||
<label class="field__label" for="preview-documents">{{ t("Attached files") }}</label>
|
||||
<input class="input" id="preview-documents" name="preview_documents"
|
||||
value="{{ sample_documents }}">
|
||||
</div>
|
||||
@@ -161,7 +154,7 @@
|
||||
filled when there is one.
|
||||
#}
|
||||
<div class="field">
|
||||
<label class="field__label" for="preview-situation">This chat is</label>
|
||||
<label class="field__label" for="preview-situation">{{ t("This chat is") }}</label>
|
||||
<select class="select" id="preview-situation" name="preview_situation">
|
||||
{% for value, label in situations %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
@@ -169,7 +162,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="preview-mode">Agent mode</label>
|
||||
<label class="field__label" for="preview-mode">{{ t("Agent mode") }}</label>
|
||||
<select class="select" id="preview-mode" name="preview_mode">
|
||||
{% for value, label in modes.items() %}
|
||||
<option value="{{ value }}"{{ ' selected' if value == 'edit' }}>{{ label }}</option>
|
||||
@@ -181,7 +174,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field__label">Tools offered</span>
|
||||
<span class="field__label">{{ t("Tools offered") }}</span>
|
||||
{% for family in families %}
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="preview_family" value="{{ family }}" checked>
|
||||
@@ -206,14 +199,8 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Tool descriptions</h2>
|
||||
<p class="card__lede">
|
||||
Defined in code — part of the schema sent to the endpoint alongside the
|
||||
prompt, not guidance layered on top of it. They are statements of fact about
|
||||
what each tool does, so they change when the tool does; editing them here
|
||||
would let the text quietly become a lie. A custom tool's description will be
|
||||
editable, because a custom tool is a row rather than a function.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Tool descriptions") }}</h2>
|
||||
<p class="card__lede">{{ t("Defined in code — part of the schema sent to the endpoint alongside the prompt, not guidance layered on top of it. They are statements of fact about what each tool does, so they change when the tool does; editing them here would let the text quietly become a lie. A custom tool's description will be editable, because a custom tool is a row rather than a function.") }}</p>
|
||||
<div class="ref-list">
|
||||
{% for tool in registry %}
|
||||
<div class="ref-row">
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
A <strong>schedule</strong> runs a piece of work because time has passed
|
||||
A <strong>{{ t("schedule") }}</strong> runs a piece of work because time has passed
|
||||
rather than because somebody asked just now — a daily summary, a check every
|
||||
Monday, a reminder in an hour. Each one has its own chat and replies into it,
|
||||
or files a report. People make their own under <strong>Scheduled</strong>;
|
||||
or files a report. People make their own under <strong>{{ t("Scheduled") }}</strong>;
|
||||
what you decide here is whether the feature exists and what it may spend.
|
||||
</p>
|
||||
|
||||
@@ -20,24 +20,24 @@
|
||||
This is the one thing here that spends model time with nobody watching. A
|
||||
schedule pointed at a chat that can run commands would run them unattended,
|
||||
so a scheduled task is never an agent chat — but everything else a model can
|
||||
reach, it can reach on a timer. Give <strong>Schedule work</strong> under
|
||||
reach, it can reach on a timer. Give <strong>{{ t("Schedule work") }}</strong> under
|
||||
Groups & permissions to the people who should have it; it is off for
|
||||
everybody by default.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if saved %}
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>Saved.</span></div>
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ t("Saved.") }}</span></div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/schedules" class="form-grid">
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Switch</h2>
|
||||
<h2 class="card__title">{{ t("Switch") }}</h2>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if values.enabled }}>
|
||||
<span>Let scheduled work run</span>
|
||||
<span>{{ t("Let scheduled work run") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off by default. Turning this off stops everything firing and deletes
|
||||
@@ -53,65 +53,48 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">How often it looks</h2>
|
||||
<h2 class="card__title">{{ t("How often it looks") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="tick">Check for due work every</label>
|
||||
<label class="field__label" for="tick">{{ t("Check for due work every") }}</label>
|
||||
<input class="input" type="number" id="tick" name="tick_seconds" min="5" max="300"
|
||||
value="{{ values.tick_seconds }}">
|
||||
<p class="field__hint">
|
||||
Seconds. This is how late a run can be, not how often anything happens:
|
||||
the finest a schedule can be set to is one minute, so anything under
|
||||
that buys nothing. One indexed query per tick.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Seconds. This is how late a run can be, not how often anything happens: the finest a schedule can be set to is one minute, so anything under that buys nothing. One indexed query per tick.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="min-interval">Nothing may repeat faster than</label>
|
||||
<label class="field__label" for="min-interval">{{ t("Nothing may repeat faster than") }}</label>
|
||||
<input class="input" type="number" id="min-interval" name="min_interval_seconds"
|
||||
min="60" max="86400" value="{{ values.min_interval_seconds }}">
|
||||
<p class="field__hint">
|
||||
Seconds. A floor on how often one schedule may come round. Raise it if
|
||||
people are setting things to run more often than the work takes.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Seconds. A floor on how often one schedule may come round. Raise it if people are setting things to run more often than the work takes.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">What it may spend</h2>
|
||||
<h2 class="card__title">{{ t("What it may spend") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="per-user">Schedules per person</label>
|
||||
<label class="field__label" for="per-user">{{ t("Schedules per person") }}</label>
|
||||
<input class="input" type="number" id="per-user" name="max_per_user" min="1" max="200"
|
||||
value="{{ values.max_per_user }}">
|
||||
<p class="field__hint">
|
||||
Refused at the point of creation, with the reason. Existing schedules
|
||||
over a lowered limit keep running; only new ones are refused.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Refused at the point of creation, with the reason. Existing schedules over a lowered limit keep running; only new ones are refused.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="concurrent">Runs at once</label>
|
||||
<label class="field__label" for="concurrent">{{ t("Runs at once") }}</label>
|
||||
<input class="input" type="number" id="concurrent" name="max_concurrent" min="1" max="20"
|
||||
value="{{ values.max_concurrent }}">
|
||||
<p class="field__hint">
|
||||
Fifty schedules due at nine o'clock must not open fifty replies against
|
||||
one endpoint. The rest wait their turn rather than being dropped.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Fifty schedules due at nine o'clock must not open fifty replies against one endpoint. The rest wait their turn rather than being dropped.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="queued">Turns that may pile up in one chat</label>
|
||||
<label class="field__label" for="queued">{{ t("Turns that may pile up in one chat") }}</label>
|
||||
<input class="input" type="number" id="queued" name="max_queued" min="1" max="50"
|
||||
value="{{ values.max_queued }}">
|
||||
<p class="field__hint">
|
||||
A schedule that comes round faster than its chat can answer would build
|
||||
a backlog for ever. Past this, a run is skipped and says so on the
|
||||
schedule rather than joining the queue.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("A schedule that comes round faster than its chat can answer would build a backlog for ever. Past this, a run is skipped and says so on the schedule rather than joining the queue.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,28 +6,23 @@
|
||||
{% block heading %}Web search{% endblock %}
|
||||
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
Lets a model look things up while it answers. It is offered as a tool the
|
||||
model chooses to call, so nothing changes for a question that does not need
|
||||
it — and it is only offered to models marked as supporting tools, because
|
||||
sending a tool list to one that does not fails the whole request.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Lets a model look things up while it answers. It is offered as a tool the model chooses to call, so nothing changes for a question that does not need it — and it is only offered to models marked as supporting tools, because sending a tool list to one that does not fails the whole request.") }}</p>
|
||||
|
||||
{% if saved %}
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>Search settings saved.</span></div>
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ t("Search settings saved.") }}</span></div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/search">
|
||||
<section class="card">
|
||||
<h2 class="card__title">
|
||||
Web search
|
||||
{% if values.enabled %}<span class="badge badge--success">on</span>
|
||||
{% else %}<span class="badge">off</span>{% endif %}
|
||||
{% if values.enabled %}<span class="badge badge--success">{{ t("on") }}</span>
|
||||
{% else %}<span class="badge">{{ t("off") }}</span>{% endif %}
|
||||
</h2>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if values.enabled }}>
|
||||
<span>Offer web search to models that support tools</span>
|
||||
<span>{{ t("Offer web search to models that support tools") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Who may use it is a permission — <code>tools.web_search</code> under
|
||||
@@ -37,7 +32,7 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Provider</h2>
|
||||
<h2 class="card__title">{{ t("Provider") }}</h2>
|
||||
<div class="field">
|
||||
{% for provider in providers %}
|
||||
<label class="checkbox" style="align-items: flex-start">
|
||||
@@ -45,7 +40,7 @@
|
||||
{{ 'checked' if values.provider == provider.key }}>
|
||||
<span>
|
||||
<strong>{{ provider.label }}</strong>
|
||||
{% if not provider.needs_setup %}<span class="badge">no setup</span>{% endif %}
|
||||
{% if not provider.needs_setup %}<span class="badge">{{ t("no setup") }}</span>{% endif %}
|
||||
<div class="text-xs faint">{{ provider.description }}</div>
|
||||
{% if problems[provider.key] %}
|
||||
<div class="text-xs" style="color: var(--danger)">{{ problems[provider.key] }}</div>
|
||||
@@ -57,13 +52,13 @@
|
||||
|
||||
<div class="grid grid--3">
|
||||
<div class="field">
|
||||
<label class="field__label" for="max-results">Results per search</label>
|
||||
<label class="field__label" for="max-results">{{ t("Results per search") }}</label>
|
||||
<input class="input" id="max-results" name="max_results" type="number"
|
||||
min="1" max="20" value="{{ values.max_results }}">
|
||||
<p class="field__hint">A ceiling — a model asking for more gets this.</p>
|
||||
<p class="field__hint">{{ t("A ceiling — a model asking for more gets this.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="safesearch">Safe search</label>
|
||||
<label class="field__label" for="safesearch">{{ t("Safe search") }}</label>
|
||||
<select class="select" id="safesearch" name="safesearch">
|
||||
{% for option in safesearch_options %}
|
||||
<option value="{{ option }}" {{ 'selected' if option == values.safesearch }}>
|
||||
@@ -73,16 +68,16 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="timeout">Timeout (seconds)</label>
|
||||
<label class="field__label" for="timeout">{{ t("Timeout (seconds)") }}</label>
|
||||
<input class="input" id="timeout" name="timeout" type="number"
|
||||
min="5" max="120" step="1" value="{{ values.timeout }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="region">DuckDuckGo region</label>
|
||||
<label class="field__label" for="region">{{ t("DuckDuckGo region") }}</label>
|
||||
<input class="input" id="region" name="region" maxlength="16"
|
||||
value="{{ values.region }}" placeholder="wt-wt">
|
||||
value="{{ values.region }}" placeholder="{{ t('wt-wt') }}">
|
||||
<p class="field__hint">
|
||||
<code>wt-wt</code> is no region at all. <code>uk-en</code>,
|
||||
<code>de-de</code> and so on bias results to a country.
|
||||
@@ -91,15 +86,12 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">SearXNG</h2>
|
||||
<p class="card__lede">
|
||||
Only used when SearXNG is the chosen provider. Your own instance, so no
|
||||
third party sees the queries.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("SearXNG") }}</h2>
|
||||
<p class="card__lede">{{ t("Only used when SearXNG is the chosen provider. Your own instance, so no third party sees the queries.") }}</p>
|
||||
<div class="field">
|
||||
<label class="field__label" for="searxng-base-url">Instance URL</label>
|
||||
<label class="field__label" for="searxng-base-url">{{ t("Instance URL") }}</label>
|
||||
<input class="input" id="searxng-base-url" name="searxng_base_url" type="url"
|
||||
value="{{ values.searxng_base_url }}" placeholder="http://127.0.0.1:8888">
|
||||
value="{{ values.searxng_base_url }}" placeholder="{{ t('http://127.0.0.1:8888') }}">
|
||||
<p class="field__hint">
|
||||
A stock SearXNG refuses JSON. Add <code>- json</code> under
|
||||
<code>search.formats</code> in its <code>settings.yml</code> and restart
|
||||
@@ -109,16 +101,16 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Saving links</h2>
|
||||
<h2 class="card__title">{{ t("Saving links") }}</h2>
|
||||
<p class="card__lede">
|
||||
Applies to the composer's <strong>Link</strong> option and to anything the
|
||||
Applies to the composer's <strong>{{ t("Link") }}</strong> option and to anything the
|
||||
model fetches: {{ brand.name }} retrieves the page and keeps its text.
|
||||
</p>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="fetch_enabled" value="true"
|
||||
{{ 'checked' if values.fetch_enabled }}>
|
||||
<span>Let a model fetch a page itself</span>
|
||||
<span>{{ t("Let a model fetch a page itself") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Offers the <code>fetch</code> tool, so a model can read an address it
|
||||
@@ -132,7 +124,7 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="allow_private_fetch" value="true"
|
||||
{{ 'checked' if values.allow_private_fetch }}>
|
||||
<span>Allow fetching addresses on this machine and this network</span>
|
||||
<span>{{ t("Allow fetching addresses on this machine and this network") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Off by default, and worth leaving off. This server can reach your
|
||||
@@ -145,36 +137,36 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Firecrawl</h2>
|
||||
<h2 class="card__title">{{ t("Firecrawl") }}</h2>
|
||||
<div class="grid grid--2">
|
||||
<div class="field">
|
||||
<label class="field__label" for="firecrawl-base-url">API URL</label>
|
||||
<label class="field__label" for="firecrawl-base-url">{{ t("API URL") }}</label>
|
||||
<input class="input" id="firecrawl-base-url" name="firecrawl_base_url" type="url"
|
||||
value="{{ values.firecrawl_base_url }}"
|
||||
placeholder="https://api.firecrawl.dev">
|
||||
<p class="field__hint">Change only for a self-hosted Firecrawl.</p>
|
||||
placeholder="{{ t('https://api.firecrawl.dev') }}">
|
||||
<p class="field__hint">{{ t("Change only for a self-hosted Firecrawl.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="firecrawl-api-key">API key</label>
|
||||
<label class="field__label" for="firecrawl-api-key">{{ t("API key") }}</label>
|
||||
<input class="input" id="firecrawl-api-key" name="firecrawl_api_key" type="password"
|
||||
value="{{ unchanged if masked else '' }}"
|
||||
placeholder="{{ masked or 'fc-...' }}" autocomplete="off">
|
||||
<p class="field__hint">Encrypted at rest. Clear the field to remove it.</p>
|
||||
<p class="field__hint">{{ t("Encrypted at rest. Clear the field to remove it.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="form-actions"><button class="btn btn--primary" type="submit">Save settings</button></div>
|
||||
<div class="form-actions"><button class="btn btn--primary" type="submit">{{ t("Save settings") }}</button></div>
|
||||
</form>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Try it</h2>
|
||||
<h2 class="card__title">{{ t("Try it") }}</h2>
|
||||
<p class="card__lede">
|
||||
Runs a real search against the <em>saved</em> settings, which is what a chat
|
||||
Runs a real search against the <em>{{ t("saved") }}</em> settings, which is what a chat
|
||||
would do. Save first if you have just changed something.
|
||||
</p>
|
||||
<div class="row" style="gap: var(--sp-2)">
|
||||
<input class="input" id="test-query" name="query" placeholder="mallorn tree"
|
||||
<input class="input" id="test-query" name="query" placeholder="{{ t('mallorn tree') }}"
|
||||
style="flex: 1">
|
||||
<button class="btn" type="button" hx-post="/admin/search/test"
|
||||
hx-include="#test-query" hx-target="#search-test" hx-swap="outerHTML">
|
||||
|
||||
@@ -22,93 +22,88 @@
|
||||
<div class="card__header">
|
||||
<h2 class="card__title">
|
||||
{{ suggestion.name }}
|
||||
{% if not suggestion.enabled %}<span class="badge">hidden</span>{% endif %}
|
||||
{% if not suggestion.enabled %}<span class="badge">{{ t("hidden") }}</span>{% endif %}
|
||||
{% if loop.index > max_shown and suggestion.enabled %}
|
||||
<span class="badge badge--warning" title="Only the first {{ max_shown }} are shown">
|
||||
below the cut
|
||||
</span>
|
||||
<span class="badge badge--warning" title="Only the first {{ max_shown }} are shown">{{ t("below the cut") }}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<button class="btn btn--sm btn--danger" type="submit"
|
||||
formaction="/admin/suggestions/{{ suggestion.id }}/delete"
|
||||
data-confirm-button="Delete the suggestion “{{ suggestion.name }}”?"
|
||||
data-confirm-title="Delete suggestion"
|
||||
aria-label="Delete suggestion" title="Delete suggestion">
|
||||
data-confirm-title="{{ t('Delete suggestion') }}"
|
||||
aria-label="{{ t('Delete suggestion') }}" title="{{ t('Delete suggestion') }}">
|
||||
{{ icon("trash", "icon--sm") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid--2">
|
||||
<div class="field">
|
||||
<label class="field__label" for="name-{{ suggestion.id }}">Name</label>
|
||||
<label class="field__label" for="name-{{ suggestion.id }}">{{ t("Name") }}</label>
|
||||
<input class="input" id="name-{{ suggestion.id }}" name="name"
|
||||
value="{{ suggestion.name }}" maxlength="120">
|
||||
<p class="field__hint">The heading on the card.</p>
|
||||
<p class="field__hint">{{ t("The heading on the card.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="position-{{ suggestion.id }}">Position</label>
|
||||
<label class="field__label" for="position-{{ suggestion.id }}">{{ t("Position") }}</label>
|
||||
<input class="input" id="position-{{ suggestion.id }}" name="position" type="number"
|
||||
min="1" value="{{ suggestion.position + 1 }}">
|
||||
<p class="field__hint">Order on the screen, lowest first.</p>
|
||||
<p class="field__hint">{{ t("Order on the screen, lowest first.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="description-{{ suggestion.id }}">Description</label>
|
||||
<label class="field__label" for="description-{{ suggestion.id }}">{{ t("Description") }}</label>
|
||||
<input class="input" id="description-{{ suggestion.id }}" name="description"
|
||||
value="{{ suggestion.description }}" maxlength="300">
|
||||
<p class="field__hint">One line under the name, saying what it is for.</p>
|
||||
<p class="field__hint">{{ t("One line under the name, saying what it is for.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="prompt-{{ suggestion.id }}">Prompt</label>
|
||||
<label class="field__label" for="prompt-{{ suggestion.id }}">{{ t("Prompt") }}</label>
|
||||
<textarea class="textarea" id="prompt-{{ suggestion.id }}" name="prompt"
|
||||
rows="4">{{ suggestion.prompt }}</textarea>
|
||||
<p class="field__hint">
|
||||
Sent as the first message. Nothing is added to it, so a prompt that needs
|
||||
material should ask for it.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Sent as the first message. Nothing is added to it, so a prompt that needs material should ask for it.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if suggestion.enabled }}>
|
||||
<span>Show this one</span>
|
||||
<span>{{ t("Show this one") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card__footer">
|
||||
<button class="btn btn--primary btn--sm" type="submit">Save</button>
|
||||
<button class="btn btn--primary btn--sm" type="submit">{{ t("Save") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="empty">
|
||||
{{ icon("sparkle", "empty__mark") }}
|
||||
<h2 class="empty__title">No suggestions</h2>
|
||||
<p class="empty__text">The new-chat screen shows its empty state instead.</p>
|
||||
<h2 class="empty__title">{{ t("No suggestions") }}</h2>
|
||||
<p class="empty__text">{{ t("The new-chat screen shows its empty state instead.") }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not at_limit %}
|
||||
<form class="card" method="post" action="/admin/suggestions">
|
||||
<h2 class="card__title">Add a suggestion</h2>
|
||||
<h2 class="card__title">{{ t("Add a suggestion") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-name">Name</label>
|
||||
<label class="field__label" for="new-name">{{ t("Name") }}</label>
|
||||
<input class="input" id="new-name" name="name" maxlength="120"
|
||||
placeholder="Summarise a document" required>
|
||||
placeholder="{{ t('Summarise a document') }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-description">Description</label>
|
||||
<label class="field__label" for="new-description">{{ t("Description") }}</label>
|
||||
<input class="input" id="new-description" name="description" maxlength="300"
|
||||
placeholder="What it is for, in one line.">
|
||||
placeholder="{{ t('What it is for, in one line.') }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-prompt">Prompt</label>
|
||||
<label class="field__label" for="new-prompt">{{ t("Prompt") }}</label>
|
||||
<textarea class="textarea" id="new-prompt" name="prompt" rows="4"
|
||||
placeholder="Sent as the first message when the card is clicked."></textarea>
|
||||
placeholder="{{ t('Sent as the first message when the card is clicked.') }}"></textarea>
|
||||
</div>
|
||||
<div class="card__footer">
|
||||
<button class="btn btn--primary" type="submit">Add</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Add") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
|
||||
@@ -20,19 +20,19 @@
|
||||
class="form-grid">
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">What it is</h2>
|
||||
<h2 class="card__title">{{ t("What it is") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input" id="name" name="name" value="{{ tool.name }}" required
|
||||
maxlength="120" placeholder="Weather">
|
||||
<p class="field__hint">Shown in the transcript when the model uses it.</p>
|
||||
maxlength="120" placeholder="{{ t('Weather') }}">
|
||||
<p class="field__hint">{{ t("Shown in the transcript when the model uses it.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="slug">Identifier</label>
|
||||
<label class="field__label" for="slug">{{ t("Identifier") }}</label>
|
||||
<input class="input input--mono" id="slug" name="slug" value="{{ tool.slug }}" required
|
||||
maxlength="48" placeholder="weather" pattern="[a-z0-9][a-z0-9_\-]*">
|
||||
maxlength="48" placeholder="{{ t('weather') }}" pattern="[a-z0-9][a-z0-9_\-]*">
|
||||
<p class="field__hint">
|
||||
The name the model calls, and the key its guidance is stored under.
|
||||
Lowercase letters, digits, hyphens and underscores.
|
||||
@@ -41,19 +41,15 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="description">Description</label>
|
||||
<label class="field__label" for="description">{{ t("Description") }}</label>
|
||||
<textarea class="textarea" id="description" name="description" rows="3"
|
||||
placeholder="Look up the current weather for a city."
|
||||
placeholder="{{ t('Look up the current weather for a city.') }}"
|
||||
>{{ tool.description }}</textarea>
|
||||
<p class="field__hint">
|
||||
Sent to the model verbatim. This is the whole basis on which it decides
|
||||
whether to call this tool, so say what it does and when it is the right
|
||||
thing to use.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Sent to the model verbatim. This is the whole basis on which it decides whether to call this tool, so say what it does and when it is the right thing to use.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="parameters">Parameters</label>
|
||||
<label class="field__label" for="parameters">{{ t("Parameters") }}</label>
|
||||
<textarea class="textarea input--mono" id="parameters" name="parameters" rows="10"
|
||||
spellcheck="false">{{ parameters_text }}</textarea>
|
||||
<p class="field__hint">
|
||||
@@ -66,10 +62,10 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">The call</h2>
|
||||
<h2 class="card__title">{{ t("The call") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="method">Method</label>
|
||||
<label class="field__label" for="method">{{ t("Method") }}</label>
|
||||
<select class="select" id="method" name="method">
|
||||
{% for method in methods %}
|
||||
<option value="{{ method }}" {{ 'selected' if method == tool.method }}>{{ method }}</option>
|
||||
@@ -78,46 +74,39 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="url_template">URL</label>
|
||||
<label class="field__label" for="url_template">{{ t("URL") }}</label>
|
||||
<input class="input input--mono" id="url_template" name="url_template" required
|
||||
value="{{ tool.url_template }}"
|
||||
placeholder="https://api.example.com/weather/{{ '{{city}}' }}">
|
||||
<p class="field__hint">
|
||||
Placeholders are filled from the arguments and escaped, so a value
|
||||
cannot add a path segment or a query of its own. The scheme and host
|
||||
must be written out — they cannot come from an argument.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Placeholders are filled from the arguments and escaped, so a value cannot add a path segment or a query of its own. The scheme and host must be written out — they cannot come from an argument.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="headers">Headers</label>
|
||||
<label class="field__label" for="headers">{{ t("Headers") }}</label>
|
||||
<textarea class="textarea input--mono" id="headers" name="headers" rows="3"
|
||||
spellcheck="false"
|
||||
placeholder="Accept: application/json">{{ headers_text }}</textarea>
|
||||
placeholder="{{ t('Accept: application/json') }}">{{ headers_text }}</textarea>
|
||||
<p class="field__hint">One <code>Name: value</code> per line. Placeholders work here too.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="body_template">Body</label>
|
||||
<label class="field__label" for="body_template">{{ t("Body") }}</label>
|
||||
<textarea class="textarea input--mono" id="body_template" name="body_template" rows="4"
|
||||
spellcheck="false">{{ tool.body_template }}</textarea>
|
||||
<p class="field__hint">
|
||||
Ignored for GET. Placeholders are escaped for JSON, so a value cannot
|
||||
end the string it sits in and add a field.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Ignored for GET. Placeholders are escaped for JSON, so a value cannot end the string it sits in and add a field.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="timeout">Timeout (seconds)</label>
|
||||
<label class="field__label" for="timeout">{{ t("Timeout (seconds)") }}</label>
|
||||
<input class="input" id="timeout" name="timeout" value="{{ tool.timeout }}" inputmode="numeric">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Credential</h2>
|
||||
<h2 class="card__title">{{ t("Credential") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="secret_placement">How it is sent</label>
|
||||
<label class="field__label" for="secret_placement">{{ t("How it is sent") }}</label>
|
||||
<select class="select" id="secret_placement" name="secret_placement">
|
||||
{% for value, label in secret_placements %}
|
||||
<option value="{{ value }}" {{ 'selected' if value == tool.secret_placement }}>
|
||||
@@ -128,15 +117,15 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="secret_name">Header or parameter name</label>
|
||||
<label class="field__label" for="secret_name">{{ t("Header or parameter name") }}</label>
|
||||
<input class="input input--mono" id="secret_name" name="secret_name"
|
||||
value="{{ tool.secret_name }}" maxlength="120">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="secret">Secret</label>
|
||||
<label class="field__label" for="secret">{{ t("Secret") }}</label>
|
||||
<input class="input input--mono" id="secret" name="secret" type="password"
|
||||
autocomplete="off" placeholder="No secret set"
|
||||
autocomplete="off" placeholder="{{ t('No secret set') }}"
|
||||
value="{{ unchanged if tool.secret_encrypted else '' }}">
|
||||
<p class="field__hint">
|
||||
{% if tool.secret_encrypted %}
|
||||
@@ -151,10 +140,10 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">The answer</h2>
|
||||
<h2 class="card__title">{{ t("The answer") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="response_mode">Read the response as</label>
|
||||
<label class="field__label" for="response_mode">{{ t("Read the response as") }}</label>
|
||||
<select class="select" id="response_mode" name="response_mode">
|
||||
{% for value, label in response_modes %}
|
||||
<option value="{{ value }}" {{ 'selected' if value == tool.response_mode }}>{{ label }}</option>
|
||||
@@ -163,32 +152,26 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="response_path">Path into the JSON</label>
|
||||
<label class="field__label" for="response_path">{{ t("Path into the JSON") }}</label>
|
||||
<input class="input input--mono" id="response_path" name="response_path"
|
||||
value="{{ tool.response_path }}" placeholder="data.items.0.title" maxlength="300">
|
||||
<p class="field__hint">
|
||||
Dotted; a number indexes a list. Leave empty for the whole document.
|
||||
A path that leads nowhere gives the whole document rather than nothing.
|
||||
</p>
|
||||
value="{{ tool.response_path }}" placeholder="{{ t('data.items.0.title') }}" maxlength="300">
|
||||
<p class="field__hint">{{ t("Dotted; a number indexes a list. Leave empty for the whole document. A path that leads nowhere gives the whole document rather than nothing.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="max_chars">Most characters to keep</label>
|
||||
<label class="field__label" for="max_chars">{{ t("Most characters to keep") }}</label>
|
||||
<input class="input" id="max_chars" name="max_chars" value="{{ tool.max_chars }}"
|
||||
inputmode="numeric">
|
||||
<p class="field__hint">
|
||||
Spent out of the context window on every call. The rest is cut off, and
|
||||
the model is told so.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Spent out of the context window on every call. The rest is cut off, and the model is told so.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Guidance</h2>
|
||||
<h2 class="card__title">{{ t("Guidance") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<textarea class="textarea" name="guidance" rows="4"
|
||||
placeholder="- Check the weather rather than guessing at it."
|
||||
placeholder="{{ t('- Check the weather rather than guessing at it.') }}"
|
||||
>{{ tool.guidance }}</textarea>
|
||||
<p class="field__hint">
|
||||
Added to the system message whenever this tool is offered, and nowhere
|
||||
@@ -205,40 +188,33 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Availability</h2>
|
||||
<h2 class="card__title">{{ t("Availability") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<div class="checkbox-row">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if tool.enabled }}>
|
||||
<span>Enabled — offered in chats</span>
|
||||
<span>{{ t("Enabled — offered in chats") }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="allow_private" value="true"
|
||||
{{ 'checked' if tool.allow_private }}>
|
||||
<span>May reach private and loopback addresses</span>
|
||||
<span>{{ t("May reach private and loopback addresses") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
Leave the second unticked unless this tool points at something on your
|
||||
own network. It is what stops a tool being aimed at this server, a
|
||||
router, or a cloud metadata endpoint.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Leave the second unticked unless this tool points at something on your own network. It is what stops a tool being aimed at this server, a router, or a cloud metadata endpoint.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="public" value="true" {{ 'checked' if tool.public }}>
|
||||
<span>Available to everyone</span>
|
||||
<span>{{ t("Available to everyone") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Uncheck to restrict this tool to chosen groups. Administrators always
|
||||
have access.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Uncheck to restrict this tool to chosen groups. Administrators always have access.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="field__label">Groups with access</span>
|
||||
<span class="field__label">{{ t("Groups with access") }}</span>
|
||||
{% if groups %}
|
||||
<div class="checkbox-row">
|
||||
{% for group in groups %}
|
||||
@@ -249,7 +225,7 @@
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="field__hint">Ignored while the tool is available to everyone.</p>
|
||||
<p class="field__hint">{{ t("Ignored while the tool is available to everyone.") }}</p>
|
||||
{% else %}
|
||||
<p class="field__hint">
|
||||
No groups yet — <a href="/admin/groups">create one</a> to restrict access.
|
||||
@@ -258,7 +234,7 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="position">Position</label>
|
||||
<label class="field__label" for="position">{{ t("Position") }}</label>
|
||||
<input class="input" id="position" name="position" value="{{ tool.position }}"
|
||||
inputmode="numeric">
|
||||
</div>
|
||||
@@ -272,23 +248,21 @@
|
||||
{% if not is_new %}
|
||||
<button class="btn btn--danger" type="submit" formnovalidate
|
||||
formaction="/admin/tools/{{ tool.id }}/delete"
|
||||
data-confirm-button="Delete the tool “{{ tool.name }}”? Chats that used it keep their transcripts.">
|
||||
Delete
|
||||
</button>
|
||||
data-confirm-button="Delete the tool “{{ tool.name }}”? Chats that used it keep their transcripts.">{{ t("Delete") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if not is_new %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Try it</h2>
|
||||
<h2 class="card__title">{{ t("Try it") }}</h2>
|
||||
<p class="field__hint">
|
||||
Calls the <em>saved</em> tool once, so what you see here is what a chat would
|
||||
Calls the <em>{{ t("saved") }}</em> tool once, so what you see here is what a chat would
|
||||
get. Nothing is sent to a model.
|
||||
</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="arguments">Arguments</label>
|
||||
<label class="field__label" for="arguments">{{ t("Arguments") }}</label>
|
||||
<textarea class="textarea input--mono" id="arguments" name="arguments" rows="3"
|
||||
spellcheck="false">{"city": "Minas Tirith"}</textarea>
|
||||
</div>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
{% block admin_content %}
|
||||
<p class="admin-lede">
|
||||
HTTP calls a model can make while it answers. Each one is offered to models
|
||||
marked <strong>Custom tools</strong>, to people who have the permission, and —
|
||||
marked <strong>{{ t("Custom tools") }}</strong>, to people who have the permission, and —
|
||||
if it is restricted — only to the groups you choose. The model decides
|
||||
<em>when</em> to call it from the description you write, so write that as if
|
||||
<em>{{ t("when") }}</em> to call it from the description you write, so write that as if
|
||||
explaining to a colleague what the tool is for.
|
||||
</p>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<form class="filter-form" method="get" action="/admin/tools">
|
||||
<input type="hidden" name="filter" value="{{ active_filter }}">
|
||||
<input class="input" type="search" name="q" value="{{ q }}"
|
||||
placeholder="Search tools…" aria-label="Search tools">
|
||||
placeholder="{{ t('Search tools…') }}" aria-label="{{ t('Search tools') }}">
|
||||
<button class="btn" type="submit">{{ icon("search", "icon--sm") }} Filter</button>
|
||||
{% if q or active_filter != "all" %}
|
||||
<a class="btn btn--ghost" href="/admin/tools">Clear</a>
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
{% if not tools %}
|
||||
<div class="empty" style="padding: var(--sp-8) 0">
|
||||
<p class="empty__text">Nothing matches that filter.</p>
|
||||
<p class="empty__text">{{ t("Nothing matches that filter.") }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -65,10 +65,10 @@
|
||||
<div class="model-row__main">
|
||||
<div class="model-row__title">
|
||||
<a class="model-row__name" href="/admin/tools/{{ tool.id }}/edit">{{ tool.name }}</a>
|
||||
{% if not tool.enabled %}<span class="badge badge--danger">disabled</span>{% endif %}
|
||||
{% if not tool.public %}<span class="badge">restricted</span>{% endif %}
|
||||
{% if tool.allow_private %}<span class="badge">private network</span>{% endif %}
|
||||
{% if tool.last_error %}<span class="badge badge--danger">last call failed</span>{% endif %}
|
||||
{% if not tool.enabled %}<span class="badge badge--danger">{{ t("disabled") }}</span>{% endif %}
|
||||
{% if not tool.public %}<span class="badge">{{ t("restricted") }}</span>{% endif %}
|
||||
{% if tool.allow_private %}<span class="badge">{{ t("private network") }}</span>{% endif %}
|
||||
{% if tool.last_error %}<span class="badge badge--danger">{{ t("last call failed") }}</span>{% endif %}
|
||||
</div>
|
||||
<code class="model-row__id">
|
||||
{{ tool.slug }} · {{ tool.method }} {{ tool.url_template }}
|
||||
@@ -94,13 +94,13 @@
|
||||
{% if page > 1 %}
|
||||
<a class="btn btn--sm" href="{{ base }}&page={{ page - 1 }}">Previous</a>
|
||||
{% else %}
|
||||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">Previous</span>
|
||||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">{{ t("Previous") }}</span>
|
||||
{% endif %}
|
||||
<span class="text-xs faint">Page {{ page }} of {{ pages }}</span>
|
||||
{% if page < pages %}
|
||||
<a class="btn btn--sm" href="{{ base }}&page={{ page + 1 }}">Next</a>
|
||||
{% else %}
|
||||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">Next</span>
|
||||
<span class="btn btn--sm" aria-disabled="true" style="opacity: .45">{{ t("Next") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
{% endif %}
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Running</h2>
|
||||
<h2 class="card__title">{{ t("Running") }}</h2>
|
||||
<dl class="mode-list">
|
||||
<div class="mode-list__row">
|
||||
<dt><strong>Version</strong></dt>
|
||||
<dt><strong>{{ t("Version") }}</strong></dt>
|
||||
<dd>
|
||||
{#
|
||||
`git describe`, not the version string: "1.0.0" exactly at a tag and
|
||||
@@ -42,16 +42,16 @@
|
||||
</div>
|
||||
{% if state.head %}
|
||||
<div class="mode-list__row">
|
||||
<dt><strong>Commit</strong></dt>
|
||||
<dt><strong>{{ t("Commit") }}</strong></dt>
|
||||
<dd>
|
||||
<code>{{ state.head.short }}</code> {{ state.head.subject }}
|
||||
{% if state.dirty %}<span class="badge">uncommitted changes</span>{% endif %}
|
||||
{% if state.dirty %}<span class="badge">{{ t("uncommitted changes") }}</span>{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if state.checkout %}
|
||||
<div class="mode-list__row">
|
||||
<dt><strong>Checkout</strong></dt>
|
||||
<dt><strong>{{ t("Checkout") }}</strong></dt>
|
||||
<dd><code>{{ state.checkout }}</code></dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -69,10 +69,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if not state.is_git %}
|
||||
<p class="field__hint">
|
||||
This was not installed from a git checkout — a container image, or a wheel —
|
||||
so there is nothing here to compare or update. Pull a new image instead.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("This was not installed from a git checkout — a container image, or a wheel — so there is nothing here to compare or update. Pull a new image instead.") }}</p>
|
||||
{% else %}
|
||||
<form method="post" action="/admin/updates/check" class="btn-row">
|
||||
<button class="btn" type="submit">
|
||||
@@ -86,8 +83,8 @@
|
||||
<section class="card">
|
||||
<h2 class="card__title">
|
||||
Available
|
||||
{% if state.no_releases %}<span class="badge">nothing released yet</span>
|
||||
{% elif state.up_to_date %}<span class="badge badge--leaf">up to date</span>
|
||||
{% if state.no_releases %}<span class="badge">{{ t("nothing released yet") }}</span>
|
||||
{% elif state.up_to_date %}<span class="badge badge--leaf">{{ t("up to date") }}</span>
|
||||
{% elif state.behind %}<span class="badge">{{ state.behind | length }} commit{{ '' if state.behind|length == 1 else 's' }} behind</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
@@ -100,9 +97,7 @@
|
||||
</p>
|
||||
|
||||
{% elif not state.available %}
|
||||
<p class="field__hint">
|
||||
Nothing known about the remote yet. Check it above.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Nothing known about the remote yet. Check it above.") }}</p>
|
||||
|
||||
{% elif state.up_to_date %}
|
||||
<p class="field__hint">
|
||||
@@ -149,11 +144,7 @@
|
||||
{% if state.dirty %}
|
||||
<div class="alert alert--warning">
|
||||
{{ icon("warning", "icon--sm") }}
|
||||
<span>
|
||||
This checkout has uncommitted changes, and updating discards them.
|
||||
Nothing here is meant to be edited in place, so this usually means somebody
|
||||
was debugging on the box.
|
||||
</span>
|
||||
<span>{{ t("This checkout has uncommitted changes, and updating discards them. Nothing here is meant to be edited in place, so this usually means somebody was debugging on the box.") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -166,18 +157,15 @@
|
||||
the worst moment to discover a thing has to be installed from a shell.
|
||||
#}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Applying an update</h2>
|
||||
<h2 class="card__title">{{ t("Applying an update") }}</h2>
|
||||
|
||||
{% if state.requested %}
|
||||
<div class="alert">
|
||||
{{ icon("clock", "icon--sm") }}
|
||||
<span>
|
||||
An update has been requested and is waiting for the helper to pick it up.
|
||||
The service restarts when it does.
|
||||
</span>
|
||||
<span>{{ t("An update has been requested and is waiting for the helper to pick it up. The service restarts when it does.") }}</span>
|
||||
</div>
|
||||
<form method="post" action="/admin/updates/cancel" class="btn-row">
|
||||
<button class="btn btn--sm" type="submit">Withdraw the request</button>
|
||||
<button class="btn btn--sm" type="submit">{{ t("Withdraw the request") }}</button>
|
||||
</form>
|
||||
|
||||
{% elif state.helper %}
|
||||
@@ -207,7 +195,7 @@
|
||||
no channel, so pressing it is never “deploy something else”.
|
||||
</p>
|
||||
<form method="post" action="/admin/updates/apply" class="btn-row"
|
||||
data-confirm="Update and restart? Replies being written are saved; open terminals are cut off.">
|
||||
data-confirm="{{ t('Update and restart? Replies being written are saved; open terminals are cut off.') }}">
|
||||
<button class="btn btn--primary" type="submit"
|
||||
{{ 'disabled' if state.up_to_date or state.no_releases }}>
|
||||
{{ icon("sparkle", "icon--sm") }}
|
||||
@@ -216,19 +204,16 @@
|
||||
{% else %}Update to {{ state.available.label }} and restart{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<p class="field__hint">
|
||||
Every restart ends every open terminal session — a command still running on
|
||||
the far side is cut off. A reply being written is saved with whatever it has.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Every restart ends every open terminal session — a command still running on the far side is cut off. A reply being written is saved with whatever it has.") }}</p>
|
||||
|
||||
{% else %}
|
||||
<div class="alert">
|
||||
{{ icon("shield", "icon--sm") }}
|
||||
<span>
|
||||
The update helper is <strong>not installed on this host</strong>, so there
|
||||
The update helper is <strong>{{ t("not installed on this host") }}</strong>, so there
|
||||
is no button. That is the honest default: the service runs as an
|
||||
unprivileged account and cannot restart itself, and a web application that
|
||||
<em>can</em> is one whose worst day is much worse. Run this instead:
|
||||
<em>{{ t("can") }}</em> is one whose worst day is much worse. Run this instead:
|
||||
<code>{{ command }}</code>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -17,49 +17,44 @@
|
||||
|
||||
<form method="post" action="/admin/users/{{ target.id }}" class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Account</h2>
|
||||
<h2 class="card__title">{{ t("Account") }}</h2>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input" id="name" name="name" value="{{ target.name }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="role">Role</label>
|
||||
<label class="field__label" for="role">{{ t("Role") }}</label>
|
||||
<select class="input" id="role" name="role">
|
||||
{% for role in roles %}
|
||||
<option value="{{ role }}" {{ 'selected' if target.role == role }}>{{ role }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="field__hint">
|
||||
An administrator bypasses every permission and every quota below.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("An administrator bypasses every permission and every quota below.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="active" value="true" {{ 'checked' if target.active }}>
|
||||
<span>Active</span>
|
||||
<span>{{ t("Active") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Turning this off signs them out everywhere at once, rather than waiting
|
||||
for a cookie to expire.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Turning this off signs them out everywhere at once, rather than waiting for a cookie to expire.") }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{# --- What they can actually do -------------------------------------------- #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">What this account can do</h2>
|
||||
<h2 class="card__title">{{ t("What this account can do") }}</h2>
|
||||
<p class="card__lede">
|
||||
Read-only, and deliberately: every switch here is set somewhere else — in the
|
||||
<a href="/admin/groups">baseline</a> or in a named group — and a control on
|
||||
this page would be a third place to change one thing. What it adds is the
|
||||
<em>source</em>, which is the question the grids could not answer without
|
||||
<em>{{ t("source") }}</em>, which is the question the grids could not answer without
|
||||
opening every group by eye.
|
||||
</p>
|
||||
|
||||
@@ -77,7 +72,7 @@
|
||||
from {{ state.source | join(", ") }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="perm-row__desc faint">not granted</span>
|
||||
<span class="perm-row__desc faint">{{ t("not granted") }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
@@ -88,11 +83,8 @@
|
||||
|
||||
{# --- Membership ----------------------------------------------------------- #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Groups</h2>
|
||||
<p class="card__lede">
|
||||
Edited from the group's own page. One control per value, so a save here
|
||||
cannot undo a save there.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Groups") }}</h2>
|
||||
<p class="card__lede">{{ t("Edited from the group's own page. One control per value, so a save here cannot undo a save there.") }}</p>
|
||||
{% if target.groups %}
|
||||
<div class="btn-row">
|
||||
{% for group in target.groups %}
|
||||
@@ -100,21 +92,17 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted text-sm">In no group. They get the baseline and nothing more.</p>
|
||||
<p class="muted text-sm">{{ t("In no group. They get the baseline and nothing more.") }}</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{# --- Quotas and usage ----------------------------------------------------- #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">This month</h2>
|
||||
<p class="card__lede">
|
||||
Counted from the first of the month, UTC. Recorded for every reply including
|
||||
one that was stopped or failed — an endpoint charges for tokens it generated
|
||||
whether or not anybody wanted them.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("This month") }}</h2>
|
||||
<p class="card__lede">{{ t("Counted from the first of the month, UTC. Recorded for every reply including one that was stopped or failed — an endpoint charges for tokens it generated whether or not anybody wanted them.") }}</p>
|
||||
<dl class="mode-list">
|
||||
<div class="mode-list__row">
|
||||
<dt><strong>Tokens</strong></dt>
|
||||
<dt><strong>{{ t("Tokens") }}</strong></dt>
|
||||
<dd>
|
||||
{{ "{:,}".format(usage.tokens) }}
|
||||
{% if limits.monthly_tokens %} of {{ "{:,}".format(limits.monthly_tokens) }}{% endif %}
|
||||
@@ -125,11 +113,11 @@
|
||||
</dd>
|
||||
</div>
|
||||
<div class="mode-list__row">
|
||||
<dt><strong>Replies</strong></dt>
|
||||
<dt><strong>{{ t("Replies") }}</strong></dt>
|
||||
<dd>{{ usage.replies }}</dd>
|
||||
</div>
|
||||
<div class="mode-list__row">
|
||||
<dt><strong>Images</strong></dt>
|
||||
<dt><strong>{{ t("Images") }}</strong></dt>
|
||||
<dd>
|
||||
{{ usage.images }} this month, {{ usage.images_today }} today
|
||||
{% if limits.images_per_day %} (limit {{ limits.images_per_day }} a day){% endif %}
|
||||
@@ -137,9 +125,9 @@
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<h3 class="section-title">Limits in force</h3>
|
||||
<h3 class="section-title">{{ t("Limits in force") }}</h3>
|
||||
<p class="field__hint">
|
||||
Resolved across their groups by <strong>maximum</strong> — the union rule
|
||||
Resolved across their groups by <strong>{{ t("maximum") }}</strong> — the union rule
|
||||
applied to numbers, so a second group can only ever grant more. Zero means no
|
||||
limit and wins outright, because a group saying “unlimited” must not count
|
||||
for less than one saying “a million”.
|
||||
@@ -159,11 +147,8 @@
|
||||
|
||||
{# --- Models --------------------------------------------------------------- #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Models they can use</h2>
|
||||
<p class="card__lede">
|
||||
Model access is separate from permissions: a permission says what somebody
|
||||
may do, this says what they may do it with.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Models they can use") }}</h2>
|
||||
<p class="card__lede">{{ t("Model access is separate from permissions: a permission says what somebody may do, this says what they may do it with.") }}</p>
|
||||
{% if models %}
|
||||
<div class="btn-row">
|
||||
{% for model in models %}
|
||||
@@ -171,22 +156,19 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted text-sm">None. They cannot start a chat at all.</p>
|
||||
<p class="muted text-sm">{{ t("None. They cannot start a chat at all.") }}</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{# --- Dangerous ------------------------------------------------------------ #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Password and removal</h2>
|
||||
<h2 class="card__title">{{ t("Password and removal") }}</h2>
|
||||
<form method="post" action="/admin/users/{{ target.id }}/password" class="btn-row">
|
||||
<input class="input" name="password" type="password" required
|
||||
placeholder="New password" aria-label="New password" style="flex: 1">
|
||||
<button class="btn" type="submit">Reset password</button>
|
||||
placeholder="{{ t('New password') }}" aria-label="{{ t('New password') }}" style="flex: 1">
|
||||
<button class="btn" type="submit">{{ t("Reset password") }}</button>
|
||||
</form>
|
||||
<p class="field__hint">
|
||||
Signs them out everywhere. An administrator resetting a password usually
|
||||
means the account is compromised or the person has gone.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Signs them out everywhere. An administrator resetting a password usually means the account is compromised or the person has gone.") }}</p>
|
||||
|
||||
<form method="post" action="/admin/users/{{ target.id }}/delete"
|
||||
data-confirm="Delete {{ target.email }}? Their chats, folders and library go with them."
|
||||
@@ -195,9 +177,6 @@
|
||||
{{ icon("trash", "icon--sm") }} Delete this account
|
||||
</button>
|
||||
</form>
|
||||
<p class="field__hint">
|
||||
Their chats, folders and library go too, and every share naming them or
|
||||
naming anything of theirs.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Their chats, folders and library go too, and every share naming them or naming anything of theirs.") }}</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="filter-bar">
|
||||
<form class="filter-form" method="get" action="/admin/users">
|
||||
<input class="input" type="search" name="q" value="{{ q }}"
|
||||
placeholder="Search by name or email…" aria-label="Search users">
|
||||
placeholder="{{ t('Search by name or email…') }}" aria-label="{{ t('Search users') }}">
|
||||
<button class="btn" type="submit">{{ icon("search", "icon--sm") }} Filter</button>
|
||||
{% if q %}<a class="btn btn--ghost" href="/admin/users">Clear</a>{% endif %}
|
||||
</form>
|
||||
@@ -32,9 +32,9 @@
|
||||
<div class="model-row__main">
|
||||
<span class="model-row__name">
|
||||
{{ person.name }}
|
||||
{% if person.role == "admin" %}<span class="badge badge--leaf">admin</span>{% endif %}
|
||||
{% if person.role == "pending" %}<span class="badge">pending</span>{% endif %}
|
||||
{% if not person.active %}<span class="badge">disabled</span>{% endif %}
|
||||
{% if person.role == "admin" %}<span class="badge badge--leaf">{{ t("admin") }}</span>{% endif %}
|
||||
{% if person.role == "pending" %}<span class="badge">{{ t("pending") }}</span>{% endif %}
|
||||
{% if not person.active %}<span class="badge">{{ t("disabled") }}</span>{% endif %}
|
||||
</span>
|
||||
<span class="model-row__id">{{ person.email }}</span>
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@
|
||||
{% endif %}
|
||||
{% set spent = usage[person.id] %}
|
||||
{% if spent.tokens %}
|
||||
<span class="badge" title="Tokens this month">{{ "{:,}".format(spent.tokens) }}</span>
|
||||
<span class="badge" title="{{ t('Tokens this month') }}">{{ "{:,}".format(spent.tokens) }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
@@ -74,26 +74,24 @@
|
||||
{% endif %}
|
||||
|
||||
<section class="card" style="margin-top: var(--sp-8)">
|
||||
<h2 class="card__title">Add an account</h2>
|
||||
<p class="card__lede">
|
||||
Without going through registration — useful when sign-up is closed.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Add an account") }}</h2>
|
||||
<p class="card__lede">{{ t("Without going through registration — useful when sign-up is closed.") }}</p>
|
||||
<form method="post" action="/admin/users" class="form-grid">
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-name">Name</label>
|
||||
<label class="field__label" for="new-name">{{ t("Name") }}</label>
|
||||
<input class="input" id="new-name" name="name" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-email">Email</label>
|
||||
<label class="field__label" for="new-email">{{ t("Email") }}</label>
|
||||
<input class="input" id="new-email" name="email" type="email" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-password">Password</label>
|
||||
<label class="field__label" for="new-password">{{ t("Password") }}</label>
|
||||
<input class="input" id="new-password" name="password" type="password" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="new-role">Role</label>
|
||||
<label class="field__label" for="new-role">{{ t("Role") }}</label>
|
||||
<select class="input" id="new-role" name="role">
|
||||
{% for role in roles %}<option value="{{ role }}">{{ role }}</option>{% endfor %}
|
||||
</select>
|
||||
|
||||
@@ -20,56 +20,53 @@
|
||||
action="{{ '/admin/images/workflows' if is_new else '/admin/images/workflows/' ~ workflow.id }}"
|
||||
class="form-grid">
|
||||
<section class="card">
|
||||
<h2 class="card__title">What it is</h2>
|
||||
<h2 class="card__title">{{ t("What it is") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input" id="name" name="name" required maxlength="120"
|
||||
value="{{ workflow.name }}" placeholder="SDXL, photographic">
|
||||
<p class="field__hint">Shown to you, in the list.</p>
|
||||
value="{{ workflow.name }}" placeholder="{{ t('SDXL, photographic') }}">
|
||||
<p class="field__hint">{{ t("Shown to you, in the list.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="slug">Name the model uses</label>
|
||||
<label class="field__label" for="slug">{{ t("Name the model uses") }}</label>
|
||||
<input class="input input--mono" id="slug" name="slug" required maxlength="48"
|
||||
value="{{ workflow.slug }}" placeholder="sdxl-photo">
|
||||
<p class="field__hint">
|
||||
Lowercase letters, digits, hyphens and underscores. This is what the model
|
||||
writes when it picks this workflow.
|
||||
</p>
|
||||
value="{{ workflow.slug }}" placeholder="{{ t('sdxl-photo') }}">
|
||||
<p class="field__hint">{{ t("Lowercase letters, digits, hyphens and underscores. This is what the model writes when it picks this workflow.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="description">Description</label>
|
||||
<label class="field__label" for="description">{{ t("Description") }}</label>
|
||||
<textarea class="textarea" id="description" name="description" rows="3"
|
||||
placeholder="Photographic and slow. Best for people, interiors and product shots at 1024px.">{{ workflow.description }}</textarea>
|
||||
placeholder="{{ t('Photographic and slow. Best for people, interiors and product shots at 1024px.') }}">{{ workflow.description }}</textarea>
|
||||
<p class="field__hint">
|
||||
The only thing the model has to choose with, so say what this is
|
||||
<em>for</em> rather than what it contains. It never sees the graph.
|
||||
<em>{{ t("for") }}</em> rather than what it contains. It never sees the graph.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if workflow.enabled }}>
|
||||
<span>Offer this workflow</span>
|
||||
<span>{{ t("Offer this workflow") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">The workflow</h2>
|
||||
<h2 class="card__title">{{ t("The workflow") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="workflow">ComfyUI API format</label>
|
||||
<label class="field__label" for="workflow">{{ t("ComfyUI API format") }}</label>
|
||||
<textarea class="textarea input--mono" id="workflow" name="workflow" rows="20"
|
||||
spellcheck="false">{{ workflow_text }}</textarea>
|
||||
<p class="field__hint">
|
||||
Export this from ComfyUI with <strong>Export (API)</strong>, not Save — the
|
||||
Export this from ComfyUI with <strong>{{ t("Export (API)") }}</strong>, not Save — the
|
||||
two formats are different and only the API one can be submitted.
|
||||
</p>
|
||||
<p class="field__hint">
|
||||
Then put a placeholder where each value goes. A placeholder that is the
|
||||
<em>whole</em> value keeps its type, so
|
||||
<em>{{ t("whole") }}</em> value keeps its type, so
|
||||
<code>"steps": {{ '{{steps}}' }}</code> sends the number 20 rather than the
|
||||
text “20”; one inside a longer string is substituted as text, so
|
||||
<code>"{{ '{{prompt}}' }}, masterpiece"</code> works. Anything you leave out
|
||||
@@ -79,7 +76,7 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">The placeholders</h2>
|
||||
<h2 class="card__title">{{ t("The placeholders") }}</h2>
|
||||
<p class="card__lede">
|
||||
Every hole a template may carry, what it fills, and what it resolves to
|
||||
right now. <code>{{ '{{prompt}}' }}</code> is required — without it every
|
||||
@@ -95,7 +92,7 @@
|
||||
<code>{{ '{{' ~ name ~ '}}' }}</code>
|
||||
<span>
|
||||
{{ what }}
|
||||
{% if name == "prompt" %}<strong>Required.</strong>{% endif %}
|
||||
{% if name == "prompt" %}<strong>{{ t("Required.") }}</strong>{% endif %}
|
||||
</span>
|
||||
<code class="faint">{{ current }}</code>
|
||||
</div>
|
||||
@@ -123,9 +120,7 @@
|
||||
{% if not is_new %}
|
||||
<button class="btn btn--danger" type="submit" formnovalidate
|
||||
formaction="/admin/images/workflows/{{ workflow.id }}/delete"
|
||||
data-confirm-button="Delete the workflow “{{ workflow.name }}”? Chats that used it fall back to the default.">
|
||||
Delete
|
||||
</button>
|
||||
data-confirm-button="Delete the workflow “{{ workflow.name }}”? Chats that used it fall back to the default.">{{ t("Delete") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<button class="picker__option" type="button" data-dir-open="{{ parent }}">
|
||||
{{ icon("chevron-left", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Up a level</span>
|
||||
<span class="picker__option-name">{{ t("Up a level") }}</span>
|
||||
<span class="picker__option-note mono">{{ parent }}</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -70,9 +70,7 @@
|
||||
|
||||
{% if not entries %}
|
||||
<li>
|
||||
<p class="muted text-sm" style="padding: var(--sp-3)">
|
||||
Nothing here.
|
||||
</p>
|
||||
<p class="muted text-sm" style="padding: var(--sp-3)">{{ t("Nothing here.") }}</p>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
@@ -33,16 +33,14 @@
|
||||
hx-post="/api/agents/{{ profile.id }}/accept"
|
||||
hx-target="#check-result"
|
||||
hx-confirm="Accept this fingerprint and pin it? Every future connection will be checked against it."
|
||||
data-confirm-label="Accept"
|
||||
data-confirm-label="{{ t('Accept') }}"
|
||||
data-confirm-danger="{{ 'true' if offer.changed else 'false' }}">
|
||||
{{ icon("check", "icon--sm") }} Accept and pin
|
||||
</button>
|
||||
{% if profile.verified %}
|
||||
<button class="btn" type="button"
|
||||
hx-post="/api/agents/{{ profile.id }}/forget"
|
||||
hx-target="#check-result">
|
||||
Forget the old key
|
||||
</button>
|
||||
hx-target="#check-result">{{ t("Forget the old key") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +56,7 @@
|
||||
{% if forgotten %}
|
||||
<div class="alert">
|
||||
{{ icon("warning", "icon--sm") }}
|
||||
<span>Key forgotten. Check again to see the new one.</span>
|
||||
<span>{{ t("Key forgotten. Check again to see the new one.") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -67,6 +65,6 @@
|
||||
{{ icon("check", "icon--sm") }}
|
||||
<span>Connected{% if found.system %} to {{ found.system }}{% endif %}.</span>
|
||||
</div>
|
||||
<p class="field__hint">What it said:</p>
|
||||
<p class="field__hint">{{ t("What it said:") }}</p>
|
||||
<pre class="tool-result__text">{{ found.output }}</pre>
|
||||
{% endif %}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
{% if not is_new %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Check it</h2>
|
||||
<h2 class="card__title">{{ t("Check it") }}</h2>
|
||||
<p class="field__hint">
|
||||
{% if profile.verified %}
|
||||
Confirms the host is still the one you accepted, then logs in and runs
|
||||
@@ -63,70 +63,66 @@
|
||||
action="{{ '/api/agents' if is_new else '/api/agents/' ~ profile.id }}">
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">The machine</h2>
|
||||
<h2 class="card__title">{{ t("The machine") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input" id="name" name="name" value="{{ profile.name }}" required
|
||||
maxlength="120" placeholder="Project container">
|
||||
<p class="field__hint">What you will pick from when starting an agent chat.</p>
|
||||
maxlength="120" placeholder="{{ t('Project container') }}">
|
||||
<p class="field__hint">{{ t("What you will pick from when starting an agent chat.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="host">Host</label>
|
||||
<label class="field__label" for="host">{{ t("Host") }}</label>
|
||||
<input class="input input--mono" id="host" name="host" value="{{ profile.host }}" required
|
||||
maxlength="255" placeholder="127.0.0.1">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="port">Port</label>
|
||||
<label class="field__label" for="port">{{ t("Port") }}</label>
|
||||
<input class="input" id="port" name="port" value="{{ profile.port }}" inputmode="numeric">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="username">Log in as</label>
|
||||
<label class="field__label" for="username">{{ t("Log in as") }}</label>
|
||||
<input class="input input--mono" id="username" name="username" required
|
||||
value="{{ profile.username }}" maxlength="120" placeholder="root">
|
||||
value="{{ profile.username }}" maxlength="120" placeholder="{{ t('root') }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="default_dir">Project directory</label>
|
||||
<label class="field__label" for="default_dir">{{ t("Project directory") }}</label>
|
||||
<input class="input input--mono" id="default_dir" name="default_dir"
|
||||
value="{{ profile.default_dir }}" maxlength="500" placeholder="/project">
|
||||
<p class="field__hint">
|
||||
Where a chat starts by default. Each chat records its own when it is
|
||||
created, so changing this later does not move a conversation already
|
||||
under way.
|
||||
</p>
|
||||
value="{{ profile.default_dir }}" maxlength="500" placeholder="{{ t('/project') }}">
|
||||
<p class="field__hint">{{ t("Where a chat starts by default. Each chat records its own when it is created, so changing this later does not move a conversation already under way.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="connect_timeout">Connect timeout (seconds)</label>
|
||||
<label class="field__label" for="connect_timeout">{{ t("Connect timeout (seconds)") }}</label>
|
||||
<input class="input" id="connect_timeout" name="connect_timeout"
|
||||
value="{{ profile.connect_timeout }}" inputmode="numeric">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">How it logs in</h2>
|
||||
<h2 class="card__title">{{ t("How it logs in") }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<div class="checkbox-row">
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="auth" value="key"
|
||||
{{ 'checked' if profile.auth != 'password' }}>
|
||||
<span>A private key</span>
|
||||
<span>{{ t("A private key") }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="auth" value="password"
|
||||
{{ 'checked' if profile.auth == 'password' }}>
|
||||
<span>A password</span>
|
||||
<span>{{ t("A password") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="private_key">Private key</label>
|
||||
<label class="field__label" for="private_key">{{ t("Private key") }}</label>
|
||||
<textarea class="textarea input--mono" id="private_key" name="private_key" rows="5"
|
||||
spellcheck="false"
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
@@ -142,15 +138,15 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="key_passphrase">Key passphrase</label>
|
||||
<label class="field__label" for="key_passphrase">{{ t("Key passphrase") }}</label>
|
||||
<input class="input input--mono" id="key_passphrase" name="key_passphrase"
|
||||
type="password" autocomplete="off" placeholder="If the key has one">
|
||||
type="password" autocomplete="off" placeholder="{{ t('If the key has one') }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="password">Password</label>
|
||||
<label class="field__label" for="password">{{ t("Password") }}</label>
|
||||
<input class="input input--mono" id="password" name="password" type="password"
|
||||
autocomplete="off" placeholder="No password set"
|
||||
autocomplete="off" placeholder="{{ t('No password set') }}"
|
||||
value="{{ unchanged if profile.password_encrypted else '' }}">
|
||||
<p class="field__hint">
|
||||
{% if profile.password_encrypted %}
|
||||
@@ -164,11 +160,11 @@
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="card__title">Availability</h2>
|
||||
<h2 class="card__title">{{ t("Availability") }}</h2>
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if profile.enabled }}>
|
||||
<span>Enabled — can be picked when starting an agent chat</span>
|
||||
<span>{{ t("Enabled — can be picked when starting an agent chat") }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
@@ -181,9 +177,7 @@
|
||||
{% if not is_new %}
|
||||
<button class="btn btn--danger" type="submit" formnovalidate
|
||||
formaction="/api/agents/{{ profile.id }}/delete"
|
||||
data-confirm-button="Delete the connection “{{ profile.name }}”? Chats that used it keep their transcripts.">
|
||||
Delete
|
||||
</button>
|
||||
data-confirm-button="Delete the connection “{{ profile.name }}”? Chats that used it keep their transcripts.">{{ t("Delete") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
{% block agents_content %}
|
||||
<p class="admin-lede">
|
||||
Machines an <strong>Agent</strong> chat can work on. A model with one of these
|
||||
can read files, write files and run commands <em>there</em> — never here.
|
||||
Machines an <strong>{{ t("Agent") }}</strong> chat can work on. A model with one of these
|
||||
can read files, write files and run commands <em>{{ t("there") }}</em> — never here.
|
||||
Which of those it may do without asking you first is the chat's mode.
|
||||
</p>
|
||||
|
||||
@@ -33,11 +33,7 @@
|
||||
{% elif not enabled %}
|
||||
<div class="alert alert--error">
|
||||
{{ icon("warning", "icon--sm") }}
|
||||
<span>
|
||||
Agent chats are switched off for this instance. You can still add
|
||||
connections here, but nothing will use them until an administrator turns
|
||||
them on.
|
||||
</span>
|
||||
<span>{{ t("Agent chats are switched off for this instance. You can still add connections here, but nothing will use them until an administrator turns them on.") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -48,10 +44,10 @@
|
||||
{% if not profiles %}
|
||||
<div class="empty">
|
||||
{{ icon("server", "empty__mark") }}
|
||||
<h2 class="empty__title">No connections yet</h2>
|
||||
<h2 class="empty__title">{{ t("No connections yet") }}</h2>
|
||||
<p class="empty__text">
|
||||
Add the host, the user to log in as, and a key or password. Then press
|
||||
<strong>Check</strong> — you will be shown its fingerprint to confirm before
|
||||
<strong>{{ t("Check") }}</strong> — you will be shown its fingerprint to confirm before
|
||||
anything is sent to it.
|
||||
</p>
|
||||
<a class="btn btn--primary" href="/agents/new">
|
||||
@@ -72,14 +68,14 @@
|
||||
<div class="model-row__title">
|
||||
<a class="model-row__name" href="/agents/{{ profile.id }}">{{ profile.name }}</a>
|
||||
{% if refused %}
|
||||
<span class="badge badge--danger">not allowed</span>
|
||||
<span class="badge badge--danger">{{ t("not allowed") }}</span>
|
||||
{% elif profile.verified %}
|
||||
<span class="badge badge--leaf">key confirmed</span>
|
||||
<span class="badge badge--leaf">{{ t("key confirmed") }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge--danger">not checked</span>
|
||||
<span class="badge badge--danger">{{ t("not checked") }}</span>
|
||||
{% endif %}
|
||||
{% if not profile.enabled %}<span class="badge">disabled</span>{% endif %}
|
||||
{% if profile.auth == "password" %}<span class="badge">password</span>{% endif %}
|
||||
{% if not profile.enabled %}<span class="badge">{{ t("disabled") }}</span>{% endif %}
|
||||
{% if profile.auth == "password" %}<span class="badge">{{ t("password") }}</span>{% endif %}
|
||||
</div>
|
||||
<code class="model-row__id">
|
||||
{{ profile.address }}{% if profile.default_dir %} · {{ profile.default_dir }}{% endif %}
|
||||
|
||||
@@ -23,18 +23,18 @@
|
||||
<input type="hidden" name="next" value="{{ next|default('/') }}">
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="email">Email</label>
|
||||
<label class="field__label" for="email">{{ t("Email") }}</label>
|
||||
<input class="input" type="email" id="email" name="email" required
|
||||
autocomplete="username" autofocus value="{{ email|default('') }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="password">Password</label>
|
||||
<label class="field__label" for="password">{{ t("Password") }}</label>
|
||||
<input class="input" type="password" id="password" name="password" required
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button class="btn btn--primary btn--block" type="submit">Sign in</button>
|
||||
<button class="btn btn--primary btn--block" type="submit">{{ t("Sign in") }}</button>
|
||||
</form>
|
||||
|
||||
{% if allow_signup %}
|
||||
|
||||
@@ -10,11 +10,9 @@
|
||||
{{ mark(cls="brand-mark", uid="auth") }}
|
||||
<h1 class="auth__title">{{ wordmark() }}</h1>
|
||||
{% if first_run %}
|
||||
<p class="auth__subtitle">
|
||||
Nobody has claimed this instance yet. The first account becomes its administrator.
|
||||
</p>
|
||||
<p class="auth__subtitle">{{ t("Nobody has claimed this instance yet. The first account becomes its administrator.") }}</p>
|
||||
{% else %}
|
||||
<p class="auth__subtitle">Create your account.</p>
|
||||
<p class="auth__subtitle">{{ t("Create your account.") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -27,22 +25,22 @@
|
||||
|
||||
<form method="post" action="/auth/register" class="stack">
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input" type="text" id="name" name="name" required autofocus
|
||||
autocomplete="name" value="{{ name|default('') }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="email">Email</label>
|
||||
<label class="field__label" for="email">{{ t("Email") }}</label>
|
||||
<input class="input" type="email" id="email" name="email" required
|
||||
autocomplete="username" value="{{ email|default('') }}">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="password">Password</label>
|
||||
<label class="field__label" for="password">{{ t("Password") }}</label>
|
||||
<input class="input" type="password" id="password" name="password" required
|
||||
autocomplete="new-password" minlength="8">
|
||||
<p class="field__hint">At least 8 characters.</p>
|
||||
<p class="field__hint">{{ t("At least 8 characters.") }}</p>
|
||||
</div>
|
||||
|
||||
<button class="btn btn--primary btn--block" type="submit">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
in order to set both attributes when somebody switches -- one attribute to
|
||||
split rather than a JSON island to parse.
|
||||
#}
|
||||
<html lang="en" data-theme="{{ theme }}" data-base="{{ brand.theme(theme).base }}"
|
||||
<html lang="{{ language() }}" dir="{{ text_direction() }}" data-theme="{{ theme }}" data-base="{{ brand.theme(theme).base }}"
|
||||
data-themes="{{ brand.theme_list }}"{% if layout %} style="{{ layout }}"{% endif %}>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<span class="attach-chip__name">{{ filename }}</span>
|
||||
<span class="attach-chip__warning">{{ error }}</span>
|
||||
</span>
|
||||
<button class="btn btn--icon btn--sm" type="button" aria-label="Dismiss"
|
||||
<button class="btn btn--icon btn--sm" type="button" aria-label="{{ t('Dismiss') }}"
|
||||
onclick="this.closest('.attach-chip').remove()">
|
||||
{{ icon("x", "icon--sm") }}
|
||||
</button>
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
<span class="attach-chip__icon">{{ icon("archive", "icon--sm") }}</span>
|
||||
<span class="attach-chip__body">
|
||||
<span class="attach-chip__name" title="{{ base.name }}">{{ base.name }}</span>
|
||||
<span class="attach-chip__meta">This chat now searches only the bases it is attached to</span>
|
||||
<span class="attach-chip__meta">{{ t("This chat now searches only the bases it is attached to") }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
path allowed to emit HTML), or into a <textarea>, whose contents Jinja escapes
|
||||
and which cannot contain markup by construction.
|
||||
#}
|
||||
<aside class="canvas" id="canvas" hidden aria-label="Canvas"
|
||||
<aside class="canvas" id="canvas" hidden aria-label="{{ t('Canvas') }}"
|
||||
data-canvas
|
||||
data-chat="{{ chat.id if chat else "" }}"
|
||||
data-resize-target>
|
||||
@@ -23,7 +23,7 @@
|
||||
mouse and the grip is a focus trap that does nothing. #}
|
||||
<div class="panel-resize" data-resize="--canvas-width" data-resize-min="384"
|
||||
role="separator" aria-orientation="vertical" tabindex="0"
|
||||
aria-label="Resize the canvas">
|
||||
aria-label="{{ t('Resize the canvas') }}">
|
||||
{{ icon("grip", "icon--sm") }}
|
||||
</div>
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
<div class="panel-head">
|
||||
<h2 class="panel-head__title">
|
||||
{{ icon("file-text", "icon--sm") }}
|
||||
<span>Canvas</span>
|
||||
<span>{{ t("Canvas") }}</span>
|
||||
</h2>
|
||||
<button class="btn btn--icon btn--sm" type="button" data-toggle="#canvas"
|
||||
aria-label="Close canvas">
|
||||
aria-label="{{ t('Close canvas') }}">
|
||||
{{ icon("x", "icon--sm") }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="canvas__body">
|
||||
<p class="canvas__empty">Opening…</p>
|
||||
<p class="canvas__empty">{{ t("Opening…") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<details class="canvas__theirs">
|
||||
<summary>What is there now</summary>
|
||||
<summary>{{ t("What is there now") }}</summary>
|
||||
<pre class="canvas__code"><code>{{ conflict.text }}</code></pre>
|
||||
</details>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
regardless, which is exactly what Overwrite means -- somebody has now
|
||||
been shown both versions and chosen. #}
|
||||
<input type="hidden" name="revision" value="">
|
||||
<label class="visually-hidden" for="canvas-mine">Your version</label>
|
||||
<label class="visually-hidden" for="canvas-mine">{{ t("Your version") }}</label>
|
||||
<textarea class="canvas__editor" id="canvas-mine" name="text"
|
||||
spellcheck="false" data-canvas-editor>{{ mine }}</textarea>
|
||||
<div class="canvas__actions">
|
||||
@@ -44,9 +44,7 @@
|
||||
<button class="btn btn--sm" type="button"
|
||||
hx-post="/api/chats/{{ chat.id }}/canvas/tabs"
|
||||
hx-vals='{"key": {{ conflict.key | tojson }}}'
|
||||
hx-target="#canvas-inner" hx-swap="innerHTML">
|
||||
Discard mine and reload
|
||||
</button>
|
||||
hx-target="#canvas-inner" hx-swap="innerHTML">{{ t("Discard mine and reload") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
{% if rendered %}
|
||||
{{ rendered | safe }}
|
||||
{% else %}
|
||||
<p class="canvas__empty">This file is empty.</p>
|
||||
<p class="canvas__empty">{{ t("This file is empty.") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
<button class="btn btn--primary btn--sm" type="submit">
|
||||
{{ icon("check", "icon--sm") }} Save
|
||||
</button>
|
||||
<button class="btn btn--sm" type="button" @click="editing = false">Cancel</button>
|
||||
<span class="canvas__hint">No colour while you type. Tab inserts a tab.</span>
|
||||
<button class="btn btn--sm" type="button" @click="editing = false">{{ t("Cancel") }}</button>
|
||||
<span class="canvas__hint">{{ t("No colour while you type. Tab inserts a tab.") }}</span>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
hx-post="/api/files/from-scratch"
|
||||
hx-vals='{"chat_id": "{{ chat.id }}"}'
|
||||
hx-target="#attachments" hx-swap="beforeend"
|
||||
title="Put this in the message box as an attachment">
|
||||
title="{{ t('Put this in the message box as an attachment') }}">
|
||||
{{ icon("attach", "icon--sm") }} Attach
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn--icon btn--sm" type="button" data-toggle="#canvas"
|
||||
aria-label="Close canvas">
|
||||
aria-label="{{ t('Close canvas') }}">
|
||||
{{ icon("x", "icon--sm") }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@
|
||||
hx-post="/api/chats/{{ chat.id }}/canvas/tabs"
|
||||
hx-vals='{"key": "scratch:{{ chat.id }}"}'
|
||||
hx-target="#canvas-inner" hx-swap="innerHTML"
|
||||
title="This chat's own working document">
|
||||
title="{{ t('This chat\'s own working document') }}">
|
||||
{{ icon("file-text", "icon--sm") }} Scratch
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<textarea class="composer__input" name="content" rows="1"
|
||||
data-autosize data-max-height="320" data-composer-input
|
||||
placeholder="{% if chat %}Send a message…{% else %}Ask anything…{% endif %}"
|
||||
aria-label="Message" {{ 'autofocus' if not chat }}></textarea>
|
||||
aria-label="{{ t('Message') }}" {{ 'autofocus' if not chat }}></textarea>
|
||||
</div>
|
||||
|
||||
<div class="composer__toolbar">
|
||||
@@ -118,34 +118,34 @@
|
||||
<div class="picker picker--up" data-picker>
|
||||
<button class="btn btn--icon composer__btn" type="button" data-picker-toggle
|
||||
aria-haspopup="menu" aria-expanded="false"
|
||||
aria-label="Attach" title="Attach">
|
||||
aria-label="{{ t('Attach') }}" title="{{ t('Attach') }}">
|
||||
{{ icon("attach") }}
|
||||
</button>
|
||||
|
||||
<div class="picker__menu picker__menu--compact" data-picker-menu role="menu"
|
||||
hidden aria-label="Attach">
|
||||
hidden aria-label="{{ t('Attach') }}">
|
||||
<button class="picker__option" type="button" role="menuitem"
|
||||
data-attach="file">
|
||||
{{ icon("attach", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">File</span>
|
||||
<span class="picker__option-note">PDF, text, code</span>
|
||||
<span class="picker__option-name">{{ t("File") }}</span>
|
||||
<span class="picker__option-note">{{ t("PDF, text, code") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="picker__option" type="button" role="menuitem"
|
||||
data-attach="image">
|
||||
{{ icon("image", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Image</span>
|
||||
<span class="picker__option-note">Sent only to vision models</span>
|
||||
<span class="picker__option-name">{{ t("Image") }}</span>
|
||||
<span class="picker__option-note">{{ t("Sent only to vision models") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="picker__option" type="button" role="menuitem"
|
||||
data-attach="link">
|
||||
{{ icon("link", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Link</span>
|
||||
<span class="picker__option-note">Fetch a page and attach its text</span>
|
||||
<span class="picker__option-name">{{ t("Link") }}</span>
|
||||
<span class="picker__option-note">{{ t("Fetch a page and attach its text") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% if can.get("library.use") %}
|
||||
@@ -153,8 +153,8 @@
|
||||
data-attach="knowledge">
|
||||
{{ icon("archive", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Knowledge</span>
|
||||
<span class="picker__option-note">From your library</span>
|
||||
<span class="picker__option-name">{{ t("Knowledge") }}</span>
|
||||
<span class="picker__option-note">{{ t("From your library") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -209,20 +209,18 @@
|
||||
<div class="picker picker--up" data-picker>
|
||||
<button class="btn btn--icon composer__btn" type="button" data-picker-toggle
|
||||
aria-haspopup="menu" aria-expanded="false"
|
||||
aria-label="Toggle" title="Toggle">
|
||||
aria-label="{{ t('Toggle') }}" title="{{ t('Toggle') }}">
|
||||
{{ icon("sliders") }}
|
||||
</button>
|
||||
|
||||
<div class="picker__menu picker__menu--scope" data-picker-menu role="menu"
|
||||
hidden aria-label="Toggle">
|
||||
hidden aria-label="{{ t('Toggle') }}">
|
||||
{% if has_scope %}
|
||||
<p class="picker__lede">
|
||||
Switched off here only. Everything is on unless you say otherwise.
|
||||
</p>
|
||||
<p class="picker__lede">{{ t("Switched off here only. Everything is on unless you say otherwise.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if scope_families %}
|
||||
<p class="picker__group">Tools</p>
|
||||
<p class="picker__group">{{ t("Tools") }}</p>
|
||||
{% for family in scope_families %}
|
||||
<label class="picker__option picker__option--toggle">
|
||||
{% if scope_prospective %}
|
||||
@@ -243,7 +241,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if scope_skills %}
|
||||
<p class="picker__group">Skills</p>
|
||||
<p class="picker__group">{{ t("Skills") }}</p>
|
||||
{% for skill in scope_skills %}
|
||||
<label class="picker__option picker__option--toggle">
|
||||
{% if scope_prospective %}
|
||||
@@ -277,7 +275,7 @@
|
||||
empty body. #}
|
||||
{% if scope_allow %}
|
||||
<div class="picker__allow">
|
||||
<p class="picker__group">Always allowed here</p>
|
||||
<p class="picker__group">{{ t("Always allowed here") }}</p>
|
||||
{% for entry in scope_allow %}
|
||||
<p class="picker__allow-entry">{{ entry }}</p>
|
||||
{% endfor %}
|
||||
@@ -286,7 +284,7 @@
|
||||
hx-target="closest .picker__allow" hx-swap="outerHTML">
|
||||
{{ icon("trash", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Ask me about these again</span>
|
||||
<span class="picker__option-name">{{ t("Ask me about these again") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -316,7 +314,7 @@
|
||||
<input type="hidden" name="kind" value="{{ starting_kind | default('chat') }}"
|
||||
id="chat-kind">
|
||||
|
||||
<div class="segmented" role="group" aria-label="Kind of chat">
|
||||
<div class="segmented" role="group" aria-label="{{ t('Kind of chat') }}">
|
||||
<label class="segmented__option">
|
||||
<input type="radio" name="kind_choice" value="chat"
|
||||
{{ '' if starting_kind == 'agent' else 'checked' }}>
|
||||
@@ -331,7 +329,7 @@
|
||||
|
||||
<span class="composer__agent" data-agent-extra hidden>
|
||||
<select class="select select--sm composer__connection"
|
||||
name="ssh_profile_id" aria-label="Connection">
|
||||
name="ssh_profile_id" aria-label="{{ t('Connection') }}">
|
||||
{% for profile in agent_profiles %}
|
||||
<option value="{{ profile.id }}" data-dir="{{ profile.default_dir }}"
|
||||
{{ 'disabled' if not profile.verified }}>
|
||||
@@ -346,14 +344,14 @@
|
||||
is also something you would rather find than spell. #}
|
||||
<input type="hidden" name="project_dir" value="" data-dir-value>
|
||||
<button class="btn btn--sm composer__dir" type="button" data-dir-browse
|
||||
aria-label="Project directory"
|
||||
title="Choose the directory this chat works in">
|
||||
aria-label="{{ t('Project directory') }}"
|
||||
title="{{ t('Choose the directory this chat works in') }}">
|
||||
{{ icon("folder", "icon--sm") }}
|
||||
<span class="composer__dir-path" data-dir-label>/</span>
|
||||
</button>
|
||||
|
||||
{% if agent_modes %}
|
||||
<select class="select select--sm" name="agent_mode" aria-label="Approval mode">
|
||||
<select class="select select--sm" name="agent_mode" aria-label="{{ t('Approval mode') }}">
|
||||
{% for value, label, hint in agent_modes %}
|
||||
<option value="{{ value }}" title="{{ hint }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
@@ -376,7 +374,7 @@
|
||||
through this element's *ancestors* -- which a sibling form is not.
|
||||
`form=` scopes the values, and only the values. #}
|
||||
<div class="composer__context">
|
||||
<select class="select select--sm" name="agent_mode" aria-label="Approval mode"
|
||||
<select class="select select--sm" name="agent_mode" aria-label="{{ t('Approval mode') }}"
|
||||
form="agent-mode-form"
|
||||
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none">
|
||||
{% for value, label, hint in agent_modes %}
|
||||
@@ -428,7 +426,7 @@
|
||||
#}
|
||||
{% if current_model and current_model.capabilities_json.get("reasoning") %}
|
||||
<select class="select select--sm" name="reasoning_effort" data-effort
|
||||
aria-label="Reasoning effort" title="How hard this model should think"
|
||||
aria-label="{{ t('Reasoning effort') }}" title="{{ t('How hard this model should think') }}"
|
||||
{% if chat %}
|
||||
form="chat-params-form"
|
||||
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none"
|
||||
@@ -449,7 +447,7 @@
|
||||
#}
|
||||
{% set chosen = resolved_effort if chat
|
||||
else (current_model.params_json or {}).get('reasoning_effort') %}
|
||||
<option value="off" {{ 'selected' if chosen not in efforts }}>Effort: off</option>
|
||||
<option value="off" {{ 'selected' if chosen not in efforts }}>{{ t("Effort: off") }}</option>
|
||||
{% for value in efforts %}
|
||||
<option value="{{ value }}" {{ 'selected' if chosen == value }}>
|
||||
Effort: {{ value }}
|
||||
@@ -464,7 +462,7 @@
|
||||
data-mic-state and the icon with it. #}
|
||||
<button class="btn btn--icon composer__btn composer__mic" type="button"
|
||||
data-mic data-mic-state="idle"
|
||||
aria-label="Dictate a message" title="Dictate a message">
|
||||
aria-label="{{ t('Dictate a message') }}" title="{{ t('Dictate a message') }}">
|
||||
<span class="composer__icon composer__icon--mic">{{ icon("mic") }}</span>
|
||||
<span class="composer__icon composer__icon--recording" aria-hidden="true">
|
||||
{{ icon("stop-circle") }}
|
||||
@@ -483,7 +481,7 @@
|
||||
layout and cannot flash an empty button.
|
||||
#}
|
||||
<button class="btn btn--primary btn--icon composer__btn" type="submit"
|
||||
data-composer-action="send" aria-label="Send">
|
||||
data-composer-action="send" aria-label="{{ t('Send') }}">
|
||||
<span class="composer__icon composer__icon--send">{{ icon("send") }}</span>
|
||||
<span class="composer__icon composer__icon--stop" aria-hidden="true">
|
||||
<span class="composer__stop-square"></span>
|
||||
@@ -514,9 +512,7 @@
|
||||
<p class="composer__hint">
|
||||
Enter to send, Shift+Enter for a new line.
|
||||
<button class="composer__hint-link" type="button"
|
||||
onclick="window.lembasCommands && window.lembasCommands.help()">
|
||||
/ for commands
|
||||
</button>,
|
||||
onclick="window.lembasCommands && window.lembasCommands.help()">{{ t("/ for commands") }}</button>,
|
||||
@ for files.
|
||||
{% if can.get("files.upload") %}
|
||||
Drag files in, or paste an image.
|
||||
@@ -531,7 +527,7 @@
|
||||
{% if can.get("files.upload") %}
|
||||
<div class="dropzone-overlay" aria-hidden="true">
|
||||
{{ icon("attach", "icon--lg") }}
|
||||
<span>Drop to attach</span>
|
||||
<span>{{ t("Drop to attach") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -14,17 +14,17 @@
|
||||
<div class="msg__main">
|
||||
<header class="msg__meta">
|
||||
<span class="msg__author">{{ user.name or "You" }}</span>
|
||||
<span class="msg__model">editing</span>
|
||||
<span class="msg__model">{{ t("editing") }}</span>
|
||||
</header>
|
||||
|
||||
<form class="edit-form"
|
||||
hx-post="/api/chats/{{ chat.id }}/messages/{{ message.id }}/edit"
|
||||
hx-target="#thread" hx-swap="innerHTML"
|
||||
hx-confirm="Rewind here? The {{ following }} message{{ '' if following == 1 else 's' }} after this one will be deleted."
|
||||
data-confirm-title="Rewind the conversation"
|
||||
data-confirm-label="Rewind and send">
|
||||
data-confirm-title="{{ t('Rewind the conversation') }}"
|
||||
data-confirm-label="{{ t('Rewind and send') }}">
|
||||
<textarea class="textarea" name="content" rows="3" data-autosize
|
||||
data-max-height="320" aria-label="Edit message"
|
||||
data-max-height="320" aria-label="{{ t('Edit message') }}"
|
||||
autofocus>{{ message.content }}</textarea>
|
||||
|
||||
<div class="btn-row">
|
||||
@@ -34,9 +34,7 @@
|
||||
</button>
|
||||
<button class="btn" type="button"
|
||||
hx-get="/api/chats/{{ chat.id }}/messages/{{ message.id }}/cancel-edit"
|
||||
hx-target="#msg-{{ message.id }}" hx-swap="outerHTML">
|
||||
Cancel
|
||||
</button>
|
||||
hx-target="#msg-{{ message.id }}" hx-swap="outerHTML">{{ t("Cancel") }}</button>
|
||||
{% if following %}
|
||||
<span class="text-xs faint">
|
||||
{{ following }} later message{{ '' if following == 1 else 's' }} will be discarded.
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
never intersects the viewport, so the request fires the first time it is
|
||||
opened and never on a page load nobody looked at.
|
||||
#}
|
||||
<aside class="inspector" id="inspector" hidden aria-label="Request inspector">
|
||||
<aside class="inspector" id="inspector" hidden aria-label="{{ t('Request inspector') }}">
|
||||
<div class="panel-head">
|
||||
<h2 class="panel-head__title">{{ icon("search", "icon--sm") }} Inspector</h2>
|
||||
<button class="btn btn--icon btn--sm" type="button" data-toggle="#inspector"
|
||||
aria-label="Close inspector">
|
||||
aria-label="{{ t('Close inspector') }}">
|
||||
{{ icon("x", "icon--sm") }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -19,6 +19,6 @@
|
||||
<div class="inspector__body" id="inspector-body"
|
||||
hx-get="/api/chats/{{ chat.id }}/inspect"
|
||||
hx-trigger="intersect once" hx-target="this" hx-swap="innerHTML">
|
||||
<p class="inspector__note">Opening…</p>
|
||||
<p class="inspector__note">{{ t("Opening…") }}</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -4,11 +4,7 @@
|
||||
JSON is full of model output, search results and uploaded documents -- hard
|
||||
rule 6 applies here exactly as it does to a chat bubble.
|
||||
#}
|
||||
<p class="inspector__note">
|
||||
Rebuilt now against the current configuration. This is not a recording of the
|
||||
request that produced the last reply — if a prompt or a setting has changed
|
||||
since, this shows what would be sent today.
|
||||
</p>
|
||||
<p class="inspector__note">{{ t("Rebuilt now against the current configuration. This is not a recording of the request that produced the last reply — if a prompt or a setting has changed since, this shows what would be sent today.") }}</p>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--sm" type="button"
|
||||
@@ -18,7 +14,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 class="inspector__heading">This chat</h3>
|
||||
<h3 class="inspector__heading">{{ t("This chat") }}</h3>
|
||||
<dl class="inspector__facts">
|
||||
<dt>Model</dt><dd>{{ chat.model_id or "—" }}</dd>
|
||||
<dt>Context</dt>
|
||||
@@ -34,7 +30,7 @@
|
||||
{% if chat.temporary %}<dt>Lifetime</dt><dd>temporary</dd>{% endif %}
|
||||
</dl>
|
||||
|
||||
<h3 class="inspector__heading">Last reply</h3>
|
||||
<h3 class="inspector__heading">{{ t("Last reply") }}</h3>
|
||||
{% if row %}
|
||||
<dl class="inspector__facts">
|
||||
<dt>Tokens</dt>
|
||||
@@ -42,7 +38,7 @@
|
||||
{% if metrics.has_anything %}
|
||||
{% if metrics.estimated %}~{% endif %}{{ metrics.prompt_tokens }} in,
|
||||
{% if metrics.estimated %}~{% endif %}{{ metrics.completion_tokens }} out
|
||||
{% if metrics.estimated %}<span class="badge">estimated</span>{% endif %}
|
||||
{% if metrics.estimated %}<span class="badge">{{ t("estimated") }}</span>{% endif %}
|
||||
{% else %}
|
||||
not reported
|
||||
{% endif %}
|
||||
@@ -53,24 +49,24 @@
|
||||
<dt>Rounds</dt><dd>{{ metrics.rounds }}</dd>
|
||||
<dt>State</dt>
|
||||
<dd>
|
||||
{% if row.error %}<span class="badge badge--danger">error</span>
|
||||
{% elif row.stopped %}<span class="badge badge--warning">stopped</span>
|
||||
{% elif not row.complete %}<span class="badge">writing</span>
|
||||
{% else %}<span class="badge badge--success">complete</span>{% endif %}
|
||||
{% if row.error %}<span class="badge badge--danger">{{ t("error") }}</span>
|
||||
{% elif row.stopped %}<span class="badge badge--warning">{{ t("stopped") }}</span>
|
||||
{% elif not row.complete %}<span class="badge">{{ t("writing") }}</span>
|
||||
{% else %}<span class="badge badge--success">{{ t("complete") }}</span>{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
{% if row.error %}
|
||||
<p class="inspector__note">{{ row.error }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="inspector__note">Nothing has been answered in this chat yet.</p>
|
||||
<p class="inspector__note">{{ t("Nothing has been answered in this chat yet.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="inspector__heading">System message</h3>
|
||||
<h3 class="inspector__heading">{{ t("System message") }}</h3>
|
||||
{% if system %}
|
||||
<pre class="inspector__json">{{ system }}</pre>
|
||||
{% else %}
|
||||
<p class="inspector__note">None is being sent.</p>
|
||||
<p class="inspector__note">{{ t("None is being sent.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="inspector__heading">
|
||||
@@ -80,11 +76,8 @@
|
||||
{% if tool_names %}
|
||||
<p class="inspector__note mono">{{ tool_names | join(", ") }}</p>
|
||||
{% else %}
|
||||
<p class="inspector__note">
|
||||
None. A model has to be marked as supporting tools, the user needs the
|
||||
permission, and the tool itself has to be turned on.
|
||||
</p>
|
||||
<p class="inspector__note">{{ t("None. A model has to be marked as supporting tools, the user needs the permission, and the tool itself has to be turned on.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="inspector__heading">Request body</h3>
|
||||
<h3 class="inspector__heading">{{ t("Request body") }}</h3>
|
||||
<pre class="inspector__json">{{ request_json }}</pre>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="interaction interaction--{{ ask.kind }}">
|
||||
<p class="interaction__from">
|
||||
{{ icon("sparkle", "icon--sm") }}
|
||||
<span>The model is asking you</span>
|
||||
<span>{{ t("The model is asking you") }}</span>
|
||||
</p>
|
||||
|
||||
<form class="interaction__form"
|
||||
@@ -70,13 +70,13 @@
|
||||
<input type="{{ 'checkbox' if item.multiple else 'radio' }}"
|
||||
name="choice.{{ item.key }}" value="{{ other_value }}">
|
||||
<span class="interaction__option-body">
|
||||
<span class="interaction__option-name">Something else</span>
|
||||
<span class="interaction__option-name">{{ t("Something else") }}</span>
|
||||
{# Never type="password". A model talked into asking for a
|
||||
credential must not be handed a field that looks built for one,
|
||||
and a chat transcript is not a place to keep secrets. #}
|
||||
<input class="input interaction__other-input" type="text"
|
||||
name="text.{{ item.key }}" autocomplete="off"
|
||||
placeholder="Your own answer…">
|
||||
placeholder="{{ t('Your own answer…') }}">
|
||||
</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
@@ -130,17 +130,12 @@
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="editing" x-cloak>
|
||||
<label class="visually-hidden" for="edit-{{ item.key }}">
|
||||
Change this before it runs
|
||||
</label>
|
||||
<label class="visually-hidden" for="edit-{{ item.key }}">{{ t("Change this before it runs") }}</label>
|
||||
<textarea class="textarea interaction__edit" id="edit-{{ item.key }}"
|
||||
name="text.{{ item.key }}" rows="3" spellcheck="false"
|
||||
x-ref="edit{{ item.key }}"
|
||||
:disabled="!editing">{{ item.detail }}</textarea>
|
||||
<p class="interaction__reason">
|
||||
Allow runs what is in the box. It is not checked against this
|
||||
chat's rules again — you typed it.
|
||||
</p>
|
||||
<p class="interaction__reason">{{ t("Allow runs what is in the box. It is not checked against this chat's rules again — you typed it.") }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<pre class="interaction__detail">{{ item.detail }}</pre>
|
||||
@@ -172,9 +167,7 @@
|
||||
<button class="btn btn--primary" type="submit" name="verdict" value="allow">
|
||||
{{ icon("check", "icon--sm") }} Allow
|
||||
</button>
|
||||
<button class="btn" type="submit" name="verdict" value="allow_always">
|
||||
Always allow this
|
||||
</button>
|
||||
<button class="btn" type="submit" name="verdict" value="allow_always">{{ t("Always allow this") }}</button>
|
||||
<button class="btn btn--danger" type="submit" name="verdict" value="deny"
|
||||
x-show="!explaining">
|
||||
{{ icon("x", "icon--sm") }} Don't
|
||||
@@ -185,9 +178,7 @@
|
||||
</button>
|
||||
|
||||
<div class="interaction__deny" x-show="explaining" x-cloak>
|
||||
<label class="visually-hidden" for="deny-reason">
|
||||
Why not, and what to do instead
|
||||
</label>
|
||||
<label class="visually-hidden" for="deny-reason">{{ t("Why not, and what to do instead") }}</label>
|
||||
{# Never type="password", for the reason the "Something else" box
|
||||
above states: a chat transcript is not a place to keep secrets. #}
|
||||
{# `--deny-input` and not `--reason`: `.interaction__reason` already
|
||||
@@ -195,14 +186,12 @@
|
||||
<textarea class="textarea interaction__deny-input" id="deny-reason"
|
||||
name="reason" rows="2" spellcheck="false" x-ref="reason"
|
||||
:disabled="!explaining"
|
||||
placeholder="Why not, and what to do instead…"></textarea>
|
||||
placeholder="{{ t('Why not, and what to do instead…') }}"></textarea>
|
||||
<div class="interaction__deny-actions">
|
||||
<button class="btn btn--danger" type="submit" name="verdict" value="deny">
|
||||
{{ icon("x", "icon--sm") }} Don't, and tell it why
|
||||
</button>
|
||||
<button class="btn btn--sm" type="button" @click="explaining = false">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn--sm" type="button" @click="explaining = false">{{ t("Cancel") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
seconds. Rendering it inside the poll would fetch output nobody has
|
||||
opened, on a loop. #}
|
||||
<div class="picker__menu picker__menu--jobs" data-picker-menu role="menu"
|
||||
hidden aria-label="Background jobs" id="jobs-panel"></div>
|
||||
hidden aria-label="{{ t('Background jobs') }}" id="jobs-panel"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
most of them.
|
||||
#}
|
||||
{% if not jobs %}
|
||||
<p class="picker__lede">Nothing is running.</p>
|
||||
<p class="picker__lede">{{ t("Nothing is running.") }}</p>
|
||||
{% else %}
|
||||
<p class="picker__group">Background jobs</p>
|
||||
<p class="picker__group">{{ t("Background jobs") }}</p>
|
||||
|
||||
{% for job in jobs %}
|
||||
<div class="jobs__row{{ ' jobs__row--open' if open_job == job.id }}">
|
||||
@@ -42,9 +42,7 @@
|
||||
<button class="btn btn--sm btn--danger jobs__stop" type="button"
|
||||
hx-post="/api/chats/{{ chat.id }}/jobs/{{ job.id }}/stop"
|
||||
hx-target="#jobs-panel" hx-swap="innerHTML"
|
||||
data-confirm-button="Stop this job? It and everything it started are killed.">
|
||||
Stop
|
||||
</button>
|
||||
data-confirm-button="{{ t('Stop this job? It and everything it started are killed.') }}">{{ t("Stop") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
{% else %}
|
||||
|
||||
{% if website %}
|
||||
<p class="picker__group">A page to read</p>
|
||||
<p class="picker__group">{{ t("A page to read") }}</p>
|
||||
<ul class="picker__list">
|
||||
<li>
|
||||
<button class="picker__option" type="button"
|
||||
data-mention-url="{{ website }}" data-mention-token="{{ website }}">
|
||||
{{ icon("link", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Fetch this page</span>
|
||||
<span class="picker__option-name">{{ t("Fetch this page") }}</span>
|
||||
<span class="picker__option-note mono">{{ website }}</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -39,7 +39,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if files %}
|
||||
<p class="picker__group">In the project</p>
|
||||
<p class="picker__group">{{ t("In the project") }}</p>
|
||||
<ul class="picker__list">
|
||||
{% for file in files %}
|
||||
<li>
|
||||
@@ -57,7 +57,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if documents %}
|
||||
<p class="picker__group">In your library</p>
|
||||
<p class="picker__group">{{ t("In your library") }}</p>
|
||||
<ul class="picker__list">
|
||||
{% for document in documents %}
|
||||
<li>
|
||||
@@ -79,7 +79,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if notes %}
|
||||
<p class="picker__group">Notes</p>
|
||||
<p class="picker__group">{{ t("Notes") }}</p>
|
||||
<ul class="picker__list">
|
||||
{% for note in notes %}
|
||||
<li>
|
||||
@@ -99,7 +99,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if skills %}
|
||||
<p class="picker__group">Skills</p>
|
||||
<p class="picker__group">{{ t("Skills") }}</p>
|
||||
<ul class="picker__list">
|
||||
{% for skill in skills %}
|
||||
<li>
|
||||
@@ -119,7 +119,7 @@
|
||||
{% if bases %}
|
||||
{# A base is a reference and not a copy: choosing one narrows what this chat
|
||||
may search rather than putting anything into the message. #}
|
||||
<p class="picker__group">Search only these</p>
|
||||
<p class="picker__group">{{ t("Search only these") }}</p>
|
||||
<ul class="picker__list">
|
||||
{% for base in bases %}
|
||||
<li>
|
||||
@@ -128,7 +128,7 @@
|
||||
{{ icon("archive", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">{{ base.name }}</span>
|
||||
<span class="picker__option-note">Scope this chat to this knowledge base</span>
|
||||
<span class="picker__option-note">{{ t("Scope this chat to this knowledge base") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -137,7 +137,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if attachments %}
|
||||
<p class="picker__group">Already in this chat</p>
|
||||
<p class="picker__group">{{ t("Already in this chat") }}</p>
|
||||
<ul class="picker__list">
|
||||
{% for attachment in attachments %}
|
||||
<li>
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
{% if crowd.get("stopped") %}
|
||||
{# Why a round ended, where it ended. Without this a crowd that ran out of
|
||||
rounds or time simply stops, which reads as the feature failing. #}
|
||||
<span class="badge badge--warning" title="The round ended here">
|
||||
<span class="badge badge--warning" title="{{ t('The round ended here') }}">
|
||||
{% if crowd.get("stopped") == "rounds" %}
|
||||
no rounds left
|
||||
{% elif crowd.get("stopped") == "time" %}
|
||||
@@ -176,7 +176,7 @@
|
||||
<summary class="reasoning__summary">
|
||||
{{ icon("sparkle", "icon--sm reasoning__icon") }}
|
||||
<span class="reasoning__label">
|
||||
<span class="reasoning__working">Thinking</span>
|
||||
<span class="reasoning__working">{{ t("Thinking") }}</span>
|
||||
<span class="reasoning__stats" sse-swap="think" hx-swap="innerHTML"></span>
|
||||
</span>
|
||||
{{ icon("chevron-down", "icon--sm reasoning__chevron") }}
|
||||
@@ -237,7 +237,7 @@
|
||||
<div class="alert alert--error msg__error" role="alert">
|
||||
{{ icon("warning", "alert__icon") }}
|
||||
<div>
|
||||
<strong>The reply could not be completed.</strong>
|
||||
<strong>{{ t("The reply could not be completed.") }}</strong>
|
||||
<div class="text-sm" style="margin-top: var(--sp-1)">{{ message.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -291,22 +291,22 @@
|
||||
holds a job's output, which is the one waiting bubble somebody actually
|
||||
wants to paste somewhere. The hidden source div is already below. #}
|
||||
<button class="btn btn--icon btn--sm" type="button"
|
||||
data-copy="msg-body-{{ message.id }}" aria-label="Copy message">
|
||||
data-copy="msg-body-{{ message.id }}" aria-label="{{ t('Copy message') }}">
|
||||
{{ icon("copy", "icon--sm") }}
|
||||
</button>
|
||||
<button class="btn btn--sm" type="button"
|
||||
hx-post="/api/chats/{{ chat.id }}/messages/{{ message.id }}/send-now"
|
||||
hx-target="#thread" hx-swap="innerHTML">Send now</button>
|
||||
hx-target="#thread" hx-swap="innerHTML">{{ t("Send now") }}</button>
|
||||
<button class="btn btn--sm" type="button"
|
||||
hx-post="/api/chats/{{ chat.id }}/messages/{{ message.id }}/discard"
|
||||
hx-target="#msg-{{ message.id }}" hx-swap="outerHTML"
|
||||
data-confirm-button="Discard this message?">Discard</button>
|
||||
data-confirm-button="{{ t('Discard this message?') }}">{{ t("Discard") }}</button>
|
||||
</footer>
|
||||
<div hidden id="msg-body-{{ message.id }}">{{ message.content }}</div>
|
||||
{% elif not streaming %}
|
||||
<footer class="msg__actions">
|
||||
<button class="btn btn--icon btn--sm" type="button"
|
||||
data-copy="msg-body-{{ message.id }}" aria-label="Copy message">
|
||||
data-copy="msg-body-{{ message.id }}" aria-label="{{ t('Copy message') }}">
|
||||
{{ icon("copy", "icon--sm") }}
|
||||
</button>
|
||||
{# Not on a machine event: editing rewinds the transcript and starts a
|
||||
@@ -318,7 +318,7 @@
|
||||
hx-get="/api/chats/{{ chat.id }}/messages/{{ message.id }}/edit"
|
||||
hx-target="#msg-{{ message.id }}" hx-swap="outerHTML"
|
||||
data-edit-message
|
||||
aria-label="Edit and retry from here">
|
||||
aria-label="{{ t('Edit and retry from here') }}">
|
||||
{{ icon("pencil", "icon--sm") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -326,7 +326,7 @@
|
||||
<button class="btn btn--icon btn--sm" type="button"
|
||||
hx-post="/api/chats/{{ chat.id }}/messages/{{ message.id }}/regenerate"
|
||||
hx-target="#msg-{{ message.id }}" hx-swap="outerHTML"
|
||||
aria-label="Regenerate reply">
|
||||
aria-label="{{ t('Regenerate reply') }}">
|
||||
{{ icon("refresh", "icon--sm") }}
|
||||
</button>
|
||||
{% if can_listen | default(false) and message.content and not message.error %}
|
||||
@@ -339,7 +339,7 @@
|
||||
<button class="btn btn--icon btn--sm" type="button"
|
||||
data-speak="/api/audio/speech/{{ chat.id }}/{{ message.id }}"
|
||||
{% if audio_autoplay | default(false) and just_finished | default(false) %}data-speak-auto{% endif %}
|
||||
aria-label="Read this reply aloud">
|
||||
aria-label="{{ t('Read this reply aloud') }}">
|
||||
<span class="speak__icon speak__icon--play">{{ icon("speaker", "icon--sm") }}</span>
|
||||
<span class="speak__icon speak__icon--stop">{{ icon("stop-circle", "icon--sm") }}</span>
|
||||
</button>
|
||||
|
||||
@@ -17,17 +17,17 @@
|
||||
{{ model_avatar(current_model, cls="picker__avatar") }}
|
||||
<span class="picker__label">{{ current_model.label }}</span>
|
||||
{% else %}
|
||||
<span class="picker__label">Choose a model</span>
|
||||
<span class="picker__label">{{ t("Choose a model") }}</span>
|
||||
{% endif %}
|
||||
{{ icon("chevron-down", "icon--sm picker__chevron") }}
|
||||
</button>
|
||||
|
||||
<div class="picker__menu" data-picker-menu role="listbox" hidden
|
||||
aria-label="Models">
|
||||
aria-label="{{ t('Models') }}">
|
||||
{% if models|length > 8 %}
|
||||
<div class="picker__search">
|
||||
<input class="input input--sm" type="search" data-picker-filter
|
||||
placeholder="Filter models…" aria-label="Filter models">
|
||||
placeholder="{{ t('Filter models…') }}" aria-label="{{ t('Filter models') }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="picker__empty" data-picker-empty hidden>No model matches that.</p>
|
||||
<p class="picker__empty" data-picker-empty hidden>{{ t("No model matches that.") }}</p>
|
||||
</div>
|
||||
|
||||
{% if chat %}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
{% if plan.findings %}
|
||||
<div class="plan__section">
|
||||
<h4 class="plan__heading">What was found</h4>
|
||||
<h4 class="plan__heading">{{ t("What was found") }}</h4>
|
||||
<ul class="plan__findings">
|
||||
{% for finding in plan.findings %}
|
||||
<li>{{ finding.text }}</li>
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
{% if plan.objectives %}
|
||||
<div class="plan__section">
|
||||
<h4 class="plan__heading">What it is for</h4>
|
||||
<h4 class="plan__heading">{{ t("What it is for") }}</h4>
|
||||
<ul class="plan__objectives">
|
||||
{% for objective in plan.objectives %}
|
||||
<li class="plan__item plan__item--{{ objective.status }}">{{ objective.text }}</li>
|
||||
@@ -79,11 +79,11 @@
|
||||
hx-post="/api/chats/{{ chat.id }}/messages/{{ message.id }}/execute-plan"
|
||||
hx-target="#thread" hx-swap="beforeend"
|
||||
hx-confirm="Switch this chat to Edit mode and carry out this plan? Every command will still be shown to you before it runs."
|
||||
data-confirm-label="Carry it out"
|
||||
data-confirm-label="{{ t('Carry it out') }}"
|
||||
data-confirm-danger="false">
|
||||
{{ icon("send", "icon--sm") }} Carry out this plan
|
||||
</button>
|
||||
<span class="plan__note">Switches to Edit — commands still ask.</span>
|
||||
<span class="plan__note">{{ t("Switches to Edit — commands still ask.") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
the person at the keyboard holds the credential and could open the same shell
|
||||
with an ssh client.
|
||||
#}
|
||||
<aside class="terminal" id="terminal" hidden aria-label="Terminal"
|
||||
<aside class="terminal" id="terminal" hidden aria-label="{{ t('Terminal') }}"
|
||||
data-terminal
|
||||
{# Empty before a chat exists: `draft.js` writes a draft id in as soon as
|
||||
the composer has a connection selected. Explicit rather than relying on
|
||||
@@ -26,7 +26,7 @@
|
||||
#}
|
||||
<div class="panel-resize" data-resize="--terminal-width" data-resize-min="384"
|
||||
role="separator" aria-orientation="vertical" tabindex="0"
|
||||
aria-label="Resize the terminal">
|
||||
aria-label="{{ t('Resize the terminal') }}">
|
||||
{{ icon("grip", "icon--sm") }}
|
||||
</div>
|
||||
|
||||
@@ -44,13 +44,13 @@
|
||||
message is worse than nothing attached at all.
|
||||
#}
|
||||
<button class="btn btn--icon btn--sm" type="button" data-terminal-copy
|
||||
title="Copy the last command and its output"
|
||||
aria-label="Copy the last command and its output">
|
||||
title="{{ t('Copy the last command and its output') }}"
|
||||
aria-label="{{ t('Copy the last command and its output') }}">
|
||||
{{ icon("copy", "icon--sm") }}
|
||||
</button>
|
||||
<button class="btn btn--icon btn--sm" type="button" data-terminal-send
|
||||
title="Put the last command and its output into the message box"
|
||||
aria-label="Send to chat">
|
||||
title="{{ t('Put the last command and its output into the message box') }}"
|
||||
aria-label="{{ t('Send to chat') }}">
|
||||
{{ icon("arrow-up", "icon--sm") }}
|
||||
</button>
|
||||
{#
|
||||
@@ -62,14 +62,14 @@
|
||||
command you run to a model is not something to inherit from last week.
|
||||
#}
|
||||
<select class="select select--sm" data-terminal-auto
|
||||
aria-label="What to do when a command finishes"
|
||||
title="What to do with each command you run">
|
||||
<option value="off" selected>Auto: off</option>
|
||||
<option value="copy">Auto: copy</option>
|
||||
<option value="send">Auto: send</option>
|
||||
aria-label="{{ t('What to do when a command finishes') }}"
|
||||
title="{{ t('What to do with each command you run') }}">
|
||||
<option value="off" selected>{{ t("Auto: off") }}</option>
|
||||
<option value="copy">{{ t("Auto: copy") }}</option>
|
||||
<option value="send">{{ t("Auto: send") }}</option>
|
||||
</select>
|
||||
<button class="btn btn--icon btn--sm" type="button" data-toggle="#terminal"
|
||||
aria-label="Close terminal">
|
||||
aria-label="{{ t('Close terminal') }}">
|
||||
{{ icon("x", "icon--sm") }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="terminal__screen" data-terminal-screen></div>
|
||||
|
||||
<div class="terminal__status">
|
||||
<span class="terminal__message" data-terminal-message>Connecting…</span>
|
||||
<span class="terminal__message" data-terminal-message>{{ t("Connecting…") }}</span>
|
||||
<span class="terminal__last" data-terminal-last></span>
|
||||
<span>Ctrl+Shift+C / V</span>
|
||||
</div>
|
||||
|
||||
@@ -19,10 +19,7 @@
|
||||
<span>{{ compacted | length }} earlier messages, summarised</span>
|
||||
{{ icon("chevron-down", "icon--sm reasoning__chevron") }}
|
||||
</summary>
|
||||
<p class="compacted__note">
|
||||
These are no longer sent to the model; a summary of them goes instead. They
|
||||
are kept here so nothing is lost.
|
||||
</p>
|
||||
<p class="compacted__note">{{ t("These are no longer sent to the model; a summary of them goes instead. They are kept here so nothing is lost.") }}</p>
|
||||
<div class="compacted__body">
|
||||
{% for message in compacted %}
|
||||
{% include "chat/_message.html" %}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
left to be inferred: attributing somebody's own typing to a model is
|
||||
the same misattribution as the other way round, and a transcript read
|
||||
back a week later has nothing else to go on. #}
|
||||
<span class="badge">edited by you</span>
|
||||
<span class="badge">{{ t("edited by you") }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if event.why %}
|
||||
@@ -107,7 +107,7 @@
|
||||
{% if event.error %}
|
||||
<p class="tool-activity__error">{{ event.error }}</p>
|
||||
{% elif not event.results and not event.text and not event.image %}
|
||||
<p class="tool-activity__error">Nothing was found.</p>
|
||||
<p class="tool-activity__error">{{ t("Nothing was found.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if event.image and event.image.id %}
|
||||
@@ -125,7 +125,7 @@
|
||||
#}
|
||||
<a class="tool-image" href="/api/files/{{ event.image.id }}/content"
|
||||
target="_blank" rel="noopener">
|
||||
<img src="/api/files/{{ event.image.id }}/content" alt="The generated image"
|
||||
<img src="/api/files/{{ event.image.id }}/content" alt="{{ t('The generated image') }}"
|
||||
loading="lazy"
|
||||
{% if event.image.width %}width="{{ event.image.width }}"{% endif %}
|
||||
{% if event.image.height %}height="{{ event.image.height }}"{% endif %}>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<table class="sheet">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>In the window now</td>
|
||||
<td>{{ t("In the window now") }}</td>
|
||||
<td>
|
||||
{% if metrics.context_tokens %}
|
||||
{{ '~' if metrics.estimated }}{{ '{:,}'.format(metrics.context_tokens) }} tokens
|
||||
@@ -33,7 +33,7 @@
|
||||
the percentage is omitted rather than computed, and automatic
|
||||
compaction never fires. #}
|
||||
<tr>
|
||||
<td>Window size</td>
|
||||
<td>{{ t("Window size") }}</td>
|
||||
<td>
|
||||
Not recorded for {{ model.label if model else "this model" }}, so there is
|
||||
no percentage and this chat will never compact itself.
|
||||
@@ -45,35 +45,32 @@
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td>Spent in total</td>
|
||||
<td>{{ t("Spent in total") }}</td>
|
||||
<td>
|
||||
{{ '~' if estimated }}{{ '{:,}'.format(totals.total) }} tokens
|
||||
across {{ replies }} repl{{ 'y' if replies == 1 else 'ies' }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Of which sent</td>
|
||||
<td>{{ t("Of which sent") }}</td>
|
||||
<td>{{ '~' if estimated }}{{ '{:,}'.format(totals.prompt) }} tokens</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Of which written</td>
|
||||
<td>{{ t("Of which written") }}</td>
|
||||
<td>{{ '~' if estimated }}{{ '{:,}'.format(totals.completion) }} tokens</td>
|
||||
</tr>
|
||||
|
||||
{% if metrics.tokens_per_second %}
|
||||
<tr>
|
||||
<td>Last reply</td>
|
||||
<td>{{ t("Last reply") }}</td>
|
||||
<td>{{ '%.1f'|format(metrics.tokens_per_second) }} tokens/second</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% if chat.compact_summary %}
|
||||
<tr>
|
||||
<td>Compacted</td>
|
||||
<td>
|
||||
Earlier turns are summarised. They are still in the transcript; they
|
||||
just stop being sent.
|
||||
</td>
|
||||
<td>{{ t("Compacted") }}</td>
|
||||
<td>{{ t("Earlier turns are summarised. They are still in the transcript; they just stop being sent.") }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
<button class="btn btn--icon btn--sm topbar__rename" type="button"
|
||||
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none"
|
||||
data-prompt="What should this chat be called?"
|
||||
data-prompt-title="Rename chat" data-prompt-field="title"
|
||||
data-prompt-title="{{ t('Rename chat') }}" data-prompt-field="title"
|
||||
data-prompt-value="{{ chat.title }}"
|
||||
aria-label="Rename chat" title="Rename chat">
|
||||
aria-label="{{ t('Rename chat') }}" title="{{ t('Rename chat') }}">
|
||||
{{ icon("pencil", "icon--sm") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -43,9 +43,7 @@
|
||||
it. The way out of it is Keep, in the overflow menu. #}
|
||||
{% if chat and chat.temporary %}
|
||||
<span class="badge badge--warning"
|
||||
title="Not listed in the sidebar, and removed 24 hours after the last message.">
|
||||
Temporary
|
||||
</span>
|
||||
title="{{ t('Not listed in the sidebar, and removed 24 hours after the last message.') }}">{{ t("Temporary") }}</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
@@ -72,7 +70,7 @@
|
||||
{% 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' }}"
|
||||
aria-label="Temporary chat"
|
||||
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") }}
|
||||
</a>
|
||||
@@ -97,8 +95,8 @@
|
||||
most likely to be open beside it. All three share one slot: at
|
||||
1280px the sidebar plus two panels leaves about seventy pixels of
|
||||
chat. #}
|
||||
<button class="btn btn--icon" type="button" aria-label="Canvas"
|
||||
title="Open a file beside the conversation"
|
||||
<button class="btn btn--icon" type="button" aria-label="{{ t('Canvas') }}"
|
||||
title="{{ t('Open a file beside the conversation') }}"
|
||||
{% if not chat %}data-agent-only hidden{% endif %}
|
||||
aria-expanded="false" data-toggle="#canvas" data-toggle-group="side">
|
||||
{{ icon("file-text") }}
|
||||
@@ -108,7 +106,7 @@
|
||||
{% if terminal_enabled %}
|
||||
{# To the left of the inspector, and never open beside it: see the
|
||||
toggle group in app.js. #}
|
||||
<button class="btn btn--icon" type="button" aria-label="Terminal"
|
||||
<button class="btn btn--icon" type="button" aria-label="{{ t('Terminal') }}"
|
||||
title="Open a shell on {{ agent_profile.name if agent_profile else 'this connection' }}"
|
||||
{% if not chat %}data-agent-only hidden{% endif %}
|
||||
aria-expanded="false" data-toggle="#terminal" data-toggle-group="side">
|
||||
@@ -117,8 +115,8 @@
|
||||
{% endif %}
|
||||
|
||||
{% if chat and user.is_admin %}
|
||||
<button class="btn btn--icon" type="button" aria-label="Inspect this chat"
|
||||
title="Inspect this chat" aria-expanded="false" data-toggle="#inspector"
|
||||
<button class="btn btn--icon" type="button" aria-label="{{ t('Inspect this chat') }}"
|
||||
title="{{ t('Inspect this chat') }}" aria-expanded="false" data-toggle="#inspector"
|
||||
data-toggle-group="side">
|
||||
{{ icon("search") }}
|
||||
</button>
|
||||
@@ -136,21 +134,19 @@
|
||||
<div class="picker" data-picker>
|
||||
<button class="btn btn--icon" type="button" data-picker-toggle
|
||||
aria-haspopup="menu" aria-expanded="false"
|
||||
aria-label="More" title="More">
|
||||
aria-label="{{ t('More') }}" title="{{ t('More') }}">
|
||||
{{ icon("dots") }}
|
||||
</button>
|
||||
|
||||
<div class="picker__menu picker__menu--compact" data-picker-menu role="menu"
|
||||
hidden aria-label="More">
|
||||
hidden aria-label="{{ t('More') }}">
|
||||
{% if chat.temporary %}
|
||||
<button class="picker__option" type="button" role="menuitem"
|
||||
hx-post="/api/chats/{{ chat.id }}/keep" hx-swap="none">
|
||||
{{ icon("pin", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Keep this chat</span>
|
||||
<span class="picker__option-note">
|
||||
Temporary — removed 24 hours after the last message
|
||||
</span>
|
||||
<span class="picker__option-name">{{ t("Keep this chat") }}</span>
|
||||
<span class="picker__option-note">{{ t("Temporary — removed 24 hours after the last message") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -160,8 +156,8 @@
|
||||
data-toggle="#chat-settings">
|
||||
{{ icon("sliders", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Chat settings</span>
|
||||
<span class="picker__option-note">Prompt, knowledge, sampling</span>
|
||||
<span class="picker__option-name">{{ t("Chat settings") }}</span>
|
||||
<span class="picker__option-note">{{ t("Prompt, knowledge, sampling") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -174,10 +170,8 @@
|
||||
onclick="window.lembasCommands && window.lembasCommands.run('compact')">
|
||||
{{ icon("archive", "icon--sm") }}
|
||||
<span class="picker__option-body">
|
||||
<span class="picker__option-name">Compact</span>
|
||||
<span class="picker__option-note">
|
||||
Summarise the earlier turns so they stop costing context
|
||||
</span>
|
||||
<span class="picker__option-name">{{ t("Compact") }}</span>
|
||||
<span class="picker__option-note">{{ t("Summarise the earlier turns so they stop costing context") }}</span>
|
||||
</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -198,7 +192,7 @@
|
||||
|
||||
{% if can.get("chat.system_prompt") %}
|
||||
<div class="field">
|
||||
<label class="field__label" for="system-prompt">System prompt</label>
|
||||
<label class="field__label" for="system-prompt">{{ t("System prompt") }}</label>
|
||||
<textarea class="textarea" id="system-prompt" name="system_prompt" rows="3"
|
||||
placeholder="{{ inherited_prompt or 'Instructions that apply to every message in this chat.' }}"
|
||||
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none"
|
||||
@@ -217,7 +211,7 @@
|
||||
{# Which bases this chat draws on. None ticked means everything you can
|
||||
see, which is what a chat with nothing chosen should do. #}
|
||||
<div class="field">
|
||||
<label class="field__label">Knowledge</label>
|
||||
<label class="field__label">{{ t("Knowledge") }}</label>
|
||||
<form hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
||||
{# Always submitted, so unticking the last box still says something.
|
||||
An absent checkbox carries no signal of its own. #}
|
||||
@@ -247,7 +241,7 @@
|
||||
{# Who else answers. Nothing ticked is every chat that has ever existed:
|
||||
one model, answering on its own. #}
|
||||
<div class="field">
|
||||
<label class="field__label">Crowd</label>
|
||||
<label class="field__label">{{ t("Crowd") }}</label>
|
||||
<form hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
||||
{# Always submitted, for the reason the bases above are. #}
|
||||
<input type="hidden" name="crowd_model_ids" value="">
|
||||
@@ -292,31 +286,28 @@
|
||||
{% if can.get("chat.params") %}
|
||||
<div class="grid grid--3">
|
||||
<div class="field">
|
||||
<label class="field__label" for="temperature">Temperature</label>
|
||||
<label class="field__label" for="temperature">{{ t("Temperature") }}</label>
|
||||
<input class="input" id="temperature" name="temperature" type="number"
|
||||
min="0" max="2" step="0.05" placeholder="default"
|
||||
min="0" max="2" step="0.05" placeholder="{{ t('default') }}"
|
||||
value="{{ chat.params_json.get('temperature') if chat.params_json.get('temperature') is not none else '' }}"
|
||||
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="top-p">Top-p</label>
|
||||
<label class="field__label" for="top-p">{{ t("Top-p") }}</label>
|
||||
<input class="input" id="top-p" name="top_p" type="number"
|
||||
min="0" max="1" step="0.05" placeholder="default"
|
||||
min="0" max="1" step="0.05" placeholder="{{ t('default') }}"
|
||||
value="{{ chat.params_json.get('top_p') if chat.params_json.get('top_p') is not none else '' }}"
|
||||
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="max-tokens">Max tokens</label>
|
||||
<label class="field__label" for="max-tokens">{{ t("Max tokens") }}</label>
|
||||
<input class="input" id="max-tokens" name="max_tokens" type="number"
|
||||
min="1" step="1" placeholder="default"
|
||||
min="1" step="1" placeholder="{{ t('default') }}"
|
||||
value="{{ chat.params_json.get('max_tokens') if chat.params_json.get('max_tokens') is not none else '' }}"
|
||||
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
||||
</div>
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
Leave a field empty to let the provider decide. Values outside the
|
||||
allowed range are ignored rather than clamped.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Leave a field empty to let the provider decide. Values outside the allowed range are ignored rather than clamped.") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -326,7 +317,7 @@
|
||||
{# Every fresh install lands here, so it points at the fix. #}
|
||||
<div class="empty">
|
||||
{{ icon("server", "empty__mark") }}
|
||||
<h2 class="empty__title">No models available</h2>
|
||||
<h2 class="empty__title">{{ t("No models available") }}</h2>
|
||||
<p class="empty__text">
|
||||
{% if user.is_admin %}
|
||||
Add an OpenAI-compatible connection and {{ brand.name }} will load its models.
|
||||
@@ -347,7 +338,7 @@
|
||||
{% if not messages %}
|
||||
<div class="thread__intro">
|
||||
{{ mark(cls="empty__mark", uid="intro") }}
|
||||
<h2 class="empty__title">What would you ask?</h2>
|
||||
<h2 class="empty__title">{{ t("What would you ask?") }}</h2>
|
||||
<p class="empty__text">{{ brand.text.chat_empty }}</p>
|
||||
|
||||
{# Only on a chat that does not exist yet. An empty chat someone
|
||||
|
||||
@@ -51,19 +51,17 @@
|
||||
<div class="page">
|
||||
<form hx-patch="/api/folders/{{ folder.id }}" hx-swap="none">
|
||||
<div class="card">
|
||||
<h2 class="card__title">Name</h2>
|
||||
<h2 class="card__title">{{ t("Name") }}</h2>
|
||||
<div class="field">
|
||||
<input class="input" type="text" name="name" maxlength="200"
|
||||
value="{{ folder.name }}" aria-label="Folder name" required>
|
||||
value="{{ folder.name }}" aria-label="{{ t('Folder name') }}" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="folder-description">Description</label>
|
||||
<label class="field__label" for="folder-description">{{ t("Description") }}</label>
|
||||
<input class="input" type="text" id="folder-description" name="description"
|
||||
maxlength="500" value="{{ folder.description }}"
|
||||
placeholder="What this folder is for.">
|
||||
<p class="field__hint">
|
||||
For you, not for any model. It is never sent anywhere.
|
||||
</p>
|
||||
placeholder="{{ t('What this folder is for.') }}">
|
||||
<p class="field__hint">{{ t("For you, not for any model. It is never sent anywhere.") }}</p>
|
||||
</div>
|
||||
{#
|
||||
Nesting has worked at the route since folders existed -- with a cycle
|
||||
@@ -77,9 +75,9 @@
|
||||
route will reject is a control that looks like it works.
|
||||
#}
|
||||
<div class="field">
|
||||
<label class="field__label" for="folder-parent">Inside</label>
|
||||
<label class="field__label" for="folder-parent">{{ t("Inside") }}</label>
|
||||
<select class="select" id="folder-parent" name="parent_id">
|
||||
<option value="">Nothing — a folder at the top</option>
|
||||
<option value="">{{ t("Nothing — a folder at the top") }}</option>
|
||||
{% for candidate in parents %}
|
||||
<option value="{{ candidate.id }}"
|
||||
{{ 'selected' if folder.parent_id == candidate.id }}>
|
||||
@@ -87,43 +85,29 @@
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="field__hint">
|
||||
A folder inside another inherits its system prompt where it has
|
||||
none of its own.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("A folder inside another inherits its system prompt where it has none of its own.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card__title">System prompt</h2>
|
||||
<p class="card__lede">
|
||||
Used by every chat in this folder, and by folders nested inside it,
|
||||
unless the chat has a prompt of its own. Read each time a reply is
|
||||
built rather than copied when a chat is made, so editing this
|
||||
reaches the chats already here.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("System prompt") }}</h2>
|
||||
<p class="card__lede">{{ t("Used by every chat in this folder, and by folders nested inside it, unless the chat has a prompt of its own. Read each time a reply is built rather than copied when a chat is made, so editing this reaches the chats already here.") }}</p>
|
||||
<div class="field">
|
||||
<textarea class="textarea" name="system_prompt" rows="8"
|
||||
aria-label="System prompt"
|
||||
placeholder="Leave empty to fall through to the model's prompt, then the instance's.">{{ folder.system_prompt }}</textarea>
|
||||
<p class="field__hint">
|
||||
Precedence, not concatenation: chat, then folder, then model, then
|
||||
instance. The most specific one wins outright.
|
||||
</p>
|
||||
aria-label="{{ t('System prompt') }}"
|
||||
placeholder="{{ t('Leave empty to fall through to the model\'s prompt, then the instance\'s.') }}">{{ folder.system_prompt }}</textarea>
|
||||
<p class="field__hint">{{ t("Precedence, not concatenation: chat, then folder, then model, then instance. The most specific one wins outright.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card__title">What a new chat starts as</h2>
|
||||
<p class="card__lede">
|
||||
Seeds, copied onto a chat when it is created and its own from then
|
||||
on. Anything chosen on the new-chat screen wins over these.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("What a new chat starts as") }}</h2>
|
||||
<p class="card__lede">{{ t("Seeds, copied onto a chat when it is created and its own from then on. Anything chosen on the new-chat screen wins over these.") }}</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="folder-model">Model</label>
|
||||
<label class="field__label" for="folder-model">{{ t("Model") }}</label>
|
||||
<select class="select" id="folder-model" name="model_id">
|
||||
<option value="">No opinion</option>
|
||||
<option value="">{{ t("No opinion") }}</option>
|
||||
{% for model in models %}
|
||||
<option value="{{ model.model_id }}"
|
||||
{{ 'selected' if model.model_id == folder.model_id }}>
|
||||
@@ -134,16 +118,13 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="folder-kind">Kind</label>
|
||||
<label class="field__label" for="folder-kind">{{ t("Kind") }}</label>
|
||||
<select class="select" id="folder-kind" name="kind">
|
||||
<option value="" {{ 'selected' if not folder.kind }}>No opinion</option>
|
||||
<option value="chat" {{ 'selected' if folder.kind == 'chat' }}>Chat</option>
|
||||
<option value="agent" {{ 'selected' if folder.kind == 'agent' }}>Agent chat</option>
|
||||
<option value="" {{ 'selected' if not folder.kind }}>{{ t("No opinion") }}</option>
|
||||
<option value="chat" {{ 'selected' if folder.kind == 'chat' }}>{{ t("Chat") }}</option>
|
||||
<option value="agent" {{ 'selected' if folder.kind == 'agent' }}>{{ t("Agent chat") }}</option>
|
||||
</select>
|
||||
<p class="field__hint">
|
||||
A folder set to one kind shows on only that side of the sidebar's
|
||||
switch, and opens the new-chat screen already on that fork.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("A folder set to one kind shows on only that side of the sidebar's switch, and opens the new-chat screen already on that fork.") }}</p>
|
||||
</div>
|
||||
|
||||
{% if agent_profiles %}
|
||||
@@ -152,9 +133,9 @@
|
||||
too, and a field that appears only once another field is right is a
|
||||
field people conclude is missing. #}
|
||||
<div class="field">
|
||||
<label class="field__label" for="folder-profile">Connection</label>
|
||||
<label class="field__label" for="folder-profile">{{ t("Connection") }}</label>
|
||||
<select class="select" id="folder-profile" name="ssh_profile_id">
|
||||
<option value="">No opinion</option>
|
||||
<option value="">{{ t("No opinion") }}</option>
|
||||
{% for profile in agent_profiles %}
|
||||
<option value="{{ profile.id }}"
|
||||
{{ 'selected' if profile.id == folder.ssh_profile_id }}>
|
||||
@@ -169,12 +150,12 @@
|
||||
path is something you would rather find than spell. `data-dir-field`
|
||||
is what wires it; see ui.js. #}
|
||||
<div class="field" data-dir-field>
|
||||
<span class="field__label">Project directory</span>
|
||||
<span class="field__label">{{ t("Project directory") }}</span>
|
||||
<input type="hidden" name="project_dir"
|
||||
value="{{ folder.project_dir }}" data-dir-value>
|
||||
<div class="dir-row">
|
||||
<button class="btn" type="button" data-dir-browse
|
||||
aria-label="Choose the project directory">
|
||||
aria-label="{{ t('Choose the project directory') }}">
|
||||
{{ icon("folder", "icon--sm") }}
|
||||
<span class="dir-row__path" data-dir-label>
|
||||
{{ folder.project_dir or "the connection's own default" }}
|
||||
@@ -184,20 +165,18 @@
|
||||
directory and then changing your mind before saving needs a way
|
||||
back to "no opinion" as much as clearing a saved one does. #}
|
||||
<button class="btn btn--icon" type="button" data-dir-clear
|
||||
aria-label="Clear the project directory" title="No opinion">
|
||||
aria-label="{{ t('Clear the project directory') }}" title="{{ t('No opinion') }}">
|
||||
{{ icon("x", "icon--sm") }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
What a relative path is measured from in chats started here.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("What a relative path is measured from in chats started here.") }}</p>
|
||||
</div>
|
||||
|
||||
{% if agent_modes %}
|
||||
<div class="field">
|
||||
<label class="field__label" for="folder-mode">Approval mode</label>
|
||||
<label class="field__label" for="folder-mode">{{ t("Approval mode") }}</label>
|
||||
<select class="select" id="folder-mode" name="agent_mode">
|
||||
<option value="">No opinion</option>
|
||||
<option value="">{{ t("No opinion") }}</option>
|
||||
{% for value, label, hint in agent_modes %}
|
||||
<option value="{{ value }}" title="{{ hint }}"
|
||||
{{ 'selected' if value == folder.agent_mode }}>{{ label }}</option>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="topbar__actions">{% block actions %}{% endblock %}</div>
|
||||
</header>
|
||||
|
||||
<nav class="tabs__bar" aria-label="Library">
|
||||
<nav class="tabs__bar" aria-label="{{ t('Library') }}">
|
||||
<a class="tabs__tab {{ 'is-active' if section == 'knowledge' }}"
|
||||
href="/library/knowledge">{{ icon("archive", "icon--sm") }} Knowledge</a>
|
||||
<a class="tabs__tab {{ 'is-active' if section == 'notes' }}"
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
something is missing. `aria-hidden` and a `role="status"` label beside it,
|
||||
because a paragraph of grey blocks is nothing to read aloud. #}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Shared with</h2>
|
||||
<span class="visually-hidden" role="status">Loading who this is shared with</span>
|
||||
<h2 class="card__title">{{ t("Shared with") }}</h2>
|
||||
<span class="visually-hidden" role="status">{{ t("Loading who this is shared with") }}</span>
|
||||
<div aria-hidden="true">
|
||||
<div class="skeleton skeleton--row"></div>
|
||||
<div class="skeleton skeleton--row skeleton--short"></div>
|
||||
@@ -36,6 +36,6 @@
|
||||
{% elif not is_owner %}
|
||||
<div class="alert">
|
||||
{{ icon("users", "alert__icon") }}
|
||||
<span>Shared with you. You can read this but not change it.</span>
|
||||
<span>{{ t("Shared with you. You can read this but not change it.") }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -22,34 +22,27 @@
|
||||
{% if share_count %}
|
||||
<span class="badge badge--leaf">{{ share_count }}</span>
|
||||
{% else %}
|
||||
<span class="badge">nobody</span>
|
||||
<span class="badge">{{ t("nobody") }}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<p class="card__lede">
|
||||
They will be able to read this, and their models will find it. They cannot
|
||||
change it, delete it, or share it on — so “who can see this?” stays a
|
||||
question you can answer.
|
||||
</p>
|
||||
<p class="card__lede">{{ t("They will be able to read this, and their models will find it. They cannot change it, delete it, or share it on — so “who can see this?” stays a question you can answer.") }}</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="share-search">Find somebody</label>
|
||||
<label class="field__label" for="share-search">{{ t("Find somebody") }}</label>
|
||||
<input class="input" id="share-search" type="search" name="q" value="{{ q }}"
|
||||
placeholder="Name, email or group…"
|
||||
placeholder="{{ t('Name, email or group…') }}"
|
||||
hx-get="/api/library/share/{{ kind }}/{{ resource.id }}"
|
||||
hx-trigger="input changed delay:250ms, search"
|
||||
hx-target="#share-panel"
|
||||
hx-swap="outerHTML">
|
||||
{% if truncated %}
|
||||
<p class="field__hint">
|
||||
Showing the first few. Type to narrow it — anything already shared stays
|
||||
listed whatever you search for.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Showing the first few. Type to narrow it — anything already shared stays listed whatever you search for.") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if groups %}
|
||||
<div class="field">
|
||||
<label class="field__label">Groups</label>
|
||||
<label class="field__label">{{ t("Groups") }}</label>
|
||||
<div class="checkbox-row">
|
||||
{% for group in groups %}
|
||||
{% set on = group.id in shared_groups %}
|
||||
@@ -69,7 +62,7 @@
|
||||
|
||||
{% if people %}
|
||||
<div class="field">
|
||||
<label class="field__label">People</label>
|
||||
<label class="field__label">{{ t("People") }}</label>
|
||||
<div class="checkbox-row">
|
||||
{% for person in people %}
|
||||
{% set on = person.id in shared_users %}
|
||||
|
||||
@@ -14,32 +14,28 @@
|
||||
|
||||
{% if is_owner %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Add to this base</h2>
|
||||
<h2 class="card__title">{{ t("Add to this base") }}</h2>
|
||||
<div class="grid grid--2">
|
||||
<form method="post" action="/api/library/documents" enctype="multipart/form-data">
|
||||
<input type="hidden" name="base_id" value="{{ base.id }}">
|
||||
<div class="field">
|
||||
<label class="field__label" for="doc-file">A file</label>
|
||||
<label class="field__label" for="doc-file">{{ t("A file") }}</label>
|
||||
<input class="input" id="doc-file" type="file" name="file" required
|
||||
accept="image/*,.pdf,.txt,.md,.csv,.json,.py,.js,.ts,.rs,.go,.sh,.sql,.yaml,.yml,.toml,.log">
|
||||
<p class="field__hint">
|
||||
Images, PDFs and text. A PDF has its text read once, now.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Images, PDFs and text. A PDF has its text read once, now.") }}</p>
|
||||
</div>
|
||||
<button class="btn btn--primary" type="submit">Upload</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Upload") }}</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/api/library/documents/link">
|
||||
<input type="hidden" name="base_id" value="{{ base.id }}">
|
||||
<div class="field">
|
||||
<label class="field__label" for="doc-url">A web page</label>
|
||||
<label class="field__label" for="doc-url">{{ t("A web page") }}</label>
|
||||
<input class="input" id="doc-url" type="url" name="url" required
|
||||
placeholder="https://example.com/article">
|
||||
<p class="field__hint">
|
||||
Fetched now and kept as text, so it survives the page changing.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Fetched now and kept as text, so it survives the page changing.") }}</p>
|
||||
</div>
|
||||
<button class="btn" type="submit">Save page</button>
|
||||
<button class="btn" type="submit">{{ t("Save page") }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@@ -48,7 +44,7 @@
|
||||
<form method="get" action="/library/knowledge/{{ base.id }}" class="btn-row"
|
||||
style="margin-bottom: var(--sp-5)">
|
||||
<input class="input" type="search" name="q" value="{{ q }}" style="flex: 1"
|
||||
placeholder="Search this base…">
|
||||
placeholder="{{ t('Search this base…') }}">
|
||||
<button class="btn" type="submit">{{ icon("search", "icon--sm") }} Search</button>
|
||||
{% if q %}<a class="btn btn--sm" href="/library/knowledge/{{ base.id }}">Clear</a>{% endif %}
|
||||
</form>
|
||||
@@ -82,7 +78,7 @@
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
{% if document.extraction_error %}
|
||||
<span class="badge badge--danger" title="{{ document.extraction_error }}">no text</span>
|
||||
<span class="badge badge--danger" title="{{ document.extraction_error }}">{{ t("no text") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
@@ -93,15 +89,15 @@
|
||||
|
||||
<form method="post" action="/api/library/bases/{{ base.id }}" style="margin-top: var(--sp-8)">
|
||||
<section class="card">
|
||||
<h2 class="card__title">This base</h2>
|
||||
<h2 class="card__title">{{ t("This base") }}</h2>
|
||||
<div class="grid grid--2">
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input" id="name" name="name" value="{{ base.name }}" maxlength="200"
|
||||
{{ 'disabled' if not is_owner }}>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="description">Description</label>
|
||||
<label class="field__label" for="description">{{ t("Description") }}</label>
|
||||
<input class="input" id="description" name="description" maxlength="2000"
|
||||
value="{{ base.description }}" {{ 'disabled' if not is_owner }}>
|
||||
</div>
|
||||
@@ -112,12 +108,12 @@
|
||||
|
||||
{% if is_owner %}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn--primary" type="submit">Save</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="btn btn--danger" type="submit"
|
||||
formaction="/api/library/bases/{{ base.id }}/delete"
|
||||
data-confirm-button="Delete “{{ base.name }}” and the {{ pager.total }} document(s) in it? Messages they were attached to keep their copies."
|
||||
data-confirm-title="Delete knowledge base">
|
||||
data-confirm-title="{{ t('Delete knowledge base') }}">
|
||||
{{ icon("trash", "icon--sm") }} Delete base
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,7 @@
|
||||
{% block heading %}Knowledge{% endblock %}
|
||||
|
||||
{% block library_content %}
|
||||
<p class="admin-lede">
|
||||
Knowledge bases are collections of documents, images and saved web pages. Keep
|
||||
them separate — one per subject, project or client — and a chat can be pointed
|
||||
at just the ones it should draw on. Sharing happens here too: share a base and
|
||||
everything in it comes with it.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Knowledge bases are collections of documents, images and saved web pages. Keep them separate — one per subject, project or client — and a chat can be pointed at just the ones it should draw on. Sharing happens here too: share a base and everything in it comes with it.") }}</p>
|
||||
|
||||
{# Links rather than a form, so a filtered view is a URL you can keep. #}
|
||||
<div class="filter-tabs" style="margin-bottom: var(--sp-4)">
|
||||
@@ -36,7 +31,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
{% if base.owner_id != user.id %}<span class="badge">shared with you</span>{% endif %}
|
||||
{% if base.owner_id != user.id %}<span class="badge">{{ t("shared with you") }}</span>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -44,27 +39,24 @@
|
||||
{% else %}
|
||||
<div class="empty">
|
||||
{{ icon("archive", "empty__mark") }}
|
||||
<h2 class="empty__title">No knowledge bases yet</h2>
|
||||
<p class="empty__text">
|
||||
Make one below, then put documents in it. A chat with no base attached
|
||||
searches everything you have; a chat pointed at one searches only that.
|
||||
</p>
|
||||
<h2 class="empty__title">{{ t("No knowledge bases yet") }}</h2>
|
||||
<p class="empty__text">{{ t("Make one below, then put documents in it. A chat with no base attached searches everything you have; a chat pointed at one searches only that.") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section class="card" style="margin-top: var(--sp-6)">
|
||||
<h2 class="card__title">New knowledge base</h2>
|
||||
<h2 class="card__title">{{ t("New knowledge base") }}</h2>
|
||||
<form method="post" action="/api/library/bases">
|
||||
<div class="grid grid--2">
|
||||
<div class="field">
|
||||
<label class="field__label" for="base-name">Name</label>
|
||||
<label class="field__label" for="base-name">{{ t("Name") }}</label>
|
||||
<input class="input" id="base-name" name="name" required maxlength="200"
|
||||
placeholder="Contracts">
|
||||
placeholder="{{ t('Contracts') }}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="base-description">Description</label>
|
||||
<label class="field__label" for="base-description">{{ t("Description") }}</label>
|
||||
<input class="input" id="base-description" name="description" maxlength="2000"
|
||||
placeholder="What belongs in here.">
|
||||
placeholder="{{ t('What belongs in here.') }}">
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn--primary" type="submit">{{ icon("plus", "icon--sm") }} Create</button>
|
||||
|
||||
@@ -14,25 +14,23 @@
|
||||
|
||||
<form method="post" action="/api/library/documents/{{ document.id }}">
|
||||
<section class="card">
|
||||
<h2 class="card__title">Details</h2>
|
||||
<h2 class="card__title">{{ t("Details") }}</h2>
|
||||
<div class="field">
|
||||
<label class="field__label" for="title">Title</label>
|
||||
<label class="field__label" for="title">{{ t("Title") }}</label>
|
||||
<input class="input" id="title" name="title" value="{{ document.title }}"
|
||||
maxlength="300" {{ 'disabled' if not is_owner }}>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="description">Description</label>
|
||||
<label class="field__label" for="description">{{ t("Description") }}</label>
|
||||
<textarea class="textarea" id="description" name="description" rows="2"
|
||||
{{ 'disabled' if not is_owner }}
|
||||
placeholder="What this is, and when it is worth reading.">{{ document.description }}</textarea>
|
||||
<p class="field__hint">
|
||||
Searched along with the contents, so a line here helps a model find it.
|
||||
</p>
|
||||
placeholder="{{ t('What this is, and when it is worth reading.') }}">{{ document.description }}</textarea>
|
||||
<p class="field__hint">{{ t("Searched along with the contents, so a line here helps a model find it.") }}</p>
|
||||
</div>
|
||||
|
||||
{% if document.base %}
|
||||
<div class="field">
|
||||
<label class="field__label" for="base_id">Knowledge base</label>
|
||||
<label class="field__label" for="base_id">{{ t("Knowledge base") }}</label>
|
||||
{# Moving a document changes who can see it, which is the point of bases.
|
||||
Only bases this person can write to are offered. #}
|
||||
<select class="select" id="base_id" name="base_id" {{ 'disabled' if not is_owner }}>
|
||||
@@ -42,7 +40,7 @@
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="field__hint">Moving it changes who can see it.</p>
|
||||
<p class="field__hint">{{ t("Moving it changes who can see it.") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -70,7 +68,7 @@
|
||||
|
||||
{% if is_owner %}
|
||||
<div class="form-actions">
|
||||
<button class="btn btn--primary" type="submit">Save</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
|
||||
{% if document.stored_name %}
|
||||
<a class="btn" href="/api/library/documents/{{ document.id }}/content">Download</a>
|
||||
{% endif %}
|
||||
@@ -78,7 +76,7 @@
|
||||
<button class="btn btn--danger" type="submit"
|
||||
formaction="/api/library/documents/{{ document.id }}/delete"
|
||||
data-confirm-button="Delete “{{ document.title }}”? Messages it was already attached to keep their copy."
|
||||
data-confirm-title="Delete document">
|
||||
data-confirm-title="{{ t('Delete document') }}">
|
||||
{{ icon("trash", "icon--sm") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -87,14 +85,14 @@
|
||||
|
||||
{% if document.is_image and document.stored_name %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Preview</h2>
|
||||
<h2 class="card__title">{{ t("Preview") }}</h2>
|
||||
<img src="/api/library/documents/{{ document.id }}/content" alt="{{ document.title }}"
|
||||
style="max-width: 100%; border-radius: var(--radius)">
|
||||
</section>
|
||||
{% elif document.extracted_text %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Text</h2>
|
||||
<p class="card__lede">What a model is given when it reads this.</p>
|
||||
<h2 class="card__title">{{ t("Text") }}</h2>
|
||||
<p class="card__lede">{{ t("What a model is given when it reads this.") }}</p>
|
||||
<div class="reasoning__body" style="max-height: 30rem">{{ document.extracted_text }}</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -14,20 +14,18 @@
|
||||
action="{{ '/api/library/notes/' ~ note.id if note else '/api/library/notes' }}">
|
||||
<section class="card">
|
||||
<div class="field">
|
||||
<label class="field__label" for="title">Title</label>
|
||||
<label class="field__label" for="title">{{ t("Title") }}</label>
|
||||
<input class="input" id="title" name="title" maxlength="300" required
|
||||
value="{{ note.title if note else '' }}"
|
||||
{{ 'disabled' if note and not is_owner }}
|
||||
placeholder="What this note is about">
|
||||
<p class="field__hint">
|
||||
Shown in search results, so it is what decides whether a model opens it.
|
||||
</p>
|
||||
placeholder="{{ t('What this note is about') }}">
|
||||
<p class="field__hint">{{ t("Shown in search results, so it is what decides whether a model opens it.") }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field__label" for="body">Body</label>
|
||||
<label class="field__label" for="body">{{ t("Body") }}</label>
|
||||
<textarea class="textarea" id="body" name="body" rows="18"
|
||||
{{ 'disabled' if note and not is_owner }}
|
||||
placeholder="Markdown.">{{ note.body if note else '' }}</textarea>
|
||||
placeholder="{{ t('Markdown.') }}">{{ note.body if note else '' }}</textarea>
|
||||
</div>
|
||||
{% if note and note.author == "model" %}
|
||||
<p class="field__hint">
|
||||
@@ -47,7 +45,7 @@
|
||||
<button class="btn btn--danger" type="submit"
|
||||
formaction="/api/library/notes/{{ note.id }}/delete"
|
||||
data-confirm-button="Delete “{{ note.title }}”?"
|
||||
data-confirm-title="Delete note">
|
||||
data-confirm-title="{{ t('Delete note') }}">
|
||||
{{ icon("trash", "icon--sm") }} Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -57,7 +55,7 @@
|
||||
|
||||
{% if note and body_html %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">Rendered</h2>
|
||||
<h2 class="card__title">{{ t("Rendered") }}</h2>
|
||||
<div class="msg__body">{{ body_html|safe }}</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -9,11 +9,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block library_content %}
|
||||
<p class="admin-lede">
|
||||
Longer things worth keeping between conversations. A model writes these itself
|
||||
when it works something out, and you can edit or delete any of them — they are
|
||||
yours, not its.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Longer things worth keeping between conversations. A model writes these itself when it works something out, and you can edit or delete any of them — they are yours, not its.") }}</p>
|
||||
|
||||
{#
|
||||
Two views, as links, so a filtered list is a real URL you can keep -- the same
|
||||
@@ -26,7 +22,7 @@
|
||||
</div>
|
||||
<form method="get" action="/library/notes" class="btn-row" style="margin-bottom: var(--sp-5)">
|
||||
<input class="input" type="search" name="q" value="{{ q }}" style="flex: 1"
|
||||
placeholder="Search notes…">
|
||||
placeholder="{{ t('Search notes…') }}">
|
||||
<button class="btn" type="submit">{{ icon("search", "icon--sm") }} Search</button>
|
||||
{% if q %}<a class="btn btn--sm" href="/library/notes">Clear</a>{% endif %}
|
||||
</form>
|
||||
@@ -53,8 +49,8 @@
|
||||
<div class="text-xs faint">{{ note.body[:160] }}{{ "…" if note.body|length > 160 }}</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
{% if note.author == "model" %}<span class="badge badge--leaf">written by a model</span>{% endif %}
|
||||
{% if note.owner_id != user.id %}<span class="badge">shared</span>{% endif %}
|
||||
{% if note.author == "model" %}<span class="badge badge--leaf">{{ t("written by a model") }}</span>{% endif %}
|
||||
{% if note.owner_id != user.id %}<span class="badge">{{ t("shared") }}</span>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -15,33 +15,30 @@
|
||||
<section class="card">
|
||||
{% if not skill %}
|
||||
<div class="field">
|
||||
<label class="field__label" for="name">Name</label>
|
||||
<label class="field__label" for="name">{{ t("Name") }}</label>
|
||||
<input class="input mono" id="name" name="name" required maxlength="60"
|
||||
placeholder="weekly-report" pattern="[a-zA-Z0-9 _-]+">
|
||||
<p class="field__hint">
|
||||
Lowercase letters, numbers and hyphens. This is how a model asks for it,
|
||||
and it cannot be changed later.
|
||||
</p>
|
||||
placeholder="{{ t('weekly-report') }}" pattern="[a-zA-Z0-9 _-]+">
|
||||
<p class="field__hint">{{ t("Lowercase letters, numbers and hyphens. This is how a model asks for it, and it cannot be changed later.") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="description">When to use it</label>
|
||||
<label class="field__label" for="description">{{ t("When to use it") }}</label>
|
||||
<textarea class="textarea" id="description" name="description" rows="2" required
|
||||
{{ 'disabled' if skill and not is_owner }}
|
||||
placeholder="When the user asks for the weekly report.">{{ skill.description if skill else '' }}</textarea>
|
||||
placeholder="{{ t('When the user asks for the weekly report.') }}">{{ skill.description if skill else '' }}</textarea>
|
||||
<p class="field__hint">
|
||||
<strong>The load-bearing field.</strong> This one line is all a model
|
||||
<strong>{{ t("The load-bearing field.") }}</strong> This one line is all a model
|
||||
sees until it opens the skill, so it has to say when the skill applies —
|
||||
not what it does.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="body">Instructions</label>
|
||||
<label class="field__label" for="body">{{ t("Instructions") }}</label>
|
||||
<textarea class="textarea" id="body" name="body" rows="18"
|
||||
{{ 'disabled' if skill and not is_owner }}
|
||||
placeholder="Markdown. Steps, conventions, things to avoid.">{{ skill.body if skill else '' }}</textarea>
|
||||
placeholder="{{ t('Markdown. Steps, conventions, things to avoid.') }}">{{ skill.body if skill else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
{% if skill %}
|
||||
@@ -49,11 +46,9 @@
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if skill.enabled }}
|
||||
{{ 'disabled' if not is_owner }}>
|
||||
<span>Offer this skill to models</span>
|
||||
<span>{{ t("Offer this skill to models") }}</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Turned off, it stays here but disappears from the list the model sees.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Turned off, it stays here but disappears from the list the model sees.") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -61,7 +56,7 @@
|
||||
<div class="alert alert--warning">
|
||||
{{ icon("sparkle", "alert__icon") }}
|
||||
<div>
|
||||
<strong>A model wrote this version.</strong>
|
||||
<strong>{{ t("A model wrote this version.") }}</strong>
|
||||
<div class="text-sm" style="margin-top: var(--sp-1)">
|
||||
Worth reading before you rely on it. A model that has just read a web
|
||||
page can be talked into things by that page, and a skill persists.
|
||||
@@ -82,7 +77,7 @@
|
||||
<button class="btn btn--danger" type="submit"
|
||||
formaction="/api/library/skills/{{ skill.id }}/delete"
|
||||
data-confirm-button="Delete the skill “{{ skill.name }}” and all its history?"
|
||||
data-confirm-title="Delete skill">
|
||||
data-confirm-title="{{ t('Delete skill') }}">
|
||||
{{ icon("trash", "icon--sm") }} Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
@@ -93,19 +88,16 @@
|
||||
{% if skill and revisions %}
|
||||
<section class="card">
|
||||
<h2 class="card__title">History <span class="badge">{{ revisions|length }}</span></h2>
|
||||
<p class="card__lede">
|
||||
What this skill said before each change. This is the whole safety story for a
|
||||
model editing its own instructions: not a gate, but a record and a way back.
|
||||
</p>
|
||||
<p class="card__lede">{{ t("What this skill said before each change. This is the whole safety story for a model editing its own instructions: not a gate, but a record and a way back.") }}</p>
|
||||
<ul class="model-list">
|
||||
{% for revision in revisions %}
|
||||
<li class="model-list__item">
|
||||
<div style="min-width: 0">
|
||||
<strong>{{ revision.created_at.strftime("%Y-%m-%d %H:%M") }}</strong>
|
||||
{% if revision.author == "model" %}
|
||||
<span class="badge badge--leaf">model</span>
|
||||
<span class="badge badge--leaf">{{ t("model") }}</span>
|
||||
{% else %}
|
||||
<span class="badge">you</span>
|
||||
<span class="badge">{{ t("you") }}</span>
|
||||
{% endif %}
|
||||
{% if revision.note %}<div class="text-xs faint">{{ revision.note }}</div>{% endif %}
|
||||
<div class="text-xs faint">{{ revision.body[:200] }}{{ "…" if revision.body|length > 200 }}</div>
|
||||
@@ -113,8 +105,8 @@
|
||||
{% if is_owner %}
|
||||
<form method="post"
|
||||
action="/api/library/skills/{{ skill.id }}/revert/{{ revision.id }}"
|
||||
data-confirm="Put the skill back to this version? The current one is kept in the history."
|
||||
data-confirm-label="Revert" data-confirm-danger="false">
|
||||
data-confirm="{{ t('Put the skill back to this version? The current one is kept in the history.') }}"
|
||||
data-confirm-label="{{ t('Revert') }}" data-confirm-danger="false">
|
||||
<button class="btn btn--sm" type="submit">{{ icon("refresh", "icon--sm") }} Revert</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
@@ -9,12 +9,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block library_content %}
|
||||
<p class="admin-lede">
|
||||
Saved procedures. Every enabled skill's name and description are shown to the
|
||||
model on each turn; it reads the instructions only when it decides one
|
||||
applies. A model may write and revise its own — every version is kept, so any
|
||||
change can be read and undone.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Saved procedures. Every enabled skill's name and description are shown to the model on each turn; it reads the instructions only when it decides one applies. A model may write and revise its own — every version is kept, so any change can be read and undone.") }}</p>
|
||||
|
||||
{#
|
||||
Two views, as links, so a filtered list is a real URL you can keep -- the same
|
||||
@@ -26,7 +21,7 @@
|
||||
</div>
|
||||
<form method="get" action="/library/skills" class="btn-row" style="margin-bottom: var(--sp-5)">
|
||||
<input class="input" type="search" name="q" value="{{ q }}" style="flex: 1"
|
||||
placeholder="Search skills…">
|
||||
placeholder="{{ t('Search skills…') }}">
|
||||
<button class="btn" type="submit">{{ icon("search", "icon--sm") }} Search</button>
|
||||
{% if q %}<a class="btn btn--sm" href="/library/skills">Clear</a>{% endif %}
|
||||
</form>
|
||||
@@ -53,9 +48,9 @@
|
||||
<div class="text-xs faint">{{ skill.description }}</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
{% if not skill.enabled %}<span class="badge">off</span>{% endif %}
|
||||
{% if skill.author == "model" %}<span class="badge badge--leaf">written by a model</span>{% endif %}
|
||||
{% if skill.owner_id != user.id %}<span class="badge">shared</span>{% endif %}
|
||||
{% if not skill.enabled %}<span class="badge">{{ t("off") }}</span>{% endif %}
|
||||
{% if skill.author == "model" %}<span class="badge badge--leaf">{{ t("written by a model") }}</span>{% endif %}
|
||||
{% if skill.owner_id != user.id %}<span class="badge">{{ t("shared") }}</span>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
hx-sync="this:drop">
|
||||
<span class="visually-hidden" role="status">Loading earlier messages</span>
|
||||
<span class="visually-hidden" role="status">{{ t("Loading earlier messages") }}</span>
|
||||
<div class="history-sentinel__shape" aria-hidden="true">
|
||||
<div class="skeleton skeleton--line"></div>
|
||||
<div class="skeleton skeleton--line skeleton--short"></div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<span class="text-xs faint">{{ item.summary }}</span>
|
||||
</span>
|
||||
{% if not item.row.enabled %}
|
||||
<span class="badge badge--warning">paused</span>
|
||||
<span class="badge badge--warning">{{ t("paused") }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -59,11 +59,8 @@
|
||||
{% if not messages %}
|
||||
<div class="thread__intro">
|
||||
{{ mark(cls="empty__mark", uid="messages") }}
|
||||
<h2 class="empty__title">Nothing said yet</h2>
|
||||
<p class="empty__text">
|
||||
One conversation that keeps going. Anything scheduled to write here
|
||||
will arrive in it.
|
||||
</p>
|
||||
<h2 class="empty__title">{{ t("Nothing said yet") }}</h2>
|
||||
<p class="empty__text">{{ t("One conversation that keeps going. Anything scheduled to write here will arrive in it.") }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -79,7 +76,7 @@
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
hx-sync="this:drop">
|
||||
<span class="visually-hidden" role="status">Loading earlier messages</span>
|
||||
<span class="visually-hidden" role="status">{{ t("Loading earlier messages") }}</span>
|
||||
<div class="history-sentinel__shape" aria-hidden="true">
|
||||
<div class="skeleton skeleton--line"></div>
|
||||
<div class="skeleton skeleton--line skeleton--short"></div>
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
{{ brand.name }} answers from your server, so there is nothing to read until it can
|
||||
be reached again.
|
||||
</p>
|
||||
<button class="btn btn--primary" type="button" onclick="window.location.reload()">
|
||||
Try again
|
||||
</button>
|
||||
<button class="btn btn--primary" type="button" onclick="window.location.reload()">{{ t("Try again") }}</button>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<span class="nav-item__label" id="chat-link-label-{{ chat_item.id }}">{{ chat_item.title }}</span>
|
||||
{# Toggled out of band by the unread poll; see /api/chats/unread. #}
|
||||
<span id="unread-{{ chat_item.id }}" class="unread-dot"
|
||||
{{ '' if chat_item.unread else 'hidden' }} title="New reply"></span>
|
||||
{{ '' if chat_item.unread else 'hidden' }} title="{{ t('New reply') }}"></span>
|
||||
</a>
|
||||
<span class="nav-item__actions">
|
||||
{# Works from any page carrying the sidebar, not only from inside the chat.
|
||||
@@ -22,9 +22,9 @@
|
||||
<button class="btn btn--icon btn--sm" type="button"
|
||||
hx-patch="/api/chats/{{ chat_item.id }}" hx-swap="none"
|
||||
data-prompt="What should this chat be called?"
|
||||
data-prompt-title="Rename chat" data-prompt-field="title"
|
||||
data-prompt-title="{{ t('Rename chat') }}" data-prompt-field="title"
|
||||
data-prompt-value="{{ chat_item.title }}"
|
||||
aria-label="Rename chat" title="Rename chat">
|
||||
aria-label="{{ t('Rename chat') }}" title="{{ t('Rename chat') }}">
|
||||
{{ icon("pencil", "icon--sm") }}
|
||||
</button>
|
||||
{#
|
||||
@@ -44,9 +44,9 @@
|
||||
<button class="btn btn--icon btn--sm" type="button"
|
||||
hx-delete="/api/chats/{{ chat_item.id }}"
|
||||
hx-confirm="Delete “{{ chat_item.title }}”? This cannot be undone."
|
||||
data-confirm-title="Delete chat"
|
||||
data-confirm-title="{{ t('Delete chat') }}"
|
||||
hx-swap="none"
|
||||
aria-label="Delete chat">
|
||||
aria-label="{{ t('Delete chat') }}">
|
||||
{{ icon("trash", "icon--sm") }}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
@@ -22,27 +22,27 @@
|
||||
folder seeds. Without this the only way into a folder is to make the
|
||||
chat somewhere else and move it. #}
|
||||
<a class="btn btn--icon btn--sm" href="/chat?folder={{ folder.id }}"
|
||||
aria-label="New chat in this folder" title="New chat here">
|
||||
aria-label="{{ t('New chat in this folder') }}" title="{{ t('New chat here') }}">
|
||||
{{ icon("plus", "icon--sm") }}
|
||||
</a>
|
||||
<button class="btn btn--icon btn--sm" type="button"
|
||||
hx-patch="/api/folders/{{ folder.id }}" hx-swap="none"
|
||||
data-prompt="What should this folder be called?"
|
||||
data-prompt-title="Rename folder" data-prompt-field="name"
|
||||
data-prompt-title="{{ t('Rename folder') }}" data-prompt-field="name"
|
||||
data-prompt-value="{{ folder.name }}"
|
||||
aria-label="Rename folder" title="Rename folder">
|
||||
aria-label="{{ t('Rename folder') }}" title="{{ t('Rename folder') }}">
|
||||
{{ icon("pencil", "icon--sm") }}
|
||||
</button>
|
||||
<a class="btn btn--icon btn--sm" href="/folders/{{ folder.id }}"
|
||||
aria-label="Folder settings" title="Folder settings">
|
||||
aria-label="{{ t('Folder settings') }}" title="{{ t('Folder settings') }}">
|
||||
{{ icon("sliders", "icon--sm") }}
|
||||
</a>
|
||||
<button class="btn btn--icon btn--sm" type="button"
|
||||
hx-delete="/api/folders/{{ folder.id }}"
|
||||
hx-confirm="Delete the folder “{{ folder.name }}”? Chats inside it are kept."
|
||||
data-confirm-title="Delete folder"
|
||||
data-confirm-title="{{ t('Delete folder') }}"
|
||||
hx-swap="none"
|
||||
aria-label="Delete folder">
|
||||
aria-label="{{ t('Delete folder') }}">
|
||||
{{ icon("trash", "icon--sm") }}
|
||||
</button>
|
||||
</span>
|
||||
@@ -67,7 +67,7 @@
|
||||
{% endfor %}
|
||||
|
||||
{% if not shown and not listed %}
|
||||
<p class="nav-empty">Empty</p>
|
||||
<p class="nav-empty">{{ t("Empty") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
request goes out; see ui.js for why this is not htmx's own hx-prompt. #}
|
||||
<button class="btn btn--icon" hx-post="/api/folders" hx-swap="none"
|
||||
data-prompt="What should this folder be called?"
|
||||
data-prompt-title="New folder" data-prompt-field="name"
|
||||
data-prompt-title="{{ t('New folder') }}" data-prompt-field="name"
|
||||
data-prompt-label="Create"
|
||||
aria-label="New folder" title="New folder">
|
||||
aria-label="{{ t('New folder') }}" title="{{ t('New folder') }}">
|
||||
{{ icon("folder") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#}
|
||||
<div class="sidebar__actions-rail">
|
||||
<button class="btn btn--icon sidebar__close" type="button"
|
||||
aria-label="Close sidebar" data-toggle="#sidebar">
|
||||
aria-label="{{ t('Close sidebar') }}" data-toggle="#sidebar">
|
||||
{{ icon("x") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
{% if can.get("reports.use") %}
|
||||
<a class="nav-item {{ 'is-active' if section == 'reports' }}" href="/reports">
|
||||
{{ icon("archive", "icon--sm") }}
|
||||
<span class="nav-item__label">Reports</span>
|
||||
<span class="nav-item__label">{{ t("Reports") }}</span>
|
||||
{# Toggled out of band by the unread poll, exactly as a chat row's is. #}
|
||||
<span id="unread-reports" class="unread-dot"
|
||||
{{ '' if unread_reports else 'hidden' }} title="New reports"></span>
|
||||
{{ '' if unread_reports else 'hidden' }} title="{{ t('New reports') }}"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
#}
|
||||
<a class="nav-item {{ 'is-active' if section == 'messages' }}" href="/messages">
|
||||
{{ icon("chat", "icon--sm") }}
|
||||
<span class="nav-item__label">Messages</span>
|
||||
<span class="nav-item__label">{{ t("Messages") }}</span>
|
||||
<span id="unread-messages" class="unread-dot"
|
||||
{{ '' if unread_messages else 'hidden' }} title="New messages"></span>
|
||||
{{ '' if unread_messages else 'hidden' }} title="{{ t('New messages') }}"></span>
|
||||
</a>
|
||||
|
||||
{% if can.get("schedule.use") %}
|
||||
<a class="nav-item {{ 'is-active' if section == 'scheduled' }}" href="/scheduled">
|
||||
{{ icon("clock", "icon--sm") }}
|
||||
<span class="nav-item__label">Scheduled</span>
|
||||
<span class="nav-item__label">{{ t("Scheduled") }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
`app.js:syncToggles` writes the honest value on load and on every change.
|
||||
#}
|
||||
<button class="btn btn--icon sidebar-toggle" type="button"
|
||||
aria-label="Toggle sidebar" aria-controls="sidebar"
|
||||
aria-label="{{ t('Toggle sidebar') }}" aria-controls="sidebar"
|
||||
data-toggle="#sidebar">
|
||||
{{ icon("sidebar") }}
|
||||
</button>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
the two radios are not inside a form, so what htmx would gather is worth
|
||||
not depending on.
|
||||
#}
|
||||
<div class="segmented segmented--grow" role="group" aria-label="Which chats to show">
|
||||
<div class="segmented segmented--grow" role="group" aria-label="{{ t('Which chats to show') }}">
|
||||
{% for option, label, glyph in [("chat", "Chats", "chat"), ("agent", "Agents", "terminal")] %}
|
||||
<label class="segmented__option">
|
||||
<input type="radio" name="sidebar_kind" value="{{ option }}"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{% if instance_voice is defined and instance_voice %}
|
||||
<option value="">Instance default — {{ instance_voice }}</option>
|
||||
{% else %}
|
||||
<option value="">Endpoint default</option>
|
||||
<option value="">{{ t("Endpoint default") }}</option>
|
||||
{% endif %}
|
||||
{% for voice in voices %}
|
||||
<option value="{{ voice }}" {{ 'selected' if voice == selected }}>{{ voice }}</option>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div hidden hx-get="/api/chats/unread" hx-trigger="every 10s"
|
||||
hx-swap="none"></div>
|
||||
|
||||
<nav class="sidebar__scroll" aria-label="Chats">
|
||||
<nav class="sidebar__scroll" aria-label="{{ t('Chats') }}">
|
||||
{% include "partials/_sidebar_pinned.html" %}
|
||||
{% include "partials/_sidebar_sections.html" %}
|
||||
{% include "partials/_sidebar_tree.html" %}
|
||||
@@ -48,21 +48,21 @@
|
||||
{% if can.get("library.use") %}
|
||||
<a class="nav-item" href="/library/knowledge">
|
||||
{{ icon("archive", "icon--sm") }}
|
||||
<span class="nav-item__label">Library</span>
|
||||
<span class="nav-item__label">{{ t("Library") }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can.get("agent.ssh") %}
|
||||
<a class="nav-item" href="/agents">
|
||||
{{ icon("server", "icon--sm") }}
|
||||
<span class="nav-item__label">Connections</span>
|
||||
<span class="nav-item__label">{{ t("Connections") }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a class="nav-item" href="/settings">
|
||||
{{ icon("user", "icon--sm") }}
|
||||
<span class="nav-item__label">{{ user.name }}</span>
|
||||
{% if user.is_admin %}<span class="badge badge--leaf">admin</span>{% endif %}
|
||||
{% if user.is_admin %}<span class="badge badge--leaf">{{ t("admin") }}</span>{% endif %}
|
||||
</a>
|
||||
|
||||
<div class="sidebar__tools">
|
||||
@@ -83,22 +83,22 @@
|
||||
#}
|
||||
<button class="btn btn--icon" type="button" data-install-app hidden
|
||||
onclick="window.lembas.promptInstall()"
|
||||
aria-label="Install as an app" title="Install as an app">
|
||||
aria-label="{{ t('Install as an app') }}" title="{{ t('Install as an app') }}">
|
||||
{{ icon("arrow-down") }}
|
||||
</button>
|
||||
{% if user.is_admin %}
|
||||
<a class="btn btn--icon" href="/admin" aria-label="Admin settings" title="Admin settings">
|
||||
<a class="btn btn--icon" href="/admin" aria-label="{{ t('Admin settings') }}" title="{{ t('Admin settings') }}">
|
||||
{{ icon("shield") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button class="btn btn--icon" type="button" data-theme-toggle
|
||||
aria-label="Switch theme" title="Switch theme">
|
||||
aria-label="{{ t('Switch theme') }}" title="{{ t('Switch theme') }}">
|
||||
<span class="theme-icon theme-icon--dark">{{ icon("moon") }}</span>
|
||||
<span class="theme-icon theme-icon--light">{{ icon("sun") }}</span>
|
||||
</button>
|
||||
<span class="spacer"></span>
|
||||
<form method="post" action="/auth/logout">
|
||||
<button class="btn btn--icon" type="submit" aria-label="Sign out" title="Sign out">
|
||||
<button class="btn btn--icon" type="submit" aria-label="{{ t('Sign out') }}" title="{{ t('Sign out') }}">
|
||||
{{ icon("logout") }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p class="text-sm faint">
|
||||
Filed
|
||||
<time datetime="{{ report.created_at.isoformat() }}">
|
||||
{{ report.created_at.strftime("%d %B %Y at %H:%M") }}
|
||||
{{ stamp(report.created_at, "%d %B %Y at %H:%M") }}
|
||||
</time>
|
||||
{%- if report.source == "schedule" %} by a schedule
|
||||
{%- elif report.source == "chat" %} from a chat
|
||||
@@ -23,7 +23,7 @@
|
||||
{% if report.error %}
|
||||
<div class="alert alert--warning" style="margin-top: var(--sp-4)">
|
||||
{{ icon("warning", "alert__icon") }}
|
||||
<span><strong>This run did not finish cleanly.</strong> {{ report.error }}</span>
|
||||
<span><strong>{{ t("This run did not finish cleanly.") }}</strong> {{ report.error }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{% if report.owner_id == user.id %}
|
||||
<div class="btn-row" style="margin-top: var(--sp-6)">
|
||||
<form method="post" action="/api/reports/{{ report.id }}/delete"
|
||||
data-confirm="Delete this report? It cannot be brought back.">
|
||||
data-confirm="{{ t('Delete this report? It cannot be brought back.') }}">
|
||||
<button class="btn btn--sm btn--danger" type="submit">
|
||||
{{ icon("trash", "icon--sm") }} Delete
|
||||
</button>
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
{% block heading %}Reports{% endblock %}
|
||||
|
||||
{% block reports_content %}
|
||||
<p class="admin-lede">
|
||||
Finished work, filed to be read later. A model writes one when you ask for it
|
||||
or when it finishes something worth keeping, and anything running on a
|
||||
schedule leaves its result here. Nothing on this page can be replied to.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Finished work, filed to be read later. A model writes one when you ask for it or when it finishes something worth keeping, and anything running on a schedule leaves its result here. Nothing on this page can be replied to.") }}</p>
|
||||
|
||||
{# Reports became shareable and this filter arrived with them: a feed that
|
||||
quietly grew somebody else's work with no way to see only theirs would be
|
||||
@@ -22,7 +18,7 @@
|
||||
|
||||
<form method="get" action="/reports" class="btn-row" style="margin-bottom: var(--sp-5)">
|
||||
<input class="input" type="search" name="q" value="{{ q }}" style="flex: 1"
|
||||
placeholder="Search reports…">
|
||||
placeholder="{{ t('Search reports…') }}">
|
||||
<button class="btn" type="submit">{{ icon("search", "icon--sm") }} Search</button>
|
||||
{% if q %}<a class="btn btn--sm" href="/reports">Clear</a>{% endif %}
|
||||
</form>
|
||||
@@ -56,9 +52,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
{% if report.error %}<span class="badge badge--warning">failed</span>{% endif %}
|
||||
{% if report.source == "schedule" %}<span class="badge">scheduled</span>{% endif %}
|
||||
{% if report.unread %}<span class="badge badge--leaf">new</span>{% endif %}
|
||||
{% if report.error %}<span class="badge badge--warning">{{ t("failed") }}</span>{% endif %}
|
||||
{% if report.source == "schedule" %}<span class="badge">{{ t("scheduled") }}</span>{% endif %}
|
||||
{% if report.unread %}<span class="badge badge--leaf">{{ t("new") }}</span>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
@@ -13,26 +13,23 @@
|
||||
card follows.
|
||||
#}
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-title">Name</label>
|
||||
<label class="field__label" for="schedule-title">{{ t("Name") }}</label>
|
||||
<input class="input" id="schedule-title" name="title" required maxlength="200"
|
||||
value="{{ form.title }}"
|
||||
placeholder="Monday build check">
|
||||
<p class="field__hint">What this appears as in the list.</p>
|
||||
placeholder="{{ t('Monday build check') }}">
|
||||
<p class="field__hint">{{ t("What this appears as in the list.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-instruction">What should happen</label>
|
||||
<label class="field__label" for="schedule-instruction">{{ t("What should happen") }}</label>
|
||||
<textarea class="input" id="schedule-instruction" name="instruction" rows="5" required
|
||||
placeholder="Check whether the build is passing and summarise anything that broke since last week."
|
||||
placeholder="{{ t('Check whether the build is passing and summarise anything that broke since last week.') }}"
|
||||
>{{ form.instruction }}</textarea>
|
||||
<p class="field__hint">
|
||||
Written for a model that will read it with no conversation around it, so say
|
||||
the whole thing. Nobody will be there to answer a question about it.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("Written for a model that will read it with no conversation around it, so say the whole thing. Nobody will be there to answer a question about it.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-target">Where the result goes</label>
|
||||
<label class="field__label" for="schedule-target">{{ t("Where the result goes") }}</label>
|
||||
<select class="select" id="schedule-target" name="target">
|
||||
{% for value, label in targets %}
|
||||
<option value="{{ value }}" {{ 'selected' if form.target == value }}>
|
||||
@@ -43,26 +40,26 @@
|
||||
</div>
|
||||
|
||||
<fieldset class="schedule-repeat">
|
||||
<legend class="field__label">When</legend>
|
||||
<legend class="field__label">{{ t("When") }}</legend>
|
||||
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="repeat" value="once"
|
||||
{{ 'checked' if form.repeat == 'once' }}>
|
||||
<span>Once</span>
|
||||
<span>{{ t("Once") }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="repeat" value="every"
|
||||
{{ 'checked' if form.repeat == 'every' }}>
|
||||
<span>Every so often</span>
|
||||
<span>{{ t("Every so often") }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="repeat" value="calendar"
|
||||
{{ 'checked' if form.repeat == 'calendar' }}>
|
||||
<span>On particular days</span>
|
||||
<span>{{ t("On particular days") }}</span>
|
||||
</label>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-start">Starting</label>
|
||||
<label class="field__label" for="schedule-start">{{ t("Starting") }}</label>
|
||||
<div class="btn-row">
|
||||
<input class="input" type="date" id="schedule-start" name="start_date">
|
||||
<input class="input" type="time" name="start_time" value="09:00">
|
||||
@@ -74,7 +71,7 @@
|
||||
</div>
|
||||
|
||||
<div class="field" data-repeat="every">
|
||||
<label class="field__label" for="schedule-every">Repeat every</label>
|
||||
<label class="field__label" for="schedule-every">{{ t("Repeat every") }}</label>
|
||||
<div class="btn-row">
|
||||
<input class="input" type="number" id="schedule-every" name="every_amount"
|
||||
min="1" value="{{ form.every_amount }}" style="max-width: 8rem">
|
||||
@@ -87,7 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div class="field" data-repeat="calendar">
|
||||
<label class="field__label">On these days</label>
|
||||
<label class="field__label">{{ t("On these days") }}</label>
|
||||
<div class="checkbox-row">
|
||||
{% for index, name in weekday_names %}
|
||||
<label class="checkbox">
|
||||
@@ -97,32 +94,29 @@
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="field__hint">Leave all of them unticked for every day.</p>
|
||||
<p class="field__hint">{{ t("Leave all of them unticked for every day.") }}</p>
|
||||
|
||||
<label class="field__label" for="schedule-times">At</label>
|
||||
<label class="field__label" for="schedule-times">{{ t("At") }}</label>
|
||||
<input class="input" id="schedule-times" name="times"
|
||||
value="{{ form.times or '09:00' }}" placeholder="09:00, 17:00">
|
||||
<p class="field__hint">
|
||||
One or more times, separated by commas. These are wall-clock times: 09:00
|
||||
stays 09:00 when the clocks change.
|
||||
</p>
|
||||
<p class="field__hint">{{ t("One or more times, separated by commas. These are wall-clock times: 09:00 stays 09:00 when the clocks change.") }}</p>
|
||||
|
||||
<label class="field__label" for="schedule-monthdays">Only on these dates</label>
|
||||
<label class="field__label" for="schedule-monthdays">{{ t("Only on these dates") }}</label>
|
||||
<input class="input" id="schedule-monthdays" name="month_days"
|
||||
value="{{ form.month_days }}" placeholder="1, 15">
|
||||
<p class="field__hint">Days of the month, if you want it narrower. Optional.</p>
|
||||
<p class="field__hint">{{ t("Days of the month, if you want it narrower. Optional.") }}</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-count">Stop after</label>
|
||||
<label class="field__label" for="schedule-count">{{ t("Stop after") }}</label>
|
||||
<input class="input" type="number" id="schedule-count" name="count" min="0"
|
||||
value="{{ form.count }}" style="max-width: 10rem">
|
||||
<p class="field__hint">Number of runs. Leave it at 0 to keep going until you stop it.</p>
|
||||
<p class="field__hint">{{ t("Number of runs. Leave it at 0 to keep going until you stop it.") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-until">Or stop on</label>
|
||||
<label class="field__label" for="schedule-until">{{ t("Or stop on") }}</label>
|
||||
<input class="input" type="date" id="schedule-until" name="until">
|
||||
<p class="field__hint">Optional. Nothing runs after this date.</p>
|
||||
<p class="field__hint">{{ t("Optional. Nothing runs after this date.") }}</p>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<div class="btn-row">
|
||||
<form method="post" action="/api/schedules/{{ schedule.id }}/run">
|
||||
<button class="btn btn--sm" type="submit" title="Run it now without using up the next scheduled run">
|
||||
<button class="btn btn--sm" type="submit" title="{{ t('Run it now without using up the next scheduled run') }}">
|
||||
{{ icon("bolt", "icon--sm") }} Run now
|
||||
</button>
|
||||
</form>
|
||||
@@ -59,10 +59,7 @@
|
||||
The schedule was removed but its chat was kept, which is the default when
|
||||
somebody deletes one. Said plainly rather than left as an empty bar.
|
||||
#}
|
||||
<p class="nav-empty">
|
||||
This chat belonged to a schedule that has been removed. It is kept as a
|
||||
record of what was done.
|
||||
</p>
|
||||
<p class="nav-empty">{{ t("This chat belonged to a schedule that has been removed. It is kept as a record of what was done.") }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,22 +12,19 @@
|
||||
<form method="post" action="/api/schedules/{{ schedule.id }}" class="card">
|
||||
{% include "schedules/_form.html" %}
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save</button>
|
||||
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
|
||||
<a class="btn" href="/scheduled">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card__title">Remove</h2>
|
||||
<p class="card__lede">
|
||||
Stops it running. Its chat is kept by default and becomes an ordinary one,
|
||||
so the transcript of everything it has already done stays where it is.
|
||||
</p>
|
||||
<h2 class="card__title">{{ t("Remove") }}</h2>
|
||||
<p class="card__lede">{{ t("Stops it running. Its chat is kept by default and becomes an ordinary one, so the transcript of everything it has already done stays where it is.") }}</p>
|
||||
<form method="post" action="/api/schedules/{{ schedule.id }}/delete"
|
||||
data-confirm="Remove this schedule? It will stop running.">
|
||||
data-confirm="{{ t('Remove this schedule? It will stop running.') }}">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="keep_chat" value="1" checked>
|
||||
<span>Keep its chat</span>
|
||||
<span>{{ t("Keep its chat") }}</span>
|
||||
</label>
|
||||
<button class="btn btn--danger btn--sm" type="submit">
|
||||
{{ icon("trash", "icon--sm") }} Remove schedule
|
||||
|
||||
@@ -11,19 +11,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block schedules_content %}
|
||||
<p class="admin-lede">
|
||||
Work that runs on its own, whether or not you are here. Each one has its own
|
||||
chat, and replies into it every time it comes round.
|
||||
</p>
|
||||
<p class="admin-lede">{{ t("Work that runs on its own, whether or not you are here. Each one has its own chat, and replies into it every time it comes round.") }}</p>
|
||||
|
||||
{% if not schedules %}
|
||||
<div class="empty">
|
||||
{{ icon("clock", "empty__mark") }}
|
||||
<h2 class="empty__title">Nothing scheduled</h2>
|
||||
<p class="empty__text">
|
||||
Set something to run later — a daily summary, a check every Monday
|
||||
morning, a reminder in an hour.
|
||||
</p>
|
||||
<h2 class="empty__title">{{ t("Nothing scheduled") }}</h2>
|
||||
<p class="empty__text">{{ t("Set something to run later — a daily summary, a check every Monday morning, a reminder in an hour.") }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<ul class="model-list">
|
||||
@@ -43,13 +37,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
{% if item.row.target == "report" %}<span class="badge">files a report</span>{% endif %}
|
||||
{% if item.row.target == "report" %}<span class="badge">{{ t("files a report") }}</span>{% endif %}
|
||||
{# Which of these nobody typed. A schedule is invisible until it fires, so
|
||||
the list is where a model's decision is checkable at all -- and the
|
||||
reader needs to be able to tell one apart from their own without
|
||||
opening it. #}
|
||||
{% if item.row.origin == "model" %}<span class="badge">set up for you</span>{% endif %}
|
||||
{% if not item.row.enabled %}<span class="badge badge--warning">paused</span>{% endif %}
|
||||
{% if item.row.origin == "model" %}<span class="badge">{{ t("set up for you") }}</span>{% endif %}
|
||||
{% if not item.row.enabled %}<span class="badge badge--warning">{{ t("paused") }}</span>{% endif %}
|
||||
<a class="btn btn--sm" href="/scheduled/{{ item.row.id }}/edit">Edit</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
#}
|
||||
<form method="post" action="/api/schedules/describe" class="card">
|
||||
<div class="field">
|
||||
<label class="field__label" for="describe">Say it in your own words</label>
|
||||
<label class="field__label" for="describe">{{ t("Say it in your own words") }}</label>
|
||||
<textarea class="input" id="describe" name="request" rows="3" required
|
||||
placeholder="Every Monday morning, check whether the build is passing and write me a report."
|
||||
placeholder="{{ t('Every Monday morning, check whether the build is passing and write me a report.') }}"
|
||||
>{{ described }}</textarea>
|
||||
<p class="field__hint">
|
||||
Times are read as {{ timezone }}. Say how often, and what should happen.
|
||||
@@ -59,9 +59,9 @@
|
||||
|
||||
{% if models %}
|
||||
<div class="field">
|
||||
<label class="field__label" for="schedule-model">Model</label>
|
||||
<label class="field__label" for="schedule-model">{{ t("Model") }}</label>
|
||||
<select class="select" id="schedule-model" name="model_id">
|
||||
<option value="">Whatever is default at the time</option>
|
||||
<option value="">{{ t("Whatever is default at the time") }}</option>
|
||||
{% for model in models %}
|
||||
<option value="{{ model.model_id }}">{{ model.display_name }}</option>
|
||||
{% endfor %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user