From 4e20d93f98004eb8ad7d5243c9fed2241d242167 Mon Sep 17 00:00:00 2001 From: joefalmko Date: Tue, 3 Dec 2024 00:57:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=87=AA=E5=8A=A8=E6=8E=92?= =?UTF-8?q?=E7=89=88=E5=8A=9F=E8=83=BD=EF=BC=88=E6=9C=AA=E5=AE=8C=E6=88=90?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GinSkeleton/app/global/consts/consts.go | 2 + .../controller/web/ai_layout_controller.go | 14 ++ .../web/ai_layout/layout_generate.go | 21 ++- .../app/service/ai_model_cli/layout_cli.go | 165 ++++++++++-------- GinSkeleton/config/config.yml | 1 + .../storage/app/prompt/layout_generate.prompt | 4 + coeditor_frontend/src/components/plugins.js | 61 ++++++- coeditor_frontend/src/components/utils.js | 16 +- coeditor_frontend/src/views/CkeditorView.vue | 12 +- 9 files changed, 203 insertions(+), 93 deletions(-) create mode 100644 GinSkeleton/storage/app/prompt/layout_generate.prompt diff --git a/GinSkeleton/app/global/consts/consts.go b/GinSkeleton/app/global/consts/consts.go index fad639d..c8e36c8 100644 --- a/GinSkeleton/app/global/consts/consts.go +++ b/GinSkeleton/app/global/consts/consts.go @@ -87,4 +87,6 @@ const ( DocRefineFailCode int = -400452 StyleGenerateFailMsg string = "样式生成失败" StyleGenerateFailCode int = -400453 + LayoutGenerateFailMsg string = "排版生成失败" + LayoutGenerateFailCode int = -400454 ) diff --git a/GinSkeleton/app/http/controller/web/ai_layout_controller.go b/GinSkeleton/app/http/controller/web/ai_layout_controller.go index 64c945a..ab3baa1 100644 --- a/GinSkeleton/app/http/controller/web/ai_layout_controller.go +++ b/GinSkeleton/app/http/controller/web/ai_layout_controller.go @@ -9,6 +9,8 @@ import ( type StyleGenerate struct { } +type LayoutGenerate struct { +} // ai生成样式 func (s *StyleGenerate) StyleGenerate(c *gin.Context) { @@ -29,3 +31,15 @@ func (s *StyleGenerate) StyleGenerate(c *gin.Context) { } } + +// ai排版 +func (l *LayoutGenerate) LayoutGenerate(c *gin.Context) { + // 流式传输 + // 设置 HTTP 头部为 SSE + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + if err := ai_model_cli.RequestLayout(c); err != nil { + response.Fail(c, consts.LayoutGenerateFailCode, consts.LayoutGenerateFailMsg, err) + } +} diff --git a/GinSkeleton/app/http/validator/web/ai_layout/layout_generate.go b/GinSkeleton/app/http/validator/web/ai_layout/layout_generate.go index 60df902..ab21c95 100644 --- a/GinSkeleton/app/http/validator/web/ai_layout/layout_generate.go +++ b/GinSkeleton/app/http/validator/web/ai_layout/layout_generate.go @@ -1,12 +1,23 @@ package ai_layout import( "github.com/gin-gonic/gin" -// "goskeleton/app/utils/response" -// "goskeleton/app/http/controller/web" -// "goskeleton/app/http/validator/core/data_transfer" -// "goskeleton/app/global/consts" + "goskeleton/app/utils/response" + "goskeleton/app/http/controller/web" + "goskeleton/app/http/validator/core/data_transfer" + "goskeleton/app/global/consts" ) type LayoutGenerate struct { + DocContent string `form:"doc_content" json:"doc_content" binging:"required"` } -func (s LayoutGenerate) CheckParams(context *gin.Context) { +func (l LayoutGenerate) CheckParams(context *gin.Context) { + if err:=context.ShouldBind(&l);err!=nil{ + response.ValidatorError(context,err) + return + } + extraAddBindDataContext := data_transfer.DataAddContext(l, consts.ValidatorPrefix, context) + if extraAddBindDataContext == nil { + response.ErrorSystem(context, "LayoutGenerate表单参数验证器json化失败", "") + return + } + (&web.LayoutGenerate{}).LayoutGenerate(extraAddBindDataContext) } \ No newline at end of file diff --git a/GinSkeleton/app/service/ai_model_cli/layout_cli.go b/GinSkeleton/app/service/ai_model_cli/layout_cli.go index e17a3a7..0f9c2d1 100644 --- a/GinSkeleton/app/service/ai_model_cli/layout_cli.go +++ b/GinSkeleton/app/service/ai_model_cli/layout_cli.go @@ -5,6 +5,7 @@ import ( "fmt" "goskeleton/app/global/variable" "os" + "strings" "goskeleton/app/global/consts" @@ -84,85 +85,107 @@ func RequestStyle(c *gin.Context) (interface{}, error) { } func RequestStyleStream(c *gin.Context) error { - userMsg := c.GetString(consts.ValidatorPrefix + "user_input") - - qianfan.GetConfig().AccessKey = variable.ConfigYml.GetString("BaiduCE.QianFanAccessKey") - qianfan.GetConfig().SecretKey = variable.ConfigYml.GetString("BaiduCE.QianFanSecretKey") - - chat := qianfan.NewChatCompletion( - qianfan.WithModel("ERNIE-4.0-8K"), - ) - - chatHistory := []qianfan.ChatCompletionMessage{} - - systemMsgPath := variable.ConfigYml.GetString("BaiduCE.StyleGeneratePromptPath") - prompt, err := os.ReadFile(variable.BasePath + systemMsgPath) - if err != nil || len(prompt) == 0 { - variable.ZapLog.Error(fmt.Sprintf("读取提示词文件失败: %v", err)) - return err - } - - userHistory, exist := c.Get(consts.ValidatorPrefix + "chat_history") - if exist && userHistory != nil { - historySlice, ok := userHistory.([]interface{}) - if !ok || len(historySlice)%2 != 0 { - variable.ZapLog.Error(fmt.Sprintf("用户历史对话格式错误: %v", userHistory)) - return fmt.Errorf("用户历史对话格式错误") - } - - var chatHistoryConverted []qianfan.ChatCompletionMessage - for _, item := range historySlice { - if itemMap, ok := item.(map[string]interface{}); ok { - role, roleOk := itemMap["role"].(string) - content, contentOk := itemMap["content"].(string) - if roleOk && contentOk { - chatHistoryConverted = append(chatHistoryConverted, qianfan.ChatCompletionMessage{ - Role: role, - Content: content, - }) - } else { - variable.ZapLog.Error(fmt.Sprintf("用户历史对话格式错误: %v\nrole 或 content 类型断言失败", userHistory)) - return fmt.Errorf("用户历史对话格式错误") - } - } else { - variable.ZapLog.Error(fmt.Sprintf("用户历史对话格式错误: %v\n无法将 item 转换为 map[string]interface{}", userHistory)) - return fmt.Errorf("用户历史对话格式错误") - } - } - - if len(chatHistoryConverted) > 0 && len(chatHistoryConverted)%2 == 0 { - chatHistory = append(chatHistory, chatHistoryConverted...) - } - } - - chatHistory = append(chatHistory, qianfan.ChatCompletionUserMessage(userMsg)) - - stream, err := chat.Stream(context.TODO(), &qianfan.ChatCompletionRequest{System: string(prompt), Messages: chatHistory}) - if err != nil { - variable.ZapLog.Error(fmt.Sprintf("对话失败: %v", err)) - return err - } - defer stream.Close() + userMsg := c.GetString(consts.ValidatorPrefix + "user_input") + + chatHistory := []qianfan.ChatCompletionMessage{} + + systemMsgPath := variable.ConfigYml.GetString("BaiduCE.StyleGeneratePromptPath") + prompt, err := os.ReadFile(variable.BasePath + systemMsgPath) + if err != nil || len(prompt) == 0 { + variable.ZapLog.Error(fmt.Sprintf("读取提示词文件失败: %v", err)) + return err + } + + userHistory, exist := c.Get(consts.ValidatorPrefix + "chat_history") + if exist && userHistory != nil { + historySlice, ok := userHistory.([]interface{}) + if !ok || len(historySlice)%2 != 0 { + variable.ZapLog.Error(fmt.Sprintf("用户历史对话格式错误: %v", userHistory)) + return fmt.Errorf("用户历史对话格式错误") + } + + var chatHistoryConverted []qianfan.ChatCompletionMessage + for _, item := range historySlice { + if itemMap, ok := item.(map[string]interface{}); ok { + role, roleOk := itemMap["role"].(string) + content, contentOk := itemMap["content"].(string) + if roleOk && contentOk { + chatHistoryConverted = append(chatHistoryConverted, qianfan.ChatCompletionMessage{ + Role: role, + Content: content, + }) + } else { + variable.ZapLog.Error(fmt.Sprintf("用户历史对话格式错误: %v\nrole 或 content 类型断言失败", userHistory)) + return fmt.Errorf("用户历史对话格式错误") + } + } else { + variable.ZapLog.Error(fmt.Sprintf("用户历史对话格式错误: %v\n无法将 item 转换为 map[string]interface{}", userHistory)) + return fmt.Errorf("用户历史对话格式错误") + } + } + + if len(chatHistoryConverted) > 0 && len(chatHistoryConverted)%2 == 0 { + chatHistory = append(chatHistory, chatHistoryConverted...) + } + } + + chatHistory = append(chatHistory, qianfan.ChatCompletionUserMessage(userMsg)) + return ChatByStream(c, string(prompt), chatHistory) +} + +func RequestLayout(c *gin.Context) error { + doc_content := c.GetString(consts.ValidatorPrefix + "doc_content") + + chatHistory := []qianfan.ChatCompletionMessage{} + + systemMsgPath := variable.ConfigYml.GetString("BaiduCE.LayoutGeneratePromptPath") + prompt, err := os.ReadFile(variable.BasePath + systemMsgPath) + if err != nil || len(prompt) == 0 { + variable.ZapLog.Error(fmt.Sprintf("读取提示词文件失败: %v", err)) + return err + } + + chatHistory = append(chatHistory, qianfan.ChatCompletionUserMessage("待排版内容\n"+doc_content)) + return ChatByStream(c, string(prompt), chatHistory) +} + +func ChatByStream(c *gin.Context, prompt string, chatHistory []qianfan.ChatCompletionMessage) error{ + + qianfan.GetConfig().AccessKey = variable.ConfigYml.GetString("BaiduCE.QianFanAccessKey") + qianfan.GetConfig().SecretKey = variable.ConfigYml.GetString("BaiduCE.QianFanSecretKey") + + chat := qianfan.NewChatCompletion( + qianfan.WithModel("ERNIE-4.0-8K"), + ) + + stream, err := chat.Stream(context.TODO(), &qianfan.ChatCompletionRequest{System: string(prompt), Messages: chatHistory}) + if err != nil { + variable.ZapLog.Error(fmt.Sprintf("对话失败: %v", err)) + return err + } + defer stream.Close() c.Writer.Flush() defer c.Writer.Flush() - for { - response, err := stream.Recv() - if response.IsEnd { - break // 流结束,退出循环 - } - if err != nil { - variable.ZapLog.Error(fmt.Sprintf("接收流失败: %v", err)) - return err - } + outputMsg:=strings.Builder{} + for { + response, err := stream.Recv() + if response.IsEnd { + break // 流结束,退出循环 + } + if err != nil { + variable.ZapLog.Error(fmt.Sprintf("接收流失败: %v", err)) + return err + } // 将结果写入到响应体 - if _,err:=fmt.Fprintf(c.Writer,"%s",response.Result);err!=nil{ + outputMsg.WriteString(response.Result) + if _, err := fmt.Fprintf(c.Writer, "%s", response.Result); err != nil { variable.ZapLog.Error(fmt.Sprintf("写入流失败: %v", err)) return err } // 立即刷新缓冲区,以确保数据立即发送到客户端 c.Writer.Flush() - } - return nil // 正常结束,返回 nil + } + return nil // 正常结束,返回 nil } \ No newline at end of file diff --git a/GinSkeleton/config/config.yml b/GinSkeleton/config/config.yml index e1b6352..ac86283 100644 --- a/GinSkeleton/config/config.yml +++ b/GinSkeleton/config/config.yml @@ -154,3 +154,4 @@ BaiduCE: # QianFanSecretKey: "1edf17c358574e75b9913ebff7d95b61" # 访问千帆sdk 时用的 SecretKey QianFanSecretKey: "cb812e1b6e56420ea858d160e1351869" StyleGeneratePromptPath: "/storage/app/prompt/style_generate.prompt" # 生成样式的提示词保存路径 + LayoutGeneratePromptPath: "/storage/app/prompt/layout_generate.prompt" # 生成布局的提示词保存路径 diff --git a/GinSkeleton/storage/app/prompt/layout_generate.prompt b/GinSkeleton/storage/app/prompt/layout_generate.prompt new file mode 100644 index 0000000..f084a58 --- /dev/null +++ b/GinSkeleton/storage/app/prompt/layout_generate.prompt @@ -0,0 +1,4 @@ +请将以下文章进行重新排版,使其格式更加美观且井然有序,同时保留文章的所有原始内容和文字,不要对内容进行任何修改。 +请你调整标题样式,将所有标题改为清晰的分级结构。文章标题使用#,一级标题使用##,以此类推。 +正文部分的段落需适当分段,避免过长或过短。 请确保整体排版整洁、易于阅读。 +不要对文章的原始内容进行修改。只返回重新排版后的内容,不要返回其他的额外内容。 \ No newline at end of file diff --git a/coeditor_frontend/src/components/plugins.js b/coeditor_frontend/src/components/plugins.js index 1c6cfb6..9bdca65 100644 --- a/coeditor_frontend/src/components/plugins.js +++ b/coeditor_frontend/src/components/plugins.js @@ -83,7 +83,8 @@ import { getPageContent, getUserConfigFromBackend, saveData, - // markdown2html + // markdown2html, + html2markdown } from './utils'; // 导出为docx插件 @@ -340,7 +341,58 @@ class SaveButton extends Plugin { }); } } +// AI 自动排版 +function aiformat(){ + console.log("ai formatting") + const editor = window.editor; + const doc_content = editor.getData() + console.log(doc_content); + // TODO 处理html文件 + // step 1 - split images and insert text tag + const markdown_content = html2markdown(doc_content) + console.log(markdown_content) + + // TODO 请求大模型 + // step 2 - convert markdown response to html text and setData + // step 3 - recover original images + // step 4 - fetch users config + // step 5 - apply title styles of user + // step 6 - apply others styles of user +} + +class AiFormat extends Plugin { + init() { + const editor = this.editor; + + editor.ui.componentFactory.add('AiFormat', () => { + // The button will be an instance of ButtonView. + const button = new ButtonView(); + button.set({ + label: '自动排版', + // withText: true + tooltip: true, + // 图标 直接插入svg文件 + icon: '', + keystroke: 'Shift+F' + + }); + + // Execute a callback function when the button is clicked + button.on('execute', () => { + aiformat(); + }); + + return button; + }); + + // 添加快捷键 Shift+F 保存 + editor.keystrokes.set('Shift+F', (event, cancel) => { + aiformat(); + cancel(); + }); + } +} // 配置CKEditor5 function setConfig() { // 获取用户的样式配置 @@ -375,7 +427,7 @@ function setConfig() { 'numberedList', 'outdent', 'indent', - '|', 'ExportToWord', 'ExportToPDF', 'translate', 'SideBar', 'SaveButton' + '|', 'ExportToWord', 'ExportToPDF', 'translate', 'SideBar', 'SaveButton','AiFormat' ], shouldNotGroupWhenFull: true }, @@ -449,9 +501,9 @@ function setConfig() { TodoList, Underline, Undo, - Export2Word, Translation, Export2PDF, ToggleSideBar, SaveButton + Export2Word, Translation, Export2PDF, ToggleSideBar, SaveButton, AiFormat ], - balloonToolbar: ['bold', 'italic', '|', 'link', 'insertImage', '|', 'bulletedList', 'numberedList'], + balloonToolbar: ['bold', 'italic', '|', 'link', 'insertImage', '|', 'bulletedList', 'numberedList','|','AiFormat'], //自定义设置字体 fontFamily: { // 自定义字体 @@ -579,7 +631,6 @@ function setConfig() { autosave: { waitingTime: 180000, // (in ms) 3minutes save() { - // TODO save return saveData(getPageContent()); } }, diff --git a/coeditor_frontend/src/components/utils.js b/coeditor_frontend/src/components/utils.js index bcd66f2..7137295 100644 --- a/coeditor_frontend/src/components/utils.js +++ b/coeditor_frontend/src/components/utils.js @@ -1,11 +1,11 @@ // utils.js import { MarkdownToHtml } from '@ckeditor/ckeditor5-markdown-gfm/src/markdown2html/markdown2html.js'; +import { HtmlToMarkdown } from '@ckeditor/ckeditor5-markdown-gfm/src/html2markdown/html2markdown.js'; // 获取用户配置 export function getUserConfigFromBackend() { // TODO 请求用户配置 const options = {}; // 字体、字号、样式 - // TODO const { fontFamilyOptions = [ 'default', @@ -92,7 +92,7 @@ export function getUserConfigFromBackend() { }; } -// 实现自动保存saveData方法,将编辑内容发送至后端 +// TODO 实现自动保存saveData方法,将编辑内容发送至后端 export function saveData(data) { // return new Promise( resolve => { // setTimeout( () => { @@ -128,7 +128,7 @@ export function getPageContent() { // return pageContent.outerHTML; } -// 获取并应用用户定义的样式 +// TODO 获取并应用用户定义的样式css export function getAndApplyUserStyles() { // 模拟从后端获取用户定义的样式 const response = fetch('/api/user-styles'); @@ -140,10 +140,18 @@ export function getAndApplyUserStyles() { } // markdown转html 便于将大语言模型的输出(一般为markdown格式)转换为ckeditor的html格式 -// 利用ckeditor markdown插件,但不能在CkeditorView.vue中使用 +// 利用ckeditor markdown插件的功能子类,但不能在CkeditorView.vue中直接使用markdown插件 // 否则会改变编辑器数据处理器为markdown,即getData()需要传入markdown string,setData()返回markdown string export function markdown2html(markdownString){ const markdownToHtml = new MarkdownToHtml(); const htmlString = markdownToHtml.parse(markdownString); return htmlString; +} + +// html转markdown 便于将文件内容发送给大语言模型来进行排版 +// 再利用 @markdown2html 将大语言模型的返回内容重新转换为文件内容并展示 或做进一步处理 +export function html2markdown(htmlString){ + const htmltomarkdown = new HtmlToMarkdown(); + const markdownString = htmltomarkdown.parse(htmlString); + return markdownString } \ No newline at end of file diff --git a/coeditor_frontend/src/views/CkeditorView.vue b/coeditor_frontend/src/views/CkeditorView.vue index 77604c6..e16da5a 100644 --- a/coeditor_frontend/src/views/CkeditorView.vue +++ b/coeditor_frontend/src/views/CkeditorView.vue @@ -341,10 +341,8 @@ export default { this.$refs.editorMenuBarElement.appendChild(editor.ui.view.menuBarView.element); // 这里向后端获取要打开文件地内容 const pageContent = this.store.state.user.filecontent; - // const pageContent = '

Congratulations on setting up CKEditor 5! 🎉

\n

\n You\'ve successfully created a CKEditor 5 project. This powerful text editor will enhance your application, enabling rich text editing\n capabilities that are customizable and easy to use.\n

\n

What\'s next?

\n
    \n
  1. \n Integrate into your app: time to bring the editing into your application. Take the code you created and add to your\n application.\n
  2. \n
  3. \n Explore features: Experiment with different plugins and toolbar options to discover what works best for your needs.\n
  4. \n
  5. \n Customize your editor: Tailor the editor\'s configuration to match your application\'s style and requirements. Or even\n write your plugin!\n
  6. \n
\n

\n Keep experimenting, and don\'t hesitate to push the boundaries of what you can achieve with CKEditor 5. Your feedback is invaluable to us\n as we strive to improve and evolve. Happy editing!\n

\n

Helpful resources

\n\n

Need help?

\n

\n See this text, but the editor is not starting up? Check the browser\'s console for clues and guidance. It may be related to an incorrect\n license key if you use premium features or another feature-related requirement. If you cannot make it work, file a GitHub issue, and we\n will help as soon as possible!\n

\n'; - // 打开已有文本就将pageContent替换 - // TODO editor.setData(pageContent); + // 将editor保存到window中,便于别的地方使用|官方推荐做法 window.editor = editor; }, // sidebar @@ -404,7 +402,6 @@ export default { messagesDiv.appendChild(messageDiv); if (sender === 'ai') { - // TODO 从返回文本中提取出css部分 preElement.textContent = `文心一言:\n` + preElement.textContent; messageDiv.style.backgroundColor = '#bdc3c7'; } @@ -421,6 +418,7 @@ export default { // 构造聊天历史 let chatHistory = []; const messages = document.getElementById('messages').children; + // TODO 如果最后一个消息是error 清空messages for (let i = 0; i < messages.length; i++) { if (i % 4 == 0) { chatHistory.push({ Role: 'user', Content: messages[i].textContent }); @@ -428,6 +426,8 @@ export default { const assistantResponse = messages[i].textContent.replaceAll('文心一言:\n', ''); chatHistory.push({ Role: 'assistant', Content: assistantResponse }); } + // i %4 == 2 preview + // i %4 == 3 button } console.log(chatHistory); @@ -435,9 +435,6 @@ export default { this.displayMessage(messageText, 'user'); // 向后端调用API并接受response - // 根据messages的内容构造聊天历史列表 - // TODO - // const chat_history = [] try { const response = await fetch('http://localhost:14514/admin/ai_layout/style_generate', { method: 'POST', @@ -451,7 +448,6 @@ export default { }); if (!response.body) { - // TODO throw new Error('No response body'); }