#!/usr/bin/env python3
"""
FGD Stack Pro GUI – Grok-Only by Default + Safe LLM Switching
"""
from PyQt6.QtWidgets import (
QApplication,
QWidget,
QVBoxLayout,
QHBoxLayout,
QPushButton,
QLabel,
QLineEdit,
QComboBox,
QTextEdit,
QFileDialog,
QMessageBox,
QGroupBox,
)
from PyQt6.QtCore import QTimer, Qt
from PyQt6.QtGui import QFont, QTextCursor
from pathlib import Path
import sys
import yaml
import subprocess
import os
from dotenv import load_dotenv
# Load .env from project root
load_dotenv()
class FGDGUI(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("FGD Stack Pro v2.4")
self.resize(960, 720)
self.layout = QVBoxLayout()
self.layout.setSpacing(18)
self.layout.setContentsMargins(28, 28, 28, 28)
self.setLayout(self.layout)
self.process = None
self.log_file = None
header = QLabel("FGD Stack Pro")
header.setAlignment(Qt.AlignmentFlag.AlignCenter)
header.setObjectName("header")
header.setFont(QFont("Segoe UI", 20, QFont.Weight.Bold))
self.layout.addWidget(header)
# Directory
dir_group = QGroupBox("Project Directory")
dir_layout = QVBoxLayout()
self.path_edit = QLineEdit()
browse = QPushButton("Browse")
browse.clicked.connect(self.browse)
h = QHBoxLayout()
h.addWidget(self.path_edit)
h.addWidget(browse)
dir_layout.addLayout(h)
dir_group.setLayout(dir_layout)
self.layout.addWidget(dir_group)
# Provider
provider_group = QGroupBox("LLM Provider")
provider_layout = QVBoxLayout()
self.provider = QComboBox()
self.provider.addItems(["grok", "openai", "claude", "ollama"])
self.provider.setCurrentText("grok") # GROK DEFAULT
provider_layout.addWidget(self.provider)
provider_group.setLayout(provider_layout)
self.layout.addWidget(provider_group)
# Start/Stop
control_group = QGroupBox("Server Control")
control_layout = QVBoxLayout()
self.start_btn = QPushButton("Start Server")
self.start_btn.clicked.connect(self.toggle_server)
control_layout.addWidget(self.start_btn)
self.status = QLabel("Status: Ready")
self.status.setObjectName("status")
control_layout.addWidget(self.status)
control_group.setLayout(control_layout)
self.layout.addWidget(control_group)
# Logs
logs_group = QGroupBox("Live Logs")
logs_layout = QVBoxLayout()
self.log_view = QTextEdit()
self.log_view.setReadOnly(True)
self.log_view.setFont(QFont("Courier", 10))
filters = QHBoxLayout()
self.level = QComboBox()
self.level.addItems(["All", "INFO", "WARNING", "ERROR"])
filters.addWidget(QLabel("Level:"))
filters.addWidget(self.level)
self.search = QLineEdit()
self.search.setPlaceholderText("Search...")
filters.addWidget(self.search)
clear = QPushButton("Clear")
clear.clicked.connect(self.clear_filters)
filters.addWidget(clear)
logs_layout.addLayout(filters)
logs_layout.addWidget(self.log_view)
logs_group.setLayout(logs_layout)
self.layout.addWidget(logs_group)
# Timer
self.timer = QTimer()
self.timer.timeout.connect(self.update_logs)
self.timer.start(1000)
self.apply_dark_mode(True)
def browse(self):
dir_path = QFileDialog.getExistingDirectory(self, "Select Project")
if dir_path:
self.path_edit.setText(dir_path)
def toggle_server(self):
if self.process and self.process.poll() is None:
self.process.terminate()
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.kill()
self.process = None
self.status.setText("Status: Stopped")
self.start_btn.setText("Start Server")
else:
self.start_server()
def start_server(self):
dir_path = self.path_edit.text().strip()
if not dir_path or not Path(dir_path).exists():
self.status.setText("Status: Invalid directory")
return
provider = self.provider.currentText()
# BLOCK NON-GROK IF KEY MISSING
if provider != "grok":
key_map = {
"openai": "OPENAI_API_KEY",
"claude": "ANTHROPIC_API_KEY"
}
key_name = key_map.get(provider)
if key_name and not os.getenv(key_name):
reply = QMessageBox.question(
self, "Missing API Key",
f"{key_name} not found in .env\n\nUse Grok instead?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self.provider.setCurrentText("grok")
provider = "grok"
else:
self.status.setText("Status: Start cancelled")
return
# Generate config
self.log_file = Path(dir_path) / "fgd_server.log"
self.log_file.write_text("")
config = {
"watch_dir": dir_path,
"memory_file": str(Path(dir_path) / ".fgd_memory.json"),
"log_file": str(self.log_file),
"context_limit": 20,
"scan": {"max_dir_size_gb": 2, "max_files_per_scan": 5, "max_file_size_kb": 250},
"llm": {
"default_provider": provider,
"providers": {
"grok": {"model": "grok-beta", "base_url": "https://api.x.ai/v1"},
"openai": {"model": "gpt-4o-mini", "base_url": "https://api.openai.com/v1"},
"claude": {"model": "claude-3-5-sonnet-20241022", "base_url": "https://api.anthropic.com/v1"},
"ollama": {"model": "llama3", "base_url": "http://localhost:11434/v1"}
}
}
}
config_path = Path(dir_path) / "fgd_config.yaml"
config_path.write_text(yaml.dump(config))
# Start subprocess with env
env = os.environ.copy()
try:
self.process = subprocess.Popen(
[sys.executable, "mcp_backend.py", str(config_path)],
cwd=dir_path,
env=env
)
except Exception as exc:
self.status.setText(f"Status: Failed to start ({exc})")
self.start_btn.setText("Start Server")
self.process = None
return
self.status.setText(f"Status: Running ({provider})")
self.start_btn.setText("Stop Server")
def update_logs(self):
if self.process and self.process.poll() is not None:
self.status.setText("Status: Server stopped")
self.start_btn.setText("Start Server")
self.process = None
if not self.log_file or not self.log_file.exists():
return
try:
lines = self.log_file.read_text().splitlines()
level = self.level.currentText()
search = self.search.text().lower()
filtered = []
for line in lines:
if level != "All" and level not in line:
continue
if search and search not in line.lower():
continue
filtered.append(line)
if len(filtered) > 500:
filtered = filtered[-500:]
self.log_view.clear()
for line in filtered:
cursor = self.log_view.textCursor()
cursor.movePosition(QTextCursor.MoveOperation.End)
self.log_view.setTextCursor(cursor)
if "ERROR" in line:
self.log_view.setTextColor(Qt.GlobalColor.red)
elif "WARNING" in line:
self.log_view.setTextColor(Qt.GlobalColor.yellow)
else:
self.log_view.setTextColor(Qt.GlobalColor.white)
self.log_view.insertPlainText(line + "\n")
self.log_view.setTextColor(Qt.GlobalColor.white)
except:
pass
def clear_filters(self):
self.level.setCurrentIndex(0)
self.search.clear()
def apply_dark_mode(self, dark):
if dark:
self.setStyleSheet(
"""
QWidget {
background-color: #0d1117;
color: #f0f6fc;
font-family: 'Segoe UI', sans-serif;
}
QGroupBox {
border: 1px solid #30363d;
border-radius: 8px;
margin-top: 10px;
padding: 12px;
font-weight: bold;
color: #f0f6fc;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 4px 0 4px;
}
QPushButton {
background-color: #238636;
color: #ffffff;
padding: 8px 16px;
border-radius: 6px;
}
QPushButton:hover {
background-color: #2ea043;
}
QPushButton:disabled {
background-color: #161b22;
color: #8b949e;
}
QLineEdit, QComboBox {
background-color: #161b22;
border: 1px solid #30363d;
border-radius: 6px;
padding: 6px 8px;
color: #f0f6fc;
}
QTextEdit {
background-color: #010409;
border: 1px solid #30363d;
border-radius: 8px;
padding: 8px;
}
QLabel#status {
font-size: 14px;
}
QLabel#header {
color: #58a6ff;
}
"""
)
else:
self.setStyleSheet(
"""
QWidget {
background-color: #f7f9fc;
color: #24292f;
font-family: 'Segoe UI', sans-serif;
}
QGroupBox {
border: 1px solid #d0d7de;
border-radius: 8px;
margin-top: 10px;
padding: 12px;
font-weight: bold;
color: #24292f;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 4px 0 4px;
}
QPushButton {
background-color: #0969da;
color: #ffffff;
padding: 8px 16px;
border-radius: 6px;
}
QPushButton:hover {
background-color: #218bff;
}
QPushButton:disabled {
background-color: #d0d7de;
color: #57606a;
}
QLineEdit, QComboBox {
background-color: #ffffff;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 6px 8px;
color: #24292f;
}
QTextEdit {
background-color: #ffffff;
border: 1px solid #d0d7de;
border-radius: 8px;
padding: 8px;
}
QLabel#status {
font-size: 14px;
}
QLabel#header {
color: #0969da;
}
"""
)
def closeEvent(self, event):
if self.process:
self.process.terminate()
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.kill()
self.process = None
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
win = FGDGUI()
win.show()
sys.exit(app.exec())