From cf2e4f9ea06082b15d65003b0a83d2a58fa6586d Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Wed, 22 Oct 2025 12:00:41 -0400 Subject: [PATCH 01/72] create react base --- client/.gitignore | 24 + client/README.md | 16 + client/eslint.config.js | 29 + client/index.html | 13 + client/package-lock.json | 3021 +++++++++++++++++++++++++++++++++++ client/package.json | 29 + client/public/vite.svg | 1 + client/src/App.jsx | 11 + client/src/assets/react.svg | 1 + client/src/index.css | 1 + client/src/main.jsx | 10 + client/vite.config.js | 8 + 12 files changed, 3164 insertions(+) create mode 100644 client/.gitignore create mode 100644 client/README.md create mode 100644 client/eslint.config.js create mode 100644 client/index.html create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/public/vite.svg create mode 100644 client/src/App.jsx create mode 100644 client/src/assets/react.svg create mode 100644 client/src/index.css create mode 100644 client/src/main.jsx create mode 100644 client/vite.config.js diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..963909f --- /dev/null +++ b/client/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 0000000..cee1e2c --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..f07b3d0 --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + client + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..e2c6b22 --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,3021 @@ +{ + "name": "client", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "client", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.1.15", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwindcss": "^4.1.15" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react-swc": "^4.1.0", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "vite": "^7.1.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.35", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz", + "integrity": "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.24" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz", + "integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.2", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.15" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz", + "integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.15", + "@tailwindcss/oxide-darwin-arm64": "4.1.15", + "@tailwindcss/oxide-darwin-x64": "4.1.15", + "@tailwindcss/oxide-freebsd-x64": "4.1.15", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.15", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.15", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.15", + "@tailwindcss/oxide-linux-x64-musl": "4.1.15", + "@tailwindcss/oxide-wasm32-wasi": "4.1.15", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.15", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.15" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz", + "integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz", + "integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz", + "integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz", + "integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz", + "integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz", + "integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz", + "integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz", + "integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz", + "integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz", + "integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz", + "integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz", + "integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.15.tgz", + "integrity": "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.15", + "@tailwindcss/oxide": "4.1.15", + "tailwindcss": "4.1.15" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz", + "integrity": "sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.35", + "@swc/core": "^1.13.5" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "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" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "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, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "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, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "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/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "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, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz", + "integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "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" + } + }, + "node_modules/vite": { + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..36cd7ec --- /dev/null +++ b/client/package.json @@ -0,0 +1,29 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.15", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwindcss": "^4.1.15" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react-swc": "^4.1.0", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "vite": "^7.1.7" + } +} diff --git a/client/public/vite.svg b/client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..7d400ef --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,11 @@ + +function App() { + + return ( + <> + + + ) +} + +export default App diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..20f080a --- /dev/null +++ b/client/src/index.css @@ -0,0 +1 @@ +@import 'tailwindcss' \ No newline at end of file diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..fb4d238 --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], +}) From 4d7ab96e9d6b559890162b985ff45e0c128e30d5 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Wed, 22 Oct 2025 12:18:21 -0400 Subject: [PATCH 02/72] add basic text --- client/components/Image.jsx | 0 client/components/Products.jsx | 0 client/components/Text.jsx | 114 +++++++++++++++++++++++++++++++++ client/src/App.jsx | 3 +- 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 client/components/Image.jsx create mode 100644 client/components/Products.jsx create mode 100644 client/components/Text.jsx diff --git a/client/components/Image.jsx b/client/components/Image.jsx new file mode 100644 index 0000000..e69de29 diff --git a/client/components/Products.jsx b/client/components/Products.jsx new file mode 100644 index 0000000..e69de29 diff --git a/client/components/Text.jsx b/client/components/Text.jsx new file mode 100644 index 0000000..9ae1635 --- /dev/null +++ b/client/components/Text.jsx @@ -0,0 +1,114 @@ +import { useState } from 'react'; + +export default function Text() { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const sendMessage = async (e) => { + e.preventDefault(); + if (!input.trim()) return; + + const userMessage = input.trim(); + setInput(''); + + // Add user message to chat + setMessages(prev => [...prev, { role: 'user', content: userMessage }]); + setIsLoading(true); + + try { + const response = await fetch('http://localhost:11434/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'granite3-moe', + messages: [{ role: 'user', content: userMessage }], + stream: false + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const botMessage = data.message?.content || 'No response'; + + // Add bot response to chat + setMessages(prev => [...prev, { role: 'assistant', content: botMessage }]); + } catch (error) { + console.error('Error sending message:', error); + setMessages(prev => [...prev, { + role: 'assistant', + content: 'Error connecting to chat server.' + }]); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Header */} +
+

NLIP Client

+
+ + {/* Chat Box */} +
+ {messages.length === 0 && ( +
+

Start a conversation!

+
+ )} + {messages.map((msg, index) => ( +
+
+

{msg.content}

+
+
+ ))} + {isLoading && ( +
+
+
+
+
+
+
+
+
+ )} +
+ + {/* Input Form */} +
+
+ setInput(e.target.value)} + placeholder="Type your message..." + className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" + disabled={isLoading} + /> + +
+
+
+
+ ); +} diff --git a/client/src/App.jsx b/client/src/App.jsx index 7d400ef..6b0def5 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,9 +1,10 @@ +import Text from "../components/Text" function App() { return ( <> - + ) } From 53d892490f0b6c2b501dc6b793792a2a6a3affd0 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Wed, 22 Oct 2025 16:19:27 -0400 Subject: [PATCH 03/72] create text component --- client/components/Text.jsx | 26 +-- client/utils/nlip.js | 419 +++++++++++++++++++++++++++++++++++++ client/vite.config.js | 9 + 3 files changed, 434 insertions(+), 20 deletions(-) create mode 100644 client/utils/nlip.js diff --git a/client/components/Text.jsx b/client/components/Text.jsx index 9ae1635..148925e 100644 --- a/client/components/Text.jsx +++ b/client/components/Text.jsx @@ -1,9 +1,11 @@ import { useState } from 'react'; +import { NLIPClient } from '../utils/nlip'; export default function Text() { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); + const nlipClient = new NLIPClient(); const sendMessage = async (e) => { e.preventDefault(); @@ -17,24 +19,8 @@ export default function Text() { setIsLoading(true); try { - const response = await fetch('http://localhost:11434/api/chat', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model: 'granite3-moe', - messages: [{ role: 'user', content: userMessage }], - stream: false - }), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - const botMessage = data.message?.content || 'No response'; + const data = await nlipClient.sendMessage(userMessage); + const botMessage = data || 'No response'; // Add bot response to chat setMessages(prev => [...prev, { role: 'assistant', content: botMessage }]); @@ -53,7 +39,7 @@ export default function Text() {
{/* Header */} -
+

NLIP Client

@@ -71,7 +57,7 @@ export default function Text() { ? 'bg-cyan-500 text-white rounded-tr-sm' : 'bg-white text-gray-800 shadow-md rounded-tl-sm border border-gray-200' }`}> -

{msg.content}

+

{msg.content}

))} diff --git a/client/utils/nlip.js b/client/utils/nlip.js new file mode 100644 index 0000000..a95ecd9 --- /dev/null +++ b/client/utils/nlip.js @@ -0,0 +1,419 @@ +function nlipCompareString(value1, value2, matchNone = false) { + if (value1 != null && value2 != null) { + return value1.toLowerCase() === value2.toLowerCase(); + } + if (matchNone) { + return true; + } else { + return value1 == null && value2 == null; + } +} + +const AllowedFormats = { + text: "text", + token: "token", + structured: "structured", + binary: "binary", + location: "location", + error: "error", + generic: "generic" +}; + +// Usage example: +// const format = CaseInsensitiveEnum.fromValue(AllowedFormats, "ToKeN"); // returns "token" + +class CaseInsensitiveEnum { + constructor(enumObj) { + this.enumObj = enumObj; + this.values = Object.values(enumObj); + } + + static fromValue(enumObj, value) { + if (typeof value !== 'string') return null; + const lowerValue = value.toLowerCase(); + for (const key in enumObj) { + if (enumObj[key].toLowerCase() === lowerValue) { + return enumObj[key]; + } + } + return null; + } +} + +const ReservedTokens = { + auth: 'authorization', + conv: 'conversation', + control: 'control', + + isReserved(field) { + if (typeof field !== 'string') return false; + const lower = field.toLowerCase(); + return lower.startsWith(this.auth) || lower.startsWith(this.conv); + }, + + isAuth(field) { + return typeof field === 'string' && field.toLowerCase().startsWith(this.auth); + }, + + isConv(field) { + return typeof field === 'string' && field.toLowerCase().startsWith(this.conv); + }, + + isControl(field) { + return nlipCompareString(this.control, field); + }, + + getSuffix(field, separator = '') { + if (this.isAuth(field)) { + return field.slice(this.auth.length + separator.length).trim(); + } else if (this.isConv(field)) { + return field.slice(this.conv.length + separator.length).trim(); + } else { + return field; + } + } +}; + +class NLIPSubMessage { + /** + * @param {string} format - One of the AllowedFormats + * @param {string} subformat + * @param {string|Object} content + * @param {string|null} label + */ + constructor(format, subformat, content, label = null) { + this.format = format; + this.subformat = subformat; + this.content = content; + this.label = label; + } + + updateContent(content) { + this.content = content; + } + + extractField(format, subformat = null, label = null) { + if (nlipCompareString(this.format, format)) { + if (nlipCompareString(this.subformat, subformat, true)) { + if (nlipCompareString(this.label, label, true)) { + return this.content; + } + } + } + return null; + } +} + + +class NLIPMessage { + constructor({ messagetype = null, format, subformat, content, label = null, submessages = [] }) { + this.messagetype = messagetype; + this.format = format; + this.subformat = subformat; + this.content = content; + this.label = label; + this.submessages = submessages; + } + + isControlMsg() { + return this.messagetype !== null && ReservedTokens.isControl(this.messagetype); + } + + addSubmessage(submsg) { + if (!Array.isArray(this.submessages)) { + this.submessages = []; + } + this.submessages.push(submsg); + } + + addConversationToken(conversationToken, forceChange = false, label = null) { + const existingToken = this.extractConversationToken(label); + const submsg = new NLIPSubMessage(AllowedFormats.token, ReservedTokens.conv, conversationToken, label); + + if (existingToken === null) { + this.addSubmessage(submsg); + } else if (forceChange) { + this.submessages.forEach((msg, i) => { + if (ReservedTokens.isConv(msg.subformat)) { + this.submessages[i].updateContent(conversationToken); + } + }); + } + } + + addAuthenticationToken(token, label = null) { + const existingToken = this.extractAuthenticationToken(label); + if (existingToken === null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.token, ReservedTokens.auth, token, label)); + } + } + + extractField(format, subformat = null, label = null) { + if ( + nlipCompareString(this.format, format) && + nlipCompareString(this.subformat, subformat, true) && + nlipCompareString(this.label, label, true) + ) { + return this.content; + } + return null; + } + + extractFieldList(format, subformat = null, label = null) { + const field = this.extractField(format, subformat, label); + let fieldList = field === null ? [] : [field]; + + if (Array.isArray(this.submessages)) { + this.submessages.forEach(submsg => { + const value = submsg.extractField(format, subformat, label); + if (value !== null) { + fieldList.push(value); + } + }); + } + + return fieldList; + } + + extractText(language = 'english', separator = ' ') { + const textList = this.extractFieldList(AllowedFormats.text, language); + return textList.length > 0 ? textList.join(separator) : null; + } + + findLabeledSubmessage(label) { + if (label === null) return null; + return this.submessages.find(submsg => nlipCompareString(submsg.label, label)) || null; + } + + extractToken(tokenType, label = null) { + const tokens = this.extractFieldList(AllowedFormats.token, tokenType, label); + return tokens.length > 0 ? tokens[0] : null; + } + + extractConversationToken(label = null) { + return this.extractToken(ReservedTokens.conv, label); + } + + extractAuthenticationToken(label = null) { + return this.extractToken(ReservedTokens.auth, label); + } + + toJSON() { + const obj = { + messagetype: this.messagetype, + format: this.format, + subformat: this.subformat, + content: this.content, + label: this.label, + submessages: this.submessages.length > 0 ? this.submessages : undefined + }; + return JSON.stringify(obj, (_, v) => (v === null ? undefined : v)); + } + + toDict() { + return JSON.parse(this.toJSON()); + } + + addText(content, language = 'english', label = null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.text, language, content, label)); + } + + addToken(token, tokenType, label = null) { + if (ReservedTokens.isAuth(tokenType)) { + return this.addAuthenticationToken(token, label); + } + if (ReservedTokens.isConv(tokenType)) { + return this.addConversationToken(token, true, label); + } + this.addSubmessage(new NLIPSubMessage(AllowedFormats.token, tokenType, token, label)); + } + + addJson(jsonDict, label = null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.structured, "JSON", jsonDict, label)); + } + + addStructuredText(content, contentType, label = null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.structured, contentType, content, label)); + } + + addBinary(content, binaryType, encoding, label = null) { + const subformat = `${binaryType}/${encoding}`; + this.addSubmessage(new NLIPSubMessage(AllowedFormats.binary, subformat, content, label)); + } + + addImage(content, encoding, label = null) { + this.addBinary(content, "image", encoding, label); + } + + addAudio(content, encoding, label = null) { + this.addBinary(content, "audio", encoding, label); + } + + addVideo(content, encoding, label = null) { + this.addBinary(content, "video", encoding, label); + } + + addLocationText(location, label = null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.location, "text", location, label)); + } + + addLocationGps(location, label = null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.location, "gps", location, label)); + } + + addErrorCode(errorCode, label = null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.error, "code", errorCode, label)); + } + + addErrorText(errorDescr, label = null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.error, "text", errorDescr, label)); + } + + addGeneric(content, subformat, label = null) { + this.addSubmessage(new NLIPSubMessage(AllowedFormats.generic, subformat, content, label)); + } +} + + +class NLIPFactory { + static createText(content, language = 'english', messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.text, subformat: language, content, label }); + } + + static createControl(content, language = 'english', label = null) { + return new NLIPMessage({ messagetype: ReservedTokens.control, format: AllowedFormats.text, subformat: language, content, label }); + } + + static createToken(token, tokenType, messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.token, subformat: tokenType, content: token, label }); + } + + static createJson(jsonDict, messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.structured, subformat: "JSON", content: jsonDict, label }); + } + + static createStructured(content, contentType, messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.structured, subformat: contentType, content, label }); + } + + static createBinary(content, binaryType, encoding, messagetype = null, label = null) { + const subformat = `${binaryType}/${encoding}`; + return new NLIPMessage({ messagetype, format: AllowedFormats.binary, subformat, content, label }); + } + + static createImage(content, encoding, messagetype = null, label = null) { + return this.createBinary(content, "image", encoding, messagetype, label); + } + + static createAudio(content, encoding, messagetype = null, label = null) { + return this.createBinary(content, "audio", encoding, messagetype, label); + } + + static createVideo(content, encoding, messagetype = null, label = null) { + return this.createBinary(content, "video", encoding, messagetype, label); + } + + static createLocationText(location, messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.location, subformat: "text", content: location, label }); + } + + static createLocationGps(location, messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.location, subformat: "gps", content: location, label }); + } + + static createErrorCode(errorCode, messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.error, subformat: "code", content: errorCode, label }); + } + + static createErrorText(errorDescr, messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.error, subformat: "text", content: errorDescr, label }); + } + + static createGeneric(content, subformat, messagetype = null, label = null) { + return new NLIPMessage({ messagetype, format: AllowedFormats.generic, subformat, content, label }); + } + + static createSubMessageFromJSON(jsonObject) { + return new NLIPSubMessage(jsonObject.format, jsonObject.subformat, jsonObject.content, jsonObject.label) + } + + static createMessageFromJSON(jsonObject) { + let submessages = null + if (jsonObject.submessages != null) { + submessages = jsonObject.submessages.map(sm => new NLIPSubMessage(sm.format, sm.subformat, sm.content, sm.label)) + } + + return new NLIPMessage({messagetype: jsonObject.messagetype, + format: jsonObject.format, subformat: jsonObject.subformat, content: jsonObject.content, + label: jsonObject.label, submessages: submessages}) + } +} + +class NLIPClient { + constructor(baseUrl = '', options = {}) { + this.baseUrl = baseUrl; + this.options = { timeout: 30000, ...options }; + this.correlator = null + } + + async sendMessage(text) { + const message = NLIPFactory.createText(text); + if (this.correlator != null) { + message.addConversationToken(this.correlator) + } + const response = await this.send(message); + const nlipMessage = NLIPFactory.createMessageFromJSON(response) + this.correlator = nlipMessage.extractToken(ReservedTokens.conv) + + const extractedText = nlipMessage.extractText() + return extractedText + } + + async sendWithImage(text, imageFile) { + const base64 = await this.fileToBase64(imageFile); + const message = NLIPFactory.createText(text); + message.addImage(base64, this.getFileExtension(imageFile)); + return this.send(message); + } + + async uploadFile(file) { + const formData = new FormData(); + formData.append('contents', file); + + const response = await fetch(`${this.baseUrl}/nlip/upload/`, { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error('Upload failed'); + return response.json(); + } + + async send(nlipMessage) { + const response = await fetch(`${this.baseUrl}/nlip/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: nlipMessage.toJSON(), + signal: AbortSignal.timeout(this.options.timeout) + }); + + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); + } + + // Utility methods + async fileToBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result.split(',')[1]); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + } + + getFileExtension(file) { + return file.name.split('.').pop() || 'bin'; + } +} + +export { NLIPMessage, NLIPFactory, NLIPClient, AllowedFormats, ReservedTokens, nlipCompareString, CaseInsensitiveEnum }; diff --git a/client/vite.config.js b/client/vite.config.js index fb4d238..32a7fb9 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -5,4 +5,13 @@ import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], + server: { + proxy: { + // Proxy API calls to the FastAPI backend during development + '/nlip': { + target: 'http://localhost:8010', + changeOrigin: true, + }, + }, + }, }) From ef9caa8d103f868845551f3b20ea9958ca44a387 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Wed, 22 Oct 2025 16:48:38 -0400 Subject: [PATCH 04/72] add text component --- client/components/Text.jsx | 97 +++----------------------- client/components/ui/ChatContainer.jsx | 13 ++++ client/components/ui/InputForm.jsx | 57 +++++++++++++++ client/components/ui/MessageBox.jsx | 39 +++++++++++ 4 files changed, 117 insertions(+), 89 deletions(-) create mode 100644 client/components/ui/ChatContainer.jsx create mode 100644 client/components/ui/InputForm.jsx create mode 100644 client/components/ui/MessageBox.jsx diff --git a/client/components/Text.jsx b/client/components/Text.jsx index 148925e..9da95cb 100644 --- a/client/components/Text.jsx +++ b/client/components/Text.jsx @@ -1,100 +1,19 @@ import { useState } from 'react'; -import { NLIPClient } from '../utils/nlip'; +import MessageBox from './ui/MessageBox'; +import InputForm from './ui/InputForm'; + +import ChatContainer from './ui/ChatContainer'; export default function Text() { const [messages, setMessages] = useState([]); - const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); - const nlipClient = new NLIPClient(); - - const sendMessage = async (e) => { - e.preventDefault(); - if (!input.trim()) return; - - const userMessage = input.trim(); - setInput(''); - - // Add user message to chat - setMessages(prev => [...prev, { role: 'user', content: userMessage }]); - setIsLoading(true); - - try { - const data = await nlipClient.sendMessage(userMessage); - const botMessage = data || 'No response'; - - // Add bot response to chat - setMessages(prev => [...prev, { role: 'assistant', content: botMessage }]); - } catch (error) { - console.error('Error sending message:', error); - setMessages(prev => [...prev, { - role: 'assistant', - content: 'Error connecting to chat server.' - }]); - } finally { - setIsLoading(false); - } - }; return (
-
- {/* Header */} -
-

NLIP Client

