重構 GUI 模組,優化版面及設置功能選擇,強化多語系

This commit is contained in:
Minidoracat 2025-06-03 03:45:33 +08:00
parent 6203a75aab
commit 48654d2c93
31 changed files with 2767 additions and 1792 deletions

View File

@ -22,7 +22,7 @@ __author__ = "Minidoracat"
__email__ = "minidora0702@gmail.com"
from .server import main as run_server
from .feedback_ui import feedback_ui
from .gui import feedback_ui
from .web_ui import WebUIManager
# 主要導出介面

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
"""
互動式回饋收集 GUI 模組
=======================
基於 PySide6 的圖形用戶介面模組提供直觀的回饋收集功能
支援文字輸入圖片上傳命令執行等功能
模組結構
- main.py: 主要介面入口點
- window/: 窗口類別
- widgets/: 自定義元件
- styles/: 樣式定義
- utils/: 工具函數
- models/: 資料模型
作者: Fábio Ferreira
靈感來源: dotcursorrules.com
增強功能: 圖片支援和現代化界面設計
多語系支援: Minidoracat
重構: 模塊化設計
"""
from .main import feedback_ui
__all__ = ['feedback_ui']

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI 主要入口點
==============
提供 GUI 回饋介面的主要入口點函數
"""
from typing import Optional
from PySide6.QtWidgets import QApplication, QMainWindow
import sys
from .models import FeedbackResult
from .window import FeedbackWindow
def feedback_ui(project_directory: str, summary: str) -> Optional[FeedbackResult]:
"""
啟動回饋收集 GUI 介面
Args:
project_directory: 專案目錄路徑
summary: AI 工作摘要
Returns:
Optional[FeedbackResult]: 回饋結果如果用戶取消則返回 None
"""
# 檢查是否已有 QApplication 實例
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# 創建主窗口
window = FeedbackWindow(project_directory, summary)
window.show()
# 運行事件循環直到窗口關閉
app.exec()
# 返回結果
return window.result

View File

@ -0,0 +1,10 @@
"""
GUI 資料模型模組
===============
定義 GUI 相關的資料結構和型別
"""
from .feedback import FeedbackResult
__all__ = ['FeedbackResult']

View File

@ -0,0 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回饋結果資料模型
===============
定義回饋收集的資料結構
"""
from typing import TypedDict, List
class FeedbackResult(TypedDict):
"""回饋結果的型別定義"""
command_logs: str
interactive_feedback: str
images: List[dict]

View File

@ -0,0 +1,17 @@
"""
GUI 樣式模組
============
集中管理 GUI 的樣式定義
"""
from .themes import *
__all__ = [
'BUTTON_BASE_STYLE',
'PRIMARY_BUTTON_STYLE',
'SUCCESS_BUTTON_STYLE',
'DANGER_BUTTON_STYLE',
'SECONDARY_BUTTON_STYLE',
'DARK_STYLE'
]

View File

@ -0,0 +1,277 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI 主題樣式定義
===============
集中定義所有 GUI 元件的樣式
"""
# 統一按鈕樣式常量
BUTTON_BASE_STYLE = """
QPushButton {
color: white;
border: none;
border-radius: 4px;
font-weight: bold;
font-size: 12px;
}
QPushButton:hover {
opacity: 0.8;
}
"""
PRIMARY_BUTTON_STYLE = BUTTON_BASE_STYLE + """
QPushButton {
background-color: #0e639c;
}
QPushButton:hover {
background-color: #005a9e;
}
"""
SUCCESS_BUTTON_STYLE = BUTTON_BASE_STYLE + """
QPushButton {
background-color: #4caf50;
}
QPushButton:hover {
background-color: #45a049;
}
"""
DANGER_BUTTON_STYLE = BUTTON_BASE_STYLE + """
QPushButton {
background-color: #f44336;
color: #ffffff;
}
QPushButton:hover {
background-color: #d32f2f;
color: #ffffff;
}
"""
SECONDARY_BUTTON_STYLE = BUTTON_BASE_STYLE + """
QPushButton {
background-color: #666666;
}
QPushButton:hover {
background-color: #555555;
}
"""
# Dark 主題樣式
DARK_STYLE = """
QMainWindow {
background-color: #1e1e1e;
color: #d4d4d4;
}
QWidget {
background-color: #1e1e1e;
color: #d4d4d4;
}
QLabel {
color: #d4d4d4;
}
QLineEdit {
background-color: #333333;
border: 1px solid #464647;
padding: 8px;
border-radius: 6px;
color: #d4d4d4;
font-size: 14px;
}
QLineEdit:focus {
border-color: #007acc;
background-color: #383838;
}
QTextEdit {
background-color: #333333;
border: 1px solid #464647;
padding: 8px;
border-radius: 6px;
color: #d4d4d4;
font-size: 14px;
line-height: 1.5;
}
QTextEdit:focus {
border-color: #007acc;
background-color: #383838;
}
QGroupBox {
font-weight: bold;
border: 2px solid #464647;
border-radius: 6px;
margin-top: 6px;
padding-top: 10px;
background-color: #2d2d30;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 8px;
background-color: #2d2d30;
color: #007acc;
}
QTabWidget::pane {
border: 1px solid #464647;
background-color: #2d2d30;
border-radius: 6px;
}
QTabBar::tab {
background-color: #3c3c3c;
color: #d4d4d4;
padding: 8px 12px;
margin-right: 2px;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
QTabBar::tab:selected {
background-color: #007acc;
color: white;
}
QTabBar::tab:hover {
background-color: #4a4a4a;
}
QComboBox {
background-color: #333333;
border: 1px solid #464647;
padding: 6px 8px;
border-radius: 6px;
color: #d4d4d4;
font-size: 14px;
min-width: 120px;
}
QComboBox:focus {
border-color: #007acc;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #d4d4d4;
margin-top: 2px;
}
QComboBox QAbstractItemView {
background-color: #333333;
border: 1px solid #464647;
color: #d4d4d4;
selection-background-color: #007acc;
}
QScrollBar:vertical {
background-color: #333333;
width: 12px;
border: none;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #555555;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #777777;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
border: none;
background: none;
}
QScrollBar:horizontal {
background-color: #333333;
height: 12px;
border: none;
border-radius: 6px;
}
QScrollBar::handle:horizontal {
background-color: #555555;
border-radius: 6px;
min-width: 20px;
}
QScrollBar::handle:horizontal:hover {
background-color: #777777;
}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
border: none;
background: none;
}
QMenuBar {
background-color: #2d2d30;
color: #d4d4d4;
border-bottom: 1px solid #464647;
}
QMenuBar::item {
background-color: transparent;
padding: 4px 8px;
}
QMenuBar::item:selected {
background-color: #007acc;
}
QMenu {
background-color: #2d2d30;
color: #d4d4d4;
border: 1px solid #464647;
}
QMenu::item {
padding: 6px 12px;
}
QMenu::item:selected {
background-color: #007acc;
}
QSplitter::handle {
background-color: #464647;
}
QSplitter::handle:horizontal {
width: 3px;
}
QSplitter::handle:vertical {
height: 3px;
}
/* 訊息框樣式 */
QMessageBox {
background-color: #2d2d30;
color: #d4d4d4;
}
QMessageBox QPushButton {
min-width: 60px;
padding: 6px 12px;
}
"""

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
分頁組件
========
包含各種專用分頁組件的實現
"""
from .feedback_tab import FeedbackTab
from .summary_tab import SummaryTab
from .command_tab import CommandTab
from .settings_tab import SettingsTab
__all__ = [
'FeedbackTab',
'SummaryTab',
'CommandTab',
'SettingsTab'
]

View File

@ -0,0 +1,194 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
命令分頁組件
============
專門處理命令執行的分頁組件
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QTextEdit, QLineEdit, QPushButton
)
from PySide6.QtCore import Signal
from PySide6.QtGui import QFont
from ..utils import apply_widget_styles
from ..window.command_executor import CommandExecutor
from ...i18n import t
class CommandTab(QWidget):
"""命令分頁組件"""
def __init__(self, project_dir: str, parent=None):
super().__init__(parent)
self.project_dir = project_dir
self.command_executor = CommandExecutor(project_dir, self)
self._setup_ui()
self._connect_signals()
def _setup_ui(self) -> None:
"""設置用戶介面"""
command_layout = QVBoxLayout(self)
command_layout.setSpacing(0) # 緊湊佈局
command_layout.setContentsMargins(0, 0, 0, 0)
# 命令說明區域(頂部,只保留說明文字)
header_widget = QWidget()
header_layout = QVBoxLayout(header_widget)
header_layout.setSpacing(6)
header_layout.setContentsMargins(12, 8, 12, 8)
self.command_description_label = QLabel(t('command.description'))
self.command_description_label.setStyleSheet("color: #9e9e9e; font-size: 11px; margin-bottom: 6px;")
self.command_description_label.setWordWrap(True)
header_layout.addWidget(self.command_description_label)
command_layout.addWidget(header_widget)
# 命令輸出區域(中間,佔大部分空間)
output_widget = QWidget()
output_layout = QVBoxLayout(output_widget)
output_layout.setSpacing(6)
output_layout.setContentsMargins(12, 4, 12, 8)
self.command_output = QTextEdit()
self.command_output.setReadOnly(True)
self.command_output.setFont(QFont("Consolas", 11))
self.command_output.setPlaceholderText(t('command.outputPlaceholder'))
# 終端機風格樣式
self.command_output.setStyleSheet("""
QTextEdit {
background-color: #1a1a1a;
border: 1px solid #333;
border-radius: 6px;
padding: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 11px;
color: #00ff00;
line-height: 1.4;
}
QScrollBar:vertical {
background-color: #2a2a2a;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #555;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #666;
}
""")
output_layout.addWidget(self.command_output, 1) # 佔據剩餘空間
command_layout.addWidget(output_widget, 1) # 輸出區域佔大部分空間
# 命令輸入區域(底部,固定高度)
input_widget = QWidget()
input_widget.setFixedHeight(70) # 固定高度
input_layout = QVBoxLayout(input_widget)
input_layout.setSpacing(6)
input_layout.setContentsMargins(12, 8, 12, 12)
# 命令輸入和執行按鈕(水平布局)
input_row_layout = QHBoxLayout()
input_row_layout.setSpacing(8)
# 提示符號標籤
prompt_label = QLabel("$")
prompt_label.setStyleSheet("color: #00ff00; font-family: 'Consolas', 'Monaco', monospace; font-size: 14px; font-weight: bold;")
prompt_label.setFixedWidth(20)
input_row_layout.addWidget(prompt_label)
self.command_input = QLineEdit()
self.command_input.setPlaceholderText(t('command.placeholder'))
self.command_input.setMinimumHeight(36)
# 終端機風格輸入框
self.command_input.setStyleSheet("""
QLineEdit {
background-color: #1a1a1a;
border: 2px solid #333;
border-radius: 4px;
padding: 8px 12px;
color: #00ff00;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
}
QLineEdit:focus {
border-color: #007acc;
background-color: #1e1e1e;
}
""")
self.command_input.returnPressed.connect(self._run_command)
input_row_layout.addWidget(self.command_input, 1) # 佔據大部分空間
self.command_run_button = QPushButton(t('command.run'))
self.command_run_button.clicked.connect(self._run_command)
self.command_run_button.setFixedSize(80, 36)
apply_widget_styles(self.command_run_button, "primary_button")
input_row_layout.addWidget(self.command_run_button)
self.command_terminate_button = QPushButton(t('command.terminate'))
self.command_terminate_button.clicked.connect(self._terminate_command)
self.command_terminate_button.setFixedSize(80, 36)
apply_widget_styles(self.command_terminate_button, "danger_button")
input_row_layout.addWidget(self.command_terminate_button)
input_layout.addLayout(input_row_layout)
command_layout.addWidget(input_widget) # 輸入區域在底部
def _connect_signals(self) -> None:
"""連接信號"""
self.command_executor.output_received.connect(self._append_command_output)
def _run_command(self) -> None:
"""執行命令"""
command = self.command_input.text().strip()
if command:
self.command_executor.run_command(command)
self.command_input.clear()
def _terminate_command(self) -> None:
"""終止命令"""
self.command_executor.terminate_command()
def _append_command_output(self, text: str) -> None:
"""添加命令輸出並自動滾動到底部"""
# 移動光標到最後
cursor = self.command_output.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
self.command_output.setTextCursor(cursor)
# 插入文本
self.command_output.insertPlainText(text)
# 確保滾動到最底部
scrollbar = self.command_output.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
# 刷新界面
from PySide6.QtWidgets import QApplication
QApplication.processEvents()
def get_command_logs(self) -> str:
"""獲取命令日誌"""
return self.command_output.toPlainText().strip()
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
self.command_description_label.setText(t('command.description'))
self.command_input.setPlaceholderText(t('command.placeholder'))
self.command_output.setPlaceholderText(t('command.outputPlaceholder'))
self.command_run_button.setText(t('command.run'))
self.command_terminate_button.setText(t('command.terminate'))
def cleanup(self) -> None:
"""清理資源"""
if self.command_executor:
self.command_executor.cleanup()

