+ 支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式 +
🎨 介面設定
+🌐 語言設定
+📋 AI 工作摘要
+💬 提供回饋
+ ++ 支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式 +
diff --git a/src/mcp_feedback_enhanced/__init__.py b/src/mcp_feedback_enhanced/__init__.py
index 2be81c7..d5048f6 100644
--- a/src/mcp_feedback_enhanced/__init__.py
+++ b/src/mcp_feedback_enhanced/__init__.py
@@ -15,6 +15,7 @@ MCP Interactive Feedback Enhanced
- 命令執行功能
- 圖片上傳支援
- 現代化深色主題
+- 重構的模組化架構
"""
__version__ = "2.0.16"
@@ -23,13 +24,18 @@ __email__ = "minidora0702@gmail.com"
from .server import main as run_server
from .gui import feedback_ui
-from .web_ui import WebUIManager
+
+# 導入新的 Web UI 模組
+from .web import WebUIManager, launch_web_feedback_ui, get_web_ui_manager, stop_web_ui
# 主要導出介面
__all__ = [
"run_server",
"feedback_ui",
"WebUIManager",
+ "launch_web_feedback_ui",
+ "get_web_ui_manager",
+ "stop_web_ui",
"__version__",
"__author__",
]
diff --git a/src/mcp_feedback_enhanced/server.py b/src/mcp_feedback_enhanced/server.py
index 89d5a37..64837f5 100644
--- a/src/mcp_feedback_enhanced/server.py
+++ b/src/mcp_feedback_enhanced/server.py
@@ -514,10 +514,11 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in
debug_log(f"啟動 Web UI 介面,超時時間: {timeout} 秒")
try:
- from .web_ui import get_web_ui_manager
+ # 使用新的 web 模組
+ from .web import launch_web_feedback_ui
# 直接運行 Web UI 會話
- return await _run_web_ui_session(project_dir, summary, timeout)
+ return await launch_web_feedback_ui(project_dir, summary)
except ImportError as e:
debug_log(f"無法導入 Web UI 模組: {e}")
return {
@@ -525,77 +526,14 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in
"interactive_feedback": f"Web UI 模組導入失敗: {str(e)}",
"images": []
}
-
-
-async def _run_web_ui_session(project_dir: str, summary: str, timeout: int) -> dict:
- """
- 運行 Web UI 會話
-
- Args:
- project_dir: 專案目錄路徑
- summary: AI 工作摘要
- timeout: 超時時間(秒)
-
- Returns:
- dict: 收集到的回饋資料
- """
- from .web_ui import get_web_ui_manager
-
- manager = get_web_ui_manager()
-
- # 創建會話
- session_id = manager.create_session(project_dir, summary)
- session_url = f"http://{manager.host}:{manager.port}/session/{session_id}"
-
- debug_log(f"Web UI 已啟動: {session_url}")
- # 注意:不能使用 print() 污染 stdout,會破壞 MCP 通信
- # try:
- # print(f"Web UI 已啟動: {session_url}")
- # except UnicodeEncodeError:
- # print(f"Web UI launched: {session_url}")
-
- # 開啟瀏覽器
- manager.open_browser(session_url)
-
- try:
- # 等待用戶回饋
- session = manager.get_session(session_id)
- if not session:
- raise RuntimeError("會話創建失敗")
-
- result = await session.wait_for_feedback(timeout=timeout)
- debug_log(f"Web UI 回饋收集成功,超時時間: {timeout} 秒")
- return result
-
- except TimeoutError:
- timeout_msg = f"等待用戶回饋超時({timeout} 秒)"
- debug_log(f"⏰ {timeout_msg}")
- # 注意:不能使用 print() 污染 stdout,會破壞 MCP 通信
- # try:
- # print(f"等待用戶回饋超時({timeout} 秒)")
- # except UnicodeEncodeError:
- # print(f"Feedback timeout ({timeout} seconds)")
- return {
- "command_logs": "",
- "interactive_feedback": f"回饋超時({timeout} 秒)",
- "images": []
- }
except Exception as e:
error_msg = f"Web UI 錯誤: {e}"
debug_log(f"❌ {error_msg}")
- # 注意:不能使用 print() 污染 stdout,會破壞 MCP 通信
- # try:
- # print(f"Web UI 錯誤: {e}")
- # except UnicodeEncodeError:
- # print(f"Web UI error: {e}")
return {
"command_logs": "",
"interactive_feedback": f"錯誤: {str(e)}",
"images": []
}
- finally:
- # 清理會話
- manager.remove_session(session_id)
@mcp.tool()
diff --git a/src/mcp_feedback_enhanced/test_web_ui.py b/src/mcp_feedback_enhanced/test_web_ui.py
index 8b38f1f..f87aac0 100644
--- a/src/mcp_feedback_enhanced/test_web_ui.py
+++ b/src/mcp_feedback_enhanced/test_web_ui.py
@@ -38,8 +38,10 @@ from .i18n import t
# 嘗試導入 Web UI 模組
try:
- from .web_ui import get_web_ui_manager, launch_web_feedback_ui
+ # 使用新的 web 模組
+ from .web import WebUIManager, launch_web_feedback_ui, get_web_ui_manager
WEB_UI_AVAILABLE = True
+ debug_log("✅ 使用新的 web 模組")
except ImportError as e:
debug_log(f"⚠️ 無法導入 Web UI 模組: {e}")
WEB_UI_AVAILABLE = False
@@ -64,7 +66,8 @@ def test_web_ui(keep_running=False):
# Test import
try:
- from .web_ui import WebUIManager, launch_web_feedback_ui
+ # 使用新的 web 模組
+ from .web import WebUIManager, launch_web_feedback_ui
debug_log("✅ Web UI 模組匯入成功")
except ImportError as e:
debug_log(f"❌ Web UI 模組匯入失敗: {e}")
diff --git a/src/mcp_feedback_enhanced/web/__init__.py b/src/mcp_feedback_enhanced/web/__init__.py
new file mode 100644
index 0000000..5509326
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/__init__.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Web UI 模組
+===========
+
+提供基於 FastAPI 的 Web 用戶介面,專為 SSH 遠端開發環境設計。
+支援文字輸入、圖片上傳、命令執行等功能,並參考 GUI 的設計模式。
+"""
+
+from .main import WebUIManager, launch_web_feedback_ui, get_web_ui_manager, stop_web_ui
+
+__all__ = [
+ 'WebUIManager',
+ 'launch_web_feedback_ui',
+ 'get_web_ui_manager',
+ 'stop_web_ui'
+]
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json
new file mode 100644
index 0000000..c1e4c3d
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json
@@ -0,0 +1,138 @@
+{
+ "app": {
+ "title": "MCP Interactive Feedback System",
+ "subtitle": "AI Assistant Interactive Feedback Platform",
+ "projectDirectory": "Project Directory"
+ },
+ "tabs": {
+ "feedback": "💬 Feedback",
+ "summary": "📋 AI Summary",
+ "commands": "⚡ Commands",
+ "command": "⚡ Commands",
+ "settings": "⚙️ Settings",
+ "combined": "📝 Combined Mode"
+ },
+ "feedback": {
+ "title": "💬 Provide Feedback",
+ "description": "Please provide your feedback on the AI assistant's work. You can enter text feedback and upload related images.",
+ "textLabel": "Text Feedback",
+ "placeholder": "Please enter your feedback here...",
+ "detailedPlaceholder": "Please enter your feedback here...\n\n💡 Tips:\n• Press Ctrl+Enter/Cmd+Enter (numpad supported) for quick submit\n• Press Ctrl+V/Cmd+V to paste clipboard images directly",
+ "imageLabel": "Image Attachments (Optional)",
+ "imageUploadText": "📎 Click to select images or drag and drop images here\nSupports PNG, JPG, JPEG, GIF, BMP, WebP formats",
+ "submit": "✅ Submit Feedback",
+ "uploading": "Uploading...",
+ "dragdrop": "Drag and drop images here or click to upload",
+ "selectfiles": "Select Files",
+ "processing": "Processing...",
+ "success": "Feedback submitted successfully!",
+ "error": "Error submitting feedback",
+ "shortcuts": {
+ "submit": "Ctrl+Enter to submit (Cmd+Enter on Mac, numpad supported)",
+ "clear": "Ctrl+Delete to clear (Cmd+Delete on Mac)",
+ "paste": "Ctrl+V to paste images (Cmd+V on Mac)"
+ }
+ },
+ "summary": {
+ "title": "📋 AI Work Summary",
+ "description": "Below is the work summary completed by the AI assistant. Please review carefully and provide your feedback.",
+ "placeholder": "AI work summary will be displayed here...",
+ "empty": "No summary content available",
+ "lastupdate": "Last updated",
+ "refresh": "Refresh"
+ },
+ "commands": {
+ "title": "⚡ Command Execution",
+ "description": "Execute commands here to verify results or collect more information. Commands will be executed in the project directory.",
+ "inputLabel": "Command Input",
+ "placeholder": "Enter command to execute...",
+ "execute": "▶️ Execute",
+ "runButton": "▶️ Execute",
+ "clear": "Clear",
+ "output": "Command Output",
+ "outputLabel": "Command Output",
+ "running": "Running...",
+ "completed": "Completed",
+ "error": "Execution Error",
+ "history": "Command History"
+ },
+ "command": {
+ "title": "⚡ Command Execution",
+ "description": "Execute commands here to verify results or collect more information. Commands will be executed in the project directory.",
+ "inputLabel": "Command Input",
+ "placeholder": "Enter command to execute...",
+ "execute": "▶️ Execute",
+ "runButton": "▶️ Execute",
+ "clear": "Clear",
+ "output": "Command Output",
+ "outputLabel": "Command Output",
+ "running": "Running...",
+ "completed": "Completed",
+ "error": "Execution Error",
+ "history": "Command History"
+ },
+ "settings": {
+ "title": "⚙️ Settings",
+ "description": "Adjust interface settings and preference options.",
+ "language": "Language",
+ "currentLanguage": "Current Language",
+ "languageDesc": "Select interface display language",
+ "interface": "Interface Settings",
+ "combinedMode": "Combined Mode",
+ "combinedModeDesc": "Merge AI summary and feedback input in the same tab",
+ "autoClose": "Auto Close Page",
+ "autoCloseDesc": "Automatically close page after submitting feedback",
+ "theme": "Theme",
+ "notifications": "Notifications",
+ "advanced": "Advanced Settings",
+ "save": "Save Settings",
+ "reset": "Reset",
+ "timeout": "Connection Timeout (seconds)",
+ "autorefresh": "Auto Refresh",
+ "debug": "Debug Mode"
+ },
+ "languages": {
+ "zh-TW": "繁體中文",
+ "zh-CN": "简体中文",
+ "en": "English"
+ },
+ "themes": {
+ "dark": "Dark",
+ "light": "Light",
+ "auto": "Auto"
+ },
+ "status": {
+ "connected": "Connected",
+ "connecting": "Connecting...",
+ "disconnected": "Disconnected",
+ "reconnecting": "Reconnecting...",
+ "error": "Connection Error"
+ },
+ "notifications": {
+ "feedback_sent": "Feedback sent",
+ "command_executed": "Command executed",
+ "settings_saved": "Settings saved",
+ "connection_lost": "Connection lost",
+ "connection_restored": "Connection restored"
+ },
+ "errors": {
+ "connection_failed": "Connection failed",
+ "upload_failed": "Upload failed",
+ "command_failed": "Command execution failed",
+ "invalid_input": "Invalid input",
+ "timeout": "Request timeout"
+ },
+ "buttons": {
+ "ok": "OK",
+ "cancel": "❌ Cancel",
+ "submit": "✅ Submit Feedback",
+ "retry": "Retry",
+ "close": "Close",
+ "upload": "Upload",
+ "download": "Download"
+ },
+ "dynamic": {
+ "aiSummary": "Test Web UI Functionality\n\n🎯 **Test Items:**\n- Web UI server startup and operation\n- WebSocket real-time communication\n- Feedback submission functionality\n- Image upload and preview\n- Command execution functionality\n- Smart Ctrl+V image pasting\n- Multi-language interface functionality\n\n📋 **Test Steps:**\n1. Test image upload (drag-drop, file selection, clipboard)\n2. Press Ctrl+V in text box to test smart pasting\n3. Try switching languages (Traditional Chinese/Simplified Chinese/English)\n4. Test command execution functionality\n5. Submit feedback and images\n\nPlease test these features and provide feedback!",
+ "terminalWelcome": "Welcome to Interactive Feedback Terminal\n========================================\nProject Directory: {sessionId}\nEnter commands and press Enter or click Execute button\nSupported commands: ls, dir, pwd, cat, type, etc.\n\n$ "
+ }
+}
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
new file mode 100644
index 0000000..5d8e151
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
@@ -0,0 +1,138 @@
+{
+ "app": {
+ "title": "MCP 交互反馈系统",
+ "subtitle": "AI 助手交互反馈平台",
+ "projectDirectory": "项目目录"
+ },
+ "tabs": {
+ "feedback": "💬 反馈",
+ "summary": "📋 AI 总结",
+ "commands": "⚡ 命令",
+ "command": "⚡ 命令",
+ "settings": "⚙️ 设置",
+ "combined": "📝 合并模式"
+ },
+ "feedback": {
+ "title": "💬 提供反馈",
+ "description": "请提供您对 AI 工作成果的反馈意见。您可以输入文字反馈并上传相关图片。",
+ "textLabel": "文字反馈",
+ "placeholder": "请在这里输入您的反馈...",
+ "detailedPlaceholder": "请在这里输入您的反馈...\n\n💡 小提示:\n• 按 Ctrl+Enter/Cmd+Enter (支持数字键盘) 可快速提交\n• 按 Ctrl+V/Cmd+V 可直接粘贴剪贴板图片",
+ "imageLabel": "图片附件(可选)",
+ "imageUploadText": "📎 点击选择图片或拖放图片到此处\n支持 PNG、JPG、JPEG、GIF、BMP、WebP 等格式",
+ "submit": "✅ 提交反馈",
+ "uploading": "上传中...",
+ "dragdrop": "拖放图片到这里或点击上传",
+ "selectfiles": "选择文件",
+ "processing": "处理中...",
+ "success": "反馈已成功提交!",
+ "error": "提交反馈时发生错误",
+ "shortcuts": {
+ "submit": "Ctrl+Enter 提交 (Mac 用 Cmd+Enter,支持数字键盘)",
+ "clear": "Ctrl+Delete 清除 (Mac 用 Cmd+Delete)",
+ "paste": "Ctrl+V 粘贴图片 (Mac 用 Cmd+V)"
+ }
+ },
+ "summary": {
+ "title": "📋 AI 工作摘要",
+ "description": "以下是 AI 助手完成的工作摘要,请仔细查看并提供您的反馈意见。",
+ "placeholder": "AI 工作摘要将在这里显示...",
+ "empty": "目前没有摘要内容",
+ "lastupdate": "最后更新",
+ "refresh": "刷新"
+ },
+ "commands": {
+ "title": "⚡ 命令执行",
+ "description": "在此执行命令来验证结果或收集更多信息。命令将在项目目录中执行。",
+ "inputLabel": "命令输入",
+ "placeholder": "输入要执行的命令...",
+ "execute": "▶️ 执行",
+ "runButton": "▶️ 执行",
+ "clear": "清除",
+ "output": "命令输出",
+ "outputLabel": "命令输出",
+ "running": "执行中...",
+ "completed": "执行完成",
+ "error": "执行错误",
+ "history": "命令历史"
+ },
+ "command": {
+ "title": "⚡ 命令执行",
+ "description": "在此执行命令来验证结果或收集更多信息。命令将在项目目录中执行。",
+ "inputLabel": "命令输入",
+ "placeholder": "输入要执行的命令...",
+ "execute": "▶️ 执行",
+ "runButton": "▶️ 执行",
+ "clear": "清除",
+ "output": "命令输出",
+ "outputLabel": "命令输出",
+ "running": "执行中...",
+ "completed": "执行完成",
+ "error": "执行错误",
+ "history": "命令历史"
+ },
+ "settings": {
+ "title": "⚙️ 设定",
+ "description": "调整界面设定和偏好选项。",
+ "language": "语言",
+ "currentLanguage": "当前语言",
+ "languageDesc": "选择界面显示语言",
+ "interface": "界面设定",
+ "combinedMode": "合并模式",
+ "combinedModeDesc": "将 AI 摘要和回馈输入合并在同一个分页中",
+ "autoClose": "自动关闭页面",
+ "autoCloseDesc": "提交回馈后自动关闭页面",
+ "theme": "主题",
+ "notifications": "通知",
+ "advanced": "进阶设定",
+ "save": "储存设定",
+ "reset": "重设",
+ "timeout": "连线逾时 (秒)",
+ "autorefresh": "自动重新整理",
+ "debug": "除错模式"
+ },
+ "languages": {
+ "zh-TW": "繁體中文",
+ "zh-CN": "简体中文",
+ "en": "English"
+ },
+ "themes": {
+ "dark": "深色",
+ "light": "浅色",
+ "auto": "自动"
+ },
+ "status": {
+ "connected": "已连接",
+ "connecting": "连接中...",
+ "disconnected": "已断开连接",
+ "reconnecting": "重新连接中...",
+ "error": "连接错误"
+ },
+ "notifications": {
+ "feedback_sent": "反馈已发送",
+ "command_executed": "命令已执行",
+ "settings_saved": "设置已保存",
+ "connection_lost": "连接中断",
+ "connection_restored": "连接已恢复"
+ },
+ "errors": {
+ "connection_failed": "连接失败",
+ "upload_failed": "上传失败",
+ "command_failed": "命令执行失败",
+ "invalid_input": "输入内容无效",
+ "timeout": "请求超时"
+ },
+ "buttons": {
+ "ok": "确定",
+ "cancel": "❌ 取消",
+ "submit": "✅ 提交反馈",
+ "retry": "重试",
+ "close": "关闭",
+ "upload": "上传",
+ "download": "下载"
+ },
+ "dynamic": {
+ "aiSummary": "测试 Web UI 功能\n\n🎯 **功能测试项目:**\n- Web UI 服务器启动和运行\n- WebSocket 实时通讯\n- 反馈提交功能\n- 图片上传和预览\n- 命令执行功能\n- 智能 Ctrl+V 图片粘贴\n- 多语言界面功能\n\n📋 **测试步骤:**\n1. 测试图片上传(拖拽、选择文件、剪贴板)\n2. 在文本框内按 Ctrl+V 测试智能粘贴\n3. 尝试切换语言(繁中/简中/英文)\n4. 测试命令执行功能\n5. 提交反馈和图片\n\n请测试这些功能并提供反馈!",
+ "terminalWelcome": "欢迎使用交互反馈终端\n========================================\n项目目录: {sessionId}\n输入命令后按 Enter 或点击执行按钮\n支持的命令: ls, dir, pwd, cat, type 等\n\n$ "
+ }
+}
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
new file mode 100644
index 0000000..fcd240a
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
@@ -0,0 +1,138 @@
+{
+ "app": {
+ "title": "MCP 互動回饋系統",
+ "subtitle": "AI 助手互動回饋平台",
+ "projectDirectory": "專案目錄"
+ },
+ "tabs": {
+ "feedback": "💬 回饋",
+ "summary": "📋 AI 摘要",
+ "commands": "⚡ 命令",
+ "command": "⚡ 命令",
+ "settings": "⚙️ 設定",
+ "combined": "📝 合併模式"
+ },
+ "feedback": {
+ "title": "💬 提供回饋",
+ "description": "請提供您對 AI 工作成果的回饋意見。您可以輸入文字回饋並上傳相關圖片。",
+ "textLabel": "文字回饋",
+ "placeholder": "請在這裡輸入您的回饋...",
+ "detailedPlaceholder": "請在這裡輸入您的回饋...\n\n💡 小提示:\n• 按 Ctrl+Enter/Cmd+Enter (支援數字鍵盤) 可快速提交\n• 按 Ctrl+V/Cmd+V 可直接貼上剪貼板圖片",
+ "imageLabel": "圖片附件(可選)",
+ "imageUploadText": "📎 點擊選擇圖片或拖放圖片到此處\n支援 PNG、JPG、JPEG、GIF、BMP、WebP 等格式",
+ "submit": "✅ 提交回饋",
+ "uploading": "上傳中...",
+ "dragdrop": "拖放圖片到這裡或點擊上傳",
+ "selectfiles": "選擇檔案",
+ "processing": "處理中...",
+ "success": "回饋已成功提交!",
+ "error": "提交回饋時發生錯誤",
+ "shortcuts": {
+ "submit": "Ctrl+Enter 提交 (Mac 用 Cmd+Enter,支援數字鍵盤)",
+ "clear": "Ctrl+Delete 清除 (Mac 用 Cmd+Delete)",
+ "paste": "Ctrl+V 貼上圖片 (Mac 用 Cmd+V)"
+ }
+ },
+ "summary": {
+ "title": "📋 AI 工作摘要",
+ "description": "以下是 AI 助手完成的工作摘要,請仔細查看並提供您的回饋意見。",
+ "placeholder": "AI 工作摘要將在這裡顯示...",
+ "empty": "目前沒有摘要內容",
+ "lastupdate": "最後更新",
+ "refresh": "重新整理"
+ },
+ "commands": {
+ "title": "⚡ 命令執行",
+ "description": "在此執行命令來驗證結果或收集更多資訊。命令將在專案目錄中執行。",
+ "inputLabel": "命令輸入",
+ "placeholder": "輸入要執行的命令...",
+ "execute": "▶️ 執行",
+ "runButton": "▶️ 執行",
+ "clear": "清除",
+ "output": "命令輸出",
+ "outputLabel": "命令輸出",
+ "running": "執行中...",
+ "completed": "執行完成",
+ "error": "執行錯誤",
+ "history": "命令歷史"
+ },
+ "command": {
+ "title": "⚡ 命令執行",
+ "description": "在此執行命令來驗證結果或收集更多資訊。命令將在專案目錄中執行。",
+ "inputLabel": "命令輸入",
+ "placeholder": "輸入要執行的命令...",
+ "execute": "▶️ 執行",
+ "runButton": "▶️ 執行",
+ "clear": "清除",
+ "output": "命令輸出",
+ "outputLabel": "命令輸出",
+ "running": "執行中...",
+ "completed": "執行完成",
+ "error": "執行錯誤",
+ "history": "命令歷史"
+ },
+ "settings": {
+ "title": "⚙️ 設定",
+ "description": "調整介面設定和偏好選項。",
+ "language": "語言",
+ "currentLanguage": "當前語言",
+ "languageDesc": "選擇界面顯示語言",
+ "interface": "介面設定",
+ "combinedMode": "合併模式",
+ "combinedModeDesc": "將 AI 摘要和回饋輸入合併在同一個分頁中",
+ "autoClose": "自動關閉頁面",
+ "autoCloseDesc": "提交回饋後自動關閉頁面",
+ "theme": "主題",
+ "notifications": "通知",
+ "advanced": "進階設定",
+ "save": "儲存設定",
+ "reset": "重設",
+ "timeout": "連線逾時 (秒)",
+ "autorefresh": "自動重新整理",
+ "debug": "除錯模式"
+ },
+ "languages": {
+ "zh-TW": "繁體中文",
+ "zh-CN": "简体中文",
+ "en": "English"
+ },
+ "themes": {
+ "dark": "深色",
+ "light": "淺色",
+ "auto": "自動"
+ },
+ "status": {
+ "connected": "已連線",
+ "connecting": "連線中...",
+ "disconnected": "已中斷連線",
+ "reconnecting": "重新連線中...",
+ "error": "連線錯誤"
+ },
+ "notifications": {
+ "feedback_sent": "回饋已發送",
+ "command_executed": "指令已執行",
+ "settings_saved": "設定已儲存",
+ "connection_lost": "連線中斷",
+ "connection_restored": "連線已恢復"
+ },
+ "errors": {
+ "connection_failed": "連線失敗",
+ "upload_failed": "上傳失敗",
+ "command_failed": "指令執行失敗",
+ "invalid_input": "輸入內容無效",
+ "timeout": "請求逾時"
+ },
+ "buttons": {
+ "ok": "確定",
+ "cancel": "❌ 取消",
+ "submit": "✅ 提交回饋",
+ "retry": "重試",
+ "close": "關閉",
+ "upload": "上傳",
+ "download": "下載"
+ },
+ "dynamic": {
+ "aiSummary": "測試 Web UI 功能\n\n🎯 **功能測試項目:**\n- Web UI 服務器啟動和運行\n- WebSocket 即時通訊\n- 回饋提交功能\n- 圖片上傳和預覽\n- 命令執行功能\n- 智能 Ctrl+V 圖片貼上\n- 多語言介面功能\n\n📋 **測試步驟:**\n1. 測試圖片上傳(拖拽、選擇檔案、剪貼簿)\n2. 在文字框內按 Ctrl+V 測試智能貼上\n3. 嘗試切換語言(繁中/簡中/英文)\n4. 測試命令執行功能\n5. 提交回饋和圖片\n\n請測試這些功能並提供回饋!",
+ "terminalWelcome": "歡迎使用互動回饋終端\n========================================\n專案目錄: {sessionId}\n輸入命令後按 Enter 或點擊執行按鈕\n支援的命令: ls, dir, pwd, cat, type 等\n\n$ "
+ }
+}
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py
new file mode 100644
index 0000000..91ca9d6
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/main.py
@@ -0,0 +1,253 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Web UI 主要管理器
+================
+
+基於 FastAPI 的 Web 用戶介面主要管理類,參考 GUI 的設計模式重構。
+專為 SSH 遠端開發環境設計,支援現代化界面和多語言。
+"""
+
+import asyncio
+import json
+import logging
+import os
+import socket
+import threading
+import time
+import webbrowser
+from pathlib import Path
+from typing import Dict, Optional
+import uuid
+
+from fastapi import FastAPI
+from fastapi.staticfiles import StaticFiles
+from fastapi.templating import Jinja2Templates
+import uvicorn
+
+from .models import WebFeedbackSession, FeedbackResult
+from .routes import setup_routes
+from .utils import find_free_port, get_browser_opener
+from ..debug import web_debug_log as debug_log
+from ..i18n import get_i18n_manager
+
+
+class WebUIManager:
+ """Web UI 管理器"""
+
+ def __init__(self, host: str = "127.0.0.1", port: int = None):
+ self.host = host
+ self.port = port or find_free_port()
+ self.app = FastAPI(title="Interactive Feedback MCP")
+ self.sessions: Dict[str, WebFeedbackSession] = {}
+ self.server_thread = None
+ self.server_process = None
+ self.i18n = get_i18n_manager()
+
+ # 設置靜態文件和模板
+ self._setup_static_files()
+ self._setup_templates()
+
+ # 設置路由
+ setup_routes(self)
+
+ debug_log(f"WebUIManager 初始化完成,將在 {self.host}:{self.port} 啟動")
+
+ def _setup_static_files(self):
+ """設置靜態文件服務"""
+ # Web UI 靜態文件
+ web_static_path = Path(__file__).parent / "static"
+ if web_static_path.exists():
+ self.app.mount("/static", StaticFiles(directory=str(web_static_path)), name="static")
+
+ # 備用:原有的靜態文件
+ fallback_static_path = Path(__file__).parent.parent / "static"
+ if fallback_static_path.exists():
+ self.app.mount("/fallback_static", StaticFiles(directory=str(fallback_static_path)), name="fallback_static")
+
+ def _setup_templates(self):
+ """設置模板引擎"""
+ # Web UI 模板
+ web_templates_path = Path(__file__).parent / "templates"
+ if web_templates_path.exists():
+ self.templates = Jinja2Templates(directory=str(web_templates_path))
+ else:
+ # 備用:原有的模板
+ fallback_templates_path = Path(__file__).parent.parent / "templates"
+ self.templates = Jinja2Templates(directory=str(fallback_templates_path))
+
+ def create_session(self, project_directory: str, summary: str) -> str:
+ """創建新的回饋會話"""
+ session_id = str(uuid.uuid4())
+ session = WebFeedbackSession(session_id, project_directory, summary)
+ self.sessions[session_id] = session
+ debug_log(f"創建回饋會話: {session_id}")
+ return session_id
+
+ def get_session(self, session_id: str) -> Optional[WebFeedbackSession]:
+ """獲取回饋會話"""
+ return self.sessions.get(session_id)
+
+ def remove_session(self, session_id: str):
+ """移除回饋會話"""
+ if session_id in self.sessions:
+ session = self.sessions[session_id]
+ session.cleanup()
+ del self.sessions[session_id]
+ debug_log(f"移除回饋會話: {session_id}")
+
+ def start_server(self):
+ """啟動 Web 伺服器"""
+ def run_server_with_retry():
+ max_retries = 5
+ retry_count = 0
+
+ while retry_count < max_retries:
+ try:
+ debug_log(f"嘗試啟動伺服器在 {self.host}:{self.port} (嘗試 {retry_count + 1}/{max_retries})")
+
+ config = uvicorn.Config(
+ app=self.app,
+ host=self.host,
+ port=self.port,
+ log_level="warning",
+ access_log=False
+ )
+
+ server = uvicorn.Server(config)
+ asyncio.run(server.serve())
+ break
+
+ except OSError as e:
+ if e.errno == 10048: # Windows: 位址已在使用中
+ retry_count += 1
+ if retry_count < max_retries:
+ debug_log(f"端口 {self.port} 被占用,嘗試下一個端口")
+ self.port = find_free_port(self.port + 1)
+ else:
+ debug_log("已達到最大重試次數,無法啟動伺服器")
+ break
+ else:
+ debug_log(f"伺服器啟動錯誤: {e}")
+ break
+ except Exception as e:
+ debug_log(f"伺服器運行錯誤: {e}")
+ break
+
+ # 在新線程中啟動伺服器
+ self.server_thread = threading.Thread(target=run_server_with_retry, daemon=True)
+ self.server_thread.start()
+
+ # 等待伺服器啟動
+ time.sleep(2)
+
+ def open_browser(self, url: str):
+ """開啟瀏覽器"""
+ try:
+ browser_opener = get_browser_opener()
+ browser_opener(url)
+ debug_log(f"已開啟瀏覽器:{url}")
+ except Exception as e:
+ debug_log(f"無法開啟瀏覽器: {e}")
+
+ def get_server_url(self) -> str:
+ """獲取伺服器 URL"""
+ return f"http://{self.host}:{self.port}"
+
+ def stop(self):
+ """停止 Web UI 服務"""
+ # 清理所有會話
+ for session in list(self.sessions.values()):
+ session.cleanup()
+ self.sessions.clear()
+
+ # 停止伺服器(注意:uvicorn 的 graceful shutdown 需要額外處理)
+ if self.server_thread and self.server_thread.is_alive():
+ debug_log("正在停止 Web UI 服務")
+
+
+# 全域實例
+_web_ui_manager: Optional[WebUIManager] = None
+
+
+def get_web_ui_manager() -> WebUIManager:
+ """獲取 Web UI 管理器實例"""
+ global _web_ui_manager
+ if _web_ui_manager is None:
+ _web_ui_manager = WebUIManager()
+ return _web_ui_manager
+
+
+async def launch_web_feedback_ui(project_directory: str, summary: str) -> dict:
+ """
+ 啟動 Web 回饋介面並等待用戶回饋
+
+ Args:
+ project_directory: 專案目錄路徑
+ summary: AI 工作摘要
+
+ Returns:
+ dict: 回饋結果,包含 logs、interactive_feedback 和 images
+ """
+ manager = get_web_ui_manager()
+
+ # 創建會話
+ session_id = manager.create_session(project_directory, summary)
+ session = manager.get_session(session_id)
+
+ if not session:
+ raise RuntimeError("無法創建回饋會話")
+
+ # 啟動伺服器(如果尚未啟動)
+ if not manager.server_thread or not manager.server_thread.is_alive():
+ manager.start_server()
+
+ # 構建完整 URL 並開啟瀏覽器
+ feedback_url = f"{manager.get_server_url()}/session/{session_id}"
+ manager.open_browser(feedback_url)
+
+ try:
+ # 等待用戶回饋
+ result = await session.wait_for_feedback()
+ debug_log(f"收到用戶回饋,會話: {session_id}")
+ return result
+ finally:
+ # 清理會話
+ manager.remove_session(session_id)
+
+
+def stop_web_ui():
+ """停止 Web UI 服務"""
+ global _web_ui_manager
+ if _web_ui_manager:
+ _web_ui_manager.stop()
+ _web_ui_manager = None
+ debug_log("Web UI 服務已停止")
+
+
+# 測試用主函數
+if __name__ == "__main__":
+ async def main():
+ try:
+ project_dir = os.getcwd()
+ summary = "這是一個測試摘要,用於驗證 Web UI 功能。"
+
+ print(f"啟動 Web UI 測試...")
+ print(f"專案目錄: {project_dir}")
+ print("等待用戶回饋...")
+
+ result = await launch_web_feedback_ui(project_dir, summary)
+
+ print("收到回饋結果:")
+ print(f"命令日誌: {result.get('logs', '')}")
+ print(f"互動回饋: {result.get('interactive_feedback', '')}")
+ print(f"圖片數量: {len(result.get('images', []))}")
+
+ except KeyboardInterrupt:
+ print("\n用戶取消操作")
+ except Exception as e:
+ print(f"錯誤: {e}")
+ finally:
+ stop_web_ui()
+
+ asyncio.run(main())
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/models/__init__.py b/src/mcp_feedback_enhanced/web/models/__init__.py
new file mode 100644
index 0000000..d06dd01
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/models/__init__.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Web UI 資料模型模組
+==================
+
+定義 Web UI 相關的資料結構和型別。
+"""
+
+from .feedback_session import WebFeedbackSession
+from .feedback_result import FeedbackResult
+
+__all__ = [
+ 'WebFeedbackSession',
+ 'FeedbackResult'
+]
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/models/feedback_result.py b/src/mcp_feedback_enhanced/web/models/feedback_result.py
new file mode 100644
index 0000000..b4d463c
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/models/feedback_result.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Web UI 回饋結果資料模型
+======================
+
+定義回饋收集的資料結構,與 GUI 版本保持一致。
+"""
+
+from typing import TypedDict, List
+
+
+class FeedbackResult(TypedDict):
+ """回饋結果的型別定義"""
+ command_logs: str
+ interactive_feedback: str
+ images: List[dict]
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/models/feedback_session.py b/src/mcp_feedback_enhanced/web/models/feedback_session.py
new file mode 100644
index 0000000..dd1981f
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/models/feedback_session.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Web 回饋會話模型
+===============
+
+管理 Web 回饋會話的資料和邏輯。
+"""
+
+import asyncio
+import base64
+import subprocess
+import threading
+from pathlib import Path
+from typing import Dict, List, Optional
+
+from fastapi import WebSocket
+
+from ...debug import web_debug_log as debug_log
+
+# 常數定義
+MAX_IMAGE_SIZE = 1 * 1024 * 1024 # 1MB 圖片大小限制
+SUPPORTED_IMAGE_TYPES = {'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/bmp', 'image/webp'}
+TEMP_DIR = Path.home() / ".cache" / "interactive-feedback-mcp-web"
+
+
+class WebFeedbackSession:
+ """Web 回饋會話管理"""
+
+ def __init__(self, session_id: str, project_directory: str, summary: str):
+ self.session_id = session_id
+ self.project_directory = project_directory
+ self.summary = summary
+ self.websocket: Optional[WebSocket] = None
+ self.feedback_result: Optional[str] = None
+ self.images: List[dict] = []
+ self.feedback_completed = threading.Event()
+ self.process: Optional[subprocess.Popen] = None
+ self.command_logs = []
+
+ # 確保臨時目錄存在
+ TEMP_DIR.mkdir(parents=True, exist_ok=True)
+
+ async def wait_for_feedback(self, timeout: int = 600) -> dict:
+ """
+ 等待用戶回饋,包含圖片
+
+ Args:
+ timeout: 超時時間(秒)
+
+ Returns:
+ dict: 回饋結果
+ """
+ loop = asyncio.get_event_loop()
+
+ def wait_in_thread():
+ return self.feedback_completed.wait(timeout)
+
+ completed = await loop.run_in_executor(None, wait_in_thread)
+
+ if completed:
+ return {
+ "logs": "\n".join(self.command_logs),
+ "interactive_feedback": self.feedback_result or "",
+ "images": self.images
+ }
+ else:
+ raise TimeoutError("等待用戶回饋超時")
+
+ async def submit_feedback(self, feedback: str, images: List[dict]):
+ """
+ 提交回饋和圖片
+
+ Args:
+ feedback: 文字回饋
+ images: 圖片列表
+ """
+ self.feedback_result = feedback
+ self.images = self._process_images(images)
+ self.feedback_completed.set()
+
+ if self.websocket:
+ try:
+ await self.websocket.close()
+ except:
+ pass
+
+ def _process_images(self, images: List[dict]) -> List[dict]:
+ """
+ 處理圖片數據,轉換為統一格式
+
+ Args:
+ images: 原始圖片數據列表
+
+ Returns:
+ List[dict]: 處理後的圖片數據
+ """
+ processed_images = []
+
+ for img in images:
+ try:
+ if not all(key in img for key in ["name", "data", "size"]):
+ continue
+
+ # 檢查文件大小
+ if img["size"] > MAX_IMAGE_SIZE:
+ debug_log(f"圖片 {img['name']} 超過大小限制,跳過")
+ continue
+
+ # 解碼 base64 數據
+ if isinstance(img["data"], str):
+ try:
+ image_bytes = base64.b64decode(img["data"])
+ except Exception as e:
+ debug_log(f"圖片 {img['name']} base64 解碼失敗: {e}")
+ continue
+ else:
+ image_bytes = img["data"]
+
+ if len(image_bytes) == 0:
+ debug_log(f"圖片 {img['name']} 數據為空,跳過")
+ continue
+
+ processed_images.append({
+ "name": img["name"],
+ "data": image_bytes, # 保存原始 bytes 數據
+ "size": len(image_bytes)
+ })
+
+ debug_log(f"圖片 {img['name']} 處理成功,大小: {len(image_bytes)} bytes")
+
+ except Exception as e:
+ debug_log(f"圖片處理錯誤: {e}")
+ continue
+
+ return processed_images
+
+ def add_log(self, log_entry: str):
+ """添加命令日誌"""
+ self.command_logs.append(log_entry)
+
+ async def run_command(self, command: str):
+ """執行命令並透過 WebSocket 發送輸出"""
+ if self.process:
+ # 終止現有進程
+ try:
+ self.process.terminate()
+ self.process.wait(timeout=5)
+ except:
+ try:
+ self.process.kill()
+ except:
+ pass
+ self.process = None
+
+ try:
+ debug_log(f"執行命令: {command}")
+
+ self.process = subprocess.Popen(
+ command,
+ shell=True,
+ cwd=self.project_directory,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ bufsize=1,
+ universal_newlines=True
+ )
+
+ # 在背景線程中讀取輸出
+ async def read_output():
+ loop = asyncio.get_event_loop()
+ try:
+ # 使用線程池執行器來處理阻塞的讀取操作
+ def read_line():
+ if self.process and self.process.stdout:
+ return self.process.stdout.readline()
+ return ''
+
+ while True:
+ line = await loop.run_in_executor(None, read_line)
+ if not line:
+ break
+
+ self.add_log(line.rstrip())
+ if self.websocket:
+ try:
+ await self.websocket.send_json({
+ "type": "command_output",
+ "output": line
+ })
+ except Exception as e:
+ debug_log(f"WebSocket 發送失敗: {e}")
+ break
+
+ except Exception as e:
+ debug_log(f"讀取命令輸出錯誤: {e}")
+ finally:
+ # 等待進程完成
+ if self.process:
+ exit_code = self.process.wait()
+
+ # 發送命令完成信號
+ if self.websocket:
+ try:
+ await self.websocket.send_json({
+ "type": "command_complete",
+ "exit_code": exit_code
+ })
+ except Exception as e:
+ debug_log(f"發送完成信號失敗: {e}")
+
+ # 啟動異步任務讀取輸出
+ asyncio.create_task(read_output())
+
+ except Exception as e:
+ debug_log(f"執行命令錯誤: {e}")
+ if self.websocket:
+ try:
+ await self.websocket.send_json({
+ "type": "command_error",
+ "error": str(e)
+ })
+ except:
+ pass
+
+ def cleanup(self):
+ """清理會話資源"""
+ if self.process:
+ try:
+ self.process.terminate()
+ self.process.wait(timeout=5)
+ except:
+ try:
+ self.process.kill()
+ except:
+ pass
+ self.process = None
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/routes/__init__.py b/src/mcp_feedback_enhanced/web/routes/__init__.py
new file mode 100644
index 0000000..d9e5e82
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/routes/__init__.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Web UI 路由模組
+==============
+
+提供 Web UI 的路由設置和處理。
+"""
+
+from .main_routes import setup_routes
+
+__all__ = ['setup_routes']
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/routes/main_routes.py b/src/mcp_feedback_enhanced/web/routes/main_routes.py
new file mode 100644
index 0000000..5008216
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/routes/main_routes.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+主要路由處理
+============
+
+設置 Web UI 的主要路由和處理邏輯。
+"""
+
+import json
+import os
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from fastapi import Request, WebSocket, WebSocketDisconnect
+from fastapi.responses import HTMLResponse, JSONResponse
+
+from ...debug import web_debug_log as debug_log
+
+if TYPE_CHECKING:
+ from ..main import WebUIManager
+
+
+def setup_routes(manager: 'WebUIManager'):
+ """設置路由"""
+
+ @manager.app.get("/", response_class=HTMLResponse)
+ async def index(request: Request):
+ """首頁"""
+ return manager.templates.TemplateResponse("index.html", {
+ "request": request,
+ "title": "Interactive Feedback MCP"
+ })
+
+ @manager.app.get("/session/{session_id}", response_class=HTMLResponse)
+ async def feedback_session(request: Request, session_id: str):
+ """回饋會話頁面"""
+ session = manager.get_session(session_id)
+ if not session:
+ return JSONResponse(
+ status_code=404,
+ content={"error": "會話不存在"}
+ )
+
+ return manager.templates.TemplateResponse("feedback.html", {
+ "request": request,
+ "session_id": session_id,
+ "project_directory": session.project_directory,
+ "summary": session.summary,
+ "title": "Interactive Feedback - 回饋收集"
+ })
+
+ @manager.app.get("/api/translations")
+ async def get_translations():
+ """獲取翻譯數據 - 從 Web 專用翻譯檔案載入"""
+ translations = {}
+
+ # 獲取 Web 翻譯檔案目錄
+ web_locales_dir = Path(__file__).parent.parent / "locales"
+ supported_languages = ["zh-TW", "zh-CN", "en"]
+
+ for lang_code in supported_languages:
+ lang_dir = web_locales_dir / lang_code
+ translation_file = lang_dir / "translation.json"
+
+ try:
+ if translation_file.exists():
+ with open(translation_file, 'r', encoding='utf-8') as f:
+ lang_data = json.load(f)
+ translations[lang_code] = lang_data
+ debug_log(f"成功載入 Web 翻譯: {lang_code}")
+ else:
+ debug_log(f"Web 翻譯檔案不存在: {translation_file}")
+ translations[lang_code] = {}
+ except Exception as e:
+ debug_log(f"載入 Web 翻譯檔案失敗 {lang_code}: {e}")
+ translations[lang_code] = {}
+
+ debug_log(f"Web 翻譯 API 返回 {len(translations)} 種語言的數據")
+ return JSONResponse(content=translations)
+
+ @manager.app.websocket("/ws/{session_id}")
+ async def websocket_endpoint(websocket: WebSocket, session_id: str):
+ """WebSocket 端點"""
+ session = manager.get_session(session_id)
+ if not session:
+ await websocket.close(code=4004, reason="會話不存在")
+ return
+
+ await websocket.accept()
+ session.websocket = websocket
+
+ debug_log(f"WebSocket 連接建立: {session_id}")
+
+ try:
+ while True:
+ data = await websocket.receive_text()
+ message = json.loads(data)
+ await handle_websocket_message(manager, session, message)
+
+ except WebSocketDisconnect:
+ debug_log(f"WebSocket 連接斷開: {session_id}")
+ except Exception as e:
+ debug_log(f"WebSocket 錯誤: {e}")
+ finally:
+ session.websocket = None
+
+
+async def handle_websocket_message(manager: 'WebUIManager', session, data: dict):
+ """處理 WebSocket 消息"""
+ message_type = data.get("type")
+
+ if message_type == "submit_feedback":
+ # 提交回饋
+ feedback = data.get("feedback", "")
+ images = data.get("images", [])
+ await session.submit_feedback(feedback, images)
+
+ elif message_type == "run_command":
+ # 執行命令
+ command = data.get("command", "")
+ if command.strip():
+ await session.run_command(command)
+
+ else:
+ debug_log(f"未知的消息類型: {message_type}")
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/static/css/styles.css b/src/mcp_feedback_enhanced/web/static/css/styles.css
new file mode 100644
index 0000000..9941aba
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/static/css/styles.css
@@ -0,0 +1,330 @@
+/**
+ * Web UI 樣式
+ * ===========
+ *
+ * 補充樣式和動畫效果
+ */
+
+/* 連接狀態指示器 */
+.connection-indicator {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.connection-indicator.connected {
+ background: rgba(76, 175, 80, 0.1);
+ color: #4caf50;
+ border: 1px solid #4caf50;
+}
+
+.connection-indicator.disconnected {
+ background: rgba(244, 67, 54, 0.1);
+ color: #f44336;
+ border: 1px solid #f44336;
+}
+
+/* 載入動畫 */
+.loading {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ border: 2px solid #464647;
+ border-radius: 50%;
+ border-top-color: #007acc;
+ animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* 淡入動畫 */
+.fade-in {
+ animation: fadeIn 0.3s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* 滑入動畫 */
+.slide-in {
+ animation: slideIn 0.3s ease-out;
+}
+
+@keyframes slideIn {
+ from { transform: translateX(-20px); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+}
+
+/* 脈衝動畫 */
+.pulse {
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.5; }
+ 100% { opacity: 1; }
+}
+
+/* 工具提示 */
+.tooltip {
+ position: relative;
+ cursor: help;
+}
+
+.tooltip::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ padding: 8px 12px;
+ border-radius: 4px;
+ border: 1px solid var(--border-color);
+ font-size: 12px;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s ease;
+ z-index: 1000;
+}
+
+.tooltip:hover::after {
+ opacity: 1;
+}
+
+/* 滾動條美化 */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-color);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #606060;
+}
+
+/* 選擇文字顏色 */
+::selection {
+ background: rgba(0, 122, 204, 0.3);
+ color: var(--text-primary);
+}
+
+/* 無障礙改進 */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* 焦點可見性 */
+button:focus-visible,
+input:focus-visible,
+textarea:focus-visible,
+select:focus-visible {
+ outline: 2px solid var(--accent-color);
+ outline-offset: 2px;
+}
+
+/* 禁用狀態 */
+button:disabled,
+input:disabled,
+textarea:disabled,
+select:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* 響應式圖片 */
+img {
+ max-width: 100%;
+ height: auto;
+}
+
+/* 表格樣式 */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 20px;
+}
+
+th, td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid var(--border-color);
+}
+
+th {
+ background: var(--bg-tertiary);
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+tr:hover {
+ background: rgba(255, 255, 255, 0.02);
+}
+
+/* 代碼區塊 */
+code {
+ background: var(--bg-tertiary);
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+ font-size: 0.9em;
+}
+
+pre {
+ background: var(--bg-tertiary);
+ padding: 16px;
+ border-radius: 6px;
+ overflow-x: auto;
+ border: 1px solid var(--border-color);
+}
+
+pre code {
+ background: none;
+ padding: 0;
+}
+
+/* 警告和提示框 */
+.alert {
+ padding: 12px 16px;
+ border-radius: 6px;
+ margin-bottom: 16px;
+ border-left: 4px solid;
+}
+
+.alert-info {
+ background: rgba(33, 150, 243, 0.1);
+ border-left-color: var(--info-color);
+ color: #bbdefb;
+}
+
+.alert-success {
+ background: rgba(76, 175, 80, 0.1);
+ border-left-color: var(--success-color);
+ color: #c8e6c9;
+}
+
+.alert-warning {
+ background: rgba(255, 152, 0, 0.1);
+ border-left-color: var(--warning-color);
+ color: #ffe0b2;
+}
+
+.alert-error {
+ background: rgba(244, 67, 54, 0.1);
+ border-left-color: var(--error-color);
+ color: #ffcdd2;
+}
+
+/* 進度條 */
+.progress {
+ width: 100%;
+ height: 8px;
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.progress-bar {
+ height: 100%;
+ background: var(--accent-color);
+ transition: width 0.3s ease;
+}
+
+/* 分隔線 */
+.divider {
+ height: 1px;
+ background: var(--border-color);
+ margin: 20px 0;
+}
+
+/* 徽章 */
+.badge {
+ display: inline-block;
+ padding: 4px 8px;
+ font-size: 12px;
+ font-weight: 500;
+ border-radius: 12px;
+ background: var(--accent-color);
+ color: white;
+}
+
+.badge-secondary {
+ background: var(--text-secondary);
+}
+
+.badge-success {
+ background: var(--success-color);
+}
+
+.badge-warning {
+ background: var(--warning-color);
+}
+
+.badge-error {
+ background: var(--error-color);
+}
+
+/* 卡片 */
+.card {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 20px;
+}
+
+.card-header {
+ font-weight: 600;
+ margin-bottom: 12px;
+ color: var(--text-primary);
+}
+
+.card-body {
+ color: var(--text-secondary);
+}
+
+/* 統計數字 */
+.stat {
+ text-align: center;
+ padding: 20px;
+}
+
+.stat-value {
+ font-size: 2em;
+ font-weight: bold;
+ color: var(--accent-color);
+ display: block;
+}
+
+.stat-label {
+ color: var(--text-secondary);
+ font-size: 0.9em;
+ margin-top: 4px;
+}
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js
new file mode 100644
index 0000000..23d690a
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/static/js/app.js
@@ -0,0 +1,824 @@
+/**
+ * 主要前端應用
+ * ============
+ *
+ * 處理 WebSocket 通信、分頁切換、圖片上傳、命令執行等功能
+ */
+
+class FeedbackApp {
+ constructor(sessionId) {
+ this.sessionId = sessionId;
+ this.websocket = null;
+ this.images = [];
+ this.isConnected = false;
+ this.combinedMode = false;
+ this.autoClose = true; // 預設開啟
+
+ this.init();
+ }
+
+ async init() {
+ // 等待國際化系統加載完成
+ if (window.i18nManager) {
+ await window.i18nManager.init();
+ }
+
+ // 處理動態摘要內容
+ this.processDynamicSummaryContent();
+
+ // 設置 WebSocket 連接
+ this.setupWebSocket();
+
+ // 設置事件監聽器
+ this.setupEventListeners();
+
+ // 初始化分頁系統
+ this.setupTabs();
+
+ // 設置圖片上傳
+ this.setupImageUpload();
+
+ // 設置鍵盤快捷鍵
+ this.setupKeyboardShortcuts();
+
+ // 載入設定
+ this.loadSettings();
+
+ // 初始化命令終端
+ this.initCommandTerminal();
+
+ // 確保合併模式狀態正確
+ this.applyCombinedModeState();
+
+ console.log('FeedbackApp 初始化完成');
+ }
+
+ processDynamicSummaryContent() {
+ // 處理所有帶有 data-dynamic-content 屬性的元素
+ const dynamicElements = document.querySelectorAll('[data-dynamic-content="aiSummary"]');
+
+ dynamicElements.forEach(element => {
+ const currentContent = element.textContent || element.innerHTML;
+
+ // 檢查是否為測試摘要
+ if (this.isTestSummary(currentContent)) {
+ // 如果是測試摘要,使用翻譯系統的內容
+ if (window.i18nManager) {
+ const translatedSummary = window.i18nManager.t('dynamic.aiSummary');
+ if (translatedSummary && translatedSummary !== 'dynamic.aiSummary') {
+ element.textContent = translatedSummary.trim();
+ console.log('已更新測試摘要為:', window.i18nManager.currentLanguage);
+ }
+ }
+ } else {
+ // 如果不是測試摘要,清理原有內容的前導和尾隨空白
+ element.textContent = currentContent.trim();
+ }
+ });
+ }
+
+ isTestSummary(content) {
+ // 簡化的測試摘要檢測邏輯 - 檢查是否包含任何測試相關關鍵詞
+ const testKeywords = [
+ // 標題關鍵詞(任何語言版本)
+ '測試 Web UI 功能', 'Test Web UI Functionality', '测试 Web UI 功能',
+ '圖片預覽和視窗調整測試', 'Image Preview and Window Adjustment Test', '图片预览和窗口调整测试',
+
+ // 功能測試項目關鍵詞
+ '功能測試項目', 'Test Items', '功能测试项目',
+
+ // 特殊標記
+ '🎯 **功能測試項目', '🎯 **Test Items', '🎯 **功能测试项目',
+ '📋 測試步驟', '📋 Test Steps', '📋 测试步骤',
+
+ // 具體測試功能
+ 'WebSocket 即時通訊', 'WebSocket real-time communication', 'WebSocket 即时通讯',
+ '智能 Ctrl+V', 'Smart Ctrl+V', '智能 Ctrl+V',
+
+ // 測試提示詞
+ '請測試這些功能', 'Please test these features', '请测试这些功能'
+ ];
+
+ // 只要包含任何一個測試關鍵詞就認為是測試摘要
+ return testKeywords.some(keyword => content.includes(keyword));
+ }
+
+ setupWebSocket() {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsUrl = `${protocol}//${window.location.host}/ws/${this.sessionId}`;
+
+ try {
+ this.websocket = new WebSocket(wsUrl);
+
+ this.websocket.onopen = () => {
+ this.isConnected = true;
+ console.log('WebSocket 連接已建立');
+ this.updateConnectionStatus(true);
+ };
+
+ this.websocket.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ this.handleWebSocketMessage(data);
+ };
+
+ this.websocket.onclose = () => {
+ this.isConnected = false;
+ console.log('WebSocket 連接已關閉');
+ this.updateConnectionStatus(false);
+ };
+
+ this.websocket.onerror = (error) => {
+ console.error('WebSocket 錯誤:', error);
+ this.updateConnectionStatus(false);
+ };
+
+ } catch (error) {
+ console.error('WebSocket 連接失敗:', error);
+ this.updateConnectionStatus(false);
+ }
+ }
+
+ handleWebSocketMessage(data) {
+ switch (data.type) {
+ case 'command_output':
+ this.appendCommandOutput(data.output);
+ break;
+ case 'command_complete':
+ this.appendCommandOutput(`\n[命令完成,退出碼: ${data.exit_code}]\n`);
+ this.enableCommandInput();
+ break;
+ case 'command_error':
+ this.appendCommandOutput(`\n[錯誤: ${data.error}]\n`);
+ this.enableCommandInput();
+ break;
+ case 'feedback_received':
+ console.log('回饋已收到');
+ // 顯示成功訊息
+ this.showSuccessMessage();
+ break;
+ default:
+ console.log('未知的 WebSocket 消息:', data);
+ }
+ }
+
+ showSuccessMessage() {
+ // 創建成功訊息提示
+ const message = document.createElement('div');
+ message.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ background: var(--success-color);
+ color: white;
+ padding: 12px 20px;
+ border-radius: 6px;
+ font-weight: 500;
+ z-index: 10000;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ animation: slideIn 0.3s ease-out;
+ `;
+ message.textContent = '✅ 回饋提交成功!';
+
+ // 添加動畫樣式
+ const style = document.createElement('style');
+ style.textContent = `
+ @keyframes slideIn {
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+ }
+ `;
+ document.head.appendChild(style);
+
+ document.body.appendChild(message);
+
+ // 3秒後移除訊息
+ setTimeout(() => {
+ if (message.parentNode) {
+ message.remove();
+ }
+ }, 3000);
+ }
+
+ updateConnectionStatus(connected) {
+ // 更新連接狀態指示器
+ const elements = document.querySelectorAll('.connection-indicator');
+ elements.forEach(el => {
+ el.textContent = connected ? '✅ 已連接' : '❌ 未連接';
+ el.className = `connection-indicator ${connected ? 'connected' : 'disconnected'}`;
+ });
+
+ // 更新命令執行按鈕狀態
+ const runCommandBtn = document.getElementById('runCommandBtn');
+ if (runCommandBtn) {
+ runCommandBtn.disabled = !connected;
+ runCommandBtn.textContent = connected ? '▶️ 執行' : '❌ 未連接';
+ }
+ }
+
+ setupEventListeners() {
+ // 提交回饋按鈕
+ const submitBtn = document.getElementById('submitBtn');
+ if (submitBtn) {
+ submitBtn.addEventListener('click', () => this.submitFeedback());
+ }
+
+ // 取消按鈕
+ const cancelBtn = document.getElementById('cancelBtn');
+ if (cancelBtn) {
+ cancelBtn.addEventListener('click', () => this.cancelFeedback());
+ }
+
+ // 執行命令按鈕
+ const runCommandBtn = document.getElementById('runCommandBtn');
+ if (runCommandBtn) {
+ runCommandBtn.addEventListener('click', () => this.runCommand());
+ }
+
+ // 命令輸入框 Enter 事件 - 修正為使用新的 input 元素
+ const commandInput = document.getElementById('commandInput');
+ if (commandInput) {
+ commandInput.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ this.runCommand();
+ }
+ });
+ }
+
+ // 設定切換
+ this.setupSettingsListeners();
+ }
+
+ setupTabs() {
+ const tabButtons = document.querySelectorAll('.tab-button');
+ const tabContents = document.querySelectorAll('.tab-content');
+
+ tabButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const targetTab = button.getAttribute('data-tab');
+
+ // 移除所有活躍狀態
+ tabButtons.forEach(btn => btn.classList.remove('active'));
+ tabContents.forEach(content => content.classList.remove('active'));
+
+ // 添加活躍狀態
+ button.classList.add('active');
+ const targetContent = document.getElementById(`tab-${targetTab}`);
+ if (targetContent) {
+ targetContent.classList.add('active');
+ }
+
+ // 保存當前分頁
+ localStorage.setItem('activeTab', targetTab);
+ });
+ });
+
+ // 恢復上次的活躍分頁
+ const savedTab = localStorage.getItem('activeTab');
+ if (savedTab) {
+ const savedButton = document.querySelector(`[data-tab="${savedTab}"]`);
+ if (savedButton) {
+ savedButton.click();
+ }
+ }
+ }
+
+ setupImageUpload() {
+ const imageUploadArea = document.getElementById('imageUploadArea');
+ const imageInput = document.getElementById('imageInput');
+ const imagePreviewContainer = document.getElementById('imagePreviewContainer');
+
+ if (!imageUploadArea || !imageInput || !imagePreviewContainer) {
+ return;
+ }
+
+ // 原始分頁的圖片上傳
+ this.setupImageUploadForArea(imageUploadArea, imageInput, imagePreviewContainer);
+
+ // 合併模式的圖片上傳
+ const combinedImageUploadArea = document.getElementById('combinedImageUploadArea');
+ const combinedImageInput = document.getElementById('combinedImageInput');
+ const combinedImagePreviewContainer = document.getElementById('combinedImagePreviewContainer');
+
+ if (combinedImageUploadArea && combinedImageInput && combinedImagePreviewContainer) {
+ this.setupImageUploadForArea(combinedImageUploadArea, combinedImageInput, combinedImagePreviewContainer);
+ }
+ }
+
+ setupImageUploadForArea(uploadArea, input, previewContainer) {
+ // 點擊上傳區域
+ uploadArea.addEventListener('click', () => {
+ input.click();
+ });
+
+ // 文件選擇
+ input.addEventListener('change', (e) => {
+ this.handleFileSelection(e.target.files);
+ });
+
+ // 拖放事件
+ uploadArea.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ uploadArea.classList.add('dragover');
+ });
+
+ uploadArea.addEventListener('dragleave', (e) => {
+ e.preventDefault();
+ uploadArea.classList.remove('dragover');
+ });
+
+ uploadArea.addEventListener('drop', (e) => {
+ e.preventDefault();
+ uploadArea.classList.remove('dragover');
+ this.handleFileSelection(e.dataTransfer.files);
+ });
+ }
+
+ setupKeyboardShortcuts() {
+ document.addEventListener('keydown', (e) => {
+ // Ctrl+Enter 或 Cmd+Enter 提交回饋
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
+ e.preventDefault();
+ this.submitFeedback();
+ }
+
+ // ESC 取消
+ if (e.key === 'Escape') {
+ this.cancelFeedback();
+ }
+ });
+
+ // 設置 Ctrl+V 貼上圖片監聽器
+ this.setupPasteListener();
+ }
+
+ setupPasteListener() {
+ document.addEventListener('paste', (e) => {
+ // 檢查是否在回饋文字框中
+ const feedbackText = document.getElementById('feedbackText');
+ const combinedFeedbackText = document.getElementById('combinedFeedbackText');
+
+ const isInFeedbackInput = document.activeElement === feedbackText ||
+ document.activeElement === combinedFeedbackText;
+
+ if (isInFeedbackInput) {
+ console.log('偵測到在回饋輸入框中貼上');
+ this.handlePasteEvent(e);
+ }
+ });
+ }
+
+ handlePasteEvent(e) {
+ const clipboardData = e.clipboardData || window.clipboardData;
+ if (!clipboardData) return;
+
+ const items = clipboardData.items;
+ let hasImages = false;
+
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+
+ if (item.type.indexOf('image') !== -1) {
+ hasImages = true;
+ e.preventDefault(); // 防止文字也被貼上
+
+ const file = item.getAsFile();
+ if (file) {
+ console.log('從剪貼簿貼上圖片:', file.name, file.type);
+ this.addImage(file);
+ }
+ }
+ }
+
+ if (hasImages) {
+ console.log('已處理剪貼簿圖片');
+ }
+ }
+
+ setupSettingsListeners() {
+ // 合併模式開關
+ const combinedModeToggle = document.getElementById('combinedModeToggle');
+ if (combinedModeToggle) {
+ combinedModeToggle.addEventListener('click', () => {
+ this.toggleCombinedMode();
+ });
+ }
+
+ // 自動關閉開關
+ const autoCloseToggle = document.getElementById('autoCloseToggle');
+ if (autoCloseToggle) {
+ autoCloseToggle.addEventListener('click', () => {
+ this.toggleAutoClose();
+ });
+ }
+
+ // 語言選擇器
+ const languageOptions = document.querySelectorAll('.language-option');
+ languageOptions.forEach(option => {
+ option.addEventListener('click', () => {
+ const language = option.getAttribute('data-lang');
+ this.setLanguage(language);
+ });
+ });
+ }
+
+ setLanguage(language) {
+ // 更新語言選擇器的活躍狀態
+ const languageOptions = document.querySelectorAll('.language-option');
+ languageOptions.forEach(option => {
+ option.classList.remove('active');
+ if (option.getAttribute('data-lang') === language) {
+ option.classList.add('active');
+ }
+ });
+
+ // 調用國際化管理器
+ if (window.i18nManager) {
+ window.i18nManager.setLanguage(language);
+
+ // 語言切換後重新處理動態摘要內容
+ setTimeout(() => {
+ console.log('語言切換到:', language, '- 重新處理動態內容');
+ this.processDynamicSummaryContent();
+ }, 200); // 增加延遲時間確保翻譯加載完成
+ }
+ }
+
+ handleFileSelection(files) {
+ for (let file of files) {
+ if (file.type.startsWith('image/')) {
+ this.addImage(file);
+ }
+ }
+ }
+
+ addImage(file) {
+ if (file.size > 1024 * 1024) { // 1MB
+ alert('圖片大小不能超過 1MB');
+ return;
+ }
+
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ const imageData = {
+ name: file.name,
+ data: e.target.result.split(',')[1], // 移除 data:image/...;base64, 前綴
+ size: file.size,
+ type: file.type,
+ preview: e.target.result
+ };
+
+ this.images.push(imageData);
+ this.updateImagePreview();
+ };
+ reader.readAsDataURL(file);
+ }
+
+ updateImagePreview() {
+ // 更新原始分頁的圖片預覽
+ this.updateImagePreviewForContainer('imagePreviewContainer', 'imageUploadArea');
+
+ // 更新合併模式的圖片預覽
+ this.updateImagePreviewForContainer('combinedImagePreviewContainer', 'combinedImageUploadArea');
+ }
+
+ updateImagePreviewForContainer(containerId, uploadAreaId) {
+ const container = document.getElementById(containerId);
+ const uploadArea = document.getElementById(uploadAreaId);
+ if (!container || !uploadArea) return;
+
+ container.innerHTML = '';
+
+ // 更新上傳區域的樣式
+ if (this.images.length > 0) {
+ uploadArea.classList.add('has-images');
+ } else {
+ uploadArea.classList.remove('has-images');
+ }
+
+ this.images.forEach((image, index) => {
+ const preview = document.createElement('div');
+ preview.className = 'image-preview';
+ preview.innerHTML = `
+
+
+ `;
+ container.appendChild(preview);
+ });
+ }
+
+ removeImage(index) {
+ this.images.splice(index, 1);
+ this.updateImagePreview();
+ }
+
+ runCommand() {
+ const commandInput = document.getElementById('commandInput');
+ const command = commandInput?.value.trim();
+
+ if (!command) {
+ this.appendCommandOutput('⚠️ 請輸入命令\n');
+ return;
+ }
+
+ if (!this.isConnected) {
+ this.appendCommandOutput('❌ WebSocket 未連接,無法執行命令\n');
+ return;
+ }
+
+ // 禁用輸入和按鈕
+ this.disableCommandInput();
+
+ // 顯示執行的命令,使用 terminal 風格
+ this.appendCommandOutput(`$ ${command}\n`);
+
+ // 發送命令
+ try {
+ this.websocket.send(JSON.stringify({
+ type: 'run_command',
+ command: command
+ }));
+
+ // 清空輸入框
+ commandInput.value = '';
+
+ // 顯示正在執行的狀態
+ this.appendCommandOutput('[正在執行...]\n');
+
+ } catch (error) {
+ this.appendCommandOutput(`❌ 發送命令失敗: ${error.message}\n`);
+ this.enableCommandInput();
+ }
+ }
+
+ disableCommandInput() {
+ const commandInput = document.getElementById('commandInput');
+ const runCommandBtn = document.getElementById('runCommandBtn');
+
+ if (commandInput) {
+ commandInput.disabled = true;
+ commandInput.style.opacity = '0.6';
+ }
+ if (runCommandBtn) {
+ runCommandBtn.disabled = true;
+ runCommandBtn.textContent = '⏳ 執行中...';
+ }
+ }
+
+ enableCommandInput() {
+ const commandInput = document.getElementById('commandInput');
+ const runCommandBtn = document.getElementById('runCommandBtn');
+
+ if (commandInput) {
+ commandInput.disabled = false;
+ commandInput.style.opacity = '1';
+ commandInput.focus(); // 自動聚焦到輸入框
+ }
+ if (runCommandBtn) {
+ runCommandBtn.disabled = false;
+ runCommandBtn.textContent = '▶️ 執行';
+ }
+ }
+
+ appendCommandOutput(text) {
+ const output = document.getElementById('commandOutput');
+ if (output) {
+ output.textContent += text;
+ output.scrollTop = output.scrollHeight;
+
+ // 添加時間戳(可選)
+ if (text.includes('[命令完成') || text.includes('[錯誤:')) {
+ const timestamp = new Date().toLocaleTimeString();
+ output.textContent += `[${timestamp}]\n`;
+ }
+ }
+ }
+
+ submitFeedback() {
+ let feedbackText;
+
+ // 根據當前模式選擇正確的輸入框
+ if (this.combinedMode) {
+ const combinedFeedbackInput = document.getElementById('combinedFeedbackText');
+ feedbackText = combinedFeedbackInput?.value.trim() || '';
+ } else {
+ const feedbackInput = document.getElementById('feedbackText');
+ feedbackText = feedbackInput?.value.trim() || '';
+ }
+
+ const feedback = feedbackText;
+
+ if (!feedback && this.images.length === 0) {
+ alert('請提供回饋文字或上傳圖片');
+ return;
+ }
+
+ if (!this.isConnected) {
+ alert('WebSocket 未連接');
+ return;
+ }
+
+ // 準備圖片數據
+ const imageData = this.images.map(img => ({
+ name: img.name,
+ data: img.data,
+ size: img.size,
+ type: img.type
+ }));
+
+ // 發送回饋
+ this.websocket.send(JSON.stringify({
+ type: 'submit_feedback',
+ feedback: feedback,
+ images: imageData
+ }));
+
+ console.log('回饋已提交');
+
+ // 根據設定決定是否自動關閉頁面
+ if (this.autoClose) {
+ // 稍微延遲一下讓用戶看到提交成功的反饋
+ setTimeout(() => {
+ window.close();
+ }, 1000);
+ }
+ }
+
+ cancelFeedback() {
+ if (confirm('確定要取消回饋嗎?')) {
+ window.close();
+ }
+ }
+
+ toggleCombinedMode() {
+ this.combinedMode = !this.combinedMode;
+
+ const toggle = document.getElementById('combinedModeToggle');
+ if (toggle) {
+ toggle.classList.toggle('active', this.combinedMode);
+ }
+
+ // 顯示/隱藏分頁
+ const feedbackTab = document.querySelector('[data-tab="feedback"]');
+ const summaryTab = document.querySelector('[data-tab="summary"]');
+ const combinedTab = document.querySelector('[data-tab="combined"]');
+
+ if (this.combinedMode) {
+ // 啟用合併模式:隱藏原本的回饋和摘要分頁,顯示合併分頁
+ if (feedbackTab) feedbackTab.classList.add('hidden');
+ if (summaryTab) summaryTab.classList.add('hidden');
+ if (combinedTab) {
+ combinedTab.classList.remove('hidden');
+ // 如果合併分頁顯示,並且當前在回饋或摘要分頁,則將合併分頁設為活躍
+ const currentActiveTab = document.querySelector('.tab-button.active');
+ if (currentActiveTab && (currentActiveTab.getAttribute('data-tab') === 'feedback' || currentActiveTab.getAttribute('data-tab') === 'summary')) {
+ combinedTab.classList.add('active');
+ currentActiveTab.classList.remove('active');
+
+ // 顯示對應的分頁內容
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
+ document.getElementById('tab-combined').classList.add('active');
+ }
+ }
+
+ // 同步數據到合併模式
+ this.syncDataToCombinedMode();
+
+ } else {
+ // 停用合併模式:顯示原本的分頁,隱藏合併分頁
+ if (feedbackTab) feedbackTab.classList.remove('hidden');
+ if (summaryTab) summaryTab.classList.remove('hidden');
+ if (combinedTab) {
+ combinedTab.classList.add('hidden');
+ // 如果當前在合併分頁,則切換到回饋分頁
+ if (combinedTab.classList.contains('active')) {
+ combinedTab.classList.remove('active');
+ if (feedbackTab) {
+ feedbackTab.classList.add('active');
+ // 顯示對應的分頁內容
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
+ document.getElementById('tab-feedback').classList.add('active');
+ }
+ }
+ }
+
+ // 同步數據回原始分頁
+ this.syncDataFromCombinedMode();
+ }
+
+ localStorage.setItem('combinedMode', this.combinedMode.toString());
+
+ console.log('合併模式已', this.combinedMode ? '啟用' : '停用');
+ }
+
+ toggleAutoClose() {
+ this.autoClose = !this.autoClose;
+
+ const toggle = document.getElementById('autoCloseToggle');
+ if (toggle) {
+ toggle.classList.toggle('active', this.autoClose);
+ }
+
+ localStorage.setItem('autoClose', this.autoClose.toString());
+
+ console.log('自動關閉頁面已', this.autoClose ? '啟用' : '停用');
+ }
+
+ syncDataToCombinedMode() {
+ // 同步回饋文字
+ const feedbackText = document.getElementById('feedbackText');
+ const combinedFeedbackText = document.getElementById('combinedFeedbackText');
+ if (feedbackText && combinedFeedbackText) {
+ combinedFeedbackText.value = feedbackText.value;
+ }
+
+ // 同步摘要內容
+ const summaryContent = document.getElementById('summaryContent');
+ const combinedSummaryContent = document.getElementById('combinedSummaryContent');
+ if (summaryContent && combinedSummaryContent) {
+ combinedSummaryContent.textContent = summaryContent.textContent;
+ }
+ }
+
+ syncDataFromCombinedMode() {
+ // 同步回饋文字
+ const feedbackText = document.getElementById('feedbackText');
+ const combinedFeedbackText = document.getElementById('combinedFeedbackText');
+ if (feedbackText && combinedFeedbackText) {
+ feedbackText.value = combinedFeedbackText.value;
+ }
+ }
+
+ loadSettings() {
+ // 載入合併模式設定
+ const savedCombinedMode = localStorage.getItem('combinedMode');
+ if (savedCombinedMode === 'true') {
+ this.combinedMode = true;
+ const toggle = document.getElementById('combinedModeToggle');
+ if (toggle) {
+ toggle.classList.add('active');
+ }
+
+ // 應用合併模式設定
+ this.applyCombinedModeState();
+ }
+
+ // 載入自動關閉設定
+ const savedAutoClose = localStorage.getItem('autoClose');
+ if (savedAutoClose !== null) {
+ this.autoClose = savedAutoClose === 'true';
+ } else {
+ // 如果沒有保存的設定,使用預設值(true)
+ this.autoClose = true;
+ }
+
+ // 更新自動關閉開關狀態
+ const autoCloseToggle = document.getElementById('autoCloseToggle');
+ if (autoCloseToggle) {
+ autoCloseToggle.classList.toggle('active', this.autoClose);
+ }
+ }
+
+ applyCombinedModeState() {
+ const feedbackTab = document.querySelector('[data-tab="feedback"]');
+ const summaryTab = document.querySelector('[data-tab="summary"]');
+ const combinedTab = document.querySelector('[data-tab="combined"]');
+
+ if (this.combinedMode) {
+ // 隱藏原本的回饋和摘要分頁,顯示合併分頁
+ if (feedbackTab) feedbackTab.classList.add('hidden');
+ if (summaryTab) summaryTab.classList.add('hidden');
+ if (combinedTab) combinedTab.classList.remove('hidden');
+ } else {
+ // 顯示原本的分頁,隱藏合併分頁
+ if (feedbackTab) feedbackTab.classList.remove('hidden');
+ if (summaryTab) summaryTab.classList.remove('hidden');
+ if (combinedTab) combinedTab.classList.add('hidden');
+ }
+ }
+
+ initCommandTerminal() {
+ // 使用翻譯的歡迎信息
+ if (window.i18nManager) {
+ const welcomeTemplate = window.i18nManager.t('dynamic.terminalWelcome');
+ if (welcomeTemplate && welcomeTemplate !== 'dynamic.terminalWelcome') {
+ const welcomeMessage = welcomeTemplate.replace('{sessionId}', this.sessionId);
+ this.appendCommandOutput(welcomeMessage);
+ return;
+ }
+ }
+
+ // 回退到預設歡迎信息(如果翻譯不可用)
+ const welcomeMessage = `Welcome to Interactive Feedback Terminal
+========================================
+Project Directory: ${this.sessionId}
+Enter commands and press Enter or click Execute button
+Supported commands: ls, dir, pwd, cat, type, etc.
+
+$ `;
+ this.appendCommandOutput(welcomeMessage);
+ }
+}
+
+// 全域函數,供 HTML 中的 onclick 使用
+window.feedbackApp = null;
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/static/js/i18n.js b/src/mcp_feedback_enhanced/web/static/js/i18n.js
new file mode 100644
index 0000000..fb25ac0
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/static/js/i18n.js
@@ -0,0 +1,203 @@
+/**
+ * 國際化(i18n)模組
+ * =================
+ *
+ * 處理多語言支援和界面文字翻譯
+ * 從後端 /api/translations 載入翻譯數據
+ */
+
+class I18nManager {
+ constructor() {
+ this.currentLanguage = 'zh-TW';
+ this.translations = {};
+ this.loadingPromise = null;
+ }
+
+ async init() {
+ // 從 localStorage 載入語言偏好
+ const savedLanguage = localStorage.getItem('language');
+ if (savedLanguage) {
+ this.currentLanguage = savedLanguage;
+ }
+
+ // 載入翻譯數據
+ await this.loadTranslations();
+
+ // 應用翻譯
+ this.applyTranslations();
+
+ // 設置語言選擇器
+ this.setupLanguageSelectors();
+
+ // 延遲一點再更新動態內容,確保應用程式已初始化
+ setTimeout(() => {
+ this.updateDynamicContent();
+ }, 100);
+ }
+
+ async loadTranslations() {
+ if (this.loadingPromise) {
+ return this.loadingPromise;
+ }
+
+ this.loadingPromise = fetch('/api/translations')
+ .then(response => response.json())
+ .then(data => {
+ this.translations = data;
+ console.log('翻譯數據載入完成:', Object.keys(this.translations));
+
+ // 檢查當前語言是否有翻譯數據
+ if (!this.translations[this.currentLanguage] || Object.keys(this.translations[this.currentLanguage]).length === 0) {
+ console.warn(`當前語言 ${this.currentLanguage} 沒有翻譯數據,回退到 zh-TW`);
+ this.currentLanguage = 'zh-TW';
+ }
+ })
+ .catch(error => {
+ console.error('載入翻譯數據失敗:', error);
+ // 使用最小的回退翻譯
+ this.translations = this.getMinimalFallbackTranslations();
+ });
+
+ return this.loadingPromise;
+ }
+
+ getMinimalFallbackTranslations() {
+ // 最小的回退翻譯,只包含關鍵項目
+ return {
+ 'zh-TW': {
+ 'app': {
+ 'title': 'Interactive Feedback MCP',
+ 'projectDirectory': '專案目錄'
+ },
+ 'tabs': {
+ 'feedback': '💬 回饋',
+ 'summary': '📋 AI 摘要',
+ 'command': '⚡ 命令',
+ 'settings': '⚙️ 設定'
+ },
+ 'buttons': {
+ 'cancel': '❌ 取消',
+ 'submit': '✅ 提交回饋'
+ },
+ 'settings': {
+ 'language': '語言'
+ }
+ }
+ };
+ }
+
+ // 支援巢狀鍵值的翻譯函數
+ t(key, defaultValue = '') {
+ const langData = this.translations[this.currentLanguage] || {};
+ return this.getNestedValue(langData, key) || defaultValue || key;
+ }
+
+ getNestedValue(obj, path) {
+ return path.split('.').reduce((current, key) => {
+ return current && current[key] !== undefined ? current[key] : null;
+ }, obj);
+ }
+
+ setLanguage(language) {
+ if (this.translations[language]) {
+ this.currentLanguage = language;
+ localStorage.setItem('language', language);
+ this.applyTranslations();
+
+ // 更新語言選擇器(只更新設定頁面的)
+ const selector = document.getElementById('settingsLanguageSelect');
+ if (selector) {
+ selector.value = language;
+ }
+
+ // 更新 HTML lang 屬性
+ document.documentElement.lang = language;
+
+ console.log('語言已切換到:', language);
+ } else {
+ console.warn('不支援的語言:', language);
+ }
+ }
+
+ applyTranslations() {
+ // 翻譯所有有 data-i18n 屬性的元素
+ const elements = document.querySelectorAll('[data-i18n]');
+ elements.forEach(element => {
+ const key = element.getAttribute('data-i18n');
+ const translation = this.t(key);
+ if (translation && translation !== key) {
+ element.textContent = translation;
+ }
+ });
+
+ // 翻譯有 data-i18n-placeholder 屬性的元素
+ const placeholderElements = document.querySelectorAll('[data-i18n-placeholder]');
+ placeholderElements.forEach(element => {
+ const key = element.getAttribute('data-i18n-placeholder');
+ const translation = this.t(key);
+ if (translation && translation !== key) {
+ element.placeholder = translation;
+ }
+ });
+
+ // 更新動態內容
+ this.updateDynamicContent();
+
+ console.log('翻譯已應用:', this.currentLanguage);
+ }
+
+ updateDynamicContent() {
+ // 只更新終端歡迎信息,不要覆蓋 AI 摘要
+ this.updateTerminalWelcome();
+ }
+
+ updateTerminalWelcome() {
+ const commandOutput = document.getElementById('commandOutput');
+ if (commandOutput && window.feedbackApp) {
+ const welcomeTemplate = this.t('dynamic.terminalWelcome');
+ if (welcomeTemplate && welcomeTemplate !== 'dynamic.terminalWelcome') {
+ const welcomeMessage = welcomeTemplate.replace('{sessionId}', window.feedbackApp.sessionId || 'unknown');
+ commandOutput.textContent = welcomeMessage;
+ }
+ }
+ }
+
+ setupLanguageSelectors() {
+ // 舊版下拉選擇器(兼容性保留)
+ const selector = document.getElementById('settingsLanguageSelect');
+ if (selector) {
+ // 設置當前值
+ selector.value = this.currentLanguage;
+
+ // 添加事件監聽器
+ selector.addEventListener('change', (e) => {
+ this.setLanguage(e.target.value);
+ });
+ }
+
+ // 新版現代化語言選擇器
+ const languageOptions = document.querySelectorAll('.language-option');
+ if (languageOptions.length > 0) {
+ // 設置當前語言的活躍狀態
+ languageOptions.forEach(option => {
+ const lang = option.getAttribute('data-lang');
+ if (lang === this.currentLanguage) {
+ option.classList.add('active');
+ } else {
+ option.classList.remove('active');
+ }
+ });
+ }
+ }
+
+ getCurrentLanguage() {
+ return this.currentLanguage;
+ }
+
+ getAvailableLanguages() {
+ return Object.keys(this.translations);
+ }
+}
+
+// 創建全域實例
+window.i18nManager = new I18nManager();
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html
new file mode 100644
index 0000000..be89921
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/templates/feedback.html
@@ -0,0 +1,918 @@
+
+
+
+ Web UI 互動式回饋收集工具 +
+服務器運行中...
- - - """ - - def _get_simple_feedback_html(self, session_id: str, session: WebFeedbackSession) -> str: - """簡單的回饋頁面 HTML""" - return f""" - - - - -{session.summary}
-