diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 732e8aed44..253caca687 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -478,6 +478,7 @@ public class AdapterMessage extends RecyclerView.Adapter 0 || tos > 0) ? View.VISIBLE : View.GONE); ibTranslate.setVisibility(tools && !outbox && button_translate && DeepL.isAvailable(context) && message.content ? View.VISIBLE : View.GONE); + ibSummarize.setVisibility(tools && !outbox && button_summarize && (OpenAI.isAvailable(context) || Gemini.isAvailable(context)) && message.content ? View.VISIBLE : View.GONE); ibFullScreen.setVisibility(tools && full && button_full_screen && message.content ? View.VISIBLE : View.GONE); ibForceLight.setVisibility(tools && full && dark && button_force_light && message.content ? View.VISIBLE : View.GONE); ibForceLight.setImageLevel(!(canDarken || fake_dark) || force_light ? 1 : 0); @@ -4529,6 +4537,8 @@ public class AdapterMessage extends RecyclerView.Adapter. + + Copyright 2018-2024 by Marcel Bokhorst (M66B) +*/ + +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.ArrowKeyMovementMethod; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StyleSpan; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ImageButton; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.preference.PreferenceManager; + +import org.jsoup.nodes.Document; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.mail.Address; + +public class FragmentDialogSummarize extends FragmentDialogBase { + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final Context context = getContext(); + final View view = LayoutInflater.from(context).inflate(R.layout.dialog_summarize, null); + final TextView tvSummary = view.findViewById(R.id.tvSummary); + final ContentLoadingProgressBar pbWait = view.findViewById(R.id.pbWait); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean compact = prefs.getBoolean("compact", false); + int zoom = prefs.getInt("view_zoom", compact ? 0 : 1); + int message_zoom = prefs.getInt("message_zoom", 100); + + float textSize = Helper.getTextSize(context, zoom) * message_zoom / 100f; + tvSummary.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + + tvSummary.setText(null); + + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + pbWait.setVisibility(View.VISIBLE); + } + + @Override + protected void onPostExecute(Bundle args) { + pbWait.setVisibility(View.GONE); + } + + @Override + protected String onExecute(Context context, Bundle args) throws Throwable { + long id = args.getLong("id"); + + File file = EntityMessage.getFile(context, id); + Document d = JsoupEx.parse(file); + d = HtmlHelper.sanitizeView(context, d, false); + HtmlHelper.removeSignatures(d); + d.select("blockquote").remove(); + HtmlHelper.truncate(d, HtmlHelper.MAX_TRANSLATABLE_TEXT_SIZE); + String text = d.text(); + + if (OpenAI.isAvailable(context)) { + String model = prefs.getString("openai_model", "gpt-3.5-turbo"); + float temperature = prefs.getFloat("openai_temperature", 0.5f); + + List result = new ArrayList<>(); + result.add(new OpenAI.Message("assistant", OpenAI.SUMMARY_PROMPT)); + result.add(new OpenAI.Message("user", text)); + OpenAI.Message[] completions = + OpenAI.completeChat(context, model, result.toArray(new OpenAI.Message[0]), temperature, 1); + StringBuilder sb = new StringBuilder(); + for (OpenAI.Message completion : completions) { + if (sb.length() != 0) + sb.append('\n'); + sb.append(completion.getContent()); + } + return sb.toString(); + } else if (Gemini.isAvailable(context)) { + String model = prefs.getString("gemini_model", "gemini-pro"); + + String[] result = Gemini.generate(context, model, new String[]{Gemini.SUMMARY_PROMPT, text}); + return TextUtils.join("\n", result); + } + + return null; + } + + @Override + protected void onExecuted(Bundle args, String text) { + tvSummary.setText(text); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + tvSummary.setText(new ThrowableWrapper(ex).toSafeString()); + } + }.execute(this, getArguments(), "message:summarize"); + + AlertDialog.Builder builder = new AlertDialog.Builder(context) + .setView(view) + .setPositiveButton(android.R.string.cancel, null); + + return builder.create(); + } +} diff --git a/app/src/main/java/eu/faircode/email/Gemini.java b/app/src/main/java/eu/faircode/email/Gemini.java index 3019c03c5c..5add2031f5 100644 --- a/app/src/main/java/eu/faircode/email/Gemini.java +++ b/app/src/main/java/eu/faircode/email/Gemini.java @@ -19,7 +19,6 @@ package eu.faircode.email; Copyright 2018-2024 by Marcel Bokhorst (M66B) */ - import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; @@ -42,6 +41,8 @@ import javax.net.ssl.HttpsURLConnection; public class Gemini { // https://ai.google.dev/models/gemini + static final String SUMMARY_PROMPT = "Summarize the following text:"; + private static final int MAX_GEMINI_LEN = 4000; // characters private static final int TIMEOUT = 30; // seconds diff --git a/app/src/main/java/eu/faircode/email/OpenAI.java b/app/src/main/java/eu/faircode/email/OpenAI.java index a52aad68c5..282b18e4c8 100644 --- a/app/src/main/java/eu/faircode/email/OpenAI.java +++ b/app/src/main/java/eu/faircode/email/OpenAI.java @@ -43,6 +43,8 @@ import java.util.Objects; import javax.net.ssl.HttpsURLConnection; public class OpenAI { + static final String SUMMARY_PROMPT = "Summarize the following text:"; + private static final int MAX_OPENAI_LEN = 1000; // characters private static final int TIMEOUT = 45; // seconds diff --git a/app/src/main/res/layout/dialog_buttons.xml b/app/src/main/res/layout/dialog_buttons.xml index 7328dcc82d..0bc2a9ce80 100644 --- a/app/src/main/res/layout/dialog_buttons.xml +++ b/app/src/main/res/layout/dialog_buttons.xml @@ -187,6 +187,19 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/cbImportance" /> + + + app:layout_constraintTop_toBottomOf="@id/cbSummarize" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/include_message_navigation.xml b/app/src/main/res/layout/include_message_navigation.xml index 5b94c46457..425008b944 100644 --- a/app/src/main/res/layout/include_message_navigation.xml +++ b/app/src/main/res/layout/include_message_navigation.xml @@ -50,7 +50,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="3dp" - app:constraint_referenced_ids="vwEmpty,ibMore,ibInbox,ibJunk,ibTrash,ibArchive,ibMove,ibCopy,ibKeywords,ibLabels,ibNotes,ibSeen,ibHide,ibImportance,ibTranslate,ibFullScreen,ibForceLight,ibSearch,ibSearchText,ibEvent,ibShare,ibPin,ibPrint,ibHeaders,ibHtml,ibRaw,ibUnsubscribe,ibRule,ibAnswer,ibUndo" + app:constraint_referenced_ids="vwEmpty,ibMore,ibInbox,ibJunk,ibTrash,ibArchive,ibMove,ibCopy,ibKeywords,ibLabels,ibNotes,ibSeen,ibHide,ibImportance,ibTranslate,ibSummarize,ibFullScreen,ibForceLight,ibSearch,ibSearchText,ibEvent,ibShare,ibPin,ibPrint,ibHeaders,ibHtml,ibRaw,ibUnsubscribe,ibRule,ibAnswer,ibUndo" app:flow_horizontalBias="0" app:flow_horizontalGap="3dp" app:flow_horizontalStyle="packed" @@ -263,6 +263,18 @@ app:srcCompat="@drawable/twotone_translate_24" tools:ignore="MissingConstraints" /> + + + + Create template Select default address Translate + Summarize OpenAI (ChatGPT) Configure … Enter key