添加快捷菜单功能
This commit is contained in:
@@ -4,6 +4,12 @@
|
||||
* 定义常用的图标
|
||||
*/
|
||||
export const Icons = {
|
||||
AiAsk: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M15 5.25C16.7949 5.25 18.25 3.79493 18.25 2H19.75C19.75 3.79493 21.2051 5.25 23 5.25V6.75C21.2051 6.75 19.75 8.20507 19.75 10H18.25C18.25 8.20507 16.7949 6.75 15 6.75V5.25ZM4 7C4 5.89543 4.89543 5 6 5H13V3H6C3.79086 3 2 4.79086 2 7V17C2 19.2091 3.79086 21 6 21H18C20.2091 21 22 19.2091 22 17V12H20V17C20 18.1046 19.1046 19 18 19H6C4.89543 19 4 18.1046 4 17V7Z" fill="currentColor"></path></svg>`,
|
||||
AiContinue: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M4 18.9997H20V13.9997H22V19.9997C22 20.552 21.5523 20.9997 21 20.9997H3C2.44772 20.9997 2 20.552 2 19.9997V13.9997H4V18.9997ZM16.1716 6.9997L12.2218 3.04996L13.636 1.63574L20 7.9997L13.636 14.3637L12.2218 12.9495L16.1716 8.9997H5V6.9997H16.1716Z"></path></svg>`,
|
||||
AiRewrite: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M21 19V21H19V19H21ZM17 19V21H15V19H17ZM13 19V21H11V19H13ZM9 19V21H7V19H9ZM5 19V21H3V19H5ZM21 15V17H19V15H21ZM5 15V17H3V15H5ZM5 11V13H3V11H5ZM16 3C18.6874 3 20.8817 5.12366 20.9954 7.78322L21 8V13H19V8C19 6.40893 17.7447 5.09681 16.1756 5.00512L16 5H11V3H16ZM5 7V9H3V7H5ZM5 3V5H3V3H5ZM9 3V5H7V3H9Z" fill="currentColor"></path></svg>`,
|
||||
AiReview: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M17.934 3.0359L19.666 4.0359L18.531 6H21V8H19V12H21V14H19V21H17V14L13.9157 14.0004C13.5914 16.8623 12.3522 19.3936 10.5466 21.1933L8.98361 19.9233C10.5031 18.4847 11.5801 16.4008 11.9008 14.0009L10 14V12L12 11.999V8H10V6H12.467L11.334 4.0359L13.066 3.0359L14.777 6H16.221L17.934 3.0359ZM5 13.803L3 14.339V12.268L5 11.732V8H3V6H5V3H7V6H9V8H7V11.197L9 10.661V12.731L7 13.267V18.5C7 19.8807 5.88071 21 4.5 21H3V19H4.5C4.74546 19 4.94961 18.8231 4.99194 18.5899L5 18.5V13.803ZM17 8H14V12H17V8Z"></path></svg>`,
|
||||
AiTranslate: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><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" fill="currentColor"></path></svg>`,
|
||||
AiImage: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M20 3C20.5523 3 21 3.44772 21 4V5.757L19 7.757V5H5V13.1L9 9.1005L13.328 13.429L11.9132 14.8422L9 11.9289L5 15.928V19H15.533L16.2414 19.0012L17.57 17.671L18.8995 19H19V16.242L21 14.242V20C21 20.5523 20.5523 21 20 21H4C3.45 21 3 20.55 3 20V4C3 3.44772 3.44772 3 4 3H20ZM21.7782 7.80761L23.1924 9.22183L15.4142 17L13.9979 16.9979L14 15.5858L21.7782 7.80761ZM15.5 7C16.3284 7 17 7.67157 17 8.5C17 9.32843 16.3284 10 15.5 10C14.6716 10 14 9.32843 14 8.5C14 7.67157 14.6716 7 15.5 7Z" fill="currentColor"></path></svg>`,
|
||||
ArrowDown: `<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="3 5 24 14"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 14L8 10H16L12 14Z"></path></svg>`,
|
||||
Loading: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30px" height="30px" viewBox="0 0 50 50" style="enable-background:new 0 0 50 50" xml:space="preserve"><path fill="#FF6700" d="M43.935,25.145c0-10.318-8.364-18.683-18.683-18.683c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615c8.072,0,14.615,6.543,14.615,14.615H43.935z" transform="rotate(275.098 25 25)"><animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0 25 25" to="360 25 25" dur="0.6s" repeatCount="indefinite"></animateTransform></path></svg>`,
|
||||
// Loading: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"></path><path d="M12 3C16.9706 3 21 7.02944 21 12H19C19 8.13401 15.866 5 12 5V3Z"></path></svg>`,
|
||||
|
||||
@@ -26,9 +26,11 @@ import i18next from "i18next";
|
||||
import { zh } from "../i18n/zh.ts";
|
||||
import { Resource } from "i18next";
|
||||
import { allExtensions } from "./UAIExtensions.ts";
|
||||
import { AICommand } from "../ai/config/AIConfig.ts";
|
||||
import { AIChatConfig, Text2ImageConfig } from "../ai/config/AIConfig.ts";
|
||||
import { markdownToHtml } from "../utils/MarkdownUtil.ts";
|
||||
import { Uploader } from "../utils/FileUploader.ts";
|
||||
import { Icons } from "../components/Icons.ts";
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_workerId, _label) {
|
||||
@@ -111,15 +113,63 @@ export type UAIEditorOptions = {
|
||||
ai?: {
|
||||
chat?: {
|
||||
models?: Record<string, AIChatConfig>,
|
||||
commands?: AICommand[],
|
||||
},
|
||||
image?: {
|
||||
models?: {
|
||||
text2image?: Record<string, Text2ImageConfig>,
|
||||
},
|
||||
commands?: AICommand[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义默认的对话快捷命令
|
||||
*/
|
||||
const defaultChatCommands = [
|
||||
{
|
||||
icon: Icons.AiAsk,
|
||||
name: "AI 问答",
|
||||
model: "default",
|
||||
},
|
||||
{
|
||||
icon: Icons.AiContinue,
|
||||
name: "AI 续写",
|
||||
prompt: "请帮我继续扩展一下这段话的内容。",
|
||||
model: "default",
|
||||
},
|
||||
{
|
||||
icon: Icons.AiRewrite,
|
||||
name: "AI 重写",
|
||||
prompt: "请帮我重写写一下这段话的内容。",
|
||||
model: "default",
|
||||
},
|
||||
{
|
||||
icon: Icons.AiReview,
|
||||
name: "AI 校阅",
|
||||
prompt: "请帮我改正这段话中的错别字和语法错误。",
|
||||
model: "default",
|
||||
},
|
||||
{
|
||||
icon: Icons.AiTranslate,
|
||||
name: "AI 翻译",
|
||||
prompt: "请帮我做这段话的中英文互译。注意,你只需要返回翻译的结果,不需要对此进行任何解释,不需要除了翻译结果以外的其他任何内容。",
|
||||
model: "default",
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 定义默认的文生图命令
|
||||
*/
|
||||
const defaultImageCommands = [
|
||||
{
|
||||
icon: Icons.AiImage,
|
||||
name: "AI 生图",
|
||||
model: "default",
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 定义内部编辑器类
|
||||
*/
|
||||
@@ -258,11 +308,13 @@ export class UAIEditor {
|
||||
ai: {
|
||||
chat: {
|
||||
models: customOptions.ai?.chat?.models,
|
||||
commands: customOptions.ai?.chat?.commands ?? defaultChatCommands
|
||||
},
|
||||
image: {
|
||||
models: {
|
||||
text2image: customOptions.ai?.image?.models?.text2image,
|
||||
},
|
||||
commands: customOptions.ai?.image?.commands ?? defaultImageCommands
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ import Indent from "../extensions/Indent.ts";
|
||||
import LineHeight from "../extensions/LineHeight.ts"
|
||||
import NodeAlign from "../extensions/NodeAlign.ts";
|
||||
import OrderedList from "../extensions/OrderedList.ts";
|
||||
import QuickCommand from "../extensions/QuickCommand.ts";
|
||||
import SelectFile from "../extensions/SelectFile.ts";
|
||||
import Selection from "../extensions/Selection.ts";
|
||||
import Shortcuts from "../extensions/Shortcuts.ts";
|
||||
@@ -232,6 +233,7 @@ export const allExtensions = (uaiEditor: UAIEditor, _options: UAIEditorOptions):
|
||||
}),
|
||||
NodeAlign,
|
||||
OrderedList,
|
||||
QuickCommand,
|
||||
SelectFile.configure({
|
||||
allowedMimeTypes: []
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2024-present AI-Labs
|
||||
|
||||
// @ ts-nocheck
|
||||
import { Extension } from '@tiptap/core';
|
||||
|
||||
import Suggestion, { SuggestionOptions, SuggestionProps } from '@tiptap/suggestion';
|
||||
import tippy, { Instance } from 'tippy.js';
|
||||
import { InnerEditor } from '../core/UAIEditor.ts';
|
||||
import { AICommand } from '../ai/config/AIConfig.ts';
|
||||
import OpenAI from "openai";
|
||||
import { markdownToHtml } from '../utils/MarkdownUtil.ts';
|
||||
import { uuid } from '../utils/UUID.ts';
|
||||
|
||||
export type QuickCommandOptions = {
|
||||
HTMLAttributes?: Record<string, any>;
|
||||
suggestion: Omit<SuggestionOptions, 'editor'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义快捷命令
|
||||
*/
|
||||
export default Extension.create<QuickCommandOptions>({
|
||||
name: 'quickCommand',
|
||||
addOptions() {
|
||||
return {
|
||||
suggestion: {
|
||||
char: '/',
|
||||
command: ({ editor, range, props }) => {
|
||||
editor.chain().focus().deleteRange(range).run();
|
||||
const from = editor.state.selection.to;
|
||||
const type = props.type;
|
||||
const command = props.command as AICommand;
|
||||
const options = (editor as InnerEditor).uaiEditor.options;
|
||||
const actionKey = "uai_waiting_action";
|
||||
|
||||
const selectedText = editor.state.selection.$head.parent.textContent;
|
||||
if (type === "chat") {
|
||||
// 模型对话命令
|
||||
var models = Object.keys(options.ai?.chat?.models ?? {});
|
||||
if (models.length > 0) {
|
||||
var model = command.model ?? "auto";
|
||||
if (model === "auto") {
|
||||
model = models[0];
|
||||
}
|
||||
const modelConfig = options.ai!.chat!.models![model];
|
||||
const client = new OpenAI({
|
||||
baseURL: modelConfig.baseUrl,
|
||||
apiKey: modelConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
|
||||
client.chat.completions.create({
|
||||
model: modelConfig.model ?? "o1",
|
||||
stream: true,
|
||||
max_tokens: modelConfig.max_tokens,
|
||||
temperature: modelConfig.temperature,
|
||||
top_p: modelConfig.top_p,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
messages: [
|
||||
{ "role": "system", "content": command.prompt ?? "你是一个很有帮助的人工智能助手。" },
|
||||
{ "role": "user", "content": selectedText }
|
||||
],
|
||||
}).then(async response => {
|
||||
for await (var chunk of response) {
|
||||
var content = chunk.choices[0]?.delta?.content || '';
|
||||
editor.view.dispatch(editor.state.tr.insertText(content));
|
||||
}
|
||||
const end = editor.state.selection.to;
|
||||
const insertedText = editor.state.doc.textBetween(from, end);
|
||||
editor.view.dispatch(editor.state.tr.replaceWith(from, end, (editor as InnerEditor).parseHtml(markdownToHtml(insertedText))).scrollIntoView());
|
||||
});
|
||||
}
|
||||
} else if (type === "text2image") {
|
||||
// 文生图命令
|
||||
var models = Object.keys(options.ai?.image?.models?.text2image ?? {});
|
||||
const id = uuid()
|
||||
if (models.length > 0) {
|
||||
var model = command.model ?? "auto";
|
||||
if (model === "auto") {
|
||||
model = models[0];
|
||||
}
|
||||
const modelConfig = options.ai!.image!.models!.text2image![model];
|
||||
const client = new OpenAI({
|
||||
baseURL: modelConfig.baseUrl,
|
||||
apiKey: modelConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
editor.view.dispatch(editor.state.tr.setMeta(actionKey, {
|
||||
type: "add",
|
||||
id,
|
||||
pos: editor.state.tr.selection.from,
|
||||
}));
|
||||
client.images.generate({
|
||||
"model": modelConfig.model ?? "dall-e-3",
|
||||
"prompt": `${command.prompt ?? ""}${selectedText}`,
|
||||
"size": "1024x1024",
|
||||
}).then(response => {
|
||||
editor.view.dispatch(editor.state.tr.setMeta(actionKey, { type: "remove", id }));
|
||||
const previewType = "image";
|
||||
const type = "image";
|
||||
|
||||
editor.commands.insertContentAt(editor.state.tr.selection.from, {
|
||||
type: type,
|
||||
attrs: {
|
||||
['src']: response.data[0].url,
|
||||
type,
|
||||
previewType,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
render: () => {
|
||||
let container: HTMLElement;
|
||||
let popup: Instance;
|
||||
|
||||
return {
|
||||
onStart: (props: SuggestionProps) => {
|
||||
container = document.createElement("div");
|
||||
container.classList.add("uai-popup-action-list");
|
||||
(props.editor as InnerEditor).uaiEditor.options.ai?.chat?.commands?.forEach(command => {
|
||||
const item = document.createElement("div");
|
||||
item.classList.add("uai-popup-action-item");
|
||||
item.innerHTML = `${command.icon} ${command.name}`;
|
||||
item.addEventListener('click', () => {
|
||||
props.command({
|
||||
type: "chat",
|
||||
command: command
|
||||
})
|
||||
});
|
||||
container.appendChild(item);
|
||||
})
|
||||
|
||||
container.appendChild(document.createElement("hr"));
|
||||
|
||||
(props.editor as InnerEditor).uaiEditor.options.ai?.image?.commands?.forEach(command => {
|
||||
const item = document.createElement("div");
|
||||
item.classList.add("uai-popup-action-item");
|
||||
item.innerHTML = `${command.icon} ${command.name}`;
|
||||
item.addEventListener('click', () => {
|
||||
props.command({
|
||||
type: "text2image",
|
||||
command: command
|
||||
})
|
||||
});
|
||||
container.appendChild(item);
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
popup = tippy('body', {
|
||||
appendTo: props.editor.options.element,
|
||||
getReferenceClientRect: props.clientRect,
|
||||
content: container,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
allowHTML: true,
|
||||
trigger: 'manual',
|
||||
placement: 'right',
|
||||
arrow: false,
|
||||
})[0]
|
||||
},
|
||||
onUpdate(props) {
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
popup.setProps({
|
||||
getReferenceClientRect: props.clientRect as any,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup.hide();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onExit() {
|
||||
popup.hide();
|
||||
container.remove();
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
Suggestion({
|
||||
editor: this.editor,
|
||||
...this.options.suggestion,
|
||||
}),
|
||||
]
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user