🔨 重構 app.js 模組化

This commit is contained in:
Minidoracat 2025-06-10 07:19:47 +08:00
parent 47dbdcd96c
commit 1a6c3babe6
14 changed files with 3271 additions and 2678 deletions

View File

@ -1,86 +0,0 @@
# WSL 環境預設使用 Web UI 修復
## 任務描述
修復 WSL 環境中 MCP 服務器錯誤地偵測為可使用 GUI 的問題。WSL 環境應該預設使用 Web UI因為大多數 WSL 安裝都是 Linux 環境,沒有桌面應用支援。
## 問題分析
根據 MCP log 顯示:
```
[SERVER] 偵測到 WSL 環境(通過 /proc/version
[SERVER] WSL 環境不被視為遠端環境
[SERVER] 成功載入 PySide6可使用 GUI
[SERVER] GUI 可用: True
```
問題在於 `can_use_gui()` 函數沒有考慮 WSL 環境的特殊性:
- WSL 環境不被視為遠端環境(正確)
- 但 WSL 環境中即使 PySide6 可以載入,也應該預設使用 Web UI
## 解決方案
採用方案 1`can_use_gui()` 函數中直接檢查 WSL 環境
### 修改內容
1. **文件**`src\mcp_feedback_enhanced\server.py`
2. **函數**`can_use_gui()` (第 203-230 行)
3. **修改邏輯**
- 保持現有的遠端環境檢查
- 在遠端環境檢查後,添加 WSL 環境檢查
- 如果是 WSL 環境,直接返回 `False`
- 保持其餘 PySide6 載入檢查邏輯不變
### 修改前後對比
**修改前**
```python
def can_use_gui() -> bool:
if is_remote_environment():
return False
try:
from PySide6.QtWidgets import QApplication
debug_log("成功載入 PySide6可使用 GUI")
return True
# ...
```
**修改後**
```python
def can_use_gui() -> bool:
if is_remote_environment():
return False
# WSL 環境預設使用 Web UI
if is_wsl_environment():
debug_log("WSL 環境偵測到,預設使用 Web UI")
return False
try:
from PySide6.QtWidgets import QApplication
debug_log("成功載入 PySide6可使用 GUI")
return True
# ...
```
## 預期結果
修改後WSL 環境的 MCP log 應該顯示:
```
[SERVER] 偵測到 WSL 環境(通過 /proc/version
[SERVER] WSL 環境不被視為遠端環境
[SERVER] WSL 環境偵測到,預設使用 Web UI
[SERVER] GUI 可用: False
[SERVER] 建議介面: Web UI
```
## 影響範圍
- ✅ WSL 環境將預設使用 Web UI
- ✅ 不影響其他環境的邏輯
- ✅ 保持向後兼容性
- ✅ 用戶仍可通過 `FORCE_WEB` 環境變數控制介面選擇
## 測試建議
1. 在 WSL 環境中測試 MCP 服務器啟動
2. 驗證日誌顯示正確的環境偵測結果
3. 確認使用 Web UI 而非 GUI
4. 測試 `FORCE_WEB` 環境變數仍然有效
## 完成時間
2025-06-08 01:45:00

View File

