|
| 1 | +import Preferences |
| 2 | +import SharedUIComponents |
| 3 | +import SwiftUI |
| 4 | + |
| 5 | +#if canImport(ProHostApp) |
| 6 | +import ProHostApp |
| 7 | +#endif |
| 8 | + |
| 9 | +struct ChatSettingsGeneralSectionView: View { |
| 10 | + class Settings: ObservableObject { |
| 11 | + static let availableLocalizedLocales = Locale.availableLocalizedLocales |
| 12 | + @AppStorage(\.chatGPTLanguage) var chatGPTLanguage |
| 13 | + @AppStorage(\.chatGPTTemperature) var chatGPTTemperature |
| 14 | + @AppStorage(\.chatGPTMaxMessageCount) var chatGPTMaxMessageCount |
| 15 | + @AppStorage(\.chatFontSize) var chatFontSize |
| 16 | + @AppStorage(\.chatCodeFont) var chatCodeFont |
| 17 | + |
| 18 | + @AppStorage(\.defaultChatFeatureChatModelId) var defaultChatFeatureChatModelId |
| 19 | + @AppStorage(\.defaultChatSystemPrompt) var defaultChatSystemPrompt |
| 20 | + @AppStorage(\.chatSearchPluginMaxIterations) var chatSearchPluginMaxIterations |
| 21 | + @AppStorage(\.defaultChatFeatureEmbeddingModelId) var defaultChatFeatureEmbeddingModelId |
| 22 | + @AppStorage(\.chatModels) var chatModels |
| 23 | + @AppStorage(\.embeddingModels) var embeddingModels |
| 24 | + @AppStorage(\.wrapCodeInChatCodeBlock) var wrapCodeInCodeBlock |
| 25 | + @AppStorage( |
| 26 | + \.keepFloatOnTopIfChatPanelAndXcodeOverlaps |
| 27 | + ) var keepFloatOnTopIfChatPanelAndXcodeOverlaps |
| 28 | + @AppStorage( |
| 29 | + \.disableFloatOnTopWhenTheChatPanelIsDetached |
| 30 | + ) var disableFloatOnTopWhenTheChatPanelIsDetached |
| 31 | + |
| 32 | + init() {} |
| 33 | + } |
| 34 | + |
| 35 | + @Environment(\.openURL) var openURL |
| 36 | + @Environment(\.toast) var toast |
| 37 | + @StateObject var settings = Settings() |
| 38 | + @State var maxTokenOverLimit = false |
| 39 | + |
| 40 | + var body: some View { |
| 41 | + VStack { |
| 42 | + chatSettingsForm |
| 43 | + SettingsDivider("UI") |
| 44 | + uiForm |
| 45 | + SettingsDivider("Plugin") |
| 46 | + pluginForm |
| 47 | + } |
| 48 | + } |
| 49 | + |
| 50 | + @ViewBuilder |
| 51 | + var chatSettingsForm: some View { |
| 52 | + Form { |
| 53 | + Picker( |
| 54 | + "Chat model", |
| 55 | + selection: $settings.defaultChatFeatureChatModelId |
| 56 | + ) { |
| 57 | + if !settings.chatModels |
| 58 | + .contains(where: { $0.id == settings.defaultChatFeatureChatModelId }) |
| 59 | + { |
| 60 | + Text( |
| 61 | + (settings.chatModels.first?.name).map { "\($0) (Default)" } |
| 62 | + ?? "No model found" |
| 63 | + ) |
| 64 | + .tag(settings.defaultChatFeatureChatModelId) |
| 65 | + } |
| 66 | + |
| 67 | + ForEach(settings.chatModels, id: \.id) { chatModel in |
| 68 | + Text(chatModel.name).tag(chatModel.id) |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + Picker( |
| 73 | + "Embedding model", |
| 74 | + selection: $settings.defaultChatFeatureEmbeddingModelId |
| 75 | + ) { |
| 76 | + if !settings.embeddingModels |
| 77 | + .contains(where: { $0.id == settings.defaultChatFeatureEmbeddingModelId }) |
| 78 | + { |
| 79 | + Text( |
| 80 | + (settings.embeddingModels.first?.name).map { "\($0) (Default)" } |
| 81 | + ?? "No model found" |
| 82 | + ) |
| 83 | + .tag(settings.defaultChatFeatureEmbeddingModelId) |
| 84 | + } |
| 85 | + |
| 86 | + ForEach(settings.embeddingModels, id: \.id) { embeddingModel in |
| 87 | + Text(embeddingModel.name).tag(embeddingModel.id) |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + if #available(macOS 13.0, *) { |
| 92 | + LabeledContent("Reply in language") { |
| 93 | + languagePicker |
| 94 | + } |
| 95 | + } else { |
| 96 | + HStack { |
| 97 | + Text("Reply in language") |
| 98 | + languagePicker |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + HStack { |
| 103 | + Slider(value: $settings.chatGPTTemperature, in: 0...2, step: 0.1) { |
| 104 | + Text("Temperature") |
| 105 | + } |
| 106 | + |
| 107 | + Text( |
| 108 | + "\(settings.chatGPTTemperature.formatted(.number.precision(.fractionLength(1))))" |
| 109 | + ) |
| 110 | + .font(.body) |
| 111 | + .foregroundColor(settings.chatGPTTemperature >= 1 ? .red : .secondary) |
| 112 | + .monospacedDigit() |
| 113 | + .padding(.vertical, 2) |
| 114 | + .padding(.horizontal, 6) |
| 115 | + .background( |
| 116 | + RoundedRectangle(cornerRadius: 4, style: .continuous) |
| 117 | + .fill(Color.primary.opacity(0.1)) |
| 118 | + ) |
| 119 | + } |
| 120 | + |
| 121 | + Picker( |
| 122 | + "Memory", |
| 123 | + selection: $settings.chatGPTMaxMessageCount |
| 124 | + ) { |
| 125 | + Text("No Limit").tag(0) |
| 126 | + Text("3 Messages").tag(3) |
| 127 | + Text("5 Messages").tag(5) |
| 128 | + Text("7 Messages").tag(7) |
| 129 | + Text("9 Messages").tag(9) |
| 130 | + Text("11 Messages").tag(11) |
| 131 | + } |
| 132 | + |
| 133 | + VStack(alignment: .leading, spacing: 4) { |
| 134 | + Text("Default system prompt") |
| 135 | + EditableText(text: $settings.defaultChatSystemPrompt) |
| 136 | + .lineLimit(6) |
| 137 | + } |
| 138 | + .padding(.vertical, 4) |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + @ViewBuilder |
| 143 | + var uiForm: some View { |
| 144 | + Form { |
| 145 | + HStack { |
| 146 | + TextField(text: .init(get: { |
| 147 | + "\(Int(settings.chatFontSize))" |
| 148 | + }, set: { |
| 149 | + settings.chatFontSize = Double(Int($0) ?? 0) |
| 150 | + })) { |
| 151 | + Text("Font size of message") |
| 152 | + } |
| 153 | + .textFieldStyle(.roundedBorder) |
| 154 | + |
| 155 | + Text("pt") |
| 156 | + } |
| 157 | + |
| 158 | + FontPicker(font: $settings.chatCodeFont) { |
| 159 | + Text("Font of code") |
| 160 | + } |
| 161 | + |
| 162 | + Toggle(isOn: $settings.wrapCodeInCodeBlock) { |
| 163 | + Text("Wrap text in code block") |
| 164 | + } |
| 165 | + |
| 166 | + #if canImport(ProHostApp) |
| 167 | + |
| 168 | + CodeHighlightThemePicker(scenario: .chat) |
| 169 | + |
| 170 | + #endif |
| 171 | + |
| 172 | + Toggle(isOn: $settings.disableFloatOnTopWhenTheChatPanelIsDetached) { |
| 173 | + Text("Disable always-on-top when the chat panel is detached") |
| 174 | + } |
| 175 | + |
| 176 | + Toggle(isOn: $settings.keepFloatOnTopIfChatPanelAndXcodeOverlaps) { |
| 177 | + Text("Keep always-on-top if the chat panel and Xcode overlaps and Xcode is active") |
| 178 | + }.disabled(!settings.disableFloatOnTopWhenTheChatPanelIsDetached) |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + @ViewBuilder |
| 183 | + var pluginForm: some View { |
| 184 | + Form { |
| 185 | + TextField(text: .init(get: { |
| 186 | + "\(Int(settings.chatSearchPluginMaxIterations))" |
| 187 | + }, set: { |
| 188 | + settings.chatSearchPluginMaxIterations = Int($0) ?? 0 |
| 189 | + })) { |
| 190 | + Text("Search plugin max iterations") |
| 191 | + } |
| 192 | + .textFieldStyle(.roundedBorder) |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + var languagePicker: some View { |
| 197 | + Menu { |
| 198 | + if !settings.chatGPTLanguage.isEmpty, |
| 199 | + !Settings.availableLocalizedLocales |
| 200 | + .contains(settings.chatGPTLanguage) |
| 201 | + { |
| 202 | + Button( |
| 203 | + settings.chatGPTLanguage, |
| 204 | + action: { self.settings.chatGPTLanguage = settings.chatGPTLanguage } |
| 205 | + ) |
| 206 | + } |
| 207 | + Button( |
| 208 | + "Auto-detected by LLM", |
| 209 | + action: { self.settings.chatGPTLanguage = "" } |
| 210 | + ) |
| 211 | + ForEach( |
| 212 | + Settings.availableLocalizedLocales, |
| 213 | + id: \.self |
| 214 | + ) { localizedLocales in |
| 215 | + Button( |
| 216 | + localizedLocales, |
| 217 | + action: { self.settings.chatGPTLanguage = localizedLocales } |
| 218 | + ) |
| 219 | + } |
| 220 | + } label: { |
| 221 | + Text( |
| 222 | + settings.chatGPTLanguage.isEmpty |
| 223 | + ? "Auto-detected by LLM" |
| 224 | + : settings.chatGPTLanguage |
| 225 | + ) |
| 226 | + } |
| 227 | + } |
| 228 | +} |
| 229 | + |
| 230 | +// MARK: - Preview |
| 231 | + |
| 232 | +// |
| 233 | +// #Preview { |
| 234 | +// ScrollView { |
| 235 | +// ChatSettingsView() |
| 236 | +// .padding() |
| 237 | +// } |
| 238 | +// .frame(height: 800) |
| 239 | +// .environment(\.overrideFeatureFlag, \.never) |
| 240 | +// } |
| 241 | +// |
| 242 | + |
0 commit comments