3 Commits

12 changed files with 1345 additions and 131 deletions

View File

@@ -15,6 +15,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"openai": "^4.85.3",
"pm2": "^5.3.0", "pm2": "^5.3.0",
"ssh2": "^1.16.0", "ssh2": "^1.16.0",
"uuid": "^9.0.1" "uuid": "^9.0.1"
@@ -100,6 +101,27 @@
"node": ">=10" "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": { "node_modules/@pm2/io": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz",
@@ -238,6 +260,27 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "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": { "node_modules/@pm2/pm2-version-check": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@pm2/pm2-version-check/-/pm2-version-check-1.0.4.tgz", "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==", "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
"license": "MIT" "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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -298,6 +372,18 @@
"node": ">= 14" "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": { "node_modules/amp": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz",
@@ -401,6 +487,12 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT" "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": { "node_modules/aws-ssl-profiles": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "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==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT" "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": { "node_modules/commander": {
"version": "2.15.1", "version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
@@ -801,6 +905,15 @@
"node": ">= 14" "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": { "node_modules/denque": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -912,6 +1025,21 @@
"node": ">= 0.4" "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": { "node_modules/escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -991,6 +1119,15 @@
"node": ">= 0.6" "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": { "node_modules/eventemitter2": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", "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": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1295,6 +1466,21 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1395,6 +1581,15 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "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": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -1857,6 +2052,45 @@
"node": ">= 0.4.0" "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": { "node_modules/nodemon": {
"version": "3.1.9", "version": "3.1.9",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
@@ -1995,6 +2229,36 @@
"node": ">= 0.8" "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": { "node_modules/pac-proxy-agent": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz",
@@ -2996,6 +3260,12 @@
"nodetouch": "bin/nodetouch.js" "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": { "node_modules/tslib": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
@@ -3062,6 +3332,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3132,17 +3408,44 @@
"lodash": "^4.17.14" "lodash": "^4.17.14"
} }
}, },
"node_modules/ws": { "node_modules/web-streams-polyfill": {
"version": "7.5.10", "version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
"license": "MIT", "license": "MIT",
"engines": { "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": { "peerDependencies": {
"bufferutil": "^4.0.1", "bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2" "utf-8-validate": ">=5.0.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"bufferutil": { "bufferutil": {

View File

@@ -24,6 +24,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"openai": "^4.85.3",
"pm2": "^5.3.0", "pm2": "^5.3.0",
"ssh2": "^1.16.0", "ssh2": "^1.16.0",
"uuid": "^9.0.1" "uuid": "^9.0.1"

View 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.

View 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;

View File

@@ -18,6 +18,7 @@ const vendorsRouter = require('./routes/vendors');
const categoriesRouter = require('./routes/categories'); const categoriesRouter = require('./routes/categories');
const testConnectionRouter = require('./routes/test-connection'); const testConnectionRouter = require('./routes/test-connection');
const importRouter = require('./routes/import'); const importRouter = require('./routes/import');
const aiValidationRouter = require('./routes/ai-validation');
// Get the absolute path to the .env file // Get the absolute path to the .env file
const envPath = path.resolve(process.cwd(), '.env'); const envPath = path.resolve(process.cwd(), '.env');
@@ -93,6 +94,7 @@ async function startServer() {
app.use('/api/vendors', vendorsRouter); app.use('/api/vendors', vendorsRouter);
app.use('/api/categories', categoriesRouter); app.use('/api/categories', categoriesRouter);
app.use('/api/import', importRouter); app.use('/api/import', importRouter);
app.use('/api/ai-validation', aiValidationRouter);
app.use('/api', testConnectionRouter); app.use('/api', testConnectionRouter);
// Basic health check route // Basic health check route

View File

@@ -15,8 +15,9 @@ import { RequireAuth } from './components/auth/RequireAuth';
import Forecasting from "@/pages/Forecasting"; import Forecasting from "@/pages/Forecasting";
import { Vendors } from '@/pages/Vendors'; import { Vendors } from '@/pages/Vendors';
import { Categories } from '@/pages/Categories'; import { Categories } from '@/pages/Categories';
import { Import } from '@/pages/import/Import'; import { Import } from '@/pages/Import';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { AiValidationDebug } from "@/pages/AiValidationDebug"
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -71,6 +72,7 @@ function App() {
<Route path="/analytics" element={<Analytics />} /> <Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} /> <Route path="/settings" element={<Settings />} />
<Route path="/forecasting" element={<Forecasting />} /> <Route path="/forecasting" element={<Forecasting />} />
<Route path="/ai-validation/debug" element={<AiValidationDebug />} />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -35,7 +35,7 @@ export const UserTableColumn = <T extends string>(props: UserTableColumnProps<T>
</Button> </Button>
<div <div
className="vertical-text font-medium text-muted-foreground" 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} {header}
</div> </div>

View File

@@ -11,11 +11,12 @@ export const findMatch = <T extends string>(
fields: Fields<T>, fields: Fields<T>,
autoMapDistance: number, autoMapDistance: number,
): T | undefined => { ): T | undefined => {
const headerLower = header.toLowerCase()
const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => { const smallestValue = fields.reduce<AutoMatchAccumulator<T>>((acc, field) => {
const distance = Math.min( const distance = Math.min(
...[ ...[
lavenstein(field.key, header), lavenstein(field.key.toLowerCase(), headerLower),
...(field.alternateMatches?.map((alternate) => lavenstein(alternate, header)) || []), ...(field.alternateMatches?.map((alternate) => lavenstein(alternate.toLowerCase(), headerLower)) || []),
], ],
) )
return distance < acc.distance || acc.distance === undefined return distance < acc.distance || acc.distance === undefined

View File

@@ -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 { useRsi } from "../../hooks/useRsi"
import type { Meta } from "./types" import type { Meta } from "./types"
import { addErrorsAndRunHooks } from "./utils/dataMutations" import { addErrorsAndRunHooks } from "./utils/dataMutations"
import type { Data, Field, SelectOption, MultiInput } from "../../types" import type { Data, SelectOption } from "../../types"
import { Check, ChevronsUpDown, ArrowDown, AlertCircle } from "lucide-react" import { Check, ChevronsUpDown, ArrowDown, AlertCircle, Loader2 } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import {
Command, Command,
@@ -56,12 +56,15 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip"
import * as Select from "@radix-ui/react-select" import config from "@/config"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogTrigger, DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { ScrollArea } from "@/components/ui/scroll-area"
type Props<T extends string> = { type Props<T extends string> = {
initialData: (Data<T> & Meta)[] initialData: (Data<T> & Meta)[]
@@ -69,19 +72,80 @@ type Props<T extends string> = {
onBack?: () => void onBack?: () => void
} }
type CellProps = { type BaseFieldType = {
value: any, multiline?: boolean;
onChange: (value: any) => void, price?: boolean;
error?: { level: string, message: string },
field: Field<string>
} }
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 [isEditing, setIsEditing] = useState(false)
const [inputValue, setInputValue] = useState(value ?? "") const [inputValue, setInputValue] = useState(value ?? "")
const [searchQuery, setSearchQuery] = useState("") const [searchQuery, setSearchQuery] = useState("")
const [localValues, setLocalValues] = useState<string[]>([]) 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 // Update input value when external value changes and we're not editing
useEffect(() => { useEffect(() => {
if (!isEditing) { if (!isEditing) {
@@ -97,6 +161,12 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
} }
}, [value, field.fieldType.type]) }, [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) => { const validateRegex = (val: any) => {
// Handle non-string values // Handle non-string values
if (val === undefined || val === null) return undefined 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") const regexValidation = field.validations?.find(v => v.rule === "regex")
if (regexValidation) { 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) const regex = new RegExp(regexValidation.value, regexValidation.flags)
if (!regex.test(strVal)) { if (!regex.test(testValue)) {
return { level: regexValidation.level || "error", message: regexValidation.errorMessage } return { level: regexValidation.level || "error", message: regexValidation.errorMessage }
} }
} }
@@ -134,10 +210,10 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
const currentError = getValidationError() const currentError = getValidationError()
const getDisplayValue = (value: any, fieldType: Field<string>["fieldType"]) => { const getDisplayValue = (value: any, fieldType: Field<string>["fieldType"]) => {
if (fieldType.type === "select") { if (fieldType.type === "select" || fieldType.type === "multi-select") {
return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value if (fieldType.type === "select") {
} return fieldType.options.find((opt: SelectOption) => opt.value === value)?.label || value
if (fieldType.type === "multi-select") { }
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(v => fieldType.options.find((opt: SelectOption) => opt.value === v)?.label || v).join(", ") 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 return true
} }
// Handle price fields
if (isPriceField(field.fieldType)) {
newValue = formatPrice(newValue)
}
// Always commit the value // Always commit the value
onChange(newValue) onChange(newValue)
@@ -172,27 +253,6 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
setIsEditing(false) 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) { if (isEditing) {
switch (field.fieldType.type) { switch (field.fieldType.type) {
case "select": case "select":
@@ -353,8 +413,59 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
{currentError && <ValidationIcon error={currentError} />} {currentError && <ValidationIcon error={currentError} />}
</div> </div>
) )
case "input":
case "multi-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 ( return (
<div className="relative" id={`cell-${field.key}`}> <div className="relative" id={`cell-${field.key}`}>
<Input <Input
@@ -365,9 +476,9 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
onBlur={handleBlur} onBlur={handleBlur}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
if (field.fieldType.type === "multi-input") { if (isMultiInputType(field.fieldType)) {
const separator = (field.fieldType as MultiInput).separator || "," const separator = getMultiInputSeparator(field.fieldType);
const values = inputValue.split(separator).map((v: string) => v.trim()).filter(Boolean) const values = inputValue.split(separator).map((v: string) => v.trim()).filter(Boolean);
if (validateAndCommit(values.join(separator))) { if (validateAndCommit(values.join(separator))) {
setIsEditing(false) setIsEditing(false)
} }
@@ -381,9 +492,47 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
currentError ? "border-destructive" : "" currentError ? "border-destructive" : ""
)} )}
autoFocus autoFocus
placeholder={field.fieldType.type === "multi-input" placeholder={
? `Enter values separated by ${(field.fieldType as MultiInput).separator || ","}` isMultiInputType(field.fieldType)
: undefined} ? `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} />} {currentError && <ValidationIcon error={currentError} />}
</div> </div>
@@ -397,38 +546,79 @@ const EditableCell = ({ value, onChange, error, field }: CellProps) => {
id={`cell-${field.key}`} id={`cell-${field.key}`}
onClick={(e) => { onClick={(e) => {
if (field.fieldType.type !== "checkbox" && !field.disabled) { if (field.fieldType.type !== "checkbox" && !field.disabled) {
e.stopPropagation() // Prevent event bubbling e.stopPropagation()
setIsEditing(true) setIsEditing(true)
setInputValue(Array.isArray(value) ? value.join(", ") : value ?? "") setInputValue(Array.isArray(value) ? value.join(", ") : value ?? "")
} }
}} }}
className={cn( className={cn(
"relative min-h-[36px] cursor-text p-2 rounded-md border bg-background", "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", currentError ? "border-destructive" : "border-input",
field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between", field.fieldType.type === "checkbox" ? "flex items-center" : "flex items-center justify-between",
field.disabled && "opacity-50 cursor-not-allowed bg-muted" field.disabled && "opacity-50 cursor-not-allowed bg-muted"
)} )}
> >
<div className={cn("flex-1 overflow-hidden text-ellipsis", !value && "text-muted-foreground")}> {((field.fieldType.type === "input" || field.fieldType.type === "multi-input") && field.fieldType.multiline) ? (
{value ? getDisplayValue(value, field.fieldType) : ""} <div className={cn(
</div> "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") && ( {(field.fieldType.type === "select" || field.fieldType.type === "multi-select") && (
<ChevronsUpDown className={cn("h-4 w-4 shrink-0", field.disabled ? "opacity-30" : "opacity-50")} /> <ChevronsUpDown className={cn("h-4 w-4 shrink-0", field.disabled ? "opacity-30" : "opacity-50")} />
)} )}
{currentError && <ValidationIcon error={currentError} />} {currentError && <ValidationIcon error={currentError} />}
</div> </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 function getMultiInputSeparator(fieldType: FieldType): string {
const ColumnHeader = <T extends 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, field,
data, data,
onCopyDown onCopyDown
}: { }: {
field: Field<T>, field: Field<string>
data: (Data<T> & Meta)[], data: (Data<string> & Meta)[]
onCopyDown: (key: T) => void onCopyDown: (key: string) => void
}) => { }) => {
return ( return (
<div className="flex items-center gap-2"> <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" className="h-4 w-4 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onCopyDown(field.key as T) onCopyDown(field.key)
}} }}
title="Copy first row's value down" title="Copy first row's value down"
> >
@@ -451,10 +641,10 @@ const ColumnHeader = <T extends string>({
)} )}
</div> </div>
) )
} })
// Add this component for the copy down confirmation dialog // Add this component for the copy down confirmation dialog
const CopyDownDialog = ({ const CopyDownDialog = memo(({
isOpen, isOpen,
onClose, onClose,
onConfirm, onConfirm,
@@ -493,7 +683,7 @@ const CopyDownDialog = ({
</AlertDialogPortal> </AlertDialogPortal>
</AlertDialog> </AlertDialog>
) )
} })
// Add type utilities at the top level // Add type utilities at the top level
type DeepReadonlyField<T extends string> = { type DeepReadonlyField<T extends string> = {
@@ -523,6 +713,16 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
const [showSubmitAlert, setShowSubmitAlert] = useState(false) const [showSubmitAlert, setShowSubmitAlert] = useState(false)
const [isSubmitting, setSubmitting] = useState(false) const [isSubmitting, setSubmitting] = useState(false)
const [copyDownField, setCopyDownField] = useState<{key: T, label: string} | null>(null) 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 // Memoize filtered data to prevent recalculation on every render
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
@@ -596,18 +796,22 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
{ {
id: "select", id: "select",
header: ({ table }) => ( header: ({ table }) => (
<Checkbox <div className="flex h-full items-center justify-center">
checked={table.getIsAllPageRowsSelected()} <Checkbox
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} checked={table.getIsAllPageRowsSelected()}
aria-label="Select all" onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/> aria-label="Select all"
/>
</div>
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<Checkbox <div className="flex items-start justify-center pt-2">
checked={row.getIsSelected()} <Checkbox
onCheckedChange={(value) => row.toggleSelected(!!value)} checked={row.getIsSelected()}
aria-label="Select row" onCheckedChange={(value) => row.toggleSelected(!!value)}
/> aria-label="Select row"
/>
</div>
), ),
enableSorting: false, enableSorting: false,
enableHiding: false, enableHiding: false,
@@ -617,10 +821,10 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
accessorKey: field.key, accessorKey: field.key,
header: () => ( header: () => (
<div className="group"> <div className="group">
<ColumnHeader <ColumnHeader
field={field as Field<T>} field={field as Field<string>}
data={data} data={data}
onCopyDown={(key) => copyValueDown(key, field.label)} onCopyDown={(key: string) => copyValueDown(key as T, field.label)}
/> />
</div> </div>
), ),
@@ -659,14 +863,14 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
}) })
const deleteSelectedRows = () => { const deleteSelectedRows = useCallback(() => {
if (Object.keys(rowSelection).length) { if (Object.keys(rowSelection).length) {
const selectedRows = Object.keys(rowSelection).map(Number) const selectedRows = Object.keys(rowSelection).map(Number)
const newData = data.filter((_, index) => !selectedRows.includes(index)) const newData = data.filter((_, index) => !selectedRows.includes(index))
updateData(newData) updateData(newData)
setRowSelection({}) setRowSelection({})
} }
} }, [rowSelection, data, updateData]);
const normalizeValue = useCallback((value: any, field: DeepReadonlyField<T>) => { const normalizeValue = useCallback((value: any, field: DeepReadonlyField<T>) => {
if (field.fieldType.type === "checkbox") { if (field.fieldType.type === "checkbox") {
@@ -694,7 +898,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
return value return value
}, []) }, [])
const submitData = async () => { const submitData = useCallback(async () => {
const calculatedData = data.reduce( const calculatedData = data.reduce(
(acc, value) => { (acc, value) => {
const { __index, __errors, ...values } = value const { __index, __errors, ...values } = value
@@ -743,20 +947,97 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
} else { } else {
onClose() onClose()
} }
} }, [data, fields, file, onClose, onSubmit, normalizeValue, toast, translations]);
const onContinue = () => {
const onContinue = useCallback(() => {
const invalidData = data.find((value) => { const invalidData = data.find((value) => {
if (value?.__errors) { 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) { if (!invalidData) {
submitData() submitData();
} else { } 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 ( return (
<div className="flex h-[calc(100vh-9.5rem)] flex-col"> <div className="flex h-[calc(100vh-9.5rem)] flex-col">
@@ -793,6 +1074,47 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
</AlertDialogContent> </AlertDialogContent>
</AlertDialogPortal> </AlertDialogPortal>
</AlertDialog> </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="flex-1 overflow-hidden">
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="px-8 pt-6"> <div className="px-8 pt-6">
@@ -808,6 +1130,17 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
> >
{translations.validationStep.discardButtonTitle} {translations.validationStep.discardButtonTitle}
</Button> </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"> <div className="flex items-center gap-2">
<Switch <Switch
checked={filterByErrors} checked={filterByErrors}
@@ -857,7 +1190,7 @@ export const ValidationStep = <T extends string>({ initialData, file, onBack }:
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
key={cell.id} key={cell.id}
className="p-2" className="p-2 align-top"
style={{ style={{
width: cell.column.getSize(), width: cell.column.getSize(),
minWidth: cell.column.getSize(), minWidth: cell.column.getSize(),

View File

@@ -70,7 +70,7 @@ export type Field<T extends string = string> = {
// Validations used for field entries // Validations used for field entries
validations?: Validation[] validations?: Validation[]
// Field entry component // Field entry component
fieldType: Checkbox | Select | Input | MultiInput | MultiSelect fieldType: FieldType
// UI-facing values shown to user as field examples pre-upload phase // UI-facing values shown to user as field examples pre-upload phase
example?: string example?: string
width?: number width?: number
@@ -78,17 +78,22 @@ export type Field<T extends string = string> = {
onChange?: (value: string) => void onChange?: (value: string) => void
} }
export type Checkbox = { export type FieldType =
type: "checkbox" | {
// Alternate values to be treated as booleans, e.g. {yes: true, no: false} type: "input" | "multi-input";
booleanMatches?: { [key: string]: boolean } multiline?: boolean;
} price?: boolean;
separator?: string;
export type Select = { }
type: "select" | {
// Options displayed in Select component type: "select" | "multi-select";
options: SelectOption[] options: readonly SelectOption[];
} separator?: string;
}
| {
type: "checkbox";
booleanMatches?: { readonly [key: string]: boolean };
};
export type SelectOption = { export type SelectOption = {
// UI-facing option label // UI-facing option label

View 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>
)
}

View File

@@ -20,16 +20,16 @@ const BASE_IMPORT_FIELDS = [
type: "select" as const, type: "select" as const,
options: [], // Will be populated from API options: [], // Will be populated from API
}, },
width: 200, width: 220,
validations: [{ rule: "required" as const, errorMessage: "Required", level: "error" as ErrorLevel }], validations: [{ rule: "required" as const, errorMessage: "Required", level: "error" as ErrorLevel }],
}, },
{ {
label: "UPC", label: "UPC",
key: "upc", key: "upc",
description: "Universal Product Code/Barcode", description: "Universal Product Code/Barcode",
alternateMatches: ["barcode", "bar code", "JAN", "EAN"], alternateMatches: ["barcode", "bar code", "jan", "ean"],
fieldType: { type: "input" }, fieldType: { type: "input" },
width: 150, width: 140,
validations: [ validations: [
{ rule: "required", errorMessage: "Required", level: "error" }, { rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" }, { rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -42,7 +42,7 @@ const BASE_IMPORT_FIELDS = [
description: "Supplier's product identifier", description: "Supplier's product identifier",
alternateMatches: ["sku", "item#", "mfg item #", "item"], alternateMatches: ["sku", "item#", "mfg item #", "item"],
fieldType: { type: "input" }, fieldType: { type: "input" },
width: 120, width: 180,
validations: [ validations: [
{ rule: "required", errorMessage: "Required", level: "error" }, { rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" }, { rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -53,7 +53,7 @@ const BASE_IMPORT_FIELDS = [
key: "notions_no", key: "notions_no",
description: "Internal notions number", description: "Internal notions number",
fieldType: { type: "input" }, fieldType: { type: "input" },
width: 120, width: 110,
validations: [ validations: [
{ rule: "required", errorMessage: "Required", level: "error" }, { rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" }, { rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -66,7 +66,7 @@ const BASE_IMPORT_FIELDS = [
description: "Product name/title", description: "Product name/title",
alternateMatches: ["sku description"], alternateMatches: ["sku description"],
fieldType: { type: "input" }, fieldType: { type: "input" },
width: 300, width: 500,
validations: [ validations: [
{ rule: "required", errorMessage: "Required", level: "error" }, { rule: "required", errorMessage: "Required", level: "error" },
{ rule: "unique", errorMessage: "Must be unique", level: "error" }, { rule: "unique", errorMessage: "Must be unique", level: "error" },
@@ -88,14 +88,17 @@ const BASE_IMPORT_FIELDS = [
key: "image_url", key: "image_url",
description: "Product image URL(s)", description: "Product image URL(s)",
fieldType: { type: "multi-input" }, fieldType: { type: "multi-input" },
width: 250, width: 300,
}, },
{ {
label: "MSRP", label: "MSRP",
key: "msrp", key: "msrp",
description: "Manufacturer's Suggested Retail Price", description: "Manufacturer's Suggested Retail Price",
alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. Retail"], alternateMatches: ["retail", "retail price", "sugg retail", "price", "sugg. retail","default price"],
fieldType: { type: "input" }, fieldType: {
type: "input",
price: true
},
width: 100, width: 100,
validations: [ validations: [
{ rule: "required", errorMessage: "Required", level: "error" }, { rule: "required", errorMessage: "Required", level: "error" },
@@ -108,7 +111,7 @@ const BASE_IMPORT_FIELDS = [
description: "Quantity of items per individual unit", description: "Quantity of items per individual unit",
alternateMatches: ["inner pack", "inner", "min qty", "unit qty", "min. order qty"], alternateMatches: ["inner pack", "inner", "min qty", "unit qty", "min. order qty"],
fieldType: { type: "input" }, fieldType: { type: "input" },
width: 100, width: 90,
validations: [ validations: [
{ rule: "required", errorMessage: "Required", level: "error" }, { rule: "required", errorMessage: "Required", level: "error" },
{ rule: "regex", value: "^[0-9]+$", errorMessage: "Must be a number", 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", key: "cost_each",
description: "Wholesale cost per unit", description: "Wholesale cost per unit",
alternateMatches: ["wholesale", "wholesale price"], alternateMatches: ["wholesale", "wholesale price"],
fieldType: { type: "input" }, fieldType: {
type: "input",
price: true
},
width: 100, width: 100,
validations: [ validations: [
{ rule: "required", errorMessage: "Required", level: "error" }, { rule: "required", errorMessage: "Required", level: "error" },
@@ -130,9 +136,9 @@ const BASE_IMPORT_FIELDS = [
label: "Case Pack", label: "Case Pack",
key: "case_qty", key: "case_qty",
description: "Number of units per case", description: "Number of units per case",
alternateMatches: ["mc qty"], alternateMatches: ["mc qty","case qty","case pack"],
fieldType: { type: "input" }, fieldType: { type: "input" },
width: 100, width: 50,
validations: [ 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" },
], ],
@@ -142,10 +148,10 @@ const BASE_IMPORT_FIELDS = [
key: "tax_cat", key: "tax_cat",
description: "Product tax category", description: "Product tax category",
fieldType: { fieldType: {
type: "multi-select", type: "select",
options: [], // Will be populated from API options: [], // Will be populated from API
}, },
width: 150, width: 180,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }], validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
}, },
{ {
@@ -167,7 +173,7 @@ const BASE_IMPORT_FIELDS = [
type: "select", type: "select",
options: [], // Will be populated dynamically based on company selection options: [], // Will be populated dynamically based on company selection
}, },
width: 150, width: 180,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }], validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
}, },
{ {
@@ -178,7 +184,7 @@ const BASE_IMPORT_FIELDS = [
type: "select", type: "select",
options: [], // Will be populated dynamically based on line selection options: [], // Will be populated dynamically based on line selection
}, },
width: 150, width: 180,
}, },
{ {
label: "Artist", label: "Artist",
@@ -188,7 +194,7 @@ const BASE_IMPORT_FIELDS = [
type: "select", type: "select",
options: [], // Will be populated from API options: [], // Will be populated from API
}, },
width: 200, width: 180,
}, },
{ {
label: "ETA Date", label: "ETA Date",
@@ -202,6 +208,7 @@ const BASE_IMPORT_FIELDS = [
label: "Weight", label: "Weight",
key: "weight", key: "weight",
description: "Product weight (in lbs)", description: "Product weight (in lbs)",
alternateMatches: ["weight (lbs.)"],
fieldType: { type: "input" }, fieldType: { type: "input" },
width: 100, width: 100,
validations: [ validations: [
@@ -250,7 +257,7 @@ const BASE_IMPORT_FIELDS = [
type: "select", type: "select",
options: [], // Will be populated from API options: [], // Will be populated from API
}, },
width: 150, width: 190,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }], validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
}, },
{ {
@@ -270,9 +277,9 @@ const BASE_IMPORT_FIELDS = [
description: "Harmonized Tariff Schedule code", description: "Harmonized Tariff Schedule code",
alternateMatches: ["taric"], alternateMatches: ["taric"],
fieldType: { type: "input" }, fieldType: { type: "input" },
width: 120, width: 130,
validations: [ 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", type: "select",
options: [], // Will be populated from API options: [], // Will be populated from API
}, },
width: 150, width: 180,
}, },
{ {
label: "Description", label: "Description",
key: "description", key: "description",
description: "Detailed product description", description: "Detailed product description",
fieldType: { type: "input" }, alternateMatches: ["details/description"],
fieldType: {
type: "input",
multiline: true
},
width: 400, width: 400,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }], validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
}, },
@@ -297,7 +308,10 @@ const BASE_IMPORT_FIELDS = [
label: "Private Notes", label: "Private Notes",
key: "priv_notes", key: "priv_notes",
description: "Internal notes about the product", description: "Internal notes about the product",
fieldType: { type: "input" }, fieldType: {
type: "input",
multiline: true
},
width: 300, width: 300,
}, },
{ {
@@ -308,7 +322,7 @@ const BASE_IMPORT_FIELDS = [
type: "select", type: "select",
options: [], // Will be populated from API options: [], // Will be populated from API
}, },
width: 200, width: 350,
validations: [{ rule: "required", errorMessage: "Required", level: "error" }], validations: [{ rule: "required", errorMessage: "Required", level: "error" }],
}, },
{ {
@@ -319,7 +333,7 @@ const BASE_IMPORT_FIELDS = [
type: "select", type: "select",
options: [], // Will be populated from API options: [], // Will be populated from API
}, },
width: 200, width: 300,
}, },
{ {
label: "Colors", label: "Colors",
@@ -329,7 +343,7 @@ const BASE_IMPORT_FIELDS = [
type: "select", type: "select",
options: [], // Will be populated from API options: [], // Will be populated from API
}, },
width: 150, width: 180,
}, },
] as const; ] as const;