Guardar histórico e melhorar estabilidade do chat
This commit is contained in:
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal 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>
|
||||||
@@ -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,33 +210,38 @@ 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();
|
||||||
|
try {
|
||||||
connection.setRequestMethod("POST");
|
connection.setRequestMethod("POST");
|
||||||
connection.setConnectTimeout(20000);
|
connection.setConnectTimeout(20000);
|
||||||
connection.setReadTimeout(60000);
|
connection.setReadTimeout(70000);
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
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()) {
|
||||||
@@ -161,6 +259,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return readAiText(response);
|
return readAiText(response);
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
@@ -12,14 +12,21 @@
|
|||||||
|
|
||||||
<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">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -33,9 +40,24 @@
|
|||||||
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:layout_marginTop="6dp"
|
||||||
android:text="Pergunta alguma coisa e recebe uma resposta da IA."
|
android:text="A conversa fica guardada neste dispositivo."
|
||||||
android:textColor="#EEF9F7"
|
android:textColor="#EEF9F7"
|
||||||
android:textSize="15sp" />
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user