Put the assistant behind the search box
On Unikdoft, a fragrance shop, the search box is also the helper: one input, three modes, nothing downloaded until someone touches it, and a fallback to the shop's own catalogue when the assistant can't be reached.
- Project
- Doftguiden, the helper in Unikdoft's search box
- Role
- Built it, as part of the move from Bigbox to Unikdoft
- Stack
- Inline loader, chat.js on first touch, /api/ask, a local product index
- Status
- Live on the unikdoft.se home page
One input, three jobs
On Unikdoft, the helper is not a chat bubble or a page of its own. It is the search box at the top of the home page, a search box that is really a conversation. I built it as part of the move from Bigbox to Unikdoft; in the code it is Doftguiden, the scent guide.
I describe it as an AI helper to search, ideate, ask and explore perfumes. On the page, those four verbs are three pills over one input, each with its own placeholder and button label:
- Sök searches. "Sök bland 221 dofter…" (search among 221 scents) filters an index of the range by name, fashion house and original.
- Fråga takes questions and ideas, like the placeholder's "en söt kvällsdoft för vintern" (a sweet evening scent for winter). It is the only mode that reaches the assistant, through
/api/ask. - Inspirerad av explores from the original a shopper already knows. The placeholder asks for the original, the button says Hitta (find), and the search looks at originals only.
Why three modes and not one box that guesses? Because a shopper who knows the name shouldn't wait for a conversation, and one who doesn't shouldn't need a name.
Load it only when someone touches the box
With the helper in place, the home page still ships zero islands and zero script files: no framework components, only small inline scripts. The helper's code, chat.js, about 7.7 KB, comes in through a dynamic import, which fetches a script only when the page asks for it: on the first interaction with the box. A visitor who never touches the box downloads none of it. Loading starts when the box gets focus, so the first answer doesn't wait on a download.
The product index, /ai/products.json, is fetched once, on demand, at about 7 KB gzipped, and never as part of the page load. Only Sök, Inspirerad av or a fallback asks for it. Those two modes never reach the assistant; they filter the index in the browser.
Keep a real form underneath
The box is a real form that sends a GET request to the collection page, where a small script filters the cards by the query. With JavaScript off, the box still opens the whole collection, unfiltered.
The loader holds a typed query until the chat module has loaded. If the module fails to load, the page falls back to the form, and if the index can't be fetched during a search, the code opens the collection page instead.
Fail in words people can act on
I made each failure say which thing went wrong. A single "Could not answer" used to cover a missing API key, an exhausted quota and a broken model alike, which made the failure impossible to act on from either side of the screen. So each case gets its own sentence, and each ends with something to do:
- Busy: "Det är många frågor just nu — prova igen om en liten stund." (Lots of questions right now; try again in a little while.)
- Not switched on: "Doftguiden är inte påslagen här. Du kan söka på ett doftnamn så länge." (Doftguiden isn't switched on here. You can search for a scent name meanwhile.)
- Anything else: "Jag kunde inte svara just nu. Prova gärna att söka på ett doftnamn." (I couldn't answer just now. Try searching for a scent name.)
For a request that fails outright, I set one rule: never strand a shopper. The sheet runs the same words through the local index and, if anything matches, says "Jag når inte assistenten just nu, men det här ur sortimentet kan passa." (I can't reach the assistant right now, but these from the range might suit.)
Whenever products appear, a line under them says where they came from: "Ur Unikdofts sortiment" (from Unikdoft's range) under an answer, "N träffar av 221" (N hits of 221) under a search.
Small rules that keep it honest
The answer sheet shows one exchange at a time: the question under "Du frågade" (you asked), the answer, and a strip of cards that link to the products. Behind it, the last ten messages travel with each question, so a follow-up like "något billigare" (something cheaper) resolves against what came before. I just don't draw the thread as a wall of bubbles, and that is what keeps the sheet minimal.
Only the model's own words go into its history. The busy message, the fallback lines and the cards are the page talking, not the assistant, so they never travel back as if it had said them. Follow-ups typed in the sheet always go to Fråga, even if the hero was left on Sök.
Product names are merchant text, so I escape shop text before it reaches the page, where it shows as text, not markup. I fold accents, so a name typed without its accent still matches. Local results show only available scents, at most ten, with those that start with what was typed first.
My rule for a helper on a small shop: it costs nothing until someone uses it, and when it fails, it still tells the shopper what to do next.