diff --git a/app/src/main/java/com/example/pap/AiConfig.java b/app/src/main/java/com/example/pap/AiConfig.java index 69c4c08..1efae78 100644 --- a/app/src/main/java/com/example/pap/AiConfig.java +++ b/app/src/main/java/com/example/pap/AiConfig.java @@ -1,7 +1,9 @@ package com.example.pap; +import okhttp3.OkHttpClient; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; +import java.util.concurrent.TimeUnit; public class AiConfig { private static Retrofit retrofit; @@ -9,8 +11,17 @@ public class AiConfig { public static Retrofit getRetrofit() { if (retrofit == null) { + + // NOVO: Adiciona paciência ao Android (Timeout de 60 segundos) + OkHttpClient clienteComPaciencia = new OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .build(); + retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) + .client(clienteComPaciencia) // Liga o cliente paciente .addConverterFactory(GsonConverterFactory.create()) .build(); } diff --git a/app/src/main/java/com/example/pap/ChatActivity.java b/app/src/main/java/com/example/pap/ChatActivity.java index 5b7f3c4..f75ad6c 100644 --- a/app/src/main/java/com/example/pap/ChatActivity.java +++ b/app/src/main/java/com/example/pap/ChatActivity.java @@ -2,6 +2,7 @@ package com.example.pap; import android.content.Intent; import android.os.Bundle; +import android.text.method.ScrollingMovementMethod; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; @@ -18,7 +19,7 @@ public class ChatActivity extends AppCompatActivity { private Button btnEnviar; private TextView btnVoltarChat; - // NÃO TE ESQUEÇAS DE COLAR A TUA CHAVE AQUI! + // A TUA CHAVE (cuidado na escola com ela) private final String MINHA_API_KEY = "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"; @Override @@ -31,17 +32,17 @@ public class ChatActivity extends AppCompatActivity { btnEnviar = findViewById(R.id.btnEnviarChat); btnVoltarChat = findViewById(R.id.btnVoltarChat); - // --- LÓGICA DO BOTÃO VOLTAR PARA O HOME --- + // Faz com que o texto do chat consiga rolar se for muito grande + tvChatLog.setMovementMethod(new ScrollingMovementMethod()); + btnVoltarChat.setOnClickListener(v -> { - // Cria a intenção de ir para a MainActivity (Home) Intent intent = new Intent(ChatActivity.this, HomeActivity.class); - // Esta linha garante que não ficas com mil ecrãs abertos uns por cima dos outros intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); - finish(); // Fecha o Chat + finish(); }); - // Receber a análise da foto se existir + // Receber a análise da foto (se veio de lá) String analiseComida = getIntent().getStringExtra("analise_comida"); if (analiseComida != null && !analiseComida.isEmpty()) { tvChatLog.setText("IA: Analisei o teu prato.\n" + analiseComida + "\n\nO que queres saber mais?"); @@ -59,9 +60,10 @@ public class ChatActivity extends AppCompatActivity { private void perguntarIA(String texto) { tvChatLog.append("\n\nIA: A pensar... ⏳"); + btnEnviar.setEnabled(false); // Bloqueia o botão para não haver spam AiRequest request = new AiRequest(java.util.Arrays.asList( - new Message("system", Collections.singletonList(new ContentPart("text", "És um nutricionista de Portugal. Responde SEMPRE de forma muito curta (máximo 2 frases). Nunca uses asteriscos."))), + new Message("system", Collections.singletonList(new ContentPart("text", "És um nutricionista de Portugal. Responde SEMPRE de forma muito curta (máximo 3 frases). Nunca uses asteriscos."))), new Message("user", Collections.singletonList(new ContentPart("text", texto))) )); @@ -70,17 +72,23 @@ public class ChatActivity extends AppCompatActivity { .enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { + btnEnviar.setEnabled(true); if (response.isSuccessful() && response.body() != null) { String resposta = response.body().choices.get(0).message.content; String limpa = resposta.replace("**", "").replace("*", ""); String atual = tvChatLog.getText().toString(); tvChatLog.setText(atual.replace("IA: A pensar... ⏳", "IA: " + limpa)); + } else { + // Se a API chumbou mas houve resposta (Erro 400, 429...) + String atual = tvChatLog.getText().toString(); + tvChatLog.setText(atual.replace("IA: A pensar... ⏳", "IA: Tive um pequeno bloqueio (Erro " + response.code() + "). Tenta outra vez!")); } } @Override public void onFailure(Call call, Throwable t) { + btnEnviar.setEnabled(true); String atual = tvChatLog.getText().toString(); - tvChatLog.setText(atual.replace("IA: A pensar... ⏳", "IA: Erro de rede.")); + tvChatLog.setText(atual.replace("IA: A pensar... ⏳", "IA: Erro de comunicação (O servidor demorou muito a responder). Tenta novamente.")); } }); } diff --git a/app/src/main/java/com/example/pap/DesafiosActivity.java b/app/src/main/java/com/example/pap/DesafiosActivity.java index cf8a7b7..88c9a51 100644 --- a/app/src/main/java/com/example/pap/DesafiosActivity.java +++ b/app/src/main/java/com/example/pap/DesafiosActivity.java @@ -1,18 +1,26 @@ package com.example.pap; +import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.provider.MediaStore; import android.util.Base64; +import android.view.View; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; + import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import java.io.ByteArrayOutputStream; import java.io.InputStream; @@ -34,6 +42,9 @@ public class DesafiosActivity extends AppCompatActivity { private int desafioAtualSendoGravado = -1; // 0=Agua, 1=D1, 2=D2, 3=D3, 4=D4 private float litrosAgua = 0.0f; + private ActivityResultLauncher videoLauncher; + private AlertDialog popupCarregamento; // A janela de loading + private final String MINHA_API_KEY = "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"; @Override @@ -42,6 +53,9 @@ public class DesafiosActivity extends AppCompatActivity { setContentView(R.layout.activity_desafios); tvStatusGeralIA = findViewById(R.id.tvStatusGeralIA); + // Esconde o texto antigo do topo, já não precisamos dele! + tvStatusGeralIA.setVisibility(View.GONE); + tvStatusAgua = findViewById(R.id.tvStatusAgua); tvStatusD1 = findViewById(R.id.tvStatusD1); tvStatusD2 = findViewById(R.id.tvStatusD2); @@ -58,7 +72,7 @@ public class DesafiosActivity extends AppCompatActivity { verificarResetMeiaNoite(); - ActivityResultLauncher videoLauncher = registerForActivityResult( + videoLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK && result.getData() != null) { @@ -69,18 +83,41 @@ public class DesafiosActivity extends AppCompatActivity { } }); - btnVideoAgua.setOnClickListener(v -> { desafioAtualSendoGravado = 0; abrirCamera(videoLauncher); }); - btnVideoD1.setOnClickListener(v -> { desafioAtualSendoGravado = 1; abrirCamera(videoLauncher); }); - btnVideoD2.setOnClickListener(v -> { desafioAtualSendoGravado = 2; abrirCamera(videoLauncher); }); - btnVideoD3.setOnClickListener(v -> { desafioAtualSendoGravado = 3; abrirCamera(videoLauncher); }); - btnVideoD4.setOnClickListener(v -> { desafioAtualSendoGravado = 4; abrirCamera(videoLauncher); }); + btnVideoAgua.setOnClickListener(v -> { desafioAtualSendoGravado = 0; verificarPermissaoEAbrir(); }); + btnVideoD1.setOnClickListener(v -> { desafioAtualSendoGravado = 1; verificarPermissaoEAbrir(); }); + btnVideoD2.setOnClickListener(v -> { desafioAtualSendoGravado = 2; verificarPermissaoEAbrir(); }); + btnVideoD3.setOnClickListener(v -> { desafioAtualSendoGravado = 3; verificarPermissaoEAbrir(); }); + btnVideoD4.setOnClickListener(v -> { desafioAtualSendoGravado = 4; verificarPermissaoEAbrir(); }); } - private void abrirCamera(ActivityResultLauncher launcher) { - Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); - intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10); - intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); - launcher.launch(intent); + private void verificarPermissaoEAbrir() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 100); + } else { + abrirCameraReal(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == 100) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + abrirCameraReal(); + } else { + Toast.makeText(this, "Precisas de dar permissão da câmara para fazer o desafio!", Toast.LENGTH_SHORT).show(); + } + } + } + + private void abrirCameraReal() { + try { + Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10); + videoLauncher.launch(intent); + } catch (Exception e) { + Toast.makeText(this, "Erro: Não foi possível iniciar a câmara.", Toast.LENGTH_LONG).show(); + } } private void verificarResetMeiaNoite() { @@ -97,7 +134,6 @@ public class DesafiosActivity extends AppCompatActivity { editor.putBoolean("d3_concluido", false); editor.putBoolean("d4_concluido", false); - // Zera a água e as calorias dos desafios diários editor.putInt("agua_hoje", 0); editor.putInt("calorias_desafios", 0); editor.apply(); @@ -128,14 +164,46 @@ public class DesafiosActivity extends AppCompatActivity { } } + // ========================================== + // NOVOS POP-UPS DE LOADING E RESULTADOS + // ========================================== + private void mostrarLoading() { + runOnUiThread(() -> { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("A analisar o vídeo... ⏳"); + builder.setMessage("A Inteligência Artificial está a avaliar o teu desempenho. Por favor, aguarda um momento."); + builder.setCancelable(false); // Impede que o utilizador feche sem querer + + popupCarregamento = builder.create(); + popupCarregamento.show(); + }); + } + + private void mostrarResultadoFinal(String titulo, String mensagem) { + runOnUiThread(() -> { + // Fecha a janela de "A aguardar..." + if (popupCarregamento != null && popupCarregamento.isShowing()) { + popupCarregamento.dismiss(); + } + + // Abre a janela com o resultado final + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(titulo); + builder.setMessage(mensagem); + builder.setPositiveButton("OK", null); + builder.show(); + }); + } + private void enviarVideoParaIA(Uri uri) { - tvStatusGeralIA.setText("A processar vídeo e enviar para a IA... ⏳"); bloquearBotoes(false); + mostrarLoading(); // Mostra a janela de loading logo aqui! new Thread(() -> { String base64Video = converterVideo(uri); if (base64Video == null) { - runOnUiThread(() -> { tvStatusGeralIA.setText("Erro ao ler vídeo."); bloquearBotoes(true); }); + mostrarResultadoFinal("Erro no Vídeo ⚠️", "Não foi possível ler o vídeo gravado."); + runOnUiThread(() -> bloquearBotoes(true)); return; } @@ -162,12 +230,14 @@ public class DesafiosActivity extends AppCompatActivity { String respostaIA = response.body().choices.get(0).message.content; processarResposta(respostaIA); } else { - runOnUiThread(() -> { tvStatusGeralIA.setText("Erro na IA: " + response.code()); bloquearBotoes(true); }); + mostrarResultadoFinal("Erro de Servidor ⚠️", "Erro ao contactar a IA. Código: " + response.code()); + runOnUiThread(() -> bloquearBotoes(true)); } } @Override public void onFailure(Call call, Throwable t) { - runOnUiThread(() -> { tvStatusGeralIA.setText("Erro de rede."); bloquearBotoes(true); }); + mostrarResultadoFinal("Sem Internet 🌐", "Não conseguimos ligar à IA. Verifica a tua rede."); + runOnUiThread(() -> bloquearBotoes(true)); } }); }).start(); @@ -191,10 +261,13 @@ public class DesafiosActivity extends AppCompatActivity { editor.putFloat("agua_litros", litrosAgua); editor.putInt("agua_hoje", (int) (litrosAgua / 0.25f)); - tvStatusGeralIA.setText("IA leu: +" + lido + " Litros!"); + // Mostra o resultado da Água! + mostrarResultadoFinal("Bom trabalho! 💧", "A IA detetou que bebeste +" + lido + " Litros!"); } catch (Exception e) { - tvStatusGeralIA.setText("Erro a ler os litros. Tenta de novo."); + mostrarResultadoFinal("Oops! 🤔", "A IA teve dificuldade em ler a quantidade exata. Tenta gravar de novo."); } + } else { + mostrarResultadoFinal("Oops! 🤔", "A IA não percebeu o vídeo. Garante que o copo ou garrafa se vê bem!"); } } else { if (texto.contains("Status: Concluido")) { @@ -205,17 +278,17 @@ public class DesafiosActivity extends AppCompatActivity { if (desafioAtualSendoGravado == 3) { editor.putBoolean("d3_concluido", true); caloriasAQueimar = 2; } if (desafioAtualSendoGravado == 4) { editor.putBoolean("d4_concluido", true); caloriasAQueimar = 3; } - // Soma as calorias queimadas e atualiza a pontuação int caloriasTotaisQueimadas = prefs.getInt("calorias_desafios", 0) + caloriasAQueimar; editor.putInt("calorias_desafios", caloriasTotaisQueimadas); - tvStatusGeralIA.setText("IA: Desafio Validado! ✅ +50 Pontos | 🔥 +" + caloriasAQueimar + " kcal"); - perfilEditor.putInt("pontos", perfilPrefs.getInt("pontos", 0) + 50); perfilEditor.putInt("desafios_concluidos", perfilPrefs.getInt("desafios_concluidos", 0) + 1); perfilEditor.apply(); + + // Mostra o resultado do Exercício! + mostrarResultadoFinal("Desafio Validado! ✅", "Ganhaste +50 Pontos e queimaste " + caloriasAQueimar + " kcal. Continua assim!"); } else { - tvStatusGeralIA.setText("IA: Desafio Falhou ou vídeo pouco claro. ❌"); + mostrarResultadoFinal("Desafio Falhou ❌", "A IA acha que o movimento não foi claro ou bem feito. Tenta outra vez!"); } } editor.apply(); diff --git a/app/src/main/java/com/example/pap/EstatisticasActivity.java b/app/src/main/java/com/example/pap/EstatisticasActivity.java index f9eb020..a1681c0 100644 --- a/app/src/main/java/com/example/pap/EstatisticasActivity.java +++ b/app/src/main/java/com/example/pap/EstatisticasActivity.java @@ -16,12 +16,15 @@ import retrofit2.Response; public class EstatisticasActivity extends AppCompatActivity { - private TextView tvValorIMC, tvStatusIMC, tvCaloriasMeta, tvDicaIA, tvCaloriasQueimadas; - private TextView tvProtGramas, tvHidrGramas, tvGordGramas, tvComida1; + private TextView tvValorIMC, tvStatusIMC, tvCaloriasMeta, tvCaloriasQueimadas; + private TextView tvProtGramas, tvHidrGramas, tvGordGramas, tvComida1, tvCaloriasConsumidas; private TextView tvAguaStats; private ProgressBar progressProt, progressHidr, progressGord; - // COLOCA A TUA API KEY AQUI + // AS 3 TEXTVIEWS DAS NOSSAS IAS ESPECIALIZADAS + private TextView tvAiTopGeral, tvAiMacrosConselho, tvAiProximaRefeicao; + + private int metaCaloriasDiarias = 0; private final String MINHA_API_KEY = "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"; @Override @@ -32,32 +35,35 @@ public class EstatisticasActivity extends AppCompatActivity { tvValorIMC = findViewById(R.id.tvValorIMC); tvStatusIMC = findViewById(R.id.tvStatusIMC); tvCaloriasMeta = findViewById(R.id.tvCaloriasMeta); - - tvDicaIA = findViewById(R.id.tvDicaIA); + tvCaloriasConsumidas = findViewById(R.id.tvCaloriasConsumidas); tvCaloriasQueimadas = findViewById(R.id.tvCaloriasQueimadas); tvProtGramas = findViewById(R.id.tvProtGramas); tvHidrGramas = findViewById(R.id.tvHidrGramas); tvGordGramas = findViewById(R.id.tvGordGramas); tvComida1 = findViewById(R.id.tvComida1); - tvAguaStats = findViewById(R.id.tvAguaStats); progressProt = findViewById(R.id.progressProt); progressHidr = findViewById(R.id.progressHidr); progressGord = findViewById(R.id.progressGord); + // Ligar os IDs das 3 novas caixas de IA + tvAiTopGeral = findViewById(R.id.tvAiTopGeral); + tvAiMacrosConselho = findViewById(R.id.tvAiMacrosConselho); + tvAiProximaRefeicao = findViewById(R.id.tvAiProximaRefeicao); + findViewById(R.id.btnVoltarStats).setOnClickListener(v -> finish()); - // Carrega os painéis locais + // Atualizar painéis locais calcularIMC(); calcularTMB(); carregarMacrosDaIA(); carregarAgua(); carregarCaloriasQueimadas(); - // CHAMA A IA PARA AVALIAR O ESTADO GERAL DO UTILIZADOR - gerarInsightInteligente(); + // ARRANCAR AS 3 IAS EM SIMULTÂNEO! + iniciarComiteIAs(); } private void calcularIMC() { @@ -92,7 +98,7 @@ public class EstatisticasActivity extends AppCompatActivity { SharedPreferences prefs = getSharedPreferences("MeusDadosApp", MODE_PRIVATE); float peso = prefs.getFloat("peso", 0); float alturaMetros = prefs.getFloat("altura", 0); - String sexo = prefs.getString("sexo", "Masculino"); // Assumido Masculino como padrão + String sexo = prefs.getString("sexo", "Masculino"); int idade = prefs.getInt("idade", 20); if (peso > 0 && alturaMetros > 0) { @@ -106,7 +112,8 @@ public class EstatisticasActivity extends AppCompatActivity { } double caloriasTotais = tmb * 1.2; - tvCaloriasMeta.setText(String.valueOf((int) caloriasTotais)); + metaCaloriasDiarias = (int) caloriasTotais; + tvCaloriasMeta.setText(String.valueOf(metaCaloriasDiarias)); } else { tvCaloriasMeta.setText("--"); } @@ -115,11 +122,16 @@ public class EstatisticasActivity extends AppCompatActivity { private void carregarMacrosDaIA() { SharedPreferences prefs = getSharedPreferences("DadosSaude", MODE_PRIVATE); + int caloriasConsumidasHoje = prefs.getInt("cal_hoje", 0); int prot = prefs.getInt("prot_hoje", 0); int hidr = prefs.getInt("hidr_hoje", 0); int gord = prefs.getInt("gord_hoje", 0); String ultimaComida = prefs.getString("ultimo_prato", "Ainda não leste nada hoje."); + int caloriasQueFaltam = metaCaloriasDiarias - caloriasConsumidasHoje; + if (caloriasQueFaltam < 0) caloriasQueFaltam = 0; + tvCaloriasConsumidas.setText("Consumido: " + caloriasConsumidasHoje + " kcal / Faltam: " + caloriasQueFaltam + " kcal"); + tvProtGramas.setText("Proteína: " + prot + "g"); tvHidrGramas.setText("Hidratos: " + hidr + "g"); tvGordGramas.setText("Gordura: " + gord + "g"); @@ -145,12 +157,10 @@ public class EstatisticasActivity extends AppCompatActivity { } // ========================================== - // NOVA FUNÇÃO: O CÉREBRO DA IA NAS ESTATÍSTICAS + // FUNÇÃO MASTER: GERE AS 3 IAS DE SEGUIDA // ========================================== - private void gerarInsightInteligente() { - tvDicaIA.setText("A analisar os teus dados diários... ⏳"); - - // 1. Recolher a informação toda das gavetas (SharedPreferences) + private void iniciarComiteIAs() { + // Puxar os dados locais necessários SharedPreferences prefsDados = getSharedPreferences("MeusDadosApp", MODE_PRIVATE); SharedPreferences prefsSaude = getSharedPreferences("DadosSaude", MODE_PRIVATE); SharedPreferences prefsGam = getSharedPreferences("DadosGamificacao", MODE_PRIVATE); @@ -159,49 +169,102 @@ public class EstatisticasActivity extends AppCompatActivity { float altura = prefsDados.getFloat("altura", 0); float imc = (peso > 0 && altura > 0) ? (peso / (altura * altura)) : 0; + int calConsumidas = prefsSaude.getInt("cal_hoje", 0); int prot = prefsSaude.getInt("prot_hoje", 0); int hidr = prefsSaude.getInt("hidr_hoje", 0); int gord = prefsSaude.getInt("gord_hoje", 0); + String ultimaComida = prefsSaude.getString("ultimo_prato", "Nenhuma"); float litrosAgua = prefsGam.getInt("agua_hoje", 0) * 0.25f; int kcalQueimadas = prefsGam.getInt("calorias_desafios", 0); - // 2. Criar o Prompt a explicar o cenário à IA - String promptDaIA = "És o treinador e nutricionista pessoal do utilizador. " + - "Os dados dele hoje são: IMC = " + String.format(Locale.getDefault(), "%.1f", imc) + ", " + - "Água bebida = " + litrosAgua + " Litros, Calorias Queimadas = " + kcalQueimadas + " kcal. " + - "Macros consumidos: Proteína=" + prot + "g, Hidratos=" + hidr + "g, Gordura=" + gord + "g. " + - "Avalia estes números de forma realista. Se ele bebeu pouca água, avisa-o. Se tem pouca proteína, alerta-o. " + - "Regra de Ouro: Escreve apenas 2 frases curtas, diretas e motivadoras. NÃO USES ASTERISCOS."; + // Chamar os 3 consultores assíncronos + chamarIa3TopGeral(imc, litrosAgua, kcalQueimadas, calConsumidas); + chamarIa1Macros(prot, hidr, gord); + chamarIa2ProximaRefeicao(ultimaComida); + } - // 3. Enviar o pedido usando a classe AiApi que já tens (enviamos só texto, sem imagem) + // --- IA 3: O DIAGNÓSTICO DO TOPO --- + private void chamarIa3TopGeral(float imc, float agua, int queimadas, int consumidas) { + tvAiTopGeral.setText("A avaliar o teu dia geral... ⏳"); + + String prompt = "És um Médico Nutricionista Clínico. Analisa estes dados globais de hoje do paciente: " + + "IMC=" + String.format(Locale.getDefault(), "%.1f", imc) + ", Água=" + agua + "L, " + + "Calorias Gastas em Exercício=" + queimadas + "kcal, Calorias Consumidas=" + consumidas + "kcal (Meta=" + metaCaloriasDiarias + "). " + + "Dá um diagnóstico curto, sério e aponta os riscos de saúde imediatos se os valores forem maus. " + + "Regra: Apenas 2 frases em Português de Portugal. Não uses asteriscos."; + + fazerPedidoIA(prompt, new AiCallback() { + @Override + public void onSucesso(String resposta) { tvAiTopGeral.setText(resposta); } + @Override + public void onFalha() { tvAiTopGeral.setText("Não foi possível processar o diagnóstico geral de saúde."); } + }); + } + + // --- IA 1: O AJUSTADOR DE MACROS --- + private void chamarIa1Macros(int prot, int hidr, int gord) { + tvAiMacrosConselho.setText("A analisar balanço de macronutrientes... ⏳"); + + String prompt = "És um Especialista em Macronutrientes de Alta Performance. O atleta consumiu hoje: " + + "Proteína=" + prot + "g, Hidratos=" + hidr + "g, Gordura=" + gord + "g. " + + "Com base nestes números exatos, diz o que ele deve ajustar ou cortar na próxima refeição de hoje. " + + "Regra: Resposta direta, sem saudações, apenas 2 frases em Português de Portugal. Não uses asteriscos."; + + fazerPedidoIA(prompt, new AiCallback() { + @Override + public void onSucesso(String resposta) { tvAiMacrosConselho.setText(resposta); } + @Override + public void onFalha() { tvAiMacrosConselho.setText("Impossível ligar ao consultor de macros."); } + }); + } + + // --- IA 2: O PLANER DE REFEIÇÕES --- + private void chamarIa2ProximaRefeicao(String ultimaComida) { + tvAiProximaRefeicao.setText("A planear receita de compensação... ⏳"); + + String prompt = "És um Chef Nutricional Inteligente. A última refeição que o utilizador registou foi: '" + ultimaComida + "'. " + + "Com base apenas nisto, sugere concretamente uma opção de refeição (ou snack) saudável e equilibrada para ele comer a seguir. " + + "Regra: Dá uma sugestão específica de comida em 2 frases curtas, em Português de Portugal. Não uses asteriscos."; + + fazerPedidoIA(prompt, new AiCallback() { + @Override + public void onSucesso(String resposta) { tvAiProximaRefeicao.setText(resposta); } + @Override + public void onFalha() { tvAiProximaRefeicao.setText("Ativa a internet para veres a sugestão do Chef IA."); } + }); + } + + // --- MOTOR GENÉRICO DE NETWORK PARA RETROFIT --- + private void fazerPedidoIA(String prompt, final AiCallback callback) { AiRequest request = new AiRequest(Collections.singletonList( - new Message("user", Collections.singletonList(new ContentPart("text", promptDaIA))) + new Message("user", Collections.singletonList(new ContentPart("text", prompt))) )); AiConfig.getRetrofit().create(AiApi.class) - .analisarImagem("Bearer " + MINHA_API_KEY, request) // Usamos o mesmo método mas sem enviar a foto + .analisarImagem("Bearer " + MINHA_API_KEY, request) .enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful() && response.body() != null) { try { - String resposta = response.body().choices.get(0).message.content; - // Limpa qualquer formatação extra que a IA tente mandar - String dicaLimpa = resposta.replace("**", "").replace("*", ""); - tvDicaIA.setText(dicaLimpa); - } catch (Exception e) { - tvDicaIA.setText("Continua o bom trabalho! Mantém o foco na água e macros."); - } - } else { - tvDicaIA.setText("Servidor indisponível no momento. Foca-te em beber água!"); - } + String texto = response.body().choices.get(0).message.content; + String limpo = texto.replace("**", "").replace("*", ""); + callback.onSucesso(limpo.trim()); + } catch (Exception e) { callback.onFalha(); } + } else { callback.onFalha(); } } @Override public void onFailure(Call call, Throwable t) { - tvDicaIA.setText("Estás offline. Regista os dados localmente!"); + callback.onFalha(); } }); } + + // Interface interna para gerir as respostas assíncronas em paralelo + interface AiCallback { + void onSucesso(String resposta); + void onFalha(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/FotoActivity.java b/app/src/main/java/com/example/pap/FotoActivity.java index 8ae6bd4..206dc12 100644 --- a/app/src/main/java/com/example/pap/FotoActivity.java +++ b/app/src/main/java/com/example/pap/FotoActivity.java @@ -38,7 +38,7 @@ public class FotoActivity extends AppCompatActivity { private Bitmap imagemCapturada; private String textoAnalise = ""; - // MANTÉM A TUA CHAVE AQUI + // A TUA CHAVE DA API private final String MINHA_API_KEY = "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"; @Override @@ -46,12 +46,13 @@ public class FotoActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_foto); + // Ligar ao XML ivFotoComida = findViewById(R.id.ivFotoComida); btnTirarFoto = findViewById(R.id.btnTirarFoto); btnGaleria = findViewById(R.id.btnGaleria); btnAnalisarIA = findViewById(R.id.btnAnalisarIA); btnIrParaChat = findViewById(R.id.btnIrParaChat); - btnCorrigir = findViewById(R.id.btnCorrigir); // Ligado ao XML + btnCorrigir = findViewById(R.id.btnCorrigir); tvResultadoIA = findViewById(R.id.tvResultadoIA); ActivityResultLauncher camLauncher = registerForActivityResult( @@ -86,6 +87,7 @@ public class FotoActivity extends AppCompatActivity { }); btnTirarFoto.setOnClickListener(v -> camLauncher.launch(new Intent(MediaStore.ACTION_IMAGE_CAPTURE))); + btnGaleria.setOnClickListener(v -> { Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); galLauncher.launch(intent); @@ -100,7 +102,7 @@ public class FotoActivity extends AppCompatActivity { startActivity(intent); }); - // NOVO: Clique para corrigir erro + // Clique para corrigir o erro da IA btnCorrigir.setOnClickListener(v -> mostrarPopupCorrecao()); findViewById(R.id.btnVoltarFoto).setOnClickListener(v -> finish()); @@ -113,12 +115,12 @@ public class FotoActivity extends AppCompatActivity { ivFotoComida.setImageBitmap(imagemCapturada); btnAnalisarIA.setVisibility(View.VISIBLE); btnIrParaChat.setVisibility(View.GONE); - btnCorrigir.setVisibility(View.GONE); // Esconde a correção até analisar + btnCorrigir.setVisibility(View.GONE); tvResultadoIA.setText("Pronto para analisar."); } } - // Função melhorada que aceita a comida certa se o utilizador corrigir + // Função blindada contra erros da IA private void enviarParaIA(String comidaCerta) { tvResultadoIA.setText("A processar... ⏳"); btnAnalisarIA.setEnabled(false); @@ -131,23 +133,22 @@ public class FotoActivity extends AppCompatActivity { String ordemParaIA; if (comidaCerta == null) { - // Análise normal - ordemParaIA = "És um nutricionista prático. Identifica a comida e dá os valores de forma SUPER RESUMIDA. " + - "REGRAS: 1. Português de Portugal. 2. SEM asteriscos. 3. Máximo 4 linhas. " + - "Formato exato: \n" + + // Regras super restritas para a primeira análise + ordemParaIA = "És uma API de nutrição. Avalia a foto. É ESTRITAMENTE PROIBIDO usar texto de conversa, saudações ou tags de segurança. " + + "Responde APENAS E SÓ neste formato exato:\n" + "Prato: [Nome]\n" + "Calorias: [Valor] kcal\n" + "Macros: [X]g Proteína, [X]g Hidratos, [X]g Gordura\n" + - "Dica: [Uma frase curta]."; + "Dica: [Frase curta sem asteriscos]."; } else { - // Análise forçada com a correção do utilizador - ordemParaIA = "Atenção: A tua análise anterior falhou. O prato na imagem é na verdade: '" + comidaCerta + "'. " + - "Esquece tudo o resto e foca-te em dar os valores reais APENAS para '" + comidaCerta + "'. " + - "Usa este formato exato: \n" + + // Regras super restritas para a correção + ordemParaIA = "Atenção: ignora a imagem. O utilizador confirmou que o prato é '" + comidaCerta + "'. " + + "É ESTRITAMENTE PROIBIDO usar texto de conversa ou avisos de segurança (ex: User:safe). " + + "Responde APENAS E SÓ com os valores nutricionais médios para '" + comidaCerta + "' neste formato exato:\n" + "Prato: " + comidaCerta + "\n" + "Calorias: [Valor] kcal\n" + "Macros: [X]g Proteína, [X]g Hidratos, [X]g Gordura\n" + - "Dica: [Frase de saúde curta e sem asteriscos]."; + "Dica: [Frase curta sem asteriscos]."; } AiRequest request = new AiRequest(Collections.singletonList( @@ -167,9 +168,15 @@ public class FotoActivity extends AppCompatActivity { try { String resposta = response.body().choices.get(0).message.content; textoAnalise = resposta.replace("**", "").replace("*", ""); - tvResultadoIA.setText(textoAnalise); - // Mostra os botões + // O NOSSO ESCUDO: Se a resposta não tiver a palavra "Calorias", a IA deu tilt! + if (!textoAnalise.contains("Calorias:") || !textoAnalise.contains("Macros:")) { + tvResultadoIA.setText("A IA ficou confusa com o prato 😵\u200D💫. Clica em 'Corrigir' e tenta ser mais específico (Ex: Bife com Arroz)."); + btnCorrigir.setVisibility(View.VISIBLE); + return; // Pára tudo aqui, não guarda lixo na memória! + } + + tvResultadoIA.setText(textoAnalise); btnIrParaChat.setVisibility(View.VISIBLE); btnCorrigir.setVisibility(View.VISIBLE); @@ -181,8 +188,8 @@ public class FotoActivity extends AppCompatActivity { // Guarda a nova resposta extrairEGuardarDados(textoAnalise); - } catch (Exception e) { tvResultadoIA.setText("Erro na resposta."); } - } else { tvResultadoIA.setText("Erro: " + response.code()); } + } catch (Exception e) { tvResultadoIA.setText("Erro na leitura da resposta."); } + } else { tvResultadoIA.setText("Erro no servidor: " + response.code()); } } @Override public void onFailure(Call call, Throwable t) { @@ -209,7 +216,7 @@ public class FotoActivity extends AppCompatActivity { builder.setPositiveButton("Re-Analisar", (dialog, which) -> { String comidaCerta = input.getText().toString().trim(); if (!comidaCerta.isEmpty()) { - enviarParaIA(comidaCerta); // Manda o texto escrito pelo user + enviarParaIA(comidaCerta); } else { Toast.makeText(FotoActivity.this, "Tens de escrever a comida!", Toast.LENGTH_SHORT).show(); } diff --git a/app/src/main/res/layout/activity_estatisticas.xml b/app/src/main/res/layout/activity_estatisticas.xml index 7b16240..14a1d57 100644 --- a/app/src/main/res/layout/activity_estatisticas.xml +++ b/app/src/main/res/layout/activity_estatisticas.xml @@ -53,10 +53,10 @@ app:cardElevation="0dp" app:cardBackgroundColor="#F8FAFC"> - + - - + + @@ -125,7 +125,6 @@ android:orientation="horizontal" android:layout_marginBottom="16dp" android:baselineAligned="false"> - - - - + + - - + - - + - + + + + + + + + + + @@ -190,6 +202,20 @@ + + + + + + + + + \ No newline at end of file