2025-06-03 06:50:19 +08:00
|
|
|
|
/**
|
2025-06-06 16:44:24 +08:00
|
|
|
|
* MCP Feedback Enhanced - 完整回饋應用程式
|
|
|
|
|
* ==========================================
|
|
|
|
|
*
|
|
|
|
|
* 支援完整的 UI 交互功能,包括頁籤切換、圖片處理、WebSocket 通信等
|
2025-06-03 06:50:19 +08:00
|
|
|
|
*/
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
/**
|
|
|
|
|
* 標籤頁管理器 - 處理多標籤頁狀態同步和智能瀏覽器管理
|
|
|
|
|
*/
|
|
|
|
|
class TabManager {
|
2025-06-03 15:09:08 +08:00
|
|
|
|
constructor() {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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('🔄 檢測到其他標籤頁狀態變化');
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
async registerTabToServer() {
|
2025-06-03 15:09:08 +08:00
|
|
|
|
try {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
const response = await fetch('/api/register-tab', {
|
2025-06-03 15:09:08 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
2025-06-06 16:44:24 +08:00
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
tabId: this.tabId
|
|
|
|
|
})
|
2025-06-03 15:09:08 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
const data = await response.json();
|
|
|
|
|
console.log(`✅ 標籤頁已向服務器註冊: ${this.tabId}`);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
} else {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.warn(`⚠️ 標籤頁服務器註冊失敗: ${response.status}`);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.warn(`⚠️ 標籤頁服務器註冊錯誤: ${error}`);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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 應用程式');
|
|
|
|
|
|
2025-06-03 15:09:08 +08:00
|
|
|
|
try {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 等待國際化系統
|
|
|
|
|
if (window.i18nManager) {
|
|
|
|
|
await window.i18nManager.init();
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
|
|
|
|
|
// 初始化 UI 組件
|
|
|
|
|
this.initUIComponents();
|
|
|
|
|
|
|
|
|
|
// 設置事件監聽器
|
|
|
|
|
this.setupEventListeners();
|
|
|
|
|
|
|
|
|
|
// 設置 WebSocket 連接
|
|
|
|
|
this.setupWebSocket();
|
|
|
|
|
|
|
|
|
|
// 載入設定(異步等待完成)
|
|
|
|
|
await this.loadSettings();
|
|
|
|
|
|
|
|
|
|
// 初始化頁籤(在設定載入完成後)
|
|
|
|
|
this.initTabs();
|
|
|
|
|
|
|
|
|
|
// 初始化圖片處理
|
|
|
|
|
this.initImageHandling();
|
|
|
|
|
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 確保狀態指示器使用正確的翻譯(在國際化系統載入後)
|
|
|
|
|
this.updateStatusIndicators();
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 設置頁面關閉時的清理
|
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
|
|
|
if (this.tabManager) {
|
|
|
|
|
this.tabManager.cleanup();
|
|
|
|
|
}
|
|
|
|
|
if (this.heartbeatInterval) {
|
|
|
|
|
clearInterval(this.heartbeatInterval);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('MCP Feedback Enhanced 應用程式初始化完成');
|
|
|
|
|
|
2025-06-03 15:09:08 +08:00
|
|
|
|
} catch (error) {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.error('應用程式初始化失敗:', error);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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.imageInput = document.getElementById('imageInput');
|
|
|
|
|
this.imageUploadArea = document.getElementById('imageUploadArea');
|
|
|
|
|
this.imagePreviewContainer = document.getElementById('imagePreviewContainer');
|
|
|
|
|
this.imageSizeLimitSelect = document.getElementById('imageSizeLimit');
|
|
|
|
|
this.enableBase64DetailCheckbox = document.getElementById('enableBase64Detail');
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
initTabs() {
|
|
|
|
|
// 設置頁籤點擊事件
|
|
|
|
|
this.tabButtons.forEach(button => {
|
|
|
|
|
button.addEventListener('click', (e) => {
|
|
|
|
|
const tabName = button.getAttribute('data-tab');
|
|
|
|
|
this.switchTab(tabName);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-06 19:55:37 +08:00
|
|
|
|
// 根據佈局模式確定初始頁籤
|
|
|
|
|
let initialTab = this.currentTab;
|
|
|
|
|
if (this.layoutMode.startsWith('combined')) {
|
|
|
|
|
// 合併模式時,確保初始頁籤是 combined
|
|
|
|
|
initialTab = 'combined';
|
|
|
|
|
} else {
|
|
|
|
|
// 分離模式時,如果當前頁籤是 combined,則切換到 feedback
|
|
|
|
|
if (this.currentTab === 'combined') {
|
|
|
|
|
initialTab = 'feedback';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 設置初始頁籤(不觸發保存,避免循環調用)
|
2025-06-06 19:55:37 +08:00
|
|
|
|
this.setInitialTab(initialTab);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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();
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
|
|
|
|
|
console.log(`初始化頁籤: ${tabName}`);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
switchTab(tabName) {
|
|
|
|
|
// 更新當前頁籤
|
|
|
|
|
this.currentTab = tabName;
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 更新按鈕狀態
|
|
|
|
|
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.saveSettings();
|
|
|
|
|
|
|
|
|
|
console.log(`切換到頁籤: ${tabName}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
initImageHandling() {
|
|
|
|
|
if (!this.imageUploadArea || !this.imageInput) return;
|
|
|
|
|
|
|
|
|
|
// 文件選擇事件
|
|
|
|
|
this.imageInput.addEventListener('change', (e) => {
|
|
|
|
|
this.handleFileSelect(e.target.files);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 點擊上傳區域
|
|
|
|
|
this.imageUploadArea.addEventListener('click', () => {
|
|
|
|
|
this.imageInput.click();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 拖放事件
|
|
|
|
|
this.imageUploadArea.addEventListener('dragover', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.imageUploadArea.classList.add('dragover');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.imageUploadArea.addEventListener('dragleave', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.imageUploadArea.classList.remove('dragover');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.imageUploadArea.addEventListener('drop', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.imageUploadArea.classList.remove('dragover');
|
|
|
|
|
this.handleFileSelect(e.dataTransfer.files);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 剪貼板貼上事件
|
|
|
|
|
document.addEventListener('paste', (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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 圖片設定事件
|
|
|
|
|
if (this.imageSizeLimitSelect) {
|
|
|
|
|
this.imageSizeLimitSelect.addEventListener('change', (e) => {
|
|
|
|
|
this.imageSizeLimit = parseInt(e.target.value);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.enableBase64DetailCheckbox) {
|
|
|
|
|
this.enableBase64DetailCheckbox.addEventListener('change', (e) => {
|
|
|
|
|
this.enableBase64Detail = e.target.checked;
|
2025-06-03 15:09:08 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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;
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
try {
|
|
|
|
|
const base64 = await this.fileToBase64(file);
|
|
|
|
|
const imageData = {
|
|
|
|
|
name: file.name,
|
|
|
|
|
size: file.size,
|
|
|
|
|
type: file.type,
|
|
|
|
|
data: base64
|
|
|
|
|
};
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.images.push(imageData);
|
|
|
|
|
this.updateImagePreview();
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('圖片處理失敗:', error);
|
|
|
|
|
alert('圖片處理失敗,請重試');
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
fileToBase64(file) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = () => resolve(reader.result.split(',')[1]);
|
|
|
|
|
reader.onerror = reject;
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
updateImagePreview() {
|
|
|
|
|
if (!this.imagePreviewContainer) return;
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.imagePreviewContainer.innerHTML = '';
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.images.forEach((image, index) => {
|
|
|
|
|
const preview = document.createElement('div');
|
|
|
|
|
preview.className = 'image-preview';
|
|
|
|
|
preview.innerHTML = `
|
|
|
|
|
<img src="data:${image.type};base64,${image.data}" alt="${image.name}">
|
|
|
|
|
<div class="image-info">
|
|
|
|
|
<span class="image-name">${image.name}</span>
|
|
|
|
|
<span class="image-size">${this.formatFileSize(image.size)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="image-remove" onclick="window.feedbackApp.removeImage(${index})">×</button>
|
|
|
|
|
`;
|
|
|
|
|
this.imagePreviewContainer.appendChild(preview);
|
|
|
|
|
});
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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() {
|
|
|
|
|
return this.feedbackState === 'waiting_for_feedback' && this.isConnected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新 UI 狀態
|
|
|
|
|
*/
|
|
|
|
|
updateUIState() {
|
|
|
|
|
// 更新提交按鈕狀態
|
|
|
|
|
if (this.submitBtn) {
|
|
|
|
|
const canSubmit = this.canSubmitFeedback();
|
|
|
|
|
this.submitBtn.disabled = !canSubmit;
|
|
|
|
|
|
|
|
|
|
switch (this.feedbackState) {
|
|
|
|
|
case 'waiting_for_feedback':
|
2025-06-06 17:56:31 +08:00
|
|
|
|
this.submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submit') : '提交回饋';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.submitBtn.className = 'btn btn-primary';
|
|
|
|
|
break;
|
|
|
|
|
case 'processing':
|
2025-06-06 17:56:31 +08:00
|
|
|
|
this.submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.processing') : '處理中...';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.submitBtn.className = 'btn btn-secondary';
|
|
|
|
|
break;
|
|
|
|
|
case 'feedback_submitted':
|
2025-06-06 17:56:31 +08:00
|
|
|
|
this.submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submitted') : '已提交';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
} else {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.imageUploadArea.classList.add('disabled');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新合併模式的圖片上傳狀態
|
|
|
|
|
const combinedImageUploadArea = document.getElementById('combinedImageUploadArea');
|
|
|
|
|
if (combinedImageUploadArea) {
|
|
|
|
|
if (this.canSubmitFeedback()) {
|
|
|
|
|
combinedImageUploadArea.classList.remove('disabled');
|
|
|
|
|
} else {
|
|
|
|
|
combinedImageUploadArea.classList.add('disabled');
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
/**
|
2025-06-06 21:09:45 +08:00
|
|
|
|
* 更新狀態指示器(新版本:只更新現有元素的狀態)
|
2025-06-06 16:44:24 +08:00
|
|
|
|
*/
|
|
|
|
|
updateStatusIndicator() {
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 獲取狀態指示器元素
|
|
|
|
|
const feedbackStatusIndicator = document.getElementById('feedbackStatusIndicator');
|
|
|
|
|
const combinedStatusIndicator = document.getElementById('combinedFeedbackStatusIndicator');
|
2025-06-06 16:44:24 +08:00
|
|
|
|
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 根據當前狀態確定圖示、標題和訊息
|
|
|
|
|
let icon, title, message, status;
|
2025-06-06 16:44:24 +08:00
|
|
|
|
|
|
|
|
|
switch (this.feedbackState) {
|
|
|
|
|
case 'waiting_for_feedback':
|
2025-06-06 21:09:45 +08:00
|
|
|
|
icon = '⏳';
|
|
|
|
|
title = window.i18nManager ? window.i18nManager.t('status.waiting.title') : '等待回饋';
|
|
|
|
|
message = window.i18nManager ? window.i18nManager.t('status.waiting.message') : '請提供您的回饋意見';
|
|
|
|
|
status = 'waiting';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'processing':
|
2025-06-06 21:09:45 +08:00
|
|
|
|
icon = '⚙️';
|
|
|
|
|
title = window.i18nManager ? window.i18nManager.t('status.processing.title') : '處理中';
|
|
|
|
|
message = window.i18nManager ? window.i18nManager.t('status.processing.message') : '正在提交您的回饋...';
|
|
|
|
|
status = 'processing';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 'feedback_submitted':
|
|
|
|
|
const timeStr = this.lastSubmissionTime ?
|
|
|
|
|
new Date(this.lastSubmissionTime).toLocaleTimeString() : '';
|
2025-06-06 21:09:45 +08:00
|
|
|
|
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';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
break;
|
2025-06-06 21:09:45 +08:00
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// 預設狀態
|
|
|
|
|
icon = '⏳';
|
|
|
|
|
title = '等待回饋';
|
|
|
|
|
message = '請提供您的回饋意見';
|
|
|
|
|
status = 'waiting';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 更新分頁模式的狀態指示器
|
|
|
|
|
if (feedbackStatusIndicator) {
|
|
|
|
|
this.updateStatusIndicatorElement(feedbackStatusIndicator, status, icon, title, message);
|
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 更新合併模式的狀態指示器
|
|
|
|
|
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}`);
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setupWebSocket() {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 確保 WebSocket URL 格式正確
|
2025-06-03 06:50:19 +08:00
|
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
const host = window.location.host;
|
|
|
|
|
const wsUrl = `${protocol}//${host}/ws`;
|
|
|
|
|
|
|
|
|
|
console.log('嘗試連接 WebSocket:', wsUrl);
|
|
|
|
|
this.updateConnectionStatus('connecting', '連接中...');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
|
|
|
|
try {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 如果已有連接,先關閉
|
|
|
|
|
if (this.websocket) {
|
|
|
|
|
this.websocket.close();
|
|
|
|
|
this.websocket = null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 06:50:19 +08:00
|
|
|
|
this.websocket = new WebSocket(wsUrl);
|
|
|
|
|
|
|
|
|
|
this.websocket.onopen = () => {
|
|
|
|
|
this.isConnected = true;
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.updateConnectionStatus('connected', '已連接');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
console.log('WebSocket 連接已建立');
|
2025-06-06 16:44:24 +08:00
|
|
|
|
|
|
|
|
|
// 開始 WebSocket 心跳
|
|
|
|
|
this.startWebSocketHeartbeat();
|
|
|
|
|
|
|
|
|
|
// 連接成功後,請求會話狀態
|
|
|
|
|
this.requestSessionStatus();
|
2025-06-03 06:50:19 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.websocket.onmessage = (event) => {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
try {
|
|
|
|
|
const data = JSON.parse(event.data);
|
|
|
|
|
this.handleWebSocketMessage(data);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('解析 WebSocket 消息失敗:', error);
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
};
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.websocket.onclose = (event) => {
|
2025-06-03 06:50:19 +08:00
|
|
|
|
this.isConnected = false;
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('WebSocket 連接已關閉, code:', event.code, 'reason:', event.reason);
|
|
|
|
|
|
|
|
|
|
// 停止心跳
|
|
|
|
|
this.stopWebSocketHeartbeat();
|
|
|
|
|
|
|
|
|
|
if (event.code === 4004) {
|
|
|
|
|
// 沒有活躍會話
|
|
|
|
|
this.updateConnectionStatus('disconnected', '沒有活躍會話');
|
|
|
|
|
} else {
|
|
|
|
|
this.updateConnectionStatus('disconnected', '已斷開');
|
|
|
|
|
|
|
|
|
|
// 只有在非正常關閉時才重連
|
|
|
|
|
if (event.code !== 1000) {
|
|
|
|
|
console.log('3秒後嘗試重連...');
|
|
|
|
|
setTimeout(() => this.setupWebSocket(), 3000);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.websocket.onerror = (error) => {
|
|
|
|
|
console.error('WebSocket 錯誤:', error);
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.updateConnectionStatus('error', '連接錯誤');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('WebSocket 連接失敗:', error);
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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 心跳已停止');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleWebSocketMessage(data) {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('收到 WebSocket 消息:', data);
|
|
|
|
|
|
2025-06-03 06:50:19 +08:00
|
|
|
|
switch (data.type) {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
case 'connection_established':
|
|
|
|
|
console.log('WebSocket 連接確認');
|
|
|
|
|
break;
|
|
|
|
|
case 'heartbeat_response':
|
|
|
|
|
// 心跳回應,更新標籤頁活躍狀態
|
|
|
|
|
this.tabManager.updateLastActivity();
|
|
|
|
|
break;
|
2025-06-03 06:50:19 +08:00
|
|
|
|
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('回饋已收到');
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.handleFeedbackReceived(data);
|
|
|
|
|
break;
|
|
|
|
|
case 'status_update':
|
|
|
|
|
console.log('狀態更新:', data.status_info);
|
|
|
|
|
this.handleStatusUpdate(data.status_info);
|
2025-06-03 06:50:19 +08:00
|
|
|
|
break;
|
2025-06-06 16:44:24 +08:00
|
|
|
|
case 'session_updated':
|
|
|
|
|
console.log('會話已更新:', data.session_info);
|
|
|
|
|
this.handleSessionUpdated(data);
|
2025-06-03 22:26:38 +08:00
|
|
|
|
break;
|
2025-06-03 06:50:19 +08:00
|
|
|
|
default:
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('未處理的消息類型:', data.type);
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
handleFeedbackReceived(data) {
|
|
|
|
|
// 使用新的狀態管理系統
|
|
|
|
|
this.setFeedbackState('feedback_submitted');
|
|
|
|
|
this.lastSubmissionTime = Date.now();
|
|
|
|
|
|
|
|
|
|
// 顯示成功訊息
|
|
|
|
|
this.showSuccessMessage(data.message || '回饋提交成功!');
|
|
|
|
|
|
|
|
|
|
// 更新 AI 摘要區域顯示「已送出反饋」狀態
|
|
|
|
|
this.updateSummaryStatus('已送出反饋,等待下次 MCP 調用...');
|
|
|
|
|
|
|
|
|
|
// 重構:不再自動關閉頁面,保持持久性
|
|
|
|
|
console.log('反饋已提交,頁面保持開啟狀態');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
handleSessionUpdated(data) {
|
|
|
|
|
console.log('🔄 處理會話更新:', data.session_info);
|
2025-06-03 22:26:38 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 顯示更新通知
|
2025-06-06 21:09:45 +08:00
|
|
|
|
this.showSuccessMessage(data.message || '會話已更新,正在局部更新內容...');
|
2025-06-03 22:26:38 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 重置回饋狀態為等待新回饋
|
|
|
|
|
this.setFeedbackState('waiting_for_feedback');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 更新會話信息
|
|
|
|
|
if (data.session_info) {
|
|
|
|
|
this.currentSessionId = data.session_info.session_id;
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 更新頁面標題
|
|
|
|
|
if (data.session_info.project_directory) {
|
|
|
|
|
const projectName = data.session_info.project_directory.split(/[/\\]/).pop();
|
|
|
|
|
document.title = `MCP Feedback - ${projectName}`;
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 使用局部更新替代整頁刷新
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.refreshPageContent();
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('✅ 會話更新處理完成');
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
async refreshPageContent() {
|
2025-06-06 21:09:45 +08:00
|
|
|
|
console.log('🔄 局部更新頁面內容...');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
try {
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 保存當前標籤頁狀態到 localStorage
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (this.tabManager) {
|
|
|
|
|
this.tabManager.updateLastActivity();
|
|
|
|
|
}
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 使用局部更新替代整頁刷新
|
|
|
|
|
await this.updatePageContentPartially();
|
2025-06-06 16:44:24 +08:00
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-06-06 21:09:45 +08:00
|
|
|
|
console.error('局部更新頁面內容失敗:', error);
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 備用方案:顯示提示讓用戶手動刷新
|
2025-06-06 21:09:45 +08:00
|
|
|
|
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('✅ 已更新合併模式摘要內容');
|
2025-06-06 16:44:24 +08:00
|
|
|
|
}
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 21:09:45 +08:00
|
|
|
|
/**
|
|
|
|
|
* 重置回饋表單
|
|
|
|
|
*/
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
handleStatusUpdate(statusInfo) {
|
|
|
|
|
console.log('處理狀態更新:', statusInfo);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 更新頁面標題顯示會話信息
|
|
|
|
|
if (statusInfo.project_directory) {
|
|
|
|
|
const projectName = statusInfo.project_directory.split(/[/\\]/).pop();
|
|
|
|
|
document.title = `MCP Feedback - ${projectName}`;
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 提取會話 ID(如果有的話)
|
|
|
|
|
const sessionId = statusInfo.session_id || this.currentSessionId;
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 根據狀態更新 UI 和狀態管理
|
|
|
|
|
switch (statusInfo.status) {
|
|
|
|
|
case 'feedback_submitted':
|
|
|
|
|
this.setFeedbackState('feedback_submitted', sessionId);
|
|
|
|
|
this.updateSummaryStatus('已送出反饋,等待下次 MCP 調用...');
|
2025-06-06 17:56:31 +08:00
|
|
|
|
const submittedConnectionText = window.i18nManager ? window.i18nManager.t('connection.submitted') : '已連接 - 反饋已提交';
|
|
|
|
|
this.updateConnectionStatus('connected', submittedConnectionText);
|
2025-06-06 16:44:24 +08:00
|
|
|
|
break;
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (statusInfo.status === 'waiting') {
|
|
|
|
|
this.updateSummaryStatus('等待用戶回饋...');
|
|
|
|
|
}
|
2025-06-06 17:56:31 +08:00
|
|
|
|
const waitingConnectionText = window.i18nManager ? window.i18nManager.t('connection.waiting') : '已連接 - 等待回饋';
|
|
|
|
|
this.updateConnectionStatus('connected', waitingConnectionText);
|
2025-06-06 16:44:24 +08:00
|
|
|
|
break;
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
default:
|
|
|
|
|
this.updateConnectionStatus('connected', `已連接 - ${statusInfo.status || '未知狀態'}`);
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
disableSubmitButton() {
|
|
|
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
|
|
|
if (submitBtn) {
|
|
|
|
|
submitBtn.disabled = true;
|
2025-06-06 17:56:31 +08:00
|
|
|
|
submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submitted') : '✅ 已提交';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
submitBtn.style.background = 'var(--success-color)';
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
enableSubmitButton() {
|
|
|
|
|
const submitBtn = document.getElementById('submitBtn');
|
|
|
|
|
if (submitBtn) {
|
|
|
|
|
submitBtn.disabled = false;
|
2025-06-06 17:56:31 +08:00
|
|
|
|
submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submit') : '📤 提交回饋';
|
2025-06-06 16:44:24 +08:00
|
|
|
|
submitBtn.style.background = 'var(--accent-color)';
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
updateSummaryStatus(message) {
|
|
|
|
|
const summaryElements = document.querySelectorAll('.ai-summary-content');
|
|
|
|
|
summaryElements.forEach(element => {
|
|
|
|
|
element.innerHTML = `
|
|
|
|
|
<div style="padding: 16px; background: var(--success-color); color: white; border-radius: 6px; text-align: center;">
|
|
|
|
|
✅ ${message}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
showSuccessMessage(message = '✅ 回饋提交成功!頁面將保持開啟等待下次調用。') {
|
|
|
|
|
this.showMessage(message, 'success');
|
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
showMessage(message, type = 'info') {
|
|
|
|
|
// 創建消息元素
|
|
|
|
|
const messageDiv = document.createElement('div');
|
|
|
|
|
messageDiv.className = `message message-${type}`;
|
|
|
|
|
messageDiv.style.cssText = `
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 80px;
|
|
|
|
|
right: 20px;
|
|
|
|
|
z-index: 1001;
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
background: var(--success-color);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
|
|
|
max-width: 300px;
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
`;
|
|
|
|
|
messageDiv.textContent = message;
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(messageDiv);
|
|
|
|
|
|
|
|
|
|
// 3秒後自動移除
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (messageDiv.parentNode) {
|
|
|
|
|
messageDiv.parentNode.removeChild(messageDiv);
|
|
|
|
|
}
|
|
|
|
|
}, 3000);
|
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
updateConnectionStatus(status, text) {
|
|
|
|
|
if (this.connectionIndicator) {
|
|
|
|
|
this.connectionIndicator.className = `connection-indicator ${status}`;
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (this.connectionText) {
|
|
|
|
|
this.connectionText.textContent = text;
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
showWaitingInterface() {
|
|
|
|
|
if (this.waitingContainer) {
|
|
|
|
|
this.waitingContainer.style.display = 'flex';
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (this.mainContainer) {
|
|
|
|
|
this.mainContainer.classList.remove('active');
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
showMainInterface() {
|
|
|
|
|
if (this.waitingContainer) {
|
|
|
|
|
this.waitingContainer.style.display = 'none';
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (this.mainContainer) {
|
|
|
|
|
this.mainContainer.classList.add('active');
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
async loadFeedbackInterface(sessionInfo) {
|
|
|
|
|
if (!this.mainContainer) return;
|
|
|
|
|
|
|
|
|
|
this.sessionInfo = sessionInfo;
|
|
|
|
|
|
|
|
|
|
// 載入完整的回饋界面
|
|
|
|
|
this.mainContainer.innerHTML = await this.generateFeedbackHTML(sessionInfo);
|
|
|
|
|
|
|
|
|
|
// 重新設置事件監聽器
|
|
|
|
|
this.setupFeedbackEventListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async generateFeedbackHTML(sessionInfo) {
|
|
|
|
|
return `
|
|
|
|
|
<div class="feedback-container">
|
|
|
|
|
<!-- 頭部 -->
|
|
|
|
|
<header class="header">
|
|
|
|
|
<div class="header-content">
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
<h1 class="title">MCP Feedback Enhanced</h1>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="project-info">
|
|
|
|
|
專案目錄: ${sessionInfo.project_directory}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- AI 摘要區域 -->
|
|
|
|
|
<div class="ai-summary-section">
|
|
|
|
|
<h2>AI 工作摘要</h2>
|
|
|
|
|
<div class="ai-summary-content">
|
|
|
|
|
<p>${sessionInfo.summary}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 回饋輸入區域 -->
|
|
|
|
|
<div class="feedback-section">
|
|
|
|
|
<h3>提供回饋</h3>
|
|
|
|
|
<div class="input-group">
|
|
|
|
|
<label class="input-label">文字回饋</label>
|
|
|
|
|
<textarea
|
|
|
|
|
id="feedbackText"
|
|
|
|
|
class="text-input"
|
|
|
|
|
placeholder="請在這裡輸入您的回饋..."
|
|
|
|
|
style="min-height: 150px;"
|
|
|
|
|
></textarea>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="button-group">
|
|
|
|
|
<button id="submitBtn" class="btn btn-primary">
|
|
|
|
|
📤 提交回饋
|
|
|
|
|
</button>
|
|
|
|
|
<button id="clearBtn" class="btn btn-secondary">
|
|
|
|
|
🗑️ 清空
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 命令執行區域 -->
|
|
|
|
|
<div class="command-section">
|
|
|
|
|
<h3>命令執行</h3>
|
|
|
|
|
<div class="input-group">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
id="commandInput"
|
|
|
|
|
class="command-input-line"
|
|
|
|
|
placeholder="輸入命令..."
|
|
|
|
|
style="width: 100%; padding: 8px; margin-bottom: 8px;"
|
|
|
|
|
>
|
|
|
|
|
<button id="runCommandBtn" class="btn btn-secondary">
|
|
|
|
|
▶️ 執行
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="commandOutput" class="command-output" style="height: 200px; overflow-y: auto;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
setupEventListeners() {
|
|
|
|
|
// 提交和取消按鈕
|
|
|
|
|
if (this.submitBtn) {
|
|
|
|
|
this.submitBtn.addEventListener('click', () => this.submitFeedback());
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (this.cancelBtn) {
|
|
|
|
|
this.cancelBtn.addEventListener('click', () => this.cancelFeedback());
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 命令執行
|
|
|
|
|
if (this.runCommandBtn) {
|
|
|
|
|
this.runCommandBtn.addEventListener('click', () => this.runCommand());
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (this.commandInput) {
|
|
|
|
|
this.commandInput.addEventListener('keydown', (e) => {
|
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.runCommand();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 快捷鍵
|
2025-06-03 06:50:19 +08:00
|
|
|
|
document.addEventListener('keydown', (e) => {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// Ctrl+Enter 提交回饋
|
2025-06-03 06:50:19 +08:00
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.submitFeedback();
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// Esc 取消
|
2025-06-03 06:50:19 +08:00
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
this.cancelFeedback();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 設定相關事件
|
|
|
|
|
this.setupSettingsEvents();
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
setupSettingsEvents() {
|
|
|
|
|
// 佈局模式切換
|
|
|
|
|
const layoutModeInputs = document.querySelectorAll('input[name="layoutMode"]');
|
|
|
|
|
layoutModeInputs.forEach(input => {
|
|
|
|
|
input.addEventListener('change', (e) => {
|
|
|
|
|
this.layoutMode = e.target.value;
|
|
|
|
|
this.applyLayoutMode();
|
|
|
|
|
this.saveSettings();
|
|
|
|
|
});
|
2025-06-03 06:50:19 +08:00
|
|
|
|
});
|
2025-06-03 17:19:52 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 自動關閉切換
|
|
|
|
|
const autoCloseToggle = document.getElementById('autoCloseToggle');
|
|
|
|
|
if (autoCloseToggle) {
|
|
|
|
|
autoCloseToggle.addEventListener('click', () => {
|
|
|
|
|
this.autoClose = !this.autoClose;
|
|
|
|
|
autoCloseToggle.classList.toggle('active', this.autoClose);
|
|
|
|
|
this.saveSettings();
|
|
|
|
|
});
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 語言切換
|
2025-06-03 06:50:19 +08:00
|
|
|
|
const languageOptions = document.querySelectorAll('.language-option');
|
|
|
|
|
languageOptions.forEach(option => {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
option.addEventListener('click', () => {
|
|
|
|
|
const lang = option.getAttribute('data-lang');
|
|
|
|
|
this.switchLanguage(lang);
|
|
|
|
|
});
|
2025-06-03 06:50:19 +08:00
|
|
|
|
});
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 重置設定
|
|
|
|
|
const resetBtn = document.getElementById('resetSettingsBtn');
|
|
|
|
|
if (resetBtn) {
|
|
|
|
|
resetBtn.addEventListener('click', () => {
|
|
|
|
|
if (confirm('確定要重置所有設定嗎?')) {
|
|
|
|
|
this.resetSettings();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 移除重複的事件監聽器設置方法
|
|
|
|
|
// 所有事件監聽器已在 setupEventListeners() 中統一設置
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
submitFeedback() {
|
|
|
|
|
// 檢查是否可以提交回饋
|
|
|
|
|
if (!this.canSubmitFeedback()) {
|
|
|
|
|
console.log('⚠️ 無法提交回饋 - 當前狀態:', this.feedbackState);
|
|
|
|
|
|
|
|
|
|
if (this.feedbackState === 'feedback_submitted') {
|
|
|
|
|
this.showMessage('回饋已提交,請等待下次 MCP 調用', 'warning');
|
|
|
|
|
} else if (this.feedbackState === 'processing') {
|
|
|
|
|
this.showMessage('正在處理中,請稍候', 'warning');
|
|
|
|
|
} else if (!this.isConnected) {
|
|
|
|
|
this.showMessage('WebSocket 未連接', 'error');
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 根據當前佈局模式獲取回饋內容
|
|
|
|
|
let feedback = '';
|
|
|
|
|
if (this.layoutMode.startsWith('combined')) {
|
|
|
|
|
const combinedFeedbackInput = document.getElementById('combinedFeedbackText');
|
|
|
|
|
feedback = combinedFeedbackInput?.value.trim() || '';
|
2025-06-03 06:50:19 +08:00
|
|
|
|
} else {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
const feedbackInput = document.getElementById('feedbackText');
|
|
|
|
|
feedback = feedbackInput?.value.trim() || '';
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (!feedback && this.images.length === 0) {
|
|
|
|
|
this.showMessage('請提供回饋文字或上傳圖片', 'warning');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 設置處理狀態
|
|
|
|
|
this.setFeedbackState('processing');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
|
|
|
|
try {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 發送回饋
|
2025-06-03 06:50:19 +08:00
|
|
|
|
this.websocket.send(JSON.stringify({
|
2025-06-06 16:44:24 +08:00
|
|
|
|
type: 'submit_feedback',
|
|
|
|
|
feedback: feedback,
|
|
|
|
|
images: this.images,
|
|
|
|
|
settings: {
|
|
|
|
|
image_size_limit: this.imageSizeLimit,
|
|
|
|
|
enable_base64_detail: this.enableBase64Detail
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}));
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 清空表單
|
|
|
|
|
this.clearFeedback();
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('📤 回饋已發送,等待服務器確認...');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.error('❌ 發送回饋失敗:', error);
|
|
|
|
|
this.showMessage('發送失敗,請重試', 'error');
|
|
|
|
|
// 恢復到等待狀態
|
|
|
|
|
this.setFeedbackState('waiting_for_feedback');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
clearFeedback() {
|
|
|
|
|
// 清空分離模式的回饋文字
|
|
|
|
|
if (this.feedbackText) {
|
|
|
|
|
this.feedbackText.value = '';
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 清空合併模式的回饋文字
|
|
|
|
|
const combinedFeedbackText = document.getElementById('combinedFeedbackText');
|
|
|
|
|
if (combinedFeedbackText) {
|
|
|
|
|
combinedFeedbackText.value = '';
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.images = [];
|
|
|
|
|
this.updateImagePreview();
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 同時清空合併模式的圖片預覽
|
|
|
|
|
const combinedImagePreviewContainer = document.getElementById('combinedImagePreviewContainer');
|
|
|
|
|
if (combinedImagePreviewContainer) {
|
|
|
|
|
combinedImagePreviewContainer.innerHTML = '';
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 重新啟用提交按鈕
|
|
|
|
|
if (this.submitBtn) {
|
|
|
|
|
this.submitBtn.disabled = false;
|
2025-06-06 17:56:31 +08:00
|
|
|
|
this.submitBtn.textContent = window.i18nManager ? window.i18nManager.t('buttons.submit') : '提交回饋';
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
runCommand() {
|
|
|
|
|
const commandInput = document.getElementById('commandInput');
|
|
|
|
|
const command = commandInput?.value.trim();
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (!command) {
|
|
|
|
|
this.appendCommandOutput('⚠️ 請輸入命令\n');
|
|
|
|
|
return;
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (!this.isConnected) {
|
|
|
|
|
this.appendCommandOutput('❌ WebSocket 未連接,無法執行命令\n');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 顯示執行的命令
|
|
|
|
|
this.appendCommandOutput(`$ ${command}\n`);
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 發送命令
|
|
|
|
|
try {
|
|
|
|
|
this.websocket.send(JSON.stringify({
|
|
|
|
|
type: 'run_command',
|
|
|
|
|
command: command
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 清空輸入框
|
|
|
|
|
commandInput.value = '';
|
|
|
|
|
this.appendCommandOutput('[正在執行...]\n');
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
this.appendCommandOutput(`❌ 發送命令失敗: ${error.message}\n`);
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
appendCommandOutput(output) {
|
|
|
|
|
const commandOutput = document.getElementById('commandOutput');
|
|
|
|
|
if (commandOutput) {
|
|
|
|
|
commandOutput.textContent += output;
|
|
|
|
|
commandOutput.scrollTop = commandOutput.scrollHeight;
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
enableCommandInput() {
|
|
|
|
|
const commandInput = document.getElementById('commandInput');
|
|
|
|
|
const runCommandBtn = document.getElementById('runCommandBtn');
|
|
|
|
|
|
|
|
|
|
if (commandInput) commandInput.disabled = false;
|
|
|
|
|
if (runCommandBtn) {
|
|
|
|
|
runCommandBtn.disabled = false;
|
|
|
|
|
runCommandBtn.textContent = '▶️ 執行';
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 設定相關方法
|
2025-06-03 15:09:08 +08:00
|
|
|
|
async loadSettings() {
|
|
|
|
|
try {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('開始載入設定...');
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 優先從伺服器端載入設定
|
|
|
|
|
let settings = null;
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/load-settings');
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const serverSettings = await response.json();
|
|
|
|
|
if (Object.keys(serverSettings).length > 0) {
|
|
|
|
|
settings = serverSettings;
|
|
|
|
|
console.log('從伺服器端載入設定成功:', settings);
|
|
|
|
|
|
|
|
|
|
// 同步到 localStorage
|
|
|
|
|
localStorage.setItem('mcp-feedback-settings', JSON.stringify(settings));
|
|
|
|
|
}
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
} catch (serverError) {
|
|
|
|
|
console.warn('從伺服器端載入設定失敗,嘗試從 localStorage 載入:', serverError);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 如果伺服器端載入失敗,回退到 localStorage
|
|
|
|
|
if (!settings) {
|
|
|
|
|
const localSettings = localStorage.getItem('mcp-feedback-settings');
|
|
|
|
|
if (localSettings) {
|
|
|
|
|
settings = JSON.parse(localSettings);
|
|
|
|
|
console.log('從 localStorage 載入設定:', settings);
|
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 應用設定
|
|
|
|
|
if (settings) {
|
|
|
|
|
this.layoutMode = settings.layoutMode || 'separate';
|
|
|
|
|
this.autoClose = settings.autoClose || false;
|
|
|
|
|
this.currentLanguage = settings.language || 'zh-TW';
|
|
|
|
|
this.imageSizeLimit = settings.imageSizeLimit || 0;
|
|
|
|
|
this.enableBase64Detail = settings.enableBase64Detail || false;
|
|
|
|
|
|
|
|
|
|
// 處理 activeTab 設定
|
|
|
|
|
if (settings.activeTab) {
|
|
|
|
|
this.currentTab = settings.activeTab;
|
|
|
|
|
}
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('設定載入完成,應用設定...');
|
2025-06-06 19:37:20 +08:00
|
|
|
|
|
|
|
|
|
// 同步語言設定到 i18nManager(確保 ui_settings.json 優先於 localStorage)
|
|
|
|
|
if (settings.language && window.i18nManager) {
|
|
|
|
|
const currentI18nLanguage = window.i18nManager.getCurrentLanguage();
|
|
|
|
|
console.log(`檢查語言設定: ui_settings.json=${settings.language}, i18nManager=${currentI18nLanguage}`);
|
|
|
|
|
if (settings.language !== currentI18nLanguage) {
|
|
|
|
|
console.log(`🔄 同步語言設定: ${currentI18nLanguage} -> ${settings.language}`);
|
|
|
|
|
window.i18nManager.setLanguage(settings.language);
|
|
|
|
|
// 同步到 localStorage,確保一致性
|
|
|
|
|
localStorage.setItem('language', settings.language);
|
|
|
|
|
console.log(`✅ 語言同步完成: ${settings.language}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`✅ 語言設定已同步: ${settings.language}`);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`⚠️ 語言同步跳過: settings.language=${settings.language}, i18nManager=${!!window.i18nManager}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.applySettings();
|
2025-06-05 01:59:56 +08:00
|
|
|
|
} else {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('沒有找到設定,使用預設值');
|
|
|
|
|
this.applySettings();
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('載入設定失敗:', error);
|
|
|
|
|
// 使用預設設定
|
|
|
|
|
this.applySettings();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
async saveSettings() {
|
|
|
|
|
try {
|
|
|
|
|
const settings = {
|
|
|
|
|
layoutMode: this.layoutMode,
|
|
|
|
|
autoClose: this.autoClose,
|
|
|
|
|
language: this.currentLanguage,
|
|
|
|
|
imageSizeLimit: this.imageSizeLimit,
|
|
|
|
|
enableBase64Detail: this.enableBase64Detail,
|
|
|
|
|
activeTab: this.currentTab
|
|
|
|
|
};
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.log('保存設定:', settings);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 保存到 localStorage
|
|
|
|
|
localStorage.setItem('mcp-feedback-settings', JSON.stringify(settings));
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 同步保存到伺服器端
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch('/api/save-settings', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(settings)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
console.log('設定已同步到伺服器端');
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('同步設定到伺服器端失敗:', response.status);
|
|
|
|
|
}
|
|
|
|
|
} catch (serverError) {
|
|
|
|
|
console.warn('同步設定到伺服器端時發生錯誤:', serverError);
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-06-06 16:44:24 +08:00
|
|
|
|
console.error('保存設定失敗:', error);
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
applySettings() {
|
|
|
|
|
// 應用佈局模式
|
|
|
|
|
this.applyLayoutMode();
|
2025-06-03 06:50:19 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 應用自動關閉設定
|
|
|
|
|
const autoCloseToggle = document.getElementById('autoCloseToggle');
|
|
|
|
|
if (autoCloseToggle) {
|
|
|
|
|
autoCloseToggle.classList.toggle('active', this.autoClose);
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 19:37:20 +08:00
|
|
|
|
// 應用語言設定
|
|
|
|
|
if (this.currentLanguage && window.i18nManager) {
|
|
|
|
|
const currentI18nLanguage = window.i18nManager.getCurrentLanguage();
|
|
|
|
|
if (this.currentLanguage !== currentI18nLanguage) {
|
|
|
|
|
console.log(`應用語言設定: ${currentI18nLanguage} -> ${this.currentLanguage}`);
|
|
|
|
|
window.i18nManager.setLanguage(this.currentLanguage);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 應用圖片設定
|
|
|
|
|
if (this.imageSizeLimitSelect) {
|
|
|
|
|
this.imageSizeLimitSelect.value = this.imageSizeLimit.toString();
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (this.enableBase64DetailCheckbox) {
|
|
|
|
|
this.enableBase64DetailCheckbox.checked = this.enableBase64Detail;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
applyLayoutMode() {
|
|
|
|
|
const layoutModeInputs = document.querySelectorAll('input[name="layoutMode"]');
|
|
|
|
|
layoutModeInputs.forEach(input => {
|
|
|
|
|
input.checked = input.value === this.layoutMode;
|
|
|
|
|
});
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 19:55:37 +08:00
|
|
|
|
// 檢查當前 body class 是否已經正確,避免不必要的 DOM 操作
|
|
|
|
|
const expectedClassName = `layout-${this.layoutMode}`;
|
|
|
|
|
if (document.body.className !== expectedClassName) {
|
|
|
|
|
console.log(`應用佈局模式: ${this.layoutMode}`);
|
|
|
|
|
document.body.className = expectedClassName;
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`佈局模式已正確: ${this.layoutMode},跳過 DOM 更新`);
|
|
|
|
|
}
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 控制頁籤顯示/隱藏
|
|
|
|
|
this.updateTabVisibility();
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 同步合併佈局和分頁中的內容
|
|
|
|
|
this.syncCombinedLayoutContent();
|
2025-06-03 15:09:08 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 如果是合併模式,確保內容同步
|
|
|
|
|
if (this.layoutMode.startsWith('combined')) {
|
|
|
|
|
this.setupCombinedModeSync();
|
|
|
|
|
// 如果當前頁籤不是合併模式,則切換到合併模式頁籤
|
|
|
|
|
if (this.currentTab !== 'combined') {
|
|
|
|
|
this.currentTab = 'combined';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 分離模式時,如果當前頁籤是合併模式,則切換到回饋頁籤
|
|
|
|
|
if (this.currentTab === 'combined') {
|
|
|
|
|
this.currentTab = 'feedback';
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
updateTabVisibility() {
|
|
|
|
|
const combinedTab = document.querySelector('.tab-button[data-tab="combined"]');
|
|
|
|
|
const feedbackTab = document.querySelector('.tab-button[data-tab="feedback"]');
|
|
|
|
|
const summaryTab = document.querySelector('.tab-button[data-tab="summary"]');
|
|
|
|
|
|
|
|
|
|
if (this.layoutMode.startsWith('combined')) {
|
|
|
|
|
// 合併模式:顯示合併模式頁籤,隱藏回饋和AI摘要頁籤
|
|
|
|
|
if (combinedTab) combinedTab.style.display = 'inline-block';
|
|
|
|
|
if (feedbackTab) feedbackTab.style.display = 'none';
|
|
|
|
|
if (summaryTab) summaryTab.style.display = 'none';
|
|
|
|
|
} else {
|
|
|
|
|
// 分離模式:隱藏合併模式頁籤,顯示回饋和AI摘要頁籤
|
|
|
|
|
if (combinedTab) combinedTab.style.display = 'none';
|
|
|
|
|
if (feedbackTab) feedbackTab.style.display = 'inline-block';
|
|
|
|
|
if (summaryTab) summaryTab.style.display = 'inline-block';
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
syncCombinedLayoutContent() {
|
|
|
|
|
// 同步文字內容
|
|
|
|
|
const feedbackText = document.getElementById('feedbackText');
|
|
|
|
|
const combinedFeedbackText = document.getElementById('combinedFeedbackText');
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (feedbackText && combinedFeedbackText) {
|
|
|
|
|
// 雙向同步文字內容
|
|
|
|
|
if (feedbackText.value && !combinedFeedbackText.value) {
|
|
|
|
|
combinedFeedbackText.value = feedbackText.value;
|
|
|
|
|
} else if (combinedFeedbackText.value && !feedbackText.value) {
|
|
|
|
|
feedbackText.value = combinedFeedbackText.value;
|
|
|
|
|
}
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 同步圖片設定
|
|
|
|
|
this.syncImageSettings();
|
|
|
|
|
|
|
|
|
|
// 同步圖片內容
|
|
|
|
|
this.syncImageContent();
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
syncImageSettings() {
|
|
|
|
|
// 同步圖片大小限制設定
|
|
|
|
|
const imageSizeLimit = document.getElementById('imageSizeLimit');
|
|
|
|
|
const combinedImageSizeLimit = document.getElementById('combinedImageSizeLimit');
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (imageSizeLimit && combinedImageSizeLimit) {
|
|
|
|
|
if (imageSizeLimit.value !== combinedImageSizeLimit.value) {
|
|
|
|
|
combinedImageSizeLimit.value = imageSizeLimit.value;
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 同步 Base64 設定
|
|
|
|
|
const enableBase64Detail = document.getElementById('enableBase64Detail');
|
|
|
|
|
const combinedEnableBase64Detail = document.getElementById('combinedEnableBase64Detail');
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (enableBase64Detail && combinedEnableBase64Detail) {
|
|
|
|
|
combinedEnableBase64Detail.checked = enableBase64Detail.checked;
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
}
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
syncImageContent() {
|
|
|
|
|
// 同步圖片預覽內容
|
|
|
|
|
const imagePreviewContainer = document.getElementById('imagePreviewContainer');
|
|
|
|
|
const combinedImagePreviewContainer = document.getElementById('combinedImagePreviewContainer');
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (imagePreviewContainer && combinedImagePreviewContainer) {
|
|
|
|
|
combinedImagePreviewContainer.innerHTML = imagePreviewContainer.innerHTML;
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
setupCombinedModeSync() {
|
|
|
|
|
// 設置文字輸入的雙向同步
|
|
|
|
|
const feedbackText = document.getElementById('feedbackText');
|
|
|
|
|
const combinedFeedbackText = document.getElementById('combinedFeedbackText');
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (feedbackText && combinedFeedbackText) {
|
|
|
|
|
// 移除舊的事件監聽器(如果存在)
|
|
|
|
|
feedbackText.removeEventListener('input', this.syncToCombinetText);
|
|
|
|
|
combinedFeedbackText.removeEventListener('input', this.syncToSeparateText);
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 添加新的事件監聽器
|
|
|
|
|
this.syncToCombinetText = (e) => {
|
|
|
|
|
combinedFeedbackText.value = e.target.value;
|
|
|
|
|
};
|
|
|
|
|
this.syncToSeparateText = (e) => {
|
|
|
|
|
feedbackText.value = e.target.value;
|
|
|
|
|
};
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
feedbackText.addEventListener('input', this.syncToCombinetText);
|
|
|
|
|
combinedFeedbackText.addEventListener('input', this.syncToSeparateText);
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 設置圖片設定的同步
|
|
|
|
|
this.setupImageSettingsSync();
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 設置圖片上傳的同步
|
|
|
|
|
this.setupImageUploadSync();
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
setupImageSettingsSync() {
|
|
|
|
|
const imageSizeLimit = document.getElementById('imageSizeLimit');
|
|
|
|
|
const combinedImageSizeLimit = document.getElementById('combinedImageSizeLimit');
|
|
|
|
|
const enableBase64Detail = document.getElementById('enableBase64Detail');
|
|
|
|
|
const combinedEnableBase64Detail = document.getElementById('combinedEnableBase64Detail');
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (imageSizeLimit && combinedImageSizeLimit) {
|
|
|
|
|
imageSizeLimit.addEventListener('change', (e) => {
|
|
|
|
|
combinedImageSizeLimit.value = e.target.value;
|
|
|
|
|
this.imageSizeLimit = parseInt(e.target.value);
|
|
|
|
|
this.saveSettings();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
combinedImageSizeLimit.addEventListener('change', (e) => {
|
|
|
|
|
imageSizeLimit.value = e.target.value;
|
|
|
|
|
this.imageSizeLimit = parseInt(e.target.value);
|
|
|
|
|
this.saveSettings();
|
|
|
|
|
});
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (enableBase64Detail && combinedEnableBase64Detail) {
|
|
|
|
|
enableBase64Detail.addEventListener('change', (e) => {
|
|
|
|
|
combinedEnableBase64Detail.checked = e.target.checked;
|
|
|
|
|
this.enableBase64Detail = e.target.checked;
|
|
|
|
|
this.saveSettings();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
combinedEnableBase64Detail.addEventListener('change', (e) => {
|
|
|
|
|
enableBase64Detail.checked = e.target.checked;
|
|
|
|
|
this.enableBase64Detail = e.target.checked;
|
|
|
|
|
this.saveSettings();
|
|
|
|
|
});
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
setupImageUploadSync() {
|
|
|
|
|
// 設置合併模式的圖片上傳功能
|
|
|
|
|
const combinedImageInput = document.getElementById('combinedImageInput');
|
|
|
|
|
const combinedImageUploadArea = document.getElementById('combinedImageUploadArea');
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
if (combinedImageInput && combinedImageUploadArea) {
|
|
|
|
|
// 簡化的圖片上傳同步 - 只需要基本的事件監聽器
|
|
|
|
|
combinedImageInput.addEventListener('change', (e) => {
|
|
|
|
|
this.handleFileSelect(e.target.files);
|
|
|
|
|
});
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
combinedImageUploadArea.addEventListener('click', () => {
|
|
|
|
|
combinedImageInput.click();
|
|
|
|
|
});
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 拖放事件
|
|
|
|
|
combinedImageUploadArea.addEventListener('dragover', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
combinedImageUploadArea.classList.add('dragover');
|
|
|
|
|
});
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
combinedImageUploadArea.addEventListener('dragleave', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
combinedImageUploadArea.classList.remove('dragover');
|
|
|
|
|
});
|
2025-06-05 01:59:56 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
combinedImageUploadArea.addEventListener('drop', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
combinedImageUploadArea.classList.remove('dragover');
|
|
|
|
|
this.handleFileSelect(e.dataTransfer.files);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
resetSettings() {
|
|
|
|
|
localStorage.removeItem('mcp-feedback-settings');
|
|
|
|
|
this.layoutMode = 'separate';
|
|
|
|
|
this.autoClose = false;
|
|
|
|
|
this.currentLanguage = 'zh-TW';
|
|
|
|
|
this.imageSizeLimit = 0;
|
|
|
|
|
this.enableBase64Detail = false;
|
|
|
|
|
this.applySettings();
|
|
|
|
|
this.saveSettings();
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
switchLanguage(lang) {
|
|
|
|
|
this.currentLanguage = lang;
|
|
|
|
|
|
|
|
|
|
// 更新語言選項顯示
|
|
|
|
|
const languageOptions = document.querySelectorAll('.language-option');
|
|
|
|
|
languageOptions.forEach(option => {
|
|
|
|
|
option.classList.toggle('active', option.getAttribute('data-lang') === lang);
|
2025-06-05 01:59:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 通知國際化系統
|
|
|
|
|
if (window.i18nManager) {
|
|
|
|
|
window.i18nManager.setLanguage(lang);
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 19:37:20 +08:00
|
|
|
|
// 同步到 localStorage,確保一致性
|
|
|
|
|
localStorage.setItem('language', lang);
|
|
|
|
|
|
|
|
|
|
// 保存到 ui_settings.json
|
2025-06-06 16:44:24 +08:00
|
|
|
|
this.saveSettings();
|
2025-06-06 19:37:20 +08:00
|
|
|
|
|
|
|
|
|
console.log(`語言已切換到: ${lang}`);
|
2025-06-05 01:59:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
handleCombinedMode() {
|
|
|
|
|
// 處理組合模式的特殊邏輯
|
|
|
|
|
console.log('切換到組合模式');
|
|
|
|
|
|
|
|
|
|
// 同步等待回饋狀態到合併模式
|
|
|
|
|
this.syncFeedbackStatusToCombined();
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 確保合併模式的佈局樣式正確應用
|
|
|
|
|
const combinedTab = document.getElementById('tab-combined');
|
|
|
|
|
if (combinedTab) {
|
|
|
|
|
combinedTab.classList.remove('combined-vertical', 'combined-horizontal');
|
|
|
|
|
if (this.layoutMode === 'combined-vertical') {
|
|
|
|
|
combinedTab.classList.add('combined-vertical');
|
|
|
|
|
} else if (this.layoutMode === 'combined-horizontal') {
|
|
|
|
|
combinedTab.classList.add('combined-horizontal');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-04 21:34:45 +08:00
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
syncFeedbackStatusToCombined() {
|
2025-06-06 21:09:45 +08:00
|
|
|
|
// 新版本:直接調用 updateStatusIndicator() 來同步狀態
|
|
|
|
|
// 因為 updateStatusIndicator() 現在會同時更新兩個狀態指示器
|
|
|
|
|
console.log('🔄 同步狀態指示器到合併模式...');
|
|
|
|
|
// 不需要手動複製,updateStatusIndicator() 會處理所有狀態指示器
|
2025-06-03 15:09:08 +08:00
|
|
|
|
}
|
2025-06-06 16:44:24 +08:00
|
|
|
|
|
|
|
|
|
|
2025-06-03 06:50:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 16:44:24 +08:00
|
|
|
|
// 注意:應用程式由模板中的 initializeApp() 函數初始化
|
|
|
|
|
// 不在此處自動初始化,避免重複實例
|