@ -1,295 +0,0 @@
# 開發守則
## 專案概述
### 核心定位
- **MCP 服務器專案** - 基於 Model Context Protocol 的互動式回饋增強服務
- **多環境支援** - 本地、SSH Remote、WSL 環境完整支援
- **Web UI 架構** - 現代化響應式網頁介面,支援圖片上傳與多語言
- **Python 模組化設計** - fastmcp + FastAPI + WebSocket 技術棧
### 技術棧識別
- **核心框架**: fastmcp (>=2.0.0), FastAPI (>=0.115.0), WebSocket
- **前端技術**: Jinja2 模板引擎, HTML/CSS/JavaScript, Bootstrap
- **多語言**: i18n 系統支援 en/zh-CN/zh-TW
- **建構工具**: pyproject.toml, uv 包管理器
## 專案架構規範
### 目錄結構邏輯
```
src/mcp_feedback_enhanced/ # 主要源碼模組
├── server.py # MCP 服務器主程式
├── __main__.py # 程式入口點
├── i18n.py # 國際化處理
├── web/ # 網頁應用模組
│ ├── main.py # FastAPI 主應用
│ ├── routes/ # API 路由
│ ├── templates/ # Jinja2 模板
│ ├── static/ # 靜態資源
│ ├── locales/ # 多語言檔案
│ └── utils/ # Web 工具函數
├── testing/ # 測試相關工具
└── utils/ # 通用工具函數
```
### 模組職責分工
- **server.py**: MCP 協議實作,工具函數註冊
- **web/main.py**: FastAPI 應用WebSocket 管理,環境檢測
- **i18n.py**: 語言檢測,翻譯管理,本地化邏輯
- **testing/**: 測試腳本,模擬工具,驗證邏輯
## 多語言文件同步規範
### **🚨 強制同步規則**
#### README 文件三語同步
- **修改 `README.md`** → 必須同步修改 `README.zh-CN.md``README.zh-TW.md`
- **修改 `README.zh-CN.md`** → 必須同步修改 `README.md``README.zh-TW.md`
- **修改 `README.zh-TW.md`** → 必須同步修改 `README.md``README.zh-CN.md`
#### 文檔目錄三語同步
- **修改 `docs/en/`** → 必須同步修改 `docs/zh-CN/``docs/zh-TW/`
- **修改 `docs/zh-CN/`** → 必須同步修改 `docs/en/``docs/zh-TW/`
- **修改 `docs/zh-TW/`** → 必須同步修改 `docs/en/``docs/zh-CN/`
#### 版本更新記錄同步
- **修改 `RELEASE_NOTES/CHANGELOG.en.md`** → 必須同步更新對應的中文版本記錄
- **版本號變更** → 必須同步更新 `pyproject.toml` 中的 version 欄位
#### Web 本地化資源同步
- **修改 `src/mcp_feedback_enhanced/web/locales/en/`** → 必須同步修改 `zh-CN/``zh-TW/`
- **新增翻譯鍵值** → 必須在三種語言的本地化檔案中都提供對應翻譯
- **模板文字變更** → 必須檢查是否影響 i18n 鍵值,如有影響需同步更新
### 語言對應標準
- **en** = English (英文)
- **zh-CN** = Simplified Chinese (简体中文)
- **zh-TW** = Traditional Chinese (繁體中文)
## 代碼規範
### Python 代碼風格
- **類別命名**: PascalCase (例: `FeedbackServer`)
- **函數命名**: snake_case (例: `handle_feedback`)
- **常數命名**: UPPER_SNAKE_CASE (例: `DEFAULT_PORT`)
- **私有成員**: 單底線前綴 (例: `_internal_method`)
### Web 前端規範
- **HTML 模板**: 使用 Jinja2 語法,模板檔案放置於 `web/templates/`
- **CSS 類別**: kebab-case (例: `feedback-container`)
- **JavaScript 函數**: camelCase (例: `handleSubmit`)
- **國際化標記**: 使用 `{{ _('translation_key') }}` 格式
### 檔案命名規範
- **Python 模組**: snake_case.py (例: `test_web_ui.py`)
- **HTML 模板**: kebab-case.html (例: `feedback-form.html`)
- **CSS 檔案**: kebab-case.css (例: `main-styles.css`)
- **JavaScript 檔案**: kebab-case.js (例: `websocket-handler.js`)
## 功能實作規範
### MCP 工具函數實作
- **必須註冊**: 所有工具函數必須在 `server.py` 中使用 `@server.tool()` 裝飾器註冊
- **參數型別**: 使用 `annotated-types` 進行參數驗證
- **錯誤處理**: 必須捕獲並適當處理異常,返回有意義的錯誤訊息
- **文檔字串**: 每個工具函數必須提供清晰的 docstring 說明用途和參數
### Web UI 功能實作
- **路由定義**: API 路由放置於 `web/routes/` 目錄,依功能分類
- **WebSocket 處理**: 連線管理邏輯統一在 `web/main.py` 中實作
- **環境檢測**: 必須支援本地、SSH Remote、WSL 三種環境的自動檢測
- **圖片處理**: 支援 PNG/JPG/JPEG/GIF/BMP/WebP 格式,自動壓縮至 1MB 以下
### 測試規範
- **測試檔案**: 對應源碼檔案,加上 `test_` 前綴 (例: `test_web_ui.py`)
- **測試覆蓋**: 每個公開函數都必須有對應測試案例
- **模擬環境**: 使用 `testing/` 目錄中的工具進行環境模擬
- **異步測試**: 使用 `pytest-asyncio` 進行異步函數測試
## 框架與依賴使用規範
### FastMCP 框架使用
- **服務器初始化**: 使用 `fastmcp.Server()` 建立服務器實例
- **工具註冊**: 使用 `@server.tool()` 裝飾器註冊工具
- **執行模式**: 支援 stdio 和 sse 兩種傳輸模式
### FastAPI 整合規範
- **應用實例**: Web 應用實例定義在 `web/main.py`
- **中介軟體**: 必須設定 CORS 中介軟體支援跨域請求
- **靜態檔案**: 使用 `StaticFiles` 提供 `/static` 路徑服務
- **模板引擎**: 使用 `Jinja2Templates` 處理 HTML 模板渲染
### WebSocket 管理
- **連線池**: 維護活動連線列表,支援多客戶端連線
- **訊息格式**: 使用 JSON 格式進行 WebSocket 通訊
- **心跳檢測**: 實作定期 ping/pong 機制確保連線穩定性
- **錯誤恢復**: 實作自動重連邏輯,處理網路中斷情況
## 工作流程規範
### 開發工作流程
1. **功能開發** → 修改源碼 → 更新測試 → 運行測試套件
2. **文檔更新** → 同步修改三語文檔 → 檢查連結有效性
3. **版本發布** → 更新版本號 → 更新 CHANGELOG → 測試完整功能
4. **多語言更新** → 更新翻譯鍵值 → 同步三語本地化檔案
### 測試工作流程
```bash
# 標準測試流程
uv run python -m mcp_feedback_enhanced test # 基本功能測試
uv run python -m mcp_feedback_enhanced test --web # Web UI 測試
uv run python -m mcp_feedback_enhanced test --enhanced # 完整測試套件
```
### 環境檢測流程
1. **系統檢測** → 識別作業系統 (Windows/Linux/macOS)
2. **環境判斷** → 檢測 SSH_CLIENT/WSL 環境變數
3. **瀏覽器啟動** → 根據環境選擇適當的啟動方式
4. **錯誤處理** → 提供環境特定的解決方案指引
## 關鍵檔案交互規範
### 版本管理檔案連動
- **修改 `pyproject.toml` version** → 必須同步檢查並更新:
- `src/mcp_feedback_enhanced/__init__.py` 中的 `__version__`
- `RELEASE_NOTES/` 目錄下的對應版本記錄
- README 文件中的版本參考
### 配置檔案管理
- **修改預設配置** → 必須同步更新:
- `src/mcp_feedback_enhanced/__main__.py` 中的預設值
- README 文件中的配置說明
- 測試檔案中的配置模擬
### 模板與本地化連動
- **修改 HTML 模板** → 檢查並更新:
- `web/locales/` 中對應的翻譯鍵值
- `i18n.py` 中的語言檢測邏輯
- CSS/JavaScript 檔案中的對應功能
### 路由與靜態資源連動
- **新增 API 路由** → 必須同步:
- 在 `web/main.py` 中註冊路由
- 更新前端 JavaScript 的 API 呼叫
- 新增對應的測試案例
## AI 決策規範
### 優先級判斷標準
1. **安全性優先** - 任何涉及安全性的修改必須優先考慮
2. **多語言一致性** - 確保三種語言版本功能和內容一致
3. **向後兼容性** - 新功能不應破壞現有的 MCP 整合
4. **跨平台兼容** - 修改必須同時考慮 Windows/Linux/macOS 環境
### 模糊情況處理決策樹
```
環境相關問題:
├── SSH Remote 環境 → 提供瀏覽器手動啟動指引
├── WSL 環境 → 使用 Windows 瀏覽器啟動方案
├── 本地環境 → 直接啟動系統預設瀏覽器
└── 未知環境 → 使用 Web UI 作為備用方案
多語言處理:
├── 新增功能 → 必須提供三語支援
├── 修改文案 → 必須同步更新三語版本
├── 錯誤訊息 → 必須國際化處理
└── 使用者介面 → 支援即時語言切換
版本控制:
├── 主要功能更新 → 提升 minor 版本號
├── 重大架構變更 → 提升 major 版本號
├── 錯誤修正 → 提升 patch 版本號
└── 文檔更新 → 不變更版本號
```
### 錯誤處理決策原則
- **網路連線問題** → 提供重試機制與手動解決方案
- **瀏覽器啟動失敗** → 根據環境提供特定解決指引
- **WebSocket 中斷** → 自動重連並通知使用者
- **檔案讀取錯誤** → 提供詳細錯誤訊息與修復建議
## 禁止事項
### **🚫 絕對禁止**
#### 多語言文件管理
- **禁止單獨修改任一語言的 README** - 必須同步更新三種語言版本
- **禁止使用機器翻譯進行粗略翻譯** - 必須確保翻譯品質和專業用詞
- **禁止破壞現有的 i18n 鍵值結構** - 新增或修改必須保持一致性
- **禁止在程式碼中硬編碼特定語言文字** - 必須使用 i18n 系統
#### 架構與相容性
- **禁止修改 MCP 協議的核心實作** - 避免破壞與 MCP 客戶端的兼容性
- **禁止移除環境檢測功能** - 必須保持多環境支援能力
- **禁止破壞 WebSocket 連線機制** - 避免影響即時通訊功能
- **禁止修改預設 port 8765** - 除非有明確需求並更新所有相關文檔
#### 測試與品質保證
- **禁止提交未經測試的代碼** - 必須通過完整測試套件
- **禁止跳過異常處理** - 所有可能的錯誤情況都必須妥善處理
- **禁止移除現有的測試案例** - 除非對應功能已被移除
- **禁止使用不安全的檔案操作** - 必須驗證路徑和權限
#### 使用者體驗
- **禁止移除圖片上傳功能** - 這是核心功能之一
- **禁止破壞響應式設計** - 必須支援不同螢幕尺寸
- **禁止移除鍵盤快捷鍵** - 維持良好的使用者體驗
- **禁止硬編碼環境特定路徑** - 必須使用動態檢測和配置
### **⚠️ 謹慎處理**
#### 依賴管理
- **謹慎升級核心依賴** - fastmcp, FastAPI 等核心依賴升級需全面測試
- **謹慎新增新依賴** - 評估必要性,避免依賴膨脹
- **謹慎修改 pyproject.toml** - 確保不破壞建構和安裝流程
#### 效能相關
- **謹慎修改 WebSocket 訊息處理邏輯** - 可能影響即時性
- **謹慎修改圖片處理演算法** - 可能影響效能和記憶體使用
- **謹慎修改環境檢測邏輯** - 可能影響多平台支援
### 開發範例指引
#### ✅ 正確做法範例
```python
# 正確的工具函數實作
@server.tool()
async def interactive_feedback(
summary: Annotated[str, "AI work summary"],
timeout: Annotated[int, "Timeout in seconds"] = 600
) -> list[types.TextContent | types.ImageContent]:
"""正確的參數驗證和錯誤處理"""
try:
# 實作邏輯
return await handle_feedback(summary, timeout)
except Exception as e:
logger.error(f"Feedback error: {e}")
return [types.TextContent(text=f"Error: {str(e)}")]
```
#### ❌ 錯誤做法範例
```python
# 錯誤:缺少錯誤處理和型別註解
@server.tool()
def bad_feedback(summary, timeout=600):
# 直接處理,沒有異常捕獲
return handle_feedback(summary, timeout)
```
#### ✅ 正確的多語言檔案同步
```bash
# 修改 README.md 後的正確流程
1. 修改 README.md (英文版)
2. 同步修改 README.zh-CN.md (簡體中文版)
3. 同步修改 README.zh-TW.md (繁體中文版)
4. 檢查三個檔案的結構和內容一致性
```
#### ❌ 錯誤的多語言處理
```bash
# 錯誤:只修改單一語言檔案
1. 修改 README.md ❌
2. 沒有同步其他語言版本 ❌
3. 造成文檔不一致 ❌
```

View File

@ -77,7 +77,7 @@ def sync_language_from_web_ui():
def get_test_summary():
"""獲取測試摘要,使用國際化系統"""
return t('test.webUiSummary')
return t('dynamic.aiSummary')
def find_free_port():
"""Find a free port to use for testing"""
@ -193,7 +193,7 @@ def test_web_ui(keep_running=False):
try:
project_dir = str(Path.cwd())
# 使用國際化系統獲取測試摘要
summary = t('test.webUiSummary')
summary = t('dynamic.aiSummary')
session_id = manager.create_session(project_dir, summary)
session_info = {
'manager': manager,

File diff suppressed because it is too large Load Diff

View File

@ -128,11 +128,8 @@ class I18nManager {
localStorage.setItem('language', language);
this.applyTranslations();
// 更新語言選擇器(只更新設定頁面的)
const selector = document.getElementById('settingsLanguageSelect');
if (selector) {
selector.value = language;
}
// 更新所有語言選擇器(包括現代化版本)
this.setupLanguageSelectors();
// 更新 HTML lang 屬性
document.documentElement.lang = language;
@ -174,23 +171,32 @@ class I18nManager {
// 只更新終端歡迎信息,不要覆蓋 AI 摘要
this.updateTerminalWelcome();
// 更新應用程式中的動態狀態文字
if (window.feedbackApp) {
window.feedbackApp.updateUIState();
window.feedbackApp.updateStatusIndicator();
// 更新應用程式中的動態狀態文字(使用新的模組化架構)
if (window.feedbackApp && window.feedbackApp.isInitialized) {
// 更新 UI 狀態
if (window.feedbackApp.uiManager && typeof window.feedbackApp.uiManager.updateUIState === 'function') {
window.feedbackApp.uiManager.updateUIState();
}
if (window.feedbackApp.uiManager && typeof window.feedbackApp.uiManager.updateStatusIndicator === 'function') {
window.feedbackApp.uiManager.updateStatusIndicator();
}
// 更新自動檢測狀態文字
if (window.feedbackApp.updateAutoRefreshStatus) {
window.feedbackApp.updateAutoRefreshStatus();
if (window.feedbackApp.autoRefreshManager && typeof window.feedbackApp.autoRefreshManager.updateAutoRefreshStatus === 'function') {
window.feedbackApp.autoRefreshManager.updateAutoRefreshStatus();
}
}
}
updateTerminalWelcome() {
const commandOutput = document.getElementById('commandOutput');
if (commandOutput && window.feedbackApp) {
if (commandOutput && window.feedbackApp && window.feedbackApp.isInitialized) {
const welcomeTemplate = this.t('dynamic.terminalWelcome');
if (welcomeTemplate && welcomeTemplate !== 'dynamic.terminalWelcome') {
const welcomeMessage = welcomeTemplate.replace('{sessionId}', window.feedbackApp.sessionId || 'unknown');
// 使用 currentSessionId 而不是 sessionId
const sessionId = window.feedbackApp.currentSessionId || window.feedbackApp.sessionId || 'unknown';
const welcomeMessage = welcomeTemplate.replace('{sessionId}', sessionId);
commandOutput.textContent = welcomeMessage;
}
}
@ -212,7 +218,7 @@ class I18nManager {
// 新版現代化語言選擇器
const languageOptions = document.querySelectorAll('.language-option');
if (languageOptions.length > 0) {
// 設置當前語言的活躍狀態
// 設置當前語言的活躍狀態和點擊事件
languageOptions.forEach(option => {
const lang = option.getAttribute('data-lang');
if (lang === this.currentLanguage) {
@ -220,6 +226,15 @@ class I18nManager {
} else {
option.classList.remove('active');
}
// 移除舊的事件監聽器(如果存在)
option.removeEventListener('click', option._languageClickHandler);
// 添加新的點擊事件監聽器
option._languageClickHandler = () => {
this.setLanguage(lang);
};
option.addEventListener('click', option._languageClickHandler);
});
}
}

View File

@ -0,0 +1,351 @@
/**
* MCP Feedback Enhanced - 自動刷新管理模組
* =======================================
*
* 處理自動檢測會話更新和頁面內容刷新
*/
(function() {
'use strict';
// 確保命名空間和依賴存在
window.MCPFeedback = window.MCPFeedback || {};
const Utils = window.MCPFeedback.Utils;
/**
* 自動刷新管理器建構函數
*/
function AutoRefreshManager(options) {
options = options || {};
// 設定
this.autoRefreshEnabled = options.autoRefreshEnabled || false;
this.autoRefreshInterval = options.autoRefreshInterval || 5; // 秒
this.lastKnownSessionId = options.lastKnownSessionId || null;
// 定時器
this.autoRefreshTimer = null;
// UI 元素
this.autoRefreshCheckbox = null;
this.autoRefreshIntervalInput = null;
this.refreshStatusIndicator = null;
this.refreshStatusText = null;
// 回調函數
this.onSessionUpdate = options.onSessionUpdate || null;
this.onSettingsChange = options.onSettingsChange || null;
this.initUIElements();
}
/**
* 初始化 UI 元素
*/
AutoRefreshManager.prototype.initUIElements = function() {
this.autoRefreshCheckbox = Utils.safeQuerySelector('#autoRefreshEnabled');
this.autoRefreshIntervalInput = Utils.safeQuerySelector('#autoRefreshInterval');
this.refreshStatusIndicator = Utils.safeQuerySelector('#refreshStatusIndicator');
this.refreshStatusText = Utils.safeQuerySelector('#refreshStatusText');
console.log('🔄 自動刷新 UI 元素初始化完成');
};
/**
* 初始化自動刷新功能
*/
AutoRefreshManager.prototype.init = function() {
console.log('🔄 初始化自動刷新功能...');
if (!this.autoRefreshCheckbox || !this.autoRefreshIntervalInput) {
console.warn('⚠️ 自動刷新元素不存在,跳過初始化');
return;
}
this.setupEventListeners();
this.applySettings();
// 延遲更新狀態指示器,確保 i18n 已完全載入
const self = this;
setTimeout(function() {
self.updateAutoRefreshStatus();
if (self.autoRefreshEnabled) {
console.log('🔄 自動刷新已啟用,啟動自動檢測...');
self.startAutoRefresh();
}
}, 100);
console.log('✅ 自動刷新功能初始化完成');
};
/**
* 設置事件監聽器
*/
AutoRefreshManager.prototype.setupEventListeners = function() {
const self = this;
// 設置開關事件監聽器
this.autoRefreshCheckbox.addEventListener('change', function(e) {
self.autoRefreshEnabled = e.target.checked;
self.handleAutoRefreshToggle();
if (self.onSettingsChange) {
self.onSettingsChange();
}
});
// 設置間隔輸入事件監聽器
this.autoRefreshIntervalInput.addEventListener('change', function(e) {
const newInterval = parseInt(e.target.value);
if (newInterval >= 5 && newInterval <= 300) {
self.autoRefreshInterval = newInterval;
if (self.onSettingsChange) {
self.onSettingsChange();
}
// 如果自動刷新已啟用,重新啟動定時器
if (self.autoRefreshEnabled) {
self.stopAutoRefresh();
self.startAutoRefresh();
}
}
});
};
/**
* 應用設定
*/
AutoRefreshManager.prototype.applySettings = function() {
if (this.autoRefreshCheckbox) {
this.autoRefreshCheckbox.checked = this.autoRefreshEnabled;
}
if (this.autoRefreshIntervalInput) {
this.autoRefreshIntervalInput.value = this.autoRefreshInterval;
}
};
/**
* 處理自動刷新開關切換
*/
AutoRefreshManager.prototype.handleAutoRefreshToggle = function() {
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
this.updateAutoRefreshStatus();
};
/**
* 啟動自動刷新
*/
AutoRefreshManager.prototype.startAutoRefresh = function() {
if (this.autoRefreshTimer) {
clearInterval(this.autoRefreshTimer);
}
const self = this;
this.autoRefreshTimer = setInterval(function() {
self.checkForSessionUpdate();
}, this.autoRefreshInterval * 1000);
console.log('🔄 自動刷新已啟動,間隔: ' + this.autoRefreshInterval + '秒');
};
/**
* 停止自動刷新
*/
AutoRefreshManager.prototype.stopAutoRefresh = function() {
if (this.autoRefreshTimer) {
clearInterval(this.autoRefreshTimer);
this.autoRefreshTimer = null;
}
console.log('⏸️ 自動刷新已停止');
};
/**
* 檢查會話更新
*/
AutoRefreshManager.prototype.checkForSessionUpdate = function() {
const self = this;
this.updateAutoRefreshStatus('checking');
fetch('/api/current-session')
.then(function(response) {
if (!response.ok) {
throw new Error('API 請求失敗: ' + response.status);
}
return response.json();
})
.then(function(sessionData) {
// 檢查會話 ID 是否變化
if (sessionData.session_id && sessionData.session_id !== self.lastKnownSessionId) {
console.log('🔄 檢測到新會話: ' + self.lastKnownSessionId + ' -> ' + sessionData.session_id);
// 更新記錄的會話 ID
self.lastKnownSessionId = sessionData.session_id;
// 觸發會話更新回調
if (self.onSessionUpdate) {
self.onSessionUpdate(sessionData);
}
self.updateAutoRefreshStatus('detected');
// 短暫顯示檢測成功狀態,然後恢復為檢測中
setTimeout(function() {
if (self.autoRefreshEnabled) {
self.updateAutoRefreshStatus('enabled');
}
}, 2000);
} else {
self.updateAutoRefreshStatus('enabled');
}
})
.catch(function(error) {
console.error('❌ 自動刷新檢測失敗:', error);
self.updateAutoRefreshStatus('error');
// 短暫顯示錯誤狀態,然後恢復
setTimeout(function() {
if (self.autoRefreshEnabled) {
self.updateAutoRefreshStatus('enabled');
}
}, 3000);
});
};
/**
* 更新自動刷新狀態指示器
*/
AutoRefreshManager.prototype.updateAutoRefreshStatus = function(status) {
status = status || (this.autoRefreshEnabled ? 'enabled' : 'disabled');
console.log('🔧 updateAutoRefreshStatus 被調用status: ' + status);
console.log('🔧 refreshStatusIndicator: ' + (this.refreshStatusIndicator ? 'found' : 'null'));
console.log('🔧 refreshStatusText: ' + (this.refreshStatusText ? 'found' : 'null'));
if (!this.refreshStatusIndicator || !this.refreshStatusText) {
console.log('⚠️ 自動檢測狀態元素未找到,跳過更新');
return;
}
let indicator = '⏸️';
let textKey = 'autoRefresh.disabled';
switch (status) {
case 'enabled':
indicator = '🔄';
textKey = 'autoRefresh.enabled';
break;
case 'checking':
indicator = '🔍';
textKey = 'autoRefresh.checking';
break;
case 'detected':
indicator = '✅';
textKey = 'autoRefresh.detected';
break;
case 'error':
indicator = '❌';
textKey = 'autoRefresh.error';
break;
case 'disabled':
default:
indicator = '⏸️';
textKey = 'autoRefresh.disabled';
break;
}
this.refreshStatusIndicator.textContent = indicator;
// 使用多語系翻譯
if (window.i18nManager) {
const translatedText = window.i18nManager.t(textKey);
console.log('🔄 自動檢測狀態翻譯: ' + textKey + ' -> ' + translatedText + ' (語言: ' + window.i18nManager.currentLanguage + ')');
this.refreshStatusText.textContent = translatedText;
} else {
// 備用翻譯
const fallbackTexts = {
'autoRefresh.enabled': '已啟用',
'autoRefresh.checking': '檢測中...',
'autoRefresh.detected': '檢測到更新',
'autoRefresh.error': '檢測錯誤',
'autoRefresh.disabled': '已停用'
};
this.refreshStatusText.textContent = fallbackTexts[textKey] || '未知狀態';
}
};
/**
* 更新設定
*/
AutoRefreshManager.prototype.updateSettings = function(settings) {
if (settings.autoRefreshEnabled !== undefined) {
this.autoRefreshEnabled = settings.autoRefreshEnabled;
}
if (settings.autoRefreshInterval !== undefined) {
this.autoRefreshInterval = settings.autoRefreshInterval;
}
this.applySettings();
// 根據新設定調整自動刷新狀態
if (this.autoRefreshEnabled && !this.autoRefreshTimer) {
this.startAutoRefresh();
} else if (!this.autoRefreshEnabled && this.autoRefreshTimer) {
this.stopAutoRefresh();
}
this.updateAutoRefreshStatus();
};
/**
* 設置最後已知會話 ID
*/
AutoRefreshManager.prototype.setLastKnownSessionId = function(sessionId) {
this.lastKnownSessionId = sessionId;
};
/**
* 獲取當前設定
*/
AutoRefreshManager.prototype.getSettings = function() {
return {
autoRefreshEnabled: this.autoRefreshEnabled,
autoRefreshInterval: this.autoRefreshInterval
};
};
/**
* 檢查是否已啟用
*/
AutoRefreshManager.prototype.isEnabled = function() {
return this.autoRefreshEnabled;
};
/**
* 手動觸發檢查
*/
AutoRefreshManager.prototype.manualCheck = function() {
if (!this.autoRefreshEnabled) {
console.log('🔄 手動觸發會話檢查...');
this.checkForSessionUpdate();
}
};
/**
* 清理資源
*/
AutoRefreshManager.prototype.cleanup = function() {
this.stopAutoRefresh();
console.log('🧹 自動刷新管理器已清理');
};
// 將 AutoRefreshManager 加入命名空間
window.MCPFeedback.AutoRefreshManager = AutoRefreshManager;
console.log('✅ AutoRefreshManager 模組載入完成');
})();

