Split the model admin into a list and a page per model
/admin/models rendered a full edit form for every model. With eight that
was merely long; with a hundred it was unusable, which is the report.
The list is now compact rows only -- avatar, name, badges, position,
reorder buttons, Edit link -- with search across id and display name,
filter tabs (All / Enabled / Disabled / Pinned / Restricted, each with a
count), a connection filter, and pagination at 40. Filters are links, so
a filtered view is a real URL you can keep. Editing moved to
/admin/models/{id}/edit, one model per page, with Previous/Next links so
a freshly imported connection can be tidied without returning to the
list each time.
Measured with 128 models: the list is 73 KB showing 40 rows over 4
pages, and a detail page is 17 KB. The old page would have rendered all
128 forms into one response.
Reordering needed rethinking at that size too. Up/down is fine for
nudging a model one place but hopeless for moving it sixty, so the
detail page has a position field you type into; the value is clamped and
a non-numeric one is ignored rather than throwing. The move buttons take
a `back` field so they return to whatever filtered, paginated view they
were pressed on instead of dumping you at page 1.
Also adds a select-all checkbox for the bulk bar, scoped to a container
selector rather than the page so a future list can carry more than one.
212 tests, ruff clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ saved }}</span></div>
|
||||
{% endif %}
|
||||
|
||||
{% if not models %}
|
||||
{% if not total %}
|
||||
<div class="empty" style="padding: var(--sp-10) 0">
|
||||
{{ icon("server", "empty__mark") }}
|
||||
<p class="empty__text">
|
||||
@@ -27,8 +27,50 @@
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
{# Filters are links, so a filtered view is a real URL you can keep or share. #}
|
||||
<div class="filter-bar">
|
||||
<div class="filter-tabs">
|
||||
{% for key, label in filters.items() %}
|
||||
<a class="filter-tab {{ 'is-active' if key == active_filter }}"
|
||||
href="/admin/models?filter={{ key }}{% if q %}&q={{ q|urlencode }}{% endif %}{% if connection_id %}&connection={{ connection_id }}{% endif %}">
|
||||
{{ label }} <span class="filter-tab__count">{{ counts[key] }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{% for conn in connections %}
|
||||
<option value="{{ conn.id }}" {{ 'selected' if conn.id == connection_id }}>
|
||||
{{ conn.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn" type="submit">{{ icon("search", "icon--sm") }} Filter</button>
|
||||
{% if q or connection_id or active_filter != "all" %}
|
||||
<a class="btn btn--ghost" href="/admin/models">Clear</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if not models %}
|
||||
<div class="empty" style="padding: var(--sp-8) 0">
|
||||
<p class="empty__text">Nothing matches that filter.</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<form method="post" action="/admin/models/bulk">
|
||||
<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>
|
||||
</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>
|
||||
@@ -36,174 +78,74 @@
|
||||
<button class="btn btn--sm" name="action" value="private" type="submit">Restrict</button>
|
||||
</div>
|
||||
|
||||
<div class="model-rows">
|
||||
<div class="model-rows" id="model-rows">
|
||||
{% for model in models %}
|
||||
<div class="model-row {{ 'is-off' if not model.enabled }}">
|
||||
<input class="model-row__check" type="checkbox" name="model_ids" value="{{ model.id }}"
|
||||
aria-label="Select {{ model.label }}">
|
||||
|
||||
<span class="model-row__pos">{{ page_start + loop.index }}</span>
|
||||
|
||||
{{ model_avatar(model, cls="model-row__avatar") }}
|
||||
|
||||
<div class="model-row__main">
|
||||
<div class="model-row__title">
|
||||
<strong>{{ model.label }}</strong>
|
||||
<a class="model-row__name" href="/admin/models/{{ model.id }}/edit">{{ model.label }}</a>
|
||||
{% if model.model_id == default_model %}
|
||||
<span class="badge badge--gold">{{ icon("star", "icon--sm") }} default</span>
|
||||
<span class="badge badge--gold">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">{{ model.groups|length }} group{{ '' if model.groups|length == 1 else 's' }}</span>
|
||||
{% endif %}
|
||||
{% if not model.public %}<span class="badge">restricted</span>{% endif %}
|
||||
{% for name, on in (model.capabilities_json or {}).items() %}
|
||||
{% if on %}<span class="badge badge--gold">{{ name }}</span>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<code class="model-row__id">{{ model.model_id }}</code>
|
||||
<span class="text-xs faint">via {{ model.connection.name }}</span>
|
||||
<code class="model-row__id">{{ model.model_id }} · {{ model.connection.name }}</code>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
formaction="/admin/models/{{ model.id }}/move" formmethod="post"
|
||||
name="direction" value="up" {{ 'disabled' if loop.first }}>
|
||||
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"
|
||||
formaction="/admin/models/{{ model.id }}/move" formmethod="post"
|
||||
name="direction" value="down" {{ 'disabled' if loop.last }}>
|
||||
formaction="/admin/models/{{ model.id }}/move" name="direction" value="down"
|
||||
{{ 'disabled' if page == pages and loop.last }}>
|
||||
{{ icon("arrow-down", "icon--sm") }}
|
||||
</button>
|
||||
<a class="btn btn--sm" href="#model-{{ model.id }}">Edit</a>
|
||||
<a class="btn btn--sm" href="/admin/models/{{ model.id }}/edit">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2 class="admin-section-title">Model settings</h2>
|
||||
<div class="list-footer">
|
||||
<span class="text-xs faint">
|
||||
Showing {{ page_start + 1 }}–{{ page_start + models|length }} of {{ matched }}
|
||||
{%- if matched != total %} (filtered from {{ total }}){% endif %}
|
||||
</span>
|
||||
|
||||
{% for model in models %}
|
||||
<section class="card" id="model-{{ model.id }}">
|
||||
<div class="card__header">
|
||||
<div class="row" style="gap: var(--sp-3); min-width: 0">
|
||||
{{ model_avatar(model, cls="model-row__avatar") }}
|
||||
<div style="min-width: 0">
|
||||
<strong class="truncate">{{ model.label }}</strong>
|
||||
<div><code class="text-xs faint">{{ model.model_id }}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
{% if model.model_id != default_model %}
|
||||
<form method="post" action="/admin/models/{{ model.id }}/default">
|
||||
<button class="btn btn--sm" type="submit">{{ icon("star", "icon--sm") }} Make default</button>
|
||||
</form>
|
||||
{% if pages > 1 %}
|
||||
{% set base = "/admin/models?filter=" ~ active_filter ~ ("&q=" ~ q|urlencode if q else "") ~ ("&connection=" ~ connection_id if connection_id else "") %}
|
||||
<div class="btn-row">
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/models/{{ model.id }}">
|
||||
<div class="field">
|
||||
<label class="field__label" for="dn-{{ model.id }}">Display name</label>
|
||||
<input class="input" id="dn-{{ model.id }}" name="display_name"
|
||||
value="{{ model.display_name }}" placeholder="{{ model.model_id }}">
|
||||
<p class="field__hint">Shown instead of the raw model id. Leave empty to use the id.</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="desc-{{ model.id }}">Description</label>
|
||||
<textarea class="textarea" id="desc-{{ model.id }}" name="description" rows="2"
|
||||
placeholder="What is this model good at?">{{ model.description }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="sp-{{ model.id }}">System prompt</label>
|
||||
<textarea class="textarea" id="sp-{{ model.id }}" name="system_prompt" rows="3"
|
||||
placeholder="{{ instance_prompt or 'No instance prompt is set.' }}"
|
||||
>{{ model.system_prompt }}</textarea>
|
||||
<p class="field__hint">
|
||||
Used by chats on this model that have no prompt of their own.
|
||||
{% if instance_prompt %}Leave empty to fall back to the instance prompt.
|
||||
{% else %}Leave empty to send no system prompt.{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<span class="field__label">Capabilities</span>
|
||||
<div class="checkbox-row">
|
||||
{% for name in capabilities %}
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="capability" value="{{ name }}"
|
||||
{{ 'checked' if (model.capabilities_json or {}).get(name) }}>
|
||||
<span>{{ name }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="field__hint">
|
||||
Endpoints rarely advertise these reliably, so they are your call.
|
||||
<strong>reasoning</strong> shows the thinking block;
|
||||
<strong>vision</strong> and <strong>tools</strong> are used by features
|
||||
not built yet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="checkbox-row">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" value="true" {{ 'checked' if model.enabled }}>
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="pinned" value="true" {{ 'checked' if model.pinned }}>
|
||||
<span>Pinned — shortcut in the chat sidebar</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="public" value="true" {{ 'checked' if model.public }}>
|
||||
<span>Available to everyone</span>
|
||||
</label>
|
||||
<p class="field__hint">
|
||||
Uncheck to restrict this model to specific groups. Administrators always
|
||||
have access.
|
||||
</p>
|
||||
{% if groups %}
|
||||
<div class="checkbox-row" style="margin-top: var(--sp-3)">
|
||||
{% for group in groups %}
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="group_ids" value="{{ group.id }}"
|
||||
{{ 'checked' if group in model.groups }}>
|
||||
<span>{{ group.name }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="field__hint">
|
||||
No groups yet — <a href="/admin/groups">create one</a> to restrict access.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn--primary" type="submit">Save {{ model.label }}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="card__footer">
|
||||
<form method="post" action="/admin/models/{{ model.id }}/image"
|
||||
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">
|
||||
<button class="btn btn--sm" type="submit">{{ icon("image", "icon--sm") }} Upload</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>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user