diff --git a/TypeaheadAI.xcodeproj/project.pbxproj b/TypeaheadAI.xcodeproj/project.pbxproj index bc36f65..c27654e 100644 --- a/TypeaheadAI.xcodeproj/project.pbxproj +++ b/TypeaheadAI.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 2B27450A2AB01CF400F37D3E /* SpecialSaveActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2745092AB01CF400F37D3E /* SpecialSaveActor.swift */; }; 2B27450E2AB0380C00F37D3E /* AppContextManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B27450D2AB0380C00F37D3E /* AppContextManager.swift */; }; 2B2745102AB03A3D00F37D3E /* CanSimulateCopy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B27450F2AB03A3D00F37D3E /* CanSimulateCopy.swift */; }; + 2B285D852ACA22FB000C5BDE /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 2B285D842ACA22FB000C5BDE /* LaunchAtLogin */; }; 2B2EF14E2AC17D4000EF2BD4 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2EF14D2AC17D4000EF2BD4 /* CustomTextField.swift */; }; 2B2EF1502AC40C8F00EF2BD4 /* ChatBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2EF14F2AC40C8F00EF2BD4 /* ChatBubble.swift */; }; 2B2EF1522AC40CB500EF2BD4 /* MessagePendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2EF1512AC40CB500EF2BD4 /* MessagePendingView.swift */; }; @@ -55,7 +56,6 @@ 2BDA45C32ABEE840006128BC /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BDA45C22ABEE840006128BC /* MessageView.swift */; }; 2BE0EC222AA0956C00E47C52 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE0EC212AA0956C00E47C52 /* ModalView.swift */; }; 2BE0EC272AA17F9100E47C52 /* MouseClickMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE0EC262AA17F9100E47C52 /* MouseClickMonitor.swift */; }; - 2BF558BE2AB8353B002F2008 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 2BF558BD2AB8353B002F2008 /* LaunchAtLogin */; }; 2BF929792AB04D2600FC105B /* MemoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF929782AB04D2600FC105B /* MemoManager.swift */; }; 2BF9297C2AB13EEA00FC105B /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 2BF9297B2AB13EEA00FC105B /* Markdown */; }; 2BF929802AB13F3600FC105B /* CodeBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF9297F2AB13F3600FC105B /* CodeBlockView.swift */; }; @@ -144,8 +144,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 2B285D852ACA22FB000C5BDE /* LaunchAtLogin in Frameworks */, 2B4BDB892ACBED2100E55D78 /* SettingsAccess in Frameworks */, - 2BF558BE2AB8353B002F2008 /* LaunchAtLogin in Frameworks */, 2BA3C2352AADAC5700537F95 /* llama in Frameworks */, 2B473E8C2AA860380042913D /* MenuBarExtraAccess in Frameworks */, 2BF929852AB13FEC00FC105B /* Highlighter in Frameworks */, @@ -366,7 +366,7 @@ 2BA3C2342AADAC5700537F95 /* llama */, 2BF9297B2AB13EEA00FC105B /* Markdown */, 2BF929842AB13FEC00FC105B /* Highlighter */, - 2BF558BD2AB8353B002F2008 /* LaunchAtLogin */, + 2B285D842ACA22FB000C5BDE /* LaunchAtLogin */, 2B4BDB882ACBED2100E55D78 /* SettingsAccess */, ); productName = TypeaheadAI; @@ -448,7 +448,7 @@ 2BA3C2332AADAC5700537F95 /* XCRemoteSwiftPackageReference "llama" */, 2BF9297A2AB13EEA00FC105B /* XCRemoteSwiftPackageReference "swift-markdown" */, 2BF929832AB13FEC00FC105B /* XCRemoteSwiftPackageReference "HighlighterSwift" */, - 2BF558BC2AB8353B002F2008 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, + 2B285D832ACA22FB000C5BDE /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, 2B4BDB872ACBED2100E55D78 /* XCRemoteSwiftPackageReference "SettingsAccess" */, ); productRefGroup = 2BA7F0762A9ABBA8003D38BA /* Products */; @@ -888,6 +888,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 2B285D832ACA22FB000C5BDE /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; + requirement = { + branch = main; + kind = branch; + }; + }; 2B473E8A2AA860380042913D /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MenuBarExtraAccess"; @@ -920,14 +928,6 @@ minimumVersion = 1.0.0; }; }; - 2BF558BC2AB8353B002F2008 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; - requirement = { - branch = main; - kind = branch; - }; - }; 2BF9297A2AB13EEA00FC105B /* XCRemoteSwiftPackageReference "swift-markdown" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-markdown"; @@ -947,6 +947,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 2B285D842ACA22FB000C5BDE /* LaunchAtLogin */ = { + isa = XCSwiftPackageProductDependency; + package = 2B285D832ACA22FB000C5BDE /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; + productName = LaunchAtLogin; + }; 2B473E8B2AA860380042913D /* MenuBarExtraAccess */ = { isa = XCSwiftPackageProductDependency; package = 2B473E8A2AA860380042913D /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */; @@ -967,11 +972,6 @@ package = 2BA7F0AC2A9ABC47003D38BA /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; productName = KeyboardShortcuts; }; - 2BF558BD2AB8353B002F2008 /* LaunchAtLogin */ = { - isa = XCSwiftPackageProductDependency; - package = 2BF558BC2AB8353B002F2008 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; - productName = LaunchAtLogin; - }; 2BF9297B2AB13EEA00FC105B /* Markdown */ = { isa = XCSwiftPackageProductDependency; package = 2BF9297A2AB13EEA00FC105B /* XCRemoteSwiftPackageReference "swift-markdown" */; diff --git a/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist b/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist index 009bde8..d5ea02b 100644 --- a/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ TypeaheadAI.xcscheme_^#shared#^_ orderHint - 1 + 0 diff --git a/TypeaheadAI/ClientManager.swift b/TypeaheadAI/ClientManager.swift index b37afd3..ec64113 100644 --- a/TypeaheadAI/ClientManager.swift +++ b/TypeaheadAI/ClientManager.swift @@ -405,10 +405,25 @@ class ClientManager { version: "onboarding_v3" ) - if let result: Result = await self?.performOnboardingTask(payload: payload, timeout: timeout, streamHandler: streamHandler) { - completion(result) - } else { - completion(.failure(ClientManagerError.appError("Something went wrong..."))) + do { + let stream = try self?.performOnboardingTask( + payload: payload, + timeout: timeout, + completion: completion + ) + + guard let stream = stream else { + self?.logger.debug("Failed to get stream") + streamHandler(.failure(ClientManagerError.networkError("Failed to connect"))) + return + } + + for try await text in stream { + self?.logger.debug("stream: \(text)") + streamHandler(.success(text)) + } + } catch { + streamHandler(.failure(error)) } } } @@ -592,37 +607,34 @@ class ClientManager { private func performOnboardingTask( payload: OnboardingRequestPayload, timeout: TimeInterval, - streamHandler: @escaping (Result) -> Void - ) async -> Result { + completion: @escaping (Result) -> Void + ) throws -> AsyncThrowingStream { guard let httpBody = try? JSONEncoder().encode(payload) else { - let error: Result = .failure(ClientManagerError.badRequest("Encoding error")) - streamHandler(error) - return error + throw ClientManagerError.badRequest("Encoding error") } - var request = URLRequest(url: self.apiOnboarding, timeoutInterval: timeout) - request.httpMethod = "POST" - request.httpBody = httpBody - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - var output = "" - do { - let (stream, _) = try await URLSession.shared.bytes(for: request) + return AsyncThrowingStream { continuation in + Task { + var urlRequest = URLRequest(url: self.apiOnboarding, timeoutInterval: timeout) + urlRequest.httpMethod = "POST" + urlRequest.httpBody = httpBody + urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") - for try await line in stream.lines { - let decodedResponse = try JSONDecoder().decode(ChunkPayload.self, from: line.data(using: .utf8)!) - if let text = decodedResponse.text { - output += text - streamHandler(.success(text)) + let (result, _) = try await URLSession.shared.bytes(for: urlRequest) + var output = "" + for try await line in result.lines { + if let data = line.data(using: .utf8), + let response = try? JSONDecoder().decode(ChunkPayload.self, from: data), + let text = response.text { + output += text + continuation.yield(text) + } } + + completion(.success(output)) + continuation.finish() } - } catch { - let err: Result = .failure(error) - streamHandler(err) - return err } - - return .success(output) } private func performStreamOfflineTask( diff --git a/TypeaheadAI/Llama/LlamaWrapper.swift b/TypeaheadAI/Llama/LlamaWrapper.swift index 82fd4e0..c1d589c 100644 --- a/TypeaheadAI/Llama/LlamaWrapper.swift +++ b/TypeaheadAI/Llama/LlamaWrapper.swift @@ -50,6 +50,28 @@ class LlamaWrapper { return model != nil } + func stream( + _ prompt: String + ) -> AsyncThrowingStream { + ctx = llama_new_context_with_model(model, params) + return AsyncThrowingStream { continuation in + do { + try simple_predict(ctx, prompt, 1) { string in + continuation.yield(string) + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + + guard let cstr = simple_predict(ctx, prompt, 1, globalHandler) else { + throw LlamaWrapperError.serverError("Failed to run simple_predict") + } + + + } + func predict( _ prompt: String, handler: @escaping (Result) -> Void diff --git a/TypeaheadAI/ModalManager.swift b/TypeaheadAI/ModalManager.swift index 79d7eb3..da8794e 100644 --- a/TypeaheadAI/ModalManager.swift +++ b/TypeaheadAI/ModalManager.swift @@ -134,7 +134,7 @@ class ModalManager: ObservableObject { /// Set an error message. @MainActor - func setError(_ responseError: String) { + func setError(_ responseError: String) async { isPending = false if let idx = messages.indices.last, !messages[idx].isCurrentUser { @@ -277,15 +277,11 @@ class ModalManager: ObservableObject { switch result { case .success(let chunk): - Task { - await self.appendText(chunk) - } self.logger.info("Received chunk: \(chunk)") + await self.appendText(chunk) case .failure(let error): - Task { - self.setError(error.localizedDescription) - } self.logger.error("An error occurred: \(error)") + await self.setError(error.localizedDescription) } }, completion: defaultCompletionHandler) } @@ -482,28 +478,18 @@ class ModalManager: ObservableObject { func defaultHandler(result: Result) { switch result { case .success(let chunk): - Task { - await self.appendText(chunk) - } self.logger.info("Received chunk: \(chunk)") + await self.appendText(chunk) case .failure(let error as ClientManagerError): self.logger.error("Error: \(error.localizedDescription)") switch error { case .badRequest(let message): - DispatchQueue.main.async { - self.setError(message) - } + await self.setError(message) default: - DispatchQueue.main.async { - self.setError("Something went wrong. Please try again.") - } - self.logger.error("Something went wrong.") + await self.setError("Something went wrong. Please try again.") } case .failure(let error): - self.logger.error("Error: \(error.localizedDescription)") - DispatchQueue.main.async { - self.setError(error.localizedDescription) - } + await self.setError(error.localizedDescription) } } } diff --git a/TypeaheadAI/TypeaheadAIApp.swift b/TypeaheadAI/TypeaheadAIApp.swift index 8975d05..8df06ab 100644 --- a/TypeaheadAI/TypeaheadAIApp.swift +++ b/TypeaheadAI/TypeaheadAIApp.swift @@ -16,10 +16,8 @@ import MenuBarExtraAccess @MainActor final class AppState: ObservableObject { @Published var isLoading: Bool = false - @Published var isBlinking: Bool = false @Published var isMenuVisible: Bool = false - private var blinkTimer: Timer? let logger = Logger( subsystem: "ai.typeahead.TypeaheadAI", category: "AppState" @@ -167,22 +165,6 @@ final class AppState: ObservableObject { mouseEventMonitor.stopMonitoring() } - private func startBlinking() { - // Invalidate the previous timer if it exists - blinkTimer?.invalidate() - - // Create and schedule a new timer - blinkTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in - DispatchQueue.main.async { self.isBlinking.toggle() } - } - } - - private func stopBlinking() { - blinkTimer?.invalidate() - blinkTimer = nil - DispatchQueue.main.async { self.isBlinking = false } - } - private func checkAndRequestNotificationPermissions() -> Void { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in if granted { @@ -197,15 +179,11 @@ final class AppState: ObservableObject { @main struct TypeaheadAIApp { static func main() { - if #available(macOS 13.0, *) { - if UserDefaults.standard.bool(forKey: "hasOnboardedV3") { - MacOS13AndLaterApp.main() - } else { - UserDefaults.standard.setValue(true, forKey: "hasOnboardedV3") - MacOS13AndLaterAppWithOnboardingV2.main() - } + if false {//if UserDefaults.standard.bool(forKey: "hasOnboardedV3") { + MacOS13AndLaterApp.main() } else { - MacOS12AndEarlierApp.main() + UserDefaults.standard.setValue(true, forKey: "hasOnboardedV3") + MacOS13AndLaterAppWithOnboardingV2.main() } } } @@ -222,58 +200,6 @@ struct SettingsScene: Scene { } } -struct MacOS12AndEarlierApp: App { - @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - - var body: some Scene { - SettingsScene(appState: appDelegate.appState) - } -} - -class AppDelegate: NSObject, NSApplicationDelegate { - var statusBarItem: NSStatusItem? - var application: NSApplication = NSApplication.shared - - let persistenceController = PersistenceController.shared - @ObservedObject var appState: AppState = { - let context = PersistenceController.shared.container.viewContext - return AppState(context: context) - }() - - override init() { - // Further customization if needed. - super.init() - } - - func applicationDidFinishLaunching(_ notification: Notification) { - let menu = NSMenu() - let menuItem = NSMenuItem() - - // SwiftUI View - let subview = CommonMenuView( - promptManager: appState.promptManager, - modalManager: appState.modalManager, - isMenuVisible: $appState.isMenuVisible - ) - - let view = NSHostingView(rootView: subview) - view.becomeFirstResponder() - - // Very important! If you don't set the frame the menu won't appear to open. - view.frame = NSRect(x: 0, y: 0, width: 300, height: 400) - menuItem.view = view - - menu.addItem(menuItem) - - NSApp.activate(ignoringOtherApps: true) - - statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - statusBarItem?.button?.image = NSImage(systemSymbolName: appState.isBlinking ? "list.clipboard.fill" : "list.clipboard", accessibilityDescription: nil) - statusBarItem?.menu = menu - } -} - -@available(macOS 13.0, *) struct MacOS13AndLaterApp: App { let persistenceController = PersistenceController.shared @StateObject var appState: AppState @@ -294,7 +220,7 @@ struct MacOS13AndLaterApp: App { isMenuVisible: $appState.isMenuVisible ) } label: { - Image(systemName: appState.isBlinking ? "list.clipboard.fill" : "list.clipboard") + Image(systemName: "list.clipboard") // TODO: Add symbolEffect when available } .menuBarExtraAccess(isPresented: $appState.isMenuVisible) @@ -302,7 +228,6 @@ struct MacOS13AndLaterApp: App { } } -@available(macOS 13.0, *) struct MacOS13AndLaterAppWithOnboardingV2: App { let persistenceController = PersistenceController.shared @StateObject var appState: AppState @@ -317,7 +242,11 @@ struct MacOS13AndLaterAppWithOnboardingV2: App { OnboardingView( modalManager: appState.modalManager ) + .onAppear { + NSApp.activate(ignoringOtherApps: true) + } } + .windowStyle(.hiddenTitleBar) SettingsScene(appState: appState) @@ -328,7 +257,7 @@ struct MacOS13AndLaterAppWithOnboardingV2: App { isMenuVisible: $appState.isMenuVisible ) } label: { - Image(systemName: appState.isBlinking ? "list.clipboard.fill" : "list.clipboard") + Image(systemName: "list.clipboard") // TODO: Add symbolEffect when available } .menuBarExtraAccess(isPresented: $appState.isMenuVisible) diff --git a/TypeaheadAI/Views/Onboarding/OnboardingView.swift b/TypeaheadAI/Views/Onboarding/OnboardingView.swift index 0ab7267..da7fea7 100644 --- a/TypeaheadAI/Views/Onboarding/OnboardingView.swift +++ b/TypeaheadAI/Views/Onboarding/OnboardingView.swift @@ -8,6 +8,11 @@ import SwiftUI import AuthenticationServices +struct VisualEffect: NSViewRepresentable { + func makeNSView(context: Self.Context) -> NSView { return NSVisualEffectView() } + func updateNSView(_ nsView: NSView, context: Context) { } +} + struct OnboardingView: View { @ObservedObject var modalManager: ModalManager @State private var messages: [Message] = [] @@ -155,6 +160,7 @@ struct OnboardingView: View { } .padding() } + .background(VisualEffect().ignoresSafeArea()) } /// Append text to the onboarding messages. Creates a new message if there is nothing to append to. diff --git a/TypeaheadAI/Views/Settings/AccountView.swift b/TypeaheadAI/Views/Settings/AccountView.swift index f5d97a9..cea7cf5 100644 --- a/TypeaheadAI/Views/Settings/AccountView.swift +++ b/TypeaheadAI/Views/Settings/AccountView.swift @@ -7,6 +7,7 @@ import SwiftUI import AuthenticationServices +import LaunchAtLogin struct AccountView: View { @AppStorage("token") var token: String? @@ -20,7 +21,7 @@ struct AccountView: View { Divider() - Text(token == nil ? "You are not signed in." : "You're signed in through Apple iCloud!") + Text(token == nil ? "You are not signed in." : "You're signed in with Apple iCloud.") if token == nil { SignInWithAppleButton(.signIn) { request in @@ -45,6 +46,10 @@ struct AccountView: View { token = nil } } + + Spacer() + + LaunchAtLogin.Toggle() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding(10)