Filter syntax is fast once you know it, and a wall until you do. Let people type what they want, like "shared projects that aren't archived", and press Enter. A model translates the words into a Query filter, the field shows the filter in place, and the list filters on it. The person sees exactly what ran, and can edit it like any other query.
The translation goes through the ngrok.ai gateway, which routes the request to a model. The demo below mocks that call, so it runs with no network and answers the same way every time.
The arrow button inside the field submits the same form, for people who reach for the mouse. enterHint on Query.KeyboardHints names the new meaning of Enter.
Try "shared projects not archived named billing", "old stuff", or "tell me a joke". Then type is: and press Enter: a query that already holds a filter never goes to the model.
How it works
The field, the language, and the model each do one job:
Querykeeps Enter for the field. Enter accepts an active suggestion, and with no active suggestion it submits the form around the field. The form's submit handler is where translation starts.@ngrok/mantle/query-filterdescribes the language for the model withdescribeFilterGrammar, and checks the model's answer withcheckFilterQuery. A query that names a key or a value the grammar doesn't define is refused, so an invented filter never empties the list.- The model sees the keys, the allowed values, and the syntax rules, and nothing else. It answers in a strict JSON schema, so the server never parses free-form text.
A few rules keep it honest:
- Translate only plain words. When the query already holds a
key:valuequalifier, Enter keeps its usual meaning. - Show the translated filter in the field, and keep the typed words so Revert can put them back.
- Abort the request in flight on a new submit, an edit, or an unmount, and drop a late answer.
- Give every outcome its own sentence: translating, translated, off topic, refused, rate limited, and unavailable. Announce it in a LiveRegion.
- Do not label the feature as AI. The model is an implementation detail: people judge the search by the filter it writes, so show that filter and let them edit it.
The hook
useNaturalLanguageQuery owns the query, the translation status, and the request in flight. It takes the translator as an argument, so the demo passes a mock and your app passes a call to its server.
The mock waits 700 ms like a real call, honors the abort signal, and maps a few words to the demo grammar.
Wire it to the ngrok.ai gateway
In your app, the translator posts the words to your own server, and the server calls the model through the ngrok.ai gateway. Keep the gateway key on the server. AI_GATEWAY_URL is the gateway origin, such as https://example.ngrok.ai, and the OpenAI-compatible API is under /v1.
The server action sends the grammar description and the words to the gateway, and asks for a strict JSON answer. The words go in as JSON data, and the system message says they're data, so a request like "ignore your instructions" stays a search.
The hook checks the query with checkFilterQuery before it shows it, so the server can pass the model's query straight through. Add the same check on the server if other clients call the endpoint.