mirror of
https://github.com/yuruotong1/autoMate.git
synced 2025-12-26 05:16:21 +08:00
388 lines
14 KiB
Python
388 lines
14 KiB
Python
"""
|
|
Main application window
|
|
"""
|
|
import os
|
|
import keyboard
|
|
from pathlib import Path
|
|
from PyQt6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QLabel, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem,
|
|
QTextEdit, QSplitter, QMessageBox, QHeaderView, QDialog, QSystemTrayIcon)
|
|
from PyQt6.QtCore import Qt, pyqtSlot, QSize
|
|
from PyQt6.QtGui import QPixmap, QIcon, QTextCursor, QTextCharFormat, QColor
|
|
|
|
from xbrain.utils.config import Config
|
|
from auto_control.agent.vision_agent import VisionAgent
|
|
from util.download_weights import OMNI_PARSER_DIR
|
|
|
|
from ui.theme import apply_theme
|
|
from ui.settings_dialog import SettingsDialog
|
|
from ui.agent_worker import AgentWorker
|
|
from ui.tray_icon import StatusTrayIcon
|
|
from ui.hotkey_edit import DEFAULT_STOP_HOTKEY
|
|
|
|
# Intro text for application
|
|
INTRO_TEXT = '''
|
|
Based on Omniparser to control desktop!
|
|
'''
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""Main application window"""
|
|
|
|
def __init__(self, args):
|
|
super().__init__()
|
|
self.args = args
|
|
|
|
# Initialize state
|
|
self.state = self.setup_initial_state()
|
|
|
|
# Initialize Agent
|
|
self.vision_agent = VisionAgent(
|
|
yolo_model_path=os.path.join(OMNI_PARSER_DIR, "icon_detect", "model.pt")
|
|
)
|
|
|
|
# Create tray icon
|
|
self.setup_tray_icon()
|
|
|
|
self.setWindowTitle("autoMate")
|
|
self.setMinimumSize(1200, 800)
|
|
|
|
self.init_ui()
|
|
self.apply_theme()
|
|
|
|
# Register hotkey handler
|
|
self.hotkey_handler = None
|
|
self.register_stop_hotkey()
|
|
|
|
# Print startup information
|
|
print(f"\n\n🚀 PyQt6 application launched")
|
|
|
|
def setup_tray_icon(self):
|
|
"""Setup system tray icon"""
|
|
# Create or load icon
|
|
try:
|
|
script_dir = Path(__file__).parent
|
|
|
|
# Use logo.png as icon
|
|
image_path = script_dir.parent / "imgs" / "logo.png"
|
|
# Load image and create suitable icon size
|
|
pixmap = QPixmap(str(image_path))
|
|
# Resize to suitable icon size
|
|
icon_pixmap = pixmap.scaled(32, 32, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
|
app_icon = QIcon(icon_pixmap)
|
|
# Set application icon
|
|
self.setWindowIcon(app_icon)
|
|
|
|
# Create system tray icon
|
|
self.tray_icon = StatusTrayIcon(app_icon, self)
|
|
self.tray_icon.show()
|
|
|
|
except Exception as e:
|
|
print(f"Error setting up tray icon: {e}")
|
|
self.tray_icon = None
|
|
|
|
def setup_initial_state(self):
|
|
"""Set up initial state"""
|
|
state = {}
|
|
|
|
# Load data from config
|
|
config = Config()
|
|
if config.OPENAI_API_KEY:
|
|
state["api_key"] = config.OPENAI_API_KEY
|
|
else:
|
|
state["api_key"] = ""
|
|
|
|
if config.OPENAI_BASE_URL:
|
|
state["base_url"] = config.OPENAI_BASE_URL
|
|
else:
|
|
state["base_url"] = "https://api.openai.com/v1"
|
|
|
|
if config.OPENAI_MODEL:
|
|
state["model"] = config.OPENAI_MODEL
|
|
else:
|
|
state["model"] = "gpt-4o"
|
|
|
|
# Default to light theme
|
|
state["theme"] = "Light"
|
|
|
|
# Default stop hotkey
|
|
state["stop_hotkey"] = DEFAULT_STOP_HOTKEY
|
|
|
|
state["messages"] = []
|
|
state["chatbox_messages"] = []
|
|
state["auth_validated"] = False
|
|
state["responses"] = {}
|
|
state["tools"] = {}
|
|
state["tasks"] = []
|
|
state["only_n_most_recent_images"] = 2
|
|
state["stop"] = False
|
|
|
|
return state
|
|
|
|
def register_stop_hotkey(self):
|
|
"""Register the global stop hotkey"""
|
|
# First unregister any existing hotkey
|
|
if self.hotkey_handler:
|
|
try:
|
|
keyboard.unhook_all()
|
|
self.hotkey_handler = None
|
|
except:
|
|
pass
|
|
|
|
# Get the current hotkey from state
|
|
hotkey = self.state.get("stop_hotkey", DEFAULT_STOP_HOTKEY)
|
|
|
|
# Check if hotkey is valid
|
|
if not hotkey:
|
|
return
|
|
|
|
try:
|
|
# Register new hotkey
|
|
self.hotkey_handler = keyboard.add_hotkey(hotkey, self.handle_stop_hotkey)
|
|
print(f"Registered stop hotkey: {hotkey}")
|
|
except Exception as e:
|
|
print(f"Error registering hotkey '{hotkey}': {e}")
|
|
|
|
def handle_stop_hotkey(self):
|
|
"""Handle stop hotkey press"""
|
|
print("Stop hotkey pressed!")
|
|
self.state["stop"] = True
|
|
|
|
# Show brief notification
|
|
if hasattr(self, 'tray_icon') and self.tray_icon is not None:
|
|
self.tray_icon.showMessage("autoMate", "Stopping automation...", QSystemTrayIcon.MessageIcon.Information, 1000)
|
|
|
|
def apply_theme(self):
|
|
"""Apply the current theme to the application"""
|
|
theme_name = self.state.get("theme", "Light")
|
|
apply_theme(self, theme_name)
|
|
|
|
def init_ui(self):
|
|
"""Initialize UI components"""
|
|
central_widget = QWidget()
|
|
main_layout = QVBoxLayout(central_widget)
|
|
|
|
# Load top image
|
|
header_layout = QVBoxLayout()
|
|
try:
|
|
script_dir = Path(__file__).parent
|
|
image_path = script_dir.parent.parent / "imgs" / "header_bar_thin.png"
|
|
if image_path.exists():
|
|
pixmap = QPixmap(str(image_path))
|
|
header_label = QLabel()
|
|
header_label.setPixmap(pixmap.scaledToWidth(self.width()))
|
|
header_layout.addWidget(header_label)
|
|
except Exception as e:
|
|
print(f"Failed to load header image: {e}")
|
|
|
|
title_label = QLabel("autoMate")
|
|
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
font = title_label.font()
|
|
font.setPointSize(20)
|
|
title_label.setFont(font)
|
|
header_layout.addWidget(title_label)
|
|
|
|
# Introduction text
|
|
intro_label = QLabel(INTRO_TEXT)
|
|
intro_label.setWordWrap(True)
|
|
font = intro_label.font()
|
|
font.setPointSize(12)
|
|
intro_label.setFont(font)
|
|
|
|
# Settings button and clear chat button (at top)
|
|
top_buttons_layout = QHBoxLayout()
|
|
self.settings_button = QPushButton("Settings")
|
|
self.settings_button.clicked.connect(self.open_settings_dialog)
|
|
self.clear_button = QPushButton("Clear Chat")
|
|
self.clear_button.clicked.connect(self.clear_chat)
|
|
top_buttons_layout.addWidget(self.settings_button)
|
|
top_buttons_layout.addWidget(self.clear_button)
|
|
top_buttons_layout.addStretch() # Add elastic space to left-align buttons
|
|
|
|
# Input area
|
|
input_layout = QHBoxLayout()
|
|
self.chat_input = QLineEdit()
|
|
self.chat_input.setPlaceholderText("Type a message to send to Omniparser + X ...")
|
|
# Send message on Enter key
|
|
self.chat_input.returnPressed.connect(self.process_input)
|
|
self.submit_button = QPushButton("Send")
|
|
self.submit_button.clicked.connect(self.process_input)
|
|
self.stop_button = QPushButton("Stop")
|
|
self.stop_button.clicked.connect(self.stop_process)
|
|
|
|
input_layout.addWidget(self.chat_input, 8)
|
|
input_layout.addWidget(self.submit_button, 1)
|
|
input_layout.addWidget(self.stop_button, 1)
|
|
|
|
# Main content area
|
|
content_splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
|
# Task list
|
|
task_widget = QWidget()
|
|
task_layout = QVBoxLayout(task_widget)
|
|
task_label = QLabel("Task List")
|
|
self.task_table = QTableWidget(0, 2)
|
|
self.task_table.setHorizontalHeaderLabels(["Status", "Task"])
|
|
self.task_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
task_layout.addWidget(task_label)
|
|
task_layout.addWidget(self.task_table)
|
|
|
|
# Chat area
|
|
chat_widget = QWidget()
|
|
chat_layout = QVBoxLayout(chat_widget)
|
|
chat_label = QLabel("Chat History")
|
|
self.chat_display = QTextEdit()
|
|
self.chat_display.setReadOnly(True)
|
|
chat_layout.addWidget(chat_label)
|
|
chat_layout.addWidget(self.chat_display)
|
|
|
|
# Add to splitter
|
|
content_splitter.addWidget(task_widget)
|
|
content_splitter.addWidget(chat_widget)
|
|
content_splitter.setSizes([int(self.width() * 0.2), int(self.width() * 0.8)])
|
|
|
|
# Add all components to main layout
|
|
main_layout.addLayout(header_layout)
|
|
main_layout.addWidget(intro_label)
|
|
main_layout.addLayout(top_buttons_layout) # Add top button area
|
|
main_layout.addLayout(input_layout)
|
|
main_layout.addWidget(content_splitter, 1) # 1 is the stretch factor
|
|
|
|
self.setCentralWidget(central_widget)
|
|
|
|
def open_settings_dialog(self):
|
|
"""Open settings dialog"""
|
|
dialog = SettingsDialog(self, self.state)
|
|
result = dialog.exec()
|
|
|
|
if result == QDialog.DialogCode.Accepted:
|
|
# Get and apply new settings
|
|
settings = dialog.get_settings()
|
|
|
|
# Check if stop hotkey changed
|
|
old_hotkey = self.state.get("stop_hotkey", DEFAULT_STOP_HOTKEY)
|
|
new_hotkey = settings["stop_hotkey"]
|
|
|
|
self.state["model"] = settings["model"]
|
|
self.state["base_url"] = settings["base_url"]
|
|
self.state["api_key"] = settings["api_key"]
|
|
self.state["stop_hotkey"] = new_hotkey
|
|
|
|
# Update theme if changed
|
|
if settings["theme"] != self.state.get("theme", "Light"):
|
|
self.state["theme"] = settings["theme"]
|
|
self.apply_theme()
|
|
|
|
if settings["screen_region"]:
|
|
self.state["screen_region"] = settings["screen_region"]
|
|
|
|
# Update hotkey if changed
|
|
if old_hotkey != new_hotkey:
|
|
self.register_stop_hotkey()
|
|
|
|
def process_input(self):
|
|
"""Process user input"""
|
|
user_input = self.chat_input.text()
|
|
if not user_input.strip():
|
|
return
|
|
|
|
# Clear input box
|
|
self.chat_input.clear()
|
|
|
|
# Show hotkey reminder
|
|
hotkey = self.state.get("stop_hotkey", DEFAULT_STOP_HOTKEY)
|
|
QMessageBox.information(self, "Automation Starting",
|
|
f"Automation will start now. You can press {hotkey} to stop at any time.")
|
|
|
|
# Minimize main window
|
|
self.showMinimized()
|
|
|
|
# Create and start worker thread
|
|
self.worker = AgentWorker(user_input, self.state, self.vision_agent)
|
|
self.worker.update_signal.connect(self.update_ui)
|
|
self.worker.error_signal.connect(self.handle_error)
|
|
|
|
# Connect signals to tray icon if available
|
|
if hasattr(self, 'tray_icon') and self.tray_icon is not None:
|
|
self.worker.status_signal.connect(self.tray_icon.update_status)
|
|
self.worker.task_signal.connect(self.tray_icon.update_task)
|
|
|
|
self.worker.start()
|
|
|
|
def handle_error(self, error_message):
|
|
"""Handle error messages"""
|
|
# Restore main window to show the error
|
|
self.showNormal()
|
|
self.activateWindow()
|
|
|
|
# Show error message
|
|
QMessageBox.warning(self, "Connection Error",
|
|
f"Error connecting to AI service:\n{error_message}\n\nPlease check your network connection and API settings.")
|
|
|
|
@pyqtSlot(list, list)
|
|
def update_ui(self, chatbox_messages, tasks):
|
|
"""Update UI display"""
|
|
# Update chat display
|
|
self.chat_display.clear()
|
|
|
|
for msg in chatbox_messages:
|
|
role = msg["role"]
|
|
content = msg["content"]
|
|
|
|
# Set different formats based on role
|
|
format = QTextCharFormat()
|
|
if role == "user":
|
|
format.setForeground(QColor(0, 0, 255)) # Blue for user
|
|
self.chat_display.append("You:")
|
|
else:
|
|
format.setForeground(QColor(0, 128, 0)) # Green for AI
|
|
self.chat_display.append("AI:")
|
|
|
|
# Add content
|
|
cursor = self.chat_display.textCursor()
|
|
cursor.movePosition(QTextCursor.MoveOperation.End)
|
|
|
|
# Special handling for HTML content
|
|
if "<" in content and ">" in content:
|
|
self.chat_display.insertHtml(content)
|
|
self.chat_display.append("") # Add empty line
|
|
else:
|
|
self.chat_display.append(content)
|
|
self.chat_display.append("") # Add empty line
|
|
|
|
# Scroll to bottom
|
|
self.chat_display.verticalScrollBar().setValue(
|
|
self.chat_display.verticalScrollBar().maximum()
|
|
)
|
|
|
|
# Update task table
|
|
self.task_table.setRowCount(len(tasks))
|
|
for i, (status, task) in enumerate(tasks):
|
|
self.task_table.setItem(i, 0, QTableWidgetItem(status))
|
|
self.task_table.setItem(i, 1, QTableWidgetItem(task))
|
|
|
|
def stop_process(self):
|
|
"""Stop processing"""
|
|
self.state["stop"] = True
|
|
|
|
def clear_chat(self):
|
|
"""Clear chat history"""
|
|
self.state["messages"] = []
|
|
self.state["chatbox_messages"] = []
|
|
self.state["responses"] = {}
|
|
self.state["tools"] = {}
|
|
self.state["tasks"] = []
|
|
|
|
self.chat_display.clear()
|
|
self.task_table.setRowCount(0)
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle window close event"""
|
|
# This allows the app to continue running in the system tray
|
|
# when the main window is closed
|
|
if hasattr(self, 'tray_icon') and self.tray_icon is not None and self.tray_icon.isVisible():
|
|
self.hide()
|
|
event.ignore()
|
|
else:
|
|
# Clean up on exit
|
|
keyboard.unhook_all()
|
|
event.accept() |