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">
-
+
+
+
+
+
+