From b35290568a9ace07a7b95fadf0e12e89bf53afb4 Mon Sep 17 00:00:00 2001 From: Logan Yang Date: Thu, 6 Mar 2025 22:38:34 -0800 Subject: [PATCH] Squashed 2.9.0 preview 250327 Update preview version Merge latest from Composer Support creating new files Update version to 2.9.0 preview Add minor fixes Update autocomplete call and prefix logic Merge Project mode into 2.9.0-preview Squashed commits from: commit a762629dee7d404b7144e207b5a168d170ee477e Author: Logan Yang Date: Tue Mar 25 16:29:31 2025 -0700 Add projectEnabled flag to CustomModel interface To commit 1694357823e66e1584828495f6af24f69926655e Author: wyh Date: Wed Feb 19 14:45:22 2025 +0800 feat: Support project-based feature. Fix package vulnerabilities Prerelease 250325 Update label for PROJECT_CHAIN to "Plus Projects (alpha)" in BasicSettings component --- manifest.json | 4 +- package-lock.json | 705 +++++++++++------- package.json | 4 + src/LLMProviders/brevilabsClient.ts | 79 +- src/LLMProviders/chainManager.ts | 141 +++- src/LLMProviders/chainRunner.ts | 46 +- src/LLMProviders/chatModelManager.ts | 5 +- src/LLMProviders/intentAnalyzer.ts | 4 +- src/LLMProviders/projectManager.ts | 339 +++++++++ src/LLMProviders/promptManager.ts | 15 +- src/aiParams.ts | 71 ++ src/autocomplete/autocompleteService.ts | 58 ++ src/autocomplete/codemirrorIntegration.ts | 154 ++++ src/autocomplete/utils.ts | 195 +++++ src/cache/autocompleteCache.ts | 64 ++ src/cache/projectContextCache.ts | 140 ++++ src/chainFactory.ts | 1 + src/chainUtils.ts | 7 +- src/commands/index.ts | 8 +- src/components/Chat.tsx | 145 +++- src/components/CopilotView.tsx | 44 +- .../chat-components/ChatControls.tsx | 75 +- src/components/chat-components/ChatInput.tsx | 134 ++-- .../chat-components/ChatMessages.tsx | 12 +- .../chat-components/ChatSingleMessage.tsx | 91 ++- src/components/chat-components/CodeBlock.tsx | 54 ++ .../chat-components/ProjectList.tsx | 404 ++++++++++ .../chat-components/SuggestedPrompts.tsx | 1 + src/components/composer/ApplyView.tsx | 436 +++++++++++ src/components/modals/AddProjectModal.tsx | 389 ++++++++++ .../modals/ApplyChangesConfirmModal.tsx | 15 + src/components/modals/ConfirmModal.tsx | 14 +- src/components/modals/InlineEditModal.tsx | 6 +- src/components/ui/button.tsx | 7 +- src/components/ui/obsidian-native-select.tsx | 59 ++ src/components/ui/tabs.tsx | 55 ++ src/composerUtils.ts | 41 + src/constants.ts | 40 +- src/hooks/useApplyCode.ts | 142 ++++ src/main.ts | 28 +- src/search/hybridRetriever.ts | 7 +- src/search/searchUtils.ts | 29 +- src/settings/model.ts | 5 +- src/settings/v2/components/ApiKeyDialog.tsx | 4 +- src/settings/v2/components/BasicSettings.tsx | 41 +- src/settings/v2/components/ModelSettings.tsx | 6 +- src/settings/v2/components/ModelTable.tsx | 4 +- src/sharedState.ts | 14 +- src/styles/tailwind.css | 10 - src/utils.ts | 4 + tailwind.config.js | 3 +- 51 files changed, 3863 insertions(+), 496 deletions(-) create mode 100644 src/LLMProviders/projectManager.ts create mode 100644 src/autocomplete/autocompleteService.ts create mode 100644 src/autocomplete/codemirrorIntegration.ts create mode 100644 src/autocomplete/utils.ts create mode 100644 src/cache/autocompleteCache.ts create mode 100644 src/cache/projectContextCache.ts create mode 100644 src/components/chat-components/CodeBlock.tsx create mode 100644 src/components/chat-components/ProjectList.tsx create mode 100644 src/components/composer/ApplyView.tsx create mode 100644 src/components/modals/AddProjectModal.tsx create mode 100644 src/components/modals/ApplyChangesConfirmModal.tsx create mode 100644 src/components/ui/obsidian-native-select.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/composerUtils.ts create mode 100644 src/hooks/useApplyCode.ts diff --git a/manifest.json b/manifest.json index 0c430ded..e01c80ab 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "copilot", "name": "Copilot", - "version": "2.8.8", + "version": "2.9.0-preview-250327", "minAppVersion": "0.15.0", "description": "An AI Copilot in Obsidian.", "author": "Logan Yang", @@ -10,4 +10,4 @@ "Buy Me a Coffee": "https://www.buymeacoffee.com/logancyang", "GitHub Sponsor": "https://github.com/sponsors/logancyang" } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5235c1fc..0fd29bab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.6", "@tabler/icons-react": "^2.14.0", "async-mutex": "^0.5.0", @@ -44,8 +45,10 @@ "chrono-node": "^2.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "codemirror-companion-extension": "^0.0.11", "cohere-ai": "^7.13.0", "crypto-js": "^4.1.1", + "diff": "^7.0.0", "esbuild-plugin-svg": "^0.1.0", "eventsource-parser": "^1.0.0", "jotai": "^2.10.3", @@ -71,6 +74,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/crypto-js": "^4.1.1", + "@types/diff": "^7.0.1", "@types/events": "^3.0.0", "@types/jest": "^29.5.11", "@types/koa": "^2.13.7", @@ -1386,89 +1390,19 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/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/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz", @@ -1665,18 +1599,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1692,109 +1626,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", - "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/traverse": "^7.21.0", - "@babel/types": "^7.21.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/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/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, + "dependencies": { + "@babel/types": "^7.27.0" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1980,25 +1831,25 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -2035,14 +1886,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -2103,22 +1953,83 @@ "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.0.3.tgz", "integrity": "sha512-ZykIcDTVv5UNmKWSTLAs3VukO6NDJkkSKxrgUTDPBkAlORVT3H9n5DbRjRl8xIotklscHdbLIa0b9+y3mQq73g==" }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz", + "integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.8", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz", + "integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.4", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz", + "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz", + "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, "node_modules/@codemirror/state": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", - "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==", - "dev": true, - "peer": true + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } }, "node_modules/@codemirror/view": { - "version": "6.9.5", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.5.tgz", - "integrity": "sha512-IZwGdkn8PDyMkGms7bCMpFuTCg6+6eCZNgLOe9xQL4RYvMw1yYLviH+qoAqsNoYGpDt4HhHoW9v3FmalobFoKw==", - "dev": true, - "peer": true, + "version": "6.36.4", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz", + "integrity": "sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==", + "license": "MIT", "dependencies": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.0.0", + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, @@ -4632,6 +4543,36 @@ "@langchain/core": ">=0.2.21 <0.4.0" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@mistralai/mistralai": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz", @@ -4644,15 +4585,15 @@ } }, "node_modules/@next/env": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.3.tgz", - "integrity": "sha512-Q1tXwQCGWyA3ehMph3VO+E6xFPHDKdHFYosadt0F78EObYxPio0S09H9UGYznDe6Wc8eLKLG89GqcFJJDiK5xw==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.4.tgz", + "integrity": "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==", "peer": true }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.3.tgz", - "integrity": "sha512-aZtmIh8jU89DZahXQt1La0f2EMPt/i7W+rG1sLtYJERsP7GRnNFghsciFpQcKHcGh4dUiyTB5C1X3Dde/Gw8gg==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.4.tgz", + "integrity": "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==", "cpu": [ "arm64" ], @@ -4666,9 +4607,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.3.tgz", - "integrity": "sha512-aw8901rjkVBK5mbq5oV32IqkJg+CQa6aULNlN8zyCWSsePzEG3kpDkAFkkTOh3eJ0p95KbkLyWBzslQKamXsLA==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.4.tgz", + "integrity": "sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==", "cpu": [ "x64" ], @@ -4682,9 +4623,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.3.tgz", - "integrity": "sha512-YbdaYjyHa4fPK4GR4k2XgXV0p8vbU1SZh7vv6El4bl9N+ZSiMfbmqCuCuNU1Z4ebJMumafaz6UCC2zaJCsdzjw==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.4.tgz", + "integrity": "sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==", "cpu": [ "arm64" ], @@ -4698,9 +4639,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.3.tgz", - "integrity": "sha512-qgH/aRj2xcr4BouwKG3XdqNu33SDadqbkqB6KaZZkozar857upxKakbRllpqZgWl/NDeSCBYPmUAZPBHZpbA0w==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.4.tgz", + "integrity": "sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==", "cpu": [ "arm64" ], @@ -4714,9 +4655,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.3.tgz", - "integrity": "sha512-uzafnTFwZCPN499fNVnS2xFME8WLC9y7PLRs/yqz5lz1X/ySoxfaK2Hbz74zYUdEg+iDZPd8KlsWaw9HKkLEVw==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.4.tgz", + "integrity": "sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==", "cpu": [ "x64" ], @@ -4730,9 +4671,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.3.tgz", - "integrity": "sha512-el6GUFi4SiDYnMTTlJJFMU+GHvw0UIFnffP1qhurrN1qJV3BqaSRUjkDUgVV44T6zpw1Lc6u+yn0puDKHs+Sbw==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.4.tgz", + "integrity": "sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==", "cpu": [ "x64" ], @@ -4746,9 +4687,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.3.tgz", - "integrity": "sha512-6RxKjvnvVMM89giYGI1qye9ODsBQpHSHVo8vqA8xGhmRPZHDQUE4jcDbhBwK0GnFMqBnu+XMg3nYukNkmLOLWw==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.4.tgz", + "integrity": "sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==", "cpu": [ "arm64" ], @@ -4762,9 +4703,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.3.tgz", - "integrity": "sha512-VId/f5blObG7IodwC5Grf+aYP0O8Saz1/aeU3YcWqNdIUAmFQY3VEPKPaIzfv32F/clvanOb2K2BR5DtDs6XyQ==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.4.tgz", + "integrity": "sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==", "cpu": [ "x64" ], @@ -6534,6 +6475,155 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz", @@ -8234,6 +8324,13 @@ "@types/ms": "*" } }, + "node_modules/@types/diff": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.1.tgz", + "integrity": "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", @@ -9238,9 +9335,10 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", + "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -9972,6 +10070,34 @@ "node": ">= 0.12.0" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codemirror-companion-extension": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/codemirror-companion-extension/-/codemirror-companion-extension-0.0.11.tgz", + "integrity": "sha512-m9/aSi0DYCwO//0IhUmdeH/ncJ7LWWYXhepZRT3GAXdsopxI9CYxnB8kbrrRmOaYVfefirzdfTcBF5CegpO9lg==", + "license": "MIT", + "dependencies": { + "codemirror": "^6.0.1" + }, + "peerDependencies": { + "@codemirror/state": "^6.2.0", + "@codemirror/view": "^6.7.2" + } + }, "node_modules/cohere-ai": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/cohere-ai/-/cohere-ai-7.13.0.tgz", @@ -10177,6 +10303,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -10626,6 +10758,15 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -12521,6 +12662,12 @@ "node": "*" } }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -12731,15 +12878,16 @@ "integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA==" }, "node_modules/ibm-cloud-sdk-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-5.1.0.tgz", - "integrity": "sha512-KJCbPz3tiXB1NGAD7cL4JtwpWV8yd/C7jsaHsxvedMo2ZblNG8emMyvSpGhiKAQVZmi3c0ujz6eJdy22NHuUWQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ibm-cloud-sdk-core/-/ibm-cloud-sdk-core-5.3.2.tgz", + "integrity": "sha512-YhtS+7hGNO61h/4jNShHxbbuJ1TnDqiFKQzfEaqePnonOvv8NnxWxOk92FlKKCCzZNOT34Gnd7WCLVJTntwEFQ==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@types/debug": "^4.1.12", - "@types/node": "~10.14.19", + "@types/node": "^18.19.80", "@types/tough-cookie": "^4.0.0", - "axios": "1.7.4", + "axios": "^1.8.2", "camelcase": "^6.3.0", "debug": "^4.3.4", "dotenv": "^16.4.5", @@ -12757,10 +12905,14 @@ } }, "node_modules/ibm-cloud-sdk-core/node_modules/@types/node": { - "version": "10.14.22", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.22.tgz", - "integrity": "sha512-9taxKC944BqoTVjE+UT3pQH0nHZlTvITwfsOZqyc+R3sfJuxaTtxWjfn1K2UlxyPcKHf0rnaXcVFrS9F9vf0bw==", - "peer": true + "version": "18.19.80", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.80.tgz", + "integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/ibm-cloud-sdk-core/node_modules/camelcase": { "version": "6.3.0", @@ -16700,12 +16852,12 @@ } }, "node_modules/next": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.3.tgz", - "integrity": "sha512-5igmb8N8AEhWDYzogcJvtcRDU6n4cMGtBklxKD4biYv4LXN8+awc/bbQ2IM2NQHdVPgJ6XumYXfo3hBtErg1DA==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.4.tgz", + "integrity": "sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==", "peer": true, "dependencies": { - "@next/env": "15.1.3", + "@next/env": "15.2.4", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -16720,14 +16872,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.3", - "@next/swc-darwin-x64": "15.1.3", - "@next/swc-linux-arm64-gnu": "15.1.3", - "@next/swc-linux-arm64-musl": "15.1.3", - "@next/swc-linux-x64-gnu": "15.1.3", - "@next/swc-linux-x64-musl": "15.1.3", - "@next/swc-win32-arm64-msvc": "15.1.3", - "@next/swc-win32-x64-msvc": "15.1.3", + "@next/swc-darwin-arm64": "15.2.4", + "@next/swc-darwin-x64": "15.2.4", + "@next/swc-linux-arm64-gnu": "15.2.4", + "@next/swc-linux-arm64-musl": "15.2.4", + "@next/swc-linux-x64-gnu": "15.2.4", + "@next/swc-linux-x64-musl": "15.2.4", + "@next/swc-win32-arm64-msvc": "15.2.4", + "@next/swc-win32-x64-msvc": "15.2.4", "sharp": "^0.33.5" }, "peerDependencies": { @@ -18004,9 +18156,10 @@ "dev": true }, "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -18323,12 +18476,14 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", - "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" @@ -18495,9 +18650,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", @@ -19618,11 +19773,10 @@ } }, "node_modules/style-mod": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.3.tgz", - "integrity": "sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==", - "dev": true, - "peer": true + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" }, "node_modules/style-to-object": { "version": "1.0.6", @@ -19919,15 +20073,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -20565,9 +20710,7 @@ "node_modules/w3c-keyname": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", - "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==", - "dev": true, - "peer": true + "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", diff --git a/package.json b/package.json index 2eee0083..8d70a1bb 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", "@types/crypto-js": "^4.1.1", + "@types/diff": "^7.0.1", "@types/events": "^3.0.0", "@types/jest": "^29.5.11", "@types/koa": "^2.13.7", @@ -97,6 +98,7 @@ "@radix-ui/react-slider": "^1.2.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.6", "@tabler/icons-react": "^2.14.0", "async-mutex": "^0.5.0", @@ -105,8 +107,10 @@ "chrono-node": "^2.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "codemirror-companion-extension": "^0.0.11", "cohere-ai": "^7.13.0", "crypto-js": "^4.1.1", + "diff": "^7.0.0", "esbuild-plugin-svg": "^0.1.0", "eventsource-parser": "^1.0.0", "jotai": "^2.10.3", diff --git a/src/LLMProviders/brevilabsClient.ts b/src/LLMProviders/brevilabsClient.ts index f3c986ab..e0e75842 100644 --- a/src/LLMProviders/brevilabsClient.ts +++ b/src/LLMProviders/brevilabsClient.ts @@ -71,11 +71,39 @@ export interface Youtube4llmResponse { elapsed_time_ms: number; } -interface LicenseResponse { +export interface LicenseResponse { is_valid: boolean; plan: string; } +export interface AutocompleteResponse { + response: { + completion: string; + }; + elapsed_time_ms: number; +} + +export interface ComposerPromptResponse { + prompt: string; +} + +export interface ComposerApplyResponse { + content: string; +} + +// Define interface for the composerApply request +export interface ComposerApplyRequest { + target_note: { + title: string; + content: string; + }; + chat_history: Array<{ + role: string; + content: string; + }>; + markdown_block: string; +} + export class BrevilabsClient { private static instance: BrevilabsClient; private pluginVersion: string = "Unknown"; @@ -251,4 +279,53 @@ export class BrevilabsClient { return data; } + + async autocomplete( + prefix: string, + noteContext: string = "", + relevant_notes: string = "" + ): Promise { + const { data, error } = await this.makeRequest("/autocomplete", { + prompt: prefix, + note_context: noteContext, + relevant_notes: relevant_notes, + max_tokens: 64, + }); + if (error) { + throw error; + } + if (!data) { + throw new Error("No data returned from autocomplete"); + } + return data; + } + + async composerPrompt(): Promise { + const { data, error } = await this.makeRequest( + "/composer/prompt", + {}, + "GET" + ); + if (error) { + throw error; + } + if (!data) { + throw new Error("No data returned from composerPrompt"); + } + return data; + } + + async composerApply(request: ComposerApplyRequest): Promise { + const { data, error } = await this.makeRequest( + "/composer/apply", + request + ); + if (error) { + throw error; + } + if (!data) { + throw new Error("No data returned from composerApply"); + } + return data; + } } diff --git a/src/LLMProviders/chainManager.ts b/src/LLMProviders/chainManager.ts index d7155b76..beccf0f6 100644 --- a/src/LLMProviders/chainManager.ts +++ b/src/LLMProviders/chainManager.ts @@ -1,17 +1,17 @@ import { getChainType, + getCurrentProject, getModelKey, SetChainOptions, setChainType, - subscribeToChainTypeChange, - subscribeToModelKeyChange, } from "@/aiParams"; import ChainFactory, { ChainType, Document } from "@/chainFactory"; -import { BUILTIN_CHAT_MODELS, USER_SENDER, VAULT_VECTOR_STORE_STRATEGY } from "@/constants"; +import { BUILTIN_CHAT_MODELS, USER_SENDER } from "@/constants"; import { ChainRunner, CopilotPlusChainRunner, LLMChainRunner, + ProjectChainRunner, VaultQAChainRunner, } from "@/LLMProviders/chainRunner"; import { logError, logInfo } from "@/logger"; @@ -32,17 +32,27 @@ import MemoryManager from "./memoryManager"; import PromptManager from "./promptManager"; export default class ChainManager { - private static chain: RunnableSequence; - private static retrievalChain: RunnableSequence; + private chain: RunnableSequence; + private retrievalChain: RunnableSequence; + private retrievedDocuments: Document[] = []; + + public getRetrievedDocuments(): Document[] { + return this.retrievedDocuments; + } public app: App; public vectorStoreManager: VectorStoreManager; public chatModelManager: ChatModelManager; public memoryManager: MemoryManager; public promptManager: PromptManager; - public static retrievedDocuments: Document[] = []; + + // A chat history that stores the messages sent and received + // Only reset when the user explicitly clicks "New Chat" + private chatMessages: ChatMessage[] = []; constructor(app: App, vectorStoreManager: VectorStoreManager) { + this.chatMessages = []; + // Instantiate singletons this.app = app; this.vectorStoreManager = vectorStoreManager; @@ -53,29 +63,21 @@ export default class ChainManager { // Initialize async operations this.initialize(); - // Set up subscriptions - subscribeToModelKeyChange(async () => await this.createChainWithNewModel()); - subscribeToChainTypeChange(() => - this.setChain(getChainType(), { - refreshIndex: - getSettings().indexVaultToVectorStore === VAULT_VECTOR_STORE_STRATEGY.ON_MODE_SWITCH && - (getChainType() === ChainType.VAULT_QA_CHAIN || - getChainType() === ChainType.COPILOT_PLUS_CHAIN), - }) - ); - subscribeToSettingsChange(async () => await this.createChainWithNewModel()); + subscribeToSettingsChange(async () => { + await this.createChainWithNewModel(); + }); } private async initialize() { await this.createChainWithNewModel(); } - static getChain(): RunnableSequence { - return ChainManager.chain; + public getChain(): RunnableSequence { + return this.chain; } - static getRetrievalChain(): RunnableSequence { - return ChainManager.retrievalChain; + public getRetrievalChain(): RunnableSequence { + return this.retrievalChain; } private validateChainType(chainType: ChainType): void { @@ -92,35 +94,60 @@ export default class ChainManager { } private validateChainInitialization() { - if (!ChainManager.chain || !isSupportedChain(ChainManager.chain)) { + if (!this.chain || !isSupportedChain(this.chain)) { console.error("Chain is not initialized properly, re-initializing chain: ", getChainType()); - this.setChain(getChainType()); + this.createChainWithNewModel({}, false); + // this.setChain(getChainType()); } } - static storeRetrieverDocuments(documents: Document[]) { - ChainManager.retrievedDocuments = documents; + public storeRetrieverDocuments(documents: Document[]) { + this.retrievedDocuments = documents; } /** * Update the active model and create a new chain with the specified model * name. */ - async createChainWithNewModel(): Promise { - let newModelKey = getModelKey(); + async createChainWithNewModel( + options: SetChainOptions = {}, + neededReInitChatMode: boolean = true + ): Promise { + const chainType = getChainType(); + const currentProject = getCurrentProject(); + + if (chainType === ChainType.PROJECT_CHAIN && !currentProject) { + return; + } + + let newModelKey = + chainType === ChainType.PROJECT_CHAIN ? currentProject?.projectModelKey : getModelKey(); + + if (!newModelKey) { + new Notice("No model key found"); + throw new Error("No model key found"); + } + try { - let customModel = findCustomModel(newModelKey, getSettings().activeModels); - if (!customModel) { - // Reset default model if no model is found - console.error("Resetting default model. No model configuration found for: ", newModelKey); - customModel = BUILTIN_CHAT_MODELS[0]; - newModelKey = customModel.name + "|" + customModel.provider; + if (neededReInitChatMode) { + let customModel = findCustomModel(newModelKey, getSettings().activeModels); + if (!customModel) { + // Reset default model if no model is found + console.error("Resetting default model. No model configuration found for: ", newModelKey); + customModel = BUILTIN_CHAT_MODELS[0]; + newModelKey = customModel.name + "|" + customModel.provider; + } + const mergedModel = { + ...customModel, + ...currentProject?.modelConfigs, + }; + await this.chatModelManager.setChatModel(mergedModel); } - await this.chatModelManager.setChatModel(customModel); + // Must update the chatModel for chain because ChainFactory always // retrieves the old chain without the chatModel change if it exists! // Create a new chain with the new chatModel - this.setChain(getChainType()); + this.setChain(chainType, options); logInfo(`Setting model to ${newModelKey}`); } catch (error) { logError(`createChainWithNewModel failed: ${error}`); @@ -143,7 +170,7 @@ export default class ChainManager { switch (chainType) { case ChainType.LLM_CHAIN: { - ChainManager.chain = ChainFactory.createNewLLMChain({ + this.chain = ChainFactory.createNewLLMChain({ llm: chatModel, memory: memory, prompt: options.prompt || chatPrompt, @@ -164,13 +191,13 @@ export default class ChainManager { }); // Create new conversational retrieval chain - ChainManager.retrievalChain = ChainFactory.createConversationalRetrievalChain( + this.retrievalChain = ChainFactory.createConversationalRetrievalChain( { llm: chatModel, retriever: retriever, systemMessage: getSystemPrompt(), }, - ChainManager.storeRetrieverDocuments.bind(ChainManager), + this.storeRetrieverDocuments.bind(this), getSettings().debug ); @@ -185,7 +212,7 @@ export default class ChainManager { case ChainType.COPILOT_PLUS_CHAIN: { // For initial load of the plugin await this.initializeQAChain(options); - ChainManager.chain = ChainFactory.createNewLLMChain({ + this.chain = ChainFactory.createNewLLMChain({ llm: chatModel, memory: memory, prompt: options.prompt || chatPrompt, @@ -196,6 +223,19 @@ export default class ChainManager { break; } + case ChainType.PROJECT_CHAIN: { + // For initial load of the plugin + await this.initializeQAChain(options); + this.chain = ChainFactory.createNewLLMChain({ + llm: chatModel, + memory: memory, + prompt: options.prompt || chatPrompt, + abortController: options.abortController, + }) as RunnableSequence; + setChainType(ChainType.PROJECT_CHAIN); + break; + } + default: this.validateChainType(chainType); break; @@ -211,6 +251,8 @@ export default class ChainManager { return new VaultQAChainRunner(this); case ChainType.COPILOT_PLUS_CHAIN: return new CopilotPlusChainRunner(this); + case ChainType.PROJECT_CHAIN: + return new ProjectChainRunner(this); default: throw new Error(`Unsupported chain type: ${chainType}`); } @@ -259,9 +301,10 @@ export default class ChainManager { ]); } - this.setChain(getChainType(), { + this.createChainWithNewModel({ prompt: effectivePrompt }, false); + /*this.setChain(getChainType(), { prompt: effectivePrompt, - }); + });*/ } const chainRunner = this.getChainRunner(); @@ -286,4 +329,20 @@ export default class ChainManager { } } } + + public clearHistory() { + this.chatMessages = []; + } + + public getChatMessages(): ChatMessage[] { + return this.chatMessages; + } + + public setChatMessages(messages: ChatMessage[]) { + this.chatMessages = [...messages]; + } + + public addChatMessage(message: ChatMessage) { + this.chatMessages.push(message); + } } diff --git a/src/LLMProviders/chainRunner.ts b/src/LLMProviders/chainRunner.ts index 330fa1dd..579b88c7 100644 --- a/src/LLMProviders/chainRunner.ts +++ b/src/LLMProviders/chainRunner.ts @@ -1,4 +1,6 @@ +import { getCurrentProject } from "@/aiParams"; import { getStandaloneQuestion } from "@/chainUtils"; +import { getComposerSystemPrompt } from "@/composerUtils"; import { ABORT_REASON, AI_SENDER, @@ -9,7 +11,6 @@ import { } from "@/constants"; import { BrevilabsClient } from "@/LLMProviders/brevilabsClient"; import { logError } from "@/logger"; -import { getSystemPrompt } from "@/settings/model"; import { ChatMessage } from "@/sharedState"; import { ToolManager } from "@/tools/toolManager"; import { @@ -28,6 +29,7 @@ import { BaseChatModel } from "@langchain/core/language_models/chat_models"; import { Notice } from "obsidian"; import ChainManager from "./chainManager"; import { COPILOT_TOOL_NAMES, IntentAnalyzer } from "./intentAnalyzer"; +import ProjectManager from "./projectManager"; class ThinkBlockStreamer { private hasOpenThinkBlock = false; @@ -80,7 +82,15 @@ export interface ChainRunner { } abstract class BaseChainRunner implements ChainRunner { - constructor(protected chainManager: ChainManager) {} + protected chainManager: ChainManager; + + constructor(chainManager: ChainManager) { + this.chainManager = chainManager; + } + + protected async getSystemPrompt(): Promise { + return getComposerSystemPrompt(); + } abstract run( userMessage: ChatMessage, @@ -201,7 +211,7 @@ class LLMChainRunner extends BaseChainRunner { const streamer = new ThinkBlockStreamer(updateCurrentAiMessage); try { - const chain = ChainManager.getChain(); + const chain = this.chainManager.getChain(); const chatStream = await chain.stream({ input: userMessage.message, } as any); @@ -257,7 +267,7 @@ class VaultQAChainRunner extends BaseChainRunner { const memory = this.chainManager.memoryManager.getMemory(); const memoryVariables = await memory.loadMemoryVariables({}); const chatHistory = extractChatHistory(memoryVariables); - const qaStream = await ChainManager.getRetrievalChain().stream({ + const qaStream = await this.chainManager.getRetrievalChain().stream({ question: userMessage.message, chat_history: chatHistory, } as any); @@ -284,7 +294,7 @@ class VaultQAChainRunner extends BaseChainRunner { } private addSourcestoResponse(response: string): string { - const docTitles = extractUniqueTitlesFromDocs(ChainManager.retrievedDocuments); + const docTitles = extractUniqueTitlesFromDocs(this.chainManager.getRetrievedDocuments()); if (docTitles.length > 0) { const links = docTitles.map((title) => `- [[${title}]]`).join("\n"); response += "\n\n#### Sources:\n\n" + links; @@ -427,7 +437,8 @@ class CopilotPlusChainRunner extends BaseChainRunner { const messages: any[] = []; // Add system message if available - let fullSystemMessage = getSystemPrompt(); + + let fullSystemMessage = await this.getSystemPrompt(); // Add chat history context to system message if exists if (chatHistory.length > 0) { @@ -756,4 +767,25 @@ class CopilotPlusChainRunner extends BaseChainRunner { } } -export { CopilotPlusChainRunner, LLMChainRunner, VaultQAChainRunner }; +class ProjectChainRunner extends CopilotPlusChainRunner { + protected async getSystemPrompt(): Promise { + // get current project + const projectConfig = getCurrentProject(); + + if (!projectConfig) { + return super.getSystemPrompt(); + } + + // Get cached context synchronously + const context = ProjectManager.instance.getProjectContext(projectConfig.id); + let finalPrompt = projectConfig.systemPrompt; + + if (context) { + finalPrompt = `${finalPrompt}\n\n${context}`; + } + + return finalPrompt; + } +} + +export { CopilotPlusChainRunner, LLMChainRunner, ProjectChainRunner, VaultQAChainRunner }; diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index 7c9665bd..bee13e2a 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -1,4 +1,4 @@ -import { CustomModel, getModelKey, ModelConfig, setModelKey } from "@/aiParams"; +import { CustomModel, getModelKey, ModelConfig } from "@/aiParams"; import { BREVILABS_API_BASE_URL, BUILTIN_CHAT_MODELS, @@ -306,7 +306,8 @@ export default class ChatModelManager { async setChatModel(model: CustomModel): Promise { const modelKey = getModelKeyFromModel(model); - setModelKey(modelKey); + // todo 为啥这里需要重新设置? + // setModelKey(modelKey); try { const modelInstance = await this.createModelInstance(model); ChatModelManager.chatModel = modelInstance; diff --git a/src/LLMProviders/intentAnalyzer.ts b/src/LLMProviders/intentAnalyzer.ts index 7aad5764..61165b49 100644 --- a/src/LLMProviders/intentAnalyzer.ts +++ b/src/LLMProviders/intentAnalyzer.ts @@ -11,8 +11,8 @@ import { simpleYoutubeTranscriptionTool } from "@/tools/YoutubeTools"; import { ToolManager } from "@/tools/toolManager"; import { extractChatHistory, extractYoutubeUrl } from "@/utils"; import { BrevilabsClient } from "./brevilabsClient"; -import MemoryManager from "./memoryManager"; import { Vault } from "obsidian"; +import ProjectManager from "@/LLMProviders/projectManager"; // TODO: Add @index with explicit pdf files in chat context menu export const COPILOT_TOOL_NAMES = ["@vault", "@web", "@youtube", "@pomodoro"]; @@ -112,7 +112,7 @@ export class IntentAnalyzer { // Handle @web command if (message.includes("@web")) { const cleanQuery = this.removeAtCommands(originalMessage); - const memory = MemoryManager.getInstance().getMemory(); + const memory = ProjectManager.instance.getCurrentChainManager().memoryManager.getMemory(); const memoryVariables = await memory.loadMemoryVariables({}); const chatHistory = extractChatHistory(memoryVariables); diff --git a/src/LLMProviders/projectManager.ts b/src/LLMProviders/projectManager.ts new file mode 100644 index 00000000..2d0b95a9 --- /dev/null +++ b/src/LLMProviders/projectManager.ts @@ -0,0 +1,339 @@ +import { + getChainType, + isProjectMode, + ProjectConfig, + setProjectLoading, + subscribeToChainTypeChange, + subscribeToModelKeyChange, + subscribeToProjectChange, +} from "@/aiParams"; +import { ProjectContextCache } from "@/cache/projectContextCache"; +import { ChainType } from "@/chainFactory"; +import { updateChatMemory } from "@/chatUtils"; +import CopilotView from "@/components/CopilotView"; +import { CHAT_VIEWTYPE, VAULT_VECTOR_STORE_STRATEGY } from "@/constants"; +import { logError, logInfo } from "@/logger"; +import CopilotPlugin from "@/main"; +import { Mention } from "@/mentions/Mention"; +import { getMatchingPatterns, shouldIndexFile } from "@/search/searchUtils"; +import { getSettings, subscribeToSettingsChange } from "@/settings/model"; +import { ChatMessage } from "@/sharedState"; +import { err2String } from "@/utils"; +import { App, Notice } from "obsidian"; +import VectorStoreManager from "../search/vectorStoreManager"; +import { BrevilabsClient } from "./brevilabsClient"; +import ChainManager from "./chainManager"; + +export default class ProjectManager { + public static instance: ProjectManager; + private currentProjectId: string | null; + private app: App; + private plugin: CopilotPlugin; + private readonly chainMangerInstance: ChainManager; + private readonly projectContextCache: ProjectContextCache; + private chatMessageCache: Map; + private defaultProjectKey: string = "defaultProjectKey"; + + private constructor(app: App, vectorStoreManager: VectorStoreManager, plugin: CopilotPlugin) { + this.app = app; + this.plugin = plugin; + this.currentProjectId = null; + this.chainMangerInstance = new ChainManager(app, vectorStoreManager); + this.projectContextCache = ProjectContextCache.getInstance(); + this.chatMessageCache = new Map(); + + // Set up subscriptions + subscribeToModelKeyChange(async () => { + await this.getCurrentChainManager().createChainWithNewModel(); + }); + + subscribeToChainTypeChange(async () => { + // When switching from other modes to project mode, no need to update the chain. + if (isProjectMode()) { + return; + } + await this.getCurrentChainManager().createChainWithNewModel({ + refreshIndex: + getSettings().indexVaultToVectorStore === VAULT_VECTOR_STORE_STRATEGY.ON_MODE_SWITCH && + (getChainType() === ChainType.VAULT_QA_CHAIN || + getChainType() === ChainType.COPILOT_PLUS_CHAIN), + }); + }); + + // Subscribe to Project changes + subscribeToProjectChange(async (project) => { + await this.switchProject(project); + }); + + // Subscribe to settings changes to monitor projectList changes + this.setupProjectListChangeMonitor(); + } + + private setupProjectListChangeMonitor() { + subscribeToSettingsChange(async (prev, next) => { + if (!prev || !next) return; + + const prevProjects = prev.projectList || []; + const nextProjects = next.projectList || []; + + // Find modified projects + for (const nextProject of nextProjects) { + const prevProject = prevProjects.find((p) => p.id === nextProject.id); + if (prevProject) { + // Check if project configuration has changed + if (JSON.stringify(prevProject) !== JSON.stringify(nextProject)) { + // Clear context cache for this project + await this.projectContextCache.clearForProject(nextProject); + + // If this is the current project, reload its context and recreate chain + if (this.currentProjectId === nextProject.id) { + await Promise.all([ + this.loadProjectContext(nextProject), + // Recreate chain to pick up new system prompt + this.getCurrentChainManager().createChainWithNewModel(), + ]); + } + } + } + } + }); + } + + public static getInstance( + app: App, + vectorStoreManager: VectorStoreManager, + plugin: CopilotPlugin + ): ProjectManager { + if (!ProjectManager.instance) { + ProjectManager.instance = new ProjectManager(app, vectorStoreManager, plugin); + } + return ProjectManager.instance; + } + + public getCurrentChainManager(): ChainManager { + return this.chainMangerInstance; + } + + public async switchProject(project: ProjectConfig | null): Promise { + try { + setProjectLoading(true); + + // 1. save current project message. 2. load next project message + + // switch default project + if (!project) { + await this.saveCurrentProjectMessage(); + this.currentProjectId = null; // ensure set currentProjectId + + await this.loadNextProjectMessage(); + this.refreshChatView(); + return; + } + + // else + const projectId = project.id; + if (this.currentProjectId === projectId) { + return; + } + + await this.saveCurrentProjectMessage(); + this.currentProjectId = projectId; // ensure set currentProjectId + + await Promise.all([ + // load memory + this.loadNextProjectMessage(), + // update chat model + this.getCurrentChainManager().createChainWithNewModel(), + // load context + this.loadProjectContext(project), + ]); + + // fresh chat view + this.refreshChatView(); + + logInfo(`Switched to project: ${project.name}`); + } catch (error) { + logError(`Failed to switch project: ${error}`); + throw error; + } finally { + setProjectLoading(false); + } + } + + private async saveCurrentProjectMessage() { + // save show message + this.chatMessageCache.set( + this.currentProjectId ? this.currentProjectId : this.defaultProjectKey, + this.getCurrentChainManager().getChatMessages() + ); + + // TODO(emt-lin): do this or not? + await this.plugin.autosaveCurrentChat(); + } + + private async loadNextProjectMessage() { + const chainManager = this.getCurrentChainManager(); + + const messages = + this.chatMessageCache.get( + this.currentProjectId ? this.currentProjectId : this.defaultProjectKey + ) ?? []; + + chainManager.setChatMessages(messages); + await updateChatMemory(messages, chainManager.memoryManager); + } + + // TODO(logan): This should be reused as a generic context loading function + private async loadProjectContext(project: ProjectConfig): Promise { + try { + if (project.contextSource) { + // Try to get context from cache first + const cachedContext = await this.projectContextCache.get(project); + if (cachedContext) { + return; + } + + const [markdownContext, webContext, youtubeContext] = await Promise.all([ + this.processMarkdownContext( + project.contextSource.inclusions, + project.contextSource.exclusions + ), + this.processWebUrlsContext(project.contextSource.webUrls), + this.processYoutubeUrlsContext(project.contextSource.youtubeUrls), + ]); + + // Build context sections only for non-null sources + const contextParts = []; + + if (project.contextSource.inclusions || project.contextSource.exclusions) { + contextParts.push(`## Markdown Files\n${markdownContext}`); + } + + if (project.contextSource.webUrls?.trim()) { + contextParts.push(`## Web Content\n${webContext}`); + } + + if (project.contextSource.youtubeUrls?.trim()) { + contextParts.push(`## YouTube Content\n${youtubeContext}`); + } + + const contextText = ` +# Project Context +The following information is the relevant context for this project. Use this information to inform your responses when appropriate: + + +${contextParts.join("\n\n")} + +`; + + // Cache the generated context + await this.projectContextCache.set(project, contextText); + } + } catch (error) { + logError(`Failed to load project context: ${error}`); + throw error; + } + } + + private refreshChatView() { + // get chat view + const chatView = this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0]?.view as CopilotView; + if (chatView) { + chatView.updateView(); + } + } + + public getProjectContext(projectId: string): string | null { + const project = getSettings().projectList.find((p) => p.id === projectId); + if (!project) { + return null; + } + return this.projectContextCache.getSync(project); + } + + public async clearContextCache(projectId: string): Promise { + const project = getSettings().projectList.find((p) => p.id === projectId); + if (project) { + await this.projectContextCache.clearForProject(project); + logInfo(`Context cache cleared for project: ${projectId}`); + } + } + + private async processMarkdownContext(inclusions?: string, exclusions?: string): Promise { + if (!inclusions && !exclusions) { + return ""; + } + + // NOTE: Must not fallback to GLOBAL inclusions and exclusions in Copilot settings in Projects! + // This is to avoid project inclusions in the project that conflict with the global ones + // Project UI should be the ONLY source of truth for project inclusions and exclusions + const { inclusions: inclusionPatterns, exclusions: exclusionPatterns } = getMatchingPatterns({ + inclusions, + exclusions, + isProject: true, + }); + + const files = this.app.vault.getMarkdownFiles().filter((file) => { + return shouldIndexFile(file, inclusionPatterns, exclusionPatterns); + }); + + // Process each file with its metadata + const processedNotes = await Promise.all( + files.map(async (file) => { + const content = await this.app.vault.cachedRead(file); + const stat = await this.app.vault.adapter.stat(file.path); + + // Format the note with metadata + return `[[${file.basename}]] +path: ${file.path} +created: ${stat ? new Date(stat.ctime).toISOString() : "unknown"} +modified: ${stat ? new Date(stat.mtime).toISOString() : "unknown"} + +${content}`; + }) + ); + + // Join all processed notes with double newlines + return processedNotes.join("\n\n"); + } + + private async processWebUrlsContext(webUrls?: string): Promise { + if (!webUrls?.trim()) { + return ""; + } + + try { + const mention = Mention.getInstance(); + const { urlContext } = await mention.processUrls(webUrls); + return urlContext || ""; + } catch (error) { + logError(`Failed to process web URLs: ${error}`); + new Notice(`Failed to process web URLs: ${err2String(error)}`); + return ""; + } + } + + private async processYoutubeUrlsContext(youtubeUrls?: string): Promise { + if (!youtubeUrls?.trim()) { + return ""; + } + + const urls = youtubeUrls.split("\n").filter((url) => url.trim()); + const processPromises = urls.map(async (url) => { + try { + const response = await BrevilabsClient.getInstance().youtube4llm(url); + if (response.response.transcript) { + return `\n\nYouTube transcript from ${url}:\n${response.response.transcript}`; + } + return ""; + } catch (error) { + logError(`Failed to process YouTube URL ${url}: ${error}`); + new Notice(`Failed to process YouTube URL ${url}: ${err2String(error)}`); + return ""; + } + }); + + const results = await Promise.all(processPromises); + return results.join(""); + } +} diff --git a/src/LLMProviders/promptManager.ts b/src/LLMProviders/promptManager.ts index a2a0edca..4bd8fc0d 100644 --- a/src/LLMProviders/promptManager.ts +++ b/src/LLMProviders/promptManager.ts @@ -5,6 +5,7 @@ import { MessagesPlaceholder, SystemMessagePromptTemplate, } from "@langchain/core/prompts"; +import { getCurrentProject, isProjectMode, subscribeToProjectChange } from "@/aiParams"; export default class PromptManager { private static instance: PromptManager; @@ -19,6 +20,11 @@ export default class PromptManager { this.initChatPrompt(); this.initQAPrompt(); }); + + subscribeToProjectChange(() => { + this.initChatPrompt(); + this.initQAPrompt(); + }); } static getInstance(): PromptManager { @@ -29,8 +35,15 @@ export default class PromptManager { } private initChatPrompt(): void { + let systemPrompt = getSystemPrompt(); + + const currentProject = getCurrentProject(); + if (currentProject && isProjectMode()) { + systemPrompt = currentProject.systemPrompt; + } + // Escape curly braces in the system message - const escapedSystemMessage = this.escapeTemplateString(getSystemPrompt()); + const escapedSystemMessage = this.escapeTemplateString(systemPrompt); this.chatPrompt = ChatPromptTemplate.fromMessages([ SystemMessagePromptTemplate.fromTemplate(escapedSystemMessage), diff --git a/src/aiParams.ts b/src/aiParams.ts index 38b45135..00d4b1ee 100644 --- a/src/aiParams.ts +++ b/src/aiParams.ts @@ -34,6 +34,29 @@ const chainTypeAtom = atom( } ); +const currentProjectAtom = atom(null); +const projectLoadingAtom = atom(false); + +export interface ProjectConfig { + id: string; + name: string; + description?: string; + systemPrompt: string; + projectModelKey: string; + modelConfigs: { + temperature?: number; + maxTokens?: number; + }; + contextSource: { + inclusions: string; + exclusions?: string; + webUrls?: string; + youtubeUrls?: string; + }; + created: number; + UsageTimestamps: number; +} + export interface ModelConfig { modelName: string; temperature: number; @@ -79,7 +102,9 @@ export interface CustomModel { stream?: boolean; temperature?: number; maxTokens?: number; + context?: number; + projectEnabled?: boolean; plusExclusive?: boolean; believerExclusive?: boolean; capabilities?: ModelCapability[]; @@ -132,3 +157,49 @@ export function useChainType() { store: settingsStore, }); } + +export function setCurrentProject(project: ProjectConfig | null) { + settingsStore.set(currentProjectAtom, project); +} + +export function getCurrentProject(): ProjectConfig | null { + return settingsStore.get(currentProjectAtom); +} + +export function subscribeToProjectChange( + callback: (project: ProjectConfig | null) => void +): () => void { + return settingsStore.sub(currentProjectAtom, () => { + callback(settingsStore.get(currentProjectAtom)); + }); +} + +export function useCurrentProject() { + return useAtom(currentProjectAtom, { + store: settingsStore, + }); +} + +export function setProjectLoading(loading: boolean) { + settingsStore.set(projectLoadingAtom, loading); +} + +export function isProjectLoading(): boolean { + return settingsStore.get(projectLoadingAtom); +} + +export function subscribeToProjectLoadingChange(callback: (loading: boolean) => void): () => void { + return settingsStore.sub(projectLoadingAtom, () => { + callback(settingsStore.get(projectLoadingAtom)); + }); +} + +export function useProjectLoading() { + return useAtom(projectLoadingAtom, { + store: settingsStore, + }); +} + +export function isProjectMode() { + return getChainType() === ChainType.PROJECT_CHAIN; +} diff --git a/src/autocomplete/autocompleteService.ts b/src/autocomplete/autocompleteService.ts new file mode 100644 index 00000000..84f81529 --- /dev/null +++ b/src/autocomplete/autocompleteService.ts @@ -0,0 +1,58 @@ +import { AUTOCOMPLETE_CONFIG } from "@/constants"; +import { logError, logInfo } from "@/logger"; +import { getSettings, subscribeToSettingsChange } from "@/settings/model"; +import { Plugin } from "obsidian"; +import { CodeMirrorIntegration } from "./codemirrorIntegration"; + +export class AutocompleteService { + private static instance: AutocompleteService; + private cmIntegration: CodeMirrorIntegration; + private unsubscribeSettings: () => void; + + private constructor(private plugin: Plugin) { + // Initialize with current settings + const settings = getSettings(); + + this.cmIntegration = CodeMirrorIntegration.getInstance({ + delay: AUTOCOMPLETE_CONFIG.DELAY_MS, + minTriggerLength: AUTOCOMPLETE_CONFIG.MIN_TRIGGER_LENGTH, + maxContextLength: AUTOCOMPLETE_CONFIG.MAX_CONTEXT_LENGTH, + }); + + // Subscribe to settings changes + this.unsubscribeSettings = subscribeToSettingsChange((prev, next) => { + // Only need to handle enableAutocomplete changes + if (prev.enableAutocomplete !== next.enableAutocomplete) { + logInfo(`Settings changed - enableAutocomplete: ${next.enableAutocomplete}`); + this.cmIntegration.setActive(next.enableAutocomplete); + } + }); + + // Set initial active state + this.cmIntegration.setActive(settings.enableAutocomplete); + + // Register the extension globally + this.registerExtension(); + } + + static getInstance(plugin: Plugin): AutocompleteService { + if (!AutocompleteService.instance) { + AutocompleteService.instance = new AutocompleteService(plugin); + } + return AutocompleteService.instance; + } + + private registerExtension() { + try { + this.plugin.registerEditorExtension([this.cmIntegration.getExtension()]); + } catch (error) { + logError("Failed to register CodeMirror extension:", error); + } + } + + destroy() { + logInfo("Destroying AutocompleteService"); + this.unsubscribeSettings(); + this.cmIntegration.setActive(false); + } +} diff --git a/src/autocomplete/codemirrorIntegration.ts b/src/autocomplete/codemirrorIntegration.ts new file mode 100644 index 00000000..0b9de9f1 --- /dev/null +++ b/src/autocomplete/codemirrorIntegration.ts @@ -0,0 +1,154 @@ +import { AutocompleteCache } from "@/cache/autocompleteCache"; +import { BrevilabsClient } from "@/LLMProviders/brevilabsClient"; +import { logError, logInfo } from "@/logger"; +import { Extension } from "@codemirror/state"; +import { forceableInlineSuggestion, type Suggestion } from "codemirror-companion-extension"; +import { MarkdownView } from "obsidian"; +import { getEditorContext, isNonSpaceDelimitedText, RelevantNotesCache } from "./utils"; + +export interface AutocompleteOptions { + delay: number; + minTriggerLength: number; + maxContextLength: number; +} + +export class CodeMirrorIntegration { + private static instance: CodeMirrorIntegration; + private cache: AutocompleteCache; + private client: BrevilabsClient; + private extension: Extension; + private forceFetch: () => void; + private isActive = false; + private lastSuggestionTime: number = 0; + private readonly MIN_SUGGESTION_INTERVAL = 200; // ms + + private constructor(private options: AutocompleteOptions) { + this.cache = AutocompleteCache.getInstance(); + this.client = BrevilabsClient.getInstance(); + + const { extension, force_fetch } = forceableInlineSuggestion({ + fetchFn: () => this.handleCompletion(), + delay: this.options.delay, + continue_suggesting: false, + accept_shortcut: "Tab", + }); + + this.extension = extension; + this.forceFetch = force_fetch; + } + + static getInstance(options: AutocompleteOptions): CodeMirrorIntegration { + if (!CodeMirrorIntegration.instance) { + CodeMirrorIntegration.instance = new CodeMirrorIntegration(options); + } + return CodeMirrorIntegration.instance; + } + + setActive(active: boolean) { + this.isActive = active; + } + + getExtension(): Extension { + return this.extension; + } + + private canGenerateNewSuggestion(): boolean { + const now = Date.now(); + if (now - this.lastSuggestionTime < this.MIN_SUGGESTION_INTERVAL) { + return false; + } + this.lastSuggestionTime = now; + return true; + } + + private shouldContinueSuggesting(context: string): boolean { + // Get the last word, ignoring emojis and special characters + const words = context + .trim() + .split(/\s+/) + .filter((word) => word.replace(/[\p{Emoji}\p{Symbol}\p{Punctuation}]/gu, "").length > 0); + const lastWord = words[words.length - 1] || ""; + + // If contains CJK characters, always trigger + if (isNonSpaceDelimitedText(lastWord)) { + return true; + } + + // For space-delimited languages (e.g., English), trigger on space or newline + return context.endsWith(" ") || context.endsWith("\n"); + } + + private async *handleCompletion(): AsyncGenerator { + if (!this.isActive) { + logInfo("[Copilot Autocomplete] Autocomplete is not active"); + return; + } + + if (!this.canGenerateNewSuggestion()) { + logInfo("[Copilot Autocomplete] Rate limit - skipping suggestion"); + return; + } + + const view = app.workspace.getActiveViewOfType(MarkdownView); + if (!view) { + logInfo("[Copilot Autocomplete] No active MarkdownView found"); + return; + } + + const editor = view.editor; + const cursor = editor.getCursor(); + const { prefix, noteContext } = getEditorContext(editor, cursor); + + // Check if we should continue suggesting based on context + if (!this.shouldContinueSuggesting(prefix)) { + logInfo("[Copilot Autocomplete] Context indicates no further suggestions needed"); + return; + } + + // Trim context if too long + const trimmedPrefix = prefix.slice(-this.options.maxContextLength); + + if (trimmedPrefix.length < this.options.minTriggerLength) { + logInfo( + `[Copilot Autocomplete] Prefix length ${trimmedPrefix.length} is below minimum trigger length ${this.options.minTriggerLength}` + ); + return; + } + + try { + // Get relevant notes from cache + const relevantNotesStr = await RelevantNotesCache.getInstance().getRelevantNotes(view.file); + const relevantNoteTitles = RelevantNotesCache.getInstance().getRelevantNoteTitles(); + logInfo("[Copilot Autocomplete] Relevant notes:", relevantNoteTitles); + + // Prepend current note title to the prefix + const currentNoteTitle = view.file?.basename || ""; + const prefixWithTitle = currentNoteTitle + ? `[[${currentNoteTitle}]]:\n\n${trimmedPrefix}` + : trimmedPrefix; + + // Get completion from API + const response = await this.client.autocomplete( + prefixWithTitle, + noteContext, + relevantNotesStr + ); + + const completion = response.response.completion; + + yield { + display_suggestion: completion, + complete_suggestion: completion, + }; + } catch (error) { + logError("[Copilot Autocomplete] Error fetching autocomplete suggestions:", error); + } + } + + triggerCompletion() { + if (!this.isActive) { + return; + } + this.forceFetch(); + } +} diff --git a/src/autocomplete/utils.ts b/src/autocomplete/utils.ts new file mode 100644 index 00000000..a2fe3977 --- /dev/null +++ b/src/autocomplete/utils.ts @@ -0,0 +1,195 @@ +import { findRelevantNotes } from "@/search/findRelevantNotes"; +import VectorStoreManager from "@/search/vectorStoreManager"; +import { Editor, TFile } from "obsidian"; + +/** + * Unicode ranges for different writing systems + * Reference: https://unicode.org/charts/ + */ +const WRITING_SYSTEMS = { + // CJK (Chinese, Japanese Kanji, Korean Hanja) + cjk: [ + [0x4e00, 0x9fff], // CJK Unified + [0x3400, 0x4dbf], // CJK Extension A + [0x20000, 0x2a6df], // CJK Extension B + [0x2a700, 0x2b73f], // CJK Extension C + [0x2b740, 0x2b81f], // CJK Extension D + [0x2b820, 0x2ceaf], // CJK Extension E + ], + // Japanese-specific + japanese: [ + [0x3040, 0x309f], // Hiragana + [0x30a0, 0x30ff], // Katakana + ], + // Korean-specific + korean: [ + [0xac00, 0xd7af], // Hangul Syllables + [0x1100, 0x11ff], // Hangul Jamo + ], + // Other non-space-delimited scripts + other: [ + [0x0e00, 0x0e7f], // Thai + [0x0600, 0x06ff], // Arabic + [0x0900, 0x097f], // Devanagari (Hindi) + [0x0980, 0x09ff], // Bengali + [0x0a80, 0x0aff], // Gujarati + [0x0b80, 0x0bff], // Tamil + ], +} satisfies Record>; + +/** + * Tests if a character is within any of the given Unicode ranges + */ +function isInRanges(char: string, ranges: readonly number[][]): boolean { + const code = char.codePointAt(0); + if (!code) return false; + return ranges.some(([start, end]) => code >= start && code <= end); +} + +/** + * Determines if the text contains any non-space-delimited writing systems + * This includes CJK characters, Japanese kana, Korean hangul, and other scripts + * that don't use spaces between words + */ +export function isNonSpaceDelimitedText(text: string): boolean { + // Remove emojis and special characters before testing + const cleanText = text.replace(/[\p{Emoji}\p{Symbol}\p{Punctuation}]/gu, ""); + if (!cleanText) return false; + + // Test each character against all writing systems + for (const char of cleanText) { + if (Object.values(WRITING_SYSTEMS).some((ranges) => isInRanges(char, ranges))) { + return true; + } + } + + return false; +} + +/** + * Gets the context around the cursor + * @param editor The editor instance + * @param cursor The cursor position + * @returns Object containing prefix (a few lines before cursor) and noteContext (50 lines before/after with cursor marker) + */ +export function getEditorContext( + editor: Editor, + cursor: { line: number; ch: number } +): { prefix: string; noteContext: string } { + let prefix = ""; + let noteContext = ""; + + // Get last 5 lines for prefix + const prefixStartLine = Math.max(0, cursor.line - 5); + for (let i = prefixStartLine; i < cursor.line; i++) { + prefix += editor.getLine(i) + "\n"; + } + // Add content from cursor line up to cursor position + const cursorLine = editor.getLine(cursor.line); + prefix += cursorLine.slice(0, cursor.ch); + + // If cursor is at the end of an empty line or after a newline, add a newline to prefix + if (cursor.ch === 0 || (cursor.ch === cursorLine.length && cursorLine.trim() === "")) { + prefix += "\n"; + } + + // Get 50 lines before cursor for noteContext + const contextStartLine = Math.max(0, cursor.line - 50); + for (let i = contextStartLine; i < cursor.line; i++) { + noteContext += editor.getLine(i) + "\n"; + } + + // Add cursor line with {{CURSOR}} marker + noteContext += cursorLine.slice(0, cursor.ch) + "{{CURSOR}}" + cursorLine.slice(cursor.ch) + "\n"; + + // Add up to 50 lines after cursor + const contextEndLine = Math.min(editor.lineCount() - 1, cursor.line + 50); + for (let i = cursor.line + 1; i <= contextEndLine; i++) { + noteContext += editor.getLine(i) + "\n"; + } + + return { prefix, noteContext }; +} + +export class RelevantNotesCache { + private static instance: RelevantNotesCache; + private currentNotePath: string | null = null; + private cachedNotes: string | null = null; + private cachedNoteTitles: string[] = []; + private static readonly MAX_RELEVANT_NOTES = 1; + + private constructor() {} + + static getInstance(): RelevantNotesCache { + if (!RelevantNotesCache.instance) { + RelevantNotesCache.instance = new RelevantNotesCache(); + } + return RelevantNotesCache.instance; + } + + /** + * Gets the titles of currently cached relevant notes + */ + getRelevantNoteTitles(): string[] { + return this.cachedNoteTitles; + } + + /** + * Gets the formatted relevant notes string for the current note. + * If the notes are already cached and the note hasn't changed, returns the cached version. + * Otherwise, fetches and caches new relevant notes. + */ + async getRelevantNotes(file: TFile | null): Promise { + // If no file is active, return empty string + if (!file) { + this.currentNotePath = null; + this.cachedNotes = null; + this.cachedNoteTitles = []; + return ""; + } + + // If we already have cached notes for this file, return them + if (this.currentNotePath === file.path && this.cachedNotes !== null) { + return this.cachedNotes; + } + + // Otherwise, fetch and cache new relevant notes + const db = await VectorStoreManager.getInstance().getDb(); + const relevantNotes = await findRelevantNotes({ db, filePath: file.path }); + + // Get top N relevant notes + const topNotes = relevantNotes.slice(0, RelevantNotesCache.MAX_RELEVANT_NOTES); + + // Update cached titles + this.cachedNoteTitles = topNotes.map((note) => note.document.title); + + // Format notes as required + const formattedNotes = await Promise.all( + topNotes.map(async (note) => { + const noteFile = app.vault.getAbstractFileByPath(note.document.path); + if (!(noteFile instanceof TFile)) return ""; + + const content = await app.vault.cachedRead(noteFile); + if (!content) return ""; + // Truncate content if over 3000 chars + const truncatedContent = content.length > 3000 ? content.slice(0, 3000) + "..." : content; + return `[[${note.document.title}]]:\n\n${truncatedContent}`; + }) + ); + + // Update cache + this.currentNotePath = file.path; + this.cachedNotes = formattedNotes.join("\n\n"); + + return this.cachedNotes; + } + + /** + * Clears the cache, forcing a refresh on the next getRelevantNotes call + */ + clearCache() { + this.currentNotePath = null; + this.cachedNotes = null; + this.cachedNoteTitles = []; + } +} diff --git a/src/cache/autocompleteCache.ts b/src/cache/autocompleteCache.ts new file mode 100644 index 00000000..8f07b898 --- /dev/null +++ b/src/cache/autocompleteCache.ts @@ -0,0 +1,64 @@ +import { logInfo } from "@/logger"; + +interface CacheEntry { + completion: string; + timestamp: number; +} + +export class AutocompleteCache { + private static instance: AutocompleteCache; + private cache: Map; + private readonly maxSize: number; + private readonly ttlMs: number; + + private constructor() { + this.cache = new Map(); + this.maxSize = 100; // Cache up to 100 completions + this.ttlMs = 5 * 60 * 1000; // 5 minutes TTL + } + + static getInstance(): AutocompleteCache { + if (!AutocompleteCache.instance) { + AutocompleteCache.instance = new AutocompleteCache(); + } + return AutocompleteCache.instance; + } + + get(key: string): string | undefined { + const entry = this.cache.get(key); + if (!entry) return undefined; + + // Check if entry has expired + if (Date.now() - entry.timestamp > this.ttlMs) { + this.cache.delete(key); + return undefined; + } + + return entry.completion; + } + + set(key: string, completion: string): void { + // If cache is full, remove oldest entry + if (this.cache.size >= this.maxSize) { + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + + this.cache.set(key, { + completion, + timestamp: Date.now(), + }); + logInfo("Cached autocomplete suggestion for key:", key); + } + + clear(): void { + this.cache.clear(); + logInfo("Cleared autocomplete cache"); + } + + generateKey(prefix: string): string { + // Use last N characters as key to keep context relevant + const keyLength = 100; + return prefix.length <= keyLength ? prefix : prefix.slice(prefix.length - keyLength); + } +} diff --git a/src/cache/projectContextCache.ts b/src/cache/projectContextCache.ts new file mode 100644 index 00000000..071c2d2d --- /dev/null +++ b/src/cache/projectContextCache.ts @@ -0,0 +1,140 @@ +import { ProjectConfig } from "@/aiParams"; +import { logError, logInfo } from "@/logger"; +import { MD5 } from "crypto-js"; + +export class ProjectContextCache { + private static instance: ProjectContextCache; + private cacheDir: string = ".copilot/project-context-cache"; + private memoryCache: Map = new Map(); + + private constructor() {} + + static getInstance(): ProjectContextCache { + if (!ProjectContextCache.instance) { + ProjectContextCache.instance = new ProjectContextCache(); + } + return ProjectContextCache.instance; + } + + private async ensureCacheDir() { + if (!(await app.vault.adapter.exists(this.cacheDir))) { + logInfo("Creating project context cache directory:", this.cacheDir); + await app.vault.adapter.mkdir(this.cacheDir); + } + } + + private getCacheKey(project: ProjectConfig): string { + // Use project ID, system prompt, and context sources for a unique cache key + logInfo("Generating cache key for project context:", project.contextSource); + const metadata = JSON.stringify({ + id: project.id, + contextSource: project.contextSource, + systemPrompt: project.systemPrompt, + }); + const key = MD5(metadata).toString(); + logInfo("Generated cache key for project:", { name: project.name, key }); + return key; + } + + private getCachePath(cacheKey: string): string { + return `${this.cacheDir}/${cacheKey}.json`; + } + + async get(project: ProjectConfig): Promise { + try { + const cacheKey = this.getCacheKey(project); + + // Check memory cache first + const memoryResult = this.memoryCache.get(cacheKey); + if (memoryResult) { + logInfo("Memory cache hit for project:", project.name); + return memoryResult; + } + + const cachePath = this.getCachePath(cacheKey); + if (await app.vault.adapter.exists(cachePath)) { + logInfo("File cache hit for project:", project.name); + const cacheContent = await app.vault.adapter.read(cachePath); + const context = JSON.parse(cacheContent).context; + // Store in memory cache + this.memoryCache.set(cacheKey, context); + return context; + } + logInfo("Cache miss for project:", project.name); + return null; + } catch (error) { + logError("Error reading from project context cache:", error); + return null; + } + } + + getSync(project: ProjectConfig): string | null { + try { + const cacheKey = this.getCacheKey(project); + const memoryResult = this.memoryCache.get(cacheKey); + if (memoryResult) { + logInfo("Memory cache hit for project:", project.name); + return memoryResult; + } + logInfo("Memory cache miss for project:", project.name); + return null; + } catch (error) { + logError("Error reading from project context memory cache:", error); + return null; + } + } + + async set(project: ProjectConfig, context: string): Promise { + try { + await this.ensureCacheDir(); + const cacheKey = this.getCacheKey(project); + const cachePath = this.getCachePath(cacheKey); + logInfo("Caching context for project:", project.name); + // Store in memory cache + this.memoryCache.set(cacheKey, context); + // Store in file cache + await app.vault.adapter.write( + cachePath, + JSON.stringify({ + context, + timestamp: Date.now(), + }) + ); + } catch (error) { + logError("Error writing to project context cache:", error); + } + } + + async clear(): Promise { + try { + // Clear memory cache + this.memoryCache.clear(); + // Clear file cache + if (await app.vault.adapter.exists(this.cacheDir)) { + const files = await app.vault.adapter.list(this.cacheDir); + logInfo("Clearing project context cache, removing files:", files.files.length); + for (const file of files.files) { + await app.vault.adapter.remove(file); + } + } + } catch (error) { + logError("Error clearing project context cache:", error); + } + } + + async clearForProject(project: ProjectConfig): Promise { + try { + const cacheKey = this.getCacheKey(project); + // Clear from memory cache + this.memoryCache.delete(cacheKey); + // Clear from file cache + const cachePath = this.getCachePath(cacheKey); + if (await app.vault.adapter.exists(cachePath)) { + logInfo("Clearing cache for project:", project.name); + await app.vault.adapter.remove(cachePath); + } + } catch (error) { + logError("Error clearing cache for project:", error); + } + } +} diff --git a/src/chainFactory.ts b/src/chainFactory.ts index cb853af1..81e556a2 100644 --- a/src/chainFactory.ts +++ b/src/chainFactory.ts @@ -54,6 +54,7 @@ export enum ChainType { LLM_CHAIN = "llm_chain", VAULT_QA_CHAIN = "vault_qa", COPILOT_PLUS_CHAIN = "copilot_plus", + PROJECT_CHAIN = "project", } class ChainFactory { diff --git a/src/chainUtils.ts b/src/chainUtils.ts index 1351c18d..1ff7d096 100644 --- a/src/chainUtils.ts +++ b/src/chainUtils.ts @@ -1,6 +1,6 @@ -import ChatModelManager from "@/LLMProviders/chatModelManager"; import { removeThinkTags } from "@/utils"; import { BaseChatModelCallOptions } from "@langchain/core/language_models/chat_models"; +import ProjectManager from "@/LLMProviders/projectManager"; export async function getStandaloneQuestion( question: string, @@ -22,8 +22,9 @@ export async function getStandaloneQuestion( .map(([human, ai]) => `Human: ${human}\nAssistant: ${ai}`) .join("\n"); - const chatModel = ChatModelManager.getInstance() - .getChatModel() + const chatModel = ProjectManager.instance + .getCurrentChainManager() + .chatModelManager.getChatModel() .bind({ temperature: 0 } as BaseChatModelCallOptions); const response = await chatModel.invoke([ { diff --git a/src/commands/index.ts b/src/commands/index.ts index b933015c..b1447b74 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -124,14 +124,18 @@ export function registerCommands( addEditorCommand(plugin, COMMAND_IDS.COUNT_WORD_AND_TOKENS_SELECTION, async (editor: Editor) => { const selectedText = await editor.getSelection(); const wordCount = selectedText.split(" ").length; - const tokenCount = await plugin.chainManager.chatModelManager.countTokens(selectedText); + const tokenCount = await plugin.projectManager + .getCurrentChainManager() + .chatModelManager.countTokens(selectedText); new Notice(`Selected text contains ${wordCount} words and ${tokenCount} tokens.`); }); addCommand(plugin, COMMAND_IDS.COUNT_TOTAL_VAULT_TOKENS, async () => { try { const allContent = await getAllQAMarkdownContent(plugin.app); - const totalTokens = await plugin.chainManager.chatModelManager.countTokens(allContent); + const totalTokens = await plugin.projectManager + .getCurrentChainManager() + .chatModelManager.countTokens(allContent); new Notice(`Total tokens in your vault: ${totalTokens}`); } catch (error) { console.error("Error counting tokens: ", error); diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 1ed9640b..6fa93c84 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -1,9 +1,17 @@ -import { useChainType, useModelKey } from "@/aiParams"; +import { + getCurrentProject, + ProjectConfig, + setCurrentProject, + useChainType, + useModelKey, +} from "@/aiParams"; import { ChainType } from "@/chainFactory"; import { updateChatMemory } from "@/chatUtils"; import { ChatControls } from "@/components/chat-components/ChatControls"; import ChatInput from "@/components/chat-components/ChatInput"; import ChatMessages from "@/components/chat-components/ChatMessages"; +import { ProjectList } from "@/components/chat-components/ProjectList"; +import { resetComposerPromptCache } from "@/composerUtils"; import { ABORT_REASON, COMMAND_IDS, EVENT_NAMES, LOADING_MESSAGES, USER_SENDER } from "@/constants"; import { AppContext, EventTargetContext } from "@/context"; import { ContextProcessor } from "@/contextProcessor"; @@ -12,13 +20,15 @@ import { getAIResponse } from "@/langchainStream"; import ChainManager from "@/LLMProviders/chainManager"; import CopilotPlugin from "@/main"; import { Mention } from "@/mentions/Mention"; -import { getSettings, useSettingsValue } from "@/settings/model"; +import { getSettings, updateSetting, useSettingsValue } from "@/settings/model"; import SharedState, { ChatMessage, useSharedState } from "@/sharedState"; import { FileParserManager } from "@/tools/FileParserManager"; import { err2String, formatDateTime } from "@/utils"; +import { Buffer } from "buffer"; import { Notice, TFile } from "obsidian"; import React, { useCallback, useContext, useEffect, useRef, useState } from "react"; -import { Buffer } from "buffer"; + +type ChatMode = "default" | "project"; interface ChatProps { sharedState: SharedState; @@ -27,6 +37,7 @@ interface ChatProps { updateUserMessageHistory: (newMessage: string) => void; fileParserManager: FileParserManager; plugin: CopilotPlugin; + mode?: ChatMode; } const Chat: React.FC = ({ @@ -51,6 +62,10 @@ const Chat: React.FC = ({ const [contextNotes, setContextNotes] = useState([]); const [includeActiveNote, setIncludeActiveNote] = useState(false); const [selectedImages, setSelectedImages] = useState([]); + const [showChatUI, setShowChatUI] = useState(false); + + const [previousMode, setPreviousMode] = useState(null); + const [mode, setSelectedChain] = useChainType(); const mention = Mention.getInstance(); @@ -533,6 +548,47 @@ ${chatContent}`; [addMessage, chainManager.memoryManager, chatHistory, clearMessages] ); + const handleAddProject = useCallback( + (project: ProjectConfig) => { + const currentProjects = settings.projectList || []; + const existingIndex = currentProjects.findIndex((p) => p.name === project.name); + + if (existingIndex >= 0) { + throw new Error(`Project "${project.name}" already exists, please use a different name`); + } + + const newProjectList = [...currentProjects, project]; + updateSetting("projectList", newProjectList); + new Notice(`${project.name} added successfully`); + return true; + }, + [settings.projectList] + ); + + const handleEditProject = useCallback( + (originP: ProjectConfig, updateP: ProjectConfig) => { + const currentProjects = settings.projectList || []; + const existingProject = currentProjects.find((p) => p.name === originP.name); + + if (!existingProject) { + throw new Error(`Project "${originP.name}" does not exist`); + } + + const newProjectList = currentProjects.map((p) => (p.name === originP.name ? updateP : p)); + updateSetting("projectList", newProjectList); + + // If this is the current project, update the current project atom + const currentProject = getCurrentProject(); + if (currentProject?.id === originP.id) { + setCurrentProject(updateP); + } + + new Notice(`${originP.name} updated successfully`); + return true; + }, + [settings.projectList] + ); + const handleInsertToChat = useCallback((prompt: string) => { setInputMessage((prev) => `${prev} ${prompt} `); }, []); @@ -544,6 +600,8 @@ ${chatContent}`; } clearMessages(); chainManager.memoryManager.clearChatMemory(); + // Reset the composer prompt cache when starting a new chat + resetComposerPromptCache(); setCurrentAiMessage(""); setContextNotes([]); setIncludeActiveNote(false); @@ -556,22 +614,35 @@ ${chatContent}`; handleSaveAsNote, ]); - return ( -
- -
- handleSaveAsNote(true)} /> + const renderChatComponents = () => ( + <> +
+ +
+
+ handleSaveAsNote(true)} + onModeChange={(newMode) => { + setPreviousMode(mode); + // Hide chat UI when switching to project mode + if (newMode === ChainType.PROJECT_CHAIN) { + setShowChatUI(false); + } + }} + /> setSelectedImages((prev) => [...prev, ...files])} setSelectedImages={setSelectedImages} + disableModelSwitch={mode === "project"} />
+ + ); + + return ( +
+
+
+ {mode === ChainType.PROJECT_CHAIN && ( +
+ { + if (previousMode) { + setSelectedChain(previousMode); + setPreviousMode(null); + } else { + // default back to chat or plus mode + setSelectedChain( + settings.isPlusUser ? ChainType.COPILOT_PLUS_CHAIN : ChainType.LLM_CHAIN + ); + } + }} + showChatUI={(v) => setShowChatUI(v)} + /> +
+ )} + {(mode !== "project" || (mode === "project" && showChatUI)) && renderChatComponents()} +
+
); }; diff --git a/src/components/CopilotView.tsx b/src/components/CopilotView.tsx index e1f7d8b7..337dcd5c 100644 --- a/src/components/CopilotView.tsx +++ b/src/components/CopilotView.tsx @@ -8,10 +8,13 @@ import { FileParserManager } from "@/tools/FileParserManager"; import * as Tooltip from "@radix-ui/react-tooltip"; import { ItemView, WorkspaceLeaf } from "obsidian"; import * as React from "react"; -import { Root, createRoot } from "react-dom/client"; +import { createRoot, Root } from "react-dom/client"; export default class CopilotView extends ItemView { - private chainManager: ChainManager; + private get chainManager(): ChainManager { + return this.plugin.projectManager.getCurrentChainManager(); + } + private fileParserManager: FileParserManager; private root: Root | null = null; private handleSaveAsNote: (() => Promise) | null = null; @@ -25,7 +28,6 @@ export default class CopilotView extends ItemView { super(leaf); this.sharedState = plugin.sharedState; this.app = plugin.app; - this.chainManager = plugin.chainManager; this.fileParserManager = plugin.fileParserManager; this.eventTarget = new EventTarget(); this.plugin = plugin; @@ -50,14 +52,24 @@ export default class CopilotView extends ItemView { } async onOpen(): Promise { - const root = createRoot(this.containerEl.children[1]); + this.root = createRoot(this.containerEl.children[1]); const handleSaveAsNote = (saveFunction: () => Promise) => { this.handleSaveAsNote = saveFunction; }; const updateUserMessageHistory = (newMessage: string) => { this.plugin.updateUserMessageHistory(newMessage); }; - root.render( + + this.renderView(handleSaveAsNote, updateUserMessageHistory); + } + + private renderView( + handleSaveAsNote: (saveFunction: () => Promise) => void, + updateUserMessageHistory: (newMessage: string) => void + ): void { + if (!this.root) return; + + this.root.render( @@ -83,14 +95,26 @@ export default class CopilotView extends ItemView { } } + updateView(): void { + // load currentChainManager chatMessages + this.sharedState.replaceMessages( + this.plugin.projectManager.getCurrentChainManager().getChatMessages() + ); + + const handleSaveAsNote = (saveFunction: () => Promise) => { + this.handleSaveAsNote = saveFunction; + }; + const updateUserMessageHistory = (newMessage: string) => { + this.plugin.updateUserMessageHistory(newMessage); + }; + + this.renderView(handleSaveAsNote, updateUserMessageHistory); + } + async onClose(): Promise { if (this.root) { this.root.unmount(); + this.root = null; } } - - updateView(): void { - // Force a re-render of the React component - this.onOpen(); - } } diff --git a/src/components/chat-components/ChatControls.tsx b/src/components/chat-components/ChatControls.tsx index 25e896be..66ebfc41 100644 --- a/src/components/chat-components/ChatControls.tsx +++ b/src/components/chat-components/ChatControls.tsx @@ -1,12 +1,13 @@ import React from "react"; import { - Download, - MoreHorizontal, - Sparkles, - FileText, - RefreshCw, - MessageCirclePlus, ChevronDown, + Download, + FileText, + LibraryBig, + MessageCirclePlus, + MoreHorizontal, + RefreshCw, + Sparkles, SquareArrowOutUpRight, } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -15,7 +16,7 @@ import { DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown- import { SettingSwitch } from "@/components/ui/setting-switch"; import { updateSetting, useSettingsValue } from "@/settings/model"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { useChainType } from "@/aiParams"; +import { setCurrentProject, useChainType } from "@/aiParams"; import { ChainType } from "@/chainFactory"; import { Notice } from "obsidian"; import VectorStoreManager from "@/search/vectorStoreManager"; @@ -35,13 +36,29 @@ export async function refreshVaultIndex() { interface ChatControlsProps { onNewChat: () => void; onSaveAsNote: () => void; + onModeChange: (mode: ChainType) => void; + onCloseProject?: () => void; } -export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) { +export function ChatControls({ + onNewChat, + onSaveAsNote, + onModeChange, + onCloseProject, +}: ChatControlsProps) { const settings = useSettingsValue(); const [selectedChain, setSelectedChain] = useChainType(); const isPlusUser = useIsPlusUser(); + const handleModeChange = (chainType: ChainType) => { + setSelectedChain(chainType); + onModeChange(chainType); + if (chainType !== ChainType.PROJECT_CHAIN) { + setCurrentProject(null); + onCloseProject?.(); + } + }; + return (
@@ -56,18 +73,31 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) { copilot plus (beta)
)} + {selectedChain === ChainType.PROJECT_CHAIN && "plus projects (alpha)"} - setSelectedChain(ChainType.LLM_CHAIN)}> + { + handleModeChange(ChainType.LLM_CHAIN); + }} + > chat - setSelectedChain(ChainType.VAULT_QA_CHAIN)}> + { + handleModeChange(ChainType.VAULT_QA_CHAIN); + }} + > vault QA {isPlusUser ? ( - setSelectedChain(ChainType.COPILOT_PLUS_CHAIN)}> + { + handleModeChange(ChainType.COPILOT_PLUS_CHAIN); + }} + >
copilot plus (beta) @@ -77,6 +107,29 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) { { navigateToPlusPage(PLUS_UTM_MEDIUMS.CHAT_MODE_SELECT); + onCloseProject?.(); + }} + > + copilot plus (beta) + + + )} + + {isPlusUser ? ( + { + handleModeChange(ChainType.PROJECT_CHAIN); + }} + > + + plus projects (alpha) + + ) : ( + { + navigateToPlusPage(PLUS_UTM_MEDIUMS.CHAT_MODE_SELECT); + onCloseProject?.(); }} > copilot plus (beta) diff --git a/src/components/chat-components/ChatInput.tsx b/src/components/chat-components/ChatInput.tsx index f77d5313..f8370f52 100644 --- a/src/components/chat-components/ChatInput.tsx +++ b/src/components/chat-components/ChatInput.tsx @@ -1,4 +1,11 @@ -import { useChainType, useModelKey } from "@/aiParams"; +import { + useChainType, + useModelKey, + useProjectLoading, + ProjectConfig, + getCurrentProject, + subscribeToProjectChange, +} from "@/aiParams"; import { ChainType } from "@/chainFactory"; import { AddContextNoteModal } from "@/components/modals/AddContextNoteModal"; import { AddImageModal } from "@/components/modals/AddImageModal"; @@ -24,9 +31,9 @@ import { Command, CornerDownLeft, Image, + Loader2, StopCircle, X, - Loader2, } from "lucide-react"; import { App, Notice, Platform, TFile } from "obsidian"; import React, { @@ -61,6 +68,7 @@ interface ChatInputProps { selectedImages: File[]; onAddImage: (files: File[]) => void; setSelectedImages: React.Dispatch>; + disableModelSwitch?: boolean; } const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>( @@ -81,6 +89,7 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>( selectedImages, onAddImage, setSelectedImages, + disableModelSwitch, }, ref ) => { @@ -93,11 +102,14 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>( const [currentModelKey, setCurrentModelKey] = useModelKey(); const [modelError, setModelError] = useState(null); const [currentChain] = useChainType(); + const [isProjectLoading] = useProjectLoading(); const [currentActiveNote, setCurrentActiveNote] = useState( app.workspace.getActiveFile() ); + const [selectedProject, setSelectedProject] = useState(null); const settings = useSettingsValue(); - const isCopilotPlus = currentChain === ChainType.COPILOT_PLUS_CHAIN; + const isCopilotPlus = + currentChain === ChainType.COPILOT_PLUS_CHAIN || currentChain === ChainType.PROJECT_CHAIN; useImperativeHandle(ref, () => ({ focus: () => { @@ -105,6 +117,33 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>( }, })); + useEffect(() => { + if (currentChain === ChainType.PROJECT_CHAIN) { + setSelectedProject(getCurrentProject()); + + const unsubscribe = subscribeToProjectChange((project) => { + setSelectedProject(project); + }); + + return () => { + unsubscribe(); + }; + } else { + setSelectedProject(null); + } + }, [currentChain]); + + const getDisplayModelKey = (): string => { + if ( + selectedProject && + currentChain === ChainType.PROJECT_CHAIN && + selectedProject.projectModelKey + ) { + return selectedProject.projectModelKey; + } + return currentModelKey; + }; + const onSendMessage = (includeVault: boolean) => { if (!isCopilotPlus) { handleSendMessage(); @@ -458,6 +497,14 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>( )}
+ {isProjectLoading && ( +
+
+ + Loading the project context... +
+
+ )}