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... +
+
+ )}