Commit 9a0d119d authored by 赵灿灿's avatar 赵灿灿

新增融合页面

parent ab5f9df9
Pipeline #22119 failed with stages
in 3 minutes and 54 seconds
...@@ -877,4 +877,114 @@ public class LangChainController { ...@@ -877,4 +877,114 @@ public class LangChainController {
.body(emitter); .body(emitter);
} }
//内部专家外部专家融合智能体
@GetMapping("/sseFusionIntelligent")
public ResponseEntity<SseEmitter> sseFusionIntelligent(@RequestParam String chatMessage,@RequestParam String dialogId,
@RequestParam String selectedExpert ,@RequestParam String selectedOrg) {
String condition="";
String regionName= chatService.getAuthRegionName();
// if("内部专家".equals(selectedExpert)) {
// if ("组织内".equals(selectedOrg) && StringUtils.isNotEmpty(regionName)) {
// condition = ",只能查找" + regionName;
// } else if ("组织外".equals(selectedOrg) && StringUtils.isNotEmpty(regionName)) {
// condition = ",排除" + regionName;
// }
// }
String urlAddr = "http://10.32.41.228:44501/scene_gateway/agent/open/5386f9144ee042578d1c0f66c2598d06";
//String urlAddr = chatService.getUrl(selectedExpert);
//String urlAddr = "http://10.32.41.35:40517/scene_gateway/agent/83f77143b09c461993dd9a7db403eb94";
SseEmitter emitter = new SseEmitter(0L);
QuestionRequest questionRequest=new QuestionRequest();
questionRequest.setKeyword(chatMessage+condition);
questionRequest.setRequestId(getRequestId());
questionRequest.setDialogId(dialogId);
Conversations conversations=chatService.saveConversations(dialogId,chatMessage,selectedExpert);
Messages messagesQusetion=new Messages();
messagesQusetion.setRequestId(dialogId);
messagesQusetion.setContent(chatMessage);
messagesQusetion.setRole("user");
messagesQusetion.setModelName("qwen2.5-72b");
messagesQusetion=chatService.insertQuestionMessage(conversations,messagesQusetion);
Messages messagesContent=new Messages();
messagesContent.setRole("assistant");
messagesContent.setModelName("qwen2.5-72b");
messagesContent.setParentMsgId(messagesQusetion.getId());
messagesContent.setSort(messagesQusetion.getSort()+1);
chatService.insertMessage(conversations,messagesContent);
String params = JsonUtils.objectToJson(questionRequest);
StringBuffer content =new StringBuffer();
StringBuffer lineCotent =new StringBuffer();
new Thread(() -> {
HttpURLConnection connection = null;
try {
URL url = new URL(urlAddr);
// 建立链接
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("AuthToken", "4009fe23e6b648539792330c14f5ed8e");
// connection.setRequestProperty("AuthToken", "fc40db5b7abe47dabfe1899e61fde2d7");
// 允许输入和输出
connection.setDoInput(true);
connection.setDoOutput(true);
// 设置超时为0,表示无限制
connection.setConnectTimeout(0);
connection.setReadTimeout(0);
try (OutputStream os = connection.getOutputStream()) {
os.write(params.getBytes(StandardCharsets.UTF_8));
os.flush();
}
// 检查响应码
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
emitter.completeWithError(new RuntimeException("SSE 连接失败: " + responseCode));
return;
}
// 持续读取 SSE 数据流
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
logger.info(line);
if (!line.startsWith("data:CALLBACK#")) {
if (line.startsWith("data:")) {
String data = line.substring(5).trim();
if("stop".equals(data))
{
emitter.send(SseEmitter.event().data("stop"),MediaType.parseMediaType("application/json; charset=UTF-8"));
}else
{
lineCotent.append(data);
String sendData=data.replace("attachment#[]#attachment","").replace("source#[]#source","")
.replace("#","").replace("*","");
emitter.send(SseEmitter.event().data(sendData), MediaType.parseMediaType("application/json; charset=UTF-8"));
if(!sendData.startsWith("SUGGEST["))
content.append(sendData);
}
}
}
}
}
emitter.complete(); // 流结束
} catch (Exception e) {
emitter.completeWithError(e);
logger.info(e.getMessage());
} finally {
messagesContent.setContent(content.toString());
chatService.updateMessage(messagesContent);
if (connection != null) {
connection.disconnect();
}
logger.info(lineCotent.toString());
}
}).start();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_EVENT_STREAM_VALUE)
.body(emitter);
}
} }
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>智能推荐专家</title>
<link rel="stylesheet" href="style/ai-chat.css">
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app" class="main-container" :class="{'dark-theme': isDarkTheme}" v-cloak>
<!-- 左侧历史对话列表 -->
<div class="sidebar">
<div class="sidebar-header">
<h1>智能推荐专家</h1>
<!-- 添加主题切换按钮 -->
<div id="theme-toggle" class="theme-toggle" title="切换主题" @click="toggleTheme">
<svg id="light-icon" viewBox="0 0 24 24" :style="{display: isDarkTheme ? 'block' : 'none'}">
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"></path>
</svg>
<svg id="dark-icon" viewBox="0 0 24 24" :style="{display: isDarkTheme ? 'none' : 'block'}">
<path d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path>
</svg>
</div>
<button id="new-chat-btn" class="new-chat-btn" @click="newChat">
<span class="plus-icon">+</span> 新对话
</button>
</div>
<div class="history-list">
<div v-for="(section, sectionIndex) in historySections" :key="sectionIndex" class="history-section">
<div class="time-label">{{ section.label }}</div>
<div v-for="(item, itemIndex) in section.items"
:key="item.id"
class="history-item"
:class="{ active: item.active }"
:data-id="item.id" @click="loadChatHistory(item)">
<div class="history-title" >{{ item.title }}</div>
<button class="delete-btn" @click.stop="deleteChat(item.id)">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"></path>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- 右侧聊天区域 -->
<div class="chat-area">
<!-- 消息历史区域 -->
<div id="scrollContainer" class="scrollContainer">
<div class="fist-loading" v-if="fistLoading">
<p>快速查找专家</p>
<div class="expert-guides-container">
<div class="expert-guides-scroll" :style="scrollStyle">
<div v-for="(item, index) in extendedGuides" :key="index" class="expert-guide-item" @click="questionClick(item)">
<p>{{ item }}</p>
</div>
</div>
</div>
</div>
<div id="chat-messages" class="chat-messages" v-if="!fistLoading">
<div v-for="(message, index) in messages"
:key="index"
class="message"
:class="message.role + '-message'">
<div class="avatar">{{ message.role === 'user' ? '我' : 'AI' }}</div>
<div class="content">
<p v-html="message.content"></p><!--v-html可能会有xss攻击,但是数据来源于大模型,是否需要清洗数据然后再显示?-->
<div v-if="message.typing" class="typing-indicator"></div>
</div>
</div>
<div class="question">
<div class="question_left"></div>
<div v-if="questions.length" class="question_right">
<p>您可以继续问我: </p>
<div class="questionContent" v-for="(q, i) in questions" :key="i" @click="questionClick(q)">
<p>{{ q }}</p>
</div>
</div>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<textarea id="user-input" v-model="userInput" placeholder="请输入您的问题 shift+enter换行" rows="3"></textarea>
<div class="input-area-content">
<div style="display: flex;align-items: center;justify-content: flex-start;">
<div class="custom-select" v-click-outside="closeExpertDropdown" style="display: none">
<div class="selected-option" @click.stop="selectExpert('内部专家')"
:class="{ 'active': selectedExpert === '内部专家' }">
<span>内部专家</span>
<!-- <svg class="dropdown-icon" :class="{ 'rotated': showExpertDropdown }" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">-->
<!-- <polyline points="6 9 12 15 18 9"></polyline>-->
<!-- </svg>-->
</div>
<!-- <div class="dropdown-menu" v-show="showExpertDropdown">-->
<!-- <div class="dropdown-item" @click.stop="selectExpert('内部专家')" :class="{ 'active': selectedExpert === '内部专家' }">-->
<!-- <span>内部专家</span>-->
<!-- </div>-->
<!--&lt;!&ndash; <div class="dropdown-item" @click.stop="selectExpert('外部专家')" :class="{ 'active': selectedExpert === '外部专家' }">&ndash;&gt;-->
<!--&lt;!&ndash; <span>外部专家</span>&ndash;&gt;-->
<!--&lt;!&ndash; </div>&ndash;&gt;-->
<!-- </div>-->
</div>
<div class="custom-select" v-click-outside="closeExpertDropdown" style="display: none">
<div class="selected-option" @click.stop="selectExpert('外部专家')"
:class="{ 'active': selectedExpert === '外部专家' }">
<span> 外部专家 </span>
</div>
</div>
<div class="custom-select" v-click-outside="closeOrgDropdown" v-show="showOrgSelection" style="display: none">
<!-- <div class="selected-option" @click.stop="toggleOrgDropdown" style="background-color: rgba(230, 220, 250, 0.5);color: #6633ff;">-->
<!-- <span>{{ selectedOrg }}</span>-->
<!-- <svg class="dropdown-icon" :class="{ 'rotated': showOrgDropdown }" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">-->
<!-- <polyline points="6 9 12 15 18 9"></polyline>-->
<!-- </svg>-->
<!-- </div>-->
<!-- <div class="dropdown-menu" v-show="showOrgDropdown">-->
<!-- <div class="dropdown-item" @click.stop="selectOrg('全部组织')" :class="{ 'active': selectedOrg === '全部组织' }">-->
<!-- <span>全部</span>-->
<!-- </div>-->
<!-- <div class="dropdown-item" @click.stop="selectOrg('组织内')" :class="{ 'active': selectedOrg === '组织内' }">-->
<!-- <span>组织内</span>-->
<!-- </div>-->
<!-- <div class="dropdown-item" @click.stop="selectOrg('组织外')" :class="{ 'active': selectedOrg === '组织外' }">-->
<!-- <span>组织外</span>-->
<!-- </div>-->
<!-- </div>-->
</div>
</div>
<div>
<button id="send-btn" class="send-btn" @click="sendMessage" v-show="!isResponding">
<svg class="send-icon" viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
<button id="stop-btn" class="stop-btn" @click="stopResponse" v-show="isResponding">
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none"
stroke-linecap="round" stroke-linejoin="round" class="stop-icon">
<rect x="6" y="6" width="12" height="12" rx="2" ry="2"></rect>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" src="../../libs/require.js/require.min.js"></script>
<script type="text/javascript" src="../../scripts/require-config.js"></script>
<!-- 引入Vue相关JS -->
<script type="text/javascript" src="js/ai-chat-vue.js"></script>
</body>
</html>
\ No newline at end of file
/**
* AI聊天页面Vue应用
*/
require(['jquery', 'vue', 'utils','marked','markdown', 'global'], function ($, Vue, utils, marked,markdown) {
// 添加点击外部关闭指令
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
el.clickOutsideEvent = function (event) {
if (!(el == event.target || el.contains(event.target))) {
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent);
},
unbind: function (el) {
document.body.removeEventListener('click', el.clickOutsideEvent);
}
});
const app = new Vue({
el: '#app',
data: {
// 用户相关数据
currentLoginUser: {},
fistLoading: true,
// 聊天相关数据
chatHistory: [],
sessionId: "",
currentEventSource: null,
// UI相关数据
theme: localStorage.getItem('ai-chat-theme') || 'light',
isResponding: false,
userInput: "",
// 专家选择相关数据
selectedExpert: '内部专家',
showExpertDropdown: false,
// 组织内网选择相关数据
selectedOrg:'全部组织',
showOrgDropdown: false,
questions:[],
guides:[
'请帮我推荐几位南京地区的网络规划方面的专家',
'推荐既懂运维规划又熟悉移动产品体系的专家',
'我需要私有云多中心容灾规划领域的专家',
'帮我找几位熟悉5G网络优化的专家',
'推荐几位通信网基础设施资源清查方面的专家',
'请帮我找工业互联网平台研发与部署的专家'
],
scrollOffset: 0,
scrollInterval: null,
transitioning: true,
// 历史对话分类
historySections: [
],
// 消息列表
messages: [
// { role: 'ai', content: '您好!我是您的专家推荐助手,有什么可以帮助您的吗?' }
],
showOrgSelection: true
},
computed: {
isDarkTheme() {
return this.theme === 'dark';
},
extendedGuides() {
// 克隆前3项到末尾
return [...this.guides, ...this.guides.slice(0, 3)];
},
scrollStyle() {
return {
transform: `translateY(-${this.scrollOffset}px)`,
transition: this.transitioning ? 'transform 0.5s ease' : 'none',
};
}
},
created: function () {
this.currentUser();
this.getSessionId();
this.getConversationHistory();
},
mounted: function () {
// 监听输入框的键盘事件
this.$nextTick(() => {
const textarea = document.getElementById('user-input');
if (textarea) {
textarea.addEventListener('keypress', this.handleKeyPress);
}
// 启动自动滚动
this.startAutoScroll();
// 添加鼠标悬停事件监听
this.$watch('fistLoading', (newVal) => {
if (newVal) {
// 等待DOM更新
this.$nextTick(() => {
const guideItems = document.querySelectorAll('.expert-guide-item');
guideItems.forEach(item => {
item.addEventListener('mouseenter', this.pauseAutoScroll);
item.addEventListener('mouseleave', this.resumeAutoScroll);
});
});
}
}, { immediate: true });
});
},
beforeDestroy: function() {
// 清除自动滚动定时器
this.stopAutoScroll();
// 移除事件监听器
const guideItems = document.querySelectorAll('.expert-guide-item');
guideItems.forEach(item => {
item.removeEventListener('mouseenter', this.pauseAutoScroll);
item.removeEventListener('mouseleave', this.resumeAutoScroll);
});
},
methods: {
// 自动滚动相关方法
startAutoScroll: function() {
const itemHeight = 80;
const visibleItems = 3;
const totalItems = this.guides.length;
this.scrollInterval = setInterval(() => {
this.transitioning = true;
this.scrollOffset += itemHeight;
// 判断是否滚动到克隆项部分(即第 totalItems 行之后)
if (this.scrollOffset >= itemHeight * (totalItems)) {
// 短暂延迟后瞬间回到原始位置
setTimeout(() => {
this.transitioning = false;
this.scrollOffset = 0;
}, 500); // 等待动画结束
}
}, 2000);
},
stopAutoScroll: function() {
if (this.scrollInterval) {
clearInterval(this.scrollInterval);
this.scrollInterval = null;
}
},
pauseAutoScroll: function() {
this.stopAutoScroll();
},
resumeAutoScroll: function() {
if (!this.scrollInterval && this.fistLoading) {
this.startAutoScroll();
}
},
// 用户相关方法
currentUser: function () {
const that = this;
$.ajax({
url: "../../auth/current-user",
type: "get",
dataType: "json",
contentType: "application/json;charset=UTF-8",
async: false,
success: function (data) {
that.currentLoginUser = data.data;
}
});
},
// 主题相关方法
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
localStorage.setItem('ai-chat-theme', this.theme);
document.body.classList.toggle('dark-theme', this.theme === 'dark');
},
// 会话相关方法
async getSessionId() {
try {
//const response = await fetch('../../api/langchain/getSessionId');
const response = await fetch('../../api/langchain/getDialogId');
const dataJson = await response.json();
this.sessionId = dataJson.data;
} catch (error) {
console.error('会话ID获取失败', error);
}
},
async getConversationHistory() {
try {
//const response = await fetch('../../api/langchain/getSessionId');
const response = await fetch('../../api/langchain/conversationHistory');
const dataJson = await response.json();
this.historySections = dataJson.data;
} catch (error) {
console.error('会话获取失败', error);
}
},
// 聊天相关方法
handleKeyPress(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
},
questionClick:function(message){
this.userInput = message;
this.sendMessage();
},
sendMessage() {
if (!this.selectedExpert) {
this.showToast('请先选择专家类型(内部/外部专家)');
return;
}
const message = this.userInput.trim();
if (!message) return;
if (this.fistLoading) {
this.fistLoading = false;
// 停止自动滚动
this.stopAutoScroll();
}
this.questions= [];
this.stopResponse();
// 添加用户消息
this.addMessage('user', message);
this.userInput = '';
// 显示终止按钮,隐藏发送按钮
this.isResponding = true;
// 调用API获取响应
this.connectSSE(message);
},
stopResponse() {
if (this.currentEventSource) {
this.currentEventSource.close();
this.currentEventSource = null;
// 切换回发送按钮
this.isResponding = false;
// 在最后一条AI消息后添加"(已终止)"标记
const aiMessages = this.messages.filter(msg => msg.role === 'ai');
if (aiMessages.length > 0) {
const lastAIMessage = aiMessages[aiMessages.length - 1];
lastAIMessage.typing = false;
if (!lastAIMessage.content.includes('(已手动结束回答)')) {
lastAIMessage.content += ' (已手动结束回答)';
}
}
}
},
addMessage(role, content) {
this.messages.push({ role, content });
this.chatHistory.push({ role, content });
// 自动滚动到底部
this.$nextTick(() => {
const messagesDiv = document.getElementById('scrollContainer');
if (messagesDiv) {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
});
},
connectSSE(chatMessage) {
// 在消息列表中添加一个带"正在输入"指示器的AI消息
const aiMessageIndex = this.messages.length;
this.messages.push({ role: 'ai', content: '', typing: true });
// 自动滚动到底部
this.$nextTick(() => {
const messagesDiv = document.getElementById('scrollContainer');
if (messagesDiv) {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
});
// 连接SSE
this.currentEventSource = new EventSource('../../api/langchain/sseFusionIntelligent?chatMessage='+chatMessage+"&dialogId="+this.sessionId
+"&selectedExpert="+this.selectedExpert+"&selectedOrg="+this.selectedOrg);
let responseText = '';
var md = new markdown({
html: true, // 允许解析 HTML 标签
linkify: true, // 自动识别链接
typographer: true, // 启用排版优化
breaks: true, // 将单个换行符视为换行
});
this.currentEventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
const content = data.dataToSend[1].data;
if (content !== "stop") {
const match = content.match(/SUGGEST\[(.*?)\]SUGGEST/);
if (match) {
const suggestionsJson = `[${match[1]}]`;
const suggestions = JSON.parse(suggestionsJson);
if (Array.isArray(suggestions)) {
this.questions = suggestions;
}
}else{//提示词不展示
responseText += content;
}
this.messages[aiMessageIndex].content = marked.parse(responseText);//md.render(responseText);
} else {
// 移除输入指示器
this.messages[aiMessageIndex].typing = false;
this.chatHistory.push({ role: 'ai', content: responseText });
// 切换回发送按钮
this.isResponding = false;
// 关闭事件源
this.currentEventSource.close();
this.currentEventSource = null;
}
// 自动滚动到底部
this.$nextTick(() => {
const messagesDiv = document.getElementById('scrollContainer');
if (messagesDiv) {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
});
};
this.currentEventSource.onerror = () => {
console.error('SSE连接异常');
// 移除输入指示器,显示错误信息
this.messages[aiMessageIndex].typing = false;
this.messages[aiMessageIndex].content = responseText || '抱歉,连接出现问题,请稍后再试。';
// 切换回发送按钮
this.isResponding = false;
// 关闭事件源
this.currentEventSource.close();
this.currentEventSource = null;
};
},
clearChat() {
// 清空消息,保留欢迎消息
this.messages = [
{ role: 'ai', content: '您好!我是您的AI助手,有什么可以帮助您的吗?' }
];
// 清空历史记录
this.chatHistory = [];
},
newChat() {
// 终止当前响应(如果有)
if (this.currentEventSource) {
this.currentEventSource.close();
this.currentEventSource = null;
this.isResponding = false;
}
this.questions = [];
// 获取新的会话ID并清空对话
this.getSessionId().then(() => {
this.clearChat();
// 这里可以添加将新对话添加到历史列表的逻辑
// this.addChatToHistory('新对话', new Date());
});
this.fistLoading=true;
this.selectedExpert = '内部专家';
this.showOrgSelection = this.selectedExpert === '内部专家';
this.selectedOrg = '全部组织';
// 重置滚动位置并重新启动自动滚动
this.scrollOffset = 0;
this.startAutoScroll();
},
loadChatHistory(chat) {
var that=this
var chatId=chat.id;
// 关闭所有菜单
this.historySections.forEach(section => {
section.items.forEach(item => {
item.showMenu = false;
});
});
//this.addMessage('user', "123456");
// 设置活跃状态
this.historySections.forEach(section => {
section.items.forEach(item => {
item.active = (item.id === chatId);
});
});
//
this.sessionId = chatId;
// this.clearChat();
this.questions= [];
this.messages=[];
// 如果正在显示初始推荐,停止自动滚动
if (this.fistLoading) {
this.stopAutoScroll();
}
this.fistLoading = false;
this.selectedExpert = '内部专家';
this.showOrgSelection = this.selectedExpert === '内部专家';
this.selectedOrg = '全部组织';
//这里添加加载对应对话历史的逻辑
//实际实现时可以调用API获取历史记录
$.ajax({
url: "../../api/langchain/conversationMessages?sessionId="+chatId,
type: "get",
dataType: "json",
contentType: "application/json;charset=UTF-8",
async: false,
success: function (data) {
var conversationMessages = data.data;
if (conversationMessages && conversationMessages.length > 0){
that.fistLoading = false;
conversationMessages.forEach(section => {
that.addMessage(section.role, section.content);
});
}
}
});
},
// 删除对话
deleteChat(chatId) {
// 关闭菜单
this.historySections.forEach(section => {
const item = section.items.find(item => item.id === chatId);
if (item) {
item.showMenu = false;
}
});
// 从历史记录中删除
this.historySections.forEach(section => {
section.items = section.items.filter(item => item.id !== chatId);
});
// 如果删除的是当前活跃的对话,则清空聊天记录
const isActiveChat = this.historySections.some(section =>
section.items.some(item => item.id === chatId && item.active)
);
if (isActiveChat) {
this.clearChat();
}
$.ajax({
url: "../../api/langchain/deleteChat",
type: "post",
dataType: "json",
contentType: "application/json;charset=UTF-8",
data: chatId,
async: false,
success: function (data) {
}
});
},
// 专家选择相关方法
toggleExpertDropdown() {
this.showExpertDropdown = !this.showExpertDropdown;
},
closeExpertDropdown() {
this.showExpertDropdown = false;
},
selectExpert(expert) {
// this.selectedExpert = expert;
if (this.selectedExpert === expert) {
this.selectedExpert = '';
} else {
this.selectedExpert = expert;
}
this.showOrgSelection = expert === '内部专家';
this.selectedOrg = '全部组织';
// 立即关闭下拉框
this.showExpertDropdown = false;
},
//组织内外选择相关方法
toggleOrgDropdown() {
this.showOrgDropdown = !this.showOrgDropdown;
},
closeOrgDropdown() {
this.showOrgDropdown = false;
},
selectOrg(org) {
this.selectedOrg = org;
// 立即关闭下拉框
this.showOrgDropdown = false;
},
showToast(message) {
// 使用你项目中已有的提示组件,或创建一个简单的提示
const toast = document.createElement('div');
toast.className = 'expert-toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
}
});
});
\ No newline at end of file
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
:root {
/* 浅色主题变量 */
--bg-color: #f5f7fa;
--sidebar-bg: #fff;
--sidebar-border: #e8e8e8;
--primary-color: #1890ff;
--primary-hover: #40a9ff;
--text-color: #333;
--text-secondary: #8c8c8c;
--message-bg: #fff;
--message-user-bg: #f5f7fa;
--message-border: #e8e8e8;
--input-area-bg: #fff;
--hover-bg: #f0f2f5;
--active-bg: #e6f7ff;
--avatar-bg: #1890ff;
--avatar-user-bg: #722ed1;
--scrollbar-track: #f1f1f1;
--scrollbar-thumb: #c1c1c1;
--scrollbar-hover: #a8a8a8;
--box-shadow: rgba(0, 0, 0, 0.08);
--question-text: #676c90;
}
.dark-theme {
/* 深色主题变量 */
--bg-color: #1f1f1f;
--sidebar-bg: #2d2d2d;
--sidebar-border: #3a3a3a;
--primary-color: #177ddc;
--primary-hover: #3c9ae8;
--text-color: #e0e0e0;
--text-secondary: #aaaaaa;
--message-bg: #2d2d2d;
--message-user-bg: #363636;
--message-border: #404040;
--input-area-bg: #363636;
--hover-bg: #404040;
--active-bg: #1a365d;
--avatar-bg: #177ddc;
--avatar-user-bg: #722ed1;
--scrollbar-track: #2d2d2d;
--scrollbar-thumb: #555555;
--scrollbar-hover: #777777;
--box-shadow: rgba(0, 0, 0, 0.2);
--question-text: #dbdef8;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
height: 100vh;
overflow: hidden;
}
/* 主容器样式 */
.main-container {
display: flex;
height: 100vh;
width: 100%;
overflow: hidden;
}
/* 左侧边栏样式 */
.sidebar {
width: 280px;
background-color: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 16px;
border-bottom: 1px solid var(--sidebar-border);
position: relative;
}
.sidebar-header h1 {
font-size: 1.2rem;
font-weight: 600;
color: var(--primary-color);
margin-bottom: 16px;
}
.new-chat-btn {
width: 100%;
padding: 10px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
transition: background-color 0.3s;
}
.new-chat-btn:hover {
background-color: var(--primary-hover);
}
.plus-icon {
margin-right: 8px;
font-size: 16px;
font-weight: bold;
}
/* 主题切换按钮 */
.theme-toggle {
position: absolute;
right: 16px;
top: 16px;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s;
}
.theme-toggle:hover {
background-color: var(--hover-bg);
}
.theme-toggle svg {
width: 18px;
height: 18px;
fill: var(--text-secondary);
}
/* 历史对话列表样式 */
.history-list {
flex: 1;
overflow-y: auto;
padding: 10px;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
max-height: calc(100vh - 90px);
}
.history-section {
margin-bottom: 16px;
}
.time-label {
font-size: 0.8rem;
color: var(--text-secondary);
padding: 5px 10px;
margin-bottom: 5px;
}
.history-item {
padding: 10px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.history-item:hover {
background-color: var(--hover-bg);
}
.history-item.active {
background-color: var(--active-bg);
border-left: 3px solid var(--primary-color);
}
.history-title {
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
color: var(--text-color);
}
.delete-btn {
background: none;
border: none;
cursor: pointer;
padding: 5px;
opacity: 0.5;
transition: opacity 0.2s;
color: red;
}
.delete-btn:hover {
opacity: 1;
}
.delete-btn svg {
display: block;
}
.history-time {
font-size: 0.75rem;
color: var(--text-secondary);
}
/* 右侧聊天区域 */
.fist-loading{
width: 900px;
margin: 8% auto 10px;
}
.fist-loading>p{
color: var(--primary-color);
font-weight: bold;
font-size: 20px;
width: 500px;
margin:0 auto;
}
.expert-guides-container {
width: 500px;
height: 240px; /* 高度为每条推荐项高度的3倍 */
margin: 0 auto;
overflow: hidden;
position: relative;
}
.expert-guides-scroll {
width: 100%;
position: absolute;
transition: transform 0.5s ease;
}
.expert-guide-item {
width: 500px;
height: 70px;
line-height: 70px;
border-radius: 16px;
padding: 0 16px;
background-color: var(--sidebar-bg);
border: 1px solid var(--box-shadow);
/*box-shadow: 0 1px 8px var(--box-shadow);*/
/*box-sizing: border-box;*/
align-items: center;
margin: 10px auto 0;
font-size: 16px;
cursor: pointer;
color: var(--question-text);
}
.expert-guide-item:hover{
background-color: #e5eaf6;
color: #475ada;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
height: 100vh;
overflow-y: auto; /* 使整个聊天区域可滚动,滚动条显示在最右边 */
overflow-x: hidden;
background-color: var(--bg-color);
}
.scrollContainer{
width: 100%;
overflow-y: auto; /* 让内容继续流动 */
height: calc(100vh - 150px); /* 确保占据足够高度 */
overflow-x: hidden;
padding-top: 20px;
}
/* 聊天消息区域 */
.chat-messages {
width: 900px;
margin: 0 auto 10px;
}
/* 输入区域 */
.input-area {
width: 900px;
max-width: 95%;
margin: 0 auto 30px auto;
padding: 8px 10px;
display: flex;
background-color: var(--input-area-bg);
border-radius: 15px;
box-shadow: 0 1px 8px var(--box-shadow);
position: sticky;
bottom: 30px;
flex-shrink: 0; /* 防止被压缩 */
flex-direction: column;
justify-content: flex-start;
}
.input-area-content{
display: flex;
justify-content: space-between;
padding-left: 10px;
}
/* 单条消息样式 */
.message {
display: flex;
margin-bottom: 20px;
align-items: flex-start;
}
/* 用户消息 */
.question{
display: flex;
justify-content: flex-start;
}
.question_left{
width: 40px;
margin: 0 10px;
}
.question_right{
padding:0 6px;
}
.question_right>p{
font-size: 14px;
color: var(--question-text);
}
.questionContent{
font-size: 13px;
color: var(--question-text);
background-color: var(--input-area-bg);
border-radius: 6px;
box-shadow: 0 16px 20px 0 rgba(174, 167, 223, .06);
line-height: 20px;
max-width: 800px;
overflow: hidden;
padding: 8px 16px;
text-overflow: ellipsis;
white-space: nowrap;
width: -moz-fit-content;
width: fit-content;
margin: 10px 0;
cursor: pointer;
}
.questionContent:hover{
background-color: #e5eaf6;
color: #475ada;
}
.user-message {
flex-direction: row-reverse;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: var(--avatar-bg);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
flex-shrink: 0;
margin: 0 10px;
}
.user-message .avatar {
background-color: var(--avatar-user-bg);
}
.message .content {
max-width: 87%;
padding: 12px 16px;
border-radius: 12px;
box-shadow: 0 1px 2px var(--box-shadow);
}
.ai-message .content {
background-color: var(--message-bg);
border-top-left-radius: 0;
}
.user-message .content {
background-color: var(--message-user-bg);
border: 1px solid var(--message-border);
color: var(--text-color);
border-top-right-radius: 0;
}
.content p {
white-space: pre-wrap;
word-break: break-word;
color: var(--text-color);
}
#user-input {
flex: 1;
padding: 8px 10px;
border: none;
border-radius: 0;
resize: none;
outline: none;
height: 100px;
max-height: 150px;
background-color: transparent;
transition: all 0.3s;
margin-right: 8px;
line-height: 20px;
box-shadow: none;
overflow-y: auto;
color: var(--text-color);
}
#user-input:focus {
box-shadow: none;
}
#user-input::placeholder {
color: var(--text-secondary);
}
.send-btn, .stop-btn {
width: 36px;
height: 36px;
min-height: 36px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
padding: 0;
}
.send-btn:hover, .stop-btn:hover {
background-color: var(--primary-hover);
}
.stop-btn {
background-color: #ff4d4f;
}
.stop-btn:hover {
background-color: #ff7875;
}
.stop-icon, .send-icon {
width: 18px;
height: 18px;
stroke: white;
}
/* 打字指示器样式 */
.typing-indicator {
display: inline-block;
position: relative;
min-height: 20px;
font-size: 25px;
font-weight: bold;
}
.typing-indicator::after {
content: "···";
animation: typing 1.5s infinite;
}
@keyframes typing {
0%, 100% { content: "."; }
33% { content: ".."; }
66% { content: "..."; }
}
@media(max-width: 1190px){
.chat-messages{
width: 800px;
}
}
@media(max-width: 1090px){
.chat-messages{
width: 700px;
}
}
@media(max-width: 990px){
.chat-messages{
width: 600px;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.sidebar {
position: absolute;
left: -280px;
height: 100vh;
z-index: 10;
transition: left 0.3s;
}
.sidebar.visible {
left: 0;
}
.chat-messages,
.input-area {
width: 100%;
max-width: 100%;
}
.message .content {
max-width: 85%;
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: var(--scrollbar-track);
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-hover);
}
/* 动画效果 */
.message {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 自定义选择器样式 */
.custom-select {
position: relative;
width: auto;
min-width: 70px;
cursor: pointer;
user-select: none;
margin-right: 10px;
margin-left: 0;
}
.selected-option {
display: flex;
align-items: center;
justify-content: center;
padding: 2px 5px;
border-radius: 8px;
/*background-color: rgba(230, 220, 250, 0.5);*/
background-color: #f5f6fa;
/*color: #6633ff;*/
color: #4c4c4c;
font-size: 14px;
transition: all 0.2s;
}
.selected-option span {
margin-right: 2px;
text-align: center;
display: inline-block;
width: 100%;
}
.dark-theme .selected-option {
background-color: rgba(130, 100, 200, 0.2);
color: #9980ff;
}
.selected-option:hover {
background-color: rgba(210, 200, 250, 0.7);
}
.dark-theme .selected-option:hover {
background-color: rgba(150, 120, 220, 0.3);
}
.dropdown-icon {
transition: transform 0.2s;
margin-left: 2px;
flex-shrink: 0;
stroke: #6633ff;
}
.dark-theme .dropdown-icon {
stroke: #9980ff;
}
.dropdown-icon.rotated {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
bottom: 100%;
left: 0;
width: 100%;
margin-bottom: 5px;
background-color: var(--message-bg);
border-radius: 8px;
box-shadow: 0 2px 10px var(--box-shadow);
z-index: 100;
overflow: hidden;
}
.dropdown-item {
padding: 6px 8px;
transition: background-color 0.2s;
font-size: 13px;
color: var(--text-color);
text-align: center;
}
.dropdown-item:hover {
background-color: var(--hover-bg);
}
.dropdown-item.active {
background-color: var(--active-bg);
font-weight: 500;
}
/* 输入区域内容样式 */
.input-area-content {
display: flex;
align-items: center;
}
.selected-option.active {
/* 选中样式 */
/*background-color: #f5f6fa;*/
background-color: rgba(230, 220, 250, 0.5);
/*color: #272933;*/
color: #6633ff;
}
.expert-toast {
position: fixed;
left: 50%;
top: 65%; /* 垂直居中 */
background-color: rgba(230, 220, 250, 0.5);
color: #6633ff;
padding: 10px 20px;
border-radius: 4px;
z-index: 1000;
}
\ No newline at end of file
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 to comment