View File

@ -0,0 +1,490 @@
/**
* MCP Feedback Enhanced - 圖片處理模組
* ==================================
*
* 處理圖片上傳預覽壓縮和管理功能
*/
(function() {
'use strict';
// 確保命名空間和依賴存在
window.MCPFeedback = window.MCPFeedback || {};
const Utils = window.MCPFeedback.Utils;
/**
* 圖片處理器建構函數
*/
function ImageHandler(options) {
options = options || {};
this.images = [];
this.imageSizeLimit = options.imageSizeLimit || 0;
this.enableBase64Detail = options.enableBase64Detail || false;
this.layoutMode = options.layoutMode || 'combined-vertical';
this.currentImagePrefix = '';
// UI 元素
this.imageInput = null;
this.imageUploadArea = null;
this.imagePreviewContainer = null;
this.imageSizeLimitSelect = null;
this.enableBase64DetailCheckbox = null;
// 事件處理器
this.imageChangeHandler = null;
this.imageClickHandler = null;
this.imageDragOverHandler = null;
this.imageDragLeaveHandler = null;
this.imageDropHandler = null;
this.pasteHandler = null;
// 回調函數
this.onSettingsChange = options.onSettingsChange || null;
}
/**
* 初始化圖片處理器
*/
ImageHandler.prototype.init = function() {
console.log('🖼️ 開始初始化圖片處理功能...');
this.initImageElements();
this.setupImageEventListeners();
this.setupGlobalPasteHandler();
console.log('✅ 圖片處理功能初始化完成');
};
/**
* 動態初始化圖片相關元素
*/
ImageHandler.prototype.initImageElements = function() {
const prefix = this.layoutMode && this.layoutMode.startsWith('combined') ? 'combined' : 'feedback';
console.log('🖼️ 初始化圖片元素,使用前綴: ' + prefix);
this.imageInput = Utils.safeQuerySelector('#' + prefix + 'ImageInput') ||
Utils.safeQuerySelector('#imageInput');
this.imageUploadArea = Utils.safeQuerySelector('#' + prefix + 'ImageUploadArea') ||
Utils.safeQuerySelector('#imageUploadArea');
this.imagePreviewContainer = Utils.safeQuerySelector('#' + prefix + 'ImagePreviewContainer') ||
Utils.safeQuerySelector('#imagePreviewContainer');
this.imageSizeLimitSelect = Utils.safeQuerySelector('#' + prefix + 'ImageSizeLimit') ||
Utils.safeQuerySelector('#imageSizeLimit');
this.enableBase64DetailCheckbox = Utils.safeQuerySelector('#' + prefix + 'EnableBase64Detail') ||
Utils.safeQuerySelector('#enableBase64Detail');
this.currentImagePrefix = prefix;
if (!this.imageInput || !this.imageUploadArea) {
console.warn('⚠️ 圖片元素初始化失敗 - imageInput: ' + !!this.imageInput + ', imageUploadArea: ' + !!this.imageUploadArea);
} else {
console.log('✅ 圖片元素初始化成功 - 前綴: ' + prefix);
}
};
/**
* 設置圖片事件監聽器
*/
ImageHandler.prototype.setupImageEventListeners = function() {
if (!this.imageInput || !this.imageUploadArea) {
console.warn('⚠️ 缺少必要的圖片元素,跳過事件監聽器設置');
return;
}
console.log('🖼️ 設置圖片事件監聽器 - imageInput: ' + this.imageInput.id + ', imageUploadArea: ' + this.imageUploadArea.id);
// 移除舊的事件監聽器
this.removeImageEventListeners();
const self = this;
// 文件選擇事件
this.imageChangeHandler = function(e) {
console.log('📁 文件選擇事件觸發 - input: ' + e.target.id + ', files: ' + e.target.files.length);
self.handleFileSelect(e.target.files);
};
this.imageInput.addEventListener('change', this.imageChangeHandler);
// 點擊上傳區域
this.imageClickHandler = function(e) {
e.preventDefault();
e.stopPropagation();
if (self.imageInput) {
console.log('🖱️ 點擊上傳區域 - 觸發 input: ' + self.imageInput.id);
self.imageInput.click();
}
};
this.imageUploadArea.addEventListener('click', this.imageClickHandler);
// 拖放事件
this.imageDragOverHandler = function(e) {
e.preventDefault();
self.imageUploadArea.classList.add('dragover');
};
this.imageUploadArea.addEventListener('dragover', this.imageDragOverHandler);
this.imageDragLeaveHandler = function(e) {
e.preventDefault();
self.imageUploadArea.classList.remove('dragover');
};
this.imageUploadArea.addEventListener('dragleave', this.imageDragLeaveHandler);
this.imageDropHandler = function(e) {
e.preventDefault();
self.imageUploadArea.classList.remove('dragover');
self.handleFileSelect(e.dataTransfer.files);
};
this.imageUploadArea.addEventListener('drop', this.imageDropHandler);
// 初始化圖片設定事件
this.initImageSettings();
};
/**
* 設置全域剪貼板貼上事件
*/
ImageHandler.prototype.setupGlobalPasteHandler = function() {
if (this.pasteHandler) {
return; // 已經設置過了
}
const self = this;
this.pasteHandler = function(e) {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.indexOf('image') !== -1) {
e.preventDefault();
const file = item.getAsFile();
self.handleFileSelect([file]);
break;
}
}
};
document.addEventListener('paste', this.pasteHandler);
console.log('✅ 全域剪貼板貼上事件已設置');
};
/**
* 移除圖片事件監聽器
*/
ImageHandler.prototype.removeImageEventListeners = function() {
if (this.imageInput && this.imageChangeHandler) {
this.imageInput.removeEventListener('change', this.imageChangeHandler);
}
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);
}
}
};
/**
* 初始化圖片設定事件
*/
ImageHandler.prototype.initImageSettings = function() {
const self = this;
if (this.imageSizeLimitSelect) {
this.imageSizeLimitSelect.addEventListener('change', function(e) {
self.imageSizeLimit = parseInt(e.target.value);
if (self.onSettingsChange) {
self.onSettingsChange();
}
});
}
if (this.enableBase64DetailCheckbox) {
this.enableBase64DetailCheckbox.addEventListener('change', function(e) {
self.enableBase64Detail = e.target.checked;
if (self.onSettingsChange) {
self.onSettingsChange();
}
});
}
};
/**
* 處理文件選擇
*/
ImageHandler.prototype.handleFileSelect = function(files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type.startsWith('image/')) {
this.addImage(file);
}
}
};
/**
* 添加圖片
*/
ImageHandler.prototype.addImage = function(file) {
// 檢查文件大小
if (this.imageSizeLimit > 0 && file.size > this.imageSizeLimit) {
Utils.showMessage('圖片大小超過限制 (' + Utils.formatFileSize(this.imageSizeLimit) + ')', Utils.CONSTANTS.MESSAGE_WARNING);
return;
}
const self = this;
this.fileToBase64(file)
.then(function(base64) {
const imageData = {
name: file.name,
size: file.size,
type: file.type,
data: base64
};
self.images.push(imageData);
self.updateImagePreview();
})
.catch(function(error) {
console.error('圖片處理失敗:', error);
Utils.showMessage('圖片處理失敗,請重試', Utils.CONSTANTS.MESSAGE_ERROR);
});
};
/**
* 將文件轉換為 Base64
*/
ImageHandler.prototype.fileToBase64 = function(file) {
return new Promise(function(resolve, reject) {
const reader = new FileReader();
reader.onload = function() {
resolve(reader.result.split(',')[1]);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
/**
* 更新圖片預覽
*/
ImageHandler.prototype.updateImagePreview = function() {
const previewContainers = [
Utils.safeQuerySelector('#feedbackImagePreviewContainer'),
Utils.safeQuerySelector('#combinedImagePreviewContainer'),
this.imagePreviewContainer
].filter(function(container) {
return container !== null;
});
if (previewContainers.length === 0) {
console.warn('⚠️ 沒有找到圖片預覽容器');
return;
}
console.log('🖼️ 更新 ' + previewContainers.length + ' 個圖片預覽容器');
const self = this;
previewContainers.forEach(function(container) {
container.innerHTML = '';
self.images.forEach(function(image, index) {
const preview = self.createImagePreviewElement(image, index);
container.appendChild(preview);
});
});
this.updateImageCount();
};
/**
* 創建圖片預覽元素
*/
ImageHandler.prototype.createImagePreviewElement = function(image, index) {
const self = this;
// 創建圖片預覽項目容器
const preview = document.createElement('div');
preview.className = 'image-preview-item';
preview.style.cssText = 'position: relative; display: inline-block;';
// 創建圖片元素
const img = document.createElement('img');
img.src = 'data:' + image.type + ';base64,' + image.data;
img.alt = image.name;
img.style.cssText = 'width: 80px; height: 80px; object-fit: cover; display: block; border-radius: 6px;';
// 創建圖片信息容器
const imageInfo = document.createElement('div');
imageInfo.className = 'image-info';
imageInfo.style.cssText = `
position: absolute; bottom: 0; left: 0; right: 0;
background: rgba(0, 0, 0, 0.7); color: white; padding: 4px;
font-size: 10px; line-height: 1.2;
`;
const imageName = document.createElement('div');
imageName.className = 'image-name';
imageName.textContent = image.name;
imageName.style.cssText = 'font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
const imageSize = document.createElement('div');
imageSize.className = 'image-size';
imageSize.textContent = Utils.formatFileSize(image.size);
imageSize.style.cssText = 'font-size: 9px; opacity: 0.8;';
// 創建刪除按鈕
const removeBtn = document.createElement('button');
removeBtn.className = 'image-remove-btn';
removeBtn.textContent = '×';
removeBtn.title = '移除圖片';
removeBtn.style.cssText = `
position: absolute; top: -8px; right: -8px; width: 20px; height: 20px;
border-radius: 50%; background: #f44336; color: white; border: none;
cursor: pointer; font-size: 12px; font-weight: bold;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; z-index: 10;
`;
// 添加刪除按鈕懸停效果
removeBtn.addEventListener('mouseenter', function() {
removeBtn.style.background = '#d32f2f';
removeBtn.style.transform = 'scale(1.1)';
});
removeBtn.addEventListener('mouseleave', function() {
removeBtn.style.background = '#f44336';
removeBtn.style.transform = 'scale(1)';
});
// 添加刪除功能
removeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
self.removeImage(index);
});
// 組裝元素
imageInfo.appendChild(imageName);
imageInfo.appendChild(imageSize);
preview.appendChild(img);
preview.appendChild(imageInfo);
preview.appendChild(removeBtn);
return preview;
};
/**
* 更新圖片計數顯示
*/
ImageHandler.prototype.updateImageCount = function() {
const count = this.images.length;
const countElements = document.querySelectorAll('.image-count');
countElements.forEach(function(element) {
element.textContent = count > 0 ? '(' + count + ')' : '';
});
// 更新上傳區域的顯示狀態
const uploadAreas = [
Utils.safeQuerySelector('#feedbackImageUploadArea'),
Utils.safeQuerySelector('#combinedImageUploadArea')
].filter(function(area) {
return area !== null;
});
uploadAreas.forEach(function(area) {
if (count > 0) {
area.classList.add('has-images');
} else {
area.classList.remove('has-images');
}
});
};
/**
* 移除圖片
*/
ImageHandler.prototype.removeImage = function(index) {
this.images.splice(index, 1);
this.updateImagePreview();
};
/**
* 清空所有圖片
*/
ImageHandler.prototype.clearImages = function() {
this.images = [];
this.updateImagePreview();
};
/**
* 獲取圖片數據
*/
ImageHandler.prototype.getImages = function() {
return Utils.deepClone(this.images);
};
/**
* 重新初始化用於佈局模式切換
*/
ImageHandler.prototype.reinitialize = function(layoutMode) {
console.log('🔄 重新初始化圖片處理功能...');
this.layoutMode = layoutMode;
this.removeImageEventListeners();
this.initImageElements();
if (this.imageUploadArea && this.imageInput) {
this.setupImageEventListeners();
console.log('✅ 圖片處理功能重新初始化完成');
} else {
console.warn('⚠️ 圖片處理重新初始化失敗 - 缺少必要元素');
}
this.updateImagePreview();
};
/**
* 更新設定
*/
ImageHandler.prototype.updateSettings = function(settings) {
this.imageSizeLimit = settings.imageSizeLimit || 0;
this.enableBase64Detail = settings.enableBase64Detail || false;
// 同步到 UI 元素
if (this.imageSizeLimitSelect) {
this.imageSizeLimitSelect.value = this.imageSizeLimit.toString();
}
if (this.enableBase64DetailCheckbox) {
this.enableBase64DetailCheckbox.checked = this.enableBase64Detail;
}
};
/**
* 清理資源
*/
ImageHandler.prototype.cleanup = function() {
this.removeImageEventListeners();
if (this.pasteHandler) {
document.removeEventListener('paste', this.pasteHandler);
this.pasteHandler = null;
}
this.clearImages();
};
// 將 ImageHandler 加入命名空間
window.MCPFeedback.ImageHandler = ImageHandler;
console.log('✅ ImageHandler 模組載入完成');
})();

