MiniChat.vue 42 KB

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