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

+
+
+ +
+ {/* Base URL Input */} +
+ +
+ + +
+ + + {/* Connection Status */} + {connectionStatus === 'success' && ( +
+ ✓ Connected successfully! Found {availableModels.length} models. +
+ )} + {connectionStatus === 'error' && ( +
+ ✗ Connection failed. Please check the URL and ensure Ollama is running. +
+ )} +
+ + {/* API Key Input (Optional) */} +
+ + + +

+ Leave empty if your Ollama instance doesn't require authentication +

+
+ + {/* Available Models Display */} + {availableModels.length > 0 && ( +
+

Available Models

+
+ {availableModels.slice(0, 6).map((model, index) => ( +
+ {model} +
+ ))} + {availableModels.length > 6 && ( +
+ +{availableModels.length - 6} more +
+ )} +
+
+ )} + + {/* Save Button */} +
+ +
+
+ + {/* Help Section */} +
+

Setup Instructions

+
    +
  1. Install Ollama from ollama.ai
  2. +
  3. Start Ollama: ollama serve
  4. +
  5. Pull a model: ollama pull llama3.1:8b
  6. +
  7. Enter your Ollama URL above (default: http://localhost:11434)
  8. +
+
+
+ ); +}; + +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": {} +}