View File

@ -0,0 +1,464 @@
/**
* MCP Feedback Enhanced - 設定管理模組
* ==================================
*
* 處理應用程式設定的載入保存和同步
*/
(function() {
'use strict';
// 確保命名空間和依賴存在
window.MCPFeedback = window.MCPFeedback || {};
const Utils = window.MCPFeedback.Utils;
/**
* 設定管理器建構函數
*/
function SettingsManager(options) {
options = options || {};
// 預設設定
this.defaultSettings = {
layoutMode: 'combined-vertical',
autoClose: false,
language: 'zh-TW',
imageSizeLimit: 0,
enableBase64Detail: false,
autoRefreshEnabled: false,
autoRefreshInterval: 5,
activeTab: 'combined'
};
// 當前設定
this.currentSettings = Utils.deepClone(this.defaultSettings);
// 回調函數
this.onSettingsChange = options.onSettingsChange || null;
this.onLanguageChange = options.onLanguageChange || null;
}
/**
* 載入設定
*/
SettingsManager.prototype.loadSettings = function() {
const self = this;
return new Promise(function(resolve, reject) {
console.log('開始載入設定...');
// 優先從伺服器端載入設定
self.loadFromServer()
.then(function(serverSettings) {
if (serverSettings && Object.keys(serverSettings).length > 0) {
self.currentSettings = self.mergeSettings(self.defaultSettings, serverSettings);
console.log('從伺服器端載入設定成功:', self.currentSettings);
// 同步到 localStorage
self.saveToLocalStorage();
resolve(self.currentSettings);
} else {
// 回退到 localStorage
return self.loadFromLocalStorage();
}
})
.then(function(localSettings) {
if (localSettings) {
self.currentSettings = self.mergeSettings(self.defaultSettings, localSettings);
console.log('從 localStorage 載入設定:', self.currentSettings);
} else {
console.log('沒有找到設定,使用預設值');
}
resolve(self.currentSettings);
})
.catch(function(error) {
console.error('載入設定失敗:', error);
self.currentSettings = Utils.deepClone(self.defaultSettings);
resolve(self.currentSettings);
});
});
};
/**
* 從伺服器載入設定
*/
SettingsManager.prototype.loadFromServer = function() {
return fetch('/api/load-settings')
.then(function(response) {
if (response.ok) {
return response.json();
} else {
throw new Error('伺服器回應錯誤: ' + response.status);
}
})
.catch(function(error) {
console.warn('從伺服器端載入設定失敗:', error);
return null;
});
};
/**
* localStorage 載入設定
*/
SettingsManager.prototype.loadFromLocalStorage = function() {
if (!Utils.isLocalStorageSupported()) {
return Promise.resolve(null);
}
try {
const localSettings = localStorage.getItem('mcp-feedback-settings');
if (localSettings) {
const parsed = Utils.safeJsonParse(localSettings, null);
console.log('從 localStorage 載入設定:', parsed);
return Promise.resolve(parsed);
}
} catch (error) {
console.warn('從 localStorage 載入設定失敗:', error);
}
return Promise.resolve(null);
};
/**
* 保存設定
*/
SettingsManager.prototype.saveSettings = function(newSettings) {
if (newSettings) {
this.currentSettings = this.mergeSettings(this.currentSettings, newSettings);
}
console.log('保存設定:', this.currentSettings);
// 保存到 localStorage
this.saveToLocalStorage();
// 同步保存到伺服器端
this.saveToServer();
// 觸發回調
if (this.onSettingsChange) {
this.onSettingsChange(this.currentSettings);
}
return this.currentSettings;
};
/**
* 保存到 localStorage
*/
SettingsManager.prototype.saveToLocalStorage = function() {
if (!Utils.isLocalStorageSupported()) {
return;
}
try {
localStorage.setItem('mcp-feedback-settings', JSON.stringify(this.currentSettings));
} catch (error) {
console.error('保存設定到 localStorage 失敗:', error);
}
};
/**
* 保存到伺服器
*/
SettingsManager.prototype.saveToServer = function() {
fetch('/api/save-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(this.currentSettings)
})
.then(function(response) {
if (response.ok) {
console.log('設定已同步到伺服器端');
} else {
console.warn('同步設定到伺服器端失敗:', response.status);
}
})
.catch(function(error) {
console.warn('同步設定到伺服器端時發生錯誤:', error);
});
};
/**
* 合併設定
*/
SettingsManager.prototype.mergeSettings = function(defaultSettings, newSettings) {
const merged = Utils.deepClone(defaultSettings);
for (const key in newSettings) {
if (newSettings.hasOwnProperty(key)) {
merged[key] = newSettings[key];
}
}
return merged;
};
/**
* 獲取設定值
*/
SettingsManager.prototype.get = function(key, defaultValue) {
if (key in this.currentSettings) {
return this.currentSettings[key];
}
return defaultValue !== undefined ? defaultValue : this.defaultSettings[key];
};
/**
* 設置設定值
*/
SettingsManager.prototype.set = function(key, value) {
const oldValue = this.currentSettings[key];
this.currentSettings[key] = value;
// 特殊處理語言變更
if (key === 'language' && oldValue !== value) {
this.handleLanguageChange(value);
}
this.saveSettings();
return this;
};
/**
* 批量設置設定
*/
SettingsManager.prototype.setMultiple = function(settings) {
let languageChanged = false;
const oldLanguage = this.currentSettings.language;
for (const key in settings) {
if (settings.hasOwnProperty(key)) {
this.currentSettings[key] = settings[key];
if (key === 'language' && oldLanguage !== settings[key]) {
languageChanged = true;
}
}
}
if (languageChanged) {
this.handleLanguageChange(this.currentSettings.language);
}
this.saveSettings();
return this;
};
/**
* 處理語言變更
*/
SettingsManager.prototype.handleLanguageChange = function(newLanguage) {
console.log('語言設定變更: ' + newLanguage);
// 同步到 localStorage
if (Utils.isLocalStorageSupported()) {
localStorage.setItem('language', newLanguage);
}
// 通知國際化系統
if (window.i18nManager) {
window.i18nManager.setLanguage(newLanguage);
}
// 觸發語言變更回調
if (this.onLanguageChange) {
this.onLanguageChange(newLanguage);
}
};
/**
* 重置設定
*/
SettingsManager.prototype.resetSettings = function() {
console.log('重置所有設定');
// 清除 localStorage
if (Utils.isLocalStorageSupported()) {
localStorage.removeItem('mcp-feedback-settings');
}
// 重置為預設值
this.currentSettings = Utils.deepClone(this.defaultSettings);
// 保存重置後的設定
this.saveSettings();
return this.currentSettings;
};
/**
* 獲取所有設定
*/
SettingsManager.prototype.getAllSettings = function() {
return Utils.deepClone(this.currentSettings);
};
/**
* 應用設定到 UI
*/
SettingsManager.prototype.applyToUI = function() {
console.log('應用設定到 UI');
// 應用佈局模式
this.applyLayoutMode();
// 應用自動關閉設定
this.applyAutoCloseToggle();
// 應用語言設定
this.applyLanguageSettings();
// 應用圖片設定
this.applyImageSettings();
// 應用自動刷新設定
this.applyAutoRefreshSettings();
};
/**
* 應用佈局模式
*/
SettingsManager.prototype.applyLayoutMode = function() {
const layoutModeInputs = document.querySelectorAll('input[name="layoutMode"]');
layoutModeInputs.forEach(function(input) {
input.checked = input.value === this.currentSettings.layoutMode;
}.bind(this));
const expectedClassName = 'layout-' + this.currentSettings.layoutMode;
if (document.body.className !== expectedClassName) {
console.log('應用佈局模式: ' + this.currentSettings.layoutMode);
document.body.className = expectedClassName;
}
};
/**
* 應用自動關閉設定
*/
SettingsManager.prototype.applyAutoCloseToggle = function() {
const autoCloseToggle = Utils.safeQuerySelector('#autoCloseToggle');
if (autoCloseToggle) {
autoCloseToggle.classList.toggle('active', this.currentSettings.autoClose);
}
};
/**
* 應用語言設定
*/
SettingsManager.prototype.applyLanguageSettings = function() {
if (this.currentSettings.language && window.i18nManager) {
const currentI18nLanguage = window.i18nManager.getCurrentLanguage();
if (this.currentSettings.language !== currentI18nLanguage) {
console.log('應用語言設定: ' + currentI18nLanguage + ' -> ' + this.currentSettings.language);
window.i18nManager.setLanguage(this.currentSettings.language);
}
}
// 更新語言選項顯示
const languageOptions = document.querySelectorAll('.language-option');
languageOptions.forEach(function(option) {
option.classList.toggle('active', option.getAttribute('data-lang') === this.currentSettings.language);
}.bind(this));
};
/**
* 應用圖片設定
*/
SettingsManager.prototype.applyImageSettings = function() {
const imageSizeLimitSelects = document.querySelectorAll('[id$="ImageSizeLimit"]');
imageSizeLimitSelects.forEach(function(select) {
select.value = this.currentSettings.imageSizeLimit.toString();
}.bind(this));
const enableBase64DetailCheckboxes = document.querySelectorAll('[id$="EnableBase64Detail"]');
enableBase64DetailCheckboxes.forEach(function(checkbox) {
checkbox.checked = this.currentSettings.enableBase64Detail;
}.bind(this));
};
/**
* 應用自動刷新設定
*/
SettingsManager.prototype.applyAutoRefreshSettings = function() {
const autoRefreshCheckbox = Utils.safeQuerySelector('#autoRefreshEnabled');
if (autoRefreshCheckbox) {
autoRefreshCheckbox.checked = this.currentSettings.autoRefreshEnabled;
}
const autoRefreshIntervalInput = Utils.safeQuerySelector('#autoRefreshInterval');
if (autoRefreshIntervalInput) {
autoRefreshIntervalInput.value = this.currentSettings.autoRefreshInterval;
}
};
/**
* 設置事件監聽器
*/
SettingsManager.prototype.setupEventListeners = function() {
const self = this;
// 佈局模式切換
const layoutModeInputs = document.querySelectorAll('input[name="layoutMode"]');
layoutModeInputs.forEach(function(input) {
input.addEventListener('change', function(e) {
self.set('layoutMode', e.target.value);
});
});
// 自動關閉切換
const autoCloseToggle = Utils.safeQuerySelector('#autoCloseToggle');
if (autoCloseToggle) {
autoCloseToggle.addEventListener('click', function() {
const newValue = !self.get('autoClose');
self.set('autoClose', newValue);
autoCloseToggle.classList.toggle('active', newValue);
});
}
// 語言切換
const languageOptions = document.querySelectorAll('.language-option');
languageOptions.forEach(function(option) {
option.addEventListener('click', function() {
const lang = option.getAttribute('data-lang');
self.set('language', lang);
});
});
// 重置設定
const resetBtn = Utils.safeQuerySelector('#resetSettingsBtn');
if (resetBtn) {
resetBtn.addEventListener('click', function() {
if (confirm('確定要重置所有設定嗎?')) {
self.resetSettings();
self.applyToUI();
}
});
}
// 自動刷新設定
const autoRefreshCheckbox = Utils.safeQuerySelector('#autoRefreshEnabled');
if (autoRefreshCheckbox) {
autoRefreshCheckbox.addEventListener('change', function(e) {
self.set('autoRefreshEnabled', e.target.checked);
});
}
const autoRefreshIntervalInput = Utils.safeQuerySelector('#autoRefreshInterval');
if (autoRefreshIntervalInput) {
autoRefreshIntervalInput.addEventListener('change', function(e) {
const newInterval = parseInt(e.target.value);
if (newInterval >= 5 && newInterval <= 300) {
self.set('autoRefreshInterval', newInterval);
}
});
}
};
// 將 SettingsManager 加入命名空間
window.MCPFeedback.SettingsManager = SettingsManager;
console.log('✅ SettingsManager 模組載入完成');
})();

