feat: add translate bubble menu item

This commit is contained in:
Michael Yang 2024-09-20 15:33:30 +08:00
parent b25269c8af
commit 0d6840085f
6 changed files with 161 additions and 8 deletions

View File

@ -1,6 +1,6 @@
import {AiModelConfig} from "./core/AiModelConfig.ts";
import {AiModelFactory} from "./AiModelFactory.ts";
import {AIBubbleMenuItem} from "../components/bubbles/types.ts";
import {AIBubbleMenuItem, TranslateMenuItem} from "../components/bubbles/types.ts";
export interface AiMenu {
icon: string,
@ -22,6 +22,10 @@ export interface AiGlobalConfig {
bubblePanelMenus?: AIBubbleMenuItem[],
menus?: AiMenu[],
commands?: AiMenu[],
translate?: {
prompt?: (language: string, selectText: string) => string,
translateMenuItems?: TranslateMenuItem[],
},
codeBlock?: {
codeComments?: {
model: string,

View File

@ -5,8 +5,9 @@ import {Code} from "./Code.ts";
import {Strike} from "./Strike.ts";
import {Italic} from "./Italic.ts";
import {MenuRecord} from "../MenuRecord.ts";
import {Translate} from "./Translate.ts";
export const AllSelectionMenuItems = new MenuRecord(
[AI, Bold, Italic, Underline, Strike, Code]
[AI, Bold, Italic, Underline, Strike, Translate, Code]
)

View File

@ -0,0 +1,105 @@
import {BubbleMenuItem, TranslateMenuItem} from "../../types.ts";
import {t} from "i18next";
import {InnerEditor} from "../../../../core/AiEditor.ts";
import {AiModelManager} from "../../../../ai/AiModelManager.ts";
import {AiClient} from "../../../../ai/core/AiClient.ts";
import tippy, {Instance} from "tippy.js";
type Holder = {
editor?: InnerEditor,
translatePanelInstance?: Instance,
tippyInstance?: Instance,
aiClient?: AiClient
}
export const defaultTranslateMenuItems = [
{title: '英语'},
{title: '中文'},
{title: '日语'},
{title: '法语'},
{title: '德语'},
{title: '葡萄牙语'},
{title: '西班牙语'},
] as TranslateMenuItem[]
const startChat = (holder: Holder, lang: string) => {
if (holder.aiClient) {
holder.aiClient.stop();
} else {
const {selection, doc} = holder.editor!.state
const selectedText = doc.textBetween(selection.from, selection.to);
let prompt = holder.editor?.aiEditor.options.ai?.translate?.prompt?.(lang, selectedText);
if (!prompt) {
prompt = `请帮我把以下内容翻译为: ${lang},并返回翻译结果。注意:只需要翻译的结果,不需要解释!您需要翻译的内容是:\n${selectedText}`
}
const aiModel = AiModelManager.get("auto");
if (aiModel) {
aiModel.chat(selectedText, prompt, {
onStart(aiClient) {
holder.aiClient = aiClient;
},
onStop() {
holder.aiClient = undefined;
},
onMessage(message) {
holder.editor?.commands.insertContent(message.content);
}
})
} else {
console.error("AI model name config error.")
}
}
}
const createTranslatePanelElement = (holder: Holder, menuItems: TranslateMenuItem[]) => {
const container = document.createElement("div");
container.classList.add("aie-translate-panel")
container.innerHTML = `
<div class="aie-translate-panel-body">
${menuItems.map((menuItem) => {
return typeof menuItem === "string" ? `<p data-lang="${menuItem}">${t(menuItem)} </p>`
: `<p data-lang="${menuItem.language || menuItem.title}">${t(menuItem.title)} </p>`;
}).join('')}
</div>
`;
container.querySelectorAll(".aie-translate-panel-body p").forEach((element) => {
const lang = element.getAttribute("data-lang")!;
element.addEventListener("click", () => {
holder.translatePanelInstance?.hide()
startChat(holder, lang);
})
})
return container;
}
export const Translate = {
id: "translate",
title: "translate",
icon: `<div style="display: flex;height: 20px">
<div style="width: 18px;height: 18px;display: inline-block" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 15V17C5 18.0544 5.81588 18.9182 6.85074 18.9945L7 19H10V21H7C4.79086 21 3 19.2091 3 17V15H5ZM18 10L22.4 21H20.245L19.044 18H14.954L13.755 21H11.601L16 10H18ZM17 12.8852L15.753 16H18.245L17 12.8852ZM8 2V4H12V11H8V14H6V11H2V4H6V2H8ZM17 3C19.2091 3 21 4.79086 21 7V9H19V7C19 5.89543 18.1046 5 17 5H14V3H17ZM6 6H4V9H6V6ZM10 6H8V9H10V6Z"></path></svg>
</div>
<div style="width: 18px;height: 18px;display: inline-block" >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 14L8 10H16L12 14Z"></path></svg>
</div>
</div>
`,
onInit: ({innerEditor}, tippyInstance, parentEle) => {
const holder: Holder = {editor: innerEditor, tippyInstance};
const translateMenuItems = innerEditor.aiEditor.options.ai?.translate?.translateMenuItems || defaultTranslateMenuItems;
holder.translatePanelInstance = tippy(parentEle.querySelector("#translate")!, {
content: createTranslatePanelElement(holder, translateMenuItems),
appendTo: innerEditor!.view.dom.closest(".aie-container")!,
placement: "bottom",
// trigger: 'click',
interactive: true,
arrow: false,
})
return holder;
},
} as BubbleMenuItem

View File

@ -15,3 +15,8 @@ export type AIBubbleMenuItem = {
icon: string,
title: string,
} | string;
export type TranslateMenuItem = {
title: string,
language?: string,
} | string;

View File

@ -1,5 +1,4 @@
import {AiEditor} from "./core/AiEditor.ts";
// import {config} from "./spark.ts";
// import { config } from "./spark.ts";
// import {OpenaiModelConfig} from "./ai/openai/OpenaiModelConfig.ts";
// @ts-ignore
@ -13,7 +12,7 @@ window.aiEditor = new AiEditor({
textSelectionBubbleMenu: {
// enable:false
//[AI, Bold, Italic, Underline, Strike, Code]
items: ["ai", "Bold", "Italic", "Underline", "Strike", "code"],
// items: ["ai", "Bold", "Italic", "Underline", "Strike", "code"],
},
// toolbarKeys: ["undo", "redo", "brush", "eraser", "divider", "heading", "font-family", "font-size", "divider", "bold", "italic", "underline"

View File

@ -11,6 +11,44 @@
box-sizing: content-box;
}
.aie-translate-panel {
margin-top: -5px;
&-body {
width: fit-content;
border: solid 1px;
border-color: var(--aie-ai-panel-border);
background: var(--aie-ai-panel-bg-color);
border-radius: 5px;
box-shadow: 0 0 7px 0 rgba(0, 0, 0, .15);
display: flex;
flex-direction: column;
overflow: hidden;
p {
width: 100%;
margin: 0;
cursor: pointer;
display: flex;
align-items: center;
box-sizing: border-box;
height: 40px;
line-height: 40px;
font-weight: 600;
font-size: 14px;
padding: 0 20px;
color: #666;
text-decoration: none;
font-family: Arial, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
}
p:hover {
background: var(--aie-menus-item-hover-color);
}
}
}
.aie-ai-panel-body {
width: 500px;
border: solid 1px;
@ -130,7 +168,7 @@
}
button:hover {
background: #3c77c0;
background: var(--aie-menus-item-hover-color);
cursor: pointer;
}
}
@ -184,7 +222,7 @@
}
p:hover {
background-color: #fafafa;
background: var(--aie-menus-item-hover-color);
}
svg {
@ -436,7 +474,7 @@
justify-content: center;
padding: 2px 1px;
span{
span {
margin-top: 2px;
}
@ -657,8 +695,9 @@
//menu tips start
.tippy-box {
max-width: 1000px!important;
max-width: 1000px !important;
}
.tippy-box[data-animation="fade"][data-state="hidden"] {
opacity: 0;
}