MiniChat.vue 43 KB

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