mirror of
https://github.com/hanzoai/prompt-sage.git
synced 2026-10-30 18:23:36 +00:00
- Add environment variable support for OpenAI, Anthropic, and Google API keys - Fix form functionality in Preferences window - Create comprehensive LLM.md architecture documentation - Update README with improved instructions for API keys - Add launcher script for setting environment variables - Set LSUIElement to true in Info.plist for proper menu bar app behavior
471 lines
18 KiB
Swift
471 lines
18 KiB
Swift
import SwiftUI
|
|
|
|
struct Preferences: View {
|
|
// App state
|
|
@State private var isActive: Bool = true
|
|
@State private var showNotifications: Bool = true
|
|
@State private var automaticEnhancement: Bool = false
|
|
|
|
// LLM configuration
|
|
@State private var selectedProvider: LLMProvider = .openAI
|
|
@State private var apiKey: String = ""
|
|
@State private var enhancementStyle: EnhancementStyle = .balanced
|
|
@State private var temperature: Double = 0.7
|
|
|
|
// OpenAI specific
|
|
@State private var openAIModel: String = "gpt-4o"
|
|
@State private var openAIModels = ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"]
|
|
|
|
// Anthropic specific
|
|
@State private var anthropicModel: String = "claude-3-haiku-20240307"
|
|
@State private var anthropicModels = ["claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"]
|
|
|
|
// Google specific
|
|
@State private var googleModel: String = "gemini-pro"
|
|
@State private var googleModels = ["gemini-pro", "gemini-ultra"]
|
|
|
|
// Local model specific
|
|
@State private var localModel: String = "llama3"
|
|
@State private var localModels = ["llama3", "mistral", "mixtral", "phi3"]
|
|
@State private var localModelURL: String = "http://localhost:11434"
|
|
|
|
// Custom settings
|
|
@State private var customAITools: String = ""
|
|
@State private var customPromptPatterns: String = ""
|
|
|
|
// UI state
|
|
@State private var selectedTab = 0
|
|
|
|
// Accent color
|
|
let accentColor = Color(red: 108/255, green: 92/255, blue: 231/255)
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Tab bar
|
|
HStack(spacing: 0) {
|
|
tabButton("General", index: 0)
|
|
tabButton("LLM Settings", index: 1)
|
|
tabButton("Detection", index: 2)
|
|
tabButton("Advanced", index: 3)
|
|
}
|
|
.background(Color.white)
|
|
.overlay(
|
|
Rectangle()
|
|
.frame(height: 1)
|
|
.foregroundColor(Color.gray.opacity(0.2)),
|
|
alignment: .bottom
|
|
)
|
|
|
|
// Content area
|
|
TabView(selection: $selectedTab) {
|
|
generalTab
|
|
.tag(0)
|
|
|
|
llmSettingsTab
|
|
.tag(1)
|
|
|
|
detectionTab
|
|
.tag(2)
|
|
|
|
advancedTab
|
|
.tag(3)
|
|
}
|
|
.tabViewStyle(DefaultTabViewStyle())
|
|
// Removed the indexViewStyle line as it's unavailable in macOS
|
|
.padding(20)
|
|
}
|
|
.frame(width: 500, height: 400)
|
|
.onAppear {
|
|
loadPreferences()
|
|
}
|
|
}
|
|
|
|
// MARK: - Tabs
|
|
|
|
// General settings tab
|
|
private var generalTab: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
Text("General Settings")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
|
|
Toggle("Active", isOn: $isActive)
|
|
.onChange(of: isActive) { _ in savePreferences() }
|
|
|
|
Toggle("Show Notifications", isOn: $showNotifications)
|
|
.onChange(of: showNotifications) { _ in savePreferences() }
|
|
|
|
Toggle("Automatic Enhancement", isOn: $automaticEnhancement)
|
|
.onChange(of: automaticEnhancement) { _ in savePreferences() }
|
|
|
|
Text("Enhancement Style")
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
|
Picker("Style", selection: $enhancementStyle) {
|
|
ForEach(EnhancementStyle.allCases) { style in
|
|
Text(style.rawValue).tag(style)
|
|
}
|
|
}
|
|
.pickerStyle(SegmentedPickerStyle())
|
|
.onChange(of: enhancementStyle) { _ in savePreferences() }
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
// LLM Settings tab
|
|
private var llmSettingsTab: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
Text("LLM Provider Settings")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
|
|
Picker("Provider", selection: $selectedProvider) {
|
|
ForEach(LLMProvider.allCases) { provider in
|
|
Text(provider.rawValue).tag(provider)
|
|
}
|
|
}
|
|
.pickerStyle(SegmentedPickerStyle())
|
|
.onChange(of: selectedProvider) { _ in savePreferences() }
|
|
|
|
// API Key (except for local provider)
|
|
if selectedProvider != .local {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("API Key")
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
|
let envKeyName = getEnvironmentKeyName(for: selectedProvider)
|
|
if ProcessInfo.processInfo.environment[envKeyName] != nil {
|
|
HStack {
|
|
Text("Using API key from \(envKeyName) environment variable")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.green)
|
|
|
|
Spacer()
|
|
|
|
Button(action: {
|
|
// Clear stored key and reload from env variable
|
|
UserDefaults.standard.removeObject(forKey: "APIKey")
|
|
let enhancer = PromptEnhancer()
|
|
apiKey = enhancer.apiKey
|
|
}) {
|
|
Text("Reload")
|
|
.font(.system(size: 12))
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
}
|
|
|
|
SecureField("Enter API key", text: $apiKey)
|
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
|
.onChange(of: apiKey) { _ in savePreferences() }
|
|
}
|
|
}
|
|
|
|
// Provider-specific settings
|
|
switch selectedProvider {
|
|
case .openAI:
|
|
providerModelPicker(title: "OpenAI Model", selection: $openAIModel, options: openAIModels)
|
|
case .anthropic:
|
|
providerModelPicker(title: "Claude Model", selection: $anthropicModel, options: anthropicModels)
|
|
case .googleAI:
|
|
providerModelPicker(title: "Gemini Model", selection: $googleModel, options: googleModels)
|
|
case .local:
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
providerModelPicker(title: "Local Model", selection: $localModel, options: localModels)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Ollama URL")
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
|
TextField("http://localhost:11434", text: $localModelURL)
|
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
|
.onChange(of: localModelURL) { _ in savePreferences() }
|
|
}
|
|
}
|
|
}
|
|
|
|
// Temperature slider
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack {
|
|
Text("Temperature")
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
|
Spacer()
|
|
|
|
Text(String(format: "%.1f", temperature))
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundColor(.gray)
|
|
}
|
|
|
|
Slider(value: $temperature, in: 0...1, step: 0.1)
|
|
.accentColor(accentColor)
|
|
.onChange(of: temperature) { _ in savePreferences() }
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
// Detection settings tab
|
|
private var detectionTab: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
Text("Detection Settings")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Custom AI Tools (One per line)")
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
|
TextEditor(text: $customAITools)
|
|
.font(.system(size: 12))
|
|
.frame(height: 100)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
|
|
)
|
|
.onChange(of: customAITools) { _ in savePreferences() }
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Custom Prompt Patterns (One per line)")
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
|
TextEditor(text: $customPromptPatterns)
|
|
.font(.system(size: 12))
|
|
.frame(height: 100)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
|
|
)
|
|
.onChange(of: customPromptPatterns) { _ in savePreferences() }
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
// Advanced settings tab
|
|
private var advancedTab: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
Text("Advanced Settings")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
|
|
Button(action: {
|
|
// Reset all preferences to defaults
|
|
resetPreferences()
|
|
}) {
|
|
Text("Reset All Preferences")
|
|
.foregroundColor(.red)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
|
|
Divider()
|
|
|
|
Text("About PromptSage")
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
|
Text("Version 1.0.0")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.gray)
|
|
|
|
HStack {
|
|
Text("© 2025 PromptSage")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.gray)
|
|
|
|
Spacer()
|
|
|
|
Button(action: {
|
|
// Open website
|
|
NSWorkspace.shared.open(URL(string: "https://promptsage.com")!)
|
|
}) {
|
|
Text("Visit Website")
|
|
.font(.system(size: 12, weight: .medium))
|
|
.foregroundColor(accentColor)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
// MARK: - Components
|
|
|
|
// Tab button
|
|
private func tabButton(_ title: String, index: Int) -> some View {
|
|
Button(action: {
|
|
selectedTab = index
|
|
}) {
|
|
Text(title)
|
|
.font(.system(size: 14, weight: selectedTab == index ? .semibold : .regular))
|
|
.padding(.vertical, 12)
|
|
.frame(maxWidth: .infinity)
|
|
.foregroundColor(selectedTab == index ? accentColor : Color.gray)
|
|
.background(Color.white)
|
|
.overlay(
|
|
Rectangle()
|
|
.frame(height: 2)
|
|
.foregroundColor(selectedTab == index ? accentColor : Color.clear),
|
|
alignment: .bottom
|
|
)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
|
|
// Provider model picker
|
|
private func providerModelPicker(title: String, selection: Binding<String>, options: [String]) -> some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(title)
|
|
.font(.system(size: 14, weight: .medium))
|
|
|
|
Picker(title, selection: selection) {
|
|
ForEach(options, id: \.self) { option in
|
|
Text(option).tag(option)
|
|
}
|
|
}
|
|
// Changed from PopUpButtonStyle to default picker style compatible with macOS
|
|
.pickerStyle(DefaultPickerStyle())
|
|
.onChange(of: selection.wrappedValue) { _ in savePreferences() }
|
|
}
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
// Get the environment variable name for a given provider
|
|
private func getEnvironmentKeyName(for provider: LLMProvider) -> String {
|
|
switch provider {
|
|
case .openAI:
|
|
return "OPENAI_API_KEY"
|
|
case .anthropic:
|
|
return "ANTHROPIC_API_KEY"
|
|
case .googleAI:
|
|
return "GOOGLE_API_KEY"
|
|
case .local:
|
|
return "" // Local provider doesn't use API keys
|
|
}
|
|
}
|
|
|
|
// MARK: - Preferences Management
|
|
|
|
// Load preferences from UserDefaults
|
|
private func loadPreferences() {
|
|
let defaults = UserDefaults.standard
|
|
|
|
// General settings
|
|
isActive = defaults.bool(forKey: "IsActive")
|
|
showNotifications = defaults.bool(forKey: "ShowNotifications")
|
|
automaticEnhancement = defaults.bool(forKey: "AutomaticEnhancement")
|
|
|
|
// Get enhancement style
|
|
if let styleString = defaults.string(forKey: "EnhancementStyle"),
|
|
let style = EnhancementStyle(rawValue: styleString) {
|
|
enhancementStyle = style
|
|
}
|
|
|
|
// LLM settings
|
|
if let providerString = defaults.string(forKey: "LLMProvider"),
|
|
let provider = LLMProvider(rawValue: providerString) {
|
|
selectedProvider = provider
|
|
}
|
|
|
|
// Get API key from PromptEnhancer to ensure we get env variables if present
|
|
let enhancer = PromptEnhancer()
|
|
apiKey = enhancer.apiKey
|
|
temperature = defaults.double(forKey: "Temperature")
|
|
|
|
// Set default temperature if not found
|
|
if temperature == 0 {
|
|
temperature = 0.7
|
|
}
|
|
|
|
// Provider-specific models
|
|
openAIModel = defaults.string(forKey: "OpenAIModel") ?? "gpt-4o"
|
|
anthropicModel = defaults.string(forKey: "AnthropicModel") ?? "claude-3-haiku-20240307"
|
|
googleModel = defaults.string(forKey: "GoogleModel") ?? "gemini-pro"
|
|
localModel = defaults.string(forKey: "LocalModel") ?? "llama3"
|
|
localModelURL = defaults.string(forKey: "LocalModelURL") ?? "http://localhost:11434"
|
|
|
|
// Custom detection settings
|
|
if let customTools = defaults.stringArray(forKey: "CustomAITools") {
|
|
customAITools = customTools.joined(separator: "\n")
|
|
}
|
|
|
|
if let customPatterns = defaults.stringArray(forKey: "CustomPromptPatterns") {
|
|
customPromptPatterns = customPatterns.joined(separator: "\n")
|
|
}
|
|
}
|
|
|
|
// Save preferences to UserDefaults
|
|
private func savePreferences() {
|
|
let defaults = UserDefaults.standard
|
|
|
|
// General settings
|
|
defaults.set(isActive, forKey: "IsActive")
|
|
defaults.set(showNotifications, forKey: "ShowNotifications")
|
|
defaults.set(automaticEnhancement, forKey: "AutomaticEnhancement")
|
|
defaults.set(enhancementStyle.rawValue, forKey: "EnhancementStyle")
|
|
|
|
// LLM settings
|
|
defaults.set(selectedProvider.rawValue, forKey: "LLMProvider")
|
|
defaults.set(apiKey, forKey: "APIKey")
|
|
defaults.set(temperature, forKey: "Temperature")
|
|
|
|
// Provider-specific models
|
|
defaults.set(openAIModel, forKey: "OpenAIModel")
|
|
defaults.set(anthropicModel, forKey: "AnthropicModel")
|
|
defaults.set(googleModel, forKey: "GoogleModel")
|
|
defaults.set(localModel, forKey: "LocalModel")
|
|
defaults.set(localModelURL, forKey: "LocalModelURL")
|
|
|
|
// Custom detection settings
|
|
let customToolsArray = customAITools.split(separator: "\n").map { String($0) }
|
|
defaults.set(customToolsArray, forKey: "CustomAITools")
|
|
|
|
let customPatternsArray = customPromptPatterns.split(separator: "\n").map { String($0) }
|
|
defaults.set(customPatternsArray, forKey: "CustomPromptPatterns")
|
|
}
|
|
|
|
// Reset preferences to defaults
|
|
private func resetPreferences() {
|
|
// General settings
|
|
isActive = true
|
|
showNotifications = true
|
|
automaticEnhancement = false
|
|
enhancementStyle = .balanced
|
|
|
|
// LLM settings
|
|
selectedProvider = .openAI
|
|
apiKey = ""
|
|
temperature = 0.7
|
|
|
|
// Provider-specific models
|
|
openAIModel = "gpt-4o"
|
|
anthropicModel = "claude-3-haiku-20240307"
|
|
googleModel = "gemini-pro"
|
|
localModel = "llama3"
|
|
localModelURL = "http://localhost:11434"
|
|
|
|
// Custom detection settings
|
|
customAITools = ""
|
|
customPromptPatterns = ""
|
|
|
|
// Save the defaults
|
|
savePreferences()
|
|
}
|
|
}
|
|
|
|
// Preview provider
|
|
struct Preferences_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
Preferences()
|
|
}
|
|
}
|