View File

@ -0,0 +1,104 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回饋分頁組件
============
專門處理用戶回饋輸入的分頁組件
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QSplitter
from PySide6.QtCore import Qt, Signal
from ..widgets import SmartTextEdit, ImageUploadWidget
from ...i18n import t
class FeedbackTab(QWidget):
"""回饋分頁組件"""
image_paste_requested = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self._setup_ui()
def _setup_ui(self) -> None:
"""設置用戶介面"""
# 主布局
tab_layout = QVBoxLayout(self)
tab_layout.setSpacing(12)
tab_layout.setContentsMargins(16, 16, 16, 16)
# 說明文字
self.feedback_description = QLabel(t('feedback.description'))
self.feedback_description.setStyleSheet("color: #9e9e9e; font-size: 12px; margin-bottom: 10px;")
self.feedback_description.setWordWrap(True)
tab_layout.addWidget(self.feedback_description)
# 使用分割器來管理回饋輸入和圖片區域
feedback_splitter = QSplitter(Qt.Vertical)
feedback_splitter.setChildrenCollapsible(False)
# 回饋輸入區域
self.feedback_input = SmartTextEdit()
placeholder_text = t('feedback.placeholder').replace("Ctrl+Enter", "Ctrl+Enter/Cmd+Enter").replace("Ctrl+V", "Ctrl+V/Cmd+V")
self.feedback_input.setPlaceholderText(placeholder_text)
self.feedback_input.setMinimumHeight(120)
self.feedback_input.setStyleSheet("""
QTextEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 10px;
color: #ffffff;
font-size: 12px;
line-height: 1.4;
}
""")
self.feedback_input.image_paste_requested.connect(self.image_paste_requested)
# 圖片上傳區域
image_upload_widget = QWidget()
image_upload_widget.setMinimumHeight(140)
image_upload_widget.setMaximumHeight(250)
image_upload_layout = QVBoxLayout(image_upload_widget)
image_upload_layout.setSpacing(8)
image_upload_layout.setContentsMargins(0, 8, 0, 0)
self.image_upload = ImageUploadWidget()
image_upload_layout.addWidget(self.image_upload, 1)
# 添加到分割器
feedback_splitter.addWidget(self.feedback_input)
feedback_splitter.addWidget(image_upload_widget)
# 設置分割器比例 (70% : 30%)
feedback_splitter.setStretchFactor(0, 3) # 回饋輸入區域較大
feedback_splitter.setStretchFactor(1, 1) # 圖片上傳區域較小
feedback_splitter.setSizes([300, 140]) # 設置初始大小
# 設置分割器的最小尺寸,防止子元件被過度壓縮
feedback_splitter.setMinimumHeight(340) # 設置分割器最小高度
tab_layout.addWidget(feedback_splitter, 1)
def get_feedback_text(self) -> str:
"""獲取回饋文字"""
return self.feedback_input.toPlainText().strip()
def get_images_data(self) -> list:
"""獲取圖片數據"""
return self.image_upload.get_images_data()
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
self.feedback_description.setText(t('feedback.description'))
placeholder_text = t('feedback.placeholder').replace("Ctrl+Enter", "Ctrl+Enter/Cmd+Enter").replace("Ctrl+V", "Ctrl+V/Cmd+V")
self.feedback_input.setPlaceholderText(placeholder_text)
if hasattr(self, 'image_upload'):
self.image_upload.update_texts()
def handle_image_paste_from_textarea(self) -> None:
"""處理從文字框智能貼上圖片的功能"""
self.image_upload.paste_from_clipboard()

View File

