mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 02:22:26 +08:00
🔨 重構 app.js 模組化
This commit is contained in:
parent
47dbdcd96c
commit
1a6c3babe6
@ -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
|
295
shrimp-rules.md
295
shrimp-rules.md
@ -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. 造成文檔不一致 ❌
|
||||
```
|
@ -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
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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 模組載入完成');
|
||||
|
||||
})();
|
490
src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js
Normal file
490
src/mcp_feedback_enhanced/web/static/js/modules/image-handler.js
Normal 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 模組載入完成');
|
||||
|
||||
})();
|
@ -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 模組載入完成');
|
||||
|
||||
})();
|
235
src/mcp_feedback_enhanced/web/static/js/modules/tab-manager.js
Normal file
235
src/mcp_feedback_enhanced/web/static/js/modules/tab-manager.js
Normal 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 模組載入完成');
|
||||
|
||||
})();
|
471
src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js
Normal file
471
src/mcp_feedback_enhanced/web/static/js/modules/ui-manager.js
Normal 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 模組載入完成');
|
||||
|
||||
})();
|
238
src/mcp_feedback_enhanced/web/static/js/modules/utils.js
Normal file
238
src/mcp_feedback_enhanced/web/static/js/modules/utils.js
Normal 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 模組載入完成');
|
||||
|
||||
})();
|
@ -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 模組載入完成');
|
||||
|
||||
})();
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
// 頁面載入完成後初始化
|
||||
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user