Skip to content
Dethink Components

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 components

    Date values, calendar arithmetic, and time-zone-aware date handling.

  • React Aria (react-aria) · via shared Dethink components

    Accessibility hooks and overlay utilities used by the implementation.

  • React Aria Components (react-aria-components) · via shared Dethink components

    Accessible 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.

Usage
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.

3 results
Show sourceexamples/filter-assistant/assistant-review.tsx
examples/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.

useFilterAssistant
PropWhat it doesDefault
fields / stateFilterField[] / FilterStateThe 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) => voidReceives errors thrown by resolve. People only see a generic message.Not set
returnsFilterAssistantStateprompt, status, proposal, decisions and submit, abort, answer, setDecision, apply({ all }), discard.Not set
FilterAssistant
PropWhat it doesDefault
assistantFilterAssistantStateState from useFilterAssistant.Not set
actionsReactNodeControls beside the prompt, such as a VoiceInput whose transcript you pass to assistant.submit.Not set
showProposalbooleanRenders 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.