mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
commit
d4cb3ef2cf
4 changed files with 98 additions and 10 deletions
|
|
@ -450,6 +450,7 @@ struct ChatInputView: View {
|
||||||
case .openai: return "brain.head.profile"
|
case .openai: return "brain.head.profile"
|
||||||
case .claude: return "doc.text"
|
case .claude: return "doc.text"
|
||||||
case .local: return "server.rack"
|
case .local: return "server.rack"
|
||||||
|
case .groq: return "bolt.fill"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,6 +473,8 @@ struct ChatInputView: View {
|
||||||
case .local:
|
case .local:
|
||||||
// Local models don't need API keys
|
// Local models don't need API keys
|
||||||
apiKey = "local"
|
apiKey = "local"
|
||||||
|
case .groq:
|
||||||
|
apiKey = Defaults[.groqApiKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
if apiKey.isEmpty {
|
if apiKey.isEmpty {
|
||||||
|
|
@ -806,4 +809,4 @@ struct ScreenshotPopoverBackground: NSViewRepresentable {
|
||||||
ScreenCaptureVisibilityManager.shared.unregister(window)
|
ScreenCaptureVisibilityManager.shared.unregister(window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ class ModelSelectionPanel: NSPanel {
|
||||||
|
|
||||||
// MARK: - Model Selection View
|
// MARK: - Model Selection View
|
||||||
struct ModelSelectionView: View {
|
struct ModelSelectionView: View {
|
||||||
|
private let primaryProviders: [AIModelProvider] = [.gemini, .openai, .claude, .local]
|
||||||
@State private var selectedProvider: AIModelProvider = Defaults[.selectedAIProvider]
|
@State private var selectedProvider: AIModelProvider = Defaults[.selectedAIProvider]
|
||||||
@State private var selectedModel: AIModel? = Defaults[.selectedAIModel]
|
@State private var selectedModel: AIModel? = Defaults[.selectedAIModel]
|
||||||
@State private var enableThinking: Bool = Defaults[.enableThinkingMode]
|
@State private var enableThinking: Bool = Defaults[.enableThinkingMode]
|
||||||
|
|
@ -126,6 +127,7 @@ struct ModelSelectionView: View {
|
||||||
@State private var openaiApiKey: String = Defaults[.openaiApiKey]
|
@State private var openaiApiKey: String = Defaults[.openaiApiKey]
|
||||||
@State private var claudeApiKey: String = Defaults[.claudeApiKey]
|
@State private var claudeApiKey: String = Defaults[.claudeApiKey]
|
||||||
@State private var localEndpoint: String = Defaults[.localModelEndpoint]
|
@State private var localEndpoint: String = Defaults[.localModelEndpoint]
|
||||||
|
@State private var groqApiKey: String = Defaults[.groqApiKey]
|
||||||
|
|
||||||
@State private var showingApiKeyAlert = false
|
@State private var showingApiKeyAlert = false
|
||||||
|
|
||||||
|
|
@ -170,14 +172,21 @@ struct ModelSelectionView: View {
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
|
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
|
||||||
ForEach(AIModelProvider.allCases) { provider in
|
ForEach(primaryProviders) { provider in
|
||||||
ProviderCard(
|
ProviderCard(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
isSelected: selectedProvider == provider,
|
isSelected: selectedProvider == provider,
|
||||||
onSelect: { selectedProvider = provider }
|
onSelect: { selectProvider(provider) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProviderCard(
|
||||||
|
provider: .groq,
|
||||||
|
isSelected: selectedProvider == .groq,
|
||||||
|
onSelect: { selectProvider(.groq) },
|
||||||
|
isWide: true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
@ -245,7 +254,8 @@ struct ModelSelectionView: View {
|
||||||
geminiApiKey: $geminiApiKey,
|
geminiApiKey: $geminiApiKey,
|
||||||
openaiApiKey: $openaiApiKey,
|
openaiApiKey: $openaiApiKey,
|
||||||
claudeApiKey: $claudeApiKey,
|
claudeApiKey: $claudeApiKey,
|
||||||
localEndpoint: $localEndpoint
|
localEndpoint: $localEndpoint,
|
||||||
|
groqApiKey: $groqApiKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -291,22 +301,27 @@ struct ModelSelectionView: View {
|
||||||
return !claudeApiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
return !claudeApiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
case .local:
|
case .local:
|
||||||
return !localEndpoint.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
return !localEndpoint.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
case .groq:
|
||||||
|
return !groqApiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadCurrentConfiguration() {
|
private func loadCurrentConfiguration() {
|
||||||
selectedProvider = Defaults[.selectedAIProvider]
|
selectedProvider = Defaults[.selectedAIProvider]
|
||||||
// Default to the latest Gemini 2.5 Flash if no model is selected
|
selectedModel = Defaults[.selectedAIModel]
|
||||||
selectedModel = Defaults[.selectedAIModel] ?? AIModel(id: "gemini-2.5-flash", name: "Gemini 2.5 Flash", supportsThinking: true)
|
ensureValidModelSelection()
|
||||||
enableThinking = Defaults[.enableThinkingMode]
|
enableThinking = Defaults[.enableThinkingMode]
|
||||||
|
|
||||||
geminiApiKey = Defaults[.geminiApiKey]
|
geminiApiKey = Defaults[.geminiApiKey]
|
||||||
openaiApiKey = Defaults[.openaiApiKey]
|
openaiApiKey = Defaults[.openaiApiKey]
|
||||||
claudeApiKey = Defaults[.claudeApiKey]
|
claudeApiKey = Defaults[.claudeApiKey]
|
||||||
localEndpoint = Defaults[.localModelEndpoint]
|
localEndpoint = Defaults[.localModelEndpoint]
|
||||||
|
groqApiKey = Defaults[.groqApiKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveConfiguration() {
|
private func saveConfiguration() {
|
||||||
|
ensureValidModelSelection()
|
||||||
|
|
||||||
Defaults[.selectedAIProvider] = selectedProvider
|
Defaults[.selectedAIProvider] = selectedProvider
|
||||||
Defaults[.selectedAIModel] = selectedModel
|
Defaults[.selectedAIModel] = selectedModel
|
||||||
Defaults[.enableThinkingMode] = enableThinking
|
Defaults[.enableThinkingMode] = enableThinking
|
||||||
|
|
@ -315,12 +330,24 @@ struct ModelSelectionView: View {
|
||||||
Defaults[.openaiApiKey] = openaiApiKey
|
Defaults[.openaiApiKey] = openaiApiKey
|
||||||
Defaults[.claudeApiKey] = claudeApiKey
|
Defaults[.claudeApiKey] = claudeApiKey
|
||||||
Defaults[.localModelEndpoint] = localEndpoint
|
Defaults[.localModelEndpoint] = localEndpoint
|
||||||
|
Defaults[.groqApiKey] = groqApiKey
|
||||||
|
|
||||||
closePanel()
|
closePanel()
|
||||||
|
|
||||||
// Notify that configuration changed
|
// Notify that configuration changed
|
||||||
NotificationCenter.default.post(name: .aiModelConfigurationChanged, object: nil)
|
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() {
|
private func closePanel() {
|
||||||
if let window = NSApp.windows.first(where: { $0 is ModelSelectionPanel }) {
|
if let window = NSApp.windows.first(where: { $0 is ModelSelectionPanel }) {
|
||||||
|
|
@ -331,9 +358,11 @@ struct ModelSelectionView: View {
|
||||||
|
|
||||||
// MARK: - Provider Card
|
// MARK: - Provider Card
|
||||||
struct ProviderCard: View {
|
struct ProviderCard: View {
|
||||||
|
private let wideCardMinHeight: CGFloat = 110
|
||||||
let provider: AIModelProvider
|
let provider: AIModelProvider
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
let onSelect: () -> Void
|
let onSelect: () -> Void
|
||||||
|
var isWide: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
|
|
@ -374,6 +403,7 @@ struct ProviderCard: View {
|
||||||
onSelect()
|
onSelect()
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.2), value: isSelected)
|
.animation(.easeInOut(duration: 0.2), value: isSelected)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: isWide ? wideCardMinHeight : nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func iconForProvider(_ provider: AIModelProvider) -> String {
|
private func iconForProvider(_ provider: AIModelProvider) -> String {
|
||||||
|
|
@ -382,6 +412,7 @@ struct ProviderCard: View {
|
||||||
case .openai: return "brain.head.profile"
|
case .openai: return "brain.head.profile"
|
||||||
case .claude: return "doc.text"
|
case .claude: return "doc.text"
|
||||||
case .local: return "server.rack"
|
case .local: return "server.rack"
|
||||||
|
case .groq: return "bolt.fill"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -441,6 +472,7 @@ struct ApiConfigurationSection: View {
|
||||||
@Binding var openaiApiKey: String
|
@Binding var openaiApiKey: String
|
||||||
@Binding var claudeApiKey: String
|
@Binding var claudeApiKey: String
|
||||||
@Binding var localEndpoint: String
|
@Binding var localEndpoint: String
|
||||||
|
@Binding var groqApiKey: String
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
|
|
@ -477,6 +509,13 @@ struct ApiConfigurationSection: View {
|
||||||
helpText: "Ollama or compatible API endpoint",
|
helpText: "Ollama or compatible API endpoint",
|
||||||
isSecure: false
|
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)
|
.padding(16)
|
||||||
|
|
@ -534,4 +573,4 @@ struct ModelSelectionVisualEffectView: NSViewRepresentable {
|
||||||
// MARK: - Notification Extension
|
// MARK: - Notification Extension
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let aiModelConfigurationChanged = Notification.Name("aiModelConfigurationChanged")
|
static let aiModelConfigurationChanged = Notification.Name("aiModelConfigurationChanged")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -397,6 +397,8 @@ class ScreenAssistantManager: NSObject, ObservableObject {
|
||||||
sendToClaudeAPI(message: message, files: files)
|
sendToClaudeAPI(message: message, files: files)
|
||||||
case .local:
|
case .local:
|
||||||
sendToLocalAPI(message: message, files: files)
|
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)
|
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]) {
|
private func sendToClaudeAPI(message: String, files: [ScreenAssistantFile]) {
|
||||||
let apiKey = Defaults[.claudeApiKey]
|
let apiKey = Defaults[.claudeApiKey]
|
||||||
|
|
@ -682,7 +718,7 @@ class ScreenAssistantManager: NSObject, ObservableObject {
|
||||||
task?.resume()
|
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)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
|
@ -710,7 +746,7 @@ class ScreenAssistantManager: NSObject, ObservableObject {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
self.activeRequest = nil
|
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 {
|
switch provider {
|
||||||
case .gemini:
|
case .gemini:
|
||||||
parseGeminiResponse(data: data)
|
parseGeminiResponse(data: data)
|
||||||
case .openai:
|
case .openai, .groq:
|
||||||
parseOpenAIResponse(data: data)
|
parseOpenAIResponse(data: data)
|
||||||
case .claude:
|
case .claude:
|
||||||
parseClaudeResponse(data: data)
|
parseClaudeResponse(data: data)
|
||||||
|
|
|
||||||
|
|
@ -613,6 +613,7 @@ enum AIModelProvider: String, CaseIterable, Identifiable, Defaults.Serializable
|
||||||
case openai = "OpenAI GPT"
|
case openai = "OpenAI GPT"
|
||||||
case claude = "Claude"
|
case claude = "Claude"
|
||||||
case local = "Local Model"
|
case local = "Local Model"
|
||||||
|
case groq = "Groq"
|
||||||
|
|
||||||
var id: String { self.rawValue }
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
|
|
@ -626,6 +627,7 @@ enum AIModelProvider: String, CaseIterable, Identifiable, Defaults.Serializable
|
||||||
case .openai: return "OpenAI's GPT models with advanced reasoning"
|
case .openai: return "OpenAI's GPT models with advanced reasoning"
|
||||||
case .claude: return "Anthropic's Claude with strong analytical skills"
|
case .claude: return "Anthropic's Claude with strong analytical skills"
|
||||||
case .local: return "Local AI model (Ollama or similar)"
|
case .local: return "Local AI model (Ollama or similar)"
|
||||||
|
case .groq: return "Groq's fast inference for OpenAI-compatible models"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -666,6 +668,13 @@ enum AIModelProvider: String, CaseIterable, Identifiable, Defaults.Serializable
|
||||||
AIModel(id: "llama3.2", name: "Llama 3.2", supportsThinking: false),
|
AIModel(id: "llama3.2", name: "Llama 3.2", supportsThinking: false),
|
||||||
AIModel(id: "qwen2.5", name: "Qwen 2.5", 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: false),
|
||||||
|
AIModel(id: "mixtral-8x7b-32768", name: "Mixtral 8x7B", supportsThinking: false)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1054,6 +1063,7 @@ extension Defaults.Keys {
|
||||||
static let geminiApiKey = Key<String>("geminiApiKey", default: "")
|
static let geminiApiKey = Key<String>("geminiApiKey", default: "")
|
||||||
static let openaiApiKey = Key<String>("openaiApiKey", default: "")
|
static let openaiApiKey = Key<String>("openaiApiKey", default: "")
|
||||||
static let claudeApiKey = Key<String>("claudeApiKey", default: "")
|
static let claudeApiKey = Key<String>("claudeApiKey", default: "")
|
||||||
|
static let groqApiKey = Key<String>("groqApiKey", default: "")
|
||||||
static let selectedAIProvider = Key<AIModelProvider>("selectedAIProvider", default: .gemini)
|
static let selectedAIProvider = Key<AIModelProvider>("selectedAIProvider", default: .gemini)
|
||||||
static let selectedAIModel = Key<AIModel?>("selectedAIModel", default: nil)
|
static let selectedAIModel = Key<AIModel?>("selectedAIModel", default: nil)
|
||||||
static let enableThinkingMode = Key<Bool>("enableThinkingMode", default: false)
|
static let enableThinkingMode = Key<Bool>("enableThinkingMode", default: false)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue