公司项目中有个需求,要发布文章,方案中通过调用豆包大模型接口,生成文章,并展示在特定区域,还包括一键生成摘要、一键生成关键词等功能。
由于生成文章要采用流式输出的方式,故采用websocket的方式,前台页面发送问题给豆包大模型接口,大模型接口发送消息给前端,前端接收后台的流式输出返回信息后,展示在对应的div中。
首先,需要增加websocket的支持,在springboot项目新增WebSocketConfig类
@Configuration
@EnableWebSocket
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpoint() {
return new ServerEndpointExporter();
}
@Bean
public WebSocketContainer webSocketContainer() {
WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer();
webSocketContainer.setDefaultMaxSessionIdleTimeout(300000L);
return webSocketContainer;
}
}
然后新建一个websocket的服务,包含了onOpen、onMessage、onClose、OnError方法
package com.xxx.logistics.server;
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.xxx.chainResource.DouBaoMessage;
import com.xxx.chainResource.IVolcengineDouBaoService;
import com.xxx.chainResource.server.ChatIMChannel;
import com.xxx.chainResource.server.DouBaoIMChannel;
import com.xxx.common.utils.EmptyUtil;
import com.xxx.common.utils.SensitiveUtils;
import io.swagger.annotations.Api;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
@Log4j2
@Component
@Api(tags = "DouBaoIM 服务")
@ServerEndpoint(value = "/api/douBaoIM/server/{userId}")
public class DouBaoIMServer {
private static IVolcengineDouBaoService volcengineDouBaoService;
static {
volcengineDouBaoService = SpringUtil.getBean(IVolcengineDouBaoService.class);
}
@OnOpen
public void onOpen(@PathParam(value = "userId") String userId, Session session) throws IOException {
if (EmptyUtil.isEmpty(ChatIMChannel.map.get(userId))) {
DouBaoIMChannel.map.put(userId, new DouBaoIMChannel()
.setSession(session)
);
} else if (!ChatIMChannel.map.get(userId).session.isOpen()) {
DouBaoIMChannel.map.get(userId).session.close();
DouBaoIMChannel.map.remove(userId);
DouBaoIMChannel.map.put(userId, new DouBaoIMChannel()
.setSession(session)
);
}
log.info("WebSocket ——> DouBaoIM 服务端 连接 {}", userId);
}
@OnMessage
public void onMessage(@PathParam(value = "userId") String userId, String message) throws IOException {
DouBaoMessage douBaoMessage = JSONObject.parseObject(message, DouBaoMessage.class);
if (EmptyUtil.isNotEmpty(userId) && EmptyUtil.isNotEmpty(douBaoMessage.getMessage())) {
DouBaoIMChannel douBaoIMChannel = DouBaoIMChannel.map.get(userId);
if (SensitiveUtils.getFilter().isSensitive(douBaoMessage.getMessage())) {
if (EmptyUtil.isNotEmpty(douBaoIMChannel) && douBaoIMChannel.session.isOpen()) {
//如果有敏感词,生成摘要和关键词时将敏感词替换成*进行提问
if(douBaoMessage.getCommand().equals("generateSummary")||douBaoMessage.getCommand().equals("generateKeywords")){
volcengineDouBaoService.aiDialogueStream(userId, SensitiveUtils.getFilter().filter(douBaoMessage.getMessage(),'*'), douBaoMessage.getCommand());
}else{
volcengineDouBaoService.aiDialogueStream(userId, douBaoMessage.getMessage(), douBaoMessage.getCommand());
douBaoIMChannel.sendMessage(
JSON.toJSONString(
new DouBaoMessage().setMessage(message).setCommand("endOfAnswer")
)
);
}
}
} else {
if (douBaoMessage.getCommand().equals("heartBeat")) {
if (EmptyUtil.isNotEmpty(douBaoIMChannel) && douBaoIMChannel.session.isOpen()) {
douBaoIMChannel.sendMessage(
JSON.toJSONString(
new DouBaoMessage().setMessage(message).setCommand("heartBeat")
)
);
}
} else if (douBaoMessage.getCommand().equals("generateArticle")) {
volcengineDouBaoService.aiDialogueStreamMultipleRounds(userId, douBaoMessage.getMessage(), douBaoMessage.getCommand(),douBaoMessage.getLoginUserId());
douBaoIMChannel.sendMessage(
JSON.toJSONString(
new DouBaoMessage().setMessage(message).setCommand("endOfAnswer")
)
);
} else {
volcengineDouBaoService.aiDialogueStream(userId, douBaoMessage.getMessage(), douBaoMessage.getCommand());
douBaoIMChannel.sendMessage(
JSON.toJSONString(
new DouBaoMessage().setMessage(message).setCommand("endOfAnswer")
)
);
}
}
}
log.info("WebSocket ——> DouBaoIM 服务端 读取 {} {}", userId, message);
}
@OnClose
public void onClose(@PathParam(value = "userId") String userId, Session session) {
try {
DouBaoIMChannel douBaoIMChannel = DouBaoIMChannel.map.get(userId);
if (EmptyUtil.isNotEmpty(douBaoIMChannel) ) {
if( EmptyUtil.isNotEmpty(douBaoIMChannel.session)) {
douBaoIMChannel.session.close();
}
DouBaoIMChannel.map.remove(userId);
}
} catch (Exception e) {
log.error("WebSocket ——> DouBaoIM 服务端 断连 {}", e.getMessage());
}
log.info("WebSocket ——> ChatIM 服务端 断连 {}", userId);
}
@OnError
public void OnError(@PathParam(value = "userId") String userId, Session session, Throwable throwable) {
try {
DouBaoIMChannel douBaoIMChannel = DouBaoIMChannel.map.get(userId);
if (EmptyUtil.isNotEmpty(douBaoIMChannel) ) {
if( EmptyUtil.isNotEmpty(douBaoIMChannel.session)) {
douBaoIMChannel.session.close();
}
DouBaoIMChannel.map.remove(userId);
}
} catch (Exception e) {
log.error("WebSocket ——> DouBaoIM 服务端 错误 {}", e.getMessage());
}
// 记录原始异常及其堆栈跟踪
log.error("WebSocket ——> DouBaoIM 服务端 错误 {}: {}", userId, throwable.getMessage());
}
}
其中,每一个DouBaoIMChannel代表一个通道,里面有sendMessage方法用于websocket发送消息
package com.xxx.chainResource.server;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.log4j.Log4j2;
import javax.websocket.Session;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
@Log4j2
@Data
@Accessors(chain = true)
public class DouBaoIMChannel {
public static ConcurrentHashMap<String, DouBaoIMChannel> map = new ConcurrentHashMap<>();
public Session session;
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
//log.info("WebSocket ——> DouBao 服务端 发送 {}",message);
}
}
接下来,新建火山引擎 豆包 服务类IVolcengineDouBaoService
package com.xxx.chainResource;
/**
* 火山引擎 豆包 服务类
*
* @description
*/
public interface IVolcengineDouBaoService {
/**
* AI 对话
* @param message
* @return
*/
String aiDialogue(String message);
/**
* AI 对话 流式
* @param userId
* @param message
* @return
*/
void aiDialogueStream(String userId,String message,String command);
/**
* AI 对话 流式 多轮次
* @param userId
* @param message
* @param command
*/
void aiDialogueStreamMultipleRounds(String userId, String message, String command,String loginUserId);
}
下面是IVolcengineDouBaoService 的实现类,aiDialogue采用一次性返回,aiDialogueStream采用流式输出通过websocket向客户端发送消息的方式返回,
aiDialogueStreamMultipleRounds方法也采用流式输出的方式返回,同时增加了连续提问的功能,当生成文章时,可以针对上一次的结果进行连续对话,当本次对话返回答案后,将结果存到数据库,并在下次提问时查询当天的该用户的最近5次问题和答案,同时发送给豆包接口,从而实现连续回话功能。
package com.xxx.chainResource.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.xxx.chainInfomation.QaPairs;
import com.xxx.chainInfomation.QaPairsService;
import com.xxx.chainInfomation.dto.QaPairsDTO;
import com.xxx.chainInfomation.qo.QaPairsQO;
import com.xxx.chainResource.DouBaoMessage;
import com.xxx.chainResource.IVolcengineDouBaoService;
import com.xxx.chainResource.server.DouBaoIMChannel;
import com.xxx.common.constant.VolcengineConstant;
import com.xxx.common.utils.EmptyUtil;
import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest;
import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionResult;
import com.volcengine.ark.runtime.model.completion.chat.ChatMessage;
import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole;
import com.volcengine.ark.runtime.service.ArkService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
/**
* 火山引擎 豆包 服务实现类
* @description
*/
@Log4j2
@Service
public class VolcengineDouBaoServiceImpl implements IVolcengineDouBaoService {
@Autowired
private QaPairsService qaPairsService;
@Override
public String aiDialogue(String message) {
StringBuffer result = new StringBuffer();
ArkService service = new ArkService(VolcengineConstant.ARK_API_KEY);
List<ChatMessage> messagesList = new ArrayList<>();
messagesList.add(
ChatMessage.builder().role(ChatMessageRole.SYSTEM)
.content(VolcengineConstant.INIT_SYSTEM_MESSAGE)
.build()
);
messagesList.add(
ChatMessage.builder().role(ChatMessageRole.USER)
.content(message)
.build()
);
log.info("火山引擎 ——> DouBao 标准 {}",message);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model(VolcengineConstant.MODEL_NODE)
.messages(messagesList)
.build();
ChatCompletionResult chatCompletion = service.createChatCompletion(chatCompletionRequest);
chatCompletion.getChoices().forEach(choice ->
result.append(choice.getMessage().getContent())
);
service.shutdownExecutor();
return result.toString();
}
@Override
public void aiDialogueStream(String userId,String message,String command) {
ArkService service = new ArkService(VolcengineConstant.ARK_API_KEY);
List<ChatMessage> messagesList = new ArrayList<>();
messagesList.add(
ChatMessage.builder().role(ChatMessageRole.SYSTEM)
.content(VolcengineConstant.INIT_SYSTEM_MESSAGE)
.build()
);
messagesList.add(
ChatMessage.builder().role(ChatMessageRole.USER)
.content(message)
.build()
);
DouBaoIMChannel douBaoIMChannel = DouBaoIMChannel.map.get(userId);
if(EmptyUtil.isNotEmpty(douBaoIMChannel) && douBaoIMChannel.session.isOpen()){
log.info("火山引擎 ——> DouBao 流式 {}",message);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model(VolcengineConstant.MODEL_NODE)
.messages(messagesList)
.build();
service.streamChatCompletion(chatCompletionRequest)
.doOnError(Throwable::printStackTrace)
.blockingForEach(
choice -> {
if (EmptyUtil.isNotEmpty(choice.getChoices())) {
douBaoIMChannel.sendMessage(
JSON.toJSONString(
new DouBaoMessage().setMessage(String.valueOf(choice.getChoices().get(0).getMessage().getContent())).setCommand(command)
)
);
}
}
);
service.shutdownExecutor();
}
}
@Override
public void aiDialogueStreamMultipleRounds(String userId , String message , String command,String loginUserId) {
ArkService service = new ArkService(VolcengineConstant.ARK_API_KEY);
List<ChatMessage> messagesList = new ArrayList<>();
messagesList.add(
ChatMessage.builder().role(ChatMessageRole.SYSTEM)
.content(VolcengineConstant.INIT_SYSTEM_MESSAGE)
.build()
);
List<QaPairs> douBaoQAPairsList = new ArrayList<QaPairs>();
QaPairsQO qaPairsQO = new QaPairsQO();
// 计算当天的开始和结束时间
LocalDateTime startOfDay = LocalDateTime.now().with(LocalTime.MIN);
LocalDateTime endOfDay = LocalDateTime.now().with(LocalTime.MAX);
douBaoQAPairsList = qaPairsService.list(com.chenglian.chainCommon.Query.getPage(qaPairsQO.getCurrent(), qaPairsQO.getSize(), QaPairs.class),
Wrappers.<QaPairs>lambdaQuery().eq(QaPairs::getUserId,loginUserId).between(QaPairs::getCreateTime, startOfDay, endOfDay).orderByDesc(QaPairs::getCreateTime)
);
if(EmptyUtil.isNotEmpty(douBaoQAPairsList)){
for(QaPairs douBaoQAPairs:douBaoQAPairsList){
messagesList.add(
ChatMessage.builder().role(ChatMessageRole.USER)
.content(douBaoQAPairs.getQuestion())
.build()
);
messagesList.add(
ChatMessage.builder().role(ChatMessageRole.ASSISTANT)
.content(douBaoQAPairs.getAnswer())
.build()
);
}
}
messagesList.add(
ChatMessage.builder().role(ChatMessageRole.USER)
.content(message)
.build()
);
DouBaoIMChannel douBaoIMChannel = DouBaoIMChannel.map.get(userId);
StringBuffer result = new StringBuffer();
if(EmptyUtil.isNotEmpty(douBaoIMChannel) && douBaoIMChannel.session.isOpen()){
log.info("火山引擎 ——> DouBao 流式多轮次 {}",message);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model(VolcengineConstant.MODEL_NODE)
.messages(messagesList)
.build();
service.streamChatCompletion(chatCompletionRequest)
.doOnError(Throwable::printStackTrace)
.blockingForEach(
choice -> {
if (EmptyUtil.isNotEmpty(choice.getChoices())) {
douBaoIMChannel.sendMessage(
com.alibaba.fastjson.JSON.toJSONString(
new DouBaoMessage().setMessage(String.valueOf(choice.getChoices().get(0).getMessage().getContent())).setCommand(command)
)
);
result.append(String.valueOf(choice.getChoices().get(0).getMessage().getContent()));
}
}
);
recordQAHistory(loginUserId,message,result.toString());
}
}
@Async("myTaskAsyncPool")
public void recordQAHistory(String loginUserId, String message,String result ) {
QaPairsDTO qaPairsDTO = new QaPairsDTO();
qaPairsDTO.setUserId(loginUserId).setQuestion(message).setAnswer(result).setCreateTime(LocalDateTime.now());
qaPairsService.submit(qaPairsDTO);
}
}
VolcengineConstant.ARK_API_KEY是大模型的鉴权 API key
VolcengineConstant.MODEL_NODE是大模型的模型节点,这两个是需要付费获取的,本实例就不在这里具体展示了。这样后端程序的实现就完成啦。下面是前端实现,同时实现了心跳发送startHeartbeat,断线重连reconnectWebSocket,通过sendCommand发送指令给大模型接口,同时ws.onmessage方法接受后台返回的消息,并根据不同的类型展示在前端的不同div中。
var ws;
var heartbeatInterval = null;
function connectWebSocket() {
var timestamp = new Date().getTime();
//生成一个随机数
var randomNum = Math.floor(Math.random() * 900 + 100)
//将时间戳和随机数拼接成一个字符串
var userId = `unique-${timestamp}-${randomNum}`;
if (ws && ws.readyState !== WebSocket.CLOSED) {
console.log('WebSocket already connected or connecting...');
return;
}
var url = websocketServerUrl+`/api/douBaoIM/server/${userId}`;
ws = new WebSocket(url);
ws.onopen = function() {
console.log('WebSocket Connected');
startHeartbeat();
};
ws.onmessage = function(event) {
//console.log(event)
var message = JSON.parse(event.data);
if (message.command === 'generateSummary') {
displaySummaryMessage(message.message);
} else if (message.command === 'generateKeywords') {
displayKeywordsMessage(message.message);
}else if (message.command === 'generateThreeQuestions') {
displayThreeQuestionsStream(message.message);
}else if (message.command === 'generateArticle') {
displayArticle(message.message);
}else if (message.command === 'generateAnswerToTheQuestion') {
displayAnswerToTheQuestion(message.message);
} else if (message.command === 'dontKnow') {
layer.msg(message.message)
}else if (message.command === 'heartBeat') {
console.log(message.message);
}else if(message.command === 'endOfAnswer'){
setButtonEnabled();
}
};
ws.onerror = function(error) {
layer.msg("连接似乎断了,正在重连,请稍后重新发送问题")
console.error('WebSocket Error: ', error);
};
ws.onclose = function() {
layer.msg("连接似乎断了,正在重连,请稍后重新发送问题")
console.log('WebSocket Connection Closed');
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
reconnectWebSocket();
};
}
/**
* 开启心跳
*/
function startHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
heartbeatInterval = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ command: 'heartBeat',message: '心跳' }));
}
}, 10000); // 假设每10秒发送一次心跳
}
/**
* 重连
*/
function reconnectWebSocket() {
console.log('Attempting to reconnect WebSocket...');
setTimeout(() => {
connectWebSocket();
}, 5000);
}
/**
* 发送指令
* @param command
* @param message
*/
function sendCommand(command, message,loginUserId) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ command: command, message: message, loginUserId:loginUserId }));
} else {
console.error('WebSocket is not open');
layer.msg("连接似乎断了,正在重连,请稍后重新发送问题");
reconnectWebSocket();
}
}
/**
* 一键生成摘要
*/
function generateSummary() {
$("#nvcAbstract").val('');
var input = document.getElementById('nvcContent');
var message = input.value.trim();
message = removeImgTags(message);
if(message==''){
layer.msg("文章内容为空,无法生成摘要")
$("#nvcContent").focus();
return false;
}
message = message+",请为上面的文章生成最多120个字的摘要,确保摘要长度严格满足不能超过120个字,且不要包含任何非摘要内容,如‘**摘要**:’标记。";
sendCommand('generateSummary', message,loginUserId);
}
/**
* 一键生成关键词
*/
function generateKeywords() {
$("#nvcKeywords").val('');
var input = document.getElementById('nvcContent');
var message = input.value.trim();
message = removeImgTags(message);
if(message==''){
layer.msg("文章内容为空,无法生成关键词")
$("#nvcContent").focus();
return false;
}
message = message+",请为上面的文章生成最多5个关键词,关键词之间用中文逗号“,”分隔,关键词中不要有隐藏符";
sendCommand('generateKeywords', message,loginUserId);
}
/**
* 去除<img>标签
* @param htmlContent
* @returns {string}
*/
function removeImgTags(htmlContent) {
// 使用DOMParser解析HTML字符串
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
// 获取所有的<img>元素
const imgs = doc.querySelectorAll('img');
// 遍历所有<img>元素并移除它们
imgs.forEach(img => img.remove());
// 将修改后的HTML转换回字符串
return doc.body.innerHTML;
}
/**
* 生成三个问题
*/
var isGeneratingQuestions = false;
function generateThreeQuestions(){
if (isGeneratingQuestions) {
return; // 如果已经在生成问题,则直接返回
}
isGeneratingQuestions = true; // 设置标志位为 true
var inspirationInspires = $("#inspirationInspires").val();
var message = inspirationInspires.trim();
if(message==''){
layer.tips("请输入内容","#inspirationInspires");
$("#inspirationInspires").focus();
isGeneratingQuestions = false; // 恢复标志位
return false;
}
message = message+",请根据上面的内容提出3个问题";
sendCommand('generateThreeQuestions', message,loginUserId);
$("#changeBatchLink").hide();
$('#threeQuestions .doub-box-ques').remove();
$('#inspirationInspiresSpan').removeClass('fosng cursor').addClass('fosnghui');
$("#loadingEffect").show();
}
/**
* 生成另外三个问题
*/
function generateOtherThreeQuestions(){
var inspirationInspires = $("#inspirationInspires").val();
var message = inspirationInspires.trim();
if(message==''){
layer.tips("请输入内容","#inspirationInspires");
$("#inspirationInspires").focus();
return false;
}
message = message+",请根据上面的内容提出与之前不同的3个问题";
sendCommand('generateThreeQuestions', message,loginUserId);
$("#changeBatchLink").hide();
$('#threeQuestions .doub-box-ques').remove();
$("#loadingEffect").show();
}
/**
* 生成文章
*/
var isGeneratingArticle = false;
function generateArticle(){
if (isGeneratingArticle) {
return; // 如果已经在生成问题,则直接返回
}
isGeneratingArticle = true;
var helpMeWrite = $("#helpMeWrite").val();
var message = helpMeWrite.trim();
if(message==''){
layer.tips("请输入内容","#helpMeWrite");
$("#helpMeWrite").focus();
isGeneratingArticle = false;
return false;
}
sendCommand('generateArticle', message,loginUserId);
$('#articleContent').empty();
$('#helpMeWriteSpan').removeClass('fosng cursor').addClass('fosnghui');
$('#articleContent').append('<div id="loadingIndicator" style="display:none;"><i class="iconfont icon-loading"></i> 正在生成文章...</div>');
$("#loadingIndicator").show();
}
/**
* 生成问题的答案
*/
function generateAnswerToTheQuestion(message){
sendCommand('generateAnswerToTheQuestion', message,loginUserId);
updateQuestion(message);
$('#answerToTheQuestion').empty();
}
/**
* 显示摘要
* @param message
*/
function displaySummaryMessage(message) {
var nvcAbstractElement = $("#nvcAbstract"); // 获取文本域元素
var nvcAbstract = nvcAbstractElement.val(); // 获取当前文本域的内容
var newContentLength = nvcAbstract.length + message.length; // 计算新内容的长度
// 检查新内容的长度是否超过120字
if (newContentLength <= 120) {
// 如果未超过,则添加消息并更新文本域
nvcAbstract += message;
nvcAbstractElement.val(nvcAbstract);
changeSize(); // 假设这个函数用于调整某些样式或布局
} else {
// 如果超过,可以选择不执行任何操作,或者给用户一些反馈
console.log("摘要已超过120字限制,无法添加更多内容。");
// 这里也可以考虑截断当前内容到120字,或者显示一个警告消息给用户
}
}
/**
* 显示关键词
* @param message
*/
function displayKeywordsMessage(message) {
var nvcKeywords = $("#nvcKeywords").val();
nvcKeywords = nvcKeywords+message;
$("#nvcKeywords").val(nvcKeywords);
}
/**
* 显示文章
* @param message
*/
function displayArticle(message) {
// 获取目标div元素
var articleContentDiv = document.getElementById('articleContent');
// 确保获取到了元素
if (articleContentDiv) {
$("#loadingIndicator").hide();
var currentContent = articleContentDiv.innerHTML;
// 使用innerHTML来设置新内容,将\n替换为<br>以实现换行,并保留当前内容
articleContentDiv.innerHTML = currentContent + message.replace(/\n/g, '<br>');
var dialogContent = $('.layui-layer-content');
dialogContent.scrollTop(dialogContent[0].scrollHeight);
} else {
// 如果没有找到元素,则在控制台输出错误信息
console.error('Element with ID "articleContent" not found.');
}
}
/**
* 显示问题答案
* @param message
*/
function displayAnswerToTheQuestion(message) {
// 获取目标div元素
var answerToTheQuestionDiv = document.getElementById('answerToTheQuestion');
// 确保获取到了元素
if (answerToTheQuestionDiv) {
// 获取当前div的内容
var currentContent = answerToTheQuestionDiv.innerHTML;
// 使用innerHTML来设置新内容,将\n替换为<br>以实现换行,并保留当前内容
answerToTheQuestionDiv.innerHTML = currentContent + message.replace(/\n/g, '<br>');
var dialogContent = $('.layui-layer-content');
dialogContent.scrollTop(dialogContent[0].scrollHeight);
} else {
// 如果没有找到元素,则在控制台输出错误信息
console.error('Element with ID "articleContent" not found.');
}
}
//更改字数限制
function changeSize() {
var text = $("#nvcAbstract").val();
$("#kewordNum").html(text.length)
}
//设置按钮成为可用状态
function setButtonEnabled() {
isGeneratingQuestions = false;
isGeneratingArticle = false;
$('#inspirationInspiresSpan').removeClass('fosnghui').addClass('fosng cursor');
$('#helpMeWriteSpan').removeClass('fosnghui').addClass('fosng cursor');
toggleLink(true);
}
function toggleLink(enable) {
var link = document.querySelector('a.back-btn');
if (enable) {
link.classList.remove('disabled');
link.href = "javascript:hideQd();"; // 或者是你想要链接到的URL
link.style.pointerEvents = 'auto'; // 重新启用鼠标事件
} else {
link.classList.add('disabled');
link.href = "javascript:void(0);"; // 阻止任何默认行为
link.style.pointerEvents = 'none'; // 禁用鼠标事件
}
}
/**
* 一次性返回三个问题展示
* @param messageFragment
*/
function displayThreeQuestions(messageFragment) {
var questionsArray =[];
// 如果需要忽略引导语,并且缓冲区中还没有换行符
if (ignoreIntro && messageFragment.indexOf('\n') === -1) {
// 那么我们什么也不做,等待更多的消息片段
return;
}
// 查找换行符以分割问题
questionsArray = messageFragment.split('\n').filter(question => question.trim() !== '');
// 移除数组中的第一个元素(可能是空字符串或引导语)
questionsArray.shift();
// 检查是否还有至少三个问题剩余
if (questionsArray.length > 3) {
questionsArray = questionsArray.slice(0, 3); // 只保留后三个问题
}
// 遍历并处理保留的问题
questionsArray.forEach((question, index) => {
// 去除问题字符串前后的空格
question = question.trim();
// 如果问题字符串中包含'.',则只取'.'之后的内容
if (question.indexOf('.') !== -1) {
question = question.substring(question.indexOf('.') + 1).trim();
}
// 如果问题不为空,则添加到DOM中
if (question) {
// 注意:这里我们假设addToDOM函数能够正确处理编号,这里直接使用index+1作为编号
addToDOM(question, index + 1);
}
});
}
/**
* 将问题添加到div
* @param question
* @param number
*/
function addToDOM(question, number) {
$("#loadingEffect").hide();
var questionsContainer = document.getElementById('threeQuestions');
// 创建一个新的span来同时包含编号和问题
var combinedSpan = document.createElement('span');
// 创建编号的文本节点
var numberText = document.createTextNode(`问题${number}:`);
// 创建问题的a标签
var questionLink = document.createElement('a');
questionLink.href = 'javascript:void(0);'; // 阻止默认行为
questionLink.textContent = question;
// 这里可以添加点击事件的处理逻辑
// 为a标签添加点击事件监听器
questionLink.addEventListener('click', function() {
showQd();
generateAnswerToTheQuestion(questionLink.textContent);
});
// 将编号和问题的a标签添加到combinedSpan中
combinedSpan.appendChild(numberText);
combinedSpan.appendChild(document.createTextNode(' ')); // 在编号和问题之间添加空格
combinedSpan.appendChild(questionLink);
// 创建一个新的div来包含span
var questionDiv = document.createElement('div');
questionDiv.className = 'doub-box-ques';
// 将combinedSpan添加到div中
questionDiv.appendChild(combinedSpan);
// 将div添加到页面上的容器中
questionsContainer.appendChild(questionDiv);
if(number==3){
$("#changeBatchLink").show();
}
}
var questionBuffer = ''; // 当前正在构建的问题的缓冲区
var numberBuffer = ''; // 用于临时存储编号的整数部分的缓冲区
var inQuestion = false; // 标记是否正在处理一个问题
var nextQuestionNumber = 1; // 下一个问题的预期编号(主要用于调试和验证)
/**
* 按流式结果返回三个问题展示
* @param messageFragment
*/
function displayThreeQuestionsStream(messageFragment) {
if(nextQuestionNumber==4){
nextQuestionNumber = 1;
}
// 移除消息片段前后的空白字符
var trimmedFragment = messageFragment;
// 处理编号的整数部分
if (/^\d+$/.test(trimmedFragment)) {
numberBuffer = trimmedFragment; // 存储编号的整数部分
// 注意:此时我们还没有接收到点,所以还不能确定这是一个新问题的开始
} else if (trimmedFragment === ".") {
// 如果之前存储了编号的整数部分,并且当前片段是点,则开始收集问题描述
if (numberBuffer) {
inQuestion = true; // 标记开始处理新问题
questionBuffer = `${numberBuffer}.`; // 将编号(包括点)添加到问题缓冲区
numberBuffer = ''; // 重置编号整数部分的缓冲区
}
// 否则,如果单独接收到点而没有之前的编号整数部分,则忽略它
} else if (inQuestion) {
// 如果正在处理一个问题,并且接收到了非空且非换行的消息片段
if (trimmedFragment !== '' && trimmedFragment !== '\n') {
questionBuffer += trimmedFragment; // 将片段添加到问题缓冲区
} else if (trimmedFragment === '\n'||trimmedFragment === '') {
// 如果接收到了换行符或者“”,并且正在处理一个问题,则处理该问题
if (questionBuffer.trim() !== '') {
// 如果问题字符串中包含'.',则只取'.'之后的内容
if (questionBuffer.trim().indexOf('.') !== -1) {
questionBuffer = questionBuffer.trim().substring(questionBuffer.trim().indexOf('.') + 1).trim();
}
addToDOM(questionBuffer.trim(), nextQuestionNumber++);
questionBuffer = ''; // 重置问题缓冲区
inQuestion = false; // 标记问题处理完成
}
}
}
}
/**
* 更新问题
* @param newContent
*/
function updateQuestion(newContent) {
// 获取div元素
var questionDiv = document.getElementById("theQuestion");
// 创建一个新的文本节点,包含传入的新内容
var newTextNode = document.createTextNode(newContent);
// 获取“返回问题”链接元素,以便稍后可以将其重新添加到div中
var backButton = questionDiv.querySelector(".back-btn");
// 清空div内的所有现有内容
questionDiv.innerHTML = "";
// 将新内容添加到div中
questionDiv.appendChild(newTextNode);
// 将“返回问题”链接重新添加到div中
questionDiv.appendChild(backButton);
toggleLink(false);
}
$(document).ready(function() {
$(document).keydown(function(event) {
// 检查触发事件的元素是否为#myInput
if (event.target.id === "inspirationInspires") {
// 检查按键是否为回车键
if (event.which === 13) {
event.preventDefault(); // 阻止表单提交(如果input位于form中)
// 这里执行你希望在回车时触发的代码
generateThreeQuestions();
}
}
if (event.target.id === "helpMeWrite") {
// 检查按键是否为回车键
if (event.which === 13) {
event.preventDefault(); // 阻止表单提交(如果input位于form中)
// 这里执行你希望在回车时触发的代码
generateArticle();
}
}
});
});
connectWebSocket();
下面是html展示,展示了生成问题,更换问题,帮我写作等功能
<div id="doub-box" style="display: none;">
<div class="doub-box">
<div class="doub-box-hd">
<ul>
<li class="on" onclick="doubxx(1,this)">灵感启发</li>
<li onclick="doubxx(2,this)">帮我写作</li>
</ul>
</div>
<div class="mt20" id="doub1">
<div class="doub-box-tips">
1. 我们可以根据您感兴趣的内容,为您推荐话题,从而帮助你提升写作灵感; <br>2. 同时你也可以选择我们推荐的话题,我们将围绕相关话题帮您写作文章。
</div>
<div class="doub-box-textarea mt20">
<input id="inspirationInspires" placeholder="可以是一个或多个关键字,也可以是简要的一句话!" maxlength="50" autocomplete="off"></input>
<div class="flex-end"><span id="inspirationInspiresWordNum">0</span>/50
<span id="inspirationInspiresSpan" class="fosng cursor" onclick="generateThreeQuestions()"><i class="iconfont icon-emizhifeiji"></i></span>
</div>
</div>
<div class="mt10" id="threeQuestions">
<div id="loadingEffect" style="display:none;">
<i class="iconfont icon-loading"></i> 正在生成问题...
</div>
<a href="javascript:generateOtherThreeQuestions();" id="changeBatchLink" class="y_blue" style="display: none"><i class="iconfont icon-shuaxin1"></i> 换一批</a>
<!-- <div class="doub-box-ques">
<span>问题1:
<a href="javascript:showQd();">运输距离长:可能涉及跨区域甚至跨国的长途运输。 比如,从澳大利亚进口的铁矿石怎么运输到中国?</a></span></div>
<div class="doub-box-ques">
<span>问题2:
<a href="javascript:showQd();">运输距离长:可能涉及跨区域甚至跨国的长途运输。 比如,从澳大利亚进口的铁矿石怎么运输到中国?</a></span>
</div>
<div class="doub-box-ques">
<span>问题3:
<a href="javascript:showQd();">运输距离长:可能涉及跨区域甚至跨国的长途运输。 比如,从澳大利亚进口的铁矿石怎么运输到中国?</a></span>
</div>-->
</div>
</div>
<div class="mt20" id="doub2" style="display: none;">
<div class="doub-box-tips">我们可以帮助你写作文章,比如你可以这样跟我互动,eg“帮我写一篇分析网络货运平台优势的文章”</div>
<div class="doub-box-textarea mt20">
<input id="helpMeWrite" placeholder="请告诉我,你要写作的内容吧!" maxlength="50" autocomplete="off"></input>
<div class="flex-end"><span id="helpMeWriteWordNum">0</span>/50
<!--<a href="javascript:generateArticle();" class="fosng"><i class="iconfont icon-emizhifeiji"></i></a>-->
<span id="helpMeWriteSpan" class="fosng cursor" onclick="generateArticle()"><i class="iconfont icon-emizhifeiji"></i></span>
</div>
</div>
<div id="articleContent" class="mt20 font14 lineheight2">
<div id="loadingIndicator" style="display:none;">
<i class="iconfont icon-loading"></i> 正在生成文章...
</div>
<!--以下是一些提高大宗物流运输效率的方法: 优化运输路线规划: 使用先进的物流软件和数据分析,考虑道路状况、交通流量、运输距离等因素,规划出最优化的运输路线。例如,通过实时交通数据,避免拥堵路段,减少运输时间。 选择合适的运输方式: 根据货物特点、运输距离和成本等因素,综合选择铁路、公路、水路或多式联运。比如,对于长途运输且时效性要求不高的大宗商品,优先选择铁路或水路运输;对于短途且时效性要求高的,可以选择公路运输。-->
</div>
</div>
<div class="mt20" id="doub3" style="display: none;">
<div id="theQuestion" class="doub-box-tips flex-between">运输距离长:可能涉及跨区域甚至跨国的长途运输。 比如,从澳大利亚进口的铁矿石怎么运输到中国?
<a href="javascript:hideQd();" class="back-btn">返回问题</a></div>
<div id="answerToTheQuestion" class="mt20 font14 lineheight2">
<!--以下是一些提高大宗物流运输效率的方法: 优化运输路线规划: 使用先进的物流软件和数据分析,考虑道路状况、交通流量、运输距离等因素,规划出最优化的运输路线。例如,通过实时交通数据,避免拥堵路段,减少运输时间。 选择合适的运输方式: 根据货物特点、运输距离和成本等因素,综合选择铁路、公路、水路或多式联运。比如,对于长途运输且时效性要求不高的大宗商品,优先选择铁路或水路运输;对于短途且时效性要求高的,可以选择公路运输。-->
</div>
</div>
</div>
</div>
好啦,这个就是通过网页调用大模型接口,并结合websocket,实现接入ai功能的整体实现了。