diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/api/MainActivity.java b/app/src/main/java/com/example/api/MainActivity.java index 9009ed3..2172840 100644 --- a/app/src/main/java/com/example/api/MainActivity.java +++ b/app/src/main/java/com/example/api/MainActivity.java @@ -1,5 +1,6 @@ package com.example.api; +import android.content.SharedPreferences; import android.os.Bundle; import android.view.Gravity; import android.view.View; @@ -16,6 +17,7 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import com.google.android.material.button.MaterialButton; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.json.JSONArray; import org.json.JSONObject; @@ -32,11 +34,20 @@ import java.nio.charset.StandardCharsets; public class MainActivity extends AppCompatActivity { private static final String API_URL = "https://text.pollinations.ai/openai"; + private static final String PREFS_NAME = "chat_preferences"; + private static final String HISTORY_KEY = "chat_history"; + private static final int MAX_CONTEXT_MESSAGES = 24; + private static final int MAX_ATTEMPTS = 3; + private static final String WELCOME_MESSAGE = + "Olá! Sou uma IA ligada a uma API. Faz-me uma pergunta."; private LinearLayout messagesContainer; private ScrollView chatScroll; private EditText promptInput; private MaterialButton sendButton; + private MaterialButton newChatButton; + private SharedPreferences preferences; + private JSONArray conversation = new JSONArray(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -54,10 +65,14 @@ public class MainActivity extends AppCompatActivity { chatScroll = findViewById(R.id.chatScroll); promptInput = findViewById(R.id.promptInput); sendButton = findViewById(R.id.sendButton); + newChatButton = findViewById(R.id.newChatButton); + preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); - addMessage("Olá! Sou uma IA ligada a uma API. Faz-me uma pergunta.", false); + loadConversation(); + renderConversation(); sendButton.setOnClickListener(v -> sendPrompt()); + newChatButton.setOnClickListener(v -> confirmNewChat()); promptInput.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND) { sendPrompt(); @@ -74,17 +89,21 @@ public class MainActivity extends AppCompatActivity { } promptInput.setText(""); + addConversationMessage("user", prompt); addMessage(prompt, true); TextView loadingMessage = addMessage("A pensar...", false); setSending(true); new Thread(() -> { try { - String answer = askAi(prompt); - runOnUiThread(() -> loadingMessage.setText(answer)); + String answer = askAiWithRetry(); + runOnUiThread(() -> { + loadingMessage.setText(answer); + addConversationMessage("assistant", answer); + }); } catch (Exception exception) { runOnUiThread(() -> loadingMessage.setText( - "Não consegui ligar à API agora. Verifica a internet e tenta novamente." + "A API está ocupada ou sem ligação. Tenta enviar novamente dentro de alguns segundos." )); } finally { runOnUiThread(() -> setSending(false)); @@ -92,11 +111,85 @@ public class MainActivity extends AppCompatActivity { }).start(); } + private String askAiWithRetry() throws Exception { + Exception lastError = null; + for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + return askAi(); + } catch (Exception exception) { + lastError = exception; + if (attempt < MAX_ATTEMPTS) { + Thread.sleep(attempt * 1200L); + } + } + } + throw lastError == null ? new IOException("API unavailable") : lastError; + } + private void setSending(boolean sending) { sendButton.setEnabled(!sending); + newChatButton.setEnabled(!sending); sendButton.setText(sending ? "..." : "Enviar"); } + private void loadConversation() { + String savedHistory = preferences.getString(HISTORY_KEY, ""); + if (savedHistory != null && !savedHistory.isEmpty()) { + try { + conversation = new JSONArray(savedHistory); + } catch (Exception ignored) { + conversation = new JSONArray(); + } + } + + if (conversation.length() == 0) { + addConversationMessage("assistant", WELCOME_MESSAGE); + } + } + + private void renderConversation() { + messagesContainer.removeAllViews(); + for (int i = 0; i < conversation.length(); i++) { + JSONObject message = conversation.optJSONObject(i); + if (message == null) { + continue; + } + String role = message.optString("role", ""); + String content = message.optString("content", ""); + if (!content.isEmpty()) { + addMessage(content, "user".equals(role)); + } + } + } + + private void addConversationMessage(String role, String content) { + try { + JSONObject message = new JSONObject(); + message.put("role", role); + message.put("content", content); + conversation.put(message); + preferences.edit().putString(HISTORY_KEY, conversation.toString()).apply(); + } catch (Exception ignored) { + // The message is still visible even if local storage is unavailable. + } + } + + private void confirmNewChat() { + new MaterialAlertDialogBuilder(this) + .setTitle("Começar um novo chat?") + .setMessage("A conversa guardada neste dispositivo será apagada.") + .setNegativeButton("Cancelar", null) + .setPositiveButton("Apagar", (dialog, which) -> resetConversation()) + .show(); + } + + private void resetConversation() { + conversation = new JSONArray(); + preferences.edit().remove(HISTORY_KEY).apply(); + addConversationMessage("assistant", WELCOME_MESSAGE); + renderConversation(); + } + private TextView addMessage(String message, boolean fromUser) { LinearLayout row = new LinearLayout(this); row.setGravity(fromUser ? Gravity.END : Gravity.START); @@ -117,50 +210,58 @@ public class MainActivity extends AppCompatActivity { return bubble; } - private String askAi(String userPrompt) throws Exception { + private String askAi() throws Exception { JSONObject systemMessage = new JSONObject(); systemMessage.put("role", "system"); systemMessage.put( "content", - "Responde sempre em português europeu. " - + "Sê natural, curto e claro, como uma IA de chat para um trabalho da escola." + "Responde sempre em português europeu. Sê natural, útil e claro. " + + "Mantém o contexto da conversa e responde como uma IA de chat." ); - JSONObject userMessage = new JSONObject(); - userMessage.put("role", "user"); - userMessage.put("content", userPrompt); - JSONArray messages = new JSONArray(); messages.put(systemMessage); - messages.put(userMessage); + + int firstMessage = Math.max(0, conversation.length() - MAX_CONTEXT_MESSAGES); + for (int i = firstMessage; i < conversation.length(); i++) { + JSONObject message = conversation.optJSONObject(i); + if (message != null) { + messages.put(message); + } + } JSONObject requestBody = new JSONObject(); requestBody.put("model", "openai"); requestBody.put("messages", messages); HttpURLConnection connection = (HttpURLConnection) new URL(API_URL).openConnection(); - connection.setRequestMethod("POST"); - connection.setConnectTimeout(20000); - connection.setReadTimeout(60000); - connection.setDoOutput(true); - connection.setRequestProperty("Content-Type", "application/json"); + try { + connection.setRequestMethod("POST"); + connection.setConnectTimeout(20000); + connection.setReadTimeout(70000); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); - byte[] body = requestBody.toString().getBytes(StandardCharsets.UTF_8); - try (OutputStream outputStream = connection.getOutputStream()) { - outputStream.write(body); + byte[] body = requestBody.toString().getBytes(StandardCharsets.UTF_8); + try (OutputStream outputStream = connection.getOutputStream()) { + outputStream.write(body); + } + + int responseCode = connection.getResponseCode(); + InputStream stream = responseCode >= 200 && responseCode < 300 + ? connection.getInputStream() + : connection.getErrorStream(); + String response = readStream(stream); + + if (responseCode < 200 || responseCode >= 300) { + throw new IOException("API error: " + responseCode); + } + + return readAiText(response); + } finally { + connection.disconnect(); } - - int responseCode = connection.getResponseCode(); - InputStream stream = responseCode >= 200 && responseCode < 300 - ? connection.getInputStream() - : connection.getErrorStream(); - String response = readStream(stream); - - if (responseCode < 200 || responseCode >= 300) { - throw new IOException("API error: " + responseCode); - } - - return readAiText(response); } private String readStream(InputStream stream) throws IOException { @@ -200,4 +301,3 @@ public class MainActivity extends AppCompatActivity { return Math.round(value * getResources().getDisplayMetrics().density); } } -q \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c261444..9ba5c05 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -12,30 +12,52 @@ - + android:layout_marginEnd="12dp" + android:layout_weight="1" + android:orientation="vertical"> - + + + + + +