MiniChat.vue 31 KB

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