diff --git a/src/components/Icons.ts b/src/components/Icons.ts
index 2af111c..3bdc54e 100644
--- a/src/components/Icons.ts
+++ b/src/components/Icons.ts
@@ -4,6 +4,12 @@
* 定义常用的图标
*/
export const Icons = {
+ AiAsk: ``,
+ AiContinue: ``,
+ AiRewrite: ``,
+ AiReview: ``,
+ AiTranslate: ``,
+ AiImage: ``,
ArrowDown: ``,
Loading: ``,
// Loading: ``,
diff --git a/src/core/UAIEditor.ts b/src/core/UAIEditor.ts
index bb57e8c..615237e 100644
--- a/src/core/UAIEditor.ts
+++ b/src/core/UAIEditor.ts
@@ -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,
+ commands?: AICommand[],
},
image?: {
models?: {
text2image?: Record,
},
+ 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
}
}
};
diff --git a/src/core/UAIExtensions.ts b/src/core/UAIExtensions.ts
index 1e16794..bf9bfc5 100644
--- a/src/core/UAIExtensions.ts
+++ b/src/core/UAIExtensions.ts
@@ -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: []
}),
diff --git a/src/extensions/QuickCommand.ts b/src/extensions/QuickCommand.ts
new file mode 100644
index 0000000..3657b11
--- /dev/null
+++ b/src/extensions/QuickCommand.ts
@@ -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;
+ suggestion: Omit;
+}
+
+/**
+ * 定义快捷命令
+ */
+export default Extension.create({
+ 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,
+ }),
+ ]
+ },
+})
\ No newline at end of file