diff --git a/README.md b/README.md index 70aa5861..d729300f 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ StackEdit中文版 - Gitlab的支持优化(2023-02-23) - 导出HTML、PDF支持带预览主题导出(2023-02-26) - 支持分享文档(2023-03-30) +- 支持ChatGPT生成内容(2023-04-10) ## 国外开源版本弊端: - 作者已经不维护了 diff --git a/package-lock.json b/package-lock.json index bde12dd1..d4101bda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "stackedit", - "version": "5.15.19", + "version": "5.15.20", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8819908e..cfe4c778 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stackedit", - "version": "5.15.19", + "version": "5.15.20", "description": "免费, 开源, 功能齐全的 Markdown 编辑器", "author": "Benoit Schweblin, 豆萁", "license": "Apache-2.0", diff --git a/src/components/Modal.vue b/src/components/Modal.vue index e6f3f441..9ab1dcac 100644 --- a/src/components/Modal.vue +++ b/src/components/Modal.vue @@ -41,6 +41,8 @@ import BadgeManagementModal from './modals/BadgeManagementModal'; import SponsorModal from './modals/SponsorModal'; import CommitMessageModal from './modals/CommitMessageModal'; import WorkspaceImgPathModal from './modals/WorkspaceImgPathModal'; +import ChatGptModal from './modals/ChatGptModal'; +import ChatGptConfigModal from './modals/ChatGptConfigModal'; // Providers import GooglePhotoModal from './modals/providers/GooglePhotoModal'; @@ -111,6 +113,8 @@ export default { SponsorModal, CommitMessageModal, WorkspaceImgPathModal, + ChatGptModal, + ChatGptConfigModal, // Providers GooglePhotoModal, GoogleDriveAccountModal, diff --git a/src/components/modals/ChatGptConfigModal.vue b/src/components/modals/ChatGptConfigModal.vue new file mode 100644 index 00000000..cd84e032 --- /dev/null +++ b/src/components/modals/ChatGptConfigModal.vue @@ -0,0 +1,52 @@ + + + diff --git a/src/components/modals/ChatGptModal.vue b/src/components/modals/ChatGptModal.vue new file mode 100644 index 00000000..27b82ade --- /dev/null +++ b/src/components/modals/ChatGptModal.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/components/modals/providers/GiteeSaveModal.vue b/src/components/modals/providers/GiteeSaveModal.vue index f0f5dd82..b093b10c 100644 --- a/src/components/modals/providers/GiteeSaveModal.vue +++ b/src/components/modals/providers/GiteeSaveModal.vue @@ -4,7 +4,7 @@ -

Save {{currentFileName}} to your Gitee repository and keep it synced.

+

保存 {{currentFileName}} 并与您的 Gitee 仓库保持同步.

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,