From 58540f3c56c37ff2004649319ea93ccba8efdc69 Mon Sep 17 00:00:00 2001 From: Minidoracat Date: Thu, 5 Jun 2025 01:59:56 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=96=B0=E5=A2=9E=E8=B6=85?= =?UTF-8?q?=E6=99=82=E6=8E=A7=E5=88=B6=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E8=B6=85=E6=99=82=E8=A8=AD=E7=BD=AE=E3=80=81=E5=80=92?= =?UTF-8?q?=E6=95=B8=E8=A8=88=E6=99=82=E5=99=A8=E9=A1=AF=E7=A4=BA=E5=8F=8A?= =?UTF-8?q?=E8=87=AA=E5=8B=95=E9=97=9C=E9=96=89=E4=BB=8B=E9=9D=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E4=B8=A6=E6=9B=B4=E6=96=B0=E7=9B=B8=E9=97=9C?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E8=88=87=E6=96=87=E6=AA=94=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mcp_feedback_enhanced/gui/main.py | 71 ++-- .../gui/tabs/settings_tab.py | 121 ++++++- .../gui/widgets/image_upload.py | 14 +- .../gui/widgets/styled_spinbox.py | 163 +++++++++ .../gui/widgets/timeout_widget.py | 322 ++++++++++++++++++ .../gui/window/config_manager.py | 34 ++ .../gui/window/feedback_window.py | 206 ++++++++++- .../gui/window/tab_manager.py | 2 + .../locales/en/translations.json | 16 + .../locales/zh-CN/translations.json | 16 + .../locales/zh-TW/translations.json | 16 + src/mcp_feedback_enhanced/server.py | 20 +- .../web/locales/en/translation.json | 16 + .../web/locales/zh-CN/translation.json | 16 + .../web/locales/zh-TW/translation.json | 16 + src/mcp_feedback_enhanced/web/main.py | 29 +- .../web/routes/main_routes.py | 25 +- .../web/static/js/app.js | 247 +++++++++++++- .../web/templates/feedback.html | 174 +++++++++- 19 files changed, 1461 insertions(+), 63 deletions(-) create mode 100644 src/mcp_feedback_enhanced/gui/widgets/styled_spinbox.py create mode 100644 src/mcp_feedback_enhanced/gui/widgets/timeout_widget.py diff --git a/src/mcp_feedback_enhanced/gui/main.py b/src/mcp_feedback_enhanced/gui/main.py index 594a260..a5d15cd 100644 --- a/src/mcp_feedback_enhanced/gui/main.py +++ b/src/mcp_feedback_enhanced/gui/main.py @@ -60,15 +60,15 @@ def feedback_ui(project_directory: str, summary: str) -> Optional[FeedbackResult def feedback_ui_with_timeout(project_directory: str, summary: str, timeout: int) -> Optional[FeedbackResult]: """ 啟動帶超時的回饋收集 GUI 介面 - + Args: project_directory: 專案目錄路徑 summary: AI 工作摘要 - timeout: 超時時間(秒) - + timeout: 超時時間(秒)- MCP 傳入的超時時間,作為最大限制 + Returns: Optional[FeedbackResult]: 回饋結果,如果用戶取消或超時則返回 None - + Raises: TimeoutError: 當超時時拋出 """ @@ -76,47 +76,72 @@ def feedback_ui_with_timeout(project_directory: str, summary: str, timeout: int) app = QApplication.instance() if app is None: app = QApplication(sys.argv) - + # 設定全域微軟正黑體字體 font = QFont("Microsoft JhengHei", 11) # 微軟正黑體,11pt app.setFont(font) - + # 設定字體回退順序,確保中文字體正確顯示 app.setStyleSheet(""" * { font-family: "Microsoft JhengHei", "微軟正黑體", "Microsoft YaHei", "微软雅黑", "SimHei", "黑体", sans-serif; } """) - - # 創建主窗口 - window = FeedbackWindow(project_directory, summary) + + # 創建主窗口,傳入 MCP 超時時間 + window = FeedbackWindow(project_directory, summary, timeout) + + # 連接超時信號 + timeout_occurred = False + def on_timeout(): + nonlocal timeout_occurred + timeout_occurred = True + + window.timeout_occurred.connect(on_timeout) + window.show() - - # 創建超時計時器 - timeout_timer = QTimer() - timeout_timer.setSingleShot(True) - timeout_timer.timeout.connect(lambda: _handle_timeout(window, app)) - timeout_timer.start(timeout * 1000) # 轉換為毫秒 - + + # 開始用戶設置的超時倒數(如果啟用) + window.start_timeout_if_enabled() + + # 創建 MCP 超時計時器作為後備 + mcp_timeout_timer = QTimer() + mcp_timeout_timer.setSingleShot(True) + mcp_timeout_timer.timeout.connect(lambda: _handle_mcp_timeout(window, app)) + mcp_timeout_timer.start(timeout * 1000) # 轉換為毫秒 + # 運行事件循環直到窗口關閉 app.exec() - + # 停止計時器(如果還在運行) - timeout_timer.stop() - + mcp_timeout_timer.stop() + window.stop_timeout() + # 檢查是否超時 - if hasattr(window, '_timeout_occurred'): + if timeout_occurred: + raise TimeoutError(f"回饋收集超時,GUI 介面已自動關閉") + elif hasattr(window, '_timeout_occurred'): raise TimeoutError(f"回饋收集超時({timeout}秒),GUI 介面已自動關閉") - + # 返回結果 return window.result def _handle_timeout(window: FeedbackWindow, app: QApplication) -> None: - """處理超時事件""" + """處理超時事件(舊版本,保留向後兼容)""" # 標記超時發生 window._timeout_occurred = True # 強制關閉視窗 window.force_close() # 退出應用程式 - app.quit() \ No newline at end of file + app.quit() + + +def _handle_mcp_timeout(window: FeedbackWindow, app: QApplication) -> None: + """處理 MCP 超時事件(後備機制)""" + # 標記超時發生 + window._timeout_occurred = True + # 強制關閉視窗 + window.force_close() + # 退出應用程式 + app.quit() \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/gui/tabs/settings_tab.py b/src/mcp_feedback_enhanced/gui/tabs/settings_tab.py index 990d573..10a390a 100644 --- a/src/mcp_feedback_enhanced/gui/tabs/settings_tab.py +++ b/src/mcp_feedback_enhanced/gui/tabs/settings_tab.py @@ -8,11 +8,12 @@ """ from PySide6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QRadioButton, QButtonGroup, QMessageBox, - QCheckBox, QPushButton, QFrame + QCheckBox, QPushButton, QFrame, QSpinBox ) from ..widgets import SwitchWithLabel +from ..widgets.styled_spinbox import StyledSpinBox from PySide6.QtCore import Signal, Qt from PySide6.QtGui import QFont @@ -25,6 +26,7 @@ class SettingsTab(QWidget): language_changed = Signal() layout_change_requested = Signal(bool, str) # 佈局變更請求信號 (combined_mode, orientation) reset_requested = Signal() # 重置設定請求信號 + timeout_settings_changed = Signal(bool, int) # 超時設置變更信號 (enabled, duration) def __init__(self, combined_mode: bool, config_manager, parent=None): super().__init__(parent) @@ -86,7 +88,13 @@ class SettingsTab(QWidget): # 添加分隔線 self._add_separator(content_layout) - + + # === 超時設置 === + self._create_timeout_section(content_layout) + + # 添加分隔線 + self._add_separator(content_layout) + # === 重置設定 === self._create_reset_section(content_layout) @@ -306,7 +314,63 @@ class SettingsTab(QWidget): options_layout.addWidget(self.always_center_switch) layout.addLayout(options_layout) - + + def _create_timeout_section(self, layout: QVBoxLayout) -> None: + """創建超時設置區域""" + header = self._create_section_header(t('timeout.settings.title'), "⏰") + layout.addWidget(header) + # 保存引用以便更新 + self.ui_elements['timeout_header'] = header + + # 選項容器 + options_layout = QVBoxLayout() + options_layout.setSpacing(12) + + # 啟用超時自動關閉開關 + self.timeout_enabled_switch = SwitchWithLabel(t('timeout.enable')) + self.timeout_enabled_switch.setChecked(self.config_manager.get_timeout_enabled()) + self.timeout_enabled_switch.toggled.connect(self._on_timeout_enabled_changed) + options_layout.addWidget(self.timeout_enabled_switch) + + # 超時時間設置 + timeout_duration_layout = QHBoxLayout() + timeout_duration_layout.setContentsMargins(0, 8, 0, 0) + + # 標籤 + timeout_duration_label = QLabel(t('timeout.duration.label')) + timeout_duration_label.setStyleSheet(""" + QLabel { + font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif; + color: #ffffff; + font-size: 13px; + } + """) + timeout_duration_layout.addWidget(timeout_duration_label) + # 保存引用以便更新 + self.ui_elements['timeout_duration_label'] = timeout_duration_label + + # 彈性空間 + timeout_duration_layout.addStretch() + + # 時間輸入框 + self.timeout_duration_spinbox = StyledSpinBox() + self.timeout_duration_spinbox.setRange(30, 7200) # 30秒到2小時 + self.timeout_duration_spinbox.setValue(self.config_manager.get_timeout_duration()) + self.timeout_duration_spinbox.setSuffix(" " + t('timeout.seconds')) + # StyledSpinBox 已經有內建樣式,不需要額外設置 + self.timeout_duration_spinbox.valueChanged.connect(self._on_timeout_duration_changed) + timeout_duration_layout.addWidget(self.timeout_duration_spinbox) + + options_layout.addLayout(timeout_duration_layout) + + # 說明文字 + description = self._create_description(t('timeout.settings.description')) + options_layout.addWidget(description) + # 保存引用以便更新 + self.ui_elements['timeout_description'] = description + + layout.addLayout(options_layout) + def _create_reset_section(self, layout: QVBoxLayout) -> None: """創建重置設定區域""" header = self._create_section_header(t('settings.reset.title'), "🔄") @@ -421,7 +485,27 @@ class SettingsTab(QWidget): # 立即保存設定 self.config_manager.set_always_center_window(checked) debug_log(f"視窗定位設置已保存: {checked}") # 調試輸出 - + + def _on_timeout_enabled_changed(self, enabled: bool) -> None: + """超時啟用狀態變更事件處理""" + # 立即保存設定 + self.config_manager.set_timeout_enabled(enabled) + debug_log(f"超時啟用設置已保存: {enabled}") + + # 發出信號通知主窗口 + duration = self.timeout_duration_spinbox.value() + self.timeout_settings_changed.emit(enabled, duration) + + def _on_timeout_duration_changed(self, duration: int) -> None: + """超時時間變更事件處理""" + # 立即保存設定 + self.config_manager.set_timeout_duration(duration) + debug_log(f"超時時間設置已保存: {duration}") + + # 發出信號通知主窗口 + enabled = self.timeout_enabled_switch.isChecked() + self.timeout_settings_changed.emit(enabled, duration) + def _on_reset_settings(self) -> None: """重置設定事件處理""" reply = QMessageBox.question( @@ -446,9 +530,10 @@ class SettingsTab(QWidget): self.ui_elements['window_header'].setText(f"🖥️ {t('settings.window.title')}") if 'reset_header' in self.ui_elements: self.ui_elements['reset_header'].setText(f"🔄 {t('settings.reset.title')}") - + if 'timeout_header' in self.ui_elements: + self.ui_elements['timeout_header'].setText(f"⏰ {t('timeout.settings.title')}") + - # 更新提示文字 if 'separate_hint' in self.ui_elements: self.ui_elements['separate_hint'].setText(f" {t('settings.layout.separateModeDescription')}") @@ -456,7 +541,9 @@ class SettingsTab(QWidget): self.ui_elements['vertical_hint'].setText(f" {t('settings.layout.combinedVerticalDescription')}") if 'horizontal_hint' in self.ui_elements: self.ui_elements['horizontal_hint'].setText(f" {t('settings.layout.combinedHorizontalDescription')}") - + if 'timeout_description' in self.ui_elements: + self.ui_elements['timeout_description'].setText(t('timeout.settings.description')) + # 更新按鈕文字 if hasattr(self, 'reset_button'): self.reset_button.setText(t('settings.reset.button')) @@ -464,6 +551,14 @@ class SettingsTab(QWidget): # 更新切換開關文字 if hasattr(self, 'always_center_switch'): self.always_center_switch.setText(t('settings.window.alwaysCenter')) + if hasattr(self, 'timeout_enabled_switch'): + self.timeout_enabled_switch.setText(t('timeout.enable')) + + # 更新超時相關標籤和控件 + if 'timeout_duration_label' in self.ui_elements: + self.ui_elements['timeout_duration_label'].setText(t('timeout.duration.label')) + if hasattr(self, 'timeout_duration_spinbox'): + self.timeout_duration_spinbox.setSuffix(" " + t('timeout.seconds')) # 更新單選按鈕文字 if hasattr(self, 'separate_mode_radio'): @@ -491,6 +586,16 @@ class SettingsTab(QWidget): always_center = self.config_manager.get_always_center_window() self.always_center_switch.setChecked(always_center) debug_log(f"重新載入視窗定位設置: {always_center}") # 調試輸出 + + # 重新載入超時設定 + if hasattr(self, 'timeout_enabled_switch'): + timeout_enabled = self.config_manager.get_timeout_enabled() + self.timeout_enabled_switch.setChecked(timeout_enabled) + debug_log(f"重新載入超時啟用設置: {timeout_enabled}") + if hasattr(self, 'timeout_duration_spinbox'): + timeout_duration = self.config_manager.get_timeout_duration() + self.timeout_duration_spinbox.setValue(timeout_duration) + debug_log(f"重新載入超時時間設置: {timeout_duration}") # 調試輸出 def set_layout_mode(self, combined_mode: bool) -> None: """設置佈局模式""" diff --git a/src/mcp_feedback_enhanced/gui/widgets/image_upload.py b/src/mcp_feedback_enhanced/gui/widgets/image_upload.py index 49aaf48..9a36ce8 100644 --- a/src/mcp_feedback_enhanced/gui/widgets/image_upload.py +++ b/src/mcp_feedback_enhanced/gui/widgets/image_upload.py @@ -132,10 +132,12 @@ class ImageUploadWidget(QWidget): def _on_size_limit_changed(self, index: int) -> None: """圖片大小限制變更處理""" - if self.config_manager: + if self.config_manager and index >= 0: size_bytes = self.size_limit_combo.itemData(index) - self.config_manager.set_image_size_limit(size_bytes) - debug_log(f"圖片大小限制已更新: {size_bytes} bytes") + # 處理 None 值 + if size_bytes is not None: + self.config_manager.set_image_size_limit(size_bytes) + debug_log(f"圖片大小限制已更新: {size_bytes} bytes") def _on_base64_detail_changed(self, state: int) -> None: """Base64 詳細模式變更處理""" @@ -683,6 +685,9 @@ class ImageUploadWidget(QWidget): # 保存當前選擇 current_data = self.size_limit_combo.currentData() + # 暫時斷開信號連接以避免觸發變更事件 + self.size_limit_combo.blockSignals(True) + # 清除並重新添加選項 self.size_limit_combo.clear() self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.unlimited'), 0) @@ -696,6 +701,9 @@ class ImageUploadWidget(QWidget): self.size_limit_combo.setCurrentIndex(i) break + # 重新連接信號 + self.size_limit_combo.blockSignals(False) + if hasattr(self, 'base64_checkbox'): self.base64_checkbox.setText(t('images.settings.base64Detail')) self.base64_checkbox.setToolTip(t('images.settings.base64DetailHelp')) diff --git a/src/mcp_feedback_enhanced/gui/widgets/styled_spinbox.py b/src/mcp_feedback_enhanced/gui/widgets/styled_spinbox.py new file mode 100644 index 0000000..d116619 --- /dev/null +++ b/src/mcp_feedback_enhanced/gui/widgets/styled_spinbox.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +自定義樣式的 QSpinBox +================== + +提供美觀的深色主題 QSpinBox,帶有自定義箭頭按鈕。 +""" + +from PySide6.QtWidgets import QSpinBox, QStyleOptionSpinBox, QStyle +from PySide6.QtCore import QRect, Qt +from PySide6.QtGui import QPainter, QPen, QBrush, QColor + + +class StyledSpinBox(QSpinBox): + """自定義樣式的 QSpinBox""" + + def __init__(self, parent=None): + super().__init__(parent) + self._setup_style() + + def _setup_style(self): + """設置基本樣式""" + self.setStyleSheet(""" + QSpinBox { + background-color: #3c3c3c; + border: 1px solid #555555; + border-radius: 6px; + padding: 4px 8px; + color: #ffffff; + font-size: 12px; + min-width: 100px; + min-height: 24px; + font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif; + } + + QSpinBox:focus { + border-color: #007acc; + background-color: #404040; + } + + QSpinBox:hover { + background-color: #404040; + border-color: #666666; + } + + QSpinBox::up-button { + subcontrol-origin: border; + subcontrol-position: top right; + width: 20px; + border-left: 1px solid #555555; + border-bottom: 1px solid #555555; + border-top-right-radius: 5px; + background-color: #4a4a4a; + } + + QSpinBox::up-button:hover { + background-color: #5a5a5a; + } + + QSpinBox::up-button:pressed { + background-color: #007acc; + } + + QSpinBox::down-button { + subcontrol-origin: border; + subcontrol-position: bottom right; + width: 20px; + border-left: 1px solid #555555; + border-top: 1px solid #555555; + border-bottom-right-radius: 5px; + background-color: #4a4a4a; + } + + QSpinBox::down-button:hover { + background-color: #5a5a5a; + } + + QSpinBox::down-button:pressed { + background-color: #007acc; + } + + QSpinBox::up-arrow { + width: 0px; + height: 0px; + } + + QSpinBox::down-arrow { + width: 0px; + height: 0px; + } + """) + + def paintEvent(self, event): + """重寫繪製事件以添加自定義箭頭""" + # 先調用父類的繪製方法 + super().paintEvent(event) + + # 創建畫筆 + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + # 獲取按鈕區域 + opt = QStyleOptionSpinBox() + self.initStyleOption(opt) + + # 計算按鈕位置 + button_width = 20 + widget_rect = self.rect() + + # 上箭頭按鈕區域 + up_rect = QRect( + widget_rect.width() - button_width, + 1, + button_width - 1, + widget_rect.height() // 2 - 1 + ) + + # 下箭頭按鈕區域 + down_rect = QRect( + widget_rect.width() - button_width, + widget_rect.height() // 2, + button_width - 1, + widget_rect.height() // 2 - 1 + ) + + # 繪製上箭頭 + self._draw_arrow(painter, up_rect, True) + + # 繪製下箭頭 + self._draw_arrow(painter, down_rect, False) + + def _draw_arrow(self, painter: QPainter, rect: QRect, is_up: bool): + """繪製箭頭""" + # 設置畫筆 + pen = QPen(QColor("#cccccc"), 1) + painter.setPen(pen) + painter.setBrush(QBrush(QColor("#cccccc"))) + + # 計算箭頭位置 + center_x = rect.center().x() + center_y = rect.center().y() + arrow_size = 4 + + if is_up: + # 上箭頭:▲ + points = [ + (center_x, center_y - arrow_size // 2), # 頂點 + (center_x - arrow_size, center_y + arrow_size // 2), # 左下 + (center_x + arrow_size, center_y + arrow_size // 2) # 右下 + ] + else: + # 下箭頭:▼ + points = [ + (center_x, center_y + arrow_size // 2), # 底點 + (center_x - arrow_size, center_y - arrow_size // 2), # 左上 + (center_x + arrow_size, center_y - arrow_size // 2) # 右上 + ] + + # 繪製三角形 + from PySide6.QtCore import QPoint + triangle = [QPoint(x, y) for x, y in points] + painter.drawPolygon(triangle) diff --git a/src/mcp_feedback_enhanced/gui/widgets/timeout_widget.py b/src/mcp_feedback_enhanced/gui/widgets/timeout_widget.py new file mode 100644 index 0000000..04cbd1f --- /dev/null +++ b/src/mcp_feedback_enhanced/gui/widgets/timeout_widget.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +超時控制組件 +============ + +提供超時設置和倒數計時器顯示功能。 +""" + +from PySide6.QtWidgets import ( + QWidget, QHBoxLayout, QVBoxLayout, QLabel, + QSpinBox, QPushButton, QFrame +) +from PySide6.QtCore import Signal, QTimer, Qt +from PySide6.QtGui import QFont + +from .switch import SwitchWidget +from ...i18n import t +from ...debug import gui_debug_log as debug_log + + +class TimeoutWidget(QWidget): + """超時控制組件""" + + # 信號 + timeout_occurred = Signal() # 超時發生 + settings_changed = Signal(bool, int) # 設置變更 (enabled, timeout_seconds) + + def __init__(self, parent=None): + super().__init__(parent) + self.timeout_enabled = False + self.timeout_seconds = 600 # 預設 10 分鐘 + self.remaining_seconds = 0 + + # 計時器 + self.countdown_timer = QTimer() + self.countdown_timer.timeout.connect(self._update_countdown) + + self._setup_ui() + self._connect_signals() + + debug_log("超時控制組件初始化完成") + + def _setup_ui(self): + """設置用戶介面""" + # 主布局 + main_layout = QHBoxLayout(self) + main_layout.setContentsMargins(8, 4, 8, 4) + main_layout.setSpacing(12) + + # 超時開關區域 + switch_layout = QHBoxLayout() + switch_layout.setSpacing(8) + + self.timeout_label = QLabel(t('timeout.enable')) + self.timeout_label.setStyleSheet("color: #cccccc; font-size: 12px;") + switch_layout.addWidget(self.timeout_label) + + self.timeout_switch = SwitchWidget() + self.timeout_switch.setToolTip(t('timeout.enableTooltip')) + switch_layout.addWidget(self.timeout_switch) + + main_layout.addLayout(switch_layout) + + # 分隔線 + separator = QFrame() + separator.setFrameShape(QFrame.VLine) + separator.setFrameShadow(QFrame.Sunken) + separator.setStyleSheet("color: #464647;") + main_layout.addWidget(separator) + + # 超時時間設置區域 + time_layout = QHBoxLayout() + time_layout.setSpacing(8) + + self.time_label = QLabel(t('timeout.duration.label')) + self.time_label.setStyleSheet("color: #cccccc; font-size: 12px;") + time_layout.addWidget(self.time_label) + + self.time_spinbox = QSpinBox() + self.time_spinbox.setRange(30, 7200) # 30秒到2小時 + self.time_spinbox.setValue(600) # 預設10分鐘 + self.time_spinbox.setSuffix(" " + t('timeout.seconds')) + # 應用自定義樣式 + style = self._get_spinbox_style(False) + self.time_spinbox.setStyleSheet(style) + debug_log("QSpinBox 樣式已應用") + time_layout.addWidget(self.time_spinbox) + + main_layout.addLayout(time_layout) + + # 分隔線 + separator2 = QFrame() + separator2.setFrameShape(QFrame.VLine) + separator2.setFrameShadow(QFrame.Sunken) + separator2.setStyleSheet("color: #464647;") + main_layout.addWidget(separator2) + + # 倒數計時器顯示區域 + countdown_layout = QHBoxLayout() + countdown_layout.setSpacing(8) + + self.countdown_label = QLabel(t('timeout.remaining')) + self.countdown_label.setStyleSheet("color: #cccccc; font-size: 12px;") + countdown_layout.addWidget(self.countdown_label) + + self.countdown_display = QLabel("--:--") + self.countdown_display.setStyleSheet(""" + color: #ffa500; + font-size: 14px; + font-weight: bold; + font-family: 'Consolas', 'Monaco', monospace; + min-width: 50px; + """) + countdown_layout.addWidget(self.countdown_display) + + main_layout.addLayout(countdown_layout) + + # 彈性空間 + main_layout.addStretch() + + # 初始狀態:隱藏倒數計時器 + self._update_visibility() + + def _connect_signals(self): + """連接信號""" + self.timeout_switch.toggled.connect(self._on_timeout_enabled_changed) + self.time_spinbox.valueChanged.connect(self._on_timeout_duration_changed) + + def _on_timeout_enabled_changed(self, enabled: bool): + """超時啟用狀態變更""" + self.timeout_enabled = enabled + self._update_visibility() + + if enabled: + self.start_countdown() + else: + self.stop_countdown() + + self.settings_changed.emit(enabled, self.timeout_seconds) + debug_log(f"超時功能已{'啟用' if enabled else '停用'}") + + def _on_timeout_duration_changed(self, seconds: int): + """超時時間變更""" + self.timeout_seconds = seconds + + # 如果正在倒數,重新開始 + if self.timeout_enabled and self.countdown_timer.isActive(): + self.start_countdown() + + self.settings_changed.emit(self.timeout_enabled, seconds) + debug_log(f"超時時間設置為 {seconds} 秒") + + def _update_visibility(self): + """更新組件可見性""" + # 倒數計時器只在啟用超時時顯示 + self.countdown_label.setVisible(self.timeout_enabled) + self.countdown_display.setVisible(self.timeout_enabled) + + # 時間設置在啟用時更明顯 + style = self._get_spinbox_style(self.timeout_enabled) + self.time_spinbox.setStyleSheet(style) + debug_log(f"QSpinBox 樣式已更新 (啟用: {self.timeout_enabled})") + + def start_countdown(self): + """開始倒數計時""" + if not self.timeout_enabled: + return + + self.remaining_seconds = self.timeout_seconds + self.countdown_timer.start(1000) # 每秒更新 + self._update_countdown_display() + debug_log(f"開始倒數計時:{self.timeout_seconds} 秒") + + def stop_countdown(self): + """停止倒數計時""" + self.countdown_timer.stop() + self.countdown_display.setText("--:--") + debug_log("倒數計時已停止") + + def _update_countdown(self): + """更新倒數計時""" + self.remaining_seconds -= 1 + self._update_countdown_display() + + if self.remaining_seconds <= 0: + self.countdown_timer.stop() + self.timeout_occurred.emit() + debug_log("倒數計時結束,觸發超時事件") + + def _update_countdown_display(self): + """更新倒數顯示""" + if self.remaining_seconds <= 0: + self.countdown_display.setText("00:00") + self.countdown_display.setStyleSheet(""" + color: #ff4444; + font-size: 14px; + font-weight: bold; + font-family: 'Consolas', 'Monaco', monospace; + min-width: 50px; + """) + else: + minutes = self.remaining_seconds // 60 + seconds = self.remaining_seconds % 60 + time_text = f"{minutes:02d}:{seconds:02d}" + self.countdown_display.setText(time_text) + + # 根據剩餘時間調整顏色 + if self.remaining_seconds <= 60: # 最後1分鐘 + color = "#ff4444" # 紅色 + elif self.remaining_seconds <= 300: # 最後5分鐘 + color = "#ffaa00" # 橙色 + else: + color = "#ffa500" # 黃色 + + self.countdown_display.setStyleSheet(f""" + color: {color}; + font-size: 14px; + font-weight: bold; + font-family: 'Consolas', 'Monaco', monospace; + min-width: 50px; + """) + + def set_timeout_settings(self, enabled: bool, seconds: int): + """設置超時參數""" + self.timeout_switch.setChecked(enabled) + self.time_spinbox.setValue(seconds) + self.timeout_enabled = enabled + self.timeout_seconds = seconds + self._update_visibility() + + def get_timeout_settings(self) -> tuple[bool, int]: + """獲取超時設置""" + return self.timeout_enabled, self.timeout_seconds + + def update_texts(self): + """更新界面文字(用於語言切換)""" + self.timeout_label.setText(t('timeout.enable')) + self.time_label.setText(t('timeout.duration.label')) + self.countdown_label.setText(t('timeout.remaining')) + self.timeout_switch.setToolTip(t('timeout.enableTooltip')) + self.time_spinbox.setSuffix(" " + t('timeout.seconds')) + + def _get_spinbox_style(self, enabled: bool) -> str: + """獲取 QSpinBox 的樣式字符串""" + border_color = "#007acc" if enabled else "#555555" + focus_color = "#0099ff" if enabled else "#007acc" + + return f""" + QSpinBox {{ + background-color: #3c3c3c; + border: 1px solid {border_color}; + border-radius: 6px; + padding: 4px 8px; + color: #ffffff; + font-size: 12px; + min-width: 90px; + min-height: 24px; + }} + + QSpinBox:focus {{ + border-color: {focus_color}; + background-color: #404040; + }} + + QSpinBox:hover {{ + background-color: #404040; + border-color: #666666; + }} + + QSpinBox::up-button {{ + subcontrol-origin: border; + subcontrol-position: top right; + width: 18px; + border-left: 1px solid #555555; + border-bottom: 1px solid #555555; + border-top-right-radius: 5px; + background-color: #4a4a4a; + }} + + QSpinBox::up-button:hover {{ + background-color: #5a5a5a; + }} + + QSpinBox::up-button:pressed {{ + background-color: #007acc; + }} + + QSpinBox::down-button {{ + subcontrol-origin: border; + subcontrol-position: bottom right; + width: 18px; + border-left: 1px solid #555555; + border-top: 1px solid #555555; + border-bottom-right-radius: 5px; + background-color: #4a4a4a; + }} + + QSpinBox::down-button:hover {{ + background-color: #5a5a5a; + }} + + QSpinBox::down-button:pressed {{ + background-color: #007acc; + }} + + QSpinBox::up-arrow {{ + width: 0px; + height: 0px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 6px solid #cccccc; + }} + + QSpinBox::down-arrow {{ + width: 0px; + height: 0px; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 6px solid #cccccc; + }} + """ diff --git a/src/mcp_feedback_enhanced/gui/window/config_manager.py b/src/mcp_feedback_enhanced/gui/window/config_manager.py index 8da869c..ce73299 100644 --- a/src/mcp_feedback_enhanced/gui/window/config_manager.py +++ b/src/mcp_feedback_enhanced/gui/window/config_manager.py @@ -152,6 +152,10 @@ class ConfigManager: def set_image_size_limit(self, size_bytes: int) -> None: """設置圖片大小限制(bytes),0 表示無限制""" + # 處理 None 值 + if size_bytes is None: + size_bytes = 0 + self.update_partial_config({'image_size_limit': size_bytes}) size_mb = size_bytes / (1024 * 1024) if size_bytes > 0 else 0 debug_log(f"圖片大小限制設置: {'無限制' if size_bytes == 0 else f'{size_mb:.1f}MB'}") @@ -165,6 +169,36 @@ class ConfigManager: self.update_partial_config({'enable_base64_detail': enabled}) debug_log(f"Base64 詳細模式設置: {'啟用' if enabled else '停用'}") + def get_timeout_enabled(self) -> bool: + """獲取是否啟用超時自動關閉""" + return self.get('timeout_enabled', False) + + def set_timeout_enabled(self, enabled: bool) -> None: + """設置是否啟用超時自動關閉""" + self.update_partial_config({'timeout_enabled': enabled}) + debug_log(f"超時自動關閉設置: {'啟用' if enabled else '停用'}") + + def get_timeout_duration(self) -> int: + """獲取超時時間(秒)""" + return self.get('timeout_duration', 600) # 預設10分鐘 + + def set_timeout_duration(self, seconds: int) -> None: + """設置超時時間(秒)""" + self.update_partial_config({'timeout_duration': seconds}) + debug_log(f"超時時間設置: {seconds} 秒") + + def get_timeout_settings(self) -> tuple[bool, int]: + """獲取超時設置(啟用狀態, 超時時間)""" + return self.get_timeout_enabled(), self.get_timeout_duration() + + def set_timeout_settings(self, enabled: bool, seconds: int) -> None: + """設置超時設置""" + self.update_partial_config({ + 'timeout_enabled': enabled, + 'timeout_duration': seconds + }) + debug_log(f"超時設置: {'啟用' if enabled else '停用'}, {seconds} 秒") + def reset_settings(self) -> None: """重置所有設定到預設值""" try: diff --git a/src/mcp_feedback_enhanced/gui/window/feedback_window.py b/src/mcp_feedback_enhanced/gui/window/feedback_window.py index e412690..5de4dca 100644 --- a/src/mcp_feedback_enhanced/gui/window/feedback_window.py +++ b/src/mcp_feedback_enhanced/gui/window/feedback_window.py @@ -24,13 +24,15 @@ from ...debug import gui_debug_log as debug_log class FeedbackWindow(QMainWindow): """回饋收集主窗口(重構版)""" language_changed = Signal() - - def __init__(self, project_dir: str, summary: str): + timeout_occurred = Signal() # 超時發生信號 + + def __init__(self, project_dir: str, summary: str, timeout_seconds: int = None): super().__init__() self.project_dir = project_dir self.summary = summary self.result = None self.i18n = get_i18n_manager() + self.mcp_timeout_seconds = timeout_seconds # MCP 傳入的超時時間 # 初始化組件 self.config_manager = ConfigManager() @@ -55,6 +57,9 @@ class FeedbackWindow(QMainWindow): self._connect_signals() debug_log("主窗口初始化完成") + + # 如果啟用了超時,自動開始倒數計時 + self.start_timeout_if_enabled() def _setup_ui(self) -> None: """設置用戶介面""" @@ -88,9 +93,72 @@ class FeedbackWindow(QMainWindow): def _create_project_header(self, layout: QVBoxLayout) -> None: """創建專案目錄頭部信息""" + # 創建水平布局來放置專案目錄和倒數計時器 + header_layout = QHBoxLayout() + self.project_label = QLabel(f"{t('app.projectDirectory')}: {self.project_dir}") self.project_label.setStyleSheet("color: #9e9e9e; font-size: 12px; padding: 4px 0;") - layout.addWidget(self.project_label) + header_layout.addWidget(self.project_label) + + # 添加彈性空間 + header_layout.addStretch() + + # 添加倒數計時器顯示(僅顯示部分) + self._create_countdown_display(header_layout) + + # 將水平布局添加到主布局 + header_widget = QWidget() + header_widget.setLayout(header_layout) + layout.addWidget(header_widget) + + def _create_countdown_display(self, layout: QHBoxLayout) -> None: + """創建倒數計時器顯示組件(僅顯示)""" + # 倒數計時器標籤 + self.countdown_label = QLabel(t('timeout.remaining')) + self.countdown_label.setStyleSheet("color: #cccccc; font-size: 12px;") + self.countdown_label.setVisible(False) # 預設隱藏 + layout.addWidget(self.countdown_label) + + # 倒數計時器顯示 + self.countdown_display = QLabel("--:--") + self.countdown_display.setStyleSheet(""" + color: #ffa500; + font-size: 14px; + font-weight: bold; + font-family: 'Consolas', 'Monaco', monospace; + min-width: 50px; + margin-left: 8px; + """) + self.countdown_display.setVisible(False) # 預設隱藏 + layout.addWidget(self.countdown_display) + + # 初始化超時控制邏輯 + self._init_timeout_logic() + + def _init_timeout_logic(self) -> None: + """初始化超時控制邏輯""" + # 載入保存的超時設置 + timeout_enabled, timeout_duration = self.config_manager.get_timeout_settings() + + # 如果有 MCP 超時參數,且用戶設置的時間大於 MCP 時間,則使用 MCP 時間 + if self.mcp_timeout_seconds is not None: + if timeout_duration > self.mcp_timeout_seconds: + timeout_duration = self.mcp_timeout_seconds + debug_log(f"用戶設置的超時時間 ({timeout_duration}s) 大於 MCP 超時時間 ({self.mcp_timeout_seconds}s),使用 MCP 時間") + + # 保存超時設置 + self.timeout_enabled = timeout_enabled + self.timeout_duration = timeout_duration + self.remaining_seconds = 0 + + # 創建計時器 + self.countdown_timer = QTimer() + self.countdown_timer.timeout.connect(self._update_countdown) + + # 更新顯示狀態 + self._update_countdown_visibility() + + def _create_tab_area(self, layout: QVBoxLayout) -> None: """創建分頁區域""" @@ -169,7 +237,10 @@ class FeedbackWindow(QMainWindow): # 創建分頁 self.tab_manager.create_tabs() - + + # 連接分頁信號 + self.tab_manager.connect_signals(self) + # 將分頁組件放入滾動區域 scroll_area.setWidget(self.tab_widget) @@ -457,16 +528,139 @@ class FeedbackWindow(QMainWindow): debug_log("強制關閉視窗(超時)") self.result = "" self.close() + + def _on_timeout_occurred(self) -> None: + """處理超時事件""" + debug_log("用戶設置的超時時間已到,自動關閉視窗") + self._timeout_occurred = True + self.timeout_occurred.emit() + self.force_close() + + def start_timeout_if_enabled(self) -> None: + """如果啟用了超時,自動開始倒數計時""" + if hasattr(self, 'tab_manager') and self.tab_manager: + timeout_widget = self.tab_manager.get_timeout_widget() + if timeout_widget: + enabled, _ = timeout_widget.get_timeout_settings() + if enabled: + timeout_widget.start_countdown() + debug_log("窗口顯示時自動開始倒數計時") + + def _on_timeout_settings_changed(self, enabled: bool, seconds: int) -> None: + """處理超時設置變更(從設置頁籤觸發)""" + # 檢查是否超過 MCP 超時限制 + if self.mcp_timeout_seconds is not None and seconds > self.mcp_timeout_seconds: + debug_log(f"用戶設置的超時時間 ({seconds}s) 超過 MCP 限制 ({self.mcp_timeout_seconds}s),調整為 MCP 時間") + seconds = self.mcp_timeout_seconds + + # 更新內部狀態 + self.timeout_enabled = enabled + self.timeout_duration = seconds + + # 保存設置 + self.config_manager.set_timeout_settings(enabled, seconds) + debug_log(f"超時設置已更新: {'啟用' if enabled else '停用'}, {seconds} 秒") + + # 更新倒數計時器顯示 + self._update_countdown_visibility() + + # 重新開始倒數計時 + if enabled: + self.start_countdown() + else: + self.stop_countdown() + + def start_timeout_if_enabled(self) -> None: + """如果啟用了超時,開始倒數計時""" + if self.timeout_enabled: + self.start_countdown() + debug_log("超時倒數計時已開始") + + def stop_timeout(self) -> None: + """停止超時倒數計時""" + self.stop_countdown() + debug_log("超時倒數計時已停止") + + def start_countdown(self) -> None: + """開始倒數計時""" + if not self.timeout_enabled: + return + + self.remaining_seconds = self.timeout_duration + self.countdown_timer.start(1000) # 每秒更新 + self._update_countdown_display() + debug_log(f"開始倒數計時:{self.timeout_duration} 秒") + + def stop_countdown(self) -> None: + """停止倒數計時""" + self.countdown_timer.stop() + self.countdown_display.setText("--:--") + debug_log("倒數計時已停止") + + def _update_countdown(self) -> None: + """更新倒數計時""" + self.remaining_seconds -= 1 + self._update_countdown_display() + + if self.remaining_seconds <= 0: + self.countdown_timer.stop() + self._on_timeout_occurred() + debug_log("倒數計時結束,觸發超時事件") + + def _update_countdown_display(self) -> None: + """更新倒數顯示""" + if self.remaining_seconds <= 0: + self.countdown_display.setText("00:00") + self.countdown_display.setStyleSheet(""" + color: #ff4444; + font-size: 14px; + font-weight: bold; + font-family: 'Consolas', 'Monaco', monospace; + min-width: 50px; + margin-left: 8px; + """) + else: + minutes = self.remaining_seconds // 60 + seconds = self.remaining_seconds % 60 + time_text = f"{minutes:02d}:{seconds:02d}" + self.countdown_display.setText(time_text) + + # 根據剩餘時間調整顏色 + if self.remaining_seconds <= 60: # 最後1分鐘 + color = "#ff4444" # 紅色 + elif self.remaining_seconds <= 300: # 最後5分鐘 + color = "#ffaa00" # 橙色 + else: + color = "#ffa500" # 黃色 + + self.countdown_display.setStyleSheet(f""" + color: {color}; + font-size: 14px; + font-weight: bold; + font-family: 'Consolas', 'Monaco', monospace; + min-width: 50px; + margin-left: 8px; + """) + + def _update_countdown_visibility(self) -> None: + """更新倒數計時器可見性""" + # 倒數計時器只在啟用超時時顯示 + self.countdown_label.setVisible(self.timeout_enabled) + self.countdown_display.setVisible(self.timeout_enabled) def _refresh_ui_texts(self) -> None: """刷新界面文字""" self.setWindowTitle(t('app.title')) self.project_label.setText(f"{t('app.projectDirectory')}: {self.project_dir}") - + # 更新按鈕文字 self.submit_button.setText(t('buttons.submit')) self.cancel_button.setText(t('buttons.cancel')) - + + # 更新倒數計時器文字 + if hasattr(self, 'countdown_label'): + self.countdown_label.setText(t('timeout.remaining')) + # 更新分頁文字 self.tab_manager.update_tab_texts() diff --git a/src/mcp_feedback_enhanced/gui/window/tab_manager.py b/src/mcp_feedback_enhanced/gui/window/tab_manager.py index 3fa64a9..559fb61 100644 --- a/src/mcp_feedback_enhanced/gui/window/tab_manager.py +++ b/src/mcp_feedback_enhanced/gui/window/tab_manager.py @@ -327,6 +327,8 @@ class TabManager: self.settings_tab.layout_change_requested.connect(parent._on_layout_change_requested) if hasattr(parent, '_on_reset_settings_requested'): self.settings_tab.reset_requested.connect(parent._on_reset_settings_requested) + if hasattr(parent, '_on_timeout_settings_changed'): + self.settings_tab.timeout_settings_changed.connect(parent._on_timeout_settings_changed) # 連接回饋分頁的圖片貼上信號 if self.feedback_tab: diff --git a/src/mcp_feedback_enhanced/locales/en/translations.json b/src/mcp_feedback_enhanced/locales/en/translations.json index e18d941..3a886b6 100644 --- a/src/mcp_feedback_enhanced/locales/en/translations.json +++ b/src/mcp_feedback_enhanced/locales/en/translations.json @@ -130,6 +130,22 @@ "errorMessage": "Error occurred while resetting settings: {error}" } }, + "timeout": { + "enable": "Auto Close", + "enableTooltip": "When enabled, the interface will automatically close after the specified time", + "duration": { + "label": "Timeout Duration", + "description": "Set the auto-close time (30 seconds - 2 hours)" + }, + "seconds": "seconds", + "remaining": "Time Remaining", + "expired": "Time Expired", + "autoCloseMessage": "Interface will automatically close in {seconds} seconds", + "settings": { + "title": "Timeout Settings", + "description": "When enabled, the interface will automatically close after the specified time. The countdown timer will be displayed in the header area." + } + }, "buttons": { "submit": "Submit Feedback", "cancel": "Cancel", diff --git a/src/mcp_feedback_enhanced/locales/zh-CN/translations.json b/src/mcp_feedback_enhanced/locales/zh-CN/translations.json index 8005fd5..3147edf 100644 --- a/src/mcp_feedback_enhanced/locales/zh-CN/translations.json +++ b/src/mcp_feedback_enhanced/locales/zh-CN/translations.json @@ -110,6 +110,22 @@ "errorMessage": "重置设置时发生错误:{error}" } }, + "timeout": { + "enable": "自动关闭", + "enableTooltip": "启用后将在指定时间后自动关闭界面", + "duration": { + "label": "超时时间", + "description": "设置自动关闭的时间(30秒 - 2小时)" + }, + "seconds": "秒", + "remaining": "剩余时间", + "expired": "时间已到", + "autoCloseMessage": "界面将在 {seconds} 秒后自动关闭", + "settings": { + "title": "超时设置", + "description": "启用后,界面将在指定时间后自动关闭。倒数计时器会显示在顶部区域。" + } + }, "buttons": { "submit": "提交反馈", "cancel": "取消", diff --git a/src/mcp_feedback_enhanced/locales/zh-TW/translations.json b/src/mcp_feedback_enhanced/locales/zh-TW/translations.json index db451e0..d479f00 100644 --- a/src/mcp_feedback_enhanced/locales/zh-TW/translations.json +++ b/src/mcp_feedback_enhanced/locales/zh-TW/translations.json @@ -96,6 +96,22 @@ "sizeLimitExceeded": "圖片 {filename} 大小為 {size},超過 {limit} 限制!", "sizeLimitExceededAdvice": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。" }, + "timeout": { + "enable": "自動關閉", + "enableTooltip": "啟用後將在指定時間後自動關閉介面", + "duration": { + "label": "超時時間", + "description": "設置自動關閉的時間(30秒 - 2小時)" + }, + "seconds": "秒", + "remaining": "剩餘時間", + "expired": "時間已到", + "autoCloseMessage": "介面將在 {seconds} 秒後自動關閉", + "settings": { + "title": "超時設置", + "description": "啟用後,介面將在指定時間後自動關閉。倒數計時器會顯示在頂部區域。" + } + }, "settings": { "title": "應用設置", "language": { diff --git a/src/mcp_feedback_enhanced/server.py b/src/mcp_feedback_enhanced/server.py index 466bf15..fc598ee 100644 --- a/src/mcp_feedback_enhanced/server.py +++ b/src/mcp_feedback_enhanced/server.py @@ -519,8 +519,8 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in try: # 使用新的 web 模組 - from .web import launch_web_feedback_ui - + from .web import launch_web_feedback_ui, stop_web_ui + # 傳遞 timeout 參數給 Web UI return await launch_web_feedback_ui(project_dir, summary, timeout) except ImportError as e: @@ -532,6 +532,14 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in } except TimeoutError as e: debug_log(f"Web UI 超時: {e}") + # 超時時確保停止 Web 服務器 + try: + from .web import stop_web_ui + stop_web_ui() + debug_log("Web UI 服務器已因超時而停止") + except Exception as stop_error: + debug_log(f"停止 Web UI 服務器時發生錯誤: {stop_error}") + return { "command_logs": "", "interactive_feedback": f"回饋收集超時({timeout}秒),介面已自動關閉。", @@ -540,6 +548,14 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in except Exception as e: error_msg = f"Web UI 錯誤: {e}" debug_log(f"❌ {error_msg}") + # 發生錯誤時也要停止 Web 服務器 + try: + from .web import stop_web_ui + stop_web_ui() + debug_log("Web UI 服務器已因錯誤而停止") + except Exception as stop_error: + debug_log(f"停止 Web UI 服務器時發生錯誤: {stop_error}") + return { "command_logs": "", "interactive_feedback": f"錯誤: {str(e)}", diff --git a/src/mcp_feedback_enhanced/web/locales/en/translation.json b/src/mcp_feedback_enhanced/web/locales/en/translation.json index 4f23b82..7f2e59f 100644 --- a/src/mcp_feedback_enhanced/web/locales/en/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/en/translation.json @@ -153,6 +153,22 @@ "timeoutDescription": "Due to prolonged inactivity, the session has timed out. The interface will automatically close in 3 seconds.", "closing": "Closing..." }, + "timeout": { + "enable": "Auto Close", + "enableTooltip": "When enabled, the interface will automatically close after the specified time", + "duration": { + "label": "Timeout Duration", + "description": "Set the auto-close time (30 seconds - 2 hours)" + }, + "seconds": "seconds", + "remaining": "Time Remaining", + "expired": "⏰ Time expired, interface will close automatically", + "autoCloseMessage": "Interface will automatically close in {seconds} seconds", + "settings": { + "title": "Timeout Settings", + "description": "When enabled, the interface will automatically close after the specified time. The countdown timer will be displayed in the header area." + } + }, "dynamic": { "aiSummary": "Test Web UI Functionality\n\n🎯 **Test Items:**\n- Web UI server startup and operation\n- WebSocket real-time communication\n- Feedback submission functionality\n- Image upload and preview\n- Command execution functionality\n- Smart Ctrl+V image pasting\n- Multi-language interface functionality\n\n📋 **Test Steps:**\n1. Test image upload (drag-drop, file selection, clipboard)\n2. Press Ctrl+V in text box to test smart pasting\n3. Try switching languages (Traditional Chinese/Simplified Chinese/English)\n4. Test command execution functionality\n5. Submit feedback and images\n\nPlease test these features and provide feedback!", "terminalWelcome": "Welcome to Interactive Feedback Terminal\n========================================\nProject Directory: {sessionId}\nEnter commands and press Enter or click Execute button\nSupported commands: ls, dir, pwd, cat, type, etc.\n\n$ " 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 eebcde0..1d8305f 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-CN/translation.json @@ -153,6 +153,22 @@ "timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。", "closing": "正在关闭..." }, + "timeout": { + "enable": "自动关闭", + "enableTooltip": "启用后将在指定时间后自动关闭界面", + "duration": { + "label": "超时时间", + "description": "设置自动关闭的时间(30秒 - 2小时)" + }, + "seconds": "秒", + "remaining": "剩余时间", + "expired": "⏰ 时间已到,界面将自动关闭", + "autoCloseMessage": "界面将在 {seconds} 秒后自动关闭", + "settings": { + "title": "超时设置", + "description": "启用后,界面将在指定时间后自动关闭。倒数计时器会显示在顶部区域。" + } + }, "dynamic": { "aiSummary": "测试 Web UI 功能\n\n🎯 **功能测试项目:**\n- Web UI 服务器启动和运行\n- WebSocket 实时通讯\n- 反馈提交功能\n- 图片上传和预览\n- 命令执行功能\n- 智能 Ctrl+V 图片粘贴\n- 多语言界面功能\n\n📋 **测试步骤:**\n1. 测试图片上传(拖拽、选择文件、剪贴板)\n2. 在文本框内按 Ctrl+V 测试智能粘贴\n3. 尝试切换语言(繁中/简中/英文)\n4. 测试命令执行功能\n5. 提交反馈和图片\n\n请测试这些功能并提供反馈!", "terminalWelcome": "欢迎使用交互反馈终端\n========================================\n项目目录: {sessionId}\n输入命令后按 Enter 或点击执行按钮\n支持的命令: ls, dir, pwd, cat, type 等\n\n$ " 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 35a2954..7d4afa9 100644 --- a/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json +++ b/src/mcp_feedback_enhanced/web/locales/zh-TW/translation.json @@ -153,6 +153,22 @@ "timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。", "closing": "正在關閉..." }, + "timeout": { + "enable": "自動關閉", + "enableTooltip": "啟用後將在指定時間後自動關閉介面", + "duration": { + "label": "超時時間", + "description": "設置自動關閉的時間(30秒 - 2小時)" + }, + "seconds": "秒", + "remaining": "剩餘時間", + "expired": "⏰ 時間已到,介面將自動關閉", + "autoCloseMessage": "介面將在 {seconds} 秒後自動關閉", + "settings": { + "title": "超時設置", + "description": "啟用後,介面將在指定時間後自動關閉。倒數計時器會顯示在頂部區域。" + } + }, "dynamic": { "aiSummary": "測試 Web UI 功能\n\n🎯 **功能測試項目:**\n- Web UI 服務器啟動和運行\n- WebSocket 即時通訊\n- 回饋提交功能\n- 圖片上傳和預覽\n- 命令執行功能\n- 智能 Ctrl+V 圖片貼上\n- 多語言介面功能\n\n📋 **測試步驟:**\n1. 測試圖片上傳(拖拽、選擇檔案、剪貼簿)\n2. 在文字框內按 Ctrl+V 測試智能貼上\n3. 嘗試切換語言(繁中/簡中/英文)\n4. 測試命令執行功能\n5. 提交回饋和圖片\n\n請測試這些功能並提供回饋!", "terminalWelcome": "歡迎使用互動回饋終端\n========================================\n專案目錄: {sessionId}\n輸入命令後按 Enter 或點擊執行按鈕\n支援的命令: ls, dir, pwd, cat, type 等\n\n$ " diff --git a/src/mcp_feedback_enhanced/web/main.py b/src/mcp_feedback_enhanced/web/main.py index 6bc640a..c4c3b00 100644 --- a/src/mcp_feedback_enhanced/web/main.py +++ b/src/mcp_feedback_enhanced/web/main.py @@ -218,6 +218,10 @@ async def launch_web_feedback_ui(project_directory: str, summary: str, timeout: finally: # 清理會話(無論成功還是失敗) manager.remove_session(session_id) + # 如果沒有其他活躍會話,停止服務器 + if len(manager.sessions) == 0: + debug_log("沒有活躍會話,停止 Web UI 服務器") + stop_web_ui() def stop_web_ui(): @@ -236,21 +240,22 @@ if __name__ == "__main__": project_dir = os.getcwd() summary = "這是一個測試摘要,用於驗證 Web UI 功能。" - print(f"啟動 Web UI 測試...") - print(f"專案目錄: {project_dir}") - print("等待用戶回饋...") - + from ..debug import debug_log + debug_log(f"啟動 Web UI 測試...") + debug_log(f"專案目錄: {project_dir}") + debug_log("等待用戶回饋...") + result = await launch_web_feedback_ui(project_dir, summary) - - print("收到回饋結果:") - print(f"命令日誌: {result.get('logs', '')}") - print(f"互動回饋: {result.get('interactive_feedback', '')}") - print(f"圖片數量: {len(result.get('images', []))}") - + + debug_log("收到回饋結果:") + debug_log(f"命令日誌: {result.get('logs', '')}") + debug_log(f"互動回饋: {result.get('interactive_feedback', '')}") + debug_log(f"圖片數量: {len(result.get('images', []))}") + except KeyboardInterrupt: - print("\n用戶取消操作") + debug_log("\n用戶取消操作") except Exception as e: - print(f"錯誤: {e}") + debug_log(f"錯誤: {e}") finally: stop_web_ui() diff --git a/src/mcp_feedback_enhanced/web/routes/main_routes.py b/src/mcp_feedback_enhanced/web/routes/main_routes.py index 365d7af..0e0d2b3 100644 --- a/src/mcp_feedback_enhanced/web/routes/main_routes.py +++ b/src/mcp_feedback_enhanced/web/routes/main_routes.py @@ -198,6 +198,27 @@ async def handle_websocket_message(manager: 'WebUIManager', session, data: dict) command = data.get("command", "") if command.strip(): await session.run_command(command) - + + elif message_type == "user_timeout": + # 用戶設置的超時已到 + debug_log(f"收到用戶超時通知: {session.session_id}") + # 清理會話資源 + await session._cleanup_resources_on_timeout() + # 如果沒有其他活躍會話,停止服務器 + if len(manager.sessions) <= 1: # 當前會話即將被移除 + debug_log("用戶超時,沒有其他活躍會話,準備停止服務器") + # 延遲停止服務器,給前端時間關閉 + import asyncio + asyncio.create_task(_delayed_server_stop(manager)) + else: - debug_log(f"未知的消息類型: {message_type}") \ No newline at end of file + debug_log(f"未知的消息類型: {message_type}") + + +async def _delayed_server_stop(manager: 'WebUIManager'): + """延遲停止服務器""" + import asyncio + await asyncio.sleep(5) # 等待 5 秒讓前端有時間關閉 + from ..main import stop_web_ui + stop_web_ui() + debug_log("Web UI 服務器已因用戶超時而停止") \ No newline at end of file diff --git a/src/mcp_feedback_enhanced/web/static/js/app.js b/src/mcp_feedback_enhanced/web/static/js/app.js index a5728d8..a66e5e4 100644 --- a/src/mcp_feedback_enhanced/web/static/js/app.js +++ b/src/mcp_feedback_enhanced/web/static/js/app.js @@ -95,6 +95,13 @@ class FeedbackApp { // 圖片設定 this.imageSizeLimit = 0; // 0 表示無限制 this.enableBase64Detail = false; + + // 超時設定 + this.timeoutEnabled = false; + this.timeoutDuration = 600; // 預設 10 分鐘 + this.timeoutTimer = null; + this.countdownTimer = null; + this.remainingSeconds = 0; // 立即檢查 DOM 狀態並初始化 if (document.readyState === 'loading') { @@ -133,13 +140,19 @@ class FeedbackApp { // 載入設定(使用 await) await this.loadSettings(); - + // 初始化命令終端 this.initCommandTerminal(); - + // 確保合併模式狀態正確 this.applyCombinedModeState(); - + + // 初始化超時控制 + this.setupTimeoutControl(); + + // 如果啟用了超時,自動開始倒數計時(在設置載入後) + this.startTimeoutIfEnabled(); + console.log('FeedbackApp 初始化完成'); } @@ -1122,6 +1135,22 @@ class FeedbackApp { this.enableBase64Detail = false; // 預設關閉 } + // 載入超時設定 + if (settings.timeoutEnabled !== undefined) { + this.timeoutEnabled = settings.timeoutEnabled; + } else { + this.timeoutEnabled = false; // 預設關閉 + } + + if (settings.timeoutDuration !== undefined) { + this.timeoutDuration = settings.timeoutDuration; + } else { + this.timeoutDuration = 600; // 預設 10 分鐘 + } + + // 更新超時 UI + this.updateTimeoutUI(); + // 同步圖片設定到 UI this.syncImageSettings(); @@ -1213,6 +1242,8 @@ $ `; this.autoClose = true; this.imageSizeLimit = 0; this.enableBase64Detail = false; + this.timeoutEnabled = false; + this.timeoutDuration = 600; // 更新佈局模式單選按鈕狀態 const layoutRadios = document.querySelectorAll('input[name="layoutMode"]'); @@ -1229,6 +1260,10 @@ $ `; // 同步圖片設定到 UI this.syncImageSettings(); + // 更新超時 UI + this.updateTimeoutUI(); + this.stopTimeout(); + // 確保語言選擇器與當前語言同步 this.syncLanguageSelector(); @@ -1307,6 +1342,210 @@ $ `; }, 3000); } + setupTimeoutControl() { + // 設置超時開關監聽器 + const timeoutToggle = document.getElementById('timeoutToggle'); + if (timeoutToggle) { + timeoutToggle.addEventListener('click', () => { + this.toggleTimeout(); + }); + } + + // 設置超時時間輸入監聽器 + const timeoutDuration = document.getElementById('timeoutDuration'); + if (timeoutDuration) { + timeoutDuration.addEventListener('change', (e) => { + this.setTimeoutDuration(parseInt(e.target.value)); + }); + } + + // 更新界面狀態 + this.updateTimeoutUI(); + } + + startTimeoutIfEnabled() { + // 如果啟用了超時,自動開始倒數計時 + if (this.timeoutEnabled) { + this.startTimeout(); + console.log('頁面載入時自動開始倒數計時'); + } + } + + toggleTimeout() { + this.timeoutEnabled = !this.timeoutEnabled; + this.updateTimeoutUI(); + this.saveSettings(); + + if (this.timeoutEnabled) { + this.startTimeout(); + } else { + this.stopTimeout(); + } + + console.log('超時功能已', this.timeoutEnabled ? '啟用' : '停用'); + } + + setTimeoutDuration(seconds) { + if (seconds >= 30 && seconds <= 7200) { + this.timeoutDuration = seconds; + this.saveSettings(); + + // 如果正在倒數,重新開始 + if (this.timeoutEnabled && this.timeoutTimer) { + this.startTimeout(); + } + + console.log('超時時間設置為', seconds, '秒'); + } + } + + updateTimeoutUI() { + const timeoutToggle = document.getElementById('timeoutToggle'); + const timeoutDuration = document.getElementById('timeoutDuration'); + const countdownDisplay = document.getElementById('countdownDisplay'); + + if (timeoutToggle) { + timeoutToggle.classList.toggle('active', this.timeoutEnabled); + } + + if (timeoutDuration) { + timeoutDuration.value = this.timeoutDuration; + } + + if (countdownDisplay) { + countdownDisplay.style.display = this.timeoutEnabled ? 'flex' : 'none'; + } + } + + startTimeout() { + this.stopTimeout(); // 先停止現有的計時器 + + this.remainingSeconds = this.timeoutDuration; + + // 開始主要的超時計時器 + this.timeoutTimer = setTimeout(() => { + this.handleTimeout(); + }, this.timeoutDuration * 1000); + + // 開始倒數顯示計時器 + this.countdownTimer = setInterval(() => { + this.updateCountdownDisplay(); + }, 1000); + + this.updateCountdownDisplay(); + console.log('開始倒數計時:', this.timeoutDuration, '秒'); + } + + stopTimeout() { + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer); + this.timeoutTimer = null; + } + + if (this.countdownTimer) { + clearInterval(this.countdownTimer); + this.countdownTimer = null; + } + + const countdownTimer = document.getElementById('countdownTimer'); + if (countdownTimer) { + countdownTimer.textContent = '--:--'; + countdownTimer.className = 'countdown-timer'; + } + + console.log('倒數計時已停止'); + } + + updateCountdownDisplay() { + this.remainingSeconds--; + + const countdownTimer = document.getElementById('countdownTimer'); + if (countdownTimer) { + if (this.remainingSeconds <= 0) { + countdownTimer.textContent = '00:00'; + countdownTimer.className = 'countdown-timer danger'; + } else { + const minutes = Math.floor(this.remainingSeconds / 60); + const seconds = this.remainingSeconds % 60; + const timeText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + countdownTimer.textContent = timeText; + + // 根據剩餘時間調整樣式 + if (this.remainingSeconds <= 60) { + countdownTimer.className = 'countdown-timer danger'; + } else if (this.remainingSeconds <= 300) { + countdownTimer.className = 'countdown-timer warning'; + } else { + countdownTimer.className = 'countdown-timer'; + } + } + } + + if (this.remainingSeconds <= 0) { + clearInterval(this.countdownTimer); + this.countdownTimer = null; + } + } + + handleTimeout() { + console.log('用戶設置的超時時間已到,自動關閉介面'); + + // 通知後端用戶超時 + this.notifyUserTimeout(); + + // 顯示超時訊息 + const timeoutMessage = window.i18nManager ? + window.i18nManager.t('timeout.expired', '⏰ 時間已到,介面將自動關閉') : + '⏰ 時間已到,介面將自動關閉'; + + this.showMessage(timeoutMessage, 'warning'); + + // 禁用所有互動元素 + this.disableAllInputs(); + + // 3秒後自動關閉頁面 + setTimeout(() => { + try { + window.close(); + } catch (e) { + console.log('無法關閉視窗,重新載入頁面'); + window.location.reload(); + } + }, 3000); + } + + notifyUserTimeout() { + // 通過 WebSocket 通知後端用戶設置的超時已到 + if (this.websocket && this.isConnected) { + try { + this.websocket.send(JSON.stringify({ + type: 'user_timeout', + message: '用戶設置的超時時間已到' + })); + console.log('已通知後端用戶超時'); + } catch (error) { + console.log('通知後端超時失敗:', error); + } + } + } + + disableAllInputs() { + // 禁用所有輸入元素 + const inputs = document.querySelectorAll('input, textarea, button, select'); + inputs.forEach(input => { + input.disabled = true; + }); + + // 禁用超時控制 + const timeoutToggle = document.getElementById('timeoutToggle'); + if (timeoutToggle) { + timeoutToggle.style.pointerEvents = 'none'; + timeoutToggle.style.opacity = '0.5'; + } + + console.log('所有輸入元素已禁用'); + } + async saveSettings() { try { const settings = { @@ -1314,6 +1553,8 @@ $ `; autoClose: this.autoClose, imageSizeLimit: this.imageSizeLimit, enableBase64Detail: this.enableBase64Detail, + timeoutEnabled: this.timeoutEnabled, + timeoutDuration: this.timeoutDuration, language: window.i18nManager?.currentLanguage || 'zh-TW', activeTab: localStorage.getItem('activeTab'), lastSaved: new Date().toISOString() diff --git a/src/mcp_feedback_enhanced/web/templates/feedback.html b/src/mcp_feedback_enhanced/web/templates/feedback.html index 53af572..eba17ff 100644 --- a/src/mcp_feedback_enhanced/web/templates/feedback.html +++ b/src/mcp_feedback_enhanced/web/templates/feedback.html @@ -67,6 +67,12 @@ padding: 0 20px; } + .header-left { + display: flex; + align-items: center; + gap: 16px; + } + .title { font-size: 24px; font-weight: bold; @@ -79,6 +85,111 @@ font-size: 14px; } + /* 倒數計時器顯示樣式 */ + .countdown-display { + display: flex; + align-items: center; + gap: 6px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 4px 8px; + } + + .countdown-label { + color: var(--text-secondary); + font-size: 11px; + } + + .countdown-timer { + color: var(--warning-color); + font-size: 13px; + font-weight: bold; + font-family: 'Consolas', 'Monaco', monospace; + min-width: 45px; + text-align: center; + } + + .countdown-timer.warning { + color: var(--warning-color); + } + + .countdown-timer.danger { + color: var(--error-color); + } + + /* 超時設置輸入組件樣式 */ + .timeout-input-group { + display: flex; + align-items: center; + gap: 8px; + } + + .timeout-input { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 6px 12px; + color: var(--text-primary); + font-size: 13px; + width: 100px; + text-align: center; + } + + .timeout-input:focus { + outline: none; + border-color: var(--accent-color); + } + + .timeout-unit { + color: var(--text-secondary); + font-size: 13px; + } + + /* 切換開關樣式 */ + .toggle-switch { + position: relative; + width: 50px; + height: 24px; + background: #666666; + border-radius: 12px; + cursor: pointer; + transition: background-color 0.2s ease; + } + + .toggle-switch.active { + background: var(--accent-color); + } + + .toggle-knob { + position: absolute; + top: 2px; + left: 2px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + transition: transform 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } + + .toggle-switch.active .toggle-knob { + transform: translateX(26px); + } + + /* 響應式調整 */ + @media (max-width: 768px) { + .timeout-controls { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .timeout-separator { + display: none; + } + } + .language-selector { display: flex; align-items: center; @@ -921,7 +1032,14 @@
-

MCP Feedback Enhanced

+
+

MCP Feedback Enhanced

+ + +
專案目錄: {{ project_directory }}
@@ -1211,6 +1329,38 @@
+ +
+
+

⏰ 超時設置

+
+
+
+
+
自動關閉
+
+ 啟用後,介面將在指定時間後自動關閉。倒數計時器會顯示在頂部區域。 +
+
+
+
+
+
+
+
+
超時時間
+
+ 設置自動關閉的時間(30秒 - 2小時) +
+
+
+ + +
+
+
+
+
@@ -1363,9 +1513,25 @@ \ No newline at end of file