From 5afc528dacfc2cb6a8d455479a36aeba55c8de1c Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Mon, 17 Jun 2024 23:44:04 -0500 Subject: [PATCH 01/40] Starting adding tab support, but cannot extend directly from history. --- .package.json.un~ | Bin 2279 -> 0 bytes electron/main/Store/storeConfig.ts | 11 + electron/main/Store/storeHandlers.ts | 43 +++ electron/preload/index.ts | 8 + package-lock.json | 294 +++++++----------- package.json | 5 +- package.json~ | 146 +++++++++ src/components/Editor/HighlightExtension.tsx | 3 - src/components/Editor/LLMQueryTab.tsx | 16 + src/components/Editor/QueryInput.tsx | 195 ++++++++++++ .../File/FileSideBar/FileHistoryBar.tsx | 33 +- .../File/hooks/use-file-by-filepath.ts | 13 +- src/components/FileEditorContainer.tsx | 18 +- .../Similarity/SimilarFilesSidebar.tsx | 1 - src/components/TitleBar.tsx | 10 + 15 files changed, 594 insertions(+), 202 deletions(-) delete mode 100644 .package.json.un~ create mode 100644 package.json~ create mode 100644 src/components/Editor/LLMQueryTab.tsx create mode 100644 src/components/Editor/QueryInput.tsx diff --git a/.package.json.un~ b/.package.json.un~ deleted file mode 100644 index 8d1ad8c986b4db1ce6a563230eba70b8a220f54a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2279 zcmWH`%$*;a=aT=FfoWgevct@~rPESgtdlBva2#zKY8#y9CQHB~#a2}c|qg*P405I36fuo595lx^dXaizUz>DOj zDp@Hg6{Y4E=@t~_XQd{WDCt0>4y29+h+zN}jUYYX2m|M^(Wrxk4Gm~u)cJy=jum^< zi7MfVJ5XG~icyf;K=BBVtdA^-pY diff --git a/electron/main/Store/storeConfig.ts b/electron/main/Store/storeConfig.ts index 349ed889..43110c1a 100644 --- a/electron/main/Store/storeConfig.ts +++ b/electron/main/Store/storeConfig.ts @@ -50,6 +50,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 + timeOpened: Date; // Timestamp to preserve order + isDirty: boolean; // Flag to indicate unsaved changes + lastAccessed: Date; // Timestamp for the last access to enable features in the future +}; + export interface StoreSchema { hasUserOpenedAppBefore: boolean; schemaVersion: number; @@ -73,6 +82,7 @@ export interface StoreSchema { chunkSize: number; isSBCompact: boolean; DisplayMarkdown: boolean; + OpenTabs: Tab[]; } export enum StoreKeys { @@ -91,4 +101,5 @@ export enum StoreKeys { ChunkSize = "chunkSize", IsSBCompact = "isSBCompact", DisplayMarkdown = "DisplayMarkdown", + OpenTabs = "OpenTabs", } diff --git a/electron/main/Store/storeHandlers.ts b/electron/main/Store/storeHandlers.ts index 7f6c9bd5..f6e31912 100644 --- a/electron/main/Store/storeHandlers.ts +++ b/electron/main/Store/storeHandlers.ts @@ -251,6 +251,49 @@ export const registerStoreHandlers = ( chatHistoriesMap[vaultDir] = filteredChatHistories.reverse(); store.set(StoreKeys.ChatHistories, chatHistoriesMap); }); + + ipcMain.handle("get-current-open-files", () => { + return store.get(StoreKeys.OpenTabs) || []; + }); + + ipcMain.handle("set-current-open-files", (event, { action, tab }) => { + console.log(`Event: ${event}, Action: ${action}`); + + const addTab = (tab) => { + console.log(`Adding new tab. TabId: ${tab.id}`); + const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || []; + const existingTab = openTabs.findIndex((item) => item.id === tab.id); + + /* If tab is already open, do not do anything */ + console.log(`Existing tab:`, existingTab); + console.log(`Open tabs:`, openTabs); + if (existingTab !== -1) return; + + openTabs.push(tab); + store.set(StoreKeys.OpenTabs, openTabs); + + /* Notify the renderer process that a new tab has been added */ + event.sender.send("new-tab-added", tab); + }; + + const removeTab = (tabId: string) => {}; + + const updateTab = (tab: Tab) => {}; + + switch (action) { + case "add": + addTab(tab); + break; + case "remove": + removeTab(tab.id); + break; + case "update": + updateTab(tab); + break; + default: + throw new Error("Unsupported action type"); + } + }); }; export function getDefaultEmbeddingModelConfig( diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 95885be8..c9621a39 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -158,6 +158,8 @@ declare global { updateChatHistory: (chatHistory: ChatHistory) => void; getChatHistory: (chatID: string) => Promise; removeChatHistoryAtID: (chatID: string) => void; + getCurrentOpenFiles: () => Promise; + setCurrentOpenFiles: (filePath: string) => void; }; } } @@ -322,6 +324,12 @@ contextBridge.exposeInMainWorld("electronStore", { setDisplayMarkdown: (displayMarkdown: boolean) => { ipcRenderer.invoke("set-display-markdown", displayMarkdown); }, + getCurrentOpenFiles: () => { + return ipcRenderer.invoke("get-current-open-files"); + }, + setCurrentOpenFiles: (action, tab) => { + ipcRenderer.invoke("set-current-open-files", { action, tab }); + }, }); contextBridge.exposeInMainWorld("ipcRenderer", { diff --git a/package-lock.json b/package-lock.json index 6a77502f..80f92abd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,7 @@ "@emotion/styled": "^11.11.0", "@headlessui/react": "^1.7.19", "@huggingface/hub": "^0.12.0", -<<<<<<< HEAD "@lancedb/vectordb-win32-x64-msvc": "^0.5.0", -======= - "@lancedb/vectordb-darwin-x64": "^0.5.0", ->>>>>>> 3d91329ca52efb1eab8a55a9c61d1018de00f32c "@material-tailwind/react": "^2.1.5", "@mui/icons-material": "^5.15.15", "@mui/joy": "^5.0.0-beta.23", @@ -66,6 +62,7 @@ "tiptap-markdown": "^0.8.10", "turndown": "^7.1.2", "use-debounce": "^10.0.1", + "uuid": "^10.0.0", "vectordb": "0.4.10" }, "devDependencies": { @@ -1085,21 +1082,6 @@ "version": "0.3.1", "license": "MIT" }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "dev": true, @@ -2147,79 +2129,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lancedb/vectordb-darwin-arm64": { - "version": "0.4.10", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lancedb/vectordb-darwin-x64": { -<<<<<<< HEAD - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.4.10.tgz", - "integrity": "sha512-XbfR58OkQpAe0xMSTrwJh9ZjGSzG9EZ7zwO6HfYem8PxcLYAcC6eWRWoSG/T0uObyrPTcYYyvHsp0eNQWYBFAQ==", - "cpu": [ - "x64" - ], - "optional": true, -======= - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.5.0.tgz", - "integrity": "sha512-N+LG+JusTK1xgax6IUnmvZgbwtmi0mwpDC1Clmnj9yO2S8kDtBqJbeYzCjdgHB+ayx6E43wMsMuH+5Haprq/zA==", - "cpu": [ - "x64" - ], ->>>>>>> 3d91329ca52efb1eab8a55a9c61d1018de00f32c - "os": [ - "darwin" - ] - }, - "node_modules/@lancedb/vectordb-linux-arm64-gnu": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.4.10.tgz", - "integrity": "sha512-x40WKH9b+KxorRmKr9G7fv8p5mMj8QJQvRMA0v6v+nbZHr2FLlAZV+9mvhHOnm4AGIkPP5335cUgv6Qz6hgwkQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lancedb/vectordb-linux-x64-gnu": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.4.10.tgz", - "integrity": "sha512-CTGPpuzlqq2nVjUxI9gAJOT1oBANIovtIaFsOmBSnEAHgX7oeAxKy2b6L/kJzsgqSzvR5vfLwYcWFrr6ZmBxSA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@lancedb/vectordb-win32-x64-msvc": { -<<<<<<< HEAD "version": "0.5.0", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.5.0.tgz", "integrity": "sha512-3CVy488/rVhC8Mp1GdFfqyqx1t34dToWnqdw8XLfRN7oaeQie0sbA8Sk+AULfs0dQLK0TtJVPmyeOVMlJupGAQ==", "cpu": [ "x64" ], -======= - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.4.10.tgz", - "integrity": "sha512-Fd7r74coZyrKzkfXg4WthqOL+uKyJyPTia6imcrMNqKOlTGdKmHf02Qi2QxWZrFaabkRYo4Tpn5FeRJ3yYX8CA==", - "cpu": [ - "x64" - ], - "optional": true, ->>>>>>> 3d91329ca52efb1eab8a55a9c61d1018de00f32c "os": [ "win32" ] @@ -2579,6 +2495,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", @@ -2619,6 +2547,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", @@ -3896,7 +3836,8 @@ }, "node_modules/@types/plist": { "version": "3.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", "optional": true, "dependencies": { "@types/node": "*", @@ -4000,8 +3941,9 @@ "license": "MIT" }, "node_modules/@types/verror": { - "version": "1.10.9", - "license": "MIT", + "version": "1.10.10", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", + "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", "optional": true }, "node_modules/@types/yargs": { @@ -4707,7 +4649,8 @@ }, "node_modules/assert-plus": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "optional": true, "engines": { "node": ">=0.8" @@ -4715,7 +4658,8 @@ }, "node_modules/astral-regex": { "version": "2.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "optional": true, "engines": { "node": ">=8" @@ -5171,6 +5115,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "dev": true, @@ -5614,7 +5581,8 @@ }, "node_modules/cli-truncate": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "optional": true, "dependencies": { "slice-ansi": "^3.0.0", @@ -5892,7 +5860,8 @@ }, "node_modules/core-util-is": { "version": "1.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "optional": true }, "node_modules/cosmiconfig": { @@ -5911,35 +5880,13 @@ }, "node_modules/crc": { "version": "3.8.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", "optional": true, "dependencies": { "buffer": "^5.1.0" } }, - "node_modules/crc/node_modules/buffer": { - "version": "5.7.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/create-jest": { "version": "29.7.0", "dev": true, @@ -6437,7 +6384,8 @@ }, "node_modules/dmg-license": { "version": "1.0.11", - "license": "MIT", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", "optional": true, "os": [ "darwin" @@ -7766,10 +7714,11 @@ }, "node_modules/extsprintf": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", "engines": [ "node >=0.6.0" ], - "license": "MIT", "optional": true }, "node_modules/fast-deep-equal": { @@ -8125,17 +8074,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -8734,7 +8672,8 @@ }, "node_modules/iconv-corefoundation": { "version": "1.1.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", "optional": true, "os": [ "darwin" @@ -11400,6 +11339,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", @@ -11432,6 +11383,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/lazy-val": { "version": "1.0.5", "license": "MIT" @@ -12445,7 +12408,8 @@ }, "node_modules/node-addon-api": { "version": "1.7.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", "optional": true }, "node_modules/node-domexception": { @@ -15727,18 +15691,6 @@ "node": ">=16" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/plist": { "version": "3.1.0", "devOptional": true, @@ -15996,28 +15948,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/prebuild-install/node_modules/buffer": { - "version": "5.7.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/prebuild-install/node_modules/chownr": { "version": "1.1.4", "license": "ISC" @@ -17284,7 +17214,8 @@ }, "node_modules/slice-ansi": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "optional": true, "dependencies": { "ansi-styles": "^4.0.0", @@ -17297,7 +17228,8 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "optional": true, "dependencies": { "color-convert": "^2.0.1" @@ -17311,7 +17243,8 @@ }, "node_modules/slice-ansi/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "optional": true, "dependencies": { "color-name": "~1.1.4" @@ -17322,12 +17255,14 @@ }, "node_modules/slice-ansi/node_modules/color-name": { "version": "1.1.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "optional": true }, "node_modules/smart-buffer": { "version": "4.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "optional": true, "engines": { "node": ">= 6.0.0", @@ -18471,12 +18406,13 @@ "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" } @@ -18525,32 +18461,22 @@ "@lancedb/vectordb-win32-x64-msvc": "0.4.10" } }, -<<<<<<< HEAD "node_modules/vectordb/node_modules/@lancedb/vectordb-win32-x64-msvc": { "version": "0.4.10", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.4.10.tgz", "integrity": "sha512-Fd7r74coZyrKzkfXg4WthqOL+uKyJyPTia6imcrMNqKOlTGdKmHf02Qi2QxWZrFaabkRYo4Tpn5FeRJ3yYX8CA==", -======= - "node_modules/vectordb/node_modules/@lancedb/vectordb-darwin-x64": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.4.10.tgz", - "integrity": "sha512-XbfR58OkQpAe0xMSTrwJh9ZjGSzG9EZ7zwO6HfYem8PxcLYAcC6eWRWoSG/T0uObyrPTcYYyvHsp0eNQWYBFAQ==", ->>>>>>> 3d91329ca52efb1eab8a55a9c61d1018de00f32c "cpu": [ "x64" ], "optional": true, "os": [ -<<<<<<< HEAD "win32" -======= - "darwin" ->>>>>>> 3d91329ca52efb1eab8a55a9c61d1018de00f32c ] }, "node_modules/verror": { "version": "1.10.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", "optional": true, "dependencies": { "assert-plus": "^1.0.0", diff --git a/package.json b/package.json index e0b942d9..be035f73 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,7 @@ "@emotion/styled": "^11.11.0", "@headlessui/react": "^1.7.19", "@huggingface/hub": "^0.12.0", -<<<<<<< HEAD "@lancedb/vectordb-win32-x64-msvc": "^0.5.0", -======= - "@lancedb/vectordb-darwin-x64": "^0.5.0", ->>>>>>> 3d91329ca52efb1eab8a55a9c61d1018de00f32c "@material-tailwind/react": "^2.1.5", "@mui/icons-material": "^5.15.15", "@mui/joy": "^5.0.0-beta.23", @@ -83,6 +79,7 @@ "tiptap-markdown": "^0.8.10", "turndown": "^7.1.2", "use-debounce": "^10.0.1", + "uuid": "^10.0.0", "vectordb": "0.4.10" }, "devDependencies": { diff --git a/package.json~ b/package.json~ new file mode 100644 index 00000000..c43be851 --- /dev/null +++ b/package.json~ @@ -0,0 +1,146 @@ +{ + "name": "reor-project", + "version": "0.2.10", + "productName": "Reor", + "main": "dist-electron/main/index.js", + "description": "An AI note-taking app that runs models locally.", + "author": "Sam L'Huillier", + "license": "AGPL-3.0", + "private": false, + "debug": { + "env": { + "VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/" + } + }, + "scripts": { + "predev": "node scripts/downloadOllama.js", + "dev": "vite", + "prebuild": "node scripts/downloadOllama.js", + "build": "tsc && vite build && electron-builder", + "postbuild": "node scripts/notarize.js", + "preview": "vite preview", + "pree2e": "vite build --mode=test", + "e2e": "playwright test", + "test": "jest", + "lint": "eslint . --ext .ts,.tsx", + "lint:fix": "eslint . --ext .ts,.tsx --fix" + }, + "dependencies": { + "@aarkue/tiptap-math-extension": "^1.2.2", + "@anthropic-ai/sdk": "^0.21.1", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.0", + "@headlessui/react": "^1.7.19", + "@huggingface/hub": "^0.12.0", + "@lancedb/vectordb-win32-x64-msvc": "^0.5.0", + "@material-tailwind/react": "^2.1.5", + "@mui/icons-material": "^5.15.15", + "@mui/joy": "^5.0.0-beta.23", + "@mui/material": "^5.15.11", + "@radix-ui/colors": "^3.0.0", + "@tailwindcss/typography": "^0.5.10", + "@tiptap/extension-link": "^2.2.4", + "@tiptap/extension-task-item": "^2.2.4", + "@tiptap/extension-task-list": "^2.2.4", + "@tiptap/extension-text-style": "^2.4.0", + "@tiptap/pm": "^2.2.4", + "@tiptap/react": "^2.2.4", + "@tiptap/starter-kit": "^2.2.4", + "@xenova/transformers": "^2.8.0", + "apache-arrow": "^14.0.2", + "chokidar": "^3.5.3", + "cm6-theme-basic-dark": "^0.2.0", + "date-fns": "^3.3.1", + "electron-store": "^8.1.0", + "electron-updater": "^6.1.1", + "install": "^0.13.0", + "js-tiktoken": "^1.0.10", + "katex": "^0.16.10", + "langchain": "^0.1.5", + "lit": "^3.0.0", + "lodash.debounce": "^4.0.8", + "marked": "^12.0.1", + "npm": "^10.3.0", + "ollama": "^0.4.9", + "openai": "^4.20.0", + "posthog-js": "^1.130.2", + "prosemirror-utils": "^1.2.2", + "react-card-flip": "^1.2.2", + "react-icons": "^4.12.0", + "react-markdown": "^9.0.1", + "react-quill": "^2.0.0", + "react-rnd": "^10.4.1", + "react-switch": "^7.0.0", + "react-toastify": "^10.0.4", + "react-type-animation": "^3.2.0", + "react-window": "^1.8.10", + "rehype-raw": "^7.0.0", + "remove-markdown": "^0.5.0", + "tiptap-markdown": "^0.8.10", + "turndown": "^7.1.2", + "use-debounce": "^10.0.1", + "vectordb": "0.4.10" + }, + "devDependencies": { + "@electron/notarize": "^2.3.0", + "@playwright/test": "^1.37.1", + "@types/jest": "^29.5.11", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@types/react-window": "^1.8.8", + "@types/remove-markdown": "^0.3.4", + "@types/tmp": "^0.2.6", + "@types/turndown": "^5.0.4", + "@typescript-eslint/eslint-plugin": "^6.19.0", + "@typescript-eslint/parser": "^6.19.0", + "@vitejs/plugin-react": "^4.0.4", + "autoprefixer": "^10.4.16", + "electron": "28.2.1", + "electron-builder": "^24.6.3", + "eslint": "^8.56.0", + "eslint-import-resolver-alias": "^1.1.2", + "eslint-import-resolver-node": "^0.3.9", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-unused-imports": "^4.0.0", + "jest": "^29.7.0", + "postcss": "^8.4.31", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.71.1", + "tailwindcss": "^3.3.5", + "tmp": "^0.2.1", + "ts-jest": "^29.1.2", + "typescript": "^5.1.6", + "vite": "^4.4.9", + "vite-plugin-electron": "^0.13.0-beta.3", + "vite-plugin-electron-renderer": "^0.14.5" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + "globals": { + "ts-jest": { + "tsconfig": "tsconfig.json" + } + }, + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ] + } +} diff --git a/src/components/Editor/HighlightExtension.tsx b/src/components/Editor/HighlightExtension.tsx index f9e59296..084d3378 100644 --- a/src/components/Editor/HighlightExtension.tsx +++ b/src/components/Editor/HighlightExtension.tsx @@ -27,9 +27,6 @@ const HighlightExtension = (setHighlightData: (data: HighlightData) => void) => const buttonTop = top - 35; // Adjust the vertical offset as needed const buttonLeft = (left + right) / 2 - 190; // Position the button horizontally centered - console.log( - `buttonTop: ${buttonTop}, buttonLeft: ${buttonLeft}` - ); setHighlightData({ text: highlightedText, position: { top: buttonTop, left: buttonLeft }, diff --git a/src/components/Editor/LLMQueryTab.tsx b/src/components/Editor/LLMQueryTab.tsx new file mode 100644 index 00000000..1a5de300 --- /dev/null +++ b/src/components/Editor/LLMQueryTab.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Extension } from "@tiptap/react"; + +const OpenQueryTab = (setShowQueryBox) => + Extension.create({ + addKeyboardShortcuts() { + return { + "Mod-Shift-l": () => { + console.log(`Toggling query box`); + setShowQueryBox((prev) => !prev); + }, + }; + }, + }); + +export default OpenQueryTab; diff --git a/src/components/Editor/QueryInput.tsx b/src/components/Editor/QueryInput.tsx new file mode 100644 index 00000000..cf18f99e --- /dev/null +++ b/src/components/Editor/QueryInput.tsx @@ -0,0 +1,195 @@ +import React, { useState } from "react"; + +/* + * Contains the options that users can query on + */ +enum QueryOptions { + summarize = "summarize", + details = "details", + analyze = "analyze", + format = "format", +} + +/** + * Represents a query with a specified action and its associated arguments + */ +interface Query { + /** + * The query operation to be performed. + */ + options: QueryOptions; + + /** + * Query a remote page + */ + remote: boolean; + + /** + * The arguments associated with the query operation. + * Args is constructed in the following way: + * 1) :summarize [URL] + * 2) :ask [URL] "What is the main argument of this article" "How can we .." ... + * 3) :analyze [URL] "What does page 4 explain?" + * + * In this case, everything after the cmd option is stored at an index in args starting at 0. + * If remote is true, then URL is stored at args[0] + */ + args: string[]; +} + +/** + * Regex for getting content between quotes + */ +const quotePattern = /^"(.+)"$/; + +const QueryInput = ({ setShowQueryBox }) => { + const [input, setInput] = useState(":"); + const [query, setQuery] = useState(null); + const [suggestions, setSuggestions] = useState([]); + const [suggestChoice, setSuggestChoice] = useState(0); + + const handleKeyDown = (event) => { + const length = Object.values(QueryOptions).length; + + if (event.key === " ") { + const command = input.slice(1).trim().split(" ")[0]; + if (!QueryOptions[command]) { + event.preventDefault(); + } + } else if (event.key === "Enter") { + // Suggestion windows is open + if (suggestions.length > 0) { + const newInput = ":" + suggestions[suggestChoice] + " "; + setInput(newInput); + updateSuggestions(newInput); + } else { + console.log("Did not select a choice!"); + /* Set up new Query Object*/ + parseInput(); + } + } else if (event.key === "Escape") { + /* Close query box */ + setShowQueryBox(false); + } else if (event.key === "ArrowUp") { + /* Select new choice for suggestion */ + setSuggestChoice((suggestChoice - 1 + length) % length); + } else if (event.key === "ArrowDown") { + setSuggestChoice((suggestChoice + 1) % length); + } + }; + + const handleChange = (event) => { + const value = event.target.value; + // Ensure the input always starts with ':' + if (value.startsWith(":")) { + setInput(value); + updateSuggestions(value); + } + }; + + /** + * Creates options that exist in the enum that the user may be typing. + */ + const updateSuggestions = (currentInput) => { + const commandStart = currentInput.slice(1); + const filteredSuggestions = Object.values(QueryOptions).filter((option) => + option.startsWith(commandStart) + ); + setSuggestions(filteredSuggestions); + }; + + const parseInput = () => { + // Extract command + const commandMatch = input.match(/^:([a-z]+)\s+(.*)$/i); + if (!commandMatch) { + console.error("No valid command found."); + return; + } + const command = commandMatch[1]; + const content = commandMatch[2]; + switch (command) { + case "summarize": + handleSummarizeCommand(content); + break; + case "format": + handleFormatCommand(content); + break; + default: + break; + } + }; + + /** + * Constructs the Query object when summarize command is invoked + */ + const handleSummarizeCommand = (content: string) => { + // Regex to match exactly one URL enclosed in square brackets + const summarizePattern = /^\[\s*https?:\/\/[^\s\]]+\s*\]$/i; + console.log(`content: ${content}`); + if (!summarizePattern.test(content.trim())) { + console.error( + "Invalid argument for summarize command. Expected a URL within square brackets." + ); + return; + } + + // If valid, extract URL and build the query object + const url = content.slice(1, -1); // Remove the square brackets + const newQuery: Query = { + options: QueryOptions.summarize, + remote: true, + args: [url], + }; + + setQuery(newQuery); + console.log("Summarize command processed:", newQuery); + }; + + /** + * Constructs the Query object when format command is invoked + */ + const handleFormatCommand = (content: string) => { + const match = content.match(quotePattern); + if (match) { + const args = match[1]; + const newQuery: Query = { + options: QueryOptions.format, + remote: false, + args: [args], + }; + + setQuery(newQuery); + console.log(`Format command processed:`, newQuery); + } + }; + + return ( +
+ {suggestions.length > 0 && ( +
    + {suggestions.map((suggestion, index) => ( +
  • + {suggestion} +
  • + ))} +
+ )} + +
+ ); +}; + +export default QueryInput; diff --git a/src/components/File/FileSideBar/FileHistoryBar.tsx b/src/components/File/FileSideBar/FileHistoryBar.tsx index 1bc7e30f..ff53459f 100644 --- a/src/components/File/FileSideBar/FileHistoryBar.tsx +++ b/src/components/File/FileSideBar/FileHistoryBar.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; import posthog from "posthog-js"; import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; @@ -36,12 +37,40 @@ const FileHistoryNavigator: React.FC = ({ }, [currentIndex]); const handleFileSelect = (path: string) => { + const newTab = createTabObjectFromPath(path); + console.log(`history: ${history}`); const updatedHistory = [ - ...history.filter((val) => val !== path).slice(0, currentIndex + 1), - path, + ...history + .filter((tab) => tab.filePath != path) + .slice(0, currentIndex + 1), + newTab, ]; setHistory(updatedHistory); setCurrentIndex(updatedHistory.length - 1); + syncTabsWithBackend(newTab); + }; + + const createTabObjectFromPath = (path) => { + return { + id: uuidv4(), + filePath: path, + title: extractFileName(path), + timeOpened: new Date(), + isDirty: false, + lastAccessed: new Date(), + }; + }; + + /* IPC Communication for Tab updates */ + const syncTabsWithBackend = async (tab) => { + /* Deals with already open files */ + console.log(`Tab:`, tab); + await window.electronStore.setCurrentOpenFiles("add", tab); + }; + + 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 }; const canGoBack = currentIndex > 0; diff --git a/src/components/File/hooks/use-file-by-filepath.ts b/src/components/File/hooks/use-file-by-filepath.ts index 79a53fa8..b6511882 100644 --- a/src/components/File/hooks/use-file-by-filepath.ts +++ b/src/components/File/hooks/use-file-by-filepath.ts @@ -20,6 +20,7 @@ import HighlightExtension, { } from "@/components/Editor/HighlightExtension"; import { RichTextLink } from "@/components/Editor/RichTextLink"; import SearchAndReplace from "@/components/Editor/SearchAndReplace"; +import OpenQueryTab from "@/components/Editor/LLMQueryTab"; import { getInvalidCharacterInFilePath, removeFileExtension, @@ -39,7 +40,7 @@ export const useFileByFilepath = () => { useState(false); const [noteToBeRenamed, setNoteToBeRenamed] = useState(""); const [fileDirToBeRenamed, setFileDirToBeRenamed] = useState(""); - const [navigationHistory, setNavigationHistory] = useState([]); + const [navigationHistory, setNavigationHistory] = useState([]); const [currentlyChangingFilePath, setCurrentlyChangingFilePath] = useState(false); const [highlightData, setHighlightData] = useState({ @@ -47,6 +48,7 @@ export const useFileByFilepath = () => { position: null, }); const [displayMarkdown, setDisplayMarkdown] = useState(false); + const [showQueryBox, setShowQueryBox] = useState(false); const setFileNodeToBeRenamed = async (filePath: string) => { const isDirectory = await window.files.isDirectory(filePath); @@ -159,9 +161,9 @@ export const useFileByFilepath = () => { }), TextStyle, SearchAndReplace.configure({ - searchResultClass: "bg-yellow-400", - caseSensitive: false, - disableRegex: false, + searchResultClass: "bg-yellow-400", + caseSensitive: false, + disableRegex: false, }), Markdown.configure({ html: true, // Allow HTML input/output @@ -185,6 +187,7 @@ export const useFileByFilepath = () => { openRelativePathRef, handleSuggestionsStateWithEventCapture ), + OpenQueryTab(setShowQueryBox), ], }); @@ -338,6 +341,8 @@ export const useFileByFilepath = () => { filePath: currentlyOpenedFilePath, saveCurrentlyOpenedFile, editor, + showQueryBox, + setShowQueryBox, navigationHistory, setNavigationHistory, openFileByPath, diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index c85a565b..b2083fa7 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -12,6 +12,7 @@ import IconsSidebar from "./Sidebars/IconsSidebar"; import SidebarManager from "./Sidebars/MainSidebar"; import SidebarComponent from "./Similarity/SimilarFilesSidebar"; import { SearchInput } from "./SearchComponent"; +import QueryInput from "./Editor/QueryInput"; import TitleBar from "./TitleBar"; interface FileEditorContainerProps {} @@ -25,6 +26,8 @@ const FileEditorContainer: React.FC = () => { const { filePath, editor, + showQueryBox, + setShowQueryBox, openFileByPath, openRelativePath, saveCurrentlyOpenedFile, @@ -251,10 +254,17 @@ const FileEditorContainer: React.FC = () => { className="fixed top-8 right-64 mt-4 mr-14 z-50 border-none rounded-md p-2 bg-transparent bg-dark-gray-c-ten text-white" /> )} - +
+ + {showQueryBox && ( +
+ +
+ )} +
{suggestionsState && ( = ({ if (!highlightData.position) { setShowArrow(false); } - console.log(`pos: ${JSON.stringify(highlightData.position)}`); }, [highlightData.position]); if (!highlightData.position) { diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index c9d43551..451ab690 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -33,6 +33,16 @@ const TitleBar: React.FC = ({ fetchPlatform(); }, []); + useEffect(() => { + const fetchHistory = async () => { + const response = await window.electronStore.getCurrentOpenFiles(); + setHistory(response); + console.log(`Fetching stored history: ${JSON.stringify(history)}`); + }; + + fetchHistory(); + }, []); + return (
Date: Wed, 19 Jun 2024 10:13:07 -0500 Subject: [PATCH 02/40] Added tab support with highlighting --- electron/main/Store/storeHandlers.ts | 4 +- package-lock.json | 260 ++++++++++-------- package.json | 6 +- package.json~ | 146 ---------- src/components/DraggableTabs.tsx | 66 +++++ .../File/FileSideBar/FileHistoryBar.tsx | 40 +-- .../File/hooks/use-file-by-filepath.ts | 5 +- src/components/FileEditorContainer.tsx | 4 + src/components/TitleBar.tsx | 51 +++- 9 files changed, 286 insertions(+), 296 deletions(-) delete mode 100644 package.json~ create mode 100644 src/components/DraggableTabs.tsx diff --git a/electron/main/Store/storeHandlers.ts b/electron/main/Store/storeHandlers.ts index f6e31912..cb76f85b 100644 --- a/electron/main/Store/storeHandlers.ts +++ b/electron/main/Store/storeHandlers.ts @@ -262,7 +262,9 @@ export const registerStoreHandlers = ( const addTab = (tab) => { console.log(`Adding new tab. TabId: ${tab.id}`); const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || []; - const existingTab = openTabs.findIndex((item) => item.id === tab.id); + const existingTab = openTabs.findIndex( + (item) => item.filePath === tab.filePath + ); /* If tab is already open, do not do anything */ console.log(`Existing tab:`, existingTab); diff --git a/package-lock.json b/package-lock.json index 80f92abd..7cc4d216 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,6 @@ "tiptap-markdown": "^0.8.10", "turndown": "^7.1.2", "use-debounce": "^10.0.1", - "uuid": "^10.0.0", "vectordb": "0.4.10" }, "devDependencies": { @@ -1082,6 +1081,21 @@ "version": "0.3.1", "license": "MIT" }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "dev": true, @@ -2129,6 +2143,53 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lancedb/vectordb-darwin-arm64": { + "version": "0.4.10", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lancedb/vectordb-darwin-x64": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.4.10.tgz", + "integrity": "sha512-XbfR58OkQpAe0xMSTrwJh9ZjGSzG9EZ7zwO6HfYem8PxcLYAcC6eWRWoSG/T0uObyrPTcYYyvHsp0eNQWYBFAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lancedb/vectordb-linux-arm64-gnu": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.4.10.tgz", + "integrity": "sha512-x40WKH9b+KxorRmKr9G7fv8p5mMj8QJQvRMA0v6v+nbZHr2FLlAZV+9mvhHOnm4AGIkPP5335cUgv6Qz6hgwkQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lancedb/vectordb-linux-x64-gnu": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.4.10.tgz", + "integrity": "sha512-CTGPpuzlqq2nVjUxI9gAJOT1oBANIovtIaFsOmBSnEAHgX7oeAxKy2b6L/kJzsgqSzvR5vfLwYcWFrr6ZmBxSA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@lancedb/vectordb-win32-x64-msvc": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.5.0.tgz", @@ -2495,18 +2556,6 @@ } } }, - "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", @@ -2547,18 +2596,6 @@ "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", @@ -3836,8 +3873,7 @@ }, "node_modules/@types/plist": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*", @@ -3941,9 +3977,8 @@ "license": "MIT" }, "node_modules/@types/verror": { - "version": "1.10.10", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", - "integrity": "sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==", + "version": "1.10.9", + "license": "MIT", "optional": true }, "node_modules/@types/yargs": { @@ -4649,8 +4684,7 @@ }, "node_modules/assert-plus": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", "optional": true, "engines": { "node": ">=0.8" @@ -4658,8 +4692,7 @@ }, "node_modules/astral-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "license": "MIT", "optional": true, "engines": { "node": ">=8" @@ -5115,29 +5148,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "dev": true, @@ -5581,8 +5591,7 @@ }, "node_modules/cli-truncate": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "license": "MIT", "optional": true, "dependencies": { "slice-ansi": "^3.0.0", @@ -5860,8 +5869,7 @@ }, "node_modules/core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT", "optional": true }, "node_modules/cosmiconfig": { @@ -5880,13 +5888,35 @@ }, "node_modules/crc": { "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "license": "MIT", "optional": true, "dependencies": { "buffer": "^5.1.0" } }, + "node_modules/crc/node_modules/buffer": { + "version": "5.7.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/create-jest": { "version": "29.7.0", "dev": true, @@ -6384,8 +6414,7 @@ }, "node_modules/dmg-license": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "license": "MIT", "optional": true, "os": [ "darwin" @@ -7714,11 +7743,10 @@ }, "node_modules/extsprintf": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", "engines": [ "node >=0.6.0" ], + "license": "MIT", "optional": true }, "node_modules/fast-deep-equal": { @@ -8074,6 +8102,17 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -8672,8 +8711,7 @@ }, "node_modules/iconv-corefoundation": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "license": "MIT", "optional": true, "os": [ "darwin" @@ -11339,18 +11377,6 @@ "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", @@ -11383,18 +11409,6 @@ "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/lazy-val": { "version": "1.0.5", "license": "MIT" @@ -12408,8 +12422,7 @@ }, "node_modules/node-addon-api": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "license": "MIT", "optional": true }, "node_modules/node-domexception": { @@ -15691,6 +15704,18 @@ "node": ">=16" } }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "devOptional": true, @@ -15948,6 +15973,28 @@ "readable-stream": "^3.4.0" } }, + "node_modules/prebuild-install/node_modules/buffer": { + "version": "5.7.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/prebuild-install/node_modules/chownr": { "version": "1.1.4", "license": "ISC" @@ -17214,8 +17261,7 @@ }, "node_modules/slice-ansi": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "license": "MIT", "optional": true, "dependencies": { "ansi-styles": "^4.0.0", @@ -17228,8 +17274,7 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "optional": true, "dependencies": { "color-convert": "^2.0.1" @@ -17243,8 +17288,7 @@ }, "node_modules/slice-ansi/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "optional": true, "dependencies": { "color-name": "~1.1.4" @@ -17255,14 +17299,12 @@ }, "node_modules/slice-ansi/node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", "optional": true }, "node_modules/smart-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", "optional": true, "engines": { "node": ">= 6.0.0", @@ -18406,13 +18448,12 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "9.0.1", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -18475,8 +18516,7 @@ }, "node_modules/verror": { "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "license": "MIT", "optional": true, "dependencies": { "assert-plus": "^1.0.0", diff --git a/package.json b/package.json index be035f73..872db6e3 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@emotion/styled": "^11.11.0", "@headlessui/react": "^1.7.19", "@huggingface/hub": "^0.12.0", - "@lancedb/vectordb-win32-x64-msvc": "^0.5.0", "@material-tailwind/react": "^2.1.5", "@mui/icons-material": "^5.15.15", "@mui/joy": "^5.0.0-beta.23", @@ -119,7 +118,10 @@ "vite-plugin-electron-renderer": "^0.14.5" }, "optionalDependencies": { - "dmg-license": "^1.0.11" + "dmg-license": "^1.0.11", + "@lancedb/vectordb-win32-x64-msvc": "^0.5.0", + "@lancedb/vectordb-darwin-x64": "^0.5.0", + "@lancedb/vectordb-linux-x64-msvc": "0.5.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" diff --git a/package.json~ b/package.json~ deleted file mode 100644 index c43be851..00000000 --- a/package.json~ +++ /dev/null @@ -1,146 +0,0 @@ -{ - "name": "reor-project", - "version": "0.2.10", - "productName": "Reor", - "main": "dist-electron/main/index.js", - "description": "An AI note-taking app that runs models locally.", - "author": "Sam L'Huillier", - "license": "AGPL-3.0", - "private": false, - "debug": { - "env": { - "VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/" - } - }, - "scripts": { - "predev": "node scripts/downloadOllama.js", - "dev": "vite", - "prebuild": "node scripts/downloadOllama.js", - "build": "tsc && vite build && electron-builder", - "postbuild": "node scripts/notarize.js", - "preview": "vite preview", - "pree2e": "vite build --mode=test", - "e2e": "playwright test", - "test": "jest", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "eslint . --ext .ts,.tsx --fix" - }, - "dependencies": { - "@aarkue/tiptap-math-extension": "^1.2.2", - "@anthropic-ai/sdk": "^0.21.1", - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.0", - "@headlessui/react": "^1.7.19", - "@huggingface/hub": "^0.12.0", - "@lancedb/vectordb-win32-x64-msvc": "^0.5.0", - "@material-tailwind/react": "^2.1.5", - "@mui/icons-material": "^5.15.15", - "@mui/joy": "^5.0.0-beta.23", - "@mui/material": "^5.15.11", - "@radix-ui/colors": "^3.0.0", - "@tailwindcss/typography": "^0.5.10", - "@tiptap/extension-link": "^2.2.4", - "@tiptap/extension-task-item": "^2.2.4", - "@tiptap/extension-task-list": "^2.2.4", - "@tiptap/extension-text-style": "^2.4.0", - "@tiptap/pm": "^2.2.4", - "@tiptap/react": "^2.2.4", - "@tiptap/starter-kit": "^2.2.4", - "@xenova/transformers": "^2.8.0", - "apache-arrow": "^14.0.2", - "chokidar": "^3.5.3", - "cm6-theme-basic-dark": "^0.2.0", - "date-fns": "^3.3.1", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.1", - "install": "^0.13.0", - "js-tiktoken": "^1.0.10", - "katex": "^0.16.10", - "langchain": "^0.1.5", - "lit": "^3.0.0", - "lodash.debounce": "^4.0.8", - "marked": "^12.0.1", - "npm": "^10.3.0", - "ollama": "^0.4.9", - "openai": "^4.20.0", - "posthog-js": "^1.130.2", - "prosemirror-utils": "^1.2.2", - "react-card-flip": "^1.2.2", - "react-icons": "^4.12.0", - "react-markdown": "^9.0.1", - "react-quill": "^2.0.0", - "react-rnd": "^10.4.1", - "react-switch": "^7.0.0", - "react-toastify": "^10.0.4", - "react-type-animation": "^3.2.0", - "react-window": "^1.8.10", - "rehype-raw": "^7.0.0", - "remove-markdown": "^0.5.0", - "tiptap-markdown": "^0.8.10", - "turndown": "^7.1.2", - "use-debounce": "^10.0.1", - "vectordb": "0.4.10" - }, - "devDependencies": { - "@electron/notarize": "^2.3.0", - "@playwright/test": "^1.37.1", - "@types/jest": "^29.5.11", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "@types/react-window": "^1.8.8", - "@types/remove-markdown": "^0.3.4", - "@types/tmp": "^0.2.6", - "@types/turndown": "^5.0.4", - "@typescript-eslint/eslint-plugin": "^6.19.0", - "@typescript-eslint/parser": "^6.19.0", - "@vitejs/plugin-react": "^4.0.4", - "autoprefixer": "^10.4.16", - "electron": "28.2.1", - "electron-builder": "^24.6.3", - "eslint": "^8.56.0", - "eslint-import-resolver-alias": "^1.1.2", - "eslint-import-resolver-node": "^0.3.9", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-unused-imports": "^4.0.0", - "jest": "^29.7.0", - "postcss": "^8.4.31", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "sass": "^1.71.1", - "tailwindcss": "^3.3.5", - "tmp": "^0.2.1", - "ts-jest": "^29.1.2", - "typescript": "^5.1.6", - "vite": "^4.4.9", - "vite-plugin-electron": "^0.13.0-beta.3", - "vite-plugin-electron-renderer": "^0.14.5" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, - "globals": { - "ts-jest": { - "tsconfig": "tsconfig.json" - } - }, - "moduleFileExtensions": [ - "ts", - "tsx", - "js", - "jsx", - "json", - "node" - ] - } -} diff --git a/src/components/DraggableTabs.tsx b/src/components/DraggableTabs.tsx new file mode 100644 index 00000000..5b5e7e05 --- /dev/null +++ b/src/components/DraggableTabs.tsx @@ -0,0 +1,66 @@ +import React from "react"; + +interface DraggableTabsProps { + openTabs: Tab[]; + setOpenTabs: (openTabs: Tab[]) => void; + onTabSelect: (path: string) => void; + onTabClose: (tabId) => void; + currentFilePath: (path: string) => void; +} + +const DraggableTabs: React.FC = ({ + openTabs, + setOpenTabs, + onTabSelect, + onTabClose, + currentFilePath, +}) => { + const onDragStart = (event, tabId) => { + event.dataTransfer.setData("tabId", tabId); + }; + + const onDrop = (event) => { + const draggedTabId = event.dataTransfer.getData("tabId"); + const targetTabId = event.target.getAttribute("data-tabid"); + const newTabs = [...openTabs]; + const draggedTab = newTabs.find((tab) => tab.id === draggedTabId); + const targetIndex = newTabs.findIndex((tab) => tab.id === targetTabId); + + newTabs.splice(newTabs.indexOf(draggedTab), 1); // Remove the dragged tab + newTabs.splice(targetIndex, 0, draggedTab); // Insert at the new index + + setOpenTabs(newTabs); + }; + + const onDragOver = (event) => { + event.preventDefault(); + }; + + return ( +
+ {openTabs.map((tab) => ( +
onDragStart(event, tab.id)} + onDrop={onDrop} + onDragOver={onDragOver} + className={`mx-2 py-1 bg-transparent text-white cursor-pointer ${ + currentFilePath === tab.filePath + ? "border-solid border-0 border-b-2 border-yellow-300" + : "" + }`} + onClick={() => onTabSelect(tab.filePath)} + > + {tab.title} + onTabClose(tab.id)}> + × + +
+ ))} +
+ ); +}; + +export { DraggableTabs }; diff --git a/src/components/File/FileSideBar/FileHistoryBar.tsx b/src/components/File/FileSideBar/FileHistoryBar.tsx index ff53459f..155118ea 100644 --- a/src/components/File/FileSideBar/FileHistoryBar.tsx +++ b/src/components/File/FileSideBar/FileHistoryBar.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useRef, useState } from "react"; -import { v4 as uuidv4 } from "uuid"; import posthog from "posthog-js"; import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; @@ -12,6 +11,7 @@ interface FileHistoryNavigatorProps { setHistory: (string: string[]) => void; onFileSelect: (path: string) => void; currentPath: string; + syncTabsWithBackend: (string: path) => void; } const FileHistoryNavigator: React.FC = ({ @@ -19,6 +19,7 @@ const FileHistoryNavigator: React.FC = ({ setHistory, onFileSelect, currentPath, + syncTabsWithBackend, }) => { const [showMenu, setShowMenu] = useState(""); const [currentIndex, setCurrentIndex] = useState(-1); @@ -32,45 +33,16 @@ const FileHistoryNavigator: React.FC = ({ handleFileSelect(currentPath); }, [currentPath]); - useEffect(() => { - console.log(`currentIndex: ${currentIndex}`, { history }); - }, [currentIndex]); + useEffect(() => {}, [currentIndex]); const handleFileSelect = (path: string) => { - const newTab = createTabObjectFromPath(path); - console.log(`history: ${history}`); const updatedHistory = [ - ...history - .filter((tab) => tab.filePath != path) - .slice(0, currentIndex + 1), - newTab, + ...history.filter((val) => val !== path).slice(0, currentIndex + 1), + path, ]; setHistory(updatedHistory); setCurrentIndex(updatedHistory.length - 1); - syncTabsWithBackend(newTab); - }; - - const createTabObjectFromPath = (path) => { - return { - id: uuidv4(), - filePath: path, - title: extractFileName(path), - timeOpened: new Date(), - isDirty: false, - lastAccessed: new Date(), - }; - }; - - /* IPC Communication for Tab updates */ - const syncTabsWithBackend = async (tab) => { - /* Deals with already open files */ - console.log(`Tab:`, tab); - await window.electronStore.setCurrentOpenFiles("add", tab); - }; - - 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 + syncTabsWithBackend(path); }; const canGoBack = currentIndex > 0; diff --git a/src/components/File/hooks/use-file-by-filepath.ts b/src/components/File/hooks/use-file-by-filepath.ts index b6511882..428dc435 100644 --- a/src/components/File/hooks/use-file-by-filepath.ts +++ b/src/components/File/hooks/use-file-by-filepath.ts @@ -40,7 +40,8 @@ export const useFileByFilepath = () => { useState(false); const [noteToBeRenamed, setNoteToBeRenamed] = useState(""); const [fileDirToBeRenamed, setFileDirToBeRenamed] = useState(""); - const [navigationHistory, setNavigationHistory] = useState([]); + const [navigationHistory, setNavigationHistory] = useState([]); + const [openTabs, setOpenTabs] = useState([]); const [currentlyChangingFilePath, setCurrentlyChangingFilePath] = useState(false); const [highlightData, setHighlightData] = useState({ @@ -343,6 +344,8 @@ export const useFileByFilepath = () => { editor, showQueryBox, setShowQueryBox, + openTabs, + setOpenTabs, navigationHistory, setNavigationHistory, openFileByPath, diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index b2083fa7..f8138f6e 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -28,6 +28,8 @@ const FileEditorContainer: React.FC = () => { editor, showQueryBox, setShowQueryBox, + openTabs, + setOpenTabs, openFileByPath, openRelativePath, saveCurrentlyOpenedFile, @@ -187,6 +189,8 @@ const FileEditorContainer: React.FC = () => { void; currentFilePath: string | null; @@ -12,6 +15,8 @@ interface TitleBarProps { toggleSimilarFiles: () => void; history: string[]; setHistory: (string: string[]) => void; + openTabs: Tab[]; + setOpenTabs: (openTabs: Tab[]) => void; } const TitleBar: React.FC = ({ @@ -21,6 +26,8 @@ const TitleBar: React.FC = ({ toggleSimilarFiles, history, setHistory, + openTabs, + setOpenTabs, }) => { const [platform, setPlatform] = useState(""); @@ -36,13 +43,45 @@ const TitleBar: React.FC = ({ useEffect(() => { const fetchHistory = async () => { const response = await window.electronStore.getCurrentOpenFiles(); - setHistory(response); - console.log(`Fetching stored history: ${JSON.stringify(history)}`); + setOpenTabs(response); + console.log(`Fetching stored history: ${JSON.stringify(openTabs)}`); }; fetchHistory(); }, []); + const createTabObjectFromPath = (path) => { + return { + id: uuidv4(), + filePath: path, + title: extractFileName(path), + timeOpened: new Date(), + isDirty: false, + lastAccessed: new Date(), + }; + }; + + /* IPC Communication for Tab updates */ + const syncTabsWithBackend = async (path: string) => { + /* Deals with already open files */ + const tab = createTabObjectFromPath(path); + await window.electronStore.setCurrentOpenFiles("add", tab); + }; + + 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 + }; + + const handleTabSelect = (path: string) => { + console.log("Tab Selected:", path); + onFileSelect(path); + }; + + const handleTabClose = (tabId) => { + setOpenTabs((prevTabs) => prevTabs.filter((tab) => tab.id !== tabId)); + }; + return (
= ({ setHistory={setHistory} onFileSelect={onFileSelect} currentPath={currentFilePath || ""} + syncTabsWithBackend={syncTabsWithBackend} />
+
Date: Fri, 21 Jun 2024 21:29:30 -0500 Subject: [PATCH 03/40] Making LLMSession more Generic --- electron/main/Store/storeHandlers.ts | 33 +++--- electron/main/llm/llmSessionHandlers.ts | 74 ++++++------ electron/preload/index.ts | 14 ++- src/components/DraggableTabs.tsx | 50 +++++--- src/components/Editor/LLMQueryTab.tsx | 1 - src/components/Editor/QueryInput.tsx | 56 ++++++--- .../File/FileSideBar/FileHistoryBar.tsx | 3 - src/components/File/FileSideBar/FileItem.tsx | 5 +- src/components/File/FileSideBar/index.tsx | 1 - src/components/File/PreviewFile.tsx | 75 ++++++++++++ src/components/FileEditorContainer.tsx | 109 +++++++++++++++++- src/components/Sidebars/MainSidebar.tsx | 1 - src/components/TitleBar.tsx | 57 +-------- 13 files changed, 319 insertions(+), 160 deletions(-) create mode 100644 src/components/File/PreviewFile.tsx diff --git a/electron/main/Store/storeHandlers.ts b/electron/main/Store/storeHandlers.ts index cb76f85b..d2d97714 100644 --- a/electron/main/Store/storeHandlers.ts +++ b/electron/main/Store/storeHandlers.ts @@ -256,41 +256,44 @@ export const registerStoreHandlers = ( return store.get(StoreKeys.OpenTabs) || []; }); - ipcMain.handle("set-current-open-files", (event, { action, tab }) => { - console.log(`Event: ${event}, Action: ${action}`); + ipcMain.handle("set-current-open-files", (event, { action, args }) => { + const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || []; - const addTab = (tab) => { - console.log(`Adding new tab. TabId: ${tab.id}`); - const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || []; + const addTab = ({ tab }) => { const existingTab = openTabs.findIndex( (item) => item.filePath === tab.filePath ); /* If tab is already open, do not do anything */ - console.log(`Existing tab:`, existingTab); - console.log(`Open tabs:`, openTabs); if (existingTab !== -1) return; openTabs.push(tab); store.set(StoreKeys.OpenTabs, openTabs); - - /* Notify the renderer process that a new tab has been added */ - event.sender.send("new-tab-added", tab); }; - const removeTab = (tabId: string) => {}; + const removeTab = ({ tabId }) => { + const updatedTabs = openTabs.filter((tab) => tab.id !== tabId); + store.set(StoreKeys.OpenTabs, updatedTabs); + }; - const updateTab = (tab: Tab) => {}; + const updateTab = ({ draggedIndex, targetIndex }) => { + // Swap dragged and target + [openTabs[draggedIndex], openTabs[targetIndex]] = [ + openTabs[targetIndex], + openTabs[draggedIndex], + ]; + store.set(StoreKeys.OpenTabs, openTabs); + }; switch (action) { case "add": - addTab(tab); + addTab(args); break; case "remove": - removeTab(tab.id); + removeTab(args); break; case "update": - updateTab(tab); + updateTab(args); break; default: throw new Error("Unsupported action type"); diff --git a/electron/main/llm/llmSessionHandlers.ts b/electron/main/llm/llmSessionHandlers.ts index 8555e097..807ff35f 100644 --- a/electron/main/llm/llmSessionHandlers.ts +++ b/electron/main/llm/llmSessionHandlers.ts @@ -4,7 +4,6 @@ import Store from "electron-store"; import { ProgressResponse } from "ollama"; import { ChatCompletionChunk } from "openai/resources/chat/completions"; - import { LLMConfig, StoreKeys, StoreSchema } from "../Store/storeConfig"; import { @@ -23,6 +22,7 @@ import { OpenAIModelSessionService } from "./models/OpenAI"; import { LLMSessionService } from "./Types"; import { ChatHistory } from "@/components/Chat/Chat"; +import { Query } from "@/components/Editor/QueryInput"; enum LLMType { OpenAI = "openai", @@ -46,41 +46,45 @@ export const registerLLMSessionHandlers = (store: Store) => { llmName: string, llmConfig: LLMConfig, isJSONMode: boolean, - chatHistory: ChatHistory + // chatHistory: ChatHistory | Query, + request: ChatHistories | Query, + requestType: boolean ): Promise => { - const handleOpenAIChunk = (chunk: ChatCompletionChunk) => { - event.sender.send("openAITokenStream", chatHistory.id, chunk); - }; - - const handleAnthropicChunk = (chunk: MessageStreamEvent) => { - event.sender.send("anthropicTokenStream", chatHistory.id, chunk); - }; - - switch (llmConfig.type) { - case LLMType.OpenAI: - await openAISession.streamingResponse( - llmName, - llmConfig, - isJSONMode, - chatHistory.displayableChatHistory, - handleOpenAIChunk, - store.get(StoreKeys.LLMGenerationParameters) - ); - break; - case LLMType.Anthropic: - await anthropicSession.streamingResponse( - llmName, - llmConfig, - isJSONMode, - chatHistory.displayableChatHistory, - handleAnthropicChunk, - store.get(StoreKeys.LLMGenerationParameters) - ); - break; - default: - throw new Error(`LLM type ${llmConfig.type} not supported.`); - } - } + // const handleOpenAIChunk = (chunk: ChatCompletionChunk) => { + // event.sender.send("openAITokenStream", request.id, chunk); + // }; + + // const handleAnthropicChunk = (chunk: MessageStreamEvent) => { + // event.sender.send("anthropicTokenStream", request.id, chunk); + // }; + + console.log(`requestType:`, requestType); + + // switch (llmConfig.type) { + // case LLMType.OpenAI: + // await openAISession.streamingResponse( + // llmName, + // llmConfig, + // isJSONMode, + // request.displayableChatHistory, + // handleOpenAIChunk, + // store.get(StoreKeys.LLMGenerationParameters) + // ); + // break; + // case LLMType.Anthropic: + // await anthropicSession.streamingResponse( + // llmName, + // llmConfig, + // isJSONMode, + // request.displayableChatHistory, + // handleAnthropicChunk, + // store.get(StoreKeys.LLMGenerationParameters) + // ); + // break; + // default: + // throw new Error(`LLM type ${llmConfig.type} not supported.`); + // } + // } ); ipcMain.handle("set-default-llm", (event, modelName: string) => { // TODO: validate that the model exists diff --git a/electron/preload/index.ts b/electron/preload/index.ts index c9621a39..0bd08afe 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -21,6 +21,7 @@ import { import { ChatHistory } from "@/components/Chat/Chat"; import { ChatHistoryMetadata } from "@/components/Chat/hooks/use-chat-history"; +import { Query } from "@/components/Editor/QueryInput"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ReceiveCallback = (...args: any[]) => void; @@ -108,7 +109,7 @@ declare global { llmName: string, llmConfig: LLMConfig, isJSONMode: boolean, - chatHistory: ChatHistory + chatHistory: ChatHistory | Query ) => Promise; getLLMConfigs: () => Promise; pullOllamaModel: (modelName: string) => Promise; @@ -327,8 +328,8 @@ contextBridge.exposeInMainWorld("electronStore", { getCurrentOpenFiles: () => { return ipcRenderer.invoke("get-current-open-files"); }, - setCurrentOpenFiles: (action, tab) => { - ipcRenderer.invoke("set-current-open-files", { action, tab }); + setCurrentOpenFiles: (action, args) => { + ipcRenderer.invoke("set-current-open-files", { action, args }); }, }); @@ -456,14 +457,17 @@ contextBridge.exposeInMainWorld("llm", { llmName: string, llmConfig: LLMConfig, isJSONMode: boolean, - chatHistory: ChatHistory + request: ChatHistory | Query ) => { + let requestType = "remote" in request ? "query" : "chatHistory"; + return await ipcRenderer.invoke( "streaming-llm-response", llmName, llmConfig, isJSONMode, - chatHistory + request, + requestType ); }, diff --git a/src/components/DraggableTabs.tsx b/src/components/DraggableTabs.tsx index 5b5e7e05..1a543fea 100644 --- a/src/components/DraggableTabs.tsx +++ b/src/components/DraggableTabs.tsx @@ -25,13 +25,24 @@ const DraggableTabs: React.FC = ({ const newTabs = [...openTabs]; const draggedTab = newTabs.find((tab) => tab.id === draggedTabId); const targetIndex = newTabs.findIndex((tab) => tab.id === targetTabId); + const draggedIndex = newTabs.indexOf(draggedTab); - newTabs.splice(newTabs.indexOf(draggedTab), 1); // Remove the dragged tab + newTabs.splice(draggedIndex, 1); // Remove the dragged tab newTabs.splice(targetIndex, 0, draggedTab); // Insert at the new index + console.log(`Dragged: ${draggedIndex}, Target: ${targetIndex}`); + syncTabsWithBackend(draggedIndex, targetIndex); setOpenTabs(newTabs); }; + /* Sync New tab update with backened */ + const syncTabsWithBackend = async (draggedIndex, targetIndex) => { + await window.electronStore.setCurrentOpenFiles("update", { + draggedIndex: draggedIndex, + targetIndex: targetIndex, + }); + }; + const onDragOver = (event) => { event.preventDefault(); }; @@ -39,22 +50,27 @@ const DraggableTabs: React.FC = ({ return (
{openTabs.map((tab) => ( -
onDragStart(event, tab.id)} - onDrop={onDrop} - onDragOver={onDragOver} - className={`mx-2 py-1 bg-transparent text-white cursor-pointer ${ - currentFilePath === tab.filePath - ? "border-solid border-0 border-b-2 border-yellow-300" - : "" - }`} - onClick={() => onTabSelect(tab.filePath)} - > - {tab.title} - onTabClose(tab.id)}> +
+
onDragStart(event, tab.id)} + onDrop={onDrop} + onDragOver={onDragOver} + className={`mx-2 py-1 bg-transparent text-white cursor-pointer ${ + currentFilePath === tab.filePath + ? "border-solid border-0 border-b-2 border-yellow-300" + : "" + }`} + onClick={() => onTabSelect(tab.filePath)} + > + {tab.title} +
+ onTabClose(tab.id)} + > ×
diff --git a/src/components/Editor/LLMQueryTab.tsx b/src/components/Editor/LLMQueryTab.tsx index 1a5de300..ee11e49d 100644 --- a/src/components/Editor/LLMQueryTab.tsx +++ b/src/components/Editor/LLMQueryTab.tsx @@ -6,7 +6,6 @@ const OpenQueryTab = (setShowQueryBox) => addKeyboardShortcuts() { return { "Mod-Shift-l": () => { - console.log(`Toggling query box`); setShowQueryBox((prev) => !prev); }, }; diff --git a/src/components/Editor/QueryInput.tsx b/src/components/Editor/QueryInput.tsx index cf18f99e..de28cadd 100644 --- a/src/components/Editor/QueryInput.tsx +++ b/src/components/Editor/QueryInput.tsx @@ -1,5 +1,7 @@ import React, { useState } from "react"; +import createPreviewFile from "../File/PreviewFile"; + /* * Contains the options that users can query on */ @@ -13,7 +15,7 @@ enum QueryOptions { /** * Represents a query with a specified action and its associated arguments */ -interface Query { +export interface Query { /** * The query operation to be performed. */ @@ -35,6 +37,11 @@ interface Query { * If remote is true, then URL is stored at args[0] */ args: string[]; + + /** + * Path where query is made + */ + filePath: string; } /** @@ -42,11 +49,24 @@ interface Query { */ const quotePattern = /^"(.+)"$/; -const QueryInput = ({ setShowQueryBox }) => { +interface QueryInputProps { + setShowQueryBox: (show: boolean) => void; + onFileSelect: (path: string) => void; + setShowQueryWindow: (show: boolean) => void; + setQuery: (query: Query) => void; +} + +const QueryInput: React.FC = ({ + setShowQueryBox, + filePath, + setShowQueryWindow, + setQuery, +}) => { const [input, setInput] = useState(":"); - const [query, setQuery] = useState(null); + // const [query, setQuery] = useState(null); const [suggestions, setSuggestions] = useState([]); const [suggestChoice, setSuggestChoice] = useState(0); + const [displayPreview, setDisplayPreview] = useState(false); const handleKeyDown = (event) => { const length = Object.values(QueryOptions).length; @@ -63,9 +83,9 @@ const QueryInput = ({ setShowQueryBox }) => { setInput(newInput); updateSuggestions(newInput); } else { - console.log("Did not select a choice!"); /* Set up new Query Object*/ - parseInput(); + createQuery(); + setShowQueryWindow(true); } } else if (event.key === "Escape") { /* Close query box */ @@ -98,7 +118,7 @@ const QueryInput = ({ setShowQueryBox }) => { setSuggestions(filteredSuggestions); }; - const parseInput = () => { + const createQuery = () => { // Extract command const commandMatch = input.match(/^:([a-z]+)\s+(.*)$/i); if (!commandMatch) { @@ -107,16 +127,21 @@ const QueryInput = ({ setShowQueryBox }) => { } const command = commandMatch[1]; const content = commandMatch[2]; + let newQuery = null; switch (command) { case "summarize": - handleSummarizeCommand(content); + newQuery = handleSummarizeCommand(content); break; case "format": - handleFormatCommand(content); + newQuery = handleFormatCommand(content, filePath); break; default: break; } + + if (newQuery) { + setQuery(newQuery); + } }; /** @@ -125,7 +150,6 @@ const QueryInput = ({ setShowQueryBox }) => { const handleSummarizeCommand = (content: string) => { // Regex to match exactly one URL enclosed in square brackets const summarizePattern = /^\[\s*https?:\/\/[^\s\]]+\s*\]$/i; - console.log(`content: ${content}`); if (!summarizePattern.test(content.trim())) { console.error( "Invalid argument for summarize command. Expected a URL within square brackets." @@ -135,31 +159,27 @@ const QueryInput = ({ setShowQueryBox }) => { // If valid, extract URL and build the query object const url = content.slice(1, -1); // Remove the square brackets - const newQuery: Query = { + return { options: QueryOptions.summarize, remote: true, args: [url], + // filePath: }; - - setQuery(newQuery); - console.log("Summarize command processed:", newQuery); }; /** * Constructs the Query object when format command is invoked */ - const handleFormatCommand = (content: string) => { + const handleFormatCommand = (content: string, filePath: string) => { const match = content.match(quotePattern); if (match) { const args = match[1]; - const newQuery: Query = { + return { options: QueryOptions.format, remote: false, args: [args], + filePath: filePath, }; - - setQuery(newQuery); - console.log(`Format command processed:`, newQuery); } }; diff --git a/src/components/File/FileSideBar/FileHistoryBar.tsx b/src/components/File/FileSideBar/FileHistoryBar.tsx index 155118ea..67bf025d 100644 --- a/src/components/File/FileSideBar/FileHistoryBar.tsx +++ b/src/components/File/FileSideBar/FileHistoryBar.tsx @@ -11,7 +11,6 @@ interface FileHistoryNavigatorProps { setHistory: (string: string[]) => void; onFileSelect: (path: string) => void; currentPath: string; - syncTabsWithBackend: (string: path) => void; } const FileHistoryNavigator: React.FC = ({ @@ -19,7 +18,6 @@ const FileHistoryNavigator: React.FC = ({ setHistory, onFileSelect, currentPath, - syncTabsWithBackend, }) => { const [showMenu, setShowMenu] = useState(""); const [currentIndex, setCurrentIndex] = useState(-1); @@ -42,7 +40,6 @@ const FileHistoryNavigator: React.FC = ({ ]; setHistory(updatedHistory); setCurrentIndex(updatedHistory.length - 1); - syncTabsWithBackend(path); }; const canGoBack = currentIndex > 0; diff --git a/src/components/File/FileSideBar/FileItem.tsx b/src/components/File/FileSideBar/FileItem.tsx index 7dc82630..aa1ac2b5 100644 --- a/src/components/File/FileSideBar/FileItem.tsx +++ b/src/components/File/FileSideBar/FileItem.tsx @@ -1,13 +1,11 @@ - import React, { useState } from "react"; import { FileInfoNode } from "electron/main/Files/Types"; import posthog from "posthog-js"; -import { FaChevronRight , FaChevronDown } from "react-icons/fa"; +import { FaChevronRight, FaChevronDown } from "react-icons/fa"; import { isFileNodeDirectory, moveFile } from "./fileOperations"; - import { removeFileExtension } from "@/functions/strings"; interface FileInfoProps { @@ -63,6 +61,7 @@ export const FileItem: React.FC = ({ // Handle error (e.g., show an error message) } }; + const toggle = () => { if (isFileNodeDirectory(file)) { onDirectoryToggle(file.path); diff --git a/src/components/File/FileSideBar/index.tsx b/src/components/File/FileSideBar/index.tsx index f7e5f674..1efb22f9 100644 --- a/src/components/File/FileSideBar/index.tsx +++ b/src/components/File/FileSideBar/index.tsx @@ -9,7 +9,6 @@ import RenameNoteModal from "../RenameNote"; import { FileItem } from "./FileItem"; import { isFileNodeDirectory } from "./fileOperations"; - interface FileListProps { files: FileInfoTree; expandedDirectories: Map; diff --git a/src/components/File/PreviewFile.tsx b/src/components/File/PreviewFile.tsx new file mode 100644 index 00000000..992c1b8d --- /dev/null +++ b/src/components/File/PreviewFile.tsx @@ -0,0 +1,75 @@ +import React, { useEffect } from "react"; + +/** + * A function to handle creating or updating a preview file. + */ +const CreatePreviewFile = ({ query }) => { + console.log(`Displaying preview file:`, query); + + const TEMPORARY_CONTENT = ` + This is just a test + + In Reality, it would contain the content of the query executing on the editor + + + !!!! + `; + + const capitalizeFirstLetter = (word: string) => { + return word.charAt(0).toUpperCase() + word.slice(1); + }; + + useEffect(() => { + const fetchLLMConfig = async () => { + try { + const defaultLLMName = await window.llm.getDefaultLLMName(); + const llmConfigs = await window.llm.getLLMConfigs(); + + const currentModelConfig = llmConfigs.find( + (config) => config.modelName === defaultLLMName + ); + + if (!currentModelConfig) { + throw new Error(`No model config found for model: ${defaultLLMName}`); + } + + console.log( + `Default LLM: ${defaultLLMName}, CurrentModel: ${JSON.stringify( + currentModelConfig + )}` + ); + + await window.llm.streamingLLMResponse( + defaultLLMName, + currentModelConfig, + false, + query + ); + } catch (error) { + console.error("Failed to fetch LLM Config:", error); + // Handle errors as appropriate for your application + } + }; + + fetchLLMConfig(); + }, []); + + return ( +
+ {/* */} +
+ {/* Make it dynamic */} +

{capitalizeFirstLetter(query.options)} Display

+
+ + {/* Display Query Request */} +
+
Query
+

{query.args[0]}

+
+
{TEMPORARY_CONTENT}
+
+ ); +}; + +export default CreatePreviewFile; diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index f8138f6e..84e4237f 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState, useCallback } from "react"; import { EditorContent } from "@tiptap/react"; import posthog from "posthog-js"; +import { v4 as uuidv4 } from "uuid"; import ChatWithLLM, { ChatFilters, ChatHistory } from "./Chat/Chat"; import { useChatHistory } from "./Chat/hooks/use-chat-history"; @@ -14,6 +15,8 @@ import SidebarComponent from "./Similarity/SimilarFilesSidebar"; import { SearchInput } from "./SearchComponent"; import QueryInput from "./Editor/QueryInput"; import TitleBar from "./TitleBar"; +import { DraggableTabs } from "./DraggableTabs"; +import CreatePreviewFile from "./File/PreviewFile"; interface FileEditorContainerProps {} export type SidebarAbleToShow = "files" | "search" | "chats"; @@ -54,7 +57,8 @@ const FileEditorContainer: React.FC = () => { setShowSimilarFiles(!showSimilarFiles); }; - // const [fileIsOpen, setFileIsOpen] = useState(false); + const [showQueryWindow, setShowQueryWindow] = useState(false); + const [query, setQuery] = useState(null); const openFileAndOpenEditor = async (path: string) => { setShowChatbot(false); @@ -184,13 +188,92 @@ const FileEditorContainer: React.FC = () => { }; }, []); + useEffect(() => { + const fetchHistoryTabs = async () => { + const response = await window.electronStore.getCurrentOpenFiles(); + setOpenTabs(response); + console.log(`Fetching stored history: ${JSON.stringify(openTabs)}`); + }; + + fetchHistoryTabs(); + }, []); + + /* IPC Communication for Tab updates */ + const syncTabsWithBackend = async (path: string) => { + /* Deals with already open files */ + const tab = createTabObjectFromPath(path); + await window.electronStore.setCurrentOpenFiles("add", { + tab: tab, + }); + }; + + 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 + }; + + /* Creates Tab to display */ + const createTabObjectFromPath = (path) => { + return { + id: uuidv4(), + filePath: path, + title: extractFileName(path), + timeOpened: new Date(), + isDirty: false, + lastAccessed: new Date(), + }; + }; + + useEffect(() => { + if (!filePath) return; + console.log(`Filepath changed!`); + const existingTab = openTabs.find((tab) => tab.filePath === filePath); + + if (!existingTab) { + syncTabsWithBackend(filePath); + const newTab = createTabObjectFromPath(filePath); + // Update the tabs state by adding the new tab + setOpenTabs((prevTabs) => [...prevTabs, newTab]); + } + }, [filePath]); + + const handleTabSelect = (path: string) => { + console.log("Tab Selected:", path); + openFileAndOpenEditor(path); + }; + + const handleTabClose = async (tabId) => { + // Get current file path from the tab to be closed + let closedFilePath = ""; + let newIndex = -1; + + // Update tabs state and determine the new file to select + setOpenTabs((prevTabs) => { + const index = prevTabs.findIndex((tab) => tab.id === tabId); + closedFilePath = index !== -1 ? prevTabs[index].filePath : ""; + newIndex = index > 0 ? index - 1 : 0; // Set newIndex to previous one or 0 + return prevTabs.filter((tab, idx) => idx !== index); + }); + + // Update the selected file path after state update + if (closedFilePath === filePath) { + // If the closed tab was the current file, update the file selection + if (newIndex === -1 || newIndex >= openTabs.length) { + openFileAndOpenEditor(""); // If no tabs left or out of range, clear selection + } else { + openFileAndOpenEditor(openTabs[newIndex].filePath); // Select the new index's file + } + } + await window.electronStore.setCurrentOpenFiles("remove", { + tabId: tabId, + }); + }; + return (
= () => { /> )}
+ {showQueryBox && (
- +
)}
@@ -279,7 +374,7 @@ const FileEditorContainer: React.FC = () => { /> )}
- {showSimilarFiles && ( + {showSimilarFiles && !showQueryWindow ? ( = () => { await saveCurrentlyOpenedFile(); }} /> + ) : ( + + + )}
diff --git a/src/components/Sidebars/MainSidebar.tsx b/src/components/Sidebars/MainSidebar.tsx index 83df1de0..88df5bb3 100644 --- a/src/components/Sidebars/MainSidebar.tsx +++ b/src/components/Sidebars/MainSidebar.tsx @@ -12,7 +12,6 @@ import { SidebarAbleToShow } from "../FileEditorContainer"; import SearchComponent from "./FileSidebarSearch"; - interface SidebarManagerProps { files: FileInfoTree; expandedDirectories: Map; diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 56c2c8aa..cfecb0c0 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useState } from "react"; -import { v4 as uuidv4 } from "uuid"; import { PiSidebar, PiSidebarFill } from "react-icons/pi"; import FileHistoryNavigator from "./File/FileSideBar/FileHistoryBar"; -import { DraggableTabs } from "./DraggableTabs"; export const titleBarHeight = "30px"; @@ -15,8 +13,6 @@ interface TitleBarProps { toggleSimilarFiles: () => void; history: string[]; setHistory: (string: string[]) => void; - openTabs: Tab[]; - setOpenTabs: (openTabs: Tab[]) => void; } const TitleBar: React.FC = ({ @@ -26,8 +22,6 @@ const TitleBar: React.FC = ({ toggleSimilarFiles, history, setHistory, - openTabs, - setOpenTabs, }) => { const [platform, setPlatform] = useState(""); @@ -40,48 +34,6 @@ const TitleBar: React.FC = ({ fetchPlatform(); }, []); - useEffect(() => { - const fetchHistory = async () => { - const response = await window.electronStore.getCurrentOpenFiles(); - setOpenTabs(response); - console.log(`Fetching stored history: ${JSON.stringify(openTabs)}`); - }; - - fetchHistory(); - }, []); - - const createTabObjectFromPath = (path) => { - return { - id: uuidv4(), - filePath: path, - title: extractFileName(path), - timeOpened: new Date(), - isDirty: false, - lastAccessed: new Date(), - }; - }; - - /* IPC Communication for Tab updates */ - const syncTabsWithBackend = async (path: string) => { - /* Deals with already open files */ - const tab = createTabObjectFromPath(path); - await window.electronStore.setCurrentOpenFiles("add", tab); - }; - - 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 - }; - - const handleTabSelect = (path: string) => { - console.log("Tab Selected:", path); - onFileSelect(path); - }; - - const handleTabClose = (tabId) => { - setOpenTabs((prevTabs) => prevTabs.filter((tab) => tab.id !== tabId)); - }; - return (
= ({ setHistory={setHistory} onFileSelect={onFileSelect} currentPath={currentFilePath || ""} - syncTabsWithBackend={syncTabsWithBackend} />
- +
Date: Fri, 21 Jun 2024 22:46:18 -0500 Subject: [PATCH 04/40] parsing bug with chat and format --- electron/main/llm/llmSessionHandlers.ts | 77 ++++++++++++------------- electron/main/llm/models/OpenAI.ts | 4 +- electron/preload/index.ts | 10 ++-- package-lock.json | 77 +++++++++++++++++++++++-- src/components/Editor/QueryInput.tsx | 13 +++-- src/components/File/PreviewFile.tsx | 2 +- 6 files changed, 123 insertions(+), 60 deletions(-) diff --git a/electron/main/llm/llmSessionHandlers.ts b/electron/main/llm/llmSessionHandlers.ts index 807ff35f..8ec73aef 100644 --- a/electron/main/llm/llmSessionHandlers.ts +++ b/electron/main/llm/llmSessionHandlers.ts @@ -1,8 +1,6 @@ -import { MessageStreamEvent } from "@anthropic-ai/sdk/resources"; import { ipcMain, IpcMainInvokeEvent } from "electron"; import Store from "electron-store"; import { ProgressResponse } from "ollama"; -import { ChatCompletionChunk } from "openai/resources/chat/completions"; import { LLMConfig, StoreKeys, StoreSchema } from "../Store/storeConfig"; @@ -12,16 +10,15 @@ import { } from "./contextLimit"; import { addOrUpdateLLMSchemaInStore, - removeLLM, getAllLLMConfigs, getLLMConfig, + removeLLM, } from "./llmConfig"; import { AnthropicModelSessionService } from "./models/Anthropic"; import { OllamaService } from "./models/Ollama"; import { OpenAIModelSessionService } from "./models/OpenAI"; import { LLMSessionService } from "./Types"; -import { ChatHistory } from "@/components/Chat/Chat"; import { Query } from "@/components/Editor/QueryInput"; enum LLMType { @@ -46,45 +43,43 @@ export const registerLLMSessionHandlers = (store: Store) => { llmName: string, llmConfig: LLMConfig, isJSONMode: boolean, - // chatHistory: ChatHistory | Query, request: ChatHistories | Query, - requestType: boolean ): Promise => { - // const handleOpenAIChunk = (chunk: ChatCompletionChunk) => { - // event.sender.send("openAITokenStream", request.id, chunk); - // }; - - // const handleAnthropicChunk = (chunk: MessageStreamEvent) => { - // event.sender.send("anthropicTokenStream", request.id, chunk); - // }; - - console.log(`requestType:`, requestType); - - // switch (llmConfig.type) { - // case LLMType.OpenAI: - // await openAISession.streamingResponse( - // llmName, - // llmConfig, - // isJSONMode, - // request.displayableChatHistory, - // handleOpenAIChunk, - // store.get(StoreKeys.LLMGenerationParameters) - // ); - // break; - // case LLMType.Anthropic: - // await anthropicSession.streamingResponse( - // llmName, - // llmConfig, - // isJSONMode, - // request.displayableChatHistory, - // handleAnthropicChunk, - // store.get(StoreKeys.LLMGenerationParameters) - // ); - // break; - // default: - // throw new Error(`LLM type ${llmConfig.type} not supported.`); - // } - // } + const handleOpenAIChunk = (chunk: ChatCompletionChunk) => { + event.sender.send("openAITokenStream", request.id, chunk); + }; + + const handleAnthropicChunk = (chunk: MessageStreamEvent) => { + event.sender.send("anthropicTokenStream", request.id, chunk); + }; + + console.log("Registered LLM"); + + switch (llmConfig.type) { + case LLMType.OpenAI: + await openAISession.streamingResponse( + llmName, + llmConfig, + isJSONMode, + request.displayableChatHistory, + handleOpenAIChunk, + store.get(StoreKeys.LLMGenerationParameters) + ); + break; + case LLMType.Anthropic: + await anthropicSession.streamingResponse( + llmName, + llmConfig, + isJSONMode, + request.displayableChatHistory, + handleAnthropicChunk, + store.get(StoreKeys.LLMGenerationParameters) + ); + break; + default: + throw new Error(`LLM type ${llmConfig.type} not supported.`); + } + } ); ipcMain.handle("set-default-llm", (event, modelName: string) => { // TODO: validate that the model exists diff --git a/electron/main/llm/models/OpenAI.ts b/electron/main/llm/models/OpenAI.ts index c7003e79..33342b86 100644 --- a/electron/main/llm/models/OpenAI.ts +++ b/electron/main/llm/models/OpenAI.ts @@ -1,6 +1,6 @@ import { - LLMGenerationParameters, LLMConfig, + LLMGenerationParameters, } from "electron/main/Store/storeConfig"; import { Tiktoken, TiktokenModel, encodingForModel } from "js-tiktoken"; import OpenAI from "openai"; @@ -71,7 +71,7 @@ export class OpenAIModelSessionService implements LLMSessionService { baseURL: modelConfig.apiURL, fetch: customFetchUsingElectronNetStreaming, }); - console.log("messageHistory: "); + console.log("messageHistory: ", messageHistory); const stream = await openai.chat.completions.create({ model: modelName, diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 0bd08afe..61322f9d 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -1,7 +1,4 @@ import { IpcRendererEvent, contextBridge, ipcRenderer } from "electron"; -import { PromptWithRagResults } from "electron/main/database/dbSessionHandlers"; -import { BasePromptRequirements } from "electron/main/database/dbSessionHandlerTypes"; -import { DBEntry, DBQueryResult } from "electron/main/database/Schema"; import { AugmentPromptWithFileProps, FileInfoNode, @@ -15,9 +12,12 @@ import { EmbeddingModelWithLocalPath, EmbeddingModelWithRepo, HardwareConfig, - LLMGenerationParameters, LLMConfig, + LLMGenerationParameters, } from "electron/main/Store/storeConfig"; +import { DBEntry, DBQueryResult } from "electron/main/database/Schema"; +import { BasePromptRequirements } from "electron/main/database/dbSessionHandlerTypes"; +import { PromptWithRagResults } from "electron/main/database/dbSessionHandlers"; import { ChatHistory } from "@/components/Chat/Chat"; import { ChatHistoryMetadata } from "@/components/Chat/hooks/use-chat-history"; @@ -459,7 +459,6 @@ contextBridge.exposeInMainWorld("llm", { isJSONMode: boolean, request: ChatHistory | Query ) => { - let requestType = "remote" in request ? "query" : "chatHistory"; return await ipcRenderer.invoke( "streaming-llm-response", @@ -467,7 +466,6 @@ contextBridge.exposeInMainWorld("llm", { llmConfig, isJSONMode, request, - requestType ); }, diff --git a/package-lock.json b/package-lock.json index 7cc4d216..fa1ee187 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@emotion/styled": "^11.11.0", "@headlessui/react": "^1.7.19", "@huggingface/hub": "^0.12.0", - "@lancedb/vectordb-win32-x64-msvc": "^0.5.0", "@material-tailwind/react": "^2.1.5", "@mui/icons-material": "^5.15.15", "@mui/joy": "^5.0.0-beta.23", @@ -62,6 +61,7 @@ "tiptap-markdown": "^0.8.10", "turndown": "^7.1.2", "use-debounce": "^10.0.1", + "uuid": "^10.0.0", "vectordb": "0.4.10" }, "devDependencies": { @@ -104,6 +104,9 @@ "node": "^14.18.0 || >=16.0.0" }, "optionalDependencies": { + "@lancedb/vectordb-darwin-x64": "^0.5.0", + "@lancedb/vectordb-linux-x64-msvc": "0.5.0", + "@lancedb/vectordb-win32-x64-msvc": "^0.5.0", "dmg-license": "^1.0.11" } }, @@ -2155,9 +2158,9 @@ ] }, "node_modules/@lancedb/vectordb-darwin-x64": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.4.10.tgz", - "integrity": "sha512-XbfR58OkQpAe0xMSTrwJh9ZjGSzG9EZ7zwO6HfYem8PxcLYAcC6eWRWoSG/T0uObyrPTcYYyvHsp0eNQWYBFAQ==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.5.0.tgz", + "integrity": "sha512-N+LG+JusTK1xgax6IUnmvZgbwtmi0mwpDC1Clmnj9yO2S8kDtBqJbeYzCjdgHB+ayx6E43wMsMuH+5Haprq/zA==", "cpu": [ "x64" ], @@ -2197,6 +2200,7 @@ "cpu": [ "x64" ], + "optional": true, "os": [ "win32" ] @@ -2556,6 +2560,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", @@ -2596,6 +2612,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", @@ -11377,6 +11405,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", @@ -11409,6 +11449,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/lazy-val": { "version": "1.0.5", "license": "MIT" @@ -18448,12 +18500,13 @@ "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" } @@ -18502,6 +18555,18 @@ "@lancedb/vectordb-win32-x64-msvc": "0.4.10" } }, + "node_modules/vectordb/node_modules/@lancedb/vectordb-darwin-x64": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.4.10.tgz", + "integrity": "sha512-XbfR58OkQpAe0xMSTrwJh9ZjGSzG9EZ7zwO6HfYem8PxcLYAcC6eWRWoSG/T0uObyrPTcYYyvHsp0eNQWYBFAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, "node_modules/vectordb/node_modules/@lancedb/vectordb-win32-x64-msvc": { "version": "0.4.10", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.4.10.tgz", diff --git a/src/components/Editor/QueryInput.tsx b/src/components/Editor/QueryInput.tsx index de28cadd..f46dce00 100644 --- a/src/components/Editor/QueryInput.tsx +++ b/src/components/Editor/QueryInput.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import createPreviewFile from "../File/PreviewFile"; +import { ChatMessageToDisplay } from "../Chat/Chat"; /* * Contains the options that users can query on @@ -42,6 +42,11 @@ export interface Query { * Path where query is made */ filePath: string; + + /** + * Response of Query + */ + displayableChatHistory: ChatMessageToDisplay[]; } /** @@ -179,6 +184,7 @@ const QueryInput: React.FC = ({ remote: false, args: [args], filePath: filePath, + displayableChatHistory: [args], }; } }; @@ -190,9 +196,8 @@ const QueryInput: React.FC = ({ {suggestions.map((suggestion, index) => (
  • {suggestion}
  • diff --git a/src/components/File/PreviewFile.tsx b/src/components/File/PreviewFile.tsx index 992c1b8d..4ec5ba72 100644 --- a/src/components/File/PreviewFile.tsx +++ b/src/components/File/PreviewFile.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import { useEffect } from "react"; /** * A function to handle creating or updating a preview file. From 25e9dd009a7dea3051d0d9ca646cd7ffc624b080 Mon Sep 17 00:00:00 2001 From: Mohamed Ilaiwi Date: Sat, 22 Jun 2024 11:35:33 -0500 Subject: [PATCH 05/40] Need to resolve answering questions properly --- electron/main/llm/models/OpenAI.ts | 1 - src/components/Chat/Chat.tsx | 51 +++++++++--------- src/components/Editor/QueryInput.tsx | 2 +- src/components/File/PreviewFile.tsx | 74 ++++++++++++++++++++++---- src/components/FileEditorContainer.tsx | 18 ++++--- 5 files changed, 101 insertions(+), 45 deletions(-) diff --git a/electron/main/llm/models/OpenAI.ts b/electron/main/llm/models/OpenAI.ts index 33342b86..9ce77fea 100644 --- a/electron/main/llm/models/OpenAI.ts +++ b/electron/main/llm/models/OpenAI.ts @@ -65,7 +65,6 @@ export class OpenAIModelSessionService implements LLMSessionService { handleChunk: (chunk: ChatCompletionChunk) => void, generationParams?: LLMGenerationParameters ): Promise { - console.log("making call to url: ", modelConfig); const openai = new OpenAI({ apiKey: modelConfig.apiKey, baseURL: modelConfig.apiURL, diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index e6a4f4ac..be130ab8 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -297,13 +297,12 @@ const ChatWithLLM: React.FC = ({ {message.visibleContent ? message.visibleContent @@ -313,25 +312,25 @@ const ChatWithLLM: React.FC = ({
    {(!currentChatHistory || currentChatHistory?.displayableChatHistory.length == 0) && ( - <> -
    - Start a conversation with your notes by typing a message below. -
    -
    -
    +
    + -
    - - )} + onClick={() => { + setIsAddContextFiltersModalOpen(true); + }} + > + {chatFilters.files.length > 0 + ? "Update filters" + : "Add filters"} + +
    + + )} {isAddContextFiltersModalOpen && ( = ({ ); })} */} {userTextFieldInput === "" && - (!currentChatHistory || - currentChatHistory?.displayableChatHistory.length == 0) ? ( + (!currentChatHistory || + currentChatHistory?.displayableChatHistory.length == 0) ? ( <> {EXAMPLE_PROMPTS[askText].map((option, index) => { return ( @@ -393,7 +392,7 @@ const ChatWithLLM: React.FC = ({ return Promise.resolve(); }} isLoadingSimilarEntries={false} - setIsRefined={() => {}} // to allow future toggling + setIsRefined={() => { }} // to allow future toggling isRefined={true} // always refined for now /> )} diff --git a/src/components/Editor/QueryInput.tsx b/src/components/Editor/QueryInput.tsx index f46dce00..63de6c7c 100644 --- a/src/components/Editor/QueryInput.tsx +++ b/src/components/Editor/QueryInput.tsx @@ -184,7 +184,7 @@ const QueryInput: React.FC = ({ remote: false, args: [args], filePath: filePath, - displayableChatHistory: [args], + displayableChatHistory: [], }; } }; diff --git a/src/components/File/PreviewFile.tsx b/src/components/File/PreviewFile.tsx index 4ec5ba72..8c4bb45a 100644 --- a/src/components/File/PreviewFile.tsx +++ b/src/components/File/PreviewFile.tsx @@ -1,19 +1,55 @@ -import { useEffect } from "react"; +import { Query } from "@/components/Editor/QueryInput"; +import { useEffect, useState } from "react"; + +interface CreatePreviewFileProps { + query: Query, + editorContent: string, +} /** * A function to handle creating or updating a preview file. */ -const CreatePreviewFile = ({ query }) => { - console.log(`Displaying preview file:`, query); +const CreatePreviewFile: React.FC = ({ + query, + editorContent +}) => { + const [queryChatWindow, setQueryChatWindow] = useState(""); - const TEMPORARY_CONTENT = ` - This is just a test + /** + * Updates the queryChatWindow state and attaches listeners for + * ipc calls + */ + useEffect(() => { + /** + * Updates state depending on content inside ChatCompletionChunk + * + * @param chunk: response from LLM + */ + const handleOpenAIChunk = ( + receivedChatID: string, + chunk: ChatCompletionChunk + ) => { + const newContent = chunk.choices[0].delta.content ?? ""; + appendContentToInlineQueryWindow(newContent); + } - In Reality, it would contain the content of the query executing on the editor + const openAITokenStreamListener = window.ipcRenderer.receive( + "openAITokenStream", + handleOpenAIChunk + ); + return () => { + openAITokenStreamListener(); + }; + }, []) - !!!! - `; + const appendContentToInlineQueryWindow = ( + newContent: string + ) => { + setQueryChatWindow((prevContent) => { + return prevContent + newContent; + }); + } const capitalizeFirstLetter = (word: string) => { return word.charAt(0).toUpperCase() + word.slice(1); @@ -39,6 +75,26 @@ const CreatePreviewFile = ({ query }) => { )}` ); + query.displayableChatHistory.push({ + role: "system", + content: "You are an expert in syntax and formatting, especially for enhancing note-taking efficiency and clarity. You will only follow the instructions given that relates to formatting. Do not provide extranneous information. The content will be given in JSON.", + messageType: "info", + context: [], + }) + + query.displayableChatHistory.push({ + role: "user", + content: query.args[0], + messageType: "success", + context: [], + }); + + query.displayableChatHistory.push({ + role: "user", + content: editorContent, + + }) + await window.llm.streamingLLMResponse( defaultLLMName, currentModelConfig, @@ -67,7 +123,7 @@ const CreatePreviewFile = ({ query }) => {
    Query

    {query.args[0]}

    -
    {TEMPORARY_CONTENT}
    +
    {queryChatWindow}
    ); }; diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index 84e4237f..bb94e01a 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -1,24 +1,23 @@ -import React, { useEffect, useState, useCallback } from "react"; import { EditorContent } from "@tiptap/react"; import posthog from "posthog-js"; +import React, { useCallback, useEffect, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import ChatWithLLM, { ChatFilters, ChatHistory } from "./Chat/Chat"; import { useChatHistory } from "./Chat/hooks/use-chat-history"; +import { DraggableTabs } from "./DraggableTabs"; import InEditorBacklinkSuggestionsDisplay from "./Editor/BacklinkSuggestionsDisplay"; +import QueryInput from "./Editor/QueryInput"; import { useFileInfoTree } from "./File/FileSideBar/hooks/use-file-info-tree"; +import CreatePreviewFile from "./File/PreviewFile"; import { useFileByFilepath } from "./File/hooks/use-file-by-filepath"; import ResizableComponent from "./Generic/ResizableComponent"; import IconsSidebar from "./Sidebars/IconsSidebar"; import SidebarManager from "./Sidebars/MainSidebar"; import SidebarComponent from "./Similarity/SimilarFilesSidebar"; -import { SearchInput } from "./SearchComponent"; -import QueryInput from "./Editor/QueryInput"; import TitleBar from "./TitleBar"; -import { DraggableTabs } from "./DraggableTabs"; -import CreatePreviewFile from "./File/PreviewFile"; -interface FileEditorContainerProps {} +interface FileEditorContainerProps { } export type SidebarAbleToShow = "files" | "search" | "chats"; const FileEditorContainer: React.FC = () => { @@ -354,7 +353,7 @@ const FileEditorContainer: React.FC = () => { editor={editor} /> {showQueryBox && ( -
    +
    = () => { /> ) : ( - + )}
    From 5b7665e8e289cb7d901df0021a9cd611bec6efb7 Mon Sep 17 00:00:00 2001 From: Mohamed Ilaiwi Date: Wed, 26 Jun 2024 13:30:58 -0500 Subject: [PATCH 06/40] Added new tab support --- electron/preload/index.ts | 4 +-- src/components/DraggableTabs.tsx | 38 ++++++++++---------- src/components/Editor/QueryInput.tsx | 8 ++--- src/components/File/PreviewFile.tsx | 3 +- src/components/FileEditorContainer.tsx | 50 +++++++++++++++----------- 5 files changed, 55 insertions(+), 48 deletions(-) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 1a8518fb..0f8e0afe 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -165,7 +165,7 @@ declare global { getChatHistory: (chatID: string) => Promise; removeChatHistoryAtID: (chatID: string) => void; getCurrentOpenFiles: () => Promise; - setCurrentOpenFiles: (filePath: string) => void; + setCurrentOpenFiles: (action: any, args: any) => void; }; } } @@ -339,7 +339,7 @@ contextBridge.exposeInMainWorld("electronStore", { getCurrentOpenFiles: () => { return ipcRenderer.invoke("get-current-open-files"); }, - setCurrentOpenFiles: (action, args) => { + setCurrentOpenFiles: (action: any, args: any) => { ipcRenderer.invoke("set-current-open-files", { action, args }); }, }); diff --git a/src/components/DraggableTabs.tsx b/src/components/DraggableTabs.tsx index 1a543fea..203c0d5d 100644 --- a/src/components/DraggableTabs.tsx +++ b/src/components/DraggableTabs.tsx @@ -4,8 +4,8 @@ interface DraggableTabsProps { openTabs: Tab[]; setOpenTabs: (openTabs: Tab[]) => void; onTabSelect: (path: string) => void; - onTabClose: (tabId) => void; - currentFilePath: (path: string) => void; + onTabClose: (event: any, tabId: string) => void; + currentFilePath: string; } const DraggableTabs: React.FC = ({ @@ -15,11 +15,12 @@ const DraggableTabs: React.FC = ({ onTabClose, currentFilePath, }) => { - const onDragStart = (event, tabId) => { + console.log("OpenTabs:", openTabs); + const onDragStart = (event: any, tabId: string) => { event.dataTransfer.setData("tabId", tabId); }; - const onDrop = (event) => { + const onDrop = (event: any) => { const draggedTabId = event.dataTransfer.getData("tabId"); const targetTabId = event.target.getAttribute("data-tabid"); const newTabs = [...openTabs]; @@ -36,21 +37,21 @@ const DraggableTabs: React.FC = ({ }; /* Sync New tab update with backened */ - const syncTabsWithBackend = async (draggedIndex, targetIndex) => { - await window.electronStore.setCurrentOpenFiles("update", { + const syncTabsWithBackend = (draggedIndex: number, targetIndex: number) => { + window.electronStore.setCurrentOpenFiles("update", { draggedIndex: draggedIndex, targetIndex: targetIndex, }); }; - const onDragOver = (event) => { + const onDragOver = (event: any) => { event.preventDefault(); }; return ( -
    +
    {openTabs.map((tab) => ( -
    +
    = ({ onDragStart={(event) => onDragStart(event, tab.id)} onDrop={onDrop} onDragOver={onDragOver} - className={`mx-2 py-1 bg-transparent text-white cursor-pointer ${ - currentFilePath === tab.filePath - ? "border-solid border-0 border-b-2 border-yellow-300" - : "" - }`} + className={`py-4 px-2 text-white cursor-pointer flex justify-center gap-1 items-center text-sm ${currentFilePath === tab.filePath + ? "bg-dark-gray-c-three" : ""}`} onClick={() => onTabSelect(tab.filePath)} > {tab.title} + onTabClose(e, tab.id)} + > + × +
    - onTabClose(tab.id)} - > - × -
    ))}
    diff --git a/src/components/Editor/QueryInput.tsx b/src/components/Editor/QueryInput.tsx index 63de6c7c..3a0732be 100644 --- a/src/components/Editor/QueryInput.tsx +++ b/src/components/Editor/QueryInput.tsx @@ -56,7 +56,7 @@ const quotePattern = /^"(.+)"$/; interface QueryInputProps { setShowQueryBox: (show: boolean) => void; - onFileSelect: (path: string) => void; + filePath: string; setShowQueryWindow: (show: boolean) => void; setQuery: (query: Query) => void; } @@ -71,13 +71,12 @@ const QueryInput: React.FC = ({ // const [query, setQuery] = useState(null); const [suggestions, setSuggestions] = useState([]); const [suggestChoice, setSuggestChoice] = useState(0); - const [displayPreview, setDisplayPreview] = useState(false); - const handleKeyDown = (event) => { + const handleKeyDown = (event: any) => { const length = Object.values(QueryOptions).length; if (event.key === " ") { - const command = input.slice(1).trim().split(" ")[0]; + const command: string = input.slice(1).trim().split(" ")[0]; if (!QueryOptions[command]) { event.preventDefault(); } @@ -211,6 +210,7 @@ const QueryInput: React.FC = ({ className="w-full bg-light-arsenic p-2 text-white" onChange={handleChange} onKeyDown={handleKeyDown} + onBlur={() => setShowQueryBox(false)} autoFocus />
    diff --git a/src/components/File/PreviewFile.tsx b/src/components/File/PreviewFile.tsx index 8c4bb45a..1358a56c 100644 --- a/src/components/File/PreviewFile.tsx +++ b/src/components/File/PreviewFile.tsx @@ -92,7 +92,8 @@ const CreatePreviewFile: React.FC = ({ query.displayableChatHistory.push({ role: "user", content: editorContent, - + messageType: "success", + context: [], }) await window.llm.streamingLLMResponse( diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index 1d81ae1e..5c16c4d7 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -1,6 +1,8 @@ import { EditorContent } from "@tiptap/react"; import posthog from "posthog-js"; import React, { useCallback, useEffect, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; + import "../styles/global.css"; import ChatWithLLM, { ChatFilters, ChatHistory } from "./Chat/Chat"; @@ -234,6 +236,7 @@ const FileEditorContainer: React.FC = () => { // Update the tabs state by adding the new tab setOpenTabs((prevTabs) => [...prevTabs, newTab]); } + setShowQueryBox(false); }, [filePath]); const handleTabSelect = (path: string) => { @@ -319,7 +322,7 @@ const FileEditorContainer: React.FC = () => {
    editor?.commands.focus()} style={{ backgroundColor: "rgb(30, 30, 30)", @@ -341,29 +344,34 @@ const FileEditorContainer: React.FC = () => { /> )}
    - - - {showQueryBox && ( -
    - +
    +
    - )} +
    +
    + + {showQueryBox && ( +
    + +
    + )} +
    - {suggestionsState && ( Date: Wed, 26 Jun 2024 23:53:25 -0500 Subject: [PATCH 07/40] Refined some code --- electron/main/Store/storeHandlers.ts | 8 +++ src/components/DraggableTabs.tsx | 18 ++++--- src/components/File/PreviewFile.tsx | 67 ++++++++++++++++++-------- src/components/FileEditorContainer.tsx | 7 +-- src/components/Settings/Settings.tsx | 16 ------ 5 files changed, 71 insertions(+), 45 deletions(-) diff --git a/electron/main/Store/storeHandlers.ts b/electron/main/Store/storeHandlers.ts index 78af8fa1..b0aa7452 100644 --- a/electron/main/Store/storeHandlers.ts +++ b/electron/main/Store/storeHandlers.ts @@ -270,6 +270,7 @@ export const registerStoreHandlers = ( const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || []; const addTab = ({ tab }) => { + if (tab === null) return; const existingTab = openTabs.findIndex( (item) => item.filePath === tab.filePath ); @@ -286,6 +287,10 @@ export const registerStoreHandlers = ( store.set(StoreKeys.OpenTabs, updatedTabs); }; + const clearAllTabs = () => { + store.set(StoreKeys.OpenTabs, []); + }; + const updateTab = ({ draggedIndex, targetIndex }) => { // Swap dragged and target [openTabs[draggedIndex], openTabs[targetIndex]] = [ @@ -305,6 +310,9 @@ export const registerStoreHandlers = ( case "update": updateTab(args); break; + case "clear": + clearAllTabs(); + break; default: throw new Error("Unsupported action type"); } diff --git a/src/components/DraggableTabs.tsx b/src/components/DraggableTabs.tsx index 203c0d5d..e5f876b4 100644 --- a/src/components/DraggableTabs.tsx +++ b/src/components/DraggableTabs.tsx @@ -51,22 +51,28 @@ const DraggableTabs: React.FC = ({ return (
    {openTabs.map((tab) => ( -
    +
    onDragStart(event, tab.id)} onDrop={onDrop} onDragOver={onDragOver} - className={`py-4 px-2 text-white cursor-pointer flex justify-center gap-1 items-center text-sm ${currentFilePath === tab.filePath - ? "bg-dark-gray-c-three" : ""}`} + className={`py-4 px-2 text-white cursor-pointer flex justify-center gap-1 items-center text-sm ${ + currentFilePath === tab.filePath ? "bg-dark-gray-c-three" : "" + }`} onClick={() => onTabSelect(tab.filePath)} > {tab.title} onTabClose(e, tab.id)} + className="text-md cursor-pointer px-1 hover:bg-dark-gray-c-five hover:rounded-md" + onClick={(e) => { + e.stopPropagation(); // Prevent triggering onClick of parent div + onTabClose(e, tab.id); + }} > × diff --git a/src/components/File/PreviewFile.tsx b/src/components/File/PreviewFile.tsx index 1358a56c..26fd925e 100644 --- a/src/components/File/PreviewFile.tsx +++ b/src/components/File/PreviewFile.tsx @@ -1,9 +1,10 @@ import { Query } from "@/components/Editor/QueryInput"; import { useEffect, useState } from "react"; +import { TypeAnimation } from "react-type-animation"; interface CreatePreviewFileProps { - query: Query, - editorContent: string, + query: Query; + editorContent: string; } /** @@ -11,27 +12,34 @@ interface CreatePreviewFileProps { */ const CreatePreviewFile: React.FC = ({ query, - editorContent + editorContent, }) => { const [queryChatWindow, setQueryChatWindow] = useState(""); + const [isLoadingQuery, setIsLoadingQuery] = useState(true); + const [errorOccured, setErrorOccured] = useState(""); + + useEffect(() => { + setIsLoadingQuery(true); + setErrorOccured(""); + }, []); /** - * Updates the queryChatWindow state and attaches listeners for + * Updates the queryChatWindow state and attaches listeners for * ipc calls */ useEffect(() => { /** - * Updates state depending on content inside ChatCompletionChunk - * - * @param chunk: response from LLM - */ + * Updates state depending on content inside ChatCompletionChunk + * + * @param chunk: response from LLM + */ const handleOpenAIChunk = ( receivedChatID: string, chunk: ChatCompletionChunk ) => { const newContent = chunk.choices[0].delta.content ?? ""; appendContentToInlineQueryWindow(newContent); - } + }; const openAITokenStreamListener = window.ipcRenderer.receive( "openAITokenStream", @@ -41,15 +49,13 @@ const CreatePreviewFile: React.FC = ({ return () => { openAITokenStreamListener(); }; - }, []) + }, []); - const appendContentToInlineQueryWindow = ( - newContent: string - ) => { + const appendContentToInlineQueryWindow = (newContent: string) => { setQueryChatWindow((prevContent) => { return prevContent + newContent; }); - } + }; const capitalizeFirstLetter = (word: string) => { return word.charAt(0).toUpperCase() + word.slice(1); @@ -57,6 +63,8 @@ const CreatePreviewFile: React.FC = ({ useEffect(() => { const fetchLLMConfig = async () => { + setIsLoadingQuery(true); + setErrorOccured(""); try { const defaultLLMName = await window.llm.getDefaultLLMName(); const llmConfigs = await window.llm.getLLMConfigs(); @@ -77,10 +85,11 @@ const CreatePreviewFile: React.FC = ({ query.displayableChatHistory.push({ role: "system", - content: "You are an expert in syntax and formatting, especially for enhancing note-taking efficiency and clarity. You will only follow the instructions given that relates to formatting. Do not provide extranneous information. The content will be given in JSON.", + content: + "You are an expert in syntax and formatting, especially for enhancing note-taking efficiency and clarity. You will only follow the instructions given that relates to formatting. Do not provide extranneous information. The content will be given in JSON.", messageType: "info", context: [], - }) + }); query.displayableChatHistory.push({ role: "user", @@ -94,17 +103,22 @@ const CreatePreviewFile: React.FC = ({ content: editorContent, messageType: "success", context: [], - }) + }); - await window.llm.streamingLLMResponse( + const response = await window.llm.streamingLLMResponse( defaultLLMName, currentModelConfig, false, query ); + + setIsLoadingQuery(false); } catch (error) { console.error("Failed to fetch LLM Config:", error); - // Handle errors as appropriate for your application + const errorMessage = + error instanceof Error ? error.message : String(error); + setErrorOccured(errorMessage); + setIsLoadingQuery(false); } }; @@ -124,7 +138,20 @@ const CreatePreviewFile: React.FC = ({
    Query

    {query.args[0]}

    -
    {queryChatWindow}
    +
    + {isLoadingQuery ? ( + + ) : errorOccured ? ( +
    {errorOccured}
    + ) : ( +
    We here
    + )} +
    ); }; diff --git a/src/components/FileEditorContainer.tsx b/src/components/FileEditorContainer.tsx index 5c16c4d7..75821251 100644 --- a/src/components/FileEditorContainer.tsx +++ b/src/components/FileEditorContainer.tsx @@ -3,7 +3,6 @@ import posthog from "posthog-js"; import React, { useCallback, useEffect, useState } from "react"; import { v4 as uuidv4 } from "uuid"; - import "../styles/global.css"; import ChatWithLLM, { ChatFilters, ChatHistory } from "./Chat/Chat"; import { useChatHistory } from "./Chat/hooks/use-chat-history"; @@ -19,7 +18,7 @@ import SidebarManager from "./Sidebars/MainSidebar"; import SidebarComponent from "./Similarity/SimilarFilesSidebar"; import TitleBar from "./TitleBar"; -interface FileEditorContainerProps { } +interface FileEditorContainerProps {} export type SidebarAbleToShow = "files" | "search" | "chats"; const FileEditorContainer: React.FC = () => { @@ -244,8 +243,10 @@ const FileEditorContainer: React.FC = () => { openFileAndOpenEditor(path); }; - const handleTabClose = async (tabId) => { + const handleTabClose = async (event, tabId) => { // Get current file path from the tab to be closed + event.stopPropagation(); + console.log("Closing tab!"); let closedFilePath = ""; let newIndex = -1; diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index bd9051a8..4acdf43d 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -112,16 +112,6 @@ const SettingsModal: React.FC = ({ > Analytics{" "}
    -
    setActiveTab(SettingsTab.GENERAL)} - > - General{" "} -
    {/* Right Content Area */} @@ -172,12 +162,6 @@ const SettingsModal: React.FC = ({
    )} - - {activeTab === SettingsTab.GENERAL && ( -
    - -
    - )}
    From 8eb8e845501ce85072855657c8bc57d1a7ded5e3 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Mon, 8 Jul 2024 21:22:38 -0500 Subject: [PATCH 08/40] Refined UI even more --- electron/main/electron-store/ipcHandlers.ts | 9 ++ electron/main/electron-store/storeConfig.ts | 2 + electron/preload/index.ts | 7 ++ package-lock.json | 4 +- src/components/Common/Modal.tsx | 2 +- src/components/Editor/EditorManager.tsx | 50 ++++++-- src/components/File/FileSideBar/index.tsx | 2 +- src/components/MainPage.tsx | 1 - src/components/Settings/GeneralSections.tsx | 76 +++++++++--- src/components/Settings/GeneralSettings.tsx | 126 ++------------------ src/components/Settings/Settings.tsx | 42 +++---- src/components/Sidebars/IconsSidebar.tsx | 62 ++++++---- 12 files changed, 184 insertions(+), 199 deletions(-) diff --git a/electron/main/electron-store/ipcHandlers.ts b/electron/main/electron-store/ipcHandlers.ts index 27137ef0..a506c995 100644 --- a/electron/main/electron-store/ipcHandlers.ts +++ b/electron/main/electron-store/ipcHandlers.ts @@ -158,6 +158,15 @@ export const registerStoreHandlers = ( return store.get(StoreKeys.IsSBCompact); }); + ipcMain.handle("get-editor-flex-center", () => { + return 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) => { console.log("setting analytics mode", isAnalytics); store.set(StoreKeys.Analytics, isAnalytics); diff --git a/electron/main/electron-store/storeConfig.ts b/electron/main/electron-store/storeConfig.ts index 8e4dc87c..5bf2a901 100644 --- a/electron/main/electron-store/storeConfig.ts +++ b/electron/main/electron-store/storeConfig.ts @@ -74,6 +74,7 @@ export interface StoreSchema { isSBCompact: boolean; DisplayMarkdown: boolean; spellCheck: string; + EditorFlexCenter: boolean; } export enum StoreKeys { @@ -93,4 +94,5 @@ export enum StoreKeys { IsSBCompact = "isSBCompact", DisplayMarkdown = "DisplayMarkdown", SpellCheck = "spellCheck", + EditorFlexCenter = "editorFlexCenter", } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index d40b5409..e9f06016 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -178,6 +178,13 @@ const electronStore = { setDisplayMarkdown: createIPCHandler< (displayMarkdown: boolean) => Promise >("set-display-markdown"), + + getEditorFlexCenter: createIPCHandler<() => Promise>( + "get-editor-flex-center" + ), + setEditorFlexCenter: createIPCHandler< + (editorFlexCenter: boolean) => Promise + >("set-editor-flex-center"), }; const fileSystem = { diff --git a/package-lock.json b/package-lock.json index b024b707..10bcfd57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "reor-project", - "version": "0.2.11", + "version": "0.2.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reor-project", - "version": "0.2.11", + "version": "0.2.13", "license": "AGPL-3.0", "dependencies": { "@aarkue/tiptap-math-extension": "^1.2.2", diff --git a/src/components/Common/Modal.tsx b/src/components/Common/Modal.tsx index ba06fd3f..6ac49260 100644 --- a/src/components/Common/Modal.tsx +++ b/src/components/Common/Modal.tsx @@ -82,7 +82,7 @@ const ReorModal: React.FC = ({ >
    {!hideCloseButton && ( diff --git a/src/components/Editor/EditorManager.tsx b/src/components/Editor/EditorManager.tsx index 7d862779..acce5d7e 100644 --- a/src/components/Editor/EditorManager.tsx +++ b/src/components/Editor/EditorManager.tsx @@ -25,7 +25,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); @@ -92,6 +92,27 @@ 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, editorFlexCenter: boolean) => { + setEditorFlex(editorFlexCenter); + }; + + initEditorContentCenter(); + window.ipcRenderer.on("editor-flex-center-changed", handleEditorChange); + return () => { + window.ipcRenderer.removeListener( + "editor-flex-center-changed", + handleEditorChange + ); + }; + }, []); + return (
    = ({ setMenuVisible={setMenuVisible} /> )} - +
    + +
    {suggestionsState && ( = ({ return (
    {/* */} diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index f2fc1b9f..27fca067 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -66,7 +66,6 @@ const FileEditorContainer: React.FC = () => { minDate: new Date(0), maxDate: new Date(), }); - const [sidebarWidth, setSidebarWidth] = useState(40); const handleAddFileToChatFilters = (file: string) => { diff --git a/src/components/Settings/GeneralSections.tsx b/src/components/Settings/GeneralSections.tsx index 6cffe6d6..c36a6004 100644 --- a/src/components/Settings/GeneralSections.tsx +++ b/src/components/Settings/GeneralSections.tsx @@ -15,12 +15,11 @@ export interface GenSettingsProps { // editorAppearance?: SettingsAppearance; } -const CreateAppearanceSection: React.FC = () => { +export const CreateAppearanceSection = ({}) => { const [isIconSBCompact, setIsIconSBCompact] = useState(false); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [displayMarkdown, setDisplayMarkdown] = useState(false); - // const [editorAppearance, setEditorApperance] = - // useState("dark"); + const [editorAppearance, setEditorApperance] = + useState("dark"); // Check if SidebarCompact is on or not useEffect(() => { @@ -50,14 +49,16 @@ const CreateAppearanceSection: React.FC = () => { return (
    -

    +

    Appearance

    -
    +
    -

    IconSidebar Compact

    -

    - If on, decreases padding on IconSidebar +

    + IconSidebar Compact +

    + If on, decreases padding on IconSidebar +

    { }} />
    - {/*
    +
    -

    Dynamic Markdown Heading

    +

    + Dynamic Markdown Heading +

    Beta
    -

    +

    Allows you to manually change header markdown on hover

    @@ -90,11 +93,54 @@ const CreateAppearanceSection: React.FC = () => { window.electronStore.setDisplayMarkdown(!displayMarkdown); } }} - disabled={false} + disabled={true} /> -
    */} +
    ); }; -export default CreateAppearanceSection; +export const CreateEditorSection = () => { + const [editorFlexCenter, setEditorFlexCenter] = useState(true); + + // Check if we should have flex center for our editor + useEffect(() => { + const fetchParams = async () => { + const editorFlexCenter = await window.electronStore.getEditorFlexCenter(); + + if (editorFlexCenter !== undefined) { + setEditorFlexCenter(editorFlexCenter); + } + }; + + fetchParams(); + }, []); + + return ( +
    +

    + Editor +

    +
    +
    +

    + Content Flex Center +

    + If on, centers content inside editor +

    +

    +
    + { + setEditorFlexCenter(!editorFlexCenter); + if (editorFlexCenter !== undefined) { + console.log("editorFlexCenter on change:", editorFlexCenter); + window.electronStore.setEditorFlexCenter(!editorFlexCenter); + } + }} + /> +
    +
    + ); +}; diff --git a/src/components/Settings/GeneralSettings.tsx b/src/components/Settings/GeneralSettings.tsx index 33720faa..5f3d10d1 100644 --- a/src/components/Settings/GeneralSettings.tsx +++ b/src/components/Settings/GeneralSettings.tsx @@ -5,7 +5,11 @@ import Switch from "@mui/material/Switch"; import { useFileByFilepath } from "../File/hooks/use-file-by-filepath"; -import CreateAppearanceSection, { GenSettingsProps } from "./GeneralSections"; +import { + CreateAppearanceSection, + CreateEditorSection, + GenSettingsProps, +} from "./GeneralSections"; /* * General Page has the following format: @@ -25,128 +29,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]); - - const handleSave = () => { - // Execute the save function here - window.electronStore.setSpellCheckMode(tempSpellCheckEnabled); - setSpellCheckEnabled(tempSpellCheckEnabled); - setUserHasMadeUpdate(false); - }; +const GeneralSettings: React.FC = ({}) => { 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/Settings.tsx b/src/components/Settings/Settings.tsx index bf6b4344..ad8bc012 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -22,14 +22,22 @@ enum SettingsTab { ANALYTICS = "analytics", } +enum SettingsTab { + GeneralSettings = "generalSettings", + LLMSettings = "llmSettings", + EmbeddingModel = "embeddingModel", + TextGeneration = "textGeneration", + RAG = "RAG", + ANALYTICS = "analytics", + ChunkSize = "chunkSize", +} + const SettingsModal: React.FC = ({ isOpen, onClose: onCloseFromParent, }) => { const [willNeedToReIndex, setWillNeedToReIndex] = useState(false); - const [activeTab, setActiveTab] = useState( - SettingsTab.LLMSettings - ); + const [activeTab, setActiveTab] = useState("generalSettings"); const handleSave = () => { if (willNeedToReIndex) { @@ -49,8 +57,8 @@ const SettingsModal: React.FC = ({ }} >
    -
    - {/*
    +
    = ({ onClick={() => setActiveTab(SettingsTab.GeneralSettings)} > General -
    */} +
    = ({ > Text Generation{" "}
    - {/*
    = ({ onClick={() => setActiveTab(SettingsTab.RAG)} > RAG{" "} -
    */} +
    = ({ > Analytics{" "}
    -
    setActiveTab(SettingsTab.GeneralSettings)} - > - General{" "} -
    {/* Right Content Area */} @@ -156,7 +154,7 @@ const SettingsModal: React.FC = ({
    )} - {/* {activeTab === SettingsTab.RAG && ( + {activeTab === SettingsTab.RAG && (

    RAG

    {" "} @@ -170,12 +168,6 @@ const SettingsModal: React.FC = ({

    - )} */} - - {activeTab === SettingsTab.GeneralSettings && ( -
    - -
    )}
    diff --git a/src/components/Sidebars/IconsSidebar.tsx b/src/components/Sidebars/IconsSidebar.tsx index 9b9991be..77e4aebd 100644 --- a/src/components/Sidebars/IconsSidebar.tsx +++ b/src/components/Sidebars/IconsSidebar.tsx @@ -87,12 +87,14 @@ const IconsSidebar: React.FC = ({ onClick={() => makeSidebarShow("files")} >
    @@ -103,12 +105,12 @@ const IconsSidebar: React.FC = ({ onClick={() => makeSidebarShow("chats")} >
    = ({ onClick={() => makeSidebarShow("search")} >
    = ({ onClick={() => setIsNewNoteModalOpen(true)} >
    - + + +
    = ({ onClick={() => setIsNewDirectoryModalOpen(true)} >
    - - {/* < /> */} + + +
    = ({ onClick={() => setIsFlashcardModeOpen(true)} >
    - - {/* < /> */} + + +
    @@ -200,17 +206,25 @@ const IconsSidebar: React.FC = ({ className="bg-transparent border-none pb-2 mb-[2px] cursor-pointer flex items-center justify-center w-full" onClick={() => window.electronUtils.openNewWindow()} > - + + +
    ); From 47667e26f9a5a011a1ea6508a909cf92d8d1fe0c Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Mon, 8 Jul 2024 22:25:48 -0500 Subject: [PATCH 09/40] Merging UI and Tab Support --- electron/main/electron-store/ipcHandlers.ts | 56 ++++++++++++++ electron/main/electron-store/storeConfig.ts | 11 +++ electron/preload/index.ts | 7 +- src/components/Settings/GeneralSections.tsx | 1 - src/components/Sidebars/TabSidebar.tsx | 86 +++++++++++++++++++++ 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 src/components/Sidebars/TabSidebar.tsx diff --git a/electron/main/electron-store/ipcHandlers.ts b/electron/main/electron-store/ipcHandlers.ts index a506c995..1d2151f7 100644 --- a/electron/main/electron-store/ipcHandlers.ts +++ b/electron/main/electron-store/ipcHandlers.ts @@ -269,6 +269,62 @@ export const registerStoreHandlers = ( chatHistoriesMap[vaultDir] = filteredChatHistories.reverse(); store.set(StoreKeys.ChatHistories, chatHistoriesMap); }); + + ipcMain.handle("get-current-open-files", () => { + return store.get(StoreKeys.OpenTabs) || []; + }); + + ipcMain.handle("set-current-open-files", (event, { action, args }) => { + const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || []; + + const addTab = ({ tab }) => { + if (tab === null) return; + 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); + }; + + const removeTab = ({ tabId }) => { + const updatedTabs = openTabs.filter((tab) => tab.id !== tabId); + store.set(StoreKeys.OpenTabs, updatedTabs); + }; + + const clearAllTabs = () => { + store.set(StoreKeys.OpenTabs, []); + }; + + const updateTab = ({ draggedIndex, targetIndex }) => { + // Swap dragged and target + [openTabs[draggedIndex], openTabs[targetIndex]] = [ + openTabs[targetIndex], + openTabs[draggedIndex], + ]; + store.set(StoreKeys.OpenTabs, openTabs); + }; + + switch (action) { + case "add": + addTab(args); + break; + case "remove": + removeTab(args); + break; + case "update": + updateTab(args); + break; + case "clear": + clearAllTabs(); + break; + default: + throw new Error("Unsupported action type"); + } + }); }; export function getDefaultEmbeddingModelConfig( diff --git a/electron/main/electron-store/storeConfig.ts b/electron/main/electron-store/storeConfig.ts index 5bf2a901..15835355 100644 --- a/electron/main/electron-store/storeConfig.ts +++ b/electron/main/electron-store/storeConfig.ts @@ -50,6 +50,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 + timeOpened: Date; // Timestamp to preserve order + isDirty: boolean; // Flag to indicate unsaved changes + lastAccessed: Date; // Timestamp for the last access (possibly used for future features) +}; + export interface StoreSchema { hasUserOpenedAppBefore: boolean; schemaVersion: number; @@ -75,6 +84,7 @@ export interface StoreSchema { DisplayMarkdown: boolean; spellCheck: string; EditorFlexCenter: boolean; + OpenTabs: Tab[]; } export enum StoreKeys { @@ -95,4 +105,5 @@ export enum StoreKeys { DisplayMarkdown = "DisplayMarkdown", SpellCheck = "spellCheck", EditorFlexCenter = "editorFlexCenter", + OpenTabs = "OpenTabs", } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index e9f06016..89b076e8 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -168,7 +168,6 @@ const electronStore = { createIPCHandler<(chatID: string) => Promise>( "get-chat-history" ), - getSBCompact: createIPCHandler<() => Promise>("get-sb-compact"), setSBCompact: createIPCHandler<(isSBCompact: boolean) => Promise>("set-sb-compact"), @@ -185,6 +184,12 @@ const electronStore = { setEditorFlexCenter: createIPCHandler< (editorFlexCenter: boolean) => Promise >("set-editor-flex-center"), + getCurrentOpenFiles: createIPCHandler<() => Promise>( + "get-current-open-files" + ), + setCurrentOpenFiles: createIPCHandler< + (action: any, args: any) => Promise + >("set-current-open-files"), }; const fileSystem = { diff --git a/src/components/Settings/GeneralSections.tsx b/src/components/Settings/GeneralSections.tsx index c36a6004..7d3f7214 100644 --- a/src/components/Settings/GeneralSections.tsx +++ b/src/components/Settings/GeneralSections.tsx @@ -135,7 +135,6 @@ export const CreateEditorSection = () => { onChange={() => { setEditorFlexCenter(!editorFlexCenter); if (editorFlexCenter !== undefined) { - console.log("editorFlexCenter on change:", editorFlexCenter); window.electronStore.setEditorFlexCenter(!editorFlexCenter); } }} diff --git a/src/components/Sidebars/TabSidebar.tsx b/src/components/Sidebars/TabSidebar.tsx new file mode 100644 index 00000000..e5f876b4 --- /dev/null +++ b/src/components/Sidebars/TabSidebar.tsx @@ -0,0 +1,86 @@ +import React from "react"; + +interface DraggableTabsProps { + openTabs: Tab[]; + setOpenTabs: (openTabs: Tab[]) => void; + onTabSelect: (path: string) => void; + onTabClose: (event: any, tabId: string) => void; + currentFilePath: string; +} + +const DraggableTabs: React.FC = ({ + openTabs, + setOpenTabs, + onTabSelect, + onTabClose, + currentFilePath, +}) => { + console.log("OpenTabs:", openTabs); + const onDragStart = (event: any, tabId: string) => { + event.dataTransfer.setData("tabId", tabId); + }; + + const onDrop = (event: any) => { + const draggedTabId = event.dataTransfer.getData("tabId"); + const targetTabId = event.target.getAttribute("data-tabid"); + const newTabs = [...openTabs]; + const draggedTab = newTabs.find((tab) => tab.id === draggedTabId); + const targetIndex = newTabs.findIndex((tab) => tab.id === targetTabId); + const draggedIndex = newTabs.indexOf(draggedTab); + + newTabs.splice(draggedIndex, 1); // Remove the dragged tab + newTabs.splice(targetIndex, 0, draggedTab); // Insert at the new index + + console.log(`Dragged: ${draggedIndex}, Target: ${targetIndex}`); + syncTabsWithBackend(draggedIndex, targetIndex); + setOpenTabs(newTabs); + }; + + /* Sync New tab update with backened */ + const syncTabsWithBackend = (draggedIndex: number, targetIndex: number) => { + window.electronStore.setCurrentOpenFiles("update", { + draggedIndex: draggedIndex, + targetIndex: targetIndex, + }); + }; + + const onDragOver = (event: any) => { + event.preventDefault(); + }; + + return ( +
    + {openTabs.map((tab) => ( +
    +
    onDragStart(event, tab.id)} + onDrop={onDrop} + onDragOver={onDragOver} + className={`py-4 px-2 text-white cursor-pointer flex justify-center gap-1 items-center text-sm ${ + currentFilePath === tab.filePath ? "bg-dark-gray-c-three" : "" + }`} + onClick={() => onTabSelect(tab.filePath)} + > + {tab.title} + { + e.stopPropagation(); // Prevent triggering onClick of parent div + onTabClose(e, tab.id); + }} + > + × + +
    +
    + ))} +
    + ); +}; + +export { DraggableTabs }; From 7c9a9b9bbe470c8ac4858f83a37cd6e9809dc324 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Mon, 8 Jul 2024 23:56:05 -0500 Subject: [PATCH 10/40] Merged with add-tab-support --- package-lock.json | 23 +++++++++++++++++++ package.json | 3 ++- .../File/hooks/use-file-by-filepath.ts | 2 -- src/components/MainPage.tsx | 4 ++-- .../{ => Sidebars}/DraggableTabs.tsx | 0 5 files changed, 27 insertions(+), 5 deletions(-) rename src/components/{ => Sidebars}/DraggableTabs.tsx (100%) diff --git a/package-lock.json b/package-lock.json index 99208e7c..0b2cd2c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "turndown": "^7.1.2", "use-debounce": "^10.0.1", "uuid": "^10.0.0", + "uuidv4": "^6.2.13", "vectordb": "0.4.10" }, "devDependencies": { @@ -18615,6 +18616,28 @@ "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" + } + }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "dev": true, diff --git a/package.json b/package.json index 3a98d62e..ca38d410 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "turndown": "^7.1.2", "use-debounce": "^10.0.1", "uuid": "^10.0.0", + "uuidv4": "^6.2.13", "vectordb": "0.4.10" }, "devDependencies": { @@ -155,4 +156,4 @@ "node" ] } -} \ No newline at end of file +} diff --git a/src/components/File/hooks/use-file-by-filepath.ts b/src/components/File/hooks/use-file-by-filepath.ts index 21eb3350..10feba72 100644 --- a/src/components/File/hooks/use-file-by-filepath.ts +++ b/src/components/File/hooks/use-file-by-filepath.ts @@ -24,7 +24,6 @@ import HighlightExtension, { } from "@/components/Editor/HighlightExtension"; import { RichTextLink } from "@/components/Editor/RichTextLink"; import SearchAndReplace from "@/components/Editor/SearchAndReplace"; -import OpenQueryTab from "@/components/Editor/LLMQueryTab"; import { getInvalidCharacterInFilePath, removeFileExtension, @@ -206,7 +205,6 @@ export const useFileByFilepath = () => { openRelativePathRef, handleSuggestionsStateWithEventCapture ), - OpenQueryTab(setShowQueryBox), ], }); diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index 3a6de0b0..aeb3e91b 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react"; import posthog from "posthog-js"; +import { v4 as uuidv4 } from "uuid"; import "../styles/global.css"; import ChatWithLLM, { ChatFilters, ChatHistory } from "./Chat/Chat"; @@ -8,7 +9,7 @@ import { useChatHistory } from "./Chat/hooks/use-chat-history"; import ResizableComponent from "./Common/ResizableComponent"; import TitleBar from "./Common/TitleBar"; import EditorManager from "./Editor/EditorManager"; -import { DraggableTabs } from ./Sidebars/DraggableTabs.tsx +import { DraggableTabs } from "./Sidebars/DraggableTabs.tsx"; import { useFileInfoTree } from "./File/FileSideBar/hooks/use-file-info-tree"; import CreatePreviewFile from "./File/PreviewFile"; import { useFileByFilepath } from "./File/hooks/use-file-by-filepath"; @@ -16,7 +17,6 @@ import IconsSidebar from "./Sidebars/IconsSidebar"; import SidebarManager from "./Sidebars/MainSidebar"; import SimilarFilesSidebarComponent from "./Sidebars/SimilarFilesSidebar"; -interface FileEditorContainerProps {} interface FileEditorContainerProps {} export type SidebarAbleToShow = "files" | "search" | "chats"; diff --git a/src/components/DraggableTabs.tsx b/src/components/Sidebars/DraggableTabs.tsx similarity index 100% rename from src/components/DraggableTabs.tsx rename to src/components/Sidebars/DraggableTabs.tsx From 4f34822e9df9536dfd140f3645d4c4f0b51711b3 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Tue, 9 Jul 2024 12:44:50 -0500 Subject: [PATCH 11/40] Added Tab Support with more UI Changes --- src/components/Common/ResizableComponent.tsx | 16 ++- src/components/Common/TitleBar.tsx | 113 +++++++++++++++++- .../File/hooks/use-file-by-filepath.ts | 3 - src/components/MainPage.tsx | 99 ++------------- src/components/Sidebars/DraggableTabs.tsx | 9 +- src/styles/global.css | 10 +- 6 files changed, 149 insertions(+), 101 deletions(-) diff --git a/src/components/Common/ResizableComponent.tsx b/src/components/Common/ResizableComponent.tsx index 54c071a8..746ad69d 100644 --- a/src/components/Common/ResizableComponent.tsx +++ b/src/components/Common/ResizableComponent.tsx @@ -4,15 +4,18 @@ interface ResizableComponentProps { children: React.ReactNode; initialWidth?: number; resizeSide: "left" | "right" | "both"; + onResize: (width: number) => void; } const ResizableComponent: React.FC = ({ children, initialWidth = 200, // Default value if not provided resizeSide, + onResize, }) => { const [width, setWidth] = useState(initialWidth); const [isDragging, setIsDragging] = useState(false); + const setMaximumWidthCap = 400; const startDragging = useCallback((e: React.MouseEvent) => { e.preventDefault(); @@ -23,12 +26,22 @@ const ResizableComponent: React.FC = ({ (e: MouseEvent) => { if (isDragging) { const deltaWidth = resizeSide === "left" ? -e.movementX : e.movementX; - setWidth((prevWidth) => prevWidth + deltaWidth); + setWidth((prevWidth) => { + const newWidth = prevWidth + deltaWidth; + if (newWidth > setMaximumWidthCap) return setMaximumWidthCap; + if (newWidth < 50) return 50; + return newWidth; + }); } }, [isDragging, resizeSide] ); + useEffect(() => { + console.log("Setting resize to:", width); + if (onResize) onResize(width); + }, [width]); + const stopDragging = useCallback(() => { setIsDragging(false); }, []); @@ -65,6 +78,7 @@ const ResizableComponent: React.FC = ({ width: `${width}px`, resize: "none", overflow: "auto", + maxWidth: `${setMaximumWidthCap}px`, position: "relative", height: "100%", }} diff --git a/src/components/Common/TitleBar.tsx b/src/components/Common/TitleBar.tsx index 60ba91de..e192c704 100644 --- a/src/components/Common/TitleBar.tsx +++ b/src/components/Common/TitleBar.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; - +import { v4 as uuidv4 } from "uuid"; import { PiSidebar, PiSidebarFill } from "react-icons/pi"; - +import { DraggableTabs } from "../Sidebars/DraggableTabs"; import FileHistoryNavigator from "../File/FileSideBar/FileHistoryBar"; export const titleBarHeight = "30px"; @@ -13,6 +13,10 @@ interface TitleBarProps { toggleSimilarFiles: () => void; history: string[]; setHistory: (string: string[]) => void; + openTabs: Tab[]; + setOpenTabs: (string: Tab[]) => void; + openFileAndOpenEditor: (path: string) => void; + sidebarWidth: number; } const TitleBar: React.FC = ({ @@ -22,9 +26,26 @@ const TitleBar: React.FC = ({ toggleSimilarFiles, history, setHistory, + openTabs, + setOpenTabs, + openFileAndOpenEditor, + sidebarWidth, }) => { const [platform, setPlatform] = useState(""); + useEffect(() => { + if (!currentFilePath) return; + const existingTab = openTabs.find( + (tab) => tab.filePath === currentFilePath + ); + + if (!existingTab) { + syncTabsWithBackend(currentFilePath); + const newTab = createTabObjectFromPath(currentFilePath); + setOpenTabs((prevTabs) => [...prevTabs, newTab]); + } + }, [currentFilePath]); + useEffect(() => { const fetchPlatform = async () => { const response = await window.electronUtils.getPlatform(); @@ -34,11 +55,72 @@ const TitleBar: React.FC = ({ fetchPlatform(); }, []); + useEffect(() => { + const fetchHistoryTabs = async () => { + const response = await window.electronStore.getCurrentOpenFiles(); + setOpenTabs(response); + }; + + fetchHistoryTabs(); + }, []); + + const handleTabSelect = (path: string) => { + openFileAndOpenEditor(path); + }; + + const handleTabClose = async (event, tabId) => { + event.stopPropagation(); + console.log("Closing tab!"); + let closedFilePath = ""; + let newIndex = -1; + + setOpenTabs((prevTabs) => { + const index = prevTabs.findIndex((tab) => tab.id === tabId); + closedFilePath = index !== -1 ? prevTabs[index].filePath : ""; + newIndex = index > 0 ? index - 1 : 0; + return prevTabs.filter((tab, idx) => idx !== index); + }); + + if (closedFilePath === filePath) { + if (newIndex === -1 || newIndex >= openTabs.length) { + openFileAndOpenEditor(""); // If no tabs left or out of range, clear selection + } else { + openFileAndOpenEditor(openTabs[newIndex].filePath); // Select the new index's file + } + } + await window.electronStore.setCurrentOpenFiles("remove", { + tabId: tabId, + }); + }; + + const syncTabsWithBackend = async (path: string) => { + const tab = createTabObjectFromPath(path); + await window.electronStore.setCurrentOpenFiles("add", { + tab: tab, + }); + }; + + 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 + }; + + const createTabObjectFromPath = (path: string) => { + return { + id: uuidv4(), + filePath: path, + title: extractFileName(path), + timeOpened: new Date(), + isDirty: false, + lastAccessed: new Date(), + }; + }; + return (
    = ({ } >
    +
    +
    + +
    +
    +
    = ({ > {similarFilesOpen ? ( = ({ /> ) : ( { }); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [displayMarkdown, setDisplayMarkdown] = useState(false); - const [showQueryBox, setShowQueryBox] = useState(false); const setFileNodeToBeRenamed = async (filePath: string) => { const isDirectory = await window.fileSystem.isDirectory(filePath); @@ -370,8 +369,6 @@ export const useFileByFilepath = () => { filePath: currentlyOpenedFilePath, saveCurrentlyOpenedFile, editor, - showQueryBox, - setShowQueryBox, openTabs, setOpenTabs, navigationHistory, diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index aeb3e91b..e6200c3d 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import posthog from "posthog-js"; -import { v4 as uuidv4 } from "uuid"; import "../styles/global.css"; import ChatWithLLM, { ChatFilters, ChatHistory } from "./Chat/Chat"; @@ -9,7 +8,6 @@ import { useChatHistory } from "./Chat/hooks/use-chat-history"; import ResizableComponent from "./Common/ResizableComponent"; import TitleBar from "./Common/TitleBar"; import EditorManager from "./Editor/EditorManager"; -import { DraggableTabs } from "./Sidebars/DraggableTabs.tsx"; import { useFileInfoTree } from "./File/FileSideBar/hooks/use-file-info-tree"; import CreatePreviewFile from "./File/PreviewFile"; import { useFileByFilepath } from "./File/hooks/use-file-by-filepath"; @@ -28,8 +26,6 @@ const FileEditorContainer: React.FC = () => { const { filePath, editor, - showQueryBox, - setShowQueryBox, openTabs, setOpenTabs, openFileByPath, @@ -74,6 +70,7 @@ const FileEditorContainer: React.FC = () => { maxDate: new Date(), }); const [sidebarWidth, setSidebarWidth] = useState(40); + const [resizableWidth, setResizableWidth] = useState(300); const handleAddFileToChatFilters = (file: string) => { setSidebarShowing("chats"); @@ -86,6 +83,10 @@ const FileEditorContainer: React.FC = () => { })); }; + const handleResize = (size) => { + setResizableWidth(size); + }; + // find all available files useEffect(() => { const updateWidth = async () => { @@ -122,90 +123,6 @@ const FileEditorContainer: React.FC = () => { }; }, []); - useEffect(() => { - const fetchHistoryTabs = async () => { - const response = await window.electronStore.getCurrentOpenFiles(); - setOpenTabs(response); - console.log(`Fetching stored history: ${JSON.stringify(openTabs)}`); - }; - - fetchHistoryTabs(); - }, []); - - /* IPC Communication for Tab updates */ - const syncTabsWithBackend = async (path: string) => { - /* Deals with already open files */ - const tab = createTabObjectFromPath(path); - await window.electronStore.setCurrentOpenFiles("add", { - tab: tab, - }); - }; - - 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 - }; - - /* Creates Tab to display */ - const createTabObjectFromPath = (path) => { - return { - id: uuidv4(), - filePath: path, - title: extractFileName(path), - timeOpened: new Date(), - isDirty: false, - lastAccessed: new Date(), - }; - }; - - useEffect(() => { - if (!filePath) return; - console.log(`Filepath changed!`); - const existingTab = openTabs.find((tab) => tab.filePath === filePath); - - if (!existingTab) { - syncTabsWithBackend(filePath); - const newTab = createTabObjectFromPath(filePath); - // Update the tabs state by adding the new tab - setOpenTabs((prevTabs) => [...prevTabs, newTab]); - } - setShowQueryBox(false); - }, [filePath]); - - const handleTabSelect = (path: string) => { - console.log("Tab Selected:", path); - openFileAndOpenEditor(path); - }; - - const handleTabClose = async (event, tabId) => { - // Get current file path from the tab to be closed - event.stopPropagation(); - console.log("Closing tab!"); - let closedFilePath = ""; - let newIndex = -1; - - // Update tabs state and determine the new file to select - setOpenTabs((prevTabs) => { - const index = prevTabs.findIndex((tab) => tab.id === tabId); - closedFilePath = index !== -1 ? prevTabs[index].filePath : ""; - newIndex = index > 0 ? index - 1 : 0; // Set newIndex to previous one or 0 - return prevTabs.filter((tab, idx) => idx !== index); - }); - - // Update the selected file path after state update - if (closedFilePath === filePath) { - // If the closed tab was the current file, update the file selection - if (newIndex === -1 || newIndex >= openTabs.length) { - openFileAndOpenEditor(""); // If no tabs left or out of range, clear selection - } else { - openFileAndOpenEditor(openTabs[newIndex].filePath); // Select the new index's file - } - } - await window.electronStore.setCurrentOpenFiles("remove", { - tabId: tabId, - }); - }; - return (
    = () => { onFileSelect={openFileAndOpenEditor} similarFilesOpen={showSimilarFiles} // This might need to be managed differently now toggleSimilarFiles={toggleSimilarFiles} // This might need to be managed differently now + openTabs={openTabs} + setOpenTabs={setOpenTabs} + openFileAndOpenEditor={openFileAndOpenEditor} + sidebarWidth={resizableWidth} />
    @@ -230,7 +151,7 @@ const FileEditorContainer: React.FC = () => { />
    - +
    = ({
    {openTabs.map((tab) => (
    = ({ onDragStart={(event) => onDragStart(event, tab.id)} onDrop={onDrop} onDragOver={onDragOver} - className={`py-4 px-2 text-white cursor-pointer flex justify-center gap-1 items-center text-sm ${ - currentFilePath === tab.filePath ? "bg-dark-gray-c-three" : "" + className={`py-2 px-2 text-white cursor-pointer flex justify-center gap-1 items-center text-sm ${ + currentFilePath === tab.filePath + ? "bg-dark-gray-c-three rounded-md" + : "rounded-md" }`} onClick={() => onTabSelect(tab.filePath)} > diff --git a/src/styles/global.css b/src/styles/global.css index 85a869cb..44e01c28 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; + height: 20px; + padding-top: 30px; +} + button { -webkit-app-region: no-drag; } From 13ae394d2c2f3722d9dccbb8a86c388acad34206 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Wed, 10 Jul 2024 01:27:29 -0500 Subject: [PATCH 12/40] More changes to UI. Added Scrolling --- src/components/Common/TitleBar.tsx | 21 +++++++++--------- src/components/Sidebars/DraggableTabs.tsx | 26 ++++++++++++++++------- src/styles/global.css | 26 ++++++++++++++++++----- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/components/Common/TitleBar.tsx b/src/components/Common/TitleBar.tsx index e192c704..a5578586 100644 --- a/src/components/Common/TitleBar.tsx +++ b/src/components/Common/TitleBar.tsx @@ -140,20 +140,21 @@ const TitleBar: React.FC = ({
    - +
    + +
    diff --git a/src/components/Sidebars/DraggableTabs.tsx b/src/components/Sidebars/DraggableTabs.tsx index 71ab9b46..e9360157 100644 --- a/src/components/Sidebars/DraggableTabs.tsx +++ b/src/components/Sidebars/DraggableTabs.tsx @@ -1,4 +1,6 @@ import React from "react"; +import { removeFileExtension } from "@/utils/strings"; +import { FaPlus } from "react-icons/fa6"; interface DraggableTabsProps { openTabs: Tab[]; @@ -49,7 +51,7 @@ const DraggableTabs: React.FC = ({ }; return ( -
    +
    {openTabs.map((tab) => (
    = ({ onDragStart={(event) => onDragStart(event, tab.id)} onDrop={onDrop} onDragOver={onDragOver} - className={`py-2 px-2 text-white cursor-pointer flex justify-center gap-1 items-center text-sm ${ - currentFilePath === tab.filePath - ? "bg-dark-gray-c-three rounded-md" - : "rounded-md" - }`} + className={`relative py-2 px-2 text-white cursor-pointer flex justify-between gap-1 items-center text-sm w-[150px] + ${ + currentFilePath === tab.filePath + ? "bg-dark-gray-c-three rounded-md" + : "rounded-md" + }`} onClick={() => onTabSelect(tab.filePath)} > - {tab.title} + {removeFileExtension(tab.title)} { e.stopPropagation(); // Prevent triggering onClick of parent div onTabClose(e, tab.id); @@ -82,6 +85,13 @@ const DraggableTabs: React.FC = ({
    ))} +
    console.log("Add button clicked")} + > + +
    ); }; diff --git a/src/styles/global.css b/src/styles/global.css index 44e01c28..2c90a7cd 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -39,8 +39,8 @@ a { #titleBarSingleTab { -webkit-app-region: no-drag; - height: 20px; - padding-top: 30px; + padding-top: 25px; + padding-bottom: 25px; } button { @@ -54,20 +54,36 @@ button { ::-webkit-scrollbar { width: 7px; - /* Adjust the width of the scrollbar */ } ::-webkit-scrollbar-track { background: transparent; - /* Make the scrollbar track transparent */ } ::-webkit-scrollbar-thumb { background: rgb(112, 112, 112); - /* Set the thumb color */ border-radius: 10px; } +/* X Scrollbar */ +.scrollable-x-thin::-webkit-scrollbar { + height: 3px; +} + +.scrollable-x-thin::-webkit-scrollbar-track { + background: transparent; +} + +.scrollable-x-thin::-webkit-scrollbar-thumb { + background-color: rgba(95, 125, 139, 0.5); + border-radius: 2px; +} + +.scrollable-x-thin { + scrollbar-width: thin; + scrollbar-color: rgba(75, 85, 99, 0.5) transparent; +} + From 9b456f5c43c338a236c709b0a34f4add596ee18b Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Fri, 12 Jul 2024 12:54:46 -0500 Subject: [PATCH 13/40] Tab better UI.Does not display + on null tabs. Shifts tabs on delete --- electron/main/electron-store/ipcHandlers.ts | 3 +- electron/main/electron-utils/ipcHandlers.ts | 19 +++++++++--- electron/preload/index.ts | 15 ++++++++- src/components/Common/TitleBar.tsx | 18 ++++++----- src/components/Editor/EditorManager.tsx | 12 ++++---- src/components/EmptyPage.tsx | 34 +++++++++++++++++++++ src/components/MainPage.tsx | 7 ++++- src/components/Sidebars/DraggableTabs.tsx | 19 +++++++----- src/components/Sidebars/IconsSidebar.tsx | 4 +-- 9 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 src/components/EmptyPage.tsx diff --git a/electron/main/electron-store/ipcHandlers.ts b/electron/main/electron-store/ipcHandlers.ts index 1d2151f7..9e34a378 100644 --- a/electron/main/electron-store/ipcHandlers.ts +++ b/electron/main/electron-store/ipcHandlers.ts @@ -274,7 +274,7 @@ export const registerStoreHandlers = ( return store.get(StoreKeys.OpenTabs) || []; }); - ipcMain.handle("set-current-open-files", (event, { action, args }) => { + ipcMain.handle("set-current-open-files", (event, action, args) => { const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || []; const addTab = ({ tab }) => { @@ -308,6 +308,7 @@ export const registerStoreHandlers = ( store.set(StoreKeys.OpenTabs, openTabs); }; + console.log("action: ", action); switch (action) { case "add": addTab(args); diff --git a/electron/main/electron-utils/ipcHandlers.ts b/electron/main/electron-utils/ipcHandlers.ts index 48d9f1f3..95b156e5 100644 --- a/electron/main/electron-utils/ipcHandlers.ts +++ b/electron/main/electron-utils/ipcHandlers.ts @@ -12,6 +12,7 @@ import { import Store from "electron-store"; import WindowsManager from "../common/windowManager"; +import { handleAddNewNoteResponse } from "../common/newFiles"; import { StoreKeys, StoreSchema } from "../electron-store/storeConfig"; export const electronUtilsHandlers = ( @@ -28,7 +29,7 @@ export const electronUtilsHandlers = ( new MenuItem({ label: "New Note", click: () => { - event.sender.send("add-new-note-listener"); + event.sender.send("add-new-note-response"); }, }) ); @@ -37,7 +38,7 @@ export const electronUtilsHandlers = ( new MenuItem({ label: "New Directory", click: () => { - event.sender.send("add-new-directory-listener"); + event.sender.send("add-new-directory-response"); }, }) ); @@ -57,7 +58,7 @@ export const electronUtilsHandlers = ( new MenuItem({ label: "New Note", click: () => { - event.sender.send("add-new-note-listener", file.relativePath); + event.sender.send("add-new-note-response", file.relativePath); }, }) ); @@ -66,7 +67,7 @@ export const electronUtilsHandlers = ( new MenuItem({ label: "New Directory", click: () => { - event.sender.send("add-new-directory-listener", file.path); + event.sender.send("add-new-directory-response", file.path); }, }) ); @@ -179,4 +180,14 @@ export const electronUtilsHandlers = ( ipcMain.handle("get-reor-app-version", async () => { return app.getVersion(); }); + + // Used on EmptyPage.tsx to create a new file + ipcMain.handle("empty-new-note-listener", (event, relativePath) => { + event.sender.send("add-new-note-response", relativePath); + }); + + // Used on EmptyPage.tsx to create a new directory + ipcMain.handle("empty-new-directory-listener", (event, relativePath) => { + event.sender.send("add-new-directory-response", relativePath); + }); }; diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 89b076e8..a60e1cc8 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -37,6 +37,13 @@ function createIPCHandler any>( ipcRenderer.invoke(channel, ...args) as Promise>; } +function createIPCHandlerWithChannel any>( + channel: string +): IPCHandler { + return (...args: Parameters) => + ipcRenderer.invoke(channel, ...args) as Promise>; +} + const database = { search: createIPCHandler< @@ -83,6 +90,12 @@ const electronUtils = { showChatItemContext: createIPCHandler< (chatRow: ChatHistoryMetadata) => Promise >("show-chat-menu-item"), + showCreateFileModal: createIPCHandler< + (relativePath: string) => Promise + >("empty-new-note-listener"), + showCreateDirectoryModal: createIPCHandler< + (relativePath: string) => Promise + >("empty-new-directory-listener"), }; const electronStore = { @@ -187,7 +200,7 @@ const electronStore = { getCurrentOpenFiles: createIPCHandler<() => Promise>( "get-current-open-files" ), - setCurrentOpenFiles: createIPCHandler< + setCurrentOpenFiles: createIPCHandlerWithChannel< (action: any, args: any) => Promise >("set-current-open-files"), }; diff --git a/src/components/Common/TitleBar.tsx b/src/components/Common/TitleBar.tsx index a5578586..b6ea2473 100644 --- a/src/components/Common/TitleBar.tsx +++ b/src/components/Common/TitleBar.tsx @@ -8,6 +8,7 @@ export const titleBarHeight = "30px"; interface TitleBarProps { onFileSelect: (path: string) => void; + setFilePath: (path: string) => void; currentFilePath: string | null; similarFilesOpen: boolean; toggleSimilarFiles: () => void; @@ -21,6 +22,7 @@ interface TitleBarProps { const TitleBar: React.FC = ({ onFileSelect, + setFilePath, currentFilePath, similarFilesOpen, toggleSimilarFiles, @@ -77,17 +79,17 @@ const TitleBar: React.FC = ({ setOpenTabs((prevTabs) => { const index = prevTabs.findIndex((tab) => tab.id === tabId); closedFilePath = index !== -1 ? prevTabs[index].filePath : ""; - newIndex = index > 0 ? index - 1 : 0; + newIndex = index > 0 ? index - 1 : 1; + if (closedFilePath === currentFilePath) { + if (newIndex === -1 || newIndex >= openTabs.length) { + openFileAndOpenEditor(""); // If no tabs left or out of range, clear selection + } else { + openFileAndOpenEditor(openTabs[newIndex].filePath); // Select the new index's file + } + } return prevTabs.filter((tab, idx) => idx !== index); }); - if (closedFilePath === filePath) { - if (newIndex === -1 || newIndex >= openTabs.length) { - openFileAndOpenEditor(""); // If no tabs left or out of range, clear selection - } else { - openFileAndOpenEditor(openTabs[newIndex].filePath); // Select the new index's file - } - } await window.electronStore.setCurrentOpenFiles("remove", { tabId: tabId, }); diff --git a/src/components/Editor/EditorManager.tsx b/src/components/Editor/EditorManager.tsx index acce5d7e..6df4b854 100644 --- a/src/components/Editor/EditorManager.tsx +++ b/src/components/Editor/EditorManager.tsx @@ -105,12 +105,12 @@ const EditorManager: React.FC = ({ initEditorContentCenter(); window.ipcRenderer.on("editor-flex-center-changed", handleEditorChange); - return () => { - window.ipcRenderer.removeListener( - "editor-flex-center-changed", - handleEditorChange - ); - }; + // return () => { + // window.ipcRenderer.removeListener( + // "editor-flex-center-changed", + // handleEditorChange + // ); + // }; }, []); return ( diff --git a/src/components/EmptyPage.tsx b/src/components/EmptyPage.tsx new file mode 100644 index 00000000..a5414f85 --- /dev/null +++ b/src/components/EmptyPage.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { ImFileEmpty } from "react-icons/im"; +// import { ipcRenderer } from "electron"; + +const EmptyPage = ({ vaultDirectory }) => { + const handleCreateFile = () => { + window.electronUtils.showCreateFileModal(vaultDirectory); + console.log("Attemtping to create new file!"); + }; + + const handleCreateFolder = () => { + window.electronUtils.showCreateDirectoryModal(vaultDirectory); + }; + + console.log("Empty page vault dir:", vaultDirectory); + + return ( +
    +
    + +
    +

    No File Selected!

    +

    Open a file to begin using Reor!

    +
      +
    • + Create a File +
    • +
    • Create a Folder
    • +
    +
    + ); +}; + +export default EmptyPage; diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index e6200c3d..a9211662 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -14,6 +14,7 @@ import { useFileByFilepath } from "./File/hooks/use-file-by-filepath"; import IconsSidebar from "./Sidebars/IconsSidebar"; import SidebarManager from "./Sidebars/MainSidebar"; import SimilarFilesSidebarComponent from "./Sidebars/SimilarFilesSidebar"; +import EmptyPage from "./EmptyPage"; interface FileEditorContainerProps {} export type SidebarAbleToShow = "files" | "search" | "chats"; @@ -174,7 +175,7 @@ const FileEditorContainer: React.FC = () => {
    - {!showChatbot && filePath && ( + {!showChatbot && filePath ? (
    = () => {
    )}
    + ) : ( +
    + +
    )} {showChatbot && ( diff --git a/src/components/Sidebars/DraggableTabs.tsx b/src/components/Sidebars/DraggableTabs.tsx index e9360157..9b838e27 100644 --- a/src/components/Sidebars/DraggableTabs.tsx +++ b/src/components/Sidebars/DraggableTabs.tsx @@ -17,7 +17,6 @@ const DraggableTabs: React.FC = ({ onTabClose, currentFilePath, }) => { - console.log("OpenTabs:", openTabs); const onDragStart = (event: any, tabId: string) => { event.dataTransfer.setData("tabId", tabId); }; @@ -85,13 +84,17 @@ const DraggableTabs: React.FC = ({
    ))} -
    console.log("Add button clicked")} - > - -
    + {openTabs.length > 0 && ( +
    { + window.electronUtils.showCreateFileModal(); + }} + > + +
    + )}
    ); }; diff --git a/src/components/Sidebars/IconsSidebar.tsx b/src/components/Sidebars/IconsSidebar.tsx index 77e4aebd..555c860d 100644 --- a/src/components/Sidebars/IconsSidebar.tsx +++ b/src/components/Sidebars/IconsSidebar.tsx @@ -61,7 +61,7 @@ const IconsSidebar: React.FC = ({ }; window.ipcRenderer.receive( - "add-new-note-listener", + "add-new-note-response", (relativePath: string) => { handleNewNote(relativePath); } @@ -75,7 +75,7 @@ const IconsSidebar: React.FC = ({ setIsNewDirectoryModalOpen(true); }; - window.ipcRenderer.receive("add-new-directory-listener", (dirPath) => { + window.ipcRenderer.receive("add-new-directory-response", (dirPath) => { handleNewDirectory(dirPath); }); }, []); From b1e17c278827cb46d05e7ae810e6ed40ae62f4f6 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Sat, 13 Jul 2024 11:42:06 -0500 Subject: [PATCH 14/40] Temp save --- electron/main/electron-store/storeConfig.ts | 6 ++-- src/components/Common/TitleBar.tsx | 29 ++++++++++--------- .../File/hooks/use-file-by-filepath.ts | 1 + src/components/MainPage.tsx | 17 +++++++++++ 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/electron/main/electron-store/storeConfig.ts b/electron/main/electron-store/storeConfig.ts index 15835355..5f0b235c 100644 --- a/electron/main/electron-store/storeConfig.ts +++ b/electron/main/electron-store/storeConfig.ts @@ -54,9 +54,9 @@ 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 - timeOpened: Date; // Timestamp to preserve order - isDirty: boolean; // Flag to indicate unsaved changes - lastAccessed: Date; // Timestamp for the last access (possibly used for future features) + lastAccessed: boolean; + // timeOpened: Date; // Timestamp to preserve order + // isDirty: boolean; // Flag to indicate unsaved changes }; export interface StoreSchema { diff --git a/src/components/Common/TitleBar.tsx b/src/components/Common/TitleBar.tsx index b6ea2473..52785073 100644 --- a/src/components/Common/TitleBar.tsx +++ b/src/components/Common/TitleBar.tsx @@ -8,14 +8,14 @@ export const titleBarHeight = "30px"; interface TitleBarProps { onFileSelect: (path: string) => void; - setFilePath: (path: string) => void; - currentFilePath: string | null; + setFilePath: (path: string | null) => void; // Used to set file path to null when no tabs are open + 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; - openTabs: Tab[]; - setOpenTabs: (string: Tab[]) => void; + openTabs: Tab[]; // Current opened tabs + setOpenTabs: (string: Tab[]) => void; // Setter for opened tabs openFileAndOpenEditor: (path: string) => void; sidebarWidth: number; } @@ -42,7 +42,7 @@ const TitleBar: React.FC = ({ ); if (!existingTab) { - syncTabsWithBackend(currentFilePath); + addNewTab(currentFilePath); const newTab = createTabObjectFromPath(currentFilePath); setOpenTabs((prevTabs) => [...prevTabs, newTab]); } @@ -78,14 +78,17 @@ const TitleBar: React.FC = ({ setOpenTabs((prevTabs) => { const index = prevTabs.findIndex((tab) => tab.id === tabId); + prevTabs[index].lastAccessed = false; closedFilePath = index !== -1 ? prevTabs[index].filePath : ""; newIndex = index > 0 ? index - 1 : 1; if (closedFilePath === currentFilePath) { - if (newIndex === -1 || newIndex >= openTabs.length) { - openFileAndOpenEditor(""); // If no tabs left or out of range, clear selection - } else { - openFileAndOpenEditor(openTabs[newIndex].filePath); // Select the new index's file + console.log("new Index:", newIndex); + if (newIndex < openTabs.length) { + openTabs[newIndex].lastAccessed = true; + openFileAndOpenEditor(openTabs[newIndex].filePath); } + // Select the new index's file + else setFilePath(null); } return prevTabs.filter((tab, idx) => idx !== index); }); @@ -95,7 +98,7 @@ const TitleBar: React.FC = ({ }); }; - const syncTabsWithBackend = async (path: string) => { + const addNewTab = async (path: string) => { const tab = createTabObjectFromPath(path); await window.electronStore.setCurrentOpenFiles("add", { tab: tab, @@ -112,9 +115,9 @@ const TitleBar: React.FC = ({ id: uuidv4(), filePath: path, title: extractFileName(path), - timeOpened: new Date(), - isDirty: false, - lastAccessed: new Date(), + lastAccessed: false, + // timeOpened: new Date(), + // isDirty: false, }; }; diff --git a/src/components/File/hooks/use-file-by-filepath.ts b/src/components/File/hooks/use-file-by-filepath.ts index 033bc5d9..b8fb054c 100644 --- a/src/components/File/hooks/use-file-by-filepath.ts +++ b/src/components/File/hooks/use-file-by-filepath.ts @@ -367,6 +367,7 @@ export const useFileByFilepath = () => { return { filePath: currentlyOpenedFilePath, + setFilePath: setCurrentlyOpenedFilePath, saveCurrentlyOpenedFile, editor, openTabs, diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index a9211662..304f7417 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -26,6 +26,7 @@ const FileEditorContainer: React.FC = () => { useState("files"); const { filePath, + setFilePath, editor, openTabs, setOpenTabs, @@ -124,6 +125,21 @@ const FileEditorContainer: React.FC = () => { }; }, []); + // On new launch of the app, sets the last previously accessed tab (if any) to be + // the current file + useEffect(() => { + const restoreFromLaunch = () => { + openTabs.forEach((tab) => { + console.log("Tab:", tab.lastAccessed); + if (tab.lastAccessed) { + openFileByPath(tab.filePath); + } + }); + }; + + restoreFromLaunch(); + }, []); + return (
    = () => { setHistory={setNavigationHistory} currentFilePath={filePath} onFileSelect={openFileAndOpenEditor} + setFilePath={setFilePath} similarFilesOpen={showSimilarFiles} // This might need to be managed differently now toggleSimilarFiles={toggleSimilarFiles} // This might need to be managed differently now openTabs={openTabs} From b62e4e4d16a0ef6e053018475068c5d195e26d59 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Mon, 15 Jul 2024 11:10:44 -0500 Subject: [PATCH 15/40] Mostly complete. Need to add name dropping on hover. --- electron/main/electron-store/ipcHandlers.ts | 8 +- electron/preload/index.ts | 2 +- src/components/Common/ResizableComponent.tsx | 1 - src/components/Common/TitleBar.tsx | 92 +++---------- .../File/hooks/use-file-by-filepath.ts | 3 - src/components/MainPage.tsx | 46 +++---- src/components/Providers/TabProvider.tsx | 121 ++++++++++++++++++ src/components/Sidebars/DraggableTabs.tsx | 102 --------------- src/components/Sidebars/IconsSidebar.tsx | 2 +- src/components/Sidebars/TabSidebar.tsx | 71 +++++----- 10 files changed, 209 insertions(+), 239 deletions(-) create mode 100644 src/components/Providers/TabProvider.tsx delete mode 100644 src/components/Sidebars/DraggableTabs.tsx diff --git a/electron/main/electron-store/ipcHandlers.ts b/electron/main/electron-store/ipcHandlers.ts index 9e34a378..84a3c532 100644 --- a/electron/main/electron-store/ipcHandlers.ts +++ b/electron/main/electron-store/ipcHandlers.ts @@ -308,7 +308,10 @@ export const registerStoreHandlers = ( store.set(StoreKeys.OpenTabs, openTabs); }; - console.log("action: ", action); + const selectTab = ({ tabs }) => { + store.set(StoreKeys.OpenTabs, tabs); + }; + switch (action) { case "add": addTab(args); @@ -319,6 +322,9 @@ export const registerStoreHandlers = ( case "update": updateTab(args); break; + case "select": + selectTab(args); + break; case "clear": clearAllTabs(); break; diff --git a/electron/preload/index.ts b/electron/preload/index.ts index a60e1cc8..63eb37de 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -201,7 +201,7 @@ const electronStore = { "get-current-open-files" ), setCurrentOpenFiles: createIPCHandlerWithChannel< - (action: any, args: any) => Promise + (action: Action, args: Args) => Promise >("set-current-open-files"), }; diff --git a/src/components/Common/ResizableComponent.tsx b/src/components/Common/ResizableComponent.tsx index 746ad69d..959f16f1 100644 --- a/src/components/Common/ResizableComponent.tsx +++ b/src/components/Common/ResizableComponent.tsx @@ -38,7 +38,6 @@ const ResizableComponent: React.FC = ({ ); useEffect(() => { - console.log("Setting resize to:", width); if (onResize) onResize(width); }, [width]); diff --git a/src/components/Common/TitleBar.tsx b/src/components/Common/TitleBar.tsx index 52785073..3c97b5ba 100644 --- a/src/components/Common/TitleBar.tsx +++ b/src/components/Common/TitleBar.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import { PiSidebar, PiSidebarFill } from "react-icons/pi"; -import { DraggableTabs } from "../Sidebars/DraggableTabs"; +import { DraggableTabs } from "../Sidebars/TabSidebar"; import FileHistoryNavigator from "../File/FileSideBar/FileHistoryBar"; - +import { useTabs } from "../Providers/TabProvider"; export const titleBarHeight = "30px"; interface TitleBarProps { @@ -14,8 +14,6 @@ interface TitleBarProps { toggleSimilarFiles: () => void; history: string[]; setHistory: (string: string[]) => void; - openTabs: Tab[]; // Current opened tabs - setOpenTabs: (string: Tab[]) => void; // Setter for opened tabs openFileAndOpenEditor: (path: string) => void; sidebarWidth: number; } @@ -28,24 +26,16 @@ const TitleBar: React.FC = ({ toggleSimilarFiles, history, setHistory, - openTabs, - setOpenTabs, openFileAndOpenEditor, sidebarWidth, }) => { const [platform, setPlatform] = useState(""); + const { openTabs, addTab, selectTab, removeTab, updateTabOrder } = useTabs(); + const [openedLastAccess, setOpenedLastAccess] = useState(false); useEffect(() => { if (!currentFilePath) return; - const existingTab = openTabs.find( - (tab) => tab.filePath === currentFilePath - ); - - if (!existingTab) { - addNewTab(currentFilePath); - const newTab = createTabObjectFromPath(currentFilePath); - setOpenTabs((prevTabs) => [...prevTabs, newTab]); - } + addTab(currentFilePath); }, [currentFilePath]); useEffect(() => { @@ -58,67 +48,27 @@ const TitleBar: React.FC = ({ }, []); useEffect(() => { - const fetchHistoryTabs = async () => { - const response = await window.electronStore.getCurrentOpenFiles(); - setOpenTabs(response); + const setUpLastAccess = () => { + if (!openedLastAccess) { + openTabs.forEach((tab) => { + if (tab.lastAccessed) { + setOpenedLastAccess(true); + openFileAndOpenEditor(tab.filePath); + } + }); + } }; - fetchHistoryTabs(); - }, []); + setUpLastAccess(); + }, [openTabs]); - const handleTabSelect = (path: string) => { - openFileAndOpenEditor(path); + const handleTabSelect = (tab: Tab) => { + selectTab(tab); }; - const handleTabClose = async (event, tabId) => { + const handleTabClose = (event, tabId) => { event.stopPropagation(); - console.log("Closing tab!"); - let closedFilePath = ""; - let newIndex = -1; - - setOpenTabs((prevTabs) => { - const index = prevTabs.findIndex((tab) => tab.id === tabId); - prevTabs[index].lastAccessed = false; - closedFilePath = index !== -1 ? prevTabs[index].filePath : ""; - newIndex = index > 0 ? index - 1 : 1; - if (closedFilePath === currentFilePath) { - console.log("new Index:", newIndex); - if (newIndex < openTabs.length) { - openTabs[newIndex].lastAccessed = true; - openFileAndOpenEditor(openTabs[newIndex].filePath); - } - // Select the new index's file - else setFilePath(null); - } - return prevTabs.filter((tab, idx) => idx !== index); - }); - - await window.electronStore.setCurrentOpenFiles("remove", { - tabId: tabId, - }); - }; - - const addNewTab = async (path: string) => { - const tab = createTabObjectFromPath(path); - await window.electronStore.setCurrentOpenFiles("add", { - tab: tab, - }); - }; - - 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 - }; - - const createTabObjectFromPath = (path: string) => { - return { - id: uuidv4(), - filePath: path, - title: extractFileName(path), - lastAccessed: false, - // timeOpened: new Date(), - // isDirty: false, - }; + removeTab(tabId); }; return ( @@ -154,10 +104,10 @@ const TitleBar: React.FC = ({
    diff --git a/src/components/File/hooks/use-file-by-filepath.ts b/src/components/File/hooks/use-file-by-filepath.ts index b8fb054c..ddc22941 100644 --- a/src/components/File/hooks/use-file-by-filepath.ts +++ b/src/components/File/hooks/use-file-by-filepath.ts @@ -55,7 +55,6 @@ export const useFileByFilepath = () => { const [noteToBeRenamed, setNoteToBeRenamed] = useState(""); const [fileDirToBeRenamed, setFileDirToBeRenamed] = useState(""); const [navigationHistory, setNavigationHistory] = useState([]); - const [openTabs, setOpenTabs] = useState([]); const [currentlyChangingFilePath, setCurrentlyChangingFilePath] = useState(false); const [highlightData, setHighlightData] = useState({ @@ -370,8 +369,6 @@ export const useFileByFilepath = () => { setFilePath: setCurrentlyOpenedFilePath, saveCurrentlyOpenedFile, editor, - openTabs, - setOpenTabs, navigationHistory, setNavigationHistory, openFileByPath, diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index 304f7417..743d92de 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -15,6 +15,7 @@ import IconsSidebar from "./Sidebars/IconsSidebar"; import SidebarManager from "./Sidebars/MainSidebar"; import SimilarFilesSidebarComponent from "./Sidebars/SimilarFilesSidebar"; import EmptyPage from "./EmptyPage"; +import { TabProvider } from "./Providers/TabProvider"; interface FileEditorContainerProps {} export type SidebarAbleToShow = "files" | "search" | "chats"; @@ -28,8 +29,6 @@ const FileEditorContainer: React.FC = () => { filePath, setFilePath, editor, - openTabs, - setOpenTabs, openFileByPath, openRelativePath, saveCurrentlyOpenedFile, @@ -125,36 +124,25 @@ const FileEditorContainer: React.FC = () => { }; }, []); - // On new launch of the app, sets the last previously accessed tab (if any) to be - // the current file - useEffect(() => { - const restoreFromLaunch = () => { - openTabs.forEach((tab) => { - console.log("Tab:", tab.lastAccessed); - if (tab.lastAccessed) { - openFileByPath(tab.filePath); - } - }); - }; - - restoreFromLaunch(); - }, []); - return (
    - + setFilePath={setFilePath} + currentFilePath={filePath} + > + +
    useContext(TabContext); + +export const TabProvider = ({ + children, + openFileAndOpenEditor, + setFilePath, + currentFilePath, + editor, +}) => { + const [openTabs, setOpenTabs] = useState([]); + + useEffect(() => { + const fetchHistoryTabs = async () => { + // await window.electronStore.setCurrentOpenFiles("clear"); + const response = await window.electronStore.getCurrentOpenFiles(); + setOpenTabs(response); + }; + + fetchHistoryTabs(); + }, []); + + const syncTabsWithBackend = async (action, args) => { + await window.electronStore.setCurrentOpenFiles(action, args); + }; + + /* Adds a new tab and syncs it with the backend */ + const addTab = (path: string) => { + const existingTab = openTabs.find((tab) => tab.filePath === path); + if (existingTab) return; + const tab = createTabObjectFromPath(path); + + setOpenTabs((prevTabs) => { + const newTabs = [...prevTabs, tab]; + syncTabsWithBackend("add", { tab: tab }); + return newTabs; + }); + }; + + /* Removes a tab and syncs it with the backend */ + const removeTab = (tabId) => { + let closedFilePath = ""; + let newIndex = -1; + + setOpenTabs((prevTabs) => { + const index = prevTabs.findIndex((tab) => tab.id === tabId); + prevTabs[index].lastAccessed = false; + closedFilePath = index !== -1 ? prevTabs[index].filePath : ""; + newIndex = index > 0 ? index - 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(null); + } + return prevTabs.filter((tab, idx) => idx !== index); + }); + window.electronStore.setCurrentOpenFiles("remove", { + tabId: tabId, + }); + }; + + /* Updates tab order (on drag) and syncs it with backend */ + const updateTabOrder = (draggedIndex, targetIndex) => { + setOpenTabs((prevTabs) => { + const newTabs = [...prevTabs]; + const [draggedTab] = newTabs.splice(draggedIndex, 1); + newTabs.splice(targetIndex, 0, draggedTab); + // console.log(`Dragged ${draggedIndex}, target ${targetIndex}`); + syncTabsWithBackend("update", { + draggedIndex: draggedIndex, + targetIndex: targetIndex, + }); + return newTabs; + }); + }; + + /* Selects a tab and syncs it with the backend */ + const selectTab = (selectedTab) => { + setOpenTabs((prevTabs) => { + const newTabs = prevTabs.map((tab) => ({ + ...tab, + lastAccessed: tab.id === selectedTab.id, + })); + syncTabsWithBackend("select", { tabs: newTabs }); + return newTabs; + }); + openFileAndOpenEditor(selectedTab.filePath); + }; + + 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 + }; + + const createTabObjectFromPath = (path: string) => { + return { + id: uuidv4(), + filePath: path, + title: extractFileName(path), + lastAccessed: true, + // timeOpened: new Date(), + // isDirty: false, + }; + }; + + return ( + + {children} + + ); +}; diff --git a/src/components/Sidebars/DraggableTabs.tsx b/src/components/Sidebars/DraggableTabs.tsx deleted file mode 100644 index 9b838e27..00000000 --- a/src/components/Sidebars/DraggableTabs.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from "react"; -import { removeFileExtension } from "@/utils/strings"; -import { FaPlus } from "react-icons/fa6"; - -interface DraggableTabsProps { - openTabs: Tab[]; - setOpenTabs: (openTabs: Tab[]) => void; - onTabSelect: (path: string) => void; - onTabClose: (event: any, tabId: string) => void; - currentFilePath: string; -} - -const DraggableTabs: React.FC = ({ - openTabs, - setOpenTabs, - onTabSelect, - onTabClose, - currentFilePath, -}) => { - const onDragStart = (event: any, tabId: string) => { - event.dataTransfer.setData("tabId", tabId); - }; - - const onDrop = (event: any) => { - const draggedTabId = event.dataTransfer.getData("tabId"); - const targetTabId = event.target.getAttribute("data-tabid"); - const newTabs = [...openTabs]; - const draggedTab = newTabs.find((tab) => tab.id === draggedTabId); - const targetIndex = newTabs.findIndex((tab) => tab.id === targetTabId); - const draggedIndex = newTabs.indexOf(draggedTab); - - newTabs.splice(draggedIndex, 1); // Remove the dragged tab - newTabs.splice(targetIndex, 0, draggedTab); // Insert at the new index - - console.log(`Dragged: ${draggedIndex}, Target: ${targetIndex}`); - syncTabsWithBackend(draggedIndex, targetIndex); - setOpenTabs(newTabs); - }; - - /* Sync New tab update with backened */ - const syncTabsWithBackend = (draggedIndex: number, targetIndex: number) => { - window.electronStore.setCurrentOpenFiles("update", { - draggedIndex: draggedIndex, - targetIndex: targetIndex, - }); - }; - - const onDragOver = (event: any) => { - event.preventDefault(); - }; - - return ( -
    - {openTabs.map((tab) => ( -
    -
    onDragStart(event, tab.id)} - onDrop={onDrop} - onDragOver={onDragOver} - className={`relative py-2 px-2 text-white cursor-pointer flex justify-between gap-1 items-center text-sm w-[150px] - ${ - currentFilePath === tab.filePath - ? "bg-dark-gray-c-three rounded-md" - : "rounded-md" - }`} - onClick={() => onTabSelect(tab.filePath)} - > - {removeFileExtension(tab.title)} - { - e.stopPropagation(); // Prevent triggering onClick of parent div - onTabClose(e, tab.id); - }} - > - × - -
    -
    - ))} - {openTabs.length > 0 && ( -
    { - window.electronUtils.showCreateFileModal(); - }} - > - -
    - )} -
    - ); -}; - -export { DraggableTabs }; diff --git a/src/components/Sidebars/IconsSidebar.tsx b/src/components/Sidebars/IconsSidebar.tsx index 555c860d..11914ef4 100644 --- a/src/components/Sidebars/IconsSidebar.tsx +++ b/src/components/Sidebars/IconsSidebar.tsx @@ -221,7 +221,7 @@ const IconsSidebar: React.FC = ({ diff --git a/src/components/Sidebars/TabSidebar.tsx b/src/components/Sidebars/TabSidebar.tsx index e5f876b4..77f8d0f1 100644 --- a/src/components/Sidebars/TabSidebar.tsx +++ b/src/components/Sidebars/TabSidebar.tsx @@ -1,47 +1,43 @@ import React from "react"; +import { removeFileExtension } from "@/utils/strings"; +import { FaPlus } from "react-icons/fa6"; interface DraggableTabsProps { openTabs: Tab[]; - setOpenTabs: (openTabs: Tab[]) => void; - onTabSelect: (path: string) => void; + onTabSelect: (tab: Tab) => void; onTabClose: (event: any, tabId: string) => void; currentFilePath: string; + updateTabOrder: (draggedIndex: number, targetIndex: number) => void; } const DraggableTabs: React.FC = ({ openTabs, - setOpenTabs, onTabSelect, onTabClose, currentFilePath, + updateTabOrder, }) => { - console.log("OpenTabs:", openTabs); const onDragStart = (event: any, tabId: string) => { event.dataTransfer.setData("tabId", tabId); }; - const onDrop = (event: any) => { + const onDrop = (event) => { + event.preventDefault(); const draggedTabId = event.dataTransfer.getData("tabId"); - const targetTabId = event.target.getAttribute("data-tabid"); - const newTabs = [...openTabs]; - const draggedTab = newTabs.find((tab) => tab.id === draggedTabId); - const targetIndex = newTabs.findIndex((tab) => tab.id === targetTabId); - const draggedIndex = newTabs.indexOf(draggedTab); - newTabs.splice(draggedIndex, 1); // Remove the dragged tab - newTabs.splice(targetIndex, 0, draggedTab); // Insert at the new index + let target = event.target; + // Iterate each child until we find the one we moved to + while (target && !target.getAttribute("data-tabid")) { + target = target.parentElement; + } - console.log(`Dragged: ${draggedIndex}, Target: ${targetIndex}`); - syncTabsWithBackend(draggedIndex, targetIndex); - setOpenTabs(newTabs); - }; + const targetTabId = target ? target.getAttribute("data-tabid") : null; - /* Sync New tab update with backened */ - const syncTabsWithBackend = (draggedIndex: number, targetIndex: number) => { - window.electronStore.setCurrentOpenFiles("update", { - draggedIndex: draggedIndex, - targetIndex: targetIndex, - }); + if (draggedTabId && targetTabId) { + const draggedIndex = openTabs.findIndex((tab) => tab.id === draggedTabId); + const targetIndex = openTabs.findIndex((tab) => tab.id === targetTabId); + updateTabOrder(draggedIndex, targetIndex); + } }; const onDragOver = (event: any) => { @@ -49,11 +45,12 @@ const DraggableTabs: React.FC = ({ }; return ( -
    +
    {openTabs.map((tab) => (
    = ({ onDragStart={(event) => onDragStart(event, tab.id)} onDrop={onDrop} onDragOver={onDragOver} - className={`py-4 px-2 text-white cursor-pointer flex justify-center gap-1 items-center text-sm ${ - currentFilePath === tab.filePath ? "bg-dark-gray-c-three" : "" - }`} - onClick={() => onTabSelect(tab.filePath)} + className={`relative py-2 px-2 text-white cursor-pointer flex justify-between gap-1 items-center text-sm w-[150px] + ${ + currentFilePath === tab.filePath + ? "bg-dark-gray-c-three rounded-md" + : "rounded-md" + }`} + onClick={() => onTabSelect(tab)} > - {tab.title} + {removeFileExtension(tab.title)} { e.stopPropagation(); // Prevent triggering onClick of parent div onTabClose(e, tab.id); @@ -79,6 +79,17 @@ const DraggableTabs: React.FC = ({
    ))} + {openTabs.length > 0 && ( +
    { + window.electronUtils.showCreateFileModal(); + }} + > + +
    + )}
    ); }; From bc51bd581cdd2d48419d39430ab577e0fe6ccd40 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Mon, 15 Jul 2024 12:25:19 -0500 Subject: [PATCH 16/40] Added displaying tab path when hovering --- src/components/MainPage.tsx | 4 +++ src/components/Sidebars/TabSidebar.tsx | 43 +++++++++++++++++++++++++- src/styles/tab.css | 24 ++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/styles/tab.css diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index 743d92de..2f523eea 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -126,6 +126,10 @@ const FileEditorContainer: React.FC = () => { 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. */} +
    void; } +interface TooltipProps { + filepath: string; + position: { x: number; y: number }; +} + const DraggableTabs: React.FC = ({ openTabs, onTabSelect, @@ -17,6 +24,9 @@ const DraggableTabs: React.FC = ({ currentFilePath, updateTabOrder, }) => { + const [hoveredTab, setHoveredTab] = useState(null); + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); + const onDragStart = (event: any, tabId: string) => { event.dataTransfer.setData("tabId", tabId); }; @@ -44,6 +54,19 @@ const DraggableTabs: React.FC = ({ event.preventDefault(); }; + const handleMouseEnter = (e, tab) => { + const rect = e.currentTarget.getBoundingClientRect(); + setHoveredTab(tab.filePath); + setTooltipPosition({ + x: rect.left - 75, + y: rect.bottom - 5, + }); + }; + + const handleMouseLevel = () => { + setHoveredTab(null); + }; + return (
    {openTabs.map((tab) => ( @@ -51,6 +74,8 @@ const DraggableTabs: React.FC = ({ id="titleBarSingleTab" key={tab.id} className="flex justify-center items-center h-[10px]" + onMouseEnter={(e) => handleMouseEnter(e, tab)} + onMouseLeave={handleMouseLevel} >
    = ({ > × + {hoveredTab === tab.filePath && ( + + )}
    ))} @@ -94,4 +122,17 @@ const DraggableTabs: React.FC = ({ ); }; +/* Displays the filepath when hovering on a tab */ +const Tooltip: React.FC = ({ filepath, position }) => { + return createPortal( +
    + {filepath} +
    , + document.getElementById("tooltip-container") as HTMLElement + ); +}; + export { DraggableTabs }; diff --git a/src/styles/tab.css b/src/styles/tab.css new file mode 100644 index 00000000..a28e5ecd --- /dev/null +++ b/src/styles/tab.css @@ -0,0 +1,24 @@ +/* CSS for dropdown animation onMouseEnter */ +.tab-tooltip { + position: absolute; + /* background-color: #fff; */ + color: white; + padding: 5px 10px; + border-radius: 4px; + white-space: no-wrap; + z-index: 1010; + animation: fadeIn 0.3s ease-in-out; + height: 30px; + width: 150px; + font-size: 14px; + text-align: center; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} From 2e23e7d811a41f11bc72d7b5ea88d447bba675f7 Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Tue, 16 Jul 2024 11:33:20 -0500 Subject: [PATCH 17/40] Complete --- src/components/Common/Modal.tsx | 12 +++++- src/components/Editor/EditorContextMenu.tsx | 2 +- src/components/MainPage.tsx | 8 ++-- src/components/Settings/AnalyticsSettings.tsx | 6 +-- src/components/Settings/Settings.tsx | 42 ++----------------- 5 files changed, 23 insertions(+), 47 deletions(-) diff --git a/src/components/Common/Modal.tsx b/src/components/Common/Modal.tsx index 6ac49260..8a12d23f 100644 --- a/src/components/Common/Modal.tsx +++ b/src/components/Common/Modal.tsx @@ -24,9 +24,16 @@ type ModalWidthType = | "newEmbeddingModel" | "localLLMSetting" | "remoteLLMSetting" - | "indexingProgress"; + | "indexingProgress" + | "settingsContainer"; -type Dimension = "[500px]" | "[750px]" | "[300px]" | "full" | "[850px]"; +type Dimension = + | "[500px]" + | "[750px]" + | "[300px]" + | "full" + | "[850px]" + | "[900px]"; const customDimensionsMap: Record = { newNote: "[500px]", @@ -39,6 +46,7 @@ const customDimensionsMap: Record = { localLLMSetting: "[500px]", remoteLLMSetting: "[500px]", indexingProgress: "[850px]", + settingsContainer: "[900px]", }; const getDimension = (name: ModalWidthType | undefined): Dimension => { diff --git a/src/components/Editor/EditorContextMenu.tsx b/src/components/Editor/EditorContextMenu.tsx index 07416955..4743af9d 100644 --- a/src/components/Editor/EditorContextMenu.tsx +++ b/src/components/Editor/EditorContextMenu.tsx @@ -229,7 +229,7 @@ const TableSizeSelector: React.FC = ({ onSelect }) => { }; return ( -
    +
    {generateCells()}
    diff --git a/src/components/MainPage.tsx b/src/components/MainPage.tsx index 2f523eea..18d4d716 100644 --- a/src/components/MainPage.tsx +++ b/src/components/MainPage.tsx @@ -208,9 +208,11 @@ const FileEditorContainer: React.FC = () => { )}
    ) : ( -
    - -
    + !showChatbot && ( +
    + +
    + ) )} {showChatbot && ( diff --git a/src/components/Settings/AnalyticsSettings.tsx b/src/components/Settings/AnalyticsSettings.tsx index 2e4ef2f9..da02963c 100644 --- a/src/components/Settings/AnalyticsSettings.tsx +++ b/src/components/Settings/AnalyticsSettings.tsx @@ -1,4 +1,3 @@ - import React, { useState, useEffect } from "react"; import { Button } from "@material-tailwind/react"; @@ -35,8 +34,9 @@ const AnalyticsSettings: React.FC = () => {

    Analytics

    {" "}

    - Reor tracks anonymous usage data to help improve the app. We never share this personal data. This is solely to track which features are - popular. You can disable this at any time: + Reor tracks anonymous usage data to help improve the app. We never share + this personal data. This is solely to track which features are popular. + You can disable this at any time:

    = ({ onClose={() => { handleSave(); }} + widthType={"settingsContainer"} > -
    -
    +
    +
    = ({ > Text Generation{" "}
    -
    setActiveTab(SettingsTab.RAG)} - > - RAG{" "} -
    = ({
    {/* Right Content Area */} -
    +
    {/*

    Settings

    */} {activeTab === SettingsTab.GeneralSettings && (
    @@ -153,22 +135,6 @@ const SettingsModal: React.FC = ({
    )} - - {activeTab === SettingsTab.RAG && ( -
    -

    RAG

    {" "} - -

    - Number of notes to feed to the LLM during Q&A -

    -
    - -

    - Change the Chunk Size -

    -
    -
    - )}
    From a9ce1bdd97d9370cd7d37152ffc5d511975a35fd Mon Sep 17 00:00:00 2001 From: MoTheRoar Date: Tue, 16 Jul 2024 13:22:41 -0500 Subject: [PATCH 18/40] Changes needed --- .eslintrc.js | 96 +- .husky/pre-commit | 1 + .prettierrc | 27 +- .vscode/settings.json | 6 +- electron/electron-env.d.ts | 2 +- electron/main/common/chunking.ts | 92 +- electron/main/common/error.ts | 22 +- electron/main/common/network.ts | 236 +-- electron/main/common/windowManager.ts | 207 +- electron/main/electron-store/ipcHandlers.ts | 365 ++-- electron/main/electron-store/storeConfig.ts | 128 +- .../electron-store/storeSchemaMigrator.ts | 110 +- electron/main/electron-utils/ipcHandlers.ts | 205 +- electron/main/filesystem/Filesystem.ts | 303 --- electron/main/filesystem/Types.ts | 33 - electron/main/filesystem/filesystem.test.ts | 68 +- electron/main/filesystem/filesystem.ts | 297 +-- electron/main/filesystem/ipcHandlers.ts | 576 ++---- .../main/filesystem/registerFilesHandler.ts | 444 ---- electron/main/filesystem/types.ts | 40 +- electron/main/index.ts | 96 +- electron/main/llm/Types.ts | 49 - electron/main/llm/contextLimit.ts | 129 +- electron/main/llm/ipcHandlers.ts | 164 +- electron/main/llm/llmConfig.ts | 112 +- electron/main/llm/models/Anthropic.ts | 72 +- electron/main/llm/models/Ollama.ts | 256 ++- electron/main/llm/models/OpenAI.ts | 61 +- electron/main/llm/types.ts | 22 +- electron/main/llm/utils.ts | 17 + electron/main/path/ipcHandlers.ts | 65 +- electron/main/path/path.ts | 14 +- .../main/vector-database/database.test.ts | 33 +- .../vector-database/downloadModelsFromHF.ts | 102 +- electron/main/vector-database/embeddings.ts | 294 ++- electron/main/vector-database/ipcHandlers.ts | 349 ++-- electron/main/vector-database/lance.ts | 67 +- .../main/vector-database/lanceTableWrapper.ts | 191 +- electron/main/vector-database/schema.ts | 103 +- .../vector-database/tableHelperFunctions.ts | 382 ++-- electron/preload/index.ts | 446 ++-- package-lock.json | 1806 ++++++++++++++--- package.json | 36 +- scripts/downloadOllama.js | 10 +- scripts/notarize.js | 2 +- src/App.tsx | 104 +- .../Chat/AddContextFiltersModal.tsx | 252 ++- src/components/Chat/Chat-Prompts.tsx | 37 +- src/components/Chat/Chat.tsx | 441 ++-- src/components/Chat/ChatAction.tsx | 22 - src/components/Chat/ChatInput.tsx | 110 +- src/components/Chat/ChatsSidebar.tsx | 140 +- src/components/Chat/CustomLinkMarkdown.tsx | 20 - src/components/Chat/chatUtils.ts | 134 +- src/components/Chat/hooks/use-chat-history.ts | 76 +- .../Chat/hooks/use-outside-click.ts | 18 + src/components/Chat/utils.ts | 8 - src/components/Common/ExternalLink.tsx | 22 +- src/components/Common/IndexingProgress.tsx | 79 +- src/components/Common/Modal.tsx | 52 +- src/components/Common/ResizableComponent.tsx | 67 +- .../Common/SearchBarWithFilesSuggestion.tsx | 77 +- src/components/Common/Select.tsx | 103 +- src/components/Common/TitleBar.tsx | 28 +- src/components/Editor/BacklinkExtension.tsx | 198 +- .../Editor/BacklinkSuggestionsDisplay.tsx | 79 +- src/components/Editor/EditorContextMenu.tsx | 367 ++-- src/components/Editor/EditorManager.tsx | 138 +- src/components/Editor/HighlightExtension.tsx | 36 +- src/components/Editor/RichTextLink.tsx | 51 +- src/components/Editor/SearchAndReplace.tsx | 329 ++- src/components/File/DBResultPreview.tsx | 138 +- .../File/FileSideBar/FileHistoryBar.tsx | 178 +- src/components/File/FileSideBar/FileItem.tsx | 111 +- .../File/FileSideBar/fileOperations.ts | 54 - .../FileSideBar/hooks/use-file-info-tree.tsx | 139 +- src/components/File/FileSideBar/index.tsx | 273 ++- src/components/File/FileSideBar/utils.ts | 70 + src/components/File/NewDirectory.tsx | 101 +- src/components/File/NewNote.tsx | 89 +- src/components/File/RenameDirectory.tsx | 128 +- src/components/File/RenameNote.tsx | 120 +- .../File/hooks/use-file-by-filepath.ts | 415 ++-- src/components/File/utils.ts | 35 + .../Flashcard/FlashcardCreateModal.tsx | 169 +- .../Flashcard/FlashcardMenuModal.tsx | 88 +- .../Flashcard/FlashcardReviewModal.tsx | 102 +- src/components/Flashcard/FlashcardsCore.tsx | 158 +- src/components/Flashcard/ProgressBar.tsx | 36 +- src/components/Flashcard/index.ts | 7 +- src/components/Flashcard/types.ts | 10 +- src/components/Flashcard/utils.ts | 114 +- src/components/MainPage.tsx | 169 +- src/components/Settings/AnalyticsSettings.tsx | 65 +- src/components/Settings/ChunkSizeSettings.tsx | 67 +- src/components/Settings/DirectorySelector.tsx | 48 +- .../EmbeddingSettings/EmbeddingSettings.tsx | 167 +- .../InitialEmbeddingSettings.tsx | 97 +- .../modals/NewEmbeddingModelBothTypes.tsx | 117 +- .../modals/NewLocalEmbeddingModel.tsx | 121 +- .../modals/NewRemoteEmbeddingModel.tsx | 96 +- src/components/Settings/GeneralSections.tsx | 88 +- src/components/Settings/GeneralSettings.tsx | 26 +- .../Settings/InitialSettingsSinglePage.tsx | 82 +- .../LLMSettings/DefaultLLMSelector.tsx | 62 +- .../LLMSettings/InitialSetupLLMSettings.tsx | 63 +- .../Settings/LLMSettings/LLMSettings.tsx | 30 +- .../LLMSettings/LLMSettingsContent.tsx | 134 +- .../LLMSettings/NormalLLMSettings.tsx | 18 +- .../LLMSettings/hooks/useLLMConfigs.tsx | 45 +- .../Settings/LLMSettings/hooks/useModals.tsx | 34 +- .../LLMSettings/modals/CloudLLMSetup.tsx | 147 +- .../LLMSettings/modals/NewOllamaModel.tsx | 186 +- .../LLMSettings/modals/RemoteLLMSetup.tsx | 144 +- .../Settings/LLMSettings/modals/utils.ts | 60 + src/components/Settings/RagSettings.tsx | 51 - src/components/Settings/Settings.tsx | 79 +- .../Settings/Shared/SettingsRow.tsx | 33 + .../Settings/TextGenerationSettings.tsx | 120 +- src/components/Sidebars/FileSidebarSearch.tsx | 77 +- src/components/Sidebars/IconsSidebar.tsx | 170 +- src/components/Sidebars/MainSidebar.tsx | 84 +- .../SemanticSidebar/HighlightButton.tsx | 49 + .../SimilarEntriesComponent.tsx | 97 + .../Sidebars/SimilarFilesSidebar.tsx | 310 +-- src/components/Sidebars/utils.ts | 13 + .../WritingAssistantFloatingMenu.tsx | 377 ++++ src/main.tsx | 12 +- src/utils/error.ts | 20 +- src/utils/strings.ts | 70 +- vite.config.ts | 60 +- 131 files changed, 8280 insertions(+), 9067 deletions(-) create mode 100644 .husky/pre-commit delete mode 100644 electron/main/filesystem/Filesystem.ts delete mode 100644 electron/main/filesystem/Types.ts delete mode 100644 electron/main/filesystem/registerFilesHandler.ts delete mode 100644 electron/main/llm/Types.ts create mode 100644 electron/main/llm/utils.ts delete mode 100644 src/components/Chat/ChatAction.tsx delete mode 100644 src/components/Chat/CustomLinkMarkdown.tsx create mode 100644 src/components/Chat/hooks/use-outside-click.ts delete mode 100644 src/components/Chat/utils.ts delete mode 100644 src/components/File/FileSideBar/fileOperations.ts create mode 100644 src/components/File/FileSideBar/utils.ts create mode 100644 src/components/File/utils.ts create mode 100644 src/components/Settings/LLMSettings/modals/utils.ts delete mode 100644 src/components/Settings/RagSettings.tsx create mode 100644 src/components/Settings/Shared/SettingsRow.tsx create mode 100644 src/components/Sidebars/SemanticSidebar/HighlightButton.tsx create mode 100644 src/components/Sidebars/SemanticSidebar/SimilarEntriesComponent.tsx create mode 100644 src/components/Sidebars/utils.ts create mode 100644 src/components/Writing-Assistant/WritingAssistantFloatingMenu.tsx diff --git a/.eslintrc.js b/.eslintrc.js index d9d1b1ce..e3492936 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,79 +5,49 @@ module.exports = { node: true, }, extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "plugin:import/typescript", - ], - overrides: [ - { - env: { - node: true, - }, - files: [".eslintrc.{js,cjs}"], - parserOptions: { - sourceType: "script", - }, - }, - { - files: ["*.ts", "*.tsx"], - rules: { - "react/prop-types": "off", - }, - }, - { - // Applies to all files - files: ["*"], - rules: { - // "@typescript-eslint/no-unused-vars": "warn", - // "react/no-unescaped-entities": "warn", - // ... add other specific rules you want as warnings here ... - }, - }, + "airbnb", + "airbnb/hooks", + "airbnb-typescript", + "plugin:prettier/recommended", + "plugin:tailwindcss/recommended", ], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: "latest", sourceType: "module", + project: "./tsconfig.json", }, - plugins: ["@typescript-eslint", "react", "import", "unused-imports"], + plugins: [ + "@typescript-eslint", + "react", + "import", + "jsx-a11y", + "unused-imports", + "prettier", + "tailwindcss", + ], rules: { - "import/order": [ + "unused-imports/no-unused-imports": "error", + "prettier/prettier": "error", + "react/function-component-definition": [ "error", { - groups: [ - "builtin", - "external", - "internal", - "parent", - "sibling", - "index", - ], - pathGroups: [ - { - pattern: "react", - group: "external", - position: "before", - }, - ], - pathGroupsExcludedImportTypes: ["react"], - "newlines-between": "always", - alphabetize: { - order: "asc", - caseInsensitive: true, - }, + namedComponents: "arrow-function", + unnamedComponents: "arrow-function", }, ], - "unused-imports/no-unused-imports": "error", - "unused-imports/no-unused-vars": [ - "warn", - { - vars: "all", - varsIgnorePattern: "^_", - args: "after-used", - argsIgnorePattern: "^_", + "import/extensions": ["off", "ignorePackages"], + "jsx-a11y/no-static-element-interactions": "off", + "jsx-a11y/click-events-have-key-events": "off", + "react/require-default-props": "off", + "import/no-extraneous-dependencies": ["error", { "devDependencies": ["**/electron/**", "**/preload/**"] }], + }, + ignorePatterns: ["vite.config.ts", ".eslintrc.js"], + settings: { + "import/resolver": { + node: { + extensions: [".js", ".jsx", ".ts", ".tsx"], }, - ], + }, }, -}; +}; \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 00000000..c48c1a6f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run lint:fix && npm run type-check diff --git a/.prettierrc b/.prettierrc index a8728cf1..30fdbfad 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,23 +1,8 @@ { - "arrowParens": "always", - "bracketSpacing": true, - "endOfLine": "lf", - "htmlWhitespaceSensitivity": "css", - "insertPragma": false, - "singleAttributePerLine": false, - "bracketSameLine": false, - "jsxBracketSameLine": false, - "jsxSingleQuote": false, - "printWidth": 80, - "proseWrap": "preserve", - "quoteProps": "as-needed", - "requirePragma": false, - "semi": true, - "singleQuote": false, + "printWidth": 120, + "tslintintegration": true, + "trailingComma": "all", "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false, - "embeddedLanguageFormatting": "auto", - "vueIndentScriptAndStyle": false, - "parser": "typescript" -} + "semi": false, + "singleQuote": true +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6337c3a5..8c6ad058 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,8 +15,8 @@ // "source.organizeImports": "explicit" }, "eslint.alwaysShowStatus": true, - "eslint.format.enable": true, - "eslint.run": "onSave", - "editor.formatOnSave": true, + "eslint.format.enable": false, + // "eslint.run": "", + "editor.formatOnSave": false, "editor.codeActionsOnSaveMode": "explicit" // This is the updated setting } diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 683a8658..2e9c5fbe 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -8,4 +8,4 @@ declare namespace NodeJS { /** /dist/ or /public/ */ VITE_PUBLIC: string } -} \ No newline at end of file +} diff --git a/electron/main/common/chunking.ts b/electron/main/common/chunking.ts index 038515ab..ae7e0001 100644 --- a/electron/main/common/chunking.ts +++ b/electron/main/common/chunking.ts @@ -1,70 +1,64 @@ -import Store from "electron-store"; -import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; +import Store from 'electron-store' +import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter' -import { StoreKeys, StoreSchema } from "../electron-store/storeConfig"; +import { StoreKeys, StoreSchema } from '../electron-store/storeConfig' // Chunk by markdown headings and then use Langchain chunker if the heading chunk is too big: -const store = new Store(); +const store = new Store() -const chunkSize = store.get(StoreKeys.ChunkSize); - -export const chunkMarkdownByHeadingsAndByCharsIfBig = async ( - markdownContent: string -): Promise => { - const chunkOverlap = 20; - const chunksByHeading = chunkMarkdownByHeadings(markdownContent); - - const chunksWithBigChunksSplit: string[] = []; - const chunksWithSmallChunksSplit: string[] = []; - chunksByHeading.forEach((chunk) => { - if (chunk.length > chunkSize) { - chunksWithBigChunksSplit.push(chunk); - } else { - chunksWithSmallChunksSplit.push(chunk); - } - }); - - const chunkedRecursively = await chunkStringsRecursively( - chunksWithBigChunksSplit, - chunkSize, - chunkOverlap - ); - - return chunksWithSmallChunksSplit.concat(chunkedRecursively); -}; +const chunkSize = store.get(StoreKeys.ChunkSize) export function chunkMarkdownByHeadings(markdownContent: string): string[] { - const lines = markdownContent.split("\n"); - const chunks: string[] = []; - let currentChunk: string[] = []; + const lines = markdownContent.split('\n') + const chunks: string[] = [] + let currentChunk: string[] = [] lines.forEach((line) => { - if (line.startsWith("#")) { + if (line.startsWith('#')) { if (currentChunk.length) { - chunks.push(currentChunk.join("\n")); - currentChunk = []; + chunks.push(currentChunk.join('\n')) + currentChunk = [] } } - currentChunk.push(line); - }); + currentChunk.push(line) + }) if (currentChunk.length) { - chunks.push(currentChunk.join("\n")); + chunks.push(currentChunk.join('\n')) } - return chunks; + return chunks } export const chunkStringsRecursively = async ( strings: string[], - chunkSize: number, - chunkOverlap: number + _chunkSize: number, + chunkOverlap: number, ): Promise => { const splitter = new RecursiveCharacterTextSplitter({ - chunkSize: chunkSize, - chunkOverlap: chunkOverlap, - }); + chunkSize: _chunkSize, + chunkOverlap, + }) - const chunks = await splitter.createDocuments(strings); - const mappedChunks = chunks.map((chunk) => chunk.pageContent); - return mappedChunks; -}; + const chunks = await splitter.createDocuments(strings) + const mappedChunks = chunks.map((chunk) => chunk.pageContent) + return mappedChunks +} + +export const chunkMarkdownByHeadingsAndByCharsIfBig = async (markdownContent: string): Promise => { + const chunkOverlap = 20 + const chunksByHeading = chunkMarkdownByHeadings(markdownContent) + + const chunksWithBigChunksSplit: string[] = [] + const chunksWithSmallChunksSplit: string[] = [] + chunksByHeading.forEach((chunk) => { + if (chunk.length > chunkSize) { + chunksWithBigChunksSplit.push(chunk) + } else { + chunksWithSmallChunksSplit.push(chunk) + } + }) + + const chunkedRecursively = await chunkStringsRecursively(chunksWithBigChunksSplit, chunkSize, chunkOverlap) + + return chunksWithSmallChunksSplit.concat(chunkedRecursively) +} diff --git a/electron/main/common/error.ts b/electron/main/common/error.ts index e5e5bc74..3e2d9829 100644 --- a/electron/main/common/error.ts +++ b/electron/main/common/error.ts @@ -1,21 +1,17 @@ -export function errorToStringMainProcess( - error: unknown, - depth: number = 0 -): string { +function errorToStringMainProcess(error: unknown, depth: number = 0): string { if (error instanceof Error) { - let errorString = `${error.name}: ${error.message}`; + let errorString = `${error.name}: ${error.message}` if (error.cause) { - errorString += `\nCaused by: ${errorToStringMainProcess( - error.cause, - depth + 1 - )}`; + errorString += `\nCaused by: ${errorToStringMainProcess(error.cause, depth + 1)}` } if (depth === 0) { // Optionally include the stack trace at the top level - errorString += `\nStack Trace:\n${error.stack}`; + errorString += `\nStack Trace:\n${error.stack}` } - return errorString; - } else { - return String(error); + return errorString } + return String(error) } + +export default errorToStringMainProcess +// diff --git a/electron/main/common/network.ts b/electron/main/common/network.ts index df52213d..1e14d000 100644 --- a/electron/main/common/network.ts +++ b/electron/main/common/network.ts @@ -1,143 +1,165 @@ +import { Readable } from 'stream' -import { Readable } from "stream"; +import { net } from 'electron' +import { ClientRequestConstructorOptions } from 'electron/main' -import { net } from "electron"; -import { ClientRequestConstructorOptions } from "electron/main"; - -export const customFetchUsingElectronNet = async ( - input: RequestInfo | URL, - init?: RequestInit -): Promise => { - console.log("input: ", input); - console.log("init: ", init); - const url = input instanceof URL ? input.href : input.toString(); - const options = init || {}; +export const customFetchUsingElectronNet = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const url = input instanceof URL ? input.href : input.toString() + const options = init || {} return new Promise((resolve, reject) => { const requestOptions: ClientRequestConstructorOptions = { - method: options.method || "GET", - url: url, - }; + method: options.method || 'GET', + url, + } - const request = net.request(requestOptions); + const request = net.request(requestOptions) // Set headers if (options.headers) { Object.entries(options.headers).forEach(([key, value]) => { - request.setHeader(key, value as string); - }); + request.setHeader(key, value as string) + }) } // Handle request body if (options.body) { - let bodyData; + let bodyData if (options.body instanceof ArrayBuffer) { - bodyData = Buffer.from(options.body); - } else if ( - typeof options.body === "string" || - Buffer.isBuffer(options.body) - ) { - bodyData = options.body; - } else if (typeof options.body === "object") { - bodyData = JSON.stringify(options.body); - request.setHeader("Content-Type", "application/json"); + bodyData = Buffer.from(options.body) + } else if (typeof options.body === 'string' || Buffer.isBuffer(options.body)) { + bodyData = options.body + } else if (typeof options.body === 'object') { + bodyData = JSON.stringify(options.body) + request.setHeader('Content-Type', 'application/json') } else { - reject(new Error("Unsupported body type")); - return; + reject(new Error('Unsupported body type')) + return } - request.write(bodyData); + request.write(bodyData) } - request.on("response", (response) => { - const chunks: Buffer[] = []; - response.on("data", (chunk) => chunks.push(chunk as Buffer)); - response.on("end", () => { - const buffer = Buffer.concat(chunks); + request.on('response', (response) => { + const chunks: Buffer[] = [] + response.on('data', (chunk) => chunks.push(chunk as Buffer)) + response.on('end', () => { + const buffer = Buffer.concat(chunks) resolve( new Response(buffer, { status: response.statusCode, statusText: response.statusMessage, // eslint-disable-next-line @typescript-eslint/no-explicit-any headers: new Headers(response.headers as any), - }) - ); - }); - }); + }), + ) + }) + }) + + request.on('error', (error) => reject(error)) + request.end() + }) +} + +function nodeToWebStream(nodeStream: Readable): ReadableStream { + let isStreamEnded = false + + const webStream = new ReadableStream({ + start(controller) { + nodeStream.on('data', (chunk) => { + if (!isStreamEnded) { + controller.enqueue(chunk instanceof Buffer ? new Uint8Array(chunk) : chunk) + } + }) + + nodeStream.on('end', () => { + if (!isStreamEnded) { + isStreamEnded = true + controller.close() + } + }) + + nodeStream.on('error', (err) => { + if (!isStreamEnded) { + isStreamEnded = true + controller.error(err) + } + }) + }, + cancel(reason) { + // Handle any cleanup or abort logic here + nodeStream.destroy(reason) + }, + }) - request.on("error", (error) => reject(error)); - request.end(); - }); -}; + return webStream +} export const customFetchUsingElectronNetStreaming = async ( input: RequestInfo | URL, - init?: RequestInit + init?: RequestInit, ): Promise => { - const url = input instanceof URL ? input.href : input.toString(); - const options = init || {}; + const url = input instanceof URL ? input.href : input.toString() + const options = init || {} return new Promise((resolve, reject) => { const requestOptions: ClientRequestConstructorOptions = { - method: options.method || "GET", - url: url, - }; + method: options.method || 'GET', + url, + } // Ignore the 'agent' property from 'init' as it's not relevant for Electron's net module - if ("agent" in options) { - delete options.agent; + if ('agent' in options) { + delete options.agent } - const request = net.request(requestOptions); + const request = net.request(requestOptions) // Set headers, except for 'content-length' which will be set automatically if (options.headers) { Object.entries(options.headers).forEach(([key, value]) => { - if (key.toLowerCase() !== "content-length") { + if (key.toLowerCase() !== 'content-length') { // Skip 'content-length' - request.setHeader(key, value as string); + request.setHeader(key, value as string) } - }); + }) } // Handle request body if (options.body) { - let bodyData; + let bodyData if (options.body instanceof ArrayBuffer) { - bodyData = Buffer.from(options.body); - } else if ( - typeof options.body === "string" || - Buffer.isBuffer(options.body) - ) { - bodyData = options.body; - } else if (typeof options.body === "object") { - bodyData = JSON.stringify(options.body); - request.setHeader("Content-Type", "application/json"); + bodyData = Buffer.from(options.body) + } else if (typeof options.body === 'string' || Buffer.isBuffer(options.body)) { + bodyData = options.body + } else if (typeof options.body === 'object') { + bodyData = JSON.stringify(options.body) + request.setHeader('Content-Type', 'application/json') } else { - reject(new Error("Unsupported body type")); - return; + reject(new Error('Unsupported body type')) + return } - request.write(bodyData); + request.write(bodyData) } - request.on("response", (response) => { + request.on('response', (response) => { const nodeStream = new Readable({ read() {}, - }); + }) - response.on("data", (chunk) => { - nodeStream.push(chunk); - }); + response.on('data', (chunk) => { + nodeStream.push(chunk) + }) - response.on("end", () => { - nodeStream.push(null); // Signal end of stream - }); + response.on('end', () => { + nodeStream.push(null) // Signal end of stream + }) // eslint-disable-next-line @typescript-eslint/no-explicit-any - response.on("error", (error: any) => { - nodeStream.destroy(error); // Handle stream errors - }); + response.on('error', (error: any) => { + nodeStream.destroy(error) // Handle stream errors + }) - const webStream = nodeToWebStream(nodeStream); + const webStream = nodeToWebStream(nodeStream) resolve( new Response(webStream, { @@ -145,50 +167,14 @@ export const customFetchUsingElectronNetStreaming = async ( statusText: response.statusMessage, // eslint-disable-next-line @typescript-eslint/no-explicit-any headers: new Headers(response.headers as any), - }) - ); - }); + }), + ) + }) - request.on("error", (error) => { - reject(error); - }); - - request.end(); - }); -}; - -function nodeToWebStream(nodeStream: Readable): ReadableStream { - let isStreamEnded = false; - - const webStream = new ReadableStream({ - start(controller) { - nodeStream.on("data", (chunk) => { - if (!isStreamEnded) { - controller.enqueue( - chunk instanceof Buffer ? new Uint8Array(chunk) : chunk - ); - } - }); - - nodeStream.on("end", () => { - if (!isStreamEnded) { - isStreamEnded = true; - controller.close(); - } - }); - - nodeStream.on("error", (err) => { - if (!isStreamEnded) { - isStreamEnded = true; - controller.error(err); - } - }); - }, - cancel(reason) { - // Handle any cleanup or abort logic here - nodeStream.destroy(reason); - }, - }); + request.on('error', (error) => { + reject(error) + }) - return webStream; + request.end() + }) } diff --git a/electron/main/common/windowManager.ts b/electron/main/common/windowManager.ts index 0c9e283d..36c559a7 100644 --- a/electron/main/common/windowManager.ts +++ b/electron/main/common/windowManager.ts @@ -1,214 +1,191 @@ -import chokidar from "chokidar"; -import { BrowserWindow, WebContents, screen, shell } from "electron"; -import Store from "electron-store"; +/* eslint-disable class-methods-use-this */ +import chokidar from 'chokidar' +import { BrowserWindow, WebContents, screen, shell } from 'electron' +import Store from 'electron-store' -import { StoreSchema, StoreKeys } from "../electron-store/storeConfig"; -import { LanceDBTableWrapper } from "../vector-database/lanceTableWrapper"; +import { StoreSchema, StoreKeys } from '../electron-store/storeConfig' +import LanceDBTableWrapper from '../vector-database/lanceTableWrapper' type WindowInfo = { - windowID: number; - dbTableClient: LanceDBTableWrapper; - vaultDirectoryForWindow: string; -}; + windowID: number + dbTableClient: LanceDBTableWrapper + vaultDirectoryForWindow: string +} class WindowsManager { - activeWindows: WindowInfo[] = []; - private errorStringsToSendWindow: string[] = []; + activeWindows: WindowInfo[] = [] - watcher: chokidar.FSWatcher | undefined; + private errorStringsToSendWindow: string[] = [] - async createWindow( - store: Store, - preload: string, - url: string | undefined, - indexHtml: string - ) { - const { x, y } = this.getNextWindowPosition(); - const { width, height } = this.getWindowSize(); + watcher: chokidar.FSWatcher | undefined + + async createWindow(store: Store, preload: string, url: string | undefined, indexHtml: string) { + const { x, y } = this.getNextWindowPosition() + const { width, height } = this.getWindowSize() const win = new BrowserWindow({ - title: "Reor", - x: x, - y: y, + title: 'Reor', + x, + y, webPreferences: { preload, }, frame: false, - titleBarStyle: "hidden", + titleBarStyle: 'hidden', titleBarOverlay: { - color: "#303030", - symbolColor: "#fff", + color: '#303030', + symbolColor: '#fff', height: 30, }, - width: width, - height: height, - }); + width, + height, + }) if (url) { // electron-vite-vue#298 - win.loadURL(url); + win.loadURL(url) // Open devTool if the app is not packaged - win.webContents.openDevTools(); + win.webContents.openDevTools() } else { - win.loadFile(indexHtml); + win.loadFile(indexHtml) } // Make all links open with the browser, not with the application - win.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith("https:")) shell.openExternal(url); - return { action: "deny" }; - }); + win.webContents.setWindowOpenHandler(({ url: _url }) => { + if (_url.startsWith('https:')) shell.openExternal(_url) + return { action: 'deny' } + }) - win.on("close", () => { - win.webContents.send("prepare-for-window-close"); + win.on('close', () => { + win.webContents.send('prepare-for-window-close') - this.prepareWindowForClose(store, win); - }); + this.prepareWindowForClose(store, win) + }) - win.webContents.on("did-finish-load", () => { - const errorsToSendWindow = this.getAndClearErrorStrings(); + win.webContents.on('did-finish-load', () => { + const errorsToSendWindow = this.getAndClearErrorStrings() errorsToSendWindow.forEach((errorStrToSendWindow) => { - win.webContents.send( - "error-to-display-in-window", - errorStrToSendWindow - ); - }); - }); + win.webContents.send('error-to-display-in-window', errorStrToSendWindow) + }) + }) } getAndSetupDirectoryForWindowFromPreviousAppSession( webContents: Electron.WebContents, - store: Store + store: Store, ): string { - const lastUsedVaultDirectory = store.get( - StoreKeys.DirectoryFromPreviousSession - ) as string; + const lastUsedVaultDirectory = store.get(StoreKeys.DirectoryFromPreviousSession) as string if (!lastUsedVaultDirectory) { - return ""; + return '' } - const isUserDirectoryUsed = this.activeWindows.some( - (w) => w.vaultDirectoryForWindow === lastUsedVaultDirectory - ); + const isUserDirectoryUsed = this.activeWindows.some((w) => w.vaultDirectoryForWindow === lastUsedVaultDirectory) if (!isUserDirectoryUsed) { - this.setVaultDirectoryForContents( - webContents, - lastUsedVaultDirectory, - store - ); - return lastUsedVaultDirectory; + this.setVaultDirectoryForContents(webContents, lastUsedVaultDirectory, store) + return lastUsedVaultDirectory } - return ""; + return '' } appendNewErrorToDisplayInWindow(errorString: string) { - this.errorStringsToSendWindow.push(errorString); + this.errorStringsToSendWindow.push(errorString) } getAndClearErrorStrings(): string[] { - const errorStrings = this.errorStringsToSendWindow; - this.errorStringsToSendWindow = []; - return errorStrings; + const errorStrings = this.errorStringsToSendWindow + this.errorStringsToSendWindow = [] + return errorStrings } getBrowserWindowId(webContents: WebContents): number | null { - const browserWindow = BrowserWindow.fromWebContents(webContents); - return browserWindow ? browserWindow.id : null; + const browserWindow = BrowserWindow.fromWebContents(webContents) + return browserWindow ? browserWindow.id : null } getWindowInfoForContents(webContents: WebContents): WindowInfo | null { - const windowID = this.getBrowserWindowId(webContents); - console.log("window id is: ", windowID); + const windowID = this.getBrowserWindowId(webContents) + if (windowID === null) { - return null; + return null } - console.log("active windows: ", this.activeWindows); - const windowInfo = this.activeWindows.find((w) => w.windowID === windowID); - return windowInfo || null; + + const windowInfo = this.activeWindows.find((w) => w.windowID === windowID) + return windowInfo || null } getVaultDirectoryForWinContents(webContents: WebContents): string | null { - const windowID = this.getBrowserWindowId(webContents); - return windowID ? this.getVaultDirectoryForWindowID(windowID) : null; + const windowID = this.getBrowserWindowId(webContents) + return windowID ? this.getVaultDirectoryForWindowID(windowID) : null } private getVaultDirectoryForWindowID(windowID: number): string | null { - const windowInfo = this.activeWindows.find((w) => w.windowID === windowID); - return windowInfo ? windowInfo.vaultDirectoryForWindow : null; + const windowInfo = this.activeWindows.find((w) => w.windowID === windowID) + return windowInfo ? windowInfo.vaultDirectoryForWindow : null } - setVaultDirectoryForContents( - webContents: WebContents, - directory: string, - store: Store - ): void { + setVaultDirectoryForContents(webContents: WebContents, directory: string, store: Store): void { if (!webContents) { - throw new Error("Invalid webContents provided."); + throw new Error('Invalid webContents provided.') } - const windowID = this.getBrowserWindowId(webContents); + const windowID = this.getBrowserWindowId(webContents) if (!windowID) { - throw new Error("Unable to find the browser window ID."); + throw new Error('Unable to find the browser window ID.') } - if (!directory || typeof directory !== "string") { - throw new Error("Invalid directory provided."); + if (!directory || typeof directory !== 'string') { + throw new Error('Invalid directory provided.') } - let windowInfo = this.activeWindows.find((w) => w.windowID === windowID); + let windowInfo = this.activeWindows.find((w) => w.windowID === windowID) if (!windowInfo) { windowInfo = { - windowID: windowID, + windowID, dbTableClient: new LanceDBTableWrapper(), // Assuming default value as null, modify as needed vaultDirectoryForWindow: directory, - }; - this.activeWindows.push(windowInfo); + } + this.activeWindows.push(windowInfo) } else { - windowInfo.vaultDirectoryForWindow = directory; + windowInfo.vaultDirectoryForWindow = directory } - store.set(StoreKeys.DirectoryFromPreviousSession, directory); + store.set(StoreKeys.DirectoryFromPreviousSession, directory) } prepareWindowForClose(store: Store, win: BrowserWindow) { - const directoryToSave = this.getVaultDirectoryForWinContents( - win.webContents - ); + const directoryToSave = this.getVaultDirectoryForWinContents(win.webContents) // Save the directory if found if (directoryToSave) { - store.set(StoreKeys.DirectoryFromPreviousSession, directoryToSave); - this.removeActiveWindowByDirectory(directoryToSave); + store.set(StoreKeys.DirectoryFromPreviousSession, directoryToSave) + this.removeActiveWindowByDirectory(directoryToSave) } } removeActiveWindowByDirectory(directory: string): void { - this.activeWindows = this.activeWindows.filter( - (w) => w.vaultDirectoryForWindow !== directory - ); + this.activeWindows = this.activeWindows.filter((w) => w.vaultDirectoryForWindow !== directory) } getNextWindowPosition(): { x: number | undefined; y: number | undefined } { - const windowOffset = 30; // Offset for each new window - const focusedWin = BrowserWindow.getFocusedWindow(); + const windowOffset = 30 // Offset for each new window + const focusedWin = BrowserWindow.getFocusedWindow() if (focusedWin) { - const [x, y] = focusedWin.getPosition(); - return { x: x + windowOffset, y: y + windowOffset }; - } else { - return { x: undefined, y: undefined }; + const [x, y] = focusedWin.getPosition() + return { x: x + windowOffset, y: y + windowOffset } } + return { x: undefined, y: undefined } } getWindowSize(): { width: number; height: number } { - const primaryDisplay = screen.getPrimaryDisplay(); - const { width, height } = primaryDisplay.workAreaSize; + const primaryDisplay = screen.getPrimaryDisplay() + const { width, height } = primaryDisplay.workAreaSize - const windowWidth = Math.min(1200, width * 0.8); // e.g., 80% of screen width or 1200px - const windowHeight = Math.min(800, height * 0.8); // e.g., 80% of screen height or 800px + const windowWidth = Math.min(1200, width * 0.8) // e.g., 80% of screen width or 1200px + const windowHeight = Math.min(800, height * 0.8) // e.g., 80% of screen height or 800px - return { width: windowWidth, height: windowHeight }; + return { width: windowWidth, height: windowHeight } } } -export default WindowsManager; +export default WindowsManager diff --git a/electron/main/electron-store/ipcHandlers.ts b/electron/main/electron-store/ipcHandlers.ts index 84a3c532..ea04f1c9 100644 --- a/electron/main/electron-store/ipcHandlers.ts +++ b/electron/main/electron-store/ipcHandlers.ts @@ -1,9 +1,9 @@ -import path from "path"; +import path from 'path' -import { ipcMain } from "electron"; -import Store from "electron-store"; +import { ipcMain } from 'electron' +import Store from 'electron-store' -import WindowsManager from "../common/windowManager"; +import WindowsManager from '../common/windowManager' import { EmbeddingModelConfig, @@ -11,277 +11,206 @@ import { EmbeddingModelWithRepo, StoreKeys, StoreSchema, -} from "./storeConfig"; -import { initializeAndMaybeMigrateStore } from "./storeSchemaMigrator"; - -import { ChatHistory } from "@/components/Chat/Chat"; - -export const registerStoreHandlers = ( - store: Store, - windowsManager: WindowsManager -) => { - initializeAndMaybeMigrateStore(store); - ipcMain.handle( - "set-vault-directory-for-window", - async (event, userDirectory: string): Promise => { - console.log("setting user directory", userDirectory); - windowsManager.setVaultDirectoryForContents( - event.sender, - userDirectory, - store - ); - } - ); - - ipcMain.handle("get-vault-directory-for-window", (event) => { - let path = windowsManager.getVaultDirectoryForWinContents(event.sender); - if (!path) { - path = windowsManager.getAndSetupDirectoryForWindowFromPreviousAppSession( - event.sender, - store - ); - } - return path; - }); - ipcMain.handle("set-default-embedding-model", (event, repoName: string) => { - store.set(StoreKeys.DefaultEmbeddingModelAlias, repoName); - }); - - ipcMain.handle( - "add-new-local-embedding-model", - (event, model: EmbeddingModelWithLocalPath) => { - const currentModels = store.get(StoreKeys.EmbeddingModels) || {}; - const modelAlias = path.basename(model.localPath); - store.set(StoreKeys.EmbeddingModels, { - ...currentModels, - [modelAlias]: model, - }); - store.set(StoreKeys.DefaultEmbeddingModelAlias, modelAlias); +} from './storeConfig' +import { initializeAndMaybeMigrateStore } from './storeSchemaMigrator' +import { ChatHistory } from '@/components/Chat/chatUtils' + +export const registerStoreHandlers = (store: Store, windowsManager: WindowsManager) => { + initializeAndMaybeMigrateStore(store) + ipcMain.handle('set-vault-directory-for-window', async (event, userDirectory: string): Promise => { + windowsManager.setVaultDirectoryForContents(event.sender, userDirectory, store) + }) + + ipcMain.handle('get-vault-directory-for-window', (event) => { + let vaultPathForWindow = windowsManager.getVaultDirectoryForWinContents(event.sender) + if (!vaultPathForWindow) { + vaultPathForWindow = windowsManager.getAndSetupDirectoryForWindowFromPreviousAppSession(event.sender, store) } - ); + return vaultPathForWindow + }) + ipcMain.handle('set-default-embedding-model', (event, repoName: string) => { + store.set(StoreKeys.DefaultEmbeddingModelAlias, repoName) + }) + + ipcMain.handle('add-new-local-embedding-model', (event, model: EmbeddingModelWithLocalPath) => { + const currentModels = store.get(StoreKeys.EmbeddingModels) || {} + const modelAlias = path.basename(model.localPath) + store.set(StoreKeys.EmbeddingModels, { + ...currentModels, + [modelAlias]: model, + }) + store.set(StoreKeys.DefaultEmbeddingModelAlias, modelAlias) + }) + + ipcMain.handle('add-new-repo-embedding-model', (event, model: EmbeddingModelWithRepo) => { + const currentModels = store.get(StoreKeys.EmbeddingModels) || {} + store.set(StoreKeys.EmbeddingModels, { + ...currentModels, + [model.repoName]: model, + }) + store.set(StoreKeys.DefaultEmbeddingModelAlias, model.repoName) + }) + + ipcMain.handle('get-embedding-models', () => store.get(StoreKeys.EmbeddingModels)) ipcMain.handle( - "add-new-repo-embedding-model", - (event, model: EmbeddingModelWithRepo) => { - const currentModels = store.get(StoreKeys.EmbeddingModels) || {}; - store.set(StoreKeys.EmbeddingModels, { - ...currentModels, - [model.repoName]: model, - }); - store.set(StoreKeys.DefaultEmbeddingModelAlias, model.repoName); - } - ); - - ipcMain.handle("get-embedding-models", () => { - return store.get(StoreKeys.EmbeddingModels); - }); - - ipcMain.handle( - "update-embedding-model", - ( - event, - modelName: string, - updatedModel: EmbeddingModelWithLocalPath | EmbeddingModelWithRepo - ) => { - const currentModels = store.get(StoreKeys.EmbeddingModels) || {}; + 'update-embedding-model', + (event, modelName: string, updatedModel: EmbeddingModelWithLocalPath | EmbeddingModelWithRepo) => { + const currentModels = store.get(StoreKeys.EmbeddingModels) || {} store.set(StoreKeys.EmbeddingModels, { ...currentModels, [modelName]: updatedModel, - }); - } - ); + }) + }, + ) - ipcMain.handle("remove-embedding-model", (event, modelName: string) => { - const currentModels = store.get(StoreKeys.EmbeddingModels) || {}; + ipcMain.handle('remove-embedding-model', (event, modelName: string) => { + const currentModels = store.get(StoreKeys.EmbeddingModels) || {} // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { [modelName]: _, ...updatedModels } = currentModels; + const { [modelName]: unused, ...updatedModels } = currentModels - store.set(StoreKeys.EmbeddingModels, updatedModels); - }); + store.set(StoreKeys.EmbeddingModels, updatedModels) + }) - ipcMain.handle("set-no-of-rag-examples", (event, noOfExamples: number) => { - store.set(StoreKeys.MaxRAGExamples, noOfExamples); - }); + ipcMain.handle('set-no-of-rag-examples', (event, noOfExamples: number) => { + store.set(StoreKeys.MaxRAGExamples, noOfExamples) + }) - ipcMain.handle("get-no-of-rag-examples", () => { - return store.get(StoreKeys.MaxRAGExamples); - }); + ipcMain.handle('get-no-of-rag-examples', () => store.get(StoreKeys.MaxRAGExamples)) - ipcMain.handle("set-chunk-size", (event, chunkSize: number) => { - store.set(StoreKeys.ChunkSize, chunkSize); - }); + ipcMain.handle('set-chunk-size', (event, chunkSize: number) => { + store.set(StoreKeys.ChunkSize, chunkSize) + }) - ipcMain.handle("get-chunk-size", () => { - return store.get(StoreKeys.ChunkSize); - }); + ipcMain.handle('get-chunk-size', () => store.get(StoreKeys.ChunkSize)) - ipcMain.handle("get-default-embedding-model", () => { - return store.get(StoreKeys.DefaultEmbeddingModelAlias); - }); + ipcMain.handle('get-default-embedding-model', () => store.get(StoreKeys.DefaultEmbeddingModelAlias)) - ipcMain.handle("get-hardware-config", () => { - return store.get(StoreKeys.Hardware); - }); + ipcMain.handle('get-hardware-config', () => store.get(StoreKeys.Hardware)) - ipcMain.handle("set-hardware-config", (event, hardwareConfig) => { - store.set(StoreKeys.Hardware, hardwareConfig); - }); + ipcMain.handle('set-hardware-config', (event, hardwareConfig) => { + store.set(StoreKeys.Hardware, hardwareConfig) + }) - ipcMain.handle("set-llm-generation-params", (event, generationParams) => { - console.log("setting generation params", generationParams); - store.set(StoreKeys.LLMGenerationParameters, generationParams); - }); + ipcMain.handle('set-llm-generation-params', (event, generationParams) => { + store.set(StoreKeys.LLMGenerationParameters, generationParams) + }) - ipcMain.handle("get-llm-generation-params", () => { - console.log( - "getting generation params", - store.get(StoreKeys.LLMGenerationParameters) - ); - return store.get(StoreKeys.LLMGenerationParameters); - }); + ipcMain.handle('get-llm-generation-params', () => { + 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('set-display-markdown', (event, displayMarkdown) => { + store.set(StoreKeys.DisplayMarkdown, displayMarkdown) + event.sender.send('display-markdown-changed', displayMarkdown) + }) - ipcMain.handle("get-display-markdown", () => { - return store.get(StoreKeys.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); - }); + ipcMain.handle('set-sb-compact', (event, isSBCompact) => { + store.set(StoreKeys.IsSBCompact, isSBCompact) + event.sender.send('sb-compact-changed', isSBCompact) + }) - ipcMain.handle("get-sb-compact", () => { - return store.get(StoreKeys.IsSBCompact); - }); + ipcMain.handle('get-sb-compact', () => store.get(StoreKeys.IsSBCompact)) - ipcMain.handle("get-editor-flex-center", () => { - return store.get(StoreKeys.EditorFlexCenter); - }); + ipcMain.handle('get-editor-flex-center', () => store.get(StoreKeys.EditorFlexCenter)); - ipcMain.handle("set-editor-flex-center", (event, setEditorFlexCenter) => { + ipcMain.handle('set-editor-flex-center', (event, setEditorFlexCenter) => { store.set(StoreKeys.EditorFlexCenter, setEditorFlexCenter); - event.sender.send("editor-flex-center-changed", setEditorFlexCenter); + event.sender.send('editor-flex-center-changed', setEditorFlexCenter); }); - ipcMain.handle("set-analytics-mode", (event, isAnalytics) => { - console.log("setting analytics mode", isAnalytics); - store.set(StoreKeys.Analytics, isAnalytics); - }); + ipcMain.handle('set-analytics-mode', (event, isAnalytics) => { + store.set(StoreKeys.Analytics, isAnalytics) + }) - ipcMain.handle("get-analytics-mode", () => { - console.log("getting analytics params", store.get(StoreKeys.Analytics)); - return store.get(StoreKeys.Analytics); - }); + ipcMain.handle('get-analytics-mode', () => { + return store.get(StoreKeys.Analytics) + }) - ipcMain.handle("set-spellcheck-mode", (event, isSpellCheck) => { - console.log("setting spellcheck params", isSpellCheck); - store.set(StoreKeys.SpellCheck, isSpellCheck); - }); + ipcMain.handle('set-spellcheck-mode', (event, isSpellCheck) => { + store.set(StoreKeys.SpellCheck, isSpellCheck) + }) - ipcMain.handle("get-spellcheck-mode", () => { - console.log("getting spellcheck params", store.get(StoreKeys.SpellCheck)); - return store.get(StoreKeys.SpellCheck); - }); + ipcMain.handle('get-spellcheck-mode', () => { + return store.get(StoreKeys.SpellCheck) + }) - ipcMain.handle("has-user-opened-app-before", () => { - return store.get(StoreKeys.hasUserOpenedAppBefore); - }); + ipcMain.handle('has-user-opened-app-before', () => store.get(StoreKeys.hasUserOpenedAppBefore)) - ipcMain.handle("set-user-has-opened-app-before", () => { - store.set(StoreKeys.hasUserOpenedAppBefore, true); - }); + ipcMain.handle('set-user-has-opened-app-before', () => { + store.set(StoreKeys.hasUserOpenedAppBefore, true) + }) - ipcMain.handle("get-all-chat-histories", (event) => { - const vaultDir = windowsManager.getVaultDirectoryForWinContents( - event.sender - ); + ipcMain.handle('get-all-chat-histories', (event) => { + const vaultDir = windowsManager.getVaultDirectoryForWinContents(event.sender) if (!vaultDir) { - return []; + return [] } - const allHistories = store.get(StoreKeys.ChatHistories); - const chatHistoriesCorrespondingToVault = allHistories?.[vaultDir] ?? []; - return chatHistoriesCorrespondingToVault; - }); + const allHistories = store.get(StoreKeys.ChatHistories) + const chatHistoriesCorrespondingToVault = allHistories?.[vaultDir] ?? [] + return chatHistoriesCorrespondingToVault + }) - ipcMain.handle("update-chat-history", (event, newChat: ChatHistory) => { - const vaultDir = windowsManager.getVaultDirectoryForWinContents( - event.sender - ); - const allChatHistories = store.get(StoreKeys.ChatHistories); + ipcMain.handle('update-chat-history', (event, newChat: ChatHistory) => { + const vaultDir = windowsManager.getVaultDirectoryForWinContents(event.sender) + const allChatHistories = store.get(StoreKeys.ChatHistories) if (!vaultDir) { - return; + return } - const chatHistoriesCorrespondingToVault = - allChatHistories?.[vaultDir] ?? []; + const chatHistoriesCorrespondingToVault = allChatHistories?.[vaultDir] ?? [] // check if chat history already exists. if it does, update it. if it doesn't append it - const existingChatIndex = chatHistoriesCorrespondingToVault.findIndex( - (chat) => chat.id === newChat.id - ); + const existingChatIndex = chatHistoriesCorrespondingToVault.findIndex((chat) => chat.id === newChat.id) if (existingChatIndex !== -1) { - chatHistoriesCorrespondingToVault[existingChatIndex] = newChat; + chatHistoriesCorrespondingToVault[existingChatIndex] = newChat } else { - chatHistoriesCorrespondingToVault.push(newChat); + chatHistoriesCorrespondingToVault.push(newChat) } // store.set(StoreKeys.ChatHistories, allChatHistories); store.set(StoreKeys.ChatHistories, { ...allChatHistories, [vaultDir]: chatHistoriesCorrespondingToVault, - }); + }) - event.sender.send( - "update-chat-histories", - chatHistoriesCorrespondingToVault - ); - }); + event.sender.send('update-chat-histories', chatHistoriesCorrespondingToVault) + }) - ipcMain.handle("get-chat-history", (event, chatId: string) => { - const vaultDir = windowsManager.getVaultDirectoryForWinContents( - event.sender - ); + ipcMain.handle('get-chat-history', (event, chatId: string) => { + const vaultDir = windowsManager.getVaultDirectoryForWinContents(event.sender) if (!vaultDir) { - return; + return null } - const allChatHistories = store.get(StoreKeys.ChatHistories); - const vaultChatHistories = allChatHistories[vaultDir] || []; - return vaultChatHistories.find((chat) => chat.id === chatId); - }); + const allChatHistories = store.get(StoreKeys.ChatHistories) + const vaultChatHistories = allChatHistories[vaultDir] || [] + return vaultChatHistories.find((chat) => chat.id === chatId) + }) - ipcMain.handle("remove-chat-history-at-id", (event, chatID: string) => { - const vaultDir = windowsManager.getVaultDirectoryForWinContents( - event.sender - ); + ipcMain.handle('remove-chat-history-at-id', (event, chatID: string) => { + const vaultDir = windowsManager.getVaultDirectoryForWinContents(event.sender) if (!vaultDir) { - return; + return } const chatHistoriesMap = store.get(StoreKeys.ChatHistories); const allChatHistories = chatHistoriesMap[vaultDir] || []; - const filteredChatHistories = allChatHistories.filter( - (item) => item.id !== chatID - ); + const filteredChatHistories = allChatHistories.filter((item) => item.id !== chatID) + chatHistoriesMap[vaultDir] = filteredChatHistories.reverse(); store.set(StoreKeys.ChatHistories, chatHistoriesMap); }); - ipcMain.handle("get-current-open-files", () => { - return store.get(StoreKeys.OpenTabs) || []; - }); + ipcMain.handle('get-current-open-files', () => store.get(StoreKeys.OpenTabs) || []); - ipcMain.handle("set-current-open-files", (event, action, args) => { + ipcMain.handle('set-current-open-files', (event, action, args) => { const openTabs: Tab[] = store.get(StoreKeys.OpenTabs) || []; const addTab = ({ tab }) => { if (tab === null) return; - const existingTab = openTabs.findIndex( - (item) => item.filePath === tab.filePath - ); + const existingTab = openTabs.findIndex((item) => item.filePath === tab.filePath); /* If tab is already open, do not do anything */ if (existingTab !== -1) return; @@ -329,32 +258,26 @@ export const registerStoreHandlers = ( clearAllTabs(); break; default: - throw new Error("Unsupported action type"); + throw new Error('Unsupported action type'); } }); }; -export function getDefaultEmbeddingModelConfig( - store: Store -): EmbeddingModelConfig { - const defaultEmbeddingModelAlias = store.get( - StoreKeys.DefaultEmbeddingModelAlias - ) as string | undefined; +export function getDefaultEmbeddingModelConfig(store: Store): EmbeddingModelConfig { + const defaultEmbeddingModelAlias = store.get(StoreKeys.DefaultEmbeddingModelAlias) as string | undefined // Check if the default model alias is defined and not empty if (!defaultEmbeddingModelAlias) { - throw new Error("No default embedding model is specified"); + throw new Error('No default embedding model is specified') } - const embeddingModels = store.get(StoreKeys.EmbeddingModels) || {}; + const embeddingModels = store.get(StoreKeys.EmbeddingModels) || {} // Check if the model with the default alias exists - const model = embeddingModels[defaultEmbeddingModelAlias]; + const model = embeddingModels[defaultEmbeddingModelAlias] if (!model) { - throw new Error( - `No embedding model found for alias '${defaultEmbeddingModelAlias}'` - ); + throw new Error(`No embedding model found for alias '${defaultEmbeddingModelAlias}'`) } - return model; + return model } diff --git a/electron/main/electron-store/storeConfig.ts b/electron/main/electron-store/storeConfig.ts index 5f0b235c..78bde17c 100644 --- a/electron/main/electron-store/storeConfig.ts +++ b/electron/main/electron-store/storeConfig.ts @@ -1,54 +1,52 @@ -import { ChatHistory } from "@/components/Chat/Chat"; +import { ChatHistory } from '@/components/Chat/chatUtils' export interface BaseLLMConfig { - modelName: string; - contextLength: number; - errorMsg?: string; + modelName: string + contextLength: number + errorMsg?: string } export interface OpenAILLMConfig extends BaseLLMConfig { - type: "openai"; - engine: "openai"; - apiURL: string; - apiKey: string; + type: 'openai' + engine: 'openai' + apiURL: string + apiKey: string } export interface AnthropicLLMConfig extends BaseLLMConfig { - type: "anthropic"; - engine: "anthropic"; - apiURL: string; - apiKey: string; + type: 'anthropic' + engine: 'anthropic' + apiURL: string + apiKey: string } -export type LLMConfig = OpenAILLMConfig | AnthropicLLMConfig; +export type LLMConfig = OpenAILLMConfig | AnthropicLLMConfig export type LLMGenerationParameters = { - maxTokens?: number; - temperature?: number; -}; + maxTokens?: number + temperature?: number +} -export type EmbeddingModelConfig = - | EmbeddingModelWithRepo - | EmbeddingModelWithLocalPath; +export type EmbeddingModelConfig = EmbeddingModelWithRepo | EmbeddingModelWithLocalPath export interface EmbeddingModelWithRepo { - type: "repo"; - repoName: string; + type: 'repo' + repoName: string } export interface EmbeddingModelWithLocalPath { - type: "local"; - localPath: string; + type: 'local' + localPath: string } export type RAGConfig = { - maxRAGExamples: number; -}; + maxRAGExamples: number +} export type HardwareConfig = { - useGPU: boolean; - useCUDA: boolean; - useVulkan: boolean; -}; + useGPU: boolean + useCUDA: boolean + useVulkan: boolean +} export type Tab = { id: string; // Unique ID for the tab, useful for operations @@ -60,50 +58,50 @@ export type Tab = { }; export interface StoreSchema { - hasUserOpenedAppBefore: boolean; - schemaVersion: number; + hasUserOpenedAppBefore: boolean + schemaVersion: number user: { - vaultDirectories: string[]; - directoryFromPreviousSession?: string; - }; - LLMs: LLMConfig[]; + vaultDirectories: string[] + directoryFromPreviousSession?: string + } + LLMs: LLMConfig[] embeddingModels: { - [modelAlias: string]: EmbeddingModelConfig; - }; - defaultLLM: string; - defaultEmbedFuncRepo: string; - RAG?: RAGConfig; - hardware: HardwareConfig; - llmGenerationParameters: LLMGenerationParameters; + [modelAlias: string]: EmbeddingModelConfig + } + defaultLLM: string + defaultEmbedFuncRepo: string + RAG?: RAGConfig + hardware: HardwareConfig + llmGenerationParameters: LLMGenerationParameters chatHistories: { - [vaultDir: string]: ChatHistory[]; - }; - analytics?: boolean; - chunkSize: number; - isSBCompact: boolean; - DisplayMarkdown: boolean; - spellCheck: string; + [vaultDir: string]: ChatHistory[] + } + analytics?: boolean + chunkSize: number + isSBCompact: boolean + DisplayMarkdown: boolean + spellCheck: string EditorFlexCenter: boolean; OpenTabs: Tab[]; } export enum StoreKeys { - hasUserOpenedAppBefore = "hasUserOpenedAppBefore", - Analytics = "analytics", - SchemaVersion = "schemaVersion", - DirectoryFromPreviousSession = "user.directoryFromPreviousSession", - LLMs = "LLMs", - EmbeddingModels = "embeddingModels", - DefaultLLM = "defaultLLM", - DefaultEmbeddingModelAlias = "defaultEmbeddingModelAlias", - MaxRAGExamples = "RAG.maxRAGExamples", - Hardware = "hardware", - LLMGenerationParameters = "llmGenerationParameters", - ChatHistories = "chatHistories", - ChunkSize = "chunkSize", - IsSBCompact = "isSBCompact", - DisplayMarkdown = "DisplayMarkdown", - SpellCheck = "spellCheck", + hasUserOpenedAppBefore = 'hasUserOpenedAppBefore', + Analytics = 'analytics', + SchemaVersion = 'schemaVersion', + DirectoryFromPreviousSession = 'user.directoryFromPreviousSession', + LLMs = 'LLMs', + EmbeddingModels = 'embeddingModels', + DefaultLLM = 'defaultLLM', + DefaultEmbeddingModelAlias = 'defaultEmbeddingModelAlias', + MaxRAGExamples = 'RAG.maxRAGExamples', + Hardware = 'hardware', + LLMGenerationParameters = 'llmGenerationParameters', + ChatHistories = 'chatHistories', + ChunkSize = 'chunkSize', + IsSBCompact = 'isSBCompact', + DisplayMarkdown = 'DisplayMarkdown', + SpellCheck = 'spellCheck', EditorFlexCenter = "editorFlexCenter", OpenTabs = "OpenTabs", } diff --git a/electron/main/electron-store/storeSchemaMigrator.ts b/electron/main/electron-store/storeSchemaMigrator.ts index c2caeb71..28dafef3 100644 --- a/electron/main/electron-store/storeSchemaMigrator.ts +++ b/electron/main/electron-store/storeSchemaMigrator.ts @@ -1,89 +1,75 @@ -import Store from "electron-store"; +import Store from 'electron-store' -import { StoreKeys, StoreSchema } from "./storeConfig"; +import { StoreKeys, StoreSchema } from './storeConfig' +import { defaultEmbeddingModelRepos } from '../vector-database/embeddings' -const currentSchemaVersion = 1; - -export const initializeAndMaybeMigrateStore = (store: Store) => { - const storeSchemaVersion = store.get(StoreKeys.SchemaVersion); - if (storeSchemaVersion !== currentSchemaVersion) { - store.set(StoreKeys.SchemaVersion, currentSchemaVersion); - store.set(StoreKeys.LLMs, []); - store.set(StoreKeys.DefaultLLM, ""); - } - setupDefaultStoreValues(store); -}; - -export function setupDefaultStoreValues(store: Store) { - if (!store.get(StoreKeys.MaxRAGExamples)) { - store.set(StoreKeys.MaxRAGExamples, 15); - } - - if (!store.get(StoreKeys.ChunkSize)) { - store.set(StoreKeys.ChunkSize, 500); - } - - setupDefaultAnalyticsValue(store); - - setupDefaultSpellCheckValue(store); - - setupDefaultEmbeddingModels(store); - - setupDefaultHardwareConfig(store); -} +const currentSchemaVersion = 1 const setupDefaultAnalyticsValue = (store: Store) => { if (store.get(StoreKeys.Analytics) === undefined) { - store.set(StoreKeys.Analytics, true); + store.set(StoreKeys.Analytics, true) } -}; +} const setupDefaultSpellCheckValue = (store: Store) => { if (store.get(StoreKeys.SpellCheck) === undefined) { - store.set(StoreKeys.SpellCheck, "false"); + store.set(StoreKeys.SpellCheck, 'false') } -}; +} const setupDefaultHardwareConfig = (store: Store) => { - const hardwareConfig = store.get(StoreKeys.Hardware); + const hardwareConfig = store.get(StoreKeys.Hardware) if (!hardwareConfig) { store.set(StoreKeys.Hardware, { - useGPU: process.platform === "darwin" && process.arch === "arm64", + useGPU: process.platform === 'darwin' && process.arch === 'arm64', useCUDA: false, useVulkan: false, - }); + }) } -}; +} const setupDefaultEmbeddingModels = (store: Store) => { - const embeddingModels = store.get(StoreKeys.EmbeddingModels); + const embeddingModels = store.get(StoreKeys.EmbeddingModels) if (!embeddingModels) { - store.set(StoreKeys.EmbeddingModels, defaultEmbeddingModelRepos); + store.set(StoreKeys.EmbeddingModels, defaultEmbeddingModelRepos) } - const defaultModel = store.get(StoreKeys.DefaultEmbeddingModelAlias); + const defaultModel = store.get(StoreKeys.DefaultEmbeddingModelAlias) if (!defaultModel) { - const embeddingModels = store.get(StoreKeys.EmbeddingModels) || {}; - if (Object.keys(embeddingModels).length === 0) { - throw new Error("No embedding models found"); + const storedEmbeddingModels = store.get(StoreKeys.EmbeddingModels) || {} + if (Object.keys(storedEmbeddingModels).length === 0) { + throw new Error('No embedding models found') } - store.set( - StoreKeys.DefaultEmbeddingModelAlias, - Object.keys(embeddingModels)[0] - ); + store.set(StoreKeys.DefaultEmbeddingModelAlias, Object.keys(storedEmbeddingModels)[0]) } -}; - -const defaultEmbeddingModelRepos = { - "Xenova/bge-base-en-v1.5": { - type: "repo", - repoName: "Xenova/bge-base-en-v1.5", - }, - "Xenova/UAE-Large-V1": { type: "repo", repoName: "Xenova/UAE-Large-V1" }, - "Xenova/bge-small-en-v1.5": { - type: "repo", - repoName: "Xenova/bge-small-en-v1.5", - }, -}; +} + +export function setupDefaultStoreValues(store: Store) { + if (!store.get(StoreKeys.MaxRAGExamples)) { + store.set(StoreKeys.MaxRAGExamples, 15) + } + + if (!store.get(StoreKeys.ChunkSize)) { + store.set(StoreKeys.ChunkSize, 500) + } + + setupDefaultAnalyticsValue(store) + + setupDefaultSpellCheckValue(store) + + setupDefaultEmbeddingModels(store) + + setupDefaultHardwareConfig(store) +} + +export const initializeAndMaybeMigrateStore = (store: Store) => { + const storeSchemaVersion = store.get(StoreKeys.SchemaVersion) + if (storeSchemaVersion !== currentSchemaVersion) { + store.set(StoreKeys.SchemaVersion, currentSchemaVersion) + store.set(StoreKeys.LLMs, []) + store.set(StoreKeys.DefaultLLM, '') + } + setupDefaultStoreValues(store) +} diff --git a/electron/main/electron-utils/ipcHandlers.ts b/electron/main/electron-utils/ipcHandlers.ts index 95b156e5..08a402ab 100644 --- a/electron/main/electron-utils/ipcHandlers.ts +++ b/electron/main/electron-utils/ipcHandlers.ts @@ -1,193 +1,168 @@ -import * as fs from "fs/promises"; - -import { - app, - BrowserWindow, - dialog, - ipcMain, - Menu, - MenuItem, - shell, -} from "electron"; -import Store from "electron-store"; - -import WindowsManager from "../common/windowManager"; -import { handleAddNewNoteResponse } from "../common/newFiles"; -import { StoreKeys, StoreSchema } from "../electron-store/storeConfig"; - -export const electronUtilsHandlers = ( +import * as fs from 'fs/promises' + +import { app, BrowserWindow, dialog, ipcMain, Menu, MenuItem, shell } from 'electron' +import Store from 'electron-store' + +import WindowsManager from '../common/windowManager' +import { StoreKeys, StoreSchema } from '../electron-store/storeConfig' + +const electronUtilsHandlers = ( store: Store, windowsManager: WindowsManager, preload: string, url: string | undefined, - indexHtml: string + indexHtml: string, ) => { - ipcMain.handle("show-context-menu-item", (event) => { - const menu = new Menu(); + ipcMain.handle('show-context-menu-item', (event) => { + const menu = new Menu() menu.append( new MenuItem({ - label: "New Note", + label: 'New Note', click: () => { - event.sender.send("add-new-note-response"); + event.sender.send('add-new-note-response'); }, - }) - ); + }), + ) menu.append( new MenuItem({ - label: "New Directory", + label: 'New Directory', click: () => { - event.sender.send("add-new-directory-response"); + event.sender.send('add-new-directory-response'); }, - }) - ); + }), + ) - const browserWindow = BrowserWindow.fromWebContents(event.sender); - if (browserWindow) menu.popup({ window: browserWindow }); - }); + const browserWindow = BrowserWindow.fromWebContents(event.sender) + if (browserWindow) menu.popup({ window: browserWindow }) + }) - ipcMain.handle("show-context-menu-file-item", async (event, file) => { - const menu = new Menu(); + ipcMain.handle('show-context-menu-file-item', async (event, file) => { + const menu = new Menu() - const stats = await fs.stat(file.path); - const isDirectory = stats.isDirectory(); + const stats = await fs.stat(file.path) + const isDirectory = stats.isDirectory() if (isDirectory) { menu.append( new MenuItem({ - label: "New Note", + label: 'New Note', click: () => { - event.sender.send("add-new-note-response", file.relativePath); + event.sender.send('add-new-note-response', file.relativePath); }, - }) - ); + }), + ) menu.append( new MenuItem({ - label: "New Directory", + label: 'New Directory', click: () => { - event.sender.send("add-new-directory-response", file.path); + event.sender.send('add-new-directory-response', file.path); }, - }) - ); + }), + ) } menu.append( new MenuItem({ - label: "Delete", - click: () => { - return dialog + label: 'Delete', + click: () => + dialog .showMessageBox({ - type: "question", - title: "Delete File", + type: 'question', + title: 'Delete File', message: `Are you sure you want to delete "${file.name}"?`, - buttons: ["Yes", "No"], + buttons: ['Yes', 'No'], }) .then((confirm) => { if (confirm.response === 0) { - console.log(file.path); - event.sender.send("delete-file-listener", file.path); + event.sender.send('delete-file-listener', file.path) } - }); - }, - }) - ); + }), + }), + ) menu.append( new MenuItem({ - label: "Rename", + label: 'Rename', click: () => { - console.log(file.path); - event.sender.send("rename-file-listener", file.path); + event.sender.send('rename-file-listener', file.path) }, - }) - ); + }), + ) menu.append( new MenuItem({ - label: "Create a flashcard set", + label: 'Create a flashcard set', click: () => { - console.log("creating: ", file.path); - event.sender.send("create-flashcard-file-listener", file.path); + event.sender.send('create-flashcard-file-listener', file.path) }, - }) - ); + }), + ) menu.append( new MenuItem({ - label: "Add file to chat context", + label: 'Add file to chat context', click: () => { - console.log("creating: ", file.path); - event.sender.send("add-file-to-chat-listener", file.path); + event.sender.send('add-file-to-chat-listener', file.path) }, - }) - ); - - console.log("menu key: ", file); + }), + ) - const browserWindow = BrowserWindow.fromWebContents(event.sender); + const browserWindow = BrowserWindow.fromWebContents(event.sender) if (browserWindow) { - menu.popup({ window: browserWindow }); + menu.popup({ window: browserWindow }) } - }); + }) - ipcMain.handle("show-chat-menu-item", (event, chatID) => { - const menu = new Menu(); + ipcMain.handle('show-chat-menu-item', (event, chatID) => { + const menu = new Menu() menu.append( new MenuItem({ - label: "Delete Chat", + label: 'Delete Chat', click: () => { - const vaultDir = windowsManager.getVaultDirectoryForWinContents( - event.sender - ); + const vaultDir = windowsManager.getVaultDirectoryForWinContents(event.sender) if (!vaultDir) { - return; + return } - const chatHistoriesMap = store.get(StoreKeys.ChatHistories); - const allChatHistories = chatHistoriesMap[vaultDir] || []; - const filteredChatHistories = allChatHistories.filter( - (item) => item.id !== chatID - ); - chatHistoriesMap[vaultDir] = filteredChatHistories; - store.set(StoreKeys.ChatHistories, chatHistoriesMap); - event.sender.send( - "update-chat-histories", - chatHistoriesMap[vaultDir] || [] - ); + const chatHistoriesMap = store.get(StoreKeys.ChatHistories) + const allChatHistories = chatHistoriesMap[vaultDir] || [] + const filteredChatHistories = allChatHistories.filter((item) => item.id !== chatID) + chatHistoriesMap[vaultDir] = filteredChatHistories + store.set(StoreKeys.ChatHistories, chatHistoriesMap) + event.sender.send('update-chat-histories', chatHistoriesMap[vaultDir] || []) }, - }) - ); + }), + ) - const browserWindow = BrowserWindow.fromWebContents(event.sender); - if (browserWindow) menu.popup({ window: browserWindow }); - }); + const browserWindow = BrowserWindow.fromWebContents(event.sender) + if (browserWindow) menu.popup({ window: browserWindow }) + }) - ipcMain.handle("open-external", (event, url) => { - shell.openExternal(url); - }); + ipcMain.handle('open-external', (event, _url) => { + shell.openExternal(_url) + }) - ipcMain.handle("get-platform", async () => { - return process.platform; - }); + ipcMain.handle('get-platform', async () => process.platform) - ipcMain.handle("open-new-window", () => { - windowsManager.createWindow(store, preload, url, indexHtml); - }); + ipcMain.handle('open-new-window', () => { + windowsManager.createWindow(store, preload, url, indexHtml) + }) - ipcMain.handle("get-reor-app-version", async () => { - return app.getVersion(); - }); + ipcMain.handle('get-reor-app-version', async () => app.getVersion()); // Used on EmptyPage.tsx to create a new file - ipcMain.handle("empty-new-note-listener", (event, relativePath) => { - event.sender.send("add-new-note-response", relativePath); + ipcMain.handle('empty-new-note-listener', (event, relativePath) => { + event.sender.send('add-new-note-response', relativePath); }); // Used on EmptyPage.tsx to create a new directory - ipcMain.handle("empty-new-directory-listener", (event, relativePath) => { - event.sender.send("add-new-directory-response", relativePath); + ipcMain.handle('empty-new-directory-listener', (event, relativePath) => { + event.sender.send('add-new-directory-response', relativePath); }); }; + +export default electronUtilsHandlers diff --git a/electron/main/filesystem/Filesystem.ts b/electron/main/filesystem/Filesystem.ts deleted file mode 100644 index bfa96264..00000000 --- a/electron/main/filesystem/Filesystem.ts +++ /dev/null @@ -1,303 +0,0 @@ -import * as fs from "fs"; -import * as fsPromises from "fs/promises"; -import * as path from "path"; - -import chokidar from "chokidar"; -import { BrowserWindow } from "electron"; - -import { LanceDBTableWrapper } from "../vector-database/lanceTableWrapper"; -import { - addFileTreeToDBTable, - removeFileTreeFromDBTable, -} from "../vector-database/tableHelperFunctions"; - -import { FileInfo, FileInfoTree, isFileNodeDirectory } from "./types"; - -export const markdownExtensions = [ - ".md", - ".markdown", - ".mdown", - ".mkdn", - ".mkd", -]; - -export function GetFilesInfoList(directory: string): FileInfo[] { - const fileInfoTree = GetFilesInfoTree(directory); - const fileInfoList = flattenFileInfoTree(fileInfoTree); - return fileInfoList; -} - -export function GetFilesInfoListForListOfPaths(paths: string[]): FileInfo[] { - // so perhaps for this function, all we maybe need to do is remove - const fileInfoTree = paths.map((path) => GetFilesInfoTree(path)).flat(); - const fileInfoList = flattenFileInfoTree(fileInfoTree); - - // remove duplicates: - const uniquePaths = new Set(); - const fileInfoListWithoutDuplicates = fileInfoList.filter((fileInfo) => { - if (uniquePaths.has(fileInfo.path)) { - return false; - } - uniquePaths.add(fileInfo.path); - return true; - }); - return fileInfoListWithoutDuplicates; -} - -export function GetFilesInfoTree( - pathInput: string, - parentRelativePath: string = "" -): FileInfoTree { - const fileInfoTree: FileInfoTree = []; - - if (!fs.existsSync(pathInput)) { - console.error("Path does not exist:", pathInput); - return fileInfoTree; - } - - try { - const stats = fs.statSync(pathInput); - if (stats.isFile()) { - if ( - fileHasExtensionInList(pathInput, markdownExtensions) && - !isHidden(path.basename(pathInput)) - ) { - fileInfoTree.push({ - name: path.basename(pathInput), - path: pathInput, - relativePath: parentRelativePath, - dateModified: stats.mtime, - dateCreated: stats.birthtime, // Add the birthtime property here - }); - } - } else { - const itemsInDir = fs - .readdirSync(pathInput) - .filter((item) => !isHidden(item)); - - const childNodes: FileInfoTree = itemsInDir - .map((item) => { - const itemPath = path.join(pathInput, item); - return GetFilesInfoTree( - itemPath, - path.join(parentRelativePath, item) - ); - }) - .flat(); - - if (parentRelativePath === "") { - return childNodes; - } - if (!isHidden(path.basename(pathInput))) { - fileInfoTree.push({ - name: path.basename(pathInput), - path: pathInput, - relativePath: parentRelativePath, - dateModified: stats.mtime, - dateCreated: stats.birthtime, - children: childNodes, - }); - } - } - } catch (error) { - console.error(`Error accessing ${pathInput}:`, error); - } - - return fileInfoTree; -} -export function isHidden(fileName: string): boolean { - return fileName.startsWith("."); -} -export function flattenFileInfoTree(tree: FileInfoTree): FileInfo[] { - let flatList: FileInfo[] = []; - - for (const node of tree) { - if (!isFileNodeDirectory(node)) { - flatList.push({ - name: node.name, - path: node.path, - relativePath: node.relativePath, - dateModified: node.dateModified, - dateCreated: node.dateCreated, - }); - } - - if (isFileNodeDirectory(node) && node.children) { - flatList = flatList.concat(flattenFileInfoTree(node.children)); - } - } - - return flatList; -} - -export function createFileRecursive( - filePath: string, - content: string, - charset?: BufferEncoding -): void { - const dirname = path.dirname(filePath); - - if (!fs.existsSync(dirname)) { - fs.mkdirSync(dirname, { recursive: true }); - } - - if (fs.existsSync(filePath)) { - return; - } - - fs.writeFileSync(filePath, content, charset); -} - -export function startWatchingDirectory( - win: BrowserWindow, - directoryToWatch: string -): chokidar.FSWatcher | undefined { - try { - const watcher = chokidar.watch(directoryToWatch, { - ignoreInitial: true, - }); - - const handleFileEvent = (eventType: string, filePath: string) => { - if ( - fileHasExtensionInList(filePath, markdownExtensions) || - eventType.includes("directory") - ) { - // TODO: add logic to update vector db - updateFileListForRenderer(win, directoryToWatch); - } - }; - - watcher - .on("add", (path) => handleFileEvent("added", path)) - .on("change", (path) => handleFileEvent("changed", path)) - .on("unlink", (path) => handleFileEvent("removed", path)) - .on("addDir", (path) => handleFileEvent("directory added", path)) - .on("unlinkDir", (path) => handleFileEvent("directory removed", path)); - - // No 'ready' event handler is needed here, as we're ignoring initial scan - return watcher; - } catch (error) { - console.error("Error setting up file watcher:", error); - } -} - -function fileHasExtensionInList( - filePath: string, - extensions: string[] -): boolean { - try { - const fileExtension = path.extname(filePath).toLowerCase(); - return extensions.includes(fileExtension); - } catch (error) { - console.error("Error checking file extension for extensions:", extensions); - return false; - } -} - -export function appendExtensionIfMissing( - filename: string, - extensions: string[] -): string { - const hasExtension = extensions.some((ext) => filename.endsWith(ext)); - - if (hasExtension) { - return filename; - } - - return filename + extensions[0]; -} - -export function updateFileListForRenderer( - win: BrowserWindow, - directory: string -): void { - const files = GetFilesInfoTree(directory); - if (win) { - win.webContents.send("files-list", files); - } -} - -export function readFile(filePath: string): string { - try { - const data = fs.readFileSync(filePath, "utf8"); - return data; - } catch (err) { - console.error("An error occurred:", err); - return ""; - } -} - -export const orchestrateEntryMove = async ( - table: LanceDBTableWrapper, - sourcePath: string, - destinationPath: string -) => { - const fileSystemTree = GetFilesInfoTree(sourcePath); - await removeFileTreeFromDBTable(table, fileSystemTree); - moveFileOrDirectoryInFileSystem(sourcePath, destinationPath).then( - (newDestinationPath) => { - if (newDestinationPath) { - addFileTreeToDBTable(table, GetFilesInfoTree(newDestinationPath)); - } - } - ); -}; - -export const moveFileOrDirectoryInFileSystem = async ( - sourcePath: string, - destinationPath: string -): Promise => { - try { - try { - await fsPromises.access(sourcePath); - } catch (error) { - throw new Error("Source path does not exist."); - } - - let destinationStats; - try { - destinationStats = await fsPromises.lstat(destinationPath); - } catch (error) { - // Error means destination path does not exist, which is fine - console.error("Error accessing destination path:", error); - } - - if (destinationStats && destinationStats.isFile()) { - destinationPath = path.dirname(destinationPath); - } - - await fsPromises.mkdir(destinationPath, { recursive: true }); - - const newPath = path.join(destinationPath, path.basename(sourcePath)); - await fsPromises.rename(sourcePath, newPath); - - console.log(`Moved ${sourcePath} to ${newPath}`); - return newPath; - } catch (error) { - console.error("Error moving file or directory:", error); - return ""; - } -}; - -export function splitDirectoryPathIntoBaseAndRepo(fullPath: string) { - const normalizedPath = path.normalize(fullPath); - - const pathWithSeparator = normalizedPath.endsWith(path.sep) - ? normalizedPath - : `${normalizedPath}${path.sep}`; - - if ( - path.dirname(pathWithSeparator.slice(0, -1)) === - pathWithSeparator.slice(0, -1) - ) { - return { - localModelPath: "", // No directory path before the root - repoName: path.basename(pathWithSeparator.slice(0, -1)), // Root directory name - }; - } - - const localModelPath = path.dirname(pathWithSeparator.slice(0, -1)); - const repoName = path.basename(pathWithSeparator.slice(0, -1)); - - return { localModelPath, repoName }; -} diff --git a/electron/main/filesystem/Types.ts b/electron/main/filesystem/Types.ts deleted file mode 100644 index f05c4326..00000000 --- a/electron/main/filesystem/Types.ts +++ /dev/null @@ -1,33 +0,0 @@ -export type FileInfo = { - name: string; - path: string; - relativePath: string; - dateModified: Date; - dateCreated: Date; -}; - -export type FileInfoNode = FileInfo & { - children?: FileInfoNode[]; -}; - -export type FileInfoTree = FileInfoNode[]; - -export const isFileNodeDirectory = (fileInfo: FileInfoNode): boolean => { - return fileInfo.children !== undefined; -}; - -export interface AugmentPromptWithFileProps { - prompt: string; - llmName: string; - filePath: string; -} - -export type WriteFileProps = { - filePath: string; - content: string; -}; - -export type RenameFileProps = { - oldFilePath: string; - newFilePath: string; -}; diff --git a/electron/main/filesystem/filesystem.test.ts b/electron/main/filesystem/filesystem.test.ts index 0203b0b1..54a15197 100644 --- a/electron/main/filesystem/filesystem.test.ts +++ b/electron/main/filesystem/filesystem.test.ts @@ -1,32 +1,32 @@ -import * as fs from "fs"; -import * as path from "path"; +import * as fs from 'fs' +import * as path from 'path' -import * as tmp from "tmp"; +import * as tmp from 'tmp' -import { GetFilesInfoTree } from "./filesystem"; +import { GetFilesInfoTree } from './filesystem' -describe("GetFilesInfoTree", () => { - let tempDir: string; +describe('GetFilesInfoTree', () => { + let tempDir: string beforeEach(() => { - tempDir = tmp.dirSync({ unsafeCleanup: true }).name; - }); + tempDir = tmp.dirSync({ unsafeCleanup: true }).name + }) afterEach(() => { - fs.rmdirSync(tempDir, { recursive: true }); - }); + fs.rmdirSync(tempDir, { recursive: true }) + }) - it("should handle empty directories", () => { - const result = GetFilesInfoTree(tempDir); - expect(result).toEqual([]); - }); + it('should handle empty directories', () => { + const result = GetFilesInfoTree(tempDir) + expect(result).toEqual([]) + }) + + it('should correctly map a single file', () => { + const filename = 'test.md' + const filePath = path.join(tempDir, filename) + fs.writeFileSync(filePath, 'Test content') + const result = GetFilesInfoTree(tempDir) - it("should correctly map a single file", () => { - const filename = "test.md"; - const filePath = path.join(tempDir, filename); - fs.writeFileSync(filePath, "Test content"); - const result = GetFilesInfoTree(tempDir); - console.log("result", result); expect(result).toEqual([ { name: filename, @@ -36,26 +36,26 @@ describe("GetFilesInfoTree", () => { dateCreated: expect.anything(), // children: undefined, }, - ]); + ]) // expect(result[0].dateModified).toBeInstanceOf(Date); - }); + }) - it("should correctly map nested directories and files", () => { - const dirName = "nested"; - const nestedDirPath = path.join(tempDir, dirName); - fs.mkdirSync(nestedDirPath); + it('should correctly map nested directories and files', () => { + const dirName = 'nested' + const nestedDirPath = path.join(tempDir, dirName) + fs.mkdirSync(nestedDirPath) - const filename = "nestedFile.md"; - const nestedFilePath = path.join(nestedDirPath, filename); - fs.writeFileSync(nestedFilePath, "Nested test content"); + const filename = 'nestedFile.md' + const nestedFilePath = path.join(nestedDirPath, filename) + fs.writeFileSync(nestedFilePath, 'Nested test content') - const result = GetFilesInfoTree(tempDir); + const result = GetFilesInfoTree(tempDir) expect(result).toEqual([ { name: dirName, path: nestedDirPath, - relativePath: "nested", + relativePath: 'nested', dateModified: expect.anything(), dateCreated: expect.anything(), children: [ @@ -69,6 +69,6 @@ describe("GetFilesInfoTree", () => { }, ], }, - ]); - }); -}); + ]) + }) +}) diff --git a/electron/main/filesystem/filesystem.ts b/electron/main/filesystem/filesystem.ts index bfa96264..7cad6a8c 100644 --- a/electron/main/filesystem/filesystem.ts +++ b/electron/main/filesystem/filesystem.ts @@ -1,92 +1,58 @@ -import * as fs from "fs"; -import * as fsPromises from "fs/promises"; -import * as path from "path"; +import * as fs from 'fs' +import * as fsPromises from 'fs/promises' +import * as path from 'path' -import chokidar from "chokidar"; -import { BrowserWindow } from "electron"; +import chokidar from 'chokidar' +import { BrowserWindow } from 'electron' -import { LanceDBTableWrapper } from "../vector-database/lanceTableWrapper"; -import { - addFileTreeToDBTable, - removeFileTreeFromDBTable, -} from "../vector-database/tableHelperFunctions"; +import { FileInfo, FileInfoTree, isFileNodeDirectory } from './types' -import { FileInfo, FileInfoTree, isFileNodeDirectory } from "./types"; +export const markdownExtensions = ['.md', '.markdown', '.mdown', '.mkdn', '.mkd'] -export const markdownExtensions = [ - ".md", - ".markdown", - ".mdown", - ".mkdn", - ".mkd", -]; - -export function GetFilesInfoList(directory: string): FileInfo[] { - const fileInfoTree = GetFilesInfoTree(directory); - const fileInfoList = flattenFileInfoTree(fileInfoTree); - return fileInfoList; +export function isHidden(fileName: string): boolean { + return fileName.startsWith('.') } -export function GetFilesInfoListForListOfPaths(paths: string[]): FileInfo[] { - // so perhaps for this function, all we maybe need to do is remove - const fileInfoTree = paths.map((path) => GetFilesInfoTree(path)).flat(); - const fileInfoList = flattenFileInfoTree(fileInfoTree); - - // remove duplicates: - const uniquePaths = new Set(); - const fileInfoListWithoutDuplicates = fileInfoList.filter((fileInfo) => { - if (uniquePaths.has(fileInfo.path)) { - return false; - } - uniquePaths.add(fileInfo.path); - return true; - }); - return fileInfoListWithoutDuplicates; +function fileHasExtensionInList(filePath: string, extensions: string[]): boolean { + try { + const fileExtension = path.extname(filePath).toLowerCase() + return extensions.includes(fileExtension) + } catch (error) { + return false + } } -export function GetFilesInfoTree( - pathInput: string, - parentRelativePath: string = "" -): FileInfoTree { - const fileInfoTree: FileInfoTree = []; +export function GetFilesInfoTree(pathInput: string, parentRelativePath: string = ''): FileInfoTree { + const fileInfoTree: FileInfoTree = [] if (!fs.existsSync(pathInput)) { - console.error("Path does not exist:", pathInput); - return fileInfoTree; + return fileInfoTree } try { - const stats = fs.statSync(pathInput); + const stats = fs.statSync(pathInput) if (stats.isFile()) { - if ( - fileHasExtensionInList(pathInput, markdownExtensions) && - !isHidden(path.basename(pathInput)) - ) { + if (fileHasExtensionInList(pathInput, markdownExtensions) && !isHidden(path.basename(pathInput))) { fileInfoTree.push({ name: path.basename(pathInput), path: pathInput, relativePath: parentRelativePath, dateModified: stats.mtime, dateCreated: stats.birthtime, // Add the birthtime property here - }); + }) } } else { - const itemsInDir = fs - .readdirSync(pathInput) - .filter((item) => !isHidden(item)); + const itemsInDir = fs.readdirSync(pathInput).filter((item) => !isHidden(item)) const childNodes: FileInfoTree = itemsInDir .map((item) => { - const itemPath = path.join(pathInput, item); - return GetFilesInfoTree( - itemPath, - path.join(parentRelativePath, item) - ); + const itemPath = path.join(pathInput, item) + return GetFilesInfoTree(itemPath, path.join(parentRelativePath, item)) }) - .flat(); + .flat() - if (parentRelativePath === "") { - return childNodes; + if (parentRelativePath === '') { + return childNodes } if (!isHidden(path.basename(pathInput))) { fileInfoTree.push({ @@ -96,22 +62,18 @@ export function GetFilesInfoTree( dateModified: stats.mtime, dateCreated: stats.birthtime, children: childNodes, - }); + }) } } } catch (error) { - console.error(`Error accessing ${pathInput}:`, error); + // no need to throw error } - return fileInfoTree; -} -export function isHidden(fileName: string): boolean { - return fileName.startsWith("."); + return fileInfoTree } -export function flattenFileInfoTree(tree: FileInfoTree): FileInfo[] { - let flatList: FileInfo[] = []; - for (const node of tree) { +export function flattenFileInfoTree(tree: FileInfoTree): FileInfo[] { + return tree.reduce((flatList: FileInfo[], node) => { if (!isFileNodeDirectory(node)) { flatList.push({ name: node.name, @@ -119,185 +81,150 @@ export function flattenFileInfoTree(tree: FileInfoTree): FileInfo[] { relativePath: node.relativePath, dateModified: node.dateModified, dateCreated: node.dateCreated, - }); + }) } - if (isFileNodeDirectory(node) && node.children) { - flatList = flatList.concat(flattenFileInfoTree(node.children)); + flatList.push(...flattenFileInfoTree(node.children)) } - } + return flatList + }, []) +} + +export function GetFilesInfoList(directory: string): FileInfo[] { + const fileInfoTree = GetFilesInfoTree(directory) + const fileInfoList = flattenFileInfoTree(fileInfoTree) + return fileInfoList +} + +export function GetFilesInfoListForListOfPaths(paths: string[]): FileInfo[] { + // so perhaps for this function, all we maybe need to do is remove + const fileInfoTree = paths.map((_path) => GetFilesInfoTree(_path)).flat() + const fileInfoList = flattenFileInfoTree(fileInfoTree) - return flatList; + // remove duplicates: + const uniquePaths = new Set() + const fileInfoListWithoutDuplicates = fileInfoList.filter((fileInfo) => { + if (uniquePaths.has(fileInfo.path)) { + return false + } + uniquePaths.add(fileInfo.path) + return true + }) + return fileInfoListWithoutDuplicates } -export function createFileRecursive( - filePath: string, - content: string, - charset?: BufferEncoding -): void { - const dirname = path.dirname(filePath); +export function createFileRecursive(filePath: string, content: string, charset?: BufferEncoding): void { + const dirname = path.dirname(filePath) if (!fs.existsSync(dirname)) { - fs.mkdirSync(dirname, { recursive: true }); + fs.mkdirSync(dirname, { recursive: true }) } if (fs.existsSync(filePath)) { - return; + return } - fs.writeFileSync(filePath, content, charset); + fs.writeFileSync(filePath, content, charset) } -export function startWatchingDirectory( - win: BrowserWindow, - directoryToWatch: string -): chokidar.FSWatcher | undefined { +export function updateFileListForRenderer(win: BrowserWindow, directory: string): void { + const files = GetFilesInfoTree(directory) + if (win) { + win.webContents.send('files-list', files) + } +} + +export function startWatchingDirectory(win: BrowserWindow, directoryToWatch: string): chokidar.FSWatcher | undefined { try { const watcher = chokidar.watch(directoryToWatch, { ignoreInitial: true, - }); + }) const handleFileEvent = (eventType: string, filePath: string) => { - if ( - fileHasExtensionInList(filePath, markdownExtensions) || - eventType.includes("directory") - ) { + if (fileHasExtensionInList(filePath, markdownExtensions) || eventType.includes('directory')) { // TODO: add logic to update vector db - updateFileListForRenderer(win, directoryToWatch); + updateFileListForRenderer(win, directoryToWatch) } - }; + } watcher - .on("add", (path) => handleFileEvent("added", path)) - .on("change", (path) => handleFileEvent("changed", path)) - .on("unlink", (path) => handleFileEvent("removed", path)) - .on("addDir", (path) => handleFileEvent("directory added", path)) - .on("unlinkDir", (path) => handleFileEvent("directory removed", path)); + .on('add', (_path) => handleFileEvent('added', _path)) + .on('change', (_path) => handleFileEvent('changed', _path)) + .on('unlink', (_path) => handleFileEvent('removed', _path)) + .on('addDir', (_path) => handleFileEvent('directory added', _path)) + .on('unlinkDir', (_path) => handleFileEvent('directory removed', _path)) // No 'ready' event handler is needed here, as we're ignoring initial scan - return watcher; - } catch (error) { - console.error("Error setting up file watcher:", error); - } -} - -function fileHasExtensionInList( - filePath: string, - extensions: string[] -): boolean { - try { - const fileExtension = path.extname(filePath).toLowerCase(); - return extensions.includes(fileExtension); + return watcher } catch (error) { - console.error("Error checking file extension for extensions:", extensions); - return false; + // no error + return undefined } } -export function appendExtensionIfMissing( - filename: string, - extensions: string[] -): string { - const hasExtension = extensions.some((ext) => filename.endsWith(ext)); +export function appendExtensionIfMissing(filename: string, extensions: string[]): string { + const hasExtension = extensions.some((ext) => filename.endsWith(ext)) if (hasExtension) { - return filename; + return filename } - return filename + extensions[0]; -} - -export function updateFileListForRenderer( - win: BrowserWindow, - directory: string -): void { - const files = GetFilesInfoTree(directory); - if (win) { - win.webContents.send("files-list", files); - } + return filename + extensions[0] } export function readFile(filePath: string): string { try { - const data = fs.readFileSync(filePath, "utf8"); - return data; + const data = fs.readFileSync(filePath, 'utf8') + return data } catch (err) { - console.error("An error occurred:", err); - return ""; + return '' } } -export const orchestrateEntryMove = async ( - table: LanceDBTableWrapper, - sourcePath: string, - destinationPath: string -) => { - const fileSystemTree = GetFilesInfoTree(sourcePath); - await removeFileTreeFromDBTable(table, fileSystemTree); - moveFileOrDirectoryInFileSystem(sourcePath, destinationPath).then( - (newDestinationPath) => { - if (newDestinationPath) { - addFileTreeToDBTable(table, GetFilesInfoTree(newDestinationPath)); - } - } - ); -}; - -export const moveFileOrDirectoryInFileSystem = async ( - sourcePath: string, - destinationPath: string -): Promise => { +export const moveFileOrDirectoryInFileSystem = async (sourcePath: string, destinationPath: string): Promise => { try { try { - await fsPromises.access(sourcePath); + await fsPromises.access(sourcePath) } catch (error) { - throw new Error("Source path does not exist."); + throw new Error('Source path does not exist.') } - let destinationStats; + let destinationStats try { - destinationStats = await fsPromises.lstat(destinationPath); + destinationStats = await fsPromises.lstat(destinationPath) } catch (error) { // Error means destination path does not exist, which is fine - console.error("Error accessing destination path:", error); } - + let resolvedDestinationPath = destinationPath if (destinationStats && destinationStats.isFile()) { - destinationPath = path.dirname(destinationPath); + resolvedDestinationPath = path.dirname(destinationPath) } - await fsPromises.mkdir(destinationPath, { recursive: true }); + await fsPromises.mkdir(resolvedDestinationPath, { recursive: true }) - const newPath = path.join(destinationPath, path.basename(sourcePath)); - await fsPromises.rename(sourcePath, newPath); + const newPath = path.join(resolvedDestinationPath, path.basename(sourcePath)) + await fsPromises.rename(sourcePath, newPath) - console.log(`Moved ${sourcePath} to ${newPath}`); - return newPath; + return newPath } catch (error) { - console.error("Error moving file or directory:", error); - return ""; + return '' } -}; +} export function splitDirectoryPathIntoBaseAndRepo(fullPath: string) { - const normalizedPath = path.normalize(fullPath); + const normalizedPath = path.normalize(fullPath) - const pathWithSeparator = normalizedPath.endsWith(path.sep) - ? normalizedPath - : `${normalizedPath}${path.sep}`; + const pathWithSeparator = normalizedPath.endsWith(path.sep) ? normalizedPath : `${normalizedPath}${path.sep}` - if ( - path.dirname(pathWithSeparator.slice(0, -1)) === - pathWithSeparator.slice(0, -1) - ) { + if (path.dirname(pathWithSeparator.slice(0, -1)) === pathWithSeparator.slice(0, -1)) { return { - localModelPath: "", // No directory path before the root + localModelPath: '', // No directory path before the root repoName: path.basename(pathWithSeparator.slice(0, -1)), // Root directory name - }; + } } - const localModelPath = path.dirname(pathWithSeparator.slice(0, -1)); - const repoName = path.basename(pathWithSeparator.slice(0, -1)); + const localModelPath = path.dirname(pathWithSeparator.slice(0, -1)) + const repoName = path.basename(pathWithSeparator.slice(0, -1)) - return { localModelPath, repoName }; + return { localModelPath, repoName } } diff --git a/electron/main/filesystem/ipcHandlers.ts b/electron/main/filesystem/ipcHandlers.ts index 91d7c619..1db83d61 100644 --- a/electron/main/filesystem/ipcHandlers.ts +++ b/electron/main/filesystem/ipcHandlers.ts @@ -1,27 +1,24 @@ -import * as fs from "fs"; -import * as path from "path"; - -import { ipcMain, BrowserWindow, dialog } from "electron"; -import Store from "electron-store"; - -import WindowsManager from "../common/windowManager"; -import { StoreKeys, StoreSchema } from "../electron-store/storeConfig"; -import { - createPromptWithContextLimitFromContent, - PromptWithContextLimit, -} from "../llm/contextLimit"; -import { ollamaService, openAISession } from "../llm/ipcHandlers"; -import { getLLMConfig } from "../llm/llmConfig"; -import { addExtensionToFilenameIfNoExtensionPresent } from "../path/path"; -import { DBEntry } from "../vector-database/schema"; +import * as fs from 'fs' +import * as path from 'path' + +import { ipcMain, BrowserWindow, dialog } from 'electron' +import Store from 'electron-store' + +import WindowsManager from '../common/windowManager' +import { StoreKeys, StoreSchema } from '../electron-store/storeConfig' +import { createPromptWithContextLimitFromContent, PromptWithContextLimit } from '../llm/contextLimit' +import { ollamaService, openAISession } from '../llm/ipcHandlers' +import { getLLMConfig } from '../llm/llmConfig' +import addExtensionToFilenameIfNoExtensionPresent from '../path/path' +import { DBEntry } from '../vector-database/schema' import { convertFileInfoListToDBItems, + orchestrateEntryMove, updateFileInTable, -} from "../vector-database/tableHelperFunctions"; +} from '../vector-database/tableHelperFunctions' import { GetFilesInfoTree, - orchestrateEntryMove, createFileRecursive, isHidden, GetFilesInfoListForListOfPaths, @@ -29,339 +26,230 @@ import { markdownExtensions, startWatchingDirectory, updateFileListForRenderer, -} from "./filesystem"; -import { - FileInfoTree, - AugmentPromptWithFileProps, - WriteFileProps, - RenameFileProps, -} from "./types"; - -export const registerFileHandlers = ( - store: Store, - windowsManager: WindowsManager -) => { - ipcMain.handle( - "get-files-tree-for-window", - async (event): Promise => { - const directoryPath = windowsManager.getVaultDirectoryForWinContents( - event.sender - ); - if (!directoryPath) return []; - - const files: FileInfoTree = GetFilesInfoTree(directoryPath); - return files; - } - ); +} from './filesystem' +import { FileInfoTree, AugmentPromptWithFileProps, WriteFileProps, RenameFileProps } from './types' - ipcMain.handle( - "read-file", - async (event, filePath: string): Promise => { - return fs.readFileSync(filePath, "utf-8"); - } - ); +const registerFileHandlers = (store: Store, _windowsManager: WindowsManager) => { + const windowsManager = _windowsManager + ipcMain.handle('get-files-tree-for-window', async (event): Promise => { + const directoryPath = windowsManager.getVaultDirectoryForWinContents(event.sender) + if (!directoryPath) return [] + + const files: FileInfoTree = GetFilesInfoTree(directoryPath) + return files + }) - ipcMain.handle("check-file-exists", async (event, filePath) => { + ipcMain.handle('read-file', async (event, filePath: string): Promise => fs.readFileSync(filePath, 'utf-8')) + + ipcMain.handle('check-file-exists', async (event, filePath) => { try { // Attempt to access the file to check existence - await fs.promises.access(filePath, fs.constants.F_OK); + await fs.promises.access(filePath, fs.constants.F_OK) // If access is successful, return true - return true; + return true } catch (error) { // If an error occurs (e.g., file doesn't exist), return false - return false; + return false } - }); + }) - ipcMain.handle( - "delete-file", - async (event, filePath: string): Promise => { - console.log("Deleting file", filePath); - fs.stat(filePath, async (err, stats) => { - if (err) { - console.error("An error occurred:", err); - return; - } + ipcMain.handle('delete-file', async (event, filePath: string): Promise => { + fs.stat(filePath, async (err, stats) => { + if (err) { + return + } - if (stats.isDirectory()) { - // For directories (Node.js v14.14.0 and later) - fs.rm(filePath, { recursive: true }, (err) => { - if (err) { - console.error("An error occurred:", err); - return; - } - console.log(`Directory at ${filePath} was deleted successfully.`); - }); - - const windowInfo = windowsManager.getWindowInfoForContents( - event.sender - ); - if (!windowInfo) { - throw new Error("Window info not found."); - } - await windowInfo.dbTableClient.deleteDBItemsByFilePaths([filePath]); - } else { - fs.unlink(filePath, (err) => { - if (err) { - console.error("An error occurred:", err); - return; - } - console.log(`File at ${filePath} was deleted successfully.`); - }); - - const windowInfo = windowsManager.getWindowInfoForContents( - event.sender - ); - if (!windowInfo) { - throw new Error("Window info not found."); - } - await windowInfo.dbTableClient.deleteDBItemsByFilePaths([filePath]); + if (stats.isDirectory()) { + // For directories (Node.js v14.14.0 and later) + fs.rm(filePath, { recursive: true }, () => { + // hi + }) + + const windowInfo = windowsManager.getWindowInfoForContents(event.sender) + if (!windowInfo) { + throw new Error('Window info not found.') } - }); - } - ); + await windowInfo.dbTableClient.deleteDBItemsByFilePaths([filePath]) + } else { + fs.unlink(filePath, () => { + // hi + }) - ipcMain.handle( - "write-file", - async (event, writeFileProps: WriteFileProps) => { - if (!fs.existsSync(path.dirname(writeFileProps.filePath))) { - fs.mkdirSync(path.dirname(writeFileProps.filePath), { - recursive: true, - }); + const windowInfo = windowsManager.getWindowInfoForContents(event.sender) + if (!windowInfo) { + throw new Error('Window info not found.') + } + await windowInfo.dbTableClient.deleteDBItemsByFilePaths([filePath]) } - fs.writeFileSync( - writeFileProps.filePath, - writeFileProps.content, - "utf-8" - ); + }) + }) + + ipcMain.handle('write-file', async (event, writeFileProps: WriteFileProps) => { + if (!fs.existsSync(path.dirname(writeFileProps.filePath))) { + fs.mkdirSync(path.dirname(writeFileProps.filePath), { + recursive: true, + }) } - ); + fs.writeFileSync(writeFileProps.filePath, writeFileProps.content, 'utf-8') + }) - ipcMain.handle("is-directory", (event, filepath: string) => { - return fs.statSync(filepath).isDirectory(); - }); + ipcMain.handle('is-directory', (event, filepath: string) => fs.statSync(filepath).isDirectory()) - ipcMain.handle( - "rename-file-recursive", - async (event, renameFileProps: RenameFileProps) => { - const windowInfo = windowsManager.getWindowInfoForContents(event.sender); + ipcMain.handle('rename-file-recursive', async (event, renameFileProps: RenameFileProps) => { + const windowInfo = windowsManager.getWindowInfoForContents(event.sender) - if (!windowInfo) { - throw new Error("Window info not found."); - } + if (!windowInfo) { + throw new Error('Window info not found.') + } - windowsManager.watcher?.unwatch(windowInfo?.vaultDirectoryForWindow); - - if (process.platform == "win32") { - windowsManager.watcher?.close().then(() => { - fs.rename( - renameFileProps.oldFilePath, - renameFileProps.newFilePath, - (err) => { - if (err) { - throw err; - } - - // Re-start watching all paths in array - const win = BrowserWindow.fromWebContents(event.sender); - if (win) { - windowsManager.watcher = startWatchingDirectory( - win, - windowInfo.vaultDirectoryForWindow - ); - updateFileListForRenderer( - win, - windowInfo.vaultDirectoryForWindow - ); - } - } - ); - }); - } else { - // On non-Windows platforms, directly perform the rename operation - fs.rename( - renameFileProps.oldFilePath, - renameFileProps.newFilePath, - (err) => { - if (err) { - throw err; - } - // Re-watch the vault directory after renaming - windowsManager.watcher?.add(windowInfo?.vaultDirectoryForWindow); + windowsManager.watcher?.unwatch(windowInfo?.vaultDirectoryForWindow) + + if (process.platform === 'win32') { + windowsManager.watcher?.close().then(() => { + fs.rename(renameFileProps.oldFilePath, renameFileProps.newFilePath, (err) => { + if (err) { + throw err } - ); - } - console.log("reindexing folder"); - // then need to trigger reindexing of folder - windowInfo.dbTableClient.updateDBItemsWithNewFilePath( - renameFileProps.oldFilePath, - renameFileProps.newFilePath - ); + // Re-start watching all paths in array + const win = BrowserWindow.fromWebContents(event.sender) + if (win) { + windowsManager.watcher = startWatchingDirectory(win, windowInfo.vaultDirectoryForWindow) + updateFileListForRenderer(win, windowInfo.vaultDirectoryForWindow) + } + }) + }) + } else { + // On non-Windows platforms, directly perform the rename operation + fs.rename(renameFileProps.oldFilePath, renameFileProps.newFilePath, (err) => { + if (err) { + throw err + } + // Re-watch the vault directory after renaming + windowsManager.watcher?.add(windowInfo?.vaultDirectoryForWindow) + }) } - ); - ipcMain.handle("index-file-in-database", async (event, filePath: string) => { - const windowInfo = windowsManager.getWindowInfoForContents(event.sender); - if (!windowInfo) { - throw new Error("Window info not found."); - } - await updateFileInTable(windowInfo.dbTableClient, filePath); - }); + // then need to trigger reindexing of folder + windowInfo.dbTableClient.updateDBItemsWithNewFilePath(renameFileProps.oldFilePath, renameFileProps.newFilePath) + }) - ipcMain.handle( - "create-file", - async (event, filePath: string, content: string): Promise => { - createFileRecursive(filePath, content, "utf-8"); + ipcMain.handle('index-file-in-database', async (event, filePath: string) => { + const windowInfo = windowsManager.getWindowInfoForContents(event.sender) + if (!windowInfo) { + throw new Error('Window info not found.') } - ); - - ipcMain.handle( - "create-directory", - async (event, dirPath: string): Promise => { - console.log("Creating directory", dirPath); - - const mkdirRecursiveSync = (dirPath: string) => { - const parentDir = path.dirname(dirPath); - if (!fs.existsSync(parentDir)) { - mkdirRecursiveSync(parentDir); - } - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath); - } - }; - - if (!fs.existsSync(dirPath)) { - mkdirRecursiveSync(dirPath); - } else { - console.log("Directory already exists:", dirPath); + await updateFileInTable(windowInfo.dbTableClient, filePath) + }) + + ipcMain.handle('create-file', async (event, filePath: string, content: string): Promise => { + createFileRecursive(filePath, content, 'utf-8') + }) + + ipcMain.handle('create-directory', async (event, dirPath: string): Promise => { + const mkdirRecursiveSync = (_dirPath: string) => { + const parentDir = path.dirname(_dirPath) + if (!fs.existsSync(parentDir)) { + mkdirRecursiveSync(parentDir) + } + if (!fs.existsSync(_dirPath)) { + fs.mkdirSync(_dirPath) } } - ); - ipcMain.handle( - "move-file-or-dir", - async (event, sourcePath: string, destinationPath: string) => { - const windowInfo = windowsManager.getWindowInfoForContents(event.sender); - if (!windowInfo) { - throw new Error("Window info not found."); - } - orchestrateEntryMove( - windowInfo.dbTableClient, - sourcePath, - destinationPath - ); + if (!fs.existsSync(dirPath)) { + mkdirRecursiveSync(dirPath) } - ); + }) - ipcMain.handle( - "augment-prompt-with-file", - async ( - _event, - { prompt, llmName, filePath }: AugmentPromptWithFileProps - ): Promise => { - try { - const content = fs.readFileSync(filePath, "utf-8"); - - const llmSession = openAISession; - const llmConfig = await getLLMConfig(store, ollamaService, llmName); - if (!llmConfig) { - throw new Error(`LLM ${llmName} not configured.`); - } - const systemPrompt = "Based on the following information:\n"; - const { prompt: filePrompt, contextCutoffAt } = - createPromptWithContextLimitFromContent( - content, - systemPrompt, - prompt, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - return { prompt: filePrompt, contextCutoffAt }; - } catch (error) { - console.error("Error searching database:", error); - throw error; - } + ipcMain.handle('move-file-or-dir', async (event, sourcePath: string, destinationPath: string) => { + const windowInfo = windowsManager.getWindowInfoForContents(event.sender) + if (!windowInfo) { + throw new Error('Window info not found.') } - ); + orchestrateEntryMove(windowInfo.dbTableClient, sourcePath, destinationPath) + }) ipcMain.handle( - "get-filesystem-paths-as-db-items", - async (_event, filePaths: string[]): Promise => { - try { - const fileItems = GetFilesInfoListForListOfPaths(filePaths); - console.log("fileItems", fileItems); - const dbItems = await convertFileInfoListToDBItems(fileItems); - console.log("dbItems", dbItems); - return dbItems.flat(); - } catch (error) { - console.error("Error searching database:", error); - throw error; + 'augment-prompt-with-file', + async (_event, { prompt, llmName, filePath }: AugmentPromptWithFileProps): Promise => { + const content = fs.readFileSync(filePath, 'utf-8') + + const llmSession = openAISession + const llmConfig = await getLLMConfig(store, ollamaService, llmName) + if (!llmConfig) { + throw new Error(`LLM ${llmName} not configured.`) } - } - ); + const systemPrompt = 'Based on the following information:\n' + const { prompt: filePrompt, contextCutoffAt } = createPromptWithContextLimitFromContent( + content, + systemPrompt, + prompt, + llmSession.getTokenizer(llmName), + llmConfig.contextLength, + ) + return { prompt: filePrompt, contextCutoffAt } + }, + ) + + ipcMain.handle('get-filesystem-paths-as-db-items', async (_event, filePaths: string[]): Promise => { + const fileItems = GetFilesInfoListForListOfPaths(filePaths) + + const dbItems = await convertFileInfoListToDBItems(fileItems) + + return dbItems.flat() + }) ipcMain.handle( - "generate-flashcards-from-file", - async ( - event, - { prompt, llmName, filePath }: AugmentPromptWithFileProps - ): Promise => { + 'generate-flashcards-from-file', + async (event, { prompt, llmName, filePath }: AugmentPromptWithFileProps): Promise => { // actual response required { question: string, answer: string} [] - const llmSession = openAISession; - console.log("llmName: ", llmName); - const llmConfig = await getLLMConfig(store, ollamaService, llmName); - console.log("llmConfig", llmConfig); + const llmSession = openAISession + + const llmConfig = await getLLMConfig(store, ollamaService, llmName) + if (!llmConfig) { - throw new Error(`LLM ${llmName} not configured.`); + throw new Error(`LLM ${llmName} not configured.`) } if (!filePath) { - throw new Error( - "Current file path is not provided for flashcard agent." - ); + throw new Error('Current file path is not provided for flashcard agent.') } - const fileResults = fs.readFileSync(filePath, "utf-8"); - const { prompt: promptToCreateAtomicFacts } = - createPromptWithContextLimitFromContent( - fileResults, - "", - `Extract atomic facts that can be used for students to study, based on this query: ${prompt}`, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); + const fileResults = fs.readFileSync(filePath, 'utf-8') + const { prompt: promptToCreateAtomicFacts } = createPromptWithContextLimitFromContent( + fileResults, + '', + `Extract atomic facts that can be used for students to study, based on this query: ${prompt}`, + llmSession.getTokenizer(llmName), + llmConfig.contextLength, + ) const llmGeneratedFacts = await llmSession.response( llmName, llmConfig, [ { - role: "system", + role: 'system', content: `You are an experienced teacher reading through some notes a student has made and extracting atomic facts. You never come up with your own facts. You generate atomic facts directly from what you read. An atomic fact is a fact that relates to a single piece of knowledge and makes it easy to create a question for which the atomic fact is the answer"`, }, { - role: "user", + role: 'user', content: promptToCreateAtomicFacts, }, ], false, - store.get(StoreKeys.LLMGenerationParameters) - ); + store.get(StoreKeys.LLMGenerationParameters), + ) - const basePrompt = "Given the following atomic facts:\n"; + const basePrompt = 'Given the following atomic facts:\n' const flashcardQuery = - "Create useful FLASHCARDS that can be used for students to study using ONLY the context. Format is Q: A: ."; - const { prompt: promptToCreateFlashcardsWithAtomicFacts } = - createPromptWithContextLimitFromContent( - llmGeneratedFacts.choices[0].message.content || "", - basePrompt, - flashcardQuery, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - console.log( - "promptToCreateFlashcardsWithAtomicFacts: ", - promptToCreateFlashcardsWithAtomicFacts - ); + 'Create useful FLASHCARDS that can be used for students to study using ONLY the context. Format is Q: A: .' + const { prompt: promptToCreateFlashcardsWithAtomicFacts } = createPromptWithContextLimitFromContent( + llmGeneratedFacts.choices[0].message.content || '', + basePrompt, + flashcardQuery, + llmSession.getTokenizer(llmName), + llmConfig.contextLength, + ) // call the query to respond const llmGeneratedFlashcards = await llmSession.response( @@ -369,76 +257,62 @@ export const registerFileHandlers = ( llmConfig, [ { - role: "system", + role: 'system', content: `You are an experienced teacher that is reading some facts given to you so that you can generate flashcards as JSON for your student for review. You never come up with your own facts. You will generate flashcards using the atomic facts given. An atomic fact is a fact that relates to a single piece of knowledge and makes it easy to create a question for which the atomic fact is the answer"`, }, { - role: "user", + role: 'user', content: promptToCreateFlashcardsWithAtomicFacts, }, ], true, - store.get(StoreKeys.LLMGenerationParameters) - ); - const content = llmGeneratedFlashcards.choices[0].message.content || ""; - return content; - } - ); - - ipcMain.handle("get-files-in-directory", (event, dirName: string) => { - const itemsInDir = fs - .readdirSync(dirName) - .filter((item) => !isHidden(item)); - return itemsInDir; - }); - - ipcMain.handle( - "get-files-in-directory-recursive", - (event, dirName: string) => { - const fileNameSet = new Set(); - - const fileList = GetFilesInfoList(dirName); - fileList.forEach((file) => { - fileNameSet.add( - addExtensionToFilenameIfNoExtensionPresent( - file.path, - markdownExtensions, - ".md" - ) - ); - }); - return Array.from(fileNameSet); - } - ); - - ipcMain.handle("open-directory-dialog", async () => { + store.get(StoreKeys.LLMGenerationParameters), + ) + const content = llmGeneratedFlashcards.choices[0].message.content || '' + return content + }, + ) + + ipcMain.handle('get-files-in-directory', (event, dirName: string) => { + const itemsInDir = fs.readdirSync(dirName).filter((item) => !isHidden(item)) + return itemsInDir + }) + + ipcMain.handle('get-files-in-directory-recursive', (event, dirName: string) => { + const fileNameSet = new Set() + + const fileList = GetFilesInfoList(dirName) + fileList.forEach((file) => { + fileNameSet.add(addExtensionToFilenameIfNoExtensionPresent(file.path, markdownExtensions, '.md')) + }) + return Array.from(fileNameSet) + }) + + ipcMain.handle('open-directory-dialog', async () => { const result = await dialog.showOpenDialog({ - properties: ["openDirectory", "createDirectory"], - }); + properties: ['openDirectory', 'createDirectory'], + }) if (!result.canceled) { - return result.filePaths; - } else { - return null; + return result.filePaths } - }); + return null + }) - ipcMain.handle("open-file-dialog", async (event, extensions) => { - const filters = - extensions && extensions.length > 0 - ? [{ name: "Files", extensions }] - : []; + ipcMain.handle('open-file-dialog', async (event, extensions) => { + const filters = extensions && extensions.length > 0 ? [{ name: 'Files', extensions }] : [] const result = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections", "showHiddenFiles"], // Add 'showHiddenFiles' here - filters: filters, - }); + properties: ['openFile', 'multiSelections', 'showHiddenFiles'], // Add 'showHiddenFiles' here + filters, + }) if (!result.canceled) { - return result.filePaths; - } else { - return []; + return result.filePaths } - }); -}; + return [] + }) +} + +export default registerFileHandlers diff --git a/electron/main/filesystem/registerFilesHandler.ts b/electron/main/filesystem/registerFilesHandler.ts deleted file mode 100644 index 91d7c619..00000000 --- a/electron/main/filesystem/registerFilesHandler.ts +++ /dev/null @@ -1,444 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; - -import { ipcMain, BrowserWindow, dialog } from "electron"; -import Store from "electron-store"; - -import WindowsManager from "../common/windowManager"; -import { StoreKeys, StoreSchema } from "../electron-store/storeConfig"; -import { - createPromptWithContextLimitFromContent, - PromptWithContextLimit, -} from "../llm/contextLimit"; -import { ollamaService, openAISession } from "../llm/ipcHandlers"; -import { getLLMConfig } from "../llm/llmConfig"; -import { addExtensionToFilenameIfNoExtensionPresent } from "../path/path"; -import { DBEntry } from "../vector-database/schema"; -import { - convertFileInfoListToDBItems, - updateFileInTable, -} from "../vector-database/tableHelperFunctions"; - -import { - GetFilesInfoTree, - orchestrateEntryMove, - createFileRecursive, - isHidden, - GetFilesInfoListForListOfPaths, - GetFilesInfoList, - markdownExtensions, - startWatchingDirectory, - updateFileListForRenderer, -} from "./filesystem"; -import { - FileInfoTree, - AugmentPromptWithFileProps, - WriteFileProps, - RenameFileProps, -} from "./types"; - -export const registerFileHandlers = ( - store: Store, - windowsManager: WindowsManager -) => { - ipcMain.handle( - "get-files-tree-for-window", - async (event): Promise => { - const directoryPath = windowsManager.getVaultDirectoryForWinContents( - event.sender - ); - if (!directoryPath) return []; - - const files: FileInfoTree = GetFilesInfoTree(directoryPath); - return files; - } - ); - - ipcMain.handle( - "read-file", - async (event, filePath: string): Promise => { - return fs.readFileSync(filePath, "utf-8"); - } - ); - - ipcMain.handle("check-file-exists", async (event, filePath) => { - try { - // Attempt to access the file to check existence - await fs.promises.access(filePath, fs.constants.F_OK); - // If access is successful, return true - return true; - } catch (error) { - // If an error occurs (e.g., file doesn't exist), return false - return false; - } - }); - - ipcMain.handle( - "delete-file", - async (event, filePath: string): Promise => { - console.log("Deleting file", filePath); - fs.stat(filePath, async (err, stats) => { - if (err) { - console.error("An error occurred:", err); - return; - } - - if (stats.isDirectory()) { - // For directories (Node.js v14.14.0 and later) - fs.rm(filePath, { recursive: true }, (err) => { - if (err) { - console.error("An error occurred:", err); - return; - } - console.log(`Directory at ${filePath} was deleted successfully.`); - }); - - const windowInfo = windowsManager.getWindowInfoForContents( - event.sender - ); - if (!windowInfo) { - throw new Error("Window info not found."); - } - await windowInfo.dbTableClient.deleteDBItemsByFilePaths([filePath]); - } else { - fs.unlink(filePath, (err) => { - if (err) { - console.error("An error occurred:", err); - return; - } - console.log(`File at ${filePath} was deleted successfully.`); - }); - - const windowInfo = windowsManager.getWindowInfoForContents( - event.sender - ); - if (!windowInfo) { - throw new Error("Window info not found."); - } - await windowInfo.dbTableClient.deleteDBItemsByFilePaths([filePath]); - } - }); - } - ); - - ipcMain.handle( - "write-file", - async (event, writeFileProps: WriteFileProps) => { - if (!fs.existsSync(path.dirname(writeFileProps.filePath))) { - fs.mkdirSync(path.dirname(writeFileProps.filePath), { - recursive: true, - }); - } - fs.writeFileSync( - writeFileProps.filePath, - writeFileProps.content, - "utf-8" - ); - } - ); - - ipcMain.handle("is-directory", (event, filepath: string) => { - return fs.statSync(filepath).isDirectory(); - }); - - ipcMain.handle( - "rename-file-recursive", - async (event, renameFileProps: RenameFileProps) => { - const windowInfo = windowsManager.getWindowInfoForContents(event.sender); - - if (!windowInfo) { - throw new Error("Window info not found."); - } - - windowsManager.watcher?.unwatch(windowInfo?.vaultDirectoryForWindow); - - if (process.platform == "win32") { - windowsManager.watcher?.close().then(() => { - fs.rename( - renameFileProps.oldFilePath, - renameFileProps.newFilePath, - (err) => { - if (err) { - throw err; - } - - // Re-start watching all paths in array - const win = BrowserWindow.fromWebContents(event.sender); - if (win) { - windowsManager.watcher = startWatchingDirectory( - win, - windowInfo.vaultDirectoryForWindow - ); - updateFileListForRenderer( - win, - windowInfo.vaultDirectoryForWindow - ); - } - } - ); - }); - } else { - // On non-Windows platforms, directly perform the rename operation - fs.rename( - renameFileProps.oldFilePath, - renameFileProps.newFilePath, - (err) => { - if (err) { - throw err; - } - // Re-watch the vault directory after renaming - windowsManager.watcher?.add(windowInfo?.vaultDirectoryForWindow); - } - ); - } - - console.log("reindexing folder"); - // then need to trigger reindexing of folder - windowInfo.dbTableClient.updateDBItemsWithNewFilePath( - renameFileProps.oldFilePath, - renameFileProps.newFilePath - ); - } - ); - - ipcMain.handle("index-file-in-database", async (event, filePath: string) => { - const windowInfo = windowsManager.getWindowInfoForContents(event.sender); - if (!windowInfo) { - throw new Error("Window info not found."); - } - await updateFileInTable(windowInfo.dbTableClient, filePath); - }); - - ipcMain.handle( - "create-file", - async (event, filePath: string, content: string): Promise => { - createFileRecursive(filePath, content, "utf-8"); - } - ); - - ipcMain.handle( - "create-directory", - async (event, dirPath: string): Promise => { - console.log("Creating directory", dirPath); - - const mkdirRecursiveSync = (dirPath: string) => { - const parentDir = path.dirname(dirPath); - if (!fs.existsSync(parentDir)) { - mkdirRecursiveSync(parentDir); - } - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath); - } - }; - - if (!fs.existsSync(dirPath)) { - mkdirRecursiveSync(dirPath); - } else { - console.log("Directory already exists:", dirPath); - } - } - ); - - ipcMain.handle( - "move-file-or-dir", - async (event, sourcePath: string, destinationPath: string) => { - const windowInfo = windowsManager.getWindowInfoForContents(event.sender); - if (!windowInfo) { - throw new Error("Window info not found."); - } - orchestrateEntryMove( - windowInfo.dbTableClient, - sourcePath, - destinationPath - ); - } - ); - - ipcMain.handle( - "augment-prompt-with-file", - async ( - _event, - { prompt, llmName, filePath }: AugmentPromptWithFileProps - ): Promise => { - try { - const content = fs.readFileSync(filePath, "utf-8"); - - const llmSession = openAISession; - const llmConfig = await getLLMConfig(store, ollamaService, llmName); - if (!llmConfig) { - throw new Error(`LLM ${llmName} not configured.`); - } - const systemPrompt = "Based on the following information:\n"; - const { prompt: filePrompt, contextCutoffAt } = - createPromptWithContextLimitFromContent( - content, - systemPrompt, - prompt, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - return { prompt: filePrompt, contextCutoffAt }; - } catch (error) { - console.error("Error searching database:", error); - throw error; - } - } - ); - - ipcMain.handle( - "get-filesystem-paths-as-db-items", - async (_event, filePaths: string[]): Promise => { - try { - const fileItems = GetFilesInfoListForListOfPaths(filePaths); - console.log("fileItems", fileItems); - const dbItems = await convertFileInfoListToDBItems(fileItems); - console.log("dbItems", dbItems); - return dbItems.flat(); - } catch (error) { - console.error("Error searching database:", error); - throw error; - } - } - ); - - ipcMain.handle( - "generate-flashcards-from-file", - async ( - event, - { prompt, llmName, filePath }: AugmentPromptWithFileProps - ): Promise => { - // actual response required { question: string, answer: string} [] - const llmSession = openAISession; - console.log("llmName: ", llmName); - const llmConfig = await getLLMConfig(store, ollamaService, llmName); - console.log("llmConfig", llmConfig); - if (!llmConfig) { - throw new Error(`LLM ${llmName} not configured.`); - } - if (!filePath) { - throw new Error( - "Current file path is not provided for flashcard agent." - ); - } - const fileResults = fs.readFileSync(filePath, "utf-8"); - const { prompt: promptToCreateAtomicFacts } = - createPromptWithContextLimitFromContent( - fileResults, - "", - `Extract atomic facts that can be used for students to study, based on this query: ${prompt}`, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - const llmGeneratedFacts = await llmSession.response( - llmName, - llmConfig, - [ - { - role: "system", - content: `You are an experienced teacher reading through some notes a student has made and extracting atomic facts. You never come up with your own facts. You generate atomic facts directly from what you read. - An atomic fact is a fact that relates to a single piece of knowledge and makes it easy to create a question for which the atomic fact is the answer"`, - }, - { - role: "user", - content: promptToCreateAtomicFacts, - }, - ], - false, - store.get(StoreKeys.LLMGenerationParameters) - ); - - const basePrompt = "Given the following atomic facts:\n"; - const flashcardQuery = - "Create useful FLASHCARDS that can be used for students to study using ONLY the context. Format is Q: A: ."; - const { prompt: promptToCreateFlashcardsWithAtomicFacts } = - createPromptWithContextLimitFromContent( - llmGeneratedFacts.choices[0].message.content || "", - basePrompt, - flashcardQuery, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - console.log( - "promptToCreateFlashcardsWithAtomicFacts: ", - promptToCreateFlashcardsWithAtomicFacts - ); - - // call the query to respond - const llmGeneratedFlashcards = await llmSession.response( - llmName, - llmConfig, - [ - { - role: "system", - content: `You are an experienced teacher that is reading some facts given to you so that you can generate flashcards as JSON for your student for review. - You never come up with your own facts. You will generate flashcards using the atomic facts given. - An atomic fact is a fact that relates to a single piece of knowledge and makes it easy to create a question for which the atomic fact is the answer"`, - }, - { - role: "user", - content: promptToCreateFlashcardsWithAtomicFacts, - }, - ], - true, - store.get(StoreKeys.LLMGenerationParameters) - ); - const content = llmGeneratedFlashcards.choices[0].message.content || ""; - return content; - } - ); - - ipcMain.handle("get-files-in-directory", (event, dirName: string) => { - const itemsInDir = fs - .readdirSync(dirName) - .filter((item) => !isHidden(item)); - return itemsInDir; - }); - - ipcMain.handle( - "get-files-in-directory-recursive", - (event, dirName: string) => { - const fileNameSet = new Set(); - - const fileList = GetFilesInfoList(dirName); - fileList.forEach((file) => { - fileNameSet.add( - addExtensionToFilenameIfNoExtensionPresent( - file.path, - markdownExtensions, - ".md" - ) - ); - }); - return Array.from(fileNameSet); - } - ); - - ipcMain.handle("open-directory-dialog", async () => { - const result = await dialog.showOpenDialog({ - properties: ["openDirectory", "createDirectory"], - }); - if (!result.canceled) { - return result.filePaths; - } else { - return null; - } - }); - - ipcMain.handle("open-file-dialog", async (event, extensions) => { - const filters = - extensions && extensions.length > 0 - ? [{ name: "Files", extensions }] - : []; - - const result = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections", "showHiddenFiles"], // Add 'showHiddenFiles' here - filters: filters, - }); - - if (!result.canceled) { - return result.filePaths; - } else { - return []; - } - }); -}; diff --git a/electron/main/filesystem/types.ts b/electron/main/filesystem/types.ts index f05c4326..0aea13ca 100644 --- a/electron/main/filesystem/types.ts +++ b/electron/main/filesystem/types.ts @@ -1,33 +1,31 @@ export type FileInfo = { - name: string; - path: string; - relativePath: string; - dateModified: Date; - dateCreated: Date; -}; + name: string + path: string + relativePath: string + dateModified: Date + dateCreated: Date +} export type FileInfoNode = FileInfo & { - children?: FileInfoNode[]; -}; + children?: FileInfoNode[] +} -export type FileInfoTree = FileInfoNode[]; +export type FileInfoTree = FileInfoNode[] -export const isFileNodeDirectory = (fileInfo: FileInfoNode): boolean => { - return fileInfo.children !== undefined; -}; +export const isFileNodeDirectory = (fileInfo: FileInfoNode): boolean => fileInfo.children !== undefined export interface AugmentPromptWithFileProps { - prompt: string; - llmName: string; - filePath: string; + prompt: string + llmName: string + filePath: string } export type WriteFileProps = { - filePath: string; - content: string; -}; + filePath: string + content: string +} export type RenameFileProps = { - oldFilePath: string; - newFilePath: string; -}; + oldFilePath: string + newFilePath: string +} diff --git a/electron/main/index.ts b/electron/main/index.ts index df8433d9..e5fb6517 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,75 +1,73 @@ -import { release } from "node:os"; -import { join } from "node:path"; +import { release } from 'node:os' +import { join } from 'node:path' -import { app, BrowserWindow } from "electron"; -import Store from "electron-store"; +import { app, BrowserWindow } from 'electron' +import Store from 'electron-store' -import { errorToStringMainProcess } from "./common/error"; -import WindowsManager from "./common/windowManager"; -import { registerStoreHandlers } from "./electron-store/ipcHandlers"; -import { StoreSchema } from "./electron-store/storeConfig"; -import { electronUtilsHandlers } from "./electron-utils/ipcHandlers"; -import { registerFileHandlers } from "./filesystem/ipcHandlers"; -import { ollamaService, registerLLMSessionHandlers } from "./llm/ipcHandlers"; -import { pathHandlers } from "./path/ipcHandlers"; -import { registerDBSessionHandlers } from "./vector-database/ipcHandlers"; +import errorToStringMainProcess from './common/error' +import WindowsManager from './common/windowManager' +import { registerStoreHandlers } from './electron-store/ipcHandlers' +import { StoreSchema } from './electron-store/storeConfig' +import electronUtilsHandlers from './electron-utils/ipcHandlers' +import registerFileHandlers from './filesystem/ipcHandlers' +import { ollamaService, registerLLMSessionHandlers } from './llm/ipcHandlers' +import pathHandlers from './path/ipcHandlers' +import { registerDBSessionHandlers } from './vector-database/ipcHandlers' -const store = new Store(); +const store = new Store() // store.clear(); // clear store for testing CAUTION: THIS WILL DELETE YOUR CHAT HISTORY -const windowsManager = new WindowsManager(); +const windowsManager = new WindowsManager() -process.env.DIST_ELECTRON = join(__dirname, "../"); -process.env.DIST = join(process.env.DIST_ELECTRON, "../dist"); +process.env.DIST_ELECTRON = join(__dirname, '../') +process.env.DIST = join(process.env.DIST_ELECTRON, '../dist') process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL - ? join(process.env.DIST_ELECTRON, "../public") - : process.env.DIST; + ? join(process.env.DIST_ELECTRON, '../public') + : process.env.DIST // Disable GPU Acceleration for Windows 7 -if (release().startsWith("6.1")) app.disableHardwareAcceleration(); +if (release().startsWith('6.1')) app.disableHardwareAcceleration() // Set application name for Windows 10+ notifications -if (process.platform === "win32") app.setAppUserModelId(app.getName()); +if (process.platform === 'win32') app.setAppUserModelId(app.getName()) if (!app.requestSingleInstanceLock()) { - app.quit(); - process.exit(0); + app.quit() + process.exit(0) } -const preload = join(__dirname, "../preload/index.js"); -const url = process.env.VITE_DEV_SERVER_URL; -const indexHtml = join(process.env.DIST, "index.html"); +const preload = join(__dirname, '../preload/index.js') +const url = process.env.VITE_DEV_SERVER_URL +const indexHtml = join(process.env.DIST, 'index.html') app.whenReady().then(async () => { try { - await ollamaService.init(); + await ollamaService.init() } catch (error) { - windowsManager.appendNewErrorToDisplayInWindow( - errorToStringMainProcess(error) - ); + windowsManager.appendNewErrorToDisplayInWindow(errorToStringMainProcess(error)) } - windowsManager.createWindow(store, preload, url, indexHtml); -}); + windowsManager.createWindow(store, preload, url, indexHtml) +}) -app.on("window-all-closed", () => { - if (process.platform !== "darwin") app.quit(); -}); +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') app.quit() +}) -app.on("before-quit", async () => { - ollamaService.stop(); -}); +app.on('before-quit', async () => { + ollamaService.stop() +}) -app.on("activate", () => { - const allWindows = BrowserWindow.getAllWindows(); +app.on('activate', () => { + const allWindows = BrowserWindow.getAllWindows() if (allWindows.length) { - allWindows[0].focus(); + allWindows[0].focus() } else { - windowsManager.createWindow(store, preload, url, indexHtml); + windowsManager.createWindow(store, preload, url, indexHtml) } -}); +}) -registerLLMSessionHandlers(store); -registerDBSessionHandlers(store, windowsManager); -registerStoreHandlers(store, windowsManager); -registerFileHandlers(store, windowsManager); -electronUtilsHandlers(store, windowsManager, preload, url, indexHtml); -pathHandlers(); +registerLLMSessionHandlers(store) +registerDBSessionHandlers(store, windowsManager) +registerStoreHandlers(store, windowsManager) +registerFileHandlers(store, windowsManager) +electronUtilsHandlers(store, windowsManager, preload, url, indexHtml) +pathHandlers() diff --git a/electron/main/llm/Types.ts b/electron/main/llm/Types.ts deleted file mode 100644 index 20729f98..00000000 --- a/electron/main/llm/Types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { MessageStreamEvent } from "@anthropic-ai/sdk/resources"; -import { - ChatCompletionChunk, - ChatCompletionMessageParam, -} from "openai/resources/chat/completions"; - -import { - LLMGenerationParameters, - LLMConfig, -} from "../electron-store/storeConfig"; - -// Any LLM engine should implement this interface: -export interface LLMSessionService { - /** - * Initializes the session. - * @returns A promise that resolves when the initialization is complete. - */ - // init( - // modelName: string, - // modelConfig: BaseLLMConfig, - // hardwareConfig: HardwareConfig - // ): Promise; - /** - * Handles the streaming of prompts. - * @param prompt The prompt to be streamed. - * @param sendFunctionImplementer The implementer of the send function. - * @returns A promise that resolves to a string response. - */ - streamingResponse( - modelName: string, - modelConfig: LLMConfig, - isJSONMode: boolean, - messageHistory: Array, - chunkResponse: (chunk: ChatCompletionChunk | MessageStreamEvent) => void, - generationParams?: LLMGenerationParameters - ): Promise; - - getTokenizer: (llmName: string) => (text: string) => number[]; - abort(): void; -} - -export interface ISendFunctionImplementer { - /** - * Sends a message to the specified channel with optional arguments. - * @param channel The channel to send the message to. - * @param args Additional arguments for the message. - */ - send(channel: string, ...args: unknown[]): void; -} diff --git a/electron/main/llm/contextLimit.ts b/electron/main/llm/contextLimit.ts index 4b528e2d..083ee453 100644 --- a/electron/main/llm/contextLimit.ts +++ b/electron/main/llm/contextLimit.ts @@ -1,83 +1,90 @@ -import { DBEntry } from "../vector-database/schema"; +import { DBEntry } from '../vector-database/schema' + export interface PromptWithContextLimit { - prompt: string; - contextCutoffAt?: string; + prompt: string + contextCutoffAt?: string } -export function createPromptWithContextLimitFromContent( +export const createPromptWithContextLimitFromContent = ( content: string | DBEntry[], basePrompt: string, query: string, tokenize: (text: string) => number[], - contextLimit: number -): PromptWithContextLimit { - let tokenCount = tokenize(query + basePrompt).length; - - const contentArray: string[] = []; - let cutOffLine: string = ""; - const contents = - typeof content === "string" - ? content.split("\n") - : content.map((entry) => entry.content); + contextLimit: number, +): PromptWithContextLimit => { + const initialTokenCount = tokenize(query + basePrompt).length + const contents = Array.isArray(content) ? content.map((entry) => entry.content) : content.split('\n') - for (const line of contents) { - const lineWithNewLine = line + "\n"; - if (tokenize(lineWithNewLine).length + tokenCount < contextLimit * 0.9) { - tokenCount += tokenize(lineWithNewLine).length; - contentArray.push(lineWithNewLine); - } else if (cutOffLine.length === 0) { - cutOffLine = lineWithNewLine; - } - } + const { contentArray, cutOffLine } = contents.reduce<{ + contentArray: string[] + tokenCount: number + cutOffLine: string + }>( + ({ contentArray: _contentArray, tokenCount, cutOffLine: _cutOffLine }, line) => { + const lineWithNewLine = `${line}\n` + const lineTokens = tokenize(lineWithNewLine).length - const outputPrompt = basePrompt + contentArray.join("") + query; + if (lineTokens + tokenCount < contextLimit * 0.9) { + return { + contentArray: [..._contentArray, lineWithNewLine], + tokenCount: tokenCount + lineTokens, + cutOffLine: _cutOffLine, + } + } + if (_cutOffLine.length === 0) { + return { contentArray: _contentArray, tokenCount, cutOffLine: lineWithNewLine } + } + return { contentArray: _contentArray, tokenCount, cutOffLine: _cutOffLine } + }, + { contentArray: [], tokenCount: initialTokenCount, cutOffLine: '' }, + ) + const outputPrompt = basePrompt + contentArray.join('') + query return { prompt: outputPrompt, contextCutoffAt: cutOffLine || undefined, - }; + } } -export function sliceListOfStringsToContextLength( +export const sliceListOfStringsToContextLength = ( strings: string[], tokenize: (text: string) => number[], - contextLimit: number -): string[] { - let tokenCount = 0; - const result: string[] = []; - - for (const string of strings) { - const tokens = tokenize(string); - const newTokenCount = tokenCount + tokens.length; - if (newTokenCount > contextLimit) break; - result.push(string); - tokenCount = newTokenCount; - } - - return result; + contextLimit: number, +): string[] => { + return strings.reduce<{ result: string[]; tokenCount: number }>( + ({ result, tokenCount }, string) => { + const tokens = tokenize(string) + const newTokenCount = tokenCount + tokens.length + if (newTokenCount > contextLimit) { + return { result, tokenCount } + } + return { + result: [...result, string], + tokenCount: newTokenCount, + } + }, + { result: [], tokenCount: 0 }, + ).result } -export function sliceStringToContextLength( +export const sliceStringToContextLength = ( inputString: string, tokenize: (text: string) => number[], - contextLimit: number -): string { - let tokenCount = 0; - let result = ""; - - // Split the input string into segments that are likely to be tokenized. - // This assumes a whitespace tokenizer; adjust the split logic as needed for your tokenizer. - const segments = inputString.split(/(\s+)/); - - for (const segment of segments) { - const tokens = tokenize(segment); - const newTokenCount = tokenCount + tokens.length; - - if (newTokenCount > contextLimit) break; - - result += segment; - tokenCount = newTokenCount; - } - - return result; + contextLimit: number, +): string => { + const segments = inputString.split(/(\s+)/) + return segments.reduce<{ result: string; tokenCount: number }>( + ({ result, tokenCount }, segment) => { + const tokens = tokenize(segment) + const newTokenCount = tokenCount + tokens.length + if (newTokenCount > contextLimit) { + return { result, tokenCount } + } + return { + result: result + segment, + tokenCount: newTokenCount, + } + }, + { result: '', tokenCount: 0 }, + ).result } diff --git a/electron/main/llm/ipcHandlers.ts b/electron/main/llm/ipcHandlers.ts index 823ab181..639ca6a0 100644 --- a/electron/main/llm/ipcHandlers.ts +++ b/electron/main/llm/ipcHandlers.ts @@ -1,60 +1,48 @@ -import { ipcMain, IpcMainInvokeEvent } from "electron"; -import Store from "electron-store"; -import { ProgressResponse } from "ollama"; -import { ChatCompletionChunk } from "openai/resources/chat/completions"; - -import { - LLMConfig, - StoreKeys, - StoreSchema, -} from "../electron-store/storeConfig"; - -import { - sliceListOfStringsToContextLength, - sliceStringToContextLength, -} from "./contextLimit"; -import { - addOrUpdateLLMSchemaInStore, - getAllLLMConfigs, - getLLMConfig, - removeLLM, -} from "./llmConfig"; -import { AnthropicModelSessionService } from "./models/Anthropic"; -import { OllamaService } from "./models/Ollama"; -import { OpenAIModelSessionService } from "./models/OpenAI"; -import { LLMSessionService } from "./types"; - -import { Query } from "@/components/Editor/QueryInput"; +import { MessageStreamEvent } from '@anthropic-ai/sdk/resources' +import { ipcMain, IpcMainInvokeEvent } from 'electron' +import Store from 'electron-store' +import { ProgressResponse } from 'ollama' +import { ChatCompletionChunk } from 'openai/resources/chat/completions' + +import { LLMConfig, StoreKeys, StoreSchema } from '../electron-store/storeConfig' + +import { sliceListOfStringsToContextLength, sliceStringToContextLength } from './contextLimit' +import { addOrUpdateLLMSchemaInStore, removeLLM, getAllLLMConfigs, getLLMConfig } from './llmConfig' +import AnthropicModelSessionService from './models/Anthropic' +import OllamaService from './models/Ollama' +import OpenAIModelSessionService from './models/OpenAI' +import { LLMSessionService } from './types' +import { ChatHistory } from '@/components/Chat/chatUtils' enum LLMType { - OpenAI = "openai", - Anthropic = "anthropic", + OpenAI = 'openai', + Anthropic = 'anthropic', } -export const LLMSessions: { [sessionId: string]: LLMSessionService } = {}; +export const LLMSessions: { [sessionId: string]: LLMSessionService } = {} -export const openAISession = new OpenAIModelSessionService(); -export const anthropicSession = new AnthropicModelSessionService(); +export const openAISession = new OpenAIModelSessionService() +export const anthropicSession = new AnthropicModelSessionService() -export const ollamaService = new OllamaService(); +export const ollamaService = new OllamaService() export const registerLLMSessionHandlers = (store: Store) => { ipcMain.handle( - "streaming-llm-response", + 'streaming-llm-response', async ( event: IpcMainInvokeEvent, llmName: string, llmConfig: LLMConfig, isJSONMode: boolean, - request: ChatHistories | Query + chatHistory: ChatHistory, ): Promise => { const handleOpenAIChunk = (chunk: ChatCompletionChunk) => { - event.sender.send("openAITokenStream", request.id, chunk); - }; + event.sender.send('openAITokenStream', chatHistory.id, chunk) + } const handleAnthropicChunk = (chunk: MessageStreamEvent) => { - event.sender.send("anthropicTokenStream", request.id, chunk); - }; + event.sender.send('anthropicTokenStream', chatHistory.id, chunk) + } console.log("Registered LLM"); @@ -66,9 +54,9 @@ export const registerLLMSessionHandlers = (store: Store) => { isJSONMode, request.displayableChatHistory, handleOpenAIChunk, - store.get(StoreKeys.LLMGenerationParameters) - ); - break; + store.get(StoreKeys.LLMGenerationParameters), + ) + break case LLMType.Anthropic: await anthropicSession.streamingResponse( llmName, @@ -76,77 +64,63 @@ export const registerLLMSessionHandlers = (store: Store) => { isJSONMode, request.displayableChatHistory, handleAnthropicChunk, - store.get(StoreKeys.LLMGenerationParameters) - ); - break; + store.get(StoreKeys.LLMGenerationParameters), + ) + break default: - throw new Error(`LLM type ${llmConfig.type} not supported.`); + throw new Error(`LLM type ${llmConfig.type} not supported.`) } - } - ); - ipcMain.handle("set-default-llm", (event, modelName: string) => { + }, + ) + ipcMain.handle('set-default-llm', (event, modelName: string) => { // TODO: validate that the model exists - store.set(StoreKeys.DefaultLLM, modelName); - }); + store.set(StoreKeys.DefaultLLM, modelName) + }) - ipcMain.handle("get-default-llm-name", () => { - return store.get(StoreKeys.DefaultLLM); - }); + ipcMain.handle('get-default-llm-name', () => store.get(StoreKeys.DefaultLLM)) - ipcMain.handle("pull-ollama-model", async (event, modelName: string) => { + ipcMain.handle('pull-ollama-model', async (event, modelName: string) => { const handleProgress = (progress: ProgressResponse) => { - event.sender.send("ollamaDownloadProgress", modelName, progress); - }; - await ollamaService.pullModel(modelName, handleProgress); - }); + event.sender.send('ollamaDownloadProgress', modelName, progress) + } + await ollamaService.pullModel(modelName, handleProgress) + }) - ipcMain.handle("get-llm-configs", async () => { - return await getAllLLMConfigs(store, ollamaService); - }); + ipcMain.handle('get-llm-configs', async () => getAllLLMConfigs(store, ollamaService)) - ipcMain.handle("add-or-update-llm", async (event, modelConfig: LLMConfig) => { - console.log("setting up new local model", modelConfig); - await addOrUpdateLLMSchemaInStore(store, modelConfig); - }); + ipcMain.handle('add-or-update-llm', async (event, modelConfig: LLMConfig) => { + await addOrUpdateLLMSchemaInStore(store, modelConfig) + }) - ipcMain.handle("remove-llm", async (event, modelNameToDelete: string) => { - console.log("deleting local model", modelNameToDelete); - await removeLLM(store, ollamaService, modelNameToDelete); - }); + ipcMain.handle('remove-llm', async (event, modelNameToDelete: string) => { + await removeLLM(store, ollamaService, modelNameToDelete) + }) ipcMain.handle( - "slice-list-of-strings-to-context-length", + 'slice-list-of-strings-to-context-length', async (event, strings: string[], llmName: string): Promise => { - const llmSession = openAISession; - const llmConfig = await getLLMConfig(store, ollamaService, llmName); - console.log("llmConfig", llmConfig); + const llmSession = openAISession + const llmConfig = await getLLMConfig(store, ollamaService, llmName) + if (!llmConfig) { - throw new Error(`LLM ${llmName} not configured.`); + throw new Error(`LLM ${llmName} not configured.`) } - return sliceListOfStringsToContextLength( - strings, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - } - ); + return sliceListOfStringsToContextLength(strings, llmSession.getTokenizer(llmName), llmConfig.contextLength) + }, + ) ipcMain.handle( - "slice-string-to-context-length", + 'slice-string-to-context-length', async (event, inputString: string, llmName: string): Promise => { - const llmSession = openAISession; - const llmConfig = await getLLMConfig(store, ollamaService, llmName); - console.log("llmConfig", llmConfig); + const llmSession = openAISession + const llmConfig = await getLLMConfig(store, ollamaService, llmName) + if (!llmConfig) { - throw new Error(`LLM ${llmName} not configured.`); + throw new Error(`LLM ${llmName} not configured.`) } - return sliceStringToContextLength( - inputString, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - } - ); -}; + return sliceStringToContextLength(inputString, llmSession.getTokenizer(llmName), llmConfig.contextLength) + }, + ) +} diff --git a/electron/main/llm/llmConfig.ts b/electron/main/llm/llmConfig.ts index 90412d2d..d1fee87a 100644 --- a/electron/main/llm/llmConfig.ts +++ b/electron/main/llm/llmConfig.ts @@ -1,107 +1,89 @@ -import Store from "electron-store"; +import Store from 'electron-store' -import { - LLMConfig, - StoreKeys, - StoreSchema, -} from "../electron-store/storeConfig"; +import { LLMConfig, StoreKeys, StoreSchema } from '../electron-store/storeConfig' -import { OllamaService } from "./models/Ollama"; +import OllamaService from './models/Ollama' export function validateAIModelConfig(config: LLMConfig): string | null { // Validate localPath: ensure it's not empty if (!config.modelName.trim()) { - return "Model name is required."; + return 'Model name is required.' } // Validate contextLength: ensure it's a positive number if (config.contextLength && config.contextLength <= 0) { - return "Context length must be a positive number."; + return 'Context length must be a positive number.' } // Validate engine: ensure it's either "openai" or "llamacpp" - if (config.engine !== "openai" && config.engine !== "anthropic") { - return "Engine must be either 'openai' or 'llamacpp'."; + if (config.engine !== 'openai' && config.engine !== 'anthropic') { + return "Engine must be either 'openai' or 'llamacpp'." } // Optional field validation for errorMsg: ensure it's not empty if provided if (config.errorMsg && !config.errorMsg.trim()) { - return "Error message should not be empty if provided."; + return 'Error message should not be empty if provided.' } - return null; + return null } -export async function addOrUpdateLLMSchemaInStore( - store: Store, - modelConfig: LLMConfig -): Promise { - const existingModelsInStore = await store.get(StoreKeys.LLMs); - console.log("existingModels: ", existingModelsInStore); - const isNotValid = validateAIModelConfig(modelConfig); +export async function addOrUpdateLLMSchemaInStore(store: Store, modelConfig: LLMConfig): Promise { + const existingModelsInStore = await store.get(StoreKeys.LLMs) + + const isNotValid = validateAIModelConfig(modelConfig) if (isNotValid) { - throw new Error(isNotValid); + throw new Error(isNotValid) } - const foundModel = existingModelsInStore.find( - (model) => model.modelName === modelConfig.modelName - ); - - console.log("foundModel: ", foundModel); + const foundModel = existingModelsInStore.find((model) => model.modelName === modelConfig.modelName) if (foundModel) { - console.log("updating model"); const updatedModels = existingModelsInStore.map((model) => - model.modelName === modelConfig.modelName ? modelConfig : model - ); - store.set(StoreKeys.LLMs, updatedModels); + model.modelName === modelConfig.modelName ? modelConfig : model, + ) + store.set(StoreKeys.LLMs, updatedModels) } else { - console.log("adding model"); - const updatedModels = [...existingModelsInStore, modelConfig]; - store.set(StoreKeys.LLMs, updatedModels); + const updatedModels = [...existingModelsInStore, modelConfig] + store.set(StoreKeys.LLMs, updatedModels) + } +} + +export async function getAllLLMConfigs(store: Store, ollamaSession: OllamaService): Promise { + const llmConfigsFromStore = store.get(StoreKeys.LLMs) + const ollamaLLMConfigs = await ollamaSession.getAvailableModels() + + return [...llmConfigsFromStore, ...ollamaLLMConfigs] +} + +export async function getLLMConfig( + store: Store, + ollamaSession: OllamaService, + modelName: string, +): Promise { + const llmConfigs = await getAllLLMConfigs(store, ollamaSession) + + if (llmConfigs) { + return llmConfigs.find((model: LLMConfig) => model.modelName === modelName) } + return undefined } export async function removeLLM( store: Store, ollamaService: OllamaService, - modelName: string + modelName: string, ): Promise { - const existingModels = (store.get(StoreKeys.LLMs) as LLMConfig[]) || []; + const existingModels = (store.get(StoreKeys.LLMs) as LLMConfig[]) || [] - const foundModel = await getLLMConfig(store, ollamaService, modelName); + const foundModel = await getLLMConfig(store, ollamaService, modelName) if (!foundModel) { - return; + return } - const updatedModels = existingModels.filter( - (model) => model.modelName !== modelName - ); - store.set(StoreKeys.LLMs, updatedModels); - - ollamaService.deleteModel(modelName); -} - -export async function getAllLLMConfigs( - store: Store, - ollamaSession: OllamaService -): Promise { - const llmConfigsFromStore = store.get(StoreKeys.LLMs); - const ollamaLLMConfigs = await ollamaSession.getAvailableModels(); - - return [...llmConfigsFromStore, ...ollamaLLMConfigs]; -} + const updatedModels = existingModels.filter((model) => model.modelName !== modelName) + store.set(StoreKeys.LLMs, updatedModels) -export async function getLLMConfig( - store: Store, - ollamaSession: OllamaService, - modelName: string -): Promise { - const llmConfigs = await getAllLLMConfigs(store, ollamaSession); - console.log("llmConfigs: ", llmConfigs); - if (llmConfigs) { - return llmConfigs.find((model: LLMConfig) => model.modelName === modelName); - } - return undefined; + ollamaService.deleteModel(modelName) } diff --git a/electron/main/llm/models/Anthropic.ts b/electron/main/llm/models/Anthropic.ts index 545dc559..1ea6cf13 100644 --- a/electron/main/llm/models/Anthropic.ts +++ b/electron/main/llm/models/Anthropic.ts @@ -1,35 +1,29 @@ -import Anthropic from "@anthropic-ai/sdk"; -import { - Message, - MessageParam, - MessageStreamEvent, -} from "@anthropic-ai/sdk/resources"; -import { - LLMGenerationParameters, - LLMConfig, -} from "electron/main/electron-store/storeConfig"; -import { Tiktoken, TiktokenModel, encodingForModel } from "js-tiktoken"; -import { ChatCompletionMessageParam } from "openai/resources/chat/completions"; +/* eslint-disable class-methods-use-this */ +/* eslint-disable no-restricted-syntax */ +import Anthropic from '@anthropic-ai/sdk' +import { Message, MessageParam, MessageStreamEvent } from '@anthropic-ai/sdk/resources' +import { LLMGenerationParameters, LLMConfig } from 'electron/main/electron-store/storeConfig' +import { Tiktoken, TiktokenModel, encodingForModel } from 'js-tiktoken' +import { ChatCompletionMessageParam } from 'openai/resources/chat/completions' -import { customFetchUsingElectronNetStreaming } from "../../common/network"; -import { LLMSessionService } from "../types"; +import { customFetchUsingElectronNetStreaming } from '../../common/network' +import { LLMSessionService } from '../types' +import cleanMessageForAnthropic from '../utils' -export class AnthropicModelSessionService implements LLMSessionService { +class AnthropicModelSessionService implements LLMSessionService { public getTokenizer = (llmName: string): ((text: string) => number[]) => { - let tokenEncoding: Tiktoken; + let tokenEncoding: Tiktoken try { - tokenEncoding = encodingForModel(llmName as TiktokenModel); + tokenEncoding = encodingForModel(llmName as TiktokenModel) } catch (e) { - tokenEncoding = encodingForModel("gpt-3.5-turbo-1106"); // hack while we think about what to do with custom remote models' tokenizers + tokenEncoding = encodingForModel('gpt-3.5-turbo-1106') // hack while we think about what to do with custom remote models' tokenizers } - const tokenize = (text: string): number[] => { - return tokenEncoding.encode(text); - }; - return tokenize; - }; + const tokenize = (text: string): number[] => tokenEncoding.encode(text) + return tokenize + } public abort(): void { - throw new Error("Abort not yet implemented."); + throw new Error('Abort not yet implemented.') } async response( @@ -37,21 +31,21 @@ export class AnthropicModelSessionService implements LLMSessionService { modelConfig: LLMConfig, messageHistory: ChatCompletionMessageParam[], isJSONMode: boolean, - generationParams?: LLMGenerationParameters + generationParams?: LLMGenerationParameters, ): Promise { const anthropic = new Anthropic({ apiKey: modelConfig.apiKey, baseURL: modelConfig.apiURL, fetch: customFetchUsingElectronNetStreaming, - }); + }) const msg = await anthropic.messages.create({ model: modelName, messages: messageHistory as MessageParam[], temperature: generationParams?.temperature, max_tokens: generationParams?.maxTokens || 1024, - }); + }) - return msg; + return msg } async streamingResponse( @@ -60,35 +54,25 @@ export class AnthropicModelSessionService implements LLMSessionService { isJSONMode: boolean, messageHistory: ChatCompletionMessageParam[], handleChunk: (chunk: MessageStreamEvent) => void, - generationParams?: LLMGenerationParameters + generationParams?: LLMGenerationParameters, ): Promise { const anthropic = new Anthropic({ apiKey: modelConfig.apiKey, baseURL: modelConfig.apiURL, fetch: customFetchUsingElectronNetStreaming, - }); + }) const stream = await anthropic.messages.create({ model: modelName, - messages: messageHistory.map(cleanMessage), + messages: messageHistory.map(cleanMessageForAnthropic), stream: true, temperature: generationParams?.temperature, max_tokens: generationParams?.maxTokens || 1024, - }); + }) for await (const messageStreamEvent of stream) { - handleChunk(messageStreamEvent); + handleChunk(messageStreamEvent) } } } -function cleanMessage(message: ChatCompletionMessageParam): MessageParam { - if (typeof message.content !== "string") { - throw new Error("Message content is not a string"); - } - if (message.role === "system") { - return { role: "user", content: message.content }; - } else if (message.role === "user" || message.role === "assistant") { - return { role: message.role, content: message.content }; - } - throw new Error("Message role is not valid"); -} +export default AnthropicModelSessionService diff --git a/electron/main/llm/models/Ollama.ts b/electron/main/llm/models/Ollama.ts index 33fe8781..607cd4c1 100644 --- a/electron/main/llm/models/Ollama.ts +++ b/electron/main/llm/models/Ollama.ts @@ -1,237 +1,219 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable class-methods-use-this */ +/* eslint-disable prefer-promise-reject-errors */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { exec } from "child_process"; -import * as os from "os"; -import * as path from "path"; - -import { app } from "electron"; -import { - LLMGenerationParameters, - OpenAILLMConfig, -} from "electron/main/electron-store/storeConfig"; -import { Tiktoken, TiktokenModel, encodingForModel } from "js-tiktoken"; -import { ModelResponse, ProgressResponse, Ollama } from "ollama"; -import { - ChatCompletionChunk, - ChatCompletionMessageParam, -} from "openai/resources/chat/completions"; +import { exec } from 'child_process' +import * as os from 'os' +import * as path from 'path' + +import { app } from 'electron' +import { LLMGenerationParameters, OpenAILLMConfig } from 'electron/main/electron-store/storeConfig' +import { Tiktoken, TiktokenModel, encodingForModel } from 'js-tiktoken' +import { ModelResponse, ProgressResponse, Ollama } from 'ollama' +import { ChatCompletionChunk, ChatCompletionMessageParam } from 'openai/resources/chat/completions' // import ollama,"ollama"; -import { LLMSessionService } from "../types"; +import { LLMSessionService } from '../types' const OllamaServeType = { - SYSTEM: "system", // ollama is installed on the system - PACKAGED: "packaged", // ollama is packaged with the app -}; + SYSTEM: 'system', // ollama is installed on the system + PACKAGED: 'packaged', // ollama is packaged with the app +} -export class OllamaService implements LLMSessionService { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private client!: Ollama; - private host = "http://127.0.0.1:11434"; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private childProcess: any; +class OllamaService implements LLMSessionService { + private client!: Ollama - constructor() { - // this.client = await import("ollama"); - // this.client = new ollama.Client(); - } + private host = 'http://127.0.0.1:11434' + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private childProcess: any public init = async () => { - console.log("Initializing Ollama client..."); - await this.serve(); + await this.serve() + + const ollamaLib = await import('ollama') + this.client = new ollamaLib.Ollama() - const ollamaLib = await import("ollama"); - this.client = new ollamaLib.Ollama(); - console.log("Ollama client: ", this.client); // const models = await this.client.default.list(); - // console.log("Ollama models: ", models); + // ; // const lists = await this.client. - // console.log("Ollama models: ", lists); - }; + // ; + } async ping() { const response = await fetch(this.host, { - method: "GET", - cache: "no-store", - }); + method: 'GET', + cache: 'no-store', + }) if (response.status !== 200) { - throw new Error(`failed to ping ollama server: ${response.status}`); + throw new Error(`failed to ping ollama server: ${response.status}`) } - return true; + return true } async serve() { try { // see if ollama is already running - await this.ping(); - return OllamaServeType.SYSTEM; + await this.ping() + return OllamaServeType.SYSTEM } catch (err) { // this is fine, we just need to start ollama } try { // See if 'ollama serve' command is available on the system - await this.execServe("ollama"); - console.log("Ollama is installed on the system"); - return OllamaServeType.SYSTEM; + await this.execServe('ollama') + + return OllamaServeType.SYSTEM } catch (err) { // ollama is not installed, run the binary directly - console.log("Ollama is not installed on the system: ", err); // logInfo(`/ is not installed on the system: ${err}`); } - let exeName = ""; - let exeDir = ""; + let exeName = '' + let exeDir = '' switch (process.platform) { - case "win32": - exeName = "ollama-windows-amd64.exe"; + case 'win32': + exeName = 'ollama-windows-amd64.exe' exeDir = app.isPackaged - ? path.join(process.resourcesPath, "binaries") - : path.join(app.getAppPath(), "binaries", "win32"); + ? path.join(process.resourcesPath, 'binaries') + : path.join(app.getAppPath(), 'binaries', 'win32') - break; - case "darwin": - exeName = "ollama-darwin"; + break + case 'darwin': + exeName = 'ollama-darwin' exeDir = app.isPackaged - ? path.join(process.resourcesPath, "binaries") - : path.join(app.getAppPath(), "binaries", "darwin"); - break; - case "linux": - exeName = "ollama-linux-amd64"; + ? path.join(process.resourcesPath, 'binaries') + : path.join(app.getAppPath(), 'binaries', 'darwin') + break + case 'linux': + exeName = 'ollama-linux-amd64' exeDir = app.isPackaged - ? path.join(process.resourcesPath, "binaries") - : path.join(app.getAppPath(), "binaries", "linux"); + ? path.join(process.resourcesPath, 'binaries') + : path.join(app.getAppPath(), 'binaries', 'linux') - break; + break default: - throw new Error("Unsupported platform"); + throw new Error('Unsupported platform') } - const exePath = path.join(exeDir, exeName); + const exePath = path.join(exeDir, exeName) try { - await this.execServe(exePath); - return OllamaServeType.PACKAGED; + await this.execServe(exePath) + return OllamaServeType.PACKAGED } catch (err) { - console.log("Failed to start Ollama: ", err); - throw new Error(`Failed to start Ollama: ${err}`); + throw new Error(`Failed to start Ollama: ${err}`) } } - async execServe(path: string) { + async execServe(_path: string) { return new Promise((resolve, reject) => { - const env = { ...process.env }; - const command = `"${path}" serve`; + const env = { ...process.env } + const command = `"${_path}" serve` this.childProcess = exec(command, { env }, (err, stdout, stderr) => { if (err) { - reject(`exec error: ${err}`); - return; + reject(`exec error: ${err}`) + return } if (stderr) { - reject(`ollama stderr: ${stderr}`); - return; + reject(`ollama stderr: ${stderr}`) + return } - reject(`ollama stdout: ${stdout}`); - }); + reject(`ollama stdout: ${stdout}`) + }) // Once the process is started, try to ping Ollama server. this.waitForPing() .then(() => { - resolve(void 0); + resolve(undefined) }) .catch((pingError) => { if (this.childProcess && !this.childProcess.killed) { - this.childProcess.kill(); + this.childProcess.kill() } - reject(pingError); - }); - }); + reject(pingError) + }) + }) } async waitForPing(delay = 1000, retries = 20) { - for (let i = 0; i < retries; i++) { + for (let i = 0; i < retries; i += 1) { try { - await this.ping(); - return; + // eslint-disable-next-line no-await-in-loop + await this.ping() + return } catch (err) { - await new Promise((resolve) => setTimeout(resolve, delay)); + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, delay) + }) } } - throw new Error("Max retries reached. Ollama server didn't respond."); + throw new Error("Max retries reached. Ollama server didn't respond.") } stop() { if (!this.childProcess) { - return; + return } - if (os.platform() === "win32") { - exec(`taskkill /pid ${this.childProcess.pid} /f /t`, (err) => { - if (err) { - console.log("Failed to kill Ollama process: ", err); - } - }); + if (os.platform() === 'win32') { + exec(`taskkill /pid ${this.childProcess.pid} /f /t`) } else { - this.childProcess.kill(); + this.childProcess.kill() } - console.log("Ollama process killed"); - this.childProcess = null; + + this.childProcess = null } public getAvailableModels = async (): Promise => { - const ollamaModelsResponse = await this.client.list(); + const ollamaModelsResponse = await this.client.list() const output = ollamaModelsResponse.models.map( - (model: ModelResponse): OpenAILLMConfig => { - return { - modelName: model.name, - type: "openai", - apiKey: "", - contextLength: 4096, - engine: "openai", - apiURL: "http://localhost:11434/v1/", - }; - } - ); - return output; - }; - - public pullModel = async ( - modelName: string, - handleProgress: (chunk: ProgressResponse) => void - ): Promise => { - console.log("Pulling model: ", modelName); + (model: ModelResponse): OpenAILLMConfig => ({ + modelName: model.name, + type: 'openai', + apiKey: '', + contextLength: 4096, + engine: 'openai', + apiURL: 'http://localhost:11434/v1/', + }), + ) + return output + } + + public pullModel = async (modelName: string, handleProgress: (chunk: ProgressResponse) => void): Promise => { const stream = await this.client.pull({ model: modelName, stream: true, - }); + }) for await (const progress of stream) { - handleProgress(progress); + handleProgress(progress) } - }; + } public deleteModel = async (modelName: string): Promise => { - await this.client.delete({ model: modelName }); - }; + await this.client.delete({ model: modelName }) + } public getTokenizer = (llmName: string): ((text: string) => number[]) => { - let tokenEncoding: Tiktoken; + let tokenEncoding: Tiktoken try { - tokenEncoding = encodingForModel(llmName as TiktokenModel); + tokenEncoding = encodingForModel(llmName as TiktokenModel) } catch (e) { - tokenEncoding = encodingForModel("gpt-3.5-turbo-1106"); // hack while we think about what to do with custom remote models' tokenizers + tokenEncoding = encodingForModel('gpt-3.5-turbo-1106') // hack while we think about what to do with custom remote models' tokenizers } - const tokenize = (text: string): number[] => { - return tokenEncoding.encode(text); - }; - return tokenize; - }; + const tokenize = (text: string): number[] => tokenEncoding.encode(text) + return tokenize + } public abort(): void { - throw new Error("Abort not yet implemented."); + throw new Error('Abort not yet implemented.') } async streamingResponse( @@ -240,8 +222,10 @@ export class OllamaService implements LLMSessionService { _isJSONMode: boolean, _messageHistory: ChatCompletionMessageParam[], _handleChunk: (chunk: ChatCompletionChunk) => void, - _generationParams?: LLMGenerationParameters + _generationParams?: LLMGenerationParameters, ): Promise { - throw new Error("Method not implemented."); + throw new Error('Method not implemented.') } } + +export default OllamaService diff --git a/electron/main/llm/models/OpenAI.ts b/electron/main/llm/models/OpenAI.ts index 0aabb98f..d4d90e2a 100644 --- a/electron/main/llm/models/OpenAI.ts +++ b/electron/main/llm/models/OpenAI.ts @@ -1,34 +1,26 @@ -import { - LLMGenerationParameters, - LLMConfig, -} from "electron/main/electron-store/storeConfig"; -import { Tiktoken, TiktokenModel, encodingForModel } from "js-tiktoken"; -import OpenAI from "openai"; -import { - ChatCompletion, - ChatCompletionChunk, - ChatCompletionMessageParam, -} from "openai/resources/chat/completions"; +/* eslint-disable class-methods-use-this */ +import { LLMGenerationParameters, LLMConfig } from 'electron/main/electron-store/storeConfig' +import { Tiktoken, TiktokenModel, encodingForModel } from 'js-tiktoken' +import OpenAI from 'openai' +import { ChatCompletion, ChatCompletionChunk, ChatCompletionMessageParam } from 'openai/resources/chat/completions' -import { customFetchUsingElectronNetStreaming } from "../../common/network"; -import { LLMSessionService } from "../types"; +import { customFetchUsingElectronNetStreaming } from '../../common/network' +import { LLMSessionService } from '../types' -export class OpenAIModelSessionService implements LLMSessionService { +class OpenAIModelSessionService implements LLMSessionService { public getTokenizer = (llmName: string): ((text: string) => number[]) => { - let tokenEncoding: Tiktoken; + let tokenEncoding: Tiktoken try { - tokenEncoding = encodingForModel(llmName as TiktokenModel); + tokenEncoding = encodingForModel(llmName as TiktokenModel) } catch (e) { - tokenEncoding = encodingForModel("gpt-3.5-turbo-1106"); // hack while we think about what to do with custom remote models' tokenizers + tokenEncoding = encodingForModel('gpt-3.5-turbo-1106') // hack while we think about what to do with custom remote models' tokenizers } - const tokenize = (text: string): number[] => { - return tokenEncoding.encode(text); - }; - return tokenize; - }; + const tokenize = (text: string): number[] => tokenEncoding.encode(text) + return tokenize + } public abort(): void { - throw new Error("Abort not yet implemented."); + throw new Error('Abort not yet implemented.') } async response( @@ -36,23 +28,23 @@ export class OpenAIModelSessionService implements LLMSessionService { modelConfig: LLMConfig, messageHistory: ChatCompletionMessageParam[], isJSONMode: boolean, - generationParams?: LLMGenerationParameters + generationParams?: LLMGenerationParameters, ): Promise { const openai = new OpenAI({ apiKey: modelConfig.apiKey, baseURL: modelConfig.apiURL, fetch: customFetchUsingElectronNetStreaming, - }); + }) const response = await openai.chat.completions.create({ model: modelName, messages: messageHistory, max_tokens: generationParams?.maxTokens, temperature: generationParams?.temperature, response_format: { - type: isJSONMode ? "json_object" : "text", + type: isJSONMode ? 'json_object' : 'text', }, - }); - return response; + }) + return response } async streamingResponse( @@ -61,13 +53,13 @@ export class OpenAIModelSessionService implements LLMSessionService { isJSONMode: boolean, messageHistory: ChatCompletionMessageParam[], handleChunk: (chunk: ChatCompletionChunk) => void, - generationParams?: LLMGenerationParameters + generationParams?: LLMGenerationParameters, ): Promise { const openai = new OpenAI({ apiKey: modelConfig.apiKey, baseURL: modelConfig.apiURL, fetch: customFetchUsingElectronNetStreaming, - }); + }) const stream = await openai.chat.completions.create({ model: modelName, @@ -76,12 +68,15 @@ export class OpenAIModelSessionService implements LLMSessionService { max_tokens: generationParams?.maxTokens, temperature: generationParams?.temperature, response_format: { - type: isJSONMode ? "json_object" : "text", + type: isJSONMode ? 'json_object' : 'text', }, - }); + }) + // eslint-disable-next-line no-restricted-syntax for await (const chunk of stream) { - handleChunk(chunk); + handleChunk(chunk) } } } + +export default OpenAIModelSessionService diff --git a/electron/main/llm/types.ts b/electron/main/llm/types.ts index 20729f98..9b6383e4 100644 --- a/electron/main/llm/types.ts +++ b/electron/main/llm/types.ts @@ -1,13 +1,7 @@ -import { MessageStreamEvent } from "@anthropic-ai/sdk/resources"; -import { - ChatCompletionChunk, - ChatCompletionMessageParam, -} from "openai/resources/chat/completions"; +import { MessageStreamEvent } from '@anthropic-ai/sdk/resources' +import { ChatCompletionChunk, ChatCompletionMessageParam } from 'openai/resources/chat/completions' -import { - LLMGenerationParameters, - LLMConfig, -} from "../electron-store/storeConfig"; +import { LLMGenerationParameters, LLMConfig } from '../electron-store/storeConfig' // Any LLM engine should implement this interface: export interface LLMSessionService { @@ -32,11 +26,11 @@ export interface LLMSessionService { isJSONMode: boolean, messageHistory: Array, chunkResponse: (chunk: ChatCompletionChunk | MessageStreamEvent) => void, - generationParams?: LLMGenerationParameters - ): Promise; + generationParams?: LLMGenerationParameters, + ): Promise - getTokenizer: (llmName: string) => (text: string) => number[]; - abort(): void; + getTokenizer: (llmName: string) => (text: string) => number[] + abort(): void } export interface ISendFunctionImplementer { @@ -45,5 +39,5 @@ export interface ISendFunctionImplementer { * @param channel The channel to send the message to. * @param args Additional arguments for the message. */ - send(channel: string, ...args: unknown[]): void; + send(channel: string, ...args: unknown[]): void } diff --git a/electron/main/llm/utils.ts b/electron/main/llm/utils.ts new file mode 100644 index 00000000..b01613a7 --- /dev/null +++ b/electron/main/llm/utils.ts @@ -0,0 +1,17 @@ +import { MessageParam } from '@anthropic-ai/sdk/resources' +import { ChatCompletionMessageParam } from 'openai/resources' + +function cleanMessageForAnthropic(message: ChatCompletionMessageParam): MessageParam { + if (typeof message.content !== 'string') { + throw new Error('Message content is not a string') + } + if (message.role === 'system') { + return { role: 'user', content: message.content } + } + if (message.role === 'user' || message.role === 'assistant') { + return { role: message.role, content: message.content } + } + throw new Error('Message role is not valid') +} + +export default cleanMessageForAnthropic diff --git a/electron/main/path/ipcHandlers.ts b/electron/main/path/ipcHandlers.ts index 3fdeff71..9edad54a 100644 --- a/electron/main/path/ipcHandlers.ts +++ b/electron/main/path/ipcHandlers.ts @@ -1,40 +1,25 @@ -import path from "path"; - -import { ipcMain } from "electron"; - -import { markdownExtensions } from "../filesystem/filesystem"; - -import { addExtensionToFilenameIfNoExtensionPresent } from "./path"; - -export const pathHandlers = () => { - ipcMain.handle("path-basename", (event, pathString: string) => { - return path.basename(pathString); - }); - - ipcMain.handle("path-sep", () => { - return path.sep; - }); - - ipcMain.handle("join-path", (event, ...args) => { - return path.join(...args); - }); - - ipcMain.handle("path-dirname", (event, pathString: string) => { - return path.dirname(pathString) + path.sep; - }); - - ipcMain.handle("path-relative", (event, from: string, to: string) => { - return path.relative(from, to); - }); - - ipcMain.handle( - "add-extension-if-no-extension-present", - (event, pathString: string) => { - return addExtensionToFilenameIfNoExtensionPresent( - pathString, - markdownExtensions, - ".md" - ); - } - ); -}; +import path from 'path' + +import { ipcMain } from 'electron' + +import { markdownExtensions } from '../filesystem/filesystem' + +import addExtensionToFilenameIfNoExtensionPresent from './path' + +const pathHandlers = () => { + ipcMain.handle('path-basename', (event, pathString: string) => path.basename(pathString)) + + ipcMain.handle('path-sep', () => path.sep) + + ipcMain.handle('join-path', (event, ...args) => path.join(...args)) + + ipcMain.handle('path-dirname', (event, pathString: string) => path.dirname(pathString) + path.sep) + + ipcMain.handle('path-relative', (event, from: string, to: string) => path.relative(from, to)) + + ipcMain.handle('add-extension-if-no-extension-present', (event, pathString: string) => + addExtensionToFilenameIfNoExtensionPresent(pathString, markdownExtensions, '.md'), + ) +} + +export default pathHandlers diff --git a/electron/main/path/path.ts b/electron/main/path/path.ts index a3f34872..d7a78db7 100644 --- a/electron/main/path/path.ts +++ b/electron/main/path/path.ts @@ -1,14 +1,16 @@ -import path from "path"; +import path from 'path' -export function addExtensionToFilenameIfNoExtensionPresent( +function addExtensionToFilenameIfNoExtensionPresent( filename: string, acceptableExtensions: string[], - defaultExtension: string + defaultExtension: string, ): string { - const extension = path.extname(filename).slice(1).toLowerCase(); + const extension = path.extname(filename).slice(1).toLowerCase() if (acceptableExtensions.includes(extension)) { - return filename; + return filename } - return `${filename}${defaultExtension}`; + return `${filename}${defaultExtension}` } + +export default addExtensionToFilenameIfNoExtensionPresent diff --git a/electron/main/vector-database/database.test.ts b/electron/main/vector-database/database.test.ts index d43764aa..855fa2f8 100644 --- a/electron/main/vector-database/database.test.ts +++ b/electron/main/vector-database/database.test.ts @@ -1,20 +1,17 @@ -import { - sanitizePathForDatabase, - unsanitizePathForFileSystem, -} from "./tableHelperFunctions"; +import { sanitizePathForDatabase, unsanitizePathForFileSystem } from './lanceTableWrapper' -describe("Path Sanitization Tests", () => { - it("should sanitize file path correctly", () => { - const unixPath = "/home/user/test'file.txt"; - const sanitized = sanitizePathForDatabase(unixPath); - expect(sanitized).toBe("/home/user/test''file.txt"); - }); -}); +describe('Path Sanitization Tests', () => { + it('should sanitize file path correctly', () => { + const unixPath = "/home/user/test'file.txt" + const sanitized = sanitizePathForDatabase(unixPath) + expect(sanitized).toBe("/home/user/test''file.txt") + }) +}) -describe("Path Unsanitization Tests", () => { - it("should unsanitize path correctly", () => { - const sanitizedPath = "/home/user/test''file.txt"; - const original = unsanitizePathForFileSystem(sanitizedPath); - expect(original).toBe("/home/user/test'file.txt"); - }); -}); +describe('Path Unsanitization Tests', () => { + it('should unsanitize path correctly', () => { + const sanitizedPath = "/home/user/test''file.txt" + const original = unsanitizePathForFileSystem(sanitizedPath) + expect(original).toBe("/home/user/test'file.txt") + }) +}) diff --git a/electron/main/vector-database/downloadModelsFromHF.ts b/electron/main/vector-database/downloadModelsFromHF.ts index 0b558042..3e26849f 100644 --- a/electron/main/vector-database/downloadModelsFromHF.ts +++ b/electron/main/vector-database/downloadModelsFromHF.ts @@ -1,76 +1,66 @@ -import fs from "fs"; -import * as path from "path"; +import fs from 'fs' +import * as path from 'path' -import { listFiles, downloadFile } from "@huggingface/hub"; +import { listFiles, downloadFile } from '@huggingface/hub' -import { customFetchUsingElectronNet } from "../common/network"; +import { customFetchUsingElectronNet } from '../common/network' -export const DownloadModelFilesFromHFRepo = async ( - repo: string, - saveDirectory: string, - quantized = true -) => { - // List the files: - const fileList = await listFiles({ - repo: repo, - recursive: true, - fetch: customFetchUsingElectronNet, - }); - - const files = []; - for await (const file of fileList) { - if (file.type === "file") { - if (file.path.endsWith("onnx")) { - const isQuantizedFile = file.path.includes("quantized"); - if (quantized === isQuantizedFile) { - files.push(file); - } - } else { - files.push(file); - } - } - } - - console.log("files: ", files); - - // Create an array of promises for each file download: - const downloadPromises = files.map((file) => - downloadAndSaveFile(repo, file.path, path.join(saveDirectory, repo)) - ); - - // Execute all download promises in parallel: - await Promise.all(downloadPromises); -}; - -async function downloadAndSaveFile( - repo: string, - HFFilePath: string, - systemFilePath: string -): Promise { +async function downloadAndSaveFile(repo: string, HFFilePath: string, systemFilePath: string): Promise { // Call the downloadFile function and await its result const res = await downloadFile({ - repo: repo, + repo, path: HFFilePath, fetch: customFetchUsingElectronNet, - }); + }) if (!res) { - throw new Error(`Failed to download file from ${repo}/${HFFilePath}`); + throw new Error(`Failed to download file from ${repo}/${HFFilePath}`) } // Convert the Response object to an ArrayBuffer - const arrayBuffer = await res.arrayBuffer(); + const arrayBuffer = await res.arrayBuffer() // Convert the ArrayBuffer to a Buffer - const buffer = Buffer.from(arrayBuffer); + const buffer = Buffer.from(arrayBuffer) // Join the systemFilePath and filePath to create the full path - const fullPath = path.join(systemFilePath, HFFilePath); - const directory = path.dirname(fullPath); + const fullPath = path.join(systemFilePath, HFFilePath) + const directory = path.dirname(fullPath) if (!fs.existsSync(directory)) { - fs.mkdirSync(directory, { recursive: true }); + fs.mkdirSync(directory, { recursive: true }) } // Save the Buffer to the full path - fs.writeFileSync(fullPath, buffer); - console.log(`Saved file to ${fullPath}`); + fs.writeFileSync(fullPath, buffer) } + +const DownloadModelFilesFromHFRepo = async (repo: string, saveDirectory: string, quantized = true) => { + // List the files: + const fileList = await listFiles({ + repo, + recursive: true, + fetch: customFetchUsingElectronNet, + }) + + const files = [] + // eslint-disable-next-line no-restricted-syntax + for await (const file of fileList) { + if (file.type === 'file') { + if (file.path.endsWith('onnx')) { + const isQuantizedFile = file.path.includes('quantized') + if (quantized === isQuantizedFile) { + files.push(file) + } + } else { + files.push(file) + } + } + } + + // Create an array of promises for each file download: + const downloadPromises = files.map((file) => downloadAndSaveFile(repo, file.path, path.join(saveDirectory, repo))) + + // Execute all download promises in parallel: + await Promise.all(downloadPromises) +} + +export default DownloadModelFilesFromHFRepo diff --git a/electron/main/vector-database/embeddings.ts b/electron/main/vector-database/embeddings.ts index da7df3a7..42e6e070 100644 --- a/electron/main/vector-database/embeddings.ts +++ b/electron/main/vector-database/embeddings.ts @@ -1,86 +1,115 @@ -import path from "path"; +import path from 'path' -import { Pipeline, PreTrainedTokenizer } from "@xenova/transformers"; -import { app } from "electron"; -import removeMd from "remove-markdown"; -import * as lancedb from "vectordb"; +import { Pipeline, PreTrainedTokenizer } from '@xenova/transformers' +import { app } from 'electron' +import removeMd from 'remove-markdown' +import * as lancedb from 'vectordb' -import { errorToStringMainProcess } from "../common/error"; +import errorToStringMainProcess from '../common/error' import { EmbeddingModelConfig, EmbeddingModelWithLocalPath, EmbeddingModelWithRepo, -} from "../electron-store/storeConfig"; -import { splitDirectoryPathIntoBaseAndRepo } from "../filesystem/filesystem"; +} from '../electron-store/storeConfig' +import { splitDirectoryPathIntoBaseAndRepo } from '../filesystem/filesystem' + +import DownloadModelFilesFromHFRepo from './downloadModelsFromHF' +import { DBEntry } from './schema' + +export const defaultEmbeddingModelRepos = { + 'Xenova/bge-base-en-v1.5': { + type: 'repo', + repoName: 'Xenova/bge-base-en-v1.5', + }, + 'Xenova/UAE-Large-V1': { type: 'repo', repoName: 'Xenova/UAE-Large-V1' }, + 'Xenova/bge-small-en-v1.5': { + type: 'repo', + repoName: 'Xenova/bge-small-en-v1.5', + }, +} -import { DownloadModelFilesFromHFRepo } from "./downloadModelsFromHF"; -import { DBEntry } from "./schema"; +function setupTokenizeFunction(tokenizer: PreTrainedTokenizer): (data: (string | number[])[]) => string[] { + return (data: (string | number[])[]): string[] => { + if (!tokenizer) { + throw new Error('Tokenizer not initialized') + } -export interface EnhancedEmbeddingFunction - extends lancedb.EmbeddingFunction { - name: string; - contextLength: number; - tokenize: (data: T[]) => string[]; + return data.map((text) => { + try { + const res = tokenizer(text) + return res + } catch (error) { + throw new Error(`Tokenization process failed for text: ${errorToStringMainProcess(error)}`) + } + }) + } } -export async function createEmbeddingFunction( - embeddingModelConfig: EmbeddingModelConfig, - sourceColumn: string -): Promise> { - if (embeddingModelConfig.type === "local") { - return createEmbeddingFunctionForLocalModel( - embeddingModelConfig, - sourceColumn - ); +async function setupEmbedFunction(pipe: Pipeline): Promise<(batch: (string | number[])[]) => Promise> { + return async (batch: (string | number[])[]): Promise => { + if (batch.length === 0 || batch[0].length === 0) { + return [] + } + + if (typeof batch[0][0] === 'number') { + return batch as number[][] + } + + if (!pipe) { + throw new Error('Pipeline not initialized') + } + + const result: number[][] = await Promise.all( + batch.map(async (text) => { + try { + const res = await pipe(removeMd(text as string), { + pooling: 'mean', + normalize: true, + }) + return Array.from(res.data) + } catch (error) { + throw new Error(`Embedding process failed for text: ${errorToStringMainProcess(error)}`) + } + }), + ) + + return result } - return createEmbeddingFunctionForRepo(embeddingModelConfig, sourceColumn); } export async function createEmbeddingFunctionForLocalModel( embeddingModelConfig: EmbeddingModelWithLocalPath, - sourceColumn: string + sourceColumn: string, ): Promise> { - let pipe: Pipeline; - let repoName = ""; - let functionName = ""; + let pipe: Pipeline + let repoName = '' + let functionName = '' try { - const { pipeline, env } = await import("@xenova/transformers"); - env.cacheDir = path.join(app.getPath("userData"), "models", "embeddings"); // set for all. Just to deal with library and remote inconsistencies - console.log("config is: ", embeddingModelConfig); + const { pipeline, env } = await import('@xenova/transformers') + env.cacheDir = path.join(app.getPath('userData'), 'models', 'embeddings') // set for all. Just to deal with library and remote inconsistencies - const pathParts = splitDirectoryPathIntoBaseAndRepo( - embeddingModelConfig.localPath - ); + const pathParts = splitDirectoryPathIntoBaseAndRepo(embeddingModelConfig.localPath) - env.localModelPath = pathParts.localModelPath; - repoName = pathParts.repoName; - env.allowRemoteModels = false; - functionName = embeddingModelConfig.localPath; + env.localModelPath = pathParts.localModelPath + repoName = pathParts.repoName + env.allowRemoteModels = false + functionName = embeddingModelConfig.localPath try { pipe = (await pipeline( - "feature-extraction", - repoName + 'feature-extraction', + repoName, // {cache_dir: cacheDir, - )) as Pipeline; + )) as Pipeline } catch (error) { // here we could run a catch and try manually downloading the model... - throw new Error( - `Pipeline initialization failed for repo ${errorToStringMainProcess( - error - )}` - ); + throw new Error(`Pipeline initialization failed for repo ${errorToStringMainProcess(error)}`) } } catch (error) { - console.error( - `Resource initialization failed: ${errorToStringMainProcess(error)}` - ); - throw new Error( - `Resource initialization failed: ${errorToStringMainProcess(error)}` - ); + throw new Error(`Resource initialization failed: ${errorToStringMainProcess(error)}`) } - const tokenize = setupTokenizeFunction(pipe.tokenizer); - const embed = await setupEmbedFunction(pipe); + const tokenize = setupTokenizeFunction(pipe.tokenizer) + const embed = await setupEmbedFunction(pipe) return { name: functionName, @@ -88,46 +117,39 @@ export async function createEmbeddingFunctionForLocalModel( sourceColumn, embed, tokenize, - }; + } } export async function createEmbeddingFunctionForRepo( embeddingModelConfig: EmbeddingModelWithRepo, - sourceColumn: string + sourceColumn: string, ): Promise> { - let pipe: Pipeline; - let repoName = ""; - let functionName = ""; + let pipe: Pipeline + let repoName = '' + let functionName = '' try { - const { pipeline, env } = await import("@xenova/transformers"); - env.cacheDir = path.join(app.getPath("userData"), "models", "embeddings"); // set for all. Just to deal with library and remote inconsistencies + const { pipeline, env } = await import('@xenova/transformers') + env.cacheDir = path.join(app.getPath('userData'), 'models', 'embeddings') // set for all. Just to deal with library and remote inconsistencies - repoName = embeddingModelConfig.repoName; - env.allowRemoteModels = true; - functionName = embeddingModelConfig.repoName; + repoName = embeddingModelConfig.repoName + env.allowRemoteModels = true + functionName = embeddingModelConfig.repoName - console.log(repoName, env.cacheDir); try { - pipe = (await pipeline("feature-extraction", repoName)) as Pipeline; + pipe = (await pipeline('feature-extraction', repoName)) as Pipeline } catch (error) { try { - await DownloadModelFilesFromHFRepo(repoName, env.cacheDir); // try to manual download to use system proxy - pipe = (await pipeline("feature-extraction", repoName)) as Pipeline; - } catch (error) { - throw new Error( - `Pipeline initialization failed for repo ${errorToStringMainProcess( - error - )}` - ); + await DownloadModelFilesFromHFRepo(repoName, env.cacheDir) // try to manual download to use system proxy + pipe = (await pipeline('feature-extraction', repoName)) as Pipeline + } catch (err) { + throw new Error(`Pipeline initialization failed for repo ${errorToStringMainProcess(err)}`) } } } catch (error) { - throw new Error( - `Resource initialization failed: ${errorToStringMainProcess(error)}` - ); + throw new Error(`Resource initialization failed: ${errorToStringMainProcess(error)}`) } - const tokenize = setupTokenizeFunction(pipe.tokenizer); - const embed = await setupEmbedFunction(pipe); + const tokenize = setupTokenizeFunction(pipe.tokenizer) + const embed = await setupEmbedFunction(pipe) // sanitize the embedding text to remove markdown content @@ -137,104 +159,46 @@ export async function createEmbeddingFunctionForRepo( sourceColumn, embed, tokenize, - }; + } } -function setupTokenizeFunction( - tokenizer: PreTrainedTokenizer -): (data: (string | number[])[]) => string[] { - return (data: (string | number[])[]): string[] => { - if (!tokenizer) { - throw new Error("Tokenizer not initialized"); - } - - return data.map((text) => { - try { - const res = tokenizer(text); - return res; - } catch (error) { - throw new Error( - `Tokenization process failed for text: ${errorToStringMainProcess( - error - )}` - ); - } - }); - }; +export interface EnhancedEmbeddingFunction extends lancedb.EmbeddingFunction { + name: string + contextLength: number + tokenize: (data: T[]) => string[] } - -async function setupEmbedFunction( - pipe: Pipeline -): Promise<(batch: (string | number[])[]) => Promise> { - return async (batch: (string | number[])[]): Promise => { - if (batch.length === 0 || batch[0].length === 0) { - return []; - } - - if (typeof batch[0][0] === "number") { - return batch as number[][]; - } - - if (!pipe) { - throw new Error("Pipeline not initialized"); - } - - const result: number[][] = await Promise.all( - batch.map(async (text) => { - try { - const res = await pipe(removeMd(text as string), { - pooling: "mean", - normalize: true, - }); - return Array.from(res.data); - } catch (error) { - throw new Error( - `Embedding process failed for text: ${errorToStringMainProcess( - error - )}` - ); - } - }) - ); - - return result; - }; +export async function createEmbeddingFunction( + embeddingModelConfig: EmbeddingModelConfig, + sourceColumn: string, +): Promise> { + if (embeddingModelConfig.type === 'local') { + return createEmbeddingFunctionForLocalModel(embeddingModelConfig, sourceColumn) + } + return createEmbeddingFunctionForRepo(embeddingModelConfig, sourceColumn) } -export const rerankSearchedEmbeddings = async ( - query: string, - searchResults: DBEntry[] -) => { - const { env, AutoModelForSequenceClassification, AutoTokenizer } = - await import("@xenova/transformers"); - env.cacheDir = path.join(app.getPath("userData"), "models", "reranker"); // set for all. Just to deal with library and remote inconsistencies +export const rerankSearchedEmbeddings = async (query: string, searchResults: DBEntry[]) => { + const { env, AutoModelForSequenceClassification, AutoTokenizer } = await import('@xenova/transformers') + env.cacheDir = path.join(app.getPath('userData'), 'models', 'reranker') // set for all. Just to deal with library and remote inconsistencies - const tokenizer = await AutoTokenizer.from_pretrained( - "Xenova/bge-reranker-base" - ); - const model = await AutoModelForSequenceClassification.from_pretrained( - "Xenova/bge-reranker-base" - ); + const tokenizer = await AutoTokenizer.from_pretrained('Xenova/bge-reranker-base') + const model = await AutoModelForSequenceClassification.from_pretrained('Xenova/bge-reranker-base') - const queries = Array(searchResults.length).fill(query); + const queries = Array(searchResults.length).fill(query) const inputs = tokenizer(queries, { text_pair: searchResults.map((item) => item.content), padding: true, truncation: true, - }); + }) - const scores = await model(inputs); + const scores = await model(inputs) // map logits to searchResults by index - const resultsWithIndex = searchResults.map((item, index) => { - return { - ...item, - score: scores.logits.data[index], - }; - }); + const resultsWithIndex = searchResults.map((item, index) => ({ + ...item, + score: scores.logits.data[index], + })) // TODO: we should allow users to set threshold for sensitivity too. - return resultsWithIndex - .sort((a, b) => b.score - a.score) - .filter((item) => item.score > 0); -}; + return resultsWithIndex.sort((a, b) => b.score - a.score).filter((item) => item.score > 0) +} diff --git a/electron/main/vector-database/ipcHandlers.ts b/electron/main/vector-database/ipcHandlers.ts index 45f15939..8492e2f3 100644 --- a/electron/main/vector-database/ipcHandlers.ts +++ b/electron/main/vector-database/ipcHandlers.ts @@ -1,159 +1,110 @@ -import * as fs from "fs"; -import * as path from "path"; - -import { app, BrowserWindow, ipcMain } from "electron"; -import Store from "electron-store"; -import * as lancedb from "vectordb"; - -import { errorToStringMainProcess } from "../common/error"; -import WindowsManager from "../common/windowManager"; -import { getDefaultEmbeddingModelConfig } from "../electron-store/ipcHandlers"; -import { StoreKeys, StoreSchema } from "../electron-store/storeConfig"; -import { - startWatchingDirectory, - updateFileListForRenderer, -} from "../filesystem/filesystem"; -import { createPromptWithContextLimitFromContent } from "../llm/contextLimit"; -import { ollamaService, openAISession } from "../llm/ipcHandlers"; -import { getLLMConfig } from "../llm/llmConfig"; - -import { rerankSearchedEmbeddings } from "./embeddings"; -import { DBEntry, DatabaseFields } from "./schema"; -import { RepopulateTableWithMissingItems } from "./tableHelperFunctions"; +import * as fs from 'fs' +import * as path from 'path' + +import { app, BrowserWindow, ipcMain } from 'electron' +import Store from 'electron-store' +import * as lancedb from 'vectordb' + +import errorToStringMainProcess from '../common/error' +import WindowsManager from '../common/windowManager' +import { getDefaultEmbeddingModelConfig } from '../electron-store/ipcHandlers' +import { StoreKeys, StoreSchema } from '../electron-store/storeConfig' +import { startWatchingDirectory, updateFileListForRenderer } from '../filesystem/filesystem' +import { createPromptWithContextLimitFromContent } from '../llm/contextLimit' +import { ollamaService, openAISession } from '../llm/ipcHandlers' +import { getLLMConfig } from '../llm/llmConfig' + +import { rerankSearchedEmbeddings } from './embeddings' +import { DBEntry, DatabaseFields } from './schema' +import { formatTimestampForLanceDB, RepopulateTableWithMissingItems } from './tableHelperFunctions' export interface PromptWithRagResults { - ragPrompt: string; - uniqueFilesReferenced: string[]; + ragPrompt: string + uniqueFilesReferenced: string[] } export interface BasePromptRequirements { - query: string; - llmName: string; - filePathToBeUsedAsContext?: string; + query: string + llmName: string + filePathToBeUsedAsContext?: string } -export const registerDBSessionHandlers = ( - store: Store, - windowManager: WindowsManager -) => { - let dbConnection: lancedb.Connection; +export const registerDBSessionHandlers = (store: Store, _windowManager: WindowsManager) => { + let dbConnection: lancedb.Connection + const windowManager = _windowManager - ipcMain.handle( - "search", - async ( - event, - query: string, - limit: number, - filter?: string - ): Promise => { - try { - const windowInfo = windowManager.getWindowInfoForContents(event.sender); - if (!windowInfo) { - throw new Error("Window info not found."); - } - const searchResults = await windowInfo.dbTableClient.search( - query, - limit, - filter - ); - return searchResults; - } catch (error) { - console.error("Error searching database:", error); - throw error; - } + ipcMain.handle('search', async (event, query: string, limit: number, filter?: string): Promise => { + const windowInfo = windowManager.getWindowInfoForContents(event.sender) + if (!windowInfo) { + throw new Error('Window info not found.') } - ); + const searchResults = await windowInfo.dbTableClient.search(query, limit, filter) + return searchResults + }) - ipcMain.handle("index-files-in-directory", async (event) => { + ipcMain.handle('index-files-in-directory', async (event) => { try { - console.log("Indexing files in directory"); - const windowInfo = windowManager.getWindowInfoForContents(event.sender); + const windowInfo = windowManager.getWindowInfoForContents(event.sender) if (!windowInfo) { - throw new Error("No window info found"); + throw new Error('No window info found') } - const defaultEmbeddingModelConfig = getDefaultEmbeddingModelConfig(store); - const dbPath = path.join(app.getPath("userData"), "vectordb"); - dbConnection = await lancedb.connect(dbPath); + const defaultEmbeddingModelConfig = getDefaultEmbeddingModelConfig(store) + const dbPath = path.join(app.getPath('userData'), 'vectordb') + dbConnection = await lancedb.connect(dbPath) await windowInfo.dbTableClient.initialize( dbConnection, windowInfo.vaultDirectoryForWindow, - defaultEmbeddingModelConfig - ); + defaultEmbeddingModelConfig, + ) await RepopulateTableWithMissingItems( windowInfo.dbTableClient, windowInfo.vaultDirectoryForWindow, (progress) => { - event.sender.send("indexing-progress", progress); - } - ); - const win = BrowserWindow.fromWebContents(event.sender); + event.sender.send('indexing-progress', progress) + }, + ) + const win = BrowserWindow.fromWebContents(event.sender) if (win) { - windowManager.watcher = startWatchingDirectory( - win, - windowInfo.vaultDirectoryForWindow - ); - updateFileListForRenderer(win, windowInfo.vaultDirectoryForWindow); + windowManager.watcher = startWatchingDirectory(win, windowInfo.vaultDirectoryForWindow) + updateFileListForRenderer(win, windowInfo.vaultDirectoryForWindow) } - event.sender.send("indexing-progress", 1); + event.sender.send('indexing-progress', 1) } catch (error) { - let errorStr = ""; + let errorStr = '' - if ( - errorToStringMainProcess(error).includes("Embedding function error") - ) { - errorStr = `${error}. Please try downloading an embedding model from Hugging Face and attaching it in settings. More information can be found in settings.`; + if (errorToStringMainProcess(error).includes('Embedding function error')) { + errorStr = `${error}. Please try downloading an embedding model from Hugging Face and attaching it in settings. More information can be found in settings.` } else { - errorStr = `${error}. Please try restarting or open a Github issue.`; + errorStr = `${error}. Please try restarting or open a Github issue.` } - event.sender.send("error-to-display-in-window", errorStr); - console.error("Error during file indexing:", error); + event.sender.send('error-to-display-in-window', errorStr) } - }); + }) ipcMain.handle( - "search-with-reranking", - async ( - event, - query: string, - limit: number, - filter?: string - ): Promise => { - try { - const windowInfo = windowManager.getWindowInfoForContents(event.sender); - if (!windowInfo) { - throw new Error("Window info not found."); - } - const searchResults = await windowInfo.dbTableClient.search( - query, - limit, - filter - ); - - const rankedResults = await rerankSearchedEmbeddings( - query, - searchResults - ); - return rankedResults; - } catch (error) { - console.error("Error searching database:", error); - throw error; + 'search-with-reranking', + async (event, query: string, limit: number, filter?: string): Promise => { + const windowInfo = windowManager.getWindowInfoForContents(event.sender) + if (!windowInfo) { + throw new Error('Window info not found.') } - } - ); + const searchResults = await windowInfo.dbTableClient.search(query, limit, filter) + + const rankedResults = await rerankSearchedEmbeddings(query, searchResults) + return rankedResults + }, + ) ipcMain.handle( - "augment-prompt-with-temporal-agent", - async ( - event, - { query, llmName }: BasePromptRequirements - ): Promise => { - const llmSession = openAISession; - const llmConfig = await getLLMConfig(store, ollamaService, llmName); - console.log("llmConfig", llmConfig); + 'augment-prompt-with-temporal-agent', + async (event, { query, llmName }: BasePromptRequirements): Promise => { + const llmSession = openAISession + const llmConfig = await getLLMConfig(store, ollamaService, llmName) + if (!llmConfig) { - throw new Error(`LLM ${llmName} not configured.`); + throw new Error(`LLM ${llmName} not configured.`) } const llmFilter = await llmSession.response( @@ -161,7 +112,7 @@ export const registerDBSessionHandlers = ( llmConfig, [ { - role: "system", + role: 'system', content: `You are an experienced SQL engineer. You are translating natural language queries into temporal filters for a database query. Below are 2 examples: @@ -179,158 +130,118 @@ Filter: ${DatabaseFields.FILE_MODIFIED} > ${formatTimestampForLanceDB(new Date())} For your reference, the timestamp right now is ${formatTimestampForLanceDB( - new Date() + new Date(), )}.Please generate ONLY the temporal filter using the same format as the example given. Please also make sure you only use the ${ DatabaseFields.FILE_MODIFIED } field in the filter. If you don't know or there is no temporal component in the query, please return an empty string.`, }, { - role: "user", + role: 'user', content: query, }, ], false, - store.get(StoreKeys.LLMGenerationParameters) - ); + store.get(StoreKeys.LLMGenerationParameters), + ) try { - let searchResults: DBEntry[] = []; - const maxRAGExamples: number = store.get(StoreKeys.MaxRAGExamples); - const windowInfo = windowManager.getWindowInfoForContents(event.sender); + let searchResults: DBEntry[] = [] + const maxRAGExamples: number = store.get(StoreKeys.MaxRAGExamples) + const windowInfo = windowManager.getWindowInfoForContents(event.sender) if (!windowInfo) { - throw new Error("Window info not found."); + throw new Error('Window info not found.') } - const llmGeneratedFilterString = - llmFilter.choices[0].message.content ?? ""; + const llmGeneratedFilterString = llmFilter.choices[0].message.content ?? '' try { - searchResults = await windowInfo.dbTableClient.search( - query, - maxRAGExamples, - llmGeneratedFilterString - ); + searchResults = await windowInfo.dbTableClient.search(query, maxRAGExamples, llmGeneratedFilterString) } catch (error) { - searchResults = await windowInfo.dbTableClient.search( - query, - maxRAGExamples - ); - searchResults = []; + searchResults = await windowInfo.dbTableClient.search(query, maxRAGExamples) + searchResults = [] } - const basePrompt = - "Answer the question below based on the following notes:\n"; + const basePrompt = 'Answer the question below based on the following notes:\n' const { prompt: ragPrompt } = createPromptWithContextLimitFromContent( searchResults, basePrompt, query, llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - console.log("ragPrompt", ragPrompt); - const uniqueFilesReferenced = [ - ...new Set(searchResults.map((entry) => entry.notepath)), - ]; + llmConfig.contextLength, + ) + + const uniqueFilesReferenced = [...new Set(searchResults.map((entry) => entry.notepath))] return { ragPrompt, uniqueFilesReferenced, - }; + } } catch (error) { - console.error("Error searching database:", error); - throw errorToStringMainProcess(error); + throw new Error(errorToStringMainProcess(error)) } - } - ); + }, + ) ipcMain.handle( - "augment-prompt-with-flashcard-agent", + 'augment-prompt-with-flashcard-agent', async ( event, - { query, llmName, filePathToBeUsedAsContext }: BasePromptRequirements + { query, llmName, filePathToBeUsedAsContext }: BasePromptRequirements, ): Promise => { - const llmSession = openAISession; - console.log("llmName: ", llmName); - const llmConfig = await getLLMConfig(store, ollamaService, llmName); - console.log("llmConfig", llmConfig); + const llmSession = openAISession + + const llmConfig = await getLLMConfig(store, ollamaService, llmName) + if (!llmConfig) { - throw new Error(`LLM ${llmName} not configured.`); + throw new Error(`LLM ${llmName} not configured.`) } if (!filePathToBeUsedAsContext) { - throw new Error( - "Current file path is not provided for flashcard agent." - ); + throw new Error('Current file path is not provided for flashcard agent.') } - const fileResults = fs.readFileSync(filePathToBeUsedAsContext, "utf-8"); - const { prompt: promptToCreateAtomicFacts } = - createPromptWithContextLimitFromContent( - fileResults, - "", - `Extract atomic facts that can be used for students to study, based on this query: ${query}`, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); + const fileResults = fs.readFileSync(filePathToBeUsedAsContext, 'utf-8') + const { prompt: promptToCreateAtomicFacts } = createPromptWithContextLimitFromContent( + fileResults, + '', + `Extract atomic facts that can be used for students to study, based on this query: ${query}`, + llmSession.getTokenizer(llmName), + llmConfig.contextLength, + ) const llmGeneratedFacts = await llmSession.response( llmName, llmConfig, [ { - role: "system", + role: 'system', content: `You are an experienced teacher reading through some notes a student has made and extracting atomic facts. You never come up with your own facts. You generate atomic facts directly from what you read. An atomic fact is a fact that relates to a single piece of knowledge and makes it easy to create a question for which the atomic fact is the answer"`, }, { - role: "user", + role: 'user', content: promptToCreateAtomicFacts, }, ], false, - store.get(StoreKeys.LLMGenerationParameters) - ); + store.get(StoreKeys.LLMGenerationParameters), + ) - console.log(llmGeneratedFacts); - const basePrompt = "Given the following atomic facts:\n"; + const basePrompt = 'Given the following atomic facts:\n' const flashcardQuery = - "Create useful FLASHCARDS that can be used for students to study using ONLY the context. Format is Q: A: ."; - const { prompt: promptToCreateFlashcardsWithAtomicFacts } = - createPromptWithContextLimitFromContent( - llmGeneratedFacts.choices[0].message.content || "", - basePrompt, - flashcardQuery, - llmSession.getTokenizer(llmName), - llmConfig.contextLength - ); - console.log( - "promptToCreateFlashcardsWithAtomicFacts: ", - promptToCreateFlashcardsWithAtomicFacts - ); - const uniqueFilesReferenced = [filePathToBeUsedAsContext]; + 'Create useful FLASHCARDS that can be used for students to study using ONLY the context. Format is Q: A: .' + const { prompt: promptToCreateFlashcardsWithAtomicFacts } = createPromptWithContextLimitFromContent( + llmGeneratedFacts.choices[0].message.content || '', + basePrompt, + flashcardQuery, + llmSession.getTokenizer(llmName), + llmConfig.contextLength, + ) + + const uniqueFilesReferenced = [filePathToBeUsedAsContext] return { ragPrompt: promptToCreateFlashcardsWithAtomicFacts, uniqueFilesReferenced, - }; - } - ); - - ipcMain.handle("get-database-fields", () => { - return DatabaseFields; - }); -}; - -function formatTimestampForLanceDB(date: Date): string { - const year = date.getFullYear(); - const month = date.getMonth() + 1; // getMonth() is zero-based - const day = date.getDate(); - const hours = date.getHours(); - const minutes = date.getMinutes(); - const seconds = date.getSeconds(); - - // Pad single digits with leading zeros - const monthPadded = month.toString().padStart(2, "0"); - const dayPadded = day.toString().padStart(2, "0"); - const hoursPadded = hours.toString().padStart(2, "0"); - const minutesPadded = minutes.toString().padStart(2, "0"); - const secondsPadded = seconds.toString().padStart(2, "0"); - - return `timestamp '${year}-${monthPadded}-${dayPadded} ${hoursPadded}:${minutesPadded}:${secondsPadded}'`; + } + }, + ) + + ipcMain.handle('get-database-fields', () => DatabaseFields) } diff --git a/electron/main/vector-database/lance.ts b/electron/main/vector-database/lance.ts index d3144457..9ca7f133 100644 --- a/electron/main/vector-database/lance.ts +++ b/electron/main/vector-database/lance.ts @@ -1,64 +1,57 @@ -import * as lancedb from "vectordb"; +import * as lancedb from 'vectordb' -import { errorToStringMainProcess } from "../common/error"; +import errorToStringMainProcess from '../common/error' -import { EnhancedEmbeddingFunction } from "./embeddings"; -import CreateDatabaseSchema, { isStringifiedSchemaEqual } from "./schema"; +import { EnhancedEmbeddingFunction } from './embeddings' +import CreateDatabaseSchema, { isStringifiedSchemaEqual } from './schema' + +export const generateTableName = (embeddingFuncName: string, userDirectory: string): string => { + const sanitizeForFileSystem = (str: string) => str.replace(/[<>:"/\\|?*]/g, '_') + + const directoryPathAlias = sanitizeForFileSystem(userDirectory) + const sanitizedEmbeddingFuncName = sanitizeForFileSystem(embeddingFuncName) + + return `ragnote_table_${sanitizedEmbeddingFuncName}_${directoryPathAlias}` +} const GetOrCreateLanceTable = async ( db: lancedb.Connection, embedFunc: EnhancedEmbeddingFunction, - userDirectory: string + userDirectory: string, ): Promise> => { try { - const allTableNames = await db.tableNames(); - const intendedSchema = CreateDatabaseSchema(embedFunc.contextLength); - const tableName = generateTableName(embedFunc.name, userDirectory); + const allTableNames = await db.tableNames() + const intendedSchema = CreateDatabaseSchema(embedFunc.contextLength) + const tableName = generateTableName(embedFunc.name, userDirectory) if (allTableNames.includes(tableName)) { - const table = await db.openTable(tableName, embedFunc); - const schema = await table.schema; + const table = await db.openTable(tableName, embedFunc) + const schema = await table.schema if (!isStringifiedSchemaEqual(schema, intendedSchema)) { - await db.dropTable(tableName); + await db.dropTable(tableName) const recreatedTable = await db.createTable({ name: tableName, schema: intendedSchema, embeddingFunction: embedFunc, - }); - return recreatedTable; + }) + return recreatedTable } - return table; + return table } const newTable = await db.createTable({ name: tableName, schema: intendedSchema, embeddingFunction: embedFunc, - }); - return newTable; + }) + return newTable } catch (error) { - const errorMessage = `Error in GetOrCreateLanceTable: ${errorToStringMainProcess( - error - )}`; - console.error(errorMessage); - throw new Error(errorMessage); - } -}; + const errorMessage = `Error in GetOrCreateLanceTable: ${errorToStringMainProcess(error)}` -export const generateTableName = ( - embeddingFuncName: string, - userDirectory: string -): string => { - const sanitizeForFileSystem = (str: string) => { - return str.replace(/[<>:"/\\|?*]/g, "_"); - }; - - const directoryPathAlias = sanitizeForFileSystem(userDirectory); - const sanitizedEmbeddingFuncName = sanitizeForFileSystem(embeddingFuncName); - - return `ragnote_table_${sanitizedEmbeddingFuncName}_${directoryPathAlias}`; -}; + throw new Error(errorMessage) + } +} -export default GetOrCreateLanceTable; +export default GetOrCreateLanceTable diff --git a/electron/main/vector-database/lanceTableWrapper.ts b/electron/main/vector-database/lanceTableWrapper.ts index 1ac9b6a7..b0715732 100644 --- a/electron/main/vector-database/lanceTableWrapper.ts +++ b/electron/main/vector-database/lanceTableWrapper.ts @@ -1,127 +1,105 @@ -import { - Connection, - Table as LanceDBTable, - MetricType, - makeArrowTable, -} from "vectordb"; - -import { EmbeddingModelConfig } from "../electron-store/storeConfig"; - -import { - EnhancedEmbeddingFunction, - createEmbeddingFunction, -} from "./embeddings"; -import GetOrCreateLanceTable from "./lance"; -import { DBEntry, DBQueryResult, DatabaseFields } from "./schema"; -import { - sanitizePathForDatabase, - convertRecordToDBType, -} from "./tableHelperFunctions"; - -export class LanceDBTableWrapper { +import { Connection, Table as LanceDBTable, MetricType, makeArrowTable } from 'vectordb' + +import { EmbeddingModelConfig } from '../electron-store/storeConfig' + +import { EnhancedEmbeddingFunction, createEmbeddingFunction } from './embeddings' +import GetOrCreateLanceTable from './lance' +import { DBEntry, DBQueryResult, DatabaseFields } from './schema' + +export function unsanitizePathForFileSystem(dbPath: string): string { + return dbPath.replace(/''/g, "'") +} + +export function convertRecordToDBType(record: Record): T | null { + const recordWithType = record as T + recordWithType.notepath = unsanitizePathForFileSystem(recordWithType.notepath) + return recordWithType +} + +export function sanitizePathForDatabase(filePath: string): string { + return filePath.replace(/'/g, "''") +} + +class LanceDBTableWrapper { // eslint-disable-next-line @typescript-eslint/no-explicit-any - public lanceTable!: LanceDBTable; - private embedFun!: EnhancedEmbeddingFunction; - - async initialize( - dbConnection: Connection, - userDirectory: string, - embeddingModelConfig: EmbeddingModelConfig - ) { + public lanceTable!: LanceDBTable + + private embedFun!: EnhancedEmbeddingFunction + + async initialize(dbConnection: Connection, userDirectory: string, embeddingModelConfig: EmbeddingModelConfig) { try { - this.embedFun = await createEmbeddingFunction( - embeddingModelConfig, - "content" - ); + this.embedFun = await createEmbeddingFunction(embeddingModelConfig, 'content') } catch (error) { - throw new Error("Embedding function error: " + error); + throw new Error(`Embedding function error: ${error}`) } - this.lanceTable = await GetOrCreateLanceTable( - dbConnection, - this.embedFun, - userDirectory - ); + this.lanceTable = await GetOrCreateLanceTable(dbConnection, this.embedFun, userDirectory) } - async add( - data: DBEntry[], - onProgress?: (progress: number) => void - ): Promise { - data = data - .filter((x) => x.content !== "") - .map((x) => { - x.notepath = sanitizePathForDatabase(x.notepath); - return x; - }); - - //clean up previously indexed entries and reindex the whole file - await this.deleteDBItemsByFilePaths(data.map((x) => x.notepath)); - - const recordEntry: Record[] = data as unknown as Record< - string, - unknown - >[]; - const numberOfChunksToIndexAtOnce = 50; - const chunks = []; + async add(_data: DBEntry[], onProgress?: (progress: number) => void): Promise { + const data = _data + .filter((x) => x.content !== '') + .map((_x) => { + const x = _x + x.notepath = sanitizePathForDatabase(x.notepath) + return x + }) + + // clean up previously indexed entries and reindex the whole file + await this.deleteDBItemsByFilePaths(data.map((x) => x.notepath)) + + const recordEntry: Record[] = data as unknown as Record[] + const numberOfChunksToIndexAtOnce = 50 + const chunks = [] for (let i = 0; i < recordEntry.length; i += numberOfChunksToIndexAtOnce) { - chunks.push(recordEntry.slice(i, i + numberOfChunksToIndexAtOnce)); + chunks.push(recordEntry.slice(i, i + numberOfChunksToIndexAtOnce)) } - if (chunks.length == 0) return; + if (chunks.length === 0) return - let index = 0; - const totalChunks = chunks.length; - for (const chunk of chunks) { - const arrowTableOfChunk = makeArrowTable(chunk); - await this.lanceTable.add(arrowTableOfChunk); + const totalChunks = chunks.length + chunks.forEach(async (chunk, index) => { + const arrowTableOfChunk = makeArrowTable(chunk) + await this.lanceTable.add(arrowTableOfChunk) - index++; - const progress = index / totalChunks; + const progress = (index + 1) / totalChunks if (onProgress) { - onProgress(progress); + onProgress(progress) } - } + }) } async deleteDBItemsByFilePaths(filePaths: string[]): Promise { const quotedFilePaths = filePaths .map((filePath) => sanitizePathForDatabase(filePath)) .map((filePath) => `'${filePath}'`) - .join(", "); - if (quotedFilePaths === "") { - return; + .join(', ') + if (quotedFilePaths === '') { + return } - const filterString = `${DatabaseFields.NOTE_PATH} IN (${quotedFilePaths})`; + const filterString = `${DatabaseFields.NOTE_PATH} IN (${quotedFilePaths})` try { - await this.lanceTable.delete(filterString); + await this.lanceTable.delete(filterString) } catch (error) { - console.error( - `Error deleting items from DB: ${error} using filter string: ${filterString}` - ); + // no need to throw error } } - async updateDBItemsWithNewFilePath( - oldFilePath: string, - newFilePath: string - ): Promise { - const sanitizedFilePath = sanitizePathForDatabase(oldFilePath); - if (sanitizedFilePath === "") { - return; + async updateDBItemsWithNewFilePath(oldFilePath: string, newFilePath: string): Promise { + const sanitizedFilePath = sanitizePathForDatabase(oldFilePath) + if (sanitizedFilePath === '') { + return } - const filterString = `${DatabaseFields.NOTE_PATH} = '${sanitizedFilePath}'`; + const filterString = `${DatabaseFields.NOTE_PATH} = '${sanitizedFilePath}'` try { await this.lanceTable.update({ where: filterString, values: { [DatabaseFields.NOTE_PATH]: sanitizePathForDatabase(newFilePath), }, - }); + }) } catch (error) { - console.error( - `Error updating items from DB: ${error} using filter string: ${filterString}` - ); + // no need to throw error } } @@ -129,33 +107,24 @@ export class LanceDBTableWrapper { query: string, // metricType: string, limit: number, - filter?: string + filter?: string, ): Promise { - const lanceQuery = await this.lanceTable - .search(query) - .metricType(MetricType.Cosine) - .limit(limit); + const lanceQuery = await this.lanceTable.search(query).metricType(MetricType.Cosine).limit(limit) if (filter) { - lanceQuery.prefilter(true); - lanceQuery.filter(filter); + lanceQuery.prefilter(true) + lanceQuery.filter(filter) } - const rawResults = await lanceQuery.execute(); - const mapped = rawResults.map(convertRecordToDBType); - return mapped as DBQueryResult[]; + const rawResults = await lanceQuery.execute() + const mapped = rawResults.map(convertRecordToDBType) + return mapped as DBQueryResult[] } async filter(filterString: string, limit: number = 10): Promise { - const rawResults = await this.lanceTable - .filter(filterString) - .limit(limit) - .execute(); - const mapped = rawResults.map(convertRecordToDBType); - return mapped as DBEntry[]; - } - - async countRows(): Promise { - this.lanceTable.countRows; - return await this.lanceTable.countRows(); + const rawResults = await this.lanceTable.filter(filterString).limit(limit).execute() + const mapped = rawResults.map(convertRecordToDBType) + return mapped as DBEntry[] } } + +export default LanceDBTableWrapper diff --git a/electron/main/vector-database/schema.ts b/electron/main/vector-database/schema.ts index 5fae2a9d..07eb0593 100644 --- a/electron/main/vector-database/schema.ts +++ b/electron/main/vector-database/schema.ts @@ -1,89 +1,60 @@ -import { - Schema, - Field, - Utf8, - FixedSizeList, - Float32, - Float64, - DateUnit, - Date_ as ArrowDate, -} from "apache-arrow"; +import { Schema, Field, Utf8, FixedSizeList, Float32, Float64, DateUnit, Date_ as ArrowDate } from 'apache-arrow' export interface DBEntry { - notepath: string; - vector?: Float32Array; - content: string; - subnoteindex: number; - timeadded: Date; - filemodified: Date; - filecreated: Date; + notepath: string + vector?: Float32Array + content: string + subnoteindex: number + timeadded: Date + filemodified: Date + filecreated: Date } export interface DBQueryResult extends DBEntry { - _distance: number; + _distance: number } -export const chunksize = 500; +export const chunksize = 500 export enum DatabaseFields { - NOTE_PATH = "notepath", - VECTOR = "vector", - CONTENT = "content", - SUB_NOTE_INDEX = "subnoteindex", - TIME_ADDED = "timeadded", - FILE_MODIFIED = "filemodified", - FILE_CREATED = "filecreated", - DISTANCE = "_distance", + NOTE_PATH = 'notepath', + VECTOR = 'vector', + CONTENT = 'content', + SUB_NOTE_INDEX = 'subnoteindex', + TIME_ADDED = 'timeadded', + FILE_MODIFIED = 'filemodified', + FILE_CREATED = 'filecreated', + DISTANCE = '_distance', } const CreateDatabaseSchema = (vectorDim: number): Schema => { const schemaFields = [ new Field(DatabaseFields.NOTE_PATH, new Utf8(), false), - new Field( - DatabaseFields.VECTOR, - new FixedSizeList(vectorDim, new Field("item", new Float32())), - false - ), + new Field(DatabaseFields.VECTOR, new FixedSizeList(vectorDim, new Field('item', new Float32())), false), new Field(DatabaseFields.CONTENT, new Utf8(), false), new Field(DatabaseFields.SUB_NOTE_INDEX, new Float64(), false), - new Field( - DatabaseFields.TIME_ADDED, - new ArrowDate(DateUnit.MILLISECOND), - false - ), - new Field( - DatabaseFields.FILE_MODIFIED, - new ArrowDate(DateUnit.MILLISECOND), - false - ), - new Field( - DatabaseFields.FILE_CREATED, - new ArrowDate(DateUnit.MILLISECOND), - false - ), - ]; - const schema = new Schema(schemaFields); - return schema; -}; + new Field(DatabaseFields.TIME_ADDED, new ArrowDate(DateUnit.MILLISECOND), false), + new Field(DatabaseFields.FILE_MODIFIED, new ArrowDate(DateUnit.MILLISECOND), false), + new Field(DatabaseFields.FILE_CREATED, new ArrowDate(DateUnit.MILLISECOND), false), + ] + const schema = new Schema(schemaFields) + return schema +} -const serializeSchema = (schema: Schema): string => { - return JSON.stringify( +const serializeSchema = (schema: Schema): string => + JSON.stringify( schema.fields.map((field) => ({ name: field.name, type: field.type.toString(), nullable: field.nullable, - })) - ); -}; + })), + ) -export const isStringifiedSchemaEqual = ( - schema1: Schema, - schema2: Schema -): boolean => { - const serializedSchema1 = serializeSchema(schema1); - const serializedSchema2 = serializeSchema(schema2); +export const isStringifiedSchemaEqual = (schema1: Schema, schema2: Schema): boolean => { + const serializedSchema1 = serializeSchema(schema1) + const serializedSchema2 = serializeSchema(schema2) - const areEqual = serializedSchema1 === serializedSchema2; - return areEqual; -}; + const areEqual = serializedSchema1 === serializedSchema2 + return areEqual +} -export default CreateDatabaseSchema; +export default CreateDatabaseSchema diff --git a/electron/main/vector-database/tableHelperFunctions.ts b/electron/main/vector-database/tableHelperFunctions.ts index a7fd354f..c15639b0 100644 --- a/electron/main/vector-database/tableHelperFunctions.ts +++ b/electron/main/vector-database/tableHelperFunctions.ts @@ -1,253 +1,223 @@ -import * as fs from "fs"; +import * as fs from 'fs' -import { chunkMarkdownByHeadingsAndByCharsIfBig } from "../common/chunking"; -import { errorToStringMainProcess } from "../common/error"; +import { chunkMarkdownByHeadingsAndByCharsIfBig } from '../common/chunking' +import errorToStringMainProcess from '../common/error' import { GetFilesInfoList, + GetFilesInfoTree, flattenFileInfoTree, + moveFileOrDirectoryInFileSystem, readFile, -} from "../filesystem/filesystem"; -import { FileInfo, FileInfoTree } from "../filesystem/types"; +} from '../filesystem/filesystem' +import { FileInfo, FileInfoTree } from '../filesystem/types' -import { LanceDBTableWrapper } from "./lanceTableWrapper"; -import { DBEntry, DBQueryResult, DatabaseFields } from "./schema"; +import LanceDBTableWrapper, { convertRecordToDBType } from './lanceTableWrapper' +import { DBEntry, DatabaseFields } from './schema' + +const convertFileTypeToDBType = async (file: FileInfo): Promise => { + const fileContent = readFile(file.path) + const chunks = await chunkMarkdownByHeadingsAndByCharsIfBig(fileContent) + const entries = chunks.map((content, index) => ({ + notepath: file.path, + content, + subnoteindex: index, + timeadded: new Date(), + filemodified: file.dateModified, + filecreated: file.dateCreated, + })) + return entries +} + +export const convertFileInfoListToDBItems = async (filesInfoList: FileInfo[]): Promise => { + const promises = filesInfoList.map(convertFileTypeToDBType) + const filesAsChunksToAddToDB = await Promise.all(promises) + return filesAsChunksToAddToDB +} + +const getTableAsArray = async (table: LanceDBTableWrapper): Promise<{ notepath: string; filemodified: Date }[]> => { + const nonEmptyResults = await table.lanceTable + .filter(`${DatabaseFields.NOTE_PATH} != ''`) + .select([DatabaseFields.NOTE_PATH, DatabaseFields.FILE_MODIFIED]) + .execute() + + const mapped = nonEmptyResults.map(convertRecordToDBType) + + return mapped as { notepath: string; filemodified: Date }[] +} + +const areChunksMissingFromTable = ( + chunksToCheck: DBEntry[], + tableArray: { notepath: string; filemodified: Date }[], +): boolean => { + // checking whether th + if (chunksToCheck.length === 0) { + // if there are no chunks and we are checking whether the table + return false + } + + if (chunksToCheck[0].content === '') { + return false + } + // then we'd check if the filepaths are not present in the table at all: + const { notepath } = chunksToCheck[0] + const itemsAlreadyInTable = tableArray.filter((item) => item.notepath === notepath) + if (itemsAlreadyInTable.length === 0) { + // if we find no items in the table with the same notepath, then we should add the chunks to the table + return true + } + + return chunksToCheck[0].filemodified > itemsAlreadyInTable[0].filemodified +} + +const computeDbItemsToAddOrUpdate = async ( + filesInfoList: FileInfo[], + tableArray: { notepath: string; filemodified: Date }[], +): Promise => { + const filesAsChunks = await convertFileInfoListToDBItems(filesInfoList) + + const fileChunksMissingFromTable = filesAsChunks.filter((chunksBelongingToFile) => + areChunksMissingFromTable(chunksBelongingToFile, tableArray), + ) + + return fileChunksMissingFromTable +} + +const computeDBItemsToRemoveFromTable = async ( + filesInfoList: FileInfo[], + tableArray: { notepath: string; filemodified: Date }[], +): Promise<{ notepath: string; filemodified: Date }[]> => { + const itemsInTableAndNotInFilesInfoList = tableArray.filter( + (item) => !filesInfoList.some((file) => file.path === item.notepath), + ) + return itemsInTableAndNotInFilesInfoList +} + +const convertFileTreeToDBEntries = async (tree: FileInfoTree): Promise => { + const flattened = flattenFileInfoTree(tree) + + const promises = flattened.map(convertFileTypeToDBType) + + const entries = await Promise.all(promises) + + return entries.flat() +} + +export const removeFileTreeFromDBTable = async ( + dbTable: LanceDBTableWrapper, + fileTree: FileInfoTree, +): Promise => { + const flattened = flattenFileInfoTree(fileTree) + const filePaths = flattened.map((x) => x.path) + await dbTable.deleteDBItemsByFilePaths(filePaths) +} + +export const updateFileInTable = async (dbTable: LanceDBTableWrapper, filePath: string): Promise => { + await dbTable.deleteDBItemsByFilePaths([filePath]) + const content = readFile(filePath) + const chunkedContentList = await chunkMarkdownByHeadingsAndByCharsIfBig(content) + const stats = fs.statSync(filePath) + const dbEntries = chunkedContentList.map((_content, index) => ({ + notepath: filePath, + content: _content, + subnoteindex: index, + timeadded: new Date(), // time now + filemodified: stats.mtime, + filecreated: stats.birthtime, + })) + await dbTable.add(dbEntries) +} export const RepopulateTableWithMissingItems = async ( table: LanceDBTableWrapper, directoryPath: string, - onProgress?: (progress: number) => void + onProgress?: (progress: number) => void, ) => { - let filesInfoTree; - console.log("getting files info list"); + let filesInfoTree + try { - filesInfoTree = GetFilesInfoList(directoryPath); + filesInfoTree = GetFilesInfoList(directoryPath) } catch (error) { - throw new Error( - `Error getting file info list: ${errorToStringMainProcess(error)}` - ); + throw new Error(`Error getting file info list: ${errorToStringMainProcess(error)}`) } - let tableArray; + let tableArray try { - tableArray = await getTableAsArray(table); + tableArray = await getTableAsArray(table) } catch (error) { - throw new Error( - `Error converting table to array: ${errorToStringMainProcess(error)}` - ); + throw new Error(`Error converting table to array: ${errorToStringMainProcess(error)}`) } - let itemsToRemove; + let itemsToRemove try { - itemsToRemove = await computeDBItemsToRemoveFromTable( - filesInfoTree, - tableArray - ); + itemsToRemove = await computeDBItemsToRemoveFromTable(filesInfoTree, tableArray) } catch (error) { - throw new Error( - `Error computing items to remove from table: ${errorToStringMainProcess( - error - )}` - ); + throw new Error(`Error computing items to remove from table: ${errorToStringMainProcess(error)}`) } - const filePathsToRemove = itemsToRemove.map((x) => x.notepath); + const filePathsToRemove = itemsToRemove.map((x) => x.notepath) try { - await table.deleteDBItemsByFilePaths(filePathsToRemove); + await table.deleteDBItemsByFilePaths(filePathsToRemove) } catch (error) { - throw new Error( - `Error deleting items by file paths: ${errorToStringMainProcess(error)}` - ); + throw new Error(`Error deleting items by file paths: ${errorToStringMainProcess(error)}`) } - let dbItemsToAdd; + let dbItemsToAdd try { - dbItemsToAdd = await computeDbItemsToAddOrUpdate(filesInfoTree, tableArray); + dbItemsToAdd = await computeDbItemsToAddOrUpdate(filesInfoTree, tableArray) } catch (error) { - throw new Error( - `Error computing DB items to add: ${errorToStringMainProcess(error)}` - ); + throw new Error(`Error computing DB items to add: ${errorToStringMainProcess(error)}`) } if (dbItemsToAdd.length === 0) { - onProgress && onProgress(1); - return; + if (onProgress) onProgress(1) + return } - const filePathsToDelete = dbItemsToAdd.map((x) => x[0].notepath); + const filePathsToDelete = dbItemsToAdd.map((x) => x[0].notepath) try { - await table.deleteDBItemsByFilePaths(filePathsToDelete); + await table.deleteDBItemsByFilePaths(filePathsToDelete) } catch (error) { - throw new Error( - `Error deleting DB items by file paths: ${errorToStringMainProcess( - error - )}` - ); + throw new Error(`Error deleting DB items by file paths: ${errorToStringMainProcess(error)}`) } - const flattenedItemsToAdd = dbItemsToAdd.flat(); + const flattenedItemsToAdd = dbItemsToAdd.flat() try { - await table.add(flattenedItemsToAdd, onProgress); + await table.add(flattenedItemsToAdd, onProgress) } catch (error) { - throw new Error( - `Error adding items to table: ${errorToStringMainProcess(error)}` - ); - } - - onProgress && onProgress(1); -}; - -const getTableAsArray = async ( - table: LanceDBTableWrapper -): Promise<{ notepath: string; filemodified: Date }[]> => { - const nonEmptyResults = await table.lanceTable - .filter(`${DatabaseFields.NOTE_PATH} != ''`) - .select([DatabaseFields.NOTE_PATH, DatabaseFields.FILE_MODIFIED]) - .execute(); - - const mapped = nonEmptyResults.map(convertRecordToDBType); - - return mapped as { notepath: string; filemodified: Date }[]; -}; - -const computeDbItemsToAddOrUpdate = async ( - filesInfoList: FileInfo[], - tableArray: { notepath: string; filemodified: Date }[] -): Promise => { - const filesAsChunks = await convertFileInfoListToDBItems(filesInfoList); - - const fileChunksMissingFromTable = filesAsChunks.filter( - (chunksBelongingToFile) => - areChunksMissingFromTable(chunksBelongingToFile, tableArray) - ); - - return fileChunksMissingFromTable; -}; - -export const convertFileInfoListToDBItems = async ( - filesInfoList: FileInfo[] -): Promise => { - const promises = filesInfoList.map(convertFileTypeToDBType); - const filesAsChunksToAddToDB = await Promise.all(promises); - return filesAsChunksToAddToDB; -}; - -const computeDBItemsToRemoveFromTable = async ( - filesInfoList: FileInfo[], - tableArray: { notepath: string; filemodified: Date }[] -): Promise<{ notepath: string; filemodified: Date }[]> => { - const itemsInTableAndNotInFilesInfoList = tableArray.filter( - (item) => !filesInfoList.some((file) => file.path == item.notepath) - ); - return itemsInTableAndNotInFilesInfoList; -}; - -const areChunksMissingFromTable = ( - chunksToCheck: DBEntry[], - tableArray: { notepath: string; filemodified: Date }[] -): boolean => { - // checking whether th - if (chunksToCheck.length == 0) { - // if there are no chunks and we are checking whether the table - return false; - } - - if (chunksToCheck[0].content === "") { - return false; + throw new Error(`Error adding items to table: ${errorToStringMainProcess(error)}`) } - // then we'd check if the filepaths are not present in the table at all: - const notepath = chunksToCheck[0].notepath; - const itemsAlreadyInTable = tableArray.filter( - (item) => item.notepath == notepath - ); - if (itemsAlreadyInTable.length == 0) { - // if we find no items in the table with the same notepath, then we should add the chunks to the table - return true; - } - - return chunksToCheck[0].filemodified > itemsAlreadyInTable[0].filemodified; -}; - -const convertFileTreeToDBEntries = async ( - tree: FileInfoTree -): Promise => { - const flattened = flattenFileInfoTree(tree); - - const promises = flattened.map(convertFileTypeToDBType); - const entries = await Promise.all(promises); - - return entries.flat(); -}; - -const convertFileTypeToDBType = async (file: FileInfo): Promise => { - const fileContent = readFile(file.path); - const chunks = await chunkMarkdownByHeadingsAndByCharsIfBig(fileContent); - const entries = chunks.map((content, index) => { - return { - notepath: file.path, - content: content, - subnoteindex: index, - timeadded: new Date(), - filemodified: file.dateModified, - filecreated: file.dateCreated, - }; - }); - return entries; -}; - -export function sanitizePathForDatabase(filePath: string): string { - return filePath.replace(/'/g, "''"); + if (onProgress) onProgress(1) } -export function unsanitizePathForFileSystem(dbPath: string): string { - return dbPath.replace(/''/g, "'"); +export const addFileTreeToDBTable = async (dbTable: LanceDBTableWrapper, fileTree: FileInfoTree): Promise => { + const dbEntries = await convertFileTreeToDBEntries(fileTree) + await dbTable.add(dbEntries) } -export const addFileTreeToDBTable = async ( - dbTable: LanceDBTableWrapper, - fileTree: FileInfoTree -): Promise => { - const dbEntries = await convertFileTreeToDBEntries(fileTree); - await dbTable.add(dbEntries); -}; - -export const removeFileTreeFromDBTable = async ( - dbTable: LanceDBTableWrapper, - fileTree: FileInfoTree -): Promise => { - const flattened = flattenFileInfoTree(fileTree); - const filePaths = flattened.map((x) => x.path); - await dbTable.deleteDBItemsByFilePaths(filePaths); -}; +export const orchestrateEntryMove = async (table: LanceDBTableWrapper, sourcePath: string, destinationPath: string) => { + const fileSystemTree = GetFilesInfoTree(sourcePath) + await removeFileTreeFromDBTable(table, fileSystemTree) + moveFileOrDirectoryInFileSystem(sourcePath, destinationPath).then((newDestinationPath) => { + if (newDestinationPath) { + addFileTreeToDBTable(table, GetFilesInfoTree(newDestinationPath)) + } + }) +} -export const updateFileInTable = async ( - dbTable: LanceDBTableWrapper, - filePath: string -): Promise => { - await dbTable.deleteDBItemsByFilePaths([filePath]); - const content = readFile(filePath); - const chunkedContentList = await chunkMarkdownByHeadingsAndByCharsIfBig( - content - ); - const stats = fs.statSync(filePath); - const dbEntries = chunkedContentList.map((content, index) => { - return { - notepath: filePath, - content: content, - subnoteindex: index, - timeadded: new Date(), // time now - filemodified: stats.mtime, - filecreated: stats.birthtime, - }; - }); - await dbTable.add(dbEntries); -}; - -export function convertRecordToDBType( - record: Record -): T | null { - const recordWithType = record as T; - recordWithType.notepath = unsanitizePathForFileSystem( - recordWithType.notepath - ); - return recordWithType; +export function formatTimestampForLanceDB(date: Date): string { + const year = date.getFullYear() + const month = date.getMonth() + 1 // getMonth() is zero-based + const day = date.getDate() + const hours = date.getHours() + const minutes = date.getMinutes() + const seconds = date.getSeconds() + + // Pad single digits with leading zeros + const monthPadded = month.toString().padStart(2, '0') + const dayPadded = day.toString().padStart(2, '0') + const hoursPadded = hours.toString().padStart(2, '0') + const minutesPadded = minutes.toString().padStart(2, '0') + const secondsPadded = seconds.toString().padStart(2, '0') + + return `timestamp '${year}-${monthPadded}-${dayPadded} ${hoursPadded}:${minutesPadded}:${secondsPadded}'` } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 63eb37de..922535a4 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from "electron"; +import { contextBridge, ipcRenderer } from 'electron' import { EmbeddingModelConfig, EmbeddingModelWithLocalPath, @@ -6,35 +6,27 @@ import { HardwareConfig, LLMConfig, LLMGenerationParameters, -} from "electron/main/electron-store/storeConfig"; +} from 'electron/main/electron-store/storeConfig' import { AugmentPromptWithFileProps, FileInfoNode, FileInfoTree, RenameFileProps, WriteFileProps, -} from "electron/main/filesystem/types"; -import { PromptWithContextLimit } from "electron/main/llm/contextLimit"; -import { - BasePromptRequirements, - PromptWithRagResults, -} from "electron/main/vector-database/ipcHandlers"; -import { DBEntry, DBQueryResult } from "electron/main/vector-database/schema"; +} from 'electron/main/filesystem/types' +import { PromptWithContextLimit } from 'electron/main/llm/contextLimit' +import { BasePromptRequirements, PromptWithRagResults } from 'electron/main/vector-database/ipcHandlers' +import { DBEntry, DBQueryResult } from 'electron/main/vector-database/schema' -import { ChatHistory } from "@/components/Chat/Chat"; -import { ChatHistoryMetadata } from "@/components/Chat/hooks/use-chat-history"; +import { ChatHistoryMetadata } from '@/components/Chat/hooks/use-chat-history' +import { ChatHistory } from '@/components/Chat/chatUtils' // eslint-disable-next-line @typescript-eslint/no-explicit-any -type IPCHandler any> = ( - ...args: Parameters -) => Promise>; +type IPCHandler any> = (...args: Parameters) => Promise> // eslint-disable-next-line @typescript-eslint/no-explicit-any -function createIPCHandler any>( - channel: string -): IPCHandler { - return (...args: Parameters) => - ipcRenderer.invoke(channel, ...args) as Promise>; +function createIPCHandler any>(channel: string): IPCHandler { + return (...args: Parameters) => ipcRenderer.invoke(channel, ...args) as Promise> } function createIPCHandlerWithChannel any>( @@ -45,314 +37,176 @@ function createIPCHandlerWithChannel any>( } const database = { - search: - createIPCHandler< - ( - query: string, - limit: number, - filter?: string - ) => Promise - >("search"), - searchWithReranking: createIPCHandler< - (query: string, limit: number, filter?: string) => Promise - >("search-with-reranking"), - deleteLanceDBEntriesByFilePath: createIPCHandler< - (filePath: string) => Promise - >("delete-lance-db-entries-by-filepath"), - indexFilesInDirectory: createIPCHandler<() => Promise>( - "index-files-in-directory" + search: createIPCHandler<(query: string, limit: number, filter?: string) => Promise>('search'), + searchWithReranking: + createIPCHandler<(query: string, limit: number, filter?: string) => Promise>( + 'search-with-reranking', + ), + deleteLanceDBEntriesByFilePath: createIPCHandler<(filePath: string) => Promise>( + 'delete-lance-db-entries-by-filepath', ), - augmentPromptWithTemporalAgent: createIPCHandler< - (args: BasePromptRequirements) => Promise - >("augment-prompt-with-temporal-agent"), - augmentPromptWithFlashcardAgent: createIPCHandler< - (args: BasePromptRequirements) => Promise - >("augment-prompt-with-flashcard-agent"), - getDatabaseFields: createIPCHandler<() => Promise>>( - "get-database-fields" + indexFilesInDirectory: createIPCHandler<() => Promise>('index-files-in-directory'), + augmentPromptWithTemporalAgent: createIPCHandler<(args: BasePromptRequirements) => Promise>( + 'augment-prompt-with-temporal-agent', ), -}; + augmentPromptWithFlashcardAgent: createIPCHandler<(args: BasePromptRequirements) => Promise>( + 'augment-prompt-with-flashcard-agent', + ), + getDatabaseFields: createIPCHandler<() => Promise>>('get-database-fields'), +} const electronUtils = { - openExternal: - createIPCHandler<(url: string) => Promise>("open-external"), - getPlatform: createIPCHandler<() => Promise>("get-platform"), - 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"), - showCreateFileModal: createIPCHandler< - (relativePath: string) => Promise - >("empty-new-note-listener"), - showCreateDirectoryModal: createIPCHandler< - (relativePath: string) => Promise - >("empty-new-directory-listener"), + openExternal: createIPCHandler<(url: string) => Promise>('open-external'), + getPlatform: createIPCHandler<() => Promise>('get-platform'), + 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'), + showCreateFileModal: createIPCHandler<(relativePath: string) => Promise>('empty-new-note-listener'), + showCreateDirectoryModal: createIPCHandler<(relativePath: string) => Promise>('empty-new-directory-listener'), }; const electronStore = { - setVaultDirectoryForWindow: createIPCHandler<(path: string) => Promise>( - "set-vault-directory-for-window" - ), - getVaultDirectoryForWindow: createIPCHandler<() => Promise>( - "get-vault-directory-for-window" - ), - getDefaultEmbeddingModel: createIPCHandler<() => Promise>( - "get-default-embedding-model" - ), - setDefaultEmbeddingModel: createIPCHandler< - (repoName: string) => Promise - >("set-default-embedding-model"), - addNewLocalEmbeddingModel: createIPCHandler< - (model: EmbeddingModelWithLocalPath) => Promise - >("add-new-local-embedding-model"), - getEmbeddingModels: createIPCHandler< - () => Promise> - >("get-embedding-models"), - addNewRepoEmbeddingModel: createIPCHandler< - (model: EmbeddingModelWithRepo) => Promise - >("add-new-repo-embedding-model"), - updateEmbeddingModel: createIPCHandler< - ( - modelName: string, - updatedModel: EmbeddingModelWithLocalPath | EmbeddingModelWithRepo - ) => Promise - >("update-embedding-model"), - removeEmbeddingModel: createIPCHandler<(modelName: string) => Promise>( - "remove-embedding-model" - ), - getNoOfRAGExamples: createIPCHandler<() => Promise>( - "get-no-of-rag-examples" - ), - setNoOfRAGExamples: createIPCHandler<(noOfExamples: number) => Promise>( - "set-no-of-rag-examples" - ), - getChunkSize: createIPCHandler<() => Promise>("get-chunk-size"), - setChunkSize: - createIPCHandler<(chunkSize: number) => Promise>("set-chunk-size"), - getHardwareConfig: createIPCHandler<() => Promise>( - "get-hardware-config" - ), - setHardwareConfig: createIPCHandler< - (config: HardwareConfig) => Promise - >("set-hardware-config"), - getLLMGenerationParams: createIPCHandler< - () => Promise - >("get-llm-generation-params"), - setLLMGenerationParams: 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" - ), - 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"), - getCurrentOpenFiles: createIPCHandler<() => Promise>( - "get-current-open-files" - ), - setCurrentOpenFiles: createIPCHandlerWithChannel< - (action: Action, args: Args) => Promise - >("set-current-open-files"), -}; + setVaultDirectoryForWindow: createIPCHandler<(path: string) => Promise>('set-vault-directory-for-window'), + getVaultDirectoryForWindow: createIPCHandler<() => Promise>('get-vault-directory-for-window'), + getDefaultEmbeddingModel: createIPCHandler<() => Promise>('get-default-embedding-model'), + setDefaultEmbeddingModel: createIPCHandler<(repoName: string) => Promise>('set-default-embedding-model'), + addNewLocalEmbeddingModel: createIPCHandler<(model: EmbeddingModelWithLocalPath) => Promise>( + 'add-new-local-embedding-model', + ), + getEmbeddingModels: createIPCHandler<() => Promise>>('get-embedding-models'), + addNewRepoEmbeddingModel: + createIPCHandler<(model: EmbeddingModelWithRepo) => Promise>('add-new-repo-embedding-model'), + updateEmbeddingModel: + createIPCHandler< + (modelName: string, updatedModel: EmbeddingModelWithLocalPath | EmbeddingModelWithRepo) => Promise + >('update-embedding-model'), + removeEmbeddingModel: createIPCHandler<(modelName: string) => Promise>('remove-embedding-model'), + getNoOfRAGExamples: createIPCHandler<() => Promise>('get-no-of-rag-examples'), + setNoOfRAGExamples: createIPCHandler<(noOfExamples: number) => Promise>('set-no-of-rag-examples'), + getChunkSize: createIPCHandler<() => Promise>('get-chunk-size'), + setChunkSize: createIPCHandler<(chunkSize: number) => Promise>('set-chunk-size'), + getHardwareConfig: createIPCHandler<() => Promise>('get-hardware-config'), + setHardwareConfig: createIPCHandler<(config: HardwareConfig) => Promise>('set-hardware-config'), + getLLMGenerationParams: createIPCHandler<() => Promise>('get-llm-generation-params'), + setLLMGenerationParams: + 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'), + 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'), + getCurrentOpenFiles: createIPCHandler<() => Promise>('get-current-open-files'), + setCurrentOpenFiles: + createIPCHandlerWithChannel<(action: Action, args: Args) => Promise>('set-current-open-files'), +} const fileSystem = { - openDirectoryDialog: createIPCHandler<() => Promise>( - "open-directory-dialog" - ), - openFileDialog: - createIPCHandler<(fileExtensions?: string[]) => Promise>( - "open-file-dialog" + openDirectoryDialog: createIPCHandler<() => Promise>('open-directory-dialog'), + openFileDialog: createIPCHandler<(fileExtensions?: string[]) => Promise>('open-file-dialog'), + getFilesTreeForWindow: createIPCHandler<() => Promise>('get-files-tree-for-window'), + writeFile: createIPCHandler<(writeFileProps: WriteFileProps) => Promise>('write-file'), + isDirectory: createIPCHandler<(filePath: string) => Promise>('is-directory'), + renameFileRecursive: createIPCHandler<(renameFileProps: RenameFileProps) => Promise>('rename-file-recursive'), + indexFileInDatabase: createIPCHandler<(filePath: string) => Promise>('index-file-in-database'), + createFile: createIPCHandler<(filePath: string, content: string) => Promise>('create-file'), + createDirectory: createIPCHandler<(dirPath: string) => Promise>('create-directory'), + readFile: createIPCHandler<(filePath: string) => Promise>('read-file'), + checkFileExists: createIPCHandler<(filePath: string) => Promise>('check-file-exists'), + deleteFile: createIPCHandler<(filePath: string) => Promise>('delete-file'), + moveFileOrDir: createIPCHandler<(sourcePath: string, destinationPath: string) => Promise>('move-file-or-dir'), + augmentPromptWithFile: + createIPCHandler<(augmentPromptWithFileProps: AugmentPromptWithFileProps) => Promise>( + 'augment-prompt-with-file', ), - getFilesTreeForWindow: createIPCHandler<() => Promise>( - "get-files-tree-for-window" + getFilesystemPathsAsDBItems: createIPCHandler<(paths: string[]) => Promise>( + 'get-filesystem-paths-as-db-items', ), - writeFile: - createIPCHandler<(writeFileProps: WriteFileProps) => Promise>( - "write-file" - ), - isDirectory: - createIPCHandler<(filePath: string) => Promise>("is-directory"), - renameFileRecursive: createIPCHandler< - (renameFileProps: RenameFileProps) => Promise - >("rename-file-recursive"), - indexFileInDatabase: createIPCHandler<(filePath: string) => Promise>( - "index-file-in-database" + generateFlashcardsWithFile: createIPCHandler<(flashcardWithFileProps: AugmentPromptWithFileProps) => Promise>( + 'generate-flashcards-from-file', ), - createFile: - createIPCHandler<(filePath: string, content: string) => Promise>( - "create-file" - ), - createDirectory: - createIPCHandler<(dirPath: string) => Promise>("create-directory"), - readFile: - createIPCHandler<(filePath: string) => Promise>("read-file"), - checkFileExists: - createIPCHandler<(filePath: string) => Promise>( - "check-file-exists" - ), - deleteFile: - createIPCHandler<(filePath: string) => Promise>("delete-file"), - moveFileOrDir: - createIPCHandler< - (sourcePath: string, destinationPath: string) => Promise - >("move-file-or-dir"), - augmentPromptWithFile: createIPCHandler< - ( - augmentPromptWithFileProps: AugmentPromptWithFileProps - ) => Promise - >("augment-prompt-with-file"), - getFilesystemPathsAsDBItems: createIPCHandler< - (paths: string[]) => Promise - >("get-filesystem-paths-as-db-items"), - generateFlashcardsWithFile: createIPCHandler< - (flashcardWithFileProps: AugmentPromptWithFileProps) => Promise - >("generate-flashcards-from-file"), -}; +} const path = { - basename: - createIPCHandler<(pathString: string) => Promise>("path-basename"), - join: createIPCHandler<(...pathSegments: string[]) => Promise>( - "join-path" + basename: createIPCHandler<(pathString: string) => Promise>('path-basename'), + join: createIPCHandler<(...pathSegments: string[]) => Promise>('join-path'), + dirname: createIPCHandler<(pathString: string) => Promise>('path-dirname'), + relative: createIPCHandler<(from: string, to: string) => Promise>('path-relative'), + addExtensionIfNoExtensionPresent: createIPCHandler<(pathString: string) => Promise>( + 'add-extension-if-no-extension-present', + ), + pathSep: createIPCHandler<() => Promise>('path-sep'), + getAllFilenamesInDirectory: createIPCHandler<(dirName: string) => Promise>('get-files-in-directory'), + getAllFilenamesInDirectoryRecursively: createIPCHandler<(dirName: string) => Promise>( + 'get-files-in-directory-recursive', ), - dirname: - createIPCHandler<(pathString: string) => Promise>("path-dirname"), - relative: - createIPCHandler<(from: string, to: string) => Promise>( - "path-relative" - ), - addExtensionIfNoExtensionPresent: createIPCHandler< - (pathString: string) => Promise - >("add-extension-if-no-extension-present"), - pathSep: createIPCHandler<() => Promise>("path-sep"), - getAllFilenamesInDirectory: createIPCHandler< - (dirName: string) => Promise - >("get-files-in-directory"), - getAllFilenamesInDirectoryRecursively: createIPCHandler< - (dirName: string) => Promise - >("get-files-in-directory-recursive"), -}; +} const llm = { - streamingLLMResponse: createIPCHandler< - ( - llmName: string, - llmConfig: LLMConfig, - isJSONMode: boolean, - chatHistory: ChatHistory - ) => Promise - >("streaming-llm-response"), - getLLMConfigs: - createIPCHandler<() => Promise>("get-llm-configs"), - pullOllamaModel: - createIPCHandler<(modelName: string) => Promise>("pull-ollama-model"), - addOrUpdateLLM: - createIPCHandler<(modelConfig: LLMConfig) => Promise>( - "add-or-update-llm" - ), - removeLLM: - createIPCHandler<(modelNameToDelete: string) => Promise>( - "remove-llm" - ), - setDefaultLLM: - createIPCHandler<(modelName: string) => Promise>("set-default-llm"), - getDefaultLLMName: createIPCHandler<() => Promise>( - "get-default-llm-name" + streamingLLMResponse: + createIPCHandler< + (llmName: string, llmConfig: LLMConfig, isJSONMode: boolean, chatHistory: ChatHistory) => Promise + >('streaming-llm-response'), + + getLLMConfigs: createIPCHandler<() => Promise>('get-llm-configs'), + pullOllamaModel: createIPCHandler<(modelName: string) => Promise>('pull-ollama-model'), + addOrUpdateLLM: createIPCHandler<(modelConfig: LLMConfig) => Promise>('add-or-update-llm'), + removeLLM: createIPCHandler<(modelNameToDelete: string) => Promise>('remove-llm'), + setDefaultLLM: createIPCHandler<(modelName: string) => Promise>('set-default-llm'), + getDefaultLLMName: createIPCHandler<() => Promise>('get-default-llm-name'), + sliceListOfStringsToContextLength: createIPCHandler<(strings: string[], llmName: string) => Promise>( + 'slice-list-of-strings-to-context-length', + ), + sliceStringToContextLength: createIPCHandler<(inputString: string, llmName: string) => Promise>( + 'slice-string-to-context-length', ), - sliceListOfStringsToContextLength: createIPCHandler< - (strings: string[], llmName: string) => Promise - >("slice-list-of-strings-to-context-length"), - sliceStringToContextLength: createIPCHandler< - (inputString: string, llmName: string) => Promise - >("slice-string-to-context-length"), -}; +} // Expose to renderer process -contextBridge.exposeInMainWorld("database", database); -contextBridge.exposeInMainWorld("electronUtils", electronUtils); -contextBridge.exposeInMainWorld("electronStore", electronStore); -contextBridge.exposeInMainWorld("fileSystem", fileSystem); -contextBridge.exposeInMainWorld("path", path); -contextBridge.exposeInMainWorld("llm", llm); +contextBridge.exposeInMainWorld('database', database) +contextBridge.exposeInMainWorld('electronUtils', electronUtils) +contextBridge.exposeInMainWorld('electronStore', electronStore) +contextBridge.exposeInMainWorld('fileSystem', fileSystem) +contextBridge.exposeInMainWorld('path', path) +contextBridge.exposeInMainWorld('llm', llm) // Additional exposures that don't fit the pattern above -contextBridge.exposeInMainWorld("ipcRenderer", { +contextBridge.exposeInMainWorld('ipcRenderer', { on: ipcRenderer.on.bind(ipcRenderer), receive: (channel: string, func: (...args: unknown[]) => void) => { - const subscription = ( - _event: Electron.IpcRendererEvent, - ...args: unknown[] - ) => func(...args); - ipcRenderer.on(channel, subscription); + const subscription = (_event: Electron.IpcRendererEvent, ...args: unknown[]) => func(...args) + ipcRenderer.on(channel, subscription) return () => { - ipcRenderer.removeListener(channel, subscription); - }; + ipcRenderer.removeListener(channel, subscription) + } }, -}); +}) // Type declarations declare global { interface Window { - database: typeof database; - electronUtils: typeof electronUtils; - electronStore: typeof electronStore; - fileSystem: typeof fileSystem; - path: typeof path; - llm: typeof llm; + database: typeof database + electronUtils: typeof electronUtils + electronStore: typeof electronStore + fileSystem: typeof fileSystem + path: typeof path + llm: typeof llm ipcRenderer: { - on: typeof ipcRenderer.on; + on: typeof ipcRenderer.on // eslint-disable-next-line @typescript-eslint/no-explicit-any - receive: (channel: string, func: (...args: any[]) => void) => () => void; - }; + receive: (channel: string, func: (...args: any[]) => void) => () => void + } } } diff --git a/package-lock.json b/package-lock.json index 0b2cd2c5..8900c034 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "reor-project", - "version": "0.2.13", + "version": "0.2.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reor-project", - "version": "0.2.13", + "version": "0.2.15", "license": "AGPL-3.0", "dependencies": { "@aarkue/tiptap-math-extension": "^1.2.2", @@ -22,14 +22,18 @@ "@mui/material": "^5.15.11", "@radix-ui/colors": "^3.0.0", "@tailwindcss/typography": "^0.5.10", + "@tiptap/core": "^2.5.0", "@tiptap/extension-bubble-menu": "^2.4.0", + "@tiptap/extension-document": "^2.5.0", "@tiptap/extension-link": "^2.2.4", + "@tiptap/extension-paragraph": "^2.5.0", "@tiptap/extension-table": "^2.4.0", "@tiptap/extension-table-cell": "^2.4.0", "@tiptap/extension-table-header": "^2.4.0", "@tiptap/extension-table-row": "^2.4.0", "@tiptap/extension-task-item": "^2.2.4", "@tiptap/extension-task-list": "^2.2.4", + "@tiptap/extension-text": "^2.5.0", "@tiptap/extension-text-style": "^2.4.0", "@tiptap/pm": "^2.2.4", "@tiptap/react": "^2.2.4", @@ -54,8 +58,10 @@ "openai": "^4.20.0", "posthog-js": "^1.130.2", "prosemirror-utils": "^1.2.2", + "react": "^18.2.0", "react-card-flip": "^1.2.2", "react-day-picker": "^8.10.1", + "react-dom": "^18.2.0", "react-icons": "^4.12.0", "react-markdown": "^9.0.1", "react-quill": "^2.0.0", @@ -84,23 +90,33 @@ "@types/remove-markdown": "^0.3.4", "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.4", - "@typescript-eslint/eslint-plugin": "^6.19.0", - "@typescript-eslint/parser": "^6.19.0", + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@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", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^18.0.0", + "eslint-config-airbnb-typescript-prettier": "^5.0.0", + "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-node": "^0.3.9", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-react": "^7.33.2", + "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.4", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-sonarjs": "^1.0.3", + "eslint-plugin-tailwindcss": "^3.17.4", + "eslint-plugin-unicorn": "^54.0.0", "eslint-plugin-unused-imports": "^4.0.0", + "husky": "^9.0.11", "jest": "^29.7.0", "postcss": "^8.4.31", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "prettier": "^3.3.3", "sass": "^1.71.1", "tailwindcss": "^3.3.5", "tmp": "^0.2.1", @@ -414,8 +430,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "license": "MIT", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "engines": { "node": ">=6.9.0" } @@ -3187,6 +3204,18 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@playwright/test": { "version": "1.41.2", "dev": true, @@ -3341,14 +3370,15 @@ } }, "node_modules/@tiptap/core": { - "version": "2.2.4", - "license": "MIT", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.5.0.tgz", + "integrity": "sha512-BEjzVhkyD2LNxiKjEBBbIEpaGE+2I4gclqEIJ4BxrWW+0BgOlYQvqoDn2PF+dAAyXGAxrlwb/+G27tDqRwE+ZQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^2.0.0" + "@tiptap/pm": "^2.5.0" } }, "node_modules/@tiptap/extension-blockquote": { @@ -3424,14 +3454,15 @@ } }, "node_modules/@tiptap/extension-document": { - "version": "2.2.4", - "license": "MIT", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.5.0.tgz", + "integrity": "sha512-deaJhDE2NGvhK5GezWIB45HAnbDdKBNbCFIjtrAjjX9CraPt5CSnGSBJJhdddKWo9tuami8Nh18JhKtgiaf6kA==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.0.0" + "@tiptap/core": "^2.5.0" } }, "node_modules/@tiptap/extension-dropcursor": { @@ -3568,14 +3599,15 @@ } }, "node_modules/@tiptap/extension-paragraph": { - "version": "2.2.4", - "license": "MIT", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.5.0.tgz", + "integrity": "sha512-UgTx+nm3XgAL5/1ruHtjKn4cPwEcGTEo1ePyvBEn0xzy3RbveW9/r/yc+7jqKvT1B6OYc87dhKbgfqzFfXDrQQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.0.0" + "@tiptap/core": "^2.5.0" } }, "node_modules/@tiptap/extension-strike": { @@ -3662,14 +3694,15 @@ } }, "node_modules/@tiptap/extension-text": { - "version": "2.2.4", - "license": "MIT", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.5.0.tgz", + "integrity": "sha512-aMY3mhHOk+ihMu4rfn7VVs8MI6uLsCPEUHVJdDlraWkpjiU98uNADKsIc0uYXX7KHSqJWphTiRiC1m+ucgPnYQ==", "funding": { "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.0.0" + "@tiptap/core": "^2.5.0" } }, "node_modules/@tiptap/extension-text-style": { @@ -3901,8 +3934,9 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/json5": { "version": "0.0.29", @@ -3960,6 +3994,12 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, "node_modules/@types/pad-left": { "version": "2.1.1", "license": "MIT" @@ -4042,9 +4082,10 @@ "license": "MIT" }, "node_modules/@types/semver": { - "version": "7.5.7", - "dev": true, - "license": "MIT" + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true }, "node_modules/@types/stack-utils": { "version": "2.0.3", @@ -4101,32 +4142,31 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", + "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", "dev": true, - "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/type-utils": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -4135,25 +4175,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", + "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -4162,15 +4203,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", + "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4178,24 +4220,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", + "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/utils": "7.16.0", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -4204,11 +4247,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", + "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", "dev": true, - "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4216,21 +4260,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", + "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4243,39 +4288,38 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", + "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", + "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", "dev": true, - "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.16.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4340,9 +4384,10 @@ } }, "node_modules/acorn": { - "version": "8.11.3", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -4645,6 +4690,47 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/aria-query/node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-back": { "version": "3.1.0", "license": "MIT", @@ -4668,14 +4754,16 @@ } }, "node_modules/array-includes": { - "version": "3.1.7", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -4687,12 +4775,33 @@ }, "node_modules/array-union": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.findlastindex": { "version": "1.2.5", "dev": true, @@ -4746,16 +4855,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.toreversed": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, "node_modules/array.prototype.tosorted": { - "version": "1.1.3", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.1.0", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { @@ -4787,6 +4912,12 @@ "node": ">=0.8" } }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true + }, "node_modules/astral-regex": { "version": "2.0.0", "license": "MIT", @@ -4808,14 +4939,6 @@ "node": ">=0.12.0" } }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - } - }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" @@ -4885,6 +5008,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axe-core": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz", + "integrity": "sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { "version": "1.6.7", "license": "MIT", @@ -4894,6 +5026,47 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axobject-query": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", + "integrity": "sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==", + "dev": true, + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/axobject-query/node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/b4a": { "version": "1.6.6", "license": "Apache-2.0" @@ -5399,6 +5572,18 @@ "node": ">= 10.0.0" } }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "dev": true, @@ -5686,6 +5871,27 @@ "version": "2.3.2", "license": "MIT" }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/cli-truncate": { "version": "2.1.0", "license": "MIT", @@ -5960,10 +6166,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, "node_modules/convert-source-map": { "version": "1.9.0", "license": "MIT" }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "license": "MIT", @@ -6136,6 +6361,12 @@ "version": "3.1.3", "license": "MIT" }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, "node_modules/data-view-buffer": { "version": "1.0.1", "dev": true, @@ -6447,8 +6678,9 @@ }, "node_modules/dir-glob": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -7060,26 +7292,46 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-iterator-helpers": { - "version": "1.0.17", + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", "dev": true, - "license": "MIT", "dependencies": { - "asynciterator.prototype": "^1.0.0", "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.4", + "es-abstract": "^1.23.3", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.2", + "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.1", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", "internal-slot": "^1.0.7", "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.1.0" + "safe-array-concat": "^1.1.2" }, "engines": { "node": ">= 0.4" @@ -7247,55 +7499,401 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-import-resolver-alias": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", - "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", + "node_modules/eslint-config-airbnb": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz", + "integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==", "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^15.0.0", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5" + }, "engines": { - "node": ">= 4" + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" }, "peerDependencies": { - "eslint-plugin-import": ">=1.4.0" + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-react": "^7.28.0", + "eslint-plugin-react-hooks": "^4.3.0" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", "dev": true, "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-airbnb-typescript": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-18.0.0.tgz", + "integrity": "sha512-oc+Lxzgzsu8FQyFVa4QFaVKiitTYiiW3frB9KYW5OWdPrqFc7FzxgB20hP4cHMlr+MBzGcLl3jnCOVOydL9mIg==", "dev": true, - "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "eslint-config-airbnb-base": "^15.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" } }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", - "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "node_modules/eslint-config-airbnb-typescript-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript-prettier/-/eslint-config-airbnb-typescript-prettier-5.0.0.tgz", + "integrity": "sha512-SVphutDwxEJedWKHF+q6FDC4+aKaOn5R8hOBxCpfWnn5qCYAChngPf86Svz78bHgMgbZfohwHPbQeETTPUN9Wg==", "dev": true, "dependencies": { - "debug": "^4.3.4", - "enhanced-resolve": "^5.12.0", - "eslint-module-utils": "^2.7.4", - "fast-glob": "^3.3.1", - "get-tsconfig": "^4.5.0", - "is-core-module": "^2.11.0", - "is-glob": "^4.0.3" + "@typescript-eslint/eslint-plugin": "^5.6.0", + "@typescript-eslint/parser": "^5.6.0", + "eslint-config-airbnb": "^19.0.2", + "eslint-config-prettier": "^6.15.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0" }, - "funding": { + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "prettier": "^1.18.2 || ^2.0.0", + "typescript": ">=3.3.1" + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/eslint-plugin-prettier": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", + "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">=5.0.0", + "prettier": ">=1.13.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-config-airbnb-typescript-prettier/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-alias": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz", + "integrity": "sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==", + "dev": true, + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "eslint-plugin-import": ">=1.4.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz", + "integrity": "sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "fast-glob": "^3.3.1", + "get-tsconfig": "^4.5.0", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" }, "peerDependencies": { @@ -7329,8 +7927,9 @@ }, "node_modules/eslint-plugin-import": { "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -7404,27 +8003,119 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz", + "integrity": "sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g==", + "dev": true, + "dependencies": { + "aria-query": "~5.1.3", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.9.1", + "axobject-query": "~3.1.1", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "es-iterator-helpers": "^1.0.19", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.0" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", + "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { - "version": "7.33.2", + "version": "7.34.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.4.tgz", + "integrity": "sha512-Np+jo9bUwJNxCsT12pXtrGhJgT3T44T1sHhn1Ssr42XFn8TES0267wPGo5nNrMHi8qkyimDAX2BUmkf9pSaVzA==", "dev": true, - "license": "MIT", "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" @@ -7433,6 +8124,18 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { "version": "1.1.11", "dev": true, @@ -7488,10 +8191,185 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-sonarjs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-1.0.3.tgz", + "integrity": "sha512-6s41HLPYPyDrp+5+7Db5yFYbod6h9pC7yx+xfcNwHRcLe1EZwbbQT/tdOAkR7ekVUkNGEvN3GmYakIoQUX7dEg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-tailwindcss": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.17.4.tgz", + "integrity": "sha512-gJAEHmCq2XFfUP/+vwEfEJ9igrPeZFg+skeMtsxquSQdxba9XRk5bn0Bp9jxG1VV9/wwPKi1g3ZjItu6MIjhNg==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.5", + "postcss": "^8.4.4" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "tailwindcss": "^3.4.0" + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "54.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", + "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "@eslint-community/eslint-utils": "^4.4.0", + "@eslint/eslintrc": "^3.0.2", + "ci-info": "^4.0.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.37.0", + "esquery": "^1.5.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.1", + "jsesc": "^3.0.2", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.10.0", + "semver": "^7.6.1", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=18.18" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.56.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint-plugin-unused-imports": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.0.0.tgz", + "integrity": "sha512-mzM+y2B7XYpQryVa1usT+Y/BdNAtAZiXzwpSyDCboFoJN/LZRN67TNvQxKtuTK/Aplya3sLNQforiubzPPaIcQ==", "dev": true, - "license": "MIT", "dependencies": { "eslint-rule-composer": "^0.3.0" }, @@ -8285,6 +9163,15 @@ "node": ">=8.0.0" } }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "dev": true, @@ -8421,8 +9308,9 @@ }, "node_modules/globby": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -8809,6 +9697,21 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "license": "MIT", @@ -8906,6 +9809,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "dev": true, @@ -9006,8 +9918,9 @@ }, "node_modules/is-async-function": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", "dev": true, - "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -9058,6 +9971,21 @@ "version": "1.1.6", "license": "MIT" }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "dev": true, @@ -9134,8 +10062,9 @@ }, "node_modules/is-finalizationregistry": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", "dev": true, - "license": "MIT", "dependencies": { "call-bind": "^1.0.2" }, @@ -9161,8 +10090,9 @@ }, "node_modules/is-generator-function": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "dev": true, - "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -9192,9 +10122,13 @@ } }, "node_modules/is-map": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9271,9 +10205,13 @@ } }, "node_modules/is-set": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, - "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9346,9 +10284,13 @@ } }, "node_modules/is-weakmap": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, - "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9365,12 +10307,16 @@ } }, "node_modules/is-weakset": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9487,8 +10433,9 @@ }, "node_modules/iterator.prototype": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", "dev": true, - "license": "MIT", "dependencies": { "define-properties": "^1.2.1", "get-intrinsic": "^1.2.1", @@ -11533,6 +12480,24 @@ "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", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/lazy-val": { "version": "1.0.5", "license": "MIT" @@ -12404,10 +13369,20 @@ "node": ">=4" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { - "version": "9.0.3", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -12534,6 +13509,12 @@ "dev": true, "license": "MIT" }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "node_modules/node-abi": { "version": "3.54.0", "license": "MIT", @@ -12594,6 +13575,33 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "license": "MIT", @@ -15237,26 +16245,29 @@ } }, "node_modules/object.entries": { - "version": "1.1.7", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.7", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -15274,30 +16285,19 @@ "define-properties": "^1.2.1", "es-abstract": "^1.23.2" }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.hasown": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, "node_modules/object.values": { - "version": "1.1.7", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -15853,6 +16853,15 @@ "node": ">=10.4.0" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/popmotion": { "version": "11.0.3", "license": "MIT", @@ -16155,6 +17164,33 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "dev": true, @@ -16793,6 +17829,108 @@ "node": ">=10" } }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "license": "MIT", @@ -16816,15 +17954,16 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.5", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.0.0", - "get-intrinsic": "^1.2.3", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", "which-builtin-type": "^1.1.3" }, @@ -16839,6 +17978,15 @@ "version": "0.14.1", "license": "MIT" }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "license": "MIT", @@ -16855,6 +18003,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regjsparser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", + "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/rehype-raw": { "version": "7.0.0", "license": "MIT", @@ -17177,11 +18346,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -17195,20 +18362,6 @@ "license": "MIT", "optional": true }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "license": "ISC" - }, "node_modules/serialize-error": { "version": "7.0.1", "dev": true, @@ -17252,12 +18405,14 @@ } }, "node_modules/set-function-name": { - "version": "2.0.1", - "license": "MIT", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -17308,11 +18463,12 @@ } }, "node_modules/side-channel": { - "version": "1.0.5", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -17497,6 +18653,38 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.1.3", "dev": true, @@ -17530,6 +18718,18 @@ "node": ">= 6" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", + "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, + "dependencies": { + "internal-slot": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-read-all": { "version": "3.0.1", "license": "MIT", @@ -17594,25 +18794,52 @@ "node": ">=8" } }, + "node_modules/string.prototype.includes": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz", + "integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.matchall": { - "version": "4.0.10", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "dev": true, @@ -17710,6 +18937,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "dev": true, @@ -17821,6 +19060,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tabbable": { "version": "6.2.0", "license": "MIT" @@ -18178,9 +19433,10 @@ } }, "node_modules/ts-api-utils": { - "version": "1.2.1", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=16" }, @@ -18269,6 +19525,27 @@ "version": "2.6.2", "license": "0BSD" }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/tunnel-agent": { "version": "0.6.0", "license": "Apache-2.0", @@ -18656,6 +19933,16 @@ "dev": true, "license": "MIT" }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/vectordb": { "version": "0.4.10", "cpu": [ @@ -18906,8 +20193,9 @@ }, "node_modules/which-builtin-type": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", "dev": true, - "license": "MIT", "dependencies": { "function.prototype.name": "^1.1.5", "has-tostringtag": "^1.0.0", @@ -18930,14 +20218,18 @@ } }, "node_modules/which-collection": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, - "license": "MIT", "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" diff --git a/package.json b/package.json index ca38d410..f4f2b03c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reor-project", - "version": "0.2.13", + "version": "0.2.15", "productName": "Reor", "main": "dist-electron/main/index.js", "description": "An AI note-taking app that runs models locally.", @@ -23,7 +23,9 @@ "e2e": "playwright test", "test": "jest", "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "eslint . --ext .ts,.tsx --fix" + "lint:fix": "eslint . --ext .ts,.tsx --fix", + "prepare": "husky", + "type-check": "tsc && vite build" }, "dependencies": { "@aarkue/tiptap-math-extension": "^1.2.2", @@ -39,14 +41,18 @@ "@mui/material": "^5.15.11", "@radix-ui/colors": "^3.0.0", "@tailwindcss/typography": "^0.5.10", + "@tiptap/core": "^2.5.0", "@tiptap/extension-bubble-menu": "^2.4.0", + "@tiptap/extension-document": "^2.5.0", "@tiptap/extension-link": "^2.2.4", + "@tiptap/extension-paragraph": "^2.5.0", "@tiptap/extension-table": "^2.4.0", "@tiptap/extension-table-cell": "^2.4.0", "@tiptap/extension-table-header": "^2.4.0", "@tiptap/extension-table-row": "^2.4.0", "@tiptap/extension-task-item": "^2.2.4", "@tiptap/extension-task-list": "^2.2.4", + "@tiptap/extension-text": "^2.5.0", "@tiptap/extension-text-style": "^2.4.0", "@tiptap/pm": "^2.2.4", "@tiptap/react": "^2.2.4", @@ -57,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", @@ -71,8 +77,10 @@ "openai": "^4.20.0", "posthog-js": "^1.130.2", "prosemirror-utils": "^1.2.2", + "react": "^18.2.0", "react-card-flip": "^1.2.2", "react-day-picker": "^8.10.1", + "react-dom": "^18.2.0", "react-icons": "^4.12.0", "react-markdown": "^9.0.1", "react-quill": "^2.0.0", @@ -101,23 +109,33 @@ "@types/remove-markdown": "^0.3.4", "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.4", - "@typescript-eslint/eslint-plugin": "^6.19.0", - "@typescript-eslint/parser": "^6.19.0", + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@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", + "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-node": "^0.3.9", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-react": "^7.33.2", + "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.4", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-sonarjs": "^1.0.3", + "eslint-plugin-tailwindcss": "^3.17.4", + "eslint-plugin-unicorn": "^54.0.0", "eslint-plugin-unused-imports": "^4.0.0", + "husky": "^9.0.11", "jest": "^29.7.0", "postcss": "^8.4.31", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "prettier": "^3.3.3", "sass": "^1.71.1", "tailwindcss": "^3.3.5", "tmp": "^0.2.1", diff --git a/scripts/downloadOllama.js b/scripts/downloadOllama.js index 476ac995..0df69049 100644 --- a/scripts/downloadOllama.js +++ b/scripts/downloadOllama.js @@ -32,7 +32,7 @@ function ensureDirectoryExistence(filePath) { function setExecutable(filePath) { fs.chmod(filePath, 0o755, (err) => { if (err) throw err; - console.log(`Set ${filePath} as executable.`); + ; }); } @@ -43,14 +43,14 @@ function downloadIfMissing(platformKey) { fs.access(filePath, fs.constants.F_OK, (err) => { if (err) { - console.log(`Downloading ${platformKey} Ollama binary...`); + ; const request = https.get(info.url, (response) => { if (response.statusCode === 200) { const file = fs.createWriteStream(filePath); response.pipe(file); file.on("finish", () => { file.close(() => { - console.log(`Downloaded ${platformKey} Ollama binary.`); + ; // Set as executable if not on Windows if (platformKey !== "win32") { setExecutable(filePath); @@ -59,7 +59,7 @@ function downloadIfMissing(platformKey) { }); } else if (response.statusCode === 302 || response.statusCode === 301) { // Handle redirection (if any) - console.log(`Redirection to ${response.headers.location}`); + ; binariesInfo[platformKey].url = response.headers.location; downloadIfMissing(platformKey); // Retry with the new URL } else { @@ -74,7 +74,7 @@ function downloadIfMissing(platformKey) { ); }); } else { - console.log(`${platformKey} Ollama binary already exists.`); + ; // Ensure it's executable if it already exists and not on Windows if (platformKey !== "win32") { setExecutable(filePath); diff --git a/scripts/notarize.js b/scripts/notarize.js index 1cde5417..85638272 100644 --- a/scripts/notarize.js +++ b/scripts/notarize.js @@ -70,4 +70,4 @@ async function notarizeApp() { notarizeApp().catch((error) => { console.error(error); process.exit(1); -}); +}); \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 0aff7db6..4ff63f5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,101 +1,91 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState } from 'react' -import { Portal } from "@headlessui/react"; -import posthog from "posthog-js"; -import { ToastContainer, toast } from "react-toastify"; +import { Portal } from '@headlessui/react' +import posthog from 'posthog-js' +import { ToastContainer, toast } from 'react-toastify' + +import 'react-toastify/dist/ReactToastify.css' +import IndexingProgress from './components/Common/IndexingProgress' +import MainPageComponent from './components/MainPage' +import InitialSetupSinglePage from './components/Settings/InitialSettingsSinglePage' -import "react-toastify/dist/ReactToastify.css"; -import IndexingProgress from "./components/Common/IndexingProgress"; -import FileEditorContainer from "./components/MainPage"; -import InitialSetupSinglePage from "./components/Settings/InitialSettingsSinglePage"; interface AppProps {} const App: React.FC = () => { - const [ - userHasConfiguredSettingsForIndexing, - setUserHasConfiguredSettingsForIndexing, - ] = useState(false); + const [userHasConfiguredSettingsForIndexing, setUserHasConfiguredSettingsForIndexing] = useState(false) - const [indexingProgress, setIndexingProgress] = useState(0); + const [indexingProgress, setIndexingProgress] = useState(0) useEffect(() => { const handleProgressUpdate = (newProgress: number) => { - setIndexingProgress(newProgress); - }; - window.ipcRenderer.receive("indexing-progress", handleProgressUpdate); - }, []); + setIndexingProgress(newProgress) + } + window.ipcRenderer.receive('indexing-progress', handleProgressUpdate) + }, []) useEffect(() => { const initialisePosthog = async () => { if (await window.electronStore.getAnalyticsMode()) { - posthog.init("phc_xi4hFToX1cZU657yzge1VW0XImaaRzuvnFUdbAKI8fu", { - api_host: "https://us.i.posthog.com", + posthog.init('phc_xi4hFToX1cZU657yzge1VW0XImaaRzuvnFUdbAKI8fu', { + api_host: 'https://us.i.posthog.com', autocapture: false, - }); + }) posthog.register({ reorAppVersion: await window.electronUtils.getReorAppVersion(), - }); + }) } - }; - initialisePosthog(); - }, []); + } + initialisePosthog() + }, []) useEffect(() => { const handleIndexingError = (error: string) => { - console.log("Indexing error:", error); toast.error(error, { - className: "mt-5", + className: 'mt-5', autoClose: false, closeOnClick: false, draggable: false, - }); - setIndexingProgress(1); - }; - window.ipcRenderer.receive( - "error-to-display-in-window", - handleIndexingError - ); - }, []); + }) + setIndexingProgress(1) + } + window.ipcRenderer.receive('error-to-display-in-window', handleIndexingError) + }, []) useEffect(() => { const fetchSettings = async () => { const [initialDirectory, defaultEmbedFunc] = await Promise.all([ window.electronStore.getVaultDirectoryForWindow(), window.electronStore.getDefaultEmbeddingModel(), - ]); + ]) if (initialDirectory && defaultEmbedFunc) { - setUserHasConfiguredSettingsForIndexing(true); - window.database.indexFilesInDirectory(); + setUserHasConfiguredSettingsForIndexing(true) + window.database.indexFilesInDirectory() } - }; + } - fetchSettings(); - }, []); + fetchSettings() + }, []) const handleAllInitialSettingsAreReady = () => { - setUserHasConfiguredSettingsForIndexing(true); - window.database.indexFilesInDirectory(); - }; + setUserHasConfiguredSettingsForIndexing(true) + window.database.indexFilesInDirectory() + } return ( -
    +
    - {userHasConfiguredSettingsForIndexing ? ( - indexingProgress < 1 ? ( - - ) : ( - - ) - ) : ( - + {!userHasConfiguredSettingsForIndexing && ( + + )} + {userHasConfiguredSettingsForIndexing && indexingProgress < 1 && ( + )} + {userHasConfiguredSettingsForIndexing && indexingProgress >= 1 && }
    - ); -}; + ) +} -export default App; +export default App diff --git a/src/components/Chat/AddContextFiltersModal.tsx b/src/components/Chat/AddContextFiltersModal.tsx index aa668230..379d1f71 100644 --- a/src/components/Chat/AddContextFiltersModal.tsx +++ b/src/components/Chat/AddContextFiltersModal.tsx @@ -1,156 +1,135 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect } from 'react' -import { List, ListItem } from "@material-tailwind/react"; -import FolderIcon from "@mui/icons-material/Folder"; -import { ListItemIcon, ListItemText } from "@mui/material"; -import Slider from "@mui/material/Slider"; -import { sub } from "date-fns"; -import { DayPicker } from "react-day-picker"; +import { List, ListItem } from '@material-tailwind/react' +import FolderIcon from '@mui/icons-material/Folder' +import { ListItemIcon, ListItemText } from '@mui/material' +import Slider from '@mui/material/Slider' +import { sub } from 'date-fns' +import { DayPicker } from 'react-day-picker' -import "react-day-picker/dist/style.css"; -import ReorModal from "../Common/Modal"; -import { SearchBarWithFilesSuggestion } from "../Common/SearchBarWithFilesSuggestion"; -import CustomSelect from "../Common/Select"; -import { SuggestionsState } from "../Editor/BacklinkSuggestionsDisplay"; - -import { ChatFilters } from "./Chat"; +import 'react-day-picker/dist/style.css' +import ReorModal from '../Common/Modal' +import SearchBarWithFilesSuggestion from '../Common/SearchBarWithFilesSuggestion' +import CustomSelect from '../Common/Select' +import { SuggestionsState } from '../Editor/BacklinkSuggestionsDisplay' +import { ChatFilters } from './chatUtils' interface Props { - isOpen: boolean; - onClose: () => void; - vaultDirectory: string; - setChatFilters: (chatFilters: ChatFilters) => void; - chatFilters: ChatFilters; + isOpen: boolean + onClose: () => void + vaultDirectory: string + setChatFilters: (chatFilters: ChatFilters) => void + chatFilters: ChatFilters } -const AddContextFiltersModal: React.FC = ({ - vaultDirectory, - isOpen, - onClose, - chatFilters, - setChatFilters, -}) => { - const [internalFilesSelected, setInternalFilesSelected] = useState( - chatFilters?.files || [] - ); - const [searchText, setSearchText] = useState(""); - const [suggestionsState, setSuggestionsState] = - useState(null); - const [numberOfChunksToFetch, setNumberOfChunksToFetch] = useState( - chatFilters.numberOfChunksToFetch || 15 - ); - const [minDate, setMinDate] = useState(chatFilters.minDate); - const [maxDate, setMaxDate] = useState(chatFilters.maxDate); - const [showAdvanced, setShowAdvanced] = useState(false); - const [selectedDateRange, setSelectedDateRange] = useState("Anytime"); +const AddContextFiltersModal: React.FC = ({ vaultDirectory, isOpen, onClose, chatFilters, setChatFilters }) => { + const [internalFilesSelected, setInternalFilesSelected] = useState(chatFilters?.files || []) + const [searchText, setSearchText] = useState('') + const [suggestionsState, setSuggestionsState] = useState(null) + const [numberOfChunksToFetch, setNumberOfChunksToFetch] = useState(chatFilters.numberOfChunksToFetch || 15) + const [minDate, setMinDate] = useState(chatFilters.minDate) + const [maxDate, setMaxDate] = useState(chatFilters.maxDate) + const [showAdvanced, setShowAdvanced] = useState(false) + const [selectedDateRange, setSelectedDateRange] = useState('Anytime') const dateRangeOptions = [ - { label: "Anytime", value: "anytime" }, - { label: "Past hour", value: "lastHour" }, - { label: "Past 24 hours", value: "lastDay" }, - { label: "Past week", value: "lastWeek" }, - { label: "Past month", value: "lastMonth" }, - { label: "Past year", value: "lastYear" }, - ]; + { label: 'Anytime', value: 'anytime' }, + { label: 'Past hour', value: 'lastHour' }, + { label: 'Past 24 hours', value: 'lastDay' }, + { label: 'Past week', value: 'lastWeek' }, + { label: 'Past month', value: 'lastMonth' }, + { label: 'Past year', value: 'lastYear' }, + ] useEffect(() => { const updatedChatFilters: ChatFilters = { ...chatFilters, files: [...new Set([...chatFilters.files, ...internalFilesSelected])], - numberOfChunksToFetch: numberOfChunksToFetch, - minDate: minDate ? minDate : undefined, - maxDate: maxDate ? maxDate : undefined, - }; - setChatFilters(updatedChatFilters); - }, [internalFilesSelected, numberOfChunksToFetch, minDate, maxDate]); + numberOfChunksToFetch, + minDate: minDate || undefined, + maxDate: maxDate || undefined, + } + setChatFilters(updatedChatFilters) + }, [internalFilesSelected, numberOfChunksToFetch, minDate, maxDate, chatFilters, setChatFilters]) - const handleNumberOfChunksChange = ( - event: Event, - value: number | number[] - ) => { - const newValue = Array.isArray(value) ? value[0] : value; - setNumberOfChunksToFetch(newValue); - }; + const handleNumberOfChunksChange = (event: Event, value: number | number[]) => { + const newValue = Array.isArray(value) ? value[0] : value + setNumberOfChunksToFetch(newValue) + } const handleDateRangeChange = (value: string) => { - const now = new Date(); - let newMinDate: Date | undefined; + const now = new Date() + let newMinDate: Date | undefined switch (value) { - case "anytime": - newMinDate = undefined; - break; - case "lastHour": - newMinDate = sub(now, { hours: 1 }); - break; - case "lastDay": - newMinDate = sub(now, { days: 1 }); - break; - case "lastWeek": - newMinDate = sub(now, { weeks: 1 }); - break; - case "lastMonth": - newMinDate = sub(now, { months: 1 }); - break; - case "lastYear": - newMinDate = sub(now, { years: 1 }); - break; + case 'anytime': + newMinDate = undefined + break + case 'lastHour': + newMinDate = sub(now, { hours: 1 }) + break + case 'lastDay': + newMinDate = sub(now, { days: 1 }) + break + case 'lastWeek': + newMinDate = sub(now, { weeks: 1 }) + break + case 'lastMonth': + newMinDate = sub(now, { months: 1 }) + break + case 'lastYear': + newMinDate = sub(now, { years: 1 }) + break default: - newMinDate = undefined; + newMinDate = undefined } - setMinDate(newMinDate); - setMaxDate(value === "anytime" ? undefined : now); - setSelectedDateRange( - dateRangeOptions.find((option) => option.value === value)?.label || "" - ); - }; + setMinDate(newMinDate) + setMaxDate(value === 'anytime' ? undefined : now) + setSelectedDateRange(dateRangeOptions.find((option) => option.value === value)?.label || '') + } const handleAdvancedToggle = () => { - setShowAdvanced(!showAdvanced); - }; + setShowAdvanced(!showAdvanced) + } // Define the marks to be closer together const marks = Array.from({ length: 31 }, (_, i) => ({ value: i, - label: i % 5 === 0 ? i.toString() : "", // Show label every 5 steps - })); + label: i % 5 === 0 ? i.toString() : '', // Show label every 5 steps + })) - useEffect(() => { - console.log("chatFilters updated:", chatFilters); - }, [chatFilters]); + useEffect(() => {}, [chatFilters]) return ( -
    -

    +
    +

    Choose specific context files or customise the RAG search

    {/* Left side: File selection */}
    -

    - Select files for context -

    +

    Select files for context

    { if (file && !internalFilesSelected.includes(file)) { - setInternalFilesSelected([...internalFilesSelected, file]); + setInternalFilesSelected([...internalFilesSelected, file]) } - setSuggestionsState(null); + setSuggestionsState(null) }} suggestionsState={suggestionsState} setSuggestionsState={setSuggestionsState} /> -
    +
    - {internalFilesSelected.map((fileItem, index) => ( - + {internalFilesSelected.map((filePath) => ( + - + ))} @@ -158,12 +137,12 @@ const AddContextFiltersModal: React.FC = ({
    {/* Vertical divider */} -
    -
    -
    +
    +
    +
    Or
    -
    +
    {/* Right side: Context settings */}
    = ({ ? "opacity-30 pointer-events-none" : "" } */} -

    Context settings

    -
    +

    Context settings

    +

    Number of notes to add to context:

    -
    +
    = ({ max={30} onChange={handleNumberOfChunksChange} sx={{ - "& .MuiSlider-thumb": { - "&:focus, &:hover, &.Mui-active, &.Mui-focusVisible": { - boxShadow: "none", + '& .MuiSlider-thumb': { + '&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': { + boxShadow: 'none', }, - "&::after": { - content: "none", + '&::after': { + content: 'none', }, }, - "& .MuiSlider-valueLabel": { - fontSize: "0.75rem", - padding: "3px 6px", - lineHeight: "1.2em", + '& .MuiSlider-valueLabel': { + fontSize: '0.75rem', + padding: '3px 6px', + lineHeight: '1.2em', }, - "& .MuiSlider-markLabel": { - color: "#FFFFFF", + '& .MuiSlider-markLabel': { + color: '#FFFFFF', }, - "& .MuiSlider-mark": { - color: "#FFFFFF", + '& .MuiSlider-mark': { + color: '#FFFFFF', }, }} />
    -
    {numberOfChunksToFetch}
    +
    {numberOfChunksToFetch}
    -
    +

    Filter context notes by last modified date:

    -
    +
    = ({
    -
    - {showAdvanced ? "Hide Advanced" : "Show Advanced"} +
    + {showAdvanced ? 'Hide Advanced' : 'Show Advanced'}
    {showAdvanced && ( -
    -
    +
    +

    Min Date:

    = ({ className="my-day-picker w-full" />
    -
    +

    Max Date:

    = ({
    - ); -}; + ) +} -export default AddContextFiltersModal; +export default AddContextFiltersModal diff --git a/src/components/Chat/Chat-Prompts.tsx b/src/components/Chat/Chat-Prompts.tsx index 59ecb753..c8a1d350 100644 --- a/src/components/Chat/Chat-Prompts.tsx +++ b/src/components/Chat/Chat-Prompts.tsx @@ -1,22 +1,23 @@ -import React from "react"; -import type { FC } from "react"; +import React from 'react' +import type { FC } from 'react' interface Props { - promptText: string; - onClick?: () => void; + promptText: string + onClick?: () => void } -export const PromptSuggestion: FC = ({ promptText, onClick }: Props) => { - return ( - - ); -}; +const PromptSuggestion: FC = ({ promptText, onClick }: Props) => ( + +) + +export default PromptSuggestion diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index ed260477..c5192284 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -1,30 +1,30 @@ -import React, { useEffect, useState } from "react"; - -import { MessageStreamEvent } from "@anthropic-ai/sdk/resources"; -import { DBEntry, DBQueryResult } from "electron/main/vector-database/schema"; -import { - ChatCompletionChunk, - ChatCompletionMessageParam, -} from "openai/resources/chat/completions"; -import posthog from "posthog-js"; -import ReactMarkdown from "react-markdown"; -import rehypeRaw from "rehype-raw"; - -import { SimilarEntriesComponent } from "../Sidebars/SimilarFilesSidebar"; - -import AddContextFiltersModal from "./AddContextFiltersModal"; -import { PromptSuggestion } from "./Chat-Prompts"; -import ChatInput from "./ChatInput"; +import React, { useCallback, useEffect, useState } from 'react' + +import { MessageStreamEvent } from '@anthropic-ai/sdk/resources' +import { DBQueryResult } from 'electron/main/vector-database/schema' +import { ChatCompletionChunk } from 'openai/resources/chat/completions' +import posthog from 'posthog-js' +import ReactMarkdown from 'react-markdown' +import rehypeRaw from 'rehype-raw' + +import AddContextFiltersModal from './AddContextFiltersModal' +import PromptSuggestion from './Chat-Prompts' +import ChatInput from './ChatInput' import { + ChatFilters, + ChatHistory, + ChatMessageToDisplay, formatOpenAIMessageContentIntoString, + getChatHistoryContext, resolveRAGContext, -} from "./chatUtils"; +} from './chatUtils' -import { errorToStringRendererProcess } from "@/utils/error"; +import errorToStringRendererProcess from '@/utils/error' +import SimilarEntriesComponent from '../Sidebars/SemanticSidebar/SimilarEntriesComponent' // convert ask options to enum enum AskOptions { - Ask = "Ask", + Ask = 'Ask', // AskFile = "Ask File", // TemporalAsk = "Temporal Ask", // FlashcardAsk = "Flashcard Ask", @@ -47,55 +47,34 @@ const EXAMPLE_PROMPTS: { [key: string]: string[] } = { // [AskOptions.FlashcardAsk]: [ // "Create some flashcards based on the current note", // ], -}; - -export type ChatHistory = { - id: string; - displayableChatHistory: ChatMessageToDisplay[]; -}; -export type ChatMessageToDisplay = ChatCompletionMessageParam & { - messageType: "success" | "error"; - context: DBEntry[]; - visibleContent?: string; -}; - -export interface ChatFilters { - numberOfChunksToFetch: number; - files: string[]; - minDate?: Date; - maxDate?: Date; } interface AnonymizedChatFilters { - numberOfChunksToFetch: number; - filesLength: number; - minDate?: Date; - maxDate?: Date; + numberOfChunksToFetch: number + filesLength: number + minDate?: Date + maxDate?: Date } -function anonymizeChatFiltersForPosthog( - chatFilters: ChatFilters -): AnonymizedChatFilters { - const { numberOfChunksToFetch, files, minDate, maxDate } = chatFilters; +function anonymizeChatFiltersForPosthog(chatFilters: ChatFilters): AnonymizedChatFilters { + const { numberOfChunksToFetch, files, minDate, maxDate } = chatFilters return { numberOfChunksToFetch, filesLength: files.length, minDate, maxDate, - }; + } } interface ChatWithLLMProps { - vaultDirectory: string; - openFileByPath: (path: string) => Promise; - - currentChatHistory: ChatHistory | undefined; - setCurrentChatHistory: React.Dispatch< - React.SetStateAction - >; - showSimilarFiles: boolean; - chatFilters: ChatFilters; - setChatFilters: React.Dispatch; + vaultDirectory: string + openFileByPath: (path: string) => Promise + + currentChatHistory: ChatHistory | undefined + setCurrentChatHistory: React.Dispatch> + showSimilarFiles: boolean + chatFilters: ChatFilters + setChatFilters: React.Dispatch } const ChatWithLLM: React.FC = ({ @@ -107,64 +86,99 @@ const ChatWithLLM: React.FC = ({ chatFilters, setChatFilters, }) => { - const [userTextFieldInput, setUserTextFieldInput] = useState(""); - const [askText] = useState(AskOptions.Ask); - const [loadingResponse, setLoadingResponse] = useState(false); - const [readyToSave, setReadyToSave] = useState(false); - const [currentContext, setCurrentContext] = useState([]); - const [isAddContextFiltersModalOpen, setIsAddContextFiltersModalOpen] = - useState(false); + const [userTextFieldInput, setUserTextFieldInput] = useState('') + const [askText] = useState(AskOptions.Ask) + const [loadingResponse, setLoadingResponse] = useState(false) + const [readyToSave, setReadyToSave] = useState(false) + const [currentContext, setCurrentContext] = useState([]) + const [isAddContextFiltersModalOpen, setIsAddContextFiltersModalOpen] = useState(false) useEffect(() => { - const context = getChatHistoryContext(currentChatHistory); - setCurrentContext(context); - }, [currentChatHistory]); + const context = getChatHistoryContext(currentChatHistory) + setCurrentContext(context) + }, [currentChatHistory]) // update chat context when files are added useEffect(() => { const setContextOnFileAdded = async () => { if (chatFilters.files.length > 0) { - const results = await window.fileSystem.getFilesystemPathsAsDBItems( - chatFilters.files - ); - setCurrentContext(results as DBQueryResult[]); + const results = await window.fileSystem.getFilesystemPathsAsDBItems(chatFilters.files) + setCurrentContext(results as DBQueryResult[]) } else if (!currentChatHistory?.id) { // if there is no prior history, set current context to empty - setCurrentContext([]); + setCurrentContext([]) } - }; - setContextOnFileAdded(); - }, [chatFilters.files]); + } + setContextOnFileAdded() + }, [chatFilters.files, currentChatHistory?.id]) useEffect(() => { if (readyToSave && currentChatHistory) { - window.electronStore.updateChatHistory(currentChatHistory); - setReadyToSave(false); + window.electronStore.updateChatHistory(currentChatHistory) + setReadyToSave(false) } - }, [readyToSave, currentChatHistory]); + }, [readyToSave, currentChatHistory]) + + const appendNewContentToMessageHistory = useCallback( + (chatID: string, newContent: string, newMessageType: 'success' | 'error') => { + setCurrentChatHistory((prev) => { + if (chatID !== prev?.id) return prev + const newDisplayableHistory = prev?.displayableChatHistory || [] + if (newDisplayableHistory.length > 0) { + const lastMessage = newDisplayableHistory[newDisplayableHistory.length - 1] + if (lastMessage.role === 'assistant') { + lastMessage.content += newContent + lastMessage.messageType = newMessageType + } else { + newDisplayableHistory.push({ + role: 'assistant', + content: newContent, + messageType: newMessageType, + context: [], + }) + } + } else { + newDisplayableHistory.push({ + role: 'assistant', + content: newContent, + messageType: newMessageType, + context: [], + }) + } + return { + id: prev!.id, + displayableChatHistory: newDisplayableHistory, + openAIChatHistory: newDisplayableHistory.map((message) => ({ + role: message.role, + content: message.content, + })), + } + }) + }, + [setCurrentChatHistory], // Add any other dependencies here if needed + ) - const handleSubmitNewMessage = async ( - chatHistory: ChatHistory | undefined - ) => { - posthog.capture("chat_message_submitted", { + const handleSubmitNewMessage = async (chatHistory: ChatHistory | undefined) => { + posthog.capture('chat_message_submitted', { chatId: chatHistory?.id, chatLength: chatHistory?.displayableChatHistory.length, chatFilters: anonymizeChatFiltersForPosthog(chatFilters), - }); + }) + let outputChatHistory = chatHistory + try { - if (loadingResponse) return; - setLoadingResponse(true); - if (!userTextFieldInput.trim()) return; - const defaultLLMName = await window.llm.getDefaultLLMName(); - - if (!chatHistory || !chatHistory.id) { - const chatID = Date.now().toString(); - chatHistory = { + if (loadingResponse) return + setLoadingResponse(true) + if (!userTextFieldInput.trim()) return + const defaultLLMName = await window.llm.getDefaultLLMName() + if (!outputChatHistory || !outputChatHistory.id) { + const chatID = Date.now().toString() + outputChatHistory = { id: chatID, displayableChatHistory: [], - }; + } } - if (chatHistory.displayableChatHistory.length === 0) { + if (outputChatHistory.displayableChatHistory.length === 0) { if (chatFilters) { // chatHistory.displayableChatHistory.push({ // role: "system", @@ -174,157 +188,93 @@ const ChatWithLLM: React.FC = ({ // context: [], // }); - chatHistory.displayableChatHistory.push( - await resolveRAGContext(userTextFieldInput, chatFilters) - ); + outputChatHistory.displayableChatHistory.push(await resolveRAGContext(userTextFieldInput, chatFilters)) } } else { - chatHistory.displayableChatHistory.push({ - role: "user", + outputChatHistory.displayableChatHistory.push({ + role: 'user', content: userTextFieldInput, - messageType: "success", + messageType: 'success', context: [], - }); + }) } - setUserTextFieldInput(""); + setUserTextFieldInput('') - setCurrentChatHistory(chatHistory); + setCurrentChatHistory(outputChatHistory) - if (!chatHistory) return; + if (!outputChatHistory) return - await window.electronStore.updateChatHistory(chatHistory); + await window.electronStore.updateChatHistory(outputChatHistory) - const llmConfigs = await window.llm.getLLMConfigs(); + const llmConfigs = await window.llm.getLLMConfigs() - const currentModelConfig = llmConfigs.find( - (config) => config.modelName === defaultLLMName - ); + const currentModelConfig = llmConfigs.find((config) => config.modelName === defaultLLMName) if (!currentModelConfig) { - throw new Error(`No model config found for model: ${defaultLLMName}`); + throw new Error(`No model config found for model: ${defaultLLMName}`) } - await window.llm.streamingLLMResponse( - defaultLLMName, - currentModelConfig, - false, - chatHistory - ); - setReadyToSave(true); + await window.llm.streamingLLMResponse(defaultLLMName, currentModelConfig, false, outputChatHistory) + setReadyToSave(true) } catch (error) { - if (chatHistory) { - appendNewContentToMessageHistory( - chatHistory.id, - errorToStringRendererProcess(error), - "error" - ); + if (outputChatHistory) { + appendNewContentToMessageHistory(outputChatHistory.id, errorToStringRendererProcess(error), 'error') } } // so here we could save the chat history - setLoadingResponse(false); - }; - - const appendNewContentToMessageHistory = ( - chatID: string, - newContent: string, - newMessageType: "success" | "error" - ) => { - setCurrentChatHistory((prev) => { - if (chatID !== prev?.id) return prev; - const newDisplayableHistory = prev?.displayableChatHistory || []; - if (newDisplayableHistory.length > 0) { - const lastMessage = - newDisplayableHistory[newDisplayableHistory.length - 1]; - - if (lastMessage.role === "assistant") { - lastMessage.content += newContent; // Append new content with a space - lastMessage.messageType = newMessageType; - } else { - newDisplayableHistory.push({ - role: "assistant", - content: newContent, - messageType: newMessageType, - context: [], - }); - } - } else { - newDisplayableHistory.push({ - role: "assistant", - content: newContent, - messageType: newMessageType, - context: [], - }); - } - - return { - id: prev!.id, - displayableChatHistory: newDisplayableHistory, - openAIChatHistory: newDisplayableHistory.map((message) => { - return { - role: message.role, - content: message.content, - }; - }), - }; - }); - }; + setLoadingResponse(false) + } useEffect(() => { - const handleOpenAIChunk = async ( - receivedChatID: string, - chunk: ChatCompletionChunk - ) => { - const newContent = chunk.choices[0].delta.content ?? ""; + const handleOpenAIChunk = async (receivedChatID: string, chunk: ChatCompletionChunk) => { + const newContent = chunk.choices[0].delta.content ?? '' if (newContent) { - appendNewContentToMessageHistory(receivedChatID, newContent, "success"); + appendNewContentToMessageHistory(receivedChatID, newContent, 'success') } - }; - - const handleAnthropicChunk = async ( - receivedChatID: string, - chunk: MessageStreamEvent - ) => { - const newContent = - chunk.type === "content_block_delta" ? chunk.delta.text ?? "" : ""; + } + + const handleAnthropicChunk = async (receivedChatID: string, chunk: MessageStreamEvent) => { + const newContent = chunk.type === 'content_block_delta' ? (chunk.delta.text ?? '') : '' if (newContent) { - appendNewContentToMessageHistory(receivedChatID, newContent, "success"); + appendNewContentToMessageHistory(receivedChatID, newContent, 'success') } - }; + } - const removeOpenAITokenStreamListener = window.ipcRenderer.receive( - "openAITokenStream", - handleOpenAIChunk - ); + const removeOpenAITokenStreamListener = window.ipcRenderer.receive('openAITokenStream', handleOpenAIChunk) - const removeAnthropicTokenStreamListener = window.ipcRenderer.receive( - "anthropicTokenStream", - handleAnthropicChunk - ); + const removeAnthropicTokenStreamListener = window.ipcRenderer.receive('anthropicTokenStream', handleAnthropicChunk) return () => { - removeOpenAITokenStreamListener(); - removeAnthropicTokenStreamListener(); - }; - }, []); + removeOpenAITokenStreamListener() + removeAnthropicTokenStreamListener() + } + }, [appendNewContentToMessageHistory]) + + const getClassName = (message: ChatMessageToDisplay): string => { + const baseClasses = 'markdown-content break-words rounded-lg p-1' + + if (message.messageType === 'error') { + return `${baseClasses} bg-red-100 text-red-800` + } + if (message.role === 'assistant') { + return `${baseClasses} bg-neutral-600 text-gray-200` + } + return `${baseClasses} bg-blue-100 text-blue-800` + } return ( -
    -
    -
    -
    +
    +
    +
    +
    {currentChatHistory?.displayableChatHistory - .filter((msg) => msg.role !== "system") + .filter((msg) => msg.role !== 'system') .map((message, index) => ( {message.visibleContent ? message.visibleContent @@ -332,23 +282,21 @@ const ChatWithLLM: React.FC = ({ ))}
    - {(!currentChatHistory || - currentChatHistory?.displayableChatHistory.length == 0) && ( + {(!currentChatHistory || currentChatHistory?.displayableChatHistory.length === 0) && ( <> -
    +
    Start a conversation with your notes by typing a message below.
    -
    +
    @@ -373,30 +321,25 @@ const ChatWithLLM: React.FC = ({ /> ); })} */} - {userTextFieldInput === "" && - (!currentChatHistory || - currentChatHistory?.displayableChatHistory.length == 0) ? ( + {userTextFieldInput === '' && + (!currentChatHistory || currentChatHistory?.displayableChatHistory.length === 0) ? ( <> - {EXAMPLE_PROMPTS[askText].map((option, index) => { - return ( - { - setUserTextFieldInput(option); - }} - /> - ); - })} + {EXAMPLE_PROMPTS[askText].map((option) => ( + { + setUserTextFieldInput(option) + }} + /> + ))} ) : undefined}
    - handleSubmitNewMessage(currentChatHistory) - } + handleSubmitNewMessage={() => handleSubmitNewMessage(currentChatHistory)} loadingResponse={loadingResponse} askText={askText} /> @@ -406,31 +349,15 @@ const ChatWithLLM: React.FC = ({ similarEntries={currentContext} titleText="Context used in chat" onFileSelect={(path: string) => { - openFileByPath(path); - posthog.capture("open_file_from_chat_context"); - }} - saveCurrentFile={() => { - return Promise.resolve(); + openFileByPath(path) + posthog.capture('open_file_from_chat_context') }} + saveCurrentFile={() => Promise.resolve()} isLoadingSimilarEntries={false} - setIsRefined={() => {}} // to allow future toggling - isRefined={true} // always refined for now /> )}
    - ); -}; - -const getChatHistoryContext = ( - chatHistory: ChatHistory | undefined -): DBQueryResult[] => { - if (!chatHistory) return []; - const contextForChat = chatHistory.displayableChatHistory - .map((message) => { - return message.context; - }) - .flat(); - return contextForChat as DBQueryResult[]; -}; + ) +} -export default ChatWithLLM; +export default ChatWithLLM diff --git a/src/components/Chat/ChatAction.tsx b/src/components/Chat/ChatAction.tsx deleted file mode 100644 index 72bb2833..00000000 --- a/src/components/Chat/ChatAction.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import type { FC } from "react"; - -interface Props { - actionText: string; - onClick?: () => void; -} - -export const ChatAction: FC = ({ actionText, onClick }: Props) => { - return ( - - ); -}; diff --git a/src/components/Chat/ChatInput.tsx b/src/components/Chat/ChatInput.tsx index 90918fc8..1daf6579 100644 --- a/src/components/Chat/ChatInput.tsx +++ b/src/components/Chat/ChatInput.tsx @@ -1,13 +1,14 @@ -import React from "react"; +import React from 'react' + +import Textarea from '@mui/joy/Textarea' +import { CircularProgress } from '@mui/material' -import Textarea from "@mui/joy/Textarea"; -import { CircularProgress } from "@mui/material"; interface ChatInputProps { - userTextFieldInput: string; - setUserTextFieldInput: (value: string) => void; - handleSubmitNewMessage: () => void; - loadingResponse: boolean; - askText: string; + userTextFieldInput: string + setUserTextFieldInput: (value: string) => void + handleSubmitNewMessage: () => void + loadingResponse: boolean + askText: string } const ChatInput: React.FC = ({ @@ -16,56 +17,49 @@ const ChatInput: React.FC = ({ handleSubmitNewMessage, loadingResponse, askText, -}) => { - return ( -
    -
    -