alpha-arena/macos-app/AlphaArena/Views/PreferencesView.swift
oho 774f8b3f61 Initial commit: Alpha Arena - Polymarket Autonomous Trading Harness
A comprehensive autonomous trading system for Polymarket prediction markets
featuring multi-LLM provider support, a native macOS menu bar app, and a
web-based control dashboard.

Key features:
- Multi-agent trading system (Research, Risk, Execution, Reflection agents)
- LLM provider flexibility (Anthropic, OpenAI, Google, xAI, Local models)
- Automatic provider fallback chain for resilience
- Native Swift/SwiftUI macOS menu bar application
- FastAPI web dashboard with real-time WebSocket updates
- Risk management with kill switch
- Technical indicators and market analysis
2026-01-12 23:04:58 +01:00

511 lines
17 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
/// Preferences window view
struct PreferencesView: View {
@ObservedObject var settings = AppSettings.shared
@StateObject private var llmState = LLMProviderState()
@State private var showingPathPicker = false
private let apiClient = APIClient()
var body: some View {
TabView {
generalTab
.tabItem {
Label("General", systemImage: "gear")
}
llmProvidersTab
.tabItem {
Label("LLM", systemImage: "brain")
}
networkTab
.tabItem {
Label("Network", systemImage: "network")
}
notificationsTab
.tabItem {
Label("Notifications", systemImage: "bell")
}
aboutTab
.tabItem {
Label("About", systemImage: "info.circle")
}
}
.frame(width: 500, height: 380)
.task {
await fetchLLMState()
}
}
// MARK: - General Tab
private var generalTab: some View {
Form {
Section {
HStack {
TextField("Project Path", text: $settings.projectPath)
.textFieldStyle(.roundedBorder)
Button("Browse...") {
showingPathPicker = true
}
}
.fileImporter(
isPresented: $showingPathPicker,
allowedContentTypes: [.folder]
) { result in
if case .success(let url) = result {
settings.projectPath = url.path
settings.saveProjectPath()
}
}
Text("Path to the Alpha Arena project directory containing the 'alpha' script.")
.font(.caption)
.foregroundColor(.secondary)
} header: {
Text("Project Location")
}
Section {
Picker("Default Port", selection: $settings.serverPort) {
Text("8000").tag(8000)
Text("8080").tag(8080)
Text("3000").tag(3000)
}
.pickerStyle(.menu)
} header: {
Text("Server")
}
Section {
Toggle("Auto-connect WebSocket", isOn: $settings.autoConnectWebSocket)
Text("Automatically connect to the WebSocket for real-time updates when the server starts.")
.font(.caption)
.foregroundColor(.secondary)
} header: {
Text("Connection")
}
}
.padding()
}
// MARK: - LLM Providers Tab
private var llmProvidersTab: some View {
Form {
Section {
if !llmState.isInitialized {
VStack(spacing: 12) {
Image(systemName: "exclamationmark.triangle")
.font(.title)
.foregroundColor(.orange)
Text("Provider registry not initialized")
.font(.headline)
Text("Start the server and click Initialize to connect to LLM providers.")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Button("Initialize Providers") {
Task { await initializeProviders() }
}
.buttonStyle(.borderedProminent)
.disabled(llmState.isInitializing)
}
.frame(maxWidth: .infinity)
.padding()
} else {
HStack {
Text("Active Provider:")
.foregroundColor(.secondary)
Text(llmState.activeProvider?.displayName ?? "None")
.fontWeight(.semibold)
Spacer()
Button("Refresh") {
Task { await fetchLLMState() }
}
}
}
} header: {
Text("Status")
}
if llmState.isInitialized {
Section {
ForEach(LLMProviderType.allCases) { provider in
ProviderRow(
provider: provider,
isActive: llmState.activeProvider == provider,
isConnected: llmState.connectedProviders.contains(provider),
healthStatus: llmState.getHealthStatus(for: provider)
) {
Task { await setActiveProvider(provider) }
}
}
} header: {
Text("Providers")
}
Section {
VStack(alignment: .leading, spacing: 8) {
Text("If the active provider fails, the system will try providers in this order:")
.font(.caption)
.foregroundColor(.secondary)
ForEach(Array(llmState.fallbackOrder.enumerated()), id: \.element) { index, provider in
HStack {
Text("\(index + 1).")
.foregroundColor(.secondary)
.frame(width: 20)
Text(provider.displayName)
Spacer()
if index > 0 {
Button("") {
moveFallback(index: index, direction: -1)
}
.buttonStyle(.plain)
}
if index < llmState.fallbackOrder.count - 1 {
Button("") {
moveFallback(index: index, direction: 1)
}
.buttonStyle(.plain)
}
}
.padding(.vertical, 2)
}
Button("Save Fallback Order") {
Task { await saveFallbackOrder() }
}
.buttonStyle(.bordered)
.padding(.top, 8)
}
} header: {
Text("Fallback Order")
}
}
}
.padding()
}
// MARK: - LLM Helper Methods
private func fetchLLMState() async {
do {
let response = try await apiClient.getLLMProviders()
await MainActor.run {
llmState.update(from: response)
}
} catch {
// Server might not be running
}
}
private func initializeProviders() async {
await MainActor.run {
llmState.isInitializing = true
}
do {
let response = try await apiClient.initializeLLMProviders()
await MainActor.run {
llmState.isInitialized = true
if let active = response.activeProvider {
llmState.activeProvider = LLMProviderType(rawValue: active)
}
if let connected = response.connectedProviders {
llmState.connectedProviders = Set(connected.compactMap { LLMProviderType(rawValue: $0) })
}
}
} catch {
await MainActor.run {
llmState.errorMessage = error.localizedDescription
}
}
await MainActor.run {
llmState.isInitializing = false
}
}
private func setActiveProvider(_ provider: LLMProviderType) async {
guard llmState.connectedProviders.contains(provider) else { return }
do {
try await apiClient.setActiveProvider(provider.rawValue)
await MainActor.run {
llmState.activeProvider = provider
}
} catch {
await MainActor.run {
llmState.errorMessage = error.localizedDescription
}
}
}
private func moveFallback(index: Int, direction: Int) {
let newIndex = index + direction
guard newIndex >= 0 && newIndex < llmState.fallbackOrder.count else { return }
llmState.fallbackOrder.swapAt(index, newIndex)
}
private func saveFallbackOrder() async {
do {
let order = llmState.fallbackOrder.map { $0.rawValue }
try await apiClient.setFallbackOrder(order)
} catch {
await MainActor.run {
llmState.errorMessage = error.localizedDescription
}
}
}
// MARK: - Network Tab
private var networkTab: some View {
Form {
Section {
Picker("Default Network Mode", selection: $settings.networkMode) {
ForEach(NetworkMode.allCases, id: \.rawValue) { mode in
VStack(alignment: .leading) {
Text(mode.displayName)
Text(mode.description)
.font(.caption)
.foregroundColor(.secondary)
}
.tag(mode)
}
}
.pickerStyle(.radioGroup)
} header: {
Text("Network Mode")
}
Section {
VStack(alignment: .leading, spacing: 8) {
Label {
VStack(alignment: .leading) {
Text("Localhost Only (127.0.0.1)")
.font(.headline)
Text("Server is only accessible from this machine. More secure.")
.font(.caption)
.foregroundColor(.secondary)
}
} icon: {
Image(systemName: "lock.shield")
.foregroundColor(.green)
}
Label {
VStack(alignment: .leading) {
Text("LAN Access (0.0.0.0)")
.font(.headline)
Text("Server is accessible from other devices on your local network.")
.font(.caption)
.foregroundColor(.secondary)
}
} icon: {
Image(systemName: "network")
.foregroundColor(.blue)
}
}
} header: {
Text("About Network Modes")
}
}
.padding()
}
// MARK: - Notifications Tab
private var notificationsTab: some View {
Form {
Section {
Toggle("Show Notifications", isOn: $settings.showNotifications)
Text("Show macOS notifications for server status changes and trading events.")
.font(.caption)
.foregroundColor(.secondary)
} header: {
Text("Notifications")
}
Section {
VStack(alignment: .leading, spacing: 8) {
NotificationExample(
title: "Alpha Arena Started",
message: "Server is running at http://127.0.0.1:8000"
)
NotificationExample(
title: "Kill Switch Activated",
message: "Trading stopped due to loss limit"
)
}
} header: {
Text("Example Notifications")
}
}
.padding()
}
// MARK: - About Tab
private var aboutTab: some View {
VStack(spacing: 16) {
Image(systemName: "chart.line.uptrend.xyaxis.circle.fill")
.font(.system(size: 64))
.foregroundStyle(.green, .green.opacity(0.3))
Text("Alpha Arena")
.font(.title)
.fontWeight(.bold)
Text("Version 1.0.0")
.foregroundColor(.secondary)
Text("Menu bar controller for Alpha Arena trading system")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
Spacer()
HStack(spacing: 20) {
Button("View Logs") {
openLogs()
}
Button("Open Dashboard") {
openDashboard()
}
.buttonStyle(.borderedProminent)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
// MARK: - Helper Methods
private func openLogs() {
let projectPath = settings.projectPath
guard !projectPath.isEmpty else { return }
let script = """
tell application "Terminal"
do script "cd '\(projectPath)' && ./alpha logs"
activate
end tell
"""
var error: NSDictionary?
if let scriptObject = NSAppleScript(source: script) {
scriptObject.executeAndReturnError(&error)
}
}
private func openDashboard() {
let host = settings.networkMode == .lan ? (getLocalIPAddress() ?? "127.0.0.1") : "127.0.0.1"
if let url = URL(string: "http://\(host):\(settings.serverPort)") {
NSWorkspace.shared.open(url)
}
}
}
// MARK: - Notification Example View
struct NotificationExample: View {
let title: String
let message: String
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: "app.badge")
.font(.title2)
.foregroundColor(.accentColor)
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.caption)
.fontWeight(.semibold)
Text(message)
.font(.caption2)
.foregroundColor(.secondary)
}
}
.padding(8)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
}
}
// MARK: - Provider Row
struct ProviderRow: View {
let provider: LLMProviderType
let isActive: Bool
let isConnected: Bool
let healthStatus: HealthStatus
let onSelect: () -> Void
var body: some View {
HStack {
Image(systemName: provider.icon)
.foregroundColor(isConnected ? .accentColor : .secondary)
.frame(width: 20)
VStack(alignment: .leading, spacing: 2) {
Text(provider.displayName)
.fontWeight(isActive ? .semibold : .regular)
Text(provider.description)
.font(.caption2)
.foregroundColor(.secondary)
}
Spacer()
// Health indicator
Circle()
.fill(healthColor)
.frame(width: 8, height: 8)
// Status badge
if isActive {
Text("Active")
.font(.caption2)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.accentColor.opacity(0.2))
.foregroundColor(.accentColor)
.cornerRadius(4)
} else if isConnected {
Button("Select") {
onSelect()
}
.buttonStyle(.bordered)
.controlSize(.small)
} else {
Text("Not Available")
.font(.caption2)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 4)
}
private var healthColor: Color {
switch healthStatus {
case .healthy: return .green
case .degraded: return .yellow
case .unhealthy: return .red
case .unknown: return .gray
}
}
}
#Preview {
PreferencesView()
}