aiIndex.vue 31 KB

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