MiniChat.vue 53 KB

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