/** * MCP Feedback Enhanced - 完整回饋應用程式 * ========================================== * * 支援完整的 UI 交互功能,包括頁籤切換、圖片處理、WebSocket 通信等 */ /** * 標籤頁管理器 - 處理多標籤頁狀態同步和智能瀏覽器管理 */ class TabManager { constructor() { this.tabId = this.generateTabId(); this.heartbeatInterval = null; this.heartbeatFrequency = 5000; // 5秒心跳 this.storageKey = 'mcp_feedback_tabs'; this.lastActivityKey = 'mcp_feedback_last_activity'; this.init(); } generateTabId() { return `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } init() { // 註冊當前標籤頁 this.registerTab(); // 向服務器註冊標籤頁 this.registerTabToServer(); // 開始心跳 this.startHeartbeat(); // 監聽頁面關閉事件 window.addEventListener('beforeunload', () => { this.unregisterTab(); }); // 監聽 localStorage 變化(其他標籤頁的狀態變化) window.addEventListener('storage', (e) => { if (e.key === this.storageKey) { this.handleTabsChange(); } }); console.log(`📋 TabManager 初始化完成,標籤頁 ID: ${this.tabId}`); } registerTab() { const tabs = this.getActiveTabs(); tabs[this.tabId] = { timestamp: Date.now(), url: window.location.href, active: true }; localStorage.setItem(this.storageKey, JSON.stringify(tabs)); this.updateLastActivity(); console.log(`✅ 標籤頁已註冊: ${this.tabId}`); } unregisterTab() { const tabs = this.getActiveTabs(); delete tabs[this.tabId]; localStorage.setItem(this.storageKey, JSON.stringify(tabs)); console.log(`❌ 標籤頁已註銷: ${this.tabId}`); } startHeartbeat() { this.heartbeatInterval = setInterval(() => { this.sendHeartbeat(); }, this.heartbeatFrequency); } sendHeartbeat() { const tabs = this.getActiveTabs(); if (tabs[this.tabId]) { tabs[this.tabId].timestamp = Date.now(); localStorage.setItem(this.storageKey, JSON.stringify(tabs)); this.updateLastActivity(); } } updateLastActivity() { localStorage.setItem(this.lastActivityKey, Date.now().toString()); } getActiveTabs() { try { const stored = localStorage.getItem(this.storageKey); const tabs = stored ? JSON.parse(stored) : {}; // 清理過期的標籤頁(超過30秒沒有心跳) const now = Date.now(); const expiredThreshold = 30000; // 30秒 Object.keys(tabs).forEach(tabId => { if (now - tabs[tabId].timestamp > expiredThreshold) { delete tabs[tabId]; } }); return tabs; } catch (error) { console.error('獲取活躍標籤頁失敗:', error); return {}; } } hasActiveTabs() { const tabs = this.getActiveTabs(); return Object.keys(tabs).length > 0; } isOnlyActiveTab() { const tabs = this.getActiveTabs(); return Object.keys(tabs).length === 1 && tabs[this.tabId]; } handleTabsChange() { // 處理其他標籤頁狀態變化 console.log('🔄 檢測到其他標籤頁狀態變化'); } async registerTabToServer() { try { const response = await fetch('/api/register-tab', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ tabId: this.tabId }) }); if (response.ok) { const data = await response.json(); console.log(`✅ 標籤頁已向服務器註冊: ${this.tabId}`); } else { console.warn(`⚠️ 標籤頁服務器註冊失敗: ${response.status}`); } } catch (error) { console.warn(`⚠️ 標籤頁服務器註冊錯誤: ${error}`); } } cleanup() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } this.unregisterTab(); } } class FeedbackApp { constructor(sessionId = null) { // 會話信息 this.sessionId = sessionId; // 標籤頁管理 this.tabManager = new TabManager(); // WebSocket 相關 this.websocket = null; this.isConnected = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.heartbeatInterval = null; this.heartbeatFrequency = 30000; // 30秒 WebSocket 心跳 // UI 狀態 this.currentTab = 'feedback'; // 回饋狀態管理 this.feedbackState = 'waiting_for_feedback'; // waiting_for_feedback, feedback_submitted, processing this.currentSessionId = null; this.lastSubmissionTime = null; // 圖片處理 this.images = []; this.imageSizeLimit = 0; this.enableBase64Detail = false; // 設定 this.autoClose = false; this.layoutMode = 'separate'; // 語言設定 this.currentLanguage = 'zh-TW'; this.init(); } async init() { console.log('初始化 MCP Feedback Enhanced 應用程式'); try { // 等待國際化系統 if (window.i18nManager) { await window.i18nManager.init(); } // 初始化 UI 組件 this.initUIComponents(); // 設置事件監聽器 this.setupEventListeners(); // 設置 WebSocket 連接 this.setupWebSocket(); // 載入設定(異步等待完成) await this.loadSettings(); // 初始化頁籤(在設定載入完成後) this.initTabs(); // 初始化圖片處理 this.initImageHandling(); // 確保狀態指示器使用正確的翻譯(在國際化系統載入後) this.updateStatusIndicators(); // 設置頁面關閉時的清理 window.addEventListener('beforeunload', () => { if (this.tabManager) { this.tabManager.cleanup(); } if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } }); console.log('MCP Feedback Enhanced 應用程式初始化完成'); } catch (error) { console.error('應用程式初始化失敗:', error); } } initUIComponents() { // 基本 UI 元素 this.connectionIndicator = document.getElementById('connectionIndicator'); this.connectionText = document.getElementById('connectionText'); // 頁籤相關元素 this.tabButtons = document.querySelectorAll('.tab-button'); this.tabContents = document.querySelectorAll('.tab-content'); // 回饋相關元素 this.feedbackText = document.getElementById('feedbackText'); this.submitBtn = document.getElementById('submitBtn'); this.cancelBtn = document.getElementById('cancelBtn'); // 命令相關元素 this.commandInput = document.getElementById('commandInput'); this.commandOutput = document.getElementById('commandOutput'); this.runCommandBtn = document.getElementById('runCommandBtn'); // 動態初始化圖片相關元素 this.initImageElements(); } /** * 動態初始化圖片相關元素,支援多佈局模式 */ initImageElements() { // 根據當前佈局模式確定元素前綴 const prefix = this.layoutMode && this.layoutMode.startsWith('combined') ? 'combined' : 'feedback'; console.log(`🖼️ 初始化圖片元素,使用前綴: ${prefix}`); // 圖片相關元素 - 優先使用當前模式的元素 this.imageInput = document.getElementById(`${prefix}ImageInput`) || document.getElementById('imageInput'); this.imageUploadArea = document.getElementById(`${prefix}ImageUploadArea`) || document.getElementById('imageUploadArea'); this.imagePreviewContainer = document.getElementById(`${prefix}ImagePreviewContainer`) || document.getElementById('imagePreviewContainer'); this.imageSizeLimitSelect = document.getElementById(`${prefix}ImageSizeLimit`) || document.getElementById('imageSizeLimit'); this.enableBase64DetailCheckbox = document.getElementById(`${prefix}EnableBase64Detail`) || document.getElementById('enableBase64Detail'); // 記錄當前使用的前綴,用於後續操作 this.currentImagePrefix = prefix; // 驗證關鍵元素是否存在 if (!this.imageInput || !this.imageUploadArea) { console.warn(`⚠️ 圖片元素初始化失敗 - imageInput: ${!!this.imageInput}, imageUploadArea: ${!!this.imageUploadArea}`); } else { console.log(`✅ 圖片元素初始化成功 - 前綴: ${prefix}`); } } initTabs() { // 設置頁籤點擊事件 this.tabButtons.forEach(button => { button.addEventListener('click', (e) => { const tabName = button.getAttribute('data-tab'); this.switchTab(tabName); }); }); // 根據佈局模式確定初始頁籤 let initialTab = this.currentTab; if (this.layoutMode.startsWith('combined')) { // 合併模式時,確保初始頁籤是 combined initialTab = 'combined'; } else { // 分離模式時,如果當前頁籤是 combined,則切換到 feedback if (this.currentTab === 'combined') { initialTab = 'feedback'; } } // 設置初始頁籤(不觸發保存,避免循環調用) this.setInitialTab(initialTab); } setInitialTab(tabName) { // 更新當前頁籤(不觸發保存) this.currentTab = tabName; // 更新按鈕狀態 this.tabButtons.forEach(button => { if (button.getAttribute('data-tab') === tabName) { button.classList.add('active'); } else { button.classList.remove('active'); } }); // 更新內容顯示 this.tabContents.forEach(content => { if (content.id === `tab-${tabName}`) { content.classList.add('active'); } else { content.classList.remove('active'); } }); // 特殊處理 if (tabName === 'combined') { this.handleCombinedMode(); } console.log(`初始化頁籤: ${tabName}`); } switchTab(tabName) { // 更新當前頁籤 this.currentTab = tabName; // 更新按鈕狀態 this.tabButtons.forEach(button => { if (button.getAttribute('data-tab') === tabName) { button.classList.add('active'); } else { button.classList.remove('active'); } }); // 更新內容顯示 this.tabContents.forEach(content => { if (content.id === `tab-${tabName}`) { content.classList.add('active'); } else { content.classList.remove('active'); } }); // 特殊處理 if (tabName === 'combined') { this.handleCombinedMode(); } // 重新初始化圖片處理(確保使用正確的佈局模式元素) this.reinitializeImageHandling(); // 保存當前頁籤設定 this.saveSettings(); console.log(`切換到頁籤: ${tabName}`); } /** * 重新初始化圖片處理功能 */ reinitializeImageHandling() { console.log('🔄 重新初始化圖片處理功能...'); // 移除舊的事件監聽器 this.removeImageEventListeners(); // 重新初始化圖片元素 this.initImageElements(); // 如果有必要的元素,重新設置事件監聽器 if (this.imageUploadArea && this.imageInput) { this.setupImageEventListeners(); console.log('✅ 圖片處理功能重新初始化完成'); } else { console.warn('⚠️ 圖片處理重新初始化失敗 - 缺少必要元素'); } // 更新圖片預覽(確保在新的容器中顯示) this.updateImagePreview(); } /** * 設置圖片事件監聽器 */ setupImageEventListeners() { // 文件選擇事件 this.imageChangeHandler = (e) => { this.handleFileSelect(e.target.files); }; this.imageInput.addEventListener('change', this.imageChangeHandler); // 點擊上傳區域 this.imageClickHandler = () => { this.imageInput.click(); }; this.imageUploadArea.addEventListener('click', this.imageClickHandler); // 拖放事件 this.imageDragOverHandler = (e) => { e.preventDefault(); this.imageUploadArea.classList.add('dragover'); }; this.imageUploadArea.addEventListener('dragover', this.imageDragOverHandler); this.imageDragLeaveHandler = (e) => { e.preventDefault(); this.imageUploadArea.classList.remove('dragover'); }; this.imageUploadArea.addEventListener('dragleave', this.imageDragLeaveHandler); this.imageDropHandler = (e) => { e.preventDefault(); this.imageUploadArea.classList.remove('dragover'); this.handleFileSelect(e.dataTransfer.files); }; this.imageUploadArea.addEventListener('drop', this.imageDropHandler); // 初始化圖片設定事件 this.initImageSettings(); } initImageHandling() { console.log('🖼️ 開始初始化圖片處理功能...'); // 重新初始化圖片元素(確保使用最新的佈局模式) this.initImageElements(); if (!this.imageUploadArea || !this.imageInput) { console.warn('⚠️ 圖片處理初始化失敗 - 缺少必要元素'); return; } // 清除舊的事件監聽器(如果存在) this.removeImageEventListeners(); // 設置圖片事件監聽器 this.setupImageEventListeners(); // 設置全域剪貼板貼上事件(只設置一次) if (!this.pasteHandler) { this.pasteHandler = (e) => { const items = e.clipboardData.items; for (let item of items) { if (item.type.indexOf('image') !== -1) { e.preventDefault(); const file = item.getAsFile(); this.handleFileSelect([file]); break; } } }; document.addEventListener('paste', this.pasteHandler); console.log('✅ 全域剪貼板貼上事件已設置'); } console.log('✅ 圖片處理功能初始化完成'); } /** * 移除舊的圖片事件監聽器 */ removeImageEventListeners() { if (this.imageInput && this.imageChangeHandler) { this.imageInput.removeEventListener('change', this.imageChangeHandler); } 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); } } } /** * 初始化圖片設定事件 */ initImageSettings() { // 圖片大小限制設定 if (this.imageSizeLimitSelect) { this.imageSizeLimitSelect.addEventListener('change', (e) => { this.imageSizeLimit = parseInt(e.target.value); this.saveSettings(); }); } // Base64 詳細模式設定 if (this.enableBase64DetailCheckbox) { this.enableBase64DetailCheckbox.addEventListener('change', (e) => { this.enableBase64Detail = e.target.checked; this.saveSettings(); }); } // 同步設定到其他佈局模式 this.syncImageSettingsAcrossLayouts(); } /** * 同步圖片設定到所有佈局模式 */ syncImageSettingsAcrossLayouts() { const prefixes = ['feedback', 'combined']; prefixes.forEach(prefix => { const sizeSelect = document.getElementById(`${prefix}ImageSizeLimit`); const base64Checkbox = document.getElementById(`${prefix}EnableBase64Detail`); if (sizeSelect && sizeSelect !== this.imageSizeLimitSelect) { sizeSelect.value = this.imageSizeLimit.toString(); sizeSelect.addEventListener('change', (e) => { this.imageSizeLimit = parseInt(e.target.value); // 同步到其他元素 prefixes.forEach(otherPrefix => { const otherSelect = document.getElementById(`${otherPrefix}ImageSizeLimit`); if (otherSelect && otherSelect !== e.target) { otherSelect.value = e.target.value; } }); this.saveSettings(); }); } if (base64Checkbox && base64Checkbox !== this.enableBase64DetailCheckbox) { base64Checkbox.checked = this.enableBase64Detail; base64Checkbox.addEventListener('change', (e) => { this.enableBase64Detail = e.target.checked; // 同步到其他元素 prefixes.forEach(otherPrefix => { const otherCheckbox = document.getElementById(`${otherPrefix}EnableBase64Detail`); if (otherCheckbox && otherCheckbox !== e.target) { otherCheckbox.checked = e.target.checked; } }); this.saveSettings(); }); } }); } handleFileSelect(files) { for (let file of files) { if (file.type.startsWith('image/')) { this.addImage(file); } } } async addImage(file) { // 檢查文件大小 if (this.imageSizeLimit > 0 && file.size > this.imageSizeLimit) { alert(`圖片大小超過限制 (${this.formatFileSize(this.imageSizeLimit)})`); return; } try { const base64 = await this.fileToBase64(file); const imageData = { name: file.name, size: file.size, type: file.type, data: base64 }; this.images.push(imageData); this.updateImagePreview(); } catch (error) { console.error('圖片處理失敗:', error); alert('圖片處理失敗,請重試'); } } fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result.split(',')[1]); reader.onerror = reject; reader.readAsDataURL(file); }); } updateImagePreview() { // 更新所有佈局模式的圖片預覽容器 const previewContainers = [ document.getElementById('feedbackImagePreviewContainer'), document.getElementById('combinedImagePreviewContainer'), this.imagePreviewContainer // 當前主要容器 ].filter(container => container); // 過濾掉不存在的容器 if (previewContainers.length === 0) { console.warn('⚠️ 沒有找到圖片預覽容器'); return; } console.log(`🖼️ 更新 ${previewContainers.length} 個圖片預覽容器`); previewContainers.forEach(container => { container.innerHTML = ''; this.images.forEach((image, index) => { // 創建圖片預覽項目容器 const preview = document.createElement('div'); preview.className = 'image-preview-item'; preview.style.position = 'relative'; preview.style.display = 'inline-block'; // 創建圖片元素 const img = document.createElement('img'); img.src = `data:${image.type};base64,${image.data}`; img.alt = image.name; img.style.width = '80px'; img.style.height = '80px'; img.style.objectFit = 'cover'; img.style.display = 'block'; img.style.borderRadius = '6px'; // 創建圖片信息容器 const imageInfo = document.createElement('div'); imageInfo.className = 'image-info'; imageInfo.style.position = 'absolute'; imageInfo.style.bottom = '0'; imageInfo.style.left = '0'; imageInfo.style.right = '0'; imageInfo.style.background = 'rgba(0, 0, 0, 0.7)'; imageInfo.style.color = 'white'; imageInfo.style.padding = '4px'; imageInfo.style.fontSize = '10px'; imageInfo.style.lineHeight = '1.2'; // 創建文件名元素 const imageName = document.createElement('div'); imageName.className = 'image-name'; imageName.textContent = image.name; imageName.style.fontWeight = 'bold'; imageName.style.overflow = 'hidden'; imageName.style.textOverflow = 'ellipsis'; imageName.style.whiteSpace = 'nowrap'; // 創建文件大小元素 const imageSize = document.createElement('div'); imageSize.className = 'image-size'; imageSize.textContent = this.formatFileSize(image.size); imageSize.style.fontSize = '9px'; imageSize.style.opacity = '0.8'; // 創建刪除按鈕 const removeBtn = document.createElement('button'); removeBtn.className = 'image-remove-btn'; removeBtn.textContent = '×'; removeBtn.title = '移除圖片'; removeBtn.style.position = 'absolute'; removeBtn.style.top = '-8px'; removeBtn.style.right = '-8px'; removeBtn.style.width = '20px'; removeBtn.style.height = '20px'; removeBtn.style.borderRadius = '50%'; removeBtn.style.background = '#f44336'; removeBtn.style.color = 'white'; removeBtn.style.border = 'none'; removeBtn.style.cursor = 'pointer'; removeBtn.style.fontSize = '12px'; removeBtn.style.fontWeight = 'bold'; removeBtn.style.display = 'flex'; removeBtn.style.alignItems = 'center'; removeBtn.style.justifyContent = 'center'; removeBtn.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.3)'; removeBtn.style.transition = 'all 0.3s ease'; removeBtn.style.zIndex = '10'; // 添加刪除按鈕懸停效果 removeBtn.addEventListener('mouseenter', () => { removeBtn.style.background = '#d32f2f'; removeBtn.style.transform = 'scale(1.1)'; }); removeBtn.addEventListener('mouseleave', () => { removeBtn.style.background = '#f44336'; removeBtn.style.transform = 'scale(1)'; }); // 添加刪除功能 removeBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.removeImage(index); }); // 組裝元素 imageInfo.appendChild(imageName); imageInfo.appendChild(imageSize); preview.appendChild(img); preview.appendChild(imageInfo); preview.appendChild(removeBtn); container.appendChild(preview); }); }); // 更新圖片計數顯示 this.updateImageCount(); } /** * 更新圖片計數顯示 */ updateImageCount() { const count = this.images.length; const countElements = document.querySelectorAll('.image-count'); countElements.forEach(element => { element.textContent = count > 0 ? `(${count})` : ''; }); // 更新上傳區域的顯示狀態 const uploadAreas = [ document.getElementById('feedbackImageUploadArea'), document.getElementById('combinedImageUploadArea') ].filter(area => area); uploadAreas.forEach(area => { if (count > 0) { area.classList.add('has-images'); } else { area.classList.remove('has-images'); } }); } removeImage(index) { this.images.splice(index, 1); this.updateImagePreview(); } formatFileSize(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]; } // ==================== 狀態管理系統 ==================== /** * 設置回饋狀態 * @param {string} state - waiting_for_feedback, feedback_submitted, processing * @param {string} sessionId - 當前會話 ID */ setFeedbackState(state, sessionId = null) { const previousState = this.feedbackState; this.feedbackState = state; if (sessionId && sessionId !== this.currentSessionId) { // 新會話開始,重置狀態 this.currentSessionId = sessionId; this.lastSubmissionTime = null; console.log(`🔄 新會話開始: ${sessionId.substring(0, 8)}...`); } console.log(`📊 狀態變更: ${previousState} → ${state}`); this.updateUIState(); this.updateStatusIndicator(); } /** * 檢查是否可以提交回饋 */ canSubmitFeedback() { const canSubmit = this.feedbackState === 'waiting_for_feedback' && this.isConnected; console.log(`🔍 檢查提交權限: feedbackState=${this.feedbackState}, isConnected=${this.isConnected}, canSubmit=${canSubmit}`); return canSubmit; } /** * 更新 UI 狀態 */ updateUIState() { // 更新提交按鈕狀態 if (this.submitBtn) { const canSubmit = this.canSubmitFeedback(); this.submitBtn.disabled = !canSubmit; switch (this.feedbackState) { case 'waiting_for_feedback': this.submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submit') : '提交回饋'; this.submitBtn.className = 'btn btn-primary'; break; case 'processing': this.submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.processing') : '處理中...'; this.submitBtn.className = 'btn btn-secondary'; break; case 'feedback_submitted': this.submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submitted') : '已提交'; this.submitBtn.className = 'btn btn-success'; break; } } // 更新回饋文字框狀態 if (this.feedbackText) { this.feedbackText.disabled = !this.canSubmitFeedback(); } // 更新合併模式的回饋文字框狀態 const combinedFeedbackText = document.getElementById('combinedFeedbackText'); if (combinedFeedbackText) { combinedFeedbackText.disabled = !this.canSubmitFeedback(); } // 更新圖片上傳狀態 if (this.imageUploadArea) { if (this.canSubmitFeedback()) { this.imageUploadArea.classList.remove('disabled'); } else { this.imageUploadArea.classList.add('disabled'); } } // 更新合併模式的圖片上傳狀態 const combinedImageUploadArea = document.getElementById('combinedImageUploadArea'); if (combinedImageUploadArea) { if (this.canSubmitFeedback()) { combinedImageUploadArea.classList.remove('disabled'); } else { combinedImageUploadArea.classList.add('disabled'); } } } /** * 更新狀態指示器(新版本:只更新現有元素的狀態) */ updateStatusIndicator() { // 獲取狀態指示器元素 const feedbackStatusIndicator = document.getElementById('feedbackStatusIndicator'); const combinedStatusIndicator = document.getElementById('combinedFeedbackStatusIndicator'); // 根據當前狀態確定圖示、標題和訊息 let icon, title, message, status; switch (this.feedbackState) { case 'waiting_for_feedback': icon = '⏳'; title = window.i18nManager ? window.i18nManager.t('status.waiting.title') : '等待回饋'; message = window.i18nManager ? window.i18nManager.t('status.waiting.message') : '請提供您的回饋意見'; status = 'waiting'; break; case 'processing': icon = '⚙️'; title = window.i18nManager ? window.i18nManager.t('status.processing.title') : '處理中'; message = window.i18nManager ? window.i18nManager.t('status.processing.message') : '正在提交您的回饋...'; status = 'processing'; break; case 'feedback_submitted': const timeStr = this.lastSubmissionTime ? new Date(this.lastSubmissionTime).toLocaleTimeString() : ''; icon = '✅'; title = window.i18nManager ? window.i18nManager.t('status.submitted.title') : '回饋已提交'; message = window.i18nManager ? window.i18nManager.t('status.submitted.message') : '等待下次 MCP 調用'; if (timeStr) { message += ` (${timeStr})`; } status = 'submitted'; break; default: // 預設狀態 icon = '⏳'; title = '等待回饋'; message = '請提供您的回饋意見'; status = 'waiting'; } // 更新分頁模式的狀態指示器 if (feedbackStatusIndicator) { this.updateStatusIndicatorElement(feedbackStatusIndicator, status, icon, title, message); } // 更新合併模式的狀態指示器 if (combinedStatusIndicator) { this.updateStatusIndicatorElement(combinedStatusIndicator, status, icon, title, message); } console.log(`✅ 狀態指示器已更新: ${status} - ${title}`); } /** * 更新單個狀態指示器元素 */ updateStatusIndicatorElement(element, status, icon, title, message) { if (!element) return; // 更新狀態類別 element.className = `feedback-status-indicator status-${status}`; element.style.display = 'block'; // 更新標題(包含圖示) const titleElement = element.querySelector('.status-title'); if (titleElement) { titleElement.textContent = `${icon} ${title}`; } // 更新訊息 const messageElement = element.querySelector('.status-message'); if (messageElement) { messageElement.textContent = message; } console.log(`🔧 已更新狀態指示器: ${element.id} -> ${status}`); } setupWebSocket() { // 確保 WebSocket URL 格式正確 const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.host; const wsUrl = `${protocol}//${host}/ws`; console.log('嘗試連接 WebSocket:', wsUrl); this.updateConnectionStatus('connecting', '連接中...'); try { // 如果已有連接,先關閉 if (this.websocket) { this.websocket.close(); this.websocket = null; } this.websocket = new WebSocket(wsUrl); this.websocket.onopen = () => { this.isConnected = true; this.updateConnectionStatus('connected', '已連接'); console.log('WebSocket 連接已建立'); // 開始 WebSocket 心跳 this.startWebSocketHeartbeat(); // 連接成功後,請求會話狀態 this.requestSessionStatus(); }; this.websocket.onmessage = (event) => { try { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); } catch (error) { console.error('解析 WebSocket 消息失敗:', error); } }; this.websocket.onclose = (event) => { this.isConnected = false; console.log('WebSocket 連接已關閉, code:', event.code, 'reason:', event.reason); // 停止心跳 this.stopWebSocketHeartbeat(); // 重置回饋狀態,避免卡在處理狀態 if (this.feedbackState === 'processing') { console.log('🔄 WebSocket 斷開,重置處理狀態'); this.setFeedbackState('waiting_for_feedback'); } if (event.code === 4004) { // 沒有活躍會話 this.updateConnectionStatus('disconnected', '沒有活躍會話'); } else { this.updateConnectionStatus('disconnected', '已斷開'); // 只有在非正常關閉時才重連 if (event.code !== 1000) { console.log('3秒後嘗試重連...'); setTimeout(() => { console.log('🔄 開始重連 WebSocket...'); this.setupWebSocket(); }, 3000); } } }; this.websocket.onerror = (error) => { console.error('WebSocket 錯誤:', error); this.updateConnectionStatus('error', '連接錯誤'); }; } catch (error) { console.error('WebSocket 連接失敗:', error); this.updateConnectionStatus('error', '連接失敗'); } } requestSessionStatus() { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { this.websocket.send(JSON.stringify({ type: 'get_status' })); } } startWebSocketHeartbeat() { // 清理現有心跳 this.stopWebSocketHeartbeat(); this.heartbeatInterval = setInterval(() => { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { this.websocket.send(JSON.stringify({ type: 'heartbeat', tabId: this.tabManager.tabId, timestamp: Date.now() })); } }, this.heartbeatFrequency); console.log(`💓 WebSocket 心跳已啟動,頻率: ${this.heartbeatFrequency}ms`); } stopWebSocketHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; console.log('💔 WebSocket 心跳已停止'); } } handleWebSocketMessage(data) { console.log('收到 WebSocket 消息:', data); switch (data.type) { case 'connection_established': console.log('WebSocket 連接確認'); break; case 'heartbeat_response': // 心跳回應,更新標籤頁活躍狀態 this.tabManager.updateLastActivity(); break; case 'command_output': this.appendCommandOutput(data.output); break; case 'command_complete': this.appendCommandOutput(`\n[命令完成,退出碼: ${data.exit_code}]\n`); this.enableCommandInput(); break; case 'command_error': this.appendCommandOutput(`\n[錯誤: ${data.error}]\n`); this.enableCommandInput(); break; case 'feedback_received': console.log('回饋已收到'); this.handleFeedbackReceived(data); break; case 'status_update': console.log('狀態更新:', data.status_info); this.handleStatusUpdate(data.status_info); break; case 'session_updated': console.log('會話已更新:', data.session_info); this.handleSessionUpdated(data); break; default: console.log('未處理的消息類型:', data.type); } } handleFeedbackReceived(data) { // 使用新的狀態管理系統 this.setFeedbackState('feedback_submitted'); this.lastSubmissionTime = Date.now(); // 顯示成功訊息 this.showSuccessMessage(data.message || '回饋提交成功!'); // 更新 AI 摘要區域顯示「已送出反饋」狀態 this.updateSummaryStatus('已送出反饋,等待下次 MCP 調用...'); // 重構:不再自動關閉頁面,保持持久性 console.log('反饋已提交,頁面保持開啟狀態'); } handleSessionUpdated(data) { console.log('🔄 處理會話更新:', data.session_info); // 顯示更新通知 this.showSuccessMessage(data.message || '會話已更新,正在局部更新內容...'); // 更新會話信息 if (data.session_info) { const newSessionId = data.session_info.session_id; console.log(`📋 會話 ID 更新: ${this.currentSessionId} -> ${newSessionId}`); // 重置回饋狀態為等待新回饋(使用新的會話 ID) this.setFeedbackState('waiting_for_feedback', newSessionId); // 更新當前會話 ID this.currentSessionId = newSessionId; // 更新頁面標題 if (data.session_info.project_directory) { const projectName = data.session_info.project_directory.split(/[/\\]/).pop(); document.title = `MCP Feedback - ${projectName}`; } // 使用局部更新替代整頁刷新 this.refreshPageContent(); } else { // 如果沒有會話信息,仍然重置狀態 console.log('⚠️ 會話更新沒有包含會話信息,僅重置狀態'); this.setFeedbackState('waiting_for_feedback'); } console.log('✅ 會話更新處理完成'); } async refreshPageContent() { console.log('🔄 局部更新頁面內容...'); try { // 保存當前標籤頁狀態到 localStorage if (this.tabManager) { this.tabManager.updateLastActivity(); } // 使用局部更新替代整頁刷新 await this.updatePageContentPartially(); } catch (error) { console.error('局部更新頁面內容失敗:', error); // 備用方案:顯示提示讓用戶手動刷新 this.showMessage('更新內容失敗,請手動刷新頁面以查看新的 AI 工作摘要', 'warning'); } } /** * 局部更新頁面內容,避免整頁刷新 */ async updatePageContentPartially() { console.log('🔄 開始局部更新頁面內容...'); try { // 1. 獲取最新的會話資料 const response = await fetch('/api/current-session'); if (!response.ok) { throw new Error(`API 請求失敗: ${response.status}`); } const sessionData = await response.json(); console.log('📥 獲取到最新會話資料:', sessionData); // 2. 更新 AI 摘要內容 this.updateAISummaryContent(sessionData.summary); // 3. 重置回饋表單 this.resetFeedbackForm(); // 4. 更新狀態指示器 this.updateStatusIndicators(); // 5. 更新頁面標題 if (sessionData.project_directory) { const projectName = sessionData.project_directory.split(/[/\\]/).pop(); document.title = `MCP Feedback - ${projectName}`; } console.log('✅ 局部更新完成'); } catch (error) { console.error('❌ 局部更新失敗:', error); throw error; // 重新拋出錯誤,讓調用者處理 } } /** * 更新 AI 摘要內容 */ updateAISummaryContent(summary) { console.log('📝 更新 AI 摘要內容...'); // 更新分頁模式的摘要內容 const summaryContent = document.getElementById('summaryContent'); if (summaryContent) { summaryContent.textContent = summary; console.log('✅ 已更新分頁模式摘要內容'); } // 更新合併模式的摘要內容 const combinedSummaryContent = document.getElementById('combinedSummaryContent'); if (combinedSummaryContent) { combinedSummaryContent.textContent = summary; console.log('✅ 已更新合併模式摘要內容'); } } /** * 重置回饋表單 */ resetFeedbackForm() { console.log('🔄 重置回饋表單...'); // 清空分頁模式的回饋輸入 const feedbackText = document.getElementById('feedbackText'); if (feedbackText) { feedbackText.value = ''; feedbackText.disabled = false; console.log('✅ 已重置分頁模式回饋輸入'); } // 清空合併模式的回饋輸入 const combinedFeedbackText = document.getElementById('combinedFeedbackText'); if (combinedFeedbackText) { combinedFeedbackText.value = ''; combinedFeedbackText.disabled = false; console.log('✅ 已重置合併模式回饋輸入'); } // 重置圖片上傳組件 this.images = []; this.updateImagePreview(); // 重新啟用提交按鈕 const submitButtons = document.querySelectorAll('.submit-button, #submitButton, #combinedSubmitButton'); submitButtons.forEach(button => { if (button) { button.disabled = false; button.textContent = button.getAttribute('data-original-text') || '提交回饋'; } }); console.log('✅ 回饋表單重置完成'); } /** * 更新狀態指示器 */ updateStatusIndicators() { console.log('🔄 更新狀態指示器...'); // 使用國際化系統獲取翻譯文字 const waitingTitle = window.i18nManager ? window.i18nManager.t('status.waiting.title') : 'Waiting for Feedback'; const waitingMessage = window.i18nManager ? window.i18nManager.t('status.waiting.message') : 'Please provide your feedback on the AI work results'; // 更新分頁模式的狀態指示器 const feedbackStatusIndicator = document.getElementById('feedbackStatusIndicator'); if (feedbackStatusIndicator) { this.setStatusIndicator(feedbackStatusIndicator, 'waiting', '⏳', waitingTitle, waitingMessage); } // 更新合併模式的狀態指示器 const combinedFeedbackStatusIndicator = document.getElementById('combinedFeedbackStatusIndicator'); if (combinedFeedbackStatusIndicator) { this.setStatusIndicator(combinedFeedbackStatusIndicator, 'waiting', '⏳', waitingTitle, waitingMessage); } console.log('✅ 狀態指示器更新完成'); } /** * 設置狀態指示器的內容(兼容舊版本調用) */ setStatusIndicator(element, status, icon, title, message) { // 直接調用新的更新方法 this.updateStatusIndicatorElement(element, status, icon, title, message); } handleStatusUpdate(statusInfo) { console.log('處理狀態更新:', statusInfo); // 更新頁面標題顯示會話信息 if (statusInfo.project_directory) { const projectName = statusInfo.project_directory.split(/[/\\]/).pop(); document.title = `MCP Feedback - ${projectName}`; } // 提取會話 ID(如果有的話) const sessionId = statusInfo.session_id || this.currentSessionId; // 根據狀態更新 UI 和狀態管理 switch (statusInfo.status) { case 'feedback_submitted': this.setFeedbackState('feedback_submitted', sessionId); this.updateSummaryStatus('已送出反饋,等待下次 MCP 調用...'); const submittedConnectionText = window.i18nManager ? window.i18nManager.t('connection.submitted') : '已連接 - 反饋已提交'; this.updateConnectionStatus('connected', submittedConnectionText); break; case 'active': case 'waiting': // 檢查是否是新會話 if (sessionId && sessionId !== this.currentSessionId) { // 新會話開始,重置狀態 this.setFeedbackState('waiting_for_feedback', sessionId); } else if (this.feedbackState !== 'feedback_submitted') { // 如果不是已提交狀態,設置為等待狀態 this.setFeedbackState('waiting_for_feedback', sessionId); } if (statusInfo.status === 'waiting') { this.updateSummaryStatus('等待用戶回饋...'); } const waitingConnectionText = window.i18nManager ? window.i18nManager.t('connection.waiting') : '已連接 - 等待回饋'; this.updateConnectionStatus('connected', waitingConnectionText); break; default: this.updateConnectionStatus('connected', `已連接 - ${statusInfo.status || '未知狀態'}`); } } disableSubmitButton() { const submitBtn = document.getElementById('submitBtn'); if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submitted') : '✅ 已提交'; submitBtn.style.background = 'var(--success-color)'; } } enableSubmitButton() { const submitBtn = document.getElementById('submitBtn'); if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submit') : '📤 提交回饋'; submitBtn.style.background = 'var(--accent-color)'; } } updateSummaryStatus(message) { const summaryElements = document.querySelectorAll('.ai-summary-content'); summaryElements.forEach(element => { element.innerHTML = `
${sessionInfo.summary}