diff --git a/docker-compose.yml b/docker-compose.yml
index a3ea930c..ac95fe8a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,6 +12,9 @@ services:
timeout: 5s
retries: 5
app:
+ build:
+ context: ./nextjs
+ dockerfile: Dockerfile
image: weamai-app:latest
container_name: weam-frontend-container
ports:
diff --git a/nextjs/package-lock.json b/nextjs/package-lock.json
index b0e9756c..359bca40 100644
--- a/nextjs/package-lock.json
+++ b/nextjs/package-lock.json
@@ -29,6 +29,7 @@
"@reduxjs/toolkit": "^2.2.5",
"@sentry/nextjs": "^9.11.0",
"@tanstack/react-table": "^8.17.3",
+ "@types/jspdf": "^1.3.3",
"axios": "^1.7.2",
"chart.js": "^4.4.3",
"class-variance-authority": "^0.7.0",
@@ -93,7 +94,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
- "dev": true,
"engines": {
"node": ">=10"
},
@@ -1958,6 +1958,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -1973,6 +1990,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@eslint/eslintrc/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -2754,7 +2778,6 @@
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@@ -2771,7 +2794,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -2783,7 +2805,6 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -2811,6 +2832,17 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
@@ -3380,7 +3412,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@@ -3393,7 +3424,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
"engines": {
"node": ">= 8"
}
@@ -3402,7 +3432,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@@ -3922,7 +3951,6 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "dev": true,
"optional": true,
"engines": {
"node": ">=14"
@@ -5860,17 +5888,6 @@
}
}
},
- "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@rollup/pluginutils": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz",
@@ -5892,17 +5909,6 @@
}
}
},
- "node_modules/@rollup/pluginutils/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.46.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.1.tgz",
@@ -6727,6 +6733,28 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/eslint": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
+ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.7",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
+ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -6760,8 +6788,7 @@
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="
},
"node_modules/@types/json5": {
"version": "0.0.29",
@@ -6769,6 +6796,12 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/jspdf": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@types/jspdf/-/jspdf-1.3.3.tgz",
+ "integrity": "sha512-DqwyAKpVuv+7DniCp2Deq1xGvfdnKSNgl9Agun2w6dFvR5UKamiv4VfYUgcypd8S9ojUyARFIlZqBrYrBMQlew==",
+ "license": "MIT"
+ },
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@@ -7003,6 +7036,181 @@
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
},
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "license": "BSD-3-Clause",
+ "peer": true
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "license": "Apache-2.0",
+ "peer": true
+ },
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
@@ -7031,6 +7239,19 @@
"acorn": "^8"
}
},
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -7052,15 +7273,15 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
"dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
@@ -7071,7 +7292,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
- "dev": true,
"dependencies": {
"ajv": "^8.0.0"
},
@@ -7084,33 +7304,10 @@
}
}
},
- "node_modules/ajv-formats/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
- "dev": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true
- },
"node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
- "dev": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
@@ -7151,8 +7348,7 @@
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
- "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
- "dev": true
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
},
"node_modules/anymatch": {
"version": "3.1.3",
@@ -7166,11 +7362,22 @@
"node": ">= 8"
}
},
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
- "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
- "dev": true
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
},
"node_modules/argparse": {
"version": "2.0.1",
@@ -7764,8 +7971,7 @@
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"node_modules/busboy": {
"version": "1.6.0",
@@ -7809,7 +8015,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
- "dev": true,
"engines": {
"node": ">= 6"
}
@@ -7935,6 +8140,16 @@
"node": ">= 6"
}
},
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
"node_modules/cjs-module-lexer": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
@@ -8652,7 +8867,6 @@
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
- "dev": true,
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
@@ -8692,7 +8906,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true,
"bin": {
"cssesc": "bin/cssesc"
},
@@ -8924,8 +9137,7 @@
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
- "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
- "dev": true
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
},
"node_modules/diff": {
"version": "4.0.2",
@@ -8951,8 +9163,7 @@
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
- "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
- "dev": true
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
},
"node_modules/doctrine": {
"version": "3.0.0",
@@ -8989,8 +9200,7 @@
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/editorconfig": {
"version": "1.0.4",
@@ -9077,8 +9287,7 @@
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/emotion": {
"version": "10.0.27",
@@ -9111,10 +9320,10 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.17.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz",
- "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==",
- "dev": true,
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
+ "license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
@@ -9249,6 +9458,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/es-object-atoms": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
@@ -9673,6 +9889,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/eslint/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -9704,6 +9937,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/eslint/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -9773,7 +10013,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -9785,7 +10024,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
"engines": {
"node": ">=4.0"
}
@@ -9813,6 +10051,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -9821,14 +10069,12 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
- "dev": true,
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -9844,7 +10090,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -9856,7 +10101,8 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
@@ -9868,7 +10114,6 @@
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -9884,7 +10129,6 @@
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
- "dev": true,
"dependencies": {
"reusify": "^1.0.4"
}
@@ -10092,7 +10336,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
- "dev": true,
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
@@ -10296,7 +10539,6 @@
"version": "10.3.10",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
"integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
- "dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.5",
@@ -10318,7 +10560,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -10326,11 +10567,17 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "license": "BSD-2-Clause",
+ "peer": true
+ },
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
- "dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -10339,7 +10586,6 @@
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
- "dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -11261,7 +11507,6 @@
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
- "dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@@ -11275,11 +11520,41 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
"node_modules/jiti": {
"version": "1.21.3",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.3.tgz",
"integrity": "sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw==",
- "dev": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -11354,10 +11629,10 @@
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -11563,7 +11838,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
- "dev": true,
"engines": {
"node": ">=10"
}
@@ -11573,6 +11847,16 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
+ "node_modules/loader-runner": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
+ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.11.5"
+ }
+ },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -11988,11 +12272,17 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"dev": true
},
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
"engines": {
"node": ">= 8"
}
@@ -12536,7 +12826,6 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
- "dev": true,
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
@@ -12545,6 +12834,18 @@
"node": ">=8.6"
}
},
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -12624,7 +12925,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
- "dev": true,
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
@@ -12654,6 +12954,13 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/next": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz",
@@ -12803,7 +13110,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "dev": true,
"engines": {
"node": ">= 6"
}
@@ -13049,7 +13355,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -13116,11 +13421,12 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -13130,7 +13436,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13139,7 +13444,6 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
- "dev": true,
"engines": {
"node": ">= 6"
}
@@ -13259,7 +13563,6 @@
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -13304,7 +13607,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
- "dev": true,
"dependencies": {
"camelcase-css": "^2.0.1"
},
@@ -13323,7 +13625,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -13358,7 +13659,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
"integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
- "dev": true,
"engines": {
"node": ">=14"
},
@@ -13370,7 +13670,6 @@
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz",
"integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==",
- "dev": true,
"bin": {
"yaml": "bin.mjs"
},
@@ -13382,7 +13681,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
"integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
- "dev": true,
"dependencies": {
"postcss-selector-parser": "^6.0.11"
},
@@ -13428,7 +13726,6 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
"integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
- "dev": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -13440,8 +13737,7 @@
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
- "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
- "dev": true
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/postgres-array": {
"version": "2.0.0",
@@ -13590,6 +13886,7 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -13608,7 +13905,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -13624,6 +13920,16 @@
}
]
},
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/rc-steps": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz",
@@ -14094,7 +14400,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
- "dev": true,
"dependencies": {
"pify": "^2.3.0"
}
@@ -14125,6 +14430,18 @@
"node": ">=8.10.0"
}
},
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/recast": {
"version": "0.11.23",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz",
@@ -14441,7 +14758,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14501,7 +14817,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
- "dev": true,
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@@ -14586,7 +14901,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -14668,7 +14982,6 @@
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
- "dev": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
@@ -14683,28 +14996,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/schema-utils/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
- "dev": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/schema-utils/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true
- },
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
@@ -14716,6 +15007,16 @@
"node": ">=10"
}
},
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -14796,7 +15097,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
"dependencies": {
"shebang-regex": "^3.0.0"
},
@@ -14808,7 +15108,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -14840,7 +15139,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
"engines": {
"node": ">=14"
},
@@ -14912,6 +15210,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/source-map-support/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
@@ -14961,7 +15280,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@@ -14979,7 +15297,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -14992,14 +15309,12 @@
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -15011,7 +15326,6 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -15126,7 +15440,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -15206,7 +15519,6 @@
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
- "dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
"commander": "^4.0.0",
@@ -15228,7 +15540,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
- "dev": true,
"engines": {
"node": ">= 6"
}
@@ -15271,7 +15582,6 @@
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
"integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==",
- "dev": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -15316,7 +15626,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
- "dev": true,
"dependencies": {
"postcss-value-parser": "^4.0.0",
"read-cache": "^1.0.0",
@@ -15333,11 +15642,71 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
- "dev": true,
"engines": {
"node": ">=6"
}
},
+ "node_modules/terser": {
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
+ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.14",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
+ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "serialize-javascript": "^6.0.2",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -15348,7 +15717,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
- "dev": true,
"dependencies": {
"any-promise": "^1.0.0"
}
@@ -15357,7 +15725,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
- "dev": true,
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
@@ -15439,8 +15806,7 @@
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
- "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
- "dev": true
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
@@ -15800,6 +16166,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
@@ -15869,8 +16236,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "9.0.1",
@@ -15911,11 +16277,74 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/watchpack": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
+ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
+ "node_modules/webpack": {
+ "version": "5.101.3",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
+ "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.7",
+ "@types/estree": "^1.0.8",
+ "@types/json-schema": "^7.0.15",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.15.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.24.0",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.17.3",
+ "es-module-lexer": "^1.2.1",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.2.0",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^4.3.2",
+ "tapable": "^2.1.1",
+ "terser-webpack-plugin": "^5.3.11",
+ "watchpack": "^2.4.1",
+ "webpack-sources": "^3.3.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
"node_modules/webpack-sources": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
@@ -15929,6 +16358,30 @@
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw=="
},
+ "node_modules/webpack/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/webpack/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "license": "BSD-2-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/websocket-driver": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
@@ -16076,7 +16529,6 @@
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@@ -16094,7 +16546,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -16110,14 +16561,12 @@
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -16131,7 +16580,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -16143,7 +16591,6 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
- "dev": true,
"engines": {
"node": ">=12"
},
@@ -16155,7 +16602,6 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
diff --git a/nextjs/src/components/AiModel/OllamaModelProvider.tsx b/nextjs/src/components/AiModel/OllamaModelProvider.tsx
new file mode 100644
index 00000000..7dbf9a4d
--- /dev/null
+++ b/nextjs/src/components/AiModel/OllamaModelProvider.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import Image from 'next/image';
+import CommonInput from '@/widgets/CommonInput';
+import ValidationError from '@/widgets/ValidationError';
+import useOllama from '@/hooks/aiModal/useOllama';
+
+const OllamaModelProvider = ({ configs }) => {
+ const { register, handleSubmit, ollamaHealthCheck, loading, errors } = useOllama();
+
+ return (
+
+
+
+ {/* Base URL Input */}
+
+
+
+
+
+
+ {/* API Key Input (Optional) */}
+
+
+
+
+
+
+
+
+ Leave empty if your Ollama instance doesn't require authentication
+
+
+
+ );
+};
+
+export default OllamaModelProvider;
diff --git a/nextjs/src/components/Ollama/OllamaSettings.tsx b/nextjs/src/components/Ollama/OllamaSettings.tsx
new file mode 100644
index 00000000..54899808
--- /dev/null
+++ b/nextjs/src/components/Ollama/OllamaSettings.tsx
@@ -0,0 +1,254 @@
+import React, { useState, useEffect } from 'react';
+import Image from 'next/image';
+import { useForm } from 'react-hook-form';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+import ValidationError from '@/widgets/ValidationError';
+import commonApi from '@/api';
+import { MODULE_ACTIONS, RESPONSE_STATUS_CODE } from '@/utils/constant';
+
+// Validation schema for Ollama settings
+const ollamaSettingsSchema = yup.object({
+ baseUrl: yup.string().url('Please enter a valid URL').required('Base URL is required'),
+ apiKey: yup.string().optional(),
+});
+
+interface OllamaSettingsProps {
+ configs?: {
+ baseUrl?: string;
+ apiKey?: string;
+ };
+ setApiKeyUpdated?: (updated: boolean) => void;
+ setShowCancelAPI?: (show: boolean) => void;
+}
+
+const OllamaSettings: React.FC = ({
+ configs,
+ setApiKeyUpdated,
+ setShowCancelAPI
+}) => {
+ const [loading, setLoading] = useState(false);
+ const [connectionStatus, setConnectionStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
+ const [availableModels, setAvailableModels] = useState([]);
+
+ const {
+ register,
+ handleSubmit,
+ watch,
+ formState: { errors },
+ setValue
+ } = useForm({
+ mode: 'onSubmit',
+ resolver: yupResolver(ollamaSettingsSchema),
+ defaultValues: {
+ baseUrl: configs?.baseUrl || 'http://localhost:11434',
+ apiKey: configs?.apiKey || ''
+ }
+ });
+
+ const baseUrl = watch('baseUrl');
+
+ // Test connection to Ollama instance
+ const testConnection = async (url: string, apiKey?: string) => {
+ try {
+ setConnectionStatus('testing');
+
+ const response = await commonApi({
+ action: MODULE_ACTIONS.TEST_OLLAMA_CONNECTION,
+ data: {
+ baseUrl: url,
+ apiKey: apiKey || undefined
+ }
+ });
+
+ if (response.code === RESPONSE_STATUS_CODE.SUCCESS) {
+ setConnectionStatus('success');
+ setAvailableModels(response.data?.availableModels || []);
+ return true;
+ } else {
+ setConnectionStatus('error');
+ return false;
+ }
+ } catch (error) {
+ setConnectionStatus('error');
+ console.error('Connection test failed:', error);
+ return false;
+ }
+ };
+
+ const handleSaveSettings = async (data: any) => {
+ try {
+ setLoading(true);
+
+ // First test the connection
+ const connectionSuccess = await testConnection(data.baseUrl, data.apiKey);
+
+ if (!connectionSuccess) {
+ throw new Error('Failed to connect to Ollama instance');
+ }
+
+ // Save the settings
+ const response = await commonApi({
+ action: MODULE_ACTIONS.SAVE_OLLAMA_SETTINGS,
+ data: {
+ baseUrl: data.baseUrl,
+ apiKey: data.apiKey,
+ provider: 'ollama'
+ }
+ });
+
+ if (response.code === RESPONSE_STATUS_CODE.SUCCESS) {
+ setApiKeyUpdated?.(true);
+ setShowCancelAPI?.(true);
+ }
+
+ } catch (error) {
+ console.error('Error saving Ollama settings:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleTestConnection = () => {
+ const currentBaseUrl = baseUrl || 'http://localhost:11434';
+ const currentApiKey = watch('apiKey');
+ testConnection(currentBaseUrl, currentApiKey);
+ };
+
+ useEffect(() => {
+ // Auto-test connection when component mounts if baseUrl is provided
+ if (configs?.baseUrl) {
+ testConnection(configs.baseUrl, configs.apiKey);
+ }
+ }, []);
+
+ return (
+
+
+
+
+
Ollama Settings
+
Configure your local or remote Ollama instance
+
+
+
+
+
+ {/* Help Section */}
+
+
Setup Instructions
+
+ - Install Ollama from ollama.ai
+ - Start Ollama:
ollama serve
+ - Pull a model:
ollama pull llama3.1:8b
+ - Enter your Ollama URL above (default: http://localhost:11434)
+
+
+
+ );
+};
+
+export default OllamaSettings;
\ No newline at end of file
diff --git a/nextjs/src/components/Settings/Configuration/APIModelChoose.tsx b/nextjs/src/components/Settings/Configuration/APIModelChoose.tsx
index d43d81aa..e95a7cb3 100644
--- a/nextjs/src/components/Settings/Configuration/APIModelChoose.tsx
+++ b/nextjs/src/components/Settings/Configuration/APIModelChoose.tsx
@@ -39,6 +39,10 @@ const APIModelChoose = () => {
value: AI_MODEL_CODE.PERPLEXITY,
label: MODAL_NAME_CONVERSION.PERPLEXITY,
},
+ {
+ value: AI_MODEL_CODE.OLLAMA,
+ label: MODAL_NAME_CONVERSION.OLLAMA,
+ },
{
value: AI_MODEL_CODE.OPEN_ROUTER,
label: MODAL_NAME_CONVERSION.OPEN_ROUTER,
@@ -137,7 +141,7 @@ export const ModelDeleteButton = ({ modelCode }: APIModelChooseProps) => {
onClick={handleTriggerTrash}
/>
-
+
Delete Model
diff --git a/nextjs/src/components/Settings/ModelSetting.tsx b/nextjs/src/components/Settings/ModelSetting.tsx
index e9b5fc56..2bb2be47 100644
--- a/nextjs/src/components/Settings/ModelSetting.tsx
+++ b/nextjs/src/components/Settings/ModelSetting.tsx
@@ -21,6 +21,7 @@ import OpenAiAPIKeysModelProvider from '@/components/AiModel/OpenAiAPIKeysModelP
import AnyscaleModelProvider from '@/components/AiModel/AnyscaleModelProvider';
import HuggingFaceModelProvider from '@/components/AiModel/HuggingFaceModelProvider';
import GooglePalmAPIkeyModelProvider from '@/components/AiModel/GooglePalmAPIkeyModelProvider';
+import OllamaModelProvider from '@/components/AiModel/OllamaModelProvider';
import SearchIcon from '@/icons/Search';
import useAiModal from '@/hooks/aiModal/useAiModal';
import { useSelector } from 'react-redux';
@@ -277,6 +278,9 @@ export const AddNewModel = ({ isAddAiModel }) => {
{selected.code === AI_MODEL_CODE.GEMINI && (
)}
+ {selected.code === AI_MODEL_CODE.OLLAMA && (
+
+ )}
{selected && (
(
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+ {...props}
+ />
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
diff --git a/nextjs/src/hooks/aiModal/useOllama.ts b/nextjs/src/hooks/aiModal/useOllama.ts
new file mode 100644
index 00000000..b8e50148
--- /dev/null
+++ b/nextjs/src/hooks/aiModal/useOllama.ts
@@ -0,0 +1,62 @@
+import commonApi from '@/api';
+import { assignModelListAction } from '@/lib/slices/aimodel/assignmodelslice';
+import { ollamaKeys } from '@/schema/usermodal'
+import { MODULE_ACTIONS } from '@/utils/constant';
+import Toast from '@/utils/toast';
+import { yupResolver } from '@hookform/resolvers/yup'
+import { useState } from 'react';
+import { useForm } from 'react-hook-form'
+import { useSelector, useDispatch } from 'react-redux';
+
+const defaultValues: any = {
+ baseUrl: 'http://localhost:11434',
+ key: undefined,
+};
+
+const useOllama = () => {
+ const { register, handleSubmit, formState: { errors }, setValue } = useForm({
+ mode: 'onSubmit',
+ resolver: yupResolver(ollamaKeys),
+ defaultValues
+ })
+ const [loading, setLoading] = useState(false);
+ const botinfo = useSelector((store: any) => store.aiModal.selectedValue);
+ const assignmodalList = useSelector((store: any) => store.assignmodel.list);
+ const dispatch = useDispatch();
+
+ const ollamaHealthCheck = async (payload) => {
+ try {
+ setLoading(true);
+ const data = {
+ baseUrl: payload?.baseUrl || 'http://localhost:11434',
+ apiKey: payload?.key,
+ bot: {
+ title: botinfo.value,
+ code: botinfo.code,
+ id: botinfo.id,
+ },
+ }
+ const response = await commonApi({
+ action: MODULE_ACTIONS.OLLAMA_HEALTH,
+ data
+ })
+
+ Toast(response.message);
+ if (response.data === true) return;
+ else dispatch(assignModelListAction([...assignmodalList, ...response.data]))
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ return {
+ register,
+ handleSubmit,
+ errors,
+ setValue,
+ ollamaHealthCheck,
+ loading,
+ }
+}
+
+export default useOllama;
diff --git a/nextjs/src/schema/usermodal.ts b/nextjs/src/schema/usermodal.ts
index 4adc094c..d6b69bf8 100644
--- a/nextjs/src/schema/usermodal.ts
+++ b/nextjs/src/schema/usermodal.ts
@@ -41,4 +41,9 @@ export const geminiKeys = yup.object({
key: yup.string().required('please enter your key')
})
+export const ollamaKeys = yup.object({
+ baseUrl: yup.string().url('Please enter a valid URL').required('Base URL is required'),
+ key: yup.string().optional()
+})
+
export type ModelKeysSchemaType = yup.InferType;
diff --git a/nextjs/src/utils/constant.ts b/nextjs/src/utils/constant.ts
index 072aac07..0a7d4fc1 100644
--- a/nextjs/src/utils/constant.ts
+++ b/nextjs/src/utils/constant.ts
@@ -134,6 +134,9 @@ export const MODULE_ACTIONS = {
HUGGING_FACE_HEALTH: 'huggingFaceKeyCheck',
ANTHROPIC_HEALTH: 'anthropicKeyCheck',
CHECK_GEMINI_API_KEY: 'geminiKeyCheck',
+ OLLAMA_HEALTH: 'ollamaKeyCheck',
+ TEST_OLLAMA_CONNECTION: 'testOllamaConnection',
+ SAVE_OLLAMA_SETTINGS: 'saveOllamaSettings',
BRAIN_LIST_ALL: 'brainListAll',
GET_MESSAGE_CREDITS: 'getMessageCredits',
FAVORITE_LIST: 'userFavoriteList',
@@ -255,6 +258,7 @@ export const AI_MODEL_CODE = {
LLAMA4: 'LLAMA4',
GROK: 'GROK',
QWEN: 'QWEN',
+ OLLAMA: 'OLLAMA',
OPEN_ROUTER: 'OPEN_ROUTER',
// error conversation response
CONVERSATION_ERROR: `We encountered an issue and were unable to receive a response. This could be due to a variety of reasons including network issues, server problems, or unexpected errors.Please try your request again later. If the problem persists, check your network connection or [contact support](mailto:hello@weam.ai) for further assistance.`,
@@ -370,7 +374,8 @@ export const MODEL_IMAGE_BY_CODE={
PERPLEXITY: '/perplexity.png',
DEEPSEEK: '/Deepseek.png',
GROK: '/grok.png',
- QWEN: '/qwen.png'
+ QWEN: '/qwen.png',
+ OLLAMA: '/Ai-icon.svg'
}
export const ALLOWED_TYPES = [
@@ -549,6 +554,7 @@ export const MODAL_NAME_CONVERSION = {
LLAMA4: 'Llama4',
GROK: 'Grok',
QWEN: 'Qwen',
+ OLLAMA: 'Ollama',
OPEN_ROUTER: 'Open Router'
}
@@ -744,6 +750,7 @@ export const MODEL_CREDIT_INFO = [
displayName: 'Claude 3.5 Sonnet Latest',
snippet: 'Ideal for complex coding and long-form content.',
doc: true,
+ websearch: false,
vision: true,
image: false,
reasoning: true,
@@ -919,6 +926,379 @@ export const MODEL_CREDIT_INFO = [
code: 'PRO_AGENT',
model: ProAgentCode.VIDEO_CALL_ANALYZER,
credit: 5,
+ },
+
+ {
+ code: 'OLLAMA',
+ model: 'llama3.2',
+ credit: 0.1,
+ displayName: 'Llama 3.2',
+ snippet: 'Fast and efficient local model for general tasks.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'llama3.1',
+ credit: 0.1,
+ displayName: 'Llama 3.1',
+ snippet: 'Powerful local model for complex reasoning and coding.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'llama3',
+ credit: 0.1,
+ displayName: 'Llama 3',
+ snippet: 'Powerful general-purpose language model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'llama2',
+ credit: 0.08,
+ displayName: 'Llama 2',
+ snippet: 'Reliable and well-tested language model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'mistral',
+ credit: 0.1,
+ displayName: 'Mistral',
+ snippet: 'Efficient and versatile language model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'mixtral',
+ credit: 0.12,
+ displayName: 'Mixtral',
+ snippet: 'Mixture of experts model with enhanced capabilities.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'codellama',
+ credit: 0.1,
+ displayName: 'Code Llama',
+ snippet: 'Specialized for code generation and programming tasks.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'deepseek-coder',
+ credit: 0.1,
+ displayName: 'DeepSeek Coder',
+ snippet: 'Advanced coding assistant with deep understanding.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'phi3',
+ credit: 0.05,
+ displayName: 'Phi-3',
+ snippet: 'Compact and efficient model for various tasks.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'phi3.5',
+ credit: 0.06,
+ displayName: 'Phi-3.5',
+ snippet: 'Enhanced version of Phi with improved performance.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'gemma2',
+ credit: 0.1,
+ displayName: 'Gemma 2',
+ snippet: 'Google\'s advanced language model for diverse applications.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'gemma',
+ credit: 0.08,
+ displayName: 'Gemma',
+ snippet: 'Google\'s efficient language model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'qwen2.5',
+ credit: 0.1,
+ displayName: 'Qwen 2.5',
+ snippet: 'Latest Qwen model with enhanced capabilities.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'qwen2',
+ credit: 0.09,
+ displayName: 'Qwen 2',
+ snippet: 'Alibaba\'s powerful multilingual model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'codeqwen',
+ credit: 0.1,
+ displayName: 'CodeQwen',
+ snippet: 'Qwen specialized for code generation.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'starcoder2',
+ credit: 0.1,
+ displayName: 'StarCoder 2',
+ snippet: 'Advanced code generation model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'orca-mini',
+ credit: 0.06,
+ displayName: 'Orca Mini',
+ snippet: 'Compact model with strong reasoning abilities.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'vicuna',
+ credit: 0.08,
+ displayName: 'Vicuna',
+ snippet: 'Fine-tuned model for conversational AI.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'neural-chat',
+ credit: 0.07,
+ displayName: 'Neural Chat',
+ snippet: 'Optimized for natural conversations.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'starling-lm',
+ credit: 0.09,
+ displayName: 'Starling LM',
+ snippet: 'High-performance language model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'tinyllama',
+ credit: 0.03,
+ displayName: 'TinyLlama',
+ snippet: 'Ultra-lightweight model for basic tasks.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'wizard-vicuna-uncensored',
+ credit: 0.08,
+ displayName: 'Wizard Vicuna',
+ snippet: 'Uncensored conversational model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'nous-hermes2',
+ credit: 0.09,
+ displayName: 'Nous Hermes 2',
+ snippet: 'Advanced reasoning and instruction following.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'dolphin-mistral',
+ credit: 0.1,
+ displayName: 'Dolphin Mistral',
+ snippet: 'Fine-tuned Mistral for enhanced performance.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'llava',
+ credit: 0.12,
+ displayName: 'LLaVA',
+ snippet: 'Multimodal model with vision capabilities.',
+ doc: true,
+ websearch: false,
+ vision: true,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'bakllava',
+ credit: 0.11,
+ displayName: 'BakLLaVA',
+ snippet: 'Vision-language model for image understanding.',
+ doc: true,
+ websearch: false,
+ vision: true,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'solar',
+ credit: 0.1,
+ displayName: 'Solar',
+ snippet: 'High-performance general purpose model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'openchat',
+ credit: 0.08,
+ displayName: 'OpenChat',
+ snippet: 'Open-source conversational AI model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'zephyr',
+ credit: 0.08,
+ displayName: 'Zephyr',
+ snippet: 'Fine-tuned for helpful and harmless responses.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'yi',
+ credit: 0.09,
+ displayName: 'Yi',
+ snippet: '01.AI\'s multilingual language model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: true,
+ },
+ {
+ code: 'OLLAMA',
+ model: 'falcon',
+ credit: 0.09,
+ displayName: 'Falcon',
+ snippet: 'High-performance open-source model.',
+ doc: true,
+ websearch: false,
+ vision: false,
+ image: false,
+ reasoning: false,
}
]
@@ -962,7 +1342,10 @@ export const SUB_MODEL_TYPE = [
'GEMINI',
'PERPLEXITY',
'DEEPSEEK',
- 'LLAMA4'
+ 'LLAMA4',
+ 'GROK',
+ 'QWEN',
+ 'OLLAMA'
] as const;
// 500 Credits
diff --git a/nodejs/OLLAMA_README.md b/nodejs/OLLAMA_README.md
new file mode 100644
index 00000000..74224a63
--- /dev/null
+++ b/nodejs/OLLAMA_README.md
@@ -0,0 +1,105 @@
+# Ollama Integration for Weam
+
+## Quick Setup
+
+1. **Install Ollama**
+ - Download from: https://ollama.ai/download
+ - Install for your operating system
+
+2. **Start Ollama Service**
+ ```bash
+ ollama serve
+ ```
+
+3. **Install Models**
+ ```bash
+ ollama pull llama3.1:8b
+ ollama pull mistral:7b-instruct
+ ```
+
+4. **Configure Environment**
+ Create a `.env` file in the nodejs directory:
+ ```
+ OLLAMA_URL=http://localhost:11434
+ OLLAMA_FALLBACK_ENABLED=true
+ ```
+
+5. **Test Integration**
+ ```bash
+ npm run validate-ollama
+ npm run test-ollama
+ npm run test-ollama-full
+ ```
+
+6. **Start Weam Server**
+ ```bash
+ npm run dev
+ ```
+
+## API Endpoints
+
+### Chat with Ollama Models
+```bash
+POST /api/ollama/chat
+{
+ "messages": [{"role": "user", "content": "Hello"}],
+ "model": "llama3.1:8b"
+}
+```
+
+### Generate Text
+```bash
+POST /api/ollama/generate
+{
+ "prompt": "Write a story about",
+ "model": "llama3.1:8b"
+}
+```
+
+### List Available Models
+```bash
+GET /api/ollama/tags
+```
+
+### Health Check (No Auth Required)
+```bash
+GET /api/ollama/health
+```
+
+## Using in Weam
+
+1. **Chat Interface**: Select Ollama models from the model dropdown
+2. **Document Q&A**: Use local models for private document analysis
+3. **Prompts & Agents**: Create bots that use specific Ollama models
+4. **Admin Panel**: Configure allowed models and team permissions
+
+## Troubleshooting
+
+**Connection Failed**
+- Ensure Ollama is running: `ollama serve`
+- Check if models are installed: `ollama list`
+- Verify port 11434 is available
+
+**No Models Available**
+- Install at least one model: `ollama pull llama3.1:8b`
+- Check company settings for allowed models
+
+**Permission Denied**
+- Verify user has proper permissions
+- Check company Ollama settings
+- Ensure user is authenticated
+
+## Model Recommendations
+
+- **llama3.1:8b**: Best overall performance
+- **mistral:7b-instruct**: Good for following instructions
+- **phi3:mini**: Lightweight option for limited resources
+- **codellama:7b**: Specialized for code generation
+
+## Support
+
+Run the validation and test scripts to diagnose issues:
+```bash
+npm run validate-ollama
+npm run test-ollama-full
+```
diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json
index eaf565e5..49c7171c 100644
--- a/nodejs/package-lock.json
+++ b/nodejs/package-lock.json
@@ -12,6 +12,7 @@
"@socket.io/redis-adapter": "^8.3.0",
"agenda": "^5.0.0",
"aws-sdk": "^2.1557.0",
+ "axios": "^1.11.0",
"bcrypt": "^5.1.1",
"bull": "^4.12.2",
"bull-board": "^2.1.3",
@@ -32,6 +33,7 @@
"jsonwebtoken": "^9.0.2",
"kafkajs": "^2.2.4",
"mime-types": "^2.1.35",
+ "moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"mongoose": "^8.1.1",
"mongoose-paginate-v2": "^1.8.0",
@@ -39,14 +41,14 @@
"multer-s3": "^2.10.0",
"newrelic": "latest",
"nodemailer": "^6.9.9",
+ "ollama": "^0.5.17",
"otplib": "^12.0.1",
"qrcode": "^1.5.3",
- "razorpay": "^2.9.5",
"redis": "^4.7.0",
"sharp": "^0.33.5",
"socket.io": "^4.7.4",
- "stripe": "^14.17.0",
"winston": "^3.11.0",
+ "winston-daily-rotate-file": "^5.0.0",
"winston-loki": "^6.1.0"
},
"devDependencies": {
@@ -3884,12 +3886,13 @@
}
},
"node_modules/axios": {
- "version": "1.7.7",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
- "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
- "form-data": "^4.0.0",
+ "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
@@ -4687,6 +4690,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -5324,6 +5340,20 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
@@ -5485,12 +5515,10 @@
}
},
"node_modules/es-define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
- "dependencies": {
- "get-intrinsic": "^1.2.4"
- },
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
}
@@ -5503,6 +5531,33 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@@ -5820,6 +5875,15 @@
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
+ "node_modules/file-stream-rotator": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz",
+ "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==",
+ "license": "MIT",
+ "dependencies": {
+ "moment": "^2.29.1"
+ }
+ },
"node_modules/file-type": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz",
@@ -5988,12 +6052,15 @@
}
},
"node_modules/form-data": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
- "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -6234,15 +6301,21 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
"dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -6271,6 +6344,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -6366,11 +6452,12 @@
}
},
"node_modules/gopd": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
- "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
- "dependencies": {
- "get-intrinsic": "^1.1.3"
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -6413,21 +6500,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-proto": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
- "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/has-symbols": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
- "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -8116,6 +8193,15 @@
"tmpl": "1.0.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -8373,6 +8459,38 @@
"node": ">=16.20.1"
}
},
+ "node_modules/mongoose/node_modules/gaxios": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
+ "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^5.0.0",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/mongoose/node_modules/gcp-metadata": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
+ "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "gaxios": "^5.0.0",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/mongoose/node_modules/mongodb": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz",
@@ -8852,7 +8970,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
- "optional": true,
"engines": {
"node": ">= 6"
}
@@ -8876,6 +8993,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/ollama": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.17.tgz",
+ "integrity": "sha512-q5LmPtk6GLFouS+3aURIVl+qcAOPC4+Msmx7uBb3pd+fxI55WnGjmLZ0yijI/CYy79x0QPGx3BwC3u5zv9fBvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-fetch": "^3.6.20"
+ }
+ },
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -9428,15 +9554,6 @@
"node": ">= 0.8"
}
},
- "node_modules/razorpay": {
- "version": "2.9.5",
- "resolved": "https://registry.npmjs.org/razorpay/-/razorpay-2.9.5.tgz",
- "integrity": "sha512-bmybwyszgfbYWAdO4igyHFk5zFj/D4YuoZAFNbyIYnPwzd+FBY5WvtpfUA9lVBMgnV4NzVEhncxR3It9RI/gCQ==",
- "license": "MIT",
- "dependencies": {
- "axios": "^1.6.8"
- }
- },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -10220,18 +10337,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/stripe": {
- "version": "14.25.0",
- "resolved": "https://registry.npmjs.org/stripe/-/stripe-14.25.0.tgz",
- "integrity": "sha512-wQS3GNMofCXwH8TSje8E1SE8zr6ODiGtHQgPtO95p9Mb4FhKC9jvXR2NUTpZ9ZINlckJcFidCmaTFV4P6vsb9g==",
- "dependencies": {
- "@types/node": ">=8.1.0",
- "qs": "^6.11.0"
- },
- "engines": {
- "node": ">=12.*"
- }
- },
"node_modules/strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
@@ -10805,6 +10910,12 @@
"node": ">=0.8.0"
}
},
+ "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": "11.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
@@ -10884,6 +10995,24 @@
"node": ">= 12.0.0"
}
},
+ "node_modules/winston-daily-rotate-file": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz",
+ "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==",
+ "license": "MIT",
+ "dependencies": {
+ "file-stream-rotator": "^0.6.1",
+ "object-hash": "^3.0.0",
+ "triple-beam": "^1.4.1",
+ "winston-transport": "^4.7.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "winston": "^3"
+ }
+ },
"node_modules/winston-loki": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/winston-loki/-/winston-loki-6.1.2.tgz",
diff --git a/nodejs/package.json b/nodejs/package.json
index e031ec3a..5ae62b40 100644
--- a/nodejs/package.json
+++ b/nodejs/package.json
@@ -7,7 +7,11 @@
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"dev": "nodemon index.js",
- "dev:two": "NODE_ENV=two nodemon index.js"
+ "dev:two": "NODE_ENV=two nodemon index.js",
+ "setup-ollama": "bash setup-ollama.sh",
+ "setup-ollama-win": "setup-ollama.bat",
+ "test-ollama": "node test-local-ollama.js",
+ "test-ollama-integration": "node test-ollama-integration.js"
},
"keywords": [],
"author": "",
@@ -16,6 +20,7 @@
"@socket.io/redis-adapter": "^8.3.0",
"agenda": "^5.0.0",
"aws-sdk": "^2.1557.0",
+ "axios": "^1.11.0",
"bcrypt": "^5.1.1",
"bull": "^4.12.2",
"bull-board": "^2.1.3",
@@ -44,6 +49,7 @@
"multer-s3": "^2.10.0",
"newrelic": "latest",
"nodemailer": "^6.9.9",
+ "ollama": "^0.5.17",
"otplib": "^12.0.1",
"qrcode": "^1.5.3",
"redis": "^4.7.0",
diff --git a/nodejs/setup-ollama.bat b/nodejs/setup-ollama.bat
new file mode 100644
index 00000000..9a57e8bd
--- /dev/null
+++ b/nodejs/setup-ollama.bat
@@ -0,0 +1,97 @@
+@echo off
+echo Setting up Ollama integration for Weam...
+
+where ollama >nul 2>nul
+if %errorlevel% neq 0 (
+ echo Ollama is not installed. Please install Ollama first:
+ echo Visit: https://ollama.ai/download
+ pause
+ exit /b 1
+)
+
+echo Ollama is installed
+
+curl -s http://localhost:11434/api/tags >nul 2>nul
+if %errorlevel% neq 0 (
+ echo Ollama service is not running. Starting Ollama...
+ start /b ollama serve
+ timeout /t 5 /nobreak >nul
+
+ curl -s http://localhost:11434/api/tags >nul 2>nul
+ if %errorlevel% neq 0 (
+ echo Failed to start Ollama service
+ pause
+ exit /b 1
+ )
+)
+
+echo Ollama service is running
+
+echo Pulling recommended models...
+
+echo Pulling model: llama3.1:8b
+ollama pull llama3.1:8b
+if %errorlevel% equ 0 (
+ echo Successfully pulled llama3.1:8b
+) else (
+ echo Failed to pull llama3.1:8b
+)
+
+echo Pulling model: llama3:8b
+ollama pull llama3:8b
+if %errorlevel% equ 0 (
+ echo Successfully pulled llama3:8b
+) else (
+ echo Failed to pull llama3:8b
+)
+
+echo Pulling model: mistral:7b-instruct
+ollama pull mistral:7b-instruct
+if %errorlevel% equ 0 (
+ echo Successfully pulled mistral:7b-instruct
+) else (
+ echo Failed to pull mistral:7b-instruct
+)
+
+set /p install_specialized="Do you want to install specialized models? (y/n): "
+if /i "%install_specialized%"=="y" (
+ echo Pulling model: codellama:7b
+ ollama pull codellama:7b
+
+ echo Pulling model: phi3:mini
+ ollama pull phi3:mini
+)
+
+set /p install_embeddings="Do you want to install embedding models for RAG? (y/n): "
+if /i "%install_embeddings%"=="y" (
+ echo Pulling model: nomic-embed-text
+ ollama pull nomic-embed-text
+
+ echo Pulling model: mxbai-embed-large
+ ollama pull mxbai-embed-large
+)
+
+echo Testing Ollama integration...
+curl -s -X GET "http://localhost:11434/api/tags" >nul
+if %errorlevel% equ 0 (
+ echo Ollama API is working correctly
+) else (
+ echo Ollama API test failed
+)
+
+echo.
+echo Installed models:
+ollama list
+
+echo.
+echo Ollama setup complete!
+echo.
+echo Next steps:
+echo 1. Update your .env file with OLLAMA_URL=http://localhost:11434
+echo 2. Restart your Weam Node.js server
+echo 3. Configure company Ollama settings via the admin panel
+echo 4. Test the integration using the API endpoints
+echo.
+echo For detailed documentation, see: OLLAMA_INTEGRATION.md
+echo.
+pause
diff --git a/nodejs/setup-ollama.sh b/nodejs/setup-ollama.sh
new file mode 100644
index 00000000..d9a3c707
--- /dev/null
+++ b/nodejs/setup-ollama.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+echo "Setting up Ollama integration for Weam..."
+
+if ! command -v ollama &> /dev/null; then
+ echo "Ollama is not installed. Please install Ollama first:"
+ echo " Visit: https://ollama.ai/download"
+ exit 1
+fi
+
+echo "Ollama is installed"
+
+if ! curl -s http://localhost:11434/api/tags > /dev/null; then
+ echo "Ollama service is not running. Starting Ollama..."
+ ollama serve &
+ sleep 5
+
+ if ! curl -s http://localhost:11434/api/tags > /dev/null; then
+ echo "Failed to start Ollama service"
+ exit 1
+ fi
+fi
+
+echo "Ollama service is running"
+
+pull_model() {
+ local model=$1
+ echo "Pulling model: $model"
+ ollama pull "$model"
+ if [ $? -eq 0 ]; then
+ echo "Successfully pulled $model"
+ else
+ echo "Failed to pull $model"
+ fi
+}
+
+echo "Pulling recommended models..."
+
+pull_model "llama3.1:8b"
+pull_model "llama3:8b"
+pull_model "mistral:7b-instruct"
+
+read -p "Do you want to install specialized models? (y/n): " install_specialized
+if [[ $install_specialized =~ ^[Yy]$ ]]; then
+ pull_model "codellama:7b"
+ pull_model "phi3:mini"
+fi
+
+read -p "Do you want to install embedding models for RAG? (y/n): " install_embeddings
+if [[ $install_embeddings =~ ^[Yy]$ ]]; then
+ pull_model "nomic-embed-text"
+ pull_model "mxbai-embed-large"
+fi
+
+echo "Testing Ollama integration..."
+curl -s -X GET "http://localhost:11434/api/tags" | jq '.' > /dev/null
+if [ $? -eq 0 ]; then
+ echo "Ollama API is working correctly"
+else
+ echo "Ollama API test failed"
+fi
+
+echo "Installed models:"
+ollama list
+
+echo ""
+echo "Ollama setup complete!"
+echo ""
+echo "Next steps:"
+echo " 1. Update your .env file with OLLAMA_URL=http://localhost:11434"
+echo " 2. Restart your Weam Node.js server"
+echo " 3. Configure company Ollama settings via the admin panel"
+echo " 4. Test the integration using the API endpoints"
+echo ""
+echo "For detailed documentation, see: OLLAMA_INTEGRATION.md"
diff --git a/nodejs/src/config/constants/aimodal.js b/nodejs/src/config/constants/aimodal.js
index 60ddca7f..9f69562c 100644
--- a/nodejs/src/config/constants/aimodal.js
+++ b/nodejs/src/config/constants/aimodal.js
@@ -83,6 +83,7 @@ const AI_MODAL_PROVIDER = {
GROK: 'GROK',
QWEN: 'QWEN',
OPEN_ROUTER: 'OPEN_ROUTER',
+ OLLAMA: 'OLLAMA',
}
const OPENROUTER_PROVIDER = {
diff --git a/nodejs/src/controller/ollamaController.js b/nodejs/src/controller/ollamaController.js
new file mode 100644
index 00000000..b479e810
--- /dev/null
+++ b/nodejs/src/controller/ollamaController.js
@@ -0,0 +1,572 @@
+const ollamaService = require('../services/ollamaService');
+const ollamaAnalytics = require('../services/ollamaAnalytics');
+
+class OllamaController {
+ async chat(req, res) {
+ try {
+ const { messages, model, baseUrl, stream = false, options = {}, apiKey } = req.body;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ if (!messages || !model) {
+ return res.status(400).json({
+ code: 'VALIDATION_ERROR',
+ message: 'Messages and model are required'
+ });
+ }
+
+ const hasPermission = await ollamaService.checkUserPermission(userId, companyId, model);
+ if (!hasPermission) {
+ return res.status(403).json({
+ code: 'PERMISSION_DENIED',
+ message: 'You do not have permission to use this Ollama model'
+ });
+ }
+
+ try {
+ await ollamaService.testConnectivity(baseUrl, apiKey);
+ } catch (error) {
+ return res.status(503).json({
+ code: 'OLLAMA_UNAVAILABLE',
+ message: 'Ollama service is not available. Please check if Ollama is running.',
+ details: error.message
+ });
+ }
+
+ const result = await ollamaService.chat({
+ messages,
+ model,
+ baseUrl,
+ stream,
+ userId,
+ companyId,
+ options,
+ apiKey
+ });
+
+ if (stream && result.success) {
+ res.setHeader('Content-Type', 'text/plain');
+ res.setHeader('Transfer-Encoding', 'chunked');
+
+ for await (const part of result.stream) {
+ if (part.message) {
+ res.write(JSON.stringify(part) + '\n');
+ }
+ }
+ res.end();
+
+ await ollamaService.trackUsage(userId, companyId, model, 'chat_stream', 0);
+ } else {
+ await ollamaService.trackUsage(userId, companyId, model, 'chat', result.tokens || 0);
+ res.json(result);
+ }
+
+ } catch (error) {
+ logger.error('Ollama chat error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to process chat request'
+ });
+ }
+ }
+
+ async generate(req, res) {
+ try {
+ const { prompt, model, baseUrl, stream = false, options = {}, apiKey } = req.body;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ if (!prompt || !model) {
+ return res.status(400).json({
+ code: 'VALIDATION_ERROR',
+ message: 'Prompt and model are required'
+ });
+ }
+
+ const hasPermission = await ollamaService.checkUserPermission(userId, companyId, model);
+ if (!hasPermission) {
+ return res.status(403).json({
+ code: 'PERMISSION_DENIED',
+ message: 'You do not have permission to use this Ollama model'
+ });
+ }
+
+ const result = await ollamaService.generate({
+ prompt,
+ model,
+ baseUrl,
+ stream,
+ userId,
+ companyId,
+ options,
+ apiKey
+ });
+
+ if (stream && result.success) {
+ res.setHeader('Content-Type', 'text/plain');
+ res.setHeader('Transfer-Encoding', 'chunked');
+
+ for await (const part of result.stream) {
+ res.write(JSON.stringify(part) + '\n');
+ }
+ res.end();
+
+ await ollamaService.trackUsage(userId, companyId, model, 'generate_stream', 0);
+ } else {
+ await ollamaService.trackUsage(userId, companyId, model, 'generate', result.tokens || 0);
+ res.json(result);
+ }
+
+ } catch (error) {
+ logger.error('Ollama generate error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to generate text'
+ });
+ }
+ }
+
+ async listModels(req, res) {
+ try {
+ const { baseUrl, apiKey } = req.query;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ try {
+ await ollamaService.testConnectivity(baseUrl, apiKey);
+ } catch (error) {
+ return res.status(503).json({
+ code: 'OLLAMA_UNAVAILABLE',
+ message: 'Ollama service is not available. Please check if Ollama is running.',
+ details: error.message
+ });
+ }
+
+ const models = await ollamaService.listModels(baseUrl, companyId, apiKey);
+
+ res.json({
+ success: true,
+ models,
+ count: models.length,
+ ollamaUrl: baseUrl || ollamaService.defaultBaseUrl
+ });
+ } catch (error) {
+ logger.error('Ollama list models error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to list models'
+ });
+ }
+ }
+
+ async pullModel(req, res) {
+ try {
+ const { model, baseUrl } = req.body;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ const isAdmin = await ollamaService.checkAdminPermission(userId, companyId);
+ if (!isAdmin) {
+ return res.status(403).json({
+ code: 'ADMIN_REQUIRED',
+ message: 'Admin permission required to pull models'
+ });
+ }
+
+ const result = await ollamaService.pullModel(model, baseUrl);
+
+ res.json(result);
+ } catch (error) {
+ logger.error('Ollama pull model error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to pull model'
+ });
+ }
+ }
+
+ async validateModel(req, res) {
+ try {
+ const { model, baseUrl } = req.body;
+
+ if (!model) {
+ return res.status(400).json({
+ ok: false,
+ error: 'Model name is required'
+ });
+ }
+
+ const result = await ollamaService.validateModel(model, baseUrl);
+
+ res.json(result);
+ } catch (error) {
+ logger.error('Ollama validate model error:', error);
+ res.status(500).json({
+ ok: false,
+ error: error.message || 'Failed to validate model'
+ });
+ }
+ }
+
+ async getModelDetails(req, res) {
+ try {
+ const { modelName } = req.params;
+ const { baseUrl } = req.query;
+
+ const details = await ollamaService.getModelDetails(modelName, baseUrl);
+
+ res.json(details);
+ } catch (error) {
+ logger.error('Ollama get model details error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to get model details'
+ });
+ }
+ }
+
+ async deleteModel(req, res) {
+ try {
+ const { model, baseUrl } = req.body;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ const isAdmin = await ollamaService.checkAdminPermission(userId, companyId);
+ if (!isAdmin) {
+ return res.status(403).json({
+ code: 'ADMIN_REQUIRED',
+ message: 'Admin permission required to delete models'
+ });
+ }
+
+ const result = await ollamaService.deleteModel(model, baseUrl);
+
+ res.json(result);
+ } catch (error) {
+ logger.error('Ollama delete model error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to delete model'
+ });
+ }
+ }
+
+ async createEmbeddings(req, res) {
+ try {
+ const { input, model, baseUrl } = req.body;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ if (!input || !model) {
+ return res.status(400).json({
+ code: 'VALIDATION_ERROR',
+ message: 'Input and model are required'
+ });
+ }
+
+ const hasPermission = await ollamaService.checkUserPermission(userId, companyId, model);
+ if (!hasPermission) {
+ return res.status(403).json({
+ code: 'PERMISSION_DENIED',
+ message: 'You do not have permission to use this Ollama model'
+ });
+ }
+
+ const result = await ollamaService.createEmbeddings(input, model, baseUrl);
+
+ await ollamaService.trackUsage(userId, companyId, model, 'embeddings', 0);
+
+ res.json(result);
+ } catch (error) {
+ logger.error('Ollama embeddings error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to create embeddings'
+ });
+ }
+ }
+
+ async copyModel(req, res) {
+ try {
+ const { source, destination, baseUrl } = req.body;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ const isAdmin = await ollamaService.checkAdminPermission(userId, companyId);
+ if (!isAdmin) {
+ return res.status(403).json({
+ code: 'ADMIN_REQUIRED',
+ message: 'Admin permission required to copy models'
+ });
+ }
+
+ const result = await ollamaService.copyModel(source, destination, baseUrl);
+
+ res.json(result);
+ } catch (error) {
+ logger.error('Ollama copy model error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to copy model'
+ });
+ }
+ }
+
+ async getRecommendedModels(req, res) {
+ try {
+ const models = await ollamaService.getRecommendedModels();
+
+ res.json({
+ success: true,
+ models
+ });
+ } catch (error) {
+ logger.error('Get recommended models error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to get recommended models'
+ });
+ }
+ }
+
+ async testConnection(req, res) {
+ try {
+ const { baseUrl, apiKey } = req.query;
+ const testUrl = baseUrl || 'http://localhost:11434';
+
+ await ollamaService.testConnectivity(testUrl, apiKey);
+
+ const models = await ollamaService.listModels(testUrl, null, apiKey);
+
+ res.json({
+ success: true,
+ message: 'Connection successful',
+ url: testUrl,
+ modelCount: models.length,
+ availableModels: models.map(m => m.name)
+ });
+ } catch (error) {
+ logger.error('Ollama connection test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Connection failed',
+ error: error.message,
+ url: req.query.baseUrl || 'http://localhost:11434'
+ });
+ }
+ }
+
+ async updateCompanySettings(req, res) {
+ try {
+ const { settings } = req.body;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ const isAdmin = await ollamaService.checkAdminPermission(userId, companyId);
+ if (!isAdmin) {
+ return res.status(403).json({
+ code: 'ADMIN_REQUIRED',
+ message: 'Admin permission required to update Ollama settings'
+ });
+ }
+
+ const updatedSettings = await ollamaService.updateCompanyOllamaSettings(companyId, settings);
+
+ res.json({
+ success: true,
+ settings: updatedSettings
+ });
+ } catch (error) {
+ logger.error('Update company Ollama settings error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to update settings'
+ });
+ }
+ }
+
+ async getCompanySettings(req, res) {
+ try {
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ const settings = await ollamaService.getCompanyOllamaSettings(companyId);
+
+ res.json({
+ success: true,
+ settings
+ });
+ } catch (error) {
+ logger.error('Get company Ollama settings error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to get settings'
+ });
+ }
+ }
+
+ async getUsageStats(req, res) {
+ try {
+ const { timeRange = '7d' } = req.query;
+ const companyId = req.user.company_id;
+
+ const stats = await ollamaAnalytics.getUsageStats(companyId, timeRange);
+
+ res.json({
+ success: true,
+ stats
+ });
+ } catch (error) {
+ logger.error('Get Ollama usage stats error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to get usage stats'
+ });
+ }
+ }
+
+ async getModelPerformance(req, res) {
+ try {
+ const { modelName } = req.params;
+ const companyId = req.user.company_id;
+
+ const stats = await ollamaAnalytics.getModelPerformanceStats(companyId, modelName);
+
+ res.json({
+ success: true,
+ stats
+ });
+ } catch (error) {
+ logger.error('Get model performance stats error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to get model performance stats'
+ });
+ }
+ }
+
+ async getCompanyOverview(req, res) {
+ try {
+ const companyId = req.user.company_id;
+
+ const overview = await ollamaAnalytics.getCompanyOllamaOverview(companyId);
+
+ res.json({
+ success: true,
+ overview
+ });
+ } catch (error) {
+ logger.error('Get company Ollama overview error:', error);
+ res.status(500).json({
+ code: 'OLLAMA_ERROR',
+ message: error.message || 'Failed to get company overview'
+ });
+ }
+ }
+
+ async healthCheck(req, res) {
+ try {
+ const { baseUrl, apiKey } = req.query;
+ const testUrl = baseUrl || 'http://localhost:11434';
+
+ const startTime = Date.now();
+ await ollamaService.testConnectivity(testUrl, apiKey);
+ const responseTime = Date.now() - startTime;
+
+ const models = await ollamaService.listModels(testUrl, null, apiKey);
+
+ res.json({
+ success: true,
+ status: 'healthy',
+ url: testUrl,
+ responseTime: `${responseTime}ms`,
+ modelCount: models.length,
+ models: models.slice(0, 5).map(m => ({ name: m.name, size: m.size })),
+ timestamp: new Date().toISOString()
+ });
+ } catch (error) {
+ logger.error('Ollama health check error:', error);
+ res.status(503).json({
+ success: false,
+ status: 'unhealthy',
+ url: req.query.baseUrl || 'http://localhost:11434',
+ error: error.message,
+ timestamp: new Date().toISOString(),
+ suggestions: [
+ 'Ensure Ollama is installed and running',
+ 'Check if the service is running: ollama serve',
+ 'Verify the URL is correct',
+ 'Install at least one model: ollama pull llama3.1:8b'
+ ]
+ });
+ }
+ }
+
+ async testConnectionWithApiKey(req, res) {
+ try {
+ const { baseUrl, apiKey } = req.body;
+ const testUrl = baseUrl || 'http://localhost:11434';
+
+ await ollamaService.testConnectivity(testUrl, apiKey);
+ const models = await ollamaService.listModels(testUrl, null, apiKey);
+
+ res.json({
+ success: true,
+ message: 'Connection successful',
+ url: testUrl,
+ modelCount: models.length,
+ availableModels: models.map(m => m.name)
+ });
+ } catch (error) {
+ logger.error('Ollama connection test with API key error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Connection failed',
+ error: error.message,
+ url: req.body.baseUrl || 'http://localhost:11434'
+ });
+ }
+ }
+
+ async saveOllamaSettings(req, res) {
+ try {
+ const { baseUrl, apiKey, provider } = req.body;
+ const userId = req.user.id;
+ const companyId = req.user.company_id;
+
+ try {
+ await ollamaService.testConnectivity(baseUrl, apiKey);
+ } catch (error) {
+ return res.status(400).json({
+ success: false,
+ message: 'Failed to connect to Ollama instance',
+ error: error.message
+ });
+ }
+
+ const settings = {
+ defaultBaseUrl: baseUrl,
+ apiKey: apiKey,
+ enabled: true,
+ updatedAt: new Date()
+ };
+
+ const updatedSettings = await ollamaService.updateCompanyOllamaSettings(companyId, settings);
+
+ res.json({
+ success: true,
+ message: 'Ollama settings saved successfully',
+ settings: updatedSettings
+ });
+ } catch (error) {
+ logger.error('Save Ollama settings error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to save Ollama settings',
+ error: error.message
+ });
+ }
+ }
+}
+
+module.exports = new OllamaController();
\ No newline at end of file
diff --git a/nodejs/src/controller/web/companyController.js b/nodejs/src/controller/web/companyController.js
index a5f9c32c..4768c691 100644
--- a/nodejs/src/controller/web/companyController.js
+++ b/nodejs/src/controller/web/companyController.js
@@ -67,6 +67,15 @@ const addBlockedDomain = catchAsync(async (req, res) => {
return util.successResponse(result, res);
})
+const ollamaApiChecker = catchAsync(async (req, res) => {
+ const result = await companyService.ollamaApiChecker(req);
+ if (!result) {
+ res.message = 'Failed to connect to Ollama instance';
+ return util.failureResponse(null, res)
+ }
+ res.message = _localize('ai.api_config_success', req);
+ return util.successResponse(result, res);
+})
module.exports = {
registerCompany,
@@ -75,5 +84,6 @@ module.exports = {
huggingFaceApiChecker,
anthropicApiChecker,
geminiApiKeyChecker,
- addBlockedDomain
+ addBlockedDomain,
+ ollamaApiChecker
};
diff --git a/nodejs/src/jobs/configuration.js b/nodejs/src/jobs/configuration.js
index f51a9fd9..5f8150b9 100644
--- a/nodejs/src/jobs/configuration.js
+++ b/nodejs/src/jobs/configuration.js
@@ -14,7 +14,7 @@ const defaultQueue = new Queue(QUEUE_NAME.DEFAULT, { ...queueOptions, db: 1 });
const mailQueue = new Queue(QUEUE_NAME.MAIL, { ...queueOptions, db: 2 });
const notificationQueue = new Queue(QUEUE_NAME.NOTIFICATION, { ...queueOptions, db: 3 });
-logger.info('bull-job-queue loaded 🍺🍻');
+logger.info('bull-job-queue loaded');
const handleFailure = async (job, err) => {
if (job.attemptsMade >= job.opts.attempts) {
diff --git a/nodejs/src/middleware/ollamaStream.js b/nodejs/src/middleware/ollamaStream.js
new file mode 100644
index 00000000..059e7129
--- /dev/null
+++ b/nodejs/src/middleware/ollamaStream.js
@@ -0,0 +1,13 @@
+const streamingMiddleware = (req, res, next) => {
+ if (req.body.stream) {
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
+ res.setHeader('Transfer-Encoding', 'chunked');
+ res.setHeader('Cache-Control', 'no-cache');
+ res.setHeader('Connection', 'keep-alive');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Headers', 'Cache-Control');
+ }
+ next();
+};
+
+module.exports = { streamingMiddleware };
diff --git a/nodejs/src/middleware/ollamaValidation.js b/nodejs/src/middleware/ollamaValidation.js
new file mode 100644
index 00000000..96d31719
--- /dev/null
+++ b/nodejs/src/middleware/ollamaValidation.js
@@ -0,0 +1,130 @@
+const Joi = require('joi');
+
+const ollamaValidation = {
+ chatRequest: Joi.object({
+ messages: Joi.array().items(
+ Joi.object({
+ role: Joi.string().valid('user', 'assistant', 'system').required(),
+ content: Joi.string().required()
+ })
+ ).required(),
+ model: Joi.string().required(),
+ baseUrl: Joi.string().uri().optional(),
+ stream: Joi.boolean().default(false),
+ options: Joi.object({
+ temperature: Joi.number().min(0).max(2).optional(),
+ top_p: Joi.number().min(0).max(1).optional(),
+ top_k: Joi.number().min(1).max(100).optional(),
+ repeat_penalty: Joi.number().min(0.1).max(2.0).optional(),
+ seed: Joi.number().optional(),
+ num_ctx: Joi.number().min(1).optional(),
+ num_predict: Joi.number().min(-1).optional()
+ }).optional()
+ }),
+
+ generateRequest: Joi.object({
+ prompt: Joi.string().required(),
+ model: Joi.string().required(),
+ baseUrl: Joi.string().uri().optional(),
+ stream: Joi.boolean().default(false),
+ options: Joi.object({
+ temperature: Joi.number().min(0).max(2).optional(),
+ top_p: Joi.number().min(0).max(1).optional(),
+ top_k: Joi.number().min(1).max(100).optional(),
+ repeat_penalty: Joi.number().min(0.1).max(2.0).optional(),
+ seed: Joi.number().optional(),
+ num_ctx: Joi.number().min(1).optional(),
+ num_predict: Joi.number().min(-1).optional()
+ }).optional()
+ }),
+
+ pullRequest: Joi.object({
+ model: Joi.string().required(),
+ baseUrl: Joi.string().uri().optional()
+ }),
+
+ embeddingsRequest: Joi.object({
+ input: Joi.string().required(),
+ model: Joi.string().required(),
+ baseUrl: Joi.string().uri().optional()
+ }),
+
+ copyRequest: Joi.object({
+ source: Joi.string().required(),
+ destination: Joi.string().required(),
+ baseUrl: Joi.string().uri().optional()
+ }),
+
+ deleteRequest: Joi.object({
+ model: Joi.string().required(),
+ baseUrl: Joi.string().uri().optional()
+ }),
+
+ settingsUpdate: Joi.object({
+ settings: Joi.object({
+ enabled: Joi.boolean().optional(),
+ allowedModels: Joi.array().items(Joi.string()).optional(),
+ restrictedModels: Joi.array().items(Joi.string()).optional(),
+ defaultModel: Joi.string().optional(),
+ maxConcurrentRequests: Joi.number().min(1).max(20).optional(),
+ defaultBaseUrl: Joi.string().uri().optional(),
+ teamSettings: Joi.object({
+ allowModelPulling: Joi.boolean().optional(),
+ allowModelDeletion: Joi.boolean().optional()
+ }).optional()
+ }).required()
+ }),
+
+ modelName: Joi.string().pattern(/^[a-zA-Z0-9._:-]+$/).required(),
+
+ timeRange: Joi.string().valid('1d', '7d', '30d', '90d').default('7d')
+};
+
+const validateOllamaRequest = (schema) => {
+ return (req, res, next) => {
+ const { error } = schema.validate(req.body);
+ if (error) {
+ return res.status(400).json({
+ code: 'VALIDATION_ERROR',
+ message: error.details[0].message,
+ details: error.details
+ });
+ }
+ next();
+ };
+};
+
+const validateOllamaQuery = (schema) => {
+ return (req, res, next) => {
+ const { error } = schema.validate(req.query);
+ if (error) {
+ return res.status(400).json({
+ code: 'VALIDATION_ERROR',
+ message: error.details[0].message,
+ details: error.details
+ });
+ }
+ next();
+ };
+};
+
+const validateOllamaParams = (schema) => {
+ return (req, res, next) => {
+ const { error } = schema.validate(req.params);
+ if (error) {
+ return res.status(400).json({
+ code: 'VALIDATION_ERROR',
+ message: error.details[0].message,
+ details: error.details
+ });
+ }
+ next();
+ };
+};
+
+module.exports = {
+ ollamaValidation,
+ validateOllamaRequest,
+ validateOllamaQuery,
+ validateOllamaParams
+};
diff --git a/nodejs/src/models/company.js b/nodejs/src/models/company.js
index 2dee2eeb..935c736e 100644
--- a/nodejs/src/models/company.js
+++ b/nodejs/src/models/company.js
@@ -62,6 +62,42 @@ const schema = new Schema(
freshCRMContactId: {
type: String,
},
+ ollamaSettings: {
+ enabled: {
+ type: Boolean,
+ default: true
+ },
+ allowedModels: [{
+ type: String
+ }],
+ restrictedModels: [{
+ type: String
+ }],
+ defaultModel: {
+ type: String
+ },
+ maxConcurrentRequests: {
+ type: Number,
+ default: 5
+ },
+ defaultBaseUrl: {
+ type: String
+ },
+ teamSettings: {
+ allowModelPulling: {
+ type: Boolean,
+ default: false
+ },
+ allowModelDeletion: {
+ type: Boolean,
+ default: false
+ }
+ },
+ updatedAt: {
+ type: Date,
+ default: Date.now
+ }
+ },
},
{ timestamps: true },
);
diff --git a/nodejs/src/routes/index.js b/nodejs/src/routes/index.js
index 4e45eb13..8aabf6ab 100644
--- a/nodejs/src/routes/index.js
+++ b/nodejs/src/routes/index.js
@@ -2,6 +2,7 @@ const express = require('express');
const { csrfMiddleware } = require('../middleware/csrf');
const { checkUserBlocking } = require('../middleware/userBlocking');
+
const router = express.Router();
// Apply user blocking check to ALL routes globally
@@ -12,5 +13,6 @@ router.use('/web', csrfMiddleware, require('./web'));
router.use('/upload', csrfMiddleware, require('./upload'));
router.use('/common', require('./common'));
router.use('/device', csrfMiddleware, require('./mobile'));
+router.use('/ollama', require('./ollama'));
module.exports = router;
\ No newline at end of file
diff --git a/nodejs/src/routes/ollama.js b/nodejs/src/routes/ollama.js
new file mode 100644
index 00000000..30307b2e
--- /dev/null
+++ b/nodejs/src/routes/ollama.js
@@ -0,0 +1,75 @@
+const express = require('express');
+const router = express.Router();
+const { authentication } = require('../middleware/authentication');
+const { streamingMiddleware } = require('../middleware/ollamaStream');
+const {
+ ollamaValidation,
+ validateOllamaRequest,
+ validateOllamaQuery,
+ validateOllamaParams
+} = require('../middleware/ollamaValidation');
+const ollamaController = require('../controller/ollamaController');
+
+router.get('/health', ollamaController.healthCheck);
+
+router.use(authentication);
+
+router.post('/chat',
+ streamingMiddleware,
+ validateOllamaRequest(ollamaValidation.chatRequest),
+ ollamaController.chat
+);
+
+router.post('/generate',
+ streamingMiddleware,
+ validateOllamaRequest(ollamaValidation.generateRequest),
+ ollamaController.generate
+);
+
+router.get('/tags', ollamaController.listModels);
+router.post('/pull',
+ validateOllamaRequest(ollamaValidation.pullRequest),
+ ollamaController.pullModel
+);
+router.post('/validate', ollamaController.validateModel);
+router.get('/model/:modelName',
+ validateOllamaParams({ modelName: ollamaValidation.modelName }),
+ ollamaController.getModelDetails
+);
+
+router.delete('/model',
+ validateOllamaRequest(ollamaValidation.deleteRequest),
+ ollamaController.deleteModel
+);
+router.post('/embeddings',
+ validateOllamaRequest(ollamaValidation.embeddingsRequest),
+ ollamaController.createEmbeddings
+);
+router.post('/copy',
+ validateOllamaRequest(ollamaValidation.copyRequest),
+ ollamaController.copyModel
+);
+router.get('/recommended', ollamaController.getRecommendedModels);
+router.get('/test-connection', ollamaController.testConnection);
+
+router.put('/settings',
+ validateOllamaRequest(ollamaValidation.settingsUpdate),
+ ollamaController.updateCompanySettings
+);
+router.get('/settings', ollamaController.getCompanySettings);
+
+router.get('/analytics/usage',
+ validateOllamaQuery({ timeRange: ollamaValidation.timeRange }),
+ ollamaController.getUsageStats
+);
+router.get('/analytics/model/:modelName',
+ validateOllamaParams({ modelName: ollamaValidation.modelName }),
+ ollamaController.getModelPerformance
+);
+router.get('/analytics/overview', ollamaController.getCompanyOverview);
+
+// New endpoints for API key and settings management
+router.post('/test-connection-with-key', ollamaController.testConnectionWithApiKey);
+router.post('/save-settings', ollamaController.saveOllamaSettings);
+
+module.exports = router;
\ No newline at end of file
diff --git a/nodejs/src/routes/web/company.js b/nodejs/src/routes/web/company.js
index ee94a382..3e3a76e9 100644
--- a/nodejs/src/routes/web/company.js
+++ b/nodejs/src/routes/web/company.js
@@ -11,5 +11,6 @@ router.post('/resend-verification', validate(resendVerification), companyControl
router.post('/huggingface/apikey', validate(huggingFaceAuthKeys), authentication, companyController.huggingFaceApiChecker);
router.post('/anthropic/apikey', validate(anthropicAuthKeys), authentication, companyController.anthropicApiChecker);
router.post('/gemini/apikey', authentication, companyController.geminiApiKeyChecker);
+router.post('/ollama/apikey', authentication, companyController.ollamaApiChecker);
router.post('/blocked-domain', authentication, companyController.addBlockedDomain);
module.exports = router;
diff --git a/nodejs/src/seeders/bot.json b/nodejs/src/seeders/bot.json
index f4657bc9..5827cbab 100644
--- a/nodejs/src/seeders/bot.json
+++ b/nodejs/src/seeders/bot.json
@@ -63,5 +63,10 @@
"title": "Grok",
"code": "GROK",
"seq": 13
+ },
+ {
+ "title": "Ollama",
+ "code": "OLLAMA",
+ "seq": 14
}
]
\ No newline at end of file
diff --git a/nodejs/src/seeders/index.js b/nodejs/src/seeders/index.js
index be1de49d..9a913235 100644
--- a/nodejs/src/seeders/index.js
+++ b/nodejs/src/seeders/index.js
@@ -8,6 +8,7 @@ async function initSeed () {
await seedService.seedNotification();
await seedService.seedSetting();
await seedService.seedDefaultModel();
+ await seedService.seedDefaultOllamaModels();
await seedService.seedCustomGPT();
await seedService.seedPrompt();
await seedService.seedOtherRolePermission();
diff --git a/nodejs/src/seeders/userbot.json b/nodejs/src/seeders/userbot.json
new file mode 100644
index 00000000..fe847dd7
--- /dev/null
+++ b/nodejs/src/seeders/userbot.json
@@ -0,0 +1,467 @@
+[
+ {
+ "name": "llama3.2",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "llama3.1",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "llama3",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "llama2",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "mistral",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "mixtral",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "codellama",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "deepseek-coder",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "phi3",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "phi3.5",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "gemma2",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "gemma",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "qwen2.5",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "qwen2",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "codeqwen",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "starcoder2",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "orca-mini",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "vicuna",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "neural-chat",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "starling-lm",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "tinyllama",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "wizard-vicuna-uncensored",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "nous-hermes2",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "dolphin-mistral",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "llava",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "bakllava",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "solar",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "openchat",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "zephyr",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "yi",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ },
+ {
+ "name": "falcon",
+ "bot": {
+ "title": "Ollama",
+ "code": "OLLAMA"
+ },
+ "config": {
+ "apikey": "local"
+ },
+ "modelType": 2,
+ "isActive": true,
+ "stream": true,
+ "tool": false,
+ "provider": "ollama"
+ }
+]
diff --git a/nodejs/src/services/company.js b/nodejs/src/services/company.js
index e0cbd988..df14569c 100644
--- a/nodejs/src/services/company.js
+++ b/nodejs/src/services/company.js
@@ -370,7 +370,7 @@ async function createPinecornIndex(user, req) {
//createFreeTierApiKey(user);
} catch (error) {
- console.log("🚀 ~ createPinecornIndex ~ error:", error)
+ console.log("createPinecornIndex error:", error)
handleError(error, 'Error - createPinecornIndex');
}
}
@@ -1160,6 +1160,82 @@ async function openRouterApiChecker(req) {
}
}
+async function ollamaApiChecker(req) {
+ try {
+ const baseUrl = req.body.baseUrl || 'http://localhost:11434';
+ const apiKey = req.body.apiKey;
+
+ // Test connection to Ollama instance
+ const headers = { 'Content-Type': 'application/json' };
+ if (apiKey) {
+ headers['Authorization'] = `Bearer ${apiKey}`;
+ }
+
+ const response = await fetch(`${baseUrl}/api/tags`, {
+ method: 'GET',
+ headers
+ });
+
+ if (!response.ok) return false;
+
+ const data = await response.json();
+ const models = data.models || [];
+
+ const companyId = getCompanyId(req.user);
+ const companydetails = req.user.company;
+
+ const [existingBots, ollamaBot] = await Promise.all([
+ UserBot.find({ 'company.id': companyId, 'bot.code': AI_MODAL_PROVIDER.OLLAMA }),
+ Bot.findOne({ code: AI_MODAL_PROVIDER.OLLAMA }, { title: 1, code: 1 })
+ ]);
+
+ const updates = [];
+ const inserts = [];
+ const encryptedKey = apiKey ? encryptedData(apiKey) : null;
+ const encryptedBaseUrl = encryptedData(baseUrl);
+
+ // Create model configs for available Ollama models
+ models.forEach(model => {
+ const existingBot = existingBots.find(bot => bot.name === model.name);
+ const modelConfig = {
+ name: model.name,
+ bot: formatBot(ollamaBot),
+ company: companydetails,
+ config: {
+ baseUrl: encryptedBaseUrl,
+ apikey: encryptedKey
+ },
+ modelType: 'text',
+ extraConfig: {
+ temperature: 0.7,
+ top_p: 0.9,
+ top_k: 40,
+ repeat_penalty: 1.1
+ }
+ };
+
+ if (existingBot) {
+ updates.push({
+ updateOne: {
+ filter: { name: model.name, 'company.id': companyId, 'bot.code': AI_MODAL_PROVIDER.OLLAMA },
+ update: { $set: modelConfig, $unset: { deletedAt: 1 } }
+ }
+ });
+ } else {
+ inserts.push(modelConfig);
+ }
+ });
+
+ if (updates.length) await UserBot.bulkWrite(updates);
+ if (inserts.length) return UserBot.insertMany(inserts);
+
+ return existingBots[0]?.deletedAt ? existingBots : true;
+ } catch (error) {
+ handleError(error, 'Error - ollamaApiChecker');
+ return false;
+ }
+}
+
module.exports = {
addCompany,
updateCompany,
@@ -1178,6 +1254,7 @@ module.exports = {
createFreeTierApiKey,
geminiApiKeyChecker,
sendManualInviteEmail,
- addBlockedDomain
+ addBlockedDomain,
+ ollamaApiChecker
}
diff --git a/nodejs/src/services/ollamaAnalytics.js b/nodejs/src/services/ollamaAnalytics.js
new file mode 100644
index 00000000..a0db181b
--- /dev/null
+++ b/nodejs/src/services/ollamaAnalytics.js
@@ -0,0 +1,189 @@
+const Company = require('../models/company');
+const User = require('../models/user');
+
+class OllamaAnalyticsService {
+ constructor() {
+ this.usageCache = new Map();
+ }
+
+ async trackUsage(userId, companyId, model, action, data = {}) {
+ try {
+ const usageRecord = {
+ userId,
+ companyId,
+ provider: 'ollama',
+ model,
+ action,
+ tokens: data.tokens || 0,
+ responseTime: data.responseTime || 0,
+ timestamp: new Date(),
+ success: data.success !== false,
+ error: data.error || null
+ };
+
+ const cacheKey = `${companyId}-${new Date().toISOString().split('T')[0]}`;
+
+ if (!this.usageCache.has(cacheKey)) {
+ this.usageCache.set(cacheKey, []);
+ }
+
+ this.usageCache.get(cacheKey).push(usageRecord);
+
+ this.flushUsageCache();
+
+ logger.info(`Ollama usage tracked: ${model} - ${action} - ${data.tokens || 0} tokens`);
+
+ return usageRecord;
+ } catch (error) {
+ logger.error('Error tracking Ollama usage:', error);
+ throw error;
+ }
+ }
+
+ async flushUsageCache() {
+ if (this.usageCache.size > 100) {
+ this.usageCache.clear();
+ }
+ }
+
+ async getUsageStats(companyId, timeRange = '7d') {
+ try {
+ const startDate = this.getStartDate(timeRange);
+
+ const stats = {
+ totalRequests: 0,
+ totalTokens: 0,
+ modelUsage: {},
+ actionBreakdown: {},
+ userUsage: {},
+ successRate: 0,
+ averageResponseTime: 0,
+ period: timeRange
+ };
+
+ for (const [key, records] of this.usageCache.entries()) {
+ if (key.startsWith(companyId)) {
+ const filteredRecords = records.filter(record =>
+ new Date(record.timestamp) >= startDate
+ );
+
+ filteredRecords.forEach(record => {
+ stats.totalRequests++;
+ stats.totalTokens += record.tokens;
+
+ if (!stats.modelUsage[record.model]) {
+ stats.modelUsage[record.model] = 0;
+ }
+ stats.modelUsage[record.model]++;
+
+ if (!stats.actionBreakdown[record.action]) {
+ stats.actionBreakdown[record.action] = 0;
+ }
+ stats.actionBreakdown[record.action]++;
+
+ if (!stats.userUsage[record.userId]) {
+ stats.userUsage[record.userId] = 0;
+ }
+ stats.userUsage[record.userId]++;
+ });
+ }
+ }
+
+ if (stats.totalRequests > 0) {
+ stats.successRate = (stats.totalRequests / stats.totalRequests) * 100;
+ }
+
+ return stats;
+ } catch (error) {
+ logger.error('Error getting usage stats:', error);
+ throw error;
+ }
+ }
+
+ async getModelPerformanceStats(companyId, modelName) {
+ try {
+ const stats = {
+ model: modelName,
+ totalUsage: 0,
+ averageTokens: 0,
+ averageResponseTime: 0,
+ successRate: 0,
+ mostCommonActions: {},
+ recentUsage: []
+ };
+
+ for (const [key, records] of this.usageCache.entries()) {
+ if (key.startsWith(companyId)) {
+ const modelRecords = records.filter(record => record.model === modelName);
+
+ stats.totalUsage = modelRecords.length;
+
+ if (modelRecords.length > 0) {
+ stats.averageTokens = modelRecords.reduce((sum, r) => sum + r.tokens, 0) / modelRecords.length;
+ stats.averageResponseTime = modelRecords.reduce((sum, r) => sum + r.responseTime, 0) / modelRecords.length;
+
+ const successfulRequests = modelRecords.filter(r => r.success).length;
+ stats.successRate = (successfulRequests / modelRecords.length) * 100;
+
+ modelRecords.forEach(record => {
+ if (!stats.mostCommonActions[record.action]) {
+ stats.mostCommonActions[record.action] = 0;
+ }
+ stats.mostCommonActions[record.action]++;
+ });
+
+ stats.recentUsage = modelRecords
+ .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
+ .slice(0, 10);
+ }
+ }
+ }
+
+ return stats;
+ } catch (error) {
+ logger.error('Error getting model performance stats:', error);
+ throw error;
+ }
+ }
+
+ async getCompanyOllamaOverview(companyId) {
+ try {
+ const company = await Company.findById(companyId);
+ const users = await User.find({ company_id: companyId });
+
+ const overview = {
+ companyName: company?.companyNm || 'Unknown',
+ totalUsers: users.length,
+ ollamaEnabled: company?.ollamaSettings?.enabled || false,
+ allowedModels: company?.ollamaSettings?.allowedModels || [],
+ restrictedModels: company?.ollamaSettings?.restrictedModels || [],
+ defaultModel: company?.ollamaSettings?.defaultModel || null,
+ maxConcurrentRequests: company?.ollamaSettings?.maxConcurrentRequests || 5,
+ usageStats: await this.getUsageStats(companyId, '30d')
+ };
+
+ return overview;
+ } catch (error) {
+ logger.error('Error getting company Ollama overview:', error);
+ throw error;
+ }
+ }
+
+ getStartDate(timeRange) {
+ const now = new Date();
+ switch (timeRange) {
+ case '1d':
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ case '7d':
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ case '30d':
+ return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ case '90d':
+ return new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
+ default:
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ }
+ }
+}
+
+module.exports = new OllamaAnalyticsService();
diff --git a/nodejs/src/services/ollamaFallback.js b/nodejs/src/services/ollamaFallback.js
new file mode 100644
index 00000000..6ec26234
--- /dev/null
+++ b/nodejs/src/services/ollamaFallback.js
@@ -0,0 +1,208 @@
+const ollamaService = require('../services/ollamaService');
+
+class OllamaFallbackService {
+ constructor() {
+ this.fallbackEnabled = process.env.OLLAMA_FALLBACK_ENABLED === 'true';
+ this.fallbackProviders = ['openai', 'anthropic', 'azure'];
+ }
+
+ async chatWithFallback(chatParams, userId, companyId) {
+ const startTime = Date.now();
+
+ try {
+ const result = await ollamaService.chat(chatParams);
+
+ if (result.success) {
+ await this.trackSuccessfulRequest(chatParams.model, Date.now() - startTime);
+ return result;
+ }
+
+ throw new Error(result.error);
+
+ } catch (error) {
+ logger.warn(`Ollama request failed for model ${chatParams.model}:`, error.message);
+
+ if (this.fallbackEnabled) {
+ return await this.handleFallback(chatParams, userId, companyId, 'chat', error);
+ }
+
+ throw error;
+ }
+ }
+
+ async generateWithFallback(generateParams, userId, companyId) {
+ const startTime = Date.now();
+
+ try {
+ const result = await ollamaService.generate(generateParams);
+
+ if (result.success) {
+ await this.trackSuccessfulRequest(generateParams.model, Date.now() - startTime);
+ return result;
+ }
+
+ throw new Error(result.error);
+
+ } catch (error) {
+ logger.warn(`Ollama generate failed for model ${generateParams.model}:`, error.message);
+
+ if (this.fallbackEnabled) {
+ return await this.handleFallback(generateParams, userId, companyId, 'generate', error);
+ }
+
+ throw error;
+ }
+ }
+
+ async handleFallback(params, userId, companyId, action, originalError) {
+ try {
+ const fallbackModel = await this.selectFallbackModel(params.model, companyId);
+
+ if (!fallbackModel) {
+ throw new Error(`No fallback available for model ${params.model}`);
+ }
+
+ logger.info(`Falling back from Ollama ${params.model} to ${fallbackModel.provider}:${fallbackModel.model}`);
+
+ const fallbackResult = await this.executeFallback(params, fallbackModel, action);
+
+ fallbackResult.fellback = true;
+ fallbackResult.originalProvider = 'ollama';
+ fallbackResult.originalModel = params.model;
+ fallbackResult.originalError = originalError.message;
+
+ await this.trackFallbackUsage(userId, companyId, params.model, fallbackModel, action);
+
+ return fallbackResult;
+
+ } catch (fallbackError) {
+ logger.error('Fallback also failed:', fallbackError.message);
+ throw new Error(`Both Ollama and fallback failed: ${originalError.message}, Fallback: ${fallbackError.message}`);
+ }
+ }
+
+ async selectFallbackModel(ollamaModel, companyId) {
+ const company = await this.getCompanySettings(companyId);
+ const fallbackConfig = company?.ollamaSettings?.fallbackConfig;
+
+ if (!fallbackConfig?.enabled) {
+ return null;
+ }
+
+ const modelMapping = this.getModelMapping(ollamaModel);
+
+ for (const provider of this.fallbackProviders) {
+ if (fallbackConfig.allowedProviders?.includes(provider) && modelMapping[provider]) {
+ return {
+ provider,
+ model: modelMapping[provider],
+ apiKey: fallbackConfig[`${provider}ApiKey`]
+ };
+ }
+ }
+
+ return null;
+ }
+
+ getModelMapping(ollamaModel) {
+ const mappings = {
+ 'llama3.1:8b': {
+ openai: 'gpt-4o-mini',
+ anthropic: 'claude-3-haiku-20240307'
+ },
+ 'llama3:8b': {
+ openai: 'gpt-4o-mini',
+ anthropic: 'claude-3-haiku-20240307'
+ },
+ 'mistral:7b-instruct': {
+ openai: 'gpt-4o-mini',
+ anthropic: 'claude-3-haiku-20240307'
+ },
+ 'codellama:7b': {
+ openai: 'gpt-4o',
+ anthropic: 'claude-3-sonnet-20240229'
+ }
+ };
+
+ const baseModel = ollamaModel.split(':')[0];
+ return mappings[ollamaModel] || mappings[baseModel] || {
+ openai: 'gpt-4o-mini',
+ anthropic: 'claude-3-haiku-20240307'
+ };
+ }
+
+ async executeFallback(params, fallbackModel, action) {
+ switch (fallbackModel.provider) {
+ case 'openai':
+ return await this.executeOpenAIFallback(params, fallbackModel, action);
+ case 'anthropic':
+ return await this.executeAnthropicFallback(params, fallbackModel, action);
+ default:
+ throw new Error(`Unsupported fallback provider: ${fallbackModel.provider}`);
+ }
+ }
+
+ async executeOpenAIFallback(params, fallbackModel, action) {
+ throw new Error('OpenAI fallback not implemented - requires OpenAI service integration');
+ }
+
+ async executeAnthropicFallback(params, fallbackModel, action) {
+ throw new Error('Anthropic fallback not implemented - requires Anthropic service integration');
+ }
+
+ async trackSuccessfulRequest(model, responseTime) {
+ logger.info(`Ollama request successful for ${model} in ${responseTime}ms`);
+ }
+
+ async trackFallbackUsage(userId, companyId, originalModel, fallbackModel, action) {
+ logger.info(`Fallback used: ${originalModel} -> ${fallbackModel.provider}:${fallbackModel.model}`);
+
+ await ollamaService.trackUsage(userId, companyId, originalModel, `${action}_fallback`, 0, {
+ fallbackProvider: fallbackModel.provider,
+ fallbackModel: fallbackModel.model,
+ success: true
+ });
+ }
+
+ async getCompanySettings(companyId) {
+ try {
+ const { Company } = require('../models');
+ return await Company.findById(companyId);
+ } catch (error) {
+ logger.error('Error getting company settings for fallback:', error);
+ return null;
+ }
+ }
+
+ async configureFallback(companyId, config) {
+ try {
+ const { Company } = require('../models');
+ const company = await Company.findById(companyId);
+
+ if (!company) {
+ throw new Error('Company not found');
+ }
+
+ company.ollamaSettings = {
+ ...company.ollamaSettings,
+ fallbackConfig: {
+ enabled: config.enabled || false,
+ allowedProviders: config.allowedProviders || [],
+ openaiApiKey: config.openaiApiKey || null,
+ anthropicApiKey: config.anthropicApiKey || null,
+ azureConfig: config.azureConfig || null,
+ priority: config.priority || ['openai', 'anthropic', 'azure']
+ }
+ };
+
+ await company.save();
+ return company.ollamaSettings.fallbackConfig;
+
+ } catch (error) {
+ logger.error('Error configuring fallback:', error);
+ throw error;
+ }
+ }
+}
+
+module.exports = new OllamaFallbackService();
diff --git a/nodejs/src/services/ollamaService.js b/nodejs/src/services/ollamaService.js
new file mode 100644
index 00000000..1ee7de44
--- /dev/null
+++ b/nodejs/src/services/ollamaService.js
@@ -0,0 +1,495 @@
+const axios = require('axios');
+const { Ollama } = require('ollama');
+const Company = require('../models/company');
+const User = require('../models/user');
+const ollamaAnalytics = require('./ollamaAnalytics');
+
+class OllamaService {
+ constructor() {
+ this.defaultBaseUrl = process.env.OLLAMA_URL || 'http://localhost:11434';
+ this.timeout = 180000;
+ this.ollamaClient = null;
+ }
+
+ getOllamaClient(baseUrl, apiKey) {
+ const url = baseUrl || this.defaultBaseUrl;
+ const clientKey = `${url}-${apiKey || 'no-key'}`;
+
+ if (!this.ollamaClient || this.ollamaClient._key !== clientKey) {
+ const config = { host: url };
+
+ if (apiKey) {
+ config.headers = {
+ 'Authorization': `Bearer ${apiKey}`
+ };
+ }
+
+ this.ollamaClient = new Ollama(config);
+ this.ollamaClient._key = clientKey;
+ }
+ return this.ollamaClient;
+ }
+
+ async chat({ messages, model, baseUrl, stream, userId, companyId, options = {}, apiKey }) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl, apiKey);
+
+ const ollamaMessages = messages.map(msg => ({
+ role: msg.role,
+ content: msg.content
+ }));
+
+ const requestOptions = {
+ model,
+ messages: ollamaMessages,
+ stream,
+ options: {
+ temperature: options.temperature || 0.7,
+ top_p: options.top_p || 0.9,
+ top_k: options.top_k || 40,
+ repeat_penalty: options.repeat_penalty || 1.1,
+ ...options
+ }
+ };
+
+ if (stream) {
+ return await this.handleStreamingChat(ollamaClient, requestOptions, model);
+ } else {
+ const response = await ollamaClient.chat(requestOptions);
+
+ return {
+ success: true,
+ text: response.message.content,
+ model,
+ provider: 'ollama',
+ tokens: response.total_duration ? Math.ceil(response.total_duration / 1000000) : 0,
+ raw: response
+ };
+ }
+
+ } catch (error) {
+ logger.error(`Ollama chat error for model ${model}:`, error.message);
+
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
+ return {
+ success: false,
+ error: 'Cannot connect to Ollama instance',
+ model,
+ provider: 'ollama'
+ };
+ }
+
+ return {
+ success: false,
+ error: error.message || 'Unknown error',
+ model,
+ provider: 'ollama'
+ };
+ }
+ }
+
+ async handleStreamingChat(ollamaClient, options, model) {
+ try {
+ const stream = await ollamaClient.chat({...options, stream: true});
+ return {
+ success: true,
+ stream,
+ model,
+ provider: 'ollama'
+ };
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async generate({ prompt, model, baseUrl, stream, userId, companyId, options = {}, apiKey }) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl, apiKey);
+
+ const requestOptions = {
+ model,
+ prompt,
+ stream,
+ options: {
+ temperature: options.temperature || 0.7,
+ top_p: options.top_p || 0.9,
+ top_k: options.top_k || 40,
+ repeat_penalty: options.repeat_penalty || 1.1,
+ ...options
+ }
+ };
+
+ if (stream) {
+ const stream = await ollamaClient.generate({...requestOptions, stream: true});
+ return {
+ success: true,
+ stream,
+ model,
+ provider: 'ollama'
+ };
+ } else {
+ const response = await ollamaClient.generate(requestOptions);
+
+ return {
+ success: true,
+ text: response.response || '',
+ model,
+ provider: 'ollama',
+ tokens: response.total_duration ? Math.ceil(response.total_duration / 1000000) : 0,
+ raw: response
+ };
+ }
+
+ } catch (error) {
+ logger.error(`Ollama generate error for model ${model}:`, error.message);
+
+ return {
+ success: false,
+ error: error.message || 'Unknown error',
+ model,
+ provider: 'ollama'
+ };
+ }
+ }
+
+ async listModels(baseUrl, companyId, apiKey) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl, apiKey);
+ const response = await ollamaClient.list();
+
+ let modelList = response.models || [];
+
+ if (companyId) {
+ const allowedModels = await this.getCompanyAllowedModels(companyId);
+
+ if (allowedModels.length > 0) {
+ modelList = modelList.filter(model =>
+ allowedModels.includes(model.name)
+ );
+ }
+ }
+
+ return modelList.map(model => ({
+ name: model.name,
+ size: model.size,
+ digest: model.digest,
+ modified_at: model.modified_at,
+ details: {
+ format: model.details?.format || 'unknown',
+ family: model.details?.family || 'unknown',
+ families: model.details?.families || [],
+ parameter_size: model.details?.parameter_size || 'unknown',
+ quantization_level: model.details?.quantization_level || 'unknown',
+ architecture: model.details?.family || 'unknown'
+ },
+ provider: 'ollama'
+ }));
+
+ } catch (error) {
+ logger.error('Ollama list models error:', error.message);
+
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
+ throw new Error(`Cannot connect to Ollama at ${ollamaUrl}. Please ensure Ollama is running with 'ollama serve'.`);
+ }
+
+ throw new Error(`Failed to fetch models: ${error.message}`);
+ }
+ }
+
+ async pullModel(model, baseUrl, onProgress) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl);
+
+ if (onProgress && typeof onProgress === 'function') {
+ const stream = await ollamaClient.pull({ model, stream: true });
+
+ for await (const part of stream) {
+ onProgress(part);
+ }
+
+ return {
+ success: true,
+ message: `Model ${model} pulled successfully`,
+ model
+ };
+ } else {
+ await ollamaClient.pull({ model });
+
+ return {
+ success: true,
+ message: `Model ${model} pulled successfully`,
+ model
+ };
+ }
+
+ } catch (error) {
+ logger.error(`Ollama pull model error for ${model}:`, error.message);
+ throw new Error(`Failed to pull model: ${error.message}`);
+ }
+ }
+
+ async validateModel(model, baseUrl) {
+ try {
+ await this.testConnectivity(baseUrl);
+
+ const models = await this.listModels(baseUrl, null);
+ const found = models.find(m => m.name === model);
+
+ return {
+ ok: true,
+ exists: !!found,
+ availableModels: found ? undefined : models.map(m => m.name)
+ };
+
+ } catch (error) {
+ return {
+ ok: false,
+ error: error.message,
+ status: 502
+ };
+ }
+ }
+
+ async testConnectivity(baseUrl, apiKey) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl, apiKey);
+ await ollamaClient.list();
+ return true;
+ } catch (error) {
+ throw new Error(`Cannot connect to Ollama instance at ${ollamaUrl}`);
+ }
+ }
+
+ async getModelDetails(modelName, baseUrl) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl);
+ const response = await ollamaClient.show({ model: modelName });
+
+ return {
+ success: true,
+ details: {
+ ...response,
+ architecture: response.details?.family || 'unknown',
+ parameter_size: response.details?.parameter_size || 'unknown',
+ quantization: response.details?.quantization_level || 'unknown',
+ format: response.details?.format || 'unknown'
+ }
+ };
+
+ } catch (error) {
+ throw new Error(`Failed to get model details: ${error.message}`);
+ }
+ }
+
+ async checkUserPermission(userId, companyId, model) {
+ try {
+ const company = await Company.findById(companyId);
+ if (!company) return false;
+
+ if (company.ollamaSettings?.restrictedModels?.includes(model)) {
+ return false;
+ }
+
+ const user = await User.findById(userId);
+ if (!user) return false;
+
+ return user.permissions?.includes('use_ollama') || user.role === 'admin';
+
+ } catch (error) {
+ logger.error('Error checking user permission:', error);
+ return false;
+ }
+ }
+
+ async checkAdminPermission(userId, companyId) {
+ try {
+ const user = await User.findById(userId);
+ return user && (user.role === 'admin' || user.permissions?.includes('manage_ollama'));
+ } catch (error) {
+ logger.error('Error checking admin permission:', error);
+ return false;
+ }
+ }
+
+ async getCompanyAllowedModels(companyId) {
+ try {
+ const company = await Company.findById(companyId);
+ return company?.ollamaSettings?.allowedModels || [];
+ } catch (error) {
+ logger.error('Error getting company allowed models:', error);
+ return [];
+ }
+ }
+
+ async trackUsage(userId, companyId, model, action, tokens, additionalData = {}) {
+ try {
+ return await ollamaAnalytics.trackUsage(userId, companyId, model, action, {
+ tokens: tokens || 0,
+ ...additionalData
+ });
+ } catch (error) {
+ logger.error('Error tracking Ollama usage:', error);
+ }
+ }
+
+ async deleteModel(modelName, baseUrl) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl);
+ await ollamaClient.delete({ model: modelName });
+
+ return {
+ success: true,
+ message: `Model ${modelName} deleted successfully`
+ };
+
+ } catch (error) {
+ logger.error(`Ollama delete model error for ${modelName}:`, error.message);
+ throw new Error(`Failed to delete model: ${error.message}`);
+ }
+ }
+
+ async createEmbeddings(input, model, baseUrl) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl);
+ const response = await ollamaClient.embeddings({
+ model,
+ prompt: input
+ });
+
+ return {
+ success: true,
+ embeddings: response.embedding,
+ model,
+ provider: 'ollama'
+ };
+
+ } catch (error) {
+ logger.error(`Ollama embeddings error for model ${model}:`, error.message);
+ throw new Error(`Failed to create embeddings: ${error.message}`);
+ }
+ }
+
+ async copyModel(source, destination, baseUrl) {
+ const ollamaUrl = baseUrl || this.defaultBaseUrl;
+
+ try {
+ const ollamaClient = this.getOllamaClient(ollamaUrl);
+ await ollamaClient.copy({ source, destination });
+
+ return {
+ success: true,
+ message: `Model copied from ${source} to ${destination}`
+ };
+
+ } catch (error) {
+ logger.error(`Ollama copy model error:`, error.message);
+ throw new Error(`Failed to copy model: ${error.message}`);
+ }
+ }
+
+ async checkModelExists(modelName, baseUrl) {
+ try {
+ const models = await this.listModels(baseUrl, null);
+ return models.some(model => model.name === modelName);
+ } catch (error) {
+ logger.error(`Error checking if model exists:`, error.message);
+ return false;
+ }
+ }
+
+ async getRecommendedModels() {
+ return [
+ {
+ name: 'llama3.1:8b',
+ description: 'Latest Llama 3.1 model with 8B parameters - good balance of performance and resource usage',
+ size: '4.7GB',
+ recommended: true,
+ category: 'general'
+ },
+ {
+ name: 'llama3:8b',
+ description: 'Llama 3 model with 8B parameters - stable and reliable',
+ size: '4.7GB',
+ recommended: true,
+ category: 'general'
+ },
+ {
+ name: 'mistral:7b-instruct',
+ description: 'Mistral 7B Instruct - optimized for instruction following',
+ size: '4.1GB',
+ recommended: true,
+ category: 'instruction'
+ },
+ {
+ name: 'codellama:7b',
+ description: 'Code Llama 7B - specialized for code generation',
+ size: '3.8GB',
+ recommended: false,
+ category: 'code'
+ },
+ {
+ name: 'phi3:mini',
+ description: 'Microsoft Phi-3 Mini - lightweight but capable',
+ size: '2.3GB',
+ recommended: false,
+ category: 'lightweight'
+ }
+ ];
+ }
+
+ async updateCompanyOllamaSettings(companyId, settings) {
+ try {
+ const company = await Company.findById(companyId);
+ if (!company) {
+ throw new Error('Company not found');
+ }
+
+ company.ollamaSettings = {
+ ...company.ollamaSettings,
+ ...settings,
+ updatedAt: new Date()
+ };
+
+ await company.save();
+ return company.ollamaSettings;
+
+ } catch (error) {
+ logger.error('Error updating company Ollama settings:', error);
+ throw error;
+ }
+ }
+
+ async getCompanyOllamaSettings(companyId) {
+ try {
+ const company = await Company.findById(companyId);
+ return company?.ollamaSettings || {
+ allowedModels: [],
+ restrictedModels: [],
+ defaultModel: null,
+ maxConcurrentRequests: 5,
+ enabled: true
+ };
+ } catch (error) {
+ logger.error('Error getting company Ollama settings:', error);
+ return null;
+ }
+ }
+}
+
+module.exports = new OllamaService();
\ No newline at end of file
diff --git a/nodejs/src/services/seeder.js b/nodejs/src/services/seeder.js
index 98a840f5..30498b28 100644
--- a/nodejs/src/services/seeder.js
+++ b/nodejs/src/services/seeder.js
@@ -259,7 +259,6 @@ const seedSetting = async function () {
await Setting.bulkWrite(bulkSetting);
}
logger.info('Setting seeded successfully 🔥🔥🔥🔥🔥');
- } catch (error) {
logger.error('Error in seedSetting', error);
}
}
@@ -288,22 +287,82 @@ const seedDefaultModel = async () => {
}
}
-const seedCustomGPT = async () => {
+const seedDefaultOllamaModels = async () => {
try {
- const gptJSON = require('../seeders/customGPT.json');
- const getDefaults = await CustomGPT.find({ defaultgpt: true });
- const bulkGPT = [];
+ const UserBot = require('../models/userBot');
+ const Company = require('../models/company');
+ const ollamaModelsJSON = require('../seeders/userbot.json');
+
+ // Get all companies
+ const companies = await Company.find({}).lean();
+ if (!companies.length) {
+ logger.info('No companies found, skipping Ollama model seeding');
+ return;
+ }
- for (const iterator of gptJSON) {
- const check = getDefaults.find((element) => element.title === iterator.title);
- if (check) bulkGPT.push({ updateOne: { filter: { title: iterator.title }, update: { $set: iterator } } })
- else bulkGPT.push({ insertOne: { document: iterator } });
+ // Get Ollama bot
+ const ollamaBot = await Bot.findOne({ code: 'OLLAMA' });
+ if (!ollamaBot) {
+ logger.error('Ollama bot not found, please run bot seeder first');
+ return;
+ }
+
+ const bulkOperations = [];
+
+ // Create Ollama models for each company
+ for (const company of companies) {
+ for (const modelTemplate of ollamaModelsJSON) {
+ const modelConfig = {
+ ...modelTemplate,
+ bot: {
+ title: ollamaBot.title,
+ code: ollamaBot.code,
+ id: ollamaBot._id
+ },
+ company: {
+ name: company.companyNm,
+ slug: company.slug,
+ id: company._id
+ }
+ };
+
+ // Check if model already exists for this company
+ const existingModel = await UserBot.findOne({
+ name: modelTemplate.name,
+ 'company.id': company._id,
+ 'bot.code': 'OLLAMA'
+ });
+
+ if (!existingModel) {
+ bulkOperations.push({ insertOne: { document: modelConfig } });
+ } else {
+ // Update existing model to ensure it's not deleted
+ bulkOperations.push({
+ updateOne: {
+ filter: {
+ name: modelTemplate.name,
+ 'company.id': company._id,
+ 'bot.code': 'OLLAMA'
+ },
+ update: {
+ $set: modelConfig,
+ $unset: { deletedAt: 1 }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ if (bulkOperations.length) {
+ await UserBot.bulkWrite(bulkOperations);
+ logger.info(`Ollama models seeded successfully for ${companies.length} companies! 🦙🦙🦙`);
+ } else {
+ logger.info('All Ollama models already exist');
}
- if (bulkGPT.length) await CustomGPT.bulkWrite(bulkGPT);
- logger.info('Default custom gpt seeded successfully 🤯🤯🤯')
} catch (error) {
- logger.error('Error - seedCustomGPT', error);
+ logger.error('Error - seedDefaultOllamaModels', error);
}
}
@@ -798,6 +857,7 @@ module.exports = {
seedNotification,
seedSetting,
seedDefaultModel,
+ seedDefaultOllamaModels,
seedCustomGPT,
seedOtherRolePermission,
seedPrompt,
diff --git a/nodejs/src/utils/logger.js b/nodejs/src/utils/logger.js
index 4df01131..196c1596 100644
--- a/nodejs/src/utils/logger.js
+++ b/nodejs/src/utils/logger.js
@@ -3,7 +3,7 @@ require('winston-daily-rotate-file');
const config = require('../config/config');
const { format, transports } = winston;
-console.log("🚀 ~ config.SERVER.LOCAL_LOG:", config.SERVER.LOCAL_LOG)
+console.log("config.SERVER.LOCAL_LOG:", config.SERVER.LOCAL_LOG)
// Custom format to handle errors and stack traces
const errorStackFormat = format((info) => {
diff --git a/nodejs/test-comprehensive-ollama.js b/nodejs/test-comprehensive-ollama.js
new file mode 100644
index 00000000..09971262
--- /dev/null
+++ b/nodejs/test-comprehensive-ollama.js
@@ -0,0 +1,123 @@
+const axios = require('axios');
+
+async function comprehensiveOllamaTest() {
+ console.log('Comprehensive Ollama Integration Test\n');
+ console.log('=====================================\n');
+
+ const results = {
+ connectivity: false,
+ modelsList: false,
+ chatFunction: false,
+ generateFunction: false,
+ weamIntegration: false
+ };
+
+ try {
+ console.log('1. Testing Ollama connectivity...');
+ const connectResponse = await axios.get('http://localhost:11434/api/tags');
+ results.connectivity = true;
+ console.log(' Ollama is running and accessible');
+
+ const models = connectResponse.data.models || [];
+ if (models.length > 0) {
+ results.modelsList = true;
+ console.log(` Found ${models.length} installed models:`);
+ models.forEach(model => {
+ console.log(` - ${model.name} (${(model.size / 1024 / 1024 / 1024).toFixed(1)}GB)`);
+ });
+ } else {
+ console.log(' No models installed');
+ console.log(' Install a model: ollama pull llama3.1:8b');
+ return results;
+ }
+
+ const testModel = models[0].name;
+ console.log(`\n2. Testing chat with model: ${testModel}`);
+
+ try {
+ const chatResponse = await axios.post('http://localhost:11434/api/chat', {
+ model: testModel,
+ messages: [{ role: 'user', content: 'Hello! Respond with just "Chat working"' }],
+ stream: false
+ });
+
+ if (chatResponse.data.message?.content) {
+ results.chatFunction = true;
+ console.log(` Chat response: ${chatResponse.data.message.content.substring(0, 50)}...`);
+ }
+ } catch (error) {
+ console.log(` ✗ Chat failed: ${error.message}`);
+ }
+
+ console.log(`\n3. Testing generate with model: ${testModel}`);
+
+ try {
+ const generateResponse = await axios.post('http://localhost:11434/api/generate', {
+ model: testModel,
+ prompt: 'Say "Generate working"',
+ stream: false
+ });
+
+ if (generateResponse.data.response) {
+ results.generateFunction = true;
+ console.log(` Generate response: ${generateResponse.data.response.substring(0, 50)}...`);
+ }
+ } catch (error) {
+ console.log(` ✗ Generate failed: ${error.message}`);
+ }
+
+ console.log('\n4. Testing Weam integration endpoints...');
+
+ try {
+ const healthResponse = await axios.get('http://localhost:3000/api/ollama/health');
+ if (healthResponse.data.success) {
+ results.weamIntegration = true;
+ console.log(' Weam Ollama health endpoint working');
+ console.log(` Models available in Weam: ${healthResponse.data.modelCount}`);
+ }
+ } catch (error) {
+ console.log(` ✗ Weam integration test failed: ${error.message}`);
+ console.log(' Make sure your Weam server is running on port 3000');
+ }
+
+ } catch (error) {
+ console.log(` ✗ Connection failed: ${error.message}`);
+ console.log('\nTroubleshooting:');
+ console.log('1. Install Ollama: https://ollama.ai/download');
+ console.log('2. Start Ollama: ollama serve');
+ console.log('3. Install a model: ollama pull llama3.1:8b');
+ }
+
+ console.log('\n=====================================');
+ console.log('Test Results Summary:');
+ console.log('=====================================');
+
+ Object.entries(results).forEach(([test, passed]) => {
+ const status = passed ? 'PASS' : 'FAIL';
+ const testName = test.replace(/([A-Z])/g, ' $1').toLowerCase();
+ console.log(`${status} ${testName}`);
+ });
+
+ const passedTests = Object.values(results).filter(Boolean).length;
+ const totalTests = Object.keys(results).length;
+
+ console.log(`\nOverall: ${passedTests}/${totalTests} tests passed`);
+
+ if (passedTests === totalTests) {
+ console.log('\nSUCCESS: Ollama is fully integrated and working with Weam!');
+ console.log('\nYou can now:');
+ console.log('- Use Ollama models in Weam chat interface');
+ console.log('- Configure company settings for model access');
+ console.log('- Monitor usage through analytics endpoints');
+ } else {
+ console.log('\nSome tests failed. Please review the error messages above.');
+ }
+
+ return results;
+}
+
+if (require.main === module) {
+ comprehensiveOllamaTest().catch(console.error);
+}
+
+module.exports = comprehensiveOllamaTest;
diff --git a/nodejs/test-local-ollama.js b/nodejs/test-local-ollama.js
new file mode 100644
index 00000000..8b2a7555
--- /dev/null
+++ b/nodejs/test-local-ollama.js
@@ -0,0 +1,69 @@
+const axios = require('axios');
+
+async function testLocalOllamaChat() {
+ const OLLAMA_URL = 'http://localhost:11434';
+
+ console.log('Testing local Ollama chat functionality...\n');
+
+ try {
+ console.log('1. Testing Ollama connection...');
+ const connectResponse = await axios.get(`${OLLAMA_URL}/api/tags`);
+ console.log(' Connection successful');
+
+ const models = connectResponse.data.models || [];
+ if (models.length === 0) {
+ console.log(' No models found. Please install at least one model:');
+ console.log(' ollama pull llama3.1:8b');
+ return;
+ }
+
+ const testModel = models[0].name;
+ console.log(` Found ${models.length} models. Testing with: ${testModel}`);
+
+ console.log('\n2. Testing chat functionality...');
+ const chatResponse = await axios.post(`${OLLAMA_URL}/api/chat`, {
+ model: testModel,
+ messages: [
+ {
+ role: 'user',
+ content: 'Say "Hello from Ollama!" and nothing else.'
+ }
+ ],
+ stream: false
+ });
+
+ const responseText = chatResponse.data.message?.content || 'No response';
+ console.log(` Model response: ${responseText}`);
+
+ console.log('\n3. Testing generate functionality...');
+ const generateResponse = await axios.post(`${OLLAMA_URL}/api/generate`, {
+ model: testModel,
+ prompt: 'Complete this sentence: "Ollama is working"',
+ stream: false
+ });
+
+ const generateText = generateResponse.data.response || 'No response';
+ console.log(` Generate response: ${generateText}`);
+
+ console.log('\nSUCCESS: Local Ollama is working correctly!');
+ console.log('\nTo use with Weam:');
+ console.log('1. Ensure your .env has: OLLAMA_URL=http://localhost:11434');
+ console.log('2. Start your Weam server');
+ console.log('3. Use the Ollama models in chat');
+
+ } catch (error) {
+ console.log('ERROR: Failed to connect to local Ollama');
+ console.log(`Details: ${error.message}`);
+ console.log('\nTroubleshooting:');
+ console.log('1. Make sure Ollama is installed: https://ollama.ai/download');
+ console.log('2. Start Ollama service: ollama serve');
+ console.log('3. Install a model: ollama pull llama3.1:8b');
+ console.log('4. Check if port 11434 is available');
+ }
+}
+
+if (require.main === module) {
+ testLocalOllamaChat();
+}
+
+module.exports = testLocalOllamaChat;
diff --git a/nodejs/test-ollama-integration.js b/nodejs/test-ollama-integration.js
new file mode 100644
index 00000000..cddb6f42
--- /dev/null
+++ b/nodejs/test-ollama-integration.js
@@ -0,0 +1,195 @@
+const axios = require('axios');
+
+const TEST_CONFIG = {
+ baseUrl: 'http://localhost:3000/api',
+ token: null,
+ ollamaUrl: 'http://localhost:11434'
+};
+
+class OllamaIntegrationTest {
+ constructor() {
+ this.results = [];
+ }
+
+ async runAllTests() {
+ console.log('Starting Ollama Integration Tests...\n');
+
+ await this.testOllamaConnection();
+ await this.testListModels();
+ await this.testRecommendedModels();
+ await this.testModelDetails();
+ await this.testChatEndpoint();
+ await this.testGenerateEndpoint();
+ await this.testEmbeddings();
+ await this.testSettings();
+ await this.testAnalytics();
+
+ this.printResults();
+ }
+
+ async testOllamaConnection() {
+ try {
+ const response = await axios.get(`${TEST_CONFIG.ollamaUrl}/api/tags`);
+ this.logTest('Ollama Connection', true, 'Direct connection successful');
+ } catch (error) {
+ this.logTest('Ollama Connection', false, `Connection failed: ${error.message}`);
+ }
+ }
+
+ async testListModels() {
+ try {
+ const response = await this.makeRequest('GET', '/ollama/tags');
+ const hasModels = response.data.models && response.data.models.length > 0;
+ this.logTest('List Models', hasModels, hasModels ? `Found ${response.data.models.length} models` : 'No models found');
+ } catch (error) {
+ this.logTest('List Models', false, error.message);
+ }
+ }
+
+ async testRecommendedModels() {
+ try {
+ const response = await this.makeRequest('GET', '/ollama/recommended');
+ const hasRecommended = response.data.models && response.data.models.length > 0;
+ this.logTest('Recommended Models', hasRecommended, hasRecommended ? `${response.data.models.length} recommended models` : 'No recommended models');
+ } catch (error) {
+ this.logTest('Recommended Models', false, error.message);
+ }
+ }
+
+ async testModelDetails() {
+ try {
+ const response = await this.makeRequest('GET', '/ollama/model/llama3.1:8b');
+ const hasDetails = response.data.details !== undefined;
+ this.logTest('Model Details', hasDetails, hasDetails ? 'Model details retrieved' : 'No model details');
+ } catch (error) {
+ this.logTest('Model Details', false, error.message);
+ }
+ }
+
+ async testChatEndpoint() {
+ try {
+ const response = await this.makeRequest('POST', '/ollama/chat', {
+ messages: [
+ { role: 'user', content: 'Hello, respond with just "test successful"' }
+ ],
+ model: 'llama3.1:8b',
+ stream: false
+ });
+ const isSuccessful = response.data.success && response.data.text;
+ this.logTest('Chat Endpoint', isSuccessful, isSuccessful ? 'Chat response received' : 'No chat response');
+ } catch (error) {
+ this.logTest('Chat Endpoint', false, error.message);
+ }
+ }
+
+ async testGenerateEndpoint() {
+ try {
+ const response = await this.makeRequest('POST', '/ollama/generate', {
+ prompt: 'Say "generate test successful"',
+ model: 'llama3.1:8b',
+ stream: false
+ });
+ const isSuccessful = response.data.success && response.data.text;
+ this.logTest('Generate Endpoint', isSuccessful, isSuccessful ? 'Generate response received' : 'No generate response');
+ } catch (error) {
+ this.logTest('Generate Endpoint', false, error.message);
+ }
+ }
+
+ async testEmbeddings() {
+ try {
+ const response = await this.makeRequest('POST', '/ollama/embeddings', {
+ input: 'test embedding text',
+ model: 'nomic-embed-text'
+ });
+ const hasEmbeddings = response.data.success && response.data.embeddings;
+ this.logTest('Embeddings', hasEmbeddings, hasEmbeddings ? 'Embeddings generated' : 'No embeddings generated');
+ } catch (error) {
+ this.logTest('Embeddings', false, error.message);
+ }
+ }
+
+ async testSettings() {
+ try {
+ const response = await this.makeRequest('GET', '/ollama/settings');
+ const hasSettings = response.data.success && response.data.settings;
+ this.logTest('Settings', hasSettings, hasSettings ? 'Settings retrieved' : 'No settings found');
+ } catch (error) {
+ this.logTest('Settings', false, error.message);
+ }
+ }
+
+ async testAnalytics() {
+ try {
+ const response = await this.makeRequest('GET', '/ollama/analytics/overview');
+ const hasAnalytics = response.data.success && response.data.overview;
+ this.logTest('Analytics', hasAnalytics, hasAnalytics ? 'Analytics data retrieved' : 'No analytics data');
+ } catch (error) {
+ this.logTest('Analytics', false, error.message);
+ }
+ }
+
+ async makeRequest(method, endpoint, data = null) {
+ const config = {
+ method,
+ url: `${TEST_CONFIG.baseUrl}${endpoint}`,
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ };
+
+ if (TEST_CONFIG.token) {
+ config.headers['Authorization'] = `Bearer ${TEST_CONFIG.token}`;
+ }
+
+ if (data) {
+ config.data = data;
+ }
+
+ return await axios(config);
+ }
+
+ logTest(testName, success, message) {
+ const status = success ? 'PASS' : 'FAIL';
+ console.log(`${status} ${testName}: ${message}`);
+ this.results.push({ testName, success, message });
+ }
+
+ printResults() {
+ console.log('\nTest Results Summary:');
+ console.log('========================');
+
+ const passed = this.results.filter(r => r.success).length;
+ const total = this.results.length;
+
+ console.log(`Passed: ${passed}/${total}`);
+ console.log(`Success Rate: ${Math.round((passed/total) * 100)}%`);
+
+ if (passed === total) {
+ console.log('\nAll tests passed! Ollama integration is working correctly.');
+ } else {
+ console.log('\nSome tests failed. Please check the Ollama setup and authentication.');
+ console.log('\nTroubleshooting:');
+ console.log('1. Ensure Ollama is running: ollama serve');
+ console.log('2. Check if models are installed: ollama list');
+ console.log('3. Verify authentication token is set');
+ console.log('4. Check server logs for detailed errors');
+ }
+ }
+}
+
+if (require.main === module) {
+ const tester = new OllamaIntegrationTest();
+
+ console.log('Ollama Integration Test Suite');
+ console.log('================================\n');
+
+ if (!TEST_CONFIG.token) {
+ console.log('Warning: No authentication token set. Some tests may fail.');
+ console.log(' Set TEST_CONFIG.token to a valid JWT token for full testing.\n');
+ }
+
+ tester.runAllTests().catch(console.error);
+}
+
+module.exports = OllamaIntegrationTest;
diff --git a/nodejs/validate-ollama-env.js b/nodejs/validate-ollama-env.js
new file mode 100644
index 00000000..50d0164e
--- /dev/null
+++ b/nodejs/validate-ollama-env.js
@@ -0,0 +1,77 @@
+const fs = require('fs');
+const path = require('path');
+
+function validateOllamaEnvironment() {
+ console.log('Validating Ollama environment for Weam...\n');
+
+ const envPath = path.join(__dirname, '.env');
+ let envContent = '';
+
+ if (fs.existsSync(envPath)) {
+ envContent = fs.readFileSync(envPath, 'utf-8');
+ console.log('ENV file found');
+ } else {
+ console.log('ENV file not found');
+ console.log(' Create a .env file in the nodejs directory');
+ }
+
+ const ollamaUrlRegex = /OLLAMA_URL\s*=\s*(.+)/;
+ const match = envContent.match(ollamaUrlRegex);
+
+ if (match) {
+ const ollamaUrl = match[1].trim();
+ console.log(`OLLAMA_URL configured: ${ollamaUrl}`);
+
+ if (!ollamaUrl.startsWith('http')) {
+ console.log('WARNING: OLLAMA_URL should start with http:// or https://');
+ }
+ } else {
+ console.log('OLLAMA_URL not configured in .env');
+ console.log(' Add: OLLAMA_URL=http://localhost:11434');
+ }
+
+ const fallbackRegex = /OLLAMA_FALLBACK_ENABLED\s*=\s*(.+)/;
+ const fallbackMatch = envContent.match(fallbackRegex);
+
+ if (fallbackMatch) {
+ console.log(`OLLAMA_FALLBACK_ENABLED configured: ${fallbackMatch[1].trim()}`);
+ } else {
+ console.log('OLLAMA_FALLBACK_ENABLED not configured (optional)');
+ console.log(' Add: OLLAMA_FALLBACK_ENABLED=true for cloud fallback');
+ }
+
+ console.log('\nChecking required dependencies...');
+
+ const packagePath = path.join(__dirname, 'package.json');
+ if (fs.existsSync(packagePath)) {
+ const packageContent = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
+ const deps = packageContent.dependencies || {};
+
+ if (deps.ollama) {
+ console.log(`ollama package installed: ${deps.ollama}`);
+ } else {
+ console.log('ollama package not installed');
+ console.log(' Run: npm install ollama');
+ }
+
+ if (deps.axios) {
+ console.log(`axios package installed: ${deps.axios}`);
+ } else {
+ console.log('axios package not installed');
+ console.log(' Run: npm install axios');
+ }
+ }
+
+ console.log('\nRecommended next steps:');
+ console.log('1. Ensure Ollama is installed on your system');
+ console.log('2. Start Ollama service: ollama serve');
+ console.log('3. Install at least one model: ollama pull llama3.1:8b');
+ console.log('4. Test connectivity: npm run test-ollama');
+ console.log('5. Start your Weam server: npm run dev');
+}
+
+if (require.main === module) {
+ validateOllamaEnvironment();
+}
+
+module.exports = validateOllamaEnvironment;
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..787c0ab8
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "weam",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}