mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-28 06:22:22 +00:00
MCP working further test and improvements needed
This commit is contained in:
parent
aa5d19fb4f
commit
27ddab7e0d
7 changed files with 734 additions and 249 deletions
118
package-lock.json
generated
118
package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
|||
"@langchain/core": "^0.3.0",
|
||||
"@langchain/google-genai": "^0.1.0",
|
||||
"@langchain/langgraph": "^0.2.74",
|
||||
"@langchain/ollama": "^1.2.0",
|
||||
"@langchain/openai": "^0.3.0",
|
||||
"@sigma/edge-curve": "^3.1.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
|
@ -564,6 +565,7 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -580,6 +582,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -596,6 +599,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -612,6 +616,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -628,6 +633,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -644,6 +650,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -660,6 +667,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -676,6 +684,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -692,6 +701,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -708,6 +718,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -724,6 +735,7 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -740,6 +752,7 @@
|
|||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -756,6 +769,7 @@
|
|||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -772,6 +786,7 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -788,6 +803,7 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -804,6 +820,7 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -820,6 +837,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -836,6 +854,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -852,6 +871,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -868,6 +888,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -884,6 +905,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -900,6 +922,7 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -916,6 +939,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1722,6 +1746,35 @@
|
|||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/ollama": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/ollama/-/ollama-1.2.0.tgz",
|
||||
"integrity": "sha512-OinxIhssKXdDQKnQoBF4TQTMBuMMV5OcNPk4Zze8UjcaSOGngn3CAI1FVbBxl0bTG5ov61w4AoWWsUwOwiSJFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ollama": "^0.6.3",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/ollama/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/openai": {
|
||||
"version": "0.3.17",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.17.tgz",
|
||||
|
|
@ -2013,6 +2066,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2026,6 +2080,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2039,6 +2094,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2052,6 +2108,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2065,6 +2122,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2078,6 +2136,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2091,6 +2150,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2104,6 +2164,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2117,6 +2178,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2130,6 +2192,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2143,6 +2206,7 @@
|
|||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2156,6 +2220,7 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2169,6 +2234,7 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2182,6 +2248,7 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2195,6 +2262,7 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2208,6 +2276,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2221,6 +2290,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2234,6 +2304,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2247,6 +2318,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2260,6 +2332,7 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2273,6 +2346,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2286,6 +2360,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2505,17 +2580,6 @@
|
|||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.18",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
|
||||
"integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.25",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
|
||||
|
|
@ -3239,12 +3303,14 @@
|
|||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
|
|
@ -4225,6 +4291,7 @@
|
|||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cytoscape": {
|
||||
|
|
@ -5037,6 +5104,7 @@
|
|||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
|
@ -5678,6 +5746,7 @@
|
|||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
|
|
@ -5886,13 +5955,6 @@
|
|||
"graphology-types": ">=0.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphology-types": {
|
||||
"version": "0.24.8",
|
||||
"resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
|
||||
"integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/graphology-utils": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz",
|
||||
|
|
@ -8190,6 +8252,7 @@
|
|||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -8304,6 +8367,15 @@
|
|||
"integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ollama": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.3.tgz",
|
||||
"integrity": "sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-fetch": "^3.6.20"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
|
@ -8527,6 +8599,7 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
|
|
@ -8597,6 +8670,7 @@
|
|||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -8991,6 +9065,7 @@
|
|||
"version": "4.53.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
|
|
@ -9984,6 +10059,7 @@
|
|||
"version": "5.4.21",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
|
|
@ -10165,6 +10241,12 @@
|
|||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.20",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"@langchain/core": "^0.3.0",
|
||||
"@langchain/google-genai": "^0.1.0",
|
||||
"@langchain/langgraph": "^0.2.74",
|
||||
"@langchain/ollama": "^1.2.0",
|
||||
"@langchain/openai": "^0.3.0",
|
||||
"@sigma/edge-curve": "^3.1.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { Search, Settings, HelpCircle, Sparkles, Github, Star } from 'lucide-react';
|
||||
import { useAppState } from '../hooks/useAppState';
|
||||
import { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
||||
import { GraphNode } from '../core/graph/types';
|
||||
import { EmbeddingStatus } from './EmbeddingStatus';
|
||||
import { MCPToggle } from './MCPToggle';
|
||||
|
||||
// Color mapping for node types in search results
|
||||
const NODE_TYPE_COLORS: Record<string, string> = {
|
||||
|
|
@ -22,7 +23,17 @@ interface HeaderProps {
|
|||
}
|
||||
|
||||
export const Header = ({ onFocusNode }: HeaderProps) => {
|
||||
const { projectName, graph, openChatPanel, isRightPanelOpen, rightPanelTab, setSettingsPanelOpen } = useAppState();
|
||||
const {
|
||||
projectName,
|
||||
graph,
|
||||
openChatPanel,
|
||||
isRightPanelOpen,
|
||||
rightPanelTab,
|
||||
setSettingsPanelOpen,
|
||||
runQuery,
|
||||
semanticSearch,
|
||||
setHighlightedNodeIds,
|
||||
} = useAppState();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
|
@ -206,6 +217,35 @@ export const Header = ({ onFocusNode }: HeaderProps) => {
|
|||
{/* Embedding Status */}
|
||||
<EmbeddingStatus />
|
||||
|
||||
{/* MCP Toggle for external AI agents */}
|
||||
<MCPToggle
|
||||
showOnboardingTip={!!graph}
|
||||
onSearch={async (query, limit = 10) => {
|
||||
// Use semantic search from the app
|
||||
const results = await semanticSearch(query, limit);
|
||||
return results;
|
||||
}}
|
||||
onCypher={async (query) => {
|
||||
// Execute Cypher query
|
||||
const results = await runQuery(query);
|
||||
return results;
|
||||
}}
|
||||
onBlastRadius={async (nodeId, hops = 2) => {
|
||||
// Run blast radius query
|
||||
const query = `
|
||||
MATCH (start)-[*1..${hops}]-(connected)
|
||||
WHERE start.id = '${nodeId}' OR start.name = '${nodeId}'
|
||||
RETURN DISTINCT connected.id AS id, connected.name AS name, labels(connected) AS labels
|
||||
`;
|
||||
const results = await runQuery(query);
|
||||
return results;
|
||||
}}
|
||||
onHighlight={(nodeIds) => {
|
||||
// Highlight nodes in the graph
|
||||
setHighlightedNodeIds(new Set(nodeIds));
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Icon buttons */}
|
||||
<button
|
||||
onClick={() => setSettingsPanelOpen(true)}
|
||||
|
|
|
|||
265
src/components/MCPToggle.tsx
Normal file
265
src/components/MCPToggle.tsx
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
/**
|
||||
* MCP Toggle Component
|
||||
*
|
||||
* Toggle switch for enabling/disabling MCP exposure to external AI agents.
|
||||
* Shows connection status and setup instructions if bridge not found.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Copy, Check, AlertCircle, X, Sparkles } from 'lucide-react';
|
||||
import { getMCPClient } from '../core/mcp/mcp-client';
|
||||
|
||||
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
||||
|
||||
interface MCPToggleProps {
|
||||
/** Handler for search tool calls from external agents */
|
||||
onSearch?: (query: string, limit?: number) => Promise<any>;
|
||||
/** Handler for cypher tool calls from external agents */
|
||||
onCypher?: (query: string) => Promise<any>;
|
||||
/** Handler for blastRadius tool calls from external agents */
|
||||
onBlastRadius?: (nodeId: string, hops?: number) => Promise<any>;
|
||||
/** Handler for highlight tool calls from external agents */
|
||||
onHighlight?: (nodeIds: string[], color?: string) => void;
|
||||
/** Whether to show the onboarding tip */
|
||||
showOnboardingTip?: boolean;
|
||||
}
|
||||
|
||||
const MCP_TIP_DISMISSED_KEY = 'gitnexus-mcp-tip-dismissed';
|
||||
|
||||
export function MCPToggle({
|
||||
onSearch,
|
||||
onCypher,
|
||||
onBlastRadius,
|
||||
onHighlight,
|
||||
showOnboardingTip = false,
|
||||
}: MCPToggleProps = {}) {
|
||||
const [status, setStatus] = useState<ConnectionState>('disconnected');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [showTip, setShowTip] = useState(false);
|
||||
|
||||
const isConnected = status === 'connected';
|
||||
const isConnecting = status === 'connecting';
|
||||
|
||||
// Show tip when graph becomes ready (only once per session)
|
||||
useEffect(() => {
|
||||
if (showOnboardingTip) {
|
||||
const dismissed = localStorage.getItem(MCP_TIP_DISMISSED_KEY);
|
||||
if (!dismissed) {
|
||||
// Small delay so it appears after graph loads
|
||||
const timer = setTimeout(() => setShowTip(true), 1500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
}, [showOnboardingTip]);
|
||||
|
||||
const dismissTip = () => {
|
||||
setShowTip(false);
|
||||
localStorage.setItem(MCP_TIP_DISMISSED_KEY, 'true');
|
||||
};
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
const client = getMCPClient();
|
||||
setStatus('connecting');
|
||||
setShowTip(false);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
// Register tool handlers if provided
|
||||
if (onSearch) {
|
||||
client.registerHandler('search', async (params) => {
|
||||
return await onSearch(params.query, params.limit);
|
||||
});
|
||||
}
|
||||
|
||||
if (onCypher) {
|
||||
client.registerHandler('cypher', async (params) => {
|
||||
return await onCypher(params.query);
|
||||
});
|
||||
}
|
||||
|
||||
if (onBlastRadius) {
|
||||
client.registerHandler('blastRadius', async (params) => {
|
||||
return await onBlastRadius(params.nodeId, params.hops);
|
||||
});
|
||||
}
|
||||
|
||||
if (onHighlight) {
|
||||
client.registerHandler('highlight', async (params) => {
|
||||
onHighlight(params.nodeIds, params.color);
|
||||
return { highlighted: params.nodeIds.length };
|
||||
});
|
||||
}
|
||||
|
||||
setStatus('connected');
|
||||
setShowSetup(false);
|
||||
localStorage.setItem(MCP_TIP_DISMISSED_KEY, 'true');
|
||||
} catch {
|
||||
setStatus('error');
|
||||
setShowSetup(true);
|
||||
}
|
||||
}, [onSearch, onCypher, onBlastRadius, onHighlight]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
const client = getMCPClient();
|
||||
client.disconnect();
|
||||
setStatus('disconnected');
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
if (isConnected) {
|
||||
disconnect();
|
||||
} else if (!isConnecting) {
|
||||
connect();
|
||||
}
|
||||
}, [isConnected, isConnecting, connect, disconnect]);
|
||||
|
||||
const copyCommand = () => {
|
||||
navigator.clipboard.writeText('npx gitnexus-mcp setup');
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
// Listen for connection changes
|
||||
useEffect(() => {
|
||||
const client = getMCPClient();
|
||||
const unsubscribe = client.onConnectionChange((connected) => {
|
||||
setStatus(connected ? 'connected' : 'disconnected');
|
||||
});
|
||||
return () => { unsubscribe(); };
|
||||
}, []);
|
||||
|
||||
// Error state - show setup popup
|
||||
if (status === 'error' && showSetup) {
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<span className="text-xs text-text-primary font-medium">MCP</span>
|
||||
<button
|
||||
onClick={() => setShowSetup(!showSetup)}
|
||||
className="flex items-center gap-1.5 px-2 py-1 text-xs text-amber-400 hover:bg-amber-400/10 rounded transition-colors"
|
||||
>
|
||||
<AlertCircle className="w-3.5 h-3.5" />
|
||||
<span>Setup</span>
|
||||
</button>
|
||||
|
||||
<div className="absolute top-full right-0 mt-2 p-4 bg-surface border border-border rounded-lg shadow-xl z-50 w-80">
|
||||
<p className="text-sm text-text-secondary mb-3">
|
||||
Run this command to enable external AI agents:
|
||||
</p>
|
||||
<div className="flex items-center gap-2 bg-background p-2 rounded font-mono text-sm">
|
||||
<code className="flex-1 text-green-400">npx gitnexus-mcp setup</code>
|
||||
<button
|
||||
onClick={copyCommand}
|
||||
className="p-1 hover:bg-surface-hover rounded"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setShowSetup(false); connect(); }}
|
||||
className="mt-3 w-full px-3 py-1.5 text-sm bg-green-500/20 text-green-400 rounded hover:bg-green-500/30 transition-colors"
|
||||
>
|
||||
Retry Connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Toggle switch UI
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
<span className="text-xs text-text-primary font-medium">MCP</span>
|
||||
<button
|
||||
onClick={toggle}
|
||||
disabled={isConnecting}
|
||||
className={`
|
||||
relative w-10 h-5 rounded-full transition-colors duration-200 ease-in-out
|
||||
focus:outline-none focus:ring-2 focus:ring-green-500/50 focus:ring-offset-2 focus:ring-offset-deep
|
||||
${isConnected
|
||||
? 'bg-green-500'
|
||||
: isConnecting
|
||||
? 'bg-surface-hover cursor-wait'
|
||||
: 'bg-surface-hover hover:bg-border'
|
||||
}
|
||||
`}
|
||||
title={isConnected
|
||||
? 'MCP Connected - Click to disconnect'
|
||||
: isConnecting
|
||||
? 'Connecting...'
|
||||
: 'Connect to MCP bridge for external AI tools'
|
||||
}
|
||||
>
|
||||
{/* Toggle knob */}
|
||||
<span
|
||||
className={`
|
||||
absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow-sm
|
||||
transition-transform duration-200 ease-in-out
|
||||
${isConnected ? 'translate-x-5' : 'translate-x-0'}
|
||||
${isConnecting ? 'animate-pulse' : ''}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
{isConnected && (
|
||||
<span className="text-xs text-green-400 font-medium hidden sm:inline">Connected</span>
|
||||
)}
|
||||
|
||||
{/* Onboarding Tip Popup */}
|
||||
{showTip && !isConnected && (
|
||||
<div className="absolute top-full right-0 mt-3 w-80 p-4 bg-surface border border-green-500/30 rounded-lg shadow-xl z-50 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
<button
|
||||
onClick={dismissTip}
|
||||
className="absolute top-2 right-2 p-1 text-text-muted hover:text-text-primary rounded transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="p-2 bg-green-500/20 rounded-lg flex-shrink-0">
|
||||
<Sparkles className="w-5 h-5 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-text-primary mb-1">
|
||||
Connect your AI coding tool
|
||||
</h4>
|
||||
<p className="text-xs text-text-secondary leading-relaxed">
|
||||
Let Cursor, Claude Code, or Antigravity use GitNexus for code intelligence.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-xs text-text-muted mb-2">Run this in your terminal:</p>
|
||||
<div className="flex items-center gap-2 bg-background p-2 rounded font-mono text-sm">
|
||||
<code className="flex-1 text-green-400">npx gitnexus-mcp setup</code>
|
||||
<button
|
||||
onClick={copyCommand}
|
||||
className="p-1 hover:bg-surface-hover rounded"
|
||||
title="Copy command"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-4 h-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-text-secondary" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => { dismissTip(); connect(); }}
|
||||
className="w-full px-3 py-2 text-sm font-medium bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
|
||||
>
|
||||
I've run setup — Connect Now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw, Loader2 } from 'lucide-react';
|
||||
import { X, Key, Server, Brain, Check, AlertCircle, Eye, EyeOff, RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
loadSettings,
|
||||
saveSettings,
|
||||
getProviderDisplayName,
|
||||
getAvailableModels,
|
||||
} from '../core/llm/settings-service';
|
||||
import type { LLMSettings, LLMProvider } from '../core/llm/types';
|
||||
|
||||
|
|
@ -15,45 +14,28 @@ interface SettingsPanelProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch available Gemini models from the API
|
||||
* Check connection to local Ollama instance
|
||||
*/
|
||||
const fetchGeminiModels = async (apiKey: string): Promise<string[]> => {
|
||||
const checkOllamaStatus = async (baseUrl: string): Promise<{ ok: boolean; error: string | null }> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
||||
);
|
||||
const response = await fetch(`${baseUrl}/api/tags`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`);
|
||||
if (response.status === 0 || response.status === 404) {
|
||||
return { ok: false, error: 'Cannot connect to Ollama. Make sure it\'s running with `ollama serve`' };
|
||||
}
|
||||
return { ok: false, error: `Ollama API error: ${response.status}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Filter for chat-capable models and extract model names
|
||||
const models = (data.models || [])
|
||||
.filter((model: any) => {
|
||||
const methods = model.supportedGenerationMethods || [];
|
||||
return methods.includes('generateContent');
|
||||
})
|
||||
.map((model: any) => {
|
||||
const name = model.name || '';
|
||||
return name.replace('models/', '');
|
||||
})
|
||||
.filter((name: string) => name.length > 0)
|
||||
.sort((a: string, b: string) => {
|
||||
const score = (n: string) => {
|
||||
if (n.includes('gemini-2')) return 0;
|
||||
if (n.includes('gemini-1.5')) return 1;
|
||||
if (n.includes('gemini-1')) return 2;
|
||||
return 3;
|
||||
};
|
||||
return score(a) - score(b) || a.localeCompare(b);
|
||||
});
|
||||
|
||||
return models;
|
||||
return { ok: true, error: null };
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch Gemini models:', error);
|
||||
return [];
|
||||
return {
|
||||
ok: false,
|
||||
error: 'Cannot connect to Ollama. Make sure it\'s running with `ollama serve`'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -61,59 +43,38 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
const [settings, setSettings] = useState<LLMSettings>(loadSettings);
|
||||
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
|
||||
const [saveStatus, setSaveStatus] = useState<'idle' | 'saved' | 'error'>('idle');
|
||||
|
||||
// Gemini model fetching state
|
||||
const [geminiModels, setGeminiModels] = useState<string[]>([]);
|
||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||
const [modelFetchError, setModelFetchError] = useState<string | null>(null);
|
||||
const [useCustomModel, setUseCustomModel] = useState(false);
|
||||
const [useCustomAnthropicModel, setUseCustomAnthropicModel] = useState(false);
|
||||
// Ollama connection state
|
||||
const [ollamaError, setOllamaError] = useState<string | null>(null);
|
||||
const [isCheckingOllama, setIsCheckingOllama] = useState(false);
|
||||
|
||||
// Load settings when panel opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSettings(loadSettings());
|
||||
setSaveStatus('idle');
|
||||
setGeminiModels([]);
|
||||
setModelFetchError(null);
|
||||
setUseCustomModel(false);
|
||||
setUseCustomAnthropicModel(false);
|
||||
setOllamaError(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Auto-fetch models when Gemini API key changes
|
||||
const fetchModels = useCallback(async (apiKey: string) => {
|
||||
if (!apiKey || apiKey.length < 10) {
|
||||
setGeminiModels([]);
|
||||
return;
|
||||
}
|
||||
// Check Ollama connection when provider is selected or base URL changes
|
||||
const checkOllamaConnection = useCallback(async (baseUrl: string) => {
|
||||
setIsCheckingOllama(true);
|
||||
setOllamaError(null);
|
||||
|
||||
setIsLoadingModels(true);
|
||||
setModelFetchError(null);
|
||||
|
||||
const models = await fetchGeminiModels(apiKey);
|
||||
|
||||
setIsLoadingModels(false);
|
||||
|
||||
if (models.length > 0) {
|
||||
setGeminiModels(models);
|
||||
setModelFetchError(null);
|
||||
} else {
|
||||
setGeminiModels([]);
|
||||
setModelFetchError('Could not fetch models. Check your API key or enter model manually.');
|
||||
}
|
||||
const { error } = await checkOllamaStatus(baseUrl);
|
||||
setIsCheckingOllama(false);
|
||||
setOllamaError(error);
|
||||
}, []);
|
||||
|
||||
// Fetch models when API key is entered (debounced)
|
||||
useEffect(() => {
|
||||
if (settings.activeProvider === 'gemini' && settings.gemini?.apiKey) {
|
||||
const apiKey = settings.gemini.apiKey ?? '';
|
||||
if (settings.activeProvider === 'ollama') {
|
||||
const baseUrl = settings.ollama?.baseUrl ?? 'http://localhost:11434';
|
||||
const timer = setTimeout(() => {
|
||||
fetchModels(apiKey);
|
||||
}, 500);
|
||||
checkOllamaConnection(baseUrl);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [settings.gemini?.apiKey, settings.activeProvider, fetchModels]);
|
||||
}, [settings.ollama?.baseUrl, settings.activeProvider, checkOllamaConnection]);
|
||||
|
||||
const handleProviderChange = (provider: LLMProvider) => {
|
||||
setSettings(prev => ({ ...prev, activeProvider: provider }));
|
||||
|
|
@ -136,17 +97,8 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai'];
|
||||
const providers: LLMProvider[] = ['openai', 'gemini', 'anthropic', 'azure-openai', 'ollama'];
|
||||
|
||||
const availableGeminiModels = geminiModels.length > 0
|
||||
? geminiModels
|
||||
: getAvailableModels('gemini');
|
||||
const currentGeminiModel = settings.gemini?.model ?? 'gemini-2.0-flash';
|
||||
const isCustomModelSelected = !availableGeminiModels.includes(currentGeminiModel);
|
||||
|
||||
const availableAnthropicModels = getAvailableModels('anthropic');
|
||||
const currentAnthropicModel = settings.anthropic?.model ?? 'claude-sonnet-4-20250514';
|
||||
const isCustomAnthropicModelSelected = !availableAnthropicModels.includes(currentAnthropicModel);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
|
|
@ -201,7 +153,7 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
w-8 h-8 rounded-lg flex items-center justify-center text-lg
|
||||
${settings.activeProvider === provider ? 'bg-accent/20' : 'bg-surface'}
|
||||
`}>
|
||||
{provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : '☁️'}
|
||||
{provider === 'openai' ? '🤖' : provider === 'gemini' ? '💎' : provider === 'anthropic' ? '🧠' : provider === 'ollama' ? '🦙' : '☁️'}
|
||||
</div>
|
||||
<span className="font-medium">{getProviderDisplayName(provider)}</span>
|
||||
</button>
|
||||
|
|
@ -251,18 +203,16 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-secondary">Model</label>
|
||||
<select
|
||||
<input
|
||||
type="text"
|
||||
value={settings.openai?.model ?? 'gpt-4o'}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
openai: { ...prev.openai!, model: e.target.value }
|
||||
}))}
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all appearance-none cursor-pointer"
|
||||
>
|
||||
{getAvailableModels('openai').map(model => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
placeholder="e.g., gpt-4o, gpt-4-turbo, gpt-3.5-turbo"
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
@ -328,92 +278,17 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-text-secondary">Model</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{isLoadingModels && (
|
||||
<span className="flex items-center gap-1 text-xs text-text-muted">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
Fetching models...
|
||||
</span>
|
||||
)}
|
||||
{geminiModels.length > 0 && (
|
||||
<span className="text-xs text-green-400">
|
||||
{geminiModels.length} models available
|
||||
</span>
|
||||
)}
|
||||
{settings.gemini?.apiKey && !isLoadingModels && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fetchModels(settings.gemini?.apiKey ?? '')}
|
||||
className="p-1 text-text-muted hover:text-text-primary transition-colors"
|
||||
title="Refresh models"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model selector or manual input */}
|
||||
{(useCustomModel || isCustomModelSelected) ? (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.gemini?.model ?? ''}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
gemini: { ...prev.gemini!, model: e.target.value }
|
||||
}))}
|
||||
placeholder="e.g., gemini-2.0-flash-exp"
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setUseCustomModel(false);
|
||||
if (availableGeminiModels.length > 0) {
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
gemini: { ...prev.gemini!, model: availableGeminiModels[0] }
|
||||
}));
|
||||
}
|
||||
}}
|
||||
className="text-xs text-accent hover:underline"
|
||||
>
|
||||
← Back to model list
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<select
|
||||
value={settings.gemini?.model ?? 'gemini-2.0-flash'}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
gemini: { ...prev.gemini!, model: e.target.value }
|
||||
}))}
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all appearance-none cursor-pointer"
|
||||
>
|
||||
{availableGeminiModels.map(model => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUseCustomModel(true)}
|
||||
className="text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
>
|
||||
Enter model name manually →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modelFetchError && !useCustomModel && (
|
||||
<p className="text-xs text-amber-400 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{modelFetchError}
|
||||
</p>
|
||||
)}
|
||||
<label className="text-sm font-medium text-text-secondary">Model</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.gemini?.model ?? 'gemini-2.0-flash'}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
gemini: { ...prev.gemini!, model: e.target.value }
|
||||
}))}
|
||||
placeholder="e.g., gemini-2.0-flash, gemini-1.5-pro"
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -460,58 +335,16 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-secondary">Model</label>
|
||||
|
||||
{(useCustomAnthropicModel || isCustomAnthropicModelSelected) ? (
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={settings.anthropic?.model ?? ''}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
anthropic: { ...prev.anthropic!, model: e.target.value }
|
||||
}))}
|
||||
placeholder="e.g., claude-sonnet-4-20250514"
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setUseCustomAnthropicModel(false);
|
||||
if (availableAnthropicModels.length > 0) {
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
anthropic: { ...prev.anthropic!, model: availableAnthropicModels[0] }
|
||||
}));
|
||||
}
|
||||
}}
|
||||
className="text-xs text-accent hover:underline"
|
||||
>
|
||||
← Back to model list
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<select
|
||||
value={currentAnthropicModel}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
anthropic: { ...prev.anthropic!, model: e.target.value }
|
||||
}))}
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all appearance-none cursor-pointer"
|
||||
>
|
||||
{availableAnthropicModels.map(model => (
|
||||
<option key={model} value={model}>{model}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUseCustomAnthropicModel(true)}
|
||||
className="text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
>
|
||||
Enter model name manually →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={settings.anthropic?.model ?? 'claude-sonnet-4-20250514'}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
anthropic: { ...prev.anthropic!, model: e.target.value }
|
||||
}))}
|
||||
placeholder="e.g., claude-sonnet-4-20250514, claude-3-opus"
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -620,6 +453,87 @@ export const SettingsPanel = ({ isOpen, onClose, onSettingsSaved }: SettingsPane
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Ollama Settings */}
|
||||
{settings.activeProvider === 'ollama' && (
|
||||
<div className="space-y-4 animate-fade-in">
|
||||
{/* How to run Ollama */}
|
||||
<div className="p-3 bg-amber-500/10 border border-amber-500/30 rounded-xl">
|
||||
<p className="text-xs text-amber-300 leading-relaxed">
|
||||
<span className="font-medium">📋 Quick Start:</span> Install Ollama from{' '}
|
||||
<a
|
||||
href="https://ollama.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
ollama.ai
|
||||
</a>, then run:
|
||||
</p>
|
||||
<code className="block mt-2 px-3 py-2 bg-black/30 rounded-lg text-amber-200 font-mono text-sm">
|
||||
ollama serve
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-text-secondary">
|
||||
<Server className="w-4 h-4" />
|
||||
Base URL
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="url"
|
||||
value={settings.ollama?.baseUrl ?? 'http://localhost:11434'}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
ollama: { ...prev.ollama!, baseUrl: e.target.value }
|
||||
}))}
|
||||
placeholder="http://localhost:11434"
|
||||
className="flex-1 px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => checkOllamaConnection(settings.ollama?.baseUrl ?? 'http://localhost:11434')}
|
||||
disabled={isCheckingOllama}
|
||||
className="px-3 py-3 bg-elevated border border-border-subtle rounded-xl text-text-secondary hover:text-text-primary hover:border-accent/50 transition-colors disabled:opacity-50"
|
||||
title="Check connection"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isCheckingOllama ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
Default port is <code className="px-1 py-0.5 bg-elevated rounded">11434</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-text-secondary">Model</label>
|
||||
|
||||
{ollamaError && !isCheckingOllama && (
|
||||
<div className="p-2 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<p className="text-xs text-red-400 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{ollamaError}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={settings.ollama?.model ?? ''}
|
||||
onChange={e => setSettings(prev => ({
|
||||
...prev,
|
||||
ollama: { ...prev.ollama!, model: e.target.value }
|
||||
}))}
|
||||
placeholder="e.g., llama3.2, mistral, codellama"
|
||||
className="w-full px-4 py-3 bg-elevated border border-border-subtle rounded-xl text-text-primary placeholder:text-text-muted focus:border-accent focus:ring-2 focus:ring-accent/20 outline-none transition-all font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-text-muted">
|
||||
Pull a model with <code className="px-1 py-0.5 bg-elevated rounded">ollama pull llama3.2</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Privacy Note */}
|
||||
<div className="p-4 bg-elevated/50 border border-border-subtle rounded-xl">
|
||||
<div className="flex gap-3">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { SystemMessage } from '@langchain/core/messages';
|
|||
import { ChatOpenAI, AzureChatOpenAI } from '@langchain/openai';
|
||||
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
|
||||
import { ChatAnthropic } from '@langchain/anthropic';
|
||||
import { ChatOllama } from '@langchain/ollama';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { createGraphRAGTools } from './tools';
|
||||
import type {
|
||||
|
|
@ -18,6 +19,7 @@ import type {
|
|||
AzureOpenAIConfig,
|
||||
GeminiConfig,
|
||||
AnthropicConfig,
|
||||
OllamaConfig,
|
||||
AgentStreamChunk,
|
||||
} from './types';
|
||||
import {
|
||||
|
|
@ -160,6 +162,21 @@ export const createChatModel = (config: ProviderConfig): BaseChatModel => {
|
|||
});
|
||||
}
|
||||
|
||||
case 'ollama': {
|
||||
const ollamaConfig = config as OllamaConfig;
|
||||
return new ChatOllama({
|
||||
baseUrl: ollamaConfig.baseUrl ?? 'http://localhost:11434',
|
||||
model: ollamaConfig.model,
|
||||
temperature: ollamaConfig.temperature ?? 0.1,
|
||||
streaming: true,
|
||||
// Allow longer responses (Ollama default is often 128-2048)
|
||||
numPredict: 30000,
|
||||
// Increase context window (Ollama default is only 2048!)
|
||||
// This is critical for agentic workflows with tool calls
|
||||
numCtx: 32768,
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported provider: ${(config as any).provider}`);
|
||||
}
|
||||
|
|
@ -289,6 +306,13 @@ export async function* streamAgentResponse(
|
|||
data = event;
|
||||
}
|
||||
|
||||
// DEBUG: Enhanced logging
|
||||
if (import.meta.env.DEV) {
|
||||
const msgType = mode === 'messages' && data?.[0]?._getType?.() || 'n/a';
|
||||
const hasContent = mode === 'messages' && data?.[0]?.content;
|
||||
const hasToolCalls = mode === 'messages' && data?.[0]?.tool_calls?.length > 0;
|
||||
console.log(`🔄 [${mode}] type:${msgType} content:${!!hasContent} tools:${hasToolCalls}`);
|
||||
}
|
||||
// Handle 'messages' mode - token-by-token streaming
|
||||
if (mode === 'messages') {
|
||||
const [msg] = Array.isArray(data) ? data : [data];
|
||||
|
|
@ -298,11 +322,23 @@ export async function* streamAgentResponse(
|
|||
|
||||
// AIMessageChunk - streaming text tokens
|
||||
if (msgType === 'ai' || msgType === 'AIMessage' || msgType === 'AIMessageChunk') {
|
||||
const content = msg.content;
|
||||
const rawContent = msg.content;
|
||||
const toolCalls = msg.tool_calls || [];
|
||||
|
||||
// Handle content that can be string or array of content blocks
|
||||
let content: string = '';
|
||||
if (typeof rawContent === 'string') {
|
||||
content = rawContent;
|
||||
} else if (Array.isArray(rawContent)) {
|
||||
// Content blocks format: [{type: 'text', text: '...'}, ...]
|
||||
content = rawContent
|
||||
.filter((block: any) => block.type === 'text' || typeof block === 'string')
|
||||
.map((block: any) => typeof block === 'string' ? block : block.text || '')
|
||||
.join('');
|
||||
}
|
||||
|
||||
// If chunk has content, stream it
|
||||
if (content && typeof content === 'string' && content.length > 0) {
|
||||
if (content && content.length > 0) {
|
||||
// Determine if this is reasoning/narration vs final answer content.
|
||||
// - Before the first tool call: treat as reasoning (narration)
|
||||
// - Between tool calls/results: treat as reasoning
|
||||
|
|
@ -416,9 +452,17 @@ export async function* streamAgentResponse(
|
|||
}
|
||||
}
|
||||
|
||||
// DEBUG: Stream completed normally
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('✅ Stream completed normally, yielding done');
|
||||
}
|
||||
yield { type: 'done' };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
// DEBUG: Stream error
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('❌ Stream error:', message, error);
|
||||
}
|
||||
yield {
|
||||
type: 'error',
|
||||
error: message,
|
||||
|
|
|
|||
139
src/core/mcp/mcp-client.ts
Normal file
139
src/core/mcp/mcp-client.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* MCP Browser Client
|
||||
*
|
||||
* WebSocket client that connects to the gitnexus-mcp bridge.
|
||||
* Receives tool calls from external AI agents and executes them.
|
||||
*/
|
||||
|
||||
export interface MCPMessage {
|
||||
id: string;
|
||||
method?: string;
|
||||
params?: Record<string, any>;
|
||||
result?: any;
|
||||
error?: { message: string };
|
||||
}
|
||||
|
||||
type ToolHandler = (params: Record<string, any>) => Promise<any>;
|
||||
|
||||
export class MCPBrowserClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private handlers: Map<string, ToolHandler> = new Map();
|
||||
private connectionListeners: Set<(connected: boolean) => void> = new Set();
|
||||
|
||||
constructor(private port = 54319) {}
|
||||
|
||||
/**
|
||||
* Connect to the MCP bridge
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.ws = new WebSocket(`ws://localhost:${this.port}`);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.notifyConnectionListeners(true);
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.ws.onerror = (event) => {
|
||||
this.notifyConnectionListeners(false);
|
||||
reject(new Error('Failed to connect to MCP bridge'));
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg: MCPMessage = JSON.parse(event.data);
|
||||
this.handleMessage(msg);
|
||||
} catch (error) {
|
||||
console.error('[MCP] Failed to parse message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.ws = null;
|
||||
this.notifyConnectionListeners(false);
|
||||
};
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming messages from bridge
|
||||
*/
|
||||
private async handleMessage(msg: MCPMessage) {
|
||||
// This is a tool call request from an external agent
|
||||
if (msg.method && msg.id) {
|
||||
const handler = this.handlers.get(msg.method);
|
||||
|
||||
if (handler) {
|
||||
try {
|
||||
const result = await handler(msg.params || {});
|
||||
this.send({ id: msg.id, result });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
this.send({ id: msg.id, error: { message } });
|
||||
}
|
||||
} else {
|
||||
this.send({
|
||||
id: msg.id,
|
||||
error: { message: `Unknown tool: ${msg.method}` }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the bridge
|
||||
*/
|
||||
private send(msg: MCPMessage) {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a handler for a tool
|
||||
*/
|
||||
registerHandler(method: string, handler: ToolHandler) {
|
||||
this.handlers.set(method, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for connection state changes
|
||||
*/
|
||||
onConnectionChange(listener: (connected: boolean) => void) {
|
||||
this.connectionListeners.add(listener);
|
||||
return () => this.connectionListeners.delete(listener);
|
||||
}
|
||||
|
||||
private notifyConnectionListeners(connected: boolean) {
|
||||
this.connectionListeners.forEach(listener => listener(connected));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected
|
||||
*/
|
||||
get isConnected(): boolean {
|
||||
return this.ws?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from bridge
|
||||
*/
|
||||
disconnect() {
|
||||
this.ws?.close();
|
||||
this.ws = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let mcpClientInstance: MCPBrowserClient | null = null;
|
||||
|
||||
export function getMCPClient(): MCPBrowserClient {
|
||||
if (!mcpClientInstance) {
|
||||
mcpClientInstance = new MCPBrowserClient();
|
||||
}
|
||||
return mcpClientInstance;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue