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