View File

@ -0,0 +1,235 @@
/**
* MCP Feedback Enhanced - 標籤頁管理模組
* ====================================
*
* 處理多標籤頁狀態同步和智能瀏覽器管理
*/
(function() {
'use strict';
// 確保命名空間和依賴存在
window.MCPFeedback = window.MCPFeedback || {};
const Utils = window.MCPFeedback.Utils;
/**
* 標籤頁管理器建構函數
*/
function TabManager() {
this.tabId = Utils.generateId('tab');
this.heartbeatInterval = null;
this.heartbeatFrequency = Utils.CONSTANTS.DEFAULT_TAB_HEARTBEAT_FREQUENCY;
this.storageKey = 'mcp_feedback_tabs';
this.lastActivityKey = 'mcp_feedback_last_activity';
this.init();
}
/**
* 初始化標籤頁管理器
*/
TabManager.prototype.init = function() {
// 註冊當前標籤頁
this.registerTab();
// 向服務器註冊標籤頁
this.registerTabToServer();
// 開始心跳
this.startHeartbeat();
// 監聽頁面關閉事件
const self = this;
window.addEventListener('beforeunload', function() {
self.unregisterTab();
});
// 監聽 localStorage 變化(其他標籤頁的狀態變化)
window.addEventListener('storage', function(e) {
if (e.key === self.storageKey) {
self.handleTabsChange();
}
});
console.log('📋 TabManager 初始化完成,標籤頁 ID: ' + this.tabId);
};
/**
* 註冊當前標籤頁
*/
TabManager.prototype.registerTab = function() {
const tabs = this.getActiveTabs();
tabs[this.tabId] = {
timestamp: Date.now(),
url: window.location.href,
active: true
};
if (Utils.isLocalStorageSupported()) {
localStorage.setItem(this.storageKey, JSON.stringify(tabs));
}
this.updateLastActivity();
console.log('✅ 標籤頁已註冊: ' + this.tabId);
};
/**
* 註銷當前標籤頁
*/
TabManager.prototype.unregisterTab = function() {
const tabs = this.getActiveTabs();
delete tabs[this.tabId];
if (Utils.isLocalStorageSupported()) {
localStorage.setItem(this.storageKey, JSON.stringify(tabs));
}
console.log('❌ 標籤頁已註銷: ' + this.tabId);
};
/**
* 開始心跳
*/
TabManager.prototype.startHeartbeat = function() {
const self = this;
this.heartbeatInterval = setInterval(function() {
self.sendHeartbeat();
}, this.heartbeatFrequency);
};
/**
* 發送心跳
*/
TabManager.prototype.sendHeartbeat = function() {
const tabs = this.getActiveTabs();
if (tabs[this.tabId]) {
tabs[this.tabId].timestamp = Date.now();
if (Utils.isLocalStorageSupported()) {
localStorage.setItem(this.storageKey, JSON.stringify(tabs));
}
this.updateLastActivity();
}
};
/**
* 更新最後活動時間
*/
TabManager.prototype.updateLastActivity = function() {
if (Utils.isLocalStorageSupported()) {
localStorage.setItem(this.lastActivityKey, Date.now().toString());
}
};
/**
* 獲取活躍標籤頁
*/
TabManager.prototype.getActiveTabs = function() {
if (!Utils.isLocalStorageSupported()) {
return {};
}
try {
const stored = localStorage.getItem(this.storageKey);
const tabs = stored ? Utils.safeJsonParse(stored, {}) : {};
// 清理過期的標籤頁
const now = Date.now();
const expiredThreshold = Utils.CONSTANTS.TAB_EXPIRED_THRESHOLD;
for (const tabId in tabs) {
if (tabs.hasOwnProperty(tabId)) {
if (now - tabs[tabId].timestamp > expiredThreshold) {
delete tabs[tabId];
}
}
}
return tabs;
} catch (error) {
console.error('獲取活躍標籤頁失敗:', error);
return {};
}
};
/**
* 檢查是否有活躍標籤頁
*/
TabManager.prototype.hasActiveTabs = function() {
const tabs = this.getActiveTabs();
return Object.keys(tabs).length > 0;
};
/**
* 檢查是否為唯一活躍標籤頁
*/
TabManager.prototype.isOnlyActiveTab = function() {
const tabs = this.getActiveTabs();
return Object.keys(tabs).length === 1 && tabs[this.tabId];
};
/**
* 處理其他標籤頁狀態變化
*/
TabManager.prototype.handleTabsChange = function() {
console.log('🔄 檢測到其他標籤頁狀態變化');
// 可以在這裡添加更多邏輯
};
/**
* 向服務器註冊標籤頁
*/
TabManager.prototype.registerTabToServer = function() {
const self = this;
fetch('/api/register-tab', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tabId: this.tabId
})
})
.then(function(response) {
if (response.ok) {
return response.json();
} else {
console.warn('⚠️ 標籤頁服務器註冊失敗: ' + response.status);
}
})
.then(function(data) {
if (data) {
console.log('✅ 標籤頁已向服務器註冊: ' + self.tabId);
}
})
.catch(function(error) {
console.warn('⚠️ 標籤頁服務器註冊錯誤: ' + error);
});
};
/**
* 清理資源
*/
TabManager.prototype.cleanup = function() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
this.unregisterTab();
};
/**
* 獲取當前標籤頁 ID
*/
TabManager.prototype.getTabId = function() {
return this.tabId;
};
// 將 TabManager 加入命名空間
window.MCPFeedback.TabManager = TabManager;
console.log('✅ TabManager 模組載入完成');
})();

View File

@ -0,0 +1,471 @@
/**
* MCP Feedback Enhanced - UI 管理模組
* =================================
*
* 處理 UI 狀態更新指示器管理和頁籤切換
*/
(function() {
'use strict';
// 確保命名空間和依賴存在
window.MCPFeedback = window.MCPFeedback || {};
const Utils = window.MCPFeedback.Utils;
/**
* UI 管理器建構函數
*/
function UIManager(options) {
options = options || {};
// 當前狀態
this.currentTab = options.currentTab || 'combined';
this.feedbackState = Utils.CONSTANTS.FEEDBACK_WAITING;
this.layoutMode = options.layoutMode || 'combined-vertical';
this.lastSubmissionTime = null;
// UI 元素
this.connectionIndicator = null;
this.connectionText = null;
this.tabButtons = null;
this.tabContents = null;
this.submitBtn = null;
this.feedbackText = null;
// 回調函數
this.onTabChange = options.onTabChange || null;
this.onLayoutModeChange = options.onLayoutModeChange || null;
this.initUIElements();
}
/**
* 初始化 UI 元素
*/
UIManager.prototype.initUIElements = function() {
// 基本 UI 元素
this.connectionIndicator = Utils.safeQuerySelector('#connectionIndicator');
this.connectionText = Utils.safeQuerySelector('#connectionText');
// 頁籤相關元素
this.tabButtons = document.querySelectorAll('.tab-button');
this.tabContents = document.querySelectorAll('.tab-content');
// 回饋相關元素
this.feedbackText = Utils.safeQuerySelector('#feedbackText');
this.submitBtn = Utils.safeQuerySelector('#submitBtn');
console.log('✅ UI 元素初始化完成');
};
/**
* 初始化頁籤功能
*/
UIManager.prototype.initTabs = function() {
const self = this;
// 設置頁籤點擊事件
this.tabButtons.forEach(function(button) {
button.addEventListener('click', function() {
const tabName = button.getAttribute('data-tab');
self.switchTab(tabName);
});
});
// 根據佈局模式確定初始頁籤
let initialTab = this.currentTab;
if (this.layoutMode.startsWith('combined')) {
initialTab = 'combined';
} else if (this.currentTab === 'combined') {
initialTab = 'feedback';
}
// 設置初始頁籤
this.setInitialTab(initialTab);
};
/**
* 設置初始頁籤不觸發保存
*/
UIManager.prototype.setInitialTab = function(tabName) {
this.currentTab = tabName;
this.updateTabDisplay(tabName);
this.handleSpecialTabs(tabName);
console.log('初始化頁籤: ' + tabName);
};
/**
* 切換頁籤
*/
UIManager.prototype.switchTab = function(tabName) {
this.currentTab = tabName;
this.updateTabDisplay(tabName);
this.handleSpecialTabs(tabName);
// 觸發回調
if (this.onTabChange) {
this.onTabChange(tabName);
}
console.log('切換到頁籤: ' + tabName);
};
/**
* 更新頁籤顯示
*/
UIManager.prototype.updateTabDisplay = function(tabName) {
// 更新按鈕狀態
this.tabButtons.forEach(function(button) {
if (button.getAttribute('data-tab') === tabName) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
});
// 更新內容顯示
this.tabContents.forEach(function(content) {
if (content.id === 'tab-' + tabName) {
content.classList.add('active');
} else {
content.classList.remove('active');
}
});
};
/**
* 處理特殊頁籤
*/
UIManager.prototype.handleSpecialTabs = function(tabName) {
if (tabName === 'combined') {
this.handleCombinedMode();
}
};
/**
* 處理合併模式
*/
UIManager.prototype.handleCombinedMode = function() {
console.log('切換到組合模式');
// 確保合併模式的佈局樣式正確應用
const combinedTab = Utils.safeQuerySelector('#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');
}
}
};
/**
* 更新頁籤可見性
*/
UIManager.prototype.updateTabVisibility = function() {
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"]');
// 只使用合併模式顯示合併模式頁籤隱藏回饋和AI摘要頁籤
if (combinedTab) combinedTab.style.display = 'inline-block';
if (feedbackTab) feedbackTab.style.display = 'none';
if (summaryTab) summaryTab.style.display = 'none';
};
/**
* 設置回饋狀態
*/
UIManager.prototype.setFeedbackState = function(state, sessionId) {
const previousState = this.feedbackState;
this.feedbackState = state;
if (sessionId) {
console.log('🔄 會話 ID: ' + sessionId.substring(0, 8) + '...');
}
console.log('📊 狀態變更: ' + previousState + ' → ' + state);
this.updateUIState();
this.updateStatusIndicator();
};
/**
* 更新 UI 狀態
*/
UIManager.prototype.updateUIState = function() {
this.updateSubmitButton();
this.updateFeedbackInputs();
this.updateImageUploadAreas();
};
/**
* 更新提交按鈕狀態
*/
UIManager.prototype.updateSubmitButton = function() {
const submitButtons = [
Utils.safeQuerySelector('#submitBtn'),
Utils.safeQuerySelector('#combinedSubmitBtn')
].filter(function(btn) { return btn !== null; });
const self = this;
submitButtons.forEach(function(button) {
if (!button) return;
switch (self.feedbackState) {
case Utils.CONSTANTS.FEEDBACK_WAITING:
button.textContent = window.i18nManager ? window.i18nManager.t('buttons.submit') : '提交回饋';
button.className = 'btn btn-primary';
button.disabled = false;
break;
case Utils.CONSTANTS.FEEDBACK_PROCESSING:
button.textContent = window.i18nManager ? window.i18nManager.t('buttons.processing') : '處理中...';
button.className = 'btn btn-secondary';
button.disabled = true;
break;
case Utils.CONSTANTS.FEEDBACK_SUBMITTED:
button.textContent = window.i18nManager ? window.i18nManager.t('buttons.submitted') : '已提交';
button.className = 'btn btn-success';
button.disabled = true;
break;
}
});
};
/**
* 更新回饋輸入框狀態
*/
UIManager.prototype.updateFeedbackInputs = function() {
const feedbackInputs = [
Utils.safeQuerySelector('#feedbackText'),
Utils.safeQuerySelector('#combinedFeedbackText')
].filter(function(input) { return input !== null; });
const canInput = this.feedbackState === Utils.CONSTANTS.FEEDBACK_WAITING;
feedbackInputs.forEach(function(input) {
input.disabled = !canInput;
});
};
/**
* 更新圖片上傳區域狀態
*/
UIManager.prototype.updateImageUploadAreas = function() {
const uploadAreas = [
Utils.safeQuerySelector('#feedbackImageUploadArea'),
Utils.safeQuerySelector('#combinedImageUploadArea')
].filter(function(area) { return area !== null; });
const canUpload = this.feedbackState === Utils.CONSTANTS.FEEDBACK_WAITING;
uploadAreas.forEach(function(area) {
if (canUpload) {
area.classList.remove('disabled');
} else {
area.classList.add('disabled');
}
});
};
/**
* 更新狀態指示器
*/
UIManager.prototype.updateStatusIndicator = function() {
const feedbackStatusIndicator = Utils.safeQuerySelector('#feedbackStatusIndicator');
const combinedStatusIndicator = Utils.safeQuerySelector('#combinedFeedbackStatusIndicator');
const statusInfo = this.getStatusInfo();
if (feedbackStatusIndicator) {
this.updateStatusIndicatorElement(feedbackStatusIndicator, statusInfo);
}
if (combinedStatusIndicator) {
this.updateStatusIndicatorElement(combinedStatusIndicator, statusInfo);
}
console.log('✅ 狀態指示器已更新: ' + statusInfo.status + ' - ' + statusInfo.title);
};
/**
* 獲取狀態信息
*/
UIManager.prototype.getStatusInfo = function() {
let icon, title, message, status;
switch (this.feedbackState) {
case Utils.CONSTANTS.FEEDBACK_WAITING:
icon = '⏳';
title = window.i18nManager ? window.i18nManager.t('status.waiting.title') : '等待回饋';
message = window.i18nManager ? window.i18nManager.t('status.waiting.message') : '請提供您的回饋意見';
status = 'waiting';
break;
case Utils.CONSTANTS.FEEDBACK_PROCESSING:
icon = '⚙️';
title = window.i18nManager ? window.i18nManager.t('status.processing.title') : '處理中';
message = window.i18nManager ? window.i18nManager.t('status.processing.message') : '正在提交您的回饋...';
status = 'processing';
break;
case Utils.CONSTANTS.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';
}
return { icon: icon, title: title, message: message, status: status };
};
/**
* 更新單個狀態指示器元素
*/
UIManager.prototype.updateStatusIndicatorElement = function(element, statusInfo) {
if (!element) return;
// 更新狀態類別
element.className = 'feedback-status-indicator status-' + statusInfo.status;
element.style.display = 'block';
// 更新標題
const titleElement = element.querySelector('.status-title');
if (titleElement) {
titleElement.textContent = statusInfo.icon + ' ' + statusInfo.title;
}
// 更新訊息
const messageElement = element.querySelector('.status-message');
if (messageElement) {
messageElement.textContent = statusInfo.message;
}
console.log('🔧 已更新狀態指示器: ' + element.id + ' -> ' + statusInfo.status);
};
/**
* 更新連接狀態
*/
UIManager.prototype.updateConnectionStatus = function(status, text) {
if (this.connectionIndicator) {
this.connectionIndicator.className = 'connection-indicator ' + status;
}
if (this.connectionText) {
this.connectionText.textContent = text;
}
};
/**
* 更新 AI 摘要內容
*/
UIManager.prototype.updateAISummaryContent = function(summary) {
console.log('📝 更新 AI 摘要內容...');
const summaryContent = Utils.safeQuerySelector('#summaryContent');
if (summaryContent) {
summaryContent.textContent = summary;
console.log('✅ 已更新分頁模式摘要內容');
}
const combinedSummaryContent = Utils.safeQuerySelector('#combinedSummaryContent');
if (combinedSummaryContent) {
combinedSummaryContent.textContent = summary;
console.log('✅ 已更新合併模式摘要內容');
}
};
/**
* 重置回饋表單
*/
UIManager.prototype.resetFeedbackForm = function() {
console.log('🔄 重置回饋表單...');
// 清空回饋輸入
const feedbackInputs = [
Utils.safeQuerySelector('#feedbackText'),
Utils.safeQuerySelector('#combinedFeedbackText')
].filter(function(input) { return input !== null; });
feedbackInputs.forEach(function(input) {
input.value = '';
input.disabled = false;
});
// 重新啟用提交按鈕
const submitButtons = [
Utils.safeQuerySelector('#submitBtn'),
Utils.safeQuerySelector('#combinedSubmitBtn')
].filter(function(btn) { return btn !== null; });
submitButtons.forEach(function(button) {
button.disabled = false;
button.textContent = button.getAttribute('data-original-text') || '提交回饋';
});
console.log('✅ 回饋表單重置完成');
};
/**
* 應用佈局模式
*/
UIManager.prototype.applyLayoutMode = function(layoutMode) {
this.layoutMode = layoutMode;
const expectedClassName = 'layout-' + layoutMode;
if (document.body.className !== expectedClassName) {
console.log('應用佈局模式: ' + layoutMode);
document.body.className = expectedClassName;
}
this.updateTabVisibility();
// 如果當前頁籤不是合併模式,則切換到合併模式頁籤
if (this.currentTab !== 'combined') {
this.currentTab = 'combined';
}
// 觸發回調
if (this.onLayoutModeChange) {
this.onLayoutModeChange(layoutMode);
}
};
/**
* 獲取當前頁籤
*/
UIManager.prototype.getCurrentTab = function() {
return this.currentTab;
};
/**
* 獲取當前回饋狀態
*/
UIManager.prototype.getFeedbackState = function() {
return this.feedbackState;
};
/**
* 設置最後提交時間
*/
UIManager.prototype.setLastSubmissionTime = function(timestamp) {
this.lastSubmissionTime = timestamp;
this.updateStatusIndicator();
};
// 將 UIManager 加入命名空間
window.MCPFeedback.UIManager = UIManager;
console.log('✅ UIManager 模組載入完成');
})();

