Customer assistant
The layer ships the shop's chat as a ready component. Add it once to your layout:
<!-- app/layouts/default.vue -->
<template>
<div>
<slot />
<AvvyrAssistant />
</div>
</template>
Enabling it
- Give the storefront key the
storefront:assistantscope. - Turn the assistant on for the shop (Management API
PUT /v1/customer-assistant/settings, or in the app). - Place
<AvvyrAssistant />in the layout.
The component asks GET /assistant/config on the client and renders nothing until the shop has the assistant enabled. A key without the scope also renders nothing, so it is safe to ship before the shop turns it on.
What it does
- A floating button in the bottom corner (respects the safe area on phones). The panel is a 380 x 600 card on larger screens and full screen below 640 px.
- The shop's greeting as the first message. Answers glide in as they stream, rendered as safe markdown (paragraphs, lists, bold, links; no images, no raw HTML). Links to your own site navigate inside the app.
- A short status line while the assistant looks something up ("Fetching your order").
- Cards the server builds from the shop's own data, so prices and tracking numbers are never made up (see Cards).
- Return cards the shopper approves or declines, with the claim terms and a required checkbox when the shop has terms.
- Suggested questions as chips in an empty chat; tapping one sends it.
- Photos: when the shop allows them, a photo button in the composer (see Photos).
- Sign-in only: when the shop lets only signed-in customers chat, a short "Sign in to chat" prompt replaces the composer (see Signed-in customers only).
- "Talk to a human" (when the shop allows handover) opens a short form: name, e-mail and an optional message. While a person answers, new messages are fetched every 4 seconds while the panel is open.
- A stop button while an answer streams; Enter sends, Shift+Enter adds a line, 1000 characters at most.
- The AI disclosure under the composer: "You are chatting with an AI assistant. Answers may contain mistakes."
- A reload continues the conversation: its id and secret are kept in
sessionStoragefor the tab. - Signed-in customers are recognised automatically: the layer's client already carries their token, and the handover form is prefilled.
- Focus stays in the panel while it is open, Escape closes it, and finished answers are announced to screen readers.
Props
| Prop | Type | Default | |
|---|---|---|---|
locale | string | the market's | Language of the texts and of the conversation. sv* gives Swedish, anything else English. |
texts | Record<string, string> | Override any text. Wins over app.config. | |
launcher | boolean | true | false hides the floating button; open the chat yourself with useAssistant().open(). |
position | 'bottom-right' | 'bottom-left' | 'bottom-right' | Corner on larger screens. |
productSku | string | The product the shopper is looking at. See Page context. | |
pageType | string | Lowercase with dashes: product, category, cart, checkout... | |
suggestions | string[] | the shop's | Questions for this page, shown as chips in an empty chat. Wins over the shop's suggestedQuestions. At most 6 are shown. |
loginUrl | string | app.config storefront.assistant.loginUrl | Where "Sign in" goes when only signed-in customers may chat. |
Events
| Event | Payload |
|---|---|
open / close | |
send | the message text |
handover | the handover form was sent |
proposal | { proposal, decision: 'confirm' | 'reject' } |
sign-in | the shopper pressed "Sign in" |
Page context
The assistant answers better when it knows where the shopper is: on a product page, "what are the measurements of this one?" just works. The context goes along when the conversation starts and with every message, so it follows the shopper from page to page.
The chat usually sits in the layout, which does not know the product. Set the context from the product page instead:
<!-- app/pages/products/[handle].vue -->
<script setup lang="ts">
const route = useRoute()
const { data: product } = await useProductByHandle(computed(() => route.params.handle as string))
const assistant = useAssistant()
watchEffect(() => assistant.setContext({ productSku: product.value?.sku, pageType: 'product' }))
onBeforeUnmount(() => assistant.setContext(null))
</script>
A page that renders the component itself can pass the props instead, together with questions for that page:
<AvvyrAssistant
:product-sku="product.sku"
page-type="product"
:suggestions="['What are the measurements?', 'When can it be delivered?']"
/>
Props that are not set leave the context alone, so a layout without them never clears what a page set.
Suggested questions
The shop's questions come from GET /assistant/config?locale= in the chat's language (set in the app, or suggestedQuestions in the Management API). The suggestions prop replaces them, for example per page. They show only before the first message.
Cards
The assistant shows products, orders and deliveries as cards instead of writing them out. They arrive with the streamed answer (the card event) and with messages read back after a reload.
| Card | Shows |
|---|---|
products | A horizontal row of small cards: image, name, price in the market's currency (Intl.NumberFormat in the chat's language), struck list price when discounted, rating. "Sign in to see the price" when the shop hides prices from anonymous visitors. The card links to the product when the shop has a product URL template: same-site links navigate in the app, others open in a new tab. |
order | Order number, date, status, the lines with thumbnails and quantity, total. |
tracking | Per parcel: carrier, tracking number (monospace, easy to copy), shipped date and a "Track parcel" link; expected delivery dates. |
return | The return or claim number and its lines, after the shopper approved a return card. |
Cards use the same CSS variables as the rest of the chat; --avvyr-assistant-card-bg sets their background and --avvyr-assistant-sale the colour of a discounted price.
Photos
When the shop allows photos (config.attachments.enabled, set in the app), the composer gets a photo button. The shopper can pick up to 3 images per message (jpeg, png or webp, 5 MB each) or paste one; anything else gets a short message in the chat. The photos show as thumbnails above the text field with a remove button, upload when the message is sent, and a message may be photos only. In the conversation they show as thumbnails on the shopper's bubble; a tap opens the full size.
The server re-encodes the photos without metadata. If the shopper then approves a claim, the conversation's photos move to the return when the shop's return settings take attachments.
Signed-in customers only
With access: "signedIn" in the shop's settings, the chat shows "Sign in to chat" instead of the composer to anonymous visitors. The same happens if the API answers 401 SIGN_IN_REQUIRED. Once the customer is signed in (the layer's useAuth()), the composer comes back.
Point the button at your login page with loginUrl (prop or app.config), listen for sign-in, or replace the whole prompt with the slot:
<AvvyrAssistant>
<template #sign-in="{ close }">
<p>Sign in to chat with us.</p>
<NuxtLink to="/account/login" @click="close()">Sign in</NuxtLink>
</template>
</AvvyrAssistant>
Texts
Built in for Swedish and English. Override per site in app.config.ts, or per language with the texts prop:
export default defineAppConfig({
storefront: {
assistant: {
texts: {
disclosure: 'Du chattar med vår AI-assistent. Dubbelkolla viktiga besked med kundtjänst.',
},
},
},
})
The keys are those in ASSISTANT_TEXTS in app/lib/assistant.ts, for example open, human, placeholder, disclosure, waiting, activity.order, proposal.confirm, signInTitle, attach, card.track, status.shipped.
Styling
The component carries its own scoped CSS and is themed with CSS variables, so it works whatever CSS pipeline the site uses:
:root {
--avvyr-assistant-accent: #0f766e; /* launcher, send button, user bubbles, primary buttons */
--avvyr-assistant-accent-fg: #ffffff;
--avvyr-assistant-bg: #ffffff;
--avvyr-assistant-fg: #171717;
--avvyr-assistant-muted: #525252;
--avvyr-assistant-border: #e5e5e5;
--avvyr-assistant-radius: 16px;
--avvyr-assistant-font: inherit;
}
Also available: --avvyr-assistant-card-bg, --avvyr-assistant-sale, --avvyr-assistant-font-size, --avvyr-assistant-user-bg, --avvyr-assistant-user-fg, --avvyr-assistant-staff-bg, --avvyr-assistant-subtle, --avvyr-assistant-link, --avvyr-assistant-error, --avvyr-assistant-shadow, --avvyr-assistant-launcher-bg, --avvyr-assistant-launcher-fg, --avvyr-assistant-launcher-size, --avvyr-assistant-offset, --avvyr-assistant-width, --avvyr-assistant-height, --avvyr-assistant-z (default 9998, under the cookie banner).
useAssistant()
The state behind the component, shared by every caller. Use it for your own button ("Chat with us" in the footer) or to build your own chat UI.
const {
config, // Ref<AssistantConfig | null>
enabled, // the shop has the chat on
canHandover,
isOpen,
status, // 'bot' | 'waiting' | 'human' | 'closed'
messages, // { key, id, role, content, author, createdAt, live?, cards?, attachments? }[]
proposals,
streaming,
activity, // the running tool's kind, or null
error, // { code, message? } | null
suggestedQuestions, // the shop's, in the chat's language
signInRequired, // show a sign-in prompt instead of the composer
canAttach, // the shop takes photos
attachments, // photos waiting to be sent: { key, name, size, previewUrl }[]
uploading,
addAttachments, // (FileList | File[]) => number added; sets error ATTACHMENT_TYPE / _SIZE / _COUNT
removeAttachment, clearAttachments,
setContext, // ({ productSku?, pageType? } | null)
open, close, toggle,
send, // (text) => Promise<boolean>; sends the waiting photos too, and the text may then be empty
stop,
requestHandover, // ({ name?, email?, message? }) => Promise<boolean>
confirmProposal, // (id, { termsAccepted? }) => Promise<boolean>
rejectProposal,
reset, // forget the conversation and start over
} = useAssistant()
The conversation is created on the first message (or handover), not when the chat opens.