Commit f63ea6ae by pengshun

fix: 对话页面调整

parent 0d09257b
No preview for this file type
...@@ -9,7 +9,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowl ...@@ -9,7 +9,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowl
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime; import java.time.LocalDateTime;
...@@ -32,6 +34,9 @@ public interface AiChatAnalysisMapper extends BaseMapperX<AiChatAnalysisDO> { ...@@ -32,6 +34,9 @@ public interface AiChatAnalysisMapper extends BaseMapperX<AiChatAnalysisDO> {
.orderByDesc(AiChatAnalysisDO::getChatTime)); .orderByDesc(AiChatAnalysisDO::getChatTime));
} }
IPage<AiChatAnalysisDO> selectPageLatestByChatId(IPage<AiChatAnalysisDO> page,
@Param("reqVO") AiChatAnalysisPageReqVO reqVO);
default List<AiChatAnalysisDO> selectByChatId(String chatId) { default List<AiChatAnalysisDO> selectByChatId(String chatId) {
return selectList(new LambdaQueryWrapperX<AiChatAnalysisDO>() return selectList(new LambdaQueryWrapperX<AiChatAnalysisDO>()
.eqIfPresent(AiChatAnalysisDO::getChatId, chatId) .eqIfPresent(AiChatAnalysisDO::getChatId, chatId)
......
...@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; ...@@ -4,6 +4,8 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.analysis.*; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.analysis.*;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -87,7 +89,13 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService { ...@@ -87,7 +89,13 @@ public class AiChatAnalysisServiceImpl implements AiChatAnalysisService {
@Override @Override
public PageResult<AiChatAnalysisDO> getChatAnalysisPage(AiChatAnalysisPageReqVO pageReqVO) { public PageResult<AiChatAnalysisDO> getChatAnalysisPage(AiChatAnalysisPageReqVO pageReqVO) {
return chatAnalysisMapper.selectPageAll(pageReqVO); // return chatAnalysisMapper.selectPageAll(pageReqVO);
// 创建分页对象
Page<AiChatAnalysisDO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
// 执行自定义分组查询
IPage<AiChatAnalysisDO> pageResult = chatAnalysisMapper.selectPageLatestByChatId(page, pageReqVO);
// 封装返回
return new PageResult<>(pageResult.getRecords(), pageResult.getTotal());
} }
@Override @Override
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatAnalysisMapper">
<select id="selectPageLatestByChatId" resultType="cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatAnalysisDO">
SELECT a.*
FROM ai_chat_analysis a
INNER JOIN (
SELECT chat_id, MAX(chat_time) AS max_time
FROM ai_chat_analysis
<where>
<if test="reqVO.user != null and reqVO.user != ''">
AND `user` = #{reqVO.user}
</if>
<if test="reqVO.question != null and reqVO.question != ''">
AND question LIKE CONCAT('%', #{reqVO.question}, '%')
</if>
<if test="reqVO.chatTime != null and reqVO.chatTime.size() == 2">
AND chat_time BETWEEN #{reqVO.chatTime[0]} AND #{reqVO.chatTime[1]}
</if>
</where>
GROUP BY chat_id
) b ON a.chat_id = b.chat_id AND a.chat_time = b.max_time
ORDER BY a.chat_time DESC
</select>
</mapper>
\ No newline at end of file
...@@ -10,8 +10,6 @@ declare module 'vue' { ...@@ -10,8 +10,6 @@ declare module 'vue' {
AddNode: typeof import('./../components/SimpleProcessDesigner/src/addNode.vue')['default'] AddNode: typeof import('./../components/SimpleProcessDesigner/src/addNode.vue')['default']
AppLinkInput: typeof import('./../components/AppLinkInput/index.vue')['default'] AppLinkInput: typeof import('./../components/AppLinkInput/index.vue')['default']
AppLinkSelectDialog: typeof import('./../components/AppLinkInput/AppLinkSelectDialog.vue')['default'] AppLinkSelectDialog: typeof import('./../components/AppLinkInput/AppLinkSelectDialog.vue')['default']
'AutoComponents.d': typeof import('./auto-components.d.ts')['default']
'AutoImports.d': typeof import('./auto-imports.d.ts')['default']
Backtop: typeof import('./../components/Backtop/src/Backtop.vue')['default'] Backtop: typeof import('./../components/Backtop/src/Backtop.vue')['default']
CardTitle: typeof import('./../components/Card/src/CardTitle.vue')['default'] CardTitle: typeof import('./../components/Card/src/CardTitle.vue')['default']
ColorInput: typeof import('./../components/ColorInput/index.vue')['default'] ColorInput: typeof import('./../components/ColorInput/index.vue')['default']
......
...@@ -22,13 +22,21 @@ ...@@ -22,13 +22,21 @@
</div> </div>
<div class="message"> <div class="message">
<div> <div>
<el-text class="time">AI 助手 · 耗时 {{ item.duration || 0 }}s</el-text> <el-text class="time">AI 助手</el-text>
</div> </div>
<div class="left-text-container"> <div class="left-text-container">
<MarkdownView class="left-text" :content="item.answer || '思考中...'" /> <MarkdownView class="left-text" :content="item.answer || '思考中...'" />
</div> </div>
<!-- 新增工具栏:按钮 + 耗时文本,向左排列 -->
<div class="analysis-toolbar">
<el-button size="small" @click="toggleAnalysis(item.id)">
{{ analysisExpanded[item.id] ? '收起分析' : '查看分析' }}
</el-button>
<span class="time">响应耗时:{{ item.duration || 0 }}s</span>
</div>
<div class="analysis-dashboard mt-12px"> <div class="analysis-dashboard mt-12px" v-show="analysisExpanded[item.id]">
<div v-if="item.intentInsight" class="analysis-item"> <div v-if="item.intentInsight" class="analysis-item">
<el-tag type="info" effect="plain" size="small">意图洞察</el-tag> <el-tag type="info" effect="plain" size="small">意图洞察</el-tag>
<span class="analysis-content">{{ item.intentInsight }}</span> <span class="analysis-content">{{ item.intentInsight }}</span>
...@@ -45,15 +53,11 @@ ...@@ -45,15 +53,11 @@
<el-tag type="success" effect="plain" size="small">优化建议</el-tag> <el-tag type="success" effect="plain" size="small">优化建议</el-tag>
<div class="analysis-content suggest-box">{{ item.optimizeSuggest }}</div> <div class="analysis-content suggest-box">{{ item.optimizeSuggest }}</div>
</div> </div>
</div>
<div class="left-btns"> <!-- 空状态展示 -->
<el-button class="btn-cus" link @click="copyContent(item.answer)"> <div v-if="!item.intentInsight && (!item.reviewDiagnosis || item.reviewDiagnosis.length === 0) && !item.optimizeSuggest" class="analysis-empty">
<img class="btn-image" src="@/assets/ai/copy.svg" /> <el-empty description="暂无分析数据" :image-size="60" />
</el-button> </div>
<el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)">
<img class="btn-image h-17px" src="@/assets/ai/delete.svg" />
</el-button>
</div> </div>
</div> </div>
</div> </div>
...@@ -100,6 +104,26 @@ const props = defineProps({ ...@@ -100,6 +104,26 @@ const props = defineProps({
} }
}) })
// 新增:存储每个消息的分析面板展开状态,默认为 true
const analysisExpanded = ref<Record<number, boolean>>({})
// 切换展开/收起
const toggleAnalysis = (itemId: number) => {
analysisExpanded.value[itemId] = !analysisExpanded.value[itemId]
}
// 或者使用一个辅助函数获取状态(如果未初始化则默认为 true)
const isAnalysisExpanded = (itemId: number) => {
return analysisExpanded.value[itemId] ?? true
}
// 新增:批量切换所有分析面板的展开/收起状态
const toggleAllAnalysis = (expanded: boolean) => {
// 遍历 list 中的每个 item,设置其展开状态
list.value.forEach((item: ChatAnalysisVO) => {
analysisExpanded.value[item.id] = expanded
})
}
const { list } = toRefs(props) const { list } = toRefs(props)
const emits = defineEmits(['onDeleteSuccess']) const emits = defineEmits(['onDeleteSuccess'])
...@@ -131,7 +155,7 @@ const handlerGoTop = () => { ...@@ -131,7 +155,7 @@ const handlerGoTop = () => {
messageContainer.value.scrollTop = 0 messageContainer.value.scrollTop = 0
} }
defineExpose({ scrollToBottom, handlerGoTop }) defineExpose({ scrollToBottom, handlerGoTop, toggleAllAnalysis })
/** 操作 */ /** 操作 */
const copyContent = async (content: string | undefined) => { const copyContent = async (content: string | undefined) => {
...@@ -148,6 +172,9 @@ const onDelete = async (id: number) => { ...@@ -148,6 +172,9 @@ const onDelete = async (id: number) => {
onMounted(() => { onMounted(() => {
messageContainer.value?.addEventListener('scroll', handleScroll) messageContainer.value?.addEventListener('scroll', handleScroll)
if (messageContainer.value) {
messageContainer.value.scrollTop = 0
}
}) })
</script> </script>
...@@ -233,4 +260,17 @@ onMounted(() => { ...@@ -233,4 +260,17 @@ onMounted(() => {
right: 20px; right: 20px;
z-index: 10; z-index: 10;
} }
.analysis-toolbar {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
margin-top: 8px; // 与上方 left-text-container 保持间距
margin-bottom: 4px; // 与下方分析面板保持适当间距(mt-12px 已提供)
.time {
font-size: 12px;
color: #999;
line-height: 24px;
}
}
</style> </style>
\ No newline at end of file
...@@ -12,8 +12,9 @@ ...@@ -12,8 +12,9 @@
<!-- 右侧:对话详情 --> <!-- 右侧:对话详情 -->
<el-container class="detail-container"> <el-container class="detail-container">
<el-header class="header"> <el-header class="header">
<div class="title"> <div class="header-left">
{{ activeConversation?.user ? '用户:' + activeConversation?.user : '对话' }} <el-button size="small" @click="handleExpandAllAnalysis">一键展开</el-button>
<el-button size="small" @click="handleCollapseAllAnalysis">一键收起</el-button>
</div> </div>
</el-header> </el-header>
...@@ -98,6 +99,23 @@ const enableContext = ref<boolean>(true) // 是否开启上下文 ...@@ -98,6 +99,23 @@ const enableContext = ref<boolean>(true) // 是否开启上下文
const receiveMessageFullText = ref('') const receiveMessageFullText = ref('')
const receiveMessageDisplayedText = ref('') const receiveMessageDisplayedText = ref('')
// 获取子组件 ref
// const messageRef = ref<InstanceType<typeof MessageList>>()
// 一键展开所有分析面板
const handleExpandAllAnalysis = () => {
if (messageRef.value) {
messageRef.value.toggleAllAnalysis(true)
}
}
// 一键收起所有分析面板
const handleCollapseAllAnalysis = () => {
if (messageRef.value) {
messageRef.value.toggleAllAnalysis(false)
}
}
// =========== 【聊天对话】相关 =========== // =========== 【聊天对话】相关 ===========
/** 获取对话信息 */ /** 获取对话信息 */
...@@ -127,7 +145,8 @@ const handleConversationClick = async (conversation: ChatAnalysisVO) => { ...@@ -127,7 +145,8 @@ const handleConversationClick = async (conversation: ChatAnalysisVO) => {
// 刷新 message 列表 // 刷新 message 列表
await getMessageList() await getMessageList()
// 滚动底部 // 滚动底部
scrollToBottom(true) // scrollToBottom(true)
scrollToTop(true)
// 清空输入框 // 清空输入框
prompt.value = '' prompt.value = ''
return true return true
...@@ -193,7 +212,7 @@ const getMessageList = async () => { ...@@ -193,7 +212,7 @@ const getMessageList = async () => {
// 滚动到最下面 // 滚动到最下面
await nextTick() await nextTick()
await scrollToBottom() // await scrollToBottom()
} finally { } finally {
// time 定时器,如果加载速度很快,就不进入加载中 // time 定时器,如果加载速度很快,就不进入加载中
if (activeMessageListLoadingTimer.value) { if (activeMessageListLoadingTimer.value) {
...@@ -442,6 +461,13 @@ const scrollToBottom = async (isIgnore?: boolean) => { ...@@ -442,6 +461,13 @@ const scrollToBottom = async (isIgnore?: boolean) => {
} }
} }
const scrollToTop = async (isIgnore?: boolean) => {
await nextTick()
if (messageRef.value) {
messageRef.value.handlerGoTop()
}
}
/** 自提滚动效果 */ /** 自提滚动效果 */
const textRoll = async () => { const textRoll = async () => {
let index = 0 let index = 0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment