From b5d8173ee3798d638514ba45c2fabeb3733bf876 Mon Sep 17 00:00:00 2001
From: RC-CHN <67079377+RC-CHN@users.noreply.github.com>
Date: Sun, 20 Jul 2025 16:02:28 +0800
Subject: [PATCH] feat: add a file uplod button in WebChat page (#2136)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat:为webchat页面添加一个手动上传文件按钮(目前只处理图片)
* fix:上传后清空value,允许触发change事件以多次上传同一张图片
* perf:webchat页面消息发送后清空图片预览缩略图,维持与文本信息行为一致
* perf:将文件输入的值重置为空字符串以提升浏览器兼容性
* feat:webchat文件上传按钮支持多选文件上传
* fix:释放blob URL以防止内存泄漏
* perf:并行化sendMessage中的图片获取逻辑
---
dashboard/src/views/ChatPage.vue | 113 ++++++++++++++++++++-----------
1 file changed, 72 insertions(+), 41 deletions(-)
diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue
index 099cf09c0..ad75cbd3f 100644
--- a/dashboard/src/views/ChatPage.vue
+++ b/dashboard/src/views/ChatPage.vue
@@ -226,6 +226,9 @@
+
+
@@ -668,34 +671,44 @@ export default {
};
},
+ async processAndUploadImage(file) {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await axios.post('/api/chat/post_image', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+
+ const img = response.data.data.filename;
+ this.stagedImagesName.push(img); // Store just the filename
+ this.stagedImagesUrl.push(URL.createObjectURL(file)); // Create a blob URL for immediate display
+
+ } catch (err) {
+ console.error('Error uploading image:', err);
+ }
+ },
+
async handlePaste(event) {
console.log('Pasting image...');
const items = event.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile();
- const formData = new FormData();
- formData.append('file', file);
-
- try {
- const response = await axios.post('/api/chat/post_image', formData, {
- headers: {
- 'Content-Type': 'multipart/form-data'
- }
- });
-
- const img = response.data.data.filename;
- this.stagedImagesName.push(img); // Store just the filename
- this.stagedImagesUrl.push(URL.createObjectURL(file)); // Create a blob URL for immediate display
-
- } catch (err) {
- console.error('Error uploading image:', err);
- }
+ this.processAndUploadImage(file);
}
}
},
removeImage(index) {
+ // Revoke the blob URL to prevent memory leaks
+ const urlToRevoke = this.stagedImagesUrl[index];
+ if (urlToRevoke && urlToRevoke.startsWith('blob:')) {
+ URL.revokeObjectURL(urlToRevoke);
+ }
+
this.stagedImagesName.splice(index, 1);
this.stagedImagesUrl.splice(index, 1);
},
@@ -703,6 +716,21 @@ export default {
clearMessage() {
this.prompt = '';
},
+
+ triggerImageInput() {
+ this.$refs.imageInput.click();
+ },
+
+ handleFileSelect(event) {
+ const files = event.target.files;
+ if (files) {
+ for (const file of files) {
+ this.processAndUploadImage(file);
+ }
+ }
+ // Reset the input value to allow selecting the same file again
+ event.target.value = '';
+ },
getConversations() {
axios.get('/api/chat/conversations').then(response => {
this.conversations = response.data.data;
@@ -846,33 +874,42 @@ export default {
// URL is already updated in newConversation method
}
+ // 保存当前要发送的数据到临时变量
+ const promptToSend = this.prompt.trim();
+ const imageNamesToSend = [...this.stagedImagesName];
+ const audioNameToSend = this.stagedAudioUrl;
+
+ // 立即清空输入和附件预览
+ this.prompt = '';
+ this.stagedImagesName = [];
+ this.stagedImagesUrl = [];
+ this.stagedAudioUrl = "";
+
// Create a message object with actual URLs for display
const userMessage = {
type: 'user',
- message: this.prompt.trim(), // 使用 trim() 去除前后空格
+ message: promptToSend,
image_url: [],
audio_url: null
};
// Convert image filenames to blob URLs for display
- if (this.stagedImagesName.length > 0) {
- for (let i = 0; i < this.stagedImagesName.length; i++) {
- // If it's just a filename, get the blob URL
- if (!this.stagedImagesName[i].startsWith('blob:')) {
- const imgUrl = await this.getMediaFile(this.stagedImagesName[i]);
- userMessage.image_url.push(imgUrl);
- } else {
- userMessage.image_url.push(this.stagedImagesName[i]);
+ if (imageNamesToSend.length > 0) {
+ const imagePromises = imageNamesToSend.map(name => {
+ if (!name.startsWith('blob:')) {
+ return this.getMediaFile(name);
}
- }
+ return Promise.resolve(name);
+ });
+ userMessage.image_url = await Promise.all(imagePromises);
}
// Convert audio filename to blob URL for display
- if (this.stagedAudioUrl) {
- if (!this.stagedAudioUrl.startsWith('blob:')) {
- userMessage.audio_url = await this.getMediaFile(this.stagedAudioUrl);
+ if (audioNameToSend) {
+ if (!audioNameToSend.startsWith('blob:')) {
+ userMessage.audio_url = await this.getMediaFile(audioNameToSend);
} else {
- userMessage.audio_url = this.stagedAudioUrl;
+ userMessage.audio_url = audioNameToSend;
}
}
@@ -885,8 +922,6 @@ export default {
const selection = this.$refs.providerModelSelector?.getCurrentSelection();
const selectedProviderId = selection?.providerId || '';
const selectedModelName = selection?.modelName || '';
- let prompt = this.prompt.trim();
- this.prompt = ''; // 清空输入框
try {
const response = await fetch('/api/chat/send', {
@@ -896,10 +931,10 @@ export default {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
- message: prompt,
+ message: promptToSend,
conversation_id: this.currCid,
- image_url: this.stagedImagesName,
- audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [],
+ image_url: imageNamesToSend,
+ audio_url: audioNameToSend ? [audioNameToSend] : [],
selected_provider: selectedProviderId,
selected_model: selectedModelName
})
@@ -1003,11 +1038,7 @@ export default {
}
}
- // Clear input after successful send
- this.prompt = '';
- this.stagedImagesName = [];
- this.stagedImagesUrl = [];
- this.stagedAudioUrl = "";
+ // Input and attachments are already cleared
this.loadingChat = false;
// get the latest conversations