Importação de Pedido por IA
Última atualização: 16 de julho de 2026
Visão Geral
Esse é o único fluxo de IA do app que de fato conversa com um backend de inteligência artificial. O vendedor fornece um pedido em texto livre, um arquivo, ou um áudio gravado na hora, e a IA devolve os produtos identificados, já casados com o catálogo, com pontuação de confiança e alternativas para os casos ambíguos. É acessível tanto a partir da carteira de clientes (documentado em Seleção e Navegação) quanto de dentro da própria tela de nova venda.
Arquivos-chave
| Arquivo | Responsabilidade |
|---|---|
services/ai/ai.service.ts | importProducts, a chamada real ao backend de IA |
features/new-sale/subscreens/ai-product-import/ai-product-import.session.ts | Estado compartilhado entre as telas do fluxo |
features/new-sale/subscreens/ai-product-import/new-sale-ai-product-import.controller.ts | Captura de texto, arquivo ou áudio |
features/new-sale/subscreens/ai-product-import/new-sale-ai-product-import-loading.tsx | Dispara a chamada real e espera a resposta |
types/ai/ai.types.ts | Formato da resposta da IA |
Três formas de fornecer o pedido, todas reais
A tela de entrada tem três abas, texto, arquivo e áudio, e as três são funcionais, não só a de texto. A aba de áudio grava de fato usando expo-audio, com permissão de microfone solicitada em tempo real, e a de arquivo usa o seletor de documentos nativo:
// new-sale-ai-product-import.controller.ts
import {
RecordingPresets,
requestRecordingPermissionsAsync,
setAudioModeAsync,
useAudioRecorder,
useAudioRecorderState,
} from "expo-audio";
import * as DocumentPicker from "expo-document-picker";
export type ImportTabId = "text" | "file" | "audio";
Cada aba também mostra dicas específicas de como formular o pedido para melhorar o reconhecimento (nomes completos, uma linha por produto, evitar ruído de fundo na gravação, e assim por diante).
Um estado compartilhado entre telas isoladas
Como o resultado da IA é grande demais para caber num parâmetro de rota, o fluxo usa um singleton em memória para carregar informação de uma tela para a próxima, com um comentário explicando exatamente por quê:
// ai-product-import.session.ts
/**
* Singleton session store that bridges the isolated route screens of the AI
* product import flow. Route params cannot carry large payloads (AI result),
* so this store is the source of truth between telas.
*/
export const aiImportSession = {
get(): AiImportSession { /* ... */ },
subscribe(listener: SessionListener): () => void { /* ... */ },
start(params: { mode: "new" | "existing"; saleDraftId?: string | null; returnTo?: string | null }): void { /* ... */ },
setContext(context: AiImportContext): void { /* ... */ },
setSource(source: AiImportSource): void { /* ... */ },
setResult(result: AiImportResponseData): void { /* ... */ },
reset(): void { /* ... */ },
};
O campo mode distingue dois cenários: "new", criando um pedido novo a partir do zero (a importação vem antes até do cabeçalho da venda estar preenchido, e por isso carrega também context com empresa/tipo de movimento/condição de pagamento/cliente/tabela de preço), e "existing", adicionando itens a um rascunho de venda já em andamento.
A chamada real, com cancelamento
A tela de carregamento dispara a chamada de fato, com suporte a cancelamento via AbortController, e trata separadamente um cancelamento intencional de uma falha real:
// new-sale-ai-product-import-loading.tsx
const externalProducts = await productService.getAiExternalProducts();
const result = await aiService.importProducts({
source: session.source,
externalProducts,
signal: controller.signal,
});
aiImportSession.setResult(result);
router.replace({
pathname: "/nova-venda/identificador-produtos-resultado",
params: { saleDraftId, returnTo },
} as any);
} catch (error) {
if ((error as { name?: string })?.name === "CanceledError" || controller.signal.aborted) {
return; // usuário cancelou, não é erro
}
toast.show("Não foi possível processar o conteúdo. Tente novamente.", "error", 3000, "top");
router.back();
}
externalProducts é o catálogo completo de produtos do vendedor, enviado junto do pedido para a IA conseguir casar cada item identificado contra um produto real. A lista de "passos" visual mostrada durante o carregamento (analisando, identificando produtos, etc.) é puramente decorativa, avança num intervalo fixo de 520ms independente do andamento real da chamada de rede, ela só existe para dar uma sensação de progresso enquanto a chamada de fato acontece em paralelo.
O formato da resposta é rico, com telemetria do próprio modelo de IA
// types/ai/ai.types.ts
export interface AiImportProduct {
codprod: number;
nome: string;
quantidade: number;
score: number;
alternativas: AiImportAlternative[];
}
export interface AiImportAiContext {
model: string;
operation: string;
provider: string;
tempoIaMs: number;
tempoTotalMs: number;
tokenEntrada: number;
tokenSaida: number;
tokenTotal: number;
}
export interface AiImportResponseData {
products: AiImportProduct[];
invalidProducts: unknown[];
aiContext: AiImportAiContext;
metadata: Metadata;
}
Cada produto identificado vem com uma pontuação de confiança (score) e uma lista de alternativas com sua própria similaridade, para os casos em que a IA não tem certeza absoluta de qual produto do catálogo o vendedor quis dizer. aiContext devolve até qual modelo e provedor de IA processaram a requisição, e quantos tokens foram consumidos, informação que normalmente só existe quando há mesmo um modelo de linguagem real por trás da chamada.
Armadilhas conhecidas
Ao depurar um resultado de importação inesperado, lembre que o catálogo inteiro do vendedor (getAiExternalProducts) é enviado junto a cada chamada, então o resultado depende tanto do que a IA entendeu do texto/áudio/arquivo quanto de quais produtos existiam no catálogo sincronizado no momento. Um produto ausente da última sincronização, documentada em Sincronização Offline, não vai aparecer como opção de casamento nem como alternativa.