Guardar histórico e melhorar estabilidade do chat

This commit is contained in:
2026-06-22 14:27:19 +01:00
parent 4786a9c740
commit 8c15b7a573
3 changed files with 178 additions and 50 deletions

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,5 +1,6 @@
package com.example.api; package com.example.api;
import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
@@ -16,6 +17,7 @@ import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@@ -32,11 +34,20 @@ import java.nio.charset.StandardCharsets;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
private static final String API_URL = "https://text.pollinations.ai/openai"; 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 LinearLayout messagesContainer;
private ScrollView chatScroll; private ScrollView chatScroll;
private EditText promptInput; private EditText promptInput;
private MaterialButton sendButton; private MaterialButton sendButton;
private MaterialButton newChatButton;
private SharedPreferences preferences;
private JSONArray conversation = new JSONArray();
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -54,10 +65,14 @@ public class MainActivity extends AppCompatActivity {
chatScroll = findViewById(R.id.chatScroll); chatScroll = findViewById(R.id.chatScroll);
promptInput = findViewById(R.id.promptInput); promptInput = findViewById(R.id.promptInput);
sendButton = findViewById(R.id.sendButton); 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()); sendButton.setOnClickListener(v -> sendPrompt());
newChatButton.setOnClickListener(v -> confirmNewChat());
promptInput.setOnEditorActionListener((v, actionId, event) -> { promptInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEND) { if (actionId == EditorInfo.IME_ACTION_SEND) {
sendPrompt(); sendPrompt();
@@ -74,17 +89,21 @@ public class MainActivity extends AppCompatActivity {
} }
promptInput.setText(""); promptInput.setText("");
addConversationMessage("user", prompt);
addMessage(prompt, true); addMessage(prompt, true);
TextView loadingMessage = addMessage("A pensar...", false); TextView loadingMessage = addMessage("A pensar...", false);
setSending(true); setSending(true);
new Thread(() -> { new Thread(() -> {
try { try {
String answer = askAi(prompt); String answer = askAiWithRetry();
runOnUiThread(() -> loadingMessage.setText(answer)); runOnUiThread(() -> {
loadingMessage.setText(answer);
addConversationMessage("assistant", answer);
});
} catch (Exception exception) { } catch (Exception exception) {
runOnUiThread(() -> loadingMessage.setText( 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 { } finally {
runOnUiThread(() -> setSending(false)); runOnUiThread(() -> setSending(false));
@@ -92,11 +111,85 @@ public class MainActivity extends AppCompatActivity {
}).start(); }).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) { private void setSending(boolean sending) {
sendButton.setEnabled(!sending); sendButton.setEnabled(!sending);
newChatButton.setEnabled(!sending);
sendButton.setText(sending ? "..." : "Enviar"); 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) { private TextView addMessage(String message, boolean fromUser) {
LinearLayout row = new LinearLayout(this); LinearLayout row = new LinearLayout(this);
row.setGravity(fromUser ? Gravity.END : Gravity.START); row.setGravity(fromUser ? Gravity.END : Gravity.START);
@@ -117,50 +210,58 @@ public class MainActivity extends AppCompatActivity {
return bubble; return bubble;
} }
private String askAi(String userPrompt) throws Exception { private String askAi() throws Exception {
JSONObject systemMessage = new JSONObject(); JSONObject systemMessage = new JSONObject();
systemMessage.put("role", "system"); systemMessage.put("role", "system");
systemMessage.put( systemMessage.put(
"content", "content",
"Responde sempre em português europeu. " "Responde sempre em português europeu. Sê natural, útil e claro. "
+ "Sê natural, curto e claro, como uma IA de chat para um trabalho da escola." + "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(); JSONArray messages = new JSONArray();
messages.put(systemMessage); 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(); JSONObject requestBody = new JSONObject();
requestBody.put("model", "openai"); requestBody.put("model", "openai");
requestBody.put("messages", messages); requestBody.put("messages", messages);
HttpURLConnection connection = (HttpURLConnection) new URL(API_URL).openConnection(); HttpURLConnection connection = (HttpURLConnection) new URL(API_URL).openConnection();
connection.setRequestMethod("POST"); try {
connection.setConnectTimeout(20000); connection.setRequestMethod("POST");
connection.setReadTimeout(60000); connection.setConnectTimeout(20000);
connection.setDoOutput(true); connection.setReadTimeout(70000);
connection.setRequestProperty("Content-Type", "application/json"); connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
byte[] body = requestBody.toString().getBytes(StandardCharsets.UTF_8); byte[] body = requestBody.toString().getBytes(StandardCharsets.UTF_8);
try (OutputStream outputStream = connection.getOutputStream()) { try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(body); 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 { private String readStream(InputStream stream) throws IOException {
@@ -200,4 +301,3 @@ public class MainActivity extends AppCompatActivity {
return Math.round(value * getResources().getDisplayMetrics().density); return Math.round(value * getResources().getDisplayMetrics().density);
} }
} }
q

View File

@@ -12,30 +12,52 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="150dp" android:layout_height="160dp"
android:background="@drawable/bg_chat_header" android:background="@drawable/bg_chat_header"
android:gravity="bottom" android:gravity="bottom|center_vertical"
android:orientation="vertical" android:orientation="horizontal"
android:paddingStart="24dp" android:paddingStart="24dp"
android:paddingEnd="24dp" android:paddingEnd="24dp"
android:paddingBottom="24dp"> android:paddingBottom="24dp">
<TextView <LinearLayout
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:includeFontPadding="false" android:layout_marginEnd="12dp"
android:text="Chat IA" android:layout_weight="1"
android:textColor="@color/white" android:orientation="vertical">
android:textSize="30sp"
android:textStyle="bold" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp" android:includeFontPadding="false"
android:text="Pergunta alguma coisa e recebe uma resposta da IA." android:text="Chat IA"
android:textColor="#EEF9F7" android:textColor="@color/white"
android:textSize="15sp" /> android:textSize="30sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="A conversa fica guardada neste dispositivo."
android:textColor="#EEF9F7"
android:textSize="14sp" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/newChatButton"
android:layout_width="104dp"
android:layout_height="46dp"
android:text="Novo chat"
android:textAllCaps="false"
android:textColor="@color/white"
android:textSize="13sp"
android:textStyle="bold"
app:backgroundTint="#26FFFFFF"
app:cornerRadius="18dp"
app:strokeColor="#70FFFFFF"
app:strokeWidth="1dp" />
</LinearLayout> </LinearLayout>
<ScrollView <ScrollView