From 84ba49251e5dff9324b1ca37cbd2f85722a4c96c Mon Sep 17 00:00:00 2001 From: Augustinas Malinauskas Date: Sun, 10 Mar 2024 13:11:01 +0000 Subject: [PATCH] Fix/accessibility (#38) * fix: optimisation * fix: entitlements and accessibility * fixes * merge --- Enchanted.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 105 ------------------ .../xcshareddata/xcschemes/Enchanted.xcscheme | 1 + Enchanted/Enchanted.entitlements | 2 - Enchanted/Helpers/Accessibility.swift | 3 +- Enchanted/Stores/AppStore.swift | 8 +- Enchanted/Stores/ConversationStore.swift | 68 +++++++----- Enchanted/Stores/LanguageModelStore.swift | 17 ++- Enchanted/UI/Shared/Chat/Chat.swift | 6 +- .../Components/Recorder/RecordingView.swift | 4 +- Enchanted/UI/iOS/ChatView_iOS.swift | 3 +- .../CompletionsEditor/CompletionsEditor.swift | 4 +- .../CompletionsEditorView.swift | 6 +- 13 files changed, 78 insertions(+), 157 deletions(-) delete mode 100644 Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Enchanted.xcodeproj/project.pbxproj b/Enchanted.xcodeproj/project.pbxproj index 09d3414..0624b1e 100644 --- a/Enchanted.xcodeproj/project.pbxproj +++ b/Enchanted.xcodeproj/project.pbxproj @@ -782,7 +782,7 @@ CODE_SIGN_ENTITLEMENTS = Enchanted/Enchanted.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 14; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Enchanted/Preview Content\""; DEVELOPMENT_TEAM = JDDZ55DT74; @@ -808,7 +808,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.5.4; + MARKETING_VERSION = 1.5.5; PRODUCT_BUNDLE_IDENTIFIER = subj.Enchanted; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -831,7 +831,7 @@ CODE_SIGN_ENTITLEMENTS = Enchanted/Enchanted.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 14; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Enchanted/Preview Content\""; DEVELOPMENT_TEAM = JDDZ55DT74; @@ -857,7 +857,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.5.4; + MARKETING_VERSION = 1.5.5; PRODUCT_BUNDLE_IDENTIFIER = subj.Enchanted; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 9fdc531..0000000 --- a/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,105 +0,0 @@ -{ - "originHash" : "5ab8135bb7491a11d5d5f5e45e52eedcdb6d5d9cc31b72a44d00105aef626d9a", - "pins" : [ - { - "identity" : "activityindicatorview", - "kind" : "remoteSourceControl", - "location" : "https://github.com/exyte/ActivityIndicatorView.git", - "state" : { - "revision" : "9970fd0bb7a05dad0b6566ae1f56937716686b24", - "version" : "1.1.1" - } - }, - { - "identity" : "alamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire.git", - "state" : { - "revision" : "723fa5a6c65812aec4a0d7cc432ee198883b6e00", - "version" : "5.9.0" - } - }, - { - "identity" : "magnet", - "kind" : "remoteSourceControl", - "location" : "https://github.com/AugustDev/Magnet", - "state" : { - "branch" : "master", - "revision" : "4865f86d9baa24684dedacd6677beb2d8b30d88e" - } - }, - { - "identity" : "networkimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/NetworkImage", - "state" : { - "revision" : "7aff8d1b31148d32c5933d75557d42f6323ee3d1", - "version" : "6.0.0" - } - }, - { - "identity" : "ollamakit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/AugustDev/OllamaKit", - "state" : { - "branch" : "main", - "revision" : "d9284533d59c0c0241120baec9f082246e3e37d6" - } - }, - { - "identity" : "sauce", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Clipy/Sauce", - "state" : { - "revision" : "8f8fabaa8509c1a653d6c2c3c87396a4c493d876", - "version" : "2.4.0" - } - }, - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms.git", - "state" : { - "revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-markdown-ui", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gonzalezreal/swift-markdown-ui", - "state" : { - "revision" : "ae799d015a5374708f7b4c85f3294c05f2a564e2", - "version" : "2.3.0" - } - }, - { - "identity" : "vortex", - "kind" : "remoteSourceControl", - "location" : "https://github.com/twostraws/Vortex", - "state" : { - "revision" : "3eabf0572fd37e943be64b9d2589f97f1999cb26", - "version" : "1.0.0" - } - }, - { - "identity" : "wrappinghstack", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ksemianov/WrappingHStack", - "state" : { - "revision" : "3300f68b6bf5f8a75ee7ca8a40f136a558053d10", - "version" : "0.2.0" - } - } - ], - "version" : 3 -} diff --git a/Enchanted.xcodeproj/xcshareddata/xcschemes/Enchanted.xcscheme b/Enchanted.xcodeproj/xcshareddata/xcschemes/Enchanted.xcscheme index 5b57e54..8b893cf 100644 --- a/Enchanted.xcodeproj/xcshareddata/xcschemes/Enchanted.xcscheme +++ b/Enchanted.xcodeproj/xcshareddata/xcschemes/Enchanted.xcscheme @@ -33,6 +33,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + enableThreadSanitizer = "YES" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Enchanted/Enchanted.entitlements b/Enchanted/Enchanted.entitlements index 2fda65e..ee95ab7 100644 --- a/Enchanted/Enchanted.entitlements +++ b/Enchanted/Enchanted.entitlements @@ -2,8 +2,6 @@ - com.apple.security.temporary-exception.apple-events - com.apple.security.app-sandbox com.apple.security.network.client diff --git a/Enchanted/Helpers/Accessibility.swift b/Enchanted/Helpers/Accessibility.swift index 85f1cdb..4aa868e 100644 --- a/Enchanted/Helpers/Accessibility.swift +++ b/Enchanted/Helpers/Accessibility.swift @@ -11,7 +11,7 @@ import AppKit import ApplicationServices import CoreGraphics -class Accessibility { +final class Accessibility { static let shared = Accessibility() /// Check if Enchanted has the right permissions @@ -20,7 +20,6 @@ class Accessibility { return AXIsProcessTrustedWithOptions(options as CFDictionary) } - @MainActor func showAccessibilityInstructionsWindow() { if checkAccessibility() { return diff --git a/Enchanted/Stores/AppStore.swift b/Enchanted/Stores/AppStore.swift index 852a09d..64aa835 100644 --- a/Enchanted/Stores/AppStore.swift +++ b/Enchanted/Stores/AppStore.swift @@ -15,7 +15,7 @@ final class AppStore { private var cancellables = Set() private var timer: Timer? - var isReachable: Bool = true + @MainActor var isReachable: Bool = true init() { startCheckingReachability() @@ -35,8 +35,10 @@ final class AppStore { } private func updateReachable(_ isReachable: Bool) { - withAnimation { - self.isReachable = isReachable + DispatchQueue.main.async { + withAnimation { + self.isReachable = isReachable + } } } diff --git a/Enchanted/Stores/ConversationStore.swift b/Enchanted/Stores/ConversationStore.swift index c3c8450..ba887d7 100644 --- a/Enchanted/Stores/ConversationStore.swift +++ b/Enchanted/Stores/ConversationStore.swift @@ -21,15 +21,15 @@ final class ConversationStore: Sendable { /// For some reason (SwiftUI bug / too frequent UI updates) updating UI for each stream message sometimes freezes the UI. /// Throttling UI updates seem to fix the issue. private var currentMessageBuffer: String = "" - #if os(macOS) +#if os(macOS) private let throttler = Throttler(delay: 0.1) - #else +#else private let throttler = Throttler(delay: 0.1) - #endif +#endif - var conversationState: ConversationState = .completed - var conversations: [ConversationSD] = [] - var selectedConversation: ConversationSD? + @MainActor var conversationState: ConversationState = .completed + @MainActor var conversations: [ConversationSD] = [] + @MainActor var selectedConversation: ConversationSD? @MainActor var messages: [MessageSD] = [] init(swiftDataService: SwiftDataService) { @@ -38,7 +38,10 @@ final class ConversationStore: Sendable { func loadConversations() async throws { print("loading conversations") - conversations = try await swiftDataService.fetchConversations() + let fetchedConversations = try await swiftDataService.fetchConversations() + DispatchQueue.main.async { + self.conversations = fetchedConversations + } print("loaded conversations") } @@ -46,8 +49,8 @@ final class ConversationStore: Sendable { Task { DispatchQueue.main.async { [weak self] in self?.messages = [] + self?.selectedConversation = nil } - selectedConversation = nil try? await swiftDataService.deleteConversations() try? await loadConversations() } @@ -57,8 +60,8 @@ final class ConversationStore: Sendable { Task { DispatchQueue.main.async { [self] in messages = [] + selectedConversation = nil } - selectedConversation = nil try? await swiftDataService.deleteConversations() try? await loadConversations() } @@ -69,29 +72,33 @@ final class ConversationStore: Sendable { try await swiftDataService.createConversation(conversation) } - @MainActor func reloadConversation(_ conversation: ConversationSD) async throws { + func reloadConversation(_ conversation: ConversationSD) async throws { let (messages, selectedConversation) = try await ( swiftDataService.fetchMessages(conversation.id), swiftDataService.getConversation(conversation.id) ) - withAnimation(.easeInOut(duration: 0.3)) { - self.messages = messages - self.selectedConversation = selectedConversation + DispatchQueue.main.async { + withAnimation(.easeInOut(duration: 0.3)) { + self.messages = messages + self.selectedConversation = selectedConversation + } } } - @MainActor func selectConversation(_ conversation: ConversationSD) async throws { + func selectConversation(_ conversation: ConversationSD) async throws { try await reloadConversation(conversation) } func delete(_ conversation: ConversationSD) async throws { - selectedConversation = nil try await swiftDataService.deleteConversation(conversation) - conversations = try await swiftDataService.fetchConversations() + let fetchedConversations = try await swiftDataService.fetchConversations() + DispatchQueue.main.async { + self.selectedConversation = nil + self.conversations = fetchedConversations + } } - // @MainActor @MainActor func stopGenerate() { generation?.cancel() handleComplete() @@ -158,18 +165,21 @@ final class ConversationStore: Sendable { try? await loadConversations() if await OllamaService.shared.ollamaKit.reachable() { - let request = OKChatRequestData(model: model.name, messages: messageHistory) - generation = OllamaService.shared.ollamaKit.chat(data: request) - .sink(receiveCompletion: { [weak self] completion in - switch completion { - case .finished: - self?.handleComplete() - case .failure(let error): - self?.handleError(error.localizedDescription) - } - }, receiveValue: { [weak self] response in - self?.handleReceive(response) - }) + DispatchQueue.global(qos: .background).async { + let request = OKChatRequestData(model: model.name, messages: messageHistory) + self.generation = OllamaService.shared.ollamaKit.chat(data: request) + .sink(receiveCompletion: { [weak self] completion in + switch completion { + case .finished: + self?.handleComplete() + case .failure(let error): + self?.handleError(error.localizedDescription) + } + }, receiveValue: { [weak self] response in + self?.handleReceive(response) + }) + + } } else { self.handleError("Server unreachable") } diff --git a/Enchanted/Stores/LanguageModelStore.swift b/Enchanted/Stores/LanguageModelStore.swift index 926a09a..6d89249 100644 --- a/Enchanted/Stores/LanguageModelStore.swift +++ b/Enchanted/Stores/LanguageModelStore.swift @@ -13,9 +13,9 @@ final class LanguageModelStore { static let shared = LanguageModelStore(swiftDataService: SwiftDataService.shared) private var swiftDataService: SwiftDataService - var models: [LanguageModelSD] = [] - var supportsImages = false - var selectedModel: LanguageModelSD? + @MainActor var models: [LanguageModelSD] = [] + @MainActor var supportsImages = false + @MainActor var selectedModel: LanguageModelSD? init(swiftDataService: SwiftDataService) { self.swiftDataService = swiftDataService @@ -49,6 +49,7 @@ final class LanguageModelStore { print("completed loadLocal()") let remoteModels = try await OllamaService.shared.getModels() print("completed loadRemote()") + print(remoteModels) _ = localModels.map { model in model.isAvailable == remoteModels.contains(model) @@ -58,11 +59,17 @@ final class LanguageModelStore { try await swiftDataService.saveModels(models: updateModelsList) print("completed saveModels()") - models = try await swiftDataService.fetchModels() + let fetchedModels = try await swiftDataService.fetchModels() + + DispatchQueue.main.async { + self.models = fetchedModels + } } func deleteAllModels() async throws { - models = [] + DispatchQueue.main.async { + self.models = [] + } try await swiftDataService.deleteModels() } } diff --git a/Enchanted/UI/Shared/Chat/Chat.swift b/Enchanted/UI/Shared/Chat/Chat.swift index d5b1235..de1f267 100644 --- a/Enchanted/UI/Shared/Chat/Chat.swift +++ b/Enchanted/UI/Shared/Chat/Chat.swift @@ -65,8 +65,10 @@ struct Chat: View { } func newConversation() { - withAnimation(.easeOut(duration: 0.3)) { - conversationStore.selectedConversation = nil + DispatchQueue.main.async { + withAnimation(.easeOut(duration: 0.3)) { + self.conversationStore.selectedConversation = nil + } } Task { diff --git a/Enchanted/UI/Shared/Chat/Components/Recorder/RecordingView.swift b/Enchanted/UI/Shared/Chat/Components/Recorder/RecordingView.swift index be1a938..4c40264 100644 --- a/Enchanted/UI/Shared/Chat/Components/Recorder/RecordingView.swift +++ b/Enchanted/UI/Shared/Chat/Components/Recorder/RecordingView.swift @@ -10,7 +10,7 @@ import SwiftUI import AVFoundation struct RecordingView: View { - @StateObject var speechRecognizer = SpeechRecognizer() + @StateObject var speechRecognizer: SpeechRecognizer @Binding var isRecording: Bool var onComplete: (_ transcription: String) -> () = {_ in} @@ -64,7 +64,7 @@ struct RecordingView: View { struct MeetingView_Previews: PreviewProvider { static var previews: some View { - RecordingView(isRecording: .constant(true)) + RecordingView(speechRecognizer: SpeechRecognizer(), isRecording: .constant(true)) } } diff --git a/Enchanted/UI/iOS/ChatView_iOS.swift b/Enchanted/UI/iOS/ChatView_iOS.swift index 3363c55..48195ec 100644 --- a/Enchanted/UI/iOS/ChatView_iOS.swift +++ b/Enchanted/UI/iOS/ChatView_iOS.swift @@ -26,6 +26,7 @@ struct ChatView: View { @State private var isRecording = false @State private var editMessage: MessageSD? @FocusState private var isFocusedInput: Bool + @StateObject var speechRecognizer = SpeechRecognizer() /// Image selection @State private var pickerSelectorActive: PhotosPickerItem? @@ -143,7 +144,7 @@ struct ChatView: View { .frame(minHeight: 40) .font(.system(size: 14)) - RecordingView(isRecording: $isRecording.animation()) { transcription in + RecordingView(speechRecognizer: speechRecognizer, isRecording: $isRecording.animation()) { transcription in self.message = transcription } } diff --git a/Enchanted/UI/macOS/CompletionsEditor/CompletionsEditor.swift b/Enchanted/UI/macOS/CompletionsEditor/CompletionsEditor.swift index 286c219..5740bb3 100644 --- a/Enchanted/UI/macOS/CompletionsEditor/CompletionsEditor.swift +++ b/Enchanted/UI/macOS/CompletionsEditor/CompletionsEditor.swift @@ -30,7 +30,9 @@ struct CompletionsEditor: View { ) .onAppear { timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { _ in - accessibilityStatus = Accessibility.shared.checkAccessibility() + withAnimation { + accessibilityStatus = Accessibility.shared.checkAccessibility() + } } } .onDisappear { diff --git a/Enchanted/UI/macOS/CompletionsEditor/CompletionsEditorView.swift b/Enchanted/UI/macOS/CompletionsEditor/CompletionsEditorView.swift index 0502c47..b4ce101 100644 --- a/Enchanted/UI/macOS/CompletionsEditor/CompletionsEditorView.swift +++ b/Enchanted/UI/macOS/CompletionsEditor/CompletionsEditorView.swift @@ -113,7 +113,11 @@ struct CompletionsEditorView: View { } } .padding() - .background(RoundedRectangle(cornerRadius: 5).fill(Color.red.opacity(0.8))) + .overlay( + RoundedRectangle(cornerRadius: 5) + .stroke(.red, lineWidth: 1) + ) + .background(RoundedRectangle(cornerRadius: 5).fill(Color.red.opacity(0.05))) .showIf(!accessibilityAccess) } .padding()