from __future__ import annotations from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTabWidget, QWidget, QFormLayout, QGroupBox, QMessageBox, QCheckBox ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QFont class SettingsDialog(QDialog): def __init__(self, config, parent=None): super().__init__(parent) self._config = config self.setWindowTitle("Settings") self.setMinimumWidth(480) self.setMinimumHeight(440) self._setup_ui() self._load_values() def _setup_ui(self): root = QVBoxLayout(self) root.setContentsMargins(16, 16, 16, 16) root.setSpacing(12) tabs = QTabWidget() tabs.addTab(self._build_api_tab(), "API Keys") tabs.addTab(self._build_brokers_tab(), "Brokers") tabs.addTab(self._build_general_tab(), "General") root.addWidget(tabs) btn_row = QHBoxLayout() btn_row.addStretch() save_btn = QPushButton("Save") save_btn.setObjectName("primary_btn") save_btn.clicked.connect(self._save) cancel_btn = QPushButton("Cancel") cancel_btn.clicked.connect(self.reject) btn_row.addWidget(cancel_btn) btn_row.addWidget(save_btn) root.addLayout(btn_row) def _build_api_tab(self) -> QWidget: tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(8, 8, 8, 8) layout.setSpacing(12) # Anthropic anthropic_box = QGroupBox("Anthropic (Claude AI)") anthropic_form = QFormLayout(anthropic_box) self._anthropic_key = QLineEdit() self._anthropic_key.setEchoMode(QLineEdit.EchoMode.Password) self._anthropic_key.setPlaceholderText("sk-ant-…") test_anthropic = QPushButton("Test") test_anthropic.setMaximumWidth(60) test_anthropic.clicked.connect(self._test_anthropic) row = QHBoxLayout() row.addWidget(self._anthropic_key) row.addWidget(test_anthropic) anthropic_form.addRow("API Key:", row) anthropic_form.addRow("", QLabel("Get key: console.anthropic.com | Pay per token")) layout.addWidget(anthropic_box) # NewsAPI news_box = QGroupBox("NewsAPI") news_form = QFormLayout(news_box) self._news_key = QLineEdit() self._news_key.setEchoMode(QLineEdit.EchoMode.Password) self._news_key.setPlaceholderText("Your NewsAPI key…") test_news = QPushButton("Test") test_news.setMaximumWidth(60) test_news.clicked.connect(self._test_news) row2 = QHBoxLayout() row2.addWidget(self._news_key) row2.addWidget(test_news) news_form.addRow("API Key:", row2) news_form.addRow("", QLabel("Get key: newsapi.org | 100 req/day free")) layout.addWidget(news_box) layout.addStretch() return tab def _build_brokers_tab(self) -> QWidget: tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(8, 8, 8, 8) layout.setSpacing(12) # Schwab schwab_box = QGroupBox("Charles Schwab") schwab_form = QFormLayout(schwab_box) self._schwab_key = QLineEdit() self._schwab_key.setPlaceholderText("App Key…") self._schwab_secret = QLineEdit() self._schwab_secret.setEchoMode(QLineEdit.EchoMode.Password) self._schwab_secret.setPlaceholderText("App Secret…") test_schwab = QPushButton("Test Connection") test_schwab.clicked.connect(self._test_schwab) schwab_form.addRow("App Key:", self._schwab_key) schwab_form.addRow("App Secret:", self._schwab_secret) schwab_form.addRow("", test_schwab) schwab_form.addRow("", QLabel("Register at: developer.schwab.com\nCallback URL: https://127.0.0.1")) layout.addWidget(schwab_box) # Robinhood rh_box = QGroupBox("Robinhood") rh_form = QFormLayout(rh_box) self._rh_username = QLineEdit() self._rh_username.setPlaceholderText("Email…") self._rh_password = QLineEdit() self._rh_password.setEchoMode(QLineEdit.EchoMode.Password) self._rh_password.setPlaceholderText("Password…") test_rh = QPushButton("Test Login") test_rh.clicked.connect(self._test_robinhood) rh_form.addRow("Username:", self._rh_username) rh_form.addRow("Password:", self._rh_password) rh_form.addRow("", test_rh) layout.addWidget(rh_box) layout.addStretch() return tab def _build_general_tab(self) -> QWidget: tab = QWidget() layout = QVBoxLayout(tab) layout.setContentsMargins(8, 8, 8, 8) layout.setSpacing(12) general_box = QGroupBox("Appearance & Behavior") general_form = QFormLayout(general_box) self._theme_dark = QCheckBox("Dark theme") self._theme_dark.setChecked(True) general_form.addRow("Theme:", self._theme_dark) layout.addWidget(general_box) layout.addStretch() return tab def _load_values(self): self._anthropic_key.setText(self._config.get_setting("anthropic_api_key") or self._config.get("ANTHROPIC_API_KEY")) self._news_key.setText(self._config.get_setting("news_api_key") or self._config.get("NEWS_API_KEY")) self._schwab_key.setText(self._config.get_setting("schwab_app_key") or self._config.get("SCHWAB_APP_KEY")) self._schwab_secret.setText(self._config.get_setting("schwab_app_secret") or self._config.get("SCHWAB_APP_SECRET")) self._rh_username.setText(self._config.get_setting("robinhood_username") or self._config.get("ROBINHOOD_USERNAME")) self._rh_password.setText(self._config.get_setting("robinhood_password") or self._config.get("ROBINHOOD_PASSWORD")) theme = self._config.get_setting("theme", "dark") self._theme_dark.setChecked(theme == "dark") def _save(self): self._config.set_setting("anthropic_api_key", self._anthropic_key.text().strip()) self._config.set_setting("news_api_key", self._news_key.text().strip()) self._config.set_setting("schwab_app_key", self._schwab_key.text().strip()) self._config.set_setting("schwab_app_secret", self._schwab_secret.text().strip()) self._config.set_setting("robinhood_username", self._rh_username.text().strip()) self._config.set_setting("robinhood_password", self._rh_password.text().strip()) self._config.set_setting("theme", "dark" if self._theme_dark.isChecked() else "light") QMessageBox.information(self, "Saved", "Settings saved. Restart to apply theme changes.") self.accept() def _test_anthropic(self): key = self._anthropic_key.text().strip() if not key: QMessageBox.warning(self, "Test Failed", "Enter an API key first.") return try: import anthropic client = anthropic.Anthropic(api_key=key) response = client.messages.create( model="claude-haiku-4-5-20251001", max_tokens=10, messages=[{"role": "user", "content": "ping"}], ) QMessageBox.information(self, "Success", "Anthropic API key is valid!") except Exception as e: QMessageBox.critical(self, "Test Failed", str(e)) def _test_news(self): key = self._news_key.text().strip() if not key: QMessageBox.warning(self, "Test Failed", "Enter an API key first.") return try: from newsapi import NewsApiClient client = NewsApiClient(api_key=key) result = client.get_top_headlines(language="en", page_size=1) if result.get("status") == "ok": QMessageBox.information(self, "Success", "NewsAPI key is valid!") else: QMessageBox.critical(self, "Test Failed", result.get("message", "Unknown error")) except Exception as e: QMessageBox.critical(self, "Test Failed", str(e)) def _test_schwab(self): key = self._schwab_key.text().strip() secret = self._schwab_secret.text().strip() if not key or not secret: QMessageBox.warning(self, "Test Failed", "Enter both App Key and Secret first.") return try: from brokers.schwab import SchwabBroker broker = SchwabBroker(key, secret) broker.connect() QMessageBox.information(self, "Success", "Schwab connected! Browser may open for OAuth if first time.") except Exception as e: QMessageBox.critical(self, "Test Failed", str(e)) def _test_robinhood(self): user = self._rh_username.text().strip() pwd = self._rh_password.text().strip() if not user or not pwd: QMessageBox.warning(self, "Test Failed", "Enter username and password first.") return try: from brokers.robinhood import RobinhoodBroker broker = RobinhoodBroker(user, pwd) broker.connect() QMessageBox.information(self, "Success", "Robinhood login successful!") except Exception as e: QMessageBox.critical(self, "Test Failed", str(e))