MiniChat.vue 53 KB

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