diff --git a/electron/main/electron-store/ipcHandlers.ts b/electron/main/electron-store/ipcHandlers.ts index 05133287..dcb8c37f 100644 --- a/electron/main/electron-store/ipcHandlers.ts +++ b/electron/main/electron-store/ipcHandlers.ts @@ -2,16 +2,17 @@ import path from 'path' import { ipcMain } from 'electron' import Store from 'electron-store' - -import WindowsManager from '../common/windowManager' - import { + Tab, EmbeddingModelConfig, EmbeddingModelWithLocalPath, EmbeddingModelWithRepo, StoreKeys, StoreSchema, } from './storeConfig' + +import WindowsManager from '../common/windowManager' + import { initializeAndMaybeMigrateStore } from './storeSchemaMigrator' import { ChatHistory } from '@/components/Chat/chatUtils' @@ -114,6 +115,13 @@ export const registerStoreHandlers = (store: Store, windowsManager: ipcMain.handle('get-sb-compact', () => store.get(StoreKeys.IsSBCompact)) + ipcMain.handle('get-editor-flex-center', () => store.get(StoreKeys.EditorFlexCenter)) + + ipcMain.handle('set-editor-flex-center', (event, setEditorFlexCenter) => { + store.set(StoreKeys.EditorFlexCenter, setEditorFlexCenter) + event.sender.send('editor-flex-center-changed', setEditorFlexCenter) + }) + ipcMain.handle('set-analytics-mode', (event, isAnalytics) => { store.set(StoreKeys.Analytics, isAnalytics) }) @@ -191,9 +199,57 @@ export const registerStoreHandlers = (store: Store, windowsManager: const chatHistoriesMap = store.get(StoreKeys.ChatHistories) const allChatHistories = chatHistoriesMap[vaultDir] || [] const filteredChatHistories = allChatHistories.filter((item) => item.id !== chatID) + chatHistoriesMap[vaultDir] = filteredChatHistories.reverse() store.set(StoreKeys.ChatHistories, chatHistoriesMap) }) + + ipcMain.handle('get-current-open-files', () => store.get(StoreKeys.OpenTabs) || []) + + ipcMain.handle('add-current-open-files', (event, tab: Tab) => { + if (tab === null) return + const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || [] + const existingTab = openTabs.findIndex((item) => item.filePath === tab.filePath) + + /* If tab is already open, do not do anything */ + if (existingTab !== -1) return + openTabs.push(tab) + store.set(StoreKeys.OpenTabs, openTabs) + }) + + ipcMain.handle('remove-current-open-files', (event, tabId: string, idx: number, newIndex: number) => { + // Ensure indices are within range + const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || [] + if (idx < 0 || idx >= openTabs.length || newIndex < 0 || newIndex >= openTabs.length) return + openTabs[idx].lastAccessed = false + openTabs[newIndex].lastAccessed = true + const updatedTabs = openTabs.filter((tab) => tab.id !== tabId) + store.set(StoreKeys.OpenTabs, updatedTabs) + }) + + ipcMain.handle('clear-current-open-files', () => { + store.set(StoreKeys.OpenTabs, []) + }) + + ipcMain.handle('update-current-open-files', (event, draggedIndex: number, targetIndex: number) => { + const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || [] + if (draggedIndex < 0 || draggedIndex >= openTabs.length || targetIndex < 0 || targetIndex >= openTabs.length) return + ;[openTabs[draggedIndex], openTabs[targetIndex]] = [openTabs[targetIndex], openTabs[draggedIndex]] + store.set(StoreKeys.OpenTabs, openTabs) + }) + + ipcMain.handle('set-current-open-files', (event, tabs: Tab[]) => { + if (tabs) store.set(StoreKeys.OpenTabs, tabs) + }) + + ipcMain.handle('remove-current-open-files-by-path', (event, filePath: string) => { + if (!filePath) return + const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || [] + // Filter out selected tab + const updatedTabs = openTabs.filter((tab) => tab.filePath !== filePath) + store.set(StoreKeys.OpenTabs, updatedTabs) + event.sender.send('remove-tab-after-deletion', updatedTabs) + }) } export function getDefaultEmbeddingModelConfig(store: Store): EmbeddingModelConfig { diff --git a/electron/main/electron-store/storeConfig.ts b/electron/main/electron-store/storeConfig.ts index 9cb72aee..c95608c4 100644 --- a/electron/main/electron-store/storeConfig.ts +++ b/electron/main/electron-store/storeConfig.ts @@ -48,6 +48,15 @@ export type HardwareConfig = { useVulkan: boolean } +export type Tab = { + id: string // Unique ID for the tab, useful for operations + filePath: string // Path to the file open in the tab + title: string // Title of the tab + lastAccessed: boolean + // timeOpened: Date; // Timestamp to preserve order + // isDirty: boolean; // Flag to indicate unsaved changes +} + export interface StoreSchema { hasUserOpenedAppBefore: boolean schemaVersion: number @@ -72,6 +81,8 @@ export interface StoreSchema { isSBCompact: boolean DisplayMarkdown: boolean spellCheck: string + EditorFlexCenter: boolean + OpenTabs: Tab[] } export enum StoreKeys { @@ -91,4 +102,6 @@ export enum StoreKeys { IsSBCompact = 'isSBCompact', DisplayMarkdown = 'DisplayMarkdown', SpellCheck = 'spellCheck', + EditorFlexCenter = 'editorFlexCenter', + OpenTabs = 'OpenTabs', } diff --git a/electron/main/electron-utils/ipcHandlers.ts b/electron/main/electron-utils/ipcHandlers.ts index 036b89e0..fd0ecc7b 100644 --- a/electron/main/electron-utils/ipcHandlers.ts +++ b/electron/main/electron-utils/ipcHandlers.ts @@ -1,5 +1,3 @@ -import * as fs from 'fs/promises' - import { app, BrowserWindow, dialog, ipcMain, Menu, MenuItem, shell } from 'electron' import Store from 'electron-store' @@ -15,57 +13,9 @@ const electronUtilsHandlers = ( url: string | undefined, indexHtml: string, ) => { - ipcMain.handle('show-context-menu-item', (event) => { - const menu = new Menu() - - menu.append( - new MenuItem({ - label: 'New Note', - click: () => { - event.sender.send('add-new-note-listener') - }, - }), - ) - - menu.append( - new MenuItem({ - label: 'New Directory', - click: () => { - event.sender.send('add-new-directory-listener') - }, - }), - ) - - const browserWindow = BrowserWindow.fromWebContents(event.sender) - if (browserWindow) menu.popup({ window: browserWindow }) - }) - ipcMain.handle('show-context-menu-file-item', async (event, file: FileInfoNode) => { const menu = new Menu() - const stats = await fs.stat(file.path) - const isDirectory = stats.isDirectory() - - if (isDirectory) { - menu.append( - new MenuItem({ - label: 'New Note', - click: () => { - event.sender.send('add-new-note-listener', file.relativePath) - }, - }), - ) - - menu.append( - new MenuItem({ - label: 'New Directory', - click: () => { - event.sender.send('add-new-directory-listener', file.path) - }, - }), - ) - } - menu.append( new MenuItem({ label: 'Delete', diff --git a/electron/main/path/ipcHandlers.ts b/electron/main/path/ipcHandlers.ts index 9edad54a..5ebdbecc 100644 --- a/electron/main/path/ipcHandlers.ts +++ b/electron/main/path/ipcHandlers.ts @@ -17,6 +17,8 @@ const pathHandlers = () => { ipcMain.handle('path-relative', (event, from: string, to: string) => path.relative(from, to)) + ipcMain.handle('path-absolute', (event, filePath: string) => path.isAbsolute(filePath)) + ipcMain.handle('add-extension-if-no-extension-present', (event, pathString: string) => addExtensionToFilenameIfNoExtensionPresent(pathString, markdownExtensions, '.md'), ) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ed2df34b..07fb6a10 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -6,6 +6,7 @@ import { HardwareConfig, LLMConfig, LLMGenerationParameters, + Tab, } from 'electron/main/electron-store/storeConfig' import { AugmentPromptWithFileProps, @@ -54,7 +55,6 @@ const electronUtils = { openNewWindow: createIPCHandler<() => Promise>('open-new-window'), getReorAppVersion: createIPCHandler<() => Promise>('get-reor-app-version'), showFileItemContextMenu: createIPCHandler<(file: FileInfoNode) => Promise>('show-context-menu-file-item'), - showMenuItemContext: createIPCHandler<() => Promise>('show-context-menu-item'), showChatItemContext: createIPCHandler<(chatRow: ChatHistoryMetadata) => Promise>('show-chat-menu-item'), } @@ -85,19 +85,30 @@ const electronStore = { createIPCHandler<(params: LLMGenerationParameters) => Promise>('set-llm-generation-params'), getAnalyticsMode: createIPCHandler<() => Promise>('get-analytics-mode'), setAnalyticsMode: createIPCHandler<(isAnalytics: boolean) => Promise>('set-analytics-mode'), - getSpellCheckMode: createIPCHandler<() => Promise>('get-spellcheck-mode'), - setSpellCheckMode: createIPCHandler<(isSpellCheck: string) => Promise>('set-spellcheck-mode'), + getSpellCheckMode: createIPCHandler<() => Promise>('get-spellcheck-mode'), + setSpellCheckMode: createIPCHandler<(isSpellCheck: boolean) => Promise>('set-spellcheck-mode'), getHasUserOpenedAppBefore: createIPCHandler<() => Promise>('has-user-opened-app-before'), setHasUserOpenedAppBefore: createIPCHandler<() => Promise>('set-user-has-opened-app-before'), getAllChatHistories: createIPCHandler<() => Promise>('get-all-chat-histories'), updateChatHistory: createIPCHandler<(chatHistory: ChatHistory) => Promise>('update-chat-history'), removeChatHistoryAtID: createIPCHandler<(chatID: string) => Promise>('remove-chat-history-at-id'), 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'), + setCurrentOpenTabs: createIPCHandler<(action: string, args: any) => Promise>('set-current-open-files'), + addOpenTabs: createIPCHandler<(tab: Tab) => Promise>('add-current-open-files'), + removeOpenTabs: + createIPCHandler<(tabId: string, idx: number, newIndex: number) => Promise>('remove-current-open-files'), + clearOpenTabs: createIPCHandler<() => Promise>('clear-current-open-files'), + updateOpenTabs: + createIPCHandler<(draggedIndex: number, targetIndex: number) => Promise>('update-current-open-files'), + selectOpenTabs: createIPCHandler<(tabs: Tab[]) => Promise>('set-current-open-files'), + removeOpenTabsByPath: createIPCHandler<(path: string) => Promise>('remove-current-open-files-by-path'), } const fileSystem = { @@ -131,6 +142,7 @@ const path = { join: createIPCHandler<(...pathSegments: string[]) => Promise>('join-path'), dirname: createIPCHandler<(pathString: string) => Promise>('path-dirname'), relative: createIPCHandler<(from: string, to: string) => Promise>('path-relative'), + isAbsolute: createIPCHandler<(filePath: string) => Promise>('path-absolute'), addExtensionIfNoExtensionPresent: createIPCHandler<(pathString: string) => Promise>( 'add-extension-if-no-extension-present', ), diff --git a/package-lock.json b/package-lock.json index 6abb4b22..26c5bb07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "reor-project", - "version": "0.2.15", + "version": "0.2.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reor-project", - "version": "0.2.15", + "version": "0.2.18", "license": "AGPL-3.0", "dependencies": { "@aarkue/tiptap-math-extension": "^1.2.2", @@ -76,6 +76,8 @@ "tiptap-markdown": "^0.8.10", "turndown": "^7.1.2", "use-debounce": "^10.0.1", + "uuid": "^10.0.0", + "uuidv4": "^6.2.13", "vectordb": "0.4.10" }, "devDependencies": { @@ -2594,6 +2596,18 @@ } } }, + "node_modules/@langchain/community/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@langchain/core": { "version": "0.1.30", "license": "MIT", @@ -2634,6 +2648,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@langchain/openai": { "version": "0.0.14", "license": "MIT", @@ -5624,7 +5650,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001587", + "version": "1.0.30001647", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001647.tgz", + "integrity": "sha512-n83xdNiyeNcHpzWY+1aFbqCK7LuLfBricc4+alSQL2Xb6OR3XpnQAmlDG+pQcdTfiHRuLcQ96VOfrPSGiNJYSg==", "dev": true, "funding": [ { @@ -5639,8 +5667,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/ccount": { "version": "2.0.1", @@ -12398,6 +12425,18 @@ "undici-types": "~5.26.4" } }, + "node_modules/langchain/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/langchain/node_modules/yaml": { "version": "2.3.4", "license": "ISC", @@ -12430,6 +12469,18 @@ "node": ">=14" } }, + "node_modules/langsmith/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -19832,12 +19883,35 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "9.0.1", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uuidv4": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz", + "integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==", + "dependencies": { + "@types/uuid": "8.3.4", + "uuid": "8.3.2" + } + }, + "node_modules/uuidv4/node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, + "node_modules/uuidv4/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index 5443d1ba..5784bbee 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "cm6-theme-basic-dark": "^0.2.0", "date-fns": "^3.3.1", "dotenv": "^16.4.5", - "electron-store": "^8.1.0", + "electron-store": "^8.1.0", "electron-updater": "^6.1.1", "install": "^0.13.0", "js-tiktoken": "^1.0.10", @@ -95,6 +95,8 @@ "tiptap-markdown": "^0.8.10", "turndown": "^7.1.2", "use-debounce": "^10.0.1", + "uuid": "^10.0.0", + "uuidv4": "^6.2.13", "vectordb": "0.4.10" }, "devDependencies": { @@ -111,9 +113,9 @@ "@typescript-eslint/parser": "^7.16.0", "@vitejs/plugin-react": "^4.0.4", "autoprefixer": "^10.4.16", + "electron": "28.2.1", "electron-builder": "^24.6.3", "eslint": "^8.56.0", - "electron": "28.2.1", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-airbnb-typescript-prettier": "^5.0.0", diff --git a/src/components/Common/Modal.tsx b/src/components/Common/Modal.tsx index 9558ae09..e2c003e6 100644 --- a/src/components/Common/Modal.tsx +++ b/src/components/Common/Modal.tsx @@ -16,7 +16,6 @@ const ReorModal: React.FC = ({ tailwindStylesOnBackground, }) => { const modalRef = useRef(null) - // const widthClass = getDimension(widthType as ModalWidthType); useEffect(() => { const handleOffClick = (event: MouseEvent) => { @@ -40,9 +39,9 @@ const ReorModal: React.FC = ({ >
-
+
{!hideCloseButton && (
{isOpen && (
void - currentFilePath: string | null + currentFilePath: 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) => void + openAbsolutePath: (path: string) => void } const TitleBar: React.FC = ({ @@ -21,6 +24,8 @@ const TitleBar: React.FC = ({ toggleSimilarFiles, history, setHistory, + openFileAndOpenEditor, + openAbsolutePath, }) => { const [platform, setPlatform] = useState('') @@ -34,7 +39,7 @@ const TitleBar: React.FC = ({ }, []) return ( -
+
= ({ currentPath={currentFilePath || ''} />
+ +
+
+
+ + + +
+
+
+
{similarFilesOpen ? ( = ({ /> ) : ( { +const copyCommand = (state: EditorState): boolean => { if (state.selection.empty) return false const { from, to } = state.selection @@ -26,7 +26,7 @@ const copyCommand = (state: EditorState) => { return true } -const cutCommand = (state: EditorState, dispatch: Dispatch | null) => { +const cutCommand = (state: EditorState, dispatch: Dispatch | null): boolean => { if (state.selection.empty) return false copyCommand(state) @@ -41,7 +41,7 @@ const cutCommand = (state: EditorState, dispatch: Dispatch | null) => { * * Pastes text that currently exists in clipboard */ -const pasteCommand = async (editor: Editor) => { +const pasteCommand = async (editor: Editor): Promise => { if (navigator.clipboard) { try { const text = await navigator.clipboard.readText() @@ -55,7 +55,7 @@ const pasteCommand = async (editor: Editor) => { /** * Deletes the text that is selected. */ -const deleteCommand = (state: EditorState, dispatch: Dispatch | null) => { +const deleteCommand = (state: EditorState, dispatch: Dispatch | null): boolean => { const transaction = state.tr.deleteSelection() if (dispatch) { @@ -65,6 +65,17 @@ const deleteCommand = (state: EditorState, dispatch: Dispatch | null) => { return true } +/** + * Table that is displayed when hovering over table in contextMenu + * + * @param param onSelect: callback function that provides row and cols that user selected + * + * @returns number of rows and cols selected + */ +type TableSizeSelectorProps = { + onSelect: (rows: number, cols: number) => void +} + const TableSizeSelector: React.FC = ({ onSelect }) => { const maxRows = 10 const maxCols = 10 @@ -131,7 +142,7 @@ interface EditorContextMenuProps { * */ const EditorContextMenu: React.FC = ({ editor, menuPosition, setMenuVisible }) => { - const [showTableSelector, setShowTableSelector] = useState(false) + const [showTableSelector, setShowTableSelector] = useState(false) /** * We use useRef instead of state's because we are changing the style of our DOM but DO NOT * want to re-render. This style gets applied once and does not change so no re-render is needed. @@ -258,25 +269,4 @@ const EditorContextMenu: React.FC = ({ editor, menuPosit ) } -/** - * Table that is displayed when hovering over table in contextMenu - * - * @param param onSelect: callback function that provides row and cols that user selected - * - * @returns number of rows and cols selected - */ -type TableSizeSelectorProps = { - onSelect: (rows: number, cols: number) => void -} - -/** - * - * Copies text that is selected - */ - -/** - * - * Cuts text that is selected - */ - export default EditorContextMenu diff --git a/src/components/Editor/EditorManager.tsx b/src/components/Editor/EditorManager.tsx index e9011f8c..274004b2 100644 --- a/src/components/Editor/EditorManager.tsx +++ b/src/components/Editor/EditorManager.tsx @@ -22,7 +22,7 @@ const EditorManager: React.FC = ({ const [searchTerm, setSearchTerm] = useState('') const [menuVisible, setMenuVisible] = useState(false) const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }) - // const [showSimilarFiles, setShowSimilarFiles] = useState(true); + const [editorFlex, setEditorFlex] = useState(true) const toggleSearch = useCallback(() => { setShowSearch((prevShowSearch) => !prevShowSearch) @@ -85,13 +85,25 @@ const EditorManager: React.FC = ({ return () => window.removeEventListener('keydown', handleKeyDown) }, [showSearch, menuVisible, toggleSearch]) + // If "Content Flex Center" is set to true in Settings, then it centers the content of the Editor + useEffect(() => { + const initEditorContentCenter = async () => { + const isCenter = await window.electronStore.getEditorFlexCenter() + setEditorFlex(isCenter) + } + + const handleEditorChange = (event: any, editorFlexCenter: boolean) => { + setEditorFlex(editorFlexCenter) + } + + initEditorContentCenter() + window.ipcRenderer.on('editor-flex-center-changed', handleEditorChange) + }, []) + return (
editor?.commands.focus()} - style={{ - backgroundColor: 'rgb(30, 30, 30)', - }} > {showSearch && ( = ({ placeholder="Search..." // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus - className="absolute right-0 top-4 z-50 mr-14 mt-4 rounded-md border-none bg-transparent p-2 text-white" + className="absolute right-0 top-4 z-50 mr-14 mt-4 rounded-md border-none bg-transparent p-2 text-white" /> )} {menuVisible && } - +
+ +
{suggestionsState && ( Promise +} + +const EmptyPage: React.FC = ({ openAbsolutePath }) => { + const { isNewNoteModalOpen, setIsNewNoteModalOpen, isNewDirectoryModalOpen, setIsNewDirectoryModalOpen } = + useModalOpeners() + + return ( +
+
+ +
+

No File Selected!

+

Open a file to begin using Reor!

+
+ + +
+ setIsNewNoteModalOpen(false)} + openAbsolutePath={openAbsolutePath} + currentOpenFilePath="" + /> + setIsNewDirectoryModalOpen(false)} + currentOpenFilePath="" + /> +
+ ) +} + +export default EmptyPage diff --git a/src/components/File/FileSideBar/FileHistoryBar.tsx b/src/components/File/FileSideBar/FileHistoryBar.tsx index 67d0cab2..67bee608 100644 --- a/src/components/File/FileSideBar/FileHistoryBar.tsx +++ b/src/components/File/FileSideBar/FileHistoryBar.tsx @@ -134,7 +134,7 @@ const FileHistoryNavigator: React.FC = ({ return ( // eslint-disable-next-line tailwindcss/no-custom-classname -
+
) diff --git a/src/components/File/FileSideBar/index.tsx b/src/components/File/FileSideBar/index.tsx index 7770073f..5c817efd 100644 --- a/src/components/File/FileSideBar/index.tsx +++ b/src/components/File/FileSideBar/index.tsx @@ -84,17 +84,12 @@ const FileExplorer: React.FC = ({ return visibleItems } - const handleMenuContext = (e: React.MouseEvent) => { - e.preventDefault() - window.electronUtils.showMenuItemContext() - } - // Calculate visible items and item count const visibleItems = getVisibleFilesAndFlatten(files, expandedDirectories) const itemCount = visibleItems.length return ( -
+
void - onDirectoryCreate: string + currentOpenFilePath: string | null } -const NewDirectoryComponent: React.FC = ({ isOpen, onClose, onDirectoryCreate }) => { +const NewDirectoryComponent: React.FC = ({ isOpen, onClose, currentOpenFilePath }) => { const [directoryName, setDirectoryName] = useState('') const [errorMessage, setErrorMessage] = useState(null) @@ -41,15 +41,16 @@ const NewDirectoryComponent: React.FC = ({ isOpen, o const sendNewDirectoryMsg = async () => { try { - if (!directoryName || errorMessage) { + if (!directoryName || errorMessage || currentOpenFilePath === null) { return } - const normalizedDirectoryName = directoryName.replace(/\\/g, '/') - const basePath = onDirectoryCreate || (await window.electronStore.getVaultDirectoryForWindow()) - const fullPath = await window.path.join(basePath, normalizedDirectoryName) - + const directoryPath = + currentOpenFilePath === '' + ? await window.electronStore.getVaultDirectoryForWindow() + : await window.path.dirname(currentOpenFilePath) + const finalPath = await window.path.join(directoryPath, directoryName) + window.fileSystem.createDirectory(finalPath) posthog.capture('created_new_directory_from_new_directory_modal') - window.fileSystem.createDirectory(fullPath) onClose() } catch (e) { toast.error(errorToStringRendererProcess(e), { diff --git a/src/components/File/NewNote.tsx b/src/components/File/NewNote.tsx index 7a5fdefd..6397564d 100644 --- a/src/components/File/NewNote.tsx +++ b/src/components/File/NewNote.tsx @@ -10,11 +10,16 @@ import { getInvalidCharacterInFilePath } from '@/utils/strings' interface NewNoteComponentProps { isOpen: boolean onClose: () => void - openRelativePath: (path: string) => void - customFilePath: string + openAbsolutePath: (path: string) => void + currentOpenFilePath: string | null } -const NewNoteComponent: React.FC = ({ isOpen, onClose, openRelativePath, customFilePath }) => { +const NewNoteComponent: React.FC = ({ + isOpen, + onClose, + openAbsolutePath, + currentOpenFilePath, +}) => { const [fileName, setFileName] = useState('') const [errorMessage, setErrorMessage] = useState(null) @@ -39,12 +44,15 @@ const NewNoteComponent: React.FC = ({ isOpen, onClose, op } const sendNewNoteMsg = async () => { - if (!fileName || errorMessage) { + if (!fileName || errorMessage || currentOpenFilePath === null) { return } - const pathPrefix = customFilePath ? customFilePath.replace(/\/?$/, '/') : '' - const fullPath = pathPrefix + fileName - openRelativePath(fullPath) + let finalPath = fileName + if (currentOpenFilePath !== '') { + const directoryName = await window.path.dirname(currentOpenFilePath) + finalPath = await window.path.join(directoryName, fileName) + } + openAbsolutePath(finalPath) posthog.capture('created_new_note_from_new_note_modal') onClose() } diff --git a/src/components/File/hooks/use-file-by-filepath.ts b/src/components/File/hooks/use-file-by-filepath.ts index a6dd513c..1106286d 100644 --- a/src/components/File/hooks/use-file-by-filepath.ts +++ b/src/components/File/hooks/use-file-by-filepath.ts @@ -18,13 +18,13 @@ import StarterKit from '@tiptap/starter-kit' import { toast } from 'react-toastify' import { Markdown } from 'tiptap-markdown' import { useDebounce } from 'use-debounce' +import { getInvalidCharacterInFilePath, removeFileExtension } from '@/utils/strings' import { BacklinkExtension } from '@/components/Editor/BacklinkExtension' import { SuggestionsState } from '@/components/Editor/BacklinkSuggestionsDisplay' import HighlightExtension, { HighlightData } from '@/components/Editor/HighlightExtension' import { RichTextLink } from '@/components/Editor/RichTextLink' import SearchAndReplace from '@/components/Editor/SearchAndReplace' -import { getInvalidCharacterInFilePath, removeFileExtension } from '@/utils/strings' import 'katex/dist/katex.min.css' import '../tiptap.scss' import welcomeNote from '../utils' @@ -34,7 +34,7 @@ const useFileByFilepath = () => { const [suggestionsState, setSuggestionsState] = useState() const [needToWriteEditorContentToDisk, setNeedToWriteEditorContentToDisk] = useState(false) const [needToIndexEditorContent, setNeedToIndexEditorContent] = useState(false) - const [spellCheckEnabled, setSpellCheckEnabled] = useState('false') + const [spellCheckEnabled, setSpellCheckEnabled] = useState(false) useEffect(() => { const fetchSpellCheckMode = async () => { @@ -107,6 +107,31 @@ const useFileByFilepath = () => { openFileByPath(absolutePath) } + const openAbsolutePath = async (filePath: string, optionalContentToWriteOnCreate?: string): Promise => { + const invalidChars = await getInvalidCharacterInFilePath(filePath) + if (invalidChars) { + toast.error(`Could not create note ${filePath}. Character ${invalidChars} cannot be included in note name.`) + throw new Error(`Could not create note ${filePath}. Character ${filePath} cannot be included in note name.`) + } + const filePathWithExtension = await window.path.addExtensionIfNoExtensionPresent(filePath) + let absolutePath = filePathWithExtension + // If we create a newNote on an empty page (no file open). + if (!(await window.path.isAbsolute(filePath))) { + absolutePath = await window.path.join( + await window.electronStore.getVaultDirectoryForWindow(), + filePathWithExtension, + ) + } + const fileExists = await window.fileSystem.checkFileExists(absolutePath) + if (!fileExists) { + const basename = await window.path.basename(absolutePath) + const content = optionalContentToWriteOnCreate || `## ${removeFileExtension(basename)}\n` + await window.fileSystem.createFile(absolutePath, content) + setNeedToIndexEditorContent(true) + } + openFileByPath(absolutePath) + } + const openRelativePathRef = useRef<(newFilePath: string) => Promise>() openRelativePathRef.current = openRelativePath @@ -185,7 +210,7 @@ const useFileByFilepath = () => { editor.setOptions({ editorProps: { attributes: { - spellcheck: spellCheckEnabled, + spellcheck: spellCheckEnabled.toString(), }, }, }) @@ -222,7 +247,7 @@ const useFileByFilepath = () => { 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) { editor?.commands.setContent('') @@ -299,12 +324,13 @@ const useFileByFilepath = () => { return { filePath: currentlyOpenedFilePath, + setFilePath: setCurrentlyOpenedFilePath, saveCurrentlyOpenedFile, editor, navigationHistory, setNavigationHistory, openFileByPath, - openRelativePath, + openAbsolutePath, suggestionsState, spellCheckEnabled, highlightData, diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index 598f4039..d6f75c57 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -15,6 +15,9 @@ import SidebarManager, { SidebarAbleToShow } from './Sidebars/MainSidebar' import SimilarFilesSidebarComponent from './Sidebars/SimilarFilesSidebar' import WritingAssistant from './Writing-Assistant/WritingAssistantFloatingMenu' import { ChatFilters, ChatHistory } from './Chat/chatUtils' +import EmptyPage from './EmptyPage' +import { TabProvider } from './Providers/TabProvider' +import { ModalProvider } from './Providers/ModalProvider' const MainPageComponent: React.FC = () => { const [showChatbot, setShowChatbot] = useState(false) @@ -23,9 +26,10 @@ const MainPageComponent: React.FC = () => { const { filePath, + setFilePath, editor, openFileByPath, - openRelativePath, + openAbsolutePath, saveCurrentlyOpenedFile, suggestionsState, highlightData, @@ -64,7 +68,7 @@ const MainPageComponent: React.FC = () => { maxDate: new Date(), }) - const [sidebarWidth, setSidebarWidth] = useState(40) + const [sidebarWidth, setSidebarWidth] = useState(40) // find all available files useEffect(() => { @@ -109,26 +113,43 @@ const MainPageComponent: React.FC = () => { }, [setCurrentChatHistory, setChatFilters]) return ( -
- + {/* Displays the dropdown tab when hovering. You cannot use z-index and position absolute inside + TitleBar since one of the Parent components inadvertently creates a new stacking context that + impacts the z-index. */} +
+ + sidebarShowing={sidebarShowing} + makeSidebarShow={setSidebarShowing} + > + +
- + + +
@@ -154,7 +175,7 @@ const MainPageComponent: React.FC = () => {
- {!showChatbot && filePath && ( + {!showChatbot && filePath ? (
{
)}
+ ) : ( + !showChatbot && ( +
+ + + +
+ ) )} {showChatbot && ( diff --git a/src/components/Providers/ModalProvider.tsx b/src/components/Providers/ModalProvider.tsx new file mode 100644 index 00000000..6dd7dbaa --- /dev/null +++ b/src/components/Providers/ModalProvider.tsx @@ -0,0 +1,59 @@ +import React, { createContext, useContext, useState, ReactNode, useMemo } from 'react' + +interface ModalProviderProps { + children: ReactNode +} + +/** + * Every modal requires a setter and opener + */ +interface ModalOpenContextType { + isNewNoteModalOpen: boolean + setIsNewNoteModalOpen: (newNote: boolean) => void + isNewDirectoryModalOpen: boolean + setIsNewDirectoryModalOpen: (newDir: boolean) => void + isSettingsModalOpen: boolean + setIsSettingsModalOpen: (settingsOpen: boolean) => void + isFlashcardModeOpen: boolean + setIsFlashcardModeOpen: (flashcardOpen: boolean) => void +} + +const defaultModalContext: ModalOpenContextType = { + isNewNoteModalOpen: false, + setIsNewNoteModalOpen: () => {}, + isNewDirectoryModalOpen: false, + setIsNewDirectoryModalOpen: () => {}, + isSettingsModalOpen: false, + setIsSettingsModalOpen: () => {}, + isFlashcardModeOpen: false, + setIsFlashcardModeOpen: () => {}, +} + +const ModalContext = createContext(defaultModalContext) + +export const useModalOpeners = (): ModalOpenContextType => useContext(ModalContext) + +export const ModalProvider: React.FC = ({ children }) => { + const [isNewNoteModalOpen, setIsNewNoteModalOpen] = useState(false) + const [isNewDirectoryModalOpen, setIsNewDirectoryModalOpen] = useState(false) + const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false) + const [isFlashcardModeOpen, setIsFlashcardModeOpen] = useState(false) + + const modalOpenContextValue = useMemo( + () => ({ + isNewNoteModalOpen, + setIsNewNoteModalOpen, + isNewDirectoryModalOpen, + setIsNewDirectoryModalOpen, + isSettingsModalOpen, + setIsSettingsModalOpen, + isFlashcardModeOpen, + setIsFlashcardModeOpen, + }), + [isNewNoteModalOpen, isNewDirectoryModalOpen, isSettingsModalOpen, isFlashcardModeOpen], + ) + + return {children} +} + +export default ModalProvider diff --git a/src/components/Providers/TabProvider.tsx b/src/components/Providers/TabProvider.tsx new file mode 100644 index 00000000..72ba46fd --- /dev/null +++ b/src/components/Providers/TabProvider.tsx @@ -0,0 +1,172 @@ +import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode } from 'react' +import { v4 as uuidv4 } from 'uuid' +import { Tab } from 'electron/main/electron-store/storeConfig' +import { SidebarAbleToShow } from '../Sidebars/MainSidebar' + +interface TabProviderProps { + children: ReactNode + openFileAndOpenEditor: (path: string) => void + setFilePath: (path: string) => void + currentFilePath: string | null + sidebarShowing: string | null + makeSidebarShow: (option: SidebarAbleToShow) => void +} + +interface TabContextType { + openTabs: Tab[] + addTab: (path: string) => void + selectTab: (tab: Tab) => void + removeTabByID: (tabId: string) => void + updateTabOrder: (draggedIdx: number, targetIdx: number) => void +} + +const defaultTypeContext: TabContextType = { + openTabs: [], + addTab: () => {}, + selectTab: () => {}, + removeTabByID: () => {}, + updateTabOrder: () => {}, +} + +const TabContext = createContext(defaultTypeContext) + +// Contains openTabs, addTab, selectTab, removeTabByID, updateTabOrder +export const useTabs = (): TabContextType => useContext(TabContext) + +export const TabProvider: React.FC = ({ + children, + openFileAndOpenEditor, + setFilePath, + currentFilePath, + sidebarShowing, + makeSidebarShow, +}) => { + const [openTabs, setOpenTabs] = useState([]) + + useEffect(() => { + const fetchHistoryTabs = async () => { + const response: Tab[] = await window.electronStore.getCurrentOpenTabs() + setOpenTabs(response) + } + + fetchHistoryTabs() + }, []) + + useEffect(() => { + const removeTabByPath = (tabs: Tab[]) => { + setOpenTabs(tabs) + } + + const removeTabByPathListener = window.ipcRenderer.receive('remove-tab-after-deletion', removeTabByPath) + return () => { + removeTabByPathListener() + } + }, []) + + 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 + } + + /* Adds a new tab and syncs it with the backend */ + const addTab = useCallback( + (path: string) => { + const createTabObjectFromPath = (tabPath: string) => { + return { + id: uuidv4(), + filePath: tabPath, + title: extractFileName(path), + lastAccessed: true, + // timeOpened: new Date(), + // isDirty: false, + } + } + + const existingTab = openTabs.find((tab: Tab) => tab.filePath === path) + if (existingTab) return + const tab = createTabObjectFromPath(path) + + setOpenTabs((prevTabs) => { + const newTabs = [...prevTabs, tab] + window.electronStore.addOpenTabs(tab) + return newTabs + }) + }, + [openTabs], + ) + + /* Removes a tab and syncs it with the backend */ + const removeTabByID = useCallback( + (tabId: string) => { + let closedFilePath = '' + let newIndex = -1 + let findIdx = -1 + + setOpenTabs((prevTabs) => { + findIdx = prevTabs.findIndex((tab: Tab) => tab.id === tabId) + if (findIdx === -1) return prevTabs + + openTabs[findIdx].lastAccessed = false + closedFilePath = findIdx !== -1 ? prevTabs[findIdx].filePath : '' + newIndex = findIdx > 0 ? findIdx - 1 : 1 + + if (closedFilePath === currentFilePath) { + if (newIndex < openTabs.length) { + openTabs[newIndex].lastAccessed = true + openFileAndOpenEditor(openTabs[newIndex].filePath) + } + // Select the new index's file + else setFilePath('') + } + + return prevTabs.filter((_, idx) => idx !== findIdx) + }) + + if (newIndex !== -1 && findIdx !== -1) { + window.electronStore.removeOpenTabs(tabId, findIdx, newIndex) + } + }, + [currentFilePath, openFileAndOpenEditor, openTabs, setFilePath], + ) + + /* Updates tab order (on drag) and syncs it with backend */ + const updateTabOrder = useCallback((draggedIndex: number, targetIndex: number) => { + setOpenTabs((prevTabs) => { + const newTabs = [...prevTabs] + const [draggedTab] = newTabs.splice(draggedIndex, 1) + newTabs.splice(targetIndex, 0, draggedTab) + window.electronStore.updateOpenTabs(draggedIndex, targetIndex) + return newTabs + }) + }, []) + + /* Selects a tab and syncs it with the backend */ + const selectTab = useCallback( + (selectedTab: Tab) => { + setOpenTabs((prevTabs) => { + const newTabs = prevTabs.map((tab) => ({ + ...tab, + lastAccessed: tab.id === selectedTab.id, + })) + window.electronStore.selectOpenTabs(newTabs) + return newTabs + }) + if (sidebarShowing !== 'files') makeSidebarShow('files') + openFileAndOpenEditor(selectedTab.filePath) + }, + [openFileAndOpenEditor, makeSidebarShow, sidebarShowing], + ) + + const TabContextMemo = useMemo( + () => ({ + openTabs, + addTab, + removeTabByID, + updateTabOrder, + selectTab, + }), + [openTabs, addTab, removeTabByID, updateTabOrder, selectTab], + ) + + return {children} +} diff --git a/src/components/Settings/EmbeddingSettings/EmbeddingSettings.tsx b/src/components/Settings/EmbeddingSettings/EmbeddingSettings.tsx index 9fb5e68e..d8130994 100644 --- a/src/components/Settings/EmbeddingSettings/EmbeddingSettings.tsx +++ b/src/components/Settings/EmbeddingSettings/EmbeddingSettings.tsx @@ -131,7 +131,7 @@ const EmbeddingModelSettings: React.FC = ({
{/* Warning message at the bottom */} -

+

Note: If you notice some lag in the editor it is likely because you chose too large of a model...

{' '} { @@ -35,14 +18,17 @@ const CreateAppearanceSection: React.FC = () => { }, []) return ( -
-

+
+

Appearance

-
+
+
-

IconSidebar Compact

-

If on, decreases padding on IconSidebar

+

+ IconSidebar Compact + Decreases padding on IconSidebar +

{ }} />
- {/*
-
-
-

Dynamic Markdown Heading

- - Beta +
+
+ ) +} + +export const EditorSection = () => { + // const { spellCheckEnabled, setSpellCheckEnabled } = useFileByFilepath() + const [tempSpellCheckEnabled, setTempSpellCheckEnabled] = useState(false) + const [editorFlexCenter, setEditorFlexCenter] = useState(true) + + useEffect(() => { + const fetchParams = async () => { + const isSpellCheckEnabled = await window.electronStore.getSpellCheckMode() + + if (isSpellCheckEnabled !== undefined) { + // setSpellCheckEnabled(isSpellCheckEnabled) + setTempSpellCheckEnabled(isSpellCheckEnabled) + } + } + + fetchParams() + }, []) + + const handleSave = (setChecked: boolean) => { + // Execute the save function here + window.electronStore.setSpellCheckMode(setChecked) + setTempSpellCheckEnabled(!tempSpellCheckEnabled) + } + + // Check if we should have flex center for our editor + useEffect(() => { + const fetchParams = async () => { + const getEditorFlexCenter = await window.electronStore.getEditorFlexCenter() + + if (getEditorFlexCenter !== undefined) { + setEditorFlexCenter(getEditorFlexCenter) + } + } + + fetchParams() + }, []) + + return ( +
+

+ Editor +

+
+
+
+

+ Content Flex Center + + Centers content inside editor. Recommended for larger screens -

-

- Allows you to manually change header markdown on hover

{ - setDisplayMarkdown(!displayMarkdown); - if (displayMarkdown !== undefined) { - window.electronStore.setDisplayMarkdown(!displayMarkdown); + setEditorFlexCenter(!editorFlexCenter) + if (editorFlexCenter !== undefined) { + window.electronStore.setEditorFlexCenter(!editorFlexCenter) } }} - disabled={false} /> -
*/} +
+
+
+
+

+ Spell Check + + Note: Quit and restart the app for this to take effect + +

+
+ { + handleSave(!tempSpellCheckEnabled) + }} + inputProps={{ 'aria-label': 'controlled' }} + /> +
+
) } - -export default CreateAppearanceSection diff --git a/src/components/Settings/GeneralSettings.tsx b/src/components/Settings/GeneralSettings.tsx index a62fdb0f..33008df7 100644 --- a/src/components/Settings/GeneralSettings.tsx +++ b/src/components/Settings/GeneralSettings.tsx @@ -1,11 +1,5 @@ -import React, { useEffect, useState } from 'react' - -import { Button } from '@material-tailwind/react' -import Switch from '@mui/material/Switch' - -import useFileByFilepath from '../File/hooks/use-file-by-filepath' - -import CreateAppearanceSection, { GenSettingsProps } from './GeneralSections' +import React from 'react' +import { AppearanceSection, EditorSection } from './GeneralSections' /* * General Page has the following format: @@ -25,122 +19,12 @@ import CreateAppearanceSection, { GenSettingsProps } from './GeneralSections' * SubHeader describe the part of the project you are changing (appearance, editor, sidebar, etc..). Option(s) is the name of the specific change. */ -const GeneralSettings: React.FC = () => { - const { spellCheckEnabled, setSpellCheckEnabled } = useFileByFilepath() - const [userHasMadeUpdate, setUserHasMadeUpdate] = useState(false) - const [tempSpellCheckEnabled, setTempSpellCheckEnabled] = useState('false') - - useEffect(() => { - const fetchParams = async () => { - const isSpellCheckEnabled = await window.electronStore.getSpellCheckMode() - - if (isSpellCheckEnabled !== undefined) { - setSpellCheckEnabled(isSpellCheckEnabled) - setTempSpellCheckEnabled(isSpellCheckEnabled) - } - } - - fetchParams() - }, [spellCheckEnabled, setSpellCheckEnabled]) - - const handleSave = () => { - // Execute the save function here - window.electronStore.setSpellCheckMode(tempSpellCheckEnabled) - setSpellCheckEnabled(tempSpellCheckEnabled) - setUserHasMadeUpdate(false) - } +const GeneralSettings = () => { return (

General

- -

Spell Check

- { - setUserHasMadeUpdate(true) - if (tempSpellCheckEnabled === 'true') setTempSpellCheckEnabled('false') - else setTempSpellCheckEnabled('true') - }} - inputProps={{ 'aria-label': 'controlled' }} - /> - {userHasMadeUpdate && ( -
- -
- )} -

Quit and restart the app for it to take effect

- {/* ======= -import { Button } from "@material-tailwind/react"; -import Switch from "@mui/material/Switch"; -import React, { useEffect, useState } from "react"; -import useFileByFilepath from "../File/hooks/use-file-by-filepath"; - -interface GeneralSettingsProps {} -const GeneralSettings: React.FC = () => { - const { spellCheckEnabled, setSpellCheckEnabled } = useFileByFilepath(); - const [userHasMadeUpdate, setUserHasMadeUpdate] = useState(false); - const [tempSpellCheckEnabled, setTempSpellCheckEnabled] = useState("false"); - - useEffect(() => { - const fetchParams = async () => { - const isSpellCheckEnabled = - await window.electronStore.getSpellCheckMode(); - - if (isSpellCheckEnabled !== undefined) { - setSpellCheckEnabled(isSpellCheckEnabled); - setTempSpellCheckEnabled(isSpellCheckEnabled); - } - }; - - fetchParams(); - }, [spellCheckEnabled]); - - const handleSave = () => { - // Execute the save function here - window.electronStore.setSpellCheckMode(tempSpellCheckEnabled); - setSpellCheckEnabled(tempSpellCheckEnabled); - setUserHasMadeUpdate(false); - }; - - return ( -
-

General

{" "} -

Spell Check

- { - setUserHasMadeUpdate(true); - if (tempSpellCheckEnabled == "true") - setTempSpellCheckEnabled("false"); - else setTempSpellCheckEnabled("true"); - }} - inputProps={{ "aria-label": "controlled" }} - /> - {userHasMadeUpdate && ( -
- -
- )} - { -

- Quit and restart the app for it to take effect -

- } ->>>>>>> main */} + +
) } diff --git a/src/components/Settings/LLMSettings/LLMSettingsContent.tsx b/src/components/Settings/LLMSettings/LLMSettingsContent.tsx index 2c9f25b6..5cc6061d 100644 --- a/src/components/Settings/LLMSettings/LLMSettingsContent.tsx +++ b/src/components/Settings/LLMSettings/LLMSettingsContent.tsx @@ -45,15 +45,15 @@ const LLMSettingsContent: React.FC = ({ ] return ( -
+

LLM

{llmConfigs.length > 0 && ( -
+
{/*

Default LLM

*/}

Default LLM

-
+
= ({
)} +
= ({ isOpen, onClose: onCloseFromParent }) => { const [willNeedToReIndex, setWillNeedToReIndex] = useState(false) - const [activeTab, setActiveTab] = useState(SettingsTab.LLM) + const [activeTab, setActiveTab] = useState(SettingsTab.GeneralSettingsTab) const handleSave = () => { if (willNeedToReIndex) { @@ -41,91 +41,63 @@ const SettingsModal: React.FC = ({ isOpen, onClose: onCloseFromParen handleSave() }} > -
-
+
+
+
setActiveTab(SettingsTab.GeneralSettingsTab)} + > + General +
setActiveTab(SettingsTab.LLM)} + onClick={() => setActiveTab(SettingsTab.LLMSettingsTab)} > LLM
setActiveTab(SettingsTab.EmbeddingModel)} + onClick={() => setActiveTab(SettingsTab.EmbeddingModelTab)} > Embedding Model
setActiveTab(SettingsTab.TextGeneration)} + onClick={() => setActiveTab(SettingsTab.TextGenerationTab)} > Text Generation{' '}
- {/*
setActiveTab(SettingsTab.RAG)} - > - RAG{" "} -
*/}
setActiveTab(SettingsTab.ANALYTICS)} + onClick={() => setActiveTab(SettingsTab.AnalyticsTab)} > Analytics{' '}
-
setActiveTab(SettingsTab.General)} - > - General{' '} -
{/* Right Content Area */} -
+
{/*

Settings

*/} - {activeTab === SettingsTab.General && ( -
- -
- )} - {activeTab === SettingsTab.LLM && ( -
- -
- )} - {activeTab === SettingsTab.EmbeddingModel && ( -
- setWillNeedToReIndex(true)} /> -
+ {activeTab === SettingsTab.GeneralSettingsTab && } + {activeTab === SettingsTab.LLMSettingsTab && } + {activeTab === SettingsTab.EmbeddingModelTab && ( + setWillNeedToReIndex(true)} /> )} - {activeTab === SettingsTab.TextGeneration && ( -
- -
- )} + {activeTab === SettingsTab.TextGenerationTab && } - {activeTab === SettingsTab.ANALYTICS && ( -
- -
- )} + {activeTab === SettingsTab.AnalyticsTab && }
diff --git a/src/components/Settings/Shared/SettingsRow.tsx b/src/components/Settings/Shared/SettingsRow.tsx index c7aed1e8..e529cb4a 100644 --- a/src/components/Settings/Shared/SettingsRow.tsx +++ b/src/components/Settings/Shared/SettingsRow.tsx @@ -8,17 +8,17 @@ const SettingsRow: React.FC<{ onClick?: () => void children?: React.ReactNode }> = ({ title, description, buttonText, onClick, children }) => ( -
-
+
+

{title} - {description &&

{description}

} + {description && {description}}

{buttonText && (
) diff --git a/src/components/Tabs/TabBar.tsx b/src/components/Tabs/TabBar.tsx new file mode 100644 index 00000000..627b6ce4 --- /dev/null +++ b/src/components/Tabs/TabBar.tsx @@ -0,0 +1,195 @@ +import React, { useState, DragEventHandler, useRef, useEffect } from 'react' +import { FaPlus } from 'react-icons/fa6' +import { createPortal } from 'react-dom' +import { Tab } from 'electron/main/electron-store/storeConfig' +import { removeFileExtension } from '@/utils/strings' +import '../../styles/tab.css' +import { useTabs } from '../Providers/TabProvider' +import NewNoteComponent from '../File/NewNote' +import { useModalOpeners } from '../Providers/ModalProvider' + +interface DraggableTabsProps { + currentFilePath: string + openFileAndOpenEditor: (path: string) => void + openAbsolutePath: (path: string) => void +} + +interface TooltipProps { + filepath: string + position: { x: number; y: number } +} + +/* Displays the filepath when hovering on a tab */ +const Tooltip: React.FC = ({ filepath, position }) => { + const [style, setStyle] = useState({ top: 0, left: 0, maxWidth: '300px' }) + + useEffect(() => { + const viewportWidth = window.innerWidth + let maxWidth = '300px' + + if (position.x && viewportWidth) { + const availableWidth = viewportWidth - position.x - 10 + maxWidth = `${availableWidth}px` + } + + setStyle({ + top: position.y, + left: position.x, + maxWidth, + }) + }, [position]) + + return createPortal( +
+ {filepath} +
, + document.getElementById('tooltip-container') as HTMLElement, + ) +} + +const DraggableTabs: React.FC = ({ currentFilePath, openFileAndOpenEditor, openAbsolutePath }) => { + const { openTabs, addTab, selectTab, removeTabByID, updateTabOrder } = useTabs() + const [isLastTabAccessed, setIsLastTabAccessed] = useState(false) + + const [hoveredTab, setHoveredTab] = useState(null) + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }) + const containerRef = useRef(null) + + const { isNewNoteModalOpen, setIsNewNoteModalOpen } = useModalOpeners() + + // Note: Do not put dependency on addTab or else removeTabByID does not work properly. + // Typically you would define addTab inside the useEffect and then call it but since + // we are using it inside a useContext we can remove it + /* eslint-disable */ + useEffect(() => { + if (!currentFilePath) return + addTab(currentFilePath) + }, [currentFilePath]) + /* eslint-enable */ + + /* + * Deals with setting which file to open on launch. + */ + useEffect(() => { + const selectLastTabAccessed = () => { + if (!isLastTabAccessed) { + openTabs.forEach((tab: Tab) => { + if (tab.lastAccessed) { + setIsLastTabAccessed(true) + openFileAndOpenEditor(tab.filePath) + return true + } + return false + }) + } + } + + selectLastTabAccessed() + }, [openTabs, openFileAndOpenEditor, isLastTabAccessed]) + + const onDragStart = (event: any, tabId: string) => { + event.dataTransfer.setData('tabId', tabId) + } + + const onDrop: DragEventHandler = (event) => { + event.preventDefault() + const draggedTabId = event.dataTransfer.getData('tabId') + + let target = event.target as HTMLElement | null + // Iterate each child until we find the one we moved to + while (target && !target.getAttribute('data-tabid')) { + target = target.parentElement as HTMLElement | null + } + + const targetTabId = target ? target.getAttribute('data-tabid') : null + + if (draggedTabId && targetTabId) { + const draggedIndex = openTabs.findIndex((tab) => tab.id === draggedTabId) + const targetIndex = openTabs.findIndex((tab) => tab.id === targetTabId) + updateTabOrder(draggedIndex, targetIndex) + } + } + + const onDragOver: DragEventHandler = (event) => { + event.preventDefault() + } + + const handleMouseEnter = (e: React.MouseEvent, tab: Tab) => { + const rect = e.currentTarget.getBoundingClientRect() || null + setHoveredTab(tab.filePath) + setTooltipPosition({ + x: rect.left - 75, + y: rect.bottom - 5, + }) + } + + const handleMouseLevel = () => { + setHoveredTab(null) + } + + const handleTabSelect = (tab: Tab) => { + selectTab(tab) + } + + const handleTabClose = (event: React.MouseEvent, tabId: string) => { + event.stopPropagation() + removeTabByID(tabId) + } + + return ( +
+
+ {openTabs && + openTabs.map((tab) => ( +
handleMouseEnter(e, tab)} + onMouseLeave={handleMouseLevel} + > +
onDragStart(event, tab.id)} + onDrop={onDrop} + onDragOver={onDragOver} + className={`relative flex w-full cursor-pointer items-center justify-between gap-1 rounded-md p-2 text-sm text-white + ${currentFilePath === tab.filePath ? 'bg-dark-gray-c-eleven' : ''}`} + onClick={() => handleTabSelect(tab)} + > + {removeFileExtension(tab.title)} + { + e.stopPropagation() // Prevent triggering onClick of parent div + handleTabClose(e, tab.id) + }} + > + × + + {hoveredTab === tab.filePath && } +
+
+ ))} +
+ {openTabs.length > 0 && ( +
setIsNewNoteModalOpen(true)} + > + +
+ )} + setIsNewNoteModalOpen(false)} + openAbsolutePath={openAbsolutePath} + currentOpenFilePath={currentFilePath} + /> +
+ ) +} + +export default DraggableTabs diff --git a/src/styles/global.css b/src/styles/global.css index 85a869cb..36b0aa81 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -29,12 +29,20 @@ a { #customTitleBar { -webkit-app-region: drag; + height: 40px; } -#customTitleBar * { +#customTitleBar #titleBarFileNavigator, +#titleBarSimilarFiles { -webkit-app-region: no-drag; } +#titleBarSingleTab { + -webkit-app-region: no-drag; + padding-top: 25px; + padding-bottom: 25px; +} + button { -webkit-app-region: no-drag; } @@ -55,15 +63,10 @@ button { } ::-webkit-scrollbar-thumb { - background: rgb(112, 112, 112); - /* Set the thumb color */ - border-radius: 10px; + background-color: rgba(95, 125, 139, 0.5); + border-radius: 2px; } - - - - /* CSS for menu that appears when selecting/highlighting text */ .bubble-menu { display: flex; diff --git a/src/styles/tab.css b/src/styles/tab.css new file mode 100644 index 00000000..af722dcd --- /dev/null +++ b/src/styles/tab.css @@ -0,0 +1,28 @@ +/* CSS for dropdown animation onMouseEnter */ +.tab-tooltip { + position: absolute; + background-color: black; + color: white; + padding: 5px 10px; + border-radius: 8px; + white-space: no-wrap; + z-index: 1010; + animation: fadeIn 0.3s ease-in-out; + height: 30px; + max-width: 100%; + font-size: 14px; + text-align: center; + white-space: nowrap; + + overflow: hidden; + text-overflow: ellipsis; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 90f40683..b3662686 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -16,7 +16,7 @@ export const getInvalidCharacterInFilePath = async (filename: string): Promise?|]/ + invalidCharacters = /["*<>?|]/ break case 'darwin': invalidCharacters = /[:]/ diff --git a/tailwind.config.js b/tailwind.config.js index 47525c77..45a579ef 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -16,6 +16,7 @@ export default { 'dark-gray-c-eight': '#333333', 'dark-gray-c-nine': '#343434', 'dark-gray-c-ten': '#383838', + 'dark-gray-c-eleven': '#191919', "dark-slate-gray": "#2F4F4F", "light-arsenic": "#182c44", @@ -39,7 +40,19 @@ export default { }, transitionDuration: { '400': '400ms' - } + }, + fontSize: { + '2lg': '1.0rem', + }, + keyframes: { + slideIn: { + '0%': { transform: 'translateX(100%)', opacity: '0'}, + '100%': { transform: 'translateX(0)', opacity: '1'}, + }, + }, + animation: { + 'slide-in': 'slideIn 0.3s ease-out', + }, }, }, corePlugins: {