commit 4786a9c7405f6d9bc37887cbbf3b0f5e6aba2e41 Author: 230410 <230410@epvc.pt> Date: Tue Jun 16 17:15:25 2026 +0100 Adicionar chat IA com API diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62a860d --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +.env.local diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..d0ff72c --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml new file mode 100644 index 0000000..539e3b8 --- /dev/null +++ b/.idea/studiobot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..edef458 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "com.example.api" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.api" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/api/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/api/ExampleInstrumentedTest.java new file mode 100644 index 0000000..431307d --- /dev/null +++ b/app/src/androidTest/java/com/example/api/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.api; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.api", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..cda226a --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/api/MainActivity.java b/app/src/main/java/com/example/api/MainActivity.java new file mode 100644 index 0000000..9009ed3 --- /dev/null +++ b/app/src/main/java/com/example/api/MainActivity.java @@ -0,0 +1,203 @@ +package com.example.api; + +import android.os.Bundle; +import android.view.Gravity; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.google.android.material.button.MaterialButton; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class MainActivity extends AppCompatActivity { + + private static final String API_URL = "https://text.pollinations.ai/openai"; + + private LinearLayout messagesContainer; + private ScrollView chatScroll; + private EditText promptInput; + private MaterialButton sendButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + messagesContainer = findViewById(R.id.messagesContainer); + chatScroll = findViewById(R.id.chatScroll); + promptInput = findViewById(R.id.promptInput); + sendButton = findViewById(R.id.sendButton); + + addMessage("Olá! Sou uma IA ligada a uma API. Faz-me uma pergunta.", false); + + sendButton.setOnClickListener(v -> sendPrompt()); + promptInput.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_SEND) { + sendPrompt(); + return true; + } + return false; + }); + } + + private void sendPrompt() { + String prompt = promptInput.getText().toString().trim(); + if (prompt.isEmpty()) { + return; + } + + promptInput.setText(""); + addMessage(prompt, true); + TextView loadingMessage = addMessage("A pensar...", false); + setSending(true); + + new Thread(() -> { + try { + String answer = askAi(prompt); + runOnUiThread(() -> loadingMessage.setText(answer)); + } catch (Exception exception) { + runOnUiThread(() -> loadingMessage.setText( + "Não consegui ligar à API agora. Verifica a internet e tenta novamente." + )); + } finally { + runOnUiThread(() -> setSending(false)); + } + }).start(); + } + + private void setSending(boolean sending) { + sendButton.setEnabled(!sending); + sendButton.setText(sending ? "..." : "Enviar"); + } + + private TextView addMessage(String message, boolean fromUser) { + LinearLayout row = new LinearLayout(this); + row.setGravity(fromUser ? Gravity.END : Gravity.START); + row.setPadding(0, dp(6), 0, dp(6)); + + TextView bubble = new TextView(this); + bubble.setText(message); + bubble.setTextSize(15); + bubble.setLineSpacing(dp(2), 1f); + bubble.setMaxWidth(getResources().getDisplayMetrics().widthPixels - dp(72)); + bubble.setPadding(dp(14), dp(10), dp(14), dp(10)); + bubble.setTextColor(fromUser ? getColor(R.color.white) : getColor(R.color.text_primary)); + bubble.setBackgroundResource(fromUser ? R.drawable.bg_user_message : R.drawable.bg_ai_message); + + row.addView(bubble); + messagesContainer.addView(row); + chatScroll.post(() -> chatScroll.fullScroll(View.FOCUS_DOWN)); + return bubble; + } + + private String askAi(String userPrompt) 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." + ); + + JSONObject userMessage = new JSONObject(); + userMessage.put("role", "user"); + userMessage.put("content", userPrompt); + + JSONArray messages = new JSONArray(); + messages.put(systemMessage); + messages.put(userMessage); + + 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"); + + 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); + } + + private String readStream(InputStream stream) throws IOException { + if (stream == null) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(stream, StandardCharsets.UTF_8) + )) { + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + } + return builder.toString(); + } + + private String readAiText(String response) throws Exception { + JSONObject json = new JSONObject(response); + JSONArray choices = json.optJSONArray("choices"); + if (choices == null || choices.length() == 0) { + throw new IOException("Response without choices"); + } + + JSONObject firstChoice = choices.optJSONObject(0); + JSONObject message = firstChoice == null ? null : firstChoice.optJSONObject("message"); + String text = message == null ? "" : message.optString("content", "").trim(); + if (text.isEmpty()) { + throw new IOException("Response without text"); + } + return text; + } + + private int dp(int value) { + return Math.round(value * getResources().getDisplayMetrics().density); + } +} +q \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_ai_message.xml b/app/src/main/res/drawable/bg_ai_message.xml new file mode 100644 index 0000000..ff7fe1f --- /dev/null +++ b/app/src/main/res/drawable/bg_ai_message.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_bottom_bar.xml b/app/src/main/res/drawable/bg_bottom_bar.xml new file mode 100644 index 0000000..26fb5a5 --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom_bar.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/bg_chat_header.xml b/app/src/main/res/drawable/bg_chat_header.xml new file mode 100644 index 0000000..376b9af --- /dev/null +++ b/app/src/main/res/drawable/bg_chat_header.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_input.xml b/app/src/main/res/drawable/bg_input.xml new file mode 100644 index 0000000..b10de48 --- /dev/null +++ b/app/src/main/res/drawable/bg_input.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_user_message.xml b/app/src/main/res/drawable/bg_user_message.xml new file mode 100644 index 0000000..d985056 --- /dev/null +++ b/app/src/main/res/drawable/bg_user_message.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ 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 new file mode 100644 index 0000000..c261444 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..09b9560 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..5d54b1c --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,15 @@ + + + #FF000000 + #FFFFFFFF + #F7F4EF + #1E2933 + #6B7280 + #FFFFFFFF + #E5E1DA + #138C86 + #0D5E5A + #F2665E + #138C86 + #FFFFFF + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..12c9193 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Chat IA + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..26da9cf --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +