1187 lines
53 KiB
Python
1187 lines
53 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Wolf Chat Setup Utility
|
|
|
|
A graphical configuration tool for Wolf Chat that manages:
|
|
- API settings (OpenAI/compatible providers)
|
|
- MCP server configurations (Exa, Chroma, custom servers)
|
|
- Environment variables (.env)
|
|
- Config file generation (config.py)
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import tkinter as tk
|
|
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
|
import configparser
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
|
|
# ===============================================================
|
|
# Constants
|
|
# ===============================================================
|
|
VERSION = "1.0.0"
|
|
CONFIG_TEMPLATE_PATH = "config_template.py"
|
|
ENV_FILE_PATH = ".env"
|
|
# Use absolute path for chroma_data
|
|
DEFAULT_CHROMA_DATA_PATH = os.path.abspath("chroma_data")
|
|
DEFAULT_CONFIG_SECTION = """# ====================================================================
|
|
# Wolf Chat Configuration
|
|
# Generated by setup.py - Edit with care
|
|
# ====================================================================
|
|
"""
|
|
|
|
# Get current Windows username for default paths
|
|
CURRENT_USERNAME = os.getenv("USERNAME", "user")
|
|
|
|
# ===============================================================
|
|
# Helper Functions
|
|
# ===============================================================
|
|
def load_env_file():
|
|
"""Load existing .env file if it exists"""
|
|
env_data = {}
|
|
env_path = Path(ENV_FILE_PATH)
|
|
if env_path.exists():
|
|
with open(env_path, 'r', encoding='utf-8') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#') and '=' in line:
|
|
key, value = line.split('=', 1)
|
|
env_data[key.strip()] = value.strip().strip('"\'')
|
|
return env_data
|
|
|
|
def save_env_file(env_data):
|
|
"""Save environment variables to .env file"""
|
|
with open(ENV_FILE_PATH, 'w', encoding='utf-8') as f:
|
|
f.write("# Environment variables for Wolf Chat\n")
|
|
f.write("# Generated by setup.py\n\n")
|
|
for key, value in env_data.items():
|
|
if value: # Only write non-empty values
|
|
f.write(f"{key}={value}\n")
|
|
print(f"Saved environment variables to {ENV_FILE_PATH}")
|
|
|
|
def load_current_config():
|
|
"""Extract settings from existing config.py if it exists"""
|
|
config_data = {
|
|
"OPENAI_API_BASE_URL": "",
|
|
"LLM_MODEL": "deepseek/deepseek-chat-v3-0324",
|
|
"MCP_SERVERS": {
|
|
"exa": {
|
|
"enabled": True,
|
|
"use_smithery": False,
|
|
"server_path": f"C:/Users/{CURRENT_USERNAME}/AppData/Roaming/npm/exa-mcp-server"
|
|
}
|
|
},
|
|
"ENABLE_CHAT_LOGGING": True,
|
|
"LOG_DIR": "chat_logs",
|
|
"GAME_WINDOW_CONFIG": {
|
|
"WINDOW_TITLE": "Last War-Survival Game",
|
|
"ENABLE_SCHEDULED_RESTART": True,
|
|
"RESTART_INTERVAL_MINUTES": 60,
|
|
"GAME_EXECUTABLE_PATH": fr"C:\Users\{CURRENT_USERNAME}\AppData\Local\TheLastWar\Launch.exe",
|
|
"GAME_WINDOW_X": 50,
|
|
"GAME_WINDOW_Y": 30,
|
|
"GAME_WINDOW_WIDTH": 600,
|
|
"GAME_WINDOW_HEIGHT": 1070,
|
|
"MONITOR_INTERVAL_SECONDS": 5
|
|
}
|
|
}
|
|
|
|
if os.path.exists("config.py"):
|
|
try:
|
|
with open("config.py", 'r', encoding='utf-8') as f:
|
|
config_content = f.read()
|
|
|
|
# Extract OPENAI_API_BASE_URL
|
|
api_url_match = re.search(r'OPENAI_API_BASE_URL\s*=\s*["\'](.+?)["\']', config_content)
|
|
if api_url_match:
|
|
config_data["OPENAI_API_BASE_URL"] = api_url_match.group(1)
|
|
|
|
# Extract LLM_MODEL
|
|
model_match = re.search(r'LLM_MODEL\s*=\s*["\'](.+?)["\']', config_content)
|
|
if model_match:
|
|
config_data["LLM_MODEL"] = model_match.group(1)
|
|
|
|
# Extract logging settings
|
|
chat_logging_match = re.search(r'ENABLE_CHAT_LOGGING\s*=\s*(True|False)', config_content)
|
|
if chat_logging_match:
|
|
config_data["ENABLE_CHAT_LOGGING"] = (chat_logging_match.group(1) == "True")
|
|
|
|
log_dir_match = re.search(r'LOG_DIR\s*=\s*["\'](.+?)["\']', config_content)
|
|
if log_dir_match:
|
|
config_data["LOG_DIR"] = log_dir_match.group(1)
|
|
|
|
# Extract game window settings
|
|
window_title_match = re.search(r'WINDOW_TITLE\s*=\s*["\'](.+?)["\']', config_content)
|
|
if window_title_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["WINDOW_TITLE"] = window_title_match.group(1)
|
|
|
|
restart_match = re.search(r'ENABLE_SCHEDULED_RESTART\s*=\s*(True|False)', config_content)
|
|
if restart_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["ENABLE_SCHEDULED_RESTART"] = (restart_match.group(1) == "True")
|
|
|
|
interval_match = re.search(r'RESTART_INTERVAL_MINUTES\s*=\s*(\d+)', config_content)
|
|
if interval_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["RESTART_INTERVAL_MINUTES"] = int(interval_match.group(1))
|
|
|
|
exec_path_match = re.search(r'GAME_EXECUTABLE_PATH\s*=\s*r"(.+?)"', config_content)
|
|
if exec_path_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["GAME_EXECUTABLE_PATH"] = exec_path_match.group(1)
|
|
|
|
x_match = re.search(r'GAME_WINDOW_X\s*=\s*(\d+)', config_content)
|
|
if x_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["GAME_WINDOW_X"] = int(x_match.group(1))
|
|
|
|
y_match = re.search(r'GAME_WINDOW_Y\s*=\s*(\d+)', config_content)
|
|
if y_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["GAME_WINDOW_Y"] = int(y_match.group(1))
|
|
|
|
width_match = re.search(r'GAME_WINDOW_WIDTH\s*=\s*(\d+)', config_content)
|
|
if width_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["GAME_WINDOW_WIDTH"] = int(width_match.group(1))
|
|
|
|
height_match = re.search(r'GAME_WINDOW_HEIGHT\s*=\s*(\d+)', config_content)
|
|
if height_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["GAME_WINDOW_HEIGHT"] = int(height_match.group(1))
|
|
|
|
monitor_interval_match = re.search(r'MONITOR_INTERVAL_SECONDS\s*=\s*(\d+)', config_content)
|
|
if monitor_interval_match:
|
|
config_data["GAME_WINDOW_CONFIG"]["MONITOR_INTERVAL_SECONDS"] = int(monitor_interval_match.group(1))
|
|
|
|
# Extract MCP_SERVERS (more complex parsing)
|
|
try:
|
|
servers_section = re.search(r'MCP_SERVERS\s*=\s*{(.+?)}(?=\n\n)', config_content, re.DOTALL)
|
|
if servers_section:
|
|
servers_text = servers_section.group(1)
|
|
|
|
# Extract and parse each server definition
|
|
server_blocks = re.findall(r'"([^"]+)":\s*{(.+?)}(?=,\s*"|,?\s*})', servers_text, re.DOTALL)
|
|
|
|
for server_name, server_block in server_blocks:
|
|
if server_name not in config_data["MCP_SERVERS"]:
|
|
config_data["MCP_SERVERS"][server_name] = {"enabled": True}
|
|
|
|
# Skip disabled servers (commented out)
|
|
if server_block.strip().startswith("#"):
|
|
config_data["MCP_SERVERS"][server_name]["enabled"] = False
|
|
continue
|
|
|
|
# Extract command
|
|
command_match = re.search(r'"command":\s*"([^"]+)"', server_block)
|
|
if command_match:
|
|
config_data["MCP_SERVERS"][server_name]["command"] = command_match.group(1)
|
|
|
|
# For Exa server
|
|
if server_name == "exa":
|
|
# Check if using Smithery
|
|
if '"@smithery/cli@latest"' in server_block:
|
|
config_data["MCP_SERVERS"]["exa"]["use_smithery"] = True
|
|
else:
|
|
config_data["MCP_SERVERS"]["exa"]["use_smithery"] = False
|
|
|
|
# Extract server path for local server
|
|
args_match = re.search(r'"args":\s*\[\s*"([^"]+)"', server_block)
|
|
if args_match:
|
|
config_data["MCP_SERVERS"]["exa"]["server_path"] = args_match.group(1)
|
|
|
|
# For Chroma server
|
|
if server_name == "chroma":
|
|
# Extract data directory
|
|
data_dir_match = re.search(r'"--data-dir",\s*"([^"]+)"', server_block)
|
|
if data_dir_match:
|
|
config_data["MCP_SERVERS"]["chroma"]["data_dir"] = data_dir_match.group(1)
|
|
|
|
# For custom servers, store the raw configuration
|
|
if server_name not in ["exa", "chroma"]:
|
|
config_data["MCP_SERVERS"][server_name]["raw_config"] = server_block
|
|
except Exception as e:
|
|
print(f"Error parsing MCP_SERVERS section: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
except Exception as e:
|
|
print(f"Error reading config.py: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
return config_data
|
|
|
|
def generate_config_file(config_data, env_data):
|
|
"""Generate config.py file based on user settings"""
|
|
# Create backup of existing config if it exists
|
|
if os.path.exists("config.py"):
|
|
backup_path = "config.py.bak"
|
|
shutil.copy2("config.py", backup_path)
|
|
print(f"Created backup of existing config at {backup_path}")
|
|
|
|
# Helper function to ensure absolute path
|
|
def ensure_absolute_path(path):
|
|
if not os.path.isabs(path):
|
|
return os.path.abspath(path)
|
|
return path
|
|
|
|
with open("config.py", 'w', encoding='utf-8') as f:
|
|
f.write(DEFAULT_CONFIG_SECTION)
|
|
f.write("import os\n")
|
|
f.write("import json\n")
|
|
f.write("from dotenv import load_dotenv\n\n")
|
|
|
|
f.write("# --- Load environment variables from .env file ---\n")
|
|
f.write("load_dotenv()\n")
|
|
f.write("print(\"Loaded environment variables from .env file.\")\n\n")
|
|
|
|
# Write OpenAI API settings
|
|
f.write("# =============================================================================\n")
|
|
f.write("# OpenAI API Configuration / OpenAI-Compatible Provider Settings\n")
|
|
f.write("# =============================================================================\n")
|
|
f.write(f"OPENAI_API_BASE_URL = \"{config_data['OPENAI_API_BASE_URL']}\"\n")
|
|
f.write("OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\")\n")
|
|
f.write(f"LLM_MODEL = \"{config_data['LLM_MODEL']}\"\n\n")
|
|
|
|
# Write API Keys section
|
|
f.write("# =============================================================================\n")
|
|
f.write("# External API Keys (loaded from environment variables)\n")
|
|
f.write("# =============================================================================\n")
|
|
f.write("EXA_API_KEY = os.getenv(\"EXA_API_KEY\")\n\n")
|
|
|
|
# Write Exa config utility
|
|
f.write("# --- Exa Configuration ---\n")
|
|
f.write("exa_config_dict = {\"exaApiKey\": EXA_API_KEY if EXA_API_KEY else \"YOUR_EXA_KEY_MISSING\"}\n")
|
|
f.write("exa_config_arg_string = json.dumps(exa_config_dict)\n\n")
|
|
|
|
# Write MCP Server Configuration
|
|
f.write("# =============================================================================\n")
|
|
f.write("# MCP Server Configuration\n")
|
|
f.write("# =============================================================================\n")
|
|
f.write("MCP_SERVERS = {\n")
|
|
|
|
# Add configured servers
|
|
for server_name, server_config in config_data["MCP_SERVERS"].items():
|
|
if not server_config.get("enabled", True):
|
|
f.write(f" # \"{server_name}\": {{ # Disabled\n")
|
|
continue
|
|
|
|
f.write(f" \"{server_name}\": {{\n")
|
|
|
|
# Handle Exa server special config
|
|
if server_name == "exa":
|
|
if server_config.get("use_smithery", False):
|
|
# Smithery config
|
|
f.write(" \"command\": \"cmd\",\n")
|
|
f.write(" \"args\": [\n")
|
|
f.write(" \"/c\",\n")
|
|
f.write(" \"npx\",\n")
|
|
f.write(" \"-y\",\n")
|
|
f.write(" \"@smithery/cli@latest\",\n")
|
|
f.write(" \"run\",\n")
|
|
f.write(" \"exa\",\n")
|
|
f.write(" \"--config\",\n")
|
|
f.write(" exa_config_arg_string\n")
|
|
f.write(" ],\n")
|
|
else:
|
|
# Local server config
|
|
server_path = server_config.get("server_path", "exa-mcp-server")
|
|
f.write(" \"command\": \"npx\",\n")
|
|
f.write(" \"args\": [\n")
|
|
f.write(f" \"{server_path}\",\n")
|
|
f.write(" \"--tools=web_search,research_paper_search,twitter_search,company_research,crawling,competitor_finder\"\n")
|
|
f.write(" ],\n")
|
|
f.write(" \"env\": {\n")
|
|
f.write(" \"EXA_API_KEY\": EXA_API_KEY\n")
|
|
f.write(" }\n")
|
|
|
|
# Handle Chroma server
|
|
elif server_name == "chroma":
|
|
# Ensure absolute path for chroma data directory
|
|
data_dir = server_config.get("data_dir", DEFAULT_CHROMA_DATA_PATH)
|
|
absolute_data_dir = ensure_absolute_path(data_dir)
|
|
|
|
f.write(" \"command\": \"uvx\",\n")
|
|
f.write(" \"args\": [\n")
|
|
f.write(" \"chroma-mcp\",\n")
|
|
f.write(" \"--client-type\",\n")
|
|
f.write(" \"persistent\",\n")
|
|
f.write(" \"--data-dir\",\n")
|
|
f.write(f" \"{absolute_data_dir}\"\n")
|
|
f.write(" ]\n")
|
|
|
|
# Handle custom server - just write as raw JSON
|
|
elif server_name == "custom" and "raw_config" in server_config:
|
|
f.write(server_config["raw_config"])
|
|
|
|
f.write(" },\n")
|
|
|
|
f.write("}\n\n")
|
|
|
|
# Write remaining configuration sections
|
|
f.write("# =============================================================================\n")
|
|
f.write("# MCP Client Configuration\n")
|
|
f.write("# =============================================================================\n")
|
|
f.write("MCP_CONFIRM_TOOL_EXECUTION = False # True: Confirm before execution, False: Execute automatically\n\n")
|
|
|
|
f.write("# =============================================================================\n")
|
|
f.write("# Chat Logging Configuration\n")
|
|
f.write("# =============================================================================\n")
|
|
f.write(f"ENABLE_CHAT_LOGGING = {str(config_data['ENABLE_CHAT_LOGGING'])}\n")
|
|
f.write(f"LOG_DIR = \"{config_data['LOG_DIR']}\"\n\n")
|
|
|
|
f.write("# =============================================================================\n")
|
|
f.write("# Persona Configuration\n")
|
|
f.write("# =============================================================================\n")
|
|
f.write("PERSONA_NAME = \"Wolfhart\"\n\n")
|
|
|
|
f.write("# =============================================================================\n")
|
|
f.write("# Game Window Configuration\n")
|
|
f.write("# =============================================================================\n")
|
|
game_config = config_data["GAME_WINDOW_CONFIG"]
|
|
f.write(f"WINDOW_TITLE = \"{game_config['WINDOW_TITLE']}\"\n")
|
|
f.write(f"ENABLE_SCHEDULED_RESTART = {str(game_config['ENABLE_SCHEDULED_RESTART'])}\n")
|
|
f.write(f"RESTART_INTERVAL_MINUTES = {game_config['RESTART_INTERVAL_MINUTES']}\n")
|
|
f.write(f"GAME_EXECUTABLE_PATH = r\"{game_config['GAME_EXECUTABLE_PATH']}\"\n")
|
|
f.write(f"GAME_WINDOW_X = {game_config['GAME_WINDOW_X']}\n")
|
|
f.write(f"GAME_WINDOW_Y = {game_config['GAME_WINDOW_Y']}\n")
|
|
f.write(f"GAME_WINDOW_WIDTH = {game_config['GAME_WINDOW_WIDTH']}\n")
|
|
f.write(f"GAME_WINDOW_HEIGHT = {game_config['GAME_WINDOW_HEIGHT']}\n")
|
|
f.write(f"MONITOR_INTERVAL_SECONDS = {game_config['MONITOR_INTERVAL_SECONDS']}\n")
|
|
|
|
print("Generated config.py file successfully")
|
|
|
|
|
|
# ===============================================================
|
|
# Main Application
|
|
# ===============================================================
|
|
class WolfChatSetup(tk.Tk):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.title(f"Wolf Chat Setup v{VERSION}")
|
|
self.geometry("800x600")
|
|
self.minsize(750, 550)
|
|
|
|
# Load existing data
|
|
self.env_data = load_env_file()
|
|
self.config_data = load_current_config()
|
|
|
|
# Create the notebook for tabs
|
|
self.notebook = ttk.Notebook(self)
|
|
self.notebook.pack(expand=True, fill=tk.BOTH, padx=10, pady=10)
|
|
|
|
# Create tabs
|
|
self.create_api_tab()
|
|
self.create_mcp_tab()
|
|
self.create_game_tab()
|
|
|
|
# Create bottom buttons
|
|
self.create_bottom_buttons()
|
|
|
|
# Set initial states based on loaded data
|
|
self.update_ui_from_data()
|
|
|
|
def create_api_tab(self):
|
|
"""Create the API Settings tab"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="API Settings")
|
|
|
|
# Main frame with padding
|
|
main_frame = ttk.Frame(tab, padding=10)
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Header
|
|
header = ttk.Label(main_frame, text="OpenAI API Settings", font=("", 12, "bold"))
|
|
header.pack(anchor=tk.W, pady=(0, 10))
|
|
|
|
# API Base URL
|
|
url_frame = ttk.Frame(main_frame)
|
|
url_frame.pack(fill=tk.X, pady=5)
|
|
|
|
url_label = ttk.Label(url_frame, text="API Base URL:", width=15)
|
|
url_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
self.api_url_var = tk.StringVar()
|
|
self.api_url_entry = ttk.Entry(url_frame, textvariable=self.api_url_var)
|
|
self.api_url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
# Predefined endpoints
|
|
url_presets_frame = ttk.Frame(main_frame)
|
|
url_presets_frame.pack(fill=tk.X, pady=2)
|
|
|
|
preset_label = ttk.Label(url_presets_frame, text="Presets:", width=15)
|
|
preset_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
openai_btn = ttk.Button(url_presets_frame, text="OpenAI", width=10,
|
|
command=lambda: self.api_url_var.set(""))
|
|
openai_btn.pack(side=tk.LEFT, padx=2)
|
|
|
|
openrouter_btn = ttk.Button(url_presets_frame, text="OpenRouter", width=10,
|
|
command=lambda: self.api_url_var.set("https://openrouter.ai/api/v1"))
|
|
openrouter_btn.pack(side=tk.LEFT, padx=2)
|
|
|
|
localhost_btn = ttk.Button(url_presets_frame, text="Localhost", width=10,
|
|
command=lambda: self.api_url_var.set("http://localhost:1234/v1"))
|
|
localhost_btn.pack(side=tk.LEFT, padx=2)
|
|
|
|
# API Key
|
|
key_frame = ttk.Frame(main_frame)
|
|
key_frame.pack(fill=tk.X, pady=5)
|
|
|
|
key_label = ttk.Label(key_frame, text="API Key:", width=15)
|
|
key_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
self.api_key_var = tk.StringVar()
|
|
self.api_key_entry = ttk.Entry(key_frame, textvariable=self.api_key_var, show="*")
|
|
self.api_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
self.show_key_var = tk.BooleanVar(value=False)
|
|
show_key_cb = ttk.Checkbutton(key_frame, text="Show", variable=self.show_key_var,
|
|
command=self.toggle_key_visibility)
|
|
show_key_cb.pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
# Model Selection
|
|
model_frame = ttk.Frame(main_frame)
|
|
model_frame.pack(fill=tk.X, pady=5)
|
|
|
|
model_label = ttk.Label(model_frame, text="LLM Model:", width=15)
|
|
model_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
self.model_var = tk.StringVar(value="deepseek/deepseek-chat-v3-0324")
|
|
self.model_entry = ttk.Entry(model_frame, textvariable=self.model_var)
|
|
self.model_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
# Information box
|
|
info_frame = ttk.LabelFrame(main_frame, text="Information")
|
|
info_frame.pack(fill=tk.BOTH, expand=True, pady=10)
|
|
|
|
info_text = (
|
|
"• Leave the API Base URL empty to use the official OpenAI API\n"
|
|
"• For OpenRouter, you need an OpenRouter API key\n"
|
|
"• For local LLMs, configure your API to use OpenAI-compatible format\n"
|
|
"• Make sure the selected model is available with your chosen provider"
|
|
)
|
|
|
|
info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=700)
|
|
info_label.pack(padx=10, pady=10, anchor=tk.W)
|
|
|
|
def create_mcp_tab(self):
|
|
"""Create the MCP Settings tab"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="MCP Servers")
|
|
|
|
# Main frame with padding
|
|
main_frame = ttk.Frame(tab, padding=10)
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Header
|
|
header = ttk.Label(main_frame, text="Modular Capability Provider (MCP) Servers", font=("", 12, "bold"))
|
|
header.pack(anchor=tk.W, pady=(0, 10))
|
|
|
|
# Create the main frame for server settings
|
|
servers_frame = ttk.Frame(main_frame)
|
|
servers_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Left side - Servers list
|
|
servers_list_frame = ttk.LabelFrame(servers_frame, text="Available Servers")
|
|
servers_list_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5))
|
|
|
|
# Servers list
|
|
self.servers_listbox = tk.Listbox(servers_list_frame, width=20, height=10)
|
|
self.servers_listbox.pack(side=tk.LEFT, fill=tk.Y, expand=True, padx=5, pady=5)
|
|
self.servers_listbox.bind('<<ListboxSelect>>', self.on_server_select)
|
|
|
|
servers_scrollbar = ttk.Scrollbar(servers_list_frame, orient=tk.VERTICAL, command=self.servers_listbox.yview)
|
|
servers_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
self.servers_listbox.config(yscrollcommand=servers_scrollbar.set)
|
|
|
|
# Buttons below listbox
|
|
servers_btn_frame = ttk.Frame(servers_list_frame)
|
|
servers_btn_frame.pack(fill=tk.X, pady=5, padx=5)
|
|
|
|
add_btn = ttk.Button(servers_btn_frame, text="Add", command=self.add_custom_server)
|
|
add_btn.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
|
|
|
|
self.remove_btn = ttk.Button(servers_btn_frame, text="Remove", command=self.remove_server, state=tk.DISABLED)
|
|
self.remove_btn.pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
|
|
|
|
# Right side - Server settings
|
|
self.server_settings_frame = ttk.LabelFrame(servers_frame, text="Server Settings")
|
|
self.server_settings_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
|
|
|
|
# Empty label for initial state
|
|
self.empty_settings_label = ttk.Label(self.server_settings_frame,
|
|
text="Select a server from the list to configure it.",
|
|
justify=tk.CENTER)
|
|
self.empty_settings_label.pack(expand=True, pady=20)
|
|
|
|
# Frames for each server type (initially hidden)
|
|
self.create_exa_settings_frame()
|
|
self.create_chroma_settings_frame()
|
|
self.create_custom_settings_frame()
|
|
|
|
# Update the servers list
|
|
self.update_servers_list()
|
|
|
|
def create_exa_settings_frame(self):
|
|
"""Create settings frame for Exa server"""
|
|
self.exa_frame = ttk.Frame(self.server_settings_frame)
|
|
|
|
# Enable checkbox
|
|
enable_frame = ttk.Frame(self.exa_frame)
|
|
enable_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.exa_enable_var = tk.BooleanVar(value=True)
|
|
enable_cb = ttk.Checkbutton(enable_frame, text="Enable Exa Server", variable=self.exa_enable_var)
|
|
enable_cb.pack(anchor=tk.W)
|
|
|
|
# API Key
|
|
key_frame = ttk.Frame(self.exa_frame)
|
|
key_frame.pack(fill=tk.X, pady=5)
|
|
|
|
key_label = ttk.Label(key_frame, text="Exa API Key:", width=15)
|
|
key_label.pack(side=tk.LEFT)
|
|
|
|
self.exa_key_var = tk.StringVar()
|
|
self.exa_key_entry = ttk.Entry(key_frame, textvariable=self.exa_key_var, show="*")
|
|
self.exa_key_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
self.show_exa_key_var = tk.BooleanVar(value=False)
|
|
show_key_cb = ttk.Checkbutton(key_frame, text="Show", variable=self.show_exa_key_var,
|
|
command=lambda: self.toggle_field_visibility(self.exa_key_entry, self.show_exa_key_var))
|
|
show_key_cb.pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
# Server Type
|
|
type_frame = ttk.Frame(self.exa_frame)
|
|
type_frame.pack(fill=tk.X, pady=5)
|
|
|
|
type_label = ttk.Label(type_frame, text="Server Type:", width=15)
|
|
type_label.pack(side=tk.LEFT)
|
|
|
|
self.exa_type_var = tk.StringVar(value="local") # Changed default to local
|
|
|
|
local_radio = ttk.Radiobutton(type_frame, text="Local Server (recommended)",
|
|
variable=self.exa_type_var, value="local",
|
|
command=self.update_exa_settings_visibility)
|
|
local_radio.pack(anchor=tk.W)
|
|
|
|
smithery_radio = ttk.Radiobutton(type_frame, text="Smithery",
|
|
variable=self.exa_type_var, value="smithery",
|
|
command=self.update_exa_settings_visibility)
|
|
smithery_radio.pack(anchor=tk.W)
|
|
|
|
# Local Server Path
|
|
self.exa_local_frame = ttk.Frame(self.exa_frame)
|
|
|
|
local_path_label = ttk.Label(self.exa_local_frame, text="Server Path:", width=15)
|
|
local_path_label.pack(side=tk.LEFT)
|
|
|
|
self.exa_path_var = tk.StringVar(value=f"C:/Users/{CURRENT_USERNAME}/AppData/Roaming/npm/exa-mcp-server")
|
|
self.exa_path_entry = ttk.Entry(self.exa_local_frame, textvariable=self.exa_path_var)
|
|
self.exa_path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
browse_btn = ttk.Button(self.exa_local_frame, text="Browse", command=self.browse_exa_server)
|
|
browse_btn.pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
# Smithery Info
|
|
self.exa_smithery_frame = ttk.Frame(self.exa_frame)
|
|
|
|
smithery_info = ttk.Label(self.exa_smithery_frame,
|
|
text="Smithery will automatically download and run the latest Exa MCP server.",
|
|
wraplength=400)
|
|
smithery_info.pack(pady=5)
|
|
|
|
# Information text
|
|
info_frame = ttk.LabelFrame(self.exa_frame, text="Information")
|
|
info_frame.pack(fill=tk.X, pady=10)
|
|
|
|
info_text = (
|
|
"• Exa MCP provides web search and research capabilities\n"
|
|
"• An Exa API key is required (obtain from https://exa.ai)\n"
|
|
"• Local server is the default and preferred option\n"
|
|
"• Smithery requires an internet connection each time"
|
|
)
|
|
|
|
info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=400)
|
|
info_label.pack(padx=10, pady=10, anchor=tk.W)
|
|
|
|
def create_chroma_settings_frame(self):
|
|
"""Create settings frame for Chroma MCP server"""
|
|
self.chroma_frame = ttk.Frame(self.server_settings_frame)
|
|
|
|
# Enable checkbox
|
|
enable_frame = ttk.Frame(self.chroma_frame)
|
|
enable_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.chroma_enable_var = tk.BooleanVar(value=True)
|
|
enable_cb = ttk.Checkbutton(enable_frame, text="Enable Chroma MCP Server", variable=self.chroma_enable_var)
|
|
enable_cb.pack(anchor=tk.W)
|
|
|
|
# Data directory
|
|
dir_frame = ttk.Frame(self.chroma_frame)
|
|
dir_frame.pack(fill=tk.X, pady=5)
|
|
|
|
dir_label = ttk.Label(dir_frame, text="Data Directory:", width=15)
|
|
dir_label.pack(side=tk.LEFT)
|
|
|
|
self.chroma_dir_var = tk.StringVar(value=DEFAULT_CHROMA_DATA_PATH)
|
|
dir_entry = ttk.Entry(dir_frame, textvariable=self.chroma_dir_var)
|
|
dir_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
browse_btn = ttk.Button(dir_frame, text="Browse", command=self.browse_chroma_dir)
|
|
browse_btn.pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
# Information text
|
|
info_frame = ttk.LabelFrame(self.chroma_frame, text="Information")
|
|
info_frame.pack(fill=tk.X, pady=10, expand=True)
|
|
|
|
info_text = (
|
|
"• Chroma MCP provides vector database storage for memory features\n"
|
|
"• The data directory will store vector embeddings and metadata\n"
|
|
"• Use a persistent directory to maintain memory between sessions\n"
|
|
"• Requires uvx to be installed in your Python environment"
|
|
)
|
|
|
|
info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=400)
|
|
info_label.pack(padx=10, pady=10, anchor=tk.W)
|
|
|
|
def create_custom_settings_frame(self):
|
|
"""Create settings frame for custom server"""
|
|
self.custom_frame = ttk.Frame(self.server_settings_frame)
|
|
|
|
# Name field
|
|
name_frame = ttk.Frame(self.custom_frame)
|
|
name_frame.pack(fill=tk.X, pady=5)
|
|
|
|
name_label = ttk.Label(name_frame, text="Server Name:", width=15)
|
|
name_label.pack(side=tk.LEFT)
|
|
|
|
self.custom_name_var = tk.StringVar()
|
|
name_entry = ttk.Entry(name_frame, textvariable=self.custom_name_var)
|
|
name_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
# Enable checkbox
|
|
enable_frame = ttk.Frame(self.custom_frame)
|
|
enable_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.custom_enable_var = tk.BooleanVar(value=True)
|
|
enable_cb = ttk.Checkbutton(enable_frame, text="Enable Server", variable=self.custom_enable_var)
|
|
enable_cb.pack(anchor=tk.W)
|
|
|
|
# Raw configuration editor
|
|
config_label = ttk.Label(self.custom_frame, text="Server Configuration (Python Dictionary Format):")
|
|
config_label.pack(anchor=tk.W, pady=(10, 5))
|
|
|
|
self.custom_config_text = scrolledtext.ScrolledText(self.custom_frame, height=12, width=50)
|
|
self.custom_config_text.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
|
|
# Default configuration template
|
|
default_config = (
|
|
' "command": "npx",\n'
|
|
' "args": [\n'
|
|
' "some-mcp-server",\n'
|
|
' "--option1",\n'
|
|
' "--option2=value"\n'
|
|
' ]'
|
|
)
|
|
self.custom_config_text.insert(tk.END, default_config)
|
|
|
|
# Information text
|
|
info_frame = ttk.LabelFrame(self.custom_frame, text="Information")
|
|
info_frame.pack(fill=tk.X, pady=10)
|
|
|
|
info_text = (
|
|
"• Enter the raw Python dictionary for your custom MCP server\n"
|
|
"• Format must match the structure in config.py\n"
|
|
"• Be careful with syntax - incorrect format may cause errors\n"
|
|
"• Server name must be unique"
|
|
)
|
|
|
|
info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=400)
|
|
info_label.pack(padx=10, pady=10, anchor=tk.W)
|
|
|
|
def create_game_tab(self):
|
|
"""Create the Game Settings tab"""
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Game Settings")
|
|
|
|
# Main frame with padding
|
|
main_frame = ttk.Frame(tab, padding=10)
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Header
|
|
header = ttk.Label(main_frame, text="Game Window Configuration", font=("", 12, "bold"))
|
|
header.pack(anchor=tk.W, pady=(0, 10))
|
|
|
|
# Game window title
|
|
title_frame = ttk.Frame(main_frame)
|
|
title_frame.pack(fill=tk.X, pady=5)
|
|
|
|
title_label = ttk.Label(title_frame, text="Window Title:", width=20)
|
|
title_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
self.window_title_var = tk.StringVar(value="Last War-Survival Game")
|
|
title_entry = ttk.Entry(title_frame, textvariable=self.window_title_var)
|
|
title_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
# Game executable path
|
|
path_frame = ttk.Frame(main_frame)
|
|
path_frame.pack(fill=tk.X, pady=5)
|
|
|
|
path_label = ttk.Label(path_frame, text="Game Executable Path:", width=20)
|
|
path_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
self.game_path_var = tk.StringVar(value=fr"C:\Users\{CURRENT_USERNAME}\AppData\Local\TheLastWar\Launch.exe")
|
|
path_entry = ttk.Entry(path_frame, textvariable=self.game_path_var)
|
|
path_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
|
|
browse_btn = ttk.Button(path_frame, text="Browse", command=self.browse_game_path)
|
|
browse_btn.pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
# Window position
|
|
pos_frame = ttk.Frame(main_frame)
|
|
pos_frame.pack(fill=tk.X, pady=5)
|
|
|
|
pos_label = ttk.Label(pos_frame, text="Window Position:", width=20)
|
|
pos_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
pos_x_label = ttk.Label(pos_frame, text="X:")
|
|
pos_x_label.pack(side=tk.LEFT)
|
|
|
|
self.pos_x_var = tk.IntVar(value=50)
|
|
pos_x_entry = ttk.Spinbox(pos_frame, textvariable=self.pos_x_var, from_=0, to=3000, width=5)
|
|
pos_x_entry.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
|
pos_y_label = ttk.Label(pos_frame, text="Y:")
|
|
pos_y_label.pack(side=tk.LEFT)
|
|
|
|
self.pos_y_var = tk.IntVar(value=30)
|
|
pos_y_entry = ttk.Spinbox(pos_frame, textvariable=self.pos_y_var, from_=0, to=3000, width=5)
|
|
pos_y_entry.pack(side=tk.LEFT)
|
|
|
|
# Window size
|
|
size_frame = ttk.Frame(main_frame)
|
|
size_frame.pack(fill=tk.X, pady=5)
|
|
|
|
size_label = ttk.Label(size_frame, text="Window Size:", width=20)
|
|
size_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
width_label = ttk.Label(size_frame, text="Width:")
|
|
width_label.pack(side=tk.LEFT)
|
|
|
|
self.width_var = tk.IntVar(value=600)
|
|
width_entry = ttk.Spinbox(size_frame, textvariable=self.width_var, from_=300, to=3000, width=5)
|
|
width_entry.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
|
height_label = ttk.Label(size_frame, text="Height:")
|
|
height_label.pack(side=tk.LEFT)
|
|
|
|
self.height_var = tk.IntVar(value=1070)
|
|
height_entry = ttk.Spinbox(size_frame, textvariable=self.height_var, from_=300, to=3000, width=5)
|
|
height_entry.pack(side=tk.LEFT)
|
|
|
|
# Auto-restart settings
|
|
restart_frame = ttk.LabelFrame(main_frame, text="Auto-Restart Settings")
|
|
restart_frame.pack(fill=tk.X, pady=10)
|
|
|
|
self.restart_var = tk.BooleanVar(value=True)
|
|
restart_cb = ttk.Checkbutton(restart_frame, text="Enable scheduled game restart", variable=self.restart_var)
|
|
restart_cb.pack(anchor=tk.W, padx=10, pady=5)
|
|
|
|
interval_frame = ttk.Frame(restart_frame)
|
|
interval_frame.pack(fill=tk.X, padx=10, pady=5)
|
|
|
|
interval_label = ttk.Label(interval_frame, text="Restart interval (minutes):")
|
|
interval_label.pack(side=tk.LEFT)
|
|
|
|
self.interval_var = tk.IntVar(value=60)
|
|
interval_entry = ttk.Spinbox(interval_frame, textvariable=self.interval_var, from_=15, to=1440, width=5)
|
|
interval_entry.pack(side=tk.LEFT, padx=(5, 0))
|
|
|
|
# Monitor interval
|
|
monitor_frame = ttk.Frame(main_frame)
|
|
monitor_frame.pack(fill=tk.X, pady=5)
|
|
|
|
monitor_label = ttk.Label(monitor_frame, text="Monitor Interval (sec):", width=20)
|
|
monitor_label.pack(side=tk.LEFT, padx=(0, 5))
|
|
|
|
self.monitor_interval_var = tk.IntVar(value=5)
|
|
monitor_entry = ttk.Spinbox(monitor_frame, textvariable=self.monitor_interval_var, from_=1, to=60, width=5)
|
|
monitor_entry.pack(side=tk.LEFT)
|
|
|
|
# Information text
|
|
info_frame = ttk.LabelFrame(main_frame, text="Information")
|
|
info_frame.pack(fill=tk.BOTH, expand=True, pady=10)
|
|
|
|
info_text = (
|
|
"• These settings control how the game window is monitored and positioned\n"
|
|
"• Window Title must match exactly what's shown in the title bar\n"
|
|
"• Scheduled restart helps prevent game crashes and memory leaks\n"
|
|
"• Monitor interval determines how often the window position is checked\n"
|
|
"• Changes will take effect after restarting Wolf Chat"
|
|
)
|
|
|
|
info_label = ttk.Label(info_frame, text=info_text, justify=tk.LEFT, wraplength=700)
|
|
info_label.pack(padx=10, pady=10, anchor=tk.W)
|
|
|
|
def create_bottom_buttons(self):
|
|
"""Create bottom action buttons"""
|
|
btn_frame = ttk.Frame(self)
|
|
btn_frame.pack(fill=tk.X, padx=10, pady=10)
|
|
|
|
# Version label on left
|
|
version_label = ttk.Label(btn_frame, text=f"v{VERSION}")
|
|
version_label.pack(side=tk.LEFT, padx=5)
|
|
|
|
# Install dependencies button
|
|
install_deps_btn = ttk.Button(btn_frame, text="Install Dependencies", command=self.install_dependencies)
|
|
install_deps_btn.pack(side=tk.RIGHT, padx=5)
|
|
|
|
# Action buttons on right
|
|
save_btn = ttk.Button(btn_frame, text="Save Settings", command=self.save_settings)
|
|
save_btn.pack(side=tk.RIGHT, padx=5)
|
|
|
|
cancel_btn = ttk.Button(btn_frame, text="Cancel", command=self.quit)
|
|
cancel_btn.pack(side=tk.RIGHT, padx=5)
|
|
|
|
def install_dependencies(self):
|
|
"""Run the installation script for dependencies"""
|
|
try:
|
|
import subprocess
|
|
import sys
|
|
|
|
# Check if install.py exists
|
|
if not os.path.exists("install.py"):
|
|
messagebox.showerror("Error", "Could not find install.py script")
|
|
return
|
|
|
|
# Run the installer script in a new process
|
|
subprocess.Popen([sys.executable, "install.py"])
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to launch installer: {str(e)}")
|
|
|
|
|
|
def update_ui_from_data(self):
|
|
"""Update UI controls from loaded data"""
|
|
try:
|
|
# API Tab
|
|
self.api_url_var.set(self.config_data.get("OPENAI_API_BASE_URL", ""))
|
|
self.api_key_var.set(self.env_data.get("OPENAI_API_KEY", ""))
|
|
self.model_var.set(self.config_data.get("LLM_MODEL", "deepseek/deepseek-chat-v3-0324"))
|
|
|
|
# MCP Servers
|
|
# Exa settings
|
|
if "exa" in self.config_data.get("MCP_SERVERS", {}):
|
|
exa_config = self.config_data["MCP_SERVERS"]["exa"]
|
|
self.exa_enable_var.set(exa_config.get("enabled", True))
|
|
self.exa_key_var.set(self.env_data.get("EXA_API_KEY", ""))
|
|
|
|
if exa_config.get("use_smithery", False):
|
|
self.exa_type_var.set("smithery")
|
|
else:
|
|
self.exa_type_var.set("local")
|
|
if "server_path" in exa_config:
|
|
self.exa_path_var.set(exa_config.get("server_path", ""))
|
|
|
|
# Chroma settings
|
|
if "chroma" in self.config_data.get("MCP_SERVERS", {}):
|
|
chroma_config = self.config_data["MCP_SERVERS"]["chroma"]
|
|
self.chroma_enable_var.set(chroma_config.get("enabled", True))
|
|
data_dir = chroma_config.get("data_dir", "")
|
|
if data_dir:
|
|
# Ensure data directory is absolute path
|
|
self.chroma_dir_var.set(os.path.abspath(data_dir))
|
|
else:
|
|
# Set default as absolute path
|
|
self.chroma_dir_var.set(DEFAULT_CHROMA_DATA_PATH)
|
|
|
|
# Update servers list to include custom servers
|
|
self.update_servers_list()
|
|
|
|
# Game settings
|
|
game_config = self.config_data.get("GAME_WINDOW_CONFIG", {})
|
|
self.window_title_var.set(game_config.get("WINDOW_TITLE", "Last War-Survival Game"))
|
|
|
|
game_path = game_config.get("GAME_EXECUTABLE_PATH", "")
|
|
if game_path:
|
|
self.game_path_var.set(game_path)
|
|
|
|
self.pos_x_var.set(game_config.get("GAME_WINDOW_X", 50))
|
|
self.pos_y_var.set(game_config.get("GAME_WINDOW_Y", 30))
|
|
self.width_var.set(game_config.get("GAME_WINDOW_WIDTH", 600))
|
|
self.height_var.set(game_config.get("GAME_WINDOW_HEIGHT", 1070))
|
|
self.restart_var.set(game_config.get("ENABLE_SCHEDULED_RESTART", True))
|
|
self.interval_var.set(game_config.get("RESTART_INTERVAL_MINUTES", 60))
|
|
self.monitor_interval_var.set(game_config.get("MONITOR_INTERVAL_SECONDS", 5))
|
|
|
|
# Update visibility and states
|
|
self.update_exa_settings_visibility()
|
|
|
|
except Exception as e:
|
|
print(f"Error updating UI from data: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
# ===============================================================
|
|
# UI Event Handlers
|
|
# ===============================================================
|
|
def toggle_key_visibility(self):
|
|
"""Toggle visibility of API key field"""
|
|
if self.show_key_var.get():
|
|
self.api_key_entry.config(show="")
|
|
else:
|
|
self.api_key_entry.config(show="*")
|
|
|
|
def toggle_field_visibility(self, entry_widget, show_var):
|
|
"""Toggle visibility of a password field"""
|
|
if show_var.get():
|
|
entry_widget.config(show="")
|
|
else:
|
|
entry_widget.config(show="*")
|
|
|
|
def update_exa_settings_visibility(self):
|
|
"""Update visibility of Exa settings based on server type"""
|
|
if self.exa_type_var.get() == "smithery":
|
|
self.exa_local_frame.pack_forget()
|
|
self.exa_smithery_frame.pack(fill=tk.X, pady=5)
|
|
else:
|
|
self.exa_smithery_frame.pack_forget()
|
|
self.exa_local_frame.pack(fill=tk.X, pady=5)
|
|
|
|
def browse_exa_server(self):
|
|
"""Browse for Exa server executable"""
|
|
file_path = filedialog.askopenfilename(
|
|
title="Select Exa MCP Server",
|
|
filetypes=[("All Files", "*.*")],
|
|
initialdir=os.path.expanduser("~")
|
|
)
|
|
if file_path:
|
|
self.exa_path_var.set(file_path)
|
|
|
|
def browse_chroma_dir(self):
|
|
"""Browse for Chroma data directory"""
|
|
dir_path = filedialog.askdirectory(
|
|
title="Select Chroma Data Directory",
|
|
initialdir=os.path.dirname(DEFAULT_CHROMA_DATA_PATH)
|
|
)
|
|
if dir_path:
|
|
# Always store as absolute path
|
|
self.chroma_dir_var.set(os.path.abspath(dir_path))
|
|
|
|
def browse_game_path(self):
|
|
"""Browse for game executable"""
|
|
file_path = filedialog.askopenfilename(
|
|
title="Select Game Executable",
|
|
filetypes=[("Executable Files", "*.exe"), ("All Files", "*.*")],
|
|
initialdir=os.path.expanduser("~")
|
|
)
|
|
if file_path:
|
|
self.game_path_var.set(file_path)
|
|
|
|
def update_servers_list(self):
|
|
"""Update the servers listbox with current servers"""
|
|
self.servers_listbox.delete(0, tk.END)
|
|
|
|
# Add built-in servers
|
|
self.servers_listbox.insert(tk.END, "exa")
|
|
self.servers_listbox.insert(tk.END, "chroma")
|
|
|
|
# Add custom servers
|
|
for server_name in self.config_data.get("MCP_SERVERS", {}):
|
|
if server_name not in ("exa", "chroma"):
|
|
self.servers_listbox.insert(tk.END, server_name)
|
|
|
|
def on_server_select(self, event):
|
|
"""Handle server selection from the listbox"""
|
|
selection = self.servers_listbox.curselection()
|
|
if not selection:
|
|
return
|
|
|
|
selected_server = self.servers_listbox.get(selection[0])
|
|
|
|
# Hide all settings frames
|
|
self.empty_settings_label.pack_forget()
|
|
self.exa_frame.pack_forget()
|
|
self.chroma_frame.pack_forget()
|
|
self.custom_frame.pack_forget()
|
|
|
|
# Show the selected server's settings frame
|
|
if selected_server == "exa":
|
|
self.exa_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
self.remove_btn.config(state=tk.DISABLED) # Can't remove built-in servers
|
|
elif selected_server == "chroma":
|
|
self.chroma_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
self.remove_btn.config(state=tk.DISABLED) # Can't remove built-in servers
|
|
else:
|
|
# Custom server
|
|
if selected_server in self.config_data.get("MCP_SERVERS", {}):
|
|
# Load existing custom server config
|
|
server_config = self.config_data["MCP_SERVERS"][selected_server]
|
|
self.custom_name_var.set(selected_server)
|
|
self.custom_enable_var.set(server_config.get("enabled", True))
|
|
|
|
if "raw_config" in server_config:
|
|
self.custom_config_text.delete("1.0", tk.END)
|
|
self.custom_config_text.insert("1.0", server_config["raw_config"])
|
|
|
|
self.custom_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
self.remove_btn.config(state=tk.NORMAL) # Can remove custom servers
|
|
|
|
def add_custom_server(self):
|
|
"""Add a new custom server"""
|
|
# Generate a unique name
|
|
new_name = "custom_server"
|
|
counter = 1
|
|
while new_name in self.config_data.get("MCP_SERVERS", {}):
|
|
new_name = f"custom_server_{counter}"
|
|
counter += 1
|
|
|
|
# Add to config data
|
|
if "MCP_SERVERS" not in self.config_data:
|
|
self.config_data["MCP_SERVERS"] = {}
|
|
|
|
self.config_data["MCP_SERVERS"][new_name] = {
|
|
"enabled": True,
|
|
"raw_config": ' "command": "npx",\n "args": [\n "custom-mcp-server"\n ]'
|
|
}
|
|
|
|
# Update UI
|
|
self.update_servers_list()
|
|
|
|
# Select the new server
|
|
idx = self.servers_listbox.get(0, tk.END).index(new_name)
|
|
self.servers_listbox.selection_clear(0, tk.END)
|
|
self.servers_listbox.selection_set(idx)
|
|
self.servers_listbox.see(idx)
|
|
self.on_server_select(None) # Trigger the selection handler
|
|
|
|
def remove_server(self):
|
|
"""Remove the selected custom server"""
|
|
selection = self.servers_listbox.curselection()
|
|
if not selection:
|
|
return
|
|
|
|
selected_server = self.servers_listbox.get(selection[0])
|
|
|
|
# Confirmation
|
|
confirm = messagebox.askyesno(
|
|
"Confirm Removal",
|
|
f"Are you sure you want to remove the server '{selected_server}'?",
|
|
icon=messagebox.WARNING
|
|
)
|
|
|
|
if confirm:
|
|
# Remove from config data
|
|
if selected_server in self.config_data.get("MCP_SERVERS", {}):
|
|
del self.config_data["MCP_SERVERS"][selected_server]
|
|
|
|
# Update UI
|
|
self.update_servers_list()
|
|
|
|
# Clear selection
|
|
self.servers_listbox.selection_clear(0, tk.END)
|
|
|
|
# Hide server settings and show empty label
|
|
self.custom_frame.pack_forget()
|
|
self.empty_settings_label.pack(expand=True, pady=20)
|
|
self.remove_btn.config(state=tk.DISABLED)
|
|
|
|
def save_settings(self):
|
|
"""Save all settings to config.py and .env files"""
|
|
try:
|
|
# Update config data from UI
|
|
|
|
# API settings
|
|
self.config_data["OPENAI_API_BASE_URL"] = self.api_url_var.get()
|
|
self.config_data["LLM_MODEL"] = self.model_var.get()
|
|
|
|
# Environment variables
|
|
self.env_data["OPENAI_API_KEY"] = self.api_key_var.get()
|
|
self.env_data["EXA_API_KEY"] = self.exa_key_var.get()
|
|
|
|
# MCP Servers
|
|
if "MCP_SERVERS" not in self.config_data:
|
|
self.config_data["MCP_SERVERS"] = {}
|
|
|
|
# Exa server
|
|
if "exa" not in self.config_data["MCP_SERVERS"]:
|
|
self.config_data["MCP_SERVERS"]["exa"] = {}
|
|
|
|
self.config_data["MCP_SERVERS"]["exa"]["enabled"] = self.exa_enable_var.get()
|
|
self.config_data["MCP_SERVERS"]["exa"]["use_smithery"] = (self.exa_type_var.get() == "smithery")
|
|
if self.exa_type_var.get() == "local":
|
|
self.config_data["MCP_SERVERS"]["exa"]["server_path"] = self.exa_path_var.get()
|
|
|
|
# Chroma server
|
|
if "chroma" not in self.config_data["MCP_SERVERS"]:
|
|
self.config_data["MCP_SERVERS"]["chroma"] = {}
|
|
|
|
self.config_data["MCP_SERVERS"]["chroma"]["enabled"] = self.chroma_enable_var.get()
|
|
self.config_data["MCP_SERVERS"]["chroma"]["data_dir"] = self.chroma_dir_var.get()
|
|
|
|
# Custom server - check if one is currently selected
|
|
selection = self.servers_listbox.curselection()
|
|
if selection:
|
|
selected_server = self.servers_listbox.get(selection[0])
|
|
if selected_server not in ("exa", "chroma"):
|
|
# Update custom server settings
|
|
new_name = self.custom_name_var.get().strip()
|
|
|
|
if not new_name:
|
|
messagebox.showerror("Error", "Custom server name cannot be empty")
|
|
return
|
|
|
|
# Handle name change
|
|
if new_name != selected_server:
|
|
if new_name in self.config_data["MCP_SERVERS"]:
|
|
messagebox.showerror("Error", f"Server name '{new_name}' already exists")
|
|
return
|
|
|
|
# Copy config and delete old entry
|
|
self.config_data["MCP_SERVERS"][new_name] = self.config_data["MCP_SERVERS"][selected_server].copy()
|
|
del self.config_data["MCP_SERVERS"][selected_server]
|
|
|
|
# Update other settings
|
|
self.config_data["MCP_SERVERS"][new_name]["enabled"] = self.custom_enable_var.get()
|
|
self.config_data["MCP_SERVERS"][new_name]["raw_config"] = self.custom_config_text.get("1.0", tk.END).strip()
|
|
|
|
# Game window settings
|
|
self.config_data["GAME_WINDOW_CONFIG"] = {
|
|
"WINDOW_TITLE": self.window_title_var.get(),
|
|
"ENABLE_SCHEDULED_RESTART": self.restart_var.get(),
|
|
"RESTART_INTERVAL_MINUTES": self.interval_var.get(),
|
|
"GAME_EXECUTABLE_PATH": self.game_path_var.get(),
|
|
"GAME_WINDOW_X": self.pos_x_var.get(),
|
|
"GAME_WINDOW_Y": self.pos_y_var.get(),
|
|
"GAME_WINDOW_WIDTH": self.width_var.get(),
|
|
"GAME_WINDOW_HEIGHT": self.height_var.get(),
|
|
"MONITOR_INTERVAL_SECONDS": self.monitor_interval_var.get()
|
|
}
|
|
|
|
# Validate critical settings
|
|
if "exa" in self.config_data["MCP_SERVERS"] and self.config_data["MCP_SERVERS"]["exa"]["enabled"]:
|
|
if not self.exa_key_var.get():
|
|
messagebox.showerror("Validation Error", "Exa API Key is required when Exa server is enabled")
|
|
return
|
|
|
|
if self.exa_type_var.get() == "local" and not self.exa_path_var.get():
|
|
messagebox.showerror("Validation Error", "Exa Server Path is required for local server type")
|
|
return
|
|
|
|
# Generate config.py and .env files
|
|
save_env_file(self.env_data)
|
|
generate_config_file(self.config_data, self.env_data)
|
|
|
|
messagebox.showinfo("Success", "Settings saved successfully.\nRestart Wolf Chat for changes to take effect.")
|
|
self.destroy()
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"An error occurred while saving settings:\n{str(e)}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
# ===============================================================
|
|
# Main Entry Point
|
|
# ===============================================================
|
|
if __name__ == "__main__":
|
|
app = WolfChatSetup()
|
|
app.mainloop() |