添加自动排版功能(未完成)

master
joefalmko 3 months ago
parent 81bd2da001
commit 4e20d93f98

@ -87,4 +87,6 @@ const (
DocRefineFailCode int = -400452
StyleGenerateFailMsg string = "样式生成失败"
StyleGenerateFailCode int = -400453
LayoutGenerateFailMsg string = "排版生成失败"
LayoutGenerateFailCode int = -400454
)

@ -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)
}
}

@ -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)
}

@ -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
}

@ -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" # 生成布局的提示词保存路径

@ -0,0 +1,4 @@
请将以下文章进行重新排版,使其格式更加美观且井然有序,同时保留文章的所有原始内容和文字,不要对内容进行任何修改。
请你调整标题样式,将所有标题改为清晰的分级结构。文章标题使用#,一级标题使用##,以此类推。
正文部分的段落需适当分段,避免过长或过短。 请确保整体排版整洁、易于阅读。
不要对文章的原始内容进行修改。只返回重新排版后的内容,不要返回其他的额外内容。

@ -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: '<svg t="1733151966769" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3854" width="200" height="200"><path d="M85.312 938.688V85.312h59.776v853.376h-59.712z m174.976-118.4a64 64 0 0 1-64-64v-128a64 64 0 0 1 64-64h614.4a64 64 0 0 1 64 64v128a64 64 0 0 1-64 64h-614.4z m0-357.376a64 64 0 0 1-64-64v-128a64 64 0 0 1 64-64h384a64 64 0 0 1 64 64v128a64 64 0 0 1-64 64h-384z" p-id="3855" fill="#2c2c2c"></path></svg>',
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());
}
},

@ -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 stringsetData()返回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
}

@ -341,10 +341,8 @@ export default {
this.$refs.editorMenuBarElement.appendChild(editor.ui.view.menuBarView.element);
//
const pageContent = this.store.state.user.filecontent;
// const pageContent = '<h2>Congratulations on setting up CKEditor 5! 🎉</h2>\n<p>\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</p>\n<h3>What\'s next?</h3>\n<ol>\n <li>\n <strong>Integrate into your app</strong>: time to bring the editing into your application. Take the code you created and add to your\n application.\n </li>\n <li>\n <strong>Explore features:</strong> Experiment with different plugins and toolbar options to discover what works best for your needs.\n </li>\n <li>\n <strong>Customize your editor:</strong> Tailor the editor\'s configuration to match your application\'s style and requirements. Or even\n write your plugin!\n </li>\n</ol>\n<p>\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</p>\n<h3>Helpful resources</h3>\n<ul>\n <li>📝 <a href="https://orders.ckeditor.com/trial/premium-features">Trial sign up</a>,</li>\n <li>📕 <a href="https://ckeditor.com/docs/ckeditor5/latest/installation/index.html">Documentation</a>,</li>\n <li> <a href="https://github.com/ckeditor/ckeditor5">GitHub</a> (star us if you can!),</li>\n <li>🏠 <a href="https://ckeditor.com">CKEditor Homepage</a>,</li>\n <li>🧑💻 <a href="https://ckeditor.com/ckeditor-5/demo/">CKEditor 5 Demos</a>,</li>\n</ul>\n<h3>Need help?</h3>\n<p>\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</p>\n';
// pageContent
// TODO
editor.setData(pageContent);
// editorwindow便使|
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');
// APIresponse
// 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');
}

Loading…
Cancel
Save