diff --git a/app/appearance/langs/en_US.json b/app/appearance/langs/en_US.json
index 359c142d0..a1c0fbaa8 100644
--- a/app/appearance/langs/en_US.json
+++ b/app/appearance/langs/en_US.json
@@ -22,7 +22,7 @@
"apiBaseURLTip": "The base address of the request, such as https://api.openai.com/v1",
"skip": "Skip",
"reboot": "Reboot",
- "saveLayout": "Save Layout",
+ "save": "Save",
"ai": "Artificial Intelligence",
"aiContinueWrite": "Continue writing",
"aiTranslate": "Translate",
diff --git a/app/appearance/langs/es_ES.json b/app/appearance/langs/es_ES.json
index 1f22e1da8..cdafd42d7 100644
--- a/app/appearance/langs/es_ES.json
+++ b/app/appearance/langs/es_ES.json
@@ -22,7 +22,7 @@
"apiBaseURLTip": "La dirección base de la solicitud, como https://api.openai.com/v1",
"skip": "barco",
"reboot": "Reiniciar",
- "saveLayout": "Guardar diseño",
+ "save": "Ahorrar",
"ai": "Inteligencia Artificial",
"aiContinueWrite": "Continuar escribiendo",
"aiTranslate": "Traducir",
diff --git a/app/appearance/langs/fr_FR.json b/app/appearance/langs/fr_FR.json
index ce9894f08..362cbfb05 100644
--- a/app/appearance/langs/fr_FR.json
+++ b/app/appearance/langs/fr_FR.json
@@ -22,7 +22,7 @@
"apiBaseURLTip": "L'adresse de base de la requête, telle que https://api.openai.com/v1",
"skip": "Navire",
"reboot": "Redémarrer",
- "saveLayout": "Enregistrer la mise en page",
+ "save": "Sauvegarder",
"ai": "Intelligence Artificielle",
"aiContinueWrite": "Continuer à écrire",
"aiTranslate": "Traduire",
diff --git a/app/appearance/langs/zh_CHT.json b/app/appearance/langs/zh_CHT.json
index 76900450d..3ba603bc3 100644
--- a/app/appearance/langs/zh_CHT.json
+++ b/app/appearance/langs/zh_CHT.json
@@ -22,7 +22,7 @@
"apiBaseURLTip": "發起請求的基礎地址,如 https://api.openai.com/v1",
"skip": "跳過",
"reboot": "重啟",
- "saveLayout": "保存佈局",
+ "save": "保存",
"ai": "人工智能",
"aiContinueWrite": "續寫",
"aiTranslate": "翻譯",
diff --git a/app/appearance/langs/zh_CN.json b/app/appearance/langs/zh_CN.json
index c16fe989b..ee8c05096 100644
--- a/app/appearance/langs/zh_CN.json
+++ b/app/appearance/langs/zh_CN.json
@@ -22,7 +22,7 @@
"apiBaseURLTip": "发起请求的基础地址,如 https://api.openai.com/v1",
"skip": "跳过",
"reboot": "重启",
- "saveLayout": "保存布局",
+ "save": "保存",
"ai": "人工智能",
"aiContinueWrite": "续写",
"aiTranslate": "翻译",
diff --git a/app/src/ai/actions.ts b/app/src/ai/actions.ts
index 1059775cc..da9184c6a 100644
--- a/app/src/ai/actions.ts
+++ b/app/src/ai/actions.ts
@@ -8,8 +8,12 @@ import {getContenteditableElement} from "../protyle/wysiwyg/getBlock";
import {blockRender} from "../protyle/markdown/blockRender";
import {processRender} from "../protyle/util/processCode";
import {highlightRender} from "../protyle/markdown/highlightRender";
+import {Constants} from "../constants";
+import {setStorageVal} from "../protyle/util/compatibility";
+import {hasClosestByClassName} from "../protyle/util/hasClosest";
+import {escapeHtml} from "../util/escape";
-export const fillContent = (protyle:IProtyle, data:string, elements:Element[]) => {
+export const fillContent = (protyle: IProtyle, data: string, elements: Element[]) => {
setLastNodeRange(getContenteditableElement(elements[elements.length - 1]), protyle.toolbar.range);
protyle.toolbar.range.collapse(true);
insertHTML(data, protyle, true, true);
@@ -23,11 +27,122 @@ export const AIActions = (elements: Element[], protyle: IProtyle) => {
elements.forEach(item => {
ids.push(item.getAttribute("data-node-id"));
});
+ const customMenu: IMenu[] = [{
+ iconHTML: Constants.ZWSP,
+ label: window.siyuan.languages.aiCustomAction,
+ click() {
+ const dialog = new Dialog({
+ title: window.siyuan.languages.aiCustomAction,
+ content: `
+
+
+
+
`,
+ width: isMobile() ? "80vw" : "520px",
+ });
+ const inputElement = dialog.element.querySelector("input") as HTMLInputElement;
+ const btnsElement = dialog.element.querySelectorAll(".b3-button");
+ dialog.bindInput(inputElement, () => {
+ (btnsElement[1] as HTMLButtonElement).click();
+ });
+ inputElement.focus();
+ btnsElement[0].addEventListener("click", () => {
+ dialog.destroy();
+ });
+ btnsElement[1].addEventListener("click", () => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: inputElement.value,
+ }, (response) => {
+ dialog.destroy();
+ let respContent = "";
+ if (response.data && "" !== response.data) {
+ respContent = "\n\n" + response.data;
+ }
+ fillContent(protyle, `${inputElement.value}${respContent}`, elements);
+ });
+ });
+ }
+ }, {
+ iconHTML: Constants.ZWSP,
+ label: window.siyuan.languages.save,
+ click() {
+ const dialog = new Dialog({
+ title: window.siyuan.languages.save,
+ content: `
+
+
+
+
`,
+ width: isMobile() ? "80vw" : "520px",
+ });
+ const inputElement = dialog.element.querySelector("input");
+ const btnsElement = dialog.element.querySelectorAll(".b3-button");
+ dialog.bindInput(inputElement, () => {
+ (btnsElement[1] as HTMLButtonElement).click();
+ });
+ inputElement.focus();
+ btnsElement[0].addEventListener("click", () => {
+ dialog.destroy();
+ });
+ btnsElement[1].addEventListener("click", () => {
+ window.siyuan.storage[Constants.LOCAL_AI].push({
+ name: inputElement.value,
+ memo: dialog.element.querySelector("textarea").value,
+ })
+ setStorageVal(Constants.LOCAL_AI, window.siyuan.storage[Constants.LOCAL_AI]);
+ dialog.destroy();
+ });
+ }
+ }]
+ window.siyuan.storage[Constants.LOCAL_AI].forEach((item: { name: string, memo: string }) => {
+ customMenu.push({
+ iconHTML: Constants.ZWSP,
+ action: "iconCloseRound",
+ label: escapeHtml(item.name),
+ bind: (element) => {
+ element.addEventListener("click", (event) => {
+ if (hasClosestByClassName(event.target as Element, "b3-menu__action")) {
+ window.siyuan.storage[Constants.LOCAL_AI].find((subItem: {
+ name: string,
+ memo: string
+ }, index: number) => {
+ if (element.querySelector(".b3-menu__label").textContent.trim() === subItem.name) {
+ window.siyuan.storage[Constants.LOCAL_AI].splice(index, 1);
+ setStorageVal(Constants.LOCAL_AI, window.siyuan.storage[Constants.LOCAL_AI]);
+ element.remove();
+ return true;
+ }
+ })
+ } else {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: item.memo,
+ }, (response) => {
+ let respContent = "";
+ if (response.data && "" !== response.data) {
+ respContent = "\n\n" + response.data;
+ }
+ fillContent(protyle, `${item.memo}${respContent}`, elements);
+ });
+ window.siyuan.menus.menu.remove();
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ });
+ }
+ })
+ });
window.siyuan.menus.menu.append(new MenuItem({
icon: "iconSparkles",
label: window.siyuan.languages.ai,
type: "submenu",
submenu: [{
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiContinueWrite,
click() {
fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Continue writing"}, (response) => {
@@ -35,115 +150,126 @@ export const AIActions = (elements: Element[], protyle: IProtyle) => {
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiTranslate,
type: "submenu",
submenu: [{
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiTranslate_zh_CN,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Translate as follows to [zh_CN]"}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: "Translate as follows to [zh_CN]"
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiTranslate_ja_JP,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Translate as follows to [ja_JP]"}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: "Translate as follows to [ja_JP]"
+ }, (response) => {
focusByRange(protyle.toolbar.range);
insertHTML(response.data, protyle, true);
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiTranslate_ko_KR,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Translate as follows to [ko_KR]"}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: "Translate as follows to [ko_KR]"
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiTranslate_en_US,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Translate as follows to [en_US]"}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: "Translate as follows to [en_US]"
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiTranslate_es_ES,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Translate as follows to [es_ES]"}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: "Translate as follows to [es_ES]"
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiTranslate_fr_FR,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Translate as follows to [fr_FR]"}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: "Translate as follows to [fr_FR]"
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiTranslate_de_DE,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: "Translate as follows to [de_DE]"}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: "Translate as follows to [de_DE]"
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
}]
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiExtractSummary,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: window.siyuan.languages.aiExtractSummary}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: window.siyuan.languages.aiExtractSummary
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiBrainStorm,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: window.siyuan.languages.aiBrainStorm}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: window.siyuan.languages.aiBrainStorm
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
}, {
+ iconHTML: Constants.ZWSP,
label: window.siyuan.languages.aiFixGrammarSpell,
click() {
- fetchPost("/api/ai/chatGPTWithAction", {ids, action: window.siyuan.languages.aiFixGrammarSpell}, (response) => {
+ fetchPost("/api/ai/chatGPTWithAction", {
+ ids,
+ action: window.siyuan.languages.aiFixGrammarSpell
+ }, (response) => {
fillContent(protyle, response.data, elements);
});
}
- },{
- label: window.siyuan.languages.aiCustomAction,
- click() {
- const dialog = new Dialog({
- title: window.siyuan.languages.aiCustomAction,
- content: `
-
-
-
-
`,
- width: isMobile() ? "80vw" : "520px",
- });
- const inputElement = dialog.element.querySelector("input") as HTMLInputElement;
- const btnsElement = dialog.element.querySelectorAll(".b3-button");
- dialog.bindInput(inputElement, () => {
- (btnsElement[1] as HTMLButtonElement).click();
- });
- inputElement.focus();
- btnsElement[0].addEventListener("click", () => {
- dialog.destroy();
- });
- btnsElement[1].addEventListener("click", () => {
- fetchPost("/api/ai/chatGPTWithAction", {
- ids,
- action: inputElement.value,
- }, (response) => {
- dialog.destroy();
- let respContent = "";
- if (response.data && "" !== response.data) {
- respContent = "\n\n" + response.data;
- }
- fillContent(protyle, `${inputElement.value}${respContent}`, elements);
- });
- });
- }
+ }, {
+ iconHTML: Constants.ZWSP,
+ label: window.siyuan.languages.custom,
+ type: "submenu",
+ submenu: customMenu
}]
}).element);
};
diff --git a/app/src/constants.ts b/app/src/constants.ts
index 9cfc237f6..ea39d4184 100644
--- a/app/src/constants.ts
+++ b/app/src/constants.ts
@@ -79,6 +79,7 @@ export abstract class Constants {
public static readonly LOCAL_BAZAAR = "local-bazaar";
public static readonly LOCAL_PDFTHEME = "local-pdftheme";
public static readonly LOCAL_LAYOUTS = "local-layouts";
+ public static readonly LOCAL_AI = "local-ai";
// timeout
public static readonly TIMEOUT_DBLCLICK = 190;
diff --git a/app/src/menus/workspace.ts b/app/src/menus/workspace.ts
index b76c82327..f5019e3ac 100644
--- a/app/src/menus/workspace.ts
+++ b/app/src/menus/workspace.ts
@@ -105,10 +105,10 @@ export const workspaceMenu = (rect: DOMRect) => {
/// #endif
const layoutSubMenu: IMenu[] = [{
iconHTML: Constants.ZWSP,
- label: window.siyuan.languages.saveLayout,
+ label: window.siyuan.languages.save,
click() {
const saveDialog = new Dialog({
- title: window.siyuan.languages.saveLayout,
+ title: window.siyuan.languages.save,
content: `
diff --git a/app/src/protyle/util/compatibility.ts b/app/src/protyle/util/compatibility.ts
index e7741385d..746fe8ad9 100644
--- a/app/src/protyle/util/compatibility.ts
+++ b/app/src/protyle/util/compatibility.ts
@@ -156,6 +156,7 @@ export const getLocalStorage = (cb: () => void) => {
};
defaultStorage[Constants.LOCAL_PDFTHEME] = {light: "light", dark: "dark", annoColor: "var(--b3-pdf-background1)"};
defaultStorage[Constants.LOCAL_LAYOUTS] = []; // {name: "", layout:{}}
+ defaultStorage[Constants.LOCAL_AI] = []; // {name: "", memo: ""}
defaultStorage[Constants.LOCAL_BAZAAR] = {
theme: "0",
template: "0",
@@ -209,7 +210,7 @@ export const getLocalStorage = (cb: () => void) => {
[Constants.LOCAL_EXPORTIMG, Constants.LOCAL_SEARCHKEYS, Constants.LOCAL_PDFTHEME, Constants.LOCAL_BAZAAR, Constants.LOCAL_EXPORTWORD,
Constants.LOCAL_EXPORTPDF, Constants.LOCAL_DOCINFO, Constants.LOCAL_FONTSTYLES, Constants.LOCAL_SEARCHDATA,
- Constants.LOCAL_ZOOM, Constants.LOCAL_SEARCHKEY, Constants.LOCAL_LAYOUTS].forEach((key) => {
+ Constants.LOCAL_ZOOM, Constants.LOCAL_SEARCHKEY, Constants.LOCAL_LAYOUTS, Constants.LOCAL_AI].forEach((key) => {
if (typeof response.data[key] === "string") {
try {
window.siyuan.storage[key] = Object.assign(defaultStorage[key], JSON.parse(response.data[key]));