diff --git a/src/data/defaults/defaultLocalSettings.js b/src/data/defaults/defaultLocalSettings.js
index b5db3a7a..052a9c90 100644
--- a/src/data/defaults/defaultLocalSettings.js
+++ b/src/data/defaults/defaultLocalSettings.js
@@ -40,4 +40,6 @@ export default () => ({
zendescPublishSectionId: '',
zendescPublishLocale: '',
zendeskPublishTemplate: 'plainHtml',
+ chatgptApiKey: '',
+ chatgptProxyHost: '',
});
diff --git a/src/data/defaults/defaultSettings.yml b/src/data/defaults/defaultSettings.yml
index 7502e286..17c2ddb2 100644
--- a/src/data/defaults/defaultSettings.yml
+++ b/src/data/defaults/defaultSettings.yml
@@ -43,6 +43,8 @@ editor:
link: true
# 图片
image: true
+ # ChatGPT
+ chatgpt: true
# Keyboard shortcuts
# See https://craig.is/killing/mice
@@ -57,6 +59,7 @@ shortcuts:
mod+shift+h: heading
mod+shift+r: hr
mod+shift+g: image
+ mod+shift+p: chatgpt
mod+shift+i: italic
mod+shift+l: link
mod+shift+o: olist
diff --git a/src/data/pagedownButtons.js b/src/data/pagedownButtons.js
index 6dad969d..d952ee27 100644
--- a/src/data/pagedownButtons.js
+++ b/src/data/pagedownButtons.js
@@ -49,4 +49,8 @@ export default [{
method: 'image',
title: '图片',
icon: 'file-image',
+}, {
+ method: 'chatgpt',
+ title: 'ChatGPT',
+ icon: 'chat-gpt',
}];
diff --git a/src/icons/ChatGpt.vue b/src/icons/ChatGpt.vue
new file mode 100644
index 00000000..18bf3dfc
--- /dev/null
+++ b/src/icons/ChatGpt.vue
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/icons/index.js b/src/icons/index.js
index fbbc8ab8..3ddd1eef 100644
--- a/src/icons/index.js
+++ b/src/icons/index.js
@@ -65,6 +65,7 @@ import SelectTheme from './SelectTheme';
import Copy from './Copy';
import Ellipsis from './Ellipsis';
import Share from './Share';
+import ChatGpt from './ChatGpt';
Vue.component('iconProvider', Provider);
Vue.component('iconFormatBold', FormatBold);
@@ -132,3 +133,4 @@ Vue.component('iconSelectTheme', SelectTheme);
Vue.component('iconCopy', Copy);
Vue.component('iconEllipsis', Ellipsis);
Vue.component('iconShare', Share);
+Vue.component('iconChatGpt', ChatGpt);
diff --git a/src/libs/pagedown.js b/src/libs/pagedown.js
index c46345ad..f4144d9a 100644
--- a/src/libs/pagedown.js
+++ b/src/libs/pagedown.js
@@ -122,6 +122,7 @@ function Pagedown(options) {
hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed
hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text
hooks.addFalse("insertImageDialog");
+ hooks.addFalse("insertChatGptDialog");
/* called with one parameter: a callback to be called with the URL of the image. If the application creates
* its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
* image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
@@ -477,6 +478,7 @@ function UIManager(input, commandManager) {
buttons.image = bindCommand(function (chunk, postProcessing) {
return this.doLinkOrImage(chunk, postProcessing, true);
});
+ buttons.chatgpt = bindCommand("doChatGpt");
buttons.olist = bindCommand(function (chunk, postProcessing) {
this.doList(chunk, postProcessing, true);
});
@@ -846,6 +848,17 @@ commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
}
};
+commandProto.doChatGpt = function (chunk, postProcessing) {
+ var enteredCallback = function (content) {
+ if (content !== null) {
+ chunk.before = `${chunk.before}${content}`;
+ chunk.selection = '';
+ }
+ postProcessing();
+ };
+ this.hooks.insertChatGptDialog(enteredCallback);
+};
+
// When making a list, hitting shift-enter will put your cursor on the next line
// at the current indent level.
commandProto.doAutoindent = function (chunk) {
diff --git a/src/services/chatGptSvc.js b/src/services/chatGptSvc.js
new file mode 100644
index 00000000..a06772ed
--- /dev/null
+++ b/src/services/chatGptSvc.js
@@ -0,0 +1,38 @@
+import store from '../store';
+
+export default {
+ chat(proxyHost, apiKey, content, callback) {
+ const xhr = new XMLHttpRequest();
+ const url = `${proxyHost || 'https://api.openai.com'}/v1/chat/completions`;
+ xhr.open('POST', url);
+ xhr.setRequestHeader('Authorization', `Bearer ${apiKey}`);
+ xhr.setRequestHeader('Content-Type', 'application/json');
+ xhr.send(JSON.stringify({
+ model: 'gpt-3.5-turbo',
+ messages: [{ role: 'user', content }],
+ temperature: 1,
+ stream: true,
+ }));
+ let lastRespLen = 0;
+ xhr.onprogress = () => {
+ const responseText = xhr.response.substr(lastRespLen);
+ lastRespLen = xhr.response.length;
+ responseText.split('\n\n')
+ .filter(l => l.length > 0)
+ .forEach((text) => {
+ const item = text.substr(6);
+ if (item === '[DONE]') {
+ callback({ done: true });
+ } else {
+ const data = JSON.parse(item);
+ callback({ content: data.choices[0].delta.content });
+ }
+ });
+ };
+ xhr.onerror = () => {
+ store.dispatch('notification/error', 'ChatGPT接口请求异常!');
+ callback({ error: 'ChatGPT接口请求异常!' });
+ };
+ return xhr;
+ },
+};
diff --git a/src/services/editorSvc.js b/src/services/editorSvc.js
index 6cec70f4..e76dbbe6 100644
--- a/src/services/editorSvc.js
+++ b/src/services/editorSvc.js
@@ -439,6 +439,13 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
});
return true;
});
+ this.pagedownEditor.hooks.set('insertChatGptDialog', (callback) => {
+ store.dispatch('modal/open', {
+ type: 'chatGpt',
+ callback,
+ });
+ return true;
+ });
this.pagedownEditor.hooks.set('insertImageUploading', (callback) => {
callback(store.getters['img/currImgId']);
return true;
diff --git a/src/services/optional/shortcuts.js b/src/services/optional/shortcuts.js
index b81f7b97..1eca3e4b 100644
--- a/src/services/optional/shortcuts.js
+++ b/src/services/optional/shortcuts.js
@@ -28,6 +28,7 @@ const methods = {
quote: pagedownHandler('quote'),
code: pagedownHandler('code'),
image: pagedownHandler('image'),
+ chatgpt: pagedownHandler('chatgpt'),
olist: pagedownHandler('olist'),
ulist: pagedownHandler('ulist'),
clist: pagedownHandler('clist'),
diff --git a/src/store/chatgpt.js b/src/store/chatgpt.js
new file mode 100644
index 00000000..1c2a0027
--- /dev/null
+++ b/src/store/chatgpt.js
@@ -0,0 +1,25 @@
+const chatgptConfigKey = 'chatgpt/config';
+
+export default {
+ namespaced: true,
+ state: {
+ config: {
+ apiKey: null,
+ proxyHost: null,
+ },
+ },
+ mutations: {
+ setCurrConfig: (state, value) => {
+ state.config = value;
+ },
+ },
+ getters: {
+ chatGptConfig: state => state.config,
+ },
+ actions: {
+ setCurrConfig({ commit }, value) {
+ commit('setCurrConfig', value);
+ localStorage.setItem(chatgptConfigKey, JSON.stringify(value));
+ },
+ },
+};
diff --git a/src/store/index.js b/src/store/index.js
index f81e6ac8..dcfb8d68 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -20,6 +20,7 @@ import userInfo from './userInfo';
import workspace from './workspace';
import img from './img';
import theme from './theme';
+import chatgpt from './chatgpt';
import locationTemplate from './locationTemplate';
import emptyPublishLocation from '../data/empties/emptyPublishLocation';
import emptySyncLocation from '../data/empties/emptySyncLocation';
@@ -51,6 +52,7 @@ const store = new Vuex.Store({
workspace,
img,
theme,
+ chatgpt,
},
state: {
light: false,