diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..f0912a20 Binary files /dev/null and b/.DS_Store differ diff --git a/Enchanted.xcodeproj/project.pbxproj b/Enchanted.xcodeproj/project.pbxproj index 5c0de759..64c07f14 100644 --- a/Enchanted.xcodeproj/project.pbxproj +++ b/Enchanted.xcodeproj/project.pbxproj @@ -7,7 +7,35 @@ objects = { /* Begin PBXBuildFile section */ + 1792B0002D8F4D9200D66667 /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792AFFF2D8F4D9200D66667 /* URL+Extension.swift */; }; + 1792B0022D8F4D9200D66667 /* DocumentStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0012D8F4D9200D66667 /* DocumentStatus.swift */; }; + 1792B0052D8F4D9200D66667 /* DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0042D8F4D9200D66667 /* DataLoader.swift */; }; + 1792B0072D8F4D9200D66667 /* TextSplitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0062D8F4D9200D66667 /* TextSplitter.swift */; }; + 1792B0092D8F4D9200D66667 /* DatabaseSD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0082D8F4D9200D66667 /* DatabaseSD.swift */; }; + 1792B00B2D8F4D9200D66667 /* DocumentSD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B00A2D8F4D9200D66667 /* DocumentSD.swift */; }; + 1792B00D2D8F4D9200D66667 /* RetrievalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B00C2D8F4D9200D66667 /* RetrievalStore.swift */; }; + 1792B00D2D8F4D9200D66667 /* RetrievalStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B00C2D8F4D9200D66667 /* RetrievalStore.swift */; }; + 1792B0102D8F4D9200D66667 /* SVDB in Frameworks */ = {isa = PBXBuildFile; productRef = 1792B00F2D8F4D9200D66667 /* SVDB */; }; + 1792B0122D8F4D9200D66667 /* CircularProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0112D8F4D9200D66667 /* CircularProgressIndicator.swift */; }; + 1792B0182D8F4D9200D66667 /* DatabaseListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0172D8F4D9200D66667 /* DatabaseListView.swift */; }; + 1792B01B2D8F4D9200D66667 /* DeleteDatabaseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B01A2D8F4D9200D66667 /* DeleteDatabaseButton.swift */; }; + 1792B01D2D8F4D9200D66667 /* ImportFilesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B01C2D8F4D9200D66667 /* ImportFilesButton.swift */; }; + 1792B01F2D8F4D9200D66667 /* IndexingStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B01E2D8F4D9200D66667 /* IndexingStatusView.swift */; }; + 1792B0222D8F4D9200D66667 /* DatabaseEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0212D8F4D9200D66667 /* DatabaseEmptyView.swift */; }; + 1792B0242D8F4D9200D66667 /* RetrievalGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0232D8F4D9200D66667 /* RetrievalGuide.swift */; }; + 1792B0262D8F4D9200D66667 /* RetrievalGuideItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0252D8F4D9200D66667 /* RetrievalGuideItem.swift */; }; + 1792B02B2D8F4D9200D66667 /* NewDatabaseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B02A2D8F4D9200D66667 /* NewDatabaseButton.swift */; }; + 1792B02D2D8F4D9200D66667 /* RetrievalHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B02C2D8F4D9200D66667 /* RetrievalHeaderView.swift */; }; + 1792B02F2D8F4D9200D66667 /* RetrievalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B02E2D8F4D9200D66667 /* RetrievalView.swift */; }; + 1792B0312D8F4D9200D66667 /* Retrieval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0302D8F4D9200D66667 /* Retrieval.swift */; }; + 1792B0332D8F4D9200D66667 /* AudioMessageInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0322D8F4D9200D66667 /* AudioMessageInput.swift */; }; + 1792B0352D8F4D9200D66667 /* TextMessageInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0342D8F4D9200D66667 /* TextMessageInput.swift */; }; + 1792B0372D8F4D9200D66667 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0362D8F4D9200D66667 /* DatabaseView.swift */; }; + 1792B0392D8F4D9200D66667 /* MessageEmbedding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B0382D8F4D9200D66667 /* MessageEmbedding.swift */; }; + 1792B03C2D8F4D9200D66667 /* UseDocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B03B2D8F4D9200D66667 /* UseDocumentView.swift */; }; + 1792B03E2D8F4D9200D66667 /* UseDocumentViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1792B03D2D8F4D9200D66667 /* UseDocumentViewItem.swift */; }; 3396E0762BD877D500213816 /* Menus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3396E0752BD877D500213816 /* Menus.swift */; }; + F8924F172D918BE500552A63 /* SVDBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8924F162D918BE100552A63 /* SVDBService.swift */; }; FF0146CB2B3DA1DF00A2A9F6 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0146CA2B3DA1DF00A2A9F6 /* Settings.swift */; }; FF0146CD2B3DADCA00A2A9F6 /* HapticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF0146CC2B3DADCA00A2A9F6 /* HapticsService.swift */; }; FF0611EA2C04A11800443B33 /* Magnet in Frameworks */ = {isa = PBXBuildFile; platformFilters = (macos, ); productRef = FF0611E92C04A11800443B33 /* Magnet */; }; @@ -106,7 +134,33 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1792AFFF2D8F4D9200D66667 /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = ""; }; + 1792B0012D8F4D9200D66667 /* DocumentStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentStatus.swift; sourceTree = ""; }; + 1792B0042D8F4D9200D66667 /* DataLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLoader.swift; sourceTree = ""; }; + 1792B0062D8F4D9200D66667 /* TextSplitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextSplitter.swift; sourceTree = ""; }; + 1792B0082D8F4D9200D66667 /* DatabaseSD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSD.swift; sourceTree = ""; }; + 1792B00A2D8F4D9200D66667 /* DocumentSD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentSD.swift; sourceTree = ""; }; + 1792B00C2D8F4D9200D66667 /* RetrievalStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrievalStore.swift; sourceTree = ""; }; + 1792B0112D8F4D9200D66667 /* CircularProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressIndicator.swift; sourceTree = ""; }; + 1792B0172D8F4D9200D66667 /* DatabaseListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseListView.swift; sourceTree = ""; }; + 1792B01A2D8F4D9200D66667 /* DeleteDatabaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteDatabaseButton.swift; sourceTree = ""; }; + 1792B01C2D8F4D9200D66667 /* ImportFilesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportFilesButton.swift; sourceTree = ""; }; + 1792B01E2D8F4D9200D66667 /* IndexingStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexingStatusView.swift; sourceTree = ""; }; + 1792B0212D8F4D9200D66667 /* DatabaseEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEmptyView.swift; sourceTree = ""; }; + 1792B0232D8F4D9200D66667 /* RetrievalGuide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrievalGuide.swift; sourceTree = ""; }; + 1792B0252D8F4D9200D66667 /* RetrievalGuideItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrievalGuideItem.swift; sourceTree = ""; }; + 1792B02A2D8F4D9200D66667 /* NewDatabaseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewDatabaseButton.swift; sourceTree = ""; }; + 1792B02C2D8F4D9200D66667 /* RetrievalHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrievalHeaderView.swift; sourceTree = ""; }; + 1792B02E2D8F4D9200D66667 /* RetrievalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrievalView.swift; sourceTree = ""; }; + 1792B0302D8F4D9200D66667 /* Retrieval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Retrieval.swift; sourceTree = ""; }; + 1792B0322D8F4D9200D66667 /* AudioMessageInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessageInput.swift; sourceTree = ""; }; + 1792B0342D8F4D9200D66667 /* TextMessageInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageInput.swift; sourceTree = ""; }; + 1792B0362D8F4D9200D66667 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = ""; }; + 1792B0382D8F4D9200D66667 /* MessageEmbedding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEmbedding.swift; sourceTree = ""; }; + 1792B03B2D8F4D9200D66667 /* UseDocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseDocumentView.swift; sourceTree = ""; }; + 1792B03D2D8F4D9200D66667 /* UseDocumentViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UseDocumentViewItem.swift; sourceTree = ""; }; 3396E0752BD877D500213816 /* Menus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Menus.swift; sourceTree = ""; }; + F8924F162D918BE100552A63 /* SVDBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVDBService.swift; sourceTree = ""; }; FF0146CA2B3DA1DF00A2A9F6 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; FF0146CC2B3DADCA00A2A9F6 /* HapticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticsService.swift; sourceTree = ""; }; FF0611F22C04BEFD00443B33 /* AVSpeechSynthesisVoice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVSpeechSynthesisVoice+Extension.swift"; sourceTree = ""; }; @@ -209,6 +263,7 @@ FF0611F12C04A2FB00443B33 /* Splash in Frameworks */, FF79AEB62CE72ECE00EDE10C /* MarkdownUI in Frameworks */, FF0611EA2C04A11800443B33 /* Magnet in Frameworks */, + 1792B0102D8F4D9200D66667 /* SVDB in Frameworks */, FF911DF12B98F08800915E94 /* WrappingHStack in Frameworks */, FF1002662B2653EE0011A4DC /* ActivityIndicatorView in Frameworks */, FFFD00C92B94CB5E00392AE6 /* AsyncAlgorithms in Frameworks */, @@ -219,9 +274,90 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1792B0032D8F4D9200D66667 /* Retrieval */ = { + isa = PBXGroup; + children = ( + 1792B0382D8F4D9200D66667 /* MessageEmbedding.swift */, + 1792B0062D8F4D9200D66667 /* TextSplitter.swift */, + 1792B0042D8F4D9200D66667 /* DataLoader.swift */, + ); + path = Retrieval; + sourceTree = ""; + }; + 1792B0132D8F4D9200D66667 /* Retrieval */ = { + isa = PBXGroup; + children = ( + 1792B0302D8F4D9200D66667 /* Retrieval.swift */, + 1792B0142D8F4D9200D66667 /* Components */, + ); + path = Retrieval; + sourceTree = ""; + }; + 1792B0142D8F4D9200D66667 /* Components */ = { + isa = PBXGroup; + children = ( + 1792B02E2D8F4D9200D66667 /* RetrievalView.swift */, + 1792B0292D8F4D9200D66667 /* RetrievalHeader */, + 1792B0152D8F4D9200D66667 /* DatabaseContainer */, + ); + path = Components; + sourceTree = ""; + }; + 1792B0152D8F4D9200D66667 /* DatabaseContainer */ = { + isa = PBXGroup; + children = ( + 1792B0202D8F4D9200D66667 /* DatabaseEmpty */, + 1792B0162D8F4D9200D66667 /* DatabaseList */, + ); + path = DatabaseContainer; + sourceTree = ""; + }; + 1792B0162D8F4D9200D66667 /* DatabaseList */ = { + isa = PBXGroup; + children = ( + 1792B0362D8F4D9200D66667 /* DatabaseView.swift */, + 1792B01E2D8F4D9200D66667 /* IndexingStatusView.swift */, + 1792B01C2D8F4D9200D66667 /* ImportFilesButton.swift */, + 1792B01A2D8F4D9200D66667 /* DeleteDatabaseButton.swift */, + 1792B0172D8F4D9200D66667 /* DatabaseListView.swift */, + ); + path = DatabaseList; + sourceTree = ""; + }; + 1792B0202D8F4D9200D66667 /* DatabaseEmpty */ = { + isa = PBXGroup; + children = ( + 1792B0252D8F4D9200D66667 /* RetrievalGuideItem.swift */, + 1792B0232D8F4D9200D66667 /* RetrievalGuide.swift */, + 1792B0212D8F4D9200D66667 /* DatabaseEmptyView.swift */, + ); + path = DatabaseEmpty; + sourceTree = ""; + }; + 1792B0292D8F4D9200D66667 /* RetrievalHeader */ = { + isa = PBXGroup; + children = ( + 1792B02C2D8F4D9200D66667 /* RetrievalHeaderView.swift */, + 1792B02A2D8F4D9200D66667 /* NewDatabaseButton.swift */, + ); + path = RetrievalHeader; + sourceTree = ""; + }; + 1792B03A2D8F4D9200D66667 /* UseDocument */ = { + isa = PBXGroup; + children = ( + 1792B03D2D8F4D9200D66667 /* UseDocumentViewItem.swift */, + 1792B03B2D8F4D9200D66667 /* UseDocumentView.swift */, + ); + path = UseDocument; + sourceTree = ""; + }; FF10022C2B2481790011A4DC /* Components */ = { isa = PBXGroup; children = ( + 1792B03A2D8F4D9200D66667 /* UseDocument */, + 1792B0342D8F4D9200D66667 /* TextMessageInput.swift */, + 1792B0322D8F4D9200D66667 /* AudioMessageInput.swift */, FF10025F2B26499B0011A4DC /* ConversationStatusView.swift */, FF9300DB2B7823B0000859F4 /* EmptyConversaitonView.swift */, FF1002532B261D460011A4DC /* MessageListVIew.swift */, @@ -249,6 +385,7 @@ FF1002422B25BAC50011A4DC /* Stores */ = { isa = PBXGroup; children = ( + 1792B00C2D8F4D9200D66667 /* RetrievalStore.swift */, FF1002742B278C170011A4DC /* AppStore.swift */, FFFD00C12B92607800392AE6 /* CompletionsStore.swift */, FF1002432B25BB730011A4DC /* ConversationStore.swift */, @@ -260,6 +397,8 @@ FF1002482B25BE580011A4DC /* Models */ = { isa = PBXGroup; children = ( + 1792B00A2D8F4D9200D66667 /* DocumentSD.swift */, + 1792B0082D8F4D9200D66667 /* DatabaseSD.swift */, FF6D82162B9122F9001183A8 /* CompletionInstructionSD.swift */, FF10024D2B25C2A70011A4DC /* ConversationSD.swift */, FF1002492B25BE740011A4DC /* LanguageModelSD.swift */, @@ -300,6 +439,7 @@ FF15EF682B826BF400D4A541 /* Components */ = { isa = PBXGroup; children = ( + 1792B0112D8F4D9200D66667 /* CircularProgressIndicator.swift */, FF15EF692B826C0300D4A541 /* SimpleFloatingButton.swift */, ); path = Components; @@ -453,6 +593,7 @@ FFEC328F2B24779A003E5C04 /* Enchanted */ = { isa = PBXGroup; children = ( + 1792B0032D8F4D9200D66667 /* Retrieval */, FF9C92072BF0B464004C8275 /* EnchantedDebug.entitlements */, FFEC32982B24779B003E5C04 /* Enchanted.entitlements */, FFE2C8222B9A657A00BD82F3 /* Accessibility.plist */, @@ -490,6 +631,7 @@ FFEC32A22B247858003E5C04 /* Extensions */ = { isa = PBXGroup; children = ( + 1792AFFF2D8F4D9200D66667 /* URL+Extension.swift */, FF5FA0D52B35169400BC471D /* Binding+Extension.swift */, FF2F03432B79631800349855 /* Button+Extension.swift */, FF1002312B2483A20011A4DC /* Colours+Extension.swift */, @@ -508,6 +650,7 @@ FFEC32A32B24786D003E5C04 /* Models */ = { isa = PBXGroup; children = ( + 1792B0012D8F4D9200D66667 /* DocumentStatus.swift */, FF1002722B276EC10011A4DC /* AppColorScheme.swift */, FF10025D2B2648460011A4DC /* ConversationState.swift */, FF9C92092BF0B7B1004C8275 /* LanguageModel.swift */, @@ -520,6 +663,7 @@ FFEC32A42B247874003E5C04 /* Services */ = { isa = PBXGroup; children = ( + F8924F162D918BE100552A63 /* SVDBService.swift */, FF2F03412B795E0B00349855 /* Clipboard.swift */, FF0146CC2B3DADCA00A2A9F6 /* HapticsService.swift */, FF2F03482B796A6500349855 /* HotkeyService.swift */, @@ -545,6 +689,7 @@ FFEC32A72B247896003E5C04 /* Shared */ = { isa = PBXGroup; children = ( + 1792B0132D8F4D9200D66667 /* Retrieval */, FF38F8522B7AA9C400546B56 /* ApplicationEntry.swift */, FFEC32A82B24795A003E5C04 /* Chat */, FF15EF682B826BF400D4A541 /* Components */, @@ -590,6 +735,7 @@ FF0611E92C04A11800443B33 /* Magnet */, FF0611F02C04A2FB00443B33 /* Splash */, FF79AEB52CE72ECE00EDE10C /* MarkdownUI */, + 1792B00F2D8F4D9200D66667 /* SVDB */, ); productName = Enchanted; productReference = FFEC328D2B24779A003E5C04 /* Enchanted.app */; @@ -629,6 +775,7 @@ FF0611E82C04A11800443B33 /* XCRemoteSwiftPackageReference "Magnet" */, FF0611EF2C04A2FB00443B33 /* XCRemoteSwiftPackageReference "Splash" */, FF79AEB42CE72ECE00EDE10C /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, + 1792B00E2D8F4D9200D66667 /* XCRemoteSwiftPackageReference "SVDB" */, ); productRefGroup = FFEC328E2B24779A003E5C04 /* Products */; projectDirPath = ""; @@ -662,20 +809,27 @@ FF15EF6A2B826C0300D4A541 /* SimpleFloatingButton.swift in Sources */, FF24B30E2B66BE8500AB618F /* RunningBorder.swift in Sources */, FF1002602B26499B0011A4DC /* ConversationStatusView.swift in Sources */, + 1792B0182D8F4D9200D66667 /* DatabaseListView.swift in Sources */, FF10024C2B25BEA00011A4DC /* MessageSD.swift in Sources */, FFCF00F12C03209A00590E79 /* SpeechService.swift in Sources */, + 1792B00D2D8F4D9200D66667 /* RetrievalStore.swift in Sources */, FF10023E2B24F4900011A4DC /* OllamaService.swift in Sources */, + 1792B0022D8F4D9200D66667 /* DocumentStatus.swift in Sources */, FFBBF48C2B35051D008D611C /* UIImage+Extension.swift in Sources */, FF1002302B2482BA0011A4DC /* ChatMessageView.swift in Sources */, FF1002322B2483A20011A4DC /* Colours+Extension.swift in Sources */, FFB21A872B7BD0BA00D148A4 /* KeyboardReadable+Extension.swift in Sources */, + 1792B0222D8F4D9200D66667 /* DatabaseEmptyView.swift in Sources */, + 1792B0312D8F4D9200D66667 /* Retrieval.swift in Sources */, FF1BC3C72BA0757700A58043 /* SplashSyntaxHighlighter+Extension.swift in Sources */, FF33066E2B83CFEC007B33E5 /* KeyboardShortcuts.swift in Sources */, FF66A51D2B76949A00FAAC1E /* Helpers.swift in Sources */, FF10026A2B2731C60011A4DC /* ModelSelectorView.swift in Sources */, + 1792B02B2D8F4D9200D66667 /* NewDatabaseButton.swift in Sources */, FF38F84F2B7A7B6700546B56 /* MenuBarControlView_macOS.swift in Sources */, FFB56F462C0383B80020AFFD /* VoiceView.swift in Sources */, FFBBF48A2B350283008D611C /* SelectedImageView.swift in Sources */, + 1792B02F2D8F4D9200D66667 /* RetrievalView.swift in Sources */, FFEC9BDF2B8131B900AFBA63 /* HotKeys.swift in Sources */, FF2F03492B796A6500349855 /* HotkeyService.swift in Sources */, FF9300E02B783945000859F4 /* InputFields_macOS.swift in Sources */, @@ -690,9 +844,13 @@ FF2F03442B79631800349855 /* Button+Extension.swift in Sources */, FF1002682B2668790011A4DC /* Date+Extension.swift in Sources */, FF6D821E2B916053001183A8 /* UpsertCompletionView.swift in Sources */, + 1792B0392D8F4D9200D66667 /* MessageEmbedding.swift in Sources */, + 1792B0332D8F4D9200D66667 /* AudioMessageInput.swift in Sources */, FFBB2C7B2C03D8AD00509204 /* Voice.swift in Sources */, FF9300D42B778829000859F4 /* ChatView_macOS.swift in Sources */, + 1792B01D2D8F4D9200D66667 /* ImportFilesButton.swift in Sources */, FF38F8512B7A919C00546B56 /* MenuBarControl_macOS.swift in Sources */, + 1792B01F2D8F4D9200D66667 /* IndexingStatusView.swift in Sources */, FF38F85A2B7AB28300546B56 /* PromptPanelView.swift in Sources */, FF1002462B25BCE70011A4DC /* LanguageModelStore.swift in Sources */, FFEB9CA82BA04304004B1F3D /* NotificationMessage.swift in Sources */, @@ -700,11 +858,17 @@ FF1002392B24BBF20011A4DC /* Chat.swift in Sources */, FFB0327D2B312F3D0066A9DB /* RecordingView.swift in Sources */, FFBB2C7D2C047B6C00509204 /* Image+Extension.swift in Sources */, + 1792B03E2D8F4D9200D66667 /* UseDocumentViewItem.swift in Sources */, FF6D82192B912804001183A8 /* CompletionButtonView.swift in Sources */, FF10024A2B25BE740011A4DC /* LanguageModelSD.swift in Sources */, FF6B7B132B3EE7AC00E8FEA3 /* Throttler.swift in Sources */, + 1792B02D2D8F4D9200D66667 /* RetrievalHeaderView.swift in Sources */, FF1002402B24F7320011A4DC /* SideBarMenuView.swift in Sources */, + 1792B0372D8F4D9200D66667 /* DatabaseView.swift in Sources */, + 1792B0122D8F4D9200D66667 /* CircularProgressIndicator.swift in Sources */, FF9300DE2B782A28000859F4 /* UnreachableAPIView.swift in Sources */, + 1792B0242D8F4D9200D66667 /* RetrievalGuide.swift in Sources */, + 1792B0072D8F4D9200D66667 /* TextSplitter.swift in Sources */, FF6D820D2B90C925001183A8 /* CompletionPanelView.swift in Sources */, FF6D821C2B914202001183A8 /* CompletionsEditorView.swift in Sources */, FFD57E302BF29145003FEFF1 /* MarkdownColours.swift in Sources */, @@ -713,20 +877,24 @@ FFEC9BE32B81358800AFBA63 /* DeallocPrinter.swift in Sources */, FFBBF4842B34881B008D611C /* SpeechRecogniser.swift in Sources */, FF6D82122B911079001183A8 /* PanelCompletionsVM.swift in Sources */, + 1792B0092D8F4D9200D66667 /* DatabaseSD.swift in Sources */, FFEC32AA2B24797C003E5C04 /* ChatView_iOS.swift in Sources */, FF6D82152B911536001183A8 /* Accessibility.swift in Sources */, FF0146CD2B3DADCA00A2A9F6 /* HapticsService.swift in Sources */, FF5FA0D62B35169400BC471D /* Binding+Extension.swift in Sources */, FFE21C782B82353A00A69B9C /* SleepTest.swift in Sources */, FF1002502B25C79F0011A4DC /* SwiftDataService.swift in Sources */, + 1792B03C2D8F4D9200D66667 /* UseDocumentView.swift in Sources */, FF7FBE4C2B78E384000901F7 /* SamplePrompt.swift in Sources */, FF38F8582B7AB1AD00546B56 /* PanelManager.swift in Sources */, FF6D82172B9122F9001183A8 /* CompletionInstructionSD.swift in Sources */, FF226A652BE2A0EC00CC91F1 /* SelectTextSheet.swift in Sources */, FF10025A2B2624C40011A4DC /* ConversationHistoryListView.swift in Sources */, + 1792B0352D8F4D9200D66667 /* TextMessageInput.swift in Sources */, FF10026D2B2751760011A4DC /* SettingsView.swift in Sources */, FF33066C2B83BB31007B33E5 /* SidebarButton.swift in Sources */, FFD57E322BF291B2003FEFF1 /* CodeBlockView.swift in Sources */, + F8924F172D918BE500552A63 /* SVDBService.swift in Sources */, FF1002542B261D460011A4DC /* MessageListVIew.swift in Sources */, FF10024E2B25C2A70011A4DC /* ConversationSD.swift in Sources */, FF9C920C2BF0C088004C8275 /* OptionsMenuView.swift in Sources */, @@ -735,9 +903,14 @@ FF2F03422B795E0B00349855 /* Clipboard.swift in Sources */, 3396E0762BD877D500213816 /* Menus.swift in Sources */, FF9300DA2B781D97000859F4 /* ToolbarView_macOS.swift in Sources */, + 1792B0002D8F4D9200D66667 /* URL+Extension.swift in Sources */, FF1002522B2609980011A4DC /* ModelContext+Extension.swift in Sources */, + 1792B00B2D8F4D9200D66667 /* DocumentSD.swift in Sources */, FF1002752B278C170011A4DC /* AppStore.swift in Sources */, FF1002732B276EC10011A4DC /* AppColorScheme.swift in Sources */, + 1792B01B2D8F4D9200D66667 /* DeleteDatabaseButton.swift in Sources */, + 1792B0052D8F4D9200D66667 /* DataLoader.swift in Sources */, + 1792B0262D8F4D9200D66667 /* RetrievalGuideItem.swift in Sources */, FF10025E2B2648460011A4DC /* ConversationState.swift in Sources */, FFEC9BE72B813A8D00AFBA63 /* RemovableImage.swift in Sources */, FFFD00C22B92607800392AE6 /* CompletionsStore.swift in Sources */, @@ -876,7 +1049,7 @@ CURRENT_PROJECT_VERSION = 51; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Enchanted/Preview Content\""; - DEVELOPMENT_TEAM = JDDZ55DT74; + DEVELOPMENT_TEAM = CQ6C9JDWVW; ENABLE_HARDENED_RUNTIME = YES; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; @@ -928,7 +1101,7 @@ CURRENT_PROJECT_VERSION = 51; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Enchanted/Preview Content\""; - DEVELOPMENT_TEAM = JDDZ55DT74; + DEVELOPMENT_TEAM = CQ6C9JDWVW; ENABLE_HARDENED_RUNTIME = YES; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; ENABLE_PREVIEWS = YES; @@ -992,6 +1165,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 1792B00E2D8F4D9200D66667 /* XCRemoteSwiftPackageReference "SVDB" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Dripfarm/SVDB"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; FF0611E82C04A11800443B33 /* XCRemoteSwiftPackageReference "Magnet" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/AugustDev/Magnet"; @@ -1067,6 +1248,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 1792B00F2D8F4D9200D66667 /* SVDB */ = { + isa = XCSwiftPackageProductDependency; + package = 1792B00E2D8F4D9200D66667 /* XCRemoteSwiftPackageReference "SVDB" */; + productName = SVDB; + }; FF0611E92C04A11800443B33 /* Magnet */ = { isa = XCSwiftPackageProductDependency; package = FF0611E82C04A11800443B33 /* XCRemoteSwiftPackageReference "Magnet" */; diff --git a/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c6bed165..f4c53d6f 100644 --- a/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Enchanted.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3fe20c6c704d04e2f924250b1ce019f513915d235cdd9aedc57f51a0a1a94763", + "originHash" : "becdcce131f7b9e81f348ddb40d78e5a6bd70e0e26e6ff3791cab9161291a68f", "pins" : [ { "identity" : "activityindicatorview", @@ -73,6 +73,15 @@ "revision" : "c31eba0866102be9be29391dac641ecb46795702" } }, + { + "identity" : "svdb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Dripfarm/SVDB", + "state" : { + "revision" : "01e78b8bb9423e40201db450180f731c9a67c554", + "version" : "2.0.0" + } + }, { "identity" : "swift-async-algorithms", "kind" : "remoteSourceControl", @@ -96,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { diff --git a/Enchanted.xcodeproj/project.xcworkspace/xcuserdata/nowornever.xcuserdatad/UserInterfaceState.xcuserstate b/Enchanted.xcodeproj/project.xcworkspace/xcuserdata/nowornever.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..afa22fcf Binary files /dev/null and b/Enchanted.xcodeproj/project.xcworkspace/xcuserdata/nowornever.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Enchanted/Application/EnchantedApp.swift b/Enchanted/Application/EnchantedApp.swift index e8f3697d..b5604483 100644 --- a/Enchanted/Application/EnchantedApp.swift +++ b/Enchanted/Application/EnchantedApp.swift @@ -18,6 +18,7 @@ extension KeyboardShortcuts.Name { @main struct EnchantedApp: App { @State private var appStore = AppStore.shared + @State private var retrievalStore = RetrievalStore.shared #if os(macOS) @NSApplicationDelegateAdaptor(PanelManager.self) var panelManager #endif @@ -30,10 +31,20 @@ struct EnchantedApp: App { print("heya") panelManager.togglePanel() } +#endif .onAppear { +#if os(macOS) NSWindow.allowsAutomaticWindowTabbing = false - } #endif + Task{ + do { + try await retrievalStore.fetchDatabases() + retrievalStore.selectDatabase(database: nil) + } catch { + print("Error fetching databases: \(error)") + } + } + } } #if os(macOS) .commands { diff --git a/Enchanted/Extensions/URL+Extension.swift b/Enchanted/Extensions/URL+Extension.swift new file mode 100644 index 00000000..fc6b298f --- /dev/null +++ b/Enchanted/Extensions/URL+Extension.swift @@ -0,0 +1,21 @@ +// +// URL+Extension.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import Foundation +import Collections +import UniformTypeIdentifiers + +extension URL { + public func mimeType() -> String { + if let mimeType = UTType(filenameExtension: self.pathExtension)?.preferredMIMEType { + return mimeType + } + else { + return "application/octet-stream" + } + } +} diff --git a/Enchanted/Models/DocumentStatus.swift b/Enchanted/Models/DocumentStatus.swift new file mode 100644 index 00000000..3877708f --- /dev/null +++ b/Enchanted/Models/DocumentStatus.swift @@ -0,0 +1,37 @@ +// +// DocumentStatus.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import Foundation +import SwiftUI + +enum DocumentIndexStatus: String, Codable { + case indexing + case completed + case notStarted + case failed + + var humanReadable: String { + switch self { + case .indexing: "Indexing" + case .completed: "Completed" + case .notStarted: "Not Started" + case .failed: "Failed" + } + } +} + +// MARK: - Icon extension +extension DocumentIndexStatus { + var icon: some View { + switch self { + case .indexing: return Image(systemName: "hourglass.circle").foregroundColor(.indigo) + case .completed: return Image(systemName: "checkmark.circle").foregroundColor(.green) + case .notStarted: return Image(systemName: "hourglass.circle").foregroundColor(.yellow) + case .failed: return Image(systemName: "circle.slash").foregroundColor(.red) + } + } +} diff --git a/Enchanted/Retrieval/DataLoader.swift b/Enchanted/Retrieval/DataLoader.swift new file mode 100644 index 00000000..faca9484 --- /dev/null +++ b/Enchanted/Retrieval/DataLoader.swift @@ -0,0 +1,63 @@ +// +// DataLoader.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import Foundation +import PDFKit + +struct DataLoader { + func fromPDF(_ url: URL) -> String? { + guard url.startAccessingSecurityScopedResource() else { + print("could not get access to file \(url.absoluteString)") + return nil + } + + let pdfDocument = PDFDocument(url: url) + + guard let document = pdfDocument else { + print("Could not load \(url.absoluteString)") + return nil + } + + var contents = "" + for i in 0.. String? { + do { + let contents = try String(contentsOf: url, encoding: .utf8) + return contents + } catch { + print("Could not parse \(url.absoluteString). Error: \(error.localizedDescription)") + } + + return nil + } + + func from(_ url: URL) -> String? { + let mimeType = url.mimeType() + + if mimeType.contains("text/") { + return fromTextFile(url) + } else if mimeType == "application/pdf" { + return fromPDF(url) + } else if mimeType == "application/octet-stream" { + return fromTextFile(url) + } else if mimeType == "application/x-yaml" { + return fromTextFile(url) + } + + print("unhandled mime type \(mimeType) for \(url.absoluteString)") + return nil + } +} diff --git a/Enchanted/Retrieval/MessageEmbedding.swift b/Enchanted/Retrieval/MessageEmbedding.swift new file mode 100644 index 00000000..8cf143f8 --- /dev/null +++ b/Enchanted/Retrieval/MessageEmbedding.swift @@ -0,0 +1,48 @@ +// +// MessageEmbedding.swift +// Enchanted +// +// Created by Now or Never on 3/19/25. +// + +import Foundation + +protocol MessageEmbeddingProtocol { + func generateContext(_ prompt: String, _ model: LanguageModelSD) async -> String +} + +struct MessageEmbedding: MessageEmbeddingProtocol { + private let languageModelStore: LanguageModelStore + private let retrievalStore: RetrievalStore + private let svdbService: SVDBService + + init( + languageModelStore: LanguageModelStore = .shared, + retrievalStore: RetrievalStore = .shared, + svdbService: SVDBService = .shared + ) { + self.languageModelStore = languageModelStore + self.retrievalStore = retrievalStore + self.svdbService = svdbService + } + + func generateContext(_ prompt: String, _ model: LanguageModelSD) async -> String { + // Obtain the embedding vector for the given prompt + guard let promptEmbedding = await languageModelStore.getEmbedding(model: model, prompt: prompt) else { + print("[MessageEmbedding] Error: Unable to generate embedding for prompt: \(prompt)") + return "" + } + + print("[MessageEmbedding] Embedding vector generated: \(promptEmbedding)") + + guard let databaseId = retrievalStore.selectedDatabase?.id else { + print("Database ID not found.") + return "" + } + + // Perform similarity search to retrieve relevant context + let retrievedContext = await svdbService.search(query: promptEmbedding, databaseId: databaseId) + + return retrievedContext + } +} diff --git a/Enchanted/Retrieval/TextSplitter.swift b/Enchanted/Retrieval/TextSplitter.swift new file mode 100644 index 00000000..725ff896 --- /dev/null +++ b/Enchanted/Retrieval/TextSplitter.swift @@ -0,0 +1,68 @@ +// +// TextSplitter.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import Foundation + +protocol TextBatchSplitter { + func split(text: String, maxChunkSize: Int) -> [String] +} + +struct SimpleTextSplitter: TextBatchSplitter { + func split(text: String, maxChunkSize: Int) -> [String] { + guard !text.isEmpty, maxChunkSize > 0 else { return [] } + + let separators = ["\n\n", "\n", ".", " ", ""] + + // Attempt to split using separators + for separator in separators { + let components = separator.isEmpty ? text.map { String($0) } : text.components(separatedBy: separator) + + // If any single component is larger than maxChunkSize, skip this separator + if components.contains(where: { $0.count > maxChunkSize }) { + continue + } + + var chunks: [String] = [] + var currentChunk = "" + + for component in components { + let separatorLength = currentChunk.isEmpty ? 0 : separator.count + if currentChunk.count + separatorLength + component.count <= maxChunkSize { + currentChunk += (currentChunk.isEmpty ? "" : separator) + component + } else { + if !currentChunk.isEmpty { + chunks.append(currentChunk) + } + currentChunk = component + } + } + + if !currentChunk.isEmpty { + chunks.append(currentChunk) + } + + return chunks + } + + // Fallback: split text into fixed-size chunks (by characters) + return fixedSizeCharacterChunks(text: text, maxChunkSize: maxChunkSize) + } + + private func fixedSizeCharacterChunks(text: String, maxChunkSize: Int) -> [String] { + var chunks: [String] = [] + var currentIndex = text.startIndex + + while currentIndex < text.endIndex { + let endIndex = text.index(currentIndex, offsetBy: maxChunkSize, limitedBy: text.endIndex) ?? text.endIndex + let chunk = String(text[currentIndex.. [Double]? { + let res = try? await ollamaKit.generateEmbeddings(data: .init(model: model.name, prompt: prompt)) + return res?.embedding + } +} diff --git a/Enchanted/Services/SVDBService.swift b/Enchanted/Services/SVDBService.swift new file mode 100644 index 00000000..d2378881 --- /dev/null +++ b/Enchanted/Services/SVDBService.swift @@ -0,0 +1,81 @@ +// +// SVDBService.swift +// Enchanted +// +// Created by Now or Never on 3/24/25. +// + +import SVDB +import Foundation + +final actor SVDBService { + var svdb: SVDB + var collection: Collection? + + static let shared = SVDBService() + + private init() { + self.svdb = SVDB.shared + } + + func createCollection(databaseId: UUID) async throws -> Collection { + do { + let newCollection = try svdb.collection("\(databaseId)") + self.collection = newCollection + return newCollection + } catch let error as SVDBError { + if(error == .collectionAlreadyExists) { + if let collection = self.collection { + return collection + } + } + throw error + } catch { + throw error + } + } + + func releaseCollection(_ collectionName: String) async throws -> Void { + svdb.releaseCollection(collectionName) + } + + func addDocument(text: String, embedding: [Double]) throws -> Void { + guard let collection = collection else { + throw NSError( + domain: "SVDBService", + code: 404, + userInfo: [NSLocalizedDescriptionKey: "Collection not initialized"] + ) + } + + collection.addDocument(text: text, embedding: embedding) + } + + func search(query: [Double], databaseId: UUID) -> String { + do { + let collection = try svdb.collection(databaseId.uuidString) + return searchInCollection(collection, with: query) + } catch let error as SVDBError { + if(error == .collectionAlreadyExists) { + guard let collection = collection else { + print("Collection not initialized") + return "" + } + return searchInCollection(collection, with: query) + } + } catch { + print("Failed to load or search collection:", error) + } + return "" + } + + private func searchInCollection(_ collection: Collection, with query: [Double]) -> String { + if let firstResult = collection.search(query: query, num_results: 1).first { + print("Result found: \(firstResult.text)") + return firstResult.text + } else { + print("No result found.") + return "" + } + } +} diff --git a/Enchanted/Services/SwiftDataService.swift b/Enchanted/Services/SwiftDataService.swift index d9071940..d25575f1 100644 --- a/Enchanted/Services/SwiftDataService.swift +++ b/Enchanted/Services/SwiftDataService.swift @@ -21,7 +21,9 @@ final actor SwiftDataService: ModelActor { LanguageModelSD.self, ConversationSD.self, MessageSD.self, - CompletionInstructionSD.self + CompletionInstructionSD.self, + DatabaseSD.self, + DocumentSD.self ]) let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) @@ -155,6 +157,44 @@ extension SwiftDataService { } } +// MARK: - Database +extension SwiftDataService { + func createDatabase(name: String, indexPath: String, languageModelName: String) throws { + let db = DatabaseSD(name: name, indexPath: indexPath) + guard let llm = try? fetchModels().filter({$0.name == languageModelName}).first else { return } + db.model = llm + self.modelContext.insert(db) + try modelContext.saveChanges() + } + + func deleteDatabase(selectedDatabase: DatabaseSD) throws { + self.modelContext.delete(selectedDatabase) + try modelContext.saveChanges() + } + + func getDatabases() throws -> [DatabaseSD] { + let sortDescriptor = SortDescriptor(\DatabaseSD.updatedAt, order: .reverse) + let fetchDescriptor = FetchDescriptor(sortBy: [sortDescriptor]) + return try modelContext.fetch(fetchDescriptor) + } + + func databaseAttachDocuments(db: DatabaseSD, paths: [URL]) throws { + let docs = paths.map({DocumentSD(documentUrl: $0, status: .notStarted)}) + db.documents = docs + db.updatedAt = .now + try modelContext.saveChanges() + } +} + +// MARK: - Documents +extension SwiftDataService { + func updateDocumentStatus(document: DocumentSD, status: DocumentIndexStatus) throws { + document.status = status + document.updatedAt = .now + try modelContext.saveChanges() + } +} + // MARK: - General extension SwiftDataService { func deleteEverything() throws { diff --git a/Enchanted/Stores/ConversationStore.swift b/Enchanted/Stores/ConversationStore.swift index c9043ea3..0b2a582c 100644 --- a/Enchanted/Stores/ConversationStore.swift +++ b/Enchanted/Stores/ConversationStore.swift @@ -107,7 +107,7 @@ final class ConversationStore: Sendable { } @MainActor - func sendPrompt(userPrompt: String, model: LanguageModelSD, image: Image? = nil, systemPrompt: String = "", trimmingMessageId: String? = nil) { + func sendPrompt(userPrompt: String, embeddedPrompt: String? = "", model: LanguageModelSD, image: Image? = nil, systemPrompt: String = "", trimmingMessageId: String? = nil) { guard userPrompt.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else { return } let conversation = selectedConversation ?? ConversationSD(name: userPrompt) @@ -130,15 +130,25 @@ final class ConversationStore: Sendable { systemMessage.conversation = conversation } + /// Combine userPrompt and embedPrompt if embedPrompt is not empty + let combinedPrompt = embeddedPrompt?.isEmpty == false ? + "\(userPrompt)\n\nContext:\n\(embeddedPrompt!)" : userPrompt + /// construct new message - let userMessage = MessageSD(content: userPrompt, role: "user", image: image?.render()?.compressImageData()) + let userMessage = MessageSD(content: userPrompt, prompt: combinedPrompt, role: "user", image: image?.render()?.compressImageData()) userMessage.conversation = conversation /// prepare message history for Ollama - var messageHistory = conversation.messages - .sorted{$0.createdAt < $1.createdAt} - .map{OKChatRequestData.Message(role: OKChatRequestData.Message.Role(rawValue: $0.role) ?? .assistant, content: $0.content)} - + let sortedMessages = conversation.messages.sorted { $0.createdAt < $1.createdAt } + + var messageHistory = sortedMessages.enumerated().map { (index, message) in + let isLastMessage = (index == sortedMessages.count - 1) + let content = isLastMessage ? (message.prompt ?? message.content) : message.content + return OKChatRequestData.Message( + role: OKChatRequestData.Message.Role(rawValue: message.role) ?? .assistant, + content: content + ) + } print(messageHistory.map({$0.content})) diff --git a/Enchanted/Stores/LanguageModelStore.swift b/Enchanted/Stores/LanguageModelStore.swift index 48ff4480..d326bf4e 100644 --- a/Enchanted/Stores/LanguageModelStore.swift +++ b/Enchanted/Stores/LanguageModelStore.swift @@ -64,4 +64,14 @@ final class LanguageModelStore { } try await swiftDataService.deleteModels() } + + func getEmbedding(model: LanguageModelSD, prompt: String) async -> [Double]? { + switch model.modelProvider { + case .ollama: + let embeddings = await OllamaService.shared.getEmbedding(prompt: prompt, model: model) + return embeddings + case .none: + return nil + } + } } diff --git a/Enchanted/Stores/RetrievalStore.swift b/Enchanted/Stores/RetrievalStore.swift new file mode 100644 index 00000000..31805f97 --- /dev/null +++ b/Enchanted/Stores/RetrievalStore.swift @@ -0,0 +1,193 @@ +// +// RetrievalStore.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import Foundation +import SwiftUI +import Observation + +@Observable +final class RetrievalStore { + + static let shared = RetrievalStore() + + private let swiftDataService: SwiftDataService + private let svdbService: SVDBService + private let splitter: SimpleTextSplitter + private let dataLoader: DataLoader + private let languageModelStore: LanguageModelStore + + var databases: [DatabaseSD] = [] + var selectedDatabase: DatabaseSD? + var progress: Double? + + // MARK: - Initialization + private init( + swiftDataService: SwiftDataService = .shared, + svdbService: SVDBService = .shared, + splitter: SimpleTextSplitter = .init(), + dataLoader: DataLoader = .init(), + languageModelStore: LanguageModelStore = .shared + ) { + self.swiftDataService = swiftDataService + self.svdbService = svdbService + self.splitter = splitter + self.dataLoader = dataLoader + self.languageModelStore = languageModelStore + } + + // MARK: - Public Methods + func createDatabase(name: String, indexPath: String, languageModelName: String) async throws { + try await swiftDataService.createDatabase(name: name, indexPath: indexPath, languageModelName: languageModelName) + try await fetchDatabases() + } + + func deleteDatabase(selectedDatabase: DatabaseSD) async throws { + try await swiftDataService.deleteDatabase(selectedDatabase: selectedDatabase) + try await fetchDatabases() + } + + func fetchDatabases() async throws { + databases = try await swiftDataService.getDatabases() + } + + func selectDatabase(database: DatabaseSD?, overWrite: Bool = false) { + if (self.databases.count == 0) { + return + } + + if (self.selectedDatabase == nil || overWrite) { + self.selectedDatabase = database ?? self.databases[0] + } + } + + func attachDocuments(databaseId: UUID, documentPaths: [URL]) async throws { + guard let database = databases.first(where: { $0.id == databaseId }) else { + throw RetrievalError.databaseNotFound + } + try await swiftDataService.databaseAttachDocuments(db: database, paths: documentPaths) + try await fetchDatabases() + } + + func indexDocuments( + databaseId: UUID, + progressCallback: @escaping @Sendable (_ progress: Double) -> Void + ) async throws { + + guard let database = databases.first(where: { $0.id == databaseId }), + let languageModel = database.model else { + throw RetrievalError.databaseOrModelNotFound + } + + let documentsToIndex = database.documents?.filter { $0.status != .completed } ?? [] + + guard !documentsToIndex.isEmpty else { + progressCallback(1.0) + return + } + + for (docIndex, document) in documentsToIndex.enumerated() { + let documentsProgress = Double(docIndex) / Double(documentsToIndex.count) + + try await swiftDataService.updateDocumentStatus(document: document, status: .indexing) + + guard let fileURL = document.documentUrl else { + try await markDocumentFailed(document) + continue + } + + do { + let fileContents = try readContentsSecurely(from: fileURL) + let chunks = splitter.split(text: fileContents, maxChunkSize: 512) + + try await indexChunks( + chunks, + with: languageModel, + document: document, + documentsProgress: documentsProgress, + progressCallback: progressCallback, + databaseId: databaseId + ) + + try await swiftDataService.updateDocumentStatus(document: document, status: .completed) + + } catch { + print("Failed indexing document: \(error.localizedDescription)") + try await markDocumentFailed(document) + } + } + + progressCallback(1.0) + } + + // MARK: - Private Helper Methods + private func readContentsSecurely(from url: URL) throws -> String { + guard url.startAccessingSecurityScopedResource() else { + throw RetrievalError.cannotAccessURL + } + defer { url.stopAccessingSecurityScopedResource() } + + guard let contents = dataLoader.from(url) else { + throw RetrievalError.failedToLoadData + } + + return contents + } + + private func indexChunks( + _ chunks: [String], + with languageModel: LanguageModelSD, + document: DocumentSD, + documentsProgress: Double, + progressCallback: @escaping @Sendable (_ progress: Double) -> Void, + databaseId: UUID + ) async throws { + + + for (chunkIndex, chunk) in chunks.enumerated() { + guard let embedding = await languageModelStore.getEmbedding(model: languageModel, prompt: chunk) else { + continue + } + + // add document in selected collection of SVDB + try await svdbService.addDocument(text: chunk, embedding: embedding) + + let chunkProgress = Double(chunkIndex + 1) / Double(chunks.count) + let overallProgress = documentsProgress + (chunkProgress / Double(chunks.count)) + + DispatchQueue.main.async { [weak document] in + document?.indexProgress = chunkProgress + } + + progressCallback(overallProgress) + } + } + + private func markDocumentFailed(_ document: DocumentSD) async throws { + try await swiftDataService.updateDocumentStatus(document: document, status: .failed) + } +} + +// MARK: - Custom Errors +enum RetrievalError: LocalizedError { + case databaseNotFound + case databaseOrModelNotFound + case cannotAccessURL + case failedToLoadData + + var errorDescription: String? { + switch self { + case .databaseNotFound: + return "The specified database could not be found." + case .databaseOrModelNotFound: + return "Database or associated language model not found." + case .cannotAccessURL: + return "Unable to access URL securely." + case .failedToLoadData: + return "Failed to load data from the specified URL." + } + } +} diff --git a/Enchanted/SwiftData/Models/DatabaseSD.swift b/Enchanted/SwiftData/Models/DatabaseSD.swift new file mode 100644 index 00000000..245bb040 --- /dev/null +++ b/Enchanted/SwiftData/Models/DatabaseSD.swift @@ -0,0 +1,42 @@ +// +// DatabaseSD.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import Foundation +import SwiftData + +@Model +final class DatabaseSD: Identifiable { + @Attribute(.unique) var id: UUID = UUID() + + var name: String + var indexPath: String? + var updatedAt: Date + + @Relationship(deleteRule: .nullify) + var model: LanguageModelSD? + + @Relationship(deleteRule: .cascade, inverse: \DocumentSD.database) + var documents: [DocumentSD]? = [] + + init(name: String, updatedAt: Date = Date.now, indexPath: String, documents: [DocumentSD] = []) { + self.name = name + self.updatedAt = updatedAt + self.indexPath = indexPath + self.documents = documents + } +} + +extension DatabaseSD { + static let sample = [ + DatabaseSD(name: "Personal", indexPath: "./home/personal", documents: DocumentSD.sample) + ] +} + +// MARK: - @unchecked Sendable +extension DatabaseSD: @unchecked Sendable { + +} diff --git a/Enchanted/SwiftData/Models/DocumentSD.swift b/Enchanted/SwiftData/Models/DocumentSD.swift new file mode 100644 index 00000000..3f3044e4 --- /dev/null +++ b/Enchanted/SwiftData/Models/DocumentSD.swift @@ -0,0 +1,44 @@ +// +// DocumentSD.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import Foundation +import SwiftData + +@Model +final class DocumentSD: Identifiable { + @Attribute(.unique) var id: UUID = UUID() + + var documentUrl: URL? + var updatedAt: Date + var status: DocumentIndexStatus + var indexProgress: Double? + + @Relationship(deleteRule: .nullify) + var database: DatabaseSD? + + init(updatedAt: Date = Date.now, documentUrl: URL, status: DocumentIndexStatus) { + self.updatedAt = updatedAt + self.documentUrl = documentUrl + self.status = status + self.indexProgress = 0 + } +} + +// MARK: - Sample +extension DocumentSD { + static let sample = [ + DocumentSD(documentUrl: URL(string: "./files/company_house.pdf")!, status: .completed), + DocumentSD(documentUrl: URL(string: "./files/somedoc.pdf")!, status: .indexing), + DocumentSD(documentUrl: URL(string: "./files/important.pdf")!, status: .completed) + ] +} + + +// MARK: - @unchecked Sendable +extension DocumentSD: @unchecked Sendable { + +} diff --git a/Enchanted/SwiftData/Models/MessageSD.swift b/Enchanted/SwiftData/Models/MessageSD.swift index c3d6d9a7..72b87ec0 100644 --- a/Enchanted/SwiftData/Models/MessageSD.swift +++ b/Enchanted/SwiftData/Models/MessageSD.swift @@ -39,6 +39,7 @@ final class MessageSD: Identifiable { return false } var content: String + var prompt: String? var realContent: String? { if content.contains("") { if content.contains("") { @@ -60,8 +61,9 @@ final class MessageSD: Identifiable { @Relationship var conversation: ConversationSD? - init(content: String, role: String, done: Bool = false, error: Bool = false, image: Data? = nil) { + init(content: String, prompt: String? = nil, role: String, done: Bool = false, error: Bool = false, image: Data? = nil) { self.content = content + self.prompt = prompt ?? content self.role = role self.done = done self.error = error diff --git a/Enchanted/UI/Shared/Chat/Chat.swift b/Enchanted/UI/Shared/Chat/Chat.swift index bc9b8645..b2008353 100644 --- a/Enchanted/UI/Shared/Chat/Chat.swift +++ b/Enchanted/UI/Shared/Chat/Chat.swift @@ -15,6 +15,7 @@ struct Chat: View, Sendable { @AppStorage("appUserInitials") private var userInitials: String = "" @AppStorage("defaultOllamaModel") private var defaultOllamaModel: String = "" @State var showMenu = false + private var messageEmbedding = MessageEmbedding() init(languageModelStore: LanguageModelStore, conversationStore: ConversationStore, appStore: AppStore) { _languageModelStore = State(initialValue: languageModelStore) @@ -43,14 +44,26 @@ struct Chat: View, Sendable { } @MainActor - func sendMessage(prompt: String, model: LanguageModelSD, image: Image?, trimmingMessageId: String?) { - conversationStore.sendPrompt( - userPrompt: prompt, - model: model, - image: image, - systemPrompt: systemPrompt, - trimmingMessageId: trimmingMessageId - ) + func sendMessage(prompt: String, model: LanguageModelSD, image: Image?, trimmingMessageId: String?, documentUsage: Bool?) { + Task { + var embeddedPrompt = "" + guard let documentUsage = documentUsage else { + return + } + + if (documentUsage) { + embeddedPrompt = await messageEmbedding.generateContext(prompt, model) + } + + conversationStore.sendPrompt( + userPrompt: prompt, + embeddedPrompt: embeddedPrompt, + model: model, + image: image, + systemPrompt: systemPrompt, + trimmingMessageId: trimmingMessageId + ) + } } func onConversationTap(_ conversation: ConversationSD) { diff --git a/Enchanted/UI/Shared/Chat/Components/AudioMessageInput.swift b/Enchanted/UI/Shared/Chat/Components/AudioMessageInput.swift new file mode 100644 index 00000000..37dfb10b --- /dev/null +++ b/Enchanted/UI/Shared/Chat/Components/AudioMessageInput.swift @@ -0,0 +1,60 @@ +// +// AudioMessageInput.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +#if os(macOS) || os(visionOS) +import SwiftUI + +struct AudioMessageInput: View { + @Binding var message: String + var conversationState: ConversationState + var onStopGenerateTap: @MainActor () -> Void + var selectedModel: LanguageModelSD? + @Binding var isRecording: Bool + @Binding var selectedImage: Image? + @Binding var fileSelectingActive: Bool + var sendMessage: () -> () + + var body: some View { + HStack { + RecordingView(isRecording: $isRecording.animation()) { transcription in + withAnimation(.easeIn(duration: 0.3)) { + self.message = transcription + } + } + + 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) { + selectedImage = Image(data: imageData) + } + url.stopAccessingSecurityScopedResource() + case .failure(let error): + print(error) + } + }) + + + Group { + switch conversationState { + case .loading: + SimpleFloatingButton(systemImage: "square.fill", onClick: onStopGenerateTap) + default: + SimpleFloatingButton(systemImage: "paperplane.fill", onClick: { Task { sendMessage() } }) + .showIf(!message.isEmpty) + } + } + + } + } +} +#endif diff --git a/Enchanted/UI/Shared/Chat/Components/TextMessageInput.swift b/Enchanted/UI/Shared/Chat/Components/TextMessageInput.swift new file mode 100644 index 00000000..8f822540 --- /dev/null +++ b/Enchanted/UI/Shared/Chat/Components/TextMessageInput.swift @@ -0,0 +1,60 @@ +// +// TextMessageInput.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +#if os(macOS) || os(visionOS) +import SwiftUI + +struct TextMessageInput: View { + @Binding var message: String + @Binding var fileDropActive: Bool + var sendMessage: () -> () + var isFocusedInput: FocusState.Binding + +#if os(macOS) + @Binding var selectedImage: Image? + + 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) + } + } + ] + } +#endif + + var body: some View { + TextField("Message", text: $message.animation(.easeOut(duration: 0.3)), axis: .vertical) + .focused(isFocusedInput) + .font(.system(size: 14)) + .frame(maxWidth:.infinity, minHeight: 40) + .clipped() + .textFieldStyle(.plain) +#if os(macOS) + .onSubmit { + if NSApp.currentEvent?.modifierFlags.contains(.shift) == true { + message += "\n" + } else { + sendMessage() + } + } +#endif + /// TextField bypasses drop area + .allowsHitTesting(!fileDropActive) +#if os(macOS) + .addCustomHotkeys(hotkeys) +#endif + .padding(.trailing, 80) + } +} +#endif diff --git a/Enchanted/UI/Shared/Chat/Components/UseDocument/UseDocumentView.swift b/Enchanted/UI/Shared/Chat/Components/UseDocument/UseDocumentView.swift new file mode 100644 index 00000000..aea1c700 --- /dev/null +++ b/Enchanted/UI/Shared/Chat/Components/UseDocument/UseDocumentView.swift @@ -0,0 +1,107 @@ +// +// UseDocumentView.swift +// Enchanted +// +// Created by Daniel on 3/22/25. +// + +import SwiftUI + +struct UseDocumentView: View { + @Binding var documentUsage: Bool + + @State var retrievalStore = RetrievalStore.shared + @State var presentPopover: Bool = false + @State var showRetrieval: Bool = false + + private func selectDatabase(database: DatabaseSD, overWrite: Bool) { + retrievalStore.selectDatabase(database: database, overWrite: overWrite) + } + + private func showRetrievalTap() { + showRetrieval.toggle() + } + + var body: some View { + Button(action: { + presentPopover.toggle() + }, label: { + Image(systemName: "doc.text.fill") + .symbolRenderingMode(.hierarchical) + .foregroundStyle(documentUsage ? Color.blue : Color.gray) + .font(.system(size: 20)) + }) + .tint(documentUsage ? Color.blue : Color.gray) + .buttonStyle(PlainButtonStyle()) + .clipShape(Capsule()) + .popover(isPresented: $presentPopover, content: { + VStack { + if (retrievalStore.databases.count == 0) { + VStack { + Image(systemName: "tray.and.arrow.down") + .font(.system(size: 60)) + .foregroundColor(.gray) + .padding(.bottom, 6) + + Text("Add Database") + .foregroundStyle(Color.blue) + .onTapGesture { + showRetrievalTap() + } + + } + .padding() + .frame(maxHeight: .infinity) + } else { + Text("Documents") + .padding() + .font(.system(size:20)) + + ScrollView { + VStack { + ForEach(retrievalStore.databases, id:\.self) { _database in + UseDocumentViewItem(database: _database, selectedDatabase: retrievalStore.selectedDatabase, selectDatabase: selectDatabase) + } + } + } +#if os(macOS) || os(visionOS) + .frame(maxHeight: 350) +#elseif os(iOS) + .frame(maxHeight: .infinity) +#endif + } + + Divider() + + HStack { + VStack(alignment: .leading) { + Text("Enable using document") + Text("Enchanted can use the document") + .foregroundStyle(Color.gray) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .leading) + } + .layoutPriority(1) + + Spacer() + + Toggle("", isOn: $documentUsage) + .toggleStyle(SwitchToggleStyle(tint: .blue)) + .disabled(retrievalStore.databases.count == 0) + } + .padding() + } + }) + .sheet(isPresented: $showRetrieval) { + Retrieval() + } + } +} + + + +#Preview { + @Previewable @State var documentUsage: Bool = false + + UseDocumentView(documentUsage: $documentUsage) +} diff --git a/Enchanted/UI/Shared/Chat/Components/UseDocument/UseDocumentViewItem.swift b/Enchanted/UI/Shared/Chat/Components/UseDocument/UseDocumentViewItem.swift new file mode 100644 index 00000000..4ef7ee8d --- /dev/null +++ b/Enchanted/UI/Shared/Chat/Components/UseDocument/UseDocumentViewItem.swift @@ -0,0 +1,55 @@ +// +// UseDocumentViewItem.swift +// Enchanted +// +// Created by Daniel on 3/22/25. +// + +import SwiftUI + +struct UseDocumentViewItem: View { + var database: DatabaseSD + var selectedDatabase: DatabaseSD? + var selectDatabase: (DatabaseSD, Bool) -> () + + @State var isHovered: Bool = false + + var body: some View { + Button(action: { + if (selectedDatabase != database) { + selectDatabase(database, true) + } + }, label: { + HStack { + VStack (alignment: .leading) { + Text("\(String(describing: database.name))") + .font(.system(size: 18)) + if let modelName = database.model?.name { + Text("\(String(describing: modelName))") + .foregroundStyle(Color.gray) + } + } + + Spacer() + + if (selectedDatabase?.id == database.id) { + Image(systemName: "checkmark") + } + } + .padding(.horizontal) + .padding(.vertical, 10) +#if os(macOS) + .frame(width: 300) +#endif + .onHover{_ in + isHovered.toggle() + } + .background(Color.white.opacity(isHovered ? 0.2 : 0)) + }) + .buttonStyle(PlainButtonStyle()) + } +} + +#Preview { + UseDocumentViewItem(database: DatabaseSD.sample[0], selectDatabase: {_,_ in}) +} diff --git a/Enchanted/UI/Shared/Components/CircularProgressIndicator.swift b/Enchanted/UI/Shared/Components/CircularProgressIndicator.swift new file mode 100644 index 00000000..da89f24c --- /dev/null +++ b/Enchanted/UI/Shared/Components/CircularProgressIndicator.swift @@ -0,0 +1,33 @@ +// +// CircularProgressIndicator.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI +import ActivityIndicatorView + +struct CircularProgressView: View { + let progress: CGFloat + + var body: some View { + ZStack { + Circle() + .stroke(lineWidth: 2) + .opacity(0.2) + .foregroundColor(.blue) + + Circle() + .trim(from: 0.0, to: min(progress, 1.0)) + .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)) + .foregroundColor(.blue) + .rotationEffect(Angle(degrees: 270.0)) + .animation(.linear, value: progress) + } + } +} + +#Preview { + CircularProgressView(progress: 0.3) +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/DatabaseEmptyView.swift b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/DatabaseEmptyView.swift new file mode 100644 index 00000000..7551cdd9 --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/DatabaseEmptyView.swift @@ -0,0 +1,42 @@ +// +// DatabaseEmptyView.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct DatabaseEmptyView: View { + private var guides: [RetrievalGuide] = [ + RetrievalGuide(imageSystemName: "plus.rectangle.on.folder", imageColor: Color(hex: "428564"), order: 1, description: "Create a database of documents for your task. It may be course notes, legal documents, coding project or anything else. Files always stay local to your machine."), + RetrievalGuide(imageSystemName: "doc.on.doc", imageColor: Color(hex: "9b72cb"), order: 2, description: "Import documents. Enchanted will create text embeddings based on the selected model. Currently text and PDF files are supported."), + RetrievalGuide(imageSystemName: "message.badge", imageColor: Color(hex: "d96570"), order: 3, description: "Make prompts using your database. Enchanted will include snippets of relevant your documents when sending the prompt."), + ] + + var body: some View { + VStack(alignment: .center, spacing: 20) { + Text("How it works") + .font(Font.system(size: 32, weight: .bold)) + .multilineTextAlignment(.center) + .foregroundStyle( + LinearGradient( + colors: [Color(hex: "4285f4"), Color(hex: "9b72cb"), Color(hex: "d96570"), Color(hex: "#d96570")], + startPoint: .leading, + endPoint: .trailing + ) + ) + + VStack (alignment: .leading, spacing: 20) { + ForEach(guides) { guide in + RetrievalGuideItem(guide: guide) + } + } + } + .padding(20) + } +} + +#Preview { + DatabaseEmptyView() +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/RetrievalGuide.swift b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/RetrievalGuide.swift new file mode 100644 index 00000000..87266ebc --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/RetrievalGuide.swift @@ -0,0 +1,17 @@ +// +// RetrievalGuide.swift +// Enchanted +// +// Created by Now or Never on 3/19/25. +// + +import SwiftUI + +struct RetrievalGuide: Identifiable { + let id = UUID() + + var imageSystemName: String + var imageColor: Color + var order: Int + var description: String +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/RetrievalGuideItem.swift b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/RetrievalGuideItem.swift new file mode 100644 index 00000000..0c4d7bf9 --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseEmpty/RetrievalGuideItem.swift @@ -0,0 +1,41 @@ +// +// RetrievalGuideItem.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct RetrievalGuideItem: View { + var guide: RetrievalGuide + + var body: some View { + HStack { + Image(systemName: guide.imageSystemName) + .resizable() + .scaledToFit() + .frame(width: 40) + .foregroundColor(guide.imageColor) +#if os(macOS) + .padding() +#endif + + Text("\(guide.description)") + .lineLimit(15) + .font(.system(size: 16)) +#if os(macOS) + .lineSpacing(5) +#endif + .fixedSize(horizontal: false, vertical: true) + .padding(10) + } + .frame(maxHeight: 100) + } +} + +#Preview { + var guide: RetrievalGuide = RetrievalGuide(imageSystemName: "plus.rectangle.on.folder", imageColor: Color(hex: "428564"), order: 1, description: "Create a database of documents for your task. It may be course notes, legal documents, coding project or anything else. Files always stay local to your machine.") + + RetrievalGuideItem(guide: guide) +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DatabaseListView.swift b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DatabaseListView.swift new file mode 100644 index 00000000..b5c469d2 --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DatabaseListView.swift @@ -0,0 +1,72 @@ +// +// DatabaseListView.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct DatabaseListView: View { + @Binding var selectedDatabase: DatabaseSD? + var databases: [DatabaseSD] + var documents: [DocumentSD] + @Binding var indexFilesProgress: Double + @Binding var indexing: Bool + + var deleteDatabase: () -> () + var indexFiles: () -> () + var attachFiles: ([URL]) -> () + + var body: some View { + HStack { + Picker(selection: $selectedDatabase) { + ForEach(databases, id:\.self) { database in + Text("\(database.name) (\(database.model?.name ?? "Unknown"))").tag(Optional(database)) + } + } label: { +#if os(macOS) || os(visionOS) + Text("Database:") + .font(.system(size: 14)) + .fontWeight(.regular) +#endif + } +#if os(macOS) || os(visionOS) + .frame(maxWidth: 300) +#endif + + DeleteDatabaseButton { + deleteDatabase() + } + + Spacer() + } +#if os(macOS) || os(visionOS) + .padding(.horizontal, 20) +#elseif os(iOS) + .padding(.leading, 8) +#endif + + DatabaseView(documents: documents) + + HStack { + Spacer() + + ImportFilesButton { urls in + attachFiles(urls) + } + + Button("Index files") { + indexFiles() + } + .buttonStyle(BorderedProminentButtonStyle()) + } + .padding(.horizontal) + + HStack { + ProgressView(value: indexFilesProgress) + } + .opacity(indexing ? 1 : 0) + .padding(.horizontal) + } +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DatabaseView.swift b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DatabaseView.swift new file mode 100644 index 00000000..caa7074d --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DatabaseView.swift @@ -0,0 +1,68 @@ +// +// DatabaseView.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct DatabaseView: View { + var documents: [DocumentSD] + + var body: some View { +#if os(macOS) || os(visionOS) + Table(documents) { + TableColumn("Path") { document in + Text(document.documentUrl?.absoluteString ?? "") + .truncationMode(.head) + .lineLimit(1) + } + .alignment(.trailing) + TableColumn("Status") { document in + HStack { + Spacer() + Group { + if document.status == .indexing { + CircularProgressView(progress: document.indexProgress ?? 0) + .frame(width: 13, height: 13, alignment: .center) + } else { + IndexingStatusView(status: document.status) + } + } + Spacer() + } + } + .width(min: 50, max: 50) + } + +#elseif os(iOS) + List(documents) { document in + HStack { + // Path column + Text(document.documentUrl?.absoluteString ?? "") + .truncationMode(.head) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .trailing) + + // Status column + Group { + if document.status == .indexing { + CircularProgressView(progress: document.indexProgress ?? 0) + .frame(width: 13, height: 13, alignment: .center) + } else { + IndexingStatusView(status: document.status) + } + } + .frame(width: 30, alignment: .center) + } + } + .listStyle(.plain) +#endif + } +} + + +#Preview { + DatabaseView(documents: DocumentSD.sample) +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DeleteDatabaseButton.swift b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DeleteDatabaseButton.swift new file mode 100644 index 00000000..00e227bd --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/DeleteDatabaseButton.swift @@ -0,0 +1,40 @@ +// +// DeleteDatabaseButton.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct DeleteDatabaseButton: View { + var handleDeleteDatabase: () -> () + + @State private var isPresented: Bool = false + + private func onDeleteTap() { + isPresented.toggle() + handleDeleteDatabase() + } + + var body: some View { + Button("Delete") { + isPresented.toggle() + } + .tint(.red) + .buttonStyle(BorderedProminentButtonStyle()) + .alert("Are you sure you want to delete this database?", isPresented: $isPresented, actions: { + Button("Delete", role: .destructive) { + onDeleteTap() + } + + Button("Cancel", role: .cancel) { + isPresented.toggle() + } + }) + } +} + +#Preview { + DeleteDatabaseButton(handleDeleteDatabase: {}) +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/ImportFilesButton.swift b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/ImportFilesButton.swift new file mode 100644 index 00000000..a38b5162 --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/ImportFilesButton.swift @@ -0,0 +1,34 @@ +// +// ImportFilesButton.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct ImportFilesButton: View { + var handleAttachFiles: ([URL]) -> () + + @State private var presentModal: Bool = false + + var body: some View { + Button(action: {presentModal.toggle()}) { + Text("Import files") + } + .fileImporter( + isPresented: $presentModal, + allowedContentTypes: [.data], + allowsMultipleSelection: true, + onCompletion: { result in + switch result { + case .success(let urls): + handleAttachFiles(urls) + + case .failure(let error): + print("File import error: \(error.localizedDescription)") + } + } + ) + } +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/IndexingStatusView.swift b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/IndexingStatusView.swift new file mode 100644 index 00000000..df110df1 --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/DatabaseContainer/DatabaseList/IndexingStatusView.swift @@ -0,0 +1,30 @@ +// +// IndexingStatusView.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct IndexingStatusView: View { + var status: DocumentIndexStatus + var body: some View { + switch status { + case .indexing: return Image(systemName: "hourglass.circle").foregroundColor(.indigo) + case .completed: return Image(systemName: "checkmark.circle").foregroundColor(.green) + case .notStarted: return Image(systemName: "hourglass.circle").foregroundColor(.yellow) + case .failed: return Image(systemName: "x.circle").foregroundColor(.red) + } + } +} + +#Preview { + HStack { + IndexingStatusView(status: .indexing) + IndexingStatusView(status: .completed) + IndexingStatusView(status: .notStarted) + IndexingStatusView(status: .failed) + } + .padding() +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/RetrievalHeader/NewDatabaseButton.swift b/Enchanted/UI/Shared/Retrieval/Components/RetrievalHeader/NewDatabaseButton.swift new file mode 100644 index 00000000..81727079 --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/RetrievalHeader/NewDatabaseButton.swift @@ -0,0 +1,46 @@ +// +// NewDatabaseButton.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct NewDatabaseButton: View { + var createDatabase: (String) -> () + + @State private var isPresented: Bool = false + @State private var databaseName: String = "" + + private func onCreateTap() { + isPresented.toggle() + createDatabase(databaseName) + databaseName = "" + } + + var body: some View { + Button("New Database") { + isPresented.toggle() + } + .alert("Enter database name", isPresented: $isPresented, actions: { + Button("Create") { + onCreateTap() + } + .disabled(databaseName.trimmingCharacters(in: .whitespaces).isEmpty) + + Button("Cancel", role: .cancel) { + isPresented.toggle() + } + + TextField(text: $databaseName, label: { + + }) + .padding() + }) + } +} + +#Preview { + NewDatabaseButton(createDatabase: {_ in}) +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/RetrievalHeader/RetrievalHeaderView.swift b/Enchanted/UI/Shared/Retrieval/Components/RetrievalHeader/RetrievalHeaderView.swift new file mode 100644 index 00000000..bb16ddbd --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/RetrievalHeader/RetrievalHeaderView.swift @@ -0,0 +1,40 @@ +// +// RetrievalHeaderView.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct RetrievalHeaderView: View { + @Environment(\.presentationMode) var presentationMode + + var createDatabase: (String) -> () + + var body: some View { + ZStack { + HStack { + NewDatabaseButton { newDatabaseName in + createDatabase(newDatabaseName) + } + + Spacer() + + Button(action: {presentationMode.wrappedValue.dismiss()}) { + Text("Close") + } + } + + HStack { + Spacer() + Text("Retrieval") + .font(.system(size: 16)) + .fontWeight(.medium) + .foregroundStyle(Color(.label)) + Spacer() + } + } + .padding() + } +} diff --git a/Enchanted/UI/Shared/Retrieval/Components/RetrievalView.swift b/Enchanted/UI/Shared/Retrieval/Components/RetrievalView.swift new file mode 100644 index 00000000..41551fa8 --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Components/RetrievalView.swift @@ -0,0 +1,45 @@ +// +// RetrievalView.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct RetrievalView: View { + @Binding var selectedDatabase: DatabaseSD? + var databases: [DatabaseSD] + var documents: [DocumentSD] + @Binding var indexFilesProgress: Double + @Binding var indexing: Bool + + var createDatabase: (String) -> () + var deleteDatabase: () -> () + var indexFiles: () -> () + var attachFiles: ([URL]) -> () + + + @State private var retrievalStore = RetrievalStore.shared + + var body: some View { + VStack (spacing: 20) { + RetrievalHeaderView { newDatabaseName in + createDatabase(newDatabaseName) + } + + if (retrievalStore.databases.isEmpty) { + DatabaseEmptyView() + } else { + DatabaseListView(selectedDatabase: $selectedDatabase, databases: databases, documents: documents, indexFilesProgress: $indexFilesProgress, indexing: $indexing, deleteDatabase: deleteDatabase, indexFiles: indexFiles, attachFiles: attachFiles) + } + +#if os(iOS) + Spacer() +#endif + } +#if os(macOS) || os(visionOS) + .frame(minWidth: 700, maxWidth: 800, minHeight: 500) +#endif + } +} diff --git a/Enchanted/UI/Shared/Retrieval/Retrieval.swift b/Enchanted/UI/Shared/Retrieval/Retrieval.swift new file mode 100644 index 00000000..12f7ab59 --- /dev/null +++ b/Enchanted/UI/Shared/Retrieval/Retrieval.swift @@ -0,0 +1,105 @@ +// +// Retrieval.swift +// Enchanted +// +// Created by Daniel on 3/19/25. +// + +import SwiftUI + +struct Retrieval: View { + @State private var retrievalStore = RetrievalStore.shared + @State private var languageModelStore = LanguageModelStore.shared + private var svdbService = SVDBService.shared + + @State private var indexFilesProgress: Double = 0 + @State private var indexing: Bool = false + + // Create Database + private func createDatabase(databaseName: String, languageModelName: String) { + Task { + try? await retrievalStore.createDatabase(name: databaseName, indexPath: "./www.com", languageModelName: languageModelName) + retrievalStore.selectDatabase(database: nil, overWrite: true) + } + } + + private func handleCreateDatabase(newDatabaseName: String) { + guard let selectedLanguageModel = languageModelStore.selectedModel else { return } + + createDatabase(databaseName: newDatabaseName, languageModelName: selectedLanguageModel.name) + } + + // Delete Database + private func deleteDatabase(selectedDatabase: DatabaseSD) { + Task { + try? await retrievalStore.deleteDatabase(selectedDatabase: selectedDatabase) + retrievalStore.selectDatabase(database: nil, overWrite: true) + + // delete Collection of SVDB + try await svdbService.releaseCollection(selectedDatabase.id.uuidString) + } + } + + private func handleDeleteDatabase() { + guard let selectedDatabase = retrievalStore.selectedDatabase else { return } + deleteDatabase(selectedDatabase: selectedDatabase) + } + + // Attach Files + private func attachFiles(databaseId: UUID, paths: [URL]) { + Task { + try? await retrievalStore.attachDocuments(databaseId: databaseId, documentPaths: paths) + } + } + + private func handleAttachFiles(urls: [URL]) { + var files = [URL]() + + for url in urls { + guard url.startAccessingSecurityScopedResource() else { + print("Couldn't access URL: \(url)") + continue + } + + defer { url.stopAccessingSecurityScopedResource() } // Ensure we stop accessing when done + + files.append(url) + } + + guard let selectedDatabase = retrievalStore.selectedDatabase else { return } + attachFiles(databaseId: selectedDatabase.id, paths: files) + } + + // Index Files + private func indexFiles(databaseId: UUID) { + Task { + do { + indexing.toggle() + // create vector database collection + try await svdbService.createCollection(databaseId: databaseId) + + try await retrievalStore.indexDocuments(databaseId: databaseId) { progress in + withAnimation { + self.indexFilesProgress = progress + } + } + indexFilesProgress = 0 + indexing.toggle() + } catch { + print("Error indexing documents: \(error)") + } + } + } + + private func handleIndexFiles() { + guard let selectedDatabase = retrievalStore.selectedDatabase, let _ = languageModelStore.selectedModel else { return } + + indexFiles(databaseId: selectedDatabase.id) + } + + + + var body: some View { + RetrievalView(selectedDatabase: $retrievalStore.selectedDatabase, databases: retrievalStore.databases, documents: retrievalStore.selectedDatabase?.documents ?? [], indexFilesProgress: $indexFilesProgress, indexing: $indexing, createDatabase: handleCreateDatabase(newDatabaseName:), deleteDatabase: handleDeleteDatabase, indexFiles: handleIndexFiles, attachFiles: handleAttachFiles(urls:)) + } +} diff --git a/Enchanted/UI/Shared/Sidebar/SidebarView.swift b/Enchanted/UI/Shared/Sidebar/SidebarView.swift index b5016609..f36fd818 100644 --- a/Enchanted/UI/Shared/Sidebar/SidebarView.swift +++ b/Enchanted/UI/Shared/Sidebar/SidebarView.swift @@ -15,6 +15,7 @@ struct SidebarView: View { var onConversationDelete: (_ conversation: ConversationSD) -> () var onDeleteDailyConversations: (_ date: Date) -> () @State var showSettings = false + @State var showRetrieval = false @State var showCompletions = false @State var showKeyboardShortcutas = false @@ -25,6 +26,10 @@ struct SidebarView: View { } } + private func showRetrievalTap() { + showRetrieval.toggle() + } + var body: some View { VStack { ScrollView() { @@ -46,6 +51,8 @@ struct SidebarView: View { SidebarButton(title: "Shortcuts", image: "keyboard.fill", onClick: {showKeyboardShortcutas.toggle()}) #endif + SidebarButton(title: "Retrieval", image: "tray.fill", onClick: showRetrievalTap) + SidebarButton(title: "Settings", image: "gearshape.fill", onClick: onSettingsTap) } @@ -56,6 +63,9 @@ struct SidebarView: View { .sheet(isPresented: $showSettings) { Settings() } + .sheet(isPresented: $showRetrieval) { + Retrieval() + } #if os(macOS) .sheet(isPresented: $showCompletions) { CompletionsEditor() diff --git a/Enchanted/UI/iOS/ChatView_iOS.swift b/Enchanted/UI/iOS/ChatView_iOS.swift index 95e8b072..5d8bb807 100644 --- a/Enchanted/UI/iOS/ChatView_iOS.swift +++ b/Enchanted/UI/iOS/ChatView_iOS.swift @@ -15,7 +15,7 @@ struct ChatView: View { var modelsList: [LanguageModelSD] var onMenuTap: () -> () var onNewConversationTap: () -> () - var onSendMessageTap: @MainActor (_ prompt: String, _ model: LanguageModelSD, _ image: Image?, _ trimmingMessageId: String?) -> () + var onSendMessageTap: @MainActor (_ prompt: String, _ model: LanguageModelSD, _ image: Image?, _ trimmingMessageId: String?, _ documentUsage: Bool?) -> () var conversationState: ConversationState var onStopGenerateTap: @MainActor () -> () var reachable: Bool @@ -32,6 +32,7 @@ struct ChatView: View { /// Image selection @State private var pickerSelectorActive: PhotosPickerItem? @State private var selectedImage: Image? + @State private var documentUsage: Bool = false init( conversation: ConversationSD? = nil, @@ -41,7 +42,7 @@ struct ChatView: View { onSelectModel: @MainActor @escaping (_ model: LanguageModelSD?) -> (), onMenuTap: @escaping () -> Void, onNewConversationTap: @escaping () -> Void, - onSendMessageTap: @MainActor @escaping (_ prompt: String, _ model: LanguageModelSD, _ image: Image?, _ trimmingMessageId: String?) -> Void, + onSendMessageTap: @MainActor @escaping (_ prompt: String, _ model: LanguageModelSD, _ image: Image?, _ trimmingMessageId: String?, _ documentUsage: Bool?) -> Void, conversationState: ConversationState, onStopGenerateTap: @MainActor @escaping () -> Void, reachable: Bool, @@ -72,7 +73,8 @@ struct ChatView: View { message, selectedModel, selectedImage, - editMessage?.id.uuidString + editMessage?.id.uuidString, + documentUsage ) withAnimation { @@ -146,6 +148,8 @@ struct ChatView: View { .frame(minHeight: 40) .font(.system(size: 14)) + UseDocumentView(documentUsage: $documentUsage) + RecordingView(speechRecognizer: speechRecognizer, isRecording: $isRecording.animation()) { transcription in self.message = transcription } @@ -195,7 +199,7 @@ struct ChatView: View { } else { EmptyConversaitonView(sendPrompt: {selectedMessage in if let selectedModel = selectedModel { - onSendMessageTap(selectedMessage, selectedModel, nil, nil) + onSendMessageTap(selectedMessage, selectedModel, nil, nil, false) } }) } @@ -230,7 +234,7 @@ struct ChatView: View { onSelectModel: {_ in }, onMenuTap: {}, onNewConversationTap: { }, - onSendMessageTap: {_,_,_,_ in}, + onSendMessageTap: {_,_,_,_,_ in}, conversationState: .loading, onStopGenerateTap: {}, reachable: false, @@ -248,7 +252,7 @@ struct ChatView: View { onSelectModel: {_ in}, onMenuTap: {}, onNewConversationTap: { }, - onSendMessageTap: {_,_,_,_ in}, + onSendMessageTap: {_,_,_,_,_ in}, conversationState: .completed, onStopGenerateTap: {}, reachable: true, diff --git a/Enchanted/UI/macOS/Chat/ChatView_macOS.swift b/Enchanted/UI/macOS/Chat/ChatView_macOS.swift index 1446225f..41efde8d 100644 --- a/Enchanted/UI/macOS/Chat/ChatView_macOS.swift +++ b/Enchanted/UI/macOS/Chat/ChatView_macOS.swift @@ -16,7 +16,7 @@ struct ChatView: View { var modelsList: [LanguageModelSD] var onMenuTap: () -> () var onNewConversationTap: () -> () - var onSendMessageTap: @MainActor (_ prompt: String, _ model: LanguageModelSD, _ image: Image?, _ trimmingMessageId: String?) -> () + var onSendMessageTap: @MainActor (_ prompt: String, _ model: LanguageModelSD, _ image: Image?, _ trimmingMessageId: String?, _ documentUsage: Bool?) -> () var onConversationTap: (_ conversation: ConversationSD) -> () var conversationState: ConversationState var onStopGenerateTap: @MainActor () -> () @@ -71,7 +71,7 @@ struct ChatView: View { } else { EmptyConversaitonView(sendPrompt: {selectedMessage in if let selectedModel = selectedModel { - onSendMessageTap(selectedMessage, selectedModel, nil, nil) + onSendMessageTap(selectedMessage, selectedModel, nil, nil, false) } }) } @@ -142,7 +142,7 @@ struct ChatView: View { modelsList: LanguageModelSD.sample, onMenuTap: {}, onNewConversationTap: { }, - onSendMessageTap: {_,_,_,_ in}, + onSendMessageTap: {_,_,_,_,_ in}, onConversationTap: {_ in}, conversationState: .completed, onStopGenerateTap: {}, diff --git a/Enchanted/UI/macOS/Chat/Components/InputFields_macOS.swift b/Enchanted/UI/macOS/Chat/Components/InputFields_macOS.swift index bca42bf1..b3978d80 100644 --- a/Enchanted/UI/macOS/Chat/Components/InputFields_macOS.swift +++ b/Enchanted/UI/macOS/Chat/Components/InputFields_macOS.swift @@ -13,7 +13,7 @@ struct InputFieldsView: View { var conversationState: ConversationState var onStopGenerateTap: @MainActor () -> Void var selectedModel: LanguageModelSD? - var onSendMessageTap: @MainActor (_ prompt: String, _ model: LanguageModelSD, _ image: Image?, _ trimmingMessageId: String?) -> () + var onSendMessageTap: @MainActor (_ prompt: String, _ model: LanguageModelSD, _ image: Image?, _ trimmingMessageId: String?,_ documentUsage: Bool?) -> () @Binding var editMessage: MessageSD? @State var isRecording = false @@ -22,6 +22,8 @@ struct InputFieldsView: View { @State private var fileSelectingActive: Bool = false @FocusState private var isFocusedInput: Bool + @State private var documentUsage: Bool = false + @MainActor private func sendMessage() { guard let selectedModel = selectedModel else { return } @@ -29,7 +31,8 @@ struct InputFieldsView: View { message, selectedModel, selectedImage, - editMessage?.id.uuidString + editMessage?.id.uuidString, + documentUsage ) withAnimation { isRecording = false @@ -40,23 +43,6 @@ struct InputFieldsView: View { } } - private func updateSelectedImage(_ image: Image) { - selectedImage = image - } - -#if os(macOS) - var hotkeys: [HotkeyCombination] { - [ - HotkeyCombination(keyBase: [.command], key: .kVK_ANSI_V) { - if let nsImage = Clipboard.shared.getImage() { - let image = Image(nsImage: nsImage) - updateSelectedImage(image) - } - } - ] - } -#endif - var body: some View { HStack(spacing: 20) { if let image = selectedImage { @@ -69,64 +55,15 @@ struct InputFieldsView: View { } ZStack(alignment: .trailing) { - TextField("Message", text: $message.animation(.easeOut(duration: 0.3)), axis: .vertical) - .focused($isFocusedInput) - .font(.system(size: 14)) - .frame(maxWidth:.infinity, minHeight: 40) - .clipped() - .textFieldStyle(.plain) -#if os(macOS) - .onSubmit { - if NSApp.currentEvent?.modifierFlags.contains(.shift) == true { - message += "\n" - } else { - sendMessage() - } - } -#endif - /// TextField bypasses drop area - .allowsHitTesting(!fileDropActive) #if os(macOS) - .addCustomHotkeys(hotkeys) + TextMessageInput(message: $message, fileDropActive: $fileDropActive, sendMessage: sendMessage, isFocusedInput: $isFocusedInput, selectedImage: $selectedImage) +#elseif os(visionOS) + TextMessageInput(message: $message, fileDropActive: $fileDropActive, sendMessage: sendMessage, isFocusedInput: $isFocusedInput) #endif - .padding(.trailing, 80) - - HStack { - RecordingView(isRecording: $isRecording.animation()) { transcription in - withAnimation(.easeIn(duration: 0.3)) { - self.message = transcription - } - } - - 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) { - selectedImage = Image(data: imageData) - } - url.stopAccessingSecurityScopedResource() - case .failure(let error): - print(error) - } - }) - - - Group { - switch conversationState { - case .loading: - SimpleFloatingButton(systemImage: "square.fill", onClick: onStopGenerateTap) - default: - SimpleFloatingButton(systemImage: "paperplane.fill", onClick: { Task { sendMessage() } }) - .showIf(!message.isEmpty) - } - } - + UseDocumentView(documentUsage: $documentUsage) + + AudioMessageInput(message: $message, conversationState: conversationState, onStopGenerateTap: onStopGenerateTap, isRecording: $isRecording, selectedImage: $selectedImage, fileSelectingActive: $fileSelectingActive, sendMessage: sendMessage) } } @@ -165,12 +102,13 @@ struct InputFieldsView: View { } #Preview { - @State var message = "" - return InputFieldsView( + @Previewable @State var message = "" + + InputFieldsView( message: $message, conversationState: .completed, onStopGenerateTap: {}, - onSendMessageTap: {_, _, _, _ in}, + onSendMessageTap: {_, _, _, _, _ in}, editMessage: .constant(nil) ) } diff --git a/Enchanted/UI/macOS/Chat/Components/ToolbarView_macOS.swift b/Enchanted/UI/macOS/Chat/Components/ToolbarView_macOS.swift index 043842b3..49cac31a 100644 --- a/Enchanted/UI/macOS/Chat/Components/ToolbarView_macOS.swift +++ b/Enchanted/UI/macOS/Chat/Components/ToolbarView_macOS.swift @@ -15,7 +15,33 @@ struct ToolbarView: View { var onNewConversationTap: () -> () var copyChat: (_ json: Bool) -> () + @State var showRetrieval = false + + private func showRetrievalTap() { + showRetrieval.toggle() + } + var body: some View { + Button(action: showRetrievalTap) { + HStack(alignment: .center) { + Image(systemName: "opticaldiscdrive") + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(height: 14) + .foregroundColor(Color.labelCustom) + + Text("Retrieval") + .font(.system(size: 14)) + } + .padding(.vertical, 10) + + } + .keyboardShortcut(KeyEquivalent("r"), modifiers: .command) + .sheet(isPresented: $showRetrieval) { + Retrieval() + } + ModelSelectorView( modelsList: modelsList, selectedModel: selectedModel,