import Preferences import SharedUIComponents import SwiftUI #if canImport(ProHostApp) import ProHostApp #endif struct ChatSettingsView: View { class Settings: ObservableObject { static let availableLocalizedLocales = Locale.availableLocalizedLocales @AppStorage(\.chatGPTLanguage) var chatGPTLanguage @AppStorage(\.chatGPTTemperature) var chatGPTTemperature @AppStorage(\.chatGPTMaxMessageCount) var chatGPTMaxMessageCount @AppStorage(\.chatFontSize) var chatFontSize @AppStorage(\.chatCodeFontSize) var chatCodeFontSize @AppStorage(\.defaultChatFeatureChatModelId) var defaultChatFeatureChatModelId @AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt @AppStorage(\.chatSearchPluginMaxIterations) var chatSearchPluginMaxIterations @AppStorage(\.defaultChatFeatureEmbeddingModelId) var defaultChatFeatureEmbeddingModelId @AppStorage(\.chatModels) var chatModels @AppStorage(\.embeddingModels) var embeddingModels @AppStorage(\.wrapCodeInChatCodeBlock) var wrapCodeInCodeBlock init() {} } @Environment(\.openURL) var openURL @Environment(\.toast) var toast @StateObject var settings = Settings() @State var maxTokenOverLimit = false var body: some View { VStack { chatSettingsForm SettingsDivider("UI") uiForm SettingsDivider("Plugin") pluginForm ScopeForm() } } @ViewBuilder var chatSettingsForm: some View { Form { Picker( "Chat Model", selection: $settings.defaultChatFeatureChatModelId ) { if !settings.chatModels .contains(where: { $0.id == settings.defaultChatFeatureChatModelId }) { Text( (settings.chatModels.first?.name).map { "\($0) (Default)" } ?? "No Model Found" ) .tag(settings.defaultChatFeatureChatModelId) } ForEach(settings.chatModels, id: \.id) { chatModel in Text(chatModel.name).tag(chatModel.id) } } Picker( "Embedding Model", selection: $settings.defaultChatFeatureEmbeddingModelId ) { if !settings.embeddingModels .contains(where: { $0.id == settings.defaultChatFeatureEmbeddingModelId }) { Text( (settings.embeddingModels.first?.name).map { "\($0) (Default)" } ?? "No Model Found" ) .tag(settings.defaultChatFeatureEmbeddingModelId) } ForEach(settings.embeddingModels, id: \.id) { embeddingModel in Text(embeddingModel.name).tag(embeddingModel.id) } } if #available(macOS 13.0, *) { LabeledContent("Reply in Language") { languagePicker } } else { HStack { Text("Reply in Language") languagePicker } } HStack { Slider(value: $settings.chatGPTTemperature, in: 0...2, step: 0.1) { Text("Temperature") } Text( "\(settings.chatGPTTemperature.formatted(.number.precision(.fractionLength(1))))" ) .font(.body) .foregroundColor(settings.chatGPTTemperature >= 1 ? .red : .secondary) .monospacedDigit() .padding(.vertical, 2) .padding(.horizontal, 6) .background( RoundedRectangle(cornerRadius: 4, style: .continuous) .fill(Color.primary.opacity(0.1)) ) } Picker( "Memory", selection: $settings.chatGPTMaxMessageCount ) { Text("No Limit").tag(0) Text("3 Messages").tag(3) Text("5 Messages").tag(5) Text("7 Messages").tag(7) Text("9 Messages").tag(9) Text("11 Messages").tag(11) } VStack(alignment: .leading, spacing: 4) { Text("Default System Prompt") EditableText(text: $settings.defaultChatSystemPrompt) .lineLimit(6) } .padding(.vertical, 4) } } @ViewBuilder var uiForm: some View { Form { HStack { TextField(text: .init(get: { "\(Int(settings.chatFontSize))" }, set: { settings.chatFontSize = Double(Int($0) ?? 0) })) { Text("Font size of message") } .textFieldStyle(.roundedBorder) Text("pt") } HStack { TextField(text: .init(get: { "\(Int(settings.chatCodeFontSize))" }, set: { settings.chatCodeFontSize = Double(Int($0) ?? 0) })) { Text("Font size of code block") } .textFieldStyle(.roundedBorder) Text("pt") } Toggle(isOn: $settings.wrapCodeInCodeBlock) { Text("Wrap code in code block") } } } @ViewBuilder var pluginForm: some View { Form { TextField(text: .init(get: { "\(Int(settings.chatSearchPluginMaxIterations))" }, set: { settings.chatSearchPluginMaxIterations = Int($0) ?? 0 })) { Text("Search Plugin Max Iterations") } .textFieldStyle(.roundedBorder) } } var languagePicker: some View { Menu { if !settings.chatGPTLanguage.isEmpty, !Settings.availableLocalizedLocales .contains(settings.chatGPTLanguage) { Button( settings.chatGPTLanguage, action: { self.settings.chatGPTLanguage = settings.chatGPTLanguage } ) } Button( "Auto-detected by ChatGPT", action: { self.settings.chatGPTLanguage = "" } ) ForEach( Settings.availableLocalizedLocales, id: \.self ) { localizedLocales in Button( localizedLocales, action: { self.settings.chatGPTLanguage = localizedLocales } ) } } label: { Text( settings.chatGPTLanguage.isEmpty ? "Auto-detected by ChatGPT" : settings.chatGPTLanguage ) } } struct ScopeForm: View { class Settings: ObservableObject { @AppStorage(\.enableFileScopeByDefaultInChatContext) var enableFileScopeByDefaultInChatContext: Bool @AppStorage(\.enableCodeScopeByDefaultInChatContext) var enableCodeScopeByDefaultInChatContext: Bool @AppStorage(\.enableSenseScopeByDefaultInChatContext) var enableSenseScopeByDefaultInChatContext: Bool @AppStorage(\.enableProjectScopeByDefaultInChatContext) var enableProjectScopeByDefaultInChatContext: Bool @AppStorage(\.enableWebScopeByDefaultInChatContext) var enableWebScopeByDefaultInChatContext: Bool @AppStorage(\.preferredChatModelIdForSenseScope) var preferredChatModelIdForSenseScope: String @AppStorage(\.preferredChatModelIdForProjectScope) var preferredChatModelIdForProjectScope: String @AppStorage(\.preferredChatModelIdForWebScope) var preferredChatModelIdForWebScope: String @AppStorage(\.chatModels) var chatModels @AppStorage(\.maxFocusedCodeLineCount) var maxFocusedCodeLineCount init() {} } @StateObject var settings = Settings() var body: some View { VStack { Scope( title: Text("File Scope"), description: "Enable the bot to read the metadata of the editing file." ) { Form { Toggle(isOn: $settings.enableFileScopeByDefaultInChatContext) { Text("Enable @file scope by default in chat context.") } } } Scope( title: Text("Code Scope"), description: "Enable the bot to read the code and metadata in the editing file." ) { Form { Toggle(isOn: $settings.enableCodeScopeByDefaultInChatContext) { Text("Enable @code scope by default in chat context.") } HStack { TextField(text: .init(get: { "\(Int(settings.maxFocusedCodeLineCount))" }, set: { settings.maxFocusedCodeLineCount = Int($0) ?? 0 })) { Text("Max focused code") } .textFieldStyle(.roundedBorder) Text("lines") } } } #if canImport(ProHostApp) Scope( title: WithFeatureEnabled(\.projectScopeInChat) { Text("Sense Scope (Experimental)") }, description: "Experimental. Enable the bot to read the relevant code of the editing file in the project, third party packages and the SDK." ) { WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { Form { Toggle(isOn: $settings.enableSenseScopeByDefaultInChatContext) { Text("Enable @sense scope by default in chat context.") } Picker( "Preferred Chat Model", selection: $settings.preferredChatModelIdForSenseScope ) { Text("None").tag("") if !settings.chatModels .contains(where: { $0.id == settings.preferredChatModelIdForSenseScope }), !settings.preferredChatModelIdForSenseScope.isEmpty { Text( (settings.chatModels.first?.name).map { "\($0) (Default)" } ?? "No Model Found" ) .tag(settings.preferredChatModelIdForSenseScope) } ForEach(settings.chatModels, id: \.id) { chatModel in Text(chatModel.name).tag(chatModel.id) } } } } } Scope( title: WithFeatureEnabled(\.projectScopeInChat) { Text("Project Scope (Experimental)") }, description: "Experimental. Enable the bot to search code symbols in the project, third party packages and the SDK." ) { WithFeatureEnabled(\.projectScopeInChat, alignment: .hidden) { Form { Toggle(isOn: $settings.enableProjectScopeByDefaultInChatContext) { Text("Enable @project scope by default in chat context.") } Picker( "Preferred Chat Model", selection: $settings.preferredChatModelIdForProjectScope ) { Text("None").tag("") if !settings.chatModels .contains(where: { $0.id == settings.preferredChatModelIdForProjectScope }), !settings.preferredChatModelIdForProjectScope.isEmpty { Text( (settings.chatModels.first?.name).map { "\($0) (Default)" } ?? "No Model Found" ) .tag(settings.preferredChatModelIdForProjectScope) } ForEach(settings.chatModels, id: \.id) { chatModel in Text(chatModel.name).tag(chatModel.id) } } } } } #endif Scope( title: Text("Web Scope"), description: "Allow the bot to search on Bing or read a web page." ) { Form { Toggle(isOn: $settings.enableWebScopeByDefaultInChatContext) { Text("Enable @web scope by default in chat context.") } Picker( "Preferred Chat Model", selection: $settings.preferredChatModelIdForWebScope ) { Text("None").tag("") if !settings.chatModels .contains(where: { $0.id == settings.preferredChatModelIdForWebScope }), !settings.preferredChatModelIdForWebScope.isEmpty { Text( (settings.chatModels.first?.name).map { "\($0) (Default)" } ?? "No Model Found" ) .tag(settings.preferredChatModelIdForWebScope) } ForEach(settings.chatModels, id: \.id) { chatModel in Text(chatModel.name).tag(chatModel.id) } } } } } } struct Scope: View { let title: Title let description: String let content: () -> Content var body: some View { SettingsDivider(title) VStack { Text(description) .multilineTextAlignment(.center) .foregroundStyle(.secondary) .frame(maxWidth: .infinity) .padding(8) .background { RoundedRectangle(cornerRadius: 8) .fill(Color.secondary.opacity(0.1)) } content() } } } } } // MARK: - Preview #Preview { ScrollView { ChatSettingsView() .padding() } .frame(height: 800) .environment(\.overrideFeatureFlag, \.never) }