From 01c42842707891fdbafdd5e89398fd65be5ec363 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 03:14:11 +0000 Subject: [PATCH] Add Groq provider to screen assistant model selection and API routing Agent-Logs-Url: https://github.com/AkhilKonduru1/Atoll/sessions/d40c666d-f589-48ca-9539-1fd33c799962 Co-authored-by: AkhilKonduru1 <210101196+AkhilKonduru1@users.noreply.github.com> --- .../ScreenAssistant/ChatPanels.swift | 5 +- .../ScreenAssistant/ModelSelectionPanel.swift | 49 ++++++++++++++++--- .../managers/ScreenAssistantManager.swift | 38 +++++++++++++- DynamicIsland/models/Constants.swift | 10 ++++ 4 files changed, 93 insertions(+), 9 deletions(-) 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..f369a38 100644 --- a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift +++ b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift @@ -126,6 +126,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 +171,21 @@ struct ModelSelectionView: View { .foregroundColor(.primary) LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) { - ForEach(AIModelProvider.allCases) { provider in + ForEach(AIModelProvider.allCases.filter { $0 != .groq }) { 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 +253,8 @@ struct ModelSelectionView: View { geminiApiKey: $geminiApiKey, openaiApiKey: $openaiApiKey, claudeApiKey: $claudeApiKey, - localEndpoint: $localEndpoint + localEndpoint: $localEndpoint, + groqApiKey: $groqApiKey ) } } @@ -291,22 +300,31 @@ 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] + if selectedModel == nil || !selectedProvider.supportedModels.contains(where: { $0.id == selectedModel?.id }) { + selectedModel = selectedProvider.supportedModels.first + } enableThinking = Defaults[.enableThinkingMode] geminiApiKey = Defaults[.geminiApiKey] openaiApiKey = Defaults[.openaiApiKey] claudeApiKey = Defaults[.claudeApiKey] localEndpoint = Defaults[.localModelEndpoint] + groqApiKey = Defaults[.groqApiKey] } private func saveConfiguration() { + if selectedModel == nil || !selectedProvider.supportedModels.contains(where: { $0.id == selectedModel?.id }) { + selectedModel = selectedProvider.supportedModels.first + } + Defaults[.selectedAIProvider] = selectedProvider Defaults[.selectedAIModel] = selectedModel Defaults[.enableThinkingMode] = enableThinking @@ -315,12 +333,20 @@ 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 + if !provider.supportedModels.contains(where: { $0.id == selectedModel?.id }) { + selectedModel = provider.supportedModels.first + } + } private func closePanel() { if let window = NSApp.windows.first(where: { $0 is ModelSelectionPanel }) { @@ -334,6 +360,7 @@ struct ProviderCard: View { let provider: AIModelProvider let isSelected: Bool let onSelect: () -> Void + var isWide: Bool = false var body: some View { VStack(spacing: 12) { @@ -374,6 +401,7 @@ struct ProviderCard: View { onSelect() } .animation(.easeInOut(duration: 0.2), value: isSelected) + .frame(maxWidth: .infinity, minHeight: isWide ? 110 : nil) } private func iconForProvider(_ provider: AIModelProvider) -> String { @@ -382,6 +410,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 +470,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 +507,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 +571,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..d6fe4e0 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,36 @@ 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 = AIModelProvider.groq.supportedModels.contains(where: { $0.id == selectedModel?.id }) + ? selectedModel!.id + : "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 +714,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 +742,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) } } @@ -792,6 +824,8 @@ class ScreenAssistantManager: NSObject, ObservableObject { parseGeminiResponse(data: data) case .openai: parseOpenAIResponse(data: data) + case .groq: + parseOpenAIResponse(data: data) case .claude: parseClaudeResponse(data: data) case .local: 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)