diff --git a/GinSkeleton/app/http/controller/web/ai_layout_controller.go b/GinSkeleton/app/http/controller/web/ai_layout_controller.go index 4daf410..64c945a 100644 --- a/GinSkeleton/app/http/controller/web/ai_layout_controller.go +++ b/GinSkeleton/app/http/controller/web/ai_layout_controller.go @@ -12,9 +12,20 @@ type StyleGenerate struct { // ai生成样式 func (s *StyleGenerate) StyleGenerate(c *gin.Context) { - if res, err := ai_model_cli.RequestStyle(c); err==nil { - response.Success(c, consts.CurdStatusOkMsg, res.(string)) - } else { + // 非流式传输 + // if res, err := ai_model_cli.RequestStyle(c); err==nil { + // response.Success(c, consts.CurdStatusOkMsg, res.(string)) + // } else { + // response.Fail(c, consts.StyleGenerateFailCode, consts.StyleGenerateFailMsg, err) + // } + + // 设置 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.RequestStyleStream(c); err != nil { response.Fail(c, consts.StyleGenerateFailCode, consts.StyleGenerateFailMsg, err) } + } diff --git a/GinSkeleton/app/service/ai_model_cli/layout_cli.go b/GinSkeleton/app/service/ai_model_cli/layout_cli.go index 097c6cf..e17a3a7 100644 --- a/GinSkeleton/app/service/ai_model_cli/layout_cli.go +++ b/GinSkeleton/app/service/ai_model_cli/layout_cli.go @@ -74,7 +74,6 @@ func RequestStyle(c *gin.Context) (interface{}, error) { // add user input to chat history chatHistory = append(chatHistory, qianfan.ChatCompletionUserMessage(userMsg)) - // define a stream chat client response, err := chat.Do(context.TODO(), &qianfan.ChatCompletionRequest{System: string(prompt), Messages: chatHistory}) if err != nil { variable.ZapLog.Error(fmt.Sprintf("对话失败: %v", err)) @@ -83,3 +82,87 @@ func RequestStyle(c *gin.Context) (interface{}, error) { return response.Result, nil } + +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() + + 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 + } + // 将结果写入到响应体 + 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 +} \ No newline at end of file diff --git a/GinSkeleton/config/config.yml b/GinSkeleton/config/config.yml index 42e5653..e1b6352 100644 --- a/GinSkeleton/config/config.yml +++ b/GinSkeleton/config/config.yml @@ -148,6 +148,9 @@ Captcha: BaiduCE: ApiKey: "AR1SUIjaKSsCcDjj11QzHDOc" # 生成鉴权签名时使用的 API_KEY SecretKey: "zvEb5CzpuGCZNdQC1TPmDh3IOWn5aWDT" # 生成鉴权签名时使用的 SECRET_KEY - QianFanAccessKey: "ALTAKOxb5YvHncyFr7Qbuv1cK0" # 访问千帆sdk 时用的 AccessKey - QianFanSecretKey: "1edf17c358574e75b9913ebff7d95b61" # 访问千帆sdk 时用的 SecretKey + # QianFanAccessKey: "ALTAKOxb5YvHncyFr7Qbuv1cK0" # 访问千帆sdk 时用的 AccessKey + + QianFanAccessKey: "ALTAK0utWNCwEoQtGHvHYf46yj" + # QianFanSecretKey: "1edf17c358574e75b9913ebff7d95b61" # 访问千帆sdk 时用的 SecretKey + QianFanSecretKey: "cb812e1b6e56420ea858d160e1351869" StyleGeneratePromptPath: "/storage/app/prompt/style_generate.prompt" # 生成样式的提示词保存路径 diff --git a/GinSkeleton/storage/app/prompt/style_generate.prompt b/GinSkeleton/storage/app/prompt/style_generate.prompt index 2e1c352..2f9b56c 100644 --- a/GinSkeleton/storage/app/prompt/style_generate.prompt +++ b/GinSkeleton/storage/app/prompt/style_generate.prompt @@ -5,7 +5,7 @@ 标题:(i为标题级别,如 h1, h2 等) 文本块:

块引用:

-行文本: +文本: 代码块:
 所有样式都需在 .ck-content 中定义,格式为:
 ``` css
@@ -17,7 +17,7 @@
 只需要精确输出用户需要生成的样式,不要生成用户未指定的样式。
 ### 示例
 #### 示例一
-用户输入:生成一个文本块样式
+用户输入:生成一个文本块(文本框)样式
 输出:
 ``` css
 .ck-content p.info-box { 
diff --git a/coeditor_frontend/src/views/CkeditorView.vue b/coeditor_frontend/src/views/CkeditorView.vue
index 44f6d9c..72844d4 100644
--- a/coeditor_frontend/src/views/CkeditorView.vue
+++ b/coeditor_frontend/src/views/CkeditorView.vue
@@ -297,7 +297,6 @@ import { setConfig } from '../components/plugins'
 // import {getUserConfigFromBackend,saveData,getPageContent,getAndApplyUserStyles} from './components/utils';
 import { useStore } from 'vuex';
 import router from '../router/index.js';
-import axios from 'axios';
 
 export default {
 	name: 'CkeditorView',
@@ -407,24 +406,24 @@ export default {
 				// TODO 从返回文本中提取出css部分
 				preElement.textContent = `文心一言:\n` + preElement.textContent;
 				messageDiv.style.backgroundColor = '#bdc3c7';
-				// 预览生成的样式
-				this.previewStyleAtMessages(text);
 			}
 			// messagesDiv.scrollTop = messagesDiv.scrollHeight;
+			return preElement;
 		},
 		// 发送消息
-		sendMessage() {
+		async sendMessage() {
 			const userInput = document.getElementById('userInput');
 			const messageText = userInput.value;
 			if (messageText.trim() === '') return;
 
+
 			// 构造聊天历史
 			let chatHistory = [];
 			const messages = document.getElementById('messages').children;
 			for (let i = 0; i < messages.length; i++) {
-				if (i%4==0){
+				if (i % 4 == 0) {
 					chatHistory.push({ Role: 'user', Content: messages[i].textContent });
-				}else if (i%4==1){
+				} else if (i % 4 == 1) {
 					const assistantResponse = messages[i].textContent.replaceAll('文心一言:\n', '');
 					chatHistory.push({ Role: 'assistant', Content: assistantResponse });
 				}
@@ -438,22 +437,49 @@ export default {
 			// 根据messages的内容构造聊天历史列表
 			// TODO
 			// const chat_history = []
+			try {
+				const response = await fetch('http://localhost:14514/admin/ai_layout/style_generate', {
+					method: 'POST',
+					headers: {
+						'Content-Type': 'application/json'
+					},
+					body: JSON.stringify({
+						user_input: userInput.value,
+						...(chatHistory.length > 0 && { chat_history: chatHistory })
+					})
+				});
 
-			axios({
-				url: 'http://localhost:14514/admin/ai_layout/style_generate',
-				method: 'POST',
-				data: {
-					user_input: userInput.value,
-					...(chatHistory.length > 0 && { chat_history: chatHistory }),
+				if (!response.body) {
+					// TODO
+					throw new Error('No response body');
 				}
-			})
-				.then(response => {
-					console.log(response);
-					this.displayMessage(response.data.data, 'ai')
-				})
-				.catch(error => {
-					console.error('Error:', error);
-				});
+
+				const reader = response.body.getReader();
+				const decoder = new TextDecoder('utf-8');
+				let result = '';
+				var messageStream;
+				/* eslint-disable no-constant-condition */
+				while (true) {
+					const { done, value } = await reader.read();
+					if (done) break;
+					const slice = decoder.decode(value, { stream: true });
+					result += slice;
+					if (!messageStream) {
+						messageStream = this.displayMessage(slice, 'ai');
+					} else {
+						messageStream.textContent += slice;
+					}
+				}
+				console.log('生成的样式:', result);
+				// 预览生成的样式
+				this.previewStyleAtMessages(result);
+				/* eslint-enable no-constant-condition */
+			} catch (error) {
+				console.error('Error:', error);
+				this.displayError(error);
+			}
+
+			// 清空输入框
 			userInput.value = '';
 		},
 		// 预览生成的样式
@@ -495,15 +521,18 @@ export default {
 			while ((match = styleRegex.exec(cssText)) !== null) {
 				if (!previewElement) {
 					previewElement = document.createElement(match[1]);
-					// previewElement.textContent = 'AaBbCcDdEeFf';
-					const previewText = document.createElement('p');
-					previewText.textContent = 'AaBbCcDd';
-					previewElement.appendChild(previewText);
+					if (match[1] === 'span') {
+						previewElement.textContent = 'AaBbCcDdEeFf';
+					} else {
+						const previewText = document.createElement('p');
+						previewText.textContent = 'AaBbCcDd';
+						previewElement.appendChild(previewText);
+					}
 					previewWrapper.appendChild(previewElement);
 				}
 
 				// className后加一个随机数表明是新的class
-				const newClassName = match[2]+"_"+Math.floor(Math.random()*1000);
+				const newClassName = match[2] + "_" + Math.floor(Math.random() * 1000);
 				previewStyle.textContent = previewStyle.textContent.replaceAll(match[2], newClassName);
 				classNames.push(newClassName);
 			}
@@ -539,6 +568,37 @@ export default {
 				console.log(styleDefinition);
 				console.log(cssText);// 保存的css代码
 			};
+			// 显示按钮
+			buttonsMessageDiv.appendChild(saveButton);
+			buttonsMessageDiv.appendChild(this.createClearButton());
+			messagesDiv.appendChild(buttonsMessageDiv);
+		},
+		// 错误信息展示
+		displayError(error) {
+			const messagesDiv = document.getElementById('messages');
+			const messageDiv = document.createElement('div');
+			messageDiv.className = 'message';
+			// 发生错误时背景颜色设置为浅红色
+			messageDiv.style.backgroundColor = 'mistyrose';  // mistyrose 是一种浅红色
+			// 使用 pre 元素来格式化显示 text
+			// 可以将css html代码内容新建一个pre set language 更好地展示输出
+			const preElement = document.createElement('pre');
+			// 错误信息设置为红色
+			preElement.textContent = error;
+			preElement.style = "color:red";
+			messageDiv.appendChild(preElement);
+			messagesDiv.appendChild(messageDiv);
+
+			// 创建按钮容器
+			const buttonsMessageDiv = document.createElement('div');
+			buttonsMessageDiv.className = 'preview-buttons';
+			buttonsMessageDiv.style.display = 'flex';
+			buttonsMessageDiv.style.justifyContent = 'flex-end';
+			buttonsMessageDiv.style.marginTop = '10px';
+			buttonsMessageDiv.appendChild(this.createClearButton());
+			messagesDiv.appendChild(buttonsMessageDiv);
+		},
+		createClearButton() {
 			// 创建 clear 按钮
 			const clearButton = document.createElement('el-button');
 			clearButton.innerHTML = '';
@@ -552,10 +612,7 @@ export default {
 					messagesDiv.removeChild(messagesDiv.firstChild);
 				}
 			};
-			// 显示按钮
-			buttonsMessageDiv.appendChild(saveButton);
-			buttonsMessageDiv.appendChild(clearButton);
-			messagesDiv.appendChild(buttonsMessageDiv);
+			return clearButton;
 		}
 	},
 	components: {