View File

@ -0,0 +1,238 @@
/**
* MCP Feedback Enhanced - 工具模組
* ================================
*
* 提供共用的工具函數和常數定義
*/
(function() {
'use strict';
// 確保命名空間存在
window.MCPFeedback = window.MCPFeedback || {};
/**
* 工具函數模組
*/
window.MCPFeedback.Utils = {
/**
* 格式化檔案大小
* @param {number} bytes - 位元組數
* @returns {string} 格式化後的檔案大小
*/
formatFileSize: function(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
/**
* 生成唯一 ID
* @param {string} prefix - ID 前綴
* @returns {string} 唯一 ID
*/
generateId: function(prefix) {
prefix = prefix || 'id';
return prefix + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
},
/**
* 深度複製物件
* @param {Object} obj - 要複製的物件
* @returns {Object} 複製後的物件
*/
deepClone: function(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof Array) return obj.map(item => this.deepClone(item));
if (typeof obj === 'object') {
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = this.deepClone(obj[key]);
}
}
return clonedObj;
}
},
/**
* 防抖函數
* @param {Function} func - 要防抖的函數
* @param {number} wait - 等待時間毫秒
* @returns {Function} 防抖後的函數
*/
debounce: function(func, wait) {
let timeout;
return function executedFunction() {
const later = () => {
clearTimeout(timeout);
func.apply(this, arguments);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* 節流函數
* @param {Function} func - 要節流的函數
* @param {number} limit - 限制時間毫秒
* @returns {Function} 節流後的函數
*/
throttle: function(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
},
/**
* 安全的 JSON 解析
* @param {string} jsonString - JSON 字串
* @param {*} defaultValue - 預設值
* @returns {*} 解析結果或預設值
*/
safeJsonParse: function(jsonString, defaultValue) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('JSON 解析失敗:', error);
return defaultValue;
}
},
/**
* 檢查元素是否存在
* @param {string} selector - CSS 選擇器
* @returns {boolean} 元素是否存在
*/
elementExists: function(selector) {
return document.querySelector(selector) !== null;
},
/**
* 安全的元素查詢
* @param {string} selector - CSS 選擇器
* @param {Element} context - 查詢上下文可選
* @returns {Element|null} 找到的元素或 null
*/
safeQuerySelector: function(selector, context) {
try {
const root = context || document;
return root.querySelector(selector);
} catch (error) {
console.warn('元素查詢失敗:', selector, error);
return null;
}
},
/**
* 顯示訊息提示
* @param {string} message - 訊息內容
* @param {string} type - 訊息類型 (success, error, warning, info)
* @param {number} duration - 顯示時間毫秒
*/
showMessage: function(message, type, duration) {
type = type || 'info';
duration = duration || 3000;
// 創建訊息元素
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(--${type === 'error' ? 'error' : type === 'warning' ? 'warning' : 'success'}-color, #4CAF50);
color: white;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
max-width: 300px;
word-wrap: break-word;
transition: opacity 0.3s ease;
`;
messageDiv.textContent = message;
document.body.appendChild(messageDiv);
// 自動移除
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.style.opacity = '0';
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.parentNode.removeChild(messageDiv);
}
}, 300);
}
}, duration);
},
/**
* 檢查 WebSocket 是否可用
* @returns {boolean} WebSocket 是否可用
*/
isWebSocketSupported: function() {
return 'WebSocket' in window;
},
/**
* 檢查 localStorage 是否可用
* @returns {boolean} localStorage 是否可用
*/
isLocalStorageSupported: function() {
try {
const test = '__localStorage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
},
/**
* 常數定義
*/
CONSTANTS: {
// WebSocket 狀態
WS_CONNECTING: 0,
WS_OPEN: 1,
WS_CLOSING: 2,
WS_CLOSED: 3,
// 回饋狀態
FEEDBACK_WAITING: 'waiting_for_feedback',
FEEDBACK_SUBMITTED: 'feedback_submitted',
FEEDBACK_PROCESSING: 'processing',
// 預設設定
DEFAULT_HEARTBEAT_FREQUENCY: 30000,
DEFAULT_TAB_HEARTBEAT_FREQUENCY: 5000,
DEFAULT_RECONNECT_DELAY: 1000,
MAX_RECONNECT_ATTEMPTS: 5,
TAB_EXPIRED_THRESHOLD: 30000,
// 訊息類型
MESSAGE_SUCCESS: 'success',
MESSAGE_ERROR: 'error',
MESSAGE_WARNING: 'warning',
MESSAGE_INFO: 'info'
}
};
console.log('✅ Utils 模組載入完成');
})();

View File

@ -0,0 +1,359 @@
/**
* MCP Feedback Enhanced - WebSocket 管理模組
* =========================================
*
* 處理 WebSocket 連接訊息傳遞和重連邏輯
*/
(function() {
'use strict';
// 確保命名空間和依賴存在
window.MCPFeedback = window.MCPFeedback || {};
const Utils = window.MCPFeedback.Utils;
/**
* WebSocket 管理器建構函數
*/
function WebSocketManager(options) {
options = options || {};
this.websocket = null;
this.isConnected = false;
this.connectionReady = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || Utils.CONSTANTS.MAX_RECONNECT_ATTEMPTS;
this.reconnectDelay = options.reconnectDelay || Utils.CONSTANTS.DEFAULT_RECONNECT_DELAY;
this.heartbeatInterval = null;
this.heartbeatFrequency = options.heartbeatFrequency || Utils.CONSTANTS.DEFAULT_HEARTBEAT_FREQUENCY;
// 事件回調
this.onOpen = options.onOpen || null;
this.onMessage = options.onMessage || null;
this.onClose = options.onClose || null;
this.onError = options.onError || null;
this.onConnectionStatusChange = options.onConnectionStatusChange || null;
// 標籤頁管理器引用
this.tabManager = options.tabManager || null;
// 待處理的提交
this.pendingSubmission = null;
this.sessionUpdatePending = false;
}
/**
* 建立 WebSocket 連接
*/
WebSocketManager.prototype.connect = function() {
if (!Utils.isWebSocketSupported()) {
console.error('❌ 瀏覽器不支援 WebSocket');
return;
}
// 確保 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.setupWebSocketEvents();
} catch (error) {
console.error('WebSocket 連接失敗:', error);
this.updateConnectionStatus('error', '連接失敗');
}
};
/**
* 設置 WebSocket 事件監聽器
*/
WebSocketManager.prototype.setupWebSocketEvents = function() {
const self = this;
this.websocket.onopen = function() {
self.handleOpen();
};
this.websocket.onmessage = function(event) {
self.handleMessage(event);
};
this.websocket.onclose = function(event) {
self.handleClose(event);
};
this.websocket.onerror = function(error) {
self.handleError(error);
};
};
/**
* 處理連接開啟
*/
WebSocketManager.prototype.handleOpen = function() {
this.isConnected = true;
this.connectionReady = false; // 等待連接確認
this.updateConnectionStatus('connected', '已連接');
console.log('WebSocket 連接已建立');
// 重置重連計數器和延遲
this.reconnectAttempts = 0;
this.reconnectDelay = Utils.CONSTANTS.DEFAULT_RECONNECT_DELAY;
// 開始心跳
this.startHeartbeat();
// 請求會話狀態
this.requestSessionStatus();
// 調用外部回調
if (this.onOpen) {
this.onOpen();
}
};
/**
* 處理訊息接收
*/
WebSocketManager.prototype.handleMessage = function(event) {
try {
const data = Utils.safeJsonParse(event.data, null);
if (data) {
this.processMessage(data);
// 調用外部回調
if (this.onMessage) {
this.onMessage(data);
}
}
} catch (error) {
console.error('解析 WebSocket 訊息失敗:', error);
}
};
/**
* 處理連接關閉
*/
WebSocketManager.prototype.handleClose = function(event) {
this.isConnected = false;
this.connectionReady = false;
console.log('WebSocket 連接已關閉, code:', event.code, 'reason:', event.reason);
// 停止心跳
this.stopHeartbeat();
// 處理不同的關閉原因
if (event.code === 4004) {
this.updateConnectionStatus('disconnected', '沒有活躍會話');
} else {
this.updateConnectionStatus('disconnected', '已斷開');
this.handleReconnection(event);
}
// 調用外部回調
if (this.onClose) {
this.onClose(event);
}
};
/**
* 處理連接錯誤
*/
WebSocketManager.prototype.handleError = function(error) {
console.error('WebSocket 錯誤:', error);
this.updateConnectionStatus('error', '連接錯誤');
// 調用外部回調
if (this.onError) {
this.onError(error);
}
};
/**
* 處理重連邏輯
*/
WebSocketManager.prototype.handleReconnection = function(event) {
// 會話更新導致的正常關閉,立即重連
if (event.code === 1000 && event.reason === '會話更新') {
console.log('🔄 會話更新導致的連接關閉,立即重連...');
this.sessionUpdatePending = true;
const self = this;
setTimeout(function() {
self.connect();
}, 200);
}
// 只有在非正常關閉時才重連
else if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 15000);
console.log(this.reconnectDelay / 1000 + '秒後嘗試重連... (第' + this.reconnectAttempts + '次)');
const self = this;
setTimeout(function() {
console.log('🔄 開始重連 WebSocket... (第' + self.reconnectAttempts + '次)');
self.connect();
}, this.reconnectDelay);
} else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('❌ 達到最大重連次數,停止重連');
Utils.showMessage('WebSocket 連接失敗,請刷新頁面重試', Utils.CONSTANTS.MESSAGE_ERROR);
}
};
/**
* 處理訊息
*/
WebSocketManager.prototype.processMessage = function(data) {
console.log('收到 WebSocket 訊息:', data);
switch (data.type) {
case 'connection_established':
console.log('WebSocket 連接確認');
this.connectionReady = true;
this.handleConnectionReady();
break;
case 'heartbeat_response':
this.handleHeartbeatResponse();
break;
default:
// 其他訊息類型由外部處理
break;
}
};
/**
* 處理連接就緒
*/
WebSocketManager.prototype.handleConnectionReady = function() {
// 如果有待提交的內容,現在可以提交了
if (this.pendingSubmission) {
console.log('🔄 連接就緒,提交待處理的內容');
const self = this;
setTimeout(function() {
if (self.pendingSubmission) {
self.send(self.pendingSubmission);
self.pendingSubmission = null;
}
}, 100);
}
};
/**
* 處理心跳回應
*/
WebSocketManager.prototype.handleHeartbeatResponse = function() {
if (this.tabManager) {
this.tabManager.updateLastActivity();
}
};
/**
* 發送訊息
*/
WebSocketManager.prototype.send = function(data) {
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
try {
this.websocket.send(JSON.stringify(data));
return true;
} catch (error) {
console.error('發送 WebSocket 訊息失敗:', error);
return false;
}
} else {
console.warn('WebSocket 未連接,無法發送訊息');
return false;
}
};
/**
* 請求會話狀態
*/
WebSocketManager.prototype.requestSessionStatus = function() {
this.send({
type: 'get_status'
});
};
/**
* 開始心跳
*/
WebSocketManager.prototype.startHeartbeat = function() {
this.stopHeartbeat();
const self = this;
this.heartbeatInterval = setInterval(function() {
if (self.websocket && self.websocket.readyState === WebSocket.OPEN) {
self.send({
type: 'heartbeat',
tabId: self.tabManager ? self.tabManager.getTabId() : null,
timestamp: Date.now()
});
}
}, this.heartbeatFrequency);
console.log('💓 WebSocket 心跳已啟動,頻率: ' + this.heartbeatFrequency + 'ms');
};
/**
* 停止心跳
*/
WebSocketManager.prototype.stopHeartbeat = function() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
console.log('💔 WebSocket 心跳已停止');
}
};
/**
* 更新連接狀態
*/
WebSocketManager.prototype.updateConnectionStatus = function(status, text) {
if (this.onConnectionStatusChange) {
this.onConnectionStatusChange(status, text);
}
};
/**
* 設置待處理的提交
*/
WebSocketManager.prototype.setPendingSubmission = function(data) {
this.pendingSubmission = data;
};
/**
* 檢查是否已連接且就緒
*/
WebSocketManager.prototype.isReady = function() {
return this.isConnected && this.connectionReady;
};
/**
* 關閉連接
*/
WebSocketManager.prototype.close = function() {
this.stopHeartbeat();
if (this.websocket) {
this.websocket.close();
this.websocket = null;
}
this.isConnected = false;
this.connectionReady = false;
};
// 將 WebSocketManager 加入命名空間
window.MCPFeedback.WebSocketManager = WebSocketManager;
console.log('✅ WebSocketManager 模組載入完成');
})();

View File

@ -747,19 +747,46 @@
<!-- WebSocket 和 JavaScript -->
<script src="/static/js/i18n.js?v=2025010510"></script>
<!-- 載入所有模組 -->
<script src="/static/js/modules/utils.js?v=2025010510"></script>
<script src="/static/js/modules/tab-manager.js?v=2025010510"></script>
<script src="/static/js/modules/websocket-manager.js?v=2025010510"></script>
<script src="/static/js/modules/image-handler.js?v=2025010510"></script>
<script src="/static/js/modules/settings-manager.js?v=2025010510"></script>
<script src="/static/js/modules/ui-manager.js?v=2025010510"></script>
<script src="/static/js/modules/auto-refresh-manager.js?v=2025010510"></script>
<!-- 主應用程式 -->
<script src="/static/js/app.js?v=2025010510"></script>
<script>
// 等待 I18nManager 初始化完成後再初始化 FeedbackApp
// 等待所有模組載入完成後再初始化 FeedbackApp
async function initializeApp() {
const sessionId = '{{ session_id }}';
// 確保 I18nManager 已經初始化
if (window.i18nManager) {
await window.i18nManager.init();
// 檢查所有必要的模組是否已載入
if (!window.MCPFeedback ||
!window.MCPFeedback.Utils ||
!window.MCPFeedback.FeedbackApp) {
console.error('❌ 模組載入不完整,延遲初始化...');
setTimeout(initializeApp, 100);
return;
}
// 初始化 FeedbackApp
window.feedbackApp = new FeedbackApp(sessionId);
try {
// 確保 I18nManager 已經初始化
if (window.i18nManager) {
await window.i18nManager.init();
}
// 初始化 FeedbackApp使用新的命名空間
window.feedbackApp = new window.MCPFeedback.FeedbackApp(sessionId);
// 初始化應用程式
await window.feedbackApp.init();
console.log('✅ 應用程式初始化完成');
} catch (error) {
console.error('❌ 應用程式初始化失敗:', error);
}
}
// 頁面載入完成後初始化

View File

@ -317,6 +317,15 @@
<!-- JavaScript -->
<script src="/static/js/i18n.js?v=2025010505"></script>
<!-- 載入所有模組 -->
<script src="/static/js/modules/utils.js?v=2025010505"></script>
<script src="/static/js/modules/tab-manager.js?v=2025010505"></script>
<script src="/static/js/modules/websocket-manager.js?v=2025010505"></script>
<script src="/static/js/modules/image-handler.js?v=2025010505"></script>
<script src="/static/js/modules/settings-manager.js?v=2025010505"></script>
<script src="/static/js/modules/ui-manager.js?v=2025010505"></script>
<script src="/static/js/modules/auto-refresh-manager.js?v=2025010505"></script>
<!-- 主應用程式 -->
<script src="/static/js/app.js?v=2025010505"></script>
</body>
</html>