v3.9.1 发布(AI应用首页,加入 Chat2BI 和 AI绘图)

This commit is contained in:
JEECG
2026-01-28 15:22:43 +08:00
parent 020ba3443c
commit 9a43a8d41a
19 changed files with 691 additions and 115 deletions

View File

@ -425,6 +425,9 @@ AI聊天助手
![](https://oscimg.oschina.net/oscnet//65298d5710b4e6039a5f802b5f8505c5.png)
AI绘图
![](https://oscimg.oschina.net/oscnet/up-a03658e8580be04d69821601de9dc5dc52d.png)
AI写文章

File diff suppressed because one or more lines are too long

View File

@ -56,7 +56,12 @@ public class AiAppConsts {
/**
* AI写作的应用id
* AI写作的流程id
*/
public static final String WRITER_APP_ID = "2010634128233779202";
public static final String ARTICLE_WRITER_FLOW_ID = "2011769909807579138";
/**
* AI写作redis请求前缀
*/
public static final String ARTICLE_WRITER_KEY = "airag:chat:article:write:{}";
}

View File

@ -168,4 +168,9 @@ public class Prompts {
*/
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
/**
* ai润色提提示词
*/
public static final String AI_TOUCHE_PROMPT = "请针对如下内容:[{}] 进行润色。 回复格式:{},语气:{},语言:{},长度:{}。";
}

View File

@ -16,11 +16,14 @@ import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.service.IAiragAppService;
import org.jeecg.modules.airag.app.service.IAiragChatService;
import org.jeecg.modules.airag.app.vo.AiArticleWriteVersionVo;
import org.jeecg.modules.airag.app.vo.AppDebugParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
/**
@ -190,4 +193,31 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
}
/**
* 写作保存
*/
@PostMapping("/save/article/write")
public Result<String> saveArticleWrite(@RequestBody AiArticleWriteVersionVo aiWriteVersionVo) {
airagAppService.saveArticleWrite(aiWriteVersionVo);
return Result.OK("保存成功!");
}
/**
* 写作删除
*/
@DeleteMapping("/delete/article/write")
public Result<String> deleteArticleWrite(@RequestParam(name = "version") String version) {
AssertUtils.assertNotEmpty("版本号不能为空", version);
airagAppService.deleteArticleWrite(version);
return Result.OK("删除成功!");
}
/**
* 写作查询
*/
@GetMapping("/list/article/write")
public Result<List<AiArticleWriteVersionVo>> listArticleWrite() {
List<AiArticleWriteVersionVo> list = airagAppService.listArticleWrite();
return Result.OK(list);
}
}

View File

@ -2,6 +2,8 @@ package org.jeecg.modules.airag.app.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.vo.AiArticleWriteVersionVo;
import java.util.List;
/**
* @Description: AI应用
@ -30,4 +32,26 @@ public interface IAiragAppService extends IService<AiragApp> {
* @return
*/
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
/**
* 写作保存
*
* @param aiWriteVersionVo
*/
void saveArticleWrite(AiArticleWriteVersionVo aiWriteVersionVo);
/**
* 写作列表
*
* @return
*/
List<AiArticleWriteVersionVo> listArticleWrite();
/**
* 写作删除
*
* @param version
*/
void deleteArticleWrite(String version);
}

View File

