From 6640162d4ddadb3ccb04ddc739d214f631d31b39 Mon Sep 17 00:00:00 2001 From: marciobever Date: Wed, 22 Apr 2026 15:02:40 -0300 Subject: [PATCH] fix(whatsapp): update Coach mode to 1 photo, fix google login infinite loading, improve welcome messages --- src/contexts/UserContext.tsx | 5 +- .../functions/meta-whatsapp-webhook/index.ts | 123 ++++++------------ 2 files changed, 43 insertions(+), 85 deletions(-) diff --git a/src/contexts/UserContext.tsx b/src/contexts/UserContext.tsx index 63861fb..acc23f9 100644 --- a/src/contexts/UserContext.tsx +++ b/src/contexts/UserContext.tsx @@ -120,7 +120,10 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => if (error) { console.error("UserContext: Falha na sessão inicial", error); if (mounted) setLoading(false); - } else if (!session) { + } else if (session) { + // Handled gracefully: forces profile fetch if session exists but events failed + handleSession(session); + } else { // Se não há sessão e onAuthStateChange não disparou INITIAL_SESSION if (mounted) setLoading(false); } diff --git a/supabase/functions/meta-whatsapp-webhook/index.ts b/supabase/functions/meta-whatsapp-webhook/index.ts index d463fe1..657acae 100644 --- a/supabase/functions/meta-whatsapp-webhook/index.ts +++ b/supabase/functions/meta-whatsapp-webhook/index.ts @@ -584,6 +584,11 @@ function formatWhatsAppResponse(analysis: any): string { lines.push(`💡 _DICA:_ ${analysis.tip.text}`); } + lines.push(""); + lines.push("━━━━━━━━━━━━━━━━━━━━━"); + lines.push("✨ *Gostou do visual?* Veja este e todos os seus pratos com gráficos interativos no nosso App:"); + lines.push("💻 https://foodsnap.com.br/dashboard"); + return lines.join("\n"); } @@ -718,11 +723,14 @@ async function processMetaMessage(msg: any) { const userId = user.id; // ── 4. Estado da conversa (Coach state machine) ───────────── - let { data: conv } = await supabase + let { data: convList } = await supabase .from("whatsapp_conversations") .select("*") .eq("phone_number", senderNumber) - .maybeSingle(); + .order("updated_at", { ascending: false }) + .limit(1); + + let conv = convList && convList.length > 0 ? convList[0] : null; // ── 3.5 Prevenir Mensagens Duplicadas (Retries da Meta) ────── if (conv && conv.last_msg_id === messageId) { @@ -868,7 +876,7 @@ RETORNE estritamente 3 bullet points recomendando o que o paciente pode adiciona // Mensagem 1: Introdução calorosa await sendWhatsAppMessage( remoteJid, - "🤖 *Coach IA ativado!*\n\nVou analisar seu biotipo e montar um protocolo *100% personalizado* de treino e dieta.\n\nO processo é simples: você envia *3 fotos* (frente, lado e costas) e a IA retorna seu plano completo em PDF. ⚡" + "🤖 *Coach IA Ativado!*\n\nVou analisar sua composição corporal e montar um protocolo *Titan 100% personalizado* de treino e dieta.\n\nO processo é super simples: você envia apenas *1 foto realista* e a IA processa o resto! ⚡" ); // Pequena pausa para não parecer robótico @@ -877,7 +885,7 @@ RETORNE estritamente 3 bullet points recomendando o que o paciente pode adiciona // Mensagem 2: Instrução da primeira foto await sendWhatsAppMessage( remoteJid, - "📸 *FOTO 1 de 3 — FRENTE*\n\nEnvie uma foto de frente do seu corpo agora.\n\n✅ Boa iluminação\n✅ Roupa justa ou sem camisa\n✅ Foto do pescoço até os joelhos" + "📸 *ENVIE SUA FOTO*\n\nTire uma selfie no espelho ou foto de frente do seu corpo.\n\n✅ Boa iluminação\n✅ Sem camisa (homens) ou de Top/Regata (mulheres)\n✅ Mostrando do pescoço até a cintura/joelhos" ); }, 0); return new Response("Coach Started", { status: 200 }); @@ -892,7 +900,7 @@ RETORNE estritamente 3 bullet points recomendando o que o paciente pode adiciona return new Response("Waiting for image", { status: 200 }); } - // Offload long-running task to background + // Offload long-running task to background setTimeout(async () => { const base64 = await getWhatsAppMedia(messageId); if (!base64) { @@ -907,84 +915,20 @@ RETORNE estritamente 3 bullet points recomendando o que o paciente pode adiciona await supabase .from("whatsapp_conversations") - .update({ state: "COACH_SIDE", temp_data: { ...conv!.temp_data, front_image: fileName } }) - .eq("phone_number", senderNumber); - - await sendWhatsAppMessage(remoteJid, "✅ Foto de frente recebida!\n\n📸 *FOTO 2 de 3 — LATERAL*\n\nAgora envie uma foto de perfil (lado direito ou esquerdo).\n\n✅ Braço ao longo do corpo\n✅ Mesmo padrão de roupa e iluminação"); - }, 0); - return new Response("Coach Front image received", { status: 200 }); - } - - // COACH_SIDE - if (state === "COACH_SIDE") { - if (!isImage) { - setTimeout(async () => { - await sendWhatsAppMessage(remoteJid, "⚠️ Por favor, envie a foto de *LADO*."); - }, 0); - return new Response("Waiting for image", { status: 200 }); - } - - // Offload long-running task to background - setTimeout(async () => { - const base64 = await getWhatsAppMedia(messageId); - if (!base64) { - await sendWhatsAppMessage(remoteJid, "⚠️ Não consegui baixar a imagem. Tente enviar novamente."); - return; - } - - const fileName = `${userId}_side_${Date.now()}.jpg`; - await supabase.storage - .from("coach-uploads") - .upload(fileName, base64ToUint8Array(base64), { contentType: "image/jpeg" }); - - await supabase - .from("whatsapp_conversations") - .update({ state: "COACH_BACK", temp_data: { ...conv!.temp_data, side_image: fileName } }) - .eq("phone_number", senderNumber); - - await sendWhatsAppMessage(remoteJid, "✅ Foto lateral recebida!\n\n📸 *FOTO 3 de 3 — COSTAS*\n\nEnvie agora uma foto de costas.\n\n✅ Postura ereta\n✅ Braços ao lado do corpo\n✅ Mesma roupa e iluminação"); - }, 0); - return new Response("Coach Side image received", { status: 200 }); - } - - // COACH_BACK - if (state === "COACH_BACK") { - if (!isImage) { - setTimeout(async () => { - await sendWhatsAppMessage(remoteJid, "⚠️ Por favor, envie a foto de *COSTAS*."); - }, 0); - return new Response("Waiting for image", { status: 200 }); - } - - // Offload long-running task to background - setTimeout(async () => { - const base64 = await getWhatsAppMedia(messageId); - if (!base64) { - await sendWhatsAppMessage(remoteJid, "⚠️ Não consegui baixar a imagem. Tente enviar novamente."); - return; - } - - const fileName = `${userId}_back_${Date.now()}.jpg`; - await supabase.storage - .from("coach-uploads") - .upload(fileName, base64ToUint8Array(base64), { contentType: "image/jpeg" }); - - await supabase - .from("whatsapp_conversations") - .update({ state: "COACH_GOAL", temp_data: { ...conv!.temp_data, back_image: fileName } }) + .update({ state: "COACH_GOAL", temp_data: { ...conv!.temp_data, front_image: fileName } }) .eq("phone_number", senderNumber); await sendWhatsAppInteractiveButtons( remoteJid, - "📸 Todas as fotos recebidas!\n\nAgora escolha o seu principal objetivo para o protocolo:", + "📸 Imagem processada com sucesso!\n\nAgora para eu calibrar seu treino e dieta, *qual o seu objetivo principal?*", [ - { id: "goal_hipertrofia", title: "💪 Hipertrofia" }, + { id: "goal_hipertrofia", title: "💪 Hipertrofia (Massa)" }, { id: "goal_emagrecer", title: "🔥 Emagrecimento" }, { id: "goal_definicao", title: "📐 Definição" } ] ); }, 0); - return new Response("Coach Back image received", { status: 200 }); + return new Response("Coach Photo image received", { status: 200 }); } // COACH_GOAL @@ -1014,9 +958,9 @@ RETORNE estritamente 3 bullet points recomendando o que o paciente pode adiciona ); try { - const { front_image, side_image, back_image } = conv!.temp_data; - const images = [front_image, side_image, back_image]; - const parts: any[] = [{ text: COACH_SYSTEM_PROMPT }, { text: `Objetivo: ${goal}` }]; + const { front_image } = conv!.temp_data; + const images = [front_image]; + const parts: any[] = [{ text: COACH_SYSTEM_PROMPT }, { text: `Objetivo: ${goal}\n(Lembre-se: baseie-se apenas nesta imagem unica enviada pelo usuario.` }]; for (const imgPath of images) { if (imgPath) { @@ -1212,20 +1156,20 @@ RETORNE estritamente 3 bullet points recomendando o que o paciente pode adiciona await sendWhatsAppListMenu( remoteJid, - "FoodSnap IA", - "👋 Olá! Sou sua Inteligência Artificial.\n\n📸 *Para calcular suas calorias, basta me enviar a foto do seu prato agora mesmo!*", - "Mais Opções 📋", + "FoodSnap IA e Nutrição", + "Fala aí! 👋 Sou a FoodSnap, a Inteligência Artificial projetada para revolucionar seu físico.\n\nEscolha uma opção no menu abaixo para navegarmos, ou se preferir, *apenas me mande diretamente a foto do que você está comendo* e eu calculo tudo na hora!", + "Menu Principal 👇", [ { - title: "Escanear Prato 📸", + title: "🍽️ Scanners Diários", rows: [ - { id: "action_help_photo", title: "Dicas de Fotografia", description: "Veja como tirar a foto perfeita" }, + { id: "action_help_photo", title: "Dicas de Leitura de Prato", description: "Veja as recomendações pro bot não falhar" }, ] }, { - title: "Evolução PRO 📈", + title: "🏋️ Especialistas", rows: [ - { id: "action_coach", title: "Protocólo Coach IA", description: "Gerar plano de treino pelo biotipo" }, + { id: "action_coach", title: "Protocolo Coach de Saúde", description: "Gerar plano de treino 100% individual" }, ] } ] @@ -1400,9 +1344,20 @@ RETORNE estritamente 3 bullet points recomendando o que o paciente pode adiciona ); // Atualizar last_analysis no temp_data para a re-pergunta "O que comer mais?" + // Sem sobrescrever o estado caso o usuário já tenha iniciado o fluxo COACH_FRONT no meio tempo + const { data: currentConv } = await supabase + .from("whatsapp_conversations") + .select("state, temp_data") + .eq("phone_number", senderNumber) + .order("updated_at", { ascending: false }) + .limit(1); + + const currentState = currentConv?.[0]?.state || "IDLE"; + const currentTempData = currentConv?.[0]?.temp_data || {}; + await supabase .from("whatsapp_conversations") - .update({ state: "IDLE", temp_data: { last_analysis: rawResponseText } }) + .update({ state: currentState, temp_data: { ...currentTempData, last_analysis: rawResponseText } }) .eq("phone_number", senderNumber); // 6g. Mapear confidence para enum do banco