HuggingChat-Mac/Settings/GeneralSettings.swift (223 lines of code) (raw):

// // GeneralSettings.swift // HuggingChat-Mac // // Created by Cyril Zakka on 8/22/24. // import SwiftUI import LaunchAtLogin import KeyboardShortcuts import Combine struct GeneralSettingsView: View { @Environment(\.openWindow) private var openWindow @Environment(ModelManager.self) private var modelManager @Environment(ConversationViewModel.self) private var conversationManager @State var externalModels: [LLMModel] = [] @State var cancellables = [AnyCancellable]() @AppStorage("baseURL") private var baseURL: String = "https://huggingface.co" @AppStorage("userLoggedIn") private var userLoggedIn: Bool = false @AppStorage("launchAtLogin") private var launchAtLogin = false @AppStorage("hideDock") private var hideDock: Bool = false @AppStorage("localModel") private var selectedLocalModel: String = "None" @AppStorage("externalModel") private var selectedExternalModel: String = "meta-llama/Meta-Llama-3.1-70B-Instruct" @AppStorage("useWebSearch") private var useWebSearch = false @AppStorage("chatClearInterval") private var chatClearInterval: String = "never" @AppStorage("isLocalGeneration") private var isLocalGeneration: Bool = false @AppStorage("useContext") private var useContext: Bool = false var body: some View { Form { Section("Account", content: { HStack { if let currentUser = HuggingChatSession.shared.currentUser { AsyncImage(url: currentUser.avatarUrl) { phase in switch phase { case .failure: ZStack { Circle() .fill(.quinary) .frame(height: 40) Text("🤗") } case .success(let image): image .resizable() .scaledToFit() .frame(height: 40) .clipShape(Circle()) default: ZStack { Circle() .fill(.quinary) .frame(height: 40) ProgressView() .controlSize(.small) } } } } else { ZStack { Circle() .fill(.quaternary) .frame(height: 40) Text("🤗") } } if HuggingChatSession.shared.currentUser == nil { // Not logged in VStack(alignment: .leading) { Text("Sign in") .font(.headline) Text("with your HuggingFace account") .font(.subheadline) .foregroundStyle(.primary) } Spacer() Button("Sign in", action: { userLoggedIn = false openWindow(id: "login") }) } else { if let currentUser = HuggingChatSession.shared.currentUser { VStack(alignment: .leading) { Text("Hi, \(currentUser.username)") .font(.headline) if currentUser.email != "" { Text(HuggingChatSession.shared.currentUser?.email ?? "userID") .font(.subheadline) .foregroundStyle(.primary) } } Spacer() Button("Sign out", action: { HuggingChatSession.shared.logout() userLoggedIn = false }) } } } }) Section(content: { LabeledContent("Model Name:", content: { HStack { Picker("", selection: $selectedLocalModel) { Text("None").tag("None") let downloadedModels = modelManager.availableModels.filter { $0.localURL != nil } ForEach(downloadedModels, id: \.id) { option in Text(option.displayName).tag(option.displayName) } } // Local model status StatusIndicatorView(status: modelManager.loadState) } .labelsHidden() .onChange(of: selectedLocalModel) { if selectedLocalModel == "None" { modelManager.loadState = .idle isLocalGeneration = false modelManager.cancelLoading() } else if let selectedLocalModel = modelManager.availableModels.first(where: { $0.displayName == selectedLocalModel }) { Task { await modelManager.localModelDidChange(to: selectedLocalModel) } } } }) }, header: { Text("Local Inference") }, footer: { Text("Local models will run queries entirely on your local machine.") .font(.footnote) .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) .lineLimit(nil) .foregroundColor(.secondary) }) Section(content: { LabeledContent("Model Name:", content: { Picker("", selection: $selectedExternalModel) { ForEach(externalModels, id: \.id) { option in let components = option.name.split(separator: "/", maxSplits: 1) let result = components.count > 1 ? String(components[1]) : "" Text(result) .tag(option.name) } } .labelsHidden() .disabled(HuggingChatSession.shared.currentUser == nil) .onChange(of: selectedExternalModel) { if let activeModel = externalModels.first(where: { $0.id == selectedExternalModel }) { DataService.shared.setActiveModel(ActiveModel(model: activeModel)) // Reset conversation and activate model conversationManager.model = activeModel conversationManager.isMultimodal = activeModel.multimodal conversationManager.isTools = activeModel.tools conversationManager.stopGenerating() conversationManager.reset() } } }) Toggle("Use web search", isOn: $useWebSearch) }, header: { Text("Server-Side Inference") }, footer: { Text("Server-side models are more suitable for general usage or complex queries, and will run on an external server. Toggling web search will enable the model to complement its answers with information queried from the web.") .font(.footnote) .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) .lineLimit(nil) .foregroundColor(.secondary) }) .disabled(HuggingChatSession.shared.currentUser == nil) Section(content: { Toggle("Use Working Context", isOn: $useContext) }, header: { Text("Experimental") }, footer: { Text("When enabled, the model will automatically use the working context of the foremost window (e.g. text editor, selected text, terminal history) and append it to your query. Your data is never used for training.") .font(.footnote) .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) .lineLimit(nil) .foregroundColor(.secondary) }) Section(content: { KeyboardShortcuts.Recorder("Global Keyboard Shortcut:", name: .showFloatingPanel) KeyboardShortcuts.Recorder("Toggle between local and server generation:", name: .toggleLocalGeneration) Toggle(isOn: $hideDock) { Text("Hide dock icon") } LaunchAtLogin.Toggle { Text("Open automatically at login") } Picker("Automatically clear chat after:", selection: $chatClearInterval) { Text("15 minutes").tag("15min") Text("1 hour").tag("1hour") Text("1 day").tag("1day") Text("Never").tag("never") } .onChange(of: hideDock) { oldValue, newValue in if newValue == false { NSApp.setActivationPolicy(.regular) } } }, header: { Text("Miscellaneous") }) } .onAppear { HuggingChatSession.shared.refreshLoginState() fetchModels() // modelManager.fetchAllLocalModels() } .formStyle(.grouped) } // Helper methods func fetchModels() { DataService.shared.getModels() .receive(on: DispatchQueue.main) .sink { completion in switch completion { case .finished: print("Did finish fetching models") case .failure(let error): print("Did fail fetching models:\n\(error)") } } receiveValue: { models in externalModels = models.filter({ !$0.unlisted }) }.store(in: &cancellables) } } #Preview { GeneralSettingsView() .environment(ConversationViewModel()) .environment(ModelManager()) }