feat: codeBlock add code comments and code explain features
This commit is contained in:
parent
56bf9e68ab
commit
1aa86333d5
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
|
||||
}
|
|
@ -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}`)
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue