Merge pull request #22 from Minidoracat/16-超时时间问题

 新增超時控制功能,包含超時設置、倒數計時器顯示及自動關閉介面功能,並更新相關界面與文檔。
This commit is contained in:
Minidoracat 2025-06-05 02:01:29 +08:00 committed by GitHub
commit d0479907e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1461 additions and 63 deletions

View File

@ -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]: def feedback_ui_with_timeout(project_directory: str, summary: str, timeout: int) -> Optional[FeedbackResult]:
""" """
啟動帶超時的回饋收集 GUI 介面 啟動帶超時的回饋收集 GUI 介面
Args: Args:
project_directory: 專案目錄路徑 project_directory: 專案目錄路徑
summary: AI 工作摘要 summary: AI 工作摘要
timeout: 超時時間 timeout: 超時時間- MCP 傳入的超時時間作為最大限制
Returns: Returns:
Optional[FeedbackResult]: 回饋結果如果用戶取消或超時則返回 None Optional[FeedbackResult]: 回饋結果如果用戶取消或超時則返回 None
Raises: Raises:
TimeoutError: 當超時時拋出 TimeoutError: 當超時時拋出
""" """
@ -76,47 +76,72 @@ def feedback_ui_with_timeout(project_directory: str, summary: str, timeout: int)
app = QApplication.instance() app = QApplication.instance()
if app is None: if app is None:
app = QApplication(sys.argv) app = QApplication(sys.argv)
# 設定全域微軟正黑體字體 # 設定全域微軟正黑體字體
font = QFont("Microsoft JhengHei", 11) # 微軟正黑體11pt font = QFont("Microsoft JhengHei", 11) # 微軟正黑體11pt
app.setFont(font) app.setFont(font)
# 設定字體回退順序,確保中文字體正確顯示 # 設定字體回退順序,確保中文字體正確顯示
app.setStyleSheet(""" app.setStyleSheet("""
* { * {
font-family: "Microsoft JhengHei", "微軟正黑體", "Microsoft YaHei", "微软雅黑", "SimHei", "黑体", sans-serif; font-family: "Microsoft JhengHei", "微軟正黑體", "Microsoft YaHei", "微软雅黑", "SimHei", "黑体", sans-serif;
} }
""") """)
# 創建主窗口 # 創建主窗口,傳入 MCP 超時時間
window = FeedbackWindow(project_directory, summary) 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() window.show()
# 創建超時計時器 # 開始用戶設置的超時倒數(如果啟用)
timeout_timer = QTimer() window.start_timeout_if_enabled()
timeout_timer.setSingleShot(True)
timeout_timer.timeout.connect(lambda: _handle_timeout(window, app)) # 創建 MCP 超時計時器作為後備
timeout_timer.start(timeout * 1000) # 轉換為毫秒 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() 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 介面已自動關閉") raise TimeoutError(f"回饋收集超時({timeout}GUI 介面已自動關閉")
# 返回結果 # 返回結果
return window.result return window.result
def _handle_timeout(window: FeedbackWindow, app: QApplication) -> None: def _handle_timeout(window: FeedbackWindow, app: QApplication) -> None:
"""處理超時事件""" """處理超時事件(舊版本,保留向後兼容)"""
# 標記超時發生 # 標記超時發生
window._timeout_occurred = True window._timeout_occurred = True
# 強制關閉視窗 # 強制關閉視窗
window.force_close() window.force_close()
# 退出應用程式 # 退出應用程式
app.quit() app.quit()
def _handle_mcp_timeout(window: FeedbackWindow, app: QApplication) -> None:
"""處理 MCP 超時事件(後備機制)"""
# 標記超時發生
window._timeout_occurred = True
# 強制關閉視窗
window.force_close()
# 退出應用程式
app.quit()

View File

