diff --git a/Enchanted.xcodeproj/project.pbxproj b/Enchanted.xcodeproj/project.pbxproj index 054d42b..d27de30 100644 --- a/Enchanted.xcodeproj/project.pbxproj +++ b/Enchanted.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ FF1002732B276EC10011A4DC /* AppColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1002722B276EC10011A4DC /* AppColorScheme.swift */; }; FF1002752B278C170011A4DC /* AppStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF1002742B278C170011A4DC /* AppStore.swift */; }; FF10027A2B27B6070011A4DC /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = FF1002792B27B6070011A4DC /* MarkdownUI */; }; + FF15EF6A2B826C0300D4A541 /* SimpleFloatingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF15EF692B826C0300D4A541 /* SimpleFloatingButton.swift */; }; FF24B30E2B66BE8500AB618F /* RunningBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF24B30D2B66BE8500AB618F /* RunningBorder.swift */; }; FF2F03422B795E0B00349855 /* Clipboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2F03412B795E0B00349855 /* Clipboard.swift */; }; FF2F03442B79631800349855 /* Button+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF2F03432B79631800349855 /* Button+Extension.swift */; }; @@ -46,7 +47,6 @@ FF38F85A2B7AB28300546B56 /* PromptPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF38F8592B7AB28300546B56 /* PromptPanelView.swift */; }; FF38F85C2B7ABC2C00546B56 /* PromptPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF38F85B2B7ABC2C00546B56 /* PromptPanel.swift */; }; FF5FA0D62B35169400BC471D /* Binding+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF5FA0D52B35169400BC471D /* Binding+Extension.swift */; }; - FF61C9632B2A7DC6003CD1CB /* OllamaKit in Frameworks */ = {isa = PBXBuildFile; productRef = FF61C9622B2A7DC6003CD1CB /* OllamaKit */; }; FF66A51D2B76949A00FAAC1E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF66A51C2B76949A00FAAC1E /* Helpers.swift */; }; FF6B7B132B3EE7AC00E8FEA3 /* Throttler.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF6B7B122B3EE7AC00E8FEA3 /* Throttler.swift */; }; FF7FBE4C2B78E384000901F7 /* SamplePrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF7FBE4B2B78E384000901F7 /* SamplePrompt.swift */; }; @@ -61,10 +61,17 @@ FFBBF4882B34F9C8008D611C /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBBF4872B34F9C8008D611C /* View+Extension.swift */; }; FFBBF48A2B350283008D611C /* SelectedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBBF4892B350283008D611C /* SelectedImageView.swift */; }; FFBBF48C2B35051D008D611C /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFBBF48B2B35051D008D611C /* UIImage+Extension.swift */; }; + FFD5FAD22B8130490055AB51 /* Vortex in Frameworks */ = {isa = PBXBuildFile; productRef = FF464B122B8026DA008E5130 /* Vortex */; }; + FFD5FAD52B8130CE0055AB51 /* OllamaKit in Frameworks */ = {isa = PBXBuildFile; productRef = FFD5FAD42B8130CE0055AB51 /* OllamaKit */; }; + FFE21C782B82353A00A69B9C /* SleepTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE21C772B82353A00A69B9C /* SleepTest.swift */; }; FFEC32912B24779A003E5C04 /* EnchantedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEC32902B24779A003E5C04 /* EnchantedApp.swift */; }; FFEC32972B24779B003E5C04 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FFEC32962B24779B003E5C04 /* Assets.xcassets */; }; FFEC329B2B24779B003E5C04 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FFEC329A2B24779B003E5C04 /* Preview Assets.xcassets */; }; FFEC32AA2B24797C003E5C04 /* ChatView_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEC32A92B24797C003E5C04 /* ChatView_iOS.swift */; }; + FFEC9BDF2B8131B900AFBA63 /* HotKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEC9BDE2B8131B900AFBA63 /* HotKeys.swift */; }; + FFEC9BE12B81327B00AFBA63 /* DragAndDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEC9BE02B81327B00AFBA63 /* DragAndDrop.swift */; }; + FFEC9BE32B81358800AFBA63 /* DeallocPrinter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEC9BE22B81358800AFBA63 /* DeallocPrinter.swift */; }; + FFEC9BE72B813A8D00AFBA63 /* RemovableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEC9BE62B813A8D00AFBA63 /* RemovableImage.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -92,6 +99,8 @@ FF10026C2B2751760011A4DC /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; FF1002722B276EC10011A4DC /* AppColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColorScheme.swift; sourceTree = ""; }; FF1002742B278C170011A4DC /* AppStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStore.swift; sourceTree = ""; }; + FF15EF692B826C0300D4A541 /* SimpleFloatingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleFloatingButton.swift; sourceTree = ""; }; + FF15EF6B2B82863400D4A541 /* OllamaKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = OllamaKit; path = ../OllamaKit; sourceTree = ""; }; FF24B30D2B66BE8500AB618F /* RunningBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningBorder.swift; sourceTree = ""; }; FF2F03412B795E0B00349855 /* Clipboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clipboard.swift; sourceTree = ""; }; FF2F03432B79631800349855 /* Button+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Button+Extension.swift"; sourceTree = ""; }; @@ -119,12 +128,17 @@ FFBBF4872B34F9C8008D611C /* View+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extension.swift"; sourceTree = ""; }; FFBBF4892B350283008D611C /* SelectedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedImageView.swift; sourceTree = ""; }; FFBBF48B2B35051D008D611C /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; + FFE21C772B82353A00A69B9C /* SleepTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepTest.swift; sourceTree = ""; }; FFEC328D2B24779A003E5C04 /* Enchanted.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Enchanted.app; sourceTree = BUILT_PRODUCTS_DIR; }; FFEC32902B24779A003E5C04 /* EnchantedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnchantedApp.swift; sourceTree = ""; }; FFEC32962B24779B003E5C04 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; FFEC32982B24779B003E5C04 /* Enchanted.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Enchanted.entitlements; sourceTree = ""; }; FFEC329A2B24779B003E5C04 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; FFEC32A92B24797C003E5C04 /* ChatView_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView_iOS.swift; sourceTree = ""; }; + FFEC9BDE2B8131B900AFBA63 /* HotKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotKeys.swift; sourceTree = ""; }; + FFEC9BE02B81327B00AFBA63 /* DragAndDrop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragAndDrop.swift; sourceTree = ""; }; + FFEC9BE22B81358800AFBA63 /* DeallocPrinter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeallocPrinter.swift; sourceTree = ""; }; + FFEC9BE62B813A8D00AFBA63 /* RemovableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemovableImage.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -132,10 +146,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FFD5FAD52B8130CE0055AB51 /* OllamaKit in Frameworks */, + FFD5FAD22B8130490055AB51 /* Vortex in Frameworks */, FF10027A2B27B6070011A4DC /* MarkdownUI in Frameworks */, FF1002662B2653EE0011A4DC /* ActivityIndicatorView in Frameworks */, FF2F03722B79743400349855 /* Magnet in Frameworks */, - FF61C9632B2A7DC6003CD1CB /* OllamaKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -145,15 +160,16 @@ FF10022C2B2481790011A4DC /* Components */ = { isa = PBXGroup; children = ( - FFB0327B2B312F310066A9DB /* Recorder */, FF10022F2B2482BA0011A4DC /* ChatMessageView.swift */, - FF1002532B261D460011A4DC /* MessageListVIew.swift */, FF10025F2B26499B0011A4DC /* ConversationStatusView.swift */, + FF9300DB2B7823B0000859F4 /* EmptyConversaitonView.swift */, + FF1002532B261D460011A4DC /* MessageListVIew.swift */, FF1002692B2731C60011A4DC /* ModelSelectorView.swift */, - FFBBF4892B350283008D611C /* SelectedImageView.swift */, + FFEC9BE62B813A8D00AFBA63 /* RemovableImage.swift */, FF24B30D2B66BE8500AB618F /* RunningBorder.swift */, - FF9300DB2B7823B0000859F4 /* EmptyConversaitonView.swift */, + FFBBF4892B350283008D611C /* SelectedImageView.swift */, FF9300DD2B782A28000859F4 /* UnreachableAPIView.swift */, + FFB0327B2B312F310066A9DB /* Recorder */, ); path = Components; sourceTree = ""; @@ -213,6 +229,14 @@ path = Settings; sourceTree = ""; }; + FF15EF682B826BF400D4A541 /* Components */ = { + isa = PBXGroup; + children = ( + FF15EF692B826C0300D4A541 /* SimpleFloatingButton.swift */, + ); + path = Components; + sourceTree = ""; + }; FF38F84D2B7A7B5300546B56 /* MenuBar */ = { isa = PBXGroup; children = ( @@ -233,9 +257,28 @@ path = PromptPanel; sourceTree = ""; }; + FF464B142B80BB9C008E5130 /* Helpers */ = { + isa = PBXGroup; + children = ( + FFEC9BE22B81358800AFBA63 /* DeallocPrinter.swift */, + FFEC9BDE2B8131B900AFBA63 /* HotKeys.swift */, + FFE21C772B82353A00A69B9C /* SleepTest.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + FF464B172B810966008E5130 /* Components */ = { + isa = PBXGroup; + children = ( + FFEC9BE02B81327B00AFBA63 /* DragAndDrop.swift */, + ); + path = Components; + sourceTree = ""; + }; FF66A51E2B77788D00FAAC1E /* macOS */ = { isa = PBXGroup; children = ( + FF464B172B810966008E5130 /* Components */, FF38F8542B7AB00100546B56 /* PromptPanel */, FF38F84D2B7A7B5300546B56 /* MenuBar */, FF9300D52B778F1A000859F4 /* Chat */, @@ -281,6 +324,7 @@ FFEC32842B24779A003E5C04 = { isa = PBXGroup; children = ( + FF15EF6B2B82863400D4A541 /* OllamaKit */, FFEC328F2B24779A003E5C04 /* Enchanted */, FFEC328E2B24779A003E5C04 /* Products */, ); @@ -297,11 +341,12 @@ FFEC328F2B24779A003E5C04 /* Enchanted */ = { isa = PBXGroup; children = ( - FFBBF4822B348345008D611C /* Info.plist */, FFEC32982B24779B003E5C04 /* Enchanted.entitlements */, + FFBBF4822B348345008D611C /* Info.plist */, FFEC32962B24779B003E5C04 /* Assets.xcassets */, FFEC32A12B24783B003E5C04 /* Application */, FFEC32A22B247858003E5C04 /* Extensions */, + FF464B142B80BB9C008E5130 /* Helpers */, FFEC32A32B24786D003E5C04 /* Models */, FFEC32992B24779B003E5C04 /* Preview Content */, FFEC32A42B247874003E5C04 /* Services */, @@ -380,6 +425,7 @@ FFEC32A72B247896003E5C04 /* Shared */ = { isa = PBXGroup; children = ( + FF15EF682B826BF400D4A541 /* Components */, FFEC32A82B24795A003E5C04 /* Chat */, FF10026B2B2751630011A4DC /* Settings */, FF1002552B2624790011A4DC /* Sidebar */, @@ -416,8 +462,9 @@ packageProductDependencies = ( FF1002652B2653EE0011A4DC /* ActivityIndicatorView */, FF1002792B27B6070011A4DC /* MarkdownUI */, - FF61C9622B2A7DC6003CD1CB /* OllamaKit */, FF2F03712B79743400349855 /* Magnet */, + FF464B122B8026DA008E5130 /* Vortex */, + FFD5FAD42B8130CE0055AB51 /* OllamaKit */, ); productName = Enchanted; productReference = FFEC328D2B24779A003E5C04 /* Enchanted.app */; @@ -450,8 +497,9 @@ packageReferences = ( FF1002642B2653EE0011A4DC /* XCRemoteSwiftPackageReference "ActivityIndicatorView" */, FF1002782B27B6070011A4DC /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, - FF61C9612B2A7DC6003CD1CB /* XCRemoteSwiftPackageReference "OllamaKit" */, FF2F03702B79743400349855 /* XCRemoteSwiftPackageReference "Magnet" */, + FF464B112B8026DA008E5130 /* XCRemoteSwiftPackageReference "Vortex" */, + FFD5FAD32B8130CE0055AB51 /* XCRemoteSwiftPackageReference "OllamaKit" */, ); productRefGroup = FFEC328E2B24779A003E5C04 /* Products */; projectDirPath = ""; @@ -480,6 +528,7 @@ buildActionMask = 2147483647; files = ( FF1002442B25BB730011A4DC /* ConversationStore.swift in Sources */, + FF15EF6A2B826C0300D4A541 /* SimpleFloatingButton.swift in Sources */, FF24B30E2B66BE8500AB618F /* RunningBorder.swift in Sources */, FF1002602B26499B0011A4DC /* ConversationStatusView.swift in Sources */, FF10024C2B25BEA00011A4DC /* MessageSD.swift in Sources */, @@ -492,12 +541,14 @@ FF10026A2B2731C60011A4DC /* ModelSelectorView.swift in Sources */, FF38F84F2B7A7B6700546B56 /* MenuBarControlView_macOS.swift in Sources */, FFBBF48A2B350283008D611C /* SelectedImageView.swift in Sources */, + FFEC9BDF2B8131B900AFBA63 /* HotKeys.swift in Sources */, FF2F03492B796A6500349855 /* HotkeyService.swift in Sources */, FF9300E02B783945000859F4 /* InputFields_macOS.swift in Sources */, FF38F8562B7AB01E00546B56 /* FloatingPanel.swift in Sources */, FF0146CB2B3DA1DF00A2A9F6 /* Settings.swift in Sources */, FF38F8532B7AA9C400546B56 /* ApplicationEntry.swift in Sources */, FF38F85C2B7ABC2C00546B56 /* PromptPanel.swift in Sources */, + FFEC9BE12B81327B00AFBA63 /* DragAndDrop.swift in Sources */, FF2F03442B79631800349855 /* Button+Extension.swift in Sources */, FF1002682B2668790011A4DC /* Date+Extension.swift in Sources */, FF9300D42B778829000859F4 /* ChatView_macOS.swift in Sources */, @@ -513,10 +564,12 @@ FF9300DE2B782A28000859F4 /* UnreachableAPIView.swift in Sources */, FF9300DC2B7823B0000859F4 /* EmptyConversaitonView.swift in Sources */, FFBBF4882B34F9C8008D611C /* View+Extension.swift in Sources */, + FFEC9BE32B81358800AFBA63 /* DeallocPrinter.swift in Sources */, FFBBF4842B34881B008D611C /* SpeechRecogniser.swift in Sources */, FFEC32AA2B24797C003E5C04 /* ChatView_iOS.swift in Sources */, FF0146CD2B3DADCA00A2A9F6 /* HapticsService.swift in Sources */, FF5FA0D62B35169400BC471D /* Binding+Extension.swift in Sources */, + FFE21C782B82353A00A69B9C /* SleepTest.swift in Sources */, FF1002502B25C79F0011A4DC /* SwiftDataService.swift in Sources */, FF7FBE4C2B78E384000901F7 /* SamplePrompt.swift in Sources */, FF38F8582B7AB1AD00546B56 /* PanelManager.swift in Sources */, @@ -531,6 +584,7 @@ FF1002752B278C170011A4DC /* AppStore.swift in Sources */, FF1002732B276EC10011A4DC /* AppColorScheme.swift in Sources */, FF10025E2B2648460011A4DC /* ConversationState.swift in Sources */, + FFEC9BE72B813A8D00AFBA63 /* RemovableImage.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -693,6 +747,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -738,6 +793,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -791,9 +847,17 @@ kind = branch; }; }; - FF61C9612B2A7DC6003CD1CB /* XCRemoteSwiftPackageReference "OllamaKit" */ = { + FF464B112B8026DA008E5130 /* XCRemoteSwiftPackageReference "Vortex" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/twostraws/Vortex"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; + FFD5FAD32B8130CE0055AB51 /* XCRemoteSwiftPackageReference "OllamaKit" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "git@github.com:AugustDev/OllamaKit.git"; + repositoryURL = "https://github.com/AugustDev/OllamaKit"; requirement = { branch = main; kind = branch; @@ -817,9 +881,14 @@ package = FF2F03702B79743400349855 /* XCRemoteSwiftPackageReference "Magnet" */; productName = Magnet; }; - FF61C9622B2A7DC6003CD1CB /* OllamaKit */ = { + FF464B122B8026DA008E5130 /* Vortex */ = { + isa = XCSwiftPackageProductDependency; + package = FF464B112B8026DA008E5130 /* XCRemoteSwiftPackageReference "Vortex" */; + productName = Vortex; + }; + FFD5FAD42B8130CE0055AB51 /* OllamaKit */ = { isa = XCSwiftPackageProductDependency; - package = FF61C9612B2A7DC6003CD1CB /* XCRemoteSwiftPackageReference "OllamaKit" */; + package = FFD5FAD32B8130CE0055AB51 /* XCRemoteSwiftPackageReference "OllamaKit" */; productName = OllamaKit; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4debbe3..76cc838 100644 --- a/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -37,21 +37,30 @@ } }, { - "identity" : "ollamakit", + "identity" : "sauce", "kind" : "remoteSourceControl", - "location" : "git@github.com:AugustDev/OllamaKit.git", + "location" : "https://github.com/Clipy/Sauce", "state" : { - "branch" : "main", - "revision" : "ff201fd2f542cad507f0906d0a51a8f9c808726f" + "revision" : "8f8fabaa8509c1a653d6c2c3c87396a4c493d876", + "version" : "2.4.0" } }, { - "identity" : "sauce", + "identity" : "swift-docc-plugin", "kind" : "remoteSourceControl", - "location" : "https://github.com/Clipy/Sauce", + "location" : "https://github.com/apple/swift-docc-plugin.git", "state" : { - "revision" : "8f8fabaa8509c1a653d6c2c3c87396a4c493d876", - "version" : "2.4.0" + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" } }, { @@ -62,6 +71,15 @@ "revision" : "5df8a4adedd6ae4eb2455ef60ff75183984daeb8", "version" : "2.2.0" } + }, + { + "identity" : "vortex", + "kind" : "remoteSourceControl", + "location" : "https://github.com/twostraws/Vortex", + "state" : { + "revision" : "3eabf0572fd37e943be64b9d2589f97f1999cb26", + "version" : "1.0.0" + } } ], "version" : 2 diff --git a/Enchanted.xcodeproj/xcshareddata/xcschemes/Enchanted.xcscheme b/Enchanted.xcodeproj/xcshareddata/xcschemes/Enchanted.xcscheme index 33177be..5b57e54 100644 --- a/Enchanted.xcodeproj/xcshareddata/xcschemes/Enchanted.xcscheme +++ b/Enchanted.xcodeproj/xcshareddata/xcschemes/Enchanted.xcscheme @@ -49,6 +49,16 @@ ReferencedContainer = "container:Enchanted.xcodeproj"> + + + + + + some View { configuration.label - .clipShape(Capsule()) .scaleEffect(configuration.isPressed ? 1.2 : 1) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) } diff --git a/Enchanted/Extensions/KeyboardReadable+Extension.swift b/Enchanted/Extensions/KeyboardReadable+Extension.swift index be99463..85de92a 100644 --- a/Enchanted/Extensions/KeyboardReadable+Extension.swift +++ b/Enchanted/Extensions/KeyboardReadable+Extension.swift @@ -11,11 +11,11 @@ import Combine #if os(iOS) /// Publisher to read keyboard changes. protocol KeyboardReadable { - var keyboardPublisher: AnyPublisher { get } + @MainActor var keyboardPublisher: AnyPublisher { get } } extension KeyboardReadable { - var keyboardPublisher: AnyPublisher { + @MainActor var keyboardPublisher: AnyPublisher { Publishers.Merge( NotificationCenter.default .publisher(for: UIResponder.keyboardWillShowNotification) diff --git a/Enchanted/Extensions/NSClipboardItem.swift b/Enchanted/Extensions/NSClipboardItem.swift new file mode 100644 index 0000000..61c423d --- /dev/null +++ b/Enchanted/Extensions/NSClipboardItem.swift @@ -0,0 +1,19 @@ +// +// NSPasteboardItem.swift +// Enchanted +// +// Created by Augustinas Malinauskas on 17/02/2024. +// + +#if os(macOS) +import Foundation +import AppKit + +extension NSPasteboardItem { + func image(forType type: NSPasteboard.PasteboardType) -> Data? { + guard let tiffData = data(forType: type) else { return nil } + let image = NSImage(data: tiffData) + return image?.tiffRepresentation + } +} +#endif diff --git a/Enchanted/Extensions/View+Extension.swift b/Enchanted/Extensions/View+Extension.swift index 27b7910..3ab5d72 100644 --- a/Enchanted/Extensions/View+Extension.swift +++ b/Enchanted/Extensions/View+Extension.swift @@ -7,6 +7,7 @@ import SwiftUI +// MARK: - Conditional View extension View { /// Whether the view should be empty. /// - Parameter bool: Set to `true` to show the view (return EmptyView instead). diff --git a/Enchanted/Helpers/DeallocPrinter.swift b/Enchanted/Helpers/DeallocPrinter.swift new file mode 100644 index 0000000..f8bdd74 --- /dev/null +++ b/Enchanted/Helpers/DeallocPrinter.swift @@ -0,0 +1,20 @@ +// +// DeallocPrinter.swift +// Enchanted +// +// Created by Augustinas Malinauskas on 17/02/2024. +// + +import Foundation + +class DeallocPrinter { + var message: String + + init(_ message: String) { + self.message = message + } + + deinit { + print("deallocated \(message)") + } +} diff --git a/Enchanted/Helpers/HotKeys.swift b/Enchanted/Helpers/HotKeys.swift new file mode 100644 index 0000000..a092221 --- /dev/null +++ b/Enchanted/Helpers/HotKeys.swift @@ -0,0 +1,280 @@ +// +// HotKeys.swift +// Enchanted +// +// Created by Augustinas Malinauskas on 17/02/2024. +// + +#if os(macOS) +import Foundation +import SwiftUI +import Combine + +@available(OSX 11.0, *) +extension View { + func addCustomHotkeys( _ hotkeys: [HotkeyCombination] ) -> some View { + self.modifier(HotKeysMod(hotkeys)) + } +} + +@available(OSX 11.0, *) +public struct HotKeysMod: ViewModifier { + @State var subs = Set() // Cancel onDisappear + var hotkeys: [HotkeyCombination] + + init(_ hotkeys: [HotkeyCombination] ) { + self.hotkeys = hotkeys + } + + public func body(content: Content) -> some View { + ZStack { + DisableSoundsView(hotkeys:hotkeys) + .frame(width: 1, height: 1) + content + } + } +} + +struct DisableSoundsView: NSViewRepresentable { + var hotkeys: [HotkeyCombination] + + func makeNSView(context: Context) -> NSView { + let view = DisableSoundsNSView() + + view.hotkeys = hotkeys + + return view + } + + func updateNSView(_ nsView: NSView, context: Context) { } +} + +class DisableSoundsNSView: NSView { + var hotkeys: [HotkeyCombination] = [] + + override func performKeyEquivalent(with event: NSEvent) -> Bool { + return hotkeysSubscription(combinations: hotkeys) + } +} + +fileprivate func hotkeysSubscription(combinations: [HotkeyCombination]) -> Bool { + for comb in combinations { + let basePressedCorrectly = comb.keyBasePressed + + if basePressedCorrectly && comb.key.isPressed { + comb.action() + return true + } + } + + return false +} + +/////////////////////// +///HELPERS +/////////////////////// +struct HotkeyCombination { + let keyBase: [KeyBase] + let key: CGKeyCode + let action: () -> () +} + +extension HotkeyCombination { + var keyBasePressed: Bool { + let mustBePressed = KeyBase.allCases.filter{ keyBase.contains($0) } + let mustBeNotPressed = KeyBase.allCases.filter{ !keyBase.contains($0) } + + for base in mustBePressed { + if !base.isPressed { + return false + } + } + + for base in mustBeNotPressed { + if base.isPressed { + return false + } + } + + return true + } +} + +enum KeyBase: CaseIterable { + case option + case command + case shift + case control + + var isPressed: Bool { + switch self { + case .option: + return CGKeyCode.kVK_Option.isPressed || CGKeyCode.kVK_RightOption.isPressed + case .command: + return CGKeyCode.kVK_Command.isPressed || CGKeyCode.kVK_RightCommand.isPressed + case .shift: + return CGKeyCode.kVK_Shift.isPressed || CGKeyCode.kVK_RightShift.isPressed + case .control: + return CGKeyCode.kVK_Control.isPressed || CGKeyCode.kVK_RightControl.isPressed + } + } +} + +import Foundation + +///https://gist.github.com/chipjarred/cbb324c797aec865918a8045c4b51d14 +extension CGKeyCode { + static let kVK_ANSI_A : CGKeyCode = 0x00 + static let kVK_ANSI_S : CGKeyCode = 0x01 + static let kVK_ANSI_D : CGKeyCode = 0x02 + static let kVK_ANSI_F : CGKeyCode = 0x03 + static let kVK_ANSI_H : CGKeyCode = 0x04 + static let kVK_ANSI_G : CGKeyCode = 0x05 + static let kVK_ANSI_Z : CGKeyCode = 0x06 + static let kVK_ANSI_X : CGKeyCode = 0x07 + static let kVK_ANSI_C : CGKeyCode = 0x08 + static let kVK_ANSI_V : CGKeyCode = 0x09 + static let kVK_ANSI_B : CGKeyCode = 0x0B + static let kVK_ANSI_Q : CGKeyCode = 0x0C + static let kVK_ANSI_W : CGKeyCode = 0x0D + static let kVK_ANSI_E : CGKeyCode = 0x0E + static let kVK_ANSI_R : CGKeyCode = 0x0F + static let kVK_ANSI_Y : CGKeyCode = 0x10 + static let kVK_ANSI_T : CGKeyCode = 0x11 + static let kVK_ANSI_1 : CGKeyCode = 0x12 + static let kVK_ANSI_2 : CGKeyCode = 0x13 + static let kVK_ANSI_3 : CGKeyCode = 0x14 + static let kVK_ANSI_4 : CGKeyCode = 0x15 + static let kVK_ANSI_6 : CGKeyCode = 0x16 + static let kVK_ANSI_5 : CGKeyCode = 0x17 + static let kVK_ANSI_Equal : CGKeyCode = 0x18 + static let kVK_ANSI_9 : CGKeyCode = 0x19 + static let kVK_ANSI_7 : CGKeyCode = 0x1A + static let kVK_ANSI_Minus : CGKeyCode = 0x1B + static let kVK_ANSI_8 : CGKeyCode = 0x1C + static let kVK_ANSI_0 : CGKeyCode = 0x1D + static let kVK_ANSI_RightBracket : CGKeyCode = 0x1E + static let kVK_ANSI_O : CGKeyCode = 0x1F + static let kVK_ANSI_U : CGKeyCode = 0x20 + static let kVK_ANSI_LeftBracket : CGKeyCode = 0x21 + static let kVK_ANSI_I : CGKeyCode = 0x22 + static let kVK_ANSI_P : CGKeyCode = 0x23 + static let kVK_ANSI_L : CGKeyCode = 0x25 + static let kVK_ANSI_J : CGKeyCode = 0x26 + static let kVK_ANSI_Quote : CGKeyCode = 0x27 + static let kVK_ANSI_K : CGKeyCode = 0x28 + static let kVK_ANSI_Semicolon : CGKeyCode = 0x29 + static let kVK_ANSI_Backslash : CGKeyCode = 0x2A + static let kVK_ANSI_Comma : CGKeyCode = 0x2B + static let kVK_ANSI_Slash : CGKeyCode = 0x2C + static let kVK_ANSI_N : CGKeyCode = 0x2D + static let kVK_ANSI_M : CGKeyCode = 0x2E + static let kVK_ANSI_Period : CGKeyCode = 0x2F + static let kVK_ANSI_Grave : CGKeyCode = 0x32 + static let kVK_ANSI_KeypadDecimal : CGKeyCode = 0x41 + static let kVK_ANSI_KeypadMultiply : CGKeyCode = 0x43 + static let kVK_ANSI_KeypadPlus : CGKeyCode = 0x45 + static let kVK_ANSI_KeypadClear : CGKeyCode = 0x47 + static let kVK_ANSI_KeypadDivide : CGKeyCode = 0x4B + static let kVK_ANSI_KeypadEnter : CGKeyCode = 0x4C + static let kVK_ANSI_KeypadMinus : CGKeyCode = 0x4E + static let kVK_ANSI_KeypadEquals : CGKeyCode = 0x51 + static let kVK_ANSI_Keypad0 : CGKeyCode = 0x52 + static let kVK_ANSI_Keypad1 : CGKeyCode = 0x53 + static let kVK_ANSI_Keypad2 : CGKeyCode = 0x54 + static let kVK_ANSI_Keypad3 : CGKeyCode = 0x55 + static let kVK_ANSI_Keypad4 : CGKeyCode = 0x56 + static let kVK_ANSI_Keypad5 : CGKeyCode = 0x57 + static let kVK_ANSI_Keypad6 : CGKeyCode = 0x58 + static let kVK_ANSI_Keypad7 : CGKeyCode = 0x59 + static let kVK_ANSI_Keypad8 : CGKeyCode = 0x5B + static let kVK_ANSI_Keypad9 : CGKeyCode = 0x5C + + // keycodes for keys that are independent of keyboard layout + static let kVK_Return : CGKeyCode = 0x24 + static let kVK_Tab : CGKeyCode = 0x30 + static let kVK_Space : CGKeyCode = 0x31 + static let kVK_Delete : CGKeyCode = 0x33 + static let kVK_Escape : CGKeyCode = 0x35 + static let kVK_Command : CGKeyCode = 0x37 + static let kVK_Shift : CGKeyCode = 0x38 + static let kVK_CapsLock : CGKeyCode = 0x39 + static let kVK_Option : CGKeyCode = 0x3A + static let kVK_Control : CGKeyCode = 0x3B + static let kVK_RightCommand : CGKeyCode = 0x36 // Out of order + static let kVK_RightShift : CGKeyCode = 0x3C + static let kVK_RightOption : CGKeyCode = 0x3D + static let kVK_RightControl : CGKeyCode = 0x3E + static let kVK_Function : CGKeyCode = 0x3F + static let kVK_F17 : CGKeyCode = 0x40 + static let kVK_VolumeUp : CGKeyCode = 0x48 + static let kVK_VolumeDown : CGKeyCode = 0x49 + static let kVK_Mute : CGKeyCode = 0x4A + static let kVK_F18 : CGKeyCode = 0x4F + static let kVK_F19 : CGKeyCode = 0x50 + static let kVK_F20 : CGKeyCode = 0x5A + static let kVK_F5 : CGKeyCode = 0x60 + static let kVK_F6 : CGKeyCode = 0x61 + static let kVK_F7 : CGKeyCode = 0x62 + static let kVK_F3 : CGKeyCode = 0x63 + static let kVK_F8 : CGKeyCode = 0x64 + static let kVK_F9 : CGKeyCode = 0x65 + static let kVK_F11 : CGKeyCode = 0x67 + static let kVK_F13 : CGKeyCode = 0x69 + static let kVK_F16 : CGKeyCode = 0x6A + static let kVK_F14 : CGKeyCode = 0x6B + static let kVK_F10 : CGKeyCode = 0x6D + static let kVK_F12 : CGKeyCode = 0x6F + static let kVK_F15 : CGKeyCode = 0x71 + static let kVK_Help : CGKeyCode = 0x72 + static let kVK_Home : CGKeyCode = 0x73 + static let kVK_PageUp : CGKeyCode = 0x74 + static let kVK_ForwardDelete : CGKeyCode = 0x75 + static let kVK_F4 : CGKeyCode = 0x76 + static let kVK_End : CGKeyCode = 0x77 + static let kVK_F2 : CGKeyCode = 0x78 + static let kVK_PageDown : CGKeyCode = 0x79 + static let kVK_F1 : CGKeyCode = 0x7A + static let kVK_LeftArrow : CGKeyCode = 0x7B + static let kVK_RightArrow : CGKeyCode = 0x7C + static let kVK_DownArrow : CGKeyCode = 0x7D + static let kVK_UpArrow : CGKeyCode = 0x7E + + // ISO keyboards only + static let kVK_ISO_Section : CGKeyCode = 0x0A + + // JIS keyboards only + static let kVK_JIS_Yen : CGKeyCode = 0x5D + static let kVK_JIS_Underscore : CGKeyCode = 0x5E + static let kVK_JIS_KeypadComma : CGKeyCode = 0x5F + static let kVK_JIS_Eisu : CGKeyCode = 0x66 + static let kVK_JIS_Kana : CGKeyCode = 0x68 + + var isModifier: Bool { + return (.kVK_RightCommand...(.kVK_Function)).contains(self) + } + + var baseModifier: CGKeyCode? + { + if (.kVK_Command...(.kVK_Control)).contains(self) + || self == .kVK_Function + { + return self + } + + switch self + { + case .kVK_RightShift: return .kVK_Shift + case .kVK_RightCommand: return .kVK_Command + case .kVK_RightOption: return .kVK_Option + case .kVK_RightControl: return .kVK_Control + + default: return nil + } + } + + var isPressed: Bool { + CGEventSource.keyState(.combinedSessionState, key: self) + } +} +#endif diff --git a/Enchanted/Helpers/SleepTest.swift b/Enchanted/Helpers/SleepTest.swift new file mode 100644 index 0000000..8788c78 --- /dev/null +++ b/Enchanted/Helpers/SleepTest.swift @@ -0,0 +1,14 @@ +// +// SleepTest.swift +// Enchanted +// +// Created by Augustinas Malinauskas on 18/02/2024. +// + +import Foundation + +func sleepTest(_ name: String) { + print("before \(name)") + sleep(3) + print("after \(name)") +} diff --git a/Enchanted/Services/Clipboard.swift b/Enchanted/Services/Clipboard.swift index 9451b61..42b71ef 100644 --- a/Enchanted/Services/Clipboard.swift +++ b/Enchanted/Services/Clipboard.swift @@ -24,6 +24,30 @@ class Clipboard { let pasteboard = NSPasteboard.general pasteboard.declareTypes([.string], owner: nil) pasteboard.setString(message, forType: .string) +#endif + } + + func getImage() -> PlatformImage? { + #if os(iOS) + if let image = UIPasteboard.general.image { + return image + } +#elseif os(macOS) + let pb = NSPasteboard.general + let type = NSPasteboard.PasteboardType.tiff + guard let imgData = pb.data(forType: type) else { return nil } + return NSImage(data: imgData) +#endif + return nil + } + + func getText() -> String? { +#if os(iOS) + return UIPasteboard.general.string +#elseif os(macOS) + return NSPasteboard.general.string(forType: .string) #endif } } + + diff --git a/Enchanted/Services/HapticsService.swift b/Enchanted/Services/HapticsService.swift index fb4df25..ca27a38 100644 --- a/Enchanted/Services/HapticsService.swift +++ b/Enchanted/Services/HapticsService.swift @@ -8,30 +8,30 @@ #if os(iOS) import UIKit -class Haptics { +class Haptics: @unchecked Sendable { static let shared = Haptics() private init() { } - private func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) { + @MainActor private func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) { let vibrations = UserDefaults.standard.bool(forKey: "vibrations") if vibrations { UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred() } } - private func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) { + @MainActor private func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) { let vibrations = UserDefaults.standard.bool(forKey: "vibrations") if vibrations { UINotificationFeedbackGenerator().notificationOccurred(feedbackType) } } - func lightTap() { + @MainActor func lightTap() { play(.light) } - func mediumTap() { + @MainActor func mediumTap() { play(.medium) } } diff --git a/Enchanted/Services/HotkeyService.swift b/Enchanted/Services/HotkeyService.swift index f6b979b..0ff4dfa 100644 --- a/Enchanted/Services/HotkeyService.swift +++ b/Enchanted/Services/HotkeyService.swift @@ -13,12 +13,11 @@ import SwiftUI final class HotkeyService { static let shared = HotkeyService() - func register(callback: @escaping () -> ()?) { + @MainActor func register(callback: @escaping () -> ()?) { NSApp.setActivationPolicy(.regular) NSApp.activate(ignoringOtherApps: true) if let keyCombo = KeyCombo(key: .k, cocoaModifiers: [.command, .control]) { let hotKey = HotKey(identifier: "CommandControlK", keyCombo: keyCombo) { hotKey in - print("invoked") callback() } hotKey.register() diff --git a/Enchanted/Services/OllamaService.swift b/Enchanted/Services/OllamaService.swift index 8f585c5..3afa595 100644 --- a/Enchanted/Services/OllamaService.swift +++ b/Enchanted/Services/OllamaService.swift @@ -8,29 +8,28 @@ import Foundation import OllamaKit -struct OllamaService { - static var shared = OllamaService() +class OllamaService: @unchecked Sendable { + static let shared = OllamaService() - static func reinit(url: String) { - OllamaService.shared = OllamaService() - } - - let ollamaKit: OllamaKit + var ollamaKit: OllamaKit init() { - var ollamaUrl = "http://localhost" - if var endpoint = UserDefaults.standard.string(forKey: "ollamaUri") { - if !endpoint.contains("http") { - endpoint = "http://" + endpoint + ollamaKit = OllamaKit(baseURL: URL(string: "http://localhost:11434")!) + initEndpoint() + } + + func initEndpoint(url: String? = nil) { + let defaultUrl = "http://localhost:11434" + let localStorageUrl = UserDefaults.standard.string(forKey: "ollamaUri") + if var ollamaUrl = [localStorageUrl, defaultUrl].compactMap({$0}).filter({$0.count > 0}).first { + if !ollamaUrl.contains("http") { + ollamaUrl = "http://" + ollamaUrl + } + + if let url = URL(string: ollamaUrl) { + ollamaKit = OllamaKit(baseURL: url) + return } - ollamaUrl = endpoint - } - - print("url", ollamaUrl) - if let url = URL(string: ollamaUrl) { - ollamaKit = OllamaKit(baseURL: url) - } else { - ollamaKit = OllamaKit(baseURL: URL(string: "http://localhost")!) } } @@ -43,6 +42,4 @@ struct OllamaService { func reachable() async -> Bool { return await ollamaKit.reachable() } - - func chat(prompt: String, model: String) {} } diff --git a/Enchanted/Services/SwiftDataService.swift b/Enchanted/Services/SwiftDataService.swift index 5fc6f1a..bef9a14 100644 --- a/Enchanted/Services/SwiftDataService.swift +++ b/Enchanted/Services/SwiftDataService.swift @@ -8,17 +8,13 @@ import Foundation import SwiftData -actor SwiftDataService { - @MainActor - static let shared = SwiftDataService() - +final actor SwiftDataService: ModelActor { + let modelContainer: ModelContainer + let modelExecutor: ModelExecutor private let modelContext: ModelContext - init(modelContext: ModelContext) { - self.modelContext = modelContext - } - - @MainActor + static let shared = SwiftDataService() + init() { let sharedModelContainer: ModelContainer = { let schema = Schema([ @@ -34,7 +30,11 @@ actor SwiftDataService { fatalError("Could not create ModelContainer: \(error)") } }() - self.modelContext = sharedModelContainer.mainContext + + self.modelContext = ModelContext(sharedModelContainer) + self.modelContext.autosaveEnabled = false + modelContainer = sharedModelContainer + modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) } } diff --git a/Enchanted/Stores/AppStore.swift b/Enchanted/Stores/AppStore.swift index 74bdd6e..852a09d 100644 --- a/Enchanted/Stores/AppStore.swift +++ b/Enchanted/Stores/AppStore.swift @@ -25,7 +25,7 @@ final class AppStore { stopCheckingReachability() } - private func startCheckingReachability(interval: TimeInterval = 10.0) { + private func startCheckingReachability(interval: TimeInterval = 5) { timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in Task { [weak self] in let status = await self?.reachable() ?? false diff --git a/Enchanted/Stores/ConversationStore.swift b/Enchanted/Stores/ConversationStore.swift index 8a61abf..c3c8450 100644 --- a/Enchanted/Stores/ConversationStore.swift +++ b/Enchanted/Stores/ConversationStore.swift @@ -12,7 +12,7 @@ import Combine import SwiftUI @Observable -final class ConversationStore { +final class ConversationStore: Sendable { static let shared = ConversationStore(swiftDataService: SwiftDataService.shared) private var swiftDataService: SwiftDataService @@ -21,7 +21,11 @@ final class ConversationStore { /// 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 = "" - private let throttler = Throttler(delay: 0.05) + #if os(macOS) + private let throttler = Throttler(delay: 0.1) + #else + private let throttler = Throttler(delay: 0.1) + #endif var conversationState: ConversationState = .completed var conversations: [ConversationSD] = [] @@ -32,7 +36,6 @@ final class ConversationStore { self.swiftDataService = swiftDataService } - @MainActor func loadConversations() async throws { print("loading conversations") conversations = try await swiftDataService.fetchConversations() @@ -41,8 +44,8 @@ final class ConversationStore { func deleteAllConversations() { Task { - DispatchQueue.main.async { [self] in - messages = [] + DispatchQueue.main.async { [weak self] in + self?.messages = [] } selectedConversation = nil try? await swiftDataService.deleteConversations() @@ -61,6 +64,7 @@ final class ConversationStore { } } + func create(_ conversation: ConversationSD) async throws { try await swiftDataService.createConversation(conversation) } @@ -127,15 +131,16 @@ final class ConversationStore { /// prepare message history for Ollama var messageHistory = conversation.messages .sorted{$0.createdAt < $1.createdAt} - .map{ChatMessage(role: $0.role, content: $0.content)} - + .map{OKChatRequestData.Message(role: OKChatRequestData.Message.Role(rawValue: $0.role) ?? .assistant, content: $0.content)} + + print(messageHistory.map({$0.content})) /// attach selected image to the last Message if let image = image?.render() { if let lastMessage = messageHistory.popLast() { let imagesBase64: [String] = [image.convertImageToBase64String()] - let messageWithImage = ChatMessage(role: lastMessage.role, content: lastMessage.content, images: imagesBase64) + let messageWithImage = OKChatRequestData.Message(role: lastMessage.role, content: lastMessage.content, images: imagesBase64) messageHistory.append(messageWithImage) } } @@ -153,7 +158,7 @@ final class ConversationStore { try? await loadConversations() if await OllamaService.shared.ollamaKit.reachable() { - let request = OkChatRequestData(model: model.name, messages: messageHistory) + let request = OKChatRequestData(model: model.name, messages: messageHistory) generation = OllamaService.shared.ollamaKit.chat(data: request) .sink(receiveCompletion: { [weak self] completion in switch completion { @@ -187,7 +192,7 @@ final class ConversationStore { } } - @MainActor + @MainActor private func handleError(_ errorMessage: String) { guard let lastMesasge = messages.last else { return } lastMesasge.error = true @@ -202,7 +207,7 @@ final class ConversationStore { } } - @MainActor + @MainActor private func handleComplete() { guard let lastMesasge = messages.last else { return } lastMesasge.error = false diff --git a/Enchanted/Stores/LanguageModelStore.swift b/Enchanted/Stores/LanguageModelStore.swift index 3c4e156..3ce825e 100644 --- a/Enchanted/Stores/LanguageModelStore.swift +++ b/Enchanted/Stores/LanguageModelStore.swift @@ -17,8 +17,6 @@ final class LanguageModelStore { var supportsImages = false var selectedModel: LanguageModelSD? - private var imageModelNames = ["llava"] - init(swiftDataService: SwiftDataService) { self.swiftDataService = swiftDataService } @@ -33,8 +31,6 @@ final class LanguageModelStore { } else { selectedModel = nil } - - checkModelFeatures() } @MainActor @@ -47,20 +43,6 @@ final class LanguageModelStore { } } - func checkModelFeatures() { - for modelName in imageModelNames { - if let selectedModelName = selectedModel?.name { - if selectedModelName.contains(modelName) { - supportsImages = true - return - } - } - } - - supportsImages = false - } - - @MainActor func loadModels() async throws { print("loading models") let localModels = try await swiftDataService.fetchModels() @@ -77,7 +59,7 @@ final class LanguageModelStore { print("completed saveModels()") models = try await swiftDataService.fetchModels() - print("loaded models") + sleepTest("loadModels") } func deleteAllModels() async throws { diff --git a/Enchanted/SwiftData/Models/ConversationSD.swift b/Enchanted/SwiftData/Models/ConversationSD.swift index 91f1753..4e86aa6 100644 --- a/Enchanted/SwiftData/Models/ConversationSD.swift +++ b/Enchanted/SwiftData/Models/ConversationSD.swift @@ -36,3 +36,8 @@ extension ConversationSD { ConversationSD(name: "What is QFT?", updatedAt: Calendar.current.date(byAdding: .day, value: -2, to: Date.now)!) ] } + +// MARK: - @unchecked Sendable +extension ConversationSD: @unchecked Sendable { + /// We hide compiler warnings for concurency. We have to make sure to modify the data only via SwiftDataManager to ensure concurrent operations. +} diff --git a/Enchanted/SwiftData/Models/LanguageModelSD.swift b/Enchanted/SwiftData/Models/LanguageModelSD.swift index 3e122b5..1a55e41 100644 --- a/Enchanted/SwiftData/Models/LanguageModelSD.swift +++ b/Enchanted/SwiftData/Models/LanguageModelSD.swift @@ -25,6 +25,7 @@ final class LanguageModelSD: Identifiable { } } +// MARK: - Helpers extension LanguageModelSD { var prettyName: String { guard let modelName = name.components(separatedBy: ":").first else { @@ -42,9 +43,24 @@ extension LanguageModelSD { return "" } + var supportsImages: Bool { + let imageSupportedModels = ["llava"] + for modelName in imageSupportedModels { + if name.contains(modelName) { + return true + } + } + return false + } + static let sample: [LanguageModelSD] = [ .init(name: "Llama:latest"), .init(name: "Mistral:latest") ] } + +// MARK: - @unchecked Sendable +extension LanguageModelSD: @unchecked Sendable { + /// We hide compiler warnings for concurency. We have to make sure to modify the data only via SwiftDataManager to ensure concurrent operations. +} diff --git a/Enchanted/SwiftData/Models/MessageSD.swift b/Enchanted/SwiftData/Models/MessageSD.swift index f97281f..b2859a9 100644 --- a/Enchanted/SwiftData/Models/MessageSD.swift +++ b/Enchanted/SwiftData/Models/MessageSD.swift @@ -44,3 +44,8 @@ extension MessageSD { .init(content: "Elementary particle is defined as an irreducible representation of the poincase group.", role: "assistant") ] } + +// MARK: - @unchecked Sendable +extension MessageSD: @unchecked Sendable { + /// We hide compiler warnings for concurency. We have to make sure to modify the data only via SwiftDataManager to ensure concurrent operations. +} diff --git a/Enchanted/UI/Shared/Chat/Chat.swift b/Enchanted/UI/Shared/Chat/Chat.swift index 5f52fc4..d5b1235 100644 --- a/Enchanted/UI/Shared/Chat/Chat.swift +++ b/Enchanted/UI/Shared/Chat/Chat.swift @@ -25,10 +25,12 @@ struct Chat: View { withAnimation(.spring) { showMenu.toggle() } - Haptics.shared.mediumTap() + Task { + await Haptics.shared.mediumTap() + } } - @MainActor + @MainActor func sendMessage(prompt: String, model: LanguageModelSD, image: Image?, trimmingMessageId: String?) { conversationStore.sendPrompt( userPrompt: prompt, @@ -44,10 +46,10 @@ struct Chat: View { Task { try await conversationStore.selectConversation(conversation) await languageModelStore.setModel(model: conversation.model) + await Haptics.shared.mediumTap() } showMenu.toggle() } - Haptics.shared.mediumTap() } @MainActor func onStopGenerateTap() { @@ -57,8 +59,8 @@ struct Chat: View { func onConversationDelete(_ conversation: ConversationSD) { Task { + await Haptics.shared.mediumTap() try? await conversationStore.delete(conversation) - Haptics.shared.mediumTap() } } @@ -66,9 +68,9 @@ struct Chat: View { withAnimation(.easeOut(duration: 0.3)) { conversationStore.selectedConversation = nil } - Haptics.shared.mediumTap() Task { + await Haptics.shared.mediumTap() try? await languageModelStore.loadModels() } } diff --git a/Enchanted/UI/Shared/Chat/Components/ChatMessageView.swift b/Enchanted/UI/Shared/Chat/Components/ChatMessageView.swift index 2b3ea82..31ae7cc 100644 --- a/Enchanted/UI/Shared/Chat/Components/ChatMessageView.swift +++ b/Enchanted/UI/Shared/Chat/Components/ChatMessageView.swift @@ -193,6 +193,7 @@ struct ChatMessageView: View { .padding(.bottom, 2) .frame(height: 27) +// Text(text) Markdown(text) .textSelection(.enabled) .markdownTheme(enchantedTheme) @@ -203,13 +204,13 @@ struct ChatMessageView: View { .resizable() .scaledToFit() .frame(width: 100) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: 5)) #elseif os(macOS) Image(nsImage: uiImage) .resizable() .scaledToFit() .frame(width: 100) - .clipShape(RoundedRectangle(cornerRadius: 8)) + .clipShape(RoundedRectangle(cornerRadius: 5)) #endif } diff --git a/Enchanted/UI/Shared/Chat/Components/RemovableImage.swift b/Enchanted/UI/Shared/Chat/Components/RemovableImage.swift new file mode 100644 index 0000000..758ceda --- /dev/null +++ b/Enchanted/UI/Shared/Chat/Components/RemovableImage.swift @@ -0,0 +1,36 @@ +// +// RemovableImage.swift +// Enchanted +// +// Created by Augustinas Malinauskas on 17/02/2024. +// + +import SwiftUI + +struct RemovableImage: View { + var image: Image + var onClick: () -> () + var height: Double = 80 + + var body: some View { + Button(action: {onClick() }) { + ZStack(alignment: .topTrailing) { + image + .resizable() + .scaledToFit() + .frame(height: height) + + Image(systemName: "x.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .padding(5) + } + } + .buttonStyle(.plain) + } +} + +#Preview { + RemovableImage(image: Image(systemName: "star"), onClick: {}) +} diff --git a/Enchanted/UI/Shared/Components/SimpleFloatingButton.swift b/Enchanted/UI/Shared/Components/SimpleFloatingButton.swift new file mode 100644 index 0000000..5eaa0aa --- /dev/null +++ b/Enchanted/UI/Shared/Components/SimpleFloatingButton.swift @@ -0,0 +1,30 @@ +// +// SimpleFloatingButton.swift +// Enchanted +// +// Created by Augustinas Malinauskas on 18/02/2024. +// + +import SwiftUI + +struct SimpleFloatingButton: View { + var systemImage: String + var onClick: () -> () + + var body: some View { + Button(action: onClick) { + Image(systemName: systemImage) + .renderingMode(.template) + .resizable() + .scaledToFit() + .foregroundColor(Color.label) + .frame(height: 18) + } + .buttonStyle(GrowingButton()) + } +} + +#Preview { + SimpleFloatingButton(systemImage: "photo.fill", onClick: {}) + .frame(width: 100, height: 100) +} diff --git a/Enchanted/UI/Shared/Settings/Settings.swift b/Enchanted/UI/Shared/Settings/Settings.swift index 05ced00..7c014f4 100644 --- a/Enchanted/UI/Shared/Settings/Settings.swift +++ b/Enchanted/UI/Shared/Settings/Settings.swift @@ -21,23 +21,23 @@ struct Settings: View { private func save() { #if os(iOS) - Haptics.shared.mediumTap() #endif // remove trailing slash if ollamaUri.last == "/" { ollamaUri = String(ollamaUri.dropLast()) } - OllamaService.reinit(url: ollamaUri) + OllamaService.shared.initEndpoint(url: ollamaUri) Task { - presentationMode.wrappedValue.dismiss() + await Haptics.shared.mediumTap() try? await languageModelStore.loadModels() } + presentationMode.wrappedValue.dismiss() } private func checkServer() { Task { - OllamaService.reinit(url: ollamaUri) + OllamaService.shared.initEndpoint(url: ollamaUri) ollamaStatus = await OllamaService.shared.reachable() try? await languageModelStore.loadModels() } diff --git a/Enchanted/UI/iOS/ChatView_iOS.swift b/Enchanted/UI/iOS/ChatView_iOS.swift index bb9a1e1..3363c55 100644 --- a/Enchanted/UI/iOS/ChatView_iOS.swift +++ b/Enchanted/UI/iOS/ChatView_iOS.swift @@ -19,7 +19,6 @@ struct ChatView: View { var conversationState: ConversationState var onStopGenerateTap: @MainActor () -> () var reachable: Bool - var modelSupportsImages: Bool var onSelectModel: @MainActor (_ model: LanguageModelSD?) -> () private var selectedModel: LanguageModelSD? @@ -29,7 +28,7 @@ struct ChatView: View { @FocusState private var isFocusedInput: Bool /// Image selection - @State private var avatarItem: PhotosPickerItem? + @State private var pickerSelectorActive: PhotosPickerItem? @State private var selectedImage: Image? init( @@ -55,11 +54,32 @@ struct ChatView: View { self.conversationState = conversationState self.onStopGenerateTap = onStopGenerateTap self.reachable = reachable - self.modelSupportsImages = modelSupportsImages self.onSelectModel = onSelectModel self.selectedModel = selectedModel } + private func onMessageSubmit() { + Task { + await Haptics.shared.mediumTap() + + guard let selectedModel = selectedModel else { return } + + await onSendMessageTap( + message, + selectedModel, + selectedImage, + editMessage?.id.uuidString + ) + + withAnimation { + isFocusedInput = false + editMessage = nil + selectedImage = nil + message = "" + } + } + } + var header: some View { HStack(alignment: .center) { Button(action: onMenuTap) { @@ -95,29 +115,30 @@ struct ChatView: View { var inputFields: some View { HStack(spacing: 10) { - PhotosPicker(selection: $avatarItem) { + PhotosPicker(selection: $pickerSelectorActive) { Image(systemName: "photo") .resizable() .scaledToFit() .foregroundStyle(.foreground) .frame(height: 19) } - .onChange(of: avatarItem) { + .onChange(of: pickerSelectorActive) { Task { - if let loaded = try? await avatarItem?.loadTransferable(type: Image.self) { + if let loaded = try? await pickerSelectorActive?.loadTransferable(type: Image.self) { selectedImage = loaded } else { print("Failed") } } } - .showIf(modelSupportsImages) + .showIf(selectedModel?.supportsImages ?? false) + - HStack { SelectedImageView(image: $selectedImage) TextField("Message", text: $message, axis: .vertical) + .autocorrectionDisabled() .focused($isFocusedInput) .frame(minHeight: 40) .font(.system(size: 14)) @@ -139,54 +160,14 @@ struct ChatView: View { style: StrokeStyle(lineWidth: isRecording ? 2 : 0.5) ) ) - - ZStack { - Circle() - .foregroundColor(Color.labelCustom) - .frame(width: 30, height: 30) - - switch conversationState { - case .loading: - Button(action: onStopGenerateTap) { - Image(systemName: "square.fill") - .renderingMode(.template) - .resizable() - .scaledToFit() - .foregroundColor(Color.bgCustom) - .frame(height: 12) - } - default: - Button(action: { - Task { - Haptics.shared.mediumTap() - - guard let selectedModel = selectedModel else { return } - - await onSendMessageTap( - message, - selectedModel, - selectedImage, - editMessage?.id.uuidString - ) - withAnimation { - isFocusedInput = false - editMessage = nil - selectedImage = nil - message = "" - } - } - }) { - Image(systemName: "arrow.up") - .renderingMode(.template) - .resizable() - .scaledToFit() - .foregroundColor(Color.bgCustom) - .frame(height: 15) - } - /// MARK :- Check iOS - .buttonStyle(.plain) - } + switch conversationState { + case .loading: + SimpleFloatingButton(systemImage: "square.fill", onClick: onStopGenerateTap) + .frame(width: 12) + default: + SimpleFloatingButton(systemImage: "paperplane.fill", onClick: onMessageSubmit) + .frame(width: 18) } } } diff --git a/Enchanted/UI/macOS/Chat/Components/InputFields_macOS.swift b/Enchanted/UI/macOS/Chat/Components/InputFields_macOS.swift index 55e2b48..36d8356 100644 --- a/Enchanted/UI/macOS/Chat/Components/InputFields_macOS.swift +++ b/Enchanted/UI/macOS/Chat/Components/InputFields_macOS.swift @@ -17,6 +17,8 @@ struct InputFieldsView: View { @Binding var editMessage: MessageSD? @State private var selectedImage: Image? + @State private var fileDropActive: Bool = false + @State private var fileSelectingActive: Bool = false @FocusState private var isFocusedInput: Bool @MainActor private func sendMessage() { @@ -36,50 +38,80 @@ struct InputFieldsView: View { } } + private func updateSelectedImage(_ image: Image) { + selectedImage = image + } + + var hotkeys: [HotkeyCombination] { + [ + HotkeyCombination(keyBase: [.command], key: .kVK_ANSI_V) { + if let nsImage = Clipboard.shared.getImage() { + let image = Image(nsImage: nsImage) + updateSelectedImage(image) + } + + if let clipboardText = Clipboard.shared.getText() { + message = clipboardText + } + } + ] + } + var body: some View { - HStack { - TextField("Message", text: $message, axis: .vertical) + HStack(spacing: 20) { + if let image = selectedImage { + RemovableImage( + image: image, + onClick: {selectedImage = nil}, + height: 70 + ) + .padding(5) + } + + TextField("Message", text: $message.animation(.easeOut(duration: 0.3)), axis: .vertical) .focused($isFocusedInput) .frame(minHeight: 40) .font(.system(size: 14)) .textFieldStyle(.plain) .onSubmit { - sendMessage() + if NSApp.currentEvent?.modifierFlags.contains(.shift) == true { + message += "\n" + } else { + sendMessage() + } } + /// TextField bypasses drop area + .allowsHitTesting(!fileDropActive) + .addCustomHotkeys(hotkeys) - ZStack { - Circle() - .foregroundColor(Color.labelCustom) - .frame(width: 30, height: 30) - - switch conversationState { - case .loading: - Button(action: onStopGenerateTap) { - Image(systemName: "square.fill") - .renderingMode(.template) - .resizable() - .scaledToFit() - .foregroundColor(Color.bgCustom) - .frame(height: 12) - } - .buttonStyle(.plain) - default: - Button(action: { - Task { - sendMessage() + SimpleFloatingButton(systemImage: "photo.fill", onClick: { fileSelectingActive.toggle() }) + .showIf(selectedModel?.supportsImages ?? false) + .fileImporter(isPresented: $fileSelectingActive, + allowedContentTypes: [.png, .jpeg, .tiff], + onCompletion: { result in + switch result { + case .success(let url): + guard url.startAccessingSecurityScopedResource() else { return } + if let imageData = try? Data(contentsOf: url), + let nsImage = NSImage(data: imageData) { + selectedImage = Image(nsImage: nsImage) } - }) { - Image(systemName: "arrow.up") - .renderingMode(.template) - .resizable() - .scaledToFit() - .foregroundColor(Color.bgCustom) - .frame(height: 15) + url.stopAccessingSecurityScopedResource() + case .failure(let error): + print(error) } - .buttonStyle(.plain) - } + }) + + + switch conversationState { + case .loading: + SimpleFloatingButton(systemImage: "square.fill", onClick: onStopGenerateTap) + default: + SimpleFloatingButton(systemImage: "paperplane.fill", onClick: { Task { sendMessage() } }) + .showIf(!message.isEmpty) } } + .transition(.slide) .padding(.horizontal) .padding(.vertical, 5) .overlay( @@ -89,6 +121,24 @@ struct InputFieldsView: View { style: StrokeStyle(lineWidth: 1) ) ) + .overlay { + if fileDropActive { + DragAndDrop(cornerRadius: 10) + } + } + .animation(.default, value: fileDropActive) + .onDrop(of: [.image], isTargeted: $fileDropActive.animation(), perform: { providers in + guard let provider = providers.first else { return false } + _ = provider.loadDataRepresentation(for: .image) { data, error in + if error == nil, let data { + if let nsImage = NSImage(data: data) { + selectedImage = Image(nsImage: nsImage) + } + } + } + + return true + }) } } diff --git a/Enchanted/UI/macOS/Components/DragAndDrop.swift b/Enchanted/UI/macOS/Components/DragAndDrop.swift new file mode 100644 index 0000000..b7cd776 --- /dev/null +++ b/Enchanted/UI/macOS/Components/DragAndDrop.swift @@ -0,0 +1,42 @@ +// +// DragAndDrop.swift +// Enchanted +// +// Created by Augustinas Malinauskas on 17/02/2024. +// + +import SwiftUI + +struct DragAndDrop: View { + var cornerRadius: CGFloat = 15 + + var body: some View { + ZStack { + Color.clear + + HStack(spacing: 8) { + Image(systemName: "photo") + .font(.system(size: 25)) + Text("Drop your image here") + .font(.title2) + } + .foregroundColor(.label) + } + .overlay( + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(style: StrokeStyle(lineWidth: 2, lineJoin: .round, dash: [10])) + .foregroundColor(.grayCustom) + ) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .padding(5) + .background { + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .fill(.ultraThinMaterial) + } + } +} + +#Preview { + DragAndDrop() + .frame(height: 100) +} diff --git a/Enchanted/UI/macOS/PromptPanel/FloatingPanel.swift b/Enchanted/UI/macOS/PromptPanel/FloatingPanel.swift index bae9192..56720a8 100644 --- a/Enchanted/UI/macOS/PromptPanel/FloatingPanel.swift +++ b/Enchanted/UI/macOS/PromptPanel/FloatingPanel.swift @@ -12,21 +12,16 @@ class FloatingPanel: NSPanel { override func sendEvent(_ event: NSEvent) { super.sendEvent(event) - // Check for left mouse down events - if event.type == .leftMouseDown { - let eventLocation = event.locationInWindow - let localLocation = self.contentView?.convert(eventLocation, from: nil) ?? NSPoint.zero - let isInside = self.contentView?.bounds.contains(localLocation) ?? false - - // If the click is outside the panel, close it - if !isInside { + // escape key closes the panel + if event.type == .keyDown { + if event.keyCode == 53 { self.orderOut(nil) } } } init(contentRect: NSRect, backing: NSWindow.BackingStoreType, defer flag: Bool) { - super.init(contentRect: contentRect, styleMask: [.nonactivatingPanel, .titled, .resizable, .closable, .fullSizeContentView], backing: backing, defer: flag) + super.init(contentRect: contentRect, styleMask: [.nonactivatingPanel, .resizable, .closable, .fullSizeContentView], backing: backing, defer: flag) self.isFloatingPanel = true self.level = .floating self.collectionBehavior.insert(.fullScreenAuxiliary) @@ -41,14 +36,17 @@ class FloatingPanel: NSPanel { self.standardWindowButton(.closeButton)?.isHidden = true self.standardWindowButton(.miniaturizeButton)?.isHidden = true self.standardWindowButton(.zoomButton)?.isHidden = true + self.styleMask.insert(.resizable) } // `canBecomeKey` and `canBecomeMain` are required so that text inputs inside the panel can receive focus override var canBecomeKey: Bool { + print("canBecomeKey") return true } override var canBecomeMain: Bool { + print("canBecomeMain") return true } } diff --git a/Enchanted/UI/macOS/PromptPanel/PanelManager.swift b/Enchanted/UI/macOS/PromptPanel/PanelManager.swift index 37f59aa..dd0b62f 100644 --- a/Enchanted/UI/macOS/PromptPanel/PanelManager.swift +++ b/Enchanted/UI/macOS/PromptPanel/PanelManager.swift @@ -15,9 +15,9 @@ class PanelManager: NSObject, NSApplicationDelegate { super.init() } - @objc func togglePanel() { + @MainActor @objc func togglePanel() { if panel == nil { - createPanel() + showPanel() return } @@ -28,16 +28,17 @@ class PanelManager: NSObject, NSApplicationDelegate { } } - @objc func hidePanel() { + @MainActor @objc func hidePanel() { panel.orderOut(nil) } - @objc func showPanel() { + @MainActor @objc func showPanel() { + createPanel() panel.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } - @objc func onSubmitMessage() { + @MainActor @objc func onSubmitMessage() { hidePanel() /// Focus Enchanted @@ -52,22 +53,57 @@ class PanelManager: NSObject, NSApplicationDelegate { } } - func createPanel() { - let contentView = PromptPanel(onSubmitPanel: onSubmitMessage) - .edgesIgnoringSafeArea(.all) - .padding(.bottom, -28) + @MainActor func createPanel() { + let contentView = PromptPanel(onSubmitPanel: onSubmitMessage, onLayoutUpdate: updatePanelSizeIfNeeded) + let hostingView = NSHostingView(rootView: contentView) + + let idealSize = hostingView.fittingSize - panel = FloatingPanel(contentRect: NSRect(x: 0, y: 0, width: 512, height: 80), backing: .buffered, defer: false) - panel.title = "Floating Panel Title" - panel.contentView = NSHostingView(rootView: contentView) + panel = FloatingPanel(contentRect: NSRect(x: 0, y: 0, width: idealSize.width, height: idealSize.height), backing: .buffered, defer: false) + panel.contentView = hostingView + panel.backgroundColor = .clear panel.center() panel.orderFront(nil) - showPanel() + } + + @MainActor func updatePanelSizeIfNeeded() { + guard let hostingView = panel.contentView as? NSHostingView else { return } + + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return } + let newSize = hostingView.fittingSize + + if newSize == .zero { + return + } + + if strongSelf.panel.frame.size != newSize { + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.2 + context.timingFunction = CAMediaTimingFunction(name: .easeOut) + + // Calculate the difference in height + let heightDifference = newSize.height - strongSelf.panel.frame.size.height + + // Adjust the y position to keep the bottom edge constant + let newY = strongSelf.panel.frame.origin.y - heightDifference + + strongSelf.panel.animator().setFrame( + NSRect(x: strongSelf.panel.frame.origin.x, + y: newY, // Use the new Y + width: newSize.width, + height: newSize.height), + display: true) + }, completionHandler: { + print("Animation completed") + }) + } + } } } extension PanelManager { - func windowDidResignKey(_ notification: Notification) { + @MainActor func windowDidResignKey(_ notification: Notification) { if let panel = notification.object as? FloatingPanel, panel == self.panel { panel.close() } diff --git a/Enchanted/UI/macOS/PromptPanel/PromptPanel.swift b/Enchanted/UI/macOS/PromptPanel/PromptPanel.swift index c3d1e83..6307007 100644 --- a/Enchanted/UI/macOS/PromptPanel/PromptPanel.swift +++ b/Enchanted/UI/macOS/PromptPanel/PromptPanel.swift @@ -5,26 +5,33 @@ // Created by Augustinas Malinauskas on 12/02/2024. // +#if os(macOS) import SwiftUI struct PromptPanel: View { + @AppStorage("colorScheme") private var colorScheme: AppColorScheme = .system @AppStorage("systemPrompt") private var systemPrompt: String = "" @State var conversationStore = ConversationStore.shared @State var languageModelStore = LanguageModelStore.shared var onSubmitPanel: () -> () + var onLayoutUpdate: () -> () @MainActor - func sendMessage(prompt: String) { + func sendMessage(prompt: String, image: Image?) { conversationStore.selectedConversation = nil conversationStore.sendPrompt( userPrompt: prompt, model: languageModelStore.selectedModel!, + image: image, systemPrompt: systemPrompt ) onSubmitPanel() } var body: some View { - PromptPanelView(onSubmit: sendMessage) + PromptPanelView(onSubmit: sendMessage, onLayoutUpdate: onLayoutUpdate, imageSupport: languageModelStore.selectedModel?.supportsImages ?? false) + .preferredColorScheme(colorScheme.toiOSFormat) + .edgesIgnoringSafeArea(.all) } } +#endif diff --git a/Enchanted/UI/macOS/PromptPanel/PromptPanelView.swift b/Enchanted/UI/macOS/PromptPanel/PromptPanelView.swift index 4f87da9..1e99168 100644 --- a/Enchanted/UI/macOS/PromptPanel/PromptPanelView.swift +++ b/Enchanted/UI/macOS/PromptPanel/PromptPanelView.swift @@ -5,44 +5,175 @@ // Created by Augustinas Malinauskas on 12/02/2024. // +#if os(macOS) import SwiftUI +import Vortex struct PromptPanelView: View { @FocusState private var focused: Bool? @State var prompt: String = "" - var onSubmit: @MainActor (_ prompt: String) -> () + var onSubmit: @MainActor (_ prompt: String, _ image: Image?) -> () + var onLayoutUpdate: () -> () + var imageSupport: Bool - var body: some View { + @State private var fileDropActive: Bool = false + @State private var selectedImage: Image? + + var hotkeys: [HotkeyCombination] { + [ + HotkeyCombination(keyBase: [.command], key: .kVK_ANSI_V) { + if let nsImage = Clipboard.shared.getImage() { + let image = Image(nsImage: nsImage) + updateSelectedImage(image) + } + + if let clipboardText = Clipboard.shared.getText() { + prompt = clipboardText + } + } + ] + } + + var imageSupportMissing: some View { HStack { - Image(systemName: "message") + Text("This model does not support images. Supported models are llava and bakllava.") + .font(.caption2) + Spacer() + } + .padding(.top) + } + + private func updateSelectedImage(_ image: Image) { + selectedImage = image + } + + var dynamicFont: Font { + if prompt.count <= 30 { + return .title + } else if prompt.count <= 100 { + return .title2 + } + + return .body + } + + var inputField: some View { + HStack(spacing: 15) { + Image("logo-nobg") .resizable() + .antialiased(true) .scaledToFit() - .frame(width: 30) - .foregroundColor(.grayCustom) + .frame(width: 20) + .foregroundColor(.label) - TextField("How can I help today", text: $prompt) - .font(.title) + TextField("How can I help today?", text: $prompt, axis: .vertical) + .font(dynamicFont) + .minimumScaleFactor(0.4) .focusEffectDisabled() - .padding(8) .background(Color.clear) .focused($focused, equals: true) - .textFieldStyle(PlainTextFieldStyle()) + .textFieldStyle(.plain) + .lineLimit(5, reservesSpace: false) .onSubmit { - onSubmit(prompt) + Task { @MainActor in + if NSApp.currentEvent?.modifierFlags.contains(.shift) == true { + prompt += "\n" + } else { + onSubmit(prompt, selectedImage) + } + } } + /// TextField bypasses drop area + .allowsHitTesting(!fileDropActive) + .layoutPriority(-1) } - .padding(12) - .background { - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(.ultraThinMaterial) + } + + var body: some View { + VStack(spacing: 0) { + // ZStack(alignment: .top) { + // VortexView(.splash.makeUniqueCopy()) { + // Circle() + // .fill(.white) + // .frame(width: 20, height: 20) + // .tag("circle") + // } + // } + // .frame(height: 50) + // .background(Color.clear) + + VStack(alignment: .leading) { + inputField + .layoutPriority(-1) + + DragAndDrop(cornerRadius: 10) + .frame(height: 150) + .layoutPriority(1) + .showIf(fileDropActive) + + if let image = selectedImage { + HStack { + RemovableImage( + image: image, + onClick: {selectedImage = nil}, + height: 150 + ) + .layoutPriority(1) + .transition(.scale) + + Spacer() + } + .layoutPriority(1) + .padding() + .background { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(.thinMaterial) + } + .transition(.slide) + .showIf(!fileDropActive) + } + + imageSupportMissing + .showIf(!imageSupport && selectedImage != nil) + } + .padding(12) + .background { + RoundedRectangle(cornerRadius: 8).fill(.ultraThinMaterial) + } } + .frame(minWidth: 500, maxWidth: 500) .onAppear { prompt = "" focused = true } + .onDrop(of: [.image], isTargeted: $fileDropActive, perform: { providers in + guard let provider = providers.first else { return false } + + _ = provider.loadDataRepresentation(for: .image) { data, error in + if error == nil, let data { + if let nsImage = NSImage(data: data) { + updateSelectedImage(Image(nsImage: nsImage)) + } + } + } + + return true + }) + .addCustomHotkeys(hotkeys) + .onChange(of: prompt) { _, _ in + onLayoutUpdate() + } + .onChange(of: fileDropActive) { _, _ in + onLayoutUpdate() + } + .onChange(of: selectedImage) { _, _ in + onLayoutUpdate() + } } } #Preview { - PromptPanelView(onSubmit: {_ in}) + PromptPanelView(onSubmit: {_,_ in}, onLayoutUpdate: {}, imageSupport: false) } + +#endif