From 1c4718fcb95a0377b9fbdd182d57bf08bc169166 Mon Sep 17 00:00:00 2001 From: Vanessa Date: Fri, 8 Mar 2024 12:36:36 +0800 Subject: [PATCH] :art: https://github.com/siyuan-note/siyuan/issues/10472 --- app/src/ai/actions.ts | 542 ++++++++++++++--------------- app/src/protyle/gutter/index.ts | 18 +- app/src/protyle/wysiwyg/keydown.ts | 12 + 3 files changed, 287 insertions(+), 285 deletions(-) diff --git a/app/src/ai/actions.ts b/app/src/ai/actions.ts index ca466a071..0aea42ec0 100644 --- a/app/src/ai/actions.ts +++ b/app/src/ai/actions.ts @@ -1,6 +1,5 @@ -import {MenuItem} from "../menus/Menu"; import {fetchPost} from "../util/fetch"; -import {setLastNodeRange} from "../protyle/util/selection"; +import {focusByRange, setLastNodeRange} from "../protyle/util/selection"; import {insertHTML} from "../protyle/util/insertHTML"; import {Dialog} from "../dialog"; import {isMobile} from "../util/functions"; @@ -10,9 +9,10 @@ import {processRender} from "../protyle/util/processCode"; import {highlightRender} from "../protyle/render/highlightRender"; import {Constants} from "../constants"; import {setStorageVal} from "../protyle/util/compatibility"; -import {hasClosestByClassName} from "../protyle/util/hasClosest"; import {escapeAriaLabel, escapeHtml} from "../util/escape"; import {showMessage} from "../dialog/message"; +import {Menu} from "../plugin/Menu"; +import {upDownHint} from "../util/upDownHint"; export const fillContent = (protyle: IProtyle, data: string, elements: Element[]) => { if (!data) { @@ -26,81 +26,10 @@ export const fillContent = (protyle: IProtyle, data: string, elements: Element[] highlightRender(protyle.wysiwyg.element); }; -export const AIActions = (elements: Element[], protyle: IProtyle) => { - const ids: string[] = []; - elements.forEach(item => { - ids.push(item.getAttribute("data-node-id")); - }); - const customMenu: IMenu[] = [{ - iconHTML: "", - label: window.siyuan.languages.aiCustomAction, - click() { - const dialog = new Dialog({ - title: window.siyuan.languages.aiCustomAction, - content: `
- -
- -
-
-
-
- -
`, - width: isMobile() ? "92vw" : "520px", - }); - dialog.element.setAttribute("data-key", Constants.DIALOG_AICUSTOMACTION); - const nameElement = dialog.element.querySelector("input"); - const customElement = dialog.element.querySelector("textarea"); - const btnsElement = dialog.element.querySelectorAll(".b3-button"); - dialog.bindInput(customElement, () => { - (btnsElement[1] as HTMLButtonElement).click(); - }); - btnsElement[0].addEventListener("click", () => { - dialog.destroy(); - }); - btnsElement[1].addEventListener("click", () => { - if (!customElement.value) { - showMessage(window.siyuan.languages["_kernel"][142]); - return; - } - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: customElement.value, - }, (response) => { - dialog.destroy(); - fillContent(protyle, response.data, elements); - }); - }); - btnsElement[2].addEventListener("click", () => { - if (!nameElement.value && !customElement.value) { - showMessage(window.siyuan.languages["_kernel"][142]); - return; - } - window.siyuan.storage[Constants.LOCAL_AI].push({ - name: nameElement.value, - memo: customElement.value - }); - setStorageVal(Constants.LOCAL_AI, window.siyuan.storage[Constants.LOCAL_AI]); - dialog.destroy(); - }); - nameElement.focus(); - } - }]; - if (window.siyuan.storage[Constants.LOCAL_AI].length > 0) { - customMenu.push({type: "separator"}); - } - window.siyuan.storage[Constants.LOCAL_AI].forEach((item: { name: string, memo: string }) => { - customMenu.push({ - iconHTML: "", - action: "iconEdit", - label: `
${escapeHtml(item.name)}
`, - bind: (element) => { - element.addEventListener("click", (event) => { - if (hasClosestByClassName(event.target as Element, "b3-menu__action")) { - const dialog = new Dialog({ - title: window.siyuan.languages.update, - content: `
+const editDialog = (customName: string, customMemo: string) => { + const dialog = new Dialog({ + title: window.siyuan.languages.update, + content: `
@@ -110,217 +39,264 @@ export const AIActions = (elements: Element[], protyle: IProtyle) => {
`, - width: isMobile() ? "92vw" : "520px", - }); - dialog.element.setAttribute("data-key", Constants.DIALOG_AIUPDATECUSTOMACTION); - const nameElement = dialog.element.querySelector("input"); - nameElement.value = item.name; - const customElement = dialog.element.querySelector("textarea"); - const btnsElement = dialog.element.querySelectorAll(".b3-button"); - dialog.bindInput(customElement, () => { - (btnsElement[1] as HTMLButtonElement).click(); - }); - customElement.value = item.memo; - btnsElement[0].addEventListener("click", () => { - dialog.destroy(); - }); - btnsElement[1].addEventListener("click", () => { - window.siyuan.storage[Constants.LOCAL_AI].find((subItem: { - name: string, - memo: string - }) => { - if (item.name === subItem.name && item.memo === subItem.memo) { - item.name = nameElement.value; - item.memo = customElement.value; - setStorageVal(Constants.LOCAL_AI, window.siyuan.storage[Constants.LOCAL_AI]); - return true; - } - }); - dialog.destroy(); - }); - btnsElement[2].addEventListener("click", () => { - window.siyuan.storage[Constants.LOCAL_AI].find((subItem: { - name: string, - memo: string - }, index: number) => { - if (item.name === subItem.name && item.memo === subItem.memo) { - window.siyuan.storage[Constants.LOCAL_AI].splice(index, 1); - setStorageVal(Constants.LOCAL_AI, window.siyuan.storage[Constants.LOCAL_AI]); - return true; - } - }); - dialog.destroy(); - }); - nameElement.focus(); + width: isMobile() ? "92vw" : "520px", + }); + dialog.element.setAttribute("data-key", Constants.DIALOG_AIUPDATECUSTOMACTION); + const nameElement = dialog.element.querySelector("input"); + nameElement.value = customName; + const customElement = dialog.element.querySelector("textarea"); + const btnsElement = dialog.element.querySelectorAll(".b3-button"); + dialog.bindInput(customElement, () => { + (btnsElement[1] as HTMLButtonElement).click(); + }); + customElement.value = customMemo; + btnsElement[0].addEventListener("click", () => { + dialog.destroy(); + }); + btnsElement[1].addEventListener("click", () => { + window.siyuan.storage[Constants.LOCAL_AI].find((subItem: { + name: string, + memo: string + }) => { + if (customName === subItem.name && customMemo === subItem.memo) { + subItem.name = nameElement.value; + subItem.memo = customElement.value; + setStorageVal(Constants.LOCAL_AI, window.siyuan.storage[Constants.LOCAL_AI]); + return true; + } + }); + dialog.destroy(); + }); + btnsElement[2].addEventListener("click", () => { + window.siyuan.storage[Constants.LOCAL_AI].find((subItem: { + name: string, + memo: string + }, index: number) => { + if (customName === subItem.name && customMemo === subItem.memo) { + window.siyuan.storage[Constants.LOCAL_AI].splice(index, 1); + setStorageVal(Constants.LOCAL_AI, window.siyuan.storage[Constants.LOCAL_AI]); + return true; + } + }); + dialog.destroy(); + }); + nameElement.focus(); +} + +const customDialog = (protyle: IProtyle, ids: string[], elements: Element[]) => { + const dialog = new Dialog({ + title: window.siyuan.languages.aiCustomAction, + content: `
+ +
+ +
+
+
+
+ +
`, + width: isMobile() ? "92vw" : "520px", + }); + dialog.element.setAttribute("data-key", Constants.DIALOG_AICUSTOMACTION); + const nameElement = dialog.element.querySelector("input"); + const customElement = dialog.element.querySelector("textarea"); + const btnsElement = dialog.element.querySelectorAll(".b3-button"); + dialog.bindInput(customElement, () => { + (btnsElement[1] as HTMLButtonElement).click(); + }); + btnsElement[0].addEventListener("click", () => { + dialog.destroy(); + }); + btnsElement[1].addEventListener("click", () => { + if (!customElement.value) { + showMessage(window.siyuan.languages["_kernel"][142]); + return; + } + fetchPost("/api/ai/chatGPTWithAction", { + ids, + action: customElement.value, + }, (response) => { + dialog.destroy(); + fillContent(protyle, response.data, elements); + }); + }); + btnsElement[2].addEventListener("click", () => { + if (!nameElement.value && !customElement.value) { + showMessage(window.siyuan.languages["_kernel"][142]); + return; + } + window.siyuan.storage[Constants.LOCAL_AI].push({ + name: nameElement.value, + memo: customElement.value + }); + setStorageVal(Constants.LOCAL_AI, window.siyuan.storage[Constants.LOCAL_AI]); + dialog.destroy(); + }); + nameElement.focus(); +} + +const filterAI = (element: HTMLElement, inputElement: HTMLInputElement) => { + element.querySelectorAll(".b3-list-item").forEach(item => { + if (item.textContent.indexOf(inputElement.value) > -1) { + item.classList.remove("fn__none"); + } else { + item.classList.add("fn__none"); + } + }) + element.querySelectorAll('.b3-menu__separator').forEach(item => { + if (inputElement.value) { + item.classList.add("fn__none"); + } else { + item.classList.remove("fn__none"); + } + }) + element.querySelector(".b3-list-item--focus").classList.remove("b3-list-item--focus"); + element.querySelector(".b3-list-item:not(.fn__none)").classList.add("b3-list-item--focus"); +} + +export const AIActions = (elements: Element[], protyle: IProtyle) => { + window.siyuan.menus.menu.remove(); + const ids: string[] = []; + elements.forEach(item => { + ids.push(item.getAttribute("data-node-id")); + }); + const menu = new Menu("ai", () => { + focusByRange(protyle.toolbar.range); + }); + let customHTML = "" + window.siyuan.storage[Constants.LOCAL_AI].forEach((item: { name: string, memo: string }) => { + customHTML += `
+ ${escapeHtml(item.name)} + +
`; + }); + if (customHTML) { + customHTML = `
${customHTML}`; + } + menu.addItem({ + iconHTML: "", + type: "empty", + label: `
+ +
+
+
+ ${window.siyuan.languages.aiContinueWrite} +
+
+
+ ${window.siyuan.languages.aiTranslate_zh_Hans} +
+
+ ${window.siyuan.languages.aiTranslate_zh_Hant} +
+
+ ${window.siyuan.languages.aiTranslate_ja_JP} +
+
+ ${window.siyuan.languages.aiTranslate_ko_KR} +
+
+ ${window.siyuan.languages.aiTranslate_en_US} +
+
+ ${window.siyuan.languages.aiTranslate_es_ES} +
+
+ ${window.siyuan.languages.aiTranslate_fr_FR} +
+
+ ${window.siyuan.languages.aiTranslate_de_DE} +
+
+
+ ${window.siyuan.languages.aiExtractSummary} +
+
+ ${window.siyuan.languages.aiBrainStorm} +
+
+ ${window.siyuan.languages.aiFixGrammarSpell} +
+
+ ${window.siyuan.languages.clearContext} +
+
+ ${window.siyuan.languages.aiCustomAction} +
+ ${customHTML} +
+
`, + bind(element) { + const listElement = element.querySelector(".b3-list"); + const inputElement = element.querySelector("input"); + inputElement.addEventListener("keydown", (event: KeyboardEvent) => { + if (event.isComposing) { + return; + } + const currentElement = upDownHint(listElement, event); + if (currentElement) { + event.stopPropagation(); + } + if (event.key === "Enter") { + event.preventDefault(); + event.stopPropagation(); + const currentElement = listElement.querySelector(".b3-list-item--focus") as HTMLElement; + if (currentElement.dataset.type === "custom") { + customDialog(protyle, ids, elements); } else { fetchPost("/api/ai/chatGPTWithAction", { ids, - action: item.memo, + action: currentElement.dataset.action }, (response) => { fillContent(protyle, response.data, elements); }); } - window.siyuan.menus.menu.remove(); - event.preventDefault(); - event.stopPropagation(); - }); - } - }); + menu.close(); + } + }); + inputElement.addEventListener("compositionend", () => { + filterAI(element, inputElement); + }) + inputElement.addEventListener("input", (event: KeyboardEvent) => { + if (event.isComposing) { + return; + } + filterAI(element, inputElement); + }); + element.addEventListener("click", (event) => { + let target = event.target as HTMLElement; + while (target && !target.isSameNode(element)) { + if (target.classList.contains("b3-list-item__action")) { + editDialog(target.previousElementSibling.textContent, target.parentElement.getAttribute("aria-label")); + event.stopPropagation(); + event.preventDefault(); + break; + } else if (target.classList.contains("b3-list-item")) { + if (target.dataset.type === "custom") { + customDialog(protyle, ids, elements); + } else { + fetchPost("/api/ai/chatGPTWithAction", {ids, action: target.dataset.action}, (response) => { + fillContent(protyle, response.data, elements); + }); + } + event.stopPropagation(); + event.preventDefault(); + break; + } + target = target.parentElement; + } + menu.close(); + }); + } }); - window.siyuan.menus.menu.append(new MenuItem({ - icon: "iconSparkles", - label: window.siyuan.languages.ai, - type: "submenu", - submenu: [{ - iconHTML: "", - label: window.siyuan.languages.aiContinueWrite, - click() { - fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Continue writing"}, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiTranslate, - type: "submenu", - submenu: [{ - iconHTML: "", - label: window.siyuan.languages.aiTranslate_zh_Hans, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Translate as follows to [zh-Hans]" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiTranslate_zh_Hant, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Translate as follows to [zh-Hant]" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiTranslate_ja_JP, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Translate as follows to [ja-JP]" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiTranslate_ko_KR, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Translate as follows to [ko-KR]" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiTranslate_en_US, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Translate as follows to [en-US]" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiTranslate_es_ES, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Translate as follows to [es-ES]" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiTranslate_fr_FR, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Translate as follows to [fr-FR]" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiTranslate_de_DE, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Translate as follows to [de-DE]" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }] - }, { - iconHTML: "", - label: window.siyuan.languages.aiExtractSummary, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: window.siyuan.languages.aiExtractSummary - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiBrainStorm, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: window.siyuan.languages.aiBrainStorm - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.aiFixGrammarSpell, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: window.siyuan.languages.aiFixGrammarSpell - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.clearContext, - click() { - fetchPost("/api/ai/chatGPTWithAction", { - ids, - action: "Clear context" - }, (response) => { - fillContent(protyle, response.data, elements); - }); - } - }, { - iconHTML: "", - label: window.siyuan.languages.custom, - type: "submenu", - submenu: customMenu - }] - }).element); + menu.element.querySelector(".b3-menu__items").setAttribute("style", "overflow: initial"); + const rect = elements[elements.length - 1].getBoundingClientRect(); + menu.open({ + x: rect.left, + y: rect.bottom, + h: rect.height, + }); + menu.element.querySelector("input").focus(); }; diff --git a/app/src/protyle/gutter/index.ts b/app/src/protyle/gutter/index.ts index 36ee04470..254a51d94 100644 --- a/app/src/protyle/gutter/index.ts +++ b/app/src/protyle/gutter/index.ts @@ -714,7 +714,14 @@ export class Gutter { } } if (!protyle.disabled) { - AIActions(selectsElement, protyle); + window.siyuan.menus.menu.append(new MenuItem({ + icon: "iconSparkles", + label: window.siyuan.languages.ai, + accelerator: window.siyuan.config.keymap.editor.general.ai.custom, + click() { + AIActions(selectsElement, protyle); + } + }).element); } const copyMenu: IMenu[] = [{ label: window.siyuan.languages.copy, @@ -1175,7 +1182,14 @@ export class Gutter { }).element); } if (!protyle.disabled && !nodeElement.classList.contains("hr")) { - AIActions([nodeElement], protyle); + window.siyuan.menus.menu.append(new MenuItem({ + icon: "iconSparkles", + label: window.siyuan.languages.ai, + accelerator: window.siyuan.config.keymap.editor.general.ai.custom, + click() { + AIActions([nodeElement], protyle); + } + }).element); } const copyMenu = (copySubMenu(id, true, nodeElement) as IMenu[]).concat([{ label: window.siyuan.languages.copy, diff --git a/app/src/protyle/wysiwyg/keydown.ts b/app/src/protyle/wysiwyg/keydown.ts index af3a7772f..374a53b0b 100644 --- a/app/src/protyle/wysiwyg/keydown.ts +++ b/app/src/protyle/wysiwyg/keydown.ts @@ -67,6 +67,7 @@ import {getSavePath, newFileBySelect} from "../../util/newFile"; import {removeSearchMark} from "../toolbar/util"; import {avKeydown} from "../render/av/keydown"; import {checkFold} from "../../util/noRelyPCFunction"; +import {AIActions} from "../../ai/actions"; export const getContentByInlineHTML = (range: Range, cb: (content: string) => void) => { let html = ""; @@ -1404,6 +1405,17 @@ export const keydown = (protyle: IProtyle, editorElement: HTMLElement) => { return; } + if (!event.repeat && matchHotKey(window.siyuan.config.keymap.editor.general.ai.custom, event)) { + event.preventDefault(); + event.stopPropagation(); + let selectsElement: HTMLElement[] = Array.from(protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--select")); + if (selectsElement.length === 0) { + selectsElement = [nodeElement]; + } + AIActions(selectsElement, protyle) + return; + } + // tab 需等待 list 和 table 处理完成 if (event.key === "Tab" && isNotCtrl(event) && !event.altKey) { event.preventDefault();