Skip to main content
Version: 1.16.0

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),
});
EstadoO 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_sent vira uma mensagem (ids <id>#0, <id>#1…), com text em content;
  • catalog_message.products[].product_retailer_info[] é convertido para product_list — retailer_id vira product_retailer_id e o resto mantém o nome. A sua UI tem um caminho só de renderização, venha do frame ou do JSON;
  • raw guarda o item original do messages_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 o initPayload.
  • openConversation(id) troca a identidade e recarrega o histórico daquela sessão do servidor.
  • conversations vem ordenada por createdAt, 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);
  • ping a cada 50s para o keepalive;
  • sessão duplicada recuperada com close_session, limitado a 3 tentativas;
  • Connection closed by request derruba 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.