UNPKG

@opentiny/fluent-editor

Version:

A rich text editor based on Quill 2.0, which extends rich modules and formats on the basis of Quill. It's powerful and out-of-the-box.

671 lines (670 loc) 25.2 kB
"use strict"; var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const constants = require("./constants.cjs.js"); const icons = require("./icons.cjs.js"); class AI { constructor(quill, options) { __publicField(this, "toolbar"); __publicField(this, "host"); __publicField(this, "apiKey"); __publicField(this, "model"); __publicField(this, "message"); __publicField(this, "isBreak", false); // 打断标记 __publicField(this, "textNumber"); // 文本字数限制 __publicField(this, "_isSelectRangeMode", false); // 选择/点击模式 __publicField(this, "_charCount", 0); // 文本字数 __publicField(this, "_debounceTimer", null); __publicField(this, "_inputPlaceholder", ""); __publicField(this, "_showOperationMenu", false); __publicField(this, "_isThinking", false); // 思考中 __publicField(this, "_showResultPopupEl", false); // 结果弹窗 __publicField(this, "selectedText", ""); // 选择的文本 __publicField(this, "inputValue", ""); // 存储输入框的值 __publicField(this, "resultMenuList", []); __publicField(this, "operationMenuList", []); __publicField(this, "_operationMenuItemList", []); __publicField(this, "alertEl", null); __publicField(this, "alertTimer", null); __publicField(this, "selectionBubbleEl", null); __publicField(this, "selectionRange", null); __publicField(this, "dialogContainerEl", null); __publicField(this, "wrapContainerEl", null); __publicField(this, "aiIconEl", null); __publicField(this, "inputContainerEl", null); __publicField(this, "inputEl", null); __publicField(this, "menuContainerEl", null); __publicField(this, "subMenuEl", null); __publicField(this, "subMenuEditorEl", null); __publicField(this, "subMenuToneEl", null); __publicField(this, "subMenuAdjustEl", null); __publicField(this, "inputRightEl", null); __publicField(this, "inputSendBtnEl", null); __publicField(this, "inputCloseBtnEl", null); __publicField(this, "thinkContainerEl", null); // 思考元素 __publicField(this, "thinkBtnEl", null); __publicField(this, "resultPopupEl", null); __publicField(this, "resultPopupHeaderEl", null); __publicField(this, "resultPopupContentEl", null); __publicField(this, "resultPopupFooterEl", null); __publicField(this, "resultPopupFooterTextEl", null); __publicField(this, "resultRefreshBtnEl", null); __publicField(this, "resultCopyBtnEl", null); // 分享和朗读功能待放开 // private resultShareBtnEl: HTMLSpanElement | null = null // private resultVoiceBtnEl: HTMLSpanElement | null = null __publicField(this, "actionMenuEl", null); this.quill = quill; this.options = options; this.quill = quill; this.toolbar = quill.getModule("toolbar"); if (typeof this.toolbar !== "undefined") { this.toolbar.addHandler("ai", this.showAIInput.bind(this)); } this.quill.on("selection-change", this.handleSelectionChange.bind(this)); this.host = options.host || "https://api.deepseek.com/v1"; this.apiKey = options.apiKey; this.model = options.model || "deepseek-chat"; this.textNumber = options.contentMaxLength || 5e3; this.resultMenuList = [ { text: constants.REPLACE_SELECT, icon: icons.REPLACE_SELECT_ICON }, { text: constants.INSERT_TEXT, icon: icons.INSERT_ICON, selectText: constants.INSERT_SUB_CONTENT_TEXT }, { text: constants.REGENERATE, icon: icons.REBUILD_ICON }, { text: constants.CLOSE, icon: icons.MENU_CLOSE_ICON } ]; this.operationMenuList = [ { id: "editor", text: "编辑调整内容", icon: icons.EDITOR_ICON }, { id: "tone", text: "改写口吻", icon: icons.CALL_ICON }, { id: "adjust", text: "整理选区内容", icon: icons.ADJUST_ICON } ]; } // 工具栏启动 showAIInput() { this.create(); this.selectionRange = this.quill.getSelection(); if (this.selectionRange.length) { this.isSelectRangeMode = true; } else { this.isSelectRangeMode = false; } this.positionElements(); const handleKeyDown = (e) => { if (e.key === "Escape") { this.closeAIPanel(); this.quill.container.removeEventListener("keydown", handleKeyDown); } }; this.quill.container.addEventListener("keydown", handleKeyDown); } // 气泡启动 selectTextEvent() { if (!this.selectionRange) return; this.create(); this.positionElements(); this.isSelectRangeMode = true; } create() { this.createResultElement(); this.createOperationMenuElements(); this.createInputBoxElements(); this.addInputEvent(); this.addResultEvent(); this.handleActionMenuDisplay(); this.quill.container.appendChild(this.dialogContainerEl); } // 创建结果弹窗 createResultElement() { if (!this.resultPopupEl) { this.resultPopupEl = document.createElement("div"); this.resultPopupEl.className = "ql-ai-result"; this.resultPopupHeaderEl = document.createElement("div"); this.resultPopupHeaderEl.className = "ql-ai-result-header"; this.resultPopupHeaderEl.textContent = constants.RESULT_HEADER_TEXT; this.resultPopupContentEl = document.createElement("div"); this.resultPopupContentEl.className = "ql-ai-result-content"; this.resultPopupFooterEl = document.createElement("div"); this.resultPopupFooterEl.className = "ql-ai-result-footer"; this.resultPopupFooterTextEl = document.createElement("span"); this.resultPopupFooterTextEl.className = "ql-ai-result-footer-text"; this.resultPopupFooterTextEl.textContent = `0`; this.resultRefreshBtnEl = document.createElement("span"); this.resultRefreshBtnEl.className = "ql-ai-result-footer-refresh"; this.resultRefreshBtnEl.innerHTML = icons.REFRESH_ICON; this.resultCopyBtnEl = document.createElement("span"); this.resultCopyBtnEl.className = "ql-ai-result-footer-copy"; this.resultCopyBtnEl.innerHTML = icons.COPY_ICON; const resultFooterRightEl = document.createElement("div"); resultFooterRightEl.className = "ql-ai-result-footer-right"; resultFooterRightEl.appendChild(this.resultRefreshBtnEl); resultFooterRightEl.appendChild(this.resultCopyBtnEl); this.resultPopupFooterEl.appendChild(this.resultPopupFooterTextEl); this.resultPopupFooterEl.appendChild(resultFooterRightEl); this.resultPopupEl.appendChild(this.resultPopupHeaderEl); this.resultPopupEl.appendChild(this.resultPopupContentEl); this.resultPopupEl.appendChild(this.resultPopupFooterEl); } this.showResultPopupEl = false; } createOperationMenuElements() { if (!this.menuContainerEl) { this.menuContainerEl = document.createElement("div"); this.menuContainerEl.className = "ql-ai-menu-container"; const mainMenu = document.createElement("div"); mainMenu.className = "ql-ai-main-menu"; this.operationMenuList.forEach(({ text, icon, id }) => { const menuItem = document.createElement("div"); menuItem.className = "ql-ai-menu-item"; menuItem.innerHTML = `${icon}<span>${text}</span>${icons.RIGHT_ARROW_ICON}`; menuItem.addEventListener("mouseenter", (e) => { e.stopPropagation(); this.subMenuEl.style.display = "block"; this.subMenuEl.className = `ql-ai-sub-menu ${id}`; this.createOperationMenuItem(id); }); mainMenu.appendChild(menuItem); }); if (!this.subMenuEl) { this.subMenuEl = document.createElement("div"); this.subMenuEl.className = "ql-ai-sub-menu"; this.subMenuEl.style.display = "none"; } this.menuContainerEl.appendChild(mainMenu); this.menuContainerEl.appendChild(this.subMenuEl); } this.showOperationMenu = false; } createOperationMenuItem(id) { let menuItemBox = this[constants.MENU_ID_MAP[id]]; if (!menuItemBox) { menuItemBox = document.createElement("div"); } while (this.subMenuEl.firstChild) { this.subMenuEl.removeChild(this.subMenuEl.firstChild); } constants.MENU_TITLE_DATA[id].forEach(({ text, icon, id: id2 }) => { const menuItem = document.createElement("div"); menuItem.className = "ql-ai-menu-item"; menuItem.innerHTML = `${icon || ""}<span>${text}</span>`; menuItem.addEventListener("click", (e) => { e.stopPropagation(); this.handleOperationMenuItemClick(text, id2); }); menuItemBox.appendChild(menuItem); }); this.subMenuEl.appendChild(menuItemBox); } createInputBoxElements() { if (!this.dialogContainerEl) { this.dialogContainerEl = document.createElement("div"); this.dialogContainerEl.className = "ql-ai-dialog"; this.wrapContainerEl = document.createElement("div"); this.wrapContainerEl.className = "ql-ai-wrapper"; this.wrapContainerEl.style.width = `${this.quill.container.clientWidth - 30}px`; this.createAIInputIcon(); this.inputEl = document.createElement("input"); this.inputEl.type = "text"; this.inputPlaceholder = this._isSelectRangeMode ? constants.SELECT_PLACEHOLDER : constants.INPUT_PLACEHOLDER; this.inputSendBtnEl = document.createElement("span"); this.inputSendBtnEl.className = "ql-ai-input-right-send"; this.inputSendBtnEl.innerHTML = icons.SEND_BTN_ICON; this.inputCloseBtnEl = document.createElement("span"); this.inputCloseBtnEl.className = "ql-ai-input-right-close"; this.inputCloseBtnEl.innerHTML = icons.CLOSE_ICON; this.inputRightEl = document.createElement("div"); this.inputRightEl.className = "ql-ai-input-right"; this.inputContainerEl = document.createElement("div"); this.inputContainerEl.className = "ql-ai-input"; this.inputContainerEl.appendChild(this.aiIconEl); this.inputContainerEl.appendChild(this.inputEl); this.inputRightEl.appendChild(this.inputSendBtnEl); this.inputRightEl.appendChild(this.inputCloseBtnEl); this.inputContainerEl.appendChild(this.inputRightEl); this.wrapContainerEl.appendChild(this.resultPopupEl); this.wrapContainerEl.appendChild(this.inputContainerEl); this.wrapContainerEl.appendChild(this.menuContainerEl); this.dialogContainerEl.appendChild(this.wrapContainerEl); } else { this.dialogContainerEl.style.display = "block"; } this.hiddenInputSendBtnEl(); } hiddenInputSendBtnEl(display = "none") { if (this.inputEl && this.inputSendBtnEl) { this.inputSendBtnEl.style.display = display; } } copyResult() { if (!this.resultPopupContentEl) return; try { const textToCopy = this.resultPopupContentEl.textContent || ""; navigator.clipboard.writeText(textToCopy).then(() => { this.showAlert("内容已复制到剪贴板"); }).catch((err) => { this.showAlert(`复制失败:${err}`); }); } catch (err) { this.showAlert(`复制失败:${err}`); const textarea = document.createElement("textarea"); textarea.value = this.resultPopupContentEl.textContent || ""; document.body.appendChild(textarea); textarea.select(); document.execCommand("copy"); document.body.removeChild(textarea); } } // 分享和朗读功能待放开 // private shareResult() { // if (!this.resultPopupContentEl) return // const textToShare = this.resultPopupContentEl.textContent || '' // const title = 'AI生成内容分享' // if (navigator.share) { // navigator.share({ // title, // text: textToShare, // }) // .catch((err) => { // this.showAlert(`分享失败:${err}`) // }) // } // else { // // 兼容不支持Web Share API的浏览器 // const shareUrl = `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(textToShare)}` // window.open(shareUrl, '_blank') // } // } // private voiceResult() { // if (!this.resultPopupContentEl) return // const textToSpeak = this.resultPopupContentEl.textContent || '' // if ('speechSynthesis' in window) { // const utterance = new SpeechSynthesisUtterance(textToSpeak) // utterance.lang = 'zh-CN' // 设置中文语音 // speechSynthesis.speak(utterance) // } // else { // this.showAlert('当前浏览器不支持语音合成API') // // 可以在这里添加不支持语音的提示 // } // } addResultEvent() { if (this.resultRefreshBtnEl) { this.resultRefreshBtnEl.addEventListener("click", () => { this.regenerateResponse(); }); } if (this.resultCopyBtnEl) { this.resultCopyBtnEl.addEventListener("click", () => { this.copyResult(); }); } } // 显示选中文本的气泡 showSelectionBubble() { if (!this.selectionBubbleEl) { this.selectionBubbleEl = document.createElement("div"); this.selectionBubbleEl.className = "ql-ai-selection-bubble"; const icon = icons.AI_ICON.replaceAll("paint_linear_2", "paint_linear_bubble"); this.selectionBubbleEl.innerHTML = `${icon}<span>AI 智能</span>`; this.selectionBubbleEl.addEventListener("click", () => this.selectTextEvent()); document.body.appendChild(this.selectionBubbleEl); } const { left, top } = this.quill.getBounds(this.selectionRange.index); const { left: endLeft } = this.quill.getBounds(this.selectionRange.index + this.selectionRange.length); const width = (endLeft - left) / 2; const editorRect = this.quill.container.getBoundingClientRect(); this.selectionBubbleEl.style.display = "flex"; this.selectionBubbleEl.style.left = `${left + editorRect.left + width - 45}px`; this.selectionBubbleEl.style.top = `${top + editorRect.top - 40}px`; } // 隐藏选中文本的气泡 hideSelectionBubble() { if (this.selectionBubbleEl) { this.selectionBubbleEl.style.display = "none"; } } // 处理文本选中变化 handleSelectionChange(range) { if (range && range.length > 0) { this.selectionRange = range; this.showSelectionBubble(); this.selectedText = this.quill.getText(range.index, range.length); } else { if (range && range.index !== null) { this.selectedText = ""; this.closeAIPanel(); } else { this.hideSelectionBubble(); } } } addInputEvent() { if (this.inputContainerEl) { this.inputContainerEl.addEventListener("click", () => { }); } if (this.inputEl) { this.inputEl.addEventListener("input", () => { this.hiddenInputSendBtnEl(this.inputEl.value.trim() ? "flex" : "none"); if (this.menuContainerEl && this._isSelectRangeMode) { this.showOperationMenu = !this.inputEl.value.trim() && !this._showResultPopupEl; } }); } if (this.inputSendBtnEl) { this.inputSendBtnEl.addEventListener("click", async () => { await this.queryAI(); }); } this.inputEl.addEventListener("keydown", async (e) => { if (e.key === "Enter") { await this.queryAI(); } }); if (this.inputCloseBtnEl) { this.inputCloseBtnEl.addEventListener("click", () => { this.closeAIPanel(); }); } } positionElements() { if (!this.dialogContainerEl) return; const range = this.selectionRange; if (range) { const bounds = this.quill.getBounds(range.index); this.dialogContainerEl.style.position = "absolute"; this.dialogContainerEl.style.top = `${bounds.top + bounds.height + 20}px`; } } // 添加创建alert元素的方法 createAlertElement() { if (!this.alertEl) { this.alertEl = document.createElement("div"); this.alertEl.className = "ql-ai-alert"; this.alertEl.style.display = "none"; document.body.appendChild(this.alertEl); } } // 添加显示alert的方法 showAlert(message, duration = 3e3) { this.createAlertElement(); if (!this.alertEl) return; if (this.alertTimer) { clearTimeout(this.alertTimer); this.alertTimer = null; } this.alertEl.textContent = message; this.alertEl.style.display = "block"; this.alertTimer = setTimeout(() => { if (this.alertEl) { this.alertEl.style.display = "none"; } this.alertTimer = null; }, duration); } createAIInputIcon() { if (!this.aiIconEl) { this.aiIconEl = document.createElement("span"); this.aiIconEl.className = "ql-ai-input-pre-icon"; const icon = icons.AI_ICON.replaceAll("paint_linear_2", "paint_linear_ai_input"); this.aiIconEl.innerHTML = icon; } } // 添加处理子菜单点击的方法 handleOperationMenuItemClick(text, id = "") { let quetion = ""; if (id.startsWith("1-") || id.startsWith("3-")) { quetion = `将目标文字${text},目标文字为:${this.selectedText}`; } else if (id.startsWith("2-")) { quetion = `改写目标文字的口吻,让其变得${text},目标文字为:${this.selectedText}`; } this.showOperationMenu = false; this.queryAI(quetion); } createActionMenu() { if (!this.actionMenuEl) { this.actionMenuEl = document.createElement("div"); this.actionMenuEl.className = "ql-ai-actions"; this.resultMenuList.forEach(({ text, icon }) => { const menuItem = document.createElement("div"); menuItem.className = "ql-ai-action-item"; menuItem.innerHTML = `${icon}<span class="ql-ai-result-menu-text">${text}</span>`; menuItem.addEventListener("click", () => this.handleAction(text)); this.actionMenuEl.appendChild(menuItem); }); this.wrapContainerEl.appendChild(this.actionMenuEl); } const secondMenuItemText = this.actionMenuEl.children[1].querySelector(".ql-ai-result-menu-text"); const firstChild = this.actionMenuEl.firstChild; if (!this._isSelectRangeMode) { if (firstChild instanceof Element) { firstChild.classList.add("hidden"); } secondMenuItemText.textContent = constants.INSERT_TEXT; } else { if (firstChild instanceof Element) { firstChild.classList.remove("hidden"); } secondMenuItemText.textContent = constants.INSERT_SUB_CONTENT_TEXT; } this.isThinking = false; } handleActionMenuDisplay(value = "none") { if (this.actionMenuEl) { this.actionMenuEl.style.display = value; } } switchInputEl(showInput = true) { if (this.inputContainerEl) { this.inputContainerEl.style.display = showInput ? "flex" : "none"; } this.handleActionMenuDisplay(showInput ? "block" : "none"); if (this.thinkContainerEl) { this.thinkContainerEl.style.display = showInput ? "none" : "flex"; } } // 创建思考元素 createThinkElements() { if (!this.thinkContainerEl) { this.thinkContainerEl = document.createElement("div"); this.thinkContainerEl.className = "ql-ai-input"; this.thinkContainerEl.innerHTML = `<span class="ql-ai-input-pre-icon ql-ai-think-icon">${icons.THINK_ICON}</span><span class="ql-ai-think-text">${constants.THINK_TEXT}</span>`; this.thinkBtnEl = document.createElement("div"); this.thinkBtnEl.className = "ql-ai-think-btn"; this.thinkBtnEl.innerHTML = `${icons.STOP_ICON}<span>${constants.STOP_ANSWER}</span>`; this.thinkContainerEl.appendChild(this.thinkBtnEl); this.wrapContainerEl.appendChild(this.thinkContainerEl); this.thinkBtnEl.addEventListener("click", () => { this.isBreak = true; this.isThinking = false; }); } this.isThinking = true; } // AI查询 async queryAI(question) { this.createThinkElements(); this.inputValue = question || this.inputEl.value; if (this.inputValue.trim() === "") { return; } this.isBreak = false; try { const response = await fetch(`${this.host}`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.apiKey}` }, body: JSON.stringify({ model: this.model, prompt: this.inputValue, stream: true }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let content = ""; while (true) { if (this.isBreak) { this.isBreak = false; break; } const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split("\n").filter((line) => line.trim() !== ""); for (const line of lines) { try { const data = JSON.parse(line); content += data.response || ""; this.showAIResponse(content); } catch (e) { console.error("解析错误:", e); } } } this.createActionMenu(); this.inputEl.value = ""; this.hiddenInputSendBtnEl(); return content; } catch (error) { console.error("AI查询失败:", error); return "AI查询失败,请重试"; } } showAIResponse(response) { if (!this.resultPopupEl) return; if (this._charCount <= this.textNumber) { this.resultPopupContentEl.innerHTML = response; this.charCount = this.resultPopupContentEl.textContent.replace(/\s+/g, "").length; } else { this.isBreak = true; this.charCount = 0; } this.showResultPopupEl = true; } handleAction(action) { switch (action) { case constants.REPLACE_SELECT: this.replaceSelectText(); break; case constants.INSERT_TEXT: this.insertAIResponse(); break; case constants.REGENERATE: this.regenerateResponse(); break; case constants.CLOSE: this.closeAIPanel(); break; } } replaceSelectText() { if (!this.resultPopupContentEl) return; const range = this.quill.getSelection(true); if (range && range.length > 0) { this.quill.deleteText(range.index, range.length); this.quill.clipboard.dangerouslyPasteHTML(range.index, this.resultPopupContentEl.innerHTML); } this.closeAIPanel(); } insertAIResponse() { if (!this.resultPopupContentEl) return; const range = this.quill.getSelection(true); if (range) { this.quill.clipboard.dangerouslyPasteHTML(range.index + range.length, this.resultPopupContentEl.innerHTML); } this.closeAIPanel(); } async regenerateResponse() { await this.queryAI(this.inputValue); } closeAIPanel() { this.isBreak = true; if (this.dialogContainerEl) { this.dialogContainerEl.style.display = "none"; } if (this.actionMenuEl) { this.actionMenuEl.style.display = "none"; } this.showResultPopupEl = false; if (this.inputEl && this.inputEl.value.trim() !== "") { this.inputEl.value = ""; } this.hideSelectionBubble(); } set charCount(value) { if (this._debounceTimer) { clearTimeout(this._debounceTimer); } this._debounceTimer = setTimeout(() => { this._charCount = value; if (this.resultPopupFooterTextEl) { this.resultPopupFooterTextEl.textContent = `${this._charCount}/${this.textNumber}`; } clearTimeout(this._debounceTimer); this._debounceTimer = null; }, 210); } get charCount() { return this._charCount; } set inputPlaceholder(value) { this._inputPlaceholder = value; if (this.inputEl) { this.inputEl.placeholder = value; } } get inputPlaceholder() { return this._inputPlaceholder; } set showOperationMenu(value) { this._showOperationMenu = value; if (this.menuContainerEl) { this.menuContainerEl.style.display = value ? "flex" : "none"; } } get showOperationMenu() { return this._showOperationMenu; } set isSelectRangeMode(value) { this._isSelectRangeMode = value; this.showOperationMenu = value; this.inputPlaceholder = value ? constants.SELECT_PLACEHOLDER : constants.INPUT_PLACEHOLDER; this.hideSelectionBubble(); } get isSelectRangeMode() { return this._isSelectRangeMode; } set isThinking(value) { this._isThinking = value; this.switchInputEl(!value); } get isThinking() { return this._isThinking; } set showResultPopupEl(value) { this._showResultPopupEl = value; if (this.resultPopupEl) { this.resultPopupEl.style.display = value ? "block" : "none"; } } get showResultPopupEl() { return this._showResultPopupEl; } } exports.AI = AI; //# sourceMappingURL=index.cjs.js.map