@ -1,5 +1,6 @@
package org.jeecg.modules.airag.app.service.impl;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -10,15 +11,19 @@ import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.output.FinishReason;
import dev.langchain4j.service.TokenStream;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.UUIDGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.app.consts.AiAppConsts;
import org.jeecg.modules.airag.app.consts.Prompts;
import org.jeecg.modules.airag.app.entity.AiragApp;
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
import org.jeecg.modules.airag.app.service.IAiragAppService;
import org.jeecg.modules.airag.app.vo.AiArticleWriteVersionVo;
import org.jeecg.modules.airag.app.vo.AppVariableVo;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.handler.AIChatParams;
@ -30,14 +35,18 @@ import org.jeecg.modules.airag.common.vo.event.EventMessageData;
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @Description: AI应用
@ -54,6 +63,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
@Autowired
private IAiragKnowledgeService airagKnowledgeService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Object generatePrompt(String prompt, boolean blocking) {
@ -247,4 +259,68 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
emitter.complete();
}
}
/**
* 写作列表
*/
@Override
public List<AiArticleWriteVersionVo> listArticleWrite() {
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
String redisKey = StrUtil.format(AiAppConsts.ARTICLE_WRITER_KEY, loginUser.getUsername());
Object data = redisTemplate.opsForValue().get(redisKey);
if (data == null) {
return new ArrayList<>();
}
List<AiArticleWriteVersionVo> aiWriteViewVoList = (List<AiArticleWriteVersionVo>) data;
Collections.reverse(aiWriteViewVoList);
return aiWriteViewVoList;
}
/**
* 写作报错
*/
@Override
public void saveArticleWrite(AiArticleWriteVersionVo aiWriteVersionVo) {
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
String redisKey = StrUtil.format(AiAppConsts.ARTICLE_WRITER_KEY, loginUser.getUsername());
//先查看redis中是否存在
Object data = redisTemplate.opsForValue().get(redisKey);
if(null != data){
List<AiArticleWriteVersionVo> aiWriteVersionVos = (List<AiArticleWriteVersionVo>) data;
aiWriteVersionVo.setVersion("V"+(aiWriteVersionVos.size() + 1));
aiWriteVersionVos.add(aiWriteVersionVo);
redisTemplate.opsForValue().set(redisKey, aiWriteVersionVos);
}else{
List<AiArticleWriteVersionVo> aiWriteVersionVos = new ArrayList<>();
aiWriteVersionVo.setVersion("V1");
aiWriteVersionVos.add(aiWriteVersionVo);
redisTemplate.opsForValue().set(redisKey, aiWriteVersionVos);
}
}
/**
* 写作删除
*/
@Override
public void deleteArticleWrite(String version) {
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
String redisKey = StrUtil.format(AiAppConsts.ARTICLE_WRITER_KEY, loginUser.getUsername());
Object data = redisTemplate.opsForValue().get(redisKey);
if (data == null) {
return;
}
List<AiArticleWriteVersionVo> aiWriteVersionVos = (List<AiArticleWriteVersionVo>) data;
if (aiWriteVersionVos.isEmpty()) {
return;
}
List<AiArticleWriteVersionVo> newList = aiWriteVersionVos.stream()
.filter(vo -> !version.equals(vo.getVersion()))
.collect(Collectors.toList());
if (newList.isEmpty()) {
redisTemplate.delete(redisKey);
} else {
redisTemplate.opsForValue().set(redisKey, newList);
}
}
}

View File

