|
@@ -1,6 +1,59 @@
|
|
<template>
|
|
<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
|
|
<LoginModal
|
|
:visible.sync="showLoginModal"
|
|
:visible.sync="showLoginModal"
|
|
@close="showLoginModal = false"
|
|
@close="showLoginModal = false"
|
|
@@ -12,6 +65,11 @@
|
|
<script>
|
|
<script>
|
|
import ChatBox from "@/components/webAi/index.vue";
|
|
import ChatBox from "@/components/webAi/index.vue";
|
|
import LoginModal from "@/components/LoginModal";
|
|
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 {
|
|
export default {
|
|
components: {
|
|
components: {
|
|
@@ -21,32 +79,813 @@ export default {
|
|
data() {
|
|
data() {
|
|
return {
|
|
return {
|
|
showLoginModal: false,
|
|
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() {
|
|
mounted() {
|
|
this.checkAIData();
|
|
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: {
|
|
methods: {
|
|
checkAIData() {
|
|
checkAIData() {
|
|
- const aiData = localStorage.getItem('AIData');
|
|
|
|
|
|
+ const aiData = localStorage.getItem("AIData");
|
|
this.showLoginModal = !aiData;
|
|
this.showLoginModal = !aiData;
|
|
},
|
|
},
|
|
handleLoginSuccess() {
|
|
handleLoginSuccess() {
|
|
this.showLoginModal = false;
|
|
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>
|
|
</script>
|
|
<style scoped>
|
|
<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>
|
|
</style>
|