Uso Avançado
Streaming da resposta
O texto chega em frames delta, que podem vir fora de ordem — a biblioteca
reordena por sequência antes de montar a mensagem. Na prática você só observa
status e content:
const agent = useAgentVtexCX({
channelUuid,
onDelta: (chunk, message) => console.log('chegou:', chunk, message.content),
});
| Estado | O que mostrar |
|---|---|
status.type === 'thinking' | "pensando..." — a IA recebeu a mensagem |
status.type === 'typing' | "digitando..." — um atendente humano |
status.type === 'streaming' | a resposta começou a chegar |
message.status === 'streaming' | aquela bolha ainda está crescendo (cursor) |
message.status === 'delivered' | mensagem completa |
O onDelta é opcional: messages já muda a cada pedaço. Use-o quando precisar
reagir ao token em si (métricas, síntese de voz, rolagem manual).
Uma resposta encerra por stream_end ou por 2 segundos de silêncio — quando o
servidor não manda o stream_end, a mensagem é fechada com o que chegou em vez
de ficar presa em streaming.
Quick replies, listas e CTA
Os campos vêm com o nome do protocolo, então o payload do CX pode ser lido do mesmo jeito que aparece na documentação dele:
{message.quick_replies?.map((reply) => (
<Button key={reply.title} onClick={() => agent.call({ content: reply.title })}>
{reply.title}
</Button>
))}
{message.list_message?.list_items.map((item) => (
<View key={item.title} onClick={() => agent.call({ content: item.title })}>
<Text>{item.title}</Text>
{item.description && <Text>{item.description}</Text>}
</View>
))}
{message.cta_message && (
<View onClick={() => Eitri.openBrowser({ url: message.cta_message.url, inApp: true })}>
<Text>{message.cta_message.display_text}</Text>
</View>
)}
Enviar o title de volta é o que o webchat oficial faz: o flow espera o texto
da opção, não o payload.
Catálogo de produtos
Produtos chegam em product_list, agrupados em seções:
const items = message.product_list?.sections.flatMap((s) => s.product_items) ?? [];
{items.map((item) => (
<View key={item.product_retailer_id} onClick={() => abrirPdp(item.product_url)}>
{item.image && <Image src={item.image} />}
<Text>{item.name}</Text>
<Text>{item.sale_price || item.price}</Text>
</View>
))}
Cada product_item traz product_retailer_id, name, price, sale_price,
currency, image, seller_id, product_url e description — dá para
montar o card sem consultar o catálogo.
Numa loja Eitri Shopping, prefira abrir a PDP nativa em vez do browser:
Eitri.nativeNavigation.open({ slug: 'pdp', initParams: { slug: produtoSlug } });
Quando o agente responde JSON
Agentes de IA no CX costumam devolver um JSON no texto em vez de usar os campos do frame:
{
"is_final_output": true,
"messages_sent": [
{
"text": "Encontrei opções de camisa no catálogo.",
"catalog_message": {
"carousel": true,
"products": [
{
"product": "Camiseta Eitri",
"product_retailer_info": [
{
"name": "Camiseta Eitri G3",
"price": "30.04",
"retailer_id": "1#1",
"image": "https://…",
"product_url": "https://…",
"currency": "BRL"
}
]
}
]
}
}
]
}
O hook reconhece esse formato e resolve sozinho:
- cada item de
messages_sentvira uma mensagem (ids<id>#0,<id>#1…), comtextemcontent; catalog_message.products[].product_retailer_info[]é convertido paraproduct_list—retailer_idviraproduct_retailer_ide o resto mantém o nome. A sua UI tem um caminho só de renderização, venha do frame ou do JSON;rawguarda o item original domessages_sent;- durante o streaming o JSON não aparece na tela: a bolha fica vazia até o
stream_end, então nunca se vê chave solta crescendo no chat; - o
call()resolve com os textos concatenados, não com o JSON.
Se o agente devolver outro JSON qualquer, ele chega inteiro em content — aí
use o helper.json do useAgent (JSONHelper) para extrair o bloco.
Várias conversas
O servidor guarda as mensagens de cada sessão, mas não sabe quais sessões este
app abriu — a lista é local (Eitri.sharedStorage).
<Button onClick={agent.newConversation}>Nova conversa</Button>
{agent.conversations.map((conversation) => (
<View key={conversation.id} onClick={() => agent.openConversation(conversation.id)}>
<Text>{conversation.id}</Text>
<Text>{new Date(conversation.createdAt).toLocaleString('pt-BR')}</Text>
{conversation.id === agent.sessionId && <Text>aberta</Text>}
</View>
))}
newConversation()deriva um id novo (identidade-<epoch>), zera a conversa na tela e reenvia oinitPayload.openConversation(id)troca a identidade e recarrega o histórico daquela sessão do servidor.conversationsvem ordenada porcreatedAt, não por último uso: a posição de cada item não muda quando a conversa recebe mensagem.
Histórico paginado
const carregarAnteriores = async () => {
if (agent.hasMore && !carregando) await agent.loadMore();
};
Chame ao chegar no topo da lista. Quando o servidor devolve menos itens que o
historyLimit, hasMore vira false.
Conexão: o cuidado que importa
O servidor aceita uma conexão por sessionId. Dois clientes vivos com a
mesma identidade se expulsam mutuamente, gerando um loop de "reconectando" —
é o caso clássico de um app que já tem outro webchat com o mesmo from.
useEffect(() => {
return () => agent.disconnect();
}, []);
Chame disconnect() ao sair da experiência de chat. O histórico não é perdido:
ele vive no servidor, e a próxima montagem do hook reconecta e recarrega.
O resto da resiliência é automático:
- reconexão com backoff exponencial (3s → 30s, com jitter, até 30 tentativas);
pinga cada 50s para o keepalive;- sessão duplicada recuperada com
close_session, limitado a 3 tentativas; Connection closed by requestderruba a conexão de vez, sem reconectar em loop.
Depurando
Com verbose: true, o hook registra no console cada mensagem que entra e sai da
conversa, inclusive os pedaços recebidos durante o streaming — útil para
conferir se o canal está respondendo em streaming ou de uma vez.