mirror of https://github.com/M66B/FairEmail.git
Proof of concept: OpenAI integration
This commit is contained in:
parent
9c1292e28f
commit
4bc0a09f41
28
FAQ.md
28
FAQ.md
|
@ -389,6 +389,7 @@ Anything on this list is in random order and *might* be added in the near future
|
|||
* [(187) Are colored stars synchronized across devices?](#user-content-faq187)
|
||||
* [(188) Why is Google backup disabled?](#user-content-faq188)
|
||||
* [(189) What is cloud sync?](#user-content-faq189)
|
||||
* [(190) How do I use OpenAI (ChatGPT)?](#user-content-faq190)
|
||||
|
||||
[I have another question.](#user-content-get-support)
|
||||
|
||||
|
@ -5221,6 +5222,33 @@ using a 256 bit key derived from the username and password with [PBKDF2](https:/
|
|||
|
||||
Cloud sync is an experimental feature. It is not available for the Play Store version of the app, yet.
|
||||
|
||||
<br>
|
||||
|
||||
<a name="faq190"></a>
|
||||
**(190) How do I use OpenAI (ChatGPT)?**
|
||||
|
||||
🌎 [Google Translate](https://translate.google.com/translate?sl=en&u=https://github.com/M66B/FairEmail/blob/master/FAQ.md%23user-content-faq180)
|
||||
|
||||
**Setup**
|
||||
|
||||
* Create an account [here](https://platform.openai.com/signup)
|
||||
* Create an APIkey [here](https://platform.openai.com/account/api-keys)
|
||||
* Copy the APIkey and paste it in the corresponding field of the miscellaneous settings tab page
|
||||
* Enable the OpenAI switch
|
||||
|
||||
**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).
|
||||
|
||||
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.
|
||||
|
||||
OpenAI isn't very fast, so be patient.
|
||||
|
||||
This feature is available in the GitHub version only and requires version 1.2052 or later.
|
||||
|
||||
<br>
|
||||
|
||||
<h2><a name="get-support"></a>Get support</h2>
|
||||
|
||||
🌎 [Google Translate](https://translate.google.com/translate?sl=en&u=https://github.com/M66B/FairEmail/blob/master/FAQ.md%23user-content-get-support)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -277,6 +277,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
private Group grpSignature;
|
||||
private Group grpReferenceHint;
|
||||
|
||||
private ImageButton ibOpenAi;
|
||||
private ContentResolver resolver;
|
||||
private AdapterAttachment adapter;
|
||||
|
||||
|
@ -1749,7 +1750,7 @@ public class FragmentCompose extends FragmentBase {
|
|||
ImageButton ibTranslate = (ImageButton) infl.inflate(R.layout.action_button, null);
|
||||
ibTranslate.setId(View.generateViewId());
|
||||
ibTranslate.setImageResource(R.drawable.twotone_translate_24);
|
||||
ib.setContentDescription(getString(R.string.title_translate));
|
||||
ibTranslate.setContentDescription(getString(R.string.title_translate));
|
||||
ibTranslate.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
|
@ -1758,6 +1759,18 @@ public class FragmentCompose extends FragmentBase {
|
|||
});
|
||||
menu.findItem(R.id.menu_translate).setActionView(ibTranslate);
|
||||
|
||||
ibOpenAi = (ImageButton) infl.inflate(R.layout.action_button, null);
|
||||
ibOpenAi.setId(View.generateViewId());
|
||||
ibOpenAi.setImageResource(R.drawable.twotone_question_answer_24);
|
||||
ibOpenAi.setContentDescription(getString(R.string.title_openai));
|
||||
ibOpenAi.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
onOpenAi(vwAnchorMenu);
|
||||
}
|
||||
});
|
||||
menu.findItem(R.id.menu_openai).setActionView(ibOpenAi);
|
||||
|
||||
ImageButton ibZoom = (ImageButton) infl.inflate(R.layout.action_button, null);
|
||||
ibZoom.setId(View.generateViewId());
|
||||
ibZoom.setImageResource(R.drawable.twotone_format_size_24);
|
||||
|
@ -1784,6 +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).setVisible(OpenAI.isAvailable(context));
|
||||
menu.findItem(R.id.menu_zoom).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_style).setEnabled(state == State.LOADED);
|
||||
menu.findItem(R.id.menu_media).setEnabled(state == State.LOADED);
|
||||
|
@ -2546,6 +2561,110 @@ public class FragmentCompose extends FragmentBase {
|
|||
popupMenu.showWithIcons(context, anchor);
|
||||
}
|
||||
|
||||
private void onOpenAi(View anchor) {
|
||||
int start = etBody.getSelectionStart();
|
||||
int end = etBody.getSelectionEnd();
|
||||
Editable edit = etBody.getText();
|
||||
String body = (start >= 0 && end > start ? edit.subSequence(start, end) : edit)
|
||||
.toString().trim();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", working);
|
||||
args.putString("body", body);
|
||||
|
||||
new SimpleTask<OpenAI.Message[]>() {
|
||||
@Override
|
||||
protected void onPreExecute(Bundle args) {
|
||||
if (ibOpenAi != null)
|
||||
ibOpenAi.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bundle args) {
|
||||
if (ibOpenAi != null)
|
||||
ibOpenAi.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OpenAI.Message[] onExecute(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
String body = args.getString("body");
|
||||
|
||||
DB db = DB.getInstance(context);
|
||||
EntityMessage draft = db.message().getMessage(id);
|
||||
if (draft == null)
|
||||
return null;
|
||||
|
||||
List<EntityMessage> 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<EntityMessage>() {
|
||||
@Override
|
||||
public int compare(EntityMessage m1, EntityMessage m2) {
|
||||
return Long.compare(m1.received, m2.received);
|
||||
}
|
||||
});
|
||||
|
||||
List<OpenAI.Message> messages = new ArrayList<>();
|
||||
//messages.add(new OpenAI.Message("system", "You are a helpful assistant."));
|
||||
|
||||
List<String> 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);
|
||||
|
||||
String text = HtmlHelper.getFullText(message.getFile(context));
|
||||
String[] paragraphs = text.split("[\\r\\n]+");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < 3 && i < paragraphs.length; i++)
|
||||
sb.append(paragraphs[i]).append("\n");
|
||||
String role = (MessageHelper.equalEmail(draft.from, message.from) ? "assistant" : "user");
|
||||
messages.add(new OpenAI.Message(role, sb.toString()));
|
||||
|
||||
if (msgids.size() >= 3)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(body))
|
||||
messages.add(new OpenAI.Message("assistant", body));
|
||||
|
||||
if (messages.size() == 0)
|
||||
return null;
|
||||
|
||||
return OpenAI.complete(context, messages.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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Log.unexpectedError(getParentFragmentManager(), ex);
|
||||
}
|
||||
}.execute(this, args, "openai");
|
||||
}
|
||||
|
||||
private void onLanguageTool(int start, int end, boolean silent) {
|
||||
etBody.clearComposingText();
|
||||
|
||||
|
|
|
@ -141,6 +141,10 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
private SwitchCompat swSend;
|
||||
private EditText etSend;
|
||||
private ImageButton ibSend;
|
||||
private SwitchCompat swOpenAi;
|
||||
private TextView tvOpenAiPrivacy;
|
||||
private TextInputLayout tilOpenAi;
|
||||
private ImageButton ibOpenAi;
|
||||
private SwitchCompat swUpdates;
|
||||
private TextView tvGithubPrivacy;
|
||||
private ImageButton ibChannelUpdated;
|
||||
|
@ -244,6 +248,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
|
||||
private Group grpVirusTotal;
|
||||
private Group grpSend;
|
||||
private Group grpOpenAi;
|
||||
private Group grpUpdates;
|
||||
private Group grpBitbucket;
|
||||
private Group grpAnnouncements;
|
||||
|
@ -263,6 +268,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
"deepl_enabled",
|
||||
"vt_enabled", "vt_apikey",
|
||||
"send_enabled", "send_host",
|
||||
"openai_enabled", "openai_apikey",
|
||||
"updates", "weekly", "beta", "show_changelog", "announcements",
|
||||
"crash_reports", "cleanup_attachments",
|
||||
"watchdog", "experiments", "main_log", "main_log_memory", "protocol", "log_level", "debug", "leak_canary",
|
||||
|
@ -365,6 +371,10 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
swSend = view.findViewById(R.id.swSend);
|
||||
etSend = view.findViewById(R.id.etSend);
|
||||
ibSend = view.findViewById(R.id.ibSend);
|
||||
swOpenAi = view.findViewById(R.id.swOpenAi);
|
||||
tvOpenAiPrivacy = view.findViewById(R.id.tvOpenAiPrivacy);
|
||||
tilOpenAi = view.findViewById(R.id.tilOpenAi);
|
||||
ibOpenAi = view.findViewById(R.id.ibOpenAi);
|
||||
swUpdates = view.findViewById(R.id.swUpdates);
|
||||
tvGithubPrivacy = view.findViewById(R.id.tvGithubPrivacy);
|
||||
ibChannelUpdated = view.findViewById(R.id.ibChannelUpdated);
|
||||
|
@ -468,6 +478,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
|
||||
grpVirusTotal = view.findViewById(R.id.grpVirusTotal);
|
||||
grpSend = view.findViewById(R.id.grpSend);
|
||||
grpOpenAi = view.findViewById(R.id.grpOpenAi);
|
||||
grpUpdates = view.findViewById(R.id.grpUpdates);
|
||||
grpBitbucket = view.findViewById(R.id.grpBitbucket);
|
||||
grpAnnouncements = view.findViewById(R.id.grpAnnouncements);
|
||||
|
@ -870,6 +881,49 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
}
|
||||
});
|
||||
|
||||
swOpenAi.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
|
||||
prefs.edit().putBoolean("openai_enabled", checked).apply();
|
||||
}
|
||||
});
|
||||
|
||||
tvOpenAiPrivacy.getPaint().setUnderlineText(true);
|
||||
tvOpenAiPrivacy.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Helper.view(v.getContext(), Uri.parse(OpenAI.URI_PRIVACY), true);
|
||||
}
|
||||
});
|
||||
|
||||
tilOpenAi.getEditText().addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
String apikey = s.toString().trim();
|
||||
if (TextUtils.isEmpty(apikey))
|
||||
prefs.edit().remove("openai_apikey").apply();
|
||||
else
|
||||
prefs.edit().putString("openai_apikey", apikey).apply();
|
||||
}
|
||||
});
|
||||
|
||||
ibOpenAi.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Helper.viewFAQ(v.getContext(), 190);
|
||||
}
|
||||
});
|
||||
|
||||
swUpdates.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
|
||||
|
@ -1959,6 +2013,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
|
||||
grpVirusTotal.setVisibility(BuildConfig.PLAY_STORE_RELEASE ? View.GONE : View.VISIBLE);
|
||||
grpSend.setVisibility(BuildConfig.PLAY_STORE_RELEASE ? View.GONE : View.VISIBLE);
|
||||
grpOpenAi.setVisibility(BuildConfig.PLAY_STORE_RELEASE ? View.GONE : View.VISIBLE);
|
||||
|
||||
grpUpdates.setVisibility(!BuildConfig.DEBUG &&
|
||||
(Helper.isPlayStoreInstall() || !Helper.hasValidFingerprint(getContext()))
|
||||
|
@ -2056,7 +2111,8 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
"lt_user".equals(key) ||
|
||||
"lt_key".equals(key) ||
|
||||
"vt_apikey".equals(key) ||
|
||||
"send_host".equals(key))
|
||||
"send_host".equals(key) ||
|
||||
"openai_apikey".equals(key))
|
||||
return;
|
||||
|
||||
if ("global_keywords".equals(key))
|
||||
|
@ -2221,6 +2277,8 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
|
|||
tilVirusTotal.getEditText().setText(prefs.getString("vt_apikey", null));
|
||||
swSend.setChecked(prefs.getBoolean("send_enabled", false));
|
||||
etSend.setText(prefs.getString("send_host", null));
|
||||
swOpenAi.setChecked(prefs.getBoolean("openai_enabled", false));
|
||||
tilOpenAi.getEditText().setText(prefs.getString("openai_apikey", null));
|
||||
swUpdates.setChecked(prefs.getBoolean("updates", true));
|
||||
swCheckWeekly.setChecked(prefs.getBoolean("weekly", Helper.hasPlayStore(getContext())));
|
||||
swCheckWeekly.setEnabled(swUpdates.isChecked());
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
package eu.faircode.email;
|
||||
|
||||
/*
|
||||
This file is part of FairEmail.
|
||||
|
||||
FairEmail is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
FairEmail is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018-2023 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
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
|
||||
|
||||
static boolean isAvailable(Context context) {
|
||||
if (BuildConfig.PLAY_STORE_RELEASE)
|
||||
return false;
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
boolean enabled = prefs.getBoolean("openai_enabled", false);
|
||||
String apikey = prefs.getString("openai_apikey", null);
|
||||
|
||||
return (enabled && !TextUtils.isEmpty(apikey));
|
||||
}
|
||||
|
||||
static Message[] complete(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
|
||||
|
||||
JSONArray jmessages = new JSONArray();
|
||||
for (Message message : messages) {
|
||||
JSONObject jmessage = new JSONObject();
|
||||
jmessage.put("role", message.role);
|
||||
jmessage.put("content", message.content);
|
||||
jmessages.put(jmessage);
|
||||
}
|
||||
|
||||
JSONObject jquestion = new JSONObject();
|
||||
jquestion.put("model", "gpt-3.5-turbo");
|
||||
jquestion.put("messages", jmessages);
|
||||
jquestion.put("n", n);
|
||||
JSONObject jresponse = call(context, "v1/chat/completions", jquestion);
|
||||
|
||||
JSONArray jchoices = jresponse.getJSONArray("choices");
|
||||
Message[] choices = new Message[jchoices.length()];
|
||||
for (int i = 0; i < jchoices.length(); i++) {
|
||||
JSONObject jchoice = jchoices.getJSONObject(i);
|
||||
JSONObject jmessage = jchoice.getJSONObject("message");
|
||||
choices[i] = new Message(jmessage.getString("role"), jmessage.getString("content"));
|
||||
}
|
||||
|
||||
return choices;
|
||||
}
|
||||
|
||||
private static JSONObject call(Context context, String method, JSONObject args) throws JSONException, IOException {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String apikey = prefs.getString("openai_apikey", null);
|
||||
|
||||
// https://platform.openai.com/docs/api-reference/introduction
|
||||
Uri uri = Uri.parse(URI_ENDPOINT).buildUpon().appendEncodedPath(method).build();
|
||||
Log.i("OpenAI uri=" + uri);
|
||||
|
||||
String json = args.toString();
|
||||
Log.i("OpenAI request=" + json);
|
||||
|
||||
URL url = new URL(uri.toString());
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setDoOutput(true);
|
||||
connection.setDoInput(true);
|
||||
connection.setReadTimeout(TIMEOUT * 1000);
|
||||
connection.setConnectTimeout(TIMEOUT * 1000);
|
||||
ConnectionHelper.setUserAgent(context, connection);
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("Authorization", "Bearer " + apikey);
|
||||
connection.connect();
|
||||
|
||||
try {
|
||||
connection.getOutputStream().write(json.getBytes());
|
||||
|
||||
int status = connection.getResponseCode();
|
||||
if (status != HttpURLConnection.HTTP_OK) {
|
||||
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
||||
String error = "Error " + status + ": " + connection.getResponseMessage();
|
||||
try {
|
||||
InputStream is = connection.getErrorStream();
|
||||
if (is != null)
|
||||
error += "\n" + Helper.readStream(is);
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
}
|
||||
throw new IOException(error);
|
||||
}
|
||||
|
||||
String response = Helper.readStream(connection.getInputStream());
|
||||
Log.i("OpenAI response=" + response);
|
||||
|
||||
return new JSONObject(response);
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
static class Message {
|
||||
private final String role; // // system, user, assistant
|
||||
private final String content;
|
||||
|
||||
public Message(String role, String content) {
|
||||
this.role = role;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return this.role;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.role + ": " + this.content;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -565,6 +565,62 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/etSend"
|
||||
app:srcCompat="@drawable/twotone_info_24" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/swOpenAi"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_openai"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ibSend"
|
||||
app:switchPadding="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvOpenAiPrivacy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:drawableEnd="@drawable/twotone_open_in_new_12"
|
||||
android:drawablePadding="6dp"
|
||||
android:text="@string/title_privacy_policy"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="?android:attr/textColorLink"
|
||||
app:drawableTint="?android:attr/textColorLink"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/swOpenAi" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilOpenAi"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
app:endIconMode="password_toggle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvOpenAiPrivacy">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:hint="API key"
|
||||
android:inputType="textPassword"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/ibOpenAi"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:contentDescription="@string/title_info"
|
||||
android:tooltipText="@string/title_info"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tilOpenAi"
|
||||
app:srcCompat="@drawable/twotone_info_24" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/swUpdates"
|
||||
android:layout_width="0dp"
|
||||
|
@ -576,7 +632,7 @@
|
|||
android:text="@string/title_advanced_updates"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/ibSend"
|
||||
app:layout_constraintTop_toBottomOf="@id/ibOpenAi"
|
||||
app:switchPadding="12dp" />
|
||||
|
||||
<TextView
|
||||
|
@ -809,6 +865,12 @@
|
|||
android:layout_height="0dp"
|
||||
app:constraint_referenced_ids="swSend,etSend,ibSend" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/grpOpenAi"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:constraint_referenced_ids="swOpenAi,tvOpenAiPrivacy,tilOpenAi,ibOpenAi" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/grpUpdates"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
android:title="@string/title_translate"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_openai"
|
||||
android:icon="@drawable/twotone_question_answer_24"
|
||||
android:title="@string/title_openai"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_zoom"
|
||||
android:icon="@drawable/twotone_format_size_24"
|
||||
|
|
|
@ -793,6 +793,7 @@
|
|||
<string name="title_advanced_deepl">DeepL integration</string>
|
||||
<string name="title_advanced_virus_total">VirusTotal integration</string>
|
||||
<string name="title_advanced_send">\'Send\' integration</string>
|
||||
<string name="title_advanced_openai">OpenAI (ChatGPT) integration</string>
|
||||
<string name="title_advanced_sdcard">I want to use an sdcard</string>
|
||||
<string name="title_advanced_watchdog">Periodically check if FairEmail is still active</string>
|
||||
<string name="title_advanced_updates">Check for GitHub updates</string>
|
||||
|
@ -1599,6 +1600,7 @@
|
|||
<string name="title_create_template">Create template</string>
|
||||
<string name="title_select_default_identity">Select default address</string>
|
||||
<string name="title_translate">Translate</string>
|
||||
<string name="title_openai" translatable="false">OpenAI (ChatGPT)</string>
|
||||
<string name="title_translate_configure">Configure …</string>
|
||||
<string name="title_translate_key">Enter key</string>
|
||||
<string name="title_translating">Translating …</string>
|
||||
|
|
Loading…
Reference in New Issue