diff --git a/src/mcp_feedback_enhanced/i18n.py b/src/mcp_feedback_enhanced/i18n.py
index b6ac67a..2e334a1 100644
--- a/src/mcp_feedback_enhanced/i18n.py
+++ b/src/mcp_feedback_enhanced/i18n.py
@@ -30,8 +30,8 @@ class I18nManager:
def __init__(self):
self._current_language = None
self._translations = {}
- self._supported_languages = ["zh-TW", "en", "zh-CN"]
- self._fallback_language = "en"
+ self._supported_languages = ["zh-CN", "zh-TW", "en"]
+ self._fallback_language = "zh-CN"
self._config_file = self._get_config_file_path()
self._locales_dir = Path(__file__).parent / "web" / "locales"
@@ -139,7 +139,7 @@ class I18nManager:
def get_current_language(self) -> str:
"""獲取當前語言"""
- return self._current_language or "zh-TW"
+ return self._current_language or "zh-CN"
def set_language(self, language: str) -> bool:
"""設定語言"""
diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json
index 7f268f8..1e7b429 100644
--- a/src/mcp_feedback_enhanced/web/locales/en/translation.json
+++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json
@@ -156,6 +156,10 @@
"submitted": {
"title": "Submitted",
"message": "Waiting for next MCP call"
+ },
+ "completed": {
+ "title": "Completed",
+ "message": "Session completed"
}
},
"notifications": {
diff --git a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
index 5f85cb3..4522fa7 100644
--- a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
+++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json
@@ -156,6 +156,10 @@
"submitted": {
"title": "反馈已提交",
"message": "等待下次 MCP 调用"
+ },
+ "completed": {
+ "title": "已完成",
+ "message": "会话已完成"
}
},
"notifications": {
diff --git a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
index 17de62d..e623206 100644
--- a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
+++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json
@@ -161,6 +161,10 @@
"submitted": {
"title": "回饋已提交",
"message": "等待下次 MCP 調用"
+ },
+ "completed": {
+ "title": "已完成",
+ "message": "會話已完成"
}
},
"notifications": {
diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py
index 49970ac..1887ebb 100644
--- a/src/mcp_feedback_enhanced/web/main.py
+++ b/src/mcp_feedback_enhanced/web/main.py
@@ -318,25 +318,46 @@ class WebUIManager:
def create_session(self, project_directory: str, summary: str) -> str:
"""創建新的回饋會話 - 重構為單一活躍會話模式,保留標籤頁狀態"""
- # 保存舊會話的 WebSocket 連接以便發送更新通知
+ # 保存舊會話的引用和 WebSocket 連接
+ old_session = self.current_session
old_websocket = None
- if self.current_session and self.current_session.websocket:
- old_websocket = self.current_session.websocket
+ if old_session and old_session.websocket:
+ old_websocket = old_session.websocket
debug_log("保存舊會話的 WebSocket 連接以發送更新通知")
- # 如果已有活躍會話,先保存其標籤頁狀態到全局狀態
- if self.current_session:
- debug_log("保存現有會話的標籤頁狀態並清理會話")
- # 保存標籤頁狀態到全局
- if hasattr(self.current_session, "active_tabs"):
- self._merge_tabs_to_global(self.current_session.active_tabs)
-
- # 同步清理會話資源(但保留 WebSocket 連接)
- self.current_session._cleanup_sync()
-
+ # 創建新會話
session_id = str(uuid.uuid4())
session = WebFeedbackSession(session_id, project_directory, summary)
+ # 如果有舊會話,處理狀態轉換和清理
+ if old_session:
+ debug_log(f"處理舊會話 {old_session.session_id} 的狀態轉換,當前狀態: {old_session.status.value}")
+
+ # 保存標籤頁狀態到全局
+ if hasattr(old_session, "active_tabs"):
+ self._merge_tabs_to_global(old_session.active_tabs)
+
+ # 如果舊會話是已提交狀態,進入下一步(已完成)
+ if old_session.status == SessionStatus.FEEDBACK_SUBMITTED:
+ debug_log(f"舊會話 {old_session.session_id} 進入下一步:已提交 → 已完成")
+ success = old_session.next_step("反饋已處理,會話完成")
+ if success:
+ debug_log(f"✅ 舊會話 {old_session.session_id} 成功進入已完成狀態")
+ else:
+ debug_log(f"❌ 舊會話 {old_session.session_id} 無法進入下一步")
+ else:
+ debug_log(f"舊會話 {old_session.session_id} 狀態為 {old_session.status.value},無需轉換")
+
+ # 確保舊會話仍在字典中(用於API獲取)
+ if old_session.session_id in self.sessions:
+ debug_log(f"舊會話 {old_session.session_id} 仍在會話字典中")
+ else:
+ debug_log(f"⚠️ 舊會話 {old_session.session_id} 不在會話字典中,重新添加")
+ self.sessions[old_session.session_id] = old_session
+
+ # 同步清理會話資源(但保留 WebSocket 連接)
+ old_session._cleanup_sync()
+
# 將全局標籤頁狀態繼承到新會話
session.active_tabs = self.global_active_tabs.copy()
@@ -348,25 +369,11 @@ class WebUIManager:
debug_log(f"創建新的活躍會話: {session_id}")
debug_log(f"繼承 {len(session.active_tabs)} 個活躍標籤頁")
- # 處理會話更新通知
+ # 處理WebSocket連接轉移
if old_websocket:
- # 有舊連接,立即發送會話更新通知並轉移連接
- self._old_websocket_for_update = old_websocket
- self._new_session_for_update = session
- debug_log("已保存舊 WebSocket 連接,準備發送會話更新通知")
-
- # 立即發送會話更新通知
- import asyncio
-
- try:
- # 在後台任務中發送通知並轉移連接
- asyncio.create_task(self._send_immediate_session_update())
- except Exception as e:
- debug_log(f"創建會話更新任務失敗: {e}")
- # 即使任務創建失敗,也要嘗試直接轉移連接
- session.websocket = old_websocket
- debug_log("任務創建失敗,直接轉移 WebSocket 連接到新會話")
- self._pending_session_update = True
+ # 直接轉移連接到新會話,消息發送由 smart_open_browser 統一處理
+ session.websocket = old_websocket
+ debug_log("已將舊 WebSocket 連接轉移到新會話")
else:
# 沒有舊連接,標記需要發送會話更新通知(當新 WebSocket 連接建立時)
self._pending_session_update = True
@@ -584,8 +591,14 @@ class WebUIManager:
has_active_tabs = await self._check_active_tabs()
if has_active_tabs:
+ debug_log("檢測到活躍標籤頁,發送刷新通知")
+ debug_log(f"向現有標籤頁發送刷新通知:{url}")
+
+ # 向現有標籤頁發送刷新通知
+ refresh_success = await self.notify_existing_tab_to_refresh()
+
+ debug_log(f"刷新通知發送結果: {refresh_success}")
debug_log("檢測到活躍標籤頁,不開啟新瀏覽器視窗")
- debug_log(f"用戶可以在現有標籤頁中查看更新:{url}")
return True
# 沒有活躍標籤頁,開啟新瀏覽器視窗
@@ -676,105 +689,9 @@ class WebUIManager:
else:
debug_log("沒有活躍的桌面應用程式實例")
- async def notify_session_update(self, session):
- """向活躍標籤頁發送會話更新通知"""
- try:
- # 檢查是否有活躍的 WebSocket 連接
- if session.websocket:
- # 直接通過當前會話的 WebSocket 發送
- await session.websocket.send_json(
- {
- "type": "session_updated",
- "message": "新會話已創建,正在更新頁面內容",
- "session_info": {
- "project_directory": session.project_directory,
- "summary": session.summary,
- "session_id": session.session_id,
- },
- }
- )
- debug_log("會話更新通知已通過 WebSocket 發送")
- else:
- # 沒有活躍連接,設置待更新標記
- self._pending_session_update = True
- debug_log("沒有活躍 WebSocket 連接,設置待更新標記")
- except Exception as e:
- debug_log(f"發送會話更新通知失敗: {e}")
- # 設置待更新標記作為備用方案
- self._pending_session_update = True
- async def _send_immediate_session_update(self):
- """立即發送會話更新通知(使用舊的 WebSocket 連接)"""
- try:
- # 檢查是否有保存的舊 WebSocket 連接
- if hasattr(self, "_old_websocket_for_update") and hasattr(
- self, "_new_session_for_update"
- ):
- old_websocket = self._old_websocket_for_update
- new_session = self._new_session_for_update
- # 改進的連接有效性檢查
- websocket_valid = False
- if old_websocket:
- try:
- # 檢查 WebSocket 連接狀態
- if hasattr(old_websocket, "client_state"):
- websocket_valid = (
- old_websocket.client_state
- != old_websocket.client_state.DISCONNECTED
- )
- else:
- # 如果沒有 client_state 屬性,嘗試發送測試消息來檢查連接
- websocket_valid = True
- except Exception as check_error:
- debug_log(f"檢查 WebSocket 連接狀態失敗: {check_error}")
- websocket_valid = False
- if websocket_valid:
- try:
- # 發送會話更新通知
- await old_websocket.send_json(
- {
- "type": "session_updated",
- "message": "新會話已創建,正在更新頁面內容",
- "session_info": {
- "project_directory": new_session.project_directory,
- "summary": new_session.summary,
- "session_id": new_session.session_id,
- },
- }
- )
- debug_log("已通過舊 WebSocket 連接發送會話更新通知")
-
- # 延遲一小段時間讓前端處理消息
- await asyncio.sleep(0.2)
-
- # 將 WebSocket 連接轉移到新會話
- new_session.websocket = old_websocket
- debug_log("已將 WebSocket 連接轉移到新會話")
-
- except Exception as send_error:
- debug_log(f"發送會話更新通知失敗: {send_error}")
- # 如果發送失敗,仍然嘗試轉移連接
- new_session.websocket = old_websocket
- debug_log("發送失敗但仍轉移 WebSocket 連接到新會話")
- else:
- debug_log("舊 WebSocket 連接無效,設置待更新標記")
- self._pending_session_update = True
-
- # 清理臨時變數
- delattr(self, "_old_websocket_for_update")
- delattr(self, "_new_session_for_update")
-
- else:
- # 沒有舊連接,設置待更新標記
- self._pending_session_update = True
- debug_log("沒有舊 WebSocket 連接,設置待更新標記")
-
- except Exception as e:
- debug_log(f"立即發送會話更新通知失敗: {e}")
- # 回退到待更新標記
- self._pending_session_update = True
async def _safe_close_websocket(self, websocket):
"""安全關閉 WebSocket 連接,避免事件循環衝突 - 僅在連接已轉移後調用"""
@@ -799,40 +716,67 @@ class WebUIManager:
except Exception as e:
debug_log(f"檢查 WebSocket 連接狀態時發生錯誤: {e}")
- async def _check_active_tabs(self) -> bool:
- """檢查是否有活躍標籤頁 - 優先檢查全局狀態,回退到 API"""
+ async def notify_existing_tab_to_refresh(self) -> bool:
+ """通知現有標籤頁刷新顯示新會話內容
+
+ Returns:
+ bool: True 表示成功發送,False 表示失敗
+ """
try:
- # 首先檢查全局標籤頁狀態
- global_count = self.get_global_active_tabs_count()
- if global_count > 0:
- debug_log(f"檢測到 {global_count} 個全局活躍標籤頁")
+ if not self.current_session or not self.current_session.websocket:
+ debug_log("沒有活躍的WebSocket連接,無法發送刷新通知")
+ return False
+
+ # 構建刷新通知消息
+ refresh_message = {
+ "type": "session_updated",
+ "action": "new_session_created",
+ "message": "新的 MCP 會話已創建,頁面將自動刷新",
+ "session_info": {
+ "session_id": self.current_session.session_id,
+ "project_directory": self.current_session.project_directory,
+ "summary": self.current_session.summary,
+ "status": self.current_session.status.value
+ }
+ }
+
+ # 發送刷新通知
+ await self.current_session.websocket.send_json(refresh_message)
+ debug_log(f"已向現有標籤頁發送刷新通知: {self.current_session.session_id}")
+
+ # 簡單等待一下讓消息發送完成
+ await asyncio.sleep(0.2)
+ debug_log("刷新通知發送完成")
+ return True
+
+ except Exception as e:
+ debug_log(f"發送刷新通知失敗: {e}")
+ return False
+
+ async def _check_active_tabs(self) -> bool:
+ """檢查是否有活躍標籤頁 - 檢查所有會話的WebSocket連接"""
+ try:
+ # 檢查當前會話的WebSocket連接
+ if self.current_session and self.current_session.websocket:
+ debug_log("檢測到當前會話的WebSocket連接")
return True
- # 如果全局狀態沒有活躍標籤頁,嘗試通過 API 檢查
- # 等待一小段時間讓服務器完全啟動
- await asyncio.sleep(0.5)
+ # 檢查其他會話的WebSocket連接
+ active_websockets = 0
+ for session_id, session in self.sessions.items():
+ if session.websocket:
+ active_websockets += 1
+ debug_log(f"檢測到會話 {session_id} 的WebSocket連接")
- # 調用活躍標籤頁 API
- import aiohttp
+ if active_websockets > 0:
+ debug_log(f"檢測到 {active_websockets} 個活躍的WebSocket連接")
+ return True
- timeout = aiohttp.ClientTimeout(total=2)
- async with aiohttp.ClientSession(timeout=timeout) as session:
- async with session.get(
- f"{self.get_server_url()}/api/active-tabs"
- ) as response:
- if response.status == 200:
- data = await response.json()
- tab_count = data.get("count", 0)
- debug_log(f"API 檢測到 {tab_count} 個活躍標籤頁")
- return bool(tab_count > 0)
- debug_log(f"檢查活躍標籤頁失敗,狀態碼:{response.status}")
- return False
-
- except TimeoutError:
- debug_log("檢查活躍標籤頁超時")
+ debug_log("沒有檢測到任何活躍的WebSocket連接")
return False
+
except Exception as e:
- debug_log(f"檢查活躍標籤頁時發生錯誤:{e}")
+ debug_log(f"檢查活躍連接時發生錯誤:{e}")
return False
def get_server_url(self) -> str:
@@ -1092,7 +1036,7 @@ async def launch_web_feedback_ui(
"""
manager = get_web_ui_manager()
- # 創建或更新當前活躍會話
+ # 創建新會話(每次AI調用都應該創建新會話)
manager.create_session(project_directory, summary)
session = manager.get_current_session()
@@ -1119,10 +1063,9 @@ async def launch_web_feedback_ui(
debug_log(f"[DEBUG] 服務器地址: {feedback_url}")
- # 如果檢測到活躍標籤頁但沒有開啟新視窗,立即發送會話更新通知
+ # 如果檢測到活躍標籤頁,消息已在 smart_open_browser 中發送,無需額外處理
if has_active_tabs:
- await manager._send_immediate_session_update()
- debug_log("已向活躍標籤頁發送會話更新通知")
+ debug_log("檢測到活躍標籤頁,會話更新通知已發送")
try:
# 等待用戶回饋,傳遞 timeout 參數
diff --git a/src/mcp_feedback_enhanced/web/models/feedback_session.py b/src/mcp_feedback_enhanced/web/models/feedback_session.py
index 7aa915b..5f2d69f 100644
--- a/src/mcp_feedback_enhanced/web/models/feedback_session.py
+++ b/src/mcp_feedback_enhanced/web/models/feedback_session.py
@@ -29,15 +29,13 @@ from ...utils.resource_manager import get_resource_manager, register_process
class SessionStatus(Enum):
- """會話狀態枚舉"""
+ """會話狀態枚舉 - 單向流轉設計"""
WAITING = "waiting" # 等待中
- ACTIVE = "active" # 活躍中
FEEDBACK_SUBMITTED = "feedback_submitted" # 已提交反饋
COMPLETED = "completed" # 已完成
- TIMEOUT = "timeout" # 超時
- ERROR = "error" # 錯誤
- EXPIRED = "expired" # 已過期
+ ERROR = "error" # 錯誤(終態)
+ EXPIRED = "expired" # 已過期(終態)
class CleanupReason(Enum):
@@ -133,6 +131,7 @@ class WebFeedbackSession:
self.feedback_completed = threading.Event()
self.process: subprocess.Popen | None = None
self.command_logs: list[str] = []
+ self.user_messages: list[dict] = [] # 用戶消息記錄
self._cleanup_done = False # 防止重複清理
# 新增:會話狀態管理
@@ -174,21 +173,79 @@ class WebFeedbackSession:
f"會話 {self.session_id} 初始化完成,自動清理延遲: {auto_cleanup_delay}秒,最大空閒: {max_idle_time}秒"
)
- def update_status(self, status: SessionStatus, message: str | None = None):
- """更新會話狀態"""
- self.status = status
+ def next_step(self, message: str | None = None) -> bool:
+ """進入下一個狀態 - 單向流轉,不可倒退"""
+ old_status = self.status
+
+ # 定義狀態流轉路徑
+ next_status_map = {
+ SessionStatus.WAITING: SessionStatus.FEEDBACK_SUBMITTED,
+ SessionStatus.FEEDBACK_SUBMITTED: SessionStatus.COMPLETED,
+ SessionStatus.COMPLETED: None, # 終態
+ SessionStatus.ERROR: None, # 終態
+ SessionStatus.EXPIRED: None # 終態
+ }
+
+ next_status = next_status_map.get(self.status)
+
+ if next_status is None:
+ debug_log(f"⚠️ 會話 {self.session_id} 已處於終態 {self.status.value},無法進入下一步")
+ return False
+
+ # 執行狀態轉換
+ self.status = next_status
if message:
self.status_message = message
- # 統一使用 time.time()
+ else:
+ # 默認消息
+ default_messages = {
+ SessionStatus.FEEDBACK_SUBMITTED: "用戶已提交反饋",
+ SessionStatus.COMPLETED: "會話已完成"
+ }
+ self.status_message = default_messages.get(next_status, "狀態已更新")
+
self.last_activity = time.time()
- # 如果會話變為活躍狀態,重置清理定時器
- if status in [SessionStatus.ACTIVE, SessionStatus.FEEDBACK_SUBMITTED]:
+ # 如果會話變為已提交狀態,重置清理定時器
+ if next_status == SessionStatus.FEEDBACK_SUBMITTED:
self._schedule_auto_cleanup()
debug_log(
- f"會話 {self.session_id} 狀態更新: {status.value} - {self.status_message}"
+ f"✅ 會話 {self.session_id} 狀態流轉: {old_status.value} → {next_status.value} - {self.status_message}"
)
+ return True
+
+ def set_error(self, message: str = "會話發生錯誤") -> bool:
+ """設置錯誤狀態(特殊方法,可從任何狀態進入)"""
+ old_status = self.status
+ self.status = SessionStatus.ERROR
+ self.status_message = message
+ self.last_activity = time.time()
+
+ debug_log(
+ f"❌ 會話 {self.session_id} 設置為錯誤狀態: {old_status.value} → {self.status.value} - {message}"
+ )
+ return True
+
+ def set_expired(self, message: str = "會話已過期") -> bool:
+ """設置過期狀態(特殊方法,可從任何狀態進入)"""
+ old_status = self.status
+ self.status = SessionStatus.EXPIRED
+ self.status_message = message
+ self.last_activity = time.time()
+
+ debug_log(
+ f"⏰ 會話 {self.session_id} 設置為過期狀態: {old_status.value} → {self.status.value} - {message}"
+ )
+ return True
+
+ def can_proceed(self) -> bool:
+ """檢查是否可以進入下一步"""
+ return self.status in [SessionStatus.WAITING, SessionStatus.FEEDBACK_SUBMITTED]
+
+ def is_terminal(self) -> bool:
+ """檢查是否處於終態"""
+ return self.status in [SessionStatus.COMPLETED, SessionStatus.ERROR, SessionStatus.EXPIRED]
def get_status_info(self) -> dict[str, Any]:
"""獲取會話狀態信息"""
@@ -404,10 +461,8 @@ class WebFeedbackSession:
self.settings = settings or {}
self.images = self._process_images(images)
- # 更新狀態為已提交反饋
- self.update_status(
- SessionStatus.FEEDBACK_SUBMITTED, "已送出反饋,等待下次 MCP 調用"
- )
+ # 進入下一步:等待中 → 已提交反饋
+ self.next_step("已送出反饋,等待下次 MCP 調用")
self.feedback_completed.set()
@@ -443,6 +498,22 @@ class WebFeedbackSession:
# 重構:不再自動關閉 WebSocket,保持連接以支援頁面持久性
+ def add_user_message(self, message_data: dict[str, Any]) -> None:
+ """添加用戶消息記錄"""
+ import time
+
+ # 創建用戶消息記錄
+ user_message = {
+ "timestamp": int(time.time() * 1000), # 毫秒時間戳
+ "content": message_data.get("content", ""),
+ "images": message_data.get("images", []),
+ "submission_method": message_data.get("submission_method", "manual"),
+ "type": "feedback"
+ }
+
+ self.user_messages.append(user_message)
+ debug_log(f"會話 {self.session_id} 添加用戶消息,總數: {len(self.user_messages)}")
+
def _process_images(self, images: list[dict]) -> list[dict]:
"""
處理圖片數據,轉換為統一格式
diff --git a/src/mcp_feedback_enhanced/web/routes/main_routes.py b/src/mcp_feedback_enhanced/web/routes/main_routes.py
index 9e0d989..5ae35f4 100644
--- a/src/mcp_feedback_enhanced/web/routes/main_routes.py
+++ b/src/mcp_feedback_enhanced/web/routes/main_routes.py
@@ -157,6 +157,68 @@ def setup_routes(manager: "WebUIManager"):
}
)
+ @manager.app.get("/api/all-sessions")
+ async def get_all_sessions():
+ """獲取所有會話的實時狀態"""
+ try:
+ sessions_data = []
+
+ # 獲取所有會話的實時狀態
+ for session_id, session in manager.sessions.items():
+ session_info = {
+ "session_id": session.session_id,
+ "project_directory": session.project_directory,
+ "summary": session.summary,
+ "status": session.status.value,
+ "status_message": session.status_message,
+ "created_at": int(session.created_at * 1000), # 轉換為毫秒
+ "last_activity": int(session.last_activity * 1000),
+ "feedback_completed": session.feedback_completed.is_set(),
+ "has_websocket": session.websocket is not None,
+ "is_current": session == manager.current_session,
+ "user_messages": session.user_messages # 包含用戶消息記錄
+ }
+ sessions_data.append(session_info)
+
+ # 按創建時間排序(最新的在前)
+ sessions_data.sort(key=lambda x: x["created_at"], reverse=True)
+
+ debug_log(f"返回 {len(sessions_data)} 個會話的實時狀態")
+ return JSONResponse(content={"sessions": sessions_data})
+
+ except Exception as e:
+ debug_log(f"獲取所有會話狀態失敗: {e}")
+ return JSONResponse(
+ status_code=500,
+ content={"error": f"獲取會話狀態失敗: {e!s}"}
+ )
+
+ @manager.app.post("/api/add-user-message")
+ async def add_user_message(request: Request):
+ """添加用戶消息到當前會話"""
+ try:
+ data = await request.json()
+ current_session = manager.get_current_session()
+
+ if not current_session:
+ return JSONResponse(
+ status_code=404,
+ content={"error": "沒有活躍會話"}
+ )
+
+ # 添加用戶消息到會話
+ current_session.add_user_message(data)
+
+ debug_log(f"用戶消息已添加到會話 {current_session.session_id}")
+ return JSONResponse(content={"status": "success", "message": "用戶消息已記錄"})
+
+ except Exception as e:
+ debug_log(f"添加用戶消息失敗: {e}")
+ return JSONResponse(
+ status_code=500,
+ content={"error": f"添加用戶消息失敗: {e!s}"}
+ )
+
@manager.app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket 端點 - 重構後移除 session_id 依賴"""
@@ -187,6 +249,7 @@ def setup_routes(manager: "WebUIManager"):
await websocket.send_json(
{
"type": "session_updated",
+ "action": "new_session_created",
"message": "新會話已創建,正在更新頁面內容",
"session_info": {
"project_directory": session.project_directory,
diff --git a/src/mcp_feedback_enhanced/web/static/css/session-management.css b/src/mcp_feedback_enhanced/web/static/css/session-management.css
index 86ee2e9..85e6d7d 100644
--- a/src/mcp_feedback_enhanced/web/static/css/session-management.css
+++ b/src/mcp_feedback_enhanced/web/static/css/session-management.css
@@ -753,7 +753,7 @@
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- max-width: 500px;
+ max-width: 750px;
width: 90%;
max-height: 80vh;
overflow: hidden;
@@ -854,11 +854,149 @@
.detail-value.summary {
background: var(--bg-secondary);
- padding: 8px 12px;
+ padding: 0;
border-radius: 6px;
border-left: 3px solid var(--accent-color);
line-height: 1.4;
margin-top: 4px;
+ position: relative;
+}
+
+.summary-actions {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ z-index: 10;
+}
+
+.btn-copy-summary {
+ background: var(--accent-color);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: all var(--transition-fast) ease;
+ opacity: 0.8;
+}
+
+.btn-copy-summary:hover {
+ opacity: 1;
+ background: #1976d2;
+ transform: scale(1.05);
+}
+
+.summary-content {
+ padding: 8px 12px;
+ padding-right: 50px; /* 为复制按钮留出空间 */
+}
+
+/* 为会话详情弹窗中的摘要内容应用与反馈页面相同的 Markdown 样式 */
+.detail-value.summary .summary-content h1,
+.detail-value.summary .summary-content h2,
+.detail-value.summary .summary-content h3,
+.detail-value.summary .summary-content h4,
+.detail-value.summary .summary-content h5,
+.detail-value.summary .summary-content h6 {
+ color: var(--text-primary);
+ margin: 16px 0 8px 0;
+ font-weight: 600;
+}
+
+.detail-value.summary .summary-content h1 { font-size: 20px; }
+.detail-value.summary .summary-content h2 { font-size: 18px; }
+.detail-value.summary .summary-content h3 { font-size: 16px; }
+.detail-value.summary .summary-content h4 { font-size: 14px; }
+.detail-value.summary .summary-content h5 { font-size: 13px; }
+.detail-value.summary .summary-content h6 { font-size: 12px; }
+
+.detail-value.summary .summary-content p {
+ margin: 8px 0;
+ line-height: 1.6;
+}
+
+.detail-value.summary .summary-content strong {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.detail-value.summary .summary-content em {
+ font-style: italic;
+ color: var(--text-primary);
+}
+
+.detail-value.summary .summary-content code {
+ background: var(--bg-tertiary);
+ padding: 2px 4px;
+ border-radius: 3px;
+ font-family: 'Consolas', 'Monaco', monospace;
+ font-size: 11px;
+ color: var(--accent-color);
+}
+
+.detail-value.summary .summary-content pre {
+ background: var(--bg-tertiary);
+ padding: 12px;
+ border-radius: 6px;
+ overflow-x: auto;
+ margin: 12px 0;
+ border-left: 3px solid var(--accent-color);
+}
+
+.detail-value.summary .summary-content pre code {
+ background: none;
+ padding: 0;
+ color: var(--text-primary);
+}
+
+.detail-value.summary .summary-content ul,
+.detail-value.summary .summary-content ol {
+ margin: 8px 0;
+ padding-left: 20px;
+}
+
+.detail-value.summary .summary-content li {
+ margin: 4px 0;
+ line-height: 1.5;
+}
+
+.detail-value.summary .summary-content blockquote {
+ border-left: 4px solid var(--accent-color);
+ margin: 12px 0;
+ padding: 8px 16px;
+ background: rgba(0, 122, 204, 0.05);
+ font-style: italic;
+}
+
+/* 复制提示样式 */
+.copy-toast {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 12px 20px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+ color: white;
+ z-index: 10000;
+ opacity: 0;
+ transform: translateX(100px);
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+}
+
+.copy-toast.show {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.copy-toast-success {
+ background: var(--success-color);
+}
+
+.copy-toast-error {
+ background: var(--error-color);
}
.modal-footer {
@@ -924,6 +1062,36 @@
background: var(--bg-secondary);
}
+.message-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.message-header .btn-copy-message {
+ background: var(--accent-color);
+ color: white;
+ border: none;
+ border-radius: 3px;
+ padding: 2px 6px;
+ font-size: 10px;
+ cursor: pointer;
+ transition: all var(--transition-fast) ease;
+ opacity: 0.7;
+ margin-left: 8px;
+}
+
+.message-header .btn-copy-message:hover {
+ opacity: 1;
+ background: #1976d2;
+ transform: scale(1.05);
+}
+
+.user-message-item:hover .btn-copy-message {
+ opacity: 0.9;
+}
+
.message-header {
display: flex;
justify-content: space-between;
diff --git a/src/mcp_feedback_enhanced/web/static/css/styles.css b/src/mcp_feedback_enhanced/web/static/css/styles.css
index afd2f5e..64d1c45 100644
--- a/src/mcp_feedback_enhanced/web/static/css/styles.css
+++ b/src/mcp_feedback_enhanced/web/static/css/styles.css
@@ -369,7 +369,6 @@ body {
/* 容器樣式 */
.container {
- max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;
diff --git a/src/mcp_feedback_enhanced/web/static/favicon.ico b/src/mcp_feedback_enhanced/web/static/favicon.ico
index 9a0fc00..ddee163 100644
Binary files a/src/mcp_feedback_enhanced/web/static/favicon.ico and b/src/mcp_feedback_enhanced/web/static/favicon.ico differ
diff --git a/src/mcp_feedback_enhanced/web/static/icon-192.png b/src/mcp_feedback_enhanced/web/static/icon-192.png
new file mode 100644
index 0000000..e458dba
Binary files /dev/null and b/src/mcp_feedback_enhanced/web/static/icon-192.png differ
diff --git a/src/mcp_feedback_enhanced/web/static/icon.svg b/src/mcp_feedback_enhanced/web/static/icon.svg
new file mode 100644
index 0000000..102ba2d
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/static/icon.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js
index 76aa23b..982c5af 100644
--- a/src/mcp_feedback_enhanced/web/static/js/app.js
+++ b/src/mcp_feedback_enhanced/web/static/js/app.js
@@ -647,9 +647,25 @@
const submittedMessage = window.i18nManager ? window.i18nManager.t('feedback.submittedWaiting') : '已送出反饋,等待下次 MCP 調用...';
this.updateSummaryStatus(submittedMessage);
+ // 刷新會話列表以顯示最新狀態
+ this.refreshSessionList();
+
console.log('反饋已提交,頁面保持開啟狀態');
};
+ /**
+ * 刷新會話列表以顯示最新狀態
+ */
+ FeedbackApp.prototype.refreshSessionList = function() {
+ // 如果有會話管理器,觸發數據刷新
+ if (this.sessionManager && this.sessionManager.dataManager) {
+ console.log('🔄 刷新會話列表以顯示最新狀態');
+ this.sessionManager.dataManager.loadFromServer();
+ } else {
+ console.log('⚠️ 會話管理器未初始化,跳過會話列表刷新');
+ }
+ };
+
/**
* 處理桌面關閉請求
*/
@@ -682,7 +698,46 @@
* 處理會話更新(原始版本,供防抖使用)
*/
FeedbackApp.prototype._originalHandleSessionUpdated = function(data) {
- console.log('🔄 處理會話更新:', data.session_info);
+ console.log('🔄 處理會話更新:', data);
+ console.log('🔍 檢查 action 字段:', data.action);
+ console.log('🔍 檢查 type 字段:', data.type);
+
+ // 檢查是否是新會話創建的通知
+ if (data.action === 'new_session_created' || data.type === 'new_session_created') {
+ console.log('🆕 檢測到新會話創建,完全刷新頁面以確保狀態同步');
+
+ // 播放音效通知
+ if (this.audioManager) {
+ this.audioManager.playNotification();
+ }
+
+ // 顯示新會話通知
+ window.MCPFeedback.Utils.showMessage(
+ data.message || '新的 MCP 會話已創建,正在打開新窗口...',
+ window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS
+ );
+
+ // 使用 window.open 打開新窗口並關閉當前窗口
+ setTimeout(function() {
+ console.log('🔄 使用 window.open 打開新窗口');
+
+ // 打開新窗口
+ const newWindow = window.open(window.location.href, '_blank');
+
+ if (newWindow) {
+ console.log('✅ 新窗口打開成功,關閉當前窗口');
+ // 短暫延遲後關閉當前窗口
+ setTimeout(function() {
+ window.close();
+ }, 500);
+ } else {
+ console.warn('❌ window.open 被阻止,回退到頁面刷新');
+ window.location.reload();
+ }
+ }, 1500);
+
+ return; // 提前返回,不執行後續的局部更新邏輯
+ }
// 播放音效通知
if (this.audioManager) {
@@ -758,8 +813,16 @@
}
}
- // 重置回饋狀態為等待新回饋
- this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, newSessionId);
+ // 檢查當前狀態,只有在非已提交狀態時才重置
+ const currentState = this.uiManager.getFeedbackState();
+ if (currentState !== window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED) {
+ this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, newSessionId);
+ console.log('🔄 會話更新:重置回饋狀態為等待新回饋');
+ } else {
+ console.log('🔒 會話更新:保護已提交狀態,不重置');
+ // 更新會話ID但保持已提交狀態
+ this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED, newSessionId);
+ }
// 檢查並啟動自動提交(如果條件滿足)
const self = this;
@@ -799,7 +862,16 @@
* 處理狀態更新(原始版本,供防抖使用)
*/
FeedbackApp.prototype._originalHandleStatusUpdate = function(statusInfo) {
- console.log('處理狀態更新:', statusInfo);
+ console.log('📊 處理狀態更新:', statusInfo);
+
+ const sessionId = statusInfo.session_id;
+ console.log('🔍 狀態更新詳情:', {
+ currentSessionId: this.currentSessionId,
+ newSessionId: sessionId,
+ status: statusInfo.status,
+ message: statusInfo.message,
+ isNewSession: sessionId !== this.currentSessionId
+ });
// 更新 SessionManager 的狀態資訊
if (this.sessionManager && this.sessionManager.updateStatusInfo) {
@@ -812,36 +884,39 @@
document.title = 'MCP Feedback - ' + projectName;
}
- // 提取會話 ID
- const sessionId = statusInfo.session_id || this.currentSessionId;
+ // 使用之前已聲明的 sessionId
- // 根據狀態更新 UI
+ // 前端只管理會話ID,所有狀態都從服務器獲取
+ console.log('📊 收到服務器狀態更新:', statusInfo.status, '會話ID:', sessionId);
+
+ // 更新當前會話ID
+ if (sessionId) {
+ this.currentSessionId = sessionId;
+ console.log('🔄 更新當前會話ID:', sessionId.substring(0, 8) + '...');
+ }
+
+ // 刷新會話列表以顯示最新狀態
+ this.refreshSessionList();
+
+ // 根據服務器狀態更新消息顯示(不修改前端狀態)
switch (statusInfo.status) {
case 'feedback_submitted':
- this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED, sessionId);
const submittedMessage = window.i18nManager ? window.i18nManager.t('feedback.submittedWaiting') : '已送出反饋,等待下次 MCP 調用...';
this.updateSummaryStatus(submittedMessage);
break;
-
- case 'active':
case 'waiting':
- // 檢查是否是新會話
- if (sessionId && sessionId !== this.currentSessionId) {
- this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionId);
- } else if (this.uiManager.getFeedbackState() !== window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED) {
- this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionId);
- }
+ const waitingMessage = window.i18nManager ? window.i18nManager.t('feedback.waitingForUser') : '等待用戶回饋...';
+ this.updateSummaryStatus(waitingMessage);
- if (statusInfo.status === 'waiting') {
- const waitingMessage = window.i18nManager ? window.i18nManager.t('feedback.waitingForUser') : '等待用戶回饋...';
- this.updateSummaryStatus(waitingMessage);
-
- // 檢查並啟動自動提交(如果條件滿足)
- const self = this;
- setTimeout(function() {
- self.checkAndStartAutoSubmit();
- }, 100); // 短暫延遲確保狀態更新完成
- }
+ // 檢查並啟動自動提交(如果條件滿足)
+ const self = this;
+ setTimeout(function() {
+ self.checkAndStartAutoSubmit();
+ }, 100);
+ break;
+ case 'completed':
+ const completedMessage = window.i18nManager ? window.i18nManager.t('feedback.completed') : '會話已完成';
+ this.updateSummaryStatus(completedMessage);
break;
}
};
@@ -884,10 +959,15 @@
* 檢查是否可以提交回饋
*/
FeedbackApp.prototype.canSubmitFeedback = function() {
- return this.webSocketManager &&
- this.webSocketManager.isReady() &&
- this.uiManager &&
- this.uiManager.getFeedbackState() === window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING;
+ // 簡化檢查:只檢查WebSocket連接,狀態由服務器端驗證
+ const wsReady = this.webSocketManager && this.webSocketManager.isReady();
+
+ console.log('🔍 提交檢查:', {
+ wsReady: wsReady,
+ sessionId: this.currentSessionId
+ });
+
+ return wsReady;
};
/**
@@ -1164,14 +1244,9 @@
FeedbackApp.prototype.handleSessionUpdate = function(sessionData) {
console.log('🔄 處理自動檢測到的會話更新:', sessionData);
- // 更新當前會話 ID
+ // 只更新當前會話 ID,不管理狀態
this.currentSessionId = sessionData.session_id;
- // 重置回饋狀態
- if (this.uiManager) {
- this.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionData.session_id);
- }
-
// 局部更新頁面內容
this.refreshPageContent();
};
@@ -1194,9 +1269,17 @@
.then(function(sessionData) {
console.log('📥 獲取到最新會話資料:', sessionData);
- // 重置回饋狀態
+ // 檢查並保護已提交狀態
if (sessionData.session_id && self.uiManager) {
- self.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionData.session_id);
+ const currentState = self.uiManager.getFeedbackState();
+ if (currentState !== window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED) {
+ self.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_WAITING, sessionData.session_id);
+ console.log('🔄 局部更新:重置回饋狀態為等待中');
+ } else {
+ console.log('🔒 局部更新:保護已提交狀態,不重置');
+ // 只更新會話ID,保持已提交狀態
+ self.uiManager.setFeedbackState(window.MCPFeedback.Utils.CONSTANTS.FEEDBACK_SUBMITTED, sessionData.session_id);
+ }
}
// 更新 AI 摘要內容
diff --git a/src/mcp_feedback_enhanced/web/static/js/i18n.js b/src/mcp_feedback_enhanced/web/static/js/i18n.js
index f257259..e2fbebb 100644
--- a/src/mcp_feedback_enhanced/web/static/js/i18n.js
+++ b/src/mcp_feedback_enhanced/web/static/js/i18n.js
@@ -8,7 +8,7 @@
class I18nManager {
constructor() {
- this.currentLanguage = 'zh-TW';
+ this.currentLanguage = 'zh-CN';
this.translations = {};
this.loadingPromise = null;
}
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js
index 1bca3f1..a40ad4a 100644
--- a/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/session-manager.js
@@ -258,6 +258,30 @@
self.showSessionDetails();
});
}
+
+ // 复制当前会话内容按钮
+ const copySessionButton = DOMUtils ?
+ DOMUtils.safeQuerySelector('#copyCurrentSessionContent') :
+ document.querySelector('#copyCurrentSessionContent');
+ if (copySessionButton) {
+ copySessionButton.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.copyCurrentSessionContent();
+ });
+ }
+
+ // 复制当前用户内容按钮
+ const copyUserButton = DOMUtils ?
+ DOMUtils.safeQuerySelector('#copyCurrentUserContent') :
+ document.querySelector('#copyCurrentUserContent');
+ if (copyUserButton) {
+ copyUserButton.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.copyCurrentUserContent();
+ });
+ }
};
/**
@@ -648,6 +672,242 @@
}
};
+ /**
+ * 复制当前会话内容
+ */
+ SessionManager.prototype.copyCurrentSessionContent = function() {
+ console.log('📋 复制当前会话内容...');
+
+ try {
+ const currentSession = this.dataManager.getCurrentSession();
+ if (!currentSession) {
+ this.showMessage('没有当前会话数据', 'error');
+ return;
+ }
+
+ const content = this.formatCurrentSessionContent(currentSession);
+ this.copyToClipboard(content, '当前会话内容已复制到剪贴板');
+ } catch (error) {
+ console.error('复制当前会话内容失败:', error);
+ this.showMessage('复制失败,请重试', 'error');
+ }
+ };
+
+ /**
+ * 复制当前用户发送的内容
+ */
+ SessionManager.prototype.copyCurrentUserContent = function() {
+ console.log('📝 复制当前用户发送的内容...');
+ console.log('📝 this.dataManager 存在吗?', !!this.dataManager);
+
+ try {
+ if (!this.dataManager) {
+ console.log('📝 dataManager 不存在,尝试其他方式获取数据');
+ this.showMessage('数据管理器未初始化', 'error');
+ return;
+ }
+
+ const currentSession = this.dataManager.getCurrentSession();
+ console.log('📝 当前会话数据:', currentSession);
+
+ if (!currentSession) {
+ console.log('📝 没有当前会话数据');
+ this.showMessage('当前会话没有数据', 'warning');
+ return;
+ }
+
+ console.log('📝 用户消息数组:', currentSession.user_messages);
+ console.log('📝 用户消息数组长度:', currentSession.user_messages ? currentSession.user_messages.length : 'undefined');
+
+ if (!currentSession.user_messages || currentSession.user_messages.length === 0) {
+ console.log('📝 没有用户消息记录');
+ this.showMessage('当前会话没有用户消息记录', 'warning');
+ return;
+ }
+
+ // 在这里也添加调试信息
+ console.log('📝 准备格式化用户消息,数量:', currentSession.user_messages.length);
+ console.log('📝 第一条消息内容:', currentSession.user_messages[0]);
+
+ const content = this.formatCurrentUserContent(currentSession.user_messages);
+ console.log('📝 格式化后的内容长度:', content.length);
+ console.log('📝 格式化后的内容预览:', content.substring(0, 200));
+
+ this.copyToClipboard(content, '当前用户内容已复制到剪贴板');
+ } catch (error) {
+ console.error('📝 复制当前用户内容失败:', error);
+ console.error('📝 错误堆栈:', error.stack);
+ this.showMessage('复制失败,请重试', 'error');
+ }
+ };
+
+ /**
+ * 格式化当前会话内容
+ */
+ SessionManager.prototype.formatCurrentSessionContent = function(sessionData) {
+ const lines = [];
+ lines.push('# MCP Feedback Enhanced - 当前会话内容');
+ lines.push('');
+ lines.push(`**会话ID**: ${sessionData.session_id || 'N/A'}`);
+ lines.push(`**项目目录**: ${sessionData.project_directory || 'N/A'}`);
+ lines.push(`**摘要**: ${sessionData.summary || 'N/A'}`);
+ lines.push(`**状态**: ${sessionData.status || 'N/A'}`);
+ lines.push(`**创建时间**: ${sessionData.created_at || 'N/A'}`);
+ lines.push(`**更新时间**: ${sessionData.updated_at || 'N/A'}`);
+ lines.push('');
+
+ if (sessionData.user_messages && sessionData.user_messages.length > 0) {
+ lines.push('## 用户消息');
+ sessionData.user_messages.forEach((msg, index) => {
+ lines.push(`### 消息 ${index + 1}`);
+ lines.push(msg);
+ lines.push('');
+ });
+ }
+
+ if (sessionData.ai_responses && sessionData.ai_responses.length > 0) {
+ lines.push('## AI 响应');
+ sessionData.ai_responses.forEach((response, index) => {
+ lines.push(`### 响应 ${index + 1}`);
+ lines.push(response);
+ lines.push('');
+ });
+ }
+
+ return lines.join('\n');
+ };
+
+ /**
+ * 格式化当前用户内容
+ */
+ SessionManager.prototype.formatCurrentUserContent = function(userMessages) {
+ const lines = [];
+ lines.push('# MCP Feedback Enhanced - 用户发送内容');
+ lines.push('');
+
+ userMessages.forEach((msg, index) => {
+ lines.push(`## 消息 ${index + 1}`);
+
+ // 调试:输出完整的消息对象
+ console.log(`📝 消息 ${index + 1} 完整对象:`, msg);
+ console.log(`📝 消息 ${index + 1} 所有属性:`, Object.keys(msg));
+
+ // 添加时间戳信息 - 简化版本,直接使用当前时间
+ let timeStr = '未知时间';
+
+ // 检查是否有时间戳字段
+ if (msg.timestamp) {
+ // 如果时间戳看起来不正常(太小),直接使用当前时间
+ if (msg.timestamp < 1000000000) { // 小于2001年的时间戳,可能是相对时间
+ timeStr = new Date().toLocaleString('zh-CN');
+ console.log('📝 时间戳异常,使用当前时间:', msg.timestamp);
+ } else {
+ // 正常处理时间戳
+ let timestamp = msg.timestamp;
+ if (timestamp > 1e12) {
+ // 毫秒时间戳
+ timeStr = new Date(timestamp).toLocaleString('zh-CN');
+ } else {
+ // 秒时间戳
+ timeStr = new Date(timestamp * 1000).toLocaleString('zh-CN');
+ }
+ }
+ } else {
+ // 没有时间戳,使用当前时间
+ timeStr = new Date().toLocaleString('zh-CN');
+ console.log('📝 没有时间戳字段,使用当前时间');
+ }
+
+ lines.push(`**时间**: ${timeStr}`);
+
+ // 添加提交方式
+ if (msg.submission_method) {
+ const methodText = msg.submission_method === 'auto' ? '自动提交' : '手动提交';
+ lines.push(`**提交方式**: ${methodText}`);
+ }
+
+ // 处理消息内容
+ if (msg.content !== undefined) {
+ // 完整记录模式 - 显示实际内容
+ lines.push(`**内容**: ${msg.content}`);
+
+ // 如果有图片,显示图片数量
+ if (msg.images && msg.images.length > 0) {
+ lines.push(`**图片数量**: ${msg.images.length}`);
+ }
+ } else if (msg.content_length !== undefined) {
+ // 基本统计模式 - 显示统计信息
+ lines.push(`**内容长度**: ${msg.content_length} 字符`);
+ lines.push(`**图片数量**: ${msg.image_count || 0}`);
+ lines.push(`**有内容**: ${msg.has_content ? '是' : '否'}`);
+ } else if (msg.privacy_note) {
+ // 隐私保护模式
+ lines.push(`**内容**: [内容记录已停用 - 隐私设置]`);
+ } else {
+ // 兜底情况 - 尝试显示对象的JSON格式
+ lines.push(`**原始数据**: ${JSON.stringify(msg, null, 2)}`);
+ }
+
+ lines.push('');
+ });
+
+ return lines.join('\n');
+ };
+
+ /**
+ * 复制到剪贴板
+ */
+ SessionManager.prototype.copyToClipboard = function(text, successMessage) {
+ if (navigator.clipboard && window.isSecureContext) {
+ navigator.clipboard.writeText(text).then(() => {
+ this.showMessage(successMessage, 'success');
+ }).catch(err => {
+ console.error('复制到剪贴板失败:', err);
+ this.fallbackCopyToClipboard(text, successMessage);
+ });
+ } else {
+ this.fallbackCopyToClipboard(text, successMessage);
+ }
+ };
+
+ /**
+ * 降级复制方法
+ */
+ SessionManager.prototype.fallbackCopyToClipboard = function(text, successMessage) {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-999999px';
+ textArea.style.top = '-999999px';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try {
+ document.execCommand('copy');
+ this.showMessage(successMessage, 'success');
+ } catch (err) {
+ console.error('降级复制失败:', err);
+ this.showMessage('复制失败,请手动复制', 'error');
+ } finally {
+ document.body.removeChild(textArea);
+ }
+ };
+
+ /**
+ * 显示消息
+ */
+ SessionManager.prototype.showMessage = function(message, type) {
+ if (window.MCPFeedback && window.MCPFeedback.Utils && window.MCPFeedback.Utils.showMessage) {
+ const messageType = type === 'success' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_SUCCESS :
+ type === 'warning' ? window.MCPFeedback.Utils.CONSTANTS.MESSAGE_WARNING :
+ window.MCPFeedback.Utils.CONSTANTS.MESSAGE_ERROR;
+ window.MCPFeedback.Utils.showMessage(message, messageType);
+ } else {
+ console.log(`[${type.toUpperCase()}] ${message}`);
+ }
+ };
+
// 全域匯出會話歷史方法
window.MCPFeedback.SessionManager.exportSessionHistory = function() {
if (window.MCPFeedback && window.MCPFeedback.app && window.MCPFeedback.app.sessionManager) {
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-data-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-data-manager.js
index 550b076..61b1906 100644
--- a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-data-manager.js
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-data-manager.js
@@ -62,9 +62,13 @@
if (this.currentSession && this.currentSession.session_id) {
console.log('📊 檢測到會話 ID 變更,處理舊會話:', this.currentSession.session_id, '->', sessionData.session_id);
- // 將舊會話標記為完成並加入歷史記錄
+ // 將舊會話加入歷史記錄,保持其原有狀態
const oldSession = Object.assign({}, this.currentSession);
- oldSession.status = 'completed';
+
+ // 完全保持舊會話的原有狀態,不做任何修改
+ // 讓服務器端負責狀態轉換,前端只負責顯示
+ console.log('📊 保持舊會話的原有狀態:', oldSession.status);
+
oldSession.completed_at = TimeUtils.getCurrentTimestamp();
// 計算持續時間
@@ -309,6 +313,9 @@
// 記錄用戶最後互動時間
this.currentSession.last_user_interaction = TimeUtils.getCurrentTimestamp();
+ // 發送用戶消息到服務器端
+ this.sendUserMessageToServer(userMessage);
+
// 立即保存當前會話到伺服器
this.saveCurrentSessionToServer();
@@ -316,6 +323,29 @@
return true;
};
+ /**
+ * 發送用戶消息到服務器端
+ */
+ SessionDataManager.prototype.sendUserMessageToServer = function(userMessage) {
+ fetch('/api/add-user-message', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(userMessage)
+ })
+ .then(function(response) {
+ if (response.ok) {
+ console.log('📊 用戶消息已發送到服務器端');
+ } else {
+ console.warn('📊 發送用戶消息到服務器端失敗:', response.status);
+ }
+ })
+ .catch(function(error) {
+ console.warn('📊 發送用戶消息到服務器端出錯:', error);
+ });
+ };
+
/**
* 建立用戶訊息記錄
*/
@@ -453,16 +483,24 @@
};
/**
- * 根據 ID 查找會話
+ * 根據 ID 查找會話(包含完整的用戶消息數據)
*/
SessionDataManager.prototype.findSessionById = function(sessionId) {
// 先檢查當前會話
if (this.currentSession && this.currentSession.session_id === sessionId) {
+ console.log('📊 從當前會話獲取數據:', sessionId, '用戶消息數量:', this.currentSession.user_messages ? this.currentSession.user_messages.length : 0);
return this.currentSession;
}
// 再檢查歷史記錄
- return this.sessionHistory.find(s => s.session_id === sessionId) || null;
+ const historySession = this.sessionHistory.find(s => s.session_id === sessionId);
+ if (historySession) {
+ console.log('📊 從歷史記錄獲取數據:', sessionId, '用戶消息數量:', historySession.user_messages ? historySession.user_messages.length : 0);
+ return historySession;
+ }
+
+ console.warn('📊 找不到會話:', sessionId);
+ return null;
};
/**
@@ -589,23 +627,25 @@
};
/**
- * 從伺服器載入會話歷史
+ * 從伺服器載入會話歷史(包含實時狀態)
*/
SessionDataManager.prototype.loadFromServer = function() {
const self = this;
- fetch('/api/load-session-history')
+ // 首先嘗試獲取實時會話狀態
+ fetch('/api/all-sessions')
.then(function(response) {
if (response.ok) {
return response.json();
} else {
- throw new Error('伺服器回應錯誤: ' + response.status);
+ throw new Error('獲取實時會話狀態失敗: ' + response.status);
}
})
.then(function(data) {
if (data && Array.isArray(data.sessions)) {
+ // 使用實時會話狀態
self.sessionHistory = data.sessions;
- console.log('📊 從伺服器載入', self.sessionHistory.length, '個會話');
+ console.log('📊 從伺服器載入', self.sessionHistory.length, '個實時會話狀態');
// 載入完成後進行清理和統計更新
self.cleanupExpiredSessions();
@@ -621,13 +661,53 @@
self.onDataChanged();
}
} else {
- console.warn('📊 伺服器回應格式錯誤:', data);
- self.sessionHistory = [];
+ console.warn('📊 實時會話狀態回應格式錯誤,回退到歷史文件');
+ self.loadFromHistoryFile();
+ }
+ })
+ .catch(function(error) {
+ console.warn('📊 獲取實時會話狀態失敗,回退到歷史文件:', error);
+ self.loadFromHistoryFile();
+ });
+ };
- // 即使沒有資料也要更新統計
+ /**
+ * 從歷史文件載入會話數據(備用方案)
+ */
+ SessionDataManager.prototype.loadFromHistoryFile = function() {
+ const self = this;
+
+ fetch('/api/load-session-history')
+ .then(function(response) {
+ if (response.ok) {
+ return response.json();
+ } else {
+ throw new Error('伺服器回應錯誤: ' + response.status);
+ }
+ })
+ .then(function(data) {
+ if (data && Array.isArray(data.sessions)) {
+ self.sessionHistory = data.sessions;
+ console.log('📊 從歷史文件載入', self.sessionHistory.length, '個會話');
+
+ // 載入完成後進行清理和統計更新
+ self.cleanupExpiredSessions();
+ self.updateStats();
+
+ // 觸發歷史記錄變更回調
+ if (self.onHistoryChange) {
+ self.onHistoryChange(self.sessionHistory);
+ }
+
+ // 觸發資料變更回調
+ if (self.onDataChanged) {
+ self.onDataChanged();
+ }
+ } else {
+ console.warn('📊 歷史文件回應格式錯誤:', data);
+ self.sessionHistory = [];
self.updateStats();
- // 觸發歷史記錄變更回調(空列表)
if (self.onHistoryChange) {
self.onHistoryChange(self.sessionHistory);
}
@@ -638,13 +718,10 @@
}
})
.catch(function(error) {
- console.warn('📊 從伺服器載入會話歷史失敗:', error);
+ console.warn('📊 從歷史文件載入失敗:', error);
self.sessionHistory = [];
-
- // 載入失敗時也要更新統計
self.updateStats();
- // 觸發歷史記錄變更回調(空列表)
if (self.onHistoryChange) {
self.onHistoryChange(self.sessionHistory);
}
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-details-modal.js b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-details-modal.js
index b355fed..0fd8521 100644
--- a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-details-modal.js
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-details-modal.js
@@ -45,6 +45,9 @@
console.log('🔍 顯示會話詳情:', sessionData.session_id);
+ // 存储当前会话数据,供复制功能使用
+ this.currentSessionData = sessionData;
+
// 關閉現有彈窗
this.closeModal();
@@ -166,7 +169,12 @@
${i18n ? i18n.t('sessionManagement.aiSummary') : 'AI 摘要'}:
-
${this.escapeHtml(details.summary)}
+
+
+ 📋
+
+
${this.renderMarkdownSafely(details.summary)}
+
${this.createUserMessagesSection(details)}
@@ -241,11 +249,12 @@
}
messagesHtml += `
-
+
${contentHtml}
@@ -310,6 +319,24 @@
};
document.addEventListener('keydown', this.keydownHandler);
}
+
+ // 复制摘要按钮
+ const copyBtn = this.currentModal.querySelector('.btn-copy-summary');
+ if (copyBtn) {
+ DOMUtils.addEventListener(copyBtn, 'click', function() {
+ self.copySummaryToClipboard();
+ });
+ }
+
+ // 复制用户消息按钮
+ const copyMessageBtns = this.currentModal.querySelectorAll('.btn-copy-message');
+ copyMessageBtns.forEach(function(btn) {
+ DOMUtils.addEventListener(btn, 'click', function(e) {
+ e.stopPropagation(); // 防止事件冒泡
+ const messageContent = btn.getAttribute('data-message-content');
+ self.copyMessageToClipboard(messageContent);
+ });
+ });
};
/**
@@ -355,12 +382,198 @@
*/
SessionDetailsModal.prototype.escapeHtml = function(text) {
if (!text) return '';
-
+
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
+ /**
+ * 安全地渲染 Markdown 內容
+ */
+ SessionDetailsModal.prototype.renderMarkdownSafely = function(content) {
+ if (!content) return '';
+
+ try {
+ // 检查 marked 和 DOMPurify 是否可用
+ if (typeof window.marked === 'undefined' || typeof window.DOMPurify === 'undefined') {
+ console.warn('⚠️ Markdown 库未载入,使用纯文字显示');
+ return this.escapeHtml(content);
+ }
+
+ // 使用 marked 解析 Markdown
+ const htmlContent = window.marked.parse(content);
+
+ // 使用 DOMPurify 清理 HTML
+ const cleanHtml = window.DOMPurify.sanitize(htmlContent, {
+ ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'a', 'hr', 'del', 's', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
+ ALLOWED_ATTR: ['href', 'title', 'class', 'align', 'style'],
+ ALLOW_DATA_ATTR: false
+ });
+
+ return cleanHtml;
+ } catch (error) {
+ console.error('❌ Markdown 渲染失败:', error);
+ return this.escapeHtml(content);
+ }
+ };
+
+ /**
+ * 复制摘要内容到剪贴板
+ */
+ SessionDetailsModal.prototype.copySummaryToClipboard = function() {
+ const self = this; // 定义 self 变量
+
+ try {
+ // 获取原始摘要内容(Markdown 源码)
+ const summaryContent = this.currentSessionData && this.currentSessionData.summary ?
+ this.currentSessionData.summary : '';
+
+ if (!summaryContent) {
+ console.warn('⚠️ 没有摘要内容可复制');
+ return;
+ }
+
+ // 传统复制方法
+ const fallbackCopyTextToClipboard = function(text) {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-999999px';
+ textArea.style.top = '-999999px';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try {
+ const successful = document.execCommand('copy');
+ if (successful) {
+ console.log('✅ 摘要内容已复制到剪贴板(传统方法)');
+ self.showToast('✅ 摘要已复制到剪贴板', 'success');
+ } else {
+ console.error('❌ 复制失败(传统方法)');
+ self.showToast('❌ 复制失败,请手动复制', 'error');
+ }
+ } catch (err) {
+ console.error('❌ 复制失败:', err);
+ self.showToast('❌ 复制失败,请手动复制', 'error');
+ } finally {
+ document.body.removeChild(textArea);
+ }
+ };
+
+ // 使用现代 Clipboard API
+ if (navigator.clipboard && window.isSecureContext) {
+ navigator.clipboard.writeText(summaryContent).then(function() {
+ console.log('✅ 摘要内容已复制到剪贴板');
+ self.showToast('✅ 摘要已复制到剪贴板', 'success');
+ }).catch(function(err) {
+ console.error('❌ 复制失败:', err);
+ // 降级到传统方法
+ fallbackCopyTextToClipboard(summaryContent);
+ });
+ } else {
+ // 降级到传统方法
+ fallbackCopyTextToClipboard(summaryContent);
+ }
+ } catch (error) {
+ console.error('❌ 复制摘要时发生错误:', error);
+ this.showToast('❌ 复制失败,请手动复制', 'error');
+ }
+ };
+
+ /**
+ * 复制用户消息内容到剪贴板
+ */
+ SessionDetailsModal.prototype.copyMessageToClipboard = function(messageContent) {
+ if (!messageContent) {
+ console.warn('⚠️ 没有消息内容可复制');
+ return;
+ }
+
+ const self = this;
+
+ try {
+ // 使用现代 Clipboard API
+ if (navigator.clipboard && window.isSecureContext) {
+ navigator.clipboard.writeText(messageContent).then(function() {
+ console.log('✅ 用户消息已复制到剪贴板');
+ self.showToast('✅ 消息已复制到剪贴板', 'success');
+ }).catch(function(err) {
+ console.error('❌ 复制失败:', err);
+ // 降级到传统方法
+ fallbackCopyTextToClipboard(messageContent);
+ });
+ } else {
+ // 降级到传统方法
+ fallbackCopyTextToClipboard(messageContent);
+ }
+
+ // 传统复制方法
+ const fallbackCopyTextToClipboard = function(text) {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-999999px';
+ textArea.style.top = '-999999px';
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+
+ try {
+ const successful = document.execCommand('copy');
+ if (successful) {
+ console.log('✅ 用户消息已复制到剪贴板(传统方法)');
+ self.showToast('✅ 消息已复制到剪贴板', 'success');
+ } else {
+ console.error('❌ 复制失败(传统方法)');
+ self.showToast('❌ 复制失败,请手动复制', 'error');
+ }
+ } catch (err) {
+ console.error('❌ 复制失败:', err);
+ self.showToast('❌ 复制失败,请手动复制', 'error');
+ } finally {
+ document.body.removeChild(textArea);
+ }
+ };
+ } catch (error) {
+ console.error('❌ 复制用户消息时发生错误:', error);
+ this.showToast('❌ 复制失败,请手动复制', 'error');
+ }
+ };
+
+
+
+ /**
+ * 显示提示消息
+ */
+ SessionDetailsModal.prototype.showToast = function(message, type) {
+ // 创建提示元素
+ const toast = document.createElement('div');
+ toast.className = 'copy-toast copy-toast-' + type;
+ toast.textContent = message;
+
+ // 添加到弹窗中
+ if (this.currentModal) {
+ this.currentModal.appendChild(toast);
+
+ // 显示动画
+ setTimeout(function() {
+ toast.classList.add('show');
+ }, 10);
+
+ // 自动隐藏
+ setTimeout(function() {
+ toast.classList.remove('show');
+ setTimeout(function() {
+ if (toast.parentNode) {
+ toast.parentNode.removeChild(toast);
+ }
+ }, 300);
+ }, 2000);
+ }
+ };
+
/**
* 檢查是否有彈窗開啟
*/
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js
index abe0f72..12d5b3b 100644
--- a/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/session/session-ui-renderer.js
@@ -486,9 +486,18 @@
// 狀態徽章
const statusContainer = DOMUtils.createElement('div', { className: 'session-status' });
+ const statusText = StatusUtils.getStatusText(sessionData.status);
+
+ // 添加調試信息
+ console.log('🎨 會話狀態調試:', {
+ sessionId: sessionData.session_id ? sessionData.session_id.substring(0, 8) + '...' : 'unknown',
+ rawStatus: sessionData.status,
+ displayText: statusText
+ });
+
const statusBadge = DOMUtils.createElement('span', {
className: 'status-badge ' + (sessionData.status || 'waiting'),
- textContent: StatusUtils.getStatusText(sessionData.status)
+ textContent: statusText
});
statusContainer.appendChild(statusBadge);
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/tab-manager.js b/src/mcp_feedback_enhanced/web/static/js/modules/tab-manager.js
new file mode 100644
index 0000000..66818d2
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/tab-manager.js
@@ -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 模組載入完成');
+
+})();
\ No newline at end of file
diff --git a/src/mcp_feedback_enhanced/web/static/js/modules/utils/status-utils.js b/src/mcp_feedback_enhanced/web/static/js/modules/utils/status-utils.js
index c547bd1..fc815a6 100644
--- a/src/mcp_feedback_enhanced/web/static/js/modules/utils/status-utils.js
+++ b/src/mcp_feedback_enhanced/web/static/js/modules/utils/status-utils.js
@@ -47,7 +47,7 @@
'waiting_for_feedback': 'connectionMonitor.waiting',
'active': 'status.processing.title',
'feedback_submitted': 'status.submitted.title',
- 'completed': 'status.submitted.title',
+ 'completed': 'status.completed.title',
'timeout': 'session.timeout',
'error': 'status.error',
'expired': 'session.timeout',
diff --git a/src/mcp_feedback_enhanced/web/static/js/vendor/marked.min.js b/src/mcp_feedback_enhanced/web/static/js/vendor/marked.min.js
new file mode 100644
index 0000000..9869504
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/static/js/vendor/marked.min.js
@@ -0,0 +1,6 @@
+/**
+ * marked v14.1.3 - a markdown parser
+ * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/markedjs/marked
+ */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/(^|[^\[])\^/g;function p(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(h,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function u(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch{return null}return e}const k={exec:()=>null};function g(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length
0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^(?: {1,4}| {0,3}\t)/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:f(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=f(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:f(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=f(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l/.test(e[l]))i.push(e[l]),t=!0;else{if(t)break;i.push(e[l])}e=e.slice(l);const o=i.join("\n"),a=o.replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1").replace(/^ {0,3}>[ \t]?/gm,"");n=n?`${n}\n${o}`:o,s=s?`${s}\n${a}`:a;const c=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(a,r,!0),this.lexer.state.top=c,0===e.length)break;const h=r[r.length-1];if("code"===h?.type)break;if("blockquote"===h?.type){const t=h,i=t.raw+"\n"+e.join("\n"),l=this.blockquote(i);r[r.length-1]=l,n=n.substring(0,n.length-t.raw.length)+l.raw,s=s.substring(0,s.length-t.text.length)+l.text;break}if("list"!==h?.type);else{const t=h,i=t.raw+"\n"+e.join("\n"),l=this.list(i);r[r.length-1]=l,n=n.substring(0,n.length-h.raw.length)+l.raw,s=s.substring(0,s.length-t.raw.length)+l.raw,e=i.substring(r[r.length-1].raw.length).split("\n")}}return{type:"blockquote",raw:n,tokens:r,text:s}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(/[^ ]/),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&/^[ \t]*$/.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,p-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,p-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,p-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,p-1)}}#`),l=new RegExp(`^ {0,${Math.min(3,p-1)}}<[a-z].*>`,"i");for(;e;){const u=e.split("\n",1)[0];let k;if(c=u,this.options.pedantic?(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," "),k=c):k=c.replace(/\t/g," "),r.test(c))break;if(i.test(c))break;if(l.test(c))break;if(t.test(c))break;if(n.test(c))break;if(k.search(/[^ ]/)>=p||!c.trim())o+="\n"+k.slice(p);else{if(h)break;if(a.replace(/\t/g," ").search(/[^ ]/)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=u+"\n",e=e.substring(u.length+1),a=k.slice(p)}}r.loose||(l?r.loose=!0:/\n[ \t]*\n[ \t]*$/.test(s)&&(l=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:s,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}r.items[r.items.length-1].raw=r.items[r.items.length-1].raw.trimEnd(),r.items[r.items.length-1].text=r.items[r.items.length-1].text.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=g(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=f(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),d(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return d(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const b=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,w=/(?:[*+-]|\d{1,9}[.)])/,m=p(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,w).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),y=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,$=/(?!\s*\])(?:\\.|[^\[\]\\])+/,z=p(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",$).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),T=p(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,w).getRegex(),R="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",_=/|$))/,A=p("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$)|(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ \t]*)+\\n|$))","i").replace("comment",_).replace("tag",R).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),S=p(y).replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex(),I={blockquote:p(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",S).getRegex(),code:/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,def:z,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:b,html:A,lheading:m,list:T,newline:/^(?:[ \t]*(?:\n|$))+/,paragraph:S,table:k,text:/^[^\n]+/},E=p("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3}\t)[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex(),q={...I,table:E,paragraph:p(y).replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",E).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex()},Z={...I,html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?\\1> *(?:\\n{2,}|\\s*$)| \\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",_).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:k,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:p(y).replace("hr",b).replace("heading"," *#{1,6} *[^\n]").replace("lheading",m).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},P=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,L=/^( {2,}|\\)\n(?!\s*$)/,v="\\p{P}\\p{S}",Q=p(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,v).getRegex(),B=p(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,v).getRegex(),M=p("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,v).getRegex(),O=p("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,v).getRegex(),j=p(/\\([punct])/,"gu").replace(/punct/g,v).getRegex(),D=p(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),C=p(_).replace("(?:--\x3e|$)","--\x3e").getRegex(),H=p("^comment|^[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",C).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),U=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,X=p(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",U).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),F=p(/^!?\[(label)\]\[(ref)\]/).replace("label",U).replace("ref",$).getRegex(),N=p(/^!?\[(ref)\](?:\[\])?/).replace("ref",$).getRegex(),G={_backpedal:k,anyPunctuation:j,autolink:D,blockSkip:/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,br:L,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:k,emStrongLDelim:B,emStrongRDelimAst:M,emStrongRDelimUnd:O,escape:P,link:X,nolink:N,punctuation:Q,reflink:F,reflinkSearch:p("reflink|nolink(?!\\()","g").replace("reflink",F).replace("nolink",N).getRegex(),tag:H,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0)))))if(s=this.tokenizer.space(e))e=e.substring(s.raw.length),1===s.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(s);else if(s=this.tokenizer.code(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?t.push(s):(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.fences(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.heading(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.hr(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.blockquote(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.list(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.html(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.def(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title}):(r.raw+="\n"+s.raw,r.text+="\n"+s.raw,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.table(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.lheading(e))e=e.substring(s.raw.length),t.push(s);else{if(i=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(i=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i)))r=t[t.length-1],n&&"paragraph"===r?.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);else if(s=this.tokenizer.text(e))e=e.substring(s.raw.length),r=t[t.length-1],r&&"text"===r.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class te{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(/^\S*/)?.[0],r=e.replace(/\n$/,"")+"\n";return s?''+(n?r:c(r,!0))+"
\n":""+(n?r:c(r,!0))+"
\n"}blockquote({tokens:e}){return`\n${this.parser.parse(e)} \n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} \n`}hr(e){return" \n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t\n"+s+""+r+">\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?e.tokens.length>0&&"paragraph"===e.tokens[0].type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+e.tokens[0].tokens[0].text)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" "}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`${t} \n`}checkbox({checked:e}){return" '}paragraph({tokens:e}){return`${this.parser.parseInline(e)}
\n`}table(e){let t="",n="";for(let t=0;t${s}`),"\n"}tablerow({text:e}){return`\n${e} \n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`${n}>\n`}strong({tokens:e}){return`${this.parser.parseInline(e)} `}em({tokens:e}){return`${this.parser.parseInline(e)} `}codespan({text:e}){return`${e}
`}br(e){return" "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=u(e);if(null===r)return s;let i='"+s+" ",i}image({href:e,title:t,text:n}){const s=u(e);if(null===s)return n;let r=` ",r}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):e.text}}class ne{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}}class se{options;renderer;textRenderer;constructor(t){this.options=t||e.defaults,this.options.renderer=this.options.renderer||new te,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new ne}static parse(e,t){return new se(t).parse(e)}static parseInline(e,t){return new se(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new te(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new x(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new re;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];re.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ee.lex(e,t??this.defaults)}parser(e,t){return se.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?ee.lex:ee.lexInline,o=r.hooks?r.hooks.provideParser():e?se.parse:se.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="An error occurred:
"+c(n.message+"",!0)+" ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const le=new ie;function oe(e,t){return le.parse(e,t)}oe.options=oe.setOptions=function(e){return le.setOptions(e),oe.defaults=le.defaults,n(oe.defaults),oe},oe.getDefaults=t,oe.defaults=e.defaults,oe.use=function(...e){return le.use(...e),oe.defaults=le.defaults,n(oe.defaults),oe},oe.walkTokens=function(e,t){return le.walkTokens(e,t)},oe.parseInline=le.parseInline,oe.Parser=se,oe.parser=se.parse,oe.Renderer=te,oe.TextRenderer=ne,oe.Lexer=ee,oe.lexer=ee.lex,oe.Tokenizer=x,oe.Hooks=re,oe.parse=oe;const ae=oe.options,ce=oe.setOptions,he=oe.use,pe=oe.walkTokens,ue=oe.parseInline,ke=oe,ge=se.parse,fe=ee.lex;e.Hooks=re,e.Lexer=ee,e.Marked=ie,e.Parser=se,e.Renderer=te,e.TextRenderer=ne,e.Tokenizer=x,e.getDefaults=t,e.lexer=fe,e.marked=oe,e.options=ae,e.parse=ke,e.parseInline=ue,e.parser=ge,e.setOptions=ce,e.use=he,e.walkTokens=pe}));
diff --git a/src/mcp_feedback_enhanced/web/static/js/vendor/purify.min.js b/src/mcp_feedback_enhanced/web/static/js/vendor/purify.min.js
new file mode 100644
index 0000000..6744382
--- /dev/null
+++ b/src/mcp_feedback_enhanced/web/static/js/vendor/purify.min.js
@@ -0,0 +1,3 @@
+/*! @license DOMPurify 3.2.2 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.2/LICENSE */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),A=b(RegExp.prototype.test),_=(S=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),K=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var $=Object.freeze({__proto__:null,ARIA_ATTR:G,ATTR_WHITESPACE:X,CUSTOM_ELEMENT:K,DATA_ATTR:W,DOCTYPE_NAME:q,ERB_EXPR:F,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,MUSTACHE_EXPR:H,TMPLIT_EXPR:B});const V=1,Z=3,J=7,Q=8,ee=9,te=function(){return"undefined"==typeof window?null:window};var ne=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:te();const o=e=>t(e);if(o.version="3.2.2",o.removed=[],!n||!n.document||n.document.nodeType!==ee)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:S,Node:b,Element:R,NodeFilter:H,NamedNodeMap:F=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=O(j,"cloneNode"),K=O(j,"remove"),ne=O(j,"nextSibling"),oe=O(j,"childNodes"),re=O(j,"parentNode");if("function"==typeof S){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ie,ae="";const{implementation:le,createNodeIterator:ce,createDocumentFragment:se,getElementsByTagName:ue}=r,{importNode:me}=a;let pe={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof e&&"function"==typeof re&&le&&void 0!==le.createHTMLDocument;const{MUSTACHE_EXPR:fe,ERB_EXPR:de,TMPLIT_EXPR:he,DATA_ATTR:ge,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:Ae}=$;let{IS_ALLOWED_URI:_e}=$,Se=null;const be=N({},[...D,...L,...v,...x,...k]);let Ne=null;const Re=N({},[...I,...U,...z,...P]);let we=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Oe=null,De=null,Le=!0,ve=!0,Ce=!1,xe=!0,Me=!1,ke=!0,Ie=!1,Ue=!1,ze=!1,Pe=!1,He=!1,Fe=!1,Be=!0,We=!1,Ge=!0,Ye=!1,je={},Xe=null;const qe=N({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ke=null;const $e=N({},["audio","video","img","source","image","track"]);let Ve=null;const Ze=N({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Je="http://www.w3.org/1998/Math/MathML",Qe="http://www.w3.org/2000/svg",et="http://www.w3.org/1999/xhtml";let tt=et,nt=!1,ot=null;const rt=N({},[Je,Qe,et],d);let it=N({},["mi","mo","mn","ms","mtext"]),at=N({},["annotation-xml"]);const lt=N({},["title","style","font","a","script"]);let ct=null;const st=["application/xhtml+xml","text/html"];let ut=null,mt=null;const pt=r.createElement("form"),ft=function(e){return e instanceof RegExp||e instanceof Function},dt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!mt||mt!==e){if(e&&"object"==typeof e||(e={}),e=w(e),ct=-1===st.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,ut="application/xhtml+xml"===ct?d:f,Se=E(e,"ALLOWED_TAGS")?N({},e.ALLOWED_TAGS,ut):be,Ne=E(e,"ALLOWED_ATTR")?N({},e.ALLOWED_ATTR,ut):Re,ot=E(e,"ALLOWED_NAMESPACES")?N({},e.ALLOWED_NAMESPACES,d):rt,Ve=E(e,"ADD_URI_SAFE_ATTR")?N(w(Ze),e.ADD_URI_SAFE_ATTR,ut):Ze,Ke=E(e,"ADD_DATA_URI_TAGS")?N(w($e),e.ADD_DATA_URI_TAGS,ut):$e,Xe=E(e,"FORBID_CONTENTS")?N({},e.FORBID_CONTENTS,ut):qe,Oe=E(e,"FORBID_TAGS")?N({},e.FORBID_TAGS,ut):{},De=E(e,"FORBID_ATTR")?N({},e.FORBID_ATTR,ut):{},je=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,Le=!1!==e.ALLOW_ARIA_ATTR,ve=!1!==e.ALLOW_DATA_ATTR,Ce=e.ALLOW_UNKNOWN_PROTOCOLS||!1,xe=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Me=e.SAFE_FOR_TEMPLATES||!1,ke=!1!==e.SAFE_FOR_XML,Ie=e.WHOLE_DOCUMENT||!1,Pe=e.RETURN_DOM||!1,He=e.RETURN_DOM_FRAGMENT||!1,Fe=e.RETURN_TRUSTED_TYPE||!1,ze=e.FORCE_BODY||!1,Be=!1!==e.SANITIZE_DOM,We=e.SANITIZE_NAMED_PROPS||!1,Ge=!1!==e.KEEP_CONTENT,Ye=e.IN_PLACE||!1,_e=e.ALLOWED_URI_REGEXP||Y,tt=e.NAMESPACE||et,it=e.MATHML_TEXT_INTEGRATION_POINTS||it,at=e.HTML_INTEGRATION_POINTS||at,we=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ft(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ft(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Me&&(ve=!1),He&&(Pe=!0),je&&(Se=N({},k),Ne=[],!0===je.html&&(N(Se,D),N(Ne,I)),!0===je.svg&&(N(Se,L),N(Ne,U),N(Ne,P)),!0===je.svgFilters&&(N(Se,v),N(Ne,U),N(Ne,P)),!0===je.mathMl&&(N(Se,x),N(Ne,z),N(Ne,P))),e.ADD_TAGS&&(Se===be&&(Se=w(Se)),N(Se,e.ADD_TAGS,ut)),e.ADD_ATTR&&(Ne===Re&&(Ne=w(Ne)),N(Ne,e.ADD_ATTR,ut)),e.ADD_URI_SAFE_ATTR&&N(Ve,e.ADD_URI_SAFE_ATTR,ut),e.FORBID_CONTENTS&&(Xe===qe&&(Xe=w(Xe)),N(Xe,e.FORBID_CONTENTS,ut)),Ge&&(Se["#text"]=!0),Ie&&N(Se,["html","head","body"]),Se.table&&(N(Se,["tbody"]),delete Oe.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ie=e.TRUSTED_TYPES_POLICY,ae=ie.createHTML("")}else void 0===ie&&(ie=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(G,c)),null!==ie&&"string"==typeof ae&&(ae=ie.createHTML(""));i&&i(e),mt=e}},ht=N({},[...L,...v,...C]),gt=N({},[...x,...M]),Tt=function(e){p(o.removed,{element:e});try{re(e).removeChild(e)}catch(t){K(e)}},yt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(Pe||He)try{Tt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Et=function(e){let t=null,n=null;if(ze)e=" "+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===ct&&tt===et&&(e=''+e+"");const o=ie?ie.createHTML(e):e;if(tt===et)try{t=(new W).parseFromString(o,ct)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(tt,"template",null);try{t.documentElement.innerHTML=nt?ae:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),tt===et?ue.call(t,Ie?"html":"body")[0]:Ie?t.documentElement:i},At=function(e){return ce.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},_t=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof F)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},St=function(e){return"function"==typeof b&&e instanceof b};function bt(e,t,n){u(e,(e=>{e.call(o,t,n,mt)}))}const Nt=function(e){let t=null;if(bt(pe.beforeSanitizeElements,e,null),_t(e))return Tt(e),!0;const n=ut(e.nodeName);if(bt(pe.uponSanitizeElement,e,{tagName:n,allowedTags:Se}),e.hasChildNodes()&&!St(e.firstElementChild)&&A(/<[/\w]/g,e.innerHTML)&&A(/<[/\w]/g,e.textContent))return Tt(e),!0;if(e.nodeType===J)return Tt(e),!0;if(ke&&e.nodeType===Q&&A(/<[/\w]/g,e.data))return Tt(e),!0;if(!Se[n]||Oe[n]){if(!Oe[n]&&wt(n)){if(we.tagNameCheck instanceof RegExp&&A(we.tagNameCheck,n))return!1;if(we.tagNameCheck instanceof Function&&we.tagNameCheck(n))return!1}if(Ge&&!Xe[n]){const t=re(e)||e.parentNode,n=oe(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=X(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,ne(e))}}}return Tt(e),!0}return e instanceof R&&!function(e){let t=re(e);t&&t.tagName||(t={namespaceURI:tt,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!ot[e.namespaceURI]&&(e.namespaceURI===Qe?t.namespaceURI===et?"svg"===n:t.namespaceURI===Je?"svg"===n&&("annotation-xml"===o||it[o]):Boolean(ht[n]):e.namespaceURI===Je?t.namespaceURI===et?"math"===n:t.namespaceURI===Qe?"math"===n&&at[o]:Boolean(gt[n]):e.namespaceURI===et?!(t.namespaceURI===Qe&&!at[o])&&!(t.namespaceURI===Je&&!it[o])&&!gt[n]&&(lt[n]||!ht[n]):!("application/xhtml+xml"!==ct||!ot[e.namespaceURI]))}(e)?(Tt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!A(/<\/no(script|embed|frames)/i,e.innerHTML)?(Me&&e.nodeType===Z&&(t=e.textContent,u([fe,de,he],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),bt(pe.afterSanitizeElements,e,null),!1):(Tt(e),!0)},Rt=function(e,t,n){if(Be&&("id"===t||"name"===t)&&(n in r||n in pt))return!1;if(ve&&!De[t]&&A(ge,t));else if(Le&&A(Te,t));else if(!Ne[t]||De[t]){if(!(wt(e)&&(we.tagNameCheck instanceof RegExp&&A(we.tagNameCheck,e)||we.tagNameCheck instanceof Function&&we.tagNameCheck(e))&&(we.attributeNameCheck instanceof RegExp&&A(we.attributeNameCheck,t)||we.attributeNameCheck instanceof Function&&we.attributeNameCheck(t))||"is"===t&&we.allowCustomizedBuiltInElements&&(we.tagNameCheck instanceof RegExp&&A(we.tagNameCheck,n)||we.tagNameCheck instanceof Function&&we.tagNameCheck(n))))return!1}else if(Ve[t]);else if(A(_e,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Ke[e]){if(Ce&&!A(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},wt=function(e){return"annotation-xml"!==e&&h(e,Ae)},Ot=function(e){bt(pe.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ne,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=ut(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,bt(pe.uponSanitizeAttribute,e,n),p=n.attrValue,!We||"id"!==s&&"name"!==s||(yt(a,e),p="user-content-"+p),ke&&A(/((--!?|])>)|<\/(style|title)/i,p)){yt(a,e);continue}if(n.forceKeepAttr)continue;if(yt(a,e),!n.keepAttr)continue;if(!xe&&A(/\/>/i,p)){yt(a,e);continue}Me&&u([fe,de,he],(e=>{p=g(p,e," ")}));const f=ut(e.nodeName);if(Rt(f,s,p)){if(ie&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ie.createHTML(p);break;case"TrustedScriptURL":p=ie.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),_t(e)?Tt(e):m(o.removed)}catch(e){}}}bt(pe.afterSanitizeAttributes,e,null)},Dt=function e(t){let n=null;const o=At(t);for(bt(pe.beforeSanitizeShadowDOM,t,null);n=o.nextNode();)bt(pe.uponSanitizeShadowNode,n,null),Nt(n)||(n.content instanceof s&&e(n.content),Ot(n));bt(pe.afterSanitizeShadowDOM,t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(nt=!e,nt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!St(e)){if("function"!=typeof e.toString)throw _("toString is not a function");if("string"!=typeof(e=e.toString()))throw _("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Ue||dt(t),o.removed=[],"string"==typeof e&&(Ye=!1),Ye){if(e.nodeName){const t=ut(e.nodeName);if(!Se[t]||Oe[t])throw _("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Et("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===V&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Pe&&!Me&&!Ie&&-1===e.indexOf("<"))return ie&&Fe?ie.createHTML(e):e;if(n=Et(e),!n)return Pe?null:Fe?ae:""}n&&ze&&Tt(n.firstChild);const c=At(Ye?e:n);for(;i=c.nextNode();)Nt(i)||(i.content instanceof s&&Dt(i.content),Ot(i));if(Ye)return e;if(Pe){if(He)for(l=se.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Ne.shadowroot||Ne.shadowrootmode)&&(l=me.call(a,l,!0)),l}let m=Ie?n.outerHTML:n.innerHTML;return Ie&&Se["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&A(q,n.ownerDocument.doctype.name)&&(m="\n"+m),Me&&u([fe,de,he],(e=>{m=g(m,e," ")})),ie&&Fe?ie.createHTML(m):m},o.setConfig=function(){dt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ue=!0},o.clearConfig=function(){mt=null,Ue=!1},o.isValidAttribute=function(e,t,n){mt||dt({});const o=ut(e),r=ut(t);return Rt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&p(pe[e],t)},o.removeHook=function(e){return m(pe[e])},o.removeHooks=function(e){pe[e]=[]},o.removeAllHooks=function(){pe={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return ne}));
+//# sourceMappingURL=purify.min.js.map
diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html
index b0f4ac4..3f2617d 100644
--- a/src/mcp_feedback_enhanced/web/templates/feedback.html
+++ b/src/mcp_feedback_enhanced/web/templates/feedback.html
@@ -1,9 +1,15 @@
-
+
{{ title }}
+
+
+
+
+
+
@@ -651,6 +657,8 @@
詳細資訊
+ 📋 复制会话内容
+ 📝 复制用户内容
@@ -733,8 +741,8 @@
- 繁體中文
简体中文
+ 繁體中文
English
@@ -1049,9 +1057,9 @@
-
-
-
+
+
+
diff --git a/src/mcp_feedback_enhanced/web/templates/index.html b/src/mcp_feedback_enhanced/web/templates/index.html
index f66db39..0a28216 100644
--- a/src/mcp_feedback_enhanced/web/templates/index.html
+++ b/src/mcp_feedback_enhanced/web/templates/index.html
@@ -1,5 +1,5 @@
-
+
@@ -129,7 +129,6 @@
/* 主容器 - 有會話時顯示 */
.main-container {
display: none;
- max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;