diff --git a/FAQ.md b/FAQ.md
index 753d9ea478..dbb6d02018 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -5238,14 +5238,16 @@ Cloud sync is an experimental feature. It is not available for the Play Store ve
**Usage**
-Tap on the conversation button in the top action bar of the message editor.
-The selected text in the message editor and the first three paragraphs of the first three messages in the conversation will be used for [chat completion](https://platform.openai.com/docs/guides/chat/introduction).
+Tap on the robot button in the top action bar of the message editor.
+The text in the message editor and the first part of the message being replied to (maximum of 1,000 characters)
+will be used for [chat completion](https://platform.openai.com/docs/guides/chat/introduction).
+If text is selected in the message editor, only the selected text will be used.
-For example: create a new draft and enter the text "*How far is the sun?*", and tap on the conversation button in the top action bar.
+For example: create a new draft and enter the text "*How far is the sun?*", and tap on the robot button in the top action bar.
OpenAI isn't very fast, so be patient.
-This feature is available in the GitHub version only and requires version 1.2052 or later.
+This feature is experimental and available in the GitHub version only and requires version 1.2052 or later.
diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java
index 874640c825..b26b42641c 100644
--- a/app/src/main/java/eu/faircode/email/FragmentCompose.java
+++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java
@@ -277,7 +277,6 @@ public class FragmentCompose extends FragmentBase {
private Group grpSignature;
private Group grpReferenceHint;
- private ImageButton ibOpenAi;
private ContentResolver resolver;
private AdapterAttachment adapter;
@@ -300,6 +299,7 @@ public class FragmentCompose extends FragmentBase {
private List last_attachments = null;
private boolean saved = false;
private String subject = null;
+ private boolean chatting = false;
private Uri photoURI = null;
@@ -1759,9 +1759,9 @@ public class FragmentCompose extends FragmentBase {
});
menu.findItem(R.id.menu_translate).setActionView(ibTranslate);
- ibOpenAi = (ImageButton) infl.inflate(R.layout.action_button, null);
+ ImageButton ibOpenAi = (ImageButton) infl.inflate(R.layout.action_button, null);
ibOpenAi.setId(View.generateViewId());
- ibOpenAi.setImageResource(R.drawable.twotone_question_answer_24);
+ ibOpenAi.setImageResource(R.drawable.twotone_smart_toy_24);
ibOpenAi.setContentDescription(getString(R.string.title_openai));
ibOpenAi.setOnClickListener(new View.OnClickListener() {
@Override
@@ -1797,7 +1797,8 @@ public class FragmentCompose extends FragmentBase {
menu.findItem(R.id.menu_encrypt).setEnabled(state == State.LOADED);
menu.findItem(R.id.menu_translate).setEnabled(state == State.LOADED);
menu.findItem(R.id.menu_translate).setVisible(DeepL.isAvailable(context));
- menu.findItem(R.id.menu_openai).setEnabled(state == State.LOADED);
+ menu.findItem(R.id.menu_openai).setEnabled(state == State.LOADED && !chatting);
+ ((ImageButton) menu.findItem(R.id.menu_openai).getActionView()).setEnabled(!chatting);
menu.findItem(R.id.menu_openai).setVisible(OpenAI.isAvailable(context));
menu.findItem(R.id.menu_zoom).setEnabled(state == State.LOADED);
menu.findItem(R.id.menu_style).setEnabled(state == State.LOADED);
@@ -2564,97 +2565,101 @@ public class FragmentCompose extends FragmentBase {
private void onOpenAi(View anchor) {
int start = etBody.getSelectionStart();
int end = etBody.getSelectionEnd();
+ boolean selection = (start >= 0 && end > start);
Editable edit = etBody.getText();
- String body = (start >= 0 && end > start ? edit.subSequence(start, end) : edit)
- .toString().trim();
+ String body = (selection ? edit.subSequence(start, end) : edit).toString().trim();
Bundle args = new Bundle();
args.putLong("id", working);
args.putString("body", body);
+ args.putBoolean("selection", selection);
new SimpleTask() {
@Override
protected void onPreExecute(Bundle args) {
- if (ibOpenAi != null)
- ibOpenAi.setEnabled(false);
+ chatting = true;
+ invalidateOptionsMenu();
}
@Override
protected void onPostExecute(Bundle args) {
- if (ibOpenAi != null)
- ibOpenAi.setEnabled(true);
+ chatting = false;
+ invalidateOptionsMenu();
}
@Override
protected OpenAI.Message[] onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
String body = args.getString("body");
+ boolean selection = args.getBoolean("selection");
DB db = DB.getInstance(context);
EntityMessage draft = db.message().getMessage(id);
if (draft == null)
return null;
- List conversation = db.message().getMessagesByThread(draft.account, draft.thread, null, null);
- if (conversation == null)
- return null;
-
- if (TextUtils.isEmpty(body) && conversation.size() == 0)
- return null;
-
- EntityFolder sent = db.folder().getFolderByType(draft.account, EntityFolder.SENT);
- if (sent == null)
- return null;
-
- Collections.sort(conversation, new Comparator() {
- @Override
- public int compare(EntityMessage m1, EntityMessage m2) {
- return Long.compare(m1.received, m2.received);
- }
- });
+ List inreplyto;
+ if (selection || TextUtils.isEmpty(draft.inreplyto))
+ inreplyto = new ArrayList<>();
+ else
+ inreplyto = db.message().getMessagesByMsgId(draft.account, draft.inreplyto);
- List messages = new ArrayList<>();
+ List result = new ArrayList<>();
//messages.add(new OpenAI.Message("system", "You are a helpful assistant."));
- List msgids = new ArrayList<>();
- for (EntityMessage message : conversation) {
- if (Objects.equals(draft.msgid, message.msgid))
- continue;
- if (msgids.contains(message.msgid))
- continue;
- msgids.add(message.msgid);
+ if (inreplyto.size() > 0 && inreplyto.get(0).content) {
+ Document parsed = JsoupEx.parse(inreplyto.get(0).getFile(context));
+ Document document = HtmlHelper.sanitizeView(context, parsed, false);
+ Spanned spanned = HtmlHelper.fromDocument(context, document, null, null);
+ String[] paragraphs = spanned.toString().split("[\\r\\n]+");
- String text = HtmlHelper.getFullText(message.getFile(context));
- String[] paragraphs = text.split("[\\r\\n]+");
+ int i = 0;
StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 3 && i < paragraphs.length; i++)
- sb.append(paragraphs[i]).append("\n");
- messages.add(new OpenAI.Message("assistant", sb.toString()));
+ while (i < paragraphs.length &&
+ sb.length() + paragraphs[i].length() + 1 < 1000)
+ sb.append(paragraphs[i++]).append('\n');
- if (msgids.size() >= 3)
- break;
+ String role = (MessageHelper.equalEmail(draft.from, inreplyto.get(0).from) ? "assistant" : "user");
+ result.add(new OpenAI.Message(role, sb.toString()));
}
if (!TextUtils.isEmpty(body))
- messages.add(new OpenAI.Message("user", body));
+ result.add(new OpenAI.Message("assistant", body));
- if (messages.size() == 0)
+ if (result.size() == 0)
return null;
- return OpenAI.complete(context, messages.toArray(new OpenAI.Message[0]), 1);
+ return OpenAI.completeChat(context, result.toArray(new OpenAI.Message[0]), 1);
}
@Override
protected void onExecuted(Bundle args, OpenAI.Message[] messages) {
- if (messages != null && messages.length > 0) {
- int start = etBody.getSelectionEnd();
- String content = messages[0].getContent();
- Editable edit = etBody.getText();
- edit.insert(start, content);
- int end = start + content.length();
- etBody.setSelection(end);
- StyleHelper.markAsInserted(edit, start, end);
- }
+ if (messages == null || messages.length == 0)
+ return;
+
+ String text = messages[0].getContent()
+ .replaceAll("^\\n+", "").replaceAll("\\n+$", "");
+
+ Editable edit = etBody.getText();
+ int start = etBody.getSelectionStart();
+ int end = etBody.getSelectionEnd();
+
+ int index;
+ if (etBody.hasSelection()) {
+ edit.delete(start, end);
+ index = start;
+ } else
+ index = end;
+
+ if (index < 0)
+ index = 0;
+ if (index > 0 && edit.charAt(index - 1) != '\n')
+ edit.insert(index++, "\n");
+
+ edit.insert(index, text + "\n");
+ etBody.setSelection(index + text.length() + 1);
+
+ StyleHelper.markAsInserted(edit, index, index + text.length() + 1);
}
@Override
diff --git a/app/src/main/java/eu/faircode/email/OpenAI.java b/app/src/main/java/eu/faircode/email/OpenAI.java
index 6ae8f68310..5aedcaa58e 100644
--- a/app/src/main/java/eu/faircode/email/OpenAI.java
+++ b/app/src/main/java/eu/faircode/email/OpenAI.java
@@ -40,7 +40,7 @@ public class OpenAI {
static final String URI_ENDPOINT = "https://api.openai.com/";
static final String URI_PRIVACY = "https://openai.com/policies/privacy-policy";
- private static final int TIMEOUT = 20; // seconds
+ private static final int TIMEOUT = 30; // seconds
static boolean isAvailable(Context context) {
if (BuildConfig.PLAY_STORE_RELEASE)
@@ -53,7 +53,7 @@ public class OpenAI {
return (enabled && !TextUtils.isEmpty(apikey));
}
- static Message[] complete(Context context, Message[] messages, int n) throws JSONException, IOException {
+ static Message[] completeChat(Context context, Message[] messages, int n) throws JSONException, IOException {
// https://platform.openai.com/docs/guides/chat/introduction
// https://platform.openai.com/docs/api-reference/chat/create
diff --git a/app/src/main/res/drawable/twotone_smart_toy_24.xml b/app/src/main/res/drawable/twotone_smart_toy_24.xml
new file mode 100644
index 0000000000..8f81a0f830
--- /dev/null
+++ b/app/src/main/res/drawable/twotone_smart_toy_24.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_compose.xml b/app/src/main/res/menu/menu_compose.xml
index d908c94183..dca879e7c2 100644
--- a/app/src/main/res/menu/menu_compose.xml
+++ b/app/src/main/res/menu/menu_compose.xml
@@ -15,7 +15,7 @@