MiniChat.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  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 v-if="isShowChatBroad" class="mini-chat">
  9. <!-- 左侧折叠区域 -->
  10. <div class="left-side" :class="{ collapsed: isFold }" id="leftSide">
  11. <div
  12. v-if="isFold"
  13. class="historyBtn"
  14. :style="{ backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/history.svg' : '/src/assets/images/vent/home/history.svg'})` }"
  15. @click="addNew"
  16. ></div>
  17. <div v-else class="historyBtn1">
  18. <span
  19. class="btn-text-bg"
  20. :style="{
  21. backgroundImage: `url(${!isFold ? '/src/assets/images/vent/home/history.svg' : ''})`,
  22. }"
  23. ></span>
  24. <span v-if="!isFold" class="btn-text">历史对话</span>
  25. <a-list style="width: 130px" :split="false" :data-source="sessionHistory" :scroll="200" class="custom-list">
  26. <template #renderItem="{ item }">
  27. <a-list-item
  28. class="session-item"
  29. :style="{
  30. padding: '8px 10px 0 8px',
  31. color: '#5e7081',
  32. fontSize: '10px',
  33. position: 'relative', // 新增定位
  34. }"
  35. >
  36. <!-- 新增flex布局容器 -->
  37. <div style="width: 100%">
  38. <div v-if="editingId !== item.id" class="text-container">
  39. <span class="edit-text" @click="sessionsHistory(item.id)">{{ item.name }}</span>
  40. <div class="btn-container">
  41. <EditOutlined class="edit-icon" @click="startEditing(item)" />
  42. <DeleteOutlined class="delete-icon" @click="startDelete(item)" />
  43. </div>
  44. </div>
  45. <!-- 输入框 -->
  46. <a-input
  47. size="small"
  48. v-else
  49. v-model:value="editText"
  50. v-focus
  51. @blur="handleSave(item)"
  52. @keyup.enter="handleSave(item)"
  53. class="edit-input"
  54. />
  55. </div>
  56. </a-list-item>
  57. </template>
  58. </a-list>
  59. </div>
  60. <div
  61. class="foldBtn"
  62. :style="{ backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/Fold.svg' : '/src/assets/images/vent/home/unfold.svg'})` }"
  63. @click="fold"
  64. ></div>
  65. </div>
  66. <!-- 右侧对话框 -->
  67. <div class="right-side">
  68. <!-- 对话区域 -->
  69. <div class="dialog-area">
  70. <div
  71. v-for="message in messageHistory"
  72. :key="message.id"
  73. class="flex items-center w-100%"
  74. :style="{ alignSelf: message.type === 'user' ? 'flex-end' : 'flex-start' }"
  75. >
  76. <template v-if="message.type === 'user'">
  77. <div class="flex-grow-1"></div>
  78. <div class="message-wrapper user-message-wrapper">
  79. <div class="ask-message">{{ message.content }}</div>
  80. <CopyOutlined class="copy-icon" @click="copyToClipboard(message.content)" title="复制消息" />
  81. </div>
  82. </template>
  83. <template v-else>
  84. <SvgIcon size="80" class="ml-2px mr-2px" name="ai-logo" />
  85. <div class="message-wrapper ai-message-wrapper">
  86. <div class="answer-message">
  87. <div v-if="message.contentR1" class="color-gray font-size-12px" v-html="message.contentR1"> </div>
  88. <div v-else v-html="message.content"> </div>
  89. </div>
  90. <CopyOutlined class="copy-icon" @click="copyToClipboard(message.contentR1 || message.content)" title="复制消息" />
  91. </div>
  92. </template>
  93. </div>
  94. </div>
  95. <!-- 底部输入区 -->
  96. <div class="input-area">
  97. <a-input v-model:value="inputText" placeholder="请输入你的问题" @keyup.enter="handleSend(inputText)" class="ant-input" />
  98. <div class="ctrl-btn">
  99. <div class="input-controls">
  100. <button class="control-btn">深度学习</button>
  101. <button class="control-btn" @click="stopReq()">停止响应</button>
  102. </div>
  103. <div class="action-bar">
  104. <Space>
  105. <Button class="control-btn1" size="small" @click="showModal()">
  106. <template #icon>
  107. <SvgIcon name="send-file" />
  108. </template>
  109. </Button>
  110. <Button class="control-btn2" size="small" @click="handleSend(inputText)">
  111. <template #icon>
  112. <SvgIcon name="send" />
  113. </template>
  114. </Button>
  115. </Space>
  116. </div>
  117. </div>
  118. <!-- 右侧文件上传区 -->
  119. <div v-if="open" class="file-upload">
  120. <!-- 输入框区域,包含确认按钮 -->
  121. <div class="input-container">
  122. <a-input v-model:value="filePath" placeholder="输入文件连接" class="file-input" @pressEnter="handlePathConfirm" />
  123. <button class="confirm-btn" @click="handlePathConfirm">确认</button>
  124. </div>
  125. <!-- 上传按钮 -->
  126. <!-- <a-upload> <button class="upload-btn" @click="customUpload">从本地上传</button></a-upload> -->
  127. <a-upload class="custom-upload" name="file" :multiple="false">
  128. <a-button class="upload-btn" @click="customUpload">
  129. <UploadOutlined></UploadOutlined>
  130. 从本地上传
  131. </a-button>
  132. </a-upload>
  133. </div>
  134. </div>
  135. </div>
  136. </div>
  137. </template>
  138. <script lang="ts" setup>
  139. import { ref, onMounted } from 'vue';
  140. import { SvgIcon } from '../Icon';
  141. import { Space, Button, Modal, Input, message } from 'ant-design-vue';
  142. // import AIChat from './index.vue';
  143. import { useUserStore } from '/@/store/modules/user';
  144. import { EditOutlined, DeleteOutlined, UploadOutlined, CopyOutlined } from '@ant-design/icons-vue';
  145. import { createVNode } from 'vue';
  146. const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
  147. const inputText = ref(''); // 输入框内容
  148. const sessionHistory = ref([]);
  149. const isShowChatBroad = ref(false);
  150. const editingId = ref<number | null>(null);
  151. const editText = ref('');
  152. const currentSessionID = ref('');
  153. const taskID = ref('');
  154. const open = ref<boolean>(false);
  155. interface ListItem {
  156. id: number;
  157. name?: string;
  158. }
  159. interface Message {
  160. id: string; // 唯一标识(可用时间戳生成)
  161. type: 'user' | 'system' | 'response';
  162. content: string;
  163. /** 深度思考时的文本 */
  164. contentR1: string;
  165. timestamp: number; // 排序依据
  166. }
  167. // 定义消息历史数组类型
  168. const messageHistory = ref<Message[]>([]);
  169. const isFold = ref(true); // 是否折叠
  170. const userid = useUserStore().getUserInfo.id as string;
  171. const filePath = ref(''); // 绑定输入框值
  172. const showConfirmBtn = ref(false); // 控制确认按钮显示状态
  173. const fileList = ref([]);
  174. function showAIChat() {
  175. isShowChatBroad.value = !isShowChatBroad.value;
  176. }
  177. //获取消息列表
  178. // async function handleSend(data) {
  179. // messageHistory.value.push({
  180. // id: `user_${Date.now()}`,
  181. // type: 'user',
  182. // content: data,
  183. // contentR1: '',
  184. // timestamp: Date.now(),
  185. // });
  186. // // 发送 POST 请求
  187. // fetch('http://39.97.59.228:8000/v1/chat-messages', {
  188. // method: 'POST',
  189. // headers: {
  190. // 'Content-Type': 'application/json',
  191. // Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  192. // },
  193. // body: JSON.stringify({
  194. // conversation_id: currentSessionID.value,
  195. // query: data,
  196. // response_mode: 'streaming',
  197. // user: userid,
  198. // inputs: {},
  199. // }),
  200. // }).then((response) => {
  201. // const decoder = new TextDecoder('utf-8');
  202. // let buffer = [];
  203. // // 获取可读流
  204. // const reader = response.body.getReader();
  205. // const newMessage = {
  206. // id: `response_${Date.now()}`,
  207. // type: 'response' as any,
  208. // content: '',
  209. // contentR1: '',
  210. // timestamp: Date.now(),
  211. // };
  212. // messageHistory.value.push(newMessage);
  213. // // 读取数据
  214. // function read() {
  215. // return reader.read().then(({ done, value }) => {
  216. // if (done) {
  217. // return buffer;
  218. // }
  219. // // 解码数据块
  220. // const chunk = decoder.decode(value, { stream: false });
  221. // // 处理每段数据
  222. // const processedData = processStreamChunk(chunk);
  223. // buffer = buffer.concat(processedData);
  224. // // 继续读取
  225. // return read();
  226. // });
  227. // }
  228. // // 开始读取
  229. // return read();
  230. // function processStreamChunk(chunk) {
  231. // try {
  232. // // 移除 "data: " 前缀
  233. // const jsonStr = chunk.replace('data: ', '');
  234. // const data = JSON.parse(jsonStr);
  235. // const targetMessage = messageHistory.value.find((msg) => msg.id === newMessage.id);
  236. // if (!targetMessage) return;
  237. // // 根据事件类型分发处理
  238. // switch (data.event) {
  239. // case 'message':
  240. // if (!taskID.value && !currentSessionID.value) {
  241. // taskID.value = data.task_id;
  242. // currentSessionID.value = data.conversation_id;
  243. // }
  244. // targetMessage.content += data.answer; // 追加内容
  245. // break;
  246. // }
  247. // return data;
  248. // } catch (error) {
  249. // // 请求失败时设置系统消息
  250. // return null;
  251. // }
  252. // }
  253. // });
  254. // inputText.value = '';
  255. // }
  256. // 复制消息
  257. function copyToClipboard(text) {
  258. if (!text || text.trim() === '') {
  259. message.warn('没有可复制的内容');
  260. return;
  261. }
  262. // 2. 创建临时textarea 元素
  263. const textarea = document.createElement('textarea');
  264. textarea.value = text;
  265. textarea.style.position = 'fixed';
  266. textarea.style.top = '-999px';
  267. textarea.style.left = '-999px';
  268. textarea.style.width = '200px';
  269. textarea.style.height = '200px';
  270. document.body.appendChild(textarea);
  271. try {
  272. textarea.select();
  273. textarea.setSelectionRange(0, text.length);
  274. const isSuccessful = document.execCommand('copy');
  275. if (isSuccessful) {
  276. message.success('复制成功!');
  277. } else {
  278. throw new Error('复制命令执行失败');
  279. }
  280. } catch (err) {
  281. console.error('复制失败:', err);
  282. message.error('复制失败,请手动复制');
  283. } finally {
  284. document.body.removeChild(textarea);
  285. }
  286. }
  287. async function handleSend(data) {
  288. inputText.value = '';
  289. messageHistory.value.push({
  290. id: `user_${Date.now()}`,
  291. type: 'user',
  292. content: data,
  293. contentR1: '',
  294. timestamp: Date.now(),
  295. });
  296. try {
  297. const response = await fetch('http://39.97.59.228:8000/v1/chat-messages', {
  298. method: 'POST',
  299. headers: {
  300. 'Content-Type': 'application/json',
  301. Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  302. },
  303. body: JSON.stringify({
  304. conversation_id: currentSessionID.value,
  305. query: data,
  306. response_mode: 'streaming',
  307. user: userid,
  308. inputs: {},
  309. }),
  310. });
  311. if (!response.ok) {
  312. throw new Error(`HTTP error! status: ${response.status}`);
  313. }
  314. const decoder = new TextDecoder('utf-8');
  315. const reader = response.body.getReader();
  316. let textBuffer = ''; // 使用字符串缓冲区来累积数据
  317. const newMessage = {
  318. id: `response_${Date.now()}`,
  319. type: 'response',
  320. content: '',
  321. contentR1: '',
  322. timestamp: Date.now(),
  323. };
  324. messageHistory.value.push(newMessage);
  325. while (true) {
  326. const { done, value } = await reader.read();
  327. if (done) {
  328. if (textBuffer) {
  329. processLine(textBuffer);
  330. }
  331. break;
  332. }
  333. textBuffer += decoder.decode(value, { stream: true });
  334. // 处理每一行数据
  335. let lineIndex;
  336. while ((lineIndex = textBuffer.indexOf('\n')) !== -1) {
  337. const line = textBuffer.substring(0, lineIndex).trim();
  338. textBuffer = textBuffer.substring(lineIndex + 1);
  339. if (line) {
  340. processLine(line);
  341. }
  342. }
  343. }
  344. function processLine(line) {
  345. if (line.startsWith('data: ')) {
  346. try {
  347. const jsonStr = line.substring('data: '.length);
  348. const data = JSON.parse(jsonStr);
  349. switch (data.event) {
  350. case 'message':
  351. if (data.answer) {
  352. const targetMessage = messageHistory.value.find((msg) => msg.id === newMessage.id);
  353. if (targetMessage) {
  354. targetMessage.content += data.answer;
  355. }
  356. }
  357. if (data.task_id && !taskID.value) taskID.value = data.task_id;
  358. if (data.conversation_id && !currentSessionID.value) currentSessionID.value = data.conversation_id;
  359. break;
  360. }
  361. } catch (error) {
  362. console.warn('Error parsing stream chunk:', error, 'Chunk:', line);
  363. }
  364. }
  365. }
  366. } catch (error) {
  367. console.error('Error in handleSend:', error);
  368. // 在 UI 上显示错误信息
  369. messageHistory.value.push({
  370. id: `system_${Date.now()}`,
  371. type: 'system',
  372. content: '请求错误',
  373. contentR1: '',
  374. timestamp: Date.now(),
  375. });
  376. }
  377. }
  378. // 上传文件
  379. const showModal = () => {
  380. open.value = !open.value;
  381. };
  382. async function customUpload(data) {
  383. const formData = new FormData();
  384. if (!data) {
  385. return message.warn('请选择文件');
  386. }
  387. // 添加文件参数
  388. formData.append('file', data.file);
  389. // 添加用户标识参数
  390. formData.append('user', userid);
  391. try {
  392. let response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
  393. method: 'POST',
  394. headers: {
  395. Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  396. },
  397. body: formData,
  398. });
  399. // if (response) {
  400. // message.success('上传成功');
  401. // }
  402. console.log(response, '123');
  403. if (!response) {
  404. throw new Error('Network response was not ok');
  405. }
  406. } catch (error) {
  407. console.error('保存失败:', error);
  408. }
  409. }
  410. //停止响应
  411. async function stopReq() {
  412. try {
  413. let response = await fetch(`http://39.97.59.228:8000/v1/chat-messages/${taskID}/stop`, {
  414. method: 'POST',
  415. headers: {
  416. 'Content-Type': 'application/json',
  417. Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  418. },
  419. body: JSON.stringify({
  420. user: userid,
  421. }),
  422. });
  423. if (!response) {
  424. throw new Error('Network response was not ok');
  425. }
  426. } catch (error) {
  427. console.error('保存失败:', error);
  428. }
  429. }
  430. //获取具体会话记录
  431. async function sessionsHistory(id: string) {
  432. console.log(id, '123');
  433. try {
  434. let response = await fetch(`http://39.97.59.228:8000/v1/messages?conversation_id=${id}&user=${userid}`, {
  435. method: 'GET',
  436. headers: {
  437. 'Content-Type': 'application/json',
  438. Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  439. },
  440. });
  441. const data = await response.json();
  442. console.log(data, '123');
  443. if (data.data.length > 0) {
  444. messageHistory.value = [];
  445. data.data.forEach((item: any) => {
  446. messageHistory.value.push({
  447. id: `user_${Date.now()}`,
  448. type: 'user',
  449. content: item.query,
  450. contentR1: '',
  451. timestamp: Date.now(),
  452. });
  453. messageHistory.value.push({
  454. id: `system_${Date.now()}`,
  455. type: 'system',
  456. content: item.answer,
  457. contentR1: '',
  458. timestamp: Date.now(),
  459. });
  460. });
  461. }
  462. if (!response.ok) {
  463. throw new Error('Network response was not ok');
  464. }
  465. } catch (error) {
  466. console.error('保存失败:', error);
  467. }
  468. editingId.value = null;
  469. }
  470. //编辑标题
  471. const startEditing = (item: ListItem) => {
  472. editingId.value = item.id;
  473. editText.value = item.name || '';
  474. };
  475. // 输入框确认按钮点击事件
  476. async function handlePathConfirm() {
  477. const formData = new FormData();
  478. // 添加文件参数
  479. formData.append('file', filePath.value);
  480. // 添加用户标识参数
  481. formData.append('user', userid);
  482. try {
  483. let response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
  484. method: 'POST',
  485. headers: {
  486. Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  487. },
  488. body: formData,
  489. });
  490. if (!response.ok) {
  491. throw new Error('Network response was not ok');
  492. }
  493. } catch (error) {
  494. console.error('保存失败:', error);
  495. }
  496. console.log('确认的文件路径:', filePath.value);
  497. // 这里可以添加路径验证、保存等逻辑
  498. filePath.value = ''; // 可选:确认后清空输入框
  499. showConfirmBtn.value = false;
  500. }
  501. // 保存修改
  502. const handleSave = async (item: ListItem) => {
  503. try {
  504. let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}/name`, {
  505. method: 'POST',
  506. headers: {
  507. 'Content-Type': 'application/json',
  508. Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  509. },
  510. body: JSON.stringify({
  511. name: editText.value,
  512. user: userid,
  513. }),
  514. });
  515. if (!response.ok) {
  516. throw new Error('Network response was not ok');
  517. }
  518. item.name = editText.value;
  519. } catch (error) {
  520. console.error('保存失败:', error);
  521. }
  522. editingId.value = null;
  523. };
  524. // 删除会话
  525. const startDelete = async (item: ListItem) => {
  526. Modal.confirm({
  527. title: '确认删除',
  528. content: `确定要删除会话 "${item.name || '新会话'}" 吗?此操作不可撤销。`,
  529. okText: '确认',
  530. cancelText: '取消',
  531. onOk: async () => {
  532. // 原有删除逻辑不变
  533. try {
  534. let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}`, {
  535. method: 'DELETE',
  536. headers: {
  537. 'Content-Type': 'application/json',
  538. Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  539. },
  540. body: JSON.stringify({
  541. user: userid,
  542. }),
  543. });
  544. if (!response.ok) {
  545. throw new Error('Network response was not ok');
  546. }
  547. getHistoryList();
  548. } catch (error) {
  549. console.error('删除失败:', error);
  550. Modal.error({
  551. title: '删除失败',
  552. content: '删除会话时出现错误,请稍后重试。',
  553. });
  554. }
  555. },
  556. });
  557. };
  558. const fold = () => {
  559. isFold.value = !isFold.value;
  560. if (!isFold.value) {
  561. getHistoryList();
  562. }
  563. };
  564. // 获取历史会话列表
  565. async function getHistoryList() {
  566. let response = await fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
  567. method: 'get',
  568. headers: {
  569. 'Content-Type': 'application/json',
  570. Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
  571. },
  572. });
  573. const data = await response.json();
  574. sessionHistory.value = data.data;
  575. console.log(sessionHistory.value, '123');
  576. }
  577. // 初始化按钮定位
  578. onMounted(() => {
  579. getHistoryList();
  580. });
  581. </script>
  582. <style lang="less" scoped>
  583. .btn-header {
  584. width: 40px;
  585. height: 40px;
  586. margin-right: 5px;
  587. margin-bottom: 5px;
  588. }
  589. .mini-chat {
  590. display: flex;
  591. width: 500px;
  592. height: 400px;
  593. border-radius: 4px;
  594. position: fixed;
  595. top: 60px;
  596. right: 20px;
  597. background-color: rgb(255, 255, 255);
  598. background: url('../../assets/images/warn-dialog-bg.png') no-repeat center;
  599. background-size: 100% 100%;
  600. z-index: 9999999;
  601. color: #fff;
  602. }
  603. .left-side {
  604. background: #0c2842;
  605. transition: width 0.5s ease; /* 平滑过渡动画 */
  606. width: 140px; /* 展开时宽度 */
  607. position: relative; /* 用于按钮定位 */
  608. }
  609. .left-side.collapsed {
  610. width: 40px; /* 折叠时宽度 */
  611. }
  612. .custom-list {
  613. height: 325px;
  614. overflow-y: auto;
  615. }
  616. .text-container {
  617. display: flex;
  618. justify-content: space-between;
  619. width: 100%;
  620. overflow: hidden;
  621. }
  622. .btn-container {
  623. display: flex;
  624. }
  625. .jeecg-layout-header-action span[role='img'] {
  626. padding: 0;
  627. }
  628. .text-ellipsis {
  629. flex: 1;
  630. }
  631. .edit-text {
  632. overflow: hidden;
  633. text-overflow: ellipsis;
  634. white-space: nowrap;
  635. width: 90px;
  636. color: #fff;
  637. font-size: 12px;
  638. cursor: pointer;
  639. }
  640. .edit-icon {
  641. flex-shrink: 0;
  642. margin-left: auto;
  643. line-height: 23px;
  644. }
  645. .delete-icon {
  646. flex-shrink: 0;
  647. margin-left: auto;
  648. line-height: 23px;
  649. }
  650. .edit-icon:hover {
  651. color: #1890ff !important;
  652. cursor: pointer;
  653. }
  654. .delete-icon:hover {
  655. color: #1890ff !important;
  656. cursor: pointer;
  657. }
  658. .edit-input {
  659. font-size: 10px;
  660. }
  661. .btn-text-bg {
  662. width: 14px;
  663. height: 14px;
  664. position: absolute;
  665. background-size: 100% 100%;
  666. right: 10px;
  667. top: 10px;
  668. left: 10px;
  669. bottom: 10px;
  670. }
  671. .btn-text {
  672. margin-left: 3px;
  673. font-size: 12px;
  674. color: #fff;
  675. white-space: nowrap;
  676. margin-left: 30px;
  677. line-height: 35px;
  678. }
  679. .historyBtn {
  680. width: 20px;
  681. height: 20px;
  682. position: absolute;
  683. background-size: 100% 100%;
  684. background-position: center;
  685. padding: 2px;
  686. right: 10px;
  687. top: 10px;
  688. }
  689. .historyBtn1 {
  690. width: 20px;
  691. height: 20px;
  692. position: absolute;
  693. background-size: 100% 100%;
  694. background-position: center;
  695. left: 3px;
  696. }
  697. .divider0 {
  698. border-bottom: 1px solid #1074c1;
  699. width: auto;
  700. margin: 0 10px;
  701. height: 13%;
  702. display: block;
  703. background: transparent;
  704. }
  705. .foldBtn {
  706. width: 20px;
  707. height: 20px;
  708. position: absolute;
  709. background-size: 100% 100%;
  710. background-position: center;
  711. padding: 2px;
  712. right: 10px;
  713. bottom: 10px;
  714. cursor: pointer;
  715. }
  716. .right-side {
  717. flex: 1; /* 占据剩余空间 */
  718. display: flex;
  719. flex-direction: column;
  720. .dialog-area {
  721. flex: 1; /* 占据剩余空间 */
  722. gap: 10px; /* 消息块间隔统一控制 */
  723. overflow-y: auto; /* 垂直滚动条 */
  724. padding: 5px;
  725. display: flex;
  726. flex-direction: column;
  727. color: #fff;
  728. .ask-message {
  729. padding: 10px;
  730. border-radius: 5px;
  731. background: #0c2842;
  732. max-width: 80%;
  733. }
  734. .answer-message {
  735. padding: 10px;
  736. border-radius: 5px;
  737. background: #0c2842;
  738. max-width: 90%;
  739. }
  740. }
  741. .input-area {
  742. margin: 10px 10px 20px 10px;
  743. padding: 10px;
  744. background-color: #043256;
  745. border: 1px solid #2cb6ff;
  746. border-radius: 5px;
  747. display: flex;
  748. flex-direction: column;
  749. justify-content: space-between;
  750. gap: 5px;
  751. height: 25%;
  752. }
  753. /* 文件列表容器 */
  754. .uploaded-files {
  755. padding: 8px;
  756. background-color: #f5f5f5;
  757. border-radius: 4px;
  758. min-height: 40px;
  759. }
  760. /* 单个文件项 */
  761. .file-item {
  762. display: inline-flex;
  763. align-items: center;
  764. padding: 4px 8px;
  765. margin-right: 8px;
  766. margin-bottom: 8px;
  767. background-color: #fff;
  768. border: 1px solid #e9e9e9;
  769. border-radius: 4px;
  770. }
  771. /* 文件名 */
  772. .file-name {
  773. margin-left: 8px;
  774. margin-right: 8px;
  775. max-width: 150px;
  776. white-space: nowrap;
  777. overflow: hidden;
  778. text-overflow: ellipsis;
  779. }
  780. .ant-input {
  781. background-color: rgba(255, 255, 255, 0) !important;
  782. border: none;
  783. outline: none;
  784. }
  785. .ant-input:focus {
  786. border: none; /* 聚焦时无边框 */
  787. outline: none; /* 聚焦时无轮廓 */
  788. box-shadow: none; /* 移除可能存在的阴影效果 */
  789. }
  790. .ctrl-btn {
  791. display: flex;
  792. flex-direction: row;
  793. justify-content: space-between;
  794. }
  795. .question-input {
  796. background-color: #1e293b !important;
  797. border-color: #334155 !important;
  798. color: #e2e8f0 !important;
  799. border-radius: 8px !important;
  800. padding: 12px 16px !important;
  801. font-size: 14px !important;
  802. }
  803. .question-input::placeholder {
  804. color: #64748b !important;
  805. }
  806. .control-btn {
  807. height: 25px;
  808. background-color: #043256;
  809. border: 1px solid #2cb6ff;
  810. color: #fff;
  811. font-size: 10px;
  812. margin-right: 10px;
  813. cursor: pointer;
  814. transition: all 0.2s;
  815. }
  816. .control-btn:hover {
  817. background-color: #043256;
  818. color: #e2e8f0;
  819. }
  820. .control-btn1 {
  821. height: 20px;
  822. background-color: #234a6b;
  823. border: 1px solid #234a6b;
  824. color: #fff;
  825. font-size: 10px;
  826. margin-right: 10px;
  827. cursor: pointer;
  828. transition: all 0.2s;
  829. }
  830. .control-btn2 {
  831. height: 20px;
  832. background-color: #2cb6ff;
  833. border: 1px solid #2cb6ff;
  834. color: #fff;
  835. font-size: 10px;
  836. margin-right: 10px;
  837. cursor: pointer;
  838. transition: all 0.2s;
  839. }
  840. /* 文件上传区 */
  841. .file-upload {
  842. position: absolute;
  843. right: 20px;
  844. bottom: 70px;
  845. width: 180px;
  846. display: flex;
  847. flex-direction: column;
  848. gap: 10px;
  849. border: 1px solid #2cb6ff;
  850. background-color: #234a6b;
  851. border-radius: 6px;
  852. padding: 10px;
  853. }
  854. .input-container {
  855. position: relative;
  856. display: flex;
  857. align-items: center;
  858. width: 100%;
  859. }
  860. .file-input {
  861. flex: 1;
  862. background-color: #234a6b;
  863. border-color: #2cb6ff !important;
  864. color: #e2e8f0 !important;
  865. border-radius: 6px !important;
  866. font-size: 10px !important;
  867. padding-right: 70px !important;
  868. height: 36px !important;
  869. width: 100% !important;
  870. }
  871. .confirm-btn {
  872. position: absolute;
  873. right: 5px;
  874. background-color: #2cb6ff;
  875. border: none;
  876. color: #fff;
  877. border-radius: 4px;
  878. font-size: 12px;
  879. padding: 4px 10px;
  880. cursor: pointer;
  881. transition: all 0.2s;
  882. height: 28px;
  883. }
  884. .confirm-btn:hover {
  885. background-color: #2cb6ff;
  886. }
  887. .custom-upload {
  888. width: 100%;
  889. padding: 0 !important;
  890. }
  891. .upload-btn {
  892. background-color: #234a6b !important;
  893. border: 1px solid #2188c3 !important;
  894. color: #dbeafe !important;
  895. border-radius: 6px !important;
  896. font-size: 12px !important;
  897. cursor: pointer;
  898. transition: all 0.2s;
  899. padding: 8px 0 !important;
  900. width: 190% !important;
  901. height: 36px !important;
  902. box-sizing: border-box !important;
  903. }
  904. .upload-btn:hover {
  905. background-color: #1f84bd !important;
  906. color: #fff !important;
  907. }
  908. .custom-upload .ant-upload-select:hover .ant-btn {
  909. border-color: #1f84bd !important;
  910. }
  911. }
  912. </style>
  913. <style scoped>
  914. .zxm-popover-inner-content {
  915. padding: 1px;
  916. }
  917. .message-wrapper {
  918. display: flex;
  919. align-items: flex-start;
  920. position: relative;
  921. }
  922. .user-message-wrapper {
  923. flex-direction: row-reverse;
  924. }
  925. .ai-message-wrapper {
  926. flex-direction: row;
  927. }
  928. /* 复制图标样式 */
  929. .copy-icon {
  930. font-size: 16px;
  931. cursor: pointer;
  932. margin: 4px 8px;
  933. color: #fff;
  934. }
  935. .message-wrapper:hover .copy-icon {
  936. opacity: 1;
  937. }
  938. .copy-icon:hover {
  939. color: #1890ff;
  940. }
  941. </style>