feat: optional inline filter toggles in the message input

Add a Settings -> Interface "Inline Filter Toggles" setting (default off).
When enabled, the message-input bar shows every toggleable filter as a
persistent chip -- greyed when off, highlighted when on -- and clicking a chip
toggles its selection in place instead of removing it from the bar. Filters
with UserValves expose those via a gear shown on hover, so nothing is lost.

Default behavior is unchanged (only selected filters show as removable chips).
Restores the inline-toggle behavior some users have asked for (e.g. #17574)
without changing the default. No change to selectedFilterIds or the backend.
This commit is contained in:
rmarnold 2026-06-10 22:07:04 -07:00
parent b1d40f3409
commit 71eb34f835
3 changed files with 75 additions and 18 deletions

View file

@ -80,6 +80,7 @@
import Spinner from '../common/Spinner.svelte';
import XMark from '../icons/XMark.svelte';
import Cog6 from '../icons/Cog6.svelte';
import GlobeAlt from '../icons/GlobeAlt.svelte';
import Photo from '../icons/Photo.svelte';
import Wrench from '../icons/Wrench.svelte';
@ -512,6 +513,14 @@
.map((id) => ($models.find((model) => model.id === id) || {})?.filters ?? [])
.reduce((acc, filters) => acc.filter((f1) => filters.some((f2) => f2.id === f1.id)));
// With the `inlineFilterToggles` setting on, every available toggle filter is
// shown as a persistent chip (greyed when off, toggled in place); otherwise the
// default shows only the selected filters as removable chips.
let filterChips = [];
$: filterChips = ($settings?.inlineFilterToggles ?? false)
? toggleFilters
: selectedFilterIds.map((id) => toggleFilters.find((f) => f.id === id)).filter((f) => f);
let showToolsButton = false;
$: showToolsButton = ($tools ?? []).length > 0 || ($toolServers ?? []).length > 0;
@ -1791,29 +1800,37 @@
</Tooltip>
{/if}
{#each selectedFilterIds as filterId (filterId)}
{@const filter = toggleFilters.find((f) => f.id === filterId)}
{#each filterChips as filter (filter.id)}
{#if filter}
<Tooltip content={filter?.name} placement="top">
<button
on:click|preventDefault={() => {
if (
if ($settings?.inlineFilterToggles ?? false) {
// In-place toggle: flip selection, keep the chip visible.
if (selectedFilterIds.includes(filter.id)) {
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filter.id
);
} else {
selectedFilterIds = [...selectedFilterIds, filter.id];
}
} else if (
filter?.has_user_valves &&
($_user?.role === 'admin' ||
($_user?.permissions?.chat?.valves ?? true))
) {
selectedValvesType = 'function';
selectedValvesItemId = filterId;
selectedValvesItemId = filter.id;
showValvesModal = true;
} else {
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filterId
(id) => id !== filter.id
);
}
}}
type="button"
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden {selectedFilterIds.includes(
filterId
filter.id
)
? 'text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-600/10 border border-sky-200/40 dark:border-sky-500/20'
: 'bg-transparent text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 '} capitalize"
@ -1834,18 +1851,36 @@
{/if}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hidden group-hover:block"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filterId
);
}}
>
<XMark className="size-4" strokeWidth="1.75" />
</div>
{#if $settings?.inlineFilterToggles ?? false}
{#if filter?.has_user_valves && ($_user?.role === 'admin' || ($_user?.permissions?.chat?.valves ?? true))}
<div
class="hidden group-hover:block"
title={$i18n.t('Settings')}
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
selectedValvesType = 'function';
selectedValvesItemId = filter.id;
showValvesModal = true;
}}
>
<Cog6 className="size-4" strokeWidth="1.75" />
</div>
{/if}
{:else}
<div
class="hidden group-hover:block"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
selectedFilterIds = selectedFilterIds.filter(
(id) => id !== filter.id
);
}}
>
<XMark className="size-4" strokeWidth="1.75" />
</div>
{/if}
</button>
</Tooltip>
{/if}

View file

@ -30,6 +30,7 @@
let responseAutoCopy = false;
let widescreenMode = false;
let inlineFilterToggles = false;
let splitLargeChunks = false;
let scrollOnBranchChange = true;
let userLocation = false;
@ -242,6 +243,7 @@
landingPageMode = $settings?.landingPageMode ?? '';
chatBubble = $settings?.chatBubble ?? true;
widescreenMode = $settings?.widescreenMode ?? false;
inlineFilterToggles = $settings?.inlineFilterToggles ?? false;
splitLargeChunks = $settings?.splitLargeChunks ?? false;
scrollOnBranchChange = $settings?.scrollOnBranchChange ?? true;
@ -740,6 +742,25 @@
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div id="inline-filter-toggles-label" class=" self-center text-xs">
{$i18n.t('Inline Filter Toggles')}
</div>
<div class="flex items-center gap-2 p-1">
<Switch
ariaLabelledbyId="inline-filter-toggles-label"
tooltip={true}
bind:state={inlineFilterToggles}
on:change={() => {
saveSettings({ inlineFilterToggles });
}}
/>
</div>
</div>
</div>
{#if $user.role === 'admin' || $user?.permissions?.chat?.temporary}
<div>
<div class=" py-0.5 flex w-full justify-between">

View file

@ -1152,6 +1152,7 @@
"Initials": "",
"Inject file content into conversation context": "",
"Inject the entire content as context for comprehensive processing, this is recommended for complex queries.": "",
"Inline Filter Toggles": "",
"Input": "",
"Input Key (e.g. text, unet_name, steps)": "",
"Input Variables": "",