aiIndex.vue 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165
  1. <template>
  2. <div class="chat-container">
  3. <div class="content">
  4. <div class="messages">
  5. <ul>
  6. <li
  7. v-for="message in messages"
  8. :key="message.id"
  9. :class="[
  10. message.user,
  11. message.user === 'user' ? 'user-message' : '',
  12. ]"
  13. >
  14. <!-- 添加判断,如果是机器人的消息也显示头像 -->
  15. <div class="message_name">
  16. <div
  17. class="avatar"
  18. :class="{
  19. 'user-avatar': message.user === 'user',
  20. 'bot-avatar': message.user === 'bot',
  21. }"
  22. ></div>
  23. <div class="name">
  24. <span
  25. class="sender-name"
  26. style="color: #666666"
  27. v-if="message.user === 'user'"
  28. >你</span
  29. >
  30. <span class="sender-name" v-if="message.user === 'bot'"
  31. >AI助理</span
  32. >
  33. </div>
  34. </div>
  35. <!-- 使用 v-html 来渲染 Markdown 文本 -->
  36. <div class="message-content">
  37. <div
  38. class="message-text"
  39. v-html="renderMarkdown(message.message)"
  40. @click="handleLinkClick"
  41. ></div>
  42. </div>
  43. </li>
  44. </ul>
  45. </div>
  46. <!-- <div class="input-container">
  47. <textarea
  48. v-model="userInput"
  49. rows="4"
  50. @keydown="handleKeydown"
  51. placeholder="发送消息..."
  52. ></textarea>
  53. <button @click="sendMessage" class="btn">发送</button>
  54. </div> -->
  55. <div class="input-container">
  56. <!-- 文件上传预览区 -->
  57. <div class="upload-preview" v-if="uploadFiles.length > 0">
  58. <div
  59. v-for="(file, index) in uploadFiles"
  60. :key="index"
  61. class="preview-item"
  62. >
  63. <div class="preview-content">
  64. <img
  65. v-if="file.type.startsWith('image/')"
  66. :src="file.preview"
  67. class="preview-image"
  68. alt="preview"
  69. />
  70. <div v-else class="file-info">
  71. <i class="el-icon-document"></i>
  72. <span>{{ file.name }}</span>
  73. </div>
  74. </div>
  75. <i class="el-icon-close remove-file" @click="removeFile(index)"></i>
  76. </div>
  77. </div>
  78. <!-- 输入区域 -->
  79. <div class="input-area">
  80. <textarea
  81. v-model="userInput"
  82. rows="4"
  83. @keydown="handleKeydown"
  84. placeholder="发送消息..."
  85. ></textarea>
  86. <!-- 工具栏 -->
  87. <div class="toolbar">
  88. <div class="left-tools">
  89. <input
  90. type="file"
  91. ref="fileInput"
  92. multiple
  93. @change="handleFileUpload"
  94. accept="image/*,.pdf,.doc,.docx,.txt"
  95. style="display: none"
  96. />
  97. <button class="tool-btn" @click="$refs.fileInput.click()">
  98. <i class="el-icon-upload2"></i>
  99. </button>
  100. </div>
  101. <!-- <button @click="playAudio" class="btn">播放音频</button> -->
  102. <button @click="sendMessage" class="btn">发送</button>
  103. </div>
  104. </div>
  105. </div>
  106. <!-- </div> -->
  107. </div>
  108. <LoginModal
  109. :visible.sync="showLoginModal"
  110. @close="showLoginModal = false"
  111. @login-success="handleLoginSuccess"
  112. />
  113. </div>
  114. </template>
  115. <script>
  116. import ChatBox from "@/components/webAi/index.vue";
  117. import LoginModal from "@/components/LoginModal";
  118. import MarkdownIt from "markdown-it";
  119. import { pcInnerAi, getMinioURl } from "@/api/api";
  120. import { modelList, get_default } from "@/api/knowledge";
  121. import axios from "axios";
  122. const md = new MarkdownIt();
  123. export default {
  124. components: {
  125. ChatBox,
  126. LoginModal,
  127. },
  128. data() {
  129. return {
  130. showLoginModal: false,
  131. isThinking: false,
  132. thinkingDots: "",
  133. messages: [
  134. {
  135. user: "bot",
  136. messageType: "TEXT",
  137. message: "欢迎使用智能AI助理",
  138. html: "",
  139. time: "",
  140. done: true,
  141. },
  142. ],
  143. generating: false,
  144. userInput: "",
  145. websocket: null,
  146. wsUrl:
  147. "http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false",
  148. tweaks: {},
  149. isLoggedIn: false, // 添加登录状态标志
  150. idArray: [],
  151. minioUrls: [], // 新增:用于存储 Minio URL
  152. AImodel: "1", //模型
  153. AIknowledgeBase: "1", //知识库
  154. AIFile: "1", //文件
  155. AIform: {
  156. modelLibrary: "ollama",
  157. modelType: "",
  158. modelName: "",
  159. knowledgeBase: "",
  160. documentDirectory: "",
  161. document: "",
  162. },
  163. currentChat_id: "",
  164. phone: "",
  165. uploadFiles: [], // 存储上传的文件
  166. };
  167. },
  168. mounted() {
  169. this.checkAIData();
  170. if (this.$route.name == "ai") {
  171. /* 外部知识库 */
  172. /* 外部知识库 */
  173. const AIDataString = localStorage.getItem("AIData");
  174. if (!AIDataString) {
  175. this.showLoginModal = true;
  176. return;
  177. }
  178. try {
  179. const AIData = JSON.parse(AIDataString);
  180. if (AIData && AIData.data) {
  181. console.log(AIData);
  182. this.wsUrl = AIData.data.url || "";
  183. this.phone = AIData.data.phone || "";
  184. this.tweaks = AIData.data.tweaks || {};
  185. } else {
  186. console.error("Invalid AIData structure");
  187. this.showLoginModal = true;
  188. }
  189. } catch (error) {
  190. console.error("Error parsing AIData:", error);
  191. this.showLoginModal = true;
  192. }
  193. } else {
  194. /* 内部知识库 */
  195. pcInnerAi().then((res) => {
  196. if (res.status !== 200) return;
  197. this.wsUrl = res.data.url;
  198. this.tweaks = res.data.tweaks;
  199. });
  200. }
  201. this.init();
  202. },
  203. methods: {
  204. /* 播放音频 */
  205. playAudio(audioBase64) {
  206. console.log(audioBase64);
  207. // 创建音频元素
  208. const audio = new Audio(audioBase64); // 播放音频
  209. audio.play().catch((error) => {
  210. console.error("Error playing audio:", error);
  211. });
  212. },
  213. async handleFileUpload(event) {
  214. const files = Array.from(event.target.files);
  215. // 检查当前已上传的图片数量
  216. const currentImageCount = this.uploadFiles.filter((file) =>
  217. file.type.startsWith("image/")
  218. ).length;
  219. // 计算还能上传多少张图片
  220. const remainingSlots = 5 - currentImageCount;
  221. if (remainingSlots <= 0) {
  222. this.$message.warning("最多只能上传5张图片");
  223. event.target.value = "";
  224. return;
  225. }
  226. // 只处理允许的数量的文件
  227. const filesToProcess = files.slice(0, remainingSlots);
  228. if (files.length > remainingSlots) {
  229. this.$message.warning(
  230. `已达到上传上限,只能再上传${remainingSlots}张图片`
  231. );
  232. }
  233. for (const file of filesToProcess) {
  234. try {
  235. // 创建 FormData 对象
  236. const formData = new FormData();
  237. formData.append("file", file);
  238. // 发送上传请求
  239. const response = await axios.post(
  240. `${process.env.VUE_APP_BASE_API}/upload/file`,
  241. formData,
  242. {
  243. headers: {
  244. "Content-Type": "multipart/form-data",
  245. },
  246. }
  247. );
  248. // 获取接口返回的文件URL
  249. const fileUrl = response.data.data.fileUrl;
  250. // 将文件信息添加到 uploadFiles 数组
  251. this.uploadFiles.push({
  252. name: file.name,
  253. type: file.type,
  254. preview: fileUrl,
  255. url: fileUrl,
  256. });
  257. } catch (error) {
  258. console.error("文件上传失败:", error);
  259. this.$message.error(`文件 ${file.name} 上传失败`);
  260. }
  261. }
  262. // 清空input以允许重复上传相同文件
  263. event.target.value = "";
  264. },
  265. removeFile(index) {
  266. this.uploadFiles.splice(index, 1);
  267. },
  268. /* */
  269. checkAIData() {
  270. const aiData = localStorage.getItem("AIData");
  271. this.showLoginModal = !aiData;
  272. },
  273. handleLoginSuccess() {
  274. this.showLoginModal = false;
  275. // 处理登录成功后的逻辑,例如刷新数据或更新状态
  276. },
  277. /* 聊天记录 */
  278. selectChat(index) {
  279. this.currentChatIndex = index;
  280. // Load the selected chat messages
  281. // This is where you would typically load the messages for the selected chat
  282. },
  283. newChat() {
  284. this.chatHistory.push({
  285. title: `聊天 ${this.chatHistory.length + 1}`,
  286. preview: "新的聊天...",
  287. });
  288. this.currentChatIndex = this.chatHistory.length - 1;
  289. // Clear the current messages and start a new chat
  290. this.messages = [];
  291. },
  292. handleLinkClick(event) {
  293. if (event.target.tagName.toLowerCase() === "a") {
  294. event.preventDefault();
  295. // 获取文件名
  296. const fullFileName = event.target.textContent.trim();
  297. // 分离文件名和后缀
  298. const lastDotIndex = fullFileName.lastIndexOf(".");
  299. const fileName =
  300. lastDotIndex > -1
  301. ? fullFileName.slice(0, lastDotIndex)
  302. : fullFileName;
  303. const fileExtension =
  304. lastDotIndex > -1 ? fullFileName.slice(lastDotIndex + 1) : "";
  305. // 编码文件名和后缀以便于URL传参
  306. const encodedFileName = encodeURIComponent(fileName);
  307. const encodedFileExtension = encodeURIComponent(fileExtension);
  308. // 构建新的URL,包含文件名和后缀作为查询参数
  309. const newUrl = `#/preview?fileName=${encodedFileName}&fileExtension=${encodedFileExtension}`;
  310. try {
  311. localStorage.setItem("href", event.target.href);
  312. } catch (e) {
  313. console.warn("无法存储 URL 到 localStorage:", e);
  314. }
  315. // 使用新的URL打开窗口
  316. const newWindow = window.open(newUrl, "_blank");
  317. if (newWindow) {
  318. newWindow.focus();
  319. } else {
  320. alert("弹出窗口被阻止,请允许此网站的弹出窗口。");
  321. }
  322. }
  323. },
  324. handleLoginSuccess() {
  325. this.showLoginModal = false;
  326. this.isLoggedIn = true;
  327. // 登录成功后,可以继续发送消息
  328. this.sendMessage();
  329. },
  330. getUuid() {
  331. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
  332. /[xy]/g,
  333. function (c) {
  334. var r = (Math.random() * 16) | 0,
  335. v = c == "x" ? r : (r & 0x3) | 0x8;
  336. return v.toString(16);
  337. }
  338. );
  339. },
  340. connectWebSocket() {
  341. const wsUrl =
  342. "http://58.246.234.210:7860/api/v1/run/2f1faff2-99df-4821-87d6-43d693084c4b?stream=false";
  343. // 这里做一个简单的鉴权,只有符合条件的鉴权才能握手成功
  344. // 移除这里的fetch调用,改用WebSocket
  345. this.websocket = new WebSocket(wsUrl);
  346. /* this.websocket.onerror = (event) => {
  347. console.error("WebSocket 连接错误:", event);
  348. };
  349. */
  350. this.websocket.onclose = (event) => {
  351. console.log("WebSocket 连接关闭:", event);
  352. };
  353. this.websocket.onopen = (event) => {
  354. console.log("WebSocket 连接已建立:", event);
  355. };
  356. this.websocket.onmessage = (event) => {
  357. // 解析收到的消息
  358. const result = JSON.parse(event.data);
  359. // 检查消息是否完结
  360. if (result.done) {
  361. this.messages[this.messages.length - 1].done = true;
  362. return;
  363. }
  364. if (this.messages[this.messages.length - 1].done) {
  365. // 添加新的消息
  366. this.messages.push({
  367. time: Date.now(),
  368. message: result.content,
  369. messageType: "TEXT",
  370. user: "bot",
  371. done: false,
  372. });
  373. } else {
  374. // 更新最后一条消息
  375. let lastMessage = this.messages[this.messages.length - 1];
  376. lastMessage.message += result.content;
  377. this.messages[this.messages.length - 1] = lastMessage;
  378. }
  379. };
  380. },
  381. async sendMessage() {
  382. if (this.$route.name == "ai") {
  383. if (!localStorage.getItem("AIData")) {
  384. this.showLoginModal = true;
  385. return;
  386. }
  387. }
  388. const chatId = this.getUuid();
  389. const wsUrl = `${process.env.VUE_APP_BASE_AI_API}/chatbot/multimodal`; //process.env.VUE_APP_BASE_AI_API
  390. let message = this.userInput.trim();
  391. const imageUrls = this.uploadFiles
  392. .filter((file) => file.type.startsWith("image/"))
  393. .map((file) => file.url);
  394. if (message || imageUrls.length > 0) {
  395. message = message.replace(/(\r\n|\r|\n)/g, " \n");
  396. this.messages.push({
  397. time: Date.now(),
  398. message: message,
  399. messageType: "TEXT",
  400. user: "user",
  401. done: true,
  402. });
  403. this.userInput = "";
  404. this.uploadFiles = [];
  405. const botMessage = {
  406. user: "bot",
  407. messageType: "TEXT",
  408. message: "",
  409. html: "",
  410. time: Date.now(),
  411. done: false,
  412. };
  413. this.messages.push(botMessage);
  414. const thinkingController = new AbortController();
  415. const thinkingPromise = this.simulateThinking(
  416. botMessage,
  417. thinkingController.signal
  418. );
  419. try {
  420. const response = await axios({
  421. method: "post",
  422. url: wsUrl,
  423. data: {
  424. message: message,
  425. chat_config_id: "17",
  426. user_id: this.phone,
  427. session_id: this.session_id || "",
  428. source: "pc",
  429. image_urls: imageUrls,
  430. //history_limit: 10
  431. },
  432. });
  433. this.session_id = response.data.data.session_id;
  434. if (response.data.code !== 200) {
  435. throw new Error(
  436. `HTTP error! status: ${response.status}, message: ${response.data.message}`
  437. );
  438. }
  439. thinkingController.abort();
  440. await thinkingPromise;
  441. if (this.$route.name !== "ai") {
  442. const additionalInput = response.data.data.milvus_ids;
  443. this.idArray = additionalInput;
  444. const idObject = { ids: this.idArray };
  445. console.log("ID Object:", idObject);
  446. await this.getMinioUrls(idObject);
  447. }
  448. // 如果有音频,播放音频
  449. if (
  450. response.data.data.audio_info &&
  451. response.data.data.audio_info.audio_base64
  452. ) {
  453. this.playAudio(response.data.data.audio_info.audio_base64);
  454. }
  455. this.handleResponse(response.data, botMessage);
  456. } catch (error) {
  457. console.error("Error sending message:", error);
  458. thinkingController.abort();
  459. await thinkingPromise;
  460. botMessage.done = true;
  461. botMessage.message = "抱歉,发生了错误,请稍后再试。";
  462. if (error.response) {
  463. console.error("Response data:", error.response.data);
  464. console.error("Response status:", error.response.status);
  465. } else if (error.request) {
  466. console.error("No response received:", error.request);
  467. } else {
  468. console.error("Error message:", error.message);
  469. }
  470. }
  471. }
  472. },
  473. async getMinioUrls(idObject) {
  474. this.minioUrls = []; // 清空之前的 URL
  475. try {
  476. console.log("Sending to backend:", JSON.stringify(idObject));
  477. const response = await axios.post(
  478. `${process.env.VUE_APP_BASE_AI_API}/milvus/getMinioURl`,
  479. idObject,
  480. {
  481. headers: {
  482. "Content-Type": "application/json",
  483. },
  484. }
  485. );
  486. if (response.status === 200 && response.data) {
  487. this.minioUrls = response.data.data; // 假设返回的是 URL 数组
  488. console.log("Received Minio URLs:", this.minioUrls);
  489. } else {
  490. console.error("Failed to get Minio URLs");
  491. }
  492. } catch (error) {
  493. console.error("Error fetching Minio URLs:", error);
  494. if (error.response) {
  495. console.error("Response data:", error.response.data);
  496. console.error("Response status:", error.response.status);
  497. console.error("Response headers:", error.response.headers);
  498. } else if (error.request) {
  499. console.error("No response received:", error.request);
  500. } else {
  501. console.error("Error message:", error.message);
  502. }
  503. }
  504. },
  505. async simulateThinking(message, signal) {
  506. this.isThinking = true;
  507. const dotInterval = 200; // 每 200ms 更新一次点
  508. return new Promise((resolve) => {
  509. const updateDots = () => {
  510. if (signal.aborted) {
  511. clearInterval(intervalId);
  512. this.isThinking = false;
  513. this.thinkingDots = "";
  514. message.message = "";
  515. resolve();
  516. return;
  517. }
  518. this.thinkingDots += ".";
  519. if (this.thinkingDots.length > 3) {
  520. this.thinkingDots = "";
  521. }
  522. message.message = "思考中" + this.thinkingDots;
  523. };
  524. const intervalId = setInterval(updateDots, dotInterval);
  525. signal.addEventListener("abort", () => {
  526. clearInterval(intervalId);
  527. this.isThinking = false;
  528. this.thinkingDots = "";
  529. message.message = "";
  530. resolve();
  531. });
  532. });
  533. },
  534. async handleResponse(value, existingMessage) {
  535. const data = value.data.answer;
  536. existingMessage.messageType = data;
  537. /* existingMessage.time = data.timestamp; */
  538. existingMessage.message = "";
  539. let mainText = data;
  540. let sourceText = "";
  541. if (
  542. this.$route.name !== "ai" &&
  543. this.minioUrls &&
  544. this.minioUrls.length > 0
  545. ) {
  546. sourceText =
  547. "\n\n<div class='source-section'><h3>相关资料来源:</h3><ol>";
  548. this.minioUrls.forEach((url, index) => {
  549. sourceText += `<li><a href="${url.url}" target="_blank" class="source-link">${url.object_name}</a></li>`;
  550. });
  551. sourceText += "</ol></div>";
  552. }
  553. console.log(mainText);
  554. // 先进行主要内容的打字效果
  555. await this.typeMessage(existingMessage, mainText);
  556. // 直接添加资料来源部分
  557. if (sourceText) {
  558. existingMessage.message += sourceText;
  559. existingMessage.html = this.renderMarkdown(existingMessage.message);
  560. }
  561. },
  562. typeMessage(message, fullText) {
  563. return new Promise((resolve) => {
  564. const delay = 30;
  565. let i = 0;
  566. const typeChar = () => {
  567. if (i < fullText.length) {
  568. message.message += fullText[i];
  569. i++;
  570. setTimeout(typeChar, delay);
  571. } else {
  572. message.done = true;
  573. message.html = this.renderMarkdown(message.message);
  574. resolve();
  575. }
  576. };
  577. typeChar();
  578. });
  579. },
  580. renderMarkdown(rawMarkdown) {
  581. const md = new MarkdownIt({
  582. html: true,
  583. breaks: true,
  584. linkify: true,
  585. });
  586. // 自定义链接渲染规则
  587. md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
  588. const token = tokens[idx];
  589. const hrefIndex = token.attrIndex("href");
  590. const href = token.attrs[hrefIndex][1];
  591. return `<span class="custom-link" data-href="${href}">`;
  592. };
  593. md.renderer.rules.link_close = () => "</span>";
  594. const renderedHtml = md.render(rawMarkdown);
  595. // 使用 setTimeout 来确保 DOM 更新后再添加事件监听器
  596. setTimeout(() => {
  597. const links = document.querySelectorAll(".custom-link");
  598. links.forEach((link) => {
  599. link.addEventListener("click", (e) => {
  600. e.preventDefault();
  601. const href = link.getAttribute("data-href");
  602. window.open(href, "_blank");
  603. });
  604. });
  605. }, 0);
  606. return renderedHtml;
  607. },
  608. handleKeydown(event) {
  609. // Check if 'Enter' is pressed without the 'Alt' key
  610. if (event.key === "Enter" && !(event.shiftKey || event.altKey)) {
  611. event.preventDefault(); // Prevent the default action to avoid line break in textarea
  612. this.sendMessage();
  613. } else if (event.key === "Enter" && event.altKey) {
  614. // Allow 'Alt + Enter' to insert a newline
  615. const cursorPosition = event.target.selectionStart;
  616. const textBeforeCursor = this.userInput.slice(0, cursorPosition);
  617. const textAfterCursor = this.userInput.slice(cursorPosition);
  618. // Insert the newline character at the cursor position
  619. this.userInput = textBeforeCursor + "\n" + textAfterCursor;
  620. // Move the cursor to the right after the inserted newline
  621. this.$nextTick(() => {
  622. event.target.selectionStart = cursorPosition + 1;
  623. event.target.selectionEnd = cursorPosition + 1;
  624. });
  625. }
  626. },
  627. beforeDestroy() {
  628. if (this.websocket) {
  629. this.websocket.close();
  630. }
  631. },
  632. /* 获取列表 */
  633. init() {
  634. /* modelList().then((res) => {
  635. console.log(res);
  636. }); */
  637. get_default().then((res) => {
  638. if (res.status !== 200) return;
  639. this.currentChat_id = res.data.id;
  640. });
  641. },
  642. },
  643. updated() {
  644. const messagesContainer = this.$el.querySelector(".messages");
  645. messagesContainer.scrollTop = messagesContainer.scrollHeight;
  646. },
  647. };
  648. </script>
  649. <style scoped>
  650. .chat-container {
  651. position: relative;
  652. display: flex;
  653. flex-direction: column;
  654. margin-bottom: 150px;
  655. /* 或者按照实际需要调整高度 */
  656. }
  657. .chat-container .header {
  658. width: 1200px;
  659. margin: 10px auto 0;
  660. border: 1px solid #d7d7d7;
  661. border-radius: 10px;
  662. }
  663. .form-container {
  664. max-width: 1200px; /* 或者其他适合你布局的宽度 */
  665. margin: 0 auto; /* 这会使容器水平居中 */
  666. padding: 0 15px; /* 添加一些左右内边距 */
  667. }
  668. .el-form-item {
  669. margin-bottom: 15px;
  670. }
  671. .el-select {
  672. width: 100%;
  673. }
  674. .generate-button {
  675. margin-top: 10px; /* 在小屏幕上给按钮一些上边距 */
  676. margin-right: 25px;
  677. }
  678. @media (max-width: 768px) {
  679. .selection-summary {
  680. flex-direction: column;
  681. }
  682. .generate-button {
  683. margin-left: 0;
  684. margin-top: 15px;
  685. align-self: flex-end; /* 在小屏幕上将按钮右对齐 */
  686. }
  687. }
  688. .selection-summary {
  689. background-color: #f2f2f2;
  690. border-radius: 4px;
  691. padding: 15px;
  692. margin: 15px;
  693. display: flex;
  694. justify-content: space-between;
  695. align-items: center;
  696. margin-right: 85px;
  697. border-radius: 10px;
  698. }
  699. .summary-content {
  700. flex-grow: 1;
  701. }
  702. .summary-label {
  703. font-weight: bold;
  704. color: #7f7f7f;
  705. margin-right: 10px;
  706. }
  707. .summary-text {
  708. color: #606266;
  709. margin: 0;
  710. margin-top: 3px;
  711. font-size: 14px;
  712. }
  713. .title {
  714. width: 1200px;
  715. margin: 0 auto 15px;
  716. display: flex;
  717. align-items: center;
  718. }
  719. .title_info {
  720. cursor: pointer;
  721. color: #1296db;
  722. font-size: 14px;
  723. margin-left: 10px;
  724. }
  725. /* .chat-container .header ::v-deep .el-select .el-input--medium .el-input__inner{
  726. border: none;
  727. }
  728. .chat-container .header ::v-deep .el-select .el-input--medium .el-input__suffix .el-input__suffix-inner{
  729. display: none;
  730. } */
  731. .chat-container .content {
  732. display: flex;
  733. justify-content: flex-start;
  734. }
  735. .chat-container .content .left {
  736. margin-left: 10px;
  737. width: 330px;
  738. height: 87vh;
  739. }
  740. .chat-item {
  741. border-radius: 10px;
  742. padding: 10px;
  743. border-bottom: 1px solid #e8edf2;
  744. background-color: #f5f5fa;
  745. margin-bottom: 10px;
  746. }
  747. .chat-header {
  748. display: flex;
  749. align-items: center;
  750. margin-bottom: 5px;
  751. }
  752. .avatar {
  753. width: 40px;
  754. height: 40px;
  755. border-radius: 50%;
  756. margin-right: 10px;
  757. }
  758. .user-info {
  759. flex-grow: 1;
  760. }
  761. .user-info .info_header {
  762. display: flex;
  763. align-items: flex-end;
  764. }
  765. .username {
  766. font-weight: bold;
  767. }
  768. .timestamp {
  769. margin-left: 13px;
  770. font-size: 0.8em;
  771. color: #888;
  772. }
  773. .more-options {
  774. cursor: pointer;
  775. }
  776. .chat-message {
  777. font-weight: bold;
  778. margin-top: 8px;
  779. margin-bottom: 10px;
  780. }
  781. .chat-preview {
  782. font-size: 0.9em;
  783. color: #666;
  784. }
  785. /* Adjust existing styles */
  786. .left {
  787. width: 300px; /* Increased width to accommodate the new layout */
  788. }
  789. .chat-history {
  790. width: 330px;
  791. height: 100%;
  792. overflow-y: auto;
  793. }
  794. .messages {
  795. width: 1180px; /* 设置消息列表宽度为屏幕宽度的 80% */
  796. max-width: 90%; /* 限制最大宽度,避免超出视口 */
  797. margin: 0 auto; /* 上下保持原有的 margin,左右自动调整以居中 */
  798. overflow-y: auto; /* 如果消息太多,允许滚动 */
  799. height: calc(100vh - 230px); /* 高度需要根据输入框和其他元素的高度调整 */
  800. padding: 10px; /* 或你希望的内边距大小 */
  801. margin-bottom: 110px;
  802. }
  803. /* 针对手机端的媒体查询 */
  804. @media screen and (max-width: 768px) {
  805. .messages {
  806. width: 100%;
  807. max-width: 100%;
  808. margin: 0;
  809. margin-bottom: 120px;
  810. }
  811. }
  812. .user-message {
  813. display: flex;
  814. flex-direction: column !important;
  815. align-items: flex-end !important;
  816. }
  817. .user-message .message-content {
  818. order: 1;
  819. text-align: right;
  820. }
  821. .user-message .avatar {
  822. order: 2;
  823. margin-left: 10px;
  824. margin-right: 0;
  825. }
  826. .message-text {
  827. margin-top: 5px;
  828. }
  829. .messages li {
  830. display: flex;
  831. flex-direction: column;
  832. align-items: flex-start; /* 这将确保所有子元素对齐到容器的顶部 */
  833. /* margin-bottom: 10px; */
  834. }
  835. .avatar {
  836. width: 40px; /* 头像的尺寸 */
  837. height: 40px;
  838. border-radius: 50%; /* 圆形头像 */
  839. /*background-color: #e9e0e0; 暂时用灰色代替,你可以用图片替换 */
  840. align-self: start; /* 这将覆盖 flex 容器的 align-items 并将头像对齐到顶部 */
  841. margin-right: 10px; /* 按需调整以在头像和消息之间提供空间 */
  842. }
  843. .user-avatar {
  844. background-image: url("../../components/webAi/assets/用户.png");
  845. background-position: center;
  846. background-repeat: no-repeat;
  847. background-size: cover;
  848. /* background-color: #93939d; */ /* 用户头像颜色 */
  849. }
  850. .bot-avatar {
  851. background-image: url("../../components/webAi/assets/机器人.png");
  852. background-position: center;
  853. background-repeat: no-repeat;
  854. background-size: cover;
  855. /* background-color: green; */ /* AI头像颜色 */
  856. }
  857. /* 针对手机端的媒体查询 */
  858. @media screen and (max-width: 768px) {
  859. .user-avatar,
  860. .bot-avatar {
  861. width: 30px; /* 或其他更小的尺寸 */
  862. height: 30px;
  863. }
  864. }
  865. .message_name {
  866. display: flex;
  867. align-items: center;
  868. padding-top: 5px;
  869. padding-bottom: 5px;
  870. }
  871. .name {
  872. margin-left: 8px;
  873. /* margin-bottom: 5px; */
  874. }
  875. .message-content {
  876. background-color: #f0f0f0; /* 消息背景 */
  877. padding-left: 20px; /* 上下左右的内边距 */
  878. padding-right: 20px; /* 上下左右的内边距 */
  879. padding-top: 10px; /* 上下左右的内边距 */
  880. padding-bottom: 0px; /* 上下左右的内边距 */
  881. max-width: calc(90% - 10px); /* 计算头像和间距 */
  882. border-radius: 7px; /* 边框圆角 */
  883. /* margin-bottom: 10px; 和下一条消息之间的间距 */
  884. /* 消息内容样式 */
  885. display: flex;
  886. flex-direction: column; /* 排列文本 */
  887. margin-top: 0;
  888. }
  889. .message-text {
  890. margin: 0;
  891. white-space: normal; /* 确保文本会换行 */
  892. word-wrap: break-word; /* 长单词或 URL 会被断开以适应容器宽度 */
  893. overflow: auto;
  894. font-size: 14px;
  895. }
  896. .message-text ::v-deep p {
  897. font-size: 14px !important;
  898. margin: 5px 0 10px;
  899. padding: 0;
  900. font-size: inherit;
  901. line-height: 22px;
  902. font-weight: inherit;
  903. }
  904. .message-text ::v-deep ol {
  905. padding-bottom: 10px;
  906. }
  907. .message-text ::v-deep .source-section h3 {
  908. margin: 20px 0 3px;
  909. color: #333;
  910. font-size: 16px;
  911. }
  912. .message-text ::v-deep ol li {
  913. padding-top: 3px;
  914. }
  915. .message-text ::v-deep ol li a {
  916. font-size: 14px;
  917. color: #1296db;
  918. }
  919. .input-container {
  920. position: absolute; /* 改为绝对定位 */
  921. bottom: -115px; /* 距离底部的距离 */
  922. left: 50%;
  923. transform: translateX(-50%);
  924. width: 90%; /* 设置宽度为弹窗宽度的90% */
  925. max-width: 1170px; /* 设置最大宽度 */
  926. display: flex;
  927. justify-content: space-between;
  928. align-items: flex-start;
  929. flex-direction: column;
  930. padding: 15px;
  931. background: white;
  932. border-radius: 10px;
  933. box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
  934. }
  935. textarea {
  936. font-size: 16px;
  937. width: 100%; /* 可以根据需要调整 */
  938. padding: 10px;
  939. border: 1px solid #ccc;
  940. border-radius: 4px;
  941. border: none;
  942. outline: none;
  943. resize: none;
  944. }
  945. .input-container textarea {
  946. width: 100%; /* 输入框占据所有可用空间 */
  947. padding: 10px;
  948. margin-right: 10px; /* 与按钮的间隔 */
  949. box-sizing: border-box;
  950. }
  951. .input-container button {
  952. min-width: 80px; /* 例如,按钮的最小宽度为 100px */
  953. white-space: nowrap; /* 防止文字折行 */
  954. padding: 10px 20px;
  955. cursor: pointer;
  956. }
  957. .sender-name {
  958. font-weight: bold;
  959. color: #333;
  960. margin-left: -5px;
  961. }
  962. .btn {
  963. font-size: 16px;
  964. color: #ffffff;
  965. border-radius: 5px;
  966. border: none;
  967. background-color: #3cbade;
  968. padding: 10px 20px; /* 添加内边距 */
  969. width: auto; /* 让宽度自适应内容 */
  970. }
  971. /* 针对手机端的媒体查询 */
  972. @media screen and (max-width: 768px) {
  973. .btn {
  974. font-size: 14px; /* 稍微减小字体大小 */
  975. padding: 8px 16px; /* 减小内边距 */
  976. max-width: 80px; /* 限制最大宽度 */
  977. margin: 0 auto; /* 居中显示 */
  978. }
  979. }
  980. /* 新输入框 */
  981. .upload-preview {
  982. display: flex;
  983. flex-wrap: wrap;
  984. gap: 10px;
  985. max-height: 100px;
  986. overflow-y: auto;
  987. }
  988. .preview-item {
  989. position: relative;
  990. width: 60px;
  991. height: 20px;
  992. border-radius: 4px;
  993. overflow: hidden;
  994. border: 1px solid #e0e0e0;
  995. }
  996. .preview-content {
  997. width: 100%;
  998. height: 100%;
  999. display: flex;
  1000. align-items: center;
  1001. justify-content: center;
  1002. }
  1003. .preview-image {
  1004. width: 100%;
  1005. height: 100%;
  1006. object-fit: cover;
  1007. }
  1008. .file-info {
  1009. display: flex;
  1010. flex-direction: column;
  1011. align-items: center;
  1012. justify-content: center;
  1013. height: 100%;
  1014. padding: 5px;
  1015. text-align: center;
  1016. }
  1017. .file-info span {
  1018. font-size: 12px;
  1019. word-break: break-word;
  1020. overflow: hidden;
  1021. display: -webkit-box;
  1022. -webkit-line-clamp: 2;
  1023. -webkit-box-orient: vertical;
  1024. }
  1025. .remove-file {
  1026. position: absolute;
  1027. top: 2px;
  1028. right: 2px;
  1029. background: rgba(0, 0, 0, 0.5);
  1030. color: white;
  1031. border-radius: 50%;
  1032. padding: 2px;
  1033. cursor: pointer;
  1034. font-size: 12px;
  1035. }
  1036. .input-area {
  1037. width: 100%;
  1038. }
  1039. .toolbar {
  1040. display: flex;
  1041. justify-content: space-between;
  1042. align-items: center;
  1043. margin-top: 10px;
  1044. }
  1045. .left-tools {
  1046. display: flex;
  1047. gap: 10px;
  1048. }
  1049. .tool-btn {
  1050. background: none;
  1051. border: none;
  1052. cursor: pointer;
  1053. padding: 5px;
  1054. color: #666;
  1055. font-size: 20px;
  1056. }
  1057. .tool-btn:hover {
  1058. color: #3cbade;
  1059. }
  1060. </style>