@ -933,21 +933,32 @@ public class AiragChatServiceImpl implements IAiragChatService {
private SseEmitter genImageChat(SseEmitter emitter, ChatSendParams sendParams, String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
AssertUtils.assertNotEmpty("请选择绘画模型", sendParams.getDrawModelId());
AIChatParams aiChatParams = new AIChatParams();
try {
List<String> images = sendParams.getImages();
List<Map<String, Object>> imageList = new ArrayList<>();
if(CollectionUtils.isEmpty(images)) {
//生成图片
imageList = aiChatHandler.imageGenerate(sendParams.getDrawModelId(), sendParams.getContent(), aiChatParams);
} else {
//图生图
imageList = aiChatHandler.imageEdit(sendParams.getDrawModelId(), sendParams.getContent(), images, aiChatParams);
}
// 记录历史消息
String imageMarkdown = imageList.stream().map(map -> {
String newUrl = this.uploadImage(map);
return "![](" + newUrl + ")";
}).collect(Collectors.joining("\n"));
//update-begin---author:wangshuai---date:2026-01-26---for: 【QQYUN-14615】应用门户加入新工具取绘画id---
String drawModelId = sendParams.getDrawModelId();
if(oConvertUtils.isEmpty(sendParams.getDrawModelId())){
AiragApp app = chatConversation.getApp();
String metadata = app.getMetadata();
if(oConvertUtils.isNotEmpty(metadata) && metadata.contains("drawModelId")){
drawModelId = JSONObject.parseObject(drawModelId).getString("drawModelId");
}
}
try {
List<String> images = sendParams.getImages();
List<Map<String, Object>> imageList;
if(CollectionUtils.isEmpty(images)) {
//生成图片
imageList = aiChatHandler.imageGenerate(drawModelId, sendParams.getContent(), aiChatParams);
} else {
//图生图
imageList = aiChatHandler.imageEdit(drawModelId, sendParams.getContent(), images, aiChatParams);
}
// 记录历史消息
String imageMarkdown = imageList.stream().map(map -> {
String newUrl = this.uploadImage(map);
return "![](" + newUrl + ")";
}).collect(Collectors.joining("\n"));
//update-end---author:wangshuai---date:2026-01-26---for:【QQYUN-14615】应用门户加入新工具取绘画id---
AiMessage aiMessage = new AiMessage(imageMarkdown);
appendMessage(messages, aiMessage, chatConversation, topicId);
// 处理绘画结果并通过SSE返回给客户端
@ -1868,18 +1879,46 @@ public class AiragChatServiceImpl implements IAiragChatService {
@Override
public SseEmitter genAiWriter(AiWriteGenerateVo aiWriteGenerateVo) {
String activeMode = "compose";
String reply = "reply";
ChatSendParams sendParams = new ChatSendParams();
sendParams.setAppId(AiAppConsts.WRITER_APP_ID);
sendParams.setAppId(AiAppConsts.ARTICLE_WRITER_FLOW_ID);
String content = "";
//写作
if (activeMode.equals(aiWriteGenerateVo.getActiveMode())) {
content = StrUtil.format(Prompts.AI_WRITER_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
} else {
} else if(reply.equals(aiWriteGenerateVo.getActiveMode())){
//回复
content = StrUtil.format(Prompts.AI_REPLY_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getOriginalContent(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
} else {
content = StrUtil.format(Prompts.AI_TOUCHE_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
}
sendParams.setContent(content);
sendParams.setIzSaveSession(false);
return this.send(sendParams);
//组装会话
String requestId = UUIDGenerator.generate();
String topicId = UUIDGenerator.generate();
String conversationId = UUIDGenerator.generate();
ChatConversation chatConversation = new ChatConversation();
chatConversation.setId(conversationId);
chatConversation.setMessages(new ArrayList<>());
Map<String,Object> flowInputs = new HashMap<>();
flowInputs.put("type", aiWriteGenerateVo.getActiveMode());
flowInputs.put("version", "V1");
chatConversation.setFlowInputs(flowInputs);
SseEmitter emitter = createSSE(requestId);
// 缓存emitter
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE, requestId, emitter);
// 缓存开始发送时间
log.info("[AI-CHAT]开始发送消息,requestId:{}", requestId);
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId, System.currentTimeMillis());
// 初始化历史消息缓存
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
// 发送就绪消息
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
eventRequestId.setData(EventMessageData.builder().message("").build());
sendMessage2Client(emitter, eventRequestId);
sendWithFlow(requestId, AiAppConsts.ARTICLE_WRITER_FLOW_ID, chatConversation, topicId, new ArrayList<>(), sendParams);
return emitter;
}
}

View File

@ -0,0 +1,23 @@
package org.jeecg.modules.airag.app.vo;
import lombok.Data;
/**
* @Description: AI写作版本号
*
* @author: wangshuai
* @date: 2026/1/16 11:57
*/
@Data
public class AiArticleWriteVersionVo {
/**
* 当前版本号
*/
private String version;
/**
* 写作内容
*/
private String content;
}

View File

@ -25,7 +25,7 @@ spring:
# lazy-initialization: true
flyway:
# 是否启用flyway
enabled: false
enabled: true
# 迁移sql脚本存放路径
locations: classpath:flyway/sql/mysql
# 是否关闭要清除已有库下的表功能,生产环境必须为true,否则会删库,非常重要!!!

View File

@ -0,0 +1,3 @@
INSERT INTO `airag_app` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `descr`, `icon`, `type`, `prologue`, `prompt`, `model_id`, `knowledge_ids`, `flow_id`, `status`, `msg_num`, `metadata`, `preset_question`, `quick_command`, `plugins`, `memory_id`, `variables`, `iz_open_memory`, `memory_prompt`) VALUES ('2008090512835629057', 'admin', '2026-01-05 16:17:41', 'admin', '2026-01-26 10:36:57', 'A05A01A01', NULL, '绘画_示例', NULL, NULL, 'chatSimple', NULL, '# 角色文生图创意引擎\n你是一位精通视觉艺术与AI绘画的创意引擎能将抽象的文字描述转化为精准高质量富有艺术感的图像提示词\n\n## 目标\n根据用户提供的文字描述生成可直接用于主流AI绘画模型如MidjourneyStable DiffusionDALL-E的详细结构化高成功率的提示词以帮助用户高效获得理想的视觉作品\n\n## 技能\n1. **深度语义理解**准确解析用户描述的意图核心元素氛围和情感\n2. **视觉元素拆解与重构**将抽象概念分解为具体的视觉构成要素主体环境风格构图光影材质等\n3. **提示词工程优化**精通各类AI绘画模型的语法规则熟练运用权重分配负面提示参数设置等技巧\n4. **艺术风格知识库**掌握从古典到现代从写实到抽象的各种艺术流派画家风格电影摄影术语\n5. **多方案生成与评估**能针对同一需求提供不同侧重点的提示词变体并简要说明其预期效果差异\n\n## 工作流\n1. **需求澄清与细化**首先与用户确认其描述中的模糊点好看具体指什么风格并主动询问关键细节如画幅比例主要色彩倾向是否包含特定艺术家风格\n2. **结构化提示词构建**按照主体描述 + 环境/背景 + 艺术风格/媒介 + 构图/视角 + 光照/色彩 + 画质/参数 + (负面提示)的逻辑结构构建提示词\n3. **优化与变体提供**生成一个主推的最符合描述的详细提示词同时提供1-2个在风格或侧重点上略有不同的变体选项供用户选择或组合\n4. **使用建议**简要说明该提示词在目标平台如Midjourney中可能需要调整的参数建议 `--ar 16:9`, `--v 6.0`\n\n## 输出格式\n请严格按照以下格式输出使用清晰的标题和分点\n\n**用户需求分析摘要**\n- 核心主题\n- 期望风格/氛围\n- 关键视觉元素\n- 已确认细节\n\n**主推提示词 (适用于 Midjourney/Stable Diffusion)**\n`[完整的结构化的英文提示词包含必要的权重符号如 :: 和参数]`\n\n**提示词变体选项**\n1. **[变体名称更写实风格]**`[变体提示词]`\n * *效果说明此变体侧重于...*\n2. **[变体名称更抽象表现]**`[变体提示词]`\n * *效果说明此变体侧重于...*\n\n**使用建议**\n- **平台参数**建议添加 `--ar [比例] --s [风格化值] --v [版本]` 根据分析给出具体建议\n- **调整建议**如需更...效果可尝试在提示词中加入...关键词如需避免...可在负面提示中添加...\n\n## 限制\n- **反幻觉校验**所有基于事实的风格或元素引用需确保准确性梵高风格若不确定具体特征[需核实具体时期或作品特征]标注\n- **伦理与合规**自动过滤涉及现实人物肖像权争议暴力血腥成人内容特定商标版权等敏感描述若用户需求涉及潜在风险应引导至合规表达一个风格化的卡通英雄形象代替具体超级英雄\n- **聚焦提示词本身**不生成实际图像不解释AI绘画原理所有输出必须围绕生成更好的图像提示词这一核心任务\n- **清晰简洁**在保证信息完整的前提下提示词和说明应尽可能精炼避免冗长堆砌关键词', '1897481367743143938', '', NULL, 'enable', 1, '{\"modelInfo\":{\"provider\":\"DEEPSEEK\",\"modelType\":\"LLM\",\"modelName\":\"deepseek-chat\"},\"izDraw\":\"1\",\"drawModelId\":\"2008060119398899713\"}', NULL, NULL, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `airag_model` (`id`, `create_by`, `create_time`, `update_by`, `update_time`, `sys_org_code`, `tenant_id`, `name`, `provider`, `model_name`, `credential`, `base_url`, `model_type`, `model_params`, `activate_flag`) VALUES ('2008060119398899713', 'admin', '2026-01-05 14:16:55', 'admin', '2026-01-27 20:11:51', 'A05A01A01', NULL, '智普图片生成', 'ZHIPU', 'glm-image', '{\"apiKey\":\"76ca78587074479d8939a13\"}', 'https://open.bigmodel.cn', 'IMAGE', NULL, 1);

View File

@ -546,7 +546,7 @@
<dependency>
<groupId>org.jeecgframework.boot3</groupId>
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
<version>3.9.1</version>
<version>3.9.1.1</version>
</dependency>
<!--flyway 支持 mysql5.7+、MariaDB10.3.16-->
<!--mysql5.6需要把版本号改成5.2.1-->

View File

@ -25,8 +25,8 @@
:historyData="historyData"
type="view"
:formState="appData"
:prologue="appData.prologue"
:presetQuestion="appData.presetQuestion"
:prologue="appData?.prologue"
:presetQuestion="appData?.presetQuestion"
@reload-message-title="reloadMessageTitle"
:chatTitle="chatTitle"
:conversationSettings="getCurrentSettings"

View File

@ -108,6 +108,21 @@
name: '看图说话',
icon: 'https://jeecgdev.oss-cn-beijing.aliyuncs.com/temp/工具-图片解析_1743065064801.png',
prologue: '上传一张图片,我来为你讲述图片中的故事',
},
{
id: '2008448202536456193',
name: 'Chat2BI',
icon: 'https://minio.jeecg.com/otatest/chatShow_1769395642452.png',
prologue: '你好,我是图表生成智能体。',
flowId: '2008379264947519489',
type: 'chatFLow',
presetQuestion: '[{"key":1,"descr":"用户性别比例","update":true}]'
},
{
id: '2008090512835629057',
name: 'AI绘画',
icon: 'https://minio.jeecg.com/otatest/AiWrite_1769395779558.png',
prologue: '请输入文本,并选择图像生成,我来为你生成图片',
},
]);

View File

@ -16,7 +16,14 @@
</div>
</div>
<div class="prompt-left-textarea">
<div class="command">指令</div>
<div class="command">
<span style="margin-right: 5px">指令</span>
<a-tooltip title="提示词库">
<span @click="openPromptApps" style="color:#1890ff;cursor: pointer">
<Icon icon="ant-design:bulb-outlined" color="#1890ff"></Icon>词库选择
</span>
</a-tooltip>
</div>
<a-textarea v-model:value="prompt" :autoSize="{ minRows: 8, maxRows: 8 }"></a-textarea>
</div>
<a-button @click="generatedPrompt" class="prompt-left-btn" type="primary" :loading="loading">
@ -50,17 +57,21 @@
</div>
</BasicModal>
</div>
<!-- Ai提示词选择弹窗 -->
<AiAppPromptMarketModal @register="registerAiPromptSelectModal" @ok="handleAiAppPromptOk"></AiAppPromptMarketModal>
</template>
<script lang="ts">
import { ref, unref } from 'vue';
import BasicModal from '@/components/Modal/src/BasicModal.vue';
import { useModalInner } from '@/components/Modal';
import {useModal, useModalInner} from '@/components/Modal';
import { promptGenerate } from '@/views/super/airag/aiapp/AiApp.api';
import AiAppPromptMarketModal from "@/views/super/airag/aiapp/components/AiAppPromptMarketModal.vue";
export default {
name: 'AiAppGeneratedPrompt',
components: {
AiAppPromptMarketModal,
BasicModal,
},
emits: ['ok', 'register'],
@ -93,6 +104,8 @@
linux: '你是一个linux专家擅长解决各种linux相关的问题。',
content: '你是一个阅读理解大师,可以阅读用户提供的文章,并提炼主要内容输出给用户。',
});
//注册提示词modal
const [registerAiPromptSelectModal, { openModal: aiPromptSelectModalOpen }] = useModal();
//注册modal
const [registerModal, { closeModal, setModalProps }] = useModalInner(async (data) => {
content.value = '';
@ -197,6 +210,20 @@
closeModal();
}
/**
* 打开提示词库弹窗
*/
function openPromptApps() {
aiPromptSelectModalOpen(true,{});
}
/**
* 提示词回调
*
* @param value
*/
function handleAiAppPromptOk(value) {
content.value = value;
}
return {
registerModal,
handleOk,
@ -207,6 +234,9 @@
loading,
instructionsClick,
content,
openPromptApps,
registerAiPromptSelectModal,
handleAiAppPromptOk,
};
},
};
@ -250,6 +280,8 @@
.prompt-left-textarea {
margin-top: 25px;
.command {
display: flex;
align-items: center;
color: #101828;
line-height: 15px;
font-weight: 500;

View File

@ -36,7 +36,7 @@
</div>
</template>
<div class="card-content">
<p class="description">{{ item.description || item.desc }}</p>
<p class="description" :title="item.description || item.desc">{{ item.description || item.desc }}</p>
<div class="card-footer" >
<span class="create-time">
{{ formatTime(item.createTime) }}
@ -315,7 +315,7 @@
line-height: 1.5;
margin: 8px 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1; // 让描述区域自适应

View File

@ -81,8 +81,7 @@
<div style="align-items: center;display:flex;justify-content: center" v-if="!isRelease">
<a-button size="middle" ghost>
<span style="align-items: center;display:flex" @click="openPromptApps">
<Icon icon="ant-design:appstore-outlined"></Icon>
<span style="margin-left: 4px">模版</span>
<Icon icon="ant-design:database-outlined"></Icon>提示词库
</span>
</a-button>
<a-button size="middle" ghost>

View File

@ -91,14 +91,14 @@
<div class="variable-container">
<div class="variable-container-header">
<Icon icon="ant-design:file-text-outlined" class="output-format-icon" />
<span class="variable-format-title">评估器内容变量要求</span>
<span class="variable-format-title">评估器内容变量要求点击变量插入到评估器内容</span>
</div>
<div class="variable-container-content">
<div class="variable-tag-wrapper">
<a-tooltip title="评估的输入内容变量">
<a-tooltip title="评估的输入内容变量(必填)">
<a-tag color="blue" class="variable-tag required-tag" @click="handleTagClick('input')">input</a-tag>
</a-tooltip>
<a-tooltip title="评估的输出内容变量">
<a-tooltip title="评估的输出内容变量(必填)">
<a-tag color="blue" class="variable-tag required-tag" @click="handleTagClick('output')">output</a-tag>
</a-tooltip>
<a-tooltip title="评估的参考内容变量">
@ -141,7 +141,7 @@
</a-row>
</a-form>
<a-button v-if="showTest" class="mt-10 ml" style="float: right" @click="showTest = false">取消</a-button>
<a-button class="mt-10" style="float: right" @click="showTest = true" type="primary">调试</a-button>
<!-- <a-button class="mt-10" style="float: right" @click="showTest = true" type="primary">调试</a-button>-->
</a-col>
<a-col :span="12" class="setting-right" v-if="showTest">
<EvaluatorDebug ref="debugRef" :content="formState.dataValue" @run="debugRun"></EvaluatorDebug>
@ -180,7 +180,7 @@
//uuid
const uuid = ref(randomString(16));
//showTest 显示调试器
const showTest = ref(false);
const showTest = ref(true);
//debugRef 调试器引用
const debugRef = ref(null);
//form表单数据
@ -263,8 +263,10 @@
* @param type
*/
function handleTagClick(type) {
if(formState.dataValue){
let label = type=='input'?'## 输入参数':type=='output'?'## 输出参数':'## 参考参数';
let label = type=='input'?'## 输入参数':type=='output'?'## 输出参数':'## 参考参数';
if(!formState.dataValue){
formState.dataValue = `${label}{{${type}}}`;
}else{
formState.dataValue += `\r\n\r\n${label}{{${type}}}`;
// 获取textarea元素并滚动到底部
setTimeout(() => {
@ -390,6 +392,8 @@
debugRef.value.loading = false;
if(res.success){
debugRef.value.result = res.result
}else{
message.error(res.message);
}
console.log("debugEvaluator",res)
}

View File

@ -5,39 +5,112 @@
</div>
<div class="preview" ref="previewRef">
<div class="preview-header">
<span>预览</span>
<a-tooltip title="复制内容">
<CopyOutlined class="copy-btn" @click="handleCopy" />
</a-tooltip>
<div class="preview-header-left">
<span class="preview-title-text">{{ isEditing ? '编辑' : '预览' }}</span>
<a-select
v-if="historyData && historyData.length"
v-model:value="activeVersion"
size="small"
class="version-select"
@change="handleVersionChange"
>
<a-select-option :value="CURRENT_VERSION_KEY">当前内容</a-select-option>
<a-select-option v-for="item in historyData" :key="item.version" :value="item.version">
{{ item.version }}
</a-select-option>
</a-select>
<a-button
v-if="historyData && historyData.length && activeVersion !== CURRENT_VERSION_KEY"
type="link"
size="small"
class="preview-action-btn version-delete-btn"
@click="handleDeleteVersion"
>
<DeleteOutlined />
删除
</a-button>
</div>
<div class="preview-actions">
<a-button v-if="!generating" type="link" size="small" class="preview-action-btn custom-save-btn" @click="toggleEdit">
<FormOutlined v-if="!isEditing" />
<CheckOutlined v-else />
{{ isEditing ? '完成' : '编辑' }}
</a-button>
<a-button
v-if="!generating"
type="link"
size="small"
class="preview-action-btn custom-save-btn"
:loading="polishLoading"
@click="handlePolish"
>
<ThunderboltOutlined class="preview-actions-icon" style="position: relative; top: 1px" />
润色
</a-button>
<a-tooltip title="保存草稿">
<a-button v-if="!generating" type="link" size="small" class="preview-action-btn custom-save-btn" :loading="saving" @click="handleSave">
<SaveOutlined class="preview-actions-icon" />
保存
</a-button>
</a-tooltip>
<a-tooltip title="复制内容">
<a-button type="link" size="small" class="preview-action-btn custom-save-btn" @click="handleCopy">
<CopyOutlined class="preview-actions-icon" />
复制
</a-button>
</a-tooltip>
</div>
</div>
<div v-if="!isEditing" v-html="previewMd" class="markdown-container" @click="!generating" />
<div v-else class="markdown-editor-container">
<JMarkdownEditor v-model:value="writeText" height="100vh" :preview="{ mode: 'view', action: [] }" />
</div>
<a-spin :spinning="writerLoading">
<div v-html="previewMd" class="markdown-container" />
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick } from 'vue';
import { ref, computed, nextTick, onMounted } from 'vue';
import AiWriterLeft from './AiWriterLeft.vue';
import { defHttp } from '@/utils/http/axios';
import { CopyOutlined } from '@ant-design/icons-vue';
import { CopyOutlined, ThunderboltOutlined, FormOutlined, CheckOutlined, SaveOutlined, DeleteOutlined } from '@ant-design/icons-vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { copyTextToClipboard } from '/@/hooks/web/useCopyToClipboard';
import { Modal } from 'ant-design-vue';
import MarkdownIt from 'markdown-it';
import mdKatex from '@traptitech/markdown-it-katex';
import mila from 'markdown-it-link-attributes';
import hljs from 'highlight.js';
import JMarkdownEditor from '/@/components/Form/src/jeecg/components/JMarkdownEditor.vue';
import '/@/views/super/airag/aiapp/chat/style/github-markdown.less';
import '/@/views/super/airag/aiapp/chat/style/highlight.less';
import '/@/views/super/airag/aiapp/chat/style/style.less';
const { createMessage } = useMessage();
//加载
const writerLoading = ref<boolean>(false);
const writeText = ref<string>('');
const previewRef = ref<HTMLElement | null>(null);
//生成的loading
const generating = ref<boolean>(false);
//是否编辑
const isEditing = ref<boolean>(false);
//润色的loading
const polishLoading = ref<boolean>(false);
//保存loading
const saving = ref<boolean>(false);
//左侧的内容
const leftData = ref<any>();
//历史数据
const historyData = ref<any>([]);
//ai提示文本
const aiText = ref<string>('');
//当期版本的key
const CURRENT_VERSION_KEY = 'CURRENT';
//当前选中的本本
const activeVersion = ref<string>(CURRENT_VERSION_KEY);
//原始内容
const originalContent = ref<string | null>(null);
//第一个回复节点之后的内容
const afterNodeFinished = ref<boolean>(false);
const mdi = new MarkdownIt({
html: true,
linkify: true,
@ -55,13 +128,27 @@
//返回文本(生成中在末尾追加打点)
const previewMd = computed(() => {
const html = mdi.render(writeText.value);
let content = writeText.value || aiText.value;
if (generating.value) {
return html + '<span class=\"typing-dot\"></span><span class=\"typing-dot\"></span><span class=\"typing-dot\"></span>';
content +=
' <span class="typing-dot"></span><span class="typing-dot" style="animation-delay: 0.2s"></span><span class="typing-dot" style="animation-delay: 0.4s"></span>';
}
return html;
return mdi.render(content);
});
/**
* 编辑
*/
function toggleEdit() {
if (generating.value) {
return;
}
isEditing.value = !isEditing.value;
}
/**
* 复制
*/
function handleCopy() {
if (!writeText.value) {
createMessage.warning('暂无内容可复制');
@ -85,26 +172,35 @@
* 生成
*
* @param data
* @param type
*/
async function onGenerate(data) {
async function onGenerate(data, type = '') {
isEditing.value = false;
data.responseMode = 'streaming';
writerLoading.value = true;
writeText.value = '';
generating.value = true;
activeVersion.value = CURRENT_VERSION_KEY;
if (!type) {
leftData.value = data;
}
try {
let readableStream = await defHttp.post(
{
url: '/airag/chat/genAiWriter',
params: { ...data },
timeout: 5 * 60 * 1000,
adapter: 'fetch',
responseType: 'stream',
},
{
isTransformResponse: false,
}
);
let readableStream = await defHttp
.post(
{
url: '/airag/chat/genAiWriter',
params: { ...data },
timeout: 5 * 60 * 1000,
adapter: 'fetch',
responseType: 'stream',
},
{
isTransformResponse: false,
}
)
.catch((res) => {
createMessage.warn(res.message ? res.message : '请求出错,请稍后重试!');
});
const reader = readableStream.getReader();
const decoder = new TextDecoder('UTF-8');
@ -122,40 +218,37 @@
const lines = buffer.split('\n\n');
// 保留最后一个片段(可能不完整)
buffer = lines.pop() || '';
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine) continue;
if (trimmedLine.startsWith('data:')) {
const content = trimmedLine.replace('data:', '').trim();
if (content) {
renderText(content);
if (line.startsWith('data:')) {
const content = line.replace('data:', '').trim();
if(!content){
continue;
}
if(!content.endsWith('}')){
buffer = buffer + line;
continue;
}
buffer = "";
renderText(content)
} else {
// 尝试直接解析兼容非SSE格式或异常格式
renderText(trimmedLine);
if(!line) {
continue;
}
if(!line.endsWith('}')) {
buffer = buffer + line;
continue;
}
buffer = "";
renderText(line)
}
}
}
// 处理剩余的buffer
if (buffer && buffer.trim()) {
const trimmedLine = buffer.trim();
if (trimmedLine.startsWith('data:')) {
renderText(trimmedLine.replace('data:', '').trim());
} else {
renderText(trimmedLine);
}
}
} catch (e) {
console.error('Generation error:', e);
writeText.value += '\n\n[生成出错]';
} finally {
writerLoading.value = false;
// 若服务端结束未触发 MESSAGE_END兜底关闭生成状态
generating.value = false;
nextTick(scrollToBottom);
}
}
@ -167,25 +260,52 @@
function renderText(item) {
try {
let parse = JSON.parse(item);
if(parse.event == 'NODE_FINISHED'){
afterNodeFinished.value = true;
return;
}
if (parse.event == 'MESSAGE') {
writeText.value += parse.data.message;
if (writerLoading.value) {
writerLoading.value = false;
aiText.value = '';
if (afterNodeFinished.value) {
writeText.value = parse.data.message;
afterNodeFinished.value = false;
} else {
writeText.value += parse.data.message;
}
generating.value = true;
polishLoading.value = true;
nextTick(scrollToBottom);
}
if (parse.event == 'MESSAGE_END') {
writerLoading.value = false;
generating.value = false;
if (activeVersion.value === CURRENT_VERSION_KEY) {
originalContent.value = writeText.value;
}
nextTick(scrollToBottom);
}
if (parse.event == 'ERROR') {
writeText.value = parse.data.message ? parse.data.message : '生成失败,请稍后重试!';
writerLoading.value = false;
generating.value = false;
polishLoading.value = false;
nextTick(scrollToBottom);
}
//开始加点
if (parse.event === 'NODE_STARTED') {
if (!parse.data || parse.data.type !== 'end') {
if (parse.data.type === 'llm' || parse.data.type === 'reply') {
aiText.value = '正在构建响应内容';
}
}
}
//流程结束节点
if (parse.event == 'FLOW_FINISHED') {
if (parse.data && !parse.data.success) {
writeText.value = parse.data.message ? parse.data.message : '生成失败,请稍后重试!';
}
generating.value = false;
polishLoading.value = false;
}
} catch (error) {
console.log('Error parsing update:', error);
}
@ -199,6 +319,127 @@
function highlightBlock(str: string, lang?: string) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制代码</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`;
}
/**
* 润色
*/
async function handlePolish() {
if (!writeText.value) {
createMessage.warning('暂无内容可润色');
return;
}
if (generating.value || polishLoading.value) {
return;
}
polishLoading.value = true;
const data: any = {
prompt: writeText.value,
originalContent: '',
length: leftData.value.length,
format: leftData.value.format,
tone: leftData.value.tone,
language: leftData.value.language,
activeMode: 'polish',
};
try {
await onGenerate(data, 'polish');
} finally {
polishLoading.value = false;
}
}
/**
* 保存
*/
async function handleSave() {
if (!writeText.value) {
createMessage.warning('暂无内容可保存');
return;
}
if (saving.value) {
return;
}
try {
saving.value = true;
await defHttp.post({ url: '/airag/app/save/article/write', params: { content: writeText.value } });
if (activeVersion.value === CURRENT_VERSION_KEY) {
originalContent.value = writeText.value;
}
initHistoryData();
} catch (e) {
createMessage.error('保存失败,请稍后重试');
} finally {
saving.value = false;
}
}
/**
* 初始化历史信息
*/
function initHistoryData() {
historyData.value = [];
defHttp.get({ url: "/airag/app/list/article/write" }, { isTransformResponse: false }).then((res)=>{
if(res.success){
historyData.value = res.result;
}
})
}
/**
* 版本删除
*/
async function handleDeleteVersion() {
if (activeVersion.value === CURRENT_VERSION_KEY) {
return;
}
const target = historyData.value.find((item) => item.version === activeVersion.value);
if (!target) {
return;
}
Modal.confirm({
title: '删除版本',
content: '是否确认删除该版本?',
okText: '确定',
cancelText: '取消',
async onOk() {
try {
await defHttp.delete(
{ url: '/airag/app/delete/article/write', params: { version: target.version } },
{ joinParamsToUrl: true }
);
historyData.value = historyData.value.filter((item) => item.version !== target.version);
activeVersion.value = CURRENT_VERSION_KEY;
writeText.value = originalContent.value ?? '';
} catch (e) {
createMessage.error('删除失败,请稍后重试');
}
},
});
}
/**
* 版本切换事件
* @param value
*/
function handleVersionChange(value: string) {
if (value === CURRENT_VERSION_KEY) {
activeVersion.value = value;
writeText.value = originalContent.value ?? '';
return;
}
const target = historyData.value.find((item) => item.version === value);
if (!target) {
return;
}
activeVersion.value = value;
writeText.value = target.content;
}
onMounted(() => {
//初始化的时候加载历史版本
initHistoryData();
});
</script>
<style scoped lang="less">
@ -222,13 +463,15 @@
position: sticky;
top: 16px;
}
/*begin 头部样式 */
.preview {
flex: 1;
border: 1px solid #eef0f5;
border-radius: 12px;
padding: 12px 16px;
padding: 16px 20px;
overflow: auto;
background: #fafbff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
}
.preview-header {
@ -239,32 +482,94 @@
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eef0f5;
padding-bottom: 8px;
padding-bottom: 10px;
}
.preview-header-left {
display: flex;
align-items: center;
gap: 12px;
}
.copy-btn {
cursor: pointer;
color: #666;
font-size: 16px;
transition: color 0.3s;
.preview-title-text {
font-weight: 500;
letter-spacing: 0.5px;
}
&:hover {
color: #1890ff;
.preview-actions {
display: flex;
align-items: center;
gap: 8px;
}
.version-select {
min-width: 120px;
}
.preview-action-btn {
padding: 0 14px;
font-size: 12px;
border-radius: 4px;
height: 26px;
line-height: 24px;
}
.custom-green-btn {
background-color: @primary-color;
border-color: @primary-color;
color: #ffffff;
&:hover,
&:focus {
background-color: @primary-color;
border-color: @primary-color;
color: #ffffff;
}
}
.preview-actions-icon {
font-size: 12px;
color: white;
}
.custom-save-btn, .version-delete-btn {
background-color: #ffffff;
border-color: @primary-color;
color: @primary-color;
&:hover,
&:focus {
background-color: fade(@primary-color, 10%);
border-color: @primary-color;
color: @primary-color;
}
.preview-actions-icon {
color: @primary-color;
}
}
/*end 头部样式 */
/*begin 编辑器的样式*/
.markdown-container {
min-height: 300px;
/* 缩小图片宽度 */
padding: 8px 4px 16px 4px;
border-radius: 8px;
background-color: #fff;
cursor: text;
:deep(img) {
width: 60% !important;
width: 40% !important;
max-width: 280px;
height: auto;
border-radius: 6px;
display: inline-block;
display: block;
}
}
.typing-dot {
.markdown-editor-container {
min-height: 300px;
}
:deep(.typing-dot) {
display: inline-block;
width: 6px;
height: 6px;
@ -287,4 +592,5 @@
opacity: 1;
}
}
/*end 编辑器的样式*/
</style>