综合办公系统
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

AgentDetail.vue 38KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301
  1. <!--
  2. * @Author: wrh
  3. * @Date: 2025-01-01 00:00:00
  4. * @LastEditors: wrh
  5. * @LastEditTime: 2025-11-19 16:41:27
  6. -->
  7. <template>
  8. <div class="agent-detail-container" v-loading="loading">
  9. <div v-if="agentInfo" class="detail-content">
  10. <!-- 智能体头部信息 -->
  11. <!-- <div class="agent-header">
  12. <div class="agent-basic-info">
  13. <h2 class="agent-title">{{ agentInfo.agentName }}</h2>
  14. <p class="agent-description">{{ agentInfo.description || '暂无描述' }}</p>
  15. <div class="agent-meta-info">
  16. <span class="meta-item">
  17. <el-icon>
  18. <Calendar />
  19. </el-icon>
  20. 创建时间:{{ agentInfo.createTime }}
  21. </span>
  22. </div>
  23. </div>
  24. </div> -->
  25. <!-- 对话区域 -->
  26. <div class="chat-section">
  27. <!-- 话题列表 -->
  28. <div v-if="topicList.length > 0" class="topic-list">
  29. <div class="topic-header">
  30. <h4>历史对话</h4>
  31. </div>
  32. <div class="topic-content">
  33. <div v-for="topic in topicList" :key="topic.topicId" class="topic-item"
  34. :class="{ 'active': currentTopicId === topic.topicId }">
  35. <div class="topic-info" @click="selectTopic(topic)">
  36. <div class="topic-name">{{ topic.topic }}</div>
  37. <div class="topic-time">{{ topic.createTime }}</div>
  38. </div>
  39. <div class="topic-actions">
  40. <el-button type="danger" size="small" icon="el-icon-delete" circle @click.stop="handleDeleteTopic(topic)"
  41. class="delete-btn" title="删除对话" />
  42. </div>
  43. </div>
  44. </div>
  45. </div>
  46. <!-- 对话内容区域 -->
  47. <div class="chat-content">
  48. <div class="section-header">
  49. <h3>{{ chatTitle }}</h3>
  50. <div class="header-actions">
  51. <el-button v-if="currentTopicId" size="small" type="primary" @click="startNewChat" icon="el-icon-plus">
  52. 新建对话
  53. </el-button>
  54. <el-button v-if="!currentTopicId && chatMessages.length > 0" size="small" @click="clearChat">
  55. 清空对话
  56. </el-button>
  57. </div>
  58. </div>
  59. <!-- 聊天消息列表 -->
  60. <div class="chat-messages" ref="messagesContainer">
  61. <!-- 开场白 -->
  62. <div v-if="openingMessage && !currentTopicId" class="message-item assistant-message">
  63. <div class="message-avatar">
  64. <svg-icon icon-class="robot" />
  65. </div>
  66. <div class="message-content">
  67. <div class="message-text">{{ openingMessage }}</div>
  68. </div>
  69. </div>
  70. <!-- 文件上传提示消息 -->
  71. <div v-if="openingMessage && !currentTopicId" class="message-item assistant-message">
  72. <div class="message-avatar">
  73. <svg-icon icon-class="robot" />
  74. </div>
  75. <div class="message-content">
  76. <div class="message-upload-area">
  77. <p class="upload-tip"> {{ selectDocumentTip }}</p>
  78. <el-upload class="inline-upload" :action="uploadAction" :multiple="false" :auto-upload="false"
  79. :file-list="chatFileList" :on-change="handleChatFileChange" :before-upload="beforeUpload"
  80. :show-file-list="true" :limit="1">
  81. <el-button type="primary" size="small" icon="el-icon-upload">
  82. {{ selectDocument }}
  83. </el-button>
  84. </el-upload>
  85. <div v-if="chatFileList.length > 0" class="chat-upload-actions">
  86. <el-button size="small" type="success" @click="submitChatUpload" icon="el-icon-check">
  87. 确认上传
  88. </el-button>
  89. </div>
  90. <div v-if="percentageDisplay">
  91. <el-progress :percentage="percentage" :stroke-width="25" :text-inside="true">
  92. <el-button text>{{ progress }}</el-button>
  93. </el-progress>
  94. </div>
  95. </div>
  96. </div>
  97. </div>
  98. <!-- 对话消息 -->
  99. <div v-for="(message, index) in chatMessages" :key="index" class="message-item" :class="[
  100. message.role === 'user' ? 'user-message' : 'assistant-message',
  101. message.isFileInfo ? 'file-info-message' : ''
  102. ]">
  103. <div class="message-avatar">
  104. <i v-if="message.role === 'user'" class="el-icon-user"></i>
  105. <svg-icon v-else icon-class="robot" />
  106. </div>
  107. <div class="message-content">
  108. <div v-if="message.isHtml" class="message-text" v-html="message.content"></div>
  109. <div v-else class="message-text">{{ message.content }}</div>
  110. <div class="message-actions">
  111. <span class="message-time">{{ message.timestamp }}</span>
  112. <el-button v-if="message.canRetry" type="text" size="small" @click="retryMessage(message)"
  113. class="retry-btn" icon="el-icon-refresh">
  114. 重新发送
  115. </el-button>
  116. </div>
  117. </div>
  118. </div>
  119. <!-- 文件上传提示消息 -->
  120. <div v-if="techUploadDisplay" class="message-item assistant-message">
  121. <div class="message-avatar">
  122. <svg-icon icon-class="robot" />
  123. </div>
  124. <div class="message-content">
  125. <div class="message-upload-area">
  126. <p class="upload-tip">请上传大纲修改后的技术文件:</p>
  127. <el-upload class="inline-upload" :action="uploadAction" :multiple="false" :auto-upload="false"
  128. :file-list="chatFileList" :on-change="handleChatFileChange" :before-upload="beforeUpload"
  129. :show-file-list="true" :limit="1">
  130. <el-button type="primary" size="small" icon="el-icon-upload">
  131. 选择技术文件
  132. </el-button>
  133. </el-upload>
  134. <div v-if="chatFileList.length > 0" class="chat-upload-actions">
  135. <el-button size="small" type="success" @click="submitFileUpload" icon="el-icon-check">
  136. 确认上传
  137. </el-button>
  138. </div>
  139. <div v-if="percentageDisplay">
  140. <el-progress :percentage="percentage" :stroke-width="25" :text-inside="true">
  141. <el-button text>{{ progress }}</el-button>
  142. </el-progress>
  143. </div>
  144. </div>
  145. </div>
  146. </div>
  147. <!-- 正在输入指示器 -->
  148. <div v-if="isTyping" class="message-item assistant-message">
  149. <div class="message-avatar">
  150. <svg-icon icon-class="robot" />
  151. </div>
  152. <div class="message-content">
  153. <div class="typing-indicator">
  154. <span></span>
  155. <span></span>
  156. <span></span>
  157. </div>
  158. </div>
  159. </div>
  160. </div>
  161. <!-- 输入区域 -->
  162. <div class="chat-input-area">
  163. <el-input v-model="inputMessage" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"
  164. placeholder="请输入您的问题..." @keyup.enter.native="handleEnterKeydown" />
  165. <div class="input-actions">
  166. <span class="input-tip">Enter 发送,Shift + Enter 换行</span>
  167. <el-button type="primary" @click="sendMessage()" :disabled="!inputMessage.trim() || isTyping">
  168. 发送
  169. </el-button>
  170. </div>
  171. </div>
  172. </div>
  173. </div>
  174. </div>
  175. <!-- 空状态 -->
  176. <div v-else class="empty-state">
  177. <i class="el-icon-search" style="font-size: 48px;"></i>
  178. <p>请选择一个智能体查看详细信息</p>
  179. </div>
  180. </div>
  181. </template>
  182. <script>
  183. import { Message } from 'element-ui';
  184. import { getAgent, opening, uploadFile, uploadModifyFile, getProcessValue } from '@/api/llm/agent';
  185. import { answer } from '@/api/llm/mcp';
  186. import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
  187. import { listChat, addChat, updateChat } from "@/api/llm/chat";
  188. import { listDocument } from "@/api/llm/document";
  189. export default {
  190. name: 'AgentDetail',
  191. props: {
  192. agentId: {
  193. type: [String, Number],
  194. default: null
  195. }
  196. },
  197. data() {
  198. return {
  199. // 响应式数据
  200. loading: false,
  201. agentInfo: null,
  202. openingMessage: '',
  203. chatMessages: [],
  204. inputMessage: '',
  205. isTyping: false,
  206. messagesContainer: null,
  207. topicList: [],
  208. currentTopicId: null,
  209. chatTitle: '智能体新对话',
  210. percentage: 0,
  211. progress: "",
  212. percentageDisplay: false,
  213. techUploadDisplay: false,
  214. selectDocument: '',
  215. selectDocumentTip: '',
  216. chatFileList: [], // 聊天内的文件列表
  217. }
  218. },
  219. computed: {
  220. uploadAction() {
  221. return '/llm/agent/upload'
  222. }
  223. },
  224. watch: {
  225. agentId: {
  226. immediate: true,
  227. handler(newAgentId) {
  228. if (newAgentId) {
  229. this.loadAgentDetail(newAgentId)
  230. } else {
  231. this.agentInfo = null
  232. this.openingMessage = ''
  233. this.chatMessages = []
  234. this.chatFileList = []
  235. this.currentTopicId = null
  236. this.chatTitle = '智能体新对话'
  237. }
  238. }
  239. }
  240. },
  241. methods: {
  242. // 加载智能体详细信息
  243. async loadAgentDetail(agentId) {
  244. this.loading = true
  245. try {
  246. // 获取智能体详细信息
  247. const response = await getAgent(agentId)
  248. this.agentInfo = response.data
  249. // 获取开场白
  250. if (this.agentInfo?.agentName) {
  251. const res = await opening(this.agentInfo.agentName)
  252. this.openingMessage = res.data.resultContent;
  253. if (this.agentInfo.agentName.includes('技术文件')) {
  254. this.selectDocument = '选择招标文件'
  255. this.selectDocumentTip = '请上传您需要分析的招标文件(单个文件):'
  256. }
  257. else if (this.agentInfo.agentName.includes('文档检查')) {
  258. this.selectDocument = '选择检查文件'
  259. this.selectDocumentTip = '请上传您需要检查的文件(单个文件):'
  260. }
  261. }
  262. // 查询当前智能体的话题列表
  263. await this.loadTopic()
  264. // 自动滚动到底部
  265. this.$nextTick(() => {
  266. this.scrollToBottom()
  267. })
  268. } catch (error) {
  269. console.error('加载智能体详细信息失败:', error)
  270. this.$message.error('加载智能体信息失败')
  271. } finally {
  272. this.loading = false
  273. }
  274. },
  275. // 加载选择智能体的话题列表
  276. async loadTopic() {
  277. const res = await listTopic({ agentId: this.agentId })
  278. if (res.rows.length > 0) {
  279. this.topicList = res.rows
  280. }
  281. else {
  282. this.topicList = res.rows
  283. this.techUploadDisplay = false
  284. }
  285. },
  286. // 加载指定话题的聊天记录(公共函数)
  287. async loadChatMessages(topicId) {
  288. try {
  289. const res = await listChat({ topicId: topicId });
  290. if (res.rows.length > 0) {
  291. // 处理聊天消息格式
  292. const messages = []
  293. for (const chat of res.rows) {
  294. // 加载该聊天记录相关的文件,优先显示
  295. if (chat.chatId) {
  296. try {
  297. const fileResponse = await listDocument({ chatId: chat.chatId });
  298. if (fileResponse.rows && fileResponse.rows.length > 0) {
  299. const fileNames = fileResponse.rows.map(doc => doc.path.split("/")[doc.path.split("/").length - 1]).join(', ');
  300. // 先添加文件信息消息
  301. messages.push({
  302. role: 'assistant',
  303. content: `📎 相关文件: ${fileNames}`,
  304. isHtml: false,
  305. isFileInfo: true // 标记为文件信息消息
  306. })
  307. }
  308. } catch (error) {
  309. console.warn('加载文件信息失败:', error)
  310. }
  311. }
  312. // 添加用户输入消息(如果存在)
  313. if (chat.input) {
  314. messages.push({
  315. role: 'user',
  316. content: chat.input,
  317. timestamp: chat.inputTime || new Date(chat.createTime).toLocaleTimeString(),
  318. isHtml: false,
  319. originalContent: chat.input, // 为历史消息添加原始内容
  320. canRetry: false // 历史消息默认不显示重试按钮
  321. })
  322. }
  323. // 添加助手回复消息
  324. if (chat.output) {
  325. messages.push({
  326. role: 'assistant',
  327. content: chat.output,
  328. timestamp: chat.outputTime || new Date(chat.createTime).toLocaleTimeString(),
  329. isHtml: true // 历史消息可能包含HTML内容
  330. })
  331. }
  332. }
  333. // 更新聊天消息
  334. this.chatMessages = messages
  335. return messages.length > 0
  336. } else {
  337. this.chatMessages = []
  338. return false
  339. }
  340. } catch (error) {
  341. console.error('加载话题对话记录失败:', error)
  342. this.chatMessages = []
  343. throw error
  344. }
  345. },
  346. // 选择话题
  347. async selectTopic(topic) {
  348. console.log('选择话题:', topic)
  349. this.currentTopicId = topic.topicId
  350. this.chatTitle = topic.topic
  351. try {
  352. const hasMessages = await this.loadChatMessages(topic.topicId)
  353. // 滚动到底部
  354. this.$nextTick(() => {
  355. this.scrollToBottom()
  356. })
  357. if (hasMessages) {
  358. this.$message.success(`已加载话题"${topic.topic}"的对话记录`)
  359. } else {
  360. this.$message.info('该话题暂无对话记录')
  361. }
  362. } catch (error) {
  363. this.$message.error('加载对话记录失败')
  364. }
  365. },
  366. // 删除话题
  367. async handleDeleteTopic(topic) {
  368. try {
  369. await this.$confirm(
  370. `确定要删除话题"${topic.topic}"吗?删除后无法恢复。`,
  371. '删除确认',
  372. {
  373. confirmButtonText: '确定删除',
  374. cancelButtonText: '取消',
  375. type: 'warning',
  376. confirmButtonClass: 'el-button--danger'
  377. }
  378. )
  379. // 调用删除API
  380. await delTopic(topic.topicId)
  381. // 如果删除的是当前选中的话题,则重置对话状态
  382. if (this.currentTopicId === topic.topicId) {
  383. this.currentTopicId = null
  384. this.chatMessages = []
  385. this.chatTitle = '智能体新对话'
  386. }
  387. // 重新加载话题列表
  388. await this.loadTopic()
  389. this.techUploadDisplay = false;
  390. this.$message.success('话题删除成功')
  391. } catch (error) {
  392. if (error !== 'cancel') {
  393. console.error('删除话题失败:', error)
  394. this.$message.error('删除话题失败')
  395. }
  396. }
  397. },
  398. // 处理 Enter 键事件
  399. handleEnterKeydown(event) {
  400. // 如果按住 Shift 键,允许换行
  401. if (event.shiftKey) {
  402. return
  403. }
  404. // 阻止默认的换行行为
  405. event.preventDefault()
  406. // 发送消息
  407. this.sendMessage()
  408. },
  409. // 发送消息(支持重试)
  410. async sendMessage(retryContent = null) {
  411. // 确保 retryContent 是字符串类型,如果是事件对象则忽略
  412. const validRetryContent = (typeof retryContent === 'string') ? retryContent : null
  413. const message = validRetryContent || this.inputMessage.trim()
  414. if (!message || this.isTyping) return
  415. // 添加用户消息
  416. const userMessage = {
  417. role: 'user',
  418. content: message,
  419. timestamp: new Date().toLocaleTimeString(),
  420. isHtml: false, // 用户消息不使用HTML渲染
  421. originalContent: message, // 保存原始内容用于重试
  422. canRetry: false // 初始时不显示重试按钮
  423. }
  424. this.chatMessages.push(userMessage)
  425. // 只有在非重试模式下才清空输入框
  426. if (!retryContent) {
  427. this.inputMessage = ''
  428. }
  429. this.isTyping = true
  430. this.$nextTick(() => {
  431. this.scrollToBottom()
  432. })
  433. try {
  434. // 调用智能体回答API
  435. const response = await answer({
  436. topicId: this.currentTopicId,
  437. question: message
  438. })
  439. let content = JSON.parse(response.resultContent).content;
  440. console.log(content);
  441. // 检查是否是默认的失败回复
  442. const defaultFailureMessage = '抱歉,我暂时无法回答这个问题。';
  443. const finalContent = this.formatContentLinks(content) || defaultFailureMessage;
  444. // 添加助手回复
  445. const assistantMessage = {
  446. role: 'assistant',
  447. content: finalContent,
  448. timestamp: new Date().toLocaleTimeString(),
  449. isHtml: true // 普通聊天消息使用HTML渲染
  450. }
  451. this.chatMessages.push(assistantMessage)
  452. // 如果是默认失败回复,为用户消息添加重试按钮
  453. if (finalContent === defaultFailureMessage) {
  454. userMessage.canRetry = true
  455. }
  456. } catch (error) {
  457. console.error('发送消息失败:', error)
  458. this.$message.error('发送消息失败')
  459. // 添加错误消息
  460. const errorMessage = {
  461. role: 'assistant',
  462. content: '抱歉,发生了一些错误,请稍后再试。',
  463. timestamp: new Date().toLocaleTimeString(),
  464. isHtml: false
  465. }
  466. this.chatMessages.push(errorMessage)
  467. // 为用户消息添加重试按钮
  468. userMessage.canRetry = true
  469. } finally {
  470. this.isTyping = false
  471. this.$nextTick(() => {
  472. this.scrollToBottom()
  473. })
  474. }
  475. },
  476. // 重新发送消息
  477. async retryMessage(message) {
  478. // 找到当前消息在数组中的索引
  479. const messageIndex = this.chatMessages.findIndex(msg => msg === message)
  480. if (messageIndex === -1) return
  481. // 获取原始消息内容
  482. const originalContent = message.originalContent || message.content
  483. if (!originalContent || typeof originalContent !== 'string') {
  484. this.$message.error('无法获取原始消息内容')
  485. return
  486. }
  487. // 移除从当前用户消息开始的所有消息(包括后续的助手回复)
  488. const messagesToKeep = this.chatMessages.slice(0, messageIndex)
  489. this.chatMessages = messagesToKeep
  490. // 重新发送原始消息
  491. await this.sendMessage(originalContent)
  492. },
  493. // 滚动到底部
  494. scrollToBottom() {
  495. if (this.messagesContainer) {
  496. this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight
  497. }
  498. },
  499. // 聊天内文件上传相关方法
  500. handleChatFileChange(file, fileList) {
  501. // 只保留最新选择的文件(限制为单文件)
  502. this.chatFileList = fileList.slice(-1)
  503. },
  504. beforeUpload(file) {
  505. const isValidType = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'].includes(file.type)
  506. const isLt10M = file.size / 1024 / 1024 < 10
  507. if (!isValidType) {
  508. this.$message.error('只支持 PDF、DOC、DOCX、TXT 格式的文件!')
  509. return false
  510. }
  511. if (!isLt10M) {
  512. this.$message.error('单个文件大小不能超过 10MB!')
  513. return false
  514. }
  515. return false // 阻止自动上传
  516. },
  517. // 聊天内文件上传提交
  518. async submitChatUpload() {
  519. if (this.chatFileList.length === 0) {
  520. this.$message.warning('请先选择文件')
  521. return
  522. }
  523. try {
  524. this.percentageDisplay = true;
  525. var timer;
  526. clearInterval(timer);
  527. const getProcess = () => {
  528. timer = setInterval(() => { //隔2000毫秒获取进度
  529. getProcessValue().then(res => {
  530. if (res.code == 200 & res.msg.includes(":")) {
  531. this.progress = res.msg;
  532. this.percentage = Number(res.msg.split(":")[1].replace("%", ""))
  533. }
  534. });
  535. }, 2000)
  536. }
  537. getProcess();
  538. const file = this.chatFileList[0].raw // 只取第一个文件
  539. const fileName = file.name;
  540. try {
  541. const response = await uploadFile(file, this.agentInfo.agentName)
  542. const chatId = response.data.chatId; //获取保存后的chatId
  543. // 解析返回的数据
  544. if (response.data && response.data.assistantMessage) {
  545. this.percentageDisplay = false;
  546. // 格式化链接:在href前加上基础API地址
  547. let assistantContent = this.formatContentLinks(response.data.assistantMessage)
  548. clearInterval(timer);
  549. // 添加上传成功的消息到聊天记录
  550. const uploadMessage = {
  551. role: 'assistant',
  552. content: assistantContent,
  553. timestamp: new Date().toLocaleTimeString(),
  554. isHtml: true // 标记这是HTML内容
  555. }
  556. this.chatMessages.push(uploadMessage);
  557. if (this.agentInfo.agentName.includes('技术文件'))
  558. this.techUploadDisplay = true;
  559. // this.$message.success('文件上传成功');
  560. let topicRes = await addTopic({ agentId: this.agentId, topic: fileName });
  561. const topicId = topicRes.msg;
  562. await updateChat({ userId: this.$store.state.user.id, chatId, topicId, output: assistantContent, outputTime: this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') });
  563. // 刷新话题列表
  564. await this.loadTopic();
  565. // 激活当前话题并加载聊天记录
  566. this.currentTopicId = topicId;
  567. this.chatTitle = fileName;
  568. // 重新加载该话题的聊天记录以显示文件相关内容
  569. try {
  570. await this.loadChatMessages(topicId)
  571. } catch (error) {
  572. console.error('加载话题聊天记录失败:', error)
  573. }
  574. // 清空文件上传列表
  575. this.chatFileList = [];
  576. this.$nextTick(() => {
  577. this.scrollToBottom()
  578. })
  579. }
  580. }
  581. catch (error) {
  582. clearInterval(timer);
  583. console.error('文件上传失败:', error)
  584. }
  585. } catch (error) {
  586. console.error('文件上传失败:', error)
  587. this.$message.error('文件上传失败')
  588. // 添加上传失败的消息到聊天记录
  589. const errorMessage = {
  590. role: 'assistant',
  591. content: '文件上传失败,请检查文件格式或网络连接后重试。',
  592. timestamp: new Date().toLocaleTimeString(),
  593. isHtml: false
  594. }
  595. this.chatMessages.push(errorMessage)
  596. this.$nextTick(() => {
  597. this.scrollToBottom()
  598. })
  599. }
  600. },
  601. // 聊天内文件上传提交
  602. async submitFileUpload() {
  603. if (this.chatFileList.length === 0) {
  604. this.$message.warning('请先选择文件')
  605. return
  606. }
  607. try {
  608. this.percentageDisplay = true;
  609. var timer;
  610. clearInterval(timer);
  611. const getProcess = () => {
  612. timer = setInterval(() => { //隔2000毫秒获取进度
  613. getProcessValue().then(res => {
  614. if (res.code == 200 & res.msg.includes(":")) {
  615. this.progress = res.msg;
  616. this.percentage = Number(res.msg.split(":")[1].replace("%", ""))
  617. }
  618. });
  619. }, 2000)
  620. }
  621. getProcess();
  622. const file = this.chatFileList[0].raw // 只取第一个文件
  623. const fileName = file.name;
  624. try {
  625. const response = await uploadModifyFile(file, this.agentInfo.agentName)
  626. const chatId = response.data.chatId; //获取保存后的chatId
  627. // 解析返回的数据
  628. if (response.data && response.data.assistantMessage) {
  629. this.percentageDisplay = false;
  630. // 格式化链接:在href前加上基础API地址
  631. let assistantContent = this.formatContentLinks(response.data.assistantMessage)
  632. clearInterval(timer);
  633. // 添加上传成功的消息到聊天记录
  634. const uploadMessage = {
  635. role: 'assistant',
  636. content: assistantContent,
  637. timestamp: new Date().toLocaleTimeString(),
  638. isHtml: true // 标记这是HTML内容
  639. }
  640. this.chatMessages.push(uploadMessage);
  641. this.techUploadDisplay = false;
  642. await updateChat({ userId: this.$store.state.user.id, chatId, topicId: this.currentTopicId, output: assistantContent, outputTime: this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') });
  643. // 清空文件上传列表
  644. this.chatFileList = [];
  645. this.$nextTick(() => {
  646. this.scrollToBottom()
  647. })
  648. }
  649. }
  650. catch (error) {
  651. clearInterval(timer);
  652. console.error('文件上传失败:', error)
  653. }
  654. } catch (error) {
  655. console.error('文件上传失败:', error)
  656. this.$message.error('文件上传失败')
  657. // 添加上传失败的消息到聊天记录
  658. const errorMessage = {
  659. role: 'assistant',
  660. content: '文件上传失败,请检查文件格式或网络连接后重试。',
  661. timestamp: new Date().toLocaleTimeString(),
  662. isHtml: false
  663. }
  664. this.chatMessages.push(errorMessage)
  665. this.$nextTick(() => {
  666. this.scrollToBottom()
  667. })
  668. }
  669. },
  670. // 开始新对话
  671. startNewChat() {
  672. this.currentTopicId = null
  673. this.chatMessages = []
  674. this.chatFileList = []
  675. this.chatTitle = '智能体新对话'
  676. // 滚动到顶部显示开场白
  677. this.$nextTick(() => {
  678. if (this.messagesContainer) {
  679. this.messagesContainer.scrollTop = 0
  680. }
  681. })
  682. this.techUploadDisplay = false
  683. this.$message.success('已切换到新对话模式')
  684. },
  685. // 清空对话
  686. clearChat() {
  687. this.chatMessages = []
  688. this.chatFileList = []
  689. this.chatTitle = '智能体新对话'
  690. // 滚动到顶部显示开场白
  691. this.$nextTick(() => {
  692. if (this.messagesContainer) {
  693. this.messagesContainer.scrollTop = 0
  694. }
  695. })
  696. this.$message.success('对话已清空')
  697. },
  698. // 格式化内容中的链接
  699. formatContentLinks(content) {
  700. if (!content) return content
  701. // 使用正则表达式匹配 <a href='/profile/...'>...</a> 格式的链接
  702. const linkRegex = /<a\s+href=['"]([^'"]*?)['"][^>]*?>(.*?)<\/a>/gi
  703. let formattedContent = content.replace(linkRegex, (match, href, text) => {
  704. // 如果href不是以http开头的,说明是相对路径,需要添加基础API地址
  705. if (!href.startsWith('http')) {
  706. const baseApi = process.env.VUE_APP_BASE_API || ''
  707. const fullUrl = baseApi + href
  708. return `<a href="${fullUrl}" target="_blank" style="color: #1890ff; text-decoration: underline;">${text}</a>`
  709. }
  710. return match
  711. })
  712. // 将普通换行符转换为HTML换行符
  713. formattedContent = formattedContent.replace(/\n/g, '<br>')
  714. return formattedContent
  715. }
  716. }
  717. }
  718. </script>
  719. <style lang="scss" scoped>
  720. .agent-detail-container {
  721. height: 100%;
  722. display: flex;
  723. flex-direction: column;
  724. }
  725. .detail-content {
  726. flex: 1;
  727. display: flex;
  728. flex-direction: column;
  729. overflow: hidden;
  730. }
  731. .agent-header {
  732. padding: 15px;
  733. border-bottom: 1px solid #e4e4e4;
  734. background: white;
  735. display: flex;
  736. justify-content: space-between;
  737. align-items: flex-start;
  738. .agent-basic-info {
  739. flex: 1;
  740. .agent-title {
  741. margin: 0 0 8px 0;
  742. font-size: 24px;
  743. font-weight: 600;
  744. color: #333;
  745. }
  746. .agent-description {
  747. margin: 0 0 12px 0;
  748. font-size: 14px;
  749. color: #666;
  750. line-height: 1.5;
  751. }
  752. .agent-meta-info {
  753. display: flex;
  754. gap: 16px;
  755. .meta-item {
  756. display: flex;
  757. align-items: center;
  758. gap: 4px;
  759. font-size: 12px;
  760. color: #999;
  761. .el-icon {
  762. font-size: 14px;
  763. }
  764. }
  765. }
  766. }
  767. .agent-actions {
  768. display: flex;
  769. gap: 12px;
  770. }
  771. }
  772. .chat-section {
  773. background: white;
  774. margin: 16px 24px;
  775. border-radius: 8px;
  776. border: 1px solid #e4e4e4;
  777. overflow: hidden;
  778. display: flex;
  779. height: calc(100vh - 150px);
  780. // 话题列表样式
  781. .topic-list {
  782. width: 250px;
  783. border-right: 1px solid #e4e4e4;
  784. display: flex;
  785. flex-direction: column;
  786. background: #f8f9fa;
  787. .topic-header {
  788. padding: 16px 20px;
  789. border-bottom: 1px solid #e4e4e4;
  790. background: #f5f7fa;
  791. color: #495057;
  792. h4 {
  793. margin: 0;
  794. font-size: 14px;
  795. font-weight: 600;
  796. color: #495057;
  797. display: flex;
  798. align-items: center;
  799. &::before {
  800. content: "📚";
  801. margin-right: 8px;
  802. font-size: 16px;
  803. }
  804. }
  805. }
  806. .topic-content {
  807. flex: 1;
  808. overflow-y: auto;
  809. padding: 8px 0;
  810. .topic-item {
  811. padding: 12px 16px;
  812. border-bottom: 1px solid #e9ecef;
  813. transition: all 0.3s ease;
  814. background: white;
  815. margin: 4px 8px;
  816. border-radius: 6px;
  817. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  818. display: flex;
  819. justify-content: space-between;
  820. align-items: center;
  821. &:hover {
  822. background: #e3f2fd;
  823. transform: translateX(2px);
  824. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  825. .topic-actions {
  826. opacity: 1;
  827. }
  828. }
  829. &.active {
  830. background: #f5f7fa;
  831. color: #495057;
  832. transform: translateX(3px);
  833. box-shadow: 0 3px 12px rgba(73, 80, 87, 0.2);
  834. border-left: 3px solid #409EFF;
  835. .topic-info {
  836. .topic-name {
  837. color: #495057;
  838. font-weight: 600;
  839. &::before {
  840. content: "💬";
  841. filter: none;
  842. }
  843. }
  844. .topic-time {
  845. color: #6c757d;
  846. }
  847. }
  848. }
  849. &:last-child {
  850. border-bottom: none;
  851. }
  852. .topic-info {
  853. flex: 1;
  854. cursor: pointer;
  855. min-width: 0; // 允许flex项目缩小到内容以下
  856. max-width: calc(100% - 40px); // 为删除按钮预留40px空间
  857. .topic-name {
  858. font-size: 13px;
  859. font-weight: 500;
  860. color: #495057;
  861. margin-bottom: 4px;
  862. overflow: hidden;
  863. text-overflow: ellipsis;
  864. white-space: nowrap;
  865. max-width: 100%;
  866. &::before {
  867. content: "💬";
  868. margin-right: 6px;
  869. font-size: 12px;
  870. }
  871. }
  872. .topic-time {
  873. font-size: 11px;
  874. color: #6c757d;
  875. font-style: italic;
  876. overflow: hidden;
  877. text-overflow: ellipsis;
  878. white-space: nowrap;
  879. max-width: 100%;
  880. }
  881. }
  882. .topic-actions {
  883. flex-shrink: 0; // 防止删除按钮被压缩
  884. width: 32px; // 固定宽度
  885. display: flex;
  886. justify-content: center;
  887. opacity: 0;
  888. transition: opacity 0.3s ease;
  889. .delete-btn {
  890. width: 24px;
  891. height: 24px;
  892. padding: 0;
  893. border: none;
  894. background: #ff4757;
  895. &:hover {
  896. background: #ff3742;
  897. transform: scale(1.1);
  898. }
  899. .el-icon {
  900. font-size: 12px;
  901. }
  902. }
  903. }
  904. }
  905. }
  906. }
  907. // 对话内容区域样式
  908. .chat-content {
  909. flex: 1;
  910. display: flex;
  911. flex-direction: column;
  912. background: #fdfdfd;
  913. .section-header {
  914. padding: 16px 20px;
  915. border-bottom: 1px solid #e4e4e4;
  916. background: #f5f7fa;
  917. display: flex;
  918. justify-content: space-between;
  919. align-items: center;
  920. h3 {
  921. margin: 0;
  922. font-size: 16px;
  923. font-weight: 600;
  924. color: #495057;
  925. display: flex;
  926. align-items: center;
  927. &::before {
  928. content: "✨";
  929. margin-right: 8px;
  930. font-size: 18px;
  931. }
  932. }
  933. .header-actions {
  934. display: flex;
  935. gap: 8px;
  936. }
  937. }
  938. .chat-messages {
  939. flex: 1;
  940. padding: 16px 20px;
  941. overflow-y: auto;
  942. max-height: 900px;
  943. .message-item {
  944. display: flex;
  945. margin-bottom: 16px;
  946. align-items: flex-start;
  947. &.user-message {
  948. flex-direction: row-reverse;
  949. .message-avatar {
  950. background: #1890ff;
  951. color: white;
  952. margin-left: 12px;
  953. margin-right: 0;
  954. }
  955. .message-content {
  956. text-align: right;
  957. .message-text {
  958. background: #1890ff;
  959. color: white;
  960. }
  961. .message-actions {
  962. justify-content: flex-end;
  963. .retry-btn {
  964. order: -1; // 将重试按钮放在时间前面
  965. margin-left: 0;
  966. margin-right: 8px;
  967. color: #1890ff;
  968. background-color: rgba(255, 255, 255, 0.9);
  969. &:hover {
  970. background-color: white;
  971. }
  972. }
  973. }
  974. }
  975. }
  976. &.assistant-message {
  977. .message-avatar {
  978. background: #f0f0f0;
  979. color: #666;
  980. margin-right: 12px;
  981. }
  982. .message-content {
  983. .message-text {
  984. background: #f0f0f0;
  985. color: #333;
  986. }
  987. }
  988. // 文件信息消息特殊样式
  989. &.file-info-message {
  990. .message-avatar {
  991. background: #e3f2fd;
  992. color: #1976d2;
  993. }
  994. .message-content {
  995. .message-text {
  996. background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
  997. color: #1565c0;
  998. border-left: 4px solid #2196f3;
  999. font-weight: 500;
  1000. &::before {
  1001. content: "📋";
  1002. margin-right: 8px;
  1003. font-size: 16px;
  1004. }
  1005. }
  1006. }
  1007. }
  1008. }
  1009. .message-avatar {
  1010. width: 32px;
  1011. height: 32px;
  1012. border-radius: 50%;
  1013. display: flex;
  1014. align-items: center;
  1015. justify-content: center;
  1016. font-size: 30px;
  1017. flex-shrink: 0;
  1018. }
  1019. .message-content {
  1020. max-width: 70%;
  1021. .message-text {
  1022. padding: 8px 12px;
  1023. border-radius: 8px;
  1024. font-size: 14px;
  1025. line-height: 1.5;
  1026. word-wrap: break-word;
  1027. // HTML消息中的链接样式
  1028. :deep(a) {
  1029. color: #1890ff;
  1030. text-decoration: underline;
  1031. &:hover {
  1032. color: #40a9ff;
  1033. text-decoration: none;
  1034. }
  1035. }
  1036. // 换行处理
  1037. :deep(br) {
  1038. display: block;
  1039. margin: 4px 0;
  1040. }
  1041. }
  1042. .message-actions {
  1043. display: flex;
  1044. align-items: center;
  1045. justify-content: space-between;
  1046. margin-top: 4px;
  1047. .message-time {
  1048. font-size: 11px;
  1049. color: #999;
  1050. }
  1051. .retry-btn {
  1052. font-size: 11px;
  1053. color: #1890ff;
  1054. padding: 2px 6px;
  1055. margin-left: 8px;
  1056. &:hover {
  1057. background-color: #f0f8ff;
  1058. }
  1059. .el-icon {
  1060. font-size: 12px;
  1061. margin-right: 2px;
  1062. }
  1063. }
  1064. }
  1065. }
  1066. // 聊天内文件上传区域样式
  1067. .message-upload-area {
  1068. background: #f8f9fa;
  1069. border: 1px dashed #d0d7de;
  1070. border-radius: 8px;
  1071. padding: 16px;
  1072. .upload-tip {
  1073. margin: 0 0 12px 0;
  1074. font-size: 14px;
  1075. color: #333;
  1076. }
  1077. .inline-upload {
  1078. margin-bottom: 12px;
  1079. :deep(.el-upload-list) {
  1080. margin-top: 8px;
  1081. .el-upload-list__item {
  1082. font-size: 12px;
  1083. margin: 2px 0;
  1084. }
  1085. }
  1086. }
  1087. .chat-upload-actions {
  1088. display: flex;
  1089. gap: 8px;
  1090. margin-top: 8px;
  1091. }
  1092. }
  1093. }
  1094. }
  1095. .chat-input-area {
  1096. padding: 16px 20px;
  1097. border-top: 1px solid #e4e4e4;
  1098. .input-actions {
  1099. display: flex;
  1100. justify-content: space-between;
  1101. align-items: center;
  1102. margin-top: 8px;
  1103. .input-tip {
  1104. font-size: 12px;
  1105. color: #999;
  1106. }
  1107. }
  1108. }
  1109. }
  1110. }
  1111. // 打字指示器动画
  1112. .typing-indicator {
  1113. display: flex;
  1114. gap: 4px;
  1115. padding: 8px 12px;
  1116. background: #f0f0f0;
  1117. border-radius: 8px;
  1118. span {
  1119. width: 6px;
  1120. height: 6px;
  1121. background: #999;
  1122. border-radius: 50%;
  1123. animation: typing 1.4s infinite ease-in-out;
  1124. &:nth-child(1) {
  1125. animation-delay: -0.32s;
  1126. }
  1127. &:nth-child(2) {
  1128. animation-delay: -0.16s;
  1129. }
  1130. }
  1131. }
  1132. @keyframes typing {
  1133. 0%,
  1134. 80%,
  1135. 100% {
  1136. transform: scale(0.8);
  1137. opacity: 0.6;
  1138. }
  1139. 40% {
  1140. transform: scale(1);
  1141. opacity: 1;
  1142. }
  1143. }
  1144. .empty-state {
  1145. flex: 1;
  1146. display: flex;
  1147. flex-direction: column;
  1148. align-items: center;
  1149. justify-content: center;
  1150. color: #999;
  1151. p {
  1152. margin-top: 16px;
  1153. font-size: 14px;
  1154. }
  1155. }
  1156. </style>