Compare commits
3 Commits
bba7362641
...
cff176e7a3
| Author | SHA1 | Date | |
|---|---|---|---|
| cff176e7a3 | |||
| 7f7e6fdd1f | |||
| 45a52cbc33 |
315
inventory-server/package-lock.json
generated
315
inventory-server/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.12.0",
|
||||
"openai": "^4.85.3",
|
||||
"pm2": "^5.3.0",
|
||||
"ssh2": "^1.16.0",
|
||||
"uuid": "^9.0.1"
|
||||
@@ -100,6 +101,27 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pm2/agent/node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@pm2/io": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz",
|
||||
@@ -238,6 +260,27 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pm2/js-api/node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@pm2/pm2-version-check": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz",
|
||||
@@ -276,6 +319,37 @@
|
||||
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.76",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.76.tgz",
|
||||
"integrity": "sha512-yvR7Q9LdPz2vGpmpJX5LolrgRdWvB67MJKDPSgIIzpFbaf9a1j/f5DnLp5VDyHGMR0QZHlTr1afsD87QCXFHKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -298,6 +372,18 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
||||
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/amp": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
|
||||
@@ -401,6 +487,12 @@
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
@@ -648,6 +740,18 @@
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
|
||||
@@ -801,6 +905,15 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
@@ -912,6 +1025,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -991,6 +1119,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter2": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz",
|
||||
@@ -1114,6 +1251,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/formdata-node": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-domexception": "1.0.0",
|
||||
"web-streams-polyfill": "4.0.0-beta.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1295,6 +1466,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -1395,6 +1581,15 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -1857,6 +2052,45 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
|
||||
@@ -1995,6 +2229,36 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.85.3",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.85.3.tgz",
|
||||
"integrity": "sha512-KTMXAK6FPd2IvsPtglMt0J1GyVrjMxCYzu/mVbCPabzzquSJoZlYpHtE0p0ScZPyt11XTc757xSO4j39j5g+Xw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pac-proxy-agent": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz",
|
||||
@@ -2996,6 +3260,12 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
|
||||
@@ -3062,6 +3332,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -3132,17 +3408,44 @@
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "4.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"express": "^4.18.2",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.12.0",
|
||||
"openai": "^4.85.3",
|
||||
"pm2": "^5.3.0",
|
||||
"ssh2": "^1.16.0",
|
||||
"uuid": "^9.0.1"
|
||||
|
||||
17
inventory-server/src/prompts/product-validation.txt
Normal file
17
inventory-server/src/prompts/product-validation.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
I will provide a JSON array with product data. Process the array by combining all products from validData and invalidData arrays into a single array, excluding any fields starting with “__”, such as “__index” or “__errors”. Process each product according to the reference guidelines below. If a field is not included in the data, do not include it in your response unless the specific field guidelines below say otherwise. Please respond with:
|
||||
|
||||
Respond in the following JSON format:
|
||||
{
|
||||
"correctedData": [], // Array of corrected products
|
||||
"changes": [], // Array of strings describing each change made
|
||||
"warnings": [] // Array of strings with warnings or suggestions for manual review
|
||||
}
|
||||
|
||||
Using the provided guidelines, focus on:
|
||||
1. Correcting typos and any incorrect spelling or grammar
|
||||
2. Standardizing product names
|
||||
3. Correcting and enhancing descriptions by adding details, keywords, and SEO-friendly language
|
||||
4. Fixing any obvious errors in measurements, prices, or quantities
|
||||
5. Adding correct categories, themes, and colors
|
||||
|
||||
Use only the provided data and your own knowledge to make changes. Do not make assumptions or make up information that you're not sure about. If you're unable to make a change you're confident about, leave the field as is.
|
||||
330
inventory-server/src/routes/ai-validation.js
Normal file
330
inventory-server/src/routes/ai-validation.js
Normal file
@@ -0,0 +1,330 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const OpenAI = require('openai');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY
|
||||
});
|
||||
|
||||
// Cache configuration
|
||||
const CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||
|
||||
// Cache structure with TTL
|
||||
let cache = {
|
||||
taxonomyData: null,
|
||||
validationPrompt: null,
|
||||
lastUpdated: null
|
||||
};
|
||||
|
||||
// Function to check if cache is valid
|
||||
function isCacheValid() {
|
||||
return cache.lastUpdated && (Date.now() - cache.lastUpdated) < CACHE_TTL;
|
||||
}
|
||||
|
||||
// Function to clear cache
|
||||
function clearCache() {
|
||||
cache = {
|
||||
taxonomyData: null,
|
||||
validationPrompt: null,
|
||||
lastUpdated: null
|
||||
};
|
||||
}
|
||||
|
||||
// Debug endpoint to view prompt and cache status
|
||||
router.get('/debug', async (req, res) => {
|
||||
try {
|
||||
console.log('Debug endpoint called');
|
||||
const pool = req.app.locals.pool;
|
||||
|
||||
// Load taxonomy data first
|
||||
console.log('Loading taxonomy data...');
|
||||
const taxonomy = await getTaxonomyData(pool);
|
||||
console.log('Taxonomy data loaded:', {
|
||||
categoriesCount: taxonomy.categories.length,
|
||||
themesCount: taxonomy.themes.length,
|
||||
colorsCount: taxonomy.colors.length,
|
||||
taxCodesCount: taxonomy.taxCodes.length,
|
||||
sizeCategoriesCount: taxonomy.sizeCategories.length
|
||||
});
|
||||
|
||||
// Then load the prompt
|
||||
console.log('Loading prompt...');
|
||||
const currentPrompt = await loadPrompt(pool);
|
||||
const sampleData = [{ name: "Sample Product" }];
|
||||
const fullPrompt = currentPrompt + '\n' + JSON.stringify(sampleData, null, 2);
|
||||
|
||||
const response = {
|
||||
cacheStatus: {
|
||||
isCacheValid: isCacheValid(),
|
||||
lastUpdated: cache.lastUpdated ? new Date(cache.lastUpdated).toISOString() : null,
|
||||
timeUntilExpiry: cache.lastUpdated ?
|
||||
Math.max(0, CACHE_TTL - (Date.now() - cache.lastUpdated)) / 1000 + ' seconds' :
|
||||
'expired',
|
||||
},
|
||||
taxonomyStats: taxonomy ? {
|
||||
categories: countItems(taxonomy.categories),
|
||||
themes: taxonomy.themes.length,
|
||||
colors: taxonomy.colors.length,
|
||||
taxCodes: taxonomy.taxCodes.length,
|
||||
sizeCategories: taxonomy.sizeCategories.length
|
||||
} : null,
|
||||
basePrompt: currentPrompt,
|
||||
sampleFullPrompt: fullPrompt,
|
||||
promptLength: fullPrompt.length,
|
||||
};
|
||||
|
||||
console.log('Sending response with stats:', response.taxonomyStats);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('Debug endpoint error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to count total items in hierarchical structure
|
||||
function countItems(items) {
|
||||
return items.reduce((count, item) => {
|
||||
return count + 1 + (item.subcategories ? countItems(item.subcategories) : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Force cache refresh endpoint
|
||||
router.post('/refresh-cache', async (req, res) => {
|
||||
try {
|
||||
clearCache();
|
||||
const pool = req.app.locals.pool;
|
||||
await loadPrompt(pool); // This will rebuild the cache
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Cache refreshed successfully',
|
||||
newCacheTime: new Date(cache.lastUpdated).toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Cache refresh error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Function to fetch and format taxonomy data
|
||||
async function getTaxonomyData(pool) {
|
||||
if (cache.taxonomyData && isCacheValid()) {
|
||||
return cache.taxonomyData;
|
||||
}
|
||||
|
||||
// Fetch categories with hierarchy
|
||||
const [categories] = await pool.query(`
|
||||
SELECT cat_id, name, master_cat_id, level_order
|
||||
FROM (
|
||||
SELECT cat_id,name,NULL AS master_cat_id,1 AS level_order
|
||||
FROM product_categories s
|
||||
WHERE type=10
|
||||
UNION ALL
|
||||
SELECT c.cat_id,c.name,c.master_cat_id,2 AS level_order
|
||||
FROM product_categories c
|
||||
JOIN product_categories s ON c.master_cat_id=s.cat_id
|
||||
WHERE c.type=11 AND s.type=10
|
||||
UNION ALL
|
||||
SELECT sc.cat_id,sc.name,sc.master_cat_id,3 AS level_order
|
||||
FROM product_categories sc
|
||||
JOIN product_categories c ON sc.master_cat_id=c.cat_id
|
||||
JOIN product_categories s ON c.master_cat_id=s.cat_id
|
||||
WHERE sc.type=12 AND c.type=11 AND s.type=10
|
||||
UNION ALL
|
||||
SELECT ssc.cat_id,ssc.name,ssc.master_cat_id,4 AS level_order
|
||||
FROM product_categories ssc
|
||||
JOIN product_categories sc ON ssc.master_cat_id=sc.cat_id
|
||||
JOIN product_categories c ON sc.master_cat_id=c.cat_id
|
||||
JOIN product_categories s ON c.master_cat_id=s.cat_id
|
||||
WHERE ssc.type=13 AND sc.type=12 AND c.type=11 AND s.type=10
|
||||
) AS hierarchy
|
||||
ORDER BY level_order,cat_id
|
||||
`);
|
||||
|
||||
// Fetch themes with hierarchy
|
||||
const [themes] = await pool.query(`
|
||||
SELECT cat_id, name, master_cat_id, level_order
|
||||
FROM (
|
||||
SELECT t.cat_id,t.name,null as master_cat_id,1 AS level_order
|
||||
FROM product_categories t
|
||||
WHERE t.type=20
|
||||
UNION ALL
|
||||
SELECT ts.cat_id,ts.name,ts.master_cat_id,2 AS level_order
|
||||
FROM product_categories ts
|
||||
JOIN product_categories t ON ts.master_cat_id=t.cat_id
|
||||
WHERE ts.type=21 AND t.type=20
|
||||
) AS hierarchy
|
||||
ORDER BY level_order,name
|
||||
`);
|
||||
|
||||
// Fetch colors
|
||||
const [colors] = await pool.query('SELECT color, name FROM product_color_list ORDER BY name');
|
||||
|
||||
// Fetch tax codes
|
||||
const [taxCodes] = await pool.query('SELECT tax_code_id, name FROM product_tax_codes ORDER BY name');
|
||||
|
||||
// Fetch size categories
|
||||
const [sizeCategories] = await pool.query('SELECT cat_id, name FROM product_categories WHERE type=50 ORDER BY name');
|
||||
|
||||
// Format categories into a hierarchical structure
|
||||
const formatHierarchy = (items, level = 1, parentId = null) => {
|
||||
return items
|
||||
.filter(item => item.level_order === level && item.master_cat_id === parentId)
|
||||
.map(item => {
|
||||
const children = formatHierarchy(items, level + 1, item.cat_id);
|
||||
return {
|
||||
name: item.name,
|
||||
...(children.length > 0 ? { subcategories: children } : {})
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Format themes similarly but with only two levels
|
||||
const formatThemes = (items) => {
|
||||
return items
|
||||
.filter(item => item.level_order === 1)
|
||||
.map(item => {
|
||||
const subthemes = items
|
||||
.filter(subitem => subitem.master_cat_id === item.cat_id)
|
||||
.map(subitem => subitem.name);
|
||||
return {
|
||||
name: item.name,
|
||||
...(subthemes.length > 0 ? { subthemes } : {})
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
cache.taxonomyData = {
|
||||
categories: formatHierarchy(categories),
|
||||
themes: formatThemes(themes),
|
||||
colors: colors.map(c => c.name),
|
||||
taxCodes: (taxCodes || []).map(tc => ({ id: tc.tax_code_id, name: tc.name })),
|
||||
sizeCategories: (sizeCategories || []).map(sc => ({ id: sc.cat_id, name: sc.name }))
|
||||
};
|
||||
cache.lastUpdated = Date.now();
|
||||
|
||||
return cache.taxonomyData;
|
||||
}
|
||||
|
||||
// Load the prompt from file and inject taxonomy data
|
||||
async function loadPrompt(pool) {
|
||||
if (cache.validationPrompt && isCacheValid()) {
|
||||
return cache.validationPrompt;
|
||||
}
|
||||
|
||||
const promptPath = path.join(__dirname, '..', 'prompts', 'product-validation.txt');
|
||||
const basePrompt = await fs.readFile(promptPath, 'utf8');
|
||||
|
||||
// Get taxonomy data
|
||||
const taxonomy = await getTaxonomyData(pool);
|
||||
|
||||
// Format taxonomy data for the prompt
|
||||
const taxonomySection = `
|
||||
Available Categories:
|
||||
${JSON.stringify(taxonomy.categories)}
|
||||
|
||||
Available Themes:
|
||||
${JSON.stringify(taxonomy.themes)}
|
||||
|
||||
Available Colors:
|
||||
${JSON.stringify(taxonomy.colors)}
|
||||
|
||||
Available Tax Codes:
|
||||
${JSON.stringify(taxonomy.taxCodes)}
|
||||
|
||||
Available Size Categories:
|
||||
${JSON.stringify(taxonomy.sizeCategories)}
|
||||
|
||||
Here is the product data to validate:`;
|
||||
|
||||
// Combine the prompt sections
|
||||
cache.validationPrompt = basePrompt + '\n' + taxonomySection;
|
||||
cache.lastUpdated = Date.now();
|
||||
|
||||
return cache.validationPrompt;
|
||||
}
|
||||
|
||||
// Set up cache clearing interval
|
||||
setInterval(clearCache, CACHE_TTL);
|
||||
|
||||
router.post('/validate', async (req, res) => {
|
||||
try {
|
||||
const { products } = req.body;
|
||||
console.log('🔍 Received products for validation:', JSON.stringify(products, null, 2));
|
||||
|
||||
if (!Array.isArray(products)) {
|
||||
console.error('❌ Invalid input: products is not an array');
|
||||
return res.status(400).json({ error: 'Products must be an array' });
|
||||
}
|
||||
|
||||
// Load the prompt and append the products data
|
||||
const basePrompt = await loadPrompt(req.app.locals.pool);
|
||||
const fullPrompt = basePrompt + '\n' + JSON.stringify(products, null, 2);
|
||||
console.log('📝 Generated prompt:', fullPrompt);
|
||||
|
||||
console.log('🤖 Sending request to OpenAI...');
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-4-turbo-preview",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: "You are a specialized e-commerce product data processor for a crafting supplies website tasked with providing complete, correct, appealing, and SEO-friendly product listings. You should write professionally, but in a friendly and engaging tone."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: fullPrompt
|
||||
}
|
||||
],
|
||||
temperature: 0.3,
|
||||
max_tokens: 4000,
|
||||
response_format: { type: "json_object" }
|
||||
});
|
||||
|
||||
console.log('✅ Received response from OpenAI');
|
||||
const rawResponse = completion.choices[0].message.content;
|
||||
console.log('📄 Raw AI response:', rawResponse);
|
||||
|
||||
const aiResponse = JSON.parse(rawResponse);
|
||||
console.log('🔄 Parsed AI response:', JSON.stringify(aiResponse, null, 2));
|
||||
|
||||
// Compare original and corrected data
|
||||
if (aiResponse.correctedData) {
|
||||
console.log('📊 Changes summary:');
|
||||
products.forEach((original, index) => {
|
||||
const corrected = aiResponse.correctedData[index];
|
||||
if (corrected) {
|
||||
const changes = Object.keys(corrected).filter(key =>
|
||||
JSON.stringify(original[key]) !== JSON.stringify(corrected[key])
|
||||
);
|
||||
if (changes.length > 0) {
|
||||
console.log(`\nProduct ${index + 1} changes:`);
|
||||
changes.forEach(key => {
|
||||
console.log(` ${key}:`);
|
||||
console.log(` - Original: ${JSON.stringify(original[key])}`);
|
||||
console.log(` - Corrected: ${JSON.stringify(corrected[key])}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
...aiResponse
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ AI Validation Error:', error);
|
||||
console.error('Error details:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message || 'Error during AI validation'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -18,6 +18,7 @@ const vendorsRouter = require('./routes/vendors');
|
||||
const categoriesRouter = require('./routes/categories');
|
||||
const testConnectionRouter = require('./routes/test-connection');
|
||||
const importRouter = require('./routes/import');
|
||||
const aiValidationRouter = require('./routes/ai-validation');
|
||||
|
||||
// Get the absolute path to the .env file
|
||||
const envPath = path.resolve(process.cwd(), '.env');
|
||||
@@ -93,6 +94,7 @@ async function startServer() {
|
||||
app.use('/api/vendors', vendorsRouter);
|
||||
app.use('/api/categories', categoriesRouter);
|
||||
app.use('/api/import', importRouter);
|
||||
app.use('/api/ai-validation', aiValidationRouter);
|
||||
app.use('/api', testConnectionRouter);
|
||||
|
||||
// Basic health check route
|
||||
|
||||
@@ -15,8 +15,9 @@ import { RequireAuth } from './components/auth/RequireAuth';
|
||||
import Forecasting from "@/pages/Forecasting";
|
||||
import { Vendors } from '@/pages/Vendors';
|
||||
import { Categories } from '@/pages/Categories';
|
||||
import { Import } from '@/pages/import/Import';
|
||||
import { Import } from '@/pages/Import';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { AiValidationDebug } from "@/pages/AiValidationDebug"
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -71,6 +72,7 @@ function App() {
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/forecasting" element={<Forecasting />} />
|
||||
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -35,7 +35,7 @@ export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>
|
||||
</Button>
|
||||
<div
|
||||
className="vertical-text font-medium text-muted-foreground"
|
||||
style={{ writingMode: 'vertical-rl', textOrientation: 'mixed', transform: 'rotate(180deg)' }}
|
||||
style={{ writingMode: 'vertical-rl', textOrientation: 'mixed', transform: 'rotate(0deg)' }}
|
||||
>
|
||||
{header}
|
||||
</div>
|
||||
|
||||
@@ -11,11 +11,12 @@ export const findMatch = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
autoMapDistance: number,
|
||||
): T | undefined => {
|
||||
const headerLower = header.toLowerCase()
|
||||
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {
|
||||
const distance = Math.min(
|
||||
...[
|
||||
lavenstein(field.key, header),
|
||||
...(field.alternateMatches?.map((alternate) => lavenstein(alternate, header)) || []),
|
||||
lavenstein(field.key.toLowerCase(), headerLower),
|
||||
...(field.alternateMatches?.map((alternate) => lavenstein(alternate.toLowerCase(), headerLower)) || []),
|
||||
],
|
||||
)
|
||||
return distance < acc.distance || acc.distance === undefined
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useMemo, useState, useEffect } from "react"
|
||||
import { useCallback, useMemo, useState, useEffect, memo } from "react"
|
||||
import { useRsi } from "../../hooks/useRsi"
|
||||
import type { Meta } from "./types"
|
||||
import { addErrorsAndRunHooks } from "./utils/dataMutations"
|
||||
import type { Data, Field, SelectOption, MultiInput } from "../../types"
|
||||
import { Check, ChevronsUpDown, ArrowDown, AlertCircle } from "lucide-react"
|
||||
import type { Data, SelectOption } from "../../types"
|
||||
import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2 } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Command,
|
||||
@@ -56,12 +56,15 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import * as Select from "@radix-ui/react-select"
|
||||
import config from "@/config"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
|
||||
type Props<T extends string> = {
|
||||
initialData: (Data<T> & Meta)[]
|
||||
@@ -69,19 +72,80 @@ type Props<T extends string> = {
|
||||
onBack?: () => void
|
||||
}
|
||||
|
||||
type CellProps = {
|
||||
value: any,
|
||||
onChange: (value: any) => void,
|
||||
error?: { level: string, message: string },
|
||||
field: Field<string>
|
||||
type BaseFieldType = {
|
||||
multiline?: boolean;
|
||||
price?: boolean;
|
||||
}
|
||||
|
||||
const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
type InputFieldType = BaseFieldType & {
|
||||
type: "input";
|
||||
}
|
||||
|
||||
type MultiInputFieldType = BaseFieldType & {
|
||||
type: "multi-input";
|
||||
separator?: string;
|
||||
}
|
||||
|
||||
type SelectFieldType = {
|
||||
type: "select" | "multi-select";
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
type CheckboxFieldType = {
|
||||
type: "checkbox";
|
||||
booleanMatches?: { [key: string]: boolean };
|
||||
}
|
||||
|
||||
type FieldType = InputFieldType | MultiInputFieldType | SelectFieldType | CheckboxFieldType;
|
||||
|
||||
type Field<T extends string> = {
|
||||
label: string;
|
||||
key: T;
|
||||
description?: string;
|
||||
alternateMatches?: string[];
|
||||
validations?: ({ rule: string } & Record<string, any>)[];
|
||||
fieldType: FieldType;
|
||||
width?: number;
|
||||
disabled?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
type CellProps = {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
error?: { level: string; message: string };
|
||||
field: Field<string>;
|
||||
}
|
||||
|
||||
// Define ValidationIcon before EditableCell
|
||||
const ValidationIcon = memo(({ error }: { error: { level: string, message: string } }) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="absolute right-2 top-2 text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{error.message}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))
|
||||
|
||||
// Wrap EditableCell with memo to avoid unnecessary re-renders
|
||||
const EditableCell = memo(({ value, onChange, error, field }: CellProps) => {
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [inputValue, setInputValue] = useState(value ?? "")
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [localValues, setLocalValues] = useState<string[]>([])
|
||||
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
const commandList = e.currentTarget;
|
||||
commandList.scrollTop += e.deltaY;
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
// Update input value when external value changes and we're not editing
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
@@ -97,6 +161,12 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
}
|
||||
}, [value, field.fieldType.type])
|
||||
|
||||
const formatPrice = (value: string) => {
|
||||
if (!value) return value
|
||||
// Remove dollar signs and trim
|
||||
return value.replace(/^\$/, '').trim()
|
||||
}
|
||||
|
||||
const validateRegex = (val: any) => {
|
||||
// Handle non-string values
|
||||
if (val === undefined || val === null) return undefined
|
||||
@@ -115,8 +185,14 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
|
||||
const regexValidation = field.validations?.find(v => v.rule === "regex")
|
||||
if (regexValidation) {
|
||||
let testValue = strVal
|
||||
// For price fields, remove dollar sign before testing
|
||||
if (isPriceField(field.fieldType)) {
|
||||
testValue = formatPrice(strVal)
|
||||
}
|
||||
|
||||
const regex = new RegExp(regexValidation.value, regexValidation.flags)
|
||||
if (!regex.test(strVal)) {
|
||||
if (!regex.test(testValue)) {
|
||||
return { level: regexValidation.level || "error", message: regexValidation.errorMessage }
|
||||
}
|
||||
}
|
||||
@@ -134,10 +210,10 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
const currentError = getValidationError()
|
||||
|
||||
const getDisplayValue = (value: any, fieldType: Field<string>["fieldType"]) => {
|
||||
if (fieldType.type === "select") {
|
||||
return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value
|
||||
}
|
||||
if (fieldType.type === "multi-select") {
|
||||
if (fieldType.type === "select" || fieldType.type === "multi-select") {
|
||||
if (fieldType.type === "select") {
|
||||
return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(v => fieldType.options.find((opt: SelectOption) => opt.value === v)?.label || v).join(", ")
|
||||
}
|
||||
@@ -160,6 +236,11 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle price fields
|
||||
if (isPriceField(field.fieldType)) {
|
||||
newValue = formatPrice(newValue)
|
||||
}
|
||||
|
||||
// Always commit the value
|
||||
onChange(newValue)
|
||||
|
||||
@@ -172,27 +253,6 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
const ValidationIcon = ({ error }: { error: { level: string, message: string } }) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="absolute right-2 top-2 text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{error.message}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
const handleWheel = (e: React.WheelEvent) => {
|
||||
const commandList = e.currentTarget;
|
||||
commandList.scrollTop += e.deltaY;
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
switch (field.fieldType.type) {
|
||||
case "select":
|
||||
@@ -353,8 +413,59 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
)
|
||||
case "input":
|
||||
case "multi-input":
|
||||
default:
|
||||
if (field.fieldType.multiline) {
|
||||
return (
|
||||
<div className="relative" id={`cell-${field.key}`}>
|
||||
<Popover
|
||||
open={isEditing}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
validateAndCommit(inputValue)
|
||||
setIsEditing(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-[36px] justify-start font-normal"
|
||||
>
|
||||
<div className="truncate">
|
||||
{inputValue || "Click to edit..."}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[400px] p-3"
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-sm">{field.label}</h3>
|
||||
<textarea
|
||||
className="w-full min-h-[150px] p-2 border rounded-md"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
if (validateAndCommit(inputValue)) {
|
||||
setIsEditing(false)
|
||||
}
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
placeholder={`Press ${navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}+Enter to save`}
|
||||
/>
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="relative" id={`cell-${field.key}`}>
|
||||
<Input
|
||||
@@ -365,9 +476,9 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (field.fieldType.type === "multi-input") {
|
||||
const separator = (field.fieldType as MultiInput).separator || ","
|
||||
const values = inputValue.split(separator).map((v: string) => v.trim()).filter(Boolean)
|
||||
if (isMultiInputType(field.fieldType)) {
|
||||
const separator = getMultiInputSeparator(field.fieldType);
|
||||
const values = inputValue.split(separator).map((v: string) => v.trim()).filter(Boolean);
|
||||
if (validateAndCommit(values.join(separator))) {
|
||||
setIsEditing(false)
|
||||
}
|
||||
@@ -381,9 +492,47 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
currentError ? "border-destructive" : ""
|
||||
)}
|
||||
autoFocus
|
||||
placeholder={field.fieldType.type === "multi-input"
|
||||
? `Enter values separated by ${(field.fieldType as MultiInput).separator || ","}`
|
||||
: undefined}
|
||||
placeholder={
|
||||
isMultiInputType(field.fieldType)
|
||||
? `Enter values separated by ${getMultiInputSeparator(field.fieldType)}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className="relative" id={`cell-${field.key}`}>
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value)
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (isMultiInputType(field.fieldType)) {
|
||||
const separator = getMultiInputSeparator(field.fieldType);
|
||||
const values = inputValue.split(separator).map((v: string) => v.trim()).filter(Boolean);
|
||||
if (validateAndCommit(values.join(separator))) {
|
||||
setIsEditing(false)
|
||||
}
|
||||
} else if (validateAndCommit(inputValue)) {
|
||||
setIsEditing(false)
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"w-full",
|
||||
currentError ? "border-destructive" : ""
|
||||
)}
|
||||
autoFocus
|
||||
placeholder={
|
||||
isMultiInputType(field.fieldType)
|
||||
? `Enter values separated by ${getMultiInputSeparator(field.fieldType)}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
@@ -397,38 +546,79 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
|
||||
id={`cell-${field.key}`}
|
||||
onClick={(e) => {
|
||||
if (field.fieldType.type !== "checkbox" && !field.disabled) {
|
||||
e.stopPropagation() // Prevent event bubbling
|
||||
e.stopPropagation()
|
||||
setIsEditing(true)
|
||||
setInputValue(Array.isArray(value) ? value.join(", ") : value ?? "")
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"relative min-h-[36px] cursor-text p-2 rounded-md border bg-background",
|
||||
(field.fieldType.type === "input" || field.fieldType.type === "multi-input") &&
|
||||
field.fieldType.multiline && "max-h-[100px] overflow-y-auto",
|
||||
currentError ? "border-destructive" : "border-input",
|
||||
field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between",
|
||||
field.disabled && "opacity-50 cursor-not-allowed bg-muted"
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex-1 overflow-hidden text-ellipsis", !value && "text-muted-foreground")}>
|
||||
{value ? getDisplayValue(value, field.fieldType) : ""}
|
||||
</div>
|
||||
{((field.fieldType.type === "input" || field.fieldType.type === "multi-input") && field.fieldType.multiline) ? (
|
||||
<div className={cn(
|
||||
"flex-1 overflow-hidden",
|
||||
!value && "text-muted-foreground"
|
||||
)}>
|
||||
<div className="line-clamp-2 whitespace-pre-wrap">
|
||||
{value || ""}
|
||||
</div>
|
||||
{value && value.length > 100 && (
|
||||
<Button
|
||||
variant="link"
|
||||
className="h-4 p-0 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsEditing(true)
|
||||
}}
|
||||
>
|
||||
Show more...
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("flex-1 overflow-hidden text-ellipsis", !value && "text-muted-foreground")}>
|
||||
{value ? getDisplayValue(value, field.fieldType) : ""}
|
||||
</div>
|
||||
)}
|
||||
{(field.fieldType.type === "select" || field.fieldType.type === "multi-select") && (
|
||||
<ChevronsUpDown className={cn("h-4 w-4 shrink-0", field.disabled ? "opacity-30" : "opacity-50")} />
|
||||
)}
|
||||
{currentError && <ValidationIcon error={currentError} />}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
// Update type guard to be more specific
|
||||
function isMultiInputType(fieldType: FieldType): fieldType is MultiInputFieldType {
|
||||
return fieldType.type === "multi-input";
|
||||
}
|
||||
|
||||
// Add this component for the column header with copy down functionality
|
||||
const ColumnHeader = <T extends string>({
|
||||
function getMultiInputSeparator(fieldType: FieldType): string {
|
||||
if (isMultiInputType(fieldType)) {
|
||||
return fieldType.separator || ",";
|
||||
}
|
||||
return ",";
|
||||
}
|
||||
|
||||
function isPriceField(fieldType: FieldType): fieldType is (InputFieldType | MultiInputFieldType) & { price: true } {
|
||||
return (fieldType.type === "input" || fieldType.type === "multi-input") && 'price' in fieldType && fieldType.price === true;
|
||||
}
|
||||
|
||||
// Wrap ColumnHeader with memo so that it re-renders only when its props change
|
||||
const ColumnHeader = memo(({
|
||||
field,
|
||||
data,
|
||||
onCopyDown
|
||||
}: {
|
||||
field: Field<T>,
|
||||
data: (Data<T> & Meta)[],
|
||||
onCopyDown: (key: T) => void
|
||||
field: Field<string>
|
||||
data: (Data<string> & Meta)[]
|
||||
onCopyDown: (key: string) => void
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -442,7 +632,7 @@ const ColumnHeader = <T extends string>({
|
||||
className="h-4 w-4 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onCopyDown(field.key as T)
|
||||
onCopyDown(field.key)
|
||||
}}
|
||||
title="Copy first row's value down"
|
||||
>
|
||||
@@ -451,10 +641,10 @@ const ColumnHeader = <T extends string>({
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Add this component for the copy down confirmation dialog
|
||||
const CopyDownDialog = ({
|
||||
const CopyDownDialog = memo(({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
@@ -493,7 +683,7 @@ const CopyDownDialog = ({
|
||||
</AlertDialogPortal>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Add type utilities at the top level
|
||||
type DeepReadonlyField<T extends string> = {
|
||||
@@ -523,6 +713,16 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
const [showSubmitAlert, setShowSubmitAlert] = useState(false)
|
||||
const [isSubmitting, setSubmitting] = useState(false)
|
||||
const [copyDownField, setCopyDownField] = useState<{key: T, label: string} | null>(null)
|
||||
const [isAiValidating, setIsAiValidating] = useState(false)
|
||||
const [aiValidationDetails, setAiValidationDetails] = useState<{
|
||||
changes: string[];
|
||||
warnings: string[];
|
||||
isOpen: boolean;
|
||||
}>({
|
||||
changes: [],
|
||||
warnings: [],
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
// Memoize filtered data to prevent recalculation on every render
|
||||
const filteredData = useMemo(() => {
|
||||
@@ -596,18 +796,22 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
<div className="flex items-start justify-center pt-2">
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
@@ -618,9 +822,9 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
header: () => (
|
||||
<div className="group">
|
||||
<ColumnHeader
|
||||
field={field as Field<T>}
|
||||
field={field as Field<string>}
|
||||
data={data}
|
||||
onCopyDown={(key) => copyValueDown(key, field.label)}
|
||||
onCopyDown={(key: string) => copyValueDown(key as T, field.label)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
@@ -659,14 +863,14 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
|
||||
const deleteSelectedRows = () => {
|
||||
const deleteSelectedRows = useCallback(() => {
|
||||
if (Object.keys(rowSelection).length) {
|
||||
const selectedRows = Object.keys(rowSelection).map(Number)
|
||||
const newData = data.filter((_, index) => !selectedRows.includes(index))
|
||||
updateData(newData)
|
||||
setRowSelection({})
|
||||
}
|
||||
}
|
||||
}, [rowSelection, data, updateData]);
|
||||
|
||||
const normalizeValue = useCallback((value: any, field: DeepReadonlyField<T>) => {
|
||||
if (field.fieldType.type === "checkbox") {
|
||||
@@ -694,7 +898,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
return value
|
||||
}, [])
|
||||
|
||||
const submitData = async () => {
|
||||
const submitData = useCallback(async () => {
|
||||
const calculatedData = data.reduce(
|
||||
(acc, value) => {
|
||||
const { __index, __errors, ...values } = value
|
||||
@@ -743,20 +947,97 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
const onContinue = () => {
|
||||
}, [data, fields, file, onClose, onSubmit, normalizeValue, toast, translations]);
|
||||
|
||||
const onContinue = useCallback(() => {
|
||||
const invalidData = data.find((value) => {
|
||||
if (value?.__errors) {
|
||||
return !!Object.values(value.__errors)?.filter((err) => err.level === "error").length
|
||||
return !!Object.values(value.__errors)?.filter((err) => err.level === "error").length;
|
||||
}
|
||||
return false
|
||||
})
|
||||
return false;
|
||||
});
|
||||
if (!invalidData) {
|
||||
submitData()
|
||||
submitData();
|
||||
} else {
|
||||
setShowSubmitAlert(true)
|
||||
setShowSubmitAlert(true);
|
||||
}
|
||||
}
|
||||
}, [data, submitData]);
|
||||
|
||||
// Add AI validation function
|
||||
const handleAiValidation = async () => {
|
||||
try {
|
||||
setIsAiValidating(true);
|
||||
console.log('Sending data for AI validation:', data);
|
||||
|
||||
const response = await fetch(`${config.apiUrl}/ai-validation/validate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ products: data }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('AI validation failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('AI validation response:', result);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'AI validation failed');
|
||||
}
|
||||
|
||||
// Update the data with AI suggestions
|
||||
if (result.correctedData && Array.isArray(result.correctedData)) {
|
||||
// Log the differences
|
||||
data.forEach((original, index) => {
|
||||
const corrected = result.correctedData[index];
|
||||
if (corrected) {
|
||||
const changes = Object.keys(corrected).filter(key => {
|
||||
const originalValue = original[key as keyof typeof original];
|
||||
const correctedValue = corrected[key as keyof typeof corrected];
|
||||
return JSON.stringify(originalValue) !== JSON.stringify(correctedValue);
|
||||
});
|
||||
if (changes.length > 0) {
|
||||
console.log(`Changes for row ${index + 1}:`, changes.map(key => ({
|
||||
field: key,
|
||||
original: original[key as keyof typeof original],
|
||||
corrected: corrected[key as keyof typeof corrected]
|
||||
})));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Preserve the __index and __errors from the original data
|
||||
const newData = result.correctedData.map((item: any, idx: number) => ({
|
||||
...item,
|
||||
__index: data[idx]?.__index,
|
||||
__errors: data[idx]?.__errors,
|
||||
}));
|
||||
|
||||
// Update the data and run validations
|
||||
await updateData(newData);
|
||||
}
|
||||
|
||||
// Show changes and warnings in dialog
|
||||
setAiValidationDetails({
|
||||
changes: result.changes || [],
|
||||
warnings: result.warnings || [],
|
||||
isOpen: true,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('AI Validation Error:', error);
|
||||
toast({
|
||||
title: "AI Validation Error",
|
||||
description: error instanceof Error ? error.message : "An error occurred during AI validation",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsAiValidating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-9.5rem)] flex-col">
|
||||
@@ -793,6 +1074,47 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialog>
|
||||
<Dialog
|
||||
open={aiValidationDetails.isOpen}
|
||||
onOpenChange={(open) => setAiValidationDetails(prev => ({ ...prev, isOpen: open }))}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI Validation Results</DialogTitle>
|
||||
<DialogDescription>
|
||||
Review the changes and warnings suggested by the AI
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[60vh]">
|
||||
{aiValidationDetails.changes.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="font-semibold mb-2">Changes Made:</h3>
|
||||
<ul className="space-y-2">
|
||||
{aiValidationDetails.changes.map((change, i) => (
|
||||
<li key={i} className="flex gap-2">
|
||||
<span className="text-green-500">✓</span>
|
||||
<span>{change}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{aiValidationDetails.warnings.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Warnings:</h3>
|
||||
<ul className="space-y-2">
|
||||
{aiValidationDetails.warnings.map((warning, i) => (
|
||||
<li key={i} className="flex gap-2">
|
||||
<span className="text-yellow-500">⚠</span>
|
||||
<span>{warning}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-8 pt-6">
|
||||
@@ -808,6 +1130,17 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
>
|
||||
{translations.validationStep.discardButtonTitle}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={handleAiValidation}
|
||||
disabled={isAiValidating || data.length === 0}
|
||||
>
|
||||
{isAiValidating && (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
AI Validate
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={filterByErrors}
|
||||
@@ -857,7 +1190,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="p-2"
|
||||
className="p-2 align-top"
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
minWidth: cell.column.getSize(),
|
||||
|
||||
@@ -70,7 +70,7 @@ export type Field<T extends string = string> = {
|
||||
// Validations used for field entries
|
||||
validations?: Validation[]
|
||||
// Field entry component
|
||||
fieldType: Checkbox | Select | Input | MultiInput | MultiSelect
|
||||
fieldType: FieldType
|
||||
// UI-facing values shown to user as field examples pre-upload phase
|
||||
example?: string
|
||||
width?: number
|
||||
@@ -78,17 +78,22 @@ export type Field<T extends string = string> = {
|
||||
onChange?: (value: string) => void
|
||||
}
|
||||
|
||||
export type Checkbox = {
|
||||
type: "checkbox"
|
||||
// Alternate values to be treated as booleans, e.g. {yes: true, no: false}
|
||||
booleanMatches?: { [key: string]: boolean }
|
||||
}
|
||||
|
||||
export type Select = {
|
||||
type: "select"
|
||||
// Options displayed in Select component
|
||||
options: SelectOption[]
|
||||
}
|
||||
export type FieldType =
|
||||
| {
|
||||
type: "input" | "multi-input";
|
||||
multiline?: boolean;
|
||||
price?: boolean;
|
||||
separator?: string;
|
||||
}
|
||||
| {
|
||||
type: "select" | "multi-select";
|
||||
options: readonly SelectOption[];
|
||||
separator?: string;
|
||||
}
|
||||
| {
|
||||
type: "checkbox";
|
||||
booleanMatches?: { readonly [key: string]: boolean };
|
||||
};
|
||||
|
||||
export type SelectOption = {
|
||||
// UI-facing option label
|
||||
|
||||
206
inventory/src/pages/AiValidationDebug.tsx
Normal file
206
inventory/src/pages/AiValidationDebug.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Code } from "@/components/ui/code"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import config from "@/config"
|
||||
|
||||
interface CacheStatus {
|
||||
isCacheValid: boolean
|
||||
lastUpdated: string | null
|
||||
timeUntilExpiry: string
|
||||
}
|
||||
|
||||
interface TaxonomyStats {
|
||||
categories: number
|
||||
themes: number
|
||||
colors: number
|
||||
taxCodes: number
|
||||
sizeCategories: number
|
||||
}
|
||||
|
||||
interface DebugData {
|
||||
cacheStatus: CacheStatus
|
||||
taxonomyStats: TaxonomyStats | null
|
||||
basePrompt: string
|
||||
sampleFullPrompt: string
|
||||
promptLength: number
|
||||
}
|
||||
|
||||
export function AiValidationDebug() {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [debugData, setDebugData] = useState<DebugData | null>(null)
|
||||
const { toast } = useToast()
|
||||
|
||||
const fetchDebugData = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/ai-validation/debug`)
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch debug data')
|
||||
}
|
||||
const data = await response.json()
|
||||
setDebugData(data)
|
||||
} catch (error) {
|
||||
console.error('Error fetching debug data:', error)
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: error instanceof Error ? error.message : "Failed to fetch debug data"
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshCache = async () => {
|
||||
if (!confirm('Are you sure you want to refresh the cache?')) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${config.apiUrl}/ai-validation/refresh-cache`, {
|
||||
method: 'POST'
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to refresh cache')
|
||||
}
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Cache refreshed successfully"
|
||||
})
|
||||
fetchDebugData()
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to refresh cache')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing cache:', error)
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: error instanceof Error ? error.message : "Failed to refresh cache"
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchDebugData()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-bold tracking-tight">AI Validation Debug</h1>
|
||||
<div className="space-x-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={fetchDebugData}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Refresh Data
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={refreshCache}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Force Cache Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{debugData && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Cache Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div>Valid: {debugData.cacheStatus.isCacheValid ? "Yes" : "No"}</div>
|
||||
<div>Last Updated: {debugData.cacheStatus.lastUpdated || "never"}</div>
|
||||
<div>Expires in: {debugData.cacheStatus.timeUntilExpiry}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Taxonomy Stats</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{debugData.taxonomyStats ? (
|
||||
<div className="space-y-2">
|
||||
<div>Categories: {debugData.taxonomyStats.categories}</div>
|
||||
<div>Themes: {debugData.taxonomyStats.themes}</div>
|
||||
<div>Colors: {debugData.taxonomyStats.colors}</div>
|
||||
<div>Tax Codes: {debugData.taxonomyStats.taxCodes}</div>
|
||||
<div>Size Categories: {debugData.taxonomyStats.sizeCategories}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>No taxonomy data available</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Prompt Length</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div>Characters: {debugData.promptLength}</div>
|
||||
<div>Tokens (est.): ~{Math.round(debugData.promptLength / 4)}</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="costPerMillion" className="text-sm text-muted-foreground">
|
||||
Cost per million tokens ($)
|
||||
</label>
|
||||
<input
|
||||
id="costPerMillion"
|
||||
type="number"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
defaultValue="3"
|
||||
onChange={(e) => {
|
||||
const costPerMillion = parseFloat(e.target.value)
|
||||
if (!isNaN(costPerMillion)) {
|
||||
const tokens = Math.round(debugData.promptLength / 4)
|
||||
const cost = (tokens / 1_000_000) * costPerMillion * 100 // Convert to cents
|
||||
const costElement = document.getElementById('tokenCost')
|
||||
if (costElement) {
|
||||
costElement.textContent = cost.toFixed(1)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm">
|
||||
Cost: <span id="tokenCost">{((Math.round(debugData.promptLength / 4) / 1_000_000) * 3 * 100).toFixed(1)}</span>¢
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Full Sample Prompt</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[500px] w-full rounded-md border p-4">
|
||||
<Code className="whitespace-pre-wrap">{debugData.sampleFullPrompt}</Code>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -20,16 +20,16 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select" as const,
|
||||
options: [], // Will be populated from API
|
||||
},
|
||||
width: 200,
|
||||
width: 220,
|
||||
validations: [{ rule: "required" as const, errorMessage: "Required", level: "error" as ErrorLevel }],
|
||||
},
|
||||
{
|
||||
label: "UPC",
|
||||
key: "upc",
|
||||
description: "Universal Product Code/Barcode",
|
||||
alternateMatches: ["barcode", "bar code", "JAN", "EAN"],
|
||||
alternateMatches: ["barcode", "bar code", "jan", "ean"],
|
||||
fieldType: { type: "input" },
|
||||
width: 150,
|
||||
width: 140,
|
||||
validations: [
|
||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
|
||||
@@ -42,7 +42,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
description: "Supplier's product identifier",
|
||||
alternateMatches: ["sku", "item#", "mfg item #", "item"],
|
||||
fieldType: { type: "input" },
|
||||
width: 120,
|
||||
width: 180,
|
||||
validations: [
|
||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
|
||||
@@ -53,7 +53,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
key: "notions_no",
|
||||
description: "Internal notions number",
|
||||
fieldType: { type: "input" },
|
||||
width: 120,
|
||||
width: 110,
|
||||
validations: [
|
||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
|
||||
@@ -66,7 +66,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
description: "Product name/title",
|
||||
alternateMatches: ["sku description"],
|
||||
fieldType: { type: "input" },
|
||||
width: 300,
|
||||
width: 500,
|
||||
validations: [
|
||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||
{ rule: "unique", errorMessage: "Must be unique", level: "error" },
|
||||
@@ -88,14 +88,17 @@ const BASE_IMPORT_FIELDS = [
|
||||
key: "image_url",
|
||||
description: "Product image URL(s)",
|
||||
fieldType: { type: "multi-input" },
|
||||
width: 250,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
label: "MSRP",
|
||||
key: "msrp",
|
||||
description: "Manufacturer's Suggested Retail Price",
|
||||
alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. Retail"],
|
||||
fieldType: { type: "input" },
|
||||
alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. retail","default price"],
|
||||
fieldType: {
|
||||
type: "input",
|
||||
price: true
|
||||
},
|
||||
width: 100,
|
||||
validations: [
|
||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||
@@ -108,7 +111,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
description: "Quantity of items per individual unit",
|
||||
alternateMatches: ["inner pack", "inner", "min qty", "unit qty", "min. order qty"],
|
||||
fieldType: { type: "input" },
|
||||
width: 100,
|
||||
width: 90,
|
||||
validations: [
|
||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" },
|
||||
@@ -119,7 +122,10 @@ const BASE_IMPORT_FIELDS = [
|
||||
key: "cost_each",
|
||||
description: "Wholesale cost per unit",
|
||||
alternateMatches: ["wholesale", "wholesale price"],
|
||||
fieldType: { type: "input" },
|
||||
fieldType: {
|
||||
type: "input",
|
||||
price: true
|
||||
},
|
||||
width: 100,
|
||||
validations: [
|
||||
{ rule: "required", errorMessage: "Required", level: "error" },
|
||||
@@ -130,9 +136,9 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "Case Pack",
|
||||
key: "case_qty",
|
||||
description: "Number of units per case",
|
||||
alternateMatches: ["mc qty"],
|
||||
alternateMatches: ["mc qty","case qty","case pack"],
|
||||
fieldType: { type: "input" },
|
||||
width: 100,
|
||||
width: 50,
|
||||
validations: [
|
||||
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" },
|
||||
],
|
||||
@@ -142,10 +148,10 @@ const BASE_IMPORT_FIELDS = [
|
||||
key: "tax_cat",
|
||||
description: "Product tax category",
|
||||
fieldType: {
|
||||
type: "multi-select",
|
||||
type: "select",
|
||||
options: [], // Will be populated from API
|
||||
},
|
||||
width: 150,
|
||||
width: 180,
|
||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||
},
|
||||
{
|
||||
@@ -167,7 +173,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select",
|
||||
options: [], // Will be populated dynamically based on company selection
|
||||
},
|
||||
width: 150,
|
||||
width: 180,
|
||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||
},
|
||||
{
|
||||
@@ -178,7 +184,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select",
|
||||
options: [], // Will be populated dynamically based on line selection
|
||||
},
|
||||
width: 150,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
label: "Artist",
|
||||
@@ -188,7 +194,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select",
|
||||
options: [], // Will be populated from API
|
||||
},
|
||||
width: 200,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
label: "ETA Date",
|
||||
@@ -202,6 +208,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "Weight",
|
||||
key: "weight",
|
||||
description: "Product weight (in lbs)",
|
||||
alternateMatches: ["weight (lbs.)"],
|
||||
fieldType: { type: "input" },
|
||||
width: 100,
|
||||
validations: [
|
||||
@@ -250,7 +257,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select",
|
||||
options: [], // Will be populated from API
|
||||
},
|
||||
width: 150,
|
||||
width: 190,
|
||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||
},
|
||||
{
|
||||
@@ -270,9 +277,9 @@ const BASE_IMPORT_FIELDS = [
|
||||
description: "Harmonized Tariff Schedule code",
|
||||
alternateMatches: ["taric"],
|
||||
fieldType: { type: "input" },
|
||||
width: 120,
|
||||
width: 130,
|
||||
validations: [
|
||||
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", level: "error" },
|
||||
{ rule: "regex", value: "^[0-9.]+$", errorMessage: "Must be a number", level: "error" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -283,13 +290,17 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select",
|
||||
options: [], // Will be populated from API
|
||||
},
|
||||
width: 150,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
label: "Description",
|
||||
key: "description",
|
||||
description: "Detailed product description",
|
||||
fieldType: { type: "input" },
|
||||
alternateMatches: ["details/description"],
|
||||
fieldType: {
|
||||
type: "input",
|
||||
multiline: true
|
||||
},
|
||||
width: 400,
|
||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||
},
|
||||
@@ -297,7 +308,10 @@ const BASE_IMPORT_FIELDS = [
|
||||
label: "Private Notes",
|
||||
key: "priv_notes",
|
||||
description: "Internal notes about the product",
|
||||
fieldType: { type: "input" },
|
||||
fieldType: {
|
||||
type: "input",
|
||||
multiline: true
|
||||
},
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
@@ -308,7 +322,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select",
|
||||
options: [], // Will be populated from API
|
||||
},
|
||||
width: 200,
|
||||
width: 350,
|
||||
validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
|
||||
},
|
||||
{
|
||||
@@ -319,7 +333,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select",
|
||||
options: [], // Will be populated from API
|
||||
},
|
||||
width: 200,
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
label: "Colors",
|
||||
@@ -329,7 +343,7 @@ const BASE_IMPORT_FIELDS = [
|
||||
type: "select",
|
||||
options: [], // Will be populated from API
|
||||
},
|
||||
width: 150,
|
||||
width: 180,
|
||||
},
|
||||
] as const;
|
||||
|
||||
Reference in New Issue
Block a user