-
- - {/* Chat Box */} -
- {messages.length === 0 && ( -
-

Start a conversation!

-
- )} - {messages.map((msg, index) => ( -
-
-

{msg.content}

-
-
- ))} - {isLoading && ( -
-
-
-
-
-
-
-
-
- )} -
- - {/* Input Form */} -
-
- setInput(e.target.value)} - placeholder="Type your message..." - className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" - disabled={isLoading} - /> - -
-
-
+ + + +
); } diff --git a/client/components/ui/ChatContainer.jsx b/client/components/ui/ChatContainer.jsx new file mode 100644 index 0000000..278be66 --- /dev/null +++ b/client/components/ui/ChatContainer.jsx @@ -0,0 +1,13 @@ + + +export default function ChatContainer({ header, children }) { + return ( +
+
+

{header}

+
+ + {children} +
+ ) +} \ No newline at end of file diff --git a/client/components/ui/InputForm.jsx b/client/components/ui/InputForm.jsx new file mode 100644 index 0000000..3b335be --- /dev/null +++ b/client/components/ui/InputForm.jsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; + +import { NLIPClient } from '../../utils/nlip'; + +export default function InputForm({ setMessages, isLoading, setIsLoading }) { + const [input, setInput] = useState(''); + const nlipClient = new NLIPClient(); + + const sendMessage = async (e) => { + e.preventDefault(); + if (!input.trim()) return; + + const userMessage = input.trim(); + setInput(''); + + // Add user message to chat + setMessages(prev => [...prev, { role: 'user', content: userMessage }]); + setIsLoading(true); + + try { + const data = await nlipClient.sendMessage(userMessage); + const botMessage = data || 'No response'; + + // Add bot response to chat + setMessages(prev => [...prev, { role: 'assistant', content: botMessage }]); + } catch (error) { + console.error('Error sending message:', error); + setMessages(prev => [...prev, { + role: 'assistant', + content: 'Error connecting to chat server.' + }]); + } finally { + setIsLoading(false); + } + }; + return ( +
+
+ setInput(e.target.value)} + placeholder="Type your message..." + className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" + disabled={isLoading} + /> + +
+
+ ) +} \ No newline at end of file diff --git a/client/components/ui/MessageBox.jsx b/client/components/ui/MessageBox.jsx new file mode 100644 index 0000000..7d9ba51 --- /dev/null +++ b/client/components/ui/MessageBox.jsx @@ -0,0 +1,39 @@ +export default function ChatBox({messages, isLoading}) { + const renderMessageBubbles = () => { + return ( +
+
+
+
+
+
+
+
+
+ ) + } + + return ( +
+ {messages.length === 0 && ( +
+

Start a conversation!

+
+ )} + {messages.map((msg, index) => ( +
+
+

{msg.content}

+
+
+ ))} + {isLoading && ( + renderMessageBubbles() + )} +
+ ) +} \ No newline at end of file From 122c1bd2a1a020c28f563386c60302b5d31480a0 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Wed, 22 Oct 2025 17:22:21 -0400 Subject: [PATCH 05/72] create vite script partially --- client/vite.config.js | 2 +- nlip_web/nlip_ext.py | 7 ++++++ nlip_web/vite_chat.py | 52 +++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 nlip_web/vite_chat.py diff --git a/client/vite.config.js b/client/vite.config.js index 32a7fb9..8edc8c8 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -9,7 +9,7 @@ export default defineConfig({ proxy: { // Proxy API calls to the FastAPI backend during development '/nlip': { - target: 'http://localhost:8010', + target: 'http://localhost:8030', changeOrigin: true, }, }, diff --git a/nlip_web/nlip_ext.py b/nlip_web/nlip_ext.py index 54587bc..06ff735 100644 --- a/nlip_web/nlip_ext.py +++ b/nlip_web/nlip_ext.py @@ -139,6 +139,13 @@ def __init__(self, indexFile="index.html", pathname = "/static", static_dir="sta def setup_webserver(self, thisapp:server.NLIP_Application, port, host="localhost") -> FastAPI: app = server.setup_server(thisapp) + + # Mount the assets folder if it exists (for Vite builds) + assets_dir = os.path.join(self.static_dir, "assets") + if os.path.exists(assets_dir) and os.path.isdir(assets_dir): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + + # Mount the main static directory app.mount(self.pathname, StaticFiles(directory=self.static_dir)) @app.get("/", response_class=HTMLResponse) diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py new file mode 100644 index 0000000..b6ea3ca --- /dev/null +++ b/nlip_web/vite_chat.py @@ -0,0 +1,52 @@ +from nlip_web.genai import StatefulGenAI +from nlip_web import nlip_ext as nlip_ext +from nlip_web.env import read_digits, read_string +from nlip_server import server +from nlip_sdk import nlip + + +class ChatApplication(nlip_ext.SafeStatefulApplication): + def __init__(self): + super().__init__() + self.local_port = read_digits("LOCAL_PORT",8030) + self.model = read_string("CHAT_MODEL", "llava") + self.host = read_string("CHAT_HOST", "localhost") + self.port = read_digits("CHAT_PORT", 11434) + + + def create_stateful_session(self) -> server.NLIP_Session: + genAI = StatefulGenAI(self.host, self.port,self.model) + session = ChatSession() + session.set_correlator() + self.store_session_data(session.get_correlator(), genAI) + return session + + + + +class ChatSession(nlip_ext.StatefulSession): + + def execute( + self, msg: nlip.NLIP_Message + ) -> nlip.NLIP_Message: + text = msg.extract_text() + images = list() + binary_contents = msg.extract_field_list("binary") + for base64_content in binary_contents: + images.append(base64_content) + + chat_server = self.nlip_app.retrieve_session_data(self.get_correlator()) + if chat_server is None: + return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") + + # print(f'Received text {text[0:10]}...') + response = chat_server.chat_multimodal(text, images = images) + # print(f'Received response {response[0:10]}...') + return nlip.NLIP_Factory.create_text(response) + + + +if __name__ == "__main__": + chatapp = ChatApplication() + webapp = nlip_ext.WebApplication(indexFile="client/dist/index.html", static_dir="client/dist") + app = webapp.setup_webserver(chatapp, port=chatapp.local_port) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 73f588b..b9a4f9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,4 +17,5 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] chat = "scripts:start_chat" image = "scripts:start_image" +vite = "scripts:start_vite" From b2a88adde00a8b9ef3b41925c362e3fd2d70d295 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Wed, 22 Oct 2025 17:25:40 -0400 Subject: [PATCH 06/72] fix command --- scripts.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts.py b/scripts.py index 6ac3686..9873594 100644 --- a/scripts.py +++ b/scripts.py @@ -34,4 +34,15 @@ def start_image(): """ my_env = get_env(local_port=8020,chat_model="llava") command = f"poetry run python nlip_web/image_chat.py" - run_command(command, my_env) \ No newline at end of file + run_command(command, my_env) + +def start_vite(): + my_env = get_env(local_port=8030,chat_model="llava") + try: + # Build in client directory + subprocess.run(["npm", "run", "build"], env=my_env, check=True, cwd="client") + # Run Python script from parent directory + subprocess.run(["poetry", "run", "python", "nlip_web/vite_chat.py"], + env=my_env, check=True) + except subprocess.CalledProcessError as e: + print(f"Error executing command: {e}") \ No newline at end of file From 39f324c4cc97438b88115ecc822ed76f5f2dfb12 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Thu, 23 Oct 2025 10:36:22 -0400 Subject: [PATCH 07/72] add conditional component rendering --- client/package.json | 3 +++ client/src/App.jsx | 7 +++++++ scripts.py | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index 36cd7ec..8823fb1 100644 --- a/client/package.json +++ b/client/package.json @@ -6,6 +6,9 @@ "scripts": { "dev": "vite", "build": "vite build", + "build:text": "VITE_APP_MODE=text vite build", + "build:products": "VITE_APP_MODE=products vite build", + "build:images": "VITE_APP_MODE=images vite build", "lint": "eslint .", "preview": "vite preview" }, diff --git a/client/src/App.jsx b/client/src/App.jsx index 6b0def5..cf62bef 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,10 +1,17 @@ import Text from "../components/Text" function App() { + const mode = import.meta.env.VITE_APP_MODE; + + if (mode === "text") return + if (mode === "products") return + if (mode === "images") return return ( <> + + ) } diff --git a/scripts.py b/scripts.py index 9873594..3847804 100644 --- a/scripts.py +++ b/scripts.py @@ -40,7 +40,7 @@ def start_vite(): my_env = get_env(local_port=8030,chat_model="llava") try: # Build in client directory - subprocess.run(["npm", "run", "build"], env=my_env, check=True, cwd="client") + subprocess.run(["npm", "run", "build:text"], env=my_env, check=True, cwd="client") # Run Python script from parent directory subprocess.run(["poetry", "run", "python", "nlip_web/vite_chat.py"], env=my_env, check=True) From 2b9ceffce44f9669497d360e125ae8e25de00ef4 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Tue, 4 Nov 2025 11:25:53 -0500 Subject: [PATCH 08/72] add products --- client/components/Products.jsx | 17 +++++++++ client/components/ui/ProductResults.jsx | 31 +++++++++++++++ client/components/ui/SearchForm.jsx | 51 +++++++++++++++++++++++++ client/src/App.jsx | 1 + pyproject.toml | 4 +- scripts.py | 13 ++++++- 6 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 client/components/ui/ProductResults.jsx create mode 100644 client/components/ui/SearchForm.jsx diff --git a/client/components/Products.jsx b/client/components/Products.jsx index e69de29..de53651 100644 --- a/client/components/Products.jsx +++ b/client/components/Products.jsx @@ -0,0 +1,17 @@ +import { useState } from 'react'; +import SearchForm from './ui/SearchForm'; +import ProductResults from './ui/ProductResults'; + +export default function Products() { + const [products, setProducts] = useState({}); + const [isLoading, setIsLoading] = useState(false); + + return ( +
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/client/components/ui/ProductResults.jsx b/client/components/ui/ProductResults.jsx new file mode 100644 index 0000000..605a16c --- /dev/null +++ b/client/components/ui/ProductResults.jsx @@ -0,0 +1,31 @@ +export default function ProductResults({products, isLoading}) { + return ( + <> + {Object.keys(products).length > 0 && ( +
+ {Object.entries(products).map(([storeName, storeProducts]) => ( +
+

{storeName}

+
+ {storeProducts.map((product, index) => ( +
+

{product.name}

+ {product.description && ( +

{product.description}

+ )} +

${product.price}

+
+ ))} +
+
+ ))} +
+ )} + {Object.keys(products).length === 0 && !isLoading && ( +
+

Search for products to see results

+
+ )} + + ) +} \ No newline at end of file diff --git a/client/components/ui/SearchForm.jsx b/client/components/ui/SearchForm.jsx new file mode 100644 index 0000000..62b7805 --- /dev/null +++ b/client/components/ui/SearchForm.jsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { NLIPClient } from "../../utils/nlip"; + +export default function SearchForm({ setProducts, isLoading, setIsLoading}) { + const [query, setQuery] = useState(''); + const nlipClient = new NLIPClient(); + + const handleSearch = async (e) => { + e.preventDefault(); + if (!query.trim()) return; + + setIsLoading(true); + try { + // const response = await nlipClient.sendMessage(query.trim()) + + const response = '{"Best Buy":[{"name":"Dell XPS 15","price":1299.99,"description":"15.6-inch FHD display, Intel Core i7, 16GB RAM, 512GB SSD"},{"name":"HP Pavilion 14","price":649.99,"description":"14-inch HD display, AMD Ryzen 5, 8GB RAM, 256GB SSD"}],"Amazon":[{"name":"Lenovo ThinkPad X1 Carbon","price":1549.99,"description":"14-inch 2K display, Intel Core i7, 16GB RAM, 1TB SSD, Business laptop"},{"name":"ASUS VivoBook 15","price":529.99,"description":"15.6-inch FHD display, Intel Core i5, 12GB RAM, 512GB SSD"}],"Walmart":[{"name":"Acer Aspire 5","price":479.99,"description":"15.6-inch FHD display, AMD Ryzen 3, 8GB RAM, 256GB SSD"},{"name":"Microsoft Surface Laptop 5","price":1199.99,"description":"13.5-inch PixelSense touchscreen, Intel Core i5, 8GB RAM, 256GB SSD"}]}'; + + const data = JSON.parse(response); + + if (data) { + setProducts(data); + } + } catch (error) { + console.error('Error fetching products:', error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Product Search

+
+ setQuery(e.target.value)} + placeholder="Search for products..." + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-500" + /> + +
+
+ ) +} \ No newline at end of file diff --git a/client/src/App.jsx b/client/src/App.jsx index cf62bef..6676196 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,4 +1,5 @@ import Text from "../components/Text" +import Products from "../components/Products" function App() { const mode = import.meta.env.VITE_APP_MODE; diff --git a/pyproject.toml b/pyproject.toml index b9a4f9f..00a42a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,5 +17,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] chat = "scripts:start_chat" image = "scripts:start_image" -vite = "scripts:start_vite" +vite-text = "scripts:start_vite_text" +vite-products = "scripts:start_vite_products" + diff --git a/scripts.py b/scripts.py index 3847804..33d0e12 100644 --- a/scripts.py +++ b/scripts.py @@ -36,7 +36,7 @@ def start_image(): command = f"poetry run python nlip_web/image_chat.py" run_command(command, my_env) -def start_vite(): +def start_vite_text(): my_env = get_env(local_port=8030,chat_model="llava") try: # Build in client directory @@ -44,5 +44,16 @@ def start_vite(): # Run Python script from parent directory subprocess.run(["poetry", "run", "python", "nlip_web/vite_chat.py"], env=my_env, check=True) + except subprocess.CalledProcessError as e: + print(f"Error executing command: {e}") + +def start_vite_products(): + my_env = get_env(local_port=8040,chat_model="llava") + try: + # Build in client directory + subprocess.run(["npm", "run", "build:products"], env=my_env, check=True, cwd="client") + # Run Python script from parent directory + subprocess.run(["poetry", "run", "python", "nlip_web/vite_chat.py"], + env=my_env, check=True) except subprocess.CalledProcessError as e: print(f"Error executing command: {e}") \ No newline at end of file From c0cf21afdd7da9fe00849e6a66e97fb5169106e8 Mon Sep 17 00:00:00 2001 From: Maximus Date: Wed, 5 Nov 2025 13:23:11 -0500 Subject: [PATCH 09/72] initial commit for website modules. Added deluce_bookstore_module. Started documentation for website_modules. --- website_modules/WEB_MODULES.md | 16 +++ website_modules/deluce_bookstore_module.py | 144 +++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 website_modules/WEB_MODULES.md create mode 100644 website_modules/deluce_bookstore_module.py diff --git a/website_modules/WEB_MODULES.md b/website_modules/WEB_MODULES.md new file mode 100644 index 0000000..9c76bbe --- /dev/null +++ b/website_modules/WEB_MODULES.md @@ -0,0 +1,16 @@ +# Website Modules + +This folder contains the *website_modules* which act as an interface between the client and the website. + +A module for each site is needed to facilitate quering and parsing the resulting information for various brands and stores. + +## Usage + +- [NLIP Server](https://github.com/nlip-project/nlip_server), is required to use the libraries provided +- [NLIP SDK](https://github.com/nlip-project/nlip_sdk), is required for development + +## Examples + +A [Hello World Program](./hello_world/) can be found in its respective folder. + +Module files are commonly used in a similar fashion to a library, although some can be run as [scripts](./deluce_bookstore_module.py) to test the usage. \ No newline at end of file diff --git a/website_modules/deluce_bookstore_module.py b/website_modules/deluce_bookstore_module.py new file mode 100644 index 0000000..e2a08f7 --- /dev/null +++ b/website_modules/deluce_bookstore_module.py @@ -0,0 +1,144 @@ +# module for UWO Deluce Bookstore + +# imports for library functions +from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support import expected_conditions as EC +import time + +## CONSTANT VARIABLES +WEB_ADDRESS = 'https://bookstore.uwo.ca/' +SEARCH_ARG = 'search/products?search=' +PAGE_NUM = "&page=" # no page number argument for fist page, second page starts at &page=1 +SORT_ARG = "&sort=" # add search filter like: &sort=search_api_relevance%20DESC +HTTP_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Connection': 'keep-alive', +} + +# takes a serch term or formatted query to search the site +# returns list of product objects +def search_product(query_term) -> list: + options = Options() + options.add_argument("--headless=new") + options.add_argument(f"user-agent={HTTP_HEADERS['User-Agent']}") + driver = webdriver.Chrome(options=options) + + product_list = [] + + try: + driver.get(WEB_ADDRESS + SEARCH_ARG + query_term) + html = driver.page_source + + parsed_content = parse_site_content(html) + # if first page is non-empty, query the next page. Otherwise no products matching the search term exist. + if len(parsed_content) < 1: + driver.quit() + return + + product_list.append(parsed_content) + + # first page non-empty, query remaining pages of content. + end_of_content = False + current_page = 1 # second page starts at 1 + while not end_of_content: + # sleep to not overwhelm site with continuous queries + time.sleep(5) + driver.get(WEB_ADDRESS + SEARCH_ARG + query_term + PAGE_NUM + str(current_page)) + parsed_content = parse_site_content(driver.page_source) + + if len(parsed_content) < 1: + # end of query content + break + # increment page counter + current_page += 1 + product_list.append(parsed_content) + finally: + driver.quit() + + return product_list + +# return search results from a specific page +def search_product_specific_page(query_term, pagenum) -> list: + options = Options() + options.add_argument("--headless=new") + options.add_argument(f"user-agent={HTTP_HEADERS['User-Agent']}") + driver = webdriver.Chrome(options=options) + + product_list = [] + + try: + driver.get(WEB_ADDRESS + SEARCH_ARG + query_term + PAGE_NUM + str(pagenum)) + html = driver.page_source + + parsed_content = parse_site_content(html) + # if first page is non-empty, query the next page. Otherwise no products matching term exist. + if len(parsed_content) < 1: + driver.quit() + return + + product_list.append(parsed_content) + finally: + driver.quit() + + return product_list + +# parses out poduct information from html return +# returns list of dicts/json formatted objects +def parse_site_content(html_content) -> list: + + soup = BeautifulSoup(html_content, 'html.parser') + + # message displayed if exceeded page with results, or if no results found + end_of_results = soup.find_all("div", class_="view-empty") + if len(end_of_results) != 0: + return [] + + product_information = [] + + products = soup.find_all("div", class_="views-row") + + for prod in products: + temp_prod = {} + # extract item name + temp_prod["item_name"] = prod.find('a').text + + + # commerce-product-field is where price information is stored + price_field = prod.find(class_="commerce-product-field") + temp_prod["price"] = price_field.find("div", class_="field-item").text + + # add link to product page using link to item id stored in anchor tag + temp_prod["link"] = (WEB_ADDRESS[:-1] + prod.find('a')['href']) + + # push product information to list + product_information.append(temp_prod) + + return product_information + +def get_web_address() -> str: + return WEB_ADDRESS + +# return list of constant variables and configurable options +def get_config() -> dict: + return { + "web_address": WEB_ADDRESS, + "search_arg": SEARCH_ARG, + "page_num": PAGE_NUM, + "sort_arg": SORT_ARG, + "sort_arg_options": [], ## TODO: populate the valid search filters + "http_headers": HTTP_HEADERS, + } + +# main function for use if run as script +if __name__ == "__main__": + print("Input serch term: ") + search_term = input() + + return_vals = search_product(search_term) + print(return_vals) + \ No newline at end of file From 940fb438a587c7bf7bfda75e951a8017c6b7096c Mon Sep 17 00:00:00 2001 From: Maximus Date: Wed, 5 Nov 2025 15:32:28 -0500 Subject: [PATCH 10/72] started walmart.ca website module --- website_modules/walmart_ca_module.py | 82 ++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 website_modules/walmart_ca_module.py diff --git a/website_modules/walmart_ca_module.py b/website_modules/walmart_ca_module.py new file mode 100644 index 0000000..eb4ed5f --- /dev/null +++ b/website_modules/walmart_ca_module.py @@ -0,0 +1,82 @@ +# module for walmart.ca/en/ + +import requests +from bs4 import BeautifulSoup +import json +import cloudscraper +import time + +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support import expected_conditions as EC +import time + +## CONSTANT VARIABLES +WEB_ADDRESS = 'https://www.walmart.ca/en/' +SEARCH_ARG = 'search?q=' +SORT_ARG = '&sort=' +SORT_ARG_OPTIONS = ['price_low','price_high','best_match'] +FILTER_CATEGORY = '&catId=' ## integer value representing category +## page numbering starts at 1 for first page +PAGE_NUM = '&page=' ## defaults to being paired with: AFFINITY_OVERRIDE +AFFINITY_OVERRIDE = '&affinityOverride=default' + +FILTER_BRAND = '&facet=brand%3A' ## brand%3a + string +## for multiple brands concat with: %7C%7C +## &facet=brand%3APentel%7C%7Cbrand%3ABIC + +HTTP_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', +} + +# return list of constant variables and configurable options +def get_config() -> dict: + return { + "web_address": WEB_ADDRESS, + "search_arg": SEARCH_ARG, + "page_num": PAGE_NUM, + "affinity_override": AFFINITY_OVERRIDE, + "sort_arg": SORT_ARG, + "sort_arg_options": SORT_ARG_OPTIONS, ## TODO: populate the valid search filters + "filter_category" :FILTER_CATEGORY, + "filter_brand": FILTER_BRAND, + "http_headers": HTTP_HEADERS, + } + +def search_product(search_term): + + options = Options() + ##options.add_argument("--headless=new") + options.add_argument(f"user-agent={HTTP_HEADERS['User-Agent']}") + ##options.add_argument("--no-sandbox") + ##options.add_argument("--disable-dev-shm-usage") + ##options.add_argument("--disable-blink-features=AutomationControlled") + driver = webdriver.Chrome(options=options) + + url = WEB_ADDRESS + SEARCH_ARG + search_term + try: + driver.get(url=url) + + time.sleep(5) + + html = driver.page_source + finally: + driver.quit() + + soup = BeautifulSoup(html, 'html.parser') + + return soup + +def parse_site_content(html_content) -> list: + return [] + +# main function for use as a script +if __name__ == "__main__": + print("Input serch term: ") + search_term = input() + + return_vals = search_product(search_term) + print(return_vals) \ No newline at end of file From c0601354e56956593b35ac00cb7714546ef7977e Mon Sep 17 00:00:00 2001 From: Maximus Date: Wed, 5 Nov 2025 17:05:59 -0500 Subject: [PATCH 11/72] updated deluce_bookstore_module to detect and list, multiple product sizes. --- website_modules/deluce_bookstore_module.py | 39 +++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/website_modules/deluce_bookstore_module.py b/website_modules/deluce_bookstore_module.py index e2a08f7..7398c06 100644 --- a/website_modules/deluce_bookstore_module.py +++ b/website_modules/deluce_bookstore_module.py @@ -13,6 +13,10 @@ SEARCH_ARG = 'search/products?search=' PAGE_NUM = "&page=" # no page number argument for fist page, second page starts at &page=1 SORT_ARG = "&sort=" # add search filter like: &sort=search_api_relevance%20DESC +## options = [relevance_desc, newest to oldest, oldest to newest, price increasing, price decreasing] +SORT_ARG_OPTIONS = ['search_api_relevance%20DESC', 'created+DESC', 'created+ASC', 'search_api_aggregation_2+ASC', 'search_api_aggregation_2+DESC'] +CATEGORY = '&category=' +CATEGORY_OPTIONS = ['All'] # All is default, category id can be specified HTTP_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', 'Accept-Language': 'en-US,en;q=0.9', @@ -99,22 +103,47 @@ def parse_site_content(html_content) -> list: return [] product_information = [] - products = soup.find_all("div", class_="views-row") for prod in products: temp_prod = {} - # extract item name + # extract it-em name temp_prod["item_name"] = prod.find('a').text - # commerce-product-field is where price information is stored price_field = prod.find(class_="commerce-product-field") temp_prod["price"] = price_field.find("div", class_="field-item").text + ## can potentially contain price If item is on sale. + + # check if item has sizes: + sizes_radio_group = prod.find(class_='attribute-widgets') + temp_prod['sizes'] = ['N/A'] + + if sizes_radio_group != None: + temp_prod['sizes'] = [] + radio_buttons = prod.find_all(class_="control-label") + for opt in radio_buttons: + if opt.text == 'Size:': ## skip control values with non-size specific information + continue + temp_prod['sizes'].append(opt.text) + + ## TODO: fix availability for items with multiple sizes + temp_prod["availability"] = availability + + availability = prod.find(class_="add-to-cart").text + if availability == "Add to cart": + availability = "in stock" + + temp_prod["product_photo"] = prod.find('img')['src'] # add link to product page using link to item id stored in anchor tag temp_prod["link"] = (WEB_ADDRESS[:-1] + prod.find('a')['href']) + ## Further product information available on the item page + # Stock count + # on order count + # SKU -> differs from product id. is different for each size listing + # push product information to list product_information.append(temp_prod) @@ -130,8 +159,10 @@ def get_config() -> dict: "search_arg": SEARCH_ARG, "page_num": PAGE_NUM, "sort_arg": SORT_ARG, - "sort_arg_options": [], ## TODO: populate the valid search filters + "sort_arg_options": SORT_ARG_OPTIONS, "http_headers": HTTP_HEADERS, + "filter_category": CATEGORY, + "filter_category_options": CATEGORY_OPTIONS } # main function for use if run as script From 06714fb627a0f000feb8af2ada0bad9995fac927 Mon Sep 17 00:00:00 2001 From: Maximus Date: Thu, 6 Nov 2025 11:01:54 -0500 Subject: [PATCH 12/72] update poetry files. began integration of website_modules to backend --- client/package-lock.json | 54 +++ nlip_web/vite_chat.py | 7 +- poetry.lock | 362 ++++++++++++++++++++- pyproject.toml | 2 + start.sh | 1 + website_modules/WEB_MODULES.md | 26 +- website_modules/deluce_bookstore_module.py | 2 +- 7 files changed, 437 insertions(+), 17 deletions(-) create mode 100755 start.sh diff --git a/client/package-lock.json b/client/package-lock.json index e2c6b22..d1c58ec 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1425,6 +1425,60 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz", diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py index b6ea3ca..2f6a0e9 100644 --- a/nlip_web/vite_chat.py +++ b/nlip_web/vite_chat.py @@ -4,6 +4,8 @@ from nlip_server import server from nlip_sdk import nlip +from website_modules import deluce_bookstore_module as dbm + class ChatApplication(nlip_ext.SafeStatefulApplication): def __init__(self): @@ -21,8 +23,6 @@ def create_stateful_session(self) -> server.NLIP_Session: self.store_session_data(session.get_correlator(), genAI) return session - - class ChatSession(nlip_ext.StatefulSession): @@ -39,6 +39,9 @@ def execute( if chat_server is None: return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") + #results = dbm.search_product(text) + #print(results) + # print(f'Received text {text[0:10]}...') response = chat_server.chat_multimodal(text, images = images) # print(f'Received response {response[0:10]}...') diff --git a/poetry.lock b/poetry.lock index be74065..bcb0254 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -6,6 +6,7 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -17,6 +18,7 @@ version = "4.10.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, @@ -31,23 +33,158 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.26.1)"] +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.2" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515"}, + {file = "beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "certifi" -version = "2025.8.3" +version = "2025.10.5" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, + {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "os_name == \"nt\" and implementation_name != \"pypy\"" files = [ - {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, - {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, ] +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + [[package]] name = "click" version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, @@ -62,6 +199,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -73,6 +212,7 @@ version = "2.8.0" description = "DNS toolkit" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, @@ -85,7 +225,7 @@ doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] doq = ["aioquic (>=1.2.0)"] idna = ["idna (>=3.10)"] trio = ["trio (>=0.30)"] -wmi = ["wmi (>=1.5.1)"] +wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] [[package]] name = "email-validator" @@ -93,6 +233,7 @@ version = "2.3.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"}, {file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"}, @@ -108,6 +249,8 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -125,6 +268,7 @@ version = "0.115.14" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, @@ -151,6 +295,7 @@ version = "0.0.10" description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi_cli-0.0.10-py3-none-any.whl", hash = "sha256:04bef56b49f7357c6c4acd4f793b4433ed3f511be431ed0af68db6d3f8bd44b3"}, {file = "fastapi_cli-0.0.10.tar.gz", hash = "sha256:85a93df72ff834c3d2a356164512cabaf8f093d50eddad9309065a9c9ac5193a"}, @@ -172,6 +317,7 @@ version = "0.1.5" description = "Deploy and manage FastAPI Cloud apps from the command line 🚀" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "fastapi_cloud_cli-0.1.5-py3-none-any.whl", hash = "sha256:d80525fb9c0e8af122370891f9fa83cf5d496e4ad47a8dd26c0496a6c85a012a"}, {file = "fastapi_cloud_cli-0.1.5.tar.gz", hash = "sha256:341ee585eb731a6d3c3656cb91ad38e5f39809bf1a16d41de1333e38635a7937"}, @@ -195,6 +341,7 @@ version = "0.16.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -206,6 +353,7 @@ version = "1.0.9" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -227,6 +375,7 @@ version = "0.6.4" description = "A collection of framework independent HTTP protocol utils." optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, @@ -282,6 +431,7 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -294,7 +444,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -306,6 +456,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -320,6 +471,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -337,6 +489,7 @@ version = "4.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, @@ -360,6 +513,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -430,6 +584,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -441,6 +596,7 @@ version = "0.1.0" description = "" optional = false python-versions = "^3.10" +groups = ["main"] files = [] develop = true @@ -457,6 +613,7 @@ version = "0.1.0" description = "" optional = false python-versions = "^3.10" +groups = ["main"] files = [] develop = true @@ -468,12 +625,41 @@ nlip-sdk = {path = "../nlip_sdk", develop = true} type = "directory" url = "../nlip_server" +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "os_name == \"nt\" and implementation_name != \"pypy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + [[package]] name = "pydantic" version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, @@ -488,7 +674,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -496,6 +682,7 @@ version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, @@ -607,6 +794,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -615,12 +803,26 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "python-dotenv" version = "1.1.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, @@ -635,6 +837,7 @@ version = "0.0.20" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, @@ -646,6 +849,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -708,6 +912,7 @@ version = "14.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, @@ -726,6 +931,7 @@ version = "0.15.1" description = "Rich toolkit for building command-line applications" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "rich_toolkit-0.15.1-py3-none-any.whl", hash = "sha256:36a0b1d9a135d26776e4b78f1d5c2655da6e0ef432380b5c6b523c8d8ab97478"}, {file = "rich_toolkit-0.15.1.tar.gz", hash = "sha256:6f9630eb29f3843d19d48c3bd5706a086d36d62016687f9d0efa027ddc2dd08a"}, @@ -742,6 +948,7 @@ version = "0.6.4" description = "Python Bindings for the ignore crate" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "rignore-0.6.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c201375cfe76e56e61fcdfe50d0882aafb49544b424bfc828e0508dc9fbc431b"}, {file = "rignore-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4962d537e377394292c4828e1e9c620618dd8daa49ba746abe533733a89f8644"}, @@ -868,12 +1075,33 @@ files = [ {file = "rignore-0.6.4.tar.gz", hash = "sha256:e893fdd2d7fdcfa9407d0b7600ef2c2e2df97f55e1c45d4a8f54364829ddb0ab"}, ] +[[package]] +name = "selenium" +version = "4.38.0" +description = "Official Python bindings for Selenium WebDriver" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "selenium-4.38.0-py3-none-any.whl", hash = "sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd"}, + {file = "selenium-4.38.0.tar.gz", hash = "sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c"}, +] + +[package.dependencies] +certifi = ">=2025.10.5" +trio = ">=0.31.0,<1.0" +trio-websocket = ">=0.12.2,<1.0" +typing_extensions = ">=4.15.0,<5.0" +urllib3 = {version = ">=2.5.0,<3.0", extras = ["socks"]} +websocket-client = ">=1.8.0,<2.0" + [[package]] name = "sentry-sdk" version = "2.37.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "sentry_sdk-2.37.0-py2.py3-none-any.whl", hash = "sha256:89c1ed205d5c25926558b64a9bed8a5b4fb295b007cecc32c0ec4bf7694da2e1"}, {file = "sentry_sdk-2.37.0.tar.gz", hash = "sha256:2c661a482dd5accf3df58464f31733545745bb4d5cf8f5e46e0e1c4eed88479f"}, @@ -931,6 +1159,7 @@ version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, @@ -942,17 +1171,43 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.8" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, + {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, +] + [[package]] name = "starlette" version = "0.46.2" description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, @@ -964,12 +1219,52 @@ anyio = ">=3.6.2,<5" [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] +[[package]] +name = "trio" +version = "0.32.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"}, + {file = "trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.12.2" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, + {file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +outcome = ">=1.2.0" +trio = ">=0.11" +wsproto = ">=0.14" + [[package]] name = "typer" version = "0.17.4" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824"}, {file = "typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580"}, @@ -987,6 +1282,7 @@ version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, @@ -998,6 +1294,7 @@ version = "0.4.1" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, @@ -1012,13 +1309,17 @@ version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] +[package.dependencies] +pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} + [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1029,6 +1330,7 @@ version = "0.35.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, @@ -1042,12 +1344,12 @@ httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standar python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +uvloop = {version = ">=0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "uvloop" @@ -1055,6 +1357,8 @@ version = "0.21.0" description = "Fast implementation of asyncio event loop on top of libuv" optional = false python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" files = [ {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, @@ -1106,6 +1410,7 @@ version = "1.1.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc"}, {file = "watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df"}, @@ -1218,12 +1523,30 @@ files = [ [package.dependencies] anyio = ">=3.0.0" +[[package]] +name = "websocket-client" +version = "1.9.0" +description = "WebSocket client for Python with low level API options" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, + {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, +] + +[package.extras] +docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] +optional = ["python-socks", "wsaccel"] +test = ["pytest", "websockets"] + [[package]] name = "websockets" version = "15.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, @@ -1296,7 +1619,22 @@ files = [ {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, ] +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "faece5baf9eadee0bfa0e32f28ab8c9976ef3877e0d55ed2bbf849bcf026e02d" +content-hash = "57819d0150c14bfc6d419c6a7152e623bba7308bb2df0ca8b37bcebf8edc9dab" diff --git a/pyproject.toml b/pyproject.toml index 00a42a4..4e5c2e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,8 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" nlip-server = {path = "../nlip_server", develop = true} +beautifulsoup4 = "^4.14.2" +selenium = "^4.38.0" [build-system] diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..768e9bf --- /dev/null +++ b/start.sh @@ -0,0 +1 @@ +poetry run vite-products \ No newline at end of file diff --git a/website_modules/WEB_MODULES.md b/website_modules/WEB_MODULES.md index 9c76bbe..0af85dc 100644 --- a/website_modules/WEB_MODULES.md +++ b/website_modules/WEB_MODULES.md @@ -4,7 +4,14 @@ This folder contains the *website_modules* which act as an interface between the A module for each site is needed to facilitate quering and parsing the resulting information for various brands and stores. -## Usage +## Requirements + +When used with nlip_web the following packages have to be added to poetry: + +1. `poetry add beautifulsoup4` +2. `poetry add selenium` + +** Server side requiremments** - [NLIP Server](https://github.com/nlip-project/nlip_server), is required to use the libraries provided - [NLIP SDK](https://github.com/nlip-project/nlip_sdk), is required for development @@ -13,4 +20,19 @@ A module for each site is needed to facilitate quering and parsing the resulting A [Hello World Program](./hello_world/) can be found in its respective folder. -Module files are commonly used in a similar fashion to a library, although some can be run as [scripts](./deluce_bookstore_module.py) to test the usage. \ No newline at end of file +Module files are commonly used in a similar fashion to a library, although some can be run as [scripts](./deluce_bookstore_module.py) to test the usage. + +## Output + +```json +{ + "item_name": "string", + "price": "string", + "sizes": [ + "string" + ], + "availability": "string", + "product_photo": "string", + "link": "string" +} +``` \ No newline at end of file diff --git a/website_modules/deluce_bookstore_module.py b/website_modules/deluce_bookstore_module.py index 7398c06..aeed70d 100644 --- a/website_modules/deluce_bookstore_module.py +++ b/website_modules/deluce_bookstore_module.py @@ -128,9 +128,9 @@ def parse_site_content(html_content) -> list: temp_prod['sizes'].append(opt.text) ## TODO: fix availability for items with multiple sizes + availability = prod.find(class_="add-to-cart").text temp_prod["availability"] = availability - availability = prod.find(class_="add-to-cart").text if availability == "Add to cart": availability = "in stock" From 66e5f116c23badf4d9bbed6d88b1fef326a8eec0 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Mon, 10 Nov 2025 12:01:16 -0500 Subject: [PATCH 13/72] fix file structure --- client/src/App.jsx | 4 ++-- client/{ => src}/components/Image.jsx | 0 client/{ => src}/components/Products.jsx | 0 client/{ => src}/components/Text.jsx | 0 client/{ => src}/components/ui/ChatContainer.jsx | 0 client/{ => src}/components/ui/InputForm.jsx | 0 client/{ => src}/components/ui/MessageBox.jsx | 0 client/{ => src}/components/ui/ProductResults.jsx | 0 client/{ => src}/components/ui/SearchForm.jsx | 0 client/{ => src}/utils/nlip.js | 0 10 files changed, 2 insertions(+), 2 deletions(-) rename client/{ => src}/components/Image.jsx (100%) rename client/{ => src}/components/Products.jsx (100%) rename client/{ => src}/components/Text.jsx (100%) rename client/{ => src}/components/ui/ChatContainer.jsx (100%) rename client/{ => src}/components/ui/InputForm.jsx (100%) rename client/{ => src}/components/ui/MessageBox.jsx (100%) rename client/{ => src}/components/ui/ProductResults.jsx (100%) rename client/{ => src}/components/ui/SearchForm.jsx (100%) rename client/{ => src}/utils/nlip.js (100%) diff --git a/client/src/App.jsx b/client/src/App.jsx index 6676196..14b95c2 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,5 +1,5 @@ -import Text from "../components/Text" -import Products from "../components/Products" +import Text from "./components/Text" +import Products from "./components/Products" function App() { const mode = import.meta.env.VITE_APP_MODE; diff --git a/client/components/Image.jsx b/client/src/components/Image.jsx similarity index 100% rename from client/components/Image.jsx rename to client/src/components/Image.jsx diff --git a/client/components/Products.jsx b/client/src/components/Products.jsx similarity index 100% rename from client/components/Products.jsx rename to client/src/components/Products.jsx diff --git a/client/components/Text.jsx b/client/src/components/Text.jsx similarity index 100% rename from client/components/Text.jsx rename to client/src/components/Text.jsx diff --git a/client/components/ui/ChatContainer.jsx b/client/src/components/ui/ChatContainer.jsx similarity index 100% rename from client/components/ui/ChatContainer.jsx rename to client/src/components/ui/ChatContainer.jsx diff --git a/client/components/ui/InputForm.jsx b/client/src/components/ui/InputForm.jsx similarity index 100% rename from client/components/ui/InputForm.jsx rename to client/src/components/ui/InputForm.jsx diff --git a/client/components/ui/MessageBox.jsx b/client/src/components/ui/MessageBox.jsx similarity index 100% rename from client/components/ui/MessageBox.jsx rename to client/src/components/ui/MessageBox.jsx diff --git a/client/components/ui/ProductResults.jsx b/client/src/components/ui/ProductResults.jsx similarity index 100% rename from client/components/ui/ProductResults.jsx rename to client/src/components/ui/ProductResults.jsx diff --git a/client/components/ui/SearchForm.jsx b/client/src/components/ui/SearchForm.jsx similarity index 100% rename from client/components/ui/SearchForm.jsx rename to client/src/components/ui/SearchForm.jsx diff --git a/client/utils/nlip.js b/client/src/utils/nlip.js similarity index 100% rename from client/utils/nlip.js rename to client/src/utils/nlip.js From 9472eff8225a983ed6e0c3156fbf302c94f2c032 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Mon, 10 Nov 2025 12:48:02 -0500 Subject: [PATCH 14/72] handle json content --- client/src/components/ui/SearchForm.jsx | 10 +++------- client/src/utils/nlip.js | 25 ++++++++++++++++++++++++- nlip_web/vite_chat.py | 7 +++++-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/client/src/components/ui/SearchForm.jsx b/client/src/components/ui/SearchForm.jsx index 62b7805..b3bd520 100644 --- a/client/src/components/ui/SearchForm.jsx +++ b/client/src/components/ui/SearchForm.jsx @@ -11,14 +11,10 @@ export default function SearchForm({ setProducts, isLoading, setIsLoading}) { setIsLoading(true); try { - // const response = await nlipClient.sendMessage(query.trim()) + const response = await nlipClient.sendMessage(query.trim(), true) - const response = '{"Best Buy":[{"name":"Dell XPS 15","price":1299.99,"description":"15.6-inch FHD display, Intel Core i7, 16GB RAM, 512GB SSD"},{"name":"HP Pavilion 14","price":649.99,"description":"14-inch HD display, AMD Ryzen 5, 8GB RAM, 256GB SSD"}],"Amazon":[{"name":"Lenovo ThinkPad X1 Carbon","price":1549.99,"description":"14-inch 2K display, Intel Core i7, 16GB RAM, 1TB SSD, Business laptop"},{"name":"ASUS VivoBook 15","price":529.99,"description":"15.6-inch FHD display, Intel Core i5, 12GB RAM, 512GB SSD"}],"Walmart":[{"name":"Acer Aspire 5","price":479.99,"description":"15.6-inch FHD display, AMD Ryzen 3, 8GB RAM, 256GB SSD"},{"name":"Microsoft Surface Laptop 5","price":1199.99,"description":"13.5-inch PixelSense touchscreen, Intel Core i5, 8GB RAM, 256GB SSD"}]}'; - - const data = JSON.parse(response); - - if (data) { - setProducts(data); + if (response) { + setProducts(response); } } catch (error) { console.error('Error fetching products:', error); diff --git a/client/src/utils/nlip.js b/client/src/utils/nlip.js index a95ecd9..282f8ad 100644 --- a/client/src/utils/nlip.js +++ b/client/src/utils/nlip.js @@ -198,6 +198,25 @@ class NLIPMessage { return this.extractToken(ReservedTokens.auth, label); } + extractJson(label = null) { + const jsonContent = this.extractField(AllowedFormats.structured, 'JSON', label); + if (jsonContent !== null) { + return jsonContent; + } + + // Check submessages + if (Array.isArray(this.submessages)) { + for (const submsg of this.submessages) { + const content = submsg.extractField(AllowedFormats.structured, 'JSON', label); + if (content !== null) { + return content; + } + } + } + + return null; + } + toJSON() { const obj = { messagetype: this.messagetype, @@ -356,7 +375,7 @@ class NLIPClient { this.correlator = null } - async sendMessage(text) { + async sendMessage(text, jsonResponse = false) { const message = NLIPFactory.createText(text); if (this.correlator != null) { message.addConversationToken(this.correlator) @@ -365,6 +384,10 @@ class NLIPClient { const nlipMessage = NLIPFactory.createMessageFromJSON(response) this.correlator = nlipMessage.extractToken(ReservedTokens.conv) + if (jsonResponse) { + const jsonData = nlipMessage.extractJson(); + return jsonData; + } const extractedText = nlipMessage.extractText() return extractedText } diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py index b6ea3ca..8c79e8d 100644 --- a/nlip_web/vite_chat.py +++ b/nlip_web/vite_chat.py @@ -3,6 +3,7 @@ from nlip_web.env import read_digits, read_string from nlip_server import server from nlip_sdk import nlip +import json class ChatApplication(nlip_ext.SafeStatefulApplication): @@ -40,9 +41,11 @@ def execute( return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") # print(f'Received text {text[0:10]}...') - response = chat_server.chat_multimodal(text, images = images) + # response = chat_server.chat_multimodal(text, images = images) # print(f'Received response {response[0:10]}...') - return nlip.NLIP_Factory.create_text(response) + response_data = '{"Best Buy":[{"name":"Dell XPS 15","price":1299.99,"description":"15.6-inch FHD display, Intel Core i7, 16GB RAM, 512GB SSD"},{"name":"HP Pavilion 14","price":649.99,"description":"14-inch HD display, AMD Ryzen 5, 8GB RAM, 256GB SSD"}],"Amazon":[{"name":"Lenovo ThinkPad X1 Carbon","price":1549.99,"description":"14-inch 2K display, Intel Core i7, 16GB RAM, 1TB SSD, Business laptop"},{"name":"ASUS VivoBook 15","price":529.99,"description":"15.6-inch FHD display, Intel Core i5, 12GB RAM, 512GB SSD"}],"Walmart":[{"name":"Acer Aspire 5","price":479.99,"description":"15.6-inch FHD display, AMD Ryzen 3, 8GB RAM, 256GB SSD"},{"name":"Microsoft Surface Laptop 5","price":1199.99,"description":"13.5-inch PixelSense touchscreen, Intel Core i5, 8GB RAM, 256GB SSD"}]}' + response_dict = json.loads(response_data) + return nlip.NLIP_Factory.create_json(response_dict) From 35a45b785ba4704d59c2803d2b4dc08a783aa3df Mon Sep 17 00:00:00 2001 From: Maximus Date: Thu, 13 Nov 2025 00:27:41 -0500 Subject: [PATCH 15/72] python script being called in chat session --- client/components/ui/SearchForm.jsx | 7 +++-- client/utils/module_test.js | 22 ++++++++++++++++ client/utils/nlip.js | 6 +++++ nlip_web/vite_chat.py | 19 +++++++++++--- website_modules/WEB_MODULES.md | 21 +++++++++------ website_modules/dbm_output.txt | 1 + website_modules/deluce_bookstore_module.py | 30 ++++++++++++++++------ 7 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 client/utils/module_test.js create mode 100644 website_modules/dbm_output.txt diff --git a/client/components/ui/SearchForm.jsx b/client/components/ui/SearchForm.jsx index 62b7805..96a3e98 100644 --- a/client/components/ui/SearchForm.jsx +++ b/client/components/ui/SearchForm.jsx @@ -1,5 +1,7 @@ import { useState } from "react"; import { NLIPClient } from "../../utils/nlip"; +//import {runModule} from "../../utils/module_test" + export default function SearchForm({ setProducts, isLoading, setIsLoading}) { const [query, setQuery] = useState(''); @@ -11,9 +13,10 @@ export default function SearchForm({ setProducts, isLoading, setIsLoading}) { setIsLoading(true); try { - // const response = await nlipClient.sendMessage(query.trim()) + const response = await nlipClient.sendMessage(query.trim()) + //const response = '{"Best Buy":[{"name":"Dell XPS 15","price":1299.99,"description":"15.6-inch FHD display, Intel Core i7, 16GB RAM, 512GB SSD"},{"name":"HP Pavilion 14","price":649.99,"description":"14-inch HD display, AMD Ryzen 5, 8GB RAM, 256GB SSD"}],"Amazon":[{"name":"Lenovo ThinkPad X1 Carbon","price":1549.99,"description":"14-inch 2K display, Intel Core i7, 16GB RAM, 1TB SSD, Business laptop"},{"name":"ASUS VivoBook 15","price":529.99,"description":"15.6-inch FHD display, Intel Core i5, 12GB RAM, 512GB SSD"}],"Walmart":[{"name":"Acer Aspire 5","price":479.99,"description":"15.6-inch FHD display, AMD Ryzen 3, 8GB RAM, 256GB SSD"},{"name":"Microsoft Surface Laptop 5","price":1199.99,"description":"13.5-inch PixelSense touchscreen, Intel Core i5, 8GB RAM, 256GB SSD"}]}'; - const response = '{"Best Buy":[{"name":"Dell XPS 15","price":1299.99,"description":"15.6-inch FHD display, Intel Core i7, 16GB RAM, 512GB SSD"},{"name":"HP Pavilion 14","price":649.99,"description":"14-inch HD display, AMD Ryzen 5, 8GB RAM, 256GB SSD"}],"Amazon":[{"name":"Lenovo ThinkPad X1 Carbon","price":1549.99,"description":"14-inch 2K display, Intel Core i7, 16GB RAM, 1TB SSD, Business laptop"},{"name":"ASUS VivoBook 15","price":529.99,"description":"15.6-inch FHD display, Intel Core i5, 12GB RAM, 512GB SSD"}],"Walmart":[{"name":"Acer Aspire 5","price":479.99,"description":"15.6-inch FHD display, AMD Ryzen 3, 8GB RAM, 256GB SSD"},{"name":"Microsoft Surface Laptop 5","price":1199.99,"description":"13.5-inch PixelSense touchscreen, Intel Core i5, 8GB RAM, 256GB SSD"}]}'; + console.log(response) const data = JSON.parse(response); diff --git a/client/utils/module_test.js b/client/utils/module_test.js new file mode 100644 index 0000000..c524fb4 --- /dev/null +++ b/client/utils/module_test.js @@ -0,0 +1,22 @@ +//const { spawn } = require('node:child_process'); +/* +import { spawn } from 'node:child_process' + +export async function runModule(search_term) { + const path = "../../website_modules/deluce_bookstore_module.py" + + const program = spawn('python', [path, search_term]) + + let data = '' + + program.stdout.on('data', (stdout) => { + data += stdout.toString() + }) + + // When script is finished, print collected data + program.on('close', (code) => { + console.log(`child process exited with code ${code}`); + console.log(data); + }); +} +*/ \ No newline at end of file diff --git a/client/utils/nlip.js b/client/utils/nlip.js index a95ecd9..861fe8c 100644 --- a/client/utils/nlip.js +++ b/client/utils/nlip.js @@ -401,6 +401,12 @@ class NLIPClient { return response.json(); } + async fetchTestData() { + const response = await fetch(`${this.baseUrl}/nlip/`); + const json = await response.json(); + console.log(json); + } + // Utility methods async fileToBase64(file) { return new Promise((resolve, reject) => { diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py index 2f6a0e9..a8728a9 100644 --- a/nlip_web/vite_chat.py +++ b/nlip_web/vite_chat.py @@ -1,6 +1,14 @@ from nlip_web.genai import StatefulGenAI from nlip_web import nlip_ext as nlip_ext from nlip_web.env import read_digits, read_string +#from nlip_server import server +#from nlip_sdk import nlip + +# resolve python module paths +import sys +sys.path.insert(0, "../../nlip_server/nlip_server/") +sys.path.insert(0, "../../nlip_sdk/nlip_sdk/") + from nlip_server import server from nlip_sdk import nlip @@ -39,13 +47,16 @@ def execute( if chat_server is None: return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") - #results = dbm.search_product(text) - #print(results) + # seems to function + results = dbm.search_product(text) + print(results) + results = nlip.NLIP_Factory.create_json(results) + return results # print(f'Received text {text[0:10]}...') - response = chat_server.chat_multimodal(text, images = images) + # response = chat_server.chat_multimodal(text, images = images) # print(f'Received response {response[0:10]}...') - return nlip.NLIP_Factory.create_text(response) + # return nlip.NLIP_Factory.create_text(response) diff --git a/website_modules/WEB_MODULES.md b/website_modules/WEB_MODULES.md index 0af85dc..44b8414 100644 --- a/website_modules/WEB_MODULES.md +++ b/website_modules/WEB_MODULES.md @@ -26,13 +26,18 @@ Module files are commonly used in a similar fashion to a library, although some ```json { - "item_name": "string", - "price": "string", - "sizes": [ - "string" - ], - "availability": "string", - "product_photo": "string", - "link": "string" + : [ + { + "item_name": "string", + "price": "string", + "sizes": [ + "string" + ], + "availability": "string", + "product_photo": "string", + "link": "string" + } + ] } + ``` \ No newline at end of file diff --git a/website_modules/dbm_output.txt b/website_modules/dbm_output.txt new file mode 100644 index 0000000..f929540 --- /dev/null +++ b/website_modules/dbm_output.txt @@ -0,0 +1 @@ +{"deulce_bookstore": [{"name": "Whistle With Lanyard", "description": "N/A", "price": "$2.69 $2.99", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/85288201103.png?itok=oAcbx9hQ", "link": "https://bookstore.uwo.ca/product/085288201103"}, {"name": "Bundle Water Bottle & Lanyard", "description": "N/A", "price": "$9.99", "sizes": ["N/A"], "availability": "Out of stock", "product_photo": "/sites/all/themes/bookstore/images/no-photo.png", "link": "https://bookstore.uwo.ca/product/88000093891"}, {"name": "Purple Sublimated Western Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2028web.jpg?itok=-2ii5Obw", "link": "https://bookstore.uwo.ca/product/2028"}, {"name": "White Lanyard Western Est 1878", "description": "N/A", "price": "$3.75", "sizes": ["N/A"], "availability": "Out of stock", "product_photo": "/sites/all/themes/bookstore/images/no-photo.png", "link": "https://bookstore.uwo.ca/product/2176"}, {"name": "Western/Univ.Canada/Crest Lanyard", "description": "N/A", "price": "$5.99", "sizes": ["N/A"], "availability": "Product not available", "product_photo": "/sites/all/themes/bookstore/images/no-photo.png", "link": "https://bookstore.uwo.ca/product/689856900018"}, {"name": "Inkerie Chargecarry Phone Lanyard Pk", "description": "N/A", "price": "$21.99", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/web_image_charge_carry.png?itok=V-geVMGH", "link": "https://bookstore.uwo.ca/product/612615128491"}, {"name": "White Western Sublimated Lanyard ", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2148web.jpg?itok=xKNfG8Ka", "link": "https://bookstore.uwo.ca/product/2148"}, {"name": "Purple Western Mustangs Sublimated Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2152web.jpg?itok=Len9SURp", "link": "https://bookstore.uwo.ca/product/2152"}, {"name": "White Western Mustangs Sublimated Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2149web.jpg?itok=GMI74wR2", "link": "https://bookstore.uwo.ca/product/2149"}, {"name": "Purple Western University Sublimated Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2151web.jpg?itok=kxUa6v_z", "link": "https://bookstore.uwo.ca/product/2151"}, {"name": "Black Sublimated Western Mustangs Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2150web.jpg?itok=K10s4h11", "link": "https://bookstore.uwo.ca/product/2150"}, {"name": "1\" Black Sublimated Lanyard Western Crest", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "/sites/all/themes/bookstore/images/no-photo.png", "link": "https://bookstore.uwo.ca/product/2325"}, {"name": "Inkerie Charge Carry Phone Lanyard Blue", "description": "N/A", "price": "$21.99", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/web_image_blue_dabadee_.png?itok=bO40POs7", "link": "https://bookstore.uwo.ca/product/612615128507"}, {"name": "Purple Western Lanyard with Crest", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2029web.jpg?itok=hd6-KKMZ", "link": "https://bookstore.uwo.ca/product/2029"}, {"name": "Student Welcome Kit", "description": "N/A", "price": "$25.00", "sizes": ["N/A"], "availability": "Pre-order", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/dsc03669.jpg?itok=tpG0JdXi", "link": "https://bookstore.uwo.ca/product/88000117866"}]} diff --git a/website_modules/deluce_bookstore_module.py b/website_modules/deluce_bookstore_module.py index aeed70d..088302e 100644 --- a/website_modules/deluce_bookstore_module.py +++ b/website_modules/deluce_bookstore_module.py @@ -6,9 +6,12 @@ from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from selenium.webdriver.support import expected_conditions as EC +import sys import time +import json ## CONSTANT VARIABLES +STORE_NAME = "deulce_bookstore" WEB_ADDRESS = 'https://bookstore.uwo.ca/' SEARCH_ARG = 'search/products?search=' PAGE_NUM = "&page=" # no page number argument for fist page, second page starts at &page=1 @@ -32,7 +35,8 @@ def search_product(query_term) -> list: options.add_argument(f"user-agent={HTTP_HEADERS['User-Agent']}") driver = webdriver.Chrome(options=options) - product_list = [] + product_list = {} + product_list[STORE_NAME] = [] try: driver.get(WEB_ADDRESS + SEARCH_ARG + query_term) @@ -44,7 +48,7 @@ def search_product(query_term) -> list: driver.quit() return - product_list.append(parsed_content) + product_list[STORE_NAME] = product_list[STORE_NAME] + parsed_content # first page non-empty, query remaining pages of content. end_of_content = False @@ -60,7 +64,7 @@ def search_product(query_term) -> list: break # increment page counter current_page += 1 - product_list.append(parsed_content) + product_list[STORE_NAME] = product_list[STORE_NAME] + parsed_content finally: driver.quit() @@ -102,13 +106,17 @@ def parse_site_content(html_content) -> list: if len(end_of_results) != 0: return [] - product_information = [] products = soup.find_all("div", class_="views-row") + + # create the dictionary to return list of product information + product_information = [] for prod in products: temp_prod = {} # extract it-em name - temp_prod["item_name"] = prod.find('a').text + temp_prod["name"] = prod.find('a').text + + temp_prod["description"] = 'N/A' # commerce-product-field is where price information is stored price_field = prod.find(class_="commerce-product-field") @@ -167,9 +175,15 @@ def get_config() -> dict: # main function for use if run as script if __name__ == "__main__": - print("Input serch term: ") - search_term = input() + search_term = "" + if len(sys.argv) > 1: + search_term = sys.argv[1] + else: + print("Input serch term: ") + search_term = input() return_vals = search_product(search_term) - print(return_vals) + + json = json.dumps(return_vals) + print(json) \ No newline at end of file From f2947017eb523397ebcb483b00d3df6872587225 Mon Sep 17 00:00:00 2001 From: Maximus Date: Thu, 13 Nov 2025 00:57:54 -0500 Subject: [PATCH 16/72] chat client uses website_modules to search products. Updated product list to display product image if able. Updated product list with clickable link to item page --- client/src/components/ui/ProductResults.jsx | 9 +++++++++ nlip_web/vite_chat.py | 19 +++++-------------- website_modules/WEB_MODULES.md | 2 +- website_modules/dbm_output.txt | 1 - website_modules/deluce_bookstore_module.py | 20 ++++++++++++-------- 5 files changed, 27 insertions(+), 24 deletions(-) delete mode 100644 website_modules/dbm_output.txt diff --git a/client/src/components/ui/ProductResults.jsx b/client/src/components/ui/ProductResults.jsx index 605a16c..22dddf8 100644 --- a/client/src/components/ui/ProductResults.jsx +++ b/client/src/components/ui/ProductResults.jsx @@ -9,11 +9,20 @@ export default function ProductResults({products, isLoading}) { diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py index e0d1c50..964ed21 100644 --- a/nlip_web/vite_chat.py +++ b/nlip_web/vite_chat.py @@ -1,17 +1,10 @@ from nlip_web.genai import StatefulGenAI from nlip_web import nlip_ext as nlip_ext from nlip_web.env import read_digits, read_string -#from nlip_server import server -#from nlip_sdk import nlip - -# resolve python module paths -import sys -sys.path.insert(0, "../../nlip_server/nlip_server/") -sys.path.insert(0, "../../nlip_sdk/nlip_sdk/") - from nlip_server import server from nlip_sdk import nlip +# import website module for deluce bookstore from website_modules import deluce_bookstore_module as dbm import json @@ -48,17 +41,15 @@ def execute( if chat_server is None: return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") - # seems to function + # pass search term to website_modules results = dbm.search_product(text) - print(results) - results = nlip.NLIP_Factory.create_json(results) - return results + # print(results) # print(f'Received text {text[0:10]}...') # response = chat_server.chat_multimodal(text, images = images) # print(f'Received response {response[0:10]}...') - response_data = '{"Best Buy":[{"name":"Dell XPS 15","price":1299.99,"description":"15.6-inch FHD display, Intel Core i7, 16GB RAM, 512GB SSD"},{"name":"HP Pavilion 14","price":649.99,"description":"14-inch HD display, AMD Ryzen 5, 8GB RAM, 256GB SSD"}],"Amazon":[{"name":"Lenovo ThinkPad X1 Carbon","price":1549.99,"description":"14-inch 2K display, Intel Core i7, 16GB RAM, 1TB SSD, Business laptop"},{"name":"ASUS VivoBook 15","price":529.99,"description":"15.6-inch FHD display, Intel Core i5, 12GB RAM, 512GB SSD"}],"Walmart":[{"name":"Acer Aspire 5","price":479.99,"description":"15.6-inch FHD display, AMD Ryzen 3, 8GB RAM, 256GB SSD"},{"name":"Microsoft Surface Laptop 5","price":1199.99,"description":"13.5-inch PixelSense touchscreen, Intel Core i5, 8GB RAM, 256GB SSD"}]}' - response_dict = json.loads(response_data) + ## response_data = '{"Best Buy":[{"name":"Dell XPS 15","price":1299.99,"description":"15.6-inch FHD display, Intel Core i7, 16GB RAM, 512GB SSD"},{"name":"HP Pavilion 14","price":649.99,"description":"14-inch HD display, AMD Ryzen 5, 8GB RAM, 256GB SSD"}],"Amazon":[{"name":"Lenovo ThinkPad X1 Carbon","price":1549.99,"description":"14-inch 2K display, Intel Core i7, 16GB RAM, 1TB SSD, Business laptop"},{"name":"ASUS VivoBook 15","price":529.99,"description":"15.6-inch FHD display, Intel Core i5, 12GB RAM, 512GB SSD"}],"Walmart":[{"name":"Acer Aspire 5","price":479.99,"description":"15.6-inch FHD display, AMD Ryzen 3, 8GB RAM, 256GB SSD"},{"name":"Microsoft Surface Laptop 5","price":1199.99,"description":"13.5-inch PixelSense touchscreen, Intel Core i5, 8GB RAM, 256GB SSD"}]}' + response_dict = json.loads(results) return nlip.NLIP_Factory.create_json(response_dict) diff --git a/website_modules/WEB_MODULES.md b/website_modules/WEB_MODULES.md index 44b8414..9297357 100644 --- a/website_modules/WEB_MODULES.md +++ b/website_modules/WEB_MODULES.md @@ -28,7 +28,7 @@ Module files are commonly used in a similar fashion to a library, although some { : [ { - "item_name": "string", + "name": "string", "price": "string", "sizes": [ "string" diff --git a/website_modules/dbm_output.txt b/website_modules/dbm_output.txt deleted file mode 100644 index f929540..0000000 --- a/website_modules/dbm_output.txt +++ /dev/null @@ -1 +0,0 @@ -{"deulce_bookstore": [{"name": "Whistle With Lanyard", "description": "N/A", "price": "$2.69 $2.99", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/85288201103.png?itok=oAcbx9hQ", "link": "https://bookstore.uwo.ca/product/085288201103"}, {"name": "Bundle Water Bottle & Lanyard", "description": "N/A", "price": "$9.99", "sizes": ["N/A"], "availability": "Out of stock", "product_photo": "/sites/all/themes/bookstore/images/no-photo.png", "link": "https://bookstore.uwo.ca/product/88000093891"}, {"name": "Purple Sublimated Western Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2028web.jpg?itok=-2ii5Obw", "link": "https://bookstore.uwo.ca/product/2028"}, {"name": "White Lanyard Western Est 1878", "description": "N/A", "price": "$3.75", "sizes": ["N/A"], "availability": "Out of stock", "product_photo": "/sites/all/themes/bookstore/images/no-photo.png", "link": "https://bookstore.uwo.ca/product/2176"}, {"name": "Western/Univ.Canada/Crest Lanyard", "description": "N/A", "price": "$5.99", "sizes": ["N/A"], "availability": "Product not available", "product_photo": "/sites/all/themes/bookstore/images/no-photo.png", "link": "https://bookstore.uwo.ca/product/689856900018"}, {"name": "Inkerie Chargecarry Phone Lanyard Pk", "description": "N/A", "price": "$21.99", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/web_image_charge_carry.png?itok=V-geVMGH", "link": "https://bookstore.uwo.ca/product/612615128491"}, {"name": "White Western Sublimated Lanyard ", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2148web.jpg?itok=xKNfG8Ka", "link": "https://bookstore.uwo.ca/product/2148"}, {"name": "Purple Western Mustangs Sublimated Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2152web.jpg?itok=Len9SURp", "link": "https://bookstore.uwo.ca/product/2152"}, {"name": "White Western Mustangs Sublimated Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2149web.jpg?itok=GMI74wR2", "link": "https://bookstore.uwo.ca/product/2149"}, {"name": "Purple Western University Sublimated Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2151web.jpg?itok=kxUa6v_z", "link": "https://bookstore.uwo.ca/product/2151"}, {"name": "Black Sublimated Western Mustangs Lanyard", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2150web.jpg?itok=K10s4h11", "link": "https://bookstore.uwo.ca/product/2150"}, {"name": "1\" Black Sublimated Lanyard Western Crest", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "/sites/all/themes/bookstore/images/no-photo.png", "link": "https://bookstore.uwo.ca/product/2325"}, {"name": "Inkerie Charge Carry Phone Lanyard Blue", "description": "N/A", "price": "$21.99", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/web_image_blue_dabadee_.png?itok=bO40POs7", "link": "https://bookstore.uwo.ca/product/612615128507"}, {"name": "Purple Western Lanyard with Crest", "description": "N/A", "price": "$5.95", "sizes": ["N/A"], "availability": "Add to cart", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/2029web.jpg?itok=hd6-KKMZ", "link": "https://bookstore.uwo.ca/product/2029"}, {"name": "Student Welcome Kit", "description": "N/A", "price": "$25.00", "sizes": ["N/A"], "availability": "Pre-order", "product_photo": "https://bookstore.uwo.ca/files/styles/product_full_thumbnail_lg/public/product-images/dsc03669.jpg?itok=tpG0JdXi", "link": "https://bookstore.uwo.ca/product/88000117866"}]} diff --git a/website_modules/deluce_bookstore_module.py b/website_modules/deluce_bookstore_module.py index 088302e..b552a40 100644 --- a/website_modules/deluce_bookstore_module.py +++ b/website_modules/deluce_bookstore_module.py @@ -29,7 +29,7 @@ # takes a serch term or formatted query to search the site # returns list of product objects -def search_product(query_term) -> list: +def search_product(query_term): options = Options() options.add_argument("--headless=new") options.add_argument(f"user-agent={HTTP_HEADERS['User-Agent']}") @@ -68,7 +68,7 @@ def search_product(query_term) -> list: finally: driver.quit() - return product_list + return json.dumps(product_list) # return search results from a specific page def search_product_specific_page(query_term, pagenum) -> list: @@ -93,7 +93,7 @@ def search_product_specific_page(query_term, pagenum) -> list: finally: driver.quit() - return product_list + return json.dumps(product_list) # parses out poduct information from html return # returns list of dicts/json formatted objects @@ -142,7 +142,14 @@ def parse_site_content(html_content) -> list: if availability == "Add to cart": availability = "in stock" - temp_prod["product_photo"] = prod.find('img')['src'] + img_src = prod.find('img')['src'] + + # if image is the placeholder for no photo, make it "" + # nlip_web_client will filter this as NO IMAGE + if img_src == "/sites/all/themes/bookstore/images/no-photo.png": + img_src = "" + + temp_prod["product_photo"] = img_src # add link to product page using link to item id stored in anchor tag temp_prod["link"] = (WEB_ADDRESS[:-1] + prod.find('a')['href']) @@ -182,8 +189,5 @@ def get_config() -> dict: print("Input serch term: ") search_term = input() - return_vals = search_product(search_term) - - json = json.dumps(return_vals) - print(json) + print(search_product(search_term)) \ No newline at end of file From 842502b830d6150b3236e7e27f9a6514fabada61 Mon Sep 17 00:00:00 2001 From: James Song Date: Thu, 13 Nov 2025 10:01:25 -0500 Subject: [PATCH 17/72] Create staples_ca_module.py - Dumped code for the Staples scraper (still needs to be integrated with NLIP) --- website_modules/staples_ca_module.py | 80 ++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 website_modules/staples_ca_module.py diff --git a/website_modules/staples_ca_module.py b/website_modules/staples_ca_module.py new file mode 100644 index 0000000..680e39b --- /dev/null +++ b/website_modules/staples_ca_module.py @@ -0,0 +1,80 @@ +import time +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.chrome.options import Options +from bs4 import BeautifulSoup + +def scrape_staples_selenium(product_name: str): + # --- Chrome options --- + options = Options() + options.add_argument("--start-maximized") + options.add_argument("--disable-blink-features=AutomationControlled") + options.add_argument( + "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + ) + # run headless + options.add_argument("--headless=new") + + driver = webdriver.Chrome(options=options) + search_url = f"https://www.staples.ca/search?query={product_name}" + driver.get(search_url) + + # wait for the page to load and scripts to render + time.sleep(5) + + # scroll to mimic human behavior (helps bypass blocks) + driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") + time.sleep(2) + + # get the rendered HTML + html = driver.page_source + driver.quit() + + # --- Parse with BeautifulSoup --- + soup = BeautifulSoup(html, "html.parser") + + results = [] + for a_tag in soup.select("a.product-link"): + text = a_tag.get_text(separator=" ", strip=True) + + # Clean up the title + if "Quick View" in text: + text = text.replace("Quick View", "").strip() + + link = f"https://www.staples.ca{a_tag.get('href', '')}" + + # Try to find the price near this link (look upward or in the same container) + card = a_tag.find_parent("div", class_="product-thumbnail") + price_el = card.select_one("span.money.pre-money") if card else None + price = price_el.get_text(strip=True) if price_el else "N/A" + + if text: + results.append({ + "title": text, + "price": price, + "link": link + }) + + return results + + +if __name__ == "__main__": + searching = 'y' + while searching == 'y': + product_desc = input("Enter the name of the product: ") + products = scrape_staples_selenium("laptop") + for i, p in enumerate(products, 1): + print("Name: " + p['title']) + print("Price: " + p['price']) + print("Link: " + p['link']) + print() + + user_input = input("Perform another search? (y/n): ") + searching = user_input[0].lower() + while searching != 'y' and searching != 'n': + searching = input("Perform another search? (y/n): ") + + print("Thank you for shopping at Staples!") + # page%5D=2 From dedbfa0c415868804ca4dd1b038efd4e25ad1e8a Mon Sep 17 00:00:00 2001 From: James Song Date: Thu, 13 Nov 2025 10:36:01 -0500 Subject: [PATCH 18/72] Create newegg_ca_module.py - Uses the same scraper as my Walmart scraper and parses the HTML exactly like my Staples scraper --- website_modules/newegg_ca_module.py | 93 +++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 website_modules/newegg_ca_module.py diff --git a/website_modules/newegg_ca_module.py b/website_modules/newegg_ca_module.py new file mode 100644 index 0000000..2684490 --- /dev/null +++ b/website_modules/newegg_ca_module.py @@ -0,0 +1,93 @@ +# https://stackoverflow.com/questions/68992032/scrape-walmart-search-results-python helped me figure out how to make search request for Walmart +# Prints all the products found on walmart.ca with a specified product name on the first page +# Must install bs4 -> BeautifulSoup and requests to work +from bs4 import BeautifulSoup # For parsing HTML +import requests # For getting into Newegg and using its search function +import json # For formatting the data so it is easy to read/extract info +from urllib.parse import quote # Proper formatting of link + +headers = { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"} # Extracted through network tab + + +def get_url(product_name, page): + return 'https://www.newegg.ca/p/pl?d=' + quote(product_name) + + +def print_products(items): + products = [] + # Get only product blocks, format them + for item in items: + # Product Name + title_tag = item.select_one(".item-title") + title = title_tag.text.strip() if title_tag else None + link = title_tag.get("href") if title_tag else None + + # Price + dollars_tag = item.select_one(".price-current strong") + cents_tag = item.select_one(".price-current sup") + price = None + if dollars_tag: + dollars = dollars_tag.text.strip() + cents = cents_tag.text.strip() if cents_tag else "00" + price = f"{dollars}{cents}" + + # Image + img_tag = item.select_one("img") + image = img_tag.get("src") if img_tag else None + + # Shipping + shipping_tag = item.select_one(".price-ship") + shipping = shipping_tag.text.strip() if shipping_tag else None + + if title and price: # Only add if title found (skip ads) + products.append({ + "Name": title, + "Price": price, + "Link": link, + "Image": image, + "Shipping Info": shipping + }) + + # How many products that were on the first page + num_of_products = len(products) + if num_of_products > 0: + print("Number of products in stock: " + str(len(products))) + # Print out all products found on the first page + print("PRODUCTS IN STOCK:\n") + for p in products: + for attribute in p: + print(attribute + ": " + str(p[attribute])) + print() + + +def search_product(product_name): + try: + session = requests.Session() + session.headers.update(headers) + + url = get_url(product_name, 1) + + r = session.get(url) # Pretend to be browser, prevent Walmart from flagging us as a bot + soup = BeautifulSoup(r.text, 'html.parser') # No need to install lxml (html.parser), parse HTML + + # Same method of extracting info as Staples + print_products(soup.select('.item-cell')) + # If you want more pages, just ask and add next page to url and redo this process with that page (ex. &page2) + except Exception as e: + print(e) + + +# Search loop +if __name__ == "__main__": + searching = 'y' + while searching == 'y': + product_desc = input("Enter the name of the product: ") + search_product(product_desc) + + user_input = input("Perform another search? (y/n): ") + searching = user_input[0].lower() + while searching != 'y' and searching != 'n': + searching = input("Perform another search? (y/n): ") + + print("Thank you for shopping at Walmart!") From dab501843090bd6c50c3b91233763f8961e5e4a4 Mon Sep 17 00:00:00 2001 From: Maximus Date: Sun, 16 Nov 2025 15:51:47 -0500 Subject: [PATCH 19/72] Updated naming of Dellelce bookstore. Updated new_egg_ca module to fit output standard. Updated staples_ca module to fit output standard. Added main_module to website modules. main_modules searched all listed websites for products, compiles data into json output. --- client/src/components/ui/ProductResults.jsx | 4 +- client/src/utils/module_test.js | 2 +- nlip_web/vite_chat.py | 6 +- website_modules/WEB_MODULES.md | 8 ++- ...module.py => dellelce_bookstore_module.py} | 4 +- website_modules/main_module.py | 57 +++++++++++++++++++ website_modules/newegg_ca_module.py | 33 +++++++---- website_modules/staples_ca_module.py | 21 ++++--- 8 files changed, 110 insertions(+), 25 deletions(-) rename website_modules/{deluce_bookstore_module.py => dellelce_bookstore_module.py} (98%) create mode 100644 website_modules/main_module.py diff --git a/client/src/components/ui/ProductResults.jsx b/client/src/components/ui/ProductResults.jsx index 22dddf8..2b557aa 100644 --- a/client/src/components/ui/ProductResults.jsx +++ b/client/src/components/ui/ProductResults.jsx @@ -8,8 +8,8 @@ export default function ProductResults({products, isLoading}) {

{storeName}

diff --git a/client/src/utils/module_test.js b/client/src/utils/module_test.js index c524fb4..7d7e409 100644 --- a/client/src/utils/module_test.js +++ b/client/src/utils/module_test.js @@ -3,7 +3,7 @@ import { spawn } from 'node:child_process' export async function runModule(search_term) { - const path = "../../website_modules/deluce_bookstore_module.py" + const path = "../../website_modules/dellelce_bookstore_module.py" const program = spawn('python', [path, search_term]) diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py index 964ed21..efdf964 100644 --- a/nlip_web/vite_chat.py +++ b/nlip_web/vite_chat.py @@ -4,8 +4,9 @@ from nlip_server import server from nlip_sdk import nlip -# import website module for deluce bookstore -from website_modules import deluce_bookstore_module as dbm +# import website module for dellelce bookstore +from website_modules import dellelce_bookstore_module as dbm +# from website_modules import main_module as mm import json @@ -43,6 +44,7 @@ def execute( # pass search term to website_modules results = dbm.search_product(text) + # results = mm.search_product(text) # print(results) # print(f'Received text {text[0:10]}...') diff --git a/website_modules/WEB_MODULES.md b/website_modules/WEB_MODULES.md index 9297357..54aa28c 100644 --- a/website_modules/WEB_MODULES.md +++ b/website_modules/WEB_MODULES.md @@ -16,11 +16,17 @@ When used with nlip_web the following packages have to be added to poetry: - [NLIP Server](https://github.com/nlip-project/nlip_server), is required to use the libraries provided - [NLIP SDK](https://github.com/nlip-project/nlip_sdk), is required for development +## Module Structure + +The [main module](./main_module.py) can be used to query a search term through every site which is currently supported, and return a list of json objects representing the results from each site. + +Each module has a primary function: **search_product(search_term: str)**. The function will return a json object representing the search results for the *search term*. + ## Examples A [Hello World Program](./hello_world/) can be found in its respective folder. -Module files are commonly used in a similar fashion to a library, although some can be run as [scripts](./deluce_bookstore_module.py) to test the usage. +Module files are commonly used in a similar fashion to a library, although some can be run as [scripts](./dellelce_bookstore_module.py) to test the usage. ## Output diff --git a/website_modules/deluce_bookstore_module.py b/website_modules/dellelce_bookstore_module.py similarity index 98% rename from website_modules/deluce_bookstore_module.py rename to website_modules/dellelce_bookstore_module.py index b552a40..0189167 100644 --- a/website_modules/deluce_bookstore_module.py +++ b/website_modules/dellelce_bookstore_module.py @@ -1,4 +1,4 @@ -# module for UWO Deluce Bookstore +# module for UWO dellelce Bookstore # imports for library functions from bs4 import BeautifulSoup @@ -11,7 +11,7 @@ import json ## CONSTANT VARIABLES -STORE_NAME = "deulce_bookstore" +STORE_NAME = "dellelce_bookstore" WEB_ADDRESS = 'https://bookstore.uwo.ca/' SEARCH_ARG = 'search/products?search=' PAGE_NUM = "&page=" # no page number argument for fist page, second page starts at &page=1 diff --git a/website_modules/main_module.py b/website_modules/main_module.py new file mode 100644 index 0000000..c9a0d28 --- /dev/null +++ b/website_modules/main_module.py @@ -0,0 +1,57 @@ +# imports for library functions +from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.support import expected_conditions as EC +import sys +import time +import json +import importlib + +# Import search modules for websites +import newegg_ca_module +import dellelce_bookstore_module +import staples_ca_module +import walmart_ca_module + +# Modules with completed search_product(str) functions +search_modules = [ + dellelce_bookstore_module, + newegg_ca_module, + staples_ca_module + ] + +def search_product(search_term: str) -> list: + search_results = [] + for module in search_modules: + + if not hasattr(module, 'search_product'): + continue + + search_function = getattr(module, 'search_product') + + try: + product_results_json = search_function(search_term) + + # convert to dict + prods = json.loads(product_results_json) + search_results.append(prods) + + except Exception as e: + print(f'ERROR with {module}\n') + print(f'\tFunction: [search_product(str) -> json]') + print(f'\t{e}\n') + + return json.dumps(search_results) + +# for use of main module as script +if __name__ == "__main__": + search_term = "" + if len(sys.argv) > 1: + search_term = sys.argv[1] + else: + print("Input serch term: ") + search_term = input() + + print(search_product(search_term)) \ No newline at end of file diff --git a/website_modules/newegg_ca_module.py b/website_modules/newegg_ca_module.py index 2684490..56f985f 100644 --- a/website_modules/newegg_ca_module.py +++ b/website_modules/newegg_ca_module.py @@ -6,9 +6,16 @@ import json # For formatting the data so it is easy to read/extract info from urllib.parse import quote # Proper formatting of link -headers = { - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"} # Extracted through network tab +STORE_NAME = 'new_egg_ca' +WEB_ADDRESS = 'https://www.newegg.ca/' +SEARCH_ARG = 'p/pl?d=' +HTTP_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + #'Accept-Language': 'en-US,en;q=0.9', + #'Accept-Encoding': 'gzip, deflate, br', + #'Connection': 'keep-alive', +} def get_url(product_name, page): return 'https://www.newegg.ca/p/pl?d=' + quote(product_name) @@ -42,11 +49,11 @@ def print_products(items): if title and price: # Only add if title found (skip ads) products.append({ - "Name": title, - "Price": price, - "Link": link, - "Image": image, - "Shipping Info": shipping + "name": title, + "price": price, + "link": link, + "product_photo": image, + "shipping_info": shipping }) # How many products that were on the first page @@ -60,11 +67,15 @@ def print_products(items): print(attribute + ": " + str(p[attribute])) print() + return products + +def search_product(product_name: str): + product_list = {} + product_list[STORE_NAME] = [] -def search_product(product_name): try: session = requests.Session() - session.headers.update(headers) + session.headers.update(HTTP_HEADERS) url = get_url(product_name, 1) @@ -72,11 +83,13 @@ def search_product(product_name): soup = BeautifulSoup(r.text, 'html.parser') # No need to install lxml (html.parser), parse HTML # Same method of extracting info as Staples - print_products(soup.select('.item-cell')) + product_list[STORE_NAME] = print_products(soup.select('.item-cell')) # If you want more pages, just ask and add next page to url and redo this process with that page (ex. &page2) except Exception as e: print(e) + return json.dumps(product_list) + # Search loop if __name__ == "__main__": diff --git a/website_modules/staples_ca_module.py b/website_modules/staples_ca_module.py index 680e39b..40bc198 100644 --- a/website_modules/staples_ca_module.py +++ b/website_modules/staples_ca_module.py @@ -1,10 +1,16 @@ +import json import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from bs4 import BeautifulSoup -def scrape_staples_selenium(product_name: str): +STORE_NAME = 'staples_ca' +WEB_ADDRESS = 'https://www.staples.ca/' +SEARCH_ARG = 'search?query=' + +#def scrape_staples_selenium(product_name: str): +def search_product(product_name: str): # --- Chrome options --- options = Options() options.add_argument("--start-maximized") @@ -18,7 +24,7 @@ def scrape_staples_selenium(product_name: str): options.add_argument("--headless=new") driver = webdriver.Chrome(options=options) - search_url = f"https://www.staples.ca/search?query={product_name}" + search_url = f"{WEB_ADDRESS}{SEARCH_ARG}{product_name}" driver.get(search_url) # wait for the page to load and scripts to render @@ -34,8 +40,8 @@ def scrape_staples_selenium(product_name: str): # --- Parse with BeautifulSoup --- soup = BeautifulSoup(html, "html.parser") - - results = [] + results = {} + results[STORE_NAME] = [] for a_tag in soup.select("a.product-link"): text = a_tag.get_text(separator=" ", strip=True) @@ -51,20 +57,21 @@ def scrape_staples_selenium(product_name: str): price = price_el.get_text(strip=True) if price_el else "N/A" if text: - results.append({ + results[STORE_NAME].append({ "title": text, "price": price, + "product_image": "", "link": link }) - return results + return json.dumps(results) if __name__ == "__main__": searching = 'y' while searching == 'y': product_desc = input("Enter the name of the product: ") - products = scrape_staples_selenium("laptop") + products = search_product("laptop") for i, p in enumerate(products, 1): print("Name: " + p['title']) print("Price: " + p['price']) From ca0c6395e86ad1c212d821970170a6de79db60d9 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Sun, 16 Nov 2025 19:35:11 -0500 Subject: [PATCH 20/72] setup image --- client/package.json | 2 +- client/src/App.jsx | 5 ++-- client/src/components/Image.jsx | 19 ++++++++++++ nlip_web/vite_image.py | 51 +++++++++++++++++++++++++++++++++ nlip_web/vite_text.py | 51 +++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- scripts.py | 13 ++++++++- 7 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 nlip_web/vite_image.py create mode 100644 nlip_web/vite_text.py diff --git a/client/package.json b/client/package.json index 8823fb1..7dcea90 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "build": "vite build", "build:text": "VITE_APP_MODE=text vite build", "build:products": "VITE_APP_MODE=products vite build", - "build:images": "VITE_APP_MODE=images vite build", + "build:image": "VITE_APP_MODE=image vite build", "lint": "eslint .", "preview": "vite preview" }, diff --git a/client/src/App.jsx b/client/src/App.jsx index 14b95c2..637c0d7 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,18 +1,19 @@ import Text from "./components/Text" import Products from "./components/Products" +import Image from "./components/Image" function App() { const mode = import.meta.env.VITE_APP_MODE; if (mode === "text") return if (mode === "products") return - if (mode === "images") return + if (mode === "image") return return ( <> - + ) } diff --git a/client/src/components/Image.jsx b/client/src/components/Image.jsx index e69de29..f6dcdf9 100644 --- a/client/src/components/Image.jsx +++ b/client/src/components/Image.jsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import MessageBox from './ui/MessageBox'; +import InputForm from './ui/InputForm'; + +import ChatContainer from './ui/ChatContainer'; + +export default function Image() { + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + return ( +
+ + + + +
+ ); +} diff --git a/nlip_web/vite_image.py b/nlip_web/vite_image.py new file mode 100644 index 0000000..9d81e0d --- /dev/null +++ b/nlip_web/vite_image.py @@ -0,0 +1,51 @@ +from nlip_web.genai import StatefulGenAI +from nlip_web import nlip_ext as nlip_ext +from nlip_web.env import read_digits, read_string +from nlip_server import server +from nlip_sdk import nlip + + +class ChatApplication(nlip_ext.SafeStatefulApplication): + def __init__(self): + super().__init__() + self.local_port = read_digits("LOCAL_PORT",8030) + self.model = read_string("CHAT_MODEL", "llava") + self.host = read_string("CHAT_HOST", "localhost") + self.port = read_digits("CHAT_PORT", 11434) + + + def create_stateful_session(self) -> server.NLIP_Session: + genAI = StatefulGenAI(self.host, self.port,self.model) + session = ChatSession() + session.set_correlator() + self.store_session_data(session.get_correlator(), genAI) + return session + + + + +class ChatSession(nlip_ext.StatefulSession): + + def execute( + self, msg: nlip.NLIP_Message + ) -> nlip.NLIP_Message: + text = msg.extract_text() + images = list() + binary_contents = msg.extract_field_list("binary") + for base64_content in binary_contents: + images.append(base64_content) + + chat_server = self.nlip_app.retrieve_session_data(self.get_correlator()) + if chat_server is None: + return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") + + response = chat_server.chat_multimodal(text, images = images) + print(f'Received response {response[0:10]}...') + return nlip.NLIP_Factory.create_text(response) + + + +if __name__ == "__main__": + chatapp = ChatApplication() + webapp = nlip_ext.WebApplication(indexFile="client/dist/index.html", static_dir="client/dist") + app = webapp.setup_webserver(chatapp, port=chatapp.local_port) \ No newline at end of file diff --git a/nlip_web/vite_text.py b/nlip_web/vite_text.py new file mode 100644 index 0000000..9d81e0d --- /dev/null +++ b/nlip_web/vite_text.py @@ -0,0 +1,51 @@ +from nlip_web.genai import StatefulGenAI +from nlip_web import nlip_ext as nlip_ext +from nlip_web.env import read_digits, read_string +from nlip_server import server +from nlip_sdk import nlip + + +class ChatApplication(nlip_ext.SafeStatefulApplication): + def __init__(self): + super().__init__() + self.local_port = read_digits("LOCAL_PORT",8030) + self.model = read_string("CHAT_MODEL", "llava") + self.host = read_string("CHAT_HOST", "localhost") + self.port = read_digits("CHAT_PORT", 11434) + + + def create_stateful_session(self) -> server.NLIP_Session: + genAI = StatefulGenAI(self.host, self.port,self.model) + session = ChatSession() + session.set_correlator() + self.store_session_data(session.get_correlator(), genAI) + return session + + + + +class ChatSession(nlip_ext.StatefulSession): + + def execute( + self, msg: nlip.NLIP_Message + ) -> nlip.NLIP_Message: + text = msg.extract_text() + images = list() + binary_contents = msg.extract_field_list("binary") + for base64_content in binary_contents: + images.append(base64_content) + + chat_server = self.nlip_app.retrieve_session_data(self.get_correlator()) + if chat_server is None: + return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") + + response = chat_server.chat_multimodal(text, images = images) + print(f'Received response {response[0:10]}...') + return nlip.NLIP_Factory.create_text(response) + + + +if __name__ == "__main__": + chatapp = ChatApplication() + webapp = nlip_ext.WebApplication(indexFile="client/dist/index.html", static_dir="client/dist") + app = webapp.setup_webserver(chatapp, port=chatapp.local_port) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 00a42a4..748b341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,5 +19,5 @@ chat = "scripts:start_chat" image = "scripts:start_image" vite-text = "scripts:start_vite_text" vite-products = "scripts:start_vite_products" - +vite-image = "scripts:start_vite_image" diff --git a/scripts.py b/scripts.py index 33d0e12..1af6964 100644 --- a/scripts.py +++ b/scripts.py @@ -42,7 +42,7 @@ def start_vite_text(): # Build in client directory subprocess.run(["npm", "run", "build:text"], env=my_env, check=True, cwd="client") # Run Python script from parent directory - subprocess.run(["poetry", "run", "python", "nlip_web/vite_chat.py"], + subprocess.run(["poetry", "run", "python", "nlip_web/vite_text.py"], env=my_env, check=True) except subprocess.CalledProcessError as e: print(f"Error executing command: {e}") @@ -55,5 +55,16 @@ def start_vite_products(): # Run Python script from parent directory subprocess.run(["poetry", "run", "python", "nlip_web/vite_chat.py"], env=my_env, check=True) + except subprocess.CalledProcessError as e: + print(f"Error executing command: {e}") + +def start_vite_image(): + my_env = get_env(local_port=8050,chat_model="llava") + try: + # Build in client directory + subprocess.run(["npm", "run", "build:image"], env=my_env, check=True, cwd="client") + # Run Python script from parent directory + subprocess.run(["poetry", "run", "python", "nlip_web/vite_image.py"], + env=my_env, check=True) except subprocess.CalledProcessError as e: print(f"Error executing command: {e}") \ No newline at end of file From c8c24870d58f25c7185f94b83e1c82de3cc66eeb Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Sun, 16 Nov 2025 20:03:16 -0500 Subject: [PATCH 21/72] works with image --- client/src/App.jsx | 2 +- client/src/components/Image.jsx | 2 +- client/src/components/ui/InputForm.jsx | 46 ++++++++++++++++++++++++-- nlip_web/vite_image.py | 1 - 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 637c0d7..13fecd9 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -3,7 +3,7 @@ import Products from "./components/Products" import Image from "./components/Image" function App() { - const mode = import.meta.env.VITE_APP_MODE; + const mode = import.meta.env.VITE_APP_MODE || 'image'; if (mode === "text") return if (mode === "products") return diff --git a/client/src/components/Image.jsx b/client/src/components/Image.jsx index f6dcdf9..4063761 100644 --- a/client/src/components/Image.jsx +++ b/client/src/components/Image.jsx @@ -12,7 +12,7 @@ export default function Image() {
- +
); diff --git a/client/src/components/ui/InputForm.jsx b/client/src/components/ui/InputForm.jsx index 3b335be..07c3fa5 100644 --- a/client/src/components/ui/InputForm.jsx +++ b/client/src/components/ui/InputForm.jsx @@ -2,10 +2,18 @@ import { useState } from 'react'; import { NLIPClient } from '../../utils/nlip'; -export default function InputForm({ setMessages, isLoading, setIsLoading }) { +export default function InputForm({ setMessages, isLoading, setIsLoading, allowFileUpload = false }) { const [input, setInput] = useState(''); + const [selectedFile, setSelectedFile] = useState(null); const nlipClient = new NLIPClient(); + const handleFileChange = (e) => { + const file = e.target.files?.[0]; + if (file) { + setSelectedFile(file); + } + }; + const sendMessage = async (e) => { e.preventDefault(); if (!input.trim()) return; @@ -14,11 +22,21 @@ export default function InputForm({ setMessages, isLoading, setIsLoading }) { setInput(''); // Add user message to chat - setMessages(prev => [...prev, { role: 'user', content: userMessage }]); + const displayMessage = selectedFile + ? `${userMessage} [File: ${selectedFile.name}]` + : userMessage; + setMessages(prev => [...prev, { role: 'user', content: displayMessage }]); setIsLoading(true); try { - const data = await nlipClient.sendMessage(userMessage); + let data; + if (allowFileUpload && selectedFile) { + data = await nlipClient.sendWithImage(userMessage, selectedFile); + data = data.content; + setSelectedFile(null); + } else { + data = await nlipClient.sendMessage(userMessage); + } const botMessage = data || 'No response'; // Add bot response to chat @@ -36,6 +54,28 @@ export default function InputForm({ setMessages, isLoading, setIsLoading }) { return (
+ {allowFileUpload && ( + <> + + + + )} Date: Sun, 16 Nov 2025 20:05:59 -0500 Subject: [PATCH 22/72] display image --- client/src/components/ui/InputForm.jsx | 13 ++++++++----- client/src/components/ui/MessageBox.jsx | 23 +++++++++++++++++------ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/client/src/components/ui/InputForm.jsx b/client/src/components/ui/InputForm.jsx index 07c3fa5..fa2d5f1 100644 --- a/client/src/components/ui/InputForm.jsx +++ b/client/src/components/ui/InputForm.jsx @@ -21,11 +21,14 @@ export default function InputForm({ setMessages, isLoading, setIsLoading, allowF const userMessage = input.trim(); setInput(''); - // Add user message to chat - const displayMessage = selectedFile - ? `${userMessage} [File: ${selectedFile.name}]` - : userMessage; - setMessages(prev => [...prev, { role: 'user', content: displayMessage }]); + // Add user message to chat with image data if present + const messageData = { + role: 'user', + content: userMessage, + image: selectedFile ? URL.createObjectURL(selectedFile) : null, + fileName: selectedFile?.name + }; + setMessages(prev => [...prev, messageData]); setIsLoading(true); try { diff --git a/client/src/components/ui/MessageBox.jsx b/client/src/components/ui/MessageBox.jsx index 7d9ba51..70cc95d 100644 --- a/client/src/components/ui/MessageBox.jsx +++ b/client/src/components/ui/MessageBox.jsx @@ -22,12 +22,23 @@ export default function ChatBox({messages, isLoading}) { )} {messages.map((msg, index) => (
-
-

{msg.content}

+
+ {msg.image && ( +
+ {msg.fileName +
+ )} +
+

{msg.content}

+
))} From 07f37a50096ea1fa939142093cfe06850f249d20 Mon Sep 17 00:00:00 2001 From: Maximus Date: Sun, 16 Nov 2025 20:46:59 -0500 Subject: [PATCH 23/72] fixed module import error. Implemented main_module search function in vite client. Fixed frontend to display product information for each store. --- client/src/components/ui/ProductResults.jsx | 71 ++++++---- nlip_web/vite_chat.py | 14 +- poetry.lock | 147 +++++++++++++++++++- pyproject.toml | 1 + website_modules/WEB_MODULES.md | 1 + website_modules/__init__.py | 0 website_modules/main_module.py | 31 +++-- website_modules/newegg_ca_module.py | 2 +- 8 files changed, 213 insertions(+), 54 deletions(-) create mode 100644 website_modules/__init__.py diff --git a/client/src/components/ui/ProductResults.jsx b/client/src/components/ui/ProductResults.jsx index 2b557aa..2f4efa0 100644 --- a/client/src/components/ui/ProductResults.jsx +++ b/client/src/components/ui/ProductResults.jsx @@ -1,36 +1,49 @@ export default function ProductResults({products, isLoading}) { return ( <> - {Object.keys(products).length > 0 && ( -
- {Object.entries(products).map(([storeName, storeProducts]) => ( - + )} + {(products?.results?.length ?? 0) === 0 && !isLoading && (

Search for products to see results

diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py index efdf964..a0550f8 100644 --- a/nlip_web/vite_chat.py +++ b/nlip_web/vite_chat.py @@ -4,11 +4,7 @@ from nlip_server import server from nlip_sdk import nlip -# import website module for dellelce bookstore -from website_modules import dellelce_bookstore_module as dbm -# from website_modules import main_module as mm -import json - +from website_modules import main_module as mm class ChatApplication(nlip_ext.SafeStatefulApplication): def __init__(self): @@ -42,17 +38,15 @@ def execute( if chat_server is None: return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") - # pass search term to website_modules - results = dbm.search_product(text) - # results = mm.search_product(text) + # pass search term to website_modules main search function + results = mm.search_product(text) # print(results) # print(f'Received text {text[0:10]}...') # response = chat_server.chat_multimodal(text, images = images) # print(f'Received response {response[0:10]}...') ## response_data = '{"Best Buy":[{"name":"Dell XPS 15","price":1299.99,"description":"15.6-inch FHD display, Intel Core i7, 16GB RAM, 512GB SSD"},{"name":"HP Pavilion 14","price":649.99,"description":"14-inch HD display, AMD Ryzen 5, 8GB RAM, 256GB SSD"}],"Amazon":[{"name":"Lenovo ThinkPad X1 Carbon","price":1549.99,"description":"14-inch 2K display, Intel Core i7, 16GB RAM, 1TB SSD, Business laptop"},{"name":"ASUS VivoBook 15","price":529.99,"description":"15.6-inch FHD display, Intel Core i5, 12GB RAM, 512GB SSD"}],"Walmart":[{"name":"Acer Aspire 5","price":479.99,"description":"15.6-inch FHD display, AMD Ryzen 3, 8GB RAM, 256GB SSD"},{"name":"Microsoft Surface Laptop 5","price":1199.99,"description":"13.5-inch PixelSense touchscreen, Intel Core i5, 8GB RAM, 256GB SSD"}]}' - response_dict = json.loads(results) - return nlip.NLIP_Factory.create_json(response_dict) + return nlip.NLIP_Factory.create_json(results) diff --git a/poetry.lock b/poetry.lock index bcb0254..8da0bcf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -178,6 +178,129 @@ files = [ [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + [[package]] name = "click" version = "8.2.1" @@ -906,6 +1029,28 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "rich" version = "14.1.0" @@ -1637,4 +1782,4 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "57819d0150c14bfc6d419c6a7152e623bba7308bb2df0ca8b37bcebf8edc9dab" +content-hash = "091128cd4bee1528e81bd02c2da7def3cd9bd761be2de24a31c32a893d9ae83c" diff --git a/pyproject.toml b/pyproject.toml index 4e5c2e0..6aca3b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ python = "^3.10" nlip-server = {path = "../nlip_server", develop = true} beautifulsoup4 = "^4.14.2" selenium = "^4.38.0" +requests = "^2.32.5" [build-system] diff --git a/website_modules/WEB_MODULES.md b/website_modules/WEB_MODULES.md index 54aa28c..823c1a9 100644 --- a/website_modules/WEB_MODULES.md +++ b/website_modules/WEB_MODULES.md @@ -10,6 +10,7 @@ When used with nlip_web the following packages have to be added to poetry: 1. `poetry add beautifulsoup4` 2. `poetry add selenium` +3. `poetry add requests` ** Server side requiremments** diff --git a/website_modules/__init__.py b/website_modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/website_modules/main_module.py b/website_modules/main_module.py index c9a0d28..731d2b9 100644 --- a/website_modules/main_module.py +++ b/website_modules/main_module.py @@ -5,24 +5,22 @@ from selenium.webdriver.chrome.options import Options from selenium.webdriver.support import expected_conditions as EC import sys -import time import json -import importlib # Import search modules for websites -import newegg_ca_module -import dellelce_bookstore_module -import staples_ca_module -import walmart_ca_module +from . import newegg_ca_module +from . import dellelce_bookstore_module +from . import staples_ca_module +# from . import walmart_ca_module # Modules with completed search_product(str) functions search_modules = [ dellelce_bookstore_module, newegg_ca_module, staples_ca_module - ] +] -def search_product(search_term: str) -> list: +def search_product(search_term: str): search_results = [] for module in search_modules: @@ -43,15 +41,22 @@ def search_product(search_term: str) -> list: print(f'\tFunction: [search_product(str) -> json]') print(f'\t{e}\n') - return json.dumps(search_results) + res = {} + res['results'] = search_results -# for use of main module as script -if __name__ == "__main__": + # has to return a proper python dictionary + return res + +def main(): search_term = "" if len(sys.argv) > 1: search_term = sys.argv[1] else: print("Input serch term: ") search_term = input() - - print(search_product(search_term)) \ No newline at end of file + + print(search_product(search_term)) + +# for use of main module as script +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/website_modules/newegg_ca_module.py b/website_modules/newegg_ca_module.py index 56f985f..70ff474 100644 --- a/website_modules/newegg_ca_module.py +++ b/website_modules/newegg_ca_module.py @@ -57,7 +57,7 @@ def print_products(items): }) # How many products that were on the first page - num_of_products = len(products) + num_of_products = 0 if num_of_products > 0: print("Number of products in stock: " + str(len(products))) # Print out all products found on the first page From 5d81d85c0ad659125795624e489c58b9f13cc5b3 Mon Sep 17 00:00:00 2001 From: Maximus Date: Tue, 18 Nov 2025 20:55:11 -0500 Subject: [PATCH 24/72] standardized no item description message. staples title and image displaying on frontend --- website_modules/dellelce_bookstore_module.py | 2 +- website_modules/newegg_ca_module.py | 3 +- website_modules/staples_ca_module.py | 33 +++++++++++++++----- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/website_modules/dellelce_bookstore_module.py b/website_modules/dellelce_bookstore_module.py index 0189167..3d432fd 100644 --- a/website_modules/dellelce_bookstore_module.py +++ b/website_modules/dellelce_bookstore_module.py @@ -116,7 +116,7 @@ def parse_site_content(html_content) -> list: # extract it-em name temp_prod["name"] = prod.find('a').text - temp_prod["description"] = 'N/A' + temp_prod["description"] = 'No description available' # commerce-product-field is where price information is stored price_field = prod.find(class_="commerce-product-field") diff --git a/website_modules/newegg_ca_module.py b/website_modules/newegg_ca_module.py index 70ff474..9ee6021 100644 --- a/website_modules/newegg_ca_module.py +++ b/website_modules/newegg_ca_module.py @@ -53,7 +53,8 @@ def print_products(items): "price": price, "link": link, "product_photo": image, - "shipping_info": shipping + "shipping_info": shipping, + "description": "No description available", }) # How many products that were on the first page diff --git a/website_modules/staples_ca_module.py b/website_modules/staples_ca_module.py index 40bc198..9c3ca48 100644 --- a/website_modules/staples_ca_module.py +++ b/website_modules/staples_ca_module.py @@ -1,13 +1,21 @@ import json +import sys import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from bs4 import BeautifulSoup +## CONSTANT VARIABLES STORE_NAME = 'staples_ca' WEB_ADDRESS = 'https://www.staples.ca/' SEARCH_ARG = 'search?query=' +HTTP_HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + # 'Accept-Language': 'en-US,en;q=0.9', + #'Accept-Encoding': 'gzip, deflate, br', + #'Connection': 'keep-alive', +} #def scrape_staples_selenium(product_name: str): def search_product(product_name: str): @@ -15,11 +23,7 @@ def search_product(product_name: str): options = Options() options.add_argument("--start-maximized") options.add_argument("--disable-blink-features=AutomationControlled") - options.add_argument( - "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/120.0.0.0 Safari/537.36" - ) + options.add_argument(f"user-agent={HTTP_HEADERS['User-Agent']}") # run headless options.add_argument("--headless=new") @@ -40,6 +44,7 @@ def search_product(product_name: str): # --- Parse with BeautifulSoup --- soup = BeautifulSoup(html, "html.parser") + results = {} results[STORE_NAME] = [] for a_tag in soup.select("a.product-link"): @@ -56,18 +61,27 @@ def search_product(product_name: str): price_el = card.select_one("span.money.pre-money") if card else None price = price_el.get_text(strip=True) if price_el else "N/A" + # extract the img src + img_tag = card.find("img") + img_src = img_tag["src"] if img_tag and img_tag.has_attr("src") else None + if text: results[STORE_NAME].append({ - "title": text, + "name": text, "price": price, - "product_image": "", + "product_photo": img_src, + "description": "No description available", "link": link }) return json.dumps(results) +def main(): + if len(sys.argv) > 1: + results = search_product(sys.argv[1]) + print(results) + return -if __name__ == "__main__": searching = 'y' while searching == 'y': product_desc = input("Enter the name of the product: ") @@ -85,3 +99,6 @@ def search_product(product_name: str): print("Thank you for shopping at Staples!") # page%5D=2 + +if __name__ == "__main__": + main() \ No newline at end of file From 03898744073947292675bde0d6153a0c17331b4a Mon Sep 17 00:00:00 2001 From: Maximus Date: Tue, 18 Nov 2025 21:41:21 -0500 Subject: [PATCH 25/72] mutli-threaded the main module web search function --- run_main_module.sh | 7 ++++ website_modules/WEB_MODULES.md | 12 +++++++ website_modules/main_module.py | 66 ++++++++++++++++++++++++++++++++-- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100755 run_main_module.sh diff --git a/run_main_module.sh b/run_main_module.sh new file mode 100755 index 0000000..363902b --- /dev/null +++ b/run_main_module.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Ask the user for input +read -p "Enter search term: " search_term + +# Run the Python module with the provided input +python3 -m website_modules.main_module "$search_term" diff --git a/website_modules/WEB_MODULES.md b/website_modules/WEB_MODULES.md index 823c1a9..1a76ffb 100644 --- a/website_modules/WEB_MODULES.md +++ b/website_modules/WEB_MODULES.md @@ -23,6 +23,18 @@ The [main module](./main_module.py) can be used to query a search term through e Each module has a primary function: **search_product(search_term: str)**. The function will return a json object representing the search results for the *search term*. +## Running vite site + +The search client site can be run from `/nlip_web/` with: +`./start.sh` + +## Running web_modules script + +The main module can be run as script from `/nlip_web/` with: +`./run_main_module.sh` + +Since website_modules has relative imports, it cannot be run from within the website_modules directory + ## Examples A [Hello World Program](./hello_world/) can be found in its respective folder. diff --git a/website_modules/main_module.py b/website_modules/main_module.py index 731d2b9..823d872 100644 --- a/website_modules/main_module.py +++ b/website_modules/main_module.py @@ -1,4 +1,5 @@ # imports for library functions +import time from bs4 import BeautifulSoup from selenium import webdriver from selenium.webdriver.common.by import By @@ -7,12 +8,18 @@ import sys import json +from concurrent.futures import ThreadPoolExecutor, as_completed + # Import search modules for websites from . import newegg_ca_module from . import dellelce_bookstore_module -from . import staples_ca_module +from . import staples_ca_module # from . import walmart_ca_module +# THREAD COUNT +MAX_THREADS = 4 +THREAD_PREFIX = "main_module_thread_" + # Modules with completed search_product(str) functions search_modules = [ dellelce_bookstore_module, @@ -20,7 +27,7 @@ staples_ca_module ] -def search_product(search_term: str): +def single_thread_search_product(search_term: str): search_results = [] for module in search_modules: @@ -47,6 +54,45 @@ def search_product(search_term: str): # has to return a proper python dictionary return res +# func for thread to execute +def run_module_search(module: str, search_term: str, ): + if not hasattr(module, 'search_product'): + return None + + try: + search_function = getattr(module, 'search_product') + product_results_json = search_function(search_term) + + # convert to dict + prods = json.loads(product_results_json) + return(prods) + + except Exception as e: + print(f'ERROR with {module}\n') + print(f'\tFunction: [search_product(str) -> json]') + print(f'\t{e}\n') + return None + +def search_product(search_term: str): + search_results = [] + + with ThreadPoolExecutor(max_workers=MAX_THREADS, thread_name_prefix=THREAD_PREFIX) as executor: + ## exectute + future = { + executor.submit(run_module_search, module, search_term): module + for module in search_modules + } + + for future in as_completed(future): + result = future.result() + if result is not None: + search_results.append(result) + + # has to return a proper python dictionary + res = {} + res['results'] = search_results + return res + def main(): search_term = "" if len(sys.argv) > 1: @@ -55,8 +101,24 @@ def main(): print("Input serch term: ") search_term = input() + #test_performance() print(search_product(search_term)) +# benchmark test for multi vs single thread performance +def test_performance(): + search_term = "lanyards" + + t1 = time.perf_counter() + print(single_thread_search_product(search_term)) + t2 = time.perf_counter() + + t3 = time.perf_counter() + print(search_product(search_term)) + t4 = time.perf_counter() + + print(f'Single thread took :{t2 - t1} seconds') + print(f'Mutli thread took :{t4 - t3} seconds') + # for use of main module as script if __name__ == "__main__": main() \ No newline at end of file From dd4919b15e65fd3bdac91955f57d004c43a71ae7 Mon Sep 17 00:00:00 2001 From: AryamanArora2003 <158627206+AryamanArora2003@users.noreply.github.com> Date: Thu, 20 Nov 2025 04:38:51 -0500 Subject: [PATCH 26/72] Add files via upload Amazon web scraper is complete. I stored the product results in the same format as James did for the Staples webscraper --- website_modules/amazon_final.py | 133 ++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 website_modules/amazon_final.py diff --git a/website_modules/amazon_final.py b/website_modules/amazon_final.py new file mode 100644 index 0000000..96dcb7b --- /dev/null +++ b/website_modules/amazon_final.py @@ -0,0 +1,133 @@ +#author Aryaman Arora +import random, time +import requests +from bs4 import BeautifulSoup +from urllib.parse import urlencode + +STORE_NAME = 'amazon_ca' +SEARCH_TERM = input("Enter search term: ").strip() + +# Define a pool of user agents to rotate through +# Used in HEADERS +UA_POOL = [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", +] + +HEADERS = {"User-Agent": random.choice(UA_POOL), + "Accept-Language": "en-CA,en;q=0.9", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": "https://www.amazon.ca/", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Connection": "keep-alive", + } + +#return search url for a given search term +def build_url( searchTerm: str) -> str: + return "https://www.amazon.ca/s?" + urlencode({"k": searchTerm}) + +# Retrieve HTML content from search URL +def get_HTML(url: str, tries: int = 4) -> str: + + for i in range(tries): + + #pause exectution of function to simulate human behaviour + time.sleep(random.uniform(0.7, 1.6) + i * 0.8) + + r = requests.get(url, headers=HEADERS, timeout=30) + #request successful return HTML content + if r.status_code == 200: + return r.text + # request bad, next itteration + if r.status_code in (429, 503): + continue + + #not status code 200, 429 or 503, raise HTTP error + r.raise_for_status() + raise SystemExit("Blocked (429/503). Try again or use Playwright/API.") + +# Parse HTML content and return as JSON-like dict +# if no products found or CAPTCHA detected, return empty dict +def parse_and_Dict(html_content:str) -> dict: + #check for CAPTCHA in HTML content + lower_html = html_content.lower() + if "captchachallenge" in lower_html or "enter the characters you see below" in lower_html: + print("Got CAPTCHA page — slow down or try again later.") + return {STORE_NAME: []} + results = {STORE_NAME: []} + soup = BeautifulSoup(html_content, "lxml") + # soup.prettify() + with open("finalAmzSoup.html", "w", encoding="utf-8") as f: + f.write(str(soup)) + + # select all divs with class 's-main-slot' && inside that dive any element with non empty data-asin attribute + # data-asin is the unique identifier for each product on amazon + product_cards = soup.select("div.s-main-slot [data-asin]:not([data-asin=''])") + + #no products found, return empty dict + if not product_cards: + print("No product cards found") + return {STORE_NAME: []} + + for c in product_cards: + #product name + name = (c.select_one("h2 a span") + or c.select_one("h2 span.a-text-normal") + or c.select_one("h2")) + prod_name = name.get_text(strip=True) if name else None + + #price + price = c.select_one("span.a-price > span.a-offscreen") + prod_price = price.get_text(strip=True) if price else "N/A" + + #image + img_tag = c.select_one("img.s-image") + img_src = img_tag["src"] if img_tag and img_tag.get("src") else None + + # PRODUCT LINK + link_tag = c.select_one("h2 a[href]") + if link_tag: + href = link_tag["href"] + if href.startswith("/"): + prod_link = "https://www.amazon.ca" + href + else: + prod_link = href + else: + prod_link = None + + if prod_name: + results[STORE_NAME].append({ + "name": prod_name, + "price": prod_price, + "product_photo": img_src, + "description": "No description available", + "link": prod_link + }) + + return results + + + +# main function for use as a script +if __name__ == "__main__": + # print("this is search term: " + SEARCH_TERM) + search_url = build_url(SEARCH_TERM) + html_content = get_HTML(search_url) + item_dict = parse_and_Dict(html_content) + + products = item_dict.get(STORE_NAME, []) + + if not products: + print("No products were extracted.") + else: + for product in products: + print( + "Name:", product["name"], + "\nPrice:", product["price"], + "\nLink:", product["link"], + "\nImage:", product["product_photo"], + "\n\n") + + From 7e225997a498597e129da01f25e30aa91bb791c4 Mon Sep 17 00:00:00 2001 From: AryamanArora2003 <158627206+AryamanArora2003@users.noreply.github.com> Date: Thu, 20 Nov 2025 05:25:51 -0500 Subject: [PATCH 27/72] Delete website_modules/amazon_final.py --- website_modules/amazon_final.py | 133 -------------------------------- 1 file changed, 133 deletions(-) delete mode 100644 website_modules/amazon_final.py diff --git a/website_modules/amazon_final.py b/website_modules/amazon_final.py deleted file mode 100644 index 96dcb7b..0000000 --- a/website_modules/amazon_final.py +++ /dev/null @@ -1,133 +0,0 @@ -#author Aryaman Arora -import random, time -import requests -from bs4 import BeautifulSoup -from urllib.parse import urlencode - -STORE_NAME = 'amazon_ca' -SEARCH_TERM = input("Enter search term: ").strip() - -# Define a pool of user agents to rotate through -# Used in HEADERS -UA_POOL = [ - "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", -] - -HEADERS = {"User-Agent": random.choice(UA_POOL), - "Accept-Language": "en-CA,en;q=0.9", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Referer": "https://www.amazon.ca/", - "Cache-Control": "no-cache", - "Pragma": "no-cache", - "Connection": "keep-alive", - } - -#return search url for a given search term -def build_url( searchTerm: str) -> str: - return "https://www.amazon.ca/s?" + urlencode({"k": searchTerm}) - -# Retrieve HTML content from search URL -def get_HTML(url: str, tries: int = 4) -> str: - - for i in range(tries): - - #pause exectution of function to simulate human behaviour - time.sleep(random.uniform(0.7, 1.6) + i * 0.8) - - r = requests.get(url, headers=HEADERS, timeout=30) - #request successful return HTML content - if r.status_code == 200: - return r.text - # request bad, next itteration - if r.status_code in (429, 503): - continue - - #not status code 200, 429 or 503, raise HTTP error - r.raise_for_status() - raise SystemExit("Blocked (429/503). Try again or use Playwright/API.") - -# Parse HTML content and return as JSON-like dict -# if no products found or CAPTCHA detected, return empty dict -def parse_and_Dict(html_content:str) -> dict: - #check for CAPTCHA in HTML content - lower_html = html_content.lower() - if "captchachallenge" in lower_html or "enter the characters you see below" in lower_html: - print("Got CAPTCHA page — slow down or try again later.") - return {STORE_NAME: []} - results = {STORE_NAME: []} - soup = BeautifulSoup(html_content, "lxml") - # soup.prettify() - with open("finalAmzSoup.html", "w", encoding="utf-8") as f: - f.write(str(soup)) - - # select all divs with class 's-main-slot' && inside that dive any element with non empty data-asin attribute - # data-asin is the unique identifier for each product on amazon - product_cards = soup.select("div.s-main-slot [data-asin]:not([data-asin=''])") - - #no products found, return empty dict - if not product_cards: - print("No product cards found") - return {STORE_NAME: []} - - for c in product_cards: - #product name - name = (c.select_one("h2 a span") - or c.select_one("h2 span.a-text-normal") - or c.select_one("h2")) - prod_name = name.get_text(strip=True) if name else None - - #price - price = c.select_one("span.a-price > span.a-offscreen") - prod_price = price.get_text(strip=True) if price else "N/A" - - #image - img_tag = c.select_one("img.s-image") - img_src = img_tag["src"] if img_tag and img_tag.get("src") else None - - # PRODUCT LINK - link_tag = c.select_one("h2 a[href]") - if link_tag: - href = link_tag["href"] - if href.startswith("/"): - prod_link = "https://www.amazon.ca" + href - else: - prod_link = href - else: - prod_link = None - - if prod_name: - results[STORE_NAME].append({ - "name": prod_name, - "price": prod_price, - "product_photo": img_src, - "description": "No description available", - "link": prod_link - }) - - return results - - - -# main function for use as a script -if __name__ == "__main__": - # print("this is search term: " + SEARCH_TERM) - search_url = build_url(SEARCH_TERM) - html_content = get_HTML(search_url) - item_dict = parse_and_Dict(html_content) - - products = item_dict.get(STORE_NAME, []) - - if not products: - print("No products were extracted.") - else: - for product in products: - print( - "Name:", product["name"], - "\nPrice:", product["price"], - "\nLink:", product["link"], - "\nImage:", product["product_photo"], - "\n\n") - - From 553735d29b26cba17bb174973bb1ff939be46466 Mon Sep 17 00:00:00 2001 From: AryamanArora2003 <158627206+AryamanArora2003@users.noreply.github.com> Date: Thu, 20 Nov 2025 05:27:49 -0500 Subject: [PATCH 28/72] Add files via upload Amazon web scraper is complete. I stored the product results in the same format as James did for the Staples webscraper --- website_modules/amazon_final.py | 133 ++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 website_modules/amazon_final.py diff --git a/website_modules/amazon_final.py b/website_modules/amazon_final.py new file mode 100644 index 0000000..63fbc7c --- /dev/null +++ b/website_modules/amazon_final.py @@ -0,0 +1,133 @@ +#author Aryaman Arora +import random, time +import requests +from bs4 import BeautifulSoup +from urllib.parse import urlencode + +STORE_NAME = 'amazon_ca' +SEARCH_TERM = input("Enter search term: ").strip() + +# Define a pool of user agents to rotate through +# Used in HEADERS +UA_POOL = [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", +] + +HEADERS = {"User-Agent": random.choice(UA_POOL), + "Accept-Language": "en-CA,en;q=0.9", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Referer": "https://www.amazon.ca/", + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Connection": "keep-alive", + } + +#return search url for a given search term +def build_url( searchTerm: str) -> str: + return "https://www.amazon.ca/s?" + urlencode({"k": searchTerm}) + +# Retrieve HTML content from search URL +def get_HTML(url: str, tries: int = 4) -> str: + + for i in range(tries): + + #pause exectution of function to simulate human behaviour + time.sleep(random.uniform(0.7, 1.6) + i * 0.8) + + r = requests.get(url, headers=HEADERS, timeout=30) + #request successful return HTML content + if r.status_code == 200: + return r.text + # request bad, next itteration + if r.status_code in (429, 503): + continue + + #not status code 200, 429 or 503, raise HTTP error + r.raise_for_status() + raise SystemExit("Blocked") + +# Parse HTML content and return as JSON-like dict +# if no products found or CAPTCHA detected, return empty dict +def parse_and_Dict(html_content:str) -> dict: + #check for CAPTCHA in HTML content + lower_html = html_content.lower() + if "captchachallenge" in lower_html or "enter the characters you see below" in lower_html: + print("Got CAPTCHA page") + return {STORE_NAME: []} + results = {STORE_NAME: []} + soup = BeautifulSoup(html_content, "lxml") + # soup.prettify() + with open("finalAmzSoup.html", "w", encoding="utf-8") as f: + f.write(str(soup)) + + # select all divs with class 's-main-slot' && inside that dive any element with non empty data-asin attribute + # data-asin is the unique identifier for each product on amazon + product_cards = soup.select("div.s-main-slot [data-asin]:not([data-asin=''])") + + #no products found, return empty dict + if not product_cards: + print("No product cards found") + return {STORE_NAME: []} + + for c in product_cards: + #product name + name = (c.select_one("h2 a span") + or c.select_one("h2 span.a-text-normal") + or c.select_one("h2")) + prod_name = name.get_text(strip=True) if name else None + + # prodcuct price + price = c.select_one("span.a-price > span.a-offscreen") + prod_price = price.get_text(strip=True) if price else "N/A" + + #product image + img_tag = c.select_one("img.s-image") + img_src = img_tag["src"] if img_tag and img_tag.get("src") else None + + #product Link + link_tag = c.select_one("h2 a[href]") + if link_tag: + href = link_tag["href"] + if href.startswith("/"): + prod_link = "https://www.amazon.ca" + href + else: + prod_link = href + else: + prod_link = None + + if prod_name: + results[STORE_NAME].append({ + "name": prod_name, + "price": prod_price, + "product_photo": img_src, + "description": "No description available", + "link": prod_link + }) + + return results + + + +# main function for use as a script +if __name__ == "__main__": + # print("this is search term: " + SEARCH_TERM) + search_url = build_url(SEARCH_TERM) + html_content = get_HTML(search_url) + item_dict = parse_and_Dict(html_content) + + products = item_dict.get(STORE_NAME, []) + + if not products: + print("No products were found") + else: + for product in products: + print( + "Name:", product["name"], + "\nPrice:", product["price"], + "\nLink:", product["link"], + "\nImage:", product["product_photo"], + "\n\n") + + From 3e52c00bff4476b109e3c7b1458c48df30bbb3bd Mon Sep 17 00:00:00 2001 From: AryamanArora2003 <158627206+AryamanArora2003@users.noreply.github.com> Date: Thu, 20 Nov 2025 07:51:24 -0500 Subject: [PATCH 29/72] Add files via upload Functional Home Depot web scraper --- website_modules/homeDepotSelen.py | 166 ++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 website_modules/homeDepotSelen.py diff --git a/website_modules/homeDepotSelen.py b/website_modules/homeDepotSelen.py new file mode 100644 index 0000000..e0a2e2b --- /dev/null +++ b/website_modules/homeDepotSelen.py @@ -0,0 +1,166 @@ +# author Aryaman Arora +import time +import random +from bs4 import BeautifulSoup +from urllib.parse import urlencode + +# Selenium imports for full JS rendering +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC + +STORE_NAME = 'homedepot_ca' +SEARCH_TERM = input("Enter search term: ").strip() + +# get HTML with Selenium +def get_html(url: str) -> str: + + # Setup Chrome Options + options = Options() + # options.add_argument("--headless") # run in background if needed + options.add_argument("--disable-blink-features=AutomationControlled") + options.add_argument( + "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + ) + + #initialize Chrome web driver + driver = webdriver.Chrome( + service=Service(ChromeDriverManager().install()), + options=options + ) + + try: + # print("open browser") + driver.get(url) + + #wait for the products to load or timeout after 15 seconds + try: + WebDriverWait(driver, 15).until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "article.acl-product-card, div.acl-product-card") + ) + ) + # print("product loaded good.") + except: + print("Timed/Product not found") + + # Scroll to trigger loading + driver.execute_script("window.scrollTo(0, 500);") + time.sleep(2) + + # Return full HTML source with loaded js content + return driver.page_source + + finally: + driver.quit() + +# build search URL +def build_url(searchTerm: str) -> str: + return "https://www.homedepot.ca/search?" + urlencode({"q": searchTerm}) + +# Parse HTML and return dictionary +def parse_and_Dict(html_content: str) -> dict: + + results = {STORE_NAME: []} + soup = BeautifulSoup(html_content, "lxml") + + # slect all product cards + product_cards = soup.select( + "article.acl-product-card, " + "div.acl-product-card, " + "[data-component='ProductCard']" + ) + + # No products found, return empty dict + if not product_cards: + return {STORE_NAME: []} + + for c in product_cards: + + # product name + name_tag = ( + c.select_one(".acl-product-card__title") + or c.select_one("h2") + or c.select_one("a.acl-product-card__title-link") + ) + prod_name = name_tag.get_text(strip=True) if name_tag else None + + #product price + price = ( + c.select_one(".acl-product-card__price") + or c.select_one("[class*='price-format']") + or c.select_one("[itemprop='price']") + ) + + if price: + # price raw text format, "$179And00Cents/ each") + raw_price = price.get_text(strip=True) + #filter text + clean_price = raw_price.replace("And", ".").replace("Cents", "").replace("/ each", "").strip() + prod_price = clean_price + else: + prod_price = "N/A" + + # product image + img_tag = c.select_one("img") + img_src = None + if img_tag: + img_src = img_tag.get("src") or img_tag.get("data-src") + + + # product Link + link_tag = c.select_one("a.acl-product-card__title-link") + + # if class above not found fallback H2 tag + if not link_tag: + link_tag = c.select_one("h2 a") + + prod_link = None + if link_tag and link_tag.get("href"): + href = link_tag["href"] + if href.startswith("/"): + prod_link = "https://www.homedepot.ca" + href + else: + prod_link = href + + # if valid append product to list + if prod_name: + results[STORE_NAME].append({ + "name": prod_name, + "price": prod_price, + "product_photo": img_src, + "description": "No description available", + "link": prod_link + }) + + return results + +# Main +if __name__ == "__main__": + search_url = build_url(SEARCH_TERM) + + # selenium gets JS rendered Html + html_content = get_html(search_url) + + # Parse and return products dict + item_dict = parse_and_Dict(html_content) + products = item_dict.get(STORE_NAME, []) + + if not products: + print("No products were extracted.") + else: + # print(f"Found {len(products)} products:\n") + print("\n") + for product in products: + print( + "Name:", product["name"], + "\nPrice:", product["price"], + "\nLink:", product["link"], + "\nImage:", product["product_photo"], + "\n" + ) From 8d613906a7a975b1e7510afff3a20072f072ba6b Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Thu, 20 Nov 2025 17:38:56 -0500 Subject: [PATCH 30/72] use same port --- scripts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts.py b/scripts.py index 1af6964..444a33c 100644 --- a/scripts.py +++ b/scripts.py @@ -48,7 +48,7 @@ def start_vite_text(): print(f"Error executing command: {e}") def start_vite_products(): - my_env = get_env(local_port=8040,chat_model="llava") + my_env = get_env(local_port=8030,chat_model="llava") try: # Build in client directory subprocess.run(["npm", "run", "build:products"], env=my_env, check=True, cwd="client") @@ -59,7 +59,7 @@ def start_vite_products(): print(f"Error executing command: {e}") def start_vite_image(): - my_env = get_env(local_port=8050,chat_model="llava") + my_env = get_env(local_port=8030,chat_model="llava") try: # Build in client directory subprocess.run(["npm", "run", "build:image"], env=my_env, check=True, cwd="client") From 72d0718cafa9c08e0e5f7fdd6934c826b93ed862 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Thu, 20 Nov 2025 18:22:57 -0500 Subject: [PATCH 31/72] search with ai --- nlip_web/vite_chat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py index a0550f8..b5ed907 100644 --- a/nlip_web/vite_chat.py +++ b/nlip_web/vite_chat.py @@ -38,8 +38,12 @@ def execute( if chat_server is None: return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") + response = chat_server.chat(text + "\nSummarize the search query and convert into a command like 'Find me X'. Be concise.") + print("Summary: ", response) + response = chat_server.chat(response + "\nGive me the product that we are searching for. Be very concise, just a 1-2 words.") + print("Shortened search: " ,response) # pass search term to website_modules main search function - results = mm.search_product(text) + results = mm.search_product(response) # print(results) # print(f'Received text {text[0:10]}...') From 5958ebc6a361193641f9f6d84a7d4200d948b5ab Mon Sep 17 00:00:00 2001 From: Maximus Slabon Date: Sun, 23 Nov 2025 17:36:34 -0500 Subject: [PATCH 32/72] refactored return json format --- website_modules/dellelce_bookstore_module.py | 12 +++++++----- website_modules/main_module.py | 5 +++-- website_modules/newegg_ca_module.py | 6 +++--- website_modules/staples_ca_module.py | 6 +++--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/website_modules/dellelce_bookstore_module.py b/website_modules/dellelce_bookstore_module.py index 3d432fd..a6f3ee0 100644 --- a/website_modules/dellelce_bookstore_module.py +++ b/website_modules/dellelce_bookstore_module.py @@ -35,8 +35,8 @@ def search_product(query_term): options.add_argument(f"user-agent={HTTP_HEADERS['User-Agent']}") driver = webdriver.Chrome(options=options) - product_list = {} - product_list[STORE_NAME] = [] + product_list = [] + #product_list[STORE_NAME] = [] try: driver.get(WEB_ADDRESS + SEARCH_ARG + query_term) @@ -48,10 +48,10 @@ def search_product(query_term): driver.quit() return - product_list[STORE_NAME] = product_list[STORE_NAME] + parsed_content + product_list = parsed_content # first page non-empty, query remaining pages of content. - end_of_content = False + end_of_content = True current_page = 1 # second page starts at 1 while not end_of_content: # sleep to not overwhelm site with continuous queries @@ -64,7 +64,7 @@ def search_product(query_term): break # increment page counter current_page += 1 - product_list[STORE_NAME] = product_list[STORE_NAME] + parsed_content + product_list.extend(parsed_content) finally: driver.quit() @@ -114,6 +114,8 @@ def parse_site_content(html_content) -> list: for prod in products: temp_prod = {} # extract it-em name + temp_prod["store"] = STORE_NAME + temp_prod["name"] = prod.find('a').text temp_prod["description"] = 'No description available' diff --git a/website_modules/main_module.py b/website_modules/main_module.py index 823d872..62e64be 100644 --- a/website_modules/main_module.py +++ b/website_modules/main_module.py @@ -40,8 +40,9 @@ def single_thread_search_product(search_term: str): product_results_json = search_function(search_term) # convert to dict - prods = json.loads(product_results_json) - search_results.append(prods) + prods = product_results_json + print(prods) + search_results = search_results + json.loads(prods) except Exception as e: print(f'ERROR with {module}\n') diff --git a/website_modules/newegg_ca_module.py b/website_modules/newegg_ca_module.py index 9ee6021..4120804 100644 --- a/website_modules/newegg_ca_module.py +++ b/website_modules/newegg_ca_module.py @@ -49,6 +49,7 @@ def print_products(items): if title and price: # Only add if title found (skip ads) products.append({ + "store": STORE_NAME, "name": title, "price": price, "link": link, @@ -71,8 +72,7 @@ def print_products(items): return products def search_product(product_name: str): - product_list = {} - product_list[STORE_NAME] = [] + product_list = [] try: session = requests.Session() @@ -84,7 +84,7 @@ def search_product(product_name: str): soup = BeautifulSoup(r.text, 'html.parser') # No need to install lxml (html.parser), parse HTML # Same method of extracting info as Staples - product_list[STORE_NAME] = print_products(soup.select('.item-cell')) + product_list = print_products(soup.select('.item-cell')) # If you want more pages, just ask and add next page to url and redo this process with that page (ex. &page2) except Exception as e: print(e) diff --git a/website_modules/staples_ca_module.py b/website_modules/staples_ca_module.py index 9c3ca48..7e89c99 100644 --- a/website_modules/staples_ca_module.py +++ b/website_modules/staples_ca_module.py @@ -45,8 +45,7 @@ def search_product(product_name: str): # --- Parse with BeautifulSoup --- soup = BeautifulSoup(html, "html.parser") - results = {} - results[STORE_NAME] = [] + results = [] for a_tag in soup.select("a.product-link"): text = a_tag.get_text(separator=" ", strip=True) @@ -66,7 +65,8 @@ def search_product(product_name: str): img_src = img_tag["src"] if img_tag and img_tag.has_attr("src") else None if text: - results[STORE_NAME].append({ + results.append({ + "store": STORE_NAME, "name": text, "price": price, "product_photo": img_src, From 507ec28af1dbe0c8097ca71d750ab2fa49c63005 Mon Sep 17 00:00:00 2001 From: Maximus Slabon Date: Sun, 23 Nov 2025 17:48:46 -0500 Subject: [PATCH 33/72] fix module list appending --- website_modules/main_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website_modules/main_module.py b/website_modules/main_module.py index 62e64be..bdd1c70 100644 --- a/website_modules/main_module.py +++ b/website_modules/main_module.py @@ -87,7 +87,7 @@ def search_product(search_term: str): for future in as_completed(future): result = future.result() if result is not None: - search_results.append(result) + search_results.extend(result) # has to return a proper python dictionary res = {} From a885a0765ef0501db6e0450368716533365dbce8 Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Sun, 23 Nov 2025 18:05:21 -0500 Subject: [PATCH 34/72] update to match new format --- client/src/components/ui/ProductResults.jsx | 73 +++++++++++---------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/client/src/components/ui/ProductResults.jsx b/client/src/components/ui/ProductResults.jsx index 2f4efa0..c07cc80 100644 --- a/client/src/components/ui/ProductResults.jsx +++ b/client/src/components/ui/ProductResults.jsx @@ -2,47 +2,48 @@ export default function ProductResults({products, isLoading}) { return ( <> {(products?.results?.length ?? 0) > 0 && ( -
- {(products?.results ?? []).map((store) => { - const storeName = Object.keys(store)[0]; - return ( - + )} {(products?.results?.length ?? 0) === 0 && !isLoading && (

Search for products to see results

From b6ab20ee57b5bfd16be5a22f355531e0d879e8be Mon Sep 17 00:00:00 2001 From: Maximus Slabon Date: Sun, 23 Nov 2025 18:07:21 -0500 Subject: [PATCH 35/72] updated price values from string to float --- website_modules/dellelce_bookstore_module.py | 11 +++++++++-- website_modules/main_module.py | 4 ++-- website_modules/newegg_ca_module.py | 6 +++++- website_modules/staples_ca_module.py | 4 +++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/website_modules/dellelce_bookstore_module.py b/website_modules/dellelce_bookstore_module.py index a6f3ee0..45b80c6 100644 --- a/website_modules/dellelce_bookstore_module.py +++ b/website_modules/dellelce_bookstore_module.py @@ -27,6 +27,11 @@ 'Connection': 'keep-alive', } +def get_next_page(query_term: str, page_num: int): + page_num = page_num - 1 + query = query_term + PAGE_NUM + str(1) + return search_product(query_term=query) + # takes a serch term or formatted query to search the site # returns list of product objects def search_product(query_term): @@ -122,7 +127,8 @@ def parse_site_content(html_content) -> list: # commerce-product-field is where price information is stored price_field = prod.find(class_="commerce-product-field") - temp_prod["price"] = price_field.find("div", class_="field-item").text + price = price_field.find("div", class_="field-item").text + temp_prod["price"] = (price[1:]) ## can potentially contain price If item is on sale. # check if item has sizes: @@ -191,5 +197,6 @@ def get_config() -> dict: print("Input serch term: ") search_term = input() - print(search_product(search_term)) + #print(search_product(search_term)) + print(get_next_page(search_term, 2)) \ No newline at end of file diff --git a/website_modules/main_module.py b/website_modules/main_module.py index bdd1c70..959850b 100644 --- a/website_modules/main_module.py +++ b/website_modules/main_module.py @@ -23,8 +23,8 @@ # Modules with completed search_product(str) functions search_modules = [ dellelce_bookstore_module, - newegg_ca_module, - staples_ca_module + #newegg_ca_module, + #staples_ca_module ] def single_thread_search_product(search_term: str): diff --git a/website_modules/newegg_ca_module.py b/website_modules/newegg_ca_module.py index 4120804..e9edb0f 100644 --- a/website_modules/newegg_ca_module.py +++ b/website_modules/newegg_ca_module.py @@ -9,6 +9,7 @@ STORE_NAME = 'new_egg_ca' WEB_ADDRESS = 'https://www.newegg.ca/' SEARCH_ARG = 'p/pl?d=' +PAGE_NUM = "&page=" HTTP_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', @@ -20,6 +21,9 @@ def get_url(product_name, page): return 'https://www.newegg.ca/p/pl?d=' + quote(product_name) +def get_next_page(query_term: str, page_num: int): + query = query_term + PAGE_NUM + str(page_num) + return search_product(query) def print_products(items): products = [] @@ -51,7 +55,7 @@ def print_products(items): products.append({ "store": STORE_NAME, "name": title, - "price": price, + "price": float(price[1:]), "link": link, "product_photo": image, "shipping_info": shipping, diff --git a/website_modules/staples_ca_module.py b/website_modules/staples_ca_module.py index 7e89c99..8ca1101 100644 --- a/website_modules/staples_ca_module.py +++ b/website_modules/staples_ca_module.py @@ -17,6 +17,8 @@ #'Connection': 'keep-alive', } + + #def scrape_staples_selenium(product_name: str): def search_product(product_name: str): # --- Chrome options --- @@ -68,7 +70,7 @@ def search_product(product_name: str): results.append({ "store": STORE_NAME, "name": text, - "price": price, + "price": float(price[1:]), "product_photo": img_src, "description": "No description available", "link": link From 49b48979e8b10d76844a4c1e9cddadcc71bedfd3 Mon Sep 17 00:00:00 2001 From: Maximus Slabon Date: Sun, 23 Nov 2025 18:07:21 -0500 Subject: [PATCH 36/72] updated price values from string to float --- website_modules/dellelce_bookstore_module.py | 11 +++++++++-- website_modules/main_module.py | 4 ++-- website_modules/newegg_ca_module.py | 6 +++++- website_modules/staples_ca_module.py | 4 +++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/website_modules/dellelce_bookstore_module.py b/website_modules/dellelce_bookstore_module.py index a6f3ee0..45b80c6 100644 --- a/website_modules/dellelce_bookstore_module.py +++ b/website_modules/dellelce_bookstore_module.py @@ -27,6 +27,11 @@ 'Connection': 'keep-alive', } +def get_next_page(query_term: str, page_num: int): + page_num = page_num - 1 + query = query_term + PAGE_NUM + str(1) + return search_product(query_term=query) + # takes a serch term or formatted query to search the site # returns list of product objects def search_product(query_term): @@ -122,7 +127,8 @@ def parse_site_content(html_content) -> list: # commerce-product-field is where price information is stored price_field = prod.find(class_="commerce-product-field") - temp_prod["price"] = price_field.find("div", class_="field-item").text + price = price_field.find("div", class_="field-item").text + temp_prod["price"] = (price[1:]) ## can potentially contain price If item is on sale. # check if item has sizes: @@ -191,5 +197,6 @@ def get_config() -> dict: print("Input serch term: ") search_term = input() - print(search_product(search_term)) + #print(search_product(search_term)) + print(get_next_page(search_term, 2)) \ No newline at end of file diff --git a/website_modules/main_module.py b/website_modules/main_module.py index bdd1c70..959850b 100644 --- a/website_modules/main_module.py +++ b/website_modules/main_module.py @@ -23,8 +23,8 @@ # Modules with completed search_product(str) functions search_modules = [ dellelce_bookstore_module, - newegg_ca_module, - staples_ca_module + #newegg_ca_module, + #staples_ca_module ] def single_thread_search_product(search_term: str): diff --git a/website_modules/newegg_ca_module.py b/website_modules/newegg_ca_module.py index 4120804..e9edb0f 100644 --- a/website_modules/newegg_ca_module.py +++ b/website_modules/newegg_ca_module.py @@ -9,6 +9,7 @@ STORE_NAME = 'new_egg_ca' WEB_ADDRESS = 'https://www.newegg.ca/' SEARCH_ARG = 'p/pl?d=' +PAGE_NUM = "&page=" HTTP_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', @@ -20,6 +21,9 @@ def get_url(product_name, page): return 'https://www.newegg.ca/p/pl?d=' + quote(product_name) +def get_next_page(query_term: str, page_num: int): + query = query_term + PAGE_NUM + str(page_num) + return search_product(query) def print_products(items): products = [] @@ -51,7 +55,7 @@ def print_products(items): products.append({ "store": STORE_NAME, "name": title, - "price": price, + "price": float(price[1:]), "link": link, "product_photo": image, "shipping_info": shipping, diff --git a/website_modules/staples_ca_module.py b/website_modules/staples_ca_module.py index 7e89c99..8ca1101 100644 --- a/website_modules/staples_ca_module.py +++ b/website_modules/staples_ca_module.py @@ -17,6 +17,8 @@ #'Connection': 'keep-alive', } + + #def scrape_staples_selenium(product_name: str): def search_product(product_name: str): # --- Chrome options --- @@ -68,7 +70,7 @@ def search_product(product_name: str): results.append({ "store": STORE_NAME, "name": text, - "price": price, + "price": float(price[1:]), "product_photo": img_src, "description": "No description available", "link": link From 00a4d18d4c5aa15bd7ba0b110deb124d3f71012b Mon Sep 17 00:00:00 2001 From: Ethan Tiger Wakefield Date: Sun, 23 Nov 2025 18:26:24 -0500 Subject: [PATCH 37/72] sort --- client/src/components/ui/SearchForm.jsx | 3 ++- nlip_web/vite_chat.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/components/ui/SearchForm.jsx b/client/src/components/ui/SearchForm.jsx index b3bd520..5981428 100644 --- a/client/src/components/ui/SearchForm.jsx +++ b/client/src/components/ui/SearchForm.jsx @@ -13,7 +13,8 @@ export default function SearchForm({ setProducts, isLoading, setIsLoading}) { try { const response = await nlipClient.sendMessage(query.trim(), true) - if (response) { + if (response && response.results.length > 0) { + response.results.sort((a,b) => a.price - b.price) setProducts(response); } } catch (error) { diff --git a/nlip_web/vite_chat.py b/nlip_web/vite_chat.py index b5ed907..2a61e07 100644 --- a/nlip_web/vite_chat.py +++ b/nlip_web/vite_chat.py @@ -38,9 +38,9 @@ def execute( if chat_server is None: return nlip.NLIP_Factory.create_text("Error: Can't find my chat server") - response = chat_server.chat(text + "\nSummarize the search query and convert into a command like 'Find me X'. Be concise.") + response = chat_server.chat("\"" + text + "\"" + "\nSummarize the search query and convert into a command like 'Find me X'. Be concise.") print("Summary: ", response) - response = chat_server.chat(response + "\nGive me the product that we are searching for. Be very concise, just a 1-2 words.") + response = chat_server.chat("\"" + response + "\"" + "\nGive me the product that we are searching for. Be very concise, just a 1-2 words.") print("Shortened search: " ,response) # pass search term to website_modules main search function results = mm.search_product(response) From 60d1d9ee64858d4be58aeb6b63d7ecf43080905f Mon Sep 17 00:00:00 2001 From: Aryaman Arora Date: Sun, 23 Nov 2025 18:36:18 -0500 Subject: [PATCH 38/72] updated amazon module to work with main --- finalAmzSoup.html | 4573 +++++++++++++++++++++++++++++++ website_modules/amazon_final.py | 56 +- 2 files changed, 4613 insertions(+), 16 deletions(-) create mode 100644 finalAmzSoup.html diff --git a/finalAmzSoup.html b/finalAmzSoup.html new file mode 100644 index 0000000..3d066cc --- /dev/null +++ b/finalAmzSoup.html @@ -0,0 +1,4573 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Amazon.ca : coffee + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/website_modules/amazon_final.py b/website_modules/amazon_final.py index 63fbc7c..1fdb6f0 100644 --- a/website_modules/amazon_final.py +++ b/website_modules/amazon_final.py @@ -5,7 +5,7 @@ from urllib.parse import urlencode STORE_NAME = 'amazon_ca' -SEARCH_TERM = input("Enter search term: ").strip() + # Define a pool of user agents to rotate through # Used in HEADERS @@ -55,8 +55,9 @@ def parse_and_Dict(html_content:str) -> dict: lower_html = html_content.lower() if "captchachallenge" in lower_html or "enter the characters you see below" in lower_html: print("Got CAPTCHA page") - return {STORE_NAME: []} - results = {STORE_NAME: []} + return [] #{STORE_NAME: []} ########################## + # results = {STORE_NAME: []} + results = [] soup = BeautifulSoup(html_content, "lxml") # soup.prettify() with open("finalAmzSoup.html", "w", encoding="utf-8") as f: @@ -69,7 +70,7 @@ def parse_and_Dict(html_content:str) -> dict: #no products found, return empty dict if not product_cards: print("No product cards found") - return {STORE_NAME: []} + return [] #{STORE_NAME: []} ########################### for c in product_cards: #product name @@ -88,46 +89,69 @@ def parse_and_Dict(html_content:str) -> dict: #product Link link_tag = c.select_one("h2 a[href]") + + # alternative select + if not link_tag: + link_tag = c.select_one("a.a-link-normal.s-underline-text[href]") + if not link_tag: + link_tag = c.select_one("a.s-no-outline[href]") + if link_tag: href = link_tag["href"] + # handle relative urls if href.startswith("/"): prod_link = "https://www.amazon.ca" + href else: prod_link = href else: prod_link = None + if prod_name: - results[STORE_NAME].append({ + results.append({ + "store": STORE_NAME, "name": prod_name, "price": prod_price, "product_photo": img_src, "description": "No description available", - "link": prod_link - }) + "link": prod_link}) return results +def search_products(search_term: str) -> list: + + search_url = build_url(search_term) + html_content = get_HTML(search_url) + item_list = parse_and_Dict(html_content) + products = item_list + + return products + + + # main function for use as a script if __name__ == "__main__": + SEARCH_TERM = input("Enter search term: ").strip() # print("this is search term: " + SEARCH_TERM) search_url = build_url(SEARCH_TERM) html_content = get_HTML(search_url) - item_dict = parse_and_Dict(html_content) + item_list = parse_and_Dict(html_content) + + - products = item_dict.get(STORE_NAME, []) + products = item_list if not products: print("No products were found") else: - for product in products: - print( - "Name:", product["name"], - "\nPrice:", product["price"], - "\nLink:", product["link"], - "\nImage:", product["product_photo"], - "\n\n") + for index, product in enumerate(products, 1): + print(f"Item {index}:") + print(f"Name: {product['name']}") + print(f"Price: {product['price']}") + print(f"Link: {product['link']}") + print(f"Image: {product['product_photo']}") + print("-" * 60) # visual separator From e38f0256dae969ac2bf36bfba92c1a6b63346ce7 Mon Sep 17 00:00:00 2001 From: Maximus Slabon Date: Sun, 23 Nov 2025 18:45:20 -0500 Subject: [PATCH 39/72] added get_next --- website_modules/main_module.py | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/website_modules/main_module.py b/website_modules/main_module.py index 959850b..b1c46df 100644 --- a/website_modules/main_module.py +++ b/website_modules/main_module.py @@ -74,6 +74,25 @@ def run_module_search(module: str, search_term: str, ): print(f'\t{e}\n') return None +# func for thread to execute +def run_module_next_page(module: str, search_term: str, page_num: int): + if not hasattr(module, 'get_next_page'): + return None + + try: + get_next_page = getattr(module, 'get_next_page') + product_results_json = get_next_page(search_term, page_num) + + # convert to dict + prods = json.loads(product_results_json) + return(prods) + + except Exception as e: + print(f'ERROR with {module}\n') + print(f'\tFunction: [search_product(str) -> json]') + print(f'\t{e}\n') + return None + def search_product(search_term: str): search_results = [] @@ -94,6 +113,25 @@ def search_product(search_term: str): res['results'] = search_results return res +def get_next_page(search_term: str, page_num: int): + search_results = [] + + with ThreadPoolExecutor(max_workers=MAX_THREADS, thread_name_prefix=THREAD_PREFIX) as executor: + ## exectute + future = { + executor.submit(run_module_next_page, module, search_term, page_num): module + for module in search_modules + } + + for future in as_completed(future): + result = future.result() + if result is not None: + search_results.extend(result) + # has to return a proper python dictionary + res = {} + res['results'] = search_results + return res + def main(): search_term = "" if len(sys.argv) > 1: From f289d72e9aba40d69c323f0a487b70ed292d537a Mon Sep 17 00:00:00 2001 From: Aryaman Arora Date: Sun, 23 Nov 2025 18:49:48 -0500 Subject: [PATCH 40/72] Update amazon scraper with image-link fallback and modular structure --- website_modules/amazon_final.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/website_modules/amazon_final.py b/website_modules/amazon_final.py index 1fdb6f0..b74fccb 100644 --- a/website_modules/amazon_final.py +++ b/website_modules/amazon_final.py @@ -128,9 +128,6 @@ def search_products(search_term: str) -> list: return products - - - # main function for use as a script if __name__ == "__main__": SEARCH_TERM = input("Enter search term: ").strip() @@ -139,8 +136,6 @@ def search_products(search_term: str) -> list: html_content = get_HTML(search_url) item_list = parse_and_Dict(html_content) - - products = item_list if not products: From b92a64a4ce45d843f7a6df21818ea5a5e5323875 Mon Sep 17 00:00:00 2001 From: Aryaman Arora Date: Sun, 23 Nov 2025 18:56:01 -0500 Subject: [PATCH 41/72] added amazon module to main modules --- finalAmzSoup.html | 3369 ++++++++++++++++++++++---------- website_modules/main_module.py | 6 +- 2 files changed, 2342 insertions(+), 1033 deletions(-) diff --git a/finalAmzSoup.html b/finalAmzSoup.html index 3d066cc..c13bfec 100644 --- a/finalAmzSoup.html +++ b/finalAmzSoup.html @@ -26,12 +26,12 @@ pec:0,ts:0,erl:[],ter:[],buffer:[],mxe:50,startTimer:function(){a.ts++;setInterval(function(){d.ue&&a.pec - - + + @@ -255,7 +255,7 @@ (window.AmazonUIPageJS ? AmazonUIPageJS : P).load.js('https://m.media-amazon.com/images/I/11mVszy8FIL.js?AUIClients/AmazonRushAssetLoader'); (window.AmazonUIPageJS ? AmazonUIPageJS : P).load.js('https://m.media-amazon.com/images/I/41k9TQrnHzL.js?AUIClients/AmazonRushFramework'); (window.AmazonUIPageJS ? AmazonUIPageJS : P).load.js('https://m.media-amazon.com/images/I/21cuxCuJB9L.js?AUIClients/AmazonRushRouter'); - (window.AmazonUIPageJS ? AmazonUIPageJS : P).load.js('https://m.media-amazon.com/images/I/1171acCLbXL._RC|31e161GOieL.js,31Y5AkqmNvL.js,01Fkn0wywQL.js,41hWhFpMP2L.js,41Kp-f+BVbL.js,41jdi0EqdCL.js,01ppWCylHHL.js,21Keh4EJl5L.js,011Ixtzv5ZL.js,11R3TFu5yIL.js,012cC7IivfL.js,01t3yn7wswL.js,11A2Y-YrPNL.js,11bfQzAbBYL.js,11LWBLxZGFL.js,01zOeEPGZVL.js,11EAjA9pEDL.js,01psJA6cYcL.js,21LWR5MuOeL.js,01-VtLVctFL.js,21wpW3i0JEL.js,31i9obf3QtL.js,01Hv3vBBwVL.js,01QUe9fZkCL.js_.js?AUIClients/SearchAssets#1275412-T1.1300981-T1.1312106-T1.1304942-T1'); + (window.AmazonUIPageJS ? AmazonUIPageJS : P).load.js('https://m.media-amazon.com/images/I/1171acCLbXL._RC|31e161GOieL.js,31Y5AkqmNvL.js,01Fkn0wywQL.js,41hWhFpMP2L.js,41Kp-f+BVbL.js,41jdi0EqdCL.js,01ppWCylHHL.js,21Keh4EJl5L.js,011Ixtzv5ZL.js,11R3TFu5yIL.js,012cC7IivfL.js,01t3yn7wswL.js,11RZ+epQ1iL.js,11bfQzAbBYL.js,11LWBLxZGFL.js,01zOeEPGZVL.js,11EAjA9pEDL.js,01psJA6cYcL.js,21LWR5MuOeL.js,01-VtLVctFL.js,21wpW3i0JEL.js,31i9obf3QtL.js,01Hv3vBBwVL.js,01QUe9fZkCL.js_.js?AUIClients/SearchAssets#1300981-T1.1312106-T1.1304942-T1'); (window.AmazonUIPageJS ? AmazonUIPageJS : P).load.js('https://m.media-amazon.com/images/I/51SSMvLbyWL._RC|51QdbHLlEUL.js,21RoFlDNKHL.js,41fFmJrK7zL.js,01N0G7oHRlL.js,31-hnA-p85L.js,21MQrCW40bL.js,01KVpvQDfZL.js,21XkditqCiL.js,21oa8AG4VxL.js,0175nbnAD4L.js,01TF4KwVp5L.js,31X4VtYohlL.js,01qtTpqN9TL.js,21izuSy0KuL.js,01QUqxbk4aL.js,018GOBOU8wL.js,014s6GxYjXL.js,01mq4gWWg+L.js,01Oy5H5EPsL.js,01bOMvXgu4L.js,210NxcRa2yL.js,01Qg3w8pUPL.js,01lx4jtyKqL.js,01+86qUwmpL.js,01tvS9Qe2bL.js,01+xQYdGnAL.js,01AvqPMFuiL.js,31koNc6s8bL.js,41VQTbCA1+L.js,21E+PtC+ukL.js,0136xjSLaaL.js,01hMcyEA2RL.js,01XYfBmJ5BL.js,315OqIvkCfL.js,01jvUNUfNdL.js,01NwzGdaqAL.js,21dIRMMiDDL.js,51m9FvkngBL.js,31jzxcc0Q6L.js,01ly-sHeg8L.js,018v226z-QL.js,21z34K3nOVL.js,31M8YAO7NgL.js,01993rJ5roL.js,01SiFpsoIzL.js,21Hswl2Ng8L.js,11AnWPET7nL.js,01DQUzT6S8L.js,01sFQp1iloL.js,01XS6qpQiCL.js,01S5dga6DzL.js,01LbPmN08WL.js,01ppIh9wmjL.js,01v8HNSpzuL.js,01PTeQhvs-L.js,01GsEhoWBNL.js,014nwk4ykeL.js,012USBOk43L.js,21OG3u9bUQL.js,11lN9sWb9SL.js,01bccP7NUYL.js,81it-4YuY1L.js_.js?AUIClients/SearchPartnerAssets#1045532-T1.1285117-T1'); 'use strict';(function(h){var k=window.AmazonUIPageJS||window.P,f=k._namespace||k.attributeErrors,e=f?f("GenericLatencyMeasure",""):k;e.guardFatal?e.guardFatal(h)(e,window):e.execute(function(){h(e,window)})})(function(h,k,f){(function(e,k){function q(b){function m(a){return g(a,(g(a)||0)+1)}function n(a,c){e.ueLogError&&e.ueLogError(a,{logLevel:"ERROR",attribution:"doGenericPageLatencyMeasurement."+c});m("GenericLatencyMeasurement.JavascriptException");m("GenericLatencyMeasurement.JavascriptException."+ c)}function p(a){return a?a.constructor===Array?a:Array.prototype.slice.call(a):[]}function r(a){var c,b={time:-1},e;for(c=0;cb.time&&(b.time=e.time,b.counter=e.idx);return-1c.time&&(c.time=b.time);c.counter++;var e=b.elem.naturalWidth;b=b.elem.naturalHeight;c.pixels+=e&&b?e*b:0}return-1 -Amazon.ca : coffee +Amazon.ca : headphones - + @@ -341,7 +341,7 @@
- + @@ -526,11 +526,11 @@
- -
- - + -