MiniChat.vue 44 KB

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