MiniChat.vue 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283
  1. <!-- eslint-disable vue/no-v-html -->
  2. <template>
  3. <div class="btn" @click="showAIChat">
  4. <div style="display: flex; flex-direction: row" class="btn-header">
  5. <img src="@/assets/images/vent/home/wakeBtn.png" />
  6. </div>
  7. </div>
  8. <div v-if="isShowChatBroad" class="mini-chat">
  9. <!-- 左侧折叠区域 -->
  10. <div class="left-side" :class="{ collapsed: isFold }" id="leftSide">
  11. <div
  12. class="addBtn"
  13. :style="{
  14. backgroundColor: isFold ? '' : '#2cb6ff',
  15. width: isFold ? '20px' : 'auto',
  16. }"
  17. @click="addNew"
  18. >
  19. <SvgIcon v-if="isFold" name="add" size="20" :color="''" />
  20. <span v-if="!isFold" class="btn-text">添加新对话</span>
  21. </div>
  22. <div
  23. v-if="isFold"
  24. class="historyBtn"
  25. :style="{ backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/history.svg' : '/src/assets/images/vent/home/history.svg'})` }"
  26. @click="addNew"
  27. ></div>
  28. <div v-else class="historyBtn1">
  29. <span
  30. class="btn-text-bg"
  31. :style="{
  32. backgroundImage: `url(${!isFold ? '/src/assets/images/vent/home/history.svg' : ''})`,
  33. }"
  34. ></span>
  35. <span v-if="!isFold" class="btn-text">历史对话</span>
  36. <a-list style="width: 130px" :split="false" :data-source="sessionHistory" :scroll="200" class="custom-list">
  37. <template #renderItem="{ item }">
  38. <a-list-item
  39. class="session-item"
  40. :style="{
  41. padding: '8px 10px 0 8px',
  42. color: '#5e7081',
  43. fontSize: '10px',
  44. position: 'relative', // 新增定位
  45. }"
  46. >
  47. <!-- 新增flex布局容器 -->
  48. <div style="width: 100%">
  49. <div v-if="editingId !== item.id" class="text-container">
  50. <span class="edit-text" @click="sessionsHistory(item.id)">{{ item.name }}</span>
  51. <div class="btn-container">
  52. <EditOutlined class="edit-icon" @click="startEditing(item)" />
  53. <DeleteOutlined class="delete-icon" @click="startDelete(item)" />
  54. </div>
  55. </div>
  56. <!-- 输入框 -->
  57. <a-input
  58. size="small"
  59. v-else
  60. v-model:value="editText"
  61. v-focus
  62. @blur="handleSave(item)"
  63. @keyup.enter="handleSave(item)"
  64. class="edit-input"
  65. />
  66. </div>
  67. </a-list-item>
  68. </template>
  69. </a-list>
  70. </div>
  71. <div
  72. class="foldBtn"
  73. :style="{ backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/Fold.svg' : '/src/assets/images/vent/home/unfold.svg'})` }"
  74. @click="fold"
  75. ></div>
  76. </div>
  77. <!-- 右侧对话框 -->
  78. <div class="right-side">
  79. <div class="title"> 智能问答 </div>
  80. <!-- 对话区域 -->
  81. <div class="dialog-area" ref="dialogAreaRef">
  82. <div
  83. v-for="message in messageHistory"
  84. :key="message.id"
  85. class="flex items-center w-100%"
  86. :style="{ alignSelf: message.type === 'user' ? 'flex-end' : 'flex-start' }"
  87. >
  88. <template v-if="message.type === 'user'">
  89. <div class="flex-grow-1"></div>
  90. <div class="message-wrapper user-message-wrapper">
  91. <div class="ask-message">{{ message.parsedContent }}</div>
  92. <div class="copy-icon-container">
  93. <CopyOutlined class="copy-icon" @click="copyToClipboard(message.parsedContent)" title="复制消息" />
  94. <EditOutlined class="copy-icon" @click="editAsk(message.parsedContent)" title="重新编辑" />
  95. </div>
  96. </div>
  97. </template>
  98. <template v-else>
  99. <SvgIcon size="40" class="answerIcon" name="ai-logo" />
  100. <div class="message-wrapper ai-message-wrapper">
  101. <div class="answer-message">
  102. <div v-if="message.parsedContentR1" class="thinking-section">
  103. <div class="thinking-header" @click="isShow(message)">
  104. <span class="thinking-title"
  105. >思考过程:<RightOutlined v-if="!message.isShowThink" /> <DownOutlined v-if="message.isShowThink"
  106. /></span>
  107. </div>
  108. <div v-show="message.isShowThink" class="color-gray font-size-12px" v-html="message.parsedContentR1"></div>
  109. </div>
  110. <div v-if="message.parsedContent" v-html="formatMessage(message.parsedContent)"> </div>
  111. </div>
  112. <div class="copy-icon-container">
  113. <CopyOutlined class="copy-icon" @click="copyToClipboard(message.parsedContent)" title="复制消息" />
  114. <RedoOutlined class="copy-icon" @click="refresh()" title="重新生成" />
  115. </div>
  116. </div>
  117. </template>
  118. </div>
  119. <!-- 建议信息 -->
  120. <div v-for="(item, index) in suggestList" :key="index" class="suggestion-item" @click="handleSuggestClick(item)">
  121. <span class="suggestion-text">{{ item }}</span>
  122. <a-icon type="right" class="suggestion-arrow" />
  123. </div>
  124. </div>
  125. <!-- 底部输入区 -->
  126. <div class="input-area">
  127. <a-textarea v-model:value="inputText" placeholder="请输入你的问题" @keyup.enter="handleSend(inputText)" class="ant-input" auto-size />
  128. <div class="ctrl-btn">
  129. <div class="input-controls">
  130. <button class="control-btn" :class="{ active: isThinking }" @click="toggleThinking">深度学习</button>
  131. <button class="control-btn" @click="stopReq()">停止响应</button>
  132. </div>
  133. <div class="action-bar">
  134. <Space>
  135. <Button class="control-btn1" size="small" @click="showModal()">
  136. <template #icon>
  137. <SvgIcon name="send-file" />
  138. </template>
  139. </Button>
  140. <Button class="control-btn2" size="small" @click="handleSend(inputText)">
  141. <template #icon>
  142. <SvgIcon name="send" />
  143. </template>
  144. </Button>
  145. </Space>
  146. </div>
  147. </div>
  148. <!-- 右侧文件上传区 -->
  149. <div v-if="open" class="file-upload">
  150. <!-- 上传按钮 -->
  151. <a-upload
  152. class="custom-upload"
  153. name="file"
  154. :multiple="false"
  155. :before-upload="handleBeforeUpload"
  156. :file-list="fileList"
  157. :remove="handleRemove"
  158. >
  159. <a-button class="upload-btn">
  160. <UploadOutlined></UploadOutlined>
  161. 从本地上传
  162. </a-button>
  163. </a-upload>
  164. </div>
  165. </div>
  166. </div>
  167. </div>
  168. </template>
  169. <script lang="ts" setup>
  170. import { ref, onMounted, nextTick } from 'vue';
  171. import { SvgIcon } from '../Icon';
  172. import { Space, Button, Modal, Input, message } from 'ant-design-vue';
  173. // import AIChat from './index.vue';
  174. import { useUserStore } from '/@/store/modules/user';
  175. import {
  176. EditOutlined,
  177. DeleteOutlined,
  178. UploadOutlined,
  179. CopyOutlined,
  180. RightOutlined,
  181. DownOutlined,
  182. FastForwardFilled,
  183. RedoOutlined,
  184. } from '@ant-design/icons-vue';
  185. import { createVNode } from 'vue';
  186. import { marked } from 'marked';
  187. import katex from 'katex';
  188. import 'katex/dist/katex.min.css';
  189. const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
  190. const inputText = ref(''); // 输入框内容
  191. const refreshText = ref(''); //重新生成文本
  192. const sessionHistory = ref([]);
  193. const isShowChatBroad = ref(false);
  194. const editingId = ref<number | null>(null);
  195. const editText = ref('');
  196. const currentSessionID = ref('');
  197. const taskID = ref('');
  198. const messageID = ref('');
  199. const open = ref<boolean>(false);
  200. const isThinking = ref(false); //深度思考是否开启
  201. const Thinking = ref(false);
  202. const APIKEY = ref('Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd');
  203. interface ListItem {
  204. id: number;
  205. name?: string;
  206. }
  207. interface Message {
  208. id: string; // 唯一标识(可用时间戳生成)
  209. type: 'user' | 'system' | 'response';
  210. content: String; // 原始 Markdown 字符串(用于拼接)
  211. parsedContent: String; // 解析后的 HTML(用于渲染)
  212. contentR1: String; // 原始思考过程 Markdown
  213. parsedContentR1: String; // 解析后的思考过程 HTML
  214. timestamp: number; // 排序依据
  215. isShowThink: boolean; //深度思考展示
  216. }
  217. // 定义消息历史数组类型
  218. const messageHistory = ref<Message[]>([]);
  219. const isFold = ref(true); // 是否折叠
  220. const userid = useUserStore().getUserInfo.id as string;
  221. const filePath = ref(''); // 绑定输入框值
  222. const showConfirmBtn = ref(false); // 控制确认按钮显示状态
  223. const fileList = ref([]);
  224. const suggestList = ref([]); //建议列表
  225. //启用深度思考
  226. const toggleThinking = () => {
  227. isThinking.value = !isThinking.value;
  228. if (isThinking.value) {
  229. APIKEY.value = 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN';
  230. } else {
  231. APIKEY.value = 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd';
  232. }
  233. };
  234. // 折叠思考过程
  235. const isShow = (message) => {
  236. message.isShowThink = !message.isShowThink;
  237. };
  238. const dialogAreaRef = ref(null);
  239. // 滚动到底部的方法
  240. const scrollToBottom = async () => {
  241. // 等待 DOM 更新(如消息渲染完成)
  242. await nextTick();
  243. if (dialogAreaRef.value) {
  244. const el = dialogAreaRef.value;
  245. // 关键:scrollTop = scrollHeight(滚动内容总高度)
  246. el.scrollTop = el.scrollHeight;
  247. }
  248. };
  249. // 点击建议项时的处理函数
  250. const handleSuggestClick = (text) => {
  251. // 将选中的建议填充到文本框
  252. inputText.value = text;
  253. };
  254. function showAIChat() {
  255. isShowChatBroad.value = !isShowChatBroad.value;
  256. }
  257. //复制消息
  258. function copyToClipboard(text) {
  259. if (!text || text.trim() === '') {
  260. message.warn('没有可复制的内容');
  261. return;
  262. }
  263. // 2. 创建临时textarea 元素
  264. const textarea = document.createElement('textarea');
  265. textarea.value = text;
  266. textarea.style.position = 'fixed';
  267. textarea.style.top = '-999px';
  268. textarea.style.left = '-999px';
  269. textarea.style.width = '200px';
  270. textarea.style.height = '200px';
  271. document.body.appendChild(textarea);
  272. try {
  273. textarea.select();
  274. textarea.setSelectionRange(0, text.length);
  275. const isSuccessful = document.execCommand('copy');
  276. if (isSuccessful) {
  277. message.success('复制成功!');
  278. } else {
  279. throw new Error('复制命令执行失败');
  280. }
  281. } catch (err) {
  282. console.error('复制失败:', err);
  283. message.error('复制失败,请手动复制');
  284. } finally {
  285. document.body.removeChild(textarea);
  286. }
  287. }
  288. const initMarked = () => {
  289. marked.setOptions({
  290. gfm: true, // 启用GitHub风格的Markdown,包含表格
  291. breaks: false, // 禁用换行符转换
  292. });
  293. };
  294. // LaTeX 公式渲染(内部使用)
  295. const renderLatexInHtml = (html) => {
  296. // 匹配块级公式($$...$$)
  297. const blockRegex = /\$\$(.*?)\$\$/gs;
  298. // 匹配行内公式($...$)
  299. const inlineRegex = /\$(.*?)\$/g;
  300. let matchCount = 0; // 统计匹配次数
  301. // // 替换块级公式(居中显示)
  302. html = html.replace(blockRegex, (match, formula) => {
  303. matchCount++;
  304. console.log(`块级公式匹配第 ${matchCount} 次:`, formula); // 查看匹配次数和内容
  305. return katex.renderToString(formula.trim(), {
  306. displayMode: true,
  307. throwOnError: false,
  308. strict: false,
  309. trust: true,
  310. });
  311. });
  312. // 替换行内公式(行内显示)
  313. html = html.replace(inlineRegex, (match, formula) => {
  314. return katex.renderToString(formula.trim(), {
  315. displayMode: false,
  316. throwOnError: false,
  317. strict: false,
  318. });
  319. });
  320. console.log(`最终匹配次数:${matchCount}`); // 输出总匹配次数
  321. return html;
  322. };
  323. // Markdown + LaTeX 解析
  324. const parseMarkdownWithLatex = (mdStr) => {
  325. if (!mdStr) return '';
  326. try {
  327. let html = marked(mdStr); // Markdown → HTML
  328. html = renderLatexInHtml(html); // 替换公式
  329. return html;
  330. } catch (error) {
  331. console.error('解析失败:', error);
  332. return mdStr; // 降级显示原始字符串
  333. }
  334. };
  335. //重新生成
  336. const refresh = () => {
  337. handleSend(refreshText.value);
  338. };
  339. //重新编辑
  340. const editAsk = (data) => {
  341. inputText.value = data;
  342. };
  343. //获取消息列表
  344. async function handleSend(data) {
  345. refreshText.value = data;
  346. inputText.value = '';
  347. if (isThinking) {
  348. messageHistory.value.push({
  349. id: `user_${Date.now()}`,
  350. type: 'user',
  351. content: '',
  352. parsedContent: data,
  353. contentR1: '',
  354. parsedContentR1: '',
  355. timestamp: Date.now(),
  356. isShowThink: true,
  357. });
  358. } else {
  359. messageHistory.value.push({
  360. id: `user_${Date.now()}`,
  361. type: 'user',
  362. content: '',
  363. parsedContent: data,
  364. contentR1: '',
  365. parsedContentR1: '',
  366. timestamp: Date.now(),
  367. isShowThink: false,
  368. });
  369. }
  370. try {
  371. const response = await fetch('http://39.97.59.228:8000/v1/chat-messages', {
  372. method: 'POST',
  373. headers: {
  374. 'Content-Type': 'application/json',
  375. Authorization: APIKEY.value,
  376. },
  377. body: JSON.stringify({
  378. conversation_id: currentSessionID.value ? currentSessionID.value : '',
  379. query: data,
  380. response_mode: 'streaming',
  381. user: userid,
  382. inputs: {},
  383. }),
  384. });
  385. if (!response.ok) {
  386. throw new Error(`HTTP error! status: ${response.status}`);
  387. }
  388. const decoder = new TextDecoder('utf-8');
  389. const reader = response.body.getReader();
  390. let textBuffer = ''; // 使用字符串缓冲区来累积数据
  391. // 在组件中定义
  392. const currentProcessingMessage = ref(null);
  393. if (isThinking) {
  394. const newMessage = {
  395. id: `response_${Date.now()}`,
  396. type: 'response',
  397. content: '',
  398. parsedContent: '',
  399. contentR1: '',
  400. parsedContentR1: '',
  401. timestamp: Date.now(),
  402. isShowThink: true,
  403. };
  404. messageHistory.value.push(newMessage);
  405. currentProcessingMessage.value = newMessage; // 保存引用
  406. } else {
  407. const newMessage = {
  408. id: `response_${Date.now()}`,
  409. type: 'response',
  410. content: '',
  411. parsedContent: '',
  412. contentR1: '',
  413. parsedContentR1: '',
  414. timestamp: Date.now(),
  415. isShowThink: false,
  416. };
  417. messageHistory.value.push(newMessage);
  418. currentProcessingMessage.value = newMessage; // 保存引用
  419. }
  420. while (true) {
  421. const { done, value } = await reader.read();
  422. if (done) {
  423. if (textBuffer) {
  424. processLine(textBuffer);
  425. }
  426. break;
  427. }
  428. textBuffer += decoder.decode(value, { stream: true });
  429. // 处理每一行数据
  430. let lineIndex;
  431. while ((lineIndex = textBuffer.indexOf('\n')) !== -1) {
  432. const line = textBuffer.substring(0, lineIndex).trim();
  433. textBuffer = textBuffer.substring(lineIndex + 1);
  434. if (line) {
  435. processLine(line);
  436. }
  437. }
  438. }
  439. function processLine(line) {
  440. if (line.startsWith('data: ')) {
  441. try {
  442. const jsonStr = line.substring('data: '.length);
  443. const data = JSON.parse(jsonStr);
  444. switch (data.event) {
  445. case 'message':
  446. if (data.answer) {
  447. const targetMessage = messageHistory.value.find((msg) => msg.id === currentProcessingMessage.value.id);
  448. if (!targetMessage) break;
  449. let currentChunk = data.answer;
  450. // 检查是否包含起始标签
  451. const startIndex = currentChunk.indexOf('<think>');
  452. if (startIndex !== -1) {
  453. // 找到起始标签:将标签前的内容作为正文
  454. if (startIndex > 0) {
  455. targetMessage.content += currentChunk.substring(0, startIndex);
  456. targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
  457. }
  458. // 进入思考模式,将标签后的内容追加到contentR1
  459. Thinking.value = true;
  460. const remainingContent = currentChunk.substring(startIndex + '<think>'.length);
  461. if (remainingContent) {
  462. targetMessage.contentR1 += remainingContent;
  463. targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
  464. }
  465. } else if (Thinking.value) {
  466. // 结束标签
  467. const endIndex = currentChunk.indexOf('</think>');
  468. if (endIndex !== -1) {
  469. // 找到结束标签:标签前的内容追加到contentR1
  470. if (endIndex > 0) {
  471. targetMessage.contentR1 += currentChunk.substring(0, endIndex);
  472. targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
  473. }
  474. // 将标签后的内容作为正文
  475. const remainingContent = currentChunk.substring(endIndex + '</think>'.length);
  476. if (remainingContent) {
  477. targetMessage.content += remainingContent;
  478. targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
  479. }
  480. targetMessage.content += currentChunk;
  481. Thinking.value = false;
  482. targetMessage.isShowThink = false;
  483. } else {
  484. // 没有结束标签,继续追加到contentR1
  485. targetMessage.contentR1 += currentChunk;
  486. targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
  487. }
  488. } else {
  489. targetMessage.content += currentChunk;
  490. targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
  491. }
  492. scrollToBottom(); // 每次接收消息都滚动
  493. }
  494. if (data.task_id && !taskID.value) taskID.value = data.task_id;
  495. if (data.conversation_id && !currentSessionID.value) currentSessionID.value = data.conversation_id;
  496. if (data.message_id && !messageID.value) {
  497. messageID.value = data.message_id;
  498. }
  499. break;
  500. }
  501. } catch (error) {
  502. console.warn('Error parsing stream chunk:', error, 'Chunk:', line);
  503. }
  504. }
  505. }
  506. getNextSuggest();
  507. } catch (error) {
  508. console.error('Error in handleSend:', error);
  509. // 在 UI 上显示错误信息
  510. messageHistory.value.push({
  511. id: `system_${Date.now()}`,
  512. type: 'system',
  513. content: '',
  514. parsedContent: '请求错误',
  515. contentR1: '',
  516. parsedContentR1: '',
  517. timestamp: Date.now(),
  518. isShowThink: false,
  519. });
  520. }
  521. }
  522. //创建新对话
  523. async function addNew() {
  524. messageHistory.value = [];
  525. currentSessionID.value = '';
  526. taskID.value = '';
  527. messageID.value = '';
  528. }
  529. // 上传文件
  530. const showModal = () => {
  531. open.value = !open.value;
  532. };
  533. // 上传文件
  534. const handleBeforeUpload = async (file) => {
  535. const formData = new FormData();
  536. formData.append('file', file);
  537. formData.append('user', userid);
  538. try {
  539. const response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
  540. method: 'POST',
  541. headers: {
  542. Authorization: APIKEY.value,
  543. },
  544. body: formData,
  545. });
  546. const result = await response.json();
  547. if (response.ok) {
  548. message.success('上传成功');
  549. const linkText = `[${result.name}](${result.source_url})`;
  550. inputText.value += `\n${linkText}`;
  551. } else {
  552. message.error(`上传失败: ${result.message || '网络错误'}`);
  553. }
  554. } catch (error) {
  555. console.error('保存失败:', error);
  556. message.error('上传失败,请重试');
  557. }
  558. return false;
  559. };
  560. const handleRemove = (file) => {
  561. const index = fileList.value.findIndex((item) => item.uid === file.uid);
  562. if (index !== -1) {
  563. fileList.value.splice(index, 1);
  564. }
  565. };
  566. //停止响应
  567. async function stopReq() {
  568. try {
  569. let response = await fetch(`http://39.97.59.228:8000/v1/chat-messages/${taskID.value}/stop`, {
  570. method: 'POST',
  571. headers: {
  572. 'Content-Type': 'application/json',
  573. Authorization: APIKEY.value,
  574. },
  575. body: JSON.stringify({
  576. user: userid,
  577. }),
  578. });
  579. if (!response) {
  580. throw new Error('Network response was not ok');
  581. }
  582. } catch (error) {
  583. console.error('保存失败:', error);
  584. }
  585. }
  586. //获取具体会话记录
  587. async function sessionsHistory(id: string) {
  588. try {
  589. let response = await fetch(`http://39.97.59.228:8000/v1/messages?conversation_id=${id}&user=${userid}`, {
  590. method: 'GET',
  591. headers: {
  592. 'Content-Type': 'application/json',
  593. Authorization: APIKEY.value,
  594. },
  595. });
  596. const data = await response.json();
  597. console.log(data, '123');
  598. if (data.data.length > 0) {
  599. messageHistory.value = [];
  600. data.data.forEach((item: any) => {
  601. currentSessionID.value = item.conversation_id;
  602. messageHistory.value.push({
  603. id: `user_${Date.now()}`,
  604. type: 'user',
  605. content: '',
  606. parsedContent: item.query,
  607. contentR1: '',
  608. parsedContentR1: '',
  609. timestamp: Date.now(),
  610. isShowThink: false,
  611. });
  612. const answer = item.answer;
  613. // 查找<think>
  614. const startIndex = answer.indexOf('<think>');
  615. const endIndex = answer.indexOf('</think>');
  616. let content = '';
  617. let contentR1 = '';
  618. // 根据标签判断
  619. if (startIndex !== -1 && endIndex !== -1) {
  620. content = answer.substring(0, startIndex) + answer.substring(endIndex + '</think>'.length);
  621. contentR1 = answer.substring(startIndex + '<think>'.length, endIndex);
  622. } else if (startIndex !== -1) {
  623. // 只有起始标签
  624. content = answer.substring(0, startIndex);
  625. contentR1 = answer.substring(startIndex + '<think>'.length);
  626. } else if (endIndex !== -1) {
  627. // 只有结束标签
  628. content = answer.substring(endIndex + '</think>'.length);
  629. contentR1 = answer.substring(0, endIndex);
  630. } else {
  631. // 没有标签
  632. content = answer;
  633. }
  634. // 添加到消息历史
  635. messageHistory.value.push({
  636. id: `system_${Date.now()}`,
  637. type: 'system',
  638. content: '',
  639. parsedContent: parseMarkdownWithLatex(content.trim()),
  640. contentR1: '',
  641. parsedContentR1: parseMarkdownWithLatex(contentR1.trim()),
  642. timestamp: Date.now(),
  643. isShowThink: contentR1.length > 0, // 如果有思考内容则显示
  644. });
  645. });
  646. }
  647. if (!response.ok) {
  648. throw new Error('Network response was not ok');
  649. }
  650. } catch (error) {
  651. console.error('保存失败:', error);
  652. }
  653. editingId.value = null;
  654. }
  655. //获取下一轮建议问题列表
  656. //停止响应
  657. async function getNextSuggest() {
  658. console.log(messageID.value, '123');
  659. try {
  660. let response = await fetch(`http://39.97.59.228:8000/v1/messages/${messageID.value}/suggested?user=${userid}`, {
  661. method: 'GET',
  662. headers: {
  663. 'Content-Type': 'application/json',
  664. Authorization: APIKEY.value,
  665. },
  666. });
  667. const data = await response.json();
  668. suggestList.value = data.data;
  669. if (!response) {
  670. throw new Error('Network response was not ok');
  671. }
  672. } catch (error) {
  673. console.error('保存失败:', error);
  674. }
  675. }
  676. //编辑标题
  677. const startEditing = (item: ListItem) => {
  678. editingId.value = item.id;
  679. editText.value = item.name || '';
  680. };
  681. // 保存修改
  682. const handleSave = async (item: ListItem) => {
  683. try {
  684. let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}/name`, {
  685. method: 'POST',
  686. headers: {
  687. 'Content-Type': 'application/json',
  688. Authorization: APIKEY.value,
  689. },
  690. body: JSON.stringify({
  691. name: editText.value,
  692. user: userid,
  693. }),
  694. });
  695. if (!response.ok) {
  696. throw new Error('Network response was not ok');
  697. }
  698. item.name = editText.value;
  699. } catch (error) {
  700. console.error('保存失败:', error);
  701. }
  702. editingId.value = null;
  703. };
  704. // 删除会话
  705. const startDelete = async (item: ListItem) => {
  706. Modal.confirm({
  707. title: '确认删除',
  708. content: `确定要删除会话 "${item.name || '新会话'}" 吗?此操作不可撤销。`,
  709. okText: '确认',
  710. cancelText: '取消',
  711. onOk: async () => {
  712. // 原有删除逻辑不变
  713. try {
  714. let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}`, {
  715. method: 'DELETE',
  716. headers: {
  717. 'Content-Type': 'application/json',
  718. Authorization: APIKEY.value,
  719. },
  720. body: JSON.stringify({
  721. user: userid,
  722. }),
  723. });
  724. if (!response.ok) {
  725. throw new Error('Network response was not ok');
  726. }
  727. getHistoryList();
  728. } catch (error) {
  729. console.error('删除失败:', error);
  730. Modal.error({
  731. title: '删除失败',
  732. content: '删除会话时出现错误,请稍后重试。',
  733. });
  734. }
  735. },
  736. });
  737. };
  738. const fold = () => {
  739. isFold.value = !isFold.value;
  740. if (!isFold.value) {
  741. getHistoryList();
  742. }
  743. };
  744. // 获取历史会话列表
  745. async function getHistoryList() {
  746. let response = await fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
  747. method: 'get',
  748. headers: {
  749. 'Content-Type': 'application/json',
  750. Authorization: APIKEY.value,
  751. },
  752. });
  753. const data = await response.json();
  754. sessionHistory.value = data.data;
  755. data.data.forEach((item) => {
  756. currentSessionID.value = item.conversation_id;
  757. });
  758. }
  759. //格式化消息
  760. const formatMessage = (text) => {
  761. if (!text) return '';
  762. let formatted = text
  763. // 处理换行
  764. .replace(/\n\n/g, '<br>')
  765. .replace(/\n###/g, '<br>')
  766. .replace(/###/g, '')
  767. .replace(/---/g, '')
  768. .replace(/^- /gm, '<br> - ')
  769. // 处理粗体
  770. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  771. // 处理斜体
  772. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  773. // 处理行内代码
  774. .replace(/`([^`]+)`/g, '<code>$1</code>');
  775. return formatted;
  776. };
  777. // 初始化按钮定位
  778. onMounted(() => {
  779. getHistoryList();
  780. // 初始化(仅执行一次)
  781. initMarked();
  782. });
  783. </script>
  784. <style lang="less" scoped>
  785. .btn-header {
  786. width: 40px;
  787. height: 40px;
  788. margin-right: 5px;
  789. margin-bottom: 5px;
  790. }
  791. .mini-chat {
  792. display: flex;
  793. width: 800px;
  794. height: 85%;
  795. border-radius: 4px;
  796. position: fixed;
  797. top: 70px;
  798. right: 20px;
  799. background-color: rgb(255, 255, 255);
  800. background: url('../../assets/images/warn-dialog-bg.png') no-repeat center;
  801. background-size: 103% 124%;
  802. z-index: 9999999;
  803. color: #fff;
  804. }
  805. .left-side {
  806. background: #0c2842;
  807. transition: width 0.5s ease; /* 平滑过渡动画 */
  808. width: 140px; /* 展开时宽度 */
  809. position: relative; /* 用于按钮定位 */
  810. }
  811. .left-side.collapsed {
  812. width: 40px; /* 折叠时宽度 */
  813. }
  814. .addBtn {
  815. height: 30px;
  816. position: absolute;
  817. background-size: 100% 100%;
  818. background-position: center;
  819. padding: 2px;
  820. right: 10px;
  821. bottom: 40px;
  822. left: 10px;
  823. align-items: center;
  824. border-radius: 3px;
  825. cursor: pointer;
  826. }
  827. .custom-list {
  828. height: 680px;
  829. overflow-y: auto;
  830. }
  831. .text-container {
  832. display: flex;
  833. justify-content: space-between;
  834. width: 100%;
  835. overflow: hidden;
  836. }
  837. .btn-container {
  838. display: flex;
  839. }
  840. .jeecg-layout-header-action span[role='img'] {
  841. padding: 0;
  842. }
  843. .text-ellipsis {
  844. flex: 1;
  845. }
  846. .edit-text {
  847. overflow: hidden;
  848. text-overflow: ellipsis;
  849. white-space: nowrap;
  850. width: 90px;
  851. color: #fff;
  852. font-size: 12px;
  853. cursor: pointer;
  854. }
  855. .edit-icon {
  856. flex-shrink: 0;
  857. margin-left: auto;
  858. line-height: 23px;
  859. }
  860. .delete-icon {
  861. flex-shrink: 0;
  862. margin-left: auto;
  863. line-height: 23px;
  864. }
  865. .edit-icon:hover {
  866. color: #1890ff !important;
  867. cursor: pointer;
  868. }
  869. .delete-icon:hover {
  870. color: #1890ff !important;
  871. cursor: pointer;
  872. }
  873. .edit-input {
  874. font-size: 10px;
  875. }
  876. .btn-text-bg {
  877. width: 14px;
  878. height: 14px;
  879. position: absolute;
  880. background-size: 100% 100%;
  881. right: 10px;
  882. top: 10px;
  883. left: 10px;
  884. bottom: 10px;
  885. }
  886. .btn-text {
  887. margin-left: 3px;
  888. font-size: 12px;
  889. color: #fff;
  890. white-space: nowrap;
  891. margin-left: 30px;
  892. line-height: 29px;
  893. }
  894. .historyBtn {
  895. width: 20px;
  896. height: 20px;
  897. position: absolute;
  898. background-size: 100% 100%;
  899. background-position: center;
  900. padding: 2px;
  901. right: 10px;
  902. top: 10px;
  903. }
  904. .historyBtn1 {
  905. width: 20px;
  906. height: 20px;
  907. position: absolute;
  908. background-size: 100% 100%;
  909. background-position: center;
  910. left: 3px;
  911. }
  912. .divider0 {
  913. border-bottom: 1px solid #1074c1;
  914. width: auto;
  915. margin: 0 10px;
  916. height: 13%;
  917. display: block;
  918. background: transparent;
  919. }
  920. .foldBtn {
  921. width: 20px;
  922. height: 20px;
  923. position: absolute;
  924. background-size: 100% 100%;
  925. background-position: center;
  926. padding: 2px;
  927. right: 10px;
  928. bottom: 10px;
  929. cursor: pointer;
  930. }
  931. .right-side {
  932. flex: 1;
  933. display: flex;
  934. flex-direction: column;
  935. width: calc(100% - 134px) !important;
  936. .title {
  937. text-align: center;
  938. font-size: 14px;
  939. padding: 5px;
  940. }
  941. .dialog-area {
  942. flex: 1;
  943. gap: 30px;
  944. overflow-y: auto;
  945. padding: 5px;
  946. display: flex;
  947. flex-direction: column;
  948. color: #fff;
  949. .ask-message {
  950. padding: 10px;
  951. border-radius: 5px;
  952. background: #0c2842;
  953. max-width: 300px;
  954. min-width: 50px;
  955. white-space: pre-wrap;
  956. word-wrap: break-word;
  957. overflow-wrap: break-word;
  958. overflow: auto;
  959. line-height: 1.5;
  960. }
  961. .answer-message {
  962. padding: 10px;
  963. border-radius: 5px;
  964. background: #0c2842;
  965. max-width: 90%;
  966. }
  967. }
  968. .input-area {
  969. margin: 10px 10px 20px 10px;
  970. padding: 10px;
  971. background-color: #043256;
  972. border: 1px solid #2cb6ff;
  973. border-radius: 5px;
  974. display: flex;
  975. flex-direction: column;
  976. justify-content: space-between;
  977. gap: 5px;
  978. height: 25%;
  979. }
  980. /* 文件列表容器 */
  981. .uploaded-files {
  982. padding: 8px;
  983. background-color: #f5f5f5;
  984. border-radius: 4px;
  985. min-height: 40px;
  986. }
  987. /* 单个文件项 */
  988. .file-item {
  989. display: inline-flex;
  990. align-items: center;
  991. padding: 4px 8px;
  992. margin-right: 8px;
  993. margin-bottom: 8px;
  994. background-color: #fff;
  995. border: 1px solid #e9e9e9;
  996. border-radius: 4px;
  997. }
  998. /* 文件名 */
  999. .file-name {
  1000. margin-left: 8px;
  1001. margin-right: 8px;
  1002. max-width: 150px;
  1003. white-space: nowrap;
  1004. overflow: hidden;
  1005. text-overflow: ellipsis;
  1006. }
  1007. .ant-input {
  1008. border: none;
  1009. background-color: rgba(255, 255, 255, 0) !important;
  1010. color: #fff;
  1011. }
  1012. .ant-input:focus {
  1013. border: none; /* 聚焦时无边框 */
  1014. outline: none; /* 聚焦时无轮廓 */
  1015. box-shadow: none; /* 移除可能存在的阴影效果 */
  1016. }
  1017. .ctrl-btn {
  1018. display: flex;
  1019. flex-direction: row;
  1020. justify-content: space-between;
  1021. }
  1022. .question-input {
  1023. background-color: #1e293b !important;
  1024. border-color: #334155 !important;
  1025. color: #e2e8f0 !important;
  1026. border-radius: 8px !important;
  1027. padding: 12px 16px !important;
  1028. font-size: 14px !important;
  1029. }
  1030. .question-input::placeholder {
  1031. color: #64748b !important;
  1032. }
  1033. .control-btn {
  1034. height: 25px;
  1035. background-color: #043256;
  1036. border: 1px solid #2cb6ff;
  1037. color: #fff;
  1038. font-size: 10px;
  1039. margin-right: 10px;
  1040. cursor: pointer;
  1041. transition: background-color 0.2s ease; /* 平滑过渡效果 */
  1042. }
  1043. /* 激活状态样式(点击后) */
  1044. .control-btn.active {
  1045. background-color: #2cb6ff; /* 蓝色背景(Ant Design 主色) */
  1046. color: white; /* 白色文字 */
  1047. }
  1048. .control-btn1 {
  1049. height: 20px;
  1050. background-color: #234a6b;
  1051. border: 1px solid #234a6b;
  1052. color: #fff;
  1053. font-size: 10px;
  1054. margin-right: 10px;
  1055. cursor: pointer;
  1056. transition: all 0.2s;
  1057. }
  1058. .control-btn2 {
  1059. height: 20px;
  1060. background-color: #2cb6ff;
  1061. border: 1px solid #2cb6ff;
  1062. color: #fff;
  1063. font-size: 10px;
  1064. margin-right: 10px;
  1065. cursor: pointer;
  1066. transition: all 0.2s;
  1067. }
  1068. /* 文件上传区 */
  1069. .file-upload {
  1070. position: absolute;
  1071. right: 20px;
  1072. bottom: 70px;
  1073. width: 180px;
  1074. display: flex;
  1075. flex-direction: column;
  1076. gap: 10px;
  1077. border: 1px solid #2cb6ff;
  1078. background-color: #234a6b;
  1079. border-radius: 6px;
  1080. padding: 10px;
  1081. }
  1082. .input-container {
  1083. position: relative;
  1084. display: flex;
  1085. align-items: center;
  1086. width: 100%;
  1087. }
  1088. .file-input {
  1089. flex: 1;
  1090. background-color: #234a6b;
  1091. border-color: #2cb6ff !important;
  1092. color: #e2e8f0 !important;
  1093. border-radius: 6px !important;
  1094. font-size: 10px !important;
  1095. padding-right: 70px !important;
  1096. height: 36px !important;
  1097. width: 100% !important;
  1098. }
  1099. .confirm-btn {
  1100. position: absolute;
  1101. right: 5px;
  1102. background-color: #2cb6ff;
  1103. border: none;
  1104. color: #fff;
  1105. border-radius: 4px;
  1106. font-size: 12px;
  1107. padding: 4px 10px;
  1108. cursor: pointer;
  1109. transition: all 0.2s;
  1110. height: 28px;
  1111. }
  1112. .confirm-btn:hover {
  1113. background-color: #2cb6ff;
  1114. }
  1115. .custom-upload {
  1116. width: 100%;
  1117. padding: 0 !important;
  1118. }
  1119. .upload-btn {
  1120. background-color: #234a6b !important;
  1121. border: 1px solid #2188c3 !important;
  1122. color: #dbeafe !important;
  1123. border-radius: 6px !important;
  1124. font-size: 12px !important;
  1125. cursor: pointer;
  1126. transition: all 0.2s;
  1127. padding: 8px 0 !important;
  1128. width: 190% !important;
  1129. height: 36px !important;
  1130. box-sizing: border-box !important;
  1131. }
  1132. .upload-btn:hover {
  1133. background-color: #1f84bd !important;
  1134. color: #fff !important;
  1135. }
  1136. .custom-upload .ant-upload-select:hover .ant-btn {
  1137. border-color: #1f84bd !important;
  1138. }
  1139. .message-wrapper.ai-message-wrapper {
  1140. display: flex;
  1141. align-items: flex-start;
  1142. }
  1143. .answerIcon {
  1144. flex: 0 0 45px;
  1145. }
  1146. .suggestion-item {
  1147. height: 30px;
  1148. margin-left: 45px;
  1149. display: flex;
  1150. align-items: center;
  1151. justify-content: space-between;
  1152. padding: 10px 16px;
  1153. border: 1px solid #1890ff;
  1154. color: white;
  1155. border-radius: 4px;
  1156. cursor: pointer;
  1157. width: 33%;
  1158. font-size: 12px;
  1159. }
  1160. .thinking-section {
  1161. border-left: 3px solid #e5e7eb;
  1162. padding-left: 12px;
  1163. margin-bottom: 16px;
  1164. }
  1165. .thinking-header {
  1166. display: flex;
  1167. justify-content: space-between;
  1168. align-items: center;
  1169. padding: 8px 0;
  1170. cursor: pointer;
  1171. user-select: none;
  1172. }
  1173. .thinking-title {
  1174. font-size: 14px;
  1175. font-weight: 500;
  1176. color: #6b7280;
  1177. }
  1178. }
  1179. </style>
  1180. <style scoped>
  1181. .zxm-popover-inner-content {
  1182. padding: 1px;
  1183. }
  1184. .message-wrapper {
  1185. display: flex;
  1186. align-items: flex-start;
  1187. position: relative;
  1188. }
  1189. .user-message-wrapper {
  1190. flex-direction: row-reverse;
  1191. }
  1192. .ai-message-wrapper {
  1193. flex-direction: row;
  1194. }
  1195. /* 鼠标滑过 .message-wrapper 时,显示图标 */
  1196. .ai-message-wrapper:hover .copy-icon-container {
  1197. opacity: 1;
  1198. visibility: visible;
  1199. }
  1200. .user-message-wrapper:hover .copy-icon-container {
  1201. opacity: 1;
  1202. visibility: visible;
  1203. }
  1204. .copy-icon {
  1205. font-size: 16px;
  1206. color: #666;
  1207. cursor: pointer;
  1208. }
  1209. /* 默认隐藏,hover 时显示 */
  1210. .copy-icon-container {
  1211. position: absolute;
  1212. display: flex;
  1213. flex-direction: row;
  1214. bottom: -20px;
  1215. right: 15%;
  1216. opacity: 0;
  1217. visibility: hidden;
  1218. transition: all 0.3s ease;
  1219. z-index: 10;
  1220. }
  1221. /* 复制图标样式 */
  1222. .copy-icon {
  1223. font-size: 16px;
  1224. cursor: pointer;
  1225. margin-right: 20%;
  1226. float: right;
  1227. color: #fff;
  1228. }
  1229. .message-wrapper:hover .copy-icon {
  1230. opacity: 1;
  1231. }
  1232. .copy-icon:hover {
  1233. color: #1890ff;
  1234. }
  1235. ::v-deep table {
  1236. border-collapse: collapse;
  1237. width: 100%;
  1238. margin: 10px 0;
  1239. border: 1px solid #333;
  1240. }
  1241. ::v-deep th {
  1242. border: 1px solid #333;
  1243. background-color: #234a6b;
  1244. padding: 5px;
  1245. text-align: center;
  1246. }
  1247. ::v-deep td {
  1248. border: 1px solid #ddd;
  1249. padding: 8px 12px;
  1250. text-align: center;
  1251. }
  1252. </style>