@ -0,0 +1,230 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
設置分頁組件
============
專門處理應用設置的分頁組件
"""
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QGroupBox, QComboBox, QRadioButton, QButtonGroup, QMessageBox
)
from PySide6.QtCore import Signal
from ...i18n import t, get_i18n_manager
class SettingsTab(QWidget):
"""設置分頁組件"""
language_changed = Signal()
layout_mode_change_requested = Signal(bool) # 佈局模式變更請求信號
def __init__(self, combined_mode: bool, parent=None):
super().__init__(parent)
self.combined_mode = combined_mode
self.i18n = get_i18n_manager()
self._setup_ui()
def _setup_ui(self) -> None:
"""設置用戶介面"""
layout = QVBoxLayout(self)
layout.setSpacing(16)
layout.setContentsMargins(16, 16, 16, 16)
# === 語言設置區域 ===
self.language_group = QGroupBox(t('settings.language.title'))
self.language_group.setObjectName('language_group')
language_layout = QVBoxLayout(self.language_group)
language_layout.setSpacing(12)
language_layout.setContentsMargins(16, 16, 16, 16)
# 語言選擇器
language_row = QHBoxLayout()
self.language_label = QLabel(t('settings.language.selector'))
self.language_label.setStyleSheet("font-weight: bold; color: #e0e0e0; font-size: 14px;")
language_row.addWidget(self.language_label)
self.language_selector = QComboBox()
self.language_selector.setMinimumWidth(180)
self.language_selector.setMinimumHeight(35)
self.language_selector.setStyleSheet("""
QComboBox {
background-color: #404040;
border: 1px solid #606060;
border-radius: 4px;
padding: 8px 12px;
color: #e0e0e0;
font-size: 14px;
}
QComboBox:hover {
border-color: #0078d4;
}
QComboBox::drop-down {
border: none;
width: 25px;
}
QComboBox::down-arrow {
image: none;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 7px solid #e0e0e0;
margin-right: 6px;
}
QComboBox QAbstractItemView {
background-color: #404040;
border: 1px solid #606060;
selection-background-color: #0078d4;
color: #e0e0e0;
font-size: 14px;
}
""")
# 填充語言選項和連接信號
self._populate_language_selector()
self.language_selector.currentIndexChanged.connect(self._on_language_changed)
language_row.addWidget(self.language_selector)
language_row.addStretch()
language_layout.addLayout(language_row)
# 語言說明
self.language_description_label = QLabel(t('settings.language.description'))
self.language_description_label.setStyleSheet("color: #9e9e9e; font-size: 12px; margin-top: 8px;")
self.language_description_label.setWordWrap(True)
language_layout.addWidget(self.language_description_label)
layout.addWidget(self.language_group)
# === 界面佈局設置區域 ===
self.layout_group = QGroupBox(t('settings.layout.title'))
self.layout_group.setObjectName('layout_group')
layout_layout = QVBoxLayout(self.layout_group)
layout_layout.setSpacing(12)
layout_layout.setContentsMargins(16, 16, 16, 16)
# 佈局模式選擇
self.layout_button_group = QButtonGroup()
# 分離模式
self.separate_mode_radio = QRadioButton(t('settings.layout.separateMode'))
self.separate_mode_radio.setChecked(not self.combined_mode)
self.separate_mode_radio.setStyleSheet("font-size: 14px; font-weight: bold; color: #e0e0e0;")
self.layout_button_group.addButton(self.separate_mode_radio, 0)
layout_layout.addWidget(self.separate_mode_radio)
self.separate_desc_label = QLabel(t('settings.layout.separateModeDescription'))
self.separate_desc_label.setStyleSheet("color: #9e9e9e; font-size: 12px; margin-left: 20px; margin-bottom: 8px;")
self.separate_desc_label.setWordWrap(True)
layout_layout.addWidget(self.separate_desc_label)
# 合併模式
self.combined_mode_radio = QRadioButton(t('settings.layout.combinedMode'))
self.combined_mode_radio.setChecked(self.combined_mode)
self.combined_mode_radio.setStyleSheet("font-size: 14px; font-weight: bold; color: #e0e0e0;")
self.layout_button_group.addButton(self.combined_mode_radio, 1)
layout_layout.addWidget(self.combined_mode_radio)
self.combined_desc_label = QLabel(t('settings.layout.combinedModeDescription'))
self.combined_desc_label.setStyleSheet("color: #9e9e9e; font-size: 12px; margin-left: 20px; margin-bottom: 8px;")
self.combined_desc_label.setWordWrap(True)
layout_layout.addWidget(self.combined_desc_label)
# 連接佈局模式變更信號
self.layout_button_group.buttonToggled.connect(self._on_layout_mode_changed)
layout.addWidget(self.layout_group)
layout.addStretch()
def _populate_language_selector(self) -> None:
"""填充語言選擇器"""
# 保存當前選擇
current_lang = self.i18n.get_current_language()
# 暫時斷開信號連接,避免觸發語言變更事件
try:
self.language_selector.currentIndexChanged.disconnect()
except RuntimeError:
pass # 如果沒有連接則忽略
# 清空並重新填充
self.language_selector.clear()
for lang_code in self.i18n.get_supported_languages():
display_name = self.i18n.get_language_display_name(lang_code)
self.language_selector.addItem(display_name, lang_code)
# 設置當前選中的語言
for i in range(self.language_selector.count()):
if self.language_selector.itemData(i) == current_lang:
self.language_selector.setCurrentIndex(i)
break
# 重新連接信號
self.language_selector.currentIndexChanged.connect(self._on_language_changed)
def _on_language_changed(self, index: int) -> None:
"""處理語言變更"""
lang_code = self.language_selector.itemData(index)
if lang_code and self.i18n.set_language(lang_code):
# 發送語言變更信號
self.language_changed.emit()
def _on_layout_mode_changed(self, button, checked: bool) -> None:
"""處理佈局模式變更"""
if not checked: # 只處理選中的按鈕
return
# 確定新的模式
new_combined_mode = button == self.combined_mode_radio
if new_combined_mode != self.combined_mode:
# 提示用戶需要重新創建界面
reply = QMessageBox.question(
self,
t('app.layoutChangeTitle'),
t('app.layoutChangeMessage'),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
# 用戶確認變更,發送佈局模式變更請求
self.combined_mode = new_combined_mode
self.layout_mode_change_requested.emit(self.combined_mode)
else:
# 用戶選擇不重新載入,恢復原來的選項
if self.combined_mode:
self.combined_mode_radio.setChecked(True)
else:
self.separate_mode_radio.setChecked(True)
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
# 更新GroupBox標題
self.language_group.setTitle(t('settings.language.title'))
self.layout_group.setTitle(t('settings.layout.title'))
# 更新標籤文字
self.language_label.setText(t('settings.language.selector'))
self.language_description_label.setText(t('settings.language.description'))
# 更新佈局設置文字
self.separate_mode_radio.setText(t('settings.layout.separateMode'))
self.combined_mode_radio.setText(t('settings.layout.combinedMode'))
# 更新佈局描述文字
self.separate_desc_label.setText(t('settings.layout.separateModeDescription'))
self.combined_desc_label.setText(t('settings.layout.combinedModeDescription'))
# 重新填充語言選擇器
self._populate_language_selector()
def set_layout_mode(self, combined_mode: bool) -> None:
"""設置佈局模式"""
self.combined_mode = combined_mode
if combined_mode:
self.combined_mode_radio.setChecked(True)
else:
self.separate_mode_radio.setChecked(True)

View File

@ -0,0 +1,93 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
摘要分頁組件
============
專門顯示AI工作摘要的分頁組件
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit
from ...i18n import t
class SummaryTab(QWidget):
"""摘要分頁組件"""
def __init__(self, summary: str, parent=None):
super().__init__(parent)
self.summary = summary
self._setup_ui()
def _setup_ui(self) -> None:
"""設置用戶介面"""
layout = QVBoxLayout(self)
layout.setSpacing(12)
layout.setContentsMargins(16, 16, 16, 16)
# 說明文字
if self._is_test_summary():
self.summary_description_label = QLabel(t('summary.testDescription'))
else:
self.summary_description_label = QLabel(t('summary.description'))
self.summary_description_label.setStyleSheet("color: #9e9e9e; font-size: 12px; margin-bottom: 10px;")
self.summary_description_label.setWordWrap(True)
layout.addWidget(self.summary_description_label)
# 摘要顯示區域
self.summary_display = QTextEdit()
# 檢查是否為測試摘要,如果是則使用翻譯的內容
if self._is_test_summary():
self.summary_display.setPlainText(t('test.qtGuiSummary'))
else:
self.summary_display.setPlainText(self.summary)
self.summary_display.setReadOnly(True)
self.summary_display.setStyleSheet("""
QTextEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 10px;
color: #ffffff;
font-size: 12px;
line-height: 1.4;
}
""")
layout.addWidget(self.summary_display, 1)
def _is_test_summary(self) -> bool:
"""檢查是否為測試摘要"""
test_indicators = [
# 繁體中文
"圖片預覽和視窗調整測試",
"圖片預覽和窗口調整測試",
"這是一個測試會話",
"功能測試項目",
# 簡體中文
"图片预览和窗口调整测试",
"这是一个测试会话",
"功能测试项目",
# 英文
"Image Preview and Window Adjustment Test",
"This is a test session",
"Test Items",
# 通用
"測試", "测试", "test", "Test",
"🎯", "", "📋" # 測試摘要特有的 emoji
]
return any(indicator in self.summary for indicator in test_indicators)
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
if self._is_test_summary():
self.summary_description_label.setText(t('summary.testDescription'))
# 更新測試摘要的內容
self.summary_display.setPlainText(t('test.qtGuiSummary'))
else:
self.summary_description_label.setText(t('summary.description'))

View File

@ -0,0 +1,14 @@
"""
GUI 工具函數模組
===============
包含各種輔助工具函數
"""
from .shortcuts import setup_shortcuts
from .utils import apply_widget_styles
__all__ = [
'setup_shortcuts',
'apply_widget_styles'
]

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快捷鍵設置工具
==============
管理 GUI 快捷鍵設置的工具函數
"""
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtCore import Qt
def setup_shortcuts(window):
"""
設置窗口的快捷鍵
Args:
window: 主窗口實例
"""
# Ctrl+Enter 提交回饋
submit_shortcut = QShortcut(QKeySequence("Ctrl+Return"), window)
submit_shortcut.activated.connect(window._submit_feedback)
# Escape 取消回饋
cancel_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), window)
cancel_shortcut.activated.connect(window._cancel_feedback)
# Ctrl+R 執行命令
run_shortcut = QShortcut(QKeySequence("Ctrl+R"), window)
run_shortcut.activated.connect(window._run_command)
# Ctrl+Shift+C 終止命令
terminate_shortcut = QShortcut(QKeySequence("Ctrl+Shift+C"), window)
terminate_shortcut.activated.connect(window._terminate_command)

View File

