mirror of
https://github.com/Minidoracat/mcp-feedback-enhanced.git
synced 2025-07-27 10:42:25 +08:00
✨ 重構 GUI 模組,優化版面及設置功能選擇,強化多語系
This commit is contained in:
parent
6203a75aab
commit
48654d2c93
@ -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
25
src/mcp_feedback_enhanced/gui/__init__.py
Normal file
25
src/mcp_feedback_enhanced/gui/__init__.py
Normal 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']
|
42
src/mcp_feedback_enhanced/gui/main.py
Normal file
42
src/mcp_feedback_enhanced/gui/main.py
Normal 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
|
10
src/mcp_feedback_enhanced/gui/models/__init__.py
Normal file
10
src/mcp_feedback_enhanced/gui/models/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""
|
||||
GUI 資料模型模組
|
||||
===============
|
||||
|
||||
定義 GUI 相關的資料結構和型別。
|
||||
"""
|
||||
|
||||
from .feedback import FeedbackResult
|
||||
|
||||
__all__ = ['FeedbackResult']
|
17
src/mcp_feedback_enhanced/gui/models/feedback.py
Normal file
17
src/mcp_feedback_enhanced/gui/models/feedback.py
Normal 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]
|
17
src/mcp_feedback_enhanced/gui/styles/__init__.py
Normal file
17
src/mcp_feedback_enhanced/gui/styles/__init__.py
Normal 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'
|
||||
]
|
277
src/mcp_feedback_enhanced/gui/styles/themes.py
Normal file
277
src/mcp_feedback_enhanced/gui/styles/themes.py
Normal 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;
|
||||
}
|
||||
"""
|
20
src/mcp_feedback_enhanced/gui/tabs/__init__.py
Normal file
20
src/mcp_feedback_enhanced/gui/tabs/__init__.py
Normal 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'
|
||||
]
|
194
src/mcp_feedback_enhanced/gui/tabs/command_tab.py
Normal file
194
src/mcp_feedback_enhanced/gui/tabs/command_tab.py
Normal 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()
|
104
src/mcp_feedback_enhanced/gui/tabs/feedback_tab.py
Normal file
104
src/mcp_feedback_enhanced/gui/tabs/feedback_tab.py
Normal 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()
|
230
src/mcp_feedback_enhanced/gui/tabs/settings_tab.py
Normal file
230
src/mcp_feedback_enhanced/gui/tabs/settings_tab.py
Normal 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)
|
93
src/mcp_feedback_enhanced/gui/tabs/summary_tab.py
Normal file
93
src/mcp_feedback_enhanced/gui/tabs/summary_tab.py
Normal 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'))
|
14
src/mcp_feedback_enhanced/gui/utils/__init__.py
Normal file
14
src/mcp_feedback_enhanced/gui/utils/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""
|
||||
GUI 工具函數模組
|
||||
===============
|
||||
|
||||
包含各種輔助工具函數。
|
||||
"""
|
||||
|
||||
from .shortcuts import setup_shortcuts
|
||||
from .utils import apply_widget_styles
|
||||
|
||||
__all__ = [
|
||||
'setup_shortcuts',
|
||||
'apply_widget_styles'
|
||||
]
|
35
src/mcp_feedback_enhanced/gui/utils/shortcuts.py
Normal file
35
src/mcp_feedback_enhanced/gui/utils/shortcuts.py
Normal 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)
|
50
src/mcp_feedback_enhanced/gui/utils/utils.py
Normal file
50
src/mcp_feedback_enhanced/gui/utils/utils.py
Normal 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"
|
16
src/mcp_feedback_enhanced/gui/widgets/__init__.py
Normal file
16
src/mcp_feedback_enhanced/gui/widgets/__init__.py
Normal 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'
|
||||
]
|
95
src/mcp_feedback_enhanced/gui/widgets/image_preview.py
Normal file
95
src/mcp_feedback_enhanced/gui/widgets/image_preview.py
Normal 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)
|
523
src/mcp_feedback_enhanced/gui/widgets/image_upload.py
Normal file
523
src/mcp_feedback_enhanced/gui/widgets/image_upload.py
Normal 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()
|
37
src/mcp_feedback_enhanced/gui/widgets/text_edit.py
Normal file
37
src/mcp_feedback_enhanced/gui/widgets/text_edit.py
Normal 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)
|
20
src/mcp_feedback_enhanced/gui/window/__init__.py
Normal file
20
src/mcp_feedback_enhanced/gui/window/__init__.py
Normal 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'
|
||||
]
|
242
src/mcp_feedback_enhanced/gui/window/command_executor.py
Normal file
242
src/mcp_feedback_enhanced/gui/window/command_executor.py
Normal 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()
|
79
src/mcp_feedback_enhanced/gui/window/config_manager.py
Normal file
79
src/mcp_feedback_enhanced/gui/window/config_manager.py
Normal 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}")
|
271
src/mcp_feedback_enhanced/gui/window/feedback_window.py
Normal file
271
src/mcp_feedback_enhanced/gui/window/feedback_window.py
Normal 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("主窗口已關閉")
|
186
src/mcp_feedback_enhanced/gui/window/tab_manager.py
Normal file
186
src/mcp_feedback_enhanced/gui/window/tab_manager.py
Normal 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)
|
@ -283,17 +283,25 @@ 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: # 如果找到了翻譯
|
||||
# 直接獲取翻譯,避免調用 self.t() 產生遞歸
|
||||
if lang_key:
|
||||
display_name = self._get_nested_value(current_translations, lang_key)
|
||||
if display_name:
|
||||
return display_name
|
||||
|
||||
# 回退到元資料中的顯示名稱
|
||||
|
@ -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": "繁體中文",
|
||||
|
@ -10,13 +10,17 @@
|
||||
"title": "交互式反馈收集",
|
||||
"projectDirectory": "项目目录",
|
||||
"language": "语言",
|
||||
"settings": "设置"
|
||||
"settings": "设置",
|
||||
"confirmCancel": "确认取消",
|
||||
"confirmCancelMessage": "确定要取消反馈吗?所有输入的内容将会丢失。",
|
||||
"layoutChangeTitle": "界面布局变更",
|
||||
"layoutChangeMessage": "布局模式已变更,需要重新加载界面才能生效。\n是否现在重新加载?"
|
||||
},
|
||||
"tabs": {
|
||||
"summary": "📋 AI 摘要",
|
||||
"feedback": "💬 反馈",
|
||||
"command": "⚡ 命令",
|
||||
"language": "🌐 语言设置",
|
||||
"language": "⚙️ 设置",
|
||||
"images": "🖼️ 图片"
|
||||
},
|
||||
"feedback": {
|
||||
@ -50,11 +54,21 @@
|
||||
"paste_image_from_textarea": "已将图片从文本框智能贴到图片区域",
|
||||
"images_clear": "清除所有图片"
|
||||
},
|
||||
"settings": {
|
||||
"title": "应用设置",
|
||||
"language": {
|
||||
"settings": "语言设置",
|
||||
"title": "语言设置",
|
||||
"selector": "🌐 语言选择",
|
||||
"description": "选择您偏好的界面语言。语言更改会立即生效。"
|
||||
},
|
||||
"layout": {
|
||||
"title": "界面布局",
|
||||
"combinedMode": "合并模式",
|
||||
"combinedModeDescription": "将 AI 摘要和反馈放在同一页面,便于对照阅读",
|
||||
"separateMode": "分离模式",
|
||||
"separateModeDescription": "AI 摘要和反馈分别在不同页签"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"submit": "提交反馈",
|
||||
"cancel": "取消",
|
||||
@ -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": {
|
||||
|
@ -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,11 +64,21 @@
|
||||
"paste_image_from_textarea": "已將圖片從文字框智能貼到圖片區域",
|
||||
"images_clear": "清除所有圖片"
|
||||
},
|
||||
"settings": {
|
||||
"title": "應用設置",
|
||||
"language": {
|
||||
"settings": "語言設置",
|
||||
"title": "語言設置",
|
||||
"selector": "🌐 語言選擇",
|
||||
"description": "選擇您偏好的界面語言。語言變更會立即生效。"
|
||||
},
|
||||
"layout": {
|
||||
"title": "界面佈局",
|
||||
"combinedMode": "合併模式",
|
||||
"combinedModeDescription": "將 AI 摘要和回饋放在同一頁面,便於對照閱讀",
|
||||
"separateMode": "分離模式",
|
||||
"separateModeDescription": "AI 摘要和回饋分別在不同頁籤"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"submit": "提交回饋",
|
||||
"cancel": "取消",
|
||||
@ -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",
|
||||
|
@ -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 {
|
||||
"logs": "",
|
||||
"command_logs": "",
|
||||
"interactive_feedback": "用戶取消了回饋。",
|
||||
"images": []
|
||||
}
|
||||
|
||||
# 轉換鍵名以保持向後兼容
|
||||
return {
|
||||
"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": []
|
||||
}
|
||||
|
@ -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}")
|
||||
|
Loading…
x
Reference in New Issue
Block a user