mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
Squashed 2.9.0 preview 250327
Update preview version Merge latest from Composer Support creating new files Update version to 2.9.0 preview Add minor fixes Update autocomplete call and prefix logic Merge Project mode into 2.9.0-preview Squashed commits from: commita762629deeAuthor: Logan Yang <logancyang@gmail.com> Date: Tue Mar 25 16:29:31 2025 -0700 Add projectEnabled flag to CustomModel interface To commit1694357823Author: wyh <emt934841028@gmail.com> Date: Wed Feb 19 14:45:22 2025 +0800 feat: Support project-based feature. Fix package vulnerabilities Prerelease 250325 Update label for PROJECT_CHAIN to "Plus Projects (alpha)" in BasicSettings component
This commit is contained in:
parent
68bec985df
commit
b35290568a
51 changed files with 3863 additions and 496 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
705
package-lock.json
generated
705
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<AutocompleteResponse> {
|
||||
const { data, error } = await this.makeRequest<AutocompleteResponse>("/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<ComposerPromptResponse> {
|
||||
const { data, error } = await this.makeRequest<ComposerPromptResponse>(
|
||||
"/composer/prompt",
|
||||
{},
|
||||
"GET"
|
||||
);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
if (!data) {
|
||||
throw new Error("No data returned from composerPrompt");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async composerApply(request: ComposerApplyRequest): Promise<ComposerApplyResponse> {
|
||||
const { data, error } = await this.makeRequest<ComposerApplyResponse>(
|
||||
"/composer/apply",
|
||||
request
|
||||
);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
if (!data) {
|
||||
throw new Error("No data returned from composerApply");
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
let newModelKey = getModelKey();
|
||||
async createChainWithNewModel(
|
||||
options: SetChainOptions = {},
|
||||
neededReInitChatMode: boolean = true
|
||||
): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
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<string> {
|
||||
// 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 };
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
const modelKey = getModelKeyFromModel(model);
|
||||
setModelKey(modelKey);
|
||||
// todo 为啥这里需要重新设置?
|
||||
// setModelKey(modelKey);
|
||||
try {
|
||||
const modelInstance = await this.createModelInstance(model);
|
||||
ChatModelManager.chatModel = modelInstance;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
339
src/LLMProviders/projectManager.ts
Normal file
339
src/LLMProviders/projectManager.ts
Normal file
|
|
@ -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<string, ChatMessage[]>;
|
||||
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<void> {
|
||||
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<void> {
|
||||
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:
|
||||
|
||||
<ProjectContext>
|
||||
${contextParts.join("\n\n")}
|
||||
</ProjectContext>
|
||||
`;
|
||||
|
||||
// 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<void> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
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("");
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -34,6 +34,29 @@ const chainTypeAtom = atom(
|
|||
}
|
||||
);
|
||||
|
||||
const currentProjectAtom = atom<ProjectConfig | null>(null);
|
||||
const projectLoadingAtom = atom<boolean>(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;
|
||||
}
|
||||
|
|
|
|||
58
src/autocomplete/autocompleteService.ts
Normal file
58
src/autocomplete/autocompleteService.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
154
src/autocomplete/codemirrorIntegration.ts
Normal file
154
src/autocomplete/codemirrorIntegration.ts
Normal file
|
|
@ -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<Suggestion> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
195
src/autocomplete/utils.ts
Normal file
195
src/autocomplete/utils.ts
Normal file
|
|
@ -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<string, Array<[number, number]>>;
|
||||
|
||||
/**
|
||||
* 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<string> {
|
||||
// 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 = [];
|
||||
}
|
||||
}
|
||||
64
src/cache/autocompleteCache.ts
vendored
Normal file
64
src/cache/autocompleteCache.ts
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { logInfo } from "@/logger";
|
||||
|
||||
interface CacheEntry {
|
||||
completion: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export class AutocompleteCache {
|
||||
private static instance: AutocompleteCache;
|
||||
private cache: Map<string, CacheEntry>;
|
||||
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);
|
||||
}
|
||||
}
|
||||
140
src/cache/projectContextCache.ts
vendored
Normal file
140
src/cache/projectContextCache.ts
vendored
Normal file
|
|
@ -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<string, string> = 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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<ChatProps> = ({
|
||||
|
|
@ -51,6 +62,10 @@ const Chat: React.FC<ChatProps> = ({
|
|||
const [contextNotes, setContextNotes] = useState<TFile[]>([]);
|
||||
const [includeActiveNote, setIncludeActiveNote] = useState(false);
|
||||
const [selectedImages, setSelectedImages] = useState<File[]>([]);
|
||||
const [showChatUI, setShowChatUI] = useState(false);
|
||||
|
||||
const [previousMode, setPreviousMode] = useState<ChainType | null>(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 (
|
||||
<div className="chat-container">
|
||||
<ChatMessages
|
||||
chatHistory={chatHistory}
|
||||
currentAiMessage={currentAiMessage}
|
||||
loading={loading}
|
||||
loadingMessage={loadingMessage}
|
||||
app={app}
|
||||
onRegenerate={handleRegenerate}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onInsertToChat={handleInsertToChat}
|
||||
onReplaceChat={setInputMessage}
|
||||
/>
|
||||
<div className="bottom-container">
|
||||
<ChatControls onNewChat={handleNewChat} onSaveAsNote={() => handleSaveAsNote(true)} />
|
||||
const renderChatComponents = () => (
|
||||
<>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ChatMessages
|
||||
chatHistory={chatHistory}
|
||||
currentAiMessage={currentAiMessage}
|
||||
loading={loading}
|
||||
loadingMessage={loadingMessage}
|
||||
app={app}
|
||||
onRegenerate={handleRegenerate}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
onInsertToChat={handleInsertToChat}
|
||||
onReplaceChat={setInputMessage}
|
||||
showHelperComponents={mode !== "project"}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-end w-full flex-none box-border sticky bottom-0 border-t border-border">
|
||||
<ChatControls
|
||||
onNewChat={handleNewChat}
|
||||
onSaveAsNote={() => handleSaveAsNote(true)}
|
||||
onModeChange={(newMode) => {
|
||||
setPreviousMode(mode);
|
||||
// Hide chat UI when switching to project mode
|
||||
if (newMode === ChainType.PROJECT_CHAIN) {
|
||||
setShowChatUI(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatInput
|
||||
ref={inputRef}
|
||||
inputMessage={inputMessage}
|
||||
|
|
@ -589,8 +660,44 @@ ${chatContent}`;
|
|||
selectedImages={selectedImages}
|
||||
onAddImage={(files: File[]) => setSelectedImages((prev) => [...prev, ...files])}
|
||||
setSelectedImages={setSelectedImages}
|
||||
disableModelSwitch={mode === "project"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="chat-container">
|
||||
<div className="h-full">
|
||||
<div className="flex flex-col h-full relative">
|
||||
{mode === ChainType.PROJECT_CHAIN && (
|
||||
<div className={`${mode === ChainType.PROJECT_CHAIN ? "z-modal" : ""}`}>
|
||||
<ProjectList
|
||||
projects={settings.projectList || []}
|
||||
defaultOpen={true}
|
||||
app={app}
|
||||
hasMessages={false}
|
||||
onProjectAdded={handleAddProject}
|
||||
onEditProject={handleEditProject}
|
||||
inputRef={inputRef}
|
||||
onClose={() => {
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{(mode !== "project" || (mode === "project" && showChatUI)) && renderChatComponents()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<void>) | 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<void> {
|
||||
const root = createRoot(this.containerEl.children[1]);
|
||||
this.root = createRoot(this.containerEl.children[1]);
|
||||
const handleSaveAsNote = (saveFunction: () => Promise<void>) => {
|
||||
this.handleSaveAsNote = saveFunction;
|
||||
};
|
||||
const updateUserMessageHistory = (newMessage: string) => {
|
||||
this.plugin.updateUserMessageHistory(newMessage);
|
||||
};
|
||||
root.render(
|
||||
|
||||
this.renderView(handleSaveAsNote, updateUserMessageHistory);
|
||||
}
|
||||
|
||||
private renderView(
|
||||
handleSaveAsNote: (saveFunction: () => Promise<void>) => void,
|
||||
updateUserMessageHistory: (newMessage: string) => void
|
||||
): void {
|
||||
if (!this.root) return;
|
||||
|
||||
this.root.render(
|
||||
<AppContext.Provider value={this.app}>
|
||||
<EventTargetContext.Provider value={this.eventTarget}>
|
||||
<React.StrictMode>
|
||||
|
|
@ -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<void>) => {
|
||||
this.handleSaveAsNote = saveFunction;
|
||||
};
|
||||
const updateUserMessageHistory = (newMessage: string) => {
|
||||
this.plugin.updateUserMessageHistory(newMessage);
|
||||
};
|
||||
|
||||
this.renderView(handleSaveAsNote, updateUserMessageHistory);
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
if (this.root) {
|
||||
this.root.unmount();
|
||||
this.root = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateView(): void {
|
||||
// Force a re-render of the React component
|
||||
this.onOpen();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="w-full py-1 flex justify-between items-center px-1">
|
||||
<div className="flex-1">
|
||||
|
|
@ -56,18 +73,31 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) {
|
|||
copilot plus (beta)
|
||||
</div>
|
||||
)}
|
||||
{selectedChain === ChainType.PROJECT_CHAIN && "plus projects (alpha)"}
|
||||
<ChevronDown className="size-5 mt-0.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.LLM_CHAIN)}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
handleModeChange(ChainType.LLM_CHAIN);
|
||||
}}
|
||||
>
|
||||
chat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.VAULT_QA_CHAIN)}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
handleModeChange(ChainType.VAULT_QA_CHAIN);
|
||||
}}
|
||||
>
|
||||
vault QA
|
||||
</DropdownMenuItem>
|
||||
{isPlusUser ? (
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.COPILOT_PLUS_CHAIN)}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
handleModeChange(ChainType.COPILOT_PLUS_CHAIN);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Sparkles className="size-4" />
|
||||
copilot plus (beta)
|
||||
|
|
@ -77,6 +107,29 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) {
|
|||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigateToPlusPage(PLUS_UTM_MEDIUMS.CHAT_MODE_SELECT);
|
||||
onCloseProject?.();
|
||||
}}
|
||||
>
|
||||
copilot plus (beta)
|
||||
<SquareArrowOutUpRight className="size-3" />
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{isPlusUser ? (
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-1"
|
||||
onSelect={() => {
|
||||
handleModeChange(ChainType.PROJECT_CHAIN);
|
||||
}}
|
||||
>
|
||||
<LibraryBig className="size-4" />
|
||||
plus projects (alpha)
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigateToPlusPage(PLUS_UTM_MEDIUMS.CHAT_MODE_SELECT);
|
||||
onCloseProject?.();
|
||||
}}
|
||||
>
|
||||
copilot plus (beta)
|
||||
|
|
|
|||
|
|
@ -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<React.SetStateAction<File[]>>;
|
||||
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<string | null>(null);
|
||||
const [currentChain] = useChainType();
|
||||
const [isProjectLoading] = useProjectLoading();
|
||||
const [currentActiveNote, setCurrentActiveNote] = useState<TFile | null>(
|
||||
app.workspace.getActiveFile()
|
||||
);
|
||||
const [selectedProject, setSelectedProject] = useState<ProjectConfig | null>(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>(
|
|||
)}
|
||||
|
||||
<div className="relative" {...(isCopilotPlus ? getRootProps() : {})}>
|
||||
{isProjectLoading && (
|
||||
<div className="absolute inset-0 z-modal bg-background/80 backdrop-blur-sm flex items-center justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span className="text-sm">Loading the project context...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
className="w-full bg-transparent focus-visible:ring-0 border-none min-h-[60px] max-h-40 overflow-y-auto resize-none px-2 rounded-md text-sm text-normal"
|
||||
|
|
@ -469,6 +516,7 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
disabled={isProjectLoading}
|
||||
/>
|
||||
{isCopilotPlus && (
|
||||
<>
|
||||
|
|
@ -492,17 +540,18 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
) : (
|
||||
<DropdownMenu open={isModelDropdownOpen} onOpenChange={setIsModelDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost2" size="fit">
|
||||
<Button variant="ghost2" size="fit" disabled={disableModelSwitch}>
|
||||
{modelError ? (
|
||||
<span className="text-error">Model Load Failed</span>
|
||||
) : settings.activeModels.find(
|
||||
(model) => model.enabled && getModelKeyFromModel(model) === currentModelKey
|
||||
(model) =>
|
||||
model.enabled && getModelKeyFromModel(model) === getDisplayModelKey()
|
||||
) ? (
|
||||
<ModelDisplay
|
||||
model={
|
||||
settings.activeModels.find(
|
||||
(model) =>
|
||||
model.enabled && getModelKeyFromModel(model) === currentModelKey
|
||||
model.enabled && getModelKeyFromModel(model) === getDisplayModelKey()
|
||||
)!
|
||||
}
|
||||
iconSize={8}
|
||||
|
|
@ -510,47 +559,48 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
) : (
|
||||
"Select Model"
|
||||
)}
|
||||
<ChevronDown className="size-5 mt-0.5" />
|
||||
{!disableModelSwitch && <ChevronDown className="size-5 mt-0.5" />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start">
|
||||
{settings.activeModels
|
||||
.filter((model) => model.enabled)
|
||||
.map((model) => {
|
||||
const { hasApiKey, errorNotice } = checkModelApiKey(model, settings);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={getModelKeyFromModel(model)}
|
||||
onSelect={async (event) => {
|
||||
if (!hasApiKey && errorNotice) {
|
||||
event.preventDefault();
|
||||
new Notice(errorNotice);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setModelError(null);
|
||||
setCurrentModelKey(getModelKeyFromModel(model));
|
||||
} catch (error) {
|
||||
const msg = `Model switch failed: ` + err2String(error);
|
||||
setModelError(msg);
|
||||
new Notice(msg);
|
||||
// Restore to the last valid model
|
||||
const lastValidModel = settings.activeModels.find(
|
||||
(m) => m.enabled && getModelKeyFromModel(m) === currentModelKey
|
||||
);
|
||||
if (lastValidModel) {
|
||||
setCurrentModelKey(getModelKeyFromModel(lastValidModel));
|
||||
{!disableModelSwitch &&
|
||||
settings.activeModels
|
||||
.filter((model) => model.enabled)
|
||||
.map((model) => {
|
||||
const { hasApiKey, errorNotice } = checkModelApiKey(model, settings);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={getModelKeyFromModel(model)}
|
||||
onSelect={async (event) => {
|
||||
if (!hasApiKey && errorNotice) {
|
||||
event.preventDefault();
|
||||
new Notice(errorNotice);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={!hasApiKey ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
<ModelDisplay model={model} iconSize={12} />
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
try {
|
||||
setModelError(null);
|
||||
setCurrentModelKey(getModelKeyFromModel(model));
|
||||
} catch (error) {
|
||||
const msg = `Model switch failed: ` + err2String(error);
|
||||
setModelError(msg);
|
||||
new Notice(msg);
|
||||
// Restore to the last valid model
|
||||
const lastValidModel = settings.activeModels.find(
|
||||
(m) => m.enabled && getModelKeyFromModel(m) === currentModelKey
|
||||
);
|
||||
if (lastValidModel) {
|
||||
setCurrentModelKey(getModelKeyFromModel(lastValidModel));
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={!hasApiKey ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
<ModelDisplay model={model} iconSize={12} />
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ interface ChatMessagesProps {
|
|||
onDelete: (messageIndex: number) => void;
|
||||
onInsertToChat: (prompt: string) => void;
|
||||
onReplaceChat: (prompt: string) => void;
|
||||
showHelperComponents: boolean;
|
||||
}
|
||||
|
||||
const ChatMessages = memo(
|
||||
|
|
@ -31,6 +32,7 @@ const ChatMessages = memo(
|
|||
onDelete,
|
||||
onInsertToChat,
|
||||
onReplaceChat,
|
||||
showHelperComponents = true,
|
||||
}: ChatMessagesProps) => {
|
||||
const [loadingDots, setLoadingDots] = useState("");
|
||||
|
||||
|
|
@ -63,14 +65,16 @@ const ChatMessages = memo(
|
|||
if (!chatHistory.filter((message) => message.isVisible).length && !currentAiMessage) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 overflow-y-auto w-full h-full">
|
||||
{settings.showRelevantNotes && (
|
||||
{showHelperComponents && settings.showRelevantNotes && (
|
||||
<RelevantNotes
|
||||
onInsertToChat={onInsertToChat}
|
||||
defaultOpen={true}
|
||||
key="relevant-notes-before-chat"
|
||||
/>
|
||||
)}
|
||||
{settings.showSuggestedPrompts && <SuggestedPrompts onClick={onReplaceChat} />}
|
||||
{showHelperComponents && settings.showSuggestedPrompts && (
|
||||
<SuggestedPrompts onClick={onReplaceChat} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -81,7 +85,7 @@ const ChatMessages = memo(
|
|||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 h-full overflow-hidden">
|
||||
{settings.showRelevantNotes && (
|
||||
{showHelperComponents && settings.showRelevantNotes && (
|
||||
<RelevantNotes
|
||||
className="mb-4"
|
||||
onInsertToChat={onInsertToChat}
|
||||
|
|
@ -104,6 +108,7 @@ const ChatMessages = memo(
|
|||
onRegenerate={() => onRegenerate(index)}
|
||||
onEdit={(newMessage) => onEdit(index, newMessage)}
|
||||
onDelete={() => onDelete(index)}
|
||||
chatHistory={chatHistory}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
@ -119,6 +124,7 @@ const ChatMessages = memo(
|
|||
app={app}
|
||||
isStreaming={true}
|
||||
onDelete={() => {}}
|
||||
chatHistory={chatHistory}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import { SourcesModal } from "@/components/modals/SourcesModal";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { USER_SENDER } from "@/constants";
|
||||
import { useApplyCode } from "@/hooks/useApplyCode";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { insertIntoEditor } from "@/utils";
|
||||
import { Bot, User } from "lucide-react";
|
||||
import { App, Component, MarkdownRenderer, MarkdownView, TFile } from "obsidian";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
|
||||
function MessageContext({ context }: { context: ChatMessage["context"] }) {
|
||||
if (!context || (context.notes.length === 0 && context.urls.length === 0)) {
|
||||
|
|
@ -48,6 +51,7 @@ interface ChatSingleMessageProps {
|
|||
onRegenerate?: () => void;
|
||||
onEdit?: (newMessage: string) => void;
|
||||
onDelete: () => void;
|
||||
chatHistory?: ChatMessage[];
|
||||
}
|
||||
|
||||
const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
|
||||
|
|
@ -57,6 +61,7 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
|
|||
onRegenerate,
|
||||
onEdit,
|
||||
onDelete,
|
||||
chatHistory = [],
|
||||
}) => {
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
|
|
@ -65,6 +70,8 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
|
|||
const componentRef = useRef<Component | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const handleApplyCode = useApplyCode(app, chatHistory);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
return;
|
||||
|
|
@ -199,8 +206,10 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
const roots: Root[] = [];
|
||||
let isUnmounting = false;
|
||||
|
||||
if (contentRef.current && message.sender !== USER_SENDER) {
|
||||
// Clear previous content
|
||||
contentRef.current.innerHTML = "";
|
||||
|
||||
// Create a new Component instance if it doesn't exist
|
||||
|
|
@ -210,23 +219,77 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
|
|||
|
||||
const processedMessage = preprocess(message.message);
|
||||
|
||||
// Use Obsidian's MarkdownRenderer to render the message
|
||||
MarkdownRenderer.renderMarkdown(
|
||||
processedMessage,
|
||||
contentRef.current,
|
||||
"", // Empty string for sourcePath as we don't have a specific source file
|
||||
componentRef.current
|
||||
);
|
||||
if (!isUnmounting) {
|
||||
// Use Obsidian's MarkdownRenderer to render the message
|
||||
MarkdownRenderer.renderMarkdown(
|
||||
processedMessage,
|
||||
contentRef.current,
|
||||
"", // Empty string for sourcePath as we don't have a specific source file
|
||||
componentRef.current
|
||||
);
|
||||
|
||||
// Process code blocks after rendering
|
||||
const codeBlocks = contentRef.current.querySelectorAll("pre");
|
||||
if (codeBlocks.length > 0) {
|
||||
codeBlocks.forEach((pre) => {
|
||||
if (isUnmounting) return;
|
||||
|
||||
const codeElement = pre.querySelector("code");
|
||||
if (!codeElement) return;
|
||||
|
||||
const originalCode = codeElement.textContent || "";
|
||||
const lines = originalCode.split("\n");
|
||||
const firstLine = lines[0].trim();
|
||||
|
||||
// Check for path in HTML comment format: <!-- path=Notes/My Notes.md -->
|
||||
const htmlCommentMatch = firstLine.match(/<!--\s*path=([^>]+?)\s*-->/);
|
||||
if (htmlCommentMatch && htmlCommentMatch[1]) {
|
||||
const path = htmlCommentMatch[1].trim();
|
||||
const cleanedCode = lines.slice(1).join("\n");
|
||||
|
||||
// Create a container for the React component
|
||||
const container = document.createElement("div");
|
||||
pre.parentNode?.replaceChild(container, pre);
|
||||
|
||||
// Create a root and render the CodeBlock component
|
||||
const root = createRoot(container);
|
||||
roots.push(root);
|
||||
if (!isUnmounting) {
|
||||
root.render(
|
||||
<CodeBlock
|
||||
code={cleanedCode}
|
||||
path={path}
|
||||
onApply={isStreaming ? undefined : handleApplyCode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (componentRef.current) {
|
||||
componentRef.current.unload();
|
||||
componentRef.current = null;
|
||||
}
|
||||
isUnmounting = true;
|
||||
|
||||
// Schedule cleanup to run after current render cycle
|
||||
setTimeout(() => {
|
||||
if (componentRef.current) {
|
||||
componentRef.current.unload();
|
||||
componentRef.current = null;
|
||||
}
|
||||
|
||||
roots.forEach((root) => {
|
||||
try {
|
||||
root.unmount();
|
||||
} catch {
|
||||
// Ignore unmount errors during cleanup
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
}, [message, app, componentRef, isStreaming, preprocess]);
|
||||
}, [message, app, componentRef, isStreaming, preprocess, handleApplyCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && textareaRef.current) {
|
||||
|
|
@ -361,7 +424,7 @@ const ChatSingleMessage: React.FC<ChatSingleMessageProps> = ({
|
|||
)}
|
||||
>
|
||||
<div className="w-6 shrink-0">{message.sender === USER_SENDER ? <User /> : <Bot />}</div>
|
||||
<div className="flex flex-col flex-grow max-w-full gap-2">
|
||||
<div className="flex flex-col flex-grow max-w-full gap-2 overflow-hidden">
|
||||
{!isEditing && <MessageContext context={message.context} />}
|
||||
<div className="message-content">{renderMessageContent()}</div>
|
||||
|
||||
|
|
|
|||
54
src/components/chat-components/CodeBlock.tsx
Normal file
54
src/components/chat-components/CodeBlock.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Check, Loader2 } from "lucide-react";
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
path?: string;
|
||||
onApply?: (path: string, code: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, path, onApply }) => {
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
const handleApply = async () => {
|
||||
if (!path || !onApply) return;
|
||||
|
||||
setIsApplying(true);
|
||||
try {
|
||||
await onApply(path, code);
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-border border-solid rounded-md my-2 flex flex-col overflow-hidden">
|
||||
{path && (
|
||||
<div className="flex justify-between items-center border-[0px] border-b border-border border-solid gap-2 p-2 overflow-hidden">
|
||||
<div className="text-xs p-1 text-muted-foreground truncate flex-1">{path}</div>
|
||||
{onApply && (
|
||||
<Button
|
||||
className="text-muted"
|
||||
variant="ghost2"
|
||||
size="fit"
|
||||
onClick={handleApply}
|
||||
disabled={isApplying}
|
||||
>
|
||||
{isApplying ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Check className="h-4 w-4" />
|
||||
)}
|
||||
Apply
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<pre className="m-0 border-none">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
404
src/components/chat-components/ProjectList.tsx
Normal file
404
src/components/chat-components/ProjectList.tsx
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
import { ProjectConfig, setCurrentProject } from "@/aiParams";
|
||||
import { AddProjectModal } from "@/components/modals/AddProjectModal";
|
||||
import { ConfirmModal } from "@/components/modals/ConfirmModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { logError } from "@/logger";
|
||||
import { updateSetting } from "@/settings/model";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Edit2,
|
||||
Folder,
|
||||
Info,
|
||||
MessageSquare,
|
||||
Plus,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { App, Notice } from "obsidian";
|
||||
import React, { memo, useEffect, useState } from "react";
|
||||
|
||||
function ProjectItem({
|
||||
project,
|
||||
loadContext,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
project: ProjectConfig;
|
||||
loadContext: (project: ProjectConfig) => void;
|
||||
onEdit: (project: ProjectConfig) => void;
|
||||
onDelete: (project: ProjectConfig) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex gap-2 p-3 justify-between items-center rounded-lg bg-secondary/40 border border-border border-solid group transition-all duration-200 hover:border-interactive-accent/30 hover:bg-interactive-accent/5 hover:shadow-[0_2px_12px_rgba(0,0,0,0.1)] active:scale-[0.98] cursor-pointer"
|
||||
onClick={() => loadContext(project)}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 overflow-hidden">
|
||||
<div className="text-blue-400">
|
||||
<Folder className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 flex-1 overflow-hidden">
|
||||
<span className="text-[13px] font-medium text-normal text-ellipsis overflow-hidden whitespace-nowrap w-full">
|
||||
{project.name}
|
||||
</span>
|
||||
{project.description && (
|
||||
<span className="text-[12px] text-muted/80 text-ellipsis overflow-hidden whitespace-nowrap w-full">
|
||||
{project.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 opacity-100 transition-opacity duration-200">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost2"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-accent/10 hover:text-accent-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(project);
|
||||
}}
|
||||
>
|
||||
<Edit2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Edit Project</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost2"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-accent/10 hover:text-accent-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
loadContext(project);
|
||||
}}
|
||||
>
|
||||
<MessageSquare className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Start Chat</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost2"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-destructive/10 hover:text-destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const modal = new ConfirmModal(
|
||||
app,
|
||||
() => onDelete(project),
|
||||
`Are you sure you want to delete project "${project.name}"?`,
|
||||
"Delete Project"
|
||||
);
|
||||
modal.open();
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Delete Project</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ProjectList = memo(
|
||||
({
|
||||
className,
|
||||
projects,
|
||||
defaultOpen = false,
|
||||
app,
|
||||
onProjectAdded,
|
||||
onEditProject,
|
||||
hasMessages = false,
|
||||
showChatUI,
|
||||
onClose,
|
||||
inputRef,
|
||||
}: {
|
||||
className?: string;
|
||||
projects: ProjectConfig[];
|
||||
defaultOpen?: boolean;
|
||||
app: App;
|
||||
onProjectAdded: (project: ProjectConfig) => void;
|
||||
onEditProject: (originP: ProjectConfig, updateP: ProjectConfig) => void;
|
||||
hasMessages?: boolean;
|
||||
showChatUI: (v: boolean) => void;
|
||||
onClose: () => void;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement>;
|
||||
}): React.ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [showChatInput, setShowChatInput] = useState(false);
|
||||
const [selectedProject, setSelectedProject] = useState<ProjectConfig | null>(null);
|
||||
|
||||
// Auto collapse when messages appear
|
||||
useEffect(() => {
|
||||
if (hasMessages) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [hasMessages]);
|
||||
|
||||
const handleAddProject = () => {
|
||||
const modal = new AddProjectModal(app, async (project: ProjectConfig) => {
|
||||
onProjectAdded(project);
|
||||
});
|
||||
modal.open();
|
||||
};
|
||||
|
||||
const handleEditProject = (originP: ProjectConfig) => {
|
||||
const modal = new AddProjectModal(
|
||||
app,
|
||||
async (updatedProject: ProjectConfig) => {
|
||||
onEditProject(originP, updatedProject);
|
||||
if (selectedProject && selectedProject.name === originP.name) {
|
||||
setSelectedProject(updatedProject);
|
||||
}
|
||||
},
|
||||
originP
|
||||
);
|
||||
modal.open();
|
||||
};
|
||||
|
||||
const handleDeleteProject = (project: ProjectConfig) => {
|
||||
const currentProjects = projects || [];
|
||||
const newProjectList = currentProjects.filter((p) => p.name !== project.name);
|
||||
|
||||
// If the deleted project is currently selected, close it
|
||||
if (selectedProject?.name === project.name) {
|
||||
enableOrDisableProject(false);
|
||||
}
|
||||
|
||||
// Update the project list in settings
|
||||
updateSetting("projectList", newProjectList);
|
||||
new Notice(`Project "${project.name}" deleted successfully`);
|
||||
};
|
||||
|
||||
const enableOrDisableProject = (enable: boolean, project?: ProjectConfig) => {
|
||||
if (!enable) {
|
||||
setSelectedProject(null);
|
||||
setShowChatInput(false);
|
||||
setIsOpen(true);
|
||||
showChatUI(false);
|
||||
setCurrentProject(null);
|
||||
return;
|
||||
} else {
|
||||
if (!project) {
|
||||
logError("Must be exist one project.");
|
||||
return;
|
||||
}
|
||||
setSelectedProject(project);
|
||||
setShowChatInput(true);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadContext = (p: ProjectConfig) => {
|
||||
setSelectedProject(p);
|
||||
setShowChatInput(true);
|
||||
setIsOpen(false);
|
||||
showChatUI(true);
|
||||
setCurrentProject(p);
|
||||
|
||||
setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)}>
|
||||
<div className="overflow-y-auto">
|
||||
<div className="flex flex-col">
|
||||
{showChatInput && selectedProject ? (
|
||||
<div className="flex justify-between items-center px-2 py-3">
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="font-semibold text-normal">Projects</span>
|
||||
<Select
|
||||
value={selectedProject.name}
|
||||
onValueChange={(value) => {
|
||||
const project = projects.find((p) => p.name === value);
|
||||
if (project) {
|
||||
handleLoadContext(project);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue>
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="size-4 text-accent/70" />
|
||||
<span>{selectedProject.name}</span>
|
||||
</div>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.name} value={project.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Folder className="size-4" />
|
||||
<span>{project.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="fit"
|
||||
variant="ghost2"
|
||||
onClick={() => handleEditProject(selectedProject)}
|
||||
className="hover:bg-accent/50 hover:text-on-accent"
|
||||
>
|
||||
<Edit2 className="size-4 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Edit Current Project</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
enableOrDisableProject(false);
|
||||
}}
|
||||
aria-label="Close Current Project"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Close Current Project</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
className="transition-all duration-200 ease-in-out"
|
||||
>
|
||||
<div className="flex justify-between items-center px-4 py-3">
|
||||
<div className="flex gap-2 items-center flex-1">
|
||||
<span className="font-semibold text-normal">Projects</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="size-4 text-muted" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="w-64">
|
||||
Manage your projects with different contexts and configurations.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="fit" className="px-2" onClick={handleAddProject}>
|
||||
Create
|
||||
<Plus className="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Add New Project</TooltipContent>
|
||||
</Tooltip>
|
||||
{projects.length > 0 && (
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost2" size="icon">
|
||||
{isOpen ? (
|
||||
<ChevronUp className="size-5" />
|
||||
) : (
|
||||
<ChevronDown className="size-5" />
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
onClick={() => onClose()}
|
||||
aria-label="close project mode"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">Close Project Mode</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{projects.length === 0 && (
|
||||
<div className="px-4 py-2 text-xs text-muted bg-secondary/30">
|
||||
No projects available
|
||||
</div>
|
||||
)}
|
||||
<CollapsibleContent className="transition-all duration-200 ease-in-out">
|
||||
<div className="relative bg-secondary/30">
|
||||
<div className="px-4 pt-3 pb-6 max-h-[calc(3*5.7rem)] overflow-y-auto">
|
||||
<div className="flex flex-col gap-2 @2xl:grid @2xl:grid-cols-2 @4xl:grid-cols-3">
|
||||
{projects.map((project) => (
|
||||
<ProjectItem
|
||||
key={project.name}
|
||||
project={project}
|
||||
loadContext={handleLoadContext}
|
||||
onEdit={handleEditProject}
|
||||
onDelete={handleDeleteProject}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{projects.length > 0 && (
|
||||
<div
|
||||
className="absolute bottom-0 left-0 right-0 h-6"
|
||||
style={{
|
||||
background: "linear-gradient(transparent, var(--background-primary) 75%)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!showChatInput && (
|
||||
<div className="flex flex-col gap-4 items-center justify-center p-8 text-muted bg-secondary/30">
|
||||
<div className="max-w-[600px] space-y-4">
|
||||
<p className="text-base text-center">
|
||||
Create your project-based AI assistants with custom instructions, context, and
|
||||
model configurations.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageSquare className="size-4" />
|
||||
<span>Click a project card to start chatting</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ProjectList.displayName = "ProjectList";
|
||||
|
|
@ -65,6 +65,7 @@ const PROMPT_KEYS: Record<ChainType, Array<keyof typeof SUGGESTED_PROMPTS>> = {
|
|||
[ChainType.LLM_CHAIN]: ["activeNote", "quoteNote", "fun"],
|
||||
[ChainType.VAULT_QA_CHAIN]: ["qaVault", "qaVault", "quoteNote"],
|
||||
[ChainType.COPILOT_PLUS_CHAIN]: ["copilotPlus", "copilotPlus", "copilotPlus"],
|
||||
[ChainType.PROJECT_CHAIN]: ["copilotPlus", "copilotPlus", "copilotPlus"],
|
||||
};
|
||||
|
||||
function getRandomPrompt(chainType: ChainType = ChainType.LLM_CHAIN) {
|
||||
|
|
|
|||
436
src/components/composer/ApplyView.tsx
Normal file
436
src/components/composer/ApplyView.tsx
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
import { ApplyChangesConfirmModal } from "@/components/modals/ApplyChangesConfirmModal";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { logError } from "@/logger";
|
||||
import { Change, diffLines } from "diff";
|
||||
import { Check, X as XIcon } from "lucide-react";
|
||||
import { App, ItemView, Notice, TFile, WorkspaceLeaf } from "obsidian";
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export const APPLY_VIEW_TYPE = "obsidian-copilot-apply-view";
|
||||
|
||||
export interface ApplyViewState {
|
||||
file: TFile;
|
||||
originalContent: string;
|
||||
newContent: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
// Extended Change interface to track user decisions
|
||||
interface ExtendedChange extends Change {
|
||||
accepted: boolean | null;
|
||||
}
|
||||
|
||||
export class ApplyView extends ItemView {
|
||||
private root: ReturnType<typeof createRoot> | null = null;
|
||||
private state: ApplyViewState | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
super(leaf);
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return APPLY_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return "Apply Changes";
|
||||
}
|
||||
|
||||
async setState(state: ApplyViewState) {
|
||||
this.state = state;
|
||||
this.render();
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
if (this.root) {
|
||||
this.root.unmount();
|
||||
this.root = null;
|
||||
}
|
||||
}
|
||||
|
||||
private render() {
|
||||
if (!this.state) return;
|
||||
|
||||
// The second child is the actual content of the view, and the first child is the title of the view
|
||||
// NOTE: While no official documentation is found, this seems like a standard pattern across community plugins.
|
||||
const contentEl = this.containerEl.children[1];
|
||||
contentEl.empty();
|
||||
|
||||
const rootEl = contentEl.createDiv();
|
||||
if (!this.root) {
|
||||
this.root = createRoot(rootEl);
|
||||
}
|
||||
|
||||
this.root.render(
|
||||
<ApplyViewRoot app={this.app} state={this.state} close={() => this.leaf.detach()} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ApplyViewRootProps {
|
||||
app: App;
|
||||
state: ApplyViewState;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
const ApplyViewRoot: React.FC<ApplyViewRootProps> = ({ app, state, close }) => {
|
||||
// Initialize diff with extended properties - moved before conditional
|
||||
const [diff, setDiff] = useState<ExtendedChange[]>(() => {
|
||||
if (!state?.originalContent || !state?.newContent) {
|
||||
return [];
|
||||
}
|
||||
const initialDiff = diffLines(state.originalContent, state.newContent);
|
||||
return initialDiff.map((change) => ({
|
||||
...change,
|
||||
accepted: null, // Start with null (undecided)
|
||||
}));
|
||||
});
|
||||
|
||||
const undecidedChanges = diff.filter(
|
||||
(change) => (change.added || change.removed) && change.accepted === null
|
||||
);
|
||||
const hasAnyDecidedChanges = diff.some((change) => change.accepted !== null);
|
||||
|
||||
// Group changes into blocks for better UI presentation
|
||||
const changeBlocks = useMemo(() => {
|
||||
if (!diff.length) return;
|
||||
|
||||
const blocks: ExtendedChange[][] = [];
|
||||
let currentBlock: ExtendedChange[] = [];
|
||||
let inChangeBlock = false;
|
||||
|
||||
diff.forEach((change) => {
|
||||
if (change.added || change.removed) {
|
||||
if (!inChangeBlock) {
|
||||
inChangeBlock = true;
|
||||
currentBlock = [];
|
||||
}
|
||||
currentBlock.push(change);
|
||||
} else {
|
||||
if (inChangeBlock) {
|
||||
blocks.push([...currentBlock]);
|
||||
currentBlock = [];
|
||||
inChangeBlock = false;
|
||||
}
|
||||
blocks.push([change]);
|
||||
}
|
||||
});
|
||||
|
||||
if (currentBlock.length > 0) {
|
||||
blocks.push(currentBlock);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}, [diff]);
|
||||
|
||||
// Add refs to track change blocks
|
||||
const blockRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
// Add defensive check for state after hooks
|
||||
if (!state || !state.originalContent || !state.newContent) {
|
||||
logError("Invalid state:", state);
|
||||
return (
|
||||
<div className="flex flex-col h-full items-center justify-center">
|
||||
<div className="text-error">Error: Invalid state - missing content</div>
|
||||
<Button onClick={close} className="mt-4">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle applying changes that have been marked as accepted
|
||||
const handleApply = async () => {
|
||||
try {
|
||||
const applyChanges = async () => {
|
||||
const newContent = diff
|
||||
.filter((change) => {
|
||||
if (change.added) return change.accepted === true;
|
||||
else if (change.removed) return change.accepted === false;
|
||||
return true; // Unchanged lines are always included
|
||||
})
|
||||
.map((change) => change.value)
|
||||
.join("");
|
||||
|
||||
await app.vault.modify(state.file, newContent);
|
||||
new Notice("Changes applied successfully");
|
||||
close();
|
||||
};
|
||||
|
||||
if (undecidedChanges.length > 0) {
|
||||
// Divide by 2 because each change has a pair of added and removed lines
|
||||
const modal = new ApplyChangesConfirmModal(app, undecidedChanges.length / 2, () => {
|
||||
applyChanges();
|
||||
});
|
||||
modal.open();
|
||||
return;
|
||||
}
|
||||
|
||||
applyChanges();
|
||||
} catch (error) {
|
||||
logError("Error applying changes:", error);
|
||||
new Notice(`Error applying changes: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Apply all changes regardless of whether they have been marked as accepted
|
||||
const handleAcceptAll = async () => {
|
||||
try {
|
||||
const newContent = diff
|
||||
.filter((change) => {
|
||||
return change.added || !change.removed;
|
||||
})
|
||||
.map((change) => change.value)
|
||||
.join("");
|
||||
await app.vault.modify(state.file, newContent);
|
||||
new Notice("Changes applied successfully");
|
||||
close();
|
||||
} catch (error) {
|
||||
logError("Error applying changes:", error);
|
||||
new Notice(`Error applying changes: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle rejecting all changes
|
||||
const handleReject = () => {
|
||||
close();
|
||||
};
|
||||
|
||||
// Function to focus on the next change block or scroll to top if it's the last block
|
||||
const focusNextChangeBlock = (currentBlockIndex: number) => {
|
||||
if (!changeBlocks) return;
|
||||
|
||||
// Find the next block with changes that is undecided
|
||||
let nextBlockIndex = -1;
|
||||
for (let i = currentBlockIndex + 1; i < changeBlocks.length; i++) {
|
||||
const block = changeBlocks[i];
|
||||
const hasChanges = block.some((change) => change.added || change.removed);
|
||||
const isUndecided = block.some(
|
||||
(change) => (change.added || change.removed) && change.accepted === null
|
||||
);
|
||||
|
||||
if (hasChanges && isUndecided) {
|
||||
nextBlockIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a next block, scroll to it
|
||||
if (nextBlockIndex !== -1 && blockRefs.current[nextBlockIndex]) {
|
||||
blockRefs.current[nextBlockIndex]?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
};
|
||||
|
||||
// Accept a block of changes
|
||||
const acceptBlock = (blockIndex: number) => {
|
||||
setDiff((prevDiff) => {
|
||||
const newDiff = [...prevDiff];
|
||||
const block = changeBlocks?.[blockIndex];
|
||||
|
||||
if (!block) return newDiff;
|
||||
|
||||
// Find the indices of the changes in this block
|
||||
block.forEach((blockChange) => {
|
||||
const index = newDiff.findIndex((change) => change === blockChange);
|
||||
if (index !== -1) {
|
||||
newDiff[index] = {
|
||||
...newDiff[index],
|
||||
accepted: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return newDiff;
|
||||
});
|
||||
|
||||
// Focus on the next change block after state update
|
||||
setTimeout(() => focusNextChangeBlock(blockIndex), 0);
|
||||
};
|
||||
|
||||
// Reject a block of changes
|
||||
const rejectBlock = (blockIndex: number) => {
|
||||
setDiff((prevDiff) => {
|
||||
const newDiff = [...prevDiff];
|
||||
const block = changeBlocks?.[blockIndex];
|
||||
|
||||
if (!block) return newDiff;
|
||||
|
||||
// Find the indices of the changes in this block
|
||||
block.forEach((blockChange) => {
|
||||
const index = newDiff.findIndex((change) => change === blockChange);
|
||||
if (index !== -1) {
|
||||
newDiff[index] = {
|
||||
...newDiff[index],
|
||||
accepted: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return newDiff;
|
||||
});
|
||||
|
||||
// Focus on the next change block after state update
|
||||
setTimeout(() => focusNextChangeBlock(blockIndex), 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full relative">
|
||||
<div className="flex z-[1] gap-2 fixed bottom-2 left-1/2 -translate-x-1/2 p-2 border border-solid border-border rounded-md bg-secondary">
|
||||
<Button variant="destructive" size="sm" onClick={handleReject}>
|
||||
<XIcon className="size-4" />
|
||||
Reject All
|
||||
</Button>
|
||||
<Button variant="success" size="sm" onClick={handleAcceptAll}>
|
||||
<Check className="size-4" />
|
||||
Accept All
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!hasAnyDecidedChanges}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleApply}
|
||||
>
|
||||
Apply Changes
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center p-2 border-[0px] border-solid border-b border-border text-sm font-medium">
|
||||
{state.path || state.file.path}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-2">
|
||||
{changeBlocks?.map((block, blockIndex) => {
|
||||
// Check if this block contains any changes (added or removed)
|
||||
const hasChanges = block.some((change) => change.added || change.removed);
|
||||
|
||||
// Get the decision status for this block
|
||||
const blockStatus = hasChanges
|
||||
? block.every(
|
||||
(change) => (!change.added && !change.removed) || change.accepted === true
|
||||
)
|
||||
? "accepted"
|
||||
: block.every(
|
||||
(change) => (!change.added && !change.removed) || change.accepted === false
|
||||
)
|
||||
? "rejected"
|
||||
: "undecided"
|
||||
: "unchanged";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={blockIndex}
|
||||
ref={(el) => (blockRefs.current[blockIndex] = el)}
|
||||
className={cn("mb-4 border rounded-md overflow-hidden border-border", {
|
||||
"border-solid": blockStatus !== "unchanged",
|
||||
})}
|
||||
>
|
||||
{blockStatus === "accepted" ? (
|
||||
// Show only the accepted version
|
||||
<div className="flex-1 font-mono text-sm whitespace-pre-wrap py-1 px-2 text-text-normal">
|
||||
{block
|
||||
.filter((change) => !change.removed)
|
||||
.map((change, idx) => (
|
||||
<div key={idx}>{change.value}</div>
|
||||
))}
|
||||
</div>
|
||||
) : blockStatus === "rejected" ? (
|
||||
// Show only the original version
|
||||
<div className="flex-1 font-mono text-sm whitespace-pre-wrap py-1 px-2 text-text-normal">
|
||||
{block
|
||||
.filter((change) => !change.added)
|
||||
.map((change, idx) => (
|
||||
<div key={idx}>{change.value}</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// Show the diff view for undecided blocks
|
||||
block.map((change, changeIndex) => (
|
||||
<div
|
||||
key={`${blockIndex}-${changeIndex}`}
|
||||
className={cn("flex relative border-l-3", {
|
||||
"bg-success border-l-green": change.added,
|
||||
"bg-error border-l-red": change.removed,
|
||||
})}
|
||||
>
|
||||
<div className="flex-1 font-mono text-sm whitespace-pre-wrap py-1 px-2 text-text-normal">
|
||||
{change.value}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Only show accept/reject buttons for blocks with changes that are undecided */}
|
||||
{hasChanges && blockStatus === "undecided" && (
|
||||
<div className="flex items-center justify-end p-2 border-[0px] border-solid border-t border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="destructive" size="sm" onClick={() => rejectBlock(blockIndex)}>
|
||||
<XIcon className="size-4" />
|
||||
Reject
|
||||
</Button>
|
||||
<Button variant="success" size="sm" onClick={() => acceptBlock(blockIndex)}>
|
||||
<Check className="size-4" />
|
||||
Accept
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show status for decided blocks with revert option */}
|
||||
{hasChanges && (blockStatus === "accepted" || blockStatus === "rejected") && (
|
||||
<div className="flex items-center justify-end p-2 border-[0px] border-solid border-t border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium mr-2">
|
||||
{blockStatus === "accepted" ? (
|
||||
<div className="flex items-center gap-1 text-success">
|
||||
<Check className="size-4" />
|
||||
<div>Accepted</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 text-error">
|
||||
<XIcon className="size-4" />
|
||||
<div>Rejected</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Reset the block to undecided state
|
||||
setDiff((prevDiff) => {
|
||||
const newDiff = [...prevDiff];
|
||||
const block = changeBlocks?.[blockIndex];
|
||||
|
||||
if (!block) return newDiff;
|
||||
|
||||
block.forEach((blockChange) => {
|
||||
const index = newDiff.findIndex((change) => change === blockChange);
|
||||
if (index !== -1) {
|
||||
newDiff[index] = {
|
||||
...newDiff[index],
|
||||
accepted: null,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return newDiff;
|
||||
});
|
||||
}}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
389
src/components/modals/AddProjectModal.tsx
Normal file
389
src/components/modals/AddProjectModal.tsx
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
import { ProjectConfig } from "@/aiParams";
|
||||
import { PatternMatchingModal } from "@/components/modals/PatternMatchingModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FormField } from "@/components/ui/form-field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { getModelDisplayWithIcons } from "@/components/ui/model-display";
|
||||
import { ObsidianNativeSelect } from "@/components/ui/obsidian-native-select";
|
||||
import { SettingSlider } from "@/components/ui/setting-slider";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getModelKeyFromModel, useSettingsValue } from "@/settings/model";
|
||||
import { checkModelApiKey, err2String, randomUUID } from "@/utils";
|
||||
import { App, Modal, Notice } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
|
||||
interface AddProjectModalContentProps {
|
||||
initialProject?: ProjectConfig;
|
||||
onSave: (project: ProjectConfig) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProjectModalContentProps) {
|
||||
const settings = useSettingsValue();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [touched, setTouched] = useState({
|
||||
name: false,
|
||||
systemPrompt: false,
|
||||
projectModelKey: false,
|
||||
inclusions: false,
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState<Partial<ProjectConfig>>(
|
||||
initialProject || {
|
||||
id: randomUUID(),
|
||||
name: "",
|
||||
description: "",
|
||||
systemPrompt: "",
|
||||
projectModelKey: "",
|
||||
modelConfigs: {
|
||||
temperature: 1.0,
|
||||
maxTokens: 1000,
|
||||
},
|
||||
contextSource: {
|
||||
inclusions: "",
|
||||
exclusions: "",
|
||||
webUrls: "",
|
||||
youtubeUrls: "",
|
||||
},
|
||||
created: Date.now(),
|
||||
UsageTimestamps: Date.now(),
|
||||
}
|
||||
);
|
||||
|
||||
const isFormValid = () => {
|
||||
return formData.name && formData.projectModelKey;
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
field: string,
|
||||
value: string | number | string[] | Record<string, any>
|
||||
) => {
|
||||
setFormData((prev) => {
|
||||
// Handle text input
|
||||
if (typeof value === "string") {
|
||||
// Only trim for specific fields that shouldn't have whitespace
|
||||
if (field === "name" || field === "projectModelKey") {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
// Handle string arrays
|
||||
if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
|
||||
value = value.map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
if (field.includes(".")) {
|
||||
const [parent, child] = field.split(".");
|
||||
const parentKey = parent as keyof typeof prev;
|
||||
const parentValue = prev[parentKey];
|
||||
|
||||
if (typeof parentValue === "object" && parentValue !== null) {
|
||||
return {
|
||||
...prev,
|
||||
[parent]: {
|
||||
...parentValue,
|
||||
[child]: value,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[field]: value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const requiredFields = ["name", "projectModelKey"];
|
||||
const missingFields = requiredFields.filter((field) => !formData[field as keyof ProjectConfig]);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
setTouched((prev) => ({
|
||||
...prev,
|
||||
...Object.fromEntries(missingFields.map((field) => [field, true])),
|
||||
}));
|
||||
new Notice("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await onSave(formData as ProjectConfig);
|
||||
} catch (e) {
|
||||
new Notice(err2String(e));
|
||||
setTouched((prev) => ({
|
||||
...prev,
|
||||
name: true,
|
||||
}));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<div className="text-xl font-bold text-normal mb-2">Add New Project</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
label="Project Name"
|
||||
required
|
||||
error={touched.name && !formData.name}
|
||||
errorMessage="Project name is required"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
onBlur={() => setTouched((prev) => ({ ...prev, name: true }))}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Description"
|
||||
description="Briefly describe the purpose and goals of the project"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Project System Prompt"
|
||||
description="Custom instructions for how the AI should behave in this project context"
|
||||
>
|
||||
<Textarea
|
||||
value={formData.systemPrompt}
|
||||
onChange={(e) => handleInputChange("systemPrompt", e.target.value)}
|
||||
onBlur={() => setTouched((prev) => ({ ...prev, systemPrompt: true }))}
|
||||
className="min-h-[8rem]"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Default Model"
|
||||
required
|
||||
error={touched.projectModelKey && !formData.projectModelKey}
|
||||
errorMessage="Default model is required"
|
||||
>
|
||||
<ObsidianNativeSelect
|
||||
value={formData.projectModelKey}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const selectedModel = settings.activeModels.find(
|
||||
(m) => m.enabled && getModelKeyFromModel(m) === value
|
||||
);
|
||||
if (!selectedModel) return;
|
||||
|
||||
const { hasApiKey, errorNotice } = checkModelApiKey(selectedModel, settings);
|
||||
if (!hasApiKey && errorNotice) {
|
||||
new Notice(errorNotice);
|
||||
return;
|
||||
}
|
||||
handleInputChange("projectModelKey", value);
|
||||
}}
|
||||
onBlur={() => setTouched((prev) => ({ ...prev, projectModelKey: true }))}
|
||||
placeholder="Select a model"
|
||||
options={settings.activeModels
|
||||
.filter((m) => m.enabled && m.projectEnabled)
|
||||
.map((model) => ({
|
||||
label: getModelDisplayWithIcons(model),
|
||||
value: getModelKeyFromModel(model),
|
||||
}))}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-base font-medium">Model Configuration</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<FormField label="Temperature">
|
||||
<SettingSlider
|
||||
value={formData.modelConfigs?.temperature ?? 1}
|
||||
onChange={(value) => handleInputChange("modelConfigs.temperature", value)}
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.01}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Token Limit">
|
||||
<SettingSlider
|
||||
value={formData.modelConfigs?.maxTokens ?? 1000}
|
||||
onChange={(value) => handleInputChange("modelConfigs.maxTokens", value)}
|
||||
min={1}
|
||||
max={16000}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-base font-medium">Context Sources</div>
|
||||
<FormField
|
||||
label="Inclusions"
|
||||
description="Define patterns to include specific files or folders in the project context"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 text-xs text-muted">
|
||||
{formData.contextSource?.inclusions?.trim()
|
||||
? "Patterns configured"
|
||||
: "No patterns configured"}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
new PatternMatchingModal(
|
||||
app,
|
||||
(value: string) => {
|
||||
handleInputChange("contextSource.inclusions", value);
|
||||
},
|
||||
formData.contextSource?.inclusions || "",
|
||||
"Manage Inclusions"
|
||||
).open()
|
||||
}
|
||||
>
|
||||
Manage Patterns
|
||||
</Button>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Exclusions"
|
||||
description="Exclude specific files or patterns from the included folders above"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 text-xs text-muted">
|
||||
{formData.contextSource?.exclusions?.trim()
|
||||
? "Patterns configured"
|
||||
: "No patterns configured"}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
new PatternMatchingModal(
|
||||
app,
|
||||
(value: string) => {
|
||||
handleInputChange("contextSource.exclusions", value);
|
||||
},
|
||||
formData.contextSource?.exclusions || "",
|
||||
"Manage Exclusions"
|
||||
).open()
|
||||
}
|
||||
>
|
||||
Manage Patterns
|
||||
</Button>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Web URLs">
|
||||
<Textarea
|
||||
value={formData.contextSource?.webUrls}
|
||||
onChange={(e) => {
|
||||
const urls = e.target.value.split("\n");
|
||||
|
||||
// Process each URL while preserving spaces and empty lines
|
||||
const processedUrls = urls.map((url) => {
|
||||
if (!url.trim()) return url; // Preserve empty lines and whitespace-only lines
|
||||
try {
|
||||
new URL(url.trim());
|
||||
return url; // Keep original formatting including spaces
|
||||
} catch {
|
||||
return url; // Keep invalid URLs for user to fix
|
||||
}
|
||||
});
|
||||
|
||||
handleInputChange("contextSource.webUrls", processedUrls.join("\n"));
|
||||
}}
|
||||
placeholder="Enter web URLs, one per line"
|
||||
className="min-h-[80px] w-full"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="YouTube URLs">
|
||||
<Textarea
|
||||
value={formData.contextSource?.youtubeUrls}
|
||||
onChange={(e) => {
|
||||
const urls = e.target.value.split("\n");
|
||||
|
||||
// Process each URL while preserving spaces and empty lines
|
||||
const processedUrls = urls.map((url) => {
|
||||
if (!url.trim()) return url; // Preserve empty lines and whitespace-only lines
|
||||
try {
|
||||
const urlObj = new URL(url.trim());
|
||||
if (
|
||||
urlObj.hostname.includes("youtube.com") ||
|
||||
urlObj.hostname.includes("youtu.be")
|
||||
) {
|
||||
return url; // Keep original formatting including spaces
|
||||
}
|
||||
return url; // Keep non-YouTube URLs for user to fix
|
||||
} catch {
|
||||
return url; // Keep invalid URLs for user to fix
|
||||
}
|
||||
});
|
||||
|
||||
handleInputChange("contextSource.youtubeUrls", processedUrls.join("\n"));
|
||||
}}
|
||||
placeholder="Enter YouTube URLs, one per line"
|
||||
className="min-h-[80px] w-full"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 mt-4">
|
||||
<Button variant="ghost" onClick={onCancel} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSubmitting || !isFormValid()}>
|
||||
{isSubmitting ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export class AddProjectModal extends Modal {
|
||||
private root: Root;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private onSave: (project: ProjectConfig) => Promise<void>,
|
||||
private initialProject?: ProjectConfig
|
||||
) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.root = createRoot(contentEl);
|
||||
|
||||
const handleSave = async (project: ProjectConfig) => {
|
||||
await this.onSave(project);
|
||||
this.close();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
this.close();
|
||||
};
|
||||
|
||||
this.root.render(
|
||||
<AddProjectModalContent
|
||||
initialProject={this.initialProject}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
15
src/components/modals/ApplyChangesConfirmModal.tsx
Normal file
15
src/components/modals/ApplyChangesConfirmModal.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { App } from "obsidian";
|
||||
import { ConfirmModal } from "./ConfirmModal";
|
||||
|
||||
export class ApplyChangesConfirmModal extends ConfirmModal {
|
||||
constructor(app: App, unDecidedChanges: number, onConfirm: () => void) {
|
||||
super(
|
||||
app,
|
||||
onConfirm,
|
||||
`There are ${unDecidedChanges} changes that have not been decided. Are you sure you want to skip these changes?`,
|
||||
"Apply Changes",
|
||||
"Confirm",
|
||||
"Back to Edit"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,16 +6,20 @@ function ConfirmModalContent({
|
|||
content,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
confirmButtonText,
|
||||
cancelButtonText,
|
||||
}: {
|
||||
content: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
confirmButtonText: string;
|
||||
cancelButtonText: string;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
||||
<div>{content}</div>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", gap: "10px" }}>
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
<button onClick={onCancel}>{cancelButtonText}</button>
|
||||
<button
|
||||
style={{
|
||||
backgroundColor: "var(--interactive-accent)",
|
||||
|
|
@ -24,7 +28,7 @@ function ConfirmModalContent({
|
|||
}}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Continue
|
||||
{confirmButtonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -38,7 +42,9 @@ export class ConfirmModal extends Modal {
|
|||
app: App,
|
||||
private onConfirm: () => void,
|
||||
private content: string,
|
||||
title: string
|
||||
title: string,
|
||||
private confirmButtonText: string = "Continue",
|
||||
private cancelButtonText: string = "Cancel"
|
||||
) {
|
||||
super(app);
|
||||
// https://docs.obsidian.md/Reference/TypeScript+API/Modal/setTitle
|
||||
|
|
@ -64,6 +70,8 @@ export class ConfirmModal extends Modal {
|
|||
content={this.content}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
confirmButtonText={this.confirmButtonText}
|
||||
cancelButtonText={this.cancelButtonText}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useModelKey } from "@/aiParams";
|
||||
import { processCommandPrompt } from "@/commands/inlineEditCommandUtils";
|
||||
import { getModelDisplayText } from "@/components/ui/model-display";
|
||||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import { InlineEditCommandSettings, useSettingsValue } from "@/settings/model";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { findCustomModel, insertIntoEditor } from "@/utils";
|
||||
|
|
@ -9,6 +8,7 @@ import { Bot, Copy, PenLine } from "lucide-react";
|
|||
import { App, Modal, Notice } from "obsidian";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { createRoot, Root } from "react-dom/client";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
interface InlineEditModalContentProps {
|
||||
originalText: string;
|
||||
|
|
@ -44,7 +44,9 @@ function InlineEditModalContent({
|
|||
async function stream() {
|
||||
const prompt = processCommandPrompt(command.prompt, originalText);
|
||||
let fullAIResponse = "";
|
||||
const chatModel = await ChatModelManager.getInstance().createModelInstance(selectedModel);
|
||||
const chatModel = await ProjectManager.instance
|
||||
.getCurrentChainManager()
|
||||
.chatModelManager.createModelInstance(selectedModel);
|
||||
const chatStream = await chatModel.stream(prompt);
|
||||
for await (const chunk of chatStream) {
|
||||
if (abortController?.signal.aborted) break;
|
||||
|
|
|
|||
|
|
@ -5,16 +5,19 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:shrink-0",
|
||||
"inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"mod-cta bg-interactive-accent text-on-accent shadow hover:bg-interactive-accent-hover",
|
||||
destructive: "bg-modifier-error text-normal shadow-sm hover:bg-modifier-error/90",
|
||||
destructive:
|
||||
"clickable-icon bg-modifier-error text-on-accent hover:bg-modifier-error hover:text-on-accent",
|
||||
secondary: "bg-secondary text-normal shadow-sm hover:bg-interactive-hover",
|
||||
ghost: "clickable-icon bg-transparent hover:bg-interactive-accent hover:text-on-accent",
|
||||
link: "text-accent underline-offset-4 hover:underline",
|
||||
success:
|
||||
"clickable-icon bg-modifier-success text-on-accent hover:bg-modifier-success hover:text-on-accent",
|
||||
ghost2:
|
||||
"text-faint clickable-icon bg-transparent hover:bg-opacity-100 hover:text-normal hover:bg-transparent outline-none focus-visible:outline-none focus-visible:text-normal focus-visible:ring-0",
|
||||
},
|
||||
|
|
|
|||
59
src/components/ui/obsidian-native-select.tsx
Normal file
59
src/components/ui/obsidian-native-select.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ObsidianNativeSelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
options: SelectOption[];
|
||||
placeholder?: string;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
export function ObsidianNativeSelect({
|
||||
options,
|
||||
placeholder,
|
||||
containerClassName,
|
||||
className,
|
||||
...props
|
||||
}: ObsidianNativeSelectProps) {
|
||||
return (
|
||||
<div className={cn("relative w-full group", containerClassName)}>
|
||||
<select
|
||||
className={cn(
|
||||
"w-full appearance-none",
|
||||
"flex h-9 rounded-md border border-solid border-border bg-dropdown px-3 py-1 pr-8",
|
||||
"text-sm !shadow transition-colors",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"hover:bg-interactive-hover hover:text-normal",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{placeholder && (
|
||||
<option value="" disabled>
|
||||
{placeholder}
|
||||
</option>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2",
|
||||
"transition-colors group-hover:[&>svg]:text-normal",
|
||||
props.disabled && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"!shadow-none",
|
||||
"data-[state=active]:!bg-interactive-accent data-[state=active]:!text-on-accent data-[state=active]:!shadow-md",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-ring transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
41
src/composerUtils.ts
Normal file
41
src/composerUtils.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { BrevilabsClient } from "@/LLMProviders/brevilabsClient";
|
||||
import { getSystemPrompt } from "@/settings/model";
|
||||
|
||||
// Cache for the composer prompt
|
||||
let cachedComposerPrompt: string | null = null;
|
||||
|
||||
/**
|
||||
* Reset the cached composer prompt.
|
||||
*/
|
||||
export function resetComposerPromptCache(): void {
|
||||
cachedComposerPrompt = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the system prompt for the composer.
|
||||
* This combines the user's system prompt with the composer-specific prompt.
|
||||
*/
|
||||
export async function getComposerSystemPrompt(): Promise<string> {
|
||||
// Get the current system prompt
|
||||
const currentSystemPrompt = getSystemPrompt();
|
||||
|
||||
// If we already have a cached composer prompt, use it
|
||||
if (cachedComposerPrompt) {
|
||||
return `${currentSystemPrompt}\n${cachedComposerPrompt}`;
|
||||
}
|
||||
|
||||
// Otherwise, fetch it from the API
|
||||
const brevilabsClient = BrevilabsClient.getInstance();
|
||||
try {
|
||||
// Get the composer prompt from the API
|
||||
const composerPromptResponse = await brevilabsClient.composerPrompt();
|
||||
cachedComposerPrompt = composerPromptResponse.prompt;
|
||||
|
||||
// Combine the prompts
|
||||
return `${currentSystemPrompt}\n${cachedComposerPrompt}`;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch composer prompt:", error);
|
||||
// Fallback to just the system prompt if API call fails
|
||||
return currentSystemPrompt;
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +50,8 @@ export enum ChatModels {
|
|||
AZURE_OPENAI = "azure-openai",
|
||||
GEMINI_PRO = "gemini-2.0-pro-exp",
|
||||
GEMINI_FLASH = "gemini-2.0-flash",
|
||||
GEMINI_15_PRO = "gemini-1.5-pro-001",
|
||||
GEMINI_15_FLASH = "gemini-1.5-flash-001", // TODO(logan): Project should use 2.0 flash once it supports context caching
|
||||
CLAUDE_3_5_SONNET = "claude-3-5-sonnet-latest",
|
||||
CLAUDE_3_5_HAIKU = "claude-3-5-haiku-latest",
|
||||
COMMAND_R = "command-r",
|
||||
|
|
@ -114,6 +116,7 @@ export const BUILTIN_CHAT_MODELS: CustomModel[] = [
|
|||
enabled: true,
|
||||
isBuiltIn: true,
|
||||
core: true,
|
||||
projectEnabled: true,
|
||||
capabilities: [ModelCapability.VISION],
|
||||
},
|
||||
{
|
||||
|
|
@ -170,6 +173,24 @@ export const BUILTIN_CHAT_MODELS: CustomModel[] = [
|
|||
isBuiltIn: true,
|
||||
capabilities: [ModelCapability.VISION],
|
||||
},
|
||||
{
|
||||
name: ChatModels.GEMINI_15_PRO,
|
||||
provider: ChatModelProviders.GOOGLE,
|
||||
enabled: true,
|
||||
isBuiltIn: true,
|
||||
core: true,
|
||||
projectEnabled: true,
|
||||
capabilities: [ModelCapability.VISION],
|
||||
},
|
||||
{
|
||||
name: ChatModels.GEMINI_15_FLASH,
|
||||
provider: ChatModelProviders.GOOGLE,
|
||||
enabled: true,
|
||||
isBuiltIn: true,
|
||||
core: true,
|
||||
projectEnabled: true,
|
||||
capabilities: [ModelCapability.VISION],
|
||||
},
|
||||
{
|
||||
name: ChatModels.AZURE_OPENAI,
|
||||
provider: ChatModelProviders.AZURE_OPENAI,
|
||||
|
|
@ -468,6 +489,18 @@ export const COMMAND_NAMES: Record<CommandId, string> = {
|
|||
|
||||
export type CommandId = (typeof COMMAND_IDS)[keyof typeof COMMAND_IDS];
|
||||
|
||||
export const EVENT_NAMES = {
|
||||
CHAT_IS_VISIBLE: "chat-is-visible",
|
||||
ACTIVE_LEAF_CHANGE: "active-leaf-change",
|
||||
};
|
||||
|
||||
export const AUTOCOMPLETE_CONFIG = {
|
||||
DELAY_MS: 300,
|
||||
MIN_TRIGGER_LENGTH: 3,
|
||||
MAX_CONTEXT_LENGTH: 10000,
|
||||
KEYBIND: "Tab",
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_SETTINGS: CopilotSettings = {
|
||||
isPlusUser: false,
|
||||
plusLicenseKey: "",
|
||||
|
|
@ -521,11 +554,8 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
|
|||
promptUsageTimestamps: {},
|
||||
defaultConversationNoteName: "{$topic}@{$date}_{$time}",
|
||||
inlineEditCommands: DEFAULT_INLINE_EDIT_COMMANDS,
|
||||
};
|
||||
|
||||
export const EVENT_NAMES = {
|
||||
CHAT_IS_VISIBLE: "chat-is-visible",
|
||||
ACTIVE_LEAF_CHANGE: "active-leaf-change",
|
||||
projectList: [],
|
||||
enableAutocomplete: true,
|
||||
};
|
||||
|
||||
export enum ABORT_REASON {
|
||||
|
|
|
|||
142
src/hooks/useApplyCode.ts
Normal file
142
src/hooks/useApplyCode.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { App, Notice, TFile } from "obsidian";
|
||||
import { useCallback } from "react";
|
||||
import { BrevilabsClient, ComposerApplyRequest } from "@/LLMProviders/brevilabsClient";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { USER_SENDER } from "@/constants";
|
||||
import { APPLY_VIEW_TYPE } from "@/components/composer/ApplyView";
|
||||
import { logError, logInfo } from "@/logger";
|
||||
|
||||
/**
|
||||
* Custom hook for applying code to a file using the composer API
|
||||
* @param app The Obsidian app instance
|
||||
* @param chatHistory The chat history to provide context for the code application
|
||||
* @returns A callback function to apply code to a file
|
||||
*/
|
||||
export function useApplyCode(app: App, chatHistory: ChatMessage[] = []) {
|
||||
return useCallback(
|
||||
async (path: string, code: string) => {
|
||||
try {
|
||||
// Get the file from the path
|
||||
let file = app.vault.getAbstractFileByPath(path);
|
||||
let isNewFile = false;
|
||||
|
||||
// If file doesn't exist, create it
|
||||
if (!file) {
|
||||
try {
|
||||
// Create the file with the provided code as content
|
||||
await app.vault.create(path, code);
|
||||
new Notice(`Created new file: ${path}`);
|
||||
|
||||
// Get the newly created file
|
||||
file = app.vault.getAbstractFileByPath(path);
|
||||
isNewFile = true;
|
||||
} catch (createError) {
|
||||
logError("Error creating file:", createError);
|
||||
new Notice(`Failed to create file: ${createError.message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(file instanceof TFile)) {
|
||||
new Notice(`Path is not a file: ${path}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the original content
|
||||
const originalContent = await app.vault.read(file);
|
||||
|
||||
// Check if the current active note is the same as the target note
|
||||
const activeFile = app.workspace.getActiveFile();
|
||||
if (!activeFile || activeFile.path !== file.path) {
|
||||
// If not, open the target file in the current leaf
|
||||
await app.workspace.getLeaf().openFile(file);
|
||||
new Notice(`Switched to ${file.name}`);
|
||||
}
|
||||
|
||||
// If the file is newly created, don't show the apply view
|
||||
if (isNewFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate new content and show diff in the apply view.
|
||||
try {
|
||||
// Call the composer apply endpoint
|
||||
const brevilabsClient = BrevilabsClient.getInstance();
|
||||
|
||||
// Convert chat history to the format expected by the API
|
||||
const formattedChatHistory = chatHistory
|
||||
.filter((msg) => msg.isVisible)
|
||||
.map((msg) => ({
|
||||
role: msg.sender === USER_SENDER ? "user" : "assistant",
|
||||
content: msg.message,
|
||||
}));
|
||||
|
||||
// Create the request object
|
||||
const request: ComposerApplyRequest = {
|
||||
target_note: {
|
||||
title: file.basename,
|
||||
content: originalContent,
|
||||
},
|
||||
chat_history: formattedChatHistory,
|
||||
markdown_block: code,
|
||||
};
|
||||
|
||||
// Call the composer apply endpoint
|
||||
logInfo("==== Composer Request ====\n", request);
|
||||
const response = await brevilabsClient.composerApply(request);
|
||||
|
||||
// Use the content from the response
|
||||
let newContent = response.content;
|
||||
|
||||
// Remove the code block markers if they exist
|
||||
//TODO: Remove this once the issue is fixed in the backend
|
||||
if (newContent.startsWith("```\n") && newContent.endsWith("\n```")) {
|
||||
newContent = newContent.slice(4, -4);
|
||||
}
|
||||
if (newContent.startsWith("```markdown\n") && newContent.endsWith("\n```")) {
|
||||
newContent = newContent.slice(12, -4);
|
||||
}
|
||||
|
||||
// Remove trailing newline from newContent if originalContent doesn't end with one
|
||||
//TODO: Remove this once the issue is fixed in the backend
|
||||
if (!originalContent.endsWith("\n") && newContent.endsWith("\n")) {
|
||||
newContent = newContent.slice(0, -1);
|
||||
}
|
||||
|
||||
// Open the Apply View in a new leaf with the processed content
|
||||
const leaf = app.workspace.getLeaf(true);
|
||||
await leaf.setViewState({
|
||||
type: APPLY_VIEW_TYPE,
|
||||
active: true,
|
||||
state: {
|
||||
file,
|
||||
originalContent,
|
||||
newContent,
|
||||
path,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError("Error calling composer apply:", error);
|
||||
new Notice(`Error processing code: ${error.message}`);
|
||||
|
||||
// Fallback to original behavior if composer apply fails
|
||||
const leaf = app.workspace.getLeaf(true);
|
||||
await leaf.setViewState({
|
||||
type: APPLY_VIEW_TYPE,
|
||||
active: true,
|
||||
state: {
|
||||
file: file,
|
||||
originalContent: originalContent,
|
||||
newContent: code,
|
||||
path: path,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError("Error applying code:", error);
|
||||
new Notice(`Error applying code: ${error.message}`);
|
||||
}
|
||||
},
|
||||
[app, chatHistory]
|
||||
);
|
||||
}
|
||||
28
src/main.ts
28
src/main.ts
|
|
@ -1,9 +1,11 @@
|
|||
import { BrevilabsClient } from "@/LLMProviders/brevilabsClient";
|
||||
import ChainManager from "@/LLMProviders/chainManager";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { AutocompleteService } from "@/autocomplete/autocompleteService";
|
||||
import { parseChatContent, updateChatMemory } from "@/chatUtils";
|
||||
import { registerCommands } from "@/commands";
|
||||
import CopilotView from "@/components/CopilotView";
|
||||
import { ApplyView, APPLY_VIEW_TYPE } from "@/components/composer/ApplyView";
|
||||
import { LoadChatHistoryModal } from "@/components/modals/LoadChatHistoryModal";
|
||||
import { CHAT_VIEWTYPE, DEFAULT_OPEN_AREA, EVENT_NAMES } from "@/constants";
|
||||
import { registerContextMenu } from "@/contextMenu";
|
||||
|
|
@ -32,17 +34,19 @@ import {
|
|||
WorkspaceLeaf,
|
||||
} from "obsidian";
|
||||
import { IntentAnalyzer } from "./LLMProviders/intentAnalyzer";
|
||||
import { logInfo } from "@/logger";
|
||||
|
||||
export default class CopilotPlugin extends Plugin {
|
||||
// A chat history that stores the messages sent and received
|
||||
// Only reset when the user explicitly clicks "New Chat"
|
||||
sharedState: SharedState;
|
||||
chainManager: ChainManager;
|
||||
projectManager: ProjectManager;
|
||||
brevilabsClient: BrevilabsClient;
|
||||
userMessageHistory: string[] = [];
|
||||
vectorStoreManager: VectorStoreManager;
|
||||
fileParserManager: FileParserManager;
|
||||
settingsUnsubscriber?: () => void;
|
||||
private autocompleteService: AutocompleteService;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
|
|
@ -55,8 +59,9 @@ export default class CopilotPlugin extends Plugin {
|
|||
registerCommands(this, prev, next);
|
||||
});
|
||||
this.addSettingTab(new CopilotSettingTab(this.app, this));
|
||||
// Always have one instance of sharedState and chainManager in the plugin
|
||||
this.sharedState = new SharedState();
|
||||
|
||||
// Always have one instance of sharedState in the plugin
|
||||
this.sharedState = new SharedState(this);
|
||||
|
||||
this.vectorStoreManager = VectorStoreManager.getInstance();
|
||||
|
||||
|
|
@ -65,12 +70,14 @@ export default class CopilotPlugin extends Plugin {
|
|||
this.brevilabsClient.setPluginVersion(this.manifest.version);
|
||||
checkIsPlusUser();
|
||||
|
||||
this.chainManager = new ChainManager(this.app, this.vectorStoreManager);
|
||||
// Initialize ProjectManager
|
||||
this.projectManager = ProjectManager.getInstance(this.app, this.vectorStoreManager, this);
|
||||
|
||||
// Initialize FileParserManager early with other core services
|
||||
this.fileParserManager = new FileParserManager(this.brevilabsClient);
|
||||
|
||||
this.registerView(CHAT_VIEWTYPE, (leaf: WorkspaceLeaf) => new CopilotView(leaf, this));
|
||||
this.registerView(APPLY_VIEW_TYPE, (leaf: WorkspaceLeaf) => new ApplyView(leaf));
|
||||
|
||||
this.initActiveLeafChangeHandler();
|
||||
|
||||
|
|
@ -108,6 +115,9 @@ export default class CopilotPlugin extends Plugin {
|
|||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Initialize autocomplete service
|
||||
this.autocompleteService = AutocompleteService.getInstance(this);
|
||||
}
|
||||
|
||||
async onunload() {
|
||||
|
|
@ -115,9 +125,11 @@ export default class CopilotPlugin extends Plugin {
|
|||
if (this.vectorStoreManager) {
|
||||
this.vectorStoreManager.onunload();
|
||||
}
|
||||
this.settingsUnsubscriber?.();
|
||||
|
||||
console.log("Copilot plugin unloaded");
|
||||
this.settingsUnsubscriber?.();
|
||||
this.autocompleteService?.destroy();
|
||||
|
||||
logInfo("Copilot plugin unloaded");
|
||||
}
|
||||
|
||||
updateUserMessageHistory(newMessage: string) {
|
||||
|
|
@ -300,7 +312,7 @@ export default class CopilotPlugin extends Plugin {
|
|||
messages.forEach((message) => this.sharedState.addMessage(message));
|
||||
|
||||
// Update the chain's memory with the loaded messages
|
||||
await updateChatMemory(messages, this.chainManager.memoryManager);
|
||||
await updateChatMemory(messages, this.projectManager.getCurrentChainManager().memoryManager);
|
||||
|
||||
// Check if the Copilot view is already active
|
||||
const existingView = this.app.workspace.getLeavesOfType(CHAT_VIEWTYPE)[0];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { BrevilabsClient } from "@/LLMProviders/brevilabsClient";
|
||||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import EmbeddingManager from "@/LLMProviders/embeddingManager";
|
||||
import { logInfo } from "@/logger";
|
||||
import VectorStoreManager from "@/search/vectorStoreManager";
|
||||
|
|
@ -13,6 +12,7 @@ import { BaseRetriever } from "@langchain/core/retrievers";
|
|||
import { search } from "@orama/orama";
|
||||
import { TFile } from "obsidian";
|
||||
import { DBOperations } from "./dbOperations";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
export class HybridRetriever extends BaseRetriever {
|
||||
public lc_namespace = ["hybrid_retriever"];
|
||||
|
|
@ -130,8 +130,9 @@ export class HybridRetriever extends BaseRetriever {
|
|||
private async rewriteQuery(query: string): Promise<string> {
|
||||
try {
|
||||
const promptResult = await this.queryRewritePrompt.format({ question: query });
|
||||
const chatModel = ChatModelManager.getInstance()
|
||||
.getChatModel()
|
||||
const chatModel = ProjectManager.instance
|
||||
.getCurrentChainManager()
|
||||
.chatModelManager.getChatModel()
|
||||
.bind({ temperature: 0 } as BaseChatModelCallOptions);
|
||||
const rewrittenQueryObject = await chatModel.invoke(promptResult);
|
||||
|
||||
|
|
|
|||
|
|
@ -94,18 +94,35 @@ function getInclusionPatterns(): string[] {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the inclusion and exclusion patterns from the settings.
|
||||
* Get the inclusion and exclusion patterns from the settings or provided values.
|
||||
* NOTE: isProject is used to determine if the patterns should be used for a project, ignoring global inclusions and exclusions
|
||||
* @param options - Optional parameters for inclusions and exclusions.
|
||||
* @returns An object containing the inclusions and exclusions patterns strings.
|
||||
*/
|
||||
export function getMatchingPatterns(): {
|
||||
export function getMatchingPatterns(options?: {
|
||||
inclusions?: string;
|
||||
exclusions?: string;
|
||||
isProject?: boolean;
|
||||
}): {
|
||||
inclusions: PatternCategory | null;
|
||||
exclusions: PatternCategory | null;
|
||||
} {
|
||||
const inclusions = getInclusionPatterns();
|
||||
const exclusions = getExclusionPatterns();
|
||||
// For projects, don't fall back to global patterns
|
||||
const inclusionPatterns = options?.inclusions
|
||||
? getDecodedPatterns(options.inclusions)
|
||||
: options?.isProject
|
||||
? []
|
||||
: getInclusionPatterns();
|
||||
|
||||
const exclusionPatterns = options?.exclusions
|
||||
? getDecodedPatterns(options.exclusions)
|
||||
: options?.isProject
|
||||
? []
|
||||
: getExclusionPatterns();
|
||||
|
||||
return {
|
||||
inclusions: inclusions.length > 0 ? categorizePatterns(inclusions) : null,
|
||||
exclusions: exclusions.length > 0 ? categorizePatterns(exclusions) : null,
|
||||
inclusions: inclusionPatterns.length > 0 ? categorizePatterns(inclusionPatterns) : null,
|
||||
exclusions: exclusionPatterns.length > 0 ? categorizePatterns(exclusionPatterns) : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CustomModel } from "@/aiParams";
|
||||
import { CustomModel, ProjectConfig } from "@/aiParams";
|
||||
import { atom, createStore, useAtomValue } from "jotai";
|
||||
|
||||
import { type ChainType } from "@/chainFactory";
|
||||
|
|
@ -90,6 +90,9 @@ export interface CopilotSettings {
|
|||
// undefined means never checked
|
||||
isPlusUser: boolean | undefined;
|
||||
inlineEditCommands: InlineEditCommandSettings[] | undefined;
|
||||
// Autocomplete settings
|
||||
enableAutocomplete: boolean;
|
||||
projectList: Array<ProjectConfig>;
|
||||
}
|
||||
|
||||
export const settingsStore = createStore();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -15,6 +14,7 @@ import { err2String, getNeedSetKeyProvider, getProviderInfo, getProviderLabel }
|
|||
import { Loader2 } from "lucide-react";
|
||||
import { Notice } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
interface ApiKeyDialogProps {
|
||||
open: boolean;
|
||||
|
|
@ -96,7 +96,7 @@ const ApiKeyDialog: React.FC<ApiKeyDialogProps> = ({
|
|||
apiKey,
|
||||
enabled: true,
|
||||
};
|
||||
await ChatModelManager.getInstance().ping(customModel);
|
||||
await ProjectManager.instance.getCurrentChainManager().chatModelManager.ping(customModel);
|
||||
|
||||
new Notice("API key verified successfully!");
|
||||
setUnverifiedKeys((prev) => {
|
||||
|
|
|
|||
|
|
@ -2,25 +2,26 @@ import { ChainType } from "@/chainFactory";
|
|||
import { RebuildIndexConfirmModal } from "@/components/modals/RebuildIndexConfirmModal";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { getModelDisplayWithIcons } from "@/components/ui/model-display";
|
||||
import { SettingItem } from "@/components/ui/setting-item";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { DEFAULT_OPEN_AREA, PLUS_UTM_MEDIUMS } from "@/constants";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
import { createPlusPageUrl } from "@/plusUtils";
|
||||
import VectorStoreManager from "@/search/vectorStoreManager";
|
||||
import { getModelKeyFromModel, updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { formatDateTime, checkModelApiKey } from "@/utils";
|
||||
import { PlusSettings } from "@/settings/v2/components/PlusSettings";
|
||||
import { checkModelApiKey, formatDateTime } from "@/utils";
|
||||
import { HelpCircle, Key, Loader2 } from "lucide-react";
|
||||
import { Notice } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
import ApiKeyDialog from "./ApiKeyDialog";
|
||||
import { PlusSettings } from "@/settings/v2/components/PlusSettings";
|
||||
import { TooltipProvider, Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { createPlusPageUrl } from "@/plusUtils";
|
||||
import { getModelDisplayWithIcons } from "@/components/ui/model-display";
|
||||
import VectorStoreManager from "@/search/vectorStoreManager";
|
||||
|
||||
const ChainType2Label: Record<ChainType, string> = {
|
||||
[ChainType.LLM_CHAIN]: "Chat",
|
||||
[ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)",
|
||||
[ChainType.COPILOT_PLUS_CHAIN]: "Copilot Plus (beta)",
|
||||
[ChainType.PROJECT_CHAIN]: "Plus Projects (alpha)",
|
||||
};
|
||||
|
||||
export const BasicSettings: React.FC = () => {
|
||||
|
|
@ -400,6 +401,34 @@ export const BasicSettings: React.FC = () => {
|
|||
checked={settings.showRelevantNotes}
|
||||
onCheckedChange={(checked) => updateSetting("showRelevantNotes", checked)}
|
||||
/>
|
||||
|
||||
{/* Autocomplete Toggle */}
|
||||
<SettingItem
|
||||
type="switch"
|
||||
title="Enable Autocomplete"
|
||||
description={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">
|
||||
Enable AI-powered autocomplete suggestions while typing
|
||||
</span>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-96 flex flex-col gap-2">
|
||||
<div className="text-sm text-muted pt-2">
|
||||
When enabled, you'll get AI-powered suggestions as you type. Press Tab
|
||||
to accept a suggestion.
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
checked={settings.enableAutocomplete}
|
||||
onCheckedChange={(checked) => updateSetting("enableAutocomplete", checked)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import React, { useState } from "react";
|
|||
import { SettingItem } from "@/components/ui/setting-item";
|
||||
import { setSettings, updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import ChatModelManager from "@/LLMProviders/chatModelManager";
|
||||
import EmbeddingManager from "@/LLMProviders/embeddingManager";
|
||||
import { ModelAddDialog } from "@/settings/v2/components/ModelAddDialog";
|
||||
import { ModelTable } from "@/settings/v2/components/ModelTable";
|
||||
import { ModelEditDialog } from "@/settings/v2/components/ModelEditDialog";
|
||||
import ProjectManager from "@/LLMProviders/projectManager";
|
||||
|
||||
export const ModelSettings: React.FC = () => {
|
||||
const settings = useSettingsValue();
|
||||
|
|
@ -94,7 +94,9 @@ export const ModelSettings: React.FC = () => {
|
|||
const updatedModels = [...settings.activeModels, model];
|
||||
updateSetting("activeModels", updatedModels);
|
||||
}}
|
||||
ping={(model) => ChatModelManager.getInstance().ping(model)}
|
||||
ping={(model) =>
|
||||
ProjectManager.instance.getCurrentChainManager().chatModelManager.ping(model)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ const ModelCard: React.FC<ModelCardProps> = ({ model, onEdit, onDelete, onUpdate
|
|||
"border-accent/50",
|
||||
"relative",
|
||||
"z-[9999]",
|
||||
"bg-background",
|
||||
"bg-primary",
|
||||
"rounded-lg",
|
||||
"transform-gpu",
|
||||
],
|
||||
|
|
@ -284,7 +284,7 @@ const SortableTableRow: React.FC<{
|
|||
className={cn(
|
||||
"hover:bg-interactive-accent/10 transition-colors duration-200",
|
||||
isDragging &&
|
||||
"shadow-lg bg-background/90 backdrop-blur-sm relative z-[100] cursor-grabbing border-2 border-accent/50",
|
||||
"shadow-lg bg-primary/90 backdrop-blur-sm relative z-[100] cursor-grabbing border-2 border-accent/50",
|
||||
!isDragging && "z-auto"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FormattedDateTime } from "./utils";
|
||||
import { TFile } from "obsidian";
|
||||
import CopilotPlugin from "@/main";
|
||||
|
||||
export interface ChatMessage {
|
||||
message: string;
|
||||
|
|
@ -18,10 +19,14 @@ export interface ChatMessage {
|
|||
}
|
||||
|
||||
class SharedState {
|
||||
constructor(private plugin: CopilotPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
chatHistory: ChatMessage[] = [];
|
||||
|
||||
addMessage(message: ChatMessage): void {
|
||||
this.chatHistory.push(message);
|
||||
this.plugin.projectManager.getCurrentChainManager().addChatMessage(message);
|
||||
}
|
||||
|
||||
getMessages(): ChatMessage[] {
|
||||
|
|
@ -30,6 +35,11 @@ class SharedState {
|
|||
|
||||
clearChatHistory(): void {
|
||||
this.chatHistory = [];
|
||||
this.plugin.projectManager.getCurrentChainManager().clearHistory();
|
||||
}
|
||||
|
||||
replaceMessages(messages: ChatMessage[]): void {
|
||||
this.chatHistory = [...messages];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,8 +58,8 @@ export function useSharedState(
|
|||
// When there are no dependencies, the effect will only run once,
|
||||
// *right after the initial render* (similar to componentDidMount in class components).
|
||||
useEffect(() => {
|
||||
setChatHistory(sharedState.getMessages());
|
||||
}, [sharedState]);
|
||||
setChatHistory([...sharedState.getMessages()]);
|
||||
}, [sharedState, sharedState.chatHistory]);
|
||||
|
||||
const addMessage = useCallback(
|
||||
(message: ChatMessage) => {
|
||||
|
|
|
|||
|
|
@ -90,16 +90,6 @@ If your plugin does not need CSS, delete this file.
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bottom-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.chat-context-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1053,3 +1053,7 @@ export function checkModelApiKey(
|
|||
export function removeThinkTags(text: string): string {
|
||||
return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
||||
}
|
||||
|
||||
export function randomUUID() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@ module.exports = {
|
|||
inherit: colors.inherit,
|
||||
current: colors.current,
|
||||
transparent: colors.transparent,
|
||||
background: "var(--background-primary)",
|
||||
primary: "var(--background-primary)",
|
||||
"primary-alt": "var(--background-primary-alt)",
|
||||
secondary: "var(--background-secondary)",
|
||||
"secondary-alt": "var(--background-secondary-alt)",
|
||||
success: "rgba(var(--color-green-rgb),0.2)",
|
||||
error: "rgba(var(--color-red-rgb),0.2)",
|
||||
modifier: {
|
||||
hover: "var(--background-modifier-hover)",
|
||||
"active-hover": "var(--background-modifier-active-hover)",
|
||||
|
|
|
|||
Loading…
Reference in a new issue