ComponentsFilterAssistant
FilterAssistant
Describe a filter in plain words and get a proposal to review: each chip it would add, change or remove, with Accept and Reject. Nothing changes until you apply, and applying is one undo step. Bring any model through a single callback.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry files- Internationalized Date
(@internationalized/date)· via shared Dethink componentsDate values, calendar arithmetic, and time-zone-aware date handling.
- React Aria
(react-aria)· via shared Dethink componentsAccessibility hooks and overlay utilities used by the implementation.
- React Aria Components
(react-aria-components)· via shared Dethink componentsAccessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import {
FilterAssistant,
useFilterAssistant,
} from "@dethink/components";This example uses a small offline stand-in for a model.
Review before applying
Try “big open or blocked bugs from last week”, “ada or lin api work”, “customer issues” (asks a question), “p1 mobile bugs” (a field and a value it can't use), then accept or reject each change.
Show sourceexamples/filter-assistant/assistant-review.tsx
"use client";
import { useMemo } from "react";
import {
FilterAssistant,
FilterBar,
countFilterMatches,
createFilter,
createFilterCondition,
defineFilterFields,
useFilterAssistant,
useFilterState,
type Filter,
} from "@dethink/components";
import { demoResolve } from "./demo-resolve";
type Issue = {
title: string;
status: "open" | "blocked" | "done";
labels: string[];
assignee: string;
estimate: number;
created: string;
customer: boolean;
};
// A fixed "today" keeps relative dates identical on the server and client.
const evaluateOptions = { now: Date.UTC(2026, 8, 30, 12), timeZone: "UTC" };
const fields = defineFilterFields<Issue>([
{ key: "title", label: "Title", type: "text" },
{
key: "status",
label: "Status",
type: "option",
options: [
{ value: "open", label: "Open" },
{ value: "blocked", label: "Blocked" },
{ value: "done", label: "Done" },
],
},
{
key: "labels",
label: "Labels",
type: "multiOption",
options: [
{ value: "bug", label: "Bug" },
{ value: "api", label: "API" },
{ value: "billing", label: "Billing" },
{ value: "auth", label: "Auth" },
],
},
{ key: "assignee", label: "Assignee", type: "text" },
{ key: "estimate", label: "Estimate", type: "number" },
{ key: "created", label: "Created", type: "date" },
{ key: "customer", label: "Customer reported", type: "boolean" },
]);
const issues: Issue[] = [
{
title: "API latency spike",
status: "blocked",
labels: ["bug", "api"],
assignee: "Ada",
estimate: 5,
created: "2026-09-29",
customer: true,
},
{
title: "Billing export drops tax",
status: "open",
labels: ["bug", "billing"],
assignee: "Lin",
estimate: 3,
created: "2026-09-24",
customer: true,
},
{
title: "SSO login loop",
status: "open",
labels: ["bug", "auth"],
assignee: "Ada",
estimate: 8,
created: "2026-09-12",
customer: false,
},
{
title: "Rate limit headers",
status: "open",
labels: ["api"],
assignee: "Sam",
estimate: 2,
created: "2026-09-28",
customer: false,
},
{
title: "Invoice locale",
status: "done",
labels: ["billing"],
assignee: "Lin",
estimate: 1,
created: "2026-08-30",
customer: true,
},
];
export function FilterAssistantReview() {
const state = useFilterState({
defaultValue: createFilter({
id: "issues",
children: [
createFilterCondition({
id: "status",
field: "status",
operator: "isAnyOf",
value: ["open"],
}),
],
}),
});
const onPreviewCount = useMemo(
() => async (filter: Filter) =>
countFilterMatches(issues, filter, fields, evaluateOptions),
[],
);
const assistant = useFilterAssistant({
fields,
state,
resolve: demoResolve,
evaluateOptions,
onPreviewCount,
});
return (
<div className="grid gap-4">
<FilterAssistant assistant={assistant} />
<FilterBar
fields={fields}
state={state}
data={issues}
evaluateOptions={evaluateOptions}
/>
</div>
);
}The assistant can only propose filters.
Model output is untrusted. Every answer is rebuilt against your fields: unknown fields, operators and values are dropped and listed under “Couldn’t use”, text is stripped of control characters and shortened, and counts and nesting are capped. Everything the model says renders as plain text, never HTML.
A prompt injection can at worst propose a filter, which the person sees and must apply. The assistant never runs actions, fetches URLs or changes anything else. Keep your API key on the server and call your model from a route that only returns the answer.
| Prop | What it does | Default |
|---|---|---|
fields / stateFilterField[] / FilterState | The field schema (the allowlist) and the filter state proposals apply to. | Not set |
resolve(request) => Promise<FilterAssistantResult | false> | Sends { prompt, current, schema, fields, answers, now, timeZone, signal } to your model and returns its answer. Return false to decline. | Not set |
evaluateOptions{ now?, timeZone? } | Time context for relative dates like “last week”. | now, UTC |
onPreviewCount(filter, signal) => Promise<number> | Counts rows for the filter the accepted changes would give, shown next to Apply. | Not set |
onError(error) => void | Receives errors thrown by resolve. People only see a generic message. | Not set |
returnsFilterAssistantState | prompt, status, proposal, decisions and submit, abort, answer, setDecision, apply({ all }), discard. | Not set |
| Prop | What it does | Default |
|---|---|---|
assistantFilterAssistantState | State from useFilterAssistant. | Not set |
actionsReactNode | Controls beside the prompt, such as a VoiceInput whose transcript you pass to assistant.submit. | Not set |
showProposalboolean | Renders FilterProposal under the prompt. Set false to place FilterProposal yourself. | true |
labelsPartial<FilterAssistantLabels> | Every visible and accessible string, shared with FilterProposal. | English |
Status changes are announced politely. Each change is labelled Add, Change or Remove in text, removals are struck through, and Accept and Reject are toggle buttons with a pressed state, so nothing relies on colour. Escape or Stop cancels a request.
Choosing a clarification moves focus to the new proposal; applying or discarding returns focus to the prompt.