diff --git a/electron/main/electron-store/ipcHandlers.ts b/electron/main/electron-store/ipcHandlers.ts index 4bbffe84..b3c2be87 100644 --- a/electron/main/electron-store/ipcHandlers.ts +++ b/electron/main/electron-store/ipcHandlers.ts @@ -95,13 +95,6 @@ export const registerStoreHandlers = (store: Store, windowsManager: return store.get(StoreKeys.LLMGenerationParameters) }) - ipcMain.handle('set-display-markdown', (event, displayMarkdown) => { - store.set(StoreKeys.DisplayMarkdown, displayMarkdown) - event.sender.send('display-markdown-changed', displayMarkdown) - }) - - ipcMain.handle('get-display-markdown', () => store.get(StoreKeys.DisplayMarkdown)) - ipcMain.handle('set-sb-compact', (event, isSBCompact) => { store.set(StoreKeys.IsSBCompact, isSBCompact) event.sender.send('sb-compact-changed', isSBCompact) diff --git a/electron/main/electron-store/storeConfig.ts b/electron/main/electron-store/storeConfig.ts index 1cbf11d2..755464f2 100644 --- a/electron/main/electron-store/storeConfig.ts +++ b/electron/main/electron-store/storeConfig.ts @@ -62,7 +62,6 @@ export interface StoreSchema { analytics?: boolean chunkSize: number isSBCompact: boolean - DisplayMarkdown: boolean spellCheck: string EditorFlexCenter: boolean OpenTabs: Tab[] @@ -83,7 +82,6 @@ export enum StoreKeys { ChatHistories = 'chatHistories', ChunkSize = 'chunkSize', IsSBCompact = 'isSBCompact', - DisplayMarkdown = 'DisplayMarkdown', SpellCheck = 'spellCheck', EditorFlexCenter = 'editorFlexCenter', OpenTabs = 'OpenTabs', diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 18d6301d..8668d364 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -77,8 +77,6 @@ const electronStore = { getChatHistory: createIPCHandler<(chatID: string) => Promise>('get-chat-history'), getSBCompact: createIPCHandler<() => Promise>('get-sb-compact'), setSBCompact: createIPCHandler<(isSBCompact: boolean) => Promise>('set-sb-compact'), - getDisplayMarkdown: createIPCHandler<() => Promise>('get-display-markdown'), - setDisplayMarkdown: createIPCHandler<(displayMarkdown: boolean) => Promise>('set-display-markdown'), getEditorFlexCenter: createIPCHandler<() => Promise>('get-editor-flex-center'), setEditorFlexCenter: createIPCHandler<(editorFlexCenter: boolean) => Promise>('set-editor-flex-center'), getCurrentOpenTabs: createIPCHandler<() => Promise>('get-current-open-files'), @@ -110,6 +108,7 @@ const fileSystem = { getFilesystemPathsAsDBItems: createIPCHandler<(paths: string[]) => Promise>( 'get-filesystem-paths-as-db-items', ), + getAllFilenamesInDirectory: createIPCHandler<(dirName: string) => Promise>('get-files-in-directory'), } const path = { @@ -122,7 +121,6 @@ const path = { 'add-extension-if-no-extension-present', ), pathSep: createIPCHandler<() => Promise>('path-sep'), - getAllFilenamesInDirectory: createIPCHandler<(dirName: string) => Promise>('get-files-in-directory'), } const llm = { diff --git a/src/App.tsx b/src/App.tsx index 2bc1d88a..71d85fea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,6 @@ import 'react-toastify/dist/ReactToastify.css' import IndexingProgress from './components/Common/IndexingProgress' import MainPageComponent from './components/MainPage' import InitialSetupSinglePage from './components/Settings/InitialSettingsSinglePage' -import { ModalProvider } from './providers/ModalProvider' interface AppProps {} @@ -83,11 +82,7 @@ const App: React.FC = () => { {userHasConfiguredSettingsForIndexing && indexingProgress < 1 && ( )} - {userHasConfiguredSettingsForIndexing && indexingProgress >= 1 && ( - - - - )} + {userHasConfiguredSettingsForIndexing && indexingProgress >= 1 && } ) } diff --git a/src/components/Chat/AddContextFiltersModal.tsx b/src/components/Chat/AddContextFiltersModal.tsx index 2e91c759..5e6249b3 100644 --- a/src/components/Chat/AddContextFiltersModal.tsx +++ b/src/components/Chat/AddContextFiltersModal.tsx @@ -17,12 +17,12 @@ import { ChatFilters } from './types' interface Props { isOpen: boolean onClose: () => void - vaultDirectory: string setChatFilters: (chatFilters: ChatFilters) => void chatFilters: ChatFilters } -const AddContextFiltersModal: React.FC = ({ vaultDirectory, isOpen, onClose, chatFilters, setChatFilters }) => { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const AddContextFiltersModal: React.FC = ({ isOpen, onClose, chatFilters, setChatFilters }) => { const [internalFilesSelected, setInternalFilesSelected] = useState(chatFilters?.files || []) const [searchText, setSearchText] = useState('') const [suggestionsState, setSuggestionsState] = useState(null) @@ -41,16 +41,16 @@ const AddContextFiltersModal: React.FC = ({ vaultDirectory, isOpen, onClo { label: 'Past year', value: 'lastYear' }, ] - useEffect(() => { - const updatedChatFilters: ChatFilters = { - ...chatFilters, - files: [...new Set([...chatFilters.files, ...internalFilesSelected])], - numberOfChunksToFetch, - minDate: minDate || undefined, - maxDate: maxDate || undefined, - } - setChatFilters(updatedChatFilters) - }, [internalFilesSelected, numberOfChunksToFetch, minDate, maxDate, chatFilters, setChatFilters]) + // useEffect(() => { + // const updatedChatFilters: ChatFilters = { + // ...chatFilters, + // files: [...new Set([...chatFilters.files, ...internalFilesSelected])], + // numberOfChunksToFetch, + // minDate: minDate || undefined, + // maxDate: maxDate || undefined, + // } + // setChatFilters(updatedChatFilters) + // }, [internalFilesSelected, numberOfChunksToFetch, minDate, maxDate, setChatFilters]) const handleNumberOfChunksChange = (event: Event, value: number | number[]) => { const newValue = Array.isArray(value) ? value[0] : value @@ -110,7 +110,6 @@ const AddContextFiltersModal: React.FC = ({ vaultDirectory, isOpen, onClo

Select files for context

{ diff --git a/src/components/Chat/ChatMessages.tsx b/src/components/Chat/ChatMessages.tsx index a35b67d7..e77f9f4a 100644 --- a/src/components/Chat/ChatMessages.tsx +++ b/src/components/Chat/ChatMessages.tsx @@ -12,7 +12,8 @@ import PromptSuggestion from './ChatPrompts' import LoadingDots from '@/utils/animations' import '../../styles/chat.css' import { ReorChatMessage } from './types' -import { useChatContext } from '@/providers/ChatContext' +import { useChatContext } from '@/contexts/ChatContext' +import { useTabsContext } from '@/contexts/TabContext' export enum AskOptions { Ask = 'Ask', @@ -28,11 +29,9 @@ export const EXAMPLE_PROMPTS: { [key: string]: string[] } = { interface ChatMessagesProps { chatContainerRef: MutableRefObject - openFileAndOpenEditor: (path: string, optionalContentToWriteOnCreate?: string) => Promise isAddContextFiltersModalOpen: boolean setUserTextFieldInput: Dispatch> defaultModelName: string - vaultDirectory: string setIsAddContextFiltersModalOpen: Dispatch> handlePromptSelection: (prompt: string | undefined) => void askText: AskOptions @@ -41,20 +40,16 @@ interface ChatMessagesProps { const ChatMessages: React.FC = ({ chatContainerRef, - openFileAndOpenEditor, - // currentChatHistory, isAddContextFiltersModalOpen, - // chatFilters, - // setChatFilters, setUserTextFieldInput, defaultModelName, - vaultDirectory, setIsAddContextFiltersModalOpen, handlePromptSelection, askText, loadAnimation, }) => { const { currentChatHistory, chatFilters, setChatFilters } = useChatContext() + const { openTabContent } = useTabsContext() const [llmConfigs, setLLMConfigs] = useState([]) const [selectedLlm, setSelectedLlm] = useState(defaultModelName) @@ -73,7 +68,7 @@ const ChatMessages: React.FC = ({ const createNewNote = async (message: ReorChatMessage) => { const title = `${(getDisplayMessage(message) ?? `${new Date().toDateString()}`).substring(0, 20)}...` - openFileAndOpenEditor(title, getDisplayMessage(message)) + openTabContent(title, getDisplayMessage(message)) } useEffect(() => { @@ -216,7 +211,6 @@ const ChatMessages: React.FC = ({ {isAddContextFiltersModalOpen && ( setIsAddContextFiltersModalOpen(false)} chatFilters={chatFilters} diff --git a/src/components/Chat/ChatsSidebar.tsx b/src/components/Chat/ChatsSidebar.tsx index f1fb0833..6ad93001 100644 --- a/src/components/Chat/ChatsSidebar.tsx +++ b/src/components/Chat/ChatsSidebar.tsx @@ -4,7 +4,7 @@ import { RiChatNewFill, RiArrowDownSLine } from 'react-icons/ri' import { IoChatbubbles } from 'react-icons/io5' import posthog from 'posthog-js' import { ChatHistoryMetadata } from './hooks/use-chat-history' -import { useChatContext } from '@/providers/ChatContext' +import { useChatContext } from '@/contexts/ChatContext' export interface ChatItemProps { chatMetadata: ChatHistoryMetadata diff --git a/src/components/Chat/ChatWrapper.tsx b/src/components/Chat/index.tsx similarity index 92% rename from src/components/Chat/ChatWrapper.tsx rename to src/components/Chat/index.tsx index 588fc0c8..82532f72 100644 --- a/src/components/Chat/ChatWrapper.tsx +++ b/src/components/Chat/index.tsx @@ -11,15 +11,14 @@ import SimilarEntriesComponent from '../Sidebars/SemanticSidebar/SimilarEntriesC import '../../styles/chat.css' import ChatMessages, { AskOptions } from './ChatMessages' import { Chat } from './types' -import { useChatContext } from '@/providers/ChatContext' +import { useChatContext } from '@/contexts/ChatContext' +import { useTabsContext } from '@/contexts/TabContext' -interface ChatWrapperProps { - vaultDirectory: string - openFileAndOpenEditor: (path: string, optionalContentToWriteOnCreate?: string) => Promise +interface ChatComponentProps { showSimilarFiles: boolean } -const ChatWrapper: React.FC = ({ vaultDirectory, openFileAndOpenEditor, showSimilarFiles }) => { +const ChatComponent: React.FC = ({ showSimilarFiles }) => { const [userTextFieldInput, setUserTextFieldInput] = useState('') const [askText] = useState(AskOptions.Ask) const [loadingResponse, setLoadingResponse] = useState(false) @@ -32,6 +31,7 @@ const ChatWrapper: React.FC = ({ vaultDirectory, openFileAndOp const chatContainerRef = useRef(null) const { setCurrentChatHistory, currentChatHistory, chatFilters } = useChatContext() + const { openTabContent } = useTabsContext() useEffect(() => { const fetchDefaultLLM = async () => { @@ -181,11 +181,9 @@ const ChatWrapper: React.FC = ({ vaultDirectory, openFileAndOp
= ({ vaultDirectory, openFileAndOp { - openFileAndOpenEditor(path) + onSelect={(path: string) => { + openTabContent(path) posthog.capture('open_file_from_chat_context') }} - saveCurrentFile={() => Promise.resolve()} isLoadingSimilarEntries={false} /> )} @@ -217,4 +214,4 @@ const ChatWrapper: React.FC = ({ vaultDirectory, openFileAndOp ) } -export default ChatWrapper +export default ChatComponent diff --git a/src/components/Common/EmptyPage.tsx b/src/components/Common/EmptyPage.tsx index f9fc2295..851d32b6 100644 --- a/src/components/Common/EmptyPage.tsx +++ b/src/components/Common/EmptyPage.tsx @@ -1,14 +1,10 @@ import React from 'react' import { ImFileEmpty } from 'react-icons/im' -import { useModalOpeners } from '../../providers/ModalProvider' +import { useModalOpeners } from '../../contexts/ModalContext' import NewNoteComponent from '../File/NewNote' import NewDirectoryComponent from '../File/NewDirectory' -interface EmptyPageProps { - openFileAndOpenEditor: (filePath: string, optionalContentToWriteOnCreate?: string) => Promise -} - -const EmptyPage: React.FC = ({ openFileAndOpenEditor }) => { +const EmptyPage: React.FC = () => { const { isNewNoteModalOpen, setIsNewNoteModalOpen, isNewDirectoryModalOpen, setIsNewDirectoryModalOpen } = useModalOpeners() @@ -35,17 +31,8 @@ const EmptyPage: React.FC = ({ openFileAndOpenEditor }) => { Create a Folder
- setIsNewNoteModalOpen(false)} - openFileAndOpenEditor={openFileAndOpenEditor} - currentOpenFilePath="" - /> - setIsNewDirectoryModalOpen(false)} - currentOpenFilePath="" - /> + setIsNewNoteModalOpen(false)} /> + setIsNewDirectoryModalOpen(false)} />
) } diff --git a/src/components/Common/SearchBarWithFilesSuggestion.tsx b/src/components/Common/SearchBarWithFilesSuggestion.tsx index d1fce152..0cc7f7a7 100644 --- a/src/components/Common/SearchBarWithFilesSuggestion.tsx +++ b/src/components/Common/SearchBarWithFilesSuggestion.tsx @@ -1,10 +1,9 @@ import React, { useEffect, useRef, useState } from 'react' import InEditorBacklinkSuggestionsDisplay, { SuggestionsState } from '../Editor/BacklinkSuggestionsDisplay' -import useFileInfoTree from '../Sidebars/FileSideBar/hooks/use-file-info-tree' +import useFileInfoTreeHook from '../Sidebars/FileSideBar/hooks/use-file-info-tree' interface Props { - vaultDirectory: string searchText: string setSearchText: (text: string) => void onSelectSuggestion: (suggestion: string) => void @@ -13,14 +12,16 @@ interface Props { } const SearchBarWithFilesSuggestion = ({ - vaultDirectory, searchText, setSearchText, onSelectSuggestion, suggestionsState, setSuggestionsState, }: Props) => { - const { flattenedFiles } = useFileInfoTree(vaultDirectory) + const [sidebarWidth, setSidebarWidth] = useState(0) + const [vaultDirectory, setVaultDirectory] = useState(null) + + const { flattenedFiles } = useFileInfoTreeHook(vaultDirectory) const inputRef = useRef(null) const initializeSuggestionsStateOnFocus = () => { @@ -39,7 +40,13 @@ const SearchBarWithFilesSuggestion = ({ }) } - const [sidebarWidth, setSidebarWidth] = useState(0) + useEffect(() => { + const setFileDirectory = async () => { + const windowDirectory = await window.electronStore.getVaultDirectoryForWindow() + setVaultDirectory(windowDirectory) + } + setFileDirectory() + }, []) useEffect(() => { // Calculate the width of the sidebar diff --git a/src/components/Editor/EditorManager.tsx b/src/components/Editor/EditorManager.tsx index a3029fd8..6c311790 100644 --- a/src/components/Editor/EditorManager.tsx +++ b/src/components/Editor/EditorManager.tsx @@ -1,22 +1,11 @@ import React, { useEffect, useState } from 'react' -import { Editor, EditorContent } from '@tiptap/react' -import InEditorBacklinkSuggestionsDisplay, { SuggestionsState } from './BacklinkSuggestionsDisplay' +import { EditorContent } from '@tiptap/react' +import InEditorBacklinkSuggestionsDisplay from './BacklinkSuggestionsDisplay' import EditorContextMenu from './EditorContextMenu' import SearchBar from './Search/SearchBar' +import { useFileContext } from '@/contexts/FileContext' -interface EditorManagerProps { - editor: Editor | null - suggestionsState: SuggestionsState | null | undefined - flattenedFiles: { relativePath: string }[] - showSimilarFiles: boolean -} - -const EditorManager: React.FC = ({ - editor, - suggestionsState, - flattenedFiles, - showSimilarFiles, -}) => { +const EditorManager: React.FC = () => { const [showSearchBar, setShowSearchBar] = useState(false) const [menuVisible, setMenuVisible] = useState(false) const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }) @@ -24,7 +13,7 @@ const EditorManager: React.FC = ({ const [showPlaceholder, setShowPlaceholder] = useState(false) const [placeholderPosition, setPlaceholderPosition] = useState({ top: 0, left: 0 }) - useEffect(() => {}, [showSimilarFiles]) + const { editor, suggestionsState, flattenedFiles } = useFileContext() const handleContextMenu = (event: React.MouseEvent) => { event.preventDefault() diff --git a/src/components/Editor/utils.ts b/src/components/Editor/utils.ts new file mode 100644 index 00000000..f2797c44 --- /dev/null +++ b/src/components/Editor/utils.ts @@ -0,0 +1,14 @@ +import { Editor } from '@tiptap/core' + +function getMarkdown(editor: Editor) { + // Fetch the current markdown content from the editor + const originalMarkdown = editor.storage.markdown.getMarkdown() + // Replace the escaped square brackets with unescaped ones + const modifiedMarkdown = originalMarkdown + .replace(/\\\[/g, '[') // Replaces \[ with [ + .replace(/\\\]/g, ']') // Replaces \] wi ] + + return modifiedMarkdown +} + +export default getMarkdown diff --git a/src/components/File/NewDirectory.tsx b/src/components/File/NewDirectory.tsx index 01fd13e8..e78c1e22 100644 --- a/src/components/File/NewDirectory.tsx +++ b/src/components/File/NewDirectory.tsx @@ -5,17 +5,19 @@ import posthog from 'posthog-js' import ReorModal from '../Common/Modal' import { getInvalidCharacterInFileName } from '@/utils/strings' +import { useFileContext } from '@/contexts/FileContext' interface NewDirectoryComponentProps { isOpen: boolean onClose: () => void - currentOpenFilePath: string | null } -const NewDirectoryComponent: React.FC = ({ isOpen, onClose, currentOpenFilePath }) => { +const NewDirectoryComponent: React.FC = ({ isOpen, onClose }) => { const [directoryName, setDirectoryName] = useState('') const [errorMessage, setErrorMessage] = useState(null) + const { currentlyOpenFilePath } = useFileContext() + useEffect(() => { if (!isOpen) { setDirectoryName('') @@ -40,12 +42,12 @@ const NewDirectoryComponent: React.FC = ({ isOpen, o const sendNewDirectoryMsg = async () => { await handleValidName(directoryName) - if (!directoryName || errorMessage || currentOpenFilePath === null) return + if (!directoryName || errorMessage || currentlyOpenFilePath === null) return const directoryPath = - currentOpenFilePath === '' + currentlyOpenFilePath === '' ? await window.electronStore.getVaultDirectoryForWindow() - : await window.path.dirname(currentOpenFilePath) + : await window.path.dirname(currentlyOpenFilePath) const finalPath = await window.path.join(directoryPath, directoryName) window.fileSystem.createDirectory(finalPath) posthog.capture('created_new_directory_from_new_directory_modal') diff --git a/src/components/File/NewNote.tsx b/src/components/File/NewNote.tsx index 3685225d..052f8583 100644 --- a/src/components/File/NewNote.tsx +++ b/src/components/File/NewNote.tsx @@ -5,23 +5,21 @@ import posthog from 'posthog-js' import ReorModal from '../Common/Modal' import { getInvalidCharacterInFileName } from '@/utils/strings' +import { useFileContext } from '@/contexts/FileContext' +import { useTabsContext } from '@/contexts/TabContext' interface NewNoteComponentProps { isOpen: boolean onClose: () => void - openFileAndOpenEditor: (path: string, optionalContentToWriteOnCreate?: string) => void - currentOpenFilePath: string | null } -const NewNoteComponent: React.FC = ({ - isOpen, - onClose, - openFileAndOpenEditor, - currentOpenFilePath, -}) => { +const NewNoteComponent: React.FC = ({ isOpen, onClose }) => { + const { openTabContent } = useTabsContext() const [fileName, setFileName] = useState('') const [errorMessage, setErrorMessage] = useState(null) + const { currentlyOpenFilePath } = useFileContext() + useEffect(() => { if (!isOpen) { setFileName('') @@ -49,12 +47,12 @@ const NewNoteComponent: React.FC = ({ if (!fileName || errorMessage) return let finalPath = fileName - if (currentOpenFilePath !== '' && currentOpenFilePath !== null) { - const directoryName = await window.path.dirname(currentOpenFilePath) + if (currentlyOpenFilePath !== '' && currentlyOpenFilePath !== null) { + const directoryName = await window.path.dirname(currentlyOpenFilePath) finalPath = await window.path.join(directoryName, fileName) } const basename = await window.path.basename(finalPath) - openFileAndOpenEditor(finalPath, `# ${basename}\n`) + openTabContent(finalPath, `# ${basename}\n`) posthog.capture('created_new_note_from_new_note_modal') onClose() } diff --git a/src/components/File/RenameDirectory.tsx b/src/components/File/RenameDirectory.tsx index cb44f46d..a079c697 100644 --- a/src/components/File/RenameDirectory.tsx +++ b/src/components/File/RenameDirectory.tsx @@ -6,20 +6,10 @@ import { toast } from 'react-toastify' import ReorModal from '../Common/Modal' import { getInvalidCharacterInFileName } from '@/utils/strings' +import { useFileContext } from '@/contexts/FileContext' -export interface RenameDirFuncProps { - path: string - newDirName: string -} - -interface RenameDirModalProps { - isOpen: boolean - fullDirName: string - onClose: () => void - renameDir: (props: RenameDirFuncProps) => Promise -} - -const RenameDirModal: React.FC = ({ isOpen, fullDirName, onClose, renameDir }) => { +const RenameDirModal: React.FC = () => { + const { fileDirToBeRenamed, setFileDirToBeRenamed, renameFile } = useFileContext() const [isUpdatingDirName, setIsUpdatingDirName] = useState(false) const [dirPrefix, setDirPrefix] = useState('') @@ -28,14 +18,14 @@ const RenameDirModal: React.FC = ({ isOpen, fullDirName, on useEffect(() => { const setDirectoryUponNoteChange = async () => { - const initialDirPathPrefix = await window.path.dirname(fullDirName) + const initialDirPathPrefix = await window.path.dirname(fileDirToBeRenamed) setDirPrefix(initialDirPathPrefix) - const trimmedInitialDirName = await window.path.basename(fullDirName) + const trimmedInitialDirName = await window.path.basename(fileDirToBeRenamed) setDirName(trimmedInitialDirName) } setDirectoryUponNoteChange() - }, [fullDirName]) + }, [fileDirToBeRenamed]) const handleNameChange = (e: React.ChangeEvent) => { const newName = e.target.value @@ -50,6 +40,10 @@ const RenameDirModal: React.FC = ({ isOpen, fullDirName, on }) } + const onClose = () => { + setFileDirToBeRenamed('') + } + const sendDirRename = async () => { if (errorMessage) { return @@ -65,10 +59,11 @@ const RenameDirModal: React.FC = ({ isOpen, fullDirName, on setIsUpdatingDirName(true) // get full path of new directory - await renameDir({ - path: `${fullDirName}`, - newDirName: `${dirPrefix}${dirName}`, - }) + // await renameDir({ + // path: `${fileDirToBeRenamed}`, + // newDirName: `${dirPrefix}${dirName}`, + // }) + await renameFile(fileDirToBeRenamed, `${dirPrefix}${dirName}`) onClose() setIsUpdatingDirName(false) } @@ -80,7 +75,7 @@ const RenameDirModal: React.FC = ({ isOpen, fullDirName, on } return ( - +

Rename Directory

void - renameNote: (props: RenameNoteFuncProps) => Promise -} +const RenameNoteModal: React.FC = () => { + const { noteToBeRenamed, setNoteToBeRenamed, renameFile } = useFileContext() -const RenameNoteModal: React.FC = ({ isOpen, fullNoteName, onClose, renameNote }) => { - const fileExtension = fullNoteName.split('.').pop() || 'md' + const fileExtension = noteToBeRenamed.split('.').pop() || 'md' const [dirPrefix, setDirPrefix] = useState('') const [noteName, setNoteName] = useState('') const [errorMessage, setErrorMessage] = useState(null) useEffect(() => { const setDirectoryUponNoteChange = async () => { - const initialNotePathPrefix = await window.path.dirname(fullNoteName) + const initialNotePathPrefix = await window.path.dirname(noteToBeRenamed) setDirPrefix(initialNotePathPrefix) - const initialNoteName = await window.path.basename(fullNoteName) + const initialNoteName = await window.path.basename(noteToBeRenamed) const trimmedInitialNoteName = removeFileExtension(initialNoteName) || '' setNoteName(trimmedInitialNoteName) } setDirectoryUponNoteChange() - }, [fullNoteName]) + }, [noteToBeRenamed]) const handleNameChange = (e: React.ChangeEvent) => { const newName = e.target.value @@ -49,6 +40,9 @@ const RenameNoteModal: React.FC = ({ isOpen, fullNoteName, } }) } + const onClose = () => { + setNoteToBeRenamed('') + } const sendNoteRename = async () => { if (errorMessage) { @@ -63,11 +57,7 @@ const RenameNoteModal: React.FC = ({ isOpen, fullNoteName, return } - // get full path of note - await renameNote({ - path: `${fullNoteName}`, - newNoteName: `${dirPrefix}${noteName}.${fileExtension}`, - }) + await renameFile(noteToBeRenamed, `${dirPrefix}${noteName}.${fileExtension}`) onClose() } @@ -78,7 +68,7 @@ const RenameNoteModal: React.FC = ({ isOpen, fullNoteName, } return ( - +

Rename Note

{ - const [currentlyOpenedFilePath, setCurrentlyOpenedFilePath] = useState(null) + const [currentlyOpenFilePath, setCurrentlyOpenFilePath] = useState(null) const [suggestionsState, setSuggestionsState] = useState() const [needToWriteEditorContentToDisk, setNeedToWriteEditorContentToDisk] = useState(false) const [needToIndexEditorContent, setNeedToIndexEditorContent] = useState(false) @@ -52,8 +53,6 @@ const useFileByFilepath = () => { text: '', position: null, }) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [displayMarkdown, setDisplayMarkdown] = useState(false) const setFileNodeToBeRenamed = async (filePath: string) => { const isDirectory = await window.fileSystem.isDirectory(filePath) @@ -71,7 +70,6 @@ const useFileByFilepath = () => { 3. when the file is deleted */ - // This function handles the creation of a file if it doesn't exist const createFileIfNotExists = async (filePath: string, optionalContent?: string): Promise => { const invalidChars = await getInvalidCharacterInFilePath(filePath) if (invalidChars) { @@ -96,14 +94,14 @@ const useFileByFilepath = () => { const loadFileIntoEditor = async (filePath: string) => { setCurrentlyChangingFilePath(true) - await writeEditorContentToDisk(editor, currentlyOpenedFilePath) - if (currentlyOpenedFilePath && needToIndexEditorContent) { - window.fileSystem.indexFileInDatabase(currentlyOpenedFilePath) + await writeEditorContentToDisk(editor, currentlyOpenFilePath) + if (currentlyOpenFilePath && needToIndexEditorContent) { + window.fileSystem.indexFileInDatabase(currentlyOpenFilePath) setNeedToIndexEditorContent(false) } const fileContent = (await window.fileSystem.readFile(filePath)) ?? '' editor?.commands.setContent(fileContent) - setCurrentlyOpenedFilePath(filePath) + setCurrentlyOpenFilePath(filePath) setCurrentlyChangingFilePath(false) } @@ -116,22 +114,6 @@ const useFileByFilepath = () => { const openRelativePathRef = useRef<(newFilePath: string) => Promise>() // openRelativePathRef.current = openOrCreateFile - // Check if we should display markdown or not - useEffect(() => { - const handleInitialStartup = async () => { - const isMarkdownSet = await window.electronStore.getDisplayMarkdown() - setDisplayMarkdown(isMarkdownSet) - } - - // Even listener - const handleChangeMarkdown = (isMarkdownSet: boolean) => { - setDisplayMarkdown(isMarkdownSet) - } - - handleInitialStartup() - window.ipcRenderer.receive('display-markdown-changed', handleChangeMarkdown) - }, []) - const editor = useEditor({ autofocus: true, @@ -198,12 +180,12 @@ const useFileByFilepath = () => { useEffect(() => { if (debouncedEditor && !currentlyChangingFilePath) { - writeEditorContentToDisk(editor, currentlyOpenedFilePath) + writeEditorContentToDisk(editor, currentlyOpenFilePath) } - }, [debouncedEditor, currentlyOpenedFilePath, editor, currentlyChangingFilePath]) + }, [debouncedEditor, currentlyOpenFilePath, editor, currentlyChangingFilePath]) const saveCurrentlyOpenedFile = async () => { - await writeEditorContentToDisk(editor, currentlyOpenedFilePath) + await writeEditorContentToDisk(editor, currentlyOpenFilePath) } const writeEditorContentToDisk = async (_editor: Editor | null, filePath: string | null) => { @@ -220,15 +202,13 @@ const useFileByFilepath = () => { } } - // delete file depending on file path returned by the listener useEffect(() => { const deleteFile = async (path: string) => { await window.fileSystem.deleteFile(path) window.electronStore.removeOpenTabsByPath(path) - // if it is the current file, clear the content and set filepath to null so that it won't save anything else - if (currentlyOpenedFilePath === path) { + if (currentlyOpenFilePath === path) { editor?.commands.setContent('') - setCurrentlyOpenedFilePath(null) + setCurrentlyOpenFilePath(null) } } @@ -237,11 +217,11 @@ const useFileByFilepath = () => { return () => { removeDeleteFileListener() } - }, [currentlyOpenedFilePath, editor]) + }, [currentlyOpenFilePath, editor]) useEffect(() => { async function checkAppUsage() { - if (!editor || currentlyOpenedFilePath) return + if (!editor || currentlyOpenFilePath) return const hasOpened = await window.electronStore.getHasUserOpenedAppBefore() if (!hasOpened) { @@ -251,7 +231,7 @@ const useFileByFilepath = () => { } checkAppUsage() - }, [editor, currentlyOpenedFilePath]) + }, [editor, currentlyOpenFilePath]) const renameFileNode = async (oldFilePath: string, newFilePath: string) => { await window.fileSystem.renameFileRecursive({ @@ -264,8 +244,8 @@ const useFileByFilepath = () => { setNavigationHistory(navigationHistoryUpdated) // reset the editor to the new file path - if (currentlyOpenedFilePath === oldFilePath) { - setCurrentlyOpenedFilePath(newFilePath) + if (currentlyOpenFilePath === oldFilePath) { + setCurrentlyOpenFilePath(newFilePath) } } @@ -282,13 +262,13 @@ const useFileByFilepath = () => { useEffect(() => { const handleWindowClose = async () => { - if (currentlyOpenedFilePath !== null && editor && editor.getHTML() !== null) { + if (currentlyOpenFilePath !== null && editor && editor.getHTML() !== null) { const markdown = getMarkdown(editor) await window.fileSystem.writeFile({ - filePath: currentlyOpenedFilePath, + filePath: currentlyOpenFilePath, content: markdown, }) - await window.fileSystem.indexFileInDatabase(currentlyOpenedFilePath) + await window.fileSystem.indexFileInDatabase(currentlyOpenFilePath) } } @@ -297,11 +277,11 @@ const useFileByFilepath = () => { return () => { removeWindowCloseListener() } - }, [currentlyOpenedFilePath, editor]) + }, [currentlyOpenFilePath, editor]) return { - filePath: currentlyOpenedFilePath, - setFilePath: setCurrentlyOpenedFilePath, + currentlyOpenFilePath, + setCurrentlyOpenFilePath, saveCurrentlyOpenedFile, editor, navigationHistory, @@ -320,15 +300,4 @@ const useFileByFilepath = () => { } } -function getMarkdown(editor: Editor) { - // Fetch the current markdown content from the editor - const originalMarkdown = editor.storage.markdown.getMarkdown() - // Replace the escaped square brackets with unescaped ones - const modifiedMarkdown = originalMarkdown - .replace(/\\\[/g, '[') // Replaces \[ with [ - .replace(/\\\]/g, ']') // Replaces \] wi ] - - return modifiedMarkdown -} - export default useFileByFilepath diff --git a/src/components/Flashcard/FlashcardCreateModal.tsx b/src/components/Flashcard/FlashcardCreateModal.tsx index a7673fc1..80f242aa 100644 --- a/src/components/Flashcard/FlashcardCreateModal.tsx +++ b/src/components/Flashcard/FlashcardCreateModal.tsx @@ -14,7 +14,7 @@ import FlashcardCore from './FlashcardsCore' import { FlashcardQAPairSchema, FlashcardQAPairUI } from './types' import { storeFlashcardPairsAsJSON } from './utils' import { resolveLLMClient } from '../Chat/utils' -import useFileInfoTree from '../Sidebars/FileSideBar/hooks/use-file-info-tree' +import useFileInfoTreeHook from '../Sidebars/FileSideBar/hooks/use-file-info-tree' interface FlashcardCreateModalProps { isOpen: boolean @@ -29,7 +29,7 @@ const FlashcardCreateModal: React.FC = ({ isOpen, onC const [selectedFile, setSelectedFile] = useState(initialFlashcardFile) const [vaultDirectory, setVaultDirectory] = useState('') - const { flattenedFiles } = useFileInfoTree(vaultDirectory) + const { flattenedFiles } = useFileInfoTreeHook(vaultDirectory) const { suggestionsState, setSuggestionsState } = useFileByFilepath() const [searchText, setSearchText] = useState(initialFlashcardFile) diff --git a/src/components/Flashcard/FlashcardReviewModal.tsx b/src/components/Flashcard/FlashcardReviewModal.tsx index f3a4be55..ccdb2f89 100644 --- a/src/components/Flashcard/FlashcardReviewModal.tsx +++ b/src/components/Flashcard/FlashcardReviewModal.tsx @@ -21,7 +21,7 @@ const FlashcardReviewModal: React.FC = ({ isOpen, onC useEffect(() => { const getFlashcardsFromDirectory = async () => { const vaultDirectoryWithFlashcards = await getFlashcardVaultDirectory() - const files = await window.path.getAllFilenamesInDirectory(vaultDirectoryWithFlashcards) + const files = await window.fileSystem.getAllFilenamesInDirectory(vaultDirectoryWithFlashcards) setFlashcardFiles(files) setCurrentSelectedFlashcard(0) } diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index ca2cfca6..cdc600e0 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -1,219 +1,71 @@ -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import '../styles/global.css' -import ChatWrapper from './Chat/ChatWrapper' +import ChatComponent from './Chat' import ResizableComponent from './Common/ResizableComponent' import TitleBar from './TitleBar/TitleBar' import EditorManager from './Editor/EditorManager' -import useFileByFilepath from './File/hooks/use-file-by-filepath' import IconsSidebar from './Sidebars/IconsSidebar' -import SidebarManager, { SidebarAbleToShow } from './Sidebars/MainSidebar' +import SidebarManager from './Sidebars/MainSidebar' import SimilarFilesSidebarComponent from './Sidebars/SimilarFilesSidebar' import EmptyPage from './Common/EmptyPage' -import { TabProvider } from '../providers/TabProvider' +import { TabProvider } from '../contexts/TabContext' import WritingAssistant from './WritingAssistant/WritingAssistant' -import useFileInfoTree from './Sidebars/FileSideBar/hooks/use-file-info-tree' -import { ChatProvider, useChatContext } from '@/providers/ChatContext' - -const UNINITIALIZED_STATE = 'UNINITIALIZED_STATE' +import { ChatProvider, useChatContext } from '@/contexts/ChatContext' +import { FileProvider, useFileContext } from '@/contexts/FileContext' +import ModalProvider from '@/contexts/ModalContext' const MainPageContent: React.FC = () => { const [showSimilarFiles, setShowSimilarFiles] = useState(true) - const [sidebarShowing, setSidebarShowing] = useState('files') - const [currentTab, setCurrentTab] = useState('') - const [vaultDirectory, setVaultDirectory] = useState('') - const filePathRef = React.useRef('') - const chatIDRef = React.useRef('') - - const { - showChatbot, - setShowChatbot, - currentChatHistory, - setCurrentChatHistory, - setChatFilters, - openChatSidebarAndChat, - chatHistoriesMetadata, - } = useChatContext() - - const { - filePath, - setFilePath, - editor, - openOrCreateFile, - saveCurrentlyOpenedFile, - suggestionsState, - highlightData, - noteToBeRenamed, - setNoteToBeRenamed, - fileDirToBeRenamed, - setFileDirToBeRenamed, - renameFile, - navigationHistory, - setNavigationHistory, - } = useFileByFilepath() - - useEffect(() => { - if (filePath != null && filePathRef.current !== filePath) { - filePathRef.current = filePath - setCurrentTab(filePath) - } - - const currentChatHistoryId = currentChatHistory?.id ?? '' - if (chatIDRef.current !== currentChatHistoryId) { - chatIDRef.current = currentChatHistoryId - const currentMetadata = chatHistoriesMetadata.find((chat) => chat.id === currentChatHistoryId) - if (currentMetadata) { - setCurrentTab(currentMetadata.displayName) - } - } - }, [currentChatHistory, chatHistoriesMetadata, filePath]) - - const { files, flattenedFiles, expandedDirectories, handleDirectoryToggle } = useFileInfoTree(filePath) - - const toggleSimilarFiles = () => { - setShowSimilarFiles(!showSimilarFiles) - } - - const getChatIdFromPath = (path: string) => { - if (chatHistoriesMetadata.length === 0) return UNINITIALIZED_STATE - const metadata = chatHistoriesMetadata.find((chat) => chat.displayName === path) - if (metadata) return metadata.id - return '' - } - - const openFileAndOpenEditor = async (path: string, optionalContentToWriteOnCreate?: string) => { - setShowChatbot(false) - setSidebarShowing('files') - openOrCreateFile(path, optionalContentToWriteOnCreate) - } - - const openTabContent = async (path: string) => { - if (!path) return - const chatID = getChatIdFromPath(path) - if (chatID) { - if (chatID === UNINITIALIZED_STATE) return - const chat = await window.electronStore.getChatHistory(chatID) - openChatSidebarAndChat(chat) - } else { - openFileAndOpenEditor(path) - } - setCurrentTab(path) - } - - useEffect(() => { - const setFileDirectory = async () => { - const windowDirectory = await window.electronStore.getVaultDirectoryForWindow() - setVaultDirectory(windowDirectory) - } - setFileDirectory() - }, []) - useEffect(() => { - const handleAddFileToChatFilters = (file: string) => { - setSidebarShowing('chats') - setShowChatbot(true) - setCurrentChatHistory(undefined) - setChatFilters((prevChatFilters) => ({ - ...prevChatFilters, - files: [...prevChatFilters.files, file], - })) - } - const removeAddChatToFileListener = window.ipcRenderer.receive('add-file-to-chat-listener', (noteName: string) => { - handleAddFileToChatFilters(noteName) - }) + const { currentlyOpenFilePath } = useFileContext() - return () => { - removeAddChatToFileListener() - } - }, [setCurrentChatHistory, setChatFilters, setShowChatbot]) + const { showChatbot } = useChatContext() return (
- - - + { + setShowSimilarFiles(!showSimilarFiles) + }} + />
- +
- +
- {!showChatbot && filePath ? ( + {!showChatbot && currentlyOpenFilePath ? (
- +
- + {showSimilarFiles && (
- +
)}
) : ( !showChatbot && (
- +
) )} {showChatbot && (
- +
)}
@@ -223,9 +75,15 @@ const MainPageContent: React.FC = () => { const MainPageComponent: React.FC = () => { return ( - - - + + + + + + + + + ) } diff --git a/src/components/Sidebars/FileSideBar/FileExplorer.tsx b/src/components/Sidebars/FileSideBar/FileExplorer.tsx new file mode 100644 index 00000000..c0a26398 --- /dev/null +++ b/src/components/Sidebars/FileSideBar/FileExplorer.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useState } from 'react' + +import { FileInfoNode, FileInfoTree } from 'electron/main/filesystem/types' +import { FixedSizeList as List } from 'react-window' + +import { isFileNodeDirectory } from './utils' +import { useFileContext } from '@/contexts/FileContext' +import FileItemRows from './FileItemRows' + +interface FileExplorerProps { + lheight?: number +} + +const FileExplorer: React.FC = ({ lheight }) => { + const [listHeight, setListHeight] = useState(lheight ?? window.innerHeight - 50) + const { files, expandedDirectories } = useFileContext() + + useEffect(() => { + const updateHeight = () => { + setListHeight(lheight ?? window.innerHeight - 50) + } + window.addEventListener('resize', updateHeight) + return () => { + window.removeEventListener('resize', updateHeight) + } + }, [lheight]) + + const getVisibleFilesAndFlatten = ( + _files: FileInfoTree, + _expandedDirectories: Map, + indentMultiplyer = 0, + ): { file: FileInfoNode; indentMultiplyer: number }[] => { + let visibleItems: { file: FileInfoNode; indentMultiplyer: number }[] = [] + _files.forEach((file) => { + const a = { file, indentMultiplyer } + visibleItems.push(a) + if (isFileNodeDirectory(file) && _expandedDirectories.has(file.path) && _expandedDirectories.get(file.path)) { + if (file.children) { + visibleItems = [ + ...visibleItems, + ...getVisibleFilesAndFlatten(file.children, _expandedDirectories, indentMultiplyer + 1), + ] + } + } + }) + return visibleItems + } + + const visibleItems = getVisibleFilesAndFlatten(files, expandedDirectories) + const itemCount = visibleItems.length + + return ( +
+ + {FileItemRows} + +
+ ) +} + +export default FileExplorer diff --git a/src/components/Sidebars/FileSideBar/FileItem.tsx b/src/components/Sidebars/FileSideBar/FileItem.tsx deleted file mode 100644 index bc7575e2..00000000 --- a/src/components/Sidebars/FileSideBar/FileItem.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState } from 'react' - -import { FileInfoNode } from 'electron/main/filesystem/types' -import posthog from 'posthog-js' -import { FaChevronDown, FaChevronRight } from 'react-icons/fa' - -import { isFileNodeDirectory, moveFile } from './utils' - -import { removeFileExtension } from '@/utils/strings' - -interface FileInfoProps { - file: FileInfoNode - selectedFilePath: string | null - onFileSelect: (path: string) => void - handleDragStart: (e: React.DragEvent, file: FileInfoNode) => void - onDirectoryToggle: (path: string) => void - isExpanded?: boolean - indentMultiplyer?: number -} - -const FileItem: React.FC = ({ - file, - selectedFilePath, - onFileSelect, - handleDragStart, - onDirectoryToggle, - isExpanded, - indentMultiplyer, -}) => { - const isDirectory = isFileNodeDirectory(file) - const isSelected = file.path === selectedFilePath - const indentation = indentMultiplyer ? 10 * indentMultiplyer : 0 - const [isDragOver, setIsDragOver] = useState(false) - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - setIsDragOver(true) - } - - const handleDragLeave = () => { - setIsDragOver(false) - } - - const handleDrop = async (e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragOver(false) // Reset drag over state - const sourcePath = e.dataTransfer.getData('text/plain') - let destinationPath = file.path // Default destination path is the path of the file item itself - - if (!isFileNodeDirectory(file)) { - const pathSegments = file.path.split('/') - pathSegments.pop() // Remove the file name from the path - destinationPath = pathSegments.join('/') - } - - moveFile(sourcePath, destinationPath) - // Refresh file list here or in moveFile function - } - - const toggle = () => { - if (isFileNodeDirectory(file)) { - onDirectoryToggle(file.path) - } else { - onFileSelect(file.path) - posthog.capture('open_file_from_sidebar') - } - } - - const localHandleDragStart = (e: React.DragEvent) => { - e.stopPropagation() - handleDragStart(e, file) - } - - const handleContextMenu = (e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - window.electronUtils.showFileItemContextMenu(file) - } - - const itemClasses = `flex items-center cursor-pointer px-2 py-1 border-b border-gray-200 hover:bg-neutral-700 h-full mt-0 mb-0 text-cyan-100 font-sans text-xs leading-relaxed rounded-md ${ - isSelected ? 'bg-neutral-700 text-white font-semibold' : 'text-gray-200' - } ${isDragOver ? 'bg-neutral-500' : ''}` - - return ( -
-
- {isDirectory && ( - - {isExpanded ? : } - - )} - {isDirectory ? file.name : removeFileExtension(file.name)} -
-
- ) -} - -export default FileItem diff --git a/src/components/Sidebars/FileSideBar/FileItemRows.tsx b/src/components/Sidebars/FileSideBar/FileItemRows.tsx new file mode 100644 index 00000000..f9037895 --- /dev/null +++ b/src/components/Sidebars/FileSideBar/FileItemRows.tsx @@ -0,0 +1,103 @@ +import React, { useState } from 'react' +import { ListChildComponentProps } from 'react-window' +import { FileInfoNode } from 'electron/main/filesystem/types' +import { FaChevronDown, FaChevronRight } from 'react-icons/fa' +import posthog from 'posthog-js' +import { useFileContext } from '@/contexts/FileContext' +import { isFileNodeDirectory, moveFile } from './utils' +import { removeFileExtension } from '@/utils/strings' +import { useTabsContext } from '@/contexts/TabContext' + +const FileItemRows: React.FC = ({ index, style, data }) => { + const { visibleItems } = data + const fileObject = visibleItems[index] + + const { handleDirectoryToggle, expandedDirectories, currentlyOpenFilePath } = useFileContext() + const { openTabContent } = useTabsContext() + + const [isDragOver, setIsDragOver] = useState(false) + + const isDirectory = isFileNodeDirectory(fileObject.file) + const isSelected = fileObject.file.path === currentlyOpenFilePath + const indentation = fileObject.indentMultiplyer ? 10 * fileObject.indentMultiplyer : 0 + const isExpanded = expandedDirectories.has(fileObject.file.path) && expandedDirectories.get(fileObject.file.path) + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + setIsDragOver(true) + } + + const handleDragLeave = () => { + setIsDragOver(false) + } + + const handleDragFile = (e: React.DragEvent, file: FileInfoNode) => { + e.dataTransfer.setData('text/plain', file.path) + e.dataTransfer.effectAllowed = 'move' + } + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragOver(false) + const sourcePath = e.dataTransfer.getData('text/plain') + let destinationPath = fileObject.file.path + if (!isFileNodeDirectory(fileObject.file)) { + const pathSegments = fileObject.file.path.split('/') + pathSegments.pop() + destinationPath = pathSegments.join('/') + } + moveFile(sourcePath, destinationPath) + // Refresh file list here or in moveFile function + } + + const toggle = () => { + if (isDirectory) { + handleDirectoryToggle(fileObject.file.path) + } else { + openTabContent(fileObject.file.path) + posthog.capture('open_file_from_sidebar') + } + } + + const localHandleDragStart = (e: React.DragEvent) => { + e.stopPropagation() + handleDragFile(e, fileObject.file) + } + + const handleContextMenu = (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + window.electronUtils.showFileItemContextMenu(fileObject.file) + } + + const itemClasses = `flex items-center cursor-pointer px-2 py-1 border-b border-gray-200 hover:bg-neutral-700 h-full mt-0 mb-0 text-cyan-100 font-sans text-xs leading-relaxed rounded-md ${ + isSelected ? 'bg-neutral-700 text-white font-semibold' : 'text-gray-200' + } ${isDragOver ? 'bg-neutral-500' : ''}` + + return ( +
+
+
+ {isDirectory && ( + + {isExpanded ? : } + + )} + + {isDirectory ? fileObject.file.name : removeFileExtension(fileObject.file.name)} + +
+
+
+ ) +} + +export default FileItemRows diff --git a/src/components/Sidebars/FileSideBar/hooks/use-file-info-tree.tsx b/src/components/Sidebars/FileSideBar/hooks/use-file-info-tree.tsx index fa094672..0d622ab0 100644 --- a/src/components/Sidebars/FileSideBar/hooks/use-file-info-tree.tsx +++ b/src/components/Sidebars/FileSideBar/hooks/use-file-info-tree.tsx @@ -4,7 +4,7 @@ import { FileInfo, FileInfoTree } from 'electron/main/filesystem/types' import flattenFileInfoTree, { sortFilesAndDirectories } from '../utils' -const useFileInfoTree = (currentFilePath: string | null) => { +const useFileInfoTreeHook = (filePath: string | null) => { const [fileInfoTree, setFileInfoTree] = useState([]) const [flattenedFiles, setFlattenedFiles] = useState([]) const [expandedDirectories, setExpandedDirectories] = useState>(new Map()) @@ -20,29 +20,39 @@ const useFileInfoTree = (currentFilePath: string | null) => { // upon indexing, update the file info tree and expand relevant directories useEffect(() => { - const findRelevantDirectoriesToBeOpened = () => { - if (currentFilePath === null) { + const findRelevantDirectoriesToBeOpened = async () => { + if (filePath === null) { return expandedDirectories } - const pathSegments = currentFilePath.split('/').filter((segment) => segment !== '') - pathSegments.pop() // Remove the file name from the path - let currentPath = '' + const pathSep = await window.path.pathSep() + const isAbsolute = await window.path.isAbsolute(filePath) + + const currentPath = isAbsolute ? '' : '.' const newExpandedDirectories = new Map(expandedDirectories) - pathSegments.forEach((segment) => { - currentPath += `/${segment}` - newExpandedDirectories.set(currentPath, true) - }) + + const pathSegments = filePath.split(pathSep).filter((segment) => segment !== '') + + pathSegments.pop() + + const updatedPath = pathSegments.reduce(async (pathPromise, segment) => { + const path = await pathPromise + const newPath = await window.path.join(path, segment) + newExpandedDirectories.set(newPath, true) + return newPath + }, Promise.resolve(currentPath)) + + await updatedPath return newExpandedDirectories } - const handleFileUpdate = (updatedFiles: FileInfoTree) => { + const handleFileUpdate = async (updatedFiles: FileInfoTree) => { const sortedFiles = sortFilesAndDirectories(updatedFiles, null) setFileInfoTree(sortedFiles) const updatedFlattenedFiles = flattenFileInfoTree(sortedFiles) setFlattenedFiles(updatedFlattenedFiles) - const directoriesToBeExpanded = findRelevantDirectoriesToBeOpened() + const directoriesToBeExpanded = await findRelevantDirectoriesToBeOpened() setExpandedDirectories(directoriesToBeExpanded) } @@ -51,7 +61,7 @@ const useFileInfoTree = (currentFilePath: string | null) => { return () => { removeFilesListListener() } - }, [currentFilePath, expandedDirectories]) + }, [filePath, expandedDirectories]) // initial load of files useEffect(() => { @@ -74,4 +84,4 @@ const useFileInfoTree = (currentFilePath: string | null) => { } } -export default useFileInfoTree +export default useFileInfoTreeHook diff --git a/src/components/Sidebars/FileSideBar/index.tsx b/src/components/Sidebars/FileSideBar/index.tsx index 9e79b20d..70e14ec4 100644 --- a/src/components/Sidebars/FileSideBar/index.tsx +++ b/src/components/Sidebars/FileSideBar/index.tsx @@ -1,173 +1,23 @@ -import React, { useEffect, useState } from 'react' +import React from 'react' -import { FileInfoNode, FileInfoTree } from 'electron/main/filesystem/types' -import { FixedSizeList as List, ListChildComponentProps } from 'react-window' - -import FileItem from './FileItem' -import { isFileNodeDirectory } from './utils' import RenameNoteModal from '@/components/File/RenameNote' import RenameDirModal from '@/components/File/RenameDirectory' +import { useFileContext } from '@/contexts/FileContext' +import FileExplorer from './FileExplorer' -const handleDragStartImpl = (e: React.DragEvent, file: FileInfoNode) => { - e.dataTransfer.setData('text/plain', file.path) - e.dataTransfer.effectAllowed = 'move' -} // Assuming FileItem is in a separate file - -const Rows: React.FC = ({ index, style, data }) => { - const { visibleItems, selectedFilePath, onFileSelect, handleDragStart, handleDirectoryToggle, expandedDirectories } = - data - const fileObject = visibleItems[index] - return ( -
- -
- ) -} - -interface FileExplorerProps { - files: FileInfoTree - selectedFilePath: string | null - onFileSelect: (path: string) => void - handleDragStart: (event: React.DragEvent, file: FileInfoNode) => void - expandedDirectories: Map - handleDirectoryToggle: (path: string) => void - lheight?: number +interface FileSidebarProps { + listHeight?: number } -const FileExplorer: React.FC = ({ - files, - selectedFilePath, - onFileSelect, - handleDragStart, - expandedDirectories, - handleDirectoryToggle, - lheight, -}) => { - const [listHeight, setListHeight] = useState(lheight ?? window.innerHeight - 50) - - useEffect(() => { - const updateHeight = () => { - setListHeight(lheight ?? window.innerHeight - 50) - } - window.addEventListener('resize', updateHeight) - return () => { - window.removeEventListener('resize', updateHeight) - } - }, [lheight]) - - const getVisibleFilesAndFlatten = ( - _files: FileInfoTree, - _expandedDirectories: Map, - indentMultiplyer = 0, - ): { file: FileInfoNode; indentMultiplyer: number }[] => { - let visibleItems: { file: FileInfoNode; indentMultiplyer: number }[] = [] - _files.forEach((file) => { - const a = { file, indentMultiplyer } - visibleItems.push(a) - if (isFileNodeDirectory(file) && _expandedDirectories.has(file.path) && _expandedDirectories.get(file.path)) { - if (file.children) { - visibleItems = [ - ...visibleItems, - ...getVisibleFilesAndFlatten(file.children, _expandedDirectories, indentMultiplyer + 1), - ] - } - } - }) - return visibleItems - } - - // Calculate visible items and item count - const visibleItems = getVisibleFilesAndFlatten(files, expandedDirectories) - const itemCount = visibleItems.length - +const FileSidebar: React.FC = ({ listHeight }) => { + const { noteToBeRenamed, fileDirToBeRenamed } = useFileContext() return ( -
- - {Rows} - +
+ {noteToBeRenamed && } + {fileDirToBeRenamed && } +
) } -interface FileListProps { - files: FileInfoTree - expandedDirectories: Map - handleDirectoryToggle: (path: string) => void - selectedFilePath: string | null - onFileSelect: (path: string) => void - renameFile: (oldFilePath: string, newFilePath: string) => Promise - noteToBeRenamed: string - setNoteToBeRenamed: (note: string) => void - fileDirToBeRenamed: string - setFileDirToBeRenamed: (dir: string) => void - listHeight?: number -} - -export const FileSidebar: React.FC = ({ - files, - expandedDirectories, - handleDirectoryToggle, - selectedFilePath, - onFileSelect, - renameFile, - noteToBeRenamed, - setNoteToBeRenamed, - fileDirToBeRenamed, - setFileDirToBeRenamed, - listHeight, -}) => ( -
- {noteToBeRenamed && ( - setNoteToBeRenamed('')} - fullNoteName={noteToBeRenamed} - renameNote={async ({ path, newNoteName }) => { - await renameFile(path, newNoteName) - }} - /> - )} - {fileDirToBeRenamed && ( - setFileDirToBeRenamed('')} - fullDirName={fileDirToBeRenamed} - renameDir={async ({ path, newDirName: newNoteName }) => { - await renameFile(path, newNoteName) - }} - /> - )} - -
-) - -export default FileExplorer +export default FileSidebar diff --git a/src/components/Sidebars/FileSideBar/utils.ts b/src/components/Sidebars/FileSideBar/utils.ts index f756ef0f..e500a3d7 100644 --- a/src/components/Sidebars/FileSideBar/utils.ts +++ b/src/components/Sidebars/FileSideBar/utils.ts @@ -26,12 +26,10 @@ export const sortFilesAndDirectories = (fileList: FileInfoTree, currentFilePath: const aIsDirectory = isFileNodeDirectory(a) const bIsDirectory = isFileNodeDirectory(b) - // Both are directories: sort alphabetically if (aIsDirectory && bIsDirectory) { return a.name.localeCompare(b.name) } - // One is a directory and the other is a file if (aIsDirectory && !bIsDirectory) { return -1 } @@ -39,7 +37,6 @@ export const sortFilesAndDirectories = (fileList: FileInfoTree, currentFilePath: return 1 } - // if current file path is not null and one of the files is the current file path, then it should be sorted as the first file after all directories if (currentFilePath !== null) { if (a.path === currentFilePath) { return -1 @@ -49,12 +46,10 @@ export const sortFilesAndDirectories = (fileList: FileInfoTree, currentFilePath: } } - // Both are files: sort by dateModified return b.dateModified.getTime() - a.dateModified.getTime() }) fileList.forEach((fileInfoNode) => { - // If a node has children, sort them recursively if (fileInfoNode.children && fileInfoNode.children.length > 0) { sortFilesAndDirectories(fileInfoNode.children, currentFilePath) } diff --git a/src/components/Sidebars/FileSidebarSearch.tsx b/src/components/Sidebars/FileSidebarSearch.tsx index 01512bc4..fb70a726 100644 --- a/src/components/Sidebars/FileSidebarSearch.tsx +++ b/src/components/Sidebars/FileSidebarSearch.tsx @@ -4,9 +4,9 @@ import posthog from 'posthog-js' import { FaSearch } from 'react-icons/fa' import { DBSearchPreview } from '../File/DBResultPreview' import debounce from './utils' +import { useTabsContext } from '@/contexts/TabContext' interface SearchComponentProps { - onFileSelect: (path: string) => void searchQuery: string setSearchQuery: (query: string) => void searchResults: DBQueryResult[] @@ -14,12 +14,12 @@ interface SearchComponentProps { } const SearchComponent: React.FC = ({ - onFileSelect, searchQuery, setSearchQuery, searchResults, setSearchResults, }) => { + const { openTabContent } = useTabsContext() const searchInputRef = useRef(null) const handleSearch = useCallback( @@ -50,10 +50,10 @@ const SearchComponent: React.FC = ({ const openFileSelectSearch = useCallback( (path: string) => { - onFileSelect(path) + openTabContent(path) posthog.capture('open_file_from_search') }, - [onFileSelect], + [openTabContent], ) return ( diff --git a/src/components/Sidebars/IconsSidebar.tsx b/src/components/Sidebars/IconsSidebar.tsx index 6fdaf82b..d075668b 100644 --- a/src/components/Sidebars/IconsSidebar.tsx +++ b/src/components/Sidebars/IconsSidebar.tsx @@ -12,22 +12,11 @@ import NewDirectoryComponent from '../File/NewDirectory' import NewNoteComponent from '../File/NewNote' import FlashcardMenuModal from '../Flashcard/FlashcardMenuModal' import SettingsModal from '../Settings/Settings' -import { SidebarAbleToShow } from './MainSidebar' -import { useModalOpeners } from '../../providers/ModalProvider' +import { useModalOpeners } from '../../contexts/ModalContext' +import { useChatContext } from '@/contexts/ChatContext' -interface IconsSidebarProps { - openFileAndOpenEditor: (path: string, optionalContentToWriteOnCreate?: string) => void - sidebarShowing: SidebarAbleToShow - makeSidebarShow: (show: SidebarAbleToShow) => void - currentFilePath: string | null -} - -const IconsSidebar: React.FC = ({ - openFileAndOpenEditor, - sidebarShowing, - makeSidebarShow, - currentFilePath, -}) => { +const IconsSidebar: React.FC = () => { + const { sidebarShowing, setSidebarShowing } = useChatContext() const [initialFileToCreateFlashcard, setInitialFileToCreateFlashcard] = useState('') const [initialFileToReviewFlashcard, setInitialFileToReviewFlashcard] = useState('') const [sidebarWidth, setSidebarWidth] = useState(40) @@ -81,7 +70,7 @@ const IconsSidebar: React.FC = ({ >
makeSidebarShow('files')} + onClick={() => setSidebarShowing('files')} >
= ({
makeSidebarShow('chats')} + onClick={() => setSidebarShowing('chats')} >
= ({
makeSidebarShow('search')} + onClick={() => setSidebarShowing('search')} >
= ({
- setIsNewNoteModalOpen(false)} - openFileAndOpenEditor={openFileAndOpenEditor} - currentOpenFilePath={currentFilePath} - /> - setIsNewDirectoryModalOpen(false)} - currentOpenFilePath={currentFilePath} - /> + setIsNewNoteModalOpen(false)} /> + setIsNewDirectoryModalOpen(false)} /> {isFlashcardModeOpen && ( - handleDirectoryToggle: (path: string) => void - selectedFilePath: string | null - onFileSelect: (path: string) => void - sidebarShowing: SidebarAbleToShow - renameFile: (oldFilePath: string, newFilePath: string) => Promise - noteToBeRenamed: string - setNoteToBeRenamed: (note: string) => void - fileDirToBeRenamed: string - setFileDirToBeRenamed: (dir: string) => void -} -const SidebarManager: React.FC = ({ - files, - expandedDirectories, - handleDirectoryToggle, - selectedFilePath, - onFileSelect, - sidebarShowing, - renameFile, - noteToBeRenamed, - setNoteToBeRenamed, - fileDirToBeRenamed, - setFileDirToBeRenamed, -}) => { +const SidebarManager: React.FC = () => { + const { sidebarShowing } = useChatContext() const [searchQuery, setSearchQuery] = useState('') const [searchResults, setSearchResults] = useState([]) return (
- {sidebarShowing === 'files' && ( - - )} + {sidebarShowing === 'files' && } {sidebarShowing === 'search' && ( void - onFileSelect: (path: string) => void - saveCurrentFile: () => Promise + onSelect: (path: string) => void updateSimilarEntries?: (isRefined?: boolean) => Promise titleText: string isLoadingSimilarEntries: boolean @@ -21,14 +21,14 @@ interface SimilarEntriesComponentProps { const SimilarEntriesComponent: React.FC = ({ similarEntries, - setSimilarEntries, // Default implementation - onFileSelect, - saveCurrentFile, - updateSimilarEntries, // Default implementation + setSimilarEntries, + onSelect, + updateSimilarEntries, titleText, isLoadingSimilarEntries, }) => { let content + const { saveCurrentlyOpenedFile } = useFileContext() if (similarEntries.length > 0) { content = ( @@ -37,12 +37,7 @@ const SimilarEntriesComponent: React.FC = ({ .filter((dbResult) => dbResult) .map((dbResult) => (
- { - onFileSelect(path) - }} - /> +
))}
@@ -70,7 +65,7 @@ const SimilarEntriesComponent: React.FC = ({
)} - setIsNewNoteModalOpen(false)} - openFileAndOpenEditor={openFileAndOpenEditor} - currentOpenFilePath={currentTab} - /> + setIsNewNoteModalOpen(false)} />
) } diff --git a/src/components/TitleBar/NavigationButtons.tsx b/src/components/TitleBar/NavigationButtons.tsx index d05c5077..fabd7469 100644 --- a/src/components/TitleBar/NavigationButtons.tsx +++ b/src/components/TitleBar/NavigationButtons.tsx @@ -5,45 +5,38 @@ import { IoMdArrowRoundBack, IoMdArrowRoundForward } from 'react-icons/io' import { removeFileExtension } from '@/utils/strings' import '../../styles/history.scss' +import { useFileContext } from '@/contexts/FileContext' +import { useTabsContext } from '@/contexts/TabContext' -interface FileHistoryNavigatorProps { - history: string[] - setHistory: (string: string[]) => void - onFileSelect: (path: string) => void - currentPath: string -} - -const FileHistoryNavigator: React.FC = ({ - history, - setHistory, - onFileSelect, - currentPath, -}) => { +const FileHistoryNavigator: React.FC = () => { const [showMenu, setShowMenu] = useState('') const [currentIndex, setCurrentIndex] = useState(-1) const longPressTimer = useRef(null) const buttonRefBack = useRef(null) const buttonRefForward = useRef(null) + const { currentTab, openTabContent } = useTabsContext() + const { navigationHistory, setNavigationHistory } = useFileContext() + useEffect(() => { const handleFileSelect = (path: string) => { - const updatedHistory = [...history.filter((val) => val !== path).slice(0, currentIndex + 1), path] - setHistory(updatedHistory) + const updatedHistory = [...navigationHistory.filter((val) => val !== path).slice(0, currentIndex + 1), path] + setNavigationHistory(updatedHistory) setCurrentIndex(updatedHistory.length - 1) } - if (currentPath && currentPath !== history[currentIndex]) { - handleFileSelect(currentPath) + if (currentTab && currentTab !== navigationHistory[currentIndex]) { + handleFileSelect(currentTab) } - }, [currentPath, history, currentIndex, setHistory]) + }, [currentTab, navigationHistory, currentIndex, setNavigationHistory]) const canGoBack = currentIndex > 0 - const canGoForward = currentIndex < history.length - 1 + const canGoForward = currentIndex < navigationHistory.length - 1 const goBack = () => { if (canGoBack && showMenu === '') { const newIndex = currentIndex - 1 setCurrentIndex(newIndex) - onFileSelect(history[newIndex]) + openTabContent(navigationHistory[newIndex]) posthog.capture('file_history_navigator_back') } } @@ -52,16 +45,16 @@ const FileHistoryNavigator: React.FC = ({ if (canGoForward && showMenu === '') { const newIndex = currentIndex + 1 setCurrentIndex(newIndex) - onFileSelect(history[newIndex]) + openTabContent(navigationHistory[newIndex]) posthog.capture('file_history_navigator_forward') } } const goSelected = (path: string): void => { if (path) { - const newIndex = history.indexOf(path) + const newIndex = navigationHistory.indexOf(path) setCurrentIndex(newIndex) - onFileSelect(path) + openTabContent(path) posthog.capture('file_history_navigator_go_to_selected_file') } setShowMenu('') @@ -102,7 +95,9 @@ const FileHistoryNavigator: React.FC = ({ const offsetHeight = currentRef.current?.offsetHeight || 0 const menuChild = - currentRef.current?.id === 'back' ? history.slice(0, currentIndex) : history.slice(currentIndex + 1) + currentRef.current?.id === 'back' + ? navigationHistory.slice(0, currentIndex) + : navigationHistory.slice(currentIndex + 1) return ( showMenu !== '' && diff --git a/src/components/TitleBar/TitleBar.tsx b/src/components/TitleBar/TitleBar.tsx index 81dce184..9c84f7f5 100644 --- a/src/components/TitleBar/TitleBar.tsx +++ b/src/components/TitleBar/TitleBar.tsx @@ -1,30 +1,17 @@ import React, { useEffect, useState } from 'react' import { PiSidebar, PiSidebarFill } from 'react-icons/pi' import DraggableTabs from '../Tabs/TabBar' -import { ModalProvider } from '../../providers/ModalProvider' +import { ModalProvider } from '../../contexts/ModalContext' import FileHistoryNavigator from './NavigationButtons' export const titleBarHeight = '30px' interface TitleBarProps { - openTabContent: (path: string) => void - currentTab: string | null // Used to create new open tabs when user clicks on new file to open similarFilesOpen: boolean toggleSimilarFiles: () => void - history: string[] - setHistory: (string: string[]) => void - openFileAndOpenEditor: (path: string, optionalContentToWriteOnCreate?: string) => void } -const TitleBar: React.FC = ({ - openTabContent, - currentTab, - similarFilesOpen, - toggleSimilarFiles, - history, - setHistory, - openFileAndOpenEditor, -}) => { +const TitleBar: React.FC = ({ similarFilesOpen, toggleSimilarFiles }) => { const [platform, setPlatform] = useState('') useEffect(() => { @@ -39,23 +26,14 @@ const TitleBar: React.FC = ({ return (
- +
- +
diff --git a/src/components/WritingAssistant/WritingAssistant.tsx b/src/components/WritingAssistant/WritingAssistant.tsx index 77af6fbf..bdc0ac9e 100644 --- a/src/components/WritingAssistant/WritingAssistant.tsx +++ b/src/components/WritingAssistant/WritingAssistant.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useLayoutEffect, useRef } from 'react' -import { Editor } from '@tiptap/react' import { FaMagic } from 'react-icons/fa' import ReactMarkdown from 'react-markdown' import rehypeRaw from 'rehype-raw' @@ -9,16 +8,11 @@ import posthog from 'posthog-js' import { streamText } from 'ai' import { appendTextContentToMessages, convertMessageToString, resolveLLMClient } from '../Chat/utils' import useOutsideClick from '../Chat/hooks/use-outside-click' -import { HighlightData } from '../Editor/HighlightExtension' import getClassNames, { generatePromptString, getLastMessage } from './utils' import { ReorChatMessage } from '../Chat/types' +import { useFileContext } from '@/contexts/FileContext' -interface WritingAssistantProps { - editor: Editor | null - highlightData: HighlightData -} - -const WritingAssistant: React.FC = ({ editor, highlightData }) => { +const WritingAssistant: React.FC = () => { const [messages, setMessages] = useState([]) const [loadingResponse, setLoadingResponse] = useState(false) const [customPrompt, setCustomPrompt] = useState('') @@ -35,6 +29,8 @@ const WritingAssistant: React.FC = ({ editor, highlightDa const lastAssistantMessage = getLastMessage(messages, 'assistant') const hasValidMessages = !!lastAssistantMessage + const { editor, highlightData } = useFileContext() + useOutsideClick(markdownContainerRef, () => { setMessages([]) setIsSpaceTrigger(false) diff --git a/src/contexts/ChatContext.tsx b/src/contexts/ChatContext.tsx new file mode 100644 index 00000000..2789c845 --- /dev/null +++ b/src/contexts/ChatContext.tsx @@ -0,0 +1,137 @@ +import React, { createContext, useContext, useState, useCallback, useEffect } from 'react' +import { ChatHistoryMetadata, useChatHistory } from '@/components/Chat/hooks/use-chat-history' +import { Chat, ChatFilters } from '@/components/Chat/types' +import { SidebarAbleToShow } from '@/components/Sidebars/MainSidebar' + +export const UNINITIALIZED_STATE = 'UNINITIALIZED_STATE' + +interface ChatContextType { + // openTabContent: (path: string, optionalContentToWriteOnCreate?: string) => void + // currentTab: string + sidebarShowing: SidebarAbleToShow + setSidebarShowing: (option: SidebarAbleToShow) => void + getChatIdFromPath: (path: string) => string + showChatbot: boolean + setShowChatbot: (show: boolean) => void + currentChatHistory: Chat | undefined + setCurrentChatHistory: React.Dispatch> + chatHistoriesMetadata: ChatHistoryMetadata[] + chatFilters: ChatFilters + setChatFilters: React.Dispatch> + openChatSidebarAndChat: (chatHistory: Chat | undefined) => void +} + +const ChatContext = createContext(undefined) + +export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [showChatbot, setShowChatbot] = useState(false) + // const [currentTab, setCurrentTab] = useState('') + const [chatFilters, setChatFilters] = useState({ + files: [], + numberOfChunksToFetch: 15, + minDate: new Date(0), + maxDate: new Date(), + }) + const [sidebarShowing, setSidebarShowing] = useState('files') + + const { currentChatHistory, setCurrentChatHistory, chatHistoriesMetadata } = useChatHistory() + + // const filePathRef = React.useRef('') + // const chatIDRef = React.useRef('') + + const openChatSidebarAndChat = useCallback( + (chatHistory: Chat | undefined) => { + setShowChatbot(true) + setCurrentChatHistory(chatHistory) + }, + [setCurrentChatHistory], + ) + + const getChatIdFromPath = useCallback( + (path: string) => { + if (chatHistoriesMetadata.length === 0) return UNINITIALIZED_STATE + const metadata = chatHistoriesMetadata.find((chat) => chat.displayName === path) + if (metadata) return metadata.id + return '' + }, + [chatHistoriesMetadata], + ) + + // useEffect(() => { + // if (currentlyOpenFilePath != null && filePathRef.current !== currentlyOpenFilePath) { + // filePathRef.current = currentlyOpenFilePath + // setCurrentTab(currentlyOpenFilePath) + // } + + // const currentChatHistoryId = currentChatHistory?.id ?? '' + // if (chatIDRef.current !== currentChatHistoryId) { + // chatIDRef.current = currentChatHistoryId + // const currentMetadata = chatHistoriesMetadata.find((chat) => chat.id === currentChatHistoryId) + // if (currentMetadata) { + // setCurrentTab(currentMetadata.displayName) + // } + // } + // }, [currentChatHistory, chatHistoriesMetadata, currentlyOpenFilePath]) + + useEffect(() => { + const handleAddFileToChatFilters = (file: string) => { + setSidebarShowing('chats') + setShowChatbot(true) + setCurrentChatHistory(undefined) + setChatFilters((prevChatFilters) => ({ + ...prevChatFilters, + files: [...prevChatFilters.files, file], + })) + } + const removeAddChatToFileListener = window.ipcRenderer.receive('add-file-to-chat-listener', (noteName: string) => { + handleAddFileToChatFilters(noteName) + }) + + return () => { + removeAddChatToFileListener() + } + }, [setCurrentChatHistory, setChatFilters, setShowChatbot]) + + const value = React.useMemo( + () => ({ + // openTabContent, + // currentTab, + sidebarShowing, + setSidebarShowing, + getChatIdFromPath, + showChatbot, + setShowChatbot, + currentChatHistory, + setCurrentChatHistory, + chatHistoriesMetadata, + chatFilters, + setChatFilters, + openChatSidebarAndChat, + }), + [ + // openTabContent, + // currentTab, + sidebarShowing, + setSidebarShowing, + getChatIdFromPath, + showChatbot, + setShowChatbot, + currentChatHistory, + setCurrentChatHistory, + chatHistoriesMetadata, + chatFilters, + setChatFilters, + openChatSidebarAndChat, + ], + ) + + return {children} +} + +export const useChatContext = () => { + const context = useContext(ChatContext) + if (context === undefined) { + throw new Error('useChatContext must be used within a ChatProvider') + } + return context +} diff --git a/src/contexts/FileContext.tsx b/src/contexts/FileContext.tsx new file mode 100644 index 00000000..1385300b --- /dev/null +++ b/src/contexts/FileContext.tsx @@ -0,0 +1,33 @@ +import React, { createContext, useContext, ReactNode } from 'react' +import useFileByFilepath from '@/components/File/hooks/use-file-by-filepath' +import useFileInfoTreeHook from '@/components/Sidebars/FileSideBar/hooks/use-file-info-tree' + +type FileByFilepathType = ReturnType +type FileInfoTreeType = ReturnType + +type FileContextType = FileByFilepathType & FileInfoTreeType + +export const FileContext = createContext(undefined) + +export const useFileContext = () => { + const context = useContext(FileContext) + if (context === undefined) { + throw new Error('useFileContext must be used within a FileProvider') + } + return context +} + +export const FileProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const fileByFilepathValue = useFileByFilepath() + const fileInfoTreeValue = useFileInfoTreeHook(fileByFilepathValue.currentlyOpenFilePath) + + const combinedContextValue: FileContextType = React.useMemo( + () => ({ + ...fileByFilepathValue, + ...fileInfoTreeValue, + }), + [fileByFilepathValue, fileInfoTreeValue], + ) + + return {children} +} diff --git a/src/providers/ModalProvider.tsx b/src/contexts/ModalContext.tsx similarity index 100% rename from src/providers/ModalProvider.tsx rename to src/contexts/ModalContext.tsx diff --git a/src/providers/TabProvider.tsx b/src/contexts/TabContext.tsx similarity index 60% rename from src/providers/TabProvider.tsx rename to src/contexts/TabContext.tsx index caf9a2fa..b04b888c 100644 --- a/src/providers/TabProvider.tsx +++ b/src/contexts/TabContext.tsx @@ -2,10 +2,12 @@ import React, { createContext, useContext, useState, useEffect, useCallback, use import { v4 as uuidv4 } from 'uuid' import { Tab } from 'electron/main/electron-store/storeConfig' -import { SidebarAbleToShow } from '../components/Sidebars/MainSidebar' -import { useChatContext } from './ChatContext' +import { UNINITIALIZED_STATE, useChatContext } from './ChatContext' +import { useFileContext } from './FileContext' interface TabContextType { + currentTab: string + openTabContent: (path: string, optionalContentToWriteOnCreate?: string) => void openTabs: Tab[] addTab: (path: string) => void selectTab: (tab: Tab) => void @@ -13,39 +15,37 @@ interface TabContextType { updateTabOrder: (draggedIdx: number, targetIdx: number) => void } -const defaultTypeContext: TabContextType = { - openTabs: [], - addTab: () => {}, - selectTab: () => {}, - removeTabByID: () => {}, - updateTabOrder: () => {}, -} - -const TabContext = createContext(defaultTypeContext) +const TabContext = createContext(undefined) -export const useTabs = (): TabContextType => useContext(TabContext) +export const useTabsContext = (): TabContextType => { + const context = useContext(TabContext) + if (context === undefined) { + throw new Error('useTabs must be used within a TabProvider') + } + return context +} interface TabProviderProps { children: ReactNode - openTabContent: (path: string) => void - setFilePath: (path: string) => void - currentTab: string | null - sidebarShowing: string | null - makeSidebarShow: (option: SidebarAbleToShow) => void - getChatIdFromPath: (path: string) => string } -export const TabProvider: React.FC = ({ - children, - openTabContent, - setFilePath, - currentTab, - sidebarShowing, - makeSidebarShow, - getChatIdFromPath, -}) => { +export const TabProvider: React.FC = ({ children }) => { + const [currentTab, setCurrentTab] = useState('') const [openTabs, setOpenTabs] = useState([]) - const { setCurrentChatHistory } = useChatContext() + const { + currentChatHistory, + setCurrentChatHistory, + chatHistoriesMetadata, + openChatSidebarAndChat, + setShowChatbot, + sidebarShowing, + setSidebarShowing, + getChatIdFromPath, + } = useChatContext() + const { currentlyOpenFilePath, setCurrentlyOpenFilePath, openOrCreateFile } = useFileContext() + + const filePathRef = React.useRef('') + const chatIDRef = React.useRef('') useEffect(() => { const fetchHistoryTabs = async () => { @@ -67,6 +67,22 @@ export const TabProvider: React.FC = ({ } }, []) + useEffect(() => { + if (currentlyOpenFilePath != null && filePathRef.current !== currentlyOpenFilePath) { + filePathRef.current = currentlyOpenFilePath + setCurrentTab(currentlyOpenFilePath) + } + + const currentChatHistoryId = currentChatHistory?.id ?? '' + if (chatIDRef.current !== currentChatHistoryId) { + chatIDRef.current = currentChatHistoryId + const currentMetadata = chatHistoriesMetadata.find((chat) => chat.id === currentChatHistoryId) + if (currentMetadata) { + setCurrentTab(currentMetadata.displayName) + } + } + }, [currentChatHistory, chatHistoriesMetadata, currentlyOpenFilePath]) + const extractFileName = (path: string) => { const parts = path.split(/[/\\]/) // Split on both forward slash and backslash return parts.pop() || '' // Returns the last element, which is the file name @@ -99,6 +115,24 @@ export const TabProvider: React.FC = ({ [openTabs], ) + const openTabContent = React.useCallback( + async (path: string, optionalContentToWriteOnCreate?: string) => { + if (!path) return + const chatID = getChatIdFromPath(path) + if (chatID) { + if (chatID === UNINITIALIZED_STATE) return + const chat = await window.electronStore.getChatHistory(chatID) + openChatSidebarAndChat(chat) + } else { + setShowChatbot(false) + setSidebarShowing('files') + openOrCreateFile(path, optionalContentToWriteOnCreate) + } + setCurrentTab(path) + }, + [getChatIdFromPath, openChatSidebarAndChat, setShowChatbot, setSidebarShowing, openOrCreateFile, setCurrentTab], + ) + /* Removes a tab and syncs it with the backend */ const removeTabByID = useCallback( (tabId: string) => { @@ -127,7 +161,7 @@ export const TabProvider: React.FC = ({ const hasChatTabs = nextTabs.some((tab) => getChatIdFromPath(tab.path)) if (!hasFileTabs) { - setFilePath('') + setCurrentlyOpenFilePath('') } if (!hasChatTabs) { @@ -141,7 +175,7 @@ export const TabProvider: React.FC = ({ window.electronStore.removeOpenTabs(tabId, findIdx, newIndex) } }, - [currentTab, openTabContent, openTabs, setFilePath, setCurrentChatHistory, getChatIdFromPath], + [openTabs, currentTab, openTabContent, getChatIdFromPath, setCurrentlyOpenFilePath, setCurrentChatHistory], ) /* Updates tab order (on drag) and syncs it with backend */ @@ -168,23 +202,25 @@ export const TabProvider: React.FC = ({ }) if (getChatIdFromPath(selectedTab.path)) { - if (sidebarShowing !== 'chats') makeSidebarShow('chats') - } else if (sidebarShowing !== 'files') makeSidebarShow('files') + if (sidebarShowing !== 'chats') setSidebarShowing('chats') + } else if (sidebarShowing !== 'files') setSidebarShowing('files') openTabContent(selectedTab.path) }, - [openTabContent, makeSidebarShow, sidebarShowing, getChatIdFromPath], + [openTabContent, setSidebarShowing, sidebarShowing, getChatIdFromPath], ) const TabContextMemo = useMemo( () => ({ + currentTab, + openTabContent, openTabs, addTab, removeTabByID, updateTabOrder, selectTab, }), - [openTabs, addTab, removeTabByID, updateTabOrder, selectTab], + [currentTab, openTabs, addTab, removeTabByID, updateTabOrder, selectTab, openTabContent], ) return {children} diff --git a/src/providers/ChatContext.tsx b/src/providers/ChatContext.tsx deleted file mode 100644 index 10fb18a3..00000000 --- a/src/providers/ChatContext.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { createContext, useContext, useState, useCallback } from 'react' -import { ChatHistoryMetadata, useChatHistory } from '@/components/Chat/hooks/use-chat-history' -import { Chat, ChatFilters } from '@/components/Chat/types' - -interface ChatContextType { - showChatbot: boolean - setShowChatbot: (show: boolean) => void - currentChatHistory: Chat | undefined - setCurrentChatHistory: React.Dispatch> - chatHistoriesMetadata: ChatHistoryMetadata[] - chatFilters: ChatFilters - setChatFilters: React.Dispatch> - openChatSidebarAndChat: (chatHistory: Chat | undefined) => void -} - -const ChatContext = createContext(undefined) - -export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [showChatbot, setShowChatbot] = useState(false) - const [chatFilters, setChatFilters] = useState({ - files: [], - numberOfChunksToFetch: 15, - minDate: new Date(0), - maxDate: new Date(), - }) - - const { currentChatHistory, setCurrentChatHistory, chatHistoriesMetadata } = useChatHistory() - - const openChatSidebarAndChat = useCallback( - (chatHistory: Chat | undefined) => { - setShowChatbot(true) - setCurrentChatHistory(chatHistory) - }, - [setCurrentChatHistory], - ) - - const value = React.useMemo( - () => ({ - showChatbot, - setShowChatbot, - currentChatHistory, - setCurrentChatHistory, - chatHistoriesMetadata, - chatFilters, - setChatFilters, - openChatSidebarAndChat, - }), - [ - showChatbot, - setShowChatbot, - currentChatHistory, - setCurrentChatHistory, - chatHistoriesMetadata, - chatFilters, - setChatFilters, - openChatSidebarAndChat, - ], - ) - - return {children} -} - -export const useChatContext = () => { - const context = useContext(ChatContext) - if (context === undefined) { - throw new Error('useChatContext must be used within a ChatProvider') - } - return context -}