feat: codeBlock add code comments and code explain features

This commit is contained in:
开源海哥 2023-12-05 16:28:29 +08:00
parent 56bf9e68ab
commit 1aa86333d5
9 changed files with 134 additions and 21 deletions

View File

@ -1,12 +1,15 @@
import {AiEditor} from "../../../src";
import {config} from "./xinghuo.ts"
const content = `
{"type":"doc","content":[{"type":"paragraph","attrs":{"lineHeight":"100%","textAlign":"left","indent":0},"content":[{"type":"text","text":"AiEditor 是一个面向 AI 的下一代富文本编辑器。"}]},{"type":"paragraph","attrs":{"lineHeight":"100%","textAlign":"left","indent":0},"content":[{"type":"text","marks":[{"type":"bold"}],"text":"提示:"},{"type":"text","text":" "}]},{"type":"orderedList","attrs":{"tight":true,"start":1},"content":[{"type":"listItem","attrs":{"indent":0},"content":[{"type":"paragraph","attrs":{"lineHeight":"100%","textAlign":"left","indent":0},"content":[{"type":"text","text":"输入 空格 + \\"/\\" 可以快速弹出 AI 菜单 "}]}]},{"type":"listItem","attrs":{"indent":0},"content":[{"type":"paragraph","attrs":{"lineHeight":"100%","textAlign":"left","indent":0},"content":[{"type":"text","text":"输入 空格 + \\"@\\" 可以提及某人"}]}]}]},{"type":"paragraph","attrs":{"lineHeight":"100%","textAlign":"left","indent":0}},{"type":"paragraph","attrs":{"lineHeight":"100%","textAlign":"left","indent":0},"content":[{"type":"text","text":"请使用 Java 帮我写一个 hello world只需要返回 java 代码内容"}]},{"type":"codeBlock","attrs":{"language":"java"},"content":[{"type":"text","text":"public class HelloWorld {\\n public static void main(String[] args) {\\n System.out.println(\\"Hello, World!\\");\\n }\\n}"}]},{"type":"paragraph","attrs":{"lineHeight":"100%","textAlign":"left","indent":0}}]}
`
// @ts-ignore
window.aiEditor = new AiEditor({
element: "#aiEditor",
placeholder: "点击输入内容...",
contentRetention: true,
content: 'AiEditor 是一个面向 AI 的下一代富文本编辑器。<p> <strong>提示:</strong> <br/>1、输入 空格 + "/" 可以快速弹出 AI 菜单 <br/> 2、输入 空格 + "@" 可以提及某人</p> ',
content: JSON.parse(content),
ai: {
model: {
xinghuo: {

View File

@ -2,6 +2,6 @@ import {Editor} from "@tiptap/core";
export interface AiModel {
start: (seletedText: string, prompt: string, editor: Editor) => void,
start: (seletedText: string, prompt: string, editor: Editor, getText?:boolean) => void,
}

View File

@ -26,9 +26,9 @@ export class XingHuoModel implements AiModel {
this.urlSignatureAlgorithm = urlSignatureAlgorithm!;
}
start(seletedText: string, prompt: string, editor: Editor): void {
start(seletedText: string, prompt: string, editor: Editor,getText:boolean = false): void {
const url = this.urlSignatureAlgorithm ? this.urlSignatureAlgorithm(this) : this.createUrl();
const socket = new XingHuoSocket(url, this.appId, this.version, editor);
const socket = new XingHuoSocket(url, this.appId, this.version, editor,getText);
socket.start(`"${seletedText}"\n${prompt}`)
}

View File

@ -2,6 +2,7 @@ import {AbstractWebSocket} from "../AbstractWebSocket.ts";
import {Editor} from "@tiptap/core";
import {uuid} from "../../util/uuid.ts";
import {InnerEditor} from "../../core/AiEditor.ts";
import {getText} from "../../util/getText.ts";
export class XingHuoSocket extends AbstractWebSocket {
@ -9,12 +10,14 @@ export class XingHuoSocket extends AbstractWebSocket {
version: string;
editor: Editor;
from: number;
getText: boolean = false;
constructor(url: string, appId: string, version: string, editor: Editor) {
constructor(url: string, appId: string, version: string, editor: Editor, getText = false) {
super(url);
this.appId = appId;
this.version = version;
this.editor = editor;
this.getText = getText;
this.from = editor.view.state.selection.from;
}
@ -72,9 +75,15 @@ export class XingHuoSocket extends AbstractWebSocket {
if (message.header.status == 2) {
const end = this.editor.state.selection.to;
const insertedText = this.editor.state.doc.textBetween(this.from, end);
const parseMarkdown = (this.editor as InnerEditor).parseMarkdown(insertedText);
const {state: {tr}, view} = this.editor!
view.dispatch(tr.replaceWith(this.from, end, parseMarkdown));
const parseMarkdown = (this.editor as InnerEditor).parseMarkdown(insertedText);
if (this.getText) {
const textString = getText(parseMarkdown);
const textNode = this.editor.schema.text(textString);
view.dispatch(tr.replaceWith(this.from, end, textNode));
} else {
view.dispatch(tr.replaceWith(this.from, end, parseMarkdown));
}
}
this.editor.commands.scrollIntoView();

View File

@ -98,6 +98,16 @@ export type AiEditorOptions = {
},
menus?: AiMenu[],
commands?: AiCommand[],
codeBlock?:{
codeComments?:{
model:string,
prompt:string,
},
codeExplain?:{
model:string,
prompt:string,
}
}
}
}
@ -206,6 +216,7 @@ export class AiEditor {
}
}
this.innerEditor = new InnerEditor(this.options, {
element: mainEl,
content: content,

View File

@ -86,10 +86,19 @@ export const getExtensions = (editor: AiEditor, options: AiEditorOptions): Exten
TaskItem.configure({
nested: true,
}),
CodeBlockExt.configure({
lowlight: createLowlight(common),
defaultLanguage: 'auto',
languageClassPrefix: 'language-',
codeExplainAi:options.ai?.codeBlock?.codeExplain || {
model:"xinghuo",
prompt:"帮我对这个代码进行解释,返回代码的解释内容,注意,不需要对代码的注释进行解释",
},
codeCommentsAi:options.ai?.codeBlock?.codeComments||{
model:"xinghuo",
prompt:"帮我对这个代码添加一些注释,并返回添加注释的代码,只返回代码",
},
}),
VideoExt.configure({
uploadUrl: options.video?.uploadUrl,

View File

@ -3,9 +3,10 @@ import tippy from "tippy.js";
import {isActive} from "../util/isActive.ts";
import {TextSelection} from "prosemirror-state";
import {textblockTypeInputRule} from "../util/textblockTypeInputRule.ts";
import {Selection} from '@tiptap/pm/state';
import {NodeSelection, Selection} from '@tiptap/pm/state';
import {Node} from '@tiptap/pm/model';
import {AiModelFactory} from "../ai/AiModelFactory.ts";
import {InnerEditor} from "../core/AiEditor.ts";
export type LanguageItem = {
name: string;
@ -141,13 +142,39 @@ export function getSelectedLineRange(
};
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
CodeBlockExt: {
/**
* add comments
*/
addCodeComments: (node: Node, pos: number) => ReturnType
/**
* add explain
*/
addCodeExplain: (node: Node, pos: number) => ReturnType
}
}
}
export const backtickInputRegex = /^[`·]{3}([a-z]+)?[\s\n]$/;
export const tildeInputRegex = /^[~]{3}([a-z]+)?[\s\n]$/;
export interface MyCodeBlockLowlightOptions extends CodeBlockLowlightOptions {
lowlight: any,
defaultLanguage: string | null | undefined,
languages: LanguageItem[]
languages: LanguageItem[],
codeCommentsAi?:null |{
model:string,
prompt:string,
},
codeExplainAi?:null |{
model:string,
prompt:string,
}
}
export const CodeBlockExt = CodeBlockLowlight.extend<MyCodeBlockLowlightOptions>({
@ -164,13 +191,47 @@ export const CodeBlockExt = CodeBlockLowlight.extend<MyCodeBlockLowlightOptions>
addCommands() {
return {
...this.parent?.(),
addCodeComments: (node, pos) => ({editor}) => {
const {storage, view: {dispatch}, state: {tr}} = editor;
dispatch(tr.setSelection(NodeSelection.create(editor.state.doc, pos + 1)).deleteSelection())
const markdown = storage.markdown.serializer.serialize(node);
const llm = AiModelFactory.create(this.options.codeCommentsAi!.model, (editor as InnerEditor).userOptions);
llm?.start(markdown, this.options.codeCommentsAi!.prompt, editor, true);
return true;
},
addCodeExplain: (node, pos) => ({editor}) => {
const {storage, view: {dispatch}, state: {tr}} = editor;
const nodeSize = editor.state.doc.nodeSize;
//there is no content after the node
if (nodeSize <= pos + node.nodeSize + 2){
editor.commands.insertContentAt(pos + node.nodeSize + 1, "<p></p>")
dispatch(tr.setSelection(TextSelection.create(editor.state.doc, nodeSize - 2)))
}else {
dispatch(tr.setSelection(TextSelection.create(editor.state.doc, pos + node.nodeSize + 1)))
}
const markdown = storage.markdown.serializer.serialize(node);
const llm = AiModelFactory.create(this.options.codeExplainAi!.model, (editor as InnerEditor).userOptions);
llm?.start(markdown, this.options.codeExplainAi!.prompt, editor);
return true;
},
toggleCodeBlock:
(attributes) =>
({commands, editor, chain}) => {
const {state} = editor;
const {from, to} = state.selection;
// 如果选中范围是连续段落,则合并后转成一个 codeBlock
// merge multi paragraph to codeBlock
if (!isActive(state, this.name) && !state.selection.empty) {
let isSelectConsecutiveParagraphs = true;
const textArr: string[] = [];
@ -180,7 +241,6 @@ export const CodeBlockExt = CodeBlockLowlight.extend<MyCodeBlockLowlightOptions>
}
if (node.type.name !== 'paragraph') {
if (pos + 1 <= from && pos + node.nodeSize - 1 >= to) {
// 不要返回 false, 否则会中断遍历子节点
return;
} else {
isSelectConsecutiveParagraphs = false;
@ -196,7 +256,6 @@ export const CodeBlockExt = CodeBlockLowlight.extend<MyCodeBlockLowlightOptions>
textArr.push(selectedText || '');
}
});
// 仅处理选择连续多个段落的情况
if (isSelectConsecutiveParagraphs && textArr.length > 1) {
return chain()
.command(({state, tr}) => {
@ -309,13 +368,15 @@ export const CodeBlockExt = CodeBlockLowlight.extend<MyCodeBlockLowlightOptions>
},
addNodeView() {
return (e) => {
return (props) => {
const container = document.createElement('div')
container.classList.add("aie-codeblock-wrapper")
const {language} = e.node.attrs;
const {language} = props.node.attrs;
container.innerHTML = `
<div class="aie-codeblock-tools" contenteditable="false">
${this.options.codeCommentsAi ? '<div class="aie-codeblock-tools-comments">自动注释</div>':''}
${this.options.codeExplainAi ? '<div class="aie-codeblock-tools-explain">代码解释</div>':''}
<div class="aie-codeblock-tools-lang" contenteditable="false"><span>${language || this.options.defaultLanguage}</span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 16L6 10H18L12 16Z"></path></svg></div>
</div>
<pre class="hljs"><code></code></pre>
@ -333,17 +394,17 @@ export const CodeBlockExt = CodeBlockLowlight.extend<MyCodeBlockLowlightOptions>
const target: HTMLDivElement = (event.target as HTMLElement).closest('.aie-codeblock-langs-item')!;
if (target) {
const language = target.getAttribute("data-item")!;
e.editor.chain().setCodeBlock({language:language}).run();
alert(language)
props.editor.chain().setCodeBlock({language: language}).run();
}
});
return div;
}
const instance = tippy(container.querySelector(".aie-codeblock-tools-lang")!, {
content: createEL(),
appendTo: e.editor.view.dom.closest(".aie-container")!,
appendTo: props.editor.options.element,
placement: 'bottom-end',
trigger: 'click',
interactive: true,
@ -354,6 +415,15 @@ export const CodeBlockExt = CodeBlockLowlight.extend<MyCodeBlockLowlightOptions>
},
});
container.querySelector(".aie-codeblock-tools-comments")
?.addEventListener("click", () => {
props.editor.chain().addCodeComments(props.node, (props.getPos as Function)());
});
container.querySelector(".aie-codeblock-tools-explain")
?.addEventListener("click", () => {
props.editor.chain().addCodeExplain(props.node, (props.getPos as Function)());
});
return {
dom: container,

View File

@ -373,18 +373,18 @@
.aie-codeblock-tools {
display: flex;
//float: right;
justify-content: flex-end;
div {
color: var(--aie-text-color);
display: flex;
cursor: pointer;
align-items: center;
font-size: 14px;
margin: 0 3px;
}
&-lang {
margin-left: auto;
svg {
fill: var(--aie-text-color);
margin: 2px;

11
src/util/getText.ts Normal file
View File

@ -0,0 +1,11 @@
import {Node,Fragment} from "prosemirror-model";
export const getText = (node:Node | Fragment) => {
let text = "";
node.descendants((node)=>{
if (node.text){
text += node.text;
}
})
return text;
}