From b3417e9e2a45c826002cd0f7c81f60ce7ff8f033 Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sat, 5 Oct 2024 14:41:11 +0800 Subject: [PATCH 01/11] :sparkles: Add OpenAI Logo --- .../MLX2.imageset/Contents.json | 21 ++++++++++++++++++ .../Assets.xcassets/MLX2.imageset/MLX2.png | Bin 0 -> 11395 bytes .../openai-lockup.imageset/Contents.json | 21 ++++++++++++++++++ .../openai-lockup.imageset/openai-lockup.svg | 1 + .../openai-logomark.imageset/Contents.json | 21 ++++++++++++++++++ .../openai-logomark.svg | 1 + .../Contents.json | 21 ++++++++++++++++++ .../openai-white-lockup.svg | 16 +++++++++++++ .../Contents.json | 21 ++++++++++++++++++ .../openai-white-logomark.svg | 10 +++++++++ 10 files changed, 133 insertions(+) create mode 100644 ChatMLX/Assets.xcassets/MLX2.imageset/Contents.json create mode 100644 ChatMLX/Assets.xcassets/MLX2.imageset/MLX2.png create mode 100644 ChatMLX/Assets.xcassets/openai-lockup.imageset/Contents.json create mode 100644 ChatMLX/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg create mode 100644 ChatMLX/Assets.xcassets/openai-logomark.imageset/Contents.json create mode 100644 ChatMLX/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg create mode 100644 ChatMLX/Assets.xcassets/openai-white-lockup.imageset/Contents.json create mode 100644 ChatMLX/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg create mode 100644 ChatMLX/Assets.xcassets/openai-white-logomark.imageset/Contents.json create mode 100644 ChatMLX/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg diff --git a/ChatMLX/Assets.xcassets/MLX2.imageset/Contents.json b/ChatMLX/Assets.xcassets/MLX2.imageset/Contents.json new file mode 100644 index 0000000..d036be4 --- /dev/null +++ b/ChatMLX/Assets.xcassets/MLX2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "MLX2.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ChatMLX/Assets.xcassets/MLX2.imageset/MLX2.png b/ChatMLX/Assets.xcassets/MLX2.imageset/MLX2.png new file mode 100644 index 0000000000000000000000000000000000000000..22a898085c2126a36f73acc3495c549f084ea405 GIT binary patch literal 11395 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 AkpKVy literal 0 HcmV?d00001 diff --git a/ChatMLX/Assets.xcassets/openai-lockup.imageset/Contents.json b/ChatMLX/Assets.xcassets/openai-lockup.imageset/Contents.json new file mode 100644 index 0000000..3bba569 --- /dev/null +++ b/ChatMLX/Assets.xcassets/openai-lockup.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "openai-lockup.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ChatMLX/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg b/ChatMLX/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg new file mode 100644 index 0000000..859d7af --- /dev/null +++ b/ChatMLX/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ChatMLX/Assets.xcassets/openai-logomark.imageset/Contents.json b/ChatMLX/Assets.xcassets/openai-logomark.imageset/Contents.json new file mode 100644 index 0000000..6d16a8f --- /dev/null +++ b/ChatMLX/Assets.xcassets/openai-logomark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "openai-logomark.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ChatMLX/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg b/ChatMLX/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg new file mode 100644 index 0000000..e04db75 --- /dev/null +++ b/ChatMLX/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ChatMLX/Assets.xcassets/openai-white-lockup.imageset/Contents.json b/ChatMLX/Assets.xcassets/openai-white-lockup.imageset/Contents.json new file mode 100644 index 0000000..53b2311 --- /dev/null +++ b/ChatMLX/Assets.xcassets/openai-white-lockup.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "openai-white-lockup.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ChatMLX/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg b/ChatMLX/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg new file mode 100644 index 0000000..610e5aa --- /dev/null +++ b/ChatMLX/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/ChatMLX/Assets.xcassets/openai-white-logomark.imageset/Contents.json b/ChatMLX/Assets.xcassets/openai-white-logomark.imageset/Contents.json new file mode 100644 index 0000000..5a327fe --- /dev/null +++ b/ChatMLX/Assets.xcassets/openai-white-logomark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "openai-white-logomark.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ChatMLX/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg b/ChatMLX/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg new file mode 100644 index 0000000..b3df81f --- /dev/null +++ b/ChatMLX/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg @@ -0,0 +1,10 @@ + + + + + + + + + + From e1649756725b9bb64f2bfc7ba3332cd95b2076ff Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sat, 5 Oct 2024 14:41:40 +0800 Subject: [PATCH 02/11] :sparkles: Add Extensions --- ChatMLX/Extensions/Binding+Extensions.swift | 9 +++++ ChatMLX/Extensions/Color+Extensions.swift | 34 +++++++++++++++++++ ChatMLX/Extensions/Defaults+Extensions.swift | 6 ++-- .../LabeledContentStyle+Extensions.swift | 22 ++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 ChatMLX/Extensions/Color+Extensions.swift create mode 100644 ChatMLX/Extensions/LabeledContentStyle+Extensions.swift diff --git a/ChatMLX/Extensions/Binding+Extensions.swift b/ChatMLX/Extensions/Binding+Extensions.swift index 3d058a1..540accc 100644 --- a/ChatMLX/Extensions/Binding+Extensions.swift +++ b/ChatMLX/Extensions/Binding+Extensions.swift @@ -13,3 +13,12 @@ extension Binding { Binding(get: { self.wrappedValue ?? defaultValue }, set: { self.wrappedValue = $0 }) } } + +extension Binding { + func asDouble() -> Binding where Value: BinaryInteger { + Binding( + get: { Double(self.wrappedValue) }, + set: { self.wrappedValue = Value($0) } + ) + } +} diff --git a/ChatMLX/Extensions/Color+Extensions.swift b/ChatMLX/Extensions/Color+Extensions.swift new file mode 100644 index 0000000..645e1b4 --- /dev/null +++ b/ChatMLX/Extensions/Color+Extensions.swift @@ -0,0 +1,34 @@ +// +// Color+Extensions.swift +// ChatMLX +// +// Created by John Mai on 2024/10/4. +// +import SwiftUI + +extension Color { + init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + + self.init( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } +} diff --git a/ChatMLX/Extensions/Defaults+Extensions.swift b/ChatMLX/Extensions/Defaults+Extensions.swift index 72b15d3..0022ec9 100644 --- a/ChatMLX/Extensions/Defaults+Extensions.swift +++ b/ChatMLX/Extensions/Defaults+Extensions.swift @@ -18,7 +18,7 @@ extension Defaults.Keys { static let customHuggingFaceEndpoints = Key<[String]>("customHuggingFaceEndpoints", default: []) static let useCustomHuggingFaceEndpoint = Key( "useCustomHuggingFaceEndpoint", default: false) - static let huggingFaceToken = Key("huggingFaceToken", default: nil) + static let huggingFaceToken = Key("huggingFaceToken", default: "") static let defaultTitle = Key("defaultTitle", default: "Default Conversation") static let defaultTemperature = Key("defaultTemperature", default: 0.6) @@ -35,5 +35,7 @@ extension Defaults.Keys { static let defaultUseSystemPrompt = Key("defaultUseSystemPrompt", default: false) static let defaultSystemPrompt = Key("defaultSystemPrompt", default: "") - static let gpuCacheLimit = Key("gpuCacheLimit", default: 128) + static let enableGPUMemorySettings = Key("enableGPUMemorySettings", default: false) + static let gpuCacheLimit = Key("gpuCacheLimit", default: 128) + static let gpuMemoryLimit = Key("gpuMemoryLimit", default: 1024) } diff --git a/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift b/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift new file mode 100644 index 0000000..05a8f92 --- /dev/null +++ b/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift @@ -0,0 +1,22 @@ +// +// LabeledContentStyle+Extensions.swift +// ChatMLX +// +// Created by John Mai on 2024/10/5. +// + +import SwiftUI + +struct HorizontalLabeledContentStyle: LabeledContentStyle { + func makeBody(configuration: Configuration) -> some View { + HStack { + configuration.label + Spacer() + configuration.content + } + } +} + +extension LabeledContentStyle where Self == HorizontalLabeledContentStyle { + static var horizontal: HorizontalLabeledContentStyle { .init() } +} From 5a2f88c34b62912357b13d1507bb4f0034cb92fe Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sat, 5 Oct 2024 14:42:17 +0800 Subject: [PATCH 03/11] :sparkles: Use LabeledContent --- .../Settings/DefaultConversationView.swift | 76 +++++------------- .../DownloadManager/DownloadManagerView.swift | 6 +- .../DownloadManager/DownloadTaskView.swift | 7 +- ChatMLX/Features/Settings/GeneralView.swift | 61 ++------------- .../Features/Settings/HuggingFaceView.swift | 36 +++------ .../ModelManager/ModelManagerView.swift | 21 +++++ .../ModelManager/Providers/MLXProvider.swift | 77 +++++++++++++++++++ .../Providers/OpenAIProvider.swift | 45 +++++++++++ .../ModelManager/Providers/ProviderView.swift | 63 +++++++++++++++ ChatMLX/Features/Settings/SettingsView.swift | 2 +- 10 files changed, 253 insertions(+), 141 deletions(-) create mode 100644 ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift create mode 100644 ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift create mode 100644 ChatMLX/Features/Settings/ModelManager/Providers/OpenAIProvider.swift create mode 100644 ChatMLX/Features/Settings/ModelManager/Providers/ProviderView.swift diff --git a/ChatMLX/Features/Settings/DefaultConversationView.swift b/ChatMLX/Features/Settings/DefaultConversationView.swift index c4b8449..8addebe 100644 --- a/ChatMLX/Features/Settings/DefaultConversationView.swift +++ b/ChatMLX/Features/Settings/DefaultConversationView.swift @@ -43,9 +43,7 @@ struct DefaultConversationView: View { } LuminareSection("Model Settings") { - HStack { - Text("Model") - Spacer() + LabeledContent("Model") { Picker( selection: $defaultModel, label: Image(systemName: "brain") @@ -57,16 +55,10 @@ struct DefaultConversationView: View { } } } - .labelsHidden() - .buttonStyle(.borderless) - .foregroundStyle(.white) - .tint(.white) } .padding(padding) - HStack { - Text("Temperature") - Spacer() + LabeledContent("Temperature") { CompactSlider( value: $defaultTemperature, in: 0 ... 2, step: 0.01 ) { @@ -77,9 +69,7 @@ struct DefaultConversationView: View { } .padding(padding) - HStack { - Text("Top P") - Spacer() + LabeledContent("Top P") { CompactSlider( value: $defaultTopP, in: 0 ... 1, step: 0.01 ) { @@ -90,23 +80,15 @@ struct DefaultConversationView: View { } .padding(padding) - HStack { - Text("Use Max Length") - Spacer() + LabeledContent("Use Max Length") { Toggle("", isOn: $defaultUseMaxLength) - .toggleStyle(.switch) } .padding(padding) if defaultUseMaxLength { - HStack { - Text("Max Length") - Spacer() + LabeledContent("Max Length") { CompactSlider( - value: Binding( - get: { Double(defaultMaxLength) }, - set: { defaultMaxLength = Int64($0) } - ), in: 0 ... 8192, step: 1 + value: $defaultMaxLength.asDouble(), in: 0 ... 8192, step: 1 ) { Text("\(defaultMaxLength)") .foregroundStyle(.white) @@ -116,14 +98,9 @@ struct DefaultConversationView: View { .padding(padding) } - HStack { - Text("Repetition Context Size") - Spacer() + LabeledContent("Repetition Context Size") { CompactSlider( - value: Binding( - get: { Double(defaultRepetitionContextSize) }, - set: { defaultRepetitionContextSize = Int32($0) } - ), in: 0 ... 100, step: 1 + value: $defaultRepetitionContextSize.asDouble(), in: 0 ... 100, step: 1 ) { Text("\(defaultRepetitionContextSize)") .foregroundStyle(.white) @@ -132,18 +109,13 @@ struct DefaultConversationView: View { } .padding(padding) - HStack { - Text("Use Repetition Penalty") - Spacer() + LabeledContent("Use Repetition Penalty") { Toggle("", isOn: $defaultUseRepetitionPenalty) - .toggleStyle(.switch) } .padding(padding) if defaultUseRepetitionPenalty { - HStack { - Text("Repetition Penalty") - Spacer() + LabeledContent("Repetition Penalty") { CompactSlider( value: $defaultRepetitionPenalty, in: 1 ... 2, step: 0.01 @@ -158,26 +130,17 @@ struct DefaultConversationView: View { .padding(padding) } } - .compactSliderSecondaryColor(.white) LuminareSection("Message Control") { - HStack { - Text("Use Max Messages Limit") - Spacer() + LabeledContent("Use Max Messages Limit") { Toggle("", isOn: $defaultUseMaxMessagesLimit) - .toggleStyle(.switch) } .padding(padding) if defaultUseMaxMessagesLimit { - HStack { - Text("Max Messages Limit") - Spacer() + LabeledContent("Max Messages Limit") { CompactSlider( - value: Binding( - get: { Double(defaultMaxMessagesLimit) }, - set: { defaultMaxMessagesLimit = Int32($0) } - ), in: 1 ... 50, step: 1 + value: $defaultMaxMessagesLimit.asDouble(), in: 1 ... 50, step: 1 ) { Text("\(defaultMaxMessagesLimit)") .foregroundStyle(.white) @@ -187,14 +150,10 @@ struct DefaultConversationView: View { .padding(padding) } } - .compactSliderSecondaryColor(.white) LuminareSection("System Prompt") { - HStack { - Text("Use System Prompt") - Spacer() + LabeledContent("Use System Prompt") { Toggle("", isOn: $defaultUseSystemPrompt) - .toggleStyle(.switch) } .padding(padding) @@ -213,9 +172,16 @@ struct DefaultConversationView: View { } .padding() } + .labeledContentStyle(.horizontal) + .compactSliderSecondaryColor(.white) .scrollContentBackground(.hidden) .onAppear(perform: loadModels) .ultramanNavigationTitle("Default Conversation") + .labelsHidden() + .buttonStyle(.borderless) + .foregroundStyle(.white) + .tint(.white) + .toggleStyle(.switch) } private func loadModels() { diff --git a/ChatMLX/Features/Settings/DownloadManager/DownloadManagerView.swift b/ChatMLX/Features/Settings/DownloadManager/DownloadManagerView.swift index 178866c..e1b8273 100644 --- a/ChatMLX/Features/Settings/DownloadManager/DownloadManagerView.swift +++ b/ChatMLX/Features/Settings/DownloadManager/DownloadManagerView.swift @@ -8,7 +8,7 @@ import SwiftUI struct DownloadManagerView: View { - @Environment(SettingsViewModel.self) private var settingsViewModel + @Environment(SettingsViewModel.self) private var vm @State private var repoId: String = "" @State var showingAlert = false @@ -16,7 +16,7 @@ struct DownloadManagerView: View { var body: some View { List { - ForEach(settingsViewModel.tasks) { task in + ForEach(vm.tasks) { task in DownloadTaskView(task: task) } } @@ -54,6 +54,6 @@ struct DownloadManagerView: View { private func addTask() { let task = DownloadTask(repoId) task.start() - settingsViewModel.tasks.append(task) + vm.tasks.append(task) } } diff --git a/ChatMLX/Features/Settings/DownloadManager/DownloadTaskView.swift b/ChatMLX/Features/Settings/DownloadManager/DownloadTaskView.swift index 40f230f..9dd56ee 100644 --- a/ChatMLX/Features/Settings/DownloadManager/DownloadTaskView.swift +++ b/ChatMLX/Features/Settings/DownloadManager/DownloadTaskView.swift @@ -8,8 +8,9 @@ import SwiftUI struct DownloadTaskView: View { - @Bindable var task: DownloadTask - @Environment(SettingsViewModel.self) private var settingsViewModel + let task: DownloadTask + + @Environment(SettingsViewModel.self) private var vm var body: some View { HStack { @@ -64,7 +65,7 @@ struct DownloadTaskView: View { } Button(action: { - settingsViewModel.tasks.removeAll(where: { + vm.tasks.removeAll(where: { $0.id == task.id }) }) { diff --git a/ChatMLX/Features/Settings/GeneralView.swift b/ChatMLX/Features/Settings/GeneralView.swift index 83c1cdb..464b9e8 100644 --- a/ChatMLX/Features/Settings/GeneralView.swift +++ b/ChatMLX/Features/Settings/GeneralView.swift @@ -29,9 +29,7 @@ struct GeneralView: View { var body: some View { VStack(spacing: 18) { LuminareSection("Language") { - HStack { - Text("Language") - Spacer() + LabeledContent("Language") { Picker( "Language", selection: $language @@ -40,7 +38,6 @@ struct GeneralView: View { Text(language.displayName).tag(language) } } - .labelsHidden() .buttonStyle(.borderless) .foregroundStyle(.white) .tint(.white) @@ -49,9 +46,7 @@ struct GeneralView: View { } LuminareSection("Window Appearance") { - HStack { - Text("Blur") - Spacer() + LabeledContent("Blur") { CompactSlider(value: $blurRadius, in: 0 ... 100) { Text("\(Int(blurRadius))") .foregroundStyle(.white) @@ -61,38 +56,13 @@ struct GeneralView: View { } .padding(5) - HStack { - Text("Color") - Spacer() + LabeledContent("Color") { ColorPicker("", selection: $backgroundColor) - .labelsHidden() } .padding(5) } LuminareSection("System Settings") { - HStack { - Text("GPU Cache Limit") - Spacer() - CompactSlider( - value: Binding( - get: { Double(gpuCacheLimit) }, - set: { gpuCacheLimit = Int32($0) } - ), in: 0 ... Double(maxRAM), step: 128 - ) { - Text("\(Int(gpuCacheLimit))MB") - .foregroundStyle(.white) - } - .frame(width: 200) - .compactSliderSecondaryColor(.white) - .onChange(of: gpuCacheLimit) { oldValue, newValue in - if oldValue != newValue { - runner.loadState = .idle - } - } - } - .padding(5) - Button("Clear All Conversations", action: clearAllConversations) .frame(height: 35) Button("Reset All Settings", action: resetAllSettings) @@ -102,33 +72,14 @@ struct GeneralView: View { Spacer() } - + .labeledContentStyle(.horizontal) .ultramanNavigationTitle("General") .padding() + .labelsHidden() } private func resetAllSettings() { - Defaults.reset(.defaultModel) - Defaults.reset(.language) - Defaults.reset(.backgroundBlurRadius) - Defaults.reset(.backgroundColor) - Defaults.reset(.huggingFaceEndpoint) - Defaults.reset(.customHuggingFaceEndpoints) - Defaults.reset(.useCustomHuggingFaceEndpoint) - Defaults.reset(.huggingFaceToken) - Defaults.reset(.defaultTitle) - Defaults.reset(.defaultTemperature) - Defaults.reset(.defaultTopP) - Defaults.reset(.defaultUseMaxLength) - Defaults.reset(.defaultMaxLength) - Defaults.reset(.defaultRepetitionContextSize) - Defaults.reset(.defaultMaxMessagesLimit) - Defaults.reset(.defaultUseMaxMessagesLimit) - Defaults.reset(.defaultRepetitionPenalty) - Defaults.reset(.defaultUseRepetitionPenalty) - Defaults.reset(.defaultUseSystemPrompt) - Defaults.reset(.defaultSystemPrompt) - Defaults.reset(.gpuCacheLimit) + Defaults.removeAll() } private func clearAllConversations() { diff --git a/ChatMLX/Features/Settings/HuggingFaceView.swift b/ChatMLX/Features/Settings/HuggingFaceView.swift index 8608868..d376927 100644 --- a/ChatMLX/Features/Settings/HuggingFaceView.swift +++ b/ChatMLX/Features/Settings/HuggingFaceView.swift @@ -23,14 +23,9 @@ struct HuggingFaceView: View { var body: some View { VStack(spacing: 18) { LuminareSection("Hugging Face Token") { - HStack { - Text("Token") - Spacer() + LabeledContent("Token") { UltramanSecureField( - Binding( - get: { token ?? "" }, - set: { token = $0.isEmpty ? nil : $0 } - ), + $token, placeholder: Text("Enter your Hugging Face token"), alignment: .trailing ) @@ -40,10 +35,7 @@ struct HuggingFaceView: View { } LuminareSection("Hugging Face Endpoint") { - - HStack { - Text("Endpoint") - Spacer() + LabeledContent("Endpoint") { Picker("", selection: $endpoint) { if useCustomEndpoint { ForEach(customEndpoints, id: \.self) { @@ -56,26 +48,16 @@ struct HuggingFaceView: View { Text("https://hf-mirror.com").tag( "https://hf-mirror.com") } - .labelsHidden() - .buttonStyle(.borderless) - .foregroundStyle(.white) - .tint(.white) } - .padding(5) + .padding(8) - HStack { - Text("Use Custom Endpoint") - Spacer() + LabeledContent("Use Custom Endpoint") { Toggle("", isOn: $useCustomEndpoint) - .toggleStyle(.switch) - .labelsHidden() } .padding(8) if useCustomEndpoint { - HStack { - Text("Custom Endpoint") - Spacer() + LabeledContent("Custom Endpoint") { UltramanTextField( $newCustomEndpoint, placeholder: Text("Custom Hugging Face Endpoint"), @@ -115,6 +97,12 @@ struct HuggingFaceView: View { Spacer() } + .toggleStyle(.switch) + .labeledContentStyle(.horizontal) + .labelsHidden() + .buttonStyle(.borderless) + .foregroundStyle(.white) + .tint(.white) .ultramanNavigationTitle("Hugging Face") .padding() .alert(isPresented: $showingAlert) { diff --git a/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift b/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift new file mode 100644 index 0000000..e1167f4 --- /dev/null +++ b/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift @@ -0,0 +1,21 @@ +// +// ModelManagerView.swift +// ChatMLX +// +// Created by John Mai on 2024/10/4. +// +import SwiftUI + +struct ModelManagerView: View { + @State var isMLXExpanded: Bool = true + + var body: some View { + VStack { + MLXProvider() + OpenAIProvider() + Spacer() + } + .padding() + .ultramanNavigationTitle("Model Manager") + } +} diff --git a/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift b/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift new file mode 100644 index 0000000..43ddf50 --- /dev/null +++ b/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift @@ -0,0 +1,77 @@ +// +// MLXProvider.swift +// ChatMLX +// +// Created by John Mai on 2024/10/4. +// + +import CompactSlider +import Defaults +import Luminare +import SwiftUI + +struct MLXProvider: View { + @State var isExpanded: Bool = true + + let maxRAM = ProcessInfo.processInfo.physicalMemory / (1024 * 1024) + + @Default(.gpuCacheLimit) var gpuCacheLimit + + @Default(.gpuMemoryLimit) var gpuMemoryLimit + + @Environment(LLMRunner.self) var runner + + var body: some View { + ProviderView(isExpanded: $isExpanded, isEnabled: .constant(nil)) { + Label() + } content: { + Content() + .labeledContentStyle(.horizontal) + .compactSliderSecondaryColor(.white) + } + } + + @ViewBuilder + func Label() -> some View { + HStack(spacing: 0) { + Text("ML") + .foregroundStyle(.black) + Text("X") + .foregroundStyle(Color(hex: "#D5D5D5")) + } + .font(.title2.weight(.medium)) + } + + @ViewBuilder + func Content() -> some View { + LabeledContent("GPU Cache Limit") { + CompactSlider( + value: $gpuCacheLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 + ) { + Text("\(Int(gpuCacheLimit))MB") + .foregroundStyle(.white) + } + .frame(width: 200) + .onChange(of: gpuCacheLimit) { oldValue, newValue in + if oldValue != newValue { + runner.loadState = .idle + } + } + } + + LabeledContent("GPU Memory Limit") { + CompactSlider( + value: $gpuMemoryLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 + ) { + Text("\(Int(gpuMemoryLimit))MB") + .foregroundStyle(.white) + } + .frame(width: 200) + .onChange(of: gpuMemoryLimit) { oldValue, newValue in + if oldValue != newValue { + runner.loadState = .idle + } + } + } + } +} diff --git a/ChatMLX/Features/Settings/ModelManager/Providers/OpenAIProvider.swift b/ChatMLX/Features/Settings/ModelManager/Providers/OpenAIProvider.swift new file mode 100644 index 0000000..9220412 --- /dev/null +++ b/ChatMLX/Features/Settings/ModelManager/Providers/OpenAIProvider.swift @@ -0,0 +1,45 @@ +// +// OpenAIProvider.swift +// ChatMLX +// +// Created by John Mai on 2024/10/4. +// + +import SwiftUI + +struct OpenAIProvider: View { + @State var isExpanded: Bool = false + @State var isEnabled: Bool? = false + + var body: some View { + ProviderView(isExpanded: $isExpanded, isEnabled: $isEnabled) { + Label() + } content: { + Content() + } + } + + @ViewBuilder + func Label() -> some View { + HStack { + Image("openai-logomark") + .resizable() + .scaledToFit() + .frame(height: 24) + Text("Other AI Provider") + } + .font(.title2.weight(.medium)) + } + + @ViewBuilder + func Content() -> some View { + VStack { + Text("👋🏻 It will be supported soon ~ ") + .font(.title2) + .fontWeight(.medium) + Text("🍻 If you have ideas, welcome to contribute.") + .font(.subheadline) + } + .italic() + } +} diff --git a/ChatMLX/Features/Settings/ModelManager/Providers/ProviderView.swift b/ChatMLX/Features/Settings/ModelManager/Providers/ProviderView.swift new file mode 100644 index 0000000..ad01732 --- /dev/null +++ b/ChatMLX/Features/Settings/ModelManager/Providers/ProviderView.swift @@ -0,0 +1,63 @@ +// +// ProviderView.swift +// ChatMLX +// +// Created by John Mai on 2024/10/4. +// + +import SwiftUI + +struct ProviderView: View { + @Binding var isExpanded: Bool + @Binding var isEnabled: Bool? + @ViewBuilder let label: () -> Label + @ViewBuilder let content: () -> Content + + let cornerRadius: CGFloat = 12 + + var body: some View { + VStack(spacing: 0) { + header + if isExpanded { + content() + .padding() + } + } + .background(.quinary) + .clipShape(.rect(cornerRadius: cornerRadius)) + .overlay { + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(.quaternary, lineWidth: 1) + } + } + + @MainActor + @ViewBuilder + private var header: some View { + HStack { + Image(systemName: "chevron.down") + .font(.title3.weight(.semibold)) + .rotationEffect(.degrees(isExpanded ? -180 : 0)) + + label() + + Spacer() + + if isEnabled != nil { + Toggle("", isOn: $isEnabled.toUnwrapped(defaultValue: false)) + .labelsHidden() + .toggleStyle(.switch) + } + } + .padding(.horizontal) + .frame(height: 45) + .background(Color.black.opacity(0.1)) + .onTapGesture { + withAnimation(.easeIn) { + isExpanded.toggle() + } + } + .zIndex(10) + .transition(.move(edge: .top)) + } +} diff --git a/ChatMLX/Features/Settings/SettingsView.swift b/ChatMLX/Features/Settings/SettingsView.swift index adff8ed..d3a1561 100644 --- a/ChatMLX/Features/Settings/SettingsView.swift +++ b/ChatMLX/Features/Settings/SettingsView.swift @@ -25,7 +25,7 @@ struct SettingsView: View { case .huggingFace: HuggingFaceView() case .models: - LocalModelsView() + ModelManagerView() case .downloadManager: DownloadManagerView() case .mlxCommunity: From 38872a0eb315a34a7f01714ed983bed02b132f98 Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sat, 5 Oct 2024 14:43:26 +0800 Subject: [PATCH 04/11] :art: Format Code --- ChatMLX.xcodeproj/project.pbxproj | 48 +++++++++++++++++++ ChatMLX/ChatMLXApp.swift | 5 +- ChatMLX/Components/ErrorAlertModifier.swift | 1 + .../UltramanNavigationSplitView.swift | 4 +- ChatMLX/Extensions/Color+Extensions.swift | 13 +++-- .../Conversation/ConversationDetailView.swift | 2 - .../ConversationSidebarView.swift | 8 ++-- .../Conversation/ConversationViewModel.swift | 1 - .../Conversation/MessageBubbleView.swift | 11 +++-- .../Conversation/RightSidebarView.swift | 34 +++---------- ChatMLX/Localizable.xcstrings | 21 ++++++++ ChatMLX/Models/DownloadTask.swift | 11 +++-- ChatMLX/Models/ModelConfig.swift | 19 ++++++++ ChatMLX/Models/Role.swift | 1 + ChatMLX/Utilities/LLMRunner.swift | 13 +++-- 15 files changed, 135 insertions(+), 57 deletions(-) create mode 100644 ChatMLX/Models/ModelConfig.swift diff --git a/ChatMLX.xcodeproj/project.pbxproj b/ChatMLX.xcodeproj/project.pbxproj index bbdd24e..1065409 100644 --- a/ChatMLX.xcodeproj/project.pbxproj +++ b/ChatMLX.xcodeproj/project.pbxproj @@ -84,6 +84,14 @@ 52A689F62CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */; }; 52A689F82CAE8DA30078CDF9 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */; }; 52A689FA2CAECFE00078CDF9 /* ErrorAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */; }; + 52B053272CAFD6DB00E8DDBA /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053262CAFD6D400E8DDBA /* ModelConfig.swift */; }; + 52B0532A2CAFDFB000E8DDBA /* ProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */; }; + 52B0532C2CAFEFD800E8DDBA /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0532B2CAFEFD000E8DDBA /* Color+Extensions.swift */; }; + 52B0534D2CB02C4600E8DDBA /* ModelManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */; }; + 52B053502CB02CF900E8DDBA /* MLXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0534F2CB02CF100E8DDBA /* MLXProvider.swift */; }; + 52B053522CB0385800E8DDBA /* OpenAIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053512CB0384E00E8DDBA /* OpenAIProvider.swift */; }; + 52B053542CB0F5B300E8DDBA /* LabeledContentStyle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053532CB0F5AA00E8DDBA /* LabeledContentStyle+Extensions.swift */; }; + 52B053572CB0F8EE00E8DDBA /* Binding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053562CB0F8EE00E8DDBA /* Binding+Extensions.swift */; }; 52E50B1D2C8D6E81005A89DE /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B1C2C8D6E81005A89DE /* LLM */; }; 52E50B202C8D719B005A89DE /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B1F2C8D719B005A89DE /* LLM */; }; 52E50B222C8D719B005A89DE /* MNIST in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B212C8D719B005A89DE /* MNIST */; }; @@ -166,6 +174,14 @@ 52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertModifier.swift; sourceTree = ""; }; + 52B053262CAFD6D400E8DDBA /* ModelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfig.swift; sourceTree = ""; }; + 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderView.swift; sourceTree = ""; }; + 52B0532B2CAFEFD000E8DDBA /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; + 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManagerView.swift; sourceTree = ""; }; + 52B0534F2CB02CF100E8DDBA /* MLXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXProvider.swift; sourceTree = ""; }; + 52B053512CB0384E00E8DDBA /* OpenAIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIProvider.swift; sourceTree = ""; }; + 52B053532CB0F5AA00E8DDBA /* LabeledContentStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LabeledContentStyle+Extensions.swift"; sourceTree = ""; }; + 52B053562CB0F8EE00E8DDBA /* Binding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -315,6 +331,7 @@ 526676222C85F903001EF113 /* Settings */ = { isa = PBXGroup; children = ( + 52B053282CAFDF8500E8DDBA /* ModelManager */, 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */, 526676142C85F903001EF113 /* DownloadManager */, 526676172C85F903001EF113 /* LocalModels */, @@ -342,6 +359,7 @@ 5266762F2C85F903001EF113 /* Models */ = { isa = PBXGroup; children = ( + 52B053262CAFD6D400E8DDBA /* ModelConfig.swift */, 526676252C85F903001EF113 /* DisplayStyle.swift */, 526676262C85F903001EF113 /* DownloadTask.swift */, 526676272C85F903001EF113 /* Language.swift */, @@ -385,6 +403,9 @@ 5266763D2C85F903001EF113 /* Extensions */ = { isa = PBXGroup; children = ( + 52B053562CB0F8EE00E8DDBA /* Binding+Extensions.swift */, + 52B053532CB0F5AA00E8DDBA /* LabeledContentStyle+Extensions.swift */, + 52B0532B2CAFEFD000E8DDBA /* Color+Extensions.swift */, 528D82252CABE19000163AAB /* Date+Extensions.swift */, 52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */, 526676382C85F903001EF113 /* Defaults+Extensions.swift */, @@ -396,6 +417,25 @@ path = Extensions; sourceTree = ""; }; + 52B053282CAFDF8500E8DDBA /* ModelManager */ = { + isa = PBXGroup; + children = ( + 52B0534E2CB02CDF00E8DDBA /* Providers */, + 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */, + ); + path = ModelManager; + sourceTree = ""; + }; + 52B0534E2CB02CDF00E8DDBA /* Providers */ = { + isa = PBXGroup; + children = ( + 52B053512CB0384E00E8DDBA /* OpenAIProvider.swift */, + 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */, + 52B0534F2CB02CF100E8DDBA /* MLXProvider.swift */, + ); + path = Providers; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -527,6 +567,8 @@ 526676562C85F903001EF113 /* MLXCommunityItemView.swift in Sources */, 526676442C85F903001EF113 /* UltramanMinimalistWindowModifier.swift in Sources */, 5266764A2C85F903001EF113 /* UltramanWindow.swift in Sources */, + 52B0532A2CAFDFB000E8DDBA /* ProviderView.swift in Sources */, + 52B0532C2CAFEFD800E8DDBA /* Color+Extensions.swift in Sources */, 526676702C85F903001EF113 /* Defaults+Extensions.swift in Sources */, 526676462C85F903001EF113 /* UltramanSecureField.swift in Sources */, 5266766C2C85F903001EF113 /* HubApi.swift in Sources */, @@ -535,6 +577,8 @@ 526676572C85F903001EF113 /* MLXCommunityView.swift in Sources */, 528D82262CABE19900163AAB /* Date+Extensions.swift in Sources */, 5266766D2C85F903001EF113 /* LLMRunner.swift in Sources */, + 52B053542CB0F5B300E8DDBA /* LabeledContentStyle+Extensions.swift in Sources */, + 52B0534D2CB02C4600E8DDBA /* ModelManagerView.swift in Sources */, 5266764E2C85F903001EF113 /* ConversationView.swift in Sources */, 5266766A2C85F903001EF113 /* Downloader.swift in Sources */, 526676732C85F903001EF113 /* String+Extensions.swift in Sources */, @@ -549,14 +593,17 @@ 526676532C85F903001EF113 /* DownloadTaskView.swift in Sources */, 52A689F82CAE8DA30078CDF9 /* SettingsViewModel.swift in Sources */, 5266764C2C85F903001EF113 /* ConversationSidebarItem.swift in Sources */, + 52B053572CB0F8EE00E8DDBA /* Binding+Extensions.swift in Sources */, 526676622C85F903001EF113 /* Language.swift in Sources */, 526676722C85F903001EF113 /* NSWindow+Extensions.swift in Sources */, + 52B053502CB02CF900E8DDBA /* MLXProvider.swift in Sources */, 526676412C85F903001EF113 /* SplashCodeSyntaxHighlighter.swift in Sources */, 526676542C85F903001EF113 /* LocalModelItemView.swift in Sources */, 526676632C85F903001EF113 /* LocalModel.swift in Sources */, 526676692C85F903001EF113 /* Styles.swift in Sources */, 5266765A2C85F903001EF113 /* GeneralView.swift in Sources */, 526675462C85EDCB001EF113 /* ChatMLXApp.swift in Sources */, + 52B053272CAFD6DB00E8DDBA /* ModelConfig.swift in Sources */, 526676492C85F903001EF113 /* UltramanTextField.swift in Sources */, 5266765E2C85F903001EF113 /* SettingsView.swift in Sources */, 528D831C2CAD49E600163AAB /* PersistenceController.swift in Sources */, @@ -569,6 +616,7 @@ 5266765B2C85F903001EF113 /* HuggingFaceView.swift in Sources */, 526676642C85F903001EF113 /* LocalModelGroup.swift in Sources */, 526676522C85F903001EF113 /* DownloadManagerView.swift in Sources */, + 52B053522CB0385800E8DDBA /* OpenAIProvider.swift in Sources */, 526676482C85F903001EF113 /* UltramanTextEditorWithPlaceholder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ChatMLX/ChatMLXApp.swift b/ChatMLX/ChatMLXApp.swift index 7148dc5..428ba3c 100644 --- a/ChatMLX/ChatMLXApp.swift +++ b/ChatMLX/ChatMLXApp.swift @@ -14,11 +14,10 @@ struct ChatMLXApp: App { @State private var conversationViewModel: ConversationViewModel = .init() @State private var settingsViewModel: SettingsViewModel = .init() + @State private var runner: LLMRunner = .init() @Default(.language) var language - @State private var runner = LLMRunner() - let persistenceController = PersistenceController.shared var body: some Scene { @@ -59,7 +58,7 @@ struct ChatMLXApp: App { \.locale, .init(identifier: language.rawValue) ) .environment(runner) - .frame(width: 620, height: 480) + .frame(width: 650, height: 480) .errorAlert( isPresented: $settingsViewModel.showErrorAlert, title: $settingsViewModel.errorTitle, diff --git a/ChatMLX/Components/ErrorAlertModifier.swift b/ChatMLX/Components/ErrorAlertModifier.swift index 8285a22..07494bc 100644 --- a/ChatMLX/Components/ErrorAlertModifier.swift +++ b/ChatMLX/Components/ErrorAlertModifier.swift @@ -9,6 +9,7 @@ import SwiftUI struct ErrorAlertModifier: ViewModifier { @Binding var showErrorAlert: Bool + @Binding var errorTitle: String? @Binding var error: Error? diff --git a/ChatMLX/Components/UltramanNavigationSplitView.swift b/ChatMLX/Components/UltramanNavigationSplitView.swift index cf420a9..e163136 100644 --- a/ChatMLX/Components/UltramanNavigationSplitView.swift +++ b/ChatMLX/Components/UltramanNavigationSplitView.swift @@ -73,7 +73,8 @@ extension View { alignment: alignment, content: { content() - }) + } + ) ] ) } @@ -110,6 +111,7 @@ struct UltramanNavigationSplitView: View { sidebar() .frame(width: sidebarWidth) .transition(.move(edge: .leading)) + .zIndex(10) } VStack(spacing: .zero) { diff --git a/ChatMLX/Extensions/Color+Extensions.swift b/ChatMLX/Extensions/Color+Extensions.swift index 645e1b4..17e6ad6 100644 --- a/ChatMLX/Extensions/Color+Extensions.swift +++ b/ChatMLX/Extensions/Color+Extensions.swift @@ -11,13 +11,16 @@ extension Color { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 Scanner(string: hex).scanHexInt64(&int) - let a, r, g, b: UInt64 + let a: UInt64 + let r: UInt64 + let g: UInt64 + let b: UInt64 switch hex.count { - case 3: // RGB (12-bit) + case 3: // RGB (12-bit) (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) - case 6: // RGB (24-bit) + case 6: // RGB (24-bit) (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) - case 8: // ARGB (32-bit) + case 8: // ARGB (32-bit) (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) default: (a, r, g, b) = (1, 1, 1, 0) @@ -27,7 +30,7 @@ extension Color { .sRGB, red: Double(r) / 255, green: Double(g) / 255, - blue: Double(b) / 255, + blue: Double(b) / 255, opacity: Double(a) / 255 ) } diff --git a/ChatMLX/Features/Conversation/ConversationDetailView.swift b/ChatMLX/Features/Conversation/ConversationDetailView.swift index 8cf4778..49de7e2 100644 --- a/ChatMLX/Features/Conversation/ConversationDetailView.swift +++ b/ChatMLX/Features/Conversation/ConversationDetailView.swift @@ -20,10 +20,8 @@ struct ConversationDetailView: View { @Environment(ConversationViewModel.self) private var vm @State private var newMessage = "" - @State private var showRightSidebar = false @State private var showInfoPopover = false - @State private var localModels: [LocalModel] = [] @State private var displayStyle: DisplayStyle = .markdown @State private var isEditorFullScreen = false diff --git a/ChatMLX/Features/Conversation/ConversationSidebarView.swift b/ChatMLX/Features/Conversation/ConversationSidebarView.swift index 3883da4..b3642a1 100644 --- a/ChatMLX/Features/Conversation/ConversationSidebarView.swift +++ b/ChatMLX/Features/Conversation/ConversationSidebarView.swift @@ -11,9 +11,6 @@ import SwiftUI struct ConversationSidebarView: View { @Environment(ConversationViewModel.self) private var conversationViewModel - - @Binding var selectedConversation: Conversation? - @Environment(\.managedObjectContext) private var viewContext @FetchRequest( @@ -22,14 +19,15 @@ struct ConversationSidebarView: View { ) private var conversations: FetchedResults + @Binding var selectedConversation: Conversation? + + @State private var keyword = "" @State private var showingNewConversationAlert = false @State private var newConversationTitle = "" @State private var showingClearConfirmation = false let padding: CGFloat = 8 - @State private var keyword = "" - var body: some View { VStack(spacing: 0) { HStack { diff --git a/ChatMLX/Features/Conversation/ConversationViewModel.swift b/ChatMLX/Features/Conversation/ConversationViewModel.swift index b9f0296..2aa5635 100644 --- a/ChatMLX/Features/Conversation/ConversationViewModel.swift +++ b/ChatMLX/Features/Conversation/ConversationViewModel.swift @@ -11,7 +11,6 @@ import SwiftUI class ConversationViewModel { var detailWidth: CGFloat = 550 var selectedConversation: Conversation? - var error: Error? var errorTitle: String? var showErrorAlert = false diff --git a/ChatMLX/Features/Conversation/MessageBubbleView.swift b/ChatMLX/Features/Conversation/MessageBubbleView.swift index 20ac639..43b6a67 100644 --- a/ChatMLX/Features/Conversation/MessageBubbleView.swift +++ b/ChatMLX/Features/Conversation/MessageBubbleView.swift @@ -10,15 +10,16 @@ import MarkdownUI import SwiftUI struct MessageBubbleView: View { - @ObservedObject var message: Message - @Binding var displayStyle: DisplayStyle - @State private var showToast = false - @Environment(LLMRunner.self) var runner @Environment(ConversationViewModel.self) var vm - @Environment(\.managedObjectContext) private var viewContext + @ObservedObject var message: Message + + @Binding var displayStyle: DisplayStyle + + @State private var showToast = false + private func copyText() { let pasteboard = NSPasteboard.general pasteboard.clearContents() diff --git a/ChatMLX/Features/Conversation/RightSidebarView.swift b/ChatMLX/Features/Conversation/RightSidebarView.swift index 0f0c09c..286df35 100644 --- a/ChatMLX/Features/Conversation/RightSidebarView.swift +++ b/ChatMLX/Features/Conversation/RightSidebarView.swift @@ -75,14 +75,7 @@ struct RightSidebarView: View { Text("Max Length") Spacer() CompactSlider( - value: Binding( - get: { - Double(conversation.maxLength) - }, - set: { - conversation.maxLength = Int64($0) - } - ), in: 0 ... 8192, step: 1 + value: $conversation.maxLength.asDouble(), in: 0 ... 8192, step: 1 ) { Text("\(Int(conversation.maxLength))") .foregroundStyle(.white) @@ -96,14 +89,8 @@ struct RightSidebarView: View { Text("Repetition Context Size") Spacer() CompactSlider( - value: Binding( - get: { - Double(conversation.repetitionContextSize) - }, - set: { - conversation.repetitionContextSize = Int($0) - } - ), in: 0 ... 100, step: 1 + value: $conversation.repetitionContextSize.asDouble(), in: 0 ... 100, + step: 1 ) { Text("\(conversation.repetitionContextSize)") .foregroundStyle(.white) @@ -157,14 +144,8 @@ struct RightSidebarView: View { Text("Max Messages Limit") Spacer() CompactSlider( - value: Binding( - get: { - Double(conversation.maxMessagesLimit) - }, - set: { - conversation.maxMessagesLimit = Int32($0) - } - ), in: 1 ... 50, step: 1 + value: $conversation.maxMessagesLimit.asDouble(), in: 1 ... 50, + step: 1 ) { Text("\(conversation.maxMessagesLimit)") .foregroundStyle(.white) @@ -190,9 +171,7 @@ struct RightSidebarView: View { UltramanTextEditor( text: $conversation.systemPrompt, placeholder: "System prompt", - onSubmit: { - - } + onSubmit: {} ) .frame(height: 100) .padding(padding) @@ -214,5 +193,6 @@ struct RightSidebarView: View { emphasized: true ) ) + .zIndex(10) } } diff --git a/ChatMLX/Localizable.xcstrings b/ChatMLX/Localizable.xcstrings index e4c4304..5c8cd85 100644 --- a/ChatMLX/Localizable.xcstrings +++ b/ChatMLX/Localizable.xcstrings @@ -46,6 +46,12 @@ }, "%lldMB" : { "shouldTranslate" : false + }, + "🍻 If you have ideas, welcome to contribute." : { + + }, + "👋🏻 It will be supported soon ~ " : { + }, "About" : { "extractionState" : "manual", @@ -851,6 +857,9 @@ } } } + }, + "GPU Memory Limit" : { + }, "https://hf-mirror.com" : { "shouldTranslate" : false @@ -1057,6 +1066,9 @@ } } } + }, + "ML" : { + }, "MLX Community" : { "extractionState" : "manual", @@ -1117,6 +1129,9 @@ } } } + }, + "Model Manager" : { + }, "Model Settings" : { "localizations" : { @@ -1326,6 +1341,9 @@ } }, "shouldTranslate" : false + }, + "Other AI Provider" : { + }, "Please enter Hugging Face Repo ID" : { "extractionState" : "manual", @@ -2086,6 +2104,9 @@ } } } + }, + "X" : { + } }, "version" : "1.0" diff --git a/ChatMLX/Models/DownloadTask.swift b/ChatMLX/Models/DownloadTask.swift index c401865..02ac2fe 100644 --- a/ChatMLX/Models/DownloadTask.swift +++ b/ChatMLX/Models/DownloadTask.swift @@ -36,9 +36,12 @@ class DownloadTask: Identifiable, Equatable { self.isDownloading = true self.error = nil self.progress = 0 + + // todo: token & custom endpoint let currentEndpoint = Defaults[.huggingFaceEndpoint] self.hub = HubApi( - downloadBase: FileManager.default.temporaryDirectory, endpoint: currentEndpoint) + downloadBase: FileManager.default.temporaryDirectory, endpoint: currentEndpoint + ) Task { [self] in do { @@ -55,7 +58,7 @@ class DownloadTask: Identifiable, Equatable { self.hub = nil - try await moveToDocumentsDirectory(from: temporaryModelDirectory) + try await self.moveToDocumentsDirectory(from: temporaryModelDirectory) await MainActor.run { self.isDownloading = false @@ -63,7 +66,7 @@ class DownloadTask: Identifiable, Equatable { self.progress = 1.0 } } catch { - logger.error("DownloadTask Error: \(error.localizedDescription)") + self.logger.error("DownloadTask Error: \(error.localizedDescription)") self.hub = nil await MainActor.run { self.error = error @@ -95,6 +98,6 @@ class DownloadTask: Identifiable, Equatable { try fileManager.copyItem(at: temporaryModelDirectory, to: destinationPath) - logger.info("Model moved to: \(destinationPath.path)") + self.logger.info("Model moved to: \(destinationPath.path)") } } diff --git a/ChatMLX/Models/ModelConfig.swift b/ChatMLX/Models/ModelConfig.swift new file mode 100644 index 0000000..8e93c93 --- /dev/null +++ b/ChatMLX/Models/ModelConfig.swift @@ -0,0 +1,19 @@ +// +// ModelConfig.swift +// ChatMLX +// +// Created by John Mai on 2024/10/4. +// + +import Defaults + +enum Provider: String, Defaults.Serializable { + case mlx +} + +struct ModelConfig { + var name: String + var path: String + var provider: Provider + var description: String +} diff --git a/ChatMLX/Models/Role.swift b/ChatMLX/Models/Role.swift index 14819da..51b5df9 100644 --- a/ChatMLX/Models/Role.swift +++ b/ChatMLX/Models/Role.swift @@ -9,6 +9,7 @@ public enum Role: String, Codable { case user case assistant case system + case tool var description: String { "\(self)" diff --git a/ChatMLX/Utilities/LLMRunner.swift b/ChatMLX/Utilities/LLMRunner.swift index dcb4c66..8eb63c2 100644 --- a/ChatMLX/Utilities/LLMRunner.swift +++ b/ChatMLX/Utilities/LLMRunner.swift @@ -41,10 +41,15 @@ class LLMRunner { switch loadState { case .idle: - let cacheLimit = - UserDefaults.standard.integer( - forKey: Defaults.Keys.gpuCacheLimit.name) * 1024 * 1024 - MLX.GPU.set(cacheLimit: cacheLimit) + + let enableGPUMemorySettings = Defaults[.enableGPUMemorySettings] + if enableGPUMemorySettings { + let cacheLimit = Defaults[.gpuCacheLimit] * 1024 * 1024 + MLX.GPU.set(cacheLimit: cacheLimit) + + let memoryLimit = Defaults[.gpuMemoryLimit] * 1024 * 1024 + MLX.GPU.set(memoryLimit: memoryLimit) + } let modelContainer = try await MLXLLM.loadModelContainer( configuration: modelConfiguration From 42eae39d34f574568abd5b4dd87011c9fa349f5f Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sat, 5 Oct 2024 14:44:29 +0800 Subject: [PATCH 05/11] :arrow_up: Update Version --- ChatMLX.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ChatMLX.xcodeproj/project.pbxproj b/ChatMLX.xcodeproj/project.pbxproj index 1065409..3b1e06e 100644 --- a/ChatMLX.xcodeproj/project.pbxproj +++ b/ChatMLX.xcodeproj/project.pbxproj @@ -750,7 +750,7 @@ CODE_SIGN_ENTITLEMENTS = ChatMLX/ChatMLX.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"ChatMLX/Preview Content\""; DEVELOPMENT_TEAM = RFGFKQEKRH; ENABLE_HARDENED_RUNTIME = YES; @@ -763,7 +763,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.1.1; + MARKETING_VERSION = 1.1.2; PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -779,7 +779,7 @@ CODE_SIGN_ENTITLEMENTS = ChatMLX/ChatMLXRelease.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"ChatMLX/Preview Content\""; DEVELOPMENT_TEAM = RFGFKQEKRH; ENABLE_HARDENED_RUNTIME = YES; @@ -792,7 +792,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.1.1; + MARKETING_VERSION = 1.1.2; PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; From 10e08c33ad4d51f179fed3d85e5fd131f15f2c9b Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sat, 5 Oct 2024 17:50:24 +0800 Subject: [PATCH 06/11] :technologist: Improvements --- .../Conversation/ConversationDetailView.swift | 2 +- .../ConversationSidebarItem.swift | 37 +++--- .../ConversationSidebarView.swift | 110 ++++++++++-------- .../Conversation/ConversationView.swift | 4 +- .../Conversation/EmptyConversation.swift | 21 ++-- .../Conversation/MessageBubbleView.swift | 36 +++--- ChatMLX/Models/Message+CoreDataClass.swift | 11 ++ ChatMLX/Utilities/LLMRunner.swift | 11 +- 8 files changed, 126 insertions(+), 106 deletions(-) diff --git a/ChatMLX/Features/Conversation/ConversationDetailView.swift b/ChatMLX/Features/Conversation/ConversationDetailView.swift index 49de7e2..f24d8bc 100644 --- a/ChatMLX/Features/Conversation/ConversationDetailView.swift +++ b/ChatMLX/Features/Conversation/ConversationDetailView.swift @@ -87,7 +87,7 @@ struct ConversationDetailView: View { MessageBubbleView( message: message, displayStyle: $displayStyle - ).id(message.id) + ) } } .padding() diff --git a/ChatMLX/Features/Conversation/ConversationSidebarItem.swift b/ChatMLX/Features/Conversation/ConversationSidebarItem.swift index c8fedfd..982017d 100644 --- a/ChatMLX/Features/Conversation/ConversationSidebarItem.swift +++ b/ChatMLX/Features/Conversation/ConversationSidebarItem.swift @@ -11,17 +11,12 @@ struct ConversationSidebarItem: View { @ObservedObject var conversation: Conversation @Environment(\.managedObjectContext) private var viewContext + @Environment(ConversationViewModel.self) private var vm - @Binding var selectedConversation: Conversation? - - @State private var isHovering: Bool = false @State private var isActive: Bool = false - @State private var showIndicator: Bool = false var body: some View { - Button { - selectedConversation = conversation - } label: { + Button(action: selectConversation) { VStack(alignment: .leading, spacing: 4) { Text(LocalizedStringKey(conversation.title)) .font(.headline) @@ -33,20 +28,18 @@ struct ConversationSidebarItem: View { Spacer() - Text(conversation.updatedAt.toFormatted()) - .font(.caption) + if !(conversation.isFault || conversation.isDeleted) { + Text(conversation.updatedAt.toFormatted()) + .font(.caption) + } } .foregroundStyle(.white.opacity(0.7)) } .padding(6) } .buttonStyle(UltramanSidebarButtonStyle(isActive: $isActive)) - .onAppear { - checkIfSelfIsActiveTab() - } - .onChange(of: selectedConversation) { _, _ in - checkIfSelfIsActiveTab() - } + .onAppear(perform: updateActiveState) + .onChange(of: vm.selectedConversation) { _, _ in updateActiveState() } .contextMenu { Button(role: .destructive, action: deleteConversation) { Label("Delete", systemImage: "trash") @@ -54,13 +47,21 @@ struct ConversationSidebarItem: View { } } - private func checkIfSelfIsActiveTab() { + private func selectConversation() { + vm.selectedConversation = conversation + } + + private func updateActiveState() { withAnimation(.easeOut(duration: 0.1)) { - isActive = selectedConversation == conversation + isActive = vm.selectedConversation == conversation } } private func deleteConversation() { - try? PersistenceController.shared.delete(conversation) + do { + try PersistenceController.shared.delete(conversation) + } catch { + vm.throwError(error, title: "Delete Conversation Failed") + } } } diff --git a/ChatMLX/Features/Conversation/ConversationSidebarView.swift b/ChatMLX/Features/Conversation/ConversationSidebarView.swift index b3642a1..4e6c588 100644 --- a/ChatMLX/Features/Conversation/ConversationSidebarView.swift +++ b/ChatMLX/Features/Conversation/ConversationSidebarView.swift @@ -10,7 +10,7 @@ import Luminare import SwiftUI struct ConversationSidebarView: View { - @Environment(ConversationViewModel.self) private var conversationViewModel + @Environment(ConversationViewModel.self) private var vm @Environment(\.managedObjectContext) private var viewContext @FetchRequest( @@ -22,70 +22,82 @@ struct ConversationSidebarView: View { @Binding var selectedConversation: Conversation? @State private var keyword = "" - @State private var showingNewConversationAlert = false - @State private var newConversationTitle = "" - @State private var showingClearConfirmation = false let padding: CGFloat = 8 var body: some View { VStack(spacing: 0) { - HStack { - Spacer() - Button(action: conversationViewModel.createConversation) { - Image(systemName: "plus") - } + headerView() + logoView() + searchField() + conversationList() + } + .background(.black.opacity(0.4)) + } - SettingsLink { - Image(systemName: "gear") - } + @MainActor + @ViewBuilder + private func headerView() -> some View { + HStack { + Spacer() + Button(action: vm.createConversation) { + Image(systemName: "plus") } - .frame(height: 50) - .padding(.horizontal, padding) - .buttonStyle(.plain) - - HStack { - Image("AppLogo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 60, height: 60) - .shadow(radius: 5) - Text("ChatMLX") - .font(.title) - .fontWeight(.bold) + SettingsLink { + Image(systemName: "gear") } + } + .frame(height: 50) + .padding(.horizontal, padding) + .buttonStyle(.plain) + } - LuminareSection { - UltramanTextField( - $keyword, placeholder: Text("Search Conversation..."), - onSubmit: updateSearchPredicate - ) + @MainActor + @ViewBuilder + private func logoView() -> some View { + HStack { + Image("AppLogo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 60, height: 60) + .shadow(radius: 5) + Text("ChatMLX") + .font(.title) + .fontWeight(.bold) + } + } - .frame(height: 25) - }.padding(.horizontal, padding) + @MainActor + @ViewBuilder + private func searchField() -> some View { + LuminareSection { + UltramanTextField( + $keyword, + placeholder: Text("Search Conversation..."), + onSubmit: updateSearchPredicate + ) + .frame(height: 25) + } + .padding(.horizontal, padding) + } - ScrollView { - LazyVStack(spacing: 0) { - ForEach(conversations) { conversation in - ConversationSidebarItem( - conversation: conversation, - selectedConversation: $selectedConversation - ) - } + @MainActor + @ViewBuilder + private func conversationList() -> some View { + ScrollView { + LazyVStack(spacing: 0) { + ForEach(conversations) { conversation in + ConversationSidebarItem(conversation: conversation) } } - .padding(.top, 6) } - .background(.black.opacity(0.4)) + .padding(.top, 6) } private func updateSearchPredicate() { - if keyword.isEmpty { - conversations.nsPredicate = nil - } else { - conversations.nsPredicate = NSPredicate( - format: "title CONTAINS [cd] %@ OR ANY messages.content CONTAINS [cd] %@", keyword, - keyword) - } + conversations.nsPredicate = keyword.isEmpty ? nil : NSPredicate( + format: "title CONTAINS [cd] %@ OR ANY messages.content CONTAINS [cd] %@", + keyword, keyword + ) } } diff --git a/ChatMLX/Features/Conversation/ConversationView.swift b/ChatMLX/Features/Conversation/ConversationView.swift index 27067d4..10b6eb0 100644 --- a/ChatMLX/Features/Conversation/ConversationView.swift +++ b/ChatMLX/Features/Conversation/ConversationView.swift @@ -19,7 +19,7 @@ struct ConversationView: View { selectedConversation: $conversationViewModel.selectedConversation) }, detail: { - Detail() + detailView() } ) .foregroundColor(.white) @@ -28,7 +28,7 @@ struct ConversationView: View { @MainActor @ViewBuilder - private func Detail() -> some View { + private func detailView() -> some View { Group { if let conversation = conversationViewModel.selectedConversation { ConversationDetailView( diff --git a/ChatMLX/Features/Conversation/EmptyConversation.swift b/ChatMLX/Features/Conversation/EmptyConversation.swift index 172df0b..df8cb1b 100644 --- a/ChatMLX/Features/Conversation/EmptyConversation.swift +++ b/ChatMLX/Features/Conversation/EmptyConversation.swift @@ -14,22 +14,15 @@ struct EmptyConversation: View { var body: some View { ContentUnavailableView { Label("No Conversation", systemImage: "tray.fill") - .foregroundColor(.white) } description: { Text("Please select a new conversation") - .foregroundColor(.white) - Button( - action: conversationViewModel.createConversation, - label: { - HStack { - Image(systemName: "plus") - .foregroundStyle(.white) - Text("New Conversation") - } - .foregroundColor(.white) - } - ).buttonStyle(LuminareCompactButtonStyle()) - .fixedSize() + + Button(action: conversationViewModel.createConversation) { + Label("New Conversation", systemImage: "plus") + } + .buttonStyle(LuminareCompactButtonStyle()) + .fixedSize() } + .foregroundColor(.white) } } diff --git a/ChatMLX/Features/Conversation/MessageBubbleView.swift b/ChatMLX/Features/Conversation/MessageBubbleView.swift index 43b6a67..83147d2 100644 --- a/ChatMLX/Features/Conversation/MessageBubbleView.swift +++ b/ChatMLX/Features/Conversation/MessageBubbleView.swift @@ -20,13 +20,6 @@ struct MessageBubbleView: View { @State private var showToast = false - private func copyText() { - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString(message.content, forType: .string) - showToast = true - } - var body: some View { HStack { if message.role == .assistant { @@ -91,9 +84,12 @@ struct MessageBubbleView: View { Image(systemName: "arrow.clockwise") .help("Regenerate") } + .disabled(runner.running) - Text(message.updatedAt.toTimeFormatted()) - .font(.caption) + if !(message.isFault || message.isDeleted) { + Text(message.updatedAt.toTimeFormatted()) + .font(.caption) + } if message.role == .assistant, message.inferring { ProgressView() @@ -144,12 +140,8 @@ struct MessageBubbleView: View { private func delete() { guard message.role == .user else { return } - let conversation = message.conversation - let messages = conversation.messages - if let index = messages.firstIndex(of: message) { - for message in messages[index...] { - viewContext.delete(message) - } + for message in message.suffixMessages() { + viewContext.delete(message) } Task(priority: .background) { @@ -170,16 +162,22 @@ struct MessageBubbleView: View { Task { let conversation = message.conversation - let messages = conversation.messages - if let index = messages.firstIndex(of: message) { - for message in messages[index...] { + + if conversation.messages.last != message { + for message in message.suffixMessages() { viewContext.delete(message) } } - await MainActor.run { runner.generate(conversation: conversation, in: viewContext) } } } + + private func copyText() { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(message.content, forType: .string) + showToast = true + } } diff --git a/ChatMLX/Models/Message+CoreDataClass.swift b/ChatMLX/Models/Message+CoreDataClass.swift index f87083e..b25bb91 100644 --- a/ChatMLX/Models/Message+CoreDataClass.swift +++ b/ChatMLX/Models/Message+CoreDataClass.swift @@ -38,4 +38,15 @@ public class Message: NSManagedObject { "content": self.content, ] } + + func suffixMessages() -> [Message] { + let conversation = self.conversation + let messages = conversation.messages + + guard let index = messages.firstIndex(of: self) else { + return [] + } + + return Array(messages[index...]) + } } diff --git a/ChatMLX/Utilities/LLMRunner.swift b/ChatMLX/Utilities/LLMRunner.swift index 8eb63c2..37dfc1c 100644 --- a/ChatMLX/Utilities/LLMRunner.swift +++ b/ChatMLX/Utilities/LLMRunner.swift @@ -6,10 +6,10 @@ // import Defaults +import Metal import MLX import MLXLLM import MLXRandom -import Metal import SwiftUI import Tokenizers @@ -113,13 +113,18 @@ class LLMRunner { } func generate( - conversation: Conversation, in context: NSManagedObjectContext, + conversation: Conversation, + in context: NSManagedObjectContext, progressing: @escaping () -> Void = {} ) { guard !running else { return } running = true - let assistantMessage = Message(context: context).assistant(conversation: conversation) + let assistantMessage: Message = if let message = conversation.messages.last, message.role == .assistant { + message + } else { + Message(context: context).assistant(conversation: conversation) + } let parameters = GenerateParameters( temperature: conversation.temperature, From ad46de4b0db43c9ab16cf6d5cadf78d094527213 Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sat, 5 Oct 2024 23:09:56 +0800 Subject: [PATCH 07/11] :memo: Update README.md --- README-zh_CN.md | 36 ++++++++++++++++++++++++++++++++++-- README.md | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/README-zh_CN.md b/README-zh_CN.md index efb9db6..8d7c8ac 100644 --- a/README-zh_CN.md +++ b/README-zh_CN.md @@ -2,7 +2,6 @@ [English](./README.md) | 简体中文 - [![贡献者][contributors-shield]][contributors-url] [![分支数][forks-shield]][forks-url] [![星标数][stars-shield]][stars-url] @@ -46,19 +45,52 @@ ![iShot_2024-08-31_23.55.39.png](images/iShot_2024-08-31_23.55.39.png) +## 参与贡献 🤝 + +我们非常欢迎各种形式的贡献。如果你对贡献 ChatMLX 感兴趣,可以大展身手,向我们展示你的奇思妙想。 + +[![][github-contrib-shield]][github-contrib-link] + +## Links 🌐 + +- [MLX Swift](https://github.com/ml-explore/mlx-swift) - [MLX](https://github.com/ml-explore/mlx)的Swift API +- [Transformers Swift](https://github.com/huggingface/swift-transformers) - + 在Swift中实现类似于[transformers](https://github.com/huggingface/transformers)的API的Swift包 +- [Jinja Swift](https://github.com/maiqingqiang/Jinja) - 一个简约的 Swift + 实现 [Jinja](https://jinja.palletsprojects.com/en/3.1.x/) 模板引擎,专门用于解析和渲染 ML 聊天模板。 + ## Star 历史 🌟 [![星标历史图表](https://api.star-history.com/svg?repos=maiqingqiang/ChatMLX&type=Date)](https://star-history.com/#maiqingqiang/ChatMLX&Date) +
+ +[![][back-to-top]](#readme-top) + +
+ [contributors-shield]: https://img.shields.io/github/contributors/maiqingqiang/ChatMLX.svg?style=for-the-badge + [contributors-url]: https://github.com/maiqingqiang/ChatMLX/graphs/contributors + [forks-shield]: https://img.shields.io/github/forks/maiqingqiang/ChatMLX.svg?style=for-the-badge + [forks-url]: https://github.com/maiqingqiang/ChatMLX/network/members + [stars-shield]: https://img.shields.io/github/stars/maiqingqiang/ChatMLX.svg?style=for-the-badge + [stars-url]: https://github.com/maiqingqiang/ChatMLX/stargazers + [issues-shield]: https://img.shields.io/github/issues/maiqingqiang/ChatMLX.svg?style=for-the-badge + [issues-url]: https://github.com/maiqingqiang/ChatMLX/issues + [license-shield]: https://img.shields.io/github/license/maiqingqiang/ChatMLX.svg?style=for-the-badge + [license-url]: https://github.com/maiqingqiang/ChatMLX/blob/main/LICENSE -

( < a href="#readme-top ">返回顶部< / a > )< p > \ No newline at end of file +[github-contrib-link]: https://github.com/maiqingqiang/ChatMLX/graphs/contributors + +[github-contrib-shield]: https://contrib.rocks/image?repo=maiqingqiang/ChatMLX + +[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square diff --git a/README.md b/README.md index 0249254..8dd7597 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ English | [简体中文](./README-zh_CN.md) Logo -

ChatMLX

+

ChatMLX

ChatMLX is a modern, open-source, high-performance chat application for MacOS based on large language models, based on the powerful performance of MLX and Apple silicon. It supports multiple models, providing users with a rich variety of conversation options. It runs LLM locally to ensure user privacy and security. @@ -45,19 +45,54 @@ https://github.com/user-attachments/assets/75984252-058f-4782-ad5d-33b3ce772639 ![iShot_2024-08-31_23.55.39.png](images/iShot_2024-08-31_23.55.39.png) +## 参与贡献 🤝 + +Contributions of all types are more than welcome, if you are interested in contributing ChatMLX, feel free to show us +what you’re made of. + +[![][github-contrib-shield]][github-contrib-link] + +## Links 🌐 + +- [MLX Swift](https://github.com/ml-explore/mlx-swift) - Swift API for [MLX](https://github.com/ml-explore/mlx) +- [Transformers Swift](https://github.com/huggingface/swift-transformers) - Swift Package to implement + a [transformers](https://github.com/huggingface/transformers)-like API in Swift +- [Jinja Swift](https://github.com/maiqingqiang/Jinja) - A minimalistic Swift implementation of + the [Jinja](https://jinja.palletsprojects.com/en/3.1.x/) templating engine, specifically designed for parsing and + rendering ML chat templates. + ## Star History 🌟 [![Star History Chart](https://api.star-history.com/svg?repos=maiqingqiang/ChatMLX&type=Date)](https://star-history.com/#maiqingqiang/ChatMLX&Date) +

+ +[![][back-to-top]](#readme-top) + +
+ [contributors-shield]: https://img.shields.io/github/contributors/maiqingqiang/ChatMLX.svg?style=for-the-badge + [contributors-url]: https://github.com/maiqingqiang/ChatMLX/graphs/contributors + [forks-shield]: https://img.shields.io/github/forks/maiqingqiang/ChatMLX.svg?style=for-the-badge + [forks-url]: https://github.com/maiqingqiang/ChatMLX/network/members + [stars-shield]: https://img.shields.io/github/stars/maiqingqiang/ChatMLX.svg?style=for-the-badge + [stars-url]: https://github.com/maiqingqiang/ChatMLX/stargazers + [issues-shield]: https://img.shields.io/github/issues/maiqingqiang/ChatMLX.svg?style=for-the-badge + [issues-url]: https://github.com/maiqingqiang/ChatMLX/issues + [license-shield]: https://img.shields.io/github/license/maiqingqiang/ChatMLX.svg?style=for-the-badge + [license-url]: https://github.com/maiqingqiang/ChatMLX/blob/main/LICENSE -

(back to top)

+[github-contrib-link]: https://github.com/maiqingqiang/ChatMLX/graphs/contributors + +[github-contrib-shield]: https://contrib.rocks/image?repo=maiqingqiang/ChatMLX + +[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square From d1532c4d938d2498919b5e95d52da5006fc6b428 Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sun, 6 Oct 2024 18:09:35 +0800 Subject: [PATCH 08/11] :technologist: Improvements --- .../LabeledContentStyle+Extensions.swift | 14 +++ .../ModelManager/ModelManagerView.swift | 13 +-- .../ModelManager/Providers/MLXProvider.swift | 85 ++++++++++++++----- ChatMLX/Localizable.xcstrings | 9 ++ ChatMLX/Utilities/LLMRunner.swift | 2 + 5 files changed, 97 insertions(+), 26 deletions(-) diff --git a/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift b/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift index 05a8f92..92da75d 100644 --- a/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift +++ b/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift @@ -20,3 +20,17 @@ struct HorizontalLabeledContentStyle: LabeledContentStyle { extension LabeledContentStyle where Self == HorizontalLabeledContentStyle { static var horizontal: HorizontalLabeledContentStyle { .init() } } + + +struct VerticalLabeledContentStyle: LabeledContentStyle { + func makeBody(configuration: Configuration) -> some View { + VStack(alignment:.leading) { + configuration.label + configuration.content + } + } +} + +extension LabeledContentStyle where Self == VerticalLabeledContentStyle { + static var vertical: VerticalLabeledContentStyle { .init() } +} diff --git a/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift b/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift index e1167f4..71ba85c 100644 --- a/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift +++ b/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift @@ -10,12 +10,15 @@ struct ModelManagerView: View { @State var isMLXExpanded: Bool = true var body: some View { - VStack { - MLXProvider() - OpenAIProvider() - Spacer() + ScrollView { + LazyVStack { + MLXProvider() + OpenAIProvider() + + } + .padding() } - .padding() + .ultramanNavigationTitle("Model Manager") } } diff --git a/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift b/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift index 43ddf50..31e47b4 100644 --- a/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift +++ b/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift @@ -15,6 +15,8 @@ struct MLXProvider: View { let maxRAM = ProcessInfo.processInfo.physicalMemory / (1024 * 1024) + @Default(.enableGPUMemorySettings) var enableGPUMemorySettings + @Default(.gpuCacheLimit) var gpuCacheLimit @Default(.gpuMemoryLimit) var gpuMemoryLimit @@ -44,34 +46,75 @@ struct MLXProvider: View { @ViewBuilder func Content() -> some View { - LabeledContent("GPU Cache Limit") { - CompactSlider( - value: $gpuCacheLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 - ) { - Text("\(Int(gpuCacheLimit))MB") - .foregroundStyle(.white) + LabeledContent("Enable GPU Memory Settings") { + Toggle("", isOn: $enableGPUMemorySettings) + .labelsHidden() + .toggleStyle(.switch) + } + + if enableGPUMemorySettings { + LabeledContent("GPU Cache Limit") { + CompactSlider( + value: $gpuCacheLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 + ) { + Text("\(Int(gpuCacheLimit))MB") + .foregroundStyle(.white) + } + .frame(width: 200) + .onChange(of: gpuCacheLimit) { oldValue, newValue in + if oldValue != newValue { + runner.loadState = .idle + } + } } - .frame(width: 200) - .onChange(of: gpuCacheLimit) { oldValue, newValue in - if oldValue != newValue { - runner.loadState = .idle + + LabeledContent("GPU Memory Limit") { + CompactSlider( + value: $gpuMemoryLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 + ) { + Text("\(Int(gpuMemoryLimit))MB") + .foregroundStyle(.white) + } + .frame(width: 200) + .onChange(of: gpuMemoryLimit) { oldValue, newValue in + if oldValue != newValue { + runner.loadState = .idle + } } } } - LabeledContent("GPU Memory Limit") { - CompactSlider( - value: $gpuMemoryLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 - ) { - Text("\(Int(gpuMemoryLimit))MB") - .foregroundStyle(.white) + LabeledContent("Model List") { + List { + item() + item() + item() + item() + item() + item() } - .frame(width: 200) - .onChange(of: gpuMemoryLimit) { oldValue, newValue in - if oldValue != newValue { - runner.loadState = .idle - } + .listStyle(.plain) + .scrollIndicators(.hidden) + .frame(height: 200) + .scrollContentBackground(.hidden) + } + .labeledContentStyle(.vertical) + } + + @MainActor + @ViewBuilder + private func item() -> some View { + VStack { + HStack { + Text("model.name") + Spacer() } } + .padding() + .background(.black.opacity(0.3)) + .listRowSeparator(.hidden) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: .black, radius: 2) + .listRowInsets(EdgeInsets(top: 0, leading: -5, bottom: 10, trailing: -5)) } } diff --git a/ChatMLX/Localizable.xcstrings b/ChatMLX/Localizable.xcstrings index 5c8cd85..31727eb 100644 --- a/ChatMLX/Localizable.xcstrings +++ b/ChatMLX/Localizable.xcstrings @@ -601,6 +601,9 @@ } } } + }, + "Enable GPU Memory Settings" : { + }, "Endpoint" : { "localizations" : { @@ -1129,6 +1132,9 @@ } } } + }, + "Model List" : { + }, "Model Manager" : { @@ -1188,6 +1194,9 @@ } } } + }, + "model.name" : { + }, "Models" : { "extractionState" : "manual", diff --git a/ChatMLX/Utilities/LLMRunner.swift b/ChatMLX/Utilities/LLMRunner.swift index 37dfc1c..856d792 100644 --- a/ChatMLX/Utilities/LLMRunner.swift +++ b/ChatMLX/Utilities/LLMRunner.swift @@ -24,8 +24,10 @@ class LLMRunner { case loaded(ModelContainer) } + @ObservationIgnored var loadState: LoadState = .idle + @ObservationIgnored var modelConfiguration: ModelConfiguration? var gpuActiveMemory: Int = 0 From 867bbbb0728b0f3b936c30dcd7b2fe671895db16 Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Tue, 8 Oct 2024 10:12:21 +0800 Subject: [PATCH 09/11] :zap: add ChatMLXCore --- .../Settings/ExperimentalFeaturesView.swift | 10 +- ChatMLXCore/.gitignore | 8 + ChatMLXCore/Package.resolved | 158 ++++++++++++++++++ ChatMLXCore/Package.swift | 56 +++++++ .../Sources/ChatMLXCore/ChatMLXApp.swift | 19 +++ .../Sources/ChatMLXCore/ChatMLXCore.swift | 2 + 6 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 ChatMLXCore/.gitignore create mode 100644 ChatMLXCore/Package.resolved create mode 100644 ChatMLXCore/Package.swift create mode 100644 ChatMLXCore/Sources/ChatMLXCore/ChatMLXApp.swift create mode 100644 ChatMLXCore/Sources/ChatMLXCore/ChatMLXCore.swift diff --git a/ChatMLX/Features/Settings/ExperimentalFeaturesView.swift b/ChatMLX/Features/Settings/ExperimentalFeaturesView.swift index 1a97b86..918a9f8 100644 --- a/ChatMLX/Features/Settings/ExperimentalFeaturesView.swift +++ b/ChatMLX/Features/Settings/ExperimentalFeaturesView.swift @@ -18,18 +18,14 @@ struct ExperimentalFeaturesView: View { var body: some View { VStack(spacing: 18) { LuminareSection("Window Appearance") { - HStack { - Text("Apple Intelligence Effect") - Spacer() + LabeledContent("Apple Intelligence Effect") { Toggle("", isOn: $enableAppleIntelligenceEffect) .toggleStyle(.switch) } .padding(6) if enableAppleIntelligenceEffect { - HStack { - Text("Display Mode") - Spacer() + LabeledContent("Display Mode") { Picker( "Display Mode", selection: $appleIntelligenceEffectDisplay @@ -46,6 +42,8 @@ struct ExperimentalFeaturesView: View { .padding(8) } } + .labeledContentStyle(.horizontal) + Spacer() } .ultramanToolbar(alignment: .trailing) { diff --git a/ChatMLXCore/.gitignore b/ChatMLXCore/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/ChatMLXCore/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/ChatMLXCore/Package.resolved b/ChatMLXCore/Package.resolved new file mode 100644 index 0000000..48f7f7c --- /dev/null +++ b/ChatMLXCore/Package.resolved @@ -0,0 +1,158 @@ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", + "version" : "5.9.1" + } + }, + { + "identity" : "alerttoast", + "kind" : "remoteSourceControl", + "location" : "https://github.com/elai950/AlertToast.git", + "state" : { + "revision" : "b39252eacd159904afd7d718bba0afabb87f2f2f", + "version" : "1.3.9" + } + }, + { + "identity" : "compactslider", + "kind" : "remoteSourceControl", + "location" : "https://github.com/buh/CompactSlider.git", + "state" : { + "revision" : "abe4d1df6f0c85dcb133266cc07c2a5d08295726", + "version" : "1.1.6" + } + }, + { + "identity" : "defaults", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/Defaults.git", + "state" : { + "branch" : "main", + "revision" : "a89f799930c92a85aa04b72131849d46c0833ab3" + } + }, + { + "identity" : "gzipswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/1024jp/GzipSwift", + "state" : { + "revision" : "731037f6cc2be2ec01562f6597c1d0aa3fe6fd05", + "version" : "6.0.1" + } + }, + { + "identity" : "jinja", + "kind" : "remoteSourceControl", + "location" : "https://github.com/maiqingqiang/Jinja", + "state" : { + "revision" : "b435eb62b0d3d5f34167ec70a128355486981712", + "version" : "1.0.5" + } + }, + { + "identity" : "luminare", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MrKai77/Luminare.git", + "state" : { + "branch" : "main", + "revision" : "5300c9888823feccfb824ac313965536686b4352" + } + }, + { + "identity" : "mlx-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ml-explore/mlx-swift", + "state" : { + "revision" : "78a7cfe6701d6e9c88e9d4a0d1f7990af84b2146", + "version" : "0.18.0" + } + }, + { + "identity" : "mlx-swift-examples", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ml-explore/mlx-swift-examples.git", + "state" : { + "branch" : "main", + "revision" : "caa5caf4ca64e79c3ad8f64e2a49f9b85ef1bc19" + } + }, + { + "identity" : "networkimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/NetworkImage", + "state" : { + "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", + "version" : "6.0.0" + } + }, + { + "identity" : "splash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/johnsundell/splash.git", + "state" : { + "revision" : "7f4df436eb78fe64fe2c32c58006e9949fa28ad8", + "version" : "0.16.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", + "version" : "1.6.1" + } + }, + { + "identity" : "swift-markdown-ui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gonzalezreal/swift-markdown-ui.git", + "state" : { + "branch" : "main", + "revision" : "55441810c0f678c78ed7e2ebd46dde89228e02fc" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-transformers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/huggingface/swift-transformers", + "state" : { + "revision" : "4d25d20e49d2269aec1556231f8e278db7b2a4f0", + "version" : "0.1.13" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/swiftui-introspect.git", + "state" : { + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" + } + } + ], + "version" : 2 +} diff --git a/ChatMLXCore/Package.swift b/ChatMLXCore/Package.swift new file mode 100644 index 0000000..6b22f47 --- /dev/null +++ b/ChatMLXCore/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "ChatMLXCore", + platforms: [.macOS(.v14)], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "ChatMLXCore", + targets: ["ChatMLXCore"] + ), + .library( + name: "Utilities", + targets: ["Utilities"] + ), + ], + dependencies: [ + .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.1"), + .package(url: "https://github.com/elai950/AlertToast.git", from: "1.3.9"), + .package(url: "https://github.com/buh/CompactSlider.git", from: "1.1.6"), + .package(url: "https://github.com/sindresorhus/Defaults.git", branch: "main"), + .package(url: "https://github.com/MrKai77/Luminare.git", branch: "main"), + .package(url: "https://github.com/ml-explore/mlx-swift-examples.git", branch: "main"), + .package(url: "https://github.com/johnsundell/splash.git", from: "0.16.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.6.1"), + .package(url: "https://github.com/gonzalezreal/swift-markdown-ui.git", branch: "main"), + .package(url: "https://github.com/siteline/swiftui-introspect.git", from: "1.3.0"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "ChatMLXCore", + dependencies: [ + .product(name: "Alamofire", package: "Alamofire"), + .product(name: "AlertToast", package: "AlertToast"), + .product(name: "CompactSlider", package: "CompactSlider"), + .product(name: "Defaults", package: "Defaults"), + .product(name: "Luminare", package: "Luminare"), + .product(name: "LLM", package: "mlx-swift-examples"), + .product(name: "Logging", package: "swift-log"), + .product(name: "MarkdownUI", package: "swift-markdown-ui"), + .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"), + ] + ), + .target( + name: "Utilities", + dependencies: [ + .product(name: "Logging", package: "swift-log"), + ] + ), + ] +) diff --git a/ChatMLXCore/Sources/ChatMLXCore/ChatMLXApp.swift b/ChatMLXCore/Sources/ChatMLXCore/ChatMLXApp.swift new file mode 100644 index 0000000..e5452d5 --- /dev/null +++ b/ChatMLXCore/Sources/ChatMLXCore/ChatMLXApp.swift @@ -0,0 +1,19 @@ +// +// ChatMLXApp.swift +// ChatMLXCore +// +// Created by John Mai on 2024/8/3. +// + +import Defaults +import SwiftUI + +@main +struct ChatMLXApp: App { + + var body: some Scene { + WindowGroup{ + Text("66") + } + } +} diff --git a/ChatMLXCore/Sources/ChatMLXCore/ChatMLXCore.swift b/ChatMLXCore/Sources/ChatMLXCore/ChatMLXCore.swift new file mode 100644 index 0000000..08b22b8 --- /dev/null +++ b/ChatMLXCore/Sources/ChatMLXCore/ChatMLXCore.swift @@ -0,0 +1,2 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book From 77d33d4e12edbebec1a932f51dbb1e6f90bc4c24 Mon Sep 17 00:00:00 2001 From: maiqingqiang <867409182@qq.com> Date: Sun, 3 Nov 2024 13:56:50 +0800 Subject: [PATCH 10/11] :zap: Update --- ChatMLX.xcodeproj/project.pbxproj | 568 +++++++++++++++--- .../xcshareddata/swiftpm/Package.resolved | 42 +- .../xcshareddata/xcschemes/ChatMLX.xcscheme | 16 + .../xcschemes/ChatMLXTests.xcscheme | 55 ++ .../xcschemes/xcschememanagement.plist | 12 +- ChatMLX/Application/ChatMLXApp.swift | 139 +++++ .../AccentColor.colorset/Contents.json | 38 -- ChatMLX/ChatMLXApp.swift | 70 --- .../AppleIntelligenceEffect/ColorWheel.metal | 25 - .../UltramanMinimalistWindowModifier.swift | 69 --- .../ChatMLX.entitlements} | 6 +- .../ChatMLX.xcdatamodeld/.xccurrentversion} | 8 +- .../ChatMLX 2.xcdatamodel/contents | 45 ++ .../ChatMLX.xcdatamodel/contents | 5 +- .../AnimatedGradientFill.metal | 14 + .../AppleIntelligenceEffectController.swift | 6 +- .../AppleIntelligenceEffectManager.swift | 2 +- .../AppleIntelligenceEffectView.swift | 32 +- .../Core/Components/DefaultModelPicker.swift | 48 ++ .../Components/DefaultProviderPicker.swift | 28 + .../{ => Core}/Components/EffectView.swift | 0 .../Components/ErrorAlertModifier.swift | 0 ChatMLX/Core/Components/LabeledToggle.swift | 22 + ChatMLX/Core/Components/ModelPicker.swift | 69 +++ .../Components/NoneInteractWindow.swift | 0 .../Components}/ProviderView.swift | 4 +- .../SplashCodeSyntaxHighlighter.swift | 0 .../SyntaxHighlighter/TextOutputFormat.swift | 0 .../UltramanMinimalistWindowModifier.swift | 67 +++ .../UltramanNavigationSplitView.swift | 11 +- .../Components/UltramanSecureField.swift | 0 .../UltramanSidebarButtonStyle.swift | 0 .../UltramanTextEditorWithPlaceholder.swift | 0 .../Components/UltramanTextField.swift | 0 .../Components/UltramanWindow.swift | 0 ChatMLX/Core/Constants.swift | 10 + ChatMLX/Core/Errors/ErrorView.swift | 40 ++ ChatMLX/Core/Errors/ErrorWrapper.swift | 20 + ChatMLX/Core/Errors/Errors.swift | 19 + .../Extensions/Binding+Extensions.swift | 4 +- .../Core/Extensions/Bundle+Extensions.swift | 26 + .../Extensions/Color+Extensions.swift | 0 .../Extensions/Date+Extensions.swift | 0 .../Extensions/Defaults+Extensions.swift | 14 +- .../LabeledContentStyle+Extensions.swift | 6 +- .../MarkdownUI+Theme+Extensions.swift | 0 .../NSManagedObjectContext+Extensions.swift | 23 + .../Core/Extensions/NSWindow+Extensions.swift | 43 ++ .../Extensions/String+Extensions.swift | 0 .../Extensions/TimeInterval+Extensions.swift | 0 .../Extensions/View+Extensions.swift | 2 +- .../AppleIntelligenceEffectDisplay.swift | 0 .../Conversation+CoreDataClass.swift | 6 +- .../Conversation+CoreDataProperties.swift | 74 +++ .../Models/CoreData Models/Conversation.swift | 97 +++ .../Message+CoreDataClass.swift | 13 + .../Message+CoreDataProperties.swift | 26 + .../Models/CoreData Models/Message.swift} | 26 +- .../ModelInfo+CoreDataClass.swift | 13 + .../ModelInfo+CoreDataProperties.swift | 43 ++ .../Models/CoreData Models/ModelInfo.swift | 15 + ChatMLX/{ => Core}/Models/DisplayStyle.swift | 0 ChatMLX/{ => Core}/Models/DownloadTask.swift | 4 +- ChatMLX/{ => Core}/Models/Language.swift | 0 ChatMLX/{ => Core}/Models/LocalModel.swift | 0 .../{ => Core}/Models/LocalModelGroup.swift | 0 .../Models/Migrations/V2MigrationPolicy.swift | 30 + ChatMLX/Core/Models/ModelType.swift | 29 + ChatMLX/Core/Models/Provider.swift | 12 + ChatMLX/Core/Models/ProviderModel.swift | 56 ++ ChatMLX/{ => Core}/Models/RemoteModel.swift | 0 ChatMLX/{ => Core}/Models/Role.swift | 2 +- ChatMLX/{ => Core}/Models/SettingsTab.swift | 0 .../{ => Core}/Models/SettingsTabGroup.swift | 0 ChatMLX/{ => Core}/Models/Styles.swift | 0 ChatMLX/Core/Services/AI/BaseProvider.swift | 71 +++ ChatMLX/Core/Services/AI/MLXProvider.swift | 157 +++++ ChatMLX/Core/Services/AI/OpenAIProvider.swift | 108 ++++ .../Core/Services/AI/ProviderFactory.swift | 45 ++ ChatMLX/Core/Utilities/Common.swift | 12 + .../Utilities/Huggingface/Downloader.swift | 0 .../Utilities/Huggingface/Hub.swift | 0 .../Utilities/Huggingface/HubApi.swift | 0 ChatMLX/{ => Core}/Utilities/LLMRunner.swift | 64 +- ChatMLX/{ => Core}/Utilities/Logger.swift | 4 +- ChatMLX/Core/Utilities/MarkdownMetadata.swift | 39 ++ .../Utilities/PersistenceController.swift | 131 ++++ ChatMLX/Extensions/NSWindow+Extensions.swift | 56 -- .../Conversation/ConversationDetailView.swift | 203 +++---- .../ConversationSidebarItem.swift | 9 +- .../ConversationSidebarView.swift | 4 - .../Conversation/ConversationView.swift | 1 - .../Conversation/MessageBubbleView.swift | 57 +- .../Conversation/RightSidebarView.swift | 2 +- ChatMLX/Features/Settings/AboutView.swift | 2 +- .../Settings/DefaultConversationView.swift | 44 +- .../DownloadManagerView.swift | 0 .../DownloadTaskView.swift | 0 .../Settings/ExperimentalFeaturesView.swift | 2 - ChatMLX/Features/Settings/GeneralView.swift | 39 +- .../Features/Settings/HuggingFaceView.swift | 5 - .../LocalModelItemView.swift | 0 .../Local Models/LocalModelsView.swift | 136 +++++ .../LocalModels/LocalModelsView.swift | 136 ----- .../MLXCommunityItemView.swift | 0 .../MLXCommunityView.swift | 5 +- .../ModelManagerView.swift | 6 +- .../Providers/MLX/GPUMemorySettingsView.swift | 42 ++ .../MLX/MLXProviderContentView.swift | 175 ++++++ .../Providers/MLX/MLXProviderLabelView.swift | 20 + .../MLX/MLXProviderModelItemView.swift | 43 ++ .../Providers/MLX/MLXProviderView.swift | 18 + .../OpenAI/OpenAIProviderView.swift} | 30 +- .../ModelManager/Providers/MLXProvider.swift | 120 ---- .../ConversationViewModel.swift | 3 + .../View Models/ModelManagerViewModel.swift | 36 ++ .../SettingsViewModel.swift | 3 + .../Conversation+CoreDataProperties.swift | 115 ---- .../Models/Message+CoreDataProperties.swift | 46 -- ChatMLX/Models/ModelConfig.swift | 19 - .../AccentColor.colorset/Contents.json | 20 + .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/128.png | Bin .../Assets.xcassets/AppIcon.appiconset/16.png | Bin .../AppIcon.appiconset/256 1.png | Bin .../AppIcon.appiconset/256.png | Bin .../AppIcon.appiconset/32 1.png | Bin .../Assets.xcassets/AppIcon.appiconset/32.png | Bin .../AppIcon.appiconset/512 1.png | Bin .../AppIcon.appiconset/512.png | Bin .../Assets.xcassets/AppIcon.appiconset/64.png | Bin .../AppIcon.appiconset/Contents.json | 0 .../AppLogo.imageset/Contents.json | 0 .../Assets.xcassets/AppLogo.imageset/logo.png | Bin .../Assets.xcassets/Contents.json | 0 .../MLX.imageset/1028322432.png | Bin .../MLX.imageset/Contents.json | 0 .../MLX2.imageset/Contents.json | 0 .../Assets.xcassets/MLX2.imageset/MLX2.png | Bin .../clear.imageset/Contents.json | 0 .../clear.imageset/clear-l.svg | 0 .../huggingface.imageset/Contents.json | 0 .../huggingface.imageset/hf-logo-pirate.svg | 0 .../markdown.imageset/Contents.json | 0 .../markdown.imageset/markdown (2).svg | 0 .../menubarIcon.imageset/Contents.json | 23 + .../menubarIcon.imageset/menubarIcon@1x.png | Bin 0 -> 3433 bytes .../menubarIcon.imageset/menubarIcon@2x.png | Bin 0 -> 4168 bytes .../menubarIcon.imageset/menubarIcon@3x.png | Bin 0 -> 4747 bytes .../openai-lockup.imageset/Contents.json | 0 .../openai-lockup.imageset/openai-lockup.svg | 0 .../openai-logomark.imageset/Contents.json | 0 .../openai-logomark.svg | 0 .../Contents.json | 0 .../openai-white-lockup.svg | 0 .../Contents.json | 0 .../openai-white-logomark.svg | 0 .../plaintext.imageset/Contents.json | 0 .../plaintext.imageset/doc-plaintext (1).svg | 0 ChatMLX/{ => Resources}/Localizable.xcstrings | 94 +-- ChatMLX/Utilities/PersistenceController.swift | 74 --- ChatMLXTests/ChatMLXTests.swift | 16 + ChatMLXTests/MarkdownMetadataTests.swift | 29 + ChatMLXTests/ProviderFactoryTests.swift | 16 + images/Logo.psd | Bin 0 -> 2816906 bytes 165 files changed, 3126 insertions(+), 1208 deletions(-) create mode 100644 ChatMLX.xcodeproj/xcshareddata/xcschemes/ChatMLXTests.xcscheme create mode 100644 ChatMLX/Application/ChatMLXApp.swift delete mode 100644 ChatMLX/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 ChatMLX/ChatMLXApp.swift delete mode 100644 ChatMLX/Components/AppleIntelligenceEffect/ColorWheel.metal delete mode 100644 ChatMLX/Components/UltramanMinimalistWindowModifier.swift rename ChatMLX/{ChatMLXRelease.entitlements => Configuration/ChatMLX.entitlements} (84%) rename ChatMLX/{ChatMLX.entitlements => Configuration/ChatMLX.xcdatamodeld/.xccurrentversion} (51%) create mode 100644 ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX 2.xcdatamodel/contents rename ChatMLX/{Models => Configuration}/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents (92%) create mode 100644 ChatMLX/Core/Components/AppleIntelligenceEffect/AnimatedGradientFill.metal rename ChatMLX/{ => Core}/Components/AppleIntelligenceEffect/AppleIntelligenceEffectController.swift (94%) rename ChatMLX/{ => Core}/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift (95%) rename ChatMLX/{ => Core}/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift (62%) create mode 100644 ChatMLX/Core/Components/DefaultModelPicker.swift create mode 100644 ChatMLX/Core/Components/DefaultProviderPicker.swift rename ChatMLX/{ => Core}/Components/EffectView.swift (100%) rename ChatMLX/{ => Core}/Components/ErrorAlertModifier.swift (100%) create mode 100644 ChatMLX/Core/Components/LabeledToggle.swift create mode 100644 ChatMLX/Core/Components/ModelPicker.swift rename ChatMLX/{ => Core}/Components/NoneInteractWindow.swift (100%) rename ChatMLX/{Features/Settings/ModelManager/Providers => Core/Components}/ProviderView.swift (94%) rename ChatMLX/{ => Core}/Components/SyntaxHighlighter/SplashCodeSyntaxHighlighter.swift (100%) rename ChatMLX/{ => Core}/Components/SyntaxHighlighter/TextOutputFormat.swift (100%) create mode 100644 ChatMLX/Core/Components/UltramanMinimalistWindowModifier.swift rename ChatMLX/{ => Core}/Components/UltramanNavigationSplitView.swift (93%) rename ChatMLX/{ => Core}/Components/UltramanSecureField.swift (100%) rename ChatMLX/{ => Core}/Components/UltramanSidebarButtonStyle.swift (100%) rename ChatMLX/{ => Core}/Components/UltramanTextEditorWithPlaceholder.swift (100%) rename ChatMLX/{ => Core}/Components/UltramanTextField.swift (100%) rename ChatMLX/{ => Core}/Components/UltramanWindow.swift (100%) create mode 100644 ChatMLX/Core/Constants.swift create mode 100644 ChatMLX/Core/Errors/ErrorView.swift create mode 100644 ChatMLX/Core/Errors/ErrorWrapper.swift create mode 100644 ChatMLX/Core/Errors/Errors.swift rename ChatMLX/{ => Core}/Extensions/Binding+Extensions.swift (78%) create mode 100644 ChatMLX/Core/Extensions/Bundle+Extensions.swift rename ChatMLX/{ => Core}/Extensions/Color+Extensions.swift (100%) rename ChatMLX/{ => Core}/Extensions/Date+Extensions.swift (100%) rename ChatMLX/{ => Core}/Extensions/Defaults+Extensions.swift (79%) rename ChatMLX/{ => Core}/Extensions/LabeledContentStyle+Extensions.swift (87%) rename ChatMLX/{ => Core}/Extensions/MarkdownUI+Theme+Extensions.swift (100%) create mode 100644 ChatMLX/Core/Extensions/NSManagedObjectContext+Extensions.swift create mode 100644 ChatMLX/Core/Extensions/NSWindow+Extensions.swift rename ChatMLX/{ => Core}/Extensions/String+Extensions.swift (100%) rename ChatMLX/{ => Core}/Extensions/TimeInterval+Extensions.swift (100%) rename ChatMLX/{ => Core}/Extensions/View+Extensions.swift (95%) rename ChatMLX/{ => Core}/Models/AppleIntelligenceEffectDisplay.swift (100%) rename ChatMLX/{Models => Core/Models/CoreData Models}/Conversation+CoreDataClass.swift (59%) create mode 100644 ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataProperties.swift create mode 100644 ChatMLX/Core/Models/CoreData Models/Conversation.swift create mode 100644 ChatMLX/Core/Models/CoreData Models/Message+CoreDataClass.swift create mode 100644 ChatMLX/Core/Models/CoreData Models/Message+CoreDataProperties.swift rename ChatMLX/{Models/Message+CoreDataClass.swift => Core/Models/CoreData Models/Message.swift} (63%) create mode 100644 ChatMLX/Core/Models/CoreData Models/ModelInfo+CoreDataClass.swift create mode 100644 ChatMLX/Core/Models/CoreData Models/ModelInfo+CoreDataProperties.swift create mode 100644 ChatMLX/Core/Models/CoreData Models/ModelInfo.swift rename ChatMLX/{ => Core}/Models/DisplayStyle.swift (100%) rename ChatMLX/{ => Core}/Models/DownloadTask.swift (96%) rename ChatMLX/{ => Core}/Models/Language.swift (100%) rename ChatMLX/{ => Core}/Models/LocalModel.swift (100%) rename ChatMLX/{ => Core}/Models/LocalModelGroup.swift (100%) create mode 100644 ChatMLX/Core/Models/Migrations/V2MigrationPolicy.swift create mode 100644 ChatMLX/Core/Models/ModelType.swift create mode 100644 ChatMLX/Core/Models/Provider.swift create mode 100644 ChatMLX/Core/Models/ProviderModel.swift rename ChatMLX/{ => Core}/Models/RemoteModel.swift (100%) rename ChatMLX/{ => Core}/Models/Role.swift (84%) rename ChatMLX/{ => Core}/Models/SettingsTab.swift (100%) rename ChatMLX/{ => Core}/Models/SettingsTabGroup.swift (100%) rename ChatMLX/{ => Core}/Models/Styles.swift (100%) create mode 100644 ChatMLX/Core/Services/AI/BaseProvider.swift create mode 100644 ChatMLX/Core/Services/AI/MLXProvider.swift create mode 100644 ChatMLX/Core/Services/AI/OpenAIProvider.swift create mode 100644 ChatMLX/Core/Services/AI/ProviderFactory.swift create mode 100644 ChatMLX/Core/Utilities/Common.swift rename ChatMLX/{ => Core}/Utilities/Huggingface/Downloader.swift (100%) rename ChatMLX/{ => Core}/Utilities/Huggingface/Hub.swift (100%) rename ChatMLX/{ => Core}/Utilities/Huggingface/HubApi.swift (100%) rename ChatMLX/{ => Core}/Utilities/LLMRunner.swift (79%) rename ChatMLX/{ => Core}/Utilities/Logger.swift (56%) create mode 100644 ChatMLX/Core/Utilities/MarkdownMetadata.swift create mode 100644 ChatMLX/Core/Utilities/PersistenceController.swift delete mode 100644 ChatMLX/Extensions/NSWindow+Extensions.swift rename ChatMLX/Features/Settings/{DownloadManager => Download Manager}/DownloadManagerView.swift (100%) rename ChatMLX/Features/Settings/{DownloadManager => Download Manager}/DownloadTaskView.swift (100%) rename ChatMLX/Features/Settings/{LocalModels => Local Models}/LocalModelItemView.swift (100%) create mode 100644 ChatMLX/Features/Settings/Local Models/LocalModelsView.swift delete mode 100644 ChatMLX/Features/Settings/LocalModels/LocalModelsView.swift rename ChatMLX/Features/Settings/{MLXCommunity => MLX Community}/MLXCommunityItemView.swift (100%) rename ChatMLX/Features/Settings/{MLXCommunity => MLX Community}/MLXCommunityView.swift (98%) rename ChatMLX/Features/Settings/{ModelManager => Model Manager}/ModelManagerView.swift (79%) create mode 100644 ChatMLX/Features/Settings/Model Manager/Providers/MLX/GPUMemorySettingsView.swift create mode 100644 ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderContentView.swift create mode 100644 ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderLabelView.swift create mode 100644 ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderModelItemView.swift create mode 100644 ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderView.swift rename ChatMLX/Features/Settings/{ModelManager/Providers/OpenAIProvider.swift => Model Manager/Providers/OpenAI/OpenAIProviderView.swift} (51%) delete mode 100644 ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift rename ChatMLX/Features/{Conversation => View Models}/ConversationViewModel.swift (87%) create mode 100644 ChatMLX/Features/View Models/ModelManagerViewModel.swift rename ChatMLX/Features/{Settings => View Models}/SettingsViewModel.swift (83%) delete mode 100644 ChatMLX/Models/Conversation+CoreDataProperties.swift delete mode 100644 ChatMLX/Models/Message+CoreDataProperties.swift delete mode 100644 ChatMLX/Models/ModelConfig.swift create mode 100644 ChatMLX/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/1024.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/128.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/16.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/256 1.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/256.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/32 1.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/32.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/512 1.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/512.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/64.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppLogo.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/AppLogo.imageset/logo.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/MLX.imageset/1028322432.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/MLX.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/MLX2.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/MLX2.imageset/MLX2.png (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/clear.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/clear.imageset/clear-l.svg (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/huggingface.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/huggingface.imageset/hf-logo-pirate.svg (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/markdown.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/markdown.imageset/markdown (2).svg (100%) create mode 100644 ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/Contents.json create mode 100644 ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/menubarIcon@1x.png create mode 100644 ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/menubarIcon@2x.png create mode 100644 ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/menubarIcon@3x.png rename ChatMLX/{ => Resources}/Assets.xcassets/openai-lockup.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/openai-logomark.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/openai-white-lockup.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/openai-white-logomark.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/plaintext.imageset/Contents.json (100%) rename ChatMLX/{ => Resources}/Assets.xcassets/plaintext.imageset/doc-plaintext (1).svg (100%) rename ChatMLX/{ => Resources}/Localizable.xcstrings (98%) delete mode 100644 ChatMLX/Utilities/PersistenceController.swift create mode 100644 ChatMLXTests/ChatMLXTests.swift create mode 100644 ChatMLXTests/MarkdownMetadataTests.swift create mode 100644 ChatMLXTests/ProviderFactoryTests.swift create mode 100644 images/Logo.psd diff --git a/ChatMLX.xcodeproj/project.pbxproj b/ChatMLX.xcodeproj/project.pbxproj index cecf7b0..259ca90 100644 --- a/ChatMLX.xcodeproj/project.pbxproj +++ b/ChatMLX.xcodeproj/project.pbxproj @@ -7,6 +7,20 @@ objects = { /* Begin PBXBuildFile section */ + 5201638B2CBC2D8D00666E82 /* V2MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201638A2CBC2D8C00666E82 /* V2MigrationPolicy.swift */; }; + 520163972CBD6B4B00666E82 /* Conversation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163962CBD6B4900666E82 /* Conversation.swift */; }; + 5201639B2CBD6BC400666E82 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639A2CBD6BC000666E82 /* Message.swift */; }; + 520163A22CBD6C4900666E82 /* Conversation+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639C2CBD6C4900666E82 /* Conversation+CoreDataClass.swift */; }; + 520163A32CBD6C4900666E82 /* Conversation+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639D2CBD6C4900666E82 /* Conversation+CoreDataProperties.swift */; }; + 520163A42CBD6C4900666E82 /* Message+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639E2CBD6C4900666E82 /* Message+CoreDataClass.swift */; }; + 520163A52CBD6C4900666E82 /* Message+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639F2CBD6C4900666E82 /* Message+CoreDataProperties.swift */; }; + 520163A62CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163A02CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift */; }; + 520163A72CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163A12CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift */; }; + 520163A92CBD6CCB00666E82 /* ModelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163A82CBD6CCA00666E82 /* ModelInfo.swift */; }; + 520163AB2CBD715900666E82 /* ProviderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163AA2CBD715900666E82 /* ProviderModel.swift */; }; + 520163AD2CBD876100666E82 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163AC2CBD875500666E82 /* Common.swift */; }; + 5207D8D42CC6846F008588FA /* Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207D8D32CC6846F008588FA /* Provider.swift */; }; + 5207D8D62CC68BAC008588FA /* DefaultProviderPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207D8D52CC68BAA008588FA /* DefaultProviderPicker.swift */; }; 523D8C512C9C7FCC0092791C /* Transformers in Frameworks */ = {isa = PBXBuildFile; productRef = 523D8C502C9C7FCC0092791C /* Transformers */; }; 526675462C85EDCB001EF113 /* ChatMLXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526675452C85EDCB001EF113 /* ChatMLXApp.swift */; }; 5266754D2C85EDCC001EF113 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5266754C2C85EDCC001EF113 /* Preview Assets.xcassets */; }; @@ -16,7 +30,6 @@ 5266756B2C85F0E8001EF113 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 5266756A2C85F0E8001EF113 /* Defaults */; }; 5266756E2C85F0FF001EF113 /* Luminare in Frameworks */ = {isa = PBXBuildFile; productRef = 5266756D2C85F0FF001EF113 /* Luminare */; }; 526675742C85F1F9001EF113 /* Splash in Frameworks */ = {isa = PBXBuildFile; productRef = 526675732C85F1F9001EF113 /* Splash */; }; - 526675772C85F26B001EF113 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 526675762C85F26B001EF113 /* Logging */; }; 5266757A2C85F487001EF113 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 526675792C85F487001EF113 /* MarkdownUI */; }; 5266757D2C85F54D001EF113 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = 5266757C2C85F54D001EF113 /* SwiftUIIntrospect */; }; 526676402C85F903001EF113 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 526675FD2C85F903001EF113 /* Assets.xcassets */; }; @@ -63,47 +76,67 @@ 5266766B2C85F903001EF113 /* Hub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676312C85F903001EF113 /* Hub.swift */; }; 5266766C2C85F903001EF113 /* HubApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676322C85F903001EF113 /* HubApi.swift */; }; 5266766D2C85F903001EF113 /* LLMRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676342C85F903001EF113 /* LLMRunner.swift */; }; - 5266766E2C85F903001EF113 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676352C85F903001EF113 /* Logger.swift */; }; 526676702C85F903001EF113 /* Defaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676382C85F903001EF113 /* Defaults+Extensions.swift */; }; 526676712C85F903001EF113 /* MarkdownUI+Theme+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676392C85F903001EF113 /* MarkdownUI+Theme+Extensions.swift */; }; 526676722C85F903001EF113 /* NSWindow+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266763A2C85F903001EF113 /* NSWindow+Extensions.swift */; }; 526676732C85F903001EF113 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266763B2C85F903001EF113 /* String+Extensions.swift */; }; 526676742C85F903001EF113 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266763C2C85F903001EF113 /* View+Extensions.swift */; }; 526676782C85F9DA001EF113 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 526676772C85F9DA001EF113 /* Localizable.xcstrings */; }; + 527821592CB817A300638477 /* ModelManagerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527821582CB8179700638477 /* ModelManagerViewModel.swift */; }; + 5278215B2CB81FEB00638477 /* MarkdownMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5278215A2CB81FEA00638477 /* MarkdownMetadata.swift */; }; + 527821A52CB821AC00638477 /* ChatMLXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527821A32CB821AC00638477 /* ChatMLXTests.swift */; }; + 527821A72CB821B800638477 /* MarkdownMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527821A62CB821B800638477 /* MarkdownMetadataTests.swift */; }; 527F48152C9EFD5D006AF9FA /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 527F48142C9EFD5D006AF9FA /* LLM */; }; 528D82262CABE19900163AAB /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D82252CABE19000163AAB /* Date+Extensions.swift */; }; 528D83192CAD491900163AAB /* ChatMLX.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 528D83172CAD491900163AAB /* ChatMLX.xcdatamodeld */; }; 528D831C2CAD49E600163AAB /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D831B2CAD49E600163AAB /* PersistenceController.swift */; }; - 528D83292CAD5C9100163AAB /* Conversation+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83252CAD5C9100163AAB /* Conversation+CoreDataClass.swift */; }; - 528D832A2CAD5C9100163AAB /* Conversation+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83262CAD5C9100163AAB /* Conversation+CoreDataProperties.swift */; }; - 528D832B2CAD5C9100163AAB /* Message+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83272CAD5C9100163AAB /* Message+CoreDataClass.swift */; }; - 528D832C2CAD5C9100163AAB /* Message+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83282CAD5C9100163AAB /* Message+CoreDataProperties.swift */; }; 528D83372CADB64600163AAB /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83362CADB64300163AAB /* ConversationViewModel.swift */; }; 528D83392CAE51EC00163AAB /* Role.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83382CAE51EC00163AAB /* Role.swift */; }; 528DBE2F2C9C86FB004CDD88 /* Transformers in Frameworks */ = {isa = PBXBuildFile; productRef = 528DBE2E2C9C86FB004CDD88 /* Transformers */; }; + 52977D2B2CCBCCEC00DD4D2F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977D2A2CCBCCEC00DD4D2F /* Logger.swift */; }; + 52977E102CCCF22E00DD4D2F /* MLXProviderLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E0F2CCCF21E00DD4D2F /* MLXProviderLabelView.swift */; }; + 52977E122CCCF81300DD4D2F /* MLXProviderContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E112CCCF7FE00DD4D2F /* MLXProviderContentView.swift */; }; + 52977E142CCCFF4100DD4D2F /* MLXProviderModelItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E132CCCFF3A00DD4D2F /* MLXProviderModelItemView.swift */; }; + 52977E162CCD0F7800DD4D2F /* LabeledToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E152CCD0F6D00DD4D2F /* LabeledToggle.swift */; }; + 52977E182CCD1AC000DD4D2F /* GPUMemorySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E172CCD1AB800DD4D2F /* GPUMemorySettingsView.swift */; }; + 52977E1B2CCD296F00DD4D2F /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E1A2CCD296B00DD4D2F /* ErrorView.swift */; }; + 52977E1D2CCD2F1900DD4D2F /* ErrorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E1C2CCD2F1800DD4D2F /* ErrorWrapper.swift */; }; + 52977E202CCD4D3400DD4D2F /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E1F2CCD4D3000DD4D2F /* Errors.swift */; }; + 52977E242CCDF43E00DD4D2F /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977E232CCDF43C00DD4D2F /* Constants.swift */; }; + 52A021CF2CB565FC007893F7 /* AnimatedGradientFill.metal in Sources */ = {isa = PBXBuildFile; fileRef = 52A021CE2CB565FC007893F7 /* AnimatedGradientFill.metal */; }; + 52A265F02CBAE63E004F6DD3 /* BaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A265EF2CBAE638004F6DD3 /* BaseProvider.swift */; }; + 52A265F32CBAEA9E004F6DD3 /* MLXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A265F22CBAEA97004F6DD3 /* MLXProvider.swift */; }; + 52A265F52CBBB3C0004F6DD3 /* ProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A265F42CBBB3B8004F6DD3 /* ProviderFactory.swift */; }; + 52A265FA2CBBC5D5004F6DD3 /* ProviderFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A265F92CBBC5D5004F6DD3 /* ProviderFactoryTests.swift */; }; 52A689F62CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */; }; 52A689F82CAE8DA30078CDF9 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */; }; 52A689FA2CAECFE00078CDF9 /* ErrorAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */; }; - 52B053272CAFD6DB00E8DDBA /* ModelConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053262CAFD6D400E8DDBA /* ModelConfig.swift */; }; 52B0532A2CAFDFB000E8DDBA /* ProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */; }; 52B0532C2CAFEFD800E8DDBA /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0532B2CAFEFD000E8DDBA /* Color+Extensions.swift */; }; 52B0534D2CB02C4600E8DDBA /* ModelManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */; }; - 52B053502CB02CF900E8DDBA /* MLXProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0534F2CB02CF100E8DDBA /* MLXProvider.swift */; }; - 52B053522CB0385800E8DDBA /* OpenAIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053512CB0384E00E8DDBA /* OpenAIProvider.swift */; }; + 52B053502CB02CF900E8DDBA /* MLXProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0534F2CB02CF100E8DDBA /* MLXProviderView.swift */; }; + 52B053522CB0385800E8DDBA /* OpenAIProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053512CB0384E00E8DDBA /* OpenAIProviderView.swift */; }; 52B053542CB0F5B300E8DDBA /* LabeledContentStyle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053532CB0F5AA00E8DDBA /* LabeledContentStyle+Extensions.swift */; }; 52B053572CB0F8EE00E8DDBA /* Binding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053562CB0F8EE00E8DDBA /* Binding+Extensions.swift */; }; - 52B053932CB2B0D500E8DDBA /* ColorWheel.metal in Sources */ = {isa = PBXBuildFile; fileRef = 52B053922CB2B0D300E8DDBA /* ColorWheel.metal */; }; 52B053952CB2B64500E8DDBA /* NoneInteractWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053942CB2B64500E8DDBA /* NoneInteractWindow.swift */; }; 52B0539D2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0539A2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift */; }; 52B0539E2CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053992CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift */; }; 52B053A02CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0539F2CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift */; }; 52B053A22CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */; }; 52B053A62CB38E8700E8DDBA /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */; }; + 52B9BD072CBE94BA0086C013 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B9BD062CBE94B80086C013 /* Bundle+Extensions.swift */; }; + 52CDC3D22CC0208100627147 /* OpenAIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CDC3D12CC0207300627147 /* OpenAIProvider.swift */; }; + 52CDC3D52CC020F500627147 /* OpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 52CDC3D42CC020F500627147 /* OpenAI */; }; + 52CDC3DA2CC03B4A00627147 /* CoreDataEvolution in Frameworks */ = {isa = PBXBuildFile; productRef = 52CDC3D92CC03B4A00627147 /* CoreDataEvolution */; }; 52E50B1D2C8D6E81005A89DE /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B1C2C8D6E81005A89DE /* LLM */; }; 52E50B202C8D719B005A89DE /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B1F2C8D719B005A89DE /* LLM */; }; 52E50B222C8D719B005A89DE /* MNIST in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B212C8D719B005A89DE /* MNIST */; }; 52E50B292C8DF111005A89DE /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B282C8DF111005A89DE /* LLM */; }; 52E50B2B2C8DF111005A89DE /* MNIST in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B2A2C8DF111005A89DE /* MNIST */; }; + 52EC64942CC3DC0E00A08069 /* NSManagedObjectContext+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EC64932CC3DC0800A08069 /* NSManagedObjectContext+Extensions.swift */; }; + 52EC64AD2CC4C38600A08069 /* ModelPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EC64AC2CC4C37C00A08069 /* ModelPicker.swift */; }; + 52EC64AF2CC4CD5400A08069 /* DefaultModelPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EC64AE2CC4CD4E00A08069 /* DefaultModelPicker.swift */; }; + 52EC64B12CC531A300A08069 /* ModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EC64B02CC531A300A08069 /* ModelType.swift */; }; 52FD80BE2C8F21C2006C50F1 /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52FD80BD2C8F21C2006C50F1 /* LLM */; }; 52FD80C02C8F21C2006C50F1 /* MNIST in Frameworks */ = {isa = PBXBuildFile; productRef = 52FD80BF2C8F21C2006C50F1 /* MNIST */; }; 52FD80C32C8F288A006C50F1 /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52FD80C22C8F288A006C50F1 /* LLM */; }; @@ -112,7 +145,32 @@ 52FD80CA2C8F5E42006C50F1 /* MNIST in Frameworks */ = {isa = PBXBuildFile; productRef = 52FD80C92C8F5E42006C50F1 /* MNIST */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 5278219E2CB821A600638477 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5266753A2C85EDCB001EF113 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 526675412C85EDCB001EF113; + remoteInfo = ChatMLX; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 520163832CBC1C9F00666E82 /* ChatMLX 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "ChatMLX 2.xcdatamodel"; sourceTree = ""; }; + 5201638A2CBC2D8C00666E82 /* V2MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2MigrationPolicy.swift; sourceTree = ""; }; + 520163962CBD6B4900666E82 /* Conversation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conversation.swift; sourceTree = ""; }; + 5201639A2CBD6BC000666E82 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + 5201639C2CBD6C4900666E82 /* Conversation+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataClass.swift"; sourceTree = ""; }; + 5201639D2CBD6C4900666E82 /* Conversation+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataProperties.swift"; sourceTree = ""; }; + 5201639E2CBD6C4900666E82 /* Message+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataClass.swift"; sourceTree = ""; }; + 5201639F2CBD6C4900666E82 /* Message+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataProperties.swift"; sourceTree = ""; }; + 520163A02CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelInfo+CoreDataClass.swift"; sourceTree = ""; }; + 520163A12CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelInfo+CoreDataProperties.swift"; sourceTree = ""; }; + 520163A82CBD6CCA00666E82 /* ModelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelInfo.swift; sourceTree = ""; }; + 520163AA2CBD715900666E82 /* ProviderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderModel.swift; sourceTree = ""; }; + 520163AC2CBD875500666E82 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; + 5207D8D32CC6846F008588FA /* Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Provider.swift; sourceTree = ""; }; + 5207D8D52CC68BAA008588FA /* DefaultProviderPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultProviderPicker.swift; sourceTree = ""; }; 526675422C85EDCB001EF113 /* ChatMLX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatMLX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 526675452C85EDCB001EF113 /* ChatMLXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMLXApp.swift; sourceTree = ""; }; 5266754C2C85EDCC001EF113 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -161,41 +219,59 @@ 526676312C85F903001EF113 /* Hub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hub.swift; sourceTree = ""; }; 526676322C85F903001EF113 /* HubApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HubApi.swift; sourceTree = ""; }; 526676342C85F903001EF113 /* LLMRunner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMRunner.swift; sourceTree = ""; }; - 526676352C85F903001EF113 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 526676382C85F903001EF113 /* Defaults+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Defaults+Extensions.swift"; sourceTree = ""; }; 526676392C85F903001EF113 /* MarkdownUI+Theme+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MarkdownUI+Theme+Extensions.swift"; sourceTree = ""; }; 5266763A2C85F903001EF113 /* NSWindow+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extensions.swift"; sourceTree = ""; }; 5266763B2C85F903001EF113 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 5266763C2C85F903001EF113 /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; - 526676762C85F952001EF113 /* ChatMLXRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ChatMLXRelease.entitlements; sourceTree = ""; }; 526676772C85F9DA001EF113 /* Localizable.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 527821582CB8179700638477 /* ModelManagerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManagerViewModel.swift; sourceTree = ""; }; + 5278215A2CB81FEA00638477 /* MarkdownMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownMetadata.swift; sourceTree = ""; }; + 5278219A2CB821A600638477 /* ChatMLXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatMLXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 527821A32CB821AC00638477 /* ChatMLXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMLXTests.swift; sourceTree = ""; }; + 527821A62CB821B800638477 /* MarkdownMetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownMetadataTests.swift; sourceTree = ""; }; 528D82252CABE19000163AAB /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; 528D83182CAD491900163AAB /* ChatMLX.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ChatMLX.xcdatamodel; sourceTree = ""; }; 528D831B2CAD49E600163AAB /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; - 528D83252CAD5C9100163AAB /* Conversation+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataClass.swift"; sourceTree = ""; }; - 528D83262CAD5C9100163AAB /* Conversation+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataProperties.swift"; sourceTree = ""; }; - 528D83272CAD5C9100163AAB /* Message+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataClass.swift"; sourceTree = ""; }; - 528D83282CAD5C9100163AAB /* Message+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataProperties.swift"; sourceTree = ""; }; 528D83362CADB64300163AAB /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = ""; }; 528D83382CAE51EC00163AAB /* Role.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Role.swift; sourceTree = ""; }; + 52977D2A2CCBCCEC00DD4D2F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 52977E0F2CCCF21E00DD4D2F /* MLXProviderLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXProviderLabelView.swift; sourceTree = ""; }; + 52977E112CCCF7FE00DD4D2F /* MLXProviderContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXProviderContentView.swift; sourceTree = ""; }; + 52977E132CCCFF3A00DD4D2F /* MLXProviderModelItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXProviderModelItemView.swift; sourceTree = ""; }; + 52977E152CCD0F6D00DD4D2F /* LabeledToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabeledToggle.swift; sourceTree = ""; }; + 52977E172CCD1AB800DD4D2F /* GPUMemorySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPUMemorySettingsView.swift; sourceTree = ""; }; + 52977E1A2CCD296B00DD4D2F /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + 52977E1C2CCD2F1800DD4D2F /* ErrorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorWrapper.swift; sourceTree = ""; }; + 52977E1F2CCD4D3000DD4D2F /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; + 52977E232CCDF43C00DD4D2F /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 52A021CE2CB565FC007893F7 /* AnimatedGradientFill.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = AnimatedGradientFill.metal; sourceTree = ""; }; + 52A265EF2CBAE638004F6DD3 /* BaseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseProvider.swift; sourceTree = ""; }; + 52A265F22CBAEA97004F6DD3 /* MLXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXProvider.swift; sourceTree = ""; }; + 52A265F42CBBB3B8004F6DD3 /* ProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderFactory.swift; sourceTree = ""; }; + 52A265F92CBBC5D5004F6DD3 /* ProviderFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderFactoryTests.swift; sourceTree = ""; }; 52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertModifier.swift; sourceTree = ""; }; - 52B053262CAFD6D400E8DDBA /* ModelConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelConfig.swift; sourceTree = ""; }; 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderView.swift; sourceTree = ""; }; 52B0532B2CAFEFD000E8DDBA /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManagerView.swift; sourceTree = ""; }; - 52B0534F2CB02CF100E8DDBA /* MLXProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXProvider.swift; sourceTree = ""; }; - 52B053512CB0384E00E8DDBA /* OpenAIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIProvider.swift; sourceTree = ""; }; + 52B0534F2CB02CF100E8DDBA /* MLXProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXProviderView.swift; sourceTree = ""; }; + 52B053512CB0384E00E8DDBA /* OpenAIProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIProviderView.swift; sourceTree = ""; }; 52B053532CB0F5AA00E8DDBA /* LabeledContentStyle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LabeledContentStyle+Extensions.swift"; sourceTree = ""; }; 52B053562CB0F8EE00E8DDBA /* Binding+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Extensions.swift"; sourceTree = ""; }; - 52B053922CB2B0D300E8DDBA /* ColorWheel.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ColorWheel.metal; sourceTree = ""; }; 52B053942CB2B64500E8DDBA /* NoneInteractWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoneInteractWindow.swift; sourceTree = ""; }; 52B053992CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectController.swift; sourceTree = ""; }; 52B0539A2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectView.swift; sourceTree = ""; }; 52B0539F2CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectManager.swift; sourceTree = ""; }; 52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectDisplay.swift; sourceTree = ""; }; 52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesView.swift; sourceTree = ""; }; + 52B9BD062CBE94B80086C013 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; + 52CDC3D12CC0207300627147 /* OpenAIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIProvider.swift; sourceTree = ""; }; + 52EC64932CC3DC0800A08069 /* NSManagedObjectContext+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Extensions.swift"; sourceTree = ""; }; + 52EC64AC2CC4C37C00A08069 /* ModelPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelPicker.swift; sourceTree = ""; }; + 52EC64AE2CC4CD4E00A08069 /* DefaultModelPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultModelPicker.swift; sourceTree = ""; }; + 52EC64B02CC531A300A08069 /* ModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelType.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -213,29 +289,55 @@ 52FD80BE2C8F21C2006C50F1 /* LLM in Frameworks */, 5266757D2C85F54D001EF113 /* SwiftUIIntrospect in Frameworks */, 52FD80C52C8F288A006C50F1 /* MNIST in Frameworks */, + 52CDC3D52CC020F500627147 /* OpenAI in Frameworks */, 52E50B2B2C8DF111005A89DE /* MNIST in Frameworks */, 5266757A2C85F487001EF113 /* MarkdownUI in Frameworks */, 526675742C85F1F9001EF113 /* Splash in Frameworks */, 52FD80C82C8F5E42006C50F1 /* LLM in Frameworks */, 526675682C85EFDF001EF113 /* CompactSlider in Frameworks */, 5266756E2C85F0FF001EF113 /* Luminare in Frameworks */, - 526675772C85F26B001EF113 /* Logging in Frameworks */, 52E50B202C8D719B005A89DE /* LLM in Frameworks */, 52E50B1D2C8D6E81005A89DE /* LLM in Frameworks */, 528DBE2F2C9C86FB004CDD88 /* Transformers in Frameworks */, 526675652C85EFC7001EF113 /* AlertToast in Frameworks */, 52FD80C32C8F288A006C50F1 /* LLM in Frameworks */, + 52CDC3DA2CC03B4A00627147 /* CoreDataEvolution in Frameworks */, 52E50B292C8DF111005A89DE /* LLM in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; + 527821972CB821A600638477 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 5248415E2CD610B1006A38E5 /* Application */ = { + isa = PBXGroup; + children = ( + 526675452C85EDCB001EF113 /* ChatMLXApp.swift */, + ); + path = Application; + sourceTree = ""; + }; + 5248415F2CD61225006A38E5 /* Configuration */ = { + isa = PBXGroup; + children = ( + 5266754E2C85EDCC001EF113 /* ChatMLX.entitlements */, + 528D83172CAD491900163AAB /* ChatMLX.xcdatamodeld */, + ); + path = Configuration; + sourceTree = ""; + }; 526675392C85EDCB001EF113 = { isa = PBXGroup; children = ( 526675442C85EDCB001EF113 /* ChatMLX */, + 527821A42CB821AC00638477 /* ChatMLXTests */, 526675432C85EDCB001EF113 /* Products */, ); sourceTree = ""; @@ -244,6 +346,7 @@ isa = PBXGroup; children = ( 526675422C85EDCB001EF113 /* ChatMLX.app */, + 5278219A2CB821A600638477 /* ChatMLXTests.xctest */, ); name = Products; sourceTree = ""; @@ -251,17 +354,12 @@ 526675442C85EDCB001EF113 /* ChatMLX */ = { isa = PBXGroup; children = ( - 5266754E2C85EDCC001EF113 /* ChatMLX.entitlements */, - 526676762C85F952001EF113 /* ChatMLXRelease.entitlements */, - 526675452C85EDCB001EF113 /* ChatMLXApp.swift */, - 526675FD2C85F903001EF113 /* Assets.xcassets */, - 526676772C85F9DA001EF113 /* Localizable.xcstrings */, - 526676092C85F903001EF113 /* Components */, - 5266763D2C85F903001EF113 /* Extensions */, + 5248415E2CD610B1006A38E5 /* Application */, + 5248415F2CD61225006A38E5 /* Configuration */, + 52977E1E2CCD4D0000DD4D2F /* Core */, 526676232C85F903001EF113 /* Features */, - 5266762F2C85F903001EF113 /* Models */, 5266754B2C85EDCC001EF113 /* Preview Content */, - 526676372C85F903001EF113 /* Utilities */, + 52977D262CCBC89200DD4D2F /* Resources */, ); path = ChatMLX; sourceTree = ""; @@ -286,11 +384,14 @@ 526676092C85F903001EF113 /* Components */ = { isa = PBXGroup; children = ( - 52B053942CB2B64500E8DDBA /* NoneInteractWindow.swift */, - 52B0538B2CB2B03200E8DDBA /* AppleIntelligenceEffect */, - 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */, - 526676002C85F903001EF113 /* SyntaxHighlighter */, + 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */, + 52977E152CCD0F6D00DD4D2F /* LabeledToggle.swift */, + 52EC64AE2CC4CD4E00A08069 /* DefaultModelPicker.swift */, + 5207D8D52CC68BAA008588FA /* DefaultProviderPicker.swift */, 526676012C85F903001EF113 /* EffectView.swift */, + 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */, + 52EC64AC2CC4C37C00A08069 /* ModelPicker.swift */, + 52B053942CB2B64500E8DDBA /* NoneInteractWindow.swift */, 526676022C85F903001EF113 /* UltramanMinimalistWindowModifier.swift */, 526676032C85F903001EF113 /* UltramanNavigationSplitView.swift */, 526676042C85F903001EF113 /* UltramanSecureField.swift */, @@ -298,6 +399,8 @@ 526676062C85F903001EF113 /* UltramanTextEditorWithPlaceholder.swift */, 526676072C85F903001EF113 /* UltramanTextField.swift */, 526676082C85F903001EF113 /* UltramanWindow.swift */, + 52B0538B2CB2B03200E8DDBA /* AppleIntelligenceEffect */, + 526676002C85F903001EF113 /* SyntaxHighlighter */, ); path = Components; sourceTree = ""; @@ -305,7 +408,6 @@ 526676112C85F903001EF113 /* Conversation */ = { isa = PBXGroup; children = ( - 528D83362CADB64300163AAB /* ConversationViewModel.swift */, 5266760A2C85F903001EF113 /* ConversationDetailView.swift */, 5266760B2C85F903001EF113 /* ConversationSidebarItem.swift */, 5266760C2C85F903001EF113 /* ConversationSidebarView.swift */, @@ -317,49 +419,48 @@ path = Conversation; sourceTree = ""; }; - 526676142C85F903001EF113 /* DownloadManager */ = { + 526676142C85F903001EF113 /* Download Manager */ = { isa = PBXGroup; children = ( 526676122C85F903001EF113 /* DownloadManagerView.swift */, 526676132C85F903001EF113 /* DownloadTaskView.swift */, ); - path = DownloadManager; + path = "Download Manager"; sourceTree = ""; }; - 526676172C85F903001EF113 /* LocalModels */ = { + 526676172C85F903001EF113 /* Local Models */ = { isa = PBXGroup; children = ( 526676152C85F903001EF113 /* LocalModelItemView.swift */, 526676162C85F903001EF113 /* LocalModelsView.swift */, ); - path = LocalModels; + path = "Local Models"; sourceTree = ""; }; - 5266761A2C85F903001EF113 /* MLXCommunity */ = { + 5266761A2C85F903001EF113 /* MLX Community */ = { isa = PBXGroup; children = ( 526676182C85F903001EF113 /* MLXCommunityItemView.swift */, 526676192C85F903001EF113 /* MLXCommunityView.swift */, ); - path = MLXCommunity; + path = "MLX Community"; sourceTree = ""; }; 526676222C85F903001EF113 /* Settings */ = { isa = PBXGroup; children = ( - 52B053282CAFDF8500E8DDBA /* ModelManager */, - 52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */, - 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */, - 526676142C85F903001EF113 /* DownloadManager */, - 526676172C85F903001EF113 /* LocalModels */, - 5266761A2C85F903001EF113 /* MLXCommunity */, 5266761B2C85F903001EF113 /* AboutView.swift */, 5266761C2C85F903001EF113 /* DefaultConversationView.swift */, + 52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */, 5266761D2C85F903001EF113 /* GeneralView.swift */, 5266761E2C85F903001EF113 /* HuggingFaceView.swift */, 5266761F2C85F903001EF113 /* SettingsSidebarItemView.swift */, 526676202C85F903001EF113 /* SettingsSidebarView.swift */, 526676212C85F903001EF113 /* SettingsView.swift */, + 526676142C85F903001EF113 /* Download Manager */, + 526676172C85F903001EF113 /* Local Models */, + 5266761A2C85F903001EF113 /* MLX Community */, + 52B053282CAFDF8500E8DDBA /* Model Manager */, ); path = Settings; sourceTree = ""; @@ -367,6 +468,7 @@ 526676232C85F903001EF113 /* Features */ = { isa = PBXGroup; children = ( + 527821572CB8176200638477 /* View Models */, 526676112C85F903001EF113 /* Conversation */, 526676222C85F903001EF113 /* Settings */, ); @@ -376,23 +478,22 @@ 5266762F2C85F903001EF113 /* Models */ = { isa = PBXGroup; children = ( - 52B053262CAFD6D400E8DDBA /* ModelConfig.swift */, + 52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */, 526676252C85F903001EF113 /* DisplayStyle.swift */, 526676262C85F903001EF113 /* DownloadTask.swift */, 526676272C85F903001EF113 /* Language.swift */, - 52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */, + 52EC64B02CC531A300A08069 /* ModelType.swift */, + 5207D8D32CC6846F008588FA /* Provider.swift */, 526676282C85F903001EF113 /* LocalModel.swift */, 526676292C85F903001EF113 /* LocalModelGroup.swift */, - 528D83382CAE51EC00163AAB /* Role.swift */, + 520163AA2CBD715900666E82 /* ProviderModel.swift */, 5266762B2C85F903001EF113 /* RemoteModel.swift */, + 528D83382CAE51EC00163AAB /* Role.swift */, 5266762C2C85F903001EF113 /* SettingsTab.swift */, 5266762D2C85F903001EF113 /* SettingsTabGroup.swift */, 5266762E2C85F903001EF113 /* Styles.swift */, - 528D83252CAD5C9100163AAB /* Conversation+CoreDataClass.swift */, - 528D83262CAD5C9100163AAB /* Conversation+CoreDataProperties.swift */, - 528D83272CAD5C9100163AAB /* Message+CoreDataClass.swift */, - 528D83282CAD5C9100163AAB /* Message+CoreDataProperties.swift */, - 528D83172CAD491900163AAB /* ChatMLX.xcdatamodeld */, + 52B9BD092CBEB5960086C013 /* CoreData Models */, + 52B9BD082CBEB4E20086C013 /* Migrations */, ); path = Models; sourceTree = ""; @@ -410,8 +511,10 @@ 526676372C85F903001EF113 /* Utilities */ = { isa = PBXGroup; children = ( + 52977D2A2CCBCCEC00DD4D2F /* Logger.swift */, + 520163AC2CBD875500666E82 /* Common.swift */, + 5278215A2CB81FEA00638477 /* MarkdownMetadata.swift */, 526676342C85F903001EF113 /* LLMRunner.swift */, - 526676352C85F903001EF113 /* Logger.swift */, 528D831B2CAD49E600163AAB /* PersistenceController.swift */, 526676332C85F903001EF113 /* Huggingface */, ); @@ -421,6 +524,8 @@ 5266763D2C85F903001EF113 /* Extensions */ = { isa = PBXGroup; children = ( + 52B9BD062CBE94B80086C013 /* Bundle+Extensions.swift */, + 52EC64932CC3DC0800A08069 /* NSManagedObjectContext+Extensions.swift */, 52B053562CB0F8EE00E8DDBA /* Binding+Extensions.swift */, 52B053532CB0F5AA00E8DDBA /* LabeledContentStyle+Extensions.swift */, 52B0532B2CAFEFD000E8DDBA /* Color+Extensions.swift */, @@ -435,34 +540,149 @@ path = Extensions; sourceTree = ""; }; + 527821572CB8176200638477 /* View Models */ = { + isa = PBXGroup; + children = ( + 527821582CB8179700638477 /* ModelManagerViewModel.swift */, + 528D83362CADB64300163AAB /* ConversationViewModel.swift */, + 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */, + ); + path = "View Models"; + sourceTree = ""; + }; + 527821A42CB821AC00638477 /* ChatMLXTests */ = { + isa = PBXGroup; + children = ( + 527821A32CB821AC00638477 /* ChatMLXTests.swift */, + 527821A62CB821B800638477 /* MarkdownMetadataTests.swift */, + 52A265F92CBBC5D5004F6DD3 /* ProviderFactoryTests.swift */, + ); + path = ChatMLXTests; + sourceTree = ""; + }; + 52977D262CCBC89200DD4D2F /* Resources */ = { + isa = PBXGroup; + children = ( + 526675FD2C85F903001EF113 /* Assets.xcassets */, + 526676772C85F9DA001EF113 /* Localizable.xcstrings */, + ); + path = Resources; + sourceTree = ""; + }; + 52977E0E2CCCF20C00DD4D2F /* MLX */ = { + isa = PBXGroup; + children = ( + 52977E172CCD1AB800DD4D2F /* GPUMemorySettingsView.swift */, + 52977E132CCCFF3A00DD4D2F /* MLXProviderModelItemView.swift */, + 52977E112CCCF7FE00DD4D2F /* MLXProviderContentView.swift */, + 52977E0F2CCCF21E00DD4D2F /* MLXProviderLabelView.swift */, + 52B0534F2CB02CF100E8DDBA /* MLXProviderView.swift */, + ); + path = MLX; + sourceTree = ""; + }; + 52977E192CCD295800DD4D2F /* Errors */ = { + isa = PBXGroup; + children = ( + 52977E1F2CCD4D3000DD4D2F /* Errors.swift */, + 52977E1C2CCD2F1800DD4D2F /* ErrorWrapper.swift */, + 52977E1A2CCD296B00DD4D2F /* ErrorView.swift */, + ); + path = Errors; + sourceTree = ""; + }; + 52977E1E2CCD4D0000DD4D2F /* Core */ = { + isa = PBXGroup; + children = ( + 52977E232CCDF43C00DD4D2F /* Constants.swift */, + 52977E192CCD295800DD4D2F /* Errors */, + 526676092C85F903001EF113 /* Components */, + 5266763D2C85F903001EF113 /* Extensions */, + 5266762F2C85F903001EF113 /* Models */, + 52A265EE2CBAE54F004F6DD3 /* Services */, + 526676372C85F903001EF113 /* Utilities */, + ); + path = Core; + sourceTree = ""; + }; + 52977E212CCDE24900DD4D2F /* OpenAI */ = { + isa = PBXGroup; + children = ( + 52B053512CB0384E00E8DDBA /* OpenAIProviderView.swift */, + ); + path = OpenAI; + sourceTree = ""; + }; + 52A265EE2CBAE54F004F6DD3 /* Services */ = { + isa = PBXGroup; + children = ( + 52A265F12CBAEA89004F6DD3 /* AI */, + ); + path = Services; + sourceTree = ""; + }; + 52A265F12CBAEA89004F6DD3 /* AI */ = { + isa = PBXGroup; + children = ( + 52CDC3D12CC0207300627147 /* OpenAIProvider.swift */, + 52A265F42CBBB3B8004F6DD3 /* ProviderFactory.swift */, + 52A265F22CBAEA97004F6DD3 /* MLXProvider.swift */, + 52A265EF2CBAE638004F6DD3 /* BaseProvider.swift */, + ); + path = AI; + sourceTree = ""; + }; + 52B053282CAFDF8500E8DDBA /* Model Manager */ = { + isa = PBXGroup; + children = ( + 52B0534E2CB02CDF00E8DDBA /* Providers */, + 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */, + ); + path = "Model Manager"; + sourceTree = ""; + }; + 52B0534E2CB02CDF00E8DDBA /* Providers */ = { + isa = PBXGroup; + children = ( + 52977E212CCDE24900DD4D2F /* OpenAI */, + 52977E0E2CCCF20C00DD4D2F /* MLX */, + ); + path = Providers; + sourceTree = ""; + }; 52B0538B2CB2B03200E8DDBA /* AppleIntelligenceEffect */ = { isa = PBXGroup; children = ( 52B0539F2CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift */, + 52A021CE2CB565FC007893F7 /* AnimatedGradientFill.metal */, 52B053992CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift */, 52B0539A2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift */, - 52B053922CB2B0D300E8DDBA /* ColorWheel.metal */, ); path = AppleIntelligenceEffect; sourceTree = ""; }; - 52B053282CAFDF8500E8DDBA /* ModelManager */ = { + 52B9BD082CBEB4E20086C013 /* Migrations */ = { isa = PBXGroup; children = ( - 52B0534E2CB02CDF00E8DDBA /* Providers */, - 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */, + 5201638A2CBC2D8C00666E82 /* V2MigrationPolicy.swift */, ); - path = ModelManager; + path = Migrations; sourceTree = ""; }; - 52B0534E2CB02CDF00E8DDBA /* Providers */ = { + 52B9BD092CBEB5960086C013 /* CoreData Models */ = { isa = PBXGroup; children = ( - 52B053512CB0384E00E8DDBA /* OpenAIProvider.swift */, - 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */, - 52B0534F2CB02CF100E8DDBA /* MLXProvider.swift */, + 520163962CBD6B4900666E82 /* Conversation.swift */, + 5201639C2CBD6C4900666E82 /* Conversation+CoreDataClass.swift */, + 5201639D2CBD6C4900666E82 /* Conversation+CoreDataProperties.swift */, + 5201639A2CBD6BC000666E82 /* Message.swift */, + 5201639E2CBD6C4900666E82 /* Message+CoreDataClass.swift */, + 5201639F2CBD6C4900666E82 /* Message+CoreDataProperties.swift */, + 520163A82CBD6CCA00666E82 /* ModelInfo.swift */, + 520163A02CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift */, + 520163A12CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift */, ); - path = Providers; + path = "CoreData Models"; sourceTree = ""; }; /* End PBXGroup section */ @@ -488,7 +708,6 @@ 5266756A2C85F0E8001EF113 /* Defaults */, 5266756D2C85F0FF001EF113 /* Luminare */, 526675732C85F1F9001EF113 /* Splash */, - 526675762C85F26B001EF113 /* Logging */, 526675792C85F487001EF113 /* MarkdownUI */, 5266757C2C85F54D001EF113 /* SwiftUIIntrospect */, 52E50B1C2C8D6E81005A89DE /* LLM */, @@ -505,11 +724,33 @@ 523D8C502C9C7FCC0092791C /* Transformers */, 528DBE2E2C9C86FB004CDD88 /* Transformers */, 527F48142C9EFD5D006AF9FA /* LLM */, + 52CDC3D42CC020F500627147 /* OpenAI */, + 52CDC3D92CC03B4A00627147 /* CoreDataEvolution */, ); productName = ChatMLX; productReference = 526675422C85EDCB001EF113 /* ChatMLX.app */; productType = "com.apple.product-type.application"; }; + 527821992CB821A600638477 /* ChatMLXTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 527821A02CB821A600638477 /* Build configuration list for PBXNativeTarget "ChatMLXTests" */; + buildPhases = ( + 527821962CB821A600638477 /* Sources */, + 527821972CB821A600638477 /* Frameworks */, + 527821982CB821A600638477 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5278219F2CB821A600638477 /* PBXTargetDependency */, + ); + name = ChatMLXTests; + packageProductDependencies = ( + ); + productName = ChatMLXTests; + productReference = 5278219A2CB821A600638477 /* ChatMLXTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -517,12 +758,16 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1540; - LastUpgradeCheck = 1540; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; TargetAttributes = { 526675412C85EDCB001EF113 = { CreatedOnToolsVersion = 15.4; }; + 527821992CB821A600638477 = { + CreatedOnToolsVersion = 16.0; + TestTargetID = 526675412C85EDCB001EF113; + }; }; }; buildConfigurationList = 5266753D2C85EDCB001EF113 /* Build configuration list for PBXProject "ChatMLX" */; @@ -545,16 +790,18 @@ 526675692C85F0E8001EF113 /* XCRemoteSwiftPackageReference "Defaults" */, 5266756C2C85F0FF001EF113 /* XCRemoteSwiftPackageReference "Luminare" */, 526675722C85F1F9001EF113 /* XCRemoteSwiftPackageReference "splash" */, - 526675752C85F26B001EF113 /* XCRemoteSwiftPackageReference "swift-log" */, 526675782C85F486001EF113 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, 5266757B2C85F54D001EF113 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, 527F48132C9EFD5D006AF9FA /* XCRemoteSwiftPackageReference "mlx-swift-examples" */, + 52CDC3D32CC020F500627147 /* XCRemoteSwiftPackageReference "OpenAI" */, + 52CDC3D82CC03B4A00627147 /* XCRemoteSwiftPackageReference "CoreDataEvolution" */, ); productRefGroup = 526675432C85EDCB001EF113 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 526675412C85EDCB001EF113 /* ChatMLX */, + 527821992CB821A600638477 /* ChatMLXTests */, ); }; /* End PBXProject section */ @@ -570,6 +817,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 527821982CB821A600638477 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -577,17 +831,25 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 52977E1D2CCD2F1900DD4D2F /* ErrorWrapper.swift in Sources */, 526676422C85F903001EF113 /* TextOutputFormat.swift in Sources */, 526676512C85F903001EF113 /* RightSidebarView.swift in Sources */, + 52977E122CCCF81300DD4D2F /* MLXProviderContentView.swift in Sources */, 52B053A62CB38E8700E8DDBA /* ExperimentalFeaturesView.swift in Sources */, 526676682C85F903001EF113 /* SettingsTabGroup.swift in Sources */, 526676452C85F903001EF113 /* UltramanNavigationSplitView.swift in Sources */, 526676672C85F903001EF113 /* SettingsTab.swift in Sources */, - 52B053932CB2B0D500E8DDBA /* ColorWheel.metal in Sources */, 5266765C2C85F903001EF113 /* SettingsSidebarItemView.swift in Sources */, + 520163A92CBD6CCB00666E82 /* ModelInfo.swift in Sources */, + 5207D8D42CC6846F008588FA /* Provider.swift in Sources */, 526676592C85F903001EF113 /* DefaultConversationView.swift in Sources */, 52B053A02CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift in Sources */, - 5266766E2C85F903001EF113 /* Logger.swift in Sources */, + 520163A22CBD6C4900666E82 /* Conversation+CoreDataClass.swift in Sources */, + 520163A32CBD6C4900666E82 /* Conversation+CoreDataProperties.swift in Sources */, + 520163A42CBD6C4900666E82 /* Message+CoreDataClass.swift in Sources */, + 520163A52CBD6C4900666E82 /* Message+CoreDataProperties.swift in Sources */, + 520163A62CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift in Sources */, + 520163A72CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift in Sources */, 526676612C85F903001EF113 /* DownloadTask.swift in Sources */, 52A689F62CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift in Sources */, 528D83192CAD491900163AAB /* ChatMLX.xcdatamodeld in Sources */, @@ -596,12 +858,15 @@ 526676552C85F903001EF113 /* LocalModelsView.swift in Sources */, 52A689FA2CAECFE00078CDF9 /* ErrorAlertModifier.swift in Sources */, 526676662C85F903001EF113 /* RemoteModel.swift in Sources */, + 52EC64942CC3DC0E00A08069 /* NSManagedObjectContext+Extensions.swift in Sources */, 526676562C85F903001EF113 /* MLXCommunityItemView.swift in Sources */, + 52A265F02CBAE63E004F6DD3 /* BaseProvider.swift in Sources */, 526676442C85F903001EF113 /* UltramanMinimalistWindowModifier.swift in Sources */, 5266764A2C85F903001EF113 /* UltramanWindow.swift in Sources */, 52B0532A2CAFDFB000E8DDBA /* ProviderView.swift in Sources */, 52B0532C2CAFEFD800E8DDBA /* Color+Extensions.swift in Sources */, 526676702C85F903001EF113 /* Defaults+Extensions.swift in Sources */, + 52A265F32CBAEA9E004F6DD3 /* MLXProvider.swift in Sources */, 526676462C85F903001EF113 /* UltramanSecureField.swift in Sources */, 5266766C2C85F903001EF113 /* HubApi.swift in Sources */, 526676472C85F903001EF113 /* UltramanSidebarButtonStyle.swift in Sources */, @@ -610,61 +875,98 @@ 528D82262CABE19900163AAB /* Date+Extensions.swift in Sources */, 5266766D2C85F903001EF113 /* LLMRunner.swift in Sources */, 52B053542CB0F5B300E8DDBA /* LabeledContentStyle+Extensions.swift in Sources */, + 52A021CF2CB565FC007893F7 /* AnimatedGradientFill.metal in Sources */, 52B0534D2CB02C4600E8DDBA /* ModelManagerView.swift in Sources */, + 52EC64AD2CC4C38600A08069 /* ModelPicker.swift in Sources */, + 5201639B2CBD6BC400666E82 /* Message.swift in Sources */, 5266764E2C85F903001EF113 /* ConversationView.swift in Sources */, + 52977E162CCD0F7800DD4D2F /* LabeledToggle.swift in Sources */, 5266766A2C85F903001EF113 /* Downloader.swift in Sources */, 526676732C85F903001EF113 /* String+Extensions.swift in Sources */, 526676742C85F903001EF113 /* View+Extensions.swift in Sources */, 526676432C85F903001EF113 /* EffectView.swift in Sources */, - 528D83292CAD5C9100163AAB /* Conversation+CoreDataClass.swift in Sources */, - 528D832A2CAD5C9100163AAB /* Conversation+CoreDataProperties.swift in Sources */, - 528D832B2CAD5C9100163AAB /* Message+CoreDataClass.swift in Sources */, 528D83392CAE51EC00163AAB /* Role.swift in Sources */, 52B053952CB2B64500E8DDBA /* NoneInteractWindow.swift in Sources */, - 528D832C2CAD5C9100163AAB /* Message+CoreDataProperties.swift in Sources */, 526676712C85F903001EF113 /* MarkdownUI+Theme+Extensions.swift in Sources */, 526676532C85F903001EF113 /* DownloadTaskView.swift in Sources */, 52A689F82CAE8DA30078CDF9 /* SettingsViewModel.swift in Sources */, + 52B9BD072CBE94BA0086C013 /* Bundle+Extensions.swift in Sources */, 5266764C2C85F903001EF113 /* ConversationSidebarItem.swift in Sources */, 52B053572CB0F8EE00E8DDBA /* Binding+Extensions.swift in Sources */, + 52CDC3D22CC0208100627147 /* OpenAIProvider.swift in Sources */, 526676622C85F903001EF113 /* Language.swift in Sources */, + 520163AD2CBD876100666E82 /* Common.swift in Sources */, 526676722C85F903001EF113 /* NSWindow+Extensions.swift in Sources */, - 52B053502CB02CF900E8DDBA /* MLXProvider.swift in Sources */, + 52977E142CCCFF4100DD4D2F /* MLXProviderModelItemView.swift in Sources */, + 52B053502CB02CF900E8DDBA /* MLXProviderView.swift in Sources */, + 52977E182CCD1AC000DD4D2F /* GPUMemorySettingsView.swift in Sources */, 526676412C85F903001EF113 /* SplashCodeSyntaxHighlighter.swift in Sources */, 526676542C85F903001EF113 /* LocalModelItemView.swift in Sources */, 526676632C85F903001EF113 /* LocalModel.swift in Sources */, + 52977D2B2CCBCCEC00DD4D2F /* Logger.swift in Sources */, + 52EC64B12CC531A300A08069 /* ModelType.swift in Sources */, 52B0539D2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift in Sources */, + 520163972CBD6B4B00666E82 /* Conversation.swift in Sources */, + 52977E202CCD4D3400DD4D2F /* Errors.swift in Sources */, 52B0539E2CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift in Sources */, + 52EC64AF2CC4CD5400A08069 /* DefaultModelPicker.swift in Sources */, 526676692C85F903001EF113 /* Styles.swift in Sources */, 5266765A2C85F903001EF113 /* GeneralView.swift in Sources */, 526675462C85EDCB001EF113 /* ChatMLXApp.swift in Sources */, - 52B053272CAFD6DB00E8DDBA /* ModelConfig.swift in Sources */, 526676492C85F903001EF113 /* UltramanTextField.swift in Sources */, 5266765E2C85F903001EF113 /* SettingsView.swift in Sources */, 528D831C2CAD49E600163AAB /* PersistenceController.swift in Sources */, 5266766B2C85F903001EF113 /* Hub.swift in Sources */, + 52977E102CCCF22E00DD4D2F /* MLXProviderLabelView.swift in Sources */, + 52977E1B2CCD296F00DD4D2F /* ErrorView.swift in Sources */, + 5207D8D62CC68BAC008588FA /* DefaultProviderPicker.swift in Sources */, + 527821592CB817A300638477 /* ModelManagerViewModel.swift in Sources */, 5266764D2C85F903001EF113 /* ConversationSidebarView.swift in Sources */, 528D83372CADB64600163AAB /* ConversationViewModel.swift in Sources */, 5266765D2C85F903001EF113 /* SettingsSidebarView.swift in Sources */, 5266764B2C85F903001EF113 /* ConversationDetailView.swift in Sources */, + 5201638B2CBC2D8D00666E82 /* V2MigrationPolicy.swift in Sources */, 52B053A22CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift in Sources */, 526676602C85F903001EF113 /* DisplayStyle.swift in Sources */, + 5278215B2CB81FEB00638477 /* MarkdownMetadata.swift in Sources */, 5266765B2C85F903001EF113 /* HuggingFaceView.swift in Sources */, 526676642C85F903001EF113 /* LocalModelGroup.swift in Sources */, 526676522C85F903001EF113 /* DownloadManagerView.swift in Sources */, - 52B053522CB0385800E8DDBA /* OpenAIProvider.swift in Sources */, + 52A265F52CBBB3C0004F6DD3 /* ProviderFactory.swift in Sources */, + 520163AB2CBD715900666E82 /* ProviderModel.swift in Sources */, + 52977E242CCDF43E00DD4D2F /* Constants.swift in Sources */, + 52B053522CB0385800E8DDBA /* OpenAIProviderView.swift in Sources */, 526676482C85F903001EF113 /* UltramanTextEditorWithPlaceholder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + 527821962CB821A600638477 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 52A265FA2CBBC5D5004F6DD3 /* ProviderFactoryTests.swift in Sources */, + 527821A52CB821AC00638477 /* ChatMLXTests.swift in Sources */, + 527821A72CB821B800638477 /* MarkdownMetadataTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 5278219F2CB821A600638477 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 526675412C85EDCB001EF113 /* ChatMLX */; + targetProxy = 5278219E2CB821A600638477 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 5266754F2C85EDCC001EF113 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -694,6 +996,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -728,6 +1031,7 @@ buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -757,6 +1061,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -783,10 +1088,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = ChatMLX/ChatMLX.entitlements; + CODE_SIGN_ENTITLEMENTS = ChatMLX/Configuration/ChatMLX.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"ChatMLX/Preview Content\""; DEVELOPMENT_TEAM = RFGFKQEKRH; ENABLE_HARDENED_RUNTIME = YES; @@ -799,10 +1106,11 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -812,10 +1120,12 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = ChatMLX/ChatMLXRelease.entitlements; + CODE_SIGN_ENTITLEMENTS = ChatMLX/Configuration/ChatMLX.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"ChatMLX/Preview Content\""; DEVELOPMENT_TEAM = RFGFKQEKRH; ENABLE_HARDENED_RUNTIME = YES; @@ -828,14 +1138,51 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Release; }; + 527821A12CB821A600638477 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RFGFKQEKRH; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLXTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatMLX.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ChatMLX"; + }; + name = Debug; + }; + 527821A22CB821A600638477 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RFGFKQEKRH; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLXTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatMLX.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ChatMLX"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -857,6 +1204,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 527821A02CB821A600638477 /* Build configuration list for PBXNativeTarget "ChatMLXTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 527821A12CB821A600638477 /* Debug */, + 527821A22CB821A600638477 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -908,14 +1264,6 @@ minimumVersion = 0.16.0; }; }; - 526675752C85F26B001EF113 /* XCRemoteSwiftPackageReference "swift-log" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-log"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.6.1; - }; - }; 526675782C85F486001EF113 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/gonzalezreal/swift-markdown-ui.git"; @@ -940,6 +1288,22 @@ kind = branch; }; }; + 52CDC3D32CC020F500627147 /* XCRemoteSwiftPackageReference "OpenAI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/MacPaw/OpenAI.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.3.0; + }; + }; + 52CDC3D82CC03B4A00627147 /* XCRemoteSwiftPackageReference "CoreDataEvolution" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/fatbobman/CoreDataEvolution.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.3.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -977,11 +1341,6 @@ package = 526675722C85F1F9001EF113 /* XCRemoteSwiftPackageReference "splash" */; productName = Splash; }; - 526675762C85F26B001EF113 /* Logging */ = { - isa = XCSwiftPackageProductDependency; - package = 526675752C85F26B001EF113 /* XCRemoteSwiftPackageReference "swift-log" */; - productName = Logging; - }; 526675792C85F487001EF113 /* MarkdownUI */ = { isa = XCSwiftPackageProductDependency; package = 526675782C85F486001EF113 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; @@ -1001,6 +1360,16 @@ isa = XCSwiftPackageProductDependency; productName = Transformers; }; + 52CDC3D42CC020F500627147 /* OpenAI */ = { + isa = XCSwiftPackageProductDependency; + package = 52CDC3D32CC020F500627147 /* XCRemoteSwiftPackageReference "OpenAI" */; + productName = OpenAI; + }; + 52CDC3D92CC03B4A00627147 /* CoreDataEvolution */ = { + isa = XCSwiftPackageProductDependency; + package = 52CDC3D82CC03B4A00627147 /* XCRemoteSwiftPackageReference "CoreDataEvolution" */; + productName = CoreDataEvolution; + }; 52E50B1C2C8D6E81005A89DE /* LLM */ = { isa = XCSwiftPackageProductDependency; productName = LLM; @@ -1051,9 +1420,10 @@ 528D83172CAD491900163AAB /* ChatMLX.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 520163832CBC1C9F00666E82 /* ChatMLX 2.xcdatamodel */, 528D83182CAD491900163AAB /* ChatMLX.xcdatamodel */, ); - currentVersion = 528D83182CAD491900163AAB /* ChatMLX.xcdatamodel */; + currentVersion = 520163832CBC1C9F00666E82 /* ChatMLX 2.xcdatamodel */; path = ChatMLX.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/ChatMLX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ChatMLX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 14501f9..ef37f90 100644 --- a/ChatMLX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ChatMLX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "91755e46d4857336740696612733433e7fa7ef978bc35290de8f756037756422", + "originHash" : "abfff542bc4df674e107b815bb2eb68cf14edf79991cd6e62fb3c1d42a5ffa1a", "pins" : [ { "identity" : "alamofire", @@ -28,6 +28,15 @@ "version" : "1.1.6" } }, + { + "identity" : "coredataevolution", + "kind" : "remoteSourceControl", + "location" : "https://github.com/fatbobman/CoreDataEvolution.git", + "state" : { + "revision" : "5ef939b16ca5f95a132458e4e14a81d8bf452390", + "version" : "0.3.1" + } + }, { "identity" : "defaults", "kind" : "remoteSourceControl", @@ -69,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ml-explore/mlx-swift", "state" : { - "revision" : "78a7cfe6701d6e9c88e9d4a0d1f7990af84b2146", - "version" : "0.18.0" + "revision" : "d649c62b77c487c25012910b0d02b30283d388ca", + "version" : "0.18.1" } }, { @@ -91,6 +100,15 @@ "version" : "6.0.0" } }, + { + "identity" : "openai", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MacPaw/OpenAI.git", + "state" : { + "revision" : "843e087929aa806adb611dbca93f9a4a7f28be04", + "version" : "0.3.0" + } + }, { "identity" : "splash", "kind" : "remoteSourceControl", @@ -109,15 +127,6 @@ "version" : "1.5.0" } }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log", - "state" : { - "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537", - "version" : "1.6.1" - } - }, { "identity" : "swift-markdown-ui", "kind" : "remoteSourceControl", @@ -136,6 +145,15 @@ "version" : "1.0.2" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, { "identity" : "swift-transformers", "kind" : "remoteSourceControl", diff --git a/ChatMLX.xcodeproj/xcshareddata/xcschemes/ChatMLX.xcscheme b/ChatMLX.xcodeproj/xcshareddata/xcschemes/ChatMLX.xcscheme index a652935..cc42827 100644 --- a/ChatMLX.xcodeproj/xcshareddata/xcschemes/ChatMLX.xcscheme +++ b/ChatMLX.xcodeproj/xcshareddata/xcschemes/ChatMLX.xcscheme @@ -55,6 +55,22 @@ argument = "-com.apple.CoreData.ConcurrencyDebug 1" isEnabled = "YES"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChatMLX.xcodeproj/xcuserdata/john.xcuserdatad/xcschemes/xcschememanagement.plist b/ChatMLX.xcodeproj/xcuserdata/john.xcuserdatad/xcschemes/xcschememanagement.plist index e2cd285..9f7e491 100644 --- a/ChatMLX.xcodeproj/xcuserdata/john.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/ChatMLX.xcodeproj/xcuserdata/john.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,12 @@ ChatMLX.xcscheme_^#shared#^_ orderHint - 3 + 0 + + ChatMLXTests.xcscheme_^#shared#^_ + + orderHint + 7 SuppressBuildableAutocreation @@ -17,6 +22,11 @@ primary + 527821992CB821A600638477 + + primary + + diff --git a/ChatMLX/Application/ChatMLXApp.swift b/ChatMLX/Application/ChatMLXApp.swift new file mode 100644 index 0000000..233ef1e --- /dev/null +++ b/ChatMLX/Application/ChatMLXApp.swift @@ -0,0 +1,139 @@ +// +// ChatMLXApp.swift +// ChatMLX +// +// Created by John Mai on 2024/8/3. +// + +import Defaults +import os +import SwiftUI + +@main +struct ChatMLXApp: App { + // MARK: - Properties + + private let currentVersion = getVersion() + private let viewContext = PersistenceController.shared.container.viewContext + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ChatMLXApp") + + // MARK: - Environment + + @Environment(\.scenePhase) private var scenePhase + @Environment(\.openSettings) private var openSettings + @Environment(\.dismissWindow) private var dismissWindow + @Environment(\.openWindow) private var openWindow + + // MARK: - State + + @State private var conversationViewModel: ConversationViewModel = .init() + @State private var settingsViewModel: SettingsViewModel = .init() + @State private var runner: LLMRunner = .init() + @State private var modelManagerViewModel: ModelManagerViewModel = .init() + @State private var errorWrapper: ErrorWrapper? + + // MARK: - User Defaults + + @Default(.language) var language + @Default(.lastLaunchedVersion) var lastLaunchedVersion + + init() { + updateVersionIfNeeded() + } + + var body: some Scene { + Group { + mainWindow() + settingsWindow() + } + .environment(modelManagerViewModel) + .environment(conversationViewModel) + .environment(settingsViewModel) + .environment(runner) + .environment(\.managedObjectContext, viewContext) + .environment(\.locale, .init(identifier: language.rawValue)) + .environment(\.appError) { error in + Task { @MainActor in + errorWrapper = ErrorWrapper(error: error, guidance: "") + } + } + .onChange(of: scenePhase) { _, newValue in + if newValue == .background { + saveContext() + } + } + + menu() + } +} + +// MARK: - Scenes + +extension ChatMLXApp { + // MARK: - Main Window + + private func mainWindow() -> some Scene { + WindowGroup(id: Constants.mainWindowID) { + ConversationView() + .frame(minWidth: 900, minHeight: 580) + .sheet(item: $errorWrapper) { errorWrapper in + ErrorView(errorWrapper: errorWrapper) + } + } + } + + // MARK: - Settings Window + + private func settingsWindow() -> some Scene { + Settings { + SettingsView() + .frame(width: 650, height: 480) + .sheet(item: $errorWrapper) { errorWrapper in + ErrorView(errorWrapper: errorWrapper) + } + } + } + + // MARK: - Menu + + private func menu() -> some Scene { + MenuBarExtra { + Button("Open \(Bundle.main.name)") { + dismissWindow(id: Constants.mainWindowID) + NSApp.activate(ignoringOtherApps: true) + openWindow(id: Constants.mainWindowID) + } + Button("New Conversation") {} + Button("Settings") { + openSettings() + } + .keyboardShortcut(",") + Button("Quit") { + NSApp.terminate(nil) + } + .keyboardShortcut("q") + } label: { + Image("menubarIcon") + .renderingMode(.template) + } + } +} + +// MARK: - Private Methods + +extension ChatMLXApp { + private func updateVersionIfNeeded() { + if currentVersion != lastLaunchedVersion { + Defaults[.lastLaunchedVersion] = currentVersion + } + } + + private func saveContext() { + guard viewContext.hasChanges else { return } + do { + try viewContext.save() + } catch { + logger.error("Failed to save context: \(error.localizedDescription)") + } + } +} diff --git a/ChatMLX/Assets.xcassets/AccentColor.colorset/Contents.json b/ChatMLX/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index 22c4bb0..0000000 --- a/ChatMLX/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ChatMLX/ChatMLXApp.swift b/ChatMLX/ChatMLXApp.swift deleted file mode 100644 index 428ba3c..0000000 --- a/ChatMLX/ChatMLXApp.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// ChatMLXApp.swift -// ChatMLX -// -// Created by John Mai on 2024/8/3. -// - -import Defaults -import SwiftUI - -@main -struct ChatMLXApp: App { - @Environment(\.scenePhase) private var scenePhase - - @State private var conversationViewModel: ConversationViewModel = .init() - @State private var settingsViewModel: SettingsViewModel = .init() - @State private var runner: LLMRunner = .init() - - @Default(.language) var language - - let persistenceController = PersistenceController.shared - - var body: some Scene { - WindowGroup { - ConversationView() - .environment(conversationViewModel) - .environment( - \.locale, .init(identifier: language.rawValue) - ) - .environment(runner) - .frame(minWidth: 900, minHeight: 580) - .errorAlert( - isPresented: $conversationViewModel.showErrorAlert, - title: $settingsViewModel.errorTitle, - error: $conversationViewModel.error - ) - } - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .onChange(of: scenePhase) { _, newValue in - if newValue == .background { - let context = persistenceController.container.viewContext - if context.hasChanges { - do { - try context.save() - } catch { - logger.error( - "scenePhase.background save error: \(error.localizedDescription)") - } - } - } - } - - Settings { - SettingsView() - .environment(conversationViewModel) - .environment(settingsViewModel) - .environment( - \.locale, .init(identifier: language.rawValue) - ) - .environment(runner) - .frame(width: 650, height: 480) - .errorAlert( - isPresented: $settingsViewModel.showErrorAlert, - title: $settingsViewModel.errorTitle, - error: $settingsViewModel.error - ) - } - .environment(\.managedObjectContext, persistenceController.container.viewContext) - } -} diff --git a/ChatMLX/Components/AppleIntelligenceEffect/ColorWheel.metal b/ChatMLX/Components/AppleIntelligenceEffect/ColorWheel.metal deleted file mode 100644 index 4e6afca..0000000 --- a/ChatMLX/Components/AppleIntelligenceEffect/ColorWheel.metal +++ /dev/null @@ -1,25 +0,0 @@ -// -// ColorWheel.metal -// ChatMLX -// -// Created by John Mai on 2024/10/6. -// - -#include -using namespace metal; - -#define M_TWO_PI_F (M_PI_F * 2) - -float3 hsv2rgb(float3 c) { - float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); - float3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); - return c.z * mix(K.xxx, saturate(p - K.xxx), c.y); -} - -[[ stitchable ]] half4 colorWheel(float2 position, float4 bounds, float brightness) { - float2 center = position / bounds.zw - 0.5; - float hue = (atan2(center.y, center.x) + M_PI_F) / M_TWO_PI_F; - float saturation = min(length(center) * 2.0, 1.0); - float value = saturate(brightness); - return half4(half3(hsv2rgb(float3(hue, saturation, value))), 1.0); -} diff --git a/ChatMLX/Components/UltramanMinimalistWindowModifier.swift b/ChatMLX/Components/UltramanMinimalistWindowModifier.swift deleted file mode 100644 index a31cadc..0000000 --- a/ChatMLX/Components/UltramanMinimalistWindowModifier.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// UltramanMinimalistWindowModifier.swift -// ChatMLX -// -// Created by John Mai on 2024/8/10. -// - -import AppKit -import Defaults -import SwiftUI -import SwiftUIIntrospect - -struct UltramanMinimalistWindowModifier: ViewModifier { - @Default(.backgroundBlurRadius) var blurRadius - @Default(.backgroundColor) var backgroundColor - @State private var isFullScreen = false - - func body(content: Content) -> some View { - content - .ignoresSafeArea() - .introspect(.window, on: .macOS(.v14, .v15)) { window in - window.setBackgroundBlur( - radius: Int(blurRadius), color: NSColor(backgroundColor)) - window.toolbarStyle = .unified - window.titlebarAppearsTransparent = true - window.titleVisibility = .hidden - - let toolbar = NSToolbar() - toolbar.showsBaselineSeparator = false - window.toolbar = toolbar - - NotificationCenter.default.addObserver( - forName: NSWindow.didEnterFullScreenNotification, - object: window, queue: .main - ) { _ in - self.isFullScreen = true - self.updateFullScreenSettings(for: window) - } - - NotificationCenter.default.addObserver( - forName: NSWindow.didExitFullScreenNotification, - object: window, queue: .main - ) { _ in - self.isFullScreen = false - self.updateFullScreenSettings(for: window) - } - } - } - - private func updateFullScreenSettings(for window: NSWindow) { - if isFullScreen { - window.collectionBehavior.insert(.fullScreenPrimary) - window.toolbar?.isVisible = false - NSApp.presentationOptions = [ - .autoHideToolbar, .autoHideMenuBar, .fullScreen, - ] - } else { - window.collectionBehavior.remove(.fullScreenPrimary) - window.toolbar?.isVisible = true - NSApp.presentationOptions = [] - } - } -} - -extension View { - func ultramanMinimalistWindowStyle() -> some View { - modifier(UltramanMinimalistWindowModifier()) - } -} diff --git a/ChatMLX/ChatMLXRelease.entitlements b/ChatMLX/Configuration/ChatMLX.entitlements similarity index 84% rename from ChatMLX/ChatMLXRelease.entitlements rename to ChatMLX/Configuration/ChatMLX.entitlements index ffc8bb3..3203b1c 100644 --- a/ChatMLX/ChatMLXRelease.entitlements +++ b/ChatMLX/Configuration/ChatMLX.entitlements @@ -2,10 +2,12 @@ - com.apple.security.files.user-selected.read-only - com.apple.security.app-sandbox + com.apple.security.files.downloads.read-only + + com.apple.security.files.user-selected.read-only + com.apple.security.network.client com.apple.security.network.server diff --git a/ChatMLX/ChatMLX.entitlements b/ChatMLX/Configuration/ChatMLX.xcdatamodeld/.xccurrentversion similarity index 51% rename from ChatMLX/ChatMLX.entitlements rename to ChatMLX/Configuration/ChatMLX.xcdatamodeld/.xccurrentversion index 7c7a703..f703c20 100644 --- a/ChatMLX/ChatMLX.entitlements +++ b/ChatMLX/Configuration/ChatMLX.xcdatamodeld/.xccurrentversion @@ -2,11 +2,7 @@ - com.apple.security.network.client - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + _XCCurrentVersionName + ChatMLX 2.xcdatamodel diff --git a/ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX 2.xcdatamodel/contents b/ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX 2.xcdatamodel/contents new file mode 100644 index 0000000..cd5770b --- /dev/null +++ b/ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX 2.xcdatamodel/contents @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ChatMLX/Models/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents b/ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents similarity index 92% rename from ChatMLX/Models/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents rename to ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents index d7ee4c8..ca47eef 100644 --- a/ChatMLX/Models/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents +++ b/ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents @@ -1,11 +1,12 @@ - + + - + diff --git a/ChatMLX/Core/Components/AppleIntelligenceEffect/AnimatedGradientFill.metal b/ChatMLX/Core/Components/AppleIntelligenceEffect/AnimatedGradientFill.metal new file mode 100644 index 0000000..d5ba816 --- /dev/null +++ b/ChatMLX/Core/Components/AppleIntelligenceEffect/AnimatedGradientFill.metal @@ -0,0 +1,14 @@ +// +// AnimatedGradientFill.metal +// Inferno https://github.com/twostraws/Inferno/blob/main/Sources/Inferno/Shaders/Transformation/AnimatedGradientFill.metal +// + +#include +using namespace metal; + +[[ stitchable ]] half4 animatedGradientFill(float2 position, half4 color, float2 size, float time) { + half2 uv = half2(position / size) * 2.0h - 1.0h; + half angle = atan2(uv.y, uv.x) + time; + half3 sinValues = abs(sin(half3(angle, angle + 2.0h, angle + 4.0h))); + return half4(sinValues, 1.0h) * color.a; +} diff --git a/ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectController.swift b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectController.swift similarity index 94% rename from ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectController.swift rename to ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectController.swift index 009773d..62030fc 100644 --- a/ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectController.swift +++ b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectController.swift @@ -1,8 +1,8 @@ // -// FireworkController.swift -// Firework +// AppleIntelligenceEffectController.swift +// ChatMLX // -// Created by 秋星桥 on 2024/2/7. +// Created by John Mai on 2024/10/6. // import AppKit diff --git a/ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift similarity index 95% rename from ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift rename to ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift index 50d2920..1ca8d5f 100644 --- a/ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift +++ b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift @@ -7,7 +7,7 @@ import AppKit -class AppleIntelligenceEffectManager { +final class AppleIntelligenceEffectManager { static let shared = AppleIntelligenceEffectManager() private var effectController: AppleIntelligenceEffectController? diff --git a/ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift similarity index 62% rename from ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift rename to ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift index 5438658..3490e0f 100644 --- a/ChatMLX/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift +++ b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift @@ -8,36 +8,42 @@ import SwiftUI struct AppleIntelligenceEffectView: View { - private let shader = ShaderLibrary.colorWheel(.boundingRect, .float(1)) - private let angles = [133.0, -133.0] + private let shader = ShaderLibrary.colorWheel(.boundingRect) + private let angles = [1, -1] private let maxBlurRadiusBase: CGFloat = 18 private let minBlurRadiusBase: CGFloat = 6 + private let startTime = Date.now var useRoundedRectangle: Bool = true var body: some View { TimelineView(.animation) { timeline in ZStack { - ForEach(angles.indices, id: \.self) { index in - colorWheelRectangle(for: timeline.date, angle: angles[index]) + ForEach(angles, id: \.self) { angle in + colorWheelRectangle(for: timeline.date, angle: angle) } } } } - @MainActor - private func colorWheelRectangle(for date: Date, angle: Double) -> some View { - let time = date.timeIntervalSince1970 + private func colorWheelRectangle(for date: Date, angle: Int) -> some View { + let elapsed = startTime.distance(to: date) + let blurRadius = angle > 0 - ? maxBlurRadiusBase + 6 * sin(time * 2) - : minBlurRadiusBase + 3 * sin(time * 4) + ? maxBlurRadiusBase + 6 * sin(elapsed * 2) + : minBlurRadiusBase + 3 * sin(elapsed * 4) return Rectangle() - .fill(shader) - .rotationEffect(.degrees(time * 60)) - .scaleEffect(2.4) - .rotationEffect(.degrees(time * angle)) + .visualEffect { content, proxy in + content + .colorEffect( + ShaderLibrary.animatedGradientFill( + .float2(proxy.size), + .float(elapsed) + ) + ) + } .mask(alignment: .center) { if useRoundedRectangle { UnevenRoundedRectangle( diff --git a/ChatMLX/Core/Components/DefaultModelPicker.swift b/ChatMLX/Core/Components/DefaultModelPicker.swift new file mode 100644 index 0000000..b8e6769 --- /dev/null +++ b/ChatMLX/Core/Components/DefaultModelPicker.swift @@ -0,0 +1,48 @@ +// +// DefaultModelPicker.swift +// ChatMLX +// +// Created by John Mai on 2024/10/20. +// + +import CoreData +import Defaults +import SwiftUI + +struct DefaultModelPicker: View { +// @FetchRequest( +// sortDescriptors: [NSSortDescriptor(keyPath: \ModelInfo.name, ascending: true)], +// animation: .default +// ) +// private var models: FetchedResults + + @Binding var provider: Provider + @Environment(\.managedObjectContext) private var viewContext + + @Default(.defaultModel) var defaultModel + + var models: [ProviderModel] { + switch provider { + case .mlx: + try! MLXProvider.fetchModels() + case .openAI: + OpenAIProvider.fetchModels() + } + } + + var body: some View { + Picker( + selection: $defaultModel, + label: Image(systemName: "brain") + ) { + Text("Not selected").tag(nil as String?) + Divider() + ForEach(models, id: \.self) { model in + Text(model.name ?? model.id).tag(model.id) + } + } + .pickerStyle(.menu) + .labelsHidden() + .tint(.white) + } +} diff --git a/ChatMLX/Core/Components/DefaultProviderPicker.swift b/ChatMLX/Core/Components/DefaultProviderPicker.swift new file mode 100644 index 0000000..bf661cf --- /dev/null +++ b/ChatMLX/Core/Components/DefaultProviderPicker.swift @@ -0,0 +1,28 @@ +// +// DefaultProviderPicker.swift +// ChatMLX +// +// Created by John Mai on 2024/10/21. +// + +import Defaults +import SwiftUI + +struct DefaultProviderPicker:View { + @Binding var provider: Provider + + @Default(.enableOpenAI) private var enableOpenAI + + var body: some View { + Picker("Provider", selection: $provider) { + Text("MLX").tag(Provider.mlx) + + if enableOpenAI { + Text("OpenAI").tag(Provider.openAI) + } + } + .pickerStyle(.menu) + .labelsHidden() + .tint(.white) + } +} diff --git a/ChatMLX/Components/EffectView.swift b/ChatMLX/Core/Components/EffectView.swift similarity index 100% rename from ChatMLX/Components/EffectView.swift rename to ChatMLX/Core/Components/EffectView.swift diff --git a/ChatMLX/Components/ErrorAlertModifier.swift b/ChatMLX/Core/Components/ErrorAlertModifier.swift similarity index 100% rename from ChatMLX/Components/ErrorAlertModifier.swift rename to ChatMLX/Core/Components/ErrorAlertModifier.swift diff --git a/ChatMLX/Core/Components/LabeledToggle.swift b/ChatMLX/Core/Components/LabeledToggle.swift new file mode 100644 index 0000000..a0a6ee6 --- /dev/null +++ b/ChatMLX/Core/Components/LabeledToggle.swift @@ -0,0 +1,22 @@ +// +// LabeledToggle.swift +// ChatMLX +// +// Created by John Mai on 2024/10/26. +// + +import SwiftUI + +struct LabeledToggle: View { + let title: String + @Binding var isOn: Bool + + var body: some View { + LabeledContent(title) { + Toggle("", isOn: $isOn) + .labelsHidden() + .toggleStyle(.switch) + } + .labeledContentStyle(.horizontal) + } +} diff --git a/ChatMLX/Core/Components/ModelPicker.swift b/ChatMLX/Core/Components/ModelPicker.swift new file mode 100644 index 0000000..762c7d5 --- /dev/null +++ b/ChatMLX/Core/Components/ModelPicker.swift @@ -0,0 +1,69 @@ +// +// ModelPicker.swift +// ChatMLX +// +// Created by John Mai on 2024/10/20. +// + +import Defaults +import SwiftUI + +struct ModelPicker: View { + @Binding var selection: ModelInfo? + @Default(.enableOpenAI) private var enableOpenAI + @State var models: [ProviderModel] = [] + + var groupedModels: [String: [ProviderModel]] { + var models: [ProviderModel] = [] + do { + models = try MLXProvider.fetchModels() + } catch { + print("Error fetching models") + } + + if enableOpenAI { + models = models + OpenAIProvider.fetchModels() + } + + return Dictionary(grouping: models) { $0.provider.rawValue } + } + + var body: some View { + Picker( + selection: $selection, + label: Image(systemName: "brain") + ) { + Text("Not selected").tag(nil as ModelInfo?) + ForEach(groupedModels.keys.sorted(), id: \.self) { provider in + Section(header: Text(provider)) { + ForEach(groupedModels[provider]!, id: \.self) { model in + Text(model.name ?? model.id).tag(model) + } + } + } + } + .pickerStyle(.menu) + .labelsHidden() + .tint(.white) + .task { + await fetchModels() + } + } + + private func fetchModels() async { + var models: [ProviderModel] = [] + do { + models = try MLXProvider.fetchModels() + } catch { + print("Error fetching models") + } + + if enableOpenAI { + models = models + OpenAIProvider.fetchModels() + } + + await MainActor.run { + self.models = models + } + } +} diff --git a/ChatMLX/Components/NoneInteractWindow.swift b/ChatMLX/Core/Components/NoneInteractWindow.swift similarity index 100% rename from ChatMLX/Components/NoneInteractWindow.swift rename to ChatMLX/Core/Components/NoneInteractWindow.swift diff --git a/ChatMLX/Features/Settings/ModelManager/Providers/ProviderView.swift b/ChatMLX/Core/Components/ProviderView.swift similarity index 94% rename from ChatMLX/Features/Settings/ModelManager/Providers/ProviderView.swift rename to ChatMLX/Core/Components/ProviderView.swift index ad01732..04a0fff 100644 --- a/ChatMLX/Features/Settings/ModelManager/Providers/ProviderView.swift +++ b/ChatMLX/Core/Components/ProviderView.swift @@ -8,7 +8,7 @@ import SwiftUI struct ProviderView: View { - @Binding var isExpanded: Bool + @State var isExpanded: Bool = false @Binding var isEnabled: Bool? @ViewBuilder let label: () -> Label @ViewBuilder let content: () -> Content @@ -20,7 +20,6 @@ struct ProviderView: View { header if isExpanded { content() - .padding() } } .background(.quinary) @@ -31,7 +30,6 @@ struct ProviderView: View { } } - @MainActor @ViewBuilder private var header: some View { HStack { diff --git a/ChatMLX/Components/SyntaxHighlighter/SplashCodeSyntaxHighlighter.swift b/ChatMLX/Core/Components/SyntaxHighlighter/SplashCodeSyntaxHighlighter.swift similarity index 100% rename from ChatMLX/Components/SyntaxHighlighter/SplashCodeSyntaxHighlighter.swift rename to ChatMLX/Core/Components/SyntaxHighlighter/SplashCodeSyntaxHighlighter.swift diff --git a/ChatMLX/Components/SyntaxHighlighter/TextOutputFormat.swift b/ChatMLX/Core/Components/SyntaxHighlighter/TextOutputFormat.swift similarity index 100% rename from ChatMLX/Components/SyntaxHighlighter/TextOutputFormat.swift rename to ChatMLX/Core/Components/SyntaxHighlighter/TextOutputFormat.swift diff --git a/ChatMLX/Core/Components/UltramanMinimalistWindowModifier.swift b/ChatMLX/Core/Components/UltramanMinimalistWindowModifier.swift new file mode 100644 index 0000000..2e93b2b --- /dev/null +++ b/ChatMLX/Core/Components/UltramanMinimalistWindowModifier.swift @@ -0,0 +1,67 @@ +// +// UltramanMinimalistWindowModifier.swift +// ChatMLX +// +// Created by John Mai on 2024/8/10. +// +import AppKit +import Defaults +import SwiftUI +import SwiftUIIntrospect + +struct UltramanMinimalistWindowModifier: ViewModifier { + @Default(.backgroundBlurRadius) var blurRadius + @Default(.backgroundColor) var backgroundColor + + func body(content: Content) -> some View { + content + .ignoresSafeArea() + .introspect(.window, on: .macOS(.v14, .v15)) { window in + configureWindow(window) + setupFullScreenObservers(for: window) + } + } + + private func configureWindow(_ window: NSWindow) { + window.setBackgroundBlur(radius: Int(blurRadius), color: NSColor(backgroundColor)) + window.toolbarStyle = .unified + window.titlebarAppearsTransparent = true + window.titleVisibility = .hidden + + let toolbar = NSToolbar() + toolbar.showsBaselineSeparator = false + window.toolbar = toolbar + } + + private func setupFullScreenObservers(for window: NSWindow) { + let notificationCenter = NotificationCenter.default + + notificationCenter.addObserver(forName: NSWindow.didEnterFullScreenNotification, object: window, queue: .main) { _ in + Task { @MainActor in + handleFullScreenEnter(window) + } + } + + notificationCenter.addObserver(forName: NSWindow.didExitFullScreenNotification, object: window, queue: .main) { _ in + Task { @MainActor in + handleFullScreenExit(window) + } + } + } + + private func handleFullScreenEnter(_ window: NSWindow) { + window.toolbar?.isVisible = false + NSApp.presentationOptions = [.autoHideToolbar, .autoHideMenuBar] + } + + private func handleFullScreenExit(_ window: NSWindow) { + window.toolbar?.isVisible = true + NSApp.presentationOptions = [] + } +} + +extension View { + func ultramanMinimalistWindowStyle() -> some View { + modifier(UltramanMinimalistWindowModifier()) + } +} diff --git a/ChatMLX/Components/UltramanNavigationSplitView.swift b/ChatMLX/Core/Components/UltramanNavigationSplitView.swift similarity index 93% rename from ChatMLX/Components/UltramanNavigationSplitView.swift rename to ChatMLX/Core/Components/UltramanNavigationSplitView.swift index e163136..804b0b2 100644 --- a/ChatMLX/Components/UltramanNavigationSplitView.swift +++ b/ChatMLX/Core/Components/UltramanNavigationSplitView.swift @@ -8,10 +8,10 @@ import SwiftUI struct UltramanNavigationTitleKey: PreferenceKey { - static var defaultValue: LocalizedStringKey = "" + static let defaultValue: String = "" static func reduce( - value: inout LocalizedStringKey, nextValue: () -> LocalizedStringKey + value: inout String, nextValue: () -> String ) { value = nextValue() } @@ -58,7 +58,7 @@ struct UltramanToolbarBuilder { } extension View { - func ultramanNavigationTitle(_ title: LocalizedStringKey) -> some View { + func ultramanNavigationTitle(_ title: String) -> some View { preference(key: UltramanNavigationTitleKey.self, value: title) } @@ -95,7 +95,7 @@ struct UltramanNavigationSplitView: View { let sidebar: () -> Sidebar let detail: () -> Detail - @State private var navigationTitle: LocalizedStringKey = "" + @State private var navigationTitle: String = "" @State private var toolbarItems: [UltramanToolbarItem] = [] @State private var isDragging = false @@ -133,7 +133,6 @@ struct UltramanNavigationSplitView: View { } } - @MainActor @ViewBuilder func header() -> some View { VStack(spacing: 0) { @@ -158,7 +157,7 @@ struct UltramanNavigationSplitView: View { } Spacer() - Text(navigationTitle) + Text(LocalizedStringKey(navigationTitle)) .font(.headline) Spacer() diff --git a/ChatMLX/Components/UltramanSecureField.swift b/ChatMLX/Core/Components/UltramanSecureField.swift similarity index 100% rename from ChatMLX/Components/UltramanSecureField.swift rename to ChatMLX/Core/Components/UltramanSecureField.swift diff --git a/ChatMLX/Components/UltramanSidebarButtonStyle.swift b/ChatMLX/Core/Components/UltramanSidebarButtonStyle.swift similarity index 100% rename from ChatMLX/Components/UltramanSidebarButtonStyle.swift rename to ChatMLX/Core/Components/UltramanSidebarButtonStyle.swift diff --git a/ChatMLX/Components/UltramanTextEditorWithPlaceholder.swift b/ChatMLX/Core/Components/UltramanTextEditorWithPlaceholder.swift similarity index 100% rename from ChatMLX/Components/UltramanTextEditorWithPlaceholder.swift rename to ChatMLX/Core/Components/UltramanTextEditorWithPlaceholder.swift diff --git a/ChatMLX/Components/UltramanTextField.swift b/ChatMLX/Core/Components/UltramanTextField.swift similarity index 100% rename from ChatMLX/Components/UltramanTextField.swift rename to ChatMLX/Core/Components/UltramanTextField.swift diff --git a/ChatMLX/Components/UltramanWindow.swift b/ChatMLX/Core/Components/UltramanWindow.swift similarity index 100% rename from ChatMLX/Components/UltramanWindow.swift rename to ChatMLX/Core/Components/UltramanWindow.swift diff --git a/ChatMLX/Core/Constants.swift b/ChatMLX/Core/Constants.swift new file mode 100644 index 0000000..b56c3d0 --- /dev/null +++ b/ChatMLX/Core/Constants.swift @@ -0,0 +1,10 @@ +// +// Constants.swift +// ChatMLX +// +// Created by John Mai on 2024/10/27. +// + +struct Constants { + static let mainWindowID = "main-window" +} diff --git a/ChatMLX/Core/Errors/ErrorView.swift b/ChatMLX/Core/Errors/ErrorView.swift new file mode 100644 index 0000000..4982dc0 --- /dev/null +++ b/ChatMLX/Core/Errors/ErrorView.swift @@ -0,0 +1,40 @@ +// +// ErrorView.swift +// ChatMLX +// +// Created by John Mai on 2024/10/26. +// + +import SwiftUI + +struct ErrorEnvironmentKey: EnvironmentKey { + static let defaultValue: @Sendable (Error) -> Void = { _ in } +} + +extension EnvironmentValues { + var appError: @Sendable (Error) -> Void { + get { self[ErrorEnvironmentKey.self] } + set { self[ErrorEnvironmentKey.self] = newValue } + } +} + +struct ErrorView: View { + let errorWrapper: ErrorWrapper + + var body: some View { + VStack { + Text("An error has occurred!") + .font(.title) + .padding(.bottom) + Text(errorWrapper.error.localizedDescription) + .font(.headline) + Text(errorWrapper.guidance) + .font(.caption) + .padding(.top) + Spacer() + } + .padding() + .background(.ultraThinMaterial) + .cornerRadius(16) + } +} diff --git a/ChatMLX/Core/Errors/ErrorWrapper.swift b/ChatMLX/Core/Errors/ErrorWrapper.swift new file mode 100644 index 0000000..4464b8d --- /dev/null +++ b/ChatMLX/Core/Errors/ErrorWrapper.swift @@ -0,0 +1,20 @@ +// +// ErrorWrapper.swift +// ChatMLX +// +// Created by John Mai on 2024/10/26. +// + +import Foundation + +struct ErrorWrapper: Identifiable { + let id: UUID + let error: Error + let guidance: String + + init(id: UUID = UUID(), error: Error, guidance: String) { + self.id = id + self.error = error + self.guidance = guidance + } +} diff --git a/ChatMLX/Core/Errors/Errors.swift b/ChatMLX/Core/Errors/Errors.swift new file mode 100644 index 0000000..d190bec --- /dev/null +++ b/ChatMLX/Core/Errors/Errors.swift @@ -0,0 +1,19 @@ +// +// Errors.swift +// ChatMLX +// +// Created by John Mai on 2024/10/27. +// + +import Foundation + +enum Errors: Error, LocalizedError { + case documentDirectoryNotFound + + var errorDescription: String? { + switch self { + case .documentDirectoryNotFound: + "Document directory not found." + } + } +} diff --git a/ChatMLX/Extensions/Binding+Extensions.swift b/ChatMLX/Core/Extensions/Binding+Extensions.swift similarity index 78% rename from ChatMLX/Extensions/Binding+Extensions.swift rename to ChatMLX/Core/Extensions/Binding+Extensions.swift index 540accc..6934988 100644 --- a/ChatMLX/Extensions/Binding+Extensions.swift +++ b/ChatMLX/Core/Extensions/Binding+Extensions.swift @@ -9,12 +9,12 @@ import Foundation import SwiftUI extension Binding { - func toUnwrapped(defaultValue: T) -> Binding where Value == T? { + func toUnwrapped(defaultValue: T) -> Binding where Value == T? { Binding(get: { self.wrappedValue ?? defaultValue }, set: { self.wrappedValue = $0 }) } } -extension Binding { +extension Binding where Value: Sendable { func asDouble() -> Binding where Value: BinaryInteger { Binding( get: { Double(self.wrappedValue) }, diff --git a/ChatMLX/Core/Extensions/Bundle+Extensions.swift b/ChatMLX/Core/Extensions/Bundle+Extensions.swift new file mode 100644 index 0000000..b1852af --- /dev/null +++ b/ChatMLX/Core/Extensions/Bundle+Extensions.swift @@ -0,0 +1,26 @@ +// +// Bundle+Extensions.swift +// ChatMLX +// +// Created by John Mai on 2024/10/15. +// + +import Foundation + +extension Bundle { + func getInfoDictionary(_ str: String) -> String? { + infoDictionary?[str] as? String + } + + var version: String { + getInfoDictionary("CFBundleVersion") ?? "Unknown" + } + + var shortVersion: String { + getInfoDictionary("CFBundleShortVersionString") ?? "Unknown" + } + + var name: String { + getInfoDictionary("CFBundleName") ?? "ChatMLX" + } +} diff --git a/ChatMLX/Extensions/Color+Extensions.swift b/ChatMLX/Core/Extensions/Color+Extensions.swift similarity index 100% rename from ChatMLX/Extensions/Color+Extensions.swift rename to ChatMLX/Core/Extensions/Color+Extensions.swift diff --git a/ChatMLX/Extensions/Date+Extensions.swift b/ChatMLX/Core/Extensions/Date+Extensions.swift similarity index 100% rename from ChatMLX/Extensions/Date+Extensions.swift rename to ChatMLX/Core/Extensions/Date+Extensions.swift diff --git a/ChatMLX/Extensions/Defaults+Extensions.swift b/ChatMLX/Core/Extensions/Defaults+Extensions.swift similarity index 79% rename from ChatMLX/Extensions/Defaults+Extensions.swift rename to ChatMLX/Core/Extensions/Defaults+Extensions.swift index 898011b..8dfb7a8 100644 --- a/ChatMLX/Extensions/Defaults+Extensions.swift +++ b/ChatMLX/Core/Extensions/Defaults+Extensions.swift @@ -9,7 +9,7 @@ import Defaults import SwiftUI extension Defaults.Keys { - static let defaultModel = Key("defaultModel", default: "") + static let lastLaunchedVersion = Key("lastLaunchedVersion", default: getVersion()) static let language = Key("language", default: .english) static let backgroundBlurRadius = Key("backgroundBlurRadius", default: 35) static let backgroundColor = Key("backgroundColor", default: .black.opacity(0.4)) @@ -40,7 +40,15 @@ extension Defaults.Keys { static let gpuMemoryLimit = Key("gpuMemoryLimit", default: 1024) static let enableAppleIntelligenceEffect = Key( - "enableAppleIntelligenceEffect", default: false) + "enableAppleIntelligenceEffect", default: false) static let appleIntelligenceEffectDisplay = Key( - "appleIntelligenceEffectDisplay", default: .appInternal) + "appleIntelligenceEffectDisplay", default: .appInternal) + + static let defaultProvider = Key("defaultProvider", default: .mlx) + static let defaultModel = Key("defaultModel", default: nil) + + static let enableOpenAI = Key("enableOpenAI", default: false) + + static let openAIBaseURL = Key("openAIBaseURL", default: "https://api.openai.com") + static let openAIApiKey = Key("openAIApiKey", default: "") } diff --git a/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift b/ChatMLX/Core/Extensions/LabeledContentStyle+Extensions.swift similarity index 87% rename from ChatMLX/Extensions/LabeledContentStyle+Extensions.swift rename to ChatMLX/Core/Extensions/LabeledContentStyle+Extensions.swift index 92da75d..8a1727b 100644 --- a/ChatMLX/Extensions/LabeledContentStyle+Extensions.swift +++ b/ChatMLX/Core/Extensions/LabeledContentStyle+Extensions.swift @@ -14,6 +14,9 @@ struct HorizontalLabeledContentStyle: LabeledContentStyle { Spacer() configuration.content } + .frame(minHeight: 35) + .padding(.horizontal) + } } @@ -21,10 +24,9 @@ extension LabeledContentStyle where Self == HorizontalLabeledContentStyle { static var horizontal: HorizontalLabeledContentStyle { .init() } } - struct VerticalLabeledContentStyle: LabeledContentStyle { func makeBody(configuration: Configuration) -> some View { - VStack(alignment:.leading) { + VStack(alignment: .leading, spacing: 0) { configuration.label configuration.content } diff --git a/ChatMLX/Extensions/MarkdownUI+Theme+Extensions.swift b/ChatMLX/Core/Extensions/MarkdownUI+Theme+Extensions.swift similarity index 100% rename from ChatMLX/Extensions/MarkdownUI+Theme+Extensions.swift rename to ChatMLX/Core/Extensions/MarkdownUI+Theme+Extensions.swift diff --git a/ChatMLX/Core/Extensions/NSManagedObjectContext+Extensions.swift b/ChatMLX/Core/Extensions/NSManagedObjectContext+Extensions.swift new file mode 100644 index 0000000..cfd8112 --- /dev/null +++ b/ChatMLX/Core/Extensions/NSManagedObjectContext+Extensions.swift @@ -0,0 +1,23 @@ +// +// NSManagedObjectContext+Extensions.swift +// ChatMLX +// +// Created by John Mai on 2024/10/19. +// + +import CoreData + +extension NSManagedObjectContext { + @discardableResult + func saveIfNeeded() throws -> Bool { + guard hasChanges else { return false } + try save() + return true + } + + func saveIfNeeded() async throws -> Bool { + try await perform { + try self.saveIfNeeded() + } + } +} diff --git a/ChatMLX/Core/Extensions/NSWindow+Extensions.swift b/ChatMLX/Core/Extensions/NSWindow+Extensions.swift new file mode 100644 index 0000000..eade173 --- /dev/null +++ b/ChatMLX/Core/Extensions/NSWindow+Extensions.swift @@ -0,0 +1,43 @@ +// +// NSWindow+Extensions.swift +// ChatMLX +// +// Created by John Mai on 2024/8/3. +// + +import SwiftUI + +extension NSWindow { + /// Sets the background blur of the window with a specified radius and color. + /// + /// - Parameters: + /// - radius: The blur radius to apply. Defaults to 0 if an error occurs. + /// - color: The background color. Defaults to semi-transparent black. + func setBackgroundBlur(radius: Int, color: NSColor = .black.withAlphaComponent(0.4)) { + guard let connection = CGSDefaultConnectionForThread() else { + NSLog("Failed to get CGS connection") + return + } + + let status = CGSSetWindowBackgroundBlurRadius(connection, windowNumber, radius) + if status != noErr { + NSLog("Error setting blur radius: \(status)") + } + + backgroundColor = color + ignoresMouseEvents = false + } +} + +// MARK: - Private APIs and Helper Functions + +@_silgen_name("CGSDefaultConnectionForThread") +func CGSDefaultConnectionForThread() -> UInt32? + +@_silgen_name("CGSSetWindowBackgroundBlurRadius") +@discardableResult +func CGSSetWindowBackgroundBlurRadius( + _ connection: UInt32, + _ windowNum: NSInteger, + _ radius: Int +) -> OSStatus diff --git a/ChatMLX/Extensions/String+Extensions.swift b/ChatMLX/Core/Extensions/String+Extensions.swift similarity index 100% rename from ChatMLX/Extensions/String+Extensions.swift rename to ChatMLX/Core/Extensions/String+Extensions.swift diff --git a/ChatMLX/Extensions/TimeInterval+Extensions.swift b/ChatMLX/Core/Extensions/TimeInterval+Extensions.swift similarity index 100% rename from ChatMLX/Extensions/TimeInterval+Extensions.swift rename to ChatMLX/Core/Extensions/TimeInterval+Extensions.swift diff --git a/ChatMLX/Extensions/View+Extensions.swift b/ChatMLX/Core/Extensions/View+Extensions.swift similarity index 95% rename from ChatMLX/Extensions/View+Extensions.swift rename to ChatMLX/Core/Extensions/View+Extensions.swift index 7d77253..b49abcf 100644 --- a/ChatMLX/Extensions/View+Extensions.swift +++ b/ChatMLX/Core/Extensions/View+Extensions.swift @@ -45,7 +45,7 @@ extension View { .preference(key: SafeAreaInsetsKey.self, value: proxy.safeAreaInsets) } .onPreferenceChange(SafeAreaInsetsKey.self) { value in - logger.debug("\(id) insets:\(value)") + NSLog("\(id) insets:\(value)") } ) } diff --git a/ChatMLX/Models/AppleIntelligenceEffectDisplay.swift b/ChatMLX/Core/Models/AppleIntelligenceEffectDisplay.swift similarity index 100% rename from ChatMLX/Models/AppleIntelligenceEffectDisplay.swift rename to ChatMLX/Core/Models/AppleIntelligenceEffectDisplay.swift diff --git a/ChatMLX/Models/Conversation+CoreDataClass.swift b/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataClass.swift similarity index 59% rename from ChatMLX/Models/Conversation+CoreDataClass.swift rename to ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataClass.swift index 6557201..4b82b69 100644 --- a/ChatMLX/Models/Conversation+CoreDataClass.swift +++ b/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataClass.swift @@ -2,14 +2,14 @@ // Conversation+CoreDataClass.swift // ChatMLX // -// Created by John Mai on 2024/10/2. +// Created by John Mai on 2024/10/14. // // -import CoreData import Foundation +import CoreData @objc(Conversation) -public class Conversation: NSManagedObject { + class Conversation: NSManagedObject { } diff --git a/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataProperties.swift b/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataProperties.swift new file mode 100644 index 0000000..15a342f --- /dev/null +++ b/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataProperties.swift @@ -0,0 +1,74 @@ +// +// Conversation+CoreDataProperties.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// +// + +import CoreData +import Foundation + +extension Conversation { + @nonobjc class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "Conversation") + } + + @NSManaged var createdAt: Date? + @NSManaged var generateTime: Double + @NSManaged var inferring: Bool + @NSManaged var maxLength: Int64 + @NSManaged var maxMessagesLimit: Int32 + @NSManaged var promptTime: Double + @NSManaged var promptTokensPerSecond: Double + @NSManaged var repetitionContextSize: Int64 + @NSManaged var repetitionPenalty: Float + @NSManaged var systemPrompt: String? + @NSManaged var temperature: Float + @NSManaged var title: String + @NSManaged var tokensPerSecond: Double + @NSManaged var topP: Float + @NSManaged var updatedAt: Date? + @NSManaged var useMaxLength: Bool + @NSManaged var useMaxMessagesLimit: Bool + @NSManaged var useRepetitionPenalty: Bool + @NSManaged var useSystemPrompt: Bool + @NSManaged var messages: [Message] + @NSManaged var model: ModelInfo? +} + +// MARK: Generated accessors for messages + +extension Conversation { + @objc(insertObject:inMessagesAtIndex:) + @NSManaged func insertIntoMessages(_ value: Message, at idx: Int) + + @objc(removeObjectFromMessagesAtIndex:) + @NSManaged func removeFromMessages(at idx: Int) + + @objc(insertMessages:atIndexes:) + @NSManaged func insertIntoMessages(_ values: [Message], at indexes: NSIndexSet) + + @objc(removeMessagesAtIndexes:) + @NSManaged func removeFromMessages(at indexes: NSIndexSet) + + @objc(replaceObjectInMessagesAtIndex:withObject:) + @NSManaged func replaceMessages(at idx: Int, with value: Message) + + @objc(replaceMessagesAtIndexes:withMessages:) + @NSManaged func replaceMessages(at indexes: NSIndexSet, with values: [Message]) + + @objc(addMessagesObject:) + @NSManaged func addToMessages(_ value: Message) + + @objc(removeMessagesObject:) + @NSManaged func removeFromMessages(_ value: Message) + + @objc(addMessages:) + @NSManaged func addToMessages(_ values: NSOrderedSet) + + @objc(removeMessages:) + @NSManaged func removeFromMessages(_ values: NSOrderedSet) +} + +extension Conversation: Identifiable {} diff --git a/ChatMLX/Core/Models/CoreData Models/Conversation.swift b/ChatMLX/Core/Models/CoreData Models/Conversation.swift new file mode 100644 index 0000000..0a3a65e --- /dev/null +++ b/ChatMLX/Core/Models/CoreData Models/Conversation.swift @@ -0,0 +1,97 @@ +// +// Conversation.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// + +import CoreData +import Defaults + +extension Conversation { + override func awakeFromInsert() { + super.awakeFromInsert() + + setPrimitiveValue(Defaults[.defaultTitle], forKey: #keyPath(Conversation.title)) +// setPrimitiveValue(Defaults[.defaultModel], forKey: #keyPath(Conversation.model)) + + setPrimitiveValue(Defaults[.defaultTemperature], forKey: #keyPath(Conversation.temperature)) + setPrimitiveValue(Defaults[.defaultTopP], forKey: #keyPath(Conversation.topP)) + setPrimitiveValue( + Defaults[.defaultRepetitionContextSize], + forKey: #keyPath(Conversation.repetitionContextSize)) + + setPrimitiveValue( + Defaults[.defaultUseRepetitionPenalty], + forKey: #keyPath(Conversation.useRepetitionPenalty)) + setPrimitiveValue( + Defaults[.defaultRepetitionPenalty], forKey: #keyPath(Conversation.repetitionPenalty)) + + setPrimitiveValue( + Defaults[.defaultUseMaxLength], forKey: #keyPath(Conversation.useMaxLength)) + setPrimitiveValue(Defaults[.defaultMaxLength], forKey: #keyPath(Conversation.maxLength)) + setPrimitiveValue( + Defaults[.defaultMaxMessagesLimit], forKey: #keyPath(Conversation.maxMessagesLimit)) + setPrimitiveValue( + Defaults[.defaultUseMaxMessagesLimit], + forKey: #keyPath(Conversation.useMaxMessagesLimit)) + + setPrimitiveValue( + Defaults[.defaultUseSystemPrompt], forKey: #keyPath(Conversation.useSystemPrompt)) + setPrimitiveValue( + Defaults[.defaultSystemPrompt], forKey: #keyPath(Conversation.systemPrompt)) + + setPrimitiveValue(Date.now, forKey: #keyPath(Conversation.createdAt)) + setPrimitiveValue(Date.now, forKey: #keyPath(Conversation.updatedAt)) + } + + override func willSave() { + super.willSave() + setPrimitiveValue(Date.now, forKey: #keyPath(Conversation.updatedAt)) + } + + func getLastAssistantMessage(context: NSManagedObjectContext) -> Message { + if let message = messages.last, message.role == .assistant { + message + } else { + Message(context: context).assistant(conversation: self) + } + } + + func prepareMessages() -> [[String: String]] { + var messages = self.messages + if self.useMaxMessagesLimit { + let maxCount = self.maxMessagesLimit + 1 + if messages.count > maxCount { + messages = Array(messages.suffix(Int(maxCount))) + if messages.first?.role != .user { + messages = Array(messages.dropFirst()) + } + } + } + + var dictionary = messages[..<(messages.count - 1)].map { + message -> [String: String] in + message.format() + } + + if self.useSystemPrompt, let systemPrompt = self.systemPrompt,!systemPrompt.isEmpty { + dictionary.insert( + self.formatMessage( + role: .system, + content: systemPrompt), + at: 0) + } + + return dictionary + } + + private func formatMessage(role: Role, content: String) -> [String: String] { + [ + "role": role.rawValue, + "content": content, + ] + } +} + +extension Conversation: @unchecked Sendable {} diff --git a/ChatMLX/Core/Models/CoreData Models/Message+CoreDataClass.swift b/ChatMLX/Core/Models/CoreData Models/Message+CoreDataClass.swift new file mode 100644 index 0000000..f0b1bcf --- /dev/null +++ b/ChatMLX/Core/Models/CoreData Models/Message+CoreDataClass.swift @@ -0,0 +1,13 @@ +// +// Message+CoreDataClass.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// +// + +import CoreData +import Foundation + +@objc(Message) +class Message: NSManagedObject {} diff --git a/ChatMLX/Core/Models/CoreData Models/Message+CoreDataProperties.swift b/ChatMLX/Core/Models/CoreData Models/Message+CoreDataProperties.swift new file mode 100644 index 0000000..9f35cd9 --- /dev/null +++ b/ChatMLX/Core/Models/CoreData Models/Message+CoreDataProperties.swift @@ -0,0 +1,26 @@ +// +// Message+CoreDataProperties.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// +// + +import CoreData +import Foundation + +extension Message { + @nonobjc class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "Message") + } + + @NSManaged var content: String + @NSManaged var createdAt: Date? + @NSManaged var error: String? + @NSManaged var inferring: Bool + @NSManaged var roleRaw: String + @NSManaged var updatedAt: Date? + @NSManaged var conversation: Conversation +} + +extension Message: Identifiable {} diff --git a/ChatMLX/Models/Message+CoreDataClass.swift b/ChatMLX/Core/Models/CoreData Models/Message.swift similarity index 63% rename from ChatMLX/Models/Message+CoreDataClass.swift rename to ChatMLX/Core/Models/CoreData Models/Message.swift index b25bb91..6bddb56 100644 --- a/ChatMLX/Models/Message+CoreDataClass.swift +++ b/ChatMLX/Core/Models/CoreData Models/Message.swift @@ -1,16 +1,28 @@ // -// Message+CoreDataClass.swift +// Message.swift // ChatMLX // -// Created by John Mai on 2024/10/2. -// +// Created by John Mai on 2024/10/14. // -import CoreData import Foundation -@objc(Message) -public class Message: NSManagedObject { +extension Message { + var role: Role { + get { Role(rawValue: roleRaw) ?? .user } + set { roleRaw = newValue.rawValue } + } + + override func awakeFromInsert() { + setPrimitiveValue(Date.now, forKey: #keyPath(Message.createdAt)) + setPrimitiveValue(Date.now, forKey: #keyPath(Message.updatedAt)) + } + + override func willSave() { + super.willSave() + setPrimitiveValue(Date.now, forKey: #keyPath(Message.updatedAt)) + } + @discardableResult func user(content: String, conversation: Conversation?) -> Self { self.role = .user @@ -50,3 +62,5 @@ public class Message: NSManagedObject { return Array(messages[index...]) } } + +extension Message: @unchecked Sendable {} diff --git a/ChatMLX/Core/Models/CoreData Models/ModelInfo+CoreDataClass.swift b/ChatMLX/Core/Models/CoreData Models/ModelInfo+CoreDataClass.swift new file mode 100644 index 0000000..38a6943 --- /dev/null +++ b/ChatMLX/Core/Models/CoreData Models/ModelInfo+CoreDataClass.swift @@ -0,0 +1,13 @@ +// +// ModelInfo+CoreDataClass.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// +// + +import CoreData +import Foundation + +@objc(ModelInfo) +class ModelInfo: NSManagedObject {} diff --git a/ChatMLX/Core/Models/CoreData Models/ModelInfo+CoreDataProperties.swift b/ChatMLX/Core/Models/CoreData Models/ModelInfo+CoreDataProperties.swift new file mode 100644 index 0000000..4af2e75 --- /dev/null +++ b/ChatMLX/Core/Models/CoreData Models/ModelInfo+CoreDataProperties.swift @@ -0,0 +1,43 @@ +// +// ModelInfo+CoreDataProperties.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// +// + +import CoreData +import Foundation + +extension ModelInfo { + @nonobjc class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "ModelInfo") + } + + @NSManaged var id: String + @NSManaged var providerRaw: String + @NSManaged var name: String? + @NSManaged var path: URL? + @NSManaged var maxInputLength: Int + @NSManaged var maxOutputLength: Int + @NSManaged var toolCall: Bool + @NSManaged var vision: Bool +} + +// MARK: Generated accessors for conversation + +extension ModelInfo { + @objc(addConversationObject:) + @NSManaged func addToConversation(_ value: Conversation) + + @objc(removeConversationObject:) + @NSManaged func removeFromConversation(_ value: Conversation) + + @objc(addConversation:) + @NSManaged func addToConversation(_ values: NSSet) + + @objc(removeConversation:) + @NSManaged func removeFromConversation(_ values: NSSet) +} + +extension ModelInfo: Identifiable {} diff --git a/ChatMLX/Core/Models/CoreData Models/ModelInfo.swift b/ChatMLX/Core/Models/CoreData Models/ModelInfo.swift new file mode 100644 index 0000000..d3396f1 --- /dev/null +++ b/ChatMLX/Core/Models/CoreData Models/ModelInfo.swift @@ -0,0 +1,15 @@ +// +// ModelInfo.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// + +extension ModelInfo { + var provider: Provider { + get { Provider(rawValue: providerRaw) ?? .mlx } + set { providerRaw = newValue.rawValue } + } +} + +extension ModelInfo: @unchecked Sendable {} diff --git a/ChatMLX/Models/DisplayStyle.swift b/ChatMLX/Core/Models/DisplayStyle.swift similarity index 100% rename from ChatMLX/Models/DisplayStyle.swift rename to ChatMLX/Core/Models/DisplayStyle.swift diff --git a/ChatMLX/Models/DownloadTask.swift b/ChatMLX/Core/Models/DownloadTask.swift similarity index 96% rename from ChatMLX/Models/DownloadTask.swift rename to ChatMLX/Core/Models/DownloadTask.swift index 02ac2fe..e366242 100644 --- a/ChatMLX/Models/DownloadTask.swift +++ b/ChatMLX/Core/Models/DownloadTask.swift @@ -7,11 +7,11 @@ import Defaults import Foundation -import Logging +import os @Observable class DownloadTask: Identifiable, Equatable { - let logger = Logger(label: Bundle.main.bundleIdentifier!) + private let logger = Logger(subsystem: "com.johnmai.ChatMLX", category: "DownloadTask") static func == (lhs: DownloadTask, rhs: DownloadTask) -> Bool { lhs.id == rhs.id diff --git a/ChatMLX/Models/Language.swift b/ChatMLX/Core/Models/Language.swift similarity index 100% rename from ChatMLX/Models/Language.swift rename to ChatMLX/Core/Models/Language.swift diff --git a/ChatMLX/Models/LocalModel.swift b/ChatMLX/Core/Models/LocalModel.swift similarity index 100% rename from ChatMLX/Models/LocalModel.swift rename to ChatMLX/Core/Models/LocalModel.swift diff --git a/ChatMLX/Models/LocalModelGroup.swift b/ChatMLX/Core/Models/LocalModelGroup.swift similarity index 100% rename from ChatMLX/Models/LocalModelGroup.swift rename to ChatMLX/Core/Models/LocalModelGroup.swift diff --git a/ChatMLX/Core/Models/Migrations/V2MigrationPolicy.swift b/ChatMLX/Core/Models/Migrations/V2MigrationPolicy.swift new file mode 100644 index 0000000..5eb27ec --- /dev/null +++ b/ChatMLX/Core/Models/Migrations/V2MigrationPolicy.swift @@ -0,0 +1,30 @@ +// +// V2MigrationPolicy.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// + +import CoreData + +class V2MigrationPolicy: NSEntityMigrationPolicy { + override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws { + let sourceKeys = sInstance.entity.attributesByName.keys + let sourceValues = sInstance.dictionaryWithValues(forKeys: sourceKeys.map { $0 as String }) + + let destinationInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext) + let destinationKeys = destinationInstance.entity.attributesByName.keys.map { $0 as String } + + for key in destinationKeys { + if let value = sourceValues[key] { + destinationInstance.setValue(value, forKey: key) + } + } + + if sourceValues["model"] is String { + destinationInstance.setValue(nil, forKey: "model") + } + + manager.associate(sourceInstance: sInstance, withDestinationInstance: destinationInstance, for: mapping) + } +} diff --git a/ChatMLX/Core/Models/ModelType.swift b/ChatMLX/Core/Models/ModelType.swift new file mode 100644 index 0000000..1e8d650 --- /dev/null +++ b/ChatMLX/Core/Models/ModelType.swift @@ -0,0 +1,29 @@ +// +// TaskType.swift +// ChatMLX +// +// Created by John Mai on 2024/10/20. +// + +import Foundation +import SwiftUI + +enum ModelType: String { + case textGeneration = "text-generation" + case imageTextToText = "image-text-to-text" + case anyToAny = "any-to-any" + case unknown + + var name: LocalizedStringKey { + switch self { + case .textGeneration: + "Text Generation" + case .imageTextToText: + "Image Text to Text (Vision)" + case .anyToAny: + "Any to Any" + case .unknown: + "Unknown" + } + } +} diff --git a/ChatMLX/Core/Models/Provider.swift b/ChatMLX/Core/Models/Provider.swift new file mode 100644 index 0000000..550a455 --- /dev/null +++ b/ChatMLX/Core/Models/Provider.swift @@ -0,0 +1,12 @@ +// +// Provider.swift +// ChatMLX +// +// Created by John Mai on 2024/10/21. +// +import Defaults + +enum Provider: String, Codable, Defaults.Serializable { + case mlx = "MLX" + case openAI = "OpenAI" +} diff --git a/ChatMLX/Core/Models/ProviderModel.swift b/ChatMLX/Core/Models/ProviderModel.swift new file mode 100644 index 0000000..4f812b0 --- /dev/null +++ b/ChatMLX/Core/Models/ProviderModel.swift @@ -0,0 +1,56 @@ +// +// ProviderModel.swift +// ChatMLX +// +// Created by John Mai on 2024/10/14. +// + +import Foundation + +struct ProviderModel { + let id: String + let provider: Provider + let name: String? + let path: URL? + let maxInputLength: Int? + let maxOutputLength: Int? + let toolCall: Bool + let vision: Bool + + init( + id: String, + provider: Provider, + name: String? = nil, + path: URL? = nil, + maxInputLength: Int? = nil, + maxOutputLength: Int? = nil, + toolCall: Bool = false, + vision: Bool = false + ) { + self.id = id + self.provider = provider + self.name = name + self.path = path + self.maxInputLength = maxInputLength + self.maxOutputLength = maxOutputLength + self.toolCall = toolCall + self.vision = vision + } +} + +extension ProviderModel: Hashable {} + +extension ProviderModel { + init(from modelInfo: ModelInfo) { + self.init( + id: modelInfo.id, + provider: modelInfo.provider, + name: modelInfo.name, + path: modelInfo.path, + maxInputLength: Int(modelInfo.maxInputLength), + maxOutputLength: Int(modelInfo.maxOutputLength), + toolCall: modelInfo.toolCall, + vision: modelInfo.vision + ) + } +} diff --git a/ChatMLX/Models/RemoteModel.swift b/ChatMLX/Core/Models/RemoteModel.swift similarity index 100% rename from ChatMLX/Models/RemoteModel.swift rename to ChatMLX/Core/Models/RemoteModel.swift diff --git a/ChatMLX/Models/Role.swift b/ChatMLX/Core/Models/Role.swift similarity index 84% rename from ChatMLX/Models/Role.swift rename to ChatMLX/Core/Models/Role.swift index 51b5df9..af7123e 100644 --- a/ChatMLX/Models/Role.swift +++ b/ChatMLX/Core/Models/Role.swift @@ -5,7 +5,7 @@ // Created by John Mai on 2024/10/3. // -public enum Role: String, Codable { +enum Role: String, Codable { case user case assistant case system diff --git a/ChatMLX/Models/SettingsTab.swift b/ChatMLX/Core/Models/SettingsTab.swift similarity index 100% rename from ChatMLX/Models/SettingsTab.swift rename to ChatMLX/Core/Models/SettingsTab.swift diff --git a/ChatMLX/Models/SettingsTabGroup.swift b/ChatMLX/Core/Models/SettingsTabGroup.swift similarity index 100% rename from ChatMLX/Models/SettingsTabGroup.swift rename to ChatMLX/Core/Models/SettingsTabGroup.swift diff --git a/ChatMLX/Models/Styles.swift b/ChatMLX/Core/Models/Styles.swift similarity index 100% rename from ChatMLX/Models/Styles.swift rename to ChatMLX/Core/Models/Styles.swift diff --git a/ChatMLX/Core/Services/AI/BaseProvider.swift b/ChatMLX/Core/Services/AI/BaseProvider.swift new file mode 100644 index 0000000..42c306d --- /dev/null +++ b/ChatMLX/Core/Services/AI/BaseProvider.swift @@ -0,0 +1,71 @@ +// +// BaseProvider.swift +// ChatMLX +// +// Created by John Mai on 2024/10/13. +// + +import Foundation +import MLXLLM + +struct ModelConfig { + struct Model { + var name: String + var path: URL? + } + + let model: Model + let temperature: Double? = nil + let useMaxTokens: Bool = false + let maxTokens: Int? = nil + let topP: Double? = nil + let frequencyPenalty: Double? = nil + let repetitionContextSize: Int? = nil + let presencePenalty: Double? = nil + let stop: [String]? = nil +} + +extension GenerateParameters { + init(from config: ModelConfig) { + self.init( + temperature: Float(config.temperature ?? 0.6), + topP: Float(config.topP ?? 1.0), + repetitionPenalty: config.frequencyPenalty.map(Float.init), + repetitionContextSize: config.repetitionContextSize ?? 20 + ) + } +} + +extension ModelConfig: Sendable {} + +struct Usage { + let promptTokens: Int? + let promptTime: Double? + let promptTokensPerSecond: Double? + let completionTokens: Int? + let completionTime: Double? + let completionTokensPerSecond: Double? + let totalTokens: Int? +} + +struct ChatResult { + let content: String + let usage: Usage? +} + +typealias ChatResultHandler = (Result) -> Void + +//enum ProviderModel { +// case name(String) +// case path(URL) +//} + +protocol BaseProvider { + func chat( + messages: [[String: String]], + config: ModelConfig, + onResult: @escaping ChatResultHandler + ) async + + static func fetchModels() throws -> [ProviderModel] +} diff --git a/ChatMLX/Core/Services/AI/MLXProvider.swift b/ChatMLX/Core/Services/AI/MLXProvider.swift new file mode 100644 index 0000000..58fdd8b --- /dev/null +++ b/ChatMLX/Core/Services/AI/MLXProvider.swift @@ -0,0 +1,157 @@ +// +// MLXProvider.swift +// ChatMLX +// +// Created by John Mai on 2024/10/13. +// + +import Defaults +import Metal +import MLX +import MLXLLM +import MLXRandom +import os +import SwiftUI +import Tokenizers + +struct MLXProvider: BaseProvider { + // MARK: - Properties + + private let displayEveryNTokens = 4 + private let maxTokens = 240 + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MLXProvider") + private let fileManager = FileManager.default + + // MARK: - Chat + + func chat( + messages: [[String: String]], + config: ModelConfig, + onResult: @escaping ChatResultHandler + ) async { + do { + guard let modelPath = config.model.path else { + throw LLMRunnerError.modelConfigurationNotSet + } + + let modelContainer = try await MLXLLM.ModelContainer( + hub: .init(), + modelDirectory: modelPath, + configuration: ModelConfiguration(directory: modelPath) + ) + + let tokens = try await modelContainer.perform { _, tokenizer in + try tokenizer.applyChatTemplate(messages: messages) + } + + MLXRandom.seed(UInt64(Date.timeIntervalSinceReferenceDate * 1000)) + + let result = await modelContainer.perform { + model, tokenizer in + MLXLLM.generate( + promptTokens: tokens, + parameters: GenerateParameters(from: config), + model: model, + tokenizer: tokenizer, + extraEOSTokens: Set(config.stop ?? [ + "<|im_end|>", + "<|end|>", + ]) + ) { tokens in + if tokens.count % displayEveryNTokens == 0 { + let content = tokenizer.decode(tokens: tokens) + onResult( + .success( + .init( + content: content, + usage: nil + ) + ) + ) + } + + if config.useMaxTokens, tokens.count >= config.maxTokens ?? maxTokens { + return .stop + } else { + return .more + } + } + } + + onResult( + .success( + .init( + content: result.output, + usage: .init( + promptTokens: result.promptTokens.count, + promptTime: result.promptTime, + promptTokensPerSecond: result.promptTokensPerSecond, + completionTokens: result.tokens.count, + completionTime: result.generateTime, + completionTokensPerSecond: result.tokensPerSecond, + totalTokens: result.tokens.count + result.promptTokens.count + ) + ) + ) + ) + } catch { + onResult(.failure(error)) + } + } + + // MARK: - Fetch Models + + static func fetchModels() throws -> [ProviderModel] { + let fileManager = FileManager.default + + guard let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { + return [] + } + + let organizations = try fileManager.contentsOfDirectory( + at: documentsURL.appendingPathComponent("huggingface/models"), + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + + guard !organizations.isEmpty else { + return [] + } + + return try organizations.flatMap { organization -> [ProviderModel] in + guard organization.hasDirectoryPath else { + return [] + } + + let huggingfaceModels = try fileManager.contentsOfDirectory( + at: organization, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + + return try huggingfaceModels.compactMap { model -> ProviderModel? in + guard model.hasDirectoryPath else { + return nil + } + + let data = try Data(contentsOf: model.appendingPathComponent("tokenizer_config.json")) + let tokenizerConfig = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + + let modelMaxLength = tokenizerConfig?["model_max_length"] as? Int + let toolCall = (tokenizerConfig?["chat_template"] as? String)?.contains("tool_calls") ?? false + let vision = false + + return .init( + id: model.absoluteString, + provider: .mlx, + name: model.lastPathComponent, + path: model, + maxInputLength: modelMaxLength, + maxOutputLength: modelMaxLength, + toolCall: toolCall, + vision: false + ) + } + } + } +} diff --git a/ChatMLX/Core/Services/AI/OpenAIProvider.swift b/ChatMLX/Core/Services/AI/OpenAIProvider.swift new file mode 100644 index 0000000..b1c320d --- /dev/null +++ b/ChatMLX/Core/Services/AI/OpenAIProvider.swift @@ -0,0 +1,108 @@ +// +// OpenAIProvider.swift +// ChatMLX +// +// Created by John Mai on 2024/10/17. +// + +import Defaults +import Foundation +import OpenAI +import SwiftUI + +struct OpenAIProvider: BaseProvider { + static func fetchModels() -> [ProviderModel] { + [ + // GPT-4o + .init( + id: "gpt-4o", + provider: .openAI, + name: "GPT-4o", + maxInputLength: 128000, + maxOutputLength: 16384, + toolCall: true, + vision: true + ), + // GPT-4o mini + .init( + id: "gpt-4o-mini", + provider: .openAI, + name: "GPT-4o Mini", + maxInputLength: 128000, + maxOutputLength: 16384, + toolCall: true, + vision: true + ), + // o1-preview and o1-mini + .init( + id: "o1-preview", + provider: .openAI, + name: "O1 Preview", + maxInputLength: 128000, + maxOutputLength: 32768 + ), + .init( + id: "o1-mini", + provider: .openAI, + name: "O1 Mini", + maxInputLength: 128000, + maxOutputLength: 65536 + ), + // GPT-4 Turbo and GPT-4 + .init( + id: "gpt-4-turbo", + provider: .openAI, + name: "GPT-4 Turbo", + maxInputLength: 128000, + maxOutputLength: 4096 + ), + .init( + id: "gpt-4", + provider: .openAI, + name: "GPT-4", + maxInputLength: 8192, + maxOutputLength: 8192 + ), + // GPT-3.5 Turbo + .init( + id: "gpt-3.5-turbo", + provider: .openAI, + name: "GPT-3.5 Turbo", + maxInputLength: 16385, + maxOutputLength: 4096 + ) + ] + } + + func chat( + messages: [[String: String]], + config: ModelConfig, + onResult: @escaping ChatResultHandler + ) async { + let apiKey = Defaults[.openAIApiKey] + let baseURL = Defaults[.openAIBaseURL] + + let client = OpenAI(configuration: .init(token: apiKey, host: baseURL)) + + var chatMessages: [ChatQuery.ChatCompletionMessageParam] = [] + for message in messages { + chatMessages.append(.init(role: message["role"] == "user" ? .user : .assistant, content: message["content"]!)!) + } + + let query = ChatQuery(messages: chatMessages, model: config.model.name) + + do { + var content = "" + for try await result in client.chatsStream(query: query) { + for choice in result.choices { + if let c = choice.delta.content { + content += c + } + onResult(.success(.init(content: content, usage: nil))) + } + } + } catch { + onResult(.failure(error)) + } + } +} diff --git a/ChatMLX/Core/Services/AI/ProviderFactory.swift b/ChatMLX/Core/Services/AI/ProviderFactory.swift new file mode 100644 index 0000000..56955f7 --- /dev/null +++ b/ChatMLX/Core/Services/AI/ProviderFactory.swift @@ -0,0 +1,45 @@ +// +// ProviderFactory.swift +// ChatMLX +// +// Created by John Mai on 2024/10/13. +// + +import Defaults + +actor ProviderFactory { + static let shared = ProviderFactory() + + private init() {} + + private var providers: [Provider: BaseProvider] = [:] + + func provider(_ type: Provider) async -> BaseProvider { + if let provider = providers[type] { + return provider + } + + let provider: BaseProvider = switch type { + case .mlx: + MLXProvider() + case .openAI: + OpenAIProvider() + } + + providers[type] = provider + + return provider + } + +// func fetchModels() -> [ProviderModel] { +// var models: [ProviderModel] = [] +// +// models = models + MLXProvider.fetchModels() +// +// if Defaults[.enableOpenAI] { +// models = models + OpenAIProvider.fetchModels() +// } +// +// return models +// } +} diff --git a/ChatMLX/Core/Utilities/Common.swift b/ChatMLX/Core/Utilities/Common.swift new file mode 100644 index 0000000..650438d --- /dev/null +++ b/ChatMLX/Core/Utilities/Common.swift @@ -0,0 +1,12 @@ +// +// Common.swift +// ChatMLX +// +// Created by John Mai on 2024/10/15. +// + +import Foundation + +func getVersion() -> String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" +} diff --git a/ChatMLX/Utilities/Huggingface/Downloader.swift b/ChatMLX/Core/Utilities/Huggingface/Downloader.swift similarity index 100% rename from ChatMLX/Utilities/Huggingface/Downloader.swift rename to ChatMLX/Core/Utilities/Huggingface/Downloader.swift diff --git a/ChatMLX/Utilities/Huggingface/Hub.swift b/ChatMLX/Core/Utilities/Huggingface/Hub.swift similarity index 100% rename from ChatMLX/Utilities/Huggingface/Hub.swift rename to ChatMLX/Core/Utilities/Huggingface/Hub.swift diff --git a/ChatMLX/Utilities/Huggingface/HubApi.swift b/ChatMLX/Core/Utilities/Huggingface/HubApi.swift similarity index 100% rename from ChatMLX/Utilities/Huggingface/HubApi.swift rename to ChatMLX/Core/Utilities/Huggingface/HubApi.swift diff --git a/ChatMLX/Utilities/LLMRunner.swift b/ChatMLX/Core/Utilities/LLMRunner.swift similarity index 79% rename from ChatMLX/Utilities/LLMRunner.swift rename to ChatMLX/Core/Utilities/LLMRunner.swift index 7b2bdf2..b63bb9d 100644 --- a/ChatMLX/Utilities/LLMRunner.swift +++ b/ChatMLX/Core/Utilities/LLMRunner.swift @@ -12,12 +12,15 @@ import MLXLLM import MLXRandom import SwiftUI import Tokenizers +import os @Observable @MainActor class LLMRunner { var running = false - var model: String? + var model: ModelInfo? + + let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "LLMRunner") enum LoadState { case idle @@ -53,8 +56,12 @@ class LLMRunner { MLX.GPU.set(memoryLimit: memoryLimit) } - let modelContainer = try await MLXLLM.loadModelContainer( - configuration: modelConfiguration +// let modelContainer = try await MLXLLM.loadModelContainer( +// configuration: modelConfiguration +// ) + + let modelContainer = try await MLXLLM.ModelContainer( + hub: .init(), modelDirectory: URL(fileURLWithPath: "/Users/john/Downloads/Llama-3.2-1B-Instruct-4bit"), configuration: modelConfiguration ) withAnimation { @@ -70,11 +77,11 @@ class LLMRunner { } private func switchModel(_ conversation: Conversation) { - if conversation.model != modelConfiguration?.name { - loadState = .idle - modelConfiguration = ModelConfiguration.configuration( - id: conversation.model) - } +// if conversation.model != modelConfiguration?.name { +// loadState = .idle +// modelConfiguration = ModelConfiguration.configuration( +// id: conversation.model) +// } } func prepare(_ conversation: Conversation) -> [[String: String]] { @@ -94,15 +101,15 @@ class LLMRunner { message.format() } - if conversation.useSystemPrompt, !conversation.systemPrompt.isEmpty { - dictionary.insert( - formatMessage( - role: .system, - content: conversation.systemPrompt - ), - at: 0 - ) - } +// if conversation.useSystemPrompt, !conversation.systemPrompt.isEmpty { +// dictionary.insert( +// formatMessage( +// role: .system, +// content: conversation.systemPrompt +// ), +// at: 0 +// ) +// } return dictionary } @@ -114,6 +121,27 @@ class LLMRunner { ] } +// func generate2( +// conversation: Conversation, +// in context: NSManagedObjectContext, +// progressing: @escaping () -> Void = {}, +// completion: (() -> Void)? +// ) { +// let service = ChatService() +// +// Task{ +// await service.chat( +// model: .init( +// name: "Llama-3.2mo-1B-Instruct-4bit", +// path: URL(fileURLWithPath: "Llama-3.2-1B-Instruct-4bit"), +// ), +// conversation: conversation, +// context: context +// ) +// +// } +// } + func generate( conversation: Conversation, in context: NSManagedObjectContext, @@ -202,7 +230,7 @@ class LLMRunner { } } } catch { - logger.error("LLM Generate Failed: \(error.localizedDescription)") + logger.error("LLM Generate Failed: \(error)") await MainActor.run { assistantMessage.inferring = false assistantMessage.error = error.localizedDescription diff --git a/ChatMLX/Utilities/Logger.swift b/ChatMLX/Core/Utilities/Logger.swift similarity index 56% rename from ChatMLX/Utilities/Logger.swift rename to ChatMLX/Core/Utilities/Logger.swift index 098a56a..2e721e8 100644 --- a/ChatMLX/Utilities/Logger.swift +++ b/ChatMLX/Core/Utilities/Logger.swift @@ -6,6 +6,6 @@ // import Foundation -import Logging -let logger = Logger(label: Bundle.main.bundleIdentifier!) + +//let logger = Logger(label: Bundle.main.bundleIdentifier!) diff --git a/ChatMLX/Core/Utilities/MarkdownMetadata.swift b/ChatMLX/Core/Utilities/MarkdownMetadata.swift new file mode 100644 index 0000000..2509f06 --- /dev/null +++ b/ChatMLX/Core/Utilities/MarkdownMetadata.swift @@ -0,0 +1,39 @@ +// +// MarkdownMetadata.swift +// ChatMLX +// +// Created by John Mai on 2024/10/10. +// + +import Foundation + +struct MarkdownMetadata { + var metadata: [String: String] = [:] + + init(markdown: String) { + let lines = markdown.components(separatedBy: .newlines) + var isMetadata = false + var metadataLines: [String] = [] + var contentLines: [String] = [] + + for line in lines { + if line.trimmingCharacters(in: .whitespaces) == "---" { + isMetadata.toggle() + continue + } + + if isMetadata { + metadataLines.append(line) + } else { + contentLines.append(line) + } + } + + for line in metadataLines { + let parts = line.split(separator: ":", maxSplits: 1).map { $0.trimmingCharacters(in: .whitespaces) } + if parts.count == 2 { + metadata[parts[0]] = parts[1] + } + } + } +} diff --git a/ChatMLX/Core/Utilities/PersistenceController.swift b/ChatMLX/Core/Utilities/PersistenceController.swift new file mode 100644 index 0000000..53ae752 --- /dev/null +++ b/ChatMLX/Core/Utilities/PersistenceController.swift @@ -0,0 +1,131 @@ +// +// Persistence.swift +// ChatMLX +// +// Created by John Mai on 2024/10/2. +// + +import CoreData + +struct PersistenceController { + static let shared = PersistenceController() + + @MainActor + static let preview: PersistenceController = { + let controller = PersistenceController(inMemory: true) + return controller + }() + + let container: NSPersistentContainer + + init(inMemory: Bool = false) { + container = NSPersistentContainer(name: "ChatMLX") + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + + container.viewContext.automaticallyMergesChangesFromParent = true + + if let description = container.persistentStoreDescriptions.first { + description.shouldMigrateStoreAutomatically = true + description.shouldInferMappingModelAutomatically = false + } + + container.loadPersistentStores(completionHandler: { _, error in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + } + + func newBackgroundContext() -> NSManagedObjectContext { + let context = container.newBackgroundContext() + context.automaticallyMergesChangesFromParent = true + context.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump + return context + } + + var viewContext: NSManagedObjectContext { + container.viewContext + } + +// func exisits( +// _ model: T, +// in context: NSManagedObjectContext +// ) -> T? { +// try? context.existingObject(with: model.objectID) as? T +// } +// +// func delete(_ model: some NSManagedObject) throws { +// if let existingContact = exisits(model, in: container.viewContext) { +// container.viewContext.delete(existingContact) +// Task(priority: .background) { +// try await container.viewContext.perform { +// try container.viewContext.save() +// } +// } +// } +// } +// +// func clear(_ entityName: String) throws -> [NSManagedObjectID] { +// let fetchRequest: NSFetchRequest = NSFetchRequest( +// entityName: entityName) +// let batchDeteleRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) +// batchDeteleRequest.resultType = .resultTypeObjectIDs +// +// if let fetchResult = try container.viewContext.execute(batchDeteleRequest) +// as? NSBatchDeleteResult, +// let deletedManagedObjectIds = fetchResult.result as? [NSManagedObjectID], +// !deletedManagedObjectIds.isEmpty +// { +// return deletedManagedObjectIds +// } +// +// return [] +// } +// + func save() throws { + Task.detached(priority: .background) { + try await viewContext.saveIfNeeded() + } + } + + func executeAndMergeChanges(using request: NSBatchDeleteRequest, in context: NSManagedObjectContext) throws { + try executeAndMergeChanges(using: [request], in: context) + } + + func executeAndMergeChanges(using requests: [NSBatchDeleteRequest], in context: NSManagedObjectContext) throws { + let changes = try requests.flatMap { try execute(request: $0, in: context) } + mergeChanges(changes) + } + + private func execute(request: NSBatchDeleteRequest, in context: NSManagedObjectContext) throws -> [NSManagedObjectID] { + request.resultType = .resultTypeObjectIDs + let result = try context.execute(request) as? NSBatchDeleteResult + return result?.result as? [NSManagedObjectID] ?? [] + } + + private func mergeChanges( + _ changes: [NSManagedObjectID], + in context: NSManagedObjectContext? = nil + ) { + guard !changes.isEmpty else { return } + + NSManagedObjectContext.mergeChanges( + fromRemoteContextSave: [NSDeletedObjectsKey: changes], + into: [context ?? viewContext] + ) + } + + func delete( + _ id: NSManagedObjectID, + in context: NSManagedObjectContext, + saveImmediately: Bool = true + ) throws { + let object = try context.existingObject(with: id) + context.delete(object) + if saveImmediately { + try save() + } + } +} diff --git a/ChatMLX/Extensions/NSWindow+Extensions.swift b/ChatMLX/Extensions/NSWindow+Extensions.swift deleted file mode 100644 index d4217bb..0000000 --- a/ChatMLX/Extensions/NSWindow+Extensions.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// NSWindow+Extensions.swift -// ChatMLX -// -// Created by John Mai on 2024/8/3. -// - -import SwiftUI - -extension NSWindow { - /// Sets the background blur of the window with a specified radius. - /// - Parameter radius: The blur radius to apply. - func setBackgroundBlur(radius: Int, color: NSColor = .black.withAlphaComponent(0.4)) { - guard let connection = try? getCGSConnection() else { - logger.error("Failed to get CGS connection") - return - } - - let status = CGSSetWindowBackgroundBlurRadius(connection, windowNumber, radius) - if status != noErr { - logger.error("Error setting blur radius: \(status)") - } - - backgroundColor = color - ignoresMouseEvents = false - } -} - -// MARK: - Private APIs and Helper Functions - -typealias CGSConnectionID = UInt32 - -@_silgen_name("CGSDefaultConnectionForThread") -func CGSDefaultConnectionForThread() -> CGSConnectionID? - -@_silgen_name("CGSSetWindowBackgroundBlurRadius") @discardableResult -func CGSSetWindowBackgroundBlurRadius( - _ connection: CGSConnectionID, - _ windowNum: NSInteger, - _ radius: Int -) -> OSStatus - -extension NSWindow { - /// Attempts to get the default CGS connection for the current thread. - /// - Returns: A `CGSConnectionID` if successful, `nil` otherwise. - fileprivate func getCGSConnection() throws -> CGSConnectionID { - guard let connection = CGSDefaultConnectionForThread() else { - throw NSError( - domain: "com.Luminare.NSWindow", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Unable to get CGS connection"] - ) - } - return connection - } -} diff --git a/ChatMLX/Features/Conversation/ConversationDetailView.swift b/ChatMLX/Features/Conversation/ConversationDetailView.swift index 0ac072a..a1896ca 100644 --- a/ChatMLX/Features/Conversation/ConversationDetailView.swift +++ b/ChatMLX/Features/Conversation/ConversationDetailView.swift @@ -14,28 +14,32 @@ import SwiftUI struct ConversationDetailView: View { @ObservedObject var conversation: Conversation - + @Environment(LLMRunner.self) var runner @Environment(\.managedObjectContext) private var viewContext - @Environment(ConversationViewModel.self) private var vm - + @Environment(ConversationViewModel.self) private var conversationViewModel + @Environment(ModelManagerViewModel.self) private var modelManagerViewModel + @State private var newMessage = "" @State private var showRightSidebar = false @State private var showInfoPopover = false - @State private var localModels: [LocalModel] = [] @State private var displayStyle: DisplayStyle = .markdown @State private var isEditorFullScreen = false @State private var showToast = false @State private var toastMessage = "" @State private var toastType: AlertToast.AlertType = .regular - @State private var loading = true @State private var scrollViewProxy: ScrollViewProxy? - + @FocusState private var isInputFocused: Bool - + @Default(.enableAppleIntelligenceEffect) var enableAppleIntelligenceEffect @Default(.appleIntelligenceEffectDisplay) var appleIntelligenceEffectDisplay - + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ModelInfo.name, ascending: true)], + animation: .default + ) var models: FetchedResults + var body: some View { ZStack(alignment: .trailing) { VStack(spacing: 0) { @@ -45,7 +49,7 @@ struct ConversationDetailView: View { } Editor() } - + if showRightSidebar { Color.black.opacity(0.00001) .ignoresSafeArea() @@ -54,33 +58,31 @@ struct ConversationDetailView: View { showRightSidebar = false } } - + RightSidebarView(conversation: conversation) } } - .onAppear(perform: loadModels) .toast(isPresenting: $showToast, duration: 1.5, offsetY: 30) { AlertToast( displayMode: .hud, type: toastType, title: toastMessage ) } .ultramanNavigationTitle( - LocalizedStringKey(conversation.title) + conversation.title ) .ultramanToolbar(alignment: .trailing) { Button(action: { withAnimation { showRightSidebar.toggle() } - + }) { Image(systemName: "slider.horizontal.3") } .buttonStyle(.plain) } } - - @MainActor + @ViewBuilder private func MessageBox() -> some View { ScrollViewReader { proxy in @@ -107,18 +109,17 @@ struct ConversationDetailView: View { } } } - + private func scrollToBottom() { guard let lastMessageId = conversation.messages.last?.id, let scrollViewProxy else { return } - + withAnimation { scrollViewProxy.scrollTo(lastMessageId, anchor: .bottom) } } - - @MainActor + @ViewBuilder private func EditorToolbar() -> some View { HStack { @@ -129,13 +130,13 @@ struct ConversationDetailView: View { } label: { Image(displayStyle == .markdown ? "plaintext" : "markdown") } - + Button(action: { conversation.messages = [] }) { Image("clear") } - + Button { withAnimation { isEditorFullScreen.toggle() @@ -147,9 +148,9 @@ struct ConversationDetailView: View { : "arrow.up.left.and.arrow.down.right") } .help(isEditorFullScreen ? "Exit Full Screen" : "Enter Full Screen") - + Spacer() - + Button { showInfoPopover.toggle() } label: { @@ -175,21 +176,21 @@ struct ConversationDetailView: View { Text("Prompt Time") .fontWeight(.bold) } - + LabeledContent { Text("\(Int(conversation.promptTokensPerSecond))") } label: { Text("Prompt Tokens/second") .fontWeight(.bold) } - + LabeledContent { Text(conversation.generateTime.formatted()) } label: { Text("Generate Time") .fontWeight(.bold) } - + LabeledContent { Text("\(Int(conversation.tokensPerSecond))") } label: { @@ -200,30 +201,8 @@ struct ConversationDetailView: View { .padding() .background(.clear) } - - Image(systemName: "circle.fill") - .controlSize(.mini) - .foregroundStyle( - runner.modelConfiguration?.name == conversation.model - ? .green : .red - ) - .symbolEffect(.variableColor, isActive: runner.running) - .help("Model State") - - Picker( - selection: $conversation.model, - label: Image(systemName: "brain") - ) { - if !loading { - Text("Not selected").tag("") - ForEach(localModels, id: \.id) { model in - Text(model.name) - .tag(model.origin) - } - } - } - .pickerStyle(.menu) - .labelsHidden() + + ModelPicker(selection: $conversation.model) } .buttonStyle(.borderless) .foregroundStyle(.white) @@ -231,13 +210,12 @@ struct ConversationDetailView: View { .frame(height: 35) .padding(.horizontal, 10) } - - @MainActor + @ViewBuilder private func Editor() -> some View { VStack(alignment: .leading, spacing: 0) { EditorToolbar() - + ZStack(alignment: .bottom) { UltramanTextEditor( text: $newMessage, @@ -245,7 +223,7 @@ struct ConversationDetailView: View { onSubmit: sendMessage ) .padding(.horizontal, 5) - + HStack(spacing: 16) { Spacer() Button("Clear") { @@ -253,7 +231,7 @@ struct ConversationDetailView: View { } .buttonStyle(.borderless) .disabled(newMessage.isEmpty) - + Button { sendMessage() } label: { @@ -283,86 +261,77 @@ struct ConversationDetailView: View { .frame(maxHeight: isEditorFullScreen ? .infinity : 150) } } - + private func sendMessage() { + guard !conversation.inferring else { + return + } + let trimmedMessage = newMessage.trimmingCharacters( in: .whitespacesAndNewlines) guard !trimmedMessage.isEmpty else { return } - - if conversation.model.isEmpty { - showToastMessage("Please select a model", type: .error(Color.red)) + + guard let model = conversation.model else { + showToastMessage("Please select a model", type: .error(.red)) return } - + newMessage = "" isInputFocused = false - + Message(context: viewContext).user(content: trimmedMessage, conversation: conversation) - + if enableAppleIntelligenceEffect, appleIntelligenceEffectDisplay == .global { AppleIntelligenceEffectManager.shared.setupEffect() } - - runner.generate(conversation: conversation, in: viewContext) { - Task { @MainActor in - scrollToBottom() - } - } completion: { - Task { @MainActor in - if enableAppleIntelligenceEffect, appleIntelligenceEffectDisplay == .global { - AppleIntelligenceEffectManager.shared.closeEffect() - } - scrollToBottom() - } - } - } - - private func loadModels() { - let fileManager = FileManager.default - let documentsURL = fileManager.urls( - for: .documentDirectory, in: .userDomainMask - )[0] - let modelsURL = documentsURL.appendingPathComponent( - "huggingface/models") - - do { - let contents = try fileManager.contentsOfDirectory( - at: modelsURL, includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - var models: [LocalModel] = [] - - for groupURL in contents { - if groupURL.hasDirectoryPath { - let modelContents = try fileManager.contentsOfDirectory( - at: groupURL, includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] + + conversation.inferring = true + + let factory = ProviderFactory.shared + + let assistantMessage = conversation.getLastAssistantMessage(context: viewContext) + assistantMessage.inferring = true + let messages = conversation.prepareMessages() + +#if DEBUG + print(messages) +#endif + + Task { + let provider = await factory.provider(.openAI) + await provider.chat( + messages: messages, + config: .init( + model: .init( + name: "model.name", + path: model.path ) - - for modelURL in modelContents { - if modelURL.hasDirectoryPath { - models.append( - LocalModel( - group: groupURL.lastPathComponent, - name: modelURL.lastPathComponent, - url: modelURL - ) - ) - } + ) + ) { result in + switch result { + case .success(let response): + Task { @MainActor in + assistantMessage.content = response.content + } + case .failure(let error): + Task { @MainActor in + assistantMessage.error = error.localizedDescription } } } - - if !models.contains(where: { $0.origin == conversation.model }) { - conversation.model = "" + + await MainActor.run { + conversation.inferring = false + assistantMessage.inferring = false } - - Task { @MainActor in - localModels = models - loading = false + + if enableAppleIntelligenceEffect, appleIntelligenceEffectDisplay == .global { + AppleIntelligenceEffectManager.shared.closeEffect() + } + + if viewContext.hasChanges { + try? viewContext.save() } - } catch { - vm.throwError(error, title: "Load Models Failed") } } diff --git a/ChatMLX/Features/Conversation/ConversationSidebarItem.swift b/ChatMLX/Features/Conversation/ConversationSidebarItem.swift index 982017d..49ef6b8 100644 --- a/ChatMLX/Features/Conversation/ConversationSidebarItem.swift +++ b/ChatMLX/Features/Conversation/ConversationSidebarItem.swift @@ -15,6 +15,8 @@ struct ConversationSidebarItem: View { @State private var isActive: Bool = false + let persistence = PersistenceController.shared + var body: some View { Button(action: selectConversation) { VStack(alignment: .leading, spacing: 4) { @@ -29,7 +31,7 @@ struct ConversationSidebarItem: View { Spacer() if !(conversation.isFault || conversation.isDeleted) { - Text(conversation.updatedAt.toFormatted()) + Text(conversation.updatedAt?.toFormatted() ?? "") .font(.caption) } } @@ -59,7 +61,10 @@ struct ConversationSidebarItem: View { private func deleteConversation() { do { - try PersistenceController.shared.delete(conversation) + try persistence.delete(conversation.objectID, in: viewContext) + if vm.selectedConversation == conversation { + vm.selectedConversation = nil + } } catch { vm.throwError(error, title: "Delete Conversation Failed") } diff --git a/ChatMLX/Features/Conversation/ConversationSidebarView.swift b/ChatMLX/Features/Conversation/ConversationSidebarView.swift index 4e6c588..d8b3f0e 100644 --- a/ChatMLX/Features/Conversation/ConversationSidebarView.swift +++ b/ChatMLX/Features/Conversation/ConversationSidebarView.swift @@ -35,7 +35,6 @@ struct ConversationSidebarView: View { .background(.black.opacity(0.4)) } - @MainActor @ViewBuilder private func headerView() -> some View { HStack { @@ -52,7 +51,6 @@ struct ConversationSidebarView: View { .buttonStyle(.plain) } - @MainActor @ViewBuilder private func logoView() -> some View { HStack { @@ -67,7 +65,6 @@ struct ConversationSidebarView: View { } } - @MainActor @ViewBuilder private func searchField() -> some View { LuminareSection { @@ -81,7 +78,6 @@ struct ConversationSidebarView: View { .padding(.horizontal, padding) } - @MainActor @ViewBuilder private func conversationList() -> some View { ScrollView { diff --git a/ChatMLX/Features/Conversation/ConversationView.swift b/ChatMLX/Features/Conversation/ConversationView.swift index ea77c0c..64e1a5d 100644 --- a/ChatMLX/Features/Conversation/ConversationView.swift +++ b/ChatMLX/Features/Conversation/ConversationView.swift @@ -40,7 +40,6 @@ struct ConversationView: View { } } - @MainActor @ViewBuilder private func detailView() -> some View { Group { diff --git a/ChatMLX/Features/Conversation/MessageBubbleView.swift b/ChatMLX/Features/Conversation/MessageBubbleView.swift index 462b026..faf965d 100644 --- a/ChatMLX/Features/Conversation/MessageBubbleView.swift +++ b/ChatMLX/Features/Conversation/MessageBubbleView.swift @@ -36,7 +36,6 @@ struct MessageBubbleView: View { } } - @MainActor @ViewBuilder private var assistantMessageView: some View { HStack(alignment: .top, spacing: 12) { @@ -87,7 +86,7 @@ struct MessageBubbleView: View { .disabled(runner.running) if !(message.isFault || message.isDeleted) { - Text(message.updatedAt.toTimeFormatted()) + Text(message.updatedAt?.toTimeFormatted() ?? "") .font(.caption) } @@ -108,7 +107,6 @@ struct MessageBubbleView: View { } } - @MainActor @ViewBuilder private var userMessageView: some View { VStack(alignment: .trailing) { @@ -119,7 +117,7 @@ struct MessageBubbleView: View { .cornerRadius(8) HStack { - Text(message.updatedAt.toTimeFormatted()) + Text(message.updatedAt?.toTimeFormatted() ?? "") .font(.caption) Button(action: copyText) { @@ -160,16 +158,55 @@ struct MessageBubbleView: View { private func regenerate() { guard message.role == .assistant else { return } - Task { - let conversation = message.conversation + let conversation = message.conversation + + if conversation.messages.last != message { + for message in message.suffixMessages() { + viewContext.delete(message) + } + } + guard let model = conversation.model else { + return + } + + conversation.inferring = true + + let factory = ProviderFactory.shared + + let assistantMessage = conversation.getLastAssistantMessage(context: viewContext) + assistantMessage.inferring = true + let messages = conversation.prepareMessages() - if conversation.messages.last != message { - for message in message.suffixMessages() { - viewContext.delete(message) + Task { + let provider = await factory.provider(model.provider) + await provider.chat( + messages: messages, + config: .init( + model: .init( + name: "", + path: model.path + ) + ) + ) { result in + switch result { + case .success(let response): + Task { @MainActor in + assistantMessage.content = response.content + } + case .failure(let error): + Task { @MainActor in + assistantMessage.error = error.localizedDescription + } } } + await MainActor.run { - runner.generate(conversation: conversation, in: viewContext, completion: nil) + conversation.inferring = false + assistantMessage.inferring = false + } + + if viewContext.hasChanges { + try? viewContext.save() } } } diff --git a/ChatMLX/Features/Conversation/RightSidebarView.swift b/ChatMLX/Features/Conversation/RightSidebarView.swift index 286df35..ca1b8ad 100644 --- a/ChatMLX/Features/Conversation/RightSidebarView.swift +++ b/ChatMLX/Features/Conversation/RightSidebarView.swift @@ -169,7 +169,7 @@ struct RightSidebarView: View { if conversation.useSystemPrompt { UltramanTextEditor( - text: $conversation.systemPrompt, + text: $conversation.systemPrompt.toUnwrapped(defaultValue: ""), placeholder: "System prompt", onSubmit: {} ) diff --git a/ChatMLX/Features/Settings/AboutView.swift b/ChatMLX/Features/Settings/AboutView.swift index 0bffc12..bdb0dee 100644 --- a/ChatMLX/Features/Settings/AboutView.swift +++ b/ChatMLX/Features/Settings/AboutView.swift @@ -25,7 +25,7 @@ struct AboutView: View { .fontWeight(.bold) Text( - "Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown")" + "Version \(getVersion())" ) .font(.subheadline) .foregroundColor(.white) diff --git a/ChatMLX/Features/Settings/DefaultConversationView.swift b/ChatMLX/Features/Settings/DefaultConversationView.swift index 8addebe..c003ad9 100644 --- a/ChatMLX/Features/Settings/DefaultConversationView.swift +++ b/ChatMLX/Features/Settings/DefaultConversationView.swift @@ -12,7 +12,8 @@ import SwiftUI struct DefaultConversationView: View { @Default(.defaultTitle) var defaultTitle - @Default(.defaultModel) var defaultModel +// @Default(.defaultModel) var defaultModel + @State var defaultModel: ModelInfo? @Default(.defaultTemperature) var defaultTemperature @Default(.defaultTopP) var defaultTopP @Default(.defaultMaxLength) var defaultMaxLength @@ -25,6 +26,8 @@ struct DefaultConversationView: View { @Default(.defaultUseSystemPrompt) var defaultUseSystemPrompt @Default(.defaultSystemPrompt) var defaultSystemPrompt + @Default(.defaultProvider) var defaultProvider + @State private var localModels: [LocalModel] = [] @Environment(SettingsViewModel.self) var vm @@ -39,24 +42,17 @@ struct DefaultConversationView: View { $defaultTitle, placeholder: Text("Default conversation title") ) - .frame(height: 25) + .frame(minHeight: 35) } LuminareSection("Model Settings") { + LabeledContent("Provider") { + DefaultProviderPicker(provider: $defaultProvider) + } + LabeledContent("Model") { - Picker( - selection: $defaultModel, - label: Image(systemName: "brain") - ) { - if !localModels.isEmpty { - Text("Not selected").tag("") - ForEach(localModels, id: \.id) { model in - Text(model.name).tag(model.origin) - } - } - } + DefaultModelPicker(provider: $defaultProvider) } - .padding(padding) LabeledContent("Temperature") { CompactSlider( @@ -67,7 +63,6 @@ struct DefaultConversationView: View { } .frame(width: 200) } - .padding(padding) LabeledContent("Top P") { CompactSlider( @@ -78,12 +73,10 @@ struct DefaultConversationView: View { } .frame(width: 200) } - .padding(padding) LabeledContent("Use Max Length") { Toggle("", isOn: $defaultUseMaxLength) } - .padding(padding) if defaultUseMaxLength { LabeledContent("Max Length") { @@ -95,7 +88,6 @@ struct DefaultConversationView: View { } .frame(width: 200) } - .padding(padding) } LabeledContent("Repetition Context Size") { @@ -107,12 +99,10 @@ struct DefaultConversationView: View { } .frame(width: 200) } - .padding(padding) LabeledContent("Use Repetition Penalty") { Toggle("", isOn: $defaultUseRepetitionPenalty) } - .padding(padding) if defaultUseRepetitionPenalty { LabeledContent("Repetition Penalty") { @@ -127,7 +117,6 @@ struct DefaultConversationView: View { } .frame(width: 200) } - .padding(padding) } } @@ -135,7 +124,6 @@ struct DefaultConversationView: View { LabeledContent("Use Max Messages Limit") { Toggle("", isOn: $defaultUseMaxMessagesLimit) } - .padding(padding) if defaultUseMaxMessagesLimit { LabeledContent("Max Messages Limit") { @@ -147,7 +135,6 @@ struct DefaultConversationView: View { } .frame(width: 200) } - .padding(padding) } } @@ -155,7 +142,6 @@ struct DefaultConversationView: View { LabeledContent("Use System Prompt") { Toggle("", isOn: $defaultUseSystemPrompt) } - .padding(padding) if defaultUseSystemPrompt { UltramanTextEditor( @@ -164,7 +150,6 @@ struct DefaultConversationView: View { onSubmit: {} ) .frame(height: 100) - .padding(padding) } } @@ -180,7 +165,6 @@ struct DefaultConversationView: View { .labelsHidden() .buttonStyle(.borderless) .foregroundStyle(.white) - .tint(.white) .toggleStyle(.switch) } @@ -219,10 +203,10 @@ struct DefaultConversationView: View { } } } - - if !models.contains(where: { $0.origin == defaultModel }) { - defaultModel = "" - } +// +// if !models.contains(where: { $0.origin == defaultModel }) { +// defaultModel = "" +// } Task { @MainActor in localModels = models diff --git a/ChatMLX/Features/Settings/DownloadManager/DownloadManagerView.swift b/ChatMLX/Features/Settings/Download Manager/DownloadManagerView.swift similarity index 100% rename from ChatMLX/Features/Settings/DownloadManager/DownloadManagerView.swift rename to ChatMLX/Features/Settings/Download Manager/DownloadManagerView.swift diff --git a/ChatMLX/Features/Settings/DownloadManager/DownloadTaskView.swift b/ChatMLX/Features/Settings/Download Manager/DownloadTaskView.swift similarity index 100% rename from ChatMLX/Features/Settings/DownloadManager/DownloadTaskView.swift rename to ChatMLX/Features/Settings/Download Manager/DownloadTaskView.swift diff --git a/ChatMLX/Features/Settings/ExperimentalFeaturesView.swift b/ChatMLX/Features/Settings/ExperimentalFeaturesView.swift index 918a9f8..c2ae9b0 100644 --- a/ChatMLX/Features/Settings/ExperimentalFeaturesView.swift +++ b/ChatMLX/Features/Settings/ExperimentalFeaturesView.swift @@ -22,7 +22,6 @@ struct ExperimentalFeaturesView: View { Toggle("", isOn: $enableAppleIntelligenceEffect) .toggleStyle(.switch) } - .padding(6) if enableAppleIntelligenceEffect { LabeledContent("Display Mode") { @@ -39,7 +38,6 @@ struct ExperimentalFeaturesView: View { .foregroundStyle(.white) .tint(.white) } - .padding(8) } } .labeledContentStyle(.horizontal) diff --git a/ChatMLX/Features/Settings/GeneralView.swift b/ChatMLX/Features/Settings/GeneralView.swift index 464b9e8..2620543 100644 --- a/ChatMLX/Features/Settings/GeneralView.swift +++ b/ChatMLX/Features/Settings/GeneralView.swift @@ -26,6 +26,8 @@ struct GeneralView: View { let maxRAM = ProcessInfo.processInfo.physicalMemory / (1024 * 1024) + let persistenceController = PersistenceController.shared + var body: some View { VStack(spacing: 18) { LuminareSection("Language") { @@ -42,7 +44,6 @@ struct GeneralView: View { .foregroundStyle(.white) .tint(.white) } - .padding(8) } LuminareSection("Window Appearance") { @@ -54,12 +55,10 @@ struct GeneralView: View { .frame(width: 200) .compactSliderSecondaryColor(.white) } - .padding(5) LabeledContent("Color") { ColorPicker("", selection: $backgroundColor) } - .padding(5) } LuminareSection("System Settings") { @@ -83,22 +82,24 @@ struct GeneralView: View { } private func clearAllConversations() { - do { - let persistenceController = PersistenceController.shared - - let messageObjectIds = try persistenceController.clear("Message") - let conversationObjectIds = try persistenceController.clear("Conversation") - - NSManagedObjectContext.mergeChanges( - fromRemoteContextSave: [ - NSDeletedObjectsKey: messageObjectIds + conversationObjectIds - ], - into: [persistenceController.container.viewContext] - ) - - conversationViewModel.selectedConversation = nil - } catch { - vm.throwError(error, title: "Clear All Conversations Failed") + let context = persistenceController.newBackgroundContext() + + Task.detached { + do { + try await context.perform { + try persistenceController.executeAndMergeChanges(using: [ + NSBatchDeleteRequest(fetchRequest: Message.fetchRequest()), + NSBatchDeleteRequest(fetchRequest: Conversation.fetchRequest()) + ], in: context) + } + await MainActor.run { + conversationViewModel.selectedConversation = nil + } + } catch { + await MainActor.run { + vm.throwError(error, title: "Clear All Conversations Failed") + } + } } } } diff --git a/ChatMLX/Features/Settings/HuggingFaceView.swift b/ChatMLX/Features/Settings/HuggingFaceView.swift index d376927..3c657d3 100644 --- a/ChatMLX/Features/Settings/HuggingFaceView.swift +++ b/ChatMLX/Features/Settings/HuggingFaceView.swift @@ -29,9 +29,7 @@ struct HuggingFaceView: View { placeholder: Text("Enter your Hugging Face token"), alignment: .trailing ) - .frame(height: 25) } - .padding(5) } LuminareSection("Hugging Face Endpoint") { @@ -49,12 +47,10 @@ struct HuggingFaceView: View { "https://hf-mirror.com") } } - .padding(8) LabeledContent("Use Custom Endpoint") { Toggle("", isOn: $useCustomEndpoint) } - .padding(8) if useCustomEndpoint { LabeledContent("Custom Endpoint") { @@ -68,7 +64,6 @@ struct HuggingFaceView: View { addCustomEndpoint() } } - .padding(5) if !customEndpoints.isEmpty { List { diff --git a/ChatMLX/Features/Settings/LocalModels/LocalModelItemView.swift b/ChatMLX/Features/Settings/Local Models/LocalModelItemView.swift similarity index 100% rename from ChatMLX/Features/Settings/LocalModels/LocalModelItemView.swift rename to ChatMLX/Features/Settings/Local Models/LocalModelItemView.swift diff --git a/ChatMLX/Features/Settings/Local Models/LocalModelsView.swift b/ChatMLX/Features/Settings/Local Models/LocalModelsView.swift new file mode 100644 index 0000000..29f18b8 --- /dev/null +++ b/ChatMLX/Features/Settings/Local Models/LocalModelsView.swift @@ -0,0 +1,136 @@ +//// +//// LocalModelsView.swift +//// ChatMLX +//// +//// Created by John Mai on 2024/8/10. +//// +// +//import Defaults +//import SwiftUI +// +//struct LocalModelsView: View { +// @State private var modelGroups: [LocalModelGroup] = [] +//// @Default(.defaultModel) var defaultModel +// +// @Environment(SettingsViewModel.self) var vm +// +// var body: some View { +// List { +// ForEach(modelGroups.indices, id: \.self) { groupIndex in +// Section( +// header: Text(modelGroups[groupIndex].name).font( +// .title2.bold()) +// ) { +// ForEach(modelGroups[groupIndex].models.indices, id: \.self) { modelIndex in +// LocalModelItemView( +// model: $modelGroups[groupIndex].models[modelIndex], +// onDelete: { +// Task { +// deleteModel( +// at: IndexSet(integer: modelIndex), +// from: groupIndex) +// loadModels() +// } +// }) +// } +// .onDelete { offsets in +// Task { +// deleteModel(at: offsets, from: groupIndex) +// loadModels() +// } +// } +// } +// } +// } +// .onAppear(perform: loadModels) +// .scrollContentBackground(.hidden) +// .listStyle(SidebarListStyle()) +// .ultramanNavigationTitle("Models") +// .ultramanToolbar { +// Button(action: openModelsDirectory) { +// Image(systemName: "folder") +// } +// .buttonStyle(.plain) +// } +// } +// +// private func loadModels() { +// let fileManager = FileManager.default +// let documentsURL = fileManager.urls( +// for: .documentDirectory, in: .userDomainMask)[0] +// let modelsURL = documentsURL.appendingPathComponent( +// "huggingface/models") +// +// do { +// let contents = try fileManager.contentsOfDirectory( +// at: modelsURL, includingPropertiesForKeys: nil, +// options: [.skipsHiddenFiles]) +// var groups: [LocalModelGroup] = [] +// +// for groupURL in contents { +// if groupURL.hasDirectoryPath { +// let groupName = groupURL.lastPathComponent +// var models: [LocalModel] = [] +// +// let modelContents = try fileManager.contentsOfDirectory( +// at: groupURL, includingPropertiesForKeys: nil, +// options: [.skipsHiddenFiles]) +// +// for modelURL in modelContents { +// if modelURL.hasDirectoryPath { +// let modelName = modelURL.lastPathComponent +// models.append( +// LocalModel( +// group: groupName, +// name: modelName, +// url: modelURL) +// ) +// } +// } +// +// groups.append( +// LocalModelGroup(name: groupName, models: models)) +// } +// } +// +//// if !groups.contains(where: { +//// $0.models.contains(where: { $0 == defaultModel }) +//// }) { +//// defaultModel = nil +//// } +// +// Task { @MainActor in +// modelGroups = groups +// } +// } catch { +// vm.throwError(error, title: "Load Models Failed") +// } +// } +// +// private func deleteModel(at offsets: IndexSet, from group: Int) { +//// let fileManager = FileManager.default +//// +//// for index in offsets { +//// let model = modelGroups[group].models[index] +//// do { +//// try fileManager.removeItem(at: model.url) +//// modelGroups[group].models.remove(at: index) +//// if defaultModel == model.origin { +//// defaultModel = "" +//// } +//// } catch { +//// vm.throwError(error, title: "Delete Model Failed") +//// } +//// } +// } +// +// private func openModelsDirectory() { +// let fileManager = FileManager.default +// let documentsURL = fileManager.urls( +// for: .documentDirectory, in: .userDomainMask)[0] +// let modelsURL = documentsURL.appendingPathComponent( +// "huggingface/models") +// +// NSWorkspace.shared.open(modelsURL) +// } +//} diff --git a/ChatMLX/Features/Settings/LocalModels/LocalModelsView.swift b/ChatMLX/Features/Settings/LocalModels/LocalModelsView.swift deleted file mode 100644 index 43e3159..0000000 --- a/ChatMLX/Features/Settings/LocalModels/LocalModelsView.swift +++ /dev/null @@ -1,136 +0,0 @@ -// -// LocalModelsView.swift -// ChatMLX -// -// Created by John Mai on 2024/8/10. -// - -import Defaults -import SwiftUI - -struct LocalModelsView: View { - @State private var modelGroups: [LocalModelGroup] = [] - @Default(.defaultModel) var defaultModel - - @Environment(SettingsViewModel.self) var vm - - var body: some View { - List { - ForEach(modelGroups.indices, id: \.self) { groupIndex in - Section( - header: Text(modelGroups[groupIndex].name).font( - .title2.bold()) - ) { - ForEach(modelGroups[groupIndex].models.indices, id: \.self) { modelIndex in - LocalModelItemView( - model: $modelGroups[groupIndex].models[modelIndex], - onDelete: { - Task { - deleteModel( - at: IndexSet(integer: modelIndex), - from: groupIndex) - loadModels() - } - }) - } - .onDelete { offsets in - Task { - deleteModel(at: offsets, from: groupIndex) - loadModels() - } - } - } - } - } - .onAppear(perform: loadModels) - .scrollContentBackground(.hidden) - .listStyle(SidebarListStyle()) - .ultramanNavigationTitle("Models") - .ultramanToolbar { - Button(action: openModelsDirectory) { - Image(systemName: "folder") - } - .buttonStyle(.plain) - } - } - - private func loadModels() { - let fileManager = FileManager.default - let documentsURL = fileManager.urls( - for: .documentDirectory, in: .userDomainMask)[0] - let modelsURL = documentsURL.appendingPathComponent( - "huggingface/models") - - do { - let contents = try fileManager.contentsOfDirectory( - at: modelsURL, includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles]) - var groups: [LocalModelGroup] = [] - - for groupURL in contents { - if groupURL.hasDirectoryPath { - let groupName = groupURL.lastPathComponent - var models: [LocalModel] = [] - - let modelContents = try fileManager.contentsOfDirectory( - at: groupURL, includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles]) - - for modelURL in modelContents { - if modelURL.hasDirectoryPath { - let modelName = modelURL.lastPathComponent - models.append( - LocalModel( - group: groupName, - name: modelName, - url: modelURL) - ) - } - } - - groups.append( - LocalModelGroup(name: groupName, models: models)) - } - } - - if !groups.contains(where: { - $0.models.contains(where: { $0.origin == defaultModel }) - }) { - defaultModel = "" - } - - Task { @MainActor in - modelGroups = groups - } - } catch { - vm.throwError(error, title: "Load Models Failed") - } - } - - private func deleteModel(at offsets: IndexSet, from group: Int) { - let fileManager = FileManager.default - - for index in offsets { - let model = modelGroups[group].models[index] - do { - try fileManager.removeItem(at: model.url) - modelGroups[group].models.remove(at: index) - if defaultModel == model.origin { - defaultModel = "" - } - } catch { - vm.throwError(error, title: "Delete Model Failed") - } - } - } - - private func openModelsDirectory() { - let fileManager = FileManager.default - let documentsURL = fileManager.urls( - for: .documentDirectory, in: .userDomainMask)[0] - let modelsURL = documentsURL.appendingPathComponent( - "huggingface/models") - - NSWorkspace.shared.open(modelsURL) - } -} diff --git a/ChatMLX/Features/Settings/MLXCommunity/MLXCommunityItemView.swift b/ChatMLX/Features/Settings/MLX Community/MLXCommunityItemView.swift similarity index 100% rename from ChatMLX/Features/Settings/MLXCommunity/MLXCommunityItemView.swift rename to ChatMLX/Features/Settings/MLX Community/MLXCommunityItemView.swift diff --git a/ChatMLX/Features/Settings/MLXCommunity/MLXCommunityView.swift b/ChatMLX/Features/Settings/MLX Community/MLXCommunityView.swift similarity index 98% rename from ChatMLX/Features/Settings/MLXCommunity/MLXCommunityView.swift rename to ChatMLX/Features/Settings/MLX Community/MLXCommunityView.swift index 5c4127c..e9e0bed 100644 --- a/ChatMLX/Features/Settings/MLXCommunity/MLXCommunityView.swift +++ b/ChatMLX/Features/Settings/MLX Community/MLXCommunityView.swift @@ -8,6 +8,7 @@ import Alamofire import Luminare import SwiftUI +import os struct MLXCommunityView: View { @Environment(SettingsViewModel.self) var settingsViewModel @@ -19,6 +20,9 @@ struct MLXCommunityView: View { @State var status: Status = .isLoading private let sessionManager: Session + + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MLXCommunityView") + enum Status { case isLoading @@ -80,7 +84,6 @@ struct MLXCommunityView: View { } } - @MainActor @ViewBuilder var lastRowView: some View { ZStack(alignment: .center) { diff --git a/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift b/ChatMLX/Features/Settings/Model Manager/ModelManagerView.swift similarity index 79% rename from ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift rename to ChatMLX/Features/Settings/Model Manager/ModelManagerView.swift index 71ba85c..0cff9bb 100644 --- a/ChatMLX/Features/Settings/ModelManager/ModelManagerView.swift +++ b/ChatMLX/Features/Settings/Model Manager/ModelManagerView.swift @@ -12,13 +12,11 @@ struct ModelManagerView: View { var body: some View { ScrollView { LazyVStack { - MLXProvider() - OpenAIProvider() - + MLXProviderView() + OpenAIProviderView() } .padding() } - .ultramanNavigationTitle("Model Manager") } } diff --git a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/GPUMemorySettingsView.swift b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/GPUMemorySettingsView.swift new file mode 100644 index 0000000..3d56142 --- /dev/null +++ b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/GPUMemorySettingsView.swift @@ -0,0 +1,42 @@ +// +// GPUMemorySettingsView.swift +// ChatMLX +// +// Created by John Mai on 2024/10/26. +// + +import CompactSlider +import Defaults +import SwiftUI + +struct GPUMemorySettingsView: View { + @Default(.enableGPUMemorySettings) var enableGPUMemorySettings + @Default(.gpuCacheLimit) var gpuCacheLimit + @Default(.gpuMemoryLimit) var gpuMemoryLimit + + let maxRAM = ProcessInfo.processInfo.physicalMemory / (1024 * 1024) + + var body: some View { + if enableGPUMemorySettings { + LabeledContent("GPU Cache Limit") { + CompactSlider( + value: $gpuCacheLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 + ) { + Text("\(Int(gpuCacheLimit))MB") + .foregroundStyle(.white) + } + .frame(width: 200) + } + + LabeledContent("GPU Memory Limit") { + CompactSlider( + value: $gpuMemoryLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 + ) { + Text("\(Int(gpuMemoryLimit))MB") + .foregroundStyle(.white) + } + .frame(width: 200) + } + } + } +} diff --git a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderContentView.swift b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderContentView.swift new file mode 100644 index 0000000..f879cc2 --- /dev/null +++ b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderContentView.swift @@ -0,0 +1,175 @@ +// +// MLXProviderContentView.swift +// ChatMLX +// +// Created by John Mai on 2024/10/26. +// + +import CompactSlider +import Defaults +import Luminare +import os +import SwiftUI + +struct MLXProviderContentView: View { + // MARK: - Properties + + let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MLXProviderContentView") + let persistenceController = PersistenceController.shared + + // MARK: - State + + @State private var isPresentedImport = false + @State var providerModels: [ProviderModel] = [] + + // MARK: - Environment + + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.appError) private var appError + + // MARK: - Fetch Request + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \ModelInfo.name, ascending: true)], + predicate: NSPredicate(format: "providerRaw == %@", Provider.mlx.rawValue), + animation: .default + ) var models: FetchedResults + + // MARK: - User Defaults + + @Default(.enableGPUMemorySettings) var enableGPUMemorySettings + + var body: some View { + DividedVStack { + LabeledToggle(title: "Enable GPU Memory Settings", isOn: $enableGPUMemorySettings) + GPUMemorySettingsView() + modelListView() + } + .labeledContentStyle(.horizontal) + } +} + +// MARK: - Views + +extension MLXProviderContentView { + // MARK: - Model List View + + @ViewBuilder + private func modelListView() -> some View { + LabeledContent { + List { + ForEach(providerModels, id: \.self) { + MLXProviderModelItemView( + model: $0, + onDelete: onDelete + ) + } + .padding(.horizontal, -8) + } + .listStyle(.plain) + .scrollIndicators(.hidden) + .frame(height: 200) + .scrollContentBackground(.hidden) + } label: { + HStack { + Text("Model List") + Spacer() + Button(action: { + isPresentedImport = true + }) { + Label("Import", systemImage: "plus.circle") + .padding(5) + } + .buttonStyle(LuminareCompactButtonStyle(extraCompact: true)) + .fileImporter( + isPresented: $isPresentedImport, + allowedContentTypes: [.folder], + allowsMultipleSelection: false + ) { result in + handleImport(result) + } + } + .frame(height: 35) + + } + .padding(.horizontal) + .labeledContentStyle(.vertical) + .compactSliderSecondaryColor(.white) + .task { + try! await loadProviderModels() + } + } +} + +// MARK: - Private Methods + +extension MLXProviderContentView { + private func handleImport(_ result: Result<[URL], Error>) { + Task { + do { + switch result { + case .success(let urls): + for url in urls { + let model = ModelInfo(context: viewContext) + model.id = url.absoluteString + model.name = url.lastPathComponent + model.path = url + model.provider = .mlx + } + try viewContext.save() + try await loadProviderModels() + case .failure(let error): + throw error + } + } catch { + appError(error) + } + } + } + + // MARK: - Delete + + private func onDelete(_ model: ProviderModel) { + Task { + let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + + if let path = model.path { + if path.standardized.path.hasPrefix(documentsURL.appendingPathComponent("huggingface/models").standardized.path) { + try? FileManager.default.removeItem(at: path) + } else { + if let model = models.first(where: { $0.id == model.id }) { + viewContext.delete(model) + try? viewContext.save() + } + } + try? await loadProviderModels() + } + } + } + + // MARK: - Open Directory + + private func openDirectory(_ path: URL?) { + if let path { + NSWorkspace.shared.open(path) + } + } + + // MARK: - Load Provider Models + + private func loadProviderModels() async throws { + var models = try MLXProvider.fetchModels() + + for model in self.models { + if let index = models.firstIndex(where: { $0.id == model.id }) { + models[index] = ProviderModel(from: model) + } else { + models.append(ProviderModel(from: model)) + } + } + + await MainActor.run { + providerModels = models + } + } +} diff --git a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderLabelView.swift b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderLabelView.swift new file mode 100644 index 0000000..0f92e75 --- /dev/null +++ b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderLabelView.swift @@ -0,0 +1,20 @@ +// +// MLXProviderLabelView.swift +// ChatMLX +// +// Created by John Mai on 2024/10/26. +// + +import SwiftUI + +struct MLXProviderLabelView: View { + var body: some View { + HStack(spacing: 0) { + Text("ML") + .foregroundStyle(.black) + Text("X") + .foregroundStyle(Color(hex: "#D5D5D5")) + } + .font(.title2.weight(.medium)) + } +} diff --git a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderModelItemView.swift b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderModelItemView.swift new file mode 100644 index 0000000..325ad3e --- /dev/null +++ b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderModelItemView.swift @@ -0,0 +1,43 @@ +// +// MLXProviderModelItemView.swift +// ChatMLX +// +// Created by John Mai on 2024/10/26. +// +import SwiftUI +struct MLXProviderModelItemView: View { + let model: ProviderModel + let onDelete: (ProviderModel) -> Void + + var body: some View { + HStack { + Text(model.name ?? model.id) + Spacer() + Button(action: { + openDirectory(model.path) + }) { + Image(systemName: "folder") + } + + Button(action: { + onDelete(model) + }) { + Image(systemName: "trash") + .renderingMode(.original) + } + } + .buttonStyle(.plain) + .padding() + .background(.black.opacity(0.3)) + .listRowSeparator(.hidden) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: .black, radius: 2) +// .listRowInsets(EdgeInsets(top: 0, leading: -5, bottom: 10, trailing: -5)) + } + + private func openDirectory(_ path: URL?) { + if let path { + NSWorkspace.shared.open(path) + } + } +} diff --git a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderView.swift b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderView.swift new file mode 100644 index 0000000..0fbc5c4 --- /dev/null +++ b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderView.swift @@ -0,0 +1,18 @@ +// +// MLXProvider.swift +// ChatMLX +// +// Created by John Mai on 2024/10/4. +// + +import SwiftUI + +struct MLXProviderView: View { + var body: some View { + ProviderView(isExpanded: true, isEnabled: .constant(nil)) { + MLXProviderLabelView() + } content: { + MLXProviderContentView() + } + } +} diff --git a/ChatMLX/Features/Settings/ModelManager/Providers/OpenAIProvider.swift b/ChatMLX/Features/Settings/Model Manager/Providers/OpenAI/OpenAIProviderView.swift similarity index 51% rename from ChatMLX/Features/Settings/ModelManager/Providers/OpenAIProvider.swift rename to ChatMLX/Features/Settings/Model Manager/Providers/OpenAI/OpenAIProviderView.swift index 9220412..1f8b50d 100644 --- a/ChatMLX/Features/Settings/ModelManager/Providers/OpenAIProvider.swift +++ b/ChatMLX/Features/Settings/Model Manager/Providers/OpenAI/OpenAIProviderView.swift @@ -5,14 +5,23 @@ // Created by John Mai on 2024/10/4. // +import Defaults +import Luminare import SwiftUI -struct OpenAIProvider: View { +struct OpenAIProviderView: View { @State var isExpanded: Bool = false @State var isEnabled: Bool? = false + @Default(.enableOpenAI) var enableOpenAI + var body: some View { - ProviderView(isExpanded: $isExpanded, isEnabled: $isEnabled) { + ProviderView( + isEnabled: Binding( + get: { enableOpenAI }, + set: { enableOpenAI = $0 ?? false } + ) + ) { Label() } content: { Content() @@ -33,13 +42,16 @@ struct OpenAIProvider: View { @ViewBuilder func Content() -> some View { - VStack { - Text("👋🏻 It will be supported soon ~ ") - .font(.title2) - .fontWeight(.medium) - Text("🍻 If you have ideas, welcome to contribute.") - .font(.subheadline) + DividedVStack { + // API Key + LabeledContent("API Key") { + SecureField("API Key", text: .constant("")) + } + + // API 代理地址 + LabeledContent("API Proxy Address") { + TextField("API Proxy Address", text: .constant("")) + } } - .italic() } } diff --git a/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift b/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift deleted file mode 100644 index 31e47b4..0000000 --- a/ChatMLX/Features/Settings/ModelManager/Providers/MLXProvider.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// MLXProvider.swift -// ChatMLX -// -// Created by John Mai on 2024/10/4. -// - -import CompactSlider -import Defaults -import Luminare -import SwiftUI - -struct MLXProvider: View { - @State var isExpanded: Bool = true - - let maxRAM = ProcessInfo.processInfo.physicalMemory / (1024 * 1024) - - @Default(.enableGPUMemorySettings) var enableGPUMemorySettings - - @Default(.gpuCacheLimit) var gpuCacheLimit - - @Default(.gpuMemoryLimit) var gpuMemoryLimit - - @Environment(LLMRunner.self) var runner - - var body: some View { - ProviderView(isExpanded: $isExpanded, isEnabled: .constant(nil)) { - Label() - } content: { - Content() - .labeledContentStyle(.horizontal) - .compactSliderSecondaryColor(.white) - } - } - - @ViewBuilder - func Label() -> some View { - HStack(spacing: 0) { - Text("ML") - .foregroundStyle(.black) - Text("X") - .foregroundStyle(Color(hex: "#D5D5D5")) - } - .font(.title2.weight(.medium)) - } - - @ViewBuilder - func Content() -> some View { - LabeledContent("Enable GPU Memory Settings") { - Toggle("", isOn: $enableGPUMemorySettings) - .labelsHidden() - .toggleStyle(.switch) - } - - if enableGPUMemorySettings { - LabeledContent("GPU Cache Limit") { - CompactSlider( - value: $gpuCacheLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 - ) { - Text("\(Int(gpuCacheLimit))MB") - .foregroundStyle(.white) - } - .frame(width: 200) - .onChange(of: gpuCacheLimit) { oldValue, newValue in - if oldValue != newValue { - runner.loadState = .idle - } - } - } - - LabeledContent("GPU Memory Limit") { - CompactSlider( - value: $gpuMemoryLimit.asDouble(), in: 0 ... Double(maxRAM), step: 128 - ) { - Text("\(Int(gpuMemoryLimit))MB") - .foregroundStyle(.white) - } - .frame(width: 200) - .onChange(of: gpuMemoryLimit) { oldValue, newValue in - if oldValue != newValue { - runner.loadState = .idle - } - } - } - } - - LabeledContent("Model List") { - List { - item() - item() - item() - item() - item() - item() - } - .listStyle(.plain) - .scrollIndicators(.hidden) - .frame(height: 200) - .scrollContentBackground(.hidden) - } - .labeledContentStyle(.vertical) - } - - @MainActor - @ViewBuilder - private func item() -> some View { - VStack { - HStack { - Text("model.name") - Spacer() - } - } - .padding() - .background(.black.opacity(0.3)) - .listRowSeparator(.hidden) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .shadow(color: .black, radius: 2) - .listRowInsets(EdgeInsets(top: 0, leading: -5, bottom: 10, trailing: -5)) - } -} diff --git a/ChatMLX/Features/Conversation/ConversationViewModel.swift b/ChatMLX/Features/View Models/ConversationViewModel.swift similarity index 87% rename from ChatMLX/Features/Conversation/ConversationViewModel.swift rename to ChatMLX/Features/View Models/ConversationViewModel.swift index 2aa5635..dc0226b 100644 --- a/ChatMLX/Features/Conversation/ConversationViewModel.swift +++ b/ChatMLX/Features/View Models/ConversationViewModel.swift @@ -6,6 +6,7 @@ // import SwiftUI +import os @Observable class ConversationViewModel { @@ -14,6 +15,8 @@ class ConversationViewModel { var error: Error? var errorTitle: String? var showErrorAlert = false + + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ConversationViewModel") func throwError(_ error: Error, title: String? = nil) { logger.error("\(error.localizedDescription)") diff --git a/ChatMLX/Features/View Models/ModelManagerViewModel.swift b/ChatMLX/Features/View Models/ModelManagerViewModel.swift new file mode 100644 index 0000000..e8c358b --- /dev/null +++ b/ChatMLX/Features/View Models/ModelManagerViewModel.swift @@ -0,0 +1,36 @@ +// +// ModelManagerViewModel.swift +// ChatMLX +// +// Created by John Mai on 2024/10/10. +// + +import SwiftUI + +@Observable +class ModelManagerViewModel { + var models: [ModelInfo] = [] + + init() { + try? loadModels() + } + + func loadModels() throws { + + } + + func deleteModel(_ model: ModelInfo) throws { + guard let path = model.path else { + return + } + +// if !model.isExternal { +// let fileManager = FileManager.default +// try fileManager.removeItem(at: path) +// } + + if let index = models.firstIndex(of: model) { + models.remove(at: index) + } + } +} diff --git a/ChatMLX/Features/Settings/SettingsViewModel.swift b/ChatMLX/Features/View Models/SettingsViewModel.swift similarity index 83% rename from ChatMLX/Features/Settings/SettingsViewModel.swift rename to ChatMLX/Features/View Models/SettingsViewModel.swift index ce26612..cc85eea 100644 --- a/ChatMLX/Features/Settings/SettingsViewModel.swift +++ b/ChatMLX/Features/View Models/SettingsViewModel.swift @@ -5,9 +5,12 @@ // Created by John Mai on 2024/10/3. // import SwiftUI +import os @Observable class SettingsViewModel { + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "SettingsViewModel") + var tasks: [DownloadTask] = [] var sidebarWidth: CGFloat = 250 var activeTabID: SettingsTab.ID = .general diff --git a/ChatMLX/Models/Conversation+CoreDataProperties.swift b/ChatMLX/Models/Conversation+CoreDataProperties.swift deleted file mode 100644 index 3de83fb..0000000 --- a/ChatMLX/Models/Conversation+CoreDataProperties.swift +++ /dev/null @@ -1,115 +0,0 @@ -// -// Conversation+CoreDataProperties.swift -// ChatMLX -// -// Created by John Mai on 2024/10/2. -// -// - -import CoreData -import Defaults -import Foundation - -extension Conversation { - @nonobjc public class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: "Conversation") - } - - @NSManaged public var title: String - @NSManaged public var model: String - @NSManaged public var createdAt: Date - @NSManaged public var updatedAt: Date - @NSManaged public var temperature: Float - @NSManaged public var topP: Float - @NSManaged public var useMaxLength: Bool - @NSManaged public var maxLength: Int64 - @NSManaged public var repetitionContextSize: Int - @NSManaged public var maxMessagesLimit: Int32 - @NSManaged public var useMaxMessagesLimit: Bool - @NSManaged public var useRepetitionPenalty: Bool - @NSManaged public var repetitionPenalty: Float - @NSManaged public var useSystemPrompt: Bool - @NSManaged public var systemPrompt: String - @NSManaged public var promptTime: TimeInterval - @NSManaged public var generateTime: TimeInterval - @NSManaged public var promptTokensPerSecond: Double - @NSManaged public var tokensPerSecond: Double - @NSManaged public var messages: [Message] - - public override func awakeFromInsert() { - super.awakeFromInsert() - - setPrimitiveValue(Defaults[.defaultTitle], forKey: #keyPath(Conversation.title)) - setPrimitiveValue(Defaults[.defaultModel], forKey: #keyPath(Conversation.model)) - - setPrimitiveValue(Defaults[.defaultTemperature], forKey: #keyPath(Conversation.temperature)) - setPrimitiveValue(Defaults[.defaultTopP], forKey: #keyPath(Conversation.topP)) - setPrimitiveValue( - Defaults[.defaultRepetitionContextSize], - forKey: #keyPath(Conversation.repetitionContextSize)) - - setPrimitiveValue( - Defaults[.defaultUseRepetitionPenalty], - forKey: #keyPath(Conversation.useRepetitionPenalty)) - setPrimitiveValue( - Defaults[.defaultRepetitionPenalty], forKey: #keyPath(Conversation.repetitionPenalty)) - - setPrimitiveValue( - Defaults[.defaultUseMaxLength], forKey: #keyPath(Conversation.useMaxLength)) - setPrimitiveValue(Defaults[.defaultMaxLength], forKey: #keyPath(Conversation.maxLength)) - setPrimitiveValue( - Defaults[.defaultMaxMessagesLimit], forKey: #keyPath(Conversation.maxMessagesLimit)) - setPrimitiveValue( - Defaults[.defaultUseMaxMessagesLimit], - forKey: #keyPath(Conversation.useMaxMessagesLimit)) - - setPrimitiveValue( - Defaults[.defaultUseSystemPrompt], forKey: #keyPath(Conversation.useSystemPrompt)) - setPrimitiveValue( - Defaults[.defaultSystemPrompt], forKey: #keyPath(Conversation.systemPrompt)) - - setPrimitiveValue(Date.now, forKey: #keyPath(Conversation.createdAt)) - setPrimitiveValue(Date.now, forKey: #keyPath(Conversation.updatedAt)) - } - - public override func willSave() { - super.willSave() - setPrimitiveValue(Date.now, forKey: #keyPath(Conversation.updatedAt)) - } -} - -// MARK: Generated accessors for messages - -extension Conversation { - @objc(insertObject:inMessagesAtIndex:) - @NSManaged public func insertIntoMessages(_ value: Message, at idx: Int) - - @objc(removeObjectFromMessagesAtIndex:) - @NSManaged public func removeFromMessages(at idx: Int) - - @objc(insertMessages:atIndexes:) - @NSManaged public func insertIntoMessages(_ values: [Message], at indexes: NSIndexSet) - - @objc(removeMessagesAtIndexes:) - @NSManaged public func removeFromMessages(at indexes: NSIndexSet) - - @objc(replaceObjectInMessagesAtIndex:withObject:) - @NSManaged public func replaceMessages(at idx: Int, with value: Message) - - @objc(replaceMessagesAtIndexes:withMessages:) - @NSManaged public func replaceMessages(at indexes: NSIndexSet, with values: [Message]) - - @objc(addMessagesObject:) - @NSManaged public func addToMessages(_ value: Message) - - @objc(removeMessagesObject:) - @NSManaged public func removeFromMessages(_ value: Message) - - @objc(addMessages:) - @NSManaged public func addToMessages(_ values: [Message]) - - @objc(removeMessages:) - @NSManaged public func removeFromMessages(_ values: [Message]) -} - -extension Conversation: Identifiable {} diff --git a/ChatMLX/Models/Message+CoreDataProperties.swift b/ChatMLX/Models/Message+CoreDataProperties.swift deleted file mode 100644 index b0ef408..0000000 --- a/ChatMLX/Models/Message+CoreDataProperties.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// Message+CoreDataProperties.swift -// ChatMLX -// -// Created by John Mai on 2024/10/2. -// -// - -import CoreData -import Foundation - -extension Message { - @nonobjc public class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: "Message") - } - - @NSManaged public var roleRaw: String - - public var role: Role { - set { - roleRaw = newValue.rawValue - } - get { - Role(rawValue: roleRaw) ?? .assistant - } - } - - @NSManaged public var content: String - @NSManaged public var createdAt: Date - @NSManaged public var inferring: Bool - @NSManaged public var updatedAt: Date - @NSManaged public var error: String? - @NSManaged public var conversation: Conversation - - public override func awakeFromInsert() { - setPrimitiveValue(Date.now, forKey: #keyPath(Message.createdAt)) - setPrimitiveValue(Date.now, forKey: #keyPath(Message.updatedAt)) - } - - public override func willSave() { - super.willSave() - setPrimitiveValue(Date.now, forKey: #keyPath(Message.updatedAt)) - } -} - -extension Message: Identifiable {} diff --git a/ChatMLX/Models/ModelConfig.swift b/ChatMLX/Models/ModelConfig.swift deleted file mode 100644 index 8e93c93..0000000 --- a/ChatMLX/Models/ModelConfig.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ModelConfig.swift -// ChatMLX -// -// Created by John Mai on 2024/10/4. -// - -import Defaults - -enum Provider: String, Defaults.Serializable { - case mlx -} - -struct ModelConfig { - var name: String - var path: String - var provider: Provider - var description: String -} diff --git a/ChatMLX/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/ChatMLX/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..274babb --- /dev/null +++ b/ChatMLX/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/1024.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/1024.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/128.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/128.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/128.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/128.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/16.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/16.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/16.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/16.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/256 1.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/256 1.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/256 1.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/256 1.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/256.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/256.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/256.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/256.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/32 1.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/32 1.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/32 1.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/32 1.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/32.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/32.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/32.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/32.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/512 1.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/512 1.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/512 1.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/512 1.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/512.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/512.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/512.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/512.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/64.png b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/64.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/64.png rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/64.png diff --git a/ChatMLX/Assets.xcassets/AppIcon.appiconset/Contents.json b/ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/AppIcon.appiconset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/ChatMLX/Assets.xcassets/AppLogo.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/AppLogo.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/AppLogo.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/AppLogo.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/AppLogo.imageset/logo.png b/ChatMLX/Resources/Assets.xcassets/AppLogo.imageset/logo.png similarity index 100% rename from ChatMLX/Assets.xcassets/AppLogo.imageset/logo.png rename to ChatMLX/Resources/Assets.xcassets/AppLogo.imageset/logo.png diff --git a/ChatMLX/Assets.xcassets/Contents.json b/ChatMLX/Resources/Assets.xcassets/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/Contents.json rename to ChatMLX/Resources/Assets.xcassets/Contents.json diff --git a/ChatMLX/Assets.xcassets/MLX.imageset/1028322432.png b/ChatMLX/Resources/Assets.xcassets/MLX.imageset/1028322432.png similarity index 100% rename from ChatMLX/Assets.xcassets/MLX.imageset/1028322432.png rename to ChatMLX/Resources/Assets.xcassets/MLX.imageset/1028322432.png diff --git a/ChatMLX/Assets.xcassets/MLX.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/MLX.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/MLX.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/MLX.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/MLX2.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/MLX2.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/MLX2.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/MLX2.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/MLX2.imageset/MLX2.png b/ChatMLX/Resources/Assets.xcassets/MLX2.imageset/MLX2.png similarity index 100% rename from ChatMLX/Assets.xcassets/MLX2.imageset/MLX2.png rename to ChatMLX/Resources/Assets.xcassets/MLX2.imageset/MLX2.png diff --git a/ChatMLX/Assets.xcassets/clear.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/clear.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/clear.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/clear.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/clear.imageset/clear-l.svg b/ChatMLX/Resources/Assets.xcassets/clear.imageset/clear-l.svg similarity index 100% rename from ChatMLX/Assets.xcassets/clear.imageset/clear-l.svg rename to ChatMLX/Resources/Assets.xcassets/clear.imageset/clear-l.svg diff --git a/ChatMLX/Assets.xcassets/huggingface.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/huggingface.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/huggingface.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/huggingface.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/huggingface.imageset/hf-logo-pirate.svg b/ChatMLX/Resources/Assets.xcassets/huggingface.imageset/hf-logo-pirate.svg similarity index 100% rename from ChatMLX/Assets.xcassets/huggingface.imageset/hf-logo-pirate.svg rename to ChatMLX/Resources/Assets.xcassets/huggingface.imageset/hf-logo-pirate.svg diff --git a/ChatMLX/Assets.xcassets/markdown.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/markdown.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/markdown.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/markdown.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/markdown.imageset/markdown (2).svg b/ChatMLX/Resources/Assets.xcassets/markdown.imageset/markdown (2).svg similarity index 100% rename from ChatMLX/Assets.xcassets/markdown.imageset/markdown (2).svg rename to ChatMLX/Resources/Assets.xcassets/markdown.imageset/markdown (2).svg diff --git a/ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/Contents.json new file mode 100644 index 0000000..38d5dd1 --- /dev/null +++ b/ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "menubarIcon@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "menubarIcon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "menubarIcon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/menubarIcon@1x.png b/ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/menubarIcon@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..7a7fbe88749d78707720bd310adce6fa5c4a18b9 GIT binary patch literal 3433 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 LNkvXXu0mjf5rTXX literal 0 HcmV?d00001 diff --git a/ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/menubarIcon@2x.png b/ChatMLX/Resources/Assets.xcassets/menubarIcon.imageset/menubarIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe13c4ba2ab6244edd05824370bab55b3f10a78 GIT binary patch literal 4168 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 Z^M6BnkV&-w_7DI7002ovPDHLkV1g~09ZmoM literal 0 HcmV?d00001 diff --git a/ChatMLX/Assets.xcassets/openai-lockup.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/openai-lockup.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/openai-lockup.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/openai-lockup.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg b/ChatMLX/Resources/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg similarity index 100% rename from ChatMLX/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg rename to ChatMLX/Resources/Assets.xcassets/openai-lockup.imageset/openai-lockup.svg diff --git a/ChatMLX/Assets.xcassets/openai-logomark.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/openai-logomark.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/openai-logomark.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/openai-logomark.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg b/ChatMLX/Resources/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg similarity index 100% rename from ChatMLX/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg rename to ChatMLX/Resources/Assets.xcassets/openai-logomark.imageset/openai-logomark.svg diff --git a/ChatMLX/Assets.xcassets/openai-white-lockup.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/openai-white-lockup.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/openai-white-lockup.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/openai-white-lockup.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg b/ChatMLX/Resources/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg similarity index 100% rename from ChatMLX/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg rename to ChatMLX/Resources/Assets.xcassets/openai-white-lockup.imageset/openai-white-lockup.svg diff --git a/ChatMLX/Assets.xcassets/openai-white-logomark.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/openai-white-logomark.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/openai-white-logomark.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/openai-white-logomark.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg b/ChatMLX/Resources/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg similarity index 100% rename from ChatMLX/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg rename to ChatMLX/Resources/Assets.xcassets/openai-white-logomark.imageset/openai-white-logomark.svg diff --git a/ChatMLX/Assets.xcassets/plaintext.imageset/Contents.json b/ChatMLX/Resources/Assets.xcassets/plaintext.imageset/Contents.json similarity index 100% rename from ChatMLX/Assets.xcassets/plaintext.imageset/Contents.json rename to ChatMLX/Resources/Assets.xcassets/plaintext.imageset/Contents.json diff --git a/ChatMLX/Assets.xcassets/plaintext.imageset/doc-plaintext (1).svg b/ChatMLX/Resources/Assets.xcassets/plaintext.imageset/doc-plaintext (1).svg similarity index 100% rename from ChatMLX/Assets.xcassets/plaintext.imageset/doc-plaintext (1).svg rename to ChatMLX/Resources/Assets.xcassets/plaintext.imageset/doc-plaintext (1).svg diff --git a/ChatMLX/Localizable.xcstrings b/ChatMLX/Resources/Localizable.xcstrings similarity index 98% rename from ChatMLX/Localizable.xcstrings rename to ChatMLX/Resources/Localizable.xcstrings index d1c5dbd..2f4a336 100644 --- a/ChatMLX/Localizable.xcstrings +++ b/ChatMLX/Resources/Localizable.xcstrings @@ -46,12 +46,6 @@ }, "%lldMB" : { "shouldTranslate" : false - }, - "🍻 If you have ideas, welcome to contribute." : { - - }, - "👋🏻 It will be supported soon ~ " : { - }, "About" : { "extractionState" : "manual", @@ -81,6 +75,18 @@ } } } + }, + "An error has occurred!" : { + + }, + "Any to Any" : { + + }, + "API Key" : { + + }, + "API Proxy Address" : { + }, "Apple Intelligence Effect" : { "localizations" : { @@ -489,6 +495,7 @@ } }, "Default Conversation" : { + "extractionState" : "stale", "localizations" : { "ja" : { "stringUnit" : { @@ -657,9 +664,6 @@ } } } - }, - "Enable GPU Memory Settings" : { - }, "Endpoint" : { "localizations" : { @@ -774,6 +778,7 @@ } }, "Experimental Features" : { + "extractionState" : "stale", "localizations" : { "ja" : { "stringUnit" : { @@ -1011,9 +1016,6 @@ "https://huggingface.co" : { "shouldTranslate" : false }, - "Hugging Face" : { - "shouldTranslate" : false - }, "Hugging Face Endpoint" : { "localizations" : { "ja" : { @@ -1098,6 +1100,12 @@ } } } + }, + "Image Text to Text (Vision)" : { + + }, + "Import" : { + }, "Internal" : { "extractionState" : "manual", @@ -1242,6 +1250,9 @@ }, "ML" : { + }, + "MLX" : { + }, "MLX Community" : { "extractionState" : "manual", @@ -1305,9 +1316,6 @@ }, "Model List" : { - }, - "Model Manager" : { - }, "Model Settings" : { "localizations" : { @@ -1336,37 +1344,6 @@ } } } - }, - "Model State" : { - "localizations" : { - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "モデル状態" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "모델 상태" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "模型状态" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "模型狀態" - } - } - } - }, - "model.name" : { - }, "Models" : { "extractionState" : "manual", @@ -1520,6 +1497,19 @@ } }, "shouldTranslate" : false + }, + "Open %@" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开 %@" + } + } + } + }, + "OpenAI" : { + }, "Other AI Provider" : { @@ -1665,6 +1655,12 @@ } } } + }, + "Provider" : { + + }, + "Quit" : { + }, "Regenerate" : { "localizations" : { @@ -1975,6 +1971,9 @@ } } } + }, + "Text Generation" : { + }, "Title" : { "localizations" : { @@ -2059,6 +2058,9 @@ } } } + }, + "Unknown" : { + }, "Use Custom Endpoint" : { "localizations" : { diff --git a/ChatMLX/Utilities/PersistenceController.swift b/ChatMLX/Utilities/PersistenceController.swift deleted file mode 100644 index f57a70d..0000000 --- a/ChatMLX/Utilities/PersistenceController.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// Persistence.swift -// ChatMLX -// -// Created by John Mai on 2024/10/2. -// - -import CoreData - -struct PersistenceController { - static let shared = PersistenceController() - - let container: NSPersistentContainer - - init(inMemory: Bool = false) { - container = NSPersistentContainer(name: "ChatMLX") - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { _, error in - if let error = error as NSError? { - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - container.viewContext.automaticallyMergesChangesFromParent = true - } - - func exisits( - _ model: T, - in context: NSManagedObjectContext - ) -> T? { - try? context.existingObject(with: model.objectID) as? T - } - - func delete(_ model: some NSManagedObject) throws { - if let existingContact = exisits(model, in: container.viewContext) { - container.viewContext.delete(existingContact) - Task(priority: .background) { - try await container.viewContext.perform { - try container.viewContext.save() - } - } - } - } - - func clear(_ entityName: String) throws -> [NSManagedObjectID] { - let fetchRequest: NSFetchRequest = NSFetchRequest( - entityName: entityName) - let batchDeteleRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - batchDeteleRequest.resultType = .resultTypeObjectIDs - - if let fetchResult = try container.viewContext.execute(batchDeteleRequest) - as? NSBatchDeleteResult, - let deletedManagedObjectIds = fetchResult.result as? [NSManagedObjectID], - !deletedManagedObjectIds.isEmpty - { - return deletedManagedObjectIds - } - - return [] - } - - func save() throws { - Task(priority: .background) { - let context = container.viewContext - - try await context.perform { - if context.hasChanges { - try context.save() - } - } - } - } -} diff --git a/ChatMLXTests/ChatMLXTests.swift b/ChatMLXTests/ChatMLXTests.swift new file mode 100644 index 0000000..86ed88b --- /dev/null +++ b/ChatMLXTests/ChatMLXTests.swift @@ -0,0 +1,16 @@ +// +// ChatMLXTests.swift +// ChatMLXTests +// +// Created by John Mai on 2024/10/10. +// + +import Testing + +struct ChatMLXTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/ChatMLXTests/MarkdownMetadataTests.swift b/ChatMLXTests/MarkdownMetadataTests.swift new file mode 100644 index 0000000..3580083 --- /dev/null +++ b/ChatMLXTests/MarkdownMetadataTests.swift @@ -0,0 +1,29 @@ +// +// MarkdownMetadataTests.swift +// ChatMLXTests +// +// Created by John Mai on 2024/10/10. +// + +@testable import ChatMLX +import Testing + +struct MarkdownMetadataTests { + @Test func testBasicMetadataParsing() async throws { + let markdown = """ + --- + title: Test Title + date: 2024-10-01 + author: John Mai + --- + + # Content + This is the content. + """ + + let parser = MarkdownMetadata(markdown: markdown) + #expect(parser.metadata["title"] == "Test Title") + #expect(parser.metadata["date"] == "2024-10-01") + #expect(parser.metadata["author"] == "John Mai") + } +} diff --git a/ChatMLXTests/ProviderFactoryTests.swift b/ChatMLXTests/ProviderFactoryTests.swift new file mode 100644 index 0000000..62ccd01 --- /dev/null +++ b/ChatMLXTests/ProviderFactoryTests.swift @@ -0,0 +1,16 @@ +// +// ProviderFactoryTests.swift +// ChatMLXTests +// +// Created by John Mai on 2024/10/13. +// + +@testable import ChatMLX +import Testing + +struct ProviderFactoryTests { + @Test func mlx() async throws { + let models = await ProviderFactory.shared.provider(.mlx).listModels() + print(models) + } +} diff --git a/images/Logo.psd b/images/Logo.psd new file mode 100644 index 0000000000000000000000000000000000000000..7e3b10a84602cd82b9e523591e139123f77a66f5 GIT binary patch literal 2816906 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
    t$B9{oJu)quptTC*M5wW1BXe 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 z zQa8{%gW>ekG6Z9=#n|H9+mK+uzF@EA{6t9{S}dX!<&~1j>U{vBcwy4iaFRrThVL3n zg3=;qL4t@CFsy+I5TX(e5N!q?Jb;(9gEs>xi8I;o0REYpOYE|wICXktxYh#%#Q$pv z@0yZsP_{Jo;3HoM$p&nXHbAIHV;(~ zPRY9L&{AM0fdq*&mInYr@rlT~C?nMJE))ezUE_k}@POQ8VxC}fyn}8abEb`|m$DXk z7$Or#1{E$UAnb_>_nF^`AEQ;L5TfQE-hie{pT+w5|D*cNw%HN5ZEjchByIh$5B$yTK=L?~41gX{C9z{H zp|R|P{TN4?Ebx7NxU#q?G;!s4R0KhJWgf#%P#d}XplI6G#(gyo9#2l9fjIdy93~Ec zGFz+(T7K{-1d33~^XlHAcO2h9KxHRFz`+Cemh1|+i? z-hkB<({$ND@;W7;770K=ki3SS4i6AR`LF3t7*f*zSj7j^z}POy5zePkS`3AHD?Q8! zk#rUum5-T^sE`;Qz~;z1;NCF2!k{n>K^LBzppFq*)j6=Blp!O*z{uR95-^2Rb7KoK z%E9tECz@IlF!0ytSMFr!K2=ZCF*Wn;h5^Mv_mJiKgRGkz00T<(X@seQWL(knSHKu) z9%{~(2BZQd8HFg{wL0z?1j9aH2MLU&I>vBDs_j`k0MOf3DpU&w% z;0$?z6dhvV^r4;0)&TAL41k1z1;Ptd%Tb$OxoP__hLK`v6p9lL^kBUOw#s3siyX;M z*@p94$!jPBUx8sV2frkT;Ich)qr$iIBFOSK2jZXQ4I~mi-^)M$_ut3cKacRi|% znW=QSYN zLFK9IsPf^vYK!UN%iGn~jC6+wNRNCm%nJ2y(?S&FS`buPatYM$h zEsNj<{YIf4dT+5Kkv(y{5UN-}OI?AJ3OiQ)=xW0a356E&FHi}`PmTcH zmARlrfEp$9LJcS6?;+*Eab&~J8U!K=UXcT8j_Wx1+kKto0gB*8TBHie&dzO{;VNVZ zKYbv1z40}`Kd|@F7o309B!Jwt^BH9Wx1^X2rd&9l>K0W*m=cBkf->}hzw zE$wYTM3?4?FQE%g_eSoiiX3{dY{#s7$v2Lu8(lM$tyOb;karg|aV#}4(dZj=cBI$K zV$baYVuafIz~`TU+vdATy=A-dXA~LQdUHa|vipCg;P|h=i-~+BPG%)je1#Y?suX;Yi3`PaUJOG=>n})m*rHfez z`ai;?8qBHA#~ZsnADow=e!%RJ8U2CmWzjMVxQzQ$ci-V0wsO`}sPpJG*oMj&-@#XX z#w<|~4OUzv3|;B+1JDxMEnhXjHDo}zmj5qraA{0kY1&mtxJ}AbhT!FMz2Sd(-Uoy@ zuy%l6KJXlTJOoG1De%(}U4{q9EgvCry5L&Mn5vwp@m{?K!pSha_zf=ki;SN)9t=WB ziJ`}LM&hQGGKB9lo3eNTyf{T13!PI7Gb!C}hWkH#nI8NK9g)XuS9b z#_)wDgI+-@9Hd<_=g+7&R3y;HCRBu~;N_@*b&^f&zAlOMdN^G^T^tHAN)WT?8Zh)P&R?-dIX0EZh1!BOj~_GRi_{b(OA z3ll^=@p)K8Ne!MV0dS5@)Z;P~ibGT@K7sbe@Dtdv31fN;;t>8mLV&td*&^4_KR7=? z+a6g|BNhNsqN;F7qNDb7T@UhQbq=nnsYQ(32~!jutPZ7Wa5nJovXOwXvIDn)M!HfZ z=O}}~S!Xi+G^1?z3H7NYhqWESwcJc#Z$Tw{juyCXcmsq*>LObZAd-Dls#{1iK1Gt7;Q>BcW<5N2b1=iy zBDrgFYC!X#cl6cs;zB0Mg^HXzPtR(6orjORTsEf^c z{s~xNEQ7EkX@nqDm>|Gb*l`n~G*5;Hz_tBXAsA-Ax=*kGuJnK%8?(wGQ^jJyf{8x? zeL!A?=uw=RO(kc+`WxOrY?N3y0A4^Bp5vo0DQSatExk8d7G4MpQ-31m*U#rFt>+da z|0Z1;-avLke_^--RRQcL11oT&DcX+L0+B{R4_=3vU$gp+>M>!be!dHPJjX-37(rj9AMR5M?vN`n658N{V#m z2@kIHIs}hE6e;36R{=IM%cr069`~Ap$GhXmj`eqfy=ttygLE179!J9%|}0JNi?bC08kB<1g9wcq2ypJ ze2_*iE=V^7FG&Zm-!=IDqlxxdXCF6%reu(4dF@pHg!hfXlj%Hs*vwFC(wc?_4r`!u zZ+bc#Yqtqg5XrlvSlz3RSYjjgg2YOK%eB~*mKj5)fY3CAmtpjeSit@b{-;=IhLi89 za5w)3mS>Bc>jfDd$u)-|>}5rI;QRn7a2@O9W{t2# z!#*ye2l#eaP1S#x08*{I6rS|g!2*Mwxle~Lil|oi#-8#fAR%?5i_SC7fTRZy3Q9>; znj(qA-M|ViIkFVR=ov@CATdIYnE|N3nY#cTv`xpM2{_haSOYnoWPq?KbXO0Zr2Cpx zf~>@ydU$}QiE=e4(3M87g`smXee?q@#P9$y2v*`p;2E-Z_a&V!UlP*`EHgZSW43Og zQo&uMU-*b-;tbfEo-~*V2qcnp9TvwBtqY%8x`X6pdjooh7OE}_*>4;aj0isB7-!g~ zbSWra)Fx*P1#%`sbN!;4>f(jtk-aj!gsb!V8P0+w1iI0I#Q1B@B@lv73pp>Bh#^x| z6ZqpAOnyI~Co)kj7BPvp0H>M3T*M!V?U}mJ#fq|~)~K6rU)?j~J%2{fr-rwUa?rZ1a$pql3{6QC79Gsxc7iS%PSB z$t#A#F@yQ&=WBThNmc}b>}MxmKUCs-Go4{NftJW|*hOk;aTz`bz#R5c>Rva@)bqiMTn~SbG2w0dO2=6AnSSY4v#z(Uu7J1EnI% zX(7!|_oG#e?j!031T$G0-qtx&O>WFH+u>y_(Gp^>XX2%Nx2uM-56!h<3OGvnAi{6} zlWunwR(=N0r}IwK^@ZU26?OqQ%wNuEMI}ozn!I6n0Or10or{GAQSEY{-#d{72(ts( zFNUaEmCcr`$W%nR;yZ4N*1y}?6Nfj@SAo5WLe3HN;@$9BO^+tOhz`SzWDgm(qUfNJ zvz=kGdZx~TE(4j+aGXUk3nJ@w^k@8U$fhG$4;nxUDqlF8>Q%W#b~}llBY@*h_!U?Q zG~fe&StrC0F?2`DXG(n6Do+qx{`^Xsl{u_|SZ3Qq;Q>GjiufD2Ij&bSqhT1CoIp!N zZb(FcWM}(9H=#hTdoT1eT5(6bUqb+$Z!jiz*n9)}bK_p-ri zn(oRt$7@OVWy$sTRDM7QoV9doc?OX(i2V3gh{Rg?QA2);e<1T8$P4J?k`uT`*!6zAX<6`%7_-YRP5x ziZh+V=Qj_6x=jkE*Dl84M$#bGjEKFjF(bvqh=)f*#r^SxMuHd!Py`90#;N>ue}Opu zsI90FU2r7r5xSb}c^RIg(8zCP8gK-_8Vjdk4TN?{A`?m_6BG+b+x(g=i_LHvk}PEj zQzGd$(;`i-rXxCyyiNEgNfXX*0JvcUV%;1_h?wjSu;Vf@0T!w^+{*I0pGym`TVzLQ zRLRoB``k4&wTCw#KSvm5XoxiK80c*nVnn8-5}bmoN>6v=3Wabd-BXRan&%wIFfW%y zR92uAH7{4|kvaqvic`hdP3Ug3ACz z$&DIxjOL7b&4?0(1a+Jt<>j9}#D#PSx?5zkh+L?GegrrOQ&FD-NO@jF#hsRhD5Ohk z8)lizU;}|el-Ix+9uVyvQ(3`eGeQHDe@>Q*Ez^|YEBGs~Da9pTFxeo? z9^OD_N@^lI&}HT~gHOa@7eh$50Z2GJKvXgtkeDSG$qZ^-jt+=RRm!`p0kXakxqilb zj5wtOa7^xxheMXWJRtPK1qeogEaD)zVeMkT_G-)*PqU2bq$N~)pgVvxF6egKxwi`c0bQPk|ZH5IS9-BfCr6o|+_Jiwh82&`2q zZ}$gP;?h*xGZ<)y3xWBn1FCsn+y?51qjSZnaRLv*VanWkiG_tyq zp5Z6(+X5bFQX&xWSduMCZRGFyX+16vAaJBtH{1iq*A$0(KBmfPce3?g63zIXE@qc0 z$i$X(6*8>)BgI%06t3i)Ijh_mr7%5CK_JLW_uUSULx$3vXONt znyQ%aZijSVV+RQ13d{sg%gZ=&4b*6UxLV7tj8Fwi%h?tfCrDd)$&Jgw^r~en9s>`b zJF<6%m_-H1-tz+rfBf_N`94sX;h(54;+aALFiQ=3HCL`52G%*AM*64jF4#B@4Lrwz z&?9K>CD4EAy?X%O4`5yMT&V)|03!;jpDtSa`ur6@W*h?Z7oZ~!x-Kmr!CO-)$xmW zO&z4&$;VHS8n`%>r$n~SzS@sv6sH(%OY%5zjuv@&0~$K*T2gqe>+F577VIf;o)&lL za5S0~u8(610PxP4wGb$M)HMKyoogn_gWpVc627=i^S4n0D!>zQJ`eV1k`v;4IJzyAccQCoL5VJJ> z3Cufyvhtc7`;V)kq&Q6yQL(v- zW!tDxP@!c`&%48yb3KZm4g*}xo}gC5prBIK$2~71Qf`j4agnl3Qo_Lm`6~z&dicYg zm_?XQtsVpSG`I=F7!f{+C~J#>dY_a^RPn(=(U_o_D?v%FPXgPqJBFWtfgw4pho6VH z`d8DpD-vbXCZ8S?7A)M>GHYm@GitU>dWl`5(}144?&(G9h}Q9r*e#_|*3C5j$?uFS(}wE??&WyU%*k9}E0 zSFe#AhJSUbSOE<-bzjCcXnVtb06UVP=vCuS2kk>NdV|hEkvFM*GJ;PKyJR&#GIt28MZa9=N>>bfUB~R3O-djxpv5elIXb{iXQ> z$Qc%q_=hV89htI8%S@Z@nt!JPMWG%JmT3N9RR^#>%&a zR=_r1s%I&+cqy>Lf+Y?2fiNyzR&I%{ln5X>tON34R`RavbNTN_JOAjt>=#sv9Flb0 zG0t(?6hTXZ@nRKGQB-cm#yOF=g{X1T8+3Yo++ht+aclz{SgIjebg!x0v@G1q$P~i^ z7~6=6?k_6I_r`xGV!=BnsDf$?4`2;L`p#FwUq})o`@a(sY2D!=41nZ&D84#Re$lmt zXNyDt0RnW&yM~!nC`S*x&(-?n{D1&iH~B%%Ik@}J%RW%#A=@JE;NyLmfQ|61a$!%{ zz@_B@D#ll>Qk?Z?>5?~+>x1c$l5k#!!$k1nzity7hKTYkuEzY9J=2l6p3h$ak>wkj z9$Q_7pkuCzO;J+5i|IE!fNxv8ZJQzA`Sjbk3z^PppbjIXQKPZe`wjvkVpjnMn4}Rb z-;rkwZ{TzJZ@Wk^A>f+^BQS%Lva>2u_Y4nEG9!v7^mdvxStyY(pB-@5+h8G&cA{$Y z`#F%IWw{PLq^t|`S?hE-Oxf+bl|&IGEW2!Mghp=pRVeUFqN(F*BdQsp<8l;Lpx(Vq zHS;22^1KGX1KMN-GdZMLq%wVDxV?_I(Ec7m8{iRYBcZV$t%J$66>w&6!BjLLhc|#> z27qWZv&(W58`2pAo!cfGiQz&aXRSLWI%oUjjumHxdJ1L<>mU}^p9@Jtmo@~RiyB>! zv@yA5&V0*-f`lct|C1JKt|8|M3gyjIBz%v~YoJ=vVgLb?r2=#_8_rEYrk!8u8H54- z7-)|k%EY3G+ZkL%)!y82L=EIoH&bO|>9VllSL}NuW03RM;mOD);TAkb6 z9mLK=yjOit*HMoS50DSQKXHVGL4SHgi*bq$qjD+c*~JiwWkY@ zLyq^}j38!;ggwJqke(x~C=S?AN|a@da$U)RJf+^p`3+!*sC3YEj9plmH-U!X1~sPv zX?J)4CMC{`z2DF~Y7|x!D#$0uko`4;cQz}*Vx+*RG6mv{#cgsl*+ZS%94m=8O#d4Irt%}OSULi}g$C|C_P z1nuPz+H~h=F5F2V*=z{?3fmGI>B+Xl`77XBQ+vRYfnmuP>;YPYB1G~xzoj(&-X+;#0`EGEX=1d=KyD$(;kNZl?cG;dzRGbNXPqb@wsYbScR zRPkj}(+Or6UAl96Ja!~`C1QvoZg>OrCiIp9%GHSS`pVB9O)UyGly$&VsS&mMsC8Le zx!PcGR3X%&Srk$K4{xAPE(cN5S>2Z!3lQfP&a+o?pvi(}+0^0X0kB9yLYypM4(DyBLT&@H zx1k0pjyhF-A}C`=3s7zp%wf=KuAWRfhtfB-1{xJcULIqtDAXUMNv>!UU#CSRo}GuUQw`IsV#&$4qUCaOmvI1BieS`JhqeDxf-Iz4}jN zT*~^F;X)D0*kZD|f+pT&SV~vT5?k4HQ_ZH~4b+W}Q4q)-uo;tsmf_QF4d#PN z{+y-y`Fcx;hUVR3it-)@jdNod-T-m|Nhgj;`c1+xOoYIS2KJcTr%8?LsB$3Y*E45=$j15g{1epbn3dSx zdA6hT8H{0)B4k~w&xt$-mLA|b!+dXIH*`|Z$$q@7u$98+)93;6fI39Yw!uGuKY#@M zG7sX6aQq6F;2l#ObsG^yWHEj%jvgI;R(jI|c!#3MQ|fBdqI0?0ttI;e{kNg_e#!KW zjFTZ*LPMs~P@ovvj*9r9(#m`b40{`c07kQ$o2wkA{kIXMy!ER zvyi4sp|?EWNN^AQf2lSMkRNY1qf(xrt$@u<{`>|i?PeScY_C{gTpu`kU`?{`(>ENz zI89P7obf;S#}!~c4H*c77KM|{cXbeO56HOE|TtiC#mhT4#cR0-PBkOMDp~;A_d}TYZhCbrr}B>oCkio4{dx2pTC*~pq`EMp?7_*+FS&8aJp;&GVN-WW6(N6Pa2my zoyHi2LUBzt9!=n&tdR_AqS1o351%nEm21@M>21+t^^^1mUnqls%WA0RKuS)l4c!g9@HdBo-+}yFGM>N3i<&KyL(cw;%Oz6@!NfHU9+bNCV1@WC%@7}k{z4p@&ajeQdZSZ;oR?YTh<0s8WyopYiY&fzmuhb=y7?jA(d*dA=Q;#iEEPkXmz5q+a>!%sxm$T1DZ$V zZ{H}nQ8*OM&L)>&>K^h$+0Jq+^Gvn%6Wi)A+6abz+kx6#HZ*|fyau`2Fd0*N$+Kfp zyi&?tgw03*Luiu&SOg&fYlUvzexez#`J~H&`8d2mHnO0{WK|7O;eJkU3!oEYX-kF& z^yfQ5rZDh=z(lHHp=ZaQ$lRRe0Wx)dGMDV%ML?ZOkwA1xM!@`Sc>vaeGBsuxpL{f+ zOtL7$fGG6r!vnY#VowMuv~+k65u+r0c{C$tolzVmv(ietQW>v@?fqGZ29OJGg7lCh zk?tFq2R(_i+sBZ-Yjg>2Ox%|8A6s637mtI4j9Z}m4C69Ej$}<4KCFSxs2`sCLt8^q z;0OI8J-#CC;h3BsAU|(xUM^QIL7f7Bvjp*SykeC)JOFTl=`FJZ(8K3(NyImZ3Th31 z+E=cKkK#Gb9 zyUcKOlSHtCtqxwIIE~swd2L;O?`&a|kirbzuS;>+NO_S+h@40(pwneUsMceS$(b<@ z*}68PaYA}5-H{LOh54CG9{~-JPC9SQKYQ2r|2hajMQ!qYVYAN#RQ_rj8ppi z#c&G9jg!|Ha$=G}u1r~&gA5H(?IG~K5wAGkkZP+7?mzDXj5@R<1O!Qi+g*zbA%YMG zwWR-OdBFeY(#W(i%v?lpc2y@QLHP2p6FF)rmQ+i~<|Wt=9kE`f>9z(m4sQ@`2aU;9 z>P|F39xlWeLf1~ib7)wqm>4@DS%*oY_+hE3+|F*pxvo#Gg!7nH`O>2h|G0CvUIsPU}!Hi6O@ffRdj=DQ_P}T4vR?We1Y4B zaZ)<#w$b~8wjVAA-F=(0oh7tqZr9Nu|w6( z%}334Yzgo1CQM4}mnL1%)rB0s zf}TQ#hrr~|(Y zKC^;yHP@sPrMln=VO3k;+mqn}}PAT$xfkL!TG zg!w+yE!kZSZy*RW&7*MiCIqkD%3o3hw(poR!-Z0@hS#S(fv|x4?z_@S@=9hT55gGU zz+oi&PAlCtirOh4z~&XCuEb$0mnpNP$yBI}6|m_p!3h%>@F0eoiGhe{Lg;JtiBuII zy71C%DFc4f@BmX204VCj$dPGzcyloL6|J&(T^^v%PNXa$Awuamp`6;*f(#k*^aeta zTUCPv>fj?flYV|f(+KjPhtH@ylBSuwz-^0h1G8hdi?)vhNt0ywjARn#U_haoR1P^p z;h`0j_5h;vjgR|)$D(KsJgMwzdWAL}-n_)AP(O74J%A6h))=`f29Hi3MB@{~R2y5m zaW1-g02CWCu2%<0)%2(FRE~o$lwivPd^!z2xF#1CeODt<;%GkOYDzH9B@ZDH+7hX@ zv`E>d-ZFx*dQH}U=K((~qBfV7l>SB~jxLp)i@%9BItjqn`7luk6Wu7efRcZ_U=>Xg zXS}F1kLO!Ck8;6Cp?tB-vmRprPcRCx|AcA|KS8%so6bZe49mvzKF3Ydz{Fw3-aJ15 zT{&s4)QY++KhrWEw<%tbcJTQuka9XRnmfeJE)*0?*NX)OQi6sCk*L6kluLS#LE{Qb zE>7f@H|b+W^yl#20&1D+X$S<4kDe=9q56k*)V;Vo0FSP>wsLq`F7-{Yc zz-e@)sZwkMbXT#_eFRiwytMe{;R<9ArWaxRp+dPxpa!mXU|QGe@EPS;GE)eEei=+r zuyqw7%{QH5=(S-aB4d*GvXPM-;EYQ(lidHBMyL=S-XN~F6xq^cRR1F@kt@kl3GNWI zmIr)rz>z0vEI|-8Z>1*iei9`IVmQtqBUAy}8+kU?0+J(NlXPu7R-{_zH^>e~Fkqoz z7>a(0hASXXcu`L;B2jRQq=sA?D2(z>E$dL(4=iPxyte!V$$?}@kpVeFDA9kU*4FM~ z@q>vn`~KJacNf+Dj1$1 zP^x{1My9xm=rp3|@XkFEtB^<&`o&=U>JzZB)3kur3e>|-(6h(pbE0_bW~>B6G=dJQ z*lhbDxL%|F9BAl1{yKagiT*`uppeH7TOLrojxX>A(iN2Ojit<(^vJ)$^KhIoHGF&W zKp+6qKphtdWgIg!f`uuI>LI7ihSK7**m5W4AW2`x_;2wFQ<`C@SEpuXBBpy3ND(!z zrt1LCdw>on0E}`5*S-*gUYLugfV1ESVi{Vp|R&M6gh7i+H;K0M zAYh`(4f7Y$;W&RVJj^b*4M?B2arW`*Afe)X7L-cal1cJVwh8OLE`}mZNYmtDu(7~| zyk@P%4`$7*WJWF1zLhQO1`n zSw!(ZGsbf0e0zY82))Q5Nt2Eahq{ydp^}X)>hc-Ay*Ix>e!Mr+N1+9gh?Kkul*5(Q z%t{6%&A<31VM6I(R!=1-&64;nZve)@0pYgriNQlIO#_m0wI&7X@vslPw9dWEmIVXS zHXsAcO(7*@nXoZD06W=V3K{-ICzM_YuIQaE8O*L>WIf6ZV_*a*_F#0f#JWUf!Yk-2r8qZkLHRex12DG7d5;A%)kynJkcqnpwRQt z^MJ1ZV1>Lc4^VEJ^u;?Q&IHRjkhBJ(^&_|(fGGMEAz8mv{fD9!l2!}eBUO@mFT+nz zAAV@8H8dv$mN^{v1zF+w1z(`X@i56BjX^4&0`8q6(I=BRSax{M5FSspo#`j?- zQm$mAVkEL0%fIv(c-O&-rA}z{=S3_rQ?-JFYrC2&eJ879O@^P~M&t{XEpZNraBjm*X#Y{fM4aUTw8mxwA^zLI zJjW{E^(P?Z3h`kUi8ze)jm6|9F>MhX%)rohNbTDmJ^zGU4z+2SdFl`rR%@h35^Kco z0)@#^WmAe}TUfpq|2q42Lkx9`7C=&C?!kz*FJ+Qj-5ms3gX95YG|PtBg=l$PY4k=+ zAOvZ`(4@(wN>^}Q?!+7q7$|UGQ5`^{i3otw-{e3QkV9a(lePUeOZwT>iuSE&1&_*8 z5?l{Hn#vq=k z^aplcrN2H;!))aLj>lkg#eag75m#A6GJsHcA|6TiulS?k7_Kx5ZB>f?gc^lg7g`U( z{iC0Q8|AJUwzA?vW9x!~~;D!UJZkKS*KEg1X)z0&(8;6D*8S zNh0a~MHsLSayBg4%MZotG;7^ro)3*ZOqm79+aBS6@&V~L96p=~*Q zMt2%zC=rzE0#R2Z-7EuXmL4{Lnjt^X8wmoGY(wC<>|VBGAVR+7h#<=w=)Jo>MwW>Owqt#OAud6#uC3X`JG1h>be^&01$rWEP9gxjjYZoJI{4 zvfKv_a3X);6PLV(tf`tPrInA6nV+FzaiMWfY;XsLp>Ctu@$zN%le$h**{5PPD~(5y zPMy%mkOl&F87OG|4P9THWW=HTr1C>f*d^4-mN8qPqQ7S7<6x?AA0_uS&xB^tg0Udn85jtllSRwx|SYYHp7S5L{hCOdr0*En{0EBHz%2xFn8NS|0Z}Gf$@l?)auXv5G0>@KYLYAu z0E8!};}QquZ@_ds7;oY{8}B>3fk844IP>hXDV525rDhB?s#ZL22rPo3K;~+NM7fyo zX8qw8yi09Y$m;M0gt-heFWEe79i0VnLB&8#WBTEc=4m;XS8<~OsoIr|nUZ&cny=V! zeuKzce!8>{wCyOM(g>2&vW)i@2?i?{nlAr?tQviTC{d%xS4C;zWjBO2Uk>j`X2v53 zD+#(U`9(N4BMKBSG;MWfU@Z^>JsCg@JOjZh3W6Of;T+cBTfwE|a4T@XCq6*&g=tIG z-pYH12M}sU2z2;_12ihRCRGbGjB)+~jq9)6@sMDcS@wZHnA$e4nwlw+(D@B?xKN*@ z`A~xfChAYHhxRpy7vxLB1DYwWP14XfZ99!yn;1dSrRY{3%Ksl#P7Jnmb#{)`bE9D< zDK_Mv!&WwC9BDzy%&A?PNRNlDNJn8Y`%%MBC_QTv(4&RQb2xTCelcG#iF4r}VDGq7 zJwq4YWMvH=P>}2foo5=B?&BeRpxat^9cZBm(U)#h+6*Hbrs8z&T_~_vQt{yFpQyL0 zUMdWE!f+N`0lI=rRD$6YuJTQZp7I&a?p&M~QHxwomW!vt%|fb_VB4Q;2Gj*5jBzd9aYj^`!8v6oOA`}tTu=EDs4;#gH8m8(ZPKDs<6{Uj{p4H46 zT;i$^K}36e1HV?a2UEnWg01pTR526NegL_TRf4)@Z(~3r%2_%T5n(`{VNXw#bI`6uv7cM<{fkVv?wCWi*N zUL)!#i>7m6kt&ngYc*q%$hqBv>MoQ=RvJn6hJ9eF7u13w3f#kP3RR53Qqu(%u^ zW=gzeM=lgfis222ljF|hmdEt{LPlA=QE0uoA5*(`Aui-2(H3H`I{e0mPOnB}#X#ogo zXf(fX_z8L`l0pOu1&VtS%eiDt83@rBNZ$nW$P+Al6^nkf5#yLF*SI-vqtkGHgG+dj zjj#(16oontk)ekO*mORI2js8Ul<^;b_YG}W;mg*=lT2?LsJ1E#?it*LwQe;f@L_Vu z&CNlgpm%(J1Ikd0C>#dOZV4l1BX=m-NfVRsYla8-+|oqB3?!#=#%hzT!&6upG6NVM z(3Gu$As$?KeAOSau(K`s{T&wZ^}>X)#34Z$q#3oR+i2tzfR5Pn{069@-;HKgD5(9o zr@`sPs0Pky=?`zfLdFhaX$48iTnC;DQK7Aak1b@LJZN%4;?`fHCeuM5L4gNKO4NTr)<}u#mge*h-(-t*@6D|yX-7mQ>`*?9*)!S290ME(h#Gv72z`;D~di!>C4Js zcz}&BB@hg<>6WVjlj%N@3bav|o#?}9bvb?7*7dFt^wck|X0r3!({L70!Av>>!{;ld z60d)+TtbS|%w7VEQiJiQflj((@FQOiBLbwWXDUh#mn!pJcper9l{;!)qqa~yAA(|3 zLBmFBWJ9UFq)wVlgr8@C(=9+Key0aW*hLhP*gaRvWNRXUkyL`4LeQW6`c;Ffn=79B9Z4HOVdZ-Y=#nt zz*AdCqU*#^C=1PMUA@X{P$j@l3MO^s)1e~uXLQWKI~j+$@78URb0Q|B6|Sj|B}au!>V zMGaq*^8toS?HTHO91BBM^`FPHzrL|X2pO)b!JP>9$)De8 z&XLxoNWM>I={Z13GNa*AH9p3o%Z32!swCVVXTfR2GSZ4XgR;!pSR!eW0wK`IG)d4W z+|Go%#xM+q?XLgakw7*~hLIqYIfx7}Ddu$|{_q(wOXYVOZHUXw{(A$laz)WVc1JHb zl1L`O1;42o4KEOqSp}&)(%b4bocBRjfe+_PXc?nAn*E@ePG`pE$jv@HAp8Q$7}=VJ zz=e<}XU6r)53TIw0lvB91_n`^1(cHKR~jolu%X*0wWKA?kOu0b(s87HPRM~~d@8Bo z4N&8nwU-~Ow-=bWvfwuQE|L6(2M`yd1F?)m|B!MgHne=~Sd0c*@B#_WX@(= zAvnqrDDwbA+-Zk5fW(7jwb?kBJbi!XNJAF(#m7UpZpf82+TG}d6%$!Bw}`hRH^G|8 z&xfgbYCV7vHJRK<R~vVf=`*S>G-M1Z6O)-Bm<`<^K@3NP!@(Yz_{>g_ zC@ML`RZIrw2&u$X@XKRnI1AdXpo7M6pekbrVx<9f9dK72Il=iWctDn)%1!l>cwqR1JB3k9f*?VwRl+=^PIV9%l2C_h&tcZ}f; z^wz@DWhycn*AYm9d43mjW9NqlI0j@~I=8I%T_Mb|RDT8lD!GWJmj@{H=+y~tVC;^t zW{D>_PB!$=;Xo$T&d{3DsC7d-lvYr7^bD&f_8Vd&f_&p66bi~Nn5qmBR}yVpa$uh6 z`6mdW#O92t*uj9(mbN)1B0C2tPTAgYoYi(BQeRO> zXIwAY%i2ckg;n)}>iToY7me0Yd-Fj-j)I}VHobs+X;=fABT```D5o*>;|&sJzBXx` zhQoOynZko}6Kv#0VSwkyOMO`yB*8`2JiY;{KL`;Ri9-SlGm-{28mbB)#gaRpMh|$w zoA{GyKk<2E0ibl2<~3JjAaQtr^GHf*AEK~pExDs!IQc336kf|32n;5ND8Qu}@)gY~ z&8zZ#>B09oI^(j@wPA)jzcetmP{lLud#3UFP3J|FSHjg*;8|EeL%h40_0VY@P)N}5 zfID@mn?%8D@;bA>SP&v3T97Y$u3QqzMuoGAADu7vcN{URYeiVM>+= z#<3A`xA9n#iT<7slN1%2$$OFwZhBugswSB<4P_V2a`+0o7O6iZ85$iBO`=NIs*RmT zs!=<`Pat1%cez>{DQxZPY;f1nFOmEWU!mqCU_bnfPWR5l11iUr(W;OjEI$GG#QUWh zjc=yTbFC`HD^OJhdBzW1sCtah0?CeP{bd1xZ|~ zf>klbi5Z-}CDyhf!m=QMZ5qSNv^o_Ni=`X{hc_U9(qwtTDywDAj#c%}Qp7nRlNxrS z!k3fo1;sbJk5v#0N>fSDgUIyGKonH83<5Ia7uRlhfHa?-2i*bDHTeS1Fx9I;U%0jwSj@byI+UH6l+9d5}gbH!wg2Q>vE7X^Ose13~vyXk1Wn@e2KK}hy+qFa-^ogN3-uw z`NAC=8x0vv;TVO!8HPv<1D3`giuOU0NCc(bMwkZANnRxJ#fTqHV>-Gd?~Oc{GOD-m z-GgQn7Y=|j+C6zc*Ulb5fzqToxuW`25sf#1l7kdtj@$AR zSSJ7qSQEWKq6BpqmR!M1T`1qsxCR9)ui*H^y4f7D5m7*qK{(&6^OiRN{HPq+VTcZF zji4y{1Bh`#v&A@XBuwWRwz5QZ?~nVu;lOT32#U1nTzuo+7!CzIO^gLj#O}13Sfhla zEN+LbZ01Oa2R)qJTxu!H!G%D#lQhfO&Od<>3ST7v9N2^H7gia{ZR2h802{o4;=R|? z0_Y(w$kKJ3m`m|@l6FBNhBr_ofgSK6A;CnBq;(P=9dra_EVbbQ>_7DS{RV<&+qF-1-8)axVeNa4cxqFCJxvSm+$dv|nBH&5$CwVW}zkk}N z25O|Gac8i1#~OYI%UPC_Hnw3rEsAniV+ zrVt+S*WU-Ou7IF9h)5(q_|7^FGf;;Hi9{z?3-0^|qNW z)}^%=RB|nxPz(}WM^q+)Lk!1$%kT#E1ZdOr3*SZOj7_BodE_X;hlz1OOj0b{n1zaO ziL#(afh}%lP&6&IGm|@O?*!&RfRKUSrwl`V*T$CQ3_n4K81Kgcg34#jo97+d0dC7p z$Qd3G=~^&RIMEuLK#Hm-(~g#cy*b>8FhgmP8;rjYF%neI3g96C1^6@#aX~Hv(D&nc z7{H;Z;u2gtPq_b!p$ac4s`m!>K@v>wz&50ff@NttnEyEJ0}lY=W#WNwkAoyYC(_j= zcp>pwegd?}Us=o2AyBiZ8ofGr+WkDxOl78mx~+5(ZaF}({!@FT^8+$2btCW?{3a3! ztlm97gu2NKWhXdzfe_s5ktrB4p~Pnsvzk!uJ`$Du$IBXgb3Wm$B(OT!2q8@x(=Ws- za@!9XicRyaVF}RGXa!5k6M#yTT~3mQ)q&(ynOmNKu&tx#jJxK?EW(&mvy6N|g!~ zOPy#>0``%Y7dH}02_j*QV{ijcXjC8pE$TLZ9kJz{rD>?K9t7(0+M@JS!iJ*35O@q} zT`vzHUEw~;iKpUEPe2>pz2@s0w*GcrM88>Zb)}#ulKN+UA7j+N(MnfV9}ko6knEuI zsBbCb)AiA8&^!ktq3xZK?H<703`tsxOh__<&)+QPo{MF##h?R^WnJOJwAI;i{%_TcqR0)dGF3W6Y4>DMAE){4UQjV0rECA zs6rZCuK7&zc76jf9L3mtr-mK40ifb($mxc^0RY2KKn2X$yFrj{M1_y;06R=x(KP3S zZNB~}=nkJsnb>gjcTkqw1+qqbZshmy6U0=esx}UrXgp)ho+!h%lwULW=6$QS)!jIP zK77BOBncfykHAt8?G10>m!jq=z+?By((_Z4FuJK7=V2p7?FBUGZOHh=WTi$rB9w@t z<+;lPg0T&0R&W4x9;IhG6#TzNKm5=UZXVw%*6p~v7#BR`Sn>1+k!Qv13`ni<9p zL*ZZ$`}s4vQ56>GG??a@dtsP88jr;~PmCN?=h`65>VVtYey(x{!UWM0eT|cJZo?bU z7Cxx&{C-y1B7x#p16p&y1vN0Dyn%fr(*#}#4Sqkawki~g(7di)%CW0l`d)12bRMSnDK!B2UENcM%WXQuK+t5vsU4||B7wvRu z@N>>T;gwrv!f_rH5WxC&+=}rqWPkCnhX)}2D3j?#2^e6^#8~y5G!3Cq%T|V55%EYR z0O^ne=&U@|9iz{N*D!oW+)fLx#(@F3Pe4Kb0|k?d*`YcYUEd_BE<&ML)vN^7t_kF~ z4He_cuVD>j+A=&3!d$Nz*W6D563mt1}IkI-Y1% z@HOq9=>h6p0w|kDlGB78rV_q?(~%wYVIQCoY7g>PQf5GZz$yN2eA$Q`kjC>@;GOU) zGjOIgRUUy?8lar?d6sG64H^#O3%C(RS6qF>cR|G;lk;g_IS=R3SA);|mup5D>j52w zY*o}AKoq`B1B#QuAnb=9H3Ld`B0~q-oQr9U+%m9*Js<-R=(G#Wkl_#a8y5_Jxz5!x zH4h03drUhA5X`((mz2S_gi-HFqH-TDRgn0lClw4q7wlxv){(zu-_rlLA+$Mzq6)?X z!hk+8k{crcqpo71fV9s0pjoy>6IC1j5Omme5^Ee&GOg|K0K*=nS6bzE5!N^v1xy@+*pouAv(@5S?*)Q1FW1b7yL3El5Qh( z)4%m^z&W?&CooQxkO*b0x*#EROr*m`Vsuvxs&m^Go>`rvL7841Im%_EbuV99oVwcg z#%*Yv>r_hBF@#Zd`Ol*)XQ?-=0m3;CR-c{cqS>475vVX`Aa`TH?DPOA82rPzZ01*v z7&zQ^B6`TO4|oKEJ{n6K3MA73D7#J}Vv(4K(+J45YxFA{=S+%%jDUrZz|R*Qyu$2! z>@V!VNAi8GR(}%pxowEfK)QZjgPbCuNIap8Bq57L`Sman5*{V-Ssq|33npMtk&z>) z#&xz15P$Bd%No!jI*rZ)R93n7g(oYp_zY_Crbxm?v^YF%(785> z*C_}OG+muHi(Pmr{6t3u#}JuBbK0NFa9y$BVg!wIAxT^%Qyoqt=>dQHNB5wL38hMQ zq=l-9+^DSEkYwTL@&OVYiE(%b=F_ZWhl!zc4yrwWMzs#gzd8}eurVwQyK8weFQbvV z4UtKo)m$jfM)nX40}hnEGvm51XKWD0nHz{S5B9;o;G;wPE-gTP;9{X|w(J8*shS@U zh9Ip}q)S5w0tlLzPI)_ADE@M@D-p;v*ertDu|b#(MA2bcN?&b>hO}GyeDi6!088LG ztRe)-q3f&1qiD+r6c^Cxv-VVJP36dTdl+Nu7P-Kd=sZ}H*R!5~*X?*kqt z2%3^A*@Wh~>IcXw>jn!GeGd15%nq*%s|?;@<>086Q`QX*QJ4>Bq0CNlMq{ketpEMf z7?!GL369ZI=bwOvM)y(e58V%dkf=r6CT_w3UVZ|H|9G*ka`a$ef*n=o)RF>(hQp-M z3s1j#<=Pr7LHYfYj5C2F*dCa1inGus@QVoJGD%QmNBOQIo&w^$21OMYDw?o59-&28 zD@`j=6Jv&3nZ*@Q7E4uQOr9VtLlT`zNrU#|a3>la*}wxd1b94K8z6vIOOhHxtG<>u z0QqEzs>)JyY7?QWLg;S7WuV}&23cy8JZKS!rWm%)VqSW<3#%Rqn1o<5@sFVQqM<|;|lbVNkK~!-) zEf09HVd8+*>g7=``A&awmw4$sEf3%X{j52tPb#uDMU!^~8U@9#-8DSG-zEe}aFsct z91z99{?x2#NXc-ROnxp}) zU5B55gM#AC+!_-~vFTMr$>ti-MJ<&p=)}|uVV;iSq)fB}cQw8Lbg^Fe} z5-g7Let@u4307{H!|)T3NnOObX+jU=D~;3I{y@<>)~06=tMk<~nI_z47gBy=ehwHQq#Rz9alQPm5ZXE@L$EG5xy1!^jm!B>tV zupK)`%pEoouDH&Kmg!xe5x6c}eGpLEiMHf`r8U#4shzl);ZDJ4bPUmM;grtkum;dz z)QwM+g&ZWgaXN;;ej5umF|0w_La8GDh{Ujg*=8l7>(#B2{+=(C;I}Yf%rMxzu3uQD zBn7nTQw&_3<^~!uP>;qTXts=8h2WQGzN~w&_7Ogu95t?DU$zQ7p%K3Spcp}z&4oQL zBB8H`H1&=%$~Sluq@?%edT4I<`C69}B^}|ZLVuvaMLBL4X_H_f$P~|k#fnR5VkW}0 zGVk~h>+rA5@q4$H=X9TK0ew3Pn2cR=(4cNCUzK3as?-(38sLlE%_XKc24xTvsD#n< z-r7;F+Tj5|&lx23!tB!2aQ-h&mC_?Z%{f2Nn?b1)3t)jANcocOH`+oNDodB5tRbLU z6<|?0!Wmm9IcYZ3FCQ4cApA>yVQ7E~1fDdIx&)s{XuOd$Cjjck0ySC$E6s<6Zk>Mu zVI{V=l9|Pu+gYy6bwK`1O~h;VoF6DwYSfDZu2T1+c%03_l?=-iWpQTkK!*gQzhY$=HchQo{qTS})nVVk^2n1-*L?D=qTu z@&Gjx{)L3bvbh=Roh!mY=El1W`UTNPQc+Y(6p8;%q9?f!7h|D(oO9*ee9-|jh&CD4 znbb5U8wGzrwke2*HPE%je=&#ylSpc&eX5*>i84|1ShP5 zrV+j^zAg{=ZVJUMHlUxbRR~cr0aNLnVfFBUKhvn+|s^YJQqzXOS!axRj-4`u{wdh;Jw`?ntH@`jI$>=Di27_@bwZSeEcNo}{Wz zuS1|1g!7KQFpj58$^-S`^9#MfE=2gH#Qqw>JN^Xey#oMP$B?E8Z15LF$cv*i;E4_( zQZSEZgGY!)T!w~Pl7z6q#H(3iqx2&CNV^V*o6zyYb_ULYS3fw^oIj%>$|kf+ZD_Kr zr%#8M*Ytrr00!o~d(6-brUeb0pdb*XW6<-3b)}jNGrWN@D*SY$RndvWNU(!75L)o< zZbQM5y*LStZ;n6%)B+3PQrU_ew`obvZ;*WjCsw9!@hp=7;KCTOOd^RKHXLUa8Zc73 z+geCaMM!=EWQMq9qV164xWE%Di0}>%$1bu5?Goi*G_B=QkM^9J-O1d=4a%A6;iZWw0~R;&+iKbZ?dc8OD`j;+LBZwa>~77m8Md zU+ZTyN{EMuOaT^@9gDPYpxR;ygZ^;NA6XG;4xsF&Lbn54_m1?i21wg@FwP?|9=Q37 z*JG@&QdrMqunMWhcJ3?MNo%bp9~Ku;o`zgPOg<;D6x1 zNwy-0P_rZhVxz|u2-2T`925Dx1{@P04^Yw6%Eb2S|FoHsWvKno@#L;g62}TSmvYG! zr6y#EO-nosjFjOGd~$+k)v~5_gwYMus+u+~+eSIr$KkhP3!@CM4HFoUdgjR`rpL1rO} zxQmT|EH#06i3Ee?fSyh2;iGBI9&rt6nQxQa4;T`+(wf?%`ja!H5?%N@pYE07(X*b+8Ke2KAdm5zCiO>(@$7U{Wk4B;GTJ&CPodHY6M|OOcE$KH0w%E#R#twu3W2zpP(}c7l&iI zL1}Lw)Xlk0y%05LfWj0PHiFcE$`<&69h(k_+Yjn6RQ9}xU)rq7+)F&sXdB_k#B{a1 zyzk*-c>pU8^*;)Yt4!C@|A0~aWY8767wJ^P z56)~6v<;UWsp(Qi>`7|aH|UYLVbu>5a*z~x+W9B==6!i6I6ogLfjSZ+4f7ljWG?FP z06rpaKj|qz*>R9*$yO1hkeAiM!vol+u%=>AMOvzVDq(gvQQV@tLUx7+(7#FSfU(Uo z)p2sG`5~5tGseU40LgmwiC}2qvpu5;^jR`{=Ey(%1i-RfMA>f~M`Hvy8~dhj_`>j* z1ycozXISSKD_As)z>v>M;qEmNXZQ+IJM#*?M$(avvCM#q=#$#ECgOP!{eBAjT>3B& z^s6;Tl2YC5aMXn54Ui80_+V1l@q)FbWEp-}I+(%wPah>gqX?jM)9{ANLLnS0yk~IO zaGa&7t|J}%;1~ARLN!FmXpVl{f~{lXAx6@W0nmsSxFcZ(f%V$aKkq|=!T!^{4x|^e z$`nf+_?;*@9&E~B%3%?c9>}pB0@Vsk?FppSMNR0Z1Q@om`UYbjSpuue{7(%Zj&-=2 zP8X1Wc>~l9q*`%-SlaOmmU5d!9nCNJypixf^fVNnRlpf3s@e%k=)nwb8*D7VF8(D3 zfs)`az(7)C%YaWqr?gyYTIY_vjKvULizT(ojR`|euM-VJyhRbC zymY3{Z(zT&=0#(9Qud=+!vWNZI`o>vUI7A|}?^u>r!$bNC9vU9-tf3(+ zgYFbAXo?0eXd7eP`Pbmog%L{(X1Otm@=KRv*5x4JPDwscC>Yt4xCGt_V&fJdi$%%>^lravxTp)1&$9G%8(VGo|#8>zTam*PoFq z65fF&)3n4IMwNwR3Iy&8;kxz!5RQ&Y5LMzG<$KJ(Vuhd({B=mvg@70-5=a4+De=rx zdE3(pAtmXrab+zLE=b&r#UKAz#+h0YF4vX-Vfoj$b9yRD8`R7+RWbts!AsK<4@WI? zDFhTROHsyP%$a852iON8P@Lkgm%_SyLRORBCJ;pVmF}$)6p2A}(6}soMhT53r*;=e zOJKV|b2?UK7`GuL6&~Ok#`(`^=P^#G5Y~(04=gG)76g*YRQya$Tj_oPn1<}5rlbNG z@Wp=p2Bg_$I;oNHkVfuc$RwFTAX5SFRMvp%Mwvyk;s^*J6q0zyC}KFr+VO=&)Hc>s zk*m8=i6fzbU$%yAz+cxWi(mf)-BD5lxVD!jsw9U}ee3^{njzK{qH-qQ>nXUEGJY|m zp`wJ6Ww;+IR9i_}4FC0K6ursMuHKxEKzIt_vJln-oXpejQSKbqg7UmBP6nDzsMpmJ{g3}Y;uMwpZI9LW_R z$#1~T6e@`%FO1Vdo+uTv2freo(DRk_soZc);Ycogx!$k$0pgGU1K_bUMxT%~yAO=! z@pl*zg`f&@i`}?(H7b)Z#=P8_#>TQ3hFbpm4HS52WD4lmm_mMFL1FZ0z204G3C4skl>acsn+a-8uvq`2B<;4Ll4U;hMu6lrG~ z1aQzW6ffrb_zm&8L8&M_fOjp(!*Nn}W+11N$krj*W{gb=574q_N5c{s>DQ+>Qb>kn zFT1A!355rUeyW=c_4<|6` z7;>Hx$d>x`PiT84SqUMMQ-tpuR8n@3Qm!yzT0V_C01q}fGJsHoFwAVl0{aImtVGW3TIUs_7T&MAd5mOW4VS)rX zg{v*r%zY}BxYV<2g6B$cIkfnU__LKaklRQyX;`qfIiVmP&}YSC#a|aH7QC4@I)X2Z zDOGl&311q%&|&S{f&e1=2mfFbOCwN3c`rW#0ytY5+J%6k+sz-|S+oRE4aiS0?lTQU zXgL>RA^?+<4mVQ#6G2_7Y|E`)X7|V;;=_dEdghS?`FkLR!+K#v^D(Ony2)n=> zfJ)lAn4y9+e6XoGq@RUxYmFNjITTD-m&7rEK`BIC33q5tVG(6#JZ*#M3aXSJ_9FHL zGDJ++3N2F|71|56t4I>yB_@K-r>2d(l`F@ucOuVA7hA}MC{bbpmL4}0#!)?Fu~NND zu9OE}8#Jm-n33mpG5kU|RtxRdZy-mEa|00DNeW%nfli~Zarg@}C{zdO37~JGLFoJA zv_W{qW@uot-NGBVzjV%Zo(a%py$1{uM2HN8k);*VJRhE)Y$`7dJus^7xhhCpz~qJl z72ZHDX2(bt{hL;sjGem>N|5bI_*!`YF&rZy$OG9ns=L?EF4W9dycF(6bt(;>+g!hu znuFO9CPS*f6r%Ua8f2OZ?u0T|OjEh(k^$q$By~;-t#g$!6_~=4HH`)dE=JrKYy(iO z>#C5gf00dS7h-wJGrT(@jllzwJb?+M`0GWKQ~2JQS&*>{&@03VazU`Nmn*nfVlW}) z!sJ~~N=4)W!y9$t!)Cz}D=Z=jkY$UTDzz*vm0|-q4EX^nB4QVQg5?H*xDEi)j1vNi zS{3*_w7*~tOmJf$a=3uH#?%kQ5b|_YY#Lx-h{78%_}8z-=e%Z`FkBWW&;?PL-)mGK_LI+tlogjLT_-s^Z5t{{x0?JOvQ5;2~&h<>2EbuJLq0nT*D{O*QS2J+C5Yi~TDQ6u1js*MDsUb2W+8L$|&Pvx?B40Ud5-W;E8<3WV+(8tJLTW z;Z(gEfZ6;#`rX1CAo;tTT~9>L8E}ooFSXHRaX1U|0}dk6Ip#cpWMhTP5+s8t^*;#- zDg}qw>EK&B)<7h$=viozJT&+tqu3P^O%Je!s5^*Q4pJO2JWtVUmLnPNuh#%9Z3iX0 zRJDbWp>l%>B2t9+Tew_znb zA$^6P5Ilf z<9vA!ueTVt`O)3+d6>>SDyK^xeevkaxO*+bVlv)*o(t;!zj$kAxYOKO3vB&7`nkKj z`{QmN#zo8bb$m^K?-TyZf=KeIa>m_ddQExsYj>Vchu!-*EU)G89+m>G@fU9mAS5Z? z@50Mo*Ls@g!|thGJnWy#vs>n^Yu&%P?)g0Jj>Eg{5Nmz*8DI0uHUFI6hv&5Q(O|uK ze!q9;W#1lcY1Eef?G;+>ynnwoul4c0UxxX;JI%w(f{aefa=h%G`}gwhhIcnV|K4f& zm(MspM=yNX9jEg-Ywfhybi4E9jr+Nu=hx;(s{_QGW%oYW+-+YwDa(!&aOtfH9@=y3 zTH9*R!}!|mpU3g%t>?qVX`fEd_wzX%U%$R2|6=pa@9FRyhuyn>z4Pd4M>pp)>Lg6> z^El7Lvh}T>hr{dgwlB`h@bl>Bb8^~GhiRJM%emYB=;vkb- z=yzNVx4!iYOW`@ryYuP2LH-L=&vv8dGrAO(Zm}lAWahxu(|DNOe=P4s z(Pum8<8HR`+>O=+E+UiKdKDaA(ADSIxxm(gE_^&+ogK2ceW#aHaP#JSy^e@0r``TI zC0@S{JAq@6;_zR5AN+xZX|jL5)_ho&{rTDLXoIrW&doG=>(?_@-c?<^}PLetbD2 z`}ya{=$RdNk(fwL-p9Eh)cT7Z^45EqFY~THNc`GCpHBUAH$7j|;rNtc7n(YM{%EC2 zLePHqe$7Ae`f_}qckfxsm&9x;w7~!3(UTlzwz$h8&uO1=@?Y%U-TP_dD3T9b^XvYZ zEabA^&8JI?At&4LAo2+x_B7u5Woym{PT|#)*nd%md22D`nrdy8UzOTQhKa zNs)R7eR$^4s)Fai4)MmP=h988gN+9r_%lt$<5ybx_rz?qdji`xKYDtq zi!QtF*|i1n=>3!nlAv^YKF4YEX&fdI|MGGMm*uD0RWhm@U&psQ!8DM=)`LDM!zb6t z?zB8x43S@(A?8n-!3qMQ72miZdQWeto6p$4+GmtWUixYG9=Zw5{y3Uq zy|IS8@RWkU1=guGD?nEyKXdz*@LuSRD3*I8jU5D3e+`jo<0Fk3~ zPa^!YAmA)1)Wrrl+Iv36#?uI@IUQYAu%~m|*LDce%I^8xzoyrzdrbub)SoZN>F~b1 zr5wAbJSq!v-p#5|Yt1I-Y1bomZ+`3cXxPf{f3(Zx^nQz{~u=zcp*yMwg%Pl;MQGK7?)?Oz>=5+VtvW=6nCV-)7Cs>6eT1xa`o5q z2b~183);^~Vm28g|J-_`kLTB{K71=)B-v|tdIJ+xs%%q0pwEKR>(4jeI79VjfV<-M z2Qj;N-<{OKMtSxN@M+@?8E0=jTMH@WPde$CI6*_CF|#u4xfQ=2r852H1$7_YT^0mb zMhz@uKrh(u7mCEK`&wORJi&RkAkVfS&`HK|yVv+!d5t-$O0iRLOr28X z=1cbAFcTZPB)>@g=ODto@xbOps3G=xJ=J3z}~S1RWA(9verO_Jv4thY>n4HkADAWJo-6662Ott$@bR2FP((A_50=N z3r;K40>$a}FNgDdw*O$Do8$Flh|%Q@=X*WZ7$WDJgx6U)-99Ozy2kp|$JZ zeOr)4h19vlY)DdWI^=@v_|G@rgc!5$Md^MzwW`2cBk%2=uAj@|a(|Co$W$k<$KqnY>*lp?1dW4nf^8iRKIDFS zZ+&a&sGIRPOM=(lF0hOwWcQvfXaV!sFQ=_Hx*J>4t5tV?xewY^aF%{3I9|$MoQq4p z6?LDdL#IjN<2_{`9&Lx5A;EKx5YcJSALgwwM6>nzK5NNnbNqpaoNPnj@I)N$p6Iq# z1+EmQqH`MiKcE(QweWYrM>%vD0LxZbYko69nHBx7&IL9on$m#9xuCDo)}4AS<&FDV zk#w0*c){#zjV|X?(xtS?IB$h&fn5LR2VLDFuY-kS5&iH(1dpWKugi0J8TItunqNQh zuX$N4yZu`dX@`hR7sc<~9W+QqRMOs!3nDHvk1*;pD^_nFZI`rySe5 z)|%%10wQ}2uRqHFgaZIIn}`0rHS+1I zy$8D%GDZ>HG2r@cEn8dQslLxOMJE{WWi$-fq6? za|Q+PRZWM@B`NaT6QFE|K(n$S(n%3(=cakS2Gqp4Ad~v@7-DmDGdOv7U{{hy3m@7k zddD)hw=RO#^u`?`yJNf7jW@=IrY&uKv|`BQdr0ljHMrKbc-EoO zZ5*HbCpBEb%$6S33u@4Ps^vxT}V)`J@ZG^xJpH zGI^-FsK-Ujqs43e-rb9vl?3ysn%r9LLPOpY_&Yh@5B4N#AOmotpzSz=) zRtt3w%DO|+Z&Z;^Z(UNme(UCY>5)uJsDjI}EeLe-H{Fe|9-YNZ1yOYT=COkm0XT6lyu&}FNo4_~{DQ_jt7|qBcVDo=*SkBUZfqd)@2eYMD2GrF1V>GQ z^~iW#U>uqce?Q@*FqPrYW5`Pc(vpUU5Le{U@DTa0z6=f4geSb6SRfrhrw~dh;(pc| z@TufI=uj`0=N?$HF{paG@gfmvf1`rq!qow4Fu|1zr{e2<(8yiGQB53{RdPb{~pqa*pyv zg1wC2!LycX*1Y2j+al^c_IRhS;xFnr?Oa&~F$|$Xm@yY(jYKrA!7UqNu(LNhyX7xpw=~0s?g&GhZek zM^o0SPci(3Ar7^W#yaP%5`LPhJs%!FSu8!>}%SR)jFk;z=KEIb*0=^K&S@q}R3^L-xo5HYEItL*pUW?tE-Y6H#Kbj5hE8h3{) zuMv*sfW{VIE^KgkGtosFt6RPwhshiSHBT|zK5ZgM&;#Qh;>}_I%8GKPw~f|O^J7*#eae7UD?Jf%8~Ae>VDzgrP2YXv6z z$Wsi2Va0IZUBQ;5D_AT2yvv!z9&h!7i#G3~znHt)smK_~>N&z?jjuP;I?s@KFv+4C zH!7AHT4jX7&%3adjfEnEn0#rW&Jafi+D z=B#-N4|rDiJ~PJz%HlV1$1@YkvR=5x@e`^(g{S?UUy5q4oq!yK4xSXJFDXd% zDST|q-HeS$%RFS6@&?`+ZhW49)u(VsG}-##Qfm7}IV)7%Fz}r9a?ZfE700Vi)kC~t7H3A=?!kn1^$Cog9e zwEaYe4BisR&H2gPsEViM&r^8)S`mY3_=?ysagR4A<(pSe=E)b=(?id@CSnoNPew(L*|%L>O`&sd4s zef24*jBYUANo!=txD0ufmEjG1{56YcUESMn$(q?2os3)I;@T=<7AnlaTyF9!(?H*KMt`zPU+fhyOfMxIc_JRaGZS3F8dG$x!9eEHtZ8%II0A zXK;h@dUqempngu4kHh<#Q~btqWN924rKP z9!#=$0$fGFv72i^B`vRGV3<6}oDy{`^trma-%4`RWyyNlJ-rF@09Fq-gX9fH$eEU7 zLZf2v=|q^^TST|qhWC?N`~XtQyD-o8os9P|tJ3_bV%99T%y;Dj#pf_z8o0&mpB1jT z(qdI{UHk(;;<%kc=fpY)n^~dpT7-v|R~Jd#NG&tSV6kozc&tp!T>$dtX##!-jHs`$IyMB)R-5F<|ZwzAb z4q#WvQrm=c6MeH^4B<2s$4$47-|?|}0(xmSTVww}W^J%$HjF$;;FyNJ7Fu&_WZ>SC z-_VQU*>zq9HJ2gdmpSU+<#l0ZnQ7f>A`@agB|~Oq0B-s(AXTweBR~QPn^KhNvRM~4 zg8Zj(8x3?Tqr{$~2O*0~`r;{&3rF0fSbpF!%`JXuSUDXhI5hiS3YJ5_XEmPqT!4$! zgmFuzTbLxk<8i%@q^imzISqKIFfw-iMy7U|FvFzJu$N>Wo@dW7ktG;5)VlbKSmKM@ zppsAOMk3RUlF}>W{F-w_#rX(oW^$h47sU?8!GuI(p)3s*LcUR8Al6N6VRaV9;ys;- z_A-7$XyPgC1Sn~=XN@VBDON^1t*Z+m78+PI19Y>xWa-AgWlFO#(_{5CW@?CmQ1BK% z$u4n2Gs=#MpiH`2!)jT_bf3StT21uU^aO)uZj|eUlY8+?tKe;TGvrJswReS0?#FIh zlNDeg)w?m-iaF6t7+cP8Z3dD0CNT7DyW+c`qw&z8dOP4Jgu1vxm!XMcy+JKR!B_BF zUB44Ulvud;!e;IP#^Pq(qKI?8WSLj}Rjuf=yqGij+SW$1kpHh~d7zK#Szt zQ0A76^{;bf7K*hg$gtO6bnbkZIT>2UaojW3=21OM%w*d@FwVj0w(Z+Pj&yA)FjZ6a3<1v%9oGE*&xZM;;ov)z^3@i z?lVy}`z7O_;h0yPm+;`Ld)NOMZ$2~#(Jo(H0>OIRZ~|#sVX)-7>ojRN?#6I-#in@Y zN;)5Ra}rGWt7by{0TlOqhSMAKi!_(-W>adeVX+jvgMZ#dzM@xeCZSc){So*Kd^LbL zW6cYLtbd-OH1|&dV%wPx6e)AyHBJrtI8}4rFOzCOkfu+qrxANitBs7(FLl!th?Y0B2tN8w@`f-2s!y?uCLCaTsLsTf`xX7g4#tLIPF$_DG%n3Jw4$G2nUXnZ ztQH{vu;rOOjEeL>jN^;rQ2a%P?8#q@2sa8fr;*KYhO^*I$rQ)xFN*Q_xW8sxc8hg- zC)OoXx6yGP)zb)pIhsoQVOAz~n+IU3s%A@W5{*oaHSp%ywOPJpO)q2k3~3M%JOzd? zk^eTyhednE{dqkBH>G&QZAYWba~e%lS4$gR~l|>ylPnFw=pt2>$znY-^H>fPS4D)Nhr95szcMx*hevaX)# zI}A0u1)`bwqR*%Mvu6nnv%@B-z6(Qk^AZncezu%9BOo${ln4F*qOCr~7@E-}BV&l# zyD*$)eXq|eq8sg7eTqS-rlCp^8&V7a*K;JCzd?jdFRMN!9@n6Eb9r(afgtYwOzt+~ zrs8t7e(C!iO!si~%r?n4+JvjJji5=P@f()+yxYG*+m{%k5Avg>t)B>eYrjecE@q>L zD9*FOrt4)&EK_=Odw{*wr%2{d$}~b-uTR*z$$%7t%)rIL>Qhc5S(D`92e%6;MlaIf zNtbsuI&}N%m$y(d?$_MTy<+BowH|WU73+b_QASmA5+p!Y#^hT0RYr}OjEo>u{6)Os zI4oe9qL+G#W0?yU4sM79`bG6AE)xA%8M|2*b{+}=%AHvlBR)Z4E>qii9j-`E$(y-( zZRZ(yro?HJmQ7SBc2WstP8u$1fKMu7BP$GV^v5F{x7E!UP4j_lm(vkfv$!suku;e# zW_pCZ8y*7*#=vzW*PKDWBSr-Kc%vaTKaEsikn8i!q z;j`w>h?NZ~U?;2D<{6ri&RB6>%z-mAS98o$oI$@x!6Qt-;pDN^Z#3ZP8OdcRR_=k! zdGEhE>G)njiy~Hi7X!Es>S1%uT2`jDY{XWGni{=Tj;INB08PDi4KY7RRtAGN+dQR- z1f+PIXlQ6q7`AUO9i>cn# z39;^c3=3rILe%b;bC zf{I=+^KX|lXAOY57ts?fVV82c(knmcN~Mx^U+-;ftZwNxP@*Kb_P}mVrE1xowbT(~uB<(bv5T;%u*rb7U8b9Q3$Vf05D|4byP1 zPw;Q^9_>-vOggmQ_AbV=fTPqlO0D{(H30gD(a6xG_7tH;#ME)jtkCjlRNP?-O5y}5 zQOczq=$qMN?uBaSU5{Sw6~-2D);)b;4vE^9J3;MM@LF8Yv=reklgfb3mc;`YGB$^J zfKInsQ+JVlWH_N+v&)#{4%qmlHAD;X4Y zEo)<<>V}%llrpo%EaokSsLnN=_Cx+Fh8yx)zLQ|q>JXdErQ%CLt#t+rfJcxBr>eLv z*y6}-@+rXP_Ur>}eS&|V7@@UHDBDM$y3>3CrB_d6hP4moCaH;Aq#_ZaDjCc*L@TaV z#1g~AC1@m#fZv2upV?Qba}3U{BD8J|rzR{$IkzwUrR8p_ItDsIt}?E;y5_Lsb|_>% z=^A>MJmvoBQ}B^TVg=b|PC#0JS7>I_(y^DX{-Ue-a(ec31}zh!cyMor(5_KX&Jb^; zC6~;`%NN2$#7W?|o3Zl&1gp1+zK<7C+Wgcdry=9Q2dc-Tr220>#T-h4Hv`u4E+?P( zC>WDGN~f#m2!jSEn9;%~%n>h-%|qR#E^jA5?&~{gF5yQis*B%9D*~wMnr(_Eu;vyf zqf1dFX=JCBSL_e~I1v-CCV&ss=xu~y5(F7t7>xJ?_!hZukx)!6uI8EkQQZW@j9?w# zgkBnM9Fz^v6=cda(jDiI0Q8vNO>S9KO+k~jAo5dpEx*e#I7+TD)#LGeNa*imR*j zX&&$e+q>~Bh>2vcx;ch0_M&<9DcXb59&DU;!^(aXZ*Oi={0a7Pn`9uY$UZYsE z?LWF7wNjCAAo1AS>o3x2z-~E;qlVlTPh@XnsH`&;IA0oW?OyD}Q28dRd-1OBp;Lg= zM>^FlhjFOc8LNiJle(??jYK7o1r(sUlZ`vH-coR#R!H*#_aP2{VNd4K7@ zs(d@03Ca+tCL#4&JC0G3VoB(jX?GN3okW!xyvX#m-bs(UdJTIHWaYud>_d-HqtU`( zx^2%^dnZp41lR3XE(~5e8+fFIk>I7uMfVgq5(m^I^7%$Y#|$kPncP9)WA!v*K8}Vy zlitV=?@F!#9m_y<_lXG>)tiL!Qt~bcuoM!qug566RRRVE!i382q8ULR;7hG{;}w-% zd+wU1D#l&$OP{VN34#oUXtJ`aqTV!ig=BiF<;Z|#XBxKH>lgS<$zgQXMb?vs-c+pZdCK-oYGE-b#Go>G2`e}Pt zwRfQ&x{2qSR7zQzlk!vUBnMVU8_PLj{&0sv!nqE5@5f?>2)0y#upw}y+b`muF9zSn zf#8Z9q4+KWQjh8P#Wbbu-7hV=3z+mwje|W>eM)pO3_7+@#7#*S(6zWOxGy)fRms;+H6g>}_`{4yXRNCDb%PvGzHhu_Rt4uK zO-Fo?!0*6!*FbiiMwX1(eR+%ZLp39#tx^rk8$PORt{R;Xd?K$E%e*~K*9$d{WKUbD z$cq=a(c%E|XR4o|vkvD)&Oo*P&6%V6i>TDWs5i_|dIb?viGusxLwvC}3bX9jitnZ* z!6TRO{Ad@pxEu6n4cvY-HEi0T)+9JIdXyAF#j8ppP(a5f1haqb$GU_36tZET8IiEO zF6ayh&U!D+hYRidjFzUf&|ej)iYGwz0?NU-gWdT`&f?v#GEc}|Usj%RbE1O{+XH?W z9t=K@HXYjp&;cZ}_%2XL(nQo!6{vU7JsQ50jdy~VOPSw>OX0(~seyfVolHUO^VKxh z@H4Br;hs=IMb&hZ?QX=A);4FPS!1a3a-GM6(CiIeu{ zFj(YIe$f@!h~n{Wgldpt?bxp#3z54Au21z$r{Tl;gja;B)h}YthZn$=1v_1vqPQ+m zwRE5bvYUqdg>4$n#SuuVy;^;Wu`tv}r>IFq2FE+%ozCvZs;#mBdaOZBq@i=^w_SIR zBT~99UwW5SHw@_x&Xn)^5&KHRU+h*2aQe;a()9)N{X&{Zx3{|MlrXV5`OG^4+yfEt z$19Xt+%>dOE6dFwYJFGVMUHKF3ACVSVdhB_O!vJ4EYl4pmpSv zOu4wQxE-tcZfH};Ay9m&P~HXSDY+Gbdqh&jI>4&-+6b^9cVo`9yq$^A9nry2s`^b0 z+;!FRgrJ9ZjxK(c0}0bdeO2GrP5{pO%^y9SMBNs55tYzZdvWuwsa4D|=^WCF4Uf&| zvFqFg!0PW&o(AtK?mA@+7P>MG1vvXfd=onq<DuRnzsNAzMi5^Y^r z_F)xh71OO(Gu5GoHfvC)_u6U_zO@{*V1FVHXtnsIqec;6b#GQSJLAtN$jAcE{NJik zs*n9HFD9#;Shr0#JoNZmZ!5bSzO)r z42;?hyUmBoAB!E-xyC-h>$s4)Uq|KrPoHm3u#S;n$>knZ75{)D&Qa!S=dRslR-lOA zL@yelh?2zGrplOvg$!guL~6JF zw^WxYD&5wB8B6tsCBPP>#LRY%#DpPA1D6^EMp;^17re2%%JVZ{NWz*NMPwqGQ!@A? zg%rt_u7cxL*Za`kS@N#1u zSX>udH?Kmxwjyna$q*&fAbvo|+~ud}d){I8!f$GOJaTF;3wN?yzFg5BBy87y%2E@;@{9M29U?C(^> zF*U5zce*|HaN93uoJ`Gfj&}Zws=EP?%>&T=>L-2B5%UzUFDRtvlG=Wgrv@S65zjiF z*gwb#c?!@Nx`SdiRCgU5jCOg?dvxXFM1o0efEeLosG{f4G=fXUpAf+%K1lZ-M5atZMru?V}#(aEo7R)m>7~0}h6&LQkx2a&Ms~X`1#8{aW>17@jl? zQaYD=8mHna5z?SzymP4T-N-HgKpGUQ_Qxkz)!dW^?7pir1#s4Tf2!zWX@fOO!>-?2 z44BRqs2aVooB6*;SsZ};BCY#}p*Fl-4C*M1m~IH<@D)%QPA%-W)5wA1ip)hvhe%cR z91QHwG*N7@Vf?0CvjR6NKt!@#^%rr1K^^0%YE?uAy#f4P5^aIgp{UksFjZ~@J&w@_ zjwMnl?UjK4EAxu-^I|{&QW)ysaI2n196enHr{=SFArHUzG|bnQHA}pOZzOboD~4B3 z0A?Jg6A0gr8|nTeh$3$*Ff4pEgGZi$z#?g@v{Gfk1_Wa9;<(qGt=4K0z&gNT=Cxgi zOH3%xOIXoo3xjLdz02SxVF97E?OoL(u_At^2iZf_8y4~i*dj1!jl}NzWv~Tii6P3b7CEk8SdwTK`Ka?E1*NP);)QbsVnBg0jz_%ZS^FW&j%Jx?fbp!Y*a0 zUsY9v4EPK2062j~6YJ<~eK4e6Hc@Jf>fXI;4Z@W{LhZKah>__q5=@r7q?pAo&FY@r z6^vr#k%3I?doDmiCdOzHvGzgqu2_0MZ6KkGS_wGZ(Gz6|t;ow=6GHFuqq`}U|qbk13bXH4cO!Rv;Osg}^eV+UGZh{Yt z46>)!p}00j2l2tlnlr%%j)A$vqPJmP(NJqX4N+ifhN_vQN3~zY&XmU)Y1OL6gY<$@ z0<_}eU=|ejOqkjV3Ln=%43|C>gliRMx@}BhUHsZ~R7VyZs`&_XeIOa0J9@9}p;$T> zzczA)?wqbCFRgrS-7!eYaMAbkkgD21YJg6Rn~WZscZoPEz3#u~2SX-A9nTWy3$aah zhe@MGGAPH^JC;u*l~~Y+`Fnp$iflz z#j2!&J>5{k+*9_ymPPd`^g4w>Mn(kcUIqwzkqPSFpmnvK#nS4#kRJOn zAc_yB_HdQT>~Xf6dhF{rGF%J^QWmY+E;6L!P1RH3p~^Olms;4wgab&Q`~U~5`it(m zoZ4Rol_V>>?D5&M`(r#zP*;A6q=#h{=eRbrL~#K?ceYP4)UWQ{9yN%W7sJc7C$MP} zmqd`-b7T`?Xo)l6Lh)BpD#nwXDT)YJ(n+?5zzI>xsYYdEj}zl!APi@U;<`v|rGpZA zB)>)m;3vdAoVSuZyy~FEA_CsVS%D``zR@;6M)e^E1eR1R?t`F5VG?&$Ms&SzUj0QB z4~!aAMBz;NM>R=uFOGoKN7bK9Gnt;zjJDHyHW$q1jcRVz(Tuz(>4%+I^6vfj|2-GVpAZ z25SgQ&N8l}`Y!D$)LkDv1!D$v#&iVeN4~U(kg7ljF@IaV1Bjt~6RL+(?#7L(Kzvuj zj5_K)-^H2M*AgNm;qp6xRW-h_&-xAVh4xdOB$;AIlgJRs@9BUrZ9v(hsujyUbi;+Z`a9gV>bn4^ zbwHDCtT)LPr9nnKNxl_0L~yx}^rc;9w8D}@=uGupWR^M$$f`fzh%Oc-S(fKUsygi; zL~8+mnb^HDq``N9aFoj12#c?B$ujvU;1qIFbNykF<6bd41YiqVFJj;6Ae^0}^IpE| z8%0vBbHXhc=77RjTo;sC)?^e5oJTgr-1jYrCWYwr0H5hJp^LMNtIQ7mn#4g5Ou`8X=j=U z9|Ut(9b5D&uPyZBQ6xa_Sh?=Bi;F%?e@9c_?^IPv zqF6J$%~VOxv0s$H{9A-!ss)|8YSwJOIgn^L=&d4CIJe=zYYecXsPc)~#_<8WvG^wV zl-Gr90;@8&?Iz4r-EEFKESd4(5?Z;3T2M++ASq=y-EMKe0FSs_sT}+Cvot(3$P}^0 zwPxsL^(HY8?>FR9Y5ua4@v}f|zER9%GOUEFiw0n|4lo`w&=|O{A)?#7gO2>-&XEXo z>vUk5r8o}Xgoc`~RE<*KKp@5EbF}tA8A08DP37?3%M7tNR81Nh~2<5q{;E zah)Kr63TTd%99DaOM4ZK`<9Vyn~kc72Z-q$q0@L5VyLjNShi9p!o8|-7mBZr7bMLw z&+3NS&$4PTZiXdkHNGHtn@H&{TD?~gYOt#_Tu)Dn6-@C=>r~pmJ~$gmzln;8=m%Zu zHmGE)>^LYJI3WqtC*4B2Ea?I;i+uR*G%{b;8n)V65?Znp)eqz!C4!QhJ zu+ko5GJ=V&J5^m>5MOR4Uv+iEfQ0H$;Qo(*iz>r2!v@J5o7I{)BLebSYC=pIh3{Vc zMxhwNUd*R%*)zz1$DffzxA9K;TYieTB+g~@Yl~KC4fCk9i$aqr-n1IBhS)~g&3z~_ zFDaQa&WthOZM7#$N=>J$HZu7wZ$pQ4)TX1kC9hR%RM#a#nkeKy(olGphfG;!cx)$t z);BMvdmm%oV=<=+jnkypJfn3DwSDl|A^g~FxxtISsE>u10x4SRtghP2dBz?=dp0Gk z`V_WOf&tb3>Wl}qCYY2@!^BZ^s!!qA9e*ioqn16hCKYI;0K7r<+R9JSlxUcD<7QI$ zowUY6%8_zb-AW8-VxtG*qCIYvpV4j9vh)yoi6#hE-vvPrauXmmyAJ7sW{1}`S1_4* z`6+bm1ATfQpw>6a3<$71+~YiQo2kAFRTM}klY-lq&g>^e9LGIn+@*SxkY1yuQ9Mde zlgE6kv7IgbNQP5Y?fq%MhnM^enf%>y2mN>Mq^&ll=`8*tOliz~eStr2eXvwaKnMSg zm#jYJ9t0l=g(-ZnGQ&IX8P(comDghSOy@2xDk>;1J%w!p+{`@PV8!7I72icG)zQV| zT@n4S&^lwTu6ro~E@oz@{EkfbGb=XVC`Vck@yfLA0=THIOPkm73RBco)icedO$(I20*85ZHNLk4g|JM`V}@V(7fF7E zT_QztO2L>Cpd4~KM)80CaiE}AGkH>hnnvYc_vTQ+a7}+Pmb-wCt3mQbI$Zyrw*bojn(jQ z7&$RJP73{MtdpUh32e1{d?Nb=@5p#xsueTOR5``Hn|UejF^cDwc_2GqL`;Dz22E8q zI}?9SpXMuwR4v~f(ZLMaKp({{uId)l4O+>)fS385&h*_g&MqkTeMXK{e-TSl{Bg7S z8Vfh2q}9P()$lX?y80BLFw~JVpj5~SknplhJHmN!^y}Yf*`MC?YuBHiGY`3G+|BR# zw0n;)r1#t!L2%L(o=M9bmzE~Bx=&oA=J>R>ph>1 z!z!+gr;z+x)la(2qMFScenj)+-N=jQ>DytR$7`WgS`rfCLJC9i|Wpc(;*1udh(yi%5b2(2g74?qd-^H)peK%N7*a2Z9Ri`6nHhvfn^vrR^Pc54rbhd_{EL1|MZpHzuuRlhDQZhyGL0yKSNT_u4sq-z z;lROasGIxEcK#Myfext7#_oFuBYG-w$e_&S=3KSA-m7_2E zYm3RKKC5GO&A*QTe9GFN3~|FeP%J?P0-6-z=qE}!mK-1;5Sf)wK8;FGN);0KPc()n zG=hP}>bH6K?Oj9{#`6T;L`ixE#a+jGMXD@4{BB4(6BuccQjOdNdAnLuYo}?xYWjV` z24f&cFaiIANQrgQ@7Shw+N0F;d_s!92nKEj}$YeA5jH# zF_`**Pv9x|2;8&O0y#&T=;pscnDit2@mT#vo`RRJHquRPcYv#a9uDCj@K;7p%(4(h zGWDl*nL}MwOGKE;7F+ZQotclkMgR+^o1qO%zuhBhBnB+;NWu}r_Pls4)MiL%II!qaUF+JJOQ2} zZW|SHTk%6As&6*M@irpYsv@Gz9YosD**4gA+Qrd+@rnvhxazuz92ZiCjf;%X)+N(T zZC%Ce9*v~hhhd7@NJSX1m5 z7=_jLiyA;{+SIJZE3hsUlP)&nS~(fHVv0}E>BE)kX}wnVAaE95VLINPxW9^%DTcND zNU<5?v?%3PBHuljSc8#X^*>D0$HX*310g%-C{ufwPVrzfpS+4qQSa;2mdU1Fl*`l|jSNNk2pvh1|awpLqXbm12)u&=MyH3VK{ zObG4R3BA0UCBi+3es#!CA3eqJq4)xjoo8Jz)KDaFxr{{Vb!3XwR+vv{h%_&s%nKPa z=7~Tc#l&{h0u9}ziuYkUYODML$kRou21wWr)%zOxoGFsi<4akWuc)=bRT;WOH&~4^ z7;)f*3~{kvwDR=`NLBP6q2WAYisy*9%Z71_w9M>FLdJogX*THAT~|E;fMt``$m!Nu zld^H+EgT~j*}AmlFKxV%-oST^Bb-K(Bsy_XLinbYuD%QQx`-d&lny^Vs)d|#7P%#ak=d9#QQ_P^j^qR)srp1g^9EQLVH{#4g`sZC(BK^e0Zz55 zuvs+#6n-DwjfP`0Scnp}sebWA&An6#xME0xs+C`?{_I;OxXW(`bZ4nD`?spS$rKJI(im<~lwd+~J@=e?UkN%?88x1Q9(@&LSH?7g z9bU1alQE8z3l)Enw);)qE=O$MU`R&X$JJ|A+uY*n2|zeBmXAp4)Hom85ZX}{n^R0R zvvwEgXRI^rFJVX7Vay_ExWJjo#oFrT6sf5-;VBraPJN?Ti>%k+mo{00)J5Mqe!AKn zYJ3^+Z?qG1 z1`=ODO$0D$E+8tG?`d~g=7^6{hTKP(Mra#G@37p!=&eQuJVuicbX(LR+VhYFLwS88NtNN>Sd82!)$G;0&TtZZYwalXO833SEMFv-x9%ObP7$G{ zz7KBb88LyD1;OvoBTVoUJ~@9B;|tHh45$WyuP7I}fqM;&m7OjMZ1E{il0rq@=!LfEAe} z5yUhK-y5yKo?+Xg-;LSAfQkyf_!K)spN^?w>nj9uQ+qn5Iw)~hsXhfgk1E8C*4T_q zGow%78!?Ev7o!73oiyGK*1%l95x*Hwo&+EOVD%gMwpvg+Ny1a|sSbHbxImK&Z>fm7 z;lAL|vdwZsws&!$g{mG$!CZ){?vZv5n*cZTLSmTME8IjP3Lrpa9f~Isk1@B2Z+vcQ z3wHx!l=niCLc7#_y_%AW+~%QD=a;UoN2;5w;S0mgFJ@w z#jMOzs5afH4Mt^ifOP}edb_J}VQe0fI&TKPE@ow!8Y;?WqL}F7CLt04R^at&qcBY>zKhVP zJx(wKtjJUJ&v_9^vA)v=Ex${63G4`jh@7KlMe9O~8uV^*P4!(+YFNm9HpB2t9RT~} zhyKIkI?$=opQ}O@>(zN@nK~$6R?Og+Cg;^9L!=>Ila!H&R)3KM+&!qj^#Kz=wty}> z=#aP@RY})xvHQk4gb}iRU98taRiYN>Vh6Fv9dM0ptez0;4tQMs(kMZ=o$UMF0ogAy zJ1XyeHX=$&^<5&4Qq0iOwYtPijm+26y>~Mm%25hpM`NzEl1e3do7NuB5i=giI`-9! z%&p9v7~>OII7v9>n6)p8xD_^j-ITn(W|w@)jQw} zP%`)vnzPoiBov@S1#*DTTDN_%75RRt4PA zw3qAw&^uu=b|I9u>O^5evML(bP4kt?oC6QJtjsC{W>`2>--VI2a|Cr<>!WQo37G>w zMJ4Pfh{c(i5_s#XE4OO9$z+glrD@hj%~$lK{eeDI-7oM^iaw(xR#i59RZh59F~*IpuYMz*P7n*oR5Vf|CUk)n z6$ce|DrS@x*TuYJPQIg^)~Zi(vEUR4HS`cjzSXBdKwUr|=5-R&Tx&|%4`#Z=YY_a6 zS|nF>Bbmsqm!#x#r^JfC_=~W5^#5(W)k6aF@cyk6FZUE0s%~dmj-9bim@7y)$r)td zQat>Xlc-bmSJKeqieRJ4GS^C24SCd!0>yJ{s@?(LDn$)c`n%On6g3FBF=aaK;jvmx z(Ecb(6k7I+UqnG662}ZYD%+|nDQ^SjJ($gI0^i7zX3)mr6xRo|+G@nK?N!uhaX(We zXjSsipjB+Asy8eT0(}j7g?Ntm8*DBO8=^-h#yC~Q{leLlSl?#zu-qh6twI(PKxz)j z{NH+tF2ZL|F-oXK{7)v(TGy++0@liK3||^JK<=(K&&<`T;y@ClvoJz#JSrz3X2)n) zRH{6^s`F){kL1RIS5LT;=$)wxw6To5HM`4kS9_AV0`yzXkcgRH8)74`VE*J)g-RxV zX50e{qE-jsdaCpZUD?#|J*(bFdh1Fgkfo~JyVyM3*H~CcJpE;LzbLSXs1#=)W>2~L zK@ySfi#AjaTSW6Y&FX`pDScK4_!GtN%+GCpe}0Tfhz#vmgBj9Q&6;%(yq7?5+z--sxX_ts|~EG$ksnYLNs80m>pkghMxD<&V@ zQmfq`8IK!-%eR`-Q}|t4VU@ix&YU^A;_7CMDF>J{p6IHNm3Fugm>+8^NdaoB>ssen zWt24zWcx(=Z+IBgzUD>j(~zBJ739+>M;iT(_s| zo#h`MSMeM5Sio^U=6Tj4rnyRY)M*pvV<=v{k1i(anIFnBWu0b$1$mdwP?jhrRK?|Y zF{X<1gkq$G7dsmq~FP<;wZ5Hrg)1R9DX z%y2N2q#jYi-3QgD$WYN2uqoFQAWNn5jTgUHsrDwYx6&oi27atsfw248fa}>bNU``X zM@2wy!w?u|b;WG?cgs2DGD9`)8kZ)riEz#FQWJ<541EeS0Yrr=%ip=h$NT~~^41B` z7A~27AI68vvWj_-ggB4>DNfK9i@ z@teoldII#07UX_F9Hj?LBTEW0}lfi>0-9(Mo_Z zA6XjFZgs`30=G?{D009RQG!QFed9hN6ECrxBV0`4 z5-zapd5sb;#urb4S;8AXIsvGctTy2;5wTg_BMxFLT=>!IM^)o4phBx(X>T=C@k!wu z{SV>Qhby8uvPvM~8W(255i|cY$sliW?b{mT(QwpG6CqdMWTV-hA z=%qcwT+NAe24YEMP`2kXK0|`3z6;hmmDy)X1Ev>W)Fk}K6wu!05+`SzFj4%J+PwNM zA+LkGRiN!l`@rm~jNXiQWtDv!(`KK!6%Z}farMfH0Mnx5RQc!V1L~Rsrv^E$?C=tG zfJQm!8?;%)y$yrLhoGk7NsxD;i9#J9f}na}ieBAyxFs5{#8q@%;!9XYau2QxN%X23 zP#wKULBMkvyW>_vUFY8-rGsG;_7%U8dN;-*Xx`1w&K;1cIf6p+u_{W8oiWYF@wI#M z_%Jo?eA&(C1CiDnKQxDHT&j2VMN^<>uol18YINcubN7V(o$x98+|z*k)(_Ql zsn$rb+0jIkK2fXr(&4Xq5=Q)Rga{UY(Y2${!eGl<*e?KHA_)3Y6{w59$oGq?EXqd8 z#C6srQ|B8_Cc8X_&{ZD`Pk|_q*c#Z--er@QGY&(@D`)$~p4-}WN9|WuHAcnCoh9`K z{b8O0TI&_4SqlbZ7!81wk-Q=5EK5W@fq7c(Pq;mpCfhQ4srk~#X*AMY zT0w&WhWxo7b*>yN&V&8xlMEyfzQv5{|aQ5)(chBdA(!Cpu zNL66(Ui}MuyqjN_!(4k((thzWSr z1HeYry>AH0yxA1SoJ_;&!#CodX~0C@+o42gdFs}fo7)si$-WBvNyVS`$h(?QzdD|#tXCR zwg8Qiu4>buAmkUZFCpWBH3?hvF<_bR&?s7#&}Aevl?h1P$QCsA*?ObD0rEtglQ6>Z-q}Q^oen1<3V&yk>BkMlT_Q zGPig?>a^Rm?peS~I|0Z>%v>}g1sWM>ab0>cS{0}8^wZU)A7&bi1>m4p=V|dKV>_X< z!7C*1LLAO&Uh<9IYu-Q+Z$LmWRhS8q@dSxd2B1Nf+|{~c+N;%dk-}pN-uJTI^&FuR zNNJP@WA;sPUG#-Bt&Pde2hP-jwKRjbnPj^~?@hW(k^Tr75CvdbjpwA0{i3r%Ky^74 zGiMBhRL?F$cVQ4Wtzvlu*R+_OR%h?TkwC(0Mgx2|Us|A$-AMsF!bMqJU41b{W12S~ z@Kv!%jd%bs<9M{ybv^SEATo*jO4XZ&ptd&`TK(zzur3hC?gf-Qu>aKBW)nE>blepv>2>gkF7r z85l1N)tte!ag1W%0*y>EkJyI)Z5RG8PwB;Pl%iFI(X20pKL`wgMzLOtWatr*yfLQ@ zvG9p`r-@v2(3Dcub!mo~y1;nbFvJ{foPQ{w28!ga_!OOF98rz16(x`Xk*XZUic*)|q@!U#EN1Xu zJx2t0vO#7em+Tix7vMLC4IiG)uUcurWi!;B>20EmcVhiO=*^_mU3agd=s!=vZTsjc zvRaw8Icai)L~s08q-lMmPB`zX(YvX1Kpmv5VN|+R6~ZB1#P#Q1jddsxW-xvmF4ORD zo1g2Z;THI_8V=PHK#m)J(h1MGWkE>NE&)Zcycv78c&2Z(e8kG~kCuBwLm7E7a|1G^ zSyp`t3Kyxf*@um>t(c(Y3`0lys$efZCAg)Z66DT)xnteI-$Ug^xRa1npMoUKSJ=ul zP7dXqpo_Nw6F))iicj&UgTBB0@s00cMI9xkIcaWb(UzZr1H-K0BhT-o>spU6UThJk zTKGQb!)8o067F|(b@5-6j08804IJzn`4qYLj%{7$T7Ln^f}U%?=bql^N=?j4b1wvl zE}kN(gYm{;9l?Pxu3FiEKWSm%xLPZZw;V#CGR1)mh&FOtd&Q}z!dTuw&SrzjbF zJ{v;+uIFcO&^hLpQtnL>C9uos~MOwFz7StFQ+fM#+*M5(Ii%b=2oN+)$!9~EVcA`8*fP}Zmm0PUK*fwfrWfzB|s(K$bW)O3i zv8;`Ia+MV-b+AlzV#;$s;mqw8l)_*ccu@IbUPnaA`-m1#Rg}F;e};{# z|01PI3S8VTl!!J=6lV<~kaRPH&V1>(?VPK7*ZqOU!$R~E1rdl1ei=V?J1>>ZCXaY= zja8;D5)E^^8{wH5*nre^Q%)4?BO_zI0l|?t+uo(kk2Pm0S*KG`T;228kV=}?BjrZo zOI*8Ek1=BQ@v8Afb<~H^sYFXIW%vV=5hgXvC2EXUeMHJyuRt<;4NREDtPgt*HDW0q-K8V1@Bj1 z>;7E{0SD&ICNNjM`dTM#yEt5o**M6Obev#J}uT3w2Af$KiJ)9%u zTbNU9(68kj6Kd~5&RVv+k;njBWYpPW30>B@GhxMYMR6raX=fVeMDRy0P$HElv&Zhm zan{+URq%^opk7DwKAKidf#T|tk{kExT1}fI6JO!6nFIjSAyBEt7a9)3EX=M$)kwvM zN-2fWmZjoX5meuWaM$jFdqyY34q!;O0(XFQ2ly(c(h{l_RzL@yoUNa+ApU5v=#7OQ60uS2ZOj>j0Fo)Cm%F-`s`nB1({*SSi*S2)WB!6Eo_rH_RJ(KVMm%Uam_v}ek;4e} zXdG1~4ic^6m!`mo#2#_0`6Ww0_J}mfazp34d-*8@j)YO+V6J%`rW~(6yerY_QAD>~ zYDWW-;Svl>{31iD!G7*}@)@OhF*4jJT4~a6qFeJ1wM1Xqa?Ulwd$3z|U1IxYHP>B- z;U{&)XFy%`CiT@-mWv6zvsxD_@Fn(a&;fW?b(R1xb`UJ)Z}|D5x5NCnV=lU&zL;N@ zzY+dIPoY90(;_C89@IF{HCU+H{TE^}iJ8;;quZJGVOYSNAjI*zFWyJOt;7&$ha%E^ z4=+$E3lg&3%~BV?5%~}kCMch>QO*#_yOCN@UIHNA*43w+jxcVc16+-{k3@c?CxE1l z$gH9G6mWZ{iBeasoD&BM)=X${4ZFRV1<6s5kRWgy1%}@wUnT?*H5G4hx~#s7$)TY! zJhHa#quAOQT<8+HDJ#nF0vrw^3R>Nc%vzKalS_4$CS58%BK$9hYD~jjXR7#%_{bmP z;YF&p#<~+wAIfz_q*7~_pTh6rf#<}tGg(lIa>G*F+MX9c8@Wz}~PNkUn-KU(#PTLG*Vp+XC)YIF_4 zGZ>Ny0zO5HQm{MGlhX~1Skt5WF8oJjc0iJbV*)_1?NILYPUt(zt7|X=yDsK+4QJ2= za=U>EfFKZy>S>hE8DIz9#SQhPjl~hYH3-a104bH90;f>(edf>&&7~Gr2AFT(Zjzu{ ze5DluoSmwgZud|exv_Ogi1LeQ=5fFk7r4zlx{Kea4K+2d#ejr%H?n!s;KR6z+uF5= zDDm}SDTWL25st}u#kR%b#SLmG@nMt&$$zz6TyND~XM7S?o(x_I-fnxf+l5OFJOwIL zyOVfPJFT!8eEmT5vdI`2o>`I*{ODZRPbqBWrS8kD+Dc`SrU(k zFt9s|zsTkV#-c8M(wjg#FuFOEJniY!N4lx>q__!s#s0ncE?ylU4TQRO70}sPfgxH% zH>QVQeTp(G4PDNK?KD~)mQ1YHptDGK)u)gXU}u~^>e)zr_Sho#zI(JDAM6kj9{+_S zm{xf;?vlM-OkE7>_(A@{w>{)Q>cXZ&J>$H%E&&A0>y7s+u1ljo;Ndf?XsgS?7hj1^ z78+W1!AP^zrKmmkQ9X?^6NFdvZ45AwtePH(Cs73FSMO=H>uP(LVdvcsI_ z`0G4SM)b_9tBXynWaV$O@;s3AWIui>2zf~ki;c&*AEDndtHYcx}pMfoWa z|8q|N=o{f2hK$Bip;cLQtauefaEzGkzG`I9B*&%XTzhWnt_HC3FcixH>DAeMH@9>M z9i&5RKVXwTPG;CBLCDoioZ60&f%kNC1p%AJC7?vl&>rcB>lGGQ`$peJQ;7|DM4$X3 z;O^17c!y7BgXqx<-0tewz6HfmgWlP3>DzO{sOD0hGMXI@=fFWrH@Mm0(of%`y|@(j z3w-cG7Rh5)s^LD;A0sVS@Eav9B`;#|^pt!R$Qu9x>o?M9v3b}Lp{P~4p++6iwW!5E znj;WnVGdjw@4e>U7*A&^$xLgbf+)rp1ZW*Tt*8?tx@4-FQf?-iF-|~I_~Nd^q9p!1 zAhrc1Mg{;=t0vB)nhm+4`jp6zwA{5>+Bf149{`YDq5ff2iJkA^TIUc!Gwe%e{)-xv zVM@5$)yYmYtjEmA#u`Y^CKP=hh(KOeA5g?%j``ie+%*c(T-%vuo&tJvusVP!RzP)K zG@SHk7(9J&tKr$|^K;mD{p1`;3BUaq66Uy3R8@}RnQ|5`KRy&%{qc_3DHeS-z)s?1nF4+$w<_We(M@#vfF89?aU0YXE_5vinRyFR> zMjnhPzs!xdS6RqbP-15h!VwIT<-SS>A&iliG2GsRqOzI=6tF5)6B zta!wZqusE?C=YA|4H$~(zKG&@ir!Z`6RaL`ADJ(CLpDNh&fM7Q-PkygAYE1wp0cq& ztzwjSm{x&<<|)|k{2esZGP%L@iXw7?r7EvOmpL)*^ox`tLa6+MyON$uHG0z%9#wao zCsvjoezU+wZJ_Ui@KoG&7(DF3rDC7(8K__sd+;!JmO0ZPbFCJ7eF>5Jr236K#ewEk zOM{X#?Lxbw#sl6?;N9xF$i9xK(Eg)!H~`n!Z9^nX(40rL4nSX4aq`UlgwKG|kABA0 zjQd@SFHvPbNG9WEtqWBAM(n&}(5m0EW`~x^Tmnr;V|vE&IkM;R7Sk5C>sarzy)a3U z-bLG8>!kWF`U+zx)m^&-T-lfzXRwr{j-;vj6uz*m^JsN)8qrLNdz2|6AEf=r%w=TPf-Mm zaDG|N5q28-H;q*;Zmo~v>dMxSU?fhGD+`z{D*-irg)53HUiB$zJ6Qw1)7n4Mt>^u% z7sPP>QLY<-9;v>KHdre@Rx7R+(j$6SSYGj601X+n@>y?!nHYmaE#x*zLwq4R;2P&6 z6A2jCRDBmY26Cy1W;Va4pca4Yr=12W7g~YhQxfpCa$TmYrcmzTv%4*YD0923Pl0(K zh$dvu?LK1Gpf$%_OdE&Xz4{a&Eb-tNHeGIexfqFW#AcsK==xplx}E}hBqG9eRUvRtuD%OS4=w_q zlGOp}sLP;l6HiGdP1USfek(8K7SIq#O#w1nf2TKV0!Gy%6SRwrCR#S0c8vaPHGckm z19z${y`QJh0A_8sKV{Ts;c;^K!C#<#RISnlSrJ^Owz}x}I;Z-jy4v|hu|!DI!;l=2 zv^p8#elQdEPRJzOwS3o)!k^JN3|3X0dMHqG1c9%8)Q3e!_B~HnCy1P>?FWa-2@jVc zYL4Q%2#}v=hsf^AGr)nLP&P&3;S&srnP95CYj>QmTKf?o`=YkW*8gl;7;2|)HJyBv#7@_ljJgt}ydCFOP4pR5X6Z?BHXNcm@$B|swrJgp#oJtEQ z^PSIKyr=J=>fWqdgwmD;VckYZ$Gd_I8Aqe~E{6Lld{g$dUlftO)|z}O1n1)2sJo}$ zHPjyHk#n?dQcoJF2kH7}?p>TE_PvgXr2PY4v!`6(+y+R~b=heIKn7?$a1njDGB*^i z>>7hd5pm}!b}hqlko-vW$YrnijgCBYPOv{t6RiuC&QWaWgiL&bV(-&H&2PAvpVVBc zHsTD1v}8^NGU-RVj&mbxliNjNmt7}diFBFk>rQ}771u?lUAd}D@=0Zb6zCisypQ|d zH*z_nV8M(t5ZDOt>bpqePR9%hYKn&_p=BOeON`#3t<|TfoDFe;L9T8o84MEYZG@yO ztBOzI0m$~3fo?;T04QFJ6~7*DT5i?*Xp~6l%%xq_stHOE)4KJ=h{eh?ykwS{{ly~d zqg?9TTTCB=z$W`;o1g04EpNOT&P;|fIgNA<#0naIJq~1fb)CW(W(d^nRx>WydpghzHqxaG(b zx1Z*Z$=g7O8c+aivsTr+Q4)yhMRTgzleo)CGJvxyMHyPHNtCe6j*M^7MYb*(7C~S} zxqsbrpsUU^FlW%*8Rj_QK?d^})M!!f!@t#_MHGiYkIpj6OT7CN>o7hD`Se0D54);v zLfeXb7FuE&Ox9&(kV}{|%lTUMiqK#OX<_%ru+F-KX@}2#U_zniSe-j@ZxaNx|9Ru& z7j1J8P>id>_c{{5Ejd~2@StR*th{^FD#cjGQ5hI!76@jBZ-m~=l!k?n3KDgyh*n5- zxa=Yf!lUvo8bCl+id48YC3SW0BIUWs$P5NwD5ueNFBd_XMkXuQu#^(QtUe(KG5h79 zH_XpWv5xay)%=8u$!;AON>iA+kvBG!DH@>8St??x=zQ>4Hz68(i@RZ6St0#tUXE40 zTAEmqdeRy+4-E&9-~m;XLpSM6wc;avW$yDx54C+Gsr)y(1z8Kvtm*`#%;2N(WpzSm zoN9tv*)IcQYI0y56?dIYs)(V{LD$V`j87KFa@|?QW7Qq-u7rUU^CfABH5K+2gn+NY zU=XkP|1-~>sZwNk;_pdXQ@rgXAU7N0&e?3`k!{kITNaVYpuq!1kG-4p43R$T+gq(- z#Pk%N1qido`{W!+JT}bDN;BgMBS1XUUhytW&_T@yc?9~%S24akjG9vsMGzjeA}%B7 z05k@?T4S?N+b?9Vd>w|K)7A$#W`coU9Hw5=xcl=Jql7lh6r;H;?p?fS!V9;6*#qqx z!6-~<0PQ|hCRMi-d@+^0ya^2`PuZsMrBg6d7C8>ds67S0lg%YSHJYmGtj!RBYtW`X624En?B`i@8I zCDTHxr?E3FNQ;lOzUgdKk+REC1`q?s>Wn~Fnk+>(juJ1oJ$`=_jwsCtyr`?iy^D>8 z*pwXi1I0sRPYyCk0Bp^=;FH0x2_~d7u970w>?!ew&=pxdSB&R7Q9X1bui0YN{!!+U z$eZxhodS2GYR31~Wnc=HQ(j#c*#YIXF*55NV4yTh0%du~`Q=!L)CcslH$HK;bdq(q z8OeXt4}h6y&+%EIX2}U4FUg>AS||<2dc^Dvr9wL4slE2fBh;1c#<<+2Swt(`>INbM z`W&C+b&x*zAwUM?Rd=?0n8u*Rt_)G|iBh5QxRYqanR}x?6~DCF&NZMxo;Q)-DX@@k zQE3ezv>U5WNwUr>MQz^VOFUN&d)26svis?OoihgVr)Kx;PAgC_hE( zOW-Ey%Db>|*{hc>7qd3KQTZ3ChnVqjSpGn*LpjKuKDNZ4Fu(*##3*4?YL{PhK~GlH zLa{M@%Kk4o!m1(NMw>O!n~SP?ns(I4L9)8y>gr{OOC*I)RgU@F4BGS^l{IA+jal3_ zJe?fKqD4$1Y^a-#b*vt$^K3K1l#Qab3_E)Ub)+4S!L7X8Iu+RFcgqD}v&axdJHFU6@~F-}XdfI+k?H-_^bv z)n3e2=)}6-NNvZO{*ISv)nia*aKJJF@KR?bXMlaQd3MpP*Q<#T%_&DU##=6> z7L6g2b`rF#jH!~gpdv&tY6`GlsxVxdFT zIsj5ZB_Zh-3l4lF-Ez4cxhlyM%b5oShg(Csx=!;Texo#Jas03u@hytyNGcgJse{+* z>EO#k`4NPokgO{3NLq9|_cDK;U69NR%;TF{(MdO=quLRB1g2w{WprYz_&{4UU9PA- zK~h=E?d5;RJ&i3GTN07MJ-sGMOC9NU^nx{^8`6>D=7(o0^8b%iP@}qP$jeD)?;!~3JW7vJBG?N=+z`&4L2V}o6 zzymO`&^UC`h*PnDL@|*$Dc~0=$M*e#FcqUtSjnpU+%%tr<$o++4D*$-Cwik>@p3c~ zCY}6SHSWqZ^n>|HeZm69faW-Q+#9*OVjid1V&}SMje{0b{L*k`ynb5rbzat4>H@;n zXtsTowbiF+_PawaBpj`)V0bel8&L-TgAl0t6rrKf^wz0cqBzAr3rHxW15iBywyi&# z=k2xCHgxlDT6QV^PPe7}i&9HS%Pujh^2-k2&Y~3BtV1%kBDW%>Vbj4u9G~bu36`-j zOhgF&A1zcMV<^1jnYYe&b=L_~&>Di{*rlY(vW-pbp}+`%t5^q2Pn3w|U@AA?#NB%t z&bw)uFT3L`1D|GH*{*o5pT|kOd=I;Eoc6otJoLNstDkqL!|^$xJDi8*S=%?yWuA}S zwCsju-kTI|g6Q16Ob_n&1s%$NDH-wmhyCV#!VJm1sq@b>6& zKE6fx+OK<`FR!B|-;t2|VlOK`>9Ov#1D9?#MgA}svETRfKF-T>+|5(J><))z^6UD; zZt44Fw0_;YFthc4{kMPrKbC)6{*ga8Q~NZPBmsizjg{MeucSgO84KjOe|Zopy6%IA z3i) z)JT8hL3k?0W2v&*8kxcxRx}|~CX^JoMgQ^B0w+)v3TOZOjaYa2mQip1ozH)EY~%X% zvmt51rQUE%^va5#-haH&|NlX1E$u&ulQ}tmV~zel528@yy72$7cCSf}BFP#+SCnQT zK)_E4!9Fr8tM8E2gb&FrrWH%gAhdc<9p!G>mPV0wg>VaDV0FP$U-Vr zb_G)IfUUwNAX@DUq)Fwdqe@!s*rQgTA1#Quqdk9PPWG?Anu&$;@u~qqmvhX#*-yWk zK;CnBUtk|Y$?ba-fuK?duRzo-oY;EfOuM!}nhSEW z6b~3g(f|L>{?`>v)L&tl5G}@rqDUfi)XY#rbma}e%fD?l1zuyz@Z*uVyOy@Eh(;o{eAWydOt`$-61rqVmsk1f*Qs}E|LQSv5bAMwV@Nb*#H)B6aatA8|=qUB`M<|2hPuy)$zX5sIxI4YF1v_>J25u_B*2 zLIv)@{n04kn%@9~g&vWA$@_a`z&q-e`Ph})YFjpUW+2d-5)!?bEH=ZY@anRIWt@m&A^F4TXn2woA^PcPvO0gwL*WWEAgN8i#Rv+L1x zFEI`R_*f@0k{3Ter5^KC>e$}PT`LMeGw2k^$s0i8|Joz&!uqmC=sI4zR-}bVk*f!h zdqb}Czg7eTi^WFnM`L5(iU{OE3~|G)61VM(UyV=`4_$?uXuDQ~^8(Igc7A;Wp7~#U zL~|-8fp7**qGR*o(P%P;fZ)lcuqO~;t|SYVi52V{2+Mzcv@rOPaWTUR5!yr|@%(6^ zSd0S@3fEW(JKk)WXb8+`wBW$J0Tce$N1MA^Fk+aIu(ms{qX?wu!>&N4oJmNZ_b38| zYYjbO*vmwL{31|~QCXF6bXxh_Kat4VDl=C;#(^H*7pO7oG!Su`WU`T`do5O^gT#j$ zEcK1a;D7!6K*+PN6FB_NM>E!{fz}bZ|9_0gf2{~T5r~YWuer&t%|^b6E=f?|_PyC` znzf;=%Q3lYMf_RA-5-JAJUE3y)9ktXBM`hIcn?j5J@r}yqTx@b z9_%F3LVWRw(5K>2+k+^*fv)?npP#uH*EQ`iZUDP}e%yb+4Khu|8=e#YwIZSmRItdl zaa`NAB8)SK8?-&X?7lz_5~k<_eV835Wb`3b{iIni(%x_a_}4uWNGH;;79z{UeEXvb zbg@Tz2LWi>Q9%j>&8qC9B=wHWbWNFcvMysw3>-LWF=e(xfJBu(6!%uO-0TF*A@+w9?dnoJb1 zQmY-QUkt_psLe`Dr{18#{nw)r9R}UPUMDcLeX|3nrAx`~Z%BQ={J&}Q5l zNDV^MXHb;xNRO=uBf?u0e1JVk#2m=PP7509bH{ZgkU~`Huf>Msv*TA22$k0$E7&Ld zR^-7*=3Q}0-e80M>kH}8!dHa~yiL07X!_29_!Naf1hckXdqn*o#|;n5+-F}P?E}zC z%(e5LZ|zEE3zW${3q7#ornN^Do{%r8_;+t;d;iz3rUwB6Z?AzqcJ0v!o@puu(YB|7 z;L*ZABFHH=d*6A4%YjftT7Zf2zDEO-4|~Tbk9Gxe5YZ|DXOi9S48+h#Kg$`gqo}dj z*mX2FAvMTx?pu*bJ>Ms?aP5vHB36VL6pUg*@jKEQfUnM74QQdTdBd#lUq6uu#2K-{ zc?x#T_C1O~9IhRIwX)KL6kSt^#AM8pnWaN;pXXfJsLIE5YX1N&7j7g zYms;~$YVki7rJ>n&TN5PXxN>QD3-MA(Trz%E)(IoyW`A`6#-XKx@T~lf(|{6#UcTzmA>*pQ6t$*9>IS{qLGo7moc-8hj3R;yEn}0-k#f{w;sG&tb zriN+5_BT5o4Ymd%iq#I8%3Ukcr(ugkDfS&n)c@LShdvq_1s?o?T`Mxl4kw}0B)8cY zDC9h^A=Xu{{C~15Nj^re&Ch+-Xj@S$Y6yp%)ZT*)+j>VM?}GQ zZMLCUD6WSh=f1B$6h5i!^0sJ9vKh_x9ugw z9Ec_4xkmJW9NV==m|l1=hAcR-y8^kgStW27183M5NWyN&3h2A%gtXbSc+&+;qukNZ zjLoJ#b)u|EPJHj9!9$ZdH}CIuT#&IM>Tb~9W|2GSLh)$ij6LE`!j=^ylF8JrM}s}?y-b7V-*(7wtcWL# zbA%iMsf`^gB3MYZu68Zy*z7ouDvdQmOw&H9lsj;2!8&BV;*{lzS7$^gp4!k62z$Ne zRYDhIPkt1EIAVlxGXe1(n=O#!fk{-z@9*G?fD00*#yz1q8%>SvKR=CAYUEdHDgi*= z@$(C$Ooz^@f?K^aP|a|aqe@s$o83#%P% z=vWci5jqX1@TVO=zd-$2{ydfU!X3e^)%~s5qJG+y}=5LP*AoVu%SL4O=q+; zIjjky?OM_0;#33Bm2B-ekE{rgQC{n6L-F0UB1V`@4JDaE7+&0prX9CP1mf01Af5`^ zjuCWhHXtl9HDnl#q+Rb34gZd<49e)V@9WnYCGX=_V%xc6MLZ2mjtFTKrrA4I6y8H} z5w_~YnBTCK{MYM9{y4Job8g1F;60;wNeACya!5^O+pfkE@jDw2VzS$9oMLd2^=vM7` zvjvKdmcJCN6?fbs5lG{>L>4j7*cnKTHSp?kf1f5-*O&kV9o7Dv4 z$9;j|A)9h=L+;44XK6=sW4_#|e%+3b)*b~so9uPiOze4&AjU!Kh&ylu-a*c}SO{nq zeRxXR`aat?Th+%!X=(u2-IP1dqtIg=n@ExPI_!{|i{pbf9N-A`yFG#v7x&P>Iu|v= zAaV!(2t!=E26=@SZnQ|ZZ?@J7N4kwZ^bV2Z>yvrq2#dma8;< zV8w%Zc2bJ$`1#M1kDkz&HUh!P9d95tFirFGv+0u)Zo%{(Iv;Z{n025u+(y6V!2mxK%r`uN)z5pZJ zSYQ@~|BhenVwiy^NV}SVZoR$Cy0SAR4XAvWF7CGNZ6ASJGeg$EAdwW+w(k+CA8u=t@qw5)`T zCj?@=BsQD=yR6C^u&f9=TaOlT-4WTR0+rk?;5t`-F^`6kM~f9CuE=yE-0~$b-Xl;& z&+}}$&Ke4jCGqn!fjDD5ou?D@_@I>4G|28B*SV{tyYGo=QHMEolaxf0F2jp+~*Eknyb%Qb1tsiRmO77S{W5kA1Vx|i96Bcf_Oeqc(>;C>$ZBpn z8!rEIBCCBB=*r>L@gExD?;mWh0uAgly%u56sLA=HrXRG?43DlMq>WOie$A!Z@KXKR`#uk8i$v< zW(k}5X#e?%sAil|5C~)xcdbYZi3$o)XTFU40&Q|(c;4*I4g=4D;>_m4Heid%33hz_ zk!b1Gole7D#eSW`S!Rp+Q`vP_C6`h*qL`Ep!#iMzV26m2I*)mRE{|Hop9yY$VQLiTB zcKRGBa>%Ab^5F@7&e&bO2xLq{vQBM-H?n=Rt?1GatqU8TlrO~dihA^?w$spn97Olo zz9O@E=8{v;=}8wQO3~*P$+Pvvxi3opLE?9;h|2I)k;^knoxS7kkIbK1F~KQ_pNz`z zi$^2s>h$kVD##ssq|8Fhg{)G(ovu5QQGs0kNS>DxWhl`_0>cllK(T98y zDAbtTB67$~C*nn*ZF;q@0@n5c9jN9q+iZ%=1{v%Lmu~IF< zgxdG5`O>yYsJ>Vc1@@}bhbrkiZr|)sV=S7!g4y2-KhB0HE@?u4!^2)GwFs|G2tI5@SEmqQ3==mAY;PZ-tmaloN zfv8iP0eU|NA}*_Ya53x+`I_xDZI1;_tP=f%PeoNHKsx_ekwUG%$^Ju#f82g$$BKA@ zQJVAXqYQ69MrMXD=k2OH_frOc^!#YSryUrI8{NgscgKn}G6D0UeJ78&)`u~ z*1)aFAflchEdp)gioOUG)5m~C@O0mP)aufh&-ZYH^y-TpA1y@1iTjr68f1;p*ydj? zm=}n<_Q_$V%oe%0nu~I8qq^U@lM?yh@u-lu5LZEl5xjW6pEzK)u>vG zh^EL0mpr}M0UPSO3gehWP(N-@MnhlRfbL4+p&zNcWcDZsUDIh9)6YUm>=G+uw?J8B z0=Zi$c;daU2n18n0rT`iA~5rb+)OTAKTr@e0wH>AEJ08Y_-|#D22Ug3E*Vcn7YDGJI3+6}Ee%?8r~5A_*LJEECTxr^GJ;bYyxQ|R z&-Z(h~UV#?=9EfMvqg}}sN(7*I zG7$6^E5h)Hags@rt>3jGs!YT$Yc6vcwc!^ly2Z5(i%JM7kpsL4MBfF>sUpphx!+y| z(i$7RMi6OBe&Dh-? zg~5dt-3#J-SD=tUiOGidGLhcA*rQvB7&xbbJ-Tm2_*wewC;rm=Haoa=4VWg4mmS|) zMYx8rFTf8ajErT$^BysDp`j^BV#C^1JAO4t1l;Ap=Rw(j&oMFw(ynDxTTbPUpI;!b zv9tHl1Tmw5_`F8~>9pBurX}ZU$8{8eHaRfASP@eF$%R`+t-SrswxX$~$gcHw21eK( z2u+ye5NUs=UWWEN0s#=V&wC`7RkbU?-2X$rBV*G=0Ix!-_TyI>GW_$31Ts3bL!8sG>$aaq0$nOp zMh6_xh#hZs1Ts35Kn$qfiH^jJN85(hcok^G&+9nNIdr%0QGkaGeR*DLo$RTO1nSQ; zFm*P-5O-Wh0$qWb^Y*gOdlZ2#K#`}34%VK8JpwtgPd!sg|}0sNIfiBm(I~xs3qGG76~AzgoaP3KD3|aP_n8IJ4(KoNBJn zY-2{3?Zt|8Y)!NqU8H8my=FxoOE)S?_KZaMixuH$o#{>DN~JnBPdBXg^ zxu_l8nh3PbuIPD>BG4w6p;v*7j;W1fpJgBouL8k)fNLmhGMK*?foK?Tnz`W;XRxC$ z0+Hk8P>+2G*SP)7RuN67HiV}-yx|>3Z6KrQ2Xq|fIJ@k)MFb*%!;axZ=_jK*^1MfZ zL*iA#tca2Kj?`}kH0g}qfEsiVb{ziPiCHEak^5wEBewr)8onbTty9~uz`1TOYLFP3 zP#Y$I&un1NnH>@anH)IkfPu%>Tug$WzQ1f%$FOKI2GWI7t)Hh$zMM`TIkl2 zLcu)?t>6r#;YFZ66lV#SN|Lwl5q8e?);J+;(W%U6Av~`LGKylgEK3F9rQG~xGrL0Z zR6X=3A#dA%wHVb?byq;@j7>Y*J`pHjU-T4eT8i7#8W^;V(uL2Pt+CEL*$+nx2cTg4 zW*a{9l`aW0w7S0Af3@ujUr&!_^3;cPhzsD{j2W${=Rg3c7d;3P@s90`s6y34)WGGg zk}5jvnk>B7T9R=YtQPtsXg4D59jO7r#tYsZs}VtVHuE0o@zFjhS8)#a8dCKb;(Xdi znbhw>Vx}DfvoixMVor-Ms&zyQGebYC!fe}iEPE8G-^FzObRjoQXC?7MqT-S&0bH3^ zga(Jh%tVQLVW4F)fhcjg3s>10n{BdISPXRbE8{?l7R@L zYv|{v?-(r&lH3^>8hsbaGnBA?Fi6|zXZkY7K!@t5MpVcbg@6Gsci^4GP5Ua)c*Phm znIC2_f3E`39x(H5Pbmq^7lHWQNRR5~G-k{=UIp5u&GIS`99s|YqdA)R`daKj%+p7qooPh9*rRQ7Auj^mrpP1PQ%hE7 z*Q0UqhO43);IrKoXq%?Si$|MuGjtGi{kz2Q;5ks>Mhc0jT8l~bj-Ov3lqgI;5Jc8~ z+xJKyN@0SjB*~%f3M9|MWa2Ub7q=@A#6BT?{&{3Sw+8~Q=z}y~4-L@xjwB*b8ijdZ zHC)^xiT=^^&mVz;!|c;%Hk4?LzY0V_e>A7RP}JM8M{L2&dS&eeS@s>99ViVT>$6ck zkKrAU7J8`Ld6sWEtSrW6$Jvp`&eqVsR(vl*#hA~gAULh7&0noFCGoE zMTQOSbJ}zFt7$J#zIjZ<^NON4F3$t8;i{i9VnMG08HkgxV0fRI9KQ;54O2J-8LnL0 zj}ecC2&K=WlhhR3kJ^|)GKEuPsoKpDH?L?OW-8WiM1y^9+u!UtkXA_uMwzj0-=hdb z9URBzHuCe$G!Xu`I+{m^X!#S35BfEkmc~B=!FevEasx{|@pG<{zLLiN(9W9c+zrdh875gu1$wLZ8VKfi7b0t0{g`ZlDTPhPEvWI)ZS z>@w`Q`(v~5^1B+emZ{J7WHbWNt-BUrkwF;Q{?SH@l~Mq+31r5MaOv|N5ias#hbSw| zPe#AyIgmjeY2Pwr?@oG|5J)MD zx#tzZcu({}g9b~5JYE?k273| z8UgbVGCVLoKUou>`xGLEnWXs**cjKyM-sOPn>%C-Jpb7spo7hm@>69F?fcAFtUk(& z1Fol=P~KL~v92EoOFK>mnh?5)M^kU=6B$C?M~Cw4ZNtnOTJZ*rz4(k6Yz8-DG6Z5< zl0hJZ?Ltjf{x4AlaT)N|!Cp-y5lvB?!rXi@;|MnflEq9l$a876y}w7?$S*86mH78z z=4Rjkabm5oz=+&8TPf-%v}2uYBr}8xabmtpCUo~e+CFq5d6$GaOzP2U`XG^aE`g$@ z!DCYIkHSKxqlGznj97pIW&jWZH@|;t+J~Wj?vvwU(j@9Nur+K|zB2bm3s0ZvO0;%p z3sS0y4!e=)GrzwpjSs+m3(5+K_voL2LcA>R5Ak9}{op^`*t9hEJ=!**fEO#$(d3`w zzNQzmV~+^oV2oi?l0e8zYMxg#59(MQ+j04J<`{YU)dUK=bSE3du+xss7RV`%Z+qCBJe4Sqbvup`D>5}c)7Wvloc280R`L6P zJQ@HQms;7H22v`mc{-0=kd%yQ4%NXt-tj^T6qHjWPlSPtbMEt_2?U&lbJX+&(AV}i zTcCJ@h=i-$GqCCxD>5}~VRX)D+57fr+vxDciYC+?R<5mqU1FT^9EgOf*T*%22jiaH zzS$S1nWOI`1X)}rI|9K%P@6eR{)Z}~qW}D8vRZUwc^e)JnBKk>ZSp>Nu_6{KB(W$d zcNs{`i$J&5j_Zu`PRwMrPhY=1nh2mUs&4eNcO3pQe}i5PI|`VWX~zW_ftoY55q2Ec zF)9t|HGMwhZzN+{sPibz(Ft(&-KX1ab) zfnqGioxBfu2g}<2LPj7;hExL2alCCmY9o;Q`piDF0Ak*^BE858L=U(o(_?$FN5<1%FLTBeXvG7iWgz&npsW)bJVT zj2W?R|JAlDdq1t{f=V&)x+fAG7?kaKcIapuW-u)>X=^jrD0vL z{ieP4(3A{ed8bvH%9$MUsF6=|lx?~|lqu;uFa2!S(WEod5DHSQ_si z{?2F67q2y^Ol8%j96$%;p`_GgfQcN^EdE#f_3NYfU7kZ|`q#%-{psLX3ug0L}8;~V+&4h{F1mNVa&tiR+De$ z`Ng{gHehg%cdWokJlg-Je-S9$iue>)qfOcu2#kqakue-TKTrRg6u zwv6@Ft3XDRWp^TGo>BLD5ojBS?KzM^)HXKMb0A&zi*^>(iGW2Wvi}!>wyEeo2ihz( z`;S1n>>$iHRq_>(efygoJ#9I%A{+HMG` zd?K#x4mGFaGwbu+vDtC|_X^uqWBXDjs>tWf7KkM%88tKwy_W5th^FR97`}^-E!9ML zT2b`b#s&xxlDQm*?JJ5v@Ri_)7f_%gk&r&GNWoA2R&EO}1wOWUkD{wZhj)W=)jw9W zRpQ}Apl$H;7lDvzTd(D5kRom0qe{=mj2_%eQo7^opW9=MT*LMp)qBUUrad+?DQ?=E zbFxU3qtAQPzzXB{b8}~*nxR$wBM@^yjB0Q%QXv`FlUISTa-(0;g{4}|OGR9$?j&g=F^t>XFL5f2J$HtKMPV^$518LZG_`*PG>@#*`wb4SP=wG&O z?f|xI-y?x&;#8NqWu=u7<$8WJI@Rt)GzO60A~PC$4m5)4DdB0?adge-X*>lAProZp zE&CiBo+^<4w}jZIJX2pV<_K$$Fn^9Zd>&)%G|0>WqzseUHb4>yc=)KJAgl1Yl(o z)~R7)mZ|&MB%bT~fzg)v({@9Ls^BzoA8iXMz@lzxk14Z)QbX52JpD3zB#ipb}c+)MFDQR&-Rq)(6sl zb=Ak8`up@zm!DrBeO(tHzkmJu{Ny&^{7*E011!kFv(w6smogYp7nI77W+Tj-csvql z(XOSOcK_uV9s#mg3>nlECjy-py{0IZK=I+;7%@?f>q-aA`y1ZU27nX@SgsB!{2|ey z==x$HIVux3Nh<*jqQ+mJ3BmFL!k&If#vD)^l;|01jrVfV9JArZ(4q4(W;Pqcn5iJc zg9rbJEDt)Q`7w$^VvpLBDqn5AyxnuMG04^G&P?CMY2^6gA|V9WXAdAMIfedoKHa6Q2@7ZwiS)$wKunK;B2=C z3h3}A#P9Q?3AD{W>p4)gq6c?o6oCYgw?CRdppZ2IC<5(>tN-L)e8 zZ1P1th*hSrd9fl)hLSGlq?+8ZB1I%XT-ntUH_C(CA59?05q4!&$o=kkvl#-BG0+>U zNaO6onjruwmG~*%1gp7vh&L zE5o|YzuMgQ*QjHEKvN?#WO?yujF-)ioU2`d;ARTs=RiuWZj&_6i$EGb zVA0-eY}W0YZABCsC@TZ(T&C@xNQ^W#HL)U1pOED2 z_;7E(j%+khKA1LX5`;{mWPkqEbR$?E;MMzZsh4m6)dT`z2PZ)uX*0yi=M@=bLPawL zW9HpuIP5P1X=(y|P3V=k?OAO!$7y`9U&Un`GtlAZ6$unHSnDYpD9i1eEf9c(iN6z| z&*0gfRz$oY0?GO%|HFfQP{8n<05LKc#b@P#ECpJCQLKN`fZM61}UYKZtX;TfPc~@a~-koBA>}bV{T}oR`f_?{-HLDls&N`2bdPvY1L+tXB&tr zk*3J}GrwAR_mTBjgmB`@W)}h83|x21aI}mskMYn5$#na=s>SGK)HhuXJ~LzNR$`A1 zYmUp#*_#9A&A(dg(KeuNdlZnCugXW%phk8<+PjJa>YBP34AoQ@kn)UaIR&2KIhx#$mH2r)3=kKUN z%g|Q5f2TKs>cb&KdW58v3DkuO7S_^zvjrl>s>G&*1FoA1ME9yIs&#(k&8F=_O3ycE zNSX-bxk89-9cRP~8KW##g%@4qM4+cOwD)=lKVTNc9yoEZd93GFy;4^DP!p0U#;z4< z_-p$(cW+aosQJf=p!E3gVfmAb%jDV5f#wc7Ih~#>wD)xTqeY+r6Sg*IO~LeDtZ0ae zP4Z_jl6kYAel;s%S0LN!ZnEti?~y=bNDtiHv#Q%&dqiBzxIfII&N~9tHb!5JpDS(m z%&hi#v!lblNiF6@AeXpCz1b*Lo$Y&MMGi`Z6$WuSrHsG$az`dyo?mN}m|UWR1Iu0XI=kyoB1>^pw7Sua8rN%XP6 z*4(}#f#ChHtB|*J*>(jAgBd(Mxqk-9`uwX|5mG(G1^cYwx_w2_J*Hb!@|**g$q>Ds zRuqANv?!ngjgJ{j#EU>Ne}r+Vpgp#yHQPJvFII#^C?D02lJRc-`FnCxguTZCSToqH zI|9M6Yp_Y)&pfpD1%hEHY9vzj9*O?Z^B&1+ubYJ3Uj^DE?EWIqHe~&Cph|7!Zu5LC zs_pjlct#+0rs1XI)|_qo&#w(lM^V47H?YZc{hl8U91-?pr0R~Mm$Bk`3beg9@*K$B zsD0zsM`Q&a+q_4!{_&(1#D^WJL~jqY84?S_dQ2lqHLTPdxZK=vqme`Quz5pKT=-pQ z9&J)nHzC41cOrtp>ZO*4VltgFZ??nr_jmbGR)3lg2-`m&omR$2=RT^c-``*7Kbc?d z0+Dsrq+1x#>Jcyv2iq_;DH($fbkSM1B7Qq?20k-}^gbHEP1wn1Hhv7I+u(#{@+ZmGM3n@-}FyQ)2nFjiXiW*GzOFx1non#T86z#g$H z;t8y;NOv}|qLDA{bTfB)bhUaOO6tff3R0#*;wO13bwZXs@={*a{031c32vr}9PJR6 z@o*Oni2Hvk;RguL+PuU>i`e)v!V29{!U~$l)Ds(nP!KO5UC3Hhq~Spr7G-V{Ds0@F zFreQhiiavu&fBs zQ&$Xfuq(apiBqy~$byYpv`e7q{E>DpFTNaz-^3oBDd%!cSq-MWkOMISDrj~s;|$Ap z57*?^$M5p1{iuKcZa%v9Py0dg{m-B3^HJ!{(5Iz%u zAWjVWpqU@7R(vlGq2v{4G$jH(b++PKigFvLg;hl2D8V*+ZfqKmJGW`!>XhwT(Y3B2 zh^|mA8CuNqirj7FCvIhs6AbOQuSlQ^{aQGk^R3E=;J;XrUcC%M7C7D#W&4Xj+k_Qf z1w#2L+?%Bt>GnNxiEI;{eqND42(gA-q<*Ld?pjem_Bo)Km}KU)FIKb-sQW4q)R>yY zYm^bhc@+qr^;S_ZJY}d1F9L;oBA+14@r+vRi$Eq(CAP2Jxyf7rPsfO#e=6E=cJt|G zY>S>(gb@j18IBJUn~WIFKLTwXkG%+_uSK9m?v13q{mtHtL4ICQbdh!2bP(atGPXs} zfi^4Z{v*)V=I^UO-lQRj>`jKq^db;Ilh4d==RDc|t7%6=MQ>>SzF5&V4DxfJ&8oxy z2&89QOhFks*JVbS`$Zt6&x#*Qo0Bkl3KaDSY@|*bWhF`X=3i}YY|3j|s*?!h+Yh!m z5aB6KbKq840nxKIA-?yTkClYZ5 zep7X#NAkQ!v7)XmSfJE8_WM>O?XGc$7=~mRn$IgjtlTDq`y5CWNprY52LzEq`}Ut7 z3mSvDSJ|EvkvjqfES5ZDTi$>O`$WtB`O!SpC|T=C>h+w|wm+J&aR3PPaNrR$UaZe6 zqJqnALIr`#!Xk3}iZ+W+Jgq1+Bd)>G!5zP}8A{BHKv-JJ$&+?lh7b81D7wg-V6iU( zZG*+W2t)_jwePmo?%MpTZP)mHUXj=E%6<+lzA7)5?FZXcYf?|S;Kay)Fhga~ERa_# z(tbJ-0G1OXw@`pyg!tC$)D;&*d%oXzDi081o)=DQ>` zme!cjc};%-y(X~E}zTa z_MlKPC>pcMYa*>-+8SLnbK-GYvaXY-oGd-Sv-fy@nj zXo%x7n|*;GS@U*$FCJ~17Q%}_9C#_A($sgc?RvD)0GK$Z$y)}`_F_dK=p4v9*1(qYnvOrOy)f9Eowp^NIx8rtkY4 zNJEq46on6z@u5nLd|m{i$U`o$D!4l)60PSzk&KcEZn;i4*)sRq)7Kw?YAhBEX8xx; z-lGUK)+19kWR{E;!t9`;L3fO^Yj9nTdg*@41cy;>aNP_TM4XKYD(& zxck{6&>ODcPl*{}?YGEwve(mQ z&w(~654;Ej+2jpU3($JnzDIs}7*y#|yiFP7k{2u5hKqa^$TX`skqx+I8VIif-RL{= zKQv$8o{hw>Hkz)T)XX{Mffp;9de{RMdw7;^|JB+8^%MNSCf)4QjzGZ`q&RUU&&x!4 zSD;`rkuELZ3KMOg=RM+ANk_@c7^t*q`*}nth`<-YN8XNC_tEA@+YUl~dNg&QO_pO- zY~4>8c-4zQ40-CZJXJTti0zLS^N`aSuTC{$6tO+fX0Ng5N83z>e+mS90RN{gb-Pj2 zp0@w|6mTT8;C|#(u#mU!k*+A_S)kYUyx9>*!ACIyk*UuRP@eZ_v)9;jpv_=r z$k+2wI_dA7h`9ST6S3ow8dQFXJvxfRe$jW1{_0;!v7_TrkAv>uDS;aWGwCK`zN>Jl znl@}tk=W0jnO)-1j?%96-W}A?Jcg8%ZLsc}m}?|{e)`LBeULfJn@x+2bPb^g zVi5$o6OV@D-2>?_nyjIP(5ux?J<7hhSR#<&zM&dz(Fd>+(3fGECM5oGBG6HD5hxMU zMykhm<$+KgvPp2HL>_RE6Dtal@cz7hv>~M~;{&Rv@D9q733TadaI+PE9M#e7L?OPZ z$2QX&anB#~t&!#M??!-Jel^`SIBWMdQT3Et^-$=4+wn!^BLqz7DPY3V)4>~ke~$>2 zP=te5{W-HkN{<>+XpSciHZoNB4;PtTuVaWqDuJrWn>K)xb0#vl!&^91|t#>dxy zBQK^5(VH6}5iY=`<V3nzvTswOoPoqLIjGjncKDk@Oq6D6MGlE$1QJr?u>YDkG+O=;ep*$OY4El_W!Wul>g%yf_C)r4+GY1Mr!CeWoEi9}0A7{w%3 zG_YRO#aM4z>MCAp-2CDS&Xo9QQ7AhK#nYmr9ULu)abh$vcFABy2Q1SQ(`D?g`4;i> z%XDtsJ(MFE*i2uY3Ibmer`usJ6lxBlN6#!@%doT>MlcCHyU%R@nh_H-;VAMH_0p8;nP zx05LN{iAh@+%_2~(#wu=`n-|`BR}dCHncSg_Ntl9)|t20T(0LAQlPMmK=q!G*i*mS zLCQ>_QwX&x(Yk*kanuHVp;c;mG2VYPfsodV{d!urI1n|kmS1iD`UBnIcwr8g*lZlg zo6Csg@m-mTz?(@gLGBYlgd))yW#Lk_S8jmI9z_yCZIkjGpjuE*U z8VqvGp3jBA;uZeveG4ho>|ezH0QkLN3(bte@R-ZO$=EQ zfpCl`y&_cl`4b_2c!h?lN-aOqm5D&j<;-%=x8{U~WU<38Ne6IKf&3-DHT+K3j;l=b zG0LoatDaXibFXuzIA#Ub^Cn(>kLRKWhp^`AfuND7j-cO=7tgBy`J;^zf_n~ zQLwlX6ekINT9Sxh`Esdj5$k)CpUh@U{SEonyMOK;BO=rFHC@kSY|#Uc-qV`-`$T-l zk|g+S48X+CKlE2{lhgXG&1be8%f?BPGJz(_;wO0ApUqZu;f?YMQtiIKqOk-JgfsOx z>w6UHUgv24NOco|PN?R1ILo8O3)xBg!ECy4Ey)BzdXS#4hyUg(dpwGG>J3sXXwNR2 zP5qQYEcIHzreaL|YU6prpE^4>=DYvuOktr1x?`Am$;^&8k{*NhQA!^}@f?pTvqtXb zmelyf<`YuAHaaF5D0EYm`T9p_BcEEeUXYH0ZDrON8VpmQ5AWHC6>Vc3(x^VBH-hVW z%z0#=!>sI49J`L#D0Mj2RJk)`K|YCnTR}lApNQ&+rVXz-tmnfnMKP*C!P6r5j3XEp&W*HOd#F5{mG z8+1&bP4M8DWOO%5@3A z639(!F2U1*W#uU&IwdPRCefO1#O3sfEn-e0LLuHzO!^g7bK>r2C#IcJg~MqCite$n zB#`wKfaKp#B0f*QC>4HxltumLqyE*kA74CSKic1I(R}^-ReWU*x35$4yQx1vs_yIa z!=(TFg`XCnd*?;y0J)A@v@w+VAPmB6p{pRh51pm?h`crn{yuee z|6P4`f8PE9Ozvvv__8(qoli|S3vOIzV5+9Oynl}S0V(uSAzqJ`C{N2k5Vzy)I-~;8 zu@2_Iaz?qvy|JV}y1s`1O+1?EbLog$3>Bk&PYJsP0Dw(P;I>NMq9cG7o7wx@9;5r} zb_;VpAn^B#L4nZU`2vzv+x7hk)M5FMv%as$Zx&Z3?2oG#$T6`;pbUt)9_`-0-6}&< z;u{^(zDHp?*Nq4x1;lDK1SlB1^w5}l@gQ*OEs0c>NJp}i%we%IM0@-T&;^TaYAxDJtRX3iq0Vw zlje5SYfr83k!JEX12cj6&DLj4_Pe-}J?d|UNfT4eDIsOVulATi$Bz@URwx8GglH*B zxiQd&(mFZ)yq_}bZqYq&YSuURM{{LsaXX!mwflrdyFhZ?(mV6>t#Pg*L0~AIG3Rm* zg@0aa2t4;oHlq>*YP!M@YRN0-<`hQ)CK7o62|;{;R9+KV#^uq1AJG_0$$km%z27Yr zh*h;fOS^1l{knXyhrz89`s1u3PK_q?<2;J;lqR}W>J66fC#C{*qzuo0w_OK4JlUXf zIiVXR&_M@V^f#e)sFsb!Qzb!EX#Iwn?uk@GLV`}C)E^|GwcfNoT11Un506@J!n?NA zb*a=SWp(cbP@O!}Hx0`FH7sM^zp_v5O)_#_F-b88fQ$vg!Ups%B?5V0Sd|+0hpCro zl9?3CNva`kfrKssyl`9a@pZDz$H*)kD4~QTzJl$(KPxFoDL{OA4ey^wblp)|9e2Qd z<`juXJL=4i=0S9(*3>VqY}pc-j;7`u{{D(s>L~uF^%j|rksgx6%XH&8p9zFwEvGR% zv@;|%dm!s*pV>Wrs);~l{i%yUn6;wQs!RRRpMNV&PxbF#r~V7M^(zxhr=AP@k|V|A zNs%S+H~r>>m4VOWpyJO|G-%L}gL7&_RbS77Mz@{}O49q&Qsr6)0P3V(=58@mFyUa& zhv8BszPGBrnYod8Use?DAQoS)O`>n7#Am{#zHp5A^?x3%Zb%RbgzTL7)f!nWBJ^K? zg6?{5>=iGQgyyfQv|k?{e*O8Qhby~LTgEzs)3(Blo1)k_lQS{i`_gz$9YC&o5-OUR9MHL?2pP=1K~3J^;sK z5(r3w7+Yus0!(G<5zZ%v^e6TFPbbWOsnYytkw@|hHaOSTC)<;FA#WU^HD{mz*REEW zyBg{7uC5X_8(eJx?P)}D|N0#xp=ZPeHK16To5bFN53HGOBog4)ngLe^)(L0k(wN2$ zF`554kD?X@3Jv>_MDfQGn$|E#!V<{WuOfkNFcQGvNo@9HctZ_&ox01l$2K5%Je6T3 zGl353M${7jiMB4P|9oxVz$W!lXJQ|-lvlmUi8NCs$Xg?Uf4yc^jULsJZRo`@~$hr0hMMK}1F<=-n zx6FzJ;#F*5`+zq}w5O>osEKFtqdGqR2HE51g!Jn}6Z2;85_-rIJ*fg<>hfmy4kyUT z9@REv2s3-6qaqnmB;6k^YX5U>U}bzD(@JbMs9#w(4#s<<#o>R|d(U zWLmg2ypZ_kMi3GSgAE3qnXmt-E8BJ))3DQ=FV(NkpZgv~SL@2*uhllcCWr*uys`*9 zqBTGAiJ;iKC4DDa?}=ZHT_`=H2S>t+Hj#FMX5)w+-D~vX{AwYt7?L|7u~k)x3lgQm zQ8*P+Qr8hxHQgg;6F&?X!-RHJ+31F<@YHShY$TeR{3{%5cwh+u0fJR$s6U3j_ds#5 zZJKclH2QACM{9_imUaZn27nOzHM6HHO z_}9I*g^_?A6K5FY{t_t0_bsA@8q%{SzJ8cp;|r}mT_33b<${c4v<2wV`t7@G;>vEW zKp%r&u9^@3=-2wjyj@a1pU(To2=W2Uwyb9P)tpD2*E8$_JKgt0AlVVF$J&gV?|z`^ z!G%rIEb|^6HLPnet0phaif(m6(v9m!jqXObD6cl!ZYhSs@@o2na^=;V?^a<^#(xK1P!JzH_9tG9Pm1464 zc6&~|*~a4WZ+}dWW3vrdYrIB$-NXfXu5TO^9w0CSpRiQW4A!E614#U8#76ORo%Hu| zeeNIw;7S0XtDA+{TOvQ=o7vK>yRC#>8Qa8@mjyZoXo76P z^AHB6k)Hp2{i=QBr^ENTeBRw9<@QOzG0E`;sW)&aq2PCvK6D=afD=Sm*O7R6h)R&FVrCJE8hvm~P@%T>90S>0Ykq zjqu#CzReRct>x#JA7SbD+-oH$Mds1YfNWZzD;d=unPr})+JC#uU>rJkgtES^UB4Kx zgnhAO#6T;NXSYY4N2Kqy>y&KEF(Mm5vL&slUkg6;{wIR0*LnRQiXYdJ^FSl=Mgh1< zH0xS8LD$&-v{-fOZf^b@cz{lnI~(S2c{J~lgqcyCkFPl;rdi;bHPhR`_(UmsInikt zNQRG~w~QkbdvwLAk1^pgJ&qR=WQfWN8s9kqLTPc&mF3lv)CH3h)nwsU)a}PJsJqnV>5y*q;b>{ zGMi1=XKJTb;x6aeH{a~9Dt>MummD&B zYO$HD;i~v#J2|1 zds8aX*?6=M-6}{jp(>C_{lGfp3Z`=RZ|!>OfQa2}Y~rC}6ZM*=GZDOI9n?kFPgT^y zTSAj&&Lb*a;pfD>^g*?v)a6}Q)eviXe~)_gHRP)F_}=w%i);*30_A{;{Kfs7Ob!T9 z>9uu<%%3U5F)dajIWnO^MSo-?N?Icbl4wPy?GuRpLwY0L;jI?K_W|_{xuzXu(6##W0$}w@(D>$SXM`m!tMRgEYN~GM76-B2av6K=JF9?a`<- z1WfZBL;pmeqsn!d#bb(NW8Pz0tP~cGDMkCEPDuA71MYGEM|+_-0@UTIHx3Qdhol?+ zU%5;`EFt*>-s?()Sz1QW0rsN1Km@38(bb6(LbqUOVWOA2-?(qCO)57CtY#wfzns{7 zXgU!`SRc(T7X_pyOP#omE=Q%uFh(bA71)SJ34wE>FR6npvw8np6X=*cJB*D3|! z;1E+B{CPBlX$J^*wkgwxyeMNTJw&+Zc`7QMK_LiW6{JR{f~2jed1*?!)FSfi=u1J4 zxe-!7|3D=As-|8ZO_Rl$sqX?S;jB&kYB#wMD*ICS`SdFGI{MH-MowrVs_rk78T6IQ zPrTVP&$b8&KovwrG2WkqJJ(KMo4!8UK#>J)z~ndq(lK5u%7MDm4?y?gbZYnyguMRy zd!)ZcA5^Npe6-PtkSfdKO21p8IF`hsXOpg6(wfa>7Tp@%IYKKxKYy5?yMG61Y2si5 z>tvsT4hP{ASbxmX$u9u3ugj8|)P#tx#n_iel}+CuBpR(Lp|7Q#dC)OJ&{e}9fMMCA zXx5RR)nXs@l@b-CS(;xN0%`A<-k3HdaQ~+i-PH5H9+mo;rJg9_Vrbky+PK$ntik*` z5{rq$|LQ)X75BIxS!g=Xz(#z{=+TL9O>?!T#k|aEVF)l73x9W_ZkbSN^v4cLv>fp5 zCFbkPdlaSe7H!QEf%2FUG?ZUC0?@Aw1S`THkRR(t%W&S`?64Wo8(*vJXpL!>;GMe? z$ZRD79re*dyriXxm?t2eM{fk3u4}JU zdNBas|JA@bO?Qk7?kghwX|UG03J3=xXe9zM7*uBwFPZhVwo@2y-szaA{zcApTZZDE z)>{UmNA6KC6mW5QG#OiTAhccKYKKGv0eStHqQ)ONZjT9@P%n|so`BJc zWO3Bq2$E7)!`Lno7_FP(fjl25rU@}KQu?;PA&DFYHVIj*S;__6BN&=yf;XiT@~6X&(NeWdFc~xN(NS5g-t(os z6zmX!W=egq(eH)xyc({dXC>ZjIBHe{N{D;^DDE}itxtyjd%~JW=kTnh@*w8AjJk`~ zxFJ0gqfBF5-p{sHQ~hT}Y(^VT7LDjcel(I%_wAF^xo0CrJ|?cxNN(2VTJP^sh$$F2 zJp`v@BQ}WQO!WT~R$&y3M`R&z^X1W;9)sBN%MHUVQTLBWl@M?P5dDu~X3YDqv@3wv z3~v+G&LwG817aY`<0!KuN;6e6Z{uDC=FIXVip5Ws2fC#3g?Ca$;Lz|V=2cr@6s(B>i1 zKi49X2m~gBfD;v@48fb;4K?QA)ZFiWAm+)*f(bKLW==?19uV8A z64ymmL7Z2Q6GtW^gE1X7@_|(asWo9;zO~Kc)hPr}Fqr=OXpv1@x|Ss4GvsAj#m5|M zaG2C;JkauJoBKyBypO3xCM~C1``~38iua+bi@B=M$AV*A&DTN}C zzrWd^zpLW>_3MM&DM{5|&50b&`PT>So9g`Q>-?ol@_w*if9mS1{q^zP{OLdH?{oX{ zshj!ndH$^1U)2{;#rJO$0$j(;v;~SH*1h!8kx1%Ll& zY;I-yoC*Srl-gI92K`MeRxGJ7 zwL|m$<0Srg_*M|ATcPbCc zZV9BWPi8iNhwGait=5>>hxwd(DG{hL*a#IqEtY6( z-q3#Q8!aSS^4Em-n2zQ_hd(g=A^COyax`0_R1Wn+Q%OAU==-1zRWx-Hd!!&X<$*-F zceb2Iv{`xrkJSLdp9#c2iP*r>*o@YOdjNU>v$9HvP8m8uc!q7z&%x$+^xFqAE-}*< z=$M*>(7cR+cG-Gn2Y65`knjt6-b-eW4ysA86%V?O2!yaNZXVZ>?gRqD1XiL78{Th7 zo$w3}sgg*kh2aCISCS#|(UQQN3=~6Vry#G8cpQu{XGh#CPp31XW(wCMCu1Q zHgq8jx!h}Uu<4e|A%fsf&05VhtG8pLCey9bBFvDU&WVNW_ zNfW{oz%+7*md5k)XzKq>I!HQWq3YyA|Lddm|D}%(ganIKpko^3YMFy-5e`P)?uWkL z#b_Em%II%sF92lkzmTW!W`nL%B}IDdrK-Ymwyh^0VZte%I`~1?5mjc*Rjt0|SA!qX zucyqw3K>H;@n{Bo6i-&_`9%{^0_T%e&=S|k1Tx~GovS8|dkwLwy71DaL@`StrUD&P z9f4^(?0FCWeEq&Y$^+<4*MxAn6jbX0{fH&j+>UmPT#?}=ZW@A#j>PHYa&C&tdS=f8 zCWZmBfk?LB$7-GYG@K44#v4%PlGQ3|Hi6Qx?u z>((>70*DHAC!)y2Vl$5x#?`^cUp|o-*W<4iH)!IFq>5jgg%y7A`W{KNfW3*e@iEr! zOl-E}M@`K7_-7Wm#o#^Ozkz1v-(o>MI<$BFipri^(SW0&Evi#%nB-_g{pPMt^G^pL zQ5~I*D*QB5+fb}oHk(P6VgfI$zcTc^2m)Wr{v@ zp*!|I5)lDSVg*+vDMHpnhnvvq2?m`*OSgP~v;h-~S<7K$rF*(<(WFG7|4}43NCF*0 z9r9C0+s82auVgC_cIrV;yUZR<;qK=#mOxUQ8*DqkZPTIeMh*zM-R!sRIgL9t(H$bTKyf& z{*OOJ9Utzm&kwCbK8wG=xyiWRx{%c?AMZS&(jD1se!g# zJ)Sd<)&)D}QQTmA1oRRMi4Zb{^u%Txh~ti~x>}I~1Rhnk>%6@tu5O)HVy=PLo+rtH zj)rSPh_7qdoy<{kR0V)*96a5>SwE2DKuV#8nW@`q%?xU9>kAqBYz|voExZB^#>8eH zmAHeo(JcFkEkNkjVeNEJ2A1T`SCe_22cDAw=UZzs`(ziL%VE5hFUbEqsHxlc33sx zq>PD^lEp8;A4>Dj$19hV+<4K5w7$Pj%=4xuSar*hD|;ie9Lyg@T!OESnN3Uw%?ILk z?zzR}2Jr_U0QWzExTBqXQ2o{%RV_83@H{;qm^t6@Q3qiYthCLiJaBXG2@w3^? zOmFgJ-jkW>mS!6PT1nvY^KXS*>DnDsa?wv6`B&)R-3cjHbWv4?7=&s*k(jEr#;T*I zu+SPKnb=~5p}DT#A5vjI3D>sr-2mIDhIGjyX|R1o2SN9gGCGH@%Jp08aYOmmz4d_DbxLkzjxUxCka7={zU3G^wkt1K^l#Z~8<4_AUdR#uoe3aI@DD z;O>o$h<$BL{{9IW-W5miNW#2w>dizE($*6a?H+7Pw}PF0W<}(yXpgTHV&@UlCvK4* zv&c-hAkbmhNQErtk)%{_!yMNyKy*2=qA*XwhFw+^l4#Oy)s3Dq86GP+rQMM>N^Ns~?38 zoCJC!3J$+GuR>yvFn2ICR^1;1NpS1nUWNBqotTuKKOu?A?!!P^CHc$Q-<`kxKg|;0 zu(Cb>{(gT|MAnd8IE5^~Tx|H&l#>|UbW6}=9Y>&h6ScnOmpl6*5nH7?{g`^Y+oRGO zXV5VtMc!;!`d}B!;THM!Q2UsD&>#zm%|5D<1d2nfRg>6sU>W0ta2=9g4va|>jJuPY z_Lu^&J*wvx78r!6LkBfZfNQIy^)p^uKvE+2iOn|l0n_@MaJk8%hc!D0I~}nVlo}Vn zu^u7H3}_?`;MrNQYC-{fQ= zPy_Q7o$@>7D#nQcZYUyCKD{rEDbPWqGP8tZswC(1z<7?1!FjHdT-z72OQg~za|zLo zlLpN(Ln*~y9a6zdNxB`~icmw~Il4CY{cR|2J|y&6ajrhda~ly((mf)% z$Z%~skedon=Px6ZZCo4H1IK{CLdm|-;8>Nvd?M_u$1BsA>Y7k&l0_VpYln!)L>+Y< z0=wt9mX}T=Wm1!5pdsy$<$~Pos(0#-Do6V(Jfl{|+9CC;ar+3rml(h# zvRWC$T*9s|Wb{WA0rr`YM+O2)HkMf};_s4YZ|?r}BcZkCk+8>1Qa2clR2u18Nc{XZ zrzRP>9{vh83`^B1B&Jcv`+=yU}t*i0Ui_XL?D7nfbn5@J!d0x#zrTZ z!Fz5>dO>aH5OiAm+J63PZ?-Mfg~LjGB6KH@No&;keK_>25*b#ctT8U7223a+u}4Sc zM_xc91Feu{v%T3^2?Yd*7D>Vr;Y1tfm_*CTT$92wT)F@J_K15a5|>nt>q26W02>jj zAKX7D0=spI2y;~M>xmF zl%fzis*uLz^C+`W+EBu}k@Z;S=MQx}4boMw&6`bc9gNG%L})ef^H1PkF^vAf|52wJ zCh%Q-$^=B_TENw)74Tu=U}HH{u~VceH`EiX{QE1?oMpuN(2fo&x^xCWjS2{<(?lRm zQS;+f7dd7~foPy1MJhj&jUb_SjkZR3;(4@yqag#t1hAyLnmGIgG6^HjwS4`qBl=z7 zmN$wW373Y`l@Nl>ruuO|M%1+7|8PEmPC>XO_J|=4UyZdj))#V6>?k9gLF$(I`Efo^ zL2Uk6k(Y>^v8iGTj&^Mbv{Axdu54s9e&br2I?G=6L|S9Ea}4y8R>Ltht+DJeg`a-m zF+Db1!KyQ#TaOVhq-M6dl1fs>n)h@aUG?Flu>Ab)wJ|!bKqXYA6RIBrxe&%)H6_#F zBVBpC#q_ppq<Fuc?v3ui zpne9LK3a&9P-+iZ!OR|MU81!Ax-Va1!-P;uHci{y=3<2sEiFP=9sS^?f`NH_iTAIZRpU*mU$8E&LGI6 zDRT%LkxBe9$f)6{Vcm_HBijPXsQY~W6p6-!64-UKqwa_IuSp&FLK62H4f5V7ZJk2Q z*+{L(Wj#Knh)pWm5p+!UaVJ_hYHwOmA&{yymPl(juoLLy^)|ppo6z$88>NsZUY{+5 z;`;~NtqEUFEJj8lp+n*fhplWbbQS_nnh0c?sn4^LngjW}$yu`%?I5&9fZWs5B6=GxkB}#Gm6R( zZf#_3G84bsQE5&5&WEHm#(K~Kl{fqG(K5)`(5t;o%4u#ae`g~`faXBHwDwMIVPo3=J7J41D+*;=n{L;Q|(0t&j=T-xGWQ}3P4L(kMdYTe^R4oUy^nEc4MPAu%< zg&2KEdRPNFb20}RMGF$h=(*H!md%c26q`k&#VIG@t92qlbWBm>H#5BM{}#p}D63?~ zi`5=Uyhq`9E3+~5n>n`Sr5U^ra8b||nI7B7#$tdRSYJr@8WI|)EN~4iy2Ofn{uIz3 zB}4@3Ot-H3369Q0Aj;~EI%Zwb=39gwlinyC{qpnsXu*gGhX+_AbBs(!Wo!art^u^% zVR|tXhyDzh4(fhJrq4DX)2g3%Orb>)55AvA(a%=C1+eObp!WW+HXW58u~I*UG{#@$ zI*r|mMz_;9)2n4#!`_`N^O9(JkDO^bkp=I`%v@w<8%ZPAj#j+ctc$|~d&Emh>=AOe z_TH-15>ruMy~cQD5SVF1p@|%GWdl$&tW^G95j!$05}D1IArG=e-zQ0E^b@Z^~ktA9fw zo{Nx0{`&Z(6g~Byr>~;_eEZj{T#)#=G)#X!l}!RUP`byAqO~w(000eZYa=Rv(iiOK zGbedJD=}q*izbtz|2!uX(b-Z3vb2by-TN(jfv6Y>^;ZSJqs0i9%L3iJ#AdfBISRK` z_|3;Vo~4#G^+f}?L?EM|AbzedrqNGmiWwf$N1fg;lC_T^;Ye3DOWCg1hA!I$fLLje zd9;fkt*jmmskx2FP{gu$^<`G%ePqtQPDf`hGN7J;7T1o14yjHX&7SbI__-)AMFlGt zZo3I@P0INGoIH6AO*TN?8w@uK81fuZh@Lug_h1`_gR%XSciHJvgG+VX>Di6B4 z;VDf=h;?3mN(oSn`mEDg7uEZR8##-Zug9A`52NDe4`&)TdSlL-`1*Yyq*>SvFF$`A zBN__6^)D|p!^ooFSqQp+#!zEREPH6` z_h*q+`T6_%PyPMz_3QWNM_sZKP?2^YBu+}W|F->|uvd*%Bp=FRTy4X(?M;0Fqe)Q` z;d}qwrp>86->&_(**Y|!X&Om9oJ81a^G+`Dc!;jc)Wj zjBVeq-Kx`P|M}++k+ySLe_%zmA77uOpNcO$kgohy|0@0@-leZU<*)MhM|`-7oVxm; z-axd2xKyLit=eCSVBtp;H`-D#$FGNW)Avv*YWT#*YeLeZ$gCsRIC7!?xHeFLsuIm| zjxlm&Ro_|#xCGoNd@kXg4G7=gC+)3cj@(L%gV4sxM!4T57XT9=lME6vHD=+a)pL@b3ma4;SHOkK%>As8p#!lC?-2aN4A!TJnLn zaV_02Eo`*DdnTEv0tCPVeJ$Ocw<3Wh`QiEmhqN-s#32N$UiL_;Od!v9LIYA3kl5^_ z3LKd6V@^sC|6>v^AIC9$5^1EFFRaXCvyIx&_hSwVx&8iT*90TVUUIX%kmM5NIwsPb zwy_d3fet#4%tYOaH#4P&*7hb+llG>eF$>8x;ffTnarnzqn298cz#8&Ie+Ah7ejdq6 z*Z?)fc;j@NK-*~@hcj=#P66j+VvER9h+zV?^TvAo{3HcJ*KUFgNLYwg5PRm53J`5u z?^CZMsl3(Spl_!6p7{Cu3yW=fg!h@-Tyb|m`?QUTxFjlo+7)6DQLRa-qn4U1(#$m& zD(~YYsygO>SawpJIQ%!W>?mLr3W)*rVE*~%N88j)K+aH}bw;E2_ecdmBbUz+1z>2m z#AZ9_o1ic)ft=Zx=o35$j%}vmCe(#PK;&!bO4#hnDFj@`QByy0X7i1Wlx9_Jj?HEQ zR=SOd0wxGMm7YO4c)ayni{fUB)Vhw1Ps|dZ(F;a5=l#t_9y}(|!b8H$ zSMn`HRF4L3Nzw!v^pa2_@b0nqC5OCZwT@}7FQJcZ=5Au0cr;^aSl=}p33<#5%v1wJ ze9CCGRe(+4EXHL&Z?+PJd%W%@ZS%y6eCY}hG^9%1DTW2@Ky^B$>?3D7awQyeB%^z{n>sQRF4Qu2>%zCPG&G#_70@u&Os z`%4Y*oe1{lmyfpi2$I}KefljL1n3lACxBv)C5+3`BY}@pUKh2+lD@Tq5woB7&uw!MEsSN6ydf2 z3z#4`RX*GrK#C;AyxAco3I)jg3!c9p6E%rqTarfV<-XZbzK@L#(~J(fCTLkc$WLe{76U&W`+M4>bBr!q19{Z)N@ericLKR^Fe zUzuQzvI&`VdAk)jo51p3nS(OiGyV6XE#f4Y>)TMa7^tm9I!};4@n}aSrljzWX({4q zwk(z(IJc-KWllHFnFQq$iK$mIWQ4kP%;AnRB-Q1p8X0sV#UVZICQ?%VY_^GKd69`# z88;cQ$il>I*2Bwg{2Lhx1B1d79oVh*h}wFHvk zUcrlHJ_uarM0?|mJ9Y-JUbHq{M>X*LnH3QJRVKqVB{?RaY>zr6!M-%dJ6aF-&5jPB ztlg}nH?T+~6s<3TkKGEG3)uB0cv{uAn53*JN`|X+ZRANZ9-sOrWFQY#mG^?uQ4^+;x|P-!$HJs}uJ>f3A=*Adq^$|KrVWbn~h~ zv7DLSgnVe6YJcrf=&q_vd}^0O*S!m(&JoELlec4^yDXQIX>YwJq# zAj>NdIDl@{J(u#F&xs1aT)Ke2c1hmfB^Llba-ai{lM{YY=*?POA#Hu%a7O87$vB;+ z(?M2FY2hi!SPoX^i^HLhNx7W^$jPFI_(T=*?xYWw`qr%%q1S}sDu6^|72HnCxbI0Vjz0Xxk?OT@XH8wtu}xH^x8O=B|>2KVk*i>St>oK!UiA zs3VT><-o0pGn=^YF-=YCUk#-cztla4jDD>yQY9y8yM&^(N8?9Y8$aCxF}Wt=fU`f; znjL-S7-9Tp^rQcDuZ|>I;9t^Ep9-RR;#WKB@V^|@W+gy#Oq-SDdn+?r%dB8V(b$#^ z_JH!46Vl~;Ob&T1+P1iMv8c5b&GO^S$eSWi!ih*T4J4irF*^CoN}wa8iogp4wajQq z2y{>~s*|qq6pD?1$%t)GYqla#4=%rJDXLbIosa_8$>pB>{+q3RbxeAE)v%M-Lg+lc zezYRgq8hz9GabDim1j#K)iLy5mV{j=q7U<^p0KRKDX~X#U`+(a$m3VTk}~y0aOpB< zHUW}jMq^jaq5_}kuhcKRGCGHXmPz6|@|n4I%E$O{9+Od(q_*bXOq`IB3=xf@{`F`? z^~Cx@K^6=saUHpzj{!F6K@80hbc=g3>e2LW$bLfu3~izlL7~gg4{L0BGmB!xqZaG;3~OZ| zl!01l%F$YoSS^7(+BUGxa8v_fI4bqi&mfiXkPw}xe&Yal!G?&4bK(<`d+@M*d~0Bu z$d}BdsP7i%LHR&D z9BtD4cMrDFb6)vK^P4>@Np!^Sro>!?e(5neJw5TBm@ph z{h%w3N&QUw;(XLdxZi8inu*F8bOZ&cF)`0ZU^^y1iq2s{uyuVQo!NwXiPE6}BTT*j zLPBMlrom^Fj5nZRhd69nhh|XS! zKcC5PC184hYiycNe_uzi@t_;15eAZp%_f9~=(}Fo+dmOQxnoj4F27_)LJRl)X18E< zy3nIrmH=et6KO}@!2{0UyGxz5=?R zSk-r51CEJjgi^w=Iqcwi-fsET^{8VLiI!;>Ki4}D<1sl_XWknv% z-&Pa-#|u7MA`pro4=uxy`4fpN+a*WAfy{>CPa+V>wvzJrx}vxsFWih+;sCTIZjp&n zu!+{AH6|GahLaGOMZm@+Rs?-gLujoGgg7C$2;=LC9@IY~A9+l@_SJuuJps9{{0iZ> zzWM0eU*$*l)qER^xrU3xZTtJr7m0lor0;K%bY=JJAp`SGO&L?NVk1|pdx>N{Lw_H zyH8qv2?2P)LJ~vmzI?#ZCZmr4Sz>yUfPS(=z_IX+5x9@DDbm1A#4qVKLW6?)FI20} zud+}>`rN#aMH1-N#RPNhZO`R|(gb3PB>DU2cfhI1BTUyNF^#inlj*BK${08PQs0K{ zv-1*(CJox zorpI()!)x2wJ?vFBJ=5MLT~p`PMFy`hq)Bk@$4CSUft1j0qh@N@W^1tBw-0r#1Y7T}wdxUT3DNDD|)n|)a4 zH%kaD2y@VNgt~eS6_ftnZ!@dwZVeD3?`GbH@8kYWeVy0wsr|g!XlVWc(uZr|{hyyJ zJDmS@MQg&`&|5ag#f={QO(GCzF5uDHw+*h=R71;I6CirxU__7VMw>*#>_{V2`{`x4tzJ7hWOKA8fzBd5>qJ#BVD}iVN8{5QK7_vF>y|F`Ks2p6x zd#@ypTd)r((jZ7A4!7`C!KsN~eKvhJ{Lv$WwY-InQ{s{YsAm!%EQb4vXk`uhE&8W5 zHfkbALf4Tsu#o?GySq?s*J6gAdcVjCX>%E|t);6X{R`Lcjhn4@D^L3F_Z7{7w#Z{4 zw6`4b9=}@j-0-~_T=2&#+r(!=F7A*v5G7RKCxYL40Vo9Lezdg7)b;(Qi$D})n5!6- zJvJMEOyYHGX`jD;lrXLLbJL7oe*TFAjK=oVPy@?M01ZQft@-HPZf+>JMeys;EL@2m z$3+KH&b^!T8-!}=TZ=%%8V(8s>#=`D(H)`Mv%J}Juf(_of|x=5`@Ir-bkK22G!#Da zF>r}1i4t^EXbbw}5+|g#2u@o#2h8Wu^;R_+=RUTuy+{}dksxr?(*Bk^gjV*&7~h=A z_Hb(^F7*DJZK@S^4=W8VjuAW^2FpBdu#+-@!t(v>ImHorT1s^^v1N!vB0CLr2v1M30alQ6f-u5d2Xl)-w$q)$hg% z5@65Vv`lnuv}w(s0MM2dNsli$j%QC;T@!k3=BK~p4<$l)Ihr2m+9#p?<~3O!EAWg`7!@--SJ6yuK3?lfOO z)tV>f>zRcBT0t9j;kN+ouuNM5`EX7o5!$_V3LAQ_ni~GPzU(;twmI?d=}N3KaY1TF zm>E6Mk=H6RTCVPtGxKEya^lc7qI1)mx6>Yrv?kusMcxmN{zw%zv=L7(HX>$ggpBMT zy;YZ7iIkWDFs?;5Mc8X~a}e7@q=teiF4 zf9cnHr{ECmKem^xVG`IBu6@M66V&1OICW}~IOxMjOk;hbr4`M=6RC+$MM6B7qmK50 z^Qn~_MFVY(SqnOT@D&K}_~0&NNS$_N#5PKdF1y12LDF{W6Q*4js+9RdHXuQe=iI)U z!1k6b7QMmQ3(?x%7Sd*&q4fu2$*C;riXes%r#kBIx5h?1f16v@{4Pt7*Ww)`^$N)qo0357V~8R>gh zkAW<}1$MuvzmLCgMP8lmUo1p!Ztq5f>W2^v+@O_z&OH&CXSe=(Oh}pdd%=#ZHAq(? zwV@n-rCYIQ|IQoe_tR{Otx&=t)rcO6q@$|2g_3tcDHfU{w<^r&MPLCtiPA2olT67j zWs}`t9&YDNLJAb3_21uUlO>J`RQnm8fIjAWNjkVrpxFpL;8e23bu_54yL`C1b{<&T zv<`E|jR@Pg3D;Irqm>@xZX>!XNxjTq%9)=gGv`IIUr9PBV22tDT@n#eON1s0!0&cwqlpkI%REZEQ^KzeKr$o?>0)m0NSvi@rr|=&X(E}j7un}1*-hPPrP9~AWnQ+vtfrO-xyGYNZR9&W zXel;mqRk3G4tz>__!Vm3(%5bSiH>$?uDb7R25tf^JrNe}iLX{G9??X+5yJQFR>d#s zPXm8q3esL(0*>C8I`_PX_w-NTbzd5qkq z!)`cVCwR+p`lDoL`FYmo3VqGQS5ql+FTeJMiniSSZ%FhKFeB4_7D=|4Ek7D`k?hCl z0VE1PAa?jH4V!JD*x8Q%NTrFQX(ApCNc#40VcAjU3(J=KW77U1Xmb4XlltR1>ZooN zq@9h{Fe3v8Vo<#Cz8P+{{cLQ~5^VoMG!d zaj!Y$Y$zXP5Ec7~iw#l{oCVZ<;)&4UQ9RrLhDgjzfJOv=tQLeMNae&rE^O^0<@$DI zQ#JyT!*SA{PU!uB4pa(ku+Y@xvavgq+rXMB=QWlszu~yNuSKg(_7? z!{@(m22mg1P6WfZ&+dcW4-2{T<@ew3`TPkud;H7!Y~Yj*$+o-Q|M+{v0EvZ^ zolmkO#r837l6M@^8;undwtmNJMJS0G0;3)5zr?{E*~gef-;6UCOxp-JYFc;I*hF^A z{513&P$XCX)@fehV3YQEePaeG__ngi&^($$KCiwiyhaWtVJop{-`awPMa=goK9 z;x>k(E9vA;($<3HN<5Kh=$dT)j1Z{?g`OG7{mkH(w_2UlMl`E)|5J|Z2Qa8IuZ=D; zwWSc0z;H@*BOpBLBobuLFG2h)-O)&q?~;G&9eR0*eZ(;W*MpGay0e$spWBDRCiv-z zyX|J+cp>~#XhhMABub|AR+c1qBQHQ8|Fl%Mks*L}trJhXtJ@zy7rM;X^FHF&Dxad!PJy z5_;6&B#ht^KW(BD4fy*fkF+$*RY(vf_(;u7U9M+S#`PgVN6PL-m$KnNnR(d+aCK~m z5v@OMP)6vKirT(@!q1j1(;21GJnL1C!rW+3pzQFVOS~h^8bE=)%N{L6ZkU(oMPd_X z7D7ktozUmXvC)~9MgYakEu3v4)CS&&kZOTgv45Ll#;RHRz}Zivf8KMQ2njwQYhbYT z>LKBL(z1F!jJpy~#QdOdOg7ckks^ocht%Iq#8SFXm@7h84SYfcj4h1BO)Jzf6O;~I z61g^R%@yQN3|li>*~nW;O`Zq_%8o23L}A3or_@6p#c=WXKB8VC`-=;DeWK`&#$cQ` zvRf%}`lo^SA^iCEjx@wCBQO9MnA0;P4y{Cc#V>A$_U%aoV~>H?Tp2_~f8+*XMI>2* zn`!+__iL}83V3WJ9d!tuBs?}*mY9@bY7ma`=eRwVcw|~*ajM-cmomsG2H_@H0wjn; zXlj#40vrTnML|>KCO&_y{OIm6>?Akh()f*J6y*PLzsbP#B#%E)H0lM#^3`JMcY`F- ziUe%R)U`e}$wD{uc4##t*{a*qPr%4NF+C2j2hDI+ENgevhJ4uN4?`2E#zd%fq^=aB zmF(+v6v!wZX5vba$`iq#c9fURwtQVk5O6_-@PnH?!}}-ZuT$*6ZHG2Ge}1X_)Lf&< zpfy10X{escy%wRC`V3n_7ciK0;`5tZaH|&D_IE^7-w**V-ON6-5X+dDsJ?pIH%Ub+ zkl*gl%!Z7vGZU+lunQv!I6Yr5E17cQiC_%}N)csRWBlaG1WpouxdtZ|sue~UO!*DF zDq4^$wf>cp7i{Z*UK&8WpAO7Gt(|!KYu(Cw09STeTi}+ zuswvtGp1)5)xUj|s7_o}j@N$y5#6o1R)r@K@aP3f@q+zUg*xxm6&CC)Dji-nGSJIk@ zkR;@TZQalOgcA#yILfDsvu@mLkQ!HgZQm>*hur|6V?TqBtovwz#Q>&~?L{L>$n%3% zb!=Fo%wIphuQnsLkV(9p5q=Xt4M;!&qJOz86i-BTMYO}ZwMmaJB!w_}n%XU4U)~WH zaSWYBC^fTCOBus6Z9mPOJbo` z12H<bBU>kH6>7=KB40{LB6R{2Gf4AD`EM|2~@E(?D;h{rB;6{nvhT61m_; zA6~2twQoBJLqc@a6qT(HGXq&li<+m$Upn~-ZASsO08!nzZ&`*KMh7nE>qEN65btYb ztlax~lJ-&Q z+lt?&0jjpv=A4d$cIcLni)PkBUn?SkalNsUYZr!7MUh!(2!YSgS=DFL0y>c(CjK@( zVpKjHzbsqrnZQASM%{X{)%-7*1u=nq2~K|{iX*{XP5wJ z_jCDbzCV8Z>kkRBuKoD@cRGD^-`y9#mhSt~{yY8?Ve;eVz;lS?HaiWg$!;KiG09xwUad2o7)pfEFSa{)bu)y&GCGorDJi$`YH&Q*aqrS z%|wEN+TDqtRw>nSDVdaqX49W{L|2r*Imfm+^V5uIA<~VND63(%;s_*Cd)BZqT`TjXpnobfF1Vbk??>i| zX3$YE)~yq`fKt@FW~HL~Ewi(qLK9MyUeHp*DdC9|!EREM#*{z)jtCggMF?l!mHZV2 zGXN00yU?A0r8%ph{^O+jUZ2Q2dJEo&jfQVG;vQ^g%@ZLt=?1G=$ag1x8W6$+UW+6b z>5cO8K4b9HqhCxeGYi$3lmbG7fWdk_uM19M3`Mi)3a4(!tv{J~qUKI^hUDr7k{3pv zhX1;Oot)=5+zMQ~Qc|in(qVs=t9!X9eMy54?coHr&EUXIAFd|<5pHcE_Kpt|?rot+ zlO4V%Q3(jj#G&=)`3^-ABDa+$>;1#0vy6`aHX+-KDd^La5h=+8ye;S-EU*7TijnyI z5{bbGsTg$z0%+2nY=RT=NQ4nK3!ynjrgy(-g%E68pd@*3;)!bI+hVLa5bE~!M3HZM zV`mT}aukVMgltrd*-97z-&adOC9{uLsU+^T(08fPoC={B3|5Gr4UyO?m{d%Zh3D2^ z6EC|~1psMPWq8+u>C{QwFOM_A4{3|-^T)l$Pb?-eh82lJd!_mzBh}OR{U-bt8p1N; zkIx^;`)~Kz{v_-l+E61R+W_*-pjTC+zdXDBJrqVspE$S>VHrq2O)R->CMoZ9(9qEu2{hwjCP|K4Hm7 zT-y|LBu+A3H&tRG*`GTS7GaG^%OsFj8^{&h-EpW4m7hne-YY}SWm-yX84zI5CP@T0 zV$I&_m4|HH0Qf@^_KwWH+%}LUiJu1C5SGuSD-k)8gemRS8jKzMoPORq@3JmTG92}| zTlg*3GXg}x=e^swWywY%G0f&fV^x#xe2tf z0aQv{0O;9u>e@y(`>LO~_1i#+)F+HQZdbWPLO_rh>Ya(4@_Kzo`u`4xf5E&?YMaE% zHut@1xNX3csF22s%2=Lyy(UJASjUyF?IxTx*jg5^4avY;d>bFR(rt^RN3sX7ZGG9C zRh!XtvyJxoB{40f6;$cAb$7{QpZGg#rTupCXPPhpGZ!(6prm2(QXm&~L7a~7BeNr* zyD)m$JAhA`NmLhyP9q_)5P(f!lFN|ObtE2nB#mK6<-|g@#v1`)e)2{vw}|ZjA(Bii zlX&kYF31+Emhyvs>w43^3rw-`>Wro>bk|ySv2IVq&k+6R0wy`}NJwNOY=~wVAo}9&oY+5;42gRl$uiZ&^1aMQmLc=ueGv{$GhF zVqQQeTB5cn5n_&Ov{2r-Lm3h&$t;AMLHXG)RM+Z4nB>t!Y_(ei@h(4;QD&*5t<~=yn@bBZhTJ-uaPx`$2<42os|ND=)s}v(1Xrr% z^W8J2pSd<@RRN_gNU09wTFBNj$?R__^)FK~We%b;k2*1qG4^0p)Sw+D^LrIp;A3Y?Pu&4M&^)+$R;-L!K z-s|@H&1~T?4F8oRmrM!a>{mf=2w;^`zphqb>n1T49qmuw9~e;^+KgmOpvidkRmvfK zv$!{wt0UMySbq<7F5^As1YM%0g|*b!6m4IpABhub>p=`^JPzH%g0GZ}imC&yW$BGX zlSK(}f=$w&NX4UCQ)E;NcqX{0`*LOL8O^*R zbdN9OuGcWyv)U-LY{!Y*zcx!k8wqSoWNT?zZ=u$et?h%=toaJql6WGW2+j4^ zX+WW*F8KE*D0>#F)IbQMDG-@Krd3kEw{($hMgBU^ zPzhh}ejD;Fmj;Dr2XH{aa9b$)kVfLb%@_s=wI&PQ_>hxpSr=MlRst6_yUPIr5*yNx zUy2&Quc$_hSmcldvoZ_uvc~f$^_qAT3*Qh6 zZ&cqh3sov1xO98be&3(37HKW*7dtyq90v`a%#sUo&$9#Rt~qYbo`DcS<~7ZX%(8v{ z=x8AjurfxrN-*tocMKjoNrS7^=?}54=pE}%!C7YJklLxZkO05iPm7vXF+edxPbtCL zkmu`-JiEWC-*E1Y6fbI{YohCiE%JDHo|25pY9|#g%?E6|3{WTZK2GFyx1YA8H5Uao zCo%G*iK1q3U32Otu)N|TmQvKwcltb9qI+ziI=Be>I_6HNf=Q0FMw^UIQ?DWUNSrm| z?o|5G5Vj2&y2=ngNNsl#0EwEGuYxq6{^_=lydxaIKZ@h0XG|pfbO=J zk1x}S&$jvc{K-Vgfq13+|Iz=~OA>=6C`dc#EAmK`Ey`=~p^+N)EUUuy*ao74_ru`qO=X|0Wunq$?eh|I3p|CV?}CN5MI%HuGAyh?Bb|6VbCZF=Lp{u_*(P2iL+IrmXM+S@LKibP5jwF`M{lGaaGGW4GWI zxnwx*o{^k{lgvU0*M?Cgy9Gqy8_7F9{DU$b38Ae{1h{EF@QQ$7TjTcHAtgxSBMfb# zM{?se3mvh`tVxqKxmjW(b0W(fe@ApJWNK)`+Y|ZxINKODA(@?!Sq)icBzZjBwQdXP zClj0YE(w&SCU!Q(e|!PE==DT=3|YW1$bfQ6{Ipt0sge=%NHc4FBH4eyz&S1wBVlHk zcp{6?9^rzqe}2k%(;4Ls-zhPTxQVr_i;M8f!YoCNe#xt=XZGt8%6ky z*#?p|h7N)g@zq4A)xhXGI!hZcZkgCLWcHYt0^BC9qX`Of6mT~o%E%$@Yy=Whe^{Wz zPty@)ex!cJwzIAm$y3g;X+Z>&o1p=9d@t zqy%PU2UM6?$k8W1Qo6{|Mg*{s!_i0S6Jk`L5$ruqccZ@b9R=bSCxUShnoH*Mqa2a3 zF#OqqzeGy{c{&S{bl(!m=-qC?*4=-ag@_zTY^4)~i4*ULER$P%pR@UCXnGF=Erh=e z3Xw!ZohzGAr$=x-Z|_KarA4NLOtl9^I`Px)5JwdLORLp8dP{1hz1OH8DWd?(cuSiHcXqtO6syRkw z&&-4ZSK^7ZLREd281`a%Lmu=`M=TqZUq z$mVM&Uk?8-Q$pZA6L{OP>@oRM2Wo1lMlik-UyXUT@iC=l`Y&ck_7N~K4M8O7>6(Cj z%+v#rnRNQrBO-}_C+fnwGbWwKpNQUkFht4)8QBQcP?Bbb!C>Hth2%7%S{c|a`q~Z8 zX70>)vT~V)+-WV zqJ$8{&^y@jY`oFv)|}6Je$Y49gjyr+DLHGHrV@`pBFvCNZ#ypFL^gs+i8#Q!bbBH% zTYY3asXrzMOMErfCm_Ft$`GmF;}q^}l-$765)0We5}_Rz85blCI!?8uT8>l_j2thY4OCcje`YqJ%Jb=VT_* zGBZjn#6=H>lDZF59O-rJDN}zp(&@njHnUKr;W!#5doYTaTLZy6LieNv%HG5vG4VvT zGPY-}-iYM5L0II0vV64=#R*B<$^4F;OrkgP#Ltvw6C&Ns-jr})=m>7KIuh@wR=vit zlwplz=CvVb7h4!?!=fh|2nJ3dUa#)4;Qyq&*AUDEE6lv32Qgcrx*QQj4a+{h%&JO) z6vyAuHw?sY*OSs25t*CspN}sQ<>NwA$JhJ*ZqGl57wb;fMuHZ;ybH=M;`X!c@a^vq zvk7*6=lUUwc9lD>F)0!WwKnlLtmK5us)aD98CQvxB}EqB&LC+BWr>m1G zCwg09pWDbu))|};k0l(N``z=aNh~V!})78`nezG!69Ng~o(|os=uaE1$ zALs4gzthL(<L$u9MzasLl|qn<$c(g=nj=Yg#W#$plZFm<06EY)`x+zzF1=ePR;{m!r$j_M~}xyhv8C z$s!F$wo&vF0b&?Lg=kXDC!Po#F=pdsVCSNwj!h%szH}rL3n9@?-UTIWMUsZka!GokhXcdR7!v)Ih}+zqu*bUHf;Ng9(gxRuE>K49!4uU< zlbsCx^!qLxut=-X5oml!Y|Y~ZpoN5=`)xlWe%k#+#f~JZ5yEEoNhY z$jqb?00Jyc$2^2Nw)&rJtHvn(FA5hZn~8nI>shJ4=i5TN z%&;k8WNz2gnUB~-((NA1|8@H1a0o@*8@0+4pFjFML|TI79X;C!{=DZHbn-_@Nc4r5 z6@_=(a+By$lQ5u++*?VKAjVlJtb%$nMr&Y;BmxS(x3}Fbi6lbg(J`iew0*VERC+d& ze|;pqvg7aQ$`R?`cmL6Tet(gCZZ036{pat;zwf3EkJjI>ArS!#3r*X&^KI`bq9oge z`Kb>;m@xBH0(5gL$yRu5@wTLDJ_kf8vk;zLpvB7Wb^$QlC5bAzHA_l7QLP+_aCDuR zq#f#u6AxVXQd|IEtjV}YXBMJ3 zaHDCyF7&qKbEiC_XRRr|y=+Z#GrPPix+W6cGsxuvvW!DZNSt31+2_PWg8VdTeHBQN zpbtC*r6>VxiO{OJT!!gP$Y{aH3|@#jI1;HbPOT z1{IbaTncGRHjwOS1k1!vS~e%cUV`G$f(f*a8_wXP!zQH?iiU6ZY4vc=+v ztk!uDw=+Kt$SxEgwlMN8?UNd!DRM&+bvtX3jfCYQRWw^ZbJmE}l2*hOSo27qin0v| z5&j>y)WlD#Rr@2c)VZ=74O*D|qGKCh?JbUN*C4?*E}t#1#X?N%NAQ-jR5kOC98Vw; zonEsnN&8(}^Qu5Gj<}fTC0@4Svk`f@X`|Gl)+vPy&DiU4#t*M@t?eF#;I$!17#a#0 zzh`_}VxfC$U;qgfYN!L9Em=Q9Ie;K6zQj+fRUy$T51K6x!8M6cA-N35Hqth`CkEss zf(T9kqikA#t`k32-p^SGH8#=uX;hvx1502FFv{(!#tQ7M-rMna1dk1O#)AC6el>Ea z0r9@!`5FSaI3TwCP%@@crOd>JNL!s+#6l*|dUWeedm~|lBf%26523U~y~abmPUDxz zZ@8GBHjTE(YWX&fm$_+;Z4#e9e172kWDd)QTt2@6lH~X@nRX{We>Yww!(~3QXh*!2 zj1onvQ?Io>bx_EkGT2|AjP z-;Rh1(v#t;$>CQT#MuAk-b3 z=`m*(A~=JntNLGIAKn$XrqP|Woq|x}iEz4QF@=L9JiaGp((#Ff%$EHBiAQSgP)5-s zISy&m&mJSMg+xFZf!27}IXS%yVppog-bw7QU*T#_~)lr!}>53 z2-b$CLp!%!L!N~K=$yEY(0nD@-F_WO*a-%MqdIHM2NF+ID<1WD6t=T;W=H+#_6SF! zHp}0{6X|E_AR8a9b$?vhxMx~X3e$va2y9QKes+(^5R>_k(~IIbJ{TQb|CMYvp+JQD z)T!5lT)*boU`gyEHKqqcqzsxRWO!pZTHcY&U%-SvP*pw?0-c?OI=2N~l=X?k>;X20 zB27QgAxnI&+Y+!jKB-TdO`ezgfyKtmQ-OT436HwhXQVQm?}_Og|uk;6X%(sG{?ahnIV61FeN7H;L@BaOpB zTCq)x>&2rwBMo%iI*k_GcRK&%@3(j40$_}%gRhmRnUMHuJ(!KAGlJKGL~#FyWEVli zlTk;aIX);rU|{yPMzF_-Ti(t5L5dT}mSF+d7nJAr(_+GIo>6ad{{k9Kel8K*oYXZ-_ zduli$!5yJ*dYzvMUSx+y0~Gr#8A(7sVC(~BIRg(QVs_Z3&g{-gGb(zKvkifQ+<8}t zpN8P4Hvjw6+}AqwhByR2GYie5*>wRkkE(YpIqA`?DL5w@EfU!}=^r?LXw4Kj!{F6W z_y}DXnF51!0M9szplU|j%=Ltv_~6LrKR#=3^}hC#?c zi6>I_8%6xzv?gd{q4o)5nOLaToH5rKZ7&I_5d z?jwg5_p3Lr4Khaw+_rHN)OpNRm8t z6^?)Yi3w8FiY3n$D*3W1yD!|DGIS4owK{z^Sv3x#VaO|tfS^&JxS=JCiJdL%>&@<% zNQQM%Ca;IG8QZx;gv1Q1(`Q4+uhVA>VKxQh#6V&H{O{HMZhdBSI1h=WmW=0C;?7!(!GcMentmbs=BfDMw0gBreEzytJuR zv8kJYSGP_C!Va+&qAE)*n7G&O1oi2Al+z!42wqJ$j5sFfnIocET&KrI`+{VoRA@c@ zp+0o8Khggf*Je^b+CX*i$7VG|hTuE4C*sC*Z}nKo{K>X68?x4QbVoCfg8I*}?nY}t zSBjj_EiuwSW3nd)?1N_!vbp@~CF zg#nZz_#tan8#6X!D1475XdsE7Hig|NK<>IwfSD-8qg4qd?zY52f;J>W;T|na9|_W| z`XkSrsA(nUbuzZcLxU|w7u!41J|sLy6@5Z&%|NRap%^1U^;lmvNP@kt%qC-^wV8NF zwK6sx-awm!iR$?4s_ES@QHQM4w(HM{aR&S>53~W7W&LR?$cJ2g25tk7NF<{n#6&Zu z2nNKJO+xC~MJKS!AfoXn>I^gIT4{|YUFd@#9`4huw;WK_p+1w7R*K{3X8DMLDIorx z*he#1mri(j+3%x$BZvUP?D{O8NIepy8kn6n5!k53NYGV(+dlI7?=#mIcacqz==#kd zQOaYp@qI(m>tP*<%m5m3{H$3BhXJmCU1(|g(hb9_WYV4?F(WI5A7smOb}4Ec5l;fA z9|Nsx;_i1FL9RR<+GSD`y;_OU^?nZV;mmY>M#a>)ewNhlZR`lg1T)YJMUVAm$LGJW zb_5>-`_G*I@O2iGN*IX}0^``^(7w({7?>UCX>ebiSHurHPCDJBA1){)b7mhw)^JTH zGP?dW8&c{g6G8SMv!B>UwUUTXybX&7_fhPl`26pwe9$SZlSEjk4%(N*RGlLN#E3bq z@4RKT!Si#ok+Tl}fJ8Pz2yu`{6hY>186T)? z0aFtT0pyc*D~Z-}`qi{9DO)^~`mv~`GiIQrtPWg_*2rUgx<&?b$P9y3>cZn*vv!*u z2_x?jPw7s0MHHj9vleq&Z68)46Gz@e>i0x4KPw}(*_Kcnz1p{gC%6qfP}NPZ*RK|R z$hR6%=GEa{Td-_q{M2+X^~6Vbmdu8%mHK`1Q1(b4*URVd(foe4AD``S9|~k)i0EJA z@9|CfeLemD^j{yBfqq$I{Pgj88oxgNK@WU=T`oUAU&pH?m{0V#p0fV74WhQihcd|F zvXxN!vXB_58rHj6B*SNR*am?;ggu-~>WS*$*#gd5r9}oe8hOmAU~B@?@D_Q%C}L7> zFHrb6DxvlJfZf3)Y}^jVFG|_j-FWs~EDWOALU<<3D*+S|ni2FOxmS?V`7`%_3(*P} ze?n#C4E(w%HH5+gS-O=lRj`ndW*}O!l9;n`GS^ow->CL(PJ0LO0=Z(6w3k}4rLE=uHDbY}CeM92-p5#A7h z&_VD~hup)tR0Iw}E497Dp~OPrrhC;7TYGIfS~o2OqTw8*n&X#%h3a%|7NQ@+<6Y>5 z&;sc~qR2XKD*D7vqtC7VQ>vt8SG|Lb?y0M(LPEmf zQ}<#3iuP~YSqmOJOxnExEKm&`|7!XO)W$@%@uJq|i2 z43ZP?2%(M7L>aY4r8L2O;>8ewNjQB#MmZ8QZT;wS*w(>f(lf8dLly#B%uCgxXhst<+DV^fLnx20mwh zB5W4WJKEvQdX67lmk%h+-H3b30o=IQO`)|;sv{9f)P7akK={7Y=7_PD^X99Gb!h8*!r@U z8Ng(aLjfo}(h@(78B6Q--XZ|=8XgVifWY_RN|0D6ya8bw1G^Iq&PB$Mlst$;3XH1o zpUgHJeq&21vEiEWK#}A-Ue{utetOXt-wKDqp8x){MLhHqj)Mb;+qR=cA}prV_e+k*^cW+Apkc4|FX;^KY)p}8 zaX@?Nl%my@m;{v=C?LaO{i@7D&{i6E#6#E2jVV&dzggo#t343`TFO{De^gf2h5R(K zIBs19m)&tZ0Blummh9!c?FN3=yYL)>?907UV^>syAl1vV;O@Q8kBFOnoA4Malr z3N$$jY+oypwZ;SD&VmUldJk{od|r#5UDR!Fi7Z%1{)q?u$L~*-bkRRokz`5QK| zNXi<4_%_SmJ$-8aH|*i*oFHPb5V%C@)_vj>u?=x9mqS{KOO0>XGzdd*L>` zi3dhDf@ZB_LwAb+)A1)V|ErT8;~}cq`53M*JD?3dJ%qFv?89ZA=-%Oa6uj+%k-(5L zvNbtR7`^m9`9pD3cx^Wb85#sSH?;SLgu;%6x`sv2?mZUiagYJcI$&-R*+>krZW_Ss zW|pR1(5z94O@slS6CioIco7oW{0Wk=vmKH6Pi8OvHgiOV%jfC)_xD3v?E0ZcbNv{; z{{1jk{P*wj*$+Rb?|+FeSF77jSHI4f3}e4e`UmY_C)on2!Xt|j)ZT7o8UeB8w9r~h zyllCj@$){G7H)+)?Eu>g1}w1<{|V-Yx@60yWY7@kxgiu5_LkdUN3U-HO^NpBe| z!B$coyzb#P0MeL60ZB#-O#qxN1bzaMRIVfiGF7AjE;X*V#FYenvWJI<_=?6Cvfm8X2wn<)qIHECCVj-#Q2m)3xNuKEWJT)P9Bgah4 zBx#YE92!Y&!A%4ZT|j5ht^fm$KM_51%1qS%wuNFKMO6Aa_no0a;$`2US>)Aa3+QLb z;>0?|ZJ0?Vo(L$?z~&*M#O{s^&Xq(|?h(kaL~3)pj(l}>rBt$!Cch1n!O~;)`#fzf zO)`IeM7ky_fB65)yDE8_*-)5H@&=*C4> zX4}#J;TJ6>;JGtreww*2kX}I~2EplZ!)-=;!oo-lf62}}47&EOjKl%S3kgJjEYMIp zkkFykG0h9)Sg6vd^+tx2@8OO}^p22QQOQ6j=S&|-BBachApeO+6r{8%j2ALMLR2js zTaxPQLbuLur3OSf#db;<2a#l!vcoo(9La{nwZF+E8lAkvJ__P-l~${M+O$|kZ9h%< zC|@&*ygcvcHuFRPLy%SvfTSft+DDkge73{9?U~rwlV~)FBg(D+xKOPWc>>S32l-g8 z-9Hv@*@j**}-gCno+RPK}~`!H=uk@iEPBqtCJtW)YPdW@9a?Z zc}Y^Or$6cu92u7Am!~oL@eLVm83hcI>$;FUdmUMGV2OA1pt0CuL~TFKyw^I931&y5EOGinzlz^bx%-zso7B(sXfGuUU-&Rr zYW)z}LC%;7m@YWmc%rxH*7zEgN{B~TqDY0d>u4!z7(VIY#!Zzd{PZx+t|YiB#3=RJ zZ6rnl!ed7^7!t=nzrnl{%K~Nl{BdOy5Qza0Kv*KM5m>F$v3Y1ZL!IuYUp%5H#=EH} zy70)nqgtbAjiNe9gc`WcO#4wQ9?8#?!$9QQkj0$sHc9=NQuY~H?A~=s`2o~=-ACXS zD9#TzHWdbX9*GSJErNCM3bn2y9CC8f%^9>Pu}~oOX3jP16UA8@HE_wJDUcE-rlhp5 z4O3lv?@xNns0oXVuvsGWr-%gv={DQmQKQ@1Go3e|>cJY0|1@X6PEn&PQYSwm{Zyx2 zKb!JO*NRO4GOmyIVaSs{6H>il=HAXTIil^WdF?@IWU2}~cFw$PCYsVCsx9m1j|fS; z8+LRCB28?_ImUIQUkg^-S;HN<6S?i1IUydbGNxbxg;uiKXtKplonXv{)m{s*)dZbFR~^@rFQ9F&NjMW?6R#*;fV-#N#tR%^NvC+HU*& z5k{~FapTPqh{z`nt+8y3M#}h5Zv^URKxeYI?uo2+t`*o=gm{9NC59&osy?zTJF`qS zf;uc)07cx6h+!QvJE5Jhe!nKtW2P#kNXXet7)enax7v>YpAl5eONrqqcwI@@na&JW_Z=7 z%sks2?@ZC@SvRDXG)N?}oUXeBC2?p?!GmxZ;DvZ1IvpYR!0LhWL&nLXe@;EJX~?YY zx5#@_h|nA&7W=oRbx@+g>RToZar!5}>XvO>8E8ge<2#9(d^6YjWvbQyoEL5krZ6OjMG@!P` zbfJEnAYF)n&;UOE1qUT*m8O)*HZgpbc7<%c$#f=gw zQXu60!(xer;P!?q*xvU@jcmY>8h`trkAK(m&u82G5^@J~P@YP*oxIm|G$37C}{@fqG!U^|y&wN`z znc^~tCvwM7CWan+m1tuGohVpIAO5JVGmRQI?-&h(5U>!<z-aDSlAlyo4`WDH4=V9N#cC6!MUfGMshq!V9lmiRdjo4nBq+ow$1AQ#*&nIrO~ zk|-}O6=j??W*aml0^X9?M}C5&GNcXGG$-b5bezyj3G#`A$hlvU)hiWZ7{w;L<|tNH&t(E%-k5lLHVK}f0oKoS)3U_l8(Qw=wb zQA7Gtnv#F=y1bvD{w8JsoCma4o&VCAa2>tHBG*g^35!?Wt;J#B#a^`mXSxCdr zd)hh^fg^cKL;xY7i>5!%w#BtUI#2!^_*TG#O-kJT1Tu&t?0O^%XlF)GASI}Uk~p-& zY8CX4<58#VayIJJCg$puuA`9Iup=q-tYIY5KUb0P$V0Nv#3QmchLVM{M=L>JniK$x zEPcb{#j>Af|Ec}_!le*&MAWL+vx6+*N?efI5>@KAA^ID}K-g6;tSvwg6oAu{kXl8x zsdS56@z^M`=1CN3w1Dp}rUa7QLRUiIV8q*|w2<7ZZw=3Zw-OORhpkSzrOv7o zB`=klQv^hepke*X{qMG=P5}B3mVP;9K7WU^M;pIfNwPLC8*Z)@%@Z0)lG=$8IY8uw z+jekge!_rF^1nH80iYtCPwX*ElQZ^_`SJu`j+2t8wl9>Vnv)y%Pmc5s+0wkV(oMpG z97Spb#cWL0w}N-#^w+vcfT|^*JJdR^ZD`cI zvlX-tAaf{WY-I#GIz=7kSlPc-%2%PO!*GH5rL@z!KZbj+m`ELT2wyH!YDP zQESC(=uX>0Z)5*G(a;;P7Ruhj{df6_2{-`>8W9Nu=$dJMlz8N)L8AJ2!vnE2acHrI zd}#ou?PWW({P2TxS+g4YbVpMz4oN-^;R}(qXhs)=Ulr@TPnehBKResnnM1;wIh}h%aQnUZ~MB)FE2_r!jGZL31@5LWpo9bU5kFe$OuSTsK z^}E~g$Ugr>&>#vqqxeKJ+L&YCV*s>$ey)~e4GhaQlBR2-J%-c@YO6-tewsX+RkGn= zGfo+UJV~Dc4TYwX9!GN=yhz)xZSM&xnuX>kmhUe67-59wN*fCG_I%=H*D7i#7a_Kg z!}y~iAy-Ih`z=>)`F z@ifn^zor)Qt(aJ>dnq7SFhO+1+6RbB;*#XGS0^EDjmA#EC|tDzZVe~X`EmGQ0R>6u z+91?xNKSUa>!&?|OUaXn0GVNw-N!VT7S0v{EIKQ^A|MdR%e?F;&A3tHX)osu&?=~x z*N%*omUrT-b?(_7=VK$I24A+LXT>`0p7A2rD+ntJoz~Zt1exTT8i*YCOcfQ=-x%6bjL<@1XG(4hY7hwp2`+{|Jw>+8#LFGPgj zacvHgFMOLHr|b?30Mm)Exwk9f`V#mp7-sJSbhHeAUEPx1=YvoAgY9l&3)D&hX%5th z-4?1djy870&426_fUoBCQ%3>dgbq%;Bd=BiQj}cJn@uT+G=G(1>XaoG0&j0g*p#m( z0@&0dq{BkxBC*idzy3%3KmYXF_GZvFHpo@~$s>i=-r11j_+sYTbz-+>)FX=9^7G^Y z1T%3buQJ>OC)og@QZqr<=tSAnw0-$<(UrjbhYkdO&M<&^8)*V5K{dGIHc@EXTtLMv zrL>k3{Wf|LlSyxBCd5U#$l42e3i-rEiM#V)+9%Okf0{|M=7#1kMn~T#Hl=I33KWz+ zI)F9fhJqxE?5)!!cjJNuC9b9J?vJS)3OF@JiOk;vqlUqeI3}I**%JCF8#0pj7ywCh zaZ)p1O>0XFQYU%eM+9jo5zC&a?j+9IBUJinSZ|@k5iuSC1|n@k1R}GLo|tOC08)bD zfcuLsGu_F+Cl;!en93#C{4w191KMGpO2Fp|A(Q!OX0{Ls$g*p=UGPY>F0UNR?mfS;5L!4 zkT3S+wgA_bur;z!rJkg|)`>~_4o8a=c@76gZ6Hd*iG5^_pq!91u1CZ|O!8bscDD55 z_(EaXb~B+V#f|Iej)H-K$?W8s2qPM9nt)Q@(YdHOfM$bwZ^iILV=|2X;jLWqYD{M=T#uwuRbE zencnSrD{8F@5s*9RtA`F)msS;pxLuj^u$|di`jmfxbKoQ_|NFI4P&jlU#L*4_2p&9 zO)JfsX+fIfB>dWkk#^t>j*yHT2!;uGJ{P8wQzxDwY_Zb$dH{LL?5jCL|QY} zx@ql|CylPZ=;%p?g$4>9o#{pZX4x(Tpw7+;ZUnTwEHLcR9Gh$Ar(4w$dW*iBKx`0rAt4_9^)*69di;*bBJ0SftCdCawoqKzP999NQTrwHM6?+-ZF=*~ zrHkCN=_O|P3?B*-j>p3Yl%;Fr`tjR;W8D4w{zPi}`T8JpfBqRS-#r)Mh#OVYHtgP0B;#mYw@s04C{d3kf`*~uuJ5T$ev+@<+QGHbgHZ;0+}Gm=`w>NLl&viu?gwN@MbF4b`&=A zgPsEg*;}Ubce|PNqkfbKpk~t^#~|VSA_Y$zkx3zgfL{X)+kjH#}wwLY6qRkSf5;5;i?CjZv9ol;9 zLaWOTGzHqwBNco>w6n~e-S0Um(Kwgq3b&t+JL;g0&rL?#i~uxS{fbVY4ve481qQ4CXs zintw-2qQQCVK>7=CE)Q5>E72Xcy*AY8xc`oqNS!u)FQx$A&5oOU`cD1=6H+1t;J3q zH&JEN*|cf)&yN>v;OqOt|6jD15!%Dgh6C0GT-U7DJY84ILDQBn(i|j!nfwSR zl3_I~>%Z5L!t3(T4}2zOBM{>~L76t;U{8=4aK=#0>k)5FOi$3DM5e}9|Lk4y6fUi~{zRqLw(Omw~+8nq0ssB`T24BY|$EiyOU+l!^h9? zdp`eMFTdoLhEre1nFiIh|%%*S4>sST_8}x%3&k<1d}+ z2kl4fTCmNfq*w|;URPLBVV|9NN0p|eoWAQ^1{PxU3-EPao4~Tb_I7622LY-gI9>@T z9EI77e#!Q-wKyP2SQG)^QprrbBb%lhbU6MfHj-@Ny^OAyknBOtCTkavuU9H)he~>H zkR;d4J|Yy95>^>v*osfB`i z7L4DuT8gB?H$-j#3~Vw-goG^k3EbAIv+0Pu<=GxwC@`&kN*Y)amJ~w}6@~W96EUy} z=W%@hpjz3B%VbfD;}i*bVneFQcE$ zbag)c_s4fOg#8@Hee1c6;QE}G!W?-DI(K5BtNDCSFY2|AB)C`{kQaNEVFS2^^prPU$PDdF(6GTUQWY~7MLiN#|191;d-X}5=M9(TUoF}T&2p-K^=LaTnf$hbHd~^@H zT2C<{YdFG?gE9+=e{!Vvf*S~E?7{8x#}p740CJP12fSTkA5BEo6sYAU5hc+a@7Ei} z%M7p0>EzUd%l_xk(v>(90X;c6i5$rY@9I~>a{*$F`N8(G zm)11wr9m)o$R&~e^RA^KZ411<+U?qC0f&Q1VxR!_7hZp|&ut+ud(?#21$^WxZ9Vr$ z+X{y?5I}VD;V-!oW;mn-Y2d2jo~6?lGdDZ?EdiL3swGS+50ckAl36^FxFk0T2*`M$ z_Yrb0<)#&nY|=27YuRmP_7Oy?tBv~nQX$2xWTdm;2M_p>nX^_auwB9&SH=EkLq2^v zl2DvZnNsUA_YLat&Pa&BW_ZqzP&t_pevddyK)i>m z)&lrX`{?WZ^YeNB`1V(wwn6u;^Ox(7H#huzebW*@9+S}H>eM9s)T@jXU5J>r4<3sf zMfP0;uuBX^hukLv62aNHD(2u$(ap+3pL9-getY(@tz*>7~AD z6B!AIm05yRKo_)#`q2u$;iZu%t{kGyP=WP>e-e=!ppuH>O^<}>_~JZ=pli6hi}6gC4~~mgsTo*;0!WP z)c6%ozPPW2+;o7`bfi&SjvrcE*d=88wk;&re#>)f3{FLWEJ|tKPh0>E zqpq;l*=Cb(1DyQ9N{66eH3K`_{U>3LRXR|GTQ~&EQQ@} z+gL~hN4TJNY(|}UqL3~>6M0#8cI>0)%`3CEze_|&obeFnz|mXqw#!dr*)HxwUEebA zNUIGhT|KfWHbjm@gG$^E_GNH*Dc`!)$ z^V8mONH-cW)naN`dbY74@ADJ~r*9L|4=pf)IH)}Q`j6Clr$_wEgmmm?E@@N&|NR-nrJ(h>4kMWXRlZnp{z2Vv@ zmG-0wsCi)Y1I?Mb*CJa3iYy$`XInzQP>K8j-bR{?XPjgI2KW2*4*%sYTUV4;T$3 zvAc~gJIT#ofHVDwFuXSid}dy_#xuPCZXp6j25zYpsA(^}B)*){reva^fQJse5L|;J zh_*x~+>#uuQ~P%}Sv8E$mTLzZfrcw23WeNf21A0F&8qQKzFKsVr{~id84OM{Ac79H z)h9##{%>Rg*ep6CGs(a$M-sLt^p+dRE|z48;MUc;Nu+sol6Hz6A*{ORk@_Pm&) zkAdn|n3Z?j4D!*(75+;Go_I&K&V znWIog;i>pvJM9Dn&DjCm4QT_B(4aYY-Tj1x#Wf|-`HO{=R>d4AYPP7m4b6g3k|_QZ z=T`%%9j|4B3Ug))I)dO}iO)|PAAHnK-o2v@e&CYj1WiKtg`mxioM~5p@-bz9i%1v? z)ftZW4=S`br1}lf`gX@gItnr>iP?5`@JJaq4w7(|_KD1v3SFI~z2}NI+X7{(71=*N zDqI_rT}k zsUII98?pl92xV$&-APB81<_whEYyqitT0O97u|@yF$Q>=`)44HWJhRfQ20R^C$r?J zDzcf{P|NGIBN9)9E*=ex(yo7d=sS$YH4rsrG$umoaPITQdXIS58N_SP8K=5D5LFBAdDr+e$o=Ct3l`CR&{55jIH2vsp+iR4c84 z)v1$>I2{d}ml8%|el&^VVNB{C&%`4L`%oPzBi5+01IuXX^EDON=xbfsJCwhvb!s+f-P8AgjW&Gp zkown+ALIAe>7)JqI`uFO|9<`@j!M`oR6!$XtV`NJ3AUr6xjCcVs4Q%RInmn&&l4+= zkY5Wi6~k=83nwO-c;Vp|MOmeEl0A{DJPZyxzc`TtjJ0w}AawC0%sdqj)S*n&S)a&I8QC%MFc z{@M0@rb9{tvkqc|vV~GTU_0x}j%EvGL<&H%iiC_9CE`k1i8f1gk;||pjB)iRP}OM? z5p6uavt`6MJLXZZeS}Za4FJ1-Z~|t1gO8Gxl-M1u4c+n>2mt@4I(|)y(A4)(}4c*Yfto%j6&Lm8*zEr zcJ??iOSn&BZbJHToWNmjCm6Q%r!6I^hMf&6DNT4ITWE%Az#!QclG2j>!9Z5v(ziLj zAsq*^f4#?bAplTEkMW}hHv{QJ!e#End6?VS>!*b%vbG|+PNqnnum{F#w*v@;+8fwE z?^OngnWh~ORJ0OyyI%L2DJ#d+peSZ8NID{QE=U!zt@ogkvJMeqh7|A^oKz6S#N_WT zBUMb6u|^D|jJbOF3$ZrSA(boC=}1I%Rt03q@Qd*0iQLwD`rT`Y$U5yhi5azKGSroB zTD47`$sabm)&u^)>!-ok(D3AqYle9I&`M#$hUBuHHKX#&Ko?Ve93H2M(|h!c@Az&6m-%oELY2xENpr_3T9kK>}*LVq*yL>DpvZZKw1OPLjIL~YvQO+ z6Q`fiAl2zhxsIX~<#0qZi>T=-189EI#%*10rOb*vTch3Z1)w>D`XN2X!vF@s+*XxF zOrFBE2!)yMACNd}_gPZ{DM5E?@Z`(_>d-ojXK00A5kY)NM(e%eEL zX72Du8~Hud@8|jF_ow-0LPbIj0O|YV>-ul}=(-)6~^BW@{`ccsPQAZfSP&u&C5h%&EgLZ6Bb<&;Blra4 z#|%12TP0~bF#*$VU3|ZfC+-#^e(wjkB|Sk0?L8lT^6gzg6Wr-ac$WTW86YLM+^ zN5a);zE9Mp9hSuBuQZdy@2!)SSV)T;sIfF#B2vD?kGMhSk-)QywyWFAeoMC3Lfyj@ zqjX3Y1{sv2J}djz1gtT-EDAj(*P3tBrvI9t|xjD=na(Cb;*-(2Z3*l5y%;3rV&&bFGx&)$J@&eeB?X2trmJ1t?NH;3%PNp z$F;*}9%nUA;vEGqFB-HP{3GJgJ1Rs*k##!lgdJ(K!JlqFE%p(cUfNL@Y6|GYPs2vi z>nBUQ^hhLI>Wa|D&{62;%s$dw(b)b!0^!vy#|#c%@Aynxu$Jn_*!T5BN&v53N;p1_ zSCSU0la=Uc)X7TxCM_s#NE8j0BhvV2oXlcCg>HAeL2jWsy?+a79f9nW(;wJBhBn-u zo#)Ig!i*F0^tm(0Y)F~}JY2XWK%*bukYLCxM|J78L+dgiii)MLXL?GQnwVwwHw^pI z{fknE5hYMrxF$RjM+Cx-x*yrEZ8{o2OQ++m;}#YviJwLk7b-5?+6rPbOqRGTs3qpa zO?wgK(S$G&_CZTXWIWv4?5PqNTipDPD4o`o81G&a=@_?rNHodBPpg&s!J%p*JVG94 zc}JdT(8!DFKW$xIn&d}TV*CWmWz?E^ zM|Zn=`da|b^i^~W{RJ9B3xbcab@xn{DX-={-rhe zeMp=VSuFr_$QhM{eNky_G^5!Nt0@v^4dD1;)Xx`vACJ8?Ly}<2Y1*dfYb=lkohW8` zWOg$}P4F2&{JF)fPZWKm=jmj7*%y7{h+K)-OZ3B>Im)ak`3RDLB3sAe$G;ktu5jiq zV4b(+Rc?Uib&iNxeWQ4xtiQga2>yMm0~{t&3D3WXJj{_o6MJ1q1u6JXfDu4#nG2HG zuWKg+#Fm9(Lxxi~3UI@IE3vbsT%>!}gB!wcm?J zk_E9pj5^o(h`zRFgepGdRT6k@`6Co0{oB%Mw~sWod213lG7wIDwOTC=n3`S{QO5OQ z0s_9J$PJWZ!e?Wd&gvFn4j}3~tl`{e*}5N-7PLWS z3K)DzMwemKJ0jf{s+85XSn%3=WU&`><-kS01&)XKgoLoM*(y1)qru(g0daGt=C({+xad zuJ+oEVMCqBkJ_F@JR)tGA-Rs28v=ZiaKNxlydwlHdZQ(W#3TXt!H2(+B_$t_SO^0R z0&4Fbd)asDQx3l*Frh%;@|tkF_s;0jK%l7$-h5V#B&uv^%+rF<*`^Vmi2fg0MZFUL zh{R8;b&J4P7#RgVYQ6h?eporWoiLYerV|kmq-O`Gzl9O)LoiutLhi?mlePX8*rB_q z+_d6R;2EzC;Z}aBiFZ`16Vd8B5K`P>AF;F9k^x>a+(=0Dx|iYC7W3-$Wk<8_9WpaH zmTR~}{Iz`|A5Sozeo#iGfh-T{1H<0}(_dcpH8j9TcO`UeL2E13{eeI-LUz{IQq)nk zH=i6)P6A$H3IX~G0UhkIwOkJla`?}-NXX!5$M+GTxjKPOst6p#3vD}Vp{frvtYqhn znHjLap4Z7C9Z^QpV_T-95h35|)#|NM=tGI$PJe);+R65EK?($kmraLR<_WFwiboBI zl@n<0lPLT`+zX_wFf{{S{g%)>^9xN@Otg*kwTz~$edr=u?bMcJL0St_Bf%gnz+|o@Hf|Vxw03jN0Pz3`_Mqo ziF?ghzD^F=*BU};9=aW2S=J31a7I^KAikc|J`;(SK7?Y?tmd_|-y4e|2wtChzS~W^ z5C^8~Nl|f$((m|LGijh>vNsUk23X!SvmTIQyVPUk*@zoR399T&%~>ny~nXgEL1CFQ#{sbMD?{Y zHj`ai#i3cfu%d&0MnLo|(4miHVzzE}$k^cH#56is+f6|1=+T^EM>8*5!N~j;Cqo7K zKp5?Qrw@D-s!>M&m)7IUx5jo_bQVW(8*^opMF`!mYw9rdO%R<0pQ0VZ)Y{ZVAH3oQQ z2f>M68=BFpwKq@1o?2SsnT1Sy%^f$|Pz>q@K1dWBpN83q>c}Sg2%#vk@(H_j-(q z)+bsL_Os@c;)|k5CPLncSmira5-_Ak z6FXbe@qrzKD*dg%bLC1$&6Y0=PArL!%+Ogs@AJd~(H_ki%2 zZi^Dd+1xmxVP^tHJuBGQTNtsA5ZEa*UpwCFnSBI^6-ew7{Qb1?8mL`Y`90Ga6~Fib znuTp&P4I`a>|17i7)6=S&v621!ROy))gCDIhP^#a25GI7S<9ilsRao9J@gWeA6lwI(9eq$_Og#Wk(-I%6T=Ysc48qS z;ZU@tF}^%eiwVcK6pl2Ju|fIO8D>tJ#OTk#g<5`E-2LykGKEx8odyCPH9(A^==MY^ zNKrfvt$oxd4()_Q&9Pchv;MSb`}6@NV(vt}?)X;=iTV@kg#UX-@Pd($_8H>I9h;jo z%sx7dC+hGQ@G5A(B=*rAe*tWSQ04yB%#3*u>I0fXC4L%^7Mf>ivMf><9Z01~woEef zj+}gO4+X&TMA5C`KLWFTr}2@YFz^U4TNOIC)|hu>Za`H>R~#cH@zZK05g`_P!^fr$ z5P-OWhCZy9L)(Bi&6#)PPeFA9M(sE@z;hFpFnP%gGXp(MJ_Y)4!TQ~Lrx=|L-Y$rz ziIzqPd-^{}%DoR6L0cQ3J;4$tUN-W)K)>I#!iYXV%tjP)3Bw6Avr2)Dm=;3s|0$uo zrGP4jsZ@&IpD?QpeestN&U*k9IFmZS0T9nl%cxNEg609`O=g2fn0w&(=hy86$es5q zJMtq)bWAHgx|xZea?=D1RD`5rm~sshW4D1Kr?ETUPvVIhiW8v~H;{Ic*4(Kp68O>w zyj?SfoUB)kQp{9JC&G2~mURJ8KLV5Xo_BkoWRs#}+ zeBAD}*hfSfX2NXxzKPH8Hq?E(qv+=6LJn=*nSnNLvgNsn$ z@rcH>@yXKgEL^()3qWP<%L`>dNdI2ygRgEmWsm2hXaLjK(+8xoG#vu}!WGWO`$E=dX#cR;d)l!=zgh<}M+p zL@)#^(^!=Z0W4de`f3o~aTAgEUpiU_YpUhwC?J0(0-JcKHEvM6HKG;(3X49;JDq@p z0gfYD=gQVRrVnp`Ri+R!HUb1}OoIoVV6RGIjG6_E4aWF3&R-Xr<{?uq1!G*0gr?k( z^!Yo4W>V(I_mSMnq&cR|Q?#K8p{r_q{Z@~K>`P)H+-`HRBF7&!ElPze(|2PLnOF#c z0kW>#v~O#Thk#Z9J@wdDIev+tF&z#OUuP|{#1m<;4aTB>KEGm}M-K@cNm61Vdt1n+ zf3!SN#O(P0tNZr>W0P2ju&1cHk5}c{A*<0`!#D7H{9dyH7;A|6uE9S#T0ou`8EOo< z61RxA+*~2){#=k0i-U7?hhffa$Vww0AB&)zYw}O87E=@(>1JC0Ox$Zs21wodv)ct3 z1qJ{5GtCdsoy6zAR7yqzW>lp&Vj;accXMeV#6CKK#E`=$?s~itWy;LL;{gu2y(0@T zz5uuho5su$ff)!l5W0CQw#Mi%7%cpF=rZ4qXa3WrQn_xS@B{nrhfUwnRqv5sp5tH8|5ewfHTF%Ry9)7>Hzm1x$Uo|Fg9^2y_Y?LzyIh$33$ zQujyj*Ol0_VUM_1yYJPH@n%B?p75vQX`m6>#LVj(m^{pOO4F4qycIu?i&zY|q9EeL{j zLVYQ-)~uGR3qCE2h{O}QqWg=<>?WoV6wgqZ*B6uJBw{u_9xodacZ)~rek0=^Qj0{; zIrEMlXTL+yrXG$&ix&du0fAUjdYsr3KaG6fh1z9FBH%{&!sGhr2GDpfTm?G5?wMDvs#ES+X=l9PUXsT zeej}J%Q^>{u4h}zjV00bv#WKAS{L>9j`Shv@I71ADJsnI7ORE&8za8bhF&Vji5r3) zuSJ#k{AvzEP`_~PL@eV~n56h^4H2W`Uk!|^PHPPIs0{>MU+>7}hpL&Bsd`q}Y@ zFouT0I*hn$EZMh`ZQzka7*VU$Dc4(B?G-GNC-`$jwW20CknCBkegdKq&#~zCvyaqqX`4$$vLQ# zjW}C|et@f6g!K98k8yIPFt4SV_-U$RP5GjM@HXvi>Wn{-C;ko(;5DOLrz3DlBoQ6W z!ZTuzk^d6dB4G!wu((_Mk?66x3!x}(<|1qN*cEr<2fHZ5@d!)2qkwt2XG@tSha|$v z5)zgKWiPc5&^WDzJVao}@j_z(*q- zk#1ySq5rXWZ^4pf>0Q`QbIT_ znR)W`G^*(;s;h~_wC#}Pj%Ljo!`#QldGkv%Q;!=tiASH-})~9@Bg1K%8&$ZlD=-*R&;zSAO)lk znBJWkX@z4m&`QS|<%4=44!ix>i`3hM?#20!rpv=y;}6w~9jGzLjS;sA;akFq2wLF2 z8li339NJaHxRrfOkW9}Fgj$Nts2|M*x#YCtK@BO^gkh8P)AJ1PaTrKV-CdycA7o~; zOo}yt25$LAg^{l`ldBzCp3qTDcq0hcy9u+@){x5&5gk|8k>W#dD$d299#x=(Gm5_# z_m0|VJrE$w8rJ{|MNxa0*+&}Z6bZD@9WKa-AbXu8A&iIPt#4Fzq`^!GaLaYobYkP)DX#lei{B5^nVxwRVrKMK>8OT`z=u$PCs1T~~G} zR1YLtyvmGWlc}9R`Zuqi@rU*ygo-ClpKrp{u2&4lI;7#MOoV!i-oDTZ0307BvVN1U z@<$VWp=|WA$;*b+kIVCtRwfX3{-c@x5(j)k5f9NLLL=cO?`{?-x_!K6F<0qdzO}KuLr;7VW zLieabpyETVq`{A%OJcD@=H5O3%mqn&CwRB{=#rR{=7<;wDrP8SX5D)U!zOo$`uMKJQ(U4;-?hI69!$tNh#2(> z1lrRk5TW5?k?n(uL|J4TWH#hdE8kGuSD${bzZIi5Mj>8=(s2VmjR=w=kB$vhsM-1G zL%MPuxyMtA0wHk-T^}7hB;l$~m0tgI|aWF9KwUXnQ|O?e-EQ7mq@i0?vnX((QiArX!gafxYlId9;to`Dns~HXSN_qg zt9VWJa*R?~s=j(_?Q?9BAo3$9vlbN_Bj#fCQ#zU?-zHKU=f8g56oj6|7rJKe?lmP56)qL2MK5t3DWoF=aRb#yBz89I z1Ui6ADbaF17v~Xe0cf}zh!h+D1Px89Dmw2fH=-$Dgr^cxuTfOU7YY+f(|?;P?TvdPbtcrSLdPk%@baZ`4h6Bs%YAXX6#6aoo*;RVnb9%|yI{sm#SfBY z|WqsH)JkSStKDSmTf+7q=vQp&vGg9B_<`$94W=52b z>lsDxxN!AH|9C17X<0#7r%iz+TB@LKZ^gwFPadeO~B=(Uw zSSQXgp(*LBYD0bz*Wjd=F#h_2^*k!49Nt>A=B@ZlD?oAt51F0a)s?;MDt`oN*3Feo z_L;55SY7YzxUw05;R>s=guwZ4&BY<39mcIXB5~H{BsoNWtVn{CBjhphG6ZMo@vKpA zF9(YvID!&~7N(A}HJoUKqB^<**VGdPLNc!;UlNZtSOs2n*fe-YbOaNP{6_!BI#CzV zZ`=*8R%I2278t)q*16Zf=?9=S?vTt83Fegr8Gt4hacBWXNQ()Q5W6PUNazareyr5E zQz}=&#by&^-kQuIaw8PX|It}9f<7`74|QBea!fg;APPd_X*;n3!}_d6HS55GI-=lP_~AIpP!`{Pk?Zb=9(k zLIDZuOr}-XX&0{1fT>^q1p^5mVf^gnXsMc(WPFA}hMOehnge5x=Faw5PE6hmJH&J%vv)egZq4>Taw^6^f zut#oKl#v>0;Oi3mh%AU5+1w&ZVl9E*qFL3V0Aw7zS{_>jIeG}fwj_cYnVNYBAQtsrz2mp_!4LjjW1+3>acBQC{U%0riCh?IcQ%_%qcG^oQH_=7FZKerk1M2X-dY%cWYsX+ zF7A2%d}lObHMjh#p?8Qwi!K;*c8_9t(vXj<8k&gM@Y%25KvH8! zX6o7z-jN{m^2LIKJj^`WSdfje{Mjy_)tE{N<|39MF{ANhq_W>|`bL=^Mt82fhD19- zp(?~*!{bO{)@#JDL8C3z!}*g5Fgz>sn|2@ z9socS)m1z?)APNN;7}U^4>bFzN@;K@bfXAS%gpOvA}Gav+eAlIF_VK5u}-Gx)YenR z8uB~q%&4~(FrsvipMjLK+#St6;^w$%A;?$Kfr;{}N@`bTWG!K!gL-`^`NW3w?se3X za~LMrVdZ+d@gWU_;tuy=y^o><9j(jdiYJ;0A&AHfBrnC;b-jPgLe4-~A(?<~&X3nZ zjb^CvQG#q<9TARJtAWMnNo18y@Mg(R{0F*;a%VX0Ef%8@y%Q6v)8b~v>bV}3Z z6FVEnmzuwAk+~rI3rtW@y^VX!&qkkB!v5+X%-sD3k_4M!UOYMxxW=Y|^+gbyxJCLL z_P{MR`zSnr$%Ddm5c(B~*WZydy5J6V<3kGb(e9xssb4?iA5jfu*@)cEN7e=Cup1u| zOr(>Xzw}2pART?{u4jxBUVC+D<$x$)71RpEkckYtj9f=vE$=}{J5fA3#_0UkOM)VM{`1t14$rok5<(D5-E)ZQ9})e+}guH5%_7;8}uL#LliDCBldBv08&TU z;rgL>f{I|f%T>bEDjM@VvE3YUwFjiHXem%B{XxR>A23ChRuw#|Sqv}VLCS^8jJqa* zhL$*jdjy7T-Worf%j_Zn^}-X9SR)r%F+H;ARgZ=aMT`9`nsibFb&}B3;1zws$9g}c zpQg-0a5@S3nuO=CQ*43_y1{2hEgNh|8w_?9B(3K^8one5VOi+A4LK!Kf?i89Fu{F@ zj?4eN;j0y{rsGNFeP^5)sk-} zB{_N4VYk*|h?S15gj4~aT66hYcI@D=$Ib?)&2vK=NR*G5VR&$~I27uw6GVR++GX() z`$!_)h4gHlR_R%9G!GSVWnv%z4Iqspw2BW2qdipKqIi=i`7GeN>4GpNhn-D2eT73) zpOuyrbODJOsqjS4OB0Jk0%5ueS>NFZ=r1u)N7-WvH63|u^f5KV2-)k?UjcOWk7KO^ z^iEty+Lk3FW=Ej6x3-Q8VeNukCnPT@np0e1jvCOQn6?#}O!L6AZ^l3hHu( zScG8K3=u1=Pm!9@lyMa0)r%cYga}@(zbS$O_y-BFW3 zpce@;g+-y!qo+T#ZiT_mCKA*=3T~r8BeO~_8Uw(sjDr8uxk{e2=oS)ddOLAh-nuIUj(63Q86njZ5wNg?kJtz5oE~Q%*@Zqu|2J zz8<_L#cD>&g+emoV-(s63#m>l(AJ_O7pnIl>iw-CF;Ir6fYsOPOhm9UtR(A^&B)gc z5*`~sz0?TZi~6luBk0Vb^6i|lO_&jhFs+%(K(kbjW=52+16)C4Aag-d5sM~ISg#}6 zCkT<217H*^8;O0?m3yqufT2Am5Gu3}h6re6X_rMJ6A_7Qh?)j63|QIn?$_H-{t3|h znHnTg7L<5wQkj`i5yTKoc74-^sg;f*zFO zFGohgsHs6l6~a?GD}d3NLmR4sdSBdYhdM2rjcEk+B##ZD()s&{@@s#oeYJrNas676 zDQNN`;VSTiL9L)w5bcOVJgHZPBQ7x`9vbglD13M{Z;hWr4A2t`+A{+|AZa;nUcbeL zs>U_IplGvB48(a6X*CJW2x_K55zJ;XoC#-itanB4%(1B=BFIHQ;L)!b`^>WjzY>sI!xy?^#k~v zpexhV)I-Lac>QHpp(sLz5oB-EFc3}zM{q2oMay@ZxM?F(8`Zuhr6CXL(E!;8OF}4# zdrgU;8*hXml)Vz+*T$cLU1G5+LdpZcHnESuar!e*vl=oS!7%|I&iaVqOxPnx^^zac zMA;G4Obe_fb4evgki?AUx}b-%CdC?1=;%a|S1OT>CJdYAASfu1!+MRpHEsnGJxY#) z1Rio>piVHOAhjK3{Rp*p+Zr@Wi;DpqJv`0=x`2zko63^e7$L~{<%%Cg|ZjvI$PT0BcV!oi>qOptgq z3_g^No@LNQVptN>8wkA;L5&r1)WqEWIJUpJL!8`q8n`MQP0Gm$GlC)v zU9NFzGs4k=7}ejq03BFl23i*Kpu{ce8MVe};mgG8N8hl<*09+t@I!BowqSy(IiKpagI?of-nL&#NeU^m{2l3i(~pl&y0))}A!FDiSi14p$Uy{qXJcxitf0;i7IOWOha_&1kVjUgYaFfi z02E6tp?R!}MAlEc4%!+SUr?09ThqR{c4;^G>?qSmUcyaoA0%>Y)9G4#uYa=MM=nMh z*QydI5Lkr7j5vNQWRuUXJb#+oDDeoECz_W5syp)7rd?e}NFjm|vF0Gu+N2eY1@d>u4R8d@F_&zN>* z`rGcP^IyL>RG*TZZ#^T$N39uX_@^tLZDMB|Xo2er81rD*Fg+kJ4eUQDuS^Nrf$het zr6SyoSF0tAJz{C^9!)-r>MFd~DnSp*3z?0zQ1M}pYt(k2MOi@lHTi6Qwsl`DWdfCN z5<8np4JQw#|1O{1N?CBBBu2hH)Hka~Yo8jMje`CHn5iB})(;o+Gz!3k2NH%YKy9nv2Xr1`kQH z5oSDaY=^TJGaA6itj3W)N}T?AS3V>lZZ|$8I2Le?%$zPiMW5> zm4C$i1g`JmCU_cq9UWRglfe+wv)M-x)6O*IK}73@N`w&euH2e=xGw^H5a=e%S{HHr zG;E3e6Q|$dSiwX!w}{gZ4$(tqMO`kFJqFGUZc?D!L+vQSb#*nrAsWq;S9MgPo5EaD zpS2iBN|eb&_Bl*E8a5BiPAVAnKu*8HXo(Ha4;Um4t%0Vh&BC^0<+v43gxC?NlM+l5 z_}NEH3im-0cezZMTD>{lm|CC(O^2FuU2f9PoGX=MVkasS=)w}k%!R}Z=`J37b@#`; z2I7;v~VVHOo=j)Rj+r&6g#3KAh#5GsS&-25poY~nDNWG|B-@P?FT0FK8DViki zevzNUrF2;jWJX$-S7bDUI4+U%=V(*AwgYksxtpOB9e1qG;^L*C9k? z0imODL-#WK2o>8f1P*G9meVf|SV|vboyveKAcP?MW=*9gikZ0QG-tUY;rGrz{ZuKs zxgb^Px=Cq(V{~(`MdK6mIH*g|>Nb+BP>~jGu*mdgg=!K@J!)J(rdA!9o9s>EuShKR z5MN@5q}9{~UOz@Gut7e6R-42?>K{E+^(;Va$RNOJ=Z&DhnFScSPK||O!!k4I7a~HU z4v857Vri&uQmk>Wfe`7=nd{WdG-Smoj{akJkb}Dtb4cS$gwd|_i4-C_dJz^cZF(c8VinFk3JJa2#19Cv$X-JND zm3aLI(#bnMHZVm5xsr1cd~?APuOH*JCXYF<@o+&pw5sCJ&g`pZvkTcXX;`(tySgLPA1;|OCX(|2A& zt0IIL3i!#&!fqujq(lfT&9N#P#$RK8DESc;PY6u#%3#FMuQknrF_2zb&~4APh1 z&$Ggqb+?a1S%?NPVp3&0e`l|$gx-*7w%x?0De$T`}T^Tm4q}^m9BG-qd zvcYFZe?JA+k>of?WM;9~U76ao7;j$DidBm}A0oS^N*~&3y1x?k$UwAQP!2V(e*kzS z79Fml;+sIRu7^ln5#ZH^ttzn0HD)LS3NA9QpH9gPsQ;N9Td?QzBZC<}^JpVTHaAZb z91Hx!lZqRn_>2Tks3sB4!Od}4qxO{=K|b{qEROXN5kV@65=;j{0+vY}5#j<%XTOPQ zTjP%i|L9@RS5?o*P1}yM&Od_@^Uv!w!k?|#FWA`Rf-{Y90X7Opc3IP3G5+mXtJYoR zMik>z=0HyBHBtaZ5rh}K2icmkkRADuOTKz1u3ro^R605WP@;iJ3u>uXpMui)aeoZd ziK#VEH_j;5jBcFKrA)mlv~u0u5mA?-@Pn?HXLKtMBI}{Hh4@m$` zSrP{lQH{imI&!p@It4ok{0sz2$Ej@szsUKkx0TV|50#vFG>JmqK6;mfH^O6c8z{P{ zY7~hweXJ>rF{ik$)Py~9JJRNWB~6f(8wmZTdKmqG%1ZXhi13fWCcL(YFLqYRa#CYfM9#gy7jxGs6z8@mXb z%qaM}zm25saha5>^D_|!awZDsk=lopx!3)?Brjl7B=V33B9Ow_28cXXA)xJCA_Zlr z^UGoennQdb8?J8=kG5QDADYLYrh~*Zm?<&XD68aK6~Qe&(%p<<@`<^kEH-asT07bN#M>Y!aj#AigXs zs?pHzsDT4Wd2ruBk|=7Qzq5nGm2e8mv_p4BWRF!776`{=>=HLE9S`@94%Ygt$wW}h zqjh6HEECmrQX)1ovQ0c`FF`w}Qmx+BO<=l;h3`KGL0>Z3O)#HaD3On;+=6)-E zBk^dsz*zt0&{iNFEQX=%v&KV_7zjHoY>!6sYDP1E!lANKOO|<`C0sxF8I9`L z9}q|HK5FO5MH~wWA51X{liCsOJ~OgZD~fn7Y-Sg$(j)UWzW}M7xlIItq@7(RCBk zO8Iu<`W4h^RWqBiSX|ky1l*$qx*IbBc?M2Hy{mp}ab=?{g&_cONJw5R4FvWGNqZP5 z%t+8zl28X{O9)FE2nRekep5Vra65i9Mt0P8KkQk^`5Y-s8f@cV4v@INVS(JF!JXYOv97n`Wg6c0| zqm%PdtW*K!UK$uBcvpa!i{SGD^IpqL%1|aAtt-zT)W{w@*4H96M$yrm5Bkw{X0fH^ z3FN^<9OC|-OO_8QSRM~iGbA3Zs}KShUy4tHuilW32u>#Xy5R(IMP_F&#Xxg{REL2o z<`xrWnJ~4| z)_}b@BI~ynhK&hGt1zHU({w^iqK>jFQya?Xdh<%UV2Q=X1FT#9Sn&1MJg&Y|m59|c zvyZx}Lck0J9PapN!COP?S_kx?u*(yVhMYz_Uqd$&eUK!xv1#aDgxolPpdelDqFqS( z12H)2?i%SE#=jLn*FYWlM}RLyaYr*7r(dV&2$hQ4H^F~I%DP8(75CB>Lry%})XswuY0g^Yv#~rb#gN#;&WADTEFQ@D=`*WM zEhLM(b`H!Ge8ri4)RkeAx|W=kjR-)+C21c#Is<8FEER{iKb#1nCzNW0?W;sgTP}-= z!`r_Tt`B9TVxT@cgC2J7M5?|N;DanQuV0SLjY(7#P6{q2@z(GXi?ck)ovFp>X+l_U z@u;7_kEo{U%?Ot914<*3Ea=ObaU@Y0djyZwO=@h?RYnUDG`td5>##;1(k(*7SxfVZ z1ae}Hz*Ke}0v`B*L%5lVNawCNnKgrRIJ(@tW z@%tK@*~+b%OGO3)noBCNMqNeOeA;ddn|>NcLJM}?E)>}sO_{{1S0a>z&Bj;>#=Mea zul7-d>ya9T+myF06R9zDuvFY+>+ftf;0Y34k-v_}^B0?XFoPt147wxzw9XppT;URu zKAH-Nfl37Ga%_$CE++y!OP-b+L7gWvP;?sfn%YsdM%)MHu7KN0_>fb9pWObk!5({S zRKnAW zQcTPm`AUG{;4G}-wMawH73#5KL{U0nv!TUxs@Z^tfesoh1Dqe@>56jg`D>)>o%e_8 zzCI#xK|=b4ox|;v8mJ?0WL7xr*x{QQ*^ok|mr%zB+gUC9uA?L`U zOK!sfRvqCwWF8A57Fo;_7E(RbvIafv0a$6VNvBZK)YIUsBxclA1c?~7n;aXcva5}-SEi+EUijev`As4! zm``O(sVCsU$RNfe=i7od+GD)bTT~U!`Ba0K7#n)8^q{1+K%i4oP2zZ3RO{wGQHHp4dm2fl*CXVwY9C z#*BDpA;>tU$C-#}2_4Ju-D!%;XhkEn5XlXf+1ZP(QW{)EFum2~sd^V0L4nb#akOBn z5CuT^!~aY&2?Xp{w&NX0$6G@?JRa)sfk02(YYYEoML@psMq(g*H8RlB^$D4x$X-A) znMA#h!uSuwN1EzJHq`k?1ZN%IgdnauY)C$&!iYEm8FMN%ro|9k9DzZ7Xl+QTTHP7K z%6@8Ri>Fma9g&0fs(4L>yd2~9Vr~wA;zed3EwHkX zOQ?5tl|W!M$8II0?wQwbQ$uz(q#Tim;#SHVYs%MWmpHVhxpQG7>mJB0A!*5w;0s+e2d9p;ePW%E8ys)p-7+=~Dn3n;aM0qqS>b zlG|U<7M19!+85*X>w(es+4t36A`yi~t%!iHv5=04f3J-V4aFjZ8P$g8nDmc*kSwH- z!r6nZ!|Rw>Be?T{e+5s~WVDbM)2tb5Ma&fHRD2zN2@R)w7Nh^jmcs)8?)NVzEb(5m7<+X*wmUDT*Z zOXP6+sZ00dqU7xX?rhA&qm8+cN^^}{BVKdw`pQTom+7cer2|)s<8WwSRH(xZ7EOPU z8ktUU&<-t-0cAagS=b)TCpz>}i`|tEsd*MG$1&AdtI7)JtECAZszU9`WsoWng;LiMs%Dh1eqs;l$epM^-z~V13q>4)8BB(YWJ}(GXs?a8H9DSMr%R3 z4$C33*QJOpF_4xjV^j#eMD|!;Ikz*~ zg$~#n7vvlX0xbt#(kwG0>e%vfjbV$lk95X@3K~iA1RLXW9hs=0_$aD1;;vz-c0r<8 zh^EHGTca-!EwoJ@GD^@Z&askfaF?AukoD{)7BLVay;3Ax``O|TOaUcuh1${d{Cmwn zt{)}Q1~tYWNg6}5$hwXajc_6&&YF9*vDp=76w%jmFh(47e#Or~a>tlAd6KcSgHr=> zu9~qTI}atX*s{)OLMSa)u}B=+nWj90sU0LxW5O$!_Uf+Q@6l#aG!B~C@hG#{9aXbd zE?UWF|uFxZPA*vK3aAD3c1l9g+NvLz-m z{|coE!)8X&hmZOAutu#*hhz%6$sZZW1ql;V``OgW5LG&FNIgsBHHn=)8I?f74IVO* z+G>I*N|mWL_H<=i1SVFbio#kGd%0fkaW%uGT=xai}?JuQtdOM8-j0q8%wA!K3}wksbGrTpEnO&}il+=eH%eSPevfk=9m? ztaF8sUm(dMH!gvjStH0?oDlOG=RZe(FNYLwEmX_S&qG2CayrGL0XL1bkM)7H)Xg59 zb!BQs!aE+i<*o(-(m}$?(OF6RCKyl{#*Tc*Xr-h|cYOU}{Ly={Yq8(Y4@K4Wh19l9 zV`{@;*D9o&if~y%R2ujIIGbx*!}DAB92-blR(`!nAM#D*)(4~_@Q-FrKYCk#3V?WB@e8;I(6$Zl>P&#W6s&cbU>9o=QSScbY*gSq9bt*h zEVlZW)&SX&Dujq^4MyE@aG)@G2{WREjwb>jtA70vX4&3pG6k|d1ArK+iS-2;xDjn< z+G1JSlo?q`T!9TY#$Vb;t(QP3HE#IR^DjuyF3~U)RXrnbZ6#X=8X=9;&zO;}qHML* zDb_p}bfqm;y|p$Pb*r#NUEm=N)RF54h}TV&ZOT(Oe0*4BY%{%&W_HRq^kDrqLPSLtj;NBEIz z3{q1cyPi*yQzhD;#A26K&8Y!b1W2bsp7Sf`3LS}orlsyY{d0#$t2$W9AZ3FX&EO$5 zzADmVEo>6wOl`EGL^~_LEn!9kX5GYVLvGb_64&doDmoA3skkD_2Z=Ru6=^V(S2+|9 z91#zd*Qg7w{Goo{<6;x2&}IV`RnBFkGH0!$A{54yj(kYq1B}w6BgeQ*gsQEgmV7ed z+VdB?=m*@02YGmFQ5IX`D&&6`b2)(2K+aT9_*M=($*kyI!5%Px(wLuLJXo0#2_W!; zy2r1|XtmH5!)@-WcSi6QMNbXBQ>ddJ&O&JyByWjF>&il+r5=??@0+v_n~fe6*T*Qn zky-4n(ub&t!4=h>>O&iGf15r?un|~a4C4(-gP*N;R1Fw`1#DDeu}41XlgasE7l1gRYwvtl0IZM==mJ{Y#A-dHDP5W0u!{3R0`w@ zro8-mM!Z^RSRSC1i5g;JMqPQeSW~)jMiB(6UGVhlOauU$oRnP|opuTB41%4mBO9Qp z)P(16Oh;rwN;h#qhV!@}Mjrx`{1O^0>}8Y=b;1rZxmu_?;3wJf!&{qj-xTYZe*HSL zOr-`|ls~#6-hpa9V6YMah^KAPHRy4ob~NhB)Z$}6bUF^I!ASdXjCK8akC1G#B#FC! z)Rk8|0yd;C6~U)9Que1d5yDPG=`5Vc=N}Ob6$>rCvV|VZg$7N>>0ix^k5xNyi%6+< zW3%xv%knzyhkX>d+OXEr^`XF=Ftr9+t@J|FN2JP)gv=pZw-{#pY%T|D5AIbHadX&V z{ai=PE!v;NVvnF0bwa6JSKw2UGEr_D;^_fQNf@@#sH5vhFQ>dHio!cnyIAmNvGGFc zW*%*fY7*fUqJk+)^^D{p6^>;AZOdgsIt3TV3edp)TqmLo#9f;c z6_hPaXo|0NWg)+e2BzxtN0{Ym3TzF6rD0}`aJ$f#S&v{>oVE!3TH&)-dP^-vbY>o| zY{oy}9*`rWS$~~4BB34L1+3bPv_ggBS$9J5Mp|*zuTWohlN%XJ_n~!b`XCW{F156+ z)F9^TGUFfV#Oo#&8H+m0=}2AV0ITNuVfVl}1zf9uf4&(RcV%kFim}=}nsP><#%9Gh z)DvU^6XbWRaaU!rF;fqwL4@U)s|AGA-K&jE8ZWU%O1yZ|8#4mE4D%lfFY;q(z7qqH zv+u)bG_`E|6Mr#~6l`s#H_N5AlvQCk4u>`pn)C4j01W%^Frg}}H7K-mj?%kvL88#b zD3dU!e_!bC1!;0z62e) zI1Q~WyBmjntfy-g0+6+-4Fm#iR^=_voB&qJ5soC&Lz(Ds`VF+^P;zfaS}GH74K%Eq zB#VJC42&9(PWxK6EClRJ&|p;8Q8-#U7nJUMJvUt<%0}#=B1_S@UhGIi&z4#?yntRN zsKHo*RTA@JJ&=fougtH3Jye`Je?yWenoSCk%weFJjE_+WEm~<%w_|W0tOI zuS#w$vNbD`19s>dfEjipGmvU#vFeX&syXY^uIUu6PoW;pu)(nfZ;dus91m(W34u~A z_#N47+_@N>0S;=L-P-fl$k~ml<<#1T!$4#}COne*(cJxvB$}~tsMIDdNG*n|1^(`5kSvNvoKfuj z@_&g(n+PD}uV=8{7@1VEWCFmnFrqE-)+GkgNlOp1p}h|ONI#07_qYtb=n9;m3xsg>Gnz}3*xti&>gxu>LVh5 zB&Z!@`y)J%acf}BLbRy?4a#RLMDTMZ?7`9!rj~tn%ih)3T)OkQs;|z-xnkY6dkxu5+E@lZ;Vfjx@c!M>a?5uS51-QUVW0^7#C*6$5j(5aB-W_Vg`)u4SV&h0S{~YNkr>P5M)ZnJEH;{uYD0#@ z9(0km60)CZ!a{;|=obt*hr>W}wXKLtRx_z5q8CRR{z$TnNfp?Z2qZFVG(&H!Y|x;6 zOc3(4@GcRMt`e9CnKV064*Bk_wIX*}&HP--$W0A0`uo=s;-xZeScJsu@5=a7CP%ud zrd99kIJD~ad;w%J36XmZ=N;W57+m{f8UOl%w5^1cD=eym0am1hTfmyR5f@-wa4ZQ_Ni8(~Cx!9A;EY9-!Be{H}f42dD-_7me4Yr+rZ4H)nchk1RvB6b!W3ze2-NYgiP%7D_Fi$6=7F2buIIw*Y zVUL(oVvR-+j5-FJoWD#2Ul8fBY~6C6h_V;lEO%|A!Gb=O)YWJ>(sPa!Ghu4AiE5g^ z(}R@Ja$GgkE7a19NIY7oO3p?yWQVj5|0pzcp<)p+Cj{L6_>Std%uhG@BTZp|=A$Mz z5*@xXG~op20KgOaYtX?os6|u_(3_vVUT|q))Ox#*B0oLM*zvAcL>e zeKcuYKlvkNI%U5eP<+PqTi_<2&_}0FUELxU8-{FYOxcghN zF=B)1sJg!BDmoGsA<;V~#v{cl!!h9!YDHQkK+NbfySX5vRD_l|=+c{1{Q|gQ$%$iO z5>fWDt9uPSWl0b4SpsD?tp}s04xJ&K!04-xrY{%bC$4OLaaw-WAIXEzWIX?@L1(ku zgbnXPh<5$`%dYH^rp#{q>;U_8Vn#SHtagR)s0^D-f3UC*LXZ==*7=Lgz4FhZ%vTvU zK2~&}NO2%go+L?Jte7Z%TLJ$j zW`saBXdIiHHrgrlLd{@qJYr_Cskh5uA9J<*BekO1Z0n#2O=u_84s&E}{bQNTJ2_o*57MyNyZ1fE%tx5XVrq<5J zc21KI6+bGh^Y@YX6{?ePS3QuM7H~vgjgAzp7MX#FlIVhaChg;{M6H38LDDQSkREuZ za4{Jjq9Yq}jbe@_^jgL~;jtmWph_^PqQ7AGFc9cB4J7#}jV*~85zHEaA=C_UGv@tx;nl!qh9FTT%m&+kro- z=x>1QX>U*|!WxA^9aJyzXk8sz82@grY%aAvqk$8D2qE~{mTW-r%%b%YH?4$&!i6S= zy5!@mP33_KiC2UQ=iefzSfO#3$e`=D77--UxS$*7H=RGD$nB$G50Z&P!Ph{@M`WgO zS7f+-3{(bXUPCp9J_(3ST|#O+#5B)-{V@<+2dvTEhHQn2Fc8j5?K};x)}yuJhh*2t zV@n~|TF`;>Z$&kS_jZk`1S`zp(IQ-Ti|asEOGNLKSRcX3nbyS3` zy^;2ZRt6JD_gG&GuNF3cJf{e+@|##AAZ+FDb(4p*v(=8N0qXCC(aQ`J(jDvL>GxWs5)<|Y5#DN*kgyw;P z;t|MgUVKC#A|=6(9$VD=^;x(9xY`N6TD=_fQE?PCTw~lC*cxPyQ;GM)mA#HF4Qj$p zA8rw51f?cva``AtZ0EmzmF7-dKm4|sp$|pyDmZ2#5e0zTfuWGQP0T2`7}knccLH1$ zYs^~m2q{`-AaD_B3J%*J64!$titiN zIwX`%v5fNlb;8v*Z8+=*6^J;>YRGJye~V!7g3Hk7Tkj*MKPnljVuP_B^29(&gjff( zbyVPrSZqveO3>~|Xx7X?ZnY^Y!1{ z2*_>UKi+HH8j@_-3-m7#1WoLtg>)l6mS#qA)^K?TPEM<5!qEad4rE7Dg{Z2c=&t1f zwWB2@!6f~}>V?$r-OlDzjws!W))P8@&PPXHt!!#!3#zyNaNf`~&_k8Y4q~?{5!D_Ul&N}YVn$uLHN#Rs zMcT6ej6@;aKT)n;iF!OSqlt~oJT{3aW(4>b@LW=HI?OD##$ensD%xH?mxw$KyigBJ zqR%8{W~98QXQ_!rM33?Sp?FhjAzPi_z(ne#BYPxCs5SQ}cNOw`Vo{6(d~J`HSnQ6V z=hCS?yn;%a_33AiKo9YT;d>hizh*qevHKd2YSO9zR z&ZghM`OrpnC}!G6(d0+d0Zl*xqougpQDU5f*UbgVTj<7N2YP3)`J;!$t|CZ`BW?o7 zqy&==vP3)uqP*s6A)hrYS_PR^@wu_D`sT7w;uuFzl+ZKHYQ{9n0 z0?zIx#}=$IOFU6ca2$^o4cRoAv^6BIqfu9eZKTo!wtjSL%!r_LPn(A(wghkh)<{|e z7jW$#Rrn(vhci0Ywn4%h8MzJDn0)FrimK>ZwvUuoP_ZV82k-`JAjcU+XQtpidb0!) z!_+|J9K==6+-o*vp4Jfp0=`2jBq)J`OG6|{#cPVz83R?3aQ)=3fHtc^Wy3@f)urbOMOoKb&C{N5iCYd1#WfZXO^PN#zH{JM|PDX@NBHRP6zqimOuZ|^aNJ^P_x)^uW^rs zeTyLoaAF`;)D>8sd`)E`BM~*Wk?3fK;$dc>jx1#K-*-}`H_%#DUVZI|t&w95LEbSj zro<7!ovkE_3Anx>t-9khs^zU71=^Vu8tREM%34YD3C_Gs)d7d)bIfxw4gi znaLio&oHyIyYgx&EOz77YIt!vWqME@*2ub65WTg3jkp#EgOAFrbYn9#vjIpg-vcGLI~dAuT{M++9Jn|lrSq2`Q`+KbN2{*)04COk0(;7Edv5%y@UH*0yPc+xgEf_xE}f)a53 z`s1dJEo5qdLy4Q#$Gf?Xq_exZj!Ir$@UF8PP12XdwNlAof{^Ka5c68nx(^T8ka9$r z1*9=EOBL6I$o+i-1-m={v?tw>*^tlAXijB1x_(hdCF->IJQ^;RIRmc$CzBexXCSr1 zrKT2H_5|tueQUG?^BJih;4`}H(d-3Sn?k2W1~+ogqhZxWqlzQWi#y+kyl)^f*Hl#T z1!U5P_YI`W4wOaWr$}T*?-^(f3Nxe*9}1`!J(|drzy$s|*tYfqxz{GJBtm0(rG(7& zbNeU;(hVibDSLRGFPf2>MC5Kp`s)*Z_UU4q5h%l4DQlox?6NglEXHW{B+Q4GY)CWG zW2ffo5|9qKV2wch)Dy~RLZ-pmejttPNCLGGpm>tiY(J3N6W~a(D8M!;)V3ce!gahJ zgE<)-S%r%Rg7L(Qq8Xix!9@en_0gO;^PYk3PA{2MJ%?LEw%&; zQcb)SvIrMF8tO(%q&Bn=T{I9F zVX;zm)iQFy>Gx=|?h|}m8hpoz8szCM5(6O!3Z3ylled2~(WBlhQYr)#AY(2VDB?yA zn>Hd;*ca?0>BEWmx`!2B$aKM5i-Car#>7TvLY+$Y-RlW7rlPfbuJcOBpON=m|8Q!ht%(B^_)v1U-Z@j5r@WV zv=}CMM(qXQ9d+7jW$Z6Fv@sBpCT>>QhfcU)MtbtGd+4!L)XL=iPhWrZ ziYoQaq5?fisKGw>`ePuT2(2PO`wU3uo*7NmZNUkk5E5#z_svLmCk-$2xhSX!;J}3D zGBZ*rm$!dwF%S^xLKb=;lDKFfILnGjMKp53J~E?uU-k%`07KXXp=b;wc|1eT3l^KG z_uupSEe|`ZsdZdl5cru9xy;^liC#^*_M*jJfDz8&8kZSei2F9=3Njn9Be9qS(d2yt zg)F$+QD62Y=|fo91sH+sI_kP<`<)#ioylC#KQAN^6Y?^rj}`;LdGoaJD`ixL?;A)) zTpd$&yuWH6ipGokU~AwCZz?Ne>7|E*glk6Yk^a zvbi9o5i@c>QgEI|aC_NeLn1~?Gv$dz`!FkJw9?}PGNzZM-#+UX197tGc!m>^VD`Fi zLjtj3!wXKd43g!p)R2M=HK+E*KWf>ktr`$Q3nRe}``n|&K%{ zW}s=Qdj$d{II-IA>=EF$rc9MFlK0Aky-nAEe4`{jg$k(s>nd} z?%9wlji^B}JR&u4(HfCI(U&pQv$JTQ8;M7&2uIdMe+*@D*^ELCTC2x$ctNEmEUbDz z)aF2A<|Q+VfmU<6Rdg9SO6?!bKyZ-y>-lZMnu`Wn!&ajsX`JuwXumEUNobn`uvE*eV>(VKTrs96HFeVbBC0^ zWFSOSnxJR`1;NV&#vk*QhJU0*_-ldv+CLgI>ke7T`9o?hF4);YUEsy*`<5O{NXtEU zWye5bzM+oOsk-O|kEX$@#KlTpMC;cDj+Pmz_Y-y3EGYeR(Tvt1xh~NjKJsM)acct- zMe6c`w-x|hJda2yw0>TY7_W#XqvvO)J8yMCF*62oj|@0}=tDBSqNf*R45aT=19S)s zwNKVnGnz`%0qUt}Up5ddqE=7M`Fb1LzkZK~^$x66m@P)WY@j(Ts!K8m8`=*vqoo3# zHR<6!_3H%#Y3y7siDjc1W#rlqL}ZSrkfz#xUjxep7bGTD{E(Ev?WFz%?lE9A{1Tfq z9L}_zwx5y88YVO?7Pv!*vb8_`!3#X?6_Fg(pL0R&D4qwRmYmN71;Pd8j2Uts%{fC_ zS_4f@?Nb`G?}m~Gy1WoiOfGP=g1Ug$M={;&CtuilL&iXBjVMtOMOoNB%tEKzr{9eK zLaWyBf|Q1$XnzXiDPj|ktlHnSF%YZ=mT~0JZj<(FWFX?*psD0F$NZ3D>&;Zgw zVm!WJ9~p=M2AF*~Y!^h?!J#SbNl>TJE}(MW{!mbeS1weKd)QZ6vwZTm&gn9cj>MqJ#zgb2rTZ>=@A&&i>k>+jMFG1rA|b7%P2P6wJ6I$_2n6MK3;FMysC z^tI3eqlIE*MyTYuHNwG3PO%03!=e})@>?; z05D(73BuNk9k6P2^@CE&z<88dBfPglt0S#r(D9)l5Q_Ft2xLsH_(fFR5Kd?s996`( z+UoJKCkB!z>V_jqOx6v_v=)qR8YsKLhR z=VBEyCvdn@vqy@ssMPR`hH^e=q7rFn z0Y`8GYVc)Fu*Ben<*X}a(~HpAns~IXs0%_Ts}t&iiNj8h^eE>LEHUsNWvPlFoJc%c zsV!GyMU0af_E>o=Vr=4qzG_nv{+%ic^3F;Zb~zu4Lr)w7^RmK1n1{UWC`G7vB%+^7!56er8+Kyg79 zk~OY_`J$IdjGqp~Kayb|YpU&!r6b#~Q4E9tgTh#kmeU3M$UxC3L}5UkE>k(a=h4gr zt=qhGS0?=IdjIGy&3t*1sY69J08yj}*LfBI7IewwF})d5Q{L=Qa4%P=Wf47FhhzNs6m%Q1-3-m=X6E}!l8g@N*Nk|{zWrd>F-AA zJjBd!!Mz4mPmN2$uUxF0y7n^?r4v5WbFczI%ml}$Yh)lmc#S!scrm%)tQkm>CbF!v z)iOh)b~2;z{3nDsRK$sSWl&@H4MYrDOWhcpt-TXb#UjCA1?&mXblE_Qz7zoP`4E+E z>(?JM(&69NDaw(^G*#TQM&YlkuxN+6aE*u!X*$5sNZq;pqY1NULX2>g16YpQAKHMQ ziA5$FMfILsVEii_+iEDEAccs@to@@&8zJP97o{j&tS(rifL3|^JfzhWOY9I4#X}`y+zSQJsc8LhDw-MJL`GAuo;Un3#{mw;H^4 z3+wgN<(nDE?vn^@lq%dJY_)qC;WhbP+#-8{*sGE7dDl(1s8pW%?lb!k9d8G0PT2YHUvJ2q1>eFlbC>9~r2d zgr?THDg)VQK?{zdkEV!+<@_?)W0?z3OwBL#J`(uNq_ac2KuR*Pj||jJvTiyKN)XFj z&&Z=`@)SZ!Dr>Jge?wA!JH&eRK>TdUJ8s`p>v<+OGS%`*3mVxK=^b?u+EjADQOFcP zW(X&|pVJ)M3~L1!IfSzlx5zyMk(z8l5q3IIWNR=WIf9E&<47#_$)m+U(5tT5RmcKe zG*COTse2wRlrtvs_dT2Gq8Y813ITnTT!8HD2XfZX6;EOh*;0ud+v%glK$87lo2w-g zuHQ3|=Cx9(7NbtaKe}h2nFbbelSr#Fxsm$@8qt;=!d5Rbig2d`F$Hsx2rL}r=s4kw z-Zv1JpZ3Wzt~~?HRkH8S!k!dz_1xeY7`z`g$*jE{fxMN^m4gySX7B2 zT{IBRB5k>Yd7wvI4-}3Tt^sTlqctVf%-#RN*Ix}(_9!z5+^}l5pHW2Ft=bn)_E7}K zdg+B)fNYE6f=4q@yRO4~W<-EXkPVFh?|0D}(W@qZIg|O#w4I&Kh<^*4LmOOMHbutv zGm5x>&Pgsq5Bh?8Ey8tlBZb^wF!Efmj||kV%jaZ`LN%`)X7psB!1^sjv-q>|7MAT7 zo9?E|A7-#8$p$^r`mNE2wrJhh0j8kQ!rTr7{BrL|6f=*3O7{4U~oGz1NsQ zvP`>@Or46&AeLnj4!MzdYYQk=eZq67@x-Hbl^X$A*A*-cRqyODY<=P`2zkcTOV6KC z#4ep^LDLThR|P^js4&>i)NhceMI5RhCNU$a1268F8jBia2=I|(Ug{Xl3=|xWGAs2O zg*T#=Qp%0EPo`anMj6GU1=WMY>yPLt*gXn_Ln#i;Sy$N_h3jrmV_K)soM99`ytVdH zm!Xc{iM$L?zZ(sfS&3UjEwxGe@UwB>l&j&2>C803MMj0bSid>_W{CZU>0it^Hi>YZ zcXpbs;naY`AW;UAOYH3D2f}y?E{;o4iRGe!)++>BM4^!1X3pBljKXHaL2HB|H@tIa1^0&c$+WuY( z4up1$1E-I+B)A+ZE`4Q@B9rq!9cWdQYn&x&<@$*FiL}qjK$@3>N@WSdwj?i>D2ks9 zq?Sp>k@|`@AKbo~Smc9FMA-c)QF*a9+ds;@wXPBfvkBCSfT33Msw&6mVFs8FG4f=b zh^~6GbOnNk1uExQreBa>7|zM1&oitHv8G+=v1h}Rk+w@J_fB886TZiJ&5n|`M!Ng|<(ilEFu2IFwf`ob4lY^dQP#S9TpnYY(hI_=$ zwxQ+xh{`KQCT7%GCSoNQQ40I-qHMbXd&#=z9>^@V1tFEvH0M+?EsV~HJn(FgN!FoD zRoj6zx|`8d`|?^L8#p1ck0NsH1UDk#QmgFgF@y&_kNKfuZGALn>;9g96R$7jz zy_6}6pAN)~fDJljlmQ^(GmrMc85xLV;&6a3V2F9-qJh>!)kpaZeMgBf>z+p=5J@&| zBoml{dY%rX@dVsvi690tZknjnoDRhB3L!PYAb06y6ueIdiag|8+K0HW1VF;kIvq$% z+b$HnXP|bW=;=UFf*y^_l?xa~E8+T`4iteO`gA;cXhJg0Irj|IPKDugAn_V!FtD|d zwHcb(lYy!*Yp$hcICRryqzdjCD7vZiuDNJ49Ga&CMVJMeN2jou&oX^S_YBmo1bsSC zR3XU0p<(9zjZ-%J1BR_VWcXx8k-Ck*M)UDdi(dOfTltXF;0D!CkG!n4sh#PkQhZoL zX%|GM64BAgqrua|ov-!9gG8+|T=k~|iT0=C9=#LjT(WtIM+2lT5pNTtZO9ZT6yU(X zf+nqz7zmfb0Jf_x9mZLcJEmETv3ijqQ_%3R)cJy>?hqZZ*n|sTS!G`Xw>c*s9pE!=|HVLwv&M> zCt}n`e>A~+ozY!C9VkwJk7BuGEk310P;)wvD_fta3e+0wNQN5xbRe1074{33wD{)*&!y%z1HYO6Bb{6>kX;4hKAE&!-2q(XLfc#y4Uk#D~#7qF?Ws7c{T=QPRwWu zodfqes;ldfh(gAL@&kKWrk@tlv>R%t1qyK851}Sf-D`3Bt9suAyVE?g*bD!(i;UJn z;7J_SZLh^xFGL{g^a)~zTP)}mB^Fyo4Nj+D-~B+@EbPd&uN6xEI5QAPRG$x0-)kuS z6d+19`Z0QsL=|G$RsP7G-`De~&FJvf#QhUgqdg>QJq^|8KboGbZk&kapwy;Jw&iXg zNsTTs%)@*R6OL9;RCUq`WuR^nQJPGsqbcv#ub-&}MH>cl$(o;%SnT1VP~q{9j&Dt5 zq_x}SHkc%05q9v4bXmP4$@urX|*Gqz9_0hqGpn>x1m+q?aJeKpEg? zq#~!$gvzvU?F8IU2XfZhAzAJjs9o0Yo`IAj3xU6*m*E;a z9jFGx$wd)`SzfRq4YVTWgK%gf-EzT(jPzl_tDq~!WG1|k(??^9LVPo*MkCkSdS{2j zPUD=0_i!lH(f-k5AZKj4gkr^3oTwe0JX%EA^HqPq{%Xq4Xdc`%5C+{jCRKVB>kA$& zE=b&f$fl%xGZKZTGvfL!C;UDT`F@H_;dCIe$du%|bnUWNW*95)8AyNM64(dYbJ5=T zhb+VlL&zAq0-H+08C`liOTzX|MvH}=8J zhQtw(PeTXAOFFtW5ISR-F$L{R37TI$E8(%~fg;69fB}fu^{*lN{25h&BH6BnOu?hs zh$WmalIeK@H?l%4qQwl9p-O1#9tl7I1pAcj(za1XVTED^-L?IJWd9B z9u$tYF+ltwgkWmH7=i0%;+%|y1fD!iXo}sJBAPXPWx^Xd9mqbCs|7)#mI1Og46j7-se1I^^cLsYiRR7LL@sGU0X z$w1+ZPNX3N#7@*Ba~*wfL&iWtN3xp|64Q+9cRC|W-Hx>Eo`DeZl(2{L*_kx-=|GW( zl!Jyq1(eLxMNS6_$goZb*%~;;3)ZN@v4Q8R^*VL^jDLg|o?`c+@gbuKrQuY+-~zNZ z;X{h@$wyCBMJv}YUO!)uo5TUs50NlzKw27Tah@Coij&r=;-|rwJMEYboGe62k<#GA z1G2jWGG6Vvt30HEx^c9)MzfwcrcR9o#k_@vM^^Glu?&YeHds!R7_TyHHc($7N@+52 z`YB`qP@=jkw4e6-^ZvLHmncjRFcxQO*(&H#^~8JXJftqokfz1N+?V1`_>d^-y5We5 z+|>zLxu!qKTRlDqEY9EArvrsEif3dZZkK6ITu1i|w3-05!VY0AOI+Fa3=|Dj%r@@3 z3?JL+KpZWoS}+MADsi~>YZL>uVi`W2QEUI`bRY&c%3<^c0irW8?L7lQRE`En9@x4I zUO)dR3KHPZgyl0dv!^rS)~NNX{~=PL^U(gS#XzlEs_vPQhAJMZyw5bFn|0qnG)LA| zTW&_>{A8fG*QjuiaIS>761kCk28y?@c}Ze(k}+%_v`5uVi)aSa4+3qF2q8{qgi2*L znGw6f>}9zVuivf_P_2UyIBr_k(Xt$hDucZ)!=oiq92EqPnhutT*PvL$eIcBda3Y+k zo&*oA%tIn7Hlz!sPkIwZKXcZ&_zP>>6vdsjV9r;E;5B8S=Xjht@q&3hzt)bwx!&|9x7WRWxxK~jjv{(<_M&gl%iRJTUT=qk&2=%_8*DQi zUGIt;6c)U?L3wljU_7_&#-uj;qIX@~Y&X~2o15{qGTVl}+ip0yJ^$=&2D?rFcGtV! zY>U#&#<$no(PnyWoY6-7UKG2;V;1{jJGd#X;j^vT-Z-0^X>r}Z-QP@xd#z;&UhU1; zV%^;Aucy1cx4E6}t|$G?_IkV>-2iTHr-na&&8GX^P|f!G<~C+F84a!{8&6#hx1-&7 zS0Mj6-!>>95dTo z4Pfx3}jHw)gXIw__V(wD&fL_L%{$cQ@{x;mvT9@yR#)+v)Un=ef26 zTccNQuBW}t&Gm3}({l=Lo%QoKtrI!D>5p!%#Thq_)6H!>VgI_{A8)qXUT?G?oo~ao z{c&%*b0YV}-kKFAwb>0F)uO!F3$dI2~Uf-0rg-x>=_jl)?&FyACz1i)i z*S+E1LEm|i+uIEj1f~rNtR|7!XExf+&Po-N-R(7_!x)XnqR-%-ZGv{=KEuse?{6IMV#54yv0PlMV@iySZ*N`6=MOd+ z-SC7)*ZqNGS!~$c$;8Z9r19-=I^y$f&mU|$-sx{;`Aa@a(ckcg%KrZP_C^M?H`)d} z#QB459Os)YYq)VDi`(twdLtMyv*~`iFI@A*`6g($*_M2?zO8!e1l;UK2IC%#IH3E% zjXFf4-*CtH?Z^DH+v(QXv?18Da(sQe8I87dMJKoY^X=Jg+uJeNlk4q%P`L-zelsrE zkNu{9?-e?G7^T{K0NVH@CZ+-t~>W;C33Z zXSXGvwj6Ax8#M_b?%9Lg^tVw{FR$%hGu!b+Cwu0Ww(D@P6$$bx6NB9p`(5Ah-Vft# zh8J|h1dSDx`um->qW$@gdE>|P^)}bHg_BhDZ)0l=-2uDsr*=~4oAbTxn{wFO^_kYq zW@l??)G$Z(qc<9FcGMpGOe`ca!CHTqpcw>6QpDBdfdAWAKA~^pML`O0DilTqj)%LU@Ml^k-sWal?(jshv|-Pzl#EMn zdwshb4MtnGX}mq(LEdkMT)GXfjB63c$sN#R-u5y|yB!~4v^(E8?&Xfk{g&C@^WWJs zUXa&tEt+F5_V&l_{M%`7rF(t;S#QIN6+I~gbXGihv4Lz_Io&2W4fm|VWHYc*m3vUg zS8ZLu*LYU;8%KU~vpfH)yS@+jl324FvFpID*x&4| zS$Ny77Up)qli5s%H@9WtZJHG)G2-7%T*nZv1@{rs7Y#@o%9^_h%? z`GQxLHgZ8z7d*V^X533yTCvVXh&mFI$j*9slk4ed$PNuQw{F0VYV!H_=B-z$m=>u# zzNl1c&)eL(ou=Et?sl>-#tCHo?R3n^U>Eh!F^+r{X4_wH^jdBE)=-_~{56x07MNQz zo-L~(S-Vk-693-_rfAH&pUxj_JK9>E*s88|uau?jIR^XQZNInQ3OOg|-vPJVo%NSh zoo>RVx)F3rCP{8sLhW(h=r}PmG03*y7Vn1atBh4-A0;<#Y_su=ER45$)?>bX^930< z>P}YbnNBN=XtR-i8>rjm+2i12$d7XveZG0?H@)(D^WSgy_Qzj+^z_lYUw!oW3(H5l z_rA7&=jrU7-TvLDkDk2SeD3fMm-}~jPu_m~^zBFQRNpv!^^N8FH@)&3zWs;3@IzmF z{I&hlP4jP>AMj7+_qzx9Co_IE>Wd$eTHF`Q^3 zbo?RycJ+zJPu_m#>G!?&^znO7{q-a9wZ+3HU)jI&-q)Hx$M0`^*L)GbfAO8C`{sM` z=lGh>SD*dT{>k@lzP5jJ_1oX{rNcxH z|KQ6{9zDAK%KqtlkH2sGyLP_$>eZL8o?Jchf45h!Uw!3jfA#e0y{pGD)YU7GkDvCp zYMA-Mr|;C$dv*KX+Yg_<{Z2Kc?|;1d^x{juK%i zYY&&lzgX0}q363Fc&lEB!zwlJ;pLdp8#k}Ne)H(P?RP%P%u7C9#597Tb|C>L&dHuexzwz$Fr`6ZL{6i1F^Nr)jU-`lPPUpsQ^K=tW zP)+j7Z+-dQW{bq1uohX z#wVI@nA`F5agJX82i1_DzWQ#zKk$F`@6{Rmmg;-2)j!m4-|>5|4*Tts)z?1c*WX!v z@}D~X{a03_{#^CY`)^_2X9t@vp9ar(c`TjH<81&ab~xjro81`&FE*_wW7h<^MIleebv4zvO$>TEx%} z1C&&qDR_t2d5Y zz54q{-hKGC7Od}o_&sr0AAI2Fe7O3aFMpl?eYE<^7njfctdG9+ zTW=rj@aiiu`u9~|GsY)3?>&9=UHfj@Tu2!`-czT-2c$iFTVTTZ+_`} z8k6|3=F1=W@W;RU=*d4nd-(QS%}Rah@bKT=eA$zK^k#p%d9O0jAOFGqlc)E5{QH|P zfAHg<*uJ%R^zhM>XBPJ}hwTBk zj^FwENB`c{NB!R?uYTFp>zm!Xk7r;0^7q8QXnv1B*Z(<8zwpy>uCK2C#&wMKU;Oev z`Xk@`FMRq-lRy61pZdxF?dyM}`QR_{`ztrP4Snw)`QLu`_=!vLL2S-n{d=B1jz9m) z{`|QoCgsonM*R7$!=L}{`1AJg=f6^Y=FKc$uk!m@tb_L;B0^~|5^ zpX(!6pIE;A?&F7>AAbGI!}x2r|2?%UW5mUy$3I-Xr`KmzY5)2c-`V~8Z+l&*JPQ|V ze)y?B_gkv9eDKTu%e$}r{{Q~!>Nozl8{j{@``T~*@vEyp?_a+17w^9It&^*(&wS$Q z>OcRBk2g;?)p+5qyyTX;`iFm(0dS9h_OJOj|LnWB!?>L+(W~>Ts}KKo@4x?Z-(t`I z;j62E{I9|8Kwl53XLF-8|av|GCAN{QZBwdae4~)y?nqp$G5(*H?ew z>eY{Z>|-DQ*sCA^_^Y4!#4Dfp%-4VF)mK0D_22l}&wl-9Kl_cJdA0hl{_XIW(_bHa z@)Mu<uA*<-vED<%bV9oC(K&FJC=)=|dm>$VWf+@mD^<0r}uJA6&ika`VkkUp@HH zgO^|W&`Tfw=tn;G;g??>`sQa|{?M=by8ee}zh(2;&;Rb69e=}L|E+(2{}2D& zKk%RYjrabCpIiT$Kk}#l#eeUQ{*}M^KmMQp=D+`+{?z~U^WT5_ga6VW`!j#xum1G^ z{0rrG{kFG$&ma8P{_KD7pZ%@>(i=bdGsAD%{=FwZ@}rBdeEsg-;%C44BQb%Ge#_`D|8eIRP{;w(v@T;l=@!R}b z+5S&m&8}_)i(7%>>q5t``twu&d$;=2V;}q6|Hmg@zZzWis{h51)zA2R{EP>KFMa9i z*ZIT8u0HzU>m0%Owx9XnmtTFj`t#M*_Q9_>{`q%3_}2O}9JmMl%8iS;K6x;x{v4L< z5C6fpEB7sC{^$JfZ(hCfkACmf6`#NUy|PoUf4OOxe|^yZ_5Ihs^-G(bphpY+uf0Ok z)B8ei{Dnd9^?vUQ!|4}_@Kay@u?L5r{r>xm;!DlX_z>%I_h+Ar|332I<$9hUdG=Wz zeE#lP9{jpzpXH?o%@_{`FCE7|4CdGG_P=i25(;%`@1f8km0x%$f8FCV-6<8y!d z&|#II`}#{)|M3qzc>fpwt=FFW-FN=n`|tnafAsUu{r=L`Ph4NU|Fb{!3(x)j($$au z9}nLD*}wYg>F@CbpZc-*>Ho@~`P!rK1gh0K#BQ#t`1|1j<1=^hkv~3&SgHEr$uG^< zKh*r;m-;>aU5xPJe=pYH#UA+o=N@>*c04Hj<((u{8M`H2WzrXq9oho%1Klqi~ zw;%4;kDh!@^nS<@zp_4l#rHq_2r!_5wF>Z3#Q_tpQ0y(67STxGSo*H|Qf2Rk* z?f$=McjS~>H|(iN2aW(~onZVNPmQ7*_SBfM{~N|)od2CTw|Z$Lu&4e)Pu&}PYKQ># zAQ%H|1K-XDkc)2Ju&4e)Pfa|x0YSv`)R?K?j;R>ie>b+In}!m5>M!)veIa;w_5*v8 zf43*W?f-4tAA4$uz@J=EL{INvPmP)SZJCPk{dePwJ@udW)Pu06h6rF!f-%50@a=2> zd+I;$snIjlxKE8)`t4YXvBlV8PyOdTHTp(8_SD#$U~htL;M>^%_SApgQ>$Q44H3XB z#TZ~4_;xmcJ@udW)I+eR#-18SDr^Ja)CM5D`kx@d^;P!o*E+XC!ePthCMYz0J9WhfNkL0*#P#`f8JB8 zK^3@9jamBbSco>$Hh?|#pZC;1U{8%bHI7u+2EM5cKzPXwKc)ADPw9<< z2;$if+@Jb({V9ws#+G!`P-0L0g`OJzZ+7gdziEF0dn)Xy{_Qq^JvBt&Pd-mQ8hdJp zz`vbI`2MjCd`la^p8C&w>M__;V^57E6}EwIY6B2na>KJ9UpV_Q79xn}slR1^0_(>8 z)qlGUkZu}E?5V%RQ%{?1V&3aCg0bsPHx`u*cfLnpq7=}X&Y{tegr5QPt<244Qyg5K znG6oy(UQt$yU?9k7WV5LU~b3qU13V!4`q!QGzN1vjYFj|=*}<;8EwChvW=NkD!pBP zpO5mFP#xRletfnio5CiwqpGyKxQQFZnYxI=UTZaXnH9{E)wG{Ai%R6oVtu%fJ%fSn z7W727`iD0-0G8@Mi{?S`WX*DPoJC{LVzZep_8c}9=|GoIY)dhsu*j;(BjrXk28%k2 zMzf+gv&bucH<#|faD;fa-Xkmr`pk2oQ7tJOVCo;%r8_LEWW;dy?06%n63TNp?lcz% zhz-)Ukm5$2OnJd?SXNJA+Q8vsp`NW-OT7wY6lYRypWDNQ6uu z0$EFry%U4xNM)kywt}w!T!0&(0B1l2NIL+%Q7*bFh3#VRLUUnze$t{4a*7{{no}G| z=#)OBS}!zNM&jR2uZ;`ck>SyXbzch(+nvKU=g`oiuKAPA?O>1C8T| zw7DT8A}`;kHGKt@$uOrdon2(tN9NbngzgB7BIQ!J!IU0mE_CW#I-&!s^_r;pd&t^t z86qdy_#evFX&2@8Udm2$`G*TB-1<1_#45o`0gU+VVS}%k= zU9Z+Nmr|K*NS^jtJuTUuG%9NzgXu;gs*IGuP0?zOjf*3D?R=_>^I9?k-JJqt`9}_* zD{KlQOH1+=kS8*OH;-*sE%p-Mng~!Xx7m?Y0Wrvmw4pU~0)=NytydH$-Uc`Wg>e&hqx?3f(R=>&VWy5q~ zQ_WrIE^ZWByNe7U(qV!X!_vi(>Of&4inZCG0)!eFW!p7~sB{Kh3w-Csn&JUvCnD`E zN|Kx7MxncyESa|ixpS00i#`XrlUMNbA$oz<=!J|2%$jFOwhKB0^Oz>|Ru?DN!Q+r; zAu~s%=AcR~XWF3uw*!xjXE`!B_J&LS&>^uw#iX-}RNp=pZ57Vvfc8UCX zOtL=huV`i@QRFqB`^bM0y7v)JYg_l!Zc_2mjO7y9n3`cL8G&nuv}*vi^?XBJzX5p zF#sz1#4*bO^>$F}$jgr1om!7&*K(kt^V@JnWms$7kdFEABZrP1AlKkN+NQwI2iyD3 zt(CYi6}3$_8oCuL7dI+0pINy{hc>1rcO|alw2|qK&NW;f0)__nbT?dn{J0-X_|5iD zKwj1||L@4p{of#ep|uS^+vXK!8>`JTq0x|SSu%_m@O;8HploG$t4|}2jA%@?j=pP& z@oXblmhgB>9M#kaU&VL@lVqDQlm45u*|d)32rkHY)(@Vg0A>SY$gX+71km7*j2j=g zWI*tMW>1V0P_@vp3*BiW+(q0Xj$P@l=w8~6X{`S>*VcWy#aBYE`Jq~Vw0$Yp{95&V zc8d>NM1$QFH4_I0U2Q&FG;<@S8KEGLbq3vR>{J`JKS>G1{OUwk|1`#ttEl<$%?k&TuabEPnhR zl!@|y#T`FTCO8lEm|vR?%D8)h=~)RVE1d(3NB09|_YGmr><}$bw$&HRoY)zZ#{LL& zqgdc=*=v}iy*UPy9R3+ha^4DF`%VNJ_7gxc6XlG(ZUGAJNx_fXu7bS7m0+ah2$17Y z1#{HSCx9n0_rTB-Ng(S=e=ubGBJdy}4d$q5bq4pTMPRT)F1Y5e#;OZLW^_0~ff-8&MfwJlz5NVhKl$CFS@GJ}1&rlxR1zcSD zije0k*0)KR12Nm>2gko3Nm&lZP?pnPB5i%aa+`S_#dpu7WW(OlRHieTuu%dl0_ z!H~UmFh}>wK%g@HDVRCQ8Vq(F0%o7?36xLmg*nEf{ekj|J49xID9hxewj8s=Es z*$tFO&n7HH`3FU4F-q{Bv>>83=H&!t%WugLB4sDQnVSxCw0B(Mh@4^!Pz|okSx3}J4qUZzA!>Qu(H1D{MS=J?FNnL41IezY z@XkWP?ZR&GP94B~I}M9$iChn>ry9(jyrgAB?Y3D3UhMoO2J6G7La!^ z1Rz8UQWrpkbzx4idniO*3%vGO0VZvF14{fi!B#;zZ_8!_-3%R28WRgJw17p`Nys-gSS+%H{4;q5s!c7kQJ0mtv?Z=@_|S5`kFr@chlY5=nI_VVJ&YFM{&c*XFnl|&694bzDlU0LLX zYRy$9n1pJLm{oS~7KpD_Ksj3)K{ER=yi<2@yVMrEEtP`%PCH>viT`TI*dmC|VUQJd z3ZnHIGI}%2DM+P3mOmyCMXb-U6GJcX4(2?G?gBl-RoDtl@OpD2Y!y@JIl^Gh{nfvK zx8@JlO6Z~FphegbUJF?W@?zn)7r=Xr zhPWXsDhIBFRkfFLV7Ea}m;;#)Ewa6o1BmG;g_P_!TEwx;S2LWE+x`iDkgr8iuhr+a z>udFPrFzbxvK*K$?ra9r%!S1!2WuTWV6CU1K~P_Kisf30JJp1~fkJbEqj;(#IR;mP zc@|6;I1hqmKQ^`v1c_)S>wEQtDmnvm=;K+yRTkyi#xJJC@kdeGox&of)MR=8>nMHo z52G}^myX*PMCHHSDAKH5E1;h3ACDqiN95?7uRDs&?tpwpm_Kp!g~%(QaR}NzyFa8l zn@Od(GB|8m=XN$)HDFCwGpB5HapSnj`bl4Fj3cX>>@PXSX`e=pamX~(3bH;LxCPVx zOdqW?^cJ7)qY)Qtn+|EcG(0Af|?+KLcfaQ1A*#ty#nJLu=w zOQ^&o3?fHAbi9-Tmx&g0D2^Rx1c|%0Vz{eW)7kSzF4{;VFM^~Q)rn1}8-#qwnGL5w z$#la5AJUh=DcW}FYOOh+;gbzvQB^n|X2P)*1;!vW+k{3}AI4#`i8MNJgkF0(%t8Kp zBeCegfeRl5Q)wSYTY<<99lqAs`I~u|dRd%gv0PEK&v zPga=DCQCxuzg=N-I8_Zpxtj}{u-xTs#Rjx1gsyBvaYyD)r6acmekT{^w7>J&95x%C z2U7=rq-_Dih2Box?`6X?+ao|!7M%*y2YsY*7R|+(j@DaRYxukQ@Z9#bC(bj0%R!FR zkFG|{LBMq%S8^qweJw4CCZFZF4o-fO`R&yr$WSA|)r!_m64c(|W@giVx~qQjDy>rfuevu!40E0Nj$&;NJiwnA%LR5;UuYWk0k+&%r^5+`hck7Xw!h1;6bUUmXp zcn(VWSjuvcHk8v|%Dn2oEpi*0e6Z>Ek=wA94k))n+x~V2$_Dd+A3Xk8b_Qj) z;)$G(Wk+r8wRR@Tmjl^(?WHVdkW_Z9_Ohe4PHi8K6OTa%ki+s8PgW0^Lz_i-DcCu!tc zr@7#C0jUbgPHn55c-8Y&+iE9T1x0H@c-0fHdg4`2E%>YeUiCz4oxi*4iTZ{ch*^iu zYlw2~=X1%YUs3<)ch@|XI!v>Xr=U*I7p!@HMO~kM{PxT0)#)qNOa3eB>iiXTb@__A zx_(7n3SUvzcVAIgx38$H`&ZP}<16aw`4x33%4W@E&oiJkqb~4yZ5Q~wBJt%P*{Heg znTF%&_%FW}mNEs;sQq(i)Qr#?8l3FyuvmrH!t4#d^;($W_LIAfj%?{_xW7rA0T;t$ zzTq3Jg&D%eMcZf3s3{t`Fdb;r>1t;1Yin?s6xHk>y!Isr=UCt}>>(vW!d*f}yEVZ6 zBudSLNhlRbl!~NWha)lzqp_!}y@26G_9SmVDWLt-|uXgDj z*1mR6B#VO%YhNGIKV$7n4eC$?MlkX+;Wq=&_X5z9Dd?M1OyY~UvMBxGsg!Mhk4S8d zBQcJ|AD%wKPo*GN@u#0kK}y^xF%o}B z|IA3N^qWX*4kNN7jLz^0Q5oCX#>7AUJtDCOj>I?;<46o&3-L9d4>epXG?+ePO;=L> zbs}-wnAj4&$swnk*Ybuf`6-hbbFL?wLh+|Y* zjwsV!6RG)M8i_Zof#}G#4v~0+9eOMpzAEIS=Rtq^ok;v4{WBx+Z^y)z#LKBZ^_GL} ze~(Cf7)N3piE$+U+L4$!g*kg#(K17swE0$%xaa#;Kls_R(@L}4-DO{|B)((!7m37O z)!==6`E#G>DYQ?2r}BR`I}M*f`%nY)ECjjXXVCDQH}IP`@S8W#8#wTrH)!zbXIJPc z|LZqzpa?2^22Cymx>n?2(eoj#p7wLzyYcDgvpPd1pZw1HziajHD?Y;`d%tFf_b$$Y z@1%2wD~KPy^==8jCr5#4R9VlcZT>Ro?=fnM#iJ%XYQm$YuRLmkuDHYY^+!!k0KF4P z!Lao*pFe6^L~%rKm!7URY2rk8_&UU|_)O1#`=}JU_W$NhG#wg4cF+k4a(L50w!Vo* zmWy)85BVmV-<~I%2X2k(_)Ro2gk^s_tRx=zkkjFro)77tIe)1H-*!oXD~D+9kPe^Z zK~MEe0u$j6e6y#FZEcZwAv*r+KVAFkFBpkGyl@1+X$HS(2L5Y3B6jYGRK8QypN6O z=8b?S;}6CF+W@u!Yy;Q^unk}vz&3zw0NVhz0c-=<2Cxla8^AVzZ2;Qm?TfNcQV0JZ^a1K0+z4PYC7Jq^iw~N+ft+3_BxisB`_Wc0c;-#15Y#?m_ zvw=iHCNUlu4}<`Yl?VmQ2FwP`2GRyF8%QK%661mKKnUPiiBQ07z-+*5AZ-A%fkZ+k zF&-EXgaD3}2nEar%m&N`(grXaNF-zu5Y#?m_vw=iHCNUlu4}<`Yl?VmQ2FwP` z2GRyF8%QK%661mKKnUPiiBQ07z-+*5AZ-A%fkZ+kF&-EXgaD3}2nEar%m&N`(grXa zNF-zu5Y#?m_vw=iHCNUlu4}<`Yl?VmQ2FwP`2GRyF8%QK%661mKKnUPiiBQ07 zz-+*5AZ-A%fkZ+kF&-EXgaD3}2nEar%m&N`(grXaNF-zu5Y#?m_vw=iHCNUlu z4}<`Yl?VmQ2FwP`2GRyF8%QK%661mKKnUPiiBQ07z-+*5AZ-A%fkZ+kF&-EXgaD3} z2nEar%m&N`(grXaNF-zu5Y#?m_vw=iHCNUlu4}<`Yl?VmQ2FwP`2GRyF8%QK% z661mKKnUPiiBQ07z-+*5AZ-A%fkZ+kF&-EXgaD3}2nEar%m&N`(grXaNF-zu5 zY#?m_vw=iHCNUlu4}<`Yl?VmQ2FwP`2GRyF8%QK%661mKKnUPiiBQ07z-+*5AZ-A% zfkZ+kF&-EXgaD3}2nEar%m&N`(grXaNF-zu5Y#?m_vw=iHCNUlu4}<`Yl?VmQ z2FwP`2GRyF8%QK%661mKKnUPiiBQ07z-+*5AZ-A%fkZ+kF&-EXgaD3}2nEar%m&N` z(grXaNF-zu2d0DaLy?&T7O?Rr+#Ayy1Q{kS5&crEH4P6Ia zZ4E8Qi4!MjPoX$XvY!MC%gIhKpDUG0nx#_dGXOeGgnD|y^zY$k(1$WT{DA!Yo$sGd zk^>XJmp|R26LhIL@K)!1c|R6c5y%yTMRkgx3lj5!xP?cY9Y))@Fa9s=aN*$=9&!H8XdCy%zqrGN`>Gha-x;^!zIcaD z2cGT5v)%ui+3wFbcH9@oeQ~@Hf(Iw$-~{)@afkAEI+S><@x`X`p)ZcsoW8}@I^RBS z6!Gj!uD8`;&fr_?JypZj&l%F+C1j^IJAaDlEXy2I*9Ai(4CNn}q}y6{zP9jK5@(Ng zZp6Ob&l1nOjIvts_{13cnb>}DH{M$g%iX4~hiPkQIBTy{Kz zpQ@|%P%rydCjnNi9UJpa80KPPb=uV-VJ;l<=edXypzs}*{M?oSNsSfE$yVmPs7Id z4_-OGf0#sEk=~F!GmbT^v(Dnfzx=XAUZLj=L6~^Nh!JMz!Q8~}{Df1cj~h23NKZQ5 zv-#b@vjcXsdu_5crY>J+13%jTxKX zUAgewY zy_6_+t?{sGy$!1pyPR5ZcIUpK!?c;ypW#x9)O#(vBc2O9;F!0p0vW0UGPLO+b zlNQTwU`;Krz7Xy|VV3bb%CR9j4)e_R%o}Oj2?VUV?v|0USx+jTWSiJI^30gNqi0T2 zpSm#2e}bG}t)5LxeGrfFzF4zxOH=n=!GTk%Zr8b2D1rE++Z3*?_SQ;DT#+d6_IR$I z?u1dEoA#WU9{>zZU(j?{uJ#fzLZ7Iv-CJ}!oi)t!uH&H^;Fr@PG*}w>^2R-SK#dT%BWv+ap_0Xw8X_M*VKIhMC?Aimb0&*>%tJaDo75n+79%_ci zr@B=fl9Puggp716u77sO;=ws}QQlA8Dk>HiVkhwox+wa78@@7H|UyzkNikLIg@TJ~^HB`^EORVr52iK!lDL5W`0Ymc$JfQ@HtlPt?r z^(NT9?s4Z%PGr#Jxrv>?j9eRD_Kd*#RcDP7Q*|`Mm%E0l&&W>gC3U&S znT}FNjb8Y}4-#$&d;92$Gqzm&1*po+rxFwMOahtb)Mv~MK2VV9P=hL5eD0ERcBlq@ zNvPgi#i3WLH4Q4%;pI}sYclT353D?=?l{WK$H6Dg5b5PmsnPxh!c~d0vr6_G1oRG> zZv*@crulfiNv2Yt^zYM;TKeFb6)HI_CWb${U%v%5@qr5i#7plGiJFQ{x+V;&sZG3P z)av}$y+bwNl_bX2PX~l)21L3CsF-!B?KNl&N?gaQe7614INQhdElnB|635Pl4J2Bg z6V5Hu8}g*TYL#|@#waa;-sSR<$Gstded{T)uJ_17T5qVo%1r0)3T>)5_@ji zFS}Y^iHmMO=@0xeHlzd`SbNRHCVsV*)%wM~w!!mX?GlMzq^E4y&&sc!+r?-0IdupM zuO?~dQ=f{v34*f1q(~j4a`RfttUU@tlq$OCzRQ)S_VG7?RjMnQO4~Tm#5z>{WwztX zYhesKc=NZOM`WDTDbBYzS#*2U+_=IC@P^`J8%r(Do9JBMvD$9clo2c7xvy4LZnX{% zRe5>onv*Mi)k#>FkWp5gvdiyj6}w_RU8+SpLRA3Jc)lih+QkdY63_5XZJMlanZ?9E92dtSEOB)b+MoYHD+RmE*G_eXnGsk=MP)Xfx)tbLuO8 z{Ne2_Iil8&riR-XBpN239(Da?FVoF4m%!RQ+Q9O^w)zO&y@r?Tlzos6z;)woJ_XJC z-l6J|8rq!a<0`rWLvNq;Q<)d;jj&zM8EJg<@Y)!JYEym^KeAqNL&dIPxufrHy5)EX z+Ex7h9-8BqjVY{i=yX_gbKx<>B=;a;-&pz)dh_Y)#S+SKwmi6XWZh+>5mL+Hw)M7W z=hcp@xQyr$ztpAT!lb=Q6}!|E6^{7y={+2soE#P`C|pwUv-)aZx27SeHpXk&dj?ys z7-zdaPh7u}h0szhbrih%HF0{yu7yE+?x*hXQZPm4kd^rGu;&v+xd&O*{x#?l?8`#? zxY`36$Mkg6O=AIgRG(#~6d#kgg6_Na#X%!|@APT~5O10j&hjsfEib#Dmf;4E_g~tV zloY?r$z#(ddgMbV;3o_kf79(sW|<_pYwE#~6iQ8T4{+e>Cc&90d`9M~fRk?|5di4! z-KRJ2SY}P3ve=-hN`FkoxL&WC>yK!2dOv=cv1@IgS@FM^Pv2@7dUe`3wU@LL`|L01 z{usG5fAG%bYerVReAjhSw(Y?im%n?t(Pl&P={NaGsngD% z**YVe^-8BW+aSC8WU)ZP&#GD(e{Fl_OD#E&SUs+R8<#J{QQJJ zEN5Z;e3*~Yu&L4^7TIFq=G+G&7EHC?uyFnbL6{?Lkp6v4B;Bei%GIIqmWtc)QFJZx@sm`&P zv2ibwqI9pxfy~UqF6rrIW~Zi&^*N&J4-+irZcj?%jVmdCNmDSLF=KCV53n}!jMJze z1{-h+;yG1&g1dvYDGN`rSvoDl7WT+bv9C?JpVb*m@nB?&Mx?*k`TF(osmz<+4TafFGrjIQX+?GgYR_}+q@M5n zYgZ4_(Yzl&0G83AZ>U}0^xnUk<34S5PT{>5LxG=FZSqaWbpPs}-O{tB#&!W}HWk^Y zG-u>x?NL0NHYmRnIPfr+VY4{ZLsvIQ_i18Gy*2Mfdq)({SLw$;+5VjGW|O_hf;omMLX!%IifEyJGsavU8O?DEtAAR+5y z^~>tmWE7nQ?4kaWE(_azH8NDz*{#YR_puTSD>^CJgo}NbzA1j8(t`19ITu6)B zwQIrri8m1{RBD>@rSM^K<6Irq9_tT5Na!l49lxouyTXnh15cj3d{GX}*Y=%7dwp@gck%qG>y)!xr`)iS01Xx8xlB5xbT(S4e8k17q+ zoTj_n2H8ZBXnT)glILN?MfO!IAeL@LGd8_Vy_vOVf!UQvS!H&FG4h8saWYTTmqc8K zSe8~_^OdHiK$FSKxC8BI%~R*`cB}aT6KpMlucBtn4X{hm4Xx2kHKr9gx}i2p_V!tQ zS!ilzw(4XI0+w#A7n`)&Iyd0q!#elf4ghRb92D>EVOyJ#VQU*fhZoWIx!+LQE2a8b zMS*S=M3EV>Y?52cHpaGXELH>q7KkZNlgJO6EdidgW zWEi{+!t7{aasQe|m2+p&&CFlBAxm64PBX?M=Rha$J&n%ZBuZwiTD9=xBLYOk5bxs7 z3gu;$$ZBG|`F>mYDaZAl^OoqNdQY%hPxGh^{OP9)W6*JW;s!lVOI&u~)V+gFhr*$0?59hWA-Mbh`VMEJVGio<*_MSw1E^0|M@&=IQ%m0==4SrmpC@xwd}}hmcDn{@hsvv)6==)>Iqj~ z#8IUa(6ev7cR$N6#42syzzOdrKq4GHHjQ<3Nw&Vn+h1z>06)j7-2FaA7bRl;`^RuZ zbl`nHV~3*6y0wq^o{6TAjQt0+2-LN`%o6vNG3>1G^gMR`x4S6mmD&=|Q?QKzZ z#x`JL((`q!DsI^0{@`I21B?+hzef4Iil(N=kKc6%PH{5}^K@qnv6Z{^UewiZD_qW8 z7Ch6dvAzUldTeNF%)2ci%8lPFf2)+M0=j23hy+Oc?k<=SJBPDH&Je;*#_dJNe}a;F8!)xsWU48O1j?>Y@~ljN6i@&V-74wY_SG87RZ+ zieYYJ4+4!nyy{MtCiemYpQxr%rHK5VfSP?wThIMV0RqGYhA`vAp zsjxH46$(ArazH8%Yk<;Q5vhZB-Fk!VPQsZOp}(n_+_`t}dH7isaIBcQxw?hq(&rd- zGmm>e6ETR9T=Zg4E8mdU_+5_NF=(pP%&AJ;SJLc!+f2>)akCLTPOV^#;>tN55R3 zpaT}oOqNsIS_HnI#Q?vA=zuwoRKSRLilBF+?1awkvfba-x%Fi-K*%OlRSxIBJo!=4 zYP(=>c~I(_ujS@dqX-UL{O2o@6}lmM@;-@$uB@Szl#`RB@Tl1>ox2<-1iD%ty=Tqs)o&26yk25_wmGJ=WepGUH=wx3>PDFwYSd>_# zrKqT(LF}Hg^pd}I>nXH{#*-(stB%+(qd*8-KkU3YBj4Y<3kXG7Y>z5MARl#&)$mp< zWT65@jvKs(MUJA^T|{qinUD zVa0~p$jF)sqRKI%c?>XUBPv&|cj=OsH*tZ?%v6Jw0-Kut;BaJu=etL&azdNR)N_sX zX++J>bH<8=$&}#qWO=`<5vz_oIbPJ;I>7@hioI{Vq_W~-WERTM5K|V|+G=YP8TU+= zRL)qcH-WDN9^|+)7AWYYEDi8h0MtNjy-<1KXmjF}LT!!ESpBEx%Zua;^D5~U6&^Q( zgh2&*U`&4V$|WzOeTu|{wn;i+&j?$CIV*V=4o?-?3-+^WK?p6?_}q0b-U-=7Efg12 zoZfM2^K^LC1sj^)b5bo&82DD@6qtfBdHD=qNsqCaDZpTE-jh<6WAxzz2J*^Q7OXYhn8T>vdm+VIxQ&%cG9|C1uMar z+`8UJ78HBrUEQmqWg!=`Y}lra)dzNmdDnWs7TkdOHXi&7Dw?M%tMc=?bh(gKJNBC$ z+gNX<7s*{#qyY}Q=GG`{X`YI_DGBmuK=~_UMQavI1*yGe`nYAG0?!>RW<2)Ye70)0 zwr(6)6sa(PmY?H0^NR12w zJ@}bPFNGCPM;gy#z?vGYkUX=r@h(pCsC@Pg!d+G6FKNgPPu9`ap4ydYPoF89?j|`$ zH92SLM`+HAdhPo@*N2_SSG~2Kze|134-r$0cAjwVleHjF?ckwH15Bn_28DTD80+FO z;LW53cO801=iPZYWztdBm3?y#jLkZ@@jKhK+ZNd`8Bw~+E&j>deZ_fAJD#30e_O#9 z`qYTMZc43G}^5?jFEF5FjHRP!X})s2I2 z&~F{;JZIkXx0`PZCfM#>2eET~;~l(diNCO0w;lrr!>NZw*4v-f?vOS+o3Cb9ZmQop z2`MH{YRs&-G3+L74!X;;_s@ksZ_K=$7ru1xV|db$)YrLpUSFtC_m!Rr9}wP~s9xya z!Ipt=PHy;cuQ&jn^;bS}YH^^4<#}4c1rKyj8-%+f)KA8E<~<94JJ0_|FFq`E_}C!5 z@K}ZpAINc+`Jvx@*&DY&*h7v9@_x=1+PN_KWnw*>$ocDO9O|f6F&bBD+ z?q%C>%kIE%xkqix#>cNOWc=iml6#Z>Y*`eXK|GNY72Gt-)>%X^$v*#%sPTDDj~RB0 za>v>xozioSu6zs|^XcnUaVqbae~j;=K(B`ooU!YgYYM#{DmOaiY}PM>hQ&^};t*zh zVyOT4oUPm-y{bvwK**_JseR$}WT&~tqeoY1_W&V>o;;cAR;$-|BIiPVapV+6WbPE; zH~mfD0UA$OMFk1!OW*1C0wG(4O46wwlF0HojNmijGiQ3iiQV{{ckjMf?v=f&->{qW zYmWoqv2*kA}_SaMh=P>}o5?!K9h{RH1>z_0f35%C2hlbyZ&lW#<> zs^@9jPX@QdKm2$zzo&9B*Taq&|Xug#p(nWppLv~~RcpYHW2 z*?ZD}GJC45ol*vBHn*IJQ3S81FWof5mIfxHKmu?uVuc3zuD4WUmPf@Aw!SzIr}l zXQrZl_FGQHxM*dtCZ!_U#AHd5@Mp(i(JMk@<-tQ%rImZ}j$WlrqemMXqh*m}sTJ=9 z4*~>-x|k-4Bi~!6v&S!mt73lZ7X~X;+P*#-Uf7rxuX)7dH z+eFJgROQqUKfJ9w-E}Ie%uFWBD0SS)C;cs^lKO_y15ygORjyC^ABkS(K^ry+tvN2T z=`~Dg->(l2MQ%KCesvnNQxaNFs{D~Ixmx-}(b^?fayNRQ2(1~$1RR~}b@IH4jg{Li zKe5maUB<9_lO%UDr59W#zchOKblv4}@oteyalw1u1D<`wuFCu?lu9eKl4Wi^%_x=0 z3RPcs$0`4&?=65%HedVlMSXs(iOsXaaTZwzZ`Gj1w#wasH)#oMBe(18IB({JFD26Q zV%^?vH206Z5Ok|1&v?@&R3Gd7B*u%n?qTe?)m^#_oB1q08shV+g2@u@Fc<@i2LAf1 zf$xf9BXpAqtn$@8;_^e)E9uYPMXYRo1N_1hR}C7xbjb+Y)B8>QH{VRRKLS_HL~!o# zz!lfrbLx%U*E^~<-y5g^R%PSa}5To%m_i?oPsxF-iFbLYx;9WY>l z%^=r;q_Kfj-f(U6*0uLcO7pEVBPzO=>>ZN83D5iy+DZ1Mty}B1=qxy={;)phRZza~ zqdD+qPR$){p}6x3yqSl6`<+Zk%e6s?+QO#28`EANh@5DXwolj;>#B(q9D4RFy;d*i zN&hk~H)DRU?nfhmpYyi!qsFhkKIiE_RAo?!CJ z?rY27V-SW^9fp!LJ-Xyc|Au+{i*DpSwS*V5I^r>Xa-~-^d%NqqJlEW?2yrMR+uVE) zvt!L6W{Gz=&4TKJyr^K<9v4av#;R@3W19QdxN8g@Y8vqHlw0%y_~gR4_f3;ZBuvA^ zJKAu4WmM?#C{$WaPZ;K~Yj>#ns(_o)rs~^hQTtWV6t~h#f*umyqQuk~?JE(hdiQl8 zbR><_@bR#BI?vwjy|HBP;loEYU_EZBs!p5uI{A%#MR$I^h&P@BB4T|Z362}@GKn; z`-i3XD-TB|Hu=?z$u;diW$#Qihrq)OZ}E*9WsRW$Fq^Ym?upMm^H)h4GkeXjsG0&elb8*sK^FlfBeOA|qBi7rNR_^*W?0IY~ZR}ZSrr%l2 zkgjuMU9&q^b70q@L$OnX_Q7URD>ymRl6Sdqq3v*u@kvn|a=G2RaNrzq{2`(&L(siv*To@QQI{BIE<8_5CEs1w#S3cRQPT|!`$;s=&z#l-qN z8MW(~s=M=`vS{<94Fmd3?#bx}ZUr@X?f2OjQvGIBrBb~2@t-fL=Br;{fo@Q+Nt$|^ zzi$I`2uGnH=|%jRGu7Gs`ro_1H~>yp-%C}kp0UYsjbEbtfO(gkGRhhnc4RVtPV5ZS z)~=Ywxx`(@H#rqHVaxW|5e}})JrmOBKqwzC?3aB~{H~-VjgffKRPFH9Yr!u?8|xE! zJ#(gmEAP&4O7@gY!vujbyZ}I|LB=j6$$q&13;Hh zX371nop#?_dk(HtTx{4~yg4_Y?^&B${L2VVk4L7a*OrYJ49IAhQrQO_PzsEQi{dv9 zbWR(6!BkD;=v;n0(^F;Y<*b8R&osdI$2(IVd8J4*qFRJ$C!cA2FL_ougH>eYI&8}E zDT6BqfM0r!9Jwv0$F_2zX3x4AoE~$|unSjMMy4MY-V&?xkOsQwOd&nlF?rgWm=*BG zTn)z^dU5ik_PmnHJliPeL6t*6$nfu)4rT5FiUM&2jkbbj;`>_5*s^*e`vl^F#KjkPZPY;^lvwh*&$Lj)I zECcS}Uu+|HtB@<8$KOA>FyLuL^Yhp7lEBpUnUg9)BRjRdfn)+AAU7gO?yWd?8dhDVcvL3IJdhUY5prd;&7G?di-_*W;cWdUu zhsVr*47g}@%_6JFKfO<%qd$ne`@_y>__3<0Sz3&-M*Lpj^!MLwj88C@9PVPgaq|P6 zV%4~nJ@;5F?6F$2Hn73mtIL+a@8|X!{fqzVIK3Y#Chou4c@U_gG*&IE53E-=Z zrP9{#icLo84lZ;j_+~XY_#<9<-1?O-I)_GsDc_-zOPMUXh5b7C&>{H(w$@zqzU3}% zlywXyjX`&A&FFtGUapUIDaD%|f)rBm!_RHN1u z<3-l3g}cf!U=al;DuvA5O=4LkbXk6TQx>3lZ@G_e8o38OS`O(c<&2a()L28RhONP)?cu%A& z3KRDduNS9D21+QB(~?4IZ&)7(={9MwG)9^xeJ*_s>sv47O8Kz%Lii(Y`}yl*{-re} z+p1PtD$Ro^T$3J=a-^oxfs$fLpu}9v74H|16orb03-`9vw-^hKH0L)f^G%z4c;Sst zxl-<^hQ;;XbrH2CwW>7^)vSu>(&{(TSGSAAg`ovo3hWA27IrHa;Xx&hfi`>*VcsBuCFtw?^|Epkj4#b+|Hvk>GS*YOPiwvjFyotkA>?+ z3ZjFs<$T1iCHj(`l5A-=*ltwm4r!<~Ntz+ehc&N;?ZzY8?cdXKWQ&t6_*K}(jeA#I zc<))n!}_$FcR6<)Zr;2#HdXUpX1ZSXmR#3Y8Px+@*>?6LwrMvfK(^g$G5rnOlsmAE ztTmQ@T?9U5QOBjeQd_Bxw1*@Q`c4PQF!5utuXvQ`p2$wf6>bwMw)nRU6dV^QHy>&4 z%iqseY}(({t0??zxSPo6{mjNc{;}%pXB(SPrOCDA-fYxrjN(n=B{Uf~74p|ND>MfQ zv;=7_HZ3JWS7DWiDtaz95T6$FC37WvB^gqAsTQo4FN`nOV4RWp@Y;Wo4=3BIR$31I zcREBNfbie_VLKg>%oSINw}=%*yG3$BhnA$4ZUWQhE&Qmar#voCv2i$8yTQ1Ad7WGB z_L|dG38m$)O$z-BLJJ~_Jc)h~5!_oM$d`R&fP)n$vzjo+D6q*sPk zQQvD-D^!=(Jg&W27hNCP5X3zQy|_n{1K+55WHV2Y)WQ*tg7J5y=(TvI_?~2x#9MM( zB9&^x{=jzW*)|_u)fRjIT|S&_!S??9i`Z84yl__L`rDc}J8sGF5PitMH}741qWL)$x4F_q`gq|g+_x8#ePPYa9%B7kCy@UQyoUmnxLn{LatvkJ24Vh2;a^ z@hVx+kK0vKYFukMb=&KYG@Ru|G{*2^nj-i?&Ax)=EyG(1g}X)Sq9n1NI7-r2;wp)g z2&I}*yH+2b`LFGVe`rD3X!ncQ*7)>|&cm>qJipJo=5i)Az}icY7PQK(-xe zWm`AMHfzYXvjUTLY&%V`Ex(Oz|2!7`v+n5A7Ud27x0Y01k}2_*SV+2vqs2BNF6`MU z3r`D&w*vR3Nooj?DGaqEsteO`kHcNiS)ZQ*J2YMa9N zlxF4Tbb&|9u$J4x<-&52o2Wv(Tzp%i41Gbgq*gjoY6*RKK${Oo{W;l~9L=TtQ+@b{ zwnF|pQyM1?l6p%mq$-j^*#9;V7mFDpp2%7FSU9?6y&$alDZidC*VK)tz!fx9)MwYl z)gG@-D|0ToU2w2qNAd2GUZv)r__6pXO?FJC%4A--T}5E!>ndUOu$sBGZgofMlNw%g zg}lB^s{G;2{hP&tr!A+2HX=FEaq%#5phR9`2iuL`>cc~&$*}f?e=wKw&*IpJyZO+9 z?fv(E#WwE#Lg9U~-%Wb$ra=Yzk2FgPi;_bDCC}PbmQqZ$u*_({hKWXz2H0@OE^#%Et(}th5fmU z5(SB^Bur8v9R_=zoHid`Ld=Kr;B2^TME+0o;qBY15PHA_>1jBdWGPjJ>lDW&Muh*a zg>zh~!oDpwg6++r{J5rMUP5CicSi%IUZ<`{O?r7k5vRbdfc0wZ+x4X;@W+(+i}wDT zAK6)qvgZ|Al^3gCR}ZhD)`r!+ZcybeZuI0GhduG==8J-ZEzZKR!a|X+xQBSJSO`aQ z5fYwMgP2P}K0F)x@bcfC5C032)%(R77XRBgIF=0?A@Yu%tw)3VWX(gbz=X=1NPXRd6=^pW6>_ z-&Tdt`z65{2tT1|0w-d;d9FxR{bc*w#RV(i)33lWE-cAZK05D0b=3*4_NrBpMk*d zTa>0m4E^^GI4-Xi`-?S2sUlNhrqH-0rbSf%-@@3e&gV2GH_2c0oO9v7_0)eClJE zo^Rl{J(u!N>W6o1tH;u0X&8L6!&a&xsg|6TSct3O{5Kc&zY~P=ExH1`=JkA5lLODF zv42Bx{qZ`}8bMiL0Wbe`;rfzErP`%B@bls4Q%7VPlH>=SL8iCg_blI0akgsU`yJKX zT1s7d!zivtV-l~HKd^bMU~EhOmMUSC$X3LI>)>4I!=vFkhPKpMx>p(jS22(euaKgZ z=ubVD@)<__w<*%G1>5((Ka*|6^0!@}KmMb>HksGaU&~TIq@g|dNn@1{uhXp`2HD1k zYBn^0F+EbjTRn!o*Nf8KuqUe};lW<*axqW5MI?sv z-^sB5tqa@8nQzx*!W+&lZ#Y}OxRzJ#{W`y(Z+_4#z0z)_>ZN1be%kMUNd5dhbwV4| zQl|3mVCC4VYt?!+$uI_oa}^t@yzr)czEGeb5VzzD;oA$K58ncxaiG98O(}e~kRkPl zs~BkXgq{ffr~B}ZE%>qj{*P>HuznQQ+M8}KK6qu6D)Y?9Uw2Gze@2J>+nV|1wbqbr z=8$csjmA79$hOhVy&>C@3AVk4Y?J@DImW-q{r)G5+6Mi1DeUbI7e$G*g;8+za;KmI zK6|p4|CHam$)vE*_GEj{`!)A}(R}Y#yR+_ieRx9>H={9^SJ;%x&uESj__x>z|Bt=* z0cq=t{sbu^ZjmBIq=+lU5!V%wA{}u$VjV~Pi6gG-IwB(CcBD9th(A|cM@mOToKA7Y z6%lb=SDYfQE8=!V9M=`UeyvTdrZJ{5CNakRGsc+2{C#VA;yM%77XviHGtt0F10%aGzs0H5hnGjBo@#Z{wvsr`;OJR zd)7SwtJo*-rnKa$hyP+f9_?(!wy+pSE?R4Uj?~!ltoO_uQp~2F?YaYd z(d+JLcOmRz5N}W0U=vAbzyf3V-()(4_%nS+kk)?_>+elhh}oup{bBABp$P;I51#`e zxi_|ZO1uPkbLzVoH_n+>VQ*7lmBQ~X8~iec9M533ah%N|{%^O{f5jaL{X{(hy=pI> z=$vt8U{hF};{{lzrNX=UTbtJQaKG=r|KnR?wr^km{=e0X>iGuL7;nlo*IGKP{k9Qg z#NLOtI*PG-*aqlT5wI@QftAdZO9!@vwP2$*?OtNM{w^N@6uIsU*c2m`5Vt{OMux{ z2RqRNwozo%J^=eq#aIls@mB!U?RQ>#2f}-zdhi6W{!Yclv1qV9*+k>)HOPo<+lun~ zO69(o`>o5ZJEtmrrlH|t%*Zz3mNaV#_=~jIo6$PQ6D$&2b2eV?ja9qmVQ=g)`1=uh z)eZ3QA=Y!0W#Qi+!~Mr<9y~Bx-9_$uVCnh7m59&c>CR!yh-EtJ&=>YDq}f(#jWutW z>Mx9jwzEk|syO{{U0I@v{{BhnRvAwtHC*k8uF4Q|v1lqWbF8_xA%qL-aKxd;R-GMq z0zT_XcJ;ZW@b**#y8IdUI%vZws}xEf{_9}4{|FM^7KHr?wf-jXJLqiF7Qegii@APf zdc93kgiv@GzPcmR>hzCcwv8LPCbuOWW?KXJ*0sQF%fX_q#gf~~AAbkJd7@1a*L!UL_`7|xZwxe6ga>A+JI*D5o#qUD1cuN|zk07rgszHjGXKKAb49#Z)hgOmCuw7ZHittO3ejI<9FXkxCs!nycre2$; zyRR2t44JacEWq$FM1n>-?m65TAH40e!A~;>G+;*9(QJbEiTNwPEW*>CjF7``m%>o~Um8fpZe#Y79GYM=ko+j@omPR@=CBlUG%G7zl5_2k#&Mi+WL)pl>s5 z!+v>-dC6k6B_l;(BVGaP@EFk0y~N|dSLB|n&BcOvDJ9_HGXd}99M5_#uvQiKxf?%s zG4NP*fc7B~BA^VxP6Y?A1UsRJ*mK7Sx?o>MrfuEU0<*#N+IZLS67ZWV+7u5dFs1Pa zCblYG9j}Si-Z|Ik*Dqd~vS1xv261mtG}V!bC4)WD0$znHz$#_Q9p%x66Z_DlHk{bS zV7@<5yBI(BI0gUf0*|2g`dbG3yFq4~HtyZMEBy8+{L$N^+4dI%j=wBZ3~F~e&(?H_ zFxxoBWK*ko8D?7w%(hCjBKUvrw&sW5fiHQY9%4fd6g)#_>_+=z)onmJW*5g=4`F7ZUvQ1kX!G3Vgm|++@n>bt+ zO^euyJf%jdhR=^l#5av8Stq6zkOs9(Dn1YL^G+G-QpQ_ynQi!O&K2@?DzcxkqQ zHar1FtrtdZ&GXCqYiYv+k6>W@4lvua5$~dyFdG1cug7JoabPK$dr9q-o#me|=o1XR z7aCKMdCa1M*;Zt)`Qa#a`-Q>oz?VExiP#``Usj@1;5(lVzL!1LDc+OPgJ4+uz4-t0 z|8y!juhj_*r578f40FFlYD+@O?TruvuMTVrG3N|kfN!~;KukUzcx*oh8x6ujjka#^ zw@L|icYZE@u(7Igr@9QT8CMl}TD^24&MIumAx6Pw4?%2v>!_vBBsSImy~~$=g#lV7Ll&DGR1{uuGXnV&JX(m16|!byk6w z=CmsfA^~W@zo!94jnt%&{yo2jCMDPiGWFlo`a8gEJ43wio5ouTm;G~EhN7sPlJzZ9 z{L7p@eO3?AZp4Oim~D^D1F&LBMsC;OKMOL1|3pnV$tS7;ePh?yQ;F*k`muq$kog zJb3)lpJhaX{~f`{Djh7q7hKO>5jaF|g$Py~*d0d-nrTl(BCTwQUKM8?zPNLyIh+-} z7Oot1D!EsQb43w6?-DKb^%MvCyy{iaq3k@FhjqAH7h{ONFoJDiomGoeK@24hSf#8x z2k<21vqyaVArQE^VEosjLlf1ZPmyC#G+mXKg}Lzk<*C3az(;B zIoll%Q9`;s@8qPvHmwc+J7GcavC43vu4S+wjD<)wDb7Jm1re;u&}a5)B*zwISu$4` z1s8?dhldLUev5OIgUSs^>z*q?F=-~3tfT7b8k%CZq@_tNB}L^#{k(ij*?%I?#K4|r zwV~QrV2-vd*$R+3yV;R|J%sq-Y`gGaPvFxHBZ!pU15KSl-gRY{k){V4!@sT{^jZW2WM4UTsLXRz2KW_&3uS_^$JysHe*qzqpA`cZx&e=ki+@UhAO?~gTC2SjRLgh(}WV1ZKV z=mZOteu!~UU_s5JriT{;+PcFH(YSD1cu!_lE?=dkD~jfk6j?)Dmmqo9`j+Whx)srm z{4lRe3Yok^$x=VmJlBrtRt)RL8FPm<(KcTR=YzG{4(ynpLU&gQ!M0qDbH zbsL5z<6YCpH@r8uU46|R`1&WR*w$q2<;DNi_)TE=e`1|(o^_w+=}^P8vBV^?)L7S% z`}ShA!O?GeX#)lnj858+5{gf0>3w0-L6bH zDX9)DPzu4)lY<)V7^1OlSzlQmnbwU@wB|!Zv?auZsRtUxE9JOX8e^2kz+Qx^B`C7w zO%e69ls8SbW!93?-kii^vm#gZ_B0-}U^9C51rN00FCl*TsND*`yxGoVrxrXkli}S- z2R>Piu%AizWKr=7nfsZ49|R9Rf}!x+GuzGtfZzVuN>ld|Z_1S>n@oxJrb;s#-##>4_DUSaS% z_Se;1KUNzMJtGSj;?15IvWu`&@eqAvPeY=u97`9mgLz&gi500uNy5^DX33(mPuUNN zypj}?COA~;V@OdvI;Z;aZcUd6NvcHUm#t-^vMPo6r22G2n*iGIPGhIJ8a%TW?5PmL zbR0&l3(tjjryAFqI|jUz$OvV$Ww^h8=3h59;eGS*?|-8ilPCf46Fc-tlL zy9e<54h^gx6lS6;P16)5?$4oWLg#*M|ENBWJw?ve&-?TxhVcI9w^HBd4t(ts1$Lkw z{C@Cr;5YFb@$ddnw`lUUap!9N+(m;a+B{)RuysSkE4L#SVx{Pu^Kgcb1|r|FJn!U$ zWjGNhh0v=0J@zxNA1eaCv*8SfPB=rY4dY?)j#9K9qO_OV5-c2ZlhJ%WD#;Q@iHd}s z2NMVBiV9_yvfC$h1>})Zq%^Y)lcr?|F4+%7`O95>uR<-IliycPfHu5etI#K0WScTA z(eU3^9R#~f1g?Mt$MwF1cT%EPqswlV@G-jBQ<7hX4Gl@axBl#BUiy*p7Dg zVp=T6(S{D$N0EMr0iI~yG?kxQCH3Mg(M#ddfkNOuOjEX9ruHj};&|tDeY=uOZIBZm z`s&?+DABDbXwR&Xt|KYB>@69bzj}CI&C$$hU+e0=7G5ECUdGp%K%6I@@Ps{rqG@AXzpkm zbn^y@QDf%9erBe93e{mT&bzP*?{_7-2Eez6@W|@F+`;f(h5wwR#+;|2M=-Gdgj|1j zpYc5FZyHf)${Ta(QeX*+OXa@u1fQxSX=aI(_HecmPoA9?o;^B`Vg5I7=^u6nZums~ zeE3bE_-5RR`jmgRaXzc>xhR2!^`tc&q9&Q)B$z_1z?tM!0UE@6(BLt|Od?JYAXec& zyDmKRSdsW0=Nxm!V9kytRAYDBBWzkY1GB|^Pd6c%5O;_;qIhBXLAPN1pk7(1tW;Kb zQsoszmi@1^WQC6`SwmeZIl2eBZ~r=~uM!C+U6D7b?y1Ml?w_~n--3mj0QN8%5F=XY z=)*=q7oLYp;Ox^4h;35_y6_1&|C2n)-rFwxXWq>WJ%SW|PhNUkkos?L{HD>Cq9}A_ zAghP;=KD)~*Ha}br{w)}LgGqFB0SNa3eGlv7-rpm5q$^3f1)V=9j_^|YnH%0eF7yU;nu8l2_T1E;g{!9SB&g)@D7e&$tp=&_>kd%_uy zbvZbWXf)lPjilS|Sk}$Yb!N$`cu-^(6$oD)tO^wS_+f{#gpo=abcvEEksNP6*&;k1 zvwi7hZ&a_$Z}!mA?xUPzqvFj;-YHA_MAv87G)f@W)fkdzUvxxZDRBC!0Urlz{!ve? z!bVT5Lh9rmB37ZV9TEMM|14DK5ex^vef1=C`3h70A@`EwEkTyNc|n^7tyulk53{Xb z)BCgB=HKY=2>*%tTjBRpOVRmJ*GcJV@maxnrasP~HO`rvJf;hrR+%;iZPD_~o*<1Pt+=1|)sJ|J06MB^YyGWIwPShl7<8%=a z&3M7oW=XKlfR{)cIt{Dv1vm-00GEPA$`+sj5%q5R>ePoo7AZeqIIjJQ&|}5K?*WIw zk%P9{2asWiB3o&R(Z7-C#4K?RXuuI+{z0c;YJYt{Td@FZT}Zyx0ygm^EJQgsG~B>ZEI* z;M>Y?eFwsSqW*^X{nYaE|4?8m_i5x=gcj8)3~RRg))7mMUL(Q95^;@aTa+&BI@l1P zf~5T?2gAyQ5GgT4eS$KyM5yl|>l2Bd4u=oj(k*!p_-AgO6`d~{?ie%8x!_?hu-Ab# zzXmKpS$L-_8vKoZ(EIVo#iaOrBg#zB^I7ncsTG+@!zIBj}6YDra^=@&eT`r98g`>==dn@FVWI7%cxYL=Ujn7C#q*8Va)Z+P z{chlG&pvk`{3q&fir<7Dh25pFs&KF`j)YiOMgtf8GoM+bY=dwHOunNLYletParla> z0Ai#NaZ6tS8mwQQp7}HBQ9_TEKYowfg%G6c9_(OB4bP9F#5!@e_^zm1s1oKK^a-~1 zk^Ojnn&hQ2;^#`zMA5cn?6_7Dr&>9!IOFT`4gJPt^E&Ki7TZ~lc^1=Ac5aK;D1TkpAhSB0>24wX9~Y*JSDI+F#gh9 znxfl%9mVX4r2RS2vt(9*(EsII>bu^7@SZ4tkKW5Sx}VklgtP^xum9IGMYJjobm2+b zL|v@m&V?8(QfjScq{+UHB3Lx$hPb8Ot~fYLm+#`-(8ly99?iIFokm!Z-G;LI$eB-HCyL7r-QHqrSvG2 zJ<896;GswGTKv{Vdil=>LSb@!?2WE8%F3W(E|i_Gy7yk3hT*;CS5zNkGZHcy|8rH3K1z< z;;{-R^e6(b16_9C1FP^JIKh6y6KR&RM}a_;Zc2}G_0a!iMCcLx8vG7|q5+YZKvugtfe(w~q*zO`3PIdk$Sf7xCBoxV7{C+Zi#@28f3e=KWLbf{jcdo-`W!~UhA z(O77{Z`rifAZ#?zQG!)D^YOcIdS(usMdpTBvoFCOW%JUn$6t@~}4$on%2gFK!j5fCb8Pp+VSqAUMbp zyxQO5yI}?XgqJU0R;(-5;NzQUULcB;rfZ0@cZuqc;0CUtBz)`d33^1OlKawad6z0h zy$1FuD~1Qg3Ui~i0*SKsqYCVTd-6*(=p`o8U$^&JrFwoB$62=9sdMezHnXb;zR$C(f@Y^(f3Y za-4Z9zdIdztggfFZi7M6C4M0;7RSK)yIzP2dk*k}MgdC@yI;wF^?8=JzQJlL(*796Y!hQS zC&8~r>#7Ic7cpjo?nTgj(VB3|qVTWQgohr%(D+TFClfV&@s!3;lJc&nYN>v%lc8kk zJaAvsZw6iKp*Fb38Q*-V^o9;P+EYsZu&E8&`~~M${vk5$%w!-_T{OH$S$h zZ7&f6n&)VO6TGVNhxody(Dl}R7h+t!22D8I6K9r+v_Zd-|40N6JyzG_w^mXtE(7g1 z3i|Ix(LLd$Fzw*YLA0P@f01wIr+hBrRqs7hj4LJrB!WxU1g`gPL&;EipjzKLs!Zk7 z_d(nJ24RL+b2JGtu8@;K%|q?DJ`yzHFD;!OO?WY!Sv}xv!87nJR}t6{MSI4KumTmp zsQpZu@X#X|3cn+~R@W3>QaOdQbeX28y1+S8j``c*Elo*LIZ;c~G)1*qU0;<>|K0%m zo&WdYJyE|9em}MR!@reg$x;;wst4*=&3$c*?yercP?=UOUA6>d*sgOVV_9(eivc3d zrnrVdtC|nzcg+GCFs+NpRrrrx&xIbV>+xGGxg(B-_$O@9sHj?W7j`i74>k{Szz@^C zU&8PEyuuUj8MtP}fKTd&GVvjjFYV2tT9VRK8=C9SA=)#!1kEg8w&kM-%i`9f)MLJ) z^Ca%{jW$^~Xi%8qEb&$yGKfYv3Sjqw(1g!`Md7#$gWZcR*vlkN&6HevW|ErlzjiM( z^azH=Zweck$Vu@C1SL__7fXH1R10QKwPeaNX{Icbre$dAs(w95n)RJA?04=yyeI0H z!tbY+g3}}Z@tXXta{6TQbnI;8d`SQLqTN&p-t}upfqfE{IuP*kTgThr)U_8b8JyqM z1~%cF9y?E|J zMWZJ*UFJlIpviI*gC^yCugrGLwp4p>d0*)uwfxG1yP_3I<8h39PF1AlomHK)49TGT zYO|Ij5%zAB=P+X$=L+70qu@;|guTobh%!r9qtKdgrcL-an?zxE6nX@|48McpCRsyY zX^LE-2Wm;6v9BBf*@0!f!2J5cpGilr!oEkhf?sk6!h52AIsATV`5*r4qf*$*j8(W* z2DM5f*79^)h85!^pg}U6z?cYUANPS)wHZ$bt!fF_7stWA*ob=-bgGJgaW?QTx$R#i z8+xp+$M0#e8&=&!G71I4x57?g)`8-n=O9inBDlBT#%J^6K9}>l_9nRVy9fW{6*1?@IEkIKAn#ck=E+=1|(s9zAjpIU1F=~0Rlmq`_ys(JNoT+% zYs`;eFS7&DpjnPuICHHSkH+U*S*{t_%d7;;h857M5*~i!UgnRqMhQJu*W>qy_^oi} zGK~wnh1J581L;BkLAqc?khi}8p7gDt1Gn;$_wu=wyH$!}NF|;tL3t;VPhSZtm~5by z{?b%mR4-KSCHdJbm=F~o>ZJYPm${_Q)vTUB(hpql!5W2c8??uxt#Iz~n6ndq48OXi zt`#`xl(ZmXU+ra5=j8nrHlU$LFns)`5R=YHd?l_->}1lkM7R{un(QH1Ka@nUdg7Y^ z?>BRgQoadd-FCvf1K~YUza)M?wfv+0=$=$2TTzUudeqID8f}@bz>sB3GNG2YVA-&Y zMuCS&zB3i}GKb;R%uyE(r=a&iyiy(m!I^L5IP=zib2{``U60?d#I?e5VVUPsCM*!9 zLJX^|gWiKo0b5W9ewdZ~6@KjJTHeUsCRep<P-t+y}rfj zhuWiCuljEd3gJCbzbJk`wfv{rqev-FHm&GUJyREJva~6>I71Yi*RyTuwIw2hb{U*q z{}4_==i^=AA@UkxB^5%P@C8q76bcJ|6oQ8ytLyQ*T^wNMFMFftR8^|8;H-zJb40HK|BxBWb1ROt**77+RU~F~uED;T+x60=hB)EfU=dD4 z30HwvkAZP=*Y{n|g&wQx@w;9$bI|9NUI2P$AKVeF3K|6w`y=~tU`1&BT+AEXV{qPH!CBRlU{9D0 z?@ej&-ZbD!02@%$UF?44i3?0wqmc0sespY<&?6W=ep3iaAtaSkd=f={xnwyomu^Y5 z^_62vP&6S!$x%63=UwvfxVg7$@NfEy6W$Z`3*+}w%l~IQN|GvM^NN>%;Ey!1XV|$4 z;)E}ohApL5HBtj7*UKCROzND2IN?UHY>+~{(wBe+KN`V9kJa_~T`np=NcTz^2Z;v} z0-<11P$P)ip8^|{C4Tzn0iK4Jx!24c+g;{tecDz;ewP##v{&C6-!l9?p9O+8K=4s% zvTR0~b~34n)xOk;FOp3KmPb}2(qmUS60vk=oKtf3-c$h5t>)Y;cynTU*8f-p4?Tk6 z<2Mr>gRPbW<0!KwDdBzft@kZc(!uydJz3{1;rT1~7mkv@DT4jporU*A{nGgT5C2M( za8!64A=^?6s_NBGG!NjU2bq2w&Rgq)Q;!8mseJM|pZO3AWpGct7soBwt>d(P zYL}b8Fd6;i{W)LKC)wJokIx!GuPE+tQkp88RwhGy!U*ki-NwaTQ<^2t%10XPY)2IK z0Ai!C@m7d2+YJ`sxgI~PO+avOYm^`7(-V3G!^dw5MJcSL%S_4}L76q)^mV@gRGoKU zB$wt96kQ@|X8H2_){_sDkBWY8(EW!0`|zHqUmCxkTAKgkA^WJ~I8$a&EU3EF6`C~A zqG>T$!+B|@Y*v|cGH{A&t8{M-h$$9aE!U9>`x0u#A}}*(zMg|cc!x^? zK0PlyUYWt{3BTdQ3fNWX5e$vrOiX0bBt8^b??wTT6_#D|&4G7I=rjc?AB4nzl%r+6UG&lIeQ z3c(hoK(-88l)+Q0wp{ndpfN?54c2)i-#!hRFDs^YE`a9C;A(YA+*$D6M4VFn*C2T4 z5e$vrG)^)x(i^pD85%j6(t+v;-gPo`n?OkJ{rzOgx3{bE?uz98(bAVf)vbJhI}qLz z^~>Y;zxcquC#jQ09M8+2E0a_T^{l2tTM8c9kry)4oTbL9K`KFuvh6|efhz>h_@M|M zdaSO;@8|>V{xoR0r{GE@f+jwrfZi4R34A`__ruR~pC9w4c=z_|VFj+)jpt;4Dq@$) zS0SxkrC>BI5r|^8TJ!Wr^y=MnpmsFRs|S9)KqYECR7-1R+sf>du~Q6eK*tR-BWhMy zCy~eStILNIkHyXzyaLy_n&6DnOn3Vwg3~7+GcAY$Op!YnfqV8rM*l3(${kScp-|BJd;Lzv-|q{b`;(d^~>Y;Q%lqTa`;HH zBCVE1$QPAOC#h(?MwvL4PMG2iu#?GmNFZA25MF>wK#Rirp=yd-L{k#Ave?K3%nP7dQ|O96!fc7>bR{16)wj8)U=2?C^pI!P{}==hJ%UX99&}h8UguxJ`Dqah zU}Rz_lV*qnAxUW(=Yxn{vI#l51^rDlbzOUZDd2pFILw6ER`dG;?>G9NhxbJN2KfEd z@_%X%$BvSvtFmW`80Gp&$7!|()3S8Kh6ZCcc=Sxz9wOcL4OHXMVqE8#2MroLdhpI5 zdhl@tyh}W-Z0@$@gXs$HD)3&F>!FGQQ*vg!e@K7WiHM&xEO<1s^$1 zm$4P?s-zSC>A+dpd8|%im^HS6S3TQSj7)(?&mD&wlffG~abiHTOAI>I7eB-nCG=Qb zkKbT>!hdmXYUY>o(>@zM&wj1~Kl)yXW%ZJ);^ywY;;=rMKgF@rb~5DMp425L6H)5A z{7^J0L6!(9e?C)!rfG_7LzZcprikxkl=fo}UWrh6Bj1pxC|{m%z>2>f^xzVs*37ZK zMjqH-pvw*p>^={J=1b_Rg?MCXfZ#-QLZ)B+_g{mD9>MVNo4`kJjPx!As`Zs1G2Wlc z#99B^;J*w0vVnWLl-sy3ZUim($n6OaUjo;Dvf({ZzXg6jwft|Rhl!E_Y3%Woya4tw z`_;Lp#RiU_al~WSkmIoj|K8Swp~uP}zbEW6 zdje8qYp{0fHv}*E@%#tZrn{eYpEo}D!C4Mw-XJe&Z)z`r+r2B^P2tpk8e%W(u(sLq zCZE*oQ*M&e9Exhb;!zy37w;02N7Ya{{jD_TirTL}V2Lv&gHnUMSUGy4(PU^_baS8w z*P7YZS4gbA9Zsy@_Uu0A;(S*PoUN1!(FuJKoOw=<-&FYqjL;((9)2^Ckx2)~Q12Qh z3j5YD`y*)Ydf!^A99W9x(iGKVjk9+65Jv35@$Cr@U;k)@_eA|B_+9Z|3GN)u9K}n= zWa;t^W&KH%`mLs38-LE%4_%a*qRmUz0^2+|UO#f=!x;}Ta8_o!E6Ua5(!v=Jq)#Rh zVS_wrjk%h`zkYxiEklph_4qw6NccQ|O`7x6O@8kET=O}ZC*}3?()X74^0P-6Q!{(4w%dJxGkb6`hCZgsaYeII*7Rs)p!RqO7lNMtc@o>uAKPo#{?B-sgJY z8iZ4Nioipgii`4nyy`=b)%Ey2w!iYZ=qeR^DCU)_d-6GvhwxT;9lVsijlBx4fLpq| zxO<;d^J$d5wWHm+vmGTbkmm8R`%)+ zg!e@KHu(MT{)sr}uty@3mK|@)D-{aW3w8YI^4YWV7~Pto*_deBvebb!_+xt;oSQp> zbver}qa;nZ@3_l6u?e}1GZ(+@8vG_>Md(jn@7M7ZkYt`KK?O^E-ZE%Us(5jG>w9%v z1GjxwyIaAT`-FbVW;gEiZBKlhmS=d#02d6Kk1zEyD zvFhmY@eBF7(hZ)pt=dT)$AE%0A=iW8&FHLS16y|X0fKWc5xn^aB6xTs*n`J8C!L8{ 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

    jdexJ7S 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%0DfzP+eMqi>mh@1yNn}0qNrACo0c@kwp7CNrmgK2 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 Date: Sat, 22 Feb 2025 02:01:35 +0800 Subject: [PATCH 11/11] update --- .swift-format | 2 + ChatMLX.xcodeproj/project.pbxproj | 295 +- .../xcshareddata/swiftpm/Package.resolved | 20 +- ChatMLX/Application/ChatMLXApp.swift | 28 +- ChatMLX/Configuration/ChatMLX.entitlements | 4 +- .../.xccurrentversion | 0 .../ChatMLX 2.xcdatamodel/contents | 11 +- .../ChatMLX2.xcdatamodel}/contents | 0 .../AppleIntelligenceEffectManager.swift | 1 + .../AppleIntelligenceEffectView.swift | 4 +- .../Core/Components/DefaultModelPicker.swift | 48 - .../Components/DefaultProviderPicker.swift | 50 +- ChatMLX/Core/Components/ModelPicker.swift | 51 +- .../SyntaxHighlighter/TextOutputFormat.swift | 8 +- .../UltramanMinimalistWindowModifier.swift | 20 +- .../UltramanNavigationSplitView.swift | 182 +- .../UltramanTextEditorWithPlaceholder.swift | 6 +- .../Core/Extensions/Defaults+Extensions.swift | 2 +- .../LabeledContentStyle+Extensions.swift | 2 +- .../MarkdownUI+Theme+Extensions.swift | 5 +- .../NSManagedObjectContext+Extensions.swift | 13 +- .../Core/Extensions/NSWindow+Extensions.swift | 2 +- ChatMLX/Core/Extensions/View+Extensions.swift | 2 +- .../Conversation+CoreDataClass.swift | 8 +- .../Conversation+CoreDataProperties.swift | 11 +- .../Models/CoreData Models/Conversation.swift | 53 +- .../Message+CoreDataProperties.swift | 2 +- .../Core/Models/CoreData Models/Message.swift | 9 +- .../Models/CoreData Models/ModelInfo.swift | 2 +- ChatMLX/Core/Models/DownloadTask.swift | 94 +- ChatMLX/Core/Models/Language.swift | 2 +- .../Models/Migrations/V2MigrationPolicy.swift | 7 +- ChatMLX/Core/Models/Model.swift | 6 + ChatMLX/Core/Models/ProviderModel.swift | 76 +- ChatMLX/Core/Models/SettingsTab.swift | 5 +- .../PersistenceController.swift | 88 +- ChatMLX/Core/Persistence/Repository.swift | 91 + ChatMLX/Core/Services/AI/MLXProvider.swift | 313 +- ChatMLX/Core/Services/AI/OpenAIProvider.swift | 210 +- .../Core/Services/AI/ProviderFactory.swift | 83 +- ChatMLX/Core/Services/ChatService.swift | 42 + .../ChatStrategies/ChatStrategy.swift | 39 + .../ChatStrategies/MLXChatStrategy.swift | 106 + .../ChatStrategies/OpenAIChatStrategy.swift | 19 + .../Core/Services/HuggingFaceService.swift | 43 + .../Core/Services/HuggingfaceHubService.swift | 45 + ChatMLX/Core/Services/MLXService.swift | 66 + ChatMLX/Core/Services/OpenAIService.swift | 37 + ChatMLX/Core/Stores/ConversationStore.swift | 166 + ChatMLX/Core/Stores/DownloadStore.swift | 16 + ChatMLX/Core/Stores/ModelStore.swift | 61 + ChatMLX/Core/Stores/SettingsStore.swift | 15 + .../Utilities/Huggingface/Downloader.swift | 142 - ChatMLX/Core/Utilities/Huggingface/Hub.swift | 222 - .../Core/Utilities/Huggingface/HubApi.swift | 359 - ChatMLX/Core/Utilities/LLMRunner.swift | 494 +- ChatMLX/Core/Utilities/Logger.swift | 1 - ChatMLX/Core/Utilities/MarkdownMetadata.swift | 97 +- .../AppleIntelligenceEffectViewModifier.swift | 33 + .../Conversation/ConversationDetailView.swift | 343 - .../Conversation/ConversationView.swift | 33 +- .../Detail/AssistantMessageView.swift | 110 + .../Detail/ConversationDetailView.swift | 74 + .../Detail/EditorToolbarView.swift | 119 + .../Conversation/Detail/EditorView.swift | 78 + .../{ => Detail}/EmptyConversation.swift | 4 +- .../Conversation/Detail/MessageBoxView.swift | 50 + .../Detail/MessageBubbleView.swift | 31 + .../{ => Detail}/RightSidebarView.swift | 0 .../Conversation/Detail/UserMessageView.swift | 51 + .../Conversation/MessageBubbleView.swift | 220 - .../ConversationSidebarItemView.swift} | 35 +- .../ConversationSidebarView.swift | 37 +- .../Settings/DefaultConversationView.swift | 67 +- ChatMLX/Features/Settings/GeneralView.swift | 33 +- .../Local Models/LocalModelItemView.swift | 8 +- .../Local Models/LocalModelsView.swift | 205 +- .../MLX Community/MLXCommunityItemView.swift | 51 +- .../MLX Community/MLXCommunityView.swift | 154 +- .../MLX/MLXProviderContentView.swift | 70 +- .../MLX/MLXProviderModelItemView.swift | 48 +- .../Providers/OpenAI/OpenAIProviderView.swift | 17 +- .../Settings/SettingsSidebarItemView.swift | 14 +- .../Settings/SettingsSidebarView.swift | 12 +- ChatMLX/Features/Settings/SettingsView.swift | 8 +- ChatMLX/Features/Theme/Theme.swift | 16 + .../View Models/ConversationViewModel.swift | 38 - .../ConversationViewModelOld.swift | 38 + .../View Models/ModelManagerViewModel.swift | 68 +- .../View Models/SettingsViewModel.swift | 4 +- ChatMLX/Resources/Localizable.xcstrings | 7044 +++++++---------- ChatMLXCore/Package.swift | 2 +- .../Sources/ChatMLXCore/ChatMLXApp.swift | 4 +- ChatMLXTests/MarkdownMetadataTests.swift | 42 +- ChatMLXTests/OpenAIServiceTests.swift | 17 + ChatMLXTests/ProviderFactoryTests.swift | 7 +- Makefile | 2 + 97 files changed, 5986 insertions(+), 6987 deletions(-) rename ChatMLX/Configuration/{ChatMLX.xcdatamodeld => ChatMLX2.xcdatamodeld}/.xccurrentversion (100%) rename ChatMLX/Configuration/{ChatMLX.xcdatamodeld => ChatMLX2.xcdatamodeld}/ChatMLX 2.xcdatamodel/contents (87%) rename ChatMLX/Configuration/{ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel => ChatMLX2.xcdatamodeld/ChatMLX2.xcdatamodel}/contents (100%) delete mode 100644 ChatMLX/Core/Components/DefaultModelPicker.swift create mode 100644 ChatMLX/Core/Models/Model.swift rename ChatMLX/Core/{Utilities => Persistence}/PersistenceController.swift (60%) create mode 100644 ChatMLX/Core/Persistence/Repository.swift create mode 100644 ChatMLX/Core/Services/ChatService.swift create mode 100644 ChatMLX/Core/Services/ChatStrategies/ChatStrategy.swift create mode 100644 ChatMLX/Core/Services/ChatStrategies/MLXChatStrategy.swift create mode 100644 ChatMLX/Core/Services/ChatStrategies/OpenAIChatStrategy.swift create mode 100644 ChatMLX/Core/Services/HuggingFaceService.swift create mode 100644 ChatMLX/Core/Services/HuggingfaceHubService.swift create mode 100644 ChatMLX/Core/Services/MLXService.swift create mode 100644 ChatMLX/Core/Services/OpenAIService.swift create mode 100644 ChatMLX/Core/Stores/ConversationStore.swift create mode 100644 ChatMLX/Core/Stores/DownloadStore.swift create mode 100644 ChatMLX/Core/Stores/ModelStore.swift create mode 100644 ChatMLX/Core/Stores/SettingsStore.swift delete mode 100644 ChatMLX/Core/Utilities/Huggingface/Downloader.swift delete mode 100644 ChatMLX/Core/Utilities/Huggingface/Hub.swift delete mode 100644 ChatMLX/Core/Utilities/Huggingface/HubApi.swift create mode 100644 ChatMLX/Core/View Modifiers/AppleIntelligenceEffectViewModifier.swift delete mode 100644 ChatMLX/Features/Conversation/ConversationDetailView.swift create mode 100644 ChatMLX/Features/Conversation/Detail/AssistantMessageView.swift create mode 100644 ChatMLX/Features/Conversation/Detail/ConversationDetailView.swift create mode 100644 ChatMLX/Features/Conversation/Detail/EditorToolbarView.swift create mode 100644 ChatMLX/Features/Conversation/Detail/EditorView.swift rename ChatMLX/Features/Conversation/{ => Detail}/EmptyConversation.swift (78%) create mode 100644 ChatMLX/Features/Conversation/Detail/MessageBoxView.swift create mode 100644 ChatMLX/Features/Conversation/Detail/MessageBubbleView.swift rename ChatMLX/Features/Conversation/{ => Detail}/RightSidebarView.swift (100%) create mode 100644 ChatMLX/Features/Conversation/Detail/UserMessageView.swift delete mode 100644 ChatMLX/Features/Conversation/MessageBubbleView.swift rename ChatMLX/Features/Conversation/{ConversationSidebarItem.swift => Sidebar/ConversationSidebarItemView.swift} (53%) rename ChatMLX/Features/Conversation/{ => Sidebar}/ConversationSidebarView.swift (71%) create mode 100644 ChatMLX/Features/Theme/Theme.swift delete mode 100644 ChatMLX/Features/View Models/ConversationViewModel.swift create mode 100644 ChatMLX/Features/View Models/ConversationViewModelOld.swift create mode 100644 ChatMLXTests/OpenAIServiceTests.swift create mode 100644 Makefile diff --git a/.swift-format b/.swift-format index 962ead4..a4334f0 100644 --- a/.swift-format +++ b/.swift-format @@ -3,5 +3,7 @@ "indentation": { "spaces": 4 }, + "lineLength": 120, + "multiElementCollectionTrailingCommas": true, "spacesAroundRangeFormationOperators": true, } \ No newline at end of file diff --git a/ChatMLX.xcodeproj/project.pbxproj b/ChatMLX.xcodeproj/project.pbxproj index 08396a1..d9f3c11 100644 --- a/ChatMLX.xcodeproj/project.pbxproj +++ b/ChatMLX.xcodeproj/project.pbxproj @@ -3,25 +3,22 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 5201638B2CBC2D8D00666E82 /* V2MigrationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201638A2CBC2D8C00666E82 /* V2MigrationPolicy.swift */; }; 520163972CBD6B4B00666E82 /* Conversation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163962CBD6B4900666E82 /* Conversation.swift */; }; 5201639B2CBD6BC400666E82 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639A2CBD6BC000666E82 /* Message.swift */; }; - 520163A22CBD6C4900666E82 /* Conversation+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639C2CBD6C4900666E82 /* Conversation+CoreDataClass.swift */; }; - 520163A32CBD6C4900666E82 /* Conversation+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639D2CBD6C4900666E82 /* Conversation+CoreDataProperties.swift */; }; - 520163A42CBD6C4900666E82 /* Message+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639E2CBD6C4900666E82 /* Message+CoreDataClass.swift */; }; - 520163A52CBD6C4900666E82 /* Message+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5201639F2CBD6C4900666E82 /* Message+CoreDataProperties.swift */; }; - 520163A62CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163A02CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift */; }; - 520163A72CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163A12CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift */; }; 520163A92CBD6CCB00666E82 /* ModelInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163A82CBD6CCA00666E82 /* ModelInfo.swift */; }; 520163AB2CBD715900666E82 /* ProviderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163AA2CBD715900666E82 /* ProviderModel.swift */; }; 520163AD2CBD876100666E82 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520163AC2CBD875500666E82 /* Common.swift */; }; 5207D8D42CC6846F008588FA /* Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207D8D32CC6846F008588FA /* Provider.swift */; }; 5207D8D62CC68BAC008588FA /* DefaultProviderPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5207D8D52CC68BAA008588FA /* DefaultProviderPicker.swift */; }; + 52212ABA2CDFA96100A54AA2 /* OpenAIServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52212AB92CDFA95D00A54AA2 /* OpenAIServiceTests.swift */; }; 523D8C512C9C7FCC0092791C /* Transformers in Frameworks */ = {isa = PBXBuildFile; productRef = 523D8C502C9C7FCC0092791C /* Transformers */; }; + 525C50D72CD79EFE00F48479 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525C50D62CD79EFD00F48479 /* Theme.swift */; }; + 525C50DF2CD7B90400F48479 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525C50DE2CD7B90200F48479 /* Repository.swift */; }; 526675462C85EDCB001EF113 /* ChatMLXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526675452C85EDCB001EF113 /* ChatMLXApp.swift */; }; 5266754D2C85EDCC001EF113 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5266754C2C85EDCC001EF113 /* Preview Assets.xcassets */; }; 526675622C85EFB3001EF113 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 526675612C85EFB3001EF113 /* Alamofire */; }; @@ -44,7 +41,7 @@ 526676492C85F903001EF113 /* UltramanTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676072C85F903001EF113 /* UltramanTextField.swift */; }; 5266764A2C85F903001EF113 /* UltramanWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676082C85F903001EF113 /* UltramanWindow.swift */; }; 5266764B2C85F903001EF113 /* ConversationDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266760A2C85F903001EF113 /* ConversationDetailView.swift */; }; - 5266764C2C85F903001EF113 /* ConversationSidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266760B2C85F903001EF113 /* ConversationSidebarItem.swift */; }; + 5266764C2C85F903001EF113 /* ConversationSidebarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266760B2C85F903001EF113 /* ConversationSidebarItemView.swift */; }; 5266764D2C85F903001EF113 /* ConversationSidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266760C2C85F903001EF113 /* ConversationSidebarView.swift */; }; 5266764E2C85F903001EF113 /* ConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266760D2C85F903001EF113 /* ConversationView.swift */; }; 5266764F2C85F903001EF113 /* EmptyConversation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266760E2C85F903001EF113 /* EmptyConversation.swift */; }; @@ -72,9 +69,6 @@ 526676672C85F903001EF113 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266762C2C85F903001EF113 /* SettingsTab.swift */; }; 526676682C85F903001EF113 /* SettingsTabGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266762D2C85F903001EF113 /* SettingsTabGroup.swift */; }; 526676692C85F903001EF113 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266762E2C85F903001EF113 /* Styles.swift */; }; - 5266766A2C85F903001EF113 /* Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676302C85F903001EF113 /* Downloader.swift */; }; - 5266766B2C85F903001EF113 /* Hub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676312C85F903001EF113 /* Hub.swift */; }; - 5266766C2C85F903001EF113 /* HubApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676322C85F903001EF113 /* HubApi.swift */; }; 5266766D2C85F903001EF113 /* LLMRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676342C85F903001EF113 /* LLMRunner.swift */; }; 526676702C85F903001EF113 /* Defaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676382C85F903001EF113 /* Defaults+Extensions.swift */; }; 526676712C85F903001EF113 /* MarkdownUI+Theme+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526676392C85F903001EF113 /* MarkdownUI+Theme+Extensions.swift */; }; @@ -82,15 +76,29 @@ 526676732C85F903001EF113 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266763B2C85F903001EF113 /* String+Extensions.swift */; }; 526676742C85F903001EF113 /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5266763C2C85F903001EF113 /* View+Extensions.swift */; }; 526676782C85F9DA001EF113 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 526676772C85F9DA001EF113 /* Localizable.xcstrings */; }; + 5270B9D82CDFBB550029E3A4 /* Conversation+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9D62CDFBB550029E3A4 /* Conversation+CoreDataClass.swift */; }; + 5270B9D92CDFBB550029E3A4 /* Conversation+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9D72CDFBB550029E3A4 /* Conversation+CoreDataProperties.swift */; }; + 5270B9DC2CDFD2090029E3A4 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9DB2CDFD1FD0029E3A4 /* ChatService.swift */; }; + 5270B9DE2CDFD2C40029E3A4 /* MLXChatStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9DD2CDFD2BA0029E3A4 /* MLXChatStrategy.swift */; }; + 5270B9E02CDFD2F20029E3A4 /* OpenAIChatStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9DF2CDFD2E80029E3A4 /* OpenAIChatStrategy.swift */; }; + 5270B9E22CDFD42B0029E3A4 /* ChatStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9E12CDFD42B0029E3A4 /* ChatStrategy.swift */; }; + 5270B9E62CE0956C0029E3A4 /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9E52CE095600029E3A4 /* SettingsStore.swift */; }; + 5270B9E82CE0AB6C0029E3A4 /* DownloadStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9E72CE0AB620029E3A4 /* DownloadStore.swift */; }; + 5270B9EA2CE0D0570029E3A4 /* MessageBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9E92CE0D0510029E3A4 /* MessageBoxView.swift */; }; + 5270B9EC2CE0D2100029E3A4 /* EditorToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9EB2CE0D20B0029E3A4 /* EditorToolbarView.swift */; }; + 5270B9EE2CE0DD780029E3A4 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9ED2CE0DD730029E3A4 /* EditorView.swift */; }; + 5270B9F22CE0E6A70029E3A4 /* UserMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9F12CE0E6A30029E3A4 /* UserMessageView.swift */; }; + 5270B9F42CE0E7BF0029E3A4 /* AssistantMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9F32CE0E7BB0029E3A4 /* AssistantMessageView.swift */; }; + 5270B9F92CE0FBEB0029E3A4 /* HuggingFaceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5270B9F82CE0FBDE0029E3A4 /* HuggingFaceService.swift */; }; 527821592CB817A300638477 /* ModelManagerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527821582CB8179700638477 /* ModelManagerViewModel.swift */; }; 5278215B2CB81FEB00638477 /* MarkdownMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5278215A2CB81FEA00638477 /* MarkdownMetadata.swift */; }; 527821A52CB821AC00638477 /* ChatMLXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527821A32CB821AC00638477 /* ChatMLXTests.swift */; }; 527821A72CB821B800638477 /* MarkdownMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 527821A62CB821B800638477 /* MarkdownMetadataTests.swift */; }; 527F48152C9EFD5D006AF9FA /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 527F48142C9EFD5D006AF9FA /* LLM */; }; 528D82262CABE19900163AAB /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D82252CABE19000163AAB /* Date+Extensions.swift */; }; - 528D83192CAD491900163AAB /* ChatMLX.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 528D83172CAD491900163AAB /* ChatMLX.xcdatamodeld */; }; + 528D83192CAD491900163AAB /* ChatMLX2.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 528D83172CAD491900163AAB /* ChatMLX2.xcdatamodeld */; }; 528D831C2CAD49E600163AAB /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D831B2CAD49E600163AAB /* PersistenceController.swift */; }; - 528D83372CADB64600163AAB /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83362CADB64300163AAB /* ConversationViewModel.swift */; }; + 528D83372CADB64600163AAB /* ConversationViewModelOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83362CADB64300163AAB /* ConversationViewModelOld.swift */; }; 528D83392CAE51EC00163AAB /* Role.swift in Sources */ = {isa = PBXBuildFile; fileRef = 528D83382CAE51EC00163AAB /* Role.swift */; }; 528DBE2F2C9C86FB004CDD88 /* Transformers in Frameworks */ = {isa = PBXBuildFile; productRef = 528DBE2E2C9C86FB004CDD88 /* Transformers */; }; 52977D2B2CCBCCEC00DD4D2F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52977D2A2CCBCCEC00DD4D2F /* Logger.swift */; }; @@ -111,6 +119,16 @@ 52A689F62CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */; }; 52A689F82CAE8DA30078CDF9 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */; }; 52A689FA2CAECFE00078CDF9 /* ErrorAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */; }; + 52A7C83F2CDE78860072D691 /* ConversationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C83E2CDE78830072D691 /* ConversationStore.swift */; }; + 52A7C8502CDF4C390072D691 /* ModelInfo+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C84E2CDF4C390072D691 /* ModelInfo+CoreDataClass.swift */; }; + 52A7C8512CDF4C390072D691 /* Message+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C84D2CDF4C390072D691 /* Message+CoreDataProperties.swift */; }; + 52A7C8522CDF4C390072D691 /* ModelInfo+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C84F2CDF4C390072D691 /* ModelInfo+CoreDataProperties.swift */; }; + 52A7C8542CDF4C390072D691 /* Message+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C84C2CDF4C390072D691 /* Message+CoreDataClass.swift */; }; + 52A7C8582CDF6D3E0072D691 /* AppleIntelligenceEffectViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C8572CDF6D3C0072D691 /* AppleIntelligenceEffectViewModifier.swift */; }; + 52A7C85A2CDF92FC0072D691 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C8592CDF92F70072D691 /* Model.swift */; }; + 52A7C85C2CDF94640072D691 /* ModelStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C85B2CDF945F0072D691 /* ModelStore.swift */; }; + 52A7C85E2CDF99DA0072D691 /* MLXService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C85D2CDF99D40072D691 /* MLXService.swift */; }; + 52A7C8602CDFA3650072D691 /* OpenAIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52A7C85F2CDFA3600072D691 /* OpenAIService.swift */; }; 52B0532A2CAFDFB000E8DDBA /* ProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */; }; 52B0532C2CAFEFD800E8DDBA /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0532B2CAFEFD000E8DDBA /* Color+Extensions.swift */; }; 52B0534D2CB02C4600E8DDBA /* ModelManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */; }; @@ -124,6 +142,8 @@ 52B053A02CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0539F2CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift */; }; 52B053A22CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */; }; 52B053A62CB38E8700E8DDBA /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */; }; + 52B0C89B2D662B4800BDC5F3 /* HuggingfaceHubService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B0C89A2D662B3200BDC5F3 /* HuggingfaceHubService.swift */; }; + 52B0C89E2D662BCE00BDC5F3 /* HuggingfaceHub in Frameworks */ = {isa = PBXBuildFile; productRef = 52B0C89D2D662BCE00BDC5F3 /* HuggingfaceHub */; }; 52B9BD072CBE94BA0086C013 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B9BD062CBE94B80086C013 /* Bundle+Extensions.swift */; }; 52CDC3D22CC0208100627147 /* OpenAIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CDC3D12CC0207300627147 /* OpenAIProvider.swift */; }; 52CDC3D52CC020F500627147 /* OpenAI in Frameworks */ = {isa = PBXBuildFile; productRef = 52CDC3D42CC020F500627147 /* OpenAI */; }; @@ -135,7 +155,6 @@ 52E50B2B2C8DF111005A89DE /* MNIST in Frameworks */ = {isa = PBXBuildFile; productRef = 52E50B2A2C8DF111005A89DE /* MNIST */; }; 52EC64942CC3DC0E00A08069 /* NSManagedObjectContext+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EC64932CC3DC0800A08069 /* NSManagedObjectContext+Extensions.swift */; }; 52EC64AD2CC4C38600A08069 /* ModelPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EC64AC2CC4C37C00A08069 /* ModelPicker.swift */; }; - 52EC64AF2CC4CD5400A08069 /* DefaultModelPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EC64AE2CC4CD4E00A08069 /* DefaultModelPicker.swift */; }; 52EC64B12CC531A300A08069 /* ModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52EC64B02CC531A300A08069 /* ModelType.swift */; }; 52FD80BE2C8F21C2006C50F1 /* LLM in Frameworks */ = {isa = PBXBuildFile; productRef = 52FD80BD2C8F21C2006C50F1 /* LLM */; }; 52FD80C02C8F21C2006C50F1 /* MNIST in Frameworks */ = {isa = PBXBuildFile; productRef = 52FD80BF2C8F21C2006C50F1 /* MNIST */; }; @@ -160,17 +179,14 @@ 5201638A2CBC2D8C00666E82 /* V2MigrationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2MigrationPolicy.swift; sourceTree = ""; }; 520163962CBD6B4900666E82 /* Conversation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conversation.swift; sourceTree = ""; }; 5201639A2CBD6BC000666E82 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; - 5201639C2CBD6C4900666E82 /* Conversation+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataClass.swift"; sourceTree = ""; }; - 5201639D2CBD6C4900666E82 /* Conversation+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataProperties.swift"; sourceTree = ""; }; - 5201639E2CBD6C4900666E82 /* Message+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataClass.swift"; sourceTree = ""; }; - 5201639F2CBD6C4900666E82 /* Message+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataProperties.swift"; sourceTree = ""; }; - 520163A02CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelInfo+CoreDataClass.swift"; sourceTree = ""; }; - 520163A12CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelInfo+CoreDataProperties.swift"; sourceTree = ""; }; 520163A82CBD6CCA00666E82 /* ModelInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelInfo.swift; sourceTree = ""; }; 520163AA2CBD715900666E82 /* ProviderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderModel.swift; sourceTree = ""; }; 520163AC2CBD875500666E82 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; 5207D8D32CC6846F008588FA /* Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Provider.swift; sourceTree = ""; }; 5207D8D52CC68BAA008588FA /* DefaultProviderPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultProviderPicker.swift; sourceTree = ""; }; + 52212AB92CDFA95D00A54AA2 /* OpenAIServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIServiceTests.swift; sourceTree = ""; }; + 525C50D62CD79EFD00F48479 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + 525C50DE2CD7B90200F48479 /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; 526675422C85EDCB001EF113 /* ChatMLX.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatMLX.app; sourceTree = BUILT_PRODUCTS_DIR; }; 526675452C85EDCB001EF113 /* ChatMLXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMLXApp.swift; sourceTree = ""; }; 5266754C2C85EDCC001EF113 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -187,7 +203,7 @@ 526676072C85F903001EF113 /* UltramanTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UltramanTextField.swift; sourceTree = ""; }; 526676082C85F903001EF113 /* UltramanWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UltramanWindow.swift; sourceTree = ""; }; 5266760A2C85F903001EF113 /* ConversationDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationDetailView.swift; sourceTree = ""; }; - 5266760B2C85F903001EF113 /* ConversationSidebarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSidebarItem.swift; sourceTree = ""; }; + 5266760B2C85F903001EF113 /* ConversationSidebarItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSidebarItemView.swift; sourceTree = ""; }; 5266760C2C85F903001EF113 /* ConversationSidebarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSidebarView.swift; sourceTree = ""; }; 5266760D2C85F903001EF113 /* ConversationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationView.swift; sourceTree = ""; }; 5266760E2C85F903001EF113 /* EmptyConversation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyConversation.swift; sourceTree = ""; }; @@ -215,9 +231,6 @@ 5266762C2C85F903001EF113 /* SettingsTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = ""; }; 5266762D2C85F903001EF113 /* SettingsTabGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTabGroup.swift; sourceTree = ""; }; 5266762E2C85F903001EF113 /* Styles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = ""; }; - 526676302C85F903001EF113 /* Downloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Downloader.swift; sourceTree = ""; }; - 526676312C85F903001EF113 /* Hub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hub.swift; sourceTree = ""; }; - 526676322C85F903001EF113 /* HubApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HubApi.swift; sourceTree = ""; }; 526676342C85F903001EF113 /* LLMRunner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LLMRunner.swift; sourceTree = ""; }; 526676382C85F903001EF113 /* Defaults+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Defaults+Extensions.swift"; sourceTree = ""; }; 526676392C85F903001EF113 /* MarkdownUI+Theme+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MarkdownUI+Theme+Extensions.swift"; sourceTree = ""; }; @@ -225,15 +238,29 @@ 5266763B2C85F903001EF113 /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 5266763C2C85F903001EF113 /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; 526676772C85F9DA001EF113 /* Localizable.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; + 5270B9D62CDFBB550029E3A4 /* Conversation+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataClass.swift"; sourceTree = ""; }; + 5270B9D72CDFBB550029E3A4 /* Conversation+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Conversation+CoreDataProperties.swift"; sourceTree = ""; }; + 5270B9DB2CDFD1FD0029E3A4 /* ChatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatService.swift; sourceTree = ""; }; + 5270B9DD2CDFD2BA0029E3A4 /* MLXChatStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXChatStrategy.swift; sourceTree = ""; }; + 5270B9DF2CDFD2E80029E3A4 /* OpenAIChatStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIChatStrategy.swift; sourceTree = ""; }; + 5270B9E12CDFD42B0029E3A4 /* ChatStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStrategy.swift; sourceTree = ""; }; + 5270B9E52CE095600029E3A4 /* SettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStore.swift; sourceTree = ""; }; + 5270B9E72CE0AB620029E3A4 /* DownloadStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadStore.swift; sourceTree = ""; }; + 5270B9E92CE0D0510029E3A4 /* MessageBoxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBoxView.swift; sourceTree = ""; }; + 5270B9EB2CE0D20B0029E3A4 /* EditorToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorToolbarView.swift; sourceTree = ""; }; + 5270B9ED2CE0DD730029E3A4 /* EditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorView.swift; sourceTree = ""; }; + 5270B9F12CE0E6A30029E3A4 /* UserMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageView.swift; sourceTree = ""; }; + 5270B9F32CE0E7BB0029E3A4 /* AssistantMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssistantMessageView.swift; sourceTree = ""; }; + 5270B9F82CE0FBDE0029E3A4 /* HuggingFaceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HuggingFaceService.swift; sourceTree = ""; }; 527821582CB8179700638477 /* ModelManagerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManagerViewModel.swift; sourceTree = ""; }; 5278215A2CB81FEA00638477 /* MarkdownMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownMetadata.swift; sourceTree = ""; }; 5278219A2CB821A600638477 /* ChatMLXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatMLXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 527821A32CB821AC00638477 /* ChatMLXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMLXTests.swift; sourceTree = ""; }; 527821A62CB821B800638477 /* MarkdownMetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownMetadataTests.swift; sourceTree = ""; }; 528D82252CABE19000163AAB /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; - 528D83182CAD491900163AAB /* ChatMLX.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ChatMLX.xcdatamodel; sourceTree = ""; }; + 528D83182CAD491900163AAB /* ChatMLX2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = ChatMLX2.xcdatamodel; sourceTree = ""; }; 528D831B2CAD49E600163AAB /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; - 528D83362CADB64300163AAB /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = ""; }; + 528D83362CADB64300163AAB /* ConversationViewModelOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModelOld.swift; sourceTree = ""; }; 528D83382CAE51EC00163AAB /* Role.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Role.swift; sourceTree = ""; }; 52977D2A2CCBCCEC00DD4D2F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 52977E0F2CCCF21E00DD4D2F /* MLXProviderLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXProviderLabelView.swift; sourceTree = ""; }; @@ -253,6 +280,16 @@ 52A689F52CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Extensions.swift"; sourceTree = ""; }; 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlertModifier.swift; sourceTree = ""; }; + 52A7C83E2CDE78830072D691 /* ConversationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationStore.swift; sourceTree = ""; }; + 52A7C84C2CDF4C390072D691 /* Message+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataClass.swift"; sourceTree = ""; }; + 52A7C84D2CDF4C390072D691 /* Message+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+CoreDataProperties.swift"; sourceTree = ""; }; + 52A7C84E2CDF4C390072D691 /* ModelInfo+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelInfo+CoreDataClass.swift"; sourceTree = ""; }; + 52A7C84F2CDF4C390072D691 /* ModelInfo+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ModelInfo+CoreDataProperties.swift"; sourceTree = ""; }; + 52A7C8572CDF6D3C0072D691 /* AppleIntelligenceEffectViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectViewModifier.swift; sourceTree = ""; }; + 52A7C8592CDF92F70072D691 /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; + 52A7C85B2CDF945F0072D691 /* ModelStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelStore.swift; sourceTree = ""; }; + 52A7C85D2CDF99D40072D691 /* MLXService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MLXService.swift; sourceTree = ""; }; + 52A7C85F2CDFA3600072D691 /* OpenAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIService.swift; sourceTree = ""; }; 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderView.swift; sourceTree = ""; }; 52B0532B2CAFEFD000E8DDBA /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; 52B0534C2CB02C3E00E8DDBA /* ModelManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelManagerView.swift; sourceTree = ""; }; @@ -266,11 +303,11 @@ 52B0539F2CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectManager.swift; sourceTree = ""; }; 52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleIntelligenceEffectDisplay.swift; sourceTree = ""; }; 52B053A52CB38E7F00E8DDBA /* ExperimentalFeaturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesView.swift; sourceTree = ""; }; + 52B0C89A2D662B3200BDC5F3 /* HuggingfaceHubService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HuggingfaceHubService.swift; sourceTree = ""; }; 52B9BD062CBE94B80086C013 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; 52CDC3D12CC0207300627147 /* OpenAIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIProvider.swift; sourceTree = ""; }; 52EC64932CC3DC0800A08069 /* NSManagedObjectContext+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Extensions.swift"; sourceTree = ""; }; 52EC64AC2CC4C37C00A08069 /* ModelPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelPicker.swift; sourceTree = ""; }; - 52EC64AE2CC4CD4E00A08069 /* DefaultModelPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultModelPicker.swift; sourceTree = ""; }; 52EC64B02CC531A300A08069 /* ModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelType.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -295,6 +332,7 @@ 526675742C85F1F9001EF113 /* Splash in Frameworks */, 52FD80C82C8F5E42006C50F1 /* LLM in Frameworks */, 526675682C85EFDF001EF113 /* CompactSlider in Frameworks */, + 52B0C89E2D662BCE00BDC5F3 /* HuggingfaceHub in Frameworks */, 5266756E2C85F0FF001EF113 /* Luminare in Frameworks */, 52E50B202C8D719B005A89DE /* LLM in Frameworks */, 52E50B1D2C8D6E81005A89DE /* LLM in Frameworks */, @@ -328,11 +366,28 @@ isa = PBXGroup; children = ( 5266754E2C85EDCC001EF113 /* ChatMLX.entitlements */, - 528D83172CAD491900163AAB /* ChatMLX.xcdatamodeld */, + 528D83172CAD491900163AAB /* ChatMLX2.xcdatamodeld */, ); path = Configuration; sourceTree = ""; }; + 525C50D52CD79EDA00F48479 /* Theme */ = { + isa = PBXGroup; + children = ( + 525C50D62CD79EFD00F48479 /* Theme.swift */, + ); + path = Theme; + sourceTree = ""; + }; + 525C50DD2CD7B8D500F48479 /* Persistence */ = { + isa = PBXGroup; + children = ( + 525C50DE2CD7B90200F48479 /* Repository.swift */, + 528D831B2CAD49E600163AAB /* PersistenceController.swift */, + ); + path = Persistence; + sourceTree = ""; + }; 526675392C85EDCB001EF113 = { isa = PBXGroup; children = ( @@ -386,7 +441,6 @@ children = ( 52B053292CAFDFAF00E8DDBA /* ProviderView.swift */, 52977E152CCD0F6D00DD4D2F /* LabeledToggle.swift */, - 52EC64AE2CC4CD4E00A08069 /* DefaultModelPicker.swift */, 5207D8D52CC68BAA008588FA /* DefaultProviderPicker.swift */, 526676012C85F903001EF113 /* EffectView.swift */, 52A689F92CAECFDE0078CDF9 /* ErrorAlertModifier.swift */, @@ -408,13 +462,9 @@ 526676112C85F903001EF113 /* Conversation */ = { isa = PBXGroup; children = ( - 5266760A2C85F903001EF113 /* ConversationDetailView.swift */, - 5266760B2C85F903001EF113 /* ConversationSidebarItem.swift */, - 5266760C2C85F903001EF113 /* ConversationSidebarView.swift */, + 52F5348C2CD7C1C5001F1F34 /* Sidebar */, + 52F5348B2CD7C1B6001F1F34 /* Detail */, 5266760D2C85F903001EF113 /* ConversationView.swift */, - 5266760E2C85F903001EF113 /* EmptyConversation.swift */, - 5266760F2C85F903001EF113 /* MessageBubbleView.swift */, - 526676102C85F903001EF113 /* RightSidebarView.swift */, ); path = Conversation; sourceTree = ""; @@ -468,6 +518,7 @@ 526676232C85F903001EF113 /* Features */ = { isa = PBXGroup; children = ( + 525C50D52CD79EDA00F48479 /* Theme */, 527821572CB8176200638477 /* View Models */, 526676112C85F903001EF113 /* Conversation */, 526676222C85F903001EF113 /* Settings */, @@ -478,6 +529,7 @@ 5266762F2C85F903001EF113 /* Models */ = { isa = PBXGroup; children = ( + 52A7C8592CDF92F70072D691 /* Model.swift */, 52B053A12CB2F6F900E8DDBA /* AppleIntelligenceEffectDisplay.swift */, 526676252C85F903001EF113 /* DisplayStyle.swift */, 526676262C85F903001EF113 /* DownloadTask.swift */, @@ -498,16 +550,6 @@ path = Models; sourceTree = ""; }; - 526676332C85F903001EF113 /* Huggingface */ = { - isa = PBXGroup; - children = ( - 526676302C85F903001EF113 /* Downloader.swift */, - 526676312C85F903001EF113 /* Hub.swift */, - 526676322C85F903001EF113 /* HubApi.swift */, - ); - path = Huggingface; - sourceTree = ""; - }; 526676372C85F903001EF113 /* Utilities */ = { isa = PBXGroup; children = ( @@ -515,8 +557,6 @@ 520163AC2CBD875500666E82 /* Common.swift */, 5278215A2CB81FEA00638477 /* MarkdownMetadata.swift */, 526676342C85F903001EF113 /* LLMRunner.swift */, - 528D831B2CAD49E600163AAB /* PersistenceController.swift */, - 526676332C85F903001EF113 /* Huggingface */, ); path = Utilities; sourceTree = ""; @@ -540,11 +580,21 @@ path = Extensions; sourceTree = ""; }; + 5270B9DA2CDFD1EF0029E3A4 /* ChatStrategies */ = { + isa = PBXGroup; + children = ( + 5270B9DF2CDFD2E80029E3A4 /* OpenAIChatStrategy.swift */, + 5270B9E12CDFD42B0029E3A4 /* ChatStrategy.swift */, + 5270B9DD2CDFD2BA0029E3A4 /* MLXChatStrategy.swift */, + ); + path = ChatStrategies; + sourceTree = ""; + }; 527821572CB8176200638477 /* View Models */ = { isa = PBXGroup; children = ( 527821582CB8179700638477 /* ModelManagerViewModel.swift */, - 528D83362CADB64300163AAB /* ConversationViewModel.swift */, + 528D83362CADB64300163AAB /* ConversationViewModelOld.swift */, 52A689F72CAE8DA00078CDF9 /* SettingsViewModel.swift */, ); path = "View Models"; @@ -553,6 +603,7 @@ 527821A42CB821AC00638477 /* ChatMLXTests */ = { isa = PBXGroup; children = ( + 52212AB92CDFA95D00A54AA2 /* OpenAIServiceTests.swift */, 527821A32CB821AC00638477 /* ChatMLXTests.swift */, 527821A62CB821B800638477 /* MarkdownMetadataTests.swift */, 52A265F92CBBC5D5004F6DD3 /* ProviderFactoryTests.swift */, @@ -595,12 +646,15 @@ isa = PBXGroup; children = ( 52977E232CCDF43C00DD4D2F /* Constants.swift */, - 52977E192CCD295800DD4D2F /* Errors */, 526676092C85F903001EF113 /* Components */, + 52977E192CCD295800DD4D2F /* Errors */, 5266763D2C85F903001EF113 /* Extensions */, 5266762F2C85F903001EF113 /* Models */, + 525C50DD2CD7B8D500F48479 /* Persistence */, 52A265EE2CBAE54F004F6DD3 /* Services */, + 52A7C83D2CDE78720072D691 /* Stores */, 526676372C85F903001EF113 /* Utilities */, + 52A7C8562CDF6D1F0072D691 /* View Modifiers */, ); path = Core; sourceTree = ""; @@ -616,7 +670,13 @@ 52A265EE2CBAE54F004F6DD3 /* Services */ = { isa = PBXGroup; children = ( + 52B0C89A2D662B3200BDC5F3 /* HuggingfaceHubService.swift */, + 5270B9F82CE0FBDE0029E3A4 /* HuggingFaceService.swift */, + 5270B9DB2CDFD1FD0029E3A4 /* ChatService.swift */, + 52A7C85D2CDF99D40072D691 /* MLXService.swift */, + 52A7C85F2CDFA3600072D691 /* OpenAIService.swift */, 52A265F12CBAEA89004F6DD3 /* AI */, + 5270B9DA2CDFD1EF0029E3A4 /* ChatStrategies */, ); path = Services; sourceTree = ""; @@ -632,6 +692,25 @@ path = AI; sourceTree = ""; }; + 52A7C83D2CDE78720072D691 /* Stores */ = { + isa = PBXGroup; + children = ( + 5270B9E72CE0AB620029E3A4 /* DownloadStore.swift */, + 5270B9E52CE095600029E3A4 /* SettingsStore.swift */, + 52A7C85B2CDF945F0072D691 /* ModelStore.swift */, + 52A7C83E2CDE78830072D691 /* ConversationStore.swift */, + ); + path = Stores; + sourceTree = ""; + }; + 52A7C8562CDF6D1F0072D691 /* View Modifiers */ = { + isa = PBXGroup; + children = ( + 52A7C8572CDF6D3C0072D691 /* AppleIntelligenceEffectViewModifier.swift */, + ); + path = "View Modifiers"; + sourceTree = ""; + }; 52B053282CAFDF8500E8DDBA /* Model Manager */ = { isa = PBXGroup; children = ( @@ -673,18 +752,43 @@ isa = PBXGroup; children = ( 520163962CBD6B4900666E82 /* Conversation.swift */, - 5201639C2CBD6C4900666E82 /* Conversation+CoreDataClass.swift */, - 5201639D2CBD6C4900666E82 /* Conversation+CoreDataProperties.swift */, + 5270B9D62CDFBB550029E3A4 /* Conversation+CoreDataClass.swift */, + 5270B9D72CDFBB550029E3A4 /* Conversation+CoreDataProperties.swift */, 5201639A2CBD6BC000666E82 /* Message.swift */, - 5201639E2CBD6C4900666E82 /* Message+CoreDataClass.swift */, - 5201639F2CBD6C4900666E82 /* Message+CoreDataProperties.swift */, + 52A7C84C2CDF4C390072D691 /* Message+CoreDataClass.swift */, + 52A7C84D2CDF4C390072D691 /* Message+CoreDataProperties.swift */, 520163A82CBD6CCA00666E82 /* ModelInfo.swift */, - 520163A02CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift */, - 520163A12CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift */, + 52A7C84E2CDF4C390072D691 /* ModelInfo+CoreDataClass.swift */, + 52A7C84F2CDF4C390072D691 /* ModelInfo+CoreDataProperties.swift */, ); path = "CoreData Models"; sourceTree = ""; }; + 52F5348B2CD7C1B6001F1F34 /* Detail */ = { + isa = PBXGroup; + children = ( + 5270B9F32CE0E7BB0029E3A4 /* AssistantMessageView.swift */, + 5270B9F12CE0E6A30029E3A4 /* UserMessageView.swift */, + 5270B9ED2CE0DD730029E3A4 /* EditorView.swift */, + 5270B9EB2CE0D20B0029E3A4 /* EditorToolbarView.swift */, + 5270B9E92CE0D0510029E3A4 /* MessageBoxView.swift */, + 5266760E2C85F903001EF113 /* EmptyConversation.swift */, + 5266760F2C85F903001EF113 /* MessageBubbleView.swift */, + 526676102C85F903001EF113 /* RightSidebarView.swift */, + 5266760A2C85F903001EF113 /* ConversationDetailView.swift */, + ); + path = Detail; + sourceTree = ""; + }; + 52F5348C2CD7C1C5001F1F34 /* Sidebar */ = { + isa = PBXGroup; + children = ( + 5266760B2C85F903001EF113 /* ConversationSidebarItemView.swift */, + 5266760C2C85F903001EF113 /* ConversationSidebarView.swift */, + ); + path = Sidebar; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -726,6 +830,7 @@ 527F48142C9EFD5D006AF9FA /* LLM */, 52CDC3D42CC020F500627147 /* OpenAI */, 52CDC3D92CC03B4A00627147 /* CoreDataEvolution */, + 52B0C89D2D662BCE00BDC5F3 /* HuggingfaceHub */, ); productName = ChatMLX; productReference = 526675422C85EDCB001EF113 /* ChatMLX.app */; @@ -795,6 +900,7 @@ 527F48132C9EFD5D006AF9FA /* XCRemoteSwiftPackageReference "mlx-swift-examples" */, 52CDC3D32CC020F500627147 /* XCRemoteSwiftPackageReference "OpenAI" */, 52CDC3D82CC03B4A00627147 /* XCRemoteSwiftPackageReference "CoreDataEvolution" */, + 52B0C89C2D662BCE00BDC5F3 /* XCLocalSwiftPackageReference "../../HuggingfaceHub" */, ); productRefGroup = 526675432C85EDCB001EF113 /* Products */; projectDirPath = ""; @@ -836,6 +942,7 @@ 526676512C85F903001EF113 /* RightSidebarView.swift in Sources */, 52977E122CCCF81300DD4D2F /* MLXProviderContentView.swift in Sources */, 52B053A62CB38E8700E8DDBA /* ExperimentalFeaturesView.swift in Sources */, + 52A7C8602CDFA3650072D691 /* OpenAIService.swift in Sources */, 526676682C85F903001EF113 /* SettingsTabGroup.swift in Sources */, 526676452C85F903001EF113 /* UltramanNavigationSplitView.swift in Sources */, 526676672C85F903001EF113 /* SettingsTab.swift in Sources */, @@ -844,19 +951,19 @@ 5207D8D42CC6846F008588FA /* Provider.swift in Sources */, 526676592C85F903001EF113 /* DefaultConversationView.swift in Sources */, 52B053A02CB2C2CD00E8DDBA /* AppleIntelligenceEffectManager.swift in Sources */, - 520163A22CBD6C4900666E82 /* Conversation+CoreDataClass.swift in Sources */, - 520163A32CBD6C4900666E82 /* Conversation+CoreDataProperties.swift in Sources */, - 520163A42CBD6C4900666E82 /* Message+CoreDataClass.swift in Sources */, - 520163A52CBD6C4900666E82 /* Message+CoreDataProperties.swift in Sources */, - 520163A62CBD6C4900666E82 /* ModelInfo+CoreDataClass.swift in Sources */, - 520163A72CBD6C4900666E82 /* ModelInfo+CoreDataProperties.swift in Sources */, 526676612C85F903001EF113 /* DownloadTask.swift in Sources */, 52A689F62CAE8AAB0078CDF9 /* TimeInterval+Extensions.swift in Sources */, - 528D83192CAD491900163AAB /* ChatMLX.xcdatamodeld in Sources */, + 5270B9EC2CE0D2100029E3A4 /* EditorToolbarView.swift in Sources */, + 528D83192CAD491900163AAB /* ChatMLX2.xcdatamodeld in Sources */, 526676502C85F903001EF113 /* MessageBubbleView.swift in Sources */, + 52A7C8502CDF4C390072D691 /* ModelInfo+CoreDataClass.swift in Sources */, + 52A7C8512CDF4C390072D691 /* Message+CoreDataProperties.swift in Sources */, + 52A7C8522CDF4C390072D691 /* ModelInfo+CoreDataProperties.swift in Sources */, + 52A7C8542CDF4C390072D691 /* Message+CoreDataClass.swift in Sources */, 526676582C85F903001EF113 /* AboutView.swift in Sources */, 526676552C85F903001EF113 /* LocalModelsView.swift in Sources */, 52A689FA2CAECFE00078CDF9 /* ErrorAlertModifier.swift in Sources */, + 525C50DF2CD7B90400F48479 /* Repository.swift in Sources */, 526676662C85F903001EF113 /* RemoteModel.swift in Sources */, 52EC64942CC3DC0E00A08069 /* NSManagedObjectContext+Extensions.swift in Sources */, 526676562C85F903001EF113 /* MLXCommunityItemView.swift in Sources */, @@ -868,20 +975,25 @@ 526676702C85F903001EF113 /* Defaults+Extensions.swift in Sources */, 52A265F32CBAEA9E004F6DD3 /* MLXProvider.swift in Sources */, 526676462C85F903001EF113 /* UltramanSecureField.swift in Sources */, - 5266766C2C85F903001EF113 /* HubApi.swift in Sources */, 526676472C85F903001EF113 /* UltramanSidebarButtonStyle.swift in Sources */, 5266764F2C85F903001EF113 /* EmptyConversation.swift in Sources */, 526676572C85F903001EF113 /* MLXCommunityView.swift in Sources */, + 5270B9E02CDFD2F20029E3A4 /* OpenAIChatStrategy.swift in Sources */, + 5270B9F42CE0E7BF0029E3A4 /* AssistantMessageView.swift in Sources */, + 52B0C89B2D662B4800BDC5F3 /* HuggingfaceHubService.swift in Sources */, 528D82262CABE19900163AAB /* Date+Extensions.swift in Sources */, 5266766D2C85F903001EF113 /* LLMRunner.swift in Sources */, 52B053542CB0F5B300E8DDBA /* LabeledContentStyle+Extensions.swift in Sources */, 52A021CF2CB565FC007893F7 /* AnimatedGradientFill.metal in Sources */, 52B0534D2CB02C4600E8DDBA /* ModelManagerView.swift in Sources */, 52EC64AD2CC4C38600A08069 /* ModelPicker.swift in Sources */, + 52A7C85A2CDF92FC0072D691 /* Model.swift in Sources */, 5201639B2CBD6BC400666E82 /* Message.swift in Sources */, + 5270B9DE2CDFD2C40029E3A4 /* MLXChatStrategy.swift in Sources */, 5266764E2C85F903001EF113 /* ConversationView.swift in Sources */, + 5270B9D82CDFBB550029E3A4 /* Conversation+CoreDataClass.swift in Sources */, + 5270B9D92CDFBB550029E3A4 /* Conversation+CoreDataProperties.swift in Sources */, 52977E162CCD0F7800DD4D2F /* LabeledToggle.swift in Sources */, - 5266766A2C85F903001EF113 /* Downloader.swift in Sources */, 526676732C85F903001EF113 /* String+Extensions.swift in Sources */, 526676742C85F903001EF113 /* View+Extensions.swift in Sources */, 526676432C85F903001EF113 /* EffectView.swift in Sources */, @@ -891,7 +1003,7 @@ 526676532C85F903001EF113 /* DownloadTaskView.swift in Sources */, 52A689F82CAE8DA30078CDF9 /* SettingsViewModel.swift in Sources */, 52B9BD072CBE94BA0086C013 /* Bundle+Extensions.swift in Sources */, - 5266764C2C85F903001EF113 /* ConversationSidebarItem.swift in Sources */, + 5266764C2C85F903001EF113 /* ConversationSidebarItemView.swift in Sources */, 52B053572CB0F8EE00E8DDBA /* Binding+Extensions.swift in Sources */, 52CDC3D22CC0208100627147 /* OpenAIProvider.swift in Sources */, 526676622C85F903001EF113 /* Language.swift in Sources */, @@ -900,29 +1012,36 @@ 52977E142CCCFF4100DD4D2F /* MLXProviderModelItemView.swift in Sources */, 52B053502CB02CF900E8DDBA /* MLXProviderView.swift in Sources */, 52977E182CCD1AC000DD4D2F /* GPUMemorySettingsView.swift in Sources */, + 52A7C8582CDF6D3E0072D691 /* AppleIntelligenceEffectViewModifier.swift in Sources */, 526676412C85F903001EF113 /* SplashCodeSyntaxHighlighter.swift in Sources */, 526676542C85F903001EF113 /* LocalModelItemView.swift in Sources */, 526676632C85F903001EF113 /* LocalModel.swift in Sources */, 52977D2B2CCBCCEC00DD4D2F /* Logger.swift in Sources */, 52EC64B12CC531A300A08069 /* ModelType.swift in Sources */, + 5270B9EA2CE0D0570029E3A4 /* MessageBoxView.swift in Sources */, + 5270B9E22CDFD42B0029E3A4 /* ChatStrategy.swift in Sources */, 52B0539D2CB2BF0D00E8DDBA /* AppleIntelligenceEffectView.swift in Sources */, 520163972CBD6B4B00666E82 /* Conversation.swift in Sources */, + 5270B9F22CE0E6A70029E3A4 /* UserMessageView.swift in Sources */, + 52A7C85E2CDF99DA0072D691 /* MLXService.swift in Sources */, 52977E202CCD4D3400DD4D2F /* Errors.swift in Sources */, 52B0539E2CB2BF0D00E8DDBA /* AppleIntelligenceEffectController.swift in Sources */, - 52EC64AF2CC4CD5400A08069 /* DefaultModelPicker.swift in Sources */, 526676692C85F903001EF113 /* Styles.swift in Sources */, 5266765A2C85F903001EF113 /* GeneralView.swift in Sources */, 526675462C85EDCB001EF113 /* ChatMLXApp.swift in Sources */, 526676492C85F903001EF113 /* UltramanTextField.swift in Sources */, 5266765E2C85F903001EF113 /* SettingsView.swift in Sources */, 528D831C2CAD49E600163AAB /* PersistenceController.swift in Sources */, - 5266766B2C85F903001EF113 /* Hub.swift in Sources */, + 5270B9F92CE0FBEB0029E3A4 /* HuggingFaceService.swift in Sources */, + 52A7C85C2CDF94640072D691 /* ModelStore.swift in Sources */, + 525C50D72CD79EFE00F48479 /* Theme.swift in Sources */, 52977E102CCCF22E00DD4D2F /* MLXProviderLabelView.swift in Sources */, 52977E1B2CCD296F00DD4D2F /* ErrorView.swift in Sources */, 5207D8D62CC68BAC008588FA /* DefaultProviderPicker.swift in Sources */, + 5270B9E82CE0AB6C0029E3A4 /* DownloadStore.swift in Sources */, 527821592CB817A300638477 /* ModelManagerViewModel.swift in Sources */, 5266764D2C85F903001EF113 /* ConversationSidebarView.swift in Sources */, - 528D83372CADB64600163AAB /* ConversationViewModel.swift in Sources */, + 528D83372CADB64600163AAB /* ConversationViewModelOld.swift in Sources */, 5266765D2C85F903001EF113 /* SettingsSidebarView.swift in Sources */, 5266764B2C85F903001EF113 /* ConversationDetailView.swift in Sources */, 5201638B2CBC2D8D00666E82 /* V2MigrationPolicy.swift in Sources */, @@ -932,9 +1051,13 @@ 5266765B2C85F903001EF113 /* HuggingFaceView.swift in Sources */, 526676642C85F903001EF113 /* LocalModelGroup.swift in Sources */, 526676522C85F903001EF113 /* DownloadManagerView.swift in Sources */, + 5270B9E62CE0956C0029E3A4 /* SettingsStore.swift in Sources */, + 5270B9DC2CDFD2090029E3A4 /* ChatService.swift in Sources */, 52A265F52CBBB3C0004F6DD3 /* ProviderFactory.swift in Sources */, + 52A7C83F2CDE78860072D691 /* ConversationStore.swift in Sources */, 520163AB2CBD715900666E82 /* ProviderModel.swift in Sources */, 52977E242CCDF43E00DD4D2F /* Constants.swift in Sources */, + 5270B9EE2CE0DD780029E3A4 /* EditorView.swift in Sources */, 52B053522CB0385800E8DDBA /* OpenAIProviderView.swift in Sources */, 526676482C85F903001EF113 /* UltramanTextEditorWithPlaceholder.swift in Sources */, ); @@ -947,6 +1070,7 @@ 52A265FA2CBBC5D5004F6DD3 /* ProviderFactoryTests.swift in Sources */, 527821A52CB821AC00638477 /* ChatMLXTests.swift in Sources */, 527821A72CB821B800638477 /* MarkdownMetadataTests.swift in Sources */, + 52212ABA2CDFA96100A54AA2 /* OpenAIServiceTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1023,6 +1147,7 @@ SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -1080,6 +1205,7 @@ MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 6.0; }; name = Release; }; @@ -1105,13 +1231,13 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 14; MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -1137,13 +1263,13 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 14; MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLX; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_STRICT_CONCURRENCY = complete; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Release; }; @@ -1155,12 +1281,12 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = RFGFKQEKRH; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 14; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatMLX.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ChatMLX"; }; name = Debug; @@ -1173,12 +1299,12 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = RFGFKQEKRH; GENERATE_INFOPLIST_FILE = YES; - MACOSX_DEPLOYMENT_TARGET = 15.0; + MACOSX_DEPLOYMENT_TARGET = 14; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = johnmai.ChatMLXTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatMLX.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ChatMLX"; }; name = Release; @@ -1215,6 +1341,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 52B0C89C2D662BCE00BDC5F3 /* XCLocalSwiftPackageReference "../../HuggingfaceHub" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../../HuggingfaceHub; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ 526675602C85EFB3001EF113 /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; @@ -1360,6 +1493,10 @@ isa = XCSwiftPackageProductDependency; productName = Transformers; }; + 52B0C89D2D662BCE00BDC5F3 /* HuggingfaceHub */ = { + isa = XCSwiftPackageProductDependency; + productName = HuggingfaceHub; + }; 52CDC3D42CC020F500627147 /* OpenAI */ = { isa = XCSwiftPackageProductDependency; package = 52CDC3D32CC020F500627147 /* XCRemoteSwiftPackageReference "OpenAI" */; @@ -1417,14 +1554,14 @@ /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ - 528D83172CAD491900163AAB /* ChatMLX.xcdatamodeld */ = { + 528D83172CAD491900163AAB /* ChatMLX2.xcdatamodeld */ = { isa = XCVersionGroup; children = ( 520163832CBC1C9F00666E82 /* ChatMLX 2.xcdatamodel */, - 528D83182CAD491900163AAB /* ChatMLX.xcdatamodel */, + 528D83182CAD491900163AAB /* ChatMLX2.xcdatamodel */, ); currentVersion = 520163832CBC1C9F00666E82 /* ChatMLX 2.xcdatamodel */; - path = ChatMLX.xcdatamodeld; + path = ChatMLX2.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; diff --git a/ChatMLX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ChatMLX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 44d40f4..ece2993 100644 --- a/ChatMLX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ChatMLX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "abfff542bc4df674e107b815bb2eb68cf14edf79991cd6e62fb3c1d42a5ffa1a", + "originHash" : "8b17ce993eabb8acc9a586f8f2954a4513d6ab871abc6590ca9536d2975b7c4f", "pins" : [ { "identity" : "alamofire", @@ -19,6 +19,15 @@ "version" : "1.3.9" } }, + { + "identity" : "anycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flight-School/AnyCodable", + "state" : { + "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", + "version" : "0.6.7" + } + }, { "identity" : "compactslider", "kind" : "remoteSourceControl", @@ -109,6 +118,15 @@ "version" : "0.3.0" } }, + { + "identity" : "semaphore", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/Semaphore", + "state" : { + "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2", + "version" : "0.1.0" + } + }, { "identity" : "splash", "kind" : "remoteSourceControl", diff --git a/ChatMLX/Application/ChatMLXApp.swift b/ChatMLX/Application/ChatMLXApp.swift index 233ef1e..1e7aba7 100644 --- a/ChatMLX/Application/ChatMLXApp.swift +++ b/ChatMLX/Application/ChatMLXApp.swift @@ -6,8 +6,8 @@ // import Defaults -import os import SwiftUI +import os @main struct ChatMLXApp: App { @@ -26,11 +26,11 @@ struct ChatMLXApp: App { // MARK: - State - @State private var conversationViewModel: ConversationViewModel = .init() + @State private var conversationStore: ConversationStore = .init() @State private var settingsViewModel: SettingsViewModel = .init() - @State private var runner: LLMRunner = .init() - @State private var modelManagerViewModel: ModelManagerViewModel = .init() @State private var errorWrapper: ErrorWrapper? + @State private var settingsStore = SettingsStore() + // MARK: - User Defaults @@ -46,10 +46,11 @@ struct ChatMLXApp: App { mainWindow() settingsWindow() } - .environment(modelManagerViewModel) - .environment(conversationViewModel) + .environment(conversationStore) + .environment(ModelStore()) + .environment(settingsStore) + .environment(DownloadStore.shared) .environment(settingsViewModel) - .environment(runner) .environment(\.managedObjectContext, viewContext) .environment(\.locale, .init(identifier: language.rawValue)) .environment(\.appError) { error in @@ -59,7 +60,7 @@ struct ChatMLXApp: App { } .onChange(of: scenePhase) { _, newValue in if newValue == .background { - saveContext() + try? viewContext.saveChanges() } } @@ -74,7 +75,7 @@ extension ChatMLXApp { private func mainWindow() -> some Scene { WindowGroup(id: Constants.mainWindowID) { - ConversationView() + ConversationView(selectedConversation: $conversationStore.selectedConversation) .frame(minWidth: 900, minHeight: 580) .sheet(item: $errorWrapper) { errorWrapper in ErrorView(errorWrapper: errorWrapper) @@ -127,13 +128,4 @@ extension ChatMLXApp { Defaults[.lastLaunchedVersion] = currentVersion } } - - private func saveContext() { - guard viewContext.hasChanges else { return } - do { - try viewContext.save() - } catch { - logger.error("Failed to save context: \(error.localizedDescription)") - } - } } diff --git a/ChatMLX/Configuration/ChatMLX.entitlements b/ChatMLX/Configuration/ChatMLX.entitlements index 3203b1c..c107fcd 100644 --- a/ChatMLX/Configuration/ChatMLX.entitlements +++ b/ChatMLX/Configuration/ChatMLX.entitlements @@ -3,11 +3,11 @@ com.apple.security.app-sandbox - + com.apple.security.files.downloads.read-only com.apple.security.files.user-selected.read-only - + com.apple.security.network.client com.apple.security.network.server diff --git a/ChatMLX/Configuration/ChatMLX.xcdatamodeld/.xccurrentversion b/ChatMLX/Configuration/ChatMLX2.xcdatamodeld/.xccurrentversion similarity index 100% rename from ChatMLX/Configuration/ChatMLX.xcdatamodeld/.xccurrentversion rename to ChatMLX/Configuration/ChatMLX2.xcdatamodeld/.xccurrentversion diff --git a/ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX 2.xcdatamodel/contents b/ChatMLX/Configuration/ChatMLX2.xcdatamodeld/ChatMLX 2.xcdatamodel/contents similarity index 87% rename from ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX 2.xcdatamodel/contents rename to ChatMLX/Configuration/ChatMLX2.xcdatamodeld/ChatMLX 2.xcdatamodel/contents index cd5770b..20d6715 100644 --- a/ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX 2.xcdatamodel/contents +++ b/ChatMLX/Configuration/ChatMLX2.xcdatamodeld/ChatMLX 2.xcdatamodel/contents @@ -1,12 +1,15 @@ - + - + + + + @@ -21,7 +24,7 @@ - + @@ -33,7 +36,7 @@ - + diff --git a/ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents b/ChatMLX/Configuration/ChatMLX2.xcdatamodeld/ChatMLX2.xcdatamodel/contents similarity index 100% rename from ChatMLX/Configuration/ChatMLX.xcdatamodeld/ChatMLX.xcdatamodel/contents rename to ChatMLX/Configuration/ChatMLX2.xcdatamodeld/ChatMLX2.xcdatamodel/contents diff --git a/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift index 1ca8d5f..3ade7d2 100644 --- a/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift +++ b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectManager.swift @@ -7,6 +7,7 @@ import AppKit +@MainActor final class AppleIntelligenceEffectManager { static let shared = AppleIntelligenceEffectManager() diff --git a/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift index 3490e0f..43265fd 100644 --- a/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift +++ b/ChatMLX/Core/Components/AppleIntelligenceEffect/AppleIntelligenceEffectView.swift @@ -31,8 +31,8 @@ struct AppleIntelligenceEffectView: View { let blurRadius = angle > 0 - ? maxBlurRadiusBase + 6 * sin(elapsed * 2) - : minBlurRadiusBase + 3 * sin(elapsed * 4) + ? maxBlurRadiusBase + 6 * sin(elapsed * 2) + : minBlurRadiusBase + 3 * sin(elapsed * 4) return Rectangle() .visualEffect { content, proxy in diff --git a/ChatMLX/Core/Components/DefaultModelPicker.swift b/ChatMLX/Core/Components/DefaultModelPicker.swift deleted file mode 100644 index b8e6769..0000000 --- a/ChatMLX/Core/Components/DefaultModelPicker.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// DefaultModelPicker.swift -// ChatMLX -// -// Created by John Mai on 2024/10/20. -// - -import CoreData -import Defaults -import SwiftUI - -struct DefaultModelPicker: View { -// @FetchRequest( -// sortDescriptors: [NSSortDescriptor(keyPath: \ModelInfo.name, ascending: true)], -// animation: .default -// ) -// private var models: FetchedResults - - @Binding var provider: Provider - @Environment(\.managedObjectContext) private var viewContext - - @Default(.defaultModel) var defaultModel - - var models: [ProviderModel] { - switch provider { - case .mlx: - try! MLXProvider.fetchModels() - case .openAI: - OpenAIProvider.fetchModels() - } - } - - var body: some View { - Picker( - selection: $defaultModel, - label: Image(systemName: "brain") - ) { - Text("Not selected").tag(nil as String?) - Divider() - ForEach(models, id: \.self) { model in - Text(model.name ?? model.id).tag(model.id) - } - } - .pickerStyle(.menu) - .labelsHidden() - .tint(.white) - } -} diff --git a/ChatMLX/Core/Components/DefaultProviderPicker.swift b/ChatMLX/Core/Components/DefaultProviderPicker.swift index bf661cf..3ae1619 100644 --- a/ChatMLX/Core/Components/DefaultProviderPicker.swift +++ b/ChatMLX/Core/Components/DefaultProviderPicker.swift @@ -1,28 +1,28 @@ +//// +//// DefaultProviderPicker.swift +//// ChatMLX +//// +//// Created by John Mai on 2024/10/21. +//// // -// DefaultProviderPicker.swift -// ChatMLX +//import Defaults +//import SwiftUI // -// Created by John Mai on 2024/10/21. +//struct DefaultProviderPicker:View { +// @Binding var provider: Provider // - -import Defaults -import SwiftUI - -struct DefaultProviderPicker:View { - @Binding var provider: Provider - - @Default(.enableOpenAI) private var enableOpenAI - - var body: some View { - Picker("Provider", selection: $provider) { - Text("MLX").tag(Provider.mlx) - - if enableOpenAI { - Text("OpenAI").tag(Provider.openAI) - } - } - .pickerStyle(.menu) - .labelsHidden() - .tint(.white) - } -} +// @Default(.enableOpenAI) private var enableOpenAI +// +// var body: some View { +// Picker("Provider", selection: $provider) { +// Text("MLX").tag(Provider.mlx) +// +// if enableOpenAI { +// Text("OpenAI").tag(Provider.openAI) +// } +// } +// .pickerStyle(.menu) +// .labelsHidden() +// .tint(.white) +// } +//} diff --git a/ChatMLX/Core/Components/ModelPicker.swift b/ChatMLX/Core/Components/ModelPicker.swift index 762c7d5..654a565 100644 --- a/ChatMLX/Core/Components/ModelPicker.swift +++ b/ChatMLX/Core/Components/ModelPicker.swift @@ -9,23 +9,16 @@ import Defaults import SwiftUI struct ModelPicker: View { - @Binding var selection: ModelInfo? - @Default(.enableOpenAI) private var enableOpenAI - @State var models: [ProviderModel] = [] + @Binding var selection: ProviderModel.Identifier? + let models: [ProviderModel] var groupedModels: [String: [ProviderModel]] { - var models: [ProviderModel] = [] - do { - models = try MLXProvider.fetchModels() - } catch { - print("Error fetching models") - } - - if enableOpenAI { - models = models + OpenAIProvider.fetchModels() + Dictionary(grouping: models) { + switch $0.id { + case .id(_, let provider), .directory(_, let provider): + provider.rawValue + } } - - return Dictionary(grouping: models) { $0.provider.rawValue } } var body: some View { @@ -33,11 +26,15 @@ struct ModelPicker: View { selection: $selection, label: Image(systemName: "brain") ) { - Text("Not selected").tag(nil as ModelInfo?) + Text("Not selected").tag(nil as ProviderModel.Identifier?) ForEach(groupedModels.keys.sorted(), id: \.self) { provider in Section(header: Text(provider)) { - ForEach(groupedModels[provider]!, id: \.self) { model in - Text(model.name ?? model.id).tag(model) + ForEach(groupedModels[provider]!) { model in + if let name = model.name { + Text(name).tag(model.id) + } else if case .id(let id, _) = model.id { + Text(id).tag(model.id) + } } } } @@ -45,25 +42,5 @@ struct ModelPicker: View { .pickerStyle(.menu) .labelsHidden() .tint(.white) - .task { - await fetchModels() - } - } - - private func fetchModels() async { - var models: [ProviderModel] = [] - do { - models = try MLXProvider.fetchModels() - } catch { - print("Error fetching models") - } - - if enableOpenAI { - models = models + OpenAIProvider.fetchModels() - } - - await MainActor.run { - self.models = models - } } } diff --git a/ChatMLX/Core/Components/SyntaxHighlighter/TextOutputFormat.swift b/ChatMLX/Core/Components/SyntaxHighlighter/TextOutputFormat.swift index c003075..8105443 100644 --- a/ChatMLX/Core/Components/SyntaxHighlighter/TextOutputFormat.swift +++ b/ChatMLX/Core/Components/SyntaxHighlighter/TextOutputFormat.swift @@ -9,9 +9,9 @@ import Splash import SwiftUI struct TextOutputFormat: OutputFormat { - private let theme: Theme + private let theme: Splash.Theme - init(theme: Theme) { + init(theme: Splash.Theme) { self.theme = theme } @@ -22,10 +22,10 @@ struct TextOutputFormat: OutputFormat { extension TextOutputFormat { struct Builder: OutputBuilder { - private let theme: Theme + private let theme: Splash.Theme private var accumulatedText: [Text] - fileprivate init(theme: Theme) { + fileprivate init(theme: Splash.Theme) { self.theme = theme self.accumulatedText = [] } diff --git a/ChatMLX/Core/Components/UltramanMinimalistWindowModifier.swift b/ChatMLX/Core/Components/UltramanMinimalistWindowModifier.swift index 2e93b2b..a0b0117 100644 --- a/ChatMLX/Core/Components/UltramanMinimalistWindowModifier.swift +++ b/ChatMLX/Core/Components/UltramanMinimalistWindowModifier.swift @@ -21,39 +21,41 @@ struct UltramanMinimalistWindowModifier: ViewModifier { setupFullScreenObservers(for: window) } } - + private func configureWindow(_ window: NSWindow) { window.setBackgroundBlur(radius: Int(blurRadius), color: NSColor(backgroundColor)) window.toolbarStyle = .unified window.titlebarAppearsTransparent = true window.titleVisibility = .hidden - + let toolbar = NSToolbar() toolbar.showsBaselineSeparator = false window.toolbar = toolbar } - + private func setupFullScreenObservers(for window: NSWindow) { let notificationCenter = NotificationCenter.default - - notificationCenter.addObserver(forName: NSWindow.didEnterFullScreenNotification, object: window, queue: .main) { _ in + + notificationCenter.addObserver(forName: NSWindow.didEnterFullScreenNotification, object: window, queue: .main) { + _ in Task { @MainActor in handleFullScreenEnter(window) } } - - notificationCenter.addObserver(forName: NSWindow.didExitFullScreenNotification, object: window, queue: .main) { _ in + + notificationCenter.addObserver(forName: NSWindow.didExitFullScreenNotification, object: window, queue: .main) { + _ in Task { @MainActor in handleFullScreenExit(window) } } } - + private func handleFullScreenEnter(_ window: NSWindow) { window.toolbar?.isVisible = false NSApp.presentationOptions = [.autoHideToolbar, .autoHideMenuBar] } - + private func handleFullScreenExit(_ window: NSWindow) { window.toolbar?.isVisible = true NSApp.presentationOptions = [] diff --git a/ChatMLX/Core/Components/UltramanNavigationSplitView.swift b/ChatMLX/Core/Components/UltramanNavigationSplitView.swift index 804b0b2..1225275 100644 --- a/ChatMLX/Core/Components/UltramanNavigationSplitView.swift +++ b/ChatMLX/Core/Components/UltramanNavigationSplitView.swift @@ -1,23 +1,15 @@ // -// UltramanNavigationSplitView.swift -// ChatMLX +// UltramanNavigationTitleKey.swift +// ChatMLXUI // -// Created by John Mai on 2024/8/3. +// Created by John Mai on 2025/2/22. // import SwiftUI -struct UltramanNavigationTitleKey: PreferenceKey { - static let defaultValue: String = "" - - static func reduce( - value: inout String, nextValue: () -> String - ) { - value = nextValue() - } -} +extension AnyView: @unchecked @retroactive Sendable {} -struct UltramanToolbarItem: Identifiable, Equatable { +struct UltramanToolbarItem: Identifiable, Equatable, Sendable { static func == (lhs: UltramanToolbarItem, rhs: UltramanToolbarItem) -> Bool { lhs.id == rhs.id && lhs.alignment == rhs.alignment } @@ -30,26 +22,12 @@ struct UltramanToolbarItem: Identifiable, Equatable { case leading, trailing } - init(alignment: ToolbarAlignment = .trailing, @ViewBuilder content: () -> some View) { + nonisolated init(alignment: ToolbarAlignment = .trailing, @ViewBuilder content: () -> some View) { self.content = AnyView(content()) self.alignment = alignment } } -struct UltramanNavigationToolbarKey: PreferenceKey { - static var defaultValue: [UltramanToolbarItem] = [] - - static func reduce( - value: inout [UltramanToolbarItem], - nextValue: () -> [UltramanToolbarItem] - ) { - let newItems = nextValue() - if !newItems.isEmpty { - value.append(contentsOf: newItems) - } - } -} - @resultBuilder struct UltramanToolbarBuilder { static func buildBlock(_ components: UltramanToolbarItem...) -> [UltramanToolbarItem] { @@ -57,36 +35,96 @@ struct UltramanToolbarBuilder { } } +private struct UltramanNavigationTitleKey: EnvironmentKey { + static let defaultValue: String = "" +} + +struct UltramanNavigationToolbarKey: EnvironmentKey { + static let defaultValue: [UltramanToolbarItem] = [] +} + +struct UltramanNavigationStateKey: EnvironmentKey { + static let defaultValue: UltramanNavigationState = .init() +} + +extension EnvironmentValues { + var ultramanNavigationState: UltramanNavigationState { + get { self[UltramanNavigationStateKey.self] } + set { self[UltramanNavigationStateKey.self] = newValue } + } +} + +struct UltramanNavigationTitleViewModifier: ViewModifier { + let title: String + + @Environment(\.ultramanNavigationState) var state + + func body(content: Content) -> some View { + content + .onAppear { + state.title = title + } + .onChange(of: title) { oldValue, newValue in + print("title changed from \(oldValue) to \(newValue)") + state.title = newValue + } + .onDisappear { + // 在视图消失时清除标题 + state.title = "" + } + } +} + +struct UltramanNavigationToolbarViewModifier: ViewModifier { + let items: [UltramanToolbarItem] + + @Environment(UltramanNavigationState.self) var state + + func body(content: Content) -> some View { + content + .onAppear { + state.toolbarItems = items + } + .onChange(of: items) { oldValue, newValue in + print("toolbar items changed") + state.toolbarItems = newValue + } + .onDisappear { + // 在视图消失时清除标题 + state.toolbarItems = [] + } + } +} + +@Observable +class UltramanNavigationState: @unchecked Sendable { + var title: String = "" + var toolbarItems: [UltramanToolbarItem] = [] +} + extension View { func ultramanNavigationTitle(_ title: String) -> some View { - preference(key: UltramanNavigationTitleKey.self, value: title) + modifier(UltramanNavigationTitleViewModifier(title: title)) } func ultramanToolbar( alignment: UltramanToolbarItem.ToolbarAlignment = .trailing, @ViewBuilder content: () -> some View ) -> some View { - preference( - key: UltramanNavigationToolbarKey.self, - value: [ - UltramanToolbarItem( - alignment: alignment, - content: { - content() - } - ) - ] - ) + modifier( + UltramanNavigationToolbarViewModifier(items: [ + UltramanToolbarItem(alignment: alignment, content: { content() }) + ])) } - func ultramanToolbar( - @UltramanToolbarBuilder content: () -> [UltramanToolbarItem] - ) -> some View { - preference( - key: UltramanNavigationToolbarKey.self, - value: content() - ) - } + // func ultramanToolbar( + // @UltramanToolbarBuilder content: () -> [UltramanToolbarItem] + // ) -> some View { + // preference( + // key: UltramanNavigationToolbarKey.self, + // value: content() + // ) + // } } struct UltramanNavigationSplitView: View { @@ -95,42 +133,34 @@ struct UltramanNavigationSplitView: View { let sidebar: () -> Sidebar let detail: () -> Detail - @State private var navigationTitle: String = "" - @State private var toolbarItems: [UltramanToolbarItem] = [] - @State private var isDragging = false @State private var isSidebarVisible = true let minSidebarWidth: CGFloat = 200 let maxSidebarWidth: CGFloat = 400 + @State private var state = UltramanNavigationState() + var body: some View { - GeometryReader { _ in - HStack(spacing: .zero) { - if isSidebarVisible { - sidebar() - .frame(width: sidebarWidth) - .transition(.move(edge: .leading)) - .zIndex(10) - } + HStack(spacing: .zero) { + if isSidebarVisible { + sidebar() + .frame(width: sidebarWidth) + .transition(.move(edge: .leading)) + .zIndex(10) + } - VStack(spacing: .zero) { - Divider() - detail() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .onPreferenceChange(UltramanNavigationTitleKey.self) { - navigationTitle = $0 - } - .onPreferenceChange(UltramanNavigationToolbarKey.self) { - toolbarItems = $0 - } - } + VStack(spacing: .zero) { + Divider() + detail() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } - .safeAreaInset(edge: .top, alignment: .center, spacing: 0) { - header().frame(height: 52) - } + .safeAreaInset(edge: .top, alignment: .center, spacing: 0) { + header().frame(height: 52) } } + .environment(state) } @ViewBuilder @@ -151,18 +181,18 @@ struct UltramanNavigationSplitView: View { } .buttonStyle(.plain) - ForEach(toolbarItems.filter { $0.alignment == .leading }) { + ForEach(state.toolbarItems.filter { $0.alignment == .leading }) { item in item.content } Spacer() - Text(LocalizedStringKey(navigationTitle)) + Text(LocalizedStringKey(state.title)) .font(.headline) Spacer() - ForEach(toolbarItems.filter { $0.alignment == .trailing }) { + ForEach(state.toolbarItems.filter { $0.alignment == .trailing }) { item in item.content } diff --git a/ChatMLX/Core/Components/UltramanTextEditorWithPlaceholder.swift b/ChatMLX/Core/Components/UltramanTextEditorWithPlaceholder.swift index a12583c..fa8c620 100644 --- a/ChatMLX/Core/Components/UltramanTextEditorWithPlaceholder.swift +++ b/ChatMLX/Core/Components/UltramanTextEditorWithPlaceholder.swift @@ -61,6 +61,7 @@ struct UltramanTextEditor: NSViewRepresentable { super.init() } + @MainActor func setupPlaceholder(for textView: NSTextView) { let placeholder = NSTextView(frame: textView.bounds) placeholder.isSelectable = false @@ -98,15 +99,16 @@ struct UltramanTextEditor: NSViewRepresentable { if commandSelector == #selector(NSResponder.insertNewline(_:)) { if NSEvent.modifierFlags.contains(.shift) { textView.insertNewlineIgnoringFieldEditor(nil) - return true } else { parent.onSubmit() - return true } + + return true } return false } + @MainActor func updatePlaceholder(for textView: NSTextView) { placeholderView?.isHidden = !textView.string.isEmpty || textView.selectedRange().length > 0 diff --git a/ChatMLX/Core/Extensions/Defaults+Extensions.swift b/ChatMLX/Core/Extensions/Defaults+Extensions.swift index 8dfb7a8..2106b05 100644 --- a/ChatMLX/Core/Extensions/Defaults+Extensions.swift +++ b/ChatMLX/Core/Extensions/Defaults+Extensions.swift @@ -45,7 +45,7 @@ extension Defaults.Keys { "appleIntelligenceEffectDisplay", default: .appInternal) static let defaultProvider = Key("defaultProvider", default: .mlx) - static let defaultModel = Key("defaultModel", default: nil) + static let defaultModel = Key("defaultModel", default: nil) static let enableOpenAI = Key("enableOpenAI", default: false) diff --git a/ChatMLX/Core/Extensions/LabeledContentStyle+Extensions.swift b/ChatMLX/Core/Extensions/LabeledContentStyle+Extensions.swift index 8a1727b..be26e45 100644 --- a/ChatMLX/Core/Extensions/LabeledContentStyle+Extensions.swift +++ b/ChatMLX/Core/Extensions/LabeledContentStyle+Extensions.swift @@ -16,7 +16,7 @@ struct HorizontalLabeledContentStyle: LabeledContentStyle { } .frame(minHeight: 35) .padding(.horizontal) - + } } diff --git a/ChatMLX/Core/Extensions/MarkdownUI+Theme+Extensions.swift b/ChatMLX/Core/Extensions/MarkdownUI+Theme+Extensions.swift index a6aebbd..4b27350 100644 --- a/ChatMLX/Core/Extensions/MarkdownUI+Theme+Extensions.swift +++ b/ChatMLX/Core/Extensions/MarkdownUI+Theme+Extensions.swift @@ -5,11 +5,12 @@ // Created by John Mai on 2024/8/18. // -import MarkdownUI +@preconcurrency import MarkdownUI import SwiftUI +@MainActor extension MarkdownUI.Theme { - static let customGitHub = Theme.gitHub.text { + static let customGitHub = MarkdownUI.Theme.gitHub.text { ForegroundColor(.white) BackgroundColor(.clear) } diff --git a/ChatMLX/Core/Extensions/NSManagedObjectContext+Extensions.swift b/ChatMLX/Core/Extensions/NSManagedObjectContext+Extensions.swift index cfd8112..1fabf16 100644 --- a/ChatMLX/Core/Extensions/NSManagedObjectContext+Extensions.swift +++ b/ChatMLX/Core/Extensions/NSManagedObjectContext+Extensions.swift @@ -8,16 +8,9 @@ import CoreData extension NSManagedObjectContext { - @discardableResult - func saveIfNeeded() throws -> Bool { - guard hasChanges else { return false } - try save() - return true - } - - func saveIfNeeded() async throws -> Bool { - try await perform { - try self.saveIfNeeded() + func saveChanges() throws { + if hasChanges { + try save() } } } diff --git a/ChatMLX/Core/Extensions/NSWindow+Extensions.swift b/ChatMLX/Core/Extensions/NSWindow+Extensions.swift index eade173..fd62502 100644 --- a/ChatMLX/Core/Extensions/NSWindow+Extensions.swift +++ b/ChatMLX/Core/Extensions/NSWindow+Extensions.swift @@ -24,7 +24,7 @@ extension NSWindow { NSLog("Error setting blur radius: \(status)") } - backgroundColor = color + backgroundColor = .white.withAlphaComponent(0.001) ignoresMouseEvents = false } } diff --git a/ChatMLX/Core/Extensions/View+Extensions.swift b/ChatMLX/Core/Extensions/View+Extensions.swift index b49abcf..183b4b3 100644 --- a/ChatMLX/Core/Extensions/View+Extensions.swift +++ b/ChatMLX/Core/Extensions/View+Extensions.swift @@ -8,7 +8,7 @@ import SwiftUI struct SafeAreaInsetsKey: PreferenceKey { - static var defaultValue = EdgeInsets() + static let defaultValue = EdgeInsets() static func reduce(value: inout EdgeInsets, nextValue: () -> EdgeInsets) { value = nextValue() } diff --git a/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataClass.swift b/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataClass.swift index 4b82b69..70c313a 100644 --- a/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataClass.swift +++ b/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataClass.swift @@ -2,14 +2,12 @@ // Conversation+CoreDataClass.swift // ChatMLX // -// Created by John Mai on 2024/10/14. +// Created by John Mai on 2024/11/9. // // -import Foundation import CoreData +import Foundation @objc(Conversation) - class Conversation: NSManagedObject { - -} +class Conversation: NSManagedObject {} diff --git a/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataProperties.swift b/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataProperties.swift index 15a342f..9c79a2c 100644 --- a/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataProperties.swift +++ b/ChatMLX/Core/Models/CoreData Models/Conversation+CoreDataProperties.swift @@ -2,7 +2,7 @@ // Conversation+CoreDataProperties.swift // ChatMLX // -// Created by John Mai on 2024/10/14. +// Created by John Mai on 2024/11/9. // // @@ -17,11 +17,13 @@ extension Conversation { @NSManaged var createdAt: Date? @NSManaged var generateTime: Double @NSManaged var inferring: Bool - @NSManaged var maxLength: Int64 + @NSManaged var maxLength: Int @NSManaged var maxMessagesLimit: Int32 + @NSManaged var modelType: String? + @NSManaged var modelRaw: String? @NSManaged var promptTime: Double @NSManaged var promptTokensPerSecond: Double - @NSManaged var repetitionContextSize: Int64 + @NSManaged var repetitionContextSize: Int @NSManaged var repetitionPenalty: Float @NSManaged var systemPrompt: String? @NSManaged var temperature: Float @@ -33,8 +35,9 @@ extension Conversation { @NSManaged var useMaxMessagesLimit: Bool @NSManaged var useRepetitionPenalty: Bool @NSManaged var useSystemPrompt: Bool + @NSManaged var modelValue: String? + @NSManaged var modelProvider: String? @NSManaged var messages: [Message] - @NSManaged var model: ModelInfo? } // MARK: Generated accessors for messages diff --git a/ChatMLX/Core/Models/CoreData Models/Conversation.swift b/ChatMLX/Core/Models/CoreData Models/Conversation.swift index 0a3a65e..d495fb3 100644 --- a/ChatMLX/Core/Models/CoreData Models/Conversation.swift +++ b/ChatMLX/Core/Models/CoreData Models/Conversation.swift @@ -9,11 +9,56 @@ import CoreData import Defaults extension Conversation { - override func awakeFromInsert() { + // var modelIdentifier: ProviderModel.Identifier? { + // get { + // guard let modelType, let modelValue, let modelProvider, let provider = Provider(rawValue: modelProvider) else { + // return nil + // } + // + // switch modelType { + // case "id": + // return .id(modelValue, provider) + // case "directory": + // guard let url = URL(string: modelValue) else { return nil } + // return .directory(url, provider) + // default: + // return nil + // } + // } + // set { + // switch newValue { + // case .id(let idString, let provider): + // modelType = "id" + // modelValue = idString + // modelProvider = provider.rawValue + // case .directory(let url, let provider): + // modelType = "directory" + // modelValue = url.absoluteString + // modelProvider = provider.rawValue + // case nil: + // modelType = nil + // modelValue = nil + // modelProvider = nil + // } + // } + // } + + var model: ProviderModel.Identifier? { + get { + guard let modelRaw = modelRaw?.data(using: .utf8) else { return nil } + return try? JSONDecoder().decode(ProviderModel.Identifier.self, from: modelRaw) + } + set { + guard let data = try? JSONEncoder().encode(newValue) else { return } + modelRaw = String(data: data, encoding: .utf8) + } + } + + override public func awakeFromInsert() { super.awakeFromInsert() setPrimitiveValue(Defaults[.defaultTitle], forKey: #keyPath(Conversation.title)) -// setPrimitiveValue(Defaults[.defaultModel], forKey: #keyPath(Conversation.model)) + // setPrimitiveValue(Defaults[.defaultModel], forKey: #keyPath(Conversation.model)) setPrimitiveValue(Defaults[.defaultTemperature], forKey: #keyPath(Conversation.temperature)) setPrimitiveValue(Defaults[.defaultTopP], forKey: #keyPath(Conversation.topP)) @@ -45,7 +90,7 @@ extension Conversation { setPrimitiveValue(Date.now, forKey: #keyPath(Conversation.updatedAt)) } - override func willSave() { + override public func willSave() { super.willSave() setPrimitiveValue(Date.now, forKey: #keyPath(Conversation.updatedAt)) } @@ -75,7 +120,7 @@ extension Conversation { message.format() } - if self.useSystemPrompt, let systemPrompt = self.systemPrompt,!systemPrompt.isEmpty { + if self.useSystemPrompt, let systemPrompt = self.systemPrompt, !systemPrompt.isEmpty { dictionary.insert( self.formatMessage( role: .system, diff --git a/ChatMLX/Core/Models/CoreData Models/Message+CoreDataProperties.swift b/ChatMLX/Core/Models/CoreData Models/Message+CoreDataProperties.swift index 9f35cd9..694c978 100644 --- a/ChatMLX/Core/Models/CoreData Models/Message+CoreDataProperties.swift +++ b/ChatMLX/Core/Models/CoreData Models/Message+CoreDataProperties.swift @@ -20,7 +20,7 @@ extension Message { @NSManaged var inferring: Bool @NSManaged var roleRaw: String @NSManaged var updatedAt: Date? - @NSManaged var conversation: Conversation + @NSManaged var conversation: Conversation? } extension Message: Identifiable {} diff --git a/ChatMLX/Core/Models/CoreData Models/Message.swift b/ChatMLX/Core/Models/CoreData Models/Message.swift index 6bddb56..8ec2d88 100644 --- a/ChatMLX/Core/Models/CoreData Models/Message.swift +++ b/ChatMLX/Core/Models/CoreData Models/Message.swift @@ -13,12 +13,12 @@ extension Message { set { roleRaw = newValue.rawValue } } - override func awakeFromInsert() { + public override func awakeFromInsert() { setPrimitiveValue(Date.now, forKey: #keyPath(Message.createdAt)) setPrimitiveValue(Date.now, forKey: #keyPath(Message.updatedAt)) } - override func willSave() { + public override func willSave() { super.willSave() setPrimitiveValue(Date.now, forKey: #keyPath(Message.updatedAt)) } @@ -52,7 +52,10 @@ extension Message { } func suffixMessages() -> [Message] { - let conversation = self.conversation + guard let conversation = self.conversation else { + return [] + } + let messages = conversation.messages guard let index = messages.firstIndex(of: self) else { diff --git a/ChatMLX/Core/Models/CoreData Models/ModelInfo.swift b/ChatMLX/Core/Models/CoreData Models/ModelInfo.swift index d3396f1..e258bba 100644 --- a/ChatMLX/Core/Models/CoreData Models/ModelInfo.swift +++ b/ChatMLX/Core/Models/CoreData Models/ModelInfo.swift @@ -7,7 +7,7 @@ extension ModelInfo { var provider: Provider { - get { Provider(rawValue: providerRaw) ?? .mlx } + get { Provider(rawValue: providerRaw ?? "mlx") ?? .mlx } set { providerRaw = newValue.rawValue } } } diff --git a/ChatMLX/Core/Models/DownloadTask.swift b/ChatMLX/Core/Models/DownloadTask.swift index e366242..c37256e 100644 --- a/ChatMLX/Core/Models/DownloadTask.swift +++ b/ChatMLX/Core/Models/DownloadTask.swift @@ -23,7 +23,7 @@ class DownloadTask: Identifiable, Equatable { var isDownloading = false var isCompleted = false var error: Error? - var hub: HubApi? +// var hub: HubApi? var totalUnitCount: Int64 = 0 var completedUnitCount: Int64 = 0 @@ -33,55 +33,55 @@ class DownloadTask: Identifiable, Equatable { } func start() { - self.isDownloading = true - self.error = nil - self.progress = 0 - - // todo: token & custom endpoint - let currentEndpoint = Defaults[.huggingFaceEndpoint] - self.hub = HubApi( - downloadBase: FileManager.default.temporaryDirectory, endpoint: currentEndpoint - ) - - Task { [self] in - do { - let repo = Hub.Repo(id: self.repoId) - let temporaryModelDirectory = try await self.hub!.snapshot( - from: repo, matching: ["*.safetensors", "*.json"] - ) { progress in - Task { @MainActor in - self.progress = progress.fractionCompleted - self.totalUnitCount = progress.totalUnitCount - self.completedUnitCount = progress.completedUnitCount - } - } - - self.hub = nil - - try await self.moveToDocumentsDirectory(from: temporaryModelDirectory) - - await MainActor.run { - self.isDownloading = false - self.isCompleted = true - self.progress = 1.0 - } - } catch { - self.logger.error("DownloadTask Error: \(error.localizedDescription)") - self.hub = nil - await MainActor.run { - self.error = error - self.isDownloading = false - } - } - } +// self.isDownloading = true +// self.error = nil +// self.progress = 0 +// +// // todo: token & custom endpoint +// let currentEndpoint = Defaults[.huggingFaceEndpoint] +// self.hub = HubApi( +// downloadBase: FileManager.default.temporaryDirectory, endpoint: currentEndpoint +// ) +// +// Task { [self] in +// do { +// let repo = Hub.Repo(id: self.repoId) +// let temporaryModelDirectory = try await self.hub!.snapshot( +// from: repo, matching: ["*.safetensors", "*.json"] +// ) { progress in +// Task { @MainActor in +// self.progress = progress.fractionCompleted +// self.totalUnitCount = progress.totalUnitCount +// self.completedUnitCount = progress.completedUnitCount +// } +// } +// +// self.hub = nil +// +// try await self.moveToDocumentsDirectory(from: temporaryModelDirectory) +// +// await MainActor.run { +// self.isDownloading = false +// self.isCompleted = true +// self.progress = 1.0 +// } +// } catch { +// self.logger.error("DownloadTask Error: \(error.localizedDescription)") +// self.hub = nil +// await MainActor.run { +// self.error = error +// self.isDownloading = false +// } +// } +// } } func stop() { - if let hub { - hub.cancelCurrentDownload() - self.isDownloading = false - self.hub = nil - } +// if let hub { +// hub.cancelCurrentDownload() +// self.isDownloading = false +// self.hub = nil +// } } private func moveToDocumentsDirectory(from temporaryModelDirectory: URL) async throws { diff --git a/ChatMLX/Core/Models/Language.swift b/ChatMLX/Core/Models/Language.swift index e6cf078..f710fde 100644 --- a/ChatMLX/Core/Models/Language.swift +++ b/ChatMLX/Core/Models/Language.swift @@ -92,7 +92,7 @@ enum Language: String, CaseIterable, Identifiable, Defaults.Serializable { case .thai: return "ไทย" case .turkish: return "Türkçe" case .ukrainian: return "Українська" - case .vietnamese: return "Tiếng Việt" + case .vietnamese: return "Tiếng Việt" } } } diff --git a/ChatMLX/Core/Models/Migrations/V2MigrationPolicy.swift b/ChatMLX/Core/Models/Migrations/V2MigrationPolicy.swift index 5eb27ec..249fa73 100644 --- a/ChatMLX/Core/Models/Migrations/V2MigrationPolicy.swift +++ b/ChatMLX/Core/Models/Migrations/V2MigrationPolicy.swift @@ -8,11 +8,14 @@ import CoreData class V2MigrationPolicy: NSEntityMigrationPolicy { - override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws { + override func createDestinationInstances( + forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager + ) throws { let sourceKeys = sInstance.entity.attributesByName.keys let sourceValues = sInstance.dictionaryWithValues(forKeys: sourceKeys.map { $0 as String }) - let destinationInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext) + let destinationInstance = NSEntityDescription.insertNewObject( + forEntityName: mapping.destinationEntityName!, into: manager.destinationContext) let destinationKeys = destinationInstance.entity.attributesByName.keys.map { $0 as String } for key in destinationKeys { diff --git a/ChatMLX/Core/Models/Model.swift b/ChatMLX/Core/Models/Model.swift new file mode 100644 index 0000000..f44357c --- /dev/null +++ b/ChatMLX/Core/Models/Model.swift @@ -0,0 +1,6 @@ +// +// Model.swift +// ChatMLX +// +// Created by John Mai on 2024/11/9. +// diff --git a/ChatMLX/Core/Models/ProviderModel.swift b/ChatMLX/Core/Models/ProviderModel.swift index 4f812b0..0ad1444 100644 --- a/ChatMLX/Core/Models/ProviderModel.swift +++ b/ChatMLX/Core/Models/ProviderModel.swift @@ -5,11 +5,58 @@ // Created by John Mai on 2024/10/14. // +import Defaults import Foundation -struct ProviderModel { - let id: String - let provider: Provider +struct ProviderModel: Identifiable { + enum Identifier: Sendable, Equatable, Hashable, Codable, Defaults.Serializable { + case id(String, Provider) + case directory(URL, Provider) + + private enum IdentifierType: String, Codable { + case id + case directory + } + + private enum CodingKeys: String, CodingKey { + case type + case provider + case model + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .id(let model, let provider): + try container.encode(IdentifierType.id, forKey: .type) + try container.encode(model, forKey: .model) + try container.encode(provider, forKey: .provider) + + case .directory(let url, let provider): + try container.encode(IdentifierType.directory, forKey: .type) + try container.encode(url.absoluteString, forKey: .model) + try container.encode(provider, forKey: .provider) + } + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(IdentifierType.self, forKey: .type) + switch type { + case .id: + let model = try container.decode(String.self, forKey: .model) + let provider = try container.decode(Provider.self, forKey: .provider) + self = .id(model, provider) + case .directory: + let model = try container.decode(String.self, forKey: .model) + let provider = try container.decode(Provider.self, forKey: .provider) + self = .directory(URL(string: model)!, provider) + } + } + } + + let id: Identifier let name: String? let path: URL? let maxInputLength: Int? @@ -18,8 +65,7 @@ struct ProviderModel { let vision: Bool init( - id: String, - provider: Provider, + id: Identifier, name: String? = nil, path: URL? = nil, maxInputLength: Int? = nil, @@ -28,7 +74,6 @@ struct ProviderModel { vision: Bool = false ) { self.id = id - self.provider = provider self.name = name self.path = path self.maxInputLength = maxInputLength @@ -38,19 +83,10 @@ struct ProviderModel { } } -extension ProviderModel: Hashable {} - -extension ProviderModel { - init(from modelInfo: ModelInfo) { - self.init( - id: modelInfo.id, - provider: modelInfo.provider, - name: modelInfo.name, - path: modelInfo.path, - maxInputLength: Int(modelInfo.maxInputLength), - maxOutputLength: Int(modelInfo.maxOutputLength), - toolCall: modelInfo.toolCall, - vision: modelInfo.vision - ) +extension ProviderModel: Equatable { + static func == (lhs: ProviderModel, rhs: ProviderModel) -> Bool { + lhs.id == rhs.id } } + +extension ProviderModel: Hashable {} diff --git a/ChatMLX/Core/Models/SettingsTab.swift b/ChatMLX/Core/Models/SettingsTab.swift index 61629ea..bb2278b 100644 --- a/ChatMLX/Core/Models/SettingsTab.swift +++ b/ChatMLX/Core/Models/SettingsTab.swift @@ -17,6 +17,7 @@ struct SettingsTab: Identifiable, Equatable { case defaultConversation = "Default Conversation" case huggingFace = "Hugging Face" case models = "Models" + case providers = "Providers" case mlxCommunity = "MLX Community" case downloadManager = "Download Manager" case experimentalFeatures = "Experimental Features" @@ -25,9 +26,9 @@ struct SettingsTab: Identifiable, Equatable { let id: ID let icon: Image - let showIndicator: ((SettingsViewModel) -> Bool)? + let showIndicator: (() -> Bool)? - init(_ id: ID, _ icon: Image, showIndicator: ((SettingsViewModel) -> Bool)? = nil) { + init(_ id: ID, _ icon: Image, showIndicator: (() -> Bool)? = nil) { self.id = id self.icon = icon self.showIndicator = showIndicator diff --git a/ChatMLX/Core/Utilities/PersistenceController.swift b/ChatMLX/Core/Persistence/PersistenceController.swift similarity index 60% rename from ChatMLX/Core/Utilities/PersistenceController.swift rename to ChatMLX/Core/Persistence/PersistenceController.swift index 53ae752..793dc27 100644 --- a/ChatMLX/Core/Utilities/PersistenceController.swift +++ b/ChatMLX/Core/Persistence/PersistenceController.swift @@ -19,7 +19,7 @@ struct PersistenceController { let container: NSPersistentContainer init(inMemory: Bool = false) { - container = NSPersistentContainer(name: "ChatMLX") + container = NSPersistentContainer(name: "ChatMLX2") if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } @@ -49,46 +49,46 @@ struct PersistenceController { container.viewContext } -// func exisits( -// _ model: T, -// in context: NSManagedObjectContext -// ) -> T? { -// try? context.existingObject(with: model.objectID) as? T -// } -// -// func delete(_ model: some NSManagedObject) throws { -// if let existingContact = exisits(model, in: container.viewContext) { -// container.viewContext.delete(existingContact) -// Task(priority: .background) { -// try await container.viewContext.perform { -// try container.viewContext.save() -// } -// } -// } -// } -// -// func clear(_ entityName: String) throws -> [NSManagedObjectID] { -// let fetchRequest: NSFetchRequest = NSFetchRequest( -// entityName: entityName) -// let batchDeteleRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) -// batchDeteleRequest.resultType = .resultTypeObjectIDs -// -// if let fetchResult = try container.viewContext.execute(batchDeteleRequest) -// as? NSBatchDeleteResult, -// let deletedManagedObjectIds = fetchResult.result as? [NSManagedObjectID], -// !deletedManagedObjectIds.isEmpty -// { -// return deletedManagedObjectIds -// } -// -// return [] -// } -// - func save() throws { - Task.detached(priority: .background) { - try await viewContext.saveIfNeeded() - } - } + // func exisits( + // _ model: T, + // in context: NSManagedObjectContext + // ) -> T? { + // try? context.existingObject(with: model.objectID) as? T + // } + // + // func delete(_ model: some NSManagedObject) throws { + // if let existingContact = exisits(model, in: container.viewContext) { + // container.viewContext.delete(existingContact) + // Task(priority: .background) { + // try await container.viewContext.perform { + // try container.viewContext.save() + // } + // } + // } + // } + // + // func clear(_ entityName: String) throws -> [NSManagedObjectID] { + // let fetchRequest: NSFetchRequest = NSFetchRequest( + // entityName: entityName) + // let batchDeteleRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + // batchDeteleRequest.resultType = .resultTypeObjectIDs + // + // if let fetchResult = try container.viewContext.execute(batchDeteleRequest) + // as? NSBatchDeleteResult, + // let deletedManagedObjectIds = fetchResult.result as? [NSManagedObjectID], + // !deletedManagedObjectIds.isEmpty + // { + // return deletedManagedObjectIds + // } + // + // return [] + // } + // + // func save() throws { + // Task.detached(priority: .background) { + // try await viewContext.saveIfNeeded() + // } + // } func executeAndMergeChanges(using request: NSBatchDeleteRequest, in context: NSManagedObjectContext) throws { try executeAndMergeChanges(using: [request], in: context) @@ -99,7 +99,9 @@ struct PersistenceController { mergeChanges(changes) } - private func execute(request: NSBatchDeleteRequest, in context: NSManagedObjectContext) throws -> [NSManagedObjectID] { + private func execute(request: NSBatchDeleteRequest, in context: NSManagedObjectContext) throws + -> [NSManagedObjectID] + { request.resultType = .resultTypeObjectIDs let result = try context.execute(request) as? NSBatchDeleteResult return result?.result as? [NSManagedObjectID] ?? [] @@ -125,7 +127,7 @@ struct PersistenceController { let object = try context.existingObject(with: id) context.delete(object) if saveImmediately { - try save() + // try save() } } } diff --git a/ChatMLX/Core/Persistence/Repository.swift b/ChatMLX/Core/Persistence/Repository.swift new file mode 100644 index 0000000..aae5f28 --- /dev/null +++ b/ChatMLX/Core/Persistence/Repository.swift @@ -0,0 +1,91 @@ +// +// Repository.swift +// ChatMLX +// +// Created by John Mai on 2024/11/3. +// + +import CoreData +import CoreDataEvolution + +@NSModelActor +actor Repository { + @discardableResult + func createConversation() throws -> NSManagedObjectID { + let conversation = Conversation(context: modelContext) + try modelContext.saveChanges() + return conversation.objectID + } + + private func deleteConversation(_ conversation: Conversation) throws { + modelContext.delete(conversation) + try modelContext.saveChanges() + } + + func deleteConversation(_ objectID: NSManagedObjectID) throws { + guard let conversation = self[objectID, as: Conversation.self] else { + fatalError("Can't load Conversation by ID:\(objectID)") + } + try deleteConversation(conversation) + } + + func deleteMessages(_ messages: [Message]) throws { + try deleteMessages(messages.map(\.objectID)) + } + + func deleteMessages(_ objectIDs: [NSManagedObjectID]) throws { + let request = NSBatchDeleteRequest(objectIDs: objectIDs) + request.resultType = .resultTypeObjectIDs + let result = try modelContext.execute(request) as? NSBatchDeleteResult + let changes = result?.result as? [NSManagedObjectID] ?? [] + guard !changes.isEmpty else { return } + + mergeChanges([NSDeletedObjectIDsKey: changes]) + } + + func mergeChanges(_ fromRemoteContextSave: [AnyHashable: Any]) { + NSManagedObjectContext.mergeChanges( + fromRemoteContextSave: fromRemoteContextSave, + into: [modelContainer.viewContext] + ) + } + + func clearMessages() throws -> [NSManagedObjectID] { + let request = NSBatchDeleteRequest(fetchRequest: Message.fetchRequest()) + request.resultType = .resultTypeObjectIDs + + let result = try modelContext.execute(request) as? NSBatchDeleteResult + + return result?.result as? [NSManagedObjectID] ?? [] + } + + func clearConversations() throws -> [NSManagedObjectID] { + let request = NSBatchDeleteRequest(fetchRequest: Conversation.fetchRequest()) + request.resultType = .resultTypeObjectIDs + + let result = try modelContext.execute(request) as? NSBatchDeleteResult + + return result?.result as? [NSManagedObjectID] ?? [] + } + + @discardableResult + func createMessage(in conversationObjectID: NSManagedObjectID, content: String, role: Role) throws + -> NSManagedObjectID + { + try createMessage(in: conversationObjectID, content: content, role: role).objectID + } + + @discardableResult + func createMessage(in conversationObjectID: NSManagedObjectID, content: String, role: Role) throws -> Message { + guard let conversation = self[conversationObjectID, as: Conversation.self] else { + fatalError("Can't load Conversation by ID:\(conversationObjectID)") + } + + let message = Message(context: modelContext) + message.conversation = conversation + message.content = content + message.role = role + try modelContext.saveChanges() + return message + } +} diff --git a/ChatMLX/Core/Services/AI/MLXProvider.swift b/ChatMLX/Core/Services/AI/MLXProvider.swift index 58fdd8b..de0bdf2 100644 --- a/ChatMLX/Core/Services/AI/MLXProvider.swift +++ b/ChatMLX/Core/Services/AI/MLXProvider.swift @@ -1,157 +1,158 @@ +//// +//// MLXProvider.swift +//// ChatMLX +//// +//// Created by John Mai on 2024/10/13. +//// // -// MLXProvider.swift -// ChatMLX -// -// Created by John Mai on 2024/10/13. -// - -import Defaults -import Metal -import MLX -import MLXLLM -import MLXRandom -import os -import SwiftUI -import Tokenizers - -struct MLXProvider: BaseProvider { - // MARK: - Properties - - private let displayEveryNTokens = 4 - private let maxTokens = 240 - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MLXProvider") - private let fileManager = FileManager.default - - // MARK: - Chat - - func chat( - messages: [[String: String]], - config: ModelConfig, - onResult: @escaping ChatResultHandler - ) async { - do { - guard let modelPath = config.model.path else { - throw LLMRunnerError.modelConfigurationNotSet - } - - let modelContainer = try await MLXLLM.ModelContainer( - hub: .init(), - modelDirectory: modelPath, - configuration: ModelConfiguration(directory: modelPath) - ) - - let tokens = try await modelContainer.perform { _, tokenizer in - try tokenizer.applyChatTemplate(messages: messages) - } - - MLXRandom.seed(UInt64(Date.timeIntervalSinceReferenceDate * 1000)) - - let result = await modelContainer.perform { - model, tokenizer in - MLXLLM.generate( - promptTokens: tokens, - parameters: GenerateParameters(from: config), - model: model, - tokenizer: tokenizer, - extraEOSTokens: Set(config.stop ?? [ - "<|im_end|>", - "<|end|>", - ]) - ) { tokens in - if tokens.count % displayEveryNTokens == 0 { - let content = tokenizer.decode(tokens: tokens) - onResult( - .success( - .init( - content: content, - usage: nil - ) - ) - ) - } - - if config.useMaxTokens, tokens.count >= config.maxTokens ?? maxTokens { - return .stop - } else { - return .more - } - } - } - - onResult( - .success( - .init( - content: result.output, - usage: .init( - promptTokens: result.promptTokens.count, - promptTime: result.promptTime, - promptTokensPerSecond: result.promptTokensPerSecond, - completionTokens: result.tokens.count, - completionTime: result.generateTime, - completionTokensPerSecond: result.tokensPerSecond, - totalTokens: result.tokens.count + result.promptTokens.count - ) - ) - ) - ) - } catch { - onResult(.failure(error)) - } - } - - // MARK: - Fetch Models - - static func fetchModels() throws -> [ProviderModel] { - let fileManager = FileManager.default - - guard let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { - return [] - } - - let organizations = try fileManager.contentsOfDirectory( - at: documentsURL.appendingPathComponent("huggingface/models"), - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - - guard !organizations.isEmpty else { - return [] - } - - return try organizations.flatMap { organization -> [ProviderModel] in - guard organization.hasDirectoryPath else { - return [] - } - - let huggingfaceModels = try fileManager.contentsOfDirectory( - at: organization, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - - return try huggingfaceModels.compactMap { model -> ProviderModel? in - guard model.hasDirectoryPath else { - return nil - } - - let data = try Data(contentsOf: model.appendingPathComponent("tokenizer_config.json")) - let tokenizerConfig = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] - - let modelMaxLength = tokenizerConfig?["model_max_length"] as? Int - let toolCall = (tokenizerConfig?["chat_template"] as? String)?.contains("tool_calls") ?? false - let vision = false - - return .init( - id: model.absoluteString, - provider: .mlx, - name: model.lastPathComponent, - path: model, - maxInputLength: modelMaxLength, - maxOutputLength: modelMaxLength, - toolCall: toolCall, - vision: false - ) - } - } - } -} +//import Defaults +//import Metal +//import MLX +//import MLXLLM +//import MLXRandom +//import os +//import SwiftUI +//import Tokenizers +// +//struct MLXProvider: BaseProvider { +// // MARK: - Properties +// +// private let displayEveryNTokens = 4 +// private let maxTokens = 240 +// private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MLXProvider") +// private let fileManager = FileManager.default +// +// // MARK: - Chat +// +// func chat( +// messages: [[String: String]], +// config: ModelConfig, +// onResult: @escaping ChatResultHandler +// ) async { +// do { +// guard let modelPath = config.model.path else { +// throw LLMRunnerError.modelConfigurationNotSet +// } +// +// let modelContainer = try await MLXLLM.ModelContainer( +// hub: .init(), +// modelDirectory: modelPath, +// configuration: ModelConfiguration(directory: modelPath) +// ) +// +// let tokens = try await modelContainer.perform { _, tokenizer in +// try tokenizer.applyChatTemplate(messages: messages) +// } +// +// MLXRandom.seed(UInt64(Date.timeIntervalSinceReferenceDate * 1000)) +// +// let result = await modelContainer.perform { +// model, tokenizer in +// MLXLLM.generate( +// promptTokens: tokens, +// parameters: GenerateParameters(from: config), +// model: model, +// tokenizer: tokenizer, +// extraEOSTokens: Set(config.stop ?? [ +// "<|im_end|>", +// "<|end|>", +// ]) +// ) { tokens in +// if tokens.count % displayEveryNTokens == 0 { +// let content = tokenizer.decode(tokens: tokens) +// onResult( +// .success( +// .init( +// content: content, +// usage: nil +// ) +// ) +// ) +// } +// +// if config.useMaxTokens, tokens.count >= config.maxTokens ?? maxTokens { +// return .stop +// } else { +// return .more +// } +// } +// } +// +// onResult( +// .success( +// .init( +// content: result.output, +// usage: .init( +// promptTokens: result.promptTokens.count, +// promptTime: result.promptTime, +// promptTokensPerSecond: result.promptTokensPerSecond, +// completionTokens: result.tokens.count, +// completionTime: result.generateTime, +// completionTokensPerSecond: result.tokensPerSecond, +// totalTokens: result.tokens.count + result.promptTokens.count +// ) +// ) +// ) +// ) +// } catch { +// onResult(.failure(error)) +// } +// } +// +// // MARK: - Fetch Models +// +// static func fetchModels() throws -> [ProviderModel] { +// let fileManager = FileManager.default +// +// guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { +// return [] +// } +// +// let organizations = try fileManager.contentsOfDirectory( +// at: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("huggingface/models"), +// includingPropertiesForKeys: nil, +// options: [.skipsHiddenFiles] +// ) +// +// guard !organizations.isEmpty else { +// return [] +// } +// +// return try organizations.flatMap { organization -> [ProviderModel] in +// guard organization.hasDirectoryPath else { +// return [] +// } +// +// let huggingfaceModels = try fileManager.contentsOfDirectory( +// at: organization, +// includingPropertiesForKeys: nil, +// options: [.skipsHiddenFiles] +// ) +// +//// return try huggingfaceModels.compactMap { model -> ProviderModel? in +//// guard model.hasDirectoryPath else { +//// return nil +//// } +//// +//// let data = try Data(contentsOf: model.appendingPathComponent("tokenizer_config.json")) +//// let tokenizerConfig = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] +//// +//// let modelMaxLength = tokenizerConfig?["model_max_length"] as? Int +//// let toolCall = (tokenizerConfig?["chat_template"] as? String)?.contains("tool_calls") ?? false +//// let vision = false +//// +//// return .init( +//// id: model.absoluteString, +//// provider: .mlx, +//// name: model.lastPathComponent, +//// path: model, +//// maxInputLength: modelMaxLength, +//// maxOutputLength: modelMaxLength, +//// toolCall: toolCall, +//// vision: false +//// ) +//// } +// return [] +// } +// } +//} diff --git a/ChatMLX/Core/Services/AI/OpenAIProvider.swift b/ChatMLX/Core/Services/AI/OpenAIProvider.swift index b1c320d..ec999ff 100644 --- a/ChatMLX/Core/Services/AI/OpenAIProvider.swift +++ b/ChatMLX/Core/Services/AI/OpenAIProvider.swift @@ -1,108 +1,108 @@ +//// +//// OpenAIProvider.swift +//// ChatMLX +//// +//// Created by John Mai on 2024/10/17. +//// // -// OpenAIProvider.swift -// ChatMLX +//import Defaults +//import Foundation +//import OpenAI +//import SwiftUI // -// Created by John Mai on 2024/10/17. +//struct OpenAIProvider: BaseProvider { +// static func fetchModels() -> [ProviderModel] { +// [ +// // GPT-4o +// .init( +// id: "gpt-4o", +// provider: .openAI, +// name: "GPT-4o", +// maxInputLength: 128000, +// maxOutputLength: 16384, +// toolCall: true, +// vision: true +// ), +// // GPT-4o mini +// .init( +// id: "gpt-4o-mini", +// provider: .openAI, +// name: "GPT-4o Mini", +// maxInputLength: 128000, +// maxOutputLength: 16384, +// toolCall: true, +// vision: true +// ), +// // o1-preview and o1-mini +// .init( +// id: "o1-preview", +// provider: .openAI, +// name: "O1 Preview", +// maxInputLength: 128000, +// maxOutputLength: 32768 +// ), +// .init( +// id: "o1-mini", +// provider: .openAI, +// name: "O1 Mini", +// maxInputLength: 128000, +// maxOutputLength: 65536 +// ), +// // GPT-4 Turbo and GPT-4 +// .init( +// id: "gpt-4-turbo", +// provider: .openAI, +// name: "GPT-4 Turbo", +// maxInputLength: 128000, +// maxOutputLength: 4096 +// ), +// .init( +// id: "gpt-4", +// provider: .openAI, +// name: "GPT-4", +// maxInputLength: 8192, +// maxOutputLength: 8192 +// ), +// // GPT-3.5 Turbo +// .init( +// id: "gpt-3.5-turbo", +// provider: .openAI, +// name: "GPT-3.5 Turbo", +// maxInputLength: 16385, +// maxOutputLength: 4096 +// ) +// ] +// } // - -import Defaults -import Foundation -import OpenAI -import SwiftUI - -struct OpenAIProvider: BaseProvider { - static func fetchModels() -> [ProviderModel] { - [ - // GPT-4o - .init( - id: "gpt-4o", - provider: .openAI, - name: "GPT-4o", - maxInputLength: 128000, - maxOutputLength: 16384, - toolCall: true, - vision: true - ), - // GPT-4o mini - .init( - id: "gpt-4o-mini", - provider: .openAI, - name: "GPT-4o Mini", - maxInputLength: 128000, - maxOutputLength: 16384, - toolCall: true, - vision: true - ), - // o1-preview and o1-mini - .init( - id: "o1-preview", - provider: .openAI, - name: "O1 Preview", - maxInputLength: 128000, - maxOutputLength: 32768 - ), - .init( - id: "o1-mini", - provider: .openAI, - name: "O1 Mini", - maxInputLength: 128000, - maxOutputLength: 65536 - ), - // GPT-4 Turbo and GPT-4 - .init( - id: "gpt-4-turbo", - provider: .openAI, - name: "GPT-4 Turbo", - maxInputLength: 128000, - maxOutputLength: 4096 - ), - .init( - id: "gpt-4", - provider: .openAI, - name: "GPT-4", - maxInputLength: 8192, - maxOutputLength: 8192 - ), - // GPT-3.5 Turbo - .init( - id: "gpt-3.5-turbo", - provider: .openAI, - name: "GPT-3.5 Turbo", - maxInputLength: 16385, - maxOutputLength: 4096 - ) - ] - } - - func chat( - messages: [[String: String]], - config: ModelConfig, - onResult: @escaping ChatResultHandler - ) async { - let apiKey = Defaults[.openAIApiKey] - let baseURL = Defaults[.openAIBaseURL] - - let client = OpenAI(configuration: .init(token: apiKey, host: baseURL)) - - var chatMessages: [ChatQuery.ChatCompletionMessageParam] = [] - for message in messages { - chatMessages.append(.init(role: message["role"] == "user" ? .user : .assistant, content: message["content"]!)!) - } - - let query = ChatQuery(messages: chatMessages, model: config.model.name) - - do { - var content = "" - for try await result in client.chatsStream(query: query) { - for choice in result.choices { - if let c = choice.delta.content { - content += c - } - onResult(.success(.init(content: content, usage: nil))) - } - } - } catch { - onResult(.failure(error)) - } - } -} +// func chat( +// messages: [[String: String]], +// config: ModelConfig, +// onResult: @escaping ChatResultHandler +// ) async { +// let apiKey = Defaults[.openAIApiKey] +// let baseURL = Defaults[.openAIBaseURL] +// +// let client = OpenAI(configuration: .init(token: apiKey, host: baseURL)) +// +// var chatMessages: [ChatQuery.ChatCompletionMessageParam] = [] +// for message in messages { +// chatMessages.append(.init(role: message["role"] == "user" ? .user : .assistant, content: message["content"]!)!) +// } +// +// let query = ChatQuery(messages: chatMessages, model: config.model.name) +// +// do { +// var content = "" +// for try await result in client.chatsStream(query: query) { +// for choice in result.choices { +// if let c = choice.delta.content { +// content += c +// } +// onResult(.success(.init(content: content, usage: nil))) +// } +// } +// } catch { +// onResult(.failure(error)) +// } +// } +//} diff --git a/ChatMLX/Core/Services/AI/ProviderFactory.swift b/ChatMLX/Core/Services/AI/ProviderFactory.swift index 56955f7..9e0218a 100644 --- a/ChatMLX/Core/Services/AI/ProviderFactory.swift +++ b/ChatMLX/Core/Services/AI/ProviderFactory.swift @@ -1,45 +1,46 @@ +//// +//// ProviderFactory.swift +//// ChatMLX +//// +//// Created by John Mai on 2024/10/13. +//// // -// ProviderFactory.swift -// ChatMLX +//import Defaults // -// Created by John Mai on 2024/10/13. +//actor ProviderFactory { +// static let shared = ProviderFactory() // - -import Defaults - -actor ProviderFactory { - static let shared = ProviderFactory() - - private init() {} - - private var providers: [Provider: BaseProvider] = [:] - - func provider(_ type: Provider) async -> BaseProvider { - if let provider = providers[type] { - return provider - } - - let provider: BaseProvider = switch type { - case .mlx: - MLXProvider() - case .openAI: - OpenAIProvider() - } - - providers[type] = provider - - return provider - } - -// func fetchModels() -> [ProviderModel] { -// var models: [ProviderModel] = [] -// -// models = models + MLXProvider.fetchModels() -// -// if Defaults[.enableOpenAI] { -// models = models + OpenAIProvider.fetchModels() -// } -// -// return models +// private init() {} +// +// private var providers: [Provider: BaseProvider] = [:] +// +// func provider(_ type: Provider) async -> BaseProvider { +//// if let provider = providers[type] { +//// return provider +//// } +//// +//// let provider: BaseProvider = switch type { +//// case .mlx: +//// MLXProvider() +//// case .openAI: +//// OpenAIProvider() +//// } +//// +//// providers[type] = provider +//// +//// return provider +// return MLXProvider() // } -} +// +//// func fetchModels() -> [ProviderModel] { +//// var models: [ProviderModel] = [] +//// +//// models = models + MLXProvider.fetchModels() +//// +//// if Defaults[.enableOpenAI] { +//// models = models + OpenAIProvider.fetchModels() +//// } +//// +//// return models +//// } +//} diff --git a/ChatMLX/Core/Services/ChatService.swift b/ChatMLX/Core/Services/ChatService.swift new file mode 100644 index 0000000..2fa5b17 --- /dev/null +++ b/ChatMLX/Core/Services/ChatService.swift @@ -0,0 +1,42 @@ +// +// ChatService.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +class ChatService { + private var strategy: ChatStrategy + private var model: ProviderModel + + init(_ model: ProviderModel) { + self.model = model + + let provider: Provider = + switch model.id { + case .id(_, let provider): + provider + case .directory(_, let provider): + provider + } + + switch provider { + case .mlx: + strategy = MLXChatStrategy() + case .openAI: + strategy = OpenAIChatStrategy() + } + } + + func setStrategy(_ strategy: ChatStrategy) { + self.strategy = strategy + } + + func send(messages: [[String: String]], parameters: ChatParameters) async throws -> AsyncStream { + try await strategy.send( + model: model, + messages: messages, + parameters: parameters + ) + } +} diff --git a/ChatMLX/Core/Services/ChatStrategies/ChatStrategy.swift b/ChatMLX/Core/Services/ChatStrategies/ChatStrategy.swift new file mode 100644 index 0000000..f6696c2 --- /dev/null +++ b/ChatMLX/Core/Services/ChatStrategies/ChatStrategy.swift @@ -0,0 +1,39 @@ +// +// ChatStrategy.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +struct ChatParameters: Sendable { + var temperature: Float = 0.6 + var topP: Float = 1.0 + var repetitionPenalty: Float? + var repetitionContextSize: Int = 20 + var extraEOSTokens: Set? + var maxTokens: Int = 1024 + + init( + temperature: Float = 0.6, + topP: Float = 1.0, + repetitionPenalty: Float? = nil, + repetitionContextSize: Int = 20, + extraEOSTokens: Set? = nil, + maxTokens: Int = 1024 + ) { + self.temperature = temperature + self.topP = topP + self.repetitionPenalty = repetitionPenalty + self.repetitionContextSize = repetitionContextSize + self.extraEOSTokens = extraEOSTokens + self.maxTokens = maxTokens + } +} + +protocol ChatStrategy { + func send( + model: ProviderModel, + messages: [[String: String]], + parameters: ChatParameters? + ) async throws -> AsyncStream +} diff --git a/ChatMLX/Core/Services/ChatStrategies/MLXChatStrategy.swift b/ChatMLX/Core/Services/ChatStrategies/MLXChatStrategy.swift new file mode 100644 index 0000000..3b75967 --- /dev/null +++ b/ChatMLX/Core/Services/ChatStrategies/MLXChatStrategy.swift @@ -0,0 +1,106 @@ +// +// MLXChatService.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +import Defaults +import MLX +import MLXLLM +import MLXRandom +import Metal +import SwiftUI +import Tokenizers +import os + +actor MLXModelManager { + enum LoadState { + case idle + case loaded(ProviderModel, ModelContainer) + } + + var state: LoadState = .idle + var running: Bool = false + + func load(model: ProviderModel) async throws -> ModelContainer? { + switch state { + case .idle: + MLX.GPU.set(cacheLimit: 20 * 1024 * 1024) + + guard case .directory(let modelURL, _) = model.id else { + return nil + } + + let modelConfiguration = ModelConfiguration(directory: modelURL) + let modelContainer = try await MLXLLM.loadModelContainer(configuration: modelConfiguration) + + state = .loaded(model, modelContainer) + + return modelContainer + case .loaded(let m, let modelContainer): + if model != m { + state = .idle + return try await load(model: model) + } + + return modelContainer + } + } +} + +class MLXChatStrategy: ChatStrategy { + func send( + model: ProviderModel, + messages: [[String: String]], + parameters: ChatParameters? = nil + ) async throws -> AsyncStream { + AsyncStream { continuation in + Task { + let manager = MLXModelManager() + + guard let modelContainer = try await manager.load(model: model) else { + continuation.finish() + return + } + + let promptTokens = try await modelContainer.perform { _, tokenizer in + try tokenizer.applyChatTemplate(messages: messages) + } + + let result = await modelContainer.perform { model, tokenizer in + var generateParameters = GenerateParameters() + + if let parameters { + generateParameters.temperature = parameters.temperature + generateParameters.topP = parameters.topP + generateParameters.repetitionContextSize = parameters.repetitionContextSize + generateParameters.repetitionPenalty = parameters.repetitionPenalty + } + + return MLXLLM.generate( + promptTokens: promptTokens, + parameters: generateParameters, + model: model, + tokenizer: tokenizer, + extraEOSTokens: [] + ) { tokens in + if tokens.count % 4 == 0 { + let text = tokenizer.decode(tokens: tokens) + continuation.yield(text) + } + + if tokens.count >= parameters?.maxTokens ?? 1024 { + return .stop + } else { + return .more + } + } + } + + continuation.yield(result.output) + continuation.finish() + } + } + } +} diff --git a/ChatMLX/Core/Services/ChatStrategies/OpenAIChatStrategy.swift b/ChatMLX/Core/Services/ChatStrategies/OpenAIChatStrategy.swift new file mode 100644 index 0000000..afc7216 --- /dev/null +++ b/ChatMLX/Core/Services/ChatStrategies/OpenAIChatStrategy.swift @@ -0,0 +1,19 @@ +// +// OpenAIChatStrategy.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +class OpenAIChatStrategy: ChatStrategy { + func send( + model: ProviderModel, + messages: [[String: String]], + parameters: ChatParameters? + ) async throws -> AsyncStream { + AsyncStream { continuation in + continuation.yield("OpenAI: \(messages)") + continuation.finish() + } + } +} diff --git a/ChatMLX/Core/Services/HuggingFaceService.swift b/ChatMLX/Core/Services/HuggingFaceService.swift new file mode 100644 index 0000000..41fa4a6 --- /dev/null +++ b/ChatMLX/Core/Services/HuggingFaceService.swift @@ -0,0 +1,43 @@ +// +// HuggingFaceService.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// +import Alamofire +import Foundation + +struct ModelQuery { + +} + +struct HuggingFaceService { + static let shared = HuggingFaceService() + + func fetchMLXCommunityModels(search: String? = nil) async -> [RemoteModel] { + var urlComponents = URLComponents(string: "https://huggingface.co/api/models")! + + var queryItems: [URLQueryItem] = [ + URLQueryItem(name: "limit", value: "20"), + URLQueryItem(name: "author", value: "mlx-community"), + URLQueryItem(name: "sort", value: "downloads"), + URLQueryItem(name: "pipeline_tag", value: "text-generation"), + ] + + if let search, !search.isEmpty { + queryItems.append(URLQueryItem(name: "search", value: search)) + } + + urlComponents.queryItems = queryItems + + guard let url = urlComponents.url else { + return [] + } + + do { + AF.request("https://huggingface.co/api/models") + + return [] + } + } +} diff --git a/ChatMLX/Core/Services/HuggingfaceHubService.swift b/ChatMLX/Core/Services/HuggingfaceHubService.swift new file mode 100644 index 0000000..f149c15 --- /dev/null +++ b/ChatMLX/Core/Services/HuggingfaceHubService.swift @@ -0,0 +1,45 @@ +// +// HuggingfaceHubService.swift +// ChatMLX +// +// Created by John Mai on 2025/2/19. +// +import HuggingfaceHub + +struct HuggingfaceHubService { + func scanMLXModels() throws -> [String: [CachedRepoInfo]] { + let hfCacheInfo = try CacheManager().scanCacheDir() + + let models = hfCacheInfo.repos.filter { repo in + repo.repoId.hasPrefix("mlx-community/") || repo.repoId.contains("-MLX") || isMLX(repo: repo) + } + + return Dictionary(grouping: models) { repo in + repo.repoId.split(separator: "/").first.map(String.init) ?? "Other" + } + } + + private func isMLX(repo: CachedRepoInfo) -> Bool { + if repo.repoId.hasPrefix("mlx-community/") { + return true + } + + let file = try? HuggingfaceHub.Utility.tryToLoadFromCache(repoId: repo.repoId, filename: "README.md") + + guard let file else { + return false + } + + let markdown = try? String(contentsOf: file) + + guard let markdown else { + return false + } + + let metadata = MarkdownMetadata(from: markdown) + + let tags = metadata.array(for: "tags") + + return tags.contains("mlx") + } +} diff --git a/ChatMLX/Core/Services/MLXService.swift b/ChatMLX/Core/Services/MLXService.swift new file mode 100644 index 0000000..0e00ac9 --- /dev/null +++ b/ChatMLX/Core/Services/MLXService.swift @@ -0,0 +1,66 @@ +// +// MLXService.swift +// ChatMLX +// +// Created by John Mai on 2024/11/9. +// + +import Foundation + +struct MLXService { + static let shared = MLXService() + + func fetchModels() async throws -> [ProviderModel] { + let fileManager = FileManager.default + + guard let documentDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + else { + return [] + } + + let organizations = try fileManager.contentsOfDirectory( + at: documentDirectoryURL.appendingPathComponent("huggingface/models"), + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + + guard !organizations.isEmpty else { + return [] + } + + return try organizations.flatMap { organization -> [ProviderModel] in + guard organization.hasDirectoryPath else { + return [] + } + + let huggingfaceModels = try fileManager.contentsOfDirectory( + at: organization, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + + return try huggingfaceModels.compactMap { model -> ProviderModel? in + guard model.hasDirectoryPath else { + return nil + } + + let data = try Data(contentsOf: model.appendingPathComponent("tokenizer_config.json")) + let tokenizerConfig = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + + let modelMaxLength = tokenizerConfig?["model_max_length"] as? Int + let toolCall = (tokenizerConfig?["chat_template"] as? String)?.contains("tool_calls") ?? false + let vision = false + + return .init( + id: .directory(model, .mlx), + name: model.lastPathComponent, + path: model, + maxInputLength: modelMaxLength, + maxOutputLength: modelMaxLength, + toolCall: toolCall, + vision: vision + ) + } + } + } +} diff --git a/ChatMLX/Core/Services/OpenAIService.swift b/ChatMLX/Core/Services/OpenAIService.swift new file mode 100644 index 0000000..b552233 --- /dev/null +++ b/ChatMLX/Core/Services/OpenAIService.swift @@ -0,0 +1,37 @@ +// +// OpenAIService.swift +// ChatMLX +// +// Created by John Mai on 2024/11/9. +// + +import Defaults +import Foundation +import OpenAI + +struct OpenAIService { + var client: OpenAI + + init(apiKey: String, baseURL: String? = nil) { + if let baseURL, let url = URL(string: baseURL), let host = url.host { + self.client = OpenAI( + configuration: .init( + token: apiKey, + host: host, + port: url.port ?? 443, + scheme: url.scheme ?? "https" + ) + ) + } else { + self.client = OpenAI(configuration: .init(token: apiKey)) + } + } + + func fetchModels() async throws -> [ProviderModel] { + let models = try await client.models() + + return models.data.map { model in + .init(id: .id(model.id, .openAI)) + } + } +} diff --git a/ChatMLX/Core/Stores/ConversationStore.swift b/ChatMLX/Core/Stores/ConversationStore.swift new file mode 100644 index 0000000..6524cca --- /dev/null +++ b/ChatMLX/Core/Stores/ConversationStore.swift @@ -0,0 +1,166 @@ +// +// ConversationStore.swift +// ChatMLX +// +// Created by John Mai on 2024/11/9. +// + +import SwiftUI + +@MainActor +@Observable +final class ConversationStore { + static let shared = ConversationStore() + + var selectedConversation: Conversation? + + private let viewContext: NSManagedObjectContext = PersistenceController.shared.container.viewContext + private let repository: Repository = .init(container: PersistenceController.shared.container) + + // Create a new conversation + nonisolated func createConversation() { + Task { + do { + let objectID = try await repository.createConversation() + + try await MainActor.run { + if let conversation = try viewContext.existingObject(with: objectID) as? Conversation { + selectConversation(conversation) + } + } + } catch { + // TODO: Handle error + } + } + } + + // Select a conversation + func selectConversation(_ conversation: Conversation) { + selectedConversation = conversation + } + + // Delete a conversation + nonisolated func deleteConversation(_ conversation: Conversation) { + Task { + do { + try await repository.deleteConversation(conversation.objectID) + + await MainActor.run { + if selectedConversation == conversation { + selectedConversation = nil + } + } + } catch { + // TODO: Handle error + } + } + } + + // Delete messages + nonisolated func deleteMessages(_ messages: [Message]) { + Task { + do { + try await repository.deleteMessages(messages) + } catch { + // TODO: Handle error + } + } + } + + // Clear Conversations + nonisolated func clearConversations() { + Task { + let messageObjectIDs = try await repository.clearMessages() + let conversationObjectIDs = try await repository.clearConversations() + + await MainActor.run { + selectedConversation = nil + } + + await repository.mergeChanges([NSDeletedObjectsKey: messageObjectIDs + conversationObjectIDs]) + } + } + + // Send a message + func send(conversation: Conversation, model: ProviderModel, content: String = "") async { + guard !conversation.inferring else { return } + conversation.inferring = true + + do { + let service = ChatService(model) + + let assistantMessage: Message = + if let message = conversation.messages.last, message.role == .assistant { + message + } else { + Message(context: viewContext).assistant(conversation: conversation) + } + + assistantMessage.inferring = true + + let messages = prepare(conversation) + + let responseStream = try await service.send( + messages: messages, + parameters: .init( + temperature: conversation.temperature, + topP: conversation.topP, + repetitionPenalty: conversation.useRepetitionPenalty ? conversation.repetitionPenalty : nil, + repetitionContextSize: conversation.repetitionContextSize, + extraEOSTokens: nil, + maxTokens: conversation.maxLength + ) + ) + + assistantMessage.content = "" + for await response in responseStream { + assistantMessage.content = response + print(response) + } + + assistantMessage.inferring = false + conversation.inferring = false + try viewContext.saveChanges() + } catch { + // TODO: Handle error + print("Error: \(error)") + } + } + + private func prepare(_ conversation: Conversation) -> [[String: String]] { + var messages = conversation.messages + if conversation.useMaxMessagesLimit { + let maxCount = conversation.maxMessagesLimit + 1 + if messages.count > maxCount { + messages = Array(messages.suffix(Int(maxCount))) + if messages.first?.role != .user { + messages = Array(messages.dropFirst()) + } + } + } + + let dictionary = messages[..<(messages.count - 1)].map { + message -> [String: String] in + message.format() + } + + // if conversation.useSystemPrompt, !conversation.systemPrompt.isEmpty { + // dictionary.insert( + // formatMessage( + // role: .system, + // content: conversation.systemPrompt + // ), + // at: 0 + // ) + // } + + return dictionary + } + + private func formatMessage(role: Role, content: String) -> [String: String] { + [ + "role": role.rawValue, + "content": content, + ] + } +} diff --git a/ChatMLX/Core/Stores/DownloadStore.swift b/ChatMLX/Core/Stores/DownloadStore.swift new file mode 100644 index 0000000..cc2db4e --- /dev/null +++ b/ChatMLX/Core/Stores/DownloadStore.swift @@ -0,0 +1,16 @@ +// +// DownloadStore.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +import SwiftUI + +@MainActor +@Observable +final class DownloadStore { + static let shared = DownloadStore() + + var tasks: [DownloadTask] = [] +} diff --git a/ChatMLX/Core/Stores/ModelStore.swift b/ChatMLX/Core/Stores/ModelStore.swift new file mode 100644 index 0000000..ff27cd2 --- /dev/null +++ b/ChatMLX/Core/Stores/ModelStore.swift @@ -0,0 +1,61 @@ +// +// ModelStore.swift +// ChatMLX +// +// Created by John Mai on 2024/11/9. +// + +import Defaults +import SwiftUI + +@MainActor +@Observable +final class ModelStore { + static let shared = ModelStore() + + var models: [ProviderModel] = [] + + // Fetch Models + nonisolated func fetchModels() async { + do { + async let mlxModels = try await fetchMLXModels() + async let openAIModels = await fetchOpenAIModels() + + let (mlxModelsResult, openAIModelsResult) = try await (mlxModels, openAIModels) + + await MainActor.run { + self.models = mlxModelsResult + openAIModelsResult + } + } catch { + // TODO: Handle error + print("Error: \(error)") + } + } + + // Fetch MLX Models + private func fetchMLXModels() async throws -> [ProviderModel] { + try await MLXService.shared.fetchModels() + } + + // Fetch OpenAI Models + private func fetchOpenAIModels() async -> [ProviderModel] { + do { + guard Defaults[.enableOpenAI] else { return [] } + + let openAIService = OpenAIService( + apiKey: Defaults[.openAIApiKey], + baseURL: Defaults[.openAIBaseURL] + ) + + return try await openAIService.fetchModels() + } catch { + // TODO: Handle error + print("Error: \(error)") + } + return [] + } + + func model(_ model: ProviderModel.Identifier?) -> ProviderModel? { + models.first(where: { $0.id == model }) + } +} diff --git a/ChatMLX/Core/Stores/SettingsStore.swift b/ChatMLX/Core/Stores/SettingsStore.swift new file mode 100644 index 0000000..d4bc8b1 --- /dev/null +++ b/ChatMLX/Core/Stores/SettingsStore.swift @@ -0,0 +1,15 @@ +// +// SettingsStore.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +import Defaults +import SwiftUI + +@MainActor +@Observable +class SettingsStore { + var activeTabID: SettingsTab.ID = .general +} diff --git a/ChatMLX/Core/Utilities/Huggingface/Downloader.swift b/ChatMLX/Core/Utilities/Huggingface/Downloader.swift deleted file mode 100644 index 25a7469..0000000 --- a/ChatMLX/Core/Utilities/Huggingface/Downloader.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// Downloader.swift -// -// Adapted from https://github.com/huggingface/swift-coreml-diffusers/blob/d041577b9f5e201baa3465bc60bc5d0a1cf7ed7f/Diffusion/Common/Downloader.swift -// Created by Pedro Cuenca on December 2022. -// See LICENSE at https://github.com/huggingface/swift-coreml-diffusers/LICENSE -// - -import Combine -import Foundation - -class Downloader: NSObject, ObservableObject { - private(set) var destination: URL - - enum DownloadState { - case notStarted - case downloading(Double) - case completed(URL) - case failed(Error) - } - - enum DownloadError: Error { - case invalidDownloadLocation - case unexpectedError - } - - private(set) lazy var downloadState: CurrentValueSubject = - CurrentValueSubject(.notStarted) - private var stateSubscriber: Cancellable? - - private var urlSession: URLSession? = nil - - init( - from url: URL, to destination: URL, using authToken: String? = nil, - inBackground: Bool = false - ) { - self.destination = destination - super.init() - let sessionIdentifier = "swift-transformers.hub.downloader" - - var config = URLSessionConfiguration.default - if inBackground { - config = URLSessionConfiguration.background(withIdentifier: sessionIdentifier) - config.isDiscretionary = false - config.sessionSendsLaunchEvents = true - } - - self.urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) - - setupDownload(from: url, with: authToken) - } - - private func setupDownload(from url: URL, with authToken: String?) { - downloadState.value = .downloading(0) - urlSession?.getAllTasks { tasks in - // If there's an existing pending background task with the same URL, let it proceed. - if let existing = tasks.filter({ $0.originalRequest?.url == url }).first { - switch existing.state { - case .running: - return - case .suspended: - existing.resume() - return - case .canceling: - break - case .completed: - break - @unknown default: - existing.cancel() - } - } - var request = URLRequest(url: url) - if let authToken = authToken { - request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization") - } - - self.urlSession?.downloadTask(with: request).resume() - } - } - - @discardableResult - func waitUntilDone() throws -> URL { - // It's either this, or stream the bytes ourselves (add to a buffer, save to disk, etc; boring and finicky) - let semaphore = DispatchSemaphore(value: 0) - stateSubscriber = downloadState.sink { state in - switch state { - case .completed: semaphore.signal() - case .failed: semaphore.signal() - default: break - } - } - semaphore.wait() - - switch downloadState.value { - case .completed(let url): return url - case .failed(let error): throw error - default: throw DownloadError.unexpectedError - } - } - - func cancel() { - urlSession?.invalidateAndCancel() - } -} - -extension Downloader: URLSessionDownloadDelegate { - func urlSession( - _: URLSession, downloadTask: URLSessionDownloadTask, didWriteData _: Int64, - totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64 - ) { - downloadState.value = .downloading( - Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)) - } - - func urlSession( - _: URLSession, downloadTask _: URLSessionDownloadTask, didFinishDownloadingTo location: URL - ) { - do { - // If the downloaded file already exists on the filesystem, overwrite it - try FileManager.default.moveDownloadedFile(from: location, to: self.destination) - downloadState.value = .completed(destination) - } catch { - downloadState.value = .failed(error) - } - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) - { - if let error = error { - downloadState.value = .failed(error) - } - } -} - -extension FileManager { - func moveDownloadedFile(from srcURL: URL, to dstURL: URL) throws { - if fileExists(atPath: dstURL.path) { - try removeItem(at: dstURL) - } - try moveItem(at: srcURL, to: dstURL) - } -} diff --git a/ChatMLX/Core/Utilities/Huggingface/Hub.swift b/ChatMLX/Core/Utilities/Huggingface/Hub.swift deleted file mode 100644 index 84b50e8..0000000 --- a/ChatMLX/Core/Utilities/Huggingface/Hub.swift +++ /dev/null @@ -1,222 +0,0 @@ -// -// Hub.swift -// -// -// Created by Pedro Cuenca on 18/5/23. -// - -import Foundation - -public struct Hub {} - -extension Hub { - public enum HubClientError: Error { - case parse - case authorizationRequired - case unexpectedError - case httpStatusCode(Int) - } - - public enum RepoType: String { - case models - case datasets - case spaces - } - - public struct Repo { - let id: String - let type: RepoType - - public init(id: String, type: RepoType = .models) { - self.id = id - self.type = type - } - } -} - -// MARK: - Configuration files with dynamic lookup - -@dynamicMemberLookup -public struct Config { - public private(set) var dictionary: [String: Any] - - public init(_ dictionary: [String: Any]) { - self.dictionary = dictionary - } - - func camelCase(_ string: String) -> String { - return - string - .split(separator: "_") - .enumerated() - .map { $0.offset == 0 ? $0.element.lowercased() : $0.element.capitalized } - .joined() - } - - func uncamelCase(_ string: String) -> String { - let scalars = string.unicodeScalars - var result = "" - - var previousCharacterIsLowercase = false - for scalar in scalars { - if CharacterSet.uppercaseLetters.contains(scalar) { - if previousCharacterIsLowercase { - result += "_" - } - let lowercaseChar = Character(scalar).lowercased() - result += lowercaseChar - previousCharacterIsLowercase = false - } else { - result += String(scalar) - previousCharacterIsLowercase = true - } - } - - return result - } - - public subscript(dynamicMember member: String) -> Config? { - let key = dictionary[member] != nil ? member : uncamelCase(member) - if let value = dictionary[key] as? [String: Any] { - return Config(value) - } else if let value = dictionary[key] { - return Config(["value": value]) - } - return nil - } - - public var value: Any? { - return dictionary["value"] - } - - public var intValue: Int? { value as? Int } - public var boolValue: Bool? { value as? Bool } - public var stringValue: String? { value as? String } - - // Instead of doing this we could provide custom classes and decode to them - public var arrayValue: [Config]? { - guard let list = value as? [Any] else { return nil } - return list.map { Config($0 as! [String: Any]) } - } - - /// Tuple of token identifier and string value - public var tokenValue: (UInt, String)? { value as? (UInt, String) } -} - -public class LanguageModelConfigurationFromHub { - struct Configurations { - var modelConfig: Config - var tokenizerConfig: Config? - var tokenizerData: Config - } - - private var configPromise: Task? = nil - - public init( - modelName: String, - hubApi: HubApi = .shared - ) { - self.configPromise = Task.init { - return try await self.loadConfig(modelName: modelName, hubApi: hubApi) - } - } - - public init( - modelFolder: URL, - hubApi: HubApi = .shared - ) { - self.configPromise = Task { - return try await self.loadConfig(modelFolder: modelFolder, hubApi: hubApi) - } - } - - public var modelConfig: Config { - get async throws { - try await configPromise!.value.modelConfig - } - } - - public var tokenizerConfig: Config? { - get async throws { - if let hubConfig = try await configPromise!.value.tokenizerConfig { - // Try to guess the class if it's not present and the modelType is - if let _ = hubConfig.tokenizerClass?.stringValue { return hubConfig } - guard let modelType = try await modelType else { return hubConfig } - - // If the config exists but doesn't contain a tokenizerClass, use a fallback config if we have it - if let fallbackConfig = Self.fallbackTokenizerConfig(for: modelType) { - let configuration = fallbackConfig.dictionary.merging( - hubConfig.dictionary, uniquingKeysWith: { current, _ in current }) - return Config(configuration) - } - - // Guess by capitalizing - var configuration = hubConfig.dictionary - configuration["tokenizer_class"] = "\(modelType.capitalized)Tokenizer" - return Config(configuration) - } - - // Fallback tokenizer config, if available - guard let modelType = try await modelType else { return nil } - return Self.fallbackTokenizerConfig(for: modelType) - } - } - - public var tokenizerData: Config { - get async throws { - try await configPromise!.value.tokenizerData - } - } - - public var modelType: String? { - get async throws { - try await modelConfig.modelType?.stringValue - } - } - - func loadConfig( - modelName: String, - hubApi: HubApi = .shared - ) async throws -> Configurations { - let filesToDownload = ["config.json", "tokenizer_config.json", "tokenizer.json"] - let repo = Hub.Repo(id: modelName) - let downloadedModelFolder = try await hubApi.snapshot(from: repo, matching: filesToDownload) - - return try await loadConfig(modelFolder: downloadedModelFolder, hubApi: hubApi) - } - - func loadConfig( - modelFolder: URL, - hubApi: HubApi = .shared - ) async throws -> Configurations { - // Note tokenizerConfig may be nil (does not exist in all models) - let modelConfig = try hubApi.configuration( - fileURL: modelFolder.appending(path: "config.json")) - let tokenizerConfig = try? hubApi.configuration( - fileURL: modelFolder.appending(path: "tokenizer_config.json")) - let tokenizerVocab = try hubApi.configuration( - fileURL: modelFolder.appending(path: "tokenizer.json")) - - let configs = Configurations( - modelConfig: modelConfig, - tokenizerConfig: tokenizerConfig, - tokenizerData: tokenizerVocab - ) - return configs - } - - static func fallbackTokenizerConfig(for modelType: String) -> Config? { - guard - let url = Bundle.main.url( - forResource: "\(modelType)_tokenizer_config", withExtension: "json") - else { return nil } - do { - let data = try Data(contentsOf: url) - let parsed = try JSONSerialization.jsonObject(with: data, options: []) - guard let dictionary = parsed as? [String: Any] else { return nil } - return Config(dictionary) - } catch { - return nil - } - } -} diff --git a/ChatMLX/Core/Utilities/Huggingface/HubApi.swift b/ChatMLX/Core/Utilities/Huggingface/HubApi.swift deleted file mode 100644 index f419f43..0000000 --- a/ChatMLX/Core/Utilities/Huggingface/HubApi.swift +++ /dev/null @@ -1,359 +0,0 @@ -// -// HubApi.swift -// -// -// Created by Pedro Cuenca on 20231230. -// - -import Foundation - -public class HubApi { - var downloadBase: URL - var hfToken: String? - var endpoint: String - var useBackgroundSession: Bool - - private var currentTask: Task? - - public typealias RepoType = Hub.RepoType - public typealias Repo = Hub.Repo - - public init( - downloadBase: URL? = nil, hfToken: String? = nil, - endpoint: String = "https://huggingface.co", useBackgroundSession: Bool = false - ) { - self.hfToken = hfToken - if let downloadBase { - self.downloadBase = downloadBase - } else { - let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - .first! - self.downloadBase = documents.appending(component: "huggingface") - } - self.endpoint = endpoint - self.useBackgroundSession = useBackgroundSession - } - - public static let shared = HubApi() -} - -/// File retrieval -extension HubApi { - /// Model data for parsed filenames - public struct Sibling: Codable { - let rfilename: String - } - - public struct SiblingsResponse: Codable { - let siblings: [Sibling] - } - - /// Throws error if the response code is not 20X - public func httpGet(for url: URL) async throws -> (Data, HTTPURLResponse) { - var request = URLRequest(url: url) - if let hfToken = hfToken { - request.setValue("Bearer \(hfToken)", forHTTPHeaderField: "Authorization") - } - let (data, response) = try await URLSession.shared.data(for: request) - guard let response = response as? HTTPURLResponse else { - throw Hub.HubClientError.unexpectedError - } - - switch response.statusCode { - case 200 ..< 300: break - case 400 ..< 500: throw Hub.HubClientError.authorizationRequired - default: throw Hub.HubClientError.httpStatusCode(response.statusCode) - } - - return (data, response) - } - - public func getFilenames(from repo: Repo, matching globs: [String] = []) async throws - -> [String] - { - // Read repo info and only parse "siblings" - let url = URL(string: "\(endpoint)/api/\(repo.type)/\(repo.id)")! - let (data, _) = try await httpGet(for: url) - let response = try JSONDecoder().decode(SiblingsResponse.self, from: data) - let filenames = response.siblings.map { $0.rfilename } - guard globs.count > 0 else { return filenames } - - var selected: Set = [] - for glob in globs { - selected = selected.union(filenames.matching(glob: glob)) - } - return Array(selected) - } - - public func getFilenames(from repoId: String, matching globs: [String] = []) async throws - -> [String] - { - return try await getFilenames(from: Repo(id: repoId), matching: globs) - } - - public func getFilenames(from repo: Repo, matching glob: String) async throws -> [String] { - return try await getFilenames(from: repo, matching: [glob]) - } - - public func getFilenames(from repoId: String, matching glob: String) async throws -> [String] { - return try await getFilenames(from: Repo(id: repoId), matching: [glob]) - } -} - -/// Configuration loading helpers -extension HubApi { - /// Assumes the file has already been downloaded. - /// `filename` is relative to the download base. - public func configuration(from filename: String, in repo: Repo) throws -> Config { - let fileURL = localRepoLocation(repo).appending(path: filename) - return try configuration(fileURL: fileURL) - } - - /// Assumes the file is already present at local url. - /// `fileURL` is a complete local file path for the given model - public func configuration(fileURL: URL) throws -> Config { - let data = try Data(contentsOf: fileURL) - let parsed = try JSONSerialization.jsonObject(with: data, options: []) - guard let dictionary = parsed as? [String: Any] else { throw Hub.HubClientError.parse } - return Config(dictionary) - } -} - -/// Whoami -extension HubApi { - public func whoami() async throws -> Config { - guard hfToken != nil else { throw Hub.HubClientError.authorizationRequired } - - let url = URL(string: "\(endpoint)/api/whoami-v2")! - let (data, _) = try await httpGet(for: url) - - let parsed = try JSONSerialization.jsonObject(with: data, options: []) - guard let dictionary = parsed as? [String: Any] else { throw Hub.HubClientError.parse } - return Config(dictionary) - } -} - -/// Snaphsot download -extension HubApi { - public func localRepoLocation(_ repo: Repo) -> URL { - downloadBase.appending(component: repo.type.rawValue).appending(component: repo.id) - } - - public struct HubFileDownloader { - let repo: Repo - let repoDestination: URL - let relativeFilename: String - let hfToken: String? - let endpoint: String? - let backgroundSession: Bool - - var source: URL { - // https://huggingface.co/coreml-projects/Llama-2-7b-chat-coreml/resolve/main/tokenizer.json?download=true - var url = URL(string: endpoint ?? "https://huggingface.co")! - if repo.type != .models { - url = url.appending(component: repo.type.rawValue) - } - url = url.appending(path: repo.id) - url = url.appending(path: "resolve/main") // TODO: revisions - url = url.appending(path: relativeFilename) - return url - } - - var destination: URL { - repoDestination.appending(path: relativeFilename) - } - - var downloaded: Bool { - FileManager.default.fileExists(atPath: destination.path) - } - - func prepareDestination() throws { - let directoryURL = destination.deletingLastPathComponent() - try FileManager.default.createDirectory( - at: directoryURL, withIntermediateDirectories: true, attributes: nil) - } - - // Note we go from Combine in Downloader to callback-based progress reporting - // We'll probably need to support Combine as well to play well with Swift UI - // (See for example PipelineLoader in swift-coreml-diffusers) - // @discardableResult - // func download(progressHandler: @escaping (Double) -> Void) async throws -> URL { - // guard !downloaded else { return destination } - // - // try prepareDestination() - // let downloader = Downloader(from: source, to: destination, using: hfToken, inBackground: backgroundSession) - // let downloadSubscriber = downloader.downloadState.sink { state in - // if case .downloading(let progress) = state { - // progressHandler(progress) - // } - // } - // _ = try withExtendedLifetime(downloadSubscriber) { - // try downloader.waitUntilDone() - // } - // return destination - // } - @discardableResult - func download(progressHandler: @escaping (Double) throws -> Void) async throws -> URL { - guard !downloaded else { return destination } - - try prepareDestination() - let downloader = Downloader( - from: source, to: destination, using: hfToken, inBackground: backgroundSession) - let downloadSubscriber = downloader.downloadState.sink { state in - if case .downloading(let progress) = state { - do { - try progressHandler(progress) - } catch { - downloader.cancel() - } - } - } - return try await withTaskCancellationHandler { - try withExtendedLifetime(downloadSubscriber) { - try downloader.waitUntilDone() - } - } onCancel: { - downloader.cancel() - } - } - } - - @discardableResult - public func snapshot( - from repoId: String, matching globs: [String] = [], - progressHandler: @escaping (Progress) -> Void = { _ in } - ) async throws -> URL { - return try await snapshot( - from: Repo(id: repoId), matching: globs, progressHandler: progressHandler) - } - - @discardableResult - public func snapshot( - from repo: Repo, matching glob: String, - progressHandler: @escaping (Progress) -> Void = { _ in } - ) async throws -> URL { - return try await snapshot(from: repo, matching: [glob], progressHandler: progressHandler) - } - - @discardableResult - public func snapshot( - from repoId: String, matching glob: String, - progressHandler: @escaping (Progress) -> Void = { _ in } - ) async throws -> URL { - return try await snapshot( - from: Repo(id: repoId), matching: [glob], progressHandler: progressHandler) - } -} - -/// Stateless wrappers that use `HubApi` instances -extension Hub { - public static func getFilenames(from repo: Hub.Repo, matching globs: [String] = []) async throws - -> [String] - { - return try await HubApi.shared.getFilenames(from: repo, matching: globs) - } - - public static func getFilenames(from repoId: String, matching globs: [String] = []) async throws - -> [String] - { - return try await HubApi.shared.getFilenames(from: Repo(id: repoId), matching: globs) - } - - public static func getFilenames(from repo: Repo, matching glob: String) async throws -> [String] - { - return try await HubApi.shared.getFilenames(from: repo, matching: glob) - } - - public static func getFilenames(from repoId: String, matching glob: String) async throws - -> [String] - { - return try await HubApi.shared.getFilenames(from: Repo(id: repoId), matching: glob) - } - - public static func snapshot( - from repo: Repo, matching globs: [String] = [], - progressHandler: @escaping (Progress) -> Void = { _ in } - ) async throws -> URL { - return try await HubApi.shared.snapshot( - from: repo, matching: globs, progressHandler: progressHandler) - } - - public static func snapshot( - from repoId: String, matching globs: [String] = [], - progressHandler: @escaping (Progress) -> Void = { _ in } - ) async throws -> URL { - return try await HubApi.shared.snapshot( - from: Repo(id: repoId), matching: globs, progressHandler: progressHandler) - } - - public static func snapshot( - from repo: Repo, matching glob: String, - progressHandler: @escaping (Progress) -> Void = { _ in } - ) async throws -> URL { - return try await HubApi.shared.snapshot( - from: repo, matching: glob, progressHandler: progressHandler) - } - - public static func snapshot( - from repoId: String, matching glob: String, - progressHandler: @escaping (Progress) -> Void = { _ in } - ) async throws -> URL { - return try await HubApi.shared.snapshot( - from: Repo(id: repoId), matching: glob, progressHandler: progressHandler) - } - - public static func whoami(token: String) async throws -> Config { - return try await HubApi(hfToken: token).whoami() - } -} - -extension [String] { - public func matching(glob: String) -> [String] { - filter { fnmatch(glob, $0, 0) == 0 } - } -} - -extension HubApi { - @discardableResult - public func snapshot( - from repo: Repo, matching globs: [String] = [], - progressHandler: @escaping (Progress) -> Void = { _ in } - ) async throws -> URL { - self.currentTask = Task { - let filenames = try await getFilenames(from: repo, matching: globs) - let progress = Progress(totalUnitCount: Int64(filenames.count)) - let repoDestination = localRepoLocation(repo) - for filename in filenames { - if Task.isCancelled { throw CancellationError() } - let fileProgress = Progress( - totalUnitCount: 100, parent: progress, pendingUnitCount: 1) - let downloader = HubFileDownloader( - repo: repo, - repoDestination: repoDestination, - relativeFilename: filename, - hfToken: hfToken, - endpoint: endpoint, - backgroundSession: useBackgroundSession - ) - - try await downloader.download { fractionDownloaded in - do { - if Task.isCancelled { throw CancellationError() } - fileProgress.completedUnitCount = Int64(100 * fractionDownloaded) - progressHandler(progress) - } catch { - throw error - } - } - fileProgress.completedUnitCount = 100 - } - progressHandler(progress) - return repoDestination - } - return try await currentTask!.value - } - - public func cancelCurrentDownload() { - currentTask?.cancel() - } -} diff --git a/ChatMLX/Core/Utilities/LLMRunner.swift b/ChatMLX/Core/Utilities/LLMRunner.swift index b63bb9d..7fe8bb0 100644 --- a/ChatMLX/Core/Utilities/LLMRunner.swift +++ b/ChatMLX/Core/Utilities/LLMRunner.swift @@ -1,259 +1,259 @@ +//// +//// LLMRunner.swift +//// ChatMLX +//// +//// Created by John Mai on 2024/8/24. +//// // -// LLMRunner.swift -// ChatMLX -// -// Created by John Mai on 2024/8/24. -// - -import Defaults -import Metal -import MLX -import MLXLLM -import MLXRandom -import SwiftUI -import Tokenizers -import os - -@Observable -@MainActor -class LLMRunner { - var running = false - var model: ModelInfo? - - let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "LLMRunner") - - enum LoadState { - case idle - case loaded(ModelContainer) - } - - @ObservationIgnored - var loadState: LoadState = .idle - - @ObservationIgnored - var modelConfiguration: ModelConfiguration? - - var gpuActiveMemory: Int = 0 - - let displayEveryNTokens = 4 - - init() {} - - private func load() async throws -> ModelContainer? { - guard let modelConfiguration else { - throw LLMRunnerError.modelConfigurationNotSet - } - - switch loadState { - case .idle: - - let enableGPUMemorySettings = Defaults[.enableGPUMemorySettings] - if enableGPUMemorySettings { - let cacheLimit = Defaults[.gpuCacheLimit] * 1024 * 1024 - MLX.GPU.set(cacheLimit: cacheLimit) - - let memoryLimit = Defaults[.gpuMemoryLimit] * 1024 * 1024 - MLX.GPU.set(memoryLimit: memoryLimit) - } - -// let modelContainer = try await MLXLLM.loadModelContainer( -// configuration: modelConfiguration -// ) - - let modelContainer = try await MLXLLM.ModelContainer( - hub: .init(), modelDirectory: URL(fileURLWithPath: "/Users/john/Downloads/Llama-3.2-1B-Instruct-4bit"), configuration: modelConfiguration - ) - - withAnimation { - gpuActiveMemory = MLX.GPU.activeMemory / 1024 / 1024 - } - - loadState = .loaded(modelContainer) - return modelContainer - - case .loaded(let modelContainer): - return modelContainer - } - } - - private func switchModel(_ conversation: Conversation) { -// if conversation.model != modelConfiguration?.name { -// loadState = .idle -// modelConfiguration = ModelConfiguration.configuration( -// id: conversation.model) +//import Defaults +//import Metal +//import MLX +//import MLXLLM +//import MLXRandom +//import SwiftUI +//import Tokenizers +//import os +// +//@Observable +//@MainActor +//class LLMRunner { +// var running = false +// var model: ModelInfo? +// +// let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "LLMRunner") +// +// enum LoadState { +// case idle +// case loaded(ModelContainer) +// } +// +// @ObservationIgnored +// var loadState: LoadState = .idle +// +// @ObservationIgnored +// var modelConfiguration: ModelConfiguration? +// +// var gpuActiveMemory: Int = 0 +// +// let displayEveryNTokens = 4 +// +// init() {} +// +// private func load() async throws -> ModelContainer? { +// guard let modelConfiguration else { +// throw LLMRunnerError.modelConfigurationNotSet // } - } - - func prepare(_ conversation: Conversation) -> [[String: String]] { - var messages = conversation.messages - if conversation.useMaxMessagesLimit { - let maxCount = conversation.maxMessagesLimit + 1 - if messages.count > maxCount { - messages = Array(messages.suffix(Int(maxCount))) - if messages.first?.role != .user { - messages = Array(messages.dropFirst()) - } - } - } - - var dictionary = messages[..<(messages.count - 1)].map { - message -> [String: String] in - message.format() - } - -// if conversation.useSystemPrompt, !conversation.systemPrompt.isEmpty { -// dictionary.insert( -// formatMessage( -// role: .system, -// content: conversation.systemPrompt -// ), -// at: 0 +// +// switch loadState { +// case .idle: +// +// let enableGPUMemorySettings = Defaults[.enableGPUMemorySettings] +// if enableGPUMemorySettings { +// let cacheLimit = Defaults[.gpuCacheLimit] * 1024 * 1024 +// MLX.GPU.set(cacheLimit: cacheLimit) +// +// let memoryLimit = Defaults[.gpuMemoryLimit] * 1024 * 1024 +// MLX.GPU.set(memoryLimit: memoryLimit) +// } +// +//// let modelContainer = try await MLXLLM.loadModelContainer( +//// configuration: modelConfiguration +//// ) +// +// let modelContainer = try await MLXLLM.ModelContainer( +// hub: .init(), modelDirectory: URL(fileURLWithPath: "/Users/john/Downloads/Llama-3.2-1B-Instruct-4bit"), configuration: modelConfiguration // ) +// +// withAnimation { +// gpuActiveMemory = MLX.GPU.activeMemory / 1024 / 1024 +// } +// +// loadState = .loaded(modelContainer) +// return modelContainer +// +// case .loaded(let modelContainer): +// return modelContainer // } - - return dictionary - } - - func formatMessage(role: Role, content: String) -> [String: String] { - [ - "role": role.rawValue, - "content": content, - ] - } - -// func generate2( +// } +// +// private func switchModel(_ conversation: Conversation) { +//// if conversation.model != modelConfiguration?.name { +//// loadState = .idle +//// modelConfiguration = ModelConfiguration.configuration( +//// id: conversation.model) +//// } +// } +// +// func prepare(_ conversation: Conversation) -> [[String: String]] { +// var messages = conversation.messages +// if conversation.useMaxMessagesLimit { +// let maxCount = conversation.maxMessagesLimit + 1 +// if messages.count > maxCount { +// messages = Array(messages.suffix(Int(maxCount))) +// if messages.first?.role != .user { +// messages = Array(messages.dropFirst()) +// } +// } +// } +// +// var dictionary = messages[..<(messages.count - 1)].map { +// message -> [String: String] in +// message.format() +// } +// +//// if conversation.useSystemPrompt, !conversation.systemPrompt.isEmpty { +//// dictionary.insert( +//// formatMessage( +//// role: .system, +//// content: conversation.systemPrompt +//// ), +//// at: 0 +//// ) +//// } +// +// return dictionary +// } +// +// func formatMessage(role: Role, content: String) -> [String: String] { +// [ +// "role": role.rawValue, +// "content": content, +// ] +// } +// +//// func generate2( +//// conversation: Conversation, +//// in context: NSManagedObjectContext, +//// progressing: @escaping () -> Void = {}, +//// completion: (() -> Void)? +//// ) { +//// let service = ChatService() +//// +//// Task{ +//// await service.chat( +//// model: .init( +//// name: "Llama-3.2mo-1B-Instruct-4bit", +//// path: URL(fileURLWithPath: "Llama-3.2-1B-Instruct-4bit"), +//// ), +//// conversation: conversation, +//// context: context +//// ) +//// +//// } +//// } +// +// func generate( // conversation: Conversation, // in context: NSManagedObjectContext, // progressing: @escaping () -> Void = {}, // completion: (() -> Void)? // ) { -// let service = ChatService() -// -// Task{ -// await service.chat( -// model: .init( -// name: "Llama-3.2mo-1B-Instruct-4bit", -// path: URL(fileURLWithPath: "Llama-3.2-1B-Instruct-4bit"), -// ), -// conversation: conversation, -// context: context -// ) +// guard !running else { return } +// withAnimation { +// running = true +// } +// +// let assistantMessage: Message = if let message = conversation.messages.last, message.role == .assistant { +// message +// } else { +// Message(context: context).assistant(conversation: conversation) +// } +// +// let parameters = GenerateParameters( +// temperature: conversation.temperature, +// topP: conversation.topP, +// repetitionPenalty: conversation.useRepetitionPenalty +// ? conversation.repetitionPenalty : nil, +// repetitionContextSize: Int(conversation.repetitionContextSize) +// ) +// +// let useMaxLength = conversation.useMaxLength +// let maxLength = conversation.maxLength +// +// Task { +// do { +// switchModel(conversation) // +// if let modelConfiguration { +// guard let modelContainer = try await load() else { +// throw LLMRunnerError.failedToLoadModel +// } +// +// let messages = prepare(conversation) +// +// logger.info("prepare messages -> \(messages)") +// +// let tokens = try await modelContainer.perform { _, tokenizer in +// try tokenizer.applyChatTemplate(messages: messages) +// } +// +// MLXRandom.seed(UInt64(Date.timeIntervalSinceReferenceDate * 1000)) +// +// let result = await modelContainer.perform { model, tokenizer in +// MLXLLM.generate( +// promptTokens: tokens, +// parameters: parameters, +// model: model, +// tokenizer: tokenizer, +// extraEOSTokens: modelConfiguration.extraEOSTokens.union([ +// "<|im_end|>", "<|end|>", +// ]) +// ) { tokens in +// if tokens.count % displayEveryNTokens == 0 { +// let text = tokenizer.decode(tokens: tokens) +// Task { @MainActor in +// assistantMessage.content = text +// progressing() +// } +// } +// +// if useMaxLength, tokens.count >= maxLength { +// return .stop +// } +// +// return .more +// } +// } +// +// conversation.promptTime = result.promptTime +// conversation.generateTime = result.generateTime +// conversation.promptTokensPerSecond = result.promptTokensPerSecond +// conversation.tokensPerSecond = result.tokensPerSecond +// +// await MainActor.run { +// if result.output != assistantMessage.content { +// assistantMessage.content = result.output +// } +// +// assistantMessage.inferring = false +// running = false +// } +// } +// } catch { +// logger.error("LLM Generate Failed: \(error)") +// await MainActor.run { +// assistantMessage.inferring = false +// assistantMessage.error = error.localizedDescription +// withAnimation { +// running = false +// } +// } +// } +// +// Task(priority: .background) { +// await context.perform { +// if context.hasChanges { +// try? context.save() +// } +// } +// } +// +// completion?() // } // } - - func generate( - conversation: Conversation, - in context: NSManagedObjectContext, - progressing: @escaping () -> Void = {}, - completion: (() -> Void)? - ) { - guard !running else { return } - withAnimation { - running = true - } - - let assistantMessage: Message = if let message = conversation.messages.last, message.role == .assistant { - message - } else { - Message(context: context).assistant(conversation: conversation) - } - - let parameters = GenerateParameters( - temperature: conversation.temperature, - topP: conversation.topP, - repetitionPenalty: conversation.useRepetitionPenalty - ? conversation.repetitionPenalty : nil, - repetitionContextSize: Int(conversation.repetitionContextSize) - ) - - let useMaxLength = conversation.useMaxLength - let maxLength = conversation.maxLength - - Task { - do { - switchModel(conversation) - - if let modelConfiguration { - guard let modelContainer = try await load() else { - throw LLMRunnerError.failedToLoadModel - } - - let messages = prepare(conversation) - - logger.info("prepare messages -> \(messages)") - - let tokens = try await modelContainer.perform { _, tokenizer in - try tokenizer.applyChatTemplate(messages: messages) - } - - MLXRandom.seed(UInt64(Date.timeIntervalSinceReferenceDate * 1000)) - - let result = await modelContainer.perform { model, tokenizer in - MLXLLM.generate( - promptTokens: tokens, - parameters: parameters, - model: model, - tokenizer: tokenizer, - extraEOSTokens: modelConfiguration.extraEOSTokens.union([ - "<|im_end|>", "<|end|>", - ]) - ) { tokens in - if tokens.count % displayEveryNTokens == 0 { - let text = tokenizer.decode(tokens: tokens) - Task { @MainActor in - assistantMessage.content = text - progressing() - } - } - - if useMaxLength, tokens.count >= maxLength { - return .stop - } - - return .more - } - } - - conversation.promptTime = result.promptTime - conversation.generateTime = result.generateTime - conversation.promptTokensPerSecond = result.promptTokensPerSecond - conversation.tokensPerSecond = result.tokensPerSecond - - await MainActor.run { - if result.output != assistantMessage.content { - assistantMessage.content = result.output - } - - assistantMessage.inferring = false - running = false - } - } - } catch { - logger.error("LLM Generate Failed: \(error)") - await MainActor.run { - assistantMessage.inferring = false - assistantMessage.error = error.localizedDescription - withAnimation { - running = false - } - } - } - - Task(priority: .background) { - await context.perform { - if context.hasChanges { - try? context.save() - } - } - } - - completion?() - } - } -} - -enum LLMRunnerError: Error { - case modelConfigurationNotSet - case failedToLoadModel -} +//} +// +//enum LLMRunnerError: Error { +// case modelConfigurationNotSet +// case failedToLoadModel +//} diff --git a/ChatMLX/Core/Utilities/Logger.swift b/ChatMLX/Core/Utilities/Logger.swift index 2e721e8..ab46753 100644 --- a/ChatMLX/Core/Utilities/Logger.swift +++ b/ChatMLX/Core/Utilities/Logger.swift @@ -7,5 +7,4 @@ import Foundation - //let logger = Logger(label: Bundle.main.bundleIdentifier!) diff --git a/ChatMLX/Core/Utilities/MarkdownMetadata.swift b/ChatMLX/Core/Utilities/MarkdownMetadata.swift index 2509f06..5a08b8b 100644 --- a/ChatMLX/Core/Utilities/MarkdownMetadata.swift +++ b/ChatMLX/Core/Utilities/MarkdownMetadata.swift @@ -1,39 +1,74 @@ -// -// MarkdownMetadata.swift -// ChatMLX -// -// Created by John Mai on 2024/10/10. -// - -import Foundation - struct MarkdownMetadata { - var metadata: [String: String] = [:] - - init(markdown: String) { - let lines = markdown.components(separatedBy: .newlines) - var isMetadata = false - var metadataLines: [String] = [] - var contentLines: [String] = [] - - for line in lines { - if line.trimmingCharacters(in: .whitespaces) == "---" { - isMetadata.toggle() + private(set) var values: [String: Any] = [:] + + init(from markdown: String) { + let lines = markdown.split(separator: "\n") + guard lines.first == "---" else { return } + + var currentKey: String? + var arrayItems: [String] = [] + var isCollectingArray = false + + for line in lines.dropFirst() { + if line == "---" { + if isCollectingArray, let key = currentKey { + values[key] = arrayItems + } + return + } + + let trimmedLine = line.trimmingCharacters(in: .whitespaces) + guard !trimmedLine.isEmpty else { continue } + + if trimmedLine.hasPrefix("-") { + let item = trimmedLine.dropFirst().trimmingCharacters(in: .whitespaces) + if !isCollectingArray { + isCollectingArray = true + arrayItems = [] + } + arrayItems.append(item) + if let key = currentKey { + values[key] = arrayItems + } continue } - - if isMetadata { - metadataLines.append(line) - } else { - contentLines.append(line) + + if let colonIndex = trimmedLine.firstIndex(of: ":") { + if isCollectingArray { + isCollectingArray = false + arrayItems = [] + } + + let key = String(trimmedLine[.. String? { + values[key] as? String + } + + func array(for key: String) -> [String] { + (values[key] as? [String]) ?? [] + } } diff --git a/ChatMLX/Core/View Modifiers/AppleIntelligenceEffectViewModifier.swift b/ChatMLX/Core/View Modifiers/AppleIntelligenceEffectViewModifier.swift new file mode 100644 index 0000000..978966a --- /dev/null +++ b/ChatMLX/Core/View Modifiers/AppleIntelligenceEffectViewModifier.swift @@ -0,0 +1,33 @@ +// +// AppleIntelligenceEffectViewModifier.swift +// ChatMLX +// +// Created by John Mai on 2024/11/9. +// + +import Defaults +import SwiftUI + +struct AppleIntelligenceEffectViewModifier: ViewModifier { + @Default(.enableAppleIntelligenceEffect) var enableAppleIntelligenceEffect + @Default(.appleIntelligenceEffectDisplay) var appleIntelligenceEffectDisplay + + @Binding var isPresented: Bool + + func body(content: Content) -> some View { + content.overlay { + if self.enableAppleIntelligenceEffect, self.appleIntelligenceEffectDisplay == .appInternal, self.isPresented + { + AppleIntelligenceEffectView(useRoundedRectangle: false) + .ignoresSafeArea() + .allowsHitTesting(false) + } + } + } +} + +extension View { + func appleIntelligenceEffect(isPresented: Binding) -> some View { + self.modifier(AppleIntelligenceEffectViewModifier(isPresented: isPresented)) + } +} diff --git a/ChatMLX/Features/Conversation/ConversationDetailView.swift b/ChatMLX/Features/Conversation/ConversationDetailView.swift deleted file mode 100644 index a1896ca..0000000 --- a/ChatMLX/Features/Conversation/ConversationDetailView.swift +++ /dev/null @@ -1,343 +0,0 @@ -// -// ConversationDetailView.swift -// ChatMLX -// -// Created by John Mai on 2024/8/4. -// - -import AlertToast -import Defaults -import Luminare -import MLX -import MLXLLM -import SwiftUI - -struct ConversationDetailView: View { - @ObservedObject var conversation: Conversation - - @Environment(LLMRunner.self) var runner - @Environment(\.managedObjectContext) private var viewContext - @Environment(ConversationViewModel.self) private var conversationViewModel - @Environment(ModelManagerViewModel.self) private var modelManagerViewModel - - @State private var newMessage = "" - @State private var showRightSidebar = false - @State private var showInfoPopover = false - @State private var displayStyle: DisplayStyle = .markdown - @State private var isEditorFullScreen = false - @State private var showToast = false - @State private var toastMessage = "" - @State private var toastType: AlertToast.AlertType = .regular - @State private var scrollViewProxy: ScrollViewProxy? - - @FocusState private var isInputFocused: Bool - - @Default(.enableAppleIntelligenceEffect) var enableAppleIntelligenceEffect - @Default(.appleIntelligenceEffectDisplay) var appleIntelligenceEffectDisplay - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \ModelInfo.name, ascending: true)], - animation: .default - ) var models: FetchedResults - - var body: some View { - ZStack(alignment: .trailing) { - VStack(spacing: 0) { - if !isEditorFullScreen { - MessageBox() - Divider() - } - Editor() - } - - if showRightSidebar { - Color.black.opacity(0.00001) - .ignoresSafeArea() - .onTapGesture { - withAnimation { - showRightSidebar = false - } - } - - RightSidebarView(conversation: conversation) - } - } - .toast(isPresenting: $showToast, duration: 1.5, offsetY: 30) { - AlertToast( - displayMode: .hud, type: toastType, title: toastMessage - ) - } - .ultramanNavigationTitle( - conversation.title - ) - .ultramanToolbar(alignment: .trailing) { - Button(action: { - withAnimation { - showRightSidebar.toggle() - } - - }) { - Image(systemName: "slider.horizontal.3") - } - .buttonStyle(.plain) - } - } - - @ViewBuilder - private func MessageBox() -> some View { - ScrollViewReader { proxy in - ScrollView { - LazyVStack { - ForEach(conversation.messages) { message in - MessageBubbleView( - message: message, - displayStyle: $displayStyle - ) - } - } - .padding() - } - .onChange( - of: conversation.messages.last, - { _, _ in - scrollToBottom() - } - ) - .onAppear { - scrollViewProxy = proxy - scrollToBottom() - } - } - } - - private func scrollToBottom() { - guard let lastMessageId = conversation.messages.last?.id, let scrollViewProxy else { - return - } - - withAnimation { - scrollViewProxy.scrollTo(lastMessageId, anchor: .bottom) - } - } - - @ViewBuilder - private func EditorToolbar() -> some View { - HStack { - Button { - withAnimation { - displayStyle = (displayStyle == .markdown) ? .plain : .markdown - } - } label: { - Image(displayStyle == .markdown ? "plaintext" : "markdown") - } - - Button(action: { - conversation.messages = [] - }) { - Image("clear") - } - - Button { - withAnimation { - isEditorFullScreen.toggle() - } - } label: { - Image( - systemName: isEditorFullScreen - ? "arrow.down.right.and.arrow.up.left" - : "arrow.up.left.and.arrow.down.right") - } - .help(isEditorFullScreen ? "Exit Full Screen" : "Enter Full Screen") - - Spacer() - - Button { - showInfoPopover.toggle() - } label: { - if runner.gpuActiveMemory > 0 { - HStack { - Image(systemName: "info.circle") - Text("\(runner.gpuActiveMemory)M") - } - .padding(4) - .background(Color.black.opacity(0.2)) - .cornerRadius(20) - } else { - Image(systemName: "info.circle") - .padding(4) - } - } - .font(.subheadline) - .popover(isPresented: $showInfoPopover) { - VStack(alignment: .leading) { - LabeledContent { - Text(conversation.promptTime.formatted()) - } label: { - Text("Prompt Time") - .fontWeight(.bold) - } - - LabeledContent { - Text("\(Int(conversation.promptTokensPerSecond))") - } label: { - Text("Prompt Tokens/second") - .fontWeight(.bold) - } - - LabeledContent { - Text(conversation.generateTime.formatted()) - } label: { - Text("Generate Time") - .fontWeight(.bold) - } - - LabeledContent { - Text("\(Int(conversation.tokensPerSecond))") - } label: { - Text("Generate Tokens/second") - .fontWeight(.bold) - } - } - .padding() - .background(.clear) - } - - ModelPicker(selection: $conversation.model) - } - .buttonStyle(.borderless) - .foregroundStyle(.white) - .tint(.white) - .frame(height: 35) - .padding(.horizontal, 10) - } - - @ViewBuilder - private func Editor() -> some View { - VStack(alignment: .leading, spacing: 0) { - EditorToolbar() - - ZStack(alignment: .bottom) { - UltramanTextEditor( - text: $newMessage, - placeholder: "Type your message…", - onSubmit: sendMessage - ) - .padding(.horizontal, 5) - - HStack(spacing: 16) { - Spacer() - Button("Clear") { - newMessage = "" - } - .buttonStyle(.borderless) - .disabled(newMessage.isEmpty) - - Button { - sendMessage() - } label: { - if runner.running { - Label { - Text("Send") - } icon: { - ProgressView() - .controlSize(.small) - .padding(.trailing, 2) - .colorInvert() - .brightness(1) - } - } else { - Label("Send", systemImage: "paperplane") - } - } - .buttonStyle(LuminareCompactButtonStyle()) - .fixedSize() - .disabled( - newMessage.trimmingCharacters( - in: .whitespacesAndNewlines - ).isEmpty || runner.running) - } - .padding() - } - .frame(maxHeight: isEditorFullScreen ? .infinity : 150) - } - } - - private func sendMessage() { - guard !conversation.inferring else { - return - } - - let trimmedMessage = newMessage.trimmingCharacters( - in: .whitespacesAndNewlines) - guard !trimmedMessage.isEmpty else { return } - - guard let model = conversation.model else { - showToastMessage("Please select a model", type: .error(.red)) - return - } - - newMessage = "" - isInputFocused = false - - Message(context: viewContext).user(content: trimmedMessage, conversation: conversation) - - if enableAppleIntelligenceEffect, appleIntelligenceEffectDisplay == .global { - AppleIntelligenceEffectManager.shared.setupEffect() - } - - conversation.inferring = true - - let factory = ProviderFactory.shared - - let assistantMessage = conversation.getLastAssistantMessage(context: viewContext) - assistantMessage.inferring = true - let messages = conversation.prepareMessages() - -#if DEBUG - print(messages) -#endif - - Task { - let provider = await factory.provider(.openAI) - await provider.chat( - messages: messages, - config: .init( - model: .init( - name: "model.name", - path: model.path - ) - ) - ) { result in - switch result { - case .success(let response): - Task { @MainActor in - assistantMessage.content = response.content - } - case .failure(let error): - Task { @MainActor in - assistantMessage.error = error.localizedDescription - } - } - } - - await MainActor.run { - conversation.inferring = false - assistantMessage.inferring = false - } - - if enableAppleIntelligenceEffect, appleIntelligenceEffectDisplay == .global { - AppleIntelligenceEffectManager.shared.closeEffect() - } - - if viewContext.hasChanges { - try? viewContext.save() - } - } - } - - private func showToastMessage(_ message: String, type: AlertToast.AlertType) { - toastMessage = message - toastType = type - showToast = true - } -} diff --git a/ChatMLX/Features/Conversation/ConversationView.swift b/ChatMLX/Features/Conversation/ConversationView.swift index 64e1a5d..c4248f0 100644 --- a/ChatMLX/Features/Conversation/ConversationView.swift +++ b/ChatMLX/Features/Conversation/ConversationView.swift @@ -9,51 +9,32 @@ import Defaults import SwiftUI struct ConversationView: View { - @Environment(ConversationViewModel.self) private var conversationViewModel - @Environment(LLMRunner.self) private var runner - - @Default(.enableAppleIntelligenceEffect) var enableAppleIntelligenceEffect - @Default(.appleIntelligenceEffectDisplay) var appleIntelligenceEffectDisplay + @Binding var selectedConversation: Conversation? var body: some View { - @Bindable var conversationViewModel = conversationViewModel - UltramanNavigationSplitView( sidebar: { - ConversationSidebarView( - selectedConversation: $conversationViewModel.selectedConversation) + ConversationSidebarView(selectedConversation: $selectedConversation) }, detail: { detailView() + } ) .foregroundColor(.white) .ultramanMinimalistWindowStyle() - .overlay { - if enableAppleIntelligenceEffect, appleIntelligenceEffectDisplay == .appInternal, - runner.running - { - AppleIntelligenceEffectView(useRoundedRectangle: false) - .ignoresSafeArea() - .allowsHitTesting(false) - } - } + .appleIntelligenceEffect(isPresented: .constant(false)) } @ViewBuilder private func detailView() -> some View { Group { - if let conversation = conversationViewModel.selectedConversation { - ConversationDetailView( - conversation: conversation - ).id(conversation.id) + if let conversation = selectedConversation { + ConversationDetailView(conversation: conversation) + } else { EmptyConversation() } } } } - -#Preview { - ConversationView() -} diff --git a/ChatMLX/Features/Conversation/Detail/AssistantMessageView.swift b/ChatMLX/Features/Conversation/Detail/AssistantMessageView.swift new file mode 100644 index 0000000..f3c2c6b --- /dev/null +++ b/ChatMLX/Features/Conversation/Detail/AssistantMessageView.swift @@ -0,0 +1,110 @@ +// +// AssistantMessageView.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +import MarkdownUI +import SwiftUI + +struct AssistantMessageView: View { + let displayStyle: DisplayStyle + + @ObservedObject var message: Message + + var body: some View { + HStack(alignment: .top, spacing: 12) { + Image("AppLogo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .background(.white) + .clipShape(RoundedRectangle(cornerRadius: 5)) + .shadow(color: .black.opacity(0.25), radius: 5, x: -1, y: 5) + + VStack(alignment: .leading) { + if displayStyle == .markdown { + Markdown(MarkdownContent(message.content)) + .markdownCodeSyntaxHighlighter( + .splash(theme: .sunset(withFont: .init(size: 16))) + ) + .markdownTextStyle { + ForegroundColor(.white) + } + .markdownTheme(.customGitHub) + } else { + Text(message.content) + } + + if let error = message.error, !error.isEmpty { + HStack { + Image(systemName: "exclamationmark.triangle") + .foregroundStyle(.yellow) + Text(error) + } + .padding(5) + .background(.red.opacity(0.3)) + .foregroundColor(.white) + .cornerRadius(5) + } + + HStack { + Button(action: copy) { + Image(systemName: "doc.on.doc") + .help("Copy") + } + + Button(action: regenerate) { + Image(systemName: "arrow.clockwise") + .help("Regenerate") + } + .disabled(message.conversation?.inferring ?? false) + + Text(message.updatedAt?.toTimeFormatted() ?? "") + .font(.caption) + + if message.role == .assistant, message.inferring { + ProgressView() + .controlSize(.small) + .colorInvert() + .brightness(1) + .padding(.leading, 5) + } + } + .buttonStyle(.plain) + .foregroundColor(.white.opacity(0.8)) + .padding(.top, 4) + .frame(maxWidth: .infinity, alignment: .leading) + } + Spacer() + } + } + + private func copy() { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(message.content, forType: .string) + } + + private func regenerate() { + guard let conversation = message.conversation else { return } + + let conversationStore = ConversationStore.shared + + if conversation.messages.last != message { + conversationStore.deleteMessages(message.suffixMessages()) + } + + guard let model = ModelStore.shared.model(conversation.model) else { + return + } + + Task { + await conversationStore.send( + conversation: conversation, + model: model + ) + } + } +} diff --git a/ChatMLX/Features/Conversation/Detail/ConversationDetailView.swift b/ChatMLX/Features/Conversation/Detail/ConversationDetailView.swift new file mode 100644 index 0000000..a57bbe6 --- /dev/null +++ b/ChatMLX/Features/Conversation/Detail/ConversationDetailView.swift @@ -0,0 +1,74 @@ +// +// ConversationDetailView.swift +// ChatMLX +// +// Created by John Mai on 2024/8/4. +// + +import AlertToast +import Defaults +import Luminare +import MLX +import MLXLLM +import SwiftUI + +struct ConversationDetailView: View { + let conversation: Conversation + + @State private var showRightSidebar = false + @State private var displayStyle: DisplayStyle = .markdown + @State private var isEditorFullScreen: Bool = false + + var body: some View { + ZStack(alignment: .trailing) { + VStack(spacing: 0) { + if !isEditorFullScreen { + MessageBoxView( + conversation: conversation, + displayStyle: displayStyle + ) + Divider() + } + + EditorToolbarView( + conversation: conversation, + displayStyle: $displayStyle, + isEditorFullScreen: $isEditorFullScreen + ) + + EditorView( + conversation: conversation, + displayStyle: displayStyle, + isEditorFullScreen: isEditorFullScreen + ) + } + + if showRightSidebar { + Color.black.opacity(0.00001) + .ignoresSafeArea() + .onTapGesture { + withAnimation { + showRightSidebar = false + } + } + + RightSidebarView(conversation: conversation) + } + } + .ultramanToolbar(alignment: .trailing) { + Button(action: showRightSidebarToggle) { + Image(systemName: "slider.horizontal.3") + } + .buttonStyle(.plain) + } + .ultramanNavigationTitle( + conversation.title + ) + } + + func showRightSidebarToggle() { + withAnimation { + showRightSidebar.toggle() + } + } +} diff --git a/ChatMLX/Features/Conversation/Detail/EditorToolbarView.swift b/ChatMLX/Features/Conversation/Detail/EditorToolbarView.swift new file mode 100644 index 0000000..56ad163 --- /dev/null +++ b/ChatMLX/Features/Conversation/Detail/EditorToolbarView.swift @@ -0,0 +1,119 @@ +// +// EditorToolbarView.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +import SwiftUI + +struct EditorToolbarView: View { + @ObservedObject var conversation: Conversation + @Binding var displayStyle: DisplayStyle + @Binding var isEditorFullScreen: Bool + @State var isPresented = false + + var body: some View { + HStack { + Button(action: switchDisplayStyle) { + Image(displayStyle == .markdown ? "plaintext" : "markdown") + } + + Button(action: clearMessages) { + Image("clear") + } + + Button(action: isEditorFullScreenToggle) { + Image( + systemName: isEditorFullScreen + ? "arrow.down.right.and.arrow.up.left" + : "arrow.up.left.and.arrow.down.right") + } + .help(isEditorFullScreen ? "Exit Full Screen" : "Enter Full Screen") + + Spacer() + + Button(action: isPresentedToggle) { + // if conversation.gpuActiveMemory > 0 { + // HStack { + // Image(systemName: "info.circle") + // Text("\(conversation.gpuActiveMemory)M") + // } + // .padding(4) + // .background(Color.black.opacity(0.2)) + // .cornerRadius(20) + // } else { + // Image(systemName: "info.circle") + // .padding(4) + // } + } + .font(.subheadline) + .popover(isPresented: $isPresented) { + VStack(alignment: .leading) { + LabeledContent { + Text(conversation.promptTime.formatted()) + } label: { + Text("Prompt Time") + .fontWeight(.bold) + } + + LabeledContent { + Text("\(Int(conversation.promptTokensPerSecond))") + } label: { + Text("Prompt Tokens/second") + .fontWeight(.bold) + } + + LabeledContent { + Text(conversation.generateTime.formatted()) + } label: { + Text("Generate Time") + .fontWeight(.bold) + } + + LabeledContent { + Text("\(Int(conversation.tokensPerSecond))") + } label: { + Text("Generate Tokens/second") + .fontWeight(.bold) + } + } + .padding() + .background(.clear) + } + + ModelPicker( + selection: $conversation.model, + models: ModelStore.shared.models + ) + } + .buttonStyle(.borderless) + .foregroundStyle(.white) + .tint(.white) + .frame(height: 35) + .padding(.horizontal, 10) + .task { + await ModelStore.shared.fetchModels() + } + } + + private func clearMessages() { + conversation.messages = [] + } + + private func isPresentedToggle() { + isPresented.toggle() + } + + private func switchDisplayStyle() { + withAnimation { + displayStyle = (displayStyle == .markdown) ? .plain : .markdown + } + } + + private func isEditorFullScreenToggle() { + withAnimation { + isEditorFullScreen.toggle() + } + } +} diff --git a/ChatMLX/Features/Conversation/Detail/EditorView.swift b/ChatMLX/Features/Conversation/Detail/EditorView.swift new file mode 100644 index 0000000..101c510 --- /dev/null +++ b/ChatMLX/Features/Conversation/Detail/EditorView.swift @@ -0,0 +1,78 @@ +// +// EditorView.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +import Luminare +import SwiftUI + +struct EditorView: View { + let conversation: Conversation + let displayStyle: DisplayStyle + let isEditorFullScreen: Bool + + @State var message = "" + + var body: some View { + ZStack(alignment: .bottom) { + UltramanTextEditor( + text: $message, + placeholder: "Type your message…", + onSubmit: send + ) + .padding(.horizontal, 5) + + HStack(spacing: 16) { + Spacer() + Button("Clear") { + message = "" + } + .buttonStyle(.borderless) + .disabled(message.isEmpty) + + Button(action: send) { + if conversation.inferring { + Label { + Text("Send") + } icon: { + ProgressView() + .controlSize(.small) + .padding(.trailing, 2) + .colorInvert() + .brightness(1) + } + } else { + Label("Send", systemImage: "paperplane") + } + } + .buttonStyle(LuminareCompactButtonStyle()) + .fixedSize() + .disabled( + message.trimmingCharacters( + in: .whitespacesAndNewlines + ).isEmpty || conversation.inferring) + } + .padding() + } + .frame(maxHeight: isEditorFullScreen ? .infinity : 150) + } + + private func send() { + guard let model = ModelStore.shared.model(conversation.model) else { + return + } + + let message = message.trimmingCharacters(in: .whitespacesAndNewlines) + Message(context: PersistenceController.shared.viewContext).user(content: message, conversation: conversation) + self.message = "" + Task { + await ConversationStore.shared.send( + conversation: conversation, + model: model, + content: message + ) + } + } +} diff --git a/ChatMLX/Features/Conversation/EmptyConversation.swift b/ChatMLX/Features/Conversation/Detail/EmptyConversation.swift similarity index 78% rename from ChatMLX/Features/Conversation/EmptyConversation.swift rename to ChatMLX/Features/Conversation/Detail/EmptyConversation.swift index df8cb1b..ef65d81 100644 --- a/ChatMLX/Features/Conversation/EmptyConversation.swift +++ b/ChatMLX/Features/Conversation/Detail/EmptyConversation.swift @@ -9,15 +9,13 @@ import Luminare import SwiftUI struct EmptyConversation: View { - @Environment(ConversationViewModel.self) private var conversationViewModel - var body: some View { ContentUnavailableView { Label("No Conversation", systemImage: "tray.fill") } description: { Text("Please select a new conversation") - Button(action: conversationViewModel.createConversation) { + Button(action: ConversationStore.shared.createConversation) { Label("New Conversation", systemImage: "plus") } .buttonStyle(LuminareCompactButtonStyle()) diff --git a/ChatMLX/Features/Conversation/Detail/MessageBoxView.swift b/ChatMLX/Features/Conversation/Detail/MessageBoxView.swift new file mode 100644 index 0000000..c606d9f --- /dev/null +++ b/ChatMLX/Features/Conversation/Detail/MessageBoxView.swift @@ -0,0 +1,50 @@ +// +// MessageBoxView.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +import SwiftUI + +struct MessageBoxView: View { + @ObservedObject var conversation: Conversation + @State var scrollViewProxy: ScrollViewProxy? + let displayStyle: DisplayStyle + + var body: some View { + ScrollViewReader { proxy in + ScrollView { + LazyVStack { + ForEach(conversation.messages) { message in + MessageBubbleView( + message: message, + displayStyle: displayStyle + ) + } + } + .padding() + } + .onChange( + of: conversation.messages.last, + { _, _ in + scrollToBottom() + } + ) + .onAppear { + scrollViewProxy = proxy + scrollToBottom() + } + } + } + + private func scrollToBottom() { + guard let lastMessageId = conversation.messages.last?.id, let scrollViewProxy else { + return + } + + withAnimation { + scrollViewProxy.scrollTo(lastMessageId, anchor: .bottom) + } + } +} diff --git a/ChatMLX/Features/Conversation/Detail/MessageBubbleView.swift b/ChatMLX/Features/Conversation/Detail/MessageBubbleView.swift new file mode 100644 index 0000000..b73345e --- /dev/null +++ b/ChatMLX/Features/Conversation/Detail/MessageBubbleView.swift @@ -0,0 +1,31 @@ +// +// MessageBubbleView.swift +// ChatMLX +// +// Created by John Mai on 2024/8/4. +// + +import AlertToast +import MarkdownUI +import SwiftUI + +struct MessageBubbleView: View { + @ObservedObject var message: Message + + let displayStyle: DisplayStyle + + var body: some View { + HStack { + if message.role == .assistant { + AssistantMessageView( + displayStyle: displayStyle, + message: message + ) + } else { + UserMessageView(message: message) + } + } + .textSelection(.enabled) + .padding(.vertical, 8) + } +} diff --git a/ChatMLX/Features/Conversation/RightSidebarView.swift b/ChatMLX/Features/Conversation/Detail/RightSidebarView.swift similarity index 100% rename from ChatMLX/Features/Conversation/RightSidebarView.swift rename to ChatMLX/Features/Conversation/Detail/RightSidebarView.swift diff --git a/ChatMLX/Features/Conversation/Detail/UserMessageView.swift b/ChatMLX/Features/Conversation/Detail/UserMessageView.swift new file mode 100644 index 0000000..5beeb4e --- /dev/null +++ b/ChatMLX/Features/Conversation/Detail/UserMessageView.swift @@ -0,0 +1,51 @@ +// +// UserMessageView.swift +// ChatMLX +// +// Created by John Mai on 2024/11/10. +// + +import SwiftUI + +struct UserMessageView: View { + let message: Message + + var body: some View { + Spacer() + VStack(alignment: .trailing) { + Text(message.content) + .padding(10) + .background(Color.black.opacity(0.1618)) + .foregroundColor(.white) + .cornerRadius(8) + + HStack { + Text(message.updatedAt?.toTimeFormatted() ?? "") + .font(.caption) + + Button(action: copy) { + Image(systemName: "doc.on.doc") + .help("Copy") + } + + Button(action: delete) { + Image(systemName: "trash") + } + } + .buttonStyle(.plain) + .foregroundColor(.white.opacity(0.8)) + .padding(.top, 4) + .frame(maxWidth: .infinity, alignment: .trailing) + } + } + + private func copy() { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(message.content, forType: .string) + } + + private func delete() { + ConversationStore.shared.deleteMessages(message.suffixMessages()) + } +} diff --git a/ChatMLX/Features/Conversation/MessageBubbleView.swift b/ChatMLX/Features/Conversation/MessageBubbleView.swift deleted file mode 100644 index faf965d..0000000 --- a/ChatMLX/Features/Conversation/MessageBubbleView.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// MessageBubbleView.swift -// ChatMLX -// -// Created by John Mai on 2024/8/4. -// - -import AlertToast -import MarkdownUI -import SwiftUI - -struct MessageBubbleView: View { - @Environment(LLMRunner.self) var runner - @Environment(ConversationViewModel.self) var vm - @Environment(\.managedObjectContext) private var viewContext - - @ObservedObject var message: Message - - @Binding var displayStyle: DisplayStyle - - @State private var showToast = false - - var body: some View { - HStack { - if message.role == .assistant { - assistantMessageView - } else { - Spacer() - userMessageView - } - } - .textSelection(.enabled) - .padding(.vertical, 8) - .toast(isPresenting: $showToast, duration: 1.5, offsetY: 30) { - AlertToast(displayMode: .hud, type: .complete(.green), title: "Copied") - } - } - - @ViewBuilder - private var assistantMessageView: some View { - HStack(alignment: .top, spacing: 12) { - Image("AppLogo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) - .background(.white) - .clipShape(RoundedRectangle(cornerRadius: 5)) - .shadow(color: .black.opacity(0.25), radius: 5, x: -1, y: 5) - - VStack(alignment: .leading) { - if displayStyle == .markdown { - Markdown(MarkdownContent(message.content)) - .markdownCodeSyntaxHighlighter( - .splash(theme: .sunset(withFont: .init(size: 16))) - ) - .markdownTextStyle { - ForegroundColor(.white) - } - .markdownTheme(.customGitHub) - } else { - Text(message.content) - } - - if let error = message.error, !error.isEmpty { - HStack { - Image(systemName: "exclamationmark.triangle") - .foregroundStyle(.yellow) - Text(error) - } - .padding(5) - .background(.red.opacity(0.3)) - .foregroundColor(.white) - .cornerRadius(5) - } - - HStack { - Button(action: copyText) { - Image(systemName: "doc.on.doc") - .help("Copy") - } - - Button(action: regenerate) { - Image(systemName: "arrow.clockwise") - .help("Regenerate") - } - .disabled(runner.running) - - if !(message.isFault || message.isDeleted) { - Text(message.updatedAt?.toTimeFormatted() ?? "") - .font(.caption) - } - - if message.role == .assistant, message.inferring { - ProgressView() - .controlSize(.small) - .colorInvert() - .brightness(1) - .padding(.leading, 5) - } - } - .buttonStyle(.plain) - .foregroundColor(.white.opacity(0.8)) - .padding(.top, 4) - .frame(maxWidth: .infinity, alignment: .leading) - } - Spacer() - } - } - - @ViewBuilder - private var userMessageView: some View { - VStack(alignment: .trailing) { - Text(message.content) - .padding(10) - .background(Color.black.opacity(0.1618)) - .foregroundColor(.white) - .cornerRadius(8) - - HStack { - Text(message.updatedAt?.toTimeFormatted() ?? "") - .font(.caption) - - Button(action: copyText) { - Image(systemName: "doc.on.doc") - .help("Copy") - } - - Button(action: delete) { - Image(systemName: "trash") - } - } - .buttonStyle(.plain) - .foregroundColor(.white.opacity(0.8)) - .padding(.top, 4) - .frame(maxWidth: .infinity, alignment: .trailing) - } - } - - private func delete() { - guard message.role == .user else { return } - for message in message.suffixMessages() { - viewContext.delete(message) - } - - Task(priority: .background) { - do { - try await viewContext.perform { - if viewContext.hasChanges { - try viewContext.save() - } - } - } catch { - vm.throwError(error, title: "Delete Message Failed") - } - } - } - - private func regenerate() { - guard message.role == .assistant else { return } - - let conversation = message.conversation - - if conversation.messages.last != message { - for message in message.suffixMessages() { - viewContext.delete(message) - } - } - guard let model = conversation.model else { - return - } - - conversation.inferring = true - - let factory = ProviderFactory.shared - - let assistantMessage = conversation.getLastAssistantMessage(context: viewContext) - assistantMessage.inferring = true - let messages = conversation.prepareMessages() - - Task { - let provider = await factory.provider(model.provider) - await provider.chat( - messages: messages, - config: .init( - model: .init( - name: "", - path: model.path - ) - ) - ) { result in - switch result { - case .success(let response): - Task { @MainActor in - assistantMessage.content = response.content - } - case .failure(let error): - Task { @MainActor in - assistantMessage.error = error.localizedDescription - } - } - } - - await MainActor.run { - conversation.inferring = false - assistantMessage.inferring = false - } - - if viewContext.hasChanges { - try? viewContext.save() - } - } - } - - private func copyText() { - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString(message.content, forType: .string) - showToast = true - } -} diff --git a/ChatMLX/Features/Conversation/ConversationSidebarItem.swift b/ChatMLX/Features/Conversation/Sidebar/ConversationSidebarItemView.swift similarity index 53% rename from ChatMLX/Features/Conversation/ConversationSidebarItem.swift rename to ChatMLX/Features/Conversation/Sidebar/ConversationSidebarItemView.swift index 49ef6b8..31f02c1 100644 --- a/ChatMLX/Features/Conversation/ConversationSidebarItem.swift +++ b/ChatMLX/Features/Conversation/Sidebar/ConversationSidebarItemView.swift @@ -7,15 +7,13 @@ import SwiftUI -struct ConversationSidebarItem: View { - @ObservedObject var conversation: Conversation +struct ConversationSidebarItemView: View { + let conversation: Conversation + @Binding var selectedConversation: Conversation? - @Environment(\.managedObjectContext) private var viewContext - @Environment(ConversationViewModel.self) private var vm - - @State private var isActive: Bool = false - - let persistence = PersistenceController.shared + var isActive: Bool { + selectedConversation == conversation + } var body: some View { Button(action: selectConversation) { @@ -39,9 +37,7 @@ struct ConversationSidebarItem: View { } .padding(6) } - .buttonStyle(UltramanSidebarButtonStyle(isActive: $isActive)) - .onAppear(perform: updateActiveState) - .onChange(of: vm.selectedConversation) { _, _ in updateActiveState() } + .buttonStyle(UltramanSidebarButtonStyle(isActive: .constant(isActive))) .contextMenu { Button(role: .destructive, action: deleteConversation) { Label("Delete", systemImage: "trash") @@ -50,23 +46,10 @@ struct ConversationSidebarItem: View { } private func selectConversation() { - vm.selectedConversation = conversation - } - - private func updateActiveState() { - withAnimation(.easeOut(duration: 0.1)) { - isActive = vm.selectedConversation == conversation - } + selectedConversation = conversation } private func deleteConversation() { - do { - try persistence.delete(conversation.objectID, in: viewContext) - if vm.selectedConversation == conversation { - vm.selectedConversation = nil - } - } catch { - vm.throwError(error, title: "Delete Conversation Failed") - } + ConversationStore.shared.deleteConversation(conversation) } } diff --git a/ChatMLX/Features/Conversation/ConversationSidebarView.swift b/ChatMLX/Features/Conversation/Sidebar/ConversationSidebarView.swift similarity index 71% rename from ChatMLX/Features/Conversation/ConversationSidebarView.swift rename to ChatMLX/Features/Conversation/Sidebar/ConversationSidebarView.swift index d8b3f0e..bf5c146 100644 --- a/ChatMLX/Features/Conversation/ConversationSidebarView.swift +++ b/ChatMLX/Features/Conversation/Sidebar/ConversationSidebarView.swift @@ -10,20 +10,17 @@ import Luminare import SwiftUI struct ConversationSidebarView: View { - @Environment(ConversationViewModel.self) private var vm - @Environment(\.managedObjectContext) private var viewContext - @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Conversation.updatedAt, ascending: false)], animation: .default ) private var conversations: FetchedResults - @Binding var selectedConversation: Conversation? - - @State private var keyword = "" + private let theme = Theme.shared + private let store = ConversationStore.shared - let padding: CGFloat = 8 + @Binding var selectedConversation: Conversation? + @State var keyword = "" var body: some View { VStack(spacing: 0) { @@ -39,7 +36,7 @@ struct ConversationSidebarView: View { private func headerView() -> some View { HStack { Spacer() - Button(action: vm.createConversation) { + Button(action: store.createConversation) { Image(systemName: "plus") } SettingsLink { @@ -47,7 +44,7 @@ struct ConversationSidebarView: View { } } .frame(height: 50) - .padding(.horizontal, padding) + .padding(.horizontal, theme.padding) .buttonStyle(.plain) } @@ -71,11 +68,11 @@ struct ConversationSidebarView: View { UltramanTextField( $keyword, placeholder: Text("Search Conversation..."), - onSubmit: updateSearchPredicate + onSubmit: search ) .frame(height: 25) } - .padding(.horizontal, padding) + .padding(.horizontal, theme.padding) } @ViewBuilder @@ -83,17 +80,23 @@ struct ConversationSidebarView: View { ScrollView { LazyVStack(spacing: 0) { ForEach(conversations) { conversation in - ConversationSidebarItem(conversation: conversation) + ConversationSidebarItemView( + conversation: conversation, + selectedConversation: $selectedConversation + ) } } } .padding(.top, 6) } - private func updateSearchPredicate() { - conversations.nsPredicate = keyword.isEmpty ? nil : NSPredicate( - format: "title CONTAINS [cd] %@ OR ANY messages.content CONTAINS [cd] %@", - keyword, keyword - ) + func search() { + conversations.nsPredicate = + keyword.isEmpty + ? nil + : NSPredicate( + format: "title CONTAINS [cd] %@ OR ANY messages.content CONTAINS [cd] %@", + keyword, keyword + ) } } diff --git a/ChatMLX/Features/Settings/DefaultConversationView.swift b/ChatMLX/Features/Settings/DefaultConversationView.swift index c003ad9..47cd492 100644 --- a/ChatMLX/Features/Settings/DefaultConversationView.swift +++ b/ChatMLX/Features/Settings/DefaultConversationView.swift @@ -12,8 +12,7 @@ import SwiftUI struct DefaultConversationView: View { @Default(.defaultTitle) var defaultTitle -// @Default(.defaultModel) var defaultModel - @State var defaultModel: ModelInfo? + @Default(.defaultModel) var defaultModel @Default(.defaultTemperature) var defaultTemperature @Default(.defaultTopP) var defaultTopP @Default(.defaultMaxLength) var defaultMaxLength @@ -28,9 +27,7 @@ struct DefaultConversationView: View { @Default(.defaultProvider) var defaultProvider - @State private var localModels: [LocalModel] = [] - - @Environment(SettingsViewModel.self) var vm + @Environment(ModelStore.self) var modelStore private let padding: CGFloat = 6 @@ -46,12 +43,13 @@ struct DefaultConversationView: View { } LuminareSection("Model Settings") { - LabeledContent("Provider") { - DefaultProviderPicker(provider: $defaultProvider) - } - LabeledContent("Model") { - DefaultModelPicker(provider: $defaultProvider) + ModelPicker( + selection: $defaultModel, + models: modelStore.models + ).task { + await modelStore.fetchModels() + } } LabeledContent("Temperature") { @@ -160,59 +158,10 @@ struct DefaultConversationView: View { .labeledContentStyle(.horizontal) .compactSliderSecondaryColor(.white) .scrollContentBackground(.hidden) - .onAppear(perform: loadModels) .ultramanNavigationTitle("Default Conversation") .labelsHidden() .buttonStyle(.borderless) .foregroundStyle(.white) .toggleStyle(.switch) } - - private func loadModels() { - let fileManager = FileManager.default - let documentsURL = fileManager.urls( - for: .documentDirectory, in: .userDomainMask - )[0] - let modelsURL = documentsURL.appendingPathComponent( - "huggingface/models") - - do { - let contents = try fileManager.contentsOfDirectory( - at: modelsURL, includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - var models: [LocalModel] = [] - - for groupURL in contents { - if groupURL.hasDirectoryPath { - let modelContents = try fileManager.contentsOfDirectory( - at: groupURL, includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - - for modelURL in modelContents { - if modelURL.hasDirectoryPath { - models.append( - LocalModel( - group: groupURL.lastPathComponent, - name: modelURL.lastPathComponent, - url: modelURL - ) - ) - } - } - } - } -// -// if !models.contains(where: { $0.origin == defaultModel }) { -// defaultModel = "" -// } - - Task { @MainActor in - localModels = models - } - } catch { - vm.throwError(error, title: "Load Models Failed") - } - } } diff --git a/ChatMLX/Features/Settings/GeneralView.swift b/ChatMLX/Features/Settings/GeneralView.swift index 2620543..9ae9e46 100644 --- a/ChatMLX/Features/Settings/GeneralView.swift +++ b/ChatMLX/Features/Settings/GeneralView.swift @@ -17,17 +17,10 @@ struct GeneralView: View { @Default(.language) var language @Default(.gpuCacheLimit) var gpuCacheLimit - @Environment(\.managedObjectContext) private var viewContext - - @Environment(SettingsViewModel.self) private var vm - @Environment(ConversationViewModel.self) private var conversationViewModel - - @Environment(LLMRunner.self) var runner + @Environment(ConversationStore.self) private var conversationStore let maxRAM = ProcessInfo.processInfo.physicalMemory / (1024 * 1024) - let persistenceController = PersistenceController.shared - var body: some View { VStack(spacing: 18) { LuminareSection("Language") { @@ -62,7 +55,7 @@ struct GeneralView: View { } LuminareSection("System Settings") { - Button("Clear All Conversations", action: clearAllConversations) + Button("Clear All Conversations", action: conversationStore.clearConversations) .frame(height: 35) Button("Reset All Settings", action: resetAllSettings) .frame(height: 35) @@ -80,28 +73,6 @@ struct GeneralView: View { private func resetAllSettings() { Defaults.removeAll() } - - private func clearAllConversations() { - let context = persistenceController.newBackgroundContext() - - Task.detached { - do { - try await context.perform { - try persistenceController.executeAndMergeChanges(using: [ - NSBatchDeleteRequest(fetchRequest: Message.fetchRequest()), - NSBatchDeleteRequest(fetchRequest: Conversation.fetchRequest()) - ], in: context) - } - await MainActor.run { - conversationViewModel.selectedConversation = nil - } - } catch { - await MainActor.run { - vm.throwError(error, title: "Clear All Conversations Failed") - } - } - } - } } #Preview { diff --git a/ChatMLX/Features/Settings/Local Models/LocalModelItemView.swift b/ChatMLX/Features/Settings/Local Models/LocalModelItemView.swift index e8fb1f6..8975958 100644 --- a/ChatMLX/Features/Settings/Local Models/LocalModelItemView.swift +++ b/ChatMLX/Features/Settings/Local Models/LocalModelItemView.swift @@ -6,16 +6,18 @@ // import SwiftUI +import HuggingfaceHub struct LocalModelItemView: View { - @Binding var model: LocalModel + let name: String + var onDelete: () -> Void @State private var showingDeleteAlert = false var body: some View { VStack { HStack { - Text(model.name) + Text(name) Spacer() Button(action: { showingDeleteAlert = true }) { Image(systemName: "trash") @@ -33,7 +35,7 @@ struct LocalModelItemView: View { Button("Cancel", role: .cancel) {} Button("Delete", role: .destructive, action: onDelete) } message: { - Text("Are you sure you want to delete '\(model.origin)'?") + Text("Are you sure you want to delete '\(name)'?") } } } diff --git a/ChatMLX/Features/Settings/Local Models/LocalModelsView.swift b/ChatMLX/Features/Settings/Local Models/LocalModelsView.swift index 29f18b8..3fe6a93 100644 --- a/ChatMLX/Features/Settings/Local Models/LocalModelsView.swift +++ b/ChatMLX/Features/Settings/Local Models/LocalModelsView.swift @@ -1,136 +1,75 @@ -//// -//// LocalModelsView.swift -//// ChatMLX -//// -//// Created by John Mai on 2024/8/10. -//// // -//import Defaults -//import SwiftUI +// LocalModelsView.swift +// ChatMLX // -//struct LocalModelsView: View { -// @State private var modelGroups: [LocalModelGroup] = [] -//// @Default(.defaultModel) var defaultModel +// Created by John Mai on 2024/8/10. // -// @Environment(SettingsViewModel.self) var vm -// -// var body: some View { -// List { -// ForEach(modelGroups.indices, id: \.self) { groupIndex in -// Section( -// header: Text(modelGroups[groupIndex].name).font( -// .title2.bold()) -// ) { -// ForEach(modelGroups[groupIndex].models.indices, id: \.self) { modelIndex in -// LocalModelItemView( -// model: $modelGroups[groupIndex].models[modelIndex], -// onDelete: { -// Task { -// deleteModel( -// at: IndexSet(integer: modelIndex), -// from: groupIndex) -// loadModels() -// } -// }) -// } -// .onDelete { offsets in -// Task { -// deleteModel(at: offsets, from: groupIndex) -// loadModels() -// } -// } -// } -// } -// } -// .onAppear(perform: loadModels) -// .scrollContentBackground(.hidden) -// .listStyle(SidebarListStyle()) -// .ultramanNavigationTitle("Models") -// .ultramanToolbar { -// Button(action: openModelsDirectory) { -// Image(systemName: "folder") -// } -// .buttonStyle(.plain) -// } -// } -// -// private func loadModels() { -// let fileManager = FileManager.default -// let documentsURL = fileManager.urls( -// for: .documentDirectory, in: .userDomainMask)[0] -// let modelsURL = documentsURL.appendingPathComponent( -// "huggingface/models") -// -// do { -// let contents = try fileManager.contentsOfDirectory( -// at: modelsURL, includingPropertiesForKeys: nil, -// options: [.skipsHiddenFiles]) -// var groups: [LocalModelGroup] = [] -// -// for groupURL in contents { -// if groupURL.hasDirectoryPath { -// let groupName = groupURL.lastPathComponent -// var models: [LocalModel] = [] -// -// let modelContents = try fileManager.contentsOfDirectory( -// at: groupURL, includingPropertiesForKeys: nil, -// options: [.skipsHiddenFiles]) -// -// for modelURL in modelContents { -// if modelURL.hasDirectoryPath { -// let modelName = modelURL.lastPathComponent -// models.append( -// LocalModel( -// group: groupName, -// name: modelName, -// url: modelURL) -// ) -// } -// } -// -// groups.append( -// LocalModelGroup(name: groupName, models: models)) -// } -// } -// -//// if !groups.contains(where: { -//// $0.models.contains(where: { $0 == defaultModel }) -//// }) { -//// defaultModel = nil -//// } -// -// Task { @MainActor in -// modelGroups = groups -// } -// } catch { -// vm.throwError(error, title: "Load Models Failed") -// } -// } -// -// private func deleteModel(at offsets: IndexSet, from group: Int) { -//// let fileManager = FileManager.default -//// -//// for index in offsets { -//// let model = modelGroups[group].models[index] -//// do { -//// try fileManager.removeItem(at: model.url) -//// modelGroups[group].models.remove(at: index) -//// if defaultModel == model.origin { -//// defaultModel = "" -//// } -//// } catch { -//// vm.throwError(error, title: "Delete Model Failed") -//// } -//// } -// } -// -// private func openModelsDirectory() { -// let fileManager = FileManager.default -// let documentsURL = fileManager.urls( -// for: .documentDirectory, in: .userDomainMask)[0] -// let modelsURL = documentsURL.appendingPathComponent( -// "huggingface/models") -// -// NSWorkspace.shared.open(modelsURL) -// } -//} + +import HuggingfaceHub +import SwiftUI + +struct LocalModelsView: View { + + @State private var groupedModels: [String: [CachedRepoInfo]] = [:] + private var service: HuggingfaceHubService = .init() + + var body: some View { + List { + ForEach(groupedModels.keys.sorted(), id: \.self) { key in + Section( + header: Text(key).font( + .title2.bold()) + ) { + EmptyView() + ForEach(groupedModels[key]!, id: \.id) { model in + LocalModelItemView(name: model.repoId.deletingPrefix("\(key)/"),onDelete: { + }) + } + } + } + } + .scrollContentBackground(.hidden) + .listStyle(SidebarListStyle()) + .ultramanNavigationTitle("Models") + .ultramanToolbar { + Button(action: openModelsDirectory) { + Image(systemName: "folder") + } + .buttonStyle(.plain) + }.task { + do { + groupedModels = try service.scanMLXModels() + } catch { + print(error) + } + } + + } + + private func deleteModel(at offsets: IndexSet, from group: Int) { + // let fileManager = FileManager.default + // + // for index in offsets { + // let model = modelGroups[group].models[index] + // do { + // try fileManager.removeItem(at: model.url) + // modelGroups[group].models.remove(at: index) + // if defaultModel == model.origin { + // defaultModel = "" + // } + // } catch { + // vm.throwError(error, title: "Delete Model Failed") + // } + // } + } + + private func openModelsDirectory() { + let fileManager = FileManager.default + let documentsURL = fileManager.urls( + for: .documentDirectory, in: .userDomainMask)[0] + let modelsURL = documentsURL.appendingPathComponent( + "huggingface/models") + + NSWorkspace.shared.open(modelsURL) + } +} diff --git a/ChatMLX/Features/Settings/MLX Community/MLXCommunityItemView.swift b/ChatMLX/Features/Settings/MLX Community/MLXCommunityItemView.swift index 3f0801f..b9329bf 100644 --- a/ChatMLX/Features/Settings/MLX Community/MLXCommunityItemView.swift +++ b/ChatMLX/Features/Settings/MLX Community/MLXCommunityItemView.swift @@ -6,9 +6,10 @@ // import SwiftUI +import HuggingfaceHub struct MLXCommunityItemView: View { - @Binding var model: RemoteModel + @Binding var model: CachedRepoInfo @Environment(SettingsViewModel.self) var settingsViewModel var body: some View { @@ -38,35 +39,35 @@ struct MLXCommunityItemView: View { } HStack { - Label("\(model.downloads)", systemImage: "arrow.down.circle") - .font(.subheadline) - - Label("\(model.likes)", systemImage: "heart.fill") - .font(.subheadline) - .foregroundColor(.red.opacity(0.6)) +// Label("\(model.downloads)", systemImage: "arrow.down.circle") +// .font(.subheadline) +// +// Label("\(model.likes)", systemImage: "heart.fill") +// .font(.subheadline) +// .foregroundColor(.red.opacity(0.6)) Spacer() - if let pipelineTag = model.pipelineTag { - Text(pipelineTag) - .font(.subheadline) - .padding(4) - .background(Color.blue.opacity(0.2)) - .cornerRadius(4) - } +// if let pipelineTag = model.pipelineTag { +// Text(pipelineTag) +// .font(.subheadline) +// .padding(4) +// .background(Color.blue.opacity(0.2)) +// .cornerRadius(4) +// } } - ScrollView(.horizontal, showsIndicators: false) { - HStack { - ForEach(model.tags, id: \.self) { tag in - Text(tag) - .font(.caption) - .padding(4) - .background(Color.gray.opacity(0.2)) - .cornerRadius(4) - } - } - } +// ScrollView(.horizontal, showsIndicators: false) { +// HStack { +// ForEach(model.tags, id: \.self) { tag in +// Text(tag) +// .font(.caption) +// .padding(4) +// .background(Color.gray.opacity(0.2)) +// .cornerRadius(4) +// } +// } +// } } .padding() .background(.black.opacity(0.3)) diff --git a/ChatMLX/Features/Settings/MLX Community/MLXCommunityView.swift b/ChatMLX/Features/Settings/MLX Community/MLXCommunityView.swift index e9e0bed..d21b97a 100644 --- a/ChatMLX/Features/Settings/MLX Community/MLXCommunityView.swift +++ b/ChatMLX/Features/Settings/MLX Community/MLXCommunityView.swift @@ -9,6 +9,7 @@ import Alamofire import Luminare import SwiftUI import os +import HuggingfaceHub struct MLXCommunityView: View { @Environment(SettingsViewModel.self) var settingsViewModel @@ -18,11 +19,12 @@ struct MLXCommunityView: View { @State var next: String? @State var status: Status = .isLoading + + @State var models: [CachedRepoInfo] = [] private let sessionManager: Session - - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MLXCommunityView") + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MLXCommunityView") enum Status { case isLoading @@ -48,16 +50,15 @@ struct MLXCommunityView: View { ) { Task { settingsViewModel.remoteModels = [] - await fetchModels(search: searchQuery) +// await fetchModels(search: searchQuery) } } - } .padding(.top) .padding(.horizontal) List { - ForEach($settingsViewModel.remoteModels) { model in + ForEach($models,id: \.repoId) { model in MLXCommunityItemView(model: model) } lastRowView @@ -66,7 +67,7 @@ struct MLXCommunityView: View { } .onAppear { Task { - await fetchModels() +// await fetchModels() } } .ultramanNavigationTitle("MLX Community") @@ -74,7 +75,7 @@ struct MLXCommunityView: View { Button(action: { Task { settingsViewModel.remoteModels = [] - await fetchModels() +// await fetchModels() } }) { Image(systemName: "arrow.clockwise") @@ -82,6 +83,9 @@ struct MLXCommunityView: View { .disabled(isFetching) .buttonStyle(.plain) } + .task { +// models = (try? HuggingfaceHubService().scanMLXModels()) ?? [] + } } @ViewBuilder @@ -100,7 +104,7 @@ struct MLXCommunityView: View { .frame(maxWidth: .infinity) .onAppear { Task { - await loadMoreModelsIfNeeded() +// await loadMoreModelsIfNeeded() } } } @@ -131,71 +135,71 @@ struct MLXCommunityView: View { return linkDict } - func fetchModels(search: String? = nil) async { - guard !isFetching else { return } - isFetching = true - status = .isLoading - - var urlComponents = URLComponents( - string: "https://huggingface.co/api/models")! - var queryItems: [URLQueryItem] = [ - URLQueryItem(name: "limit", value: "20"), - URLQueryItem(name: "author", value: "mlx-community"), - URLQueryItem(name: "sort", value: "downloads"), - URLQueryItem(name: "pipeline_tag", value: "text-generation"), - ] - - if let search { - queryItems.append(URLQueryItem(name: "search", value: search)) - } - - urlComponents.queryItems = queryItems - - guard let url = urlComponents.url else { return } - - sessionManager.request(url).validate().responseDecodable( - of: [RemoteModel].self - ) { response in - switch response.result { - case .success(let decodedResponse): - settingsViewModel.remoteModels = decodedResponse - if let links = response.response?.allHeaderFields["Link"] - as? String - { - next = parseLinks(links)["next"] - } - status = .idle - case .failure(let error): - logger.error("Failed to fetch models: \(error)") - status = .error(error.localizedDescription) - } - isFetching = false - } - } - - func loadMoreModelsIfNeeded() async { - guard !isFetching, let nextURL = URL(string: next ?? "") else { return } - isFetching = true - status = .isLoading - - sessionManager.request(nextURL).validate().responseDecodable( - of: [RemoteModel].self - ) { response in - switch response.result { - case .success(let decodedResponse): - settingsViewModel.remoteModels.append( - contentsOf: decodedResponse) - if let links = response.response?.allHeaderFields["Link"] - as? String - { - next = parseLinks(links)["next"] - } - status = .idle - case .failure(let error): - logger.error("Failed to fetch more models: \(error)") - status = .error(error.localizedDescription) - } - isFetching = false - } - } +// func fetchModels(search: String? = nil) async { +// guard !isFetching else { return } +// isFetching = true +// status = .isLoading +// +// var urlComponents = URLComponents( +// string: "https://huggingface.co/api/models")! +// var queryItems: [URLQueryItem] = [ +// URLQueryItem(name: "limit", value: "20"), +// URLQueryItem(name: "author", value: "mlx-community"), +// URLQueryItem(name: "sort", value: "downloads"), +// URLQueryItem(name: "pipeline_tag", value: "text-generation"), +// ] +// +// if let search { +// queryItems.append(URLQueryItem(name: "search", value: search)) +// } +// +// urlComponents.queryItems = queryItems +// +// guard let url = urlComponents.url else { return } +// +// sessionManager.request(url).validate().responseDecodable( +// of: [RemoteModel].self +// ) { response in +// switch response.result { +// case .success(let decodedResponse): +// settingsViewModel.remoteModels = decodedResponse +// if let links = response.response?.allHeaderFields["Link"] +// as? String +// { +// next = parseLinks(links)["next"] +// } +// status = .idle +// case .failure(let error): +// logger.error("Failed to fetch models: \(error)") +// status = .error(error.localizedDescription) +// } +// isFetching = false +// } +// } +// +// func loadMoreModelsIfNeeded() async { +// guard !isFetching, let nextURL = URL(string: next ?? "") else { return } +// isFetching = true +// status = .isLoading +// +// sessionManager.request(nextURL).validate().responseDecodable( +// of: [RemoteModel].self +// ) { response in +// switch response.result { +// case .success(let decodedResponse): +// settingsViewModel.remoteModels.append( +// contentsOf: decodedResponse) +// if let links = response.response?.allHeaderFields["Link"] +// as? String +// { +// next = parseLinks(links)["next"] +// } +// status = .idle +// case .failure(let error): +// logger.error("Failed to fetch more models: \(error)") +// status = .error(error.localizedDescription) +// } +// isFetching = false +// } +// } } diff --git a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderContentView.swift b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderContentView.swift index f879cc2..74a27d1 100644 --- a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderContentView.swift +++ b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderContentView.swift @@ -8,8 +8,8 @@ import CompactSlider import Defaults import Luminare -import os import SwiftUI +import os struct MLXProviderContentView: View { // MARK: - Properties @@ -58,13 +58,13 @@ extension MLXProviderContentView { private func modelListView() -> some View { LabeledContent { List { - ForEach(providerModels, id: \.self) { - MLXProviderModelItemView( - model: $0, - onDelete: onDelete - ) - } - .padding(.horizontal, -8) + // ForEach(providerModels, id: \.self) { + // MLXProviderModelItemView( + // model: $0, + // onDelete: onDelete + // ) + // } + // .padding(.horizontal, -8) } .listStyle(.plain) .scrollIndicators(.hidden) @@ -90,7 +90,7 @@ extension MLXProviderContentView { } } .frame(height: 35) - + } .padding(.horizontal) .labeledContentStyle(.vertical) @@ -131,19 +131,19 @@ extension MLXProviderContentView { private func onDelete(_ model: ProviderModel) { Task { - let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - - if let path = model.path { - if path.standardized.path.hasPrefix(documentsURL.appendingPathComponent("huggingface/models").standardized.path) { - try? FileManager.default.removeItem(at: path) - } else { - if let model = models.first(where: { $0.id == model.id }) { - viewContext.delete(model) - try? viewContext.save() - } - } - try? await loadProviderModels() - } + // let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + // + // if let path = model.path { + // if path.standardized.path.hasPrefix(documentsURL.appendingPathComponent("huggingface/models").standardized.path) { + // try? FileManager.default.removeItem(at: path) + // } else { + // if let model = models.first(where: { $0.id == model.id }) { + // viewContext.delete(model) + // try? viewContext.save() + // } + // } + // try? await loadProviderModels() + // } } } @@ -158,18 +158,18 @@ extension MLXProviderContentView { // MARK: - Load Provider Models private func loadProviderModels() async throws { - var models = try MLXProvider.fetchModels() - - for model in self.models { - if let index = models.firstIndex(where: { $0.id == model.id }) { - models[index] = ProviderModel(from: model) - } else { - models.append(ProviderModel(from: model)) - } - } - - await MainActor.run { - providerModels = models - } + // var models = try MLXProvider.fetchModels() + // + // for model in self.models { + // if let index = models.firstIndex(where: { $0.id == model.id }) { + // models[index] = ProviderModel(from: model) + // } else { + // models.append(ProviderModel(from: model)) + // } + // } + // + // await MainActor.run { + // providerModels = models + // } } } diff --git a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderModelItemView.swift b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderModelItemView.swift index 325ad3e..d3e1322 100644 --- a/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderModelItemView.swift +++ b/ChatMLX/Features/Settings/Model Manager/Providers/MLX/MLXProviderModelItemView.swift @@ -5,34 +5,36 @@ // Created by John Mai on 2024/10/26. // import SwiftUI + struct MLXProviderModelItemView: View { let model: ProviderModel let onDelete: (ProviderModel) -> Void var body: some View { - HStack { - Text(model.name ?? model.id) - Spacer() - Button(action: { - openDirectory(model.path) - }) { - Image(systemName: "folder") - } - - Button(action: { - onDelete(model) - }) { - Image(systemName: "trash") - .renderingMode(.original) - } - } - .buttonStyle(.plain) - .padding() - .background(.black.opacity(0.3)) - .listRowSeparator(.hidden) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .shadow(color: .black, radius: 2) -// .listRowInsets(EdgeInsets(top: 0, leading: -5, bottom: 10, trailing: -5)) + EmptyView() + // HStack { + // Text(model.name ?? model.id) + // Spacer() + // Button(action: { + // openDirectory(model.path) + // }) { + // Image(systemName: "folder") + // } + // + // Button(action: { + // onDelete(model) + // }) { + // Image(systemName: "trash") + // .renderingMode(.original) + // } + // } + // .buttonStyle(.plain) + // .padding() + // .background(.black.opacity(0.3)) + // .listRowSeparator(.hidden) + // .clipShape(RoundedRectangle(cornerRadius: 10)) + // .shadow(color: .black, radius: 2) + // .listRowInsets(EdgeInsets(top: 0, leading: -5, bottom: 10, trailing: -5)) } private func openDirectory(_ path: URL?) { diff --git a/ChatMLX/Features/Settings/Model Manager/Providers/OpenAI/OpenAIProviderView.swift b/ChatMLX/Features/Settings/Model Manager/Providers/OpenAI/OpenAIProviderView.swift index 1f8b50d..e828d2f 100644 --- a/ChatMLX/Features/Settings/Model Manager/Providers/OpenAI/OpenAIProviderView.swift +++ b/ChatMLX/Features/Settings/Model Manager/Providers/OpenAI/OpenAIProviderView.swift @@ -14,6 +14,8 @@ struct OpenAIProviderView: View { @State var isEnabled: Bool? = false @Default(.enableOpenAI) var enableOpenAI + @Default(.openAIApiKey) var openAIApiKey + @Default(.openAIBaseURL) var openAIBaseURL var body: some View { ProviderView( @@ -43,15 +45,22 @@ struct OpenAIProviderView: View { @ViewBuilder func Content() -> some View { DividedVStack { - // API Key LabeledContent("API Key") { - SecureField("API Key", text: .constant("")) + UltramanSecureField( + $openAIApiKey, + placeholder: Text("Enter your OpenAI API Key"), + alignment: .trailing + ) } - // API 代理地址 LabeledContent("API Proxy Address") { - TextField("API Proxy Address", text: .constant("")) + UltramanTextField( + $openAIBaseURL, + placeholder: Text("Enter your OpenAI API Proxy Address"), + alignment: .trailing + ) } } + .labeledContentStyle(.horizontal) } } diff --git a/ChatMLX/Features/Settings/SettingsSidebarItemView.swift b/ChatMLX/Features/Settings/SettingsSidebarItemView.swift index 1c09dd1..d6a1009 100644 --- a/ChatMLX/Features/Settings/SettingsSidebarItemView.swift +++ b/ChatMLX/Features/Settings/SettingsSidebarItemView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SettingsSidebarItemView: View { - @Environment(SettingsViewModel.self) var settingsViewModel + @Environment(SettingsStore.self) var store let tab: SettingsTab @@ -16,10 +16,6 @@ struct SettingsSidebarItemView: View { @State private var isActive: Bool = false @State private var showIndicator: Bool = false - init(_ tab: SettingsTab) { - self.tab = tab - } - var body: some View { HStack(spacing: 8) { tab.iconView() @@ -56,12 +52,12 @@ struct SettingsSidebarItemView: View { .onHover { isHovering = $0 } .onAppear { checkIfSelfIsActiveTab() - showIndicator = tab.showIndicator?(settingsViewModel) ?? false + showIndicator = tab.showIndicator?() ?? false } - .onChange(of: settingsViewModel.activeTabID) { _, _ in + .onChange(of: store.activeTabID) { _, _ in checkIfSelfIsActiveTab() } - .onChange(of: tab.showIndicator?(settingsViewModel) ?? false) { + .onChange(of: tab.showIndicator?() ?? false) { _, newValue in withAnimation { showIndicator = newValue @@ -72,7 +68,7 @@ struct SettingsSidebarItemView: View { func checkIfSelfIsActiveTab() { withAnimation(.easeOut(duration: 0.1)) { - isActive = settingsViewModel.activeTabID == tab.id + isActive = store.activeTabID == tab.id } } } diff --git a/ChatMLX/Features/Settings/SettingsSidebarView.swift b/ChatMLX/Features/Settings/SettingsSidebarView.swift index ccbc8de..e7eba55 100644 --- a/ChatMLX/Features/Settings/SettingsSidebarView.swift +++ b/ChatMLX/Features/Settings/SettingsSidebarView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SettingsSidebarView: View { - @Environment(SettingsViewModel.self) var settingsViewModel + @Environment(SettingsStore.self) var store let titlebarHeight: CGFloat = 50 let groupSpacing: CGFloat = 4 @@ -21,17 +21,18 @@ struct SettingsSidebarView: View { .init(.defaultConversation, Image(systemName: "person.bubble")), .init(.huggingFace, Image("huggingface")), .init(.models, Image(systemName: "brain")), + .init(.providers, Image(systemName: "brain")), .init(.mlxCommunity, Image("MLX")), .init( .downloadManager, Image(systemName: "arrow.down.circle"), - showIndicator: { $0.tasks.contains { $0.isDownloading } } + showIndicator: { DownloadStore.shared.tasks.contains { $0.isDownloading } } ), .init(.experimentalFeatures, Image(systemName: "flask")), .init(.about, Image(systemName: "info.circle")), ] var body: some View { - @Bindable var settingsViewModel = settingsViewModel + @Bindable var store = store VStack(alignment: .leading) { Group { Text("Settings") @@ -43,9 +44,10 @@ struct SettingsSidebarView: View { } .padding(.horizontal, itemPadding) - List(selection: $settingsViewModel.activeTabID) { + List(selection: $store.activeTabID) { ForEach(Self.tabs) { tab in - SettingsSidebarItemView(tab) + SettingsSidebarItemView(tab: tab) + .tag(tab.id) } } .scrollContentBackground(.hidden) diff --git a/ChatMLX/Features/Settings/SettingsView.swift b/ChatMLX/Features/Settings/SettingsView.swift index 3fb4d0b..2c40a3f 100644 --- a/ChatMLX/Features/Settings/SettingsView.swift +++ b/ChatMLX/Features/Settings/SettingsView.swift @@ -8,16 +8,14 @@ import SwiftUI struct SettingsView: View { - @Environment(SettingsViewModel.self) var vm + @Environment(SettingsStore.self) var store var body: some View { - @Bindable var vm = vm - UltramanNavigationSplitView(sidebarWidth: 220) { SettingsSidebarView() } detail: { Group { - switch vm.activeTabID { + switch store.activeTabID { case .general: GeneralView() case .defaultConversation: @@ -25,6 +23,8 @@ struct SettingsView: View { case .huggingFace: HuggingFaceView() case .models: + LocalModelsView() + case .providers: ModelManagerView() case .downloadManager: DownloadManagerView() diff --git a/ChatMLX/Features/Theme/Theme.swift b/ChatMLX/Features/Theme/Theme.swift new file mode 100644 index 0000000..6d58b0c --- /dev/null +++ b/ChatMLX/Features/Theme/Theme.swift @@ -0,0 +1,16 @@ +// +// Theme.swift +// ChatMLX +// +// Created by John Mai on 2024/11/3. +// +import SwiftUI + +@MainActor +@Observable +final class Theme { + static let shared = Theme() + + let conversationDetailWidth: CGFloat = 550 + let padding: CGFloat = 8 +} diff --git a/ChatMLX/Features/View Models/ConversationViewModel.swift b/ChatMLX/Features/View Models/ConversationViewModel.swift deleted file mode 100644 index dc0226b..0000000 --- a/ChatMLX/Features/View Models/ConversationViewModel.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// ConversationViewModel.swift -// ChatMLX -// -// Created by John Mai on 2024/10/3. -// - -import SwiftUI -import os - -@Observable -class ConversationViewModel { - var detailWidth: CGFloat = 550 - var selectedConversation: Conversation? - var error: Error? - var errorTitle: String? - var showErrorAlert = false - - private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ConversationViewModel") - - func throwError(_ error: Error, title: String? = nil) { - logger.error("\(error.localizedDescription)") - self.error = error - errorTitle = title - showErrorAlert = true - } - - func createConversation() { - do { - let context = PersistenceController.shared.container.viewContext - let conversation = Conversation(context: context) - try PersistenceController.shared.save() - selectedConversation = conversation - } catch { - throwError(error, title: "Create Conversation Failed") - } - } -} diff --git a/ChatMLX/Features/View Models/ConversationViewModelOld.swift b/ChatMLX/Features/View Models/ConversationViewModelOld.swift new file mode 100644 index 0000000..cc1291c --- /dev/null +++ b/ChatMLX/Features/View Models/ConversationViewModelOld.swift @@ -0,0 +1,38 @@ +//// +//// ConversationViewModel.swift +//// ChatMLX +//// +//// Created by John Mai on 2024/10/3. +//// +// +//import SwiftUI +//import os +// +//@Observable +//class ConversationViewModelOld { +// var detailWidth: CGFloat = 550 +// var selectedConversation: Conversation? +// var error: Error? +// var errorTitle: String? +// var showErrorAlert = false +// +// private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ConversationViewModel") +// +// func throwError(_ error: Error, title: String? = nil) { +// logger.error("\(error.localizedDescription)") +// self.error = error +// errorTitle = title +// showErrorAlert = true +// } +// +// func createConversation() { +// do { +// let context = PersistenceController.shared.container.viewContext +// let conversation = Conversation(context: context) +//// try PersistenceController.shared.save() +// selectedConversation = conversation +// } catch { +// throwError(error, title: "Create Conversation Failed") +// } +// } +//} diff --git a/ChatMLX/Features/View Models/ModelManagerViewModel.swift b/ChatMLX/Features/View Models/ModelManagerViewModel.swift index e8c358b..c6fee12 100644 --- a/ChatMLX/Features/View Models/ModelManagerViewModel.swift +++ b/ChatMLX/Features/View Models/ModelManagerViewModel.swift @@ -1,36 +1,36 @@ +//// +//// ModelManagerViewModel.swift +//// ChatMLX +//// +//// Created by John Mai on 2024/10/10. +//// // -// ModelManagerViewModel.swift -// ChatMLX -// -// Created by John Mai on 2024/10/10. -// - -import SwiftUI - -@Observable -class ModelManagerViewModel { - var models: [ModelInfo] = [] - - init() { - try? loadModels() - } - - func loadModels() throws { - - } - - func deleteModel(_ model: ModelInfo) throws { - guard let path = model.path else { - return - } - -// if !model.isExternal { -// let fileManager = FileManager.default -// try fileManager.removeItem(at: path) +//import SwiftUI +// +//@Observable +//class ModelManagerViewModel { +// var models: [ModelInfo] = [] +// +// init() { +// try? loadModels() +// } +// +// func loadModels() throws { +// +// } +// +// func deleteModel(_ model: ModelInfo) throws { +// guard let path = model.path else { +// return +// } +// +//// if !model.isExternal { +//// let fileManager = FileManager.default +//// try fileManager.removeItem(at: path) +//// } +// +// if let index = models.firstIndex(of: model) { +// models.remove(at: index) // } - - if let index = models.firstIndex(of: model) { - models.remove(at: index) - } - } -} +// } +//} diff --git a/ChatMLX/Features/View Models/SettingsViewModel.swift b/ChatMLX/Features/View Models/SettingsViewModel.swift index cc85eea..f91cfe8 100644 --- a/ChatMLX/Features/View Models/SettingsViewModel.swift +++ b/ChatMLX/Features/View Models/SettingsViewModel.swift @@ -8,9 +8,9 @@ import SwiftUI import os @Observable -class SettingsViewModel { +class SettingsViewModel:@unchecked Sendable { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "SettingsViewModel") - + var tasks: [DownloadTask] = [] var sidebarWidth: CGFloat = 250 var activeTabID: SettingsTab.ID = .general diff --git a/ChatMLX/Resources/Localizable.xcstrings b/ChatMLX/Resources/Localizable.xcstrings index 201d43e..84b0f4f 100644 --- a/ChatMLX/Resources/Localizable.xcstrings +++ b/ChatMLX/Resources/Localizable.xcstrings @@ -219,19 +219,19 @@ "value" : "" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "" @@ -457,19 +457,19 @@ "value" : "%.2f" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%.2f" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%.2f" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%.2f" @@ -695,19 +695,19 @@ "value" : "%.2f%%" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%.2f%%" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%.2f%%" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%.2f%%" @@ -933,19 +933,19 @@ "value" : "%d" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%d" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%d" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%d" @@ -1171,19 +1171,19 @@ "value" : "%lld" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld" @@ -1415,12 +1415,6 @@ "value" : "%1$lld / %2$lld" } }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$lld / %2$lld" - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1432,243 +1426,11 @@ "state" : "translated", "value" : "%1$lld / %2$lld" } - } - } - }, - "%lldM" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "‏%lld مليون" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld M" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld M" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "en-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld M" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Mt" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldJuta" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldJ" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld M" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld M" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld МБ" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldМ" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } }, "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "%lldM" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lldM" + "value" : "%1$lld / %2$lld" } } } @@ -1891,19 +1653,19 @@ "value" : "%lldMB" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lldMB" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lldMB" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lldMB" @@ -2130,25 +1892,37 @@ "value" : "Giới thiệu" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "關於" + "value" : "关于" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "关于" + "value" : "關於" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "關於" } } } + }, + "An error has occurred!" : { + + }, + "Any to Any" : { + + }, + "API Key" : { + + }, + "API Proxy Address" : { + }, "Apple Intelligence Effect" : { "localizations" : { @@ -2368,22 +2142,22 @@ "value" : "Hiệu ứng Trí tuệ của Apple" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Apple 智能效果" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "Apple 智能效果" + "value" : "Apple 智能效應" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "Apple 智能效應" + "value" : "Apple 智能效果" } } } @@ -2606,19 +2380,19 @@ "value" : "Bạn có chắc chắn muốn xóa '%@' không?" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "您確定要刪除「%@」嗎?" + "value" : "您确定要删除'%@'吗?" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "您确定要删除'%@'吗?" + "value" : "您確定要刪除「%@」嗎?" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "您確定要刪除「%@」嗎?" @@ -2844,19 +2618,19 @@ "value" : "Làm mờ" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "模糊" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "模糊" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "模糊" @@ -3082,19 +2856,19 @@ "value" : "Hủy" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消" @@ -3320,19 +3094,19 @@ "value" : "ChatMLX" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "ChatMLX" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "ChatMLX" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "ChatMLX" @@ -3559,19 +3333,19 @@ "value" : "Kiểm tra cập nhật" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "檢查更新" + "value" : "检查更新" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "检查更新" + "value" : "檢查更新" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢查更新" @@ -3798,12 +3572,6 @@ "value" : "Đang kiểm tra..." } }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "正在檢查..." - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3815,6 +3583,12 @@ "state" : "translated", "value" : "正在檢查" } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在檢查..." + } } } }, @@ -4037,19 +3811,19 @@ "value" : "Xóa" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "清除" + "value" : "清空" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "清空" + "value" : "清除" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "清除" @@ -4275,19 +4049,19 @@ "value" : "Xóa Tất Cả Cuộc Trò Chuyện" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "清除所有對話" + "value" : "清除所有对话" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "清除所有对话" + "value" : "清除所有對話" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "清除所有對話" @@ -4513,19 +4287,19 @@ "value" : "Màu sắc" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "顏色" + "value" : "颜色" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "颜色" + "value" : "顏色" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顏色" @@ -4751,19 +4525,19 @@ "value" : "Xác nhận Xóa" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "確認刪除" + "value" : "确认删除" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "确认删除" + "value" : "確認刪除" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "確認刪除" @@ -4989,19 +4763,19 @@ "value" : "Tiêu đề cuộc trò chuyện" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "對話標題" + "value" : "对话标题" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "对话标题" + "value" : "對話標題" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "對話標題" @@ -5227,12 +5001,6 @@ "value" : "Sao chép" } }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "複製" - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5244,6 +5012,12 @@ "state" : "translated", "value" : "拷貝" } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "複製" + } } } }, @@ -5465,12 +5239,6 @@ "value" : "Điểm Kết Thúc Tùy Chỉnh" } }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "自訂端點" - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5482,6 +5250,12 @@ "state" : "translated", "value" : "自定義端點" } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "自訂端點" + } } } }, @@ -5703,12 +5477,6 @@ "value" : "Điểm cuối Custom Hugging Face" } }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "自定 Hugging Face 端點" - } - }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5720,6436 +5488,6460 @@ "state" : "translated", "value" : "自訂 Hugging Face 端點" } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "自定 Hugging Face 端點" + } } } }, - "Default Conversation" : { + "Default conversation title" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "المحادثة الافتراضية" + "value" : "عنوان المحادثة الافتراضي" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Conversa predeterminada" + "value" : "Títol de la conversa per defecte" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Výchozí konverzace" + "value" : "Výchozí název konverzace" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Standard samtale" + "value" : "Standard samtaletitel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Standardunterhaltung" + "value" : "Standard-Konversationstitel" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Προεπιλεγμένη Συνομιλία" + "value" : "Προεπιλεγμένος τίτλος συνομιλίας" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Default Conversation" + "value" : "Default conversation title" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Default Conversation" + "value" : "Default conversation title" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Default Conversation" + "value" : "Default conversation title" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Conversación predeterminada" + "value" : "Título de conversación predeterminado" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Conversación predeterminada" + "value" : "Título predeterminado de la conversación" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Oletuskeskustelu" + "value" : "Keskustelun oletusnimeke" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Conversation par défaut" + "value" : "Titre de conversation par défaut" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Conversation par défaut" + "value" : "Titre de la conversation par défaut" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שיחה ברירת מחדל" + "value" : "כותרת שיחה ברירת מחדל" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "डिफ़ॉल्ट बातचीत" + "value" : "डिफ़ॉल्ट बातचीत शीर्षक" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Zadani razgovor" + "value" : "Zadani naslov razgovora" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Alapértelmezett beszélgetés" + "value" : "Alapértelmezett beszélgetés címe" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Percakapan Default" + "value" : "Judul percakapan default" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Conversazione predefinita" + "value" : "Titolo conversazione predefinito" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "デフォルトの会話" + "value" : "デフォルトの会話タイトル" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "기본 대화" + "value" : "기본 대화 제목" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Perbualan Lalai" + "value" : "Tajuk perbualan lalai" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Standardkonversasjon" + "value" : "Standard samtaletittel" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Standaardgesprek" + "value" : "Standaard gesprekstitel" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Domyślna konwersacja" + "value" : "Domyślny tytuł rozmowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Conversa Padrão" + "value" : "Título da conversa padrão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Conversa Predefinida" + "value" : "Título de conversa padrão" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Conversație implicită" + "value" : "Titlu conversație implicită" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Разговор по умолчанию" + "value" : "Название беседы по умолчанию" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Predvolená konverzácia" + "value" : "Predvolený názov konverzácie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Standardkonversation" + "value" : "Standardkonversationstitel" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การสนทนาเริ่มต้น" + "value" : "ชื่อการสนทนาเริ่มต้น" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Varsayılan Konuşma" + "value" : "Varsayılan sohbet başlığı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Розмова за замовчуванням" + "value" : "Типова назва розмови" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Đoạn hội thoại mặc định" + "value" : "Tiêu đề cuộc trò chuyện mặc định" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "默認對話" + "value" : "默认对话标题" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "默认对话" + "value" : "預設對話標題" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "預設對話" + "value" : "預設對話標題" } } } }, - "Default conversation title" : { + "Delete" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "عنوان المحادثة الافتراضي" + "value" : "مسح" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Títol de la conversa per defecte" + "value" : "Esborrar" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Výchozí název konverzace" + "value" : "Smazat" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Standard samtaletitel" + "value" : "Slet" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Standard-Konversationstitel" + "value" : "Löschen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Προεπιλεγμένος τίτλος συνομιλίας" + "value" : "Διαγραφή" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Default conversation title" + "value" : "Delete" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Default conversation title" + "value" : "Delete" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Default conversation title" + "value" : "Delete" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Título de conversación predeterminado" + "value" : "Eliminar" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Título predeterminado de la conversación" + "value" : "Eliminar" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Keskustelun oletusnimeke" + "value" : "Poista" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Titre de conversation par défaut" + "value" : "Supprimer" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Titre de la conversation par défaut" + "value" : "Supprimer" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "כותרת שיחה ברירת מחדל" + "value" : "מחק" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "डिफ़ॉल्ट बातचीत शीर्षक" + "value" : "हटाएं" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Zadani naslov razgovora" + "value" : "Izbriši" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Alapértelmezett beszélgetés címe" + "value" : "Törlés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Judul percakapan default" + "value" : "Hapus" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Titolo conversazione predefinito" + "value" : "Elimina" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "デフォルトの会話タイトル" + "value" : "削除" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "기본 대화 제목" + "value" : "삭제" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tajuk perbualan lalai" + "value" : "Padam" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Standard samtaletittel" + "value" : "Slett" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Standaard gesprekstitel" + "value" : "Verwijderen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Domyślny tytuł rozmowy" + "value" : "Usuń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Título da conversa padrão" + "value" : "Apagar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Título de conversa padrão" + "value" : "Eliminar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Titlu conversație implicită" + "value" : "Ștergeți" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Название беседы по умолчанию" + "value" : "Удалить" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Predvolený názov konverzácie" + "value" : "Odstrániť" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Standardkonversationstitel" + "value" : "Radera" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ชื่อการสนทนาเริ่มต้น" + "value" : "ลบ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Varsayılan sohbet başlığı" + "value" : "Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Типова назва розмови" + "value" : "Видалити" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tiêu đề cuộc trò chuyện mặc định" + "value" : "Xóa" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "預設對話標題" + "value" : "删除" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "默认对话标题" + "value" : "刪除" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "預設對話標題" + "value" : "刪除" } } } }, - "Delete" : { + "Display Mode" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مسح" + "value" : "طريقة العرض" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Esborrar" + "value" : "Mode de visualització" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Smazat" + "value" : "Režim zobrazení" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Slet" + "value" : "Visningstilstand" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Löschen" + "value" : "Anzeige-Modus" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Διαγραφή" + "value" : "Λειτουργία εμφάνισης" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Delete" + "value" : "Display Mode" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Delete" + "value" : "Display Mode" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Delete" + "value" : "Display Mode" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar" + "value" : "Modo de visualización" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar" + "value" : "Modo de visualización" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Poista" + "value" : "Näyttötila" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Supprimer" + "value" : "Mode dʼaffichage" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Supprimer" + "value" : "Mode d'affichage" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מחק" + "value" : "מצב תצוגה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "हटाएं" + "value" : "डिस्प्ले मोड" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Izbriši" + "value" : "Način prikaza" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Törlés" + "value" : "Megjelenítési mód" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hapus" + "value" : "Mode Tampilan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Elimina" + "value" : "Modalità di visualizzazione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "削除" + "value" : "表示モード" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "삭제" + "value" : "화면 모드" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Padam" + "value" : "Mod Paparan" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Slett" + "value" : "Visningsmodus" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verwijderen" + "value" : "Weergavemodus" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Usuń" + "value" : "Tryb wyświetlania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Apagar" + "value" : "Modo de Exibição" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Eliminar" + "value" : "Modo de Exibição" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ștergeți" + "value" : "Mod de afișare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Удалить" + "value" : "Режим отображения" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Odstrániť" + "value" : "Režim zobrazenia" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Radera" + "value" : "Visningsläget" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ลบ" + "value" : "โหมดการแสดงผล" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sil" + "value" : "Görüntüleme Modu" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Видалити" + "value" : "Режим відображення" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Xóa" + "value" : "Chế độ hiển thị" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "刪除" + "value" : "显示模式" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "删除" + "value" : "顯示模式" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "刪除" + "value" : "顯示模式" } } } }, - "Display Mode" : { + "Done" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "طريقة العرض" + "value" : "تم" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Mode de visualització" + "value" : "Fet" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Režim zobrazení" + "value" : "Hotovo" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Visningstilstand" + "value" : "OK" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Anzeige-Modus" + "value" : "Fertig" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Λειτουργία εμφάνισης" + "value" : "Τέλος" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Display Mode" + "value" : "Done" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Display Mode" + "value" : "Done" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Display Mode" + "value" : "Done" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Modo de visualización" + "value" : "OK" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Modo de visualización" + "value" : "OK" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Näyttötila" + "value" : "Valmis" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Mode dʼaffichage" + "value" : "Terminé" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Mode d'affichage" + "value" : "OK" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מצב תצוגה" + "value" : "סיום" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "डिस्प्ले मोड" + "value" : "पूर्ण करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Način prikaza" + "value" : "Završi" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Megjelenítési mód" + "value" : "Kész" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Mode Tampilan" + "value" : "Selesai" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Modalità di visualizzazione" + "value" : "Fine" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "表示モード" + "value" : "完了" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "화면 모드" + "value" : "완료" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Mod Paparan" + "value" : "Selesai" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Visningsmodus" + "value" : "Ferdig" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Weergavemodus" + "value" : "Gereed" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Tryb wyświetlania" + "value" : "Gotowe" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Modo de Exibição" + "value" : "OK" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Modo de Exibição" + "value" : "Concluído" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Mod de afișare" + "value" : "OK" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Режим отображения" + "value" : "Готово" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Režim zobrazenia" + "value" : "Hotovo" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Visningsläget" + "value" : "Klar" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โหมดการแสดงผล" + "value" : "เสร็จสิ้น" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Görüntüleme Modu" + "value" : "Bitti" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Режим відображення" + "value" : "Готово" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chế độ hiển thị" + "value" : "Xong" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "顯示模式" + "value" : "完成" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "显示模式" + "value" : "完成" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "顯示模式" + "value" : "完成" } } } }, - "Done" : { + "Download Manager" : { "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تم" + "value" : "إدارة التنزيلات" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Fet" + "value" : "Gestor de descàrregues" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Hotovo" + "value" : "Správce stahování" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Overførselsstyring" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Fertig" + "value" : "Download-Manager" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Τέλος" + "value" : "Διαχειριστής Λήψεων" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Done" + "value" : "Download Manager" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Done" + "value" : "Download Manager" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Done" + "value" : "Download Manager" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Gestor de Descargas" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Gestor de Descargas" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Valmis" + "value" : "Latausten Hallinta" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Terminé" + "value" : "Gestionnaire de téléchargements" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Gestionnaire de téléchargement" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "סיום" + "value" : "מנהל הורדות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "पूर्ण करें" + "value" : "डाउनलोड मैनेजर" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Završi" + "value" : "Upravitelj preuzimanja" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kész" + "value" : "Letöltéskezelő" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Selesai" + "value" : "Pengelola Unduhan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Fine" + "value" : "Gestore dei download" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "完了" + "value" : "ダウンロードマネージャー" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "완료" + "value" : "다운로드 관리자" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Selesai" + "value" : "Pengurus Muat Turun" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ferdig" + "value" : "Nedlastingsbehandling" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gereed" + "value" : "Downloadbeheerder" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Gotowe" + "value" : "Menedżer pobierania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Gerenciador de Downloads" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Concluído" + "value" : "Gestor de Transferências" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "OK" + "value" : "Manager de descărcări" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Готово" + "value" : "Менеджер загрузок" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Hotovo" + "value" : "Manažér sťahovania" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Klar" + "value" : "Nedladdningshanterare" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เสร็จสิ้น" + "value" : "ตัวจัดการดาวน์โหลด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bitti" + "value" : "İndirme Yöneticisi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Готово" + "value" : "Менеджер завантажень" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Xong" + "value" : "Trình quản lý Tải xuống" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "完成" + "value" : "下载管理" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "完成" + "value" : "下載管理器" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "完成" + "value" : "下載管理器" } } } }, - "Download Manager" : { - "extractionState" : "manual", + "Endpoint" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إدارة التنزيلات" + "value" : "نقطة النهاية" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Gestor de descàrregues" + "value" : "Punt final" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Správce stahování" + "value" : "Koncový bod" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Overførselsstyring" + "value" : "Slutpunkt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Download-Manager" + "value" : "Endpunkt" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Διαχειριστής Λήψεων" + "value" : "Σημείο Τερματισμού" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Download Manager" + "value" : "Endpoint" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Download Manager" + "value" : "Endpoint" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Download Manager" + "value" : "Endpoint" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Gestor de Descargas" + "value" : "Punto de conexión" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Gestor de Descargas" + "value" : "Punto de conexión" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Latausten Hallinta" + "value" : "Loppupiste" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Gestionnaire de téléchargements" + "value" : "Point de terminaison" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Gestionnaire de téléchargement" + "value" : "Point de terminaison" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מנהל הורדות" + "value" : "נקודת קצה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "डाउनलोड मैनेजर" + "value" : "एंडपॉइंट" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Upravitelj preuzimanja" + "value" : "Krajnja točka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Letöltéskezelő" + "value" : "Végpont" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Pengelola Unduhan" + "value" : "Endpoint" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Gestore dei download" + "value" : "Endpoint" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ダウンロードマネージャー" + "value" : "エンドポイント" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "다운로드 관리자" + "value" : "엔드포인트" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Pengurus Muat Turun" + "value" : "Titik akhir" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Nedlastingsbehandling" + "value" : "Endepunkt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Downloadbeheerder" + "value" : "Eindpunt" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Menedżer pobierania" + "value" : "Punkt końcowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Gerenciador de Downloads" + "value" : "Endpoint" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Gestor de Transferências" + "value" : "Endpoint" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Manager de descărcări" + "value" : "Punct final" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Менеджер загрузок" + "value" : "Конечная точка" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Manažér sťahovania" + "value" : "Koncový bod" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Nedladdningshanterare" + "value" : "Slutpunkt" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ตัวจัดการดาวน์โหลด" + "value" : "เอนด์พอยต์" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "İndirme Yöneticisi" + "value" : "Uç Nokta" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Менеджер завантажень" + "value" : "Кінцева точка" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trình quản lý Tải xuống" + "value" : "Điểm cuối" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "下載管理器" + "value" : "节点" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "下载管理" + "value" : "端點" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "下載管理器" + "value" : "端點" } } } }, - "Endpoint" : { + "Enter Full Screen" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نقطة النهاية" + "value" : "دخول وضع ملء الشاشة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Punt final" + "value" : "Pantalla completa" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Koncový bod" + "value" : "Zobrazit na celou obrazovku" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Slutpunkt" + "value" : "Fuld skærm" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Endpunkt" + "value" : "Vollbildmodus aktivieren" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Σημείο Τερματισμού" + "value" : "Πλήρης Οθόνη" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint" + "value" : "Enter Full Screen" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint" + "value" : "Enter Full Screen" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint" + "value" : "Enter Full Screen" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Punto de conexión" + "value" : "Entrar en pantalla completa" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Punto de conexión" + "value" : "Pantalla completa" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Loppupiste" + "value" : "Siirry koko näytön tilaan" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Point de terminaison" + "value" : "Passer en plein écran" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Point de terminaison" + "value" : "Mode Plein Écran" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "נקודת קצה" + "value" : "כניסה למסך מלא" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "एंडपॉइंट" + "value" : "पूर्ण स्क्रीन दर्ज करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Krajnja točka" + "value" : "Prijeđi na cijeli zaslon" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Végpont" + "value" : "Teljes képernyőre lépés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint" + "value" : "Masuk Layar Penuh" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint" + "value" : "Entra in Full Screen" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "エンドポイント" + "value" : "フルスクリーンにする" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "엔드포인트" + "value" : "전체 화면으로 전환" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Titik akhir" + "value" : "Masuk Skrin Penuh" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Endepunkt" + "value" : "Gå til fullskjerm" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Eindpunt" + "value" : "Volledig scherm openen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Punkt końcowy" + "value" : "Pełny ekran" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint" + "value" : "Tela Cheia" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint" + "value" : "Entrar em Ecrã Completo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Punct final" + "value" : "Intră în Ecran Complet" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Конечная точка" + "value" : "Перейти в полноэкранный режим" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Koncový bod" + "value" : "Režim celej obrazovky" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Slutpunkt" + "value" : "Ange fullskärm" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เอนด์พอยต์" + "value" : "เข้าสู่โหมดเต็มจอ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uç Nokta" + "value" : "Tam Ekrana Geç" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Кінцева точка" + "value" : "На весь екран" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Điểm cuối" + "value" : "Toàn Màn Hình" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "端點" + "value" : "进入全屏模式" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "节点" + "value" : "進入全螢幕模式" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "端點" + "value" : "全螢幕模式" } } } }, - "Enter Full Screen" : { + "Enter your Hugging Face token" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "دخول وضع ملء الشاشة" + "value" : "أدخل رمز Hugging Face الخاص بك" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Pantalla completa" + "value" : "Introdueix el teu token de Hugging Face" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zobrazit na celou obrazovku" + "value" : "Zadejte váš token Hugging Face" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Fuld skærm" + "value" : "Indtast din Hugging Face-token" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Vollbildmodus aktivieren" + "value" : "Gib dein Hugging Face-Token ein" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Πλήρης Οθόνη" + "value" : "Εισαγάγετε το Hugging Face token σας" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Enter Full Screen" + "value" : "Enter your Hugging Face token" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Enter Full Screen" + "value" : "Enter your Hugging Face token" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Enter Full Screen" + "value" : "Enter your Hugging Face token" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Entrar en pantalla completa" + "value" : "Introduce tu token de Hugging Face" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Pantalla completa" + "value" : "Introduce tu token de Hugging Face" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Siirry koko näytön tilaan" + "value" : "Anna Hugging Face -tunnuksesi" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Passer en plein écran" + "value" : "Entrez votre jeton Hugging Face" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Mode Plein Écran" + "value" : "Entrez votre jeton Hugging Face" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "כניסה למסך מלא" + "value" : "הזן את הטוקן שלך ל-Hugging Face" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "पूर्ण स्क्रीन दर्ज करें" + "value" : "अपना Hugging Face टोकन दर्ज करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Prijeđi na cijeli zaslon" + "value" : "Unesite svoj Hugging Face token" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Teljes képernyőre lépés" + "value" : "Adja meg a Hugging Face tokenjét" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Masuk Layar Penuh" + "value" : "Masukkan token Hugging Face Anda" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Entra in Full Screen" + "value" : "Inserisci il tuo token Hugging Face" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "フルスクリーンにする" + "value" : "Hugging Faceトークンを入力" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "전체 화면으로 전환" + "value" : "Hugging Face 토큰을 입력하세요" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Masuk Skrin Penuh" + "value" : "Masukkan token Hugging Face anda" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Gå til fullskjerm" + "value" : "Skriv inn Hugging Face-tokenet ditt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Volledig scherm openen" + "value" : "Voer je Hugging Face-token in" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Pełny ekran" + "value" : "Wprowadź swój token Hugging Face" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Tela Cheia" + "value" : "Insira seu token do Hugging Face" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Entrar em Ecrã Completo" + "value" : "Introduza o seu token do Hugging Face" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Intră în Ecran Complet" + "value" : "Introduceți tokenul dvs. Hugging Face" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Перейти в полноэкранный режим" + "value" : "Введите ваш токен Hugging Face" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Režim celej obrazovky" + "value" : "Zadajte svoj Hugging Face token" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ange fullskärm" + "value" : "Ange din Hugging Face-token" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เข้าสู่โหมดเต็มจอ" + "value" : "ป้อนโทเค็น Hugging Face ของคุณ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tam Ekrana Geç" + "value" : "Hugging Face tokenınızı girin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "На весь екран" + "value" : "Введіть ваш токен Hugging Face" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Toàn Màn Hình" + "value" : "Nhập mã thông báo Hugging Face của bạn" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "全螢幕模式" + "value" : "输入您的 Hugging Face 令牌" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "进入全屏模式" + "value" : "輸入您的 Hugging Face 令牌" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "進入全螢幕模式" + "value" : "輸入你的 Hugging Face 令牌" } } } }, - "Enter your Hugging Face token" : { + "Enter your OpenAI API Key" : { + + }, + "Enter your OpenAI API Proxy Address" : { + + }, + "Exit Full Screen" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أدخل رمز Hugging Face الخاص بك" + "value" : "إنهاء ملء الشاشة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Introdueix el teu token de Hugging Face" + "value" : "Sortir de la pantalla completa" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zadejte váš token Hugging Face" + "value" : "Ukončit režim celé obrazovky" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Indtast din Hugging Face-token" + "value" : "Afslut fuld skærm" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Gib dein Hugging Face-Token ein" + "value" : "Vollbildmodus beenden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Εισαγάγετε το Hugging Face token σας" + "value" : "Έξοδος από την Πλήρη Οθόνη" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Enter your Hugging Face token" + "value" : "Exit Full Screen" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Enter your Hugging Face token" + "value" : "Exit Full Screen" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Enter your Hugging Face token" + "value" : "Exit Full Screen" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Introduce tu token de Hugging Face" + "value" : "Salir de pantalla completa" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Introduce tu token de Hugging Face" + "value" : "Salir de pantalla completa" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Anna Hugging Face -tunnuksesi" + "value" : "Poistu koko näytöstä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Entrez votre jeton Hugging Face" + "value" : "Quitter le mode plein écran" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Entrez votre jeton Hugging Face" + "value" : "Quitter le plein écran" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "הזן את הטוקן שלך ל-Hugging Face" + "value" : "יציאה ממסך מלא" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अपना Hugging Face टोकन दर्ज करें" + "value" : "पूर्ण स्क्रीन से बाहर निकलें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Unesite svoj Hugging Face token" + "value" : "Izađi iz punog zaslona" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Adja meg a Hugging Face tokenjét" + "value" : "Kilépés a teljes képernyőről" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Masukkan token Hugging Face Anda" + "value" : "Keluar Layar Penuh" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Inserisci il tuo token Hugging Face" + "value" : "Esci da schermo intero" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Faceトークンを入力" + "value" : "フルスクリーンを終了" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 토큰을 입력하세요" + "value" : "전체 화면 종료" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Masukkan token Hugging Face anda" + "value" : "Keluar Skrin Penuh" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Skriv inn Hugging Face-tokenet ditt" + "value" : "Avslutt fullskjerm" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voer je Hugging Face-token in" + "value" : "Verlaat Volledig Scherm" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wprowadź swój token Hugging Face" + "value" : "Zakończ Pełny Ekran" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Insira seu token do Hugging Face" + "value" : "Sair da Tela Cheia" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Introduza o seu token do Hugging Face" + "value" : "Sair do Ecrã Completo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Introduceți tokenul dvs. Hugging Face" + "value" : "Ieşire Din Ecran Complet" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Введите ваш токен Hugging Face" + "value" : "Выйти из полноэкранного режима" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Zadajte svoj Hugging Face token" + "value" : "Ukončiť celú obrazovku" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ange din Hugging Face-token" + "value" : "Avsluta helskärm" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ป้อนโทเค็น Hugging Face ของคุณ" + "value" : "ออกจากเต็มจอ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face tokenınızı girin" + "value" : "Tam Ekrandan Çık" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Введіть ваш токен Hugging Face" + "value" : "Вийти з повноекранного режиму" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Nhập mã thông báo Hugging Face của bạn" + "value" : "Thoát Chế Độ Toàn Màn Hình" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "輸入你的 Hugging Face 令牌" + "value" : "退出全屏" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "输入您的 Hugging Face 令牌" + "value" : "退出全螢幕" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "輸入您的 Hugging Face 令牌" + "value" : "退出全屏幕" } } } }, - "Exit Full Screen" : { + "Experimental features may have performance limitations. Features and programmatic interfaces are subject to change at any time." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إنهاء ملء الشاشة" + "value" : "قد تكون الميزات التجريبية محدودة الأداء. الميزات والواجهات البرمجية قابلة للتغيير في أي وقت." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Sortir de la pantalla completa" + "value" : "Les funcions experimentals poden tenir limitacions de rendiment. Les funcions i interfícies programàtiques poden canviar en qualsevol moment." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Ukončit režim celé obrazovky" + "value" : "Experimentální funkce mohou mít omezení výkonu. Funkce a programové rozhraní mohou být kdykoli změněny." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Afslut fuld skærm" + "value" : "Eksperimentelle funktioner kan have ydeevnebegrænsninger. Funktioner og programmeringsgrænseflader kan ændres når som helst." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Vollbildmodus beenden" + "value" : "Experimentelle Funktionen können Leistungseinschränkungen haben. Funktionen und programmgesteuerte Schnittstellen können jederzeit geändert werden." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Έξοδος από την Πλήρη Οθόνη" + "value" : "Πειραματικές δυνατότητες μπορεί να έχουν περιορισμούς απόδοσης. Οι δυνατότητες και οι προγραμματικές διεπαφές ενδέχεται να αλλάξουν ανά πάσα στιγμή." } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Exit Full Screen" + "value" : "Experimental features may have performance limitations. Features and programmatic interfaces are subject to change at any time." } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Exit Full Screen" + "value" : "Experimental features may have performance limitations. Features and programmatic interfaces are subject to change at any time" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Exit Full Screen" + "value" : "Experimental features may have performance limitations. Features and programmatic interfaces are subject to change at any time." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Salir de pantalla completa" + "value" : "Las funciones experimentales pueden tener limitaciones de rendimiento. Las funciones e interfaces programáticas están sujetas a cambios en cualquier momento." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Salir de pantalla completa" + "value" : "Las funciones experimentales pueden tener limitaciones de rendimiento. Las características e interfaces programáticas están sujetas a cambios en cualquier momento." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Poistu koko näytöstä" + "value" : "Kokeelliset ominaisuudet voivat sisältää suorituskykyrajoituksia. Ominaisuudet ja ohjelmointirajapinnat voivat muuttua milloin tahansa." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Quitter le mode plein écran" + "value" : "Les fonctionnalités expérimentales peuvent avoir des limitations de performance. Les fonctionnalités et les interfaces programmatiques peuvent être modifiées à tout moment." } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Quitter le plein écran" + "value" : "Les fonctionnalités expérimentales peuvent avoir des limitations de performance. Les fonctionnalités et les interfaces programmatiques peuvent changer à tout moment." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יציאה ממסך מלא" + "value" : "ייתכן שלמאפיינים ניסיוניים יהיו מגבלות ביצועים. מאפיינים ממשקיים ותכנותיים נתונים לשינוי בכל עת." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "पूर्ण स्क्रीन से बाहर निकलें" + "value" : "प्रयोगात्मक विशेषताएं प्रदर्शन सीमाएँ हो सकती हैं। विशेषताएं और कार्यक्रमात्मक इंटरफेस कभी भी परिवर्तन के अधीन हो सकते हैं।" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Izađi iz punog zaslona" + "value" : "Eksperimentalne značajke mogu imati ograničenja u performansama. Značajke i programska sučelja podložni su promjenama u bilo kojem trenutku." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kilépés a teljes képernyőről" + "value" : "Kísérleti funkciók teljesítménybeli korlátozásokkal járhatnak. A funkciók és a programozási felületek bármikor változhatnak." } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Keluar Layar Penuh" + "value" : "Fitur eksperimental mungkin memiliki keterbatasan kinerja. Fitur dan antarmuka program dapat berubah sewaktu-waktu." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Esci da schermo intero" + "value" : "Le funzionalità sperimentali potrebbero avere limitazioni di prestazioni. Le funzionalità e le interfacce programmatiche sono soggette a modifiche in qualsiasi momento." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "フルスクリーンを終了" + "value" : "実験的な機能にはパフォーマンスの制限がある場合があります。機能やプログラムインターフェースはいつでも変更される可能性があります。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "전체 화면 종료" + "value" : "실험적인 기능은 성능 제한이 있을 수 있습니다. 기능 및 프로그래밍 인터페이스는 언제든지 변경될 수 있습니다." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Keluar Skrin Penuh" + "value" : "Ciri eksperimen mungkin mempunyai had prestasi. Ciri dan antara muka berasaskan program boleh berubah pada bila-bila masa." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Avslutt fullskjerm" + "value" : "Eksperimentelle funksjoner kan ha ytelsesbegrensninger. Funksjoner og programmeringsgrensesnitt kan endres når som helst." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verlaat Volledig Scherm" + "value" : "Experimentele functies kunnen prestatiebeperkingen hebben. Functies en programmatische interfaces kunnen op elk moment worden gewijzigd." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Zakończ Pełny Ekran" + "value" : "Funkcje eksperymentalne mogą mieć ograniczenia wydajności. Funkcje i interfejsy programistyczne mogą ulec zmianie w dowolnym momencie." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Sair da Tela Cheia" + "value" : "Recursos experimentais podem ter limitações de desempenho. Recursos e interfaces programáticas estão sujeitos a alterações a qualquer momento." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Sair do Ecrã Completo" + "value" : "Funcionalidades experimentais podem ter limitações de desempenho. Funcionalidades e interfaces programáticas estão sujeitas a alterações a qualquer momento." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ieşire Din Ecran Complet" + "value" : "Funcționalitățile experimentale pot avea limitări de performanță. Funcționalitățile și interfețele programatice pot fi modificate oricând." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Выйти из полноэкранного режима" + "value" : "Экспериментальные функции могут иметь ограничения производительности. Функции и программные интерфейсы могут изменяться в любое время." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Ukončiť celú obrazovku" + "value" : "Experimentálne funkcie môžu mať obmedzenia výkonu. Funkcie a programovateľné rozhrania sa môžu kedykoľvek zmeniť." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Avsluta helskärm" + "value" : "Experimentella funktioner kan ha prestandabegränsningar. Funktioner och programmatiska gränssnitt kan ändras när som helst." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ออกจากเต็มจอ" + "value" : "คุณลักษณะทดลองอาจมีข้อจำกัดด้านประสิทธิภาพ คุณลักษณะและอินเทอร์เฟซโปรแกรมอาจเปลี่ยนแปลงได้ตลอดเวลา" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tam Ekrandan Çık" + "value" : "Deneysel özellikler performans sınırlamalarına sahip olabilir. Özellikler ve programatik arabirimler herhangi bir zamanda değiştirilebilir." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Вийти з повноекранного режиму" + "value" : "Експериментальні функції можуть мати обмеження продуктивності. Функції та програмні інтерфейси можуть змінюватися в будь-який час." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Thoát Chế Độ Toàn Màn Hình" + "value" : "Các tính năng thử nghiệm có thể có những hạn chế về hiệu suất. Các tính năng và giao diện lập trình có thể thay đổi bất kỳ lúc nào." } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "退出全屏幕" + "value" : "实验性功能可能有性能限制。功能和编程接口随时可能更改。" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "退出全屏" + "value" : "實驗功能可能有性能限制。功能和程式介面可能隨時更改。" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "退出全螢幕" + "value" : "實驗功能可能有性能限制。功能和編程接口可能會隨時更改。" } } } }, - "Experimental Features" : { + "Feedback" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الميزات التجريبية" + "value" : "التعليقات" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Funcions experimentals" + "value" : "Comentari" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentální funkce" + "value" : "Zpětná vazba" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Eksperimentelle funktioner" + "value" : "Feedback" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentelle Funktionen" + "value" : "Feedback" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Πειραματικές Δυνατότητες" + "value" : "Σχόλια" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Experimental Features" + "value" : "Feedback" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Experimental Features" + "value" : "Feedback" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Experimental Features" + "value" : "Feedback" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Funciones experimentales" + "value" : "Comentarios" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Funciones experimentales" + "value" : "Comentarios" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kokeelliset ominaisuudet" + "value" : "Palaute" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Fonctionnalités expérimentales" + "value" : "Retour d’information" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Fonctionnalités expérimentales" + "value" : "Commentaires" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "פיצ'רים ניסיוניים" + "value" : "משוב" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "प्रायोगिक फीचर्स" + "value" : "प्रतिक्रिया" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Eksperimentalne značajke" + "value" : "Povratne informacije" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kísérleti funkciók" + "value" : "Visszajelzés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Fitur Eksperimental" + "value" : "Masukan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Funzionalità Sperimentali" + "value" : "Feedback" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "実験的な機能" + "value" : "フィードバック" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "실험적 기능" + "value" : "피드백" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Ciri Eksperimen" + "value" : "Maklum Balas" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Eksperimentelle funksjoner" + "value" : "Tilbakemelding" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentele functies" + "value" : "Feedback" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcje eksperymentalne" + "value" : "Opinie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Recursos experimentais" + "value" : "Feedback" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Funcionalidades Experimentais" + "value" : "Feedback" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Funcții Experimentale" + "value" : "Feedback" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Экспериментальные функции" + "value" : "Отзыв" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentálne funkcie" + "value" : "Spätná väzba" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentella funktioner" + "value" : "Feedback" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "คุณสมบัติการทดลอง" + "value" : "คำติชม" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Deneysel Özellikler" + "value" : "Geri Bildirim" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Експериментальні функції" + "value" : "Відгук" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tính Năng Thử Nghiệm" + "value" : "Phản hồi" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "實驗性功能" + "value" : "反馈" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "实验性功能" + "value" : "意見回饋" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "實驗性功能" + "value" : "意見反饋" } } } }, - "Experimental features may have performance limitations. Features and programmatic interfaces are subject to change at any time." : { + "General" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "قد تكون الميزات التجريبية محدودة الأداء. الميزات والواجهات البرمجية قابلة للتغيير في أي وقت." + "value" : "عام" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Les funcions experimentals poden tenir limitacions de rendiment. Les funcions i interfícies programàtiques poden canviar en qualsevol moment." + "value" : "General" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentální funkce mohou mít omezení výkonu. Funkce a programové rozhraní mohou být kdykoli změněny." + "value" : "Obecné" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Eksperimentelle funktioner kan have ydeevnebegrænsninger. Funktioner og programmeringsgrænseflader kan ændres når som helst." + "value" : "Generelt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentelle Funktionen können Leistungseinschränkungen haben. Funktionen und programmgesteuerte Schnittstellen können jederzeit geändert werden." + "value" : "Allgemein" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Πειραματικές δυνατότητες μπορεί να έχουν περιορισμούς απόδοσης. Οι δυνατότητες και οι προγραμματικές διεπαφές ενδέχεται να αλλάξουν ανά πάσα στιγμή." + "value" : "Γενικά" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Experimental features may have performance limitations. Features and programmatic interfaces are subject to change at any time." + "value" : "General" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Experimental features may have performance limitations. Features and programmatic interfaces are subject to change at any time" + "value" : "General" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Experimental features may have performance limitations. Features and programmatic interfaces are subject to change at any time." + "value" : "General" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Las funciones experimentales pueden tener limitaciones de rendimiento. Las funciones e interfaces programáticas están sujetas a cambios en cualquier momento." + "value" : "General" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Las funciones experimentales pueden tener limitaciones de rendimiento. Las características e interfaces programáticas están sujetas a cambios en cualquier momento." + "value" : "General" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kokeelliset ominaisuudet voivat sisältää suorituskykyrajoituksia. Ominaisuudet ja ohjelmointirajapinnat voivat muuttua milloin tahansa." + "value" : "Yleiset" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Les fonctionnalités expérimentales peuvent avoir des limitations de performance. Les fonctionnalités et les interfaces programmatiques peuvent être modifiées à tout moment." + "value" : "Général" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Les fonctionnalités expérimentales peuvent avoir des limitations de performance. Les fonctionnalités et les interfaces programmatiques peuvent changer à tout moment." + "value" : "Général" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "ייתכן שלמאפיינים ניסיוניים יהיו מגבלות ביצועים. מאפיינים ממשקיים ותכנותיים נתונים לשינוי בכל עת." + "value" : "כללי" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "प्रयोगात्मक विशेषताएं प्रदर्शन सीमाएँ हो सकती हैं। विशेषताएं और कार्यक्रमात्मक इंटरफेस कभी भी परिवर्तन के अधीन हो सकते हैं।" + "value" : "सामान्य" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Eksperimentalne značajke mogu imati ograničenja u performansama. Značajke i programska sučelja podložni su promjenama u bilo kojem trenutku." + "value" : "Općenito" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Kísérleti funkciók teljesítménybeli korlátozásokkal járhatnak. A funkciók és a programozási felületek bármikor változhatnak." + "value" : "Általános" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Fitur eksperimental mungkin memiliki keterbatasan kinerja. Fitur dan antarmuka program dapat berubah sewaktu-waktu." + "value" : "Umum" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Le funzionalità sperimentali potrebbero avere limitazioni di prestazioni. Le funzionalità e le interfacce programmatiche sono soggette a modifiche in qualsiasi momento." + "value" : "Generali" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "実験的な機能にはパフォーマンスの制限がある場合があります。機能やプログラムインターフェースはいつでも変更される可能性があります。" + "value" : "一般" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "실험적인 기능은 성능 제한이 있을 수 있습니다. 기능 및 프로그래밍 인터페이스는 언제든지 변경될 수 있습니다." + "value" : "일반" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Ciri eksperimen mungkin mempunyai had prestasi. Ciri dan antara muka berasaskan program boleh berubah pada bila-bila masa." + "value" : "Umum" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Eksperimentelle funksjoner kan ha ytelsesbegrensninger. Funksjoner og programmeringsgrensesnitt kan endres når som helst." + "value" : "Generelt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentele functies kunnen prestatiebeperkingen hebben. Functies en programmatische interfaces kunnen op elk moment worden gewijzigd." + "value" : "Algemeen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Funkcje eksperymentalne mogą mieć ograniczenia wydajności. Funkcje i interfejsy programistyczne mogą ulec zmianie w dowolnym momencie." + "value" : "Ogólne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Recursos experimentais podem ter limitações de desempenho. Recursos e interfaces programáticas estão sujeitos a alterações a qualquer momento." + "value" : "Geral" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Funcionalidades experimentais podem ter limitações de desempenho. Funcionalidades e interfaces programáticas estão sujeitas a alterações a qualquer momento." + "value" : "Geral" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Funcționalitățile experimentale pot avea limitări de performanță. Funcționalitățile și interfețele programatice pot fi modificate oricând." + "value" : "General" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Экспериментальные функции могут иметь ограничения производительности. Функции и программные интерфейсы могут изменяться в любое время." + "value" : "Основные настройки" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentálne funkcie môžu mať obmedzenia výkonu. Funkcie a programovateľné rozhrania sa môžu kedykoľvek zmeniť." + "value" : "Všeobecné" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Experimentella funktioner kan ha prestandabegränsningar. Funktioner och programmatiska gränssnitt kan ändras när som helst." + "value" : "Allmänt" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "คุณลักษณะทดลองอาจมีข้อจำกัดด้านประสิทธิภาพ คุณลักษณะและอินเทอร์เฟซโปรแกรมอาจเปลี่ยนแปลงได้ตลอดเวลา" + "value" : "ทั่วไป" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Deneysel özellikler performans sınırlamalarına sahip olabilir. Özellikler ve programatik arabirimler herhangi bir zamanda değiştirilebilir." + "value" : "Genel" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Експериментальні функції можуть мати обмеження продуктивності. Функції та програмні інтерфейси можуть змінюватися в будь-який час." + "value" : "Основні" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Các tính năng thử nghiệm có thể có những hạn chế về hiệu suất. Các tính năng và giao diện lập trình có thể thay đổi bất kỳ lúc nào." + "value" : "Chung" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "實驗功能可能有性能限制。功能和編程接口可能會隨時更改。" + "value" : "通用" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "实验性功能可能有性能限制。功能和编程接口随时可能更改。" + "value" : "概述" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "實驗功能可能有性能限制。功能和程式介面可能隨時更改。" + "value" : "一般" } } } }, - "Feedback" : { + "Generate Time" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التعليقات" + "value" : "توليد الوقت" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Comentari" + "value" : "Genera hora" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zpětná vazba" + "value" : "Vygenerovat čas" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Generer tid" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Zeit generieren" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Σχόλια" + "value" : "Δημιουργία Χρόνου" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Generate Time" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Generate Time" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Generate Time" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Comentarios" + "value" : "Generar hora" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Comentarios" + "value" : "Generar hora" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Palaute" + "value" : "Luo aika" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Retour d’information" + "value" : "Générer l'heure" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Commentaires" + "value" : "Générer l'heure" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "משוב" + "value" : "צור זמן" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "प्रतिक्रिया" + "value" : "समय उत्पन्न करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Povratne informacije" + "value" : "Generiraj vrijeme" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Visszajelzés" + "value" : "Idő létrehozása" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Masukan" + "value" : "Hasilkan Waktu" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Genera ora" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "フィードバック" + "value" : "時間を生成" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "피드백" + "value" : "시간 생성" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Maklum Balas" + "value" : "Jana Masa" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Tilbakemelding" + "value" : "Generer tid" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Tijd genereren" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Opinie" + "value" : "Generuj czas" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Gerar Horário" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Gerar Tempo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Generează timp" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Отзыв" + "value" : "Сгенерировать время" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Spätná väzba" + "value" : "Generovať čas" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Feedback" + "value" : "Generera tid" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "คำติชม" + "value" : "สร้างเวลา" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Geri Bildirim" + "value" : "Süre Oluştur" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Відгук" + "value" : "Згенерувати час" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Phản hồi" + "value" : "Tạo thời gian" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "意見反饋" + "value" : "生成时间" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "反馈" + "value" : "生成時間" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "意見回饋" + "value" : "生成時間" } } } }, - "GPU Cache Limit" : { + "Generate Tokens/second" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حد ذاكرة التخزين المؤقت لوحدة معالجة الرسوميات" + "value" : "توليد الرموز/الثانية" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Límit de memòria cau de la GPU" + "value" : "Genera tokens/segon" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Limit pro mezipaměť GPU" + "value" : "Generovat tokeny/sekundu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Begrænsning for GPU-cache" + "value" : "Generer Tokens/sekund" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "GPU-Cache-Grenze" + "value" : "Token pro Sekunde generieren" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Όριο προσωρινής μνήμης GPU" + "value" : "Δημιουργία διακριτικών/δευτερόλεπτο" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "GPU Cache Limit" + "value" : "Generate Tokens/second" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "GPU Cache Limit" + "value" : "Generate Tokens/second" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "GPU Cache Limit" + "value" : "Generate Tokens per second" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Límite de caché de GPU" + "value" : "Generar tokens/segundo" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Límite de Caché de GPU" + "value" : "Generar tokens/segundo" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "GPU-välimuistin raja" + "value" : "Luo merkkejä/sekunti" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Limite de cache GPU" + "value" : "Générer des jetons/seconde" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Limite de cache GPU" + "value" : "Générer des jetons/seconde" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מגבלת המטמון של ה-GPU" + "value" : "יצירת סמלים/שנייה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "GPU कैश सीमा" + "value" : "प्रती/सेकंड टोकन जनरेट करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Ograničenje GPU predmemorije" + "value" : "Generiraj toka/sekundi" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "GPU gyorsítótár korlátja" + "value" : "Tokenek létrehozása/másodperc" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Batas Cache GPU" + "value" : "Hasilkan Token/detik" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Limite cache GPU" + "value" : "Genera token/secondo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "GPUキャッシュ制限" + "value" : "トークン/秒を生成" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "GPU 캐시 제한" + "value" : "초당 토큰 생성" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Had Cache GPU" + "value" : "Jana Token/saat" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "GPU-hurtigbuffergrense" + "value" : "Generer token/sekund" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "GPU-cachelimiet" + "value" : "Genereer Tokens/seconde" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Limit bufora GPU" + "value" : "Generuj Tokeny/sekundę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Limite de Cache da GPU" + "value" : "Gerar Tokens/segundo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Limite da Cache da GPU" + "value" : "Gerar Tokens/segundo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Limită cache GPU" + "value" : "Generează jetoane/secundă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Ограничение кэша GPU" + "value" : "Создать токены/секунда" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Limit vyrovnávacej pamäte GPU" + "value" : "Generovať tokeny/sekundu" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Gräns för GPU-cache" + "value" : "Generera tokens/sekund" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ขีดจำกัดแคช GPU" + "value" : "สร้างโทเคน/วินาที" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "GPU Önbellek Limiti" + "value" : "Saniye Başı Jeton Üret" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Граничення кешу GPU" + "value" : "Генерувати токени/секунда" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Giới hạn bộ nhớ đệm GPU" + "value" : "Tạo mã mỗi giây" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "GPU 緩存限制" + "value" : "生成令牌/秒" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "GPU缓存限制" + "value" : "每秒生成 Token" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "GPU 快取上限" + "value" : "每秒產生代幣" } } } }, - "General" : { - "extractionState" : "manual", + "GitHub" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "عام" + "value" : "GitHub" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "General" + "value" : "GitHub" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Obecné" + "value" : "GitHub" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Generelt" + "value" : "GitHub" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Allgemein" + "value" : "GitHub" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Γενικά" + "value" : "GitHub" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "General" + "value" : "GitHub" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "General" + "value" : "GitHub" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "General" + "value" : "GitHub" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "General" + "value" : "GitHub" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "General" + "value" : "GitHub" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Yleiset" + "value" : "GitHub" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Général" + "value" : "GitHub" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Général" + "value" : "GitHub" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "כללי" + "value" : "GitHub" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सामान्य" + "value" : "GitHub" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Općenito" + "value" : "GitHub" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Általános" + "value" : "GitHub" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Umum" + "value" : "GitHub" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Generali" + "value" : "GitHub" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "一般" + "value" : "GitHub" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "일반" + "value" : "GitHub" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Umum" + "value" : "GitHub" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Generelt" + "value" : "GitHub" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Algemeen" + "value" : "GitHub" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ogólne" + "value" : "GitHub" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Geral" + "value" : "GitHub" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Geral" + "value" : "GitHub" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "General" + "value" : "GitHub" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Основные настройки" + "value" : "GitHub" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Všeobecné" + "value" : "GitHub" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Allmänt" + "value" : "GitHub" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ทั่วไป" + "value" : "GitHub" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Genel" + "value" : "GitHub" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Основні" + "value" : "GitHub" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chung" + "value" : "GitHub" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "一般" + "value" : "GitHub" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "通用" + "value" : "GitHub" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "概述" + "value" : "GitHub" } } } }, - "Generate Time" : { + "GPU Cache Limit" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "توليد الوقت" + "value" : "حد ذاكرة التخزين المؤقت لوحدة معالجة الرسوميات" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Genera hora" + "value" : "Límit de memòria cau de la GPU" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vygenerovat čas" + "value" : "Limit pro mezipaměť GPU" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Generer tid" + "value" : "Begrænsning for GPU-cache" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Zeit generieren" + "value" : "GPU-Cache-Grenze" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Δημιουργία Χρόνου" + "value" : "Όριο προσωρινής μνήμης GPU" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Generate Time" + "value" : "GPU Cache Limit" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Generate Time" + "value" : "GPU Cache Limit" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Generate Time" + "value" : "GPU Cache Limit" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Generar hora" + "value" : "Límite de caché de GPU" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Generar hora" + "value" : "Límite de Caché de GPU" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Luo aika" + "value" : "GPU-välimuistin raja" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Générer l'heure" + "value" : "Limite de cache GPU" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Générer l'heure" + "value" : "Limite de cache GPU" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "צור זמן" + "value" : "מגבלת המטמון של ה-GPU" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "समय उत्पन्न करें" + "value" : "GPU कैश सीमा" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Generiraj vrijeme" + "value" : "Ograničenje GPU predmemorije" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Idő létrehozása" + "value" : "GPU gyorsítótár korlátja" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hasilkan Waktu" + "value" : "Batas Cache GPU" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Genera ora" + "value" : "Limite cache GPU" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "時間を生成" + "value" : "GPUキャッシュ制限" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "시간 생성" + "value" : "GPU 캐시 제한" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Jana Masa" + "value" : "Had Cache GPU" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Generer tid" + "value" : "GPU-hurtigbuffergrense" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Tijd genereren" + "value" : "GPU-cachelimiet" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Generuj czas" + "value" : "Limit bufora GPU" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Gerar Horário" + "value" : "Limite de Cache da GPU" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Gerar Tempo" + "value" : "Limite da Cache da GPU" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Generează timp" + "value" : "Limită cache GPU" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сгенерировать время" + "value" : "Ограничение кэша GPU" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Generovať čas" + "value" : "Limit vyrovnávacej pamäte GPU" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Generera tid" + "value" : "Gräns för GPU-cache" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "สร้างเวลา" + "value" : "ขีดจำกัดแคช GPU" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Süre Oluştur" + "value" : "GPU Önbellek Limiti" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Згенерувати час" + "value" : "Граничення кешу GPU" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tạo thời gian" + "value" : "Giới hạn bộ nhớ đệm GPU" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "生成時間" + "value" : "GPU缓存限制" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "生成时间" + "value" : "GPU 快取上限" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "生成時間" + "value" : "GPU 緩存限制" } } } }, - "Generate Tokens/second" : { + "GPU Memory Limit" : { + + }, + "https://hf-mirror.com" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "توليد الرموز/الثانية" + "value" : "https://hf-mirror.com" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Genera tokens/segon" + "value" : "https://hf-mirror.com" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Generovat tokeny/sekundu" + "value" : "https://hf-mirror.com" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Generer Tokens/sekund" + "value" : "https://hf-mirror.com" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Token pro Sekunde generieren" + "value" : "https://hf-mirror.com" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Δημιουργία διακριτικών/δευτερόλεπτο" + "value" : "https://hf-mirror.com" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Generate Tokens/second" + "value" : "https://hf-mirror.com" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Generate Tokens/second" + "value" : "https://hf-mirror.com" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Generate Tokens per second" + "value" : "https://hf-mirror.com" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Generar tokens/segundo" + "value" : "https://hf-mirror.com" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Generar tokens/segundo" + "value" : "https://hf-mirror.com" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Luo merkkejä/sekunti" + "value" : "https://hf-mirror.com" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Générer des jetons/seconde" + "value" : "https://hf-mirror.com" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Générer des jetons/seconde" + "value" : "https://hf-mirror.com" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "יצירת סמלים/שנייה" + "value" : "https://hf-mirror.com" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "प्रती/सेकंड टोकन जनरेट करें" + "value" : "https://hf-mirror.com" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Generiraj toka/sekundi" + "value" : "https://hf-mirror.com" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Tokenek létrehozása/másodperc" + "value" : "https://hf-mirror.com" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hasilkan Token/detik" + "value" : "https://hf-mirror.com" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Genera token/secondo" + "value" : "https://hf-mirror.com" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "トークン/秒を生成" + "value" : "https://hf-mirror.com" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "초당 토큰 생성" + "value" : "https://hf-mirror.com" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Jana Token/saat" + "value" : "https://hf-mirror.com" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Generer token/sekund" + "value" : "https://hf-mirror.com" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Genereer Tokens/seconde" + "value" : "https://hf-mirror.com" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Generuj Tokeny/sekundę" + "value" : "https://hf-mirror.com" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Gerar Tokens/segundo" + "value" : "https://hf-mirror.com" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Gerar Tokens/segundo" + "value" : "https://hf-mirror.com" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Generează jetoane/secundă" + "value" : "https://hf-mirror.com" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Создать токены/секунда" + "value" : "https://hf-mirror.com" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Generovať tokeny/sekundu" + "value" : "https://hf-mirror.com" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Generera tokens/sekund" + "value" : "https://hf-mirror.com" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "สร้างโทเคน/วินาที" + "value" : "https://hf-mirror.com" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Saniye Başı Jeton Üret" + "value" : "https://hf-mirror.com" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Генерувати токени/секунда" + "value" : "https://hf-mirror.com" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tạo mã mỗi giây" + "value" : "https://hf-mirror.com" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "每秒產生代幣" + "value" : "https://hf-mirror.com" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "生成令牌/秒" + "value" : "https://hf-mirror.com" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "每秒生成 Token" + "value" : "https://hf-mirror.com" } } } }, - "GitHub" : { + "https://huggingface.co" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "GitHub" + "value" : "https://huggingface.co" } } } }, - "Hugging Face" : { + "Hugging Face Endpoint" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "نقطة النهاية Hugging Face" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Punt final de Hugging Face" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face koncový bod" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face-endepunkt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face-Endpunkt" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Endpoint Hugging Face" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face Endpoint" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face Endpoint" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face Endpoint" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Punto Final de Hugging Face" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Punto de conexión de Hugging Face" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face -päätepiste" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Point de terminaison Hugging Face" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Point de terminaison Hugging Face" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "נקודת קצה של Hugging Face" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face एंडपॉइंट" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face završna točka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face végpont" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face Endpoint" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Endpoint di Hugging Face" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face エンドポイント" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "허깅 페이스 엔드포인트" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Titik Akhir Hugging Face" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face-endepunkt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face-eindpunt" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face Endpoint" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Endpoint do Hugging Face" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Ponto Final Hugging Face" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Endpoint Hugging Face" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Конечная точка Hugging Face" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face endpoint" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face-slutpunkt" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face Endpoint" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face Uç Noktası" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Кінцева точка Hugging Face" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Điểm cuối Hugging Face" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face 节点" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face 端點" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face" + "value" : "Hugging Face 端點" } } } }, - "Hugging Face Endpoint" : { + "Hugging Face Repo Id" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "نقطة النهاية Hugging Face" + "value" : "معرّف مستودع Hugging Face" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Punt final de Hugging Face" + "value" : "Id de Repositori de Hugging Face" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face koncový bod" + "value" : "ID úložiště Hugging Face" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-endepunkt" + "value" : "Hugging Face Repo-id" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-Endpunkt" + "value" : "Hugging Face Repo-ID" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint Hugging Face" + "value" : "Αναγνωριστικό Αποθετηρίου Hugging Face" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Endpoint" + "value" : "Hugging Face Repo ID" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Endpoint" + "value" : "Hugging Face Repo ID" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Endpoint" + "value" : "Hugging Face Repo Id" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Punto Final de Hugging Face" + "value" : "ID del repositorio de Hugging Face" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Punto de conexión de Hugging Face" + "value" : "ID del repo de Hugging Face" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face -päätepiste" + "value" : "Hugging Face -repojen tunnus" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Point de terminaison Hugging Face" + "value" : "ID du dépôt Hugging Face" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Point de terminaison Hugging Face" + "value" : "ID du dépôt Hugging Face" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "נקודת קצה של Hugging Face" + "value" : "Hugging Face מזהה מאגר" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face एंडपॉइंट" + "value" : "Hugging Face रेपो आईडी" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face završna točka" + "value" : "ID spremišta Hugging Face" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face végpont" + "value" : "Hugging Face tárolóazonosító" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Endpoint" + "value" : "ID Repo Hugging Face" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint di Hugging Face" + "value" : "ID Repository Hugging Face" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face エンドポイント" + "value" : "Hugging Face リポジトリID" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "허깅 페이스 엔드포인트" + "value" : "Hugging Face 저장소 ID" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Titik Akhir Hugging Face" + "value" : "ID Repo Hugging Face" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-endepunkt" + "value" : "Hugging Face Repo-ID" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-eindpunt" + "value" : "Hugging Face-repo-ID" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Endpoint" + "value" : "Identyfikator repozytorium Hugging Face" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint do Hugging Face" + "value" : "ID do Repositório Hugging Face" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Ponto Final Hugging Face" + "value" : "ID do Repositório Hugging Face" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Endpoint Hugging Face" + "value" : "ID-ul depozitului Hugging Face" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Конечная точка Hugging Face" + "value" : "Идентификатор репозитория Hugging Face" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face endpoint" + "value" : "Hugging Face Repo ID" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-slutpunkt" + "value" : "Hugging Face Repo-ID" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Endpoint" + "value" : "Hugging Face Repo Id" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Uç Noktası" + "value" : "Hugging Face Repo Kimliği" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Кінцева точка Hugging Face" + "value" : "Ідентифікатор репозиторію Hugging Face" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Điểm cuối Hugging Face" + "value" : "Hugging Face Repo Id" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 端點" + "value" : "Hugging Face 仓库 ID" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 节点" + "value" : "Hugging Face 儲存庫 ID" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 端點" + "value" : "Hugging Face Repo ID" } } } }, - "Hugging Face Repo Id" : { - "extractionState" : "manual", + "Hugging Face Token" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "معرّف مستودع Hugging Face" + "value" : "كود Hugging Face" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Id de Repositori de Hugging Face" + "value" : "Token de Hugging Face" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "ID úložiště Hugging Face" + "value" : "Token Hugging Face" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo-id" + "value" : "Hugging Face-token" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo-ID" + "value" : "Hugging-Face-Token" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αναγνωριστικό Αποθετηρίου Hugging Face" + "value" : "Διακριτικό Hugging Face" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo ID" + "value" : "Hugging Face Token" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo ID" + "value" : "Hugging Face Token" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo Id" + "value" : "Hugging Face Token" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "ID del repositorio de Hugging Face" + "value" : "Token de Hugging Face" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "ID del repo de Hugging Face" + "value" : "Token de Hugging Face" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face -repojen tunnus" + "value" : "Hugging Face -avain" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "ID du dépôt Hugging Face" + "value" : "Jeton Hugging Face" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "ID du dépôt Hugging Face" + "value" : "Jeton Hugging Face" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face מזהה מאגר" + "value" : "אסימון Hugging Face" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face रेपो आईडी" + "value" : "Hugging Face टोकन" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "ID spremišta Hugging Face" + "value" : "Hugging Face token" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face tárolóazonosító" + "value" : "Hugging Face token" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "ID Repo Hugging Face" + "value" : "Hugging Face Token" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "ID Repository Hugging Face" + "value" : "Token di Hugging Face" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face リポジトリID" + "value" : "Hugging Face トークン" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 저장소 ID" + "value" : "허깅 페이스 토큰" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "ID Repo Hugging Face" + "value" : "Token Hugging Face" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo-ID" + "value" : "Hugging Face-token" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-repo-ID" + "value" : "Hugging Face-token" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Identyfikator repozytorium Hugging Face" + "value" : "Token Hugging Face" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "ID do Repositório Hugging Face" + "value" : "Token do Hugging Face" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "ID do Repositório Hugging Face" + "value" : "Token do Hugging Face" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "ID-ul depozitului Hugging Face" + "value" : "Token Hugging Face" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Идентификатор репозитория Hugging Face" + "value" : "Токен Hugging Face" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo ID" + "value" : "Hugging Face Token" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo-ID" + "value" : "Hugging Face-token" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo Id" + "value" : "โทเค็น Hugging Face" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo Kimliği" + "value" : "Hugging Face Token" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Ідентифікатор репозиторію Hugging Face" + "value" : "Токен Hugging Face" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo Id" + "value" : "Mã Hugging Face" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Repo ID" + "value" : "Hugging Face 令牌" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 仓库 ID" + "value" : "Hugging Face Token" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 儲存庫 ID" + "value" : "Hugging Face Token" } } } }, - "Hugging Face Token" : { + "Image Text to Text (Vision)" : { + + }, + "Import" : { + + }, + "Language" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "كود Hugging Face" + "value" : "اللغة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Token de Hugging Face" + "value" : "Idioma" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Token Hugging Face" + "value" : "Jazyk" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-token" + "value" : "Sprog" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging-Face-Token" + "value" : "Sprache" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Διακριτικό Hugging Face" + "value" : "Γλώσσα" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Token" + "value" : "Language" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Token" + "value" : "Language" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Token" + "value" : "Language" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Token de Hugging Face" + "value" : "Idioma" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Token de Hugging Face" + "value" : "Idioma" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face -avain" + "value" : "Kieli" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jeton Hugging Face" + "value" : "Langue" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Jeton Hugging Face" + "value" : "Langue" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אסימון Hugging Face" + "value" : "שפה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face टोकन" + "value" : "भाषा" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face token" + "value" : "Jezik" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face token" + "value" : "Nyelv" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Token" + "value" : "Bahasa" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Token di Hugging Face" + "value" : "Lingua" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face トークン" + "value" : "言語" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "허깅 페이스 토큰" + "value" : "언어" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Token Hugging Face" + "value" : "Bahasa" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-token" + "value" : "Språk" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-token" + "value" : "Taal" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Token Hugging Face" + "value" : "Język" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Token do Hugging Face" + "value" : "Idioma" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Token do Hugging Face" + "value" : "Idioma" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Token Hugging Face" + "value" : "Limbă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Токен Hugging Face" + "value" : "Язык" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Token" + "value" : "Jazyk" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face-token" + "value" : "Språk" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โทเค็น Hugging Face" + "value" : "ภาษา" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Token" + "value" : "Dil" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Токен Hugging Face" + "value" : "Мова" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mã Hugging Face" + "value" : "Ngôn ngữ" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Token" + "value" : "语言" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 令牌" + "value" : "語言" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face Token" + "value" : "語言" } } } }, - "Language" : { + "Max Length" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اللغة" + "value" : "الحد الأقصى لطول" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Idioma" + "value" : "Longitud màxima" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Jazyk" + "value" : "Maximální délka" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Sprog" + "value" : "Maks. længde" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Sprache" + "value" : "Maximale Länge" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Γλώσσα" + "value" : "Μέγιστο μήκος" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Language" + "value" : "Max Length" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Language" + "value" : "Max Length" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Language" + "value" : "Max Length" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Idioma" + "value" : "Longitud máxima" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Idioma" + "value" : "Longitud Máxima" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kieli" + "value" : "Enimmäispituus" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Langue" + "value" : "Longueur maximale" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Langue" + "value" : "Longueur maximale" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שפה" + "value" : "אורך מרבי" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "भाषा" + "value" : "अधिकतम लंबाई" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Jezik" + "value" : "Maksimalna duljina" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Nyelv" + "value" : "Maximális hosszasság" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Bahasa" + "value" : "Panjang Maksimum" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Lingua" + "value" : "Lunghezza massima" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "言語" + "value" : "最大長" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "언어" + "value" : "최대 길이" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Bahasa" + "value" : "Panjang Maksimum" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Språk" + "value" : "Maks lengde" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Taal" + "value" : "Maximale lengte" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Język" + "value" : "Maksymalna długość" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Idioma" + "value" : "Comprimento Máximo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Idioma" + "value" : "Comprimento Máximo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Limbă" + "value" : "Lungime maximă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Язык" + "value" : "Макс. длина" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Jazyk" + "value" : "Maximálna dĺžka" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Språk" + "value" : "Maxlängd" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ภาษา" + "value" : "ความยาวสูงสุด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Dil" + "value" : "Maksimum Uzunluk" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Мова" + "value" : "Максимальна довжина" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Ngôn ngữ" + "value" : "Chiều dài tối đa" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "語言" + "value" : "最大长度" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "语言" + "value" : "最大長度" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "語言" + "value" : "最大長度" } } } }, - "MLX Community" : { - "extractionState" : "manual", + "Max Messages Limit" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مجتمع MLX" + "value" : "الحد الأقصى للرسائل" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Comunitat MLX" + "value" : "Límit màxim de missatges" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Komunita MLX" + "value" : "Maximální limit zpráv" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "MLX-fællesskab" + "value" : "Maksimumgrænse for beskeder" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "MLX Community" + "value" : "Maximale Nachrichtenanzahl" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Κοινότητα MLX" + "value" : "Μέγιστο Όριο Μηνυμάτων" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "MLX Community" + "value" : "Maximum Messages Limit" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "MLX Community" + "value" : "Maximum Messages Limit" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "MLX Community" + "value" : "Max Messages Limit" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Comunidad MLX" + "value" : "Límite Máximo de Mensajes" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Comunidad MLX" + "value" : "Límite Máximo de Mensajes" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "MLX-yhteisö" + "value" : "Viestiin enimmäisraja" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Communauté MLX" + "value" : "Limite maximale de messages" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Communauté MLX" + "value" : "Limite maximale de messages" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "קהילת MLX" + "value" : "מגבלת הודעות מרבית" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "MLX समुदाय" + "value" : "अधिकतम संदेश सीमा" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Zajednica MLX" + "value" : "Maksimalni broj poruka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "MLX Közösség" + "value" : "Üzenetek maximális száma" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Komunitas MLX" + "value" : "Batas Pesan Maksimal" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Community MLX" + "value" : "Limite massimo messaggi" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "MLXコミュニティ" + "value" : "メッセージ上限" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "MLX 커뮤니티" + "value" : "최대 메시지 제한" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Komuniti MLX" + "value" : "Had Maksimum Mesej" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "MLX-fellesskap" + "value" : "Maks grense for meldinger" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "MLX-community" + "value" : "Maximaal aantal berichten" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Społeczność MLX" + "value" : "Limit maksymalnej liczby wiadomości" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Comunidade MLX" + "value" : "Limite Máximo de Mensagens" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "MLX Comunidade" + "value" : "Limite Máximo de Mensagens" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Comunitatea MLX" + "value" : "Limită maximă mesaje" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "MLX Сообщество" + "value" : "Максимальный лимит сообщений" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Komunita MLX" + "value" : "Maximálny limit správ" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "MLX-community" + "value" : "Maximalt antal meddelanden" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ชุมชน MLX" + "value" : "จำกัดจำนวนข้อความสูงสุด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "MLX Topluluğu" + "value" : "Maksimum Mesaj Sınırı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Спільнота MLX" + "value" : "Максимальна кількість повідомлень" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cộng đồng MLX" + "value" : "Giới hạn tin nhắn tối đa" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "MLX 社群" + "value" : "最大消息限制" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "MLX 社区" + "value" : "訊息數量上限" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "MLX 社群" + "value" : "訊息數量上限" } } } }, - "Max Length" : { + "Message Control" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الحد الأقصى لطول" + "value" : "التحكم في الرسائل" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Longitud màxima" + "value" : "Control de missatges" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Maximální délka" + "value" : "Řízení zpráv" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Maks. længde" + "value" : "Beskedkontrol" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale Länge" + "value" : "Nachrichtensteuerung" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Μέγιστο μήκος" + "value" : "Έλεγχος Μηνυμάτων" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Max Length" + "value" : "Message Control" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Max Length" + "value" : "Message Control" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Max Length" + "value" : "Message Control" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Longitud máxima" + "value" : "Control de mensajes" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Longitud Máxima" + "value" : "Control de Mensajes" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Enimmäispituus" + "value" : "Viestien hallinta" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Longueur maximale" + "value" : "Contrôle des messages" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Longueur maximale" + "value" : "Contrôle des messages" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אורך מרבי" + "value" : "בקרת הודעות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अधिकतम लंबाई" + "value" : "संदेश नियंत्रण" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Maksimalna duljina" + "value" : "Kontrola poruka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Maximális hosszasság" + "value" : "Üzenetvezérlés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Panjang Maksimum" + "value" : "Kontrol Pesan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Lunghezza massima" + "value" : "Controllo messaggi" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "最大長" + "value" : "メッセージコントロール" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "최대 길이" + "value" : "메시지 제어" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Panjang Maksimum" + "value" : "Kawalan Mesej" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Maks lengde" + "value" : "Meldingskontroll" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale lengte" + "value" : "Berichtbeheer" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Maksymalna długość" + "value" : "Sterowanie wiadomością" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Comprimento Máximo" + "value" : "Controle de Mensagens" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Comprimento Máximo" + "value" : "Controlo de Mensagens" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Lungime maximă" + "value" : "Control Mesaje" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Макс. длина" + "value" : "Управление сообщениями" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Maximálna dĺžka" + "value" : "Ovládanie správ" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Maxlängd" + "value" : "Meddelandekontroll" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ความยาวสูงสุด" + "value" : "การควบคุมข้อความ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maksimum Uzunluk" + "value" : "Mesaj Kontrolü" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Максимальна довжина" + "value" : "Керування повідомленнями" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Chiều dài tối đa" + "value" : "Kiểm soát Tin nhắn" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "最大長度" + "value" : "消息控制" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "最大长度" + "value" : "訊息控制" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "最大長度" + "value" : "訊息控制" } } } }, - "Max Messages Limit" : { + "ML" : { + + }, + "MLX Community" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الحد الأقصى للرسائل" + "value" : "مجتمع MLX" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Límit màxim de missatges" + "value" : "Comunitat MLX" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Maximální limit zpráv" + "value" : "Komunita MLX" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Maksimumgrænse for beskeder" + "value" : "MLX-fællesskab" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale Nachrichtenanzahl" + "value" : "MLX Community" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Μέγιστο Όριο Μηνυμάτων" + "value" : "Κοινότητα MLX" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Maximum Messages Limit" + "value" : "MLX Community" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Maximum Messages Limit" + "value" : "MLX Community" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Max Messages Limit" + "value" : "MLX Community" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Límite Máximo de Mensajes" + "value" : "Comunidad MLX" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Límite Máximo de Mensajes" + "value" : "Comunidad MLX" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Viestiin enimmäisraja" + "value" : "MLX-yhteisö" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Limite maximale de messages" + "value" : "Communauté MLX" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Limite maximale de messages" + "value" : "Communauté MLX" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מגבלת הודעות מרבית" + "value" : "קהילת MLX" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अधिकतम संदेश सीमा" + "value" : "MLX समुदाय" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Maksimalni broj poruka" + "value" : "Zajednica MLX" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenetek maximális száma" + "value" : "MLX Közösség" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Batas Pesan Maksimal" + "value" : "Komunitas MLX" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Limite massimo messaggi" + "value" : "Community MLX" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージ上限" + "value" : "MLXコミュニティ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "최대 메시지 제한" + "value" : "MLX 커뮤니티" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Had Maksimum Mesej" + "value" : "Komuniti MLX" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Maks grense for meldinger" + "value" : "MLX-fellesskap" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maximaal aantal berichten" + "value" : "MLX-community" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Limit maksymalnej liczby wiadomości" + "value" : "Społeczność MLX" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Limite Máximo de Mensagens" + "value" : "Comunidade MLX" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Limite Máximo de Mensagens" + "value" : "MLX Comunidade" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Limită maximă mesaje" + "value" : "Comunitatea MLX" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Максимальный лимит сообщений" + "value" : "MLX Сообщество" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Maximálny limit správ" + "value" : "Komunita MLX" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Maximalt antal meddelanden" + "value" : "MLX-community" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "จำกัดจำนวนข้อความสูงสุด" + "value" : "ชุมชน MLX" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maksimum Mesaj Sınırı" + "value" : "MLX Topluluğu" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Максимальна кількість повідомлень" + "value" : "Спільнота MLX" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Giới hạn tin nhắn tối đa" + "value" : "Cộng đồng MLX" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "訊息數量上限" + "value" : "MLX 社区" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "最大消息限制" + "value" : "MLX 社群" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "訊息數量上限" + "value" : "MLX 社群" } } } }, - "Message Control" : { + "mlx-community/OpenELM-3B" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التحكم في الرسائل" + "value" : "mlx-community/OpenELM-3B" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Control de missatges" + "value" : "mlx-community/OpenELM-3B" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Řízení zpráv" + "value" : "mlx-community/OpenELM-3B" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Beskedkontrol" + "value" : "mlx-community/OpenELM-3B" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nachrichtensteuerung" + "value" : "mlx-community/OpenELM-3B" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Έλεγχος Μηνυμάτων" + "value" : "mlx-community/OpenELM-3B" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Message Control" + "value" : "mlx-community/OpenELM-3B" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Message Control" + "value" : "mlx-community/OpenELM-3B" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Message Control" + "value" : "mlx-community/OpenELM-3B" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Control de mensajes" + "value" : "mlx-community/OpenELM-3B" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Control de Mensajes" + "value" : "mlx-community/OpenELM-3B" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Viestien hallinta" + "value" : "mlx-community/OpenELM-3B" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Contrôle des messages" + "value" : "mlx-community/OpenELM-3B" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Contrôle des messages" + "value" : "mlx-community/OpenELM-3B" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "בקרת הודעות" + "value" : "mlx-community/OpenELM-3B" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "संदेश नियंत्रण" + "value" : "mlx-community/OpenELM-3B" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Kontrola poruka" + "value" : "mlx-community/OpenELM-3B" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Üzenetvezérlés" + "value" : "mlx-community/OpenELM-3B" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Kontrol Pesan" + "value" : "mlx-community/OpenELM-3B" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Controllo messaggi" + "value" : "mlx-community/OpenELM-3B" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージコントロール" + "value" : "mlx-community/OpenELM-3B" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "메시지 제어" + "value" : "mlx-community/OpenELM-3B" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Kawalan Mesej" + "value" : "mlx-community/OpenELM-3B" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Meldingskontroll" + "value" : "mlx-community/OpenELM-3B" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Berichtbeheer" + "value" : "mlx-community/OpenELM-3B" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Sterowanie wiadomością" + "value" : "mlx-community/OpenELM-3B" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Controle de Mensagens" + "value" : "mlx-community/OpenELM-3B" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Controlo de Mensagens" + "value" : "mlx-community/OpenELM-3B" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Control Mesaje" + "value" : "mlx-community/OpenELM-3B" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Управление сообщениями" + "value" : "mlx-community/OpenELM-3B" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Ovládanie správ" + "value" : "mlx-community/OpenELM-3B" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Meddelandekontroll" + "value" : "mlx-community/OpenELM-3B" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การควบคุมข้อความ" + "value" : "mlx-community/OpenELM-3B" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Mesaj Kontrolü" + "value" : "mlx-community/OpenELM-3B" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Керування повідомленнями" + "value" : "mlx-community/OpenELM-3B" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Kiểm soát Tin nhắn" + "value" : "mlx-community/OpenELM-3B" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "訊息控制" + "value" : "mlx-community/OpenELM-3B" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "消息控制" + "value" : "mlx-community/OpenELM-3B" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "訊息控制" + "value" : "mlx-community/OpenELM-3B" } } } @@ -12372,25 +12164,28 @@ "value" : "Kiểu máy" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "模型" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "模型" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "模型" } } } + }, + "Model List" : { + }, "Model Settings" : { "localizations" : { @@ -12610,19 +12405,19 @@ "value" : "Cài đặt Mô hình" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "模型設定" + "value" : "模型设置" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "模型设置" + "value" : "模型設定" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "模型設定" @@ -12630,520 +12425,520 @@ } } }, - "Model State" : { + "Models" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حالة النموذج" + "value" : "النماذج" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Estat del model" + "value" : "Models" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Stav modelu" + "value" : "Modely" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Modeltilstand" + "value" : "Modeller" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Modellstatus" + "value" : "Modelle" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Κατάσταση Μοντέλου" + "value" : "Μοντέλα" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Model State" + "value" : "Models" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Model Status" + "value" : "Models" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Model State" + "value" : "Models" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Estado del Modelo" + "value" : "Modelos" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Estado del modelo" + "value" : "Modelos" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Mallin tila" + "value" : "Mallit" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "État du modèle" + "value" : "Modèles" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "État du modèle" + "value" : "Modèles" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מצב דגם" + "value" : "דגמים" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "मॉडल स्थिति" + "value" : "मॉडल्स" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Stanje modela" + "value" : "Modeli" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Modellállapot" + "value" : "Modellek" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Keadaan Model" + "value" : "Model" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Stato modello" + "value" : "Modelli" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "モデルの状態" + "value" : "モデル" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "모델 상태" + "value" : "모델" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Keadaan Model" + "value" : "Model" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Modelltilstand" + "value" : "Modeller" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Modelstatus" + "value" : "Modellen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Stan modelu" + "value" : "Modele" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Estado do Modelo" + "value" : "Modelos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Estado do Modelo" + "value" : "Modelos" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Model de stare" + "value" : "Modele" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Состояние модели" + "value" : "Модели" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Stav modelu" + "value" : "Modely" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Modelläge" + "value" : "Modeller" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "สถานะโมเดล" + "value" : "โมเดล" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Model Durumu" + "value" : "Modeller" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Стан моделі" + "value" : "Моделі" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trạng thái mô hình" + "value" : "Mô hình" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "模型狀態" + "value" : "模型" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "模型状态" + "value" : "型號" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "模型狀態" + "value" : "模型" } } } }, - "Models" : { + "New Chat" : { "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "النماذج" + "value" : "دردشة جديدة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Models" + "value" : "Xat nou" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Modely" + "value" : "Nový chat" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Modeller" + "value" : "Ny chat" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Modelle" + "value" : "Neuer Chat" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Μοντέλα" + "value" : "Νέα συνομιλία" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Models" + "value" : "New Chat" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Models" + "value" : "New Chat" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Models" + "value" : "New Chat" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Modelos" + "value" : "Nuevo Chat" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Modelos" + "value" : "Nuevo chat" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Mallit" + "value" : "Uusi keskustelu" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Modèles" + "value" : "Nouvelle discussion" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Modèles" + "value" : "Nouvelle Discussion" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דגמים" + "value" : "צ'אט חדש" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "मॉडल्स" + "value" : "नयी चैट" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Modeli" + "value" : "Nova poruka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Modellek" + "value" : "Új csevegés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Model" + "value" : "Obrolan Baru" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Modelli" + "value" : "Nuova chat" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "モデル" + "value" : "新規チャット" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "모델" + "value" : "새로운 채팅" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Model" + "value" : "Sembang Baru" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Modeller" + "value" : "Ny chat" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Modellen" + "value" : "Nieuwe chat" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Modele" + "value" : "Nowy czat" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Modelos" + "value" : "Nova Conversa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Modelos" + "value" : "Nova Conversa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Modele" + "value" : "Chat nou" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Модели" + "value" : "Новый чат" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Modely" + "value" : "Nový chat" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Modeller" + "value" : "Ny Chatt" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โมเดล" + "value" : "เริ่มแชทใหม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Modeller" + "value" : "Yeni Sohbet" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Моделі" + "value" : "Нова розмова" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mô hình" + "value" : "Trò chuyện mới" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "模型" + "value" : "新建对话" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "模型" + "value" : "新聊天" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "型號" + "value" : "新聊天" } } } }, - "New Chat" : { - "extractionState" : "manual", + "New Conversation" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "دردشة جديدة" + "value" : "محادثة جديدة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Xat nou" + "value" : "Nova conversa" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nový chat" + "value" : "Nová konverzace" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ny chat" + "value" : "Ny samtale" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Neuer Chat" + "value" : "Neue Konversation" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Νέα συνομιλία" + "value" : "Νέα Συνομιλία" } }, "en-AU" : { @@ -13155,7 +12950,7 @@ "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "New Chat" + "value" : "New Conversation" } }, "en-IN" : { @@ -13167,13 +12962,13 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "Nuevo Chat" + "value" : "Nueva conversación" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Nuevo chat" + "value" : "Nueva conversación" } }, "fi" : { @@ -13185,85 +12980,85 @@ "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nouvelle discussion" + "value" : "Nouvelle conversation" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Nouvelle Discussion" + "value" : "Nouvelle conversation" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "צ'אט חדש" + "value" : "שיחה חדשה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "नयी चैट" + "value" : "नई बातचीत" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Nova poruka" + "value" : "Novi razgovor" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Új csevegés" + "value" : "Új beszélgetés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Obrolan Baru" + "value" : "Percakapan Baru" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nuova chat" + "value" : "Nuova conversazione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "新規チャット" + "value" : "新規メッセージ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "새로운 채팅" + "value" : "새 대화" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Sembang Baru" + "value" : "Perbualan Baru" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ny chat" + "value" : "Ny samtale" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Nieuwe chat" + "value" : "Nieuw gesprek" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nowy czat" + "value" : "Nowa rozmowa" } }, "pt-BR" : { @@ -13281,7 +13076,7 @@ "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Chat nou" + "value" : "Conversație Nouă" } }, "ru" : { @@ -13293,25 +13088,25 @@ "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nový chat" + "value" : "Nový rozhovor" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ny Chatt" + "value" : "Ny konversation" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เริ่มแชทใหม่" + "value" : "เริ่มการสนทนาใหม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yeni Sohbet" + "value" : "Yeni Konuşma" } }, "uk" : { @@ -13323,508 +13118,508 @@ "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Trò chuyện mới" + "value" : "Cuộc trò chuyện mới" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "新聊天" + "value" : "新建对话" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "新建对话" + "value" : "新對話" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "新聊天" + "value" : "新對話" } } } }, - "New Conversation" : { + "New Task" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "محادثة جديدة" + "value" : "مهمة جديدة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Nova conversa" + "value" : "Nova tasca" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nová konverzace" + "value" : "Nový úkol" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ny samtale" + "value" : "Ny Opgave" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Neue Konversation" + "value" : "Neue Aufgabe" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Νέα Συνομιλία" + "value" : "Νέα Εργασία" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "New Chat" + "value" : "New Task" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "New Conversation" + "value" : "New Task" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "New Chat" + "value" : "New Task" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Nueva conversación" + "value" : "Nueva tarea" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Nueva conversación" + "value" : "Nueva Tarea" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Uusi keskustelu" + "value" : "Uusi tehtävä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nouvelle conversation" + "value" : "Nouvelle tâche" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Nouvelle conversation" + "value" : "Nouvelle tâche" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שיחה חדשה" + "value" : "משימה חדשה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "नई बातचीत" + "value" : "नई कार्य" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Novi razgovor" + "value" : "Novi zadatak" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Új beszélgetés" + "value" : "Új feladat" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Percakapan Baru" + "value" : "Tugas Baru" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nuova conversazione" + "value" : "Nuova attività" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "新規メッセージ" + "value" : "新規タスク" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "새 대화" + "value" : "새 작업" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Perbualan Baru" + "value" : "Tugasan Baharu" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ny samtale" + "value" : "Ny oppgave" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Nieuw gesprek" + "value" : "Nieuwe taak" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nowa rozmowa" + "value" : "Nowe zadanie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Nova Conversa" + "value" : "Nova Tarefa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Nova Conversa" + "value" : "Nova Tarefa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Conversație Nouă" + "value" : "Activitate nouă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Новый чат" + "value" : "Новая задача" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nový rozhovor" + "value" : "Nová úloha" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ny konversation" + "value" : "Ny uppgift" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เริ่มการสนทนาใหม่" + "value" : "งานใหม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yeni Konuşma" + "value" : "Yeni Görev" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Нова розмова" + "value" : "Нове завдання" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cuộc trò chuyện mới" + "value" : "Tác vụ Mới" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "新對話" + "value" : "新建任务" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "新建对话" + "value" : "新增任務" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "新對話" + "value" : "新任務" } } } }, - "New Task" : { + "No Chat" : { "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مهمة جديدة" + "value" : "لا توجد محادثة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Nova tasca" + "value" : "Sense xat" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nový úkol" + "value" : "Žádný chat" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ny Opgave" + "value" : "Ingen chat" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Neue Aufgabe" + "value" : "Kein Chat" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Νέα Εργασία" + "value" : "Χωρίς Συνομιλία" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "New Task" + "value" : "No Chat" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "New Task" + "value" : "No Chat" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "New Task" + "value" : "No Chat" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Nueva tarea" + "value" : "Sin chat" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Nueva Tarea" + "value" : "Sin chat" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Uusi tehtävä" + "value" : "Ei keskustelua" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Nouvelle tâche" + "value" : "Pas de discussion" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Nouvelle tâche" + "value" : "Pas de discussion" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "משימה חדשה" + "value" : "אין צ'אט" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "नई कार्य" + "value" : "कोई चैट नहीं" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Novi zadatak" + "value" : "Nema chata" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Új feladat" + "value" : "Nincs csevegés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Tugas Baru" + "value" : "Tidak Ada Obrolan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Nuova attività" + "value" : "Nessuna chat" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "新規タスク" + "value" : "チャットなし" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "새 작업" + "value" : "채팅 없음" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tugasan Baharu" + "value" : "Tiada Sembang" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ny oppgave" + "value" : "Ingen Chat" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Nieuwe taak" + "value" : "Geen chat" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nowe zadanie" + "value" : "Brak czatu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Nova Tarefa" + "value" : "Sem bate-papo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Nova Tarefa" + "value" : "Sem Chat" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Activitate nouă" + "value" : "Fără chat" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Новая задача" + "value" : "Нет чата" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nová úloha" + "value" : "Žiadny chat" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ny uppgift" + "value" : "Ingen chatt" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "งานใหม่" + "value" : "ไม่มีการแชท" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yeni Görev" + "value" : "Sohbet Yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Нове завдання" + "value" : "Немає чату" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tác vụ Mới" + "value" : "Không có Trò chuyện" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "新任務" + "value" : "无对话" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "新建任务" + "value" : "沒有聊天" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "新增任務" + "value" : "無聊天" } } } }, - "No Chat" : { - "extractionState" : "manual", + "No Conversation" : { "localizations" : { "ar" : { "stringUnit" : { @@ -13835,7 +13630,7 @@ "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Sense xat" + "value" : "Cap Conversa" } }, "cs" : { @@ -13847,323 +13642,85 @@ "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ingen chat" + "value" : "Ingen samtale" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Kein Chat" + "value" : "Kein Gespräch" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Χωρίς Συνομιλία" + "value" : "Καμία συνομιλία" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "No Chat" + "value" : "No Conversation" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "No Chat" + "value" : "No Conversation" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "No Chat" + "value" : "No Conversation" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Sin chat" + "value" : "Sin conversación" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Sin chat" + "value" : "Sin conversación" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ei keskustelua" + "value" : "Ei keskusteluja" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Pas de discussion" + "value" : "Aucune conversation" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Pas de discussion" + "value" : "Aucune conversation" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אין צ'אט" + "value" : "אין שיחה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "कोई चैट नहीं" + "value" : "कोई बातचीत नहीं" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Nema chata" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs csevegés" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tidak Ada Obrolan" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessuna chat" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "チャットなし" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "채팅 없음" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiada Sembang" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen Chat" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen chat" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak czatu" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sem bate-papo" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sem Chat" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fără chat" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет чата" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žiadny chat" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen chatt" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ไม่มีการแชท" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sohbet Yok" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немає чату" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có Trò chuyện" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "無聊天" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无对话" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "沒有聊天" - } - } - } - }, - "No Conversation" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "لا توجد محادثة" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cap Conversa" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žádný chat" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen samtale" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein Gespräch" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Καμία συνομιλία" - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Conversation" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Conversation" - } - }, - "en-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "No Conversation" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin conversación" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sin conversación" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei keskusteluja" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune conversation" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aucune conversation" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אין שיחה" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "कोई बातचीत नहीं" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nema razgovora" + "value" : "Nema razgovora" } }, "hu" : { @@ -14175,8468 +13732,7769 @@ "id" : { "stringUnit" : { "state" : "translated", - "value" : "Tidak Ada Percakapan" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nessuna conversazione" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "会話なし" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "대화 없음" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tiada Perbualan" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen samtaler" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Geen gesprek" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Brak rozmowy" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sem conversa" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sem Conversa" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nicio conversație" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Нет беседы" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Žiadna konverzácia" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ingen konversation" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ไม่มีการสนทนา" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sohbet Yok" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Немає розмов" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không có hội thoại" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "沒有對話" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无对话" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "尚未有對話" - } - } - } - }, - "Not selected" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "غير محدد" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "No seleccionat" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nevybráno" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikke valgt" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nicht ausgewählt" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Δεν έχει επιλεγεί" - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not selected" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not selected" - } - }, - "en-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "Not selected" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "No seleccionado" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "No seleccionado" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ei valittu" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Non sélectionné" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Non sélectionné" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "לא נבחר" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "चयनित नहीं" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nije odabrano" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nincs kiválasztva" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Belum dipilih" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Non selezionato" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "未選択" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "선택 안 됨" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tidak dipilih" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ikke valgt" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Niet geselecteerd" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie wybrano" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Não selecionado" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Não selecionado" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Neselectat" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не выбрано" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nevybraté" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Inte valt" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ไม่ได้เลือก" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seçilmedi" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Не вибрано" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Không được chọn" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "未選擇" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "无选择" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "未選取" - } - } - } - }, - "OK" : { - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "حسنًا" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "D'acord" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "en-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אישור" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "ठीक है" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "U redu" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "확인" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "ОК" - } - }, - "sk" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "OK" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "ตกลง" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Tamam" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Гаразд" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Đồng ý" - } - }, - "zh-HK" : { - "stringUnit" : { - "state" : "translated", - "value" : "確定" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "确定" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "好的" - } - } - } - }, - "Please enter Hugging Face Repo ID" : { - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "يرجى إدخال معرف مستودع Hugging Face" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Introduïu l'ID del repositori de Hugging Face" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zadejte ID repozitáře Hugging Face" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Indtast venligst Hugging Face Repo ID" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bitte Hugging Face Repo-ID eingeben" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Καταχωρίστε το ID του αποθετηρίου Hugging Face" - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "Please enter Hugging Face Repo ID" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "Please enter Hugging Face Repository ID" - } - }, - "en-IN" : { - "stringUnit" : { - "state" : "translated", - "value" : "Please enter Hugging Face Repo ID" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Introduce el ID del repositorio de Hugging Face" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Por favor, ingresa el ID del repositorio Hugging Face" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anna Hugging Face -repo ID" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Veuillez entrer l'ID du dépôt Hugging Face" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Veuillez entrer l'ID du dépôt Hugging Face" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "אנא הזן את מזהה המאגר של Hugging Face" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "कृपया Hugging Face Repo ID दर्ज करें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Unesite Hugging Face ID spremišta" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adja meg a Hugging Face tárház azonosítóját" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Masukkan Hugging Face Repo ID" + "value" : "Tidak Ada Percakapan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Inserisci l'ID del repository di Hugging Face" + "value" : "Nessuna conversazione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face レポIDを入力してください" + "value" : "会話なし" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "Hugging Face 리포지토리 ID를 입력하세요" + "value" : "대화 없음" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Sila masukkan ID Repo Hugging Face" + "value" : "Tiada Perbualan" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Vennligst skriv inn Hugging Face-repo-ID" + "value" : "Ingen samtaler" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voer alstublieft Hugging Face-opslag-ID in" + "value" : "Geen gesprek" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Proszę wprowadzić ID repozytorium Hugging Face" + "value" : "Brak rozmowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Insira o ID do Repositório Hugging Face" + "value" : "Sem conversa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Insira o ID do Repositório Hugging Face" + "value" : "Sem Conversa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Introduceți Hugging Face Repo ID" + "value" : "Nicio conversație" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Введите ID репозитория Hugging Face" + "value" : "Нет беседы" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Zadajte ID úložiska Hugging Face" + "value" : "Žiadna konverzácia" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Ange Hugging Face Repo-ID" + "value" : "Ingen konversation" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โปรดป้อน Hugging Face Repo ID" + "value" : "ไม่มีการสนทนา" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Lütfen Hugging Face Repo Kimliğini girin" + "value" : "Sohbet Yok" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Введіть ID репозиторія Hugging Face" + "value" : "Немає розмов" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Vui lòng nhập ID Kho Hugging Face" + "value" : "Không có hội thoại" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "請輸入 Hugging Face Repo ID" + "value" : "无对话" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "请输入 Hugging Face 仓库 ID" + "value" : "尚未有對話" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "請輸入 Hugging Face Repo ID" + "value" : "沒有對話" } } } }, - "Please select a new chat" : { - "extractionState" : "manual", + "Not selected" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يرجى اختيار دردشة جديدة" + "value" : "غير محدد" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Seleccioneu un xat nou" + "value" : "No seleccionat" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vyberte nový chat" + "value" : "Nevybráno" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Vælg venligst en ny chat" + "value" : "Ikke valgt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Neuen Chat auswählen" + "value" : "Nicht ausgewählt" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Επιλέξτε νέα συνομιλία" + "value" : "Δεν έχει επιλεγεί" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Please select a new conversation" + "value" : "Not selected" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Please select a new conversation" + "value" : "Not selected" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Please select a new chat" + "value" : "Not selected" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Seleccione un nuevo chat" + "value" : "No seleccionado" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Seleccione un nuevo chat" + "value" : "No seleccionado" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Valitse uusi keskustelu" + "value" : "Ei valittu" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Veuillez sélectionner une nouvelle conversation" + "value" : "Non sélectionné" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Veuillez sélectionner une nouvelle conversation" + "value" : "Non sélectionné" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "בחר/י צ'אט חדש" + "value" : "לא נבחר" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "कृपया एक नया चैट चुनें" + "value" : "चयनित नहीं" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Odaberite novi chat" + "value" : "Nije odabrano" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Válassz egy új csevegést" + "value" : "Nincs kiválasztva" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Pilih obrolan baru" + "value" : "Belum dipilih" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Seleziona una nuova chat" + "value" : "Non selezionato" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "新しいチャットを選択してください" + "value" : "未選択" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "새 채팅을 선택하십시오" + "value" : "선택 안 됨" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Sila pilih sembang baru" + "value" : "Tidak dipilih" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Velg en ny chat" + "value" : "Ikke valgt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Selecteer een nieuw gesprek" + "value" : "Niet geselecteerd" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wybierz nowy czat" + "value" : "Nie wybrano" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Selecione um novo chat" + "value" : "Não selecionado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Selecione uma nova conversa" + "value" : "Não selecionado" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Selectează un chat nou" + "value" : "Neselectat" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Выберите новый чат" + "value" : "Не выбрано" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vyberte novú konverzáciu" + "value" : "Nevybraté" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Välj en ny chatt" + "value" : "Inte valt" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โปรดเลือกแชทใหม่" + "value" : "ไม่ได้เลือก" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yeni sohbet seçin" + "value" : "Seçilmedi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Виберіть новий чат" + "value" : "Не вибрано" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Vui lòng chọn cuộc trò chuyện mới" + "value" : "Không được chọn" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "請選擇新的聊天" + "value" : "无选择" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "请选择一个新的对话" + "value" : "未選取" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "請選擇新的聊天" + "value" : "未選擇" } } } }, - "Please select a new conversation" : { + "OK" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يرجى اختيار محادثة جديدة" + "value" : "حسنًا" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Seleccioneu una nova conversa" + "value" : "D'acord" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Vyberte novou konverzaci" + "value" : "OK" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Vælg venligst en ny samtale" + "value" : "OK" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Bitte wählen Sie ein neues Gespräch aus" + "value" : "OK" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Επιλέξτε μια νέα συνομιλία" + "value" : "OK" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Please select a new chat" + "value" : "OK" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Please select a new chat" + "value" : "OK" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Please select a new chat conversation" + "value" : "OK" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Selecciona una nueva conversación" + "value" : "OK" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Seleccione una nueva conversación" + "value" : "OK" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Valitse uusi keskustelu" + "value" : "OK" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Veuillez sélectionner une nouvelle conversation" + "value" : "OK" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Veuillez sélectionner une nouvelle conversation" + "value" : "OK" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אנא בחר שיחה חדשה" + "value" : "אישור" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "कृपया नई बातचीत चुनें" + "value" : "ठीक है" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Odaberite novi razgovor" + "value" : "U redu" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Válassz egy új beszélgetést" + "value" : "OK" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Harap pilih percakapan baru" + "value" : "OK" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Seleziona una nuova conversazione" + "value" : "OK" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "新しい会話を選択してください" + "value" : "OK" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "새 대화를 선택하세요" + "value" : "확인" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Sila pilih perbualan baharu" + "value" : "OK" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Velg en ny samtale" + "value" : "OK" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Selecteer een nieuw gesprek" + "value" : "OK" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wybierz nową rozmowę" + "value" : "OK" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Selecione uma nova conversa" + "value" : "OK" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Selecione uma nova conversa" + "value" : "OK" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Selectează o conversație nouă" + "value" : "OK" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Выберите новый разговор" + "value" : "ОК" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vyberte novú konverzáciu" + "value" : "OK" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Välj en ny konversation" + "value" : "OK" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โปรดเลือกการสนทนาใหม่" + "value" : "ตกลง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yeni bir sohbet seçin" + "value" : "Tamam" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Будь ласка, виберіть нову розмову" + "value" : "Гаразд" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Vui lòng chọn cuộc trò chuyện mới" + "value" : "Đồng ý" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "請選擇新的對話" + "value" : "确定" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "请选择一个新的对话" + "value" : "好的" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "請選擇新的對話" + "value" : "確定" } } } }, - "Preferences and model settings" : { + "Open %@" : { + + }, + "Other AI Provider" : { + + }, + "Please enter Hugging Face Repo ID" : { "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التفضيلات وإعدادات النموذج" + "value" : "يرجى إدخال معرف مستودع Hugging Face" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Preferències i configuració del model" + "value" : "Introduïu l'ID del repositori de Hugging Face" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Předvolby a nastavení modelu" + "value" : "Zadejte ID repozitáře Hugging Face" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Præferencer og modelindstillinger" + "value" : "Indtast venligst Hugging Face Repo ID" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Einstellungen und Modelloptionen" + "value" : "Bitte Hugging Face Repo-ID eingeben" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Προτιμήσεις και ρυθμίσεις μοντέλου" + "value" : "Καταχωρίστε το ID του αποθετηρίου Hugging Face" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Preferences and model settings" + "value" : "Please enter Hugging Face Repo ID" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Preferences and model settings" + "value" : "Please enter Hugging Face Repository ID" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Preferences and model settings" + "value" : "Please enter Hugging Face Repo ID" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Preferencias y configuración del modelo" + "value" : "Introduce el ID del repositorio de Hugging Face" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Preferencias y configuración del modelo" + "value" : "Por favor, ingresa el ID del repositorio Hugging Face" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Asetukset ja mallin asetukset" + "value" : "Anna Hugging Face -repo ID" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Préférences et paramètres du modèle" + "value" : "Veuillez entrer l'ID du dépôt Hugging Face" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Préférences et paramètres du modèle" + "value" : "Veuillez entrer l'ID du dépôt Hugging Face" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "העדפות והגדרות מודל" + "value" : "אנא הזן את מזהה המאגר של Hugging Face" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "प्राथमिकताएँ और मॉडल सेटिंग्स" + "value" : "कृपया Hugging Face Repo ID दर्ज करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Postavke i postavke modela" + "value" : "Unesite Hugging Face ID spremišta" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Preferenciák és modellbeállítások" + "value" : "Adja meg a Hugging Face tárház azonosítóját" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Preferensi dan pengaturan model" + "value" : "Masukkan Hugging Face Repo ID" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Preferenze e impostazioni del modello" + "value" : "Inserisci l'ID del repository di Hugging Face" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "環境設定とモデル設定" + "value" : "Hugging Face レポIDを入力してください" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "환경설정 및 모델 설정" + "value" : "Hugging Face 리포지토리 ID를 입력하세요" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Keutamaan dan tetapan model" + "value" : "Sila masukkan ID Repo Hugging Face" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Preferanser og modellinnstillinger" + "value" : "Vennligst skriv inn Hugging Face-repo-ID" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Voorkeuren en modelinstellingen" + "value" : "Voer alstublieft Hugging Face-opslag-ID in" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Preferencje i ustawienia modelu" + "value" : "Proszę wprowadzić ID repozytorium Hugging Face" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Preferências e configurações do modelo" + "value" : "Insira o ID do Repositório Hugging Face" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Preferências e definições do modelo" + "value" : "Insira o ID do Repositório Hugging Face" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Preferințe și setări model" + "value" : "Introduceți Hugging Face Repo ID" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Настройки и параметры модели" + "value" : "Введите ID репозитория Hugging Face" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Predvoľby a nastavenia modelu" + "value" : "Zadajte ID úložiska Hugging Face" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inställningar och modellanpassningar" + "value" : "Ange Hugging Face Repo-ID" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การตั้งค่าและการตั้งค่าโมเดล" + "value" : "โปรดป้อน Hugging Face Repo ID" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tercihler ve model ayarları" + "value" : "Lütfen Hugging Face Repo Kimliğini girin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Налаштування і параметри моделі" + "value" : "Введіть ID репозиторія Hugging Face" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tùy chọn và cài đặt mô hình" + "value" : "Vui lòng nhập ID Kho Hugging Face" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "喜好設定和模型設定" + "value" : "请输入 Hugging Face 仓库 ID" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "偏好和模型设置" + "value" : "請輸入 Hugging Face Repo ID" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "偏好設定與模型設定" + "value" : "請輸入 Hugging Face Repo ID" } } } }, - "Prompt Time" : { + "Please select a new chat" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "وقت التنبيه" + "value" : "يرجى اختيار دردشة جديدة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Hora de la notificació" + "value" : "Seleccioneu un xat nou" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Čas výzvy" + "value" : "Vyberte nový chat" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Tidsforspørgsel" + "value" : "Vælg venligst en ny chat" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Eingabezeit" + "value" : "Neuen Chat auswählen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Χρόνος Προτροπής" + "value" : "Επιλέξτε νέα συνομιλία" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt Time" + "value" : "Please select a new conversation" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt Time" + "value" : "Please select a new conversation" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt Time" + "value" : "Please select a new chat" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Hora del aviso" + "value" : "Seleccione un nuevo chat" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Hora de aviso" + "value" : "Seleccione un nuevo chat" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Pyyntöaika" + "value" : "Valitse uusi keskustelu" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Heure d'invite" + "value" : "Veuillez sélectionner une nouvelle conversation" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Heure de rappel" + "value" : "Veuillez sélectionner une nouvelle conversation" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "זמן התראה" + "value" : "בחר/י צ'אט חדש" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "प्रॉम्प्ट समय" + "value" : "कृपया एक नया चैट चुनें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Vrijeme Podsjetnika" + "value" : "Odaberite novi chat" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Válaszidő" + "value" : "Válassz egy új csevegést" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Waktu Prompt" + "value" : "Pilih obrolan baru" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Tempo di Prompt" + "value" : "Seleziona una nuova chat" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "プロンプト時間" + "value" : "新しいチャットを選択してください" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "실행 시간" + "value" : "새 채팅을 선택하십시오" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Masa Arahan" + "value" : "Sila pilih sembang baru" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Varslingstid" + "value" : "Velg en ny chat" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Montagetijd" + "value" : "Selecteer een nieuw gesprek" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Czas monitowania" + "value" : "Wybierz nowy czat" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Tempo do Prompt" + "value" : "Selecione um novo chat" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Tempo de Prompt" + "value" : "Selecione uma nova conversa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Ora solicitării" + "value" : "Selectează un chat nou" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Время напоминания" + "value" : "Выберите новый чат" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Čas výzvy" + "value" : "Vyberte novú konverzáciu" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Frågetid" + "value" : "Välj en ny chatt" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ตั้งค่าการแจ้งเตือน" + "value" : "โปรดเลือกแชทใหม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Hızlandırma Saati" + "value" : "Yeni sohbet seçin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Час запиту" + "value" : "Виберіть новий чат" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Thời gian nhắc nhở" + "value" : "Vui lòng chọn cuộc trò chuyện mới" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "提示時間" + "value" : "请选择一个新的对话" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "提示词处理时间" + "value" : "請選擇新的聊天" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "提示時間" + "value" : "請選擇新的聊天" } } } }, - "Prompt Tokens/second" : { + "Please select a new conversation" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "رموز التنبيه/الثانية" + "value" : "يرجى اختيار محادثة جديدة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Tokens d'indicació/segon" + "value" : "Seleccioneu una nova conversa" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Počet vstupních tokenů za sekundu" + "value" : "Vyberte novou konverzaci" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt Tokens/sekund" + "value" : "Vælg venligst en ny samtale" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt-Token/Sekunde" + "value" : "Bitte wählen Sie ein neues Gespräch aus" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Προτροπή διακριτά/δευτερόλεπτο" + "value" : "Επιλέξτε μια νέα συνομιλία" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt Tokens/second" + "value" : "Please select a new chat" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt Tokens/second" + "value" : "Please select a new chat" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt Tokens/second" + "value" : "Please select a new chat conversation" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Tokens de indicación/segundo" + "value" : "Selecciona una nueva conversación" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Tokens de solicitud/segundo" + "value" : "Seleccione una nueva conversación" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kehotetokeneita/sekunti" + "value" : "Valitse uusi keskustelu" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jetons d'invite/seconde" + "value" : "Veuillez sélectionner une nouvelle conversation" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Jetons d'invite/seconde" + "value" : "Veuillez sélectionner une nouvelle conversation" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אסימוני הנחיה לשנייה" + "value" : "אנא בחר שיחה חדשה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "प्रॉम्प्ट टोकन्स/सेकंड" + "value" : "कृपया नई बातचीत चुनें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Podaci upita/sekundi" + "value" : "Odaberite novi razgovor" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Felszólító tokenek/másodperc" + "value" : "Válassz egy új beszélgetést" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Token Perintah/detik" + "value" : "Harap pilih percakapan baru" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Token Prompt/secondo" + "value" : "Seleziona una nuova conversazione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "プロンプトトークン/秒" + "value" : "新しい会話を選択してください" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "프롬프트 토큰/초당" + "value" : "새 대화를 선택하세요" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Token Prompt/saat" + "value" : "Sila pilih perbualan baharu" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Starttokener/sekund" + "value" : "Velg en ny samtale" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt-tokens/seconde" + "value" : "Selecteer een nieuw gesprek" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Tokeny monitu/sekundę" + "value" : "Wybierz nową rozmowę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Tokens de Prompt/segundo" + "value" : "Selecione uma nova conversa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Tokens de Prompt/segundo" + "value" : "Selecione uma nova conversa" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Marcaje de solicitare/secundă" + "value" : "Selectează o conversație nouă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Токены запроса/секунда" + "value" : "Выберите новый разговор" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Počet vstupných tokenov za sekundu" + "value" : "Vyberte novú konverzáciu" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Uppmaningstoken/sekund" + "value" : "Välj en ny konversation" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โทเค็นพร้อมท์/วินาที" + "value" : "โปรดเลือกการสนทนาใหม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uyarı Jetonları/saniye" + "value" : "Yeni bir sohbet seçin" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Токени підказки/секунда" + "value" : "Будь ласка, виберіть нову розмову" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Phím nhắc/Giây" + "value" : "Vui lòng chọn cuộc trò chuyện mới" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "提示字元/秒" + "value" : "请选择一个新的对话" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "提示词处理令牌/秒" + "value" : "請選擇新的對話" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "提示 Token/每秒" + "value" : "請選擇新的對話" } } } }, - "Regenerate" : { + "Preferences and model settings" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إعادة إنشاء" + "value" : "التفضيلات وإعدادات النموذج" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Regenera" + "value" : "Preferències i configuració del model" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerovat" + "value" : "Předvolby a nastavení modelu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Generer igen" + "value" : "Præferencer og modelindstillinger" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Erneut generieren" + "value" : "Einstellungen und Modelloptionen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αναδημιουργία" + "value" : "Προτιμήσεις και ρυθμίσεις μοντέλου" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerate" + "value" : "Preferences and model settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerate" + "value" : "Preferences and model settings" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerate" + "value" : "Preferences and model settings" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerar" + "value" : "Preferencias y configuración del modelo" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerar" + "value" : "Preferencias y configuración del modelo" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Luo uudelleen" + "value" : "Asetukset ja mallin asetukset" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Régénérer" + "value" : "Préférences et paramètres du modèle" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Régénérer" + "value" : "Préférences et paramètres du modèle" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "צור מחדש" + "value" : "העדפות והגדרות מודל" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "फिर से जनरेट करें" + "value" : "प्राथमिकताएँ और मॉडल सेटिंग्स" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Ponovi generiranje" + "value" : "Postavke i postavke modela" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Újragenerálás" + "value" : "Preferenciák és modellbeállítások" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerasi" + "value" : "Preferensi dan pengaturan model" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Rigenera" + "value" : "Preferenze e impostazioni del modello" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "再生成" + "value" : "環境設定とモデル設定" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "다시 생성" + "value" : "환경설정 및 모델 설정" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Jana semula" + "value" : "Keutamaan dan tetapan model" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Generer på nytt" + "value" : "Preferanser og modellinnstillinger" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Opnieuw genereren" + "value" : "Voorkeuren en modelinstellingen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Regeneruj" + "value" : "Preferencje i ustawienia modelu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerar" + "value" : "Preferências e configurações do modelo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerar" + "value" : "Preferências e definições do modelo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerează" + "value" : "Preferințe și setări model" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Генерировать заново" + "value" : "Настройки и параметры модели" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Obnoviť" + "value" : "Predvoľby a nastavenia modelu" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Regenerera" + "value" : "Inställningar och modellanpassningar" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "สร้างใหม่" + "value" : "การตั้งค่าและการตั้งค่าโมเดล" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Yeniden Oluştur" + "value" : "Tercihler ve model ayarları" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Оновити" + "value" : "Налаштування і параметри моделі" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tạo lại" + "value" : "Tùy chọn và cài đặt mô hình" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重新生成" + "value" : "偏好和模型设置" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "重新生成" + "value" : "偏好設定與模型設定" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "重新生成" + "value" : "喜好設定和模型設定" } } } }, - "Repetition Context Size" : { + "Prompt Time" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "حجم سياق التكرار" + "value" : "وقت التنبيه" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Mida del context de repetició" + "value" : "Hora de la notificació" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Velikost kontextu opakování" + "value" : "Čas výzvy" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Repetitionskontekststørrelse" + "value" : "Tidsforspørgsel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Wiederholungskontextgröße" + "value" : "Eingabezeit" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Μέγεθος Πλαισίου Επανάληψης" + "value" : "Χρόνος Προτροπής" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Repetition Context Size" + "value" : "Prompt Time" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Repetition Context Size" + "value" : "Prompt Time" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Repetition Context Size" + "value" : "Prompt Time" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Tamaño del Contexto de Repetición" + "value" : "Hora del aviso" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Tamaño del Contexto de Repetición" + "value" : "Hora de aviso" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Toiston kontekstin koko" + "value" : "Pyyntöaika" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Taille du contexte de répétition" + "value" : "Heure d'invite" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Taille du Contexte de Répétition" + "value" : "Heure de rappel" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "גודל הקשר לחזרה" + "value" : "זמן התראה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "पुनरावृत्ति संदर्भ आकार" + "value" : "प्रॉम्प्ट समय" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Veličina konteksta ponavljanja" + "value" : "Vrijeme Podsjetnika" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Ismétlés kontextusmérete" + "value" : "Válaszidő" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Ukuran Konteks Pengulangan" + "value" : "Waktu Prompt" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Dimensione contesto di ripetizione" + "value" : "Tempo di Prompt" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "繰り返しコンテキストサイズ" + "value" : "プロンプト時間" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "반복 컨텍스트 크기" + "value" : "실행 시간" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Saiz Konteks Pengulangan" + "value" : "Masa Arahan" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Størrelse på gjentakelsessammenheng" + "value" : "Varslingstid" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Herhalingscontextgrootte" + "value" : "Montagetijd" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Rozmiar kontekstu powtórzeń" + "value" : "Czas monitowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Tamanho do Contexto de Repetição" + "value" : "Tempo do Prompt" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Tamanho do Contexto de Repetição" + "value" : "Tempo de Prompt" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Dimensiunea contextului repetiției" + "value" : "Ora solicitării" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Размер контекста повторений" + "value" : "Время напоминания" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Veľkosť kontextu opakovania" + "value" : "Čas výzvy" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Repetitionskontextstorlek" + "value" : "Frågetid" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ขนาดบริบทการทำซ้ำ" + "value" : "ตั้งค่าการแจ้งเตือน" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tekrar Bağlam Boyutu" + "value" : "Hızlandırma Saati" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Розмір контексту повторення" + "value" : "Час запиту" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Kích thước ngữ cảnh lặp lại" + "value" : "Thời gian nhắc nhở" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重複上下文大小" + "value" : "提示词处理时间" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "重复上下文大小" + "value" : "提示時間" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "重複上下文大小" + "value" : "提示時間" } } } }, - "Repetition Penalty" : { + "Prompt Tokens/second" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "عقوبة التكرار" + "value" : "رموز التنبيه/الثانية" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Càstig de Repetició" + "value" : "Tokens d'indicació/segon" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Penalizace Opakování" + "value" : "Počet vstupních tokenů za sekundu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Gentagelsesstraf" + "value" : "Prompt Tokens/sekund" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Wiederholungsstrafe" + "value" : "Prompt-Token/Sekunde" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ποινή Επανάληψης" + "value" : "Προτροπή διακριτά/δευτερόλεπτο" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Repetition Penalty" + "value" : "Prompt Tokens/second" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Repetition Penalty" + "value" : "Prompt Tokens/second" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Repetition Penalty" + "value" : "Prompt Tokens/second" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Penalización por Repetición" + "value" : "Tokens de indicación/segundo" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Penalización por Repetición" + "value" : "Tokens de solicitud/segundo" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Toistopenalti" + "value" : "Kehotetokeneita/sekunti" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Pénalité de répétition" + "value" : "Jetons d'invite/seconde" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Pénalité de Répétition" + "value" : "Jetons d'invite/seconde" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "עונש על חזרה" + "value" : "אסימוני הנחיה לשנייה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "दोहराव दंड" + "value" : "प्रॉम्प्ट टोकन्स/सेकंड" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Kazna za ponavljanje" + "value" : "Podaci upita/sekundi" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Ismétlési Büntetés" + "value" : "Felszólító tokenek/másodperc" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Penalti Pengulangan" + "value" : "Token Perintah/detik" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Penalità di Ripetizione" + "value" : "Token Prompt/secondo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "反復ペナルティ" + "value" : "プロンプトトークン/秒" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "반복 패널티" + "value" : "프롬프트 토큰/초당" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Penalti Pengulangan" + "value" : "Token Prompt/saat" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Repetisjonstraff" + "value" : "Starttokener/sekund" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Herhalingsstraf" + "value" : "Prompt-tokens/seconde" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Kara za powtórzenia" + "value" : "Tokeny monitu/sekundę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Penalidade de Repetição" + "value" : "Tokens de Prompt/segundo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Penalização de Repetição" + "value" : "Tokens de Prompt/segundo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Penalizare Repetare" + "value" : "Marcaje de solicitare/secundă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Штраф за повторение" + "value" : "Токены запроса/секунда" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Trest za opakovanie" + "value" : "Počet vstupných tokenov za sekundu" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Repetitionsstraff" + "value" : "Uppmaningstoken/sekund" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "บทลงโทษการทำซ้ำ" + "value" : "โทเค็นพร้อมท์/วินาที" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tekrar Cezası" + "value" : "Uyarı Jetonları/saniye" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Штраф за повторення" + "value" : "Токени підказки/секунда" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Hình phạt lặp lại" + "value" : "Phím nhắc/Giây" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重複懲罰" + "value" : "提示词处理令牌/秒" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "重复惩罚" + "value" : "提示 Token/每秒" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "重複懲罰" + "value" : "提示字元/秒" } } } }, - "Reset All Settings" : { + "Quit" : { + + }, + "Regenerate" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إعادة تعيين جميع الإعدادات" + "value" : "إعادة إنشاء" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Restableix tots els ajustos" + "value" : "Regenera" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Obnovit všechna nastavení" + "value" : "Regenerovat" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Nulstil alle indstillinger" + "value" : "Generer igen" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Alle Einstellungen zurücksetzen" + "value" : "Erneut generieren" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Επαναφορά όλων των ρυθμίσεων" + "value" : "Αναδημιουργία" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Reset All Settings" + "value" : "Regenerate" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Reset All Settings" + "value" : "Regenerate" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Reset All Settings" + "value" : "Regenerate" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Restablecer ajustes" + "value" : "Regenerar" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Restablecer Todos los Ajustes" + "value" : "Regenerar" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Palauta kaikki asetukset" + "value" : "Luo uudelleen" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Réinitialiser tous les réglages" + "value" : "Régénérer" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Réinitialiser tous les réglages" + "value" : "Régénérer" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אפס את כל ההגדרות" + "value" : "צור מחדש" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सभी सेटिंग रीसेट करें" + "value" : "फिर से जनरेट करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Poništi sve postavke" + "value" : "Ponovi generiranje" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Összes beállítás visszaállítása" + "value" : "Újragenerálás" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Atur Ulang Semua Pengaturan" + "value" : "Regenerasi" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Reimposta tutte le impostazioni" + "value" : "Rigenera" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "すべての設定をリセット" + "value" : "再生成" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "모든 설정 재설정" + "value" : "다시 생성" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tetapkan Semula Semua Tetapan" + "value" : "Jana semula" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Tilbakestill alle innstillinger" + "value" : "Generer på nytt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Stel alle instellingen opnieuw in" + "value" : "Opnieuw genereren" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wyzeruj wszystkie ustawienia" + "value" : "Regeneruj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Redefinir Ajustes" + "value" : "Regenerar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Redefinir Todos os Ajustes" + "value" : "Regenerar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Resetați toate configurările" + "value" : "Regenerează" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Сбросить все настройки" + "value" : "Генерировать заново" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Resetovať všetky nastavenia" + "value" : "Obnoviť" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Återställ alla inställningar" + "value" : "Regenerera" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "รีเซ็ตการตั้งค่าทั้งหมด" + "value" : "สร้างใหม่" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tüm Ayarları Sıfırla" + "value" : "Yeniden Oluştur" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Скинути всі налаштування" + "value" : "Оновити" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Đặt Lại Tất Cả Cài Đặt" + "value" : "Tạo lại" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "重設所有設定" + "value" : "重新生成" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "重置所有设置" + "value" : "重新生成" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "重設所有設定" + "value" : "重新生成" } } } }, - "Search Conversation..." : { + "Repetition Context Size" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "البحث في المحادثة..." + "value" : "حجم سياق التكرار" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Cerca a la conversa..." + "value" : "Mida del context de repetició" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Hledat konverzaci..." + "value" : "Velikost kontextu opakování" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Søg i samtale ..." + "value" : "Repetitionskontekststørrelse" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Gespräch durchsuchen ..." + "value" : "Wiederholungskontextgröße" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αναζήτηση συνομιλίας..." + "value" : "Μέγεθος Πλαισίου Επανάληψης" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Search Conversation..." + "value" : "Repetition Context Size" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Search Conversation..." + "value" : "Repetition Context Size" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Search Conversation..." + "value" : "Repetition Context Size" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Buscar conversación..." + "value" : "Tamaño del Contexto de Repetición" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Buscar conversación..." + "value" : "Tamaño del Contexto de Repetición" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Etsi keskustelua..." + "value" : "Toiston kontekstin koko" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Rechercher dans la conversation..." + "value" : "Taille du contexte de répétition" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Rechercher dans la conversation..." + "value" : "Taille du Contexte de Répétition" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "חפש שיחה..." + "value" : "גודל הקשר לחזרה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "वार्ता खोजें..." + "value" : "पुनरावृत्ति संदर्भ आकार" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Pretraži razgovor..." + "value" : "Veličina konteksta ponavljanja" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Beszélgetés keresése..." + "value" : "Ismétlés kontextusmérete" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Cari Percakapan..." + "value" : "Ukuran Konteks Pengulangan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Cerca Conversazione..." + "value" : "Dimensione contesto di ripetizione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "会話を検索..." + "value" : "繰り返しコンテキストサイズ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "대화 검색..." + "value" : "반복 컨텍스트 크기" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Cari Perbualan..." + "value" : "Saiz Konteks Pengulangan" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Søk i samtale ..." + "value" : "Størrelse på gjentakelsessammenheng" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gesprek doorzoeken..." + "value" : "Herhalingscontextgrootte" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Szukaj w rozmowie..." + "value" : "Rozmiar kontekstu powtórzeń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Buscar Conversa..." + "value" : "Tamanho do Contexto de Repetição" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Procurar Conversa..." + "value" : "Tamanho do Contexto de Repetição" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Căutați în conversație..." + "value" : "Dimensiunea contextului repetiției" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Поиск по переписке..." + "value" : "Размер контекста повторений" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Hľadať v konverzácii..." + "value" : "Veľkosť kontextu opakovania" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Sök konversation..." + "value" : "Repetitionskontextstorlek" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ค้นหาบทสนทนา..." + "value" : "ขนาดบริบทการทำซ้ำ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sohbeti Ara..." + "value" : "Tekrar Bağlam Boyutu" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Шукати розмову…" + "value" : "Розмір контексту повторення" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tìm kiếm Cuộc trò chuyện..." + "value" : "Kích thước ngữ cảnh lặp lại" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "搜尋對話…" + "value" : "重复上下文大小" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "搜索对话..." + "value" : "重複上下文大小" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "搜尋對話..." + "value" : "重複上下文大小" } } } }, - "Search..." : { + "Repetition Penalty" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "بحث..." + "value" : "عقوبة التكرار" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Cerca..." + "value" : "Càstig de Repetició" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Hledat..." + "value" : "Penalizace Opakování" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Søg..." + "value" : "Gentagelsesstraf" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Suchen ..." + "value" : "Wiederholungsstrafe" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αναζήτηση..." + "value" : "Ποινή Επανάληψης" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Search..." + "value" : "Repetition Penalty" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Search..." + "value" : "Repetition Penalty" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Search..." + "value" : "Repetition Penalty" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Buscar..." + "value" : "Penalización por Repetición" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Buscar..." + "value" : "Penalización por Repetición" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Etsi..." + "value" : "Toistopenalti" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Recherche..." + "value" : "Pénalité de répétition" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Rechercher…" + "value" : "Pénalité de Répétition" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "חיפוש..." + "value" : "עונש על חזרה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "खोजें..." + "value" : "दोहराव दंड" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Pretraži..." + "value" : "Kazna za ponavljanje" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Keresés..." + "value" : "Ismétlési Büntetés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Cari..." + "value" : "Penalti Pengulangan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Cerca..." + "value" : "Penalità di Ripetizione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "検索..." + "value" : "反復ペナルティ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "검색..." + "value" : "반복 패널티" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Carian..." + "value" : "Penalti Pengulangan" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Søk..." + "value" : "Repetisjonstraff" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Zoeken..." + "value" : "Herhalingsstraf" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Szukaj..." + "value" : "Kara za powtórzenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Buscar..." + "value" : "Penalidade de Repetição" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Pesquisar..." + "value" : "Penalização de Repetição" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Căutare..." + "value" : "Penalizare Repetare" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Поиск..." + "value" : "Штраф за повторение" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Hľadať..." + "value" : "Trest za opakovanie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Sök..." + "value" : "Repetitionsstraff" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ค้นหา..." + "value" : "บทลงโทษการทำซ้ำ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ara..." + "value" : "Tekrar Cezası" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Пошук..." + "value" : "Штраф за повторення" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tìm kiếm..." + "value" : "Hình phạt lặp lại" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "搜尋..." + "value" : "重复惩罚" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "搜索..." + "value" : "重複懲罰" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "搜尋..." + "value" : "重複懲罰" } } } }, - "Send" : { - "extractionState" : "manual", + "Reset All Settings" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إرسال" + "value" : "إعادة تعيين جميع الإعدادات" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Envia" + "value" : "Restableix tots els ajustos" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Odeslat" + "value" : "Obnovit všechna nastavení" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Send" + "value" : "Nulstil alle indstillinger" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Senden" + "value" : "Alle Einstellungen zurücksetzen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Αποστολή" + "value" : "Επαναφορά όλων των ρυθμίσεων" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Send" + "value" : "Reset All Settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Send" + "value" : "Reset All Settings" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Send" + "value" : "Reset All Settings" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar" + "value" : "Restablecer ajustes" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar" + "value" : "Restablecer Todos los Ajustes" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Lähetä" + "value" : "Palauta kaikki asetukset" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Envoyer" + "value" : "Réinitialiser tous les réglages" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Envoyer" + "value" : "Réinitialiser tous les réglages" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "שלח" + "value" : "אפס את כל ההגדרות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "भेजें" + "value" : "सभी सेटिंग रीसेट करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Pošalji" + "value" : "Poništi sve postavke" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Küldés" + "value" : "Összes beállítás visszaállítása" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Kirim" + "value" : "Atur Ulang Semua Pengaturan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Invia" + "value" : "Reimposta tutte le impostazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "送信" + "value" : "すべての設定をリセット" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "보내기" + "value" : "모든 설정 재설정" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Hantar" + "value" : "Tetapkan Semula Semua Tetapan" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Send" + "value" : "Tilbakestill alle innstillinger" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Verstuur" + "value" : "Stel alle instellingen opnieuw in" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wyślij" + "value" : "Wyzeruj wszystkie ustawienia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar" + "value" : "Redefinir Ajustes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Enviar" + "value" : "Redefinir Todos os Ajustes" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Trimite" + "value" : "Resetați toate configurările" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Отправить" + "value" : "Сбросить все настройки" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Odoslať" + "value" : "Resetovať všetky nastavenia" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Skicka" + "value" : "Återställ alla inställningar" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ส่ง" + "value" : "รีเซ็ตการตั้งค่าทั้งหมด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Gönder" + "value" : "Tüm Ayarları Sıfırla" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Надіслати" + "value" : "Скинути всі налаштування" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Gửi" + "value" : "Đặt Lại Tất Cả Cài Đặt" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "傳送" + "value" : "重置所有设置" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "发送" + "value" : "重設所有設定" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "發送" + "value" : "重設所有設定" } } } }, - "Settings" : { - "extractionState" : "manual", + "Search Conversation..." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الإعدادات" + "value" : "البحث في المحادثة..." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Configuració" + "value" : "Cerca a la conversa..." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nastavení" + "value" : "Hledat konverzaci..." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Indstillinger" + "value" : "Søg i samtale ..." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Einstellungen" + "value" : "Gespräch durchsuchen ..." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ρυθμίσεις" + "value" : "Αναζήτηση συνομιλίας..." } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Settings" + "value" : "Search Conversation..." } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Settings" + "value" : "Search Conversation..." } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Settings" + "value" : "Search Conversation..." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ajustes" + "value" : "Buscar conversación..." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Configuración" + "value" : "Buscar conversación..." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Asetukset" + "value" : "Etsi keskustelua..." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Réglages" + "value" : "Rechercher dans la conversation..." } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Réglages" + "value" : "Rechercher dans la conversation..." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "הגדרות" + "value" : "חפש שיחה..." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सेटिंग्स" + "value" : "वार्ता खोजें..." } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Postavke" + "value" : "Pretraži razgovor..." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Beállítások" + "value" : "Beszélgetés keresése..." } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Pengaturan" + "value" : "Cari Percakapan..." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Impostazioni" + "value" : "Cerca Conversazione..." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "設定" + "value" : "会話を検索..." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "설정" + "value" : "대화 검색..." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tetapan" + "value" : "Cari Perbualan..." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Innstillinger" + "value" : "Søk i samtale ..." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Instellingen" + "value" : "Gesprek doorzoeken..." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ustawienia" + "value" : "Szukaj w rozmowie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Ajustes" + "value" : "Buscar Conversa..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Definições" + "value" : "Procurar Conversa..." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Configurări" + "value" : "Căutați în conversație..." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Настройки" + "value" : "Поиск по переписке..." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Nastavenia" + "value" : "Hľadať v konverzácii..." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Inställningar" + "value" : "Sök konversation..." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การตั้งค่า" + "value" : "ค้นหาบทสนทนา..." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Ayarlar" + "value" : "Sohbeti Ara..." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Налаштування" + "value" : "Шукати розмову…" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cài đặt" + "value" : "Tìm kiếm Cuộc trò chuyện..." } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "設定" + "value" : "搜索对话..." } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "设置" + "value" : "搜尋對話..." } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "設定" + "value" : "搜尋對話…" } } } }, - "System Prompt" : { + "Search..." : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "التنبيه النظامي" + "value" : "بحث..." } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Indicació del sistema" + "value" : "Cerca..." } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Systémová výzva" + "value" : "Hledat..." } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Systemprompt" + "value" : "Søg..." } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Systemaufforderung" + "value" : "Suchen ..." } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Προτροπή Συστήματος" + "value" : "Αναζήτηση..." } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "System Prompt" + "value" : "Search..." } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "System Prompt" + "value" : "Search..." } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "System Prompt" + "value" : "Search..." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Indicador del sistema" + "value" : "Buscar..." } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Mensaje del sistema" + "value" : "Buscar..." } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Järjestelmäkehotus" + "value" : "Etsi..." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Invite système" + "value" : "Recherche..." } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Invite du système" + "value" : "Rechercher…" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "הנחיית מערכת" + "value" : "חיפוש..." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सिस्टम प्रॉम्प्ट" + "value" : "खोजें..." } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Sistemski upit" + "value" : "Pretraži..." } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Rendszerüzenet" + "value" : "Keresés..." } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt Sistem" + "value" : "Cari..." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt di Sistema" + "value" : "Cerca..." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "システムプロンプト" + "value" : "検索..." } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "시스템 프롬프트" + "value" : "검색..." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Arahan Sistem" + "value" : "Carian..." } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Systemprompt" + "value" : "Søk..." } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Systeemprompt" + "value" : "Zoeken..." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Komunikat systemowy" + "value" : "Szukaj..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Prompt do Sistema" + "value" : "Buscar..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Mensagem do Sistema" + "value" : "Pesquisar..." } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Indicație Sistem" + "value" : "Căutare..." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Системная подсказка" + "value" : "Поиск..." } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Systemová výzva" + "value" : "Hľadať..." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Systemuppmaning" + "value" : "Sök..." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "พร้อมท์ของระบบ" + "value" : "ค้นหา..." } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem İstemi" + "value" : "Ara..." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Системний запит" + "value" : "Пошук..." } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Nhắc Hệ Thống" + "value" : "Tìm kiếm..." } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "系統提示" + "value" : "搜索..." } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "系统提示词" + "value" : "搜尋..." } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "系統提示" + "value" : "搜尋..." } } } }, - "System Settings" : { + "Send" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "إعدادات النظام" + "value" : "إرسال" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Configuració del sistema" + "value" : "Envia" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nastavení systému" + "value" : "Odeslat" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Indstillinger" + "value" : "Send" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "System­einstellungen" + "value" : "Senden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Ρυθμίσεις συστήματος" + "value" : "Αποστολή" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "System Settings" + "value" : "Send" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "System Settings" + "value" : "Send" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "System Settings" + "value" : "Send" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Ajustes del sistema" + "value" : "Enviar" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Configuración del sistema" + "value" : "Enviar" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Järjestelmäasetukset" + "value" : "Lähetä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Réglages Système" + "value" : "Envoyer" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Réglages du système" + "value" : "Envoyer" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "הגדרות מערכת" + "value" : "שלח" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सिस्टम सेटिंग्स" + "value" : "भेजें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Postavke sustava" + "value" : "Pošalji" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Beállítások" + "value" : "Küldés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Pengaturan Sistem" + "value" : "Kirim" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Impostazioni di Sistema" + "value" : "Invia" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "システム設定" + "value" : "送信" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "시스템 설정" + "value" : "보내기" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tetapan Sistem" + "value" : "Hantar" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Systeminnstillinger" + "value" : "Send" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Systeeminstellingen" + "value" : "Verstuur" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ustawienia systemu" + "value" : "Wyślij" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Ajustes do Sistema" + "value" : "Enviar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Definições do Sistema" + "value" : "Enviar" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Setări Sistem" + "value" : "Trimite" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Настройки системы" + "value" : "Отправить" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Systémové nastavenia" + "value" : "Odoslať" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Systeminställningar" + "value" : "Skicka" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "การตั้งค่าระบบ" + "value" : "ส่ง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem Ayarları" + "value" : "Gönder" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Параметри системи" + "value" : "Надіслати" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cài đặt hệ thống" + "value" : "Gửi" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "系統設定" + "value" : "发送" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "系统设置" + "value" : "發送" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "系統設定" + "value" : "傳送" } } } }, - "Temperature" : { + "Settings" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "درجة الحرارة" + "value" : "الإعدادات" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatura" + "value" : "Configuració" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Teplota" + "value" : "Nastavení" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatur" + "value" : "Indstillinger" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatur" + "value" : "Einstellungen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Θερμοκρασία" + "value" : "Ρυθμίσεις" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Temperature" + "value" : "Settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Temperature" + "value" : "Settings" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Temperature" + "value" : "Settings" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatura" + "value" : "Ajustes" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatura" + "value" : "Configuración" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Lämpötila" + "value" : "Asetukset" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Température" + "value" : "Réglages" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Température" + "value" : "Réglages" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "טמפרטורה" + "value" : "הגדרות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "टेम्परेचर" + "value" : "सेटिंग्स" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatura" + "value" : "Postavke" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Hőmérséklet" + "value" : "Beállítások" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Suhu" + "value" : "Pengaturan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatura" + "value" : "Impostazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "気温" + "value" : "設定" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "온도" + "value" : "설정" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Suhu" + "value" : "Tetapan" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatur" + "value" : "Innstillinger" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatuur" + "value" : "Instellingen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatura" + "value" : "Ustawienia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatura" + "value" : "Ajustes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatura" + "value" : "Definições" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatură" + "value" : "Configurări" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Температура" + "value" : "Настройки" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Teplota" + "value" : "Nastavenia" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Temperatur" + "value" : "Inställningar" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "อุณหภูมิ" + "value" : "การตั้งค่า" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sıcaklık" + "value" : "Ayarlar" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Температура" + "value" : "Налаштування" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Nhiệt độ" + "value" : "Cài đặt" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "溫度" + "value" : "设置" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "温度" + "value" : "設定" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "溫度" + "value" : "設定" } } } }, - "Title" : { + "System Prompt" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "العنوان" + "value" : "التنبيه النظامي" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Títol" + "value" : "Indicació del sistema" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Název" + "value" : "Systémová výzva" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Titel" + "value" : "Systemprompt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Titel" + "value" : "Systemaufforderung" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Τίτλος" + "value" : "Προτροπή Συστήματος" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Title" + "value" : "System Prompt" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Title" + "value" : "System Prompt" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Title" + "value" : "System Prompt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Título" + "value" : "Indicador del sistema" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Título" + "value" : "Mensaje del sistema" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Otsikko" + "value" : "Järjestelmäkehotus" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Titre" + "value" : "Invite système" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Titre" + "value" : "Invite du système" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "כותרת" + "value" : "הנחיית מערכת" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "शीर्षक" + "value" : "सिस्टम प्रॉम्प्ट" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Naslov" + "value" : "Sistemski upit" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Cím" + "value" : "Rendszerüzenet" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Judul" + "value" : "Prompt Sistem" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Titolo" + "value" : "Prompt di Sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "タイトル" + "value" : "システムプロンプト" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "제목" + "value" : "시스템 프롬프트" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tajuk" + "value" : "Arahan Sistem" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Tittel" + "value" : "Systemprompt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Titel" + "value" : "Systeemprompt" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Tytuł" + "value" : "Komunikat systemowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Título" + "value" : "Prompt do Sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Título" + "value" : "Mensagem do Sistema" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Titlu" + "value" : "Indicație Sistem" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Название" + "value" : "Системная подсказка" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Názov" + "value" : "Systemová výzva" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Titel" + "value" : "Systemuppmaning" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ชื่อเรื่อง" + "value" : "พร้อมท์ของระบบ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Başlık" + "value" : "Sistem İstemi" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Назва" + "value" : "Системний запит" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Tiêu đề" + "value" : "Nhắc Hệ Thống" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "標題" + "value" : "系统提示词" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "标题" + "value" : "系統提示" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "標題" + "value" : "系統提示" } } } }, - "Token" : { + "System Settings" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "رمز" + "value" : "إعدادات النظام" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Configuració del sistema" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Nastavení systému" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Indstillinger" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "System­einstellungen" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Διακριτικό" + "value" : "Ρυθμίσεις συστήματος" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "System Settings" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "System Settings" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "System Settings" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Ajustes del sistema" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Configuración del sistema" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Varmenne" + "value" : "Järjestelmäasetukset" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Jeton" + "value" : "Réglages Système" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Jeton" + "value" : "Réglages du système" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אסימון" + "value" : "הגדרות מערכת" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "टोकन" + "value" : "सिस्टम सेटिंग्स" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Postavke sustava" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Biztonsági kód" + "value" : "Beállítások" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Pengaturan Sistem" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Impostazioni di Sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "トークン" + "value" : "システム設定" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "토큰" + "value" : "시스템 설정" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Tetapan Sistem" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Systeminnstillinger" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Systeeminstellingen" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Ustawienia systemu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Ajustes do Sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Definições do Sistema" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Jeton" + "value" : "Setări Sistem" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Токен" + "value" : "Настройки системы" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Systémové nastavenia" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Token" + "value" : "Systeminställningar" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "โทเค็น" + "value" : "การตั้งค่าระบบ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Jeton" + "value" : "Sistem Ayarları" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Токен" + "value" : "Параметри системи" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Mã thông báo" + "value" : "Cài đặt hệ thống" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "憑證" + "value" : "系统设置" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "令牌" + "value" : "系統設定" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "代幣" + "value" : "系統設定" } } } }, - "Top P" : { + "Temperature" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "أعلى P" + "value" : "درجة الحرارة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Principal P" + "value" : "Temperatura" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Nejlepší P" + "value" : "Teplota" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Top P" + "value" : "Temperatur" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Top P" + "value" : "Temperatur" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Κορυφαία π" + "value" : "Θερμοκρασία" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Top P" + "value" : "Temperature" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Top P" + "value" : "Temperature" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Top P" + "value" : "Temperature" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Superior P" + "value" : "Temperatura" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Mejor P" + "value" : "Temperatura" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ylä-P" + "value" : "Lämpötila" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Top P" + "value" : "Température" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Haut P" + "value" : "Température" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "דירוג גבוה" + "value" : "טמפרטורה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "शीर्ष P" + "value" : "टेम्परेचर" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Vrh P" + "value" : "Temperatura" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Csúcs P" + "value" : "Hőmérséklet" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Teratas P" + "value" : "Suhu" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "In Evidenza" + "value" : "Temperatura" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "上位P" + "value" : "気温" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "상위 P" + "value" : "온도" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Paling Atas" + "value" : "Suhu" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Topp P" + "value" : "Temperatur" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Top-P" + "value" : "Temperatuur" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Najlepsze P" + "value" : "Temperatura" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Top P" + "value" : "Temperatura" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "P Principal" + "value" : "Temperatura" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "De sus P" + "value" : "Temperatură" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Самый популярный" + "value" : "Температура" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Najlepšie P" + "value" : "Teplota" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Topp P" + "value" : "Temperatur" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "บนสุด P" + "value" : "อุณหภูมิ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "En Üst P" + "value" : "Sıcaklık" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Топ P" + "value" : "Температура" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Top P" + "value" : "Nhiệt độ" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "熱門 P" + "value" : "温度" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "核采样" + "value" : "溫度" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "熱門 P" + "value" : "溫度" } } } }, - "Type your message…" : { - "extractionState" : "manual", + "Text Generation" : { + + }, + "Title" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اكتب رسالتك…" + "value" : "العنوان" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Escriu el teu missatge…" + "value" : "Títol" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Napište zprávu…" + "value" : "Název" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Indtast din besked…" + "value" : "Titel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Nachricht eingeben …" + "value" : "Titel" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Πληκτρολογήστε το μήνυμά σας…" + "value" : "Τίτλος" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Type your message…" + "value" : "Title" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Type your message…" + "value" : "Title" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Type your message…" + "value" : "Title" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Escribe tu mensaje…" + "value" : "Título" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Escribe tu mensaje…" + "value" : "Título" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Kirjoita viestisi…" + "value" : "Otsikko" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Tapez votre message…" + "value" : "Titre" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Tapez votre message…" + "value" : "Titre" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "הקלידו את ההודעה שלכם…" + "value" : "כותרת" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अपना संदेश टाइप करें…" + "value" : "शीर्षक" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Unesite poruku…" + "value" : "Naslov" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Írja be üzenetét…" + "value" : "Cím" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Ketik pesan Anda…" + "value" : "Judul" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Digita il tuo messaggio…" + "value" : "Titolo" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "メッセージを入力…" + "value" : "タイトル" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "메시지를 입력하세요…" + "value" : "제목" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Taip mesej anda…" + "value" : "Tajuk" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Skriv meldingen din…" + "value" : "Tittel" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Typ je bericht…" + "value" : "Titel" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wpisz swoją wiadomość…" + "value" : "Tytuł" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Digite sua mensagem…" + "value" : "Título" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Escreva a sua mensagem…" + "value" : "Título" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Scrieți mesajul…" + "value" : "Titlu" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Введите сообщение…" + "value" : "Название" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Napíšte svoju správu…" + "value" : "Názov" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Skriv ditt meddelande..." + "value" : "Titel" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "พิมพ์ข้อความของคุณ…" + "value" : "ชื่อเรื่อง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Mesajınızı yazın…" + "value" : "Başlık" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Введіть повідомлення…" + "value" : "Назва" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Nhập tin nhắn của bạn…" + "value" : "Tiêu đề" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "輸入您的訊息……" + "value" : "标题" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "输入您的消息…" + "value" : "標題" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "鍵入您的消息…" + "value" : "標題" } } } }, - "Unknown" : { - "extractionState" : "manual", + "Token" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "غير معروف" + "value" : "رمز" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Desconegut" + "value" : "Token" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Neznámé" + "value" : "Token" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Ukendt" + "value" : "Token" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Unbekannt" + "value" : "Token" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Άγνωστο" + "value" : "Διακριτικό" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Unknown" + "value" : "Token" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Unknown" + "value" : "Token" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Unknown" + "value" : "Token" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Desconocido" + "value" : "Token" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Desconocido" + "value" : "Token" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Tuntematon" + "value" : "Varmenne" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Inconnu" + "value" : "Jeton" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Inconnu" + "value" : "Jeton" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "לא ידוע" + "value" : "אסימון" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अज्ञात" + "value" : "टोकन" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Nepoznato" + "value" : "Token" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Ismeretlen" + "value" : "Biztonsági kód" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Tidak Diketahui" + "value" : "Token" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Sconosciuto" + "value" : "Token" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "不明" + "value" : "トークン" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "알 수 없음" + "value" : "토큰" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Tidak diketahui" + "value" : "Token" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Ukjent" + "value" : "Token" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Onbekend" + "value" : "Token" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Nieznany" + "value" : "Token" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Desconhecido" + "value" : "Token" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Desconhecido" + "value" : "Token" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Necunoscut" + "value" : "Jeton" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Неизвестно" + "value" : "Токен" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Neznáme" + "value" : "Token" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Okänd" + "value" : "Token" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ไม่ทราบ" + "value" : "โทเค็น" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Bilinmeyen" + "value" : "Jeton" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Невідомо" + "value" : "Токен" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Không xác định" + "value" : "Mã thông báo" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "不明" + "value" : "令牌" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "未知" + "value" : "代幣" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "未知" + "value" : "憑證" } } } }, - "Use Custom Endpoint" : { + "Top P" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "استخدام نقطة نهاية مخصصة" + "value" : "أعلى P" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Utilitza el punt final personalitzat" + "value" : "Principal P" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Použít vlastní koncový bod" + "value" : "Nejlepší P" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Brug brugerdefineret slutpunkt" + "value" : "Top P" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Benutzerdefinierten Endpunkt verwenden" + "value" : "Top P" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Χρήση Προσαρμοσμένου Τερματικού Σημείου" + "value" : "Κορυφαία π" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Use Custom Endpoint" + "value" : "Top P" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Use Custom Endpoint" + "value" : "Top P" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Use Custom Endpoint" + "value" : "Top P" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Usar punto final personalizado" + "value" : "Superior P" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Usar punto de acceso personalizado" + "value" : "Mejor P" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Käytä mukautettua päätepistettä" + "value" : "Ylä-P" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser un point de terminaison personnalisé" + "value" : "Top P" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Utilisateur un point de terminaison personnalisé" + "value" : "Haut P" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "השתמש בקצה מותאם אישית" + "value" : "דירוג גבוה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "कस्टम एंडपॉइंट का उपयोग करें" + "value" : "शीर्ष P" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Upotrijebi prilagođenu krajnju točku" + "value" : "Vrh P" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Egyéni végpont használata" + "value" : "Csúcs P" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Titik Akhir Kustom" + "value" : "Teratas P" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Usa endpoint personalizzato" + "value" : "In Evidenza" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "カスタムエンドポイントを使用" + "value" : "上位P" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "사용자 지정 엔드포인트 사용" + "value" : "상위 P" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Titik Akhir Tersuai" + "value" : "Paling Atas" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Bruk egendefinert endepunkt" + "value" : "Topp P" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Aangepast eindpunt gebruiken" + "value" : "Top-P" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Użyj niestandardowego punktu końcowego" + "value" : "Najlepsze P" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Ponto de Extremidade Personalizado" + "value" : "Top P" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Endpoint Personalizado" + "value" : "P Principal" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Utilizați Endpoint Personalizat" + "value" : "De sus P" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Использовать пользовательский конечный узел" + "value" : "Самый популярный" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Použiť vlastný koncový bod" + "value" : "Najlepšie P" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Använd anpassad slutpunkt" + "value" : "Topp P" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ใช้ปลายทางที่กำหนดเอง" + "value" : "บนสุด P" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Özel Uç Nokta Kullan" + "value" : "En Üst P" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Використовувати користувацьку кінцеву точку" + "value" : "Топ P" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Sử Dụng Điểm Kết Nối Tùy Chỉnh" + "value" : "Top P" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "使用自定端點" + "value" : "核采样" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "使用自定义节点" + "value" : "熱門 P" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "使用自訂端點" + "value" : "熱門 P" } } } }, - "Use Max Length" : { + "Type your message…" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "استخدام الحد الأقصى للطول" + "value" : "اكتب رسالتك…" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Usa longitud màxima" + "value" : "Escriu el teu missatge…" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Použít maximální délku" + "value" : "Napište zprávu…" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Brug Max-længde" + "value" : "Indtast din besked…" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale Länge verwenden" + "value" : "Nachricht eingeben …" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Χρήση Μέγιστου Μήκους" + "value" : "Πληκτρολογήστε το μήνυμά σας…" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Use Max Length" + "value" : "Type your message…" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Use Max Length" + "value" : "Type your message…" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Use Max Length" + "value" : "Type your message…" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Usar longitud máxima" + "value" : "Escribe tu mensaje…" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Usar longitud máxima" + "value" : "Escribe tu mensaje…" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Käytä maksimipituutta" + "value" : "Kirjoita viestisi…" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser la longueur max" + "value" : "Tapez votre message…" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser la longueur max" + "value" : "Tapez votre message…" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "השתמש באורך מרבי" + "value" : "הקלידו את ההודעה שלכם…" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अधिकतम लंबाई का उपयोग करें" + "value" : "अपना संदेश टाइप करें…" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Koristi maksimalnu duljinu" + "value" : "Unesite poruku…" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Maximális hossz használata" + "value" : "Írja be üzenetét…" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Panjang Maksimum" + "value" : "Ketik pesan Anda…" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Usa lunghezza massima" + "value" : "Digita il tuo messaggio…" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "最大長を使用" + "value" : "メッセージを入力…" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "최대 길이 사용" + "value" : "메시지를 입력하세요…" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Panjang Maksimum" + "value" : "Taip mesej anda…" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Bruk maks lengde" + "value" : "Skriv meldingen din…" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale lengte gebruiken" + "value" : "Typ je bericht…" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Użyj maks. długości" + "value" : "Wpisz swoją wiadomość…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Usar comprimento máximo" + "value" : "Digite sua mensagem…" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Comprimento Máximo" + "value" : "Escreva a sua mensagem…" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Folosește Lungime Maximă" + "value" : "Scrieți mesajul…" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Использовать максимальную длину" + "value" : "Введите сообщение…" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Použiť maximálnu dĺžku" + "value" : "Napíšte svoju správu…" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Använd Maxlängd" + "value" : "Skriv ditt meddelande..." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ใช้ความยาวสูงสุด" + "value" : "พิมพ์ข้อความของคุณ…" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maksimum Uzunluğu Kullan" + "value" : "Mesajınızı yazın…" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Використовувати максимальну довжину" + "value" : "Введіть повідомлення…" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Sử dụng độ dài tối đa" + "value" : "Nhập tin nhắn của bạn…" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "使用最大長度" + "value" : "输入您的消息…" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "使用最大长度" + "value" : "鍵入您的消息…" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "使用最大長度" + "value" : "輸入您的訊息……" } } } }, - "Use Max Messages Limit" : { + "Unknown" : { + "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "استخدم الحد الأقصى للرسائل" + "value" : "غير معروف" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Utilitza el límit màxim de missatges" + "value" : "Desconegut" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Použít maximální limit zpráv" + "value" : "Neznámé" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Brug maksimal grænse for beskeder" + "value" : "Ukendt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Maximale Nachrichtenanzahl verwenden" + "value" : "Unbekannt" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Χρήση Ορίου Μέγιστων Μηνυμάτων" + "value" : "Άγνωστο" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Use Max Messages Limit" + "value" : "Unknown" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Use Max Messages Limit" + "value" : "Unknown" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Enable Max Messages Limit" + "value" : "Unknown" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Usar límite máximo de mensajes" + "value" : "Desconocido" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Límite Máximo de Mensajes" + "value" : "Desconocido" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Käytä viestien enimmäisrajaa" + "value" : "Tuntematon" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser la limite maximale de messages" + "value" : "Inconnu" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser la limite maximale de messages" + "value" : "Inconnu" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "השתמש בגבלת הודעות מקסימלית" + "value" : "לא ידוע" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "अधिकतम संदेश सीमा का उपयोग करें" + "value" : "अज्ञात" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Koristi Ograničenje Maksimalnih Poruka" + "value" : "Nepoznato" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Maximális üzenetlimit használata" + "value" : "Ismeretlen" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Batas Maksimum Pesan" + "value" : "Tidak Diketahui" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Usa limite massimo messaggi" + "value" : "Sconosciuto" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "最大メッセージ制限を使用" + "value" : "不明" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "최대 메시지 제한 사용" + "value" : "알 수 없음" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Had Maksimum Mesej" + "value" : "Tidak diketahui" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Bruk maksgrense for meldinger" + "value" : "Ukjent" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Maximaal aantal berichten gebruiken" + "value" : "Onbekend" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ustaw maksymalny limit wiadomości" + "value" : "Nieznany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Usar limite máximo de mensagens" + "value" : "Desconhecido" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Limite Máximo de Mensagens" + "value" : "Desconhecido" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Folosește limita maximă de mesaje" + "value" : "Necunoscut" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Использовать максимальное ограничение сообщений" + "value" : "Неизвестно" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Použiť maximálny limit správ" + "value" : "Neznáme" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Använd maxgräns för meddelanden" + "value" : "Okänd" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ใช้ขีดจำกัดข้อความสูงสุด" + "value" : "ไม่ทราบ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Maksimum Mesaj Sınırını Kullan" + "value" : "Bilinmeyen" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Використовувати максимальну кількість повідомлень" + "value" : "Невідомо" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Sử dụng Giới hạn Tin nhắn Tối đa" + "value" : "Không xác định" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "使用最大消息限制" + "value" : "未知" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "使用最大消息限制" + "value" : "未知" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "使用最大訊息限制" + "value" : "不明" } } } }, - "Use Repetition Penalty" : { + "Use Custom Endpoint" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "استخدام عقوبة التكرار" + "value" : "استخدام نقطة نهاية مخصصة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Utilitza la pena de repetició" + "value" : "Utilitza el punt final personalitzat" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Použít postih opakování" + "value" : "Použít vlastní koncový bod" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Brug gentagelsesstraf" + "value" : "Brug brugerdefineret slutpunkt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Strafparameter verwenden" + "value" : "Benutzerdefinierten Endpunkt verwenden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Χρήση Ποινής Επανάληψης" + "value" : "Χρήση Προσαρμοσμένου Τερματικού Σημείου" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Use Repetition Penalty" + "value" : "Use Custom Endpoint" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Use Repetition Penalty" + "value" : "Use Custom Endpoint" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Use Repetition Penalty" + "value" : "Use Custom Endpoint" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Penalización por Repetición" + "value" : "Usar punto final personalizado" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Usar penalización de repetición" + "value" : "Usar punto de acceso personalizado" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Käytä toistopenalttia" + "value" : "Käytä mukautettua päätepistettä" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser la pénalité de répétition" + "value" : "Utiliser un point de terminaison personnalisé" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser la Pénalité de Répétition" + "value" : "Utilisateur un point de terminaison personnalisé" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "השתמש בקנס חזרה" + "value" : "השתמש בקצה מותאם אישית" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "दोहराव जुर्माना का उपयोग करें" + "value" : "कस्टम एंडपॉइंट का उपयोग करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Primijeni Kaznu za Ponavljanje" + "value" : "Upotrijebi prilagođenu krajnju točku" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Használjon ismétlési büntetést" + "value" : "Egyéni végpont használata" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Penalti Pengulangan" + "value" : "Gunakan Titik Akhir Kustom" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Usa penalità ripetizione" + "value" : "Usa endpoint personalizzato" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "反復ペナルティを使用" + "value" : "カスタムエンドポイントを使用" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "반복 페널티 사용" + "value" : "사용자 지정 엔드포인트 사용" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Penalti Pengulangan" + "value" : "Gunakan Titik Akhir Tersuai" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Bruk gjentakelsesstraff" + "value" : "Bruk egendefinert endepunkt" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Gebruik Herhalingsboete" + "value" : "Aangepast eindpunt gebruiken" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Użyj kary powtórzeń" + "value" : "Użyj niestandardowego punktu końcowego" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Penalidade de Repetição" + "value" : "Usar Ponto de Extremidade Personalizado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Penalização de Repetição" + "value" : "Usar Endpoint Personalizado" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Folosește Penalizare Repetiție" + "value" : "Utilizați Endpoint Personalizat" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Использовать штраф за повторение" + "value" : "Использовать пользовательский конечный узел" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Použiť trest za opakovanie" + "value" : "Použiť vlastný koncový bod" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Använd repeteringsstraff" + "value" : "Använd anpassad slutpunkt" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ใช้บทลงโทษการทำซ้ำ" + "value" : "ใช้ปลายทางที่กำหนดเอง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Tekrar Cezasını Kullan" + "value" : "Özel Uç Nokta Kullan" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Використовувати штраф за повторення" + "value" : "Використовувати користувацьку кінцеву точку" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Sử dụng Phạt Lặp Lại" + "value" : "Sử Dụng Điểm Kết Nối Tùy Chỉnh" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "使用重複懲罰" + "value" : "使用自定义节点" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "使用重复惩罚" + "value" : "使用自訂端點" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "使用重複懲罰" + "value" : "使用自定端點" } } } }, - "Use System Prompt" : { + "Use Max Length" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "استخدام موجه النظام" + "value" : "استخدام الحد الأقصى للطول" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Utilitza l'indicador del sistema" + "value" : "Usa longitud màxima" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Použít systémovou výzvu" + "value" : "Použít maximální délku" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Brug systemprompt" + "value" : "Brug Max-længde" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Systemaufforderung verwenden" + "value" : "Maximale Länge verwenden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Χρήση Προτροπής Συστήματος" + "value" : "Χρήση Μέγιστου Μήκους" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Use System Prompt" + "value" : "Use Max Length" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Use System Prompt" + "value" : "Use Max Length" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Use System Prompt" + "value" : "Use Max Length" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Usar aviso del sistema" + "value" : "Usar longitud máxima" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Usar aviso del sistema" + "value" : "Usar longitud máxima" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Käytä järjestelmän kehotetta" + "value" : "Käytä maksimipituutta" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser l'invite système" + "value" : "Utiliser la longueur max" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Utiliser l'invite système" + "value" : "Utiliser la longueur max" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "השתמש בהנחיית המערכת" + "value" : "השתמש באורך מרבי" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सिस्टम प्रॉम्प्ट का उपयोग करें" + "value" : "अधिकतम लंबाई का उपयोग करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Koristi sistemske upute" + "value" : "Koristi maksimalnu duljinu" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Rendszerprompt használata" + "value" : "Maximális hossz használata" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Prompt Sistem" + "value" : "Gunakan Panjang Maksimum" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Usa Richiesta di Sistema" + "value" : "Usa lunghezza massima" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "システムプロンプトを使用" + "value" : "最大長を使用" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "시스템 프롬프트 사용" + "value" : "최대 길이 사용" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Gunakan Prompt Sistem" + "value" : "Gunakan Panjang Maksimum" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Bruk systemforespørsel" + "value" : "Bruk maks lengde" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Systeemprompt gebruiken" + "value" : "Maximale lengte gebruiken" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Użyj Monitu Systemowego" + "value" : "Użyj maks. długości" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Prompt do Sistema" + "value" : "Usar comprimento máximo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Usar Indicação do Sistema" + "value" : "Usar Comprimento Máximo" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Utilizați Comanda Sistemului" + "value" : "Folosește Lungime Maximă" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Использовать системную подсказку" + "value" : "Использовать максимальную длину" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Použiť systémovú výzvu" + "value" : "Použiť maximálnu dĺžku" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Använd systemsignal" + "value" : "Använd Maxlängd" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ใช้พร้อมท์ระบบ" + "value" : "ใช้ความยาวสูงสุด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sistem İstemi Kullan" + "value" : "Maksimum Uzunluğu Kullan" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Використовувати системний запит" + "value" : "Використовувати максимальну довжину" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Sử dụng nhắc nhở hệ thống" + "value" : "Sử dụng độ dài tối đa" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "使用系統提示" + "value" : "使用最大长度" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "使用系统提示词" + "value" : "使用最大長度" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "使用系統提示" + "value" : "使用最大長度" } } } }, - "Version %@" : { + "Use Max Messages Limit" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "الإصدار %@" + "value" : "استخدم الحد الأقصى للرسائل" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Versió %@\n" + "value" : "Utilitza el límit màxim de missatges" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Verze %@" + "value" : "Použít maximální limit zpráv" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Version %@" + "value" : "Brug maksimal grænse for beskeder" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Version %@" + "value" : "Maximale Nachrichtenanzahl verwenden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Έκδοση %@\"" + "value" : "Χρήση Ορίου Μέγιστων Μηνυμάτων" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Version %@" + "value" : "Use Max Messages Limit" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Version %@" + "value" : "Use Max Messages Limit" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Version %@" + "value" : "Enable Max Messages Limit" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Versión %@\n" + "value" : "Usar límite máximo de mensajes" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Versión %@" + "value" : "Usar Límite Máximo de Mensajes" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Versio %@" + "value" : "Käytä viestien enimmäisrajaa" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Version %@" + "value" : "Utiliser la limite maximale de messages" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Version %@" + "value" : "Utiliser la limite maximale de messages" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "גרסה %@" + "value" : "השתמש בגבלת הודעות מקסימלית" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "संस्करण %@\n" + "value" : "अधिकतम संदेश सीमा का उपयोग करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Verzija %@" + "value" : "Koristi Ograničenje Maksimalnih Poruka" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Verzió %@" + "value" : "Maximális üzenetlimit használata" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Versi %@" + "value" : "Gunakan Batas Maksimum Pesan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Versione %@" + "value" : "Usa limite massimo messaggi" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "バージョン %@" + "value" : "最大メッセージ制限を使用" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "버전 %@" + "value" : "최대 메시지 제한 사용" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Versi %@" + "value" : "Gunakan Had Maksimum Mesej" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Versjon %@" + "value" : "Bruk maksgrense for meldinger" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Versie %@\"" + "value" : "Maximaal aantal berichten gebruiken" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wersja %@" + "value" : "Ustaw maksymalny limit wiadomości" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Versão %@" + "value" : "Usar limite máximo de mensagens" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Versão %@" + "value" : "Usar Limite Máximo de Mensagens" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Versiunea %@\n" + "value" : "Folosește limita maximă de mesaje" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Версия %@" + "value" : "Использовать максимальное ограничение сообщений" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Verzia %@" + "value" : "Použiť maximálny limit správ" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Version %@" + "value" : "Använd maxgräns för meddelanden" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "เวอร์ชัน %@" + "value" : "ใช้ขีดจำกัดข้อความสูงสุด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Sürüm %@" + "value" : "Maksimum Mesaj Sınırını Kullan" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Версія %@" + "value" : "Використовувати максимальну кількість повідомлень" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Phiên bản %@" + "value" : "Sử dụng Giới hạn Tin nhắn Tối đa" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "版本 %@" + "value" : "使用最大消息限制" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "版本 %@" + "value" : "使用最大訊息限制" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "版本 %@" + "value" : "使用最大消息限制" } } } }, - "Warning" : { + "Use Repetition Penalty" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "تحذير" + "value" : "استخدام عقوبة التكرار" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Advertència" + "value" : "Utilitza la pena de repetició" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Upozornění" + "value" : "Použít postih opakování" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Advarsel" + "value" : "Brug gentagelsesstraf" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Warnung" + "value" : "Strafparameter verwenden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Προειδοποίηση" + "value" : "Χρήση Ποινής Επανάληψης" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Warning" + "value" : "Use Repetition Penalty" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Warning" + "value" : "Use Repetition Penalty" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Caution" + "value" : "Use Repetition Penalty" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Advertencia" + "value" : "Usar Penalización por Repetición" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Advertencia" + "value" : "Usar penalización de repetición" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Varoitus" + "value" : "Käytä toistopenalttia" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Avertissement" + "value" : "Utiliser la pénalité de répétition" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Avertissement" + "value" : "Utiliser la Pénalité de Répétition" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "אזהרה" + "value" : "השתמש בקנס חזרה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "चेतावनी" + "value" : "दोहराव जुर्माना का उपयोग करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Upozorenje" + "value" : "Primijeni Kaznu za Ponavljanje" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Figyelmeztetés" + "value" : "Használjon ismétlési büntetést" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Peringatan" + "value" : "Gunakan Penalti Pengulangan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Avviso" + "value" : "Usa penalità ripetizione" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "警告" + "value" : "反復ペナルティを使用" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "경고" + "value" : "반복 페널티 사용" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Amaran" + "value" : "Gunakan Penalti Pengulangan" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Advarsel" + "value" : "Bruk gjentakelsesstraff" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Waarschuwing" + "value" : "Gebruik Herhalingsboete" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Ostrzeżenie" + "value" : "Użyj kary powtórzeń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Aviso" + "value" : "Usar Penalidade de Repetição" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Aviso" + "value" : "Usar Penalização de Repetição" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Avertisment" + "value" : "Folosește Penalizare Repetiție" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Предупреждение" + "value" : "Использовать штраф за повторение" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Upozornenie" + "value" : "Použiť trest za opakovanie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Varning" + "value" : "Använd repeteringsstraff" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "คำเตือน" + "value" : "ใช้บทลงโทษการทำซ้ำ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Uyarı" + "value" : "Tekrar Cezasını Kullan" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Попередження" + "value" : "Використовувати штраф за повторення" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Cảnh báo" + "value" : "Sử dụng Phạt Lặp Lại" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "警告" + "value" : "使用重复惩罚" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "警告" + "value" : "使用重複懲罰" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "警告" + "value" : "使用重複懲罰" } } } }, - "Window Appearance" : { + "Use System Prompt" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "مظهر النافذة" + "value" : "استخدام موجه النظام" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "Aparença de la finestra" + "value" : "Utilitza l'indicador del sistema" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "Zobrazení okna" + "value" : "Použít systémovou výzvu" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "Vinduesudseende" + "value" : "Brug systemprompt" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "Darstellung des Fensters" + "value" : "Systemaufforderung verwenden" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "Εμφάνιση παραθύρου" + "value" : "Χρήση Προτροπής Συστήματος" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "Window Appearance" + "value" : "Use System Prompt" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "Window Appearance" + "value" : "Use System Prompt" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "Window Appearance" + "value" : "Use System Prompt" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "Apariencia de la Ventana" + "value" : "Usar aviso del sistema" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "Apariencia de la ventana" + "value" : "Usar aviso del sistema" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "Ikkunan ulkoasu" + "value" : "Käytä järjestelmän kehotetta" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Apparence de la fenêtre" + "value" : "Utiliser l'invite système" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "Apparence de la fenêtre" + "value" : "Utiliser l'invite système" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מראה החלון" + "value" : "השתמש בהנחיית המערכת" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "विंडो उपस्थिति" + "value" : "सिस्टम प्रॉम्प्ट का उपयोग करें" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "Prikaz prozora" + "value" : "Koristi sistemske upute" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "Ablak megjelenés" + "value" : "Rendszerprompt használata" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "Tampilan Jendela" + "value" : "Gunakan Prompt Sistem" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "Aspetto della finestra" + "value" : "Usa Richiesta di Sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ウィンドウ表示" + "value" : "システムプロンプトを使用" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "창 모양" + "value" : "시스템 프롬프트 사용" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "Penampilan Tetingkap" + "value" : "Gunakan Prompt Sistem" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "Vinduets utseende" + "value" : "Bruk systemforespørsel" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "Vensterweergave" + "value" : "Systeemprompt gebruiken" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "Wygląd okna" + "value" : "Użyj Monitu Systemowego" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "Aparência da Janela" + "value" : "Usar Prompt do Sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "Aparência da Janela" + "value" : "Usar Indicação do Sistema" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "Aspectul ferestrei" + "value" : "Utilizați Comanda Sistemului" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "Оформление окна" + "value" : "Использовать системную подсказку" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "Vzhľad okna" + "value" : "Použiť systémovú výzvu" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "Fönstrets utseende" + "value" : "Använd systemsignal" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ลักษณะหน้าต่าง" + "value" : "ใช้พร้อมท์ระบบ" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "Pencere Görünümü" + "value" : "Sistem İstemi Kullan" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "Оформлення вікна" + "value" : "Використовувати системний запит" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "Hiển thị cửa sổ" + "value" : "Sử dụng nhắc nhở hệ thống" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "視窗外觀" + "value" : "使用系统提示词" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "窗口样式" + "value" : "使用系統提示" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "視窗外觀" + "value" : "使用系統提示" } } } }, - "https://hf-mirror.com" : { + "Version %@" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "الإصدار %@" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versió %@\n" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Verze %@" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Version %@" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Version %@" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Έκδοση %@\"" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Version %@" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Version %@" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Version %@" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versión %@\n" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versión %@" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versio %@" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Version %@" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Version %@" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "גרסה %@" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "संस्करण %@\n" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Verzija %@" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Verzió %@" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versi %@" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versione %@" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "バージョン %@" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "버전 %@" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versi %@" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versjon %@" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versie %@\"" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Wersja %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versão %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versão %@" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Versiunea %@\n" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Версия %@" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Verzia %@" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Version %@" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "เวอร์ชัน %@" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Sürüm %@" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Версія %@" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "Phiên bản %@" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "版本 %@" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "版本 %@" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "https://hf-mirror.com" + "value" : "版本 %@" } } } }, - "https://huggingface.co" : { + "Warning" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "تحذير" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Advertència" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Upozornění" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Advarsel" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Warnung" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Προειδοποίηση" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Warning" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Warning" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Caution" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Advertencia" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Advertencia" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Varoitus" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Avertissement" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Avertissement" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "אזהרה" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "चेतावनी" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Upozorenje" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Figyelmeztetés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Peringatan" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Avviso" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "警告" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "경고" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Amaran" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Advarsel" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Waarschuwing" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Ostrzeżenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Aviso" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Aviso" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Avertisment" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Предупреждение" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Upozornenie" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Varning" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "คำเตือน" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Uyarı" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Попередження" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "Cảnh báo" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "警告" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "警告" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "https://huggingface.co" + "value" : "警告" } } } }, - "mlx-community/OpenELM-3B" : { + "Window Appearance" : { "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "مظهر النافذة" } }, "ca" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Aparença de la finestra" } }, "cs" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Zobrazení okna" } }, "da" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Vinduesudseende" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Darstellung des Fensters" } }, "el" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Εμφάνιση παραθύρου" } }, "en-AU" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Window Appearance" } }, "en-GB" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Window Appearance" } }, "en-IN" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Window Appearance" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Apariencia de la Ventana" } }, "es-419" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Apariencia de la ventana" } }, "fi" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Ikkunan ulkoasu" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Apparence de la fenêtre" } }, "fr-CA" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Apparence de la fenêtre" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "מראה החלון" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "विंडो उपस्थिति" } }, "hr" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Prikaz prozora" } }, "hu" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Ablak megjelenés" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Tampilan Jendela" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Aspetto della finestra" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "ウィンドウ表示" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "창 모양" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Penampilan Tetingkap" } }, "nb" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Vinduets utseende" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Vensterweergave" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Wygląd okna" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Aparência da Janela" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Aparência da Janela" } }, "ro" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Aspectul ferestrei" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Оформление окна" } }, "sk" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Vzhľad okna" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Fönstrets utseende" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "ลักษณะหน้าต่าง" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Pencere Görünümü" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Оформлення вікна" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "Hiển thị cửa sổ" } }, - "zh-HK" : { + "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "窗口样式" } }, - "zh-Hans" : { + "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "視窗外觀" } }, - "zh-Hant" : { + "zh-HK" : { "stringUnit" : { "state" : "translated", - "value" : "mlx-community/OpenELM-3B" + "value" : "視窗外觀" } } } + }, + "X" : { + } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/ChatMLXCore/Package.swift b/ChatMLXCore/Package.swift index 6b22f47..2e7d3c6 100644 --- a/ChatMLXCore/Package.swift +++ b/ChatMLXCore/Package.swift @@ -49,7 +49,7 @@ let package = Package( .target( name: "Utilities", dependencies: [ - .product(name: "Logging", package: "swift-log"), + .product(name: "Logging", package: "swift-log") ] ), ] diff --git a/ChatMLXCore/Sources/ChatMLXCore/ChatMLXApp.swift b/ChatMLXCore/Sources/ChatMLXCore/ChatMLXApp.swift index e5452d5..33f508c 100644 --- a/ChatMLXCore/Sources/ChatMLXCore/ChatMLXApp.swift +++ b/ChatMLXCore/Sources/ChatMLXCore/ChatMLXApp.swift @@ -10,9 +10,9 @@ import SwiftUI @main struct ChatMLXApp: App { - + var body: some Scene { - WindowGroup{ + WindowGroup { Text("66") } } diff --git a/ChatMLXTests/MarkdownMetadataTests.swift b/ChatMLXTests/MarkdownMetadataTests.swift index 3580083..1fb3305 100644 --- a/ChatMLXTests/MarkdownMetadataTests.swift +++ b/ChatMLXTests/MarkdownMetadataTests.swift @@ -5,25 +5,41 @@ // Created by John Mai on 2024/10/10. // -@testable import ChatMLX import Testing +@testable import ChatMLX + struct MarkdownMetadataTests { @Test func testBasicMetadataParsing() async throws { let markdown = """ - --- - title: Test Title - date: 2024-10-01 - author: John Mai - --- + --- + license: other + license_name: qwen + license_link: https://huggingface.co/Qwen/Qwen2.5-VL-72B-Instruct/blob/main/LICENSE + language: + - en + pipeline_tag: image-text-to-text + tags: + - multimodal + - mlx + library_name: transformers + base_model: + - Qwen/Qwen2.5-VL-72B-Instruct + --- - # Content - This is the content. - """ + # Content + This is the content. + """ - let parser = MarkdownMetadata(markdown: markdown) - #expect(parser.metadata["title"] == "Test Title") - #expect(parser.metadata["date"] == "2024-10-01") - #expect(parser.metadata["author"] == "John Mai") + let metadata = MarkdownMetadata(from: markdown) + + #expect(metadata.string(for: "license") == "other") + #expect(metadata.string(for: "license_name") == "qwen") + #expect(metadata.string(for: "license_link") == "https://huggingface.co/Qwen/Qwen2.5-VL-72B-Instruct/blob/main/LICENSE") + #expect(metadata.array(for: "language") == ["en"]) + #expect(metadata.string(for: "pipeline_tag") == "image-text-to-text") + #expect(metadata.array(for: "tags") == ["multimodal", "mlx"]) + #expect(metadata.string(for: "library_name") == "transformers") + #expect(metadata.array(for: "base_model") == ["Qwen/Qwen2.5-VL-72B-Instruct"]) } } diff --git a/ChatMLXTests/OpenAIServiceTests.swift b/ChatMLXTests/OpenAIServiceTests.swift new file mode 100644 index 0000000..d91a4e8 --- /dev/null +++ b/ChatMLXTests/OpenAIServiceTests.swift @@ -0,0 +1,17 @@ +// +// OpenAIServiceTests.swift +// ChatMLX +// +// Created by John Mai on 2024/11/9. +// + +import Testing + +@testable import ChatMLX + +struct OpenAIServiceTests { + @Test + func fetchModels() async throws { + + } +} diff --git a/ChatMLXTests/ProviderFactoryTests.swift b/ChatMLXTests/ProviderFactoryTests.swift index 62ccd01..066b218 100644 --- a/ChatMLXTests/ProviderFactoryTests.swift +++ b/ChatMLXTests/ProviderFactoryTests.swift @@ -5,12 +5,13 @@ // Created by John Mai on 2024/10/13. // -@testable import ChatMLX import Testing +@testable import ChatMLX + struct ProviderFactoryTests { @Test func mlx() async throws { - let models = await ProviderFactory.shared.provider(.mlx).listModels() - print(models) + // let models = await ProviderFactory.shared.provider(.mlx).listModels() + // print(models) } } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..82f0545 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +format: + swift-format . -i -r \ No newline at end of file