添加大模型对话功能
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2024-present AI-Labs
|
||||
|
||||
/**
|
||||
* 模型对话的基本配置信息
|
||||
*/
|
||||
export interface AIChatConfig {
|
||||
modelType: "openai" | "custom" | "LMDeploy";
|
||||
|
||||
temperature?: number;
|
||||
max_tokens?: number;
|
||||
top_p?: number;
|
||||
top_k?: number;
|
||||
frequency_penalty?: number;
|
||||
|
||||
baseUrl?: string; // openai && gitee
|
||||
apiKey?: string; // openai && gitee && spark
|
||||
model?: string; // openai
|
||||
|
||||
appId?: string, // spark
|
||||
apiSecret?: string, // spark
|
||||
protocol?: string, // spark && wenxin
|
||||
version?: string, // spark && wenxin
|
||||
|
||||
access_token?: string, // wenxin
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷命令基本配置信息
|
||||
*/
|
||||
export interface AICommand {
|
||||
icon?: string,
|
||||
name: string,
|
||||
prompt?: string,
|
||||
text?: "selected" | "focusBefore",
|
||||
model?: string,
|
||||
action?: () => void,
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { EditorEvents } from "@tiptap/core";
|
||||
import { UAIEditorEventListener, UAIEditorOptions } from "../core/UAIEditor.ts";
|
||||
|
||||
import { ToggleToc } from "./menus/toolbar/page/ToggleToc";
|
||||
import { ToggleChat } from "./menus/toolbar/ai/ToggleChat.ts";
|
||||
import { CharacterCount } from "./menus/statusbar/CharacterCount.ts";
|
||||
|
||||
import { Fullscreen } from "./menus/statusbar/Fullscreen.ts";
|
||||
@@ -19,6 +20,8 @@ export class Footer extends HTMLElement implements UAIEditorEventListener {
|
||||
|
||||
// 文档大纲
|
||||
toggleToc!: ToggleToc;
|
||||
// 模型对话
|
||||
toggleChat!: ToggleChat;
|
||||
|
||||
// 字数统计
|
||||
characterCount!: CharacterCount;
|
||||
@@ -35,6 +38,7 @@ export class Footer extends HTMLElement implements UAIEditorEventListener {
|
||||
|
||||
// 创建状态栏菜单
|
||||
this.toggleToc = new ToggleToc({ menuType: "button", enable: true });
|
||||
this.toggleChat = new ToggleChat({ menuType: "button", enable: true });
|
||||
this.characterCount = new CharacterCount();
|
||||
|
||||
this.fullscreen = new Fullscreen({ menuType: "button", enable: true });
|
||||
@@ -50,6 +54,7 @@ export class Footer extends HTMLElement implements UAIEditorEventListener {
|
||||
onCreate(event: EditorEvents["create"], options: UAIEditorOptions) {
|
||||
// 初始化状态栏菜单
|
||||
this.toggleToc.onCreate(event, options);
|
||||
this.toggleChat.onCreate(event, options);
|
||||
this.characterCount.onCreate(event, options);
|
||||
|
||||
this.fullscreen.onCreate(event, options);
|
||||
@@ -76,12 +81,15 @@ export class Footer extends HTMLElement implements UAIEditorEventListener {
|
||||
statusBar.appendChild(statusBarRight);
|
||||
|
||||
this.toggleToc.classList.add("uai-status-bar-button");
|
||||
this.toggleChat.classList.add("uai-status-bar-button");
|
||||
|
||||
this.fullscreen.classList.add("uai-status-bar-button");
|
||||
|
||||
// 状态栏中添加功能菜单按钮
|
||||
statusBarLeft.appendChild(this.toggleToc);
|
||||
statusBarLeft.appendChild(this.createSplit());
|
||||
statusBarLeft.appendChild(this.toggleChat);
|
||||
statusBarLeft.appendChild(this.createSplit());
|
||||
statusBarLeft.appendChild(this.characterCount);
|
||||
|
||||
statusBarRight.appendChild(this.fullscreen);
|
||||
@@ -107,6 +115,7 @@ export class Footer extends HTMLElement implements UAIEditorEventListener {
|
||||
*/
|
||||
onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) {
|
||||
this.toggleToc.onTransaction(event, options);
|
||||
this.toggleChat.onTransaction(event, options);
|
||||
this.characterCount.onTransaction(event, options);
|
||||
|
||||
this.fullscreen.onTransaction(event, options);
|
||||
@@ -116,6 +125,7 @@ export class Footer extends HTMLElement implements UAIEditorEventListener {
|
||||
|
||||
onEditableChange(editable: boolean) {
|
||||
this.toggleToc.onEditableChange(editable);
|
||||
this.toggleChat.onEditableChange(editable);
|
||||
this.characterCount.onEditableChange(editable);
|
||||
|
||||
this.fullscreen.onEditableChange(editable);
|
||||
|
||||
@@ -7,4 +7,13 @@ export const Icons = {
|
||||
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>`,
|
||||
BotAvatar: '🤖',
|
||||
UserAvatar: '😀',
|
||||
SendIcon: `<svg width="24" height="24" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.0007 2L1.33398 6.71277L8.00065 8.00277L9.66905 14.6667L14.0007 2Z" fill="#464A53" stroke="#464A53" stroke-width="1.33" stroke-linejoin="round"/>
|
||||
<path d="M8.00195 8.00279L9.88755 6.11719" stroke="white" stroke-width="1.33" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>`,
|
||||
EditAction: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 48 48"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="4"><path stroke-linejoin="round" d="M29 4H9a2 2 0 0 0-2 2v36a2 2 0 0 0 2 2h28a2 2 0 0 0 2-2V20.005"/><path d="M13 18h8m-8 10h12"/><path stroke-linejoin="round" d="M41 6.001L29.003 17.996"/></g></svg>`,
|
||||
ReplaceAction: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M10.071 4.92902L11.4852 6.34323L6.82834 11.0001L16.0002 11.0002L16.0002 13.0002L6.82839 13.0001L11.4852 17.6569L10.071 19.0712L2.99994 12.0001L10.071 4.92902ZM18.0001 19V5.00003H20.0001V19H18.0001Z" fill="currentColor"></path></svg>`,
|
||||
InsertAction: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M2 18H12V20H2V18ZM2 11H22V13H2V11ZM2 4H22V6H2V4ZM18 18V15H20V18H23V20H20V23H18V20H15V18H18Z" fill="currentColor"></path></svg>`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) 2024-present AI-Labs
|
||||
|
||||
// @ ts-nocheck
|
||||
import { Editor, EditorEvents } from "@tiptap/core";
|
||||
import { Slice } from "@tiptap/pm/model";
|
||||
import iconTask from "../../assets/icons/task-list.svg";
|
||||
|
||||
import { Icons } from "../Icons";
|
||||
import { AICommand } from "../../ai/config/AIConfig";
|
||||
import { InnerEditor, UAIEditorEventListener, UAIEditorOptions } from "../../core/UAIEditor";
|
||||
import { markdownToHtml } from "../../utils/MarkdownUtil";
|
||||
import { t } from "i18next";
|
||||
import { MenuButton, MenuButtonOptions } from "../menus/MenuButton";
|
||||
import tippy, { Instance, Props } from "tippy.js";
|
||||
|
||||
/**
|
||||
* 抽象AI Action类
|
||||
*/
|
||||
export abstract class AbstractAIAction extends HTMLElement implements UAIEditorEventListener {
|
||||
editor!: Editor;
|
||||
aiActions: AICommand[] = [];
|
||||
// 按钮的选项设置信息
|
||||
menuButtonOptions: MenuButtonOptions = {
|
||||
menuType: "popup",
|
||||
enable: true,
|
||||
icon: iconTask,
|
||||
hideText: true,
|
||||
}
|
||||
|
||||
// 按钮
|
||||
menuButton!: MenuButton;
|
||||
// 提示
|
||||
tippyInstance!: Instance<Props>;
|
||||
|
||||
/**
|
||||
* 定义初始化方法,创建一个按钮
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.menuButton = new MenuButton(this.menuButtonOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义创建方法
|
||||
* @param event
|
||||
* @param options
|
||||
*/
|
||||
onCreate(event: EditorEvents["create"], options: UAIEditorOptions) {
|
||||
// 初始化变量信息
|
||||
this.editor = event.editor;
|
||||
this.menuButton.onCreate(event, options);
|
||||
// 界面上添加当前按钮
|
||||
this.appendChild(this.menuButton);
|
||||
|
||||
// 定义按钮的提示信息
|
||||
if (this.menuButtonOptions.enable && this.menuButton.menuButtonArrow) {
|
||||
this.tippyInstance = tippy(this.menuButton.menuButtonArrow, {
|
||||
content: this.createContainer(event, options),
|
||||
placement: 'bottom',
|
||||
trigger: 'click',
|
||||
interactive: true,
|
||||
arrow: false,
|
||||
onShow: () => {
|
||||
this.menuButton.tippyInstance?.disable();
|
||||
},
|
||||
onHidden: () => {
|
||||
this.menuButton.tippyInstance?.enable();
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) {
|
||||
}
|
||||
|
||||
onEditableChange(editable: boolean) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义容器的创建方法
|
||||
* @param event
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
createContainer(event: EditorEvents["create"], options: UAIEditorOptions) {
|
||||
// 容器DIV
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("uai-popup-action-list")
|
||||
|
||||
// 在DIV中添加所有自定义的Action
|
||||
this.aiActions?.forEach(action => {
|
||||
const item = document.createElement("div");
|
||||
item.classList.add("uai-popup-action-item")
|
||||
item.innerHTML = `${action.icon ?? ""} ${action.name}`;
|
||||
// 定义点击事件
|
||||
item.addEventListener('click', () => {
|
||||
// 调用Action定义个点击事件
|
||||
action.action?.call(this);
|
||||
this.tippyInstance.hide();
|
||||
});
|
||||
container.appendChild(item);
|
||||
})
|
||||
|
||||
return container
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义对话界面中用户请求内容的功能菜单
|
||||
*/
|
||||
export class AIRequestAction extends AbstractAIAction {
|
||||
sourceDivElement!: HTMLElement
|
||||
sourceInputElement!: HTMLElement
|
||||
|
||||
// 默认的用户请求功能菜单项
|
||||
defaultAIActions: AICommand[] = [
|
||||
{
|
||||
// 编辑用户发送的请求内容
|
||||
icon: Icons.EditAction,
|
||||
name: `编辑`,
|
||||
action: () => {
|
||||
(this.sourceInputElement as HTMLTextAreaElement).value = this.sourceDivElement.innerText
|
||||
this.sourceInputElement.style.height = 'auto'
|
||||
this.sourceInputElement.style.height = this.sourceInputElement.scrollHeight + 'px'
|
||||
}
|
||||
},
|
||||
{
|
||||
// 追加内容到编辑器的当前光标位置
|
||||
icon: Icons.InsertAction,
|
||||
name: `追加`,
|
||||
action: () => {
|
||||
const {
|
||||
state: { selection, tr },
|
||||
view: { dispatch }
|
||||
} = this.editor;
|
||||
const content = (this.editor as InnerEditor).parseHtml(markdownToHtml(this.sourceDivElement.getAttribute("InnerContent")!));
|
||||
dispatch(tr.replace(selection.to, selection.to, new Slice(content, 0, 0)).scrollIntoView())
|
||||
}
|
||||
},
|
||||
{
|
||||
// 替换编辑器中选中的内容
|
||||
icon: Icons.ReplaceAction,
|
||||
name: `替换`,
|
||||
action: () => {
|
||||
const {
|
||||
state: { tr },
|
||||
view: { dispatch }
|
||||
} = this.editor
|
||||
const content = (this.editor as InnerEditor).parseHtml(markdownToHtml(this.sourceDivElement.getAttribute("InnerContent")!));
|
||||
dispatch(tr.replaceSelection(new Slice(content, 0, 0)).scrollIntoView())
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.aiActions = this.defaultAIActions.map((menu) => {
|
||||
return {
|
||||
...menu,
|
||||
name: `${t(menu.name)}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
bindSourceElement(sourceDivElement: HTMLElement, sourceInputElement: HTMLElement) {
|
||||
this.sourceDivElement = sourceDivElement
|
||||
this.sourceInputElement = sourceInputElement
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义对话界面中响应数据的功能菜单
|
||||
*/
|
||||
export class AIChatResponseAction extends AbstractAIAction {
|
||||
sourceDivElement!: HTMLElement
|
||||
sourceInputElement!: HTMLElement
|
||||
|
||||
// 默认的菜单项
|
||||
defaultAIActions: AICommand[] = [
|
||||
{
|
||||
// 追加内容到编辑器的当前光标位置
|
||||
icon: Icons.InsertAction,
|
||||
name: `追加`,
|
||||
action: () => {
|
||||
const {
|
||||
state: { selection, tr },
|
||||
view: { dispatch }
|
||||
} = this.editor;
|
||||
const content = (this.editor as InnerEditor).parseHtml(markdownToHtml(this.sourceDivElement.getAttribute("InnerContent")!));
|
||||
dispatch(tr.replace(selection.to, selection.to, new Slice(content, 0, 0)).scrollIntoView())
|
||||
}
|
||||
},
|
||||
{
|
||||
// 替换编辑器中选中的内容
|
||||
icon: Icons.ReplaceAction,
|
||||
name: `替换`,
|
||||
action: () => {
|
||||
const {
|
||||
state: { tr },
|
||||
view: { dispatch }
|
||||
} = this.editor
|
||||
const content = (this.editor as InnerEditor).parseHtml(markdownToHtml(this.sourceDivElement.getAttribute("InnerContent")!));
|
||||
dispatch(tr.replaceSelection(new Slice(content, 0, 0)).scrollIntoView())
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.aiActions = this.defaultAIActions.map((menu) => {
|
||||
return {
|
||||
...menu,
|
||||
name: `${t(menu.name)}`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
bindSourceElement(sourceDivElement: HTMLElement, sourceInputElement: HTMLElement) {
|
||||
this.sourceDivElement = sourceDivElement
|
||||
this.sourceInputElement = sourceInputElement
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// Copyright (c) 2024-present AI-Labs
|
||||
|
||||
// @ ts-nocheck
|
||||
import closeIcon from "../../assets/icons/close.svg";
|
||||
import titleIcon from "../../assets/icons/chat-bot.svg";
|
||||
import sendIcon from "../../assets/icons/send-message.svg";
|
||||
import { t } from "i18next";
|
||||
import { UAIEditorEventListener, UAIEditorOptions } from "../../core/UAIEditor.ts";
|
||||
import { EditorEvents } from "@tiptap/core";
|
||||
import { createAvatar } from "../../utils/CreateAvatar.ts";
|
||||
import { Icons } from "../Icons.ts";
|
||||
import OpenAI from "openai";
|
||||
import { AIChatConfig } from "../../ai/config/AIConfig.ts";
|
||||
import { markdownToHtml } from "../../utils/MarkdownUtil.ts";
|
||||
import { AIRequestAction, AIChatResponseAction } from "../ai/AIAction.ts";
|
||||
|
||||
/**
|
||||
* 定义聊天对话界面的容器
|
||||
*/
|
||||
export class ChatContainer extends HTMLElement implements UAIEditorEventListener {
|
||||
// 容器元素
|
||||
container!: HTMLElement;
|
||||
|
||||
// 标题元素
|
||||
chatTitle!: HTMLElement;
|
||||
|
||||
// 内容元素
|
||||
content!: HTMLElement;
|
||||
|
||||
// 文本输入框
|
||||
messageTextarea!: HTMLTextAreaElement;
|
||||
imageButton!: HTMLButtonElement;
|
||||
modelSelect!: HTMLSelectElement;
|
||||
|
||||
client?: OpenAI;
|
||||
modelConfig?: AIChatConfig;
|
||||
|
||||
event!: EditorEvents["create"];
|
||||
options!: UAIEditorOptions;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// 初始化容器
|
||||
this.container = document.createElement("div");
|
||||
this.container.classList.add("uai-pannel-container");
|
||||
this.appendChild(this.container);
|
||||
|
||||
// 初始化标题
|
||||
this.chatTitle = document.createElement("div");
|
||||
this.chatTitle.classList.add("uai-pannel-title");
|
||||
this.chatTitle.innerHTML = `<img src="${titleIcon}" width="18" /> ${t('ai.chat.title')}`
|
||||
this.container.appendChild(this.chatTitle);
|
||||
|
||||
// 初始化关闭按钮
|
||||
const dialogClose = document.createElement("div");
|
||||
dialogClose.classList.add("uai-pannel-close");
|
||||
dialogClose.innerHTML = `<img src="${closeIcon}" width="24"/>`;
|
||||
// 点击关闭按钮时关闭容器
|
||||
dialogClose.addEventListener("click", () => {
|
||||
this.style.display = "none";
|
||||
})
|
||||
this.chatTitle.appendChild(dialogClose);
|
||||
|
||||
// 初始化对话模型选择
|
||||
const modelSelectDiv = document.createElement('div');
|
||||
modelSelectDiv.classList.add('uai-container-app-models');
|
||||
|
||||
const modelSelectText = document.createElement('div');
|
||||
modelSelectText.innerText = "模型:";
|
||||
modelSelectDiv.appendChild(modelSelectText);
|
||||
|
||||
this.modelSelect = document.createElement('select');
|
||||
this.modelSelect.classList.add('uai-model-select');
|
||||
modelSelectDiv.appendChild(this.modelSelect);
|
||||
this.container.appendChild(modelSelectDiv);
|
||||
|
||||
// 初始化对话内容容器
|
||||
this.content = document.createElement('div');
|
||||
this.content.classList.add('uai-ai-content');
|
||||
this.container.appendChild(this.content);
|
||||
|
||||
// 定义一个监听,当聊天内容有变动时,聊天内容容器滚动到最底部
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList' || mutation.type === 'characterData') {
|
||||
this.content.scrollTop = this.content.scrollHeight;
|
||||
}
|
||||
})
|
||||
})
|
||||
// 配置MutationObserver
|
||||
const config = { childList: true, characterData: true };
|
||||
|
||||
// 开始监听div的变化
|
||||
observer.observe(this.content, config);
|
||||
|
||||
// 定义对话输入框组件
|
||||
const divAppInput = document.createElement('div');
|
||||
divAppInput.classList.add('uai-container-app-input');
|
||||
|
||||
const divMessageContainer = document.createElement('div');
|
||||
divMessageContainer.classList.add('uai-message-container');
|
||||
|
||||
this.messageTextarea = document.createElement('textarea');
|
||||
this.messageTextarea.classList.add('uai-message-textarea');
|
||||
this.messageTextarea.rows = 1;
|
||||
this.messageTextarea.placeholder = '请输入内容... Shift+Enter换行;Enter发送';
|
||||
|
||||
// 定义键盘事件,Enter发送数据
|
||||
this.messageTextarea.addEventListener('keydown', (event) => {
|
||||
this.handleKeyDown(event);
|
||||
})
|
||||
// 定义输入事件,输入框自动根据输入内容调整大小
|
||||
this.messageTextarea.addEventListener('input', () => {
|
||||
this.messageTextarea.style.height = 'auto';
|
||||
this.messageTextarea.style.height = this.messageTextarea.scrollHeight + 'px';
|
||||
})
|
||||
divMessageContainer.appendChild(this.messageTextarea);
|
||||
|
||||
// 初始化发送按钮
|
||||
this.imageButton = document.createElement('button');
|
||||
this.imageButton.classList.add('uai-image-button');
|
||||
this.imageButton.innerHTML = `<img src="${sendIcon}" height="24" />`;
|
||||
this.imageButton.addEventListener('click', () => {
|
||||
this.sendMessage(this.messageTextarea.value)
|
||||
})
|
||||
divMessageContainer.appendChild(this.imageButton);
|
||||
|
||||
divAppInput.appendChild(divMessageContainer);
|
||||
this.container.appendChild(divAppInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义创建方法
|
||||
* @param event
|
||||
* @param options
|
||||
*/
|
||||
onCreate(event: EditorEvents["create"], options: UAIEditorOptions) {
|
||||
this.event = event;
|
||||
this.options = options;
|
||||
|
||||
// 初始化模型对话客户端
|
||||
if (options.ai && options.ai.chat?.models) {
|
||||
// 初始化所有可选对话模型
|
||||
for (let model of Object.keys(options.ai.chat?.models)) {
|
||||
const option = document.createElement('option');
|
||||
option.textContent = model;
|
||||
this.modelSelect.appendChild(option);
|
||||
}
|
||||
// 获取当前选中的模型
|
||||
const model = this.modelSelect.selectedOptions[0].value;
|
||||
// 获取选中的模型的配置信息
|
||||
this.modelConfig = options.ai!.chat!.models![model];
|
||||
// 根据模型配置信息初始化一个客户端
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.modelConfig.baseUrl,
|
||||
apiKey: this.modelConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
// 定义模型选择的改变事件,当重新选择了对话模型,则需要重新创建一个客户端
|
||||
this.modelSelect.addEventListener("change", () => {
|
||||
// 获取当前选中的模型
|
||||
const model = this.modelSelect.selectedOptions[0].value;
|
||||
// 获取选中的模型的配置信息
|
||||
this.modelConfig = options.ai!.chat!.models![model];
|
||||
// 根据模型配置信息初始化一个客户端
|
||||
this.client = new OpenAI({
|
||||
baseURL: this.modelConfig.baseUrl,
|
||||
apiKey: this.modelConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) {
|
||||
}
|
||||
|
||||
onEditableChange(editable: boolean) {
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
// 检查是否按下了 Shift 和 Enter 键
|
||||
if (event.shiftKey && event.key === 'Enter') {
|
||||
// 如果是 Shift + Enter,允许默认行为(通常是换行)
|
||||
return;
|
||||
}
|
||||
// 检查是否按下了 Enter 键
|
||||
if (event.key === 'Enter') {
|
||||
// 阻止默认的换行行为
|
||||
event.preventDefault();
|
||||
// 处理发送消息的逻辑
|
||||
this.sendMessage(this.messageTextarea.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义发送消息的方法
|
||||
* @param message
|
||||
*/
|
||||
sendMessage(message: string) {
|
||||
if (message.trim() !== '') {
|
||||
// 创建用户消息容器
|
||||
const userMessageDiv = document.createElement('div');
|
||||
userMessageDiv.classList.add('message');
|
||||
userMessageDiv.classList.add('message-sent');
|
||||
|
||||
// 创建用户发送消息容器
|
||||
const userBubbleDiv = document.createElement('div');
|
||||
userBubbleDiv.classList.add('markdown-body');
|
||||
userBubbleDiv.classList.add('bubble');
|
||||
userBubbleDiv.classList.add('bubble-sent');
|
||||
userBubbleDiv.innerHTML = markdownToHtml(message);
|
||||
userBubbleDiv.setAttribute("InnerContent", message);
|
||||
|
||||
// 创建用户输入的功能按钮
|
||||
const userActionDown = document.createElement(
|
||||
'uai-editor-ai-action-request-action'
|
||||
) as AIRequestAction;
|
||||
userActionDown.classList.add('hidden-content');
|
||||
userActionDown.onCreate(this.event, this.options);
|
||||
userActionDown.bindSourceElement(userBubbleDiv, this.messageTextarea);
|
||||
|
||||
// 创建用户头像容器
|
||||
const userAvatarDiv = createAvatar(Icons.UserAvatar);
|
||||
userAvatarDiv.classList.add('user-avatar');
|
||||
|
||||
// 用户消息容器中,添加功能按钮
|
||||
userMessageDiv.appendChild(userActionDown);
|
||||
// 用户消息容器中,添加用户发送内容
|
||||
userMessageDiv.appendChild(userBubbleDiv);
|
||||
// 用户消息容器中,添加用户头像
|
||||
userMessageDiv.appendChild(userAvatarDiv);
|
||||
|
||||
// 展示用户发送的消息
|
||||
this.content.appendChild(userMessageDiv);
|
||||
|
||||
// 清空消息输入框
|
||||
this.messageTextarea.value = '';
|
||||
this.messageTextarea.style.height = 'auto';
|
||||
this.messageTextarea.style.height = this.messageTextarea.scrollHeight + 'px';
|
||||
this.messageTextarea.focus();
|
||||
|
||||
// 发送消息并等待响应
|
||||
this.receiveResponse(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义接收模型响应的方法
|
||||
* @param message
|
||||
*/
|
||||
receiveResponse(message: string) {
|
||||
// 创建模型响应消息容器
|
||||
const botResponseDiv = document.createElement('div');
|
||||
botResponseDiv.classList.add('message');
|
||||
botResponseDiv.classList.add('message-received');
|
||||
|
||||
// 定义模型头像容器
|
||||
const botAvatarDiv = createAvatar(Icons.BotAvatar);
|
||||
botAvatarDiv.classList.add('bot-avatar');
|
||||
|
||||
// 定义模型输出内容容器
|
||||
const botBubbleDiv = document.createElement('div');
|
||||
botBubbleDiv.classList.add('markdown-body');
|
||||
botBubbleDiv.classList.add('bubble');
|
||||
botBubbleDiv.classList.add('bubble-received');
|
||||
botBubbleDiv.innerHTML = Icons.Loading;
|
||||
|
||||
// 定义模型输出的功能按钮
|
||||
const botActionDown = document.createElement(
|
||||
'uai-editor-ai-action-chat-response-action'
|
||||
) as AIChatResponseAction;
|
||||
botActionDown.classList.add('hidden-content');
|
||||
botActionDown.onCreate(this.event, this.options);
|
||||
botActionDown.bindSourceElement(botBubbleDiv, this.messageTextarea);
|
||||
|
||||
// 在模型响应消息容器中,添加头像
|
||||
botResponseDiv.appendChild(botAvatarDiv);
|
||||
// 在模型响应消息容器中,添加模型响应消息内容
|
||||
botResponseDiv.appendChild(botBubbleDiv);
|
||||
// 在模型响应消息容器中,添加功能按钮
|
||||
botResponseDiv.appendChild(botActionDown);
|
||||
|
||||
this.content.appendChild(botResponseDiv);
|
||||
|
||||
// 发起对话请求,并接收模型响应内容
|
||||
if (this.client && this.modelConfig) {
|
||||
const textQueue: string[] = [];
|
||||
const stream = this.modelConfig.modelType === "LMDeploy" ? false : true;
|
||||
// 通过客户端发起请求
|
||||
this.client.chat.completions.create({
|
||||
model: this.modelConfig.model ?? "o1",
|
||||
stream: stream,
|
||||
max_tokens: this.modelConfig.max_tokens,
|
||||
temperature: this.modelConfig.temperature,
|
||||
top_p: this.modelConfig.top_p,
|
||||
frequency_penalty: this.modelConfig.frequency_penalty,
|
||||
messages: [
|
||||
{
|
||||
"role": "user",
|
||||
"content": message
|
||||
}
|
||||
],
|
||||
}).then(async response => {
|
||||
// 接收模型响应数据
|
||||
if (stream) {
|
||||
// 流式响应
|
||||
for await (var chunk of response) {
|
||||
textQueue.push(chunk.choices[0]?.delta?.content || '');
|
||||
botBubbleDiv.innerHTML = markdownToHtml(textQueue.join(''));
|
||||
this.content.scrollTop = this.content.scrollHeight;
|
||||
}
|
||||
} else {
|
||||
// 非流式响应
|
||||
textQueue.push(response.choices[0]?.message?.content || '');
|
||||
botBubbleDiv.innerHTML = markdownToHtml(textQueue.join(''));
|
||||
this.content.scrollTop = this.content.scrollHeight;
|
||||
}
|
||||
botBubbleDiv.setAttribute("InnerContent", textQueue.join(''));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Header } from "../components/Header.ts";
|
||||
import { Editor } from "../components/Editor.ts";
|
||||
import { Footer } from "../components/Footer.ts";
|
||||
|
||||
import { ChatContainer } from "../components/containers/ChatContainer.ts";
|
||||
import { TocContainer } from "../components/containers/TocContainer.ts";
|
||||
|
||||
import { Ribbon } from "./menus/toolbar/Ribbon.ts";
|
||||
@@ -86,6 +87,8 @@ import { ExportPdf } from "./menus/toolbar/export/ExportPdf.ts";
|
||||
import { ExportMarkdown } from "./menus/toolbar/export/ExportMarkdown.ts";
|
||||
import { ExportImage } from "./menus/toolbar/export/ExportImage.ts";
|
||||
|
||||
import { ToggleChat } from "./menus/toolbar/ai/ToggleChat.ts";
|
||||
|
||||
import { TextSelectionBubbleMenu } from "./menus/bubble/TextSelectionBubbleMenu.ts";
|
||||
|
||||
import { ImageBubbleMenu } from "./menus/bubble/ImageBubbleMenu.ts";
|
||||
@@ -102,11 +105,14 @@ import { Fullscreen } from "./menus/statusbar/Fullscreen.ts";
|
||||
import { Feedback } from "./menus/statusbar/Feedback.ts";
|
||||
import { PoweredBy } from "./menus/statusbar/PoweredBy.ts";
|
||||
|
||||
import { AIRequestAction, AIChatResponseAction } from "./ai/AIAction.ts";
|
||||
|
||||
// 注册组件
|
||||
defineCustomElement('uai-editor-header', Header);
|
||||
defineCustomElement('uai-editor-editor', Editor);
|
||||
defineCustomElement('uai-editor-footer', Footer);
|
||||
|
||||
defineCustomElement('uai-editor-chat-container', ChatContainer);
|
||||
defineCustomElement('uai-editor-toc-container', TocContainer);
|
||||
|
||||
defineCustomElement('uai-editor-ribbon-menu', Ribbon);
|
||||
@@ -188,6 +194,8 @@ defineCustomElement('uai-editor-export-menu-pdf', ExportPdf);
|
||||
defineCustomElement('uai-editor-export-menu-markdown', ExportMarkdown);
|
||||
defineCustomElement('uai-editor-export-menu-image', ExportImage);
|
||||
|
||||
defineCustomElement('uai-editor-ai-menu-toggle-chat', ToggleChat);
|
||||
|
||||
defineCustomElement('uai-editor-statusbar-menu-character-count', CharacterCount);
|
||||
|
||||
defineCustomElement('uai-editor-statusbar-menu-fullscreen', Fullscreen);
|
||||
@@ -203,3 +211,6 @@ defineCustomElement('uai-editor-bubble-menu-image-flip-y', ImageFlipY);
|
||||
defineCustomElement('uai-editor-bubble-menu-video', VideoBubbleMenu);
|
||||
|
||||
defineCustomElement('uai-editor-bubble-menu-audio', AudioBubbleMenu);
|
||||
|
||||
defineCustomElement('uai-editor-ai-action-request-action', AIRequestAction);
|
||||
defineCustomElement('uai-editor-ai-action-chat-response-action', AIChatResponseAction);
|
||||
|
||||
@@ -77,6 +77,8 @@ import { ExportPdf } from "./export/ExportPdf.ts";
|
||||
import { ExportMarkdown } from "./export/ExportMarkdown.ts";
|
||||
import { ExportImage } from "./export/ExportImage.ts";
|
||||
|
||||
import { ToggleChat } from "./ai/ToggleChat.ts";
|
||||
|
||||
/**
|
||||
* 传统菜单栏
|
||||
*/
|
||||
@@ -112,6 +114,10 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
||||
classicMenuExportScrollable!: ScrollableDiv;
|
||||
classicMenuExportGroup!: HTMLElement;
|
||||
|
||||
// 人工智能菜单容器
|
||||
classicMenuAIScrollable!: ScrollableDiv;
|
||||
classicMenuAIGroup!: HTMLElement;
|
||||
|
||||
// 基础菜单
|
||||
baseMenuUndo!: Undo;
|
||||
baseMenuRedo!: Redo;
|
||||
@@ -184,6 +190,9 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
||||
exportMenuExportMarkdown!: ExportMarkdown;
|
||||
exportMenuExportImage!: ExportImage;
|
||||
|
||||
// 人工智能菜单
|
||||
aiMenuToggleChat!: ToggleChat;
|
||||
|
||||
constructor(defaultToolbarMenus: Record<string, any>[]) {
|
||||
super();
|
||||
this.defaultToolbarMenus = defaultToolbarMenus;
|
||||
@@ -233,6 +242,7 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
||||
this.classicMenuToolScrollable.style.display = "none";
|
||||
this.classicMenuPageScrollable.style.display = "none";
|
||||
this.classicMenuExportScrollable.style.display = "none";
|
||||
this.classicMenuAIScrollable.style.display = "none";
|
||||
if (menu === "base") {
|
||||
this.classicMenuBaseScrollable.style.display = "flex";
|
||||
}
|
||||
@@ -251,6 +261,9 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
||||
if (menu === "export") {
|
||||
this.classicMenuExportScrollable.style.display = "flex";
|
||||
}
|
||||
if (menu === "ai") {
|
||||
this.classicMenuAIScrollable.style.display = "flex";
|
||||
}
|
||||
})
|
||||
// 创建分组菜单
|
||||
this.createBaseMenu(event, options);
|
||||
@@ -259,6 +272,7 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
||||
this.createToolMenu(event, options);
|
||||
this.createPageMenu(event, options);
|
||||
this.createExportMenu(event, options);
|
||||
this.createAIMenu(event, options);
|
||||
this.classicMenuBaseScrollable.style.display = "flex";
|
||||
}
|
||||
|
||||
@@ -450,6 +464,9 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
||||
|
||||
this.exportMenuExportImage = new ExportImage({ menuType: "button", enable: true, header: "classic", hideText: false });
|
||||
this.eventComponents.push(this.exportMenuExportImage);
|
||||
|
||||
this.aiMenuToggleChat = new ToggleChat({ menuType: "button", enable: true, header: "classic", hideText: false });
|
||||
this.eventComponents.push(this.aiMenuToggleChat);
|
||||
}
|
||||
/**
|
||||
* 创建基础菜单
|
||||
@@ -650,4 +667,23 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
||||
this.classicMenuExportGroup.appendChild(group2);
|
||||
group2.appendChild(this.exportMenuExportImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建人工智能菜单
|
||||
* @param event
|
||||
* @param options
|
||||
*/
|
||||
createAIMenu(event: EditorEvents["create"], options: UAIEditorOptions) {
|
||||
this.classicMenuAIGroup = document.createElement("div");
|
||||
this.classicMenuAIGroup.style.display = "flex";
|
||||
this.classicMenuAIScrollable = new ScrollableDiv(this.classicMenuAIGroup);
|
||||
this.classicMenuAIScrollable.style.display = "none";
|
||||
this.classicMenu.appendChild(this.classicMenuAIScrollable);
|
||||
this.classicMenuAIScrollable.onCreate(event, options);
|
||||
|
||||
const group1 = document.createElement("div");
|
||||
group1.classList.add("uai-classic-virtual-group");
|
||||
this.classicMenuAIGroup.appendChild(group1);
|
||||
group1.appendChild(this.aiMenuToggleChat);
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,8 @@ import { ExportPdf } from "./export/ExportPdf.ts";
|
||||
import { ExportMarkdown } from "./export/ExportMarkdown.ts";
|
||||
import { ExportImage } from "./export/ExportImage.ts";
|
||||
|
||||
import { ToggleChat } from "./ai/ToggleChat.ts";
|
||||
|
||||
/**
|
||||
* 经典菜单栏
|
||||
*/
|
||||
@@ -115,6 +117,10 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
||||
ribbonMenuExportScrollable!: ScrollableDiv;
|
||||
ribbonMenuExportGroup!: HTMLElement;
|
||||
|
||||
// 人工智能菜单容器
|
||||
ribbonMenuAIScrollable!: ScrollableDiv;
|
||||
ribbonMenuAIGroup!: HTMLElement;
|
||||
|
||||
// 基础菜单
|
||||
baseMenuUndo!: Undo;
|
||||
baseMenuRedo!: Redo;
|
||||
@@ -187,6 +193,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
||||
exportMenuExportMarkdown!: ExportMarkdown;
|
||||
exportMenuExportImage!: ExportImage;
|
||||
|
||||
// 人工智能菜单
|
||||
aiMenuToggleChat!: ToggleChat;
|
||||
|
||||
constructor(defaultToolbarMenus: Record<string, any>[]) {
|
||||
super();
|
||||
this.defaultToolbarMenus = defaultToolbarMenus;
|
||||
@@ -229,6 +238,7 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
||||
this.ribbonMenuToolScrollable.style.display = "none";
|
||||
this.ribbonMenuPageScrollable.style.display = "none";
|
||||
this.ribbonMenuExportScrollable.style.display = "none";
|
||||
this.ribbonMenuAIScrollable.style.display = "none";
|
||||
if (menu.value === "base") {
|
||||
this.ribbonMenuBaseScrollable.style.display = "flex";
|
||||
}
|
||||
@@ -247,6 +257,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
||||
if (menu.value === "export") {
|
||||
this.ribbonMenuExportScrollable.style.display = "flex";
|
||||
}
|
||||
if (menu.value === "ai") {
|
||||
this.ribbonMenuAIScrollable.style.display = "flex";
|
||||
}
|
||||
})
|
||||
ribbonTabs.appendChild(tab);
|
||||
});
|
||||
@@ -270,6 +283,7 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
||||
this.createToolMenu(event, options);
|
||||
this.createPageMenu(event, options);
|
||||
this.createExportMenu(event, options);
|
||||
this.createAIMenu(event, options);
|
||||
this.ribbonMenuBaseScrollable.style.display = "flex";
|
||||
}
|
||||
|
||||
@@ -473,6 +487,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
||||
|
||||
this.exportMenuExportImage = new ExportImage({ menuType: "button", enable: true, huge: true, hideText: false });
|
||||
this.eventComponents.push(this.exportMenuExportImage);
|
||||
|
||||
this.aiMenuToggleChat = new ToggleChat({ menuType: "button", enable: true, huge: true, hideText: false });
|
||||
this.eventComponents.push(this.aiMenuToggleChat);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -755,4 +772,23 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
||||
this.ribbonMenuExportGroup.appendChild(group2);
|
||||
group2.appendChild(this.exportMenuExportImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建人工智能菜单
|
||||
* @param event
|
||||
* @param options
|
||||
*/
|
||||
createAIMenu(_event: EditorEvents["create"], _options: UAIEditorOptions) {
|
||||
this.ribbonMenuAIGroup = document.createElement("div");
|
||||
this.ribbonMenuAIGroup.classList.add("uai-ribbon-container");
|
||||
this.ribbonMenuAIGroup.style.display = "flex";
|
||||
this.ribbonMenuAIScrollable = new ScrollableDiv(this.ribbonMenuAIGroup);
|
||||
this.ribbonMenuAIScrollable.style.display = "none";
|
||||
this.ribbonScrollableContainer.appendChild(this.ribbonMenuAIScrollable);
|
||||
|
||||
const group1 = document.createElement("div");
|
||||
group1.classList.add("uai-ribbon-virtual-group");
|
||||
this.ribbonMenuAIGroup.appendChild(group1);
|
||||
group1.appendChild(this.aiMenuToggleChat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2024-present AI-Labs
|
||||
|
||||
// @ ts-nocheck
|
||||
import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts";
|
||||
import icon from "../../../../assets/icons/chat-bot.svg";
|
||||
import { t } from "i18next";
|
||||
import { UAIEditorEventListener, UAIEditorOptions, InnerEditor } from "../../../../core/UAIEditor.ts";
|
||||
import { EditorEvents } from "@tiptap/core";
|
||||
|
||||
/**
|
||||
* 人工智能菜单:聊天
|
||||
*/
|
||||
export class ToggleChat extends HTMLElement implements UAIEditorEventListener {
|
||||
// 按钮选项
|
||||
menuButtonOptions: MenuButtonOptions = {
|
||||
menuType: "button",
|
||||
enable: true,
|
||||
icon: icon,
|
||||
hideText: false,
|
||||
text: t('ai.chat.title'),
|
||||
tooltip: t('ai.chat.title'),
|
||||
}
|
||||
|
||||
// 功能按钮
|
||||
menuButton: MenuButton;
|
||||
|
||||
constructor(options: MenuButtonOptions) {
|
||||
super();
|
||||
|
||||
// 初始化功能按钮选项
|
||||
this.menuButtonOptions = { ...this.menuButtonOptions, ...options };
|
||||
|
||||
// 创建功能按钮
|
||||
this.menuButton = new MenuButton(this.menuButtonOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义创建方法
|
||||
* @param event
|
||||
* @param options
|
||||
*/
|
||||
onCreate(event: EditorEvents["create"], options: UAIEditorOptions) {
|
||||
this.menuButton.onCreate(event, options);
|
||||
this.appendChild(this.menuButton);
|
||||
|
||||
// 定义按钮点击事件,打开人工智能聊天窗口
|
||||
this.addEventListener("click", () => {
|
||||
if (this.menuButtonOptions.enable) {
|
||||
// 判断聊天窗口是否已打开
|
||||
const display = (event.editor as InnerEditor).uaiEditor.chatContainer.style.display;
|
||||
if (display != "none") {
|
||||
// 如果窗口已打开,则关闭窗口
|
||||
(event.editor as InnerEditor).uaiEditor.chatContainer.style.display = "none";
|
||||
} else {
|
||||
// 如果窗口没打开,则先关闭其他窗口,再打开聊天窗口
|
||||
(event.editor as InnerEditor).uaiEditor.toggleContainers.forEach(toggle => {
|
||||
toggle.style.display = "none";
|
||||
});
|
||||
(event.editor as InnerEditor).uaiEditor.chatContainer.style.display = "block";
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义Transaction监听方法
|
||||
* @param event
|
||||
* @param options
|
||||
*/
|
||||
onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) {
|
||||
this.menuButton.onTransaction(event, options);
|
||||
}
|
||||
|
||||
onEditableChange(editable: boolean) {
|
||||
this.menuButtonOptions.enable = editable;
|
||||
this.menuButton.onEditableChange(editable);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-1
@@ -12,6 +12,7 @@ import { TableOfContentData } from "@tiptap-pro/extension-table-of-contents";
|
||||
|
||||
import "../components"
|
||||
import { Header } from "../components/Header.ts";
|
||||
import { ChatContainer } from "../components/containers/ChatContainer.ts";
|
||||
import { TocContainer } from "../components/containers/TocContainer.ts";
|
||||
import { Editor } from "../components/Editor.ts";
|
||||
import { Footer } from "../components/Footer.ts";
|
||||
@@ -24,6 +25,7 @@ import i18next from "i18next";
|
||||
import { zh } from "../i18n/zh.ts";
|
||||
import { Resource } from "i18next";
|
||||
import { allExtensions } from "./UAIExtensions.ts";
|
||||
import { AIChatConfig } from "../ai/config/AIConfig.ts";
|
||||
import { markdownToHtml } from "../utils/MarkdownUtil.ts";
|
||||
import { Uploader } from "../utils/FileUploader.ts";
|
||||
|
||||
@@ -105,6 +107,11 @@ export type UAIEditorOptions = {
|
||||
uploadFormName?: string,
|
||||
uploader?: Uploader,
|
||||
},
|
||||
ai?: {
|
||||
chat?: {
|
||||
models?: Record<string, AIChatConfig>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +125,15 @@ export class InnerEditor extends TipTap {
|
||||
super(options);
|
||||
this.uaiEditor = uaiEditor;
|
||||
}
|
||||
|
||||
parseHtml(html: string) {
|
||||
function bodyElement(value: string): HTMLElement {
|
||||
return new window.DOMParser().parseFromString(`<body>${value}</body>`, 'text/html').body;
|
||||
}
|
||||
|
||||
const parser = DOMParser.fromSchema(this.schema);
|
||||
return parser.parse(bodyElement(html), {}).content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,6 +149,7 @@ export class UAIEditor {
|
||||
toggleContainers: HTMLElement[] = [];
|
||||
|
||||
header!: Header;
|
||||
chatContainer!: ChatContainer;
|
||||
tocContainer!: TocContainer;
|
||||
editor!: Editor;
|
||||
footer!: Footer;
|
||||
@@ -231,6 +248,11 @@ export class UAIEditor {
|
||||
{ label: '旗帜', value: '🏁 🎌 🏴', },
|
||||
],
|
||||
},
|
||||
ai: {
|
||||
chat: {
|
||||
models: customOptions.ai?.chat?.models,
|
||||
},
|
||||
}
|
||||
};
|
||||
const i18nConfig = this.options.i18n || {};
|
||||
const resources = {
|
||||
@@ -268,6 +290,11 @@ export class UAIEditor {
|
||||
this.center.appendChild(this.tocContainer);
|
||||
this.toggleContainers.push(this.tocContainer);
|
||||
|
||||
this.chatContainer = new ChatContainer();
|
||||
this.chatContainer.style.display = "none";
|
||||
this.center.appendChild(this.chatContainer);
|
||||
this.toggleContainers.push(this.chatContainer);
|
||||
|
||||
this.editor = new Editor();
|
||||
this.editor.classList.add("uai-main");
|
||||
this.center.appendChild(this.editor);
|
||||
@@ -330,6 +357,7 @@ export class UAIEditor {
|
||||
}
|
||||
this.header.onCreate(event, this.options);
|
||||
this.tocContainer.onCreate(event, this.options);
|
||||
this.chatContainer.onCreate(event, this.options);
|
||||
this.editor.onCreate(event, this.options);
|
||||
this.footer.onCreate(event, this.options);
|
||||
this.eventComponents.forEach(component => {
|
||||
@@ -339,9 +367,10 @@ export class UAIEditor {
|
||||
|
||||
protected onTransaction(event: EditorEvents['transaction']) {
|
||||
this.header.onTransaction(event, this.options);
|
||||
this.tocContainer.onTransaction(event, this.options);
|
||||
this.chatContainer.onTransaction(event, this.options);
|
||||
this.editor.onTransaction(event, this.options);
|
||||
this.footer.onTransaction(event, this.options);
|
||||
this.tocContainer.onTransaction(event, this.options);
|
||||
this.eventComponents.forEach(component => {
|
||||
component.onTransaction(event, this.options);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2024-present AI-Labs
|
||||
|
||||
/**
|
||||
* 创建头像
|
||||
* @param icon
|
||||
* @returns
|
||||
*/
|
||||
export function createAvatar(icon: string) {
|
||||
const avatarDiv = document.createElement('div')
|
||||
avatarDiv.style.fontSize = '30px'
|
||||
avatarDiv.style.marginLeft = '5px'
|
||||
avatarDiv.innerHTML = icon
|
||||
return avatarDiv
|
||||
}
|
||||
Reference in New Issue
Block a user