diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json
index 5b119f6..1ecbabf 100644
--- a/src/mcp_feedback_enhanced/web/locales/en/translation.json
+++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json
@@ -320,6 +320,7 @@
"settings": {
"title": "Image Settings",
"sizeLimit": "Image Size Limit",
+ "sizeLimitDesc": "Set the maximum file size limit for uploaded images",
"sizeLimitOptions": {
"unlimited": "Unlimited",
"1mb": "1MB",
diff --git a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
index 7fe305e..936476a 100644
--- a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
+++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
@@ -320,6 +320,7 @@
"settings": {
"title": "图片设置",
"sizeLimit": "图片大小限制",
+ "sizeLimitDesc": "设定上传图片的最大文件大小限制",
"sizeLimitOptions": {
"unlimited": "无限制",
"1mb": "1MB",
diff --git a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
index ef5786c..0ada157 100644
--- a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
+++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
@@ -325,6 +325,7 @@
"settings": {
"title": "圖片設定",
"sizeLimit": "圖片大小限制",
+ "sizeLimitDesc": "設定上傳圖片的最大檔案大小限制",
"sizeLimitOptions": {
"unlimited": "無限制",
"1mb": "1MB",
diff --git a/src/mcp_feedback_enhanced/web/static/css/styles.css b/src/mcp_feedback_enhanced/web/static/css/styles.css
index f297ba1..8ab7189 100644
--- a/src/mcp_feedback_enhanced/web/static/css/styles.css
+++ b/src/mcp_feedback_enhanced/web/static/css/styles.css
@@ -554,6 +554,100 @@ body {
line-height: 1.4;
}
+.settings-card .setting-warning {
+ font-size: 12px;
+ color: var(--warning-color);
+ margin-top: 4px;
+ font-weight: 500;
+}
+
+/* 圖片大小限制選擇器樣式 */
+.image-size-limit-selector {
+ display: flex;
+ align-items: center;
+}
+
+.image-size-limit-select {
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ color: var(--text-primary);
+ padding: 8px 12px;
+ font-size: 14px;
+ min-width: 120px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.image-size-limit-select:hover {
+ border-color: var(--accent-color);
+ background: var(--bg-tertiary);
+}
+
+.image-size-limit-select:focus {
+ outline: none;
+ border-color: var(--accent-color);
+ box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
+}
+
+/* Base64 切換器容器樣式 */
+.base64-toggle-container {
+ display: flex;
+ align-items: center;
+}
+
+/* 現代化切換開關樣式 */
+.toggle-switch {
+ position: relative;
+ display: inline-block;
+ width: 50px;
+ height: 24px;
+ cursor: pointer;
+}
+
+.toggle-input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.toggle-slider {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: var(--border-color);
+ border-radius: 24px;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+}
+
+.toggle-slider:before {
+ content: "";
+ position: absolute;
+ height: 18px;
+ width: 18px;
+ left: 3px;
+ background: white;
+ border-radius: 50%;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.toggle-input:checked + .toggle-slider {
+ background: var(--accent-color);
+}
+
+.toggle-input:checked + .toggle-slider:before {
+ transform: translateX(26px);
+}
+
+.toggle-switch:hover .toggle-slider {
+ box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
+}
+
/* 佈局模式選擇器 */
.layout-mode-selector {
display: flex;
@@ -1102,106 +1196,9 @@ body {
transform: scale(1.1);
}
-/* 圖片設定區域樣式 */
-.image-settings-details {
- background: var(--bg-tertiary);
- border: 1px solid var(--border-color);
- border-radius: 6px;
- margin-bottom: 8px;
- overflow: hidden;
- transition: all 0.3s ease;
-}
-.image-settings-details:hover {
- border-color: rgba(0, 122, 204, 0.5);
- box-shadow: 0 2px 8px rgba(0, 122, 204, 0.1);
-}
-.image-settings-summary {
- padding: 8px 12px;
- background: var(--bg-secondary);
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- color: var(--text-primary);
- border: none;
- outline: none;
- transition: all 0.3s ease;
- list-style: none;
- display: flex;
- align-items: center;
- justify-content: space-between;
- position: relative;
- user-select: none;
-}
-.image-settings-summary:hover {
- background: var(--surface-color);
- color: var(--accent-color);
-}
-
-.image-settings-summary::-webkit-details-marker {
- display: none;
-}
-
-/* 箭頭指示器 */
-.image-settings-summary::after {
- content: '▼';
- font-size: 10px;
- color: var(--text-secondary);
- transition: all 0.3s ease;
- transform-origin: center;
- display: inline-block;
- margin-left: auto;
-}
-
-.image-settings-summary:hover::after {
- color: var(--accent-color);
-}
-
-/* 展開狀態的箭頭 */
-.image-settings-details[open] .image-settings-summary::after {
- transform: rotate(180deg);
- color: var(--accent-color);
-}
-
-/* 展開狀態的 summary 樣式 */
-.image-settings-details[open] .image-settings-summary {
- background: var(--surface-color);
- color: var(--accent-color);
- border-bottom: 1px solid var(--border-color);
-}
-
-.image-settings-content {
- padding: 12px;
- background: var(--bg-tertiary);
- animation: slideDown 0.3s ease-out;
-}
-
-/* 展開內容的滑入動畫 */
-@keyframes slideDown {
- from {
- opacity: 0;
- transform: translateY(-10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-/* 圖片設定行 */
-.image-setting-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 8px;
- gap: 12px;
-}
-
-.image-setting-row:last-child {
- margin-bottom: 0;
-}
.image-setting-label {
font-size: 12px;
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js
new file mode 100644
index 0000000..a7b3159
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/file-upload-manager.js
@@ -0,0 +1,543 @@
+/**
+ * 現代化檔案上傳管理器
+ * 使用事件委託模式,避免重複事件監聽器問題
+ */
+
+(function() {
+ 'use strict';
+
+ // 確保命名空間存在
+ if (!window.MCPFeedback) {
+ window.MCPFeedback = {};
+ }
+
+ /**
+ * 檔案上傳管理器建構函數
+ */
+ function FileUploadManager(options) {
+ options = options || {};
+
+ // 配置選項
+ this.maxFileSize = options.maxFileSize || 0; // 0 表示無限制
+ this.enableBase64Detail = options.enableBase64Detail || false;
+ this.acceptedTypes = options.acceptedTypes || 'image/*';
+ this.maxFiles = options.maxFiles || 10;
+
+ // 狀態管理
+ this.files = [];
+ this.isInitialized = false;
+ this.debounceTimeout = null;
+ this.lastClickTime = 0;
+ this.isProcessingClick = false;
+ this.lastClickTime = 0;
+
+ // 事件回調
+ this.onFileAdd = options.onFileAdd || null;
+ this.onFileRemove = options.onFileRemove || null;
+ this.onSettingsChange = options.onSettingsChange || null;
+
+ // 綁定方法上下文
+ this.handleDelegatedEvent = this.handleDelegatedEvent.bind(this);
+ this.handleGlobalPaste = this.handleGlobalPaste.bind(this);
+
+ console.log('📁 FileUploadManager 初始化完成');
+ }
+
+ /**
+ * 初始化檔案上傳管理器
+ */
+ FileUploadManager.prototype.initialize = function() {
+ if (this.isInitialized) {
+ console.warn('⚠️ FileUploadManager 已經初始化過了');
+ return;
+ }
+
+ this.setupEventDelegation();
+ this.setupGlobalPasteHandler();
+ this.isInitialized = true;
+
+ console.log('✅ FileUploadManager 事件委託設置完成');
+ };
+
+ /**
+ * 設置事件委託
+ * 使用單一事件監聽器處理所有檔案上傳相關事件
+ */
+ FileUploadManager.prototype.setupEventDelegation = function() {
+ // 移除舊的事件監聽器
+ document.removeEventListener('click', this.handleDelegatedEvent);
+ document.removeEventListener('dragover', this.handleDelegatedEvent);
+ document.removeEventListener('dragleave', this.handleDelegatedEvent);
+ document.removeEventListener('drop', this.handleDelegatedEvent);
+ document.removeEventListener('change', this.handleDelegatedEvent);
+
+ // 設置新的事件委託
+ document.addEventListener('click', this.handleDelegatedEvent);
+ document.addEventListener('dragover', this.handleDelegatedEvent);
+ document.addEventListener('dragleave', this.handleDelegatedEvent);
+ document.addEventListener('drop', this.handleDelegatedEvent);
+ document.addEventListener('change', this.handleDelegatedEvent);
+ };
+
+ /**
+ * 處理委託事件
+ */
+ FileUploadManager.prototype.handleDelegatedEvent = function(event) {
+ const target = event.target;
+
+ // 處理檔案移除按鈕點擊
+ const removeBtn = target.closest('.image-remove-btn');
+ if (removeBtn) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.handleRemoveFile(removeBtn);
+ return;
+ }
+
+ // 處理檔案輸入變更
+ if (target.type === 'file' && event.type === 'change') {
+ this.handleFileInputChange(target, event);
+ return;
+ }
+
+ // 處理上傳區域事件 - 只處理直接點擊上傳區域的情況
+ const uploadArea = target.closest('.image-upload-area');
+ if (uploadArea && event.type === 'click') {
+ // 確保不是點擊 input 元素本身
+ if (target.type === 'file') {
+ return;
+ }
+
+ // 確保不是點擊預覽圖片或移除按鈕
+ if (target.closest('.image-preview-item') || target.closest('.image-remove-btn')) {
+ return;
+ }
+
+ this.handleUploadAreaClick(uploadArea, event);
+ return;
+ }
+
+ // 處理拖放事件
+ if (uploadArea && (event.type === 'dragover' || event.type === 'dragleave' || event.type === 'drop')) {
+ switch (event.type) {
+ case 'dragover':
+ this.handleDragOver(uploadArea, event);
+ break;
+ case 'dragleave':
+ this.handleDragLeave(uploadArea, event);
+ break;
+ case 'drop':
+ this.handleDrop(uploadArea, event);
+ break;
+ }
+ }
+ };
+
+ /**
+ * 處理上傳區域點擊(使用防抖機制)
+ */
+ FileUploadManager.prototype.handleUploadAreaClick = function(uploadArea, event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // 強力防抖機制 - 防止無限循環
+ const now = Date.now();
+ if (this.lastClickTime && (now - this.lastClickTime) < 500) {
+ console.log('🚫 防抖:忽略重複點擊,間隔:', now - this.lastClickTime, 'ms');
+ return;
+ }
+ this.lastClickTime = now;
+
+ // 如果已經有待處理的點擊,忽略新的點擊
+ if (this.isProcessingClick) {
+ console.log('🚫 正在處理點擊,忽略新的點擊');
+ return;
+ }
+
+ this.isProcessingClick = true;
+
+ const fileInput = uploadArea.querySelector('input[type="file"]');
+ if (fileInput) {
+ console.log('🖱️ 觸發檔案選擇:', fileInput.id);
+
+ // 重置 input 值以確保可以重複選擇同一檔案
+ fileInput.value = '';
+
+ // 使用 setTimeout 確保在下一個事件循環中執行,避免事件冒泡問題
+ const self = this;
+ setTimeout(function() {
+ try {
+ fileInput.click();
+ console.log('✅ 檔案選擇對話框已觸發');
+ } catch (error) {
+ console.error('❌ 檔案選擇對話框觸發失敗:', error);
+ } finally {
+ // 重置處理狀態
+ setTimeout(function() {
+ self.isProcessingClick = false;
+ }, 100);
+ }
+ }, 50);
+ } else {
+ this.isProcessingClick = false;
+ }
+ };
+
+ /**
+ * 處理檔案輸入變更
+ */
+ FileUploadManager.prototype.handleFileInputChange = function(fileInput, event) {
+ const files = event.target.files;
+ if (files && files.length > 0) {
+ console.log('📁 檔案選擇變更:', files.length, '個檔案');
+ this.processFiles(Array.from(files), fileInput);
+ }
+ };
+
+ /**
+ * 處理拖放事件
+ */
+ FileUploadManager.prototype.handleDragOver = function(uploadArea, event) {
+ event.preventDefault();
+ uploadArea.classList.add('dragover');
+ };
+
+ FileUploadManager.prototype.handleDragLeave = function(uploadArea, event) {
+ event.preventDefault();
+ // 只有當滑鼠真正離開上傳區域時才移除樣式
+ if (!uploadArea.contains(event.relatedTarget)) {
+ uploadArea.classList.remove('dragover');
+ }
+ };
+
+ FileUploadManager.prototype.handleDrop = function(uploadArea, event) {
+ event.preventDefault();
+ uploadArea.classList.remove('dragover');
+
+ const files = event.dataTransfer.files;
+ if (files && files.length > 0) {
+ console.log('📁 拖放檔案:', files.length, '個檔案');
+ this.processFiles(Array.from(files), uploadArea.querySelector('input[type="file"]'));
+ }
+ };
+
+ /**
+ * 處理檔案移除
+ */
+ FileUploadManager.prototype.handleRemoveFile = function(removeBtn) {
+ const index = parseInt(removeBtn.dataset.index);
+ if (!isNaN(index) && index >= 0 && index < this.files.length) {
+ const removedFile = this.files.splice(index, 1)[0];
+ console.log('🗑️ 移除檔案:', removedFile.name);
+
+ this.updateAllPreviews();
+
+ if (this.onFileRemove) {
+ this.onFileRemove(removedFile, index);
+ }
+ }
+ };
+
+ /**
+ * 設置全域剪貼板貼上處理
+ */
+ FileUploadManager.prototype.setupGlobalPasteHandler = function() {
+ document.removeEventListener('paste', this.handleGlobalPaste);
+ document.addEventListener('paste', this.handleGlobalPaste);
+ };
+
+ /**
+ * 處理全域剪貼板貼上
+ */
+ FileUploadManager.prototype.handleGlobalPaste = function(event) {
+ const items = event.clipboardData.items;
+ const imageFiles = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (item.type.indexOf('image') !== -1) {
+ const file = item.getAsFile();
+ if (file) {
+ imageFiles.push(file);
+ }
+ }
+ }
+
+ if (imageFiles.length > 0) {
+ event.preventDefault();
+ console.log('📋 剪貼板貼上圖片:', imageFiles.length, '個檔案');
+ this.processFiles(imageFiles);
+ }
+ };
+
+ /**
+ * 處理檔案
+ */
+ FileUploadManager.prototype.processFiles = function(files, sourceInput) {
+ const validFiles = [];
+
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+
+ // 檢查檔案類型
+ if (!file.type.startsWith('image/')) {
+ console.warn('⚠️ 跳過非圖片檔案:', file.name);
+ continue;
+ }
+
+ // 檢查檔案大小
+ if (this.maxFileSize > 0 && file.size > this.maxFileSize) {
+ const sizeLimit = this.formatFileSize(this.maxFileSize);
+ console.warn('⚠️ 檔案過大:', file.name, '超過限制', sizeLimit);
+ this.showMessage('圖片大小超過限制 (' + sizeLimit + '): ' + file.name, 'warning');
+ continue;
+ }
+
+ // 檢查檔案數量限制
+ if (this.files.length + validFiles.length >= this.maxFiles) {
+ console.warn('⚠️ 檔案數量超過限制:', this.maxFiles);
+ this.showMessage('最多只能上傳 ' + this.maxFiles + ' 個檔案', 'warning');
+ break;
+ }
+
+ validFiles.push(file);
+ }
+
+ // 處理有效檔案
+ if (validFiles.length > 0) {
+ this.addFiles(validFiles);
+ }
+ };
+
+ /**
+ * 添加檔案到列表
+ */
+ FileUploadManager.prototype.addFiles = function(files) {
+ const promises = files.map(file => this.fileToBase64(file));
+
+ const self = this;
+ Promise.all(promises)
+ .then(function(base64Results) {
+ base64Results.forEach(function(base64, index) {
+ const file = files[index];
+ const fileData = {
+ name: file.name,
+ size: file.size,
+ type: file.type,
+ data: base64,
+ timestamp: Date.now()
+ };
+
+ self.files.push(fileData);
+ console.log('✅ 檔案已添加:', file.name);
+
+ if (self.onFileAdd) {
+ self.onFileAdd(fileData);
+ }
+ });
+
+ self.updateAllPreviews();
+ })
+ .catch(function(error) {
+ console.error('❌ 檔案處理失敗:', error);
+ self.showMessage('檔案處理失敗,請重試', 'error');
+ });
+ };
+
+ /**
+ * 將檔案轉換為 Base64
+ */
+ FileUploadManager.prototype.fileToBase64 = function(file) {
+ return new Promise(function(resolve, reject) {
+ const reader = new FileReader();
+ reader.onload = function() {
+ resolve(reader.result.split(',')[1]);
+ };
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+ };
+
+ /**
+ * 更新所有預覽容器
+ */
+ FileUploadManager.prototype.updateAllPreviews = function() {
+ const previewContainers = document.querySelectorAll('.image-preview-container');
+ const self = this;
+
+ previewContainers.forEach(function(container) {
+ self.updatePreviewContainer(container);
+ });
+
+ this.updateFileCount();
+ console.log('🖼️ 已更新', previewContainers.length, '個預覽容器');
+ };
+
+ /**
+ * 更新單個預覽容器
+ */
+ FileUploadManager.prototype.updatePreviewContainer = function(container) {
+ container.innerHTML = '';
+
+ const self = this;
+ this.files.forEach(function(file, index) {
+ const previewElement = self.createPreviewElement(file, index);
+ container.appendChild(previewElement);
+ });
+ };
+
+ /**
+ * 創建預覽元素
+ */
+ FileUploadManager.prototype.createPreviewElement = function(file, index) {
+ const preview = document.createElement('div');
+ preview.className = 'image-preview-item';
+
+ // 圖片元素
+ const img = document.createElement('img');
+ img.src = 'data:' + file.type + ';base64,' + file.data;
+ img.alt = file.name;
+ img.title = file.name + ' (' + this.formatFileSize(file.size) + ')';
+
+ // 檔案資訊
+ const info = document.createElement('div');
+ info.className = 'image-info';
+
+ const name = document.createElement('div');
+ name.className = 'image-name';
+ name.textContent = file.name;
+
+ const size = document.createElement('div');
+ size.className = 'image-size';
+ size.textContent = this.formatFileSize(file.size);
+
+ // 移除按鈕
+ const removeBtn = document.createElement('button');
+ removeBtn.className = 'image-remove-btn';
+ removeBtn.textContent = '×';
+ removeBtn.title = '移除圖片';
+ removeBtn.dataset.index = index;
+ removeBtn.setAttribute('aria-label', '移除圖片 ' + file.name);
+
+ // 組裝元素
+ info.appendChild(name);
+ info.appendChild(size);
+ preview.appendChild(img);
+ preview.appendChild(info);
+ preview.appendChild(removeBtn);
+
+ return preview;
+ };
+
+ /**
+ * 更新檔案計數顯示
+ */
+ FileUploadManager.prototype.updateFileCount = function() {
+ const count = this.files.length;
+ const countElements = document.querySelectorAll('.image-count');
+
+ countElements.forEach(function(element) {
+ element.textContent = count > 0 ? '(' + count + ')' : '';
+ });
+
+ // 更新上傳區域狀態
+ const uploadAreas = document.querySelectorAll('.image-upload-area');
+ uploadAreas.forEach(function(area) {
+ if (count > 0) {
+ area.classList.add('has-images');
+ } else {
+ area.classList.remove('has-images');
+ }
+ });
+ };
+
+ /**
+ * 格式化檔案大小
+ */
+ FileUploadManager.prototype.formatFileSize = function(bytes) {
+ if (bytes === 0) return '0 Bytes';
+
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ /**
+ * 顯示訊息
+ */
+ FileUploadManager.prototype.showMessage = function(message, type) {
+ // 使用現有的 Utils.showMessage 如果可用
+ if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
+ const messageType = type === 'warning' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_WARNING :
+ type === 'error' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_ERROR :
+ window.MCPFeedback.Utils.CONSTANTS.MESSAGE_INFO;
+ window.MCPFeedback.Utils.showMessage(message, messageType);
+ } else {
+ // 後備方案
+ console.log('[' + type.toUpperCase() + ']', message);
+ alert(message);
+ }
+ };
+
+ /**
+ * 更新設定
+ */
+ FileUploadManager.prototype.updateSettings = function(settings) {
+ this.maxFileSize = settings.imageSizeLimit || 0;
+ this.enableBase64Detail = settings.enableBase64Detail || false;
+
+ console.log('⚙️ FileUploadManager 設定已更新:', {
+ maxFileSize: this.maxFileSize,
+ enableBase64Detail: this.enableBase64Detail
+ });
+ };
+
+ /**
+ * 獲取檔案列表
+ */
+ FileUploadManager.prototype.getFiles = function() {
+ return this.files.slice(); // 返回副本
+ };
+
+ /**
+ * 清空所有檔案
+ */
+ FileUploadManager.prototype.clearFiles = function() {
+ this.files = [];
+ this.updateAllPreviews();
+ console.log('🗑️ 已清空所有檔案');
+ };
+
+ /**
+ * 清理資源
+ */
+ FileUploadManager.prototype.cleanup = function() {
+ // 移除事件監聽器
+ document.removeEventListener('click', this.handleDelegatedEvent);
+ document.removeEventListener('dragover', this.handleDelegatedEvent);
+ document.removeEventListener('dragleave', this.handleDelegatedEvent);
+ document.removeEventListener('drop', this.handleDelegatedEvent);
+ document.removeEventListener('change', this.handleDelegatedEvent);
+ document.removeEventListener('paste', this.handleGlobalPaste);
+
+ // 清理防抖計時器
+ if (this.debounceTimeout) {
+ clearTimeout(this.debounceTimeout);
+ this.debounceTimeout = null;
+ }
+
+ // 清空檔案
+ this.clearFiles();
+
+ this.isInitialized = false;
+ console.log('🧹 FileUploadManager 資源已清理');
+ };
+
+ // 將 FileUploadManager 加入命名空間
+ window.MCPFeedback.FileUploadManager = FileUploadManager;
+
+ console.log('✅ FileUploadManager 模組載入完成');
+
+})();
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js b/src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js
index 5bb58cc..2d3571c 100644
--- a/src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js
@@ -17,30 +17,38 @@
*/
function ImageHandler(options) {
options = options || {};
-
- this.images = [];
+
this.imageSizeLimit = options.imageSizeLimit || 0;
this.enableBase64Detail = options.enableBase64Detail || false;
this.layoutMode = options.layoutMode || 'combined-vertical';
this.currentImagePrefix = '';
-
- // UI 元素
- this.imageInput = null;
- this.imageUploadArea = null;
- this.imagePreviewContainer = null;
+
+ // UI 元素(保留用於設定同步)
this.imageSizeLimitSelect = null;
this.enableBase64DetailCheckbox = null;
-
- // 事件處理器
- this.imageChangeHandler = null;
- this.imageClickHandler = null;
- this.imageDragOverHandler = null;
- this.imageDragLeaveHandler = null;
- this.imageDropHandler = null;
- this.pasteHandler = null;
-
+
// 回調函數
this.onSettingsChange = options.onSettingsChange || null;
+
+ // 創建檔案上傳管理器
+ const self = this;
+ this.fileUploadManager = new window.MCPFeedback.FileUploadManager({
+ maxFileSize: this.imageSizeLimit,
+ enableBase64Detail: this.enableBase64Detail,
+ onFileAdd: function(fileData) {
+ console.log('📁 檔案已添加:', fileData.name);
+ },
+ onFileRemove: function(fileData, index) {
+ console.log('🗑️ 檔案已移除:', fileData.name);
+ },
+ onSettingsChange: function() {
+ if (self.onSettingsChange) {
+ self.onSettingsChange();
+ }
+ }
+ });
+
+ console.log('🖼️ ImageHandler 建構函數初始化完成');
}
/**
@@ -48,148 +56,46 @@
*/
ImageHandler.prototype.init = function() {
console.log('🖼️ 開始初始化圖片處理功能...');
-
- this.initImageElements();
- this.setupImageEventListeners();
- this.setupGlobalPasteHandler();
-
+
+ // 初始化設定元素
+ this.initImageSettingsElements();
+
+ // 初始化檔案上傳管理器
+ this.fileUploadManager.initialize();
+
console.log('✅ 圖片處理功能初始化完成');
};
/**
* 動態初始化圖片相關元素
*/
- ImageHandler.prototype.initImageElements = function() {
- const prefix = this.layoutMode && this.layoutMode.startsWith('combined') ? 'combined' : 'feedback';
-
- console.log('🖼️ 初始化圖片元素,使用前綴: ' + prefix);
-
- this.imageInput = Utils.safeQuerySelector('#' + prefix + 'ImageInput') ||
- Utils.safeQuerySelector('#imageInput');
- this.imageUploadArea = Utils.safeQuerySelector('#' + prefix + 'ImageUploadArea') ||
- Utils.safeQuerySelector('#imageUploadArea');
- this.imagePreviewContainer = Utils.safeQuerySelector('#' + prefix + 'ImagePreviewContainer') ||
- Utils.safeQuerySelector('#imagePreviewContainer');
- this.imageSizeLimitSelect = Utils.safeQuerySelector('#' + prefix + 'ImageSizeLimit') ||
- Utils.safeQuerySelector('#imageSizeLimit');
- this.enableBase64DetailCheckbox = Utils.safeQuerySelector('#' + prefix + 'EnableBase64Detail') ||
- Utils.safeQuerySelector('#enableBase64Detail');
-
- this.currentImagePrefix = prefix;
-
- if (!this.imageInput || !this.imageUploadArea) {
- console.warn('⚠️ 圖片元素初始化失敗 - imageInput: ' + !!this.imageInput + ', imageUploadArea: ' + !!this.imageUploadArea);
- } else {
- console.log('✅ 圖片元素初始化成功 - 前綴: ' + prefix);
- }
- };
+ ImageHandler.prototype.initImageSettingsElements = function() {
+ // 查找設定頁籤中的圖片設定元素
+ this.imageSizeLimitSelect = Utils.safeQuerySelector('#settingsImageSizeLimit');
+ this.enableBase64DetailCheckbox = Utils.safeQuerySelector('#settingsEnableBase64Detail');
- /**
- * 設置圖片事件監聽器
- */
- ImageHandler.prototype.setupImageEventListeners = function() {
- if (!this.imageInput || !this.imageUploadArea) {
- console.warn('⚠️ 缺少必要的圖片元素,跳過事件監聽器設置');
- return;
- }
-
- console.log('🖼️ 設置圖片事件監聽器 - imageInput: ' + this.imageInput.id + ', imageUploadArea: ' + this.imageUploadArea.id);
-
- // 移除舊的事件監聽器
- this.removeImageEventListeners();
-
- const self = this;
-
- // 文件選擇事件
- this.imageChangeHandler = function(e) {
- console.log('📁 文件選擇事件觸發 - input: ' + e.target.id + ', files: ' + e.target.files.length);
- self.handleFileSelect(e.target.files);
- };
- this.imageInput.addEventListener('change', this.imageChangeHandler);
-
- // 點擊上傳區域
- this.imageClickHandler = function(e) {
- e.preventDefault();
- e.stopPropagation();
-
- if (self.imageInput) {
- console.log('🖱️ 點擊上傳區域 - 觸發 input: ' + self.imageInput.id);
- self.imageInput.click();
- }
- };
- this.imageUploadArea.addEventListener('click', this.imageClickHandler);
-
- // 拖放事件
- this.imageDragOverHandler = function(e) {
- e.preventDefault();
- self.imageUploadArea.classList.add('dragover');
- };
- this.imageUploadArea.addEventListener('dragover', this.imageDragOverHandler);
-
- this.imageDragLeaveHandler = function(e) {
- e.preventDefault();
- self.imageUploadArea.classList.remove('dragover');
- };
- this.imageUploadArea.addEventListener('dragleave', this.imageDragLeaveHandler);
-
- this.imageDropHandler = function(e) {
- e.preventDefault();
- self.imageUploadArea.classList.remove('dragover');
- self.handleFileSelect(e.dataTransfer.files);
- };
- this.imageUploadArea.addEventListener('drop', this.imageDropHandler);
-
- // 初始化圖片設定事件
+ // 初始化設定事件監聽器
this.initImageSettings();
+
+ console.log('✅ 圖片設定元素初始化完成');
};
- /**
- * 設置全域剪貼板貼上事件
- */
- ImageHandler.prototype.setupGlobalPasteHandler = function() {
- if (this.pasteHandler) {
- return; // 已經設置過了
- }
- const self = this;
- this.pasteHandler = function(e) {
- const items = e.clipboardData.items;
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- if (item.type.indexOf('image') !== -1) {
- e.preventDefault();
- const file = item.getAsFile();
- self.handleFileSelect([file]);
- break;
- }
- }
- };
-
- document.addEventListener('paste', this.pasteHandler);
- console.log('✅ 全域剪貼板貼上事件已設置');
- };
+
+
/**
- * 移除圖片事件監聽器
+ * 移除圖片設定事件監聽器
*/
- ImageHandler.prototype.removeImageEventListeners = function() {
- if (this.imageInput && this.imageChangeHandler) {
- this.imageInput.removeEventListener('change', this.imageChangeHandler);
+ ImageHandler.prototype.removeImageSettingsListeners = function() {
+ if (this.imageSizeLimitSelect && this.imageSizeLimitChangeHandler) {
+ this.imageSizeLimitSelect.removeEventListener('change', this.imageSizeLimitChangeHandler);
+ this.imageSizeLimitChangeHandler = null;
}
-
- if (this.imageUploadArea) {
- if (this.imageClickHandler) {
- this.imageUploadArea.removeEventListener('click', this.imageClickHandler);
- }
- if (this.imageDragOverHandler) {
- this.imageUploadArea.removeEventListener('dragover', this.imageDragOverHandler);
- }
- if (this.imageDragLeaveHandler) {
- this.imageUploadArea.removeEventListener('dragleave', this.imageDragLeaveHandler);
- }
- if (this.imageDropHandler) {
- this.imageUploadArea.removeEventListener('drop', this.imageDropHandler);
- }
+
+ if (this.enableBase64DetailCheckbox && this.enableBase64DetailChangeHandler) {
+ this.enableBase64DetailCheckbox.removeEventListener('change', this.enableBase64DetailChangeHandler);
+ this.enableBase64DetailChangeHandler = null;
}
};
@@ -198,238 +104,47 @@
*/
ImageHandler.prototype.initImageSettings = function() {
const self = this;
-
+
+ // 移除舊的設定事件監聽器
+ this.removeImageSettingsListeners();
+
if (this.imageSizeLimitSelect) {
- this.imageSizeLimitSelect.addEventListener('change', function(e) {
+ this.imageSizeLimitChangeHandler = function(e) {
self.imageSizeLimit = parseInt(e.target.value);
if (self.onSettingsChange) {
self.onSettingsChange();
}
- });
+ };
+ this.imageSizeLimitSelect.addEventListener('change', this.imageSizeLimitChangeHandler);
}
if (this.enableBase64DetailCheckbox) {
- this.enableBase64DetailCheckbox.addEventListener('change', function(e) {
+ this.enableBase64DetailChangeHandler = function(e) {
self.enableBase64Detail = e.target.checked;
if (self.onSettingsChange) {
self.onSettingsChange();
}
- });
- }
- };
-
- /**
- * 處理文件選擇
- */
- ImageHandler.prototype.handleFileSelect = function(files) {
- for (let i = 0; i < files.length; i++) {
- const file = files[i];
- if (file.type.startsWith('image/')) {
- this.addImage(file);
- }
- }
- };
-
- /**
- * 添加圖片
- */
- ImageHandler.prototype.addImage = function(file) {
- // 檢查文件大小
- if (this.imageSizeLimit > 0 && file.size > this.imageSizeLimit) {
- Utils.showMessage('圖片大小超過限制 (' + Utils.formatFileSize(this.imageSizeLimit) + ')', Utils.CONSTANTS.MESSAGE_WARNING);
- return;
- }
-
- const self = this;
- this.fileToBase64(file)
- .then(function(base64) {
- const imageData = {
- name: file.name,
- size: file.size,
- type: file.type,
- data: base64
- };
-
- self.images.push(imageData);
- self.updateImagePreview();
- })
- .catch(function(error) {
- console.error('圖片處理失敗:', error);
- Utils.showMessage('圖片處理失敗,請重試', Utils.CONSTANTS.MESSAGE_ERROR);
- });
- };
-
- /**
- * 將文件轉換為 Base64
- */
- ImageHandler.prototype.fileToBase64 = function(file) {
- return new Promise(function(resolve, reject) {
- const reader = new FileReader();
- reader.onload = function() {
- resolve(reader.result.split(',')[1]);
};
- reader.onerror = reject;
- reader.readAsDataURL(file);
- });
- };
-
- /**
- * 更新圖片預覽
- */
- ImageHandler.prototype.updateImagePreview = function() {
- const previewContainers = [
- Utils.safeQuerySelector('#feedbackImagePreviewContainer'),
- Utils.safeQuerySelector('#combinedImagePreviewContainer'),
- this.imagePreviewContainer
- ].filter(function(container) {
- return container !== null;
- });
-
- if (previewContainers.length === 0) {
- console.warn('⚠️ 沒有找到圖片預覽容器');
- return;
+ this.enableBase64DetailCheckbox.addEventListener('change', this.enableBase64DetailChangeHandler);
}
-
- console.log('🖼️ 更新 ' + previewContainers.length + ' 個圖片預覽容器');
-
- const self = this;
- previewContainers.forEach(function(container) {
- container.innerHTML = '';
-
- self.images.forEach(function(image, index) {
- const preview = self.createImagePreviewElement(image, index);
- container.appendChild(preview);
- });
- });
-
- this.updateImageCount();
};
- /**
- * 創建圖片預覽元素
- */
- ImageHandler.prototype.createImagePreviewElement = function(image, index) {
- const self = this;
-
- // 創建圖片預覽項目容器
- const preview = document.createElement('div');
- preview.className = 'image-preview-item';
- preview.style.cssText = 'position: relative; display: inline-block;';
- // 創建圖片元素
- const img = document.createElement('img');
- img.src = 'data:' + image.type + ';base64,' + image.data;
- img.alt = image.name;
- img.style.cssText = 'width: 80px; height: 80px; object-fit: cover; display: block; border-radius: 6px;';
- // 創建圖片信息容器
- const imageInfo = document.createElement('div');
- imageInfo.className = 'image-info';
- imageInfo.style.cssText = `
- position: absolute; bottom: 0; left: 0; right: 0;
- background: rgba(0, 0, 0, 0.7); color: white; padding: 4px;
- font-size: 10px; line-height: 1.2;
- `;
- const imageName = document.createElement('div');
- imageName.className = 'image-name';
- imageName.textContent = image.name;
- imageName.style.cssText = 'font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
-
- const imageSize = document.createElement('div');
- imageSize.className = 'image-size';
- imageSize.textContent = Utils.formatFileSize(image.size);
- imageSize.style.cssText = 'font-size: 9px; opacity: 0.8;';
-
- // 創建刪除按鈕
- const removeBtn = document.createElement('button');
- removeBtn.className = 'image-remove-btn';
- removeBtn.textContent = '×';
- removeBtn.title = '移除圖片';
- removeBtn.style.cssText = `
- position: absolute; top: -8px; right: -8px; width: 20px; height: 20px;
- border-radius: 50%; background: #f44336; color: white; border: none;
- cursor: pointer; font-size: 12px; font-weight: bold;
- display: flex; align-items: center; justify-content: center;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; z-index: 10;
- `;
-
- // 添加刪除按鈕懸停效果
- removeBtn.addEventListener('mouseenter', function() {
- removeBtn.style.background = '#d32f2f';
- removeBtn.style.transform = 'scale(1.1)';
- });
- removeBtn.addEventListener('mouseleave', function() {
- removeBtn.style.background = '#f44336';
- removeBtn.style.transform = 'scale(1)';
- });
-
- // 添加刪除功能
- removeBtn.addEventListener('click', function(e) {
- e.preventDefault();
- e.stopPropagation();
- self.removeImage(index);
- });
-
- // 組裝元素
- imageInfo.appendChild(imageName);
- imageInfo.appendChild(imageSize);
- preview.appendChild(img);
- preview.appendChild(imageInfo);
- preview.appendChild(removeBtn);
-
- return preview;
- };
/**
- * 更新圖片計數顯示
+ * 獲取圖片數據
*/
- ImageHandler.prototype.updateImageCount = function() {
- const count = this.images.length;
- const countElements = document.querySelectorAll('.image-count');
-
- countElements.forEach(function(element) {
- element.textContent = count > 0 ? '(' + count + ')' : '';
- });
-
- // 更新上傳區域的顯示狀態
- const uploadAreas = [
- Utils.safeQuerySelector('#feedbackImageUploadArea'),
- Utils.safeQuerySelector('#combinedImageUploadArea')
- ].filter(function(area) {
- return area !== null;
- });
-
- uploadAreas.forEach(function(area) {
- if (count > 0) {
- area.classList.add('has-images');
- } else {
- area.classList.remove('has-images');
- }
- });
- };
-
- /**
- * 移除圖片
- */
- ImageHandler.prototype.removeImage = function(index) {
- this.images.splice(index, 1);
- this.updateImagePreview();
+ ImageHandler.prototype.getImages = function() {
+ return this.fileUploadManager.getFiles();
};
/**
* 清空所有圖片
*/
ImageHandler.prototype.clearImages = function() {
- this.images = [];
- this.updateImagePreview();
- };
-
- /**
- * 獲取圖片數據
- */
- ImageHandler.prototype.getImages = function() {
- return Utils.deepClone(this.images);
+ this.fileUploadManager.clearFiles();
};
/**
@@ -437,19 +152,13 @@
*/
ImageHandler.prototype.reinitialize = function(layoutMode) {
console.log('🔄 重新初始化圖片處理功能...');
-
+
this.layoutMode = layoutMode;
- this.removeImageEventListeners();
- this.initImageElements();
-
- if (this.imageUploadArea && this.imageInput) {
- this.setupImageEventListeners();
- console.log('✅ 圖片處理功能重新初始化完成');
- } else {
- console.warn('⚠️ 圖片處理重新初始化失敗 - 缺少必要元素');
- }
-
- this.updateImagePreview();
+
+ // 重新初始化設定元素
+ this.initImageSettingsElements();
+
+ console.log('✅ 圖片處理功能重新初始化完成');
};
/**
@@ -458,7 +167,13 @@
ImageHandler.prototype.updateSettings = function(settings) {
this.imageSizeLimit = settings.imageSizeLimit || 0;
this.enableBase64Detail = settings.enableBase64Detail || false;
-
+
+ // 更新檔案上傳管理器設定
+ this.fileUploadManager.updateSettings({
+ imageSizeLimit: this.imageSizeLimit,
+ enableBase64Detail: this.enableBase64Detail
+ });
+
// 同步到 UI 元素
if (this.imageSizeLimitSelect) {
this.imageSizeLimitSelect.value = this.imageSizeLimit.toString();
@@ -472,14 +187,8 @@
* 清理資源
*/
ImageHandler.prototype.cleanup = function() {
- this.removeImageEventListeners();
-
- if (this.pasteHandler) {
- document.removeEventListener('paste', this.pasteHandler);
- this.pasteHandler = null;
- }
-
- this.clearImages();
+ this.removeImageSettingsListeners();
+ this.fileUploadManager.cleanup();
};
// 將 ImageHandler 加入命名空間
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js
index 400bc96..6ac8e33 100644
--- a/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/settings-manager.js
@@ -369,15 +369,22 @@
* 應用圖片設定
*/
SettingsManager.prototype.applyImageSettings = function() {
+ // 更新所有圖片大小限制選擇器(包括設定頁籤中的)
const imageSizeLimitSelects = document.querySelectorAll('[id$="ImageSizeLimit"]');
imageSizeLimitSelects.forEach(function(select) {
select.value = this.currentSettings.imageSizeLimit.toString();
}.bind(this));
+ // 更新所有 Base64 相容模式複選框(包括設定頁籤中的)
const enableBase64DetailCheckboxes = document.querySelectorAll('[id$="EnableBase64Detail"]');
enableBase64DetailCheckboxes.forEach(function(checkbox) {
checkbox.checked = this.currentSettings.enableBase64Detail;
}.bind(this));
+
+ console.log('圖片設定已應用到 UI:', {
+ imageSizeLimit: this.currentSettings.imageSizeLimit,
+ enableBase64Detail: this.currentSettings.enableBase64Detail
+ });
};
@@ -424,6 +431,26 @@
});
});
+ // 圖片設定 - 大小限制選擇器
+ const settingsImageSizeLimit = Utils.safeQuerySelector('#settingsImageSizeLimit');
+ if (settingsImageSizeLimit) {
+ settingsImageSizeLimit.addEventListener('change', function(e) {
+ const value = parseInt(e.target.value);
+ self.set('imageSizeLimit', value);
+ console.log('圖片大小限制已更新:', value);
+ });
+ }
+
+ // 圖片設定 - Base64 相容模式切換器
+ const settingsEnableBase64Detail = Utils.safeQuerySelector('#settingsEnableBase64Detail');
+ if (settingsEnableBase64Detail) {
+ settingsEnableBase64Detail.addEventListener('change', function(e) {
+ const value = e.target.checked;
+ self.set('enableBase64Detail', value);
+ console.log('Base64 相容模式已更新:', value);
+ });
+ }
+
// 重置設定
const resetBtn = Utils.safeQuerySelector('#resetSettingsBtn');
if (resetBtn) {
@@ -435,7 +462,6 @@
});
}
-
};
// 將 SettingsManager 加入命名空間
diff --git a/src/mcp_feedback_enhanced/web/templates/components/image-upload.html b/src/mcp_feedback_enhanced/web/templates/components/image-upload.html
index 24dd397..0b6d1a7 100644
--- a/src/mcp_feedback_enhanced/web/templates/components/image-upload.html
+++ b/src/mcp_feedback_enhanced/web/templates/components/image-upload.html
@@ -19,34 +19,7 @@
{% set min_height = min_height or "120px" %}
{% set upload_text = upload_text or "📎 點擊選擇圖片或拖放圖片到此處
支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式" %}
-
-