@ -8,11 +8,12 @@
""" """
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QRadioButton, QButtonGroup, QMessageBox, QComboBox, QRadioButton, QButtonGroup, QMessageBox,
QCheckBox, QPushButton, QFrame QCheckBox, QPushButton, QFrame, QSpinBox
) )
from ..widgets import SwitchWithLabel from ..widgets import SwitchWithLabel
from ..widgets.styled_spinbox import StyledSpinBox
from PySide6.QtCore import Signal, Qt from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QFont from PySide6.QtGui import QFont
@ -25,6 +26,7 @@ class SettingsTab(QWidget):
language_changed = Signal() language_changed = Signal()
layout_change_requested = Signal(bool, str) # 佈局變更請求信號 (combined_mode, orientation) layout_change_requested = Signal(bool, str) # 佈局變更請求信號 (combined_mode, orientation)
reset_requested = Signal() # 重置設定請求信號 reset_requested = Signal() # 重置設定請求信號
timeout_settings_changed = Signal(bool, int) # 超時設置變更信號 (enabled, duration)
def __init__(self, combined_mode: bool, config_manager, parent=None): def __init__(self, combined_mode: bool, config_manager, parent=None):
super().__init__(parent) super().__init__(parent)
@ -86,7 +88,13 @@ class SettingsTab(QWidget):
# 添加分隔線 # 添加分隔線
self._add_separator(content_layout) self._add_separator(content_layout)
# === 超時設置 ===
self._create_timeout_section(content_layout)
# 添加分隔線
self._add_separator(content_layout)
# === 重置設定 === # === 重置設定 ===
self._create_reset_section(content_layout) self._create_reset_section(content_layout)
@ -306,7 +314,63 @@ class SettingsTab(QWidget):
options_layout.addWidget(self.always_center_switch) options_layout.addWidget(self.always_center_switch)
layout.addLayout(options_layout) 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: def _create_reset_section(self, layout: QVBoxLayout) -> None:
"""創建重置設定區域""" """創建重置設定區域"""
header = self._create_section_header(t('settings.reset.title'), "🔄") header = self._create_section_header(t('settings.reset.title'), "🔄")
@ -421,7 +485,27 @@ class SettingsTab(QWidget):
# 立即保存設定 # 立即保存設定
self.config_manager.set_always_center_window(checked) self.config_manager.set_always_center_window(checked)
debug_log(f"視窗定位設置已保存: {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: def _on_reset_settings(self) -> None:
"""重置設定事件處理""" """重置設定事件處理"""
reply = QMessageBox.question( reply = QMessageBox.question(
@ -446,9 +530,10 @@ class SettingsTab(QWidget):
self.ui_elements['window_header'].setText(f"🖥️ {t('settings.window.title')}") self.ui_elements['window_header'].setText(f"🖥️ {t('settings.window.title')}")
if 'reset_header' in self.ui_elements: if 'reset_header' in self.ui_elements:
self.ui_elements['reset_header'].setText(f"🔄 {t('settings.reset.title')}") 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: if 'separate_hint' in self.ui_elements:
self.ui_elements['separate_hint'].setText(f" {t('settings.layout.separateModeDescription')}") 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')}") self.ui_elements['vertical_hint'].setText(f" {t('settings.layout.combinedVerticalDescription')}")
if 'horizontal_hint' in self.ui_elements: if 'horizontal_hint' in self.ui_elements:
self.ui_elements['horizontal_hint'].setText(f" {t('settings.layout.combinedHorizontalDescription')}") 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'): if hasattr(self, 'reset_button'):
self.reset_button.setText(t('settings.reset.button')) self.reset_button.setText(t('settings.reset.button'))
@ -464,6 +551,14 @@ class SettingsTab(QWidget):
# 更新切換開關文字 # 更新切換開關文字
if hasattr(self, 'always_center_switch'): if hasattr(self, 'always_center_switch'):
self.always_center_switch.setText(t('settings.window.alwaysCenter')) 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'): if hasattr(self, 'separate_mode_radio'):
@ -491,6 +586,16 @@ class SettingsTab(QWidget):
always_center = self.config_manager.get_always_center_window() always_center = self.config_manager.get_always_center_window()
self.always_center_switch.setChecked(always_center) self.always_center_switch.setChecked(always_center)
debug_log(f"重新載入視窗定位設置: {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: def set_layout_mode(self, combined_mode: bool) -> None:
"""設置佈局模式""" """設置佈局模式"""

View File

@ -132,10 +132,12 @@ class ImageUploadWidget(QWidget):
def _on_size_limit_changed(self, index: int) -> None: 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) size_bytes = self.size_limit_combo.itemData(index)
self.config_manager.set_image_size_limit(size_bytes) # 處理 None 值
debug_log(f"圖片大小限制已更新: {size_bytes} bytes") 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: def _on_base64_detail_changed(self, state: int) -> None:
"""Base64 詳細模式變更處理""" """Base64 詳細模式變更處理"""
@ -683,6 +685,9 @@ class ImageUploadWidget(QWidget):
# 保存當前選擇 # 保存當前選擇
current_data = self.size_limit_combo.currentData() current_data = self.size_limit_combo.currentData()
# 暫時斷開信號連接以避免觸發變更事件
self.size_limit_combo.blockSignals(True)
# 清除並重新添加選項 # 清除並重新添加選項
self.size_limit_combo.clear() self.size_limit_combo.clear()
self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.unlimited'), 0) self.size_limit_combo.addItem(t('images.settings.sizeLimitOptions.unlimited'), 0)
@ -696,6 +701,9 @@ class ImageUploadWidget(QWidget):
self.size_limit_combo.setCurrentIndex(i) self.size_limit_combo.setCurrentIndex(i)
break break
# 重新連接信號
self.size_limit_combo.blockSignals(False)
if hasattr(self, 'base64_checkbox'): if hasattr(self, 'base64_checkbox'):
self.base64_checkbox.setText(t('images.settings.base64Detail')) self.base64_checkbox.setText(t('images.settings.base64Detail'))
self.base64_checkbox.setToolTip(t('images.settings.base64DetailHelp')) self.base64_checkbox.setToolTip(t('images.settings.base64DetailHelp'))

View File

@ -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)

View File

@ -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;
}}
"""

View File

@ -152,6 +152,10 @@ class ConfigManager:
def set_image_size_limit(self, size_bytes: int) -> None: def set_image_size_limit(self, size_bytes: int) -> None:
"""設置圖片大小限制bytes0 表示無限制""" """設置圖片大小限制bytes0 表示無限制"""
# 處理 None 值
if size_bytes is None:
size_bytes = 0
self.update_partial_config({'image_size_limit': size_bytes}) self.update_partial_config({'image_size_limit': size_bytes})
size_mb = size_bytes / (1024 * 1024) if size_bytes > 0 else 0 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'}") 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}) self.update_partial_config({'enable_base64_detail': enabled})
debug_log(f"Base64 詳細模式設置: {'啟用' if enabled else '停用'}") 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: def reset_settings(self) -> None:
"""重置所有設定到預設值""" """重置所有設定到預設值"""
try: try:

View File

@ -24,13 +24,15 @@ from ...debug import gui_debug_log as debug_log
class FeedbackWindow(QMainWindow): class FeedbackWindow(QMainWindow):
"""回饋收集主窗口(重構版)""" """回饋收集主窗口(重構版)"""
language_changed = Signal() language_changed = Signal()
timeout_occurred = Signal() # 超時發生信號
def __init__(self, project_dir: str, summary: str):
def __init__(self, project_dir: str, summary: str, timeout_seconds: int = None):
super().__init__() super().__init__()
self.project_dir = project_dir self.project_dir = project_dir
self.summary = summary self.summary = summary
self.result = None self.result = None
self.i18n = get_i18n_manager() self.i18n = get_i18n_manager()
self.mcp_timeout_seconds = timeout_seconds # MCP 傳入的超時時間
# 初始化組件 # 初始化組件
self.config_manager = ConfigManager() self.config_manager = ConfigManager()
@ -55,6 +57,9 @@ class FeedbackWindow(QMainWindow):
self._connect_signals() self._connect_signals()
debug_log("主窗口初始化完成") debug_log("主窗口初始化完成")
# 如果啟用了超時,自動開始倒數計時
self.start_timeout_if_enabled()
def _setup_ui(self) -> None: def _setup_ui(self) -> None:
"""設置用戶介面""" """設置用戶介面"""
@ -88,9 +93,72 @@ class FeedbackWindow(QMainWindow):
def _create_project_header(self, layout: QVBoxLayout) -> None: 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 = QLabel(f"{t('app.projectDirectory')}: {self.project_dir}")
self.project_label.setStyleSheet("color: #9e9e9e; font-size: 12px; padding: 4px 0;") 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: def _create_tab_area(self, layout: QVBoxLayout) -> None:
"""創建分頁區域""" """創建分頁區域"""
@ -169,7 +237,10 @@ class FeedbackWindow(QMainWindow):
# 創建分頁 # 創建分頁
self.tab_manager.create_tabs() self.tab_manager.create_tabs()
# 連接分頁信號
self.tab_manager.connect_signals(self)
# 將分頁組件放入滾動區域 # 將分頁組件放入滾動區域
scroll_area.setWidget(self.tab_widget) scroll_area.setWidget(self.tab_widget)
@ -457,16 +528,139 @@ class FeedbackWindow(QMainWindow):
debug_log("強制關閉視窗(超時)") debug_log("強制關閉視窗(超時)")
self.result = "" self.result = ""
self.close() 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: def _refresh_ui_texts(self) -> None:
"""刷新界面文字""" """刷新界面文字"""
self.setWindowTitle(t('app.title')) self.setWindowTitle(t('app.title'))
self.project_label.setText(f"{t('app.projectDirectory')}: {self.project_dir}") self.project_label.setText(f"{t('app.projectDirectory')}: {self.project_dir}")
# 更新按鈕文字 # 更新按鈕文字
self.submit_button.setText(t('buttons.submit')) self.submit_button.setText(t('buttons.submit'))
self.cancel_button.setText(t('buttons.cancel')) 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() self.tab_manager.update_tab_texts()

View File

@ -327,6 +327,8 @@ class TabManager:
self.settings_tab.layout_change_requested.connect(parent._on_layout_change_requested) self.settings_tab.layout_change_requested.connect(parent._on_layout_change_requested)
if hasattr(parent, '_on_reset_settings_requested'): if hasattr(parent, '_on_reset_settings_requested'):
self.settings_tab.reset_requested.connect(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: if self.feedback_tab:

View File

@ -130,6 +130,22 @@
"errorMessage": "Error occurred while resetting settings: {error}" "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": { "buttons": {
"submit": "Submit Feedback", "submit": "Submit Feedback",
"cancel": "Cancel", "cancel": "Cancel",

View File

@ -110,6 +110,22 @@
"errorMessage": "重置设置时发生错误:{error}" "errorMessage": "重置设置时发生错误:{error}"
} }
}, },
"timeout": {
"enable": "自动关闭",
"enableTooltip": "启用后将在指定时间后自动关闭界面",
"duration": {
"label": "超时时间",
"description": "设置自动关闭的时间30秒 - 2小时"
},
"seconds": "秒",
"remaining": "剩余时间",
"expired": "时间已到",
"autoCloseMessage": "界面将在 {seconds} 秒后自动关闭",
"settings": {
"title": "超时设置",
"description": "启用后,界面将在指定时间后自动关闭。倒数计时器会显示在顶部区域。"
}
},
"buttons": { "buttons": {
"submit": "提交反馈", "submit": "提交反馈",
"cancel": "取消", "cancel": "取消",

View File

@ -96,6 +96,22 @@
"sizeLimitExceeded": "圖片 {filename} 大小為 {size},超過 {limit} 限制!", "sizeLimitExceeded": "圖片 {filename} 大小為 {size},超過 {limit} 限制!",
"sizeLimitExceededAdvice": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。" "sizeLimitExceededAdvice": "建議使用圖片編輯軟體壓縮後再上傳,或調整圖片大小限制設定。"
}, },
"timeout": {
"enable": "自動關閉",
"enableTooltip": "啟用後將在指定時間後自動關閉介面",
"duration": {
"label": "超時時間",
"description": "設置自動關閉的時間30秒 - 2小時"
},
"seconds": "秒",
"remaining": "剩餘時間",
"expired": "時間已到",
"autoCloseMessage": "介面將在 {seconds} 秒後自動關閉",
"settings": {
"title": "超時設置",
"description": "啟用後,介面將在指定時間後自動關閉。倒數計時器會顯示在頂部區域。"
}
},
"settings": { "settings": {
"title": "應用設置", "title": "應用設置",
"language": { "language": {

View File

@ -519,8 +519,8 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in
try: try:
# 使用新的 web 模組 # 使用新的 web 模組
from .web import launch_web_feedback_ui from .web import launch_web_feedback_ui, stop_web_ui
# 傳遞 timeout 參數給 Web UI # 傳遞 timeout 參數給 Web UI
return await launch_web_feedback_ui(project_dir, summary, timeout) return await launch_web_feedback_ui(project_dir, summary, timeout)
except ImportError as e: 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: except TimeoutError as e:
debug_log(f"Web UI 超時: {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 { return {
"command_logs": "", "command_logs": "",
"interactive_feedback": f"回饋收集超時({timeout}秒),介面已自動關閉。", "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: except Exception as e:
error_msg = f"Web UI 錯誤: {e}" error_msg = f"Web UI 錯誤: {e}"
debug_log(f"{error_msg}") 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 { return {
"command_logs": "", "command_logs": "",
"interactive_feedback": f"錯誤: {str(e)}", "interactive_feedback": f"錯誤: {str(e)}",

View File

@ -153,6 +153,22 @@
"timeoutDescription": "Due to prolonged inactivity, the session has timed out. The interface will automatically close in 3 seconds.", "timeoutDescription": "Due to prolonged inactivity, the session has timed out. The interface will automatically close in 3 seconds.",
"closing": "Closing..." "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": { "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!", "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$ " "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$ "

View File

@ -153,6 +153,22 @@
"timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。", "timeoutDescription": "由于长时间无响应,会话已超时。界面将在 3 秒后自动关闭。",
"closing": "正在关闭..." "closing": "正在关闭..."
}, },
"timeout": {
"enable": "自动关闭",
"enableTooltip": "启用后将在指定时间后自动关闭界面",
"duration": {
"label": "超时时间",
"description": "设置自动关闭的时间30秒 - 2小时"
},
"seconds": "秒",
"remaining": "剩余时间",
"expired": "⏰ 时间已到,界面将自动关闭",
"autoCloseMessage": "界面将在 {seconds} 秒后自动关闭",
"settings": {
"title": "超时设置",
"description": "启用后,界面将在指定时间后自动关闭。倒数计时器会显示在顶部区域。"
}
},
"dynamic": { "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请测试这些功能并提供反馈", "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$ " "terminalWelcome": "欢迎使用交互反馈终端\n========================================\n项目目录: {sessionId}\n输入命令后按 Enter 或点击执行按钮\n支持的命令: ls, dir, pwd, cat, type 等\n\n$ "

View File

@ -153,6 +153,22 @@
"timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。", "timeoutDescription": "由於長時間無回應,會話已超時。介面將在 3 秒後自動關閉。",
"closing": "正在關閉..." "closing": "正在關閉..."
}, },
"timeout": {
"enable": "自動關閉",
"enableTooltip": "啟用後將在指定時間後自動關閉介面",
"duration": {
"label": "超時時間",
"description": "設置自動關閉的時間30秒 - 2小時"
},
"seconds": "秒",
"remaining": "剩餘時間",
"expired": "⏰ 時間已到,介面將自動關閉",
"autoCloseMessage": "介面將在 {seconds} 秒後自動關閉",
"settings": {
"title": "超時設置",
"description": "啟用後,介面將在指定時間後自動關閉。倒數計時器會顯示在頂部區域。"
}
},
"dynamic": { "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請測試這些功能並提供回饋", "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$ " "terminalWelcome": "歡迎使用互動回饋終端\n========================================\n專案目錄: {sessionId}\n輸入命令後按 Enter 或點擊執行按鈕\n支援的命令: ls, dir, pwd, cat, type 等\n\n$ "

View File

@ -218,6 +218,10 @@ async def launch_web_feedback_ui(project_directory: str, summary: str, timeout:
finally: finally:
# 清理會話(無論成功還是失敗) # 清理會話(無論成功還是失敗)
manager.remove_session(session_id) manager.remove_session(session_id)
# 如果沒有其他活躍會話,停止服務器
if len(manager.sessions) == 0:
debug_log("沒有活躍會話,停止 Web UI 服務器")
stop_web_ui()
def stop_web_ui(): def stop_web_ui():
@ -236,21 +240,22 @@ if __name__ == "__main__":
project_dir = os.getcwd() project_dir = os.getcwd()
summary = "這是一個測試摘要,用於驗證 Web UI 功能。" summary = "這是一個測試摘要,用於驗證 Web UI 功能。"
print(f"啟動 Web UI 測試...") from ..debug import debug_log
print(f"專案目錄: {project_dir}") debug_log(f"啟動 Web UI 測試...")
print("等待用戶回饋...") debug_log(f"專案目錄: {project_dir}")
debug_log("等待用戶回饋...")
result = await launch_web_feedback_ui(project_dir, summary) result = await launch_web_feedback_ui(project_dir, summary)
print("收到回饋結果:") debug_log("收到回饋結果:")
print(f"命令日誌: {result.get('logs', '')}") debug_log(f"命令日誌: {result.get('logs', '')}")
print(f"互動回饋: {result.get('interactive_feedback', '')}") debug_log(f"互動回饋: {result.get('interactive_feedback', '')}")
print(f"圖片數量: {len(result.get('images', []))}") debug_log(f"圖片數量: {len(result.get('images', []))}")
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n用戶取消操作") debug_log("\n用戶取消操作")
except Exception as e: except Exception as e:
print(f"錯誤: {e}") debug_log(f"錯誤: {e}")
finally: finally:
stop_web_ui() stop_web_ui()

View File

@ -198,6 +198,27 @@ async def handle_websocket_message(manager: 'WebUIManager', session, data: dict)
command = data.get("command", "") command = data.get("command", "")
if command.strip(): if command.strip():
await session.run_command(command) 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: else:
debug_log(f"未知的消息類型: {message_type}") 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 服務器已因用戶超時而停止")

View File

@ -95,6 +95,13 @@ class FeedbackApp {
// 圖片設定 // 圖片設定
this.imageSizeLimit = 0; // 0 表示無限制 this.imageSizeLimit = 0; // 0 表示無限制
this.enableBase64Detail = false; this.enableBase64Detail = false;
// 超時設定
this.timeoutEnabled = false;
this.timeoutDuration = 600; // 預設 10 分鐘
this.timeoutTimer = null;
this.countdownTimer = null;
this.remainingSeconds = 0;
// 立即檢查 DOM 狀態並初始化 // 立即檢查 DOM 狀態並初始化
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
@ -133,13 +140,19 @@ class FeedbackApp {
// 載入設定(使用 await // 載入設定(使用 await
await this.loadSettings(); await this.loadSettings();
// 初始化命令終端 // 初始化命令終端
this.initCommandTerminal(); this.initCommandTerminal();
// 確保合併模式狀態正確 // 確保合併模式狀態正確
this.applyCombinedModeState(); this.applyCombinedModeState();
// 初始化超時控制
this.setupTimeoutControl();
// 如果啟用了超時,自動開始倒數計時(在設置載入後)
this.startTimeoutIfEnabled();
console.log('FeedbackApp 初始化完成'); console.log('FeedbackApp 初始化完成');
} }
@ -1122,6 +1135,22 @@ class FeedbackApp {
this.enableBase64Detail = false; // 預設關閉 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 // 同步圖片設定到 UI
this.syncImageSettings(); this.syncImageSettings();
@ -1213,6 +1242,8 @@ $ `;
this.autoClose = true; this.autoClose = true;
this.imageSizeLimit = 0; this.imageSizeLimit = 0;
this.enableBase64Detail = false; this.enableBase64Detail = false;
this.timeoutEnabled = false;
this.timeoutDuration = 600;
// 更新佈局模式單選按鈕狀態 // 更新佈局模式單選按鈕狀態
const layoutRadios = document.querySelectorAll('input[name="layoutMode"]'); const layoutRadios = document.querySelectorAll('input[name="layoutMode"]');
@ -1229,6 +1260,10 @@ $ `;
// 同步圖片設定到 UI // 同步圖片設定到 UI
this.syncImageSettings(); this.syncImageSettings();
// 更新超時 UI
this.updateTimeoutUI();
this.stopTimeout();
// 確保語言選擇器與當前語言同步 // 確保語言選擇器與當前語言同步
this.syncLanguageSelector(); this.syncLanguageSelector();
@ -1307,6 +1342,210 @@ $ `;
}, 3000); }, 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() { async saveSettings() {
try { try {
const settings = { const settings = {
@ -1314,6 +1553,8 @@ $ `;
autoClose: this.autoClose, autoClose: this.autoClose,
imageSizeLimit: this.imageSizeLimit, imageSizeLimit: this.imageSizeLimit,
enableBase64Detail: this.enableBase64Detail, enableBase64Detail: this.enableBase64Detail,
timeoutEnabled: this.timeoutEnabled,
timeoutDuration: this.timeoutDuration,
language: window.i18nManager?.currentLanguage || 'zh-TW', language: window.i18nManager?.currentLanguage || 'zh-TW',
activeTab: localStorage.getItem('activeTab'), activeTab: localStorage.getItem('activeTab'),
lastSaved: new Date().toISOString() lastSaved: new Date().toISOString()

View File

@ -67,6 +67,12 @@
padding: 0 20px; padding: 0 20px;
} }
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.title { .title {
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
@ -79,6 +85,111 @@
font-size: 14px; 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 { .language-selector {
display: flex; display: flex;
align-items: center; align-items: center;
@ -921,7 +1032,14 @@
<!-- 頭部 --> <!-- 頭部 -->
<header class="header"> <header class="header">
<div class="header-content"> <div class="header-content">
<h1 class="title" data-i18n="app.title">MCP Feedback Enhanced</h1> <div class="header-left">
<h1 class="title" data-i18n="app.title">MCP Feedback Enhanced</h1>
<!-- 倒數計時器顯示 -->
<div id="countdownDisplay" class="countdown-display" style="display: none;">
<span class="countdown-label" data-i18n="timeout.remaining">剩餘時間</span>
<span id="countdownTimer" class="countdown-timer">--:--</span>
</div>
</div>
<div class="project-info"> <div class="project-info">
<span data-i18n="app.projectDirectory">專案目錄</span>: {{ project_directory }} <span data-i18n="app.projectDirectory">專案目錄</span>: {{ project_directory }}
</div> </div>
@ -1211,6 +1329,38 @@
</div> </div>
</div> </div>
<!-- 超時設定卡片 -->
<div class="settings-card">
<div class="settings-card-header">
<h3 class="settings-card-title" data-i18n="timeout.settings.title">⏰ 超時設置</h3>
</div>
<div class="settings-card-body">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label" data-i18n="timeout.enable">自動關閉</div>
<div class="setting-description" data-i18n="timeout.settings.description">
啟用後,介面將在指定時間後自動關閉。倒數計時器會顯示在頂部區域。
</div>
</div>
<div id="timeoutToggle" class="toggle-switch">
<div class="toggle-knob"></div>
</div>
</div>
<div class="setting-item" style="border-bottom: none;">
<div class="setting-info">
<div class="setting-label" data-i18n="timeout.duration.label">超時時間</div>
<div class="setting-description" data-i18n="timeout.duration.description">
設置自動關閉的時間30秒 - 2小時
</div>
</div>
<div class="timeout-input-group">
<input type="number" id="timeoutDuration" class="timeout-input" min="30" max="7200" value="600">
<span class="timeout-unit" data-i18n="timeout.seconds"></span>
</div>
</div>
</div>
</div>
<!-- 語言設定卡片 --> <!-- 語言設定卡片 -->
<div class="settings-card"> <div class="settings-card">
<div class="settings-card-header"> <div class="settings-card-header">
@ -1363,9 +1513,25 @@
<script src="/static/js/i18n.js"></script> <script src="/static/js/i18n.js"></script>
<script src="/static/js/app.js"></script> <script src="/static/js/app.js"></script>
<script> <script>
// 初始化全域應用程式實例 // 等待 I18nManager 初始化完成後再初始化 FeedbackApp
const sessionId = '{{ session_id }}'; async function initializeApp() {
window.feedbackApp = new FeedbackApp(sessionId); const sessionId = '{{ session_id }}';
// 確保 I18nManager 已經初始化
if (window.i18nManager) {
await window.i18nManager.init();
}
// 初始化 FeedbackApp
window.feedbackApp = new FeedbackApp(sessionId);
}
// 頁面載入完成後初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}
</script> </script>
</body> </body>
</html> </html>