MiniChat.vue 52 KB

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