|
@@ -43,7 +43,7 @@
|
|
|
</li>
|
|
|
</ul>
|
|
|
</div>
|
|
|
- <div class="input-container">
|
|
|
+ <!-- <div class="input-container">
|
|
|
<textarea
|
|
|
v-model="userInput"
|
|
|
rows="4"
|
|
@@ -51,6 +51,58 @@
|
|
|
placeholder="发送消息..."
|
|
|
></textarea>
|
|
|
<button @click="sendMessage" class="btn">发送</button>
|
|
|
+ </div> -->
|
|
|
+ <div class="input-container">
|
|
|
+ <!-- 文件上传预览区 -->
|
|
|
+ <div class="upload-preview" v-if="uploadFiles.length > 0">
|
|
|
+ <div
|
|
|
+ v-for="(file, index) in uploadFiles"
|
|
|
+ :key="index"
|
|
|
+ class="preview-item"
|
|
|
+ >
|
|
|
+ <div class="preview-content">
|
|
|
+ <img
|
|
|
+ v-if="file.type.startsWith('image/')"
|
|
|
+ :src="file.preview"
|
|
|
+ class="preview-image"
|
|
|
+ alt="preview"
|
|
|
+ />
|
|
|
+ <div v-else class="file-info">
|
|
|
+ <i class="el-icon-document"></i>
|
|
|
+ <span>{{ file.name }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <i class="el-icon-close remove-file" @click="removeFile(index)"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 输入区域 -->
|
|
|
+ <div class="input-area">
|
|
|
+ <textarea
|
|
|
+ v-model="userInput"
|
|
|
+ rows="4"
|
|
|
+ @keydown="handleKeydown"
|
|
|
+ placeholder="发送消息..."
|
|
|
+ ></textarea>
|
|
|
+
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <div class="toolbar">
|
|
|
+ <div class="left-tools">
|
|
|
+ <input
|
|
|
+ type="file"
|
|
|
+ ref="fileInput"
|
|
|
+ multiple
|
|
|
+ @change="handleFileUpload"
|
|
|
+ accept="image/*,.pdf,.doc,.docx,.txt"
|
|
|
+ style="display: none"
|
|
|
+ />
|
|
|
+ <button class="tool-btn" @click="$refs.fileInput.click()">
|
|
|
+ <i class="el-icon-upload2"></i>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <button @click="sendMessage" class="btn">发送</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<!-- </div> -->
|
|
|
</div>
|
|
@@ -67,7 +119,7 @@ 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,get_default } from "@/api/knowledge";
|
|
|
+import { modelList, get_default } from "@/api/knowledge";
|
|
|
import axios from "axios";
|
|
|
const md = new MarkdownIt();
|
|
|
|
|
@@ -111,12 +163,12 @@ export default {
|
|
|
documentDirectory: "",
|
|
|
document: "",
|
|
|
},
|
|
|
- currentChat_id:'',
|
|
|
- phone:''
|
|
|
+ currentChat_id: "",
|
|
|
+ phone: "",
|
|
|
+ uploadFiles: [], // 存储上传的文件
|
|
|
};
|
|
|
},
|
|
|
mounted() {
|
|
|
- localStorage.removeItem("AIData")
|
|
|
this.checkAIData();
|
|
|
if (this.$route.name == "ai") {
|
|
|
/* 外部知识库 */
|
|
@@ -131,7 +183,7 @@ export default {
|
|
|
if (AIData && AIData.data) {
|
|
|
console.log(AIData);
|
|
|
this.wsUrl = AIData.data.url || "";
|
|
|
- this.phone=AIData.data.phone || ""
|
|
|
+ this.phone = AIData.data.phone || "";
|
|
|
this.tweaks = AIData.data.tweaks || {};
|
|
|
} else {
|
|
|
console.error("Invalid AIData structure");
|
|
@@ -149,9 +201,53 @@ export default {
|
|
|
this.tweaks = res.data.tweaks;
|
|
|
});
|
|
|
}
|
|
|
- this.init()
|
|
|
+ this.init();
|
|
|
},
|
|
|
methods: {
|
|
|
+ /* 上传文件 */
|
|
|
+ async handleFileUpload(event) {
|
|
|
+ const files = Array.from(event.target.files);
|
|
|
+
|
|
|
+ for (const file of files) {
|
|
|
+ try {
|
|
|
+ // 创建 FormData 对象
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("file", file);
|
|
|
+
|
|
|
+ // 发送上传请求
|
|
|
+ const response = await axios.post(
|
|
|
+ `${process.env.VUE_APP_BASE_API}/upload/file`,
|
|
|
+ formData,
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ "Content-Type": "multipart/form-data",
|
|
|
+ },
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 获取接口返回的文件URL
|
|
|
+ const fileUrl = response.data.data.fileUrl; // 根据实际接口返回格式调整
|
|
|
+ // 将文件信息添加到 uploadFiles 数组
|
|
|
+ this.uploadFiles.push({
|
|
|
+ name: file.name,
|
|
|
+ type: file.type,
|
|
|
+ preview: fileUrl, // 直接使用接口返回的URL作为预览
|
|
|
+ url: fileUrl,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error("文件上传失败:", error);
|
|
|
+ this.$message.error(`文件 ${file.name} 上传失败`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清空input以允许重复上传相同文件
|
|
|
+ event.target.value = "";
|
|
|
+ },
|
|
|
+
|
|
|
+ removeFile(index) {
|
|
|
+ this.uploadFiles.splice(index, 1);
|
|
|
+ },
|
|
|
+ /* */
|
|
|
checkAIData() {
|
|
|
const aiData = localStorage.getItem("AIData");
|
|
|
this.showLoginModal = !aiData;
|
|
@@ -284,10 +380,17 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
const chatId = this.getUuid();
|
|
|
- const wsUrl = `${process.env.VUE_APP_BASE_API}/chatbot/chat` //this.wsUrl;'http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false'
|
|
|
+ const wsUrl = `${process.env.VUE_APP_BASE_API}/chatbot/multimodal`; ///chatbot/chatthis.wsUrl;'http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false'
|
|
|
let message = this.userInput.trim();
|
|
|
- if (message) {
|
|
|
- // Markdown换行:在每个换行符之前添加两个空格
|
|
|
+ // 获取所有上传文件的URL并组合成字符串
|
|
|
+ // 修改:直接将图片URL组装成数组
|
|
|
+ const imageUrls = this.uploadFiles
|
|
|
+ .filter((file) => file.type.startsWith("image/"))
|
|
|
+ .map((file) => file.url); // 直接获取URL数组,不需要join
|
|
|
+
|
|
|
+ if (message || imageUrls.length>0) {
|
|
|
+ // 修改判断条件,允许只有图片没有文字的情况
|
|
|
+ // Markdown换行处理
|
|
|
message = message.replace(/(\r\n|\r|\n)/g, " \n");
|
|
|
|
|
|
this.messages.push({
|
|
@@ -297,8 +400,12 @@ export default {
|
|
|
user: "user",
|
|
|
done: true,
|
|
|
});
|
|
|
+
|
|
|
this.userInput = "";
|
|
|
- // 添加机器人的响应消息(初始为空)
|
|
|
+ // 清空上传的文件
|
|
|
+ this.uploadFiles = [];
|
|
|
+
|
|
|
+ // 添加机器人的响应消息
|
|
|
const botMessage = {
|
|
|
user: "bot",
|
|
|
messageType: "TEXT",
|
|
@@ -309,8 +416,13 @@ export default {
|
|
|
};
|
|
|
this.messages.push(botMessage);
|
|
|
|
|
|
- // 开始模拟思考
|
|
|
- const thinkingPromise = this.simulateThinking(botMessage);
|
|
|
+ // 开始思考动画
|
|
|
+ const thinkingController = new AbortController();
|
|
|
+ const thinkingPromise = this.simulateThinking(
|
|
|
+ botMessage,
|
|
|
+ thinkingController.signal
|
|
|
+ );
|
|
|
+
|
|
|
// 通过 HTTP 发送消息
|
|
|
try {
|
|
|
///send-message
|
|
@@ -324,9 +436,11 @@ export default {
|
|
|
message: message,
|
|
|
/* output_type: "chat",
|
|
|
input_type: "chat", */
|
|
|
- chat_config_id:'3',//this.currentChat_id,
|
|
|
- user_id: this.phone,//this.tweaks.Memory-KtRT7.session_id,
|
|
|
- session_id: this.session_id || ""/* {
|
|
|
+ chat_config_id: "17", //this.currentChat_id,
|
|
|
+ user_id: this.phone, //this.tweaks.Memory-KtRT7.session_id,
|
|
|
+ session_id: this.session_id || "",
|
|
|
+ image_url: imageUrls,
|
|
|
+ history_limit: 10 /* {
|
|
|
"ChatInput-U82Vu": {},
|
|
|
"Prompt-b8Z1E": {},
|
|
|
"OllamaModel-z9SVx": {},
|
|
@@ -340,10 +454,8 @@ export default {
|
|
|
} */,
|
|
|
}),
|
|
|
});
|
|
|
-
|
|
|
const result = await response.json();
|
|
|
this.session_id = result.data.session_id;
|
|
|
- console.log(result);
|
|
|
if (result.code !== 200) {
|
|
|
const errorText = await response.text(); // 获取错误文本
|
|
|
throw new Error(
|
|
@@ -351,14 +463,16 @@ export default {
|
|
|
);
|
|
|
}
|
|
|
// 等待思考动画完成
|
|
|
+ // 停止思考动画
|
|
|
+ thinkingController.abort();
|
|
|
await thinkingPromise;
|
|
|
if (this.$route.name !== "ai") {
|
|
|
// 提取 additional_input 数据
|
|
|
const additionalInput = result.data.milvus_ids;
|
|
|
|
|
|
// 处理字符串,提取 ID 值
|
|
|
- this.idArray = additionalInput
|
|
|
- /* .split("\n") // 按换行符分割
|
|
|
+ this.idArray = additionalInput;
|
|
|
+ /* .split("\n") // 按换行符分割
|
|
|
.map((line) => line.trim()) // 去除每行首尾空白
|
|
|
.filter((line) => line.startsWith("ID:")) // 只保留以 'ID:' 开头的行
|
|
|
.map((line) => line.substring(3).trim()); // 提取 'ID:' 后面的内容并去除空白 */
|
|
@@ -375,7 +489,9 @@ export default {
|
|
|
this.handleResponse(result, botMessage);
|
|
|
} catch (error) {
|
|
|
console.error("Error sending message:", error);
|
|
|
- // 如果发生错误,停止思考动画
|
|
|
+ // 停止思考动画
|
|
|
+ thinkingController.abort();
|
|
|
+ await thinkingPromise;
|
|
|
botMessage.done = true;
|
|
|
botMessage.message = "抱歉,发生了错误,请稍后再试。";
|
|
|
}
|
|
@@ -416,36 +532,46 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- async simulateThinking(message) {
|
|
|
+ async simulateThinking(message, signal) {
|
|
|
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);
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const updateDots = () => {
|
|
|
+ if (signal.aborted) {
|
|
|
+ clearInterval(intervalId);
|
|
|
+ this.isThinking = false;
|
|
|
+ this.thinkingDots = "";
|
|
|
+ message.message = "";
|
|
|
+ resolve();
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- await new Promise((resolve) => setTimeout(resolve, thinkingTime));
|
|
|
+ this.thinkingDots += ".";
|
|
|
+ if (this.thinkingDots.length > 3) {
|
|
|
+ this.thinkingDots = "";
|
|
|
+ }
|
|
|
+ message.message = "思考中" + this.thinkingDots;
|
|
|
+ };
|
|
|
|
|
|
- clearInterval(intervalId);
|
|
|
- this.isThinking = false;
|
|
|
- this.thinkingDots = "";
|
|
|
- message.message = "";
|
|
|
+ const intervalId = setInterval(updateDots, dotInterval);
|
|
|
+ signal.addEventListener("abort", () => {
|
|
|
+ clearInterval(intervalId);
|
|
|
+ this.isThinking = false;
|
|
|
+ this.thinkingDots = "";
|
|
|
+ message.message = "";
|
|
|
+ resolve();
|
|
|
+ });
|
|
|
+ });
|
|
|
},
|
|
|
|
|
|
async handleResponse(value, existingMessage) {
|
|
|
- const data = value.data.answer
|
|
|
+ const data = value.data.answer.answer;
|
|
|
|
|
|
existingMessage.messageType = data;
|
|
|
/* existingMessage.time = data.timestamp; */
|
|
|
- existingMessage.message = '';
|
|
|
-
|
|
|
+ existingMessage.message = "";
|
|
|
+
|
|
|
let mainText = data;
|
|
|
let sourceText = "";
|
|
|
|
|
@@ -738,7 +864,7 @@ export default {
|
|
|
overflow-y: auto; /* 如果消息太多,允许滚动 */
|
|
|
height: calc(100vh - 230px); /* 高度需要根据输入框和其他元素的高度调整 */
|
|
|
padding: 10px; /* 或你希望的内边距大小 */
|
|
|
- margin-bottom: 120px;
|
|
|
+ margin-bottom: 110px;
|
|
|
}
|
|
|
/* 针对手机端的媒体查询 */
|
|
|
@media screen and (max-width: 768px) {
|
|
@@ -875,8 +1001,9 @@ export default {
|
|
|
max-width: 800px; /* 设置最大宽度 */
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- padding: 10px;
|
|
|
+ align-items: flex-start;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 15px;
|
|
|
background: white;
|
|
|
border-radius: 10px;
|
|
|
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.1);
|
|
@@ -933,4 +1060,96 @@ textarea {
|
|
|
margin: 0 auto; /* 居中显示 */
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/* 新输入框 */
|
|
|
+.upload-preview {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ max-height: 100px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-item {
|
|
|
+ position: relative;
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-content {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-image {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.file-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 100%;
|
|
|
+ padding: 5px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.file-info span {
|
|
|
+ font-size: 12px;
|
|
|
+ word-break: break-word;
|
|
|
+ overflow: hidden;
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+}
|
|
|
+.remove-file {
|
|
|
+ position: absolute;
|
|
|
+ top: 2px;
|
|
|
+ right: 2px;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ color: white;
|
|
|
+ border-radius: 50%;
|
|
|
+ padding: 2px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.input-area {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.left-tools {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.tool-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 5px;
|
|
|
+ color: #666;
|
|
|
+ font-size: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.tool-btn:hover {
|
|
|
+ color: #3cbade;
|
|
|
+}
|
|
|
</style>
|