diff --git a/DynamicIsland/components/ScreenAssistant/ChatPanels.swift b/DynamicIsland/components/ScreenAssistant/ChatPanels.swift index 095d491..2f79dee 100644 --- a/DynamicIsland/components/ScreenAssistant/ChatPanels.swift +++ b/DynamicIsland/components/ScreenAssistant/ChatPanels.swift @@ -450,6 +450,7 @@ struct ChatInputView: View { case .openai: return "brain.head.profile" case .claude: return "doc.text" case .local: return "server.rack" + case .groq: return "bolt.fill" } } @@ -472,6 +473,8 @@ struct ChatInputView: View { case .local: // Local models don't need API keys apiKey = "local" + case .groq: + apiKey = Defaults[.groqApiKey] } if apiKey.isEmpty { @@ -806,4 +809,4 @@ struct ScreenshotPopoverBackground: NSViewRepresentable { ScreenCaptureVisibilityManager.shared.unregister(window) } } -} \ No newline at end of file +} diff --git a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift index e8f819c..4786607 100644 --- a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift +++ b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift @@ -117,6 +117,7 @@ class ModelSelectionPanel: NSPanel { // MARK: - Model Selection View struct ModelSelectionView: View { + private let primaryProviders: [AIModelProvider] = [.gemini, .openai, .claude, .local] @State private var selectedProvider: AIModelProvider = Defaults[.selectedAIProvider] @State private var selectedModel: AIModel? = Defaults[.selectedAIModel] @State private var enableThinking: Bool = Defaults[.enableThinkingMode] @@ -126,6 +127,7 @@ struct ModelSelectionView: View { @State private var openaiApiKey: String = Defaults[.openaiApiKey] @State private var claudeApiKey: String = Defaults[.claudeApiKey] @State private var localEndpoint: String = Defaults[.localModelEndpoint] + @State private var groqApiKey: String = Defaults[.groqApiKey] @State private var showingApiKeyAlert = false @@ -170,14 +172,21 @@ struct ModelSelectionView: View { .foregroundColor(.primary) LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) { - ForEach(AIModelProvider.allCases) { provider in + ForEach(primaryProviders) { provider in ProviderCard( provider: provider, isSelected: selectedProvider == provider, - onSelect: { selectedProvider = provider } + onSelect: { selectProvider(provider) } ) } } + + ProviderCard( + provider: .groq, + isSelected: selectedProvider == .groq, + onSelect: { selectProvider(.groq) }, + isWide: true + ) } Divider() @@ -245,7 +254,8 @@ struct ModelSelectionView: View { geminiApiKey: $geminiApiKey, openaiApiKey: $openaiApiKey, claudeApiKey: $claudeApiKey, - localEndpoint: $localEndpoint + localEndpoint: $localEndpoint, + groqApiKey: $groqApiKey ) } } @@ -291,22 +301,27 @@ struct ModelSelectionView: View { return !claudeApiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty case .local: return !localEndpoint.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + case .groq: + return !groqApiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } } private func loadCurrentConfiguration() { selectedProvider = Defaults[.selectedAIProvider] - // Default to the latest Gemini 2.5 Flash if no model is selected - selectedModel = Defaults[.selectedAIModel] ?? AIModel(id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", supportsThinking: true) + selectedModel = Defaults[.selectedAIModel] + ensureValidModelSelection() enableThinking = Defaults[.enableThinkingMode] geminiApiKey = Defaults[.geminiApiKey] openaiApiKey = Defaults[.openaiApiKey] claudeApiKey = Defaults[.claudeApiKey] localEndpoint = Defaults[.localModelEndpoint] + groqApiKey = Defaults[.groqApiKey] } private func saveConfiguration() { + ensureValidModelSelection() + Defaults[.selectedAIProvider] = selectedProvider Defaults[.selectedAIModel] = selectedModel Defaults[.enableThinkingMode] = enableThinking @@ -315,12 +330,24 @@ struct ModelSelectionView: View { Defaults[.openaiApiKey] = openaiApiKey Defaults[.claudeApiKey] = claudeApiKey Defaults[.localModelEndpoint] = localEndpoint + Defaults[.groqApiKey] = groqApiKey closePanel() // Notify that configuration changed NotificationCenter.default.post(name: .aiModelConfigurationChanged, object: nil) } + + private func selectProvider(_ provider: AIModelProvider) { + selectedProvider = provider + ensureValidModelSelection() + } + + private func ensureValidModelSelection() { + if selectedModel == nil || !selectedProvider.supportedModels.contains(where: { $0.id == selectedModel?.id }) { + selectedModel = selectedProvider.supportedModels.first + } + } private func closePanel() { if let window = NSApp.windows.first(where: { $0 is ModelSelectionPanel }) { @@ -331,9 +358,11 @@ struct ModelSelectionView: View { // MARK: - Provider Card struct ProviderCard: View { + private let wideCardMinHeight: CGFloat = 110 let provider: AIModelProvider let isSelected: Bool let onSelect: () -> Void + var isWide: Bool = false var body: some View { VStack(spacing: 12) { @@ -374,6 +403,7 @@ struct ProviderCard: View { onSelect() } .animation(.easeInOut(duration: 0.2), value: isSelected) + .frame(maxWidth: .infinity, minHeight: isWide ? wideCardMinHeight : nil) } private func iconForProvider(_ provider: AIModelProvider) -> String { @@ -382,6 +412,7 @@ struct ProviderCard: View { case .openai: return "brain.head.profile" case .claude: return "doc.text" case .local: return "server.rack" + case .groq: return "bolt.fill" } } } @@ -441,6 +472,7 @@ struct ApiConfigurationSection: View { @Binding var openaiApiKey: String @Binding var claudeApiKey: String @Binding var localEndpoint: String + @Binding var groqApiKey: String var body: some View { VStack(spacing: 12) { @@ -477,6 +509,13 @@ struct ApiConfigurationSection: View { helpText: "Ollama or compatible API endpoint", isSecure: false ) + case .groq: + ApiKeyField( + title: "Groq API Key", + placeholder: "Enter your Groq API key", + value: $groqApiKey, + helpText: "Get your API key from Groq Console" + ) } } .padding(16) @@ -534,4 +573,4 @@ struct ModelSelectionVisualEffectView: NSViewRepresentable { // MARK: - Notification Extension extension Notification.Name { static let aiModelConfigurationChanged = Notification.Name("aiModelConfigurationChanged") -} \ No newline at end of file +} diff --git a/DynamicIsland/managers/ScreenAssistantManager.swift b/DynamicIsland/managers/ScreenAssistantManager.swift index 9bf0c2c..17b28ed 100644 --- a/DynamicIsland/managers/ScreenAssistantManager.swift +++ b/DynamicIsland/managers/ScreenAssistantManager.swift @@ -397,6 +397,8 @@ class ScreenAssistantManager: NSObject, ObservableObject { sendToClaudeAPI(message: message, files: files) case .local: sendToLocalAPI(message: message, files: files) + case .groq: + sendToGroqAPI(message: message, files: files) } } @@ -445,6 +447,40 @@ class ScreenAssistantManager: NSObject, ObservableObject { performOpenAIRequest(url: url, requestBody: buildOpenAIRequestBody(message: message, files: files, model: modelId), apiKey: apiKey) } + + private func sendToGroqAPI(message: String, files: [ScreenAssistantFile]) { + let apiKey = Defaults[.groqApiKey] + guard !apiKey.isEmpty else { + print("❌ ScreenAssistant: No Groq API key configured") + addAssistantMessage("Error: No Groq API key configured. Please set your API key in model settings.") + isLoading = false + return + } + + // Get selected model or default to llama-3.3-70b-versatile + let selectedModel = Defaults[.selectedAIModel] + let modelId: String + if let selectedId = selectedModel?.id, + AIModelProvider.groq.supportedModels.contains(where: { $0.id == selectedId }) { + modelId = selectedId + } else { + modelId = "llama-3.3-70b-versatile" + } + + guard let url = URL(string: "https://api.groq.com/openai/v1/chat/completions") else { + print("❌ ScreenAssistant: Invalid Groq API URL") + addAssistantMessage("Error: Invalid API URL") + isLoading = false + return + } + + performOpenAIRequest( + url: url, + requestBody: buildOpenAIRequestBody(message: message, files: files, model: modelId), + apiKey: apiKey, + provider: .groq + ) + } private func sendToClaudeAPI(message: String, files: [ScreenAssistantFile]) { let apiKey = Defaults[.claudeApiKey] @@ -682,7 +718,7 @@ class ScreenAssistantManager: NSObject, ObservableObject { task?.resume() } - private func performOpenAIRequest(url: URL, requestBody: [String: Any], apiKey: String) { + private func performOpenAIRequest(url: URL, requestBody: [String: Any], apiKey: String, provider: AIModelProvider = .openai) { var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") @@ -710,7 +746,7 @@ class ScreenAssistantManager: NSObject, ObservableObject { self.isLoading = false self.activeRequest = nil - self.handleResponse(data: data, response: response, error: error, provider: .openai) + self.handleResponse(data: data, response: response, error: error, provider: provider) } } @@ -790,7 +826,7 @@ class ScreenAssistantManager: NSObject, ObservableObject { switch provider { case .gemini: parseGeminiResponse(data: data) - case .openai: + case .openai, .groq: parseOpenAIResponse(data: data) case .claude: parseClaudeResponse(data: data) diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index c15f9ea..e45029b 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -611,6 +611,7 @@ enum AIModelProvider: String, CaseIterable, Identifiable, Defaults.Serializable case openai = "OpenAI GPT" case claude = "Claude" case local = "Local Model" + case groq = "Groq" var id: String { self.rawValue } @@ -624,6 +625,7 @@ enum AIModelProvider: String, CaseIterable, Identifiable, Defaults.Serializable case .openai: return "OpenAI's GPT models with advanced reasoning" case .claude: return "Anthropic's Claude with strong analytical skills" case .local: return "Local AI model (Ollama or similar)" + case .groq: return "Groq's fast inference for OpenAI-compatible models" } } @@ -664,6 +666,13 @@ enum AIModelProvider: String, CaseIterable, Identifiable, Defaults.Serializable AIModel(id: "llama3.2", name: "Llama 3.2", supportsThinking: false), AIModel(id: "qwen2.5", name: "Qwen 2.5", supportsThinking: false) ] + case .groq: + return [ + AIModel(id: "llama-3.3-70b-versatile", name: "Llama 3.3 70B Versatile", supportsThinking: false), + AIModel(id: "llama-3.1-8b-instant", name: "Llama 3.1 8B Instant", supportsThinking: false), + AIModel(id: "qwen-qwq-32b", name: "Qwen QWQ 32B", supportsThinking: true), + AIModel(id: "mixtral-8x7b-32768", name: "Mixtral 8x7B", supportsThinking: false) + ] } } } @@ -1052,6 +1061,7 @@ extension Defaults.Keys { static let geminiApiKey = Key("geminiApiKey", default: "") static let openaiApiKey = Key("openaiApiKey", default: "") static let claudeApiKey = Key("claudeApiKey", default: "") + static let groqApiKey = Key("groqApiKey", default: "") static let selectedAIProvider = Key("selectedAIProvider", default: .gemini) static let selectedAIModel = Key("selectedAIModel", default: nil) static let enableThinkingMode = Key("enableThinkingMode", default: false)