@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通用工具函數
============
提供 GUI 相關的通用工具函數
"""
from ..styles import *
def apply_widget_styles(widget, style_type="default"):
"""
應用樣式到元件
Args:
widget: 要應用樣式的元件
style_type: 樣式類型
"""
if style_type == "primary_button":
widget.setStyleSheet(PRIMARY_BUTTON_STYLE)
elif style_type == "success_button":
widget.setStyleSheet(SUCCESS_BUTTON_STYLE)
elif style_type == "danger_button":
widget.setStyleSheet(DANGER_BUTTON_STYLE)
elif style_type == "secondary_button":
widget.setStyleSheet(SECONDARY_BUTTON_STYLE)
elif style_type == "dark_theme":
widget.setStyleSheet(DARK_STYLE)
def format_file_size(size_bytes):
"""
格式化文件大小顯示
Args:
size_bytes: 文件大小字節
Returns:
str: 格式化後的文件大小字符串
"""
if size_bytes < 1024:
return f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
size_kb = size_bytes / 1024
return f"{size_kb:.1f} KB"
else:
size_mb = size_bytes / (1024 * 1024)
return f"{size_mb:.1f} MB"

View File

@ -0,0 +1,16 @@
"""
GUI 自定義元件模組
==================
包含所有自定義的 GUI 元件
"""
from .text_edit import SmartTextEdit
from .image_preview import ImagePreviewWidget
from .image_upload import ImageUploadWidget
__all__ = [
'SmartTextEdit',
'ImagePreviewWidget',
'ImageUploadWidget'
]

View File

@ -0,0 +1,95 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
圖片預覽元件
============
提供圖片預覽和刪除功能的自定義元件
"""
import os
from PySide6.QtWidgets import QLabel, QPushButton, QFrame, QMessageBox
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QPixmap
# 導入多語系支援
from ...i18n import t
class ImagePreviewWidget(QLabel):
"""圖片預覽元件"""
remove_clicked = Signal(str)
def __init__(self, image_path: str, image_id: str, parent=None):
super().__init__(parent)
self.image_path = image_path
self.image_id = image_id
self._setup_widget()
self._load_image()
self._create_delete_button()
def _setup_widget(self) -> None:
"""設置元件基本屬性"""
self.setFixedSize(100, 100)
self.setFrameStyle(QFrame.Box)
self.setStyleSheet("""
QLabel {
border: 2px solid #464647;
border-radius: 8px;
background-color: #2d2d30;
padding: 2px;
}
QLabel:hover {
border-color: #007acc;
background-color: #383838;
}
""")
self.setToolTip(f"圖片: {os.path.basename(self.image_path)}")
def _load_image(self) -> None:
"""載入並顯示圖片"""
try:
pixmap = QPixmap(self.image_path)
if not pixmap.isNull():
scaled_pixmap = pixmap.scaled(96, 96, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.setPixmap(scaled_pixmap)
self.setAlignment(Qt.AlignCenter)
else:
self.setText("無法載入圖片")
self.setAlignment(Qt.AlignCenter)
except Exception:
self.setText("載入錯誤")
self.setAlignment(Qt.AlignCenter)
def _create_delete_button(self) -> None:
"""創建刪除按鈕"""
self.delete_button = QPushButton("×", self)
self.delete_button.setFixedSize(20, 20)
self.delete_button.move(78, 2)
self.delete_button.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: #ffffff;
border: none;
border-radius: 10px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #d32f2f;
color: #ffffff;
}
""")
self.delete_button.clicked.connect(self._on_delete_clicked)
self.delete_button.setToolTip(t('images.clear'))
def _on_delete_clicked(self) -> None:
"""處理刪除按鈕點擊事件"""
reply = QMessageBox.question(
self, t('images.deleteTitle'),
t('images.deleteConfirm', filename=os.path.basename(self.image_path)),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.remove_clicked.emit(self.image_id)

View File

@ -0,0 +1,523 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
圖片上傳元件
============
支援文件選擇剪貼板貼上拖拽上傳等多種方式的圖片上傳元件
"""
import os
import uuid
import time
from typing import Dict, List
from pathlib import Path
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QScrollArea, QGridLayout, QFileDialog, QMessageBox, QApplication
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QDragEnterEvent, QDropEvent
# 導入多語系支援
from ...i18n import t
from ...debug import gui_debug_log as debug_log
from .image_preview import ImagePreviewWidget
class ImageUploadWidget(QWidget):
"""圖片上傳元件"""
images_changed = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.images: Dict[str, Dict[str, str]] = {}
self._setup_ui()
self.setAcceptDrops(True)
# 啟動時清理舊的臨時文件
self._cleanup_old_temp_files()
def _setup_ui(self) -> None:
"""設置用戶介面"""
layout = QVBoxLayout(self)
layout.setSpacing(6)
layout.setContentsMargins(12, 8, 12, 8)
# 標題
self.title = QLabel(t('images.title'))
self.title.setFont(QFont("", 10, QFont.Bold))
self.title.setStyleSheet("color: #007acc; margin: 1px 0;")
layout.addWidget(self.title)
# 狀態標籤
self.status_label = QLabel(t('images.status', count=0))
self.status_label.setStyleSheet("color: #9e9e9e; font-size: 10px; margin: 5px 0;")
layout.addWidget(self.status_label)
# 統一的圖片區域(整合按鈕、拖拽、預覽)
self._create_unified_image_area(layout)
def _create_unified_image_area(self, layout: QVBoxLayout) -> None:
"""創建統一的圖片區域"""
# 創建滾動區域
self.preview_scroll = QScrollArea()
self.preview_widget = QWidget()
self.preview_layout = QVBoxLayout(self.preview_widget)
self.preview_layout.setSpacing(6)
self.preview_layout.setContentsMargins(8, 8, 8, 8)
# 創建操作按鈕區域
self._create_buttons_in_area()
# 創建拖拽提示標籤(初始顯示)
self.drop_hint_label = QLabel(t('images.dragHint'))
self.drop_hint_label.setAlignment(Qt.AlignCenter)
self.drop_hint_label.setMinimumHeight(60)
self.drop_hint_label.setStyleSheet("""
QLabel {
border: 2px dashed #464647;
border-radius: 6px;
background-color: #2d2d30;
color: #9e9e9e;
font-size: 11px;
margin: 4px 0;
}
""")
# 創建圖片網格容器
self.images_grid_widget = QWidget()
self.images_grid_layout = QGridLayout(self.images_grid_widget)
self.images_grid_layout.setSpacing(4)
self.images_grid_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
# 將部分添加到主布局
self.preview_layout.addWidget(self.button_widget) # 按鈕始終顯示
self.preview_layout.addWidget(self.drop_hint_label)
self.preview_layout.addWidget(self.images_grid_widget)
# 初始時隱藏圖片網格
self.images_grid_widget.hide()
# 設置滾動區域
self.preview_scroll.setWidget(self.preview_widget)
self.preview_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.preview_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.preview_scroll.setMinimumHeight(120) # 增加最小高度以容納按鈕
self.preview_scroll.setMaximumHeight(200) # 調整最大高度
self.preview_scroll.setWidgetResizable(True)
self.preview_scroll.setStyleSheet("""
QScrollArea {
border: 1px solid #464647;
border-radius: 4px;
background-color: #1e1e1e;
}
""")
layout.addWidget(self.preview_scroll)
def _create_buttons_in_area(self) -> None:
"""在統一區域內創建操作按鈕"""
self.button_widget = QWidget()
button_layout = QHBoxLayout(self.button_widget)
button_layout.setContentsMargins(0, 0, 0, 4)
button_layout.setSpacing(6)
# 選擇文件按鈕
self.file_button = QPushButton(t('buttons.selectFiles'))
self.file_button.clicked.connect(self.select_files)
# 剪貼板按鈕
self.paste_button = QPushButton(t('buttons.pasteClipboard'))
self.paste_button.clicked.connect(self.paste_from_clipboard)
# 清除按鈕
self.clear_button = QPushButton(t('buttons.clearAll'))
self.clear_button.clicked.connect(self.clear_all_images)
# 設置按鈕樣式(更緊湊)
button_style = """
QPushButton {
color: white;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-weight: bold;
font-size: 10px;
min-height: 24px;
}
QPushButton:hover {
opacity: 0.8;
}
"""
self.file_button.setStyleSheet(button_style + """
QPushButton {
background-color: #0e639c;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.paste_button.setStyleSheet(button_style + """
QPushButton {
background-color: #4caf50;
}
QPushButton:hover {
background-color: #45a049;
}
""")
self.clear_button.setStyleSheet(button_style + """
QPushButton {
background-color: #f44336;
color: #ffffff;
}
QPushButton:hover {
background-color: #d32f2f;
color: #ffffff;
}
""")
button_layout.addWidget(self.file_button)
button_layout.addWidget(self.paste_button)
button_layout.addWidget(self.clear_button)
button_layout.addStretch() # 左對齊按鈕
def select_files(self) -> None:
"""選擇文件對話框"""
files, _ = QFileDialog.getOpenFileNames(
self,
t('images.select'),
"",
"Image files (*.png *.jpg *.jpeg *.gif *.bmp *.webp);;All files (*)"
)
if files:
self._add_images(files)
def paste_from_clipboard(self) -> None:
"""從剪貼板粘貼圖片"""
clipboard = QApplication.clipboard()
mimeData = clipboard.mimeData()
if mimeData.hasImage():
image = clipboard.image()
if not image.isNull():
# 創建一個唯一的臨時文件名
temp_dir = Path.home() / ".cache" / "mcp-feedback-enhanced"
temp_dir.mkdir(parents=True, exist_ok=True)
timestamp = int(time.time() * 1000)
temp_file = temp_dir / f"clipboard_{timestamp}_{uuid.uuid4().hex[:8]}.png"
# 保存剪貼板圖片
if image.save(str(temp_file), "PNG"):
if os.path.getsize(temp_file) > 0:
self._add_images([str(temp_file)])
debug_log(f"從剪貼板成功粘貼圖片: {temp_file}")
else:
QMessageBox.warning(self, t('errors.warning'), t('errors.imageSaveEmpty', path=str(temp_file)))
else:
QMessageBox.warning(self, t('errors.warning'), t('errors.imageSaveFailed'))
else:
QMessageBox.warning(self, t('errors.warning'), t('errors.clipboardSaveFailed'))
elif mimeData.hasText():
# 檢查是否為圖片數據
text = mimeData.text()
if text.startswith('data:image/') or any(ext in text.lower() for ext in ['.png', '.jpg', '.jpeg', '.gif']):
QMessageBox.information(self, t('errors.info'), t('errors.noValidImage'))
else:
QMessageBox.information(self, t('errors.info'), t('errors.noImageContent'))
def clear_all_images(self) -> None:
"""清除所有圖片"""
if self.images:
reply = QMessageBox.question(
self, t('errors.confirmClearTitle'),
t('errors.confirmClearAll', count=len(self.images)),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
# 清理臨時文件
temp_files_cleaned = 0
for image_info in self.images.values():
file_path = image_info["path"]
if "clipboard_" in os.path.basename(file_path) and ".cache" in file_path:
try:
if os.path.exists(file_path):
os.remove(file_path)
temp_files_cleaned += 1
debug_log(f"已刪除臨時文件: {file_path}")
except Exception as e:
debug_log(f"刪除臨時文件失敗: {e}")
# 清除內存中的圖片數據
self.images.clear()
self._refresh_preview()
self._update_status()
self.images_changed.emit()
debug_log(f"已清除所有圖片,包括 {temp_files_cleaned} 個臨時文件")
def _add_images(self, file_paths: List[str]) -> None:
"""添加圖片"""
added_count = 0
for file_path in file_paths:
try:
debug_log(f"嘗試添加圖片: {file_path}")
if not os.path.exists(file_path):
debug_log(f"文件不存在: {file_path}")
continue
if not self._is_image_file(file_path):
debug_log(f"不是圖片文件: {file_path}")
continue
file_size = os.path.getsize(file_path)
debug_log(f"文件大小: {file_size} bytes")
# 更嚴格的大小限制1MB
if file_size > 1 * 1024 * 1024:
QMessageBox.warning(
self, t('errors.warning'),
t('errors.fileSizeExceeded', filename=os.path.basename(file_path), size=f"{file_size/1024/1024:.1f}")
)
continue
if file_size == 0:
QMessageBox.warning(self, t('errors.warning'), t('errors.emptyFile', filename=os.path.basename(file_path)))
continue
# 讀取圖片原始二進制數據
with open(file_path, 'rb') as f:
raw_data = f.read()
debug_log(f"讀取原始數據大小: {len(raw_data)} bytes")
if len(raw_data) == 0:
debug_log(f"讀取的數據為空!")
continue
# 再次檢查內存中的數據大小
if len(raw_data) > 1 * 1024 * 1024:
QMessageBox.warning(
self, t('errors.warning'),
t('errors.dataSizeExceeded', filename=os.path.basename(file_path))
)
continue
image_id = str(uuid.uuid4())
self.images[image_id] = {
"path": file_path,
"data": raw_data, # 直接保存原始二進制數據
"name": os.path.basename(file_path),
"size": file_size
}
added_count += 1
debug_log(f"圖片添加成功: {os.path.basename(file_path)}")
except Exception as e:
debug_log(f"添加圖片失敗: {e}")
QMessageBox.warning(self, t('errors.title'), t('errors.loadImageFailed', filename=os.path.basename(file_path), error=str(e)))
if added_count > 0:
debug_log(f"共添加 {added_count} 張圖片,當前總數: {len(self.images)}")
self._refresh_preview()
self._update_status()
self.images_changed.emit()
def _is_image_file(self, file_path: str) -> bool:
"""檢查是否為支援的圖片格式"""
extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
return Path(file_path).suffix.lower() in extensions
def _refresh_preview(self) -> None:
"""刷新預覽布局"""
# 清除現有預覽
while self.images_grid_layout.count():
child = self.images_grid_layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
# 根據圖片數量決定顯示內容
if len(self.images) == 0:
# 沒有圖片時,顯示拖拽提示
self.drop_hint_label.show()
self.images_grid_widget.hide()
else:
# 有圖片時,隱藏拖拽提示,顯示圖片網格
self.drop_hint_label.hide()
self.images_grid_widget.show()
# 重新添加圖片預覽
for i, (image_id, image_info) in enumerate(self.images.items()):
preview = ImagePreviewWidget(image_info["path"], image_id, self)
preview.remove_clicked.connect(self._remove_image)
row = i // 5
col = i % 5
self.images_grid_layout.addWidget(preview, row, col)
def _remove_image(self, image_id: str) -> None:
"""移除圖片"""
if image_id in self.images:
image_info = self.images[image_id]
# 如果是臨時文件(剪貼板圖片),則物理刪除文件
file_path = image_info["path"]
if "clipboard_" in os.path.basename(file_path) and ".cache" in file_path:
try:
if os.path.exists(file_path):
os.remove(file_path)
debug_log(f"已刪除臨時文件: {file_path}")
except Exception as e:
debug_log(f"刪除臨時文件失敗: {e}")
# 從內存中移除圖片數據
del self.images[image_id]
self._refresh_preview()
self._update_status()
self.images_changed.emit()
debug_log(f"已移除圖片: {image_info['name']}")
def _update_status(self) -> None:
"""更新狀態標籤"""
count = len(self.images)
if count == 0:
self.status_label.setText(t('images.status', count=0))
else:
total_size = sum(img["size"] for img in self.images.values())
# 格式化文件大小
if total_size > 1024 * 1024: # MB
size_mb = total_size / (1024 * 1024)
size_str = f"{size_mb:.1f} MB"
else: # KB
size_kb = total_size / 1024
size_str = f"{size_kb:.1f} KB"
self.status_label.setText(t('images.statusWithSize', count=count, size=size_str))
# 基本調試信息
debug_log(f"圖片狀態: {count} 張圖片,總大小: {size_str}")
def get_images_data(self) -> List[dict]:
"""獲取所有圖片的數據列表"""
images_data = []
for image_info in self.images.values():
images_data.append(image_info)
return images_data
def add_image_data(self, image_data: dict) -> None:
"""添加圖片數據(用於恢復界面時的圖片)"""
try:
# 檢查必要的字段
if not all(key in image_data for key in ['filename', 'data', 'size']):
debug_log("圖片數據格式不正確,缺少必要字段")
return
# 生成新的圖片ID
image_id = str(uuid.uuid4())
# 復制圖片數據
self.images[image_id] = image_data.copy()
# 刷新預覽
self._refresh_preview()
self._update_status()
self.images_changed.emit()
debug_log(f"成功恢復圖片: {image_data['filename']}")
except Exception as e:
debug_log(f"添加圖片數據失敗: {e}")
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
"""拖拽進入事件"""
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
if url.isLocalFile() and self._is_image_file(url.toLocalFile()):
event.acceptProposedAction()
self.drop_hint_label.setStyleSheet("""
QLabel {
border: 2px dashed #007acc;
border-radius: 6px;
background-color: #383838;
color: #007acc;
font-size: 11px;
}
""")
return
event.ignore()
def dragLeaveEvent(self, event) -> None:
"""拖拽離開事件"""
self.drop_hint_label.setStyleSheet("""
QLabel {
border: 2px dashed #464647;
border-radius: 6px;
background-color: #2d2d30;
color: #9e9e9e;
font-size: 11px;
}
""")
def dropEvent(self, event: QDropEvent) -> None:
"""拖拽放下事件"""
self.dragLeaveEvent(event)
files = []
for url in event.mimeData().urls():
if url.isLocalFile():
file_path = url.toLocalFile()
if self._is_image_file(file_path):
files.append(file_path)
if files:
self._add_images(files)
event.acceptProposedAction()
else:
QMessageBox.warning(self, t('errors.warning'), t('errors.dragInvalidFiles'))
def _cleanup_old_temp_files(self) -> None:
"""清理舊的臨時文件"""
try:
temp_dir = Path.home() / ".cache" / "interactive-feedback-mcp"
if temp_dir.exists():
cleaned_count = 0
for temp_file in temp_dir.glob("clipboard_*.png"):
try:
# 清理超過1小時的臨時文件
if temp_file.exists():
file_age = time.time() - temp_file.stat().st_mtime
if file_age > 3600: # 1小時 = 3600秒
temp_file.unlink()
cleaned_count += 1
except Exception as e:
debug_log(f"清理舊臨時文件失敗: {e}")
if cleaned_count > 0:
debug_log(f"清理了 {cleaned_count} 個舊的臨時文件")
except Exception as e:
debug_log(f"臨時文件清理過程出錯: {e}")
def update_texts(self) -> None:
"""更新界面文字(用於語言切換)"""
# 更新標題
if hasattr(self, 'title'):
self.title.setText(t('images.title'))
# 更新按鈕文字
if hasattr(self, 'file_button'):
self.file_button.setText(t('buttons.selectFiles'))
if hasattr(self, 'paste_button'):
self.paste_button.setText(t('buttons.pasteClipboard'))
if hasattr(self, 'clear_button'):
self.clear_button.setText(t('buttons.clearAll'))
# 更新拖拽區域文字
if hasattr(self, 'drop_hint_label'):
self.drop_hint_label.setText(t('images.dragHint'))
# 更新狀態文字
self._update_status()

View File

@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
智能文字編輯器
==============
支援智能 Ctrl+V 的文字輸入框能自動處理圖片貼上
"""
from PySide6.QtWidgets import QTextEdit, QApplication
from PySide6.QtCore import Qt, Signal
class SmartTextEdit(QTextEdit):
"""支援智能 Ctrl+V 的文字輸入框"""
image_paste_requested = Signal()
def __init__(self, parent=None):
super().__init__(parent)
def keyPressEvent(self, event):
"""處理按鍵事件,實現智能 Ctrl+V"""
if event.key() == Qt.Key_V and event.modifiers() == Qt.ControlModifier:
# 檢查剪貼簿是否有圖片
clipboard = QApplication.clipboard()
if clipboard.mimeData().hasImage():
# 如果有圖片,發送信號通知主窗口處理圖片貼上
self.image_paste_requested.emit()
# 不執行預設的文字貼上行為
return
else:
# 如果沒有圖片,執行正常的文字貼上
super().keyPressEvent(event)
else:
# 其他按鍵正常處理
super().keyPressEvent(event)

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI 窗口模組
============
包含各種窗口類別
"""
from .feedback_window import FeedbackWindow
from .config_manager import ConfigManager
from .command_executor import CommandExecutor
from .tab_manager import TabManager
__all__ = [
'FeedbackWindow',
'ConfigManager',
'CommandExecutor',
'TabManager'
]

View File

@ -0,0 +1,242 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
命令執行管理器
===============
負責處理命令執行輸出讀取和進程管理
"""
import os
import subprocess
import threading
import time
import queue
import select
import sys
from typing import Optional, Callable
from PySide6.QtCore import QObject, QTimer, Signal
from ...debug import gui_debug_log as debug_log
class CommandExecutor(QObject):
"""命令執行管理器"""
output_received = Signal(str) # 輸出接收信號
def __init__(self, project_dir: str, parent=None):
super().__init__(parent)
self.project_dir = project_dir
self.command_process: Optional[subprocess.Popen] = None
self.timer: Optional[QTimer] = None
self._output_queue: Optional[queue.Queue] = None
self._reader_thread: Optional[threading.Thread] = None
self._command_start_time: Optional[float] = None
def run_command(self, command: str) -> None:
"""執行命令"""
if not command.strip():
return
# 如果已經有命令在執行,先停止
if self.timer and self.timer.isActive():
self.terminate_command()
self.output_received.emit(f"$ {command}\n")
# 保存當前命令用於輸出過濾
self._last_command = command
try:
# 準備環境變數以避免不必要的輸出
env = os.environ.copy()
env['NO_UPDATE_NOTIFIER'] = '1'
env['NPM_CONFIG_UPDATE_NOTIFIER'] = 'false'
env['NPM_CONFIG_FUND'] = 'false'
env['NPM_CONFIG_AUDIT'] = 'false'
env['PYTHONUNBUFFERED'] = '1'
# 啟動進程
self.command_process = subprocess.Popen(
command,
shell=True,
cwd=self.project_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=0,
env=env,
universal_newlines=True
)
# 設置計時器來定期讀取輸出
if not self.timer:
self.timer = QTimer()
self.timer.timeout.connect(self._read_command_output)
self.timer.start(100) # 每100ms檢查一次
self._command_start_time = time.time()
debug_log(f"命令已啟動: {command}")
except Exception as e:
self.output_received.emit(f"錯誤: 無法執行命令 - {str(e)}\n")
debug_log(f"命令執行錯誤: {e}")
def terminate_command(self) -> None:
"""終止正在運行的命令"""
if self.command_process and self.command_process.poll() is None:
try:
self.command_process.terminate()
self.output_received.emit("命令已被用戶終止。\n")
debug_log("用戶終止了正在運行的命令")
# 停止計時器
if self.timer:
self.timer.stop()
except Exception as e:
debug_log(f"終止命令失敗: {e}")
self.output_received.emit(f"終止命令失敗: {e}\n")
else:
self.output_received.emit("沒有正在運行的命令可以終止。\n")
def _read_command_output(self) -> None:
"""讀取命令輸出(非阻塞方式)"""
if not self.command_process:
if self.timer:
self.timer.stop()
return
# 檢查進程是否還在運行
if self.command_process.poll() is None:
try:
if sys.platform == "win32":
# Windows 下使用隊列方式
try:
if not self._output_queue:
self._output_queue = queue.Queue()
self._reader_thread = threading.Thread(
target=self._read_process_output_thread,
daemon=True
)
self._reader_thread.start()
# 從隊列中獲取輸出(非阻塞)
try:
while True:
output = self._output_queue.get_nowait()
if output is None: # 進程結束信號
break
self.output_received.emit(output)
except queue.Empty:
pass # 沒有新輸出,繼續等待
except ImportError:
output = self.command_process.stdout.readline()
if output:
filtered_output = self._filter_command_output(output)
if filtered_output:
self.output_received.emit(filtered_output)
else:
# Unix/Linux/macOS 下使用 select
ready, _, _ = select.select([self.command_process.stdout], [], [], 0.1)
if ready:
output = self.command_process.stdout.readline()
if output:
filtered_output = self._filter_command_output(output)
if filtered_output:
self.output_received.emit(filtered_output)
# 檢查命令執行超時30秒
if self._command_start_time and time.time() - self._command_start_time > 30:
self.output_received.emit(f"\n⚠️ 命令執行超過30秒自動終止...")
self.terminate_command()
except Exception as e:
debug_log(f"讀取命令輸出錯誤: {e}")
else:
# 進程結束,停止計時器並讀取剩餘輸出
if self.timer:
self.timer.stop()
# 清理資源
self._cleanup_resources()
try:
# 讀取剩餘的輸出
remaining_output, _ = self.command_process.communicate(timeout=2)
if remaining_output and remaining_output.strip():
filtered_output = self._filter_command_output(remaining_output)
if filtered_output:
self.output_received.emit(filtered_output)
except subprocess.TimeoutExpired:
debug_log("讀取剩餘輸出超時")
except Exception as e:
debug_log(f"讀取剩餘輸出錯誤: {e}")
return_code = self.command_process.returncode
self.output_received.emit(f"\n進程結束,返回碼: {return_code}\n")
def _read_process_output_thread(self) -> None:
"""在背景線程中讀取進程輸出"""
try:
while self.command_process and self.command_process.poll() is None:
output = self.command_process.stdout.readline()
if output:
self._output_queue.put(output)
else:
break
# 進程結束信號
if self._output_queue:
self._output_queue.put(None)
except Exception as e:
debug_log(f"背景讀取線程錯誤: {e}")
def _filter_command_output(self, output: str) -> str:
"""過濾命令輸出,移除不必要的行"""
if not output:
return ""
# 要過濾的字串(避免干擾的輸出)
filter_patterns = [
"npm notice",
"npm WARN deprecated",
"npm fund",
"npm audit",
"found 0 vulnerabilities",
"Run `npm audit` for details",
"[##", # 進度條
"⸩ ░░░░░░░░░░░░░░░░" # 其他進度指示器
]
# 檢查是否需要過濾
for pattern in filter_patterns:
if pattern in output:
return ""
return output
def _cleanup_resources(self) -> None:
"""清理資源"""
if hasattr(self, '_output_queue') and self._output_queue:
self._output_queue = None
if hasattr(self, '_reader_thread') and self._reader_thread:
self._reader_thread = None
if hasattr(self, '_command_start_time') and self._command_start_time:
self._command_start_time = None
def cleanup(self) -> None:
"""清理所有資源"""
if self.command_process and self.command_process.poll() is None:
try:
self.command_process.terminate()
debug_log("已終止正在運行的命令")
except Exception as e:
debug_log(f"終止命令失敗: {e}")
if self.timer:
self.timer.stop()
self._cleanup_resources()

View File

@ -0,0 +1,79 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
配置管理器
===========
負責處理用戶配置的載入保存和管理
"""
import json
from pathlib import Path
from typing import Dict, Any
from ...debug import gui_debug_log as debug_log
class ConfigManager:
"""配置管理器"""
def __init__(self):
self._config_file = self._get_config_file_path()
self._config_cache = {}
self._load_config()
def _get_config_file_path(self) -> Path:
"""獲取配置文件路徑"""
config_dir = Path.home() / ".config" / "mcp-feedback-enhanced"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir / "ui_settings.json"
def _load_config(self) -> None:
"""載入配置"""
try:
if self._config_file.exists():
with open(self._config_file, 'r', encoding='utf-8') as f:
self._config_cache = json.load(f)
debug_log("配置文件載入成功")
else:
self._config_cache = {}
debug_log("配置文件不存在,使用預設配置")
except Exception as e:
debug_log(f"載入配置失敗: {e}")
self._config_cache = {}
def _save_config(self) -> None:
"""保存配置"""
try:
with open(self._config_file, 'w', encoding='utf-8') as f:
json.dump(self._config_cache, f, ensure_ascii=False, indent=2)
debug_log("配置文件保存成功")
except Exception as e:
debug_log(f"保存配置失敗: {e}")
def get(self, key: str, default: Any = None) -> Any:
"""獲取配置值"""
return self._config_cache.get(key, default)
def set(self, key: str, value: Any) -> None:
"""設置配置值"""
self._config_cache[key] = value
self._save_config()
def get_layout_mode(self) -> bool:
"""獲取佈局模式False=分離模式True=合併模式)"""
return self.get('combined_mode', False)
def set_layout_mode(self, combined_mode: bool) -> None:
"""設置佈局模式"""
self.set('combined_mode', combined_mode)
debug_log(f"佈局模式設置: {'合併模式' if combined_mode else '分離模式'}")
def get_language(self) -> str:
"""獲取語言設置"""
return self.get('language', 'zh-TW')
def set_language(self, language: str) -> None:
"""設置語言"""
self.set('language', language)
debug_log(f"語言設置: {language}")

View File

@ -0,0 +1,271 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回饋收集主窗口重構版
========================
簡化的主窗口專注於主要職責窗口管理和協調各組件
"""
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QTabWidget, QPushButton, QMessageBox
)
from PySide6.QtCore import Signal, Qt
from PySide6.QtGui import QKeySequence, QShortcut
from .config_manager import ConfigManager
from .tab_manager import TabManager
from ..utils import apply_widget_styles
from ...i18n import t, get_i18n_manager
from ...debug import gui_debug_log as debug_log
class FeedbackWindow(QMainWindow):
"""回饋收集主窗口(重構版)"""
language_changed = Signal()
def __init__(self, project_dir: str, summary: str):
super().__init__()
self.project_dir = project_dir
self.summary = summary
self.result = None
self.i18n = get_i18n_manager()
# 初始化組件
self.config_manager = ConfigManager()
self.combined_mode = self.config_manager.get_layout_mode()
# 設置UI
self._setup_ui()
self._setup_shortcuts()
self._connect_signals()
debug_log("主窗口初始化完成")
def _setup_ui(self) -> None:
"""設置用戶介面"""
self.setWindowTitle(t('app.title'))
self.setMinimumSize(1000, 800)
self.resize(1200, 900)
# 中央元件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(8)
main_layout.setContentsMargins(16, 8, 16, 12)
# 頂部專案目錄信息
self._create_project_header(main_layout)
# 分頁區域
self._create_tab_area(main_layout)
# 操作按鈕
self._create_action_buttons(main_layout)
# 應用深色主題
self._apply_dark_style()
def _create_project_header(self, layout: QVBoxLayout) -> None:
"""創建專案目錄頭部信息"""
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)
def _create_tab_area(self, layout: QVBoxLayout) -> None:
"""創建分頁區域"""
self.tab_widget = QTabWidget()
self.tab_widget.setMinimumHeight(500)
# 初始化分頁管理器
self.tab_manager = TabManager(
self.tab_widget,
self.project_dir,
self.summary,
self.combined_mode
)
# 創建分頁
self.tab_manager.create_tabs()
layout.addWidget(self.tab_widget, 1)
def _create_action_buttons(self, layout: QVBoxLayout) -> None:
"""創建操作按鈕"""
button_layout = QHBoxLayout()
button_layout.addStretch()
# 取消按鈕
self.cancel_button = QPushButton(t('buttons.cancel'))
self.cancel_button.clicked.connect(self._cancel_feedback)
self.cancel_button.setFixedSize(130, 40)
apply_widget_styles(self.cancel_button, "secondary_button")
button_layout.addWidget(self.cancel_button)
# 提交按鈕
self.submit_button = QPushButton(t('buttons.submit'))
self.submit_button.clicked.connect(self._submit_feedback)
self.submit_button.setFixedSize(160, 40)
self.submit_button.setDefault(True)
apply_widget_styles(self.submit_button, "success_button")
button_layout.addWidget(self.submit_button)
layout.addLayout(button_layout)
def _setup_shortcuts(self) -> None:
"""設置快捷鍵"""
# Ctrl+Enter 或 Cmd+Enter 提交回饋
submit_shortcut = QShortcut(QKeySequence("Ctrl+Return"), self)
submit_shortcut.activated.connect(self._submit_feedback)
# macOS 支援
submit_shortcut_mac = QShortcut(QKeySequence("Meta+Return"), self)
submit_shortcut_mac.activated.connect(self._submit_feedback)
# Escape 取消回饋
cancel_shortcut = QShortcut(QKeySequence(Qt.Key_Escape), self)
cancel_shortcut.activated.connect(self._cancel_feedback)
def _connect_signals(self) -> None:
"""連接信號"""
# 連接語言變更信號
self.language_changed.connect(self._refresh_ui_texts)
# 連接分頁管理器的信號
self.tab_manager.connect_signals(self)
def _apply_dark_style(self) -> None:
"""應用深色主題"""
self.setStyleSheet("""
QMainWindow {
background-color: #2b2b2b;
color: #ffffff;
}
QGroupBox {
font-weight: bold;
border: 2px solid #464647;
border-radius: 8px;
margin-top: 1ex;
padding: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
}
QTextEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 8px;
color: #ffffff;
}
QLineEdit {
background-color: #2d2d30;
border: 1px solid #464647;
border-radius: 4px;
padding: 8px;
color: #ffffff;
}
QTabWidget::pane {
border: 1px solid #464647;
border-radius: 4px;
}
QTabBar::tab {
background-color: #2d2d30;
color: #ffffff;
border: 1px solid #464647;
padding: 8px 16px;
margin-right: 2px;
}
QTabBar::tab:selected {
background-color: #007acc;
}
""")
def _on_layout_mode_change_requested(self, combined_mode: bool) -> None:
"""處理佈局模式變更請求"""
try:
# 保存當前內容
current_data = self.tab_manager.get_feedback_data()
# 保存新設置
self.combined_mode = combined_mode
self.config_manager.set_layout_mode(combined_mode)
# 重新創建分頁
self.tab_manager.set_layout_mode(combined_mode)
self.tab_manager.create_tabs()
# 恢復內容
self.tab_manager.restore_content(
current_data["interactive_feedback"],
current_data["command_logs"],
current_data["images"]
)
# 重新連接信號
self.tab_manager.connect_signals(self)
# 刷新UI文字
self._refresh_ui_texts()
debug_log(f"佈局模式已切換到: {'合併模式' if combined_mode else '分離模式'}")
except Exception as e:
debug_log(f"佈局模式切換失敗: {e}")
QMessageBox.warning(self, t('errors.title'), t('errors.interfaceReloadError', error=str(e)))
def _handle_image_paste_from_textarea(self) -> None:
"""處理從文字框智能貼上圖片的功能"""
if self.tab_manager.feedback_tab:
self.tab_manager.feedback_tab.handle_image_paste_from_textarea()
def _submit_feedback(self) -> None:
"""提交回饋"""
# 獲取所有回饋數據
data = self.tab_manager.get_feedback_data()
self.result = data
debug_log(f"回饋提交: 文字長度={len(data['interactive_feedback'])}, "
f"命令日誌長度={len(data['command_logs'])}, "
f"圖片數量={len(data['images'])}")
# 關閉窗口
self.close()
def _cancel_feedback(self) -> None:
"""取消回饋"""
reply = QMessageBox.question(
self, t('app.confirmCancel'),
t('app.confirmCancelMessage'),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.result = None
self.close()
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'))
# 更新分頁文字
self.tab_manager.update_tab_texts()
def closeEvent(self, event) -> None:
"""窗口關閉事件"""
# 清理分頁管理器
self.tab_manager.cleanup()
event.accept()
debug_log("主窗口已關閉")

View File

@ -0,0 +1,186 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
分頁管理器
==========
負責管理和創建各種分頁組件
"""
from typing import Dict, Any
from PySide6.QtWidgets import QTabWidget, QSplitter, QWidget, QVBoxLayout
from PySide6.QtCore import Signal, Qt
from ..tabs import FeedbackTab, SummaryTab, CommandTab, SettingsTab
from ..widgets import SmartTextEdit, ImageUploadWidget
from ...i18n import t
from ...debug import gui_debug_log as debug_log
class TabManager:
"""分頁管理器"""
def __init__(self, tab_widget: QTabWidget, project_dir: str, summary: str, combined_mode: bool):
self.tab_widget = tab_widget
self.project_dir = project_dir
self.summary = summary
self.combined_mode = combined_mode
# 分頁組件實例
self.feedback_tab = None
self.summary_tab = None
self.command_tab = None
self.settings_tab = None
self.combined_feedback_tab = None
def create_tabs(self) -> None:
"""創建所有分頁"""
# 清除現有分頁
self.tab_widget.clear()
if self.combined_mode:
# 合併模式回饋頁包含AI摘要
self._create_combined_feedback_tab()
self.tab_widget.addTab(self.combined_feedback_tab, t('tabs.feedback'))
else:
# 分離模式:分別的回饋和摘要頁
self.feedback_tab = FeedbackTab()
self.tab_widget.addTab(self.feedback_tab, t('tabs.feedback'))
self.summary_tab = SummaryTab(self.summary)
self.tab_widget.addTab(self.summary_tab, t('tabs.summary'))
# 命令分頁
self.command_tab = CommandTab(self.project_dir)
self.tab_widget.addTab(self.command_tab, t('tabs.command'))
# 設置分頁
self.settings_tab = SettingsTab(self.combined_mode)
self.tab_widget.addTab(self.settings_tab, t('tabs.language'))
debug_log(f"分頁創建完成,模式: {'合併' if self.combined_mode else '分離'}")
def _create_combined_feedback_tab(self) -> None:
"""創建合併模式的回饋分頁包含AI摘要"""
self.combined_feedback_tab = QWidget()
# 主布局
tab_layout = QVBoxLayout(self.combined_feedback_tab)
tab_layout.setSpacing(12)
tab_layout.setContentsMargins(16, 16, 16, 16)
# 使用垂直分割器管理 AI摘要、回饋輸入和圖片區域
main_splitter = QSplitter(Qt.Vertical)
main_splitter.setChildrenCollapsible(False)
# 創建AI摘要組件
self.summary_tab = SummaryTab(self.summary)
self.summary_tab.setMinimumHeight(150)
self.summary_tab.setMaximumHeight(300)
# 創建回饋輸入組件
self.feedback_tab = FeedbackTab()
# 添加到主分割器
main_splitter.addWidget(self.summary_tab)
main_splitter.addWidget(self.feedback_tab)
# 設置分割器比例 (摘要:回饋 = 2:3)
main_splitter.setStretchFactor(0, 2) # AI摘要區域
main_splitter.setStretchFactor(1, 3) # 回饋輸入區域(包含圖片)
main_splitter.setSizes([200, 400]) # 設置初始大小
tab_layout.addWidget(main_splitter, 1)
def update_tab_texts(self) -> None:
"""更新分頁標籤文字"""
if self.combined_mode:
# 合併模式:只有回饋、命令、設置
self.tab_widget.setTabText(0, t('tabs.feedback'))
self.tab_widget.setTabText(1, t('tabs.command'))
self.tab_widget.setTabText(2, t('tabs.language'))
else:
# 分離模式:回饋、摘要、命令、設置
self.tab_widget.setTabText(0, t('tabs.feedback'))
self.tab_widget.setTabText(1, t('tabs.summary'))
self.tab_widget.setTabText(2, t('tabs.command'))
self.tab_widget.setTabText(3, t('tabs.language'))
# 更新各分頁的內部文字
if self.feedback_tab:
self.feedback_tab.update_texts()
if self.summary_tab:
self.summary_tab.update_texts()
if self.command_tab:
self.command_tab.update_texts()
if self.settings_tab:
self.settings_tab.update_texts()
def get_feedback_data(self) -> Dict[str, Any]:
"""獲取回饋數據"""
result = {
"interactive_feedback": "",
"command_logs": "",
"images": []
}
# 獲取回饋文字和圖片
if self.feedback_tab:
result["interactive_feedback"] = self.feedback_tab.get_feedback_text()
result["images"] = self.feedback_tab.get_images_data()
# 獲取命令日誌
if self.command_tab:
result["command_logs"] = self.command_tab.get_command_logs()
return result
def restore_content(self, feedback_text: str, command_logs: str, images_data: list) -> None:
"""恢復內容(用於界面重新創建時)"""
try:
if self.feedback_tab and feedback_text:
if hasattr(self.feedback_tab, 'feedback_input'):
self.feedback_tab.feedback_input.setPlainText(feedback_text)
if self.command_tab and command_logs:
if hasattr(self.command_tab, 'command_output'):
self.command_tab.command_output.setPlainText(command_logs)
if self.feedback_tab and images_data:
if hasattr(self.feedback_tab, 'image_upload'):
for img_data in images_data:
try:
self.feedback_tab.image_upload.add_image_data(img_data)
except:
pass # 如果無法恢復圖片,忽略錯誤
debug_log("內容恢復完成")
except Exception as e:
debug_log(f"恢復內容失敗: {e}")
def connect_signals(self, parent) -> None:
"""連接信號"""
# 連接設置分頁的信號
if self.settings_tab:
if hasattr(parent, 'language_changed'):
self.settings_tab.language_changed.connect(parent.language_changed)
if hasattr(parent, '_on_layout_mode_change_requested'):
self.settings_tab.layout_mode_change_requested.connect(parent._on_layout_mode_change_requested)
# 連接回饋分頁的圖片貼上信號
if self.feedback_tab:
if hasattr(parent, '_handle_image_paste_from_textarea'):
self.feedback_tab.image_paste_requested.connect(parent._handle_image_paste_from_textarea)
def cleanup(self) -> None:
"""清理資源"""
if self.command_tab:
self.command_tab.cleanup()
debug_log("分頁管理器清理完成")
def set_layout_mode(self, combined_mode: bool) -> None:
"""設置佈局模式"""
self.combined_mode = combined_mode
if self.settings_tab:
self.settings_tab.set_layout_mode(combined_mode)

View File

@ -283,18 +283,26 @@ class I18nManager:
def get_language_display_name(self, language_code: str) -> str:
"""獲取語言的顯示名稱"""
# 從當前語言的翻譯中獲取
lang_key = f"languageNames.{language_code.replace('-', '').lower()}"
# 直接從當前語言的翻譯中獲取,避免遞歸
current_translations = self._translations.get(self._current_language, {})
# 根據語言代碼構建鍵值
lang_key = None
if language_code == 'zh-TW':
lang_key = 'languageNames.zhTw'
elif language_code == 'zh-CN':
lang_key = 'languageNames.zhCn'
elif language_code == 'en':
lang_key = 'languageNames.en'
else:
# 通用格式
lang_key = f"languageNames.{language_code.replace('-', '').lower()}"
display_name = self.t(lang_key)
if display_name != lang_key: # 如果找到了翻譯
return display_name
# 直接獲取翻譯,避免調用 self.t() 產生遞歸
if lang_key:
display_name = self._get_nested_value(current_translations, lang_key)
if display_name:
return display_name
# 回退到元資料中的顯示名稱
meta = self.get_language_info(language_code)

View File

@ -10,13 +10,17 @@
"title": "Interactive Feedback Collection",
"projectDirectory": "Project Directory",
"language": "Language",
"settings": "Settings"
"settings": "Settings",
"confirmCancel": "Confirm Cancel",
"confirmCancelMessage": "Are you sure you want to cancel feedback? All input content will be lost.",
"layoutChangeTitle": "Interface Layout Change",
"layoutChangeMessage": "Layout mode has been changed and requires reloading the interface to take effect.\nReload now?"
},
"tabs": {
"summary": "📋 AI Summary",
"feedback": "💬 Feedback",
"command": "⚡ Command",
"language": "🌐 Language Settings",
"language": "⚙️ Settings",
"images": "🖼️ Images"
},
"feedback": {
@ -55,6 +59,21 @@
"selector": "🌐 Language Selection",
"description": "Choose your preferred interface language. Language changes take effect immediately."
},
"settings": {
"title": "Application Settings",
"language": {
"title": "Language Settings",
"selector": "🌐 Language Selection",
"description": "Choose your preferred interface language. Language changes take effect immediately."
},
"layout": {
"title": "Interface Layout",
"combinedMode": "Combined Mode",
"combinedModeDescription": "Display AI summary and feedback on the same page for easy comparison",
"separateMode": "Separate Mode",
"separateModeDescription": "AI summary and feedback are in separate tabs"
}
},
"buttons": {
"submit": "Submit Feedback",
"cancel": "Cancel",
@ -85,7 +104,24 @@
"invalidFileType": "Unsupported file type",
"fileTooLarge": "File too large (max 1MB)"
},
"aiSummary": "AI Work Summary",
"errors": {
"title": "Error",
"warning": "Warning",
"info": "Information",
"interfaceReloadError": "Error occurred while reloading interface: {error}",
"imageSaveEmpty": "Saved image file is empty! Location: {path}",
"imageSaveFailed": "Image save failed!",
"clipboardSaveFailed": "Failed to save clipboard image!",
"noValidImage": "No valid image in clipboard!",
"noImageContent": "No image content in clipboard!",
"emptyFile": "Image {filename} is an empty file!",
"loadImageFailed": "Failed to load image {filename}:\n{error}",
"dragInvalidFiles": "Please drag valid image files!",
"confirmClearAll": "Are you sure you want to clear all {count} images?",
"confirmClearTitle": "Confirm Clear",
"fileSizeExceeded": "Image {filename} size is {size}MB, exceeding 1MB limit!\nRecommend using image editing software to compress before uploading.",
"dataSizeExceeded": "Image {filename} data size exceeds 1MB limit!"
},
"languageSelector": "🌐 Language",
"languageNames": {
"zhTw": "繁體中文",

View File

@ -10,13 +10,17 @@
"title": "交互式反馈收集",
"projectDirectory": "项目目录",
"language": "语言",
"settings": "设置"
"settings": "设置",
"confirmCancel": "确认取消",
"confirmCancelMessage": "确定要取消反馈吗?所有输入的内容将会丢失。",
"layoutChangeTitle": "界面布局变更",
"layoutChangeMessage": "布局模式已变更,需要重新加载界面才能生效。\n是否现在重新加载"
},
"tabs": {
"summary": "📋 AI 摘要",
"feedback": "💬 反馈",
"command": "⚡ 命令",
"language": "🌐 语言设置",
"language": "⚙️ 设置",
"images": "🖼️ 图片"
},
"feedback": {
@ -50,10 +54,20 @@
"paste_image_from_textarea": "已将图片从文本框智能贴到图片区域",
"images_clear": "清除所有图片"
},
"language": {
"settings": "语言设置",
"selector": "🌐 语言选择",
"description": "选择您偏好的界面语言。语言更改会立即生效。"
"settings": {
"title": "应用设置",
"language": {
"title": "语言设置",
"selector": "🌐 语言选择",
"description": "选择您偏好的界面语言。语言更改会立即生效。"
},
"layout": {
"title": "界面布局",
"combinedMode": "合并模式",
"combinedModeDescription": "将 AI 摘要和反馈放在同一页面,便于对照阅读",
"separateMode": "分离模式",
"separateModeDescription": "AI 摘要和反馈分别在不同页签"
}
},
"buttons": {
"submit": "提交反馈",
@ -85,6 +99,24 @@
"invalidFileType": "不支持的文件类型",
"fileTooLarge": "文件过大(最大 1MB"
},
"errors": {
"title": "错误",
"warning": "警告",
"info": "提示",
"interfaceReloadError": "重新加载界面时发生错误: {error}",
"imageSaveEmpty": "保存的图片文件为空!位置: {path}",
"imageSaveFailed": "图片保存失败!",
"clipboardSaveFailed": "无法保存剪贴板图片!",
"noValidImage": "剪贴板中没有有效的图片!",
"noImageContent": "剪贴板中没有图片内容!",
"emptyFile": "图片 {filename} 是空文件!",
"loadImageFailed": "无法加载图片 {filename}:\n{error}",
"dragInvalidFiles": "请拖拽有效的图片文件!",
"confirmClearAll": "确定要清除所有 {count} 张图片吗?",
"confirmClearTitle": "确认清除",
"fileSizeExceeded": "图片 {filename} 大小为 {size}MB超过 1MB 限制!\n建议使用图片编辑软件压缩后再上传。",
"dataSizeExceeded": "图片 {filename} 数据大小超过 1MB 限制!"
},
"aiSummary": "AI 工作摘要",
"languageSelector": "🌐 语言选择",
"languageNames": {

View File

@ -10,13 +10,17 @@
"title": "互動式回饋收集",
"projectDirectory": "專案目錄",
"language": "語言",
"settings": "設定"
"settings": "設定",
"confirmCancel": "確認取消",
"confirmCancelMessage": "確定要取消回饋嗎?所有輸入的內容將會遺失。",
"layoutChangeTitle": "界面佈局變更",
"layoutChangeMessage": "佈局模式已變更,需要重新載入界面才能生效。\n是否現在重新載入"
},
"tabs": {
"summary": "📋 AI 摘要",
"feedback": "💬 回饋",
"command": "⚡ 命令",
"language": "🌐 語言設置",
"language": "⚙️ 設置",
"images": "🖼️ 圖片"
},
"feedback": {
@ -24,13 +28,23 @@
"description": "請描述您對 AI 工作結果的想法、建議或需要修改的地方。",
"placeholder": "請在這裡輸入您的回饋、建議或問題...\n\n💡 小提示:\n• 按 Ctrl+Enter支援數字鍵盤可快速提交回饋\n• 按 Ctrl+V 可直接貼上剪貼簿圖片",
"emptyTitle": "回饋內容為空",
"emptyMessage": "請先輸入回饋內容再提交。您可以描述想法、建議或需要修改的地方。"
"emptyMessage": "請先輸入回饋內容再提交。您可以描述想法、建議或需要修改的地方。",
"input": "您的回饋"
},
"summary": {
"title": "AI 工作摘要",
"description": "以下是 AI 剛才為您完成的工作內容,請檢視並提供回饋。",
"testDescription": "以下是 AI 回復的訊息內容,請檢視並提供回饋。"
},
"command": {
"title": "命令執行",
"description": "您可以執行命令來驗證結果或收集更多資訊。",
"input": "命令",
"placeholder": "輸入要執行的命令...",
"output": "命令輸出"
"output": "命令輸出",
"outputPlaceholder": "命令輸出將顯示在這裡...",
"run": "執行",
"terminate": "終止"
},
"images": {
"title": "🖼️ 圖片附件(可選)",
@ -50,10 +64,20 @@
"paste_image_from_textarea": "已將圖片從文字框智能貼到圖片區域",
"images_clear": "清除所有圖片"
},
"language": {
"settings": "語言設置",
"selector": "🌐 語言選擇",
"description": "選擇您偏好的界面語言。語言變更會立即生效。"
"settings": {
"title": "應用設置",
"language": {
"title": "語言設置",
"selector": "🌐 語言選擇",
"description": "選擇您偏好的界面語言。語言變更會立即生效。"
},
"layout": {
"title": "界面佈局",
"combinedMode": "合併模式",
"combinedModeDescription": "將 AI 摘要和回饋放在同一頁面,便於對照閱讀",
"separateMode": "分離模式",
"separateModeDescription": "AI 摘要和回饋分別在不同頁籤"
}
},
"buttons": {
"submit": "提交回饋",
@ -85,8 +109,24 @@
"invalidFileType": "不支援的文件類型",
"fileTooLarge": "文件過大(最大 1MB"
},
"aiSummary": "AI 工作摘要",
"languageSelector": "🌐 語言選擇",
"errors": {
"title": "錯誤",
"warning": "警告",
"info": "提示",
"interfaceReloadError": "重新載入界面時發生錯誤: {error}",
"imageSaveEmpty": "保存的圖片文件為空!位置: {path}",
"imageSaveFailed": "圖片保存失敗!",
"clipboardSaveFailed": "無法保存剪貼板圖片!",
"noValidImage": "剪貼板中沒有有效的圖片!",
"noImageContent": "剪貼板中沒有圖片內容!",
"emptyFile": "圖片 {filename} 是空文件!",
"loadImageFailed": "無法載入圖片 {filename}:\n{error}",
"dragInvalidFiles": "請拖拽有效的圖片文件!",
"confirmClearAll": "確定要清除所有 {count} 張圖片嗎?",
"confirmClearTitle": "確認清除",
"fileSizeExceeded": "圖片 {filename} 大小為 {size}MB超過 1MB 限制!\n建議使用圖片編輯軟體壓縮後再上傳。",
"dataSizeExceeded": "圖片 {filename} 數據大小超過 1MB 限制!"
},
"languageNames": {
"zhTw": "繁體中文",
"en": "English",

View File

@ -15,6 +15,7 @@ Interactive Feedback MCP 的核心伺服器程式,提供用戶互動回饋功
作者: Fábio Ferreira (原作者)
增強: Minidoracat (Web UI, 圖片支援, 環境檢測)
重構: 模塊化設計
"""
import os
@ -237,8 +238,8 @@ def create_feedback_text(feedback_data: dict) -> str:
text_parts.append(f"=== 用戶回饋 ===\n{feedback_data['interactive_feedback']}")
# 命令執行日誌
if feedback_data.get("logs"):
text_parts.append(f"=== 命令執行日誌 ===\n{feedback_data['logs']}")
if feedback_data.get("command_logs"):
text_parts.append(f"=== 命令執行日誌 ===\n{feedback_data['command_logs']}")
# 圖片附件概要
if feedback_data.get("images"):
@ -369,12 +370,28 @@ def launch_gui(project_dir: str, summary: str) -> dict:
debug_log("啟動 Qt GUI 介面")
try:
from .feedback_ui import feedback_ui
return feedback_ui(project_dir, summary)
except ImportError as e:
debug_log(f"無法導入 feedback_ui 模組: {e}")
from .gui import feedback_ui
result = feedback_ui(project_dir, summary)
if result is None:
# 用戶取消
return {
"command_logs": "",
"interactive_feedback": "用戶取消了回饋。",
"images": []
}
# 轉換鍵名以保持向後兼容
return {
"logs": "",
"command_logs": result.get("command_logs", ""),
"interactive_feedback": result.get("interactive_feedback", ""),
"images": result.get("images", [])
}
except ImportError as e:
debug_log(f"無法導入 GUI 模組: {e}")
return {
"command_logs": "",
"interactive_feedback": f"Qt GUI 模組導入失敗: {str(e)}",
"images": []
}
@ -458,7 +475,7 @@ async def interactive_feedback(
feedback_items = []
# 添加文字回饋
if result.get("interactive_feedback") or result.get("logs") or result.get("images"):
if result.get("interactive_feedback") or result.get("command_logs") or result.get("images"):
feedback_text = create_feedback_text(result)
feedback_items.append(TextContent(type="text", text=feedback_text))
debug_log("文字回饋已添加")
@ -504,7 +521,7 @@ async def launch_web_ui_with_timeout(project_dir: str, summary: str, timeout: in
except ImportError as e:
debug_log(f"無法導入 Web UI 模組: {e}")
return {
"logs": "",
"command_logs": "",
"interactive_feedback": f"Web UI 模組導入失敗: {str(e)}",
"images": []
}
@ -559,7 +576,7 @@ async def _run_web_ui_session(project_dir: str, summary: str, timeout: int) -> d
# except UnicodeEncodeError:
# print(f"Feedback timeout ({timeout} seconds)")
return {
"logs": "",
"command_logs": "",
"interactive_feedback": f"回饋超時({timeout} 秒)",
"images": []
}
@ -572,7 +589,7 @@ async def _run_web_ui_session(project_dir: str, summary: str, timeout: int) -> d
# except UnicodeEncodeError:
# print(f"Web UI error: {e}")
return {
"logs": "",
"command_logs": "",
"interactive_feedback": f"錯誤: {str(e)}",
"images": []
}

View File

@ -32,7 +32,7 @@ from .i18n import t
# 嘗試導入 Qt GUI 模組
try:
from .feedback_ui import feedback_ui
from .gui import feedback_ui
QT_GUI_AVAILABLE = True
except ImportError as e:
debug_log(f"⚠️ 無法導入 Qt GUI 模組: {e}")