feat: add translate bubble menu item
This commit is contained in:
parent
b25269c8af
commit
0d6840085f
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -15,3 +15,8 @@ export type AIBubbleMenuItem = {
|
|||
icon: string,
|
||||
title: string,
|
||||
} | string;
|
||||
|
||||
export type TranslateMenuItem = {
|
||||
title: string,
|
||||
language?: string,
|
||||
} | string;
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue