浏览代码

修改聊天应用页面及打包

yangg 8 月之前
父节点
当前提交
c964c32fde

文件差异内容过多而无法显示
+ 0 - 0
dist/index.html


文件差异内容过多而无法显示
+ 0 - 0
dist/static/css/app.7cee0d84.css


+ 0 - 1
dist/static/css/chunk-434b24b1.6b80018e.css

@@ -1 +0,0 @@
-[data-v-e99d0770] .chat-container{height:100vh}[data-v-e99d0770] .messages{margin-bottom:0;height:calc(100vh - 120px)}[data-v-e99d0770] .input-container{max-width:1200px}

+ 1 - 0
dist/static/css/chunk-50c331cc.71210f15.css

@@ -0,0 +1 @@
+[data-v-1d6fd656] .chat-container .header{width:98%;margin:10px 20px}[data-v-1d6fd656] .messages{width:98%;margin-bottom:0;height:calc(100vh - 120px)}[data-v-1d6fd656] .input-container{max-width:1200px}[data-v-1d6fd656] .title{width:98%;margin:0 20px 15px;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}

文件差异内容过多而无法显示
+ 0 - 0
dist/static/css/chunk-57d45b95.e89de760.css


+ 0 - 0
dist/static/css/chunk-86317d32.360612b2.css → dist/static/css/chunk-8e8fa6da.360612b2.css


文件差异内容过多而无法显示
+ 0 - 0
dist/static/js/app.253ace18.js


文件差异内容过多而无法显示
+ 0 - 0
dist/static/js/app.882d681d.js


文件差异内容过多而无法显示
+ 0 - 0
dist/static/js/chunk-15506e7b.09dc5af1.js


文件差异内容过多而无法显示
+ 0 - 0
dist/static/js/chunk-21725832.bde6520e.js


+ 0 - 1
dist/static/js/chunk-434b24b1.e8e63c84.js

@@ -1 +0,0 @@
-(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-434b24b1"],{"5cb5":function(o,n,t){"use strict";t.r(n);var c=function(){var o=this,n=o.$createElement,t=o._self._c||n;return t("div",[t("ChatBox"),t("LoginModal",{attrs:{visible:o.showLoginModal},on:{"update:visible":function(n){o.showLoginModal=n},close:function(n){o.showLoginModal=!1},"login-success":o.handleLoginSuccess}})],1)},a=[],e=t("c3b3"),i=t("ecd9"),s={components:{ChatBox:e["default"],LoginModal:i["default"]},data:function(){return{showLoginModal:!1}},mounted:function(){this.checkAIData()},methods:{checkAIData:function(){var o=localStorage.getItem("AIData");this.showLoginModal=!o},handleLoginSuccess:function(){this.showLoginModal=!1}}},u=s,l=(t("c8ac"),t("2877")),d=Object(l["a"])(u,c,a,!1,null,"e99d0770",null);n["default"]=d.exports},6831:function(o,n,t){},c8ac:function(o,n,t){"use strict";t("6831")}}]);

+ 1 - 0
dist/static/js/chunk-50c331cc.22ee25a2.js

@@ -0,0 +1 @@
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-50c331cc"],{"3ec3":function(c,n,t){"use strict";t("dacc")},c8cf:function(c,n,t){"use strict";t.r(n);var o=function(){var c=this,n=c.$createElement,t=c._self._c||n;return t("div",[t("ChatBox")],1)},e=[],a=t("c3b3"),i=t("ecd9"),u={components:{ChatBox:a["default"],LoginModal:i["default"]},data:function(){return{showLoginModal:!1}},mounted:function(){},methods:{checkAIData:function(){var c=localStorage.getItem("AIData");this.showLoginModal=!c},handleLoginSuccess:function(){this.showLoginModal=!1}}},d=u,s=(t("3ec3"),t("2877")),l=Object(s["a"])(d,o,e,!1,null,"1d6fd656",null);n["default"]=l.exports},dacc:function(c,n,t){}}]);

文件差异内容过多而无法显示
+ 0 - 0
dist/static/js/chunk-57d45b95.d305b1cb.js


文件差异内容过多而无法显示
+ 0 - 0
dist/static/js/chunk-8e8fa6da.68c13bda.js


文件差异内容过多而无法显示
+ 0 - 0
dist/static/js/chunk-ad361d30.735e4fc6.js


+ 20 - 1
src/api/knowledge.js

@@ -226,4 +226,23 @@ export function configList(data) {
     method: 'post',
     data
   })
-}
+}
+
+/* 删除 */
+
+export function configDelete(data) {
+  return request({
+    url: '/chatbot/configuration/delete/',
+    method: 'post',
+    data
+  })
+}
+/* 获取模型列表 */
+
+/* export function modelList(data) {
+  return request({
+    url: 'model-provider/search',
+    method: 'post',
+    data
+  })
+} */

二进制
src/assets/images/u5.png


+ 891 - 0
src/components/ai.vue

@@ -0,0 +1,891 @@
+<template>
+    <div class="chat-container">
+      <div class="content">
+        <div class="messages">
+          <ul>
+            <li
+              v-for="message in messages"
+              :key="message.id"
+              :class="[
+                message.user,
+                message.user === 'user' ? 'user-message' : '',
+              ]"
+            >
+              <!-- 添加判断,如果是机器人的消息也显示头像 -->
+              <div class="message_name">
+                <div
+                  class="avatar"
+                  :class="{
+                    'user-avatar': message.user === 'user',
+                    'bot-avatar': message.user === 'bot',
+                  }"
+                ></div>
+                <div class="name">
+                  <span
+                    class="sender-name"
+                    style="color: #666666"
+                    v-if="message.user === 'user'"
+                    >你</span
+                  >
+                  <span class="sender-name" v-if="message.user === 'bot'"
+                    >轻良AI助理</span
+                  >
+                </div>
+              </div>
+              <!-- 使用 v-html 来渲染 Markdown 文本 -->
+              <div class="message-content">
+                <div
+                  class="message-text"
+                  v-html="renderMarkdown(message.message)"
+                  @click="handleLinkClick"
+                ></div>
+              </div>
+            </li>
+          </ul>
+        </div>
+        <div class="input-container">
+          <textarea
+            v-model="userInput"
+            rows="4"
+            @keydown="handleKeydown"
+            placeholder="发送消息..."
+          ></textarea>
+          <button @click="sendMessage" class="btn">发送</button>
+        </div>
+        <!-- </div> -->
+      </div>
+      <LoginModal
+        :visible.sync="showLoginModal"
+        @close="showLoginModal = false"
+        @login-success="handleLoginSuccess"
+      />
+    </div>
+  </template>
+  
+  <script>
+  import ChatBox from "@/components/webAi/index.vue";
+  import LoginModal from "@/components/LoginModal";
+  import MarkdownIt from "markdown-it";
+  import { pcInnerAi,getMinioURl } from "@/api/api";
+  import {modelList} from "@/api/knowledge"
+  import axios from 'axios';
+  const md = new MarkdownIt();
+  
+  export default {
+    components: {
+      ChatBox,
+      LoginModal,
+    },
+    data() {
+      return {
+        showLoginModal: false,
+        isThinking: false,
+        thinkingDots: '',
+        messages: [
+          {
+            user: "bot",
+            messageType: "TEXT",
+            message: "欢迎使用智能AI助理",
+            html: "",
+            time: "",
+            done: true,
+          },
+        ],
+        generating: false,
+        userInput: "",
+        websocket: null,
+        wsUrl:'http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false',
+        tweaks:{},
+        isLoggedIn: false, // 添加登录状态标志
+        idArray: [],
+        minioUrls: [], // 新增:用于存储 Minio URL
+        AImodel:'1',//模型
+        AIknowledgeBase:"1",//知识库
+        AIFile:'1',//文件
+        AIform:{
+          modelLibrary: 'ollama',
+          modelType: '',
+          modelName: '',
+          knowledgeBase: '',
+          documentDirectory: '',
+          document: '',
+        }
+      };
+    },
+    mounted() {
+      
+      if(this.$route.name=='ai'){
+        /* 外部知识库 */this.checkAIData();
+         /* 外部知识库 */
+         const AIDataString = localStorage.getItem("AIData");
+          if (!AIDataString) {
+            this.showLoginModal = true;
+            return;
+          }
+          try {
+            const AIData = JSON.parse(AIDataString);
+            if (AIData && AIData.data) {
+              this.wsUrl = AIData.data.url || '';
+              this.tweaks = AIData.data.tweaks || {};
+            } else {
+              console.error('Invalid AIData structure');
+              this.showLoginModal = true;
+            }
+          } catch (error) {
+            console.error('Error parsing AIData:', error);
+            this.showLoginModal = true;
+          }
+      }else{
+        /* 内部知识库 */
+        pcInnerAi().then(res=>{
+          if(res.status!==200) return
+          this.wsUrl=res.data.url
+          this.tweaks=res.data.tweaks
+          
+        })
+        
+      }
+    },
+    methods: {
+      checkAIData() {
+        const aiData = localStorage.getItem("AIData");
+        this.showLoginModal = !aiData;
+      },
+      handleLoginSuccess() {
+        this.showLoginModal = false;
+        // 处理登录成功后的逻辑,例如刷新数据或更新状态
+      },
+  /* 聊天记录 */
+  selectChat(index) {
+    this.currentChatIndex = index;
+    // Load the selected chat messages
+    // This is where you would typically load the messages for the selected chat
+  },
+  newChat() {
+    this.chatHistory.push({ title: `聊天 ${this.chatHistory.length + 1}`, preview: "新的聊天..." });
+    this.currentChatIndex = this.chatHistory.length - 1;
+    // Clear the current messages and start a new chat
+    this.messages = [];
+  },
+  
+      handleLinkClick(event) {
+        if (event.target.tagName === 'A') {
+          localStorage.setItem('href',event.target.href)
+          let _this = this;
+          let a = document.createElement("a");
+          a.href = "#/preview"; // 使用 hash
+          a.target = "_blank";
+          a.click();
+        }
+      },
+  
+      handleLoginSuccess() {
+        this.showLoginModal = false;
+        this.isLoggedIn = true;
+        // 登录成功后,可以继续发送消息
+        this.sendMessage();
+      },
+      getUuid() {
+        return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
+          /[xy]/g,
+          function (c) {
+            var r = (Math.random() * 16) | 0,
+              v = c == "x" ? r : (r & 0x3) | 0x8;
+            return v.toString(16);
+          }
+        );
+      },
+      connectWebSocket() {
+        const wsUrl = 'http://58.246.234.210:7860/api/v1/run/2f1faff2-99df-4821-87d6-43d693084c4b?stream=false'
+        // 这里做一个简单的鉴权,只有符合条件的鉴权才能握手成功
+        // 移除这里的fetch调用,改用WebSocket
+        this.websocket = new WebSocket(wsUrl);
+      
+       /*  this.websocket.onerror = (event) => {
+          console.error("WebSocket 连接错误:", event);
+        };
+       */
+        this.websocket.onclose = (event) => {
+          console.log("WebSocket 连接关闭:", event);
+        };
+      
+        this.websocket.onopen = (event) => {
+          console.log("WebSocket 连接已建立:", event);
+        };
+        this.websocket.onmessage = (event) => {
+          // 解析收到的消息
+          const result = JSON.parse(event.data);
+  
+         
+  
+          // 检查消息是否完结
+          if (result.done) {
+            this.messages[this.messages.length - 1].done = true;
+            return;
+          }
+  
+          if (this.messages[this.messages.length - 1].done) {
+            // 添加新的消息
+            this.messages.push({
+              time: Date.now(),
+              message: result.content,
+              messageType: "TEXT",
+              user: "bot",
+              done: false,
+            });
+          } else {
+            // 更新最后一条消息
+            let lastMessage = this.messages[this.messages.length - 1];
+            lastMessage.message += result.content;
+            this.messages[this.messages.length - 1] = lastMessage;
+          }
+        };
+      },
+      async sendMessage() {
+        if(this.$route.name=='ai'){
+          if(!localStorage.getItem('AIData')){
+            this.showLoginModal=true
+            return
+          }
+        }
+        const chatId = this.getUuid();
+        const wsUrl =this.wsUrl//'http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false'
+        let message = this.userInput.trim();
+        if (message) {
+          // Markdown换行:在每个换行符之前添加两个空格
+          message = message.replace(/(\r\n|\r|\n)/g, "  \n");
+  
+          this.messages.push({
+            time: Date.now(),
+            message: message,
+            messageType: "TEXT",
+            user: "user",
+            done: true,
+          });
+          this.userInput = "";
+         // 添加机器人的响应消息(初始为空)
+         const botMessage = {
+          user: "bot",
+          messageType: "TEXT",
+          message: "",
+          html: "",
+          time: Date.now(),
+          done: false,
+        };
+        this.messages.push(botMessage);
+  
+        // 开始模拟思考
+        const thinkingPromise = this.simulateThinking(botMessage);
+          // 通过 HTTP 发送消息
+          try {
+            ///send-message
+            const response = await fetch(`${wsUrl}`, {
+              method: "POST",
+              headers: {
+                "Content-Type": "application/json",
+              },
+              body: JSON.stringify({
+                chatId: chatId,
+                input_value: message,
+                output_type: "chat",
+                input_type: "chat",
+                tweaks:this.tweaks /* {
+                  "ChatInput-U82Vu": {},
+                  "Prompt-b8Z1E": {},
+                  "OllamaModel-z9SVx": {},
+                  "Milvus-l0vvr": {},
+                  "OllamaEmbeddings-4Ml5q": {},
+                  "ParseData-LM8yW": {},
+                  "ParseData-jVoZg": {},
+                  "APIRequest-OnZHl": {},
+                  "ParseData-fH1vY": {},
+                  "ChatOutput-ybzb9": {}
+                } */,
+              }),
+            });
+  
+            if (!response.ok) {
+              const errorText = await response.text(); // 获取错误文本
+              throw new Error(
+                `HTTP error! status: ${response.status}, message: ${errorText}`
+              );
+            }
+  
+            const result = await response.json();
+           // 等待思考动画完成
+           await thinkingPromise;
+           if(this.$route.name !== 'ai'){
+             // 提取 additional_input 数据
+          const additionalInput = result.outputs?.[0]?.outputs?.[0]?.results?.message?.data?.additional_input;
+          
+          // 处理字符串,提取 ID 值
+          this.idArray = additionalInput.split('\n')  // 按换行符分割
+            .map(line => line.trim())  // 去除每行首尾空白
+            .filter(line => line.startsWith('ID:'))  // 只保留以 'ID:' 开头的行
+            .map(line => line.substring(3).trim());  // 提取 'ID:' 后面的内容并去除空白
+  
+          // 创建符合要求格式的对象
+          const idObject = { ids: this.idArray };
+  
+          console.log("ID Object:", idObject);
+  
+          // 调用方法来获取 Minio URL,传递新的 idObject
+          await this.getMinioUrls(idObject);
+           }
+           
+           this.handleResponse(result, botMessage);
+         } catch (error) {
+           console.error("Error sending message:", error);
+           // 如果发生错误,停止思考动画
+           botMessage.done = true;
+           botMessage.message = "抱歉,发生了错误,请稍后再试。";
+         }
+        }
+      },
+      async getMinioUrls(idObject) {
+        this.minioUrls = []; // 清空之前的 URL
+  
+        try {
+          console.log("Sending to backend:", JSON.stringify(idObject));
+  
+          const response = await axios.post('http://58.246.234.210:8084/milvus/getMinioURl', idObject, {
+            headers: {
+              'Content-Type': 'application/json'
+            }
+          });
+  
+          if (response.status === 200 && response.data) {
+            this.minioUrls = response.data.data; // 假设返回的是 URL 数组
+            console.log("Received Minio URLs:", this.minioUrls);
+          } else {
+            console.error('Failed to get Minio URLs');
+          }
+        } catch (error) {
+          console.error('Error fetching Minio URLs:', error);
+          if (error.response) {
+            console.error('Response data:', error.response.data);
+            console.error('Response status:', error.response.status);
+            console.error('Response headers:', error.response.headers);
+          } else if (error.request) {
+            console.error('No response received:', error.request);
+          } else {
+            console.error('Error message:', error.message);
+          }
+        }
+      },
+      async simulateThinking(message) {
+        this.isThinking = true;
+        const thinkingTime = Math.random() * 1000 + 500; // 思考时间为 0.5-1.5 秒
+        const dotInterval = 200; // 每 200ms 更新一次点
+        
+        const updateDots = () => {
+          this.thinkingDots += '.';
+          if (this.thinkingDots.length > 3) {
+            this.thinkingDots = '';
+          }
+          message.message =  this.thinkingDots;
+        };
+    
+        const intervalId = setInterval(updateDots, dotInterval);
+    
+        await new Promise(resolve => setTimeout(resolve, thinkingTime));
+        
+        clearInterval(intervalId);
+        this.isThinking = false;
+        this.thinkingDots = '';
+        message.message = '';
+      },
+    
+      async handleResponse(value, existingMessage) {
+        const data = value.outputs[0].outputs[0].results.message;
+        
+        existingMessage.messageType = data.text_key;
+        existingMessage.time = data.timestamp;
+        existingMessage.message = '';
+        
+        let mainText = data.text;
+        let sourceText = '';
+  
+        if (this.$route.name !== 'ai' && this.minioUrls && this.minioUrls.length > 0) {
+          sourceText = "\n\n<div class='source-section'><h3>相关资料来源:</h3><ol>";
+          this.minioUrls.forEach((url, index) => {
+            sourceText += `<li><a href="${url.url}" target="_blank" class="source-link">${url.object_name}</a></li>`;
+          });
+          sourceText += "</ol></div>";
+        }
+  
+        // 先进行主要内容的打字效果
+        await this.typeMessage(existingMessage, mainText);
+  
+        // 直接添加资料来源部分
+        if (sourceText) {
+          existingMessage.message += sourceText;
+          existingMessage.html = this.renderMarkdown(existingMessage.message);
+        }
+      },
+  
+      typeMessage(message, fullText) {
+        return new Promise(resolve => {
+          const delay = 30;
+          let i = 0;
+          const typeChar = () => {
+            if (i < fullText.length) {
+              message.message += fullText[i];
+              i++;
+              setTimeout(typeChar, delay);
+            } else {
+              message.done = true;
+              message.html = this.renderMarkdown(message.message);
+              resolve();
+            }
+          };
+          
+          typeChar();
+        });
+      },
+      renderMarkdown(rawMarkdown) {
+        const md = new MarkdownIt({
+          html: true,
+          breaks: true,
+          linkify: true
+        });
+  
+        // 自定义链接渲染规则
+        md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
+          const token = tokens[idx];
+          const hrefIndex = token.attrIndex('href');
+          const href = token.attrs[hrefIndex][1];
+          return `<span class="custom-link" data-href="${href}">`;
+        };
+  
+        md.renderer.rules.link_close = () => '</span>';
+  
+        const renderedHtml = md.render(rawMarkdown);
+  
+        // 使用 setTimeout 来确保 DOM 更新后再添加事件监听器
+        setTimeout(() => {
+          const links = document.querySelectorAll('.custom-link');
+          links.forEach(link => {
+            link.addEventListener('click', (e) => {
+              e.preventDefault();
+              const href = link.getAttribute('data-href');
+              window.open(href, '_blank');
+            });
+          });
+        }, 0);
+  
+        return renderedHtml;
+      },
+      handleKeydown(event) {
+        // Check if 'Enter' is pressed without the 'Alt' key
+        if (event.key === "Enter" && !(event.shiftKey || event.altKey)) {
+          event.preventDefault(); // Prevent the default action to avoid line break in textarea
+          this.sendMessage();
+        } else if (event.key === "Enter" && event.altKey) {
+          // Allow 'Alt + Enter' to insert a newline
+          const cursorPosition = event.target.selectionStart;
+          const textBeforeCursor = this.userInput.slice(0, cursorPosition);
+          const textAfterCursor = this.userInput.slice(cursorPosition);
+  
+          // Insert the newline character at the cursor position
+          this.userInput = textBeforeCursor + "\n" + textAfterCursor;
+  
+          // Move the cursor to the right after the inserted newline
+          this.$nextTick(() => {
+            event.target.selectionStart = cursorPosition + 1;
+            event.target.selectionEnd = cursorPosition + 1;
+          });
+        }
+      },
+      beforeDestroy() {
+        if (this.websocket) {
+          this.websocket.close();
+        }
+      },
+        /* 获取列表 */
+    init(){
+      modelList().then(res=>{
+        console.log(res);
+      })
+    }
+    },
+    updated() {
+      const messagesContainer = this.$el.querySelector(".messages");
+      messagesContainer.scrollTop = messagesContainer.scrollHeight;
+    },
+  };
+  </script>
+  <style scoped>
+  
+  .chat-container {
+      position: relative;
+      display: flex;
+      flex-direction: column;
+      margin-bottom: 150px;
+       /* 或者按照实际需要调整高度 */
+  }
+  .chat-container .header{
+    width: 1200px;
+    margin: 10px auto 0;
+    border: 1px solid #d7d7d7;
+    border-radius: 10px;
+  }
+  .form-container {
+  max-width: 1200px; /* 或者其他适合你布局的宽度 */
+  margin: 0 auto; /* 这会使容器水平居中 */
+  padding: 0 15px; /* 添加一些左右内边距 */
+  }
+  
+  .el-form-item {
+  margin-bottom: 15px;
+  }
+  
+  .el-select {
+  width: 100%;
+  }
+  .generate-button {
+  margin-top: 10px; /* 在小屏幕上给按钮一些上边距 */
+  margin-right: 25px;
+  }
+  
+  @media (max-width: 768px) {
+  .selection-summary {
+    flex-direction: column;
+  }
+  
+  .generate-button {
+    margin-left: 0;
+    margin-top: 15px;
+    align-self: flex-end; /* 在小屏幕上将按钮右对齐 */
+  }
+  }
+  .selection-summary {
+  background-color: #f2f2f2;
+  border-radius: 4px;
+  padding: 15px;
+  margin: 15px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-right: 85px;
+  border-radius:10px;
+  }
+  
+  .summary-content {
+  flex-grow: 1;
+  }
+  
+  .summary-label {
+  font-weight: bold;
+  color: #7f7f7f;
+  margin-right: 10px;
+  }
+  
+  .summary-text {
+  color: #606266;
+  margin: 0;
+  margin-top: 3px;
+  font-size: 14px;
+  }
+  .title{
+  width: 1200px;
+  margin: 0 auto 15px;
+  display: flex;
+  align-items: center;
+  
+  }
+  .title_info{
+  cursor: pointer;
+  color: #1296db;
+  font-size: 14px;
+  margin-left: 10px;
+  }
+  /* .chat-container .header ::v-deep .el-select .el-input--medium .el-input__inner{
+    border: none;
+  }
+  .chat-container .header ::v-deep .el-select .el-input--medium .el-input__suffix .el-input__suffix-inner{
+    display: none;
+  } */
+  .chat-container .content {
+      display: flex;
+      justify-content: flex-start;
+  }
+  
+  .chat-container .content .left{
+      margin-left: 10px;
+      width: 330px;
+      height: 87vh;
+  }
+  .chat-item {
+      border-radius: 10px;
+      padding: 10px;
+      border-bottom: 1px solid #e8edf2;
+      background-color: #f5f5fa;
+      margin-bottom: 10px;
+    }
+    
+    .chat-header {
+      display: flex;
+      align-items: center;
+      margin-bottom: 5px;
+    }
+    
+    .avatar {
+      width: 40px;
+      height: 40px;
+      border-radius: 50%;
+      margin-right: 10px;
+    }
+    
+    .user-info {
+      flex-grow: 1;
+    }
+    .user-info .info_header{
+      display: flex;
+      align-items: flex-end;
+    }
+    
+    .username {
+      font-weight: bold;
+    }
+    
+    .timestamp {
+      margin-left:13px;
+      font-size: 0.8em;
+      color: #888;
+    }
+    
+    .more-options {
+      cursor: pointer;
+    }
+    
+    .chat-message {
+      font-weight: bold;
+      margin-top: 8px;
+      margin-bottom: 10px;
+    }
+    
+    .chat-preview {
+      font-size: 0.9em;
+      color: #666;
+    }
+    
+    /* Adjust existing styles */
+    .left {
+      width: 300px; /* Increased width to accommodate the new layout */
+    }
+    
+    .chat-history {
+      width: 330px;
+      height: 100%;
+      overflow-y: auto;
+    }
+  
+  .messages {
+      width: 1180px; /* 设置消息列表宽度为屏幕宽度的 80% */
+      max-width: 90%; /* 限制最大宽度,避免超出视口 */
+      margin: 0 auto; /* 上下保持原有的 margin,左右自动调整以居中 */
+      overflow-y: auto; /* 如果消息太多,允许滚动 */
+      height: calc(100vh - 230px); /* 高度需要根据输入框和其他元素的高度调整 */
+      padding: 10px; /* 或你希望的内边距大小 */
+     
+  }
+  /* 针对手机端的媒体查询 */
+  @media screen and (max-width: 768px) {
+      .messages {
+          width: 100%;
+          max-width: 100%;
+          margin: 0;
+          margin-bottom: 120px;
+      }
+  }
+  .user-message {
+      display: flex;
+      flex-direction: column !important;
+      align-items: flex-end !important;
+    }
+    
+    .user-message .message-content {
+      order: 1;
+      text-align: right;
+    }
+    
+    .user-message .avatar {
+      order: 2;
+      margin-left: 10px;
+      margin-right: 0;
+    }
+    
+    .message-text {
+      margin-top: 5px;
+    }
+  .messages li {
+      display: flex;
+      flex-direction: column;
+  
+      align-items: flex-start; /* 这将确保所有子元素对齐到容器的顶部 */
+     /*  margin-bottom: 10px; */
+  }
+  
+  .avatar {
+      width: 40px; /* 头像的尺寸 */
+      height: 40px;
+      border-radius: 50%; /* 圆形头像 */
+      /*background-color: #e9e0e0;  暂时用灰色代替,你可以用图片替换 */
+      align-self: start; /* 这将覆盖 flex 容器的 align-items 并将头像对齐到顶部 */
+      margin-right: 10px; /* 按需调整以在头像和消息之间提供空间 */
+  }
+  
+  
+  .user-avatar {
+  
+      background-image: url('./webAi/assets/用户.png') ;
+      background-position: center;
+      background-repeat: no-repeat;
+      background-size: cover;
+     /*  background-color: #93939d; */ /* 用户头像颜色 */
+  }
+  
+  .bot-avatar {
+    
+      background-image: url('./webAi/assets/机器人.png');
+      background-position: center;
+      background-repeat: no-repeat;
+      background-size: cover;
+      /* background-color: green; */ /* AI头像颜色 */
+  }
+  /* 针对手机端的媒体查询 */
+  @media screen and (max-width: 768px) {
+      .user-avatar, .bot-avatar {
+          width: 30px;  /* 或其他更小的尺寸 */
+          height: 30px;
+      }
+  }
+  
+  .message_name{
+      display: flex;
+      align-items: center;
+      padding-top: 5px;
+      padding-bottom: 5px;
+  }
+  .name{
+      margin-left: 8px;
+      /* margin-bottom: 5px; */
+  }
+  
+  .message-content {
+      background-color: #f0f0f0; /* 消息背景 */
+      padding-left: 20px; /* 上下左右的内边距 */
+      padding-right: 20px; /* 上下左右的内边距 */
+      padding-top: 10px; /* 上下左右的内边距 */
+      padding-bottom: 0px; /* 上下左右的内边距 */
+      max-width: calc(90% - 10px); /* 计算头像和间距 */
+      border-radius: 7px; /* 边框圆角 */
+      /* margin-bottom: 10px; 和下一条消息之间的间距 */
+      /* 消息内容样式 */
+      display: flex;
+      flex-direction: column; /* 排列文本 */
+      margin-top: 0;
+  }
+  
+  
+  .message-text {
+      margin: 0;
+      white-space: normal; /* 确保文本会换行 */
+      word-wrap: break-word; /* 长单词或 URL 会被断开以适应容器宽度 */
+      overflow: auto;
+  }
+  .message-text ::v-deep p {
+      font-size: 14px !important;
+      margin: 5px 0 10px;
+      padding: 0;
+      font-size: inherit;
+      line-height: 22px;
+      font-weight: inherit;
+    }
+    .message-text ::v-deep ol{
+      padding-bottom: 10px ;
+  
+    }
+    .message-text ::v-deep .source-section h3 {
+      margin: 20px 0 3px;
+      color: #333;
+      font-size: 1.1em;
+    }
+    .message-text ::v-deep ol li{
+      padding-top:3px ;
+    }
+    .message-text ::v-deep ol li a{
+      color: #1296db;
+    }
+  .input-container {
+      position: absolute; /* 改为绝对定位 */
+      bottom: -115px; /* 距离底部的距离 */
+      left: 50%;
+      transform: translateX(-50%);
+      width: 90%; /* 设置宽度为弹窗宽度的90% */
+      max-width: 800px; /* 设置最大宽度 */
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 10px;
+      background: white;
+      border-radius: 10px;
+      box-shadow: 0px 2px 5px rgba(0,0,0,0.1);
+  }
+  
+  textarea {
+      font-size: 16px;
+      width: 100%; /* 可以根据需要调整 */
+      padding: 10px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      border: none;
+    outline: none;
+    resize: none;
+  }
+  
+  .input-container textarea {
+      width: 100%; /* 输入框占据所有可用空间 */
+      padding: 10px;
+      margin-right: 10px; /* 与按钮的间隔 */
+      box-sizing: border-box;
+  }
+  
+  .input-container button {
+      min-width: 80px; /* 例如,按钮的最小宽度为 100px */
+      white-space: nowrap; /* 防止文字折行 */
+      padding: 10px 20px;
+      cursor: pointer;
+  }
+  
+  
+  
+  
+  .sender-name {
+      font-weight: bold;
+      color: #333;
+      margin-left: -5px;
+  }
+  
+  .btn {
+      font-size: 16px;
+      color: #ffffff;
+      border-radius: 5px;
+      border: none;
+      background-color: #3cbade;
+      padding: 10px 20px;  /* 添加内边距 */
+      width: auto;  /* 让宽度自适应内容 */
+  }
+  
+  /* 针对手机端的媒体查询 */
+  @media screen and (max-width: 768px) {
+      .btn {
+          font-size: 14px;  /* 稍微减小字体大小 */
+          padding: 8px 16px;  /* 减小内边距 */
+          
+          max-width: 80px;  /* 限制最大宽度 */
+          margin: 0 auto;  /* 居中显示 */
+      }
+  }
+  </style>

+ 84 - 1
src/components/webAi/css/ChatBox.css

@@ -301,7 +301,7 @@
   }
 .input-container {
     position: absolute; /* 改为绝对定位 */
-    bottom: -115px; /* 距离底部的距离 */
+    bottom: 10px; /* 距离底部的距离 */
     left: 50%;
     transform: translateX(-50%);
     width: 90%; /* 设置宽度为弹窗宽度的90% */
@@ -368,4 +368,87 @@ textarea {
         max-width: 80px;  /* 限制最大宽度 */
         margin: 0 auto;  /* 居中显示 */
     }
+}
+
+.content_card{
+  width: 98%;
+  margin: 0 20px;
+}
+
+/* 应用卡片 */
+.card-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+}
+
+.card {
+  width: calc(20% - 15px);
+  background-color: #fff;
+  height: 300px;
+  border-radius: 8px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  padding: 20px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.card-header h2 {
+  margin: 0;
+  font-size: 22px;
+  margin-bottom: 5px;
+}
+
+.card-description {
+  color: #000;
+  font-size: 18px;
+  font-weight: 600;
+  margin-bottom: 15px;
+}
+
+.file-types {
+  margin-top: 10px;
+  display: flex;
+  gap: 15px;
+  margin-bottom: 15px;
+}
+
+.file-type {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+}
+
+.file-type i {
+  font-size: 20px;
+}
+
+.card_info{
+  width: 100%;
+  height: 60px;
+  font-size: 14px;
+  overflow:hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 3;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+}
+
+.card-footer {
+  margin-top: 20px;
+  font-size: 12px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.footer_left{
+
+}
+.footer_right{
+  color: #1296db;
 }

+ 268 - 104
src/components/webAi/index.vue

@@ -11,15 +11,9 @@
         label-position="right"
         label-width="90px"
       >
-        <el-form-item label="应用名称:" prop="chat_name">
-          <el-input
-            v-model="AIform.chat_name"
-            placeholder="请输入应用名称"
-          ></el-input>
-        </el-form-item>
         <el-row :gutter="24">
-          <el-col :span="9">
-            <el-form-item label="模型库:" prop="modelLibrary">
+          <el-col :span="8">
+            <!--  <el-form-item label="模型库:" prop="modelLibrary">
               <el-select
                 v-model="AIform.modelLibrary"
                 placeholder="ollama"
@@ -27,10 +21,16 @@
               >
                 <el-option label="ollama" value="ollama"></el-option>
               </el-select>
+            </el-form-item> -->
+            <el-form-item label="应用名称:" prop="chat_name">
+              <el-input
+                v-model="AIform.chat_name"
+                placeholder="请输入应用名称"
+              ></el-input>
             </el-form-item>
           </el-col>
-          <el-col :span="9">
-            <el-form-item label="模型类型:" prop="model_type">
+          <el-col :span="8">
+            <!-- <el-form-item label="模型类型:" prop="model_type">
               <el-select
                 v-model="AIform.model_type"
                 placeholder="请输入选择"
@@ -38,9 +38,7 @@
               >
                 <el-option label="chat" value="chat"></el-option>
               </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="6">
+            </el-form-item> -->
             <el-form-item label="模型名称:" prop="model_name">
               <el-select
                 v-model="AIform.model_name"
@@ -56,9 +54,7 @@
               </el-select>
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="24">
-          <el-col :span="9">
+          <el-col :span="8">
             <el-form-item label="知识库:" prop="knowledge_base_names">
               <el-select
                 v-model="AIform.knowledge_base_names"
@@ -73,10 +69,11 @@
                   :value="item.id"
                   :key="index"
                 ></el-option>
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="9">
+              </el-select> </el-form-item
+          ></el-col>
+        </el-row>
+        <el-row :gutter="24">
+          <el-col :span="8">
             <el-form-item label="文档目录:" prop="document_directories">
               <el-select
                 v-model="AIform.document_directories"
@@ -91,10 +88,9 @@
                   :value="dir.id"
                   :key="index"
                 ></el-option>
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="6">
+              </el-select> </el-form-item
+          ></el-col>
+          <el-col :span="8">
             <el-form-item label="文  档:" prop="documents">
               <el-select
                 v-model="AIform.documents"
@@ -112,6 +108,11 @@
               </el-select>
             </el-form-item>
           </el-col>
+          <el-col :span="8"> 
+            <el-form-item label="描 述:">
+              <el-input v-model="AIform.role_description" type="textarea" style="width: 350px;"></el-input>
+            </el-form-item> 
+          </el-col>
         </el-row>
       </el-form>
       <div class="selection-summary">
@@ -119,11 +120,11 @@
           <span class="summary-label">当前选择了:</span>
           <p class="summary-text">
             模型库: OLLAMA 模型类型: CHAT 模型名称:
-            <span>{{ AIform.modelName }}</span>
+            <span>{{ AIform.model_name }}</span>
           </p>
           <p class="summary-text">
-            知识库:<span>{{ AIform.knowledge_base_names }}</span> 文档:
-            <span>{{ AIform.documents }}</span>
+            知识库:<span>{{ getKnowledgeBaseNames }}</span> 文档:
+            <span>{{ getDocumentNames }}</span>
           </p>
         </div>
         <el-button
@@ -135,90 +136,253 @@
       </div>
     </div>
     <div class="title" v-if="$route.name !== 'ai'">
-      <h1>我的聊天应用</h1>
-      <span class="title_info">选择其他应用</span>
+      <h1>聊天应用列表</h1>
+      <!-- <span class="title_info">选择其他应用</span> -->
     </div>
-    <div class="content">
-      <!-- <div class="left">
-        <div class="chat-history">
-          <div
-            v-for="(chat, index) in chatHistory"
-            :key="index"
-            class="chat-item"
-          >
-            <div class="chat-header">
-              <img
-                src="../../assets/images/logo.png"
-                alt="User avatar"
-                class="avatar"
-              />
-              <div class="user-info">
-                <div class="info_header">
-                  <div class="username">{{ chat.username }}</div>
-                  <div class="timestamp">{{ chat.timestamp }}</div>
-                </div>
-                <div class="chat-message">{{ chat.message }}</div>
-                <div class="chat-preview">{{ chat.preview }}</div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div> -->
-      <!--  <div class="right" style="position: relative"> -->
-      <div class="messages">
-        <ul>
-          <li
-            v-for="message in messages"
-            :key="message.id"
-            :class="[
-              message.user,
-              message.user === 'user' ? 'user-message' : '',
-            ]"
-          >
-            <!-- 添加判断,如果是机器人的消息也显示头像 -->
-            <div class="message_name">
-              <div
-                class="avatar"
-                :class="{
-                  'user-avatar': message.user === 'user',
-                  'bot-avatar': message.user === 'bot',
-                }"
-              ></div>
-              <div class="name">
-                <span
-                  class="sender-name"
-                  style="color: #666666"
-                  v-if="message.user === 'user'"
-                  >你</span
+
+    <div class="content_card">
+      <div class="card-container">
+        <div
+          class="card"
+          v-for="(card, index) in knowledgeBases"
+          :key="index"
+         
+        >
+          <div class="card-header">
+            <h2>{{ card.chat_name }}</h2>
+            <el-dropdown>
+              <span class="el-dropdown-link">
+                <i class="el-icon-more"></i>
+              </span>
+              <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item @click.native="editApplication(card)"
+                  >编辑</el-dropdown-item
                 >
-                <span class="sender-name" v-if="message.user === 'bot'"
-                  >轻良AI助理</span
+                <el-dropdown-item @click.native="deleteApplication(card)"
+                  >删除</el-dropdown-item
                 >
-              </div>
+              </el-dropdown-menu>
+            </el-dropdown>
+          </div>
+          <p class="card-description">{{ card.model_name }}</p>
+          <div class="file-types">
+            <div class="file-type">
+              <span
+                v-for="(count, type) in card.knowledge_base_names"
+                :key="type"
+                >{{ count }}</span
+              >
             </div>
-            <!-- 使用 v-html 来渲染 Markdown 文本 -->
-            <div class="message-content">
-              <div
-                class="message-text"
-                v-html="renderMarkdown(message.message)"
-                @click="handleLinkClick"
-              ></div>
+          </div>
+          <div class="card_info">
+            {{ card.role_description }}
+          </div>
+          <div class="card-footer">
+            <div class="footer_left">
+              <img src="../../assets/images/u5.png" alt="" />
             </div>
-          </li>
-        </ul>
-      </div>
-      <div class="input-container">
-        <textarea
-          v-model="userInput"
-          rows="4"
-          @keydown="handleKeydown"
-          placeholder="发送消息..."
-        ></textarea>
-        <button @click="sendMessage" class="btn">发送</button>
+            <div class="footer_right">
+              <span  @click="openChatDialog(card)" style="cursor: pointer;">进入应用</span>
+              <span style="margin-left: 20px">默认选择</span>
+            </div>
+            <!-- <span class="update-time"></span> -->
+          </div>
+        </div>
       </div>
-      <!-- </div> -->
     </div>
-
+    <el-dialog
+      :title="currentChat.name"
+      :visible.sync="chatDialogVisible"
+      width="80%"
+      :before-close="handleCloseDialog"
+      top="20px"
+      :append-to-body="true"
+    >
+      <div class="content" style="margin-bottom: 115px">
+        <div class="messages">
+          <ul>
+            <li
+              v-for="message in messages"
+              :key="message.id"
+              :class="[
+                message.user,
+                message.user === 'user' ? 'user-message' : '',
+              ]"
+            >
+              <!-- 添加判断,如果是机器人的消息也显示头像 -->
+              <div class="message_name">
+                <div
+                  class="avatar"
+                  :class="{
+                    'user-avatar': message.user === 'user',
+                    'bot-avatar': message.user === 'bot',
+                  }"
+                ></div>
+                <div class="name">
+                  <span
+                    class="sender-name"
+                    style="color: #666666"
+                    v-if="message.user === 'user'"
+                    >你</span
+                  >
+                  <span class="sender-name" v-if="message.user === 'bot'"
+                    >轻良AI助理</span
+                  >
+                </div>
+              </div>
+              <!-- 使用 v-html 来渲染 Markdown 文本 -->
+              <div class="message-content">
+                <div
+                  class="message-text"
+                  v-html="renderMarkdown(message.message)"
+                  @click="handleLinkClick"
+                ></div>
+              </div>
+            </li>
+          </ul>
+        </div>
+        <div class="input-container">
+          <textarea
+            v-model="userInput"
+            rows="4"
+            @keydown="handleKeydown"
+            placeholder="发送消息..."
+          ></textarea>
+          <button @click="sendMessage" class="btn">发送</button>
+        </div>
+      </div>
+    </el-dialog>
+    <!-- 修改应用 -->
+    <!-- 添加编辑弹窗 -->
+    <el-dialog
+      title="编辑应用"
+      :visible.sync="editDialogVisible"
+      width="80%"
+      @close="handleEditDialogClose"
+      :append-to-body="true"
+    >
+      <el-form
+        :model="editForm"
+        :rules="rules"
+        ref="editFormRef"
+        label-width="120px"
+      >
+        <el-row :gutter="24">
+          <el-col :span="8">
+            <el-form-item label="应用名称:" prop="chat_name">
+          <el-input
+            v-model="editForm.chat_name"
+            placeholder="请输入应用名称"
+          ></el-input>
+        </el-form-item>
+            <!-- <el-form-item label="模型库:" prop="modelLibrary">
+              <el-select
+                v-model="editForm.modelLibrary"
+                placeholder="ollama"
+                style="width: 100%"
+              >
+                <el-option label="ollama" value="ollama"></el-option>
+              </el-select>
+            </el-form-item> -->
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="模型名称:" prop="model_name">
+              <el-select
+                v-model="editForm.model_name"
+                placeholder="请输入选择"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="(item, index) in modelNameList"
+                  :label="item.name"
+                  :value="item.name"
+                  :key="index"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+            <!-- <el-form-item label="模型类型:" prop="model_type">
+              <el-select
+                v-model="editForm.model_type"
+                placeholder="请输入选择"
+                style="width: 100%"
+              >
+                <el-option label="chat" value="chat"></el-option>
+              </el-select>
+            </el-form-item> -->
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="知识库:" prop="knowledge_base_names">
+              <el-select
+                v-model="editForm.knowledge_base_names"
+                multiple
+                placeholder="请选择知识库"
+                style="width: 100%"
+                @change="handleEditKnowledgeBaseChange"
+              >
+                <el-option
+                  v-for="(item, index) in kneList"
+                  :label="item.name"
+                  :value="item.id"
+                  :key="index"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="24">
+          <el-col :span="8">
+            <el-form-item label="文档目录:" prop="document_directories">
+              <el-select
+                v-model="editForm.document_directories"
+                placeholder="请选择文档目录"
+                style="width: 100%"
+                :disabled="
+                  !(
+                    editForm.knowledge_base_names &&
+                    editForm.knowledge_base_names.length
+                  )
+                "
+                @change="handleEditDirectoryChange"
+              >
+                <el-option
+                  v-for="(dir, index) in editDirectoryList"
+                  :label="dir.name"
+                  :value="dir.id"
+                  :key="index"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="文  档:" prop="documents">
+              <el-select
+                v-model="editForm.documents"
+                multiple
+                placeholder="请选择文档"
+                style="width: 100%"
+                :disabled="!editForm.document_directories"
+              >
+                <el-option
+                  v-for="(doc, index) in editDocumentList"
+                  :label="doc.name"
+                  :value="doc.id"
+                  :key="index"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="描 述:">
+              <el-input v-model="editForm.role_description" type="textarea" style="width: 350px;"></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="editDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitEdit">确 定</el-button>
+      </span>
+    </el-dialog>
     <!-- 添加 LoginModal 组件 -->
     <LoginModal
       :visible.sync="showLoginModal"

+ 332 - 16
src/components/webAi/js/ChatBox.js

@@ -1,7 +1,7 @@
 import LoginModal from "@/components/LoginModal";
 import MarkdownIt from "markdown-it";
 import { pcInnerAi,getMinioURl } from "@/api/api";
-import {modelList,listBuckets,selectTypeList,getBucketContents,configSave,configList} from "@/api/knowledge"
+import {modelList,listBuckets,selectTypeList,getBucketContents,configSave,configList,configDelete} from "@/api/knowledge"
 import axios from 'axios';
 const md = new MarkdownIt();
 
@@ -55,7 +55,7 @@ export default {
          language: "en",
          timeout: 30,
          role_name: "Admin",
-         role_description: "Administrator role with full access.",
+         role_description: "",
          role_permissions: ["read", "write", "delete"]
       },
       /* 模型库 */
@@ -75,19 +75,51 @@ export default {
         chat_name: [
           { required: true, message: '请填写应名称', trigger: 'blur' }
         ],
-        modelName: [
+        model_name: [
           { required: true, message: '请选择模型名称', trigger: 'change' }
         ],
         knowledge_base_names: [
           { required: true, message: '请选择至少一个知识库', trigger: 'change' }
         ],
-        document_directories: [
+        /* document_directories: [
           { required: true, message: '请选择文档目录', trigger: 'change' }
-        ],
+        ], */
         documents: [
           { required: true, message: '请选择至少一个文档', trigger: 'change' }
         ]
-      }
+      },
+      chatDialogVisible: false,
+      /* 应用列表 */
+      knowledgeBases: [],
+      currentChat: {},
+      /* 修改弹窗 */
+      editDialogVisible: false,
+      editForm: {
+        // 复制 AIform 的结构,但初始值为空
+        chat_name: '',
+        modelLibrary: 'ollama',
+        model_type: 'chat',
+        model_name: '',
+        knowledge_base_names: [],
+        document_directories: [],
+        documents: [],
+        temperature: 0.7,
+        max_tokens: 150,
+        top_p: 1.0,
+        frequency_penalty: 0.0,
+        presence_penalty: 0.0,
+        response_format: "text",
+        context_window: 2048,
+        user_id: "user123",
+        session_id: "session456",
+        language: "en",
+        timeout: 30,
+        role_name: "Admin",
+        role_description: "",
+        role_permissions: ["read", "write", "delete"]
+      },
+      editDirectoryList: [],
+      editDocumentList: [],
     };
   },
   created() {
@@ -111,7 +143,284 @@ export default {
     /* 获取模型列表 */
     this.init()
   },
+  computed: {
+    getKnowledgeBaseNames() {
+      const names = this.AIform.knowledge_base_names.map(id => 
+        this.kneList.find(item => item.id === id)?.name || id
+      );
+      if (names.length <= 2) {
+        return names.join(', ');
+      } else {
+        return `${names[0]}, ${names[1]} 等${names.length}个`;
+      }
+    },
+    getDocumentNames() {
+      const names = this.AIform.documents.map(id => 
+        this.documentList.find(item => item.id === id)?.name || id
+      );
+      if (names.length <= 2) {
+        return names.join(', ');
+      } else {
+        return `${names[0]}, ${names[1]} 等${names.length}个`;
+      }
+    }
+  },
   methods: {
+    /* 删除 */
+    deleteApplication(card) {
+      this.$confirm('确定要删除这个应用吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        // 调用删除 API
+        configDelete({id:card.id}).then(response => {
+          if (response.status==200) {
+            this.$message({
+              type: 'success',
+              message: '删除成功!'
+            });
+            // 从列表中移除已删除的应用
+            /* const index = this.knowledgeBases.findIndex(kb => kb.id === card.id);
+            if (index > -1) {
+              this.knowledgeBases.splice(index, 1);
+            } */
+              this.fetchApplicationList();
+          } else {
+            this.$message.error('删除失败: ' + response.message);
+          }
+        }).catch(error => {
+          console.error('删除应用时出错:', error);
+          this.$message.error('删除失败,请稍后重试');
+        });
+      }).catch(() => {
+        this.$message({
+          type: 'info',
+          message: '已取消删除'
+        });
+      });
+    },
+    handleEditKnowledgeBaseChange(val) {
+      // 重置目录和文档选择
+      this.editForm.document_directories = [];
+      this.editForm.documents = [];
+      this.editDocumentList = [];
+      // 加载新选择的知识库对应的目录列表
+      this.loadEditDirectoryList(val);
+    },
+    handleEditDirectoryChange(val) {
+      // 重置文档选择
+      this.editForm.documents = [];
+      
+      // 加载选中目录对应的文档列表
+      this.loadEditDocumentList(val);
+    },
+    /* 编辑 */
+    editApplication(card) {
+        // 根据 card 的数据填充 editForm
+      
+      this.editForm = JSON.parse(JSON.stringify(card)); // 深拷贝以避免直接修改原对象
+      this.editForm.knowledge_base_names = this.editForm.knowledge_base_names.map(name => {
+        const kb = this.kneList.find(kb => kb.name === name);
+        return kb ? kb.id : name; // 如果找不到对应的知识库,保留原名称
+      });
+      this.editDialogVisible = true;
+      console.log(this.editForm);
+      // 加载知识库对应的目录列表
+      this.loadEditDirectoryList(this.editForm.knowledge_base_names);
+   
+    },
+
+    handleEditDialogClose() {
+      this.$refs.editFormRef.resetFields();
+      this.editDirectoryList = [];
+      this.editDocumentList = [];
+    },
+    
+
+    async loadEditDirectoryList(val) {
+      this.editDirectoryList = []; // 清空现有目录列表
+      let totalDocuments = 0;
+      let otherFolderCount = 0;
+      
+      for (const kbName of val) {
+        // 根据知识库名称找到对应的 ID
+        const kbId = this.kneList.find(kb => kb.name === kbName)?.id;
+        
+        /* if (!kbId) {
+          console.error(`No matching knowledge base found for name: ${kbName}`);
+          continue;
+        } */
+    
+        const typeForm = {
+          page: 1,
+          pageSize: 9999,
+          kb_id: kbName,
+        };
+        
+        try {
+          const res = await selectTypeList(typeForm);
+          if (res.data && Array.isArray(res.data)) {
+            this.editDirectoryList = [...new Set([...this.editDirectoryList, ...res.data])];
+            
+            res.data.forEach(folder => {
+              if (folder.id === "other") {
+                otherFolderCount += folder.document_count || 0;
+              } else {
+                totalDocuments += folder.document_count || 0;
+              }
+            });
+          }
+        } catch (error) {
+          console.error(`Error loading directory list for kb_id ${kbId}:`, error);
+        }
+      }
+      
+      this.editDirectoryList.unshift({
+        id: "001",
+        name: "全部",
+        document_count: totalDocuments + otherFolderCount,
+      });
+    },
+    
+    loadEditDocumentList(val) {
+      // 找到选中目录的名称
+      const selectedDirectoryName = val[0];
+    
+      // 从 kneList 中找到对应的知识库
+      const selectedKnowledgeBase = this.kneList.find(kb => 
+        kb.name === this.editForm.knowledge_base_names[0]
+      );
+    
+      /* if (!selectedKnowledgeBase) {
+        console.error('No matching knowledge base found');
+        return;
+      } */
+    
+      let queryForm = {
+        page: 1,
+        pageSize: 9999,
+        bucket_id: this.editForm.knowledge_base_names[0],
+        doc_type_id: selectedDirectoryName === '全部' ? 0 : this.getDirectoryIdByName(selectedDirectoryName),
+      };
+    
+      getBucketContents(queryForm).then(res => {
+        console.log(res);
+        this.editDocumentList = res.data.documents;
+      });
+    },
+    
+    // 添加一个辅助方法来根据目录名称获取目录ID
+    getDirectoryIdByName(directoryName) {
+      const directory = this.editDirectoryList.find(dir => dir.name === directoryName);
+      return directory ? directory.id : null;
+    },
+    
+    submitEdit() {
+      this.$refs.editFormRef.validate(async (valid) => {
+        if (valid) {
+          try {
+            const convertedForm = { ...this.editForm };
+            console.log(this.editForm.documents);
+            // 转换知识库、文档目录和文档的ID为名称
+            convertedForm.knowledge_base_names = this.safeGetNamesByIds(this.editForm.knowledge_base_names, this.kneList);
+           /*  convertedForm.documents = this.safeGetNamesByIds(this.editForm.documents, this.editDocumentList); */
+            convertedForm.document_directories = this.safeGetNamesByIds(this.editForm.document_directories, this.editDirectoryList);
+            if (convertedForm.document_directories.length === 0) {
+              convertedForm.document_directories = ['全部'];
+            }
+    
+            console.log('Converted form for edit:', convertedForm);
+    
+            const response = await axios.post(`${process.env.VUE_APP_BASE_API}/chatbot/configuration/update/`, convertedForm, {
+              headers: {
+                'Content-Type': 'application/json'
+              }
+            });
+    
+            if (response.status === 200) {
+              this.$message.success('应用更新成功');
+              this.editDialogVisible = false;
+              this.fetchApplicationList();
+            } else {
+              this.$message.error(response.data.message || '应用更新失败');
+            }
+          } catch (error) {
+            console.error('Error updating application:', error);
+            this.$message.error('应用更新失败,请稍后重试');
+          }
+        } else {
+          this.$message.error('请填写所有必填字段');
+          return false;
+        }
+      });
+    },
+
+    // 添加一个方法来获取完整的应用配置
+    async fetchFullApplicationConfig(appId) {
+      try {
+        const response = await axios.get(`${process.env.VUE_APP_BASE_API}/chatbot/configDetail/${appId}`);
+        if (response.status === 200 && response.data.code === 200) {
+          return response.data.data;
+        } else {
+          throw new Error(response.data.message || '获取应用配置失败');
+        }
+      } catch (error) {
+        console.error('Error fetching application config:', error);
+        this.$message.error('获取应用配置失败,请稍后重试');
+        return null;
+      }
+    },
+
+    // 实现获取应用列表的方法
+    async fetchApplicationList() {
+      try {
+        const response = await configList();
+        if (response.status === 200) {
+          this.knowledgeBases = response.data; // 假设返回的数据结构符合要求
+        } else {
+          throw new Error(response.data.message || '获取应用列表失败');
+        }
+      } catch (error) {
+        console.error('Error fetching application list:', error);
+        this.$message.error('获取应用列表失败,请稍后重试');
+      }
+    },
+    /*  */
+    openChatDialog(card) {
+      console.log(card);
+      this.currentChat = card;
+      this.chatDialogVisible = true;
+      // 可能需要在这里初始化聊天记录或执行其他操作
+      this.messages = []; // 清空之前的聊天记录
+      // 可以添加一个欢迎消息
+      this.messages.push({
+        user: "bot",
+        messageType: "TEXT",
+        message: `欢迎使用 ${card.name} 聊天应用`,
+        html: "",
+        time: "",
+        done: true,
+      });
+    },
+    /* 关闭 */
+    handleCloseDialog(done) {
+      // 在这里可以添加关闭前的确认逻辑
+      this.$confirm('确认关闭?')
+        .then(_ => {
+          done();
+        })
+        .catch(_ => {});
+    },
+    /* 点击应用 */
+    getFileTypeIcon(type) {
+      const iconMap = {
+        word: 'el-icon-document',
+        excel: 'el-icon-tickets',
+        pdf: 'el-icon-document-copy'
+      };
+      return iconMap[type] || 'el-icon-document';
+    },
     /* 生成引用 */
     generateApplication() {
       this.$refs.AIformRef.validate(async (valid) => {
@@ -135,14 +444,16 @@ export default {
             console.log('Converted form:', convertedForm);
 
             // 使用 axios 发送 POST 请求
-            const response = await axios.post(`http://192.168.1.199:8084/chatbot/configSave/`, convertedForm, {
+            const response = await axios.post(`${process.env.VUE_APP_BASE_API}/chatbot/configSave/`, convertedForm, {
               headers: {
                 'Content-Type': 'application/json'
               }
             });
 
-            if (response.status === 200 && response.data.code === 200) {
+            if (response.status === 200) {
               this.$message.success('应用生成成功');
+              this.AIform={}
+              this.fetchApplicationList();
               // 可选:重定向到新应用或更新 UI
             } else {
               this.$message.error(response.data.message || '应用生成失败');
@@ -261,12 +572,13 @@ newChat() {
 
     handleLinkClick(event) {
       if (event.target.tagName === 'A') {
-        localStorage.setItem('href',event.target.href)
-        let _this = this;
-        let a = document.createElement("a");
-        a.href = "#/preview"; // 使用 hash
-        a.target = "_blank";
-        a.click();
+        event.preventDefault(); // 阻止默认行为
+    
+        const href = event.target.href;
+        localStorage.setItem('href', href);
+    
+        // 使用 window.open 打开新标签页
+        window.open('#/preview', '_blank');
       }
     },
 
@@ -603,15 +915,19 @@ newChat() {
       listBuckets({ user_id: this.$store.state.user.id }).then(res=>{
         this.kneList=res.data
       })
-      /* 配置列表 */
+      /* 应用列表 */
       configList().then(res=>{
+        this.knowledgeBases=res.data
         console.log(res);
       })
     }
   },
   updated() {
     const messagesContainer = this.$el.querySelector(".messages");
-    messagesContainer.scrollTop = messagesContainer.scrollHeight;
+    if(messagesContainer){
+          messagesContainer.scrollTop = messagesContainer.scrollHeight;
+    }
+
   },
   
 };

+ 2 - 2
src/layout/components/Navbar.vue

@@ -12,7 +12,7 @@
     <div class="right-menu">
       <template v-if="device !== 'mobile'">
         <div class="right-menu-item">
-          <svg-icon className="svg-style" size="120" icon-class="AI模块" @click="openChatDialog" />
+          <img src="../../assets/images/人工智能.png" alt="" style="cursor: pointer; margin-top: 14px;width: 25px;height: 25px" @click="openChatDialog">
         </div>
         <search id="header-search" class="right-menu-item" />
         <error-log class="errLog-container right-menu-item hover-effect" />
@@ -58,7 +58,7 @@ import ErrorLog from "@/components/ErrorLog";
 import Screenfull from "@/components/Screenfull";
 import SizeSelect from "@/components/SizeSelect";
 import Search from "@/components/HeaderSearch";
-import ChatBox from "@/components/webAi/index.vue";
+import ChatBox from "@/components/ai.vue";
 import elDragDialog from "@/directive/el-drag-dialog";
 
 export default {

+ 2 - 2
src/router/index.js

@@ -340,13 +340,13 @@ export const asyncRoutes = [
         hidden:true,
         meta: { title: '知识库配置',  noCache: true ,roles: ['admin']}
       },
-      /* {
+      {
         path: 'chatPage/index',
         component: () => import('@/views/chatPage/index.vue'),
         name: 'chatbot',
       
         meta: { title: '聊天应用',  noCache: true ,roles: ['admin']}
-      } */
+      }
     ]
   },
     {

+ 1 - 1
src/views/document/com/menus.vue

@@ -143,7 +143,7 @@ import sourceAi from "./components/sourceAi/index.vue";
 import sourceEs from "./components/sourceEs/index.vue";
 import Variable from "./components/Variable";
 import elDragDialog from "@/directive/el-drag-dialog";
-import ChatBox from "@/components/webAi/index.vue";
+import ChatBox from "@/components/ai.vue";
 export default {
   name: "menus",
   emits: ["onEvents", "onVariable"],

+ 849 - 10
src/views/login/aiIndex.vue

@@ -1,6 +1,59 @@
 <template>
-  <div>
-    <ChatBox />
+  <div class="chat-container">
+    <div class="content">
+      <div class="messages">
+        <ul>
+          <li
+            v-for="message in messages"
+            :key="message.id"
+            :class="[
+              message.user,
+              message.user === 'user' ? 'user-message' : '',
+            ]"
+          >
+            <!-- 添加判断,如果是机器人的消息也显示头像 -->
+            <div class="message_name">
+              <div
+                class="avatar"
+                :class="{
+                  'user-avatar': message.user === 'user',
+                  'bot-avatar': message.user === 'bot',
+                }"
+              ></div>
+              <div class="name">
+                <span
+                  class="sender-name"
+                  style="color: #666666"
+                  v-if="message.user === 'user'"
+                  >你</span
+                >
+                <span class="sender-name" v-if="message.user === 'bot'"
+                  >轻良AI助理</span
+                >
+              </div>
+            </div>
+            <!-- 使用 v-html 来渲染 Markdown 文本 -->
+            <div class="message-content">
+              <div
+                class="message-text"
+                v-html="renderMarkdown(message.message)"
+                @click="handleLinkClick"
+              ></div>
+            </div>
+          </li>
+        </ul>
+      </div>
+      <div class="input-container">
+        <textarea
+          v-model="userInput"
+          rows="4"
+          @keydown="handleKeydown"
+          placeholder="发送消息..."
+        ></textarea>
+        <button @click="sendMessage" class="btn">发送</button>
+      </div>
+      <!-- </div> -->
+    </div>
     <LoginModal
       :visible.sync="showLoginModal"
       @close="showLoginModal = false"
@@ -12,6 +65,11 @@
 <script>
 import ChatBox from "@/components/webAi/index.vue";
 import LoginModal from "@/components/LoginModal";
+import MarkdownIt from "markdown-it";
+import { pcInnerAi,getMinioURl } from "@/api/api";
+import {modelList} from "@/api/knowledge"
+import axios from 'axios';
+const md = new MarkdownIt();
 
 export default {
   components: {
@@ -21,32 +79,813 @@ export default {
   data() {
     return {
       showLoginModal: false,
+      isThinking: false,
+      thinkingDots: '',
+      messages: [
+        {
+          user: "bot",
+          messageType: "TEXT",
+          message: "欢迎使用智能AI助理",
+          html: "",
+          time: "",
+          done: true,
+        },
+      ],
+      generating: false,
+      userInput: "",
+      websocket: null,
+      wsUrl:'http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false',
+      tweaks:{},
+      isLoggedIn: false, // 添加登录状态标志
+      idArray: [],
+      minioUrls: [], // 新增:用于存储 Minio URL
+      AImodel:'1',//模型
+      AIknowledgeBase:"1",//知识库
+      AIFile:'1',//文件
+      AIform:{
+        modelLibrary: 'ollama',
+        modelType: '',
+        modelName: '',
+        knowledgeBase: '',
+        documentDirectory: '',
+        document: '',
+      }
     };
   },
   mounted() {
     this.checkAIData();
+    if(this.$route.name=='ai'){
+      /* 外部知识库 */
+       /* 外部知识库 */
+       const AIDataString = localStorage.getItem("AIData");
+        if (!AIDataString) {
+          this.showLoginModal = true;
+          return;
+        }
+        try {
+          const AIData = JSON.parse(AIDataString);
+          if (AIData && AIData.data) {
+            this.wsUrl = AIData.data.url || '';
+            this.tweaks = AIData.data.tweaks || {};
+          } else {
+            console.error('Invalid AIData structure');
+            this.showLoginModal = true;
+          }
+        } catch (error) {
+          console.error('Error parsing AIData:', error);
+          this.showLoginModal = true;
+        }
+    }else{
+      /* 内部知识库 */
+      pcInnerAi().then(res=>{
+        if(res.status!==200) return
+        this.wsUrl=res.data.url
+        this.tweaks=res.data.tweaks
+        
+      })
+      
+    }
   },
   methods: {
     checkAIData() {
-      const aiData = localStorage.getItem('AIData');
+      const aiData = localStorage.getItem("AIData");
       this.showLoginModal = !aiData;
     },
     handleLoginSuccess() {
       this.showLoginModal = false;
       // 处理登录成功后的逻辑,例如刷新数据或更新状态
     },
+/* 聊天记录 */
+selectChat(index) {
+  this.currentChatIndex = index;
+  // Load the selected chat messages
+  // This is where you would typically load the messages for the selected chat
+},
+newChat() {
+  this.chatHistory.push({ title: `聊天 ${this.chatHistory.length + 1}`, preview: "新的聊天..." });
+  this.currentChatIndex = this.chatHistory.length - 1;
+  // Clear the current messages and start a new chat
+  this.messages = [];
+},
+
+    handleLinkClick(event) {
+      if (event.target.tagName === 'A') {
+        localStorage.setItem('href',event.target.href)
+        let _this = this;
+        let a = document.createElement("a");
+        a.href = "#/preview"; // 使用 hash
+        a.target = "_blank";
+        a.click();
+      }
+    },
+
+    handleLoginSuccess() {
+      this.showLoginModal = false;
+      this.isLoggedIn = true;
+      // 登录成功后,可以继续发送消息
+      this.sendMessage();
+    },
+    getUuid() {
+      return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
+        /[xy]/g,
+        function (c) {
+          var r = (Math.random() * 16) | 0,
+            v = c == "x" ? r : (r & 0x3) | 0x8;
+          return v.toString(16);
+        }
+      );
+    },
+    connectWebSocket() {
+      const wsUrl = 'http://58.246.234.210:7860/api/v1/run/2f1faff2-99df-4821-87d6-43d693084c4b?stream=false'
+      // 这里做一个简单的鉴权,只有符合条件的鉴权才能握手成功
+      // 移除这里的fetch调用,改用WebSocket
+      this.websocket = new WebSocket(wsUrl);
+    
+     /*  this.websocket.onerror = (event) => {
+        console.error("WebSocket 连接错误:", event);
+      };
+     */
+      this.websocket.onclose = (event) => {
+        console.log("WebSocket 连接关闭:", event);
+      };
+    
+      this.websocket.onopen = (event) => {
+        console.log("WebSocket 连接已建立:", event);
+      };
+      this.websocket.onmessage = (event) => {
+        // 解析收到的消息
+        const result = JSON.parse(event.data);
+
+       
+
+        // 检查消息是否完结
+        if (result.done) {
+          this.messages[this.messages.length - 1].done = true;
+          return;
+        }
+
+        if (this.messages[this.messages.length - 1].done) {
+          // 添加新的消息
+          this.messages.push({
+            time: Date.now(),
+            message: result.content,
+            messageType: "TEXT",
+            user: "bot",
+            done: false,
+          });
+        } else {
+          // 更新最后一条消息
+          let lastMessage = this.messages[this.messages.length - 1];
+          lastMessage.message += result.content;
+          this.messages[this.messages.length - 1] = lastMessage;
+        }
+      };
+    },
+    async sendMessage() {
+      if(this.$route.name=='ai'){
+        if(!localStorage.getItem('AIData')){
+          this.showLoginModal=true
+          return
+        }
+      }
+      const chatId = this.getUuid();
+      const wsUrl =this.wsUrl//'http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false'
+      let message = this.userInput.trim();
+      if (message) {
+        // Markdown换行:在每个换行符之前添加两个空格
+        message = message.replace(/(\r\n|\r|\n)/g, "  \n");
+
+        this.messages.push({
+          time: Date.now(),
+          message: message,
+          messageType: "TEXT",
+          user: "user",
+          done: true,
+        });
+        this.userInput = "";
+       // 添加机器人的响应消息(初始为空)
+       const botMessage = {
+        user: "bot",
+        messageType: "TEXT",
+        message: "",
+        html: "",
+        time: Date.now(),
+        done: false,
+      };
+      this.messages.push(botMessage);
+
+      // 开始模拟思考
+      const thinkingPromise = this.simulateThinking(botMessage);
+        // 通过 HTTP 发送消息
+        try {
+          ///send-message
+          const response = await fetch(`${wsUrl}`, {
+            method: "POST",
+            headers: {
+              "Content-Type": "application/json",
+            },
+            body: JSON.stringify({
+              chatId: chatId,
+              input_value: message,
+              output_type: "chat",
+              input_type: "chat",
+              tweaks:this.tweaks /* {
+                "ChatInput-U82Vu": {},
+                "Prompt-b8Z1E": {},
+                "OllamaModel-z9SVx": {},
+                "Milvus-l0vvr": {},
+                "OllamaEmbeddings-4Ml5q": {},
+                "ParseData-LM8yW": {},
+                "ParseData-jVoZg": {},
+                "APIRequest-OnZHl": {},
+                "ParseData-fH1vY": {},
+                "ChatOutput-ybzb9": {}
+              } */,
+            }),
+          });
+
+          if (!response.ok) {
+            const errorText = await response.text(); // 获取错误文本
+            throw new Error(
+              `HTTP error! status: ${response.status}, message: ${errorText}`
+            );
+          }
+
+          const result = await response.json();
+         // 等待思考动画完成
+         await thinkingPromise;
+         if(this.$route.name !== 'ai'){
+           // 提取 additional_input 数据
+        const additionalInput = result.outputs?.[0]?.outputs?.[0]?.results?.message?.data?.additional_input;
+        
+        // 处理字符串,提取 ID 值
+        this.idArray = additionalInput.split('\n')  // 按换行符分割
+          .map(line => line.trim())  // 去除每行首尾空白
+          .filter(line => line.startsWith('ID:'))  // 只保留以 'ID:' 开头的行
+          .map(line => line.substring(3).trim());  // 提取 'ID:' 后面的内容并去除空白
+
+        // 创建符合要求格式的对象
+        const idObject = { ids: this.idArray };
+
+        console.log("ID Object:", idObject);
+
+        // 调用方法来获取 Minio URL,传递新的 idObject
+        await this.getMinioUrls(idObject);
+         }
+         
+         this.handleResponse(result, botMessage);
+       } catch (error) {
+         console.error("Error sending message:", error);
+         // 如果发生错误,停止思考动画
+         botMessage.done = true;
+         botMessage.message = "抱歉,发生了错误,请稍后再试。";
+       }
+      }
+    },
+    async getMinioUrls(idObject) {
+      this.minioUrls = []; // 清空之前的 URL
+
+      try {
+        console.log("Sending to backend:", JSON.stringify(idObject));
+
+        const response = await axios.post('http://58.246.234.210:8084/milvus/getMinioURl', idObject, {
+          headers: {
+            'Content-Type': 'application/json'
+          }
+        });
+
+        if (response.status === 200 && response.data) {
+          this.minioUrls = response.data.data; // 假设返回的是 URL 数组
+          console.log("Received Minio URLs:", this.minioUrls);
+        } else {
+          console.error('Failed to get Minio URLs');
+        }
+      } catch (error) {
+        console.error('Error fetching Minio URLs:', error);
+        if (error.response) {
+          console.error('Response data:', error.response.data);
+          console.error('Response status:', error.response.status);
+          console.error('Response headers:', error.response.headers);
+        } else if (error.request) {
+          console.error('No response received:', error.request);
+        } else {
+          console.error('Error message:', error.message);
+        }
+      }
+    },
+    async simulateThinking(message) {
+      this.isThinking = true;
+      const thinkingTime = Math.random() * 1000 + 500; // 思考时间为 0.5-1.5 秒
+      const dotInterval = 200; // 每 200ms 更新一次点
+      
+      const updateDots = () => {
+        this.thinkingDots += '.';
+        if (this.thinkingDots.length > 3) {
+          this.thinkingDots = '';
+        }
+        message.message =  this.thinkingDots;
+      };
+  
+      const intervalId = setInterval(updateDots, dotInterval);
+  
+      await new Promise(resolve => setTimeout(resolve, thinkingTime));
+      
+      clearInterval(intervalId);
+      this.isThinking = false;
+      this.thinkingDots = '';
+      message.message = '';
+    },
+  
+    async handleResponse(value, existingMessage) {
+      const data = value.outputs[0].outputs[0].results.message;
+      
+      existingMessage.messageType = data.text_key;
+      existingMessage.time = data.timestamp;
+      existingMessage.message = '';
+      
+      let mainText = data.text;
+      let sourceText = '';
+
+      if (this.$route.name !== 'ai' && this.minioUrls && this.minioUrls.length > 0) {
+        sourceText = "\n\n<div class='source-section'><h3>相关资料来源:</h3><ol>";
+        this.minioUrls.forEach((url, index) => {
+          sourceText += `<li><a href="${url.url}" target="_blank" class="source-link">${url.object_name}</a></li>`;
+        });
+        sourceText += "</ol></div>";
+      }
+
+      // 先进行主要内容的打字效果
+      await this.typeMessage(existingMessage, mainText);
+
+      // 直接添加资料来源部分
+      if (sourceText) {
+        existingMessage.message += sourceText;
+        existingMessage.html = this.renderMarkdown(existingMessage.message);
+      }
+    },
+
+    typeMessage(message, fullText) {
+      return new Promise(resolve => {
+        const delay = 30;
+        let i = 0;
+        const typeChar = () => {
+          if (i < fullText.length) {
+            message.message += fullText[i];
+            i++;
+            setTimeout(typeChar, delay);
+          } else {
+            message.done = true;
+            message.html = this.renderMarkdown(message.message);
+            resolve();
+          }
+        };
+        
+        typeChar();
+      });
+    },
+    renderMarkdown(rawMarkdown) {
+      const md = new MarkdownIt({
+        html: true,
+        breaks: true,
+        linkify: true
+      });
+
+      // 自定义链接渲染规则
+      md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
+        const token = tokens[idx];
+        const hrefIndex = token.attrIndex('href');
+        const href = token.attrs[hrefIndex][1];
+        return `<span class="custom-link" data-href="${href}">`;
+      };
+
+      md.renderer.rules.link_close = () => '</span>';
+
+      const renderedHtml = md.render(rawMarkdown);
+
+      // 使用 setTimeout 来确保 DOM 更新后再添加事件监听器
+      setTimeout(() => {
+        const links = document.querySelectorAll('.custom-link');
+        links.forEach(link => {
+          link.addEventListener('click', (e) => {
+            e.preventDefault();
+            const href = link.getAttribute('data-href');
+            window.open(href, '_blank');
+          });
+        });
+      }, 0);
+
+      return renderedHtml;
+    },
+    handleKeydown(event) {
+      // Check if 'Enter' is pressed without the 'Alt' key
+      if (event.key === "Enter" && !(event.shiftKey || event.altKey)) {
+        event.preventDefault(); // Prevent the default action to avoid line break in textarea
+        this.sendMessage();
+      } else if (event.key === "Enter" && event.altKey) {
+        // Allow 'Alt + Enter' to insert a newline
+        const cursorPosition = event.target.selectionStart;
+        const textBeforeCursor = this.userInput.slice(0, cursorPosition);
+        const textAfterCursor = this.userInput.slice(cursorPosition);
+
+        // Insert the newline character at the cursor position
+        this.userInput = textBeforeCursor + "\n" + textAfterCursor;
+
+        // Move the cursor to the right after the inserted newline
+        this.$nextTick(() => {
+          event.target.selectionStart = cursorPosition + 1;
+          event.target.selectionEnd = cursorPosition + 1;
+        });
+      }
+    },
+    beforeDestroy() {
+      if (this.websocket) {
+        this.websocket.close();
+      }
+    },
+      /* 获取列表 */
+  init(){
+    modelList().then(res=>{
+      console.log(res);
+    })
+  }
+  },
+  updated() {
+    const messagesContainer = this.$el.querySelector(".messages");
+    messagesContainer.scrollTop = messagesContainer.scrollHeight;
   },
 };
 </script>
 <style scoped>
-::v-deep .chat-container {
-  height: 100vh;
+
+.chat-container {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    margin-bottom: 150px;
+     /* 或者按照实际需要调整高度 */
+}
+.chat-container .header{
+  width: 1200px;
+  margin: 10px auto 0;
+  border: 1px solid #d7d7d7;
+  border-radius: 10px;
+}
+.form-container {
+max-width: 1200px; /* 或者其他适合你布局的宽度 */
+margin: 0 auto; /* 这会使容器水平居中 */
+padding: 0 15px; /* 添加一些左右内边距 */
+}
+
+.el-form-item {
+margin-bottom: 15px;
+}
+
+.el-select {
+width: 100%;
+}
+.generate-button {
+margin-top: 10px; /* 在小屏幕上给按钮一些上边距 */
+margin-right: 25px;
+}
+
+@media (max-width: 768px) {
+.selection-summary {
+  flex-direction: column;
+}
+
+.generate-button {
+  margin-left: 0;
+  margin-top: 15px;
+  align-self: flex-end; /* 在小屏幕上将按钮右对齐 */
 }
-::v-deep .messages {
-  margin-bottom: 0;
-  height: calc(100vh - 120px);
 }
-::v-deep .input-container {
-  max-width: 1200px;
+.selection-summary {
+background-color: #f2f2f2;
+border-radius: 4px;
+padding: 15px;
+margin: 15px;
+display: flex;
+justify-content: space-between;
+align-items: center;
+margin-right: 85px;
+border-radius:10px;
+}
+
+.summary-content {
+flex-grow: 1;
+}
+
+.summary-label {
+font-weight: bold;
+color: #7f7f7f;
+margin-right: 10px;
+}
+
+.summary-text {
+color: #606266;
+margin: 0;
+margin-top: 3px;
+font-size: 14px;
+}
+.title{
+width: 1200px;
+margin: 0 auto 15px;
+display: flex;
+align-items: center;
+
+}
+.title_info{
+cursor: pointer;
+color: #1296db;
+font-size: 14px;
+margin-left: 10px;
+}
+/* .chat-container .header ::v-deep .el-select .el-input--medium .el-input__inner{
+  border: none;
+}
+.chat-container .header ::v-deep .el-select .el-input--medium .el-input__suffix .el-input__suffix-inner{
+  display: none;
+} */
+.chat-container .content {
+    display: flex;
+    justify-content: flex-start;
+}
+
+.chat-container .content .left{
+    margin-left: 10px;
+    width: 330px;
+    height: 87vh;
+}
+.chat-item {
+    border-radius: 10px;
+    padding: 10px;
+    border-bottom: 1px solid #e8edf2;
+    background-color: #f5f5fa;
+    margin-bottom: 10px;
+  }
+  
+  .chat-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 5px;
+  }
+  
+  .avatar {
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+    margin-right: 10px;
+  }
+  
+  .user-info {
+    flex-grow: 1;
+  }
+  .user-info .info_header{
+    display: flex;
+    align-items: flex-end;
+  }
+  
+  .username {
+    font-weight: bold;
+  }
+  
+  .timestamp {
+    margin-left:13px;
+    font-size: 0.8em;
+    color: #888;
+  }
+  
+  .more-options {
+    cursor: pointer;
+  }
+  
+  .chat-message {
+    font-weight: bold;
+    margin-top: 8px;
+    margin-bottom: 10px;
+  }
+  
+  .chat-preview {
+    font-size: 0.9em;
+    color: #666;
+  }
+  
+  /* Adjust existing styles */
+  .left {
+    width: 300px; /* Increased width to accommodate the new layout */
+  }
+  
+  .chat-history {
+    width: 330px;
+    height: 100%;
+    overflow-y: auto;
+  }
+
+.messages {
+    width: 1180px; /* 设置消息列表宽度为屏幕宽度的 80% */
+    max-width: 90%; /* 限制最大宽度,避免超出视口 */
+    margin: 0 auto; /* 上下保持原有的 margin,左右自动调整以居中 */
+    overflow-y: auto; /* 如果消息太多,允许滚动 */
+    height: calc(100vh - 230px); /* 高度需要根据输入框和其他元素的高度调整 */
+    padding: 10px; /* 或你希望的内边距大小 */
+    margin-bottom: 120px;
+}
+/* 针对手机端的媒体查询 */
+@media screen and (max-width: 768px) {
+    .messages {
+        width: 100%;
+        max-width: 100%;
+        margin: 0;
+        margin-bottom: 120px;
+    }
+}
+.user-message {
+    display: flex;
+    flex-direction: column !important;
+    align-items: flex-end !important;
+  }
+  
+  .user-message .message-content {
+    order: 1;
+    text-align: right;
+  }
+  
+  .user-message .avatar {
+    order: 2;
+    margin-left: 10px;
+    margin-right: 0;
+  }
+  
+  .message-text {
+    margin-top: 5px;
+  }
+.messages li {
+    display: flex;
+    flex-direction: column;
+
+    align-items: flex-start; /* 这将确保所有子元素对齐到容器的顶部 */
+   /*  margin-bottom: 10px; */
+}
+
+.avatar {
+    width: 40px; /* 头像的尺寸 */
+    height: 40px;
+    border-radius: 50%; /* 圆形头像 */
+    /*background-color: #e9e0e0;  暂时用灰色代替,你可以用图片替换 */
+    align-self: start; /* 这将覆盖 flex 容器的 align-items 并将头像对齐到顶部 */
+    margin-right: 10px; /* 按需调整以在头像和消息之间提供空间 */
+}
+
+
+.user-avatar {
+
+    background-image: url('../../components/webAi/assets/用户.png') ;
+    background-position: center;
+    background-repeat: no-repeat;
+    background-size: cover;
+   /*  background-color: #93939d; */ /* 用户头像颜色 */
+}
+
+.bot-avatar {
+  
+    background-image: url('../../components/webAi/assets/机器人.png');
+    background-position: center;
+    background-repeat: no-repeat;
+    background-size: cover;
+    /* background-color: green; */ /* AI头像颜色 */
+}
+/* 针对手机端的媒体查询 */
+@media screen and (max-width: 768px) {
+    .user-avatar, .bot-avatar {
+        width: 30px;  /* 或其他更小的尺寸 */
+        height: 30px;
+    }
+}
+
+.message_name{
+    display: flex;
+    align-items: center;
+    padding-top: 5px;
+    padding-bottom: 5px;
+}
+.name{
+    margin-left: 8px;
+    /* margin-bottom: 5px; */
+}
+
+.message-content {
+    background-color: #f0f0f0; /* 消息背景 */
+    padding-left: 20px; /* 上下左右的内边距 */
+    padding-right: 20px; /* 上下左右的内边距 */
+    padding-top: 10px; /* 上下左右的内边距 */
+    padding-bottom: 0px; /* 上下左右的内边距 */
+    max-width: calc(90% - 10px); /* 计算头像和间距 */
+    border-radius: 7px; /* 边框圆角 */
+    /* margin-bottom: 10px; 和下一条消息之间的间距 */
+    /* 消息内容样式 */
+    display: flex;
+    flex-direction: column; /* 排列文本 */
+    margin-top: 0;
+}
+
+
+.message-text {
+    margin: 0;
+    white-space: normal; /* 确保文本会换行 */
+    word-wrap: break-word; /* 长单词或 URL 会被断开以适应容器宽度 */
+    overflow: auto;
+}
+.message-text ::v-deep p {
+    font-size: 14px !important;
+    margin: 5px 0 10px;
+    padding: 0;
+    font-size: inherit;
+    line-height: 22px;
+    font-weight: inherit;
+  }
+  .message-text ::v-deep ol{
+    padding-bottom: 10px ;
+
+  }
+  .message-text ::v-deep .source-section h3 {
+    margin: 20px 0 3px;
+    color: #333;
+    font-size: 1.1em;
+  }
+  .message-text ::v-deep ol li{
+    padding-top:3px ;
+  }
+  .message-text ::v-deep ol li a{
+    color: #1296db;
+  }
+.input-container {
+    position: absolute; /* 改为绝对定位 */
+    bottom: -115px; /* 距离底部的距离 */
+    left: 50%;
+    transform: translateX(-50%);
+    width: 90%; /* 设置宽度为弹窗宽度的90% */
+    max-width: 800px; /* 设置最大宽度 */
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px;
+    background: white;
+    border-radius: 10px;
+    box-shadow: 0px 2px 5px rgba(0,0,0,0.1);
+}
+
+textarea {
+    font-size: 16px;
+    width: 100%; /* 可以根据需要调整 */
+    padding: 10px;
+    border: 1px solid #ccc;
+    border-radius: 4px;
+    border: none;
+  outline: none;
+  resize: none;
+}
+
+.input-container textarea {
+    width: 100%; /* 输入框占据所有可用空间 */
+    padding: 10px;
+    margin-right: 10px; /* 与按钮的间隔 */
+    box-sizing: border-box;
+}
+
+.input-container button {
+    min-width: 80px; /* 例如,按钮的最小宽度为 100px */
+    white-space: nowrap; /* 防止文字折行 */
+    padding: 10px 20px;
+    cursor: pointer;
+}
+
+
+
+
+.sender-name {
+    font-weight: bold;
+    color: #333;
+    margin-left: -5px;
+}
+
+.btn {
+    font-size: 16px;
+    color: #ffffff;
+    border-radius: 5px;
+    border: none;
+    background-color: #3cbade;
+    padding: 10px 20px;  /* 添加内边距 */
+    width: auto;  /* 让宽度自适应内容 */
+}
+
+/* 针对手机端的媒体查询 */
+@media screen and (max-width: 768px) {
+    .btn {
+        font-size: 14px;  /* 稍微减小字体大小 */
+        padding: 8px 16px;  /* 减小内边距 */
+        
+        max-width: 80px;  /* 限制最大宽度 */
+        margin: 0 auto;  /* 居中显示 */
+    }
 }
 </style>

部分文件因为文件数量过多而无法显示