|
@@ -34,7 +34,26 @@
|
|
|
</div>
|
|
|
<!-- 使用 v-html 来渲染 Markdown 文本 -->
|
|
|
<div class="message-content">
|
|
|
- <div
|
|
|
+ <!-- 添加文件预览区域 -->
|
|
|
+ <div class="message-files" v-if="message.files && message.files.length > 0">
|
|
|
+ <div
|
|
|
+ v-for="(file, index) in message.files"
|
|
|
+ :key="index"
|
|
|
+ class="message-file-item"
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ v-if="file.type.startsWith('image/')"
|
|
|
+ :src="file.url"
|
|
|
+ class="message-image"
|
|
|
+ alt="uploaded image"
|
|
|
+ />
|
|
|
+ <div v-else class="message-document">
|
|
|
+ <i class="el-icon-document"></i>
|
|
|
+ <span>{{ file.name }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
class="message-text"
|
|
|
v-html="renderMarkdown(message.message)"
|
|
|
@click="handleLinkClick"
|
|
@@ -43,7 +62,7 @@
|
|
|
</li>
|
|
|
</ul>
|
|
|
</div>
|
|
|
- <div class="input-container">
|
|
|
+ <!-- <div class="input-container">
|
|
|
<textarea
|
|
|
v-model="userInput"
|
|
|
rows="4"
|
|
@@ -51,6 +70,79 @@
|
|
|
placeholder="发送消息..."
|
|
|
></textarea>
|
|
|
<button @click="sendMessage" class="btn">发送</button>
|
|
|
+ </div> -->
|
|
|
+ <div class="input-container">
|
|
|
+ <!-- 文件上传预览区 -->
|
|
|
+ <div
|
|
|
+ class="upload-preview"
|
|
|
+ v-if="uploadFiles.length > 0 || documents.length > 0"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-for="(file, index) in uploadFiles"
|
|
|
+ :key="'img-' + 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
|
|
|
+ v-for="(doc, index) in documents"
|
|
|
+ :key="'doc-' + index"
|
|
|
+ class="preview-item document-item"
|
|
|
+ >
|
|
|
+ <div class="preview-content">
|
|
|
+ <div class="file-info">
|
|
|
+ <i class="el-icon-document"></i>
|
|
|
+ <span>{{ doc.name }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <i
|
|
|
+ class="el-icon-close remove-file"
|
|
|
+ @click="removeDocument(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="playAudio" class="btn">播放音频</button> -->
|
|
|
+ <button @click="sendMessage" class="btn">发送</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<!-- </div> -->
|
|
|
</div>
|
|
@@ -68,7 +160,7 @@ import LoginModal from "@/components/LoginModal";
|
|
|
import MarkdownIt from "markdown-it";
|
|
|
import { pcInnerAi, getMinioURl } from "@/api/api";
|
|
|
import { modelList, get_default } from "@/api/knowledge";
|
|
|
-import axios from 'axios';
|
|
|
+import axios from "axios";
|
|
|
const md = new MarkdownIt();
|
|
|
|
|
|
export default {
|
|
@@ -112,6 +204,8 @@ export default {
|
|
|
document: "",
|
|
|
},
|
|
|
currentChat_id: "",
|
|
|
+ uploadFiles: [], // 存储上传的文件
|
|
|
+ documents: [], // 新增:专门存储文档类型文件的数组
|
|
|
};
|
|
|
},
|
|
|
mounted() {
|
|
@@ -147,6 +241,65 @@ export default {
|
|
|
this.init();
|
|
|
},
|
|
|
methods: {
|
|
|
+ /* 播放音频 */
|
|
|
+ playAudio(audioBase64) {
|
|
|
+ console.log(audioBase64);
|
|
|
+ // 创建音频元素
|
|
|
+ const audio = new Audio(audioBase64); // 播放音频
|
|
|
+ audio.play().catch((error) => {
|
|
|
+ console.error("Error playing audio:", error);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ async handleFileUpload(event) {
|
|
|
+ const files = Array.from(event.target.files);
|
|
|
+
|
|
|
+ for (const file of files) {
|
|
|
+ try {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append("file", file);
|
|
|
+
|
|
|
+ const response = await axios.post(
|
|
|
+ `${process.env.VUE_APP_BASE_AI_API}/upload/file`, //process.env.VUE_APP_BASE_API
|
|
|
+ formData,
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ "Content-Type": "multipart/form-data",
|
|
|
+ },
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ const fileUrl = response.data.data.fileUrl;
|
|
|
+ const fileInfo = {
|
|
|
+ name: file.name,
|
|
|
+ type: file.type,
|
|
|
+ preview: fileUrl,
|
|
|
+ url: fileUrl,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 根据文件类型分别存储
|
|
|
+ if (file.type.startsWith("image/")) {
|
|
|
+ this.uploadFiles.push(fileInfo);
|
|
|
+ } else {
|
|
|
+ // 文档类型:pdf, doc, docx, txt 等
|
|
|
+ this.documents.push(fileInfo);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("文件上传失败:", error);
|
|
|
+ this.$message.error(`文件 ${file.name} 上传失败`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ event.target.value = "";
|
|
|
+ },
|
|
|
+
|
|
|
+ removeFile(index) {
|
|
|
+ this.uploadFiles.splice(index, 1);
|
|
|
+ },
|
|
|
+ removeDocument(index) {
|
|
|
+ this.documents.splice(index, 1);
|
|
|
+ },
|
|
|
+
|
|
|
+ /* */
|
|
|
checkAIData() {
|
|
|
const aiData = localStorage.getItem("AIData");
|
|
|
this.showLoginModal = !aiData;
|
|
@@ -280,8 +433,28 @@ export default {
|
|
|
}
|
|
|
const chatId = this.getUuid();
|
|
|
let message = this.userInput.trim();
|
|
|
+ const imageUrls = this.uploadFiles
|
|
|
+ .filter((file) => file.type.startsWith("image/"))
|
|
|
+ .map((file) => file.url);
|
|
|
+
|
|
|
+ // 获取文档 URL
|
|
|
+ const documentUrls = this.documents.map((doc) => doc.url);
|
|
|
if (!message) return;
|
|
|
|
|
|
+ // 添加文件信息到消息中
|
|
|
+ const files = [
|
|
|
+ ...this.uploadFiles.map(file => ({
|
|
|
+ type: file.type,
|
|
|
+ url: file.url,
|
|
|
+ name: file.name
|
|
|
+ })),
|
|
|
+ ...this.documents.map(doc => ({
|
|
|
+ type: 'document',
|
|
|
+ url: doc.url,
|
|
|
+ name: doc.name
|
|
|
+ }))
|
|
|
+ ];
|
|
|
+
|
|
|
// 添加用户消息
|
|
|
this.messages.push({
|
|
|
time: Date.now(),
|
|
@@ -289,9 +462,11 @@ export default {
|
|
|
messageType: "TEXT",
|
|
|
user: "user",
|
|
|
done: true,
|
|
|
+ files: files // 添加文件数组
|
|
|
});
|
|
|
this.userInput = "";
|
|
|
-
|
|
|
+ this.uploadFiles = [];
|
|
|
+ this.documents = [];
|
|
|
// 添加机器人的响应消息
|
|
|
const botMessage = {
|
|
|
user: "bot",
|
|
@@ -312,13 +487,18 @@ export default {
|
|
|
|
|
|
try {
|
|
|
// 直接使用 axios 而不是 this.$axios
|
|
|
- const response = await axios.post(`${process.env.VUE_APP_BASE_AI_API}/chatbot/chat`, {
|
|
|
- message: message,
|
|
|
- chat_config_id: this.currentChat_id,
|
|
|
- user_id: this.$store.state.user.id,
|
|
|
- session_id: this.session_id || "",
|
|
|
- source: "pc",
|
|
|
- });
|
|
|
+ const response = await axios.post(
|
|
|
+ `${process.env.VUE_APP_BASE_AI_API}/chatbot/multimodal`,
|
|
|
+ {
|
|
|
+ message: message,
|
|
|
+ chat_config_id: this.currentChat_id,
|
|
|
+ user_id: this.$store.state.user.id,
|
|
|
+ session_id: this.session_id || "",
|
|
|
+ source: "pc",
|
|
|
+ image_urls: imageUrls,
|
|
|
+ documents: documentUrls, // 添加文档 URLs
|
|
|
+ }
|
|
|
+ );
|
|
|
|
|
|
// 停止思考动画
|
|
|
thinkingController.abort();
|
|
@@ -327,14 +507,19 @@ export default {
|
|
|
if (response.status === 200) {
|
|
|
const result = response.data;
|
|
|
this.session_id = result.data.session_id;
|
|
|
-
|
|
|
+
|
|
|
if (this.$route.name !== "ai") {
|
|
|
const additionalInput = result.data.milvus_ids;
|
|
|
this.idArray = additionalInput;
|
|
|
const idObject = { ids: this.idArray };
|
|
|
await this.getMinioUrls(idObject);
|
|
|
}
|
|
|
-
|
|
|
+ if (
|
|
|
+ response.data.data.audio_info &&
|
|
|
+ response.data.data.audio_info.audio
|
|
|
+ ) {
|
|
|
+ this.playAudio(response.data.data.audio_info.audio);
|
|
|
+ }
|
|
|
this.handleResponse(result, botMessage);
|
|
|
} else {
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
@@ -486,7 +671,7 @@ export default {
|
|
|
|
|
|
const renderedHtml = md.render(rawMarkdown);
|
|
|
|
|
|
- // 使用 setTimeout 来确保 DOM 更新后再添加事件监听器
|
|
|
+ // 使用 setTimeout ���确保 DOM 更新后再添加事件监听器
|
|
|
setTimeout(() => {
|
|
|
const links = document.querySelectorAll(".custom-link");
|
|
|
links.forEach((link) => {
|
|
@@ -851,7 +1036,8 @@ export default {
|
|
|
max-width: 800px; /* 设置最大宽度 */
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
+ align-items: flex-start;
|
|
|
+ flex-direction: column;
|
|
|
padding: 10px;
|
|
|
background: #fdfdfd;
|
|
|
border-radius: 10px;
|
|
@@ -910,4 +1096,161 @@ textarea {
|
|
|
margin: 0 auto; /* 居中显示 */
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/* 新输入框 */
|
|
|
+.upload-preview {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10px;
|
|
|
+ max-height: 100px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-item {
|
|
|
+ position: relative;
|
|
|
+ width: 60px;
|
|
|
+ height: 20px;
|
|
|
+ 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;
|
|
|
+}
|
|
|
+
|
|
|
+.document-item {
|
|
|
+ width: 120px; /* 文档预览项可以设置得更宽一些 */
|
|
|
+ height: 60px;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+}
|
|
|
+
|
|
|
+.document-item .file-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.document-item .file-info i {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.document-item .file-info span {
|
|
|
+ font-size: 12px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.message-files {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-file-item {
|
|
|
+ max-width: 200px;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.message-image {
|
|
|
+ width: 100%;
|
|
|
+ height: auto;
|
|
|
+ max-height: 200px;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-document {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 8px;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-document i {
|
|
|
+ font-size: 20px;
|
|
|
+ color: #666;
|
|
|
+ margin-right: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-document span